From dc45bd7b11b12ba9eff294c8a2de11c5b8fc545e Mon Sep 17 00:00:00 2001 From: simonbrandhof Date: Sun, 27 Feb 2011 23:49:20 +0100 Subject: [PATCH] SONAR-2218 aggregate variations of coverage of changed code + add sample of widget --- .../org/sonar/plugins/core/CorePlugin.java | 4 +- .../timemachine/NewCoverageAggregator.java | 86 ++++++++ ...ator.java => NewCoverageFileAnalyzer.java} | 17 +- .../core/widgets/NewCoverageWidget.java | 42 ++++ .../core/widgets/new_coverage.html.erb | 39 ++++ ....java => NewCoverageFileAnalyzerTest.java} | 23 +- .../app/controllers/browse_controller.rb | 172 +++++++++++++++ .../app/views/browse/_violation.html.erb | 5 + .../WEB-INF/app/views/browse/index.html.erb | 198 ++++++++++++++++++ 9 files changed, 562 insertions(+), 24 deletions(-) create mode 100644 plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/timemachine/NewCoverageAggregator.java rename plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/timemachine/{NewCoverageDecorator.java => NewCoverageFileAnalyzer.java} (94%) create mode 100644 plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/widgets/NewCoverageWidget.java create mode 100644 plugins/sonar-core-plugin/src/main/resources/org/sonar/plugins/core/widgets/new_coverage.html.erb rename plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/timemachine/{NewCoverageDecoratorTest.java => NewCoverageFileAnalyzerTest.java} (92%) create mode 100644 sonar-server/src/main/webapp/WEB-INF/app/controllers/browse_controller.rb create mode 100644 sonar-server/src/main/webapp/WEB-INF/app/views/browse/_violation.html.erb create mode 100644 sonar-server/src/main/webapp/WEB-INF/app/views/browse/index.html.erb 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 a343985c1de..c8f802a0e5d 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 @@ -201,6 +201,7 @@ public class CorePlugin implements Plugin { extensions.add(SizeWidget.class); extensions.add(EventsWidget.class); extensions.add(CustomMeasuresWidget.class); + extensions.add(NewCoverageWidget.class); // chart extensions.add(XradarChart.class); @@ -237,7 +238,8 @@ public class CorePlugin implements Plugin { extensions.add(ViolationPersisterDecorator.class); extensions.add(NewViolationsDecorator.class); extensions.add(TimeMachineConfigurationPersister.class); - extensions.add(NewCoverageDecorator.class); + extensions.add(NewCoverageFileAnalyzer.class); + extensions.add(NewCoverageAggregator.class); return extensions; } diff --git a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/timemachine/NewCoverageAggregator.java b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/timemachine/NewCoverageAggregator.java new file mode 100644 index 00000000000..f4d56097161 --- /dev/null +++ b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/timemachine/NewCoverageAggregator.java @@ -0,0 +1,86 @@ +/* + * Sonar, open source software quality management tool. + * Copyright (C) 2008-2011 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.timemachine; + +import org.apache.commons.lang.ArrayUtils; +import org.sonar.api.batch.Decorator; +import org.sonar.api.batch.DecoratorContext; +import org.sonar.api.batch.DependedUpon; +import org.sonar.api.measures.CoreMetrics; +import org.sonar.api.measures.Measure; +import org.sonar.api.measures.Metric; +import org.sonar.api.resources.Project; +import org.sonar.api.resources.Qualifiers; +import org.sonar.api.resources.Resource; +import org.sonar.api.resources.Scopes; + +import java.util.Arrays; +import java.util.List; + +public final class NewCoverageAggregator implements Decorator { + + public boolean shouldExecuteOnProject(Project project) { + return true; + } + + @DependedUpon + public List generatesNewCoverageMetrics() { + return Arrays.asList(CoreMetrics.NEW_LINES_TO_COVER, CoreMetrics.NEW_UNCOVERED_LINES, + CoreMetrics.NEW_CONDITIONS_TO_COVER, CoreMetrics.NEW_UNCOVERED_CONDITIONS); + } + + public void decorate(Resource resource, DecoratorContext context) { + if (shouldDecorate(resource)) { + int maxPeriods = (Qualifiers.isView(resource, true) ? 3 : 5); + aggregate(context, CoreMetrics.NEW_LINES_TO_COVER, maxPeriods); + aggregate(context, CoreMetrics.NEW_UNCOVERED_LINES, maxPeriods); + aggregate(context, CoreMetrics.NEW_CONDITIONS_TO_COVER, maxPeriods); + aggregate(context, CoreMetrics.NEW_UNCOVERED_CONDITIONS, maxPeriods); + } + } + + private void aggregate(DecoratorContext context, Metric metric, int maxPeriods) { + int[] variations = {0,0,0,0,0}; + boolean[] hasValues = {false,false,false,false,false}; + for (Measure child : context.getChildrenMeasures(metric)) { + for (int indexPeriod=1 ; indexPeriod<=maxPeriods ; indexPeriod++) { + Double variation = child.getVariation(indexPeriod); + if (variation!=null) { + variations[indexPeriod-1]=variations[indexPeriod-1] + variation.intValue(); + hasValues[indexPeriod-1]=true; + } + } + } + + if (ArrayUtils.contains(hasValues, true)) { + Measure measure = new Measure(metric); + for (int index=0 ; index<5 ; index++) { + if (hasValues[index]) { + measure.setVariation(index+1, (double)variations[index]); + } + } + context.saveMeasure(measure); + } + } + + private boolean shouldDecorate(Resource resource) { + return Scopes.isHigherThan(resource, Scopes.FILE); + } +} diff --git a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/timemachine/NewCoverageDecorator.java b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/timemachine/NewCoverageFileAnalyzer.java similarity index 94% rename from plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/timemachine/NewCoverageDecorator.java rename to plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/timemachine/NewCoverageFileAnalyzer.java index 18297718721..6b7018560b1 100644 --- a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/timemachine/NewCoverageDecorator.java +++ b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/timemachine/NewCoverageFileAnalyzer.java @@ -21,7 +21,6 @@ package org.sonar.plugins.core.timemachine; import com.google.common.collect.Lists; import com.google.common.collect.Maps; -import org.apache.commons.lang.NumberUtils; import org.apache.commons.lang.ObjectUtils; import org.sonar.api.batch.Decorator; import org.sonar.api.batch.DecoratorContext; @@ -46,18 +45,18 @@ import java.util.Map; /** * @since 2.7 */ -public final class NewCoverageDecorator implements Decorator { +public final class NewCoverageFileAnalyzer implements Decorator { private List structs; - public NewCoverageDecorator(TimeMachineConfiguration timeMachineConfiguration) { + public NewCoverageFileAnalyzer(TimeMachineConfiguration timeMachineConfiguration) { structs = Lists.newArrayList(); for (PastSnapshot pastSnapshot : timeMachineConfiguration.getProjectPastSnapshots()) { structs.add(new PeriodStruct(pastSnapshot)); } } - NewCoverageDecorator(List structs) { + NewCoverageFileAnalyzer(List structs) { this.structs = structs; } @@ -67,7 +66,7 @@ public final class NewCoverageDecorator implements Decorator { } private boolean shouldDecorate(Resource resource) { - return isFile(resource); + return Scopes.isFile(resource) && !Qualifiers.UNIT_TEST_FILE.equals(resource.getQualifier()); } @DependsUpon @@ -156,10 +155,6 @@ public final class NewCoverageDecorator implements Decorator { } - private boolean isFile(Resource resource) { - return Scopes.isFile(resource) && !Qualifiers.UNIT_TEST_FILE.equals(resource.getQualifier()); - } - public static final class PeriodStruct { int index; Date date; @@ -167,7 +162,7 @@ public final class NewCoverageDecorator implements Decorator { PeriodStruct(PastSnapshot pastSnapshot) { this.index = pastSnapshot.getIndex(); - this.date = pastSnapshot.getDate(); + this.date = pastSnapshot.getTargetDate(); } PeriodStruct(int index, Date date) { @@ -187,7 +182,7 @@ public final class NewCoverageDecorator implements Decorator { //TODO warning } else if (lineDate.after(date)) { - // TODO experiment if date comparison is faster or not + // TODO test if string comparison is faster or not addLine(hits > 0); addConditions(conditions, coveredConditions); } diff --git a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/widgets/NewCoverageWidget.java b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/widgets/NewCoverageWidget.java new file mode 100644 index 00000000000..8ca5e63cb4e --- /dev/null +++ b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/widgets/NewCoverageWidget.java @@ -0,0 +1,42 @@ +/* + * Sonar, open source software quality management tool. + * Copyright (C) 2008-2011 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; + +import org.sonar.api.web.AbstractRubyTemplate; +import org.sonar.api.web.Description; +import org.sonar.api.web.RubyRailsWidget; +import org.sonar.api.web.WidgetCategory; + +@WidgetCategory({"Tests"}) +@Description("TODO") +public class NewCoverageWidget extends AbstractRubyTemplate implements RubyRailsWidget { + public String getId() { + return "new_coverage"; + } + + public String getTitle() { + return "New coverage"; + } + + @Override + protected String getTemplatePath() { + return "/org/sonar/plugins/core/widgets/new_coverage.html.erb"; + } +} \ No newline at end of file diff --git a/plugins/sonar-core-plugin/src/main/resources/org/sonar/plugins/core/widgets/new_coverage.html.erb b/plugins/sonar-core-plugin/src/main/resources/org/sonar/plugins/core/widgets/new_coverage.html.erb new file mode 100644 index 00000000000..102db31ca17 --- /dev/null +++ b/plugins/sonar-core-plugin/src/main/resources/org/sonar/plugins/core/widgets/new_coverage.html.erb @@ -0,0 +1,39 @@ +

New coverage

+ + + + <% m=measure('new_lines_to_cover') %> + <% 5.times do |index| %> + + <% end %> + + + + <% m=measure('new_uncovered_lines') %> + <% 5.times do |index| %> + + <% end %> + + + + <% m=measure('new_conditions_to_cover') %> + <% 5.times do |index| %> + + <% end %> + + + + <% m=measure('new_uncovered_conditions') %> + <% 5.times do |index| %> + + <% end %> + +
New lines + <%= format_variation(m, :index => index+1) -%> +
New uncovered + <%= format_variation(m, :index => index+1) -%> +
New conditions + <%= format_variation(m, :index => index+1) -%> +
New uncovered conditions + <%= format_variation(m, :index => index+1) -%> +
diff --git a/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/timemachine/NewCoverageDecoratorTest.java b/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/timemachine/NewCoverageFileAnalyzerTest.java similarity index 92% rename from plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/timemachine/NewCoverageDecoratorTest.java rename to plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/timemachine/NewCoverageFileAnalyzerTest.java index e94cb5b8518..3eb6a0f27ed 100644 --- a/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/timemachine/NewCoverageDecoratorTest.java +++ b/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/timemachine/NewCoverageFileAnalyzerTest.java @@ -19,7 +19,6 @@ */ package org.sonar.plugins.core.timemachine; -import org.apache.commons.lang.NumberUtils; import org.hamcrest.BaseMatcher; import org.hamcrest.Description; import org.junit.Test; @@ -37,7 +36,7 @@ import java.util.List; import static org.mockito.Mockito.*; -public class NewCoverageDecoratorTest { +public class NewCoverageFileAnalyzerTest { @Test public void shouldDoNothingIfNoScmData() throws ParseException { @@ -45,7 +44,7 @@ public class NewCoverageDecoratorTest { when(context.getMeasure(CoreMetrics.COVERAGE_LINE_HITS_DATA)) .thenReturn(new Measure(CoreMetrics.COVERAGE_LINE_HITS_DATA, "1=10")); - NewCoverageDecorator decorator = newDecorator(); + NewCoverageFileAnalyzer decorator = newDecorator(); decorator.doDecorate(context); verify(context, never()).saveMeasure((Measure) anyObject()); } @@ -56,7 +55,7 @@ public class NewCoverageDecoratorTest { when(context.getMeasure(CoreMetrics.SCM_LAST_COMMIT_DATETIMES_BY_LINE)) .thenReturn(new Measure(CoreMetrics.SCM_LAST_COMMIT_DATETIMES_BY_LINE, "10=2008-05-18T00:00:00+0000")); - NewCoverageDecorator decorator = newDecorator(); + NewCoverageFileAnalyzer decorator = newDecorator(); decorator.doDecorate(context); verify(context, never()).saveMeasure((Measure) anyObject()); @@ -70,7 +69,7 @@ public class NewCoverageDecoratorTest { when(context.getMeasure(CoreMetrics.SCM_LAST_COMMIT_DATETIMES_BY_LINE)).thenReturn( new Measure(CoreMetrics.SCM_LAST_COMMIT_DATETIMES_BY_LINE, "10=2007-01-15T00:00:00+0000;11=2011-01-01T00:00:00+0000")); - NewCoverageDecorator decorator = newDecorator(); + NewCoverageFileAnalyzer decorator = newDecorator(); decorator.doDecorate(context); // line 11 has been updated after date1 (2009-12-25). This line is covered. @@ -102,7 +101,7 @@ public class NewCoverageDecoratorTest { when(context.getMeasure(CoreMetrics.SCM_LAST_COMMIT_DATETIMES_BY_LINE)).thenReturn( new Measure(CoreMetrics.SCM_LAST_COMMIT_DATETIMES_BY_LINE, "10=2007-01-15T00:00:00+0000;11=2011-01-01T00:00:00+0000")); - NewCoverageDecorator decorator = newDecorator(); + NewCoverageFileAnalyzer decorator = newDecorator(); decorator.doDecorate(context); // line 11 has been updated after date1 (2009-12-25). This line has 1 covered condition amongst 4 @@ -134,7 +133,7 @@ public class NewCoverageDecoratorTest { when(context.getMeasure(CoreMetrics.SCM_LAST_COMMIT_DATETIMES_BY_LINE)).thenReturn( new Measure(CoreMetrics.SCM_LAST_COMMIT_DATETIMES_BY_LINE, "10=2007-01-15T00:00:00+0000;11=2011-01-01T00:00:00+0000")); - NewCoverageDecorator decorator = newDecorator(); + NewCoverageFileAnalyzer decorator = newDecorator(); decorator.doDecorate(context); // line 11 has been updated after date1 (2009-12-25) but it has no conditions @@ -170,11 +169,11 @@ public class NewCoverageDecoratorTest { } } - private NewCoverageDecorator newDecorator() throws ParseException { - List structs = Arrays.asList( - new NewCoverageDecorator.PeriodStruct(1, newDate("2009-12-25")), - new NewCoverageDecorator.PeriodStruct(3, newDate("2011-02-18"))); - return new NewCoverageDecorator(structs); + private NewCoverageFileAnalyzer newDecorator() throws ParseException { + List structs = Arrays.asList( + new NewCoverageFileAnalyzer.PeriodStruct(1, newDate("2009-12-25")), + new NewCoverageFileAnalyzer.PeriodStruct(3, newDate("2011-02-18"))); + return new NewCoverageFileAnalyzer(structs); } private Date newDate(String s) throws ParseException { diff --git a/sonar-server/src/main/webapp/WEB-INF/app/controllers/browse_controller.rb b/sonar-server/src/main/webapp/WEB-INF/app/controllers/browse_controller.rb new file mode 100644 index 00000000000..5ae0ae64c12 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/app/controllers/browse_controller.rb @@ -0,0 +1,172 @@ +# +# Sonar, entreprise quality control tool. +# Copyright (C) 2008-2011 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 +# +class BrowseController < ApplicationController + + SECTION=Navigation::SECTION_RESOURCE + + def index + @resource = Project.by_key(params[:id]) + + if (@resource && has_role?(:user, @resource)) + @snapshot=@resource.last_snapshot + render_resource() + else + access_denied + end + end + + private + + + def render_resource + @source = @snapshot.source + if @source + source_lines=Java::OrgSonarServerUi::JRubyFacade.new.colorizeCode(@source.data, @snapshot.project.language).split("\n") + load_scm() + + @lines=[] + current_revision=nil + source_lines.each_with_index do |source, index| + line=Line.new(source) + @lines< 'rule', :conditions => [conditions] + values, :order => 'failure_level DESC').each do |violation| + # sorted by severity => from blocker to info + if violation.line && violation.line>0 + @lines[violation.line-1].add_violation(violation) + else + @global_violations<0 + end + + def violation_severity + if @violations && @violations.size>0 + @violations[0].failure_level + else + nil + end + end + end +end \ No newline at end of file diff --git a/sonar-server/src/main/webapp/WEB-INF/app/views/browse/_violation.html.erb b/sonar-server/src/main/webapp/WEB-INF/app/views/browse/_violation.html.erb new file mode 100644 index 00000000000..e0803a8ec42 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/app/views/browse/_violation.html.erb @@ -0,0 +1,5 @@ + + +<%= h(violation.rule.name) -%> + » +<%= h(violation.message) -%> \ No newline at end of file diff --git a/sonar-server/src/main/webapp/WEB-INF/app/views/browse/index.html.erb b/sonar-server/src/main/webapp/WEB-INF/app/views/browse/index.html.erb new file mode 100644 index 00000000000..d05ecd6f3c6 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/app/views/browse/index.html.erb @@ -0,0 +1,198 @@ + + +
+ <%= qualifier_icon(@resource) -%> <%= @resource.long_name -%> + <% if @lines %> + | raw + <% end %> +
+ + +
+ +<% if @display_violations && @global_violations && @global_violations.size>0 -%> + + <% @global_violations.each do |violation| %> + + + + <% end %> +
<%= render :partial => 'violation', :locals => {:violation => violation} -%>
+<% end %> + +<% if @lines && @lines.size>0 %> + + <% + @lines.each_with_index do |line, index| + status=hits_status=conditions_status='' + if @display_coverage && line.hits + hits_status=(line.hits>0 ? 'ok' : 'ko') + if line.conditions && line.conditions>0 && line.covered_conditions + if line.covered_conditions==0 + status='ko' + conditions_status='ko' + elsif line.covered_conditions==line.conditions + status='' + conditions_status='ok' + else + conditions_status='warn' + status='warn' + end + elsif line.hits + status=(line.hits>0 ? '' : 'ko') + end + elsif @display_violations + status="sev#{line.violation_severity}" + end + %> + + <% if @display_scm + if line.display_scm + title = "Revision #{h(line.revision)} (#{line.date})" + %> + + <% else %> + + <% end + end %> + <% if @display_violations %> + + <% end %> + + + <% if @display_coverage %> + + + <% end %> + + + <% end %> +
<%= line.date[0..9] -%> <%= h(line.author) -%> + <% if line.violations? + line.violations.each_with_index do |violation, violation_index| %> + <%= '
' if violation_index>0 %> + <%= render :partial => 'violation', :locals => {:violation => violation} -%> + <% + end + end + %> +
<%= index + 1 -%><%= line.hits -%><% if line.conditions && line.conditions>0 %><%= line.covered_conditions -%>/<%= line.conditions -%><% end %><%= line.source -%>
+<% end %> \ No newline at end of file -- 2.39.5