summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLi Haoyi <haoyi.sg@gmail.com>2017-12-30 18:00:18 -0800
committerLi Haoyi <haoyi.sg@gmail.com>2017-12-30 18:14:18 -0800
commit282d9a667b69863fefbddb83ade4b9efbc98b994 (patch)
tree06ef80cf9cfd11b80bde22e1c6cffa61905e139b
parent476640d8b78a2d1bc902fcdd15fcef996b73ca2c (diff)
downloadmill-282d9a667b69863fefbddb83ade4b9efbc98b994.tar.gz
mill-282d9a667b69863fefbddb83ade4b9efbc98b994.tar.bz2
mill-282d9a667b69863fefbddb83ade4b9efbc98b994.zip
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`
-rw-r--r--core/src/main/scala/mill/eval/Evaluator.scala19
-rw-r--r--core/src/main/scala/mill/main/RunScript.scala2
-rw-r--r--core/src/main/scala/mill/modules/Jvm.scala7
-rw-r--r--core/src/main/scala/mill/util/JsonFormatters.scala27
-rw-r--r--core/src/test/scala/mill/eval/JavaCompileJarTests.scala2
-rw-r--r--readme.md10
-rw-r--r--scalaplugin/src/main/scala/mill/scalaplugin/ScalaModule.scala32
-rw-r--r--scalaplugin/src/main/scala/mill/scalaplugin/TestRunner.scala70
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)
+ )
}
+
}