From: Sébastien Lesaint Date: Mon, 21 Sep 2015 08:53:59 +0000 (+0200) Subject: SONAR-5876 add SqaleNewMeasuresVisitor to compute new_sqale_debt_ratio X-Git-Tag: 5.2-RC1~292 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=44236bb6fb6ce4cc2311c487c70df17228ec5c1e;p=sonarqube.git SONAR-5876 add SqaleNewMeasuresVisitor to compute new_sqale_debt_ratio --- 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 index 00000000000..4ea4c91bacb --- /dev/null +++ b/it/it-projects/measure/xoo-new-debt-ratio-v1/sonar-project.properties @@ -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 index 00000000000..467e82d8f2c --- /dev/null +++ b/it/it-projects/measure/xoo-new-debt-ratio-v1/src/main/xoo/sample/Sample.xoo @@ -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 index 00000000000..874fe7bfea5 --- /dev/null +++ b/it/it-projects/measure/xoo-new-debt-ratio-v1/src/main/xoo/sample/Sample.xoo.measures @@ -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 index 00000000000..9f1974d8cba --- /dev/null +++ b/it/it-projects/measure/xoo-new-debt-ratio-v1/src/main/xoo/sample/Sample.xoo.scm @@ -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 index 00000000000..4ea4c91bacb --- /dev/null +++ b/it/it-projects/measure/xoo-new-debt-ratio-v2/sonar-project.properties @@ -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 index 00000000000..882793391e0 --- /dev/null +++ b/it/it-projects/measure/xoo-new-debt-ratio-v2/src/main/xoo/sample/Sample.xoo @@ -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 index 00000000000..98f63c1fe47 --- /dev/null +++ b/it/it-projects/measure/xoo-new-debt-ratio-v2/src/main/xoo/sample/Sample.xoo.measures @@ -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 index 00000000000..280b7d64326 --- /dev/null +++ b/it/it-projects/measure/xoo-new-debt-ratio-v2/src/main/xoo/sample/Sample.xoo.scm @@ -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 index 00000000000..4ea4c91bacb --- /dev/null +++ b/it/it-projects/measure/xoo-new-debt-ratio-v3/sonar-project.properties @@ -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 index 00000000000..0b9e023d079 --- /dev/null +++ b/it/it-projects/measure/xoo-new-debt-ratio-v3/src/main/xoo/sample/Sample.xoo @@ -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 index 00000000000..d467f571f3f --- /dev/null +++ b/it/it-projects/measure/xoo-new-debt-ratio-v3/src/main/xoo/sample/Sample.xoo.measures @@ -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 index 00000000000..4b429f3c152 --- /dev/null +++ b/it/it-projects/measure/xoo-new-debt-ratio-v3/src/main/xoo/sample/Sample.xoo.scm @@ -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 diff --git a/it/it-tests/src/test/java/analysis/suite/AnalysisTestSuite.java b/it/it-tests/src/test/java/analysis/suite/AnalysisTestSuite.java index ef43f3e535a..396e03981cb 100644 --- a/it/it-tests/src/test/java/analysis/suite/AnalysisTestSuite.java +++ b/it/it-tests/src/test/java/analysis/suite/AnalysisTestSuite.java @@ -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 index 00000000000..40d6c62e2fc --- /dev/null +++ b/it/it-tests/src/test/java/analysis/suite/measure/NewDebtRatioMeasureTest.java @@ -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 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)); + } + +} diff --git a/it/it-tests/src/test/java/util/ItUtils.java b/it/it-tests/src/test/java/util/ItUtils.java index 4e299355a75..1d8e3406ea8 100644 --- a/it/it-tests/src/test/java/util/ItUtils.java +++ b/it/it-tests/src/test/java/util/ItUtils.java @@ -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 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.: + *
+   * ItUtils.runProjectAnalysis(orchestrator, "project_name",
+   *    ItUtils.concat(properties, "sonar.scm.disabled", "false")
+   *    );
+   * 
+ */ + 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); } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/container/ReportComputeEngineContainerPopulator.java b/server/sonar-server/src/main/java/org/sonar/server/computation/container/ReportComputeEngineContainerPopulator.java index 8ed0f9a2f1c..cc8f01c859c 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/computation/container/ReportComputeEngineContainerPopulator.java +++ b/server/sonar-server/src/main/java/org/sonar/server/computation/container/ReportComputeEngineContainerPopulator.java @@ -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, diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/sqale/SqaleMeasuresVisitor.java b/server/sonar-server/src/main/java/org/sonar/server/computation/sqale/SqaleMeasuresVisitor.java index ab62abd3392..2e1171d0148 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/computation/sqale/SqaleMeasuresVisitor.java +++ b/server/sonar-server/src/main/java/org/sonar/server/computation/sqale/SqaleMeasuresVisitor.java @@ -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 { +public class SqaleMeasuresVisitor extends PathAwareVisitorAdapter { private static final Logger LOG = Loggers.get(SqaleMeasuresVisitor.class); private final MetricRepository metricRepository; @@ -48,18 +48,7 @@ public class SqaleMeasuresVisitor extends PathAwareVisitorAdapter() { - @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 path) { + public void visitProject(Component project, Path path) { computeAndSaveMeasures(project, path); } @Override - public void visitDirectory(Component directory, Path path) { + public void visitDirectory(Component directory, Path path) { computeAndSaveMeasures(directory, path); } @Override - public void visitModule(Component module, Path path) { + public void visitModule(Component module, Path path) { computeAndSaveMeasures(module, path); } @Override - public void visitFile(Component file, Path path) { + public void visitFile(Component file, Path path) { if (!file.getFileAttributes().isUnitTest()) { long developmentCosts = computeDevelopmentCost(file); path.current().add(developmentCosts); @@ -95,17 +84,17 @@ public class SqaleMeasuresVisitor extends PathAwareVisitorAdapter path) { + public void visitView(Component view, Path path) { computeAndSaveMeasures(view, path); } @Override - public void visitSubView(Component subView, Path path) { + public void visitSubView(Component subView, Path path) { computeAndSaveMeasures(subView, path); } @Override - public void visitProjectView(Component projectView, Path path) { + public void visitProjectView(Component projectView, Path path) { Optional developmentCostMeasure = measureRepository.getRawMeasure(projectView, developmentCostMetric); if (developmentCostMeasure.isPresent()) { try { @@ -116,7 +105,7 @@ public class SqaleMeasuresVisitor extends PathAwareVisitorAdapter path) { + private void computeAndSaveMeasures(Component component, Path path) { saveDevelopmentCostMeasure(component, path.current()); double density = computeDensity(component, path.current()); @@ -126,12 +115,12 @@ public class SqaleMeasuresVisitor extends PathAwareVisitorAdapter path) { + private void increaseParentDevelopmentCost(Path 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 { + 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 index 00000000000..8d96057d1ef --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/computation/sqale/SqaleNewMeasuresVisitor.java @@ -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 { + 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 path) { + computeAndSaveNewDebtRatioMeasure(project, path); + } + + @Override + public void visitModule(Component module, Path path) { + computeAndSaveNewDebtRatioMeasure(module, path); + increaseNewDevCostOfParent(path); + } + + @Override + public void visitDirectory(Component directory, Path path) { + computeAndSaveNewDebtRatioMeasure(directory, path); + increaseNewDevCostOfParent(path); + } + + @Override + public void visitFile(Component file, Path path) { + if (file.getFileAttributes().isUnitTest()) { + return; + } + + initNewDebtRatioCounter(file, path); + computeAndSaveNewDebtRatioMeasure(file, path); + increaseNewDevCostOfParent(path); + } + + private void computeAndSaveNewDebtRatioMeasure(Component component, Path 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, 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 path) { + // first analysis, no period, no differential value to compute, save processing time and return now + if (periodsHolder.getPeriods().isEmpty()) { + return; + } + + Optional 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 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 nclocLineIndexes(Optional nclocDataMeasure) { + Map 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> { + INSTANCE; + + @Override + public boolean apply(@Nonnull Map.Entry input) { + return input.getValue() == 1; + } + } + + private enum MapEntryToKey implements Function, Integer> { + INSTANCE; + + @Override + @Nullable + public Integer apply(@Nonnull Map.Entry input) { + return input.getKey(); + } + } + + private static class NewDevelopmentCostCounterFactory extends SimpleStackElementFactory { + 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 index 00000000000..2b70179f66b --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/computation/sqale/SqaleNewMeasuresVisitorTest.java @@ -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 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.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 nclocLinesSet = ImmutableSet.copyOf(nclocLines); + int max = Ordering.natural().max(nclocLinesSet); + ImmutableMap.Builder 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 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); + } +}