Apply setup and cleanup hooks to forked tests.

The actual class loader for test classes cannot be provided because it
is in another jvm.
This commit is contained in:
Mark Harrah 2013-07-16 13:55:10 -04:00
parent c2df1e8d68
commit c0e06a14fe
6 changed files with 170 additions and 63 deletions

View File

@ -10,80 +10,93 @@ import java.io._
import Tests.{Output => TestOutput, _}
import ForkMain._
private[sbt] object ForkTests {
private[sbt] object ForkTests
{
def apply(runners: Map[TestFramework, Runner], tests: List[TestDefinition], config: Execution, classpath: Seq[File], fork: ForkOptions, log: Logger): Task[TestOutput] = {
val opts = processOptions(config, tests, log)
val testListeners = opts.testListeners flatMap {
case tl: TestsListener => Some(tl)
case _ => None
import std.TaskExtra._
val dummyLoader = this.getClass.getClassLoader // can't provide the loader for test classes, which is in another jvm
def all(work: Seq[ClassLoader => Unit]) = work.fork(f => f(dummyLoader))
val main =
if(opts.tests.isEmpty)
constant( TestOutput(TestResult.Passed, Map.empty[String, SuiteResult], Iterable.empty) )
else
mainTestTask(runners, opts, classpath, fork, log).tagw(config.tags: _*)
main.dependsOn( all(opts.setup) : _*) flatMap { results =>
all(opts.cleanup).join.map( _ => results)
}
}
std.TaskExtra.task {
if (!tests.isEmpty) {
val server = new ServerSocket(0)
object Acceptor extends Runnable {
val resultsAcc = mutable.Map.empty[String, SuiteResult]
lazy val result = TestOutput(overall(resultsAcc.values.map(_.result)), resultsAcc.toMap, Iterable.empty)
def run: Unit = {
val socket =
try {
server.accept()
} catch {
case _: java.net.SocketException => return
}
val os = new ObjectOutputStream(socket.getOutputStream)
val is = new ObjectInputStream(socket.getInputStream)
private[this] def mainTestTask(runners: Map[TestFramework, Runner], opts: ProcessedOptions, classpath: Seq[File], fork: ForkOptions, log: Logger): Task[TestOutput] =
std.TaskExtra.task
{
val server = new ServerSocket(0)
val testListeners = opts.testListeners flatMap {
case tl: TestsListener => Some(tl)
case _ => None
}
object Acceptor extends Runnable {
val resultsAcc = mutable.Map.empty[String, SuiteResult]
lazy val result = TestOutput(overall(resultsAcc.values.map(_.result)), resultsAcc.toMap, Iterable.empty)
def run: Unit = {
val socket =
try {
os.writeBoolean(log.ansiCodesSupported)
val testsFiltered = opts.tests.map{
t => new TaskDef(t.name, forkFingerprint(t.fingerprint), t.explicitlySpecified, t.selectors)
}.toArray
os.writeObject(testsFiltered)
os.writeInt(runners.size)
for ((testFramework, mainRunner) <- runners) {
val remoteArgs = mainRunner.remoteArgs()
os.writeObject(testFramework.implClassNames.toArray)
os.writeObject(mainRunner.args)
os.writeObject(remoteArgs)
}
os.flush()
(new React(is, os, log, opts.testListeners, resultsAcc)).react()
} finally {
is.close(); os.close(); socket.close()
server.accept()
} catch {
case _: java.net.SocketException => return
}
val os = new ObjectOutputStream(socket.getOutputStream)
val is = new ObjectInputStream(socket.getInputStream)
try {
os.writeBoolean(log.ansiCodesSupported)
val taskdefs = opts.tests.map(t => new TaskDef(t.name, forkFingerprint(t.fingerprint), t.explicitlySpecified, t.selectors))
os.writeObject(taskdefs.toArray)
os.writeInt(runners.size)
for ((testFramework, mainRunner) <- runners) {
val remoteArgs = mainRunner.remoteArgs()
os.writeObject(testFramework.implClassNames.toArray)
os.writeObject(mainRunner.args)
os.writeObject(remoteArgs)
}
os.flush()
(new React(is, os, log, opts.testListeners, resultsAcc)).react()
} finally {
is.close(); os.close(); socket.close()
}
}
}
try {
testListeners.foreach(_.doInit())
val acceptorThread = new Thread(Acceptor)
acceptorThread.start()
try {
testListeners.foreach(_.doInit())
val acceptorThread = new Thread(Acceptor)
acceptorThread.start()
val fullCp = classpath ++: Seq(IO.classLocationFile[ForkMain], IO.classLocationFile[Framework])
val options = Seq("-classpath", fullCp mkString File.pathSeparator, classOf[ForkMain].getCanonicalName, server.getLocalPort.toString)
val ec = Fork.java(fork, options)
val result =
if (ec != 0)
TestOutput(TestResult.Error, Map("Running java with options " + options.mkString(" ") + " failed with exit code " + ec -> SuiteResult.Error), Iterable.empty)
else {
// Need to wait acceptor thread to finish its business
acceptorThread.join()
Acceptor.result
}
testListeners.foreach(_.doComplete(result.overall))
result
} finally {
server.close()
}
}
val fullCp = classpath ++: Seq(IO.classLocationFile[ForkMain], IO.classLocationFile[Framework])
val options = Seq("-classpath", fullCp mkString File.pathSeparator, classOf[ForkMain].getCanonicalName, server.getLocalPort.toString)
val ec = Fork.java(fork, options)
val result =
if (ec != 0)
TestOutput(TestResult.Error, Map("Running java with options " + options.mkString(" ") + " failed with exit code " + ec -> SuiteResult.Error), Iterable.empty)
else {
// Need to wait acceptor thread to finish its business
acceptorThread.join()
Acceptor.result
}
testListeners.foreach(_.doComplete(result.overall))
result
} finally {
server.close()
}
} else
TestOutput(TestResult.Passed, Map.empty[String, SuiteResult], Iterable.empty)
} tagw (config.tags: _*)
}
private[this] def forkFingerprint(f: Fingerprint): Fingerprint with Serializable =
f match {
case s: SubclassFingerprint => new ForkMain.SubclassFingerscan(s)

View File

@ -0,0 +1 @@
libraryDependencies += "org.scalatest" %% "scalatest" % "1.9.1"

View File

@ -0,0 +1 @@
fork in Test := true

View File

@ -0,0 +1,14 @@
testOptions in Test +=
Tests.Setup { () =>
IO.touch(baseDirectory.value / "setup")
}
testOptions in Test += {
val t = baseDirectory.value / "tested"
val c = baseDirectory.value / "cleanup"
Tests.Cleanup { () =>
assert(t.exists, "Didn't exist: " + t.getAbsolutePath)
IO.delete(t)
IO.touch(c)
}
}

View File

@ -0,0 +1,18 @@
import org.scalatest.FlatSpec
import org.scalatest.matchers.MustMatchers
import java.io.File
class CheckSetupCleanup extends FlatSpec with MustMatchers
{
val f = new File("setup")
"setup file" must "exist" in {
assert(f.exists, "Setup file didn't exist: " + f.getAbsolutePath)
f.delete()
}
val t = new File("tested")
"cleanup file" must "not exist" in {
assert(!t.exists, "Cleanup file already existed: " + t.getAbsolutePath)
t.createNewFile()
}
}

View File

@ -0,0 +1,60 @@
# check that we are starting clean
$ absent setup
$ absent tested
$ absent cleanup
# without Setup configured, the setup file won't exist and the test will fail
-> test
# check that we are starting clean
$ absent setup
$ exists tested
$ delete tested
$ absent cleanup
$ copy-file changes/setup.sbt setup.sbt
> reload
> test
# setup should have been deleted by the test
$ absent setup
# tested should have been deleted by the cleanup
$ absent tested
# cleanup should have been created by cleanup
$ exists cleanup
$ delete cleanup
# Same test for forking
$ delete setup.sbt
$ copy-file changes/fork.sbt fork.sbt
> reload
# check that we are starting clean
$ absent setup
$ absent tested
$ absent cleanup
# without Setup configured, the setup file won't exist and the test will fail
-> test
# check that we are starting clean
$ absent setup
$ exists tested
$ delete tested
$ absent cleanup
$ copy-file changes/setup.sbt setup.sbt
> reload
> test
# setup should have been deleted by the test
$ absent setup
# tested should have been deleted by the cleanup
$ absent tested
# cleanup should have been created by cleanup
$ exists cleanup