From f497e15cd889761e734a8af78b4da7c7af35f37d Mon Sep 17 00:00:00 2001 From: jvican Date: Fri, 28 Apr 2017 11:41:22 +0200 Subject: [PATCH] Run scripted tests in parallel Add binary-compatible methods to run scripted tests in parallel using parallel collections. Parallel collections are not tuned and a default of 4 cores is used for the `ForkJoinPool`. This can be a matter of experimentation in the future but for now I prefer to stay with it. The non-parallel methods are not affected by this change. The changes to the scripted plugin have not been used, only the sbt scripted plugin is using `runInParallel`. In the future, this change can also happen so that plugin authors can benefit from parallel sbt testing. This is not a priority for me now :) --- project/Scripted.scala | 19 +++--- .../main/scala/sbt/test/ScriptedTests.scala | 59 ++++++++++++++++--- 2 files changed, 62 insertions(+), 16 deletions(-) diff --git a/project/Scripted.scala b/project/Scripted.scala index a669e6256..621db9af5 100644 --- a/project/Scripted.scala +++ b/project/Scripted.scala @@ -89,12 +89,12 @@ object Scripted { // Interface to cross class loader type SbtScriptedRunner = { - def run(resourceBaseDirectory: File, - bufferLog: Boolean, - tests: Array[String], - bootProperties: File, - launchOpts: Array[String], - prescripted: java.util.List[File]): Unit + def runInParallel(resourceBaseDirectory: File, + bufferLog: Boolean, + tests: Array[String], + bootProperties: File, + launchOpts: Array[String], + prescripted: java.util.List[File]): Unit } def doScripted(launcher: File, @@ -120,7 +120,12 @@ object Scripted { def get(x: Int): sbt.File = ??? def size(): Int = 0 } - bridge.run(sourcePath, bufferLog, args.toArray, launcher, launchOpts.toArray, callback) + bridge.runInParallel(sourcePath, + bufferLog, + args.toArray, + launcher, + launchOpts.toArray, + callback) } catch { case ite: java.lang.reflect.InvocationTargetException => throw ite.getCause } } } diff --git a/scripted/sbt/src/main/scala/sbt/test/ScriptedTests.scala b/scripted/sbt/src/main/scala/sbt/test/ScriptedTests.scala index f06004d5f..72e62c3be 100644 --- a/scripted/sbt/src/main/scala/sbt/test/ScriptedTests.scala +++ b/scripted/sbt/src/main/scala/sbt/test/ScriptedTests.scala @@ -8,21 +8,21 @@ package test import java.io.File import scala.util.control.NonFatal - import sbt.internal.scripted.{ CommentHandler, FileCommands, ScriptRunner, - TestScriptParser, - TestException + TestException, + TestScriptParser } import sbt.io.{ DirectoryFilter, HiddenFileFilter } import sbt.io.IO.wrapNull import sbt.internal.io.Resources - import sbt.internal.util.{ BufferedLogger, ConsoleLogger, FullLogger } import sbt.util.{ AbstractLogger, Logger } +import scala.collection.parallel.mutable.ParSeq + final class ScriptedTests(resourceBaseDirectory: File, bufferLog: Boolean, launcher: File, @@ -40,7 +40,7 @@ final class ScriptedTests(resourceBaseDirectory: File, def scriptedTest(group: String, name: String, prescripted: File => Unit, - log: Logger): Seq[() => Option[String]] = { + log: Logger): Seq[TestRunner] = { import sbt.io.syntax._ for (groupDir <- (resourceBaseDirectory * group).get; nme <- (groupDir * name).get) yield { val g = groupDir.getName @@ -119,6 +119,10 @@ final class ScriptedTests(resourceBaseDirectory: File, } object ScriptedTests extends ScriptedRunner { + + /** Represents the function that runs the scripted tests. */ + type TestRunner = () => Option[String] + val emptyCallback: File => Unit = { _ => () } @@ -136,8 +140,6 @@ object ScriptedTests extends ScriptedRunner { } class ScriptedRunner { - import ScriptedTests._ - // This is called by project/Scripted.scala // Using java.util.List[File] to encode File => Unit def run(resourceBaseDirectory: File, @@ -166,17 +168,56 @@ class ScriptedRunner { runAll(allTests) } - def runAll(tests: Seq[() => Option[String]]): Unit = { + def runInParallel(resourceBaseDirectory: File, + bufferLog: Boolean, + tests: Array[String], + bootProperties: File, + launchOpts: Array[String], + prescripted: java.util.List[File]): Unit = { + val logger = ConsoleLogger() + val addTestFile = (f: File) => { prescripted.add(f); () } + runInParallel(resourceBaseDirectory, + bufferLog, + tests, + logger, + bootProperties, + launchOpts, + addTestFile) + } + + def runInParallel( + resourceBaseDirectory: File, + bufferLog: Boolean, + tests: Array[String], + logger: AbstractLogger, + bootProperties: File, + launchOpts: Array[String], + prescripted: File => Unit + ): Unit = { + val runner = new ScriptedTests(resourceBaseDirectory, bufferLog, bootProperties, launchOpts) + val scriptedTests = get(tests, resourceBaseDirectory, logger) + val scriptedTestRunners = scriptedTests + .flatMap(t => runner.scriptedTest(t.group, t.name, prescripted, logger)) + runAllInParallel(scriptedTestRunners.toParArray) + } + + def runAll(tests: Seq[ScriptedTests.TestRunner]): Unit = { val errors = for (test <- tests; err <- test()) yield err if (errors.nonEmpty) sys.error(errors.mkString("Failed tests:\n\t", "\n\t", "\n")) } + def runAllInParallel(tests: ParSeq[ScriptedTests.TestRunner]): Unit = { + val executedTests = tests.flatMap(test => test.apply().toList).toList + if (executedTests.nonEmpty) + sys.error(executedTests.mkString("Failed tests:\n\t", "\n\t", "\n")) + } + def get(tests: Seq[String], baseDirectory: File, log: Logger): Seq[ScriptedTest] = if (tests.isEmpty) listTests(baseDirectory, log) else parseTests(tests) def listTests(baseDirectory: File, log: Logger): Seq[ScriptedTest] = - (new ListTests(baseDirectory, _ => true, log)).listTests + new ListTests(baseDirectory, _ => true, log).listTests def parseTests(in: Seq[String]): Seq[ScriptedTest] = for (testString <- in) yield {