summaryrefslogtreecommitdiff
path: root/main/test
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/test
parent90d0a3388d280554eaa51371f666d2f7a965a8af (diff)
downloadmill-8ddd2fa054bc8639c28db2e95b7903e2954fdb7d.tar.gz
mill-8ddd2fa054bc8639c28db2e95b7903e2954fdb7d.tar.bz2
mill-8ddd2fa054bc8639c28db2e95b7903e2954fdb7d.zip
.
Diffstat (limited to 'main/test')
-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
26 files changed, 2499 insertions, 0 deletions
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))
+ }
+ }
+ }
+}