diff options
author | Fabrice Bellingard <bellingard@gmail.com> | 2012-01-12 18:30:40 +0100 |
---|---|---|
committer | Fabrice Bellingard <bellingard@gmail.com> | 2012-01-12 18:40:45 +0100 |
commit | c82a64fb8ea2dc4b031dd1bb5e766c846fdbd443 (patch) | |
tree | e431daf56f84d610736a6500ed3c4e3bf2d5abc0 /plugins | |
parent | d9336198cba4299a3fd2f0b911c9ce06a6514009 (diff) | |
download | sonarqube-c82a64fb8ea2dc4b031dd1bb5e766c846fdbd443.tar.gz sonarqube-c82a64fb8ea2dc4b031dd1bb5e766c846fdbd443.zip |
SONAR-3012 New widget to monitor the review activity
- 5 new metrics added
- Decorator implemented to compute those metrics
- Widget implemented to report those metrics
Diffstat (limited to 'plugins')
8 files changed, 444 insertions, 1 deletions
diff --git a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/CorePlugin.java b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/CorePlugin.java index 463ccb89502..24319ea90df 100644 --- a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/CorePlugin.java +++ b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/CorePlugin.java @@ -48,6 +48,7 @@ import org.sonar.plugins.core.widgets.reviews.FalsePositiveReviewsWidget; 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; @@ -248,6 +249,7 @@ public class CorePlugin extends SonarPlugin { extensions.add(PlannedReviewsWidget.class); extensions.add(UnplannedReviewsWidget.class); extensions.add(ActionPlansWidget.class); + extensions.add(ReviewsMetricsWidget.class); // dashboards extensions.add(DefaultDashboard.class); @@ -291,6 +293,7 @@ public class CorePlugin extends SonarPlugin { extensions.add(UpdateReviewsDecorator.class); extensions.add(ViolationSeverityUpdater.class); extensions.add(IndexProjectPostJob.class); + extensions.add(ReviewsMeasuresDecorator.class); // time machine extensions.add(TendencyDecorator.class); diff --git a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/sensors/ManualViolationInjector.java b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/sensors/ManualViolationInjector.java index 78065d011db..5a5c0d64d02 100644 --- a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/sensors/ManualViolationInjector.java +++ b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/sensors/ManualViolationInjector.java @@ -51,7 +51,7 @@ public class ManualViolationInjector implements Decorator { 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) { diff --git a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/sensors/ReviewsMeasuresDecorator.java b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/sensors/ReviewsMeasuresDecorator.java new file mode 100644 index 00000000000..706cc6fc047 --- /dev/null +++ b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/sensors/ReviewsMeasuresDecorator.java @@ -0,0 +1,90 @@ +/* + * 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)); + } + +} diff --git a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/widgets/reviews/ReviewsMetricsWidget.java b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/widgets/reviews/ReviewsMetricsWidget.java new file mode 100644 index 00000000000..7ad4e69cd18 --- /dev/null +++ b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/widgets/reviews/ReviewsMetricsWidget.java @@ -0,0 +1,40 @@ +/* + * 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"; + } +} diff --git a/plugins/sonar-core-plugin/src/main/resources/org/sonar/plugins/core/widgets/reviews/reviews_metrics.html.erb b/plugins/sonar-core-plugin/src/main/resources/org/sonar/plugins/core/widgets/reviews/reviews_metrics.html.erb new file mode 100644 index 00000000000..3388d4fe12e --- /dev/null +++ b/plugins/sonar-core-plugin/src/main/resources/org/sonar/plugins/core/widgets/reviews/reviews_metrics.html.erb @@ -0,0 +1,50 @@ +<% + 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 diff --git a/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/sensors/ReviewsMeasuresDecoratorTest.java b/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/sensors/ReviewsMeasuresDecoratorTest.java new file mode 100644 index 00000000000..b72c4b989f8 --- /dev/null +++ b/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/sensors/ReviewsMeasuresDecoratorTest.java @@ -0,0 +1,162 @@ +/* + * 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) { + } + }; + } +} diff --git a/plugins/sonar-core-plugin/src/test/resources/org/sonar/plugins/core/sensors/ReviewsMeasuresDecoratorTest/fixture.xml b/plugins/sonar-core-plugin/src/test/resources/org/sonar/plugins/core/sensors/ReviewsMeasuresDecoratorTest/fixture.xml new file mode 100644 index 00000000000..7384c0e2e5f --- /dev/null +++ b/plugins/sonar-core-plugin/src/test/resources/org/sonar/plugins/core/sensors/ReviewsMeasuresDecoratorTest/fixture.xml @@ -0,0 +1,69 @@ +<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 diff --git a/plugins/sonar-l10n-en-plugin/src/main/resources/org/sonar/l10n/core.properties b/plugins/sonar-l10n-en-plugin/src/main/resources/org/sonar/l10n/core.properties index 5798be1507c..dbb2a5c49fc 100644 --- a/plugins/sonar-l10n-en-plugin/src/main/resources/org/sonar/l10n/core.properties +++ b/plugins/sonar-l10n-en-plugin/src/main/resources/org/sonar/l10n/core.properties @@ -719,6 +719,15 @@ widget.planned_reviews.no_action_plan=No action plan 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 + #------------------------------------------------------------------------------ @@ -1542,3 +1551,23 @@ metric.business_value.description=An indication on the value of the project for 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 |