From 282d9a667b69863fefbddb83ade4b9efbc98b994 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Sat, 30 Dec 2017 18:00:18 -0800 Subject: Make use of `T.command`s `T.ctx().dest` in `ScalaPlugin#test.test` and `forkTest`, and allow users to dump the structured JSON test results via `--show` --- core/src/main/scala/mill/eval/Evaluator.scala | 19 ++++-- core/src/main/scala/mill/main/RunScript.scala | 2 +- core/src/main/scala/mill/modules/Jvm.scala | 7 ++- core/src/main/scala/mill/util/JsonFormatters.scala | 27 +++++++-- .../test/scala/mill/eval/JavaCompileJarTests.scala | 2 +- readme.md | 10 ++++ .../main/scala/mill/scalaplugin/ScalaModule.scala | 32 ++++++---- .../main/scala/mill/scalaplugin/TestRunner.scala | 70 ++++++++++++++++++---- 8 files changed, 132 insertions(+), 37 deletions(-) diff --git a/core/src/main/scala/mill/eval/Evaluator.scala b/core/src/main/scala/mill/eval/Evaluator.scala index 0a295ee1..ebb21a94 100644 --- a/core/src/main/scala/mill/eval/Evaluator.scala +++ b/core/src/main/scala/mill/eval/Evaluator.scala @@ -14,7 +14,12 @@ import scala.collection.mutable case class Labelled[T](target: NamedTask[T], segments: Seq[Segment]){ def format = target match{ - case t: Target[Any] => Some(t.readWrite.asInstanceOf[upickle.default.ReadWriter[Any]]) + case t: Target[T] => Some(t.readWrite.asInstanceOf[upickle.default.ReadWriter[T]]) + case _ => None + } + def writer = target match{ + case t: mill.define.Command[T] => Some(t.writer.asInstanceOf[upickle.default.Writer[T]]) + case t: Target[T] => Some(t.readWrite.asInstanceOf[upickle.default.ReadWriter[T]]) case _ => None } } @@ -90,12 +95,14 @@ class Evaluator[T](val workspacePath: Path, json <- scala.util.Try(upickle.json.read(read(paths.meta))).toOption (cachedHash, terminalResult) <- scala.util.Try(upickle.default.readJs[(Int, upickle.Js.Value)](json)).toOption if cachedHash == inputsHash - } yield terminalResult + reader <- labelledTarget.format + parsed <- reader.read.lift(terminalResult) + } yield parsed cached match{ - case Some(terminalResult) => + case Some(parsed) => val newResults = mutable.LinkedHashMap.empty[Task[_], Result[Any]] - newResults(labelledTarget.target) = labelledTarget.format.get.read(terminalResult) + newResults(labelledTarget.target) = parsed (newResults, Nil) case _ => @@ -116,8 +123,8 @@ class Evaluator[T](val workspacePath: Path, newResults(labelledTarget.target) match{ case Result.Success(v) => val terminalResult = labelledTarget - .format - .asInstanceOf[Option[upickle.default.ReadWriter[Any]]] + .writer + .asInstanceOf[Option[upickle.default.Writer[Any]]] .map(_.write(v)) for(t <- terminalResult){ diff --git a/core/src/main/scala/mill/main/RunScript.scala b/core/src/main/scala/mill/main/RunScript.scala index 22d6fc95..ad813026 100644 --- a/core/src/main/scala/mill/main/RunScript.scala +++ b/core/src/main/scala/mill/main/RunScript.scala @@ -141,7 +141,7 @@ object RunScript{ case 0 => val json = for(t <- Seq(target)) yield { t match { - case t: mill.define.Target[_] => + case t: mill.define.NamedTask[_] => for (segments <- evaluator.mapping.modules.get(t.owner)) yield { val jsonFile = Evaluator.resolveDestPaths(evaluator.workspacePath, segments :+ Segment.Label(t.name)).meta val metadata = upickle.json.read(jsonFile.toIO) diff --git a/core/src/main/scala/mill/modules/Jvm.scala b/core/src/main/scala/mill/modules/Jvm.scala index f366a0e0..3bed0917 100644 --- a/core/src/main/scala/mill/modules/Jvm.scala +++ b/core/src/main/scala/mill/modules/Jvm.scala @@ -58,7 +58,8 @@ object Jvm { val stdout = proc.getInputStream val stderr = proc.getErrorStream - val sources = Seq(stdout , stderr) + val sources = Seq(stdout -> (Left(_: Bytes)), stderr -> (Right(_: Bytes))) + val chunks = mutable.Buffer.empty[Either[Bytes, Bytes]] while( // Process.isAlive doesn't exist on JDK 7 =/ util.Try(proc.exitValue).isFailure || @@ -66,11 +67,12 @@ object Jvm { stderr.available() > 0 ){ var readSomething = false - for (std <- sources){ + for ((std, wrapper) <- sources){ while (std.available() > 0){ readSomething = true val array = new Array[Byte](std.available()) val actuallyRead = std.read(array) + chunks.append(wrapper(new ammonite.ops.Bytes(array))) ctx.log.outputStream.write(array, 0, actuallyRead) } } @@ -80,6 +82,7 @@ object Jvm { } if (proc.exitValue() != 0) throw new InteractiveShelloutException() + else ammonite.ops.CommandResult(proc.exitValue(), chunks) } private def createManifest(mainClass: Option[String]) = { diff --git a/core/src/main/scala/mill/util/JsonFormatters.scala b/core/src/main/scala/mill/util/JsonFormatters.scala index d8125ff2..00a40e7d 100644 --- a/core/src/main/scala/mill/util/JsonFormatters.scala +++ b/core/src/main/scala/mill/util/JsonFormatters.scala @@ -1,17 +1,18 @@ package mill.util import ammonite.ops.{Bytes, Path} +import upickle.Js import upickle.default.{ReadWriter => RW} object JsonFormatters extends JsonFormatters trait JsonFormatters { implicit val pathReadWrite: RW[ammonite.ops.Path] = RW[ammonite.ops.Path]( - o => upickle.Js.Str(o.toString()), - {case upickle.Js.Str(json) => Path(json.toString)}, + o => Js.Str(o.toString()), + {case Js.Str(json) => Path(json.toString)}, ) implicit val bytesReadWrite: RW[Bytes] = RW[Bytes]( - o => upickle.Js.Str(javax.xml.bind.DatatypeConverter.printBase64Binary(o.array)), - {case upickle.Js.Str(json) => new Bytes(javax.xml.bind.DatatypeConverter.parseBase64Binary(json.toString))} + o => Js.Str(javax.xml.bind.DatatypeConverter.printBase64Binary(o.array)), + {case Js.Str(json) => new Bytes(javax.xml.bind.DatatypeConverter.parseBase64Binary(json.toString))} ) @@ -20,4 +21,22 @@ trait JsonFormatters { implicit lazy val modFormat: RW[coursier.Module] = upickle.default.macroRW implicit lazy val depFormat: RW[coursier.Dependency]= upickle.default.macroRW implicit lazy val attrFormat: RW[coursier.Attributes] = upickle.default.macroRW + implicit val stackTraceRW = upickle.default.ReadWriter[StackTraceElement]( + ste => Js.Obj( + "declaringClass" -> Js.Str(ste.getClassName), + "methodName" -> Js.Str(ste.getMethodName), + "fileName" -> Js.Str(ste.getFileName), + "lineNumber" -> Js.Num(ste.getLineNumber) + ), + {case json: Js.Obj => + new StackTraceElement( + json("declaringClass").str.toString, + json("methodName").str.toString, + json("fileName").str.toString, + json("lineNumber").num.toInt + ) + } + ) + + } diff --git a/core/src/test/scala/mill/eval/JavaCompileJarTests.scala b/core/src/test/scala/mill/eval/JavaCompileJarTests.scala index ba74dea3..0efd0bf1 100644 --- a/core/src/test/scala/mill/eval/JavaCompileJarTests.scala +++ b/core/src/test/scala/mill/eval/JavaCompileJarTests.scala @@ -50,7 +50,7 @@ object JavaCompileJarTests extends TestSuite{ import Build._ val mapping = Discovered.mapping(Build) - def eval[T](t: Task[T]): Either[Result.Failing, (T, Int)] = { + def eval[T](t: Task[T]) = { val evaluator = new Evaluator(workspacePath, mapping, DummyLogger) val evaluated = evaluator.evaluate(OSet(t)) diff --git a/readme.md b/readme.md index 30c473c4..875e9a56 100644 --- a/readme.md +++ b/readme.md @@ -34,6 +34,16 @@ There is already a `watch` option that looks for changes on files, e.g.: ./bin/target/mill --watch Core.compile ``` +You can get Mill to show the JSON-structured output for a particular `Target` or +`Command` using the `--show` flag: + +```bash +./bin/target/mill --show Core.scalaVersion +./bin/target/mill --show Core.compile +./bin/target/mill --show Core.assemblyClasspath +./bin/target/mill --show Core.test +``` + Output will be generated into a the `./out` folder. If you are repeatedly testing Mill manually by running it against the `build.sc` diff --git a/scalaplugin/src/main/scala/mill/scalaplugin/ScalaModule.scala b/scalaplugin/src/main/scala/mill/scalaplugin/ScalaModule.scala index 13e91c2a..4fe3ebc2 100644 --- a/scalaplugin/src/main/scala/mill/scalaplugin/ScalaModule.scala +++ b/scalaplugin/src/main/scala/mill/scalaplugin/ScalaModule.scala @@ -8,8 +8,18 @@ import mill.define.Task.{Module, TaskModule} import mill.eval.{PathRef, Result} import mill.modules.Jvm import mill.modules.Jvm.{createAssembly, createJar, interactiveSubprocess, subprocess} - import Lib._ +import sbt.testing.Status +object TestScalaModule{ + def handleResults(doneMsg: String, results: Seq[TestRunner.Result]) = { + if (results.count(Set(Status.Error, Status.Failure)) == 0) Result.Success((doneMsg, results)) + else { + val grouped = results.map(_.status).groupBy(x => x).mapValues(_.length).filter(_._2 != 0).toList.sorted + + Result.Failure(grouped.map{case (k, v) => k + ": " + v}.mkString(",")) + } + } +} trait TestScalaModule extends ScalaModule with TaskModule { override def defaultCommandName() = "test" def testFramework: T[String] @@ -17,7 +27,8 @@ trait TestScalaModule extends ScalaModule with TaskModule { def forkWorkingDir = ammonite.ops.pwd def forkArgs = T{ Seq.empty[String] } def forkTest(args: String*) = T.command{ - val outputPath = tmp.dir()/"out.json" + mkdir(T.ctx().dest) + val outputPath = T.ctx().dest/"out.json" Jvm.subprocess( mainClass = "mill.scalaplugin.TestRunner", @@ -32,21 +43,20 @@ trait TestScalaModule extends ScalaModule with TaskModule { ), workingDir = forkWorkingDir ) - upickle.default.read[Option[String]](ammonite.ops.read(outputPath)) match{ - case Some(errMsg) => Result.Failure(errMsg) - case None => Result.Success(()) - } + + val jsonOutput = upickle.json.read(outputPath.toIO) + val (doneMsg, results) = upickle.default.readJs[(String, Seq[TestRunner.Result])](jsonOutput) + TestScalaModule.handleResults(doneMsg, results) + } def test(args: String*) = T.command{ - TestRunner( + val (doneMsg, results) = TestRunner( testFramework(), runDepClasspath().map(_.path) :+ compile().classes.path, Seq(compile().classes.path), args - ) match{ - case Some(errMsg) => Result.Failure(errMsg) - case None => Result.Success(()) - } + ) + TestScalaModule.handleResults(doneMsg, results) } } diff --git a/scalaplugin/src/main/scala/mill/scalaplugin/TestRunner.scala b/scalaplugin/src/main/scala/mill/scalaplugin/TestRunner.scala index bc36d9c7..8819d452 100644 --- a/scalaplugin/src/main/scala/mill/scalaplugin/TestRunner.scala +++ b/scalaplugin/src/main/scala/mill/scalaplugin/TestRunner.scala @@ -9,7 +9,8 @@ import ammonite.ops.{Path, ls, pwd} import mill.util.Ctx.LogCtx import mill.util.PrintLogger import sbt.testing._ - +import upickle.Js +import mill.util.JsonFormatters._ import scala.collection.mutable object TestRunner { @@ -49,6 +50,7 @@ object TestRunner { def log = new PrintLogger(true) }) val outputPath = args(4) + ammonite.ops.write(Path(outputPath), upickle.default.write(result)) // Tests are over, kill the JVM whether or not anyone's threads are still running @@ -60,7 +62,7 @@ object TestRunner { entireClasspath: Seq[Path], testClassfilePath: Seq[Path], args: Seq[String]) - (implicit ctx: LogCtx): Option[String] = { + (implicit ctx: LogCtx): (String, Seq[Result]) = { val outerClassLoader = getClass.getClassLoader val cl = new URLClassLoader( entireClasspath.map(_.toIO.toURI.toURL).toArray, @@ -88,11 +90,11 @@ object TestRunner { new TaskDef(cls.getName.stripSuffix("$"), fingerprint, true, Array()) } ) - val events = mutable.Buffer.empty[Status] + val events = mutable.Buffer.empty[Event] for(t <- tasks){ t.execute( new EventHandler { - def handle(event: Event) = events.append(event.status()) + def handle(event: Event) = events.append(event) }, Array( new Logger { @@ -111,14 +113,58 @@ object TestRunner { ) } val doneMsg = runner.done() - val msg = - if (doneMsg.trim.nonEmpty) doneMsg - else{ - val grouped = events.groupBy(x => x).mapValues(_.length).filter(_._2 != 0).toList.sorted - grouped.map{case (k, v) => k + ": " + v}.mkString(",") + + val results = for(e <- events) yield { + val ex = if (e.throwable().isDefined) Some(e.throwable().get) else None + 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(), + ex.map(_.getClass.getName), + ex.map(_.getMessage), + ex.map(_.getStackTrace) + ) + } + (doneMsg, results) + } + + case class Result(fullyQualifiedName: String, + selector: String, + duration: Long, + status: Status, + exceptionName: Option[String], + exceptionMsg: Option[String], + exceptionTrace: Option[Seq[StackTraceElement]]) + + object Result{ + implicit def resultRW: upickle.default.ReadWriter[Result] = upickle.default.macroRW[Result] + implicit def statusRW: upickle.default.ReadWriter[Status] = upickle.default.ReadWriter[Status]( + { + case Status.Success => Js.Str("Success") + case Status.Error => Js.Str("Error") + case Status.Failure => Js.Str("Failure") + case Status.Skipped => Js.Str("Skipped") + case Status.Ignored => Js.Str("Ignored") + case Status.Canceled => Js.Str("Canceled") + case Status.Pending => Js.Str("Pending") + }, + { + case Js.Str("Success") => Status.Success + case Js.Str("Error") => Status.Error + case Js.Str("Failure") => Status.Failure + case Js.Str("Skipped") => Status.Skipped + case Js.Str("Ignored") => Status.Ignored + case Js.Str("Canceled") => Status.Canceled + case Js.Str("Pending") => Status.Pending } - ctx.log.info(msg) - if (events.count(Set(Status.Error, Status.Failure)) == 0) None - else Some(msg) + ) } + } -- cgit v1.2.3