]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-10852 Fix computation of project without src but with test measures
authorJulien Lancelot <julien.lancelot@sonarsource.com>
Wed, 18 Jul 2018 15:12:59 +0000 (17:12 +0200)
committerGitHub <noreply@github.com>
Wed, 18 Jul 2018 15:12:59 +0000 (17:12 +0200)
14 files changed:
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/step/UnitTestMeasuresStep.java
server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/formula/ReportFormulaExecutorComponentVisitorTest.java
server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/step/ReportUnitTestMeasuresStepTest.java
tests/plugins/pom.xml
tests/plugins/save-measure-on-module-plugin/pom.xml [new file with mode: 0644]
tests/plugins/save-measure-on-module-plugin/src/main/java/org/sonar/measure/ModuleMeasureSensor.java [new file with mode: 0644]
tests/plugins/save-measure-on-module-plugin/src/main/java/org/sonar/measure/SaveMeasureOnModulePlugin.java [new file with mode: 0644]
tests/projects/analysis/xoo-module-b-without-source/module_a/sonar-project.properties [new file with mode: 0644]
tests/projects/analysis/xoo-module-b-without-source/module_a/src/main/xoo/com/sonar/it/samples/modules/a1/HelloA1.xoo [new file with mode: 0644]
tests/projects/analysis/xoo-module-b-without-source/module_a/src/main/xoo/com/sonar/it/samples/modules/a1/HelloA1.xoo.measures [new file with mode: 0644]
tests/projects/analysis/xoo-module-b-without-source/module_b/sonar-project.properties [new file with mode: 0644]
tests/projects/analysis/xoo-module-b-without-source/sonar-project.properties [new file with mode: 0644]
tests/src/test/java/org/sonarqube/tests/Category3Suite.java
tests/src/test/java/org/sonarqube/tests/analysis/ProjectMeasureTest.java [new file with mode: 0644]

index 1e7e8af84d3a488472395e0b1f3650fdb0bb5247..f936f4f72d322fbc42211c81f439747cabacb051 100644 (file)
@@ -22,7 +22,6 @@ package org.sonar.server.computation.task.projectanalysis.step;
 import com.google.common.base.Optional;
 import com.google.common.collect.ImmutableList;
 import org.sonar.server.computation.task.projectanalysis.component.Component;
-import org.sonar.server.computation.task.projectanalysis.component.CrawlerDepthLimit;
 import org.sonar.server.computation.task.projectanalysis.component.PathAwareCrawler;
 import org.sonar.server.computation.task.projectanalysis.component.TreeRootHolder;
 import org.sonar.server.computation.task.projectanalysis.formula.Counter;
@@ -31,6 +30,7 @@ import org.sonar.server.computation.task.projectanalysis.formula.CreateMeasureCo
 import org.sonar.server.computation.task.projectanalysis.formula.Formula;
 import org.sonar.server.computation.task.projectanalysis.formula.FormulaExecutorComponentVisitor;
 import org.sonar.server.computation.task.projectanalysis.formula.counter.IntSumCounter;
+import org.sonar.server.computation.task.projectanalysis.formula.counter.LongSumCounter;
 import org.sonar.server.computation.task.projectanalysis.measure.Measure;
 import org.sonar.server.computation.task.projectanalysis.measure.MeasureRepository;
 import org.sonar.server.computation.task.projectanalysis.metric.MetricRepository;
@@ -42,20 +42,15 @@ import static org.sonar.api.measures.CoreMetrics.TEST_ERRORS_KEY;
 import static org.sonar.api.measures.CoreMetrics.TEST_EXECUTION_TIME_KEY;
 import static org.sonar.api.measures.CoreMetrics.TEST_FAILURES_KEY;
 import static org.sonar.api.measures.CoreMetrics.TEST_SUCCESS_DENSITY_KEY;
-import static org.sonar.server.computation.task.projectanalysis.formula.SumFormula.createIntSumFormula;
-import static org.sonar.server.computation.task.projectanalysis.formula.SumFormula.createLongSumFormula;
 
 /**
  * Computes unit test measures on files and then aggregates them on higher components.
  */
 public class UnitTestMeasuresStep implements ComputationStep {
 
-  private static final String[] METRICS = new String[] {TESTS_KEY, TEST_ERRORS_KEY, TEST_FAILURES_KEY, TEST_SUCCESS_DENSITY_KEY};
+  private static final String[] METRICS = new String[] {TESTS_KEY, TEST_ERRORS_KEY, TEST_FAILURES_KEY, SKIPPED_TESTS_KEY, TEST_SUCCESS_DENSITY_KEY, TEST_EXECUTION_TIME_KEY};
 
-  private static final ImmutableList<Formula> FORMULAS = ImmutableList.<Formula>of(
-    createLongSumFormula(TEST_EXECUTION_TIME_KEY),
-    createIntSumFormula(SKIPPED_TESTS_KEY),
-    new UnitTestsFormula());
+  private static final ImmutableList<Formula> FORMULAS = ImmutableList.of(new UnitTestsFormula());
 
   private final TreeRootHolder treeRootHolder;
   private final MetricRepository metricRepository;
@@ -84,13 +79,18 @@ public class UnitTestMeasuresStep implements ComputationStep {
     @Override
     public Optional<Measure> createMeasure(UnitTestsCounter counter, CreateMeasureContext context) {
       String metricKey = context.getMetric().getKey();
+      Component leaf = counter.getLeaf();
       switch (metricKey) {
         case TESTS_KEY:
-          return createMeasure(context.getComponent().getType(), counter.testsCounter.getValue());
+          return createIntMeasure(context.getComponent(), leaf, counter.testsCounter.getValue());
         case TEST_ERRORS_KEY:
-          return createMeasure(context.getComponent().getType(), counter.testsErrorsCounter.getValue());
+          return createIntMeasure(context.getComponent(), leaf, counter.testsErrorsCounter.getValue());
         case TEST_FAILURES_KEY:
-          return createMeasure(context.getComponent().getType(), counter.testsFailuresCounter.getValue());
+          return createIntMeasure(context.getComponent(), leaf, counter.testsFailuresCounter.getValue());
+        case SKIPPED_TESTS_KEY:
+          return createIntMeasure(context.getComponent(), leaf, counter.skippedTestsCounter.getValue());
+        case TEST_EXECUTION_TIME_KEY:
+          return createLongMeasure(context.getComponent(), leaf, counter.testExecutionTimeCounter.getValue());
         case TEST_SUCCESS_DENSITY_KEY:
           return createDensityMeasure(counter, context.getMetric().getDecimalScale());
         default:
@@ -98,8 +98,15 @@ public class UnitTestMeasuresStep implements ComputationStep {
       }
     }
 
-    private static Optional<Measure> createMeasure(Component.Type componentType, Optional<Integer> metricValue) {
-      if (metricValue.isPresent() && CrawlerDepthLimit.LEAVES.isDeeperThan(componentType)) {
+    private static Optional<Measure> createIntMeasure(Component currentComponent, Component leafComponent, Optional<Integer> metricValue) {
+      if (metricValue.isPresent() && leafComponent.getType().isDeeperThan(currentComponent.getType())) {
+        return Optional.of(Measure.newMeasureBuilder().create(metricValue.get()));
+      }
+      return Optional.absent();
+    }
+
+    private static Optional<Measure> createLongMeasure(Component currentComponent, Component leafComponent, Optional<Long> metricValue) {
+      if (metricValue.isPresent() && leafComponent.getType().isDeeperThan(currentComponent.getType())) {
         return Optional.of(Measure.newMeasureBuilder().create(metricValue.get()));
       }
       return Optional.absent();
@@ -133,19 +140,32 @@ public class UnitTestMeasuresStep implements ComputationStep {
     private final IntSumCounter testsCounter = new IntSumCounter(TESTS_KEY);
     private final IntSumCounter testsErrorsCounter = new IntSumCounter(TEST_ERRORS_KEY);
     private final IntSumCounter testsFailuresCounter = new IntSumCounter(TEST_FAILURES_KEY);
+    private final IntSumCounter skippedTestsCounter = new IntSumCounter(SKIPPED_TESTS_KEY);
+    private final LongSumCounter testExecutionTimeCounter = new LongSumCounter(TEST_EXECUTION_TIME_KEY);
+
+    private Component leaf;
 
     @Override
     public void aggregate(UnitTestsCounter counter) {
       testsCounter.aggregate(counter.testsCounter);
       testsErrorsCounter.aggregate(counter.testsErrorsCounter);
       testsFailuresCounter.aggregate(counter.testsFailuresCounter);
+      skippedTestsCounter.aggregate(counter.skippedTestsCounter);
+      testExecutionTimeCounter.aggregate(counter.testExecutionTimeCounter);
     }
 
     @Override
     public void initialize(CounterInitializationContext context) {
+      this.leaf = context.getLeaf();
       testsCounter.initialize(context);
       testsErrorsCounter.initialize(context);
       testsFailuresCounter.initialize(context);
+      skippedTestsCounter.initialize(context);
+      testExecutionTimeCounter.initialize(context);
+    }
+
+    Component getLeaf() {
+      return leaf;
     }
   }
 
index 3665b1dba699813e9e190959068740b13a80ce9c..deac28e42aaa83394b424897ca7132e0730f2308 100644 (file)
@@ -23,6 +23,7 @@ import com.google.common.base.Optional;
 import com.google.common.collect.ImmutableList;
 import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.ExpectedException;
 import org.sonar.api.measures.CoreMetrics;
 import org.sonar.server.computation.task.projectanalysis.component.Component;
 import org.sonar.server.computation.task.projectanalysis.component.PathAwareCrawler;
@@ -48,6 +49,7 @@ import static org.sonar.server.computation.task.projectanalysis.component.Report
 import static org.sonar.server.computation.task.projectanalysis.measure.Measure.newMeasureBuilder;
 import static org.sonar.server.computation.task.projectanalysis.measure.MeasureRepoEntry.entryOf;
 import static org.sonar.server.computation.task.projectanalysis.measure.MeasureRepoEntry.toEntries;
+import static org.sonar.test.ExceptionCauseMatcher.hasType;
 
 public class ReportFormulaExecutorComponentVisitorTest {
   private static final int ROOT_REF = 1;
@@ -78,6 +80,8 @@ public class ReportFormulaExecutorComponentVisitorTest {
         .build())
     .build();
 
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
   @Rule
   public TreeRootHolderRule treeRootHolder = new TreeRootHolderRule();
   @Rule
@@ -92,7 +96,7 @@ public class ReportFormulaExecutorComponentVisitorTest {
   public PeriodHolderRule periodsHolder = new PeriodHolderRule().setPeriod(new Period("some mode", null, 95l, "756l"));
 
   @Test
-  public void verify_aggregation_on_value() throws Exception {
+  public void verify_aggregation_on_value() {
     treeRootHolder.setRoot(BALANCED_COMPONENT_TREE);
 
     measureRepository.addRawMeasure(FILE_1_REF, LINES_KEY, newMeasureBuilder().create(10));
@@ -113,7 +117,7 @@ public class ReportFormulaExecutorComponentVisitorTest {
   }
 
   @Test
-  public void verify_multi_metric_formula_support_and_aggregation() throws Exception {
+  public void verify_multi_metric_formula_support_and_aggregation() {
     treeRootHolder.setRoot(BALANCED_COMPONENT_TREE);
 
     measureRepository.addRawMeasure(FILE_1_REF, LINES_KEY, newMeasureBuilder().create(10));
@@ -150,7 +154,7 @@ public class ReportFormulaExecutorComponentVisitorTest {
   }
 
   @Test
-  public void verify_aggregation_on_variation() throws Exception {
+  public void verify_aggregation_on_variation() {
     treeRootHolder.setRoot(BALANCED_COMPONENT_TREE);
 
     measureRepository.addRawMeasure(FILE_1_REF, NEW_LINES_TO_COVER_KEY, createMeasureWithVariation(10));
@@ -171,7 +175,7 @@ public class ReportFormulaExecutorComponentVisitorTest {
   }
 
   @Test
-  public void measures_are_0_when_there_is_no_input_measure() throws Exception {
+  public void measures_are_0_when_there_is_no_input_measure() {
     ReportComponent project = ReportComponent.builder(PROJECT, ROOT_REF)
       .addChildren(
         ReportComponent.builder(MODULE, MODULE_1_REF)
@@ -194,7 +198,7 @@ public class ReportFormulaExecutorComponentVisitorTest {
   }
 
   @Test
-  public void add_measure_even_when_leaf_is_not_FILE() throws Exception {
+  public void add_measure_even_when_leaf_is_not_FILE() {
     ReportComponent project = ReportComponent.builder(PROJECT, ROOT_REF)
       .addChildren(
         ReportComponent.builder(MODULE, MODULE_1_REF)
@@ -211,6 +215,83 @@ public class ReportFormulaExecutorComponentVisitorTest {
     assertAddedRawMeasure(DIRECTORY_1_REF, 0);
   }
 
+  @Test
+  public void compute_measure_on_project_without_children() {
+    ReportComponent root = builder(PROJECT, ROOT_REF).build();
+    treeRootHolder.setRoot(root);
+    measureRepository.addRawMeasure(ROOT_REF, LINES_KEY, newMeasureBuilder().create(10));
+
+    new PathAwareCrawler<>(formulaExecutorComponentVisitor(new FakeFormula()))
+      .visit(root);
+
+    assertThat(toEntries(measureRepository.getAddedRawMeasures(ROOT_REF))).containsOnly(entryOf(NCLOC_KEY, newMeasureBuilder().create(10)));
+  }
+
+  @Test
+  public void ignore_measure_defined_on_project_when_measure_is_defined_on_leaf() {
+    ReportComponent root = builder(PROJECT, ROOT_REF)
+      .addChildren(
+        builder(Component.Type.FILE, FILE_1_REF).build())
+      .build();
+    treeRootHolder.setRoot(root);
+    measureRepository.addRawMeasure(ROOT_REF, LINES_KEY, newMeasureBuilder().create(10));
+    measureRepository.addRawMeasure(FILE_1_REF, LINES_KEY, newMeasureBuilder().create(2));
+
+    new PathAwareCrawler<>(formulaExecutorComponentVisitor(new FakeFormula()))
+      .visit(root);
+
+    assertAddedRawMeasure(ROOT_REF, 2);
+    assertAddedRawMeasure(FILE_1_REF, 2);
+  }
+
+  @Test
+  public void fail_when_trying_to_compute_file_measure_already_existing_in_report() {
+    ReportComponent root = builder(PROJECT, ROOT_REF)
+      .addChildren(
+        builder(Component.Type.FILE, FILE_1_REF).build())
+      .build();
+    treeRootHolder.setRoot(root);
+    measureRepository.addRawMeasure(FILE_1_REF, NCLOC_KEY, newMeasureBuilder().create(2));
+
+    expectedException.expectCause(hasType(UnsupportedOperationException.class)
+      .andMessage(String.format("A measure can only be set once for Component (ref=%s), Metric (key=%s)", FILE_1_REF, NCLOC_KEY)));
+
+    new PathAwareCrawler<>(formulaExecutorComponentVisitor(new FakeFormula()))
+      .visit(root);
+  }
+
+  @Test
+  public void fail_on_project_without_children_already_having_computed_measure() {
+    ReportComponent root = builder(PROJECT, ROOT_REF).build();
+    treeRootHolder.setRoot(root);
+    measureRepository.addRawMeasure(ROOT_REF, NCLOC_KEY, newMeasureBuilder().create(10));
+
+    expectedException.expectCause(hasType(UnsupportedOperationException.class)
+      .andMessage(String.format("A measure can only be set once for Component (ref=%s), Metric (key=%s)", ROOT_REF, NCLOC_KEY)));
+
+    new PathAwareCrawler<>(formulaExecutorComponentVisitor(new FakeFormula()))
+      .visit(root);
+  }
+
+  @Test
+  public void fail_on_project_containing_module_without_children_already_having_computed_measure() {
+    ReportComponent root = builder(PROJECT, ROOT_REF)
+      .addChildren(
+        ReportComponent.builder(MODULE, MODULE_1_REF).build(),
+        builder(Component.Type.FILE, FILE_1_REF).build())
+      .build();
+    treeRootHolder.setRoot(root);
+    measureRepository.addRawMeasure(FILE_1_REF, LINES_KEY, newMeasureBuilder().create(10));
+    // Ncloc is already computed on module
+    measureRepository.addRawMeasure(MODULE_1_REF, NCLOC_KEY, newMeasureBuilder().create(3));
+
+    expectedException.expectCause(hasType(UnsupportedOperationException.class)
+      .andMessage(String.format("A measure can only be set once for Component (ref=%s), Metric (key=%s)", MODULE_1_REF, NCLOC_KEY)));
+
+    new PathAwareCrawler<>(formulaExecutorComponentVisitor(new FakeFormula()))
+      .visit(root);
+  }
+
   private FormulaExecutorComponentVisitor formulaExecutorComponentVisitor(Formula formula) {
     return FormulaExecutorComponentVisitor.newBuilder(metricRepository, measureRepository)
       .withVariationSupport(periodsHolder)
index 2f90c318629063480e55f420276f48d3fa02a88a..0d0fc13a1ae3dc33e6192b79e1fe87237927f0d5 100644 (file)
@@ -113,7 +113,7 @@ public class ReportUnitTestMeasuresStepTest {
   }
 
   @Test
-  public void aggregate_skipped_tests_time() {
+  public void aggregate_skipped_tests() {
     checkMeasuresAggregation(SKIPPED_TESTS_KEY, 100, 400, 500);
   }
 
@@ -258,6 +258,54 @@ public class ReportUnitTestMeasuresStepTest {
     assertThat(measureRepository.getAddedRawMeasure(ROOT_REF, TEST_SUCCESS_DENSITY_KEY)).isAbsent();
   }
 
+  @Test
+  public void aggregate_measures_when_tests_measures_are_defined_on_directory() {
+    treeRootHolder.setRoot(builder(PROJECT, ROOT_REF)
+      .addChildren(
+        builder(MODULE, MODULE_REF)
+          .addChildren(
+            builder(DIRECTORY, DIRECTORY_REF).build())
+          .build())
+      .build());
+    measureRepository.addRawMeasure(DIRECTORY_REF, TESTS_KEY, newMeasureBuilder().create(10));
+    measureRepository.addRawMeasure(DIRECTORY_REF, TEST_ERRORS_KEY, newMeasureBuilder().create(2));
+    measureRepository.addRawMeasure(DIRECTORY_REF, TEST_FAILURES_KEY, newMeasureBuilder().create(1));
+    measureRepository.addRawMeasure(DIRECTORY_REF, SKIPPED_TESTS_KEY, newMeasureBuilder().create(5));
+    measureRepository.addRawMeasure(DIRECTORY_REF, TEST_EXECUTION_TIME_KEY, newMeasureBuilder().create(100L));
+
+    underTest.execute();
+
+    assertThat(toEntries(measureRepository.getAddedRawMeasures(MODULE_REF))).containsOnly(
+      entryOf(TESTS_KEY, newMeasureBuilder().create(10)),
+      entryOf(TEST_ERRORS_KEY, newMeasureBuilder().create(2)),
+      entryOf(TEST_FAILURES_KEY, newMeasureBuilder().create(1)),
+      entryOf(SKIPPED_TESTS_KEY, newMeasureBuilder().create(5)),
+      entryOf(TEST_EXECUTION_TIME_KEY, newMeasureBuilder().create(100L)),
+      entryOf(TEST_SUCCESS_DENSITY_KEY, newMeasureBuilder().create(70d, 1)));
+    assertThat(toEntries(measureRepository.getAddedRawMeasures(ROOT_REF))).containsOnly(
+      entryOf(TESTS_KEY, newMeasureBuilder().create(10)),
+      entryOf(TEST_ERRORS_KEY, newMeasureBuilder().create(2)),
+      entryOf(TEST_FAILURES_KEY, newMeasureBuilder().create(1)),
+      entryOf(SKIPPED_TESTS_KEY, newMeasureBuilder().create(5)),
+      entryOf(TEST_EXECUTION_TIME_KEY, newMeasureBuilder().create(100L)),
+      entryOf(TEST_SUCCESS_DENSITY_KEY, newMeasureBuilder().create(70d, 1)));
+  }
+
+  @Test
+  public void compute_test_success_density_measure_when_tests_measures_are_defined_at_project_level_and_no_children() {
+    treeRootHolder.setRoot(builder(PROJECT, ROOT_REF).build());
+    measureRepository.addRawMeasure(ROOT_REF, TESTS_KEY, newMeasureBuilder().create(10));
+    measureRepository.addRawMeasure(ROOT_REF, TEST_ERRORS_KEY, newMeasureBuilder().create(2));
+    measureRepository.addRawMeasure(ROOT_REF, TEST_FAILURES_KEY, newMeasureBuilder().create(1));
+    measureRepository.addRawMeasure(ROOT_REF, SKIPPED_TESTS_KEY, newMeasureBuilder().create(5));
+    measureRepository.addRawMeasure(ROOT_REF, TEST_EXECUTION_TIME_KEY, newMeasureBuilder().create(100L));
+
+    underTest.execute();
+
+    assertThat(toEntries(measureRepository.getAddedRawMeasures(ROOT_REF))).containsOnly(
+      entryOf(TEST_SUCCESS_DENSITY_KEY, newMeasureBuilder().create(70d, 1)));
+  }
+
   private void checkMeasuresAggregation(String metricKey, int file1Value, int file2Value, int expectedValue) {
     measureRepository.addRawMeasure(FILE_1_REF, metricKey, newMeasureBuilder().create(file1Value));
     measureRepository.addRawMeasure(FILE_2_REF, metricKey, newMeasureBuilder().create(file2Value));
index af4cd0155f76ce040cc11c691967f76665930659..e428e662eabb1d6b2e4ba1735974eefb87ad5cb8 100644 (file)
@@ -52,6 +52,7 @@
     <module>project-builder-plugin</module>
     <module>property-relocation-plugin</module>
     <module>property-sets-plugin</module>
+    <module>save-measure-on-module-plugin</module>
     <module>security-plugin</module>
     <module>server-plugin</module>
     <module>settings-encryption-plugin</module>
diff --git a/tests/plugins/save-measure-on-module-plugin/pom.xml b/tests/plugins/save-measure-on-module-plugin/pom.xml
new file mode 100644 (file)
index 0000000..e84fd85
--- /dev/null
@@ -0,0 +1,39 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.sonarsource.sonarqube.tests</groupId>
+    <artifactId>plugins</artifactId>
+    <version>6.7.5-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>save-measure-on-module-plugin</artifactId>
+  <packaging>sonar-plugin</packaging>
+  <version>1.0-SNAPSHOT</version>
+  <description>Plugins :: Save Measure On Module</description>
+
+  <dependencies>
+    <dependency>
+      <groupId>org.sonarsource.sonarqube</groupId>
+      <artifactId>sonar-plugin-api</artifactId>
+      <version>${apiVersion}</version>
+      <scope>provided</scope>
+    </dependency>
+  </dependencies>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.sonarsource.sonar-packaging-maven-plugin</groupId>
+        <artifactId>sonar-packaging-maven-plugin</artifactId>
+        <version>1.15</version>
+        <extensions>true</extensions>
+        <configuration>
+          <pluginClass>org.sonar.measure.SaveMeasureOnModulePlugin</pluginClass>
+        </configuration>
+      </plugin>
+    </plugins>
+  </build>
+
+</project>
diff --git a/tests/plugins/save-measure-on-module-plugin/src/main/java/org/sonar/measure/ModuleMeasureSensor.java b/tests/plugins/save-measure-on-module-plugin/src/main/java/org/sonar/measure/ModuleMeasureSensor.java
new file mode 100644 (file)
index 0000000..09e3358
--- /dev/null
@@ -0,0 +1,68 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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.measure;
+
+import java.io.Serializable;
+import java.util.Map;
+import java.util.Optional;
+import org.sonar.api.batch.measure.MetricFinder;
+import org.sonar.api.batch.sensor.Sensor;
+import org.sonar.api.batch.sensor.SensorContext;
+import org.sonar.api.batch.sensor.SensorDescriptor;
+import org.sonar.api.batch.sensor.measure.NewMeasure;
+import org.sonar.api.utils.KeyValueFormat;
+
+public class ModuleMeasureSensor implements Sensor {
+
+  private static final String PROPERTY = "sonar.measure.valueByMetric";
+
+  private final MetricFinder metricFinder;
+
+  public ModuleMeasureSensor(MetricFinder metricFinder) {
+    this.metricFinder = metricFinder;
+  }
+
+  @Override
+  public void describe(SensorDescriptor descriptor) {
+    descriptor
+      .name("Generate measure on each module");
+  }
+
+  @Override
+  public void execute(SensorContext context) {
+    Optional<String> property = context.config().get(PROPERTY);
+    if (!property.isPresent()) {
+      return;
+    }
+    Map<String, String> valueByMetric = KeyValueFormat.parse(property.get());
+    valueByMetric.forEach((metricKey, value) -> {
+      org.sonar.api.batch.measure.Metric<Serializable> metric = metricFinder.findByKey(metricKey);
+      if (metric == null) {
+        throw new IllegalStateException(String.format("Metric '%s' doesn't exist", metricKey));
+      }
+      NewMeasure<Serializable> newMeasure = context.newMeasure()
+        .forMetric(metric)
+        .on(context.module())
+        .withValue(Integer.parseInt(value));
+      newMeasure.save();
+    });
+  }
+
+}
\ No newline at end of file
diff --git a/tests/plugins/save-measure-on-module-plugin/src/main/java/org/sonar/measure/SaveMeasureOnModulePlugin.java b/tests/plugins/save-measure-on-module-plugin/src/main/java/org/sonar/measure/SaveMeasureOnModulePlugin.java
new file mode 100644 (file)
index 0000000..1046e8a
--- /dev/null
@@ -0,0 +1,34 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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.measure;
+
+import org.sonar.api.Plugin;
+
+/**
+ * Plugin entry-point, as declared in pom.xml.
+ */
+public class SaveMeasureOnModulePlugin implements Plugin {
+
+  @Override
+  public void define(Context context) {
+    context.addExtension(ModuleMeasureSensor.class);
+  }
+
+}
diff --git a/tests/projects/analysis/xoo-module-b-without-source/module_a/sonar-project.properties b/tests/projects/analysis/xoo-module-b-without-source/module_a/sonar-project.properties
new file mode 100644 (file)
index 0000000..6063a23
--- /dev/null
@@ -0,0 +1,6 @@
+sonar.projectKey=module_a
+sonar.projectName=Module A
+
+sonar.sources=src/main/xoo
+
diff --git a/tests/projects/analysis/xoo-module-b-without-source/module_a/src/main/xoo/com/sonar/it/samples/modules/a1/HelloA1.xoo b/tests/projects/analysis/xoo-module-b-without-source/module_a/src/main/xoo/com/sonar/it/samples/modules/a1/HelloA1.xoo
new file mode 100644 (file)
index 0000000..74d29a4
--- /dev/null
@@ -0,0 +1,16 @@
+package com.sonar.it.samples.modules.a1;
+
+public class HelloA1 {
+       private int i;
+       private HelloA1() {
+               
+       }
+       
+       public void hello() {
+               System.out.println("hello" + " xoo");
+       }
+       
+       protected String getHello() {
+               return "hello";
+       }
+}
\ No newline at end of file
diff --git a/tests/projects/analysis/xoo-module-b-without-source/module_a/src/main/xoo/com/sonar/it/samples/modules/a1/HelloA1.xoo.measures b/tests/projects/analysis/xoo-module-b-without-source/module_a/src/main/xoo/com/sonar/it/samples/modules/a1/HelloA1.xoo.measures
new file mode 100644 (file)
index 0000000..f3953cc
--- /dev/null
@@ -0,0 +1,4 @@
+ncloc:12
+classes:1
+complexity:3
+cognitive_complexity:4
diff --git a/tests/projects/analysis/xoo-module-b-without-source/module_b/sonar-project.properties b/tests/projects/analysis/xoo-module-b-without-source/module_b/sonar-project.properties
new file mode 100644 (file)
index 0000000..6025243
--- /dev/null
@@ -0,0 +1,4 @@
+sonar.projectKey=module_b
+sonar.projectName=Module B
+
+sonar.sources=.
diff --git a/tests/projects/analysis/xoo-module-b-without-source/sonar-project.properties b/tests/projects/analysis/xoo-module-b-without-source/sonar-project.properties
new file mode 100644 (file)
index 0000000..8bed48f
--- /dev/null
@@ -0,0 +1,9 @@
+# Root project information
+sonar.projectKey=com.sonarsource.it.samples:multi-modules-sample
+sonar.projectName=Sonar :: Integration Tests :: Multi-modules Sample
+sonar.projectVersion=1.0-SNAPSHOT
+
+sonar.language=xoo
+# List of the module identifiers
+sonar.modules=module_a,module_b
index 51328e715c5f75015d0283ad90bafec63445b948..6b0f3c68d4ddb31893ab45606edb4f395e3a67ae 100644 (file)
@@ -31,6 +31,7 @@ import org.sonarqube.tests.analysis.LinksTest;
 import org.sonarqube.tests.analysis.MultiLanguageTest;
 import org.sonarqube.tests.analysis.PermissionTest;
 import org.sonarqube.tests.analysis.ProjectBuilderTest;
+import org.sonarqube.tests.analysis.ProjectMeasureTest;
 import org.sonarqube.tests.analysis.RedirectTest;
 import org.sonarqube.tests.analysis.ReportDumpTest;
 import org.sonarqube.tests.analysis.SSLTest;
@@ -64,7 +65,8 @@ import static util.ItUtils.xooPlugin;
   RedirectTest.class,
   // measures
   DecimalScaleMetricTest.class,
-  WebhooksTest.class
+  WebhooksTest.class,
+  ProjectMeasureTest.class
 })
 public class Category3Suite {
 
@@ -88,6 +90,9 @@ public class Category3Suite {
     // used by ProjectBuilderTest
     .addPlugin(pluginArtifact("project-builder-plugin"))
 
+    // used by ProjectMeasureTest
+    .addPlugin(pluginArtifact("save-measure-on-module-plugin"))
+
     // reduce memory for Elasticsearch to 128M
     .setServerProperty("sonar.search.javaOpts", "-Xms128m -Xmx128m")
 
diff --git a/tests/src/test/java/org/sonarqube/tests/analysis/ProjectMeasureTest.java b/tests/src/test/java/org/sonarqube/tests/analysis/ProjectMeasureTest.java
new file mode 100644 (file)
index 0000000..609ba7f
--- /dev/null
@@ -0,0 +1,121 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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.sonarqube.tests.analysis;
+
+import com.sonar.orchestrator.Orchestrator;
+import com.sonar.orchestrator.build.SonarScanner;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonarqube.tests.Category3Suite;
+import org.sonarqube.tests.Tester;
+import org.sonarqube.ws.WsProjects.CreateWsResponse.Project;
+import org.sonarqube.ws.client.measure.ComponentWsRequest;
+
+import static java.util.Arrays.asList;
+import static java.util.Collections.singletonList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.tuple;
+import static org.sonarqube.ws.WsMeasures.Measure;
+import static util.ItUtils.projectDir;
+
+public class ProjectMeasureTest {
+
+  @ClassRule
+  public static final Orchestrator orchestrator = Category3Suite.ORCHESTRATOR;
+
+  @Rule
+  public Tester tester = new Tester(orchestrator);
+
+  @Test
+  public void project_without_source_but_tests_related_measures() {
+    Project project = tester.projects().generate(null);
+
+    orchestrator.executeBuild(SonarScanner.create(projectDir("shared/xoo-sample"),
+      "sonar.projectKey", project.getKey(),
+      // Exclude all file => no source code
+      "sonar.exclusions", "**/*",
+      "sonar.measure.valueByMetric", "tests=20;test_errors=1;test_failures=2;skipped_tests=3"));
+
+    assertThat(tester.wsClient().measures().component(
+      new ComponentWsRequest()
+        .setComponent(project.getKey())
+        .setMetricKeys(asList("tests", "test_errors", "test_failures", "skipped_tests")))
+      .getComponent().getMeasuresList())
+        .extracting(Measure::getMetric, Measure::getValue)
+        .containsExactlyInAnyOrder(
+          tuple("tests", "20"),
+          tuple("test_errors", "1"),
+          tuple("test_failures", "2"),
+          tuple("skipped_tests", "3"));
+  }
+
+  @Test
+  public void module_without_source_but_tests_related_measure() {
+    Project project = tester.projects().generate(null);
+
+    orchestrator.executeBuild(SonarScanner.create(projectDir("analysis/xoo-module-b-without-source"),
+      "sonar.projectKey", project.getKey(),
+      "sonar.measure.valueByMetric", "tests=20;test_errors=1;test_failures=2;skipped_tests=3"));
+
+    String moduleBKey = project.getKey() + ":module_b";
+    assertThat(tester.wsClient().measures().component(
+      new ComponentWsRequest()
+        .setComponent(moduleBKey)
+        .setMetricKeys(asList("tests", "test_errors", "test_failures", "skipped_tests")))
+      .getComponent().getMeasuresList())
+        .extracting(Measure::getMetric, Measure::getValue)
+        .containsExactlyInAnyOrder(
+          tuple("tests", "20"),
+          tuple("test_errors", "1"),
+          tuple("test_failures", "2"),
+          tuple("skipped_tests", "3"));
+
+    assertThat(tester.wsClient().measures().component(
+      new ComponentWsRequest()
+        .setComponent(project.getKey())
+        .setMetricKeys(asList("tests", "test_errors", "test_failures", "skipped_tests")))
+      .getComponent().getMeasuresList())
+        .extracting(Measure::getMetric, Measure::getValue)
+        .containsExactlyInAnyOrder(
+          tuple("tests", "20"),
+          tuple("test_errors", "1"),
+          tuple("test_failures", "2"),
+          tuple("skipped_tests", "3"));
+  }
+
+  @Test
+  public void ignore_measure_injected_at_project_level_when_measure_is_defined_on_file() {
+    Project project = tester.projects().generate(null);
+
+    orchestrator.executeBuild(SonarScanner.create(projectDir("shared/xoo-sample-with-tests"),
+      "sonar.projectKey", project.getKey(),
+      "sonar.measure.valueByMetric", "tests=12"));
+
+    assertThat(tester.wsClient().measures().component(
+      new ComponentWsRequest()
+        .setComponent(project.getKey())
+        .setMetricKeys(singletonList("tests")))
+      .getComponent().getMeasuresList())
+        .extracting(Measure::getMetric, Measure::getValue)
+        // Measure set by the sensor is ignored
+        .containsExactlyInAnyOrder(tuple("tests", "2"));
+  }
+}