summaryrefslogtreecommitdiff
path: root/core/test
diff options
context:
space:
mode:
Diffstat (limited to 'core/test')
-rw-r--r--core/test/resources/examples/javac/resources/hello.txt1
-rw-r--r--core/test/resources/examples/javac/src/Bar.java4
-rw-r--r--core/test/resources/examples/javac/src/Foo.java7
-rw-r--r--core/test/src/mill/TestMain.scala6
-rw-r--r--core/test/src/mill/UTestFramework.scala11
-rw-r--r--core/test/src/mill/define/ApplicativeTests.scala125
-rw-r--r--core/test/src/mill/define/BasePathTests.scala50
-rw-r--r--core/test/src/mill/define/CacherTests.scala76
-rw-r--r--core/test/src/mill/define/DiscoverTests.scala60
-rw-r--r--core/test/src/mill/define/GraphTests.scala199
-rw-r--r--core/test/src/mill/define/MacroErrorTests.scala83
-rw-r--r--core/test/src/mill/eval/EvaluationTests.scala270
-rw-r--r--core/test/src/mill/eval/FailureTests.scala109
-rw-r--r--core/test/src/mill/eval/JavaCompileJarTests.scala177
-rw-r--r--core/test/src/mill/eval/TarjanTests.scala91
-rw-r--r--core/test/src/mill/main/MainTests.scala81
-rw-r--r--core/test/src/mill/main/ParseArgsTest.scala229
-rw-r--r--core/test/src/mill/util/TestEvaluator.scala30
-rw-r--r--core/test/src/mill/util/TestGraphs.scala222
-rw-r--r--core/test/src/mill/util/TestUtil.scala60
20 files changed, 1891 insertions, 0 deletions
diff --git a/core/test/resources/examples/javac/resources/hello.txt b/core/test/resources/examples/javac/resources/hello.txt
new file mode 100644
index 00000000..5e1c309d
--- /dev/null
+++ b/core/test/resources/examples/javac/resources/hello.txt
@@ -0,0 +1 @@
+Hello World \ No newline at end of file
diff --git a/core/test/resources/examples/javac/src/Bar.java b/core/test/resources/examples/javac/src/Bar.java
new file mode 100644
index 00000000..4e30c89b
--- /dev/null
+++ b/core/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/core/test/resources/examples/javac/src/Foo.java b/core/test/resources/examples/javac/src/Foo.java
new file mode 100644
index 00000000..e694f9fa
--- /dev/null
+++ b/core/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/core/test/src/mill/TestMain.scala b/core/test/src/mill/TestMain.scala
new file mode 100644
index 00000000..80e7e627
--- /dev/null
+++ b/core/test/src/mill/TestMain.scala
@@ -0,0 +1,6 @@
+package mill
+
+object TestMain {
+ def main(args: Array[String]): Unit = {
+ }
+}
diff --git a/core/test/src/mill/UTestFramework.scala b/core/test/src/mill/UTestFramework.scala
new file mode 100644
index 00000000..6c0d5191
--- /dev/null
+++ b/core/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/core/test/src/mill/define/ApplicativeTests.scala b/core/test/src/mill/define/ApplicativeTests.scala
new file mode 100644
index 00000000..72b715bb
--- /dev/null
+++ b/core/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/core/test/src/mill/define/BasePathTests.scala b/core/test/src/mill/define/BasePathTests.scala
new file mode 100644
index 00000000..a0be4762
--- /dev/null
+++ b/core/test/src/mill/define/BasePathTests.scala
@@ -0,0 +1,50 @@
+package mill.define
+
+import mill.util.TestGraphs
+import utest._
+import ammonite.ops._
+object BasePathTests extends TestSuite{
+ val testGraphs = new TestGraphs
+ val tests = Tests{
+ def check(m: Module, segments: String*) = {
+ val remaining = m.basePath.relativeTo(pwd).segments.drop(1)
+ assert(remaining == segments)
+ }
+ 'singleton - {
+ check(testGraphs.singleton)
+ }
+ 'separateGroups - {
+ check(TestGraphs.triangleTask)
+ }
+ '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"
+ )
+ }
+
+ }
+}
+
diff --git a/core/test/src/mill/define/CacherTests.scala b/core/test/src/mill/define/CacherTests.scala
new file mode 100644
index 00000000..eb981d46
--- /dev/null
+++ b/core/test/src/mill/define/CacherTests.scala
@@ -0,0 +1,76 @@
+package mill.define
+
+import mill.eval.Evaluator
+import mill.util.{DummyLogger, TestUtil}
+import mill.util.Strict.Agg
+import mill.T
+import mill.eval.Result.Success
+import utest._
+import utest.framework.TestPath
+
+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[V](mapping: mill.Module, v: Task[V])(implicit tp: TestPath) = {
+ val workspace = ammonite.ops.pwd / 'target / 'workspace / tp.value
+ val evaluator = new Evaluator(workspace, ammonite.ops.pwd, mapping, DummyLogger)
+ evaluator.evaluate(Agg(v)).values(0)
+ }
+
+ 'simpleDefIsCached - assert(
+ Base.value eq Base.value,
+ eval(Base, Base.value) == 1
+ )
+
+ 'resultDefIsCached - assert(
+ Base.result eq Base.result,
+ eval(Base, Base.result) == 1
+ )
+
+
+ 'overridingDefIsAlsoCached - assert(
+ eval(Middle, Middle.value) == 3,
+ Middle.value eq Middle.value
+ )
+
+ 'overridenDefRemainsAvailable - assert(
+ eval(Middle, Middle.overriden) == 1
+ )
+
+
+ 'multipleOverridesWork- assert(
+ eval(Terminal, Terminal.value) == 7,
+ 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/core/test/src/mill/define/DiscoverTests.scala b/core/test/src/mill/define/DiscoverTests.scala
new file mode 100644
index 00000000..7621169a
--- /dev/null
+++ b/core/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/core/test/src/mill/define/GraphTests.scala b/core/test/src/mill/define/GraphTests.scala
new file mode 100644
index 00000000..7e6680be
--- /dev/null
+++ b/core/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/core/test/src/mill/define/MacroErrorTests.scala b/core/test/src/mill/define/MacroErrorTests.scala
new file mode 100644
index 00000000..a389feaa
--- /dev/null
+++ b/core/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/core/test/src/mill/eval/EvaluationTests.scala b/core/test/src/mill/eval/EvaluationTests.scala
new file mode 100644
index 00000000..0d14988a
--- /dev/null
+++ b/core/test/src/mill/eval/EvaluationTests.scala
@@ -0,0 +1,270 @@
+package mill.eval
+
+
+import mill.util.TestUtil.{Test, test}
+import mill.define.{Graph, Target, Task}
+import mill.{Module, T}
+import mill.util.{DummyLogger, TestGraphs, TestUtil}
+import mill.util.Strict.Agg
+import utest._
+import utest.framework.TestPath
+
+object EvaluationTests extends TestSuite{
+ class Checker(module: mill.Module)(implicit tp: TestPath) {
+ val workspace = ammonite.ops.pwd / 'target / 'workspace / tp.value
+ ammonite.ops.rm(ammonite.ops.Path(workspace, ammonite.ops.pwd))
+ // Make sure data is persisted even if we re-create the evaluator each time
+ def evaluator = new Evaluator(workspace, ammonite.ops.pwd, module, DummyLogger)
+
+ 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{
+ val graphs = new 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 - {
+ import canOverrideSuper._
+
+ val checker = new Checker(canOverrideSuper)
+ checker(foo, Seq("base", "object"), Agg(foo), extraEvaled = -1)
+ }
+
+ '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/core/test/src/mill/eval/FailureTests.scala b/core/test/src/mill/eval/FailureTests.scala
new file mode 100644
index 00000000..12ed345d
--- /dev/null
+++ b/core/test/src/mill/eval/FailureTests.scala
@@ -0,0 +1,109 @@
+package mill.eval
+
+import mill.define.Target
+import mill.util.DummyLogger
+import mill.util.Strict.Agg
+import utest._
+import utest.framework.TestPath
+
+object FailureTests extends TestSuite{
+
+ def workspace(implicit tp: TestPath) = {
+ ammonite.ops.pwd / 'target / 'workspace / 'failure / implicitly[TestPath].value
+ }
+ class Checker(module: mill.Module)(implicit tp: TestPath){
+
+ val evaluator = new Evaluator(workspace, ammonite.ops.pwd, module, DummyLogger)
+
+ def apply(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, Nil)
+ case x => x
+ }
+
+ assert(
+ cleaned == expectedRawValues,
+ res.failing.keyCount == expectedFailCount
+ )
+
+ }
+ }
+ val tests = Tests{
+ val graphs = new mill.util.TestGraphs()
+ import graphs._
+
+ 'evaluateSingle - {
+ ammonite.ops.rm(ammonite.ops.Path(workspace, ammonite.ops.pwd))
+ val check = new Checker(singleton)
+ check(
+ target = singleton.single,
+ expectedFailCount = 0,
+ expectedRawValues = Seq(Result.Success(0))
+ )
+
+ singleton.single.failure = Some("lols")
+
+ check(
+ target = singleton.single,
+ expectedFailCount = 1,
+ expectedRawValues = Seq(Result.Failure("lols"))
+ )
+
+ singleton.single.failure = None
+
+ check(
+ target = singleton.single,
+ expectedFailCount = 0,
+ expectedRawValues = Seq(Result.Success(0))
+ )
+
+
+ val ex = new IndexOutOfBoundsException()
+ singleton.single.exception = Some(ex)
+
+
+ check(
+ target = singleton.single,
+ expectedFailCount = 1,
+ expectedRawValues = Seq(Result.Exception(ex, Nil))
+ )
+ }
+ 'evaluatePair - {
+ ammonite.ops.rm(ammonite.ops.Path(workspace, ammonite.ops.pwd))
+ val check = new Checker(pair)
+ check(
+ pair.down,
+ expectedFailCount = 0,
+ expectedRawValues = Seq(Result.Success(0))
+ )
+
+ pair.up.failure = Some("lols")
+
+ check(
+ pair.down,
+ expectedFailCount = 1,
+ expectedRawValues = Seq(Result.Skipped)
+ )
+
+ pair.up.failure = None
+
+ check(
+ pair.down,
+ expectedFailCount = 0,
+ expectedRawValues = Seq(Result.Success(0))
+ )
+
+ pair.up.exception = Some(new IndexOutOfBoundsException())
+
+ check(
+ pair.down,
+ expectedFailCount = 1,
+ expectedRawValues = Seq(Result.Skipped)
+ )
+ }
+ }
+}
+
diff --git a/core/test/src/mill/eval/JavaCompileJarTests.scala b/core/test/src/mill/eval/JavaCompileJarTests.scala
new file mode 100644
index 00000000..5dd9cae1
--- /dev/null
+++ b/core/test/src/mill/eval/JavaCompileJarTests.scala
@@ -0,0 +1,177 @@
+package mill.eval
+
+import ammonite.ops.ImplicitWd._
+import ammonite.ops._
+import mill.define.{Input, Target, Task}
+import mill.modules.Jvm
+import mill.util.Ctx.DestCtx
+import mill.{Module, T}
+import mill.util.{DummyLogger, Loose, TestUtil}
+import mill.util.Strict.Agg
+import utest._
+import mill._
+
+object JavaCompileJarTests extends TestSuite{
+ def compileAll(sources: Seq[PathRef])(implicit ctx: DestCtx) = {
+ 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 workspacePath = pwd / 'target / 'workspace / 'javac
+ val javacSrcPath = pwd / 'core / 'test / 'resources / 'examples / 'javac
+ val javacDestPath = workspacePath / 'src
+
+ mkdir(pwd / 'target / 'workspace / 'javac)
+ 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.source{ sourceRootPath }
+ def resourceRoot = T.source{ resourceRootPath }
+ def allSources = T{ ls.rec(sourceRoot().path).map(PathRef(_)) }
+ def classFiles = T{ compileAll(allSources()) }
+ def jar = T{ Jvm.createJar(Loose.Agg(resourceRoot().path, classFiles().path)) }
+
+ def run(mainClsName: String) = T.command{
+ %%('java, "-cp", classFiles().path, mainClsName)
+ }
+ }
+
+ import Build._
+
+ def eval[T](t: Task[T]) = {
+ val evaluator = new Evaluator(workspacePath, pwd, Build, DummyLogger)
+ val evaluated = evaluator.evaluate(Agg(t))
+
+ if (evaluated.failing.keyCount == 0){
+ Right(Tuple2(
+ evaluated.rawValues(0).asInstanceOf[Result.Success[T]].value,
+ evaluated.evaluated.collect{
+ case t: Target[_] if Build.millInternal.targets.contains(t) => t
+ case t: mill.define.Command[_] => t
+ }.size
+ ))
+ }else{
+ Left(evaluated.failing.lookupKey(evaluated.failing.keys().next).items.next())
+ }
+
+ }
+ def check(targets: Agg[Task[_]], expected: Agg[Task[_]]) = {
+ val evaluator = new Evaluator(workspacePath, pwd, Build, DummyLogger)
+
+ val evaluated = evaluator.evaluate(targets)
+ .evaluated
+ .flatMap(_.asTarget)
+ .filter(Build.millInternal.targets.contains)
+ .filter(!_.isInstanceOf[Input[_]])
+ assert(evaluated == 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))
+
+ // 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", workspacePath/'jar/'dest)(workspacePath).out.string
+ val expectedJarContents =
+ """META-INF/MANIFEST.MF
+ |hello.txt
+ |test/Bar.class
+ |test/BarThree.class
+ |test/BarTwo.class
+ |test/Foo.class
+ |test/FooTwo.class
+ |""".stripMargin
+ assert(jarContents == expectedJarContents)
+
+ val executed = %%('java, "-cp", workspacePath/'jar/'dest, "test.Foo")(workspacePath).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 == 2
+ )
+ }
+
+ 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 == 4
+ )
+ val Right((runOutput3, evalCount3)) = eval(Build.run("test.BarFour"))
+ assert(
+ runOutput3.out.string == "New Cls!\n",
+ evalCount3 == 2
+ )
+ }
+ }
+}
diff --git a/core/test/src/mill/eval/TarjanTests.scala b/core/test/src/mill/eval/TarjanTests.scala
new file mode 100644
index 00000000..2f9d0a4d
--- /dev/null
+++ b/core/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/core/test/src/mill/main/MainTests.scala b/core/test/src/mill/main/MainTests.scala
new file mode 100644
index 00000000..c2499835
--- /dev/null
+++ b/core/test/src/mill/main/MainTests.scala
@@ -0,0 +1,81 @@
+package mill.main
+
+import mill.Module
+import mill.define.{Discover, Segment, Task}
+import mill.util.TestGraphs._
+import mill.util.TestUtil.test
+import utest._
+object MainTests extends TestSuite{
+ def check[T](module: mill.Module,
+ discover: Discover,
+ selectorString: String,
+ expected: Either[String, Task[_]]) = {
+
+ val resolved = for{
+ selectors <- mill.main.ParseArgs(Seq(selectorString)).map(_._1.head)
+ val crossSelectors = selectors.map{case Segment.Cross(x) => x.toList.map(_.toString) case _ => Nil}
+ task <- mill.main.Resolve.resolve(selectors, module, discover, Nil, crossSelectors, Nil)
+ } yield task
+ assert(resolved == expected)
+ }
+ val tests = Tests{
+ val graphs = new mill.util.TestGraphs()
+ import graphs._
+ 'single - {
+ 'pos - check(singleton, Discover[singleton.type], "single", Right(singleton.single))
+ 'neg1 - check(singleton, Discover[singleton.type], "doesntExist", Left("Cannot resolve task doesntExist"))
+ 'neg2 - check(singleton, Discover[singleton.type], "single.doesntExist", Left("Cannot resolve module single"))
+ 'neg3 - check(singleton, Discover[singleton.type], "", Left("Selector cannot be empty"))
+ }
+ 'nested - {
+
+ 'pos1 - check(nestedModule, Discover[nestedModule.type], "single", Right(nestedModule.single))
+ 'pos2 - check(nestedModule, Discover[nestedModule.type], "nested.single", Right(nestedModule.nested.single))
+ 'pos3 - check(nestedModule, Discover[nestedModule.type], "classInstance.single", Right(nestedModule.classInstance.single))
+ 'neg1 - check(nestedModule, Discover[nestedModule.type], "doesntExist", Left("Cannot resolve task doesntExist"))
+ 'neg2 - check(nestedModule, Discover[nestedModule.type], "single.doesntExist", Left("Cannot resolve module single"))
+ 'neg3 - check(nestedModule, Discover[nestedModule.type], "nested.doesntExist", Left("Cannot resolve task nested.doesntExist"))
+ 'neg4 - check(nestedModule, Discover[nestedModule.type], "classInstance.doesntExist", Left("Cannot resolve task classInstance.doesntExist"))
+ }
+ 'cross - {
+ 'single - {
+
+ 'pos1 - check(singleCross, Discover[singleCross.type], "cross[210].suffix", Right(singleCross.cross("210").suffix))
+ 'pos2 - check(singleCross, Discover[singleCross.type], "cross[211].suffix", Right(singleCross.cross("211").suffix))
+ 'neg1 - check(singleCross, Discover[singleCross.type], "cross[210].doesntExist", Left("Cannot resolve task cross[210].doesntExist"))
+ 'neg2 - check(singleCross, Discover[singleCross.type], "cross[doesntExist].doesntExist", Left("Cannot resolve cross cross[doesntExist]"))
+ 'neg2 - check(singleCross, Discover[singleCross.type], "cross[doesntExist].suffix", Left("Cannot resolve cross cross[doesntExist]"))
+ }
+ 'double - {
+
+ 'pos1 - check(
+ doubleCross,
+ Discover[doubleCross.type],
+ "cross[210,jvm].suffix",
+ Right(doubleCross.cross("210", "jvm").suffix)
+ )
+ 'pos2 - check(
+ doubleCross,
+ Discover[doubleCross.type],
+ "cross[211,jvm].suffix",
+ Right(doubleCross.cross("211", "jvm").suffix)
+ )
+ }
+ 'nested - {
+ 'pos1 - check(
+ nestedCrosses,
+ Discover[nestedCrosses.type],
+ "cross[210].cross2[js].suffix",
+ Right(nestedCrosses.cross("210").cross2("js").suffix)
+ )
+ 'pos2 - check(
+ nestedCrosses,
+ Discover[nestedCrosses.type],
+ "cross[211].cross2[jvm].suffix",
+ Right(nestedCrosses.cross("211").cross2("jvm").suffix)
+ )
+ }
+ }
+
+ }
+}
diff --git a/core/test/src/mill/main/ParseArgsTest.scala b/core/test/src/mill/main/ParseArgsTest.scala
new file mode 100644
index 00000000..2ef07d36
--- /dev/null
+++ b/core/test/src/mill/main/ParseArgsTest.scala
@@ -0,0 +1,229 @@
+package mill.main
+
+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[List[Segment]],
+ expectedArgs: Seq[String]) = {
+ val Right((selectors, args)) = ParseArgs(input)
+
+ assert(
+ selectors == expectedSelectors,
+ args == expectedArgs
+ )
+ }
+
+ 'rejectEmpty {
+ assert(ParseArgs(Seq.empty) == Left("Selector cannot be empty"))
+ }
+ 'singleSelector - check(
+ input = Seq("core.compile"),
+ expectedSelectors = List(
+ List(Label("core"), Label("compile"))
+ ),
+ expectedArgs = Seq.empty
+ )
+ 'singleSelectorWithArgs - check(
+ input = Seq("application.run", "hello", "world"),
+ expectedSelectors = List(
+ List(Label("application"), Label("run"))
+ ),
+ expectedArgs = Seq("hello", "world")
+ )
+ 'singleSelectorWithCross - check(
+ input = Seq("bridges[2.12.4,jvm].compile"),
+ expectedSelectors = List(
+ List(Label("bridges"), Cross(Seq("2.12.4", "jvm")), Label("compile"))
+ ),
+ expectedArgs = Seq.empty
+ )
+ 'multiSelectorsBraceExpansion - check(
+ input = Seq("--all", "{core,application}.compile"),
+ expectedSelectors = List(
+ List(Label("core"), Label("compile")),
+ List(Label("application"), Label("compile"))
+ ),
+ expectedArgs = Seq.empty
+ )
+ 'multiSelectorsBraceExpansionWithArgs - check(
+ input = Seq("--all", "{core,application}.run", "--", "hello", "world"),
+ expectedSelectors = List(
+ List(Label("core"), Label("run")),
+ List(Label("application"), Label("run"))
+ ),
+ expectedArgs = Seq("hello", "world")
+ )
+ 'multiSelectorsBraceExpansionWithCross - check(
+ input = Seq("--all", "bridges[2.12.4,jvm].{test,jar}"),
+ expectedSelectors = List(
+ List(Label("bridges"), Cross(Seq("2.12.4", "jvm")), Label("test")),
+ 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(
+ List(Label("bridges"), Cross(Seq("2.11.11")), Label("jar")),
+ 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(
+ List(Label("core"), Label("compile"))
+ ),
+ expectedArgs = Seq("application.compile")
+ )
+ }
+ }
+
+}
diff --git a/core/test/src/mill/util/TestEvaluator.scala b/core/test/src/mill/util/TestEvaluator.scala
new file mode 100644
index 00000000..47c9d940
--- /dev/null
+++ b/core/test/src/mill/util/TestEvaluator.scala
@@ -0,0 +1,30 @@
+package mill.util
+
+import ammonite.ops.Path
+import mill.define.{Input, Target, Task}
+import mill.eval.{Evaluator, Result}
+import mill.util.Strict.Agg
+class TestEvaluator(module: mill.Module,
+ workspacePath: Path,
+ basePath: Path){
+ val evaluator = new Evaluator(workspacePath, basePath, module, DummyLogger)
+
+ def apply[T](t: Task[T]): Either[Result.Failing, (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
+ case t: mill.define.Command[_] => t
+ }.size
+ ))
+ } else {
+ Left(
+ evaluated.failing.lookupKey(evaluated.failing.keys().next).items.next())
+ }
+ }
+
+}
diff --git a/core/test/src/mill/util/TestGraphs.scala b/core/test/src/mill/util/TestGraphs.scala
new file mode 100644
index 00000000..54c8d815
--- /dev/null
+++ b/core/test/src/mill/util/TestGraphs.scala
@@ -0,0 +1,222 @@
+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") }
+ }
+
+ object canOverrideSuper extends TestUtil.BaseModule with BaseModule {
+ override def foo = T{ super.foo() ++ Seq("object") }
+ }
+
+ 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 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/core/test/src/mill/util/TestUtil.scala b/core/test/src/mill/util/TestUtil.scala
new file mode 100644
index 00000000..1af12a74
--- /dev/null
+++ b/core/test/src/mill/util/TestUtil.scala
@@ -0,0 +1,60 @@
+package mill.util
+
+import ammonite.main.Router.Overrides
+import mill.define._
+import mill.eval.Result
+import utest.assert
+import mill.util.Strict.Agg
+import scala.collection.mutable
+
+object TestUtil {
+ class BaseModule(implicit millModuleEnclosing0: sourcecode.Enclosing,
+ millModuleLine0: sourcecode.Line,
+ millName0: sourcecode.Name,
+ overrides: Overrides)
+ extends mill.define.BaseModule(ammonite.ops.pwd / millModuleEnclosing0.value)
+
+ 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(_, 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))
+ }
+ }
+ }
+}