summaryrefslogtreecommitdiff
path: root/core/src/test/scala/mill
diff options
context:
space:
mode:
authorLi Haoyi <haoyi.sg@gmail.com>2017-11-10 20:52:57 -0800
committerLi Haoyi <haoyi.sg@gmail.com>2017-11-10 20:52:57 -0800
commit337508c82cbe598b8796fc532cd92e8230b099cb (patch)
tree8e1732b4978a7d9e40dc545bb1dd1fdc8acdf2e9 /core/src/test/scala/mill
parent79bfb324a231bb3f46a180039b6d68ca042f181b (diff)
downloadmill-337508c82cbe598b8796fc532cd92e8230b099cb.tar.gz
mill-337508c82cbe598b8796fc532cd92e8230b099cb.tar.bz2
mill-337508c82cbe598b8796fc532cd92e8230b099cb.zip
rename forge -> mill
Diffstat (limited to 'core/src/test/scala/mill')
-rw-r--r--core/src/test/scala/mill/ApplicativeTests.scala139
-rw-r--r--core/src/test/scala/mill/CacherTests.scala53
-rw-r--r--core/src/test/scala/mill/EvaluationTests.scala215
-rw-r--r--core/src/test/scala/mill/GraphTests.scala256
-rw-r--r--core/src/test/scala/mill/JavaCompileJarTests.scala168
-rw-r--r--core/src/test/scala/mill/MacroErrorTests.scala78
-rw-r--r--core/src/test/scala/mill/TarjanTests.scala90
-rw-r--r--core/src/test/scala/mill/TestGraphs.scala97
-rw-r--r--core/src/test/scala/mill/TestMain.scala100
-rw-r--r--core/src/test/scala/mill/TestUtil.scala38
-rw-r--r--core/src/test/scala/mill/UTestFramework.scala11
11 files changed, 1245 insertions, 0 deletions
diff --git a/core/src/test/scala/mill/ApplicativeTests.scala b/core/src/test/scala/mill/ApplicativeTests.scala
new file mode 100644
index 00000000..5bb9945c
--- /dev/null
+++ b/core/src/test/scala/mill/ApplicativeTests.scala
@@ -0,0 +1,139 @@
+package mill
+import mill.define.Applicative
+import utest._
+import language.experimental.macros
+
+
+object ApplicativeTests extends TestSuite {
+ implicit def optionToOpt[T](o: Option[T]): Opt[T] = new Opt(o)
+ class Opt[T](val o: Option[T]) extends Applicative.Applyable[T]
+ object Opt extends define.Applicative.Applyer[Opt, Option, String]{
+
+ val injectedCtx = "helloooo"
+ def underlying[A](v: Opt[A]) = v.o
+ def apply[T](t: T): Option[T] = macro Applicative.impl[Option, T, String]
+
+ type O[+T] = Option[T]
+ def mapCtx[A, B](a: O[A])(f: (A, String) => B): Option[B] = a.map(f(_, injectedCtx))
+ def zip() = Some(())
+ def zip[A](a: O[A]) = a.map(Tuple1(_))
+ def zip[A, B](a: O[A], b: O[B]) = {
+ for(a <- a; b <- b) yield (a, b)
+ }
+ def zip[A, B, C](a: O[A], b: O[B], c: O[C]) = {
+ for(a <- a; b <- b; c <- c) yield (a, b, c)
+ }
+ def zip[A, B, C, D](a: O[A], b: O[B], c: O[C], d: O[D]) = {
+ for(a <- a; b <- b; c <- c; d <- d) yield (a, b, c, d)
+ }
+ def zip[A, B, C, D, E](a: O[A], b: O[B], c: O[C], d: O[D], e: O[E]) = {
+ for(a <- a; b <- b; c <- c; d <- d; e <- e) yield (a, b, c, d, e)
+ }
+ def zip[A, B, C, D, E, F](a: O[A], b: O[B], c: O[C], d: O[D], e: O[E], f: O[F]) ={
+ for(a <- a; b <- b; c <- c; d <- d; e <- e; f <- f) yield (a, b, c, d, e, f)
+ }
+ def zip[A, B, C, D, E, F, G](a: O[A], b: O[B], c: O[C], d: O[D], e: O[E], f: O[F], g: O[G]) = {
+ for(a <- a; b <- b; c <- c; d <- d; e <- e; f <- f; g <- g) yield (a, b, c, d, e, f, g)
+ }
+ }
+ class Counter{
+ var value = 0
+ def apply() = {
+ value += 1
+ value
+ }
+ }
+
+ 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/src/test/scala/mill/CacherTests.scala b/core/src/test/scala/mill/CacherTests.scala
new file mode 100644
index 00000000..1a510e4c
--- /dev/null
+++ b/core/src/test/scala/mill/CacherTests.scala
@@ -0,0 +1,53 @@
+package mill
+
+import mill.define.Task
+import mill.discover.Discovered
+import mill.eval.Evaluator
+import mill.util.OSet
+import utest._
+import utest.framework.TestPath
+
+object CacherTests extends TestSuite{
+ object Base extends Base
+ trait Base extends Task.Cacher{
+ def value = T{ 1 }
+ }
+ object Middle extends Middle
+ trait Middle extends Base{
+ override 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: Discovered, V](base: T, v: Task[V])(implicit tp: TestPath) = {
+ val workspace = ammonite.ops.pwd / 'target / 'workspace / tp.value
+ val evaluator = new Evaluator(workspace, Discovered.mapping(base))
+ evaluator.evaluate(OSet(v)).values(0)
+ }
+
+ 'simpleDefIsCached - assert(
+ Base.value eq Base.value,
+ eval(Base, Base.value) == 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
+ )
+ }
+}
diff --git a/core/src/test/scala/mill/EvaluationTests.scala b/core/src/test/scala/mill/EvaluationTests.scala
new file mode 100644
index 00000000..48b42539
--- /dev/null
+++ b/core/src/test/scala/mill/EvaluationTests.scala
@@ -0,0 +1,215 @@
+package mill
+
+
+import mill.TestUtil.{Test, test}
+import mill.define.{Target, Task}
+import mill.define.Task.Cacher
+import mill.discover.Discovered
+import mill.eval.Evaluator
+import mill.util.OSet
+import utest._
+import utest.framework.TestPath
+
+object EvaluationTests extends TestSuite{
+ class Checker[T: Discovered](base: T)(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, Discovered.mapping(base))
+
+ def apply(target: Task[_], expValue: Any,
+ expEvaled: OSet[Task[_]],
+ extraEvaled: Int = 0,
+ secondRun: Boolean = true) = {
+
+ val Evaluator.Results(returnedValues, returnedEvaluated) = evaluator.evaluate(OSet(target))
+
+ val (matchingReturnedEvaled, extra) = returnedEvaluated.items.partition(expEvaled.contains)
+
+ assert(
+ returnedValues == Seq(expValue),
+ matchingReturnedEvaled.toSet == expEvaled.toSet,
+ extra.length == extraEvaled
+ )
+
+ // Second time the value is already cached, so no evaluation needed
+ if (secondRun){
+ val Evaluator.Results(returnedValues2, returnedEvaluated2) = evaluator.evaluate(OSet(target))
+ val expecteSecondRunEvaluated = OSet()
+ assert(
+ returnedValues2 == returnedValues,
+ returnedEvaluated2 == expecteSecondRunEvaluated
+ )
+ }
+ }
+ }
+
+
+ val tests = Tests{
+ val graphs = new TestGraphs()
+ import graphs._
+ 'evaluateSingle - {
+
+
+ 'singleton - {
+ import singleton._
+ val check = new Checker(singleton)
+ // First time the target is evaluated
+ check(single, expValue = 0, expEvaled = OSet(single))
+
+ single.counter += 1
+ // After incrementing the counter, it forces re-evaluation
+ check(single, expValue = 1, expEvaled = OSet(single))
+ }
+ 'pair - {
+ import pair._
+ val check = new Checker(pair)
+ check(down, expValue = 0, expEvaled = OSet(up, down))
+
+ down.counter += 1
+ check(down, expValue = 1, expEvaled = OSet(down))
+
+ up.counter += 1
+ check(down, expValue = 2, expEvaled = OSet(up, down))
+ }
+ 'anonTriple - {
+ import anonTriple._
+ val check = new Checker(anonTriple)
+ val middle = down.inputs(0)
+ check(down, expValue = 0, expEvaled = OSet(up, middle, down))
+
+ down.counter += 1
+ check(down, expValue = 1, expEvaled = OSet(middle, down))
+
+ up.counter += 1
+ check(down, expValue = 2, expEvaled = OSet(up, middle, down))
+
+ middle.asInstanceOf[TestUtil.Test].counter += 1
+
+ check(down, expValue = 3, expEvaled = OSet(middle, down))
+ }
+ 'diamond - {
+ import diamond._
+ val check = new Checker(diamond)
+ check(down, expValue = 0, expEvaled = OSet(up, left, right, down))
+
+ down.counter += 1
+ check(down, expValue = 1, expEvaled = OSet(down))
+
+ up.counter += 1
+ // Increment by 2 because up is referenced twice: once by left once by right
+ check(down, expValue = 3, expEvaled = OSet(up, left, right, down))
+
+ left.counter += 1
+ check(down, expValue = 4, expEvaled = OSet(left, down))
+
+ right.counter += 1
+ check(down, expValue = 5, expEvaled = OSet(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 = OSet(up, left, right, down))
+
+ down.counter += 1
+ check(down, expValue = 1, expEvaled = OSet(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 = OSet(up, left, right, down))
+
+ left.counter += 1
+ check(down, expValue = 4, expEvaled = OSet(left, right, down))
+
+ right.counter += 1
+ check(down, expValue = 5, expEvaled = OSet(left, right, down))
+ }
+
+ 'bigSingleTerminal - {
+ import bigSingleTerminal._
+ val check = new Checker(bigSingleTerminal)
+
+ check(j, expValue = 0, expEvaled = OSet(a, b, e, f, i, j), extraEvaled = 22)
+
+ j.counter += 1
+ check(j, expValue = 1, expEvaled = OSet(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 = OSet(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 = OSet(b, e, f, i, j), extraEvaled = 20)
+ }
+ }
+
+ 'evaluateMixed - {
+ 'tasksAreUncached - {
+ // ___ left ___
+ // / \
+ // up middle -- down
+ // /
+ // right
+ object taskDiamond extends Cacher{
+ var leftCount = 0
+ var rightCount = 0
+ var middleCount = 0
+ def up = T{ test() }
+ 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 taskDiamond._
+
+ // 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(taskDiamond)
+ assert(leftCount == 0, rightCount == 0)
+ check(down, expValue = 10101, expEvaled = OSet(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 = OSet())
+ 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 = OSet(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 = OSet(), extraEvaled = 1, secondRun = false)
+ assert(leftCount == 3, middleCount == 2, rightCount == 1)
+ check(left, expValue = 2, expEvaled = OSet(), extraEvaled = 1, secondRun = false)
+ assert(leftCount == 4, middleCount == 2, rightCount == 1)
+
+ check(middle, expValue = 100, expEvaled = OSet(), extraEvaled = 2, secondRun = false)
+ assert(leftCount == 4, middleCount == 3, rightCount == 1)
+ check(middle, expValue = 100, expEvaled = OSet(), extraEvaled = 2, secondRun = false)
+ assert(leftCount == 4, middleCount == 4, rightCount == 1)
+ }
+
+
+ }
+
+ }
+}
diff --git a/core/src/test/scala/mill/GraphTests.scala b/core/src/test/scala/mill/GraphTests.scala
new file mode 100644
index 00000000..575b4a89
--- /dev/null
+++ b/core/src/test/scala/mill/GraphTests.scala
@@ -0,0 +1,256 @@
+package mill
+
+import utest._
+import TestUtil.test
+import mill.define.Task
+import mill.discover.Discovered
+import mill.eval.Evaluator
+import mill.util.OSet
+
+object GraphTests extends TestSuite{
+
+ val tests = Tests{
+
+
+ val graphs = new TestGraphs()
+ import graphs._
+
+ 'discovery{
+ class CanNest{
+ val single = test()
+ val invisible: Any = test()
+ }
+ object outer {
+ val single = test()
+ val invisible: Any = test()
+ object nested{
+ val single = test()
+ val invisible: Any = test()
+
+ }
+ val classInstance = new CanNest
+
+ }
+ val discovered = Discovered[outer.type].apply(outer).map(x => (x._1, x._3))
+ val expected = Seq(
+ (List("classInstance", "single"), outer.classInstance.single),
+ (List("nested", "single"), outer.nested.single),
+ (List("single"), outer.single)
+ )
+ assert(discovered == expected)
+ }
+
+ 'failConsistencyChecks - {
+ // Make sure these fail because `def`s without `Cacher` will re-evaluate
+ // each time, returning different sets of targets.
+ //
+ // Maybe later we can convert them into compile errors somehow
+
+ val expected = List(List("down"), List("right"), List("left"), List("up"))
+
+ 'diamond - {
+ val inconsistent = Discovered.consistencyCheck(
+ diamond,
+ Discovered[diamond.type]
+ )
+
+ assert(inconsistent == Nil)
+ }
+ 'anonDiamond - {
+ val inconsistent = Discovered.consistencyCheck(
+ anonDiamond,
+ Discovered[anonDiamond.type]
+ )
+
+ assert(inconsistent == Nil)
+ }
+
+ 'borkedCachedDiamond2 - {
+ val inconsistent = Discovered.consistencyCheck(
+ borkedCachedDiamond2,
+ Discovered[borkedCachedDiamond2.type]
+ )
+ assert(inconsistent == expected)
+ }
+ 'borkedCachedDiamond3 - {
+ val inconsistent = Discovered.consistencyCheck(
+ borkedCachedDiamond3,
+ Discovered[borkedCachedDiamond3.type]
+ )
+ assert(inconsistent == expected)
+ }
+ }
+
+
+ 'topoSortedTransitiveTargets - {
+ def check(targets: OSet[Task[_]], expected: OSet[Task[_]]) = {
+ val result = Evaluator.topoSortedTransitiveTargets(targets).values
+ TestUtil.checkTopological(result)
+ assert(result == expected)
+ }
+
+ 'singleton - check(
+ targets = OSet(singleton.single),
+ expected = OSet(singleton.single)
+ )
+ 'pair - check(
+ targets = OSet(pair.down),
+ expected = OSet(pair.up, pair.down)
+ )
+ 'anonTriple - check(
+ targets = OSet(anonTriple.down),
+ expected = OSet(anonTriple.up, anonTriple.down.inputs(0), anonTriple.down)
+ )
+ 'diamond - check(
+ targets = OSet(diamond.down),
+ expected = OSet(diamond.up, diamond.left, diamond.right, diamond.down)
+ )
+ 'anonDiamond - check(
+ targets = OSet(diamond.down),
+ expected = OSet(
+ diamond.up,
+ diamond.down.inputs(0),
+ diamond.down.inputs(1),
+ diamond.down
+ )
+ )
+ 'defCachedDiamond - check(
+ targets = OSet(defCachedDiamond.down),
+ expected = OSet(
+ 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 = Evaluator.topoSortedTransitiveTargets(OSet(bigSingleTerminal.j)).values
+ TestUtil.checkTopological(result)
+ assert(result.size == 28)
+ }
+ }
+
+ 'groupAroundNamedTargets - {
+ def check[T: Discovered, R <: Task[Int]](base: T,
+ target: R,
+ expected: OSet[(OSet[R], Int)]) = {
+
+ val mapping = Discovered.mapping(base)
+ val topoSortedTransitive = Evaluator.topoSortedTransitiveTargets(OSet(target))
+
+ val grouped = Evaluator.groupAroundNamedTargets(topoSortedTransitive, mapping)
+ val flattened = OSet.from(grouped.values().flatMap(_.items))
+
+ TestUtil.checkTopological(flattened)
+ for(((expectedPresent, expectedSize), i) <- expected.items.zipWithIndex){
+ val grouping = grouped.lookupKey(i)
+ assert(
+ grouping.size == expectedSize,
+ grouping.filter(mapping.contains) == expectedPresent
+ )
+ }
+ }
+ 'singleton - check(
+ singleton,
+ singleton.single,
+ OSet(OSet(singleton.single) -> 1)
+ )
+ 'pair - check(
+ pair,
+ pair.down,
+ OSet(OSet(pair.up) -> 1, OSet(pair.down) -> 1)
+ )
+ 'anonTriple - check(
+ anonTriple,
+ anonTriple.down,
+ OSet(OSet(anonTriple.up) -> 1, OSet(anonTriple.down) -> 2)
+ )
+ 'diamond - check(
+ diamond,
+ diamond.down,
+ OSet(
+ OSet(diamond.up) -> 1,
+ OSet(diamond.left) -> 1,
+ OSet(diamond.right) -> 1,
+ OSet(diamond.down) -> 1
+ )
+ )
+
+ 'defCachedDiamond - check(
+ defCachedDiamond,
+ defCachedDiamond.down,
+ OSet(
+ OSet(defCachedDiamond.up) -> 2,
+ OSet(defCachedDiamond.left) -> 2,
+ OSet(defCachedDiamond.right) -> 2,
+ OSet(defCachedDiamond.down) -> 2
+ )
+ )
+
+ 'anonDiamond - check(
+ anonDiamond,
+ anonDiamond.down,
+ OSet(
+ OSet(anonDiamond.up) -> 1,
+ OSet(anonDiamond.down) -> 3
+ )
+ )
+ 'bigSingleTerminal - check(
+ bigSingleTerminal,
+ bigSingleTerminal.j,
+ OSet(
+ OSet(bigSingleTerminal.a) -> 3,
+ OSet(bigSingleTerminal.b) -> 2,
+ OSet(bigSingleTerminal.e) -> 9,
+ OSet(bigSingleTerminal.i) -> 6,
+ OSet(bigSingleTerminal.f) -> 4,
+ OSet(bigSingleTerminal.j) -> 4
+ )
+ )
+ }
+
+ 'labeling - {
+
+ def check[T: Discovered](base: T, t: Task[_], relPath: Option[String]) = {
+
+
+ val names: Seq[(Task[_], Seq[String])] = Discovered.mapping(base).mapValues(_.segments).toSeq
+ val nameMap = names.toMap
+
+ val targetLabel = nameMap.get(t).map(_.mkString("."))
+ assert(targetLabel == relPath)
+ }
+ 'singleton - check(singleton, singleton.single, Some("single"))
+ 'pair - {
+ check(pair, pair.up, Some("up"))
+ check(pair, pair.down, Some("down"))
+ }
+
+ 'anonTriple - {
+ check(anonTriple, anonTriple.up, Some("up"))
+ check(anonTriple, anonTriple.down.inputs(0), None)
+ check(anonTriple, anonTriple.down, Some("down"))
+ }
+
+ 'diamond - {
+ check(diamond, diamond.up, Some("up"))
+ check(diamond, diamond.left, Some("left"))
+ check(diamond, diamond.right, Some("right"))
+ check(diamond, diamond.down, Some("down"))
+ }
+
+ 'anonDiamond - {
+ check(anonDiamond, anonDiamond.up, Some("up"))
+ check(anonDiamond, anonDiamond.down.inputs(0), None)
+ check(anonDiamond, anonDiamond.down.inputs(1), None)
+ check(anonDiamond, anonDiamond.down, Some("down"))
+ }
+
+ }
+
+ }
+}
diff --git a/core/src/test/scala/mill/JavaCompileJarTests.scala b/core/src/test/scala/mill/JavaCompileJarTests.scala
new file mode 100644
index 00000000..6e398540
--- /dev/null
+++ b/core/src/test/scala/mill/JavaCompileJarTests.scala
@@ -0,0 +1,168 @@
+package mill
+
+
+import ammonite.ops._
+import ImplicitWd._
+import mill.define.Task
+import mill.discover.Discovered
+import mill.eval.{Evaluator, PathRef}
+import mill.modules.Jvm.jarUp
+import mill.util.OSet
+import utest._
+
+object JavaCompileJarTests extends TestSuite{
+ def compileAll(dest: Path, sources: Seq[PathRef]) = {
+ mkdir(dest)
+ import ammonite.ops._
+ %("javac", sources.map(_.path.toString()), "-d", dest)(wd = dest)
+ PathRef(dest)
+//
+ }
+
+
+
+ val tests = Tests{
+ 'javac {
+ val workspacePath = pwd / 'target / 'workspace / 'javac
+ val javacSrcPath = pwd / 'core / 'src / 'test / 'examples / 'javac
+ val javacDestPath = workspacePath / 'src
+
+ mkdir(pwd / 'target / 'workspace / 'javac)
+ cp(javacSrcPath, javacDestPath)
+
+ object Build extends Task.Cacher{
+ def sourceRootPath = javacDestPath / 'src
+ def resourceRootPath = javacDestPath / 'resources
+
+ // sourceRoot -> allSources -> classFiles
+ // |
+ // v
+ // resourceRoot ----> jar
+ def sourceRoot = T{ Task.path(sourceRootPath) }
+ def resourceRoot = T{ Task.path(resourceRootPath) }
+ def allSources = T{ ls.rec(sourceRoot().path).map(PathRef(_)) }
+ def classFiles = T{ compileAll(Task.ctx().dest, allSources()) }
+ def jar = T{ jarUp(resourceRoot, classFiles) }
+
+ def run(mainClsName: String) = T.command{
+ %%('java, "-cp", classFiles().path, mainClsName)
+ }
+ }
+ import Build._
+ val mapping = Discovered.mapping(Build)
+
+ def eval[T](t: Task[T]): (T, Int) = {
+ val evaluator = new Evaluator(workspacePath, mapping)
+ val evaluated = evaluator.evaluate(OSet(t))
+ Tuple2(
+ evaluated.values(0).asInstanceOf[T],
+ evaluated.targets.filter(x => mapping.contains(x) || x.isInstanceOf[mill.define.Command[_]]).size
+ )
+ }
+ def check(targets: OSet[Task[_]], expected: OSet[Task[_]]) = {
+ val evaluator = new Evaluator(workspacePath, mapping)
+
+ val evaluated = evaluator.evaluate(targets).targets.filter(mapping.contains)
+ assert(evaluated == expected)
+ }
+
+ def append(path: Path, txt: String) = ammonite.ops.write.append(path, txt)
+
+
+ check(
+ targets = OSet(jar),
+ expected = OSet(resourceRoot, sourceRoot, allSources, classFiles, jar)
+ )
+
+ // Re-running with no changes results in nothing being evaluated
+ check(targets = OSet(jar), expected = OSet())
+
+ // Appending an empty string gets ignored due to file-content hashing
+ append(sourceRootPath / "Foo.java", "")
+ check(targets = OSet(jar), expected = OSet())
+
+ // Appending whitespace forces a recompile, but the classfilesend up
+ // exactly the same so no re-jarring.
+ append(sourceRootPath / "Foo.java", " ")
+ check(targets = OSet(jar), expected = OSet(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 = OSet(jar), expected = OSet(sourceRoot, allSources, classFiles, jar))
+
+ // Tweaking the resources forces rebuild of the final jar, without
+ // recompiling classfiles
+ append(resourceRootPath / "hello.txt", " ")
+ check(targets = OSet(jar), expected = OSet(resourceRoot, 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 = OSet(classFiles), expected = OSet(sourceRoot, allSources, classFiles))
+ check(targets = OSet(jar), expected = OSet(resourceRoot, jar))
+ check(targets = OSet(allSources), expected = OSet())
+
+ append(sourceRootPath / "Bar.java", "\nclass BarThree{}")
+ append(resourceRootPath / "hello.txt", " ")
+ check(targets = OSet(resourceRoot), expected = OSet(resourceRoot))
+ check(targets = OSet(allSources), expected = OSet(sourceRoot, allSources))
+ check(targets = OSet(jar), expected = OSet(classFiles, jar))
+
+ val jarContents = %%('jar, "-tf", workspacePath/'jar)(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, "test.Foo")(workspacePath).out.string
+ assert(executed == (31337 + 271828) + "\n")
+
+ println("="*20 + "Run Main" + "="*20)
+ for(i <- 0 until 3){
+ // Build.run is not cached, so every time we eval it it has to
+ // re-evaluate
+ val (runOutput, evalCount) = eval(Build.run("test.Foo"))
+ assert(
+ runOutput.out.string == (31337 + 271828) + "\n",
+ evalCount == 1
+ )
+ }
+
+ val ex = intercept[ammonite.ops.ShelloutException]{
+ 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 (runOutput2, evalCount2) = eval(Build.run("test.BarFour"))
+ assert(
+ runOutput2.out.string == "New Cls!\n",
+ evalCount2 == 4
+ )
+ val (runOutput3, evalCount3) = eval(Build.run("test.BarFour"))
+ assert(
+ runOutput3.out.string == "New Cls!\n",
+ evalCount3 == 1
+ )
+
+
+ }
+ }
+}
diff --git a/core/src/test/scala/mill/MacroErrorTests.scala b/core/src/test/scala/mill/MacroErrorTests.scala
new file mode 100644
index 00000000..20bf805a
--- /dev/null
+++ b/core/src/test/scala/mill/MacroErrorTests.scala
@@ -0,0 +1,78 @@
+package mill
+
+import utest._
+
+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 mill.define.Task.Cacher{ 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("must be defs"))
+ }
+ 'neg - {
+
+ val expectedMsg =
+ "Target#apply() call cannot use `value n` defined within the T{...} block"
+ val err = compileError("""new mill.define.Task.Cacher{
+ 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 mill.define.Task.Cacher{
+ 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("must be defs"))
+ }
+ }
+ }
+}
diff --git a/core/src/test/scala/mill/TarjanTests.scala b/core/src/test/scala/mill/TarjanTests.scala
new file mode 100644
index 00000000..7ae7dd04
--- /dev/null
+++ b/core/src/test/scala/mill/TarjanTests.scala
@@ -0,0 +1,90 @@
+package mill
+import mill.eval.Tarjans
+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/src/test/scala/mill/TestGraphs.scala b/core/src/test/scala/mill/TestGraphs.scala
new file mode 100644
index 00000000..46fad053
--- /dev/null
+++ b/core/src/test/scala/mill/TestGraphs.scala
@@ -0,0 +1,97 @@
+package mill
+
+import mill.define.Task.Cacher
+import mill.TestUtil.test
+
+class TestGraphs(){
+ // single
+ object singleton {
+ val single = test()
+ }
+
+ // up---down
+ object pair {
+ val up = test()
+ val down = test(up)
+ }
+
+ // up---o---down
+ object anonTriple{
+ val up = test()
+ val down = test(test(up))
+ }
+
+ // left
+ // / \
+ // up down
+ // \ /
+ // right
+ object diamond{
+ val up = test()
+ val left = test(up)
+ val right = test(up)
+ val down = test(left, right)
+ }
+
+ // o
+ // / \
+ // up down
+ // \ /
+ // o
+ object anonDiamond{
+ val up = test()
+ val down = test(test(up), test(up))
+ }
+
+ object defCachedDiamond extends Cacher{
+ def up = T{ test() }
+ def left = T{ test(up) }
+ def right = T{ test(up) }
+ def down = T{ test(left, right) }
+ }
+
+
+ object borkedCachedDiamond2 extends Cacher {
+ def up = test()
+ def left = test(up)
+ def right = test(up)
+ def down = test(left, right)
+ }
+
+ object borkedCachedDiamond3 {
+ 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{
+ val a = test(test(), test())
+ val b = test(test())
+ val e = {
+ val c = test(a)
+ val d = test(a)
+ test(test(test(), test(c)), test(test(c, test(d, b))))
+ }
+ val f = test(test(test(), test(e)))
+
+ val i = {
+ val g = test()
+ val h = test(g, e)
+ test(test(g), test(test(h)))
+ }
+ val j = test(test(i), test(i, f), test(f))
+ }
+}
+
diff --git a/core/src/test/scala/mill/TestMain.scala b/core/src/test/scala/mill/TestMain.scala
new file mode 100644
index 00000000..d0ebfc9d
--- /dev/null
+++ b/core/src/test/scala/mill/TestMain.scala
@@ -0,0 +1,100 @@
+package mill
+import ammonite.ops._
+import java.io.File
+
+import coursier._
+import sbt.internal.inc.{FreshCompilerCache, ScalaInstance, ZincUtil}
+import sbt.internal.util.{ConsoleOut, MainAppender}
+import sbt.util.LogExchange
+import xsbti.api.{ClassLike, DependencyContext}
+import xsbti.compile._
+
+import scalaz.concurrent.Task
+
+object TestMain {
+ def main(args: Array[String]): Unit = {
+ val scalaVersion = "2.12.4"
+ val start = Resolution(
+ Set(
+ Dependency(Module("org.scala-lang", "scala-reflect"), scalaVersion),
+ Dependency(Module("org.scala-lang", "scala-compiler"), scalaVersion),
+ Dependency(Module("org.scala-lang", "scala-reflect"), scalaVersion),
+ Dependency(Module("org.scala-sbt", "compiler-bridge_2.12"), "1.0.3"),
+ Dependency(Module("com.lihaoyi", "sourcecode_2.12"), "0.1.4"),
+ Dependency(Module("com.lihaoyi", "pprint_2.12"), "0.5.3"),
+ Dependency(Module("com.lihaoyi", "ammonite_2.12.4"), "1.0.3"),
+ Dependency(Module("com.typesafe.play", "play-json_2.12"), "2.6.6"),
+ Dependency(Module("org.scala-sbt", "zinc_2.12"), "1.0.3")
+ )
+ )
+ val repositories = Seq(
+ Cache.ivy2Local,
+ MavenRepository("https://repo1.maven.org/maven2")
+ )
+
+ val fetch = Fetch.from(repositories, Cache.fetch())
+ val resolution = start.process.run(fetch).unsafePerformSync
+
+
+ val localArtifacts: Seq[File] = Task.gatherUnordered(
+ resolution.artifacts.map(Cache.file(_).run)
+ ).unsafePerformSync.flatMap(_.toOption)
+
+ pprint.log(localArtifacts)
+ def grepJar(s: String) = localArtifacts.find(_.toString.endsWith(s)).get
+
+ val scalac = ZincUtil.scalaCompiler(
+ new ScalaInstance(
+ version = scalaVersion,
+ loader = getClass.getClassLoader,
+ libraryJar = grepJar(s"scala-library-$scalaVersion.jar"),
+ compilerJar = grepJar(s"scala-compiler-$scalaVersion.jar"),
+ allJars = localArtifacts.toArray,
+ explicitActual = None
+ ),
+ grepJar("compiler-bridge_2.12-1.0.3.jar")
+ )
+
+ val outputDir = pwd/'target/'zinc
+ mkdir(outputDir)
+ val scalaFiles = ls.rec(pwd/'src/'main/'scala/'mill).filter(_.ext == "scala").map(_.toIO).toArray
+
+ pprint.log(scalaFiles)
+ scalac.apply(
+ sources = scalaFiles,
+ changes = new DependencyChanges {
+ def isEmpty = true
+ def modifiedBinaries() = Array[File]()
+ def modifiedClasses() = Array[String]()
+ },
+ classpath = localArtifacts.toArray,
+ singleOutput = outputDir.toIO,
+ options = Array(),
+ callback = new xsbti.AnalysisCallback {
+ def startSource(source: File) = ()
+ def apiPhaseCompleted() = ()
+ def enabled() = true
+ def binaryDependency(onBinaryEntry: File, onBinaryClassName: String, fromClassName: String, fromSourceFile: File, context: DependencyContext) = ()
+ def generatedNonLocalClass(source: File, classFile: File, binaryClassName: String, srcClassName: String) = ()
+ def problem(what: String, pos: xsbti.Position, msg: String, severity: xsbti.Severity, reported: Boolean) = ()
+ def dependencyPhaseCompleted() = ()
+ def classDependency(onClassName: String, sourceClassName: String, context: DependencyContext) = ()
+ def generatedLocalClass(source: File, classFile: File) = ()
+ def api(sourceFile: File, classApi: ClassLike) = ()
+
+ def mainClass(sourceFile: File, className: String) = ()
+ def usedName(className: String, name: String, useScopes: java.util.EnumSet[xsbti.UseScope]) = ()
+ },
+ maximumErrors = 10,
+ cache = new FreshCompilerCache(),
+ log = {
+ val console = ConsoleOut.systemOut
+ val consoleAppender = MainAppender.defaultScreen(console)
+ val l = LogExchange.logger("Hello")
+ LogExchange.unbindLoggerAppenders("Hello")
+ LogExchange.bindLoggerAppenders("Hello", (consoleAppender -> sbt.util.Level.Warn) :: Nil)
+ l
+ }
+ )
+ }
+}
diff --git a/core/src/test/scala/mill/TestUtil.scala b/core/src/test/scala/mill/TestUtil.scala
new file mode 100644
index 00000000..f2c4ad3a
--- /dev/null
+++ b/core/src/test/scala/mill/TestUtil.scala
@@ -0,0 +1,38 @@
+package mill
+
+import mill.define.Task
+import mill.util.{Args, OSet}
+import utest.assert
+
+import scala.collection.mutable
+
+object TestUtil {
+ def test(inputs: Task[Int]*) = {
+ new Test(inputs, pure = inputs.nonEmpty)
+ }
+
+ /**
+ * 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 Test(override val inputs: Seq[Task[Int]],
+ val pure: Boolean) extends Task[Int]{
+ var counter = 0
+ override def evaluate(args: Args) = {
+ counter + args.args.map(_.asInstanceOf[Int]).sum
+ }
+
+ override def sideHash = counter
+ }
+ def checkTopological(targets: OSet[Task[_]]) = {
+ val seen = mutable.Set.empty[Task[_]]
+ for(t <- targets.items.reverseIterator){
+ seen.add(t)
+ for(upstream <- t.inputs){
+ assert(!seen(upstream))
+ }
+ }
+ }
+
+}
diff --git a/core/src/test/scala/mill/UTestFramework.scala b/core/src/test/scala/mill/UTestFramework.scala
new file mode 100644
index 00000000..6c0d5191
--- /dev/null
+++ b/core/src/test/scala/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)
+ }
+}