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
Show file tree
Hide file tree
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
Expand Up @@ -69,6 +69,7 @@ object ExternalJSRun {
validator
.supportsInheritIO()
.supportsOnOutputStream()
.supportsEnv()
}

/** Configuration for a [[ExternalJSRun]]
Expand Down Expand Up @@ -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 {
Expand Down
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
Expand Up @@ -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.
Expand All @@ -62,6 +76,7 @@ final class RunConfig private (
inheritOutput = true,
inheritError = true,
logger = NullLogger,
env = Map.empty,
eternallyUnsupportedOption = false)
}

Expand All @@ -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)
Expand All @@ -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. */
Expand Down Expand Up @@ -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]].
Expand All @@ -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.
Expand All @@ -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)
}
}

Expand Down
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
Expand Up @@ -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
Expand Up @@ -13,6 +13,7 @@
package org.scalajs.jsenv

import org.junit.Test
import org.junit.Assert._

class RunConfigTest {
@Test
Expand Down Expand Up @@ -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()
Expand Down