From 3a5d10ad8f95205f010aaae640eca0648ad65b0c Mon Sep 17 00:00:00 2001 From: zglicz Date: Thu, 12 Dec 2024 15:08:54 +0100 Subject: [PATCH] JS-493 Add 'get-telemetry' endpoint on bridge-server (#5011) Co-authored-by: Ilia Kebets --- packages/bridge/src/handle-request.ts | 6 +- packages/bridge/src/request.ts | 13 +++- packages/bridge/src/router.ts | 1 + packages/bridge/tests/router.test.ts | 6 ++ packages/jsts/src/analysis/analyzer.ts | 9 ++- .../jsts/src/rules/helpers/package-json.ts | 73 ++++++++++++++----- packages/jsts/tests/analysis/analyzer.test.ts | 47 +++++++++++- .../analysis/fixtures/dependencies/index.js | 1 + .../fixtures/dependencies/package.json | 11 +++ .../javascript/bridge/BridgeServer.java | 6 ++ .../javascript/bridge/BridgeServerImpl.java | 10 +++ .../bridge/BridgeServerImplTest.java | 12 +++ .../test/resources/mock-bridge/startServer.js | 2 + .../analysis/AnalysisWithProgram.java | 2 + .../javascript/analysis/PluginTelemetry.java | 59 +++++++++++++++ .../JavaScriptEslintBasedSensorTest.java | 25 +++++++ .../analysis/PluginTelemetryTest.java | 63 ++++++++++++++++ 17 files changed, 323 insertions(+), 23 deletions(-) create mode 100644 packages/jsts/tests/analysis/fixtures/dependencies/index.js create mode 100644 packages/jsts/tests/analysis/fixtures/dependencies/package.json create mode 100644 sonar-plugin/sonar-javascript-plugin/src/main/java/org/sonar/plugins/javascript/analysis/PluginTelemetry.java create mode 100644 sonar-plugin/sonar-javascript-plugin/src/test/java/org/sonar/plugins/javascript/analysis/PluginTelemetryTest.java diff --git a/packages/bridge/src/handle-request.ts b/packages/bridge/src/handle-request.ts index 1144f10b6da..89d42a3e3b1 100644 --- a/packages/bridge/src/handle-request.ts +++ b/packages/bridge/src/handle-request.ts @@ -16,7 +16,7 @@ */ import { analyzeCSS } from '../../css/src/analysis/analyzer.js'; import { analyzeHTML } from '../../html/src/index.js'; -import { analyzeJSTS } from '../../jsts/src/analysis/analyzer.js'; +import { analyzeJSTS, getTelemetry } from '../../jsts/src/analysis/analyzer.js'; import { analyzeProject } from '../../jsts/src/analysis/projectAnalysis/projectAnalyzer.js'; import { analyzeYAML } from '../../yaml/src/index.js'; import { logHeapStatistics } from './memory.js'; @@ -107,6 +107,10 @@ export async function handleRequest(request: BridgeRequest): Promise; }; +export type Telemetry = { + dependencies: NamedDependency[]; +}; + export type RequestType = BridgeRequest['type']; type MaybeIncompleteCssAnalysisInput = Omit & { @@ -61,7 +66,8 @@ export type BridgeRequest = | DeleteProgramRequest | InitLinterRequest | NewTsConfigRequest - | TsConfigFilesRequest; + | TsConfigFilesRequest + | GetTelemetryRequest; type CssRequest = { type: 'on-analyze-css'; @@ -115,6 +121,9 @@ type TsConfigFilesRequest = { type: 'on-tsconfig-files'; data: { tsConfig: string }; }; +type GetTelemetryRequest = { + type: 'on-get-telemetry'; +}; /** * In SonarQube context, an analysis input includes both path and content of a file diff --git a/packages/bridge/src/router.ts b/packages/bridge/src/router.ts index 44b9d6efa79..744fd636612 100644 --- a/packages/bridge/src/router.ts +++ b/packages/bridge/src/router.ts @@ -36,6 +36,7 @@ export default function (worker?: Worker): express.Router { router.post('/init-linter', delegate('on-init-linter')); router.post('/new-tsconfig', delegate('on-new-tsconfig')); router.post('/tsconfig-files', delegate('on-tsconfig-files')); + router.get('/get-telemetry', delegate('on-get-telemetry')); /** Endpoints running on the main thread */ router.get('/status', (_, response) => response.send('OK!')); diff --git a/packages/bridge/tests/router.test.ts b/packages/bridge/tests/router.test.ts index 3fe5c02ef61..8d73d4cc61f 100644 --- a/packages/bridge/tests/router.test.ts +++ b/packages/bridge/tests/router.test.ts @@ -326,6 +326,12 @@ describe('router', () => { expect(json.filename).toBeTruthy(); expect(fs.existsSync(json.filename)).toBe(true); }); + + it('should return empty get-telemetry on fresh server', async () => { + const response = (await request(server, '/get-telemetry', 'GET')) as string; + const json = JSON.parse(response); + expect(json).toEqual({ dependencies: [] }); + }); }); function requestInitLinter(server: http.Server, rules: RuleConfig[]) { diff --git a/packages/jsts/src/analysis/analyzer.ts b/packages/jsts/src/analysis/analyzer.ts index e122646dab6..bd4719d6b29 100644 --- a/packages/jsts/src/analysis/analyzer.ts +++ b/packages/jsts/src/analysis/analyzer.ts @@ -29,7 +29,8 @@ import { getContext } from '../../../shared/src/helpers/context.js'; import { computeMetrics, findNoSonarLines } from '../linter/visitors/metrics/index.js'; import { getSyntaxHighlighting } from '../linter/visitors/syntax-highlighting.js'; import { getCpdTokens } from '../linter/visitors/cpd.js'; -import { clearDependenciesCache } from '../rules/helpers/package-json.js'; +import { clearDependenciesCache, getAllDependencies } from '../rules/index.js'; +import { Telemetry } from '../../../bridge/src/request.js'; /** * Analyzes a JavaScript / TypeScript analysis input @@ -160,3 +161,9 @@ function computeExtendedMetrics( }; } } + +export function getTelemetry(): Telemetry { + return { + dependencies: getAllDependencies(), + }; +} diff --git a/packages/jsts/src/rules/helpers/package-json.ts b/packages/jsts/src/rules/helpers/package-json.ts index 583eae6bc6c..71a4e839132 100644 --- a/packages/jsts/src/rules/helpers/package-json.ts +++ b/packages/jsts/src/rules/helpers/package-json.ts @@ -30,10 +30,42 @@ const findPackageJsons = createFindUp(PACKAGE_JSON); const DefinitelyTyped = '@types/'; +type MinimatchDependency = { + name: Minimatch; + version?: string; +}; + +export type NamedDependency = { + name: string; + version?: string; +}; + +type Dependency = MinimatchDependency | NamedDependency; + /** * Cache for the available dependencies by dirname. */ -const cache: Map> = new Map(); +const cache: Map> = new Map(); + +/** + * Returns the dependencies of the root package.json file collected in the cache. + * As the cache is populated lazily, it could be null in case no rule execution has touched it. + * This removes duplicate dependencies and keeps the last occurrence. + */ +export function getAllDependencies(): NamedDependency[] { + const dependencies = [...cache.values()] + .flatMap(dependencies => [...dependencies]) + .filter((dependency): dependency is NamedDependency => typeof dependency.name === 'string'); + return Object.values( + dependencies.reduce( + (result, dependency) => ({ + ...result, + [dependency.name]: dependency, + }), + {}, + ), + ); +} /** * Retrieve the dependencies of all the package.json files available for the given file. @@ -46,9 +78,9 @@ export function getDependencies(filename: string, cwd: string) { const dirname = Path.dirname(toUnixPath(filename)); const cached = cache.get(dirname); if (cached) { - return cached; + return new Set([...cached].map(item => item.name)); } - const result = new Set(); + const result = new Set(); cache.set(dirname, result); getManifests(dirname, cwd, fs).forEach(manifest => { @@ -59,7 +91,7 @@ export function getDependencies(filename: string, cwd: string) { }); }); - return result; + return new Set([...result].map(item => item.name)); } /** @@ -71,7 +103,7 @@ export function clearDependenciesCache() { } export function getDependenciesFromPackageJson(content: PackageJson) { - const result = new Set(); + const result = new Set(); if (content.name) { addDependencies(result, { [content.name]: '*' }); } @@ -99,30 +131,37 @@ export function getDependenciesFromPackageJson(content: PackageJson) { } function addDependencies( - result: Set, + result: Set, dependencies: PackageJson.Dependency, isGlob = false, ) { - Object.keys(dependencies).forEach(name => addDependency(result, name, isGlob)); + Object.keys(dependencies).forEach(name => + addDependency(result, name, isGlob, dependencies[name]), + ); } -function addDependenciesArray( - result: Set, - dependencies: string[], - isGlob = true, -) { +function addDependenciesArray(result: Set, dependencies: string[], isGlob = true) { dependencies.forEach(name => addDependency(result, name, isGlob)); } -function addDependency(result: Set, dependency: string, isGlob: boolean) { +function addDependency( + result: Set, + dependency: string, + isGlob: boolean, + version?: string, +) { if (isGlob) { - result.add(new Minimatch(dependency, { nocase: true, matchBase: true })); + result.add({ + name: new Minimatch(dependency, { nocase: true, matchBase: true }), + version, + }); } else { - result.add( - dependency.startsWith(DefinitelyTyped) + result.add({ + name: dependency.startsWith(DefinitelyTyped) ? dependency.substring(DefinitelyTyped.length) : dependency, - ); + version, + }); } } diff --git a/packages/jsts/tests/analysis/analyzer.test.ts b/packages/jsts/tests/analysis/analyzer.test.ts index b61b5cb71b3..cf5311c9c5f 100644 --- a/packages/jsts/tests/analysis/analyzer.test.ts +++ b/packages/jsts/tests/analysis/analyzer.test.ts @@ -19,9 +19,9 @@ import { jsTsInput, parseJavaScriptSourceFile } from '../tools/index.js'; import { Linter, Rule } from 'eslint'; import { describe, beforeEach, it } from 'node:test'; import { expect } from 'expect'; -import { getManifests, toUnixPath } from '../../src/rules/helpers/index.js'; +import { getDependencies, getManifests, toUnixPath } from '../../src/rules/helpers/index.js'; import { setContext } from '../../../shared/src/helpers/context.js'; -import { analyzeJSTS } from '../../src/analysis/analyzer.js'; +import { analyzeJSTS, getTelemetry } from '../../src/analysis/analyzer.js'; import { APIError } from '../../../shared/src/errors/error.js'; import { RuleConfig } from '../../src/linter/config/rule-config.js'; import { initializeLinter } from '../../src/linter/linters.js'; @@ -899,6 +899,49 @@ describe('analyzeJSTS', () => { expect(vueIssues[0].message).toEqual('call'); }); + it('should populate dependencies after analysis', async () => { + const baseDir = path.join(currentPath, 'fixtures', 'dependencies'); + const linter = new Linter(); + linter.defineRule('custom-rule-file', { + create(context) { + return { + CallExpression(node) { + // Necessarily call 'getDependencies' to populate the cache of dependencies + const dependencies = getDependencies(toUnixPath(context.filename), baseDir); + if (dependencies.size) { + context.report({ + node: node.callee, + message: 'call', + }); + } + }, + }; + }, + } as Rule.RuleModule); + const filePath = path.join(currentPath, 'fixtures', 'dependencies', 'index.js'); + const sourceCode = await parseJavaScriptSourceFile(filePath); + linter.verify( + sourceCode, + { rules: { 'custom-rule-file': 'error' } }, + { filename: filePath, allowInlineConfig: false }, + ); + const { dependencies } = getTelemetry(); + expect(dependencies).toStrictEqual([ + { + name: 'test-module', + version: '*', + }, + { + name: 'pkg1', + version: '1.0.0', + }, + { + name: 'pkg2', + version: '2.0.0', + }, + ]); + }); + it('should return the AST along with the issues', async () => { const rules = [{ key: 'S4524', configurations: [], fileTypeTarget: ['MAIN'] }] as RuleConfig[]; await initializeLinter(rules); diff --git a/packages/jsts/tests/analysis/fixtures/dependencies/index.js b/packages/jsts/tests/analysis/fixtures/dependencies/index.js new file mode 100644 index 00000000000..019c0f4bc8e --- /dev/null +++ b/packages/jsts/tests/analysis/fixtures/dependencies/index.js @@ -0,0 +1 @@ +console.log("Hello World!"); diff --git a/packages/jsts/tests/analysis/fixtures/dependencies/package.json b/packages/jsts/tests/analysis/fixtures/dependencies/package.json new file mode 100644 index 00000000000..e6b03dd2cae --- /dev/null +++ b/packages/jsts/tests/analysis/fixtures/dependencies/package.json @@ -0,0 +1,11 @@ +{ + "name": "test-module", + "version": "1.0.0", + "author": "Your Name ", + "dependencies": { + "pkg1": "1.0.0" + }, + "devDependencies": { + "pkg2": "2.0.0" + } +} diff --git a/sonar-plugin/bridge/src/main/java/org/sonar/plugins/javascript/bridge/BridgeServer.java b/sonar-plugin/bridge/src/main/java/org/sonar/plugins/javascript/bridge/BridgeServer.java index ac030ab1779..52b7d28720e 100644 --- a/sonar-plugin/bridge/src/main/java/org/sonar/plugins/javascript/bridge/BridgeServer.java +++ b/sonar-plugin/bridge/src/main/java/org/sonar/plugins/javascript/bridge/BridgeServer.java @@ -68,6 +68,8 @@ void initLinter( TsConfigFile createTsConfigFile(String content) throws IOException; + TelemetryResponse getTelemetry(); + record JsAnalysisRequest( String filePath, String fileType, @@ -275,4 +277,8 @@ public String toString() { } record TsProgramRequest(String tsConfig) {} + + record TelemetryResponse(List dependencies) {} + + record Dependency(String name, String version) {} } diff --git a/sonar-plugin/bridge/src/main/java/org/sonar/plugins/javascript/bridge/BridgeServerImpl.java b/sonar-plugin/bridge/src/main/java/org/sonar/plugins/javascript/bridge/BridgeServerImpl.java index 704a8356b73..6ea90a4fe9d 100644 --- a/sonar-plugin/bridge/src/main/java/org/sonar/plugins/javascript/bridge/BridgeServerImpl.java +++ b/sonar-plugin/bridge/src/main/java/org/sonar/plugins/javascript/bridge/BridgeServerImpl.java @@ -533,6 +533,16 @@ public TsConfigFile createTsConfigFile(String content) { return GSON.fromJson(response.json(), TsConfigFile.class); } + @Override + public TelemetryResponse getTelemetry() { + try { + var result = http.get(url("get-telemetry")); + return GSON.fromJson(result, TelemetryResponse.class); + } catch (IOException e) { + return new TelemetryResponse(List.of()); + } + } + private static List emptyListIfNull(@Nullable List list) { return list == null ? emptyList() : list; } diff --git a/sonar-plugin/bridge/src/test/java/org/sonar/plugins/javascript/bridge/BridgeServerImplTest.java b/sonar-plugin/bridge/src/test/java/org/sonar/plugins/javascript/bridge/BridgeServerImplTest.java index 4792fc6fce3..a56439ab100 100644 --- a/sonar-plugin/bridge/src/test/java/org/sonar/plugins/javascript/bridge/BridgeServerImplTest.java +++ b/sonar-plugin/bridge/src/test/java/org/sonar/plugins/javascript/bridge/BridgeServerImplTest.java @@ -62,7 +62,9 @@ import org.sonar.api.utils.TempFolder; import org.sonar.api.utils.Version; import org.sonar.plugins.javascript.bridge.BridgeServer.CssAnalysisRequest; +import org.sonar.plugins.javascript.bridge.BridgeServer.Dependency; import org.sonar.plugins.javascript.bridge.BridgeServer.JsAnalysisRequest; +import org.sonar.plugins.javascript.bridge.BridgeServer.TelemetryResponse; import org.sonar.plugins.javascript.bridge.BridgeServer.TsProgram; import org.sonar.plugins.javascript.bridge.BridgeServer.TsProgramRequest; import org.sonar.plugins.javascript.bridge.protobuf.Node; @@ -752,6 +754,16 @@ void test_ucfg_bundle_version() throws Exception { ); } + @Test + void should_return_telemetry() throws Exception { + bridgeServer = createBridgeServer(START_SERVER_SCRIPT); + bridgeServer.startServer(serverConfig, emptyList()); + var telemetry = bridgeServer.getTelemetry(); + assertThat(telemetry).isEqualTo( + new TelemetryResponse(List.of(new Dependency("pkg1", "1.0.0"))) + ); + } + @Test void should_return_an_ast() throws Exception { bridgeServer = createBridgeServer(START_SERVER_SCRIPT); diff --git a/sonar-plugin/bridge/src/test/resources/mock-bridge/startServer.js b/sonar-plugin/bridge/src/test/resources/mock-bridge/startServer.js index 80e540faa4b..e01118015d3 100644 --- a/sonar-plugin/bridge/src/test/resources/mock-bridge/startServer.js +++ b/sonar-plugin/bridge/src/test/resources/mock-bridge/startServer.js @@ -47,6 +47,8 @@ const requestHandler = (request, response) => { loc: {}}]}]}], highlights: [{location: {startLine: 0, startColumn: 0, endLine: 0, endColumn: 0}}], metrics: {}, highlightedSymbols: [{}], cpdTokens: [{}] }`); + } else if (request.url === '/get-telemetry') { + response.end('{"dependencies": [{"name": "pkg1", "version": "1.0.0"}]}'); } else { // /analyze-with-program // /analyze-js diff --git a/sonar-plugin/sonar-javascript-plugin/src/main/java/org/sonar/plugins/javascript/analysis/AnalysisWithProgram.java b/sonar-plugin/sonar-javascript-plugin/src/main/java/org/sonar/plugins/javascript/analysis/AnalysisWithProgram.java index 076fa77e0ad..b7ff2ec225a 100644 --- a/sonar-plugin/sonar-javascript-plugin/src/main/java/org/sonar/plugins/javascript/analysis/AnalysisWithProgram.java +++ b/sonar-plugin/sonar-javascript-plugin/src/main/java/org/sonar/plugins/javascript/analysis/AnalysisWithProgram.java @@ -111,6 +111,8 @@ void analyzeFiles(List inputFiles) throws IOException { ) ); } + var telemetry = bridgeServer.getTelemetry(); + new PluginTelemetry(context).reportTelemetry(telemetry); } finally { if (success) { progressReport.stop(); diff --git a/sonar-plugin/sonar-javascript-plugin/src/main/java/org/sonar/plugins/javascript/analysis/PluginTelemetry.java b/sonar-plugin/sonar-javascript-plugin/src/main/java/org/sonar/plugins/javascript/analysis/PluginTelemetry.java new file mode 100644 index 00000000000..2bb09af2751 --- /dev/null +++ b/sonar-plugin/sonar-javascript-plugin/src/main/java/org/sonar/plugins/javascript/analysis/PluginTelemetry.java @@ -0,0 +1,59 @@ +/* + * SonarQube JavaScript Plugin + * Copyright (C) 2011-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 Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * 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 Sonar Source-Available License for more details. + * + * You should have received a copy of the Sonar Source-Available License + * along with this program; if not, see https://sonarsource.com/license/ssal/ + */ +package org.sonar.plugins.javascript.analysis; + +import java.util.stream.Collectors; +import javax.annotation.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.sonar.api.batch.sensor.SensorContext; +import org.sonar.api.utils.Version; +import org.sonar.plugins.javascript.bridge.BridgeServer.Dependency; +import org.sonar.plugins.javascript.bridge.BridgeServer.TelemetryResponse; + +public class PluginTelemetry { + + private static final Logger LOG = LoggerFactory.getLogger(PluginTelemetry.class); + private static final String KEY_PREFIX = "javascript."; + private static final String DEPENDENCY_PREFIX = KEY_PREFIX + "dependency."; + + private final SensorContext ctx; + + public PluginTelemetry(SensorContext ctx) { + this.ctx = ctx; + } + + void reportTelemetry(@Nullable TelemetryResponse telemetry) { + var isTelemetrySupported = ctx + .runtime() + .getApiVersion() + .isGreaterThanOrEqual(Version.create(10, 9)); + if (telemetry == null || !isTelemetrySupported) { + // addTelemetryProperty is added in 10.9: + // https://github.com/SonarSource/sonar-plugin-api/releases/tag/10.9.0.2362 + return; + } + var keyMapToSave = telemetry + .dependencies() + .stream() + .collect( + Collectors.toMap(dependency -> DEPENDENCY_PREFIX + dependency.name(), Dependency::version) + ); + keyMapToSave.forEach(ctx::addTelemetryProperty); + LOG.debug("Telemetry saved: {}", keyMapToSave); + } +} diff --git a/sonar-plugin/sonar-javascript-plugin/src/test/java/org/sonar/plugins/javascript/analysis/JavaScriptEslintBasedSensorTest.java b/sonar-plugin/sonar-javascript-plugin/src/test/java/org/sonar/plugins/javascript/analysis/JavaScriptEslintBasedSensorTest.java index ffdf84be48e..8d6883ff81e 100644 --- a/sonar-plugin/sonar-javascript-plugin/src/test/java/org/sonar/plugins/javascript/analysis/JavaScriptEslintBasedSensorTest.java +++ b/sonar-plugin/sonar-javascript-plugin/src/test/java/org/sonar/plugins/javascript/analysis/JavaScriptEslintBasedSensorTest.java @@ -57,6 +57,7 @@ import org.sonar.api.batch.rule.CheckFactory; import org.sonar.api.batch.rule.internal.ActiveRulesBuilder; import org.sonar.api.batch.rule.internal.NewActiveRule; +import org.sonar.api.batch.sensor.SensorContext; import org.sonar.api.batch.sensor.cache.WriteCache; import org.sonar.api.batch.sensor.highlighting.TypeOfText; import org.sonar.api.batch.sensor.internal.DefaultSensorDescriptor; @@ -79,7 +80,9 @@ import org.sonar.plugins.javascript.bridge.AnalysisWarningsWrapper; import org.sonar.plugins.javascript.bridge.BridgeServer; import org.sonar.plugins.javascript.bridge.BridgeServer.AnalysisResponse; +import org.sonar.plugins.javascript.bridge.BridgeServer.Dependency; import org.sonar.plugins.javascript.bridge.BridgeServer.JsAnalysisRequest; +import org.sonar.plugins.javascript.bridge.BridgeServer.TelemetryResponse; import org.sonar.plugins.javascript.bridge.BridgeServer.TsProgram; import org.sonar.plugins.javascript.bridge.EslintRule; import org.sonar.plugins.javascript.bridge.PluginInfo; @@ -752,6 +755,28 @@ void log_debug_analyzed_filename() throws Exception { assertThat(logTester.logs(Level.DEBUG)).contains("Analyzing file: " + file.uri()); } + @Test + void should_add_telemetry_for_scanner_analysis() throws Exception { + when(bridgeServerMock.analyzeJavaScript(any())).thenReturn(new AnalysisResponse()); + when(bridgeServerMock.getTelemetry()).thenReturn( + new TelemetryResponse(List.of(new Dependency("pkg1", "1.1.0"))) + ); + var sensor = createSensor(); + context.setRuntime( + SonarRuntimeImpl.forSonarQube( + Version.create(10, 9), + SonarQubeSide.SCANNER, + SonarEdition.COMMUNITY + ) + ); + context.setNextCache(mock(WriteCache.class)); + createInputFile(context); + sensor.execute(context); + assertThat(logTester.logs(Level.DEBUG)).contains( + "Telemetry saved: {javascript.dependency.pkg1=1.1.0}" + ); + } + private static JsTsChecks checks(String... ruleKeys) { ActiveRulesBuilder builder = new ActiveRulesBuilder(); for (String ruleKey : ruleKeys) { diff --git a/sonar-plugin/sonar-javascript-plugin/src/test/java/org/sonar/plugins/javascript/analysis/PluginTelemetryTest.java b/sonar-plugin/sonar-javascript-plugin/src/test/java/org/sonar/plugins/javascript/analysis/PluginTelemetryTest.java new file mode 100644 index 00000000000..973cbbfd1c2 --- /dev/null +++ b/sonar-plugin/sonar-javascript-plugin/src/test/java/org/sonar/plugins/javascript/analysis/PluginTelemetryTest.java @@ -0,0 +1,63 @@ +/* + * SonarQube JavaScript Plugin + * Copyright (C) 2011-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 Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * 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 Sonar Source-Available License for more details. + * + * You should have received a copy of the Sonar Source-Available License + * along with this program; if not, see https://sonarsource.com/license/ssal/ + */ +package org.sonar.plugins.javascript.analysis; + +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.sonar.api.SonarRuntime; +import org.sonar.api.batch.sensor.SensorContext; +import org.sonar.api.utils.Version; +import org.sonar.plugins.javascript.bridge.BridgeServer.Dependency; +import org.sonar.plugins.javascript.bridge.BridgeServer.TelemetryResponse; + +class PluginTelemetryTest { + + private SensorContext ctx; + private PluginTelemetry pluginTelemetry; + private TelemetryResponse telemetryResponse; + + @BeforeEach + void setUp() { + ctx = mock(SensorContext.class); + SonarRuntime sonarRuntime = mock(SonarRuntime.class); + when(ctx.runtime()).thenReturn(sonarRuntime); + pluginTelemetry = new PluginTelemetry(ctx); + telemetryResponse = new TelemetryResponse(List.of(new Dependency("pkg1", "1.0.0"))); + } + + @Test + void shouldNotReportIfApiVersionIsLessThan109() { + when(ctx.runtime().getApiVersion()).thenReturn(Version.create(10, 8)); + pluginTelemetry.reportTelemetry(telemetryResponse); + verify(ctx, never()).addTelemetryProperty(anyString(), anyString()); + } + + @Test + void shouldReportIfApiVersionIsGreaterThanOrEqualTo109() { + when(ctx.runtime().getApiVersion()).thenReturn(Version.create(10, 9)); + pluginTelemetry.reportTelemetry(telemetryResponse); + verify(ctx).addTelemetryProperty("javascript.dependency.pkg1", "1.0.0"); + } +}