aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJulien Lancelot <julien.lancelot@sonarsource.com>2018-04-16 18:06:08 +0200
committerSonarTech <sonartech@sonarsource.com>2018-05-02 20:20:56 +0200
commitd13e2d9a08ea6fc3cb29bcf4add0c4f2f12cc3b8 (patch)
tree73bffcc464d1995e7c90fd27af1f84ea7f608b09
parent5c78e445609eaf1c6bb00b0c863623ea497237a0 (diff)
downloadsonarqube-d13e2d9a08ea6fc3cb29bcf4add0c4f2f12cc3b8.tar.gz
sonarqube-d13e2d9a08ea6fc3cb29bcf4add0c4f2f12cc3b8.zip
SONAR-9384 Fix computation of project without src but with test measures
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/step/UnitTestMeasuresStep.java46
-rw-r--r--server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/formula/ReportFormulaExecutorComponentVisitorTest.java81
-rw-r--r--server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/step/ReportUnitTestMeasuresStepTest.java50
-rw-r--r--settings.gradle1
-rw-r--r--tests/build.gradle11
-rw-r--r--tests/plugins/save-measure-on-project-plugin/build.gradle19
-rw-r--r--tests/plugins/save-measure-on-project-plugin/src/main/java/org/sonar/measure/ModuleMeasureSensor.java68
-rw-r--r--tests/plugins/save-measure-on-project-plugin/src/main/java/org/sonar/measure/SaveMeasureOnModulePlugin.java34
-rw-r--r--tests/projects/analysis/xoo-module-b-without-source/module_a/sonar-project.properties6
-rw-r--r--tests/projects/analysis/xoo-module-b-without-source/module_a/src/main/xoo/com/sonar/it/samples/modules/a1/HelloA1.xoo16
-rw-r--r--tests/projects/analysis/xoo-module-b-without-source/module_a/src/main/xoo/com/sonar/it/samples/modules/a1/HelloA1.xoo.measures4
-rw-r--r--tests/projects/analysis/xoo-module-b-without-source/module_b/sonar-project.properties4
-rw-r--r--tests/projects/analysis/xoo-module-b-without-source/sonar-project.properties9
-rw-r--r--tests/src/test/java/org/sonarqube/tests/Category3Suite.java9
-rw-r--r--tests/src/test/java/org/sonarqube/tests/analysis/ProjectMeasureTest.java121
15 files changed, 459 insertions, 20 deletions
diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/step/UnitTestMeasuresStep.java b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/step/UnitTestMeasuresStep.java
index 54712d2e572..87253d0fef5 100644
--- a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/step/UnitTestMeasuresStep.java
+++ b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/step/UnitTestMeasuresStep.java
@@ -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.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;
}
}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/formula/ReportFormulaExecutorComponentVisitorTest.java b/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/formula/ReportFormulaExecutorComponentVisitorTest.java
index 2c57de63585..4238d9d4d3a 100644
--- a/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/formula/ReportFormulaExecutorComponentVisitorTest.java
+++ b/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/formula/ReportFormulaExecutorComponentVisitorTest.java
@@ -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;
@@ -79,6 +81,8 @@ public class ReportFormulaExecutorComponentVisitorTest {
.build();
@Rule
+ public ExpectedException expectedException = ExpectedException.none();
+ @Rule
public TreeRootHolderRule treeRootHolder = new TreeRootHolderRule();
@Rule
public MetricRepositoryRule metricRepository = new MetricRepositoryRule()
@@ -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)
diff --git a/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/step/ReportUnitTestMeasuresStepTest.java b/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/step/ReportUnitTestMeasuresStepTest.java
index d26db3dfee6..8426f509bc6 100644
--- a/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/step/ReportUnitTestMeasuresStepTest.java
+++ b/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/step/ReportUnitTestMeasuresStepTest.java
@@ -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));
diff --git a/settings.gradle b/settings.gradle
index 09bc97e3c28..a3f0c94d685 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -50,6 +50,7 @@ include 'tests:plugins:posttask-plugin'
include 'tests:plugins:project-builder-plugin'
include 'tests:plugins:property-relocation-plugin'
include 'tests:plugins:property-sets-plugin'
+include 'tests:plugins:save-measure-on-project-plugin'
include 'tests:plugins:security-plugin'
include 'tests:plugins:server-plugin'
include 'tests:plugins:settings-encryption-plugin'
diff --git a/tests/build.gradle b/tests/build.gradle
index f1eb642dd21..2a6eb0c40e9 100644
--- a/tests/build.gradle
+++ b/tests/build.gradle
@@ -11,6 +11,9 @@ configurations {
def pluginsForITs = [
':plugins:sonar-xoo-plugin',
':tests:plugins:access-secured-props-plugin',
+ ':tests:plugins:backdating-plugin-v1',
+ ':tests:plugins:backdating-plugin-v2',
+ ':tests:plugins:backdating-customplugin',
':tests:plugins:base-auth-plugin',
':tests:plugins:batch-plugin',
':tests:plugins:extension-lifecycle-plugin',
@@ -23,9 +26,11 @@ def pluginsForITs = [
':tests:plugins:l10n-fr-pack',
':tests:plugins:license-plugin',
':tests:plugins:oauth2-auth-plugin',
+ ':tests:plugins:posttask-plugin',
':tests:plugins:project-builder-plugin',
':tests:plugins:property-relocation-plugin',
':tests:plugins:property-sets-plugin',
+ ':tests:plugins:save-measure-on-project-plugin',
':tests:plugins:security-plugin',
':tests:plugins:server-plugin',
':tests:plugins:settings-encryption-plugin',
@@ -33,12 +38,8 @@ def pluginsForITs = [
':tests:plugins:sonar-fake-plugin',
':tests:plugins:sonar-subcategories-plugin',
':tests:plugins:ui-extensions-plugin',
- ':tests:plugins:posttask-plugin',
':tests:plugins:wait-at-platform-level4-plugin',
- ':tests:plugins:ws-plugin',
- ':tests:plugins:backdating-plugin-v1',
- ':tests:plugins:backdating-plugin-v2',
- ':tests:plugins:backdating-customplugin'
+ ':tests:plugins:ws-plugin'
]
dependencies {
diff --git a/tests/plugins/save-measure-on-project-plugin/build.gradle b/tests/plugins/save-measure-on-project-plugin/build.gradle
new file mode 100644
index 00000000000..b694560dd47
--- /dev/null
+++ b/tests/plugins/save-measure-on-project-plugin/build.gradle
@@ -0,0 +1,19 @@
+dependencies {
+ compileOnly project(path: ':sonar-plugin-api', configuration: 'shadow')
+}
+
+jar {
+ manifest {
+ attributes(
+ 'Plugin-Key': 'save-measure-on-project-plugin',
+ 'Plugin-Version': version,
+ 'Plugin-Class': 'org.sonar.measure.SaveMeasureOnModulePlugin',
+ 'Plugin-ChildFirstClassLoader': 'false',
+ 'Sonar-Version': version,
+ 'SonarLint-Supported': 'false',
+ 'Plugin-Name': 'Custom',
+ 'Plugin-License': 'GNU LGPL 3',
+ )
+ }
+}
+
diff --git a/tests/plugins/save-measure-on-project-plugin/src/main/java/org/sonar/measure/ModuleMeasureSensor.java b/tests/plugins/save-measure-on-project-plugin/src/main/java/org/sonar/measure/ModuleMeasureSensor.java
new file mode 100644
index 00000000000..49dc52977ae
--- /dev/null
+++ b/tests/plugins/save-measure-on-project-plugin/src/main/java/org/sonar/measure/ModuleMeasureSensor.java
@@ -0,0 +1,68 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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();
+ });
+ }
+
+}
diff --git a/tests/plugins/save-measure-on-project-plugin/src/main/java/org/sonar/measure/SaveMeasureOnModulePlugin.java b/tests/plugins/save-measure-on-project-plugin/src/main/java/org/sonar/measure/SaveMeasureOnModulePlugin.java
new file mode 100644
index 00000000000..fa28928bcc6
--- /dev/null
+++ b/tests/plugins/save-measure-on-project-plugin/src/main/java/org/sonar/measure/SaveMeasureOnModulePlugin.java
@@ -0,0 +1,34 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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
index 00000000000..6063a2316fc
--- /dev/null
+++ b/tests/projects/analysis/xoo-module-b-without-source/module_a/sonar-project.properties
@@ -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
index 00000000000..74d29a4fa08
--- /dev/null
+++ b/tests/projects/analysis/xoo-module-b-without-source/module_a/src/main/xoo/com/sonar/it/samples/modules/a1/HelloA1.xoo
@@ -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
index 00000000000..f3953ccd0fe
--- /dev/null
+++ b/tests/projects/analysis/xoo-module-b-without-source/module_a/src/main/xoo/com/sonar/it/samples/modules/a1/HelloA1.xoo.measures
@@ -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
index 00000000000..6025243d074
--- /dev/null
+++ b/tests/projects/analysis/xoo-module-b-without-source/module_b/sonar-project.properties
@@ -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
index 00000000000..8bed48f65c5
--- /dev/null
+++ b/tests/projects/analysis/xoo-module-b-without-source/sonar-project.properties
@@ -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
diff --git a/tests/src/test/java/org/sonarqube/tests/Category3Suite.java b/tests/src/test/java/org/sonarqube/tests/Category3Suite.java
index 912aab2381f..6cec2f483b3 100644
--- a/tests/src/test/java/org/sonarqube/tests/Category3Suite.java
+++ b/tests/src/test/java/org/sonarqube/tests/Category3Suite.java
@@ -32,6 +32,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;
@@ -66,7 +67,8 @@ import static util.ItUtils.xooPlugin;
ReportDumpTest.class,
SSLTest.class,
FavoriteTest.class,
- RedirectTest.class
+ RedirectTest.class,
+ ProjectMeasureTest.class
})
public class Category3Suite {
@@ -90,5 +92,10 @@ public class Category3Suite {
// used by ProjectBuilderTest
.addPlugin(pluginArtifact("project-builder-plugin"))
+ // used by ProjectWithoutSourceTest
+ .addPlugin(pluginArtifact("save-measure-on-project-plugin"))
+
+ .setServerProperty("sonar.ce.javaAdditionalOpts", "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005")
+
.build();
}
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
index 00000000000..a43bf1d4de5
--- /dev/null
+++ b/tests/src/test/java/org/sonarqube/tests/analysis/ProjectMeasureTest.java
@@ -0,0 +1,121 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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.qa.util.Tester;
+import org.sonarqube.tests.Category3Suite;
+import org.sonarqube.ws.Measures;
+import org.sonarqube.ws.Projects.CreateWsResponse.Project;
+import org.sonarqube.ws.client.measures.ComponentRequest;
+
+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 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().provision();
+
+ 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 ComponentRequest()
+ .setComponent(project.getKey())
+ .setMetricKeys(asList("tests", "test_errors", "test_failures", "skipped_tests")))
+ .getComponent().getMeasuresList())
+ .extracting(Measures.Measure::getMetric, Measures.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().provision();
+
+ 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 ComponentRequest()
+ .setComponent(moduleBKey)
+ .setMetricKeys(asList("tests", "test_errors", "test_failures", "skipped_tests")))
+ .getComponent().getMeasuresList())
+ .extracting(Measures.Measure::getMetric, Measures.Measure::getValue)
+ .containsExactlyInAnyOrder(
+ tuple("tests", "20"),
+ tuple("test_errors", "1"),
+ tuple("test_failures", "2"),
+ tuple("skipped_tests", "3"));
+
+ assertThat(tester.wsClient().measures().component(
+ new ComponentRequest()
+ .setComponent(project.getKey())
+ .setMetricKeys(asList("tests", "test_errors", "test_failures", "skipped_tests")))
+ .getComponent().getMeasuresList())
+ .extracting(Measures.Measure::getMetric, Measures.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().provision();
+
+ 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 ComponentRequest()
+ .setComponent(project.getKey())
+ .setMetricKeys(singletonList("tests")))
+ .getComponent().getMeasuresList())
+ .extracting(Measures.Measure::getMetric, Measures.Measure::getValue)
+ // Measure set by the sensor is ignored
+ .containsExactlyInAnyOrder(tuple("tests", "2"));
+ }
+}