diff options
Diffstat (limited to 'main/test/src/mill')
22 files changed, 2464 insertions, 0 deletions
diff --git a/main/test/src/mill/TestMain.scala b/main/test/src/mill/TestMain.scala new file mode 100644 index 00000000..80e7e627 --- /dev/null +++ b/main/test/src/mill/TestMain.scala @@ -0,0 +1,6 @@ +package mill + +object TestMain { + def main(args: Array[String]): Unit = { + } +} diff --git a/main/test/src/mill/UTestFramework.scala b/main/test/src/mill/UTestFramework.scala new file mode 100644 index 00000000..6c0d5191 --- /dev/null +++ b/main/test/src/mill/UTestFramework.scala @@ -0,0 +1,11 @@ +package mill + +class UTestFramework extends utest.runner.Framework { + override def exceptionStackFrameHighlighter(s: StackTraceElement) = { + s.getClassName.startsWith("mill.") + } + override def setup() = { + import ammonite.ops._ + rm(pwd / 'target / 'workspace) + } +} diff --git a/main/test/src/mill/define/ApplicativeTests.scala b/main/test/src/mill/define/ApplicativeTests.scala new file mode 100644 index 00000000..72b715bb --- /dev/null +++ b/main/test/src/mill/define/ApplicativeTests.scala @@ -0,0 +1,125 @@ +package mill.define + +import mill.define.Applicative.ImplicitStub +import utest._ + +import scala.annotation.compileTimeOnly +import scala.language.experimental.macros + + +object ApplicativeTests extends TestSuite { + implicit def optionToOpt[T](o: Option[T]): Opt[T] = new Opt(o) + class Opt[T](val self: Option[T]) extends Applicative.Applyable[Option, T] + object Opt extends OptGenerated with Applicative.Applyer[Opt, Option, Applicative.Id, String]{ + + val injectedCtx = "helloooo" + def underlying[A](v: Opt[A]) = v.self + def apply[T](t: T): Option[T] = macro Applicative.impl[Option, T, String] + + def mapCtx[A, B](a: Option[A])(f: (A, String) => B): Option[B] = a.map(f(_, injectedCtx)) + def zip() = Some(()) + def zip[A](a: Option[A]) = a.map(Tuple1(_)) + } + class Counter{ + var value = 0 + def apply() = { + value += 1 + value + } + } + @compileTimeOnly("Target.ctx() can only be used with a T{...} block") + @ImplicitStub + implicit def taskCtx: String = ??? + + val tests = Tests{ + + 'selfContained - { + + 'simple - assert(Opt("lol " + 1) == Some("lol 1")) + 'singleSome - assert(Opt("lol " + Some("hello")()) == Some("lol hello")) + 'twoSomes - assert(Opt(Some("lol ")() + Some("hello")()) == Some("lol hello")) + 'singleNone - assert(Opt("lol " + None()) == None) + 'twoNones - assert(Opt("lol " + None() + None()) == None) + } + 'context - { + assert(Opt(Opt.ctx() + Some("World")()) == Some("hellooooWorld")) + } + 'capturing - { + val lol = "lol " + def hell(o: String) = "hell" + o + 'simple - assert(Opt(lol + 1) == Some("lol 1")) + 'singleSome - assert(Opt(lol + Some(hell("o"))()) == Some("lol hello")) + 'twoSomes - assert(Opt(Some(lol)() + Some(hell("o"))()) == Some("lol hello")) + 'singleNone - assert(Opt(lol + None()) == None) + 'twoNones - assert(Opt(lol + None() + None()) == None) + } + 'allowedLocalDef - { + // Although x is defined inside the Opt{...} block, it is also defined + // within the LHS of the Applyable#apply call, so it is safe to life it + // out into the `zipMap` arguments list. + val res = Opt{ "lol " + Some("hello").flatMap(x => Some(x)).apply() } + assert(res == Some("lol hello")) + } + 'upstreamAlwaysEvaluated - { + // Whether or not control-flow reaches the Applyable#apply call inside an + // Opt{...} block, we always evaluate the LHS of the Applyable#apply + // because it gets lifted out of any control flow statements + val counter = new Counter() + def up = Opt{ "lol " + counter() } + val down = Opt{ if ("lol".length > 10) up() else "fail" } + assert( + down == Some("fail"), + counter.value == 1 + ) + } + 'upstreamEvaluatedOnlyOnce - { + // Even if control-flow reaches the Applyable#apply call more than once, + // it only gets evaluated once due to its lifting out of the Opt{...} block + val counter = new Counter() + def up = Opt{ "lol " + counter() } + def runTwice[T](t: => T) = (t, t) + val down = Opt{ runTwice(up()) } + assert( + down == Some(("lol 1", "lol 1")), + counter.value == 1 + ) + } + 'evaluationsInsideLambdasWork - { + // This required some fiddling with owner chains inside the macro to get + // working, so ensure it doesn't regress + val counter = new Counter() + def up = Opt{ "hello" + counter() } + val down1 = Opt{ (() => up())() } + val down2 = Opt{ Seq(1, 2, 3).map(n => up() * n) } + assert( + down1 == Some("hello1"), + down2 == Some(Seq("hello2", "hello2hello2", "hello2hello2hello2")) + ) + } + 'appliesEvaluatedOncePerLexicalCallsite - { + // If you have multiple Applyable#apply() lexically in the source code of + // your Opt{...} call, each one gets evaluated once, even if the LHS of each + // apply() call is identical. It's up to the downstream zipMap() + // implementation to decide if it wants to dedup them or do other things. + val counter = new Counter() + def up = Opt{ "hello" + counter() } + val down = Opt{ Seq(1, 2, 3).map(n => n + up() + up()) } + assert(down == Some(Seq("1hello1hello2", "2hello1hello2", "3hello1hello2"))) + } + 'appliesEvaluateBeforehand - { + // Every Applyable#apply() within a Opt{...} block evaluates before any + // other logic within that block, even if they would happen first in the + // normal Scala evaluation order + val counter = new Counter() + def up = Opt{ counter() } + val down = Opt{ + val res = counter() + val one = up() + val two = up() + val three = up() + (res, one, two, three) + } + assert(down == Some((4, 1, 2, 3))) + } + } +} diff --git a/main/test/src/mill/define/BasePathTests.scala b/main/test/src/mill/define/BasePathTests.scala new file mode 100644 index 00000000..1f5b4037 --- /dev/null +++ b/main/test/src/mill/define/BasePathTests.scala @@ -0,0 +1,68 @@ +package mill.define + +import mill.util.{TestGraphs, TestUtil} +import utest._ +import ammonite.ops._ +import mill.{Module, T} +object BasePathTests extends TestSuite{ + val testGraphs = new TestGraphs + val tests = Tests{ + def check[T <: Module](m: T)(f: T => Module, segments: String*) = { + val remaining = f(m).millSourcePath.relativeTo(m.millSourcePath).segments + assert(remaining == segments) + } + 'singleton - { + check(testGraphs.singleton)(identity) + } + 'separateGroups - { + check(TestGraphs.triangleTask)(identity) + } + 'TraitWithModuleObject - { + check(TestGraphs.TraitWithModuleObject)( + _.TraitModule, + "TraitModule" + ) + } + 'nestedModuleNested - { + check(TestGraphs.nestedModule)(_.nested, "nested") + } + 'nestedModuleInstance - { + check(TestGraphs.nestedModule)(_.classInstance, "classInstance") + } + 'singleCross - { + check(TestGraphs.singleCross)(_.cross, "cross") + check(TestGraphs.singleCross)(_.cross("210"), "cross", "210") + check(TestGraphs.singleCross)(_.cross("211"), "cross", "211") + } + 'doubleCross - { + check(TestGraphs.doubleCross)(_.cross, "cross") + check(TestGraphs.doubleCross)(_.cross("210", "jvm"), "cross", "210", "jvm") + check(TestGraphs.doubleCross)(_.cross("212", "js"), "cross", "212", "js") + } + 'nestedCrosses - { + check(TestGraphs.nestedCrosses)(_.cross, "cross") + check(TestGraphs.nestedCrosses)( + _.cross("210").cross2("js"), + "cross", "210", "cross2", "js" + ) + } + 'overriden - { + object overridenBasePath extends TestUtil.BaseModule { + override def millSourcePath = pwd / 'overridenBasePathRootValue + object nested extends Module{ + override def millSourcePath = super.millSourcePath / 'overridenBasePathNested + object nested extends Module{ + override def millSourcePath = super.millSourcePath / 'overridenBasePathDoubleNested + } + } + } + assert( + overridenBasePath.millSourcePath == pwd / 'overridenBasePathRootValue, + overridenBasePath.nested.millSourcePath == pwd / 'overridenBasePathRootValue / 'nested / 'overridenBasePathNested, + overridenBasePath.nested.nested.millSourcePath == pwd / 'overridenBasePathRootValue / 'nested / 'overridenBasePathNested / 'nested / 'overridenBasePathDoubleNested + ) + } + + } +} + diff --git a/main/test/src/mill/define/CacherTests.scala b/main/test/src/mill/define/CacherTests.scala new file mode 100644 index 00000000..606de846 --- /dev/null +++ b/main/test/src/mill/define/CacherTests.scala @@ -0,0 +1,76 @@ +package mill.define + +import ammonite.ops.pwd +import mill.util.{DummyLogger, TestEvaluator, TestUtil} +import mill.util.Strict.Agg +import mill.T +import mill.eval.Result.Success +import utest._ +import utest.framework.TestPath +import mill.util.TestEvaluator.implicitDisover + +object CacherTests extends TestSuite{ + object Base extends Base + trait Base extends TestUtil.BaseModule{ + def value = T{ 1 } + def result = T{ Success(1) } + } + object Middle extends Middle + trait Middle extends Base{ + def value = T{ super.value() + 2} + def overriden = T{ super.value()} + } + object Terminal extends Terminal + trait Terminal extends Middle{ + override def value = T{ super.value() + 4} + } + + val tests = Tests{ + def eval[T <: TestUtil.BaseModule, V](mapping: T, v: Task[V]) + (implicit discover: Discover[T], tp: TestPath) = { + val evaluator = new TestEvaluator(mapping) + evaluator(v).right.get._1 + } + def check(x: Any, y: Any) = assert(x == y) + + 'simpleDefIsCached - { + Predef.assert(Base.value eq Base.value) + Predef.assert(eval(Base, Base.value) == 1) + } + + 'resultDefIsCached - { + Predef.assert(Base.result eq Base.result) + Predef.assert(eval(Base, Base.result) == 1) + } + + + 'overridingDefIsAlsoCached - { + Predef.assert(eval(Middle, Middle.value) == 3) + Predef.assert(Middle.value eq Middle.value) + } + + 'overridenDefRemainsAvailable - { + Predef.assert(eval(Middle, Middle.overriden) == 1) + } + + + 'multipleOverridesWork- { + Predef.assert(eval(Terminal, Terminal.value) == 7) + Predef.assert(eval(Terminal, Terminal.overriden) == 1) + } + // Doesn't fail, presumably compileError doesn't go far enough in the + // compilation pipeline to hit the override checks + // + // 'overrideOutsideModuleFails - { + // compileError(""" + // trait Foo{ + // def x = 1 + // } + // object Bar extends Foo{ + // def x = 2 + // } + // """) + // } + } +} + diff --git a/main/test/src/mill/define/DiscoverTests.scala b/main/test/src/mill/define/DiscoverTests.scala new file mode 100644 index 00000000..7621169a --- /dev/null +++ b/main/test/src/mill/define/DiscoverTests.scala @@ -0,0 +1,60 @@ +package mill.define + +import mill.util.TestGraphs +import utest._ + +object DiscoverTests extends TestSuite{ + val testGraphs = new TestGraphs + val tests = Tests{ + def check[T <: Module](m: T)(targets: (T => Target[_])*) = { + val discovered = m.millInternal.targets + val expected = targets.map(_(m)).toSet + assert(discovered == expected) + } + 'singleton - { + check(testGraphs.singleton)(_.single) + } + 'separateGroups - { + check(TestGraphs.triangleTask)(_.left, _.right) + } + 'TraitWithModuleObject - { + check(TestGraphs.TraitWithModuleObject)(_.TraitModule.testFramework) + } + 'nestedModule - { + check(TestGraphs.nestedModule)(_.single, _.nested.single, _.classInstance.single) + } + 'singleCross - { + check(TestGraphs.singleCross)( + _.cross("210").suffix, + _.cross("211").suffix, + _.cross("212").suffix + ) + } + 'doubleCross - { + check(TestGraphs.doubleCross)( + _.cross("210", "jvm").suffix, + _.cross("210", "js").suffix, + _.cross("211", "jvm").suffix, + _.cross("211", "js").suffix, + _.cross("212", "jvm").suffix, + _.cross("212", "js").suffix, + _.cross("212", "native").suffix + ) + } + 'nestedCrosses - { + check(TestGraphs.nestedCrosses)( + _.cross("210").cross2("jvm").suffix, + _.cross("210").cross2("js").suffix, + _.cross("210").cross2("native").suffix, + _.cross("211").cross2("jvm").suffix, + _.cross("211").cross2("js").suffix, + _.cross("211").cross2("native").suffix, + _.cross("212").cross2("jvm").suffix, + _.cross("212").cross2("js").suffix, + _.cross("212").cross2("native").suffix + ) + } + + } +} + diff --git a/main/test/src/mill/define/GraphTests.scala b/main/test/src/mill/define/GraphTests.scala new file mode 100644 index 00000000..7e6680be --- /dev/null +++ b/main/test/src/mill/define/GraphTests.scala @@ -0,0 +1,199 @@ +package mill.define + + +import mill.eval.Evaluator +import mill.util.{TestGraphs, TestUtil} +import utest._ +import mill.util.Strict.Agg +object GraphTests extends TestSuite{ + + val tests = Tests{ + + + val graphs = new TestGraphs() + import graphs._ + import TestGraphs._ + + 'topoSortedTransitiveTargets - { + def check(targets: Agg[Task[_]], expected: Agg[Task[_]]) = { + val result = Graph.topoSorted(Graph.transitiveTargets(targets)).values + TestUtil.checkTopological(result) + assert(result == expected) + } + + 'singleton - check( + targets = Agg(singleton.single), + expected = Agg(singleton.single) + ) + 'pair - check( + targets = Agg(pair.down), + expected = Agg(pair.up, pair.down) + ) + 'anonTriple - check( + targets = Agg(anonTriple.down), + expected = Agg(anonTriple.up, anonTriple.down.inputs(0), anonTriple.down) + ) + 'diamond - check( + targets = Agg(diamond.down), + expected = Agg(diamond.up, diamond.left, diamond.right, diamond.down) + ) + 'anonDiamond - check( + targets = Agg(diamond.down), + expected = Agg( + diamond.up, + diamond.down.inputs(0), + diamond.down.inputs(1), + diamond.down + ) + ) + 'defCachedDiamond - check( + targets = Agg(defCachedDiamond.down), + expected = Agg( + defCachedDiamond.up.inputs(0), + defCachedDiamond.up, + defCachedDiamond.down.inputs(0).inputs(0).inputs(0), + defCachedDiamond.down.inputs(0).inputs(0), + defCachedDiamond.down.inputs(0).inputs(1).inputs(0), + defCachedDiamond.down.inputs(0).inputs(1), + defCachedDiamond.down.inputs(0), + defCachedDiamond.down + ) + ) + 'bigSingleTerminal - { + val result = Graph.topoSorted(Graph.transitiveTargets(Agg(bigSingleTerminal.j))).values + TestUtil.checkTopological(result) + assert(result.size == 28) + } + } + + 'groupAroundNamedTargets - { + def check[T, R <: Target[Int]](base: T) + (target: T => R, + important0: Agg[T => Target[_]], + expected: Agg[(R, Int)]) = { + + val topoSorted = Graph.topoSorted(Graph.transitiveTargets(Agg(target(base)))) + + val important = important0.map(_ (base)) + val grouped = Graph.groupAroundImportantTargets(topoSorted) { + case t: Target[_] if important.contains(t) => t + } + val flattened = Agg.from(grouped.values().flatMap(_.items)) + + TestUtil.checkTopological(flattened) + for ((terminal, expectedSize) <- expected) { + val grouping = grouped.lookupKey(terminal) + assert( + grouping.size == expectedSize, + grouping.flatMap(_.asTarget: Option[Target[_]]).filter(important.contains) == Agg(terminal) + ) + } + } + + 'singleton - check(singleton)( + _.single, + Agg(_.single), + Agg(singleton.single -> 1) + ) + 'pair - check(pair)( + _.down, + Agg(_.up, _.down), + Agg(pair.up -> 1, pair.down -> 1) + ) + 'anonTriple - check(anonTriple)( + _.down, + Agg(_.up, _.down), + Agg(anonTriple.up -> 1, anonTriple.down -> 2) + ) + 'diamond - check(diamond)( + _.down, + Agg(_.up, _.left, _.right, _.down), + Agg( + diamond.up -> 1, + diamond.left -> 1, + diamond.right -> 1, + diamond.down -> 1 + ) + ) + + 'defCachedDiamond - check(defCachedDiamond)( + _.down, + Agg(_.up, _.left, _.right, _.down), + Agg( + defCachedDiamond.up -> 2, + defCachedDiamond.left -> 2, + defCachedDiamond.right -> 2, + defCachedDiamond.down -> 2 + ) + ) + + 'anonDiamond - check(anonDiamond)( + _.down, + Agg(_.down, _.up), + Agg( + anonDiamond.up -> 1, + anonDiamond.down -> 3 + ) + ) + 'bigSingleTerminal - check(bigSingleTerminal)( + _.j, + Agg(_.a, _.b, _.e, _.f, _.i, _.j), + Agg( + bigSingleTerminal.a -> 3, + bigSingleTerminal.b -> 2, + bigSingleTerminal.e -> 9, + bigSingleTerminal.i -> 6, + bigSingleTerminal.f -> 4, + bigSingleTerminal.j -> 4 + ) + ) + } + 'multiTerminalGroupCounts - { + def countGroups(goals: Task[_]*) = { + + val topoSorted = Graph.topoSorted( + Graph.transitiveTargets(Agg.from(goals)) + ) + val grouped = Graph.groupAroundImportantTargets(topoSorted) { + case t: NamedTask[Any] => t + case t if goals.contains(t) => t + } + grouped.keyCount + } + + 'separateGroups - { + import separateGroups._ + val groupCount = countGroups(right, left) + assert(groupCount == 3) + } + + 'triangleTask - { + // Make sure the following graph ends up as a single group, since although + // `right` depends on `left`, both of them depend on the un-cached `task` + // which would force them both to re-compute every time `task` changes + import triangleTask._ + val groupCount = countGroups(right, left) + assert(groupCount == 2) + } + + + 'multiTerminalGroup - { + // Make sure the following graph ends up as two groups + import multiTerminalGroup._ + val groupCount = countGroups(right, left) + assert(groupCount == 2) + } + + + 'multiTerminalBoundary - { + // Make sure the following graph ends up as a three groups: one for + // each cached target, and one for the downstream task we are running + import multiTerminalBoundary._ + val groupCount = countGroups(task2) + assert(groupCount == 3) + } + } + + + } +} diff --git a/main/test/src/mill/define/MacroErrorTests.scala b/main/test/src/mill/define/MacroErrorTests.scala new file mode 100644 index 00000000..a389feaa --- /dev/null +++ b/main/test/src/mill/define/MacroErrorTests.scala @@ -0,0 +1,83 @@ +package mill.define + +import utest._ +import mill.{T, Module} +import mill.util.TestUtil +object MacroErrorTests extends TestSuite{ + + val tests = Tests{ + + 'errors{ + val expectedMsg = + "T{} members must be defs defined in a Cacher class/trait/object body" + + val err = compileError("object Foo extends TestUtil.BaseModule{ val x = T{1} }") + assert(err.msg == expectedMsg) + } + + 'badTmacro - { + // Make sure we can reference values from outside the T{...} block as part + // of our `Target#apply()` calls, but we cannot reference any values that + // come from inside the T{...} block + 'pos - { + val e = compileError(""" + val a = T{ 1 } + val arr = Array(a) + val b = { + val c = 0 + T{ + arr(c)() + } + } + """) + assert(e.msg.contains( + "Modules, Targets and Commands can only be defined within a mill Module") + ) + } + 'neg - { + + val expectedMsg = + "Target#apply() call cannot use `value n` defined within the T{...} block" + val err = compileError("""new Module{ + def a = T{ 1 } + val arr = Array(a) + def b = { + T{ + val n = 0 + arr(n)() + } + } + }""") + assert(err.msg == expectedMsg) + } + 'neg2 - { + + val expectedMsg = + "Target#apply() call cannot use `value x` defined within the T{...} block" + val err = compileError("""new Module{ + def a = T{ 1 } + val arr = Array(a) + def b = { + T{ + arr.map{x => x()} + } + } + }""") + assert(err.msg == expectedMsg) + } + 'neg3{ + val borkedCachedDiamond1 = utest.compileError(""" + object borkedCachedDiamond1 { + def up = T{ TestUtil.test() } + def left = T{ TestUtil.test(up) } + def right = T{ TestUtil.test(up) } + def down = T{ TestUtil.test(left, right) } + } + """) + assert(borkedCachedDiamond1.msg.contains( + "Modules, Targets and Commands can only be defined within a mill Module") + ) + } + } + } +} diff --git a/main/test/src/mill/eval/CrossTests.scala b/main/test/src/mill/eval/CrossTests.scala new file mode 100644 index 00000000..aa12e180 --- /dev/null +++ b/main/test/src/mill/eval/CrossTests.scala @@ -0,0 +1,56 @@ +package mill.eval + +import ammonite.ops._ +import mill.define.Discover +import mill.util.TestEvaluator +import mill.util.TestEvaluator.implicitDisover +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/mill/eval/EvaluationTests.scala b/main/test/src/mill/eval/EvaluationTests.scala new file mode 100644 index 00000000..e5f0e57d --- /dev/null +++ b/main/test/src/mill/eval/EvaluationTests.scala @@ -0,0 +1,317 @@ +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 +import mill.util.TestEvaluator.implicitDisover +import ammonite.ops._ + +object EvaluationTests extends TestSuite{ + class Checker[T <: TestUtil.BaseModule](module: T) + (implicit tp: TestPath, discover: Discover[T]) { + // 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)) + } + '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") + ) + } + + '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/mill/eval/FailureTests.scala b/main/test/src/mill/eval/FailureTests.scala new file mode 100644 index 00000000..90cff686 --- /dev/null +++ b/main/test/src/mill/eval/FailureTests.scala @@ -0,0 +1,100 @@ +package mill.eval +import mill.T +import mill.util.{TestEvaluator, TestUtil} +import ammonite.ops.{Path, pwd, rm} +import mill.eval.Result.OuterStack +import utest._ +import utest.framework.TestPath +import mill.util.TestEvaluator.implicitDisover + +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) + ) + } + '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/mill/eval/JavaCompileJarTests.scala b/main/test/src/mill/eval/JavaCompileJarTests.scala new file mode 100644 index 00000000..966c272f --- /dev/null +++ b/main/test/src/mill/eval/JavaCompileJarTests.scala @@ -0,0 +1,159 @@ +package mill.eval + +import ammonite.ops.ImplicitWd._ +import ammonite.ops._ +import mill.define.{Discover, Input, Target, Task} +import mill.modules.Jvm +import mill.util.Ctx.Dest +import mill.{Module, T} +import mill.util.{DummyLogger, Loose, TestEvaluator, TestUtil} +import mill.util.Strict.Agg +import utest._ +import mill._ +import TestEvaluator.implicitDisover +object JavaCompileJarTests extends TestSuite{ + def compileAll(sources: Seq[PathRef])(implicit ctx: Dest) = { + mkdir(ctx.dest) + import ammonite.ops._ + %("javac", sources.map(_.path.toString()), "-d", ctx.dest)(wd = ctx.dest) + PathRef(ctx.dest) + } + + val tests = Tests{ + 'javac { + val javacSrcPath = pwd / 'main / 'test / 'resources / 'examples / 'javac + val javacDestPath = TestUtil.getOutPath() / 'src + + mkdir(javacDestPath / up) + cp(javacSrcPath, javacDestPath) + + object Build extends TestUtil.BaseModule{ + def sourceRootPath = javacDestPath / 'src + def resourceRootPath = javacDestPath / 'resources + + // sourceRoot -> allSources -> classFiles + // | + // v + // resourceRoot ----> jar + def sourceRoot = T.sources{ sourceRootPath } + def resourceRoot = T.sources{ resourceRootPath } + def allSources = T{ sourceRoot().flatMap(p => ls.rec(p.path)).map(PathRef(_)) } + def classFiles = T{ compileAll(allSources()) } + def jar = T{ Jvm.createJar(Loose.Agg(classFiles().path) ++ resourceRoot().map(_.path)) } + + def run(mainClsName: String) = T.command{ + %%('java, "-cp", classFiles().path, mainClsName) + } + } + + 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: 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 = %%('jar, "-tf", evaluator.outPath/'jar/'dest/"out.jar")(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 == expectedJarContents) + + val executed = %%('java, "-cp", evaluator.outPath/'jar/'dest/"out.jar", "test.Foo")(evaluator.outPath).out.string + assert(executed == (31337 + 271828) + "\n") + + for(i <- 0 until 3){ + // Build.run is not cached, so every time we eval it it has to + // re-evaluate + val Right((runOutput, evalCount)) = eval(Build.run("test.Foo")) + assert( + runOutput.out.string == (31337 + 271828) + "\n", + evalCount == 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!\n", + evalCount2 == 3 + ) + val Right((runOutput3, evalCount3)) = eval(Build.run("test.BarFour")) + assert( + runOutput3.out.string == "New Cls!\n", + evalCount3 == 1 + ) + } + } +} diff --git a/main/test/src/mill/eval/ModuleTests.scala b/main/test/src/mill/eval/ModuleTests.scala new file mode 100644 index 00000000..c6125b32 --- /dev/null +++ b/main/test/src/mill/eval/ModuleTests.scala @@ -0,0 +1,45 @@ +package mill.eval + +import ammonite.ops._ +import mill.util.{TestEvaluator, TestUtil} +import mill.T +import mill.define.Discover +import mill.util.TestEvaluator.implicitDisover +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} + } + def millDiscover = Discover[this.type] + } + object Build extends TestUtil.BaseModule{ + def z = T{ ExternalModule.x() + ExternalModule.inner.y() } + } + val tests = Tests { + rm(TestEvaluator.externalOutPath) + 'externalModuleTargetsAreNamespacedByModulePackagePath - { + val check = new TestEvaluator(Build) + + val Right((30, 1)) = check.apply(Build.z) + assert( + read(check.evaluator.outPath / 'z / "meta.json").contains("30"), + read(TestEvaluator.externalOutPath / 'mill / 'eval / 'ModuleTests / 'ExternalModule / 'x / "meta.json").contains("13"), + 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() } + def millDiscover = Discover[this.type] + } + + intercept[java.lang.AssertionError]{ Build } + } + } +} diff --git a/main/test/src/mill/eval/TarjanTests.scala b/main/test/src/mill/eval/TarjanTests.scala new file mode 100644 index 00000000..2f9d0a4d --- /dev/null +++ b/main/test/src/mill/eval/TarjanTests.scala @@ -0,0 +1,91 @@ +package mill.eval + +import utest._ + +object TarjanTests extends TestSuite{ + def check(input: Seq[Seq[Int]], expected: Seq[Seq[Int]]) = { + val result = Tarjans(input).map(_.sorted) + val sortedExpected = expected.map(_.sorted) + assert(result == sortedExpected) + } + val tests = Tests{ + // + 'empty - check(Seq(), Seq()) + + // (0) + 'singleton - check(Seq(Seq()), Seq(Seq(0))) + + + // (0)-. + // ^._/ + 'selfCycle - check(Seq(Seq(0)), Seq(Seq(0))) + + // (0) <-> (1) + 'simpleCycle- check(Seq(Seq(1), Seq(0)), Seq(Seq(1, 0))) + + // (0) (1) (2) + 'multipleSingletons - check( + Seq(Seq(), Seq(), Seq()), + Seq(Seq(0), Seq(1), Seq(2)) + ) + + // (0) -> (1) -> (2) + 'straightLineNoCycles- check( + Seq(Seq(1), Seq(2), Seq()), + Seq(Seq(2), Seq(1), Seq(0)) + ) + + // (0) <- (1) <- (2) + 'straightLineNoCyclesReversed- check( + Seq(Seq(), Seq(0), Seq(1)), + Seq(Seq(0), Seq(1), Seq(2)) + ) + + // (0) <-> (1) (2) -> (3) -> (4) + // ^.____________/ + 'independentSimpleCycles - check( + Seq(Seq(1), Seq(0), Seq(3), Seq(4), Seq(2)), + Seq(Seq(1, 0), Seq(4, 3, 2)) + ) + + // ___________________ + // v \ + // (0) <-> (1) (2) -> (3) -> (4) + // ^.____________/ + 'independentLinkedCycles - check( + Seq(Seq(1), Seq(0), Seq(3), Seq(4), Seq(2, 1)), + Seq(Seq(1, 0), Seq(4, 3, 2)) + ) + // _____________ + // / v + // (0) <-> (1) (2) -> (3) -> (4) + // ^.____________/ + 'independentLinkedCycles2 - check( + Seq(Seq(1, 2), Seq(0), Seq(3), Seq(4), Seq(2)), + Seq(Seq(4, 3, 2), Seq(1, 0)) + ) + + // _____________ + // / v + // (0) <-> (1) (2) -> (3) -> (4) + // ^. ^.____________/ + // \________________/ + 'combinedCycles - check( + Seq(Seq(1, 2), Seq(0), Seq(3), Seq(4), Seq(2, 1)), + Seq(Seq(4, 3, 2, 1, 0)) + ) + // + // (0) <-> (1) <- (2) <- (3) <-> (4) <- (5) + // ^.____________/ / / + // / / + // (6) <- (7) <-/ (8) <-' + // / / + // v / + // (9) <--------' + 'combinedCycles - check( + Seq(Seq(1), Seq(0), Seq(0, 1), Seq(2, 4, 7, 9), Seq(3), Seq(4, 8), Seq(9), Seq(6), Seq(), Seq()), + Seq(Seq(0, 1), Seq(2), Seq(9), Seq(6), Seq(7), Seq(3, 4), Seq(8), Seq(5)) + ) + + } +}
\ No newline at end of file diff --git a/main/test/src/mill/eval/TaskTests.scala b/main/test/src/mill/eval/TaskTests.scala new file mode 100644 index 00000000..114a2910 --- /dev/null +++ b/main/test/src/mill/eval/TaskTests.scala @@ -0,0 +1,95 @@ +package mill.eval + +import utest._ +import ammonite.ops._ +import mill.T +import mill.util.TestEvaluator.implicitDisover +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 + mkdir(T.ctx().dest) + write.append(T.ctx().dest/'count, "hello\n") + read.lines(T.ctx().dest/'count).length + } + def nonPersistent = T{ + input() // force re-computation + mkdir(T.ctx().dest) + write.append(T.ctx().dest/'count, "hello\n") + 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) + } + } +} diff --git a/main/test/src/mill/main/JavaCompileJarTests.scala b/main/test/src/mill/main/JavaCompileJarTests.scala new file mode 100644 index 00000000..fb047675 --- /dev/null +++ b/main/test/src/mill/main/JavaCompileJarTests.scala @@ -0,0 +1,66 @@ +package mill.main + +import ammonite.ops._ +import mill.util.ScriptTestSuite +import utest._ + +object JavaCompileJarTests extends ScriptTestSuite { + def workspaceSlug = "java-compile-jar" + def scriptSourcePath = pwd / 'main / 'test / 'resources / 'examples / 'javac + val tests = Tests{ + initWorkspace() + 'test - { + // Basic target evaluation works + assert(eval("classFiles")) + assert(eval("jar")) + + val classFiles1 = meta("classFiles") + val jar1 = meta("jar") + + assert(eval("classFiles")) + assert(eval("jar")) + + // Repeated evaluation has the same results + val classFiles2 = meta("classFiles") + val jar2 = meta("jar") + + assert( + jar1 == jar2, + classFiles1 == classFiles2 + ) + + // If we update resources, classFiles are unchanged but jar changes + for(scalaFile <- ls.rec(workspacePath).filter(_.ext == "txt")){ + write.append(scalaFile, "\n") + } + + assert(eval("classFiles")) + assert(eval("jar")) + + val classFiles3 = meta("classFiles") + val jar3 = meta("jar") + + assert( + jar2 != jar3, + classFiles2 == classFiles3 + ) + + // We can intentionally break the code, have the targets break, then + // fix the code and have them recover. + for(scalaFile <- ls.rec(workspacePath).filter(_.ext == "java")){ + write.append(scalaFile, "\n}") + } + + assert(!eval("classFiles")) + assert(!eval("jar")) + + for(scalaFile <- ls.rec(workspacePath).filter(_.ext == "java")){ + write.over(scalaFile, read(scalaFile).dropRight(2)) + } + + assert(eval("classFiles")) + assert(eval("jar")) + } + } +} + diff --git a/main/test/src/mill/main/MainTests.scala b/main/test/src/mill/main/MainTests.scala new file mode 100644 index 00000000..157fff6f --- /dev/null +++ b/main/test/src/mill/main/MainTests.scala @@ -0,0 +1,222 @@ +package mill.main + +import mill.define.{Discover, Segment, Task} +import mill.util.TestGraphs._ +import mill.util.TestEvaluator.implicitDisover +import utest._ +object MainTests extends TestSuite{ + + def check[T <: mill.Module](module: T)( + selectorString: String, + expected0: Either[String, Seq[T => Task[_]]]) + (implicit discover: Discover[T])= { + + val expected = expected0.map(_.map(_(module))) + val resolved = for{ + selectors <- mill.util.ParseArgs(Seq(selectorString)).map(_._1.head) + val crossSelectors = selectors._2.value.map{case Segment.Cross(x) => x.toList.map(_.toString) case _ => Nil} + task <- mill.main.Resolve.resolve( + selectors._2.value.toList, module, discover, Nil, crossSelectors.toList, Nil + ) + } yield task + assert(resolved == expected) + } + val tests = Tests{ + val graphs = new mill.util.TestGraphs() + import graphs._ + 'single - { + val check = MainTests.check(singleton) _ + 'pos - check("single", Right(Seq(_.single))) + 'neg1 - check("doesntExist", Left("Cannot resolve task doesntExist")) + 'neg2 - check("single.doesntExist", Left("Cannot resolve module single")) + 'neg3 - check("", Left("Selector cannot be empty")) + } + 'nested - { + val check = MainTests.check(nestedModule) _ + 'pos1 - check("single", Right(Seq(_.single))) + 'pos2 - check("nested.single", Right(Seq(_.nested.single))) + 'pos3 - check("classInstance.single", Right(Seq(_.classInstance.single))) + 'neg1 - check("doesntExist", Left("Cannot resolve task doesntExist")) + 'neg2 - check("single.doesntExist", Left("Cannot resolve module single")) + 'neg3 - check("nested.doesntExist", Left("Cannot resolve task nested.doesntExist")) + 'neg4 - check("classInstance.doesntExist", Left("Cannot resolve task classInstance.doesntExist")) + 'wildcard - check( + "_.single", + Right(Seq( + _.classInstance.single, + _.nested.single + )) + ) + 'wildcardNeg - check( + "_._.single", + Left("Cannot resolve module _") + ) + 'wildcardNeg2 - check( + "_._.__", + Left("Cannot resolve module _") + ) + 'wildcard2 - check( + "__.single", + Right(Seq( + _.single, + _.classInstance.single, + _.nested.single + )) + ) + + 'wildcard3 - check( + "_.__.single", + Right(Seq( + _.classInstance.single, + _.nested.single + )) + ) + + } + 'cross - { + 'single - { + val check = MainTests.check(singleCross) _ + 'pos1 - check("cross[210].suffix", Right(Seq(_.cross("210").suffix))) + 'pos2 - check("cross[211].suffix", Right(Seq(_.cross("211").suffix))) + 'neg1 - check("cross[210].doesntExist", Left("Cannot resolve task cross[210].doesntExist")) + 'neg2 - check("cross[doesntExist].doesntExist", Left("Cannot resolve cross cross[doesntExist]")) + 'neg2 - check("cross[doesntExist].suffix", Left("Cannot resolve cross cross[doesntExist]")) + 'wildcard - check( + "cross[_].suffix", + Right(Seq( + _.cross("210").suffix, + _.cross("211").suffix, + _.cross("212").suffix + )) + ) + 'wildcard2 - check( + "cross[__].suffix", + Right(Seq( + _.cross("210").suffix, + _.cross("211").suffix, + _.cross("212").suffix + )) + ) + } + 'double - { + val check = MainTests.check(doubleCross) _ + 'pos1 - check( + "cross[210,jvm].suffix", + Right(Seq(_.cross("210", "jvm").suffix)) + ) + 'pos2 - check( + "cross[211,jvm].suffix", + Right(Seq(_.cross("211", "jvm").suffix)) + ) + 'wildcard - { + 'labelNeg - check( + "_.suffix", + Left("Cannot resolve module _") + ) + 'labelPos - check( + "__.suffix", + Right(Seq( + _.cross("210", "jvm").suffix, + _.cross("210", "js").suffix, + + _.cross("211", "jvm").suffix, + _.cross("211", "js").suffix, + + _.cross("212", "jvm").suffix, + _.cross("212", "js").suffix, + _.cross("212", "native").suffix + )) + ) + 'first - check( + "cross[_,jvm].suffix", + Right(Seq( + _.cross("210", "jvm").suffix, + _.cross("211", "jvm").suffix, + _.cross("212", "jvm").suffix + )) + ) + 'second - check( + "cross[210,_].suffix", + Right(Seq( + _.cross("210", "jvm").suffix, + _.cross("210", "js").suffix + )) + ) + 'both - check( + "cross[_,_].suffix", + Right(Seq( + _.cross("210", "jvm").suffix, + _.cross("210", "js").suffix, + + _.cross("211", "jvm").suffix, + _.cross("211", "js").suffix, + + _.cross("212", "jvm").suffix, + _.cross("212", "js").suffix, + _.cross("212", "native").suffix + )) + ) + 'both2 - check( + "cross[__].suffix", + Right(Seq( + _.cross("210", "jvm").suffix, + _.cross("210", "js").suffix, + + _.cross("211", "jvm").suffix, + _.cross("211", "js").suffix, + + _.cross("212", "jvm").suffix, + _.cross("212", "js").suffix, + _.cross("212", "native").suffix + )) + ) + } + } + 'nested - { + val check = MainTests.check(nestedCrosses) _ + 'pos1 - check( + "cross[210].cross2[js].suffix", + Right(Seq(_.cross("210").cross2("js").suffix)) + ) + 'pos2 - check( + "cross[211].cross2[jvm].suffix", + Right(Seq(_.cross("211").cross2("jvm").suffix)) + ) + 'wildcard - { + 'first - check( + "cross[_].cross2[jvm].suffix", + Right(Seq( + _.cross("210").cross2("jvm").suffix, + _.cross("211").cross2("jvm").suffix, + _.cross("212").cross2("jvm").suffix + )) + ) + 'second - check( + "cross[210].cross2[_].suffix", + Right(Seq( + _.cross("210").cross2("jvm").suffix, + _.cross("210").cross2("js").suffix, + _.cross("210").cross2("native").suffix + )) + ) + 'both - check( + "cross[_].cross2[_].suffix", + Right(Seq( + _.cross("210").cross2("jvm").suffix, + _.cross("210").cross2("js").suffix, + _.cross("210").cross2("native").suffix, + + _.cross("211").cross2("jvm").suffix, + _.cross("211").cross2("js").suffix, + _.cross("211").cross2("native").suffix, + + _.cross("212").cross2("jvm").suffix, + _.cross("212").cross2("js").suffix, + _.cross("212").cross2("native").suffix + )) + ) + } + } + } + } +} diff --git a/main/test/src/mill/util/ParseArgsTest.scala b/main/test/src/mill/util/ParseArgsTest.scala new file mode 100644 index 00000000..9121bca5 --- /dev/null +++ b/main/test/src/mill/util/ParseArgsTest.scala @@ -0,0 +1,241 @@ +package mill.util + +import mill.define.Segment +import mill.define.Segment.{Cross, Label} +import utest._ + +object ParseArgsTest extends TestSuite { + + val tests = Tests { + 'extractSelsAndArgs - { + def check(input: Seq[String], + expectedSelectors: Seq[String], + expectedArgs: Seq[String], + expectedIsMulti: Boolean) = { + val (selectors, args, isMulti) = ParseArgs.extractSelsAndArgs(input) + + assert( + selectors == expectedSelectors, + args == expectedArgs, + isMulti == expectedIsMulti + ) + } + + 'empty - check(input = Seq.empty, + expectedSelectors = Seq.empty, + expectedArgs = Seq.empty, + expectedIsMulti = false) + 'singleSelector - check( + input = Seq("core.compile"), + expectedSelectors = Seq("core.compile"), + expectedArgs = Seq.empty, + expectedIsMulti = false + ) + 'singleSelectorWithArgs - check( + input = Seq("application.run", "hello", "world"), + expectedSelectors = Seq("application.run"), + expectedArgs = Seq("hello", "world"), + expectedIsMulti = false + ) + 'singleSelectorWithAllInArgs - check( + input = Seq("application.run", "hello", "world", "--all"), + expectedSelectors = Seq("application.run"), + expectedArgs = Seq("hello", "world", "--all"), + expectedIsMulti = false + ) + 'multiSelectors - check( + input = Seq("--all", "core.jar", "core.docsJar", "core.sourcesJar"), + expectedSelectors = Seq("core.jar", "core.docsJar", "core.sourcesJar"), + expectedArgs = Seq.empty, + expectedIsMulti = true + ) + 'multiSelectorsSeq - check( + input = Seq("--seq", "core.jar", "core.docsJar", "core.sourcesJar"), + expectedSelectors = Seq("core.jar", "core.docsJar", "core.sourcesJar"), + expectedArgs = Seq.empty, + expectedIsMulti = true + ) + 'multiSelectorsWithArgs - check( + input = Seq("--all", + "core.compile", + "application.runMain", + "--", + "Main", + "hello", + "world"), + expectedSelectors = Seq("core.compile", "application.runMain"), + expectedArgs = Seq("Main", "hello", "world"), + expectedIsMulti = true + ) + 'multiSelectorsWithArgsWithAllInArgs - check( + input = Seq("--all", + "core.compile", + "application.runMain", + "--", + "Main", + "--all", + "world"), + expectedSelectors = Seq("core.compile", "application.runMain"), + expectedArgs = Seq("Main", "--all", "world"), + expectedIsMulti = true + ) + } + 'expandBraces - { + def check(input: String, expectedExpansion: List[String]) = { + val Right(expanded) = ParseArgs.expandBraces(input) + + assert(expanded == expectedExpansion) + } + + 'expandLeft - check( + "{application,core}.compile", + List("application.compile", "core.compile") + ) + 'expandRight - check( + "application.{jar,docsJar,sourcesJar}", + List("application.jar", "application.docsJar", "application.sourcesJar") + ) + 'expandBoth - check( + "{core,application}.{jar,docsJar}", + List( + "core.jar", + "core.docsJar", + "application.jar", + "application.docsJar" + ) + ) + 'expandNested - { + check("{hello,world.{cow,moo}}", + List("hello", "world.cow", "world.moo")) + check("{a,b{c,d}}", List("a", "bc", "bd")) + check("{a,b,{c,d}}", List("a", "b", "c", "d")) + check("{a,b{c,d{e,f}}}", List("a", "bc", "bde", "bdf")) + check("{a{b,c},d}", List("ab", "ac", "d")) + check("{a,{b,c}d}", List("a", "bd", "cd")) + check("{a{b,c},d{e,f}}", List("ab", "ac", "de", "df")) + check("{a,b{c,d},e{f,g}}", List("a", "bc", "bd", "ef", "eg")) + } + 'expandMixed - check( + "{a,b}.{c}.{}.e", + List("a.{c}.{}.e", "b.{c}.{}.e") + ) + 'malformed - { + val malformed = Seq("core.{compile", "core.{compile,test]") + + malformed.foreach { m => + val Left(error) = ParseArgs.expandBraces(m) + assert(error.contains("Parsing exception")) + } + } + 'dontExpand - { + check("core.compile", List("core.compile")) + check("{}.compile", List("{}.compile")) + check("{core}.compile", List("{core}.compile")) + } + 'keepUnknownSymbols - { + check("{a,b}.e<>", List("a.e<>", "b.e<>")) + check("a[99]&&", List("a[99]&&")) + check( + "{a,b}.<%%>.{c,d}", + List("a.<%%>.c", "a.<%%>.d", "b.<%%>.c", "b.<%%>.d") + ) + } + } + + 'apply - { + def check(input: Seq[String], + expectedSelectors: List[(Option[List[Segment]], List[Segment])], + expectedArgs: Seq[String]) = { + val Right((selectors0, args)) = ParseArgs(input) + + val selectors = selectors0.map{ + case (Some(v1), v2) => (Some(v1.value), v2.value) + case (None, v2) => (None, v2.value) + } + assert( + selectors == expectedSelectors, + args == expectedArgs + ) + } + + 'rejectEmpty { + assert(ParseArgs(Seq.empty) == Left("Selector cannot be empty")) + } + 'singleSelector - check( + input = Seq("core.compile"), + expectedSelectors = List( + None -> List(Label("core"), Label("compile")) + ), + expectedArgs = Seq.empty + ) + 'externalSelector - check( + input = Seq("foo.bar/core.compile"), + expectedSelectors = List( + Some(List(Label("foo"), Label("bar"))) -> List(Label("core"), Label("compile")) + ), + expectedArgs = Seq.empty + ) + 'singleSelectorWithArgs - check( + input = Seq("application.run", "hello", "world"), + expectedSelectors = List( + None -> List(Label("application"), Label("run")) + ), + expectedArgs = Seq("hello", "world") + ) + 'singleSelectorWithCross - check( + input = Seq("bridges[2.12.4,jvm].compile"), + expectedSelectors = List( + None -> List(Label("bridges"), Cross(Seq("2.12.4", "jvm")), Label("compile")) + ), + expectedArgs = Seq.empty + ) + 'multiSelectorsBraceExpansion - check( + input = Seq("--all", "{core,application}.compile"), + expectedSelectors = List( + None -> List(Label("core"), Label("compile")), + None -> List(Label("application"), Label("compile")) + ), + expectedArgs = Seq.empty + ) + 'multiSelectorsBraceExpansionWithArgs - check( + input = Seq("--all", "{core,application}.run", "--", "hello", "world"), + expectedSelectors = List( + None -> List(Label("core"), Label("run")), + None -> List(Label("application"), Label("run")) + ), + expectedArgs = Seq("hello", "world") + ) + 'multiSelectorsBraceExpansionWithCross - check( + input = Seq("--all", "bridges[2.12.4,jvm].{test,jar}"), + expectedSelectors = List( + None -> List(Label("bridges"), Cross(Seq("2.12.4", "jvm")), Label("test")), + None -> List(Label("bridges"), Cross(Seq("2.12.4", "jvm")), Label("jar")) + ), + expectedArgs = Seq.empty + ) + 'multiSelectorsBraceExpansionInsideCross - check( + input = Seq("--all", "bridges[{2.11.11,2.11.8}].jar"), + expectedSelectors = List( + None -> List(Label("bridges"), Cross(Seq("2.11.11")), Label("jar")), + None -> List(Label("bridges"), Cross(Seq("2.11.8")), Label("jar")) + ), + expectedArgs = Seq.empty + ) + 'multiSelectorsBraceExpansionWithoutAll - { + assert( + ParseArgs(Seq("{core,application}.compile")) == Left( + "Please use --all flag to run multiple tasks") + ) + } + 'multiSelectorsWithoutAllAsSingle - check( + // this is how it works when we pass multiple tasks without --all flag + input = Seq("core.compile", "application.compile"), + expectedSelectors = List( + None -> List(Label("core"), Label("compile")) + ), + expectedArgs = Seq("application.compile") + ) + } + } + +} diff --git a/main/test/src/mill/util/ScriptTestSuite.scala b/main/test/src/mill/util/ScriptTestSuite.scala new file mode 100644 index 00000000..916d3af6 --- /dev/null +++ b/main/test/src/mill/util/ScriptTestSuite.scala @@ -0,0 +1,38 @@ +package mill.util + +import java.io.{ByteArrayInputStream, ByteArrayOutputStream, PrintStream} + +import ammonite.ops._ +import mill.util.ParseArgs +import utest._ + +abstract class ScriptTestSuite extends TestSuite{ + def workspaceSlug: String + def scriptSourcePath: Path + + val workspacePath = pwd / 'target / 'workspace / workspaceSlug + val stdOutErr = new PrintStream(new ByteArrayOutputStream()) +// val stdOutErr = new PrintStream(System.out) + val stdIn = new ByteArrayInputStream(Array()) + val runner = new mill.main.MainRunner( + ammonite.main.Cli.Config(wd = workspacePath), false, + stdOutErr, stdOutErr, stdIn + ) + def eval(s: String*) = runner.runScript(workspacePath / "build.sc", s.toList) + def meta(s: String) = { + val (List(selector), args) = ParseArgs.apply(Seq(s)).right.get + + read(workspacePath / "out" / selector._2.value.flatMap(_.pathSegments) / "meta.json") + } + + + def initWorkspace() = { + rm(workspacePath) + mkdir(workspacePath / up) + // The unzipped git repo snapshots we get from github come with a + // wrapper-folder inside the zip file, so copy the wrapper folder to the + // destination instead of the folder containing the wrapper. + + cp(scriptSourcePath, workspacePath) + } +} diff --git a/main/test/src/mill/util/TestEvaluator.scala b/main/test/src/mill/util/TestEvaluator.scala new file mode 100644 index 00000000..a5be0488 --- /dev/null +++ b/main/test/src/mill/util/TestEvaluator.scala @@ -0,0 +1,83 @@ +package mill.util + +import ammonite.ops.{Path, pwd} +import mill.define.Discover.applyImpl +import mill.define.{Discover, Input, Target, Task} +import mill.eval.Result.OuterStack +import mill.eval.{Evaluator, Result} +import mill.util.Strict.Agg +import utest.assert +import utest.framework.TestPath + +import language.experimental.macros +object TestEvaluator{ + implicit def implicitDisover[T]: Discover[T] = macro applyImpl[T] + val externalOutPath = pwd / 'target / 'external + + + def static[T <: TestUtil.BaseModule](module: T) + (implicit discover: Discover[T], + fullName: sourcecode.FullName) = { + new TestEvaluator[T](module)(discover, fullName, TestPath(Nil)) + } +} + +class TestEvaluator[T <: TestUtil.BaseModule](module: T) + (implicit discover: Discover[T], + fullName: sourcecode.FullName, + tp: TestPath){ + val outPath = TestUtil.getOutPath() + + val logger = DummyLogger +// val logger = new PrintLogger(true, ammonite.util.Colors.Default, System.out, System.out, System.err) + val evaluator = new Evaluator(outPath, TestEvaluator.externalOutPath, module, discover, logger) + + def apply[T](t: Task[T]): Either[Result.Failing[T], (T, Int)] = { + val evaluated = evaluator.evaluate(Agg(t)) + + if (evaluated.failing.keyCount == 0) { + Right( + Tuple2( + evaluated.rawValues.head.asInstanceOf[Result.Success[T]].value, + evaluated.evaluated.collect { + case t: Target[_] + if module.millInternal.targets.contains(t) + && !t.isInstanceOf[Input[_]] + && !t.ctx.external => t + case t: mill.define.Command[_] => t + }.size + )) + } else { + Left( + evaluated.failing.lookupKey(evaluated.failing.keys().next).items.next() + .asInstanceOf[Result.Failing[T]] + ) + } + } + + def fail(target: Target[_], expectedFailCount: Int, expectedRawValues: Seq[Result[_]]) = { + + val res = evaluator.evaluate(Agg(target)) + + val cleaned = res.rawValues.map{ + case Result.Exception(ex, _) => Result.Exception(ex, new OuterStack(Nil)) + case x => x + } + + assert( + cleaned == expectedRawValues, + res.failing.keyCount == expectedFailCount + ) + + } + + def check(targets: Agg[Task[_]], expected: Agg[Task[_]]) = { + val evaluated = evaluator.evaluate(targets) + .evaluated + .flatMap(_.asTarget) + .filter(module.millInternal.targets.contains) + .filter(!_.isInstanceOf[Input[_]]) + assert(evaluated == expected) + } + +} diff --git a/main/test/src/mill/util/TestGraphs.scala b/main/test/src/mill/util/TestGraphs.scala new file mode 100644 index 00000000..581d5e0a --- /dev/null +++ b/main/test/src/mill/util/TestGraphs.scala @@ -0,0 +1,242 @@ +package mill.util +import TestUtil.test +import mill.define.Cross +import mill.{Module, T} + +/** + * Example dependency graphs for us to use in our test suite. + * + * The graphs using `test()` live in the `class` and need to be instantiated + * every time you use them, because they are mutable (you can poke at the + * `test`'s `counter`/`failure`/`exception` fields to test various graph + * evaluation scenarios. + * + * The immutable graphs, used for testing discovery & target resolution, + * live in the companion object. + */ +class TestGraphs(){ + // single + object singleton extends TestUtil.BaseModule { + val single = test() + } + + // up---down + object pair extends TestUtil.BaseModule{ + val up = test() + val down = test(up) + } + + // up---o---down + object anonTriple extends TestUtil.BaseModule { + val up = test() + val down = test(test.anon(up)) + } + + // left + // / \ + // up down + // \ / + // right + object diamond extends TestUtil.BaseModule { + val up = test() + val left = test(up) + val right = test(up) + val down = test(left, right) + } + + // o + // / \ + // up down + // \ / + // o + object anonDiamond extends TestUtil.BaseModule { + val up = test() + val down = test(test.anon(up), test.anon(up)) + } + + object defCachedDiamond extends TestUtil.BaseModule { + def up = T{ test() } + def left = T{ test(up) } + def right = T{ test(up) } + def down = T{ test(left, right) } + } + + + object borkedCachedDiamond2 extends TestUtil.BaseModule { + def up = test() + def left = test(up) + def right = test(up) + def down = test(left, right) + } + + object borkedCachedDiamond3 extends TestUtil.BaseModule { + def up = test() + def left = test(up) + def right = test(up) + def down = test(left, right) + } + + // o g-----o + // \ \ \ + // o o h-----I---o + // \ / \ / \ / \ \ + // A---c--o E o-o \ \ + // / \ / \ / \ o---J + // o d o--o o / / + // \ / \ / / + // o o---F---o + // / / + // o--B o + object bigSingleTerminal extends TestUtil.BaseModule { + val a = test(test.anon(), test.anon()) + val b = test(test.anon()) + val e = { + val c = test.anon(a) + val d = test.anon(a) + test( + test.anon(test.anon(), test.anon(c)), + test.anon(test.anon(c, test.anon(d, b))) + ) + } + val f = test(test.anon(test.anon(), test.anon(e))) + + val i = { + val g = test.anon() + val h = test.anon(g, e) + test(test.anon(g), test.anon(test.anon(h))) + } + val j = test(test.anon(i), test.anon(i, f), test.anon(f)) + } + // _ left _ + // / \ + // task1 -------- right + // _/ + // change - task2 + object separateGroups extends TestUtil.BaseModule { + val task1 = T.task{ 1 } + def left = T{ task1() } + val change = test() + val task2 = T.task{ change() } + def right = T{ task1() + task2() + left() + 1 } + + } +} + + +object TestGraphs{ + // _ left _ + // / \ + // task -------- right + object triangleTask extends TestUtil.BaseModule { + val task = T.task{ 1 } + def left = T{ task() } + def right = T{ task() + left() + 1 } + } + + + // _ left + // / + // task -------- right + object multiTerminalGroup extends TestUtil.BaseModule { + val task = T.task{ 1 } + def left = T{ task() } + def right = T{ task() } + } + + // _ left _____________ + // / \ \ + // task1 -------- right ----- task2 + object multiTerminalBoundary extends TestUtil.BaseModule { + val task1 = T.task{ 1 } + def left = T{ task1() } + def right = T{ task1() + left() + 1 } + val task2 = T.task{ left() + right() } + } + + + trait CanNest extends Module{ + def single = T{ 1 } + def invisible: Any = T{ 2 } + def invisible2: mill.define.Task[Int] = T{ 3 } + def invisible3: mill.define.Task[_] = T{ 4 } + } + object nestedModule extends TestUtil.BaseModule { + def single = T{ 5 } + def invisible: Any = T{ 6 } + object nested extends Module{ + def single = T{ 7 } + def invisible: Any = T{ 8 } + + } + object classInstance extends CanNest + + } + + trait BaseModule extends Module { + def foo = T{ Seq("base") } + def cmd(i: Int) = T.command{ Seq("base" + i) } + } + + object canOverrideSuper extends TestUtil.BaseModule with BaseModule { + override def foo = T{ super.foo() ++ Seq("object") } + override def cmd(i: Int) = T.command{ super.cmd(i)() ++ Seq("object" + i) } + } + + trait TraitWithModule extends Module{ outer => + object TraitModule extends Module{ + def testFramework = T{ "mill.UTestFramework" } + def test() = T.command{ ()/*donothing*/ } + } + } + + + // Make sure nested objects inherited from traits work + object TraitWithModuleObject extends TestUtil.BaseModule with TraitWithModule + + + object singleCross extends TestUtil.BaseModule { + object cross extends mill.Cross[Cross]("210", "211", "212") + class Cross(scalaVersion: String) extends Module{ + def suffix = T{ scalaVersion } + } + } + object crossResolved extends TestUtil.BaseModule { + trait MyModule extends Module{ + def crossVersion: String + implicit object resolver extends mill.define.Cross.Resolver[MyModule]{ + def resolve[V <: MyModule](c: Cross[V]): V = c.itemMap(List(crossVersion)) + } + } + + object foo extends mill.Cross[FooModule]("2.10", "2.11", "2.12") + class FooModule(val crossVersion: String) extends MyModule{ + def suffix = T{ crossVersion } + } + + object bar extends mill.Cross[BarModule]("2.10", "2.11", "2.12") + class BarModule(val crossVersion: String) extends MyModule{ + def longSuffix = T{ "_" + foo().suffix() } + } + } + object doubleCross extends TestUtil.BaseModule { + val crossMatrix = for{ + scalaVersion <- Seq("210", "211", "212") + platform <- Seq("jvm", "js", "native") + if !(platform == "native" && scalaVersion != "212") + } yield (scalaVersion, platform) + object cross extends mill.Cross[Cross](crossMatrix:_*) + class Cross(scalaVersion: String, platform: String) extends Module{ + def suffix = T{ scalaVersion + "_" + platform } + } + } + + object nestedCrosses extends TestUtil.BaseModule { + object cross extends mill.Cross[Cross]("210", "211", "212") + class Cross(scalaVersion: String) extends mill.Module{ + object cross2 extends mill.Cross[Cross]("jvm", "js", "native") + class Cross(platform: String) extends mill.Module{ + def suffix = T{ scalaVersion + "_" + platform } + } + } + } +} diff --git a/main/test/src/mill/util/TestUtil.scala b/main/test/src/mill/util/TestUtil.scala new file mode 100644 index 00000000..b30d5d51 --- /dev/null +++ b/main/test/src/mill/util/TestUtil.scala @@ -0,0 +1,81 @@ +package mill.util + +import mill.util.Router.Overrides +import ammonite.ops.pwd +import mill.define._ +import mill.eval.Result +import mill.eval.Result.OuterStack +import utest.assert +import mill.util.Strict.Agg +import utest.framework.TestPath + +import scala.collection.mutable + +object TestUtil { + def getOutPath()(implicit fullName: sourcecode.FullName, + tp: TestPath) = { + pwd / 'target / 'workspace / (fullName.value.split('.') ++ tp.value) + } + def getOutPathStatic()(implicit fullName: sourcecode.FullName) = { + pwd / 'target / 'workspace / fullName.value.split('.') + } + + def getSrcPathStatic()(implicit fullName: sourcecode.FullName) = { + pwd / 'target / 'worksources / fullName.value.split('.') + } + def getSrcPathBase() = { + pwd / 'target / 'worksources + } + + class BaseModule(implicit millModuleEnclosing0: sourcecode.Enclosing, + millModuleLine0: sourcecode.Line, + millName0: sourcecode.Name, + overrides: Overrides) + extends mill.define.BaseModule(getSrcPathBase() / millModuleEnclosing0.value.split("\\.| |#")){ + def millDiscover: Discover[this.type] = Discover[this.type] + } + + object test{ + + def anon(inputs: Task[Int]*) = new Test(inputs) + def apply(inputs: Task[Int]*) + (implicit ctx: mill.define.Ctx)= { + new TestTarget(inputs, pure = inputs.nonEmpty) + } + } + + class Test(val inputs: Seq[Task[Int]]) extends Task[Int]{ + var counter = 0 + var failure = Option.empty[String] + var exception = Option.empty[Throwable] + override def evaluate(args: Ctx) = { + failure.map(Result.Failure(_)) orElse + exception.map(Result.Exception(_, new OuterStack(Nil))) getOrElse + Result.Success(counter + args.args.map(_.asInstanceOf[Int]).sum) + } + override def sideHash = counter + failure.hashCode() + exception.hashCode() + } + /** + * A dummy target that takes any number of inputs, and whose output can be + * controlled externally, so you can construct arbitrary dataflow graphs and + * test how changes propagate. + */ + class TestTarget(inputs: Seq[Task[Int]], + val pure: Boolean) + (implicit ctx0: mill.define.Ctx) + extends Test(inputs) with Target[Int]{ + val ctx = ctx0.copy(segments = ctx0.segments ++ Seq(ctx0.segment)) + val readWrite = upickle.default.IntRW + + + } + def checkTopological(targets: Agg[Task[_]]) = { + val seen = mutable.Set.empty[Task[_]] + for(t <- targets.indexed.reverseIterator){ + seen.add(t) + for(upstream <- t.inputs){ + assert(!seen(upstream)) + } + } + } +} |