diff --git a/launcher-package/integration-test/src/test/scala/BspConfigTest.scala b/launcher-package/integration-test/src/test/scala/BspConfigTest.scala index 70c179fdf..50b5bad0a 100644 --- a/launcher-package/integration-test/src/test/scala/BspConfigTest.scala +++ b/launcher-package/integration-test/src/test/scala/BspConfigTest.scala @@ -1,84 +1,84 @@ -package example.test - -import scala.sys.process.* -import java.io.File -import java.util.Locale -import sbt.io.IO -import verify.BasicTestSuite - -// Test for issues #7792/#7794: BSP config generation and argv execution -object BspConfigTest extends BasicTestSuite: - lazy val isWindows: Boolean = - sys.props("os.name").toLowerCase(Locale.ENGLISH).contains("windows") - lazy val sbtScript = IntegrationTestPaths.sbtScript(isWindows) - - private def launcherCmd = LauncherTestHelper.launcherCommand(sbtScript.getAbsolutePath) - - def sbtProcessInDir(dir: File)(args: String*) = - Process( - launcherCmd ++ args, - dir, - "JAVA_OPTS" -> "", - "SBT_OPTS" -> "" - ) - - test("sbt bspConfig") { - import ujson.* - - IO.withTemporaryDirectory { tmp => - // Create minimal build.sbt for the test project - IO.write(new File(tmp, "build.sbt"), """name := "test-bsp-config"""") - - // Run bspConfig to generate .bsp/sbt.json - val configResult = sbtProcessInDir(tmp)("bspConfig", "--batch").! - assert(configResult == 0, s"bspConfig command failed with exit code $configResult") - - // Verify .bsp/sbt.json exists - val bspFile = new File(tmp, ".bsp/sbt.json") - assert(bspFile.exists, ".bsp/sbt.json should exist after running bspConfig") - - // Parse and verify JSON content - val content = IO.read(bspFile) - val json = ujson.read(content) - - // Extract argv array from JSON - val argvValue = json.obj.get("argv") - assert(argvValue.isDefined, "argv field not found in sbt.json") - - val argv = argvValue.get.arr.map(_.str).toVector - - // Verify argv structure - assert(argv.nonEmpty, "argv should not be empty") - assert(argv.head.contains("java"), s"argv should start with java command, got: ${argv.head}") - assert(argv.contains("-bsp"), s"argv should contain -bsp flag, got: $argv") - - // Test execution of the generated argv - // Run the BSP command with a very short timeout to verify it starts correctly - // We just need to verify the command doesn't fail immediately on startup - if (!isWindows) { - // On Unix, we can test the argv execution - // Create a process and check if it starts (will timeout waiting for BSP input) - val process = Process(argv.toSeq, tmp) - val processBuilder = process.run(ProcessLogger(_ => (), _ => ())) - - // Give it a moment to fail if it's going to fail immediately - Thread.sleep(500) - - // If still running, it means the BSP server started successfully - val isAlive = processBuilder.isAlive() - processBuilder.destroy() - - // The process should either still be alive (waiting for BSP messages) - // or have exited with code 0 (graceful) - if (!isAlive) { - val exitCode = processBuilder.exitValue() - assert( - exitCode == 0 || exitCode == 143, // 143 = SIGTERM from destroy() - s"BSP process failed with exit code $exitCode" - ) - } - } - } - () - } -end BspConfigTest +package example.test + +import scala.sys.process.* +import java.io.File +import java.util.Locale +import sbt.io.IO +import verify.BasicTestSuite + +// Test for issues #7792/#7794: BSP config generation and argv execution +object BspConfigTest extends BasicTestSuite: + lazy val isWindows: Boolean = + sys.props("os.name").toLowerCase(Locale.ENGLISH).contains("windows") + lazy val sbtScript = IntegrationTestPaths.sbtScript(isWindows) + + private def launcherCmd = LauncherTestHelper.launcherCommand(sbtScript.getAbsolutePath) + + def sbtProcessInDir(dir: File)(args: String*) = + Process( + launcherCmd ++ args, + dir, + "JAVA_OPTS" -> "", + "SBT_OPTS" -> "" + ) + + test("sbt bspConfig") { + import ujson.* + + IO.withTemporaryDirectory { tmp => + // Create minimal build.sbt for the test project + IO.write(new File(tmp, "build.sbt"), """name := "test-bsp-config"""") + + // Run bspConfig to generate .bsp/sbt.json + val configResult = sbtProcessInDir(tmp)("bspConfig", "--batch").! + assert(configResult == 0, s"bspConfig command failed with exit code $configResult") + + // Verify .bsp/sbt.json exists + val bspFile = new File(tmp, ".bsp/sbt.json") + assert(bspFile.exists, ".bsp/sbt.json should exist after running bspConfig") + + // Parse and verify JSON content + val content = IO.read(bspFile) + val json = ujson.read(content) + + // Extract argv array from JSON + val argvValue = json.obj.get("argv") + assert(argvValue.isDefined, "argv field not found in sbt.json") + + val argv = argvValue.get.arr.map(_.str).toVector + + // Verify argv structure + assert(argv.nonEmpty, "argv should not be empty") + assert(argv.head.contains("java"), s"argv should start with java command, got: ${argv.head}") + assert(argv.contains("-bsp"), s"argv should contain -bsp flag, got: $argv") + + // Test execution of the generated argv + // Run the BSP command with a very short timeout to verify it starts correctly + // We just need to verify the command doesn't fail immediately on startup + if (!isWindows) { + // On Unix, we can test the argv execution + // Create a process and check if it starts (will timeout waiting for BSP input) + val process = Process(argv.toSeq, tmp) + val processBuilder = process.run(ProcessLogger(_ => (), _ => ())) + + // Give it a moment to fail if it's going to fail immediately + Thread.sleep(500) + + // If still running, it means the BSP server started successfully + val isAlive = processBuilder.isAlive() + processBuilder.destroy() + + // The process should either still be alive (waiting for BSP messages) + // or have exited with code 0 (graceful) + if (!isAlive) { + val exitCode = processBuilder.exitValue() + assert( + exitCode == 0 || exitCode == 143, // 143 = SIGTERM from destroy() + s"BSP process failed with exit code $exitCode" + ) + } + } + } + () + } +end BspConfigTest diff --git a/launcher-package/integration-test/src/test/scala/ExtendedRunnerTest.scala b/launcher-package/integration-test/src/test/scala/ExtendedRunnerTest.scala index 2929df95f..bf579a3bd 100755 --- a/launcher-package/integration-test/src/test/scala/ExtendedRunnerTest.scala +++ b/launcher-package/integration-test/src/test/scala/ExtendedRunnerTest.scala @@ -1,237 +1,237 @@ -package example.test - -import scala.sys.process.* -import java.io.File -import java.util.Locale -import sbt.io.IO -import verify.BasicTestSuite - -object ExtendedRunnerTest extends BasicTestSuite: - // 1.3.0, 1.3.0-M4 - private[test] val versionRegEx = "\\d(\\.\\d+){2}(-\\w+)?" - - lazy val isWindows: Boolean = sys.props("os.name").toLowerCase(Locale.ENGLISH).contains("windows") - lazy val isMac: Boolean = sys.props("os.name").toLowerCase(Locale.ENGLISH).contains("mac") - lazy val sbtScript = IntegrationTestPaths.sbtScript(isWindows) - - private def launcherCmd = LauncherTestHelper.launcherCommand(sbtScript.getAbsolutePath) - - def sbtProcess(args: String*) = sbtProcessWithOpts(args*)("", "") - def sbtProcessWithOpts(args: String*)(javaOpts: String, sbtOpts: String) = - Process( - launcherCmd ++ args, - IntegrationTestPaths.citestDir("citest"), - "JAVA_OPTS" -> javaOpts, - "SBT_OPTS" -> sbtOpts - ) - def sbtProcessInDir(dir: File)(args: String*) = - Process( - launcherCmd ++ args, - dir, - "JAVA_OPTS" -> "", - "SBT_OPTS" -> "" - ) - - test("sbt runs") { - assert(sbtScript.exists) - val out = sbtProcess("compile", "-v").! - assert(out == 0) - () - } - - def testVersion(lines: List[String]): Unit = { - assert(lines.size >= 2) - val expected0 = s"(?m)^sbt version in this project: $versionRegEx(\\r)?" - assert(lines(0).matches(expected0)) - val expected1 = s"sbt runner version: $versionRegEx$$" - assert(lines(1).matches(expected1)) - } - - /* TODO: The lines seems to return List([0Jsbt runner version: 1.11.4) on CI - test("sbt -V|-version|--version should print sbtVersion") { - val out = sbtProcess("-version").!!.trim - testVersion(out.linesIterator.toList) - - val out2 = sbtProcess("--version").!!.trim - testVersion(out2.linesIterator.toList) - - val out3 = sbtProcess("-V").!!.trim - testVersion(out3.linesIterator.toList) - } - */ - - test("sbt -V in empty directory") { - IO.withTemporaryDirectory { tmp => - val out = sbtProcessInDir(tmp)("-V").!!.trim - val expectedVersion = "^" + versionRegEx + "$" - val targetDir = new File(tmp, "target") - assert(!targetDir.exists, "expected target directory to not exist, but existed") - } - () - } - - /* TODO: Not sure why but the output is returning [0J on CI - test("sbt --numeric-version should print sbt script version") { - val out = sbtProcess("--numeric-version").!!.trim - val expectedVersion = "^"+versionRegEx+"$" - assert(out.matches(expectedVersion)) - () - } - */ - - test("sbt --sbt-jar should run") { - val out = sbtProcess( - "compile", - "-v", - "--sbt-jar", - "../target/universal/stage/bin/sbt-launch.jar" - ).!!.linesIterator.toList - assert( - out.contains[String]("../target/universal/stage/bin/sbt-launch.jar") || - out.contains[String]("\"../target/universal/stage/bin/sbt-launch.jar\"") - ) - () - } - - test("sbt \"testOnly *\"") { - if (isMac) () - else { - val out = sbtProcess("testOnly *", "--no-colors", "-v").!!.linesIterator.toList - assert(out.contains[String]("[info] HelloTest")) - () - } - } - - test("sbt in empty directory") { - IO.withTemporaryDirectory { tmp => - val out = sbtProcessInDir(tmp)("about").! - assert(out == 1) - } - IO.withTemporaryDirectory { tmp => - val out = sbtProcessInDir(tmp)("about", "--allow-empty").! - assert(out == 0) - } - () - } - - test("sbt --script-version in empty directory") { - IO.withTemporaryDirectory { tmp => - val out = sbtProcessInDir(tmp)("--script-version").!!.trim - val expectedVersion = "^" + versionRegEx + "$" - assert(out.matches(expectedVersion)) - } - () - } - - test("sbt --jvm-client") { - val out = sbtProcess("--jvm-client", "--no-colors", "compile").!!.linesIterator.toList - if (isWindows) { - println(out) - } else { - assert(out.exists { _.contains("server was not detected") }) - } - val out2 = sbtProcess("--jvm-client", "--no-colors", "shutdown").!!.linesIterator.toList - if (isWindows) { - println(out2) - } else { - assert(out2.exists { _.contains("disconnected") }) - } - () - } - - // Test for issue #6485: Test `sbt --client` startup - // https://github.com/sbt/sbt/issues/6485 - test("sbt --client startup time") { - if (isWindows || isMac) { - // Skip on Windows (sbtn behavior differs) and macOS CI (slow hostname resolution) - () - } else { - // First call starts the server if not running (warmup) - val warmup = sbtProcess("--client", "version").! - assert(warmup == 0, "Warmup sbt --client version failed") - - // Measure startup time for sbt --client when server is already running - // Run multiple times and take the average to reduce variance - val iterations = 5 - val times = (1 to iterations).map { _ => - val start = System.nanoTime() - val exitCode = sbtProcess("--client", "version").! - val elapsed = (System.nanoTime() - start) / 1_000_000 // Convert to milliseconds - assert(exitCode == 0, "sbt --client version failed") - elapsed - } - - val avgTime = times.sum / iterations - val maxTime = times.max - - println(s"sbt --client startup times (ms): ${times.mkString(", ")}") - println(s"Average: ${avgTime}ms, Max: ${maxTime}ms") - - // Cap at 2000ms to catch significant regressions while allowing for CI variance. - // The original issue #5980 mentioned ~200ms on developer machines in 2021, - // but CI runners are typically 2-3x slower than local development machines. - assert( - avgTime < 2000, - s"sbt --client startup time (${avgTime}ms average) exceeded 2000ms threshold" - ) - - // Cleanup: shutdown the server - val shutdown = sbtProcess("--client", "shutdown").! - assert(shutdown == 0, "Failed to shutdown sbt server") - } - () - } - - // Test for issue #8644: sbt.bat fails when project path contains parentheses - // https://github.com/sbt/sbt/issues/8644 - test("sbt.bat handles paths with parentheses") { - if (!isWindows) { - // This test is Windows-specific, skip on other platforms - () - } else { - IO.withTemporaryDirectory { baseDir => - // Create a temporary directory with parentheses in the name - val testDir = new File(baseDir, "test(parentheses)") - - // Create the directory structure - IO.createDirectory(testDir) - val projectDir = new File(testDir, "project") - IO.createDirectory(projectDir) - - // Create a minimal build.properties to make it a valid sbt project - val buildProps = new File(projectDir, "build.properties") - IO.write(buildProps, "sbt.version=1.12.1\n") - - // Test 1: Run sbt from directory with parentheses - should work without parsing errors - val out1 = sbtProcessInDir(testDir)("--script-version").!!.trim - val expectedVersion = "^" + versionRegEx + "$" - assert(out1.matches(expectedVersion), s"Expected version format, got: $out1") - - // Test 2: Test error message when no build.sbt exists (this is where the fix is most visible) - // Create a directory with parentheses but no build.sbt - val emptyDir = new File(baseDir, "empty(parentheses)") - IO.createDirectory(emptyDir) - - // Run sbt from empty directory - should fail gracefully with proper error message - // Use ProcessLogger to capture stderr without throwing on non-zero exit - import scala.sys.process.ProcessLogger - val errorBuffer = new StringBuilder - val logger = ProcessLogger( - _ => (), // ignore stdout - line => errorBuffer.append(line).append("\n") // capture stderr - ) - val exitCode = sbtProcessInDir(emptyDir)("compile").!(logger) - assert(exitCode == 1, "Expected sbt to fail when no build.sbt exists") - - // Verify the error output doesn't contain ") was unexpected" parsing error - val errorOutput = errorBuffer.toString - val hasParsingError = errorOutput.contains(") was unexpected") - assert( - !hasParsingError, - s"Error message should not contain parsing error when path has parentheses. Error output: $errorOutput" - ) - } - } - () - } -end ExtendedRunnerTest +package example.test + +import scala.sys.process.* +import java.io.File +import java.util.Locale +import sbt.io.IO +import verify.BasicTestSuite + +object ExtendedRunnerTest extends BasicTestSuite: + // 1.3.0, 1.3.0-M4 + private[test] val versionRegEx = "\\d(\\.\\d+){2}(-\\w+)?" + + lazy val isWindows: Boolean = sys.props("os.name").toLowerCase(Locale.ENGLISH).contains("windows") + lazy val isMac: Boolean = sys.props("os.name").toLowerCase(Locale.ENGLISH).contains("mac") + lazy val sbtScript = IntegrationTestPaths.sbtScript(isWindows) + + private def launcherCmd = LauncherTestHelper.launcherCommand(sbtScript.getAbsolutePath) + + def sbtProcess(args: String*) = sbtProcessWithOpts(args*)("", "") + def sbtProcessWithOpts(args: String*)(javaOpts: String, sbtOpts: String) = + Process( + launcherCmd ++ args, + IntegrationTestPaths.citestDir("citest"), + "JAVA_OPTS" -> javaOpts, + "SBT_OPTS" -> sbtOpts + ) + def sbtProcessInDir(dir: File)(args: String*) = + Process( + launcherCmd ++ args, + dir, + "JAVA_OPTS" -> "", + "SBT_OPTS" -> "" + ) + + test("sbt runs") { + assert(sbtScript.exists) + val out = sbtProcess("compile", "-v").! + assert(out == 0) + () + } + + def testVersion(lines: List[String]): Unit = { + assert(lines.size >= 2) + val expected0 = s"(?m)^sbt version in this project: $versionRegEx(\\r)?" + assert(lines(0).matches(expected0)) + val expected1 = s"sbt runner version: $versionRegEx$$" + assert(lines(1).matches(expected1)) + } + + /* TODO: The lines seems to return List([0Jsbt runner version: 1.11.4) on CI + test("sbt -V|-version|--version should print sbtVersion") { + val out = sbtProcess("-version").!!.trim + testVersion(out.linesIterator.toList) + + val out2 = sbtProcess("--version").!!.trim + testVersion(out2.linesIterator.toList) + + val out3 = sbtProcess("-V").!!.trim + testVersion(out3.linesIterator.toList) + } + */ + + test("sbt -V in empty directory") { + IO.withTemporaryDirectory { tmp => + val out = sbtProcessInDir(tmp)("-V").!!.trim + val expectedVersion = "^" + versionRegEx + "$" + val targetDir = new File(tmp, "target") + assert(!targetDir.exists, "expected target directory to not exist, but existed") + } + () + } + + /* TODO: Not sure why but the output is returning [0J on CI + test("sbt --numeric-version should print sbt script version") { + val out = sbtProcess("--numeric-version").!!.trim + val expectedVersion = "^"+versionRegEx+"$" + assert(out.matches(expectedVersion)) + () + } + */ + + test("sbt --sbt-jar should run") { + val out = sbtProcess( + "compile", + "-v", + "--sbt-jar", + "../target/universal/stage/bin/sbt-launch.jar" + ).!!.linesIterator.toList + assert( + out.contains[String]("../target/universal/stage/bin/sbt-launch.jar") || + out.contains[String]("\"../target/universal/stage/bin/sbt-launch.jar\"") + ) + () + } + + test("sbt \"testOnly *\"") { + if (isMac) () + else { + val out = sbtProcess("testOnly *", "--no-colors", "-v").!!.linesIterator.toList + assert(out.contains[String]("[info] HelloTest")) + () + } + } + + test("sbt in empty directory") { + IO.withTemporaryDirectory { tmp => + val out = sbtProcessInDir(tmp)("about").! + assert(out == 1) + } + IO.withTemporaryDirectory { tmp => + val out = sbtProcessInDir(tmp)("about", "--allow-empty").! + assert(out == 0) + } + () + } + + test("sbt --script-version in empty directory") { + IO.withTemporaryDirectory { tmp => + val out = sbtProcessInDir(tmp)("--script-version").!!.trim + val expectedVersion = "^" + versionRegEx + "$" + assert(out.matches(expectedVersion)) + } + () + } + + test("sbt --jvm-client") { + val out = sbtProcess("--jvm-client", "--no-colors", "compile").!!.linesIterator.toList + if (isWindows) { + println(out) + } else { + assert(out.exists { _.contains("server was not detected") }) + } + val out2 = sbtProcess("--jvm-client", "--no-colors", "shutdown").!!.linesIterator.toList + if (isWindows) { + println(out2) + } else { + assert(out2.exists { _.contains("disconnected") }) + } + () + } + + // Test for issue #6485: Test `sbt --client` startup + // https://github.com/sbt/sbt/issues/6485 + test("sbt --client startup time") { + if (isWindows || isMac) { + // Skip on Windows (sbtn behavior differs) and macOS CI (slow hostname resolution) + () + } else { + // First call starts the server if not running (warmup) + val warmup = sbtProcess("--client", "version").! + assert(warmup == 0, "Warmup sbt --client version failed") + + // Measure startup time for sbt --client when server is already running + // Run multiple times and take the average to reduce variance + val iterations = 5 + val times = (1 to iterations).map { _ => + val start = System.nanoTime() + val exitCode = sbtProcess("--client", "version").! + val elapsed = (System.nanoTime() - start) / 1_000_000 // Convert to milliseconds + assert(exitCode == 0, "sbt --client version failed") + elapsed + } + + val avgTime = times.sum / iterations + val maxTime = times.max + + println(s"sbt --client startup times (ms): ${times.mkString(", ")}") + println(s"Average: ${avgTime}ms, Max: ${maxTime}ms") + + // Cap at 2000ms to catch significant regressions while allowing for CI variance. + // The original issue #5980 mentioned ~200ms on developer machines in 2021, + // but CI runners are typically 2-3x slower than local development machines. + assert( + avgTime < 2000, + s"sbt --client startup time (${avgTime}ms average) exceeded 2000ms threshold" + ) + + // Cleanup: shutdown the server + val shutdown = sbtProcess("--client", "shutdown").! + assert(shutdown == 0, "Failed to shutdown sbt server") + } + () + } + + // Test for issue #8644: sbt.bat fails when project path contains parentheses + // https://github.com/sbt/sbt/issues/8644 + test("sbt.bat handles paths with parentheses") { + if (!isWindows) { + // This test is Windows-specific, skip on other platforms + () + } else { + IO.withTemporaryDirectory { baseDir => + // Create a temporary directory with parentheses in the name + val testDir = new File(baseDir, "test(parentheses)") + + // Create the directory structure + IO.createDirectory(testDir) + val projectDir = new File(testDir, "project") + IO.createDirectory(projectDir) + + // Create a minimal build.properties to make it a valid sbt project + val buildProps = new File(projectDir, "build.properties") + IO.write(buildProps, "sbt.version=1.12.1\n") + + // Test 1: Run sbt from directory with parentheses - should work without parsing errors + val out1 = sbtProcessInDir(testDir)("--script-version").!!.trim + val expectedVersion = "^" + versionRegEx + "$" + assert(out1.matches(expectedVersion), s"Expected version format, got: $out1") + + // Test 2: Test error message when no build.sbt exists (this is where the fix is most visible) + // Create a directory with parentheses but no build.sbt + val emptyDir = new File(baseDir, "empty(parentheses)") + IO.createDirectory(emptyDir) + + // Run sbt from empty directory - should fail gracefully with proper error message + // Use ProcessLogger to capture stderr without throwing on non-zero exit + import scala.sys.process.ProcessLogger + val errorBuffer = new StringBuilder + val logger = ProcessLogger( + _ => (), // ignore stdout + line => errorBuffer.append(line).append("\n") // capture stderr + ) + val exitCode = sbtProcessInDir(emptyDir)("compile").!(logger) + assert(exitCode == 1, "Expected sbt to fail when no build.sbt exists") + + // Verify the error output doesn't contain ") was unexpected" parsing error + val errorOutput = errorBuffer.toString + val hasParsingError = errorOutput.contains(") was unexpected") + assert( + !hasParsingError, + s"Error message should not contain parsing error when path has parentheses. Error output: $errorOutput" + ) + } + } + () + } +end ExtendedRunnerTest diff --git a/launcher-package/integration-test/src/test/scala/LauncherTestHelper.scala b/launcher-package/integration-test/src/test/scala/LauncherTestHelper.scala index 31da6d5d8..324b5e67c 100644 --- a/launcher-package/integration-test/src/test/scala/LauncherTestHelper.scala +++ b/launcher-package/integration-test/src/test/scala/LauncherTestHelper.scala @@ -14,10 +14,11 @@ object LauncherTestHelper { isWindows && sys.props.get("sbt.test.useSbtw").contains("true") /** Command prefix to run the launcher: either script path or java -cp sbtw.Main */ - def launcherCommand(scriptPath: String): Seq[String] = - if (useSbtw) { + def launcherCommand(scriptPath: String, useGitBash: Boolean = false): Seq[String] = + if useGitBash then + Seq("C:\\Program Files\\Git\\bin\\bash.EXE", "--noprofile", "-e", "--", scriptPath) + else if useSbtw then val cp = sys.props.get("sbt.test.classpath").getOrElse(System.getProperty("java.class.path")) Seq("java", "-cp", cp, "sbtw.Main") - } else - Seq(scriptPath) + else Seq(scriptPath) } diff --git a/launcher-package/integration-test/src/test/scala/RunnerScriptTest.scala b/launcher-package/integration-test/src/test/scala/RunnerScriptTest.scala index a4425338d..03205e5fa 100644 --- a/launcher-package/integration-test/src/test/scala/RunnerScriptTest.scala +++ b/launcher-package/integration-test/src/test/scala/RunnerScriptTest.scala @@ -3,7 +3,7 @@ package example.test /** * RunnerScriptTest is used to test the sbt shell script, for both macOS/Linux and Windows. */ -object RunnerScriptTest extends verify.BasicTestSuite with ShellScriptUtil: +abstract class RunnerScriptTest extends verify.BasicTestSuite with ShellScriptUtil: private val versionPattern = "\\d(\\.\\d+){2}(-\\w+)?" private def assertScriptVersion(out: List[String]): Unit = @@ -59,11 +59,11 @@ object RunnerScriptTest extends verify.BasicTestSuite with ShellScriptUtil: assert(out.contains[String]("-Dhttp.proxyPort=8080")) testOutput( - name = "sbt with -XX:ParallelGCThreads=16 -XX:PermSize=128M in SBT_OPTS", - sbtOpts = "-XX:ParallelGCThreads=16 -XX:PermSize=128M", + name = "sbt with -XX:+UseG1GC -XX:+PrintGC in SBT_OPTS", + sbtOpts = "-XX:+UseG1GC -XX:+PrintGC", )("-v"): (out: List[String]) => - assert(out.contains[String]("-XX:ParallelGCThreads=16")) - assert(out.contains[String]("-XX:PermSize=128M")) + assert(out.contains[String]("-XX:+UseG1GC")) + assert(out.contains[String]("-XX:+PrintGC")) testOutput( "sbt with -XX:+UseG1GC -XX:+PrintGC in JAVA_OPTS", @@ -117,10 +117,11 @@ object RunnerScriptTest extends verify.BasicTestSuite with ShellScriptUtil: out.contains[String]("-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=12346") ) - testOutput("sbt --no-share adds three system properties")("--no-share"): (out: List[String]) => - assert(out.contains[String]("-Dsbt.global.base=project/.sbtboot")) - assert(out.contains[String]("-Dsbt.boot.directory=project/.boot")) - assert(out.contains[String]("-Dsbt.ivy.home=project/.ivy")) + testOutput("sbt --no-share adds three system properties")("--no-share", "-v"): + (out: List[String]) => + assert(out.contains[String]("-Dsbt.global.base=project/.sbtboot")) + assert(out.contains[String]("-Dsbt.boot.directory=project/.boot")) + assert(out.contains[String]("-Dsbt.ivy.home=project/.ivy")) testOutput("accept `--ivy` in `SBT_OPTS`", sbtOpts = "--ivy /ivy/dir")("-v"): (out: List[String]) => @@ -253,6 +254,23 @@ object RunnerScriptTest extends verify.BasicTestSuite with ShellScriptUtil: s"sbtopts options should appear before CLI memory settings. g1Index=$g1Index, xmxCliIndex=$xmxCliIndex" ) + // Test for issue #7333: JVM parameters with spaces in .sbtopts + testOutput( + "sbt with -J--add-modules ALL-DEFAULT in .sbtopts (args with spaces)", + sbtOptsFileContents = "-J--add-modules ALL-DEFAULT", + windowsSupport = false, + )("-v"): (out: List[String]) => + assert(out.contains[String]("--add-modules")) + assert(out.contains[String]("ALL-DEFAULT")) + + // Test for issue #8767 + testOutput( + "sbt with newline in .jvmopts", + jvmoptsFileContents = "-Xmx2G\n-Xss1M\n", + windowsSupport = false, + )("-v"): (out: List[String]) => + assert(out.exists(_.contains("-Xmx2G"))) + // Test for issue #7289: Special characters in .jvmopts should not cause shell expansion testOutput( "sbt with special characters in .jvmopts (pipes, wildcards, ampersands)", @@ -322,3 +340,9 @@ object RunnerScriptTest extends verify.BasicTestSuite with ShellScriptUtil: ) end RunnerScriptTest + +object RunnerScriptTest extends RunnerScriptTest + +object RunnerScriptGitBashTest extends RunnerScriptTest: + override def isGitBashTest: Boolean = true +end RunnerScriptGitBashTest diff --git a/launcher-package/integration-test/src/test/scala/ShellScriptUtil.scala b/launcher-package/integration-test/src/test/scala/ShellScriptUtil.scala index 918b00b7e..97df2d965 100644 --- a/launcher-package/integration-test/src/test/scala/ShellScriptUtil.scala +++ b/launcher-package/integration-test/src/test/scala/ShellScriptUtil.scala @@ -24,7 +24,8 @@ trait ShellScriptUtil extends BasicTestSuite { case e: Exception => throw e } - val sbtScript = IntegrationTestPaths.sbtScript(isWindows) + def isGitBashTest: Boolean = false + lazy val sbtScript = IntegrationTestPaths.sbtScript(isWindows && !isGitBashTest) /** * testOutput is a helper function to create a test for shell script. @@ -41,11 +42,14 @@ trait ShellScriptUtil extends BasicTestSuite { windowsSupport: Boolean = true, citestVariant: String = "citest", )(args: String*)(f: List[String] => Any) = - if !windowsSupport && isWindows then + if isGitBashTest && !isWindows then + test("gitbash: " + name): + cancel("skip") + else if !isGitBashTest && !windowsSupport && isWindows then test(name): cancel("test not supported on Windows") else - test(name) { + test(if isGitBashTest then "gitbash: " + name else name) { val workingDirectory = Files.createTempDirectory("sbt-launcher-package-test").toFile val citestDir = IntegrationTestPaths.citestDir(citestVariant) // Clean target directory if it exists to avoid copying temporary files that may be deleted during copy @@ -154,12 +158,14 @@ trait ShellScriptUtil extends BasicTestSuite { envVars("JAVA_OPTS") = javaOpts envVars("SBT_OPTS") = sbtOpts envVars("JAVA_TOOL_OPTIONS") = javaToolOptions - if (isWindows) + if isWindows then envVars("JAVACMD") = new File(javaBinDir, "java").getAbsolutePath() + envVars("JAVA_HOME") = sys.env("JAVA_HOME") else envVars("PATH") = javaBinDir + File.pathSeparator + path - - val cmd = LauncherTestHelper.launcherCommand(testSbtScript.getAbsolutePath) ++ args + envVars("JAVA_HOME") = sys.env("JAVA_HOME") + val cmd = + LauncherTestHelper.launcherCommand(testSbtScript.getAbsolutePath, isGitBashTest) ++ args val lines = mutable.ListBuffer.empty[String] def processLine(line: String): Unit = Console.err.println(line) diff --git a/sbt b/sbt index 358c461a5..b0bcd7d4b 100755 --- a/sbt +++ b/sbt @@ -862,6 +862,10 @@ original_args=("$@") sbt_file_opts=() +if [[ -f "$JAVACMD" ]]; then + java_cmd="$JAVACMD" +fi + # Pull in the machine-wide settings configuration. if [[ -f "$machine_sbt_opts_file" ]]; then sbt_file_opts+=($(loadConfigFile "$machine_sbt_opts_file"))