From 7afc9e77c61c222caaa2a2df1634d03290c58b48 Mon Sep 17 00:00:00 2001 From: Eugene Vigdorchik Date: Sun, 1 Apr 2012 11:44:05 +0400 Subject: [PATCH] \'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 +}