diff options
author | Li Haoyi <haoyi.sg@gmail.com> | 2017-11-10 20:52:57 -0800 |
---|---|---|
committer | Li Haoyi <haoyi.sg@gmail.com> | 2017-11-10 20:52:57 -0800 |
commit | 337508c82cbe598b8796fc532cd92e8230b099cb (patch) | |
tree | 8e1732b4978a7d9e40dc545bb1dd1fdc8acdf2e9 /core/src/test/scala/mill | |
parent | 79bfb324a231bb3f46a180039b6d68ca042f181b (diff) | |
download | mill-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.scala | 139 | ||||
-rw-r--r-- | core/src/test/scala/mill/CacherTests.scala | 53 | ||||
-rw-r--r-- | core/src/test/scala/mill/EvaluationTests.scala | 215 | ||||
-rw-r--r-- | core/src/test/scala/mill/GraphTests.scala | 256 | ||||
-rw-r--r-- | core/src/test/scala/mill/JavaCompileJarTests.scala | 168 | ||||
-rw-r--r-- | core/src/test/scala/mill/MacroErrorTests.scala | 78 | ||||
-rw-r--r-- | core/src/test/scala/mill/TarjanTests.scala | 90 | ||||
-rw-r--r-- | core/src/test/scala/mill/TestGraphs.scala | 97 | ||||
-rw-r--r-- | core/src/test/scala/mill/TestMain.scala | 100 | ||||
-rw-r--r-- | core/src/test/scala/mill/TestUtil.scala | 38 | ||||
-rw-r--r-- | core/src/test/scala/mill/UTestFramework.scala | 11 |
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) + } +} |