package mill.scalalib import ammonite.util.Colors import mill.Agg import mill.modules.Jvm import mill.scalalib.Lib.discoverTests import mill.util.{Ctx, PrintLogger} import mill.util.JsonFormatters._ import sbt.testing._ import scala.collection.mutable object TestRunner { def main(args: Array[String]): Unit = { try{ var i = 0 def readArray() = { val count = args(i).toInt val slice = args.slice(i + 1, i + count + 1) i = i + count + 1 slice } val frameworks = readArray() val classpath = readArray() val arguments = readArray() val outputPath = args(i + 0) val colored = args(i + 1) val testCp = args(i + 2) val homeStr = args(i + 3) val ctx = new Ctx.Log with Ctx.Home { val log = PrintLogger( colored == "true", true, if(colored == "true") Colors.Default else Colors.BlackWhite, System.out, System.err, System.err, System.in, debugEnabled = false ) val home = os.Path(homeStr) } val result = runTests( frameworkInstances = TestRunner.frameworks(frameworks), entireClasspath = Agg.from(classpath.map(os.Path(_))), testClassfilePath = Agg(os.Path(testCp)), args = arguments )(ctx) // Clear interrupted state in case some badly-behaved test suite // dirtied the thread-interrupted flag and forgot to clean up. Otherwise // that flag causes writing the results to disk to fail Thread.interrupted() ammonite.ops.write(os.Path(outputPath), upickle.default.write(result)) }catch{case e: Throwable => println(e) e.printStackTrace() } // Tests are over, kill the JVM whether or not anyone's threads are still running // Always return 0, even if tests fail. The caller can pick up the detailed test // results from the outputPath System.exit(0) } def runTests(frameworkInstances: ClassLoader => Seq[sbt.testing.Framework], entireClasspath: Agg[os.Path], testClassfilePath: Agg[os.Path], args: Seq[String]) (implicit ctx: Ctx.Log with Ctx.Home): (String, Seq[mill.scalalib.TestRunner.Result]) = { //Leave the context class loader set and open so that shutdown hooks can access it Jvm.inprocess(entireClasspath, classLoaderOverrideSbtTesting = true, isolated = true, closeContextClassLoaderWhenDone = false, cl => { val frameworks = frameworkInstances(cl) val events = mutable.Buffer.empty[Event] val doneMessages = frameworks.map{ framework => val runner = framework.runner(args.toArray, Array[String](), cl) val testClasses = discoverTests(cl, framework, testClassfilePath) val tasks = runner.tasks( for ((cls, fingerprint) <- testClasses.toArray) yield new TaskDef(cls.getName.stripSuffix("$"), fingerprint, true, Array(new SuiteSelector)) ) val taskQueue = tasks.to[mutable.Queue] while (taskQueue.nonEmpty){ val next = taskQueue.dequeue().execute( new EventHandler { def handle(event: Event) = events.append(event) }, Array( new Logger { def debug(msg: String) = ctx.log.outputStream.println(msg) def error(msg: String) = ctx.log.outputStream.println(msg) def ansiCodesSupported() = true def warn(msg: String) = ctx.log.outputStream.println(msg) def trace(t: Throwable) = t.printStackTrace(ctx.log.outputStream) def info(msg: String) = ctx.log.outputStream.println(msg) }) ) taskQueue.enqueue(next:_*) } runner.done() } val results = for(e <- events) yield { val ex = if (e.throwable().isDefined) Some(e.throwable().get) else None mill.scalalib.TestRunner.Result( e.fullyQualifiedName(), e.selector() match{ case s: NestedSuiteSelector => s.suiteId() case s: NestedTestSelector => s.suiteId() + "." + s.testName() case s: SuiteSelector => s.toString case s: TestSelector => s.testName() case s: TestWildcardSelector => s.testWildcard() }, e.duration(), e.status().toString, ex.map(_.getClass.getName), ex.map(_.getMessage), ex.map(_.getStackTrace) ) } (doneMessages.mkString("\n"), results) }) } def frameworks(frameworkNames: Seq[String])(cl: ClassLoader): Seq[sbt.testing.Framework] = { frameworkNames.map { name => cl.loadClass(name).newInstance().asInstanceOf[sbt.testing.Framework] } } case class Result(fullyQualifiedName: String, selector: String, duration: Long, status: String, exceptionName: Option[String] = None, exceptionMsg: Option[String] = None, exceptionTrace: Option[Seq[StackTraceElement]] = None) object Result{ implicit def resultRW: upickle.default.ReadWriter[Result] = upickle.default.macroRW[Result] } }