From 8e6415a4c7444f7c6e03b10e8fa8eb535cc7e505 Mon Sep 17 00:00:00 2001 From: Simon Brandhof Date: Sat, 6 Aug 2016 11:28:40 +0200 Subject: [PATCH] SONAR-7654 API to propagate props from scanner to CE --- .../main/java/AddScannerContextSensor.java | 36 +++++++ .../main/java/LogScannerContextPostTask.java | 37 ++++++++ .../src/main/java/PostTaskPlugin.java | 3 +- .../PostProjectAnalysisTasksExecutor.java | 22 ++++- .../api/posttask/ScannerContextImpl.java | 55 +++++++++++ .../batch/BatchReportReader.java | 2 + .../batch/BatchReportReaderImpl.java | 35 ++++++- .../PostProjectAnalysisTasksExecutorTest.java | 24 ++++- .../batch/BatchReportReaderRule.java | 12 +++ .../sonar/api/batch/sensor/SensorContext.java | 9 ++ .../internal/InMemorySensorStorage.java | 9 ++ .../sensor/internal/SensorContextTester.java | 13 +++ .../batch/sensor/internal/SensorStorage.java | 10 +- .../ce/posttask/PostProjectAnalysisTask.java | 8 ++ .../PostProjectAnalysisTaskTester.java | 38 ++++++++ .../sonar/api/ce/posttask/ScannerContext.java | 33 +++++++ .../internal/InMemorySensorStorageTest.java | 59 ++++++++++++ .../internal/SensorContextTesterTest.java | 11 ++- .../report/ContextPropertiesPublisher.java | 51 ++++++++++ .../repository/ContextPropertiesCache.java | 49 ++++++++++ .../scanner/scan/ProjectScanContainer.java | 6 ++ .../scanner/sensor/DefaultSensorContext.java | 4 + .../scanner/sensor/DefaultSensorStorage.java | 11 ++- .../ContextPropertiesPublisherTest.java | 94 +++++++++++++++++++ .../ContextPropertiesCacheTest.java | 66 +++++++++++++ .../sensor/DefaultSensorStorageTest.java | 15 ++- .../protocol/output/FileStructure.java | 3 + .../protocol/output/ScannerReportReader.java | 8 ++ .../protocol/output/ScannerReportWriter.java | 6 ++ .../src/main/protobuf/scanner_report.proto | 5 + .../protocol/output/FileStructureTest.java | 13 ++- 31 files changed, 729 insertions(+), 18 deletions(-) create mode 100644 it/it-plugins/posttask-plugin/src/main/java/AddScannerContextSensor.java create mode 100644 it/it-plugins/posttask-plugin/src/main/java/LogScannerContextPostTask.java create mode 100644 server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/api/posttask/ScannerContextImpl.java create mode 100644 sonar-plugin-api/src/main/java/org/sonar/api/ce/posttask/ScannerContext.java create mode 100644 sonar-plugin-api/src/test/java/org/sonar/api/batch/sensor/internal/InMemorySensorStorageTest.java create mode 100644 sonar-scanner-engine/src/main/java/org/sonar/scanner/report/ContextPropertiesPublisher.java create mode 100644 sonar-scanner-engine/src/main/java/org/sonar/scanner/repository/ContextPropertiesCache.java create mode 100644 sonar-scanner-engine/src/test/java/org/sonar/scanner/report/ContextPropertiesPublisherTest.java create mode 100644 sonar-scanner-engine/src/test/java/org/sonar/scanner/repository/ContextPropertiesCacheTest.java diff --git a/it/it-plugins/posttask-plugin/src/main/java/AddScannerContextSensor.java b/it/it-plugins/posttask-plugin/src/main/java/AddScannerContextSensor.java new file mode 100644 index 00000000000..34261a6e33b --- /dev/null +++ b/it/it-plugins/posttask-plugin/src/main/java/AddScannerContextSensor.java @@ -0,0 +1,36 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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. + */ + +import org.sonar.api.batch.sensor.Sensor; +import org.sonar.api.batch.sensor.SensorContext; +import org.sonar.api.batch.sensor.SensorDescriptor; + +public class AddScannerContextSensor implements Sensor { + @Override + public void execute(SensorContext context) { + context.addContextProperty("foo1", "bar1"); + context.addContextProperty("foo2", "bar2"); + } + + @Override + public void describe(SensorDescriptor descriptor) { + + } +} diff --git a/it/it-plugins/posttask-plugin/src/main/java/LogScannerContextPostTask.java b/it/it-plugins/posttask-plugin/src/main/java/LogScannerContextPostTask.java new file mode 100644 index 00000000000..55216b031b1 --- /dev/null +++ b/it/it-plugins/posttask-plugin/src/main/java/LogScannerContextPostTask.java @@ -0,0 +1,37 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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. + */ + +import java.util.Map; +import org.sonar.api.ce.posttask.PostProjectAnalysisTask; +import org.sonar.api.utils.log.Logger; +import org.sonar.api.utils.log.Loggers; + +public class LogScannerContextPostTask implements PostProjectAnalysisTask { + private static final Logger LOG = Loggers.get(LogScannerContextPostTask.class); + + @Override + public void finished(ProjectAnalysis analysis) { + for (Map.Entry prop : analysis.getScannerContext().getProperties().entrySet()) { + LOG.info("POSTASKPLUGIN: ScannerProperty {}={}", + prop.getKey(), + prop.getValue()); + } + } +} diff --git a/it/it-plugins/posttask-plugin/src/main/java/PostTaskPlugin.java b/it/it-plugins/posttask-plugin/src/main/java/PostTaskPlugin.java index df813bb6599..69f2e0bbfa7 100644 --- a/it/it-plugins/posttask-plugin/src/main/java/PostTaskPlugin.java +++ b/it/it-plugins/posttask-plugin/src/main/java/PostTaskPlugin.java @@ -23,6 +23,7 @@ import org.sonar.api.SonarPlugin; public class PostTaskPlugin extends SonarPlugin { public List getExtensions() { - return Arrays.asList(PostProjectAnalysisTaskImpl.class); + return Arrays.asList(PostProjectAnalysisTaskImpl.class, + LogScannerContextPostTask.class, AddScannerContextSensor.class); } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/api/posttask/PostProjectAnalysisTasksExecutor.java b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/api/posttask/PostProjectAnalysisTasksExecutor.java index c2071a83d17..10d5dda0a97 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/api/posttask/PostProjectAnalysisTasksExecutor.java +++ b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/api/posttask/PostProjectAnalysisTasksExecutor.java @@ -30,7 +30,9 @@ import org.sonar.api.ce.posttask.CeTask; import org.sonar.api.ce.posttask.PostProjectAnalysisTask; import org.sonar.api.ce.posttask.Project; import org.sonar.api.ce.posttask.QualityGate; +import org.sonar.api.ce.posttask.ScannerContext; import org.sonar.server.computation.task.projectanalysis.analysis.AnalysisMetadataHolder; +import org.sonar.server.computation.task.projectanalysis.batch.BatchReportReader; import org.sonar.server.computation.task.projectanalysis.qualitygate.Condition; import org.sonar.server.computation.task.projectanalysis.qualitygate.ConditionStatus; import org.sonar.server.computation.task.projectanalysis.qualitygate.QualityGateHolder; @@ -55,24 +57,27 @@ public class PostProjectAnalysisTasksExecutor implements ComputationStepExecutor private final QualityGateHolder qualityGateHolder; private final QualityGateStatusHolder qualityGateStatusHolder; private final PostProjectAnalysisTask[] postProjectAnalysisTasks; + private final BatchReportReader reportReader; /** * Constructor used by Pico when there is no {@link PostProjectAnalysisTask} in the container. */ public PostProjectAnalysisTasksExecutor(org.sonar.ce.queue.CeTask ceTask, AnalysisMetadataHolder analysisMetadataHolder, - QualityGateHolder qualityGateHolder, QualityGateStatusHolder qualityGateStatusHolder) { - this(ceTask, analysisMetadataHolder, qualityGateHolder, qualityGateStatusHolder, null); + QualityGateHolder qualityGateHolder, QualityGateStatusHolder qualityGateStatusHolder, + BatchReportReader reportReader) { + this(ceTask, analysisMetadataHolder, qualityGateHolder, qualityGateStatusHolder, reportReader, null); } public PostProjectAnalysisTasksExecutor(org.sonar.ce.queue.CeTask ceTask, AnalysisMetadataHolder analysisMetadataHolder, - QualityGateHolder qualityGateHolder, QualityGateStatusHolder qualityGateStatusHolder, + QualityGateHolder qualityGateHolder, QualityGateStatusHolder qualityGateStatusHolder, BatchReportReader reportReader, @Nullable PostProjectAnalysisTask[] postProjectAnalysisTasks) { this.analysisMetadataHolder = analysisMetadataHolder; this.qualityGateHolder = qualityGateHolder; this.qualityGateStatusHolder = qualityGateStatusHolder; this.ceTask = ceTask; + this.reportReader = reportReader; this.postProjectAnalysisTasks = postProjectAnalysisTasks == null ? NO_POST_PROJECT_ANALYSIS_TASKS : postProjectAnalysisTasks; } @@ -93,6 +98,7 @@ public class PostProjectAnalysisTasksExecutor implements ComputationStepExecutor new CeTaskImpl(this.ceTask.getUuid(), status), createProject(this.ceTask), getAnalysisDate(), + ScannerContextImpl.from(reportReader.readContextProperties()), status == SUCCESS ? createQualityGate(this.qualityGateHolder) : null); } @@ -147,13 +153,16 @@ public class PostProjectAnalysisTasksExecutor implements ComputationStepExecutor private final CeTask ceTask; private final Project project; private final Date date; + private final ScannerContext scannerContext; @CheckForNull private final QualityGate qualityGate; - private ProjectAnalysis(CeTask ceTask, Project project, Date date, @Nullable QualityGate qualityGate) { + private ProjectAnalysis(CeTask ceTask, Project project, Date date, + ScannerContext scannerContext, @Nullable QualityGate qualityGate) { this.ceTask = requireNonNull(ceTask, "ceTask can not be null"); this.project = requireNonNull(project, "project can not be null"); this.date = requireNonNull(date, "date can not be null"); + this.scannerContext = requireNonNull(scannerContext, "scannerContext can not be null"); this.qualityGate = qualityGate; } @@ -178,6 +187,11 @@ public class PostProjectAnalysisTasksExecutor implements ComputationStepExecutor return date; } + @Override + public ScannerContext getScannerContext() { + return scannerContext; + } + @Override public String toString() { return "ProjectAnalysis{" + diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/api/posttask/ScannerContextImpl.java b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/api/posttask/ScannerContextImpl.java new file mode 100644 index 00000000000..499e392a386 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/api/posttask/ScannerContextImpl.java @@ -0,0 +1,55 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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.server.computation.task.projectanalysis.api.posttask; + +import com.google.common.collect.ImmutableMap; +import java.util.Map; +import javax.annotation.concurrent.Immutable; +import org.sonar.api.ce.posttask.ScannerContext; +import org.sonar.core.util.CloseableIterator; +import org.sonar.scanner.protocol.output.ScannerReport; + +@Immutable +class ScannerContextImpl implements ScannerContext { + + private final Map props; + + private ScannerContextImpl(Map props) { + this.props = props; + } + + @Override + public Map getProperties() { + return props; + } + + static ScannerContextImpl from(CloseableIterator it) { + try { + ImmutableMap.Builder mapBuilder = ImmutableMap.builder(); + while (it.hasNext()) { + ScannerReport.ContextProperty prop = it.next(); + mapBuilder.put(prop.getKey(), prop.getValue()); + } + return new ScannerContextImpl(mapBuilder.build()); + } finally { + it.close(); + } + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/batch/BatchReportReader.java b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/batch/BatchReportReader.java index 41f68e6fe26..fcdd4526f24 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/batch/BatchReportReader.java +++ b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/batch/BatchReportReader.java @@ -58,4 +58,6 @@ public interface BatchReportReader { CloseableIterator readTests(int testFileRef); CloseableIterator readCoverageDetails(int testFileRef); + + CloseableIterator readContextProperties(); } diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/batch/BatchReportReaderImpl.java b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/batch/BatchReportReaderImpl.java index af8a751d748..8c6284620a8 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/batch/BatchReportReaderImpl.java +++ b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/batch/BatchReportReaderImpl.java @@ -38,16 +38,25 @@ import org.sonar.core.util.LineReaderIterator; import org.sonar.scanner.protocol.output.ScannerReport; public class BatchReportReaderImpl implements BatchReportReader { - private final org.sonar.scanner.protocol.output.ScannerReportReader delegate; + + private final BatchReportDirectoryHolder batchReportDirectoryHolder; + private org.sonar.scanner.protocol.output.ScannerReportReader delegate; // caching of metadata which are read often private ScannerReport.Metadata metadata; public BatchReportReaderImpl(BatchReportDirectoryHolder batchReportDirectoryHolder) { - this.delegate = new org.sonar.scanner.protocol.output.ScannerReportReader(batchReportDirectoryHolder.getDirectory()); + this.batchReportDirectoryHolder = batchReportDirectoryHolder; + } + + private void ensureInitialized() { + if (this.delegate == null) { + this.delegate = new org.sonar.scanner.protocol.output.ScannerReportReader(batchReportDirectoryHolder.getDirectory()); + } } @Override public ScannerReport.Metadata readMetadata() { + ensureInitialized(); if (this.metadata == null) { this.metadata = delegate.readMetadata(); } @@ -56,6 +65,7 @@ public class BatchReportReaderImpl implements BatchReportReader { @Override public CloseableIterator readScannerLogs() { + ensureInitialized(); File file = delegate.getFileStructure().analysisLog(); if (!file.exists()) { return CloseableIterator.emptyCloseableIterator(); @@ -70,64 +80,75 @@ public class BatchReportReaderImpl implements BatchReportReader { @Override public CloseableIterator readActiveRules() { + ensureInitialized(); return delegate.readActiveRules(); } @Override public CloseableIterator readComponentMeasures(int componentRef) { + ensureInitialized(); return delegate.readComponentMeasures(componentRef); } @Override @CheckForNull public ScannerReport.Changesets readChangesets(int componentRef) { + ensureInitialized(); return delegate.readChangesets(componentRef); } @Override public ScannerReport.Component readComponent(int componentRef) { + ensureInitialized(); return delegate.readComponent(componentRef); } @Override public CloseableIterator readComponentIssues(int componentRef) { + ensureInitialized(); return delegate.readComponentIssues(componentRef); } @Override public CloseableIterator readComponentDuplications(int componentRef) { + ensureInitialized(); return delegate.readComponentDuplications(componentRef); } @Override public CloseableIterator readCpdTextBlocks(int componentRef) { + ensureInitialized(); return delegate.readCpdTextBlocks(componentRef); } @Override public CloseableIterator readComponentSymbols(int componentRef) { + ensureInitialized(); return delegate.readComponentSymbols(componentRef); } @Override public CloseableIterator readComponentSyntaxHighlighting(int fileRef) { + ensureInitialized(); return delegate.readComponentSyntaxHighlighting(fileRef); } @Override public CloseableIterator readComponentCoverage(int fileRef) { + ensureInitialized(); return delegate.readComponentCoverage(fileRef); } @Override public Optional> readFileSource(int fileRef) { + ensureInitialized(); File file = delegate.readFileSource(fileRef); if (file == null) { return Optional.absent(); } try { - return Optional.>of(new CloseableLineIterator(IOUtils.lineIterator(FileUtils.openInputStream(file), StandardCharsets.UTF_8))); + return Optional.of(new CloseableLineIterator(IOUtils.lineIterator(FileUtils.openInputStream(file), StandardCharsets.UTF_8))); } catch (IOException e) { throw new IllegalStateException("Fail to traverse file: " + file, e); } @@ -164,6 +185,7 @@ public class BatchReportReaderImpl implements BatchReportReader { @Override public CloseableIterator readTests(int testFileRef) { + ensureInitialized(); File file = delegate.readTests(testFileRef); if (file == null) { return CloseableIterator.emptyCloseableIterator(); @@ -180,6 +202,7 @@ public class BatchReportReaderImpl implements BatchReportReader { @Override public CloseableIterator readCoverageDetails(int testFileRef) { + ensureInitialized(); File file = delegate.readCoverageDetails(testFileRef); if (file == null) { return CloseableIterator.emptyCloseableIterator(); @@ -194,6 +217,12 @@ public class BatchReportReaderImpl implements BatchReportReader { } } + @Override + public CloseableIterator readContextProperties() { + ensureInitialized(); + return delegate.readContextProperties(); + } + private static class ParserCloseableIterator extends CloseableIterator { private final Parser parser; private final FileInputStream fileInputStream; diff --git a/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/api/posttask/PostProjectAnalysisTasksExecutorTest.java b/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/api/posttask/PostProjectAnalysisTasksExecutorTest.java index 5d4506e68f1..52237137b3f 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/api/posttask/PostProjectAnalysisTasksExecutorTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/api/posttask/PostProjectAnalysisTasksExecutorTest.java @@ -34,7 +34,9 @@ import org.mockito.InOrder; import org.sonar.api.ce.posttask.PostProjectAnalysisTask; import org.sonar.api.ce.posttask.Project; import org.sonar.ce.queue.CeTask; +import org.sonar.scanner.protocol.output.ScannerReport; import org.sonar.server.computation.task.projectanalysis.analysis.AnalysisMetadataHolderRule; +import org.sonar.server.computation.task.projectanalysis.batch.BatchReportReaderRule; import org.sonar.server.computation.task.projectanalysis.metric.Metric; import org.sonar.server.computation.task.projectanalysis.qualitygate.Condition; import org.sonar.server.computation.task.projectanalysis.qualitygate.ConditionStatus; @@ -44,7 +46,9 @@ import org.sonar.server.computation.task.projectanalysis.qualitygate.QualityGate import org.sonar.server.computation.task.projectanalysis.qualitygate.QualityGateStatus; import static com.google.common.collect.ImmutableList.of; +import static java.util.Arrays.asList; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.data.MapEntry.entry; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -65,6 +69,9 @@ public class PostProjectAnalysisTasksExecutorTest { @Rule public MutableQualityGateStatusHolderRule qualityGateStatusHolder = new MutableQualityGateStatusHolderRule(); + @Rule + public BatchReportReaderRule reportReader = new BatchReportReaderRule(); + private ArgumentCaptor projectAnalysisArgumentCaptor = ArgumentCaptor.forClass(PostProjectAnalysisTask.ProjectAnalysis.class); private CeTask ceTask = new CeTask.Builder() .setType("type") @@ -76,7 +83,7 @@ public class PostProjectAnalysisTasksExecutorTest { private PostProjectAnalysisTask postProjectAnalysisTask = mock(PostProjectAnalysisTask.class); private PostProjectAnalysisTasksExecutor underTest = new PostProjectAnalysisTasksExecutor( ceTask, analysisMetadataHolder, qualityGateHolder, qualityGateStatusHolder, - new PostProjectAnalysisTask[] {postProjectAnalysisTask}); + reportReader, new PostProjectAnalysisTask[] {postProjectAnalysisTask}); @Before public void setUp() throws Exception { @@ -89,7 +96,7 @@ public class PostProjectAnalysisTasksExecutorTest { @Test @UseDataProvider("booleanValues") public void does_not_fail_when_there_is_no_PostProjectAnalysisTasksExecutor(boolean allStepsExecuted) { - new PostProjectAnalysisTasksExecutor(ceTask, analysisMetadataHolder, qualityGateHolder, qualityGateStatusHolder) + new PostProjectAnalysisTasksExecutor(ceTask, analysisMetadataHolder, qualityGateHolder, qualityGateStatusHolder, reportReader) .finished(allStepsExecuted); } @@ -101,7 +108,7 @@ public class PostProjectAnalysisTasksExecutorTest { InOrder inOrder = inOrder(postProjectAnalysisTask1, postProjectAnalysisTask2); new PostProjectAnalysisTasksExecutor( - ceTask, analysisMetadataHolder, qualityGateHolder, qualityGateStatusHolder, + ceTask, analysisMetadataHolder, qualityGateHolder, qualityGateStatusHolder, reportReader, new PostProjectAnalysisTask[] {postProjectAnalysisTask1, postProjectAnalysisTask2}) .finished(allStepsExecuted); @@ -180,6 +187,17 @@ public class PostProjectAnalysisTasksExecutorTest { assertThat(qualityGate.getConditions()).hasSize(2); } + @Test + public void scannerContext_loads_properties_from_scanner_report() { + reportReader.putContextProperties(asList(ScannerReport.ContextProperty.newBuilder().setKey("foo").setValue("bar").build())); + underTest.finished(true); + + verify(postProjectAnalysisTask).finished(projectAnalysisArgumentCaptor.capture()); + + org.sonar.api.ce.posttask.ScannerContext scannerContext = projectAnalysisArgumentCaptor.getValue().getScannerContext(); + assertThat(scannerContext.getProperties()).containsExactly(entry("foo", "bar")); + } + @DataProvider public static Object[][] booleanValues() { return new Object[][] { diff --git a/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/batch/BatchReportReaderRule.java b/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/batch/BatchReportReaderRule.java index 51781aaa676..4bbca4f2646 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/batch/BatchReportReaderRule.java +++ b/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/batch/BatchReportReaderRule.java @@ -26,6 +26,7 @@ import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import javax.annotation.CheckForNull; import javax.annotation.Nullable; import org.junit.rules.TestRule; @@ -38,6 +39,7 @@ public class BatchReportReaderRule implements TestRule, BatchReportReader { private ScannerReport.Metadata metadata; private List scannerLogs; private List activeRules = new ArrayList<>(); + private List contextProperties = new ArrayList<>(); private Map> measures = new HashMap<>(); private Map changesets = new HashMap<>(); private Map components = new HashMap<>(); @@ -82,6 +84,16 @@ public class BatchReportReaderRule implements TestRule, BatchReportReader { this.coverageDetails.clear(); } + @Override + public CloseableIterator readContextProperties() { + return CloseableIterator.from(contextProperties.iterator()); + } + + public BatchReportReaderRule putContextProperties(List contextProperties) { + this.contextProperties = Objects.requireNonNull(contextProperties); + return this; + } + @Override public ScannerReport.Metadata readMetadata() { if (metadata == null) { diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/SensorContext.java b/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/SensorContext.java index e11f6da0153..998ca07919a 100644 --- a/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/SensorContext.java +++ b/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/SensorContext.java @@ -144,4 +144,13 @@ public interface SensorContext { */ NewAnalysisError newAnalysisError(); + /** + * Add a property to the scanner context. This context is available + * in Compute Engine when processing the report. + * + * @see org.sonar.api.ce.posttask.PostProjectAnalysisTask.ProjectAnalysis#getScannerContext() + * @since 6.1 + */ + void addContextProperty(String key, String value); + } diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/internal/InMemorySensorStorage.java b/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/internal/InMemorySensorStorage.java index 5a79149cbc3..6cc91e10760 100644 --- a/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/internal/InMemorySensorStorage.java +++ b/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/internal/InMemorySensorStorage.java @@ -35,6 +35,8 @@ import org.sonar.api.batch.sensor.measure.Measure; import org.sonar.api.batch.sensor.symbol.internal.DefaultSymbolTable; import org.sonar.api.utils.SonarException; +import static com.google.common.base.Preconditions.checkArgument; + class InMemorySensorStorage implements SensorStorage { Table measuresByComponentAndMetric = HashBasedTable.create(); @@ -46,6 +48,7 @@ class InMemorySensorStorage implements SensorStorage { Map cpdTokensByComponent = new HashMap<>(); Table coverageByComponentAndType = HashBasedTable.create(); Map symbolsPerComponent = new HashMap<>(); + Map contextProperties = new HashMap<>(); @Override public void store(Measure measure) { @@ -108,4 +111,10 @@ class InMemorySensorStorage implements SensorStorage { allAnalysisErrors.add(analysisError); } + @Override + public void storeProperty(String key, String value) { + checkArgument(key != null, "Key of context property must not be null"); + checkArgument(value != null, "Value of context property must not be null"); + contextProperties.put(key, value); + } } diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/internal/SensorContextTester.java b/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/internal/SensorContextTester.java index ba9020829a4..f3971eb7fb7 100644 --- a/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/internal/SensorContextTester.java +++ b/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/internal/SensorContextTester.java @@ -19,6 +19,7 @@ */ package org.sonar.api.batch.sensor.internal; +import com.google.common.collect.ImmutableMap; import java.io.File; import java.io.Serializable; import java.nio.file.Path; @@ -310,4 +311,16 @@ public class SensorContextTester implements SensorContext { return null; } + @Override + public void addContextProperty(String key, String value) { + sensorStorage.storeProperty(key, value); + } + + /** + * @return an immutable map of the context properties defined with {@link SensorContext#addContextProperty(String, String)}. + * @since 6.1 + */ + public Map getContextProperties() { + return ImmutableMap.copyOf(sensorStorage.contextProperties); + } } diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/internal/SensorStorage.java b/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/internal/SensorStorage.java index 9f61766afec..cc8af33dbc5 100644 --- a/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/internal/SensorStorage.java +++ b/sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/internal/SensorStorage.java @@ -55,9 +55,17 @@ public interface SensorStorage { * @since 5.6 */ void store(DefaultSymbolTable symbolTable); - + /** * @since 6.0 */ void store(AnalysisError analysisError); + + /** + * Value is overridden if the key was already stored. + * @throws IllegalArgumentException if key is null + * @throws IllegalArgumentException if value is null + * @since 6.1 + */ + void storeProperty(String key, String value); } diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/ce/posttask/PostProjectAnalysisTask.java b/sonar-plugin-api/src/main/java/org/sonar/api/ce/posttask/PostProjectAnalysisTask.java index b33da79e6f1..b4ed6ea8963 100644 --- a/sonar-plugin-api/src/main/java/org/sonar/api/ce/posttask/PostProjectAnalysisTask.java +++ b/sonar-plugin-api/src/main/java/org/sonar/api/ce/posttask/PostProjectAnalysisTask.java @@ -74,5 +74,13 @@ public interface PostProjectAnalysisTask { * */ Date getDate(); + + /** + * Context as defined by scanner through {@link org.sonar.api.batch.sensor.SensorContext#addContextProperty(String, String)}. + * It does not contain the settings used by scanner. + * + * @since 6.1 + */ + ScannerContext getScannerContext(); } } diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/ce/posttask/PostProjectAnalysisTaskTester.java b/sonar-plugin-api/src/main/java/org/sonar/api/ce/posttask/PostProjectAnalysisTaskTester.java index 39ae7776f98..394ce62b7a4 100644 --- a/sonar-plugin-api/src/main/java/org/sonar/api/ce/posttask/PostProjectAnalysisTaskTester.java +++ b/sonar-plugin-api/src/main/java/org/sonar/api/ce/posttask/PostProjectAnalysisTaskTester.java @@ -22,7 +22,9 @@ package org.sonar.api.ce.posttask; import java.util.ArrayList; import java.util.Collection; import java.util.Date; +import java.util.HashMap; import java.util.List; +import java.util.Map; import javax.annotation.CheckForNull; import javax.annotation.Nullable; @@ -93,6 +95,7 @@ public class PostProjectAnalysisTaskTester { private static final String PROJECT_CAN_NOT_BE_NULL = "project cannot be null"; private static final String CE_TASK_CAN_NOT_BE_NULL = "ceTask cannot be null"; private static final String STATUS_CAN_NOT_BE_NULL = "status cannot be null"; + private static final String SCANNER_CONTEXT_CAN_NOT_BE_NULL = "scannerContext cannot be null"; private final PostProjectAnalysisTask underTest; @CheckForNull @@ -103,6 +106,7 @@ public class PostProjectAnalysisTaskTester { private Date date; @CheckForNull private QualityGate qualityGate; + private ScannerContext scannerContext; private PostProjectAnalysisTaskTester(PostProjectAnalysisTask underTest) { this.underTest = requireNonNull(underTest, "PostProjectAnalysisTask instance cannot be null"); @@ -128,6 +132,10 @@ public class PostProjectAnalysisTaskTester { return new ConditionBuilder(); } + public static ScannerContextBuilder newScannerContextBuilder() { + return new ScannerContextBuilder(); + } + public PostProjectAnalysisTaskTester withCeTask(CeTask ceTask) { this.ceTask = requireNonNull(ceTask, CE_TASK_CAN_NOT_BE_NULL); return this; @@ -138,6 +146,14 @@ public class PostProjectAnalysisTaskTester { return this; } + /** + * @since 6.1 + */ + public PostProjectAnalysisTaskTester withScannerContext(ScannerContext scannerContext) { + this.scannerContext = requireNonNull(scannerContext, SCANNER_CONTEXT_CAN_NOT_BE_NULL); + return this; + } + public PostProjectAnalysisTaskTester at(Date date) { this.date = requireNonNull(date, DATE_CAN_NOT_BE_NULL); return this; @@ -155,6 +171,11 @@ public class PostProjectAnalysisTaskTester { this.underTest.finished( new PostProjectAnalysisTask.ProjectAnalysis() { + @Override + public ScannerContext getScannerContext() { + return scannerContext; + } + @Override public CeTask getCeTask() { return ceTask; @@ -525,4 +546,21 @@ public class PostProjectAnalysisTaskTester { checkState(errorThreshold != null || warningThreshold != null, "At least one of errorThreshold and warningThreshold must be non null"); } } + + public static final class ScannerContextBuilder { + private final Map properties = new HashMap<>(); + + private ScannerContextBuilder() { + // prevents instantiation outside PostProjectAnalysisTaskTester + } + + public ScannerContext build() { + return new ScannerContext() { + @Override + public Map getProperties() { + return properties; + } + }; + } + } } diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/ce/posttask/ScannerContext.java b/sonar-plugin-api/src/main/java/org/sonar/api/ce/posttask/ScannerContext.java new file mode 100644 index 00000000000..6a714bdfd36 --- /dev/null +++ b/sonar-plugin-api/src/main/java/org/sonar/api/ce/posttask/ScannerContext.java @@ -0,0 +1,33 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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.api.ce.posttask; + +import java.util.Map; + +/** + * @since 6.1 + */ +public interface ScannerContext { + + /** + * @return immutable map of properties sent by scanner + */ + Map getProperties(); +} diff --git a/sonar-plugin-api/src/test/java/org/sonar/api/batch/sensor/internal/InMemorySensorStorageTest.java b/sonar-plugin-api/src/test/java/org/sonar/api/batch/sensor/internal/InMemorySensorStorageTest.java new file mode 100644 index 00000000000..22bfa504ee1 --- /dev/null +++ b/sonar-plugin-api/src/test/java/org/sonar/api/batch/sensor/internal/InMemorySensorStorageTest.java @@ -0,0 +1,59 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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.api.batch.sensor.internal; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.data.MapEntry.entry; + +public class InMemorySensorStorageTest { + + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + InMemorySensorStorage underTest = new InMemorySensorStorage(); + + @Test + public void test_storeProperty() { + assertThat(underTest.contextProperties).isEmpty(); + + underTest.storeProperty("foo", "bar"); + assertThat(underTest.contextProperties).containsOnly(entry("foo", "bar")); + } + + @Test + public void storeProperty_throws_IAE_if_key_is_null() { + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("Key of context property must not be null"); + + underTest.storeProperty(null, "bar"); + } + + @Test + public void storeProperty_throws_IAE_if_value_is_null() { + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("Value of context property must not be null"); + + underTest.storeProperty("foo", null); + } +} diff --git a/sonar-plugin-api/src/test/java/org/sonar/api/batch/sensor/internal/SensorContextTesterTest.java b/sonar-plugin-api/src/test/java/org/sonar/api/batch/sensor/internal/SensorContextTesterTest.java index a4dad0ea510..79f96f23e55 100644 --- a/sonar-plugin-api/src/test/java/org/sonar/api/batch/sensor/internal/SensorContextTesterTest.java +++ b/sonar-plugin-api/src/test/java/org/sonar/api/batch/sensor/internal/SensorContextTesterTest.java @@ -48,6 +48,7 @@ import org.sonar.api.utils.SonarException; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.tuple; +import static org.assertj.core.data.MapEntry.entry; public class SensorContextTesterTest { @@ -332,9 +333,17 @@ public class SensorContextTesterTest { } @Test - public void testCacnellation() { + public void testCancellation() { assertThat(tester.isCancelled()).isFalse(); tester.setCancelled(true); assertThat(tester.isCancelled()).isTrue(); } + + @Test + public void testContextProperties() { + assertThat(tester.getContextProperties()).isEmpty(); + + tester.addContextProperty("foo", "bar"); + assertThat(tester.getContextProperties()).containsOnly(entry("foo", "bar")); + } } diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/report/ContextPropertiesPublisher.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/report/ContextPropertiesPublisher.java new file mode 100644 index 00000000000..87b6574e612 --- /dev/null +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/report/ContextPropertiesPublisher.java @@ -0,0 +1,51 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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.scanner.report; + +import com.google.common.base.Function; +import java.util.Map; +import javax.annotation.Nonnull; +import org.sonar.scanner.protocol.output.ScannerReport; +import org.sonar.scanner.protocol.output.ScannerReportWriter; +import org.sonar.scanner.repository.ContextPropertiesCache; + +import static com.google.common.collect.FluentIterable.from; + +public class ContextPropertiesPublisher implements ReportPublisherStep { + private final ContextPropertiesCache cache; + + public ContextPropertiesPublisher(ContextPropertiesCache cache) { + this.cache = cache; + } + + @Override + public void publish(ScannerReportWriter writer) { + Iterable it = from(cache.getAll().entrySet()).transform(new MapEntryToContextPropertyFunction()); + writer.writeContextProperties(it); + } + + private static final class MapEntryToContextPropertyFunction implements Function, ScannerReport.ContextProperty> { + private final ScannerReport.ContextProperty.Builder builder = ScannerReport.ContextProperty.newBuilder(); + + public ScannerReport.ContextProperty apply(@Nonnull Map.Entry input) { + return builder.clear().setKey(input.getKey()).setValue(input.getValue()).build(); + } + } +} diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/repository/ContextPropertiesCache.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/repository/ContextPropertiesCache.java new file mode 100644 index 00000000000..db7a6785c75 --- /dev/null +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/repository/ContextPropertiesCache.java @@ -0,0 +1,49 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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.scanner.repository; + +import java.util.HashMap; +import java.util.Map; +import org.sonar.api.batch.ScannerSide; + +import static com.google.common.base.Preconditions.checkArgument; + +@ScannerSide +public class ContextPropertiesCache { + + private final Map props = new HashMap<>(); + + /** + * Value is overridden if the key was already stored. + * @throws IllegalArgumentException if key is null + * @throws IllegalArgumentException if value is null + * @since 6.1 + */ + public ContextPropertiesCache put(String key, String value) { + checkArgument(key != null, "Key of context property must not be null"); + checkArgument(value != null, "Value of context property must not be null"); + props.put(key, value); + return this; + } + + public Map getAll() { + return props; + } +} diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/ProjectScanContainer.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/ProjectScanContainer.java index 65a6ff8dada..649e994cb05 100644 --- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/ProjectScanContainer.java +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/ProjectScanContainer.java @@ -60,12 +60,14 @@ import org.sonar.scanner.profiling.PhasesSumUpTimeProfiler; import org.sonar.scanner.report.ActiveRulesPublisher; import org.sonar.scanner.report.AnalysisContextReportPublisher; import org.sonar.scanner.report.ComponentsPublisher; +import org.sonar.scanner.report.ContextPropertiesPublisher; import org.sonar.scanner.report.CoveragePublisher; import org.sonar.scanner.report.MeasuresPublisher; import org.sonar.scanner.report.MetadataPublisher; import org.sonar.scanner.report.ReportPublisher; import org.sonar.scanner.report.SourcePublisher; import org.sonar.scanner.report.TestExecutionAndCoveragePublisher; +import org.sonar.scanner.repository.ContextPropertiesCache; import org.sonar.scanner.repository.DefaultProjectRepositoriesLoader; import org.sonar.scanner.repository.DefaultQualityProfileLoader; import org.sonar.scanner.repository.DefaultServerIssuesLoader; @@ -174,6 +176,10 @@ public class ProjectScanContainer extends ComponentContainer { // Measures MeasureCache.class, + // context + ContextPropertiesCache.class, + ContextPropertiesPublisher.class, + ProjectSettings.class, // Report diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/sensor/DefaultSensorContext.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/sensor/DefaultSensorContext.java index f19633527af..ec7b8cfe5b6 100644 --- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/sensor/DefaultSensorContext.java +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/sensor/DefaultSensorContext.java @@ -152,4 +152,8 @@ public class DefaultSensorContext implements SensorContext { return false; } + @Override + public void addContextProperty(String key, String value) { + sensorStorage.storeProperty(key, value); + } } diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/sensor/DefaultSensorStorage.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/sensor/DefaultSensorStorage.java index 8487a379085..f50c95e41d2 100644 --- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/sensor/DefaultSensorStorage.java +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/sensor/DefaultSensorStorage.java @@ -64,6 +64,7 @@ import org.sonar.scanner.protocol.output.ScannerReport; import org.sonar.scanner.protocol.output.ScannerReportWriter; import org.sonar.scanner.report.ReportPublisher; import org.sonar.scanner.report.ScannerReportUtils; +import org.sonar.scanner.repository.ContextPropertiesCache; import org.sonar.scanner.scan.measure.MeasureCache; import org.sonar.scanner.sensor.coverage.CoverageExclusions; @@ -95,11 +96,14 @@ public class DefaultSensorStorage implements SensorStorage { private final ReportPublisher reportPublisher; private final MeasureCache measureCache; private final SonarCpdBlockIndex index; + private final ContextPropertiesCache contextPropertiesCache; private final Settings settings; public DefaultSensorStorage(MetricFinder metricFinder, ModuleIssues moduleIssues, Settings settings, - CoverageExclusions coverageExclusions, BatchComponentCache componentCache, ReportPublisher reportPublisher, MeasureCache measureCache, SonarCpdBlockIndex index) { + CoverageExclusions coverageExclusions, BatchComponentCache componentCache, ReportPublisher reportPublisher, + MeasureCache measureCache, SonarCpdBlockIndex index, + ContextPropertiesCache contextPropertiesCache) { this.metricFinder = metricFinder; this.moduleIssues = moduleIssues; this.settings = settings; @@ -108,6 +112,7 @@ public class DefaultSensorStorage implements SensorStorage { this.reportPublisher = reportPublisher; this.measureCache = measureCache; this.index = index; + this.contextPropertiesCache = contextPropertiesCache; } private Metric findMetricOrFail(String metricKey) { @@ -294,4 +299,8 @@ public class DefaultSensorStorage implements SensorStorage { // no op } + @Override + public void storeProperty(String key, String value) { + contextPropertiesCache.put(key, value); + } } diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scanner/report/ContextPropertiesPublisherTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scanner/report/ContextPropertiesPublisherTest.java new file mode 100644 index 00000000000..337f1b7a924 --- /dev/null +++ b/sonar-scanner-engine/src/test/java/org/sonar/scanner/report/ContextPropertiesPublisherTest.java @@ -0,0 +1,94 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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.scanner.report; + +import com.google.common.base.Function; +import com.google.common.collect.Iterables; +import com.google.common.collect.Maps; +import java.util.Map; +import javax.annotation.Nonnull; +import org.hamcrest.Description; +import org.hamcrest.TypeSafeMatcher; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.sonar.scanner.repository.ContextPropertiesCache; +import org.sonar.scanner.protocol.output.ScannerReport; +import org.sonar.scanner.protocol.output.ScannerReportWriter; + +import static org.mockito.Matchers.argThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +public class ContextPropertiesPublisherTest { + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + ContextPropertiesCache cache = new ContextPropertiesCache(); + ContextPropertiesPublisher underTest = new ContextPropertiesPublisher(cache); + + @Test + public void publish_writes_properties_to_report() { + cache.put("foo1", "bar1"); + cache.put("foo2", "bar2"); + + ScannerReportWriter writer = mock(ScannerReportWriter.class); + underTest.publish(writer); + + verify(writer).writeContextProperties(argThat(new TypeSafeMatcher>() { + @Override + protected boolean matchesSafely(Iterable props) { + Map map = Maps.uniqueIndex(props, ContextPropertyToKey.INSTANCE); + return map.size() == 2 && + map.get("foo1").getValue().equals("bar1") && + map.get("foo2").getValue().equals("bar2"); + } + + @Override + public void describeTo(Description description) { + } + })); + } + + @Test + public void publish_writes_no_properties_to_report() { + ScannerReportWriter writer = mock(ScannerReportWriter.class); + underTest.publish(writer); + + verify(writer).writeContextProperties(argThat(new TypeSafeMatcher>() { + @Override + protected boolean matchesSafely(Iterable props) { + return Iterables.isEmpty(props); + } + + @Override + public void describeTo(Description description) { + } + })); + } + + private enum ContextPropertyToKey implements Function { + INSTANCE; + @Override + public String apply(@Nonnull ScannerReport.ContextProperty input) { + return input.getKey(); + } + } +} diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scanner/repository/ContextPropertiesCacheTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scanner/repository/ContextPropertiesCacheTest.java new file mode 100644 index 00000000000..94eb23d36d1 --- /dev/null +++ b/sonar-scanner-engine/src/test/java/org/sonar/scanner/repository/ContextPropertiesCacheTest.java @@ -0,0 +1,66 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact 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.scanner.repository; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.data.MapEntry.entry; + +public class ContextPropertiesCacheTest { + + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + ContextPropertiesCache underTest = new ContextPropertiesCache(); + + @Test + public void put_property() { + assertThat(underTest.getAll()).isEmpty(); + + underTest.put("foo", "bar"); + assertThat(underTest.getAll()).containsOnly(entry("foo", "bar")); + } + + @Test + public void put_overrides_existing_value() { + underTest.put("foo", "bar"); + underTest.put("foo", "baz"); + assertThat(underTest.getAll()).containsOnly(entry("foo", "baz")); + } + + @Test + public void put_throws_IAE_if_key_is_null() { + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("Key of context property must not be null"); + + underTest.put(null, "bar"); + } + + @Test + public void put_throws_IAE_if_value_is_null() { + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("Value of context property must not be null"); + + underTest.put("foo", null); + } +} diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scanner/sensor/DefaultSensorStorageTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scanner/sensor/DefaultSensorStorageTest.java index 72bbfafac76..c0d8053ece5 100644 --- a/sonar-scanner-engine/src/test/java/org/sonar/scanner/sensor/DefaultSensorStorageTest.java +++ b/sonar-scanner-engine/src/test/java/org/sonar/scanner/sensor/DefaultSensorStorageTest.java @@ -43,10 +43,12 @@ import org.sonar.scanner.index.BatchComponentCache; import org.sonar.scanner.issue.ModuleIssues; import org.sonar.scanner.protocol.output.ScannerReportWriter; import org.sonar.scanner.report.ReportPublisher; +import org.sonar.scanner.repository.ContextPropertiesCache; import org.sonar.scanner.scan.measure.MeasureCache; import org.sonar.scanner.sensor.coverage.CoverageExclusions; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.data.MapEntry.entry; import static org.mockito.Matchers.any; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.mock; @@ -65,7 +67,7 @@ public class DefaultSensorStorageTest { private ModuleIssues moduleIssues; private Project project; private MeasureCache measureCache; - + private ContextPropertiesCache contextPropertiesCache = new ContextPropertiesCache(); private BatchComponentCache resourceCache; @Before @@ -83,7 +85,8 @@ public class DefaultSensorStorageTest { ReportPublisher reportPublisher = mock(ReportPublisher.class); when(reportPublisher.getWriter()).thenReturn(new ScannerReportWriter(temp.newFolder())); underTest = new DefaultSensorStorage(metricFinder, - moduleIssues, settings, coverageExclusions, resourceCache, reportPublisher, measureCache, mock(SonarCpdBlockIndex.class)); + moduleIssues, settings, coverageExclusions, resourceCache, reportPublisher, measureCache, + mock(SonarCpdBlockIndex.class), contextPropertiesCache); } @Test @@ -159,4 +162,12 @@ public class DefaultSensorStorageTest { underTest.store(st); } + @Test + public void shouldStoreContextProperty() { + underTest.storeProperty("foo", "bar"); + + assertThat(contextPropertiesCache.getAll()).containsOnly(entry("foo", "bar")); + + } + } diff --git a/sonar-scanner-protocol/src/main/java/org/sonar/scanner/protocol/output/FileStructure.java b/sonar-scanner-protocol/src/main/java/org/sonar/scanner/protocol/output/FileStructure.java index c4d3197cb43..5d408aef691 100644 --- a/sonar-scanner-protocol/src/main/java/org/sonar/scanner/protocol/output/FileStructure.java +++ b/sonar-scanner-protocol/src/main/java/org/sonar/scanner/protocol/output/FileStructure.java @@ -75,4 +75,7 @@ public class FileStructure { return new File(dir, domain.filePrefix + componentRef + domain.fileSuffix); } + public File contextProperties() { + return new File(dir, "context-props.pb"); + } } diff --git a/sonar-scanner-protocol/src/main/java/org/sonar/scanner/protocol/output/ScannerReportReader.java b/sonar-scanner-protocol/src/main/java/org/sonar/scanner/protocol/output/ScannerReportReader.java index 5cb0d7b8bf2..932373bb3e9 100644 --- a/sonar-scanner-protocol/src/main/java/org/sonar/scanner/protocol/output/ScannerReportReader.java +++ b/sonar-scanner-protocol/src/main/java/org/sonar/scanner/protocol/output/ScannerReportReader.java @@ -162,6 +162,14 @@ public class ScannerReportReader { return null; } + public CloseableIterator readContextProperties() { + File file = fileStructure.contextProperties(); + if (!fileExists(file)) { + return emptyCloseableIterator(); + } + return Protobuf.readStream(file, ScannerReport.ContextProperty.parser()); + } + private static boolean fileExists(File file) { return file.exists() && file.isFile(); } diff --git a/sonar-scanner-protocol/src/main/java/org/sonar/scanner/protocol/output/ScannerReportWriter.java b/sonar-scanner-protocol/src/main/java/org/sonar/scanner/protocol/output/ScannerReportWriter.java index b2da5ba4b56..f85f82495e5 100644 --- a/sonar-scanner-protocol/src/main/java/org/sonar/scanner/protocol/output/ScannerReportWriter.java +++ b/sonar-scanner-protocol/src/main/java/org/sonar/scanner/protocol/output/ScannerReportWriter.java @@ -134,6 +134,12 @@ public class ScannerReportWriter { return file; } + public File writeContextProperties(Iterable properties) { + File file = fileStructure.contextProperties(); + Protobuf.writeStream(properties, file, false); + return file; + } + public File getSourceFile(int componentRef) { return fileStructure.fileFor(FileStructure.Domain.SOURCE, componentRef); } diff --git a/sonar-scanner-protocol/src/main/protobuf/scanner_report.proto b/sonar-scanner-protocol/src/main/protobuf/scanner_report.proto index 1a6efc592f9..f0ab94c1e2a 100644 --- a/sonar-scanner-protocol/src/main/protobuf/scanner_report.proto +++ b/sonar-scanner-protocol/src/main/protobuf/scanner_report.proto @@ -45,6 +45,11 @@ message Metadata { } } +message ContextProperty { + string key = 1; + string value = 2; +} + message ActiveRule { string rule_repository = 1; string rule_key = 2; diff --git a/sonar-scanner-protocol/src/test/java/org/sonar/scanner/protocol/output/FileStructureTest.java b/sonar-scanner-protocol/src/test/java/org/sonar/scanner/protocol/output/FileStructureTest.java index 69d5266cf50..84bb9693598 100644 --- a/sonar-scanner-protocol/src/test/java/org/sonar/scanner/protocol/output/FileStructureTest.java +++ b/sonar-scanner-protocol/src/test/java/org/sonar/scanner/protocol/output/FileStructureTest.java @@ -19,12 +19,11 @@ */ package org.sonar.scanner.protocol.output; +import java.io.File; import org.apache.commons.io.FileUtils; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; -import org.sonar.scanner.protocol.output.FileStructure; -import java.io.File; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.fail; @@ -71,4 +70,14 @@ public class FileStructureTest { assertThat(structure.fileFor(FileStructure.Domain.ISSUES, 3)).exists().isFile(); assertThat(structure.fileFor(FileStructure.Domain.ISSUES, 42)).doesNotExist(); } + + @Test + public void contextProperties_file() throws Exception { + File dir = temp.newFolder(); + File file = new File(dir, "context-props.pb"); + FileUtils.write(file, "content"); + + FileStructure structure = new FileStructure(dir); + assertThat(structure.contextProperties()).exists().isFile().isEqualTo(file); + } } -- 2.39.5