summaryrefslogtreecommitdiff
path: root/main/test/src/eval
diff options
context:
space:
mode:
Diffstat (limited to 'main/test/src/eval')
-rw-r--r--main/test/src/eval/CrossTests.scala56
-rw-r--r--main/test/src/eval/EvaluationTests.scala354
-rw-r--r--main/test/src/eval/FailureTests.scala132
-rw-r--r--main/test/src/eval/JavaCompileJarTests.scala164
-rw-r--r--main/test/src/eval/ModuleTests.scala45
-rw-r--r--main/test/src/eval/TarjanTests.scala91
-rw-r--r--main/test/src/eval/TaskTests.scala95
7 files changed, 937 insertions, 0 deletions
diff --git a/main/test/src/eval/CrossTests.scala b/main/test/src/eval/CrossTests.scala
new file mode 100644
index 00000000..f194924e
--- /dev/null
+++ b/main/test/src/eval/CrossTests.scala
@@ -0,0 +1,56 @@
+package mill.eval
+
+
+import mill.define.Discover
+import mill.util.TestEvaluator
+
+import mill.util.TestGraphs.{crossResolved, doubleCross, nestedCrosses, singleCross}
+import utest._
+object CrossTests extends TestSuite{
+ val tests = Tests{
+ 'singleCross - {
+ val check = new TestEvaluator(singleCross)
+
+ val Right(("210", 1)) = check.apply(singleCross.cross("210").suffix)
+ val Right(("211", 1)) = check.apply(singleCross.cross("211").suffix)
+ val Right(("212", 1)) = check.apply(singleCross.cross("212").suffix)
+ }
+
+ 'crossResolved - {
+ val check = new TestEvaluator(crossResolved)
+
+ val Right(("2.10", 1)) = check.apply(crossResolved.foo("2.10").suffix)
+ val Right(("2.11", 1)) = check.apply(crossResolved.foo("2.11").suffix)
+ val Right(("2.12", 1)) = check.apply(crossResolved.foo("2.12").suffix)
+
+ val Right(("_2.10", 1)) = check.apply(crossResolved.bar("2.10").longSuffix)
+ val Right(("_2.11", 1)) = check.apply(crossResolved.bar("2.11").longSuffix)
+ val Right(("_2.12", 1)) = check.apply(crossResolved.bar("2.12").longSuffix)
+ }
+
+
+ 'doubleCross - {
+ val check = new TestEvaluator(doubleCross)
+
+ val Right(("210_jvm", 1)) = check.apply(doubleCross.cross("210", "jvm").suffix)
+ val Right(("210_js", 1)) = check.apply(doubleCross.cross("210", "js").suffix)
+ val Right(("211_jvm", 1)) = check.apply(doubleCross.cross("211", "jvm").suffix)
+ val Right(("211_js", 1)) = check.apply(doubleCross.cross("211", "js").suffix)
+ val Right(("212_jvm", 1)) = check.apply(doubleCross.cross("212", "jvm").suffix)
+ val Right(("212_js", 1)) = check.apply(doubleCross.cross("212", "js").suffix)
+ val Right(("212_native", 1)) = check.apply(doubleCross.cross("212", "native").suffix)
+ }
+
+ 'nestedCrosses - {
+ val check = new TestEvaluator(nestedCrosses)
+
+ val Right(("210_jvm", 1)) = check.apply(nestedCrosses.cross("210").cross2("jvm").suffix)
+ val Right(("210_js", 1)) = check.apply(nestedCrosses.cross("210").cross2("js").suffix)
+ val Right(("211_jvm", 1)) = check.apply(nestedCrosses.cross("211").cross2("jvm").suffix)
+ val Right(("211_js", 1)) = check.apply(nestedCrosses.cross("211").cross2("js").suffix)
+ val Right(("212_jvm", 1)) = check.apply(nestedCrosses.cross("212").cross2("jvm").suffix)
+ val Right(("212_js", 1)) = check.apply(nestedCrosses.cross("212").cross2("js").suffix)
+ val Right(("212_native", 1)) = check.apply(nestedCrosses.cross("212").cross2("native").suffix)
+ }
+ }
+}
diff --git a/main/test/src/eval/EvaluationTests.scala b/main/test/src/eval/EvaluationTests.scala
new file mode 100644
index 00000000..74f9088c
--- /dev/null
+++ b/main/test/src/eval/EvaluationTests.scala
@@ -0,0 +1,354 @@
+package mill.eval
+
+
+import mill.util.TestUtil.{Test, test}
+import mill.define.{Discover, Graph, Target, Task}
+import mill.{Module, T}
+import mill.util.{DummyLogger, TestEvaluator, TestGraphs, TestUtil}
+import mill.util.Strict.Agg
+import utest._
+import utest.framework.TestPath
+
+
+
+object EvaluationTests extends TestSuite{
+ class Checker[T <: TestUtil.BaseModule](module: T)(implicit tp: TestPath) {
+ // Make sure data is persisted even if we re-create the evaluator each time
+
+ def evaluator = new TestEvaluator(module).evaluator
+
+ def apply(target: Task[_], expValue: Any,
+ expEvaled: Agg[Task[_]],
+ // How many "other" tasks were evaluated other than those listed above.
+ // Pass in -1 to skip the check entirely
+ extraEvaled: Int = 0,
+ // Perform a second evaluation of the same tasks, and make sure the
+ // outputs are the same but nothing was evaluated. Disable this if you
+ // are directly evaluating tasks which need to re-evaluate every time
+ secondRunNoOp: Boolean = true) = {
+
+ val evaled = evaluator.evaluate(Agg(target))
+
+ val (matchingReturnedEvaled, extra) =
+ evaled.evaluated.indexed.partition(expEvaled.contains)
+
+ assert(
+ evaled.values == Seq(expValue),
+ matchingReturnedEvaled.toSet == expEvaled.toSet,
+ extraEvaled == -1 || extra.length == extraEvaled
+ )
+
+ // Second time the value is already cached, so no evaluation needed
+ if (secondRunNoOp){
+ val evaled2 = evaluator.evaluate(Agg(target))
+ val expecteSecondRunEvaluated = Agg()
+ assert(
+ evaled2.values == evaled.values,
+ evaled2.evaluated == expecteSecondRunEvaluated
+ )
+ }
+ }
+ }
+
+
+ val tests = Tests{
+ object graphs extends TestGraphs()
+ import graphs._
+ import TestGraphs._
+ 'evaluateSingle - {
+
+ 'singleton - {
+ import singleton._
+ val check = new Checker(singleton)
+ // First time the target is evaluated
+ check(single, expValue = 0, expEvaled = Agg(single))
+
+ single.counter += 1
+ // After incrementing the counter, it forces re-evaluation
+ check(single, expValue = 1, expEvaled = Agg(single))
+ }
+ 'backtickIdentifiers - {
+ import graphs.bactickIdentifiers._
+ val check = new Checker(bactickIdentifiers)
+
+ check(`a-down-target`, expValue = 0, expEvaled = Agg(`up-target`, `a-down-target`))
+
+ `a-down-target`.counter += 1
+ check(`a-down-target`, expValue = 1, expEvaled = Agg(`a-down-target`))
+
+ `up-target`.counter += 1
+ check(`a-down-target`, expValue = 2, expEvaled = Agg(`up-target`, `a-down-target`))
+ }
+ 'pair - {
+ import pair._
+ val check = new Checker(pair)
+ check(down, expValue = 0, expEvaled = Agg(up, down))
+
+ down.counter += 1
+ check(down, expValue = 1, expEvaled = Agg(down))
+
+ up.counter += 1
+ check(down, expValue = 2, expEvaled = Agg(up, down))
+ }
+ 'anonTriple - {
+ import anonTriple._
+ val check = new Checker(anonTriple)
+ val middle = down.inputs(0)
+ check(down, expValue = 0, expEvaled = Agg(up, middle, down))
+
+ down.counter += 1
+ check(down, expValue = 1, expEvaled = Agg(middle, down))
+
+ up.counter += 1
+ check(down, expValue = 2, expEvaled = Agg(up, middle, down))
+
+ middle.asInstanceOf[TestUtil.Test].counter += 1
+
+ check(down, expValue = 3, expEvaled = Agg(middle, down))
+ }
+ 'diamond - {
+ import diamond._
+ val check = new Checker(diamond)
+ check(down, expValue = 0, expEvaled = Agg(up, left, right, down))
+
+ down.counter += 1
+ check(down, expValue = 1, expEvaled = Agg(down))
+
+ up.counter += 1
+ // Increment by 2 because up is referenced twice: once by left once by right
+ check(down, expValue = 3, expEvaled = Agg(up, left, right, down))
+
+ left.counter += 1
+ check(down, expValue = 4, expEvaled = Agg(left, down))
+
+ right.counter += 1
+ check(down, expValue = 5, expEvaled = Agg(right, down))
+ }
+ 'anonDiamond - {
+ import anonDiamond._
+ val check = new Checker(anonDiamond)
+ val left = down.inputs(0).asInstanceOf[TestUtil.Test]
+ val right = down.inputs(1).asInstanceOf[TestUtil.Test]
+ check(down, expValue = 0, expEvaled = Agg(up, left, right, down))
+
+ down.counter += 1
+ check(down, expValue = 1, expEvaled = Agg(left, right, down))
+
+ up.counter += 1
+ // Increment by 2 because up is referenced twice: once by left once by right
+ check(down, expValue = 3, expEvaled = Agg(up, left, right, down))
+
+ left.counter += 1
+ check(down, expValue = 4, expEvaled = Agg(left, right, down))
+
+ right.counter += 1
+ check(down, expValue = 5, expEvaled = Agg(left, right, down))
+ }
+
+ 'bigSingleTerminal - {
+ import bigSingleTerminal._
+ val check = new Checker(bigSingleTerminal)
+
+ check(j, expValue = 0, expEvaled = Agg(a, b, e, f, i, j), extraEvaled = 22)
+
+ j.counter += 1
+ check(j, expValue = 1, expEvaled = Agg(j), extraEvaled = 3)
+
+ i.counter += 1
+ // increment value by 2 because `i` is used twice on the way to `j`
+ check(j, expValue = 3, expEvaled = Agg(j, i), extraEvaled = 8)
+
+ b.counter += 1
+ // increment value by 4 because `b` is used four times on the way to `j`
+ check(j, expValue = 7, expEvaled = Agg(b, e, f, i, j), extraEvaled = 20)
+ }
+ }
+
+ 'evaluateMixed - {
+ 'separateGroups - {
+ // Make sure that `left` and `right` are able to recompute separately,
+ // even though one depends on the other
+
+ import separateGroups._
+ val checker = new Checker(separateGroups)
+ val evaled1 = checker.evaluator.evaluate(Agg(right, left))
+ val filtered1 = evaled1.evaluated.filter(_.isInstanceOf[Target[_]])
+ assert(filtered1 == Agg(change, left, right))
+ val evaled2 = checker.evaluator.evaluate(Agg(right, left))
+ val filtered2 = evaled2.evaluated.filter(_.isInstanceOf[Target[_]])
+ assert(filtered2 == Agg())
+ change.counter += 1
+ val evaled3 = checker.evaluator.evaluate(Agg(right, left))
+ val filtered3 = evaled3.evaluated.filter(_.isInstanceOf[Target[_]])
+ assert(filtered3 == Agg(change, right))
+
+
+ }
+ 'triangleTask - {
+
+ import triangleTask._
+ val checker = new Checker(triangleTask)
+ checker(right, 3, Agg(left, right), extraEvaled = -1)
+ checker(left, 1, Agg(), extraEvaled = -1)
+
+ }
+ 'multiTerminalGroup - {
+ import multiTerminalGroup._
+
+ val checker = new Checker(multiTerminalGroup)
+ checker(right, 1, Agg(right), extraEvaled = -1)
+ checker(left, 1, Agg(left), extraEvaled = -1)
+ }
+
+ 'multiTerminalBoundary - {
+
+ import multiTerminalBoundary._
+
+ val checker = new Checker(multiTerminalBoundary)
+ checker(task2, 4, Agg(right, left), extraEvaled = -1, secondRunNoOp = false)
+ checker(task2, 4, Agg(), extraEvaled = -1, secondRunNoOp = false)
+ }
+
+ 'overrideSuperTask - {
+ // Make sure you can override targets, call their supers, and have the
+ // overriden target be allocated a spot within the overriden/ folder of
+ // the main publically-available target
+ import canOverrideSuper._
+
+ val checker = new Checker(canOverrideSuper)
+ checker(foo, Seq("base", "object"), Agg(foo), extraEvaled = -1)
+
+
+ val public = ammonite.ops.read(checker.evaluator.outPath / 'foo / "meta.json")
+ val overriden = ammonite.ops.read(
+ checker.evaluator.outPath / 'foo /
+ 'overriden / "mill" / "util" / "TestGraphs" / "BaseModule" / "foo" / "meta.json"
+ )
+ assert(
+ public.contains("base"),
+ public.contains("object"),
+ overriden.contains("base"),
+ !overriden.contains("object")
+ )
+ }
+ 'overrideSuperCommand - {
+ // Make sure you can override commands, call their supers, and have the
+ // overriden command be allocated a spot within the overriden/ folder of
+ // the main publically-available command
+ import canOverrideSuper._
+
+ val checker = new Checker(canOverrideSuper)
+ val runCmd = cmd(1)
+ checker(
+ runCmd,
+ Seq("base1", "object1"),
+ Agg(runCmd),
+ extraEvaled = -1,
+ secondRunNoOp = false
+ )
+
+ val public = ammonite.ops.read(checker.evaluator.outPath / 'cmd / "meta.json")
+ val overriden = ammonite.ops.read(
+ checker.evaluator.outPath / 'cmd /
+ 'overriden / "mill" / "util" / "TestGraphs" / "BaseModule"/ "cmd" / "meta.json"
+ )
+ assert(
+ public.contains("base1"),
+ public.contains("object1"),
+ overriden.contains("base1"),
+ !overriden.contains("object1")
+ )
+ }
+ 'nullTasks - {
+ import nullTasks._
+ val checker = new Checker(nullTasks)
+ checker(nullTarget1, null, Agg(nullTarget1), extraEvaled = -1)
+ checker(nullTarget1, null, Agg(), extraEvaled = -1)
+ checker(nullTarget2, null, Agg(nullTarget2), extraEvaled = -1)
+ checker(nullTarget2, null, Agg(), extraEvaled = -1)
+ checker(nullTarget3, null, Agg(nullTarget3), extraEvaled = -1)
+ checker(nullTarget3, null, Agg(), extraEvaled = -1)
+ checker(nullTarget4, null, Agg(nullTarget4), extraEvaled = -1)
+ checker(nullTarget4, null, Agg(), extraEvaled = -1)
+
+ val nc1 = nullCommand1()
+ val nc2 = nullCommand2()
+ val nc3 = nullCommand3()
+ val nc4 = nullCommand4()
+
+ checker(nc1, null, Agg(nc1), extraEvaled = -1, secondRunNoOp = false)
+ checker(nc1, null, Agg(nc1), extraEvaled = -1, secondRunNoOp = false)
+ checker(nc2, null, Agg(nc2), extraEvaled = -1, secondRunNoOp = false)
+ checker(nc2, null, Agg(nc2), extraEvaled = -1, secondRunNoOp = false)
+ checker(nc3, null, Agg(nc3), extraEvaled = -1, secondRunNoOp = false)
+ checker(nc3, null, Agg(nc3), extraEvaled = -1, secondRunNoOp = false)
+ checker(nc4, null, Agg(nc4), extraEvaled = -1, secondRunNoOp = false)
+ checker(nc4, null, Agg(nc4), extraEvaled = -1, secondRunNoOp = false)
+ }
+
+ 'tasksAreUncached - {
+ // Make sure the tasks `left` and `middle` re-compute every time, while
+ // the target `right` does not
+ //
+ // ___ left ___
+ // / \
+ // up middle -- down
+ // /
+ // right
+ object build extends TestUtil.BaseModule{
+ var leftCount = 0
+ var rightCount = 0
+ var middleCount = 0
+ def up = T{ test.anon() }
+ def left = T.task{ leftCount += 1; up() + 1 }
+ def middle = T.task{ middleCount += 1; 100 }
+ def right = T{ rightCount += 1; 10000 }
+ def down = T{ left() + middle() + right() }
+ }
+
+ import build._
+
+ // Ensure task objects themselves are not cached, and recomputed each time
+ assert(
+ up eq up,
+ left ne left,
+ middle ne middle,
+ right eq right,
+ down eq down
+ )
+
+ // During the first evaluation, they get computed normally like any
+ // cached target
+ val check = new Checker(build)
+ assert(leftCount == 0, rightCount == 0)
+ check(down, expValue = 10101, expEvaled = Agg(up, right, down), extraEvaled = 8)
+ assert(leftCount == 1, middleCount == 1, rightCount == 1)
+
+ // If the upstream `up` doesn't change, the entire block of tasks
+ // doesn't need to recompute
+ check(down, expValue = 10101, expEvaled = Agg())
+ assert(leftCount == 1, middleCount == 1, rightCount == 1)
+
+ // But if `up` changes, the entire block of downstream tasks needs to
+ // recompute together, including `middle` which doesn't depend on `up`,
+ // because tasks have no cached value that can be used. `right`, which
+ // is a cached Target, does not recompute
+ up.inputs(0).asInstanceOf[Test].counter += 1
+ check(down, expValue = 10102, expEvaled = Agg(up, down), extraEvaled = 6)
+ assert(leftCount == 2, middleCount == 2, rightCount == 1)
+
+ // Running the tasks themselves results in them being recomputed every
+ // single time, even if nothing changes
+ check(left, expValue = 2, expEvaled = Agg(), extraEvaled = 1, secondRunNoOp = false)
+ assert(leftCount == 3, middleCount == 2, rightCount == 1)
+ check(left, expValue = 2, expEvaled = Agg(), extraEvaled = 1, secondRunNoOp = false)
+ assert(leftCount == 4, middleCount == 2, rightCount == 1)
+
+ check(middle, expValue = 100, expEvaled = Agg(), extraEvaled = 2, secondRunNoOp = false)
+ assert(leftCount == 4, middleCount == 3, rightCount == 1)
+ check(middle, expValue = 100, expEvaled = Agg(), extraEvaled = 2, secondRunNoOp = false)
+ assert(leftCount == 4, middleCount == 4, rightCount == 1)
+ }
+ }
+ }
+}
diff --git a/main/test/src/eval/FailureTests.scala b/main/test/src/eval/FailureTests.scala
new file mode 100644
index 00000000..dcfbcb60
--- /dev/null
+++ b/main/test/src/eval/FailureTests.scala
@@ -0,0 +1,132 @@
+package mill.eval
+import mill.T
+import mill.util.{TestEvaluator, TestUtil}
+import mill.api.Result.OuterStack
+import utest._
+import utest.framework.TestPath
+
+
+object FailureTests extends TestSuite{
+
+ val tests = Tests{
+ val graphs = new mill.util.TestGraphs()
+ import graphs._
+
+ 'evaluateSingle - {
+ val check = new TestEvaluator(singleton)
+ check.fail(
+ target = singleton.single,
+ expectedFailCount = 0,
+ expectedRawValues = Seq(Result.Success(0))
+ )
+
+ singleton.single.failure = Some("lols")
+
+ check.fail(
+ target = singleton.single,
+ expectedFailCount = 1,
+ expectedRawValues = Seq(Result.Failure("lols"))
+ )
+
+ singleton.single.failure = None
+
+ check.fail(
+ target = singleton.single,
+ expectedFailCount = 0,
+ expectedRawValues = Seq(Result.Success(0))
+ )
+
+
+ val ex = new IndexOutOfBoundsException()
+ singleton.single.exception = Some(ex)
+
+
+ check.fail(
+ target = singleton.single,
+ expectedFailCount = 1,
+ expectedRawValues = Seq(Result.Exception(ex, new OuterStack(Nil)))
+ )
+ }
+ 'evaluatePair - {
+ val check = new TestEvaluator(pair)
+ check.fail(
+ pair.down,
+ expectedFailCount = 0,
+ expectedRawValues = Seq(Result.Success(0))
+ )
+
+ pair.up.failure = Some("lols")
+
+ check.fail(
+ pair.down,
+ expectedFailCount = 1,
+ expectedRawValues = Seq(Result.Skipped)
+ )
+
+ pair.up.failure = None
+
+ check.fail(
+ pair.down,
+ expectedFailCount = 0,
+ expectedRawValues = Seq(Result.Success(0))
+ )
+
+ pair.up.exception = Some(new IndexOutOfBoundsException())
+
+ check.fail(
+ pair.down,
+ expectedFailCount = 1,
+ expectedRawValues = Seq(Result.Skipped)
+ )
+ }
+ 'evaluateBacktickIdentifiers - {
+ val check = new TestEvaluator(bactickIdentifiers)
+ import bactickIdentifiers._
+ check.fail(
+ `a-down-target`,
+ expectedFailCount = 0,
+ expectedRawValues = Seq(Result.Success(0))
+ )
+
+ `up-target`.failure = Some("lols")
+
+ check.fail(
+ `a-down-target`,
+ expectedFailCount = 1,
+ expectedRawValues = Seq(Result.Skipped)
+ )
+
+ `up-target`.failure = None
+
+ check.fail(
+ `a-down-target`,
+ expectedFailCount = 0,
+ expectedRawValues = Seq(Result.Success(0))
+ )
+
+ `up-target`.exception = Some(new IndexOutOfBoundsException())
+
+ check.fail(
+ `a-down-target`,
+ expectedFailCount = 1,
+ expectedRawValues = Seq(Result.Skipped)
+ )
+ }
+ 'multipleUsesOfDest - {
+ object build extends TestUtil.BaseModule {
+ // Using `T.ctx( ).dest` twice in a single task is ok
+ def left = T{ + T.ctx().dest.toString.length + T.ctx().dest.toString.length }
+
+ // Using `T.ctx( ).dest` once in two different tasks is not ok
+ val task = T.task{ T.ctx().dest.toString.length }
+ def right = T{ task() + left() + T.ctx().dest.toString().length }
+ }
+
+ val check = new TestEvaluator(build)
+ val Right(_) = check(build.left)
+ val Left(Result.Exception(e, _)) = check(build.right)
+ assert(e.getMessage.contains("`dest` can only be used in one place"))
+ }
+ }
+}
+
diff --git a/main/test/src/eval/JavaCompileJarTests.scala b/main/test/src/eval/JavaCompileJarTests.scala
new file mode 100644
index 00000000..426c6ea6
--- /dev/null
+++ b/main/test/src/eval/JavaCompileJarTests.scala
@@ -0,0 +1,164 @@
+package mill.eval
+
+import mill.define.{Discover, Input, Target, Task}
+import mill.modules.Jvm
+import mill.api.Ctx.Dest
+import mill.{Module, T}
+import mill.util.{DummyLogger, Loose, TestEvaluator, TestUtil}
+import mill.util.Strict.Agg
+import utest._
+import mill._
+object JavaCompileJarTests extends TestSuite{
+ def compileAll(sources: mill.util.Loose.Agg[PathRef])(implicit ctx: Dest) = {
+ os.makeDir.all(ctx.dest)
+
+ os.proc("javac", sources.map(_.path.toString()).toSeq, "-d", ctx.dest).call(ctx.dest)
+ PathRef(ctx.dest)
+ }
+
+ val tests = Tests{
+ 'javac {
+ val javacSrcPath = os.pwd / 'main / 'test / 'resources / 'examples / 'javac
+ val javacDestPath = TestUtil.getOutPath() / 'src
+
+ os.makeDir.all(javacDestPath / os.up)
+ os.copy(javacSrcPath, javacDestPath)
+
+ object Build extends TestUtil.BaseModule{
+ def sourceRootPath = javacDestPath / 'src
+ def resourceRootPath = javacDestPath / 'resources
+
+ // sourceRoot -> allSources -> classFiles
+ // |
+ // v
+ // resourceRoot ----> jar
+ def sourceRoot = T.sources{ sourceRootPath }
+ def resourceRoot = T.sources{ resourceRootPath }
+ def allSources = T{ sourceRoot().flatMap(p => os.walk(p.path)).map(PathRef(_)) }
+ def classFiles = T{ compileAll(allSources()) }
+ def jar = T{ Jvm.createJar(Loose.Agg(classFiles().path) ++ resourceRoot().map(_.path)) }
+ // Test createJar() with optional file filter.
+ def filterJar(fileFilter: (os.Path, os.RelPath) => Boolean) = T{ Jvm.createJar(Loose.Agg(classFiles().path) ++ resourceRoot().map(_.path), None, fileFilter) }
+
+ def run(mainClsName: String) = T.command{
+ os.proc('java, "-Duser.language=en", "-cp", classFiles().path, mainClsName).call()
+ }
+ }
+
+ import Build._
+
+ var evaluator = new TestEvaluator(Build)
+ def eval[T](t: Task[T]) = {
+ evaluator.apply(t)
+ }
+ def check(targets: Agg[Task[_]], expected: Agg[Task[_]]) = {
+ evaluator.check(targets, expected)
+ }
+
+ def append(path: os.Path, txt: String) = ammonite.ops.write.append(path, txt)
+
+
+ check(
+ targets = Agg(jar),
+ expected = Agg(allSources, classFiles, jar)
+ )
+
+ // Re-running with no changes results in nothing being evaluated
+ check(targets = Agg(jar), expected = Agg())
+ // Appending an empty string gets ignored due to file-content hashing
+ append(sourceRootPath / "Foo.java", "")
+ check(targets = Agg(jar), expected = Agg())
+
+ // Appending whitespace forces a recompile, but the classfilesend up
+ // exactly the same so no re-jarring.
+ append(sourceRootPath / "Foo.java", " ")
+ // Note that `sourceRoot` and `resourceRoot` never turn up in the `expected`
+ // list, because they are `Source`s not `Target`s
+ check(targets = Agg(jar), expected = Agg(/*sourceRoot, */allSources, classFiles))
+
+ // Appending a new class changes the classfiles, which forces us to
+ // re-create the final jar
+ append(sourceRootPath / "Foo.java", "\nclass FooTwo{}")
+ check(targets = Agg(jar), expected = Agg(allSources, classFiles, jar))
+
+ // Tweaking the resources forces rebuild of the final jar, without
+ // recompiling classfiles
+ append(resourceRootPath / "hello.txt", " ")
+ check(targets = Agg(jar), expected = Agg(jar))
+
+ // You can swap evaluators halfway without any ill effects
+ evaluator = new TestEvaluator(Build)
+
+ // Asking for an intermediate target forces things to be build up to that
+ // target only; these are re-used for any downstream targets requested
+ append(sourceRootPath / "Bar.java", "\nclass BarTwo{}")
+ append(resourceRootPath / "hello.txt", " ")
+ check(targets = Agg(classFiles), expected = Agg(allSources, classFiles))
+ check(targets = Agg(jar), expected = Agg(jar))
+ check(targets = Agg(allSources), expected = Agg())
+
+ append(sourceRootPath / "Bar.java", "\nclass BarThree{}")
+ append(resourceRootPath / "hello.txt", " ")
+ check(targets = Agg(resourceRoot), expected = Agg())
+ check(targets = Agg(allSources), expected = Agg(allSources))
+ check(targets = Agg(jar), expected = Agg(classFiles, jar))
+
+ val jarContents = os.proc('jar, "-tf", evaluator.outPath/'jar/'dest/"out.jar").call(evaluator.outPath).out.string
+ val expectedJarContents =
+ """META-INF/MANIFEST.MF
+ |test/Bar.class
+ |test/BarThree.class
+ |test/BarTwo.class
+ |test/Foo.class
+ |test/FooTwo.class
+ |hello.txt
+ |""".stripMargin
+ assert(jarContents.linesIterator.toSeq == expectedJarContents.linesIterator.toSeq)
+
+ // Create the Jar again, but this time, filter out the Foo files.
+ def noFoos(s: String) = !s.contains("Foo")
+ val filterFunc = (p: os.Path, r: os.RelPath) => noFoos(r.last)
+ eval(filterJar(filterFunc))
+ val filteredJarContents = os.proc('jar, "-tf", evaluator.outPath/'filterJar/'dest/"out.jar").call(evaluator.outPath).out.string
+ assert(filteredJarContents.linesIterator.toSeq == expectedJarContents.linesIterator.filter(noFoos(_)).toSeq)
+
+ val executed = os.proc('java, "-cp", evaluator.outPath/'jar/'dest/"out.jar", "test.Foo").call(evaluator.outPath).out.string
+ assert(executed == (31337 + 271828) + System.lineSeparator)
+
+ 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) + System.lineSeparator,
+ evalCount == 1
+ )
+ }
+
+ val Left(Result.Exception(ex, _)) = eval(Build.run("test.BarFour"))
+
+ assert(ex.getMessage.contains("Could not find or load main class"))
+
+ append(
+ sourceRootPath / "Bar.java",
+ """
+ class BarFour{
+ public static void main(String[] args){
+ System.out.println("New Cls!");
+ }
+ }
+ """
+ )
+ val Right((runOutput2, evalCount2)) = eval(Build.run("test.BarFour"))
+ assert(
+ runOutput2.out.string == "New Cls!" + System.lineSeparator,
+ evalCount2 == 3
+ )
+ val Right((runOutput3, evalCount3)) = eval(Build.run("test.BarFour"))
+ assert(
+ runOutput3.out.string == "New Cls!" + System.lineSeparator,
+ evalCount3 == 1
+ )
+ }
+ }
+}
diff --git a/main/test/src/eval/ModuleTests.scala b/main/test/src/eval/ModuleTests.scala
new file mode 100644
index 00000000..f28fc9b6
--- /dev/null
+++ b/main/test/src/eval/ModuleTests.scala
@@ -0,0 +1,45 @@
+package mill.eval
+
+
+import mill.util.{TestEvaluator, TestUtil}
+import mill.T
+import mill.define.Discover
+
+import utest._
+
+object ModuleTests extends TestSuite{
+ object ExternalModule extends mill.define.ExternalModule {
+ def x = T{13}
+ object inner extends mill.Module{
+ def y = T{17}
+ }
+ lazy val millDiscover = Discover[this.type]
+ }
+ object Build extends TestUtil.BaseModule{
+ def z = T{ ExternalModule.x() + ExternalModule.inner.y() }
+ }
+ val tests = Tests {
+ os.remove.all(TestEvaluator.externalOutPath)
+ 'externalModuleTargetsAreNamespacedByModulePackagePath - {
+ val check = new TestEvaluator(Build)
+ val zresult = check.apply(Build.z)
+ assert(
+ zresult == Right((30, 1)),
+ os.read(check.evaluator.outPath / 'z / "meta.json").contains("30"),
+ os.read(TestEvaluator.externalOutPath / 'mill / 'eval / 'ModuleTests / 'ExternalModule / 'x / "meta.json").contains("13"),
+ os.read(TestEvaluator.externalOutPath / 'mill / 'eval / 'ModuleTests / 'ExternalModule / 'inner / 'y / "meta.json").contains("17")
+ )
+ }
+ 'externalModuleMustBeGlobalStatic - {
+
+
+ object Build extends mill.define.ExternalModule {
+
+ def z = T{ ExternalModule.x() + ExternalModule.inner.y() }
+ lazy val millDiscover = Discover[this.type]
+ }
+
+ intercept[java.lang.AssertionError]{ Build }
+ }
+ }
+}
diff --git a/main/test/src/eval/TarjanTests.scala b/main/test/src/eval/TarjanTests.scala
new file mode 100644
index 00000000..2f9d0a4d
--- /dev/null
+++ b/main/test/src/eval/TarjanTests.scala
@@ -0,0 +1,91 @@
+package mill.eval
+
+import utest._
+
+object TarjanTests extends TestSuite{
+ def check(input: Seq[Seq[Int]], expected: Seq[Seq[Int]]) = {
+ val result = Tarjans(input).map(_.sorted)
+ val sortedExpected = expected.map(_.sorted)
+ assert(result == sortedExpected)
+ }
+ val tests = Tests{
+ //
+ 'empty - check(Seq(), Seq())
+
+ // (0)
+ 'singleton - check(Seq(Seq()), Seq(Seq(0)))
+
+
+ // (0)-.
+ // ^._/
+ 'selfCycle - check(Seq(Seq(0)), Seq(Seq(0)))
+
+ // (0) <-> (1)
+ 'simpleCycle- check(Seq(Seq(1), Seq(0)), Seq(Seq(1, 0)))
+
+ // (0) (1) (2)
+ 'multipleSingletons - check(
+ Seq(Seq(), Seq(), Seq()),
+ Seq(Seq(0), Seq(1), Seq(2))
+ )
+
+ // (0) -> (1) -> (2)
+ 'straightLineNoCycles- check(
+ Seq(Seq(1), Seq(2), Seq()),
+ Seq(Seq(2), Seq(1), Seq(0))
+ )
+
+ // (0) <- (1) <- (2)
+ 'straightLineNoCyclesReversed- check(
+ Seq(Seq(), Seq(0), Seq(1)),
+ Seq(Seq(0), Seq(1), Seq(2))
+ )
+
+ // (0) <-> (1) (2) -> (3) -> (4)
+ // ^.____________/
+ 'independentSimpleCycles - check(
+ Seq(Seq(1), Seq(0), Seq(3), Seq(4), Seq(2)),
+ Seq(Seq(1, 0), Seq(4, 3, 2))
+ )
+
+ // ___________________
+ // v \
+ // (0) <-> (1) (2) -> (3) -> (4)
+ // ^.____________/
+ 'independentLinkedCycles - check(
+ Seq(Seq(1), Seq(0), Seq(3), Seq(4), Seq(2, 1)),
+ Seq(Seq(1, 0), Seq(4, 3, 2))
+ )
+ // _____________
+ // / v
+ // (0) <-> (1) (2) -> (3) -> (4)
+ // ^.____________/
+ 'independentLinkedCycles2 - check(
+ Seq(Seq(1, 2), Seq(0), Seq(3), Seq(4), Seq(2)),
+ Seq(Seq(4, 3, 2), Seq(1, 0))
+ )
+
+ // _____________
+ // / v
+ // (0) <-> (1) (2) -> (3) -> (4)
+ // ^. ^.____________/
+ // \________________/
+ 'combinedCycles - check(
+ Seq(Seq(1, 2), Seq(0), Seq(3), Seq(4), Seq(2, 1)),
+ Seq(Seq(4, 3, 2, 1, 0))
+ )
+ //
+ // (0) <-> (1) <- (2) <- (3) <-> (4) <- (5)
+ // ^.____________/ / /
+ // / /
+ // (6) <- (7) <-/ (8) <-'
+ // / /
+ // v /
+ // (9) <--------'
+ 'combinedCycles - check(
+ Seq(Seq(1), Seq(0), Seq(0, 1), Seq(2, 4, 7, 9), Seq(3), Seq(4, 8), Seq(9), Seq(6), Seq(), Seq()),
+ Seq(Seq(0, 1), Seq(2), Seq(9), Seq(6), Seq(7), Seq(3, 4), Seq(8), Seq(5))
+ )
+
+ }
+} \ No newline at end of file
diff --git a/main/test/src/eval/TaskTests.scala b/main/test/src/eval/TaskTests.scala
new file mode 100644
index 00000000..0bfd8efc
--- /dev/null
+++ b/main/test/src/eval/TaskTests.scala
@@ -0,0 +1,95 @@
+package mill.eval
+
+import utest._
+
+import mill.T
+
+import mill.util.TestEvaluator
+object TaskTests extends TestSuite{
+ val tests = Tests{
+ object build extends mill.util.TestUtil.BaseModule{
+ var count = 0
+ // Explicitly instantiate `Function1` objects to make sure we get
+ // different instances each time
+ def staticWorker = T.worker{
+ new Function1[Int, Int] {
+ def apply(v1: Int) = v1 + 1
+ }
+ }
+ def noisyWorker = T.worker{
+ new Function1[Int, Int] {
+ def apply(v1: Int) = input() + 1
+ }
+ }
+ def input = T.input{
+ count += 1
+ count
+ }
+ def task = T.task{
+ count += 1
+ count
+ }
+ def taskInput = T{ input() }
+ def taskNoInput = T{ task() }
+
+ def persistent = T.persistent{
+ input() // force re-computation
+ os.makeDir.all(T.ctx().dest)
+ os.write.append(T.ctx().dest/'count, "hello\n")
+ os.read.lines(T.ctx().dest/'count).length
+ }
+ def nonPersistent = T{
+ input() // force re-computation
+ os.makeDir.all(T.ctx().dest)
+ os.write.append(T.ctx().dest/'count, "hello\n")
+ os.read.lines(T.ctx().dest/'count).length
+ }
+
+ def staticWorkerDownstream = T{
+ staticWorker().apply(1)
+ }
+ def noisyWorkerDownstream = T{
+ noisyWorker().apply(1)
+ }
+ }
+
+ 'inputs - {
+ // Inputs always re-evaluate, including forcing downstream cached Targets
+ // to re-evaluate, but normal Tasks behind a Target run once then are cached
+ val check = new TestEvaluator(build)
+
+ val Right((1, 1)) = check.apply(build.taskInput)
+ val Right((2, 1)) = check.apply(build.taskInput)
+ val Right((3, 1)) = check.apply(build.taskInput)
+
+ val Right((4, 1)) = check.apply(build.taskNoInput)
+ val Right((4, 0)) = check.apply(build.taskNoInput)
+ val Right((4, 0)) = check.apply(build.taskNoInput)
+ }
+
+ 'persistent - {
+ // Persistent tasks keep the working dir around between runs
+ val check = new TestEvaluator(build)
+ val Right((1, 1)) = check.apply(build.persistent)
+ val Right((2, 1)) = check.apply(build.persistent)
+ val Right((3, 1)) = check.apply(build.persistent)
+
+ val Right((1, 1)) = check.apply(build.nonPersistent)
+ val Right((1, 1)) = check.apply(build.nonPersistent)
+ val Right((1, 1)) = check.apply(build.nonPersistent)
+ }
+
+ 'worker - {
+ // Persistent task
+ def check = new TestEvaluator(build)
+
+ val Right((2, 1)) = check.apply(build.noisyWorkerDownstream)
+ val Right((3, 1)) = check.apply(build.noisyWorkerDownstream)
+ val Right((4, 1)) = check.apply(build.noisyWorkerDownstream)
+
+ val Right((2, 1)) = check.apply(build.staticWorkerDownstream)
+ val Right((2, 0)) = check.apply(build.staticWorkerDownstream)
+ val Right((2, 0)) = check.apply(build.staticWorkerDownstream)
+ }
+ }
+}