]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-5876 add SqaleNewMeasuresVisitor to compute new_sqale_debt_ratio 520/head
authorSébastien Lesaint <sebastien.lesaint@sonarsource.com>
Mon, 21 Sep 2015 08:53:59 +0000 (10:53 +0200)
committerSébastien Lesaint <sebastien.lesaint@sonarsource.com>
Tue, 22 Sep 2015 15:20:56 +0000 (17:20 +0200)
19 files changed:
it/it-projects/measure/xoo-new-debt-ratio-v1/sonar-project.properties [new file with mode: 0644]
it/it-projects/measure/xoo-new-debt-ratio-v1/src/main/xoo/sample/Sample.xoo [new file with mode: 0644]
it/it-projects/measure/xoo-new-debt-ratio-v1/src/main/xoo/sample/Sample.xoo.measures [new file with mode: 0644]
it/it-projects/measure/xoo-new-debt-ratio-v1/src/main/xoo/sample/Sample.xoo.scm [new file with mode: 0644]
it/it-projects/measure/xoo-new-debt-ratio-v2/sonar-project.properties [new file with mode: 0644]
it/it-projects/measure/xoo-new-debt-ratio-v2/src/main/xoo/sample/Sample.xoo [new file with mode: 0644]
it/it-projects/measure/xoo-new-debt-ratio-v2/src/main/xoo/sample/Sample.xoo.measures [new file with mode: 0644]
it/it-projects/measure/xoo-new-debt-ratio-v2/src/main/xoo/sample/Sample.xoo.scm [new file with mode: 0644]
it/it-projects/measure/xoo-new-debt-ratio-v3/sonar-project.properties [new file with mode: 0644]
it/it-projects/measure/xoo-new-debt-ratio-v3/src/main/xoo/sample/Sample.xoo [new file with mode: 0644]
it/it-projects/measure/xoo-new-debt-ratio-v3/src/main/xoo/sample/Sample.xoo.measures [new file with mode: 0644]
it/it-projects/measure/xoo-new-debt-ratio-v3/src/main/xoo/sample/Sample.xoo.scm [new file with mode: 0644]
it/it-tests/src/test/java/analysis/suite/AnalysisTestSuite.java
it/it-tests/src/test/java/analysis/suite/measure/NewDebtRatioMeasureTest.java [new file with mode: 0644]
it/it-tests/src/test/java/util/ItUtils.java
server/sonar-server/src/main/java/org/sonar/server/computation/container/ReportComputeEngineContainerPopulator.java
server/sonar-server/src/main/java/org/sonar/server/computation/sqale/SqaleMeasuresVisitor.java
server/sonar-server/src/main/java/org/sonar/server/computation/sqale/SqaleNewMeasuresVisitor.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/computation/sqale/SqaleNewMeasuresVisitorTest.java [new file with mode: 0644]

diff --git a/it/it-projects/measure/xoo-new-debt-ratio-v1/sonar-project.properties b/it/it-projects/measure/xoo-new-debt-ratio-v1/sonar-project.properties
new file mode 100644 (file)
index 0000000..4ea4c91
--- /dev/null
@@ -0,0 +1,6 @@
+sonar.projectKey=sample
+sonar.projectName=Sample
+sonar.projectVersion=1.0-SNAPSHOT
+sonar.sources=src/main/xoo
+sonar.language=xoo
+sonar.scm.provider=xoo
diff --git a/it/it-projects/measure/xoo-new-debt-ratio-v1/src/main/xoo/sample/Sample.xoo b/it/it-projects/measure/xoo-new-debt-ratio-v1/src/main/xoo/sample/Sample.xoo
new file mode 100644 (file)
index 0000000..467e82d
--- /dev/null
@@ -0,0 +1,13 @@
+package sample;
+
+// class comment
+public class Sample {
+       
+       public Sample(int i) {
+               int j = i++;
+       }
+       
+       private String method1() {
+         return "hello";
+       }
+}
diff --git a/it/it-projects/measure/xoo-new-debt-ratio-v1/src/main/xoo/sample/Sample.xoo.measures b/it/it-projects/measure/xoo-new-debt-ratio-v1/src/main/xoo/sample/Sample.xoo.measures
new file mode 100644 (file)
index 0000000..874fe7b
--- /dev/null
@@ -0,0 +1,4 @@
+ncloc:9
+comment_lines:5
+ncloc_data:1=1;2=0;3=0;4=1;5=0;6=1;7=1;8=1;9=0;10=1;11=1;12=1;13=1;14=0
+classes:1
diff --git a/it/it-projects/measure/xoo-new-debt-ratio-v1/src/main/xoo/sample/Sample.xoo.scm b/it/it-projects/measure/xoo-new-debt-ratio-v1/src/main/xoo/sample/Sample.xoo.scm
new file mode 100644 (file)
index 0000000..9f1974d
--- /dev/null
@@ -0,0 +1,14 @@
+1,user1,2015-08-01
+1,user1,2015-08-01
+1,user1,2015-08-01
+1,user1,2015-08-01
+1,user1,2015-08-01
+1,user1,2015-08-01
+1,user1,2015-08-01
+1,user1,2015-08-01
+1,user1,2015-08-01
+1,user1,2015-08-01
+1,user1,2015-08-01
+1,user1,2015-08-01
+1,user1,2015-08-01
+1,user1,2015-08-01
diff --git a/it/it-projects/measure/xoo-new-debt-ratio-v2/sonar-project.properties b/it/it-projects/measure/xoo-new-debt-ratio-v2/sonar-project.properties
new file mode 100644 (file)
index 0000000..4ea4c91
--- /dev/null
@@ -0,0 +1,6 @@
+sonar.projectKey=sample
+sonar.projectName=Sample
+sonar.projectVersion=1.0-SNAPSHOT
+sonar.sources=src/main/xoo
+sonar.language=xoo
+sonar.scm.provider=xoo
diff --git a/it/it-projects/measure/xoo-new-debt-ratio-v2/src/main/xoo/sample/Sample.xoo b/it/it-projects/measure/xoo-new-debt-ratio-v2/src/main/xoo/sample/Sample.xoo
new file mode 100644 (file)
index 0000000..8827933
--- /dev/null
@@ -0,0 +1,17 @@
+package sample;
+
+// class comment
+public class Sample {
+       
+       public Sample(int i) {
+               int j = i++;
+       }
+       
+       private String method1() {
+         return "hello";
+       }
+
+       private String method2() {
+         return "hello2";
+       }
+}
diff --git a/it/it-projects/measure/xoo-new-debt-ratio-v2/src/main/xoo/sample/Sample.xoo.measures b/it/it-projects/measure/xoo-new-debt-ratio-v2/src/main/xoo/sample/Sample.xoo.measures
new file mode 100644 (file)
index 0000000..98f63c1
--- /dev/null
@@ -0,0 +1,4 @@
+ncloc:12
+comment_lines:6
+ncloc_data:1=1;2=0;3=0;4=1;5=0;6=1;7=1;8=1;9=0;10=1;11=1;12=1;13=0;14=1;15=1;16=1;17=1;18=0
+classes:1
diff --git a/it/it-projects/measure/xoo-new-debt-ratio-v2/src/main/xoo/sample/Sample.xoo.scm b/it/it-projects/measure/xoo-new-debt-ratio-v2/src/main/xoo/sample/Sample.xoo.scm
new file mode 100644 (file)
index 0000000..280b7d6
--- /dev/null
@@ -0,0 +1,18 @@
+1,user1,2015-09-01
+1,user1,2015-09-01
+1,user1,2015-09-01
+1,user1,2015-09-01
+1,user1,2015-09-01
+1,user1,2015-09-01
+1,user1,2015-09-01
+1,user1,2015-09-01
+1,user1,2015-09-01
+1,user1,2015-09-01
+1,user1,2015-09-01
+1,user1,2015-09-01
+2,user2,2015-09-17
+2,user2,2015-09-17
+2,user2,2015-09-17
+2,user2,2015-09-17
+1,user1,2015-09-01
+1,user1,2015-09-01
diff --git a/it/it-projects/measure/xoo-new-debt-ratio-v3/sonar-project.properties b/it/it-projects/measure/xoo-new-debt-ratio-v3/sonar-project.properties
new file mode 100644 (file)
index 0000000..4ea4c91
--- /dev/null
@@ -0,0 +1,6 @@
+sonar.projectKey=sample
+sonar.projectName=Sample
+sonar.projectVersion=1.0-SNAPSHOT
+sonar.sources=src/main/xoo
+sonar.language=xoo
+sonar.scm.provider=xoo
diff --git a/it/it-projects/measure/xoo-new-debt-ratio-v3/src/main/xoo/sample/Sample.xoo b/it/it-projects/measure/xoo-new-debt-ratio-v3/src/main/xoo/sample/Sample.xoo
new file mode 100644 (file)
index 0000000..0b9e023
--- /dev/null
@@ -0,0 +1,22 @@
+package sample;
+
+// class comment
+public class Sample {
+       
+       public Sample(int i) {
+               int j = i++;
+       }
+       
+       private String method1() {
+         return "hello";
+       }
+
+       private String method2() {
+         return "hello2";
+       }
+
+       private String method3() {
+         String e = "hello3";
+         return e;
+       }
+}
diff --git a/it/it-projects/measure/xoo-new-debt-ratio-v3/src/main/xoo/sample/Sample.xoo.measures b/it/it-projects/measure/xoo-new-debt-ratio-v3/src/main/xoo/sample/Sample.xoo.measures
new file mode 100644 (file)
index 0000000..d467f57
--- /dev/null
@@ -0,0 +1,4 @@
+ncloc:16
+comment_lines:7
+ncloc_data:1=1;2=0;3=0;4=1;5=0;6=1;7=1;8=1;9=0;10=1;11=1;12=1;13=0;14=1;15=1;16=1;17=0;18=1;19=1;20=1;21=1;22=1;23=0
+classes:1
diff --git a/it/it-projects/measure/xoo-new-debt-ratio-v3/src/main/xoo/sample/Sample.xoo.scm b/it/it-projects/measure/xoo-new-debt-ratio-v3/src/main/xoo/sample/Sample.xoo.scm
new file mode 100644 (file)
index 0000000..4b429f3
--- /dev/null
@@ -0,0 +1,23 @@
+1,user1,2015-09-01
+1,user1,2015-09-01
+1,user1,2015-09-01
+1,user1,2015-09-01
+1,user1,2015-09-01
+1,user1,2015-09-01
+1,user1,2015-09-01
+1,user1,2015-09-01
+1,user1,2015-09-01
+1,user1,2015-09-01
+1,user1,2015-09-01
+1,user1,2015-09-01
+2,user2,2015-09-17
+2,user2,2015-09-17
+2,user2,2015-09-17
+2,user2,2015-09-17
+3,user2,2015-09-20
+3,user2,2015-09-20
+3,user2,2015-09-20
+3,user2,2015-09-20
+3,user2,2015-09-20
+1,user1,2015-09-01
+1,user1,2015-09-01
index ef43f3e535a56e697dd9920af748f996b1fa1ac9..396e03981cbae33672be5d4eee152978844476ab 100644 (file)
@@ -22,6 +22,7 @@ package analysis.suite;
 import analysis.suite.measure.CustomMeasuresTest;
 import analysis.suite.measure.DifferentialPeriodsTest;
 import analysis.suite.measure.MeasureFiltersTest;
+import analysis.suite.measure.NewDebtRatioMeasureTest;
 import analysis.suite.measure.TechnicalDebtMeasureVariationTest;
 import analysis.suite.measure.TimeMachineTest;
 import analysis.suite.testing.CoverageTest;
@@ -44,7 +45,8 @@ import util.ItUtils;
   CoverageTest.class,
   NewCoverageTest.class,
   TestExecutionTest.class,
-  TechnicalDebtMeasureVariationTest.class
+  TechnicalDebtMeasureVariationTest.class,
+  NewDebtRatioMeasureTest.class
 })
 public class AnalysisTestSuite {
 
diff --git a/it/it-tests/src/test/java/analysis/suite/measure/NewDebtRatioMeasureTest.java b/it/it-tests/src/test/java/analysis/suite/measure/NewDebtRatioMeasureTest.java
new file mode 100644 (file)
index 0000000..40d6c62
--- /dev/null
@@ -0,0 +1,96 @@
+package analysis.suite.measure;
+
+import analysis.suite.AnalysisTestSuite;
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.locator.FileLocation;
+import java.util.List;
+import javax.annotation.Nullable;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.sonar.wsclient.services.Measure;
+import org.sonar.wsclient.services.Resource;
+import org.sonar.wsclient.services.ResourceQuery;
+import util.ItUtils;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.within;
+
+/**
+ * SONAR-5876
+ */
+public class NewDebtRatioMeasureTest {
+
+  private static final String NEW_DEBT_RATIO_METRIC_KEY = "new_sqale_debt_ratio";
+
+  @ClassRule
+  public static Orchestrator orchestrator = AnalysisTestSuite.ORCHESTRATOR;
+
+  @Before
+  public void cleanUpAnalysisData() {
+    orchestrator.resetData();
+  }
+
+  @Test
+  public void new_debt_ratio_is_computed_from_nes_debt_and_new_ncloc_count_per_file() throws Exception {
+    // This test assumes that period 1 is "since previous analysis" and 2 is "over 30 days"
+
+    // run analysis on the day of after the first commit (2015-09-01), with 'one-issue-per-line' profile
+    // => some issues at date 2015-09-02
+    defineQualityProfile("one-issue-per-line");
+    provisionSampleProject();
+    setSampleProjectQualityProfile("one-issue-per-line");
+    runSampleProjectAnalysis("v1", "sonar.projectDate", "2015-09-02");
+
+    // first analysis, no previous snapshot => periods not resolved => no value
+    assertNoNewDebtRatio();
+
+    // run analysis on the day after of second commit (2015-09-17) 'one-issue-per-line' profile*
+    // => 3 new issues will be created at date 2015-09-18
+    runSampleProjectAnalysis("v2", "sonar.projectDate", "2015-09-18");
+    assertNewDebtRatio(4.44, 4.44);
+
+    // run analysis on the day after of third commit (2015-09-20) 'one-issue-per-line' profile*
+    // => 4 new issues will be created at date 2015-09-21
+    runSampleProjectAnalysis("v3", "sonar.projectDate", "2015-09-21");
+    assertNewDebtRatio(4.17, 4.28);
+  }
+
+  private void assertNoNewDebtRatio() {
+    assertThat(getFileResourceWithVariations(NEW_DEBT_RATIO_METRIC_KEY)).isNull();
+  }
+
+  private void assertNewDebtRatio(@Nullable Double valuePeriod1, @Nullable Double valuePeriod2) {
+    Resource newTechnicalDebt = getFileResourceWithVariations(NEW_DEBT_RATIO_METRIC_KEY);
+    List<Measure> measures = newTechnicalDebt.getMeasures();
+    assertThat(measures.get(0).getVariation1()).isEqualTo(valuePeriod1, within(0.01));
+    assertThat(measures.get(0).getVariation2()).isEqualTo(valuePeriod2, within(0.01));
+  }
+
+  private void setSampleProjectQualityProfile(String qualityProfileKey) {
+    orchestrator.getServer().associateProjectToQualityProfile("sample", "xoo", qualityProfileKey);
+  }
+
+  private void provisionSampleProject() {
+    orchestrator.getServer().provisionProject("sample", "sample");
+  }
+
+  private void defineQualityProfile(String qualityProfileKey) {
+    orchestrator.getServer().restoreProfile(FileLocation.ofClasspath("/measure/suite/" + qualityProfileKey + ".xml"));
+  }
+
+  private void runSampleProjectAnalysis(String projectVersion, String... properties) {
+    ItUtils.runProjectAnalysis(
+      NewDebtRatioMeasureTest.orchestrator,
+      "measure/xoo-new-debt-ratio-" + projectVersion,
+      ItUtils.concat(properties,
+        // disable standard scm support so that it does not interfere with Xoo Scm sensor
+        "sonar.scm.disabled", "false")
+      );
+  }
+
+  private Resource getFileResourceWithVariations(String metricKey) {
+    return orchestrator.getServer().getWsClient().find(ResourceQuery.createForMetrics("sample:src/main/xoo/sample/Sample.xoo", metricKey).setIncludeTrends(true));
+  }
+
+}
index 4e299355a7536cbbd5e25b3d96bb135a659281ac..1d8e3406ea804b60d3583090a1ba1686ec0c84c3 100644 (file)
@@ -5,6 +5,7 @@ package util;/*
  */
 
 import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
 import com.sonar.orchestrator.Orchestrator;
 import com.sonar.orchestrator.build.BuildResult;
 import com.sonar.orchestrator.build.SonarRunner;
@@ -20,6 +21,8 @@ import java.io.IOException;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
+import static com.google.common.collect.FluentIterable.from;
+import static java.util.Arrays.asList;
 import static org.assertj.core.api.Assertions.fail;
 
 import static org.assertj.core.api.Assertions.assertThat;
@@ -132,10 +135,26 @@ public class ItUtils {
   public static void runProjectAnalysis(Orchestrator orchestrator, String projectRelativePath, String... properties) {
     SonarRunner sonarRunner = SonarRunner.create(projectDir(projectRelativePath));
     ImmutableMap.Builder<String, String> builder = ImmutableMap.builder();
-    for (int i = 0; i < properties.length; i++) {
+    for (int i = 0; i < properties.length; i+=2) {
       builder.put(properties[i], properties[i+1]);
-      i+=2;
     }
-    orchestrator.executeBuild(sonarRunner.setProperties(builder.build()));
+    orchestrator.executeBuild(sonarRunner.setDebugLogs(true).setProperties(builder.build()));
+  }
+
+  /**
+   * Concatenates a vararg to a String array.
+   *
+   * Useful when using {@link #runProjectAnalysis(Orchestrator, String, String...)}, eg.:
+   * <pre>
+   * ItUtils.runProjectAnalysis(orchestrator, "project_name",
+   *    ItUtils.concat(properties, "sonar.scm.disabled", "false")
+   *    );
+   * </pre>
+   */
+  public static String[] concat(String[] properties, String... str) {
+    if (properties == null || properties.length == 0) {
+      return str;
+    }
+    return from(Iterables.concat(asList(properties), asList(str))).toArray(String.class);
   }
 }
index 8ed0f9a2f1cd2470d9b1ca45e8fdadb96a933a76..cc8f01c859cf6309f3f55a4b3c33f7c13867b824 100644 (file)
@@ -80,6 +80,7 @@ import org.sonar.server.computation.source.LastCommitVisitor;
 import org.sonar.server.computation.source.SourceLinesRepositoryImpl;
 import org.sonar.server.computation.sqale.SqaleMeasuresVisitor;
 import org.sonar.server.computation.sqale.SqaleRatingSettings;
+import org.sonar.server.computation.sqale.SqaleNewMeasuresVisitor;
 import org.sonar.server.computation.step.ComputationSteps;
 import org.sonar.server.computation.step.ReportComputationSteps;
 import org.sonar.server.view.index.ViewIndex;
@@ -169,6 +170,7 @@ public final class ReportComputeEngineContainerPopulator implements ContainerPop
       IntegrateIssuesVisitor.class,
       CloseIssuesOnRemovedComponentsVisitor.class,
       SqaleMeasuresVisitor.class,
+      SqaleNewMeasuresVisitor.class,
       LastCommitVisitor.class,
       MeasureComputersVisitor.class,
 
index ab62abd339229b1d07b2ae783d0d1403c1873b38..2e1171d0148e1dcb9cc0386ccc7535068935c30b 100644 (file)
@@ -35,7 +35,7 @@ import org.sonar.server.computation.metric.MetricRepository;
 import static org.sonar.server.computation.component.ComponentVisitor.Order.POST_ORDER;
 import static org.sonar.server.computation.measure.Measure.newMeasureBuilder;
 
-public class SqaleMeasuresVisitor extends PathAwareVisitorAdapter<SqaleMeasuresVisitor.DevelopmentCost> {
+public class SqaleMeasuresVisitor extends PathAwareVisitorAdapter<SqaleMeasuresVisitor.DevelopmentCostCounter> {
   private static final Logger LOG = Loggers.get(SqaleMeasuresVisitor.class);
 
   private final MetricRepository metricRepository;
@@ -48,18 +48,7 @@ public class SqaleMeasuresVisitor extends PathAwareVisitorAdapter<SqaleMeasuresV
   private final Metric sqaleRatingMetric;
 
   public SqaleMeasuresVisitor(MetricRepository metricRepository, MeasureRepository measureRepository, SqaleRatingSettings sqaleRatingSettings) {
-    super(CrawlerDepthLimit.LEAVES, POST_ORDER, new SimpleStackElementFactory<DevelopmentCost>() {
-      @Override
-      public DevelopmentCost createForAny(Component component) {
-        return new DevelopmentCost();
-      }
-
-      /** Counter is not used at ProjectView level, saves on instantiating useless objects */
-      @Override
-      public DevelopmentCost createForProjectView(Component projectView) {
-        return null;
-      }
-    });
+    super(CrawlerDepthLimit.LEAVES, POST_ORDER, DevelopmentCostCounterFactory.INSTANCE);
     this.metricRepository = metricRepository;
     this.measureRepository = measureRepository;
     this.sqaleRatingSettings = sqaleRatingSettings;
@@ -71,22 +60,22 @@ public class SqaleMeasuresVisitor extends PathAwareVisitorAdapter<SqaleMeasuresV
   }
 
   @Override
-  public void visitProject(Component project, Path<DevelopmentCost> path) {
+  public void visitProject(Component project, Path<DevelopmentCostCounter> path) {
     computeAndSaveMeasures(project, path);
   }
 
   @Override
-  public void visitDirectory(Component directory, Path<DevelopmentCost> path) {
+  public void visitDirectory(Component directory, Path<DevelopmentCostCounter> path) {
     computeAndSaveMeasures(directory, path);
   }
 
   @Override
-  public void visitModule(Component module, Path<DevelopmentCost> path) {
+  public void visitModule(Component module, Path<DevelopmentCostCounter> path) {
     computeAndSaveMeasures(module, path);
   }
 
   @Override
-  public void visitFile(Component file, Path<DevelopmentCost> path) {
+  public void visitFile(Component file, Path<DevelopmentCostCounter> path) {
     if (!file.getFileAttributes().isUnitTest()) {
       long developmentCosts = computeDevelopmentCost(file);
       path.current().add(developmentCosts);
@@ -95,17 +84,17 @@ public class SqaleMeasuresVisitor extends PathAwareVisitorAdapter<SqaleMeasuresV
   }
 
   @Override
-  public void visitView(Component view, Path<DevelopmentCost> path) {
+  public void visitView(Component view, Path<DevelopmentCostCounter> path) {
     computeAndSaveMeasures(view, path);
   }
 
   @Override
-  public void visitSubView(Component subView, Path<DevelopmentCost> path) {
+  public void visitSubView(Component subView, Path<DevelopmentCostCounter> path) {
     computeAndSaveMeasures(subView, path);
   }
 
   @Override
-  public void visitProjectView(Component projectView, Path<DevelopmentCost> path) {
+  public void visitProjectView(Component projectView, Path<DevelopmentCostCounter> path) {
     Optional<Measure> developmentCostMeasure = measureRepository.getRawMeasure(projectView, developmentCostMetric);
     if (developmentCostMeasure.isPresent()) {
       try {
@@ -116,7 +105,7 @@ public class SqaleMeasuresVisitor extends PathAwareVisitorAdapter<SqaleMeasuresV
     }
   }
 
-  private void computeAndSaveMeasures(Component component, Path<DevelopmentCost> path) {
+  private void computeAndSaveMeasures(Component component, Path<DevelopmentCostCounter> path) {
     saveDevelopmentCostMeasure(component, path.current());
 
     double density = computeDensity(component, path.current());
@@ -126,12 +115,12 @@ public class SqaleMeasuresVisitor extends PathAwareVisitorAdapter<SqaleMeasuresV
     increaseParentDevelopmentCost(path);
   }
 
-  private void saveDevelopmentCostMeasure(Component component, DevelopmentCost developmentCost) {
+  private void saveDevelopmentCostMeasure(Component component, DevelopmentCostCounter developmentCost) {
     // the value of this measure is stored as a string because it can exceed the size limit of number storage on some DB
     measureRepository.add(component, developmentCostMetric, newMeasureBuilder().create(Long.toString(developmentCost.getValue())));
   }
 
-  private double computeDensity(Component component, DevelopmentCost developmentCost) {
+  private double computeDensity(Component component, DevelopmentCostCounter developmentCost) {
     double debt = getLongValue(measureRepository.getRawMeasure(component, technicalDebtMetric));
     if (Double.doubleToRawLongBits(developmentCost.getValue()) != 0L) {
       return debt / (double) developmentCost.getValue();
@@ -150,7 +139,7 @@ public class SqaleMeasuresVisitor extends PathAwareVisitorAdapter<SqaleMeasuresV
     measureRepository.add(component, sqaleRatingMetric, newMeasureBuilder().create(rating, ratingLetter));
   }
 
-  private void increaseParentDevelopmentCost(Path<DevelopmentCost> path) {
+  private void increaseParentDevelopmentCost(Path<DevelopmentCostCounter> path) {
     if (!path.isRoot()) {
       // increase parent's developmentCost with our own
       path.parent().add(path.current().getValue());
@@ -191,9 +180,13 @@ public class SqaleMeasuresVisitor extends PathAwareVisitorAdapter<SqaleMeasuresV
   /**
    * A wrapper class around a long which can be increased and represents the development cost of a Component
    */
-  public static final class DevelopmentCost {
+  public static final class DevelopmentCostCounter {
     private long value = 0;
 
+    private DevelopmentCostCounter() {
+      // prevents instantiation outside SqaleMeasuresVisitor
+    }
+
     public void add(long developmentCosts) {
       this.value += developmentCosts;
     }
@@ -202,4 +195,23 @@ public class SqaleMeasuresVisitor extends PathAwareVisitorAdapter<SqaleMeasuresV
       return value;
     }
   }
+
+  private static final class DevelopmentCostCounterFactory extends SimpleStackElementFactory<DevelopmentCostCounter> {
+    public static final DevelopmentCostCounterFactory INSTANCE = new DevelopmentCostCounterFactory();
+
+    private DevelopmentCostCounterFactory() {
+      // prevents instantiation
+    }
+
+    @Override
+    public DevelopmentCostCounter createForAny(Component component) {
+      return new DevelopmentCostCounter();
+    }
+
+    /** Counter is not used at ProjectView level, saves on instantiating useless objects */
+    @Override
+    public DevelopmentCostCounter createForProjectView(Component projectView) {
+      return null;
+    }
+  }
 }
diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/sqale/SqaleNewMeasuresVisitor.java b/server/sonar-server/src/main/java/org/sonar/server/computation/sqale/SqaleNewMeasuresVisitor.java
new file mode 100644 (file)
index 0000000..8d96057
--- /dev/null
@@ -0,0 +1,249 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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 this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.computation.sqale;
+
+import com.google.common.base.Function;
+import com.google.common.base.Optional;
+import com.google.common.base.Predicate;
+import java.util.Map;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import org.sonar.api.measures.CoreMetrics;
+import org.sonar.api.utils.KeyValueFormat;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+import org.sonar.batch.protocol.output.BatchReport;
+import org.sonar.server.computation.batch.BatchReportReader;
+import org.sonar.server.computation.component.Component;
+import org.sonar.server.computation.component.CrawlerDepthLimit;
+import org.sonar.server.computation.component.PathAwareVisitorAdapter;
+import org.sonar.server.computation.formula.counter.LongVariationValue;
+import org.sonar.server.computation.measure.Measure;
+import org.sonar.server.computation.measure.MeasureRepository;
+import org.sonar.server.computation.measure.MeasureVariations;
+import org.sonar.server.computation.metric.Metric;
+import org.sonar.server.computation.metric.MetricRepository;
+import org.sonar.server.computation.period.Period;
+import org.sonar.server.computation.period.PeriodsHolder;
+
+import static com.google.common.collect.FluentIterable.from;
+import static org.sonar.api.utils.KeyValueFormat.newIntegerConverter;
+import static org.sonar.server.computation.component.ComponentVisitor.Order.POST_ORDER;
+import static org.sonar.server.computation.measure.Measure.newMeasureBuilder;
+import static org.sonar.server.computation.measure.MeasureVariations.newMeasureVariationsBuilder;
+
+/**
+ * This visitor depends on {@link org.sonar.server.computation.issue.IntegrateIssuesVisitor} for the computation of
+ * metric {@link CoreMetrics#NEW_TECHNICAL_DEBT}.
+ */
+public class SqaleNewMeasuresVisitor extends PathAwareVisitorAdapter<SqaleNewMeasuresVisitor.NewDevelopmentCostCounter> {
+  private static final Logger LOG = Loggers.get(SqaleNewMeasuresVisitor.class);
+
+  private final BatchReportReader batchReportReader;
+  private final MeasureRepository measureRepository;
+  private final PeriodsHolder periodsHolder;
+  private final SqaleRatingSettings sqaleRatingSettings;
+
+  private final Metric newDebtMetric;
+  private final Metric nclocDataMetric;
+  private final Metric newDebtRatioMetric;
+
+  public SqaleNewMeasuresVisitor(MetricRepository metricRepository, MeasureRepository measureRepository, BatchReportReader batchReportReader, PeriodsHolder periodsHolder,
+    SqaleRatingSettings sqaleRatingSettings) {
+    super(CrawlerDepthLimit.FILE, POST_ORDER, NewDevelopmentCostCounterFactory.INSTANCE);
+    this.batchReportReader = batchReportReader;
+    this.measureRepository = measureRepository;
+    this.periodsHolder = periodsHolder;
+    this.sqaleRatingSettings = sqaleRatingSettings;
+
+    // computed by NewDebtAggregator which is executed by IntegrateIssuesVisitor
+    this.newDebtMetric = metricRepository.getByKey(CoreMetrics.NEW_TECHNICAL_DEBT_KEY);
+    // which line is ncloc and which isn't
+    this.nclocDataMetric = metricRepository.getByKey(CoreMetrics.NCLOC_DATA_KEY);
+    // output metric
+    this.newDebtRatioMetric = metricRepository.getByKey(CoreMetrics.NEW_SQALE_DEBT_RATIO_KEY);
+  }
+
+  @Override
+  public void visitProject(Component project, Path<NewDevelopmentCostCounter> path) {
+    computeAndSaveNewDebtRatioMeasure(project, path);
+  }
+
+  @Override
+  public void visitModule(Component module, Path<NewDevelopmentCostCounter> path) {
+    computeAndSaveNewDebtRatioMeasure(module, path);
+    increaseNewDevCostOfParent(path);
+  }
+
+  @Override
+  public void visitDirectory(Component directory, Path<NewDevelopmentCostCounter> path) {
+    computeAndSaveNewDebtRatioMeasure(directory, path);
+    increaseNewDevCostOfParent(path);
+  }
+
+  @Override
+  public void visitFile(Component file, Path<NewDevelopmentCostCounter> path) {
+    if (file.getFileAttributes().isUnitTest()) {
+      return;
+    }
+
+    initNewDebtRatioCounter(file, path);
+    computeAndSaveNewDebtRatioMeasure(file, path);
+    increaseNewDevCostOfParent(path);
+  }
+
+  private void computeAndSaveNewDebtRatioMeasure(Component component, Path<NewDevelopmentCostCounter> path) {
+    MeasureVariations.Builder builder = newMeasureVariationsBuilder();
+    for (Period period : periodsHolder.getPeriods()) {
+      long newDevCost = path.current().getValue(period).getValue();
+      double density = computeDensity(component, period, newDevCost);
+      builder.setVariation(period, 100.0 * density);
+    }
+    if (!builder.isEmpty()) {
+      Measure measure = newMeasureBuilder().setVariations(builder.build()).createNoValue();
+      measureRepository.add(component, this.newDebtRatioMetric, measure);
+    }
+  }
+
+  private double computeDensity(Component component, Period period, long developmentCost) {
+    double debt = getLongValue(measureRepository.getRawMeasure(component, this.newDebtMetric), period);
+    if (developmentCost != 0L) {
+      return debt / (double) developmentCost;
+    }
+    return 0d;
+  }
+
+  private static long getLongValue(Optional<Measure> measure, Period period) {
+    if (!measure.isPresent()) {
+      return 0L;
+    }
+    return getLongValue(measure.get(), period);
+  }
+
+  private static long getLongValue(Measure measure, Period period) {
+    if (measure.hasVariations() && measure.getVariations().hasVariation(period.getIndex())) {
+      return (long) measure.getVariations().getVariation(period.getIndex());
+    }
+    return 0l;
+  }
+
+  private void initNewDebtRatioCounter(Component file, Path<NewDevelopmentCostCounter> path) {
+    // first analysis, no period, no differential value to compute, save processing time and return now
+    if (periodsHolder.getPeriods().isEmpty()) {
+      return;
+    }
+
+    Optional<Measure> nclocDataMeasure = measureRepository.getRawMeasure(file, this.nclocDataMetric);
+    if (!nclocDataMeasure.isPresent()) {
+      return;
+    }
+
+    long lineDevCost = sqaleRatingSettings.getDevCost(file.getFileAttributes().getLanguageKey());
+    BatchReport.Changesets changesets = batchReportReader.readChangesets(file.getReportAttributes().getRef());
+    if (changesets == null) {
+      LOG.trace(String.format("No changeset for file %s. Dev cost will be zero.", file.getKey()));
+      return;
+    }
+
+    for (Integer nclocLineIndex : nclocLineIndexes(nclocDataMeasure)) {
+      // lines are 0-based in changesetIndexByLine array
+      int changesetIndex = changesets.getChangesetIndexByLine(nclocLineIndex - 1);
+      BatchReport.Changesets.Changeset changeset = changesets.getChangeset(changesetIndex);
+      if (!changeset.hasDate()) {
+        continue;
+      }
+
+      for (Period period : periodsHolder.getPeriods()) {
+        if (isLineInPeriod(changeset.getDate(), period)) {
+          path.current().increment(period, lineDevCost);
+        }
+      }
+    }
+  }
+
+  private static void increaseNewDevCostOfParent(Path<NewDevelopmentCostCounter> path) {
+    path.parent().add(path.current());
+  }
+
+  /**
+   * A line belongs to a Period if its date is older than the SNAPSHOT's date of the period.
+   */
+  private static boolean isLineInPeriod(long lineDate, Period period) {
+    return lineDate > period.getSnapshotDate();
+  }
+
+  /**
+   * NCLOC_DATA contains Key-value pairs, where key - is a number of line, and value - is an indicator of whether line
+   * contains code (1) or not (0).
+   *
+   * This method parses the value of the NCLOC_DATA measure and return the line numbers of lines which contain code.
+   */
+  private static Iterable<Integer> nclocLineIndexes(Optional<Measure> nclocDataMeasure) {
+    Map<Integer, Integer> parsedNclocData = KeyValueFormat.parse(nclocDataMeasure.get().getData(), newIntegerConverter(), newIntegerConverter());
+    return from(parsedNclocData.entrySet())
+      .filter(NclocEntryNclocLine.INSTANCE)
+      .transform(MapEntryToKey.INSTANCE);
+  }
+
+  public static final class NewDevelopmentCostCounter {
+    private final LongVariationValue.Array devCosts = LongVariationValue.newArray();
+
+    public void add(NewDevelopmentCostCounter counter) {
+      this.devCosts.incrementAll(counter.devCosts);
+    }
+
+    public LongVariationValue.Array increment(Period period, long value) {
+      return devCosts.increment(period, value);
+    }
+
+    public LongVariationValue getValue(Period period) {
+      return this.devCosts.get(period);
+    }
+
+  }
+
+  private enum NclocEntryNclocLine implements Predicate<Map.Entry<Integer, Integer>> {
+    INSTANCE;
+
+    @Override
+    public boolean apply(@Nonnull Map.Entry<Integer, Integer> input) {
+      return input.getValue() == 1;
+    }
+  }
+
+  private enum MapEntryToKey implements Function<Map.Entry<Integer, Integer>, Integer> {
+    INSTANCE;
+
+    @Override
+    @Nullable
+    public Integer apply(@Nonnull Map.Entry<Integer, Integer> input) {
+      return input.getKey();
+    }
+  }
+
+  private static class NewDevelopmentCostCounterFactory extends SimpleStackElementFactory<NewDevelopmentCostCounter> {
+    public static final NewDevelopmentCostCounterFactory INSTANCE = new NewDevelopmentCostCounterFactory();
+
+    @Override
+    public NewDevelopmentCostCounter createForAny(Component component) {
+      return new NewDevelopmentCostCounter();
+    }
+  }
+}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/computation/sqale/SqaleNewMeasuresVisitorTest.java b/server/sonar-server/src/test/java/org/sonar/server/computation/sqale/SqaleNewMeasuresVisitorTest.java
new file mode 100644 (file)
index 0000000..2b70179
--- /dev/null
@@ -0,0 +1,385 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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 this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.computation.sqale;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Ordering;
+import java.util.Arrays;
+import java.util.Set;
+import org.assertj.core.data.Offset;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.api.measures.CoreMetrics;
+import org.sonar.api.utils.KeyValueFormat;
+import org.sonar.batch.protocol.output.BatchReport;
+import org.sonar.server.computation.batch.BatchReportReaderRule;
+import org.sonar.server.computation.batch.TreeRootHolderRule;
+import org.sonar.server.computation.component.Component;
+import org.sonar.server.computation.component.ComponentVisitor;
+import org.sonar.server.computation.component.FileAttributes;
+import org.sonar.server.computation.component.ReportComponent;
+import org.sonar.server.computation.component.VisitorsCrawler;
+import org.sonar.server.computation.measure.Measure;
+import org.sonar.server.computation.measure.MeasureRepositoryRule;
+import org.sonar.server.computation.measure.MeasureVariations;
+import org.sonar.server.computation.metric.MetricRepositoryRule;
+import org.sonar.server.computation.period.Period;
+import org.sonar.server.computation.period.PeriodsHolderRule;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.sonar.api.measures.CoreMetrics.NCLOC_DATA_KEY;
+import static org.sonar.api.measures.CoreMetrics.NEW_TECHNICAL_DEBT_KEY;
+import static org.sonar.server.computation.component.Component.Type.DIRECTORY;
+import static org.sonar.server.computation.component.Component.Type.FILE;
+import static org.sonar.server.computation.component.Component.Type.MODULE;
+import static org.sonar.server.computation.component.Component.Type.PROJECT;
+import static org.sonar.server.computation.measure.Measure.newMeasureBuilder;
+import static org.sonar.server.computation.measure.MeasureAssert.assertThat;
+
+public class SqaleNewMeasuresVisitorTest {
+  private static final String LANGUAGE_1_KEY = "language 1 key";
+  private static final long LANGUAGE_1_DEV_COST = 30l;
+  private static final long PERIOD_2_SNAPSHOT_DATE = 12323l;
+  private static final long PERIOD_5_SNAPSHOT_DATE = 99999999l;
+  private static final long SOME_SNAPSHOT_ID = 9993l;
+  private static final String SOME_PERIOD_MODE = "some mode";
+  private static final int ROOT_REF = 1;
+  private static final int LANGUAGE_1_FILE_REF = 11111;
+  private static final Offset<Double> VARIATION_COMPARISON_OFFSET = Offset.offset(0.01);
+
+  @Rule
+  public BatchReportReaderRule reportReader = new BatchReportReaderRule();
+  @Rule
+  public TreeRootHolderRule treeRootHolder = new TreeRootHolderRule();
+  @Rule
+  public MetricRepositoryRule metricRepository = new MetricRepositoryRule()
+    .add(CoreMetrics.NEW_TECHNICAL_DEBT)
+    .add(CoreMetrics.NCLOC_DATA)
+    .add(CoreMetrics.NEW_SQALE_DEBT_RATIO);
+  @Rule
+  public MeasureRepositoryRule measureRepository = MeasureRepositoryRule.create(treeRootHolder, metricRepository);
+  @Rule
+  public PeriodsHolderRule periodsHolder = new PeriodsHolderRule();
+
+  private SqaleRatingSettings sqaleRatingSettings = mock(SqaleRatingSettings.class);
+
+  private VisitorsCrawler underTest = new VisitorsCrawler(Arrays.<ComponentVisitor>asList(new SqaleNewMeasuresVisitor(metricRepository, measureRepository, reportReader,
+    periodsHolder, sqaleRatingSettings)));
+
+  @Before
+  public void setUp() throws Exception {
+    periodsHolder.setPeriods(
+      new Period(2, SOME_PERIOD_MODE, null, PERIOD_2_SNAPSHOT_DATE, SOME_SNAPSHOT_ID),
+      new Period(4, SOME_PERIOD_MODE, null, PERIOD_5_SNAPSHOT_DATE, SOME_SNAPSHOT_ID));
+  }
+
+  @Test
+  public void project_has_new_debt_ratio_variation_for_each_defined_period() {
+    treeRootHolder.setRoot(builder(PROJECT, ROOT_REF).build());
+
+    underTest.visit(treeRootHolder.getRoot());
+
+    assertNewDebtRatioValues(ROOT_REF, 0, 0);
+  }
+
+  @Test
+  public void project_has_no_new_debt_ratio_variation_if_there_is_no_period() {
+    periodsHolder.setPeriods();
+    treeRootHolder.setRoot(builder(PROJECT, ROOT_REF).build());
+
+    underTest.visit(treeRootHolder.getRoot());
+
+    assertNoNewDebtRatioMeasure(ROOT_REF);
+  }
+
+  @Test
+  public void file_has_no_new_debt_ratio_variation_if_there_is_no_period() {
+    periodsHolder.setPeriods();
+    when(sqaleRatingSettings.getDevCost(LANGUAGE_1_KEY)).thenReturn(LANGUAGE_1_DEV_COST);
+    setupOneFileAloneInAProject(50, 12, Flag.SRC_FILE, Flag.WITH_NCLOC, Flag.WITH_CHANGESET);
+
+    underTest.visit(treeRootHolder.getRoot());
+
+    assertNoNewDebtRatioMeasure(LANGUAGE_1_FILE_REF);
+    assertNoNewDebtRatioMeasure(ROOT_REF);
+  }
+
+  @Test
+  public void file_has_0_new_debt_ratio_if_all_scm_dates_are_before_snapshot_dates() {
+    treeRootHolder.setRoot(
+      builder(PROJECT, ROOT_REF)
+        .addChildren(
+          builder(FILE, LANGUAGE_1_FILE_REF).setFileAttributes(new FileAttributes(false, LANGUAGE_1_KEY)).build()
+        )
+        .build()
+      );
+    measureRepository.addRawMeasure(LANGUAGE_1_FILE_REF, NEW_TECHNICAL_DEBT_KEY, createNewDebtMeasure(50, 12));
+    measureRepository.addRawMeasure(LANGUAGE_1_FILE_REF, NCLOC_DATA_KEY, createNclocDataMeasure(2, 3, 4));
+    reportReader.putChangesets(createChangesets(LANGUAGE_1_FILE_REF, PERIOD_2_SNAPSHOT_DATE - 100, 4));
+
+    underTest.visit(treeRootHolder.getRoot());
+
+    assertNewDebtRatioValues(LANGUAGE_1_FILE_REF, 0, 0);
+    assertNewDebtRatioValues(ROOT_REF, 0, 0);
+  }
+
+  @Test
+  public void file_has_new_debt_ratio_if_some_scm_dates_are_after_snapshot_dates() {
+    when(sqaleRatingSettings.getDevCost(LANGUAGE_1_KEY)).thenReturn(LANGUAGE_1_DEV_COST);
+    setupOneFileAloneInAProject(50, 12, Flag.SRC_FILE, Flag.WITH_NCLOC, Flag.WITH_CHANGESET);
+
+    underTest.visit(treeRootHolder.getRoot());
+
+    assertNewDebtRatioValues(LANGUAGE_1_FILE_REF, 83.33, 0);
+    assertNewDebtRatioValues(ROOT_REF, 83.33, 0);
+  }
+
+  @Test
+  public void new_debt_ratio_changes_with_language_cost() {
+    when(sqaleRatingSettings.getDevCost(LANGUAGE_1_KEY)).thenReturn(LANGUAGE_1_DEV_COST * 10);
+    setupOneFileAloneInAProject(50, 12, Flag.SRC_FILE, Flag.WITH_NCLOC, Flag.WITH_CHANGESET);
+
+    underTest.visit(treeRootHolder.getRoot());
+
+    assertNewDebtRatioValues(LANGUAGE_1_FILE_REF, 8.33, 0);
+    assertNewDebtRatioValues(ROOT_REF, 8.33, 0);
+  }
+
+  @Test
+  public void new_debt_ratio_changes_with_new_technical_debt() {
+    when(sqaleRatingSettings.getDevCost(LANGUAGE_1_KEY)).thenReturn(LANGUAGE_1_DEV_COST);
+    setupOneFileAloneInAProject(500, 120, Flag.SRC_FILE, Flag.WITH_NCLOC, Flag.WITH_CHANGESET);
+
+    underTest.visit(treeRootHolder.getRoot());
+
+    assertNewDebtRatioValues(LANGUAGE_1_FILE_REF, 833.33, 0);
+    assertNewDebtRatioValues(ROOT_REF, 833.33, 0);
+  }
+
+  @Test
+  public void no_new_debt_ratio_when_file_is_unit_test() {
+    when(sqaleRatingSettings.getDevCost(LANGUAGE_1_KEY)).thenReturn(LANGUAGE_1_DEV_COST);
+    setupOneFileAloneInAProject(50, 12, Flag.UT_FILE, Flag.WITH_NCLOC, Flag.WITH_CHANGESET);
+
+    underTest.visit(treeRootHolder.getRoot());
+
+    assertNoNewDebtRatioMeasure(LANGUAGE_1_FILE_REF);
+    assertNewDebtRatioValues(ROOT_REF, 0, 0);
+  }
+
+  @Test
+  public void new_debt_ratio_is_0_when_file_has_no_changesets() {
+    when(sqaleRatingSettings.getDevCost(LANGUAGE_1_KEY)).thenReturn(LANGUAGE_1_DEV_COST);
+    setupOneFileAloneInAProject(50, 12, Flag.SRC_FILE, Flag.WITH_NCLOC, Flag.NO_CHANGESET);
+
+    underTest.visit(treeRootHolder.getRoot());
+
+    assertNewDebtRatioValues(LANGUAGE_1_FILE_REF, 0, 0);
+    assertNewDebtRatioValues(ROOT_REF, 0, 0);
+  }
+
+  @Test
+  public void new_debt_ratio_is_0_when_file_has_empty_changesets() {
+    when(sqaleRatingSettings.getDevCost(LANGUAGE_1_KEY)).thenReturn(LANGUAGE_1_DEV_COST);
+    setupOneFileAloneInAProject(50, 12, Flag.SRC_FILE, Flag.WITH_NCLOC, Flag.NO_DATE_CHANGESET);
+
+    underTest.visit(treeRootHolder.getRoot());
+
+    assertNewDebtRatioValues(LANGUAGE_1_FILE_REF, 0, 0);
+    assertNewDebtRatioValues(ROOT_REF, 0, 0);
+  }
+
+  @Test
+  public void new_debt_ratio_is_0_when_there_is_no_ncloc_in_file() {
+    when(sqaleRatingSettings.getDevCost(LANGUAGE_1_KEY)).thenReturn(LANGUAGE_1_DEV_COST);
+    setupOneFileAloneInAProject(50, 12, Flag.SRC_FILE, Flag.NO_NCLOC, Flag.WITH_CHANGESET);
+
+    underTest.visit(treeRootHolder.getRoot());
+
+    assertNewDebtRatioValues(LANGUAGE_1_FILE_REF, 0, 0);
+    assertNewDebtRatioValues(ROOT_REF, 0, 0);
+  }
+
+  @Test
+  public void new_debt_ratio_is_0_when_ncloc_measure_is_missing() {
+    when(sqaleRatingSettings.getDevCost(LANGUAGE_1_KEY)).thenReturn(LANGUAGE_1_DEV_COST);
+    setupOneFileAloneInAProject(50, 12, Flag.SRC_FILE, Flag.MISSING_MEASURE_NCLOC, Flag.WITH_CHANGESET);
+
+    underTest.visit(treeRootHolder.getRoot());
+
+    assertNewDebtRatioValues(LANGUAGE_1_FILE_REF, 0, 0);
+    assertNewDebtRatioValues(ROOT_REF, 0, 0);
+  }
+
+  @Test
+  public void no_leaf_components_always_have_a_measure_when_at_least_one_period_exist() {
+    when(sqaleRatingSettings.getDevCost(LANGUAGE_1_KEY)).thenReturn(LANGUAGE_1_DEV_COST);
+    treeRootHolder.setRoot(
+      builder(PROJECT, ROOT_REF)
+        .addChildren(
+          builder(MODULE, 11)
+            .addChildren(
+              builder(DIRECTORY, 111)
+                .addChildren(
+                  builder(FILE, LANGUAGE_1_FILE_REF).setFileAttributes(new FileAttributes(false, LANGUAGE_1_KEY)).build()
+                ).build()
+            ).build()
+        ).build()
+      );
+
+    Measure newDebtMeasure = createNewDebtMeasure(50, 12);
+    measureRepository.addRawMeasure(LANGUAGE_1_FILE_REF, NEW_TECHNICAL_DEBT_KEY, newDebtMeasure);
+    measureRepository.addRawMeasure(111, NEW_TECHNICAL_DEBT_KEY, newDebtMeasure);
+    measureRepository.addRawMeasure(11, NEW_TECHNICAL_DEBT_KEY, newDebtMeasure);
+    measureRepository.addRawMeasure(ROOT_REF, NEW_TECHNICAL_DEBT_KEY, newDebtMeasure);
+    // 4 lines file, only first one is not ncloc
+    measureRepository.addRawMeasure(LANGUAGE_1_FILE_REF, NCLOC_DATA_KEY, createNclocDataMeasure(2, 3, 4));
+    // first 2 lines are before all snapshots, 2 last lines are after PERIOD 2's snapshot date
+    reportReader.putChangesets(createChangesets(LANGUAGE_1_FILE_REF, PERIOD_2_SNAPSHOT_DATE - 100, 2, PERIOD_2_SNAPSHOT_DATE + 100, 2));
+
+    underTest.visit(treeRootHolder.getRoot());
+
+    assertNewDebtRatioValues(LANGUAGE_1_FILE_REF, 83.33, 0);
+    assertNewDebtRatioValues(111, 83.33, 0);
+    assertNewDebtRatioValues(11, 83.33, 0);
+    assertNewDebtRatioValues(ROOT_REF, 83.33, 0);
+  }
+
+  private void setupOneFileAloneInAProject(int newDebtPeriod2, int newDebtPeriod4, Flag isUnitTest, Flag withNclocLines, Flag withChangeSets) {
+    checkArgument(isUnitTest == Flag.UT_FILE || isUnitTest == Flag.SRC_FILE);
+    checkArgument(withNclocLines == Flag.WITH_NCLOC || withNclocLines == Flag.NO_NCLOC || withNclocLines == Flag.MISSING_MEASURE_NCLOC);
+    checkArgument(withChangeSets == Flag.WITH_CHANGESET || withChangeSets == Flag.NO_CHANGESET || withChangeSets == Flag.NO_DATE_CHANGESET);
+
+    treeRootHolder.setRoot(
+      builder(PROJECT, ROOT_REF)
+        .addChildren(
+          builder(FILE, LANGUAGE_1_FILE_REF).setFileAttributes(new FileAttributes(isUnitTest == Flag.UT_FILE, LANGUAGE_1_KEY)).build()
+        )
+        .build()
+      );
+
+    Measure newDebtMeasure = createNewDebtMeasure(newDebtPeriod2, newDebtPeriod4);
+    measureRepository.addRawMeasure(LANGUAGE_1_FILE_REF, NEW_TECHNICAL_DEBT_KEY, newDebtMeasure);
+    measureRepository.addRawMeasure(ROOT_REF, NEW_TECHNICAL_DEBT_KEY, newDebtMeasure);
+    if (withNclocLines == Flag.WITH_NCLOC) {
+      // 4 lines file, only first one is not ncloc
+      measureRepository.addRawMeasure(LANGUAGE_1_FILE_REF, NCLOC_DATA_KEY, createNclocDataMeasure(2, 3, 4));
+    } else if (withNclocLines == Flag.NO_NCLOC) {
+      // 4 lines file, none of which is ncloc
+      measureRepository.addRawMeasure(LANGUAGE_1_FILE_REF, NCLOC_DATA_KEY, createNoNclocDataMeasure(4));
+    }
+    if (withChangeSets == Flag.WITH_CHANGESET) {
+      // first 2 lines are before all snapshots, 2 last lines are after PERIOD 2's snapshot date
+      reportReader.putChangesets(createChangesets(LANGUAGE_1_FILE_REF, PERIOD_2_SNAPSHOT_DATE - 100, 2, PERIOD_2_SNAPSHOT_DATE + 100, 2));
+    } else if (withChangeSets == Flag.NO_DATE_CHANGESET) {
+      reportReader.putChangesets(createNoDateChangesets(LANGUAGE_1_FILE_REF, 4));
+    }
+  }
+
+  private enum Flag {
+    UT_FILE, SRC_FILE, NO_CHANGESET, WITH_CHANGESET, NO_DATE_CHANGESET, WITH_NCLOC, NO_NCLOC, MISSING_MEASURE_NCLOC
+  }
+
+  public static ReportComponent.Builder builder(Component.Type type, int ref) {
+    return ReportComponent.builder(type, ref).setKey(String.valueOf(ref));
+  }
+
+  private Measure createNewDebtMeasure(double period2Value, double period4Value) {
+    return newMeasureBuilder().setVariations(new MeasureVariations(null, period2Value, null, period4Value, null)).createNoValue();
+  }
+
+  private static Measure createNclocDataMeasure(Integer... nclocLines) {
+    Set<Integer> nclocLinesSet = ImmutableSet.copyOf(nclocLines);
+    int max = Ordering.<Integer>natural().max(nclocLinesSet);
+    ImmutableMap.Builder<Integer, Integer> builder = ImmutableMap.builder();
+    for (int i = 1; i <= max; i++) {
+      builder.put(i, nclocLinesSet.contains(i) ? 1 : 0);
+    }
+    return newMeasureBuilder().create(KeyValueFormat.format(builder.build(), KeyValueFormat.newIntegerConverter(), KeyValueFormat.newIntegerConverter()));
+  }
+
+  private static Measure createNoNclocDataMeasure(int lineCount) {
+    ImmutableMap.Builder<Integer, Integer> builder = ImmutableMap.builder();
+    for (int i = 1; i <= lineCount; i++) {
+      builder.put(i, 0);
+    }
+    return newMeasureBuilder().create(KeyValueFormat.format(builder.build(), KeyValueFormat.newIntegerConverter(), KeyValueFormat.newIntegerConverter()));
+  }
+
+  /**
+   * Creates a changeset of {@code lines} lines which all have the same date {@code scmDate}.
+   */
+  private static BatchReport.Changesets createChangesets(int componentRef, long scmDate, int lines) {
+    BatchReport.Changesets.Builder builder = BatchReport.Changesets.newBuilder()
+      .setComponentRef(componentRef);
+    addChangeSet(builder, scmDate, lines);
+    return builder.build();
+  }
+
+  /**
+   * Creates a changeset of {@code lineCount} lines which have the date {@code scmDate} and {@code otherLineCount} lines which
+   * have the date {@code otherScmDate}.
+   */
+  private static BatchReport.Changesets createChangesets(int componentRef, long scmDate, int lineCount, long otherScmDate, int otherLineCount) {
+    BatchReport.Changesets.Builder builder = BatchReport.Changesets.newBuilder()
+      .setComponentRef(componentRef);
+    addChangeSet(builder, scmDate, lineCount);
+    addChangeSet(builder, otherScmDate, otherLineCount);
+    return builder.build();
+  }
+
+  private static void addChangeSet(BatchReport.Changesets.Builder builder, long scmDate, int lines) {
+    BatchReport.Changesets.Changeset.Builder changesetBuilder = BatchReport.Changesets.Changeset.newBuilder();
+    changesetBuilder.setRevision("rev" + scmDate);
+    changesetBuilder.setDate(scmDate);
+    builder.addChangeset(changesetBuilder.build());
+    for (int i = 0; i < lines; i++) {
+      builder.addChangesetIndexByLine(builder.getChangesetCount() - 1);
+    }
+  }
+
+  private BatchReport.Changesets createNoDateChangesets(int componentRef, int lineCount) {
+    BatchReport.Changesets.Builder builder = BatchReport.Changesets.newBuilder().setComponentRef(componentRef);
+
+    BatchReport.Changesets.Changeset.Builder changesetBuilder = BatchReport.Changesets.Changeset.newBuilder();
+    changesetBuilder.setRevision("rev");
+    builder.addChangeset(changesetBuilder.build());
+    for (int i = 0; i < lineCount; i++) {
+      builder.addChangesetIndexByLine(builder.getChangesetCount() - 1);
+    }
+
+    return builder.build();
+  }
+
+  private void assertNoNewDebtRatioMeasure(int componentRef) {
+    assertThat(measureRepository.getAddedRawMeasure(componentRef, CoreMetrics.NEW_SQALE_DEBT_RATIO_KEY))
+      .isAbsent();
+  }
+
+  private void assertNewDebtRatioValues(int componentRef, double expectedPeriod2Value, double expectedPeriod4Value) {
+    assertThat(measureRepository.getAddedRawMeasure(componentRef, CoreMetrics.NEW_SQALE_DEBT_RATIO_KEY))
+      .hasVariation2(expectedPeriod2Value, VARIATION_COMPARISON_OFFSET)
+      .hasVariation4(expectedPeriod4Value, VARIATION_COMPARISON_OFFSET);
+  }
+}