]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-7654 API to propagate props from scanner to CE
authorSimon Brandhof <simon.brandhof@sonarsource.com>
Sat, 6 Aug 2016 09:28:40 +0000 (11:28 +0200)
committerSimon Brandhof <simon.brandhof@sonarsource.com>
Sat, 6 Aug 2016 10:52:33 +0000 (12:52 +0200)
31 files changed:
it/it-plugins/posttask-plugin/src/main/java/AddScannerContextSensor.java [new file with mode: 0644]
it/it-plugins/posttask-plugin/src/main/java/LogScannerContextPostTask.java [new file with mode: 0644]
it/it-plugins/posttask-plugin/src/main/java/PostTaskPlugin.java
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/api/posttask/PostProjectAnalysisTasksExecutor.java
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/api/posttask/ScannerContextImpl.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/batch/BatchReportReader.java
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/batch/BatchReportReaderImpl.java
server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/api/posttask/PostProjectAnalysisTasksExecutorTest.java
server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/batch/BatchReportReaderRule.java
sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/SensorContext.java
sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/internal/InMemorySensorStorage.java
sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/internal/SensorContextTester.java
sonar-plugin-api/src/main/java/org/sonar/api/batch/sensor/internal/SensorStorage.java
sonar-plugin-api/src/main/java/org/sonar/api/ce/posttask/PostProjectAnalysisTask.java
sonar-plugin-api/src/main/java/org/sonar/api/ce/posttask/PostProjectAnalysisTaskTester.java
sonar-plugin-api/src/main/java/org/sonar/api/ce/posttask/ScannerContext.java [new file with mode: 0644]
sonar-plugin-api/src/test/java/org/sonar/api/batch/sensor/internal/InMemorySensorStorageTest.java [new file with mode: 0644]
sonar-plugin-api/src/test/java/org/sonar/api/batch/sensor/internal/SensorContextTesterTest.java
sonar-scanner-engine/src/main/java/org/sonar/scanner/report/ContextPropertiesPublisher.java [new file with mode: 0644]
sonar-scanner-engine/src/main/java/org/sonar/scanner/repository/ContextPropertiesCache.java [new file with mode: 0644]
sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/ProjectScanContainer.java
sonar-scanner-engine/src/main/java/org/sonar/scanner/sensor/DefaultSensorContext.java
sonar-scanner-engine/src/main/java/org/sonar/scanner/sensor/DefaultSensorStorage.java
sonar-scanner-engine/src/test/java/org/sonar/scanner/report/ContextPropertiesPublisherTest.java [new file with mode: 0644]
sonar-scanner-engine/src/test/java/org/sonar/scanner/repository/ContextPropertiesCacheTest.java [new file with mode: 0644]
sonar-scanner-engine/src/test/java/org/sonar/scanner/sensor/DefaultSensorStorageTest.java
sonar-scanner-protocol/src/main/java/org/sonar/scanner/protocol/output/FileStructure.java
sonar-scanner-protocol/src/main/java/org/sonar/scanner/protocol/output/ScannerReportReader.java
sonar-scanner-protocol/src/main/java/org/sonar/scanner/protocol/output/ScannerReportWriter.java
sonar-scanner-protocol/src/main/protobuf/scanner_report.proto
sonar-scanner-protocol/src/test/java/org/sonar/scanner/protocol/output/FileStructureTest.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 (file)
index 0000000..34261a6
--- /dev/null
@@ -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 (file)
index 0000000..55216b0
--- /dev/null
@@ -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<String, String> prop : analysis.getScannerContext().getProperties().entrySet()) {
+      LOG.info("POSTASKPLUGIN: ScannerProperty {}={}",
+        prop.getKey(),
+        prop.getValue());
+    }
+  }
+}
index df813bb6599002838d4b431da752ffce4a5ef547..69f2e0bbfa73cf1e3c012e925fd0f6bc64df4493 100644 (file)
@@ -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);
   }
 }
index c2071a83d17fa3b290646ab42d801f79fb770867..10d5dda0a9732b158b9d242ab07c5f0e6f0ed1ce 100644 (file)
@@ -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 (file)
index 0000000..499e392
--- /dev/null
@@ -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<String, String> props;
+
+  private ScannerContextImpl(Map<String, String> props) {
+    this.props = props;
+  }
+
+  @Override
+  public Map<String, String> getProperties() {
+    return props;
+  }
+
+  static ScannerContextImpl from(CloseableIterator<ScannerReport.ContextProperty> it) {
+    try {
+      ImmutableMap.Builder<String, String> 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();
+    }
+  }
+}
index 41f68e6fe26ecbebea5dda78638809453c6b41dd..fcdd4526f24781c1ac8c087279657676dd54468f 100644 (file)
@@ -58,4 +58,6 @@ public interface BatchReportReader {
   CloseableIterator<ScannerReport.Test> readTests(int testFileRef);
 
   CloseableIterator<ScannerReport.CoverageDetail> readCoverageDetails(int testFileRef);
+
+  CloseableIterator<ScannerReport.ContextProperty> readContextProperties();
 }
index af8a751d748b16c1c13dd9be991456e4fe29a208..8c6284620a82fff3900ddd8e584fefab3a2db825 100644 (file)
@@ -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<String> 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<ScannerReport.ActiveRule> readActiveRules() {
+    ensureInitialized();
     return delegate.readActiveRules();
   }
 
   @Override
   public CloseableIterator<ScannerReport.Measure> 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<ScannerReport.Issue> readComponentIssues(int componentRef) {
+    ensureInitialized();
     return delegate.readComponentIssues(componentRef);
   }
 
   @Override
   public CloseableIterator<ScannerReport.Duplication> readComponentDuplications(int componentRef) {
+    ensureInitialized();
     return delegate.readComponentDuplications(componentRef);
   }
 
   @Override
   public CloseableIterator<ScannerReport.CpdTextBlock> readCpdTextBlocks(int componentRef) {
+    ensureInitialized();
     return delegate.readCpdTextBlocks(componentRef);
   }
 
   @Override
   public CloseableIterator<ScannerReport.Symbol> readComponentSymbols(int componentRef) {
+    ensureInitialized();
     return delegate.readComponentSymbols(componentRef);
   }
 
   @Override
   public CloseableIterator<ScannerReport.SyntaxHighlightingRule> readComponentSyntaxHighlighting(int fileRef) {
+    ensureInitialized();
     return delegate.readComponentSyntaxHighlighting(fileRef);
   }
 
   @Override
   public CloseableIterator<ScannerReport.LineCoverage> readComponentCoverage(int fileRef) {
+    ensureInitialized();
     return delegate.readComponentCoverage(fileRef);
   }
 
   @Override
   public Optional<CloseableIterator<String>> readFileSource(int fileRef) {
+    ensureInitialized();
     File file = delegate.readFileSource(fileRef);
     if (file == null) {
       return Optional.absent();
     }
 
     try {
-      return Optional.<CloseableIterator<String>>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<ScannerReport.Test> 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<ScannerReport.CoverageDetail> 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<ScannerReport.ContextProperty> readContextProperties() {
+    ensureInitialized();
+    return delegate.readContextProperties();
+  }
+
   private static class ParserCloseableIterator<T> extends CloseableIterator<T> {
     private final Parser<T> parser;
     private final FileInputStream fileInputStream;
index 5d4506e68f1b204734b8b498dbbc804bb8c5cab2..52237137b3fe32d5ed11bb0e5323c2be89af6d30 100644 (file)
@@ -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<PostProjectAnalysisTask.ProjectAnalysis> 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[][] {
index 51781aaa676d3ade9a3867cd7824b9a122a83e14..4bbca4f2646953bf4ea1b1e27ee26b64eb28a56f 100644 (file)
@@ -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<String> scannerLogs;
   private List<ScannerReport.ActiveRule> activeRules = new ArrayList<>();
+  private List<ScannerReport.ContextProperty> contextProperties = new ArrayList<>();
   private Map<Integer, List<ScannerReport.Measure>> measures = new HashMap<>();
   private Map<Integer, ScannerReport.Changesets> changesets = new HashMap<>();
   private Map<Integer, ScannerReport.Component> components = new HashMap<>();
@@ -82,6 +84,16 @@ public class BatchReportReaderRule implements TestRule, BatchReportReader {
     this.coverageDetails.clear();
   }
 
+  @Override
+  public CloseableIterator<ScannerReport.ContextProperty> readContextProperties() {
+    return CloseableIterator.from(contextProperties.iterator());
+  }
+
+  public BatchReportReaderRule putContextProperties(List<ScannerReport.ContextProperty> contextProperties) {
+    this.contextProperties = Objects.requireNonNull(contextProperties);
+    return this;
+  }
+
   @Override
   public ScannerReport.Metadata readMetadata() {
     if (metadata == null) {
index e11f6da0153caed2b21640df9f9cd310eeb55750..998ca07919a441ab80b5272c155d03e96c35cb3b 100644 (file)
@@ -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);
+
 }
index 5a79149cbc3eea0eda8506af5fdbbefac7a6a319..6cc91e10760f4a29db8f8bbde9d56571f1ecba0a 100644 (file)
@@ -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<String, String, Measure> measuresByComponentAndMetric = HashBasedTable.create();
@@ -46,6 +48,7 @@ class InMemorySensorStorage implements SensorStorage {
   Map<String, DefaultCpdTokens> cpdTokensByComponent = new HashMap<>();
   Table<String, CoverageType, DefaultCoverage> coverageByComponentAndType = HashBasedTable.create();
   Map<String, DefaultSymbolTable> symbolsPerComponent = new HashMap<>();
+  Map<String, String> 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);
+  }
 }
index ba9020829a4d40b4fe5aa67c8cb63de2fe07d18b..f3971eb7fb7db80d292206b32fa5539135f9046c 100644 (file)
@@ -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<String, String> getContextProperties() {
+    return ImmutableMap.copyOf(sensorStorage.contextProperties);
+  }
 }
index 9f61766afec78843252e19ab265f9abc65459273..cc8af33dbc502d99d0e09cf57996a96fdb583a62 100644 (file)
@@ -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);
 }
index b33da79e6f1157bf89d7d7123a3a98ee431e21f7..b4ed6ea896385b254844a869a611705613bc97ad 100644 (file)
@@ -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();
   }
 }
index 39ae7776f98790641c3052bf64b71d0540fb7fa9..394ce62b7a43fbec4f2ffcfe41d26b1219bfc774 100644 (file)
@@ -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<String, String> properties = new HashMap<>();
+
+    private ScannerContextBuilder() {
+      // prevents instantiation outside PostProjectAnalysisTaskTester
+    }
+
+    public ScannerContext build() {
+      return new ScannerContext() {
+        @Override
+        public Map<String, String> 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 (file)
index 0000000..6a714bd
--- /dev/null
@@ -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<String, String> 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 (file)
index 0000000..22bfa50
--- /dev/null
@@ -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);
+  }
+}
index a4dad0ea510bdf05a8a5a1d28e82a8b1c093cdeb..79f96f23e553c51ba5e736b3637a198c285cc637 100644 (file)
@@ -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 (file)
index 0000000..87b6574
--- /dev/null
@@ -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<ScannerReport.ContextProperty> it = from(cache.getAll().entrySet()).transform(new MapEntryToContextPropertyFunction());
+    writer.writeContextProperties(it);
+  }
+
+  private static final class MapEntryToContextPropertyFunction implements Function<Map.Entry<String, String>, ScannerReport.ContextProperty> {
+    private final ScannerReport.ContextProperty.Builder builder = ScannerReport.ContextProperty.newBuilder();
+
+    public ScannerReport.ContextProperty apply(@Nonnull Map.Entry<String, String> 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 (file)
index 0000000..db7a678
--- /dev/null
@@ -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<String, String> 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<String, String> getAll() {
+    return props;
+  }
+}
index 65a6ff8dada7fd6d7d6e2d85081d0b69e2d5ffe1..649e994cb05521730933645d95ace4bbfbb3f0ed 100644 (file)
@@ -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
index f19633527af6648591fd334e3e901443c53a4e02..ec7b8cfe5b65c7d5da274870015333a36050fe98 100644 (file)
@@ -152,4 +152,8 @@ public class DefaultSensorContext implements SensorContext {
     return false;
   }
 
+  @Override
+  public void addContextProperty(String key, String value) {
+    sensorStorage.storeProperty(key, value);
+  }
 }
index 8487a37908539beebf8507b052742769085e265a..f50c95e41d2a53d2aa765fe015f00e82fc7a53d8 100644 (file)
@@ -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 (file)
index 0000000..337f1b7
--- /dev/null
@@ -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<Iterable<ScannerReport.ContextProperty>>() {
+      @Override
+      protected boolean matchesSafely(Iterable<ScannerReport.ContextProperty> props) {
+        Map<String, ScannerReport.ContextProperty> 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<Iterable<ScannerReport.ContextProperty>>() {
+      @Override
+      protected boolean matchesSafely(Iterable<ScannerReport.ContextProperty> props) {
+        return Iterables.isEmpty(props);
+      }
+
+      @Override
+      public void describeTo(Description description) {
+      }
+    }));
+  }
+
+  private enum ContextPropertyToKey implements Function<ScannerReport.ContextProperty, String> {
+    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 (file)
index 0000000..94eb23d
--- /dev/null
@@ -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);
+  }
+}
index 72bbfafac7691f207e279aa1f80ee39c73eace1f..c0d8053ece5b58a08dcf6c0ef9de9dae1ff029bd 100644 (file)
@@ -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"));
+
+  }
+
 }
index c4d3197cb43a2c5a7911b6aa909b3c17bc94d504..5d408aef691a77b34a1ff30b6e9f10096c94f784 100644 (file)
@@ -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");
+  }
 }
index 5cb0d7b8bf24c1c98af0f511f515fe508413384e..932373bb3e937987dc6a57f91bf94f8c4072eaea 100644 (file)
@@ -162,6 +162,14 @@ public class ScannerReportReader {
     return null;
   }
 
+  public CloseableIterator<ScannerReport.ContextProperty> 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();
   }
index b2da5ba4b569e0817161fbfbfb38bc1a0d44be3a..f85f82495e5a6bac39c40b2292625bacdd1b2b8d 100644 (file)
@@ -134,6 +134,12 @@ public class ScannerReportWriter {
     return file;
   }
 
+  public File writeContextProperties(Iterable<ScannerReport.ContextProperty> properties) {
+    File file = fileStructure.contextProperties();
+    Protobuf.writeStream(properties, file, false);
+    return file;
+  }
+
   public File getSourceFile(int componentRef) {
     return fileStructure.fileFor(FileStructure.Domain.SOURCE, componentRef);
   }
index 1a6efc592f9fb4f2bf2c7d5d2ddd9c378fad34c3..f0ab94c1e2a41bfe67b0f34e180cb0366af99f90 100644 (file)
@@ -45,6 +45,11 @@ message Metadata {
   }
 }
 
+message ContextProperty {
+  string key = 1;
+  string value = 2;
+}
+
 message ActiveRule {
   string rule_repository = 1;
   string rule_key = 2;
index 69d5266cf50b849fdd9a7d8c1910a2d10c2ee198..84bb96935984deb3042732a44d02228c5e7cdfc3 100644 (file)
  */
 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);
+  }
 }