]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-2218 aggregate variations of coverage of changed code + add sample of widget
authorsimonbrandhof <simon.brandhof@gmail.com>
Sun, 27 Feb 2011 22:49:20 +0000 (23:49 +0100)
committersimonbrandhof <simon.brandhof@gmail.com>
Sun, 27 Feb 2011 22:50:12 +0000 (23:50 +0100)
plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/CorePlugin.java
plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/timemachine/NewCoverageAggregator.java [new file with mode: 0644]
plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/timemachine/NewCoverageDecorator.java [deleted file]
plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/timemachine/NewCoverageFileAnalyzer.java [new file with mode: 0644]
plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/widgets/NewCoverageWidget.java [new file with mode: 0644]
plugins/sonar-core-plugin/src/main/resources/org/sonar/plugins/core/widgets/new_coverage.html.erb [new file with mode: 0644]
plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/timemachine/NewCoverageDecoratorTest.java [deleted file]
plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/timemachine/NewCoverageFileAnalyzerTest.java [new file with mode: 0644]
sonar-server/src/main/webapp/WEB-INF/app/controllers/browse_controller.rb [new file with mode: 0644]
sonar-server/src/main/webapp/WEB-INF/app/views/browse/_violation.html.erb [new file with mode: 0644]
sonar-server/src/main/webapp/WEB-INF/app/views/browse/index.html.erb [new file with mode: 0644]

index a343985c1dead93d8d6e306c931c101f59f0c2dd..c8f802a0e5d0250127d92ad92c487cd7a1f8b72c 100644 (file)
@@ -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 (file)
index 0000000..f4d5609
--- /dev/null
@@ -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<Metric> 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/NewCoverageDecorator.java
deleted file mode 100644 (file)
index 1829771..0000000
+++ /dev/null
@@ -1,210 +0,0 @@
-/*
- * 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 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;
-import org.sonar.api.batch.DependedUpon;
-import org.sonar.api.batch.DependsUpon;
-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 org.sonar.api.utils.KeyValueFormat;
-import org.sonar.batch.components.PastSnapshot;
-import org.sonar.batch.components.TimeMachineConfiguration;
-
-import java.util.Arrays;
-import java.util.Date;
-import java.util.List;
-import java.util.Map;
-
-/**
- * @since 2.7
- */
-public final class NewCoverageDecorator implements Decorator {
-
-  private List<PeriodStruct> structs;
-
-  public NewCoverageDecorator(TimeMachineConfiguration timeMachineConfiguration) {
-    structs = Lists.newArrayList();
-    for (PastSnapshot pastSnapshot : timeMachineConfiguration.getProjectPastSnapshots()) {
-      structs.add(new PeriodStruct(pastSnapshot));
-    }
-  }
-
-  NewCoverageDecorator(List<PeriodStruct> structs) {
-    this.structs = structs;
-  }
-
-
-  public boolean shouldExecuteOnProject(Project project) {
-    return project.isLatestAnalysis() && !structs.isEmpty();
-  }
-
-  private boolean shouldDecorate(Resource resource) {
-    return isFile(resource);
-  }
-
-  @DependsUpon
-  public List<Metric> dependsOnMetrics() {
-    return Arrays.asList(CoreMetrics.SCM_LAST_COMMIT_DATETIMES_BY_LINE, CoreMetrics.COVERAGE_LINE_HITS_DATA,
-        CoreMetrics.CONDITIONS_BY_LINE, CoreMetrics.COVERED_CONDITIONS_BY_LINE);
-  }
-
-  @DependedUpon
-  public List<Metric> 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)) {
-      doDecorate(context);
-    }
-  }
-
-  void doDecorate(DecoratorContext context) {
-    if (parse(context)) {
-      compute(context);
-    }
-  }
-
-  private boolean parse(DecoratorContext context) {
-    Measure lastCommits = context.getMeasure(CoreMetrics.SCM_LAST_COMMIT_DATETIMES_BY_LINE);
-    Measure hitsByLineMeasure = context.getMeasure(CoreMetrics.COVERAGE_LINE_HITS_DATA);
-
-    if (lastCommits != null && lastCommits.hasData() && hitsByLineMeasure != null && hitsByLineMeasure.hasData()) {
-      Map<Integer, Date> datesByLine = KeyValueFormat.parseIntDateTime(lastCommits.getData());
-      Map<Integer, Integer> hitsByLine = parseCountByLine(hitsByLineMeasure);
-      Map<Integer, Integer> conditionsByLine = parseCountByLine(context.getMeasure(CoreMetrics.CONDITIONS_BY_LINE));
-      Map<Integer, Integer> coveredConditionsByLine = parseCountByLine(context.getMeasure(CoreMetrics.COVERED_CONDITIONS_BY_LINE));
-
-      reset();
-
-      for (Map.Entry<Integer, Integer> entry : hitsByLine.entrySet()) {
-        int lineId = entry.getKey();
-        int hits = entry.getValue();
-        int conditions = (Integer)ObjectUtils.defaultIfNull(conditionsByLine.get(lineId), 0);
-        int coveredConditions = (Integer)ObjectUtils.defaultIfNull(coveredConditionsByLine.get(lineId), 0);
-        Date date = datesByLine.get(lineId);
-        for (PeriodStruct struct : structs) {
-          struct.analyze(date, hits, conditions, coveredConditions);
-        }
-      }
-
-      return true;
-    }
-    return false;
-  }
-
-  private void reset() {
-    for (PeriodStruct struct : structs) {
-      struct.reset();
-    }
-  }
-
-  private void compute(DecoratorContext context) {
-    Measure newLines = new Measure(CoreMetrics.NEW_LINES_TO_COVER);
-    Measure newUncoveredLines = new Measure(CoreMetrics.NEW_UNCOVERED_LINES);
-    Measure newConditions = new Measure(CoreMetrics.NEW_CONDITIONS_TO_COVER);
-    Measure newUncoveredConditions = new Measure(CoreMetrics.NEW_UNCOVERED_CONDITIONS);
-
-    for (PeriodStruct struct : structs) {
-      newLines.setVariation(struct.index, (double)struct.newLines);
-      newUncoveredLines.setVariation(struct.index, (double) (struct.newLines - struct.newCoveredLines));
-      newConditions.setVariation(struct.index, (double)struct.newConditions);
-      newUncoveredConditions.setVariation(struct.index, (double)struct.newConditions-struct.newCoveredConditions);
-    }
-
-    context.saveMeasure(newLines);
-    context.saveMeasure(newUncoveredLines);
-    context.saveMeasure(newConditions);
-    context.saveMeasure(newUncoveredConditions);
-  }
-
-
-  private Map<Integer, Integer> parseCountByLine(Measure measure) {
-    if (measure != null && measure.hasData()) {
-      return KeyValueFormat.parseIntInt(measure.getData());
-    }
-    return Maps.newHashMap();
-  }
-
-
-  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;
-    int newLines = 0, newCoveredLines = 0, newConditions = 0, newCoveredConditions = 0;
-
-    PeriodStruct(PastSnapshot pastSnapshot) {
-      this.index = pastSnapshot.getIndex();
-      this.date = pastSnapshot.getDate();
-    }
-
-    PeriodStruct(int index, Date date) {
-      this.index = index;
-      this.date = date;
-    }
-
-    void reset() {
-      newLines = 0;
-      newCoveredLines = 0;
-      newConditions = 0;
-      newCoveredConditions = 0;
-    }
-
-    void analyze(Date lineDate, int hits, int conditions, int coveredConditions) {
-      if (lineDate == null) {
-        //TODO warning
-
-      } else if (lineDate.after(date)) {
-        // TODO experiment if date comparison is faster or not
-        addLine(hits > 0);
-        addConditions(conditions, coveredConditions);
-      }
-    }
-
-    void addLine(boolean covered) {
-      newLines += 1;
-      if (covered) {
-        newCoveredLines += 1;
-      }
-    }
-
-    void addConditions(int count, int countCovered) {
-      newConditions += count;
-      if (count > 0) {
-        newCoveredConditions += countCovered;
-      }
-    }
-  }
-}
diff --git a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/timemachine/NewCoverageFileAnalyzer.java b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/timemachine/NewCoverageFileAnalyzer.java
new file mode 100644 (file)
index 0000000..6b70185
--- /dev/null
@@ -0,0 +1,205 @@
+/*
+ * 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 com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import org.apache.commons.lang.ObjectUtils;
+import org.sonar.api.batch.Decorator;
+import org.sonar.api.batch.DecoratorContext;
+import org.sonar.api.batch.DependedUpon;
+import org.sonar.api.batch.DependsUpon;
+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 org.sonar.api.utils.KeyValueFormat;
+import org.sonar.batch.components.PastSnapshot;
+import org.sonar.batch.components.TimeMachineConfiguration;
+
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @since 2.7
+ */
+public final class NewCoverageFileAnalyzer implements Decorator {
+
+  private List<PeriodStruct> structs;
+
+  public NewCoverageFileAnalyzer(TimeMachineConfiguration timeMachineConfiguration) {
+    structs = Lists.newArrayList();
+    for (PastSnapshot pastSnapshot : timeMachineConfiguration.getProjectPastSnapshots()) {
+      structs.add(new PeriodStruct(pastSnapshot));
+    }
+  }
+
+  NewCoverageFileAnalyzer(List<PeriodStruct> structs) {
+    this.structs = structs;
+  }
+
+
+  public boolean shouldExecuteOnProject(Project project) {
+    return project.isLatestAnalysis() && !structs.isEmpty();
+  }
+
+  private boolean shouldDecorate(Resource resource) {
+    return Scopes.isFile(resource) && !Qualifiers.UNIT_TEST_FILE.equals(resource.getQualifier());
+  }
+
+  @DependsUpon
+  public List<Metric> dependsOnMetrics() {
+    return Arrays.asList(CoreMetrics.SCM_LAST_COMMIT_DATETIMES_BY_LINE, CoreMetrics.COVERAGE_LINE_HITS_DATA,
+        CoreMetrics.CONDITIONS_BY_LINE, CoreMetrics.COVERED_CONDITIONS_BY_LINE);
+  }
+
+  @DependedUpon
+  public List<Metric> 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)) {
+      doDecorate(context);
+    }
+  }
+
+  void doDecorate(DecoratorContext context) {
+    if (parse(context)) {
+      compute(context);
+    }
+  }
+
+  private boolean parse(DecoratorContext context) {
+    Measure lastCommits = context.getMeasure(CoreMetrics.SCM_LAST_COMMIT_DATETIMES_BY_LINE);
+    Measure hitsByLineMeasure = context.getMeasure(CoreMetrics.COVERAGE_LINE_HITS_DATA);
+
+    if (lastCommits != null && lastCommits.hasData() && hitsByLineMeasure != null && hitsByLineMeasure.hasData()) {
+      Map<Integer, Date> datesByLine = KeyValueFormat.parseIntDateTime(lastCommits.getData());
+      Map<Integer, Integer> hitsByLine = parseCountByLine(hitsByLineMeasure);
+      Map<Integer, Integer> conditionsByLine = parseCountByLine(context.getMeasure(CoreMetrics.CONDITIONS_BY_LINE));
+      Map<Integer, Integer> coveredConditionsByLine = parseCountByLine(context.getMeasure(CoreMetrics.COVERED_CONDITIONS_BY_LINE));
+
+      reset();
+
+      for (Map.Entry<Integer, Integer> entry : hitsByLine.entrySet()) {
+        int lineId = entry.getKey();
+        int hits = entry.getValue();
+        int conditions = (Integer)ObjectUtils.defaultIfNull(conditionsByLine.get(lineId), 0);
+        int coveredConditions = (Integer)ObjectUtils.defaultIfNull(coveredConditionsByLine.get(lineId), 0);
+        Date date = datesByLine.get(lineId);
+        for (PeriodStruct struct : structs) {
+          struct.analyze(date, hits, conditions, coveredConditions);
+        }
+      }
+
+      return true;
+    }
+    return false;
+  }
+
+  private void reset() {
+    for (PeriodStruct struct : structs) {
+      struct.reset();
+    }
+  }
+
+  private void compute(DecoratorContext context) {
+    Measure newLines = new Measure(CoreMetrics.NEW_LINES_TO_COVER);
+    Measure newUncoveredLines = new Measure(CoreMetrics.NEW_UNCOVERED_LINES);
+    Measure newConditions = new Measure(CoreMetrics.NEW_CONDITIONS_TO_COVER);
+    Measure newUncoveredConditions = new Measure(CoreMetrics.NEW_UNCOVERED_CONDITIONS);
+
+    for (PeriodStruct struct : structs) {
+      newLines.setVariation(struct.index, (double)struct.newLines);
+      newUncoveredLines.setVariation(struct.index, (double) (struct.newLines - struct.newCoveredLines));
+      newConditions.setVariation(struct.index, (double)struct.newConditions);
+      newUncoveredConditions.setVariation(struct.index, (double)struct.newConditions-struct.newCoveredConditions);
+    }
+
+    context.saveMeasure(newLines);
+    context.saveMeasure(newUncoveredLines);
+    context.saveMeasure(newConditions);
+    context.saveMeasure(newUncoveredConditions);
+  }
+
+
+  private Map<Integer, Integer> parseCountByLine(Measure measure) {
+    if (measure != null && measure.hasData()) {
+      return KeyValueFormat.parseIntInt(measure.getData());
+    }
+    return Maps.newHashMap();
+  }
+
+
+  public static final class PeriodStruct {
+    int index;
+    Date date;
+    int newLines = 0, newCoveredLines = 0, newConditions = 0, newCoveredConditions = 0;
+
+    PeriodStruct(PastSnapshot pastSnapshot) {
+      this.index = pastSnapshot.getIndex();
+      this.date = pastSnapshot.getTargetDate();
+    }
+
+    PeriodStruct(int index, Date date) {
+      this.index = index;
+      this.date = date;
+    }
+
+    void reset() {
+      newLines = 0;
+      newCoveredLines = 0;
+      newConditions = 0;
+      newCoveredConditions = 0;
+    }
+
+    void analyze(Date lineDate, int hits, int conditions, int coveredConditions) {
+      if (lineDate == null) {
+        //TODO warning
+
+      } else if (lineDate.after(date)) {
+        // TODO test if string comparison is faster or not
+        addLine(hits > 0);
+        addConditions(conditions, coveredConditions);
+      }
+    }
+
+    void addLine(boolean covered) {
+      newLines += 1;
+      if (covered) {
+        newCoveredLines += 1;
+      }
+    }
+
+    void addConditions(int count, int countCovered) {
+      newConditions += count;
+      if (count > 0) {
+        newCoveredConditions += countCovered;
+      }
+    }
+  }
+}
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 (file)
index 0000000..8ca5e63
--- /dev/null
@@ -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 (file)
index 0000000..102db31
--- /dev/null
@@ -0,0 +1,39 @@
+<h2>New coverage</h2>
+  <table>
+    <tr>
+      <td>New lines</td>
+      <% m=measure('new_lines_to_cover') %>
+      <% 5.times do |index| %>
+      <td>
+        <%= format_variation(m, :index => index+1) -%>
+      </td>
+      <% end %>
+    </tr>
+    <tr>
+      <td>New uncovered</td>
+      <% m=measure('new_uncovered_lines') %>
+      <% 5.times do |index| %>
+      <td>
+        <%= format_variation(m, :index => index+1) -%>
+      </td>
+      <% end %>
+    </tr>
+    <tr>
+      <td>New conditions</td>
+      <% m=measure('new_conditions_to_cover') %>
+      <% 5.times do |index| %>
+      <td>
+        <%= format_variation(m, :index => index+1) -%>
+      </td>
+      <% end %>
+    </tr>
+    <tr>
+      <td>New uncovered conditions</td>
+      <% m=measure('new_uncovered_conditions') %>
+      <% 5.times do |index| %>
+      <td>
+        <%= format_variation(m, :index => index+1) -%>
+      </td>
+      <% end %>
+    </tr>
+  </table>
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/NewCoverageDecoratorTest.java
deleted file mode 100644 (file)
index e94cb5b..0000000
+++ /dev/null
@@ -1,183 +0,0 @@
-/*
- * 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.NumberUtils;
-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.Measure;
-import org.sonar.api.measures.Metric;
-import org.sonar.api.utils.DateUtils;
-
-import java.text.ParseException;
-import java.text.SimpleDateFormat;
-import java.util.Arrays;
-import java.util.Date;
-import java.util.List;
-
-import static org.mockito.Mockito.*;
-
-public class NewCoverageDecoratorTest {
-
-  @Test
-  public void shouldDoNothingIfNoScmData() throws ParseException {
-    DecoratorContext context = mock(DecoratorContext.class);
-    when(context.getMeasure(CoreMetrics.COVERAGE_LINE_HITS_DATA))
-        .thenReturn(new Measure(CoreMetrics.COVERAGE_LINE_HITS_DATA, "1=10"));
-
-    NewCoverageDecorator decorator = newDecorator();
-    decorator.doDecorate(context);
-    verify(context, never()).saveMeasure((Measure) anyObject());
-  }
-
-  @Test
-  public void shouldDoNothingIfNoCoverageData() throws ParseException {
-    DecoratorContext context = mock(DecoratorContext.class);
-    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();
-    decorator.doDecorate(context);
-
-    verify(context, never()).saveMeasure((Measure) anyObject());
-  }
-
-  @Test
-  public void shouldGetNewLines() throws ParseException {
-    DecoratorContext context = mock(DecoratorContext.class);
-    when(context.getMeasure(CoreMetrics.COVERAGE_LINE_HITS_DATA)).thenReturn(
-        new Measure(CoreMetrics.COVERAGE_LINE_HITS_DATA, "10=2;11=3"));
-    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();
-    decorator.doDecorate(context);
-
-    // line 11 has been updated after date1 (2009-12-25). This line is covered.
-    verify(context).saveMeasure(argThat(new VariationMatcher(CoreMetrics.NEW_LINES_TO_COVER, 1, 1.0)));
-    verify(context).saveMeasure(argThat(new VariationMatcher(CoreMetrics.NEW_UNCOVERED_LINES, 1, 0.0)));
-
-    // no line have been updated after date3 (2011-02-18)
-    verify(context).saveMeasure(argThat(new VariationMatcher(CoreMetrics.NEW_LINES_TO_COVER, 3, 0.0)));
-    verify(context).saveMeasure(argThat(new VariationMatcher(CoreMetrics.NEW_UNCOVERED_LINES, 3, 0.0)));
-
-    // no data on other periods
-    verify(context).saveMeasure(argThat(new VariationMatcher(CoreMetrics.NEW_LINES_TO_COVER, 2, null)));
-    verify(context).saveMeasure(argThat(new VariationMatcher(CoreMetrics.NEW_LINES_TO_COVER, 4, null)));
-    verify(context).saveMeasure(argThat(new VariationMatcher(CoreMetrics.NEW_LINES_TO_COVER, 5, null)));
-    verify(context).saveMeasure(argThat(new VariationMatcher(CoreMetrics.NEW_UNCOVERED_LINES, 2, null)));
-    verify(context).saveMeasure(argThat(new VariationMatcher(CoreMetrics.NEW_UNCOVERED_LINES, 4, null)));
-    verify(context).saveMeasure(argThat(new VariationMatcher(CoreMetrics.NEW_UNCOVERED_LINES, 5, null)));
-  }
-
-  @Test
-  public void shouldGetNewConditions() throws ParseException {
-    DecoratorContext context = mock(DecoratorContext.class);
-    when(context.getMeasure(CoreMetrics.COVERAGE_LINE_HITS_DATA)).thenReturn(
-        new Measure(CoreMetrics.COVERAGE_LINE_HITS_DATA, "10=2;11=3"));
-    when(context.getMeasure(CoreMetrics.CONDITIONS_BY_LINE)).thenReturn(
-        new Measure(CoreMetrics.CONDITIONS_BY_LINE, "11=4"));
-    when(context.getMeasure(CoreMetrics.COVERED_CONDITIONS_BY_LINE)).thenReturn(
-        new Measure(CoreMetrics.COVERED_CONDITIONS_BY_LINE, "11=1"));
-    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();
-    decorator.doDecorate(context);
-
-    // line 11 has been updated after date1 (2009-12-25). This line has 1 covered condition amongst 4
-    verify(context).saveMeasure(argThat(new VariationMatcher(CoreMetrics.NEW_CONDITIONS_TO_COVER, 1, 4.0)));
-    verify(context).saveMeasure(argThat(new VariationMatcher(CoreMetrics.NEW_UNCOVERED_CONDITIONS, 1, 3.0)));
-
-    // no line have been updated after date3 (2011-02-18)
-    verify(context).saveMeasure(argThat(new VariationMatcher(CoreMetrics.NEW_CONDITIONS_TO_COVER, 3, 0.0)));
-    verify(context).saveMeasure(argThat(new VariationMatcher(CoreMetrics.NEW_UNCOVERED_CONDITIONS, 3, 0.0)));
-
-    // no data on other periods
-    verify(context).saveMeasure(argThat(new VariationMatcher(CoreMetrics.NEW_CONDITIONS_TO_COVER, 2, null)));
-    verify(context).saveMeasure(argThat(new VariationMatcher(CoreMetrics.NEW_CONDITIONS_TO_COVER, 4, null)));
-    verify(context).saveMeasure(argThat(new VariationMatcher(CoreMetrics.NEW_CONDITIONS_TO_COVER, 5, null)));
-    verify(context).saveMeasure(argThat(new VariationMatcher(CoreMetrics.NEW_UNCOVERED_CONDITIONS, 2, null)));
-    verify(context).saveMeasure(argThat(new VariationMatcher(CoreMetrics.NEW_UNCOVERED_CONDITIONS, 4, null)));
-    verify(context).saveMeasure(argThat(new VariationMatcher(CoreMetrics.NEW_UNCOVERED_CONDITIONS, 5, null)));
-  }
-
-  @Test
-  public void shouldNotGetNewConditionsWhenNewLineHasNoConditions() throws ParseException {
-    DecoratorContext context = mock(DecoratorContext.class);
-    when(context.getMeasure(CoreMetrics.COVERAGE_LINE_HITS_DATA)).thenReturn(
-        new Measure(CoreMetrics.COVERAGE_LINE_HITS_DATA, "10=2;11=3"));
-    when(context.getMeasure(CoreMetrics.CONDITIONS_BY_LINE)).thenReturn(
-        new Measure(CoreMetrics.CONDITIONS_BY_LINE, "10=1"));
-    when(context.getMeasure(CoreMetrics.COVERED_CONDITIONS_BY_LINE)).thenReturn(
-        new Measure(CoreMetrics.COVERED_CONDITIONS_BY_LINE, "10=1"));
-    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();
-    decorator.doDecorate(context);
-
-    // line 11 has been updated after date1 (2009-12-25) but it has no conditions
-    verify(context).saveMeasure(argThat(new VariationMatcher(CoreMetrics.NEW_CONDITIONS_TO_COVER, 1, 0.0)));
-    verify(context).saveMeasure(argThat(new VariationMatcher(CoreMetrics.NEW_UNCOVERED_CONDITIONS, 1, 0.0)));
-  }
-
-
-  static class VariationMatcher extends BaseMatcher<Measure> {
-    Metric metric;
-    int index;
-    Double variation;
-
-    VariationMatcher(Metric metric, int index, Double variation) {
-      this.metric = metric;
-      this.index = index;
-      this.variation = variation;
-    }
-
-    public boolean matches(Object o) {
-      Measure m = (Measure)o;
-      if (m.getMetric().equals(metric)) {
-        if ((variation==null && m.getVariation(index)==null) ||
-            (variation!=null && variation.equals(m.getVariation(index)))) {
-          return true;
-        }
-      }
-      return false;
-    }
-
-    public void describeTo(Description description) {
-
-    }
-  }
-
-  private NewCoverageDecorator newDecorator() throws ParseException {
-    List<NewCoverageDecorator.PeriodStruct> structs = Arrays.asList(
-        new NewCoverageDecorator.PeriodStruct(1, newDate("2009-12-25")),
-        new NewCoverageDecorator.PeriodStruct(3, newDate("2011-02-18")));
-    return new NewCoverageDecorator(structs);
-  }
-
-  private Date newDate(String s) throws ParseException {
-    return new SimpleDateFormat(DateUtils.DATE_FORMAT).parse(s);
-  }
-}
diff --git a/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/timemachine/NewCoverageFileAnalyzerTest.java b/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/timemachine/NewCoverageFileAnalyzerTest.java
new file mode 100644 (file)
index 0000000..3eb6a0f
--- /dev/null
@@ -0,0 +1,182 @@
+/*
+ * 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.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.Measure;
+import org.sonar.api.measures.Metric;
+import org.sonar.api.utils.DateUtils;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+
+import static org.mockito.Mockito.*;
+
+public class NewCoverageFileAnalyzerTest {
+
+  @Test
+  public void shouldDoNothingIfNoScmData() throws ParseException {
+    DecoratorContext context = mock(DecoratorContext.class);
+    when(context.getMeasure(CoreMetrics.COVERAGE_LINE_HITS_DATA))
+        .thenReturn(new Measure(CoreMetrics.COVERAGE_LINE_HITS_DATA, "1=10"));
+
+    NewCoverageFileAnalyzer decorator = newDecorator();
+    decorator.doDecorate(context);
+    verify(context, never()).saveMeasure((Measure) anyObject());
+  }
+
+  @Test
+  public void shouldDoNothingIfNoCoverageData() throws ParseException {
+    DecoratorContext context = mock(DecoratorContext.class);
+    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"));
+
+    NewCoverageFileAnalyzer decorator = newDecorator();
+    decorator.doDecorate(context);
+
+    verify(context, never()).saveMeasure((Measure) anyObject());
+  }
+
+  @Test
+  public void shouldGetNewLines() throws ParseException {
+    DecoratorContext context = mock(DecoratorContext.class);
+    when(context.getMeasure(CoreMetrics.COVERAGE_LINE_HITS_DATA)).thenReturn(
+        new Measure(CoreMetrics.COVERAGE_LINE_HITS_DATA, "10=2;11=3"));
+    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"));
+
+    NewCoverageFileAnalyzer decorator = newDecorator();
+    decorator.doDecorate(context);
+
+    // line 11 has been updated after date1 (2009-12-25). This line is covered.
+    verify(context).saveMeasure(argThat(new VariationMatcher(CoreMetrics.NEW_LINES_TO_COVER, 1, 1.0)));
+    verify(context).saveMeasure(argThat(new VariationMatcher(CoreMetrics.NEW_UNCOVERED_LINES, 1, 0.0)));
+
+    // no line have been updated after date3 (2011-02-18)
+    verify(context).saveMeasure(argThat(new VariationMatcher(CoreMetrics.NEW_LINES_TO_COVER, 3, 0.0)));
+    verify(context).saveMeasure(argThat(new VariationMatcher(CoreMetrics.NEW_UNCOVERED_LINES, 3, 0.0)));
+
+    // no data on other periods
+    verify(context).saveMeasure(argThat(new VariationMatcher(CoreMetrics.NEW_LINES_TO_COVER, 2, null)));
+    verify(context).saveMeasure(argThat(new VariationMatcher(CoreMetrics.NEW_LINES_TO_COVER, 4, null)));
+    verify(context).saveMeasure(argThat(new VariationMatcher(CoreMetrics.NEW_LINES_TO_COVER, 5, null)));
+    verify(context).saveMeasure(argThat(new VariationMatcher(CoreMetrics.NEW_UNCOVERED_LINES, 2, null)));
+    verify(context).saveMeasure(argThat(new VariationMatcher(CoreMetrics.NEW_UNCOVERED_LINES, 4, null)));
+    verify(context).saveMeasure(argThat(new VariationMatcher(CoreMetrics.NEW_UNCOVERED_LINES, 5, null)));
+  }
+
+  @Test
+  public void shouldGetNewConditions() throws ParseException {
+    DecoratorContext context = mock(DecoratorContext.class);
+    when(context.getMeasure(CoreMetrics.COVERAGE_LINE_HITS_DATA)).thenReturn(
+        new Measure(CoreMetrics.COVERAGE_LINE_HITS_DATA, "10=2;11=3"));
+    when(context.getMeasure(CoreMetrics.CONDITIONS_BY_LINE)).thenReturn(
+        new Measure(CoreMetrics.CONDITIONS_BY_LINE, "11=4"));
+    when(context.getMeasure(CoreMetrics.COVERED_CONDITIONS_BY_LINE)).thenReturn(
+        new Measure(CoreMetrics.COVERED_CONDITIONS_BY_LINE, "11=1"));
+    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"));
+
+    NewCoverageFileAnalyzer decorator = newDecorator();
+    decorator.doDecorate(context);
+
+    // line 11 has been updated after date1 (2009-12-25). This line has 1 covered condition amongst 4
+    verify(context).saveMeasure(argThat(new VariationMatcher(CoreMetrics.NEW_CONDITIONS_TO_COVER, 1, 4.0)));
+    verify(context).saveMeasure(argThat(new VariationMatcher(CoreMetrics.NEW_UNCOVERED_CONDITIONS, 1, 3.0)));
+
+    // no line have been updated after date3 (2011-02-18)
+    verify(context).saveMeasure(argThat(new VariationMatcher(CoreMetrics.NEW_CONDITIONS_TO_COVER, 3, 0.0)));
+    verify(context).saveMeasure(argThat(new VariationMatcher(CoreMetrics.NEW_UNCOVERED_CONDITIONS, 3, 0.0)));
+
+    // no data on other periods
+    verify(context).saveMeasure(argThat(new VariationMatcher(CoreMetrics.NEW_CONDITIONS_TO_COVER, 2, null)));
+    verify(context).saveMeasure(argThat(new VariationMatcher(CoreMetrics.NEW_CONDITIONS_TO_COVER, 4, null)));
+    verify(context).saveMeasure(argThat(new VariationMatcher(CoreMetrics.NEW_CONDITIONS_TO_COVER, 5, null)));
+    verify(context).saveMeasure(argThat(new VariationMatcher(CoreMetrics.NEW_UNCOVERED_CONDITIONS, 2, null)));
+    verify(context).saveMeasure(argThat(new VariationMatcher(CoreMetrics.NEW_UNCOVERED_CONDITIONS, 4, null)));
+    verify(context).saveMeasure(argThat(new VariationMatcher(CoreMetrics.NEW_UNCOVERED_CONDITIONS, 5, null)));
+  }
+
+  @Test
+  public void shouldNotGetNewConditionsWhenNewLineHasNoConditions() throws ParseException {
+    DecoratorContext context = mock(DecoratorContext.class);
+    when(context.getMeasure(CoreMetrics.COVERAGE_LINE_HITS_DATA)).thenReturn(
+        new Measure(CoreMetrics.COVERAGE_LINE_HITS_DATA, "10=2;11=3"));
+    when(context.getMeasure(CoreMetrics.CONDITIONS_BY_LINE)).thenReturn(
+        new Measure(CoreMetrics.CONDITIONS_BY_LINE, "10=1"));
+    when(context.getMeasure(CoreMetrics.COVERED_CONDITIONS_BY_LINE)).thenReturn(
+        new Measure(CoreMetrics.COVERED_CONDITIONS_BY_LINE, "10=1"));
+    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"));
+
+    NewCoverageFileAnalyzer decorator = newDecorator();
+    decorator.doDecorate(context);
+
+    // line 11 has been updated after date1 (2009-12-25) but it has no conditions
+    verify(context).saveMeasure(argThat(new VariationMatcher(CoreMetrics.NEW_CONDITIONS_TO_COVER, 1, 0.0)));
+    verify(context).saveMeasure(argThat(new VariationMatcher(CoreMetrics.NEW_UNCOVERED_CONDITIONS, 1, 0.0)));
+  }
+
+
+  static class VariationMatcher extends BaseMatcher<Measure> {
+    Metric metric;
+    int index;
+    Double variation;
+
+    VariationMatcher(Metric metric, int index, Double variation) {
+      this.metric = metric;
+      this.index = index;
+      this.variation = variation;
+    }
+
+    public boolean matches(Object o) {
+      Measure m = (Measure)o;
+      if (m.getMetric().equals(metric)) {
+        if ((variation==null && m.getVariation(index)==null) ||
+            (variation!=null && variation.equals(m.getVariation(index)))) {
+          return true;
+        }
+      }
+      return false;
+    }
+
+    public void describeTo(Description description) {
+
+    }
+  }
+
+  private NewCoverageFileAnalyzer newDecorator() throws ParseException {
+    List<NewCoverageFileAnalyzer.PeriodStruct> 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 {
+    return new SimpleDateFormat(DateUtils.DATE_FORMAT).parse(s);
+  }
+}
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 (file)
index 0000000..5ae0ae6
--- /dev/null
@@ -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<<line
+
+        line.revision=@revisions_by_line[index+1]
+        line.author=@authors_by_line[index+1]
+        line.date=@dates_by_line[index+1]
+
+        if line.revision
+          if current_revision!=line.revision
+            current_revision=line.revision
+            line.display_scm=true
+          end
+        end
+      end
+     end
+
+    if (params[:tab]=='violations')
+      load_violations_tab()
+    elsif (params[:tab]=='coverage')
+      load_coverage_tab()
+    else
+      load_source_tab()
+    end
+
+    params[:layout]='false'
+  end
+
+  def load_scm()
+    @scm_available=(@snapshot.measure('last_commit_datetimes_by_line')!=nil)
+    @display_scm=(params[:scm]=='true')
+    if @display_scm
+      @authors_by_line=load_distribution('authors_by_line')
+      @revisions_by_line=load_distribution('revisions_by_line')
+      @dates_by_line=load_distribution('last_commit_datetimes_by_line')
+    else
+      @authors_by_line={}
+      @revisions_by_line={}
+      @dates_by_line={}
+    end
+  end
+
+  def load_distribution(metric_key)
+    m=@snapshot.measure(metric_key)
+    m ? m.data_as_line_distribution() : {}
+  end
+
+  def load_coverage_tab
+    @display_coverage=true
+    @hits_by_line=load_distribution('coverage_line_hits_data')
+    @conditions_by_line=load_distribution('conditions_by_line')
+    @covered_conditions_by_line=load_distribution('covered_conditions_by_line')
+
+    @hits_by_line.each_pair do |line_id,hits|
+      line=@lines[line_id-1]
+      if line
+        line.hits=hits.to_i
+        line.conditions=@conditions_by_line[line_id].to_i
+        line.covered_conditions=@covered_conditions_by_line[line_id].to_i
+      end
+    end
+  end
+
+  def load_violations_tab
+    @display_violations=true
+    @global_violations=[]
+
+    conditions='snapshot_id=?'
+    values=[@snapshot.id]
+    unless params[:rule].blank?
+      if params[:rule].include?(':')
+        rule=Rule.by_key_or_id(params[:rule])
+        conditions += ' AND rule_id=?'
+        values<<(rule ? rule.id : -1)
+      else
+        # severity
+        conditions += ' AND failure_level=?'
+        values<<params[:rule].to_i
+      end
+    end
+
+    unless params[:date].blank?
+      conditions+=' AND created_at>=?'
+      values<<Date::strptime(params[:date])
+    end
+
+    RuleFailure.find(:all, :include => '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<<violation
+      end
+    end
+  end
+
+  def load_source_tab
+
+  end
+
+  class Line
+    attr_accessor :source, :revision, :author, :date, :display_scm, :violations, :hits, :conditions, :covered_conditions
+
+    def initialize(source)
+      @source=source
+      @display_scm=false
+    end
+
+    def add_violation(violation)
+      @violations||=[]
+      @violations<<violation
+      @visible=true
+    end
+
+    def violations?
+      @violations && @violations.size>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 (file)
index 0000000..e0803a8
--- /dev/null
@@ -0,0 +1,5 @@
+<img src="<%= ApplicationController.root_context -%>/images/priority/<%=violation.failure_level-%>.png"/>
+
+<span class="rulename"><a onclick="window.open(this.href,'rule','height=800,width=900,scrollbars=1,resizable=1');return false;" href="<%= url_for :controller => 'rules', :action => 'show', :id => violation.rule.key, :layout => 'false' -%>"><%= h(violation.rule.name) -%></a></span>
+ ยป
+<%= 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 (file)
index 0000000..d05ecd6
--- /dev/null
@@ -0,0 +1,198 @@
+<style>
+#source_metrics {
+  width: 100%;
+}
+#source_metrics {
+  width: 100%;
+}
+.sources2 {
+  width: 100%;
+  border: 1px solid #DDD;
+  margin: 0;
+}
+.sources2 td.lid {
+  background-color: #ECECEC;
+  border-right: 1px solid #DDDDDD;
+  border-left: 1px solid #DDDDDD;
+  text-align: right;
+  padding: 2px 0.5em 0 0.5em;
+  vertical-align: top;
+  font-size: 11px;
+}
+.sources2 td.lid a {
+  text-decoration: none;
+  color: #AAA;
+}
+.sources2 td.scm {
+  border-right: 1px solid #DDD;
+  background-color: #ECECEC;
+}
+.sources2 td.revision {
+  border-top: 1px solid #DDD;
+  vertical-align: top;
+  padding: 2px 0.3em;
+  white-space: nowrap;
+}
+.sources2 span.date, .sources2 span.date a {
+  color: #AAA;
+  font-size: 11px;
+  text-decoration: none;
+}
+.sources2 span.author, .sources2 span.author a {
+  font-size: 11px;
+}
+.sources2 td.rule {
+  padding: 1px 0.5em;
+}
+.sources2 td.violations {
+  background-color: #ECECEC;
+  min-width: 400px;
+}
+span.rulename, span.rulename a {
+  color: #4183C4;
+  text-decoration: none;
+}
+span.rulename a:hover {
+  text-decoration: underline;
+}
+.sources2 td.violations img {
+  vertical-align: sub;
+}
+.sources2 td.line {
+  padding-left: 1em;
+  width: 100%;
+  white-space: pre;
+  font-size: 12px;
+  font-family: monospace;
+}
+.sources2 td.section {
+  border-top: 1px solid #DDD;
+  border-bottom: 1px solid #DDD;
+}
+.sources2 td.ind {
+  border-right: 1px solid #DDD;
+  min-width: 1.5em;
+  padding: 0 0.3em;
+  text-align: center;
+  vertical-align: middle;
+}
+.sources2 td.ok {
+  background-color: #ACE97C;
+  border-top: 1px solid #6EC563;
+  border-bottom: 1px solid #6EC563;
+}
+.sources2 td.sev0, .sources2 td.sev1, .sources2 td.warn {
+  /* info, minor */
+  background-color: #FFF6BF;
+  border-top: 1px solid #FFD324;
+  border-bottom: 1px solid #FFD324;
+}
+.sources2 td.sev2, .sources2 td.sev3, .sources2 td.sev4, .sources2 td.ko {
+  /* major, critical, blocker */
+  background-color: #FF9090;
+  border-top: 1px solid #FF5252;
+  border-bottom: 1px solid #FF5252;
+}
+
+#source_title {
+  padding: 10px 0;
+}
+#source_title span.h1 {
+  font-size: 16px;
+  margin-right: 10px;
+}
+.source_links {
+  font-size: 11px;
+}
+#global_violations {
+  width: 100%;
+  border: 1px solid #DDD;
+  margin-bottom: 10px;
+}
+#global_violations td {
+  background-color: #ECECEC;
+  padding: 3px 0.5em;
+}
+#global_violations td img, #source_title img {
+  vertical-align: sub;
+}
+</style>
+
+<div id="source_title">
+  <span class="h1"><%= qualifier_icon(@resource) -%> <%= @resource.long_name -%></span>
+  <% if @lines  %>
+    | <span class="source_link"><a href="<%= ApplicationController.root_context -%>/api/sources?resource=<%= @resource.key -%>&format=txt" target="raw">raw</a></span>
+  <% end %>
+</div>
+<table id="source_metrics" cellpadding="0" cellspacing="0" border="0">
+
+</table>
+
+<% if @display_violations && @global_violations && @global_violations.size>0 -%>
+  <table id="global_violations" cellpadding="0" cellspacing="0" border="0">
+    <% @global_violations.each do |violation| %>
+      <tr>
+        <td><%= render :partial => 'violation', :locals => {:violation => violation} -%></td>
+      </tr>
+    <% end %>
+  </table>
+<% end %>
+
+<% if @lines && @lines.size>0 %>
+<table id="sources" class="sources2 code" cellpadding="0" cellspacing="0" border="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
+  %>
+  <tr>
+    <% if @display_scm
+         if line.display_scm
+           title = "Revision #{h(line.revision)} (#{line.date})"
+    %>
+            <td class="scm revision"><span class="date"><a href="#" title="<%= title -%>" alt="<%= title -%>"><%= line.date[0..9] -%></a></span> <span class="author"><%= h(line.author) -%></span></td>
+    <%   else %>
+            <td class="scm"></td>
+    <%   end
+       end %>
+    <% if @display_violations %>
+      <td class="rule <%= 'violations section' if line.violations? -%>">
+        <% if line.violations?
+             line.violations.each_with_index do |violation, violation_index| %>
+               <%= '<br/>' if violation_index>0 %>
+               <%= render :partial => 'violation', :locals => {:violation => violation} -%>
+        <%
+             end
+           end
+        %>
+      </td>
+    <% end %>
+    <td class="lid <%= ' section' if line.violations? -%>" id="L<%= index+1 -%>"><a name="L<%= index+1 -%>" href="#L<%= index+1 -%>"><%= index + 1 -%></a></td>
+
+    <% if @display_coverage  %>
+      <td class="ind <%= hits_status -%>"><%= line.hits -%></td>
+      <td class="ind <%= conditions_status -%>"><% if line.conditions && line.conditions>0 %><%= line.covered_conditions -%>/<%= line.conditions -%><% end %></td>
+    <% end %>
+    <td class="line <%= status -%>"><%= line.source -%></td>
+  </tr>
+  <% end %>
+</table>
+<% end %>
\ No newline at end of file