summaryrefslogtreecommitdiff
path: root/main/test/src/eval/EvaluationTests.scala
diff options
context:
space:
mode:
Diffstat (limited to 'main/test/src/eval/EvaluationTests.scala')
-rw-r--r--main/test/src/eval/EvaluationTests.scala354
1 files changed, 354 insertions, 0 deletions
diff --git a/main/test/src/eval/EvaluationTests.scala b/main/test/src/eval/EvaluationTests.scala
new file mode 100644
index 00000000..7f924db2
--- /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.api.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)
+ }
+ }
+ }
+}