From 7afc9e77c61c222caaa2a2df1634d03290c58b48 Mon Sep 17 00:00:00 2001 From: Eugene Vigdorchik Date: Sun, 1 Apr 2012 11:44:05 +0400 Subject: [PATCH 01/15] \'fork in test\' initial implementation. --- main/Defaults.scala | 68 +++++--- main/Keys.scala | 4 +- main/actions/ForkTests.scala | 106 ++++++++++++ main/actions/Tests.scala | 19 ++- project/Sbt.scala | 8 +- testing/TestFramework.scala | 3 +- testing/TestReportListener.scala | 2 +- testing/agent/src/main/java/sbt/ForkMain.java | 151 ++++++++++++++++++ util/io/IPC.scala | 2 +- 9 files changed, 327 insertions(+), 36 deletions(-) create mode 100755 main/actions/ForkTests.scala create mode 100755 testing/agent/src/main/java/sbt/ForkMain.java diff --git a/main/Defaults.scala b/main/Defaults.scala index b4c8a24e4..5ace1c061 100755 --- a/main/Defaults.scala +++ b/main/Defaults.scala @@ -125,7 +125,12 @@ object Defaults extends BuildCommon name <<= thisProject(_.id), logManager <<= extraLoggers(LogManager.defaults), onLoadMessage <<= onLoadMessage or (name, thisProjectRef)("Set current project to " + _ + " (in build " + _.build +")"), - runnerTask + runnerTask, + forkOptions <<= (scalaInstance, baseDirectory, javaOptions, outputStrategy, javaHome, connectInput) map { + (si, base, options, strategy, javaHomeDir, connectIn) => + ForkOptions(scalaJars = si.jars, javaHome = javaHomeDir, connectInput = connectIn, outputStrategy = strategy, + runJVMOptions = options, workingDirectory = Some(base)) + } ) def paths = Seq( baseDirectory <<= thisProject(_.base), @@ -286,17 +291,27 @@ object Defaults extends BuildCommon testOptions in GlobalScope :== Nil, testFilter in testOnly :== (selectedFilter _), testFilter in testQuick <<= testQuickFilter, - executeTests <<= (streams in test, loadedTestFrameworks, testExecution in test, testLoader, definedTests, resolvedScoped, state) flatMap { - (s, frameworkMap, config, loader, discovered, scoped, st) => + executeTests <<= (streams in test, loadedTestFrameworks, testLoader, testGrouping in test, fullClasspath in test, forkOptions in test, resolvedScoped, state) flatMap { + (s, frameworkMap, loader, groups, cp, forkOpts, scoped, st) => implicit val display = Project.showContextKey(st) - Tests(frameworkMap, loader, discovered, config, noTestsMessage(ScopedKey(scoped.scope, test.key)), s.log) + val results = groups map { + case Tests.TestGroup(name, tests, config) => + config.subproc match { + case Tests.Fork(extraJvm) => + val runner = new ForkRun(forkOpts.copy(runJVMOptions = forkOpts.runJVMOptions ++ extraJvm)) + ForkTests(frameworkMap.keys.toSeq, tests.toList, config, cp.map(_.data), runner, s.log) + case Tests.InProcess => + Tests(frameworkMap, loader, tests, config, noTestsMessage(scoped, name), s.log) + } + } + Tests.reduce(results) }, test <<= (executeTests, streams) map { (results, s) => Tests.showResults(s.log, results) }, testOnly <<= inputTests(testOnly), testQuick <<= inputTests(testQuick) ) - private[this] def noTestsMessage(scoped: ScopedKey[_])(implicit display: Show[ScopedKey[_]]): String = - "No tests to run for " + display(scoped) + private[this] def noTestsMessage(scoped: ScopedKey[_], group: String)(implicit display: Show[ScopedKey[_]]): String = + "No tests to run for group " + group + " in " + display(scoped) lazy val TaskGlobal: Scope = ThisScope.copy(task = Global) lazy val ConfigGlobal: Scope = ThisScope.copy(config = Global) @@ -305,7 +320,10 @@ object Defaults extends BuildCommon TestLogger(s.log, testLogger(sm, test in sco.scope), buff) +: new TestStatusReporter(succeededFile(dir)) +: ls }, testOptions <<= (testOptions in TaskGlobal, testListeners) map { (options, ls) => Tests.Listeners(ls) +: options }, - testExecution <<= testExecutionTask(key) + testExecution <<= testExecutionTask(key), + testGrouping <<= ((definedTests, testExecution) map { + (tests, exec) => Seq(new Tests.TestGroup("", tests, exec)) + }) ) ) def testLogger(manager: Streams, baseKey: Scoped)(tdef: TestDefinition): Logger = { @@ -322,7 +340,9 @@ object Defaults extends BuildCommon } def testExecutionTask(task: Scoped): Initialize[Task[Tests.Execution]] = - (testOptions in task, parallelExecution in task, tags in task) map { (opts, par, ts) => new Tests.Execution(opts, par, ts) } + (testOptions in task, parallelExecution in task, fork in task, javaOptions in task, tags in task) map { + (opts, par, fork, jvmOpts, ts) => new Tests.Execution(opts, par, if (fork) Tests.Fork(jvmOpts) else Tests.InProcess, ts) + } def testQuickFilter: Initialize[Task[Seq[String] => String => Boolean]] = (fullClasspath in test, cacheDirectory) map { @@ -354,18 +374,25 @@ object Defaults extends BuildCommon def inputTests(key: InputKey[_]): Initialize[InputTask[Unit]] = InputTask( loadForParser(definedTestNames)( (s, i) => testOnlyParser(s, i getOrElse Nil) ) ) { result => - (streams, loadedTestFrameworks, testFilter in key, testExecution in key, testLoader, definedTests, resolvedScoped, result, state) flatMap { - case (s, frameworks, filter, config, loader, discovered, scoped, (tests, frameworkOptions), st) => - val modifiedOpts = Tests.Filter(filter(tests)) +: Tests.Argument(frameworkOptions : _*) +: config.options - val newConfig = new Tests.Execution(modifiedOpts, config.parallel, config.tags) + (streams, loadedTestFrameworks, testFilter in key, testGrouping in key, testLoader, resolvedScoped, result, fullClasspath in key, forkOptions in key, state) flatMap { + case (s, frameworks, filter, groups, loader, scoped, (selected, frameworkOptions), cp, forkOpts, st) => implicit val display = Project.showContextKey(st) - Tests(frameworks, loader, discovered, newConfig, noTestsMessage(scoped), s.log) map { results => - Tests.showResults(s.log, results) - } + val results = groups map { + case Tests.TestGroup(name, tests, config) => + val modifiedOpts = Tests.Filter(filter(selected)) +: Tests.Argument(frameworkOptions : _*) +: config.options + val newConfig = config.copy(options = modifiedOpts) + newConfig.subproc match { + case Tests.Fork(extraJvm) => + val runner = new ForkRun(forkOpts.copy(runJVMOptions = forkOpts.runJVMOptions ++ extraJvm)) + ForkTests(frameworks.keys.toSeq, tests.toList, newConfig, cp.map(_.data), runner, s.log) + case Tests.InProcess => + Tests(frameworks, loader, tests, newConfig, noTestsMessage(scoped, name), s.log) + } + } + Tests.reduce(results) map (Tests.showResults(s.log, _)) } } - def selectedFilter(args: Seq[String]): String => Boolean = { val filters = args map GlobFilter.apply @@ -502,13 +529,8 @@ 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) + (taskTemporaryDirectory, scalaInstance, fork, trapExit, forkOptions) map { (tmp, si, forkRun, trap, forkOptions) => + if(forkRun) new ForkRun(forkOptions) else new Run(si, trap, tmp) } @deprecated("Use `docTaskSettings` instead", "0.12.0") diff --git a/main/Keys.scala b/main/Keys.scala index e235f8b42..82cb74ed4 100644 --- a/main/Keys.scala +++ b/main/Keys.scala @@ -179,13 +179,14 @@ object Keys val trapExit = SettingKey[Boolean]("trap-exit", "If true, enables exit trapping and thread management for 'run'-like tasks. This is currently only suitable for serially-executed 'run'-like tasks.") val fork = SettingKey[Boolean]("fork", "If true, forks a new JVM when running. If false, runs in the same JVM as the build.") + val forkOptions = TaskKey[ForkOptions]("fork-options", "Options for starting new JVM when forking.") val outputStrategy = SettingKey[Option[sbt.OutputStrategy]]("output-strategy", "Selects how to log output when running a main class.") val connectInput = SettingKey[Boolean]("connect-input", "If true, connects standard input when running a main class forked.") 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.") val javaOptions = TaskKey[Seq[String]]("java-options", "Options passed to a new JVM when forking.") // Test Keys - val testLoader = TaskKey[ClassLoader]("test-loader", "Provides the class loader used for testing.") + val testLoader = TaskKey[ClassLoader]("test-loader", "Provides the class loader used for testing in the same JVM.") val loadedTestFrameworks = TaskKey[Map[TestFramework,Framework]]("loaded-test-frameworks", "Loads Framework definitions from the test loader.") val definedTests = TaskKey[Seq[TestDefinition]]("defined-tests", "Provides the list of defined tests.") val definedTestNames = TaskKey[Seq[String]]("defined-test-names", "Provides the set of defined test names.") @@ -198,6 +199,7 @@ object Keys val testListeners = TaskKey[Seq[TestReportListener]]("test-listeners", "Defines test listeners.") val testExecution = TaskKey[Tests.Execution]("test-execution", "Settings controlling test execution") val testFilter = TaskKey[Seq[String] => String => Boolean]("test-filter", "Filter controlling whether the test is executed") + val testGrouping = TaskKey[Seq[Tests.TestGroup]]("test-grouping", "Groups discovered tests into groups. Groups are run sequentially.") val isModule = AttributeKey[Boolean]("is-module", "True if the target is a module.") // Classpath/Dependency Management Keys diff --git a/main/actions/ForkTests.scala b/main/actions/ForkTests.scala new file mode 100755 index 000000000..0b56d8fca --- /dev/null +++ b/main/actions/ForkTests.scala @@ -0,0 +1,106 @@ +/* sbt -- Simple Build Tool + * Copyright 2012 Eugene Vigdorchik + */ +package sbt + +import org.scalatools.testing._ +import java.net.ServerSocket +import java.io._ +import Tests._ +import ForkMain._ + +private[sbt] object ForkTests { + def apply(frameworks: Seq[TestFramework], tests: List[TestDefinition], config: Execution, classpath: Seq[File], runner: ScalaRun, log: Logger): Task[Output] = { + val opts = config.options.toList + val listeners = opts flatMap { + case Listeners(ls) => ls + case _ => List.empty + } + val testListeners = listeners flatMap { + case tl: TestsListener => Some(tl) + case _ => None + } + val filters = opts flatMap { + case Filter(f) => Some(f) + case _ => None + } + val argMap = frameworks.map { + f => f.implClassName -> opts.flatMap { + case Argument(None, args) => args + case Argument(Some(`f`), args) => args + case _ => List.empty + } + }.toMap + + std.TaskExtra.toTask { + val server = new ServerSocket(0) + object Acceptor extends Runnable { + val results = collection.mutable.Map.empty[String, TestResult.Value] + def output = (overall(results.values), results.toMap) + def run = { + val socketOpt = try { + Some(server.accept()) + } catch { + case e: IOException => None + } + for (socket <- socketOpt) { + val os = new ObjectOutputStream(socket.getOutputStream) + val is = new ObjectInputStream(socket.getInputStream) + + val testsFiltered = tests.filter(test => filters.forall(_(test.name))).map{ + t => new ForkTestDefinition(t.name, t.fingerprint) + }.toArray + os.writeObject(testsFiltered) + + os.writeInt(frameworks.size) + for ((clazz, args) <- argMap) { + os.writeObject(clazz) + os.writeObject(args.toArray) + } + + @annotation.tailrec def react: Unit = is.readObject match { + case `TestsDone` => os.writeObject(TestsDone); + case Array(`ErrorTag`, s: String) => log.error(s); react + case Array(`WarnTag`, s: String) => log.warn(s); react + case Array(`InfoTag`, s: String) => log.info(s); react + case Array(`DebugTag`, s: String) => log.debug(s); react + case t: Throwable => log.trace(t); react + case tEvents: Array[Event] => + for (first <- tEvents.headOption) listeners.foreach(_ startGroup first.testName) + val event = TestEvent(tEvents) + listeners.foreach(_ testEvent event) + for (first <- tEvents.headOption) { + val result = event.result getOrElse TestResult.Passed + results += first.testName -> result + listeners.foreach(_ endGroup (first.testName, result)) + } + react + } + react + } + } + } + + () => { + try { + testListeners.foreach(_.doInit()) + val t = new Thread(Acceptor) + t.start() + + val fullCp = classpath ++: Seq(IO.classLocationFile[ForkMain], IO.classLocationFile[Framework]) + for (msg <- runner.run(classOf[ForkMain].getCanonicalName, fullCp, Seq(server.getLocalPort.toString), log)) { + log.error(msg) + server.close() + } + + t.join() + val result = Acceptor.output + testListeners.foreach(_.doComplete(result._1)) + result + } finally { + if (!server.isClosed) server.close() + } + } + } + } +} diff --git a/main/actions/Tests.scala b/main/actions/Tests.scala index c6e19fec0..a568ab273 100644 --- a/main/actions/Tests.scala +++ b/main/actions/Tests.scala @@ -13,7 +13,6 @@ package sbt import org.scalatools.testing.{AnnotatedFingerprint, Fingerprint, Framework, SubclassFingerprint} - import collection.mutable import java.io.File sealed trait TestOption @@ -40,14 +39,15 @@ object Tests // None means apply to all, Some(tf) means apply to a particular framework only. final case class Argument(framework: Option[TestFramework], args: List[String]) extends TestOption - final class Execution(val options: Seq[TestOption], val parallel: Boolean, val tags: Seq[(Tag, Int)]) + sealed trait SubProcessPolicy + object InProcess extends SubProcessPolicy + final case class Fork(extraJvm: Seq[String]) extends SubProcessPolicy - def apply(frameworks: Map[TestFramework, Framework], testLoader: ClassLoader, discovered: Seq[TestDefinition], options: Seq[TestOption], parallel: Boolean, noTestsMessage: => String, log: Logger): Task[Output] = - apply(frameworks, testLoader, discovered, new Execution(options, parallel, Nil), noTestsMessage, log) + final case class Execution(options: Seq[TestOption], parallel: Boolean, subproc: SubProcessPolicy, tags: Seq[(Tag, Int)]) def apply(frameworks: Map[TestFramework, Framework], testLoader: ClassLoader, discovered: Seq[TestDefinition], config: Execution, noTestsMessage: => String, log: Logger): Task[Output] = { - import mutable.{HashSet, ListBuffer, Map, Set} + import collection.mutable.{HashSet, ListBuffer, Map, Set} val testFilters = new ListBuffer[String => Boolean] val excludeTestsSet = new HashSet[String] val setup, cleanup = new ListBuffer[ClassLoader => Unit] @@ -126,6 +126,10 @@ object Tests def processResults(results: Iterable[(String, TestResult.Value)]): (TestResult.Value, Map[String, TestResult.Value]) = (overall(results.map(_._2)), results.toMap) + def reduce(results: Seq[Task[Output]]): Task[Output] = + reduced(results.toIndexedSeq, { + case ((v1, m1), (v2, m2)) => (if (v1.id < v2.id) v2 else v1, m1 ++ m2) + }) def overall(results: Iterable[TestResult.Value]): TestResult.Value = (TestResult.Passed /: results) { (acc, result) => if(acc.id < result.id) result else acc } def discover(frameworks: Seq[Framework], analysis: Analysis, log: Logger): (Seq[TestDefinition], Set[String]) = @@ -177,4 +181,7 @@ object Tests if(!failures.isEmpty || !errors.isEmpty) error("Tests unsuccessful") } -} \ No newline at end of file + + final case class TestGroup(name: String, tests: Seq[TestDefinition], config: Execution) +} + diff --git a/project/Sbt.scala b/project/Sbt.scala index f3a48ab52..dd8eb83b5 100644 --- a/project/Sbt.scala +++ b/project/Sbt.scala @@ -72,8 +72,12 @@ object Sbt extends Build // Apache Ivy integration lazy val ivySub = baseProject(file("ivy"), "Ivy") dependsOn(interfaceSub, launchInterfaceSub, logSub % "compile;test->test", ioSub % "compile;test->test", launchSub % "test->test") settings(ivy, jsch, httpclient) - // Runner for uniform test interface - lazy val testingSub = baseProject(file("testing"), "Testing") dependsOn(ioSub, classpathSub, logSub) settings(libraryDependencies += "org.scala-tools.testing" % "test-interface" % "0.5") + // Runner for uniform test interface + lazy val testingSub = baseProject(file("testing"), "Testing") dependsOn(ioSub, classpathSub, logSub, launchInterfaceSub, testAgentSub) settings(libraryDependencies += "org.scala-tools.testing" % "test-interface" % "0.5") + // Testing agent for running tests in a separate process. + lazy val testAgentSub = project(file("testing/agent"), "Test Agent") settings( + libraryDependencies += "org.scala-tools.testing" % "test-interface" % "0.5" + ) // Basic task engine lazy val taskSub = testedBaseProject(tasksPath, "Tasks") dependsOn(controlSub, collectionSub) diff --git a/testing/TestFramework.scala b/testing/TestFramework.scala index 8c3dd6d1a..4e5883bad 100644 --- a/testing/TestFramework.scala +++ b/testing/TestFramework.scala @@ -23,14 +23,13 @@ object TestFrameworks val JUnit = new TestFramework("com.novocode.junit.JUnitFramework") } -class TestFramework(val implClassName: String) +case class TestFramework(val implClassName: String) { def create(loader: ClassLoader, log: Logger): Option[Framework] = { try { Some(Class.forName(implClassName, true, loader).newInstance.asInstanceOf[Framework]) } catch { case e: ClassNotFoundException => log.debug("Framework implementation '" + implClassName + "' not present."); None } } - override def toString = "TestFramework(" + implClassName + ")" } final class TestDefinition(val name: String, val fingerprint: Fingerprint) { diff --git a/testing/TestReportListener.scala b/testing/TestReportListener.scala index 542648cd8..d20dbbb76 100644 --- a/testing/TestReportListener.scala +++ b/testing/TestReportListener.scala @@ -23,7 +23,7 @@ trait TestReportListener trait TestsListener extends TestReportListener { /** called once, at beginning. */ - def doInit + def doInit() /** called once, at end. */ def doComplete(finalResult: TestResult.Value) } diff --git a/testing/agent/src/main/java/sbt/ForkMain.java b/testing/agent/src/main/java/sbt/ForkMain.java new file mode 100755 index 000000000..f935c20da --- /dev/null +++ b/testing/agent/src/main/java/sbt/ForkMain.java @@ -0,0 +1,151 @@ +/* sbt -- Simple Build Tool + * Copyright 2012 Eugene Vigdorchik + */ +package sbt; + +import org.scalatools.testing.*; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; +import java.net.Socket; +import java.net.InetAddress; +import java.util.ArrayList; +import java.util.List; + +public class ForkMain { + public static final String TestsDone = "TestsDone"; + public static final String ErrorTag = "[error]"; + public static final String WarnTag = "[warn]"; + public static final String InfoTag = "[info]"; + public static final String DebugTag = "[debug]"; + + static class SubclassFingerscan implements TestFingerprint, Serializable { + private boolean isModule; + private String superClassName; + SubclassFingerscan(SubclassFingerprint print) { + isModule = print.isModule(); + superClassName = print.superClassName(); + } + public boolean isModule() { return isModule; } + public String superClassName() { return superClassName; } + } + static class AnnotatedFingerscan implements AnnotatedFingerprint, Serializable { + private boolean isModule; + private String annotationName; + AnnotatedFingerscan(AnnotatedFingerprint print) { + isModule = print.isModule(); + annotationName = print.annotationName(); + } + public boolean isModule() { return isModule; } + public String annotationName() { return annotationName; } + } + public static class ForkTestDefinition implements Serializable { + public String name; + public Fingerprint fingerprint; + + public ForkTestDefinition(String name, Fingerprint fingerprint) { + this.name = name; + if (fingerprint instanceof SubclassFingerprint) { + this.fingerprint = new SubclassFingerscan((SubclassFingerprint) fingerprint); + } else { + this.fingerprint = new AnnotatedFingerscan((AnnotatedFingerprint) fingerprint); + } + } + } + static class ForkEvent implements Event, Serializable { + private String testName; + private String description; + private Result result; + ForkEvent(Event e) { + testName = e.testName(); + description = e.description(); + result = e.result(); + } + public String testName() { return testName; } + public String description() { return description; } + public Result result() { return result;} + public Throwable error() { return null; } + } + public static void main(String[] args) { + try { + Socket socket = new Socket(InetAddress.getByName(null), Integer.valueOf(args[0])); + final ObjectInputStream is = new ObjectInputStream(socket.getInputStream()); + final ObjectOutputStream os = new ObjectOutputStream(socket.getOutputStream()); + new Run().run(is, os); + } catch (Exception e) { + e.printStackTrace(); + } + } + private static class Run { + boolean matches(Fingerprint f1, Fingerprint f2) { + if (f1 instanceof SubclassFingerprint && f2 instanceof SubclassFingerprint) { + final SubclassFingerprint sf1 = (SubclassFingerprint) f1; + final SubclassFingerprint sf2 = (SubclassFingerprint) f2; + return sf1.isModule() == sf2.isModule() && sf1.superClassName().equals(sf2.superClassName()); + } else if (f1 instanceof AnnotatedFingerprint && f2 instanceof AnnotatedFingerprint) { + AnnotatedFingerprint af1 = (AnnotatedFingerprint) f1; + AnnotatedFingerprint af2 = (AnnotatedFingerprint) f2; + return af1.isModule() == af2.isModule() && af1.annotationName().equals(af2.annotationName()); + } + return false; + } + void run(ObjectInputStream is, final ObjectOutputStream os) throws Exception { + Logger[] loggers = { + new Logger() { + public boolean ansiCodesSupported() { return false; } + void print(Object obj) { + try { + os.writeObject(obj); + } catch (IOException e) { + System.err.println("Cannot write to socket"); + } + } + public void error(String s) { print(new String[]{ErrorTag, s}); } + public void warn(String s) { print(new String[]{WarnTag, s}); } + public void info(String s) { print(new String[]{InfoTag, s}); } + public void debug(String s) { print(new String[]{DebugTag, s}); } + public void trace(Throwable t) { print(t); } + } + }; + + final ForkTestDefinition[] tests = (ForkTestDefinition[]) is.readObject(); + int nFrameworks = is.readInt(); + for (int i = 0; i < nFrameworks; i++) { + final Framework framework; + final String implClassName = (String) is.readObject(); + try { + framework = (Framework) Class.forName(implClassName).newInstance(); + } catch (ClassNotFoundException e) { + System.err.println("Framework implementation '" + implClassName + "' not present."); + continue; + } + + final String[] frameworkArgs = (String[]) is.readObject(); + + ArrayList filteredTests = new ArrayList(); + for (Fingerprint testFingerprint : framework.tests()) { + for (ForkTestDefinition test : tests) { + if (matches(testFingerprint, test.fingerprint)) filteredTests.add(test); + } + } + final org.scalatools.testing.Runner runner = framework.testRunner(getClass().getClassLoader(), loggers); + for (ForkTestDefinition test : filteredTests) { + final List events = new ArrayList(); + EventHandler handler = new EventHandler() { public void handle(Event e){ events.add(new ForkEvent(e)); } }; + if (runner instanceof Runner2) { + ((Runner2) runner).run(test.name, test.fingerprint, handler, frameworkArgs); + } else if (test.fingerprint instanceof TestFingerprint) { + runner.run(test.name, (TestFingerprint) test.fingerprint, handler, frameworkArgs); + } else { + System.err.println("Framework '" + framework + "' does not support test '" + test.name + "'"); + } + os.writeObject(events.toArray(new ForkEvent[events.size()])); + } + } + os.writeObject(TestsDone); + is.readObject(); + } + } +} diff --git a/util/io/IPC.scala b/util/io/IPC.scala index d0ccf7b03..2cad6106f 100644 --- a/util/io/IPC.scala +++ b/util/io/IPC.scala @@ -69,4 +69,4 @@ final class IPC private(s: Socket) extends NotNull def send(s: String) = { out.write(s); out.newLine(); out.flush() } def receive: String = in.readLine() -} \ No newline at end of file +} From 68c5cbe438834cb1d9347b1d1ac4e66c5cf888ac Mon Sep 17 00:00:00 2001 From: Eugene Vigdorchik Date: Sun, 1 Apr 2012 15:49:29 +0400 Subject: [PATCH 02/15] Fork java, not scala. --- main/Defaults.scala | 30 ++++++++++++++---------------- main/Keys.scala | 1 - main/actions/ForkTests.scala | 8 +++++--- 3 files changed, 19 insertions(+), 20 deletions(-) diff --git a/main/Defaults.scala b/main/Defaults.scala index 5ace1c061..dc3198943 100755 --- a/main/Defaults.scala +++ b/main/Defaults.scala @@ -125,12 +125,7 @@ object Defaults extends BuildCommon name <<= thisProject(_.id), logManager <<= extraLoggers(LogManager.defaults), onLoadMessage <<= onLoadMessage or (name, thisProjectRef)("Set current project to " + _ + " (in build " + _.build +")"), - runnerTask, - forkOptions <<= (scalaInstance, baseDirectory, javaOptions, outputStrategy, javaHome, connectInput) map { - (si, base, options, strategy, javaHomeDir, connectIn) => - ForkOptions(scalaJars = si.jars, javaHome = javaHomeDir, connectInput = connectIn, outputStrategy = strategy, - runJVMOptions = options, workingDirectory = Some(base)) - } + runnerTask ) def paths = Seq( baseDirectory <<= thisProject(_.base), @@ -291,15 +286,14 @@ object Defaults extends BuildCommon testOptions in GlobalScope :== Nil, testFilter in testOnly :== (selectedFilter _), testFilter in testQuick <<= testQuickFilter, - executeTests <<= (streams in test, loadedTestFrameworks, testLoader, testGrouping in test, fullClasspath in test, forkOptions in test, resolvedScoped, state) flatMap { - (s, frameworkMap, loader, groups, cp, forkOpts, scoped, st) => + executeTests <<= (streams in test, loadedTestFrameworks, testLoader, testGrouping in test, fullClasspath in test, javaOptions in test, javaHome, resolvedScoped, state) flatMap { + (s, frameworkMap, loader, groups, cp, javaOpts, javaHome, scoped, st) => implicit val display = Project.showContextKey(st) val results = groups map { case Tests.TestGroup(name, tests, config) => config.subproc match { case Tests.Fork(extraJvm) => - val runner = new ForkRun(forkOpts.copy(runJVMOptions = forkOpts.runJVMOptions ++ extraJvm)) - ForkTests(frameworkMap.keys.toSeq, tests.toList, config, cp.map(_.data), runner, s.log) + ForkTests(frameworkMap.keys.toSeq, tests.toList, config, cp.map(_.data), javaHome, javaOpts, s.log) case Tests.InProcess => Tests(frameworkMap, loader, tests, config, noTestsMessage(scoped, name), s.log) } @@ -374,8 +368,8 @@ object Defaults extends BuildCommon def inputTests(key: InputKey[_]): Initialize[InputTask[Unit]] = InputTask( loadForParser(definedTestNames)( (s, i) => testOnlyParser(s, i getOrElse Nil) ) ) { result => - (streams, loadedTestFrameworks, testFilter in key, testGrouping in key, testLoader, resolvedScoped, result, fullClasspath in key, forkOptions in key, state) flatMap { - case (s, frameworks, filter, groups, loader, scoped, (selected, frameworkOptions), cp, forkOpts, st) => + (streams, loadedTestFrameworks, testFilter in key, testGrouping in key, testLoader, resolvedScoped, result, fullClasspath in key, javaOptions in key, javaHome, state) flatMap { + case (s, frameworks, filter, groups, loader, scoped, (selected, frameworkOptions), cp, javaOpts, javaHome, st) => implicit val display = Project.showContextKey(st) val results = groups map { case Tests.TestGroup(name, tests, config) => @@ -383,8 +377,7 @@ object Defaults extends BuildCommon val newConfig = config.copy(options = modifiedOpts) newConfig.subproc match { case Tests.Fork(extraJvm) => - val runner = new ForkRun(forkOpts.copy(runJVMOptions = forkOpts.runJVMOptions ++ extraJvm)) - ForkTests(frameworks.keys.toSeq, tests.toList, newConfig, cp.map(_.data), runner, s.log) + ForkTests(frameworks.keys.toSeq, tests.toList, newConfig, cp.map(_.data), javaHome, javaOpts, s.log) case Tests.InProcess => Tests(frameworks, loader, tests, newConfig, noTestsMessage(scoped, name), s.log) } @@ -529,8 +522,13 @@ object Defaults extends BuildCommon def runnerTask = runner <<= runnerInit def runnerInit: Initialize[Task[ScalaRun]] = - (taskTemporaryDirectory, scalaInstance, fork, trapExit, forkOptions) map { (tmp, si, forkRun, trap, forkOptions) => - if(forkRun) new ForkRun(forkOptions) else new Run(si, trap, tmp) + (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) } @deprecated("Use `docTaskSettings` instead", "0.12.0") diff --git a/main/Keys.scala b/main/Keys.scala index 82cb74ed4..0a9ce289f 100644 --- a/main/Keys.scala +++ b/main/Keys.scala @@ -179,7 +179,6 @@ object Keys val trapExit = SettingKey[Boolean]("trap-exit", "If true, enables exit trapping and thread management for 'run'-like tasks. This is currently only suitable for serially-executed 'run'-like tasks.") val fork = SettingKey[Boolean]("fork", "If true, forks a new JVM when running. If false, runs in the same JVM as the build.") - val forkOptions = TaskKey[ForkOptions]("fork-options", "Options for starting new JVM when forking.") val outputStrategy = SettingKey[Option[sbt.OutputStrategy]]("output-strategy", "Selects how to log output when running a main class.") val connectInput = SettingKey[Boolean]("connect-input", "If true, connects standard input when running a main class forked.") 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.") diff --git a/main/actions/ForkTests.scala b/main/actions/ForkTests.scala index 0b56d8fca..fd892879b 100755 --- a/main/actions/ForkTests.scala +++ b/main/actions/ForkTests.scala @@ -10,7 +10,7 @@ import Tests._ import ForkMain._ private[sbt] object ForkTests { - def apply(frameworks: Seq[TestFramework], tests: List[TestDefinition], config: Execution, classpath: Seq[File], runner: ScalaRun, log: Logger): Task[Output] = { + def apply(frameworks: Seq[TestFramework], tests: List[TestDefinition], config: Execution, classpath: Seq[File], javaHome: Option[File], javaOpts: Seq[String], log: Logger): Task[Output] = { val opts = config.options.toList val listeners = opts flatMap { case Listeners(ls) => ls @@ -88,8 +88,10 @@ private[sbt] object ForkTests { t.start() val fullCp = classpath ++: Seq(IO.classLocationFile[ForkMain], IO.classLocationFile[Framework]) - for (msg <- runner.run(classOf[ForkMain].getCanonicalName, fullCp, Seq(server.getLocalPort.toString), log)) { - log.error(msg) + val options = javaOpts ++: Seq("-classpath", fullCp mkString File.pathSeparator, classOf[ForkMain].getCanonicalName, server.getLocalPort.toString) + val ec = Fork.java(javaHome, options, StdoutOutput) + if (ec != 0) { + log.error("Running java with options " + options.mkString(" ") + " failed with exit code " + ec) server.close() } From e3a7a331d5e66043c4e364047cad528069ffbf35 Mon Sep 17 00:00:00 2001 From: Eugene Vigdorchik Date: Mon, 2 Apr 2012 10:46:13 +0400 Subject: [PATCH 03/15] Changes after review, step 1. --- main/Defaults.scala | 8 ++++---- main/actions/ForkTests.scala | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/main/Defaults.scala b/main/Defaults.scala index dc3198943..7061ba19a 100755 --- a/main/Defaults.scala +++ b/main/Defaults.scala @@ -286,14 +286,14 @@ object Defaults extends BuildCommon testOptions in GlobalScope :== Nil, testFilter in testOnly :== (selectedFilter _), testFilter in testQuick <<= testQuickFilter, - executeTests <<= (streams in test, loadedTestFrameworks, testLoader, testGrouping in test, fullClasspath in test, javaOptions in test, javaHome, resolvedScoped, state) flatMap { + executeTests <<= (streams in test, loadedTestFrameworks, testLoader, testGrouping in test, fullClasspath in test, javaOptions in test, javaHome in test, resolvedScoped, state) flatMap { (s, frameworkMap, loader, groups, cp, javaOpts, javaHome, scoped, st) => implicit val display = Project.showContextKey(st) val results = groups map { case Tests.TestGroup(name, tests, config) => config.subproc match { case Tests.Fork(extraJvm) => - ForkTests(frameworkMap.keys.toSeq, tests.toList, config, cp.map(_.data), javaHome, javaOpts, s.log) + ForkTests(frameworkMap.keys.toSeq, tests.toList, config, cp.files, javaHome, javaOpts, s.log) case Tests.InProcess => Tests(frameworkMap, loader, tests, config, noTestsMessage(scoped, name), s.log) } @@ -368,7 +368,7 @@ object Defaults extends BuildCommon def inputTests(key: InputKey[_]): Initialize[InputTask[Unit]] = InputTask( loadForParser(definedTestNames)( (s, i) => testOnlyParser(s, i getOrElse Nil) ) ) { result => - (streams, loadedTestFrameworks, testFilter in key, testGrouping in key, testLoader, resolvedScoped, result, fullClasspath in key, javaOptions in key, javaHome, state) flatMap { + (streams, loadedTestFrameworks, testFilter in key, testGrouping in key, testLoader, resolvedScoped, result, fullClasspath in key, javaOptions in key, javaHome in key, state) flatMap { case (s, frameworks, filter, groups, loader, scoped, (selected, frameworkOptions), cp, javaOpts, javaHome, st) => implicit val display = Project.showContextKey(st) val results = groups map { @@ -377,7 +377,7 @@ object Defaults extends BuildCommon val newConfig = config.copy(options = modifiedOpts) newConfig.subproc match { case Tests.Fork(extraJvm) => - ForkTests(frameworks.keys.toSeq, tests.toList, newConfig, cp.map(_.data), javaHome, javaOpts, s.log) + ForkTests(frameworks.keys.toSeq, tests.toList, newConfig, cp.files, javaHome, javaOpts, s.log) case Tests.InProcess => Tests(frameworks, loader, tests, newConfig, noTestsMessage(scoped, name), s.log) } diff --git a/main/actions/ForkTests.scala b/main/actions/ForkTests.scala index fd892879b..09bf8117e 100755 --- a/main/actions/ForkTests.scala +++ b/main/actions/ForkTests.scala @@ -100,7 +100,7 @@ private[sbt] object ForkTests { testListeners.foreach(_.doComplete(result._1)) result } finally { - if (!server.isClosed) server.close() + server.close() } } } From 4d5effcb28c5094150c9148754f002ae74bb920e Mon Sep 17 00:00:00 2001 From: Eugene Vigdorchik Date: Mon, 2 Apr 2012 12:08:18 +0400 Subject: [PATCH 04/15] Fixes after review, take 2. --- main/Defaults.scala | 29 ++++++----- main/Keys.scala | 2 +- main/actions/ForkTests.scala | 11 +++-- main/actions/Tests.scala | 48 ++++++++++--------- testing/TestFramework.scala | 3 +- testing/agent/src/main/java/sbt/ForkMain.java | 22 ++++----- 6 files changed, 60 insertions(+), 55 deletions(-) diff --git a/main/Defaults.scala b/main/Defaults.scala index 7061ba19a..74340121b 100755 --- a/main/Defaults.scala +++ b/main/Defaults.scala @@ -286,26 +286,29 @@ object Defaults extends BuildCommon testOptions in GlobalScope :== Nil, testFilter in testOnly :== (selectedFilter _), testFilter in testQuick <<= testQuickFilter, - executeTests <<= (streams in test, loadedTestFrameworks, testLoader, testGrouping in test, fullClasspath in test, javaOptions in test, javaHome in test, resolvedScoped, state) flatMap { - (s, frameworkMap, loader, groups, cp, javaOpts, javaHome, scoped, st) => - implicit val display = Project.showContextKey(st) + executeTests <<= (streams in test, loadedTestFrameworks, testLoader, testGrouping in test, fullClasspath in test, javaOptions in test, javaHome in test) flatMap { + (s, frameworkMap, loader, groups, cp, javaOpts, javaHome) => val results = groups map { - case Tests.TestGroup(name, tests, config) => + case Tests.Group(name, tests, config) => config.subproc match { case Tests.Fork(extraJvm) => ForkTests(frameworkMap.keys.toSeq, tests.toList, config, cp.files, javaHome, javaOpts, s.log) case Tests.InProcess => - Tests(frameworkMap, loader, tests, config, noTestsMessage(scoped, name), s.log) + Tests(frameworkMap, loader, tests, config, s.log) } } - Tests.reduce(results) + Tests.flatten(results) + }, + test <<= (executeTests, streams, resolvedScoped, state) map { + (results, s, scoped, st) => + implicit val display = Project.showContextKey(st) + Tests.showResults(s.log, results, noTestsMessage(scoped)) }, - test <<= (executeTests, streams) map { (results, s) => Tests.showResults(s.log, results) }, testOnly <<= inputTests(testOnly), testQuick <<= inputTests(testQuick) ) - private[this] def noTestsMessage(scoped: ScopedKey[_], group: String)(implicit display: Show[ScopedKey[_]]): String = - "No tests to run for group " + group + " in " + display(scoped) + private[this] def noTestsMessage(scoped: ScopedKey[_])(implicit display: Show[ScopedKey[_]]): String = + "No tests to run for " + display(scoped) lazy val TaskGlobal: Scope = ThisScope.copy(task = Global) lazy val ConfigGlobal: Scope = ThisScope.copy(config = Global) @@ -316,7 +319,7 @@ object Defaults extends BuildCommon testOptions <<= (testOptions in TaskGlobal, testListeners) map { (options, ls) => Tests.Listeners(ls) +: options }, testExecution <<= testExecutionTask(key), testGrouping <<= ((definedTests, testExecution) map { - (tests, exec) => Seq(new Tests.TestGroup("", tests, exec)) + (tests, exec) => Seq(new Tests.Group("", tests, exec)) }) ) ) def testLogger(manager: Streams, baseKey: Scoped)(tdef: TestDefinition): Logger = @@ -372,17 +375,17 @@ object Defaults extends BuildCommon case (s, frameworks, filter, groups, loader, scoped, (selected, frameworkOptions), cp, javaOpts, javaHome, st) => implicit val display = Project.showContextKey(st) val results = groups map { - case Tests.TestGroup(name, tests, config) => + case Tests.Group(name, tests, config) => val modifiedOpts = Tests.Filter(filter(selected)) +: Tests.Argument(frameworkOptions : _*) +: config.options val newConfig = config.copy(options = modifiedOpts) newConfig.subproc match { case Tests.Fork(extraJvm) => ForkTests(frameworks.keys.toSeq, tests.toList, newConfig, cp.files, javaHome, javaOpts, s.log) case Tests.InProcess => - Tests(frameworks, loader, tests, newConfig, noTestsMessage(scoped, name), s.log) + Tests(frameworks, loader, tests, newConfig, s.log) } } - Tests.reduce(results) map (Tests.showResults(s.log, _)) + Tests.flatten(results) map (Tests.showResults(s.log, _, noTestsMessage(scoped))) } } diff --git a/main/Keys.scala b/main/Keys.scala index 0a9ce289f..8c41696dd 100644 --- a/main/Keys.scala +++ b/main/Keys.scala @@ -198,7 +198,7 @@ object Keys val testListeners = TaskKey[Seq[TestReportListener]]("test-listeners", "Defines test listeners.") val testExecution = TaskKey[Tests.Execution]("test-execution", "Settings controlling test execution") val testFilter = TaskKey[Seq[String] => String => Boolean]("test-filter", "Filter controlling whether the test is executed") - val testGrouping = TaskKey[Seq[Tests.TestGroup]]("test-grouping", "Groups discovered tests into groups. Groups are run sequentially.") + val testGrouping = TaskKey[Seq[Tests.Group]]("test-grouping", "Groups discovered tests into groups. Groups are run sequentially.") val isModule = AttributeKey[Boolean]("is-module", "True if the target is a module.") // Classpath/Dependency Management Keys diff --git a/main/actions/ForkTests.scala b/main/actions/ForkTests.scala index 09bf8117e..64371b815 100755 --- a/main/actions/ForkTests.scala +++ b/main/actions/ForkTests.scala @@ -58,12 +58,13 @@ private[sbt] object ForkTests { os.writeObject(args.toArray) } + import Tags._ @annotation.tailrec def react: Unit = is.readObject match { - case `TestsDone` => os.writeObject(TestsDone); - case Array(`ErrorTag`, s: String) => log.error(s); react - case Array(`WarnTag`, s: String) => log.warn(s); react - case Array(`InfoTag`, s: String) => log.info(s); react - case Array(`DebugTag`, s: String) => log.debug(s); react + case `Done` => os.writeObject(Done); + case Array(`Error`, s: String) => log.error(s); react + case Array(`Warn`, s: String) => log.warn(s); react + case Array(`Info`, s: String) => log.info(s); react + case Array(`Debug`, s: String) => log.debug(s); react case t: Throwable => log.trace(t); react case tEvents: Array[Event] => for (first <- tEvents.headOption) listeners.foreach(_ startGroup first.testName) diff --git a/main/actions/Tests.scala b/main/actions/Tests.scala index a568ab273..9b8013629 100644 --- a/main/actions/Tests.scala +++ b/main/actions/Tests.scala @@ -45,7 +45,7 @@ object Tests final case class Execution(options: Seq[TestOption], parallel: Boolean, subproc: SubProcessPolicy, tags: Seq[(Tag, Int)]) - def apply(frameworks: Map[TestFramework, Framework], testLoader: ClassLoader, discovered: Seq[TestDefinition], config: Execution, noTestsMessage: => String, log: Logger): Task[Output] = + def apply(frameworks: Map[TestFramework, Framework], testLoader: ClassLoader, discovered: Seq[TestDefinition], config: Execution, log: Logger): Task[Output] = { import collection.mutable.{HashSet, ListBuffer, Map, Set} val testFilters = new ListBuffer[String => Boolean] @@ -93,10 +93,10 @@ object Tests def includeTest(test: TestDefinition) = !excludeTestsSet.contains(test.name) && testFilters.forall(filter => filter(test.name)) val tests = discovered.filter(includeTest).toSet.toSeq val arguments = testArgsByFramework.map { case (k,v) => (k, v.toList) } toMap; - testTask(frameworks.values.toSeq, testLoader, tests, noTestsMessage, setup.readOnly, cleanup.readOnly, log, testListeners.readOnly, arguments, config) + testTask(frameworks.values.toSeq, testLoader, tests, setup.readOnly, cleanup.readOnly, log, testListeners.readOnly, arguments, config) } - def testTask(frameworks: Seq[Framework], loader: ClassLoader, tests: Seq[TestDefinition], noTestsMessage: => String, + def testTask(frameworks: Seq[Framework], loader: ClassLoader, tests: Seq[TestDefinition], userSetup: Iterable[ClassLoader => Unit], userCleanup: Iterable[ClassLoader => Unit], log: Logger, testListeners: Seq[TestReportListener], arguments: Map[Framework, Seq[String]], config: Execution): Task[Output] = { @@ -104,7 +104,7 @@ object Tests def partApp(actions: Iterable[ClassLoader => Unit]) = actions.toSeq map {a => () => a(loader) } val (frameworkSetup, runnables, frameworkCleanup) = - TestFramework.testTasks(frameworks, loader, tests, noTestsMessage, log, testListeners, arguments) + TestFramework.testTasks(frameworks, loader, tests, log, testListeners, arguments) val setupTasks = fj(partApp(userSetup) :+ frameworkSetup) val mainTasks = @@ -126,7 +126,7 @@ object Tests def processResults(results: Iterable[(String, TestResult.Value)]): (TestResult.Value, Map[String, TestResult.Value]) = (overall(results.map(_._2)), results.toMap) - def reduce(results: Seq[Task[Output]]): Task[Output] = + def flatten(results: Seq[Task[Output]]): Task[Output] = reduced(results.toIndexedSeq, { case ((v1, m1), (v2, m2)) => (if (v1.id < v2.id) v2 else v1, m1 ++ m2) }) @@ -157,31 +157,35 @@ object Tests (tests, mains.toSet) } - def showResults(log: Logger, results: (TestResult.Value, Map[String, TestResult.Value])): Unit = + def showResults(log: Logger, results: (TestResult.Value, Map[String, TestResult.Value]), noTestsMessage: =>String): Unit = { + if (results._2.isEmpty) + log.info(noTestsMessage) + else { import TestResult.{Error, Failed, Passed} - def select(Tpe: TestResult.Value) = results._2 collect { case (name, Tpe) => name } + def select(Tpe: TestResult.Value) = results._2 collect { case (name, Tpe) => name } - val failures = select(Failed) - val errors = select(Error) - val passed = select(Passed) + val failures = select(Failed) + val errors = select(Error) + val passed = select(Passed) - def show(label: String, level: Level.Value, tests: Iterable[String]): Unit = - if(!tests.isEmpty) - { - log.log(level, label) - log.log(level, tests.mkString("\t", "\n\t", "")) - } + def show(label: String, level: Level.Value, tests: Iterable[String]): Unit = + if(!tests.isEmpty) + { + log.log(level, label) + log.log(level, tests.mkString("\t", "\n\t", "")) + } - show("Passed tests:", Level.Debug, passed ) - show("Failed tests:", Level.Error, failures) - show("Error during tests:", Level.Error, errors) + show("Passed tests:", Level.Debug, passed ) + show("Failed tests:", Level.Error, failures) + show("Error during tests:", Level.Error, errors) - if(!failures.isEmpty || !errors.isEmpty) - error("Tests unsuccessful") + if(!failures.isEmpty || !errors.isEmpty) + error("Tests unsuccessful") + } } - final case class TestGroup(name: String, tests: Seq[TestDefinition], config: Execution) + final case class Group(name: String, tests: Seq[TestDefinition], config: Execution) } diff --git a/testing/TestFramework.scala b/testing/TestFramework.scala index 4e5883bad..2261f497e 100644 --- a/testing/TestFramework.scala +++ b/testing/TestFramework.scala @@ -129,7 +129,6 @@ object TestFramework def testTasks(frameworks: Seq[Framework], testLoader: ClassLoader, tests: Seq[TestDefinition], - noTestsMessage: => String, log: Logger, listeners: Seq[TestReportListener], testArgsByFramework: Map[Framework, Seq[String]]): @@ -138,7 +137,7 @@ object TestFramework val arguments = testArgsByFramework withDefaultValue Nil val mappedTests = testMap(frameworks, tests, arguments) if(mappedTests.isEmpty) - (() => (), Nil, _ => () => log.info(noTestsMessage) ) + (() => (), Nil, _ => () => () ) else createTestTasks(testLoader, mappedTests, log, listeners) } diff --git a/testing/agent/src/main/java/sbt/ForkMain.java b/testing/agent/src/main/java/sbt/ForkMain.java index f935c20da..6112cd6c2 100755 --- a/testing/agent/src/main/java/sbt/ForkMain.java +++ b/testing/agent/src/main/java/sbt/ForkMain.java @@ -15,11 +15,9 @@ import java.util.ArrayList; import java.util.List; public class ForkMain { - public static final String TestsDone = "TestsDone"; - public static final String ErrorTag = "[error]"; - public static final String WarnTag = "[warn]"; - public static final String InfoTag = "[info]"; - public static final String DebugTag = "[debug]"; + public static enum Tags { + Error, Warn, Info, Debug, Done; + } static class SubclassFingerscan implements TestFingerprint, Serializable { private boolean isModule; @@ -95,18 +93,18 @@ public class ForkMain { Logger[] loggers = { new Logger() { public boolean ansiCodesSupported() { return false; } - void print(Object obj) { + void write(Object obj) { try { os.writeObject(obj); } catch (IOException e) { System.err.println("Cannot write to socket"); } } - public void error(String s) { print(new String[]{ErrorTag, s}); } - public void warn(String s) { print(new String[]{WarnTag, s}); } - public void info(String s) { print(new String[]{InfoTag, s}); } - public void debug(String s) { print(new String[]{DebugTag, s}); } - public void trace(Throwable t) { print(t); } + public void error(String s) { write(new Object[]{Tags.Error, s}); } + public void warn(String s) { write(new Object[]{Tags.Warn, s}); } + public void info(String s) { write(new Object[]{Tags.Info, s}); } + public void debug(String s) { write(new Object[]{Tags.Debug, s}); } + public void trace(Throwable t) { write(t); } } }; @@ -144,7 +142,7 @@ public class ForkMain { os.writeObject(events.toArray(new ForkEvent[events.size()])); } } - os.writeObject(TestsDone); + os.writeObject(Tags.Done); is.readObject(); } } From 2741515d441a3c56a29f2991ba660540b9f1843c Mon Sep 17 00:00:00 2001 From: Eugene Vigdorchik Date: Mon, 2 Apr 2012 16:05:56 +0400 Subject: [PATCH 05/15] Fixes after review, take 3. --- main/Defaults.scala | 4 +- main/actions/ForkTests.scala | 70 +++++++++---------- main/actions/Tests.scala | 10 +-- testing/agent/src/main/java/sbt/ForkMain.java | 5 +- 4 files changed, 46 insertions(+), 43 deletions(-) diff --git a/main/Defaults.scala b/main/Defaults.scala index 74340121b..00ec1437a 100755 --- a/main/Defaults.scala +++ b/main/Defaults.scala @@ -297,7 +297,7 @@ object Defaults extends BuildCommon Tests(frameworkMap, loader, tests, config, s.log) } } - Tests.flatten(results) + Tests.foldTasks(results) }, test <<= (executeTests, streams, resolvedScoped, state) map { (results, s, scoped, st) => @@ -385,7 +385,7 @@ object Defaults extends BuildCommon Tests(frameworks, loader, tests, newConfig, s.log) } } - Tests.flatten(results) map (Tests.showResults(s.log, _, noTestsMessage(scoped))) + Tests.foldTasks(results) map (Tests.showResults(s.log, _, noTestsMessage(scoped))) } } diff --git a/main/actions/ForkTests.scala b/main/actions/ForkTests.scala index 64371b815..44bdb796d 100755 --- a/main/actions/ForkTests.scala +++ b/main/actions/ForkTests.scala @@ -14,7 +14,7 @@ private[sbt] object ForkTests { val opts = config.options.toList val listeners = opts flatMap { case Listeners(ls) => ls - case _ => List.empty + case _ => Nil } val testListeners = listeners flatMap { case tl: TestsListener => Some(tl) @@ -28,11 +28,11 @@ private[sbt] object ForkTests { f => f.implClassName -> opts.flatMap { case Argument(None, args) => args case Argument(Some(`f`), args) => args - case _ => List.empty + case _ => Nil } }.toMap - std.TaskExtra.toTask { + std.TaskExtra.task { val server = new ServerSocket(0) object Acceptor extends Runnable { val results = collection.mutable.Map.empty[String, TestResult.Value] @@ -47,17 +47,6 @@ private[sbt] object ForkTests { val os = new ObjectOutputStream(socket.getOutputStream) val is = new ObjectInputStream(socket.getInputStream) - val testsFiltered = tests.filter(test => filters.forall(_(test.name))).map{ - t => new ForkTestDefinition(t.name, t.fingerprint) - }.toArray - os.writeObject(testsFiltered) - - os.writeInt(frameworks.size) - for ((clazz, args) <- argMap) { - os.writeObject(clazz) - os.writeObject(args.toArray) - } - import Tags._ @annotation.tailrec def react: Unit = is.readObject match { case `Done` => os.writeObject(Done); @@ -77,33 +66,44 @@ private[sbt] object ForkTests { } react } - react + + try { + os.writeBoolean(log.ansiCodesSupported) + + val testsFiltered = tests.filter(test => filters.forall(_(test.name))).map{ + t => new ForkTestDefinition(t.name, t.fingerprint) + }.toArray + os.writeObject(testsFiltered) + + os.writeInt(frameworks.size) + for ((clazz, args) <- argMap) { + os.writeObject(clazz) + os.writeObject(args.toArray) + } + + react + } finally { + is.close() + os.close() + } } } } - () => { - try { - testListeners.foreach(_.doInit()) - val t = new Thread(Acceptor) - t.start() + testListeners.foreach(_.doInit()) + try { + 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) - if (ec != 0) { - log.error("Running java with options " + options.mkString(" ") + " failed with exit code " + ec) - server.close() - } - - t.join() - val result = Acceptor.output - testListeners.foreach(_.doComplete(result._1)) - result - } finally { - server.close() - } + 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) + if (ec != 0) log.error("Running java with options " + options.mkString(" ") + " failed with exit code " + ec) + } finally { + server.close() } + val result = Acceptor.output + testListeners.foreach(_.doComplete(result._1)) + result } } } diff --git a/main/actions/Tests.scala b/main/actions/Tests.scala index 9b8013629..5406d7d68 100644 --- a/main/actions/Tests.scala +++ b/main/actions/Tests.scala @@ -126,10 +126,12 @@ object Tests def processResults(results: Iterable[(String, TestResult.Value)]): (TestResult.Value, Map[String, TestResult.Value]) = (overall(results.map(_._2)), results.toMap) - def flatten(results: Seq[Task[Output]]): Task[Output] = - reduced(results.toIndexedSeq, { - case ((v1, m1), (v2, m2)) => (if (v1.id < v2.id) v2 else v1, m1 ++ m2) - }) + def foldTasks(results: Seq[Task[Output]]): Task[Output] = + (task((TestResult.Passed, Map.empty[String, TestResult.Value])) /: results) { + reducePair(_, _, { + case ((v1, m1), (v2, m2)) => (if (v1.id < v2.id) v2 else v1, m1 ++ m2) + }) + } def overall(results: Iterable[TestResult.Value]): TestResult.Value = (TestResult.Passed /: results) { (acc, result) => if(acc.id < result.id) result else acc } def discover(frameworks: Seq[Framework], analysis: Analysis, log: Logger): (Seq[TestDefinition], Set[String]) = diff --git a/testing/agent/src/main/java/sbt/ForkMain.java b/testing/agent/src/main/java/sbt/ForkMain.java index 6112cd6c2..f109aa860 100755 --- a/testing/agent/src/main/java/sbt/ForkMain.java +++ b/testing/agent/src/main/java/sbt/ForkMain.java @@ -90,9 +90,10 @@ public class ForkMain { return false; } void run(ObjectInputStream is, final ObjectOutputStream os) throws Exception { + final boolean ansiCodesSupported = is.readBoolean(); Logger[] loggers = { new Logger() { - public boolean ansiCodesSupported() { return false; } + public boolean ansiCodesSupported() { return ansiCodesSupported; } void write(Object obj) { try { os.writeObject(obj); @@ -100,7 +101,7 @@ public class ForkMain { System.err.println("Cannot write to socket"); } } - public void error(String s) { write(new Object[]{Tags.Error, s}); } + public void error(String s) { write(new Object[]{Tags.Error, s}); } public void warn(String s) { write(new Object[]{Tags.Warn, s}); } public void info(String s) { write(new Object[]{Tags.Info, s}); } public void debug(String s) { write(new Object[]{Tags.Debug, s}); } From ce0a2a3625faa105213230507093bc37ca537945 Mon Sep 17 00:00:00 2001 From: Eugene Vigdorchik Date: Mon, 2 Apr 2012 19:01:54 +0400 Subject: [PATCH 06/15] More fixes. --- main/Defaults.scala | 10 +++++----- main/Keys.scala | 2 +- main/Tags.scala | 3 ++- main/actions/ForkTests.scala | 4 ++-- main/actions/Tests.scala | 8 +++----- 5 files changed, 13 insertions(+), 14 deletions(-) diff --git a/main/Defaults.scala b/main/Defaults.scala index 00ec1437a..3e0550ead 100755 --- a/main/Defaults.scala +++ b/main/Defaults.scala @@ -118,7 +118,7 @@ object Defaults extends BuildCommon pomIncludeRepository :== Classpaths.defaultRepositoryFilter )) def defaultTestTasks(key: Scoped): Seq[Setting[_]] = Seq( - tags in key := Seq(Tags.Test -> 1), + tags in key := Seq(Tags.Test -> 1, Tags.Subprocess -> 1), logBuffered in key := true ) def projectCore: Seq[Setting[_]] = Seq( @@ -288,7 +288,7 @@ object Defaults extends BuildCommon testFilter in testQuick <<= testQuickFilter, executeTests <<= (streams in test, loadedTestFrameworks, testLoader, testGrouping in test, fullClasspath in test, javaOptions in test, javaHome in test) flatMap { (s, frameworkMap, loader, groups, cp, javaOpts, javaHome) => - val results = groups map { + val tasks = groups map { case Tests.Group(name, tests, config) => config.subproc match { case Tests.Fork(extraJvm) => @@ -297,7 +297,7 @@ object Defaults extends BuildCommon Tests(frameworkMap, loader, tests, config, s.log) } } - Tests.foldTasks(results) + Tests.foldTasks(tasks) }, test <<= (executeTests, streams, resolvedScoped, state) map { (results, s, scoped, st) => @@ -374,7 +374,7 @@ object Defaults extends BuildCommon (streams, loadedTestFrameworks, testFilter in key, testGrouping in key, testLoader, resolvedScoped, result, fullClasspath in key, javaOptions in key, javaHome in key, state) flatMap { case (s, frameworks, filter, groups, loader, scoped, (selected, frameworkOptions), cp, javaOpts, javaHome, st) => implicit val display = Project.showContextKey(st) - val results = groups map { + val tasks = groups map { case Tests.Group(name, tests, config) => val modifiedOpts = Tests.Filter(filter(selected)) +: Tests.Argument(frameworkOptions : _*) +: config.options val newConfig = config.copy(options = modifiedOpts) @@ -385,7 +385,7 @@ object Defaults extends BuildCommon Tests(frameworks, loader, tests, newConfig, s.log) } } - Tests.foldTasks(results) map (Tests.showResults(s.log, _, noTestsMessage(scoped))) + Tests.foldTasks(tasks) map (Tests.showResults(s.log, _, noTestsMessage(scoped))) } } diff --git a/main/Keys.scala b/main/Keys.scala index 8c41696dd..f332a7a6c 100644 --- a/main/Keys.scala +++ b/main/Keys.scala @@ -198,7 +198,7 @@ object Keys val testListeners = TaskKey[Seq[TestReportListener]]("test-listeners", "Defines test listeners.") val testExecution = TaskKey[Tests.Execution]("test-execution", "Settings controlling test execution") val testFilter = TaskKey[Seq[String] => String => Boolean]("test-filter", "Filter controlling whether the test is executed") - val testGrouping = TaskKey[Seq[Tests.Group]]("test-grouping", "Groups discovered tests into groups. Groups are run sequentially.") + val testGrouping = TaskKey[Seq[Tests.Group]]("test-grouping", "Groups discovered tests into groups.") val isModule = AttributeKey[Boolean]("is-module", "True if the target is a module.") // Classpath/Dependency Management Keys diff --git a/main/Tags.scala b/main/Tags.scala index 346f09275..25df28a3c 100644 --- a/main/Tags.scala +++ b/main/Tags.scala @@ -18,6 +18,7 @@ object Tags val CPU = Tag("cpu") val Network = Tag("network") val Disk = Tag("disk") + val Subprocess = Tag("subprocess") /** Describes a restriction on concurrently executing tasks. * A Rule is constructed using one of the Tags.limit* methods. */ @@ -55,4 +56,4 @@ object Tags def limitUntagged(max: Int): Rule = limit(Untagged, max) def limit(tag: Tag, max: Int): Rule = new Single(tag, max) def limitSum(max: Int, tags: Tag*): Rule = new Sum(tags, max) -} \ No newline at end of file +} diff --git a/main/actions/ForkTests.scala b/main/actions/ForkTests.scala index 44bdb796d..2fbb6624d 100755 --- a/main/actions/ForkTests.scala +++ b/main/actions/ForkTests.scala @@ -90,8 +90,8 @@ private[sbt] object ForkTests { } } - testListeners.foreach(_.doInit()) try { + testListeners.foreach(_.doInit()) new Thread(Acceptor).start() val fullCp = classpath ++: Seq(IO.classLocationFile[ForkMain], IO.classLocationFile[Framework]) @@ -104,6 +104,6 @@ private[sbt] object ForkTests { val result = Acceptor.output testListeners.foreach(_.doComplete(result._1)) result - } + } tagw (config.tags: _*) } } diff --git a/main/actions/Tests.scala b/main/actions/Tests.scala index 5406d7d68..f4ace8cdf 100644 --- a/main/actions/Tests.scala +++ b/main/actions/Tests.scala @@ -127,11 +127,9 @@ object Tests def processResults(results: Iterable[(String, TestResult.Value)]): (TestResult.Value, Map[String, TestResult.Value]) = (overall(results.map(_._2)), results.toMap) def foldTasks(results: Seq[Task[Output]]): Task[Output] = - (task((TestResult.Passed, Map.empty[String, TestResult.Value])) /: results) { - reducePair(_, _, { - case ((v1, m1), (v2, m2)) => (if (v1.id < v2.id) v2 else v1, m1 ++ m2) - }) - } + reduced(results.toIndexedSeq, { + case ((v1, m1), (v2, m2)) => (if (v1.id < v2.id) v2 else v1, m1 ++ m2) + }) def overall(results: Iterable[TestResult.Value]): TestResult.Value = (TestResult.Passed /: results) { (acc, result) => if(acc.id < result.id) result else acc } def discover(frameworks: Seq[Framework], analysis: Analysis, log: Logger): (Seq[TestDefinition], Set[String]) = From 4abefbd3b67bd1300e54c322f4471738e2ce8237 Mon Sep 17 00:00:00 2001 From: Eugene Vigdorchik Date: Mon, 2 Apr 2012 23:56:42 +0400 Subject: [PATCH 07/15] Remove redundant try/catch. --- testing/agent/src/main/java/sbt/ForkMain.java | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/testing/agent/src/main/java/sbt/ForkMain.java b/testing/agent/src/main/java/sbt/ForkMain.java index f109aa860..5a2ecfdc2 100755 --- a/testing/agent/src/main/java/sbt/ForkMain.java +++ b/testing/agent/src/main/java/sbt/ForkMain.java @@ -66,15 +66,11 @@ public class ForkMain { public Result result() { return result;} public Throwable error() { return null; } } - public static void main(String[] args) { - try { - Socket socket = new Socket(InetAddress.getByName(null), Integer.valueOf(args[0])); - final ObjectInputStream is = new ObjectInputStream(socket.getInputStream()); - final ObjectOutputStream os = new ObjectOutputStream(socket.getOutputStream()); - new Run().run(is, os); - } catch (Exception e) { - e.printStackTrace(); - } + public static void main(String[] args) throws Exception { + Socket socket = new Socket(InetAddress.getByName(null), Integer.valueOf(args[0])); + final ObjectInputStream is = new ObjectInputStream(socket.getInputStream()); + final ObjectOutputStream os = new ObjectOutputStream(socket.getOutputStream()); + new Run().run(is, os); } private static class Run { boolean matches(Fingerprint f1, Fingerprint f2) { From c4385adce098efdfc3573314a355aa16074bfe0e Mon Sep 17 00:00:00 2001 From: Eugene Vigdorchik Date: Tue, 3 Apr 2012 11:49:41 +0400 Subject: [PATCH 08/15] Some more fixes. --- main/Defaults.scala | 14 ++++++++------ main/Keys.scala | 1 + main/Tags.scala | 3 ++- testing/agent/src/main/java/sbt/ForkMain.java | 7 ++++++- 4 files changed, 17 insertions(+), 8 deletions(-) diff --git a/main/Defaults.scala b/main/Defaults.scala index 3e0550ead..145aeab06 100755 --- a/main/Defaults.scala +++ b/main/Defaults.scala @@ -57,6 +57,7 @@ object Defaults extends BuildCommon onComplete <<= taskTemporaryDirectory { dir => () => IO.delete(dir); IO.createDirectory(dir) }, concurrentRestrictions <<= concurrentRestrictions or defaultRestrictions, parallelExecution :== true, + parallelTestGroups := 1, sbtVersion <<= appConfiguration { _.provider.id.version }, sbtBinaryVersion <<= sbtVersion apply binarySbtVersion, sbtResolver <<= sbtVersion { sbtV => if(sbtV endsWith "-SNAPSHOT") Classpaths.typesafeSnapshots else Classpaths.typesafeReleases }, @@ -117,10 +118,10 @@ object Defaults extends BuildCommon excludeFilter :== (".*" - ".") || HiddenFileFilter, pomIncludeRepository :== Classpaths.defaultRepositoryFilter )) - def defaultTestTasks(key: Scoped): Seq[Setting[_]] = Seq( - tags in key := Seq(Tags.Test -> 1, Tags.Subprocess -> 1), - logBuffered in key := true - ) + def defaultTestTasks(key: Scoped): Seq[Setting[_]] = inTask(key)(Seq( + tags := Seq(Tags.Test -> 1), + logBuffered := true + )) def projectCore: Seq[Setting[_]] = Seq( name <<= thisProject(_.id), logManager <<= extraLoggers(LogManager.defaults), @@ -337,8 +338,9 @@ object Defaults extends BuildCommon } def testExecutionTask(task: Scoped): Initialize[Task[Tests.Execution]] = - (testOptions in task, parallelExecution in task, fork in task, javaOptions in task, tags in task) map { - (opts, par, fork, jvmOpts, ts) => new Tests.Execution(opts, par, if (fork) Tests.Fork(jvmOpts) else Tests.InProcess, ts) + (testOptions in task, parallelExecution in task, fork in task, javaOptions in task, tags in task, parallelTestGroups in task) map { + (opts, par, fork, jvmOpts, ts, lim) => + new Tests.Execution(opts, par, if (fork) Tests.Fork(jvmOpts) else Tests.InProcess, if (fork) ts :+ (Tags.ForkedTestGroup -> lim) else ts) } def testQuickFilter: Initialize[Task[Seq[String] => String => Boolean]] = diff --git a/main/Keys.scala b/main/Keys.scala index f332a7a6c..4aab237c4 100644 --- a/main/Keys.scala +++ b/main/Keys.scala @@ -199,6 +199,7 @@ object Keys val testExecution = TaskKey[Tests.Execution]("test-execution", "Settings controlling test execution") val testFilter = TaskKey[Seq[String] => String => Boolean]("test-filter", "Filter controlling whether the test is executed") val testGrouping = TaskKey[Seq[Tests.Group]]("test-grouping", "Groups discovered tests into groups.") + val parallelTestGroups = SettingKey[Int]("parallel-test-groups", "Maximum number of test groups that may run at the same time.") val isModule = AttributeKey[Boolean]("is-module", "True if the target is a module.") // Classpath/Dependency Management Keys diff --git a/main/Tags.scala b/main/Tags.scala index 25df28a3c..9d32dde25 100644 --- a/main/Tags.scala +++ b/main/Tags.scala @@ -18,7 +18,8 @@ object Tags val CPU = Tag("cpu") val Network = Tag("network") val Disk = Tag("disk") - val Subprocess = Tag("subprocess") + + val ForkedTestGroup = Tag("forked-test-group") /** Describes a restriction on concurrently executing tasks. * A Rule is constructed using one of the Tags.limit* methods. */ diff --git a/testing/agent/src/main/java/sbt/ForkMain.java b/testing/agent/src/main/java/sbt/ForkMain.java index 5a2ecfdc2..3abab7953 100755 --- a/testing/agent/src/main/java/sbt/ForkMain.java +++ b/testing/agent/src/main/java/sbt/ForkMain.java @@ -70,7 +70,12 @@ public class ForkMain { Socket socket = new Socket(InetAddress.getByName(null), Integer.valueOf(args[0])); final ObjectInputStream is = new ObjectInputStream(socket.getInputStream()); final ObjectOutputStream os = new ObjectOutputStream(socket.getOutputStream()); - new Run().run(is, os); + try { + new Run().run(is, os); + } finally { + is.close(); + os.close(); + } } private static class Run { boolean matches(Fingerprint f1, Fingerprint f2) { From e169e4f222c9fcbe489ddcffc0dfdb27478505ed Mon Sep 17 00:00:00 2001 From: Eugene Vigdorchik Date: Tue, 3 Apr 2012 18:13:02 +0400 Subject: [PATCH 09/15] Correctly initialize ForkedTestGroup restriction. --- main/Defaults.scala | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/main/Defaults.scala b/main/Defaults.scala index 145aeab06..f03465865 100755 --- a/main/Defaults.scala +++ b/main/Defaults.scala @@ -293,7 +293,7 @@ object Defaults extends BuildCommon case Tests.Group(name, tests, config) => config.subproc match { case Tests.Fork(extraJvm) => - ForkTests(frameworkMap.keys.toSeq, tests.toList, config, cp.files, javaHome, javaOpts, s.log) + ForkTests(frameworkMap.keys.toSeq, tests.toList, config, cp.files, javaHome, javaOpts, s.log) tag Tags.ForkedTestGroup case Tests.InProcess => Tests(frameworkMap, loader, tests, config, s.log) } @@ -338,9 +338,9 @@ object Defaults extends BuildCommon } def testExecutionTask(task: Scoped): Initialize[Task[Tests.Execution]] = - (testOptions in task, parallelExecution in task, fork in task, javaOptions in task, tags in task, parallelTestGroups in task) map { - (opts, par, fork, jvmOpts, ts, lim) => - new Tests.Execution(opts, par, if (fork) Tests.Fork(jvmOpts) else Tests.InProcess, if (fork) ts :+ (Tags.ForkedTestGroup -> lim) else ts) + (testOptions in task, parallelExecution in task, fork in task, javaOptions in task, tags in task) map { + (opts, par, fork, jvmOpts, ts) => + new Tests.Execution(opts, par, if (fork) Tests.Fork(jvmOpts) else Tests.InProcess, ts) } def testQuickFilter: Initialize[Task[Seq[String] => String => Boolean]] = @@ -382,7 +382,7 @@ object Defaults extends BuildCommon val newConfig = config.copy(options = modifiedOpts) newConfig.subproc match { case Tests.Fork(extraJvm) => - ForkTests(frameworks.keys.toSeq, tests.toList, newConfig, cp.files, javaHome, javaOpts, s.log) + ForkTests(frameworks.keys.toSeq, tests.toList, newConfig, cp.files, javaHome, javaOpts, s.log) tag Tags.ForkedTestGroup case Tests.InProcess => Tests(frameworks, loader, tests, newConfig, s.log) } @@ -399,9 +399,9 @@ object Defaults extends BuildCommon def detectTests: Initialize[Task[Seq[TestDefinition]]] = (loadedTestFrameworks, compile, streams) map { (frameworkMap, analysis, s) => Tests.discover(frameworkMap.values.toSeq, analysis, s.log)._1 } - def defaultRestrictions: Initialize[Seq[Tags.Rule]] = parallelExecution { par => + def defaultRestrictions: Initialize[Seq[Tags.Rule]] = (parallelExecution, parallelTestGroups) { (par, ptg) => val max = EvaluateTask.SystemProcessors - Tags.limitAll(if(par) max else 1) :: Nil + Tags.limitAll(if(par) max else 1) :: Tags.limit(Tags.ForkedTestGroup, ptg) :: Nil } lazy val packageBase: Seq[Setting[_]] = Seq( From cdfc72d05d5bdf3c7cb37b2e74237c0bd94ec943 Mon Sep 17 00:00:00 2001 From: Eugene Vigdorchik Date: Tue, 3 Apr 2012 18:35:07 +0400 Subject: [PATCH 10/15] Given the ease of defining concurrentRestictions, I think it should be left to the user to correctly provide the limit for forked test groups. --- main/Defaults.scala | 5 ++--- main/Keys.scala | 1 - 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/main/Defaults.scala b/main/Defaults.scala index f03465865..a14f91ef3 100755 --- a/main/Defaults.scala +++ b/main/Defaults.scala @@ -57,7 +57,6 @@ object Defaults extends BuildCommon onComplete <<= taskTemporaryDirectory { dir => () => IO.delete(dir); IO.createDirectory(dir) }, concurrentRestrictions <<= concurrentRestrictions or defaultRestrictions, parallelExecution :== true, - parallelTestGroups := 1, sbtVersion <<= appConfiguration { _.provider.id.version }, sbtBinaryVersion <<= sbtVersion apply binarySbtVersion, sbtResolver <<= sbtVersion { sbtV => if(sbtV endsWith "-SNAPSHOT") Classpaths.typesafeSnapshots else Classpaths.typesafeReleases }, @@ -399,9 +398,9 @@ object Defaults extends BuildCommon def detectTests: Initialize[Task[Seq[TestDefinition]]] = (loadedTestFrameworks, compile, streams) map { (frameworkMap, analysis, s) => Tests.discover(frameworkMap.values.toSeq, analysis, s.log)._1 } - def defaultRestrictions: Initialize[Seq[Tags.Rule]] = (parallelExecution, parallelTestGroups) { (par, ptg) => + def defaultRestrictions: Initialize[Seq[Tags.Rule]] = parallelExecution { par => val max = EvaluateTask.SystemProcessors - Tags.limitAll(if(par) max else 1) :: Tags.limit(Tags.ForkedTestGroup, ptg) :: Nil + Tags.limitAll(if(par) max else 1) :: Tags.limit(Tags.ForkedTestGroup, 1) :: Nil } lazy val packageBase: Seq[Setting[_]] = Seq( diff --git a/main/Keys.scala b/main/Keys.scala index 4aab237c4..f332a7a6c 100644 --- a/main/Keys.scala +++ b/main/Keys.scala @@ -199,7 +199,6 @@ object Keys val testExecution = TaskKey[Tests.Execution]("test-execution", "Settings controlling test execution") val testFilter = TaskKey[Seq[String] => String => Boolean]("test-filter", "Filter controlling whether the test is executed") val testGrouping = TaskKey[Seq[Tests.Group]]("test-grouping", "Groups discovered tests into groups.") - val parallelTestGroups = SettingKey[Int]("parallel-test-groups", "Maximum number of test groups that may run at the same time.") val isModule = AttributeKey[Boolean]("is-module", "True if the target is a module.") // Classpath/Dependency Management Keys From 291db63af334daad45c849ac3ae787403c62511c Mon Sep 17 00:00:00 2001 From: Eugene Vigdorchik Date: Tue, 3 Apr 2012 19:43:59 +0400 Subject: [PATCH 11/15] Fixes --- main/Keys.scala | 2 +- main/actions/ForkTests.scala | 89 +++++++++---------- main/actions/Tests.scala | 2 +- testing/agent/src/main/java/sbt/ForkMain.java | 38 ++++---- 4 files changed, 62 insertions(+), 69 deletions(-) diff --git a/main/Keys.scala b/main/Keys.scala index f332a7a6c..9d6c5f48b 100644 --- a/main/Keys.scala +++ b/main/Keys.scala @@ -198,7 +198,7 @@ object Keys val testListeners = TaskKey[Seq[TestReportListener]]("test-listeners", "Defines test listeners.") val testExecution = TaskKey[Tests.Execution]("test-execution", "Settings controlling test execution") val testFilter = TaskKey[Seq[String] => String => Boolean]("test-filter", "Filter controlling whether the test is executed") - val testGrouping = TaskKey[Seq[Tests.Group]]("test-grouping", "Groups discovered tests into groups.") + val testGrouping = TaskKey[Seq[Tests.Group]]("test-grouping", "Collects discovered tests into groups. Whether to fork and the options for forking are configurable on a per-group basis.") val isModule = AttributeKey[Boolean]("is-module", "True if the target is a module.") // Classpath/Dependency Management Keys diff --git a/main/actions/ForkTests.scala b/main/actions/ForkTests.scala index 2fbb6624d..35d50d3b2 100755 --- a/main/actions/ForkTests.scala +++ b/main/actions/ForkTests.scala @@ -38,57 +38,50 @@ private[sbt] object ForkTests { val results = collection.mutable.Map.empty[String, TestResult.Value] def output = (overall(results.values), results.toMap) def run = { - val socketOpt = try { - Some(server.accept()) - } catch { - case e: IOException => None - } - for (socket <- socketOpt) { - val os = new ObjectOutputStream(socket.getOutputStream) - val is = new ObjectInputStream(socket.getInputStream) + val socket = server.accept() + val os = new ObjectOutputStream(socket.getOutputStream) + val is = new ObjectInputStream(socket.getInputStream) - import Tags._ - @annotation.tailrec def react: Unit = is.readObject match { - case `Done` => os.writeObject(Done); - case Array(`Error`, s: String) => log.error(s); react - case Array(`Warn`, s: String) => log.warn(s); react - case Array(`Info`, s: String) => log.info(s); react - case Array(`Debug`, s: String) => log.debug(s); react - case t: Throwable => log.trace(t); react - case tEvents: Array[Event] => - for (first <- tEvents.headOption) listeners.foreach(_ startGroup first.testName) - val event = TestEvent(tEvents) - listeners.foreach(_ testEvent event) - for (first <- tEvents.headOption) { - val result = event.result getOrElse TestResult.Passed - results += first.testName -> result - listeners.foreach(_ endGroup (first.testName, result)) - } - react - } - - try { - os.writeBoolean(log.ansiCodesSupported) - - val testsFiltered = tests.filter(test => filters.forall(_(test.name))).map{ - t => new ForkTestDefinition(t.name, t.fingerprint) - }.toArray - os.writeObject(testsFiltered) - - os.writeInt(frameworks.size) - for ((clazz, args) <- argMap) { - os.writeObject(clazz) - os.writeObject(args.toArray) + import Tags._ + @annotation.tailrec def react: Unit = is.readObject match { + case `Done` => os.writeObject(Done); + case Array(`Error`, s: String) => log.error(s); react + case Array(`Warn`, s: String) => log.warn(s); react + case Array(`Info`, s: String) => log.info(s); react + case Array(`Debug`, s: String) => log.debug(s); react + case t: Throwable => log.trace(t); react + case tEvents: Array[Event] => + for (first <- tEvents.headOption) listeners.foreach(_ startGroup first.testName) + val event = TestEvent(tEvents) + listeners.foreach(_ testEvent event) + for (first <- tEvents.headOption) { + val result = event.result getOrElse TestResult.Passed + results += first.testName -> result + listeners.foreach(_ endGroup (first.testName, result)) } - react - } finally { - is.close() - os.close() + } + + try { + os.writeBoolean(log.ansiCodesSupported) + + val testsFiltered = tests.filter(test => filters.forall(_(test.name))).map{ + t => new ForkTestDefinition(t.name, t.fingerprint) + }.toArray + os.writeObject(testsFiltered) + + os.writeInt(frameworks.size) + for ((clazz, args) <- argMap) { + os.writeObject(clazz) + os.writeObject(args.toArray) } - } - } - } + + react + } finally { + is.close(); os.close(); socket.close() + } + } + } try { testListeners.foreach(_.doInit()) @@ -96,7 +89,7 @@ private[sbt] object ForkTests { 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 ec = Fork.java(javaHome, options, LoggedOutput(log)) if (ec != 0) log.error("Running java with options " + options.mkString(" ") + " failed with exit code " + ec) } finally { server.close() diff --git a/main/actions/Tests.scala b/main/actions/Tests.scala index f4ace8cdf..7905cbfbc 100644 --- a/main/actions/Tests.scala +++ b/main/actions/Tests.scala @@ -40,7 +40,7 @@ object Tests final case class Argument(framework: Option[TestFramework], args: List[String]) extends TestOption sealed trait SubProcessPolicy - object InProcess extends SubProcessPolicy + case object InProcess extends SubProcessPolicy final case class Fork(extraJvm: Seq[String]) extends SubProcessPolicy final case class Execution(options: Seq[TestOption], parallel: Boolean, subproc: SubProcessPolicy, tags: Seq[(Tag, Int)]) diff --git a/testing/agent/src/main/java/sbt/ForkMain.java b/testing/agent/src/main/java/sbt/ForkMain.java index 3abab7953..5fab43e47 100755 --- a/testing/agent/src/main/java/sbt/ForkMain.java +++ b/testing/agent/src/main/java/sbt/ForkMain.java @@ -90,24 +90,24 @@ public class ForkMain { } return false; } + void write(ObjectOutputStream os, Object obj) { + try { + os.writeObject(obj); + } catch (IOException e) { + System.err.println("Cannot write to socket"); + } + } void run(ObjectInputStream is, final ObjectOutputStream os) throws Exception { final boolean ansiCodesSupported = is.readBoolean(); Logger[] loggers = { - new Logger() { - public boolean ansiCodesSupported() { return ansiCodesSupported; } - void write(Object obj) { - try { - os.writeObject(obj); - } catch (IOException e) { - System.err.println("Cannot write to socket"); - } - } - public void error(String s) { write(new Object[]{Tags.Error, s}); } - public void warn(String s) { write(new Object[]{Tags.Warn, s}); } - public void info(String s) { write(new Object[]{Tags.Info, s}); } - public void debug(String s) { write(new Object[]{Tags.Debug, s}); } - public void trace(Throwable t) { write(t); } - } + new Logger() { + public boolean ansiCodesSupported() { return ansiCodesSupported; } + public void error(String s) { write(os, new Object[]{Tags.Error, s}); } + public void warn(String s) { write(os, new Object[]{Tags.Warn, s}); } + public void info(String s) { write(os, new Object[]{Tags.Info, s}); } + public void debug(String s) { write(os, new Object[]{Tags.Debug, s}); } + public void trace(Throwable t) { write(os, t); } + } }; final ForkTestDefinition[] tests = (ForkTestDefinition[]) is.readObject(); @@ -118,7 +118,7 @@ public class ForkMain { try { framework = (Framework) Class.forName(implClassName).newInstance(); } catch (ClassNotFoundException e) { - System.err.println("Framework implementation '" + implClassName + "' not present."); + write(os, new Object[]{Tags.Error, "Framework implementation '" + implClassName + "' not present."}); continue; } @@ -139,12 +139,12 @@ public class ForkMain { } else if (test.fingerprint instanceof TestFingerprint) { runner.run(test.name, (TestFingerprint) test.fingerprint, handler, frameworkArgs); } else { - System.err.println("Framework '" + framework + "' does not support test '" + test.name + "'"); + write(os, new Object[]{Tags.Error, "Framework '" + framework + "' does not support test '" + test.name + "'"}); } - os.writeObject(events.toArray(new ForkEvent[events.size()])); + write(os, events.toArray(new ForkEvent[events.size()])); } } - os.writeObject(Tags.Done); + write(os, Tags.Done); is.readObject(); } } From b5766e405737229e64d4cafeefcb8808368e5770 Mon Sep 17 00:00:00 2001 From: Eugene Vigdorchik Date: Wed, 4 Apr 2012 09:30:46 +0400 Subject: [PATCH 12/15] Fix newlines and indentation. --- main/actions/ForkTests.scala | 204 ++++++------ testing/agent/src/main/java/sbt/ForkMain.java | 302 +++++++++--------- 2 files changed, 253 insertions(+), 253 deletions(-) diff --git a/main/actions/ForkTests.scala b/main/actions/ForkTests.scala index 35d50d3b2..b22784f59 100755 --- a/main/actions/ForkTests.scala +++ b/main/actions/ForkTests.scala @@ -1,102 +1,102 @@ -/* sbt -- Simple Build Tool - * Copyright 2012 Eugene Vigdorchik - */ -package sbt - -import org.scalatools.testing._ -import java.net.ServerSocket -import java.io._ -import Tests._ -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[Output] = { - val opts = config.options.toList - val listeners = opts flatMap { - case Listeners(ls) => ls - case _ => Nil - } - val testListeners = listeners flatMap { - case tl: TestsListener => Some(tl) - case _ => None - } - val filters = opts flatMap { - case Filter(f) => Some(f) - case _ => None - } - val argMap = frameworks.map { - f => f.implClassName -> opts.flatMap { - case Argument(None, args) => args - case Argument(Some(`f`), args) => args - case _ => Nil - } - }.toMap - - std.TaskExtra.task { - val server = new ServerSocket(0) - object Acceptor extends Runnable { - val results = collection.mutable.Map.empty[String, TestResult.Value] - def output = (overall(results.values), results.toMap) - def run = { - val socket = server.accept() - val os = new ObjectOutputStream(socket.getOutputStream) - val is = new ObjectInputStream(socket.getInputStream) - - import Tags._ - @annotation.tailrec def react: Unit = is.readObject match { - case `Done` => os.writeObject(Done); - case Array(`Error`, s: String) => log.error(s); react - case Array(`Warn`, s: String) => log.warn(s); react - case Array(`Info`, s: String) => log.info(s); react - case Array(`Debug`, s: String) => log.debug(s); react - case t: Throwable => log.trace(t); react - case tEvents: Array[Event] => - for (first <- tEvents.headOption) listeners.foreach(_ startGroup first.testName) - val event = TestEvent(tEvents) - listeners.foreach(_ testEvent event) - for (first <- tEvents.headOption) { - val result = event.result getOrElse TestResult.Passed - results += first.testName -> result - listeners.foreach(_ endGroup (first.testName, result)) - } - react - } - - try { - os.writeBoolean(log.ansiCodesSupported) - - val testsFiltered = tests.filter(test => filters.forall(_(test.name))).map{ - t => new ForkTestDefinition(t.name, t.fingerprint) - }.toArray - os.writeObject(testsFiltered) - - os.writeInt(frameworks.size) - for ((clazz, args) <- argMap) { - os.writeObject(clazz) - os.writeObject(args.toArray) - } - - react - } finally { - is.close(); os.close(); socket.close() - } - } - } - - try { - testListeners.foreach(_.doInit()) - 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, LoggedOutput(log)) - if (ec != 0) log.error("Running java with options " + options.mkString(" ") + " failed with exit code " + ec) - } finally { - server.close() - } - val result = Acceptor.output - testListeners.foreach(_.doComplete(result._1)) - result - } tagw (config.tags: _*) - } -} +/* sbt -- Simple Build Tool + * Copyright 2012 Eugene Vigdorchik + */ +package sbt + +import org.scalatools.testing._ +import java.net.ServerSocket +import java.io._ +import Tests._ +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[Output] = { + val opts = config.options.toList + val listeners = opts flatMap { + case Listeners(ls) => ls + case _ => Nil + } + val testListeners = listeners flatMap { + case tl: TestsListener => Some(tl) + case _ => None + } + val filters = opts flatMap { + case Filter(f) => Some(f) + case _ => None + } + val argMap = frameworks.map { + f => f.implClassName -> opts.flatMap { + case Argument(None, args) => args + case Argument(Some(`f`), args) => args + case _ => Nil + } + }.toMap + + std.TaskExtra.task { + val server = new ServerSocket(0) + object Acceptor extends Runnable { + val results = collection.mutable.Map.empty[String, TestResult.Value] + def output = (overall(results.values), results.toMap) + def run = { + val socket = server.accept() + val os = new ObjectOutputStream(socket.getOutputStream) + val is = new ObjectInputStream(socket.getInputStream) + + import Tags._ + @annotation.tailrec def react: Unit = is.readObject match { + case `Done` => os.writeObject(Done); + case Array(`Error`, s: String) => log.error(s); react + case Array(`Warn`, s: String) => log.warn(s); react + case Array(`Info`, s: String) => log.info(s); react + case Array(`Debug`, s: String) => log.debug(s); react + case t: Throwable => log.trace(t); react + case tEvents: Array[Event] => + for (first <- tEvents.headOption) listeners.foreach(_ startGroup first.testName) + val event = TestEvent(tEvents) + listeners.foreach(_ testEvent event) + for (first <- tEvents.headOption) { + val result = event.result getOrElse TestResult.Passed + results += first.testName -> result + listeners.foreach(_ endGroup (first.testName, result)) + } + react + } + + try { + os.writeBoolean(log.ansiCodesSupported) + + val testsFiltered = tests.filter(test => filters.forall(_(test.name))).map{ + t => new ForkTestDefinition(t.name, t.fingerprint) + }.toArray + os.writeObject(testsFiltered) + + os.writeInt(frameworks.size) + for ((clazz, args) <- argMap) { + os.writeObject(clazz) + os.writeObject(args.toArray) + } + + react + } finally { + is.close(); os.close(); socket.close() + } + } + } + + try { + testListeners.foreach(_.doInit()) + 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, LoggedOutput(log)) + if (ec != 0) log.error("Running java with options " + options.mkString(" ") + " failed with exit code " + ec) + } finally { + server.close() + } + val result = Acceptor.output + testListeners.foreach(_.doComplete(result._1)) + result + } tagw (config.tags: _*) + } +} diff --git a/testing/agent/src/main/java/sbt/ForkMain.java b/testing/agent/src/main/java/sbt/ForkMain.java index 5fab43e47..416592b46 100755 --- a/testing/agent/src/main/java/sbt/ForkMain.java +++ b/testing/agent/src/main/java/sbt/ForkMain.java @@ -1,151 +1,151 @@ -/* sbt -- Simple Build Tool - * Copyright 2012 Eugene Vigdorchik - */ -package sbt; - -import org.scalatools.testing.*; - -import java.io.IOException; -import java.io.ObjectInputStream; -import java.io.ObjectOutputStream; -import java.io.Serializable; -import java.net.Socket; -import java.net.InetAddress; -import java.util.ArrayList; -import java.util.List; - -public class ForkMain { - public static enum Tags { - Error, Warn, Info, Debug, Done; - } - - static class SubclassFingerscan implements TestFingerprint, Serializable { - private boolean isModule; - private String superClassName; - SubclassFingerscan(SubclassFingerprint print) { - isModule = print.isModule(); - superClassName = print.superClassName(); - } - public boolean isModule() { return isModule; } - public String superClassName() { return superClassName; } - } - static class AnnotatedFingerscan implements AnnotatedFingerprint, Serializable { - private boolean isModule; - private String annotationName; - AnnotatedFingerscan(AnnotatedFingerprint print) { - isModule = print.isModule(); - annotationName = print.annotationName(); - } - public boolean isModule() { return isModule; } - public String annotationName() { return annotationName; } - } - public static class ForkTestDefinition implements Serializable { - public String name; - public Fingerprint fingerprint; - - public ForkTestDefinition(String name, Fingerprint fingerprint) { - this.name = name; - if (fingerprint instanceof SubclassFingerprint) { - this.fingerprint = new SubclassFingerscan((SubclassFingerprint) fingerprint); - } else { - this.fingerprint = new AnnotatedFingerscan((AnnotatedFingerprint) fingerprint); - } - } - } - static class ForkEvent implements Event, Serializable { - private String testName; - private String description; - private Result result; - ForkEvent(Event e) { - testName = e.testName(); - description = e.description(); - result = e.result(); - } - public String testName() { return testName; } - public String description() { return description; } - public Result result() { return result;} - public Throwable error() { return null; } - } - public static void main(String[] args) throws Exception { - Socket socket = new Socket(InetAddress.getByName(null), Integer.valueOf(args[0])); - final ObjectInputStream is = new ObjectInputStream(socket.getInputStream()); - final ObjectOutputStream os = new ObjectOutputStream(socket.getOutputStream()); - try { - new Run().run(is, os); - } finally { - is.close(); - os.close(); - } - } - private static class Run { - boolean matches(Fingerprint f1, Fingerprint f2) { - if (f1 instanceof SubclassFingerprint && f2 instanceof SubclassFingerprint) { - final SubclassFingerprint sf1 = (SubclassFingerprint) f1; - final SubclassFingerprint sf2 = (SubclassFingerprint) f2; - return sf1.isModule() == sf2.isModule() && sf1.superClassName().equals(sf2.superClassName()); - } else if (f1 instanceof AnnotatedFingerprint && f2 instanceof AnnotatedFingerprint) { - AnnotatedFingerprint af1 = (AnnotatedFingerprint) f1; - AnnotatedFingerprint af2 = (AnnotatedFingerprint) f2; - return af1.isModule() == af2.isModule() && af1.annotationName().equals(af2.annotationName()); - } - return false; - } - void write(ObjectOutputStream os, Object obj) { - try { - os.writeObject(obj); - } catch (IOException e) { - System.err.println("Cannot write to socket"); - } - } - void run(ObjectInputStream is, final ObjectOutputStream os) throws Exception { - final boolean ansiCodesSupported = is.readBoolean(); - Logger[] loggers = { - new Logger() { - public boolean ansiCodesSupported() { return ansiCodesSupported; } - public void error(String s) { write(os, new Object[]{Tags.Error, s}); } - public void warn(String s) { write(os, new Object[]{Tags.Warn, s}); } - public void info(String s) { write(os, new Object[]{Tags.Info, s}); } - public void debug(String s) { write(os, new Object[]{Tags.Debug, s}); } - public void trace(Throwable t) { write(os, t); } - } - }; - - final ForkTestDefinition[] tests = (ForkTestDefinition[]) is.readObject(); - int nFrameworks = is.readInt(); - for (int i = 0; i < nFrameworks; i++) { - final Framework framework; - final String implClassName = (String) is.readObject(); - try { - framework = (Framework) Class.forName(implClassName).newInstance(); - } catch (ClassNotFoundException e) { - write(os, new Object[]{Tags.Error, "Framework implementation '" + implClassName + "' not present."}); - continue; - } - - final String[] frameworkArgs = (String[]) is.readObject(); - - ArrayList filteredTests = new ArrayList(); - for (Fingerprint testFingerprint : framework.tests()) { - for (ForkTestDefinition test : tests) { - if (matches(testFingerprint, test.fingerprint)) filteredTests.add(test); - } - } - final org.scalatools.testing.Runner runner = framework.testRunner(getClass().getClassLoader(), loggers); - for (ForkTestDefinition test : filteredTests) { - final List events = new ArrayList(); - EventHandler handler = new EventHandler() { public void handle(Event e){ events.add(new ForkEvent(e)); } }; - if (runner instanceof Runner2) { - ((Runner2) runner).run(test.name, test.fingerprint, handler, frameworkArgs); - } else if (test.fingerprint instanceof TestFingerprint) { - runner.run(test.name, (TestFingerprint) test.fingerprint, handler, frameworkArgs); - } else { - write(os, new Object[]{Tags.Error, "Framework '" + framework + "' does not support test '" + test.name + "'"}); - } - write(os, events.toArray(new ForkEvent[events.size()])); - } - } - write(os, Tags.Done); - is.readObject(); - } - } -} +/* sbt -- Simple Build Tool + * Copyright 2012 Eugene Vigdorchik + */ +package sbt; + +import org.scalatools.testing.*; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; +import java.net.Socket; +import java.net.InetAddress; +import java.util.ArrayList; +import java.util.List; + +public class ForkMain { + public static enum Tags { + Error, Warn, Info, Debug, Done; + } + + static class SubclassFingerscan implements TestFingerprint, Serializable { + private boolean isModule; + private String superClassName; + SubclassFingerscan(SubclassFingerprint print) { + isModule = print.isModule(); + superClassName = print.superClassName(); + } + public boolean isModule() { return isModule; } + public String superClassName() { return superClassName; } + } + static class AnnotatedFingerscan implements AnnotatedFingerprint, Serializable { + private boolean isModule; + private String annotationName; + AnnotatedFingerscan(AnnotatedFingerprint print) { + isModule = print.isModule(); + annotationName = print.annotationName(); + } + public boolean isModule() { return isModule; } + public String annotationName() { return annotationName; } + } + public static class ForkTestDefinition implements Serializable { + public String name; + public Fingerprint fingerprint; + + public ForkTestDefinition(String name, Fingerprint fingerprint) { + this.name = name; + if (fingerprint instanceof SubclassFingerprint) { + this.fingerprint = new SubclassFingerscan((SubclassFingerprint) fingerprint); + } else { + this.fingerprint = new AnnotatedFingerscan((AnnotatedFingerprint) fingerprint); + } + } + } + static class ForkEvent implements Event, Serializable { + private String testName; + private String description; + private Result result; + ForkEvent(Event e) { + testName = e.testName(); + description = e.description(); + result = e.result(); + } + public String testName() { return testName; } + public String description() { return description; } + public Result result() { return result;} + public Throwable error() { return null; } + } + public static void main(String[] args) throws Exception { + Socket socket = new Socket(InetAddress.getByName(null), Integer.valueOf(args[0])); + final ObjectInputStream is = new ObjectInputStream(socket.getInputStream()); + final ObjectOutputStream os = new ObjectOutputStream(socket.getOutputStream()); + try { + new Run().run(is, os); + } finally { + is.close(); + os.close(); + } + } + private static class Run { + boolean matches(Fingerprint f1, Fingerprint f2) { + if (f1 instanceof SubclassFingerprint && f2 instanceof SubclassFingerprint) { + final SubclassFingerprint sf1 = (SubclassFingerprint) f1; + final SubclassFingerprint sf2 = (SubclassFingerprint) f2; + return sf1.isModule() == sf2.isModule() && sf1.superClassName().equals(sf2.superClassName()); + } else if (f1 instanceof AnnotatedFingerprint && f2 instanceof AnnotatedFingerprint) { + AnnotatedFingerprint af1 = (AnnotatedFingerprint) f1; + AnnotatedFingerprint af2 = (AnnotatedFingerprint) f2; + return af1.isModule() == af2.isModule() && af1.annotationName().equals(af2.annotationName()); + } + return false; + } + void write(ObjectOutputStream os, Object obj) { + try { + os.writeObject(obj); + } catch (IOException e) { + System.err.println("Cannot write to socket"); + } + } + void run(ObjectInputStream is, final ObjectOutputStream os) throws Exception { + final boolean ansiCodesSupported = is.readBoolean(); + Logger[] loggers = { + new Logger() { + public boolean ansiCodesSupported() { return ansiCodesSupported; } + public void error(String s) { write(os, new Object[]{Tags.Error, s}); } + public void warn(String s) { write(os, new Object[]{Tags.Warn, s}); } + public void info(String s) { write(os, new Object[]{Tags.Info, s}); } + public void debug(String s) { write(os, new Object[]{Tags.Debug, s}); } + public void trace(Throwable t) { write(os, t); } + } + }; + + final ForkTestDefinition[] tests = (ForkTestDefinition[]) is.readObject(); + int nFrameworks = is.readInt(); + for (int i = 0; i < nFrameworks; i++) { + final Framework framework; + final String implClassName = (String) is.readObject(); + try { + framework = (Framework) Class.forName(implClassName).newInstance(); + } catch (ClassNotFoundException e) { + write(os, new Object[]{Tags.Error, "Framework implementation '" + implClassName + "' not present."}); + continue; + } + + final String[] frameworkArgs = (String[]) is.readObject(); + + ArrayList filteredTests = new ArrayList(); + for (Fingerprint testFingerprint : framework.tests()) { + for (ForkTestDefinition test : tests) { + if (matches(testFingerprint, test.fingerprint)) filteredTests.add(test); + } + } + final org.scalatools.testing.Runner runner = framework.testRunner(getClass().getClassLoader(), loggers); + for (ForkTestDefinition test : filteredTests) { + final List events = new ArrayList(); + EventHandler handler = new EventHandler() { public void handle(Event e){ events.add(new ForkEvent(e)); } }; + if (runner instanceof Runner2) { + ((Runner2) runner).run(test.name, test.fingerprint, handler, frameworkArgs); + } else if (test.fingerprint instanceof TestFingerprint) { + runner.run(test.name, (TestFingerprint) test.fingerprint, handler, frameworkArgs); + } else { + write(os, new Object[]{Tags.Error, "Framework '" + framework + "' does not support test '" + test.name + "'"}); + } + write(os, events.toArray(new ForkEvent[events.size()])); + } + } + write(os, Tags.Done); + is.readObject(); + } + } +} From 2ba9bbc7675d8d8b866445b3ec2267a33e8dfdcd Mon Sep 17 00:00:00 2001 From: Eugene Vigdorchik Date: Wed, 4 Apr 2012 16:25:12 +0400 Subject: [PATCH 13/15] More indentation messed up. --- main/Defaults.scala | 15 +++++++-------- main/actions/ForkTests.scala | 22 +++++++++++----------- main/actions/Tests.scala | 4 ++-- 3 files changed, 20 insertions(+), 21 deletions(-) diff --git a/main/Defaults.scala b/main/Defaults.scala index a14f91ef3..70b641359 100755 --- a/main/Defaults.scala +++ b/main/Defaults.scala @@ -288,7 +288,7 @@ object Defaults extends BuildCommon testFilter in testQuick <<= testQuickFilter, executeTests <<= (streams in test, loadedTestFrameworks, testLoader, testGrouping in test, fullClasspath in test, javaOptions in test, javaHome in test) flatMap { (s, frameworkMap, loader, groups, cp, javaOpts, javaHome) => - val tasks = groups map { + val tasks = groups map { case Tests.Group(name, tests, config) => config.subproc match { case Tests.Fork(extraJvm) => @@ -297,7 +297,7 @@ object Defaults extends BuildCommon Tests(frameworkMap, loader, tests, config, s.log) } } - Tests.foldTasks(tasks) + Tests.foldTasks(tasks) }, test <<= (executeTests, streams, resolvedScoped, state) map { (results, s, scoped, st) => @@ -378,7 +378,7 @@ object Defaults extends BuildCommon val tasks = groups map { case Tests.Group(name, tests, config) => val modifiedOpts = Tests.Filter(filter(selected)) +: Tests.Argument(frameworkOptions : _*) +: config.options - val newConfig = config.copy(options = modifiedOpts) + val newConfig = config.copy(options = modifiedOpts) newConfig.subproc match { case Tests.Fork(extraJvm) => ForkTests(frameworks.keys.toSeq, tests.toList, newConfig, cp.files, javaHome, javaOpts, s.log) tag Tags.ForkedTestGroup @@ -527,12 +527,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) => + (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) + new ForkRun( ForkOptions(scalaJars = si.jars, javaHome = javaHomeDir, connectInput = connectIn, outputStrategy = strategy, runJVMOptions = options, workingDirectory = Some(base)) ) + } else + new Run(si, trap, tmp) } @deprecated("Use `docTaskSettings` instead", "0.12.0") diff --git a/main/actions/ForkTests.scala b/main/actions/ForkTests.scala index b22784f59..cf29f4e34 100755 --- a/main/actions/ForkTests.scala +++ b/main/actions/ForkTests.scala @@ -43,7 +43,7 @@ private[sbt] object ForkTests { val is = new ObjectInputStream(socket.getInputStream) import Tags._ - @annotation.tailrec def react: Unit = is.readObject match { + @annotation.tailrec def react: Unit = is.readObject match { case `Done` => os.writeObject(Done); case Array(`Error`, s: String) => log.error(s); react case Array(`Warn`, s: String) => log.warn(s); react @@ -52,14 +52,14 @@ private[sbt] object ForkTests { case t: Throwable => log.trace(t); react case tEvents: Array[Event] => for (first <- tEvents.headOption) listeners.foreach(_ startGroup first.testName) - val event = TestEvent(tEvents) - listeners.foreach(_ testEvent event) - for (first <- tEvents.headOption) { - val result = event.result getOrElse TestResult.Passed - results += first.testName -> result - listeners.foreach(_ endGroup (first.testName, result)) - } - react + val event = TestEvent(tEvents) + listeners.foreach(_ testEvent event) + for (first <- tEvents.headOption) { + val result = event.result getOrElse TestResult.Passed + results += first.testName -> result + listeners.foreach(_ endGroup (first.testName, result)) + } + react } try { @@ -80,8 +80,8 @@ private[sbt] object ForkTests { } finally { is.close(); os.close(); socket.close() } - } - } + } + } try { testListeners.foreach(_.doInit()) diff --git a/main/actions/Tests.scala b/main/actions/Tests.scala index 7905cbfbc..905e68130 100644 --- a/main/actions/Tests.scala +++ b/main/actions/Tests.scala @@ -128,8 +128,8 @@ object Tests (overall(results.map(_._2)), results.toMap) def foldTasks(results: Seq[Task[Output]]): Task[Output] = reduced(results.toIndexedSeq, { - case ((v1, m1), (v2, m2)) => (if (v1.id < v2.id) v2 else v1, m1 ++ m2) - }) + case ((v1, m1), (v2, m2)) => (if (v1.id < v2.id) v2 else v1, m1 ++ m2) + }) def overall(results: Iterable[TestResult.Value]): TestResult.Value = (TestResult.Passed /: results) { (acc, result) => if(acc.id < result.id) result else acc } def discover(frameworks: Seq[Framework], analysis: Analysis, log: Logger): (Seq[TestDefinition], Set[String]) = From 05c4fbdc3d9360851c5aa4bd30b9b956f7c42906 Mon Sep 17 00:00:00 2001 From: Eugene Vigdorchik Date: Wed, 4 Apr 2012 23:50:32 +0400 Subject: [PATCH 14/15] Cleanup: 1) Rename policies since Fork is popular name and clashes with sbt.Fork2. Don't put entire Execution into Group, but just RunPolicy. --- main/Defaults.scala | 30 +++++++++++++++--------------- main/actions/Tests.scala | 12 ++++++------ 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/main/Defaults.scala b/main/Defaults.scala index 70b641359..255fb9561 100755 --- a/main/Defaults.scala +++ b/main/Defaults.scala @@ -286,12 +286,12 @@ object Defaults extends BuildCommon testOptions in GlobalScope :== Nil, testFilter in testOnly :== (selectedFilter _), testFilter in testQuick <<= testQuickFilter, - executeTests <<= (streams in test, loadedTestFrameworks, testLoader, testGrouping in test, fullClasspath in test, javaOptions in test, javaHome in test) flatMap { - (s, frameworkMap, loader, groups, cp, javaOpts, javaHome) => + executeTests <<= (streams in test, loadedTestFrameworks, testLoader, testGrouping in test, testExecution in test, fullClasspath in test, javaHome in test) flatMap { + (s, frameworkMap, loader, groups, config, cp, javaHome) => val tasks = groups map { - case Tests.Group(name, tests, config) => - config.subproc match { - case Tests.Fork(extraJvm) => + case Tests.Group(name, tests, runPolicy) => + runPolicy match { + case Tests.SubProcess(javaOpts) => ForkTests(frameworkMap.keys.toSeq, tests.toList, config, cp.files, javaHome, javaOpts, s.log) tag Tags.ForkedTestGroup case Tests.InProcess => Tests(frameworkMap, loader, tests, config, s.log) @@ -318,8 +318,8 @@ object Defaults extends BuildCommon }, testOptions <<= (testOptions in TaskGlobal, testListeners) map { (options, ls) => Tests.Listeners(ls) +: options }, testExecution <<= testExecutionTask(key), - testGrouping <<= ((definedTests, testExecution) map { - (tests, exec) => Seq(new Tests.Group("", tests, exec)) + testGrouping <<= testGrouping in TaskGlobal or ((definedTests, fork) map { + (tests, fork) => Seq(new Tests.Group("", tests, if (fork) Tests.SubProcess(Seq()) else Tests.InProcess)) }) ) ) def testLogger(manager: Streams, baseKey: Scoped)(tdef: TestDefinition): Logger = @@ -337,9 +337,9 @@ object Defaults extends BuildCommon } def testExecutionTask(task: Scoped): Initialize[Task[Tests.Execution]] = - (testOptions in task, parallelExecution in task, fork in task, javaOptions in task, tags in task) map { - (opts, par, fork, jvmOpts, ts) => - new Tests.Execution(opts, par, if (fork) Tests.Fork(jvmOpts) else Tests.InProcess, ts) + (testOptions in task, parallelExecution in task, fork in task, tags in task) map { + (opts, par, fork, ts) => + new Tests.Execution(opts, par, ts) } def testQuickFilter: Initialize[Task[Seq[String] => String => Boolean]] = @@ -372,15 +372,15 @@ object Defaults extends BuildCommon def inputTests(key: InputKey[_]): Initialize[InputTask[Unit]] = InputTask( loadForParser(definedTestNames)( (s, i) => testOnlyParser(s, i getOrElse Nil) ) ) { result => - (streams, loadedTestFrameworks, testFilter in key, testGrouping in key, testLoader, resolvedScoped, result, fullClasspath in key, javaOptions in key, javaHome in key, state) flatMap { - case (s, frameworks, filter, groups, loader, scoped, (selected, frameworkOptions), cp, javaOpts, javaHome, st) => + (streams, loadedTestFrameworks, testFilter in key, testGrouping in key, testExecution in key, testLoader, resolvedScoped, result, fullClasspath in key, javaHome in key, state) flatMap { + case (s, frameworks, filter, groups, config, loader, scoped, (selected, frameworkOptions), cp, javaHome, st) => implicit val display = Project.showContextKey(st) val tasks = groups map { - case Tests.Group(name, tests, config) => + case Tests.Group(name, tests, runPolicy) => val modifiedOpts = Tests.Filter(filter(selected)) +: Tests.Argument(frameworkOptions : _*) +: config.options val newConfig = config.copy(options = modifiedOpts) - newConfig.subproc match { - case Tests.Fork(extraJvm) => + runPolicy match { + case Tests.SubProcess(javaOpts) => ForkTests(frameworks.keys.toSeq, tests.toList, newConfig, cp.files, javaHome, javaOpts, s.log) tag Tags.ForkedTestGroup case Tests.InProcess => Tests(frameworks, loader, tests, newConfig, s.log) diff --git a/main/actions/Tests.scala b/main/actions/Tests.scala index 905e68130..e72cc8c3e 100644 --- a/main/actions/Tests.scala +++ b/main/actions/Tests.scala @@ -39,11 +39,7 @@ object Tests // None means apply to all, Some(tf) means apply to a particular framework only. final case class Argument(framework: Option[TestFramework], args: List[String]) extends TestOption - sealed trait SubProcessPolicy - case object InProcess extends SubProcessPolicy - final case class Fork(extraJvm: Seq[String]) extends SubProcessPolicy - - final case class Execution(options: Seq[TestOption], parallel: Boolean, subproc: SubProcessPolicy, tags: Seq[(Tag, Int)]) + final case class Execution(options: Seq[TestOption], parallel: Boolean, tags: Seq[(Tag, Int)]) def apply(frameworks: Map[TestFramework, Framework], testLoader: ClassLoader, discovered: Seq[TestDefinition], config: Execution, log: Logger): Task[Output] = { @@ -186,6 +182,10 @@ object Tests } } - final case class Group(name: String, tests: Seq[TestDefinition], config: Execution) + sealed trait RunPolicy + case object InProcess extends RunPolicy + final case class SubProcess(javaOptions: Seq[String]) extends RunPolicy + + final case class Group(name: String, tests: Seq[TestDefinition], runPolicy: RunPolicy) } From e8798ddca8688172b175cac81fccd06bc95df2b7 Mon Sep 17 00:00:00 2001 From: Eugene Vigdorchik Date: Thu, 5 Apr 2012 12:02:53 +0400 Subject: [PATCH 15/15] Add test and allow the java options to be picked for a default test group created. --- main/Defaults.scala | 8 +++-- main/actions/Tests.scala | 8 ++--- .../tests/fork/project/ForkTestsTest.scala | 36 +++++++++++++++++++ .../tests/fork/src/test/scala/Ensemble.scala | 20 +++++++++++ sbt/src/sbt-test/tests/fork/test | 2 ++ 5 files changed, 67 insertions(+), 7 deletions(-) create mode 100755 sbt/src/sbt-test/tests/fork/project/ForkTestsTest.scala create mode 100755 sbt/src/sbt-test/tests/fork/src/test/scala/Ensemble.scala create mode 100755 sbt/src/sbt-test/tests/fork/test diff --git a/main/Defaults.scala b/main/Defaults.scala index 255fb9561..e968955e3 100755 --- a/main/Defaults.scala +++ b/main/Defaults.scala @@ -318,9 +318,7 @@ object Defaults extends BuildCommon }, testOptions <<= (testOptions in TaskGlobal, testListeners) map { (options, ls) => Tests.Listeners(ls) +: options }, testExecution <<= testExecutionTask(key), - testGrouping <<= testGrouping in TaskGlobal or ((definedTests, fork) map { - (tests, fork) => Seq(new Tests.Group("", tests, if (fork) Tests.SubProcess(Seq()) else Tests.InProcess)) - }) + testGrouping <<= testGrouping or singleTestGroup(key) ) ) def testLogger(manager: Streams, baseKey: Scoped)(tdef: TestDefinition): Logger = { @@ -335,6 +333,10 @@ 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 testExecutionTask(task: Scoped): Initialize[Task[Tests.Execution]] = (testOptions in task, parallelExecution in task, fork in task, tags in task) map { diff --git a/main/actions/Tests.scala b/main/actions/Tests.scala index e72cc8c3e..4487f7139 100644 --- a/main/actions/Tests.scala +++ b/main/actions/Tests.scala @@ -182,10 +182,10 @@ object Tests } } - sealed trait RunPolicy - case object InProcess extends RunPolicy - final case class SubProcess(javaOptions: Seq[String]) extends RunPolicy + sealed trait TestRunPolicy + case object InProcess extends TestRunPolicy + final case class SubProcess(javaOptions: Seq[String]) extends TestRunPolicy - final case class Group(name: String, tests: Seq[TestDefinition], runPolicy: RunPolicy) + final case class Group(name: String, tests: Seq[TestDefinition], runPolicy: TestRunPolicy) } diff --git a/sbt/src/sbt-test/tests/fork/project/ForkTestsTest.scala b/sbt/src/sbt-test/tests/fork/project/ForkTestsTest.scala new file mode 100755 index 000000000..4cfa42fef --- /dev/null +++ b/sbt/src/sbt-test/tests/fork/project/ForkTestsTest.scala @@ -0,0 +1,36 @@ +import sbt._ +import Keys._ +import Tests._ +import Defaults._ + +object ForkTestsTest extends Build { + val totalFiles = 9 + val groupSize = 3 + + val check = TaskKey[Unit]("check", "Check all files were created and remove them.") + + def groupId(idx: Int) = "group_" + (idx + 1) + def groupPrefix(idx: Int) = groupId(idx) + "_file_" + + lazy val root = Project("root", file("."), settings = defaultSettings ++ Seq( + testGrouping <<= definedTests in Test map { tests => + assert(tests.size == 1) + val groups = Stream const tests(0) take totalFiles grouped groupSize + for ((ts, idx) <- groups.toSeq.zipWithIndex) yield { + new Group(groupId(idx), ts, SubProcess(Seq("-Dgroup.prefix=" + groupPrefix(idx), "-Dgroup.size=" + ts.size))) + } + }, + check := { + for (i <- 0 until totalFiles/groupSize) + for (j <- 1 to groupSize) { + val f = file(groupPrefix(i) + j) + if (!f.exists) + error("File " + f.getName + " was not created.") + else + f.delete() + } + }, + concurrentRestrictions := Tags.limit(Tags.ForkedTestGroup, 2) :: Nil, + libraryDependencies += "org.scalatest" % "scalatest_2.9.0" % "1.6.1" % "test" + )) +} diff --git a/sbt/src/sbt-test/tests/fork/src/test/scala/Ensemble.scala b/sbt/src/sbt-test/tests/fork/src/test/scala/Ensemble.scala new file mode 100755 index 000000000..fc009691f --- /dev/null +++ b/sbt/src/sbt-test/tests/fork/src/test/scala/Ensemble.scala @@ -0,0 +1,20 @@ +import org.scalatest.FlatSpec +import org.scalatest.matchers.MustMatchers +import java.io.File + +class Ensemble extends FlatSpec with MustMatchers { + val prefix = System.getProperty("group.prefix") + val countTo = System.getProperty("group.size").toInt + + "an ensemble" must "create all files" in { + @annotation.tailrec + def step(i: Int): Unit = { + val f = new File(prefix + i) + if (!f.createNewFile) + step(if (f.exists) i + 1 else i) + else + i must be <= (countTo) + } + step(1) + } +} diff --git a/sbt/src/sbt-test/tests/fork/test b/sbt/src/sbt-test/tests/fork/test new file mode 100755 index 000000000..de5041ba0 --- /dev/null +++ b/sbt/src/sbt-test/tests/fork/test @@ -0,0 +1,2 @@ +> test +> check \ No newline at end of file