diff --git a/its/ruling/src/test/expected/js/angular.js/javascript-S2486.json b/its/ruling/src/test/expected/js/angular.js/javascript-S2486.json index 49cd72c2cd7..57a05f87564 100644 --- a/its/ruling/src/test/expected/js/angular.js/javascript-S2486.json +++ b/its/ruling/src/test/expected/js/angular.js/javascript-S2486.json @@ -9,6 +9,9 @@ "angular.js:test/helpers/support.js": [ 16 ], +"angular.js:test/ng/compileSpec.fixture.js": [ +688 +], "angular.js:test/ng/compileSpec.js": [ 688 ], diff --git a/its/ruling/src/test/expected/js/javascript-test-sources/javascript-S1874.json b/its/ruling/src/test/expected/js/javascript-test-sources/javascript-S1874.json index 5ddd21ad7cd..ed966d562ba 100644 --- a/its/ruling/src/test/expected/js/javascript-test-sources/javascript-S1874.json +++ b/its/ruling/src/test/expected/js/javascript-test-sources/javascript-S1874.json @@ -94,6 +94,11 @@ "javascript-test-sources:src/ace/src/mouse/touch_handler.js": [ 59 ], +"javascript-test-sources:src/ace/src/scrollbar.js": [ +125, +130, +223 +], "javascript-test-sources:src/ace/src/scrollbar_test.js": [ 13 ], @@ -101,8 +106,13 @@ 270 ], "javascript-test-sources:src/ace/src/test/all_browser.js": [ +11, 116 ], +"javascript-test-sources:src/ace/src/virtual_renderer.js": [ +1159, +1167 +], "javascript-test-sources:src/ace/static.js": [ 33 ], @@ -129,6 +139,15 @@ 123, 124 ], +"javascript-test-sources:src/ecmascript6/Ghost/core/server/models/post.js": [ +329 +], +"javascript-test-sources:src/ecmascript6/Ghost/core/server/models/tag.js": [ +68 +], +"javascript-test-sources:src/ecmascript6/Ghost/core/server/models/user.js": [ +184 +], "javascript-test-sources:src/ecmascript6/ecmascript6-today/7-rest-params/js/rest-spread.js": [ 25 ], @@ -141,6 +160,8 @@ ], "javascript-test-sources:src/ecmascript6/router/third_party/brick/brick-1.0.1.byob.js": [ 337, +572, +575, 877, 1412, 1637, diff --git a/its/ruling/src/test/expected/js/p5.js/javascript-S1874.json b/its/ruling/src/test/expected/js/p5.js/javascript-S1874.json index eac6ca179cf..14c38f6c2ae 100644 --- a/its/ruling/src/test/expected/js/p5.js/javascript-S1874.json +++ b/its/ruling/src/test/expected/js/p5.js/javascript-S1874.json @@ -2,8 +2,6 @@ "p5.js:docs/yuidoc-p5-theme/assets/js/reference.js": [ 2433, 2433, -2535, -3895, 4316, 4316, 4503, diff --git a/its/ruling/src/test/java/org/sonar/javascript/it/JavaScriptRulingTest.java b/its/ruling/src/test/java/org/sonar/javascript/it/JavaScriptRulingTest.java index 1c39f50ba12..350f209c5e2 100644 --- a/its/ruling/src/test/java/org/sonar/javascript/it/JavaScriptRulingTest.java +++ b/its/ruling/src/test/java/org/sonar/javascript/it/JavaScriptRulingTest.java @@ -272,8 +272,7 @@ static void runRulingTest( .setProperty("sonar.javascript.node.maxspace", "2048") .setProperty("sonar.javascript.maxFileSize", "4000") .setProperty("sonar.cpd.exclusions", "**/*") - .setProperty("sonar.internal.analysis.failFast", "true") - .setDebugLogs(true); + .setProperty("sonar.internal.analysis.failFast", "true"); orchestrator.executeBuild(build); assertThat(differencesPath).hasContent(""); diff --git a/sonar-plugin/sonar-javascript-plugin/pom.xml b/sonar-plugin/sonar-javascript-plugin/pom.xml index dcfc79ca6d5..93401193e44 100644 --- a/sonar-plugin/sonar-javascript-plugin/pom.xml +++ b/sonar-plugin/sonar-javascript-plugin/pom.xml @@ -164,6 +164,10 @@ win-x64/node.exe.xz ${project.build.directory}/node/win-x64/node.exe.xz + + win-x64/version.txt + ${project.build.directory}/node/win-x64/version.txt + @@ -188,6 +192,10 @@ linux-x64/node.xz ${project.build.directory}/node/linux-x64/node.xz + + linux-x64/version.txt + ${project.build.directory}/node/linux-x64/version.txt + @@ -212,6 +220,10 @@ darwin-arm64/node.xz ${project.build.directory}/node/darwin-arm64/node.xz + + darwin-arm64/version.txt + ${project.build.directory}/node/darwin-arm64/version.txt + @@ -236,14 +248,26 @@ win-x64/node.exe.xz ${project.build.directory}/node/win-x64/node.exe.xz + + win-x64/version.txt + ${project.build.directory}/node/win-x64/version.txt + linux-x64/node.xz ${project.build.directory}/node/linux-x64/node.xz + + linux-x64/version.txt + ${project.build.directory}/node/linux-x64/version.txt + darwin-arm64/node.xz ${project.build.directory}/node/darwin-arm64/node.xz + + darwin-arm64/version.txt + ${project.build.directory}/node/darwin-arm64/version.txt + diff --git a/sonar-plugin/sonar-javascript-plugin/src/main/java/org/sonar/plugins/javascript/JavaScriptPlugin.java b/sonar-plugin/sonar-javascript-plugin/src/main/java/org/sonar/plugins/javascript/JavaScriptPlugin.java index 07fd5a67959..085f0749c67 100644 --- a/sonar-plugin/sonar-javascript-plugin/src/main/java/org/sonar/plugins/javascript/JavaScriptPlugin.java +++ b/sonar-plugin/sonar-javascript-plugin/src/main/java/org/sonar/plugins/javascript/JavaScriptPlugin.java @@ -39,6 +39,7 @@ import org.sonar.plugins.javascript.bridge.BundleImpl; import org.sonar.plugins.javascript.bridge.CssRuleSensor; import org.sonar.plugins.javascript.bridge.EmbeddedNode; +import org.sonar.plugins.javascript.bridge.Environment; import org.sonar.plugins.javascript.bridge.HtmlSensor; import org.sonar.plugins.javascript.bridge.JsTsChecks; import org.sonar.plugins.javascript.bridge.JsTsSensor; @@ -152,7 +153,8 @@ public void define(Context context) { AnalysisProcessor.class, YamlSensor.class, HtmlSensor.class, - EmbeddedNode.class + EmbeddedNode.class, + Environment.class ); context.addExtensions( diff --git a/sonar-plugin/sonar-javascript-plugin/src/main/java/org/sonar/plugins/javascript/bridge/BridgeServerImpl.java b/sonar-plugin/sonar-javascript-plugin/src/main/java/org/sonar/plugins/javascript/bridge/BridgeServerImpl.java index 16917cd0168..b1260151283 100644 --- a/sonar-plugin/sonar-javascript-plugin/src/main/java/org/sonar/plugins/javascript/bridge/BridgeServerImpl.java +++ b/sonar-plugin/sonar-javascript-plugin/src/main/java/org/sonar/plugins/javascript/bridge/BridgeServerImpl.java @@ -74,8 +74,7 @@ private enum Status { public static final String SONARJS_EXISTING_NODE_PROCESS_PORT = "SONARJS_EXISTING_NODE_PROCESS_PORT"; private static final Gson GSON = new Gson(); - - private static final String DEPLOY_LOCATION = "bridge-bundle"; + private static final String BRIDGE_DEPLOY_LOCATION = "bridge-bundle"; private final HttpClient client; private final NodeCommandBuilder nodeCommandBuilder; @@ -87,7 +86,7 @@ private enum Status { private Status status = Status.NOT_STARTED; private final RulesBundles rulesBundles; private final NodeDeprecationWarning deprecationWarning; - private final Path deployLocation; + private final Path temporaryDeployLocation; private final Monitoring monitoring; private final EmbeddedNode embeddedNode; private static final int HEARTBEAT_INTERVAL_SECONDS = 5; @@ -134,7 +133,7 @@ public BridgeServerImpl( this.rulesBundles = rulesBundles; this.deprecationWarning = deprecationWarning; this.hostAddress = InetAddress.getLoopbackAddress().getHostAddress(); - this.deployLocation = tempFolder.newDir(DEPLOY_LOCATION).toPath(); + this.temporaryDeployLocation = tempFolder.newDir(BRIDGE_DEPLOY_LOCATION).toPath(); this.monitoring = monitoring; this.heartbeatService = Executors.newSingleThreadScheduledExecutor(); this.embeddedNode = embeddedNode; @@ -163,9 +162,14 @@ int getTimeoutSeconds() { return timeoutSeconds; } + /** + * Extracts the bridge files and node.js runtime (if included) + * + * @throws IOException + */ void deploy() throws IOException { - bundle.deploy(deployLocation); - embeddedNode.deployNode(deployLocation); + bundle.deploy(temporaryDeployLocation); + embeddedNode.deploy(); } void startServer(SensorContext context, List deployedBundles) throws IOException { @@ -292,7 +296,7 @@ public void startServerLazily(SensorContext context) throws IOException { throw new ServerAlreadyFailedException(); } deploy(); - List deployedBundles = rulesBundles.deploy(deployLocation.resolve("package")); + List deployedBundles = rulesBundles.deploy(temporaryDeployLocation.resolve("package")); rulesBundles .getUcfgRulesBundle() .ifPresent(rulesBundle -> PluginInfo.setUcfgPluginVersion(rulesBundle.bundleVersion())); diff --git a/sonar-plugin/sonar-javascript-plugin/src/main/java/org/sonar/plugins/javascript/bridge/EmbeddedNode.java b/sonar-plugin/sonar-javascript-plugin/src/main/java/org/sonar/plugins/javascript/bridge/EmbeddedNode.java index ce7bfd6dd8e..cb20a502abe 100644 --- a/sonar-plugin/sonar-javascript-plugin/src/main/java/org/sonar/plugins/javascript/bridge/EmbeddedNode.java +++ b/sonar-plugin/sonar-javascript-plugin/src/main/java/org/sonar/plugins/javascript/bridge/EmbeddedNode.java @@ -19,13 +19,17 @@ */ package org.sonar.plugins.javascript.bridge; +import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; import static java.nio.file.attribute.PosixFilePermission.OWNER_EXECUTE; import static java.nio.file.attribute.PosixFilePermission.OWNER_READ; +import static java.nio.file.attribute.PosixFilePermission.OWNER_WRITE; import static org.sonar.plugins.javascript.bridge.EmbeddedNode.Platform.UNSUPPORTED; import static org.sonarsource.api.sonarlint.SonarLintSide.INSTANCE; import java.io.BufferedInputStream; import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.Locale; @@ -36,12 +40,20 @@ import org.sonarsource.api.sonarlint.SonarLintSide; import org.tukaani.xz.XZInputStream; +/** + * Class handling the extraction of the embedded Node.JS runtime + */ @ScannerSide @SonarLintSide(lifespan = INSTANCE) public class EmbeddedNode { + private static final String DEPLOY_LOCATION = Path.of(".sonar", "js", "node-runtime").toString(); + public static final String VERSION_FILENAME = "version.txt"; private static final Logger LOG = Loggers.get(EmbeddedNode.class); private Path deployLocation; + private final Platform platform; + private boolean isAvailable; + private Environment env; enum Platform { WIN_X64, @@ -49,19 +61,33 @@ enum Platform { DARWIN_ARM64, UNSUPPORTED; - String pathInJar() { + private String pathInJar() { switch (this) { case WIN_X64: - return "/win-x64/node.exe.xz"; + return "/win-x64/"; case LINUX_X64: - return "/linux-x64/node.xz"; + return "/linux-x64/"; case DARWIN_ARM64: - return "/darwin-arm64/node.xz"; + return "/darwin-arm64/"; default: return ""; } } + /** + * @return the path of the node compressed node runtime in the JAR + */ + String archivePathInJar() { + return pathInJar() + binary() + ".xz"; + } + + /** + * @return the path of the file storing the version of the node runtime in the JAR + */ + String versionPathInJar() { + return pathInJar() + VERSION_FILENAME; + } + /** * @return the correct binary name depending on the platform: `node` or `node.exe` */ @@ -73,61 +99,99 @@ String binary() { } } - static Platform detect() { - var osName = System.getProperty("os.name"); + /** + * @return The platform where this code is running + */ + static Platform detect(Environment env) { + var osName = env.getOsName(); var lowerCaseOsName = osName.toLowerCase(Locale.ROOT); - if (osName.contains("Windows") && isX64()) { + if (osName.contains("Windows") && isX64(env)) { return WIN_X64; - } else if (lowerCaseOsName.contains("linux") && isX64()) { + } else if (lowerCaseOsName.contains("linux") && isX64(env)) { return LINUX_X64; - } else if (lowerCaseOsName.contains("mac os") && (isARM64())) { + } else if (lowerCaseOsName.contains("mac os") && isARM64(env)) { return DARWIN_ARM64; } return UNSUPPORTED; } - static boolean isX64() { - var arch = System.getProperty("os.arch"); - return arch.contains("amd64"); + private static boolean isX64(Environment env) { + return env.getOsArch().contains("amd64"); } - static boolean isARM64() { - var arch = System.getProperty("os.arch"); - return arch.contains("aarch64"); + private static boolean isARM64(Environment env) { + return env.getOsArch().contains("aarch64"); } } - private final Platform platform = Platform.detect(); + public EmbeddedNode(Environment env) { + this.platform = Platform.detect(env); + this.deployLocation = getPluginCache(env.getUserHome()); + this.env = env; + } - private boolean isAvailable; + /** + * @return a path to `DEPLOY_LOCATION` from the given `baseDir` + */ + private static Path getPluginCache(String baseDir) { + return Path.of(baseDir).resolve(DEPLOY_LOCATION); + } public boolean isAvailable() { return platform != UNSUPPORTED && isAvailable; } - public void deployNode(Path deployLocation) throws IOException { - LOG.debug( - "Detected os: {} arch: {} platform: {}", - System.getProperty("os.name"), - System.getProperty("os.arch"), - platform - ); + /** + * Extracts the node runtime from the JAR to the given `deployLocation`. + * Skips the operation if the platform is unsupported, already extracted or missing from the JAR (legacy). + * + * @throws IOException + */ + public void deploy() throws IOException { + LOG.debug("Detected os: {} arch: {} platform: {}", env.getOsName(), env.getOsArch(), platform); if (platform == UNSUPPORTED || isAvailable) { return; } - this.deployLocation = deployLocation; - var is = getClass().getResourceAsStream(platform.pathInJar()); + var is = getClass().getResourceAsStream(platform.archivePathInJar()); if (is == null) { - LOG.debug("Embedded node not found for platform {}", platform.pathInJar()); + LOG.debug("Embedded node not found for platform {}", platform.archivePathInJar()); return; } - var target = deployLocation.resolve(platform.binary() + ".xz"); - LOG.debug("Copy embedded node to {}", target); - Files.copy(is, target); - extract(target); + + var targetArchive = deployLocation.resolve(platform.binary() + ".xz"); + var targetDirectory = targetArchive.getParent(); + var targetVersion = targetDirectory.resolve(VERSION_FILENAME); + // we assume that since the archive exists, the version file must as well + var versionIs = getClass().getResourceAsStream(platform.versionPathInJar()); + + if (!Files.exists(targetVersion) || isDifferent(versionIs, targetVersion)) { + LOG.debug("Copy embedded node to {}", targetArchive); + Files.createDirectories(targetDirectory); + Files.copy(is, targetArchive, REPLACE_EXISTING); + extract(targetArchive); + Files.copy(versionIs, deployLocation.resolve(VERSION_FILENAME), REPLACE_EXISTING); + } else { + LOG.debug("Skipping node deploy. Deployed node has latest version."); + } + isAvailable = true; } + private static boolean isDifferent(InputStream newVersionIs, Path currentVersionPath) + throws IOException { + var newVersionString = new String(newVersionIs.readAllBytes(), StandardCharsets.UTF_8); + var currentVersionString = Files.readString(currentVersionPath); + LOG.debug( + "Currently installed Node.JS version: " + + currentVersionString + + " at " + + currentVersionPath + + ". Available version in analyzer: " + + newVersionString + ); + return !newVersionString.equals(currentVersionString); + } + /** * Expects a path to a xz-compressed file ending in `.xz` like `node.xz` and * extracts it into the same place as `node`. @@ -140,11 +204,6 @@ public void deployNode(Path deployLocation) throws IOException { private void extract(Path source) throws IOException { var sourceAsString = source.toString(); var target = Path.of(sourceAsString.substring(0, sourceAsString.length() - 3)); - if (Files.exists(target)) { - // TODO drop this skip if it prevents us from upgrading the runtime - LOG.debug("Skipping decompression. " + target.toString() + " already exists."); - return; - } LOG.debug("Decompressing " + source.toAbsolutePath() + " into " + target); try ( var is = Files.newInputStream(source); @@ -158,7 +217,7 @@ private void extract(Path source) throws IOException { os.write(buf, 0, nextBytes); } if (platform != Platform.WIN_X64) { - Files.setPosixFilePermissions(target, Set.of(OWNER_EXECUTE, OWNER_READ)); + Files.setPosixFilePermissions(target, Set.of(OWNER_EXECUTE, OWNER_READ, OWNER_WRITE)); } } } diff --git a/sonar-plugin/sonar-javascript-plugin/src/main/java/org/sonar/plugins/javascript/bridge/Environment.java b/sonar-plugin/sonar-javascript-plugin/src/main/java/org/sonar/plugins/javascript/bridge/Environment.java new file mode 100644 index 00000000000..ebbc0d5c8a6 --- /dev/null +++ b/sonar-plugin/sonar-javascript-plugin/src/main/java/org/sonar/plugins/javascript/bridge/Environment.java @@ -0,0 +1,27 @@ +package org.sonar.plugins.javascript.bridge; + +import static org.sonarsource.api.sonarlint.SonarLintSide.INSTANCE; + +import org.sonar.api.scanner.ScannerSide; +import org.sonarsource.api.sonarlint.SonarLintSide; + +/** + * Class to access host parameters. + * This abstraction is necessary to mock it in tests. + */ +@ScannerSide +@SonarLintSide(lifespan = INSTANCE) +public class Environment { + + public String getUserHome() { + return System.getProperty("user.home"); + } + + public String getOsName() { + return System.getProperty("os.name"); + } + + public String getOsArch() { + return System.getProperty("os.arch"); + } +} diff --git a/sonar-plugin/sonar-javascript-plugin/src/main/java/org/sonar/plugins/javascript/nodejs/NodeCommandBuilderImpl.java b/sonar-plugin/sonar-javascript-plugin/src/main/java/org/sonar/plugins/javascript/nodejs/NodeCommandBuilderImpl.java index eddf482d360..359b29b4d7a 100644 --- a/sonar-plugin/sonar-javascript-plugin/src/main/java/org/sonar/plugins/javascript/nodejs/NodeCommandBuilderImpl.java +++ b/sonar-plugin/sonar-javascript-plugin/src/main/java/org/sonar/plugins/javascript/nodejs/NodeCommandBuilderImpl.java @@ -42,6 +42,7 @@ import org.sonar.api.utils.log.Logger; import org.sonar.api.utils.log.Loggers; import org.sonar.plugins.javascript.bridge.EmbeddedNode; +import org.sonar.plugins.javascript.bridge.Environment; public class NodeCommandBuilderImpl implements NodeCommandBuilder { @@ -59,7 +60,7 @@ public class NodeCommandBuilderImpl implements NodeCommandBuilder { ); private final ProcessWrapper processWrapper; - private EmbeddedNode embeddedNode = new EmbeddedNode(); + private EmbeddedNode embeddedNode = new EmbeddedNode(new Environment()); private Version minNodeVersion; private Configuration configuration; private List args = new ArrayList<>(); diff --git a/sonar-plugin/sonar-javascript-plugin/src/test/java/org/sonar/plugins/javascript/JavaScriptPluginTest.java b/sonar-plugin/sonar-javascript-plugin/src/test/java/org/sonar/plugins/javascript/JavaScriptPluginTest.java index 3bf63647dca..51727f17e9b 100644 --- a/sonar-plugin/sonar-javascript-plugin/src/test/java/org/sonar/plugins/javascript/JavaScriptPluginTest.java +++ b/sonar-plugin/sonar-javascript-plugin/src/test/java/org/sonar/plugins/javascript/JavaScriptPluginTest.java @@ -40,7 +40,7 @@ class JavaScriptPluginTest { - private static final int BASE_EXTENSIONS = 35; + private static final int BASE_EXTENSIONS = 36; private static final int JS_ADDITIONAL_EXTENSIONS = 4; private static final int TS_ADDITIONAL_EXTENSIONS = 3; private static final int CSS_ADDITIONAL_EXTENSIONS = 3; diff --git a/sonar-plugin/sonar-javascript-plugin/src/test/java/org/sonar/plugins/javascript/bridge/BridgeServerImplTest.java b/sonar-plugin/sonar-javascript-plugin/src/test/java/org/sonar/plugins/javascript/bridge/BridgeServerImplTest.java index ed8e3dc970d..b8ff074fba7 100644 --- a/sonar-plugin/sonar-javascript-plugin/src/test/java/org/sonar/plugins/javascript/bridge/BridgeServerImplTest.java +++ b/sonar-plugin/sonar-javascript-plugin/src/test/java/org/sonar/plugins/javascript/bridge/BridgeServerImplTest.java @@ -112,7 +112,7 @@ public void setUp() throws Exception { } @AfterEach - public void tearDown() throws Exception { + public void tearDown() { try { if (bridgeServer != null) { bridgeServer.clean(); @@ -156,7 +156,7 @@ void should_throw_if_failed_to_build_node_command() throws Exception { deprecationWarning, tempFolder, monitoring, - new EmbeddedNode() + new EmbeddedNode(createMockEnvironment()) ); bridgeServer.deploy(); List deployedBundles = emptyList(); @@ -660,7 +660,7 @@ void should_use_default_timeout() { deprecationWarning, tempFolder, monitoring, - new EmbeddedNode() + new EmbeddedNode(createMockEnvironment()) ); assertThat(bridgeServer.getTimeoutSeconds()).isEqualTo(300); } @@ -723,7 +723,7 @@ public void execute(SensorContext context) {} deprecationWarning, tempFolder, monitoring, - new EmbeddedNode() + new EmbeddedNode(createMockEnvironment()) ); bridgeServer.deploy(); bridgeServer.startServerLazily(context); @@ -755,7 +755,7 @@ void test_ucfg_bundle_version() throws Exception { deprecationWarning, tempFolder, monitoring, - new EmbeddedNode() + new EmbeddedNode(createMockEnvironment()) ); bridgeServer.startServerLazily(context); @@ -772,10 +772,18 @@ private BridgeServerImpl createBridgeServer(String startServerScript) { deprecationWarning, tempFolder, monitoring, - new EmbeddedNode() + new EmbeddedNode(createMockEnvironment()) ); } + private Environment createMockEnvironment() { + Environment mockEnvironment = mock(Environment.class); + when(mockEnvironment.getUserHome()).thenReturn(""); + when(mockEnvironment.getOsName()).thenReturn(""); + when(mockEnvironment.getOsArch()).thenReturn(""); + return mockEnvironment; + } + static class TestBundle implements Bundle { final String startServerScript; diff --git a/sonar-plugin/sonar-javascript-plugin/src/test/java/org/sonar/plugins/javascript/bridge/EmbeddedNodeTest.java b/sonar-plugin/sonar-javascript-plugin/src/test/java/org/sonar/plugins/javascript/bridge/EmbeddedNodeTest.java index 2e6418d37be..353282b398c 100644 --- a/sonar-plugin/sonar-javascript-plugin/src/test/java/org/sonar/plugins/javascript/bridge/EmbeddedNodeTest.java +++ b/sonar-plugin/sonar-javascript-plugin/src/test/java/org/sonar/plugins/javascript/bridge/EmbeddedNodeTest.java @@ -1,15 +1,111 @@ package org.sonar.plugins.javascript.bridge; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.api.io.TempDir; +import org.slf4j.event.Level; +import org.sonar.api.testfixtures.log.LogTesterJUnit5; class EmbeddedNodeTest { + @TempDir + Path tempDir; + + @RegisterExtension + public LogTesterJUnit5 logTester = new LogTesterJUnit5().setLevel(Level.DEBUG); + + private Environment currentEnvironment = new Environment(); + + @Test + void should_extract_if_deployLocation_contains_a_different_version() throws Exception { + var en = new EmbeddedNode(createTestEnvironment()); + var runtimeFolder = en.binary().getParent(); + Files.createDirectories(runtimeFolder); + Files.write(runtimeFolder.resolve("version.txt"), "a-different-version".getBytes()); + en.deploy(); + assertThat(en.binary()).exists(); + } + + @Test + void should_not_extract_if_deployLocation_contains_the_same_version() throws Exception { + var en = new EmbeddedNode(createTestEnvironment()); + var runtimeFolder = en.binary().getParent(); + Files.createDirectories(runtimeFolder); + Files.write( + runtimeFolder.resolve("version.txt"), + extractCurrentVersion(createTestEnvironment()) + ); + en.deploy(); + assertThat(en.binary()).doesNotExist(); + } + + @Test + void should_extract_if_deployLocation_has_no_version() throws Exception { + var en = new EmbeddedNode(createTestEnvironment()); + en.deploy(); + assertThat(tempDir.resolve(en.binary())).exists(); + } + + @Test + void should_detect_platform_for_windows_environment() { + var platform = EmbeddedNode.Platform.detect(createWindowsEnvironment()); + assertThat(platform).isEqualTo(EmbeddedNode.Platform.WIN_X64); + assertThat(platform.archivePathInJar()).isEqualTo("/win-x64/node.exe.xz"); + } + + @Test + void should_detect_platform_for_mac_os_environment() { + var platform = EmbeddedNode.Platform.detect(createMacOSEnvironment()); + assertThat(platform).isEqualTo(EmbeddedNode.Platform.DARWIN_ARM64); + assertThat(platform.archivePathInJar()).isEqualTo("/darwin-arm64/node.xz"); + } + @Test - void should_detect_platform_macos() { - if (System.getProperty("os.name").startsWith("Mac")) { - assertThat(EmbeddedNode.Platform.detect()).isEqualTo(EmbeddedNode.Platform.DARWIN_ARM64); - } + void should_return_unsupported_for_unknown_environment() { + var platform = EmbeddedNode.Platform.detect(createUnsupportedEnvironment()); + assertThat(platform).isEqualTo(EmbeddedNode.Platform.UNSUPPORTED); + assertThat(platform.archivePathInJar()).isEqualTo("node.xz"); + } + + private byte[] extractCurrentVersion(Environment env) throws IOException { + return getClass() + .getResourceAsStream(EmbeddedNode.Platform.detect(env).versionPathInJar()) + .readAllBytes(); + } + + private Environment createTestEnvironment() { + Environment mockEnvironment = mock(Environment.class); + when(mockEnvironment.getUserHome()).thenReturn(tempDir.toString()); + when(mockEnvironment.getOsName()).thenReturn(currentEnvironment.getOsName()); + when(mockEnvironment.getOsArch()).thenReturn(currentEnvironment.getOsArch()); + return mockEnvironment; + } + + private Environment createMacOSEnvironment() { + Environment mockEnvironment = mock(Environment.class); + when(mockEnvironment.getOsName()).thenReturn("mac os x"); + when(mockEnvironment.getOsArch()).thenReturn("aarch64"); + return mockEnvironment; + } + + private Environment createWindowsEnvironment() { + Environment mockEnvironment = mock(Environment.class); + when(mockEnvironment.getOsName()).thenReturn("Windows 99"); + when(mockEnvironment.getOsArch()).thenReturn("amd64"); + return mockEnvironment; + } + + private Environment createUnsupportedEnvironment() { + Environment mockEnvironment = mock(Environment.class); + when(mockEnvironment.getOsName()).thenReturn(""); + when(mockEnvironment.getOsArch()).thenReturn(""); + return mockEnvironment; } } diff --git a/sonar-plugin/sonar-javascript-plugin/src/test/java/org/sonar/plugins/javascript/nodejs/NodeCommandTest.java b/sonar-plugin/sonar-javascript-plugin/src/test/java/org/sonar/plugins/javascript/nodejs/NodeCommandTest.java index 42d52f2a892..97024685978 100644 --- a/sonar-plugin/sonar-javascript-plugin/src/test/java/org/sonar/plugins/javascript/nodejs/NodeCommandTest.java +++ b/sonar-plugin/sonar-javascript-plugin/src/test/java/org/sonar/plugins/javascript/nodejs/NodeCommandTest.java @@ -53,6 +53,7 @@ import org.sonar.api.utils.Version; import org.sonar.api.utils.log.LoggerLevel; import org.sonar.plugins.javascript.bridge.EmbeddedNode; +import org.sonar.plugins.javascript.bridge.Environment; class NodeCommandTest { @@ -406,17 +407,16 @@ void test_windows_default_node_not_found() throws Exception { @Test void test_embedded_runtime() throws Exception { - var en = new EmbeddedNode(); - en.deployNode(tempDir); + var en = new EmbeddedNode(createTestEnvironment()); + en.deploy(); NodeCommand nodeCommand = NodeCommand .builder() .script(PATH_TO_SCRIPT) .pathResolver(getPathResolver()) .embeddedNode(en) .build(); - // TODO for some reason, using mockProcessWrapper to test for the used command does not yield the expected result - var expectedCommand = - Paths.get(tempDir.toString(), en.binary().getFileName().toString()) + " " + PATH_TO_SCRIPT; + // For some reason, using mockProcessWrapper to test for the used command does not yield the expected result + var expectedCommand = Paths.get(en.binary().toString()) + " " + PATH_TO_SCRIPT; assertThat(nodeCommand.toString()).isEqualTo(expectedCommand); } @@ -431,8 +431,8 @@ void test_embedded_runtime_with_forceHost_for_macos() throws Exception { mapSettings.setProperty(NODE_FORCE_HOST_PROPERTY, true); Configuration configuration = mapSettings.asConfig(); - var en = new EmbeddedNode(); - en.deployNode(tempDir); + var en = new EmbeddedNode(createTestEnvironment()); + en.deploy(); NodeCommand nodeCommand = NodeCommand .builder() .script(PATH_TO_SCRIPT) @@ -453,4 +453,12 @@ private static BundlePathResolver getPathResolver() { File file = new File("src/test/resources"); return p -> new File(file.getAbsoluteFile(), p).getAbsolutePath(); } + + private Environment createTestEnvironment() { + Environment mockEnvironment = mock(Environment.class); + when(mockEnvironment.getUserHome()).thenReturn(tempDir.toString()); + when(mockEnvironment.getOsName()).thenReturn(new Environment().getOsName()); + when(mockEnvironment.getOsArch()).thenReturn(new Environment().getOsArch()); + return mockEnvironment; + } } diff --git a/tools/fetch-node/node-distros.mjs b/tools/fetch-node/node-distros.mjs index ea9c66b548b..a94fd05f6c0 100644 --- a/tools/fetch-node/node-distros.mjs +++ b/tools/fetch-node/node-distros.mjs @@ -1,4 +1,4 @@ -const NODE_VERSION = 'v20.5.1'; +export const NODE_VERSION = 'v20.5.1'; const NODE_ORG_URL = `https://nodejs.org/dist/${NODE_VERSION}/node-${NODE_VERSION}`; const NODE_ARTIFACTORY_URL = `https://repox.jfrog.io/artifactory/nodejs-dist/${NODE_VERSION}/node-${NODE_VERSION}`; @@ -11,7 +11,7 @@ const NODE_ARTIFACTORY_URL = `https://repox.jfrog.io/artifactory/nodejs-dist/${N * - `sonar-plugin/sonar-javascript-plugin/src/main/java/org/sonar/plugins/javascript/bridge/EmbeddedNode.java` * - `sonar-plugin/sonar-javascript-plugin/pom.xml` */ -export default [ +export const DISTROS = [ { id: 'win-x64', url: `${NODE_ORG_URL}-win-x64.zip`, @@ -31,3 +31,5 @@ export default [ sha: 'a8678ae00425acdf692e943e3f1cea11a4c46281e4257b82886423bd4ef6f2b5', }, ]; + +export const VERSION_FILENAME = 'version.txt'; diff --git a/tools/fetch-node/scripts/copy-to-plugin.mjs b/tools/fetch-node/scripts/copy-to-plugin.mjs index 4d26ded9e6f..462bd9f3554 100644 --- a/tools/fetch-node/scripts/copy-to-plugin.mjs +++ b/tools/fetch-node/scripts/copy-to-plugin.mjs @@ -21,7 +21,7 @@ import fse from 'fs-extra'; import * as path from 'node:path'; import * as fs from 'node:fs'; import { RUNTIMES_DIR, TARGET_DIR } from './directories.mjs'; -import NODE_DISTROS from '../node-distros.mjs'; +import { DISTROS, NODE_VERSION, VERSION_FILENAME } from '../node-distros.mjs'; /** * Copies tools/fetch-node/downloads/runtimes @@ -29,7 +29,7 @@ import NODE_DISTROS from '../node-distros.mjs'; * sonar-plugin/sonar-javascript-plugin/target/node */ -for (const distro of NODE_DISTROS) { +for (const distro of DISTROS) { const sourceDir = path.join(RUNTIMES_DIR, distro.id); const filename = fs.readdirSync(sourceDir).filter(filename => filename.endsWith('.xz'))[0]; const targetDir = path.join(TARGET_DIR, distro.id); @@ -38,4 +38,5 @@ for (const distro of NODE_DISTROS) { const targetFilename = path.join(targetDir, filename); console.log(`Copying ${sourceFilename} to ${targetFilename}`); fse.copySync(sourceFilename, targetFilename); + fs.writeFileSync(path.join(targetDir, VERSION_FILENAME), NODE_VERSION); } diff --git a/tools/fetch-node/scripts/fetch-node.mjs b/tools/fetch-node/scripts/fetch-node.mjs index a367442aef3..4d90f196abd 100644 --- a/tools/fetch-node/scripts/fetch-node.mjs +++ b/tools/fetch-node/scripts/fetch-node.mjs @@ -26,7 +26,7 @@ import * as path from 'node:path'; import * as stream from 'node:stream'; import * as crypto from 'node:crypto'; import * as os from 'node:os'; -import NODE_DISTROS from '../node-distros.mjs'; +import { DISTROS } from '../node-distros.mjs'; import { DOWNLOAD_DIR, RUNTIMES_DIR } from './directories.mjs'; /** @@ -34,7 +34,7 @@ import { DOWNLOAD_DIR, RUNTIMES_DIR } from './directories.mjs'; * downloads/runtimes/{distro.id}/node{.exe} */ -for (const distro of NODE_DISTROS) { +for (const distro of DISTROS) { const filename = getFilenameFromUrl(distro.url); const archiveFilename = path.join(DOWNLOAD_DIR, filename); await downloadRuntime(distro, archiveFilename);