diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSRun.scala b/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSRun.scala index a519d9b..17cf994 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSRun.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSRun.scala @@ -69,6 +69,7 @@ object ExternalJSRun { validator .supportsInheritIO() .supportsOnOutputStream() + .supportsEnv() } /** Configuration for a [[ExternalJSRun]] @@ -138,6 +139,10 @@ object ExternalJSRun { for ((name, value) <- env) builder.environment().put(name, value) + // RunConfig#env takes precedence in case of collisions. + for ((name, value) <- config.env) + builder.environment().put(name, value) + config.logger.debug("Starting process: " + command.mkString(" ")) try { diff --git a/js-envs/src/main/scala/org/scalajs/jsenv/RunConfig.scala b/js-envs/src/main/scala/org/scalajs/jsenv/RunConfig.scala index 15af4db..c4e1bb7 100644 --- a/js-envs/src/main/scala/org/scalajs/jsenv/RunConfig.scala +++ b/js-envs/src/main/scala/org/scalajs/jsenv/RunConfig.scala @@ -42,12 +42,26 @@ import org.scalajs.logging._ * * @param logger The logger to use in the run. A [[JSEnv]] is not required to * log anything. + * + * @param env Additional environment variables for this run. + * + * How these are retrieved in the JS code run inside the [[JSEnv]] is + * completely up to the implementation, including whether: + * - they are implemented with system environment variables, + * - they share the same namespace than the system environment variables. + * + * However, in any case, the variables in [[env]] take precedence + * over any (explicitly or implicitly) ambiant environment vars. + * + * This is an optional feature; but [[JSEnv]]s are required to support an + * empty [[env]]. */ final class RunConfig private ( val onOutputStream: Option[RunConfig.OnOutputStream], val inheritOutput: Boolean, val inheritError: Boolean, val logger: Logger, + val env: Map[String, String], /** An option that will never be supported by anything because it is not exposed. * * This is used to test that [[JSEnv]]s properly validate their configuration. @@ -62,6 +76,7 @@ final class RunConfig private ( inheritOutput = true, inheritError = true, logger = NullLogger, + env = Map.empty, eternallyUnsupportedOption = false) } @@ -77,6 +92,9 @@ final class RunConfig private ( def withLogger(logger: Logger): RunConfig = copy(logger = logger) + def withEnv(env: Map[String, String]): RunConfig = + copy(env = env) + private[jsenv] def withEternallyUnsupportedOption( eternallyUnsupportedOption: Boolean): RunConfig = copy(eternallyUnsupportedOption = eternallyUnsupportedOption) @@ -85,10 +103,11 @@ final class RunConfig private ( inheritOutput: Boolean = inheritOutput, inheritError: Boolean = inheritError, logger: Logger = logger, + env: Map[String, String] = env, eternallyUnsupportedOption: Boolean = eternallyUnsupportedOption ): RunConfig = { new RunConfig(onOutputStream, inheritOutput, inheritError, logger, - eternallyUnsupportedOption) + env, eternallyUnsupportedOption) } /** Validates constraints on the config itself. */ @@ -119,9 +138,10 @@ final object RunConfig { */ final class Validator private ( inheritIO: Boolean, - onOutputStream: Boolean + onOutputStream: Boolean, + env: Boolean ) { - private def this() = this(false, false) + private def this() = this(false, false, false) /** The caller supports [[RunConfig#inheritOutput]] and * [[RunConfig#inheritError]]. @@ -131,6 +151,9 @@ final object RunConfig { /** The caller supports [[RunConfig#onOutputStream]]. */ def supportsOnOutputStream(): Validator = copy(onOutputStream = true) + /** The caller supports [[RunConfig#env]]. */ + def supportsEnv(): Validator = copy(env = true) + /** Validates that `config` is valid and only sets supported options. * * @throws java.lang.IllegalArgumentException if there are unsupported options. @@ -146,13 +169,19 @@ final object RunConfig { if (!onOutputStream && config.onOutputStream.isDefined) fail("onOutputStream is not supported.") + if (!env && config.env.nonEmpty) + fail("env is not supported.") + if (config.eternallyUnsupportedOption) fail("eternallyUnsupportedOption is not supported.") } - private def copy(inheritIO: Boolean = inheritIO, - onOutputStream: Boolean = onOutputStream) = { - new Validator(inheritIO, onOutputStream) + private def copy( + inheritIO: Boolean = inheritIO, + onOutputStream: Boolean = onOutputStream, + env: Boolean = env + ) = { + new Validator(inheritIO, onOutputStream, env) } } diff --git a/js-envs/src/test/scala/org/scalajs/jsenv/ExternalJSRunTest.scala b/js-envs/src/test/scala/org/scalajs/jsenv/ExternalJSRunTest.scala index 6e81e07..601420c 100644 --- a/js-envs/src/test/scala/org/scalajs/jsenv/ExternalJSRunTest.scala +++ b/js-envs/src/test/scala/org/scalajs/jsenv/ExternalJSRunTest.scala @@ -90,4 +90,67 @@ class ExternalJSRunTest { run.close() Await.result(run.future, 1.second) } + + private def checkEnvRun(name: String, want: String, config: ExternalJSRun.Config) = { + ExternalJSRun.start(List("node"), config) { stdin => + val p = new java.io.PrintStream(stdin) + p.println("""const process = require("process");"""); + p.println(s"""process.exit(process.env["$name"] !== "$want");"""); + p.close() + } + } + + @Test + def setEnv: Unit = { + val config = silentConfig + .withEnv(Map("EXTERNAL_JS_RUN_TEST" -> "witness")) + val run = checkEnvRun("EXTERNAL_JS_RUN_TEST", "witness", config) + + Await.result(run.future, 1.second) + } + + @Test + def setEnvOnRunConfig: Unit = { + val runConfig = RunConfig() + .withEnv(Map("EXTERNAL_JS_RUN_TEST" -> "witness")) + val config = silentConfig + .withRunConfig(runConfig) + val run = checkEnvRun("EXTERNAL_JS_RUN_TEST", "witness", config) + + Await.result(run.future, 1.second) + } + + @Test + def envOverrides: Unit = { + val runConfig = RunConfig() + .withEnv(Map("EXTERNAL_JS_RUN_TEST" -> "override")) + val config = silentConfig + .withEnv(Map("EXTERNAL_JS_RUN_TEST" -> "witness")) + .withRunConfig(runConfig) + val run = checkEnvRun("EXTERNAL_JS_RUN_TEST", "override", config) + + Await.result(run.future, 1.second) + } + + // Confidence tests for checkEnvRun. + + @Test + def setEnvWrong: Unit = { + val config = silentConfig + .withEnv(Map("EXTERNAL_JS_RUN_TEST" -> "not-witness")) + val run = checkEnvRun("EXTERNAL_JS_RUN_TEST", "witness", config) + + assertFails(run.future) { + case ExternalJSRun.NonZeroExitException(1) => // OK + } + } + + @Test + def setEnvMissing: Unit = { + val run = checkEnvRun("EXTERNAL_JS_RUN_TEST", "witness", silentConfig) + + assertFails(run.future) { + case ExternalJSRun.NonZeroExitException(1) => // OK + } + } } diff --git a/js-envs/src/test/scala/org/scalajs/jsenv/RunConfigTest.scala b/js-envs/src/test/scala/org/scalajs/jsenv/RunConfigTest.scala index b57cc48..d66820b 100644 --- a/js-envs/src/test/scala/org/scalajs/jsenv/RunConfigTest.scala +++ b/js-envs/src/test/scala/org/scalajs/jsenv/RunConfigTest.scala @@ -13,6 +13,7 @@ package org.scalajs.jsenv import org.junit.Test +import org.junit.Assert._ class RunConfigTest { @Test @@ -90,6 +91,25 @@ class RunConfigTest { .validate(cfg) } + @Test + def supportedEnv: Unit = { + val cfg = RunConfig() + .withEnv(Map("x" -> "y")) + RunConfig.Validator() + .supportsInheritIO() + .supportsEnv() + .validate(cfg) + } + + @Test(expected = classOf[IllegalArgumentException]) + def unsupportedEnv: Unit = { + val cfg = RunConfig() + .withEnv(Map("x" -> "y")) + RunConfig.Validator() + .supportsInheritIO() + .validate(cfg) + } + @Test(expected = classOf[IllegalArgumentException]) def failValidationForTest: Unit = { val cfg = RunConfig()