]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-23327 Implement addTelemetryProperty in the scanner engine
authorMatteo Mara <matteo.mara@sonarsource.com>
Wed, 9 Oct 2024 13:09:39 +0000 (15:09 +0200)
committersonartech <sonartech@sonarsource.com>
Fri, 11 Oct 2024 20:02:42 +0000 (20:02 +0000)
18 files changed:
sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/sensor/internal/InMemorySensorStorage.java
sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/sensor/internal/SensorContextTester.java
sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/SpringScannerContainer.java
sonar-scanner-engine/src/main/java/org/sonar/scanner/report/TelemetryPublisher.java [new file with mode: 0644]
sonar-scanner-engine/src/main/java/org/sonar/scanner/repository/TelemetryCache.java [new file with mode: 0644]
sonar-scanner-engine/src/main/java/org/sonar/scanner/sensor/DefaultSensorStorage.java
sonar-scanner-engine/src/main/java/org/sonar/scanner/sensor/ModuleSensorContext.java
sonar-scanner-engine/src/main/java/org/sonar/scanner/sensor/ProjectSensorContext.java
sonar-scanner-engine/src/test/java/org/sonar/scanner/report/TelemetryPublisherTest.java [new file with mode: 0644]
sonar-scanner-engine/src/test/java/org/sonar/scanner/repository/TelemetryCacheTest.java [new file with mode: 0644]
sonar-scanner-engine/src/test/java/org/sonar/scanner/sensor/DefaultSensorStorageTest.java
sonar-scanner-engine/src/test/java/org/sonar/scanner/sensor/ModuleSensorContextTest.java
sonar-scanner-engine/src/test/java/org/sonar/scanner/sensor/ProjectSensorContextTest.java [new file with mode: 0644]
sonar-scanner-protocol/src/main/java/org/sonar/scanner/protocol/output/FileStructure.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
sonar-scanner-protocol/src/test/java/org/sonar/scanner/protocol/output/ScannerReportWriterTest.java

index de7add875beb4b963579a7ee80131b839e32b1db..0b4d729aee8a78414f9e576157aa5fbfaa550448 100644 (file)
@@ -57,6 +57,7 @@ class InMemorySensorStorage implements SensorStorage {
   Map<String, List<DefaultCoverage>> coverageByComponent = new HashMap<>();
   Map<String, DefaultSymbolTable> symbolsPerComponent = new HashMap<>();
   Map<String, String> contextProperties = new HashMap<>();
+  Map<String, String> telemetryEntries = new HashMap<>();
   Map<String, DefaultSignificantCode> significantCodePerComponent = new HashMap<>();
 
   @Override
@@ -132,6 +133,15 @@ class InMemorySensorStorage implements SensorStorage {
     contextProperties.put(key, value);
   }
 
+  public void storeTelemetry(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");
+
+    if (telemetryEntries.size() < 1000 || telemetryEntries.containsKey(key)) {
+      telemetryEntries.put(key, value);
+    }
+  }
+
   @Override
   public void store(ExternalIssue issue) {
     allExternalIssues.add(issue);
index ca8aae07c0fbf3d6af5c82c922fbdfaac2144305..c94a0ccaff0cecc94e1227735f52ed95b12da7b6 100644 (file)
@@ -442,8 +442,9 @@ public class SensorContextTester implements SensorContext {
   }
 
   @Override
-  public void addTelemetryProperty(String s, String s1) {
-    throw new UnsupportedOperationException("addTelemetryProperty");
+  public void addTelemetryProperty(String key, String value) {
+    //No Need to check the source of the plugin in the tester
+    sensorStorage.storeTelemetry(key, value);
   }
 
   public void setCacheEnabled(boolean enabled) {
index 16d66d5b460b2ce336b11077597829dfca35be8a..44afe06d7af5a10e3bfef5bb1780c6cca5c19f16 100644 (file)
@@ -79,6 +79,7 @@ import org.sonar.scanner.report.MetadataPublisher;
 import org.sonar.scanner.report.ReportPublisher;
 import org.sonar.scanner.report.ScannerFileStructureProvider;
 import org.sonar.scanner.report.SourcePublisher;
+import org.sonar.scanner.report.TelemetryPublisher;
 import org.sonar.scanner.report.TestExecutionPublisher;
 import org.sonar.scanner.repository.ContextPropertiesCache;
 import org.sonar.scanner.repository.DefaultProjectRepositoriesLoader;
@@ -86,6 +87,7 @@ import org.sonar.scanner.repository.DefaultQualityProfileLoader;
 import org.sonar.scanner.repository.ProjectRepositoriesProvider;
 import org.sonar.scanner.repository.QualityProfilesProvider;
 import org.sonar.scanner.repository.ReferenceBranchSupplier;
+import org.sonar.scanner.repository.TelemetryCache;
 import org.sonar.scanner.repository.language.DefaultLanguagesLoader;
 import org.sonar.scanner.repository.language.DefaultLanguagesRepository;
 import org.sonar.scanner.repository.settings.DefaultProjectSettingsLoader;
@@ -235,6 +237,7 @@ public class SpringScannerContainer extends SpringComponentContainer {
 
       // context
       ContextPropertiesCache.class,
+      TelemetryCache.class,
 
       MutableProjectSettings.class,
       SonarGlobalPropertiesFilter.class,
@@ -258,6 +261,7 @@ public class SpringScannerContainer extends SpringComponentContainer {
       ActiveRulesPublisher.class,
       ComponentsPublisher.class,
       ContextPropertiesPublisher.class,
+      TelemetryPublisher.class,
       AnalysisCachePublisher.class,
       TestExecutionPublisher.class,
       SourcePublisher.class,
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/report/TelemetryPublisher.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/report/TelemetryPublisher.java
new file mode 100644 (file)
index 0000000..fdc2e96
--- /dev/null
@@ -0,0 +1,44 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.scanner.report;
+
+import org.sonar.scanner.protocol.output.ScannerReport;
+import org.sonar.scanner.protocol.output.ScannerReportWriter;
+import org.sonar.scanner.repository.TelemetryCache;
+
+public class TelemetryPublisher implements ReportPublisherStep {
+  private final TelemetryCache telemetryCache;
+
+  public TelemetryPublisher(TelemetryCache telemetryCache) {
+    this.telemetryCache = telemetryCache;
+  }
+
+  @Override
+  public void publish(ScannerReportWriter writer) {
+    writer.writeTelemetry(telemetryCache.getAll().entrySet()
+      .stream()
+      .map(e -> ScannerReport.TelemetryEntry.newBuilder()
+        .setKey(e.getKey())
+        .setValue(e.getValue())
+        .build())
+      .toList());
+  }
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/repository/TelemetryCache.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/repository/TelemetryCache.java
new file mode 100644 (file)
index 0000000..7afeafb
--- /dev/null
@@ -0,0 +1,59 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.scanner.repository;
+
+import java.util.HashMap;
+import java.util.Map;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static org.sonar.api.utils.Preconditions.checkArgument;
+
+public class TelemetryCache {
+
+  private static final Logger LOG = LoggerFactory.getLogger(TelemetryCache.class);
+
+  private static final int MAX_ENTRIES = 1000;
+
+  private final Map<String, String> telemetryEntries = new HashMap<>();
+
+  /**
+   * Value is overridden if the key was already stored.
+   * Only the first {@link #MAX_ENTRIES} entries are stored.
+   * @throws IllegalArgumentException if key is null
+   * @throws IllegalArgumentException if value is null
+   * @since 10.8
+   */
+  public TelemetryCache put(String key, String value) {
+    checkArgument(key != null, "Key of the telemetry entry must not be null");
+    checkArgument(value != null, "Value of the telemetry entry must not be null");
+
+    if (telemetryEntries.size() < MAX_ENTRIES || telemetryEntries.containsKey(key)) {
+      telemetryEntries.put(key, value);
+    } else {
+      LOG.warn("Telemetry cache is full, dropping telemetry metric '{}'", key);
+    }
+    return this;
+  }
+
+  public Map<String, String> getAll() {
+    return telemetryEntries;
+  }
+}
index d6011d56c743b58dd904f58df6b870b5e94a46e7..e0930fc537258e7025c33a36757b0305d38ce919 100644 (file)
@@ -76,6 +76,7 @@ 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.repository.TelemetryCache;
 import org.sonar.scanner.scan.branch.BranchConfiguration;
 
 import static java.lang.Math.max;
@@ -115,20 +116,21 @@ public class DefaultSensorStorage implements SensorStorage {
   private final ReportPublisher reportPublisher;
   private final SonarCpdBlockIndex index;
   private final ContextPropertiesCache contextPropertiesCache;
+  private final TelemetryCache telemetryCache;
   private final Configuration settings;
   private final ScannerMetrics scannerMetrics;
   private final BranchConfiguration branchConfiguration;
   private final Set<String> alreadyLogged = new HashSet<>();
 
-  public DefaultSensorStorage(MetricFinder metricFinder, IssuePublisher moduleIssues, Configuration settings,
-    ReportPublisher reportPublisher, SonarCpdBlockIndex index,
-    ContextPropertiesCache contextPropertiesCache, ScannerMetrics scannerMetrics, BranchConfiguration branchConfiguration) {
+  public DefaultSensorStorage(MetricFinder metricFinder, IssuePublisher moduleIssues, Configuration settings, ReportPublisher reportPublisher, SonarCpdBlockIndex index,
+    ContextPropertiesCache contextPropertiesCache, TelemetryCache telemetryCache, ScannerMetrics scannerMetrics, BranchConfiguration branchConfiguration) {
     this.metricFinder = metricFinder;
     this.moduleIssues = moduleIssues;
     this.settings = settings;
     this.reportPublisher = reportPublisher;
     this.index = index;
     this.contextPropertiesCache = contextPropertiesCache;
+    this.telemetryCache = telemetryCache;
     this.scannerMetrics = scannerMetrics;
     this.branchConfiguration = branchConfiguration;
   }
@@ -151,7 +153,8 @@ public class DefaultSensorStorage implements SensorStorage {
     }
 
     if (component instanceof InputDir || (component instanceof DefaultInputModule defaultInputModule && defaultInputModule.definition().getParent() != null)) {
-      logOnce(measure.metric().key(), "Storing measures on folders or modules is deprecated. Provided value of metric '{}' is ignored.", measure.metric().key());
+      logOnce(measure.metric().key(), "Storing measures on folders or modules is deprecated. Provided value of metric '{}' is ignored.",
+        measure.metric().key());
       return;
     }
 
@@ -166,7 +169,8 @@ public class DefaultSensorStorage implements SensorStorage {
     }
 
     if (!measure.isFromCore() && NEWLY_CORE_METRICS_KEYS.contains(measure.metric().key())) {
-      logOnce(measure.metric().key(), "Metric '{}' is an internal metric computed by SonarQube. Provided value is ignored.", measure.metric().key());
+      logOnce(measure.metric().key(), "Metric '{}' is an internal metric computed by SonarQube. Provided value is ignored.",
+        measure.metric().key());
       return;
     }
 
@@ -361,8 +365,10 @@ public class DefaultSensorStorage implements SensorStorage {
     SortedMap<Integer, ScannerReport.LineCoverage.Builder> coveragePerLine = reloadExistingCoverage(inputFile);
 
     int lineCount = inputFile.lines();
-    mergeLineCoverageValues(lineCount, defaultCoverage.hitsByLine(), coveragePerLine, (value, builder) -> builder.setHits(builder.getHits() || value > 0));
-    mergeLineCoverageValues(lineCount, defaultCoverage.conditionsByLine(), coveragePerLine, (value, builder) -> builder.setConditions(max(value, builder.getConditions())));
+    mergeLineCoverageValues(lineCount, defaultCoverage.hitsByLine(), coveragePerLine,
+      (value, builder) -> builder.setHits(builder.getHits() || value > 0));
+    mergeLineCoverageValues(lineCount, defaultCoverage.conditionsByLine(), coveragePerLine,
+      (value, builder) -> builder.setConditions(max(value, builder.getConditions())));
     mergeLineCoverageValues(lineCount, defaultCoverage.coveredConditionsByLine(), coveragePerLine,
       (value, builder) -> builder.setCoveredConditions(max(value, builder.getCoveredConditions())));
 
@@ -373,7 +379,8 @@ public class DefaultSensorStorage implements SensorStorage {
 
   private SortedMap<Integer, ScannerReport.LineCoverage.Builder> reloadExistingCoverage(DefaultInputFile inputFile) {
     SortedMap<Integer, ScannerReport.LineCoverage.Builder> coveragePerLine = new TreeMap<>();
-    try (CloseableIterator<ScannerReport.LineCoverage> lineCoverageCloseableIterator = reportPublisher.getReader().readComponentCoverage(inputFile.scannerId())) {
+    try (CloseableIterator<ScannerReport.LineCoverage> lineCoverageCloseableIterator =
+           reportPublisher.getReader().readComponentCoverage(inputFile.scannerId())) {
       while (lineCoverageCloseableIterator.hasNext()) {
         final ScannerReport.LineCoverage lineCoverage = lineCoverageCloseableIterator.next();
         coveragePerLine.put(lineCoverage.getLine(), ScannerReport.LineCoverage.newBuilder(lineCoverage));
@@ -386,8 +393,8 @@ public class DefaultSensorStorage implements SensorStorage {
     void apply(Integer value, ScannerReport.LineCoverage.Builder builder);
   }
 
-  private static void mergeLineCoverageValues(int lineCount, SortedMap<Integer, Integer> valueByLine, SortedMap<Integer, ScannerReport.LineCoverage.Builder> coveragePerLine,
-    LineCoverageOperation op) {
+  private static void mergeLineCoverageValues(int lineCount, SortedMap<Integer, Integer> valueByLine, SortedMap<Integer,
+    ScannerReport.LineCoverage.Builder> coveragePerLine, LineCoverageOperation op) {
     for (Map.Entry<Integer, Integer> lineMeasure : valueByLine.entrySet()) {
       int lineIdx = lineMeasure.getKey();
       if (lineIdx <= lineCount) {
@@ -437,6 +444,10 @@ public class DefaultSensorStorage implements SensorStorage {
     contextPropertiesCache.put(key, value);
   }
 
+  public void storeTelemetry(String key, String value) {
+    telemetryCache.put(key, value);
+  }
+
   @Override
   public void store(NewSignificantCode newSignificantCode) {
     DefaultSignificantCode significantCode = (DefaultSignificantCode) newSignificantCode;
index 3161da1e96db5ef67edc238bd7fcdc476caa1c08..6f27d1be09f8508a0f84ff3b7f24de9981c38617 100644 (file)
@@ -29,6 +29,7 @@ import org.sonar.api.batch.sensor.cache.ReadCache;
 import org.sonar.api.batch.sensor.cache.WriteCache;
 import org.sonar.api.config.Configuration;
 import org.sonar.api.config.Settings;
+import org.sonar.scanner.bootstrap.ScannerPluginRepository;
 import org.sonar.scanner.cache.AnalysisCacheEnabled;
 import org.sonar.scanner.scan.branch.BranchConfiguration;
 
@@ -39,9 +40,10 @@ public class ModuleSensorContext extends ProjectSensorContext {
 
   public ModuleSensorContext(DefaultInputProject project, InputModule module, Configuration config, Settings mutableModuleSettings, FileSystem fs, ActiveRules activeRules,
     DefaultSensorStorage sensorStorage, SonarRuntime sonarRuntime, BranchConfiguration branchConfiguration,
-    WriteCache writeCache, ReadCache readCache, AnalysisCacheEnabled analysisCacheEnabled, UnchangedFilesHandler unchangedFilesHandler) {
+    WriteCache writeCache, ReadCache readCache, AnalysisCacheEnabled analysisCacheEnabled, UnchangedFilesHandler unchangedFilesHandler,
+    ExecutingSensorContext executingSensorContext, ScannerPluginRepository pluginRepository) {
     super(project, config, mutableModuleSettings, fs, activeRules, sensorStorage, sonarRuntime, branchConfiguration, writeCache, readCache, analysisCacheEnabled,
-      unchangedFilesHandler);
+      unchangedFilesHandler, executingSensorContext, pluginRepository);
     this.module = module;
   }
 
index ec0b9e59f2674d0f61c789272b61331700c0c16d..7694b77e8b7c46740963477e10dfe725cd651cbe 100644 (file)
@@ -54,6 +54,8 @@ import org.sonar.api.config.Configuration;
 import org.sonar.api.config.Settings;
 import org.sonar.api.scanner.fs.InputProject;
 import org.sonar.api.utils.Version;
+import org.sonar.core.platform.PluginInfo;
+import org.sonar.scanner.bootstrap.ScannerPluginRepository;
 import org.sonar.scanner.cache.AnalysisCacheEnabled;
 import org.sonar.scanner.scan.branch.BranchConfiguration;
 import org.sonar.scanner.sensor.noop.NoOpNewAnalysisError;
@@ -75,10 +77,15 @@ public class ProjectSensorContext implements SensorContext {
   private final WriteCache writeCache;
   private final ReadCache readCache;
   private final AnalysisCacheEnabled analysisCacheEnabled;
-
-  public ProjectSensorContext(DefaultInputProject project, Configuration config, Settings mutableSettings, FileSystem fs, ActiveRules activeRules,
-    DefaultSensorStorage sensorStorage, SonarRuntime sonarRuntime, BranchConfiguration branchConfiguration, WriteCache writeCache, ReadCache readCache,
-    AnalysisCacheEnabled analysisCacheEnabled, UnchangedFilesHandler unchangedFilesHandler) {
+  private final ExecutingSensorContext executingSensorContext;
+  private final ScannerPluginRepository pluginRepo;
+
+  public ProjectSensorContext(DefaultInputProject project, Configuration config, Settings mutableSettings, FileSystem fs,
+                              ActiveRules activeRules,
+                              DefaultSensorStorage sensorStorage, SonarRuntime sonarRuntime, BranchConfiguration branchConfiguration,
+                              WriteCache writeCache, ReadCache readCache,
+                              AnalysisCacheEnabled analysisCacheEnabled, UnchangedFilesHandler unchangedFilesHandler,
+                              ExecutingSensorContext executingSensorContext, ScannerPluginRepository pluginRepo) {
     this.project = project;
     this.config = config;
     this.mutableSettings = mutableSettings;
@@ -91,6 +98,8 @@ public class ProjectSensorContext implements SensorContext {
     this.analysisCacheEnabled = analysisCacheEnabled;
     this.skipUnchangedFiles = branchConfiguration.isPullRequest();
     this.unchangedFilesHandler = unchangedFilesHandler;
+    this.executingSensorContext = executingSensorContext;
+    this.pluginRepo = pluginRepo;
   }
 
   @Override
@@ -215,8 +224,12 @@ public class ProjectSensorContext implements SensorContext {
   }
 
   @Override
-  public void addTelemetryProperty(String s, String s1) {
-    //NOOP
+  public void addTelemetryProperty(String key, String value) {
+    if (isSonarSourcePlugin()) {
+      this.sensorStorage.storeTelemetry(key, value);
+    } else {
+      throw new IllegalStateException("Telemetry properties can only be added by SonarSource plugins");
+    }
   }
 
   @Override
@@ -228,4 +241,13 @@ public class ProjectSensorContext implements SensorContext {
   public boolean canSkipUnchangedFiles() {
     return this.skipUnchangedFiles;
   }
+
+  private boolean isSonarSourcePlugin() {
+    SensorId sensorExecuting = executingSensorContext.getSensorExecuting();
+    if (sensorExecuting != null) {
+      PluginInfo pluginInfo = pluginRepo.getPluginInfo(sensorExecuting.getPluginKey());
+      return "sonarsource".equalsIgnoreCase(pluginInfo.getOrganizationName());
+    }
+    return false;
+  }
 }
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scanner/report/TelemetryPublisherTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scanner/report/TelemetryPublisherTest.java
new file mode 100644 (file)
index 0000000..f71e9a8
--- /dev/null
@@ -0,0 +1,66 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.scanner.report;
+
+import com.google.common.collect.Lists;
+import java.util.Arrays;
+import java.util.List;
+import org.junit.jupiter.api.Test;
+import org.sonar.scanner.protocol.output.ScannerReport;
+import org.sonar.scanner.protocol.output.ScannerReportWriter;
+import org.sonar.scanner.repository.TelemetryCache;
+
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+class TelemetryPublisherTest {
+  private final ScannerReportWriter writer = mock(ScannerReportWriter.class);
+  private final TelemetryCache telemetryCache = new TelemetryCache();
+  private final TelemetryPublisher underTest = new TelemetryPublisher(telemetryCache);
+
+  @Test
+  void publish_writes_telemetry_to_report() {
+    telemetryCache.put("key1", "value1");
+    telemetryCache.put("key2", "value2");
+
+    underTest.publish(writer);
+
+    List<ScannerReport.TelemetryEntry> expected = Arrays.asList(
+      newTelemetryEntry("key1", "value1"),
+      newTelemetryEntry("key2", "value2"));
+    expectWritten(expected);
+  }
+
+  private void expectWritten(List<ScannerReport.TelemetryEntry> expected) {
+    verify(writer).writeTelemetry(argThat(entries -> {
+      List<ScannerReport.TelemetryEntry> copy = Lists.newArrayList(entries);
+      copy.removeAll(expected);
+      return copy.isEmpty();
+    }));
+  }
+
+  private static ScannerReport.TelemetryEntry newTelemetryEntry(String key, String value) {
+    return ScannerReport.TelemetryEntry.newBuilder()
+      .setKey(key)
+      .setValue(value)
+      .build();
+  }
+}
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scanner/repository/TelemetryCacheTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scanner/repository/TelemetryCacheTest.java
new file mode 100644 (file)
index 0000000..cbdb518
--- /dev/null
@@ -0,0 +1,83 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.scanner.repository;
+
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.assertj.core.data.MapEntry.entry;
+
+class TelemetryCacheTest {
+
+    TelemetryCache underTest = new TelemetryCache();
+
+    @Test
+    void put_EntryIsAddedToCache() {
+        assertThat(underTest.getAll()).isEmpty();
+
+        underTest.put("key", "value");
+        assertThat(underTest.getAll()).containsOnly(entry("key", "value"));
+    }
+
+    @Test
+    void put_whenKeyIsAlreadyThere_EntryOverridesPreviousValue() {
+        underTest.put("key", "value");
+        underTest.put("key", "newValue");
+        assertThat(underTest.getAll()).containsOnly(entry("key", "newValue"));
+    }
+
+    @Test
+    void put_whenCacheIsAlreadyFull_newEntryIsNotAdded() {
+        for (int i = 0; i < 1000; i++) {
+            underTest.put("key" + i, "value" + i);
+        }
+        underTest.put("key", "value");
+        assertThat(underTest.getAll()).hasSize(1000);
+        assertThat(underTest.getAll()).doesNotContain(entry("key", "value"));
+    }
+
+    @Test
+    void put_whenCacheIsAlreadyFull_newEntryIsAddedIfKeyAlreadyThere() {
+        for (int i = 0; i < 1000; i++) {
+            underTest.put("key" + i, "value" + i);
+        }
+        underTest.put("key1", "newValue");
+        underTest.put("key", "newValue");
+
+        assertThat(underTest.getAll()).hasSize(1000);
+        assertThat(underTest.getAll()).contains(entry("key1", "newValue"));
+    }
+
+    @Test
+    void put_whenKeyIsNull_IAEIsThrown() {
+        assertThatThrownBy(() -> underTest.put(null, "value"))
+          .isInstanceOf(IllegalArgumentException.class)
+          .hasMessage("Key of the telemetry entry must not be null");
+    }
+
+    @Test
+    void put_whenValueIsNull_IAEIsThrown() {
+        assertThatThrownBy(() -> underTest.put("key", null))
+          .isInstanceOf(IllegalArgumentException.class)
+          .hasMessage("Value of the telemetry entry must not be null");
+    }
+
+}
index 1eeea7ede1203399425e79b00d63baa84ee7b33c..41680ce3a1d691ed2ac2d1dfd3bb50f9cdb1fdcf 100644 (file)
 package org.sonar.scanner.sensor;
 
 import java.io.File;
-import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
 import org.assertj.core.groups.Tuple;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.TemporaryFolder;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
 import org.mockito.ArgumentCaptor;
 import org.sonar.api.batch.bootstrap.ProjectDefinition;
 import org.sonar.api.batch.fs.InputFile;
@@ -66,34 +64,37 @@ import org.sonar.scanner.protocol.output.ScannerReportReader;
 import org.sonar.scanner.protocol.output.ScannerReportWriter;
 import org.sonar.scanner.report.ReportPublisher;
 import org.sonar.scanner.repository.ContextPropertiesCache;
+import org.sonar.scanner.repository.TelemetryCache;
 import org.sonar.scanner.scan.branch.BranchConfiguration;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.assertj.core.data.MapEntry.entry;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoInteractions;
 import static org.mockito.Mockito.verifyNoMoreInteractions;
 import static org.mockito.Mockito.when;
 
-public class DefaultSensorStorageTest {
+class DefaultSensorStorageTest {
 
-  @Rule
-  public TemporaryFolder temp = new TemporaryFolder();
+  @TempDir
+  public File temp;
 
   private DefaultSensorStorage underTest;
   private MapSettings settings;
   private IssuePublisher moduleIssues;
   private ScannerReportWriter reportWriter;
   private ContextPropertiesCache contextPropertiesCache = new ContextPropertiesCache();
+  private TelemetryCache telemetryCache = new TelemetryCache();
   private BranchConfiguration branchConfiguration;
   private DefaultInputProject project;
   private ScannerReportReader reportReader;
   private ReportPublisher reportPublisher;
 
-  @Before
-  public void prepare() throws Exception {
+  @BeforeEach
+  public void prepare() {
     MetricFinder metricFinder = mock(MetricFinder.class);
     when(metricFinder.<Integer>findByKey(CoreMetrics.NCLOC_KEY)).thenReturn(CoreMetrics.NCLOC);
     when(metricFinder.<String>findByKey(CoreMetrics.FUNCTION_COMPLEXITY_DISTRIBUTION_KEY)).thenReturn(CoreMetrics.FUNCTION_COMPLEXITY_DISTRIBUTION);
@@ -103,8 +104,7 @@ public class DefaultSensorStorageTest {
     moduleIssues = mock(IssuePublisher.class);
 
     reportPublisher = mock(ReportPublisher.class);
-    final File reportDir = temp.newFolder();
-    FileStructure fileStructure = new FileStructure(reportDir);
+    FileStructure fileStructure = new FileStructure(temp);
     reportWriter = new ScannerReportWriter(fileStructure);
     reportReader = new ScannerReportReader(fileStructure);
     when(reportPublisher.getWriter()).thenReturn(reportWriter);
@@ -113,16 +113,16 @@ public class DefaultSensorStorageTest {
     branchConfiguration = mock(BranchConfiguration.class);
 
     underTest = new DefaultSensorStorage(metricFinder,
-      moduleIssues, settings.asConfig(), reportPublisher, mock(SonarCpdBlockIndex.class), contextPropertiesCache, new ScannerMetrics(), branchConfiguration);
+      moduleIssues, settings.asConfig(), reportPublisher, mock(SonarCpdBlockIndex.class), contextPropertiesCache, telemetryCache, new ScannerMetrics(), branchConfiguration);
 
     project = new DefaultInputProject(ProjectDefinition.create()
       .setKey("foo")
-      .setBaseDir(temp.newFolder())
-      .setWorkDir(temp.newFolder()));
+      .setBaseDir(temp)
+      .setWorkDir(temp));
   }
 
   @Test
-  public void should_merge_coverage() {
+  void should_merge_coverage() {
     DefaultInputFile file = new TestInputFileBuilder("foo", "src/Foo.php").setLines(5).build();
 
     DefaultCoverage coverage = new DefaultCoverage(underTest);
@@ -144,7 +144,7 @@ public class DefaultSensorStorageTest {
   }
 
   @Test
-  public void shouldFailIfUnknownMetric() {
+  void shouldFailIfUnknownMetric() {
     InputFile file = new TestInputFileBuilder("foo", "src/Foo.php").build();
 
     assertThatThrownBy(() -> underTest.store(new DefaultMeasure()
@@ -156,7 +156,7 @@ public class DefaultSensorStorageTest {
   }
 
   @Test
-  public void shouldIgnoreMeasuresOnFolders() {
+  void shouldIgnoreMeasuresOnFolders() {
     underTest.store(new DefaultMeasure()
       .on(new DefaultInputDir("foo", "bar"))
       .forMetric(CoreMetrics.LINES)
@@ -166,9 +166,9 @@ public class DefaultSensorStorageTest {
   }
 
   @Test
-  public void shouldIgnoreMeasuresOnModules() throws IOException {
-    ProjectDefinition module = ProjectDefinition.create().setBaseDir(temp.newFolder()).setWorkDir(temp.newFolder());
-    ProjectDefinition root = ProjectDefinition.create().addSubProject(module);
+  void shouldIgnoreMeasuresOnModules() {
+    ProjectDefinition module = ProjectDefinition.create().setBaseDir(temp).setWorkDir(temp);
+    ProjectDefinition.create().addSubProject(module);
 
     underTest.store(new DefaultMeasure()
       .on(new DefaultInputModule(module))
@@ -179,7 +179,7 @@ public class DefaultSensorStorageTest {
   }
 
   @Test
-  public void should_save_issue() {
+  void should_save_issue() {
     InputFile file = new TestInputFileBuilder("foo", "src/Foo.php").build();
 
     DefaultIssue issue = new DefaultIssue(project).at(new DefaultIssueLocation().on(file));
@@ -191,7 +191,7 @@ public class DefaultSensorStorageTest {
   }
 
   @Test
-  public void should_save_external_issue() {
+  void should_save_external_issue() {
     InputFile file = new TestInputFileBuilder("foo", "src/Foo.php").build();
 
     DefaultExternalIssue externalIssue = new DefaultExternalIssue(project).at(new DefaultIssueLocation().on(file));
@@ -203,7 +203,7 @@ public class DefaultSensorStorageTest {
   }
 
   @Test
-  public void should_skip_issue_on_pr_when_file_status_is_SAME() {
+  void should_skip_issue_on_pr_when_file_status_is_SAME() {
     InputFile file = new TestInputFileBuilder("foo", "src/Foo.php").setStatus(InputFile.Status.SAME).build();
     when(branchConfiguration.isPullRequest()).thenReturn(true);
 
@@ -214,7 +214,7 @@ public class DefaultSensorStorageTest {
   }
 
   @Test
-  public void has_issues_delegates_to_report_publisher() {
+  void has_issues_delegates_to_report_publisher() {
     DefaultInputFile file1 = new TestInputFileBuilder("foo", "src/Foo1.php").setStatus(InputFile.Status.SAME).build();
     DefaultInputFile file2 = new TestInputFileBuilder("foo", "src/Foo2.php").setStatus(InputFile.Status.SAME).build();
 
@@ -224,7 +224,7 @@ public class DefaultSensorStorageTest {
   }
 
   @Test
-  public void should_save_highlighting() {
+  void should_save_highlighting() {
     DefaultInputFile file = new TestInputFileBuilder("foo", "src/Foo.php")
       .setContents("// comment").build();
 
@@ -235,7 +235,7 @@ public class DefaultSensorStorageTest {
   }
 
   @Test
-  public void should_skip_highlighting_on_pr_when_file_status_is_SAME() {
+  void should_skip_highlighting_on_pr_when_file_status_is_SAME() {
     DefaultInputFile file = new TestInputFileBuilder("foo", "src/Foo.php")
       .setContents("// comment")
       .setStatus(InputFile.Status.SAME).build();
@@ -248,7 +248,7 @@ public class DefaultSensorStorageTest {
   }
 
   @Test
-  public void should_save_file_measure() {
+  void should_save_file_measure() {
     DefaultInputFile file = new TestInputFileBuilder("foo", "src/Foo.php")
       .build();
 
@@ -263,7 +263,7 @@ public class DefaultSensorStorageTest {
   }
 
   @Test
-  public void should_not_skip_file_measures_on_pull_request_when_file_status_is_SAME() {
+  void should_not_skip_file_measures_on_pull_request_when_file_status_is_SAME() {
     DefaultInputFile file = new TestInputFileBuilder("foo", "src/Foo.php").setStatus(InputFile.Status.SAME).build();
     when(branchConfiguration.isPullRequest()).thenReturn(true);
 
@@ -278,7 +278,7 @@ public class DefaultSensorStorageTest {
   }
 
   @Test
-  public void should_skip_significant_code_on_pull_request_when_file_status_is_SAME() {
+  void should_skip_significant_code_on_pull_request_when_file_status_is_SAME() {
     DefaultInputFile file = new TestInputFileBuilder("foo", "src/Foo.php")
       .setStatus(InputFile.Status.SAME)
       .setContents("foo")
@@ -293,7 +293,7 @@ public class DefaultSensorStorageTest {
   }
 
   @Test
-  public void should_save_significant_code() {
+  void should_save_significant_code() {
     DefaultInputFile file = new TestInputFileBuilder("foo", "src/Foo.php")
       .setContents("foo")
       .build();
@@ -305,9 +305,9 @@ public class DefaultSensorStorageTest {
   }
 
   @Test
-  public void should_save_project_measure() throws IOException {
+  void should_save_project_measure() {
     String projectKey = "myProject";
-    DefaultInputModule module = new DefaultInputModule(ProjectDefinition.create().setKey(projectKey).setBaseDir(temp.newFolder()).setWorkDir(temp.newFolder()));
+    DefaultInputModule module = new DefaultInputModule(ProjectDefinition.create().setKey(projectKey).setBaseDir(temp).setWorkDir(temp));
 
     underTest.store(new DefaultMeasure()
       .on(module)
@@ -319,45 +319,56 @@ public class DefaultSensorStorageTest {
     assertThat(m.getMetricKey()).isEqualTo(CoreMetrics.NCLOC_KEY);
   }
 
-  @Test(expected = UnsupportedOperationException.class)
-  public void duplicateHighlighting() throws Exception {
+  @Test
+  void duplicateHighlighting() {
     InputFile inputFile = new TestInputFileBuilder("foo", "src/Foo.java")
-      .setModuleBaseDir(temp.newFolder().toPath()).build();
+      .setModuleBaseDir(temp.toPath()).build();
     DefaultHighlighting h = new DefaultHighlighting(null)
       .onFile(inputFile);
     underTest.store(h);
-    underTest.store(h);
+    assertThrows(UnsupportedOperationException.class, () -> {
+      underTest.store(h);
+    });
   }
 
-  @Test(expected = UnsupportedOperationException.class)
-  public void duplicateSignificantCode() throws Exception {
+  @Test
+  void duplicateSignificantCode() {
     InputFile inputFile = new TestInputFileBuilder("foo", "src/Foo.java")
-      .setModuleBaseDir(temp.newFolder().toPath()).build();
+      .setModuleBaseDir(temp.toPath()).build();
     DefaultSignificantCode h = new DefaultSignificantCode(null)
       .onFile(inputFile);
     underTest.store(h);
-    underTest.store(h);
+    assertThrows(UnsupportedOperationException.class, () -> {
+      underTest.store(h);
+    });
   }
 
-  @Test(expected = UnsupportedOperationException.class)
-  public void duplicateSymbolTable() throws Exception {
+  @Test
+  void duplicateSymbolTable() {
     InputFile inputFile = new TestInputFileBuilder("foo", "src/Foo.java")
-      .setModuleBaseDir(temp.newFolder().toPath()).build();
+      .setModuleBaseDir(temp.toPath()).build();
     DefaultSymbolTable st = new DefaultSymbolTable(null)
       .onFile(inputFile);
     underTest.store(st);
-    underTest.store(st);
+    assertThrows(UnsupportedOperationException.class, () -> {
+      underTest.store(st);
+    });
   }
 
   @Test
-  public void shouldStoreContextProperty() {
+  void shouldStoreContextProperty() {
     underTest.storeProperty("foo", "bar");
     assertThat(contextPropertiesCache.getAll()).containsOnly(entry("foo", "bar"));
   }
 
   @Test
-  public void store_whenAdhocRuleIsSpecified_shouldWriteAdhocRuleToReport() {
+  void shouldStoreTelemetryEntries() {
+    underTest.storeTelemetry("key", "value");
+    assertThat(telemetryCache.getAll()).containsOnly(entry("key", "value"));
+  }
 
+  @Test
+  void store_whenAdhocRuleIsSpecified_shouldWriteAdhocRuleToReport() {
     underTest.store(new DefaultAdHocRule().ruleId("ruleId").engineId("engineId")
       .name("name")
       .addDefaultImpact(SoftwareQuality.MAINTAINABILITY, Severity.HIGH)
@@ -383,7 +394,7 @@ public class DefaultSensorStorageTest {
   }
 
   @Test
-  public void store_whenAdhocRuleIsSpecifiedWithOptionalFieldEmpty_shouldWriteAdhocRuleWithDefaultImpactsToReport() {
+  void store_whenAdhocRuleIsSpecifiedWithOptionalFieldEmpty_shouldWriteAdhocRuleWithDefaultImpactsToReport() {
     underTest.store(new DefaultAdHocRule().ruleId("ruleId").engineId("engineId")
       .name("name")
       .description("description"));
index 94d7a2ce282afd89adb46283c513d7a0720c4f17..4ca7d33d6428804e31d4f5d98cb5043d66421d76 100644 (file)
@@ -35,6 +35,7 @@ import org.sonar.api.batch.rule.internal.ActiveRulesBuilder;
 import org.sonar.api.config.internal.MapSettings;
 import org.sonar.api.internal.SonarRuntimeImpl;
 import org.sonar.api.utils.Version;
+import org.sonar.scanner.bootstrap.ScannerPluginRepository;
 import org.sonar.scanner.cache.AnalysisCacheEnabled;
 import org.sonar.scanner.cache.ReadCacheImpl;
 import org.sonar.scanner.cache.WriteCacheImpl;
@@ -61,12 +62,14 @@ public class ModuleSensorContextTest {
   private final SonarRuntime runtime = SonarRuntimeImpl.forSonarQube(Version.parse("5.5"), SonarQubeSide.SCANNER, SonarEdition.COMMUNITY);
   private DefaultFileSystem fs;
   private ModuleSensorContext underTest;
+  private ExecutingSensorContext executingSensorContext = mock(ExecutingSensorContext.class);
+  private ScannerPluginRepository pluginRepository = mock(ScannerPluginRepository.class);
 
   @Before
   public void prepare() throws Exception {
     fs = new DefaultFileSystem(temp.newFolder().toPath());
     underTest = new ModuleSensorContext(mock(DefaultInputProject.class), mock(InputModule.class), settings.asConfig(), settings, fs, activeRules, sensorStorage, runtime,
-      branchConfiguration, writeCache, readCache, analysisCacheEnabled, unchangedFilesHandler);
+      branchConfiguration, writeCache, readCache, analysisCacheEnabled, unchangedFilesHandler, executingSensorContext, pluginRepository);
   }
 
   @Test
@@ -102,7 +105,7 @@ public class ModuleSensorContextTest {
   public void pull_request_can_skip_unchanged_files() {
     when(branchConfiguration.isPullRequest()).thenReturn(true);
     underTest = new ModuleSensorContext(mock(DefaultInputProject.class), mock(InputModule.class), settings.asConfig(), settings, fs, activeRules, sensorStorage, runtime,
-      branchConfiguration, writeCache, readCache, analysisCacheEnabled, unchangedFilesHandler);
+      branchConfiguration, writeCache, readCache, analysisCacheEnabled, unchangedFilesHandler, executingSensorContext, pluginRepository);
     assertThat(underTest.canSkipUnchangedFiles()).isTrue();
   }
 
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scanner/sensor/ProjectSensorContextTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scanner/sensor/ProjectSensorContextTest.java
new file mode 100644 (file)
index 0000000..5d25250
--- /dev/null
@@ -0,0 +1,92 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.scanner.sensor;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.sonar.api.SonarEdition;
+import org.sonar.api.SonarQubeSide;
+import org.sonar.api.SonarRuntime;
+import org.sonar.api.batch.fs.internal.DefaultFileSystem;
+import org.sonar.api.batch.fs.internal.DefaultInputProject;
+import org.sonar.api.batch.rule.ActiveRules;
+import org.sonar.api.batch.rule.internal.ActiveRulesBuilder;
+import org.sonar.api.config.internal.MapSettings;
+import org.sonar.api.internal.SonarRuntimeImpl;
+import org.sonar.api.utils.Version;
+import org.sonar.core.platform.PluginInfo;
+import org.sonar.scanner.bootstrap.ScannerPluginRepository;
+import org.sonar.scanner.cache.AnalysisCacheEnabled;
+import org.sonar.scanner.cache.ReadCacheImpl;
+import org.sonar.scanner.cache.WriteCacheImpl;
+import org.sonar.scanner.scan.branch.BranchConfiguration;
+
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.when;
+
+class ProjectSensorContextTest {
+
+  private final ActiveRules activeRules = new ActiveRulesBuilder().build();
+  private final MapSettings settings = new MapSettings();
+  private final DefaultSensorStorage sensorStorage = mock(DefaultSensorStorage.class);
+  private final BranchConfiguration branchConfiguration = mock(BranchConfiguration.class);
+  private final WriteCacheImpl writeCache = mock(WriteCacheImpl.class);
+  private final ReadCacheImpl readCache = mock(ReadCacheImpl.class);
+  private final AnalysisCacheEnabled analysisCacheEnabled = mock(AnalysisCacheEnabled.class);
+  private final UnchangedFilesHandler unchangedFilesHandler = mock(UnchangedFilesHandler.class);
+  private final SonarRuntime runtime = SonarRuntimeImpl.forSonarQube(Version.parse("5.5"), SonarQubeSide.SCANNER, SonarEdition.COMMUNITY);
+  private DefaultFileSystem fs = mock(DefaultFileSystem.class);
+  private ExecutingSensorContext executingSensorContext = mock(ExecutingSensorContext.class);
+  private ScannerPluginRepository pluginRepository = mock(ScannerPluginRepository.class);
+
+  private ProjectSensorContext underTest = new ProjectSensorContext(mock(DefaultInputProject.class), settings.asConfig(), settings, fs, activeRules, sensorStorage, runtime,
+  branchConfiguration, writeCache, readCache, analysisCacheEnabled, unchangedFilesHandler, executingSensorContext, pluginRepository);
+
+  private static final String PLUGIN_KEY = "org.sonarsource.pluginKey";
+
+  @BeforeEach
+  void prepare() {
+    when(executingSensorContext.getSensorExecuting()).thenReturn(new SensorId(PLUGIN_KEY, "sensorName"));
+  }
+
+
+  @Test
+  void addTelemetryProperty_whenTheOrganizationIsSonarSource_mustStoreTheTelemetry() {
+
+    when(pluginRepository.getPluginInfo(PLUGIN_KEY)).thenReturn(new PluginInfo(PLUGIN_KEY).setOrganizationName("sonarsource"));
+
+    underTest.addTelemetryProperty("key", "value");
+
+    //then verify that the defaultStorage is called with the telemetry property once
+    verify(sensorStorage).storeTelemetry("key", "value");
+  }
+
+  @Test
+  void addTelemetryProperty_whenTheOrganizationIsNotSonarSource_mustThrowExcaption() {
+    when(pluginRepository.getPluginInfo(PLUGIN_KEY)).thenReturn(new PluginInfo(PLUGIN_KEY).setOrganizationName("notSonarsource"));
+
+    assertThrows(IllegalStateException.class, () -> underTest.addTelemetryProperty("key", "value"));
+
+    verifyNoInteractions(sensorStorage);
+  }
+}
index a3fb8835fbab19f8db883e7c9272b13734846b94..686b7d200666baa80eee641f09c409a3c3703fca 100644 (file)
@@ -94,6 +94,10 @@ public class FileStructure {
     return new File(dir, "context-props.pb");
   }
 
+  public File telemetryEntries() {
+    return new File(dir, "telemetry-entries.pb");
+  }
+
   public File analysisWarnings() {
     return new File(dir, "analysis-warnings.pb");
   }
index 20e5f30a1f0a5f5f54679f69c3c80880e1afe0c4..ad4720cbb5ff2d9d9db5c5ba0e6e28212624f287 100644 (file)
@@ -163,6 +163,12 @@ public class ScannerReportWriter {
     return file;
   }
 
+  public File writeTelemetry(Iterable<ScannerReport.TelemetryEntry> telemetryEntries) {
+    File file = fileStructure.telemetryEntries();
+    Protobuf.writeStream(telemetryEntries, file, false);
+    return file;
+  }
+
   public File getSourceFile(int componentRef) {
     return fileStructure.fileFor(FileStructure.Domain.SOURCE, componentRef);
   }
index e495438385972735f7cdfaf2d2b720622886781b..b7c1d3d50641bf1ca3342c1dbfcd308a5c4f2974 100644 (file)
@@ -87,6 +87,11 @@ message ContextProperty {
   string value = 2;
 }
 
+message TelemetryEntry {
+  string key = 1;
+  string value = 2;
+}
+
 message ActiveRule {
   string rule_repository = 1;
   string rule_key = 2;
index b2787934275b801f2682be501da9d2000336bc58..9c7ada548b35580ae1fd3366327463a9218acad4 100644 (file)
@@ -20,6 +20,7 @@
 package org.sonar.scanner.protocol.output;
 
 import java.io.File;
+import java.nio.charset.Charset;
 import org.apache.commons.io.FileUtils;
 import org.junit.Rule;
 import org.junit.Test;
@@ -60,10 +61,10 @@ public class FileStructureTest {
   @Test
   public void locate_files() throws Exception {
     File dir = temp.newFolder();
-    FileUtils.write(new File(dir, "metadata.pb"), "metadata content");
-    FileUtils.write(new File(dir, "issues-3.pb"), "external issues of component 3");
-    FileUtils.write(new File(dir, "external-issues-3.pb"), "issues of component 3");
-    FileUtils.write(new File(dir, "component-42.pb"), "details of component 42");
+    FileUtils.write(new File(dir, "metadata.pb"), "metadata content", Charset.defaultCharset());
+    FileUtils.write(new File(dir, "issues-3.pb"), "external issues of component 3", Charset.defaultCharset());
+    FileUtils.write(new File(dir, "external-issues-3.pb"), "issues of component 3", Charset.defaultCharset());
+    FileUtils.write(new File(dir, "component-42.pb"), "details of component 42", Charset.defaultCharset());
 
     FileStructure structure = new FileStructure(dir);
     assertThat(structure.metadataFile()).exists().isFile();
@@ -78,9 +79,19 @@ public class FileStructureTest {
   public void contextProperties_file() throws Exception {
     File dir = temp.newFolder();
     File file = new File(dir, "context-props.pb");
-    FileUtils.write(file, "content");
+    FileUtils.write(file, "content", Charset.defaultCharset());
 
     FileStructure structure = new FileStructure(dir);
     assertThat(structure.contextProperties()).exists().isFile().isEqualTo(file);
   }
+
+  @Test
+  public void telemetryFile_hasTheCorrectName() throws Exception {
+    File dir = temp.newFolder();
+    File file = new File(dir, "telemetry-entries.pb");
+    FileUtils.write(file, "content", Charset.defaultCharset());
+
+    FileStructure structure = new FileStructure(dir);
+    assertThat(structure.telemetryEntries()).exists().isFile().isEqualTo(file);
+  }
 }
index 368886e2265d35b471bc973ffdc81ff80c3006b5..4b89c9a3db57a8b03535dda172cddc4d2eb27784 100644 (file)
@@ -154,7 +154,8 @@ public class ScannerReportWriterTest {
     // write data
     ScannerReport.Cve cve = ScannerReport.Cve.newBuilder()
       .setCveId("CVE-2023-20863")
-      .setDescription("In spring framework versions prior to 5.2.24 release+ ,5.3.27+ and 6.0.8+ , it is possible for a user to provide a specially crafted SpEL expression that may cause a denial-of-service (DoS) condition.")
+      .setDescription("In spring framework versions prior to 5.2.24 release+ ,5.3.27+ and 6.0.8+ , it is possible for a user to provide a" +
+        " specially crafted SpEL expression that may cause a denial-of-service (DoS) condition.")
       .setCvssScore(6.5f)
       .setEpssScore(0.00306f)
       .setEpssPercentile(0.70277f)
@@ -366,4 +367,25 @@ public class ScannerReportWriterTest {
     assertThat(underTest.hasComponentData(FileStructure.Domain.COVERAGES, 1)).isTrue();
   }
 
+  @Test
+  public void write_telemetry() {
+
+    List<ScannerReport.TelemetryEntry> input = List.of(
+      ScannerReport.TelemetryEntry.newBuilder()
+        .setKey("key")
+        .setValue("value").build(),
+      ScannerReport.TelemetryEntry.newBuilder()
+        .setKey("key2")
+        .setValue("value2").build());
+
+    underTest.writeTelemetry(input);
+
+    try (CloseableIterator<ScannerReport.TelemetryEntry> telemetryIterator =
+           Protobuf.readStream(underTest.getFileStructure().telemetryEntries(), ScannerReport.TelemetryEntry.parser())) {
+
+      assertThat(telemetryIterator).toIterable()
+        .containsExactlyElementsOf(input)
+        .hasSize(input.size());
+    }
+  }
 }