From d13e2d9a08ea6fc3cb29bcf4add0c4f2f12cc3b8 Mon Sep 17 00:00:00 2001 From: Julien Lancelot Date: Mon, 16 Apr 2018 18:06:08 +0200 Subject: [PATCH] SONAR-9384 Fix computation of project without src but with test measures --- .../step/UnitTestMeasuresStep.java | 46 +++++-- ...rtFormulaExecutorComponentVisitorTest.java | 81 ++++++++++++ .../step/ReportUnitTestMeasuresStepTest.java | 50 +++++++- settings.gradle | 1 + tests/build.gradle | 11 +- .../build.gradle | 19 +++ .../sonar/measure/ModuleMeasureSensor.java | 68 ++++++++++ .../measure/SaveMeasureOnModulePlugin.java | 34 +++++ .../module_a/sonar-project.properties | 6 + .../sonar/it/samples/modules/a1/HelloA1.xoo | 16 +++ .../samples/modules/a1/HelloA1.xoo.measures | 4 + .../module_b/sonar-project.properties | 4 + .../sonar-project.properties | 9 ++ .../org/sonarqube/tests/Category3Suite.java | 9 +- .../tests/analysis/ProjectMeasureTest.java | 121 ++++++++++++++++++ 15 files changed, 459 insertions(+), 20 deletions(-) create mode 100644 tests/plugins/save-measure-on-project-plugin/build.gradle create mode 100644 tests/plugins/save-measure-on-project-plugin/src/main/java/org/sonar/measure/ModuleMeasureSensor.java create mode 100644 tests/plugins/save-measure-on-project-plugin/src/main/java/org/sonar/measure/SaveMeasureOnModulePlugin.java create mode 100644 tests/projects/analysis/xoo-module-b-without-source/module_a/sonar-project.properties create mode 100644 tests/projects/analysis/xoo-module-b-without-source/module_a/src/main/xoo/com/sonar/it/samples/modules/a1/HelloA1.xoo create mode 100644 tests/projects/analysis/xoo-module-b-without-source/module_a/src/main/xoo/com/sonar/it/samples/modules/a1/HelloA1.xoo.measures create mode 100644 tests/projects/analysis/xoo-module-b-without-source/module_b/sonar-project.properties create mode 100644 tests/projects/analysis/xoo-module-b-without-source/sonar-project.properties create mode 100644 tests/src/test/java/org/sonarqube/tests/analysis/ProjectMeasureTest.java 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 54712d2e572b..87253d0fef5e 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 @@ 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.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_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 FORMULAS = ImmutableList.of( - createLongSumFormula(TEST_EXECUTION_TIME_KEY), - createIntSumFormula(SKIPPED_TESTS_KEY), - new UnitTestsFormula()); + private static final ImmutableList FORMULAS = ImmutableList.of(new UnitTestsFormula()); private final TreeRootHolder treeRootHolder; private final MetricRepository metricRepository; @@ -84,13 +79,18 @@ public UnitTestsCounter createNewCounter() { @Override public Optional 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 Optional createMeasure(UnitTestsCounter counter, CreateMeasureCo } } - private static Optional createMeasure(Component.Type componentType, Optional metricValue) { - if (metricValue.isPresent() && CrawlerDepthLimit.LEAVES.isDeeperThan(componentType)) { + private static Optional createIntMeasure(Component currentComponent, Component leafComponent, Optional metricValue) { + if (metricValue.isPresent() && leafComponent.getType().isDeeperThan(currentComponent.getType())) { + return Optional.of(Measure.newMeasureBuilder().create(metricValue.get())); + } + return Optional.absent(); + } + + private static Optional createLongMeasure(Component currentComponent, Component leafComponent, Optional metricValue) { + if (metricValue.isPresent() && leafComponent.getType().isDeeperThan(currentComponent.getType())) { return Optional.of(Measure.newMeasureBuilder().create(metricValue.get())); } return Optional.absent(); @@ -133,19 +140,32 @@ private static class UnitTestsCounter implements Counter { 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 2c57de635853..4238d9d4d3a6 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.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.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 @@ -211,6 +215,83 @@ public void add_measure_even_when_leaf_is_not_FILE() { 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 d26db3dfee60..8426f509bc63 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 void aggregate_tests_execution_time() { } @Test - public void aggregate_skipped_tests_time() { + public void aggregate_skipped_tests() { checkMeasuresAggregation(SKIPPED_TESTS_KEY, 100, 400, 500); } @@ -258,6 +258,54 @@ public void do_not_compute_test_success_density_when_no_tests_in_failure() { 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 09bc97e3c287..a3f0c94d6859 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 f1eb642dd21f..2a6eb0c40e96 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 000000000000..b694560dd47a --- /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 000000000000..49dc52977aec --- /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 property = context.config().get(PROPERTY); + if (!property.isPresent()) { + return; + } + Map valueByMetric = KeyValueFormat.parse(property.get()); + valueByMetric.forEach((metricKey, value) -> { + org.sonar.api.batch.measure.Metric metric = metricFinder.findByKey(metricKey); + if (metric == null) { + throw new IllegalStateException(String.format("Metric '%s' doesn't exist", metricKey)); + } + NewMeasure 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 000000000000..fa28928bcc6e --- /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 000000000000..6063a2316fc8 --- /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 000000000000..74d29a4fa08b --- /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 000000000000..f3953ccd0fef --- /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 000000000000..6025243d0745 --- /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 000000000000..8bed48f65c52 --- /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 912aab2381f9..6cec2f483b3e 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.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 @@ 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 000000000000..a43bf1d4de50 --- /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")); + } +}