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 :)
This commit is contained in:
jvican 2017-04-28 11:41:22 +02:00
parent 23762a3ac5
commit f497e15cd8
No known key found for this signature in database
GPG Key ID: 42DAFA0F112E8050
2 changed files with 62 additions and 16 deletions

View File

@ -89,12 +89,12 @@ object Scripted {
// Interface to cross class loader // Interface to cross class loader
type SbtScriptedRunner = { type SbtScriptedRunner = {
def run(resourceBaseDirectory: File, def runInParallel(resourceBaseDirectory: File,
bufferLog: Boolean, bufferLog: Boolean,
tests: Array[String], tests: Array[String],
bootProperties: File, bootProperties: File,
launchOpts: Array[String], launchOpts: Array[String],
prescripted: java.util.List[File]): Unit prescripted: java.util.List[File]): Unit
} }
def doScripted(launcher: File, def doScripted(launcher: File,
@ -120,7 +120,12 @@ object Scripted {
def get(x: Int): sbt.File = ??? def get(x: Int): sbt.File = ???
def size(): Int = 0 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 } } catch { case ite: java.lang.reflect.InvocationTargetException => throw ite.getCause }
} }
} }

View File

@ -8,21 +8,21 @@ package test
import java.io.File import java.io.File
import scala.util.control.NonFatal import scala.util.control.NonFatal
import sbt.internal.scripted.{ import sbt.internal.scripted.{
CommentHandler, CommentHandler,
FileCommands, FileCommands,
ScriptRunner, ScriptRunner,
TestScriptParser, TestException,
TestException TestScriptParser
} }
import sbt.io.{ DirectoryFilter, HiddenFileFilter } import sbt.io.{ DirectoryFilter, HiddenFileFilter }
import sbt.io.IO.wrapNull import sbt.io.IO.wrapNull
import sbt.internal.io.Resources import sbt.internal.io.Resources
import sbt.internal.util.{ BufferedLogger, ConsoleLogger, FullLogger } import sbt.internal.util.{ BufferedLogger, ConsoleLogger, FullLogger }
import sbt.util.{ AbstractLogger, Logger } import sbt.util.{ AbstractLogger, Logger }
import scala.collection.parallel.mutable.ParSeq
final class ScriptedTests(resourceBaseDirectory: File, final class ScriptedTests(resourceBaseDirectory: File,
bufferLog: Boolean, bufferLog: Boolean,
launcher: File, launcher: File,
@ -40,7 +40,7 @@ final class ScriptedTests(resourceBaseDirectory: File,
def scriptedTest(group: String, def scriptedTest(group: String,
name: String, name: String,
prescripted: File => Unit, prescripted: File => Unit,
log: Logger): Seq[() => Option[String]] = { log: Logger): Seq[TestRunner] = {
import sbt.io.syntax._ import sbt.io.syntax._
for (groupDir <- (resourceBaseDirectory * group).get; nme <- (groupDir * name).get) yield { for (groupDir <- (resourceBaseDirectory * group).get; nme <- (groupDir * name).get) yield {
val g = groupDir.getName val g = groupDir.getName
@ -119,6 +119,10 @@ final class ScriptedTests(resourceBaseDirectory: File,
} }
object ScriptedTests extends ScriptedRunner { object ScriptedTests extends ScriptedRunner {
/** Represents the function that runs the scripted tests. */
type TestRunner = () => Option[String]
val emptyCallback: File => Unit = { _ => val emptyCallback: File => Unit = { _ =>
() ()
} }
@ -136,8 +140,6 @@ object ScriptedTests extends ScriptedRunner {
} }
class ScriptedRunner { class ScriptedRunner {
import ScriptedTests._
// This is called by project/Scripted.scala // This is called by project/Scripted.scala
// Using java.util.List[File] to encode File => Unit // Using java.util.List[File] to encode File => Unit
def run(resourceBaseDirectory: File, def run(resourceBaseDirectory: File,
@ -166,17 +168,56 @@ class ScriptedRunner {
runAll(allTests) 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 val errors = for (test <- tests; err <- test()) yield err
if (errors.nonEmpty) if (errors.nonEmpty)
sys.error(errors.mkString("Failed tests:\n\t", "\n\t", "\n")) 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] = def get(tests: Seq[String], baseDirectory: File, log: Logger): Seq[ScriptedTest] =
if (tests.isEmpty) listTests(baseDirectory, log) else parseTests(tests) if (tests.isEmpty) listTests(baseDirectory, log) else parseTests(tests)
def listTests(baseDirectory: File, log: Logger): Seq[ScriptedTest] = 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] = def parseTests(in: Seq[String]): Seq[ScriptedTest] =
for (testString <- in) yield { for (testString <- in) yield {