From 3f0a34901c55eb1e204fe4028d2773f8484b513f Mon Sep 17 00:00:00 2001 From: Matteo Mara Date: Wed, 9 Oct 2024 15:09:39 +0200 Subject: [PATCH] SONAR-23327 Implement addTelemetryProperty in the scanner engine --- .../internal/InMemorySensorStorage.java | 10 ++ .../sensor/internal/SensorContextTester.java | 5 +- .../bootstrap/SpringScannerContainer.java | 4 + .../scanner/report/TelemetryPublisher.java | 44 +++++++ .../scanner/repository/TelemetryCache.java | 59 ++++++++++ .../scanner/sensor/DefaultSensorStorage.java | 31 +++-- .../scanner/sensor/ModuleSensorContext.java | 6 +- .../scanner/sensor/ProjectSensorContext.java | 34 +++++- .../report/TelemetryPublisherTest.java | 66 +++++++++++ .../repository/TelemetryCacheTest.java | 83 ++++++++++++++ .../sensor/DefaultSensorStorageTest.java | 107 ++++++++++-------- .../sensor/ModuleSensorContextTest.java | 7 +- .../sensor/ProjectSensorContextTest.java | 92 +++++++++++++++ .../protocol/output/FileStructure.java | 4 + .../protocol/output/ScannerReportWriter.java | 6 + .../src/main/protobuf/scanner_report.proto | 5 + .../protocol/output/FileStructureTest.java | 21 +++- .../output/ScannerReportWriterTest.java | 24 +++- 18 files changed, 532 insertions(+), 76 deletions(-) create mode 100644 sonar-scanner-engine/src/main/java/org/sonar/scanner/report/TelemetryPublisher.java create mode 100644 sonar-scanner-engine/src/main/java/org/sonar/scanner/repository/TelemetryCache.java create mode 100644 sonar-scanner-engine/src/test/java/org/sonar/scanner/report/TelemetryPublisherTest.java create mode 100644 sonar-scanner-engine/src/test/java/org/sonar/scanner/repository/TelemetryCacheTest.java create mode 100644 sonar-scanner-engine/src/test/java/org/sonar/scanner/sensor/ProjectSensorContextTest.java diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/sensor/internal/InMemorySensorStorage.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/sensor/internal/InMemorySensorStorage.java index de7add875be..0b4d729aee8 100644 --- a/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/sensor/internal/InMemorySensorStorage.java +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/sensor/internal/InMemorySensorStorage.java @@ -57,6 +57,7 @@ class InMemorySensorStorage implements SensorStorage { Map> coverageByComponent = new HashMap<>(); Map symbolsPerComponent = new HashMap<>(); Map contextProperties = new HashMap<>(); + Map telemetryEntries = new HashMap<>(); Map 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); diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/sensor/internal/SensorContextTester.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/sensor/internal/SensorContextTester.java index ca8aae07c0f..c94a0ccaff0 100644 --- a/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/sensor/internal/SensorContextTester.java +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/sensor/internal/SensorContextTester.java @@ -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) { diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/SpringScannerContainer.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/SpringScannerContainer.java index 16d66d5b460..44afe06d7af 100644 --- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/SpringScannerContainer.java +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/SpringScannerContainer.java @@ -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 index 00000000000..fdc2e96e9d6 --- /dev/null +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/report/TelemetryPublisher.java @@ -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 index 00000000000..7afeafb5858 --- /dev/null +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/repository/TelemetryCache.java @@ -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 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 getAll() { + return telemetryEntries; + } +} diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/sensor/DefaultSensorStorage.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/sensor/DefaultSensorStorage.java index d6011d56c74..e0930fc5372 100644 --- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/sensor/DefaultSensorStorage.java +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/sensor/DefaultSensorStorage.java @@ -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 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 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 reloadExistingCoverage(DefaultInputFile inputFile) { SortedMap coveragePerLine = new TreeMap<>(); - try (CloseableIterator lineCoverageCloseableIterator = reportPublisher.getReader().readComponentCoverage(inputFile.scannerId())) { + try (CloseableIterator 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 valueByLine, SortedMap coveragePerLine, - LineCoverageOperation op) { + private static void mergeLineCoverageValues(int lineCount, SortedMap valueByLine, SortedMap coveragePerLine, LineCoverageOperation op) { for (Map.Entry 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; diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/sensor/ModuleSensorContext.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/sensor/ModuleSensorContext.java index 3161da1e96d..6f27d1be09f 100644 --- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/sensor/ModuleSensorContext.java +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/sensor/ModuleSensorContext.java @@ -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; } diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/sensor/ProjectSensorContext.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/sensor/ProjectSensorContext.java index ec0b9e59f26..7694b77e8b7 100644 --- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/sensor/ProjectSensorContext.java +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/sensor/ProjectSensorContext.java @@ -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 index 00000000000..f71e9a8f8c5 --- /dev/null +++ b/sonar-scanner-engine/src/test/java/org/sonar/scanner/report/TelemetryPublisherTest.java @@ -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 expected = Arrays.asList( + newTelemetryEntry("key1", "value1"), + newTelemetryEntry("key2", "value2")); + expectWritten(expected); + } + + private void expectWritten(List expected) { + verify(writer).writeTelemetry(argThat(entries -> { + List 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 index 00000000000..cbdb518db7f --- /dev/null +++ b/sonar-scanner-engine/src/test/java/org/sonar/scanner/repository/TelemetryCacheTest.java @@ -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"); + } + +} diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scanner/sensor/DefaultSensorStorageTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scanner/sensor/DefaultSensorStorageTest.java index 1eeea7ede12..41680ce3a1d 100644 --- a/sonar-scanner-engine/src/test/java/org/sonar/scanner/sensor/DefaultSensorStorageTest.java +++ b/sonar-scanner-engine/src/test/java/org/sonar/scanner/sensor/DefaultSensorStorageTest.java @@ -20,14 +20,12 @@ 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.findByKey(CoreMetrics.NCLOC_KEY)).thenReturn(CoreMetrics.NCLOC); when(metricFinder.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")); diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scanner/sensor/ModuleSensorContextTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scanner/sensor/ModuleSensorContextTest.java index 94d7a2ce282..4ca7d33d642 100644 --- a/sonar-scanner-engine/src/test/java/org/sonar/scanner/sensor/ModuleSensorContextTest.java +++ b/sonar-scanner-engine/src/test/java/org/sonar/scanner/sensor/ModuleSensorContextTest.java @@ -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 index 00000000000..5d25250e8bb --- /dev/null +++ b/sonar-scanner-engine/src/test/java/org/sonar/scanner/sensor/ProjectSensorContextTest.java @@ -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); + } +} diff --git a/sonar-scanner-protocol/src/main/java/org/sonar/scanner/protocol/output/FileStructure.java b/sonar-scanner-protocol/src/main/java/org/sonar/scanner/protocol/output/FileStructure.java index a3fb8835fba..686b7d20066 100644 --- a/sonar-scanner-protocol/src/main/java/org/sonar/scanner/protocol/output/FileStructure.java +++ b/sonar-scanner-protocol/src/main/java/org/sonar/scanner/protocol/output/FileStructure.java @@ -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"); } diff --git a/sonar-scanner-protocol/src/main/java/org/sonar/scanner/protocol/output/ScannerReportWriter.java b/sonar-scanner-protocol/src/main/java/org/sonar/scanner/protocol/output/ScannerReportWriter.java index 20e5f30a1f0..ad4720cbb5f 100644 --- a/sonar-scanner-protocol/src/main/java/org/sonar/scanner/protocol/output/ScannerReportWriter.java +++ b/sonar-scanner-protocol/src/main/java/org/sonar/scanner/protocol/output/ScannerReportWriter.java @@ -163,6 +163,12 @@ public class ScannerReportWriter { return file; } + public File writeTelemetry(Iterable telemetryEntries) { + File file = fileStructure.telemetryEntries(); + Protobuf.writeStream(telemetryEntries, file, false); + return file; + } + public File getSourceFile(int componentRef) { return fileStructure.fileFor(FileStructure.Domain.SOURCE, componentRef); } diff --git a/sonar-scanner-protocol/src/main/protobuf/scanner_report.proto b/sonar-scanner-protocol/src/main/protobuf/scanner_report.proto index e4954383859..b7c1d3d5064 100644 --- a/sonar-scanner-protocol/src/main/protobuf/scanner_report.proto +++ b/sonar-scanner-protocol/src/main/protobuf/scanner_report.proto @@ -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; diff --git a/sonar-scanner-protocol/src/test/java/org/sonar/scanner/protocol/output/FileStructureTest.java b/sonar-scanner-protocol/src/test/java/org/sonar/scanner/protocol/output/FileStructureTest.java index b2787934275..9c7ada548b3 100644 --- a/sonar-scanner-protocol/src/test/java/org/sonar/scanner/protocol/output/FileStructureTest.java +++ b/sonar-scanner-protocol/src/test/java/org/sonar/scanner/protocol/output/FileStructureTest.java @@ -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); + } } diff --git a/sonar-scanner-protocol/src/test/java/org/sonar/scanner/protocol/output/ScannerReportWriterTest.java b/sonar-scanner-protocol/src/test/java/org/sonar/scanner/protocol/output/ScannerReportWriterTest.java index 368886e2265..4b89c9a3db5 100644 --- a/sonar-scanner-protocol/src/test/java/org/sonar/scanner/protocol/output/ScannerReportWriterTest.java +++ b/sonar-scanner-protocol/src/test/java/org/sonar/scanner/protocol/output/ScannerReportWriterTest.java @@ -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 input = List.of( + ScannerReport.TelemetryEntry.newBuilder() + .setKey("key") + .setValue("value").build(), + ScannerReport.TelemetryEntry.newBuilder() + .setKey("key2") + .setValue("value2").build()); + + underTest.writeTelemetry(input); + + try (CloseableIterator telemetryIterator = + Protobuf.readStream(underTest.getFileStructure().telemetryEntries(), ScannerReport.TelemetryEntry.parser())) { + + assertThat(telemetryIterator).toIterable() + .containsExactlyElementsOf(input) + .hasSize(input.size()); + } + } } -- 2.39.5