From 98e2662bc493a39c6aa19d1aaeb4d9abcaf849e9 Mon Sep 17 00:00:00 2001 From: Mark Harrah Date: Thu, 21 Feb 2013 20:44:26 -0500 Subject: [PATCH] Make full set of ForkOptions configurable for both run and test. Fixes #665. Environment variables may be passed by configuring the envVars task. --- .../src/main/scala/sbt/ForkTests.scala | 6 ++-- main/actions/src/main/scala/sbt/Tests.scala | 6 +++- main/src/main/scala/sbt/Defaults.scala | 33 ++++++++++-------- main/src/main/scala/sbt/Keys.scala | 1 + main/src/main/scala/sbt/Project.scala | 7 ++++ run/src/main/scala/sbt/Fork.scala | 34 ++++++++++++++++--- run/src/main/scala/sbt/Run.scala | 9 +++-- .../run/fork/src/main/scala/ForkFail.scala | 4 ++- sbt/src/sbt-test/run/fork/test | 9 ++++- .../sbt-test/tests/fork2/changes/Test.scala | 3 +- sbt/src/sbt-test/tests/fork2/test | 6 +++- 11 files changed, 89 insertions(+), 29 deletions(-) diff --git a/main/actions/src/main/scala/sbt/ForkTests.scala b/main/actions/src/main/scala/sbt/ForkTests.scala index b8236591f..3be3ad60d 100755 --- a/main/actions/src/main/scala/sbt/ForkTests.scala +++ b/main/actions/src/main/scala/sbt/ForkTests.scala @@ -11,7 +11,7 @@ import Tests.{Output => TestOutput, _} import ForkMain._ private[sbt] object ForkTests { - def apply(frameworks: Seq[TestFramework], tests: List[TestDefinition], config: Execution, classpath: Seq[File], javaHome: Option[File], javaOpts: Seq[String], log: Logger): Task[TestOutput] = { + def apply(frameworks: Seq[TestFramework], tests: List[TestDefinition], config: Execution, classpath: Seq[File], fork: ForkOptions, log: Logger): Task[TestOutput] = { val opts = config.options.toList val listeners = opts flatMap { case Listeners(ls) => ls @@ -75,8 +75,8 @@ private[sbt] object ForkTests { new Thread(Acceptor).start() val fullCp = classpath ++: Seq(IO.classLocationFile[ForkMain], IO.classLocationFile[Framework]) - val options = javaOpts ++: Seq("-classpath", fullCp mkString File.pathSeparator, classOf[ForkMain].getCanonicalName, server.getLocalPort.toString) - val ec = Fork.java(javaHome, options, StdoutOutput) + val options = Seq("-classpath", fullCp mkString File.pathSeparator, classOf[ForkMain].getCanonicalName, server.getLocalPort.toString) + val ec = Fork.java(fork, options) val result = if (ec != 0) (TestResult.Error, Map("Running java with options " + options.mkString(" ") + " failed with exit code " + ec -> TestResult.Error)) diff --git a/main/actions/src/main/scala/sbt/Tests.scala b/main/actions/src/main/scala/sbt/Tests.scala index 33b2826a1..613185d13 100644 --- a/main/actions/src/main/scala/sbt/Tests.scala +++ b/main/actions/src/main/scala/sbt/Tests.scala @@ -200,7 +200,11 @@ object Tests sealed trait TestRunPolicy case object InProcess extends TestRunPolicy - final case class SubProcess(javaOptions: Seq[String]) extends TestRunPolicy + final case class SubProcess(config: ForkOptions) extends TestRunPolicy + object SubProcess { + @deprecated("Construct SubProcess with a ForkOptions argument.", "0.13.0") + def apply(javaOptions: Seq[String]): SubProcess = SubProcess(ForkOptions(runJVMOptions = javaOptions)) + } final case class Group(name: String, tests: Seq[TestDefinition], runPolicy: TestRunPolicy) } diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index 36c90dca9..7f955bb2a 100755 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -70,6 +70,7 @@ object Defaults extends BuildCommon logBuffered :== false, connectInput :== false, cancelable :== false, + envVars :== Map.empty, sourcesInBase :== true, autoAPIMappings := false, apiMappings := Map.empty, @@ -378,10 +379,17 @@ object Defaults extends BuildCommon val mod = tdef.fingerprint match { case f: SubclassFingerprint => f.isModule; case f: AnnotatedFingerprint => f.isModule; case _ => false } extra.put(name.key, tdef.name).put(isModule, mod) } - def singleTestGroup(key: Scoped): Initialize[Task[Seq[Tests.Group]]] = - ((definedTests in key, fork in key, javaOptions in key) map { - (tests, fork, javaOpts) => Seq(new Tests.Group("", tests, if (fork) Tests.SubProcess(javaOpts) else Tests.InProcess)) - }) + def singleTestGroup(key: Scoped): Initialize[Task[Seq[Tests.Group]]] = Def.task { + val tests = (definedTests in key).value + val fk = (fork in key).value + val opts = inTask(key, forkOptions).value + Seq(new Tests.Group("", tests, if(fk) Tests.SubProcess(opts) else Tests.InProcess)) + } + private[this] def forkOptions: Initialize[Task[ForkOptions]] = + (baseDirectory, scalaInstance, javaOptions, outputStrategy, envVars, javaHome, connectInput) map { + (base, si, options, strategy, env, javaHomeDir, connectIn) => + ForkOptions(scalaJars = si.jars, javaHome = javaHomeDir, connectInput = connectIn, outputStrategy = strategy, runJVMOptions = options, workingDirectory = Some(base), envVars = env) + } def testExecutionTask(task: Scoped): Initialize[Task[Tests.Execution]] = (testOptions in task, parallelExecution in task, tags in task) map { @@ -445,8 +453,8 @@ object Defaults extends BuildCommon val groupTasks = groups map { case Tests.Group(name, tests, runPolicy) => runPolicy match { - case Tests.SubProcess(javaOpts) => - ForkTests(frameworks.keys.toSeq, tests.toList, config, cp.files, javaHome, javaOpts, s.log) tag Tags.ForkedTestGroup + case Tests.SubProcess(opts) => + ForkTests(frameworks.keys.toSeq, tests.toList, config, cp.files, opts, s.log) tag Tags.ForkedTestGroup case Tests.InProcess => Tests(frameworks, loader, tests, config, s.log) } @@ -598,14 +606,11 @@ object Defaults extends BuildCommon } def runnerTask = runner <<= runnerInit - def runnerInit: Initialize[Task[ScalaRun]] = - (taskTemporaryDirectory, scalaInstance, baseDirectory, javaOptions, outputStrategy, fork, javaHome, trapExit, connectInput) map { - (tmp, si, base, options, strategy, forkRun, javaHomeDir, trap, connectIn) => - if(forkRun) { - new ForkRun( ForkOptions(scalaJars = si.jars, javaHome = javaHomeDir, connectInput = connectIn, outputStrategy = strategy, runJVMOptions = options, workingDirectory = Some(base)) ) - } else - new Run(si, trap, tmp) - } + def runnerInit: Initialize[Task[ScalaRun]] = Def.task { + val tmp = taskTemporaryDirectory.value + val si = scalaInstance.value + if(fork.value) new ForkRun(forkOptions.value) else new Run(si, trapExit.value, tmp) + } @deprecated("Use `docTaskSettings` instead", "0.12.0") def docSetting(key: TaskKey[File]) = docTaskSettings(key) diff --git a/main/src/main/scala/sbt/Keys.scala b/main/src/main/scala/sbt/Keys.scala index 939f840c5..f9d8c8158 100644 --- a/main/src/main/scala/sbt/Keys.scala +++ b/main/src/main/scala/sbt/Keys.scala @@ -187,6 +187,7 @@ object Keys val connectInput = SettingKey[Boolean]("connect-input", "If true, connects standard input when running a main class forked.", CSetting) val javaHome = SettingKey[Option[File]]("java-home", "Selects the Java installation used for compiling and forking. If None, uses the Java installation running the build.", ASetting) val javaOptions = TaskKey[Seq[String]]("java-options", "Options passed to a new JVM when forking.", BPlusTask) + val envVars = TaskKey[Map[String,String]]("envVars", "Environment variables used when forking a new JVM", BTask) // Test Keys val testLoader = TaskKey[ClassLoader]("test-loader", "Provides the class loader used for testing.", DTask) diff --git a/main/src/main/scala/sbt/Project.scala b/main/src/main/scala/sbt/Project.scala index 39eb3444a..d1a0a8545 100755 --- a/main/src/main/scala/sbt/Project.scala +++ b/main/src/main/scala/sbt/Project.scala @@ -435,6 +435,13 @@ trait ProjectExtra def inScope(scope: Scope)(ss: Seq[Setting[_]]): Seq[Setting[_]] = Project.transform(Scope.replaceThis(scope), ss) + private[sbt] def inConfig[T](conf: Configuration, i: Initialize[T]): Initialize[T] = + inScope(ThisScope.copy(config = Select(conf)), i) + private[sbt] def inTask[T](t: Scoped, i: Initialize[T]): Initialize[T] = + inScope(ThisScope.copy(task = Select(t.key)), i) + private[sbt] def inScope[T](scope: Scope, i: Initialize[T]): Initialize[T] = + i mapReferenced Project.mapScope(Scope.replaceThis(scope)) + /** Creates a new Project. This is a macro that expects to be assigned directly to a val. * The name of the val is used as the project ID and the name of the base directory of the project. */ def project: Project = macro Project.projectMacroImpl diff --git a/run/src/main/scala/sbt/Fork.scala b/run/src/main/scala/sbt/Fork.scala index 52216bcb0..7b68be4d3 100644 --- a/run/src/main/scala/sbt/Fork.scala +++ b/run/src/main/scala/sbt/Fork.scala @@ -20,7 +20,7 @@ trait ForkScalaRun extends ForkScala def workingDirectory: Option[File] def runJVMOptions: Seq[String] } -final case class ForkOptions(javaHome: Option[File] = None, outputStrategy: Option[OutputStrategy] = None, scalaJars: Iterable[File] = Nil, workingDirectory: Option[File] = None, runJVMOptions: Seq[String] = Nil, connectInput: Boolean = false) extends ForkScalaRun +final case class ForkOptions(javaHome: Option[File] = None, outputStrategy: Option[OutputStrategy] = None, scalaJars: Iterable[File] = Nil, workingDirectory: Option[File] = None, runJVMOptions: Seq[String] = Nil, connectInput: Boolean = false, envVars: Map[String,String] = Map.empty) extends ForkScalaRun sealed abstract class OutputStrategy extends NotNull case object StdoutOutput extends OutputStrategy @@ -45,16 +45,28 @@ object Fork new File(new File(home, "bin"), name) } - final class ForkJava(commandName: String) extends NotNull + final class ForkJava(commandName: String) { + def apply(config: ForkOptions, arguments: Seq[String]): Int = fork(config, arguments).exitValue() + def fork(config: ForkOptions, arguments: Seq[String]): Process = + fork(config.javaHome, config.runJVMOptions ++ arguments, config.workingDirectory, config.envVars, config.connectInput, config.outputStrategy getOrElse StdoutOutput) + @deprecated("Use apply(ForkOptions, Seq[String])", "0.13.0") def apply(javaHome: Option[File], options: Seq[String], log: Logger): Int = apply(javaHome, options, BufferedOutput(log)) + + @deprecated("Use apply(ForkOptions, Seq[String])", "0.13.0") def apply(javaHome: Option[File], options: Seq[String], outputStrategy: OutputStrategy): Int = apply(javaHome, options, None, outputStrategy) + + @deprecated("Use apply(ForkOptions, Seq[String])", "0.13.0") def apply(javaHome: Option[File], options: Seq[String], workingDirectory: Option[File], log: Logger): Int = apply(javaHome, options, workingDirectory, BufferedOutput(log)) + + @deprecated("Use apply(ForkOptions, Seq[String])", "0.13.0") def apply(javaHome: Option[File], options: Seq[String], workingDirectory: Option[File], outputStrategy: OutputStrategy): Int = apply(javaHome, options, workingDirectory, Map.empty, outputStrategy) + + @deprecated("Use apply(ForkOptions, Seq[String])", "0.13.0") def apply(javaHome: Option[File], options: Seq[String], workingDirectory: Option[File], env: Map[String, String], outputStrategy: OutputStrategy): Int = fork(javaHome, options, workingDirectory, env, false, outputStrategy).exitValue def fork(javaHome: Option[File], options: Seq[String], workingDirectory: Option[File], env: Map[String, String], connectInput: Boolean, outputStrategy: OutputStrategy): Process = @@ -75,21 +87,35 @@ object Fork } } - final class ForkScala(mainClassName: String) extends NotNull + final class ForkScala(mainClassName: String) { + def apply(options: ForkOptions, arguments: Seq[String]): Int = fork(options, arguments).exitValue() + def fork(options: ForkOptions, arguments: Seq[String]): Process = + fork(options.javaHome, options.runJVMOptions, options.scalaJars, arguments, options.workingDirectory, options.envVars, options.connectInput, options.outputStrategy getOrElse StdoutOutput) + + @deprecated("Use apply(ForkOptions, Seq[String])", "0.13.0") def apply(javaHome: Option[File], jvmOptions: Seq[String], scalaJars: Iterable[File], arguments: Seq[String], log: Logger): Int = apply(javaHome, jvmOptions, scalaJars, arguments, None, BufferedOutput(log)) + + @deprecated("Use apply(ForkOptions, Seq[String])", "0.13.0") def apply(javaHome: Option[File], jvmOptions: Seq[String], scalaJars: Iterable[File], arguments: Seq[String], workingDirectory: Option[File], log: Logger): Int = apply(javaHome, jvmOptions, scalaJars, arguments, workingDirectory, BufferedOutput(log)) + + @deprecated("Use apply(ForkOptions, Seq[String])", "0.13.0") def apply(javaHome: Option[File], jvmOptions: Seq[String], scalaJars: Iterable[File], arguments: Seq[String], workingDirectory: Option[File], outputStrategy: OutputStrategy): Int = fork(javaHome, jvmOptions, scalaJars, arguments, workingDirectory, false, outputStrategy).exitValue() + + @deprecated("Use fork(ForkOptions, Seq[String])", "0.13.0") def fork(javaHome: Option[File], jvmOptions: Seq[String], scalaJars: Iterable[File], arguments: Seq[String], workingDirectory: Option[File], connectInput: Boolean, outputStrategy: OutputStrategy): Process = + fork(javaHome, jvmOptions, scalaJars, arguments, workingDirectory, connectInput, outputStrategy) + + def fork(javaHome: Option[File], jvmOptions: Seq[String], scalaJars: Iterable[File], arguments: Seq[String], workingDirectory: Option[File], env: Map[String,String], connectInput: Boolean, outputStrategy: OutputStrategy): Process = { if(scalaJars.isEmpty) error("Scala jars not specified") val scalaClasspathString = "-Xbootclasspath/a:" + scalaJars.map(_.getAbsolutePath).mkString(File.pathSeparator) val mainClass = if(mainClassName.isEmpty) Nil else mainClassName :: Nil val options = jvmOptions ++ (scalaClasspathString :: mainClass ::: arguments.toList) - Fork.java.fork(javaHome, options, workingDirectory, Map.empty, connectInput, outputStrategy) + Fork.java.fork(javaHome, options, workingDirectory, env, connectInput, outputStrategy) } } } diff --git a/run/src/main/scala/sbt/Run.scala b/run/src/main/scala/sbt/Run.scala index 40053f1f9..a11c4cc00 100644 --- a/run/src/main/scala/sbt/Run.scala +++ b/run/src/main/scala/sbt/Run.scala @@ -13,15 +13,18 @@ trait ScalaRun { def run(mainClass: String, classpath: Seq[File], options: Seq[String], log: Logger): Option[String] } -class ForkRun(config: ForkScalaRun) extends ScalaRun +class ForkRun(config: ForkOptions) extends ScalaRun { + @deprecated("Use the `ForkRun(ForkOptions) constructor`", "0.13.0") + def this(options: ForkScalaRun) = this(ForkOptions(options.javaHome, options.outputStrategy, options.scalaJars, options.workingDirectory, options.runJVMOptions, options.connectInput)) + def run(mainClass: String, classpath: Seq[File], options: Seq[String], log: Logger): Option[String] = { log.info("Running " + mainClass + " " + options.mkString(" ")) val scalaOptions = classpathOption(classpath) ::: mainClass :: options.toList - val strategy = config.outputStrategy getOrElse LoggedOutput(log) - val process = Fork.scala.fork(config.javaHome, config.runJVMOptions, config.scalaJars, scalaOptions, config.workingDirectory, config.connectInput, strategy) + val configLogged = if(config.outputStrategy.isDefined) config else config.copy(outputStrategy = Some(LoggedOutput(log))) + val process = Fork.scala.fork(configLogged, scalaOptions) def cancel() = { log.warn("Run canceled.") process.destroy() diff --git a/sbt/src/sbt-test/run/fork/src/main/scala/ForkFail.scala b/sbt/src/sbt-test/run/fork/src/main/scala/ForkFail.scala index 162216bfb..2fc9c2b26 100644 --- a/sbt/src/sbt-test/run/fork/src/main/scala/ForkFail.scala +++ b/sbt/src/sbt-test/run/fork/src/main/scala/ForkFail.scala @@ -1,6 +1,8 @@ object ForkTest { def main(args:Array[String]) { - val cwd = (new java.io.File("flag")).getAbsoluteFile + val name = Option(System.getenv("flag.name")) getOrElse("flag") + println("Name: " + name) + val cwd = (new java.io.File(name)).getAbsoluteFile cwd.getParentFile.mkdirs() cwd.createNewFile() } diff --git a/sbt/src/sbt-test/run/fork/test b/sbt/src/sbt-test/run/fork/test index 2ff82f66e..c0fae0eb0 100644 --- a/sbt/src/sbt-test/run/fork/test +++ b/sbt/src/sbt-test/run/fork/test @@ -8,4 +8,11 @@ $ mkdir "forked" > run "forked" $ exists forked/flag -$ absent flag \ No newline at end of file +$ absent flag +$ delete forked/flag + +> 'set envVars += ("flag.name" -> "env.flag")' +> run "forked" +$ exists forked/env.flag +$ absent flag +$ absent forked/flag diff --git a/sbt/src/sbt-test/tests/fork2/changes/Test.scala b/sbt/src/sbt-test/tests/fork2/changes/Test.scala index 19789db2c..f0ecc32bb 100644 --- a/sbt/src/sbt-test/tests/fork2/changes/Test.scala +++ b/sbt/src/sbt-test/tests/fork2/changes/Test.scala @@ -2,7 +2,8 @@ import org.scalatest.FlatSpec import org.scalatest.matchers.MustMatchers class Test extends FlatSpec with MustMatchers { + val v = Option(System.getenv("tests.max.value")) getOrElse Int.MaxValue "A simple equation" must "hold" in { - Int.MaxValue must equal (Int.MaxValue) + Int.MaxValue must equal (v) } } diff --git a/sbt/src/sbt-test/tests/fork2/test b/sbt/src/sbt-test/tests/fork2/test index 7ccfe7768..46ab1ff8d 100644 --- a/sbt/src/sbt-test/tests/fork2/test +++ b/sbt/src/sbt-test/tests/fork2/test @@ -5,4 +5,8 @@ $ copy-file changes/Test.scala src/test/scala/Test.scala > test > 'set javaOptions += "-Xno-opt"' --> test \ No newline at end of file +-> test + +> session clear +> 'set envVars += ("tests.max.value" -> "0")' +-> test