From eff50df8308e3e1989dcbc509c5efbc5c0d087ac Mon Sep 17 00:00:00 2001 From: Felix Mulder Date: Tue, 4 Apr 2017 21:23:29 +0200 Subject: Add `RunnerOrchestration` to `ParallelTesting` trait --- .../test/dotty/tools/dotc/CompilationTests.scala | 2 +- .../test/dotty/tools/dotc/vulpix/ChildMain.scala | 83 ++++++++++++ .../dotty/tools/dotc/vulpix/ParallelTesting.scala | 143 +++++++++------------ .../test/dotty/tools/dotc/vulpix/Statuses.java | 9 +- 4 files changed, 149 insertions(+), 88 deletions(-) create mode 100644 compiler/test/dotty/tools/dotc/vulpix/ChildMain.scala diff --git a/compiler/test/dotty/tools/dotc/CompilationTests.scala b/compiler/test/dotty/tools/dotc/CompilationTests.scala index 241714f8b..fc7ab8a8d 100644 --- a/compiler/test/dotty/tools/dotc/CompilationTests.scala +++ b/compiler/test/dotty/tools/dotc/CompilationTests.scala @@ -15,7 +15,7 @@ class CompilationTests extends SummaryReport with ParallelTesting { // Test suite configuration -------------------------------------------------- - def maxDuration = 3.seconds + def maxDuration = 180.seconds def numberOfSlaves = 5 def safeMode = sys.env.get("SAFEMODE").isDefined def isInteractive = SummaryReport.isInteractive diff --git a/compiler/test/dotty/tools/dotc/vulpix/ChildMain.scala b/compiler/test/dotty/tools/dotc/vulpix/ChildMain.scala new file mode 100644 index 000000000..fdd602379 --- /dev/null +++ b/compiler/test/dotty/tools/dotc/vulpix/ChildMain.scala @@ -0,0 +1,83 @@ +package dotty.tools.dotc +package vulpix + +import java.io.{ + File => JFile, + InputStream, ObjectInputStream, + OutputStream, ObjectOutputStream, + ByteArrayOutputStream, PrintStream +} +import java.lang.reflect.InvocationTargetException + +import dotty.tools.dotc.vulpix.Statuses._ + +object ChildMain { + val realStdin = System.in + val realStderr = System.err + val realStdout = System.out + + private def runMain(dir: JFile): Status = { + def renderStackTrace(ex: Throwable): String = + ex.getStackTrace + .takeWhile(_.getMethodName != "invoke0") + .mkString(" ", "\n ", "") + + def resetOutDescriptors(): Unit = { + System.setOut(realStdout) + System.setErr(realStderr) + } + + import java.net.{ URL, URLClassLoader } + + val printStream = new ByteArrayOutputStream + + try { + // Do classloading magic and running here: + val ucl = new URLClassLoader(Array(dir.toURI.toURL)) + val cls = ucl.loadClass("Test") + val meth = cls.getMethod("main", classOf[Array[String]]) + + try { + val ps = new PrintStream(printStream) + System.setOut(ps) + System.setErr(ps) + Console.withOut(printStream) { + Console.withErr(printStream) { + // invoke main with "jvm" as arg + meth.invoke(null, Array("jvm")) + } + } + resetOutDescriptors() + } catch { + case t: Throwable => + resetOutDescriptors() + throw t + } + new Success(printStream.toString("utf-8")) + } + catch { + case ex: NoSuchMethodException => + val msg = s"test in '$dir' did not contain method: ${ex.getMessage}" + new Failure(msg, renderStackTrace(ex.getCause)) + + case ex: ClassNotFoundException => + val msg = s"test in '$dir' did not contain class: ${ex.getMessage}" + new Failure(msg, renderStackTrace(ex.getCause)) + + case ex: InvocationTargetException => + val msg = s"An exception ocurred when running main: ${ex.getCause}" + new Failure(msg, renderStackTrace(ex.getCause)) + } + } + + def main(args: Array[String]): Unit = { + val stdin = new ObjectInputStream(System.in); + val stdout = new ObjectOutputStream(System.out); + + while (true) { + val dir = stdin.readObject().asInstanceOf[JFile] + stdout.writeObject(runMain(dir)) + stdout.flush() + } + } +} diff --git a/compiler/test/dotty/tools/dotc/vulpix/ParallelTesting.scala b/compiler/test/dotty/tools/dotc/vulpix/ParallelTesting.scala index 0b5d41329..28c2ea22b 100644 --- a/compiler/test/dotty/tools/dotc/vulpix/ParallelTesting.scala +++ b/compiler/test/dotty/tools/dotc/vulpix/ParallelTesting.scala @@ -6,7 +6,6 @@ package vulpix import java.io.{ File => JFile } import java.text.SimpleDateFormat import java.util.HashMap -import java.lang.reflect.InvocationTargetException import java.nio.file.StandardCopyOption.REPLACE_EXISTING import java.nio.file.{ Files, Path, Paths, NoSuchFileException } import java.util.concurrent.{ Executors => JExecutors, TimeUnit, TimeoutException } @@ -24,13 +23,15 @@ import reporting.diagnostic.MessageContainer import interfaces.Diagnostic.ERROR import dotc.util.DiffUtil +import vulpix.Statuses._ + /** A parallel testing suite whose goal is to integrate nicely with JUnit * * This trait can be mixed in to offer parallel testing to compile runs. When * using this, you should be running your JUnit tests **sequentially**, as the * test suite itself runs with a high level of concurrency. */ -trait ParallelTesting { self => +trait ParallelTesting extends RunnerOrchestration { self => import ParallelTesting._ import SummaryReport._ @@ -225,14 +226,18 @@ trait ParallelTesting { self => /** The test sources that failed according to the implementing subclass */ private[this] val failedTestSources = mutable.ArrayBuffer.empty[String] - protected final def failTestSource(testSource: TestSource) = synchronized { - failedTestSources.append(testSource.name + " failed") + protected final def failTestSource(testSource: TestSource, reason: Option[String] = None) = synchronized { + val extra = reason.map(" with reason: " + _).getOrElse("") + failedTestSources.append(testSource.name + " failed" + extra) fail() } /** Prints to `System.err` if we're not suppressing all output */ - protected def echo(msg: String): Unit = - if (!suppressAllOutput) realStderr.println(msg) + protected def echo(msg: String): Unit = if (!suppressAllOutput) { + // pad right so that output is at least as large as progress bar line + val paddingRight = " " * math.max(0, 80 - msg.length) + realStderr.println(msg + paddingRight) + } /** A single `Runnable` that prints a progress bar for the curent `Test` */ private def createProgressMonitor: Runnable = new Runnable { @@ -418,98 +423,58 @@ trait ParallelTesting { self => echoBuildInstructions(reporters.head, testSource, errorCount, warningCount) } } - } } } private final class RunTest(testSources: List[TestSource], times: Int, threadLimit: Option[Int], suppressAllOutput: Boolean) extends Test(testSources, times, threadLimit, suppressAllOutput) { - private def runMain(dir: JFile, testSource: TestSource): Array[String] = { - def renderStackTrace(ex: Throwable): String = - if (ex == null) "" - else ex.getStackTrace - .takeWhile(_.getMethodName != "invoke0") - .mkString(" ", "\n ", "") - - import java.io.{ ByteArrayOutputStream, PrintStream } - import java.net.{ URL, URLClassLoader } - - val printStream = new ByteArrayOutputStream - - try { - // Do classloading magic and running here: - val ucl = new URLClassLoader(Array(dir.toURI.toURL)) - val cls = ucl.loadClass("Test") - val meth = cls.getMethod("main", classOf[Array[String]]) - - synchronized { - try { - val ps = new PrintStream(printStream) - System.setOut(ps) - System.setErr(ps) - Console.withOut(printStream) { - Console.withErr(printStream) { - meth.invoke(null, Array("jvm")) // partest passes at least "jvm" as an arg - } - } - System.setOut(realStdout) - System.setErr(realStderr) - } catch { - case t: Throwable => - System.setOut(realStdout) - System.setErr(realStderr) - throw t + private def verifyOutput(checkFile: JFile, dir: JFile, testSource: TestSource, warnings: Int) = { + runMain(dir) match { + case success: Success => { + val outputLines = success.output.lines.toArray + val checkLines: Array[String] = Source.fromFile(checkFile).getLines.toArray + val sourceTitle = testSource.title + + def linesMatch = + outputLines + .zip(checkLines) + .forall { case (x, y) => x == y } + + if (outputLines.length != checkLines.length || !linesMatch) { + // Print diff to files and summary: + val diff = outputLines.zip(checkLines).map { case (act, exp) => + DiffUtil.mkColoredLineDiff(exp, act) + }.mkString("\n") + + val msg = + s"""|Output from '$sourceTitle' did not match check file. + |Diff ('e' is expected, 'a' is actual): + |""".stripMargin + diff + "\n" + echo(msg) + addFailureInstruction(msg) + + // Print build instructions to file and summary: + val buildInstr = testSource.buildInstructions(0, warnings) + addFailureInstruction(buildInstr) + + // Fail target: + failTestSource(testSource) } } - } - catch { - case ex: NoSuchMethodException => - echo(s"test in '$dir' did not contain method: ${ex.getMessage}\n${renderStackTrace(ex.getCause)}") - failTestSource(testSource) - case ex: ClassNotFoundException => - echo(s"test in '$dir' did not contain class: ${ex.getMessage}\n${renderStackTrace(ex.getCause)}") + case failure: Failure => + echo(renderFailure(failure)) failTestSource(testSource) - case ex: InvocationTargetException => - echo(s"An exception ocurred when running main: ${ex.getCause}\n${renderStackTrace(ex.getCause)}") - failTestSource(testSource) + case _: Timeout => + echo("failed because test " + testSource.title + " timed out") + failTestSource(testSource, Some("test timed out")) } - printStream.toString("utf-8").lines.toArray } - private def verifyOutput(checkFile: JFile, dir: JFile, testSource: TestSource, warnings: Int) = { - val outputLines = runMain(dir, testSource) - val checkLines = Source.fromFile(checkFile).getLines.toArray - val sourceTitle = testSource.title - - def linesMatch = - outputLines - .zip(checkLines) - .forall { case (x, y) => x == y } - - if (outputLines.length != checkLines.length || !linesMatch) { - // Print diff to files and summary: - val diff = outputLines.zip(checkLines).map { case (act, exp) => - DiffUtil.mkColoredLineDiff(exp, act) - }.mkString("\n") - - val msg = - s"""|Output from '$sourceTitle' did not match check file. - |Diff ('e' is expected, 'a' is actual): - |""".stripMargin + diff + "\n" - echo(msg) - addFailureInstruction(msg) - - // Print build instructions to file and summary: - val buildInstr = testSource.buildInstructions(0, warnings) - addFailureInstruction(buildInstr) - - // Fail target: - failTestSource(testSource) - } - } + private def renderFailure(failure: Failure): String = + failure.message + "\n" + failure.stacktrace protected def compilationRunnable(testSource: TestSource): Runnable = new Runnable { def run(): Unit = tryCompile(testSource) { @@ -554,7 +519,15 @@ trait ParallelTesting { self => } if (errorCount == 0 && hasCheckFile) verifier() - else if (errorCount == 0) runMain(testSource.outDir, testSource) + else if (errorCount == 0) runMain(testSource.outDir) match { + case status: Failure => + echo(renderFailure(status)) + failTestSource(testSource) + case _: Timeout => + echo("failed because test " + testSource.title + " timed out") + failTestSource(testSource, Some("test timed out")) + case _: Success => // success! + } else if (errorCount > 0) { echo(s"\nCompilation failed for: '$testSource'") val buildInstr = testSource.buildInstructions(errorCount, warningCount) diff --git a/compiler/test/dotty/tools/dotc/vulpix/Statuses.java b/compiler/test/dotty/tools/dotc/vulpix/Statuses.java index d5d801c8c..bec687d01 100644 --- a/compiler/test/dotty/tools/dotc/vulpix/Statuses.java +++ b/compiler/test/dotty/tools/dotc/vulpix/Statuses.java @@ -12,8 +12,13 @@ public class Statuses { } static class Failure implements Status, Serializable { - public final String output; - public Failure(String output) { this.output = output; } + public final String message; + public final String stacktrace; + + public Failure(String message, String stacktrace) { + this.message = message; + this.stacktrace = stacktrace; + } } static class Timeout implements Status, Serializable {} -- cgit v1.2.3