diff options
Diffstat (limited to 'core/test')
20 files changed, 1891 insertions, 0 deletions
diff --git a/core/test/resources/examples/javac/resources/hello.txt b/core/test/resources/examples/javac/resources/hello.txt new file mode 100644 index 00000000..5e1c309d --- /dev/null +++ b/core/test/resources/examples/javac/resources/hello.txt @@ -0,0 +1 @@ +Hello World
\ No newline at end of file diff --git a/core/test/resources/examples/javac/src/Bar.java b/core/test/resources/examples/javac/src/Bar.java new file mode 100644 index 00000000..4e30c89b --- /dev/null +++ b/core/test/resources/examples/javac/src/Bar.java @@ -0,0 +1,4 @@ +package test; +public class Bar{ + static int value = 271828; +}
\ No newline at end of file diff --git a/core/test/resources/examples/javac/src/Foo.java b/core/test/resources/examples/javac/src/Foo.java new file mode 100644 index 00000000..e694f9fa --- /dev/null +++ b/core/test/resources/examples/javac/src/Foo.java @@ -0,0 +1,7 @@ +package test; +public class Foo{ + static int value = 31337; + public static void main(String[] args){ + System.out.println(value + Bar.value); + } +}
\ No newline at end of file diff --git a/core/test/src/mill/TestMain.scala b/core/test/src/mill/TestMain.scala new file mode 100644 index 00000000..80e7e627 --- /dev/null +++ b/core/test/src/mill/TestMain.scala @@ -0,0 +1,6 @@ +package mill + +object TestMain { + def main(args: Array[String]): Unit = { + } +} diff --git a/core/test/src/mill/UTestFramework.scala b/core/test/src/mill/UTestFramework.scala new file mode 100644 index 00000000..6c0d5191 --- /dev/null +++ b/core/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/core/test/src/mill/define/ApplicativeTests.scala b/core/test/src/mill/define/ApplicativeTests.scala new file mode 100644 index 00000000..72b715bb --- /dev/null +++ b/core/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/core/test/src/mill/define/BasePathTests.scala b/core/test/src/mill/define/BasePathTests.scala new file mode 100644 index 00000000..a0be4762 --- /dev/null +++ b/core/test/src/mill/define/BasePathTests.scala @@ -0,0 +1,50 @@ +package mill.define + +import mill.util.TestGraphs +import utest._ +import ammonite.ops._ +object BasePathTests extends TestSuite{ + val testGraphs = new TestGraphs + val tests = Tests{ + def check(m: Module, segments: String*) = { + val remaining = m.basePath.relativeTo(pwd).segments.drop(1) + assert(remaining == segments) + } + 'singleton - { + check(testGraphs.singleton) + } + 'separateGroups - { + check(TestGraphs.triangleTask) + } + '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" + ) + } + + } +} + diff --git a/core/test/src/mill/define/CacherTests.scala b/core/test/src/mill/define/CacherTests.scala new file mode 100644 index 00000000..eb981d46 --- /dev/null +++ b/core/test/src/mill/define/CacherTests.scala @@ -0,0 +1,76 @@ +package mill.define + +import mill.eval.Evaluator +import mill.util.{DummyLogger, TestUtil} +import mill.util.Strict.Agg +import mill.T +import mill.eval.Result.Success +import utest._ +import utest.framework.TestPath + +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[V](mapping: mill.Module, v: Task[V])(implicit tp: TestPath) = { + val workspace = ammonite.ops.pwd / 'target / 'workspace / tp.value + val evaluator = new Evaluator(workspace, ammonite.ops.pwd, mapping, DummyLogger) + evaluator.evaluate(Agg(v)).values(0) + } + + 'simpleDefIsCached - assert( + Base.value eq Base.value, + eval(Base, Base.value) == 1 + ) + + 'resultDefIsCached - assert( + Base.result eq Base.result, + eval(Base, Base.result) == 1 + ) + + + 'overridingDefIsAlsoCached - assert( + eval(Middle, Middle.value) == 3, + Middle.value eq Middle.value + ) + + 'overridenDefRemainsAvailable - assert( + eval(Middle, Middle.overriden) == 1 + ) + + + 'multipleOverridesWork- assert( + eval(Terminal, Terminal.value) == 7, + eval(Terminal, Terminal.overriden) == 1 + ) + // 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/core/test/src/mill/define/DiscoverTests.scala b/core/test/src/mill/define/DiscoverTests.scala new file mode 100644 index 00000000..7621169a --- /dev/null +++ b/core/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/core/test/src/mill/define/GraphTests.scala b/core/test/src/mill/define/GraphTests.scala new file mode 100644 index 00000000..7e6680be --- /dev/null +++ b/core/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/core/test/src/mill/define/MacroErrorTests.scala b/core/test/src/mill/define/MacroErrorTests.scala new file mode 100644 index 00000000..a389feaa --- /dev/null +++ b/core/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/core/test/src/mill/eval/EvaluationTests.scala b/core/test/src/mill/eval/EvaluationTests.scala new file mode 100644 index 00000000..0d14988a --- /dev/null +++ b/core/test/src/mill/eval/EvaluationTests.scala @@ -0,0 +1,270 @@ +package mill.eval + + +import mill.util.TestUtil.{Test, test} +import mill.define.{Graph, Target, Task} +import mill.{Module, T} +import mill.util.{DummyLogger, TestGraphs, TestUtil} +import mill.util.Strict.Agg +import utest._ +import utest.framework.TestPath + +object EvaluationTests extends TestSuite{ + class Checker(module: mill.Module)(implicit tp: TestPath) { + val workspace = ammonite.ops.pwd / 'target / 'workspace / tp.value + ammonite.ops.rm(ammonite.ops.Path(workspace, ammonite.ops.pwd)) + // Make sure data is persisted even if we re-create the evaluator each time + def evaluator = new Evaluator(workspace, ammonite.ops.pwd, module, DummyLogger) + + 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{ + val graphs = new 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 - { + import canOverrideSuper._ + + val checker = new Checker(canOverrideSuper) + checker(foo, Seq("base", "object"), Agg(foo), extraEvaled = -1) + } + + '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/core/test/src/mill/eval/FailureTests.scala b/core/test/src/mill/eval/FailureTests.scala new file mode 100644 index 00000000..12ed345d --- /dev/null +++ b/core/test/src/mill/eval/FailureTests.scala @@ -0,0 +1,109 @@ +package mill.eval + +import mill.define.Target +import mill.util.DummyLogger +import mill.util.Strict.Agg +import utest._ +import utest.framework.TestPath + +object FailureTests extends TestSuite{ + + def workspace(implicit tp: TestPath) = { + ammonite.ops.pwd / 'target / 'workspace / 'failure / implicitly[TestPath].value + } + class Checker(module: mill.Module)(implicit tp: TestPath){ + + val evaluator = new Evaluator(workspace, ammonite.ops.pwd, module, DummyLogger) + + def apply(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, Nil) + case x => x + } + + assert( + cleaned == expectedRawValues, + res.failing.keyCount == expectedFailCount + ) + + } + } + val tests = Tests{ + val graphs = new mill.util.TestGraphs() + import graphs._ + + 'evaluateSingle - { + ammonite.ops.rm(ammonite.ops.Path(workspace, ammonite.ops.pwd)) + val check = new Checker(singleton) + check( + target = singleton.single, + expectedFailCount = 0, + expectedRawValues = Seq(Result.Success(0)) + ) + + singleton.single.failure = Some("lols") + + check( + target = singleton.single, + expectedFailCount = 1, + expectedRawValues = Seq(Result.Failure("lols")) + ) + + singleton.single.failure = None + + check( + target = singleton.single, + expectedFailCount = 0, + expectedRawValues = Seq(Result.Success(0)) + ) + + + val ex = new IndexOutOfBoundsException() + singleton.single.exception = Some(ex) + + + check( + target = singleton.single, + expectedFailCount = 1, + expectedRawValues = Seq(Result.Exception(ex, Nil)) + ) + } + 'evaluatePair - { + ammonite.ops.rm(ammonite.ops.Path(workspace, ammonite.ops.pwd)) + val check = new Checker(pair) + check( + pair.down, + expectedFailCount = 0, + expectedRawValues = Seq(Result.Success(0)) + ) + + pair.up.failure = Some("lols") + + check( + pair.down, + expectedFailCount = 1, + expectedRawValues = Seq(Result.Skipped) + ) + + pair.up.failure = None + + check( + pair.down, + expectedFailCount = 0, + expectedRawValues = Seq(Result.Success(0)) + ) + + pair.up.exception = Some(new IndexOutOfBoundsException()) + + check( + pair.down, + expectedFailCount = 1, + expectedRawValues = Seq(Result.Skipped) + ) + } + } +} + diff --git a/core/test/src/mill/eval/JavaCompileJarTests.scala b/core/test/src/mill/eval/JavaCompileJarTests.scala new file mode 100644 index 00000000..5dd9cae1 --- /dev/null +++ b/core/test/src/mill/eval/JavaCompileJarTests.scala @@ -0,0 +1,177 @@ +package mill.eval + +import ammonite.ops.ImplicitWd._ +import ammonite.ops._ +import mill.define.{Input, Target, Task} +import mill.modules.Jvm +import mill.util.Ctx.DestCtx +import mill.{Module, T} +import mill.util.{DummyLogger, Loose, TestUtil} +import mill.util.Strict.Agg +import utest._ +import mill._ + +object JavaCompileJarTests extends TestSuite{ + def compileAll(sources: Seq[PathRef])(implicit ctx: DestCtx) = { + 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 workspacePath = pwd / 'target / 'workspace / 'javac + val javacSrcPath = pwd / 'core / 'test / 'resources / 'examples / 'javac + val javacDestPath = workspacePath / 'src + + mkdir(pwd / 'target / 'workspace / 'javac) + 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.source{ sourceRootPath } + def resourceRoot = T.source{ resourceRootPath } + def allSources = T{ ls.rec(sourceRoot().path).map(PathRef(_)) } + def classFiles = T{ compileAll(allSources()) } + def jar = T{ Jvm.createJar(Loose.Agg(resourceRoot().path, classFiles().path)) } + + def run(mainClsName: String) = T.command{ + %%('java, "-cp", classFiles().path, mainClsName) + } + } + + import Build._ + + def eval[T](t: Task[T]) = { + val evaluator = new Evaluator(workspacePath, pwd, Build, DummyLogger) + val evaluated = evaluator.evaluate(Agg(t)) + + if (evaluated.failing.keyCount == 0){ + Right(Tuple2( + evaluated.rawValues(0).asInstanceOf[Result.Success[T]].value, + evaluated.evaluated.collect{ + case t: Target[_] if Build.millInternal.targets.contains(t) => t + case t: mill.define.Command[_] => t + }.size + )) + }else{ + Left(evaluated.failing.lookupKey(evaluated.failing.keys().next).items.next()) + } + + } + def check(targets: Agg[Task[_]], expected: Agg[Task[_]]) = { + val evaluator = new Evaluator(workspacePath, pwd, Build, DummyLogger) + + val evaluated = evaluator.evaluate(targets) + .evaluated + .flatMap(_.asTarget) + .filter(Build.millInternal.targets.contains) + .filter(!_.isInstanceOf[Input[_]]) + assert(evaluated == 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)) + + // 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", workspacePath/'jar/'dest)(workspacePath).out.string + val expectedJarContents = + """META-INF/MANIFEST.MF + |hello.txt + |test/Bar.class + |test/BarThree.class + |test/BarTwo.class + |test/Foo.class + |test/FooTwo.class + |""".stripMargin + assert(jarContents == expectedJarContents) + + val executed = %%('java, "-cp", workspacePath/'jar/'dest, "test.Foo")(workspacePath).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 == 2 + ) + } + + 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 == 4 + ) + val Right((runOutput3, evalCount3)) = eval(Build.run("test.BarFour")) + assert( + runOutput3.out.string == "New Cls!\n", + evalCount3 == 2 + ) + } + } +} diff --git a/core/test/src/mill/eval/TarjanTests.scala b/core/test/src/mill/eval/TarjanTests.scala new file mode 100644 index 00000000..2f9d0a4d --- /dev/null +++ b/core/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/core/test/src/mill/main/MainTests.scala b/core/test/src/mill/main/MainTests.scala new file mode 100644 index 00000000..c2499835 --- /dev/null +++ b/core/test/src/mill/main/MainTests.scala @@ -0,0 +1,81 @@ +package mill.main + +import mill.Module +import mill.define.{Discover, Segment, Task} +import mill.util.TestGraphs._ +import mill.util.TestUtil.test +import utest._ +object MainTests extends TestSuite{ + def check[T](module: mill.Module, + discover: Discover, + selectorString: String, + expected: Either[String, Task[_]]) = { + + val resolved = for{ + selectors <- mill.main.ParseArgs(Seq(selectorString)).map(_._1.head) + val crossSelectors = selectors.map{case Segment.Cross(x) => x.toList.map(_.toString) case _ => Nil} + task <- mill.main.Resolve.resolve(selectors, module, discover, Nil, crossSelectors, Nil) + } yield task + assert(resolved == expected) + } + val tests = Tests{ + val graphs = new mill.util.TestGraphs() + import graphs._ + 'single - { + 'pos - check(singleton, Discover[singleton.type], "single", Right(singleton.single)) + 'neg1 - check(singleton, Discover[singleton.type], "doesntExist", Left("Cannot resolve task doesntExist")) + 'neg2 - check(singleton, Discover[singleton.type], "single.doesntExist", Left("Cannot resolve module single")) + 'neg3 - check(singleton, Discover[singleton.type], "", Left("Selector cannot be empty")) + } + 'nested - { + + 'pos1 - check(nestedModule, Discover[nestedModule.type], "single", Right(nestedModule.single)) + 'pos2 - check(nestedModule, Discover[nestedModule.type], "nested.single", Right(nestedModule.nested.single)) + 'pos3 - check(nestedModule, Discover[nestedModule.type], "classInstance.single", Right(nestedModule.classInstance.single)) + 'neg1 - check(nestedModule, Discover[nestedModule.type], "doesntExist", Left("Cannot resolve task doesntExist")) + 'neg2 - check(nestedModule, Discover[nestedModule.type], "single.doesntExist", Left("Cannot resolve module single")) + 'neg3 - check(nestedModule, Discover[nestedModule.type], "nested.doesntExist", Left("Cannot resolve task nested.doesntExist")) + 'neg4 - check(nestedModule, Discover[nestedModule.type], "classInstance.doesntExist", Left("Cannot resolve task classInstance.doesntExist")) + } + 'cross - { + 'single - { + + 'pos1 - check(singleCross, Discover[singleCross.type], "cross[210].suffix", Right(singleCross.cross("210").suffix)) + 'pos2 - check(singleCross, Discover[singleCross.type], "cross[211].suffix", Right(singleCross.cross("211").suffix)) + 'neg1 - check(singleCross, Discover[singleCross.type], "cross[210].doesntExist", Left("Cannot resolve task cross[210].doesntExist")) + 'neg2 - check(singleCross, Discover[singleCross.type], "cross[doesntExist].doesntExist", Left("Cannot resolve cross cross[doesntExist]")) + 'neg2 - check(singleCross, Discover[singleCross.type], "cross[doesntExist].suffix", Left("Cannot resolve cross cross[doesntExist]")) + } + 'double - { + + 'pos1 - check( + doubleCross, + Discover[doubleCross.type], + "cross[210,jvm].suffix", + Right(doubleCross.cross("210", "jvm").suffix) + ) + 'pos2 - check( + doubleCross, + Discover[doubleCross.type], + "cross[211,jvm].suffix", + Right(doubleCross.cross("211", "jvm").suffix) + ) + } + 'nested - { + 'pos1 - check( + nestedCrosses, + Discover[nestedCrosses.type], + "cross[210].cross2[js].suffix", + Right(nestedCrosses.cross("210").cross2("js").suffix) + ) + 'pos2 - check( + nestedCrosses, + Discover[nestedCrosses.type], + "cross[211].cross2[jvm].suffix", + Right(nestedCrosses.cross("211").cross2("jvm").suffix) + ) + } + } + + } +} diff --git a/core/test/src/mill/main/ParseArgsTest.scala b/core/test/src/mill/main/ParseArgsTest.scala new file mode 100644 index 00000000..2ef07d36 --- /dev/null +++ b/core/test/src/mill/main/ParseArgsTest.scala @@ -0,0 +1,229 @@ +package mill.main + +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[List[Segment]], + expectedArgs: Seq[String]) = { + val Right((selectors, args)) = ParseArgs(input) + + assert( + selectors == expectedSelectors, + args == expectedArgs + ) + } + + 'rejectEmpty { + assert(ParseArgs(Seq.empty) == Left("Selector cannot be empty")) + } + 'singleSelector - check( + input = Seq("core.compile"), + expectedSelectors = List( + List(Label("core"), Label("compile")) + ), + expectedArgs = Seq.empty + ) + 'singleSelectorWithArgs - check( + input = Seq("application.run", "hello", "world"), + expectedSelectors = List( + List(Label("application"), Label("run")) + ), + expectedArgs = Seq("hello", "world") + ) + 'singleSelectorWithCross - check( + input = Seq("bridges[2.12.4,jvm].compile"), + expectedSelectors = List( + List(Label("bridges"), Cross(Seq("2.12.4", "jvm")), Label("compile")) + ), + expectedArgs = Seq.empty + ) + 'multiSelectorsBraceExpansion - check( + input = Seq("--all", "{core,application}.compile"), + expectedSelectors = List( + List(Label("core"), Label("compile")), + List(Label("application"), Label("compile")) + ), + expectedArgs = Seq.empty + ) + 'multiSelectorsBraceExpansionWithArgs - check( + input = Seq("--all", "{core,application}.run", "--", "hello", "world"), + expectedSelectors = List( + List(Label("core"), Label("run")), + List(Label("application"), Label("run")) + ), + expectedArgs = Seq("hello", "world") + ) + 'multiSelectorsBraceExpansionWithCross - check( + input = Seq("--all", "bridges[2.12.4,jvm].{test,jar}"), + expectedSelectors = List( + List(Label("bridges"), Cross(Seq("2.12.4", "jvm")), Label("test")), + 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( + List(Label("bridges"), Cross(Seq("2.11.11")), Label("jar")), + 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( + List(Label("core"), Label("compile")) + ), + expectedArgs = Seq("application.compile") + ) + } + } + +} diff --git a/core/test/src/mill/util/TestEvaluator.scala b/core/test/src/mill/util/TestEvaluator.scala new file mode 100644 index 00000000..47c9d940 --- /dev/null +++ b/core/test/src/mill/util/TestEvaluator.scala @@ -0,0 +1,30 @@ +package mill.util + +import ammonite.ops.Path +import mill.define.{Input, Target, Task} +import mill.eval.{Evaluator, Result} +import mill.util.Strict.Agg +class TestEvaluator(module: mill.Module, + workspacePath: Path, + basePath: Path){ + val evaluator = new Evaluator(workspacePath, basePath, module, DummyLogger) + + def apply[T](t: Task[T]): Either[Result.Failing, (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 + case t: mill.define.Command[_] => t + }.size + )) + } else { + Left( + evaluated.failing.lookupKey(evaluated.failing.keys().next).items.next()) + } + } + +} diff --git a/core/test/src/mill/util/TestGraphs.scala b/core/test/src/mill/util/TestGraphs.scala new file mode 100644 index 00000000..54c8d815 --- /dev/null +++ b/core/test/src/mill/util/TestGraphs.scala @@ -0,0 +1,222 @@ +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") } + } + + object canOverrideSuper extends TestUtil.BaseModule with BaseModule { + override def foo = T{ super.foo() ++ Seq("object") } + } + + 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 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/core/test/src/mill/util/TestUtil.scala b/core/test/src/mill/util/TestUtil.scala new file mode 100644 index 00000000..1af12a74 --- /dev/null +++ b/core/test/src/mill/util/TestUtil.scala @@ -0,0 +1,60 @@ +package mill.util + +import ammonite.main.Router.Overrides +import mill.define._ +import mill.eval.Result +import utest.assert +import mill.util.Strict.Agg +import scala.collection.mutable + +object TestUtil { + class BaseModule(implicit millModuleEnclosing0: sourcecode.Enclosing, + millModuleLine0: sourcecode.Line, + millName0: sourcecode.Name, + overrides: Overrides) + extends mill.define.BaseModule(ammonite.ops.pwd / millModuleEnclosing0.value) + + 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(_, 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)) + } + } + } +} |