import org.sonar.plugins.core.widgets.reviews.MyReviewsWidget;
import org.sonar.plugins.core.widgets.reviews.PlannedReviewsWidget;
import org.sonar.plugins.core.widgets.reviews.ProjectReviewsWidget;
+import org.sonar.plugins.core.widgets.reviews.ReviewsMetricsWidget;
import org.sonar.plugins.core.widgets.reviews.ReviewsPerDeveloperWidget;
import org.sonar.plugins.core.widgets.reviews.UnplannedReviewsWidget;
extensions.add(PlannedReviewsWidget.class);
extensions.add(UnplannedReviewsWidget.class);
extensions.add(ActionPlansWidget.class);
+ extensions.add(ReviewsMetricsWidget.class);
// dashboards
extensions.add(DefaultDashboard.class);
extensions.add(UpdateReviewsDecorator.class);
extensions.add(ViolationSeverityUpdater.class);
extensions.add(IndexProjectPostJob.class);
+ extensions.add(ReviewsMeasuresDecorator.class);
// time machine
extensions.add(TendencyDecorator.class);
public void decorate(Resource resource, DecoratorContext context) {
if (resource.getId() != null) {
- ReviewQuery query = ReviewQuery.create().setManualViolation(true).setResourceId(resource.getId()).setStatus(ReviewDto.STATUS_OPEN);
+ ReviewQuery query = ReviewQuery.create().setManualViolation(true).setResourceId(resource.getId()).addStatus(ReviewDto.STATUS_OPEN);
List<ReviewDto> reviewDtos = reviewDao.selectByQuery(query);
for (ReviewDto reviewDto : reviewDtos) {
if (reviewDto.getRuleId() == null) {
--- /dev/null
+/*
+ * Sonar, open source software quality management tool.
+ * Copyright (C) 2008-2012 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * Sonar is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * Sonar is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Sonar; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02
+ */
+package org.sonar.plugins.core.sensors;
+
+import org.sonar.api.batch.Decorator;
+import org.sonar.api.batch.DecoratorContext;
+import org.sonar.api.batch.DependsUpon;
+import org.sonar.api.measures.CoreMetrics;
+import org.sonar.api.resources.Project;
+import org.sonar.api.resources.Resource;
+import org.sonar.api.resources.ResourceUtils;
+import org.sonar.core.review.ReviewDao;
+import org.sonar.core.review.ReviewDto;
+import org.sonar.core.review.ReviewQuery;
+import org.sonar.plugins.core.timemachine.ViolationTrackingDecorator;
+
+/**
+ * Decorator that creates measures related to reviews.
+ */
+@DependsUpon(CloseReviewsDecorator.REVIEW_LIFECYCLE_BARRIER)
+public class ReviewsMeasuresDecorator implements Decorator {
+
+ private ReviewDao reviewDao;
+
+ public ReviewsMeasuresDecorator(ReviewDao reviewDao) {
+ this.reviewDao = reviewDao;
+ }
+
+ public boolean shouldExecuteOnProject(Project project) {
+ return project.isLatestAnalysis();
+ }
+
+ @SuppressWarnings("rawtypes")
+ @DependsUpon
+ public Class dependsUponViolationTracking() {
+ // permanent ids of violations have been updated, so we can link them with reviews
+ return ViolationTrackingDecorator.class;
+ }
+
+ @SuppressWarnings({"rawtypes"})
+ public void decorate(Resource resource, DecoratorContext context) {
+ if (!ResourceUtils.isFile(resource)) {
+ return;
+ }
+
+ // Open reviews
+ ReviewQuery openReviewQuery = ReviewQuery.create().setResourceId(resource.getId()).addStatus(ReviewDto.STATUS_OPEN)
+ .addStatus(ReviewDto.STATUS_REOPENED);
+ Integer openReviewsCount = reviewDao.countByQuery(openReviewQuery);
+ context.saveMeasure(CoreMetrics.ACTIVE_REVIEWS, openReviewsCount.doubleValue());
+
+ // Unassigned reviews
+ ReviewQuery unassignedReviewQuery = ReviewQuery.copy(openReviewQuery).setNoAssignee();
+ Integer unassignedReviewsCount = reviewDao.countByQuery(unassignedReviewQuery);
+ context.saveMeasure(CoreMetrics.UNASSIGNED_REVIEWS, unassignedReviewsCount.doubleValue());
+
+ // Unplanned reviews
+ ReviewQuery plannedReviewQuery = ReviewQuery.copy(openReviewQuery).setPlanned();
+ int plannedReviewsCount = reviewDao.countByQuery(plannedReviewQuery);
+ context.saveMeasure(CoreMetrics.UNPLANNED_REVIEWS, (double) (openReviewsCount - plannedReviewsCount));
+
+ // False positive reviews
+ ReviewQuery falsePositiveReviewQuery = ReviewQuery.create().setResourceId(resource.getId())
+ .addResolution(ReviewDto.RESOLUTION_FALSE_POSITIVE);
+ Integer falsePositiveReviewsCount = reviewDao.countByQuery(falsePositiveReviewQuery);
+ context.saveMeasure(CoreMetrics.FALSE_POSITIVE_REVIEWS, falsePositiveReviewsCount.doubleValue());
+
+ // Violations without a review
+ int violationsCount = context.getViolations().size();
+ context.saveMeasure(CoreMetrics.VIOLATIONS_WITHOUT_REVIEW, (double) (violationsCount - openReviewsCount - falsePositiveReviewsCount));
+ }
+
+}
--- /dev/null
+/*
+ * Sonar, open source software quality management tool.
+ * Copyright (C) 2008-2012 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * Sonar is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * Sonar is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Sonar; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02
+ */
+package org.sonar.plugins.core.widgets.reviews;
+
+import org.sonar.api.web.AbstractRubyTemplate;
+import org.sonar.api.web.RubyRailsWidget;
+import org.sonar.api.web.WidgetCategory;
+
+@WidgetCategory({ "Reviews" })
+public class ReviewsMetricsWidget extends AbstractRubyTemplate implements RubyRailsWidget {
+ public String getId() {
+ return "reviews_metrics";
+ }
+
+ public String getTitle() {
+ return "Reviews metrics";
+ }
+
+ @Override
+ protected String getTemplatePath() {
+ return "/org/sonar/plugins/core/widgets/reviews/reviews_metrics.html.erb";
+ }
+}
--- /dev/null
+<%
+ active_reviews=measure('active_reviews')
+ unassigned_reviews=measure('unassigned_reviews')
+ unplanned_reviews=measure('unplanned_reviews')
+ false_positive_reviews=measure('false_positive_reviews')
+ violations_without_review=measure('violations_without_review')
+%>
+<table width="100%">
+ <tr>
+ <td valign="top" width="48%" nowrap>
+ <div class="dashbox">
+ <h3><%= message('widget.reviews_metrics.active_reviews') -%></h3>
+ <% if active_reviews %>
+ <p>
+ <span class="big"><%= format_measure(active_reviews, :suffix => '', :url => url_for_drilldown(active_reviews)) -%></span>
+ <%= dashboard_configuration.selected_period? ? format_variation(active_reviews) : trend_icon(active_reviews) -%>
+ </p>
+ <p>
+ <%= format_measure(unassigned_reviews, :suffix => message('widget.reviews_metrics.unassigned.suffix'), :url => url_for_drilldown(unassigned_reviews)) -%>
+ <%= dashboard_configuration.selected_period? ? format_variation(unassigned_reviews) : trend_icon(unassigned_reviews) -%>
+ </p>
+ <p>
+ <%= format_measure(unplanned_reviews, :suffix => message('widget.reviews_metrics.unplanned.suffix'), :url => url_for_drilldown(unplanned_reviews)) -%>
+ <%= dashboard_configuration.selected_period? ? format_variation(unplanned_reviews) : trend_icon(unplanned_reviews) -%>
+ </p>
+ <p>
+ <%= format_measure(false_positive_reviews, :suffix => message('widget.reviews_metrics.false_positives.suffix'), :url => url_for_drilldown(false_positive_reviews)) -%>
+ <%= dashboard_configuration.selected_period? ? format_variation(false_positive_reviews) : trend_icon(false_positive_reviews) -%>
+ </p>
+ <% else %>
+ <span class="empty_widget"><%= message('widget.reviews_metrics.no_data') -%></span>
+ <% end %>
+ </div>
+ </td>
+ <td width="10"> </td>
+ <td valign="top">
+ <div class="dashbox">
+ <h3><%= message('widget.reviews_metrics.unreviewed_violations') -%></h3>
+ <% if violations_without_review %>
+ <p>
+ <span class="big"><%= format_measure(violations_without_review, :suffix => '', :url => url_for_drilldown(violations_without_review)) -%></span>
+ <%= dashboard_configuration.selected_period? ? format_variation(violations_without_review) : trend_icon(violations_without_review) -%>
+ </p>
+ <% else %>
+ <span class="empty_widget"><%= message('widget.reviews_metrics.no_data') -%></span>
+ <% end %>
+ </div>
+ </td>
+ </tr>
+</table>
\ No newline at end of file
--- /dev/null
+/*
+ * Sonar, open source software quality management tool.
+ * Copyright (C) 2008-2012 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * Sonar is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * Sonar is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Sonar; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02
+ */
+package org.sonar.plugins.core.sensors;
+
+import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyDouble;
+import static org.mockito.Matchers.argThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.List;
+
+import org.hamcrest.BaseMatcher;
+import org.hamcrest.Description;
+import org.junit.Test;
+import org.sonar.api.batch.DecoratorContext;
+import org.sonar.api.measures.CoreMetrics;
+import org.sonar.api.measures.Metric;
+import org.sonar.api.resources.File;
+import org.sonar.api.resources.Project;
+import org.sonar.api.resources.Resource;
+import org.sonar.api.rules.Violation;
+import org.sonar.core.review.ReviewDao;
+import org.sonar.core.review.ReviewDto;
+import org.sonar.core.review.ReviewQuery;
+import org.sonar.plugins.core.timemachine.ViolationTrackingDecorator;
+
+import com.google.common.collect.Lists;
+
+public class ReviewsMeasuresDecoratorTest {
+
+ @Test
+ public void testDependsUponViolationTracking() throws Exception {
+ ReviewsMeasuresDecorator decorator = new ReviewsMeasuresDecorator(null);
+ assertEquals(decorator.dependsUponViolationTracking(), ViolationTrackingDecorator.class);
+ }
+
+ @Test
+ public void shouldExecuteOnProject() throws Exception {
+ ReviewsMeasuresDecorator decorator = new ReviewsMeasuresDecorator(null);
+ Project project = new Project("foo");
+ project.setLatestAnalysis(true);
+ assertThat(decorator.shouldExecuteOnProject(project), is(true));
+ }
+
+ @Test
+ public void shouldDecorateOnlyFiles() throws Exception {
+ ReviewsMeasuresDecorator decorator = new ReviewsMeasuresDecorator(null);
+ DecoratorContext context = mock(DecoratorContext.class);
+ Resource<?> resource = new Project("Foo");
+ decorator.decorate(resource, context);
+ verify(context, never()).saveMeasure(any(Metric.class), anyDouble());
+ }
+
+ @Test
+ public void shouldComputeReviewMetrics() throws Exception {
+ ReviewDao reviewDao = mock(ReviewDao.class);
+ when(reviewDao.countByQuery(argThat(openReviewQueryMatcher()))).thenReturn(10);
+ when(reviewDao.countByQuery(argThat(unassignedReviewQueryMatcher()))).thenReturn(2);
+ when(reviewDao.countByQuery(argThat(plannedReviewQueryMatcher()))).thenReturn(3);
+ when(reviewDao.countByQuery(argThat(falsePositiveReviewQueryMatcher()))).thenReturn(4);
+
+ ReviewsMeasuresDecorator decorator = new ReviewsMeasuresDecorator(reviewDao);
+ Resource<?> resource = new File("foo").setId(1);
+ DecoratorContext context = mock(DecoratorContext.class);
+ List<Violation> violations = mock(List.class);
+ when(violations.size()).thenReturn(35);
+ when(context.getViolations()).thenReturn(violations);
+ decorator.decorate(resource, context);
+
+ verify(context).saveMeasure(CoreMetrics.ACTIVE_REVIEWS, 10d);
+ verify(context).saveMeasure(CoreMetrics.UNASSIGNED_REVIEWS, 2d);
+ verify(context).saveMeasure(CoreMetrics.UNPLANNED_REVIEWS, 7d);
+ verify(context).saveMeasure(CoreMetrics.FALSE_POSITIVE_REVIEWS, 4d);
+ verify(context).saveMeasure(CoreMetrics.VIOLATIONS_WITHOUT_REVIEW, 21d);
+ }
+
+ private BaseMatcher<ReviewQuery> openReviewQueryMatcher() {
+ return new BaseMatcher<ReviewQuery>() {
+ public boolean matches(Object o) {
+ ReviewQuery query = (ReviewQuery) o;
+ if (query == null) {
+ return false;
+ }
+ return Lists.newArrayList(ReviewDto.STATUS_OPEN, ReviewDto.STATUS_REOPENED).equals(query.getStatuses())
+ && query.getNoAssignee() == null && query.getPlanned() == null;
+ }
+
+ public void describeTo(Description description) {
+ }
+ };
+ }
+
+ private BaseMatcher<ReviewQuery> unassignedReviewQueryMatcher() {
+ return new BaseMatcher<ReviewQuery>() {
+ public boolean matches(Object o) {
+ ReviewQuery query = (ReviewQuery) o;
+ if (query == null) {
+ return false;
+ }
+ return Lists.newArrayList(ReviewDto.STATUS_OPEN, ReviewDto.STATUS_REOPENED).equals(query.getStatuses())
+ && query.getNoAssignee() == Boolean.TRUE;
+ }
+
+ public void describeTo(Description description) {
+ }
+ };
+ }
+
+ private BaseMatcher<ReviewQuery> plannedReviewQueryMatcher() {
+ return new BaseMatcher<ReviewQuery>() {
+ public boolean matches(Object o) {
+ ReviewQuery query = (ReviewQuery) o;
+ if (query == null) {
+ return false;
+ }
+ return Lists.newArrayList(ReviewDto.STATUS_OPEN, ReviewDto.STATUS_REOPENED).equals(query.getStatuses())
+ && query.getPlanned() == Boolean.TRUE;
+ }
+
+ public void describeTo(Description description) {
+ }
+ };
+ }
+
+ private BaseMatcher<ReviewQuery> falsePositiveReviewQueryMatcher() {
+ return new BaseMatcher<ReviewQuery>() {
+ public boolean matches(Object o) {
+ ReviewQuery query = (ReviewQuery) o;
+ if (query == null) {
+ return false;
+ }
+ return Lists.newArrayList(ReviewDto.RESOLUTION_FALSE_POSITIVE).equals(query.getResolutions());
+ }
+
+ public void describeTo(Description description) {
+ }
+ };
+ }
+}
--- /dev/null
+<dataset>
+ <reviews
+ id="1"
+ status="OPEN"
+ rule_failure_permanent_id="1"
+ resource_id="1"
+ title="message OLD"
+ resource_line="0"
+ resolution="[null]"
+ created_at="[null]"
+ updated_at="[null]"
+ project_id="[null]"
+ severity="[null]"
+ user_id="[null]"
+ rule_id="[null]"
+ manual_violation="false"
+ manual_severity="false"/>
+ <reviews
+ id="2"
+ status="OPEN"
+ rule_failure_permanent_id="2"
+ resource_id="1"
+ title="message 2"
+ resource_line="2"
+ rule_id="[null]"
+ manual_violation="false"
+ manual_severity="false"/>
+ <reviews
+ id="3"
+ status="OPEN"
+ rule_failure_permanent_id="3"
+ resource_id="1"
+ title="message 3"
+ resource_line="0"
+ rule_id="[null]"
+ manual_violation="false"
+ manual_severity="false"/>
+ <reviews
+ id="4"
+ status="OPEN"
+ rule_failure_permanent_id="4"
+ resource_id="1"
+ title="message OLD"
+ resource_line="4"
+ rule_id="[null]"
+ manual_violation="false"
+ manual_severity="false"/>
+ <reviews
+ id="5"
+ status="OPEN"
+ rule_failure_permanent_id="5"
+ resource_id="1"
+ title="message 5"
+ resource_line="[null]"
+ rule_id="[null]"
+ manual_violation="false"
+ manual_severity="false"/>
+ <reviews
+ id="6"
+ status="OPEN"
+ rule_failure_permanent_id="6"
+ resource_id="1"
+ title="message OLD"
+ resource_line="[null]"
+ rule_id="[null]"
+ manual_violation="false"
+ manual_severity="false"/>
+
+</dataset>
\ No newline at end of file
widget.unplanned_reviews.name=Unplanned reviews
widget.unplanned_reviews.description=Shows all the reviews of the project that are not planned yet in an action plan
+widget.reviews_metrics.name=Reviews metrics
+widget.reviews_metrics.description=Reports metrics about reviews
+widget.reviews_metrics.no_data=No data
+widget.reviews_metrics.active_reviews=Active reviews
+widget.reviews_metrics.unassigned.suffix=\ unassigned
+widget.reviews_metrics.unplanned.suffix=\ unplanned
+widget.reviews_metrics.false_positives.suffix=\ false-positives
+widget.reviews_metrics.unreviewed_violations=Unreviewed violations
+
#------------------------------------------------------------------------------
metric.team_size.name=Team size
metric.team_size.description=Size of the project team
+
+#--------------------------------------------------------------------------------------------------------------------
+#
+# REVIEWS METRICS
+#
+#--------------------------------------------------------------------------------------------------------------------
+metric.violations_without_review.name=Unreviewed violations
+metric.violations_without_review.description=Violations that have not been reviewed yet
+
+metric.false_positive_reviews.name=False-positive reviews
+metric.false_positive_reviews.description=Active false-positive reviews
+
+metric.active_reviews.name=Active reviews
+metric.active_reviews.description=Active open and reopened reviews
+
+metric.unassigned_reviews.name=Unassigned reviews
+metric.unassigned_reviews.description=Active unassigned reviews
+
+metric.unplanned_reviews.name=Unplanned reviews
+metric.unplanned_reviews.description=Active unplanned reviews
*/
package org.sonar.core.review;
-import com.google.common.collect.Lists;
+import java.util.List;
+
import org.apache.ibatis.session.SqlSession;
import org.sonar.api.BatchComponent;
import org.sonar.api.ServerComponent;
import org.sonar.core.persistence.MyBatis;
-import java.util.List;
+import com.google.common.collect.Lists;
public class ReviewDao implements BatchComponent, ServerComponent {
private final MyBatis mybatis;
}
}
- public List<ReviewDto> selectByResource(int resourceId) {
+ public List<ReviewDto> selectByQuery(ReviewQuery query) {
SqlSession sqlSession = mybatis.openSession();
try {
ReviewMapper mapper = sqlSession.getMapper(ReviewMapper.class);
- return mapper.selectByResource(resourceId);
+ List<ReviewDto> result;
+ if (query.needToPartitionQuery()) {
+ result = Lists.newArrayList();
+ for (ReviewQuery partitionedQuery : query.partition()) {
+ result.addAll(mapper.selectByQuery(partitionedQuery));
+ }
+ } else {
+ result = mapper.selectByQuery(query);
+ }
+ return result;
} finally {
sqlSession.close();
}
}
- public List<ReviewDto> selectByQuery(ReviewQuery query) {
+ public Integer countByQuery(ReviewQuery query) {
SqlSession sqlSession = mybatis.openSession();
try {
ReviewMapper mapper = sqlSession.getMapper(ReviewMapper.class);
- List<ReviewDto> result;
+ Integer result = 0;
if (query.needToPartitionQuery()) {
- result = Lists.newArrayList();
for (ReviewQuery partitionedQuery : query.partition()) {
- result.addAll(mapper.selectByQuery(partitionedQuery));
+ result += mapper.countByQuery(partitionedQuery);
}
-
} else {
- result = mapper.selectByQuery(query);
+ result = mapper.countByQuery(query);
}
return result;
} finally {
public static final String STATUS_RESOLVED = "RESOLVED";
public static final String STATUS_CLOSED = "CLOSED";
+ public static final String RESOLUTION_FALSE_POSITIVE = "FALSE-POSITIVE";
+ public static final String RESOLUTION_FIXED = "FIXED";
+
private Long id;
private Integer userId;
private Integer assigneeId;
*/
package org.sonar.core.review;
-import org.apache.ibatis.annotations.Param;
-
import java.util.List;
/**
public interface ReviewMapper {
ReviewDto selectById(long id);
- List<ReviewDto> selectByResource(int resourceId);
-
List<ReviewDto> selectByQuery(ReviewQuery query);
+
+ Integer countByQuery(ReviewQuery query);
}
*/
package org.sonar.core.review;
-import com.google.common.collect.Lists;
-import org.sonar.core.persistence.DatabaseUtils;
-
import java.util.Collection;
import java.util.List;
+import org.sonar.core.persistence.DatabaseUtils;
+
+import com.google.common.collect.Lists;
+
/**
* @since 2.13
*/
private Integer userId;
private List<Integer> violationPermanentIds;
private Integer ruleId;
- private String status;
- private String resolution;
+ private List<String> statuses;
+ private List<String> resolutions;
+ private Boolean noAssignee;
+ private Boolean planned;
private ReviewQuery() {
}
- private ReviewQuery(ReviewQuery other, List<Integer> permanentIds) {
+ private ReviewQuery(ReviewQuery other) {
this.manualViolation = other.manualViolation;
this.manualSeverity = other.manualSeverity;
this.resourceId = other.resourceId;
this.userId = other.userId;
- this.violationPermanentIds = permanentIds;
+ this.violationPermanentIds = other.violationPermanentIds;
this.ruleId = other.ruleId;
- this.status = other.status;
- this.resolution = other.resolution;
+ this.statuses = other.statuses;
+ this.resolutions = other.resolutions;
+ this.noAssignee = other.noAssignee;
+ this.planned = other.planned;
}
public static ReviewQuery create() {
return new ReviewQuery();
}
+ public static ReviewQuery copy(ReviewQuery reviewQuery) {
+ return new ReviewQuery(reviewQuery);
+ }
+
public Boolean getManualViolation() {
return manualViolation;
}
return this;
}
- public String getStatus() {
- return status;
+ public List<String> getStatuses() {
+ return statuses;
}
- public ReviewQuery setStatus(String status) {
- this.status = status;
+ public ReviewQuery addStatus(String status) {
+ if (statuses == null) {
+ statuses = Lists.newArrayList();
+ }
+ statuses.add(status);
return this;
}
return this;
}
- public String getResolution() {
- return resolution;
+ public List<String> getResolutions() {
+ return resolutions;
}
- public ReviewQuery setResolution(String resolution) {
- this.resolution = resolution;
+ public ReviewQuery addResolution(String resolution) {
+ if (resolutions == null) {
+ resolutions = Lists.newArrayList();
+ }
+ resolutions.add(resolution);
return this;
}
return this;
}
+ public Boolean getNoAssignee() {
+ return noAssignee;
+ }
+
+ public ReviewQuery setNoAssignee() {
+ this.noAssignee = Boolean.TRUE;
+ return this;
+ }
+
+ public Boolean getPlanned() {
+ return planned;
+ }
+
+ public ReviewQuery setPlanned() {
+ this.planned = Boolean.TRUE;
+ return this;
+ }
+
boolean needToPartitionQuery() {
return violationPermanentIds != null && violationPermanentIds.size() > DatabaseUtils.MAX_IN_ELEMENTS;
}
List<List<Integer>> partitions = Lists.partition(violationPermanentIds, DatabaseUtils.MAX_IN_ELEMENTS);
ReviewQuery[] result = new ReviewQuery[partitions.size()];
for (int index = 0; index < partitions.size(); index++) {
- result[index] = new ReviewQuery(this, partitions.get(index));
+ result[index] = ReviewQuery.copy(this).setViolationPermanentIds(partitions.get(index));
}
return result;
<include refid="reviewColumns"/>
from reviews where id=#{id}
</select>
-
- <select id="selectByResource" parameterType="int" resultMap="reviewResultMap">
- select
- <include refid="reviewColumns"/>
- from reviews where resource_id=#{id}
- </select>
-
- <select id="selectByQuery" parameterType="org.sonar.core.review.ReviewQuery" resultMap="reviewResultMap">
- select
- <include refid="reviewColumns"/>
+
+ <sql id="selectOrCountFromWhere">
from reviews
+ <if test="planned != null">R, action_plans_reviews APR</if>
<where>
<if test="userId != null">user_id = #{userId}</if>
<if test="violationPermanentIds != null">AND rule_failure_permanent_id in
</if>
<if test="ruleId != null">AND rule_id = #{ruleId}</if>
<if test="resourceId != null">AND resource_id = #{resourceId}</if>
- <if test="status != null">AND status = #{status}</if>
+ <if test="statuses != null">AND
+ <foreach item="status" index="index" collection="statuses"
+ open="(" separator=" OR " close=")">status = #{status}
+ </foreach>
+ </if>
<if test="manualViolation != null">AND manual_violation = #{manualViolation}</if>
<if test="manualSeverity != null">AND manual_severity = #{manualSeverity}</if>
- <if test="resolution != null">AND resolution = #{resolution}</if>
+ <if test="resolutions != null">AND
+ <foreach item="resolution" index="index" collection="resolutions"
+ open="(" separator=" OR " close=")">resolution = #{resolution}
+ </foreach>
+ </if>
+ <if test="noAssignee != null">AND assignee_id IS NULL</if>
+ <if test="planned != null">AND R.id = APR.review_id</if>
</where>
+ </sql>
+
+ <select id="selectByQuery" parameterType="org.sonar.core.review.ReviewQuery" resultMap="reviewResultMap">
+ select
+ <include refid="reviewColumns"/>
+ <include refid="selectOrCountFromWhere"/>
+ </select>
+
+
+ <select id="countByQuery" parameterType="org.sonar.core.review.ReviewQuery" resultType="Integer">
+ select count(id)
+ <include refid="selectOrCountFromWhere"/>
</select>
</mapper>
*/
package org.sonar.core.review;
-import com.google.common.collect.Lists;
+import static org.hamcrest.Matchers.anyOf;
+import static org.hamcrest.Matchers.hasItem;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.nullValue;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThat;
+
+import java.util.List;
+
import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
import org.junit.Before;
import org.junit.Test;
import org.sonar.core.persistence.DaoTestCase;
-import java.util.List;
-
-import static org.hamcrest.Matchers.*;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertThat;
+import com.google.common.collect.Lists;
public class ReviewDaoTest extends DaoTestCase {
ReviewDto reviewDto = dao.selectById(100L);
assertThat(reviewDto.getId(), is(100L));
assertThat(reviewDto.getStatus(), is("OPEN"));
- assertThat(reviewDto.getResolution(), is("RESOLVE"));
+ assertThat(reviewDto.getResolution(), is(nullValue()));
assertThat(reviewDto.getProjectId(), is(20));
assertThat(reviewDto.getViolationPermanentId(), is(1));
assertThat(reviewDto.getSeverity(), is("BLOCKER"));
}
@Test
- public void shouldSelectByResource() {
+ public void shouldSelectByQuery() {
setupData("shared");
- List<ReviewDto> reviewDtos = dao.selectByResource(400);
+ List<ReviewDto> reviewDtos = dao.selectByQuery(ReviewQuery.create().setResourceId(400));
assertThat(reviewDtos.size(), is(2));
for (ReviewDto reviewDto : reviewDtos) {
assertThat(reviewDto.getId(), anyOf(is(100L), is(101L)));
}
@Test
- public void shouldSelectByQuery() {
+ public void shouldSelectByQueryWithStatuses() {
setupData("shared");
- List<ReviewDto> reviewDtos = dao.selectByQuery(ReviewQuery.create().setResourceId(400));
+ List<ReviewDto> reviewDtos = dao.selectByQuery(ReviewQuery.create().addStatus(ReviewDto.STATUS_OPEN)
+ .addStatus(ReviewDto.STATUS_REOPENED));
+ assertThat(reviewDtos.size(), is(3));
+ for (ReviewDto reviewDto : reviewDtos) {
+ assertThat(reviewDto.getId(), anyOf(is(100L), is(102L), is(103L)));
+ }
+ }
+
+ @Test
+ public void shouldSelectByQueryWithResolutions() {
+ setupData("shared");
+
+ List<ReviewDto> reviewDtos = dao.selectByQuery(ReviewQuery.create().addResolution(ReviewDto.RESOLUTION_FALSE_POSITIVE)
+ .addResolution(ReviewDto.RESOLUTION_FIXED));
+ assertThat(reviewDtos.size(), is(2));
+ for (ReviewDto reviewDto : reviewDtos) {
+ assertThat(reviewDto.getId(), anyOf(is(101L), is(104L)));
+ }
+ }
+
+ @Test
+ public void shouldSelectByQueryWithNoAssignee() {
+ setupData("shared");
+
+ List<ReviewDto> reviewDtos = dao.selectByQuery(ReviewQuery.create().setNoAssignee());
+ assertThat(reviewDtos.size(), is(2));
+ for (ReviewDto reviewDto : reviewDtos) {
+ assertThat(reviewDto.getId(), anyOf(is(101L), is(103L)));
+ }
+ }
+
+ @Test
+ public void shouldSelectByQueryWithPlanned() {
+ setupData("shared");
+
+ List<ReviewDto> reviewDtos = dao.selectByQuery(ReviewQuery.create().setPlanned());
assertThat(reviewDtos.size(), is(2));
for (ReviewDto reviewDto : reviewDtos) {
assertThat(reviewDto.getId(), anyOf(is(100L), is(101L)));
- assertThat(reviewDto.getResourceId(), is(400));
}
}
@Test
- public void shouldSelectByQuery_booleanCriteria() {
+ public void shouldCountByQuery() {
+ setupData("shared");
+
+ Integer count = dao.countByQuery(ReviewQuery.create().addStatus(ReviewDto.STATUS_OPEN)
+ .addStatus(ReviewDto.STATUS_REOPENED));
+ assertThat(count, is(3));
+ }
+
+ @Test
+ public void shouldSelectByQueryWithBooleanCriteria() {
setupData("shared");
List<ReviewDto> reviewDtos = dao.selectByQuery(ReviewQuery.create().setResourceId(400).setManualViolation(true));
}
ReviewQuery query = ReviewQuery.create().setViolationPermanentIds(permanentIds);
+ // test select query
List<ReviewDto> reviewDtos = dao.selectByQuery(query);
assertThat(reviewDtos.size(), is(3));
assertThat(reviewDtos, hasItem(new ReviewMatcherByViolationPermanentId(100)));
assertThat(reviewDtos, hasItem(new ReviewMatcherByViolationPermanentId(1300)));
assertThat(reviewDtos, hasItem(new ReviewMatcherByViolationPermanentId(3200)));
+
+ // and test count query
+ assertThat(dao.countByQuery(query), is(3));
}
static class ReviewMatcherByViolationPermanentId extends BaseMatcher<ReviewDto> {
<dataset>
+ <action_plans_reviews action_plan_id="1" review_id="100"/>
+ <action_plans_reviews action_plan_id="2" review_id="101"/>
+
<!-- First resource -->
<reviews
id="100"
status="OPEN"
rule_failure_permanent_id="1"
- resolution="RESOLVE"
+ resolution="[null]"
created_at="[null]"
updated_at="[null]"
project_id="20"
resource_line="200"
severity="BLOCKER"
user_id="300"
+ assignee_id="300"
resource_id="400"
rule_id="500"
manual_violation="[true]"
id="101"
status="CLOSED"
rule_failure_permanent_id="1"
- resolution="RESOLVE"
+ resolution="FIXED"
created_at="[null]"
updated_at="[null]"
project_id="30"
resource_line="120"
severity="MAJOR"
user_id="300"
+ assignee_id="[null]"
resource_id="400"
rule_id="505"
manual_violation="[false]"
id="102"
status="OPEN"
rule_failure_permanent_id="1"
- resolution="RESOLVE"
+ resolution="[null]"
created_at="[null]"
updated_at="[null]"
project_id="20"
resource_line="200"
severity="BLOCKER"
user_id="300"
+ assignee_id="300"
resource_id="401"
rule_id="500"
manual_violation="[true]"
manual_severity="[false]"/>
+
+ <reviews
+ id="103"
+ status="REOPENED"
+ rule_failure_permanent_id="1"
+ resolution="[null]"
+ created_at="[null]"
+ updated_at="[null]"
+ project_id="20"
+ resource_line="200"
+ severity="BLOCKER"
+ user_id="300"
+ assignee_id="[null]"
+ resource_id="401"
+ rule_id="500"
+ manual_violation="[false]"
+ manual_severity="[false]"/>
+
+ <reviews
+ id="104"
+ status="RESOLVED"
+ rule_failure_permanent_id="1"
+ resolution="FALSE-POSITIVE"
+ created_at="[null]"
+ updated_at="[null]"
+ project_id="20"
+ resource_line="200"
+ severity="BLOCKER"
+ user_id="300"
+ assignee_id="300"
+ resource_id="401"
+ rule_id="500"
+ manual_violation="[false]"
+ manual_severity="[false]"/>
</dataset>
public static final String DOMAIN_DOCUMENTATION = "Documentation";
public static final String DOMAIN_RULES = "Rules";
public static final String DOMAIN_SCM = "SCM";
+ public static final String DOMAIN_REVIEWS = "Reviews";
/**
* @deprecated since 2.5 See http://jira.codehaus.org/browse/SONAR-2007
.create();
+ //--------------------------------------------------------------------------------------------------------------------
+ //
+ // REVIEWS (since 2.14)
+ //
+ //--------------------------------------------------------------------------------------------------------------------
+
+ /**
+ * @since 2.14
+ */
+ public static final String VIOLATIONS_WITHOUT_REVIEW_KEY = "violations_without_review";
+
+ /**
+ * @since 2.14
+ */
+ public static final Metric VIOLATIONS_WITHOUT_REVIEW = new Metric.Builder(VIOLATIONS_WITHOUT_REVIEW_KEY, "Unreviewed violations", Metric.ValueType.INT)
+ .setDescription("Violations that have not been reviewed yet")
+ .setDirection(Metric.DIRECTION_WORST)
+ .setDomain(DOMAIN_REVIEWS)
+ .setBestValue(0.0)
+ .setOptimizedBestValue(true)
+ .setFormula(new SumChildValuesFormula(false))
+ .create();
+
+ /**
+ * @since 2.14
+ */
+ public static final String FALSE_POSITIVE_REVIEWS_KEY = "false_positive_reviews";
+
+ /**
+ * @since 2.14
+ */
+ public static final Metric FALSE_POSITIVE_REVIEWS = new Metric.Builder(FALSE_POSITIVE_REVIEWS_KEY, "False-positive reviews", Metric.ValueType.INT)
+ .setDescription("Active false-positive reviews")
+ .setDirection(Metric.DIRECTION_WORST)
+ .setDomain(DOMAIN_REVIEWS)
+ .setBestValue(0.0)
+ .setOptimizedBestValue(true)
+ .setFormula(new SumChildValuesFormula(false))
+ .create();
+
+ /**
+ * @since 2.14
+ */
+ public static final String ACTIVE_REVIEWS_KEY = "active_reviews";
+
+ /**
+ * @since 2.14
+ */
+ public static final Metric ACTIVE_REVIEWS = new Metric.Builder(ACTIVE_REVIEWS_KEY, "Active reviews", Metric.ValueType.INT)
+ .setDescription("Active open and reopened reviews")
+ .setDirection(Metric.DIRECTION_WORST)
+ .setDomain(DOMAIN_REVIEWS)
+ .setBestValue(0.0)
+ .setOptimizedBestValue(true)
+ .setFormula(new SumChildValuesFormula(false))
+ .create();
+
+ /**
+ * @since 2.14
+ */
+ public static final String UNASSIGNED_REVIEWS_KEY = "unassigned_reviews";
+
+ /**
+ * @since 2.14
+ */
+ public static final Metric UNASSIGNED_REVIEWS = new Metric.Builder(UNASSIGNED_REVIEWS_KEY, "Unassigned reviews", Metric.ValueType.INT)
+ .setDescription("Active unassigned reviews")
+ .setDirection(Metric.DIRECTION_WORST)
+ .setDomain(DOMAIN_REVIEWS)
+ .setBestValue(0.0)
+ .setOptimizedBestValue(true)
+ .setFormula(new SumChildValuesFormula(false))
+ .create();
+
+ /**
+ * @since 2.14
+ */
+ public static final String UNPLANNED_REVIEWS_KEY = "unplanned_reviews";
+
+ /**
+ * @since 2.14
+ */
+ public static final Metric UNPLANNED_REVIEWS = new Metric.Builder(UNPLANNED_REVIEWS_KEY, "Unplanned reviews", Metric.ValueType.INT)
+ .setDescription("Active unplanned reviews")
+ .setDirection(Metric.DIRECTION_WORST)
+ .setDomain(DOMAIN_REVIEWS)
+ .setBestValue(0.0)
+ .setOptimizedBestValue(true)
+ .setFormula(new SumChildValuesFormula(false))
+ .create();
+
+
+
+
+
//--------------------------------------------------------------------------------------------------------------------
//
// OTHERS
@DefaultTab(metrics = {CoreMetrics.VIOLATIONS_DENSITY_KEY, CoreMetrics.WEIGHTED_VIOLATIONS_KEY, CoreMetrics.VIOLATIONS_KEY, CoreMetrics.BLOCKER_VIOLATIONS_KEY,
CoreMetrics.CRITICAL_VIOLATIONS_KEY, CoreMetrics.MAJOR_VIOLATIONS_KEY, CoreMetrics.MINOR_VIOLATIONS_KEY, CoreMetrics.INFO_VIOLATIONS_KEY,
CoreMetrics.NEW_VIOLATIONS_KEY, CoreMetrics.NEW_BLOCKER_VIOLATIONS_KEY, CoreMetrics.NEW_CRITICAL_VIOLATIONS_KEY, CoreMetrics.NEW_MAJOR_VIOLATIONS_KEY,
- CoreMetrics.NEW_MINOR_VIOLATIONS_KEY, CoreMetrics.NEW_INFO_VIOLATIONS_KEY})
+ CoreMetrics.NEW_MINOR_VIOLATIONS_KEY, CoreMetrics.NEW_INFO_VIOLATIONS_KEY, CoreMetrics.ACTIVE_REVIEWS_KEY, CoreMetrics.UNASSIGNED_REVIEWS_KEY,
+ CoreMetrics.UNPLANNED_REVIEWS_KEY, CoreMetrics.FALSE_POSITIVE_REVIEWS_KEY, CoreMetrics.VIOLATIONS_WITHOUT_REVIEW_KEY})
@ResourceQualifier({Qualifiers.VIEW, Qualifiers.SUBVIEW, Qualifiers.PROJECT, Qualifiers.MODULE, Qualifiers.PACKAGE, Qualifiers.DIRECTORY, Qualifiers.FILE, Qualifiers.CLASS})
/* all exept unit tests...*/
@UserRole(UserRole.CODEVIEWER)