\'fork in test\' initial implementation.

This commit is contained in:
Eugene Vigdorchik 2012-04-01 11:44:05 +04:00
parent eef3a1ed31
commit 7afc9e77c6
9 changed files with 327 additions and 36 deletions

View File

@ -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("<default>", 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")

View File

@ -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

106
main/actions/ForkTests.scala Executable file
View File

@ -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()
}
}
}
}
}

View File

@ -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")
}
}
final case class TestGroup(name: String, tests: Seq[TestDefinition], config: Execution)
}

View File

@ -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)

View File

@ -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)
{

View File

@ -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)
}

View File

@ -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<ForkTestDefinition> filteredTests = new ArrayList<ForkTestDefinition>();
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<ForkEvent> events = new ArrayList<ForkEvent>();
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();
}
}
}

View File

@ -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()
}
}