Skip to content

Add support for Environment vars in RunConfig #23

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Aug 28, 2022
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSRun.scala
Original file line number Diff line number Diff line change
@@ -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 {
41 changes: 35 additions & 6 deletions js-envs/src/main/scala/org/scalajs/jsenv/RunConfig.scala
Original file line number Diff line number Diff line change
@@ -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)
}
}

63 changes: 63 additions & 0 deletions js-envs/src/test/scala/org/scalajs/jsenv/ExternalJSRunTest.scala
Original file line number Diff line number Diff line change
@@ -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
}
}
}
20 changes: 20 additions & 0 deletions js-envs/src/test/scala/org/scalajs/jsenv/RunConfigTest.scala
Original file line number Diff line number Diff line change
@@ -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()