summaryrefslogtreecommitdiff
path: root/main
diff options
context:
space:
mode:
authorLi Haoyi <haoyi.sg@gmail.com>2018-02-09 00:14:47 -0800
committerLi Haoyi <haoyi.sg@gmail.com>2018-02-09 08:17:47 -0800
commit8ddd2fa054bc8639c28db2e95b7903e2954fdb7d (patch)
treeaa985f1e715f07eb279e6facad61de8a187e316c /main
parent90d0a3388d280554eaa51371f666d2f7a965a8af (diff)
downloadmill-8ddd2fa054bc8639c28db2e95b7903e2954fdb7d.tar.gz
mill-8ddd2fa054bc8639c28db2e95b7903e2954fdb7d.tar.bz2
mill-8ddd2fa054bc8639c28db2e95b7903e2954fdb7d.zip
.
Diffstat (limited to 'main')
-rw-r--r--main/src/mill/Main.scala83
-rw-r--r--main/src/mill/main/MagicScopt.scala46
-rw-r--r--main/src/mill/main/MainModule.scala56
-rw-r--r--main/src/mill/main/MainRunner.scala120
-rw-r--r--main/src/mill/main/ReplApplyHandler.scala137
-rw-r--r--main/src/mill/main/Resolve.scala144
-rw-r--r--main/src/mill/main/RunScript.scala240
-rw-r--r--main/src/mill/modules/Jvm.scala257
-rw-r--r--main/src/mill/modules/Util.scala65
-rw-r--r--main/src/mill/package.scala12
-rw-r--r--main/test/resources/examples/javac/build.sc23
-rw-r--r--main/test/resources/examples/javac/resources/hello.txt1
-rw-r--r--main/test/resources/examples/javac/src/Bar.java4
-rw-r--r--main/test/resources/examples/javac/src/Foo.java7
-rw-r--r--main/test/src/mill/TestMain.scala6
-rw-r--r--main/test/src/mill/UTestFramework.scala11
-rw-r--r--main/test/src/mill/define/ApplicativeTests.scala125
-rw-r--r--main/test/src/mill/define/BasePathTests.scala68
-rw-r--r--main/test/src/mill/define/CacherTests.scala76
-rw-r--r--main/test/src/mill/define/DiscoverTests.scala60
-rw-r--r--main/test/src/mill/define/GraphTests.scala199
-rw-r--r--main/test/src/mill/define/MacroErrorTests.scala83
-rw-r--r--main/test/src/mill/eval/CrossTests.scala56
-rw-r--r--main/test/src/mill/eval/EvaluationTests.scala317
-rw-r--r--main/test/src/mill/eval/FailureTests.scala100
-rw-r--r--main/test/src/mill/eval/JavaCompileJarTests.scala159
-rw-r--r--main/test/src/mill/eval/ModuleTests.scala45
-rw-r--r--main/test/src/mill/eval/TarjanTests.scala91
-rw-r--r--main/test/src/mill/eval/TaskTests.scala95
-rw-r--r--main/test/src/mill/main/JavaCompileJarTests.scala66
-rw-r--r--main/test/src/mill/main/MainTests.scala222
-rw-r--r--main/test/src/mill/util/ParseArgsTest.scala241
-rw-r--r--main/test/src/mill/util/ScriptTestSuite.scala38
-rw-r--r--main/test/src/mill/util/TestEvaluator.scala83
-rw-r--r--main/test/src/mill/util/TestGraphs.scala242
-rw-r--r--main/test/src/mill/util/TestUtil.scala81
36 files changed, 3659 insertions, 0 deletions
diff --git a/main/src/mill/Main.scala b/main/src/mill/Main.scala
new file mode 100644
index 00000000..3025994c
--- /dev/null
+++ b/main/src/mill/Main.scala
@@ -0,0 +1,83 @@
+package mill
+
+import ammonite.main.Cli.{formatBlock, genericSignature, replSignature}
+import ammonite.ops._
+import ammonite.util.Util
+
+object Main {
+ case class Config(home: ammonite.ops.Path = pwd/'out/'ammonite,
+ colored: Option[Boolean] = None,
+ help: Boolean = false,
+ repl: Boolean = false,
+ watch: Boolean = false)
+
+ def main(args: Array[String]): Unit = {
+
+ import ammonite.main.Cli
+
+ var show = false
+ val showCliArg = Cli.Arg[Cli.Config, Unit](
+ "show",
+ None,
+ "Display the json-formatted value of the given target, if any",
+ (x, _) => {
+ show = true
+ x
+ }
+ )
+ val removed = Set("predef-code", "home", "no-home-predef")
+ val millArgSignature = (Cli.genericSignature :+ showCliArg).filter(a => !removed(a.name))
+ Cli.groupArgs(
+ args.toList,
+ millArgSignature,
+ Cli.Config(remoteLogging = false)
+ ) match{
+ case Left(msg) =>
+ System.err.println(msg)
+ System.exit(1)
+ case Right((cliConfig, _)) if cliConfig.help =>
+ val leftMargin = millArgSignature.map(ammonite.main.Cli.showArg(_).length).max + 2
+ System.out.println(
+ s"""Mill Build Tool
+ |usage: mill [mill-options] [target [target-options]]
+ |
+ |${formatBlock(millArgSignature, leftMargin).mkString(Util.newLine)}""".stripMargin
+ )
+ System.exit(0)
+ case Right((cliConfig, leftoverArgs)) =>
+
+ val repl = leftoverArgs.isEmpty
+ val config =
+ if(!repl) cliConfig
+ else cliConfig.copy(
+ predefCode =
+ """import $file.build, build._
+ |implicit val replApplyHandler = mill.main.ReplApplyHandler(
+ | interp.colors(),
+ | repl.pprinter(),
+ | build.millSelf.get,
+ | build.millDiscover
+ |)
+ |repl.pprinter() = replApplyHandler.pprinter
+ |import replApplyHandler.generatedEval._
+ |
+ """.stripMargin,
+ welcomeBanner = None
+ )
+
+ val runner = new mill.main.MainRunner(
+ config, show,
+ System.out, System.err, System.in
+ )
+ if (repl){
+ runner.printInfo("Loading...")
+ runner.runRepl()
+ } else {
+ val result = runner.runScript(pwd / "build.sc", leftoverArgs)
+ System.exit(if(result) 0 else 1)
+ }
+ }
+ }
+}
+
+
diff --git a/main/src/mill/main/MagicScopt.scala b/main/src/mill/main/MagicScopt.scala
new file mode 100644
index 00000000..e18816c8
--- /dev/null
+++ b/main/src/mill/main/MagicScopt.scala
@@ -0,0 +1,46 @@
+package mill.main
+import mill.define.ExternalModule
+import mill.eval.{Evaluator, PathRef}
+import mill.util.ParseArgs
+
+object MagicScopt{
+
+
+ case class Tasks[T](value: Seq[mill.define.NamedTask[T]])
+}
+class EvaluatorScopt[T]()
+ extends scopt.Read[mill.eval.Evaluator[T]]{
+ def arity = 0
+ def reads = s => try{
+ Evaluator.currentEvaluator.get.asInstanceOf[mill.eval.Evaluator[T]]
+ }
+}
+class TargetScopt[T]()
+ extends scopt.Read[MagicScopt.Tasks[T]]{
+ def arity = 0
+ def reads = s => {
+ val rootModule = Evaluator.currentEvaluator.get.rootModule
+ val d = rootModule.millDiscover
+ val (expanded, leftover) = ParseArgs(Seq(s)).fold(e => throw new Exception(e), identity)
+ val resolved = expanded.map{
+ case (Some(scoping), segments) =>
+ val moduleCls = rootModule.getClass.getClassLoader.loadClass(scoping.render + "$")
+ val externalRootModule = moduleCls.getField("MODULE$").get(moduleCls).asInstanceOf[ExternalModule]
+ val crossSelectors = segments.value.map {
+ case mill.define.Segment.Cross(x) => x.toList.map(_.toString)
+ case _ => Nil
+ }
+ mill.main.Resolve.resolve(segments.value.toList, externalRootModule, d, leftover, crossSelectors.toList, Nil)
+ case (None, segments) =>
+ val crossSelectors = segments.value.map {
+ case mill.define.Segment.Cross(x) => x.toList.map(_.toString)
+ case _ => Nil
+ }
+ mill.main.Resolve.resolve(segments.value.toList, rootModule, d, leftover, crossSelectors.toList, Nil)
+ }
+ mill.util.EitherOps.sequence(resolved) match{
+ case Left(s) => throw new Exception(s)
+ case Right(ts) => MagicScopt.Tasks(ts.flatten).asInstanceOf[MagicScopt.Tasks[T]]
+ }
+ }
+} \ No newline at end of file
diff --git a/main/src/mill/main/MainModule.scala b/main/src/mill/main/MainModule.scala
new file mode 100644
index 00000000..012ecce5
--- /dev/null
+++ b/main/src/mill/main/MainModule.scala
@@ -0,0 +1,56 @@
+package mill.main
+
+import mill.util
+import mill.main.RunScript
+import mill.util.Watched
+import pprint.{Renderer, Truncated}
+
+trait MainModule extends mill.Module{
+
+ implicit def millDiscover: mill.define.Discover[_]
+ implicit def millScoptTargetReads[T] = new mill.main.TargetScopt[T]()
+ implicit def millScoptEvaluatorReads[T] = new mill.main.EvaluatorScopt[T]()
+ mill.define.Ctx.make
+ def resolve(targets: mill.main.MagicScopt.Tasks[Any]*) = mill.T.command{
+ targets.flatMap(_.value).foreach(println)
+ }
+ def describe(evaluator: mill.eval.Evaluator[Any],
+ targets: mill.main.MagicScopt.Tasks[Any]*) = mill.T.command{
+ for{
+ t <- targets
+ target <- t.value
+ tree = ReplApplyHandler.pprintTask(target, evaluator)
+ val defaults = pprint.PPrinter()
+ val renderer = new Renderer(
+ defaults.defaultWidth,
+ defaults.colorApplyPrefix,
+ defaults.colorLiteral,
+ defaults.defaultIndent
+ )
+ val rendered = renderer.rec(tree, 0, 0).iter
+ val truncated = new Truncated(rendered, defaults.defaultWidth, defaults.defaultHeight)
+ str <- truncated ++ Iterator("\n")
+ } {
+ print(str)
+ }
+ }
+ def all(evaluator: mill.eval.Evaluator[Any],
+ targets: mill.main.MagicScopt.Tasks[Any]*) = mill.T.command{
+ val (watched, res) = RunScript.evaluate(
+ evaluator,
+ mill.util.Strict.Agg.from(targets.flatMap(_.value))
+ )
+ Watched((), watched)
+ }
+ def show(evaluator: mill.eval.Evaluator[Any],
+ targets: mill.main.MagicScopt.Tasks[Any]*) = mill.T.command{
+ val (watched, res) = mill.main.RunScript.evaluate(
+ evaluator,
+ mill.util.Strict.Agg.from(targets.flatMap(_.value))
+ )
+ for(json <- res.right.get.flatMap(_._2)){
+ println(json)
+ }
+ Watched((), watched)
+ }
+}
diff --git a/main/src/mill/main/MainRunner.scala b/main/src/mill/main/MainRunner.scala
new file mode 100644
index 00000000..c073e583
--- /dev/null
+++ b/main/src/mill/main/MainRunner.scala
@@ -0,0 +1,120 @@
+package mill.main
+import java.io.{InputStream, OutputStream, PrintStream}
+
+import ammonite.Main
+import ammonite.interp.{Interpreter, Preprocessor}
+import ammonite.ops.Path
+import ammonite.util._
+import mill.define.Discover
+import mill.eval.{Evaluator, PathRef}
+import mill.util.PrintLogger
+import mill.main.RunScript
+import upickle.Js
+
+/**
+ * Customized version of [[ammonite.MainRunner]], allowing us to run Mill
+ * `build.sc` scripts with mill-specific tweaks such as a custom
+ * `scriptCodeWrapper` or with a persistent evaluator between runs.
+ */
+class MainRunner(config: ammonite.main.Cli.Config,
+ show: Boolean,
+ outprintStream: PrintStream,
+ errPrintStream: PrintStream,
+ stdIn: InputStream)
+ extends ammonite.MainRunner(
+ config, outprintStream, errPrintStream,
+ stdIn, outprintStream, errPrintStream
+ ){
+
+ var lastEvaluator: Option[(Seq[(Path, Long)], Evaluator[Any])] = None
+
+ override def runScript(scriptPath: Path, scriptArgs: List[String]) =
+ watchLoop(
+ isRepl = false,
+ printing = true,
+ mainCfg => {
+ val (result, interpWatched) = RunScript.runScript(
+ mainCfg.wd,
+ scriptPath,
+ mainCfg.instantiateInterpreter(),
+ scriptArgs,
+ lastEvaluator,
+ new PrintLogger(
+ colors != ammonite.util.Colors.BlackWhite,
+ colors,
+ if (show) errPrintStream else outprintStream,
+ errPrintStream,
+ errPrintStream
+ )
+ )
+
+ result match{
+ case Res.Success(data) =>
+ val (eval, evaluationWatches, res) = data
+
+ lastEvaluator = Some((interpWatched, eval))
+
+ (Res(res), interpWatched ++ evaluationWatches)
+ case _ => (result, interpWatched)
+ }
+ }
+ )
+
+ override def handleWatchRes[T](res: Res[T], printing: Boolean) = {
+ res match{
+ case Res.Success(value) =>
+// if (show){
+// for(json <- value.asInstanceOf[Seq[Js.Value]]){
+// outprintStream.println(json)
+// }
+// }
+
+ true
+
+ case _ => super.handleWatchRes(res, printing)
+ }
+
+ }
+ override def initMain(isRepl: Boolean) = {
+ super.initMain(isRepl).copy(
+ scriptCodeWrapper = CustomCodeWrapper,
+ // Ammonite does not properly forward the wd from CliConfig to Main, so
+ // force forward it outselves
+ wd = config.wd
+ )
+ }
+ object CustomCodeWrapper extends Preprocessor.CodeWrapper {
+ def top(pkgName: Seq[Name], imports: Imports, indexedWrapperName: Name) = {
+ val wrapName = indexedWrapperName.backticked
+ val literalPath = pprint.Util.literalize(config.wd.toString)
+ s"""
+ |package ${pkgName.head.encoded}
+ |package ${Util.encodeScalaSourcePath(pkgName.tail)}
+ |$imports
+ |import mill._
+ |object $wrapName
+ |extends mill.define.BaseModule(ammonite.ops.Path($literalPath))
+ |with $wrapName{
+ | // Stub to make sure Ammonite has something to call after it evaluates a script,
+ | // even if it does nothing...
+ | def $$main() = Iterator[String]()
+ |
+ | implicit def millDiscover: mill.define.Discover[this.type] = mill.define.Discover[this.type]
+ | // Need to wrap the returned Module in Some(...) to make sure it
+ | // doesn't get picked up during reflective child-module discovery
+ | val millSelf = Some(this)
+ |}
+ |
+ |sealed trait $wrapName extends mill.main.MainModule{
+ |""".stripMargin
+ }
+
+
+ def bottom(printCode: String, indexedWrapperName: Name, extraCode: String) = {
+ // We need to disable the `$main` method definition inside the wrapper class,
+ // because otherwise it might get picked up by Ammonite and run as a static
+ // class method, which blows up since it's defined as an instance method
+ "\n}"
+ }
+ }
+}
diff --git a/main/src/mill/main/ReplApplyHandler.scala b/main/src/mill/main/ReplApplyHandler.scala
new file mode 100644
index 00000000..76ef354f
--- /dev/null
+++ b/main/src/mill/main/ReplApplyHandler.scala
@@ -0,0 +1,137 @@
+package mill.main
+
+
+import mill.define.Applicative.ApplyHandler
+import mill.define.Segment.Label
+import mill.define._
+import mill.eval.{Evaluator, Result}
+import mill.util.Strict.Agg
+
+import scala.collection.mutable
+object ReplApplyHandler{
+ def apply[T](colors: ammonite.util.Colors,
+ pprinter0: pprint.PPrinter,
+ rootModule: mill.define.BaseModule,
+ discover: Discover[_]) = {
+ new ReplApplyHandler(
+ pprinter0,
+ new Evaluator(
+ ammonite.ops.pwd / 'out,
+ ammonite.ops.pwd / 'out,
+ rootModule,
+ discover,
+ new mill.util.PrintLogger(
+ colors != ammonite.util.Colors.BlackWhite,
+ colors,
+ System.out,
+ System.err,
+ System.err
+ )
+ )
+ )
+ }
+ def pprintCross(c: mill.define.Cross[_], evaluator: Evaluator[_]) = {
+ pprint.Tree.Lazy( ctx =>
+ Iterator(c.millOuterCtx.enclosing , ":", c.millOuterCtx.lineNum.toString, ctx.applyPrefixColor("\nChildren:").toString) ++
+ c.items.iterator.map(x =>
+ "\n (" + x._1.map(pprint.PPrinter.BlackWhite.apply(_)).mkString(", ") + ")"
+ )
+ )
+ }
+ def pprintModule(m: mill.define.Module, evaluator: Evaluator[_]) = {
+ pprint.Tree.Lazy( ctx =>
+ Iterator(m.millInternal.millModuleEnclosing, ":", m.millInternal.millModuleLine.toString) ++
+ (if (m.millInternal.reflect[mill.Module].isEmpty) Nil
+ else
+ ctx.applyPrefixColor("\nChildren:").toString +:
+ m.millInternal.reflect[mill.Module].map("\n ." + _.millOuterCtx.segment.pathSegments.mkString("."))) ++
+ (evaluator.discover.value.get(m.getClass) match{
+ case None => Nil
+ case Some(commands) =>
+ ctx.applyPrefixColor("\nCommands:").toString +: commands.map{c =>
+ "\n ." + c._2.name + "(" +
+ c._2.argSignatures.map(s => s.name + ": " + s.typeString).mkString(", ") +
+ ")()"
+ }
+ }) ++
+ (if (m.millInternal.reflect[Target[_]].isEmpty) Nil
+ else {
+ Seq(ctx.applyPrefixColor("\nTargets:").toString) ++
+ m.millInternal.reflect[Target[_]].sortBy(_.label).map(t =>
+ "\n ." + t.label + "()"
+ )
+ })
+
+ )
+ }
+ def pprintTask(t: NamedTask[_], evaluator: Evaluator[_]) = {
+ val seen = mutable.Set.empty[Task[_]]
+ def rec(t: Task[_]): Seq[Segments] = {
+ if (seen(t)) Nil // do nothing
+ else t match {
+ case t: Target[_] if evaluator.rootModule.millInternal.targets.contains(t) =>
+ Seq(t.ctx.segments)
+ case _ =>
+ seen.add(t)
+ t.inputs.flatMap(rec)
+ }
+ }
+ pprint.Tree.Lazy(ctx =>
+ Iterator(
+ t.toString, "(", t.ctx.fileName, ":", t.ctx.lineNum.toString, ")",
+ t.ctx.lineNum.toString, "\n", ctx.applyPrefixColor("Inputs:").toString
+ ) ++ t.inputs.iterator.flatMap(rec).map("\n " + _.render)
+ )
+ }
+
+}
+class ReplApplyHandler(pprinter0: pprint.PPrinter,
+ evaluator: Evaluator[_]) extends ApplyHandler[Task] {
+ // Evaluate classLoaderSig only once in the REPL to avoid busting caches
+ // as the user enters more REPL commands and changes the classpath
+ val classLoaderSig = Evaluator.classLoaderSig
+ override def apply[V](t: Task[V]) = {
+ val res = evaluator.evaluate(Agg(t))
+ res.values match{
+ case Seq(head: V) => head
+ case Nil =>
+ val msg = new mutable.StringBuilder()
+ msg.append(res.failing.keyCount + " targets failed\n")
+ for((k, vs) <- res.failing.items){
+ msg.append(k match{
+ case Left(t) => "Anonymous Task\n"
+ case Right(k) => k.segments.render + "\n"
+ })
+
+ for(v <- vs){
+ v match{
+ case Result.Failure(m, _) => msg.append(m + "\n")
+ case Result.Exception(t, outerStack) =>
+ msg.append(
+ t.toString +
+ t.getStackTrace.dropRight(outerStack.value.length).map("\n " + _).mkString +
+ "\n"
+ )
+
+ }
+ }
+ }
+ throw new Exception(msg.toString)
+ }
+ }
+
+ val generatedEval = new EvalGenerated(evaluator)
+
+ val millHandlers: PartialFunction[Any, pprint.Tree] = {
+ case c: Cross[_] =>
+ ReplApplyHandler.pprintCross(c, evaluator)
+ case m: mill.Module if evaluator.rootModule.millInternal.modules.contains(m) =>
+ ReplApplyHandler.pprintModule(m, evaluator)
+ case t: mill.define.Target[_] if evaluator.rootModule.millInternal.targets.contains(t) =>
+ ReplApplyHandler.pprintTask(t, evaluator)
+
+ }
+ val pprinter = pprinter0.copy(
+ additionalHandlers = millHandlers orElse pprinter0.additionalHandlers
+ )
+}
diff --git a/main/src/mill/main/Resolve.scala b/main/src/mill/main/Resolve.scala
new file mode 100644
index 00000000..c134d70d
--- /dev/null
+++ b/main/src/mill/main/Resolve.scala
@@ -0,0 +1,144 @@
+package mill.main
+
+import mill.define._
+import mill.define.TaskModule
+import ammonite.util.Res
+import mill.util.Router.EntryPoint
+import mill.util.Scripts
+
+object Resolve {
+ def resolve[T, V](remainingSelector: List[Segment],
+ obj: mill.Module,
+ discover: Discover[_],
+ rest: Seq[String],
+ remainingCrossSelectors: List[List[String]],
+ revSelectorsSoFar: List[Segment]): Either[String, Seq[NamedTask[Any]]] = {
+
+ remainingSelector match{
+ case Segment.Cross(_) :: Nil => Left("Selector cannot start with a [cross] segment")
+ case Segment.Label(last) :: Nil =>
+ val target =
+ obj
+ .millInternal
+ .reflect[Target[_]]
+ .find(_.label == last)
+ .map(Right(_))
+
+ def shimArgsig[T](a: mill.util.Router.ArgSig[T, _]) = {
+ ammonite.main.Router.ArgSig[T](
+ a.name,
+ a.typeString,
+ a.doc,
+ a.default
+ )
+ }
+ def invokeCommand(target: mill.Module, name: String) = for{
+ (cls, entryPoints) <- discover.value
+ if cls.isAssignableFrom(target.getClass)
+ ep <- entryPoints
+ if ep._2.name == name
+ } yield Scripts.runMainMethod(
+ target,
+ ep._2.asInstanceOf[EntryPoint[mill.Module]],
+ ammonite.main.Scripts.groupArgs(rest.toList)
+ ) match{
+ case Res.Success(v: Command[_]) => Right(v)
+ case Res.Failure(msg) => Left(msg)
+ case Res.Exception(ex, msg) =>
+ val sw = new java.io.StringWriter()
+ ex.printStackTrace(new java.io.PrintWriter(sw))
+ val prefix = if (msg.nonEmpty) msg + "\n" else msg
+ Left(prefix + sw.toString)
+
+ }
+
+ val runDefault = for{
+ child <- obj.millInternal.reflectNestedObjects[mill.Module]
+ if child.millOuterCtx.segment == Segment.Label(last)
+ res <- child match{
+ case taskMod: TaskModule => Some(invokeCommand(child, taskMod.defaultCommandName()).headOption)
+ case _ => None
+ }
+ } yield res
+
+ val command = invokeCommand(obj, last).headOption
+
+ command orElse target orElse runDefault.flatten.headOption match{
+ case None => Left("Cannot resolve task " +
+ Segments((Segment.Label(last) :: revSelectorsSoFar).reverse:_*).render
+ )
+ // Contents of `either` *must* be a `Task`, because we only select
+ // methods returning `Task` in the discovery process
+ case Some(either) => either.right.map(Seq(_))
+ }
+
+
+ case head :: tail =>
+ val newRevSelectorsSoFar = head :: revSelectorsSoFar
+ head match{
+ case Segment.Label(singleLabel) =>
+ if (singleLabel == "__") {
+
+ val matching =
+ obj.millInternal.modules
+ .map(resolve(tail, _, discover, rest, remainingCrossSelectors, newRevSelectorsSoFar))
+ .collect{case Right(vs) => vs}.flatten
+
+ if (matching.nonEmpty)Right(matching)
+ else Left("Cannot resolve module " + Segments(newRevSelectorsSoFar.reverse:_*).render)
+ } else if (singleLabel == "_") {
+
+ val matching =
+ obj.millModuleDirectChildren
+ .map(resolve(tail, _, discover, rest, remainingCrossSelectors, newRevSelectorsSoFar))
+ .collect{case Right(vs) => vs}.flatten
+
+ if (matching.nonEmpty)Right(matching)
+ else Left("Cannot resolve module " + Segments(newRevSelectorsSoFar.reverse:_*).render)
+ }else{
+
+ obj.millInternal.reflectNestedObjects[mill.Module].find{
+ _.millOuterCtx.segment == Segment.Label(singleLabel)
+ } match{
+ case Some(child: mill.Module) => resolve(tail, child, discover, rest, remainingCrossSelectors, newRevSelectorsSoFar)
+ case None => Left("Cannot resolve module " + Segments(newRevSelectorsSoFar.reverse:_*).render)
+ }
+ }
+
+ case Segment.Cross(cross) =>
+ obj match{
+ case c: Cross[_] =>
+ if(cross == Seq("__")){
+ val matching =
+ for ((k, v) <- c.items)
+ yield resolve(tail, v.asInstanceOf[mill.Module], discover, rest, remainingCrossSelectors, newRevSelectorsSoFar)
+
+ val results = matching.collect{case Right(res) => res}.flatten
+
+ if (results.isEmpty) Left("Cannot resolve cross " + Segments(newRevSelectorsSoFar.reverse:_*).render)
+ else Right(results)
+ } else if (cross.contains("_")){
+ val matching = for {
+ (k, v) <- c.items
+ if k.length == cross.length
+ if k.zip(cross).forall { case (l, r) => l == r || r == "_" }
+ } yield resolve(tail, v.asInstanceOf[mill.Module], discover, rest, remainingCrossSelectors, newRevSelectorsSoFar)
+
+ val results = matching.collect{case Right(res) => res}.flatten
+
+ if (results.isEmpty) Left("Cannot resolve cross " + Segments(newRevSelectorsSoFar.reverse:_*).render)
+ else Right(results)
+ }else{
+ c.itemMap.get(cross.toList) match{
+ case Some(m: mill.Module) => resolve(tail, m, discover, rest, remainingCrossSelectors, newRevSelectorsSoFar)
+ case None => Left("Cannot resolve cross " + Segments(newRevSelectorsSoFar.reverse:_*).render)
+ }
+ }
+ case _ => Left("Cannot resolve cross " + Segments(newRevSelectorsSoFar.reverse:_*).render)
+ }
+ }
+
+ case Nil => Left("Selector cannot be empty")
+ }
+ }
+}
diff --git a/main/src/mill/main/RunScript.scala b/main/src/mill/main/RunScript.scala
new file mode 100644
index 00000000..bd21a821
--- /dev/null
+++ b/main/src/mill/main/RunScript.scala
@@ -0,0 +1,240 @@
+package mill.main
+
+import java.nio.file.NoSuchFileException
+
+import ammonite.interp.Interpreter
+import ammonite.ops.{Path, read}
+import ammonite.runtime.SpecialClassLoader
+import ammonite.util.Util.CodeSource
+import ammonite.util.{Name, Res, Util}
+import mill.define
+import mill.define.{Discover, ExternalModule, Segment, Task}
+import mill.eval.{Evaluator, PathRef, Result}
+import mill.util.{EitherOps, Logger, ParseArgs, Watched}
+import mill.util.Strict.Agg
+import upickle.Js
+
+/**
+ * Custom version of ammonite.main.Scripts, letting us run the build.sc script
+ * directly without going through Ammonite's main-method/argument-parsing
+ * subsystem
+ */
+object RunScript{
+ def runScript(wd: Path,
+ path: Path,
+ instantiateInterpreter: => Either[(Res.Failing, Seq[(Path, Long)]), ammonite.interp.Interpreter],
+ scriptArgs: Seq[String],
+ lastEvaluator: Option[(Seq[(Path, Long)], Evaluator[Any])],
+ log: Logger)
+ : (Res[(Evaluator[Any], Seq[(Path, Long)], Either[String, Seq[Js.Value]])], Seq[(Path, Long)]) = {
+
+ val (evalRes, interpWatched) = lastEvaluator match{
+ case Some((prevInterpWatchedSig, prevEvaluator))
+ if watchedSigUnchanged(prevInterpWatchedSig) =>
+
+ (Res.Success(prevEvaluator), prevInterpWatchedSig)
+
+ case _ =>
+ instantiateInterpreter match{
+ case Left((res, watched)) => (res, watched)
+ case Right(interp) =>
+ interp.watch(path)
+ val eval =
+ for((mapping, discover) <- evaluateMapping(wd, path, interp))
+ yield new Evaluator[Any](
+ wd / 'out, wd / 'out, mapping, discover, log,
+ mapping.getClass.getClassLoader.asInstanceOf[SpecialClassLoader].classpathSignature
+ )
+
+ (eval, interp.watchedFiles)
+ }
+ }
+
+ val evaluated = for{
+ evaluator <- evalRes
+ (evalWatches, res) <- Res(evaluateTarget(evaluator, scriptArgs))
+ } yield {
+ val alreadyStale = evalWatches.exists(p => p.sig != new PathRef(p.path, p.quick).sig)
+ // If the file changed between the creation of the original
+ // `PathRef` and the current moment, use random junk .sig values
+ // to force an immediate re-run. Otherwise calculate the
+ // pathSignatures the same way Ammonite would and hand over the
+ // values, so Ammonite can watch them and only re-run if they
+ // subsequently change
+ val evaluationWatches =
+ if (alreadyStale) evalWatches.map(_.path -> util.Random.nextLong())
+ else evalWatches.map(p => p.path -> Interpreter.pathSignature(p.path))
+
+ (evaluator, evaluationWatches, res.map(_.flatMap(_._2)))
+ }
+ (evaluated, interpWatched)
+ }
+
+ def watchedSigUnchanged(sig: Seq[(Path, Long)]) = {
+ sig.forall{case (p, l) => Interpreter.pathSignature(p) == l}
+ }
+
+ def evaluateMapping(wd: Path,
+ path: Path,
+ interp: ammonite.interp.Interpreter): Res[(mill.define.BaseModule, Discover[Any])] = {
+
+ val (pkg, wrapper) = Util.pathToPackageWrapper(Seq(), path relativeTo wd)
+
+ for {
+ scriptTxt <-
+ try Res.Success(Util.normalizeNewlines(read(path)))
+ catch { case e: NoSuchFileException => Res.Failure("Script file not found: " + path) }
+
+ processed <- interp.processModule(
+ scriptTxt,
+ CodeSource(wrapper, pkg, Seq(Name("ammonite"), Name("$file")), Some(path)),
+ autoImport = true,
+ extraCode = "",
+ hardcoded = true
+ )
+
+ buildClsName <- processed.blockInfo.lastOption match {
+ case Some(meta) => Res.Success(meta.id.wrapperPath)
+ case None => Res.Skip
+ }
+
+ buildCls = interp
+ .evalClassloader
+ .loadClass(buildClsName)
+
+ module <- try {
+ Util.withContextClassloader(interp.evalClassloader) {
+ Res.Success(
+ buildCls.getMethod("millSelf")
+ .invoke(null)
+ .asInstanceOf[Some[mill.define.BaseModule]]
+ .get
+ )
+ }
+ } catch {
+ case e: Throwable => Res.Exception(e, "")
+ }
+ discover <- try {
+ Util.withContextClassloader(interp.evalClassloader) {
+ Res.Success(
+ buildCls.getMethod("millDiscover")
+ .invoke(module)
+ .asInstanceOf[Discover[Any]]
+ )
+ }
+ } catch {
+ case e: Throwable => Res.Exception(e, "")
+ }
+// _ <- Res(consistencyCheck(mapping))
+ } yield (module, discover)
+ }
+
+ def evaluateTarget[T](evaluator: Evaluator[T], scriptArgs: Seq[String]) = {
+ for {
+ parsed <- ParseArgs(scriptArgs)
+ (selectors, args) = parsed
+ targets <- {
+ val selected = selectors.map { case (scopedSel, sel) =>
+ val (rootModule, discover) = scopedSel match{
+ case None => (evaluator.rootModule, evaluator.discover)
+ case Some(scoping) =>
+ val moduleCls =
+ evaluator.rootModule.getClass.getClassLoader.loadClass(scoping.render + "$")
+
+ val rootModule = moduleCls.getField("MODULE$").get(moduleCls).asInstanceOf[ExternalModule]
+ (rootModule, rootModule.millDiscover)
+ }
+ val crossSelectors = sel.value.map {
+ case Segment.Cross(x) => x.toList.map(_.toString)
+ case _ => Nil
+ }
+
+ try {
+ // We inject the `evaluator.rootModule` into the TargetScopt, rather
+ // than the `rootModule`, because even if you are running an external
+ // module we still want you to be able to resolve targets from your
+ // main build. Resolving targets from external builds as CLI arguments
+ // is not currently supported
+ mill.eval.Evaluator.currentEvaluator.set(evaluator)
+ mill.main.Resolve.resolve(
+ sel.value.toList, rootModule,
+ discover,
+ args, crossSelectors.toList, Nil
+ )
+ } finally{
+ mill.eval.Evaluator.currentEvaluator.set(null)
+ }
+ }
+ EitherOps.sequence(selected)
+ }
+ } yield {
+ val (watched, res) = evaluate(
+ evaluator,
+ Agg.from(targets.flatten.distinct)
+ )
+
+ val watched2 = for{
+ x <- res.right.toSeq
+ (Watched(_, extraWatched), _) <- x
+ w <- extraWatched
+ } yield w
+
+ (watched ++ watched2, res)
+ }
+ }
+
+ def evaluate(evaluator: Evaluator[_],
+ targets: Agg[Task[Any]]): (Seq[PathRef], Either[String, Seq[(Any, Option[upickle.Js.Value])]]) = {
+ val evaluated = evaluator.evaluate(targets)
+ val watched = evaluated.results
+ .iterator
+ .collect {
+ case (t: define.Sources, Result.Success(p: Seq[PathRef])) => p
+ }
+ .flatten
+ .toSeq
+
+ val errorStr =
+ (for((k, fs) <- evaluated.failing.items()) yield {
+ val ks = k match{
+ case Left(t) => t.toString
+ case Right(t) => t.segments.render
+ }
+ val fss = fs.map{
+ case Result.Exception(t, outerStack) =>
+ t.toString +
+ t.getStackTrace.dropRight(outerStack.value.length).map("\n " + _).mkString
+ case Result.Failure(t, _) => t
+ }
+ s"$ks ${fss.mkString(", ")}"
+ }).mkString("\n")
+
+ evaluated.failing.keyCount match {
+ case 0 =>
+ val json = for(t <- targets.toSeq) yield {
+ t match {
+ case t: mill.define.NamedTask[_] =>
+ val jsonFile = Evaluator
+ .resolveDestPaths(evaluator.outPath, t.ctx.segments)
+ .meta
+ val metadata = upickle.json.read(jsonFile.toIO)
+ Some(metadata(1))
+
+ case _ => None
+ }
+ }
+
+ watched -> Right(evaluated.values.zip(json))
+ case n => watched -> Left(s"$n targets failed\n$errorStr")
+ }
+ }
+
+// def consistencyCheck[T](mapping: Discovered.Mapping[T]): Either[String, Unit] = {
+// val consistencyErrors = Discovered.consistencyCheck(mapping)
+// if (consistencyErrors.nonEmpty) {
+// Left(s"Failed Discovered.consistencyCheck: ${consistencyErrors.map(_.render)}")
+// } else {
+// Right(())
+// }
+// }
+}
diff --git a/main/src/mill/modules/Jvm.scala b/main/src/mill/modules/Jvm.scala
new file mode 100644
index 00000000..297dcf1f
--- /dev/null
+++ b/main/src/mill/modules/Jvm.scala
@@ -0,0 +1,257 @@
+package mill.modules
+
+import java.io.FileOutputStream
+import java.lang.reflect.Modifier
+import java.net.URLClassLoader
+import java.nio.file.attribute.PosixFilePermission
+import java.util.jar.{JarEntry, JarFile, JarOutputStream}
+
+import ammonite.ops._
+import mill.define.Task
+import mill.eval.PathRef
+import mill.util.{Ctx, Loose}
+import mill.util.Ctx.Log
+import mill.util.Loose.Agg
+import upickle.default.{Reader, Writer}
+
+import scala.annotation.tailrec
+import scala.collection.mutable
+import scala.reflect.ClassTag
+
+
+object Jvm {
+
+ def interactiveSubprocess(mainClass: String,
+ classPath: Agg[Path],
+ jvmArgs: Seq[String] = Seq.empty,
+ envArgs: Map[String, String] = Map.empty,
+ mainArgs: Seq[String] = Seq.empty,
+ workingDir: Path = null): Unit = {
+ import ammonite.ops.ImplicitWd._
+ val commandArgs =
+ Vector("java") ++
+ jvmArgs ++
+ Vector("-cp", classPath.mkString(":"), mainClass) ++
+ mainArgs
+
+ %.copy(envArgs = envArgs)(commandArgs)(workingDir)
+ }
+
+ def runLocal(mainClass: String,
+ classPath: Agg[Path],
+ mainArgs: Seq[String] = Seq.empty)
+ (implicit ctx: Ctx): Unit = {
+ inprocess(classPath, classLoaderOverrideSbtTesting = false, cl => {
+ getMainMethod(mainClass, cl).invoke(null, mainArgs.toArray)
+ })
+ }
+
+ private def getMainMethod(mainClassName: String, cl: ClassLoader) = {
+ val mainClass = cl.loadClass(mainClassName)
+ val method = mainClass.getMethod("main", classOf[Array[String]])
+ // jvm allows the actual main class to be non-public and to run a method in the non-public class,
+ // we need to make it accessible
+ method.setAccessible(true)
+ val modifiers = method.getModifiers
+ if (!Modifier.isPublic(modifiers))
+ throw new NoSuchMethodException(mainClassName + ".main is not public")
+ if (!Modifier.isStatic(modifiers))
+ throw new NoSuchMethodException(mainClassName + ".main is not static")
+ method
+ }
+
+
+
+ def inprocess[T](classPath: Agg[Path],
+ classLoaderOverrideSbtTesting: Boolean,
+ body: ClassLoader => T): T = {
+ val cl = if (classLoaderOverrideSbtTesting) {
+ val outerClassLoader = getClass.getClassLoader
+ new URLClassLoader(classPath.map(_.toIO.toURI.toURL).toArray, null){
+ override def findClass(name: String) = {
+ if (name.startsWith("sbt.testing.")){
+ outerClassLoader.loadClass(name)
+ }else{
+ super.findClass(name)
+ }
+ }
+ }
+ } else {
+ new URLClassLoader(classPath.map(_.toIO.toURI.toURL).toArray, null)
+ }
+ val oldCl = Thread.currentThread().getContextClassLoader
+ Thread.currentThread().setContextClassLoader(cl)
+ try {
+ body(cl)
+ }finally{
+ Thread.currentThread().setContextClassLoader(oldCl)
+ cl.close()
+ }
+ }
+
+ def subprocess(mainClass: String,
+ classPath: Agg[Path],
+ jvmArgs: Seq[String] = Seq.empty,
+ envArgs: Map[String, String] = Map.empty,
+ mainArgs: Seq[String] = Seq.empty,
+ workingDir: Path = null)
+ (implicit ctx: Ctx) = {
+
+ val commandArgs =
+ Vector("java") ++
+ jvmArgs ++
+ Vector("-cp", classPath.mkString(":"), mainClass) ++
+ mainArgs
+
+ val workingDir1 = Option(workingDir).getOrElse(ctx.dest)
+ mkdir(workingDir1)
+ val builder =
+ new java.lang.ProcessBuilder()
+ .directory(workingDir1.toIO)
+ .command(commandArgs:_*)
+ .redirectOutput(ProcessBuilder.Redirect.PIPE)
+ .redirectError(ProcessBuilder.Redirect.PIPE)
+
+ for((k, v) <- envArgs) builder.environment().put(k, v)
+ val proc = builder.start()
+ val stdout = proc.getInputStream
+ val stderr = proc.getErrorStream
+ val sources = Seq(
+ (stdout, Left(_: Bytes), ctx.log.outputStream),
+ (stderr, Right(_: Bytes),ctx.log.errorStream )
+ )
+ val chunks = mutable.Buffer.empty[Either[Bytes, Bytes]]
+ while(
+ // Process.isAlive doesn't exist on JDK 7 =/
+ util.Try(proc.exitValue).isFailure ||
+ stdout.available() > 0 ||
+ stderr.available() > 0
+ ){
+ var readSomething = false
+ for ((subStream, wrapper, parentStream) <- sources){
+ while (subStream.available() > 0){
+ readSomething = true
+ val array = new Array[Byte](subStream.available())
+ val actuallyRead = subStream.read(array)
+ chunks.append(wrapper(new ammonite.ops.Bytes(array)))
+ parentStream.write(array, 0, actuallyRead)
+ }
+ }
+ // if we did not read anything sleep briefly to avoid spinning
+ if(!readSomething)
+ Thread.sleep(2)
+ }
+
+ if (proc.exitValue() != 0) throw new InteractiveShelloutException()
+ else ammonite.ops.CommandResult(proc.exitValue(), chunks)
+ }
+
+ private def createManifest(mainClass: Option[String]) = {
+ val m = new java.util.jar.Manifest()
+ m.getMainAttributes.put(java.util.jar.Attributes.Name.MANIFEST_VERSION, "1.0")
+ m.getMainAttributes.putValue( "Created-By", "Scala mill" )
+ mainClass.foreach(
+ m.getMainAttributes.put(java.util.jar.Attributes.Name.MAIN_CLASS, _)
+ )
+ m
+ }
+
+ def createJar(inputPaths: Agg[Path], mainClass: Option[String] = None)
+ (implicit ctx: Ctx.Dest): PathRef = {
+ val outputPath = ctx.dest / "out.jar"
+ rm(outputPath)
+
+ val seen = mutable.Set.empty[RelPath]
+ seen.add("META-INF" / "MANIFEST.MF")
+ val jar = new JarOutputStream(
+ new FileOutputStream(outputPath.toIO),
+ createManifest(mainClass)
+ )
+
+ try{
+ assert(inputPaths.forall(exists(_)))
+ for{
+ p <- inputPaths
+ (file, mapping) <-
+ if (p.isFile) Iterator(p -> empty/p.last)
+ else ls.rec(p).filter(_.isFile).map(sub => sub -> sub.relativeTo(p))
+ if !seen(mapping)
+ } {
+ seen.add(mapping)
+ val entry = new JarEntry(mapping.toString)
+ entry.setTime(file.mtime.toMillis)
+ jar.putNextEntry(entry)
+ jar.write(read.bytes(file))
+ jar.closeEntry()
+ }
+ } finally {
+ jar.close()
+ }
+
+ PathRef(outputPath)
+ }
+
+ def createAssembly(inputPaths: Agg[Path],
+ mainClass: Option[String] = None,
+ prependShellScript: String = "")
+ (implicit ctx: Ctx.Dest): PathRef = {
+ val outputPath = ctx.dest / "out.jar"
+ rm(outputPath)
+
+ if(inputPaths.nonEmpty) {
+
+ val output = new FileOutputStream(outputPath.toIO)
+
+ // Prepend shell script and make it executable
+ if (prependShellScript.nonEmpty) {
+ output.write((prependShellScript + "\n").getBytes)
+ val perms = java.nio.file.Files.getPosixFilePermissions(outputPath.toNIO)
+ perms.add(PosixFilePermission.GROUP_EXECUTE)
+ perms.add(PosixFilePermission.OWNER_EXECUTE)
+ perms.add(PosixFilePermission.OTHERS_EXECUTE)
+ java.nio.file.Files.setPosixFilePermissions(outputPath.toNIO, perms)
+ }
+
+ val jar = new JarOutputStream(
+ output,
+ createManifest(mainClass)
+ )
+
+ val seen = mutable.Set("META-INF/MANIFEST.MF")
+ try{
+
+
+ for{
+ p <- inputPaths
+ if exists(p)
+ (file, mapping) <-
+ if (p.isFile) {
+ val jf = new JarFile(p.toIO)
+ import collection.JavaConverters._
+ for(entry <- jf.entries().asScala if !entry.isDirectory) yield {
+ read.bytes(jf.getInputStream(entry)) -> entry.getName
+ }
+ }
+ else {
+ ls.rec(p).iterator
+ .filter(_.isFile)
+ .map(sub => read.bytes(sub) -> sub.relativeTo(p).toString)
+ }
+ if !seen(mapping)
+ } {
+ seen.add(mapping)
+ val entry = new JarEntry(mapping.toString)
+ jar.putNextEntry(entry)
+ jar.write(file)
+ jar.closeEntry()
+ }
+ } finally {
+ jar.close()
+ output.close()
+ }
+
+ }
+ PathRef(outputPath)
+ }
+
+}
diff --git a/main/src/mill/modules/Util.scala b/main/src/mill/modules/Util.scala
new file mode 100644
index 00000000..cef11859
--- /dev/null
+++ b/main/src/mill/modules/Util.scala
@@ -0,0 +1,65 @@
+package mill.modules
+
+
+import ammonite.ops.{Path, RelPath, empty, mkdir, read}
+import mill.eval.PathRef
+import mill.util.Ctx
+
+object Util {
+ def download(url: String, dest: RelPath = "download")(implicit ctx: Ctx.Dest) = {
+ val out = ctx.dest / dest
+
+ val website = new java.net.URI(url).toURL
+ val rbc = java.nio.channels.Channels.newChannel(website.openStream)
+ try{
+ val fos = new java.io.FileOutputStream(out.toIO)
+ try{
+ fos.getChannel.transferFrom(rbc, 0, java.lang.Long.MAX_VALUE)
+ PathRef(out)
+ } finally{
+ fos.close()
+ }
+ } finally{
+ rbc.close()
+ }
+ }
+
+ def downloadUnpackZip(url: String, dest: RelPath = "unpacked")
+ (implicit ctx: Ctx.Dest) = {
+
+ val tmpName = if (dest == empty / "tmp.zip") "tmp2.zip" else "tmp.zip"
+ val downloaded = download(url, tmpName)
+ unpackZip(downloaded.path, dest)
+ }
+
+ def unpackZip(src: Path, dest: RelPath = "unpacked")
+ (implicit ctx: Ctx.Dest) = {
+
+ val byteStream = read.getInputStream(src)
+ val zipStream = new java.util.zip.ZipInputStream(byteStream)
+ while({
+ zipStream.getNextEntry match{
+ case null => false
+ case entry =>
+ if (!entry.isDirectory) {
+ val entryDest = ctx.dest / dest / RelPath(entry.getName)
+ mkdir(entryDest / ammonite.ops.up)
+ val fileOut = new java.io.FileOutputStream(entryDest.toString)
+ val buffer = new Array[Byte](4096)
+ while ( {
+ zipStream.read(buffer) match {
+ case -1 => false
+ case n =>
+ fileOut.write(buffer, 0, n)
+ true
+ }
+ }) ()
+ fileOut.close()
+ }
+ zipStream.closeEntry()
+ true
+ }
+ })()
+ PathRef(ctx.dest / dest)
+ }
+}
diff --git a/main/src/mill/package.scala b/main/src/mill/package.scala
new file mode 100644
index 00000000..93916c8b
--- /dev/null
+++ b/main/src/mill/package.scala
@@ -0,0 +1,12 @@
+import mill.util.JsonFormatters
+
+package object mill extends JsonFormatters{
+ val T = define.Target
+ type T[T] = define.Target[T]
+ val PathRef = mill.eval.PathRef
+ type PathRef = mill.eval.PathRef
+ type Module = define.Module
+ type Cross[T] = define.Cross[T]
+ type Agg[T] = util.Loose.Agg[T]
+ val Agg = util.Loose.Agg
+}
diff --git a/main/test/resources/examples/javac/build.sc b/main/test/resources/examples/javac/build.sc
new file mode 100644
index 00000000..0783ac17
--- /dev/null
+++ b/main/test/resources/examples/javac/build.sc
@@ -0,0 +1,23 @@
+import ammonite.ops._
+import mill.T
+import mill.eval.JavaCompileJarTests.compileAll
+import mill.eval.PathRef
+import mill.modules.Jvm
+import mill.util.Loose
+
+def sourceRootPath = millSourcePath / 'src
+def resourceRootPath = millSourcePath / 'resources
+
+// sourceRoot -> allSources -> classFiles
+// |
+// v
+// resourceRoot ----> jar
+def sourceRoot = T.sources{ sourceRootPath }
+def resourceRoot = T.sources{ resourceRootPath }
+def allSources = T{ sourceRoot().flatMap(p => ls.rec(p.path)).map(PathRef(_)) }
+def classFiles = T{ compileAll(allSources()) }
+def jar = T{ Jvm.createJar(Loose.Agg(classFiles().path) ++ resourceRoot().map(_.path)) }
+
+def run(mainClsName: String) = T.command{
+ %%('java, "-cp", classFiles().path, mainClsName)(T.ctx().dest)
+}
diff --git a/main/test/resources/examples/javac/resources/hello.txt b/main/test/resources/examples/javac/resources/hello.txt
new file mode 100644
index 00000000..5e1c309d
--- /dev/null
+++ b/main/test/resources/examples/javac/resources/hello.txt
@@ -0,0 +1 @@
+Hello World \ No newline at end of file
diff --git a/main/test/resources/examples/javac/src/Bar.java b/main/test/resources/examples/javac/src/Bar.java
new file mode 100644
index 00000000..4e30c89b
--- /dev/null
+++ b/main/test/resources/examples/javac/src/Bar.java
@@ -0,0 +1,4 @@
+package test;
+public class Bar{
+ static int value = 271828;
+} \ No newline at end of file
diff --git a/main/test/resources/examples/javac/src/Foo.java b/main/test/resources/examples/javac/src/Foo.java
new file mode 100644
index 00000000..e694f9fa
--- /dev/null
+++ b/main/test/resources/examples/javac/src/Foo.java
@@ -0,0 +1,7 @@
+package test;
+public class Foo{
+ static int value = 31337;
+ public static void main(String[] args){
+ System.out.println(value + Bar.value);
+ }
+} \ No newline at end of file
diff --git a/main/test/src/mill/TestMain.scala b/main/test/src/mill/TestMain.scala
new file mode 100644
index 00000000..80e7e627
--- /dev/null
+++ b/main/test/src/mill/TestMain.scala
@@ -0,0 +1,6 @@
+package mill
+
+object TestMain {
+ def main(args: Array[String]): Unit = {
+ }
+}
diff --git a/main/test/src/mill/UTestFramework.scala b/main/test/src/mill/UTestFramework.scala
new file mode 100644
index 00000000..6c0d5191
--- /dev/null
+++ b/main/test/src/mill/UTestFramework.scala
@@ -0,0 +1,11 @@
+package mill
+
+class UTestFramework extends utest.runner.Framework {
+ override def exceptionStackFrameHighlighter(s: StackTraceElement) = {
+ s.getClassName.startsWith("mill.")
+ }
+ override def setup() = {
+ import ammonite.ops._
+ rm(pwd / 'target / 'workspace)
+ }
+}
diff --git a/main/test/src/mill/define/ApplicativeTests.scala b/main/test/src/mill/define/ApplicativeTests.scala
new file mode 100644
index 00000000..72b715bb
--- /dev/null
+++ b/main/test/src/mill/define/ApplicativeTests.scala
@@ -0,0 +1,125 @@
+package mill.define
+
+import mill.define.Applicative.ImplicitStub
+import utest._
+
+import scala.annotation.compileTimeOnly
+import scala.language.experimental.macros
+
+
+object ApplicativeTests extends TestSuite {
+ implicit def optionToOpt[T](o: Option[T]): Opt[T] = new Opt(o)
+ class Opt[T](val self: Option[T]) extends Applicative.Applyable[Option, T]
+ object Opt extends OptGenerated with Applicative.Applyer[Opt, Option, Applicative.Id, String]{
+
+ val injectedCtx = "helloooo"
+ def underlying[A](v: Opt[A]) = v.self
+ def apply[T](t: T): Option[T] = macro Applicative.impl[Option, T, String]
+
+ def mapCtx[A, B](a: Option[A])(f: (A, String) => B): Option[B] = a.map(f(_, injectedCtx))
+ def zip() = Some(())
+ def zip[A](a: Option[A]) = a.map(Tuple1(_))
+ }
+ class Counter{
+ var value = 0
+ def apply() = {
+ value += 1
+ value
+ }
+ }
+ @compileTimeOnly("Target.ctx() can only be used with a T{...} block")
+ @ImplicitStub
+ implicit def taskCtx: String = ???
+
+ val tests = Tests{
+
+ 'selfContained - {
+
+ 'simple - assert(Opt("lol " + 1) == Some("lol 1"))
+ 'singleSome - assert(Opt("lol " + Some("hello")()) == Some("lol hello"))
+ 'twoSomes - assert(Opt(Some("lol ")() + Some("hello")()) == Some("lol hello"))
+ 'singleNone - assert(Opt("lol " + None()) == None)
+ 'twoNones - assert(Opt("lol " + None() + None()) == None)
+ }
+ 'context - {
+ assert(Opt(Opt.ctx() + Some("World")()) == Some("hellooooWorld"))
+ }
+ 'capturing - {
+ val lol = "lol "
+ def hell(o: String) = "hell" + o
+ 'simple - assert(Opt(lol + 1) == Some("lol 1"))
+ 'singleSome - assert(Opt(lol + Some(hell("o"))()) == Some("lol hello"))
+ 'twoSomes - assert(Opt(Some(lol)() + Some(hell("o"))()) == Some("lol hello"))
+ 'singleNone - assert(Opt(lol + None()) == None)
+ 'twoNones - assert(Opt(lol + None() + None()) == None)
+ }
+ 'allowedLocalDef - {
+ // Although x is defined inside the Opt{...} block, it is also defined
+ // within the LHS of the Applyable#apply call, so it is safe to life it
+ // out into the `zipMap` arguments list.
+ val res = Opt{ "lol " + Some("hello").flatMap(x => Some(x)).apply() }
+ assert(res == Some("lol hello"))
+ }
+ 'upstreamAlwaysEvaluated - {
+ // Whether or not control-flow reaches the Applyable#apply call inside an
+ // Opt{...} block, we always evaluate the LHS of the Applyable#apply
+ // because it gets lifted out of any control flow statements
+ val counter = new Counter()
+ def up = Opt{ "lol " + counter() }
+ val down = Opt{ if ("lol".length > 10) up() else "fail" }
+ assert(
+ down == Some("fail"),
+ counter.value == 1
+ )
+ }
+ 'upstreamEvaluatedOnlyOnce - {
+ // Even if control-flow reaches the Applyable#apply call more than once,
+ // it only gets evaluated once due to its lifting out of the Opt{...} block
+ val counter = new Counter()
+ def up = Opt{ "lol " + counter() }
+ def runTwice[T](t: => T) = (t, t)
+ val down = Opt{ runTwice(up()) }
+ assert(
+ down == Some(("lol 1", "lol 1")),
+ counter.value == 1
+ )
+ }
+ 'evaluationsInsideLambdasWork - {
+ // This required some fiddling with owner chains inside the macro to get
+ // working, so ensure it doesn't regress
+ val counter = new Counter()
+ def up = Opt{ "hello" + counter() }
+ val down1 = Opt{ (() => up())() }
+ val down2 = Opt{ Seq(1, 2, 3).map(n => up() * n) }
+ assert(
+ down1 == Some("hello1"),
+ down2 == Some(Seq("hello2", "hello2hello2", "hello2hello2hello2"))
+ )
+ }
+ 'appliesEvaluatedOncePerLexicalCallsite - {
+ // If you have multiple Applyable#apply() lexically in the source code of
+ // your Opt{...} call, each one gets evaluated once, even if the LHS of each
+ // apply() call is identical. It's up to the downstream zipMap()
+ // implementation to decide if it wants to dedup them or do other things.
+ val counter = new Counter()
+ def up = Opt{ "hello" + counter() }
+ val down = Opt{ Seq(1, 2, 3).map(n => n + up() + up()) }
+ assert(down == Some(Seq("1hello1hello2", "2hello1hello2", "3hello1hello2")))
+ }
+ 'appliesEvaluateBeforehand - {
+ // Every Applyable#apply() within a Opt{...} block evaluates before any
+ // other logic within that block, even if they would happen first in the
+ // normal Scala evaluation order
+ val counter = new Counter()
+ def up = Opt{ counter() }
+ val down = Opt{
+ val res = counter()
+ val one = up()
+ val two = up()
+ val three = up()
+ (res, one, two, three)
+ }
+ assert(down == Some((4, 1, 2, 3)))
+ }
+ }
+}
diff --git a/main/test/src/mill/define/BasePathTests.scala b/main/test/src/mill/define/BasePathTests.scala
new file mode 100644
index 00000000..1f5b4037
--- /dev/null
+++ b/main/test/src/mill/define/BasePathTests.scala
@@ -0,0 +1,68 @@
+package mill.define
+
+import mill.util.{TestGraphs, TestUtil}
+import utest._
+import ammonite.ops._
+import mill.{Module, T}
+object BasePathTests extends TestSuite{
+ val testGraphs = new TestGraphs
+ val tests = Tests{
+ def check[T <: Module](m: T)(f: T => Module, segments: String*) = {
+ val remaining = f(m).millSourcePath.relativeTo(m.millSourcePath).segments
+ assert(remaining == segments)
+ }
+ 'singleton - {
+ check(testGraphs.singleton)(identity)
+ }
+ 'separateGroups - {
+ check(TestGraphs.triangleTask)(identity)
+ }
+ 'TraitWithModuleObject - {
+ check(TestGraphs.TraitWithModuleObject)(
+ _.TraitModule,
+ "TraitModule"
+ )
+ }
+ 'nestedModuleNested - {
+ check(TestGraphs.nestedModule)(_.nested, "nested")
+ }
+ 'nestedModuleInstance - {
+ check(TestGraphs.nestedModule)(_.classInstance, "classInstance")
+ }
+ 'singleCross - {
+ check(TestGraphs.singleCross)(_.cross, "cross")
+ check(TestGraphs.singleCross)(_.cross("210"), "cross", "210")
+ check(TestGraphs.singleCross)(_.cross("211"), "cross", "211")
+ }
+ 'doubleCross - {
+ check(TestGraphs.doubleCross)(_.cross, "cross")
+ check(TestGraphs.doubleCross)(_.cross("210", "jvm"), "cross", "210", "jvm")
+ check(TestGraphs.doubleCross)(_.cross("212", "js"), "cross", "212", "js")
+ }
+ 'nestedCrosses - {
+ check(TestGraphs.nestedCrosses)(_.cross, "cross")
+ check(TestGraphs.nestedCrosses)(
+ _.cross("210").cross2("js"),
+ "cross", "210", "cross2", "js"
+ )
+ }
+ 'overriden - {
+ object overridenBasePath extends TestUtil.BaseModule {
+ override def millSourcePath = pwd / 'overridenBasePathRootValue
+ object nested extends Module{
+ override def millSourcePath = super.millSourcePath / 'overridenBasePathNested
+ object nested extends Module{
+ override def millSourcePath = super.millSourcePath / 'overridenBasePathDoubleNested
+ }
+ }
+ }
+ assert(
+ overridenBasePath.millSourcePath == pwd / 'overridenBasePathRootValue,
+ overridenBasePath.nested.millSourcePath == pwd / 'overridenBasePathRootValue / 'nested / 'overridenBasePathNested,
+ overridenBasePath.nested.nested.millSourcePath == pwd / 'overridenBasePathRootValue / 'nested / 'overridenBasePathNested / 'nested / 'overridenBasePathDoubleNested
+ )
+ }
+
+ }
+}
+
diff --git a/main/test/src/mill/define/CacherTests.scala b/main/test/src/mill/define/CacherTests.scala
new file mode 100644
index 00000000..606de846
--- /dev/null
+++ b/main/test/src/mill/define/CacherTests.scala
@@ -0,0 +1,76 @@
+package mill.define
+
+import ammonite.ops.pwd
+import mill.util.{DummyLogger, TestEvaluator, TestUtil}
+import mill.util.Strict.Agg
+import mill.T
+import mill.eval.Result.Success
+import utest._
+import utest.framework.TestPath
+import mill.util.TestEvaluator.implicitDisover
+
+object CacherTests extends TestSuite{
+ object Base extends Base
+ trait Base extends TestUtil.BaseModule{
+ def value = T{ 1 }
+ def result = T{ Success(1) }
+ }
+ object Middle extends Middle
+ trait Middle extends Base{
+ def value = T{ super.value() + 2}
+ def overriden = T{ super.value()}
+ }
+ object Terminal extends Terminal
+ trait Terminal extends Middle{
+ override def value = T{ super.value() + 4}
+ }
+
+ val tests = Tests{
+ def eval[T <: TestUtil.BaseModule, V](mapping: T, v: Task[V])
+ (implicit discover: Discover[T], tp: TestPath) = {
+ val evaluator = new TestEvaluator(mapping)
+ evaluator(v).right.get._1
+ }
+ def check(x: Any, y: Any) = assert(x == y)
+
+ 'simpleDefIsCached - {
+ Predef.assert(Base.value eq Base.value)
+ Predef.assert(eval(Base, Base.value) == 1)
+ }
+
+ 'resultDefIsCached - {
+ Predef.assert(Base.result eq Base.result)
+ Predef.assert(eval(Base, Base.result) == 1)
+ }
+
+
+ 'overridingDefIsAlsoCached - {
+ Predef.assert(eval(Middle, Middle.value) == 3)
+ Predef.assert(Middle.value eq Middle.value)
+ }
+
+ 'overridenDefRemainsAvailable - {
+ Predef.assert(eval(Middle, Middle.overriden) == 1)
+ }
+
+
+ 'multipleOverridesWork- {
+ Predef.assert(eval(Terminal, Terminal.value) == 7)
+ Predef.assert(eval(Terminal, Terminal.overriden) == 1)
+ }
+ // Doesn't fail, presumably compileError doesn't go far enough in the
+ // compilation pipeline to hit the override checks
+ //
+ // 'overrideOutsideModuleFails - {
+ // compileError("""
+ // trait Foo{
+ // def x = 1
+ // }
+ // object Bar extends Foo{
+ // def x = 2
+ // }
+ // """)
+ // }
+ }
+}
+
diff --git a/main/test/src/mill/define/DiscoverTests.scala b/main/test/src/mill/define/DiscoverTests.scala
new file mode 100644
index 00000000..7621169a
--- /dev/null
+++ b/main/test/src/mill/define/DiscoverTests.scala
@@ -0,0 +1,60 @@
+package mill.define
+
+import mill.util.TestGraphs
+import utest._
+
+object DiscoverTests extends TestSuite{
+ val testGraphs = new TestGraphs
+ val tests = Tests{
+ def check[T <: Module](m: T)(targets: (T => Target[_])*) = {
+ val discovered = m.millInternal.targets
+ val expected = targets.map(_(m)).toSet
+ assert(discovered == expected)
+ }
+ 'singleton - {
+ check(testGraphs.singleton)(_.single)
+ }
+ 'separateGroups - {
+ check(TestGraphs.triangleTask)(_.left, _.right)
+ }
+ 'TraitWithModuleObject - {
+ check(TestGraphs.TraitWithModuleObject)(_.TraitModule.testFramework)
+ }
+ 'nestedModule - {
+ check(TestGraphs.nestedModule)(_.single, _.nested.single, _.classInstance.single)
+ }
+ 'singleCross - {
+ check(TestGraphs.singleCross)(
+ _.cross("210").suffix,
+ _.cross("211").suffix,
+ _.cross("212").suffix
+ )
+ }
+ 'doubleCross - {
+ check(TestGraphs.doubleCross)(
+ _.cross("210", "jvm").suffix,
+ _.cross("210", "js").suffix,
+ _.cross("211", "jvm").suffix,
+ _.cross("211", "js").suffix,
+ _.cross("212", "jvm").suffix,
+ _.cross("212", "js").suffix,
+ _.cross("212", "native").suffix
+ )
+ }
+ 'nestedCrosses - {
+ check(TestGraphs.nestedCrosses)(
+ _.cross("210").cross2("jvm").suffix,
+ _.cross("210").cross2("js").suffix,
+ _.cross("210").cross2("native").suffix,
+ _.cross("211").cross2("jvm").suffix,
+ _.cross("211").cross2("js").suffix,
+ _.cross("211").cross2("native").suffix,
+ _.cross("212").cross2("jvm").suffix,
+ _.cross("212").cross2("js").suffix,
+ _.cross("212").cross2("native").suffix
+ )
+ }
+
+ }
+}
+
diff --git a/main/test/src/mill/define/GraphTests.scala b/main/test/src/mill/define/GraphTests.scala
new file mode 100644
index 00000000..7e6680be
--- /dev/null
+++ b/main/test/src/mill/define/GraphTests.scala
@@ -0,0 +1,199 @@
+package mill.define
+
+
+import mill.eval.Evaluator
+import mill.util.{TestGraphs, TestUtil}
+import utest._
+import mill.util.Strict.Agg
+object GraphTests extends TestSuite{
+
+ val tests = Tests{
+
+
+ val graphs = new TestGraphs()
+ import graphs._
+ import TestGraphs._
+
+ 'topoSortedTransitiveTargets - {
+ def check(targets: Agg[Task[_]], expected: Agg[Task[_]]) = {
+ val result = Graph.topoSorted(Graph.transitiveTargets(targets)).values
+ TestUtil.checkTopological(result)
+ assert(result == expected)
+ }
+
+ 'singleton - check(
+ targets = Agg(singleton.single),
+ expected = Agg(singleton.single)
+ )
+ 'pair - check(
+ targets = Agg(pair.down),
+ expected = Agg(pair.up, pair.down)
+ )
+ 'anonTriple - check(
+ targets = Agg(anonTriple.down),
+ expected = Agg(anonTriple.up, anonTriple.down.inputs(0), anonTriple.down)
+ )
+ 'diamond - check(
+ targets = Agg(diamond.down),
+ expected = Agg(diamond.up, diamond.left, diamond.right, diamond.down)
+ )
+ 'anonDiamond - check(
+ targets = Agg(diamond.down),
+ expected = Agg(
+ diamond.up,
+ diamond.down.inputs(0),
+ diamond.down.inputs(1),
+ diamond.down
+ )
+ )
+ 'defCachedDiamond - check(
+ targets = Agg(defCachedDiamond.down),
+ expected = Agg(
+ defCachedDiamond.up.inputs(0),
+ defCachedDiamond.up,
+ defCachedDiamond.down.inputs(0).inputs(0).inputs(0),
+ defCachedDiamond.down.inputs(0).inputs(0),
+ defCachedDiamond.down.inputs(0).inputs(1).inputs(0),
+ defCachedDiamond.down.inputs(0).inputs(1),
+ defCachedDiamond.down.inputs(0),
+ defCachedDiamond.down
+ )
+ )
+ 'bigSingleTerminal - {
+ val result = Graph.topoSorted(Graph.transitiveTargets(Agg(bigSingleTerminal.j))).values
+ TestUtil.checkTopological(result)
+ assert(result.size == 28)
+ }
+ }
+
+ 'groupAroundNamedTargets - {
+ def check[T, R <: Target[Int]](base: T)
+ (target: T => R,
+ important0: Agg[T => Target[_]],
+ expected: Agg[(R, Int)]) = {
+
+ val topoSorted = Graph.topoSorted(Graph.transitiveTargets(Agg(target(base))))
+
+ val important = important0.map(_ (base))
+ val grouped = Graph.groupAroundImportantTargets(topoSorted) {
+ case t: Target[_] if important.contains(t) => t
+ }
+ val flattened = Agg.from(grouped.values().flatMap(_.items))
+
+ TestUtil.checkTopological(flattened)
+ for ((terminal, expectedSize) <- expected) {
+ val grouping = grouped.lookupKey(terminal)
+ assert(
+ grouping.size == expectedSize,
+ grouping.flatMap(_.asTarget: Option[Target[_]]).filter(important.contains) == Agg(terminal)
+ )
+ }
+ }
+
+ 'singleton - check(singleton)(
+ _.single,
+ Agg(_.single),
+ Agg(singleton.single -> 1)
+ )
+ 'pair - check(pair)(
+ _.down,
+ Agg(_.up, _.down),
+ Agg(pair.up -> 1, pair.down -> 1)
+ )
+ 'anonTriple - check(anonTriple)(
+ _.down,
+ Agg(_.up, _.down),
+ Agg(anonTriple.up -> 1, anonTriple.down -> 2)
+ )
+ 'diamond - check(diamond)(
+ _.down,
+ Agg(_.up, _.left, _.right, _.down),
+ Agg(
+ diamond.up -> 1,
+ diamond.left -> 1,
+ diamond.right -> 1,
+ diamond.down -> 1
+ )
+ )
+
+ 'defCachedDiamond - check(defCachedDiamond)(
+ _.down,
+ Agg(_.up, _.left, _.right, _.down),
+ Agg(
+ defCachedDiamond.up -> 2,
+ defCachedDiamond.left -> 2,
+ defCachedDiamond.right -> 2,
+ defCachedDiamond.down -> 2
+ )
+ )
+
+ 'anonDiamond - check(anonDiamond)(
+ _.down,
+ Agg(_.down, _.up),
+ Agg(
+ anonDiamond.up -> 1,
+ anonDiamond.down -> 3
+ )
+ )
+ 'bigSingleTerminal - check(bigSingleTerminal)(
+ _.j,
+ Agg(_.a, _.b, _.e, _.f, _.i, _.j),
+ Agg(
+ bigSingleTerminal.a -> 3,
+ bigSingleTerminal.b -> 2,
+ bigSingleTerminal.e -> 9,
+ bigSingleTerminal.i -> 6,
+ bigSingleTerminal.f -> 4,
+ bigSingleTerminal.j -> 4
+ )
+ )
+ }
+ 'multiTerminalGroupCounts - {
+ def countGroups(goals: Task[_]*) = {
+
+ val topoSorted = Graph.topoSorted(
+ Graph.transitiveTargets(Agg.from(goals))
+ )
+ val grouped = Graph.groupAroundImportantTargets(topoSorted) {
+ case t: NamedTask[Any] => t
+ case t if goals.contains(t) => t
+ }
+ grouped.keyCount
+ }
+
+ 'separateGroups - {
+ import separateGroups._
+ val groupCount = countGroups(right, left)
+ assert(groupCount == 3)
+ }
+
+ 'triangleTask - {
+ // Make sure the following graph ends up as a single group, since although
+ // `right` depends on `left`, both of them depend on the un-cached `task`
+ // which would force them both to re-compute every time `task` changes
+ import triangleTask._
+ val groupCount = countGroups(right, left)
+ assert(groupCount == 2)
+ }
+
+
+ 'multiTerminalGroup - {
+ // Make sure the following graph ends up as two groups
+ import multiTerminalGroup._
+ val groupCount = countGroups(right, left)
+ assert(groupCount == 2)
+ }
+
+
+ 'multiTerminalBoundary - {
+ // Make sure the following graph ends up as a three groups: one for
+ // each cached target, and one for the downstream task we are running
+ import multiTerminalBoundary._
+ val groupCount = countGroups(task2)
+ assert(groupCount == 3)
+ }
+ }
+
+
+ }
+}
diff --git a/main/test/src/mill/define/MacroErrorTests.scala b/main/test/src/mill/define/MacroErrorTests.scala
new file mode 100644
index 00000000..a389feaa
--- /dev/null
+++ b/main/test/src/mill/define/MacroErrorTests.scala
@@ -0,0 +1,83 @@
+package mill.define
+
+import utest._
+import mill.{T, Module}
+import mill.util.TestUtil
+object MacroErrorTests extends TestSuite{
+
+ val tests = Tests{
+
+ 'errors{
+ val expectedMsg =
+ "T{} members must be defs defined in a Cacher class/trait/object body"
+
+ val err = compileError("object Foo extends TestUtil.BaseModule{ val x = T{1} }")
+ assert(err.msg == expectedMsg)
+ }
+
+ 'badTmacro - {
+ // Make sure we can reference values from outside the T{...} block as part
+ // of our `Target#apply()` calls, but we cannot reference any values that
+ // come from inside the T{...} block
+ 'pos - {
+ val e = compileError("""
+ val a = T{ 1 }
+ val arr = Array(a)
+ val b = {
+ val c = 0
+ T{
+ arr(c)()
+ }
+ }
+ """)
+ assert(e.msg.contains(
+ "Modules, Targets and Commands can only be defined within a mill Module")
+ )
+ }
+ 'neg - {
+
+ val expectedMsg =
+ "Target#apply() call cannot use `value n` defined within the T{...} block"
+ val err = compileError("""new Module{
+ def a = T{ 1 }
+ val arr = Array(a)
+ def b = {
+ T{
+ val n = 0
+ arr(n)()
+ }
+ }
+ }""")
+ assert(err.msg == expectedMsg)
+ }
+ 'neg2 - {
+
+ val expectedMsg =
+ "Target#apply() call cannot use `value x` defined within the T{...} block"
+ val err = compileError("""new Module{
+ def a = T{ 1 }
+ val arr = Array(a)
+ def b = {
+ T{
+ arr.map{x => x()}
+ }
+ }
+ }""")
+ assert(err.msg == expectedMsg)
+ }
+ 'neg3{
+ val borkedCachedDiamond1 = utest.compileError("""
+ object borkedCachedDiamond1 {
+ def up = T{ TestUtil.test() }
+ def left = T{ TestUtil.test(up) }
+ def right = T{ TestUtil.test(up) }
+ def down = T{ TestUtil.test(left, right) }
+ }
+ """)
+ assert(borkedCachedDiamond1.msg.contains(
+ "Modules, Targets and Commands can only be defined within a mill Module")
+ )
+ }
+ }
+ }
+}
diff --git a/main/test/src/mill/eval/CrossTests.scala b/main/test/src/mill/eval/CrossTests.scala
new file mode 100644
index 00000000..aa12e180
--- /dev/null
+++ b/main/test/src/mill/eval/CrossTests.scala
@@ -0,0 +1,56 @@
+package mill.eval
+
+import ammonite.ops._
+import mill.define.Discover
+import mill.util.TestEvaluator
+import mill.util.TestEvaluator.implicitDisover
+import mill.util.TestGraphs.{crossResolved, doubleCross, nestedCrosses, singleCross}
+import utest._
+object CrossTests extends TestSuite{
+ val tests = Tests{
+ 'singleCross - {
+ val check = new TestEvaluator(singleCross)
+
+ val Right(("210", 1)) = check.apply(singleCross.cross("210").suffix)
+ val Right(("211", 1)) = check.apply(singleCross.cross("211").suffix)
+ val Right(("212", 1)) = check.apply(singleCross.cross("212").suffix)
+ }
+
+ 'crossResolved - {
+ val check = new TestEvaluator(crossResolved)
+
+ val Right(("2.10", 1)) = check.apply(crossResolved.foo("2.10").suffix)
+ val Right(("2.11", 1)) = check.apply(crossResolved.foo("2.11").suffix)
+ val Right(("2.12", 1)) = check.apply(crossResolved.foo("2.12").suffix)
+
+ val Right(("_2.10", 1)) = check.apply(crossResolved.bar("2.10").longSuffix)
+ val Right(("_2.11", 1)) = check.apply(crossResolved.bar("2.11").longSuffix)
+ val Right(("_2.12", 1)) = check.apply(crossResolved.bar("2.12").longSuffix)
+ }
+
+
+ 'doubleCross - {
+ val check = new TestEvaluator(doubleCross)
+
+ val Right(("210_jvm", 1)) = check.apply(doubleCross.cross("210", "jvm").suffix)
+ val Right(("210_js", 1)) = check.apply(doubleCross.cross("210", "js").suffix)
+ val Right(("211_jvm", 1)) = check.apply(doubleCross.cross("211", "jvm").suffix)
+ val Right(("211_js", 1)) = check.apply(doubleCross.cross("211", "js").suffix)
+ val Right(("212_jvm", 1)) = check.apply(doubleCross.cross("212", "jvm").suffix)
+ val Right(("212_js", 1)) = check.apply(doubleCross.cross("212", "js").suffix)
+ val Right(("212_native", 1)) = check.apply(doubleCross.cross("212", "native").suffix)
+ }
+
+ 'nestedCrosses - {
+ val check = new TestEvaluator(nestedCrosses)
+
+ val Right(("210_jvm", 1)) = check.apply(nestedCrosses.cross("210").cross2("jvm").suffix)
+ val Right(("210_js", 1)) = check.apply(nestedCrosses.cross("210").cross2("js").suffix)
+ val Right(("211_jvm", 1)) = check.apply(nestedCrosses.cross("211").cross2("jvm").suffix)
+ val Right(("211_js", 1)) = check.apply(nestedCrosses.cross("211").cross2("js").suffix)
+ val Right(("212_jvm", 1)) = check.apply(nestedCrosses.cross("212").cross2("jvm").suffix)
+ val Right(("212_js", 1)) = check.apply(nestedCrosses.cross("212").cross2("js").suffix)
+ val Right(("212_native", 1)) = check.apply(nestedCrosses.cross("212").cross2("native").suffix)
+ }
+ }
+}
diff --git a/main/test/src/mill/eval/EvaluationTests.scala b/main/test/src/mill/eval/EvaluationTests.scala
new file mode 100644
index 00000000..e5f0e57d
--- /dev/null
+++ b/main/test/src/mill/eval/EvaluationTests.scala
@@ -0,0 +1,317 @@
+package mill.eval
+
+
+import mill.util.TestUtil.{Test, test}
+import mill.define.{Discover, Graph, Target, Task}
+import mill.{Module, T}
+import mill.util.{DummyLogger, TestEvaluator, TestGraphs, TestUtil}
+import mill.util.Strict.Agg
+import utest._
+import utest.framework.TestPath
+import mill.util.TestEvaluator.implicitDisover
+import ammonite.ops._
+
+object EvaluationTests extends TestSuite{
+ class Checker[T <: TestUtil.BaseModule](module: T)
+ (implicit tp: TestPath, discover: Discover[T]) {
+ // Make sure data is persisted even if we re-create the evaluator each time
+
+ def evaluator = new TestEvaluator(module).evaluator
+
+ def apply(target: Task[_], expValue: Any,
+ expEvaled: Agg[Task[_]],
+ // How many "other" tasks were evaluated other than those listed above.
+ // Pass in -1 to skip the check entirely
+ extraEvaled: Int = 0,
+ // Perform a second evaluation of the same tasks, and make sure the
+ // outputs are the same but nothing was evaluated. Disable this if you
+ // are directly evaluating tasks which need to re-evaluate every time
+ secondRunNoOp: Boolean = true) = {
+
+ val evaled = evaluator.evaluate(Agg(target))
+
+ val (matchingReturnedEvaled, extra) =
+ evaled.evaluated.indexed.partition(expEvaled.contains)
+
+ assert(
+ evaled.values == Seq(expValue),
+ matchingReturnedEvaled.toSet == expEvaled.toSet,
+ extraEvaled == -1 || extra.length == extraEvaled
+ )
+
+ // Second time the value is already cached, so no evaluation needed
+ if (secondRunNoOp){
+ val evaled2 = evaluator.evaluate(Agg(target))
+ val expecteSecondRunEvaluated = Agg()
+ assert(
+ evaled2.values == evaled.values,
+ evaled2.evaluated == expecteSecondRunEvaluated
+ )
+ }
+ }
+ }
+
+
+ val tests = Tests{
+ object graphs extends TestGraphs()
+ import graphs._
+ import TestGraphs._
+ 'evaluateSingle - {
+
+ 'singleton - {
+ import singleton._
+ val check = new Checker(singleton)
+ // First time the target is evaluated
+ check(single, expValue = 0, expEvaled = Agg(single))
+
+ single.counter += 1
+ // After incrementing the counter, it forces re-evaluation
+ check(single, expValue = 1, expEvaled = Agg(single))
+ }
+ 'pair - {
+ import pair._
+ val check = new Checker(pair)
+ check(down, expValue = 0, expEvaled = Agg(up, down))
+
+ down.counter += 1
+ check(down, expValue = 1, expEvaled = Agg(down))
+
+ up.counter += 1
+ check(down, expValue = 2, expEvaled = Agg(up, down))
+ }
+ 'anonTriple - {
+ import anonTriple._
+ val check = new Checker(anonTriple)
+ val middle = down.inputs(0)
+ check(down, expValue = 0, expEvaled = Agg(up, middle, down))
+
+ down.counter += 1
+ check(down, expValue = 1, expEvaled = Agg(middle, down))
+
+ up.counter += 1
+ check(down, expValue = 2, expEvaled = Agg(up, middle, down))
+
+ middle.asInstanceOf[TestUtil.Test].counter += 1
+
+ check(down, expValue = 3, expEvaled = Agg(middle, down))
+ }
+ 'diamond - {
+ import diamond._
+ val check = new Checker(diamond)
+ check(down, expValue = 0, expEvaled = Agg(up, left, right, down))
+
+ down.counter += 1
+ check(down, expValue = 1, expEvaled = Agg(down))
+
+ up.counter += 1
+ // Increment by 2 because up is referenced twice: once by left once by right
+ check(down, expValue = 3, expEvaled = Agg(up, left, right, down))
+
+ left.counter += 1
+ check(down, expValue = 4, expEvaled = Agg(left, down))
+
+ right.counter += 1
+ check(down, expValue = 5, expEvaled = Agg(right, down))
+ }
+ 'anonDiamond - {
+ import anonDiamond._
+ val check = new Checker(anonDiamond)
+ val left = down.inputs(0).asInstanceOf[TestUtil.Test]
+ val right = down.inputs(1).asInstanceOf[TestUtil.Test]
+ check(down, expValue = 0, expEvaled = Agg(up, left, right, down))
+
+ down.counter += 1
+ check(down, expValue = 1, expEvaled = Agg(left, right, down))
+
+ up.counter += 1
+ // Increment by 2 because up is referenced twice: once by left once by right
+ check(down, expValue = 3, expEvaled = Agg(up, left, right, down))
+
+ left.counter += 1
+ check(down, expValue = 4, expEvaled = Agg(left, right, down))
+
+ right.counter += 1
+ check(down, expValue = 5, expEvaled = Agg(left, right, down))
+ }
+
+ 'bigSingleTerminal - {
+ import bigSingleTerminal._
+ val check = new Checker(bigSingleTerminal)
+
+ check(j, expValue = 0, expEvaled = Agg(a, b, e, f, i, j), extraEvaled = 22)
+
+ j.counter += 1
+ check(j, expValue = 1, expEvaled = Agg(j), extraEvaled = 3)
+
+ i.counter += 1
+ // increment value by 2 because `i` is used twice on the way to `j`
+ check(j, expValue = 3, expEvaled = Agg(j, i), extraEvaled = 8)
+
+ b.counter += 1
+ // increment value by 4 because `b` is used four times on the way to `j`
+ check(j, expValue = 7, expEvaled = Agg(b, e, f, i, j), extraEvaled = 20)
+ }
+ }
+
+ 'evaluateMixed - {
+ 'separateGroups - {
+ // Make sure that `left` and `right` are able to recompute separately,
+ // even though one depends on the other
+
+ import separateGroups._
+ val checker = new Checker(separateGroups)
+ val evaled1 = checker.evaluator.evaluate(Agg(right, left))
+ val filtered1 = evaled1.evaluated.filter(_.isInstanceOf[Target[_]])
+ assert(filtered1 == Agg(change, left, right))
+ val evaled2 = checker.evaluator.evaluate(Agg(right, left))
+ val filtered2 = evaled2.evaluated.filter(_.isInstanceOf[Target[_]])
+ assert(filtered2 == Agg())
+ change.counter += 1
+ val evaled3 = checker.evaluator.evaluate(Agg(right, left))
+ val filtered3 = evaled3.evaluated.filter(_.isInstanceOf[Target[_]])
+ assert(filtered3 == Agg(change, right))
+
+
+ }
+ 'triangleTask - {
+
+ import triangleTask._
+ val checker = new Checker(triangleTask)
+ checker(right, 3, Agg(left, right), extraEvaled = -1)
+ checker(left, 1, Agg(), extraEvaled = -1)
+
+ }
+ 'multiTerminalGroup - {
+ import multiTerminalGroup._
+
+ val checker = new Checker(multiTerminalGroup)
+ checker(right, 1, Agg(right), extraEvaled = -1)
+ checker(left, 1, Agg(left), extraEvaled = -1)
+ }
+
+ 'multiTerminalBoundary - {
+
+ import multiTerminalBoundary._
+
+ val checker = new Checker(multiTerminalBoundary)
+ checker(task2, 4, Agg(right, left), extraEvaled = -1, secondRunNoOp = false)
+ checker(task2, 4, Agg(), extraEvaled = -1, secondRunNoOp = false)
+ }
+
+ 'overrideSuperTask - {
+ // Make sure you can override targets, call their supers, and have the
+ // overriden target be allocated a spot within the overriden/ folder of
+ // the main publically-available target
+ import canOverrideSuper._
+
+ val checker = new Checker(canOverrideSuper)
+ checker(foo, Seq("base", "object"), Agg(foo), extraEvaled = -1)
+
+
+ val public = ammonite.ops.read(checker.evaluator.outPath / 'foo / "meta.json")
+ val overriden = ammonite.ops.read(
+ checker.evaluator.outPath / 'foo /
+ 'overriden / "mill.util.TestGraphs.BaseModule#foo" / "meta.json"
+ )
+ assert(
+ public.contains("base"),
+ public.contains("object"),
+ overriden.contains("base"),
+ !overriden.contains("object")
+ )
+ }
+ 'overrideSuperCommand - {
+ // Make sure you can override commands, call their supers, and have the
+ // overriden command be allocated a spot within the overriden/ folder of
+ // the main publically-available command
+ import canOverrideSuper._
+
+ val checker = new Checker(canOverrideSuper)
+ val runCmd = cmd(1)
+ checker(
+ runCmd,
+ Seq("base1", "object1"),
+ Agg(runCmd),
+ extraEvaled = -1,
+ secondRunNoOp = false
+ )
+
+ val public = ammonite.ops.read(checker.evaluator.outPath / 'cmd / "meta.json")
+ val overriden = ammonite.ops.read(
+ checker.evaluator.outPath / 'cmd /
+ 'overriden / "mill.util.TestGraphs.BaseModule#cmd" / "meta.json"
+ )
+ assert(
+ public.contains("base1"),
+ public.contains("object1"),
+ overriden.contains("base1"),
+ !overriden.contains("object1")
+ )
+ }
+
+ 'tasksAreUncached - {
+ // Make sure the tasks `left` and `middle` re-compute every time, while
+ // the target `right` does not
+ //
+ // ___ left ___
+ // / \
+ // up middle -- down
+ // /
+ // right
+ object build extends TestUtil.BaseModule{
+ var leftCount = 0
+ var rightCount = 0
+ var middleCount = 0
+ def up = T{ test.anon() }
+ def left = T.task{ leftCount += 1; up() + 1 }
+ def middle = T.task{ middleCount += 1; 100 }
+ def right = T{ rightCount += 1; 10000 }
+ def down = T{ left() + middle() + right() }
+ }
+
+ import build._
+
+ // Ensure task objects themselves are not cached, and recomputed each time
+ assert(
+ up eq up,
+ left ne left,
+ middle ne middle,
+ right eq right,
+ down eq down
+ )
+
+ // During the first evaluation, they get computed normally like any
+ // cached target
+ val check = new Checker(build)
+ assert(leftCount == 0, rightCount == 0)
+ check(down, expValue = 10101, expEvaled = Agg(up, right, down), extraEvaled = 8)
+ assert(leftCount == 1, middleCount == 1, rightCount == 1)
+
+ // If the upstream `up` doesn't change, the entire block of tasks
+ // doesn't need to recompute
+ check(down, expValue = 10101, expEvaled = Agg())
+ assert(leftCount == 1, middleCount == 1, rightCount == 1)
+
+ // But if `up` changes, the entire block of downstream tasks needs to
+ // recompute together, including `middle` which doesn't depend on `up`,
+ // because tasks have no cached value that can be used. `right`, which
+ // is a cached Target, does not recompute
+ up.inputs(0).asInstanceOf[Test].counter += 1
+ check(down, expValue = 10102, expEvaled = Agg(up, down), extraEvaled = 6)
+ assert(leftCount == 2, middleCount == 2, rightCount == 1)
+
+ // Running the tasks themselves results in them being recomputed every
+ // single time, even if nothing changes
+ check(left, expValue = 2, expEvaled = Agg(), extraEvaled = 1, secondRunNoOp = false)
+ assert(leftCount == 3, middleCount == 2, rightCount == 1)
+ check(left, expValue = 2, expEvaled = Agg(), extraEvaled = 1, secondRunNoOp = false)
+ assert(leftCount == 4, middleCount == 2, rightCount == 1)
+
+ check(middle, expValue = 100, expEvaled = Agg(), extraEvaled = 2, secondRunNoOp = false)
+ assert(leftCount == 4, middleCount == 3, rightCount == 1)
+ check(middle, expValue = 100, expEvaled = Agg(), extraEvaled = 2, secondRunNoOp = false)
+ assert(leftCount == 4, middleCount == 4, rightCount == 1)
+ }
+ }
+ }
+}
diff --git a/main/test/src/mill/eval/FailureTests.scala b/main/test/src/mill/eval/FailureTests.scala
new file mode 100644
index 00000000..90cff686
--- /dev/null
+++ b/main/test/src/mill/eval/FailureTests.scala
@@ -0,0 +1,100 @@
+package mill.eval
+import mill.T
+import mill.util.{TestEvaluator, TestUtil}
+import ammonite.ops.{Path, pwd, rm}
+import mill.eval.Result.OuterStack
+import utest._
+import utest.framework.TestPath
+import mill.util.TestEvaluator.implicitDisover
+
+object FailureTests extends TestSuite{
+
+ val tests = Tests{
+ val graphs = new mill.util.TestGraphs()
+ import graphs._
+
+ 'evaluateSingle - {
+ val check = new TestEvaluator(singleton)
+ check.fail(
+ target = singleton.single,
+ expectedFailCount = 0,
+ expectedRawValues = Seq(Result.Success(0))
+ )
+
+ singleton.single.failure = Some("lols")
+
+ check.fail(
+ target = singleton.single,
+ expectedFailCount = 1,
+ expectedRawValues = Seq(Result.Failure("lols"))
+ )
+
+ singleton.single.failure = None
+
+ check.fail(
+ target = singleton.single,
+ expectedFailCount = 0,
+ expectedRawValues = Seq(Result.Success(0))
+ )
+
+
+ val ex = new IndexOutOfBoundsException()
+ singleton.single.exception = Some(ex)
+
+
+ check.fail(
+ target = singleton.single,
+ expectedFailCount = 1,
+ expectedRawValues = Seq(Result.Exception(ex, new OuterStack(Nil)))
+ )
+ }
+ 'evaluatePair - {
+ val check = new TestEvaluator(pair)
+ check.fail(
+ pair.down,
+ expectedFailCount = 0,
+ expectedRawValues = Seq(Result.Success(0))
+ )
+
+ pair.up.failure = Some("lols")
+
+ check.fail(
+ pair.down,
+ expectedFailCount = 1,
+ expectedRawValues = Seq(Result.Skipped)
+ )
+
+ pair.up.failure = None
+
+ check.fail(
+ pair.down,
+ expectedFailCount = 0,
+ expectedRawValues = Seq(Result.Success(0))
+ )
+
+ pair.up.exception = Some(new IndexOutOfBoundsException())
+
+ check.fail(
+ pair.down,
+ expectedFailCount = 1,
+ expectedRawValues = Seq(Result.Skipped)
+ )
+ }
+ 'multipleUsesOfDest - {
+ object build extends TestUtil.BaseModule {
+ // Using `T.ctx( ).dest` twice in a single task is ok
+ def left = T{ + T.ctx().dest.toString.length + T.ctx().dest.toString.length }
+
+ // Using `T.ctx( ).dest` once in two different tasks is not ok
+ val task = T.task{ T.ctx().dest.toString.length }
+ def right = T{ task() + left() + T.ctx().dest.toString().length }
+ }
+
+ val check = new TestEvaluator(build)
+ val Right(_) = check(build.left)
+ val Left(Result.Exception(e, _)) = check(build.right)
+ assert(e.getMessage.contains("`dest` can only be used in one place"))
+ }
+ }
+}
+
diff --git a/main/test/src/mill/eval/JavaCompileJarTests.scala b/main/test/src/mill/eval/JavaCompileJarTests.scala
new file mode 100644
index 00000000..966c272f
--- /dev/null
+++ b/main/test/src/mill/eval/JavaCompileJarTests.scala
@@ -0,0 +1,159 @@
+package mill.eval
+
+import ammonite.ops.ImplicitWd._
+import ammonite.ops._
+import mill.define.{Discover, Input, Target, Task}
+import mill.modules.Jvm
+import mill.util.Ctx.Dest
+import mill.{Module, T}
+import mill.util.{DummyLogger, Loose, TestEvaluator, TestUtil}
+import mill.util.Strict.Agg
+import utest._
+import mill._
+import TestEvaluator.implicitDisover
+object JavaCompileJarTests extends TestSuite{
+ def compileAll(sources: Seq[PathRef])(implicit ctx: Dest) = {
+ mkdir(ctx.dest)
+ import ammonite.ops._
+ %("javac", sources.map(_.path.toString()), "-d", ctx.dest)(wd = ctx.dest)
+ PathRef(ctx.dest)
+ }
+
+ val tests = Tests{
+ 'javac {
+ val javacSrcPath = pwd / 'main / 'test / 'resources / 'examples / 'javac
+ val javacDestPath = TestUtil.getOutPath() / 'src
+
+ mkdir(javacDestPath / up)
+ cp(javacSrcPath, javacDestPath)
+
+ object Build extends TestUtil.BaseModule{
+ def sourceRootPath = javacDestPath / 'src
+ def resourceRootPath = javacDestPath / 'resources
+
+ // sourceRoot -> allSources -> classFiles
+ // |
+ // v
+ // resourceRoot ----> jar
+ def sourceRoot = T.sources{ sourceRootPath }
+ def resourceRoot = T.sources{ resourceRootPath }
+ def allSources = T{ sourceRoot().flatMap(p => ls.rec(p.path)).map(PathRef(_)) }
+ def classFiles = T{ compileAll(allSources()) }
+ def jar = T{ Jvm.createJar(Loose.Agg(classFiles().path) ++ resourceRoot().map(_.path)) }
+
+ def run(mainClsName: String) = T.command{
+ %%('java, "-cp", classFiles().path, mainClsName)
+ }
+ }
+
+ import Build._
+
+ var evaluator = new TestEvaluator(Build)
+ def eval[T](t: Task[T]) = {
+ evaluator.apply(t)
+ }
+ def check(targets: Agg[Task[_]], expected: Agg[Task[_]]) = {
+ evaluator.check(targets, expected)
+ }
+
+ def append(path: Path, txt: String) = ammonite.ops.write.append(path, txt)
+
+
+ check(
+ targets = Agg(jar),
+ expected = Agg(allSources, classFiles, jar)
+ )
+
+ // Re-running with no changes results in nothing being evaluated
+ check(targets = Agg(jar), expected = Agg())
+
+ // Appending an empty string gets ignored due to file-content hashing
+ append(sourceRootPath / "Foo.java", "")
+ check(targets = Agg(jar), expected = Agg())
+
+ // Appending whitespace forces a recompile, but the classfilesend up
+ // exactly the same so no re-jarring.
+ append(sourceRootPath / "Foo.java", " ")
+ // Note that `sourceRoot` and `resourceRoot` never turn up in the `expected`
+ // list, because they are `Source`s not `Target`s
+ check(targets = Agg(jar), expected = Agg(/*sourceRoot, */allSources, classFiles))
+
+ // Appending a new class changes the classfiles, which forces us to
+ // re-create the final jar
+ append(sourceRootPath / "Foo.java", "\nclass FooTwo{}")
+ check(targets = Agg(jar), expected = Agg(allSources, classFiles, jar))
+
+ // Tweaking the resources forces rebuild of the final jar, without
+ // recompiling classfiles
+ append(resourceRootPath / "hello.txt", " ")
+ check(targets = Agg(jar), expected = Agg(jar))
+
+ // You can swap evaluators halfway without any ill effects
+ evaluator = new TestEvaluator(Build)
+
+ // Asking for an intermediate target forces things to be build up to that
+ // target only; these are re-used for any downstream targets requested
+ append(sourceRootPath / "Bar.java", "\nclass BarTwo{}")
+ append(resourceRootPath / "hello.txt", " ")
+ check(targets = Agg(classFiles), expected = Agg(allSources, classFiles))
+ check(targets = Agg(jar), expected = Agg(jar))
+ check(targets = Agg(allSources), expected = Agg())
+
+ append(sourceRootPath / "Bar.java", "\nclass BarThree{}")
+ append(resourceRootPath / "hello.txt", " ")
+ check(targets = Agg(resourceRoot), expected = Agg())
+ check(targets = Agg(allSources), expected = Agg(allSources))
+ check(targets = Agg(jar), expected = Agg(classFiles, jar))
+
+ val jarContents = %%('jar, "-tf", evaluator.outPath/'jar/'dest/"out.jar")(evaluator.outPath).out.string
+ val expectedJarContents =
+ """META-INF/MANIFEST.MF
+ |test/Bar.class
+ |test/BarThree.class
+ |test/BarTwo.class
+ |test/Foo.class
+ |test/FooTwo.class
+ |hello.txt
+ |""".stripMargin
+ assert(jarContents == expectedJarContents)
+
+ val executed = %%('java, "-cp", evaluator.outPath/'jar/'dest/"out.jar", "test.Foo")(evaluator.outPath).out.string
+ assert(executed == (31337 + 271828) + "\n")
+
+ for(i <- 0 until 3){
+ // Build.run is not cached, so every time we eval it it has to
+ // re-evaluate
+ val Right((runOutput, evalCount)) = eval(Build.run("test.Foo"))
+ assert(
+ runOutput.out.string == (31337 + 271828) + "\n",
+ evalCount == 1
+ )
+ }
+
+ val Left(Result.Exception(ex, _)) = eval(Build.run("test.BarFour"))
+
+ assert(ex.getMessage.contains("Could not find or load main class"))
+
+ append(
+ sourceRootPath / "Bar.java",
+ """
+ class BarFour{
+ public static void main(String[] args){
+ System.out.println("New Cls!");
+ }
+ }
+ """
+ )
+ val Right((runOutput2, evalCount2)) = eval(Build.run("test.BarFour"))
+ assert(
+ runOutput2.out.string == "New Cls!\n",
+ evalCount2 == 3
+ )
+ val Right((runOutput3, evalCount3)) = eval(Build.run("test.BarFour"))
+ assert(
+ runOutput3.out.string == "New Cls!\n",
+ evalCount3 == 1
+ )
+ }
+ }
+}
diff --git a/main/test/src/mill/eval/ModuleTests.scala b/main/test/src/mill/eval/ModuleTests.scala
new file mode 100644
index 00000000..c6125b32
--- /dev/null
+++ b/main/test/src/mill/eval/ModuleTests.scala
@@ -0,0 +1,45 @@
+package mill.eval
+
+import ammonite.ops._
+import mill.util.{TestEvaluator, TestUtil}
+import mill.T
+import mill.define.Discover
+import mill.util.TestEvaluator.implicitDisover
+import utest._
+
+object ModuleTests extends TestSuite{
+ object ExternalModule extends mill.define.ExternalModule {
+ def x = T{13}
+ object inner extends mill.Module{
+ def y = T{17}
+ }
+ def millDiscover = Discover[this.type]
+ }
+ object Build extends TestUtil.BaseModule{
+ def z = T{ ExternalModule.x() + ExternalModule.inner.y() }
+ }
+ val tests = Tests {
+ rm(TestEvaluator.externalOutPath)
+ 'externalModuleTargetsAreNamespacedByModulePackagePath - {
+ val check = new TestEvaluator(Build)
+
+ val Right((30, 1)) = check.apply(Build.z)
+ assert(
+ read(check.evaluator.outPath / 'z / "meta.json").contains("30"),
+ read(TestEvaluator.externalOutPath / 'mill / 'eval / 'ModuleTests / 'ExternalModule / 'x / "meta.json").contains("13"),
+ read(TestEvaluator.externalOutPath / 'mill / 'eval / 'ModuleTests / 'ExternalModule / 'inner / 'y / "meta.json").contains("17")
+ )
+ }
+ 'externalModuleMustBeGlobalStatic - {
+
+
+ object Build extends mill.define.ExternalModule {
+
+ def z = T{ ExternalModule.x() + ExternalModule.inner.y() }
+ def millDiscover = Discover[this.type]
+ }
+
+ intercept[java.lang.AssertionError]{ Build }
+ }
+ }
+}
diff --git a/main/test/src/mill/eval/TarjanTests.scala b/main/test/src/mill/eval/TarjanTests.scala
new file mode 100644
index 00000000..2f9d0a4d
--- /dev/null
+++ b/main/test/src/mill/eval/TarjanTests.scala
@@ -0,0 +1,91 @@
+package mill.eval
+
+import utest._
+
+object TarjanTests extends TestSuite{
+ def check(input: Seq[Seq[Int]], expected: Seq[Seq[Int]]) = {
+ val result = Tarjans(input).map(_.sorted)
+ val sortedExpected = expected.map(_.sorted)
+ assert(result == sortedExpected)
+ }
+ val tests = Tests{
+ //
+ 'empty - check(Seq(), Seq())
+
+ // (0)
+ 'singleton - check(Seq(Seq()), Seq(Seq(0)))
+
+
+ // (0)-.
+ // ^._/
+ 'selfCycle - check(Seq(Seq(0)), Seq(Seq(0)))
+
+ // (0) <-> (1)
+ 'simpleCycle- check(Seq(Seq(1), Seq(0)), Seq(Seq(1, 0)))
+
+ // (0) (1) (2)
+ 'multipleSingletons - check(
+ Seq(Seq(), Seq(), Seq()),
+ Seq(Seq(0), Seq(1), Seq(2))
+ )
+
+ // (0) -> (1) -> (2)
+ 'straightLineNoCycles- check(
+ Seq(Seq(1), Seq(2), Seq()),
+ Seq(Seq(2), Seq(1), Seq(0))
+ )
+
+ // (0) <- (1) <- (2)
+ 'straightLineNoCyclesReversed- check(
+ Seq(Seq(), Seq(0), Seq(1)),
+ Seq(Seq(0), Seq(1), Seq(2))
+ )
+
+ // (0) <-> (1) (2) -> (3) -> (4)
+ // ^.____________/
+ 'independentSimpleCycles - check(
+ Seq(Seq(1), Seq(0), Seq(3), Seq(4), Seq(2)),
+ Seq(Seq(1, 0), Seq(4, 3, 2))
+ )
+
+ // ___________________
+ // v \
+ // (0) <-> (1) (2) -> (3) -> (4)
+ // ^.____________/
+ 'independentLinkedCycles - check(
+ Seq(Seq(1), Seq(0), Seq(3), Seq(4), Seq(2, 1)),
+ Seq(Seq(1, 0), Seq(4, 3, 2))
+ )
+ // _____________
+ // / v
+ // (0) <-> (1) (2) -> (3) -> (4)
+ // ^.____________/
+ 'independentLinkedCycles2 - check(
+ Seq(Seq(1, 2), Seq(0), Seq(3), Seq(4), Seq(2)),
+ Seq(Seq(4, 3, 2), Seq(1, 0))
+ )
+
+ // _____________
+ // / v
+ // (0) <-> (1) (2) -> (3) -> (4)
+ // ^. ^.____________/
+ // \________________/
+ 'combinedCycles - check(
+ Seq(Seq(1, 2), Seq(0), Seq(3), Seq(4), Seq(2, 1)),
+ Seq(Seq(4, 3, 2, 1, 0))
+ )
+ //
+ // (0) <-> (1) <- (2) <- (3) <-> (4) <- (5)
+ // ^.____________/ / /
+ // / /
+ // (6) <- (7) <-/ (8) <-'
+ // / /
+ // v /
+ // (9) <--------'
+ 'combinedCycles - check(
+ Seq(Seq(1), Seq(0), Seq(0, 1), Seq(2, 4, 7, 9), Seq(3), Seq(4, 8), Seq(9), Seq(6), Seq(), Seq()),
+ Seq(Seq(0, 1), Seq(2), Seq(9), Seq(6), Seq(7), Seq(3, 4), Seq(8), Seq(5))
+ )
+
+ }
+} \ No newline at end of file
diff --git a/main/test/src/mill/eval/TaskTests.scala b/main/test/src/mill/eval/TaskTests.scala
new file mode 100644
index 00000000..114a2910
--- /dev/null
+++ b/main/test/src/mill/eval/TaskTests.scala
@@ -0,0 +1,95 @@
+package mill.eval
+
+import utest._
+import ammonite.ops._
+import mill.T
+import mill.util.TestEvaluator.implicitDisover
+import mill.util.TestEvaluator
+object TaskTests extends TestSuite{
+ val tests = Tests{
+ object build extends mill.util.TestUtil.BaseModule{
+ var count = 0
+ // Explicitly instantiate `Function1` objects to make sure we get
+ // different instances each time
+ def staticWorker = T.worker{
+ new Function1[Int, Int] {
+ def apply(v1: Int) = v1 + 1
+ }
+ }
+ def noisyWorker = T.worker{
+ new Function1[Int, Int] {
+ def apply(v1: Int) = input() + 1
+ }
+ }
+ def input = T.input{
+ count += 1
+ count
+ }
+ def task = T.task{
+ count += 1
+ count
+ }
+ def taskInput = T{ input() }
+ def taskNoInput = T{ task() }
+
+ def persistent = T.persistent{
+ input() // force re-computation
+ mkdir(T.ctx().dest)
+ write.append(T.ctx().dest/'count, "hello\n")
+ read.lines(T.ctx().dest/'count).length
+ }
+ def nonPersistent = T{
+ input() // force re-computation
+ mkdir(T.ctx().dest)
+ write.append(T.ctx().dest/'count, "hello\n")
+ read.lines(T.ctx().dest/'count).length
+ }
+
+ def staticWorkerDownstream = T{
+ staticWorker().apply(1)
+ }
+ def noisyWorkerDownstream = T{
+ noisyWorker().apply(1)
+ }
+ }
+
+ 'inputs - {
+ // Inputs always re-evaluate, including forcing downstream cached Targets
+ // to re-evaluate, but normal Tasks behind a Target run once then are cached
+ val check = new TestEvaluator(build)
+
+ val Right((1, 1)) = check.apply(build.taskInput)
+ val Right((2, 1)) = check.apply(build.taskInput)
+ val Right((3, 1)) = check.apply(build.taskInput)
+
+ val Right((4, 1)) = check.apply(build.taskNoInput)
+ val Right((4, 0)) = check.apply(build.taskNoInput)
+ val Right((4, 0)) = check.apply(build.taskNoInput)
+ }
+
+ 'persistent - {
+ // Persistent tasks keep the working dir around between runs
+ val check = new TestEvaluator(build)
+ val Right((1, 1)) = check.apply(build.persistent)
+ val Right((2, 1)) = check.apply(build.persistent)
+ val Right((3, 1)) = check.apply(build.persistent)
+
+ val Right((1, 1)) = check.apply(build.nonPersistent)
+ val Right((1, 1)) = check.apply(build.nonPersistent)
+ val Right((1, 1)) = check.apply(build.nonPersistent)
+ }
+
+ 'worker - {
+ // Persistent task
+ def check = new TestEvaluator(build)
+
+ val Right((2, 1)) = check.apply(build.noisyWorkerDownstream)
+ val Right((3, 1)) = check.apply(build.noisyWorkerDownstream)
+ val Right((4, 1)) = check.apply(build.noisyWorkerDownstream)
+
+ val Right((2, 1)) = check.apply(build.staticWorkerDownstream)
+ val Right((2, 0)) = check.apply(build.staticWorkerDownstream)
+ val Right((2, 0)) = check.apply(build.staticWorkerDownstream)
+ }
+ }
+}
diff --git a/main/test/src/mill/main/JavaCompileJarTests.scala b/main/test/src/mill/main/JavaCompileJarTests.scala
new file mode 100644
index 00000000..fb047675
--- /dev/null
+++ b/main/test/src/mill/main/JavaCompileJarTests.scala
@@ -0,0 +1,66 @@
+package mill.main
+
+import ammonite.ops._
+import mill.util.ScriptTestSuite
+import utest._
+
+object JavaCompileJarTests extends ScriptTestSuite {
+ def workspaceSlug = "java-compile-jar"
+ def scriptSourcePath = pwd / 'main / 'test / 'resources / 'examples / 'javac
+ val tests = Tests{
+ initWorkspace()
+ 'test - {
+ // Basic target evaluation works
+ assert(eval("classFiles"))
+ assert(eval("jar"))
+
+ val classFiles1 = meta("classFiles")
+ val jar1 = meta("jar")
+
+ assert(eval("classFiles"))
+ assert(eval("jar"))
+
+ // Repeated evaluation has the same results
+ val classFiles2 = meta("classFiles")
+ val jar2 = meta("jar")
+
+ assert(
+ jar1 == jar2,
+ classFiles1 == classFiles2
+ )
+
+ // If we update resources, classFiles are unchanged but jar changes
+ for(scalaFile <- ls.rec(workspacePath).filter(_.ext == "txt")){
+ write.append(scalaFile, "\n")
+ }
+
+ assert(eval("classFiles"))
+ assert(eval("jar"))
+
+ val classFiles3 = meta("classFiles")
+ val jar3 = meta("jar")
+
+ assert(
+ jar2 != jar3,
+ classFiles2 == classFiles3
+ )
+
+ // We can intentionally break the code, have the targets break, then
+ // fix the code and have them recover.
+ for(scalaFile <- ls.rec(workspacePath).filter(_.ext == "java")){
+ write.append(scalaFile, "\n}")
+ }
+
+ assert(!eval("classFiles"))
+ assert(!eval("jar"))
+
+ for(scalaFile <- ls.rec(workspacePath).filter(_.ext == "java")){
+ write.over(scalaFile, read(scalaFile).dropRight(2))
+ }
+
+ assert(eval("classFiles"))
+ assert(eval("jar"))
+ }
+ }
+}
+
diff --git a/main/test/src/mill/main/MainTests.scala b/main/test/src/mill/main/MainTests.scala
new file mode 100644
index 00000000..157fff6f
--- /dev/null
+++ b/main/test/src/mill/main/MainTests.scala
@@ -0,0 +1,222 @@
+package mill.main
+
+import mill.define.{Discover, Segment, Task}
+import mill.util.TestGraphs._
+import mill.util.TestEvaluator.implicitDisover
+import utest._
+object MainTests extends TestSuite{
+
+ def check[T <: mill.Module](module: T)(
+ selectorString: String,
+ expected0: Either[String, Seq[T => Task[_]]])
+ (implicit discover: Discover[T])= {
+
+ val expected = expected0.map(_.map(_(module)))
+ val resolved = for{
+ selectors <- mill.util.ParseArgs(Seq(selectorString)).map(_._1.head)
+ val crossSelectors = selectors._2.value.map{case Segment.Cross(x) => x.toList.map(_.toString) case _ => Nil}
+ task <- mill.main.Resolve.resolve(
+ selectors._2.value.toList, module, discover, Nil, crossSelectors.toList, Nil
+ )
+ } yield task
+ assert(resolved == expected)
+ }
+ val tests = Tests{
+ val graphs = new mill.util.TestGraphs()
+ import graphs._
+ 'single - {
+ val check = MainTests.check(singleton) _
+ 'pos - check("single", Right(Seq(_.single)))
+ 'neg1 - check("doesntExist", Left("Cannot resolve task doesntExist"))
+ 'neg2 - check("single.doesntExist", Left("Cannot resolve module single"))
+ 'neg3 - check("", Left("Selector cannot be empty"))
+ }
+ 'nested - {
+ val check = MainTests.check(nestedModule) _
+ 'pos1 - check("single", Right(Seq(_.single)))
+ 'pos2 - check("nested.single", Right(Seq(_.nested.single)))
+ 'pos3 - check("classInstance.single", Right(Seq(_.classInstance.single)))
+ 'neg1 - check("doesntExist", Left("Cannot resolve task doesntExist"))
+ 'neg2 - check("single.doesntExist", Left("Cannot resolve module single"))
+ 'neg3 - check("nested.doesntExist", Left("Cannot resolve task nested.doesntExist"))
+ 'neg4 - check("classInstance.doesntExist", Left("Cannot resolve task classInstance.doesntExist"))
+ 'wildcard - check(
+ "_.single",
+ Right(Seq(
+ _.classInstance.single,
+ _.nested.single
+ ))
+ )
+ 'wildcardNeg - check(
+ "_._.single",
+ Left("Cannot resolve module _")
+ )
+ 'wildcardNeg2 - check(
+ "_._.__",
+ Left("Cannot resolve module _")
+ )
+ 'wildcard2 - check(
+ "__.single",
+ Right(Seq(
+ _.single,
+ _.classInstance.single,
+ _.nested.single
+ ))
+ )
+
+ 'wildcard3 - check(
+ "_.__.single",
+ Right(Seq(
+ _.classInstance.single,
+ _.nested.single
+ ))
+ )
+
+ }
+ 'cross - {
+ 'single - {
+ val check = MainTests.check(singleCross) _
+ 'pos1 - check("cross[210].suffix", Right(Seq(_.cross("210").suffix)))
+ 'pos2 - check("cross[211].suffix", Right(Seq(_.cross("211").suffix)))
+ 'neg1 - check("cross[210].doesntExist", Left("Cannot resolve task cross[210].doesntExist"))
+ 'neg2 - check("cross[doesntExist].doesntExist", Left("Cannot resolve cross cross[doesntExist]"))
+ 'neg2 - check("cross[doesntExist].suffix", Left("Cannot resolve cross cross[doesntExist]"))
+ 'wildcard - check(
+ "cross[_].suffix",
+ Right(Seq(
+ _.cross("210").suffix,
+ _.cross("211").suffix,
+ _.cross("212").suffix
+ ))
+ )
+ 'wildcard2 - check(
+ "cross[__].suffix",
+ Right(Seq(
+ _.cross("210").suffix,
+ _.cross("211").suffix,
+ _.cross("212").suffix
+ ))
+ )
+ }
+ 'double - {
+ val check = MainTests.check(doubleCross) _
+ 'pos1 - check(
+ "cross[210,jvm].suffix",
+ Right(Seq(_.cross("210", "jvm").suffix))
+ )
+ 'pos2 - check(
+ "cross[211,jvm].suffix",
+ Right(Seq(_.cross("211", "jvm").suffix))
+ )
+ 'wildcard - {
+ 'labelNeg - check(
+ "_.suffix",
+ Left("Cannot resolve module _")
+ )
+ 'labelPos - check(
+ "__.suffix",
+ Right(Seq(
+ _.cross("210", "jvm").suffix,
+ _.cross("210", "js").suffix,
+
+ _.cross("211", "jvm").suffix,
+ _.cross("211", "js").suffix,
+
+ _.cross("212", "jvm").suffix,
+ _.cross("212", "js").suffix,
+ _.cross("212", "native").suffix
+ ))
+ )
+ 'first - check(
+ "cross[_,jvm].suffix",
+ Right(Seq(
+ _.cross("210", "jvm").suffix,
+ _.cross("211", "jvm").suffix,
+ _.cross("212", "jvm").suffix
+ ))
+ )
+ 'second - check(
+ "cross[210,_].suffix",
+ Right(Seq(
+ _.cross("210", "jvm").suffix,
+ _.cross("210", "js").suffix
+ ))
+ )
+ 'both - check(
+ "cross[_,_].suffix",
+ Right(Seq(
+ _.cross("210", "jvm").suffix,
+ _.cross("210", "js").suffix,
+
+ _.cross("211", "jvm").suffix,
+ _.cross("211", "js").suffix,
+
+ _.cross("212", "jvm").suffix,
+ _.cross("212", "js").suffix,
+ _.cross("212", "native").suffix
+ ))
+ )
+ 'both2 - check(
+ "cross[__].suffix",
+ Right(Seq(
+ _.cross("210", "jvm").suffix,
+ _.cross("210", "js").suffix,
+
+ _.cross("211", "jvm").suffix,
+ _.cross("211", "js").suffix,
+
+ _.cross("212", "jvm").suffix,
+ _.cross("212", "js").suffix,
+ _.cross("212", "native").suffix
+ ))
+ )
+ }
+ }
+ 'nested - {
+ val check = MainTests.check(nestedCrosses) _
+ 'pos1 - check(
+ "cross[210].cross2[js].suffix",
+ Right(Seq(_.cross("210").cross2("js").suffix))
+ )
+ 'pos2 - check(
+ "cross[211].cross2[jvm].suffix",
+ Right(Seq(_.cross("211").cross2("jvm").suffix))
+ )
+ 'wildcard - {
+ 'first - check(
+ "cross[_].cross2[jvm].suffix",
+ Right(Seq(
+ _.cross("210").cross2("jvm").suffix,
+ _.cross("211").cross2("jvm").suffix,
+ _.cross("212").cross2("jvm").suffix
+ ))
+ )
+ 'second - check(
+ "cross[210].cross2[_].suffix",
+ Right(Seq(
+ _.cross("210").cross2("jvm").suffix,
+ _.cross("210").cross2("js").suffix,
+ _.cross("210").cross2("native").suffix
+ ))
+ )
+ 'both - check(
+ "cross[_].cross2[_].suffix",
+ Right(Seq(
+ _.cross("210").cross2("jvm").suffix,
+ _.cross("210").cross2("js").suffix,
+ _.cross("210").cross2("native").suffix,
+
+ _.cross("211").cross2("jvm").suffix,
+ _.cross("211").cross2("js").suffix,
+ _.cross("211").cross2("native").suffix,
+
+ _.cross("212").cross2("jvm").suffix,
+ _.cross("212").cross2("js").suffix,
+ _.cross("212").cross2("native").suffix
+ ))
+ )
+ }
+ }
+ }
+ }
+}
diff --git a/main/test/src/mill/util/ParseArgsTest.scala b/main/test/src/mill/util/ParseArgsTest.scala
new file mode 100644
index 00000000..9121bca5
--- /dev/null
+++ b/main/test/src/mill/util/ParseArgsTest.scala
@@ -0,0 +1,241 @@
+package mill.util
+
+import mill.define.Segment
+import mill.define.Segment.{Cross, Label}
+import utest._
+
+object ParseArgsTest extends TestSuite {
+
+ val tests = Tests {
+ 'extractSelsAndArgs - {
+ def check(input: Seq[String],
+ expectedSelectors: Seq[String],
+ expectedArgs: Seq[String],
+ expectedIsMulti: Boolean) = {
+ val (selectors, args, isMulti) = ParseArgs.extractSelsAndArgs(input)
+
+ assert(
+ selectors == expectedSelectors,
+ args == expectedArgs,
+ isMulti == expectedIsMulti
+ )
+ }
+
+ 'empty - check(input = Seq.empty,
+ expectedSelectors = Seq.empty,
+ expectedArgs = Seq.empty,
+ expectedIsMulti = false)
+ 'singleSelector - check(
+ input = Seq("core.compile"),
+ expectedSelectors = Seq("core.compile"),
+ expectedArgs = Seq.empty,
+ expectedIsMulti = false
+ )
+ 'singleSelectorWithArgs - check(
+ input = Seq("application.run", "hello", "world"),
+ expectedSelectors = Seq("application.run"),
+ expectedArgs = Seq("hello", "world"),
+ expectedIsMulti = false
+ )
+ 'singleSelectorWithAllInArgs - check(
+ input = Seq("application.run", "hello", "world", "--all"),
+ expectedSelectors = Seq("application.run"),
+ expectedArgs = Seq("hello", "world", "--all"),
+ expectedIsMulti = false
+ )
+ 'multiSelectors - check(
+ input = Seq("--all", "core.jar", "core.docsJar", "core.sourcesJar"),
+ expectedSelectors = Seq("core.jar", "core.docsJar", "core.sourcesJar"),
+ expectedArgs = Seq.empty,
+ expectedIsMulti = true
+ )
+ 'multiSelectorsSeq - check(
+ input = Seq("--seq", "core.jar", "core.docsJar", "core.sourcesJar"),
+ expectedSelectors = Seq("core.jar", "core.docsJar", "core.sourcesJar"),
+ expectedArgs = Seq.empty,
+ expectedIsMulti = true
+ )
+ 'multiSelectorsWithArgs - check(
+ input = Seq("--all",
+ "core.compile",
+ "application.runMain",
+ "--",
+ "Main",
+ "hello",
+ "world"),
+ expectedSelectors = Seq("core.compile", "application.runMain"),
+ expectedArgs = Seq("Main", "hello", "world"),
+ expectedIsMulti = true
+ )
+ 'multiSelectorsWithArgsWithAllInArgs - check(
+ input = Seq("--all",
+ "core.compile",
+ "application.runMain",
+ "--",
+ "Main",
+ "--all",
+ "world"),
+ expectedSelectors = Seq("core.compile", "application.runMain"),
+ expectedArgs = Seq("Main", "--all", "world"),
+ expectedIsMulti = true
+ )
+ }
+ 'expandBraces - {
+ def check(input: String, expectedExpansion: List[String]) = {
+ val Right(expanded) = ParseArgs.expandBraces(input)
+
+ assert(expanded == expectedExpansion)
+ }
+
+ 'expandLeft - check(
+ "{application,core}.compile",
+ List("application.compile", "core.compile")
+ )
+ 'expandRight - check(
+ "application.{jar,docsJar,sourcesJar}",
+ List("application.jar", "application.docsJar", "application.sourcesJar")
+ )
+ 'expandBoth - check(
+ "{core,application}.{jar,docsJar}",
+ List(
+ "core.jar",
+ "core.docsJar",
+ "application.jar",
+ "application.docsJar"
+ )
+ )
+ 'expandNested - {
+ check("{hello,world.{cow,moo}}",
+ List("hello", "world.cow", "world.moo"))
+ check("{a,b{c,d}}", List("a", "bc", "bd"))
+ check("{a,b,{c,d}}", List("a", "b", "c", "d"))
+ check("{a,b{c,d{e,f}}}", List("a", "bc", "bde", "bdf"))
+ check("{a{b,c},d}", List("ab", "ac", "d"))
+ check("{a,{b,c}d}", List("a", "bd", "cd"))
+ check("{a{b,c},d{e,f}}", List("ab", "ac", "de", "df"))
+ check("{a,b{c,d},e{f,g}}", List("a", "bc", "bd", "ef", "eg"))
+ }
+ 'expandMixed - check(
+ "{a,b}.{c}.{}.e",
+ List("a.{c}.{}.e", "b.{c}.{}.e")
+ )
+ 'malformed - {
+ val malformed = Seq("core.{compile", "core.{compile,test]")
+
+ malformed.foreach { m =>
+ val Left(error) = ParseArgs.expandBraces(m)
+ assert(error.contains("Parsing exception"))
+ }
+ }
+ 'dontExpand - {
+ check("core.compile", List("core.compile"))
+ check("{}.compile", List("{}.compile"))
+ check("{core}.compile", List("{core}.compile"))
+ }
+ 'keepUnknownSymbols - {
+ check("{a,b}.e<>", List("a.e<>", "b.e<>"))
+ check("a[99]&&", List("a[99]&&"))
+ check(
+ "{a,b}.<%%>.{c,d}",
+ List("a.<%%>.c", "a.<%%>.d", "b.<%%>.c", "b.<%%>.d")
+ )
+ }
+ }
+
+ 'apply - {
+ def check(input: Seq[String],
+ expectedSelectors: List[(Option[List[Segment]], List[Segment])],
+ expectedArgs: Seq[String]) = {
+ val Right((selectors0, args)) = ParseArgs(input)
+
+ val selectors = selectors0.map{
+ case (Some(v1), v2) => (Some(v1.value), v2.value)
+ case (None, v2) => (None, v2.value)
+ }
+ assert(
+ selectors == expectedSelectors,
+ args == expectedArgs
+ )
+ }
+
+ 'rejectEmpty {
+ assert(ParseArgs(Seq.empty) == Left("Selector cannot be empty"))
+ }
+ 'singleSelector - check(
+ input = Seq("core.compile"),
+ expectedSelectors = List(
+ None -> List(Label("core"), Label("compile"))
+ ),
+ expectedArgs = Seq.empty
+ )
+ 'externalSelector - check(
+ input = Seq("foo.bar/core.compile"),
+ expectedSelectors = List(
+ Some(List(Label("foo"), Label("bar"))) -> List(Label("core"), Label("compile"))
+ ),
+ expectedArgs = Seq.empty
+ )
+ 'singleSelectorWithArgs - check(
+ input = Seq("application.run", "hello", "world"),
+ expectedSelectors = List(
+ None -> List(Label("application"), Label("run"))
+ ),
+ expectedArgs = Seq("hello", "world")
+ )
+ 'singleSelectorWithCross - check(
+ input = Seq("bridges[2.12.4,jvm].compile"),
+ expectedSelectors = List(
+ None -> List(Label("bridges"), Cross(Seq("2.12.4", "jvm")), Label("compile"))
+ ),
+ expectedArgs = Seq.empty
+ )
+ 'multiSelectorsBraceExpansion - check(
+ input = Seq("--all", "{core,application}.compile"),
+ expectedSelectors = List(
+ None -> List(Label("core"), Label("compile")),
+ None -> List(Label("application"), Label("compile"))
+ ),
+ expectedArgs = Seq.empty
+ )
+ 'multiSelectorsBraceExpansionWithArgs - check(
+ input = Seq("--all", "{core,application}.run", "--", "hello", "world"),
+ expectedSelectors = List(
+ None -> List(Label("core"), Label("run")),
+ None -> List(Label("application"), Label("run"))
+ ),
+ expectedArgs = Seq("hello", "world")
+ )
+ 'multiSelectorsBraceExpansionWithCross - check(
+ input = Seq("--all", "bridges[2.12.4,jvm].{test,jar}"),
+ expectedSelectors = List(
+ None -> List(Label("bridges"), Cross(Seq("2.12.4", "jvm")), Label("test")),
+ None -> List(Label("bridges"), Cross(Seq("2.12.4", "jvm")), Label("jar"))
+ ),
+ expectedArgs = Seq.empty
+ )
+ 'multiSelectorsBraceExpansionInsideCross - check(
+ input = Seq("--all", "bridges[{2.11.11,2.11.8}].jar"),
+ expectedSelectors = List(
+ None -> List(Label("bridges"), Cross(Seq("2.11.11")), Label("jar")),
+ None -> List(Label("bridges"), Cross(Seq("2.11.8")), Label("jar"))
+ ),
+ expectedArgs = Seq.empty
+ )
+ 'multiSelectorsBraceExpansionWithoutAll - {
+ assert(
+ ParseArgs(Seq("{core,application}.compile")) == Left(
+ "Please use --all flag to run multiple tasks")
+ )
+ }
+ 'multiSelectorsWithoutAllAsSingle - check(
+ // this is how it works when we pass multiple tasks without --all flag
+ input = Seq("core.compile", "application.compile"),
+ expectedSelectors = List(
+ None -> List(Label("core"), Label("compile"))
+ ),
+ expectedArgs = Seq("application.compile")
+ )
+ }
+ }
+
+}
diff --git a/main/test/src/mill/util/ScriptTestSuite.scala b/main/test/src/mill/util/ScriptTestSuite.scala
new file mode 100644
index 00000000..916d3af6
--- /dev/null
+++ b/main/test/src/mill/util/ScriptTestSuite.scala
@@ -0,0 +1,38 @@
+package mill.util
+
+import java.io.{ByteArrayInputStream, ByteArrayOutputStream, PrintStream}
+
+import ammonite.ops._
+import mill.util.ParseArgs
+import utest._
+
+abstract class ScriptTestSuite extends TestSuite{
+ def workspaceSlug: String
+ def scriptSourcePath: Path
+
+ val workspacePath = pwd / 'target / 'workspace / workspaceSlug
+ val stdOutErr = new PrintStream(new ByteArrayOutputStream())
+// val stdOutErr = new PrintStream(System.out)
+ val stdIn = new ByteArrayInputStream(Array())
+ val runner = new mill.main.MainRunner(
+ ammonite.main.Cli.Config(wd = workspacePath), false,
+ stdOutErr, stdOutErr, stdIn
+ )
+ def eval(s: String*) = runner.runScript(workspacePath / "build.sc", s.toList)
+ def meta(s: String) = {
+ val (List(selector), args) = ParseArgs.apply(Seq(s)).right.get
+
+ read(workspacePath / "out" / selector._2.value.flatMap(_.pathSegments) / "meta.json")
+ }
+
+
+ def initWorkspace() = {
+ rm(workspacePath)
+ mkdir(workspacePath / up)
+ // The unzipped git repo snapshots we get from github come with a
+ // wrapper-folder inside the zip file, so copy the wrapper folder to the
+ // destination instead of the folder containing the wrapper.
+
+ cp(scriptSourcePath, workspacePath)
+ }
+}
diff --git a/main/test/src/mill/util/TestEvaluator.scala b/main/test/src/mill/util/TestEvaluator.scala
new file mode 100644
index 00000000..a5be0488
--- /dev/null
+++ b/main/test/src/mill/util/TestEvaluator.scala
@@ -0,0 +1,83 @@
+package mill.util
+
+import ammonite.ops.{Path, pwd}
+import mill.define.Discover.applyImpl
+import mill.define.{Discover, Input, Target, Task}
+import mill.eval.Result.OuterStack
+import mill.eval.{Evaluator, Result}
+import mill.util.Strict.Agg
+import utest.assert
+import utest.framework.TestPath
+
+import language.experimental.macros
+object TestEvaluator{
+ implicit def implicitDisover[T]: Discover[T] = macro applyImpl[T]
+ val externalOutPath = pwd / 'target / 'external
+
+
+ def static[T <: TestUtil.BaseModule](module: T)
+ (implicit discover: Discover[T],
+ fullName: sourcecode.FullName) = {
+ new TestEvaluator[T](module)(discover, fullName, TestPath(Nil))
+ }
+}
+
+class TestEvaluator[T <: TestUtil.BaseModule](module: T)
+ (implicit discover: Discover[T],
+ fullName: sourcecode.FullName,
+ tp: TestPath){
+ val outPath = TestUtil.getOutPath()
+
+ val logger = DummyLogger
+// val logger = new PrintLogger(true, ammonite.util.Colors.Default, System.out, System.out, System.err)
+ val evaluator = new Evaluator(outPath, TestEvaluator.externalOutPath, module, discover, logger)
+
+ def apply[T](t: Task[T]): Either[Result.Failing[T], (T, Int)] = {
+ val evaluated = evaluator.evaluate(Agg(t))
+
+ if (evaluated.failing.keyCount == 0) {
+ Right(
+ Tuple2(
+ evaluated.rawValues.head.asInstanceOf[Result.Success[T]].value,
+ evaluated.evaluated.collect {
+ case t: Target[_]
+ if module.millInternal.targets.contains(t)
+ && !t.isInstanceOf[Input[_]]
+ && !t.ctx.external => t
+ case t: mill.define.Command[_] => t
+ }.size
+ ))
+ } else {
+ Left(
+ evaluated.failing.lookupKey(evaluated.failing.keys().next).items.next()
+ .asInstanceOf[Result.Failing[T]]
+ )
+ }
+ }
+
+ def fail(target: Target[_], expectedFailCount: Int, expectedRawValues: Seq[Result[_]]) = {
+
+ val res = evaluator.evaluate(Agg(target))
+
+ val cleaned = res.rawValues.map{
+ case Result.Exception(ex, _) => Result.Exception(ex, new OuterStack(Nil))
+ case x => x
+ }
+
+ assert(
+ cleaned == expectedRawValues,
+ res.failing.keyCount == expectedFailCount
+ )
+
+ }
+
+ def check(targets: Agg[Task[_]], expected: Agg[Task[_]]) = {
+ val evaluated = evaluator.evaluate(targets)
+ .evaluated
+ .flatMap(_.asTarget)
+ .filter(module.millInternal.targets.contains)
+ .filter(!_.isInstanceOf[Input[_]])
+ assert(evaluated == expected)
+ }
+
+}
diff --git a/main/test/src/mill/util/TestGraphs.scala b/main/test/src/mill/util/TestGraphs.scala
new file mode 100644
index 00000000..581d5e0a
--- /dev/null
+++ b/main/test/src/mill/util/TestGraphs.scala
@@ -0,0 +1,242 @@
+package mill.util
+import TestUtil.test
+import mill.define.Cross
+import mill.{Module, T}
+
+/**
+ * Example dependency graphs for us to use in our test suite.
+ *
+ * The graphs using `test()` live in the `class` and need to be instantiated
+ * every time you use them, because they are mutable (you can poke at the
+ * `test`'s `counter`/`failure`/`exception` fields to test various graph
+ * evaluation scenarios.
+ *
+ * The immutable graphs, used for testing discovery & target resolution,
+ * live in the companion object.
+ */
+class TestGraphs(){
+ // single
+ object singleton extends TestUtil.BaseModule {
+ val single = test()
+ }
+
+ // up---down
+ object pair extends TestUtil.BaseModule{
+ val up = test()
+ val down = test(up)
+ }
+
+ // up---o---down
+ object anonTriple extends TestUtil.BaseModule {
+ val up = test()
+ val down = test(test.anon(up))
+ }
+
+ // left
+ // / \
+ // up down
+ // \ /
+ // right
+ object diamond extends TestUtil.BaseModule {
+ val up = test()
+ val left = test(up)
+ val right = test(up)
+ val down = test(left, right)
+ }
+
+ // o
+ // / \
+ // up down
+ // \ /
+ // o
+ object anonDiamond extends TestUtil.BaseModule {
+ val up = test()
+ val down = test(test.anon(up), test.anon(up))
+ }
+
+ object defCachedDiamond extends TestUtil.BaseModule {
+ def up = T{ test() }
+ def left = T{ test(up) }
+ def right = T{ test(up) }
+ def down = T{ test(left, right) }
+ }
+
+
+ object borkedCachedDiamond2 extends TestUtil.BaseModule {
+ def up = test()
+ def left = test(up)
+ def right = test(up)
+ def down = test(left, right)
+ }
+
+ object borkedCachedDiamond3 extends TestUtil.BaseModule {
+ def up = test()
+ def left = test(up)
+ def right = test(up)
+ def down = test(left, right)
+ }
+
+ // o g-----o
+ // \ \ \
+ // o o h-----I---o
+ // \ / \ / \ / \ \
+ // A---c--o E o-o \ \
+ // / \ / \ / \ o---J
+ // o d o--o o / /
+ // \ / \ / /
+ // o o---F---o
+ // / /
+ // o--B o
+ object bigSingleTerminal extends TestUtil.BaseModule {
+ val a = test(test.anon(), test.anon())
+ val b = test(test.anon())
+ val e = {
+ val c = test.anon(a)
+ val d = test.anon(a)
+ test(
+ test.anon(test.anon(), test.anon(c)),
+ test.anon(test.anon(c, test.anon(d, b)))
+ )
+ }
+ val f = test(test.anon(test.anon(), test.anon(e)))
+
+ val i = {
+ val g = test.anon()
+ val h = test.anon(g, e)
+ test(test.anon(g), test.anon(test.anon(h)))
+ }
+ val j = test(test.anon(i), test.anon(i, f), test.anon(f))
+ }
+ // _ left _
+ // / \
+ // task1 -------- right
+ // _/
+ // change - task2
+ object separateGroups extends TestUtil.BaseModule {
+ val task1 = T.task{ 1 }
+ def left = T{ task1() }
+ val change = test()
+ val task2 = T.task{ change() }
+ def right = T{ task1() + task2() + left() + 1 }
+
+ }
+}
+
+
+object TestGraphs{
+ // _ left _
+ // / \
+ // task -------- right
+ object triangleTask extends TestUtil.BaseModule {
+ val task = T.task{ 1 }
+ def left = T{ task() }
+ def right = T{ task() + left() + 1 }
+ }
+
+
+ // _ left
+ // /
+ // task -------- right
+ object multiTerminalGroup extends TestUtil.BaseModule {
+ val task = T.task{ 1 }
+ def left = T{ task() }
+ def right = T{ task() }
+ }
+
+ // _ left _____________
+ // / \ \
+ // task1 -------- right ----- task2
+ object multiTerminalBoundary extends TestUtil.BaseModule {
+ val task1 = T.task{ 1 }
+ def left = T{ task1() }
+ def right = T{ task1() + left() + 1 }
+ val task2 = T.task{ left() + right() }
+ }
+
+
+ trait CanNest extends Module{
+ def single = T{ 1 }
+ def invisible: Any = T{ 2 }
+ def invisible2: mill.define.Task[Int] = T{ 3 }
+ def invisible3: mill.define.Task[_] = T{ 4 }
+ }
+ object nestedModule extends TestUtil.BaseModule {
+ def single = T{ 5 }
+ def invisible: Any = T{ 6 }
+ object nested extends Module{
+ def single = T{ 7 }
+ def invisible: Any = T{ 8 }
+
+ }
+ object classInstance extends CanNest
+
+ }
+
+ trait BaseModule extends Module {
+ def foo = T{ Seq("base") }
+ def cmd(i: Int) = T.command{ Seq("base" + i) }
+ }
+
+ object canOverrideSuper extends TestUtil.BaseModule with BaseModule {
+ override def foo = T{ super.foo() ++ Seq("object") }
+ override def cmd(i: Int) = T.command{ super.cmd(i)() ++ Seq("object" + i) }
+ }
+
+ trait TraitWithModule extends Module{ outer =>
+ object TraitModule extends Module{
+ def testFramework = T{ "mill.UTestFramework" }
+ def test() = T.command{ ()/*donothing*/ }
+ }
+ }
+
+
+ // Make sure nested objects inherited from traits work
+ object TraitWithModuleObject extends TestUtil.BaseModule with TraitWithModule
+
+
+ object singleCross extends TestUtil.BaseModule {
+ object cross extends mill.Cross[Cross]("210", "211", "212")
+ class Cross(scalaVersion: String) extends Module{
+ def suffix = T{ scalaVersion }
+ }
+ }
+ object crossResolved extends TestUtil.BaseModule {
+ trait MyModule extends Module{
+ def crossVersion: String
+ implicit object resolver extends mill.define.Cross.Resolver[MyModule]{
+ def resolve[V <: MyModule](c: Cross[V]): V = c.itemMap(List(crossVersion))
+ }
+ }
+
+ object foo extends mill.Cross[FooModule]("2.10", "2.11", "2.12")
+ class FooModule(val crossVersion: String) extends MyModule{
+ def suffix = T{ crossVersion }
+ }
+
+ object bar extends mill.Cross[BarModule]("2.10", "2.11", "2.12")
+ class BarModule(val crossVersion: String) extends MyModule{
+ def longSuffix = T{ "_" + foo().suffix() }
+ }
+ }
+ object doubleCross extends TestUtil.BaseModule {
+ val crossMatrix = for{
+ scalaVersion <- Seq("210", "211", "212")
+ platform <- Seq("jvm", "js", "native")
+ if !(platform == "native" && scalaVersion != "212")
+ } yield (scalaVersion, platform)
+ object cross extends mill.Cross[Cross](crossMatrix:_*)
+ class Cross(scalaVersion: String, platform: String) extends Module{
+ def suffix = T{ scalaVersion + "_" + platform }
+ }
+ }
+
+ object nestedCrosses extends TestUtil.BaseModule {
+ object cross extends mill.Cross[Cross]("210", "211", "212")
+ class Cross(scalaVersion: String) extends mill.Module{
+ object cross2 extends mill.Cross[Cross]("jvm", "js", "native")
+ class Cross(platform: String) extends mill.Module{
+ def suffix = T{ scalaVersion + "_" + platform }
+ }
+ }
+ }
+}
diff --git a/main/test/src/mill/util/TestUtil.scala b/main/test/src/mill/util/TestUtil.scala
new file mode 100644
index 00000000..b30d5d51
--- /dev/null
+++ b/main/test/src/mill/util/TestUtil.scala
@@ -0,0 +1,81 @@
+package mill.util
+
+import mill.util.Router.Overrides
+import ammonite.ops.pwd
+import mill.define._
+import mill.eval.Result
+import mill.eval.Result.OuterStack
+import utest.assert
+import mill.util.Strict.Agg
+import utest.framework.TestPath
+
+import scala.collection.mutable
+
+object TestUtil {
+ def getOutPath()(implicit fullName: sourcecode.FullName,
+ tp: TestPath) = {
+ pwd / 'target / 'workspace / (fullName.value.split('.') ++ tp.value)
+ }
+ def getOutPathStatic()(implicit fullName: sourcecode.FullName) = {
+ pwd / 'target / 'workspace / fullName.value.split('.')
+ }
+
+ def getSrcPathStatic()(implicit fullName: sourcecode.FullName) = {
+ pwd / 'target / 'worksources / fullName.value.split('.')
+ }
+ def getSrcPathBase() = {
+ pwd / 'target / 'worksources
+ }
+
+ class BaseModule(implicit millModuleEnclosing0: sourcecode.Enclosing,
+ millModuleLine0: sourcecode.Line,
+ millName0: sourcecode.Name,
+ overrides: Overrides)
+ extends mill.define.BaseModule(getSrcPathBase() / millModuleEnclosing0.value.split("\\.| |#")){
+ def millDiscover: Discover[this.type] = Discover[this.type]
+ }
+
+ object test{
+
+ def anon(inputs: Task[Int]*) = new Test(inputs)
+ def apply(inputs: Task[Int]*)
+ (implicit ctx: mill.define.Ctx)= {
+ new TestTarget(inputs, pure = inputs.nonEmpty)
+ }
+ }
+
+ class Test(val inputs: Seq[Task[Int]]) extends Task[Int]{
+ var counter = 0
+ var failure = Option.empty[String]
+ var exception = Option.empty[Throwable]
+ override def evaluate(args: Ctx) = {
+ failure.map(Result.Failure(_)) orElse
+ exception.map(Result.Exception(_, new OuterStack(Nil))) getOrElse
+ Result.Success(counter + args.args.map(_.asInstanceOf[Int]).sum)
+ }
+ override def sideHash = counter + failure.hashCode() + exception.hashCode()
+ }
+ /**
+ * A dummy target that takes any number of inputs, and whose output can be
+ * controlled externally, so you can construct arbitrary dataflow graphs and
+ * test how changes propagate.
+ */
+ class TestTarget(inputs: Seq[Task[Int]],
+ val pure: Boolean)
+ (implicit ctx0: mill.define.Ctx)
+ extends Test(inputs) with Target[Int]{
+ val ctx = ctx0.copy(segments = ctx0.segments ++ Seq(ctx0.segment))
+ val readWrite = upickle.default.IntRW
+
+
+ }
+ def checkTopological(targets: Agg[Task[_]]) = {
+ val seen = mutable.Set.empty[Task[_]]
+ for(t <- targets.indexed.reverseIterator){
+ seen.add(t)
+ for(upstream <- t.inputs){
+ assert(!seen(upstream))
+ }
+ }
+ }
+}