diff options
author | Li Haoyi <haoyi.sg@gmail.com> | 2018-02-09 00:14:47 -0800 |
---|---|---|
committer | Li Haoyi <haoyi.sg@gmail.com> | 2018-02-09 08:17:47 -0800 |
commit | 8ddd2fa054bc8639c28db2e95b7903e2954fdb7d (patch) | |
tree | aa985f1e715f07eb279e6facad61de8a187e316c /main | |
parent | 90d0a3388d280554eaa51371f666d2f7a965a8af (diff) | |
download | mill-8ddd2fa054bc8639c28db2e95b7903e2954fdb7d.tar.gz mill-8ddd2fa054bc8639c28db2e95b7903e2954fdb7d.tar.bz2 mill-8ddd2fa054bc8639c28db2e95b7903e2954fdb7d.zip |
.
Diffstat (limited to 'main')
36 files changed, 3659 insertions, 0 deletions
diff --git a/main/src/mill/Main.scala b/main/src/mill/Main.scala new file mode 100644 index 00000000..3025994c --- /dev/null +++ b/main/src/mill/Main.scala @@ -0,0 +1,83 @@ +package mill + +import ammonite.main.Cli.{formatBlock, genericSignature, replSignature} +import ammonite.ops._ +import ammonite.util.Util + +object Main { + case class Config(home: ammonite.ops.Path = pwd/'out/'ammonite, + colored: Option[Boolean] = None, + help: Boolean = false, + repl: Boolean = false, + watch: Boolean = false) + + def main(args: Array[String]): Unit = { + + import ammonite.main.Cli + + var show = false + val showCliArg = Cli.Arg[Cli.Config, Unit]( + "show", + None, + "Display the json-formatted value of the given target, if any", + (x, _) => { + show = true + x + } + ) + val removed = Set("predef-code", "home", "no-home-predef") + val millArgSignature = (Cli.genericSignature :+ showCliArg).filter(a => !removed(a.name)) + Cli.groupArgs( + args.toList, + millArgSignature, + Cli.Config(remoteLogging = false) + ) match{ + case Left(msg) => + System.err.println(msg) + System.exit(1) + case Right((cliConfig, _)) if cliConfig.help => + val leftMargin = millArgSignature.map(ammonite.main.Cli.showArg(_).length).max + 2 + System.out.println( + s"""Mill Build Tool + |usage: mill [mill-options] [target [target-options]] + | + |${formatBlock(millArgSignature, leftMargin).mkString(Util.newLine)}""".stripMargin + ) + System.exit(0) + case Right((cliConfig, leftoverArgs)) => + + val repl = leftoverArgs.isEmpty + val config = + if(!repl) cliConfig + else cliConfig.copy( + predefCode = + """import $file.build, build._ + |implicit val replApplyHandler = mill.main.ReplApplyHandler( + | interp.colors(), + | repl.pprinter(), + | build.millSelf.get, + | build.millDiscover + |) + |repl.pprinter() = replApplyHandler.pprinter + |import replApplyHandler.generatedEval._ + | + """.stripMargin, + welcomeBanner = None + ) + + val runner = new mill.main.MainRunner( + config, show, + System.out, System.err, System.in + ) + if (repl){ + runner.printInfo("Loading...") + runner.runRepl() + } else { + val result = runner.runScript(pwd / "build.sc", leftoverArgs) + System.exit(if(result) 0 else 1) + } + } + } +} + + diff --git a/main/src/mill/main/MagicScopt.scala b/main/src/mill/main/MagicScopt.scala new file mode 100644 index 00000000..e18816c8 --- /dev/null +++ b/main/src/mill/main/MagicScopt.scala @@ -0,0 +1,46 @@ +package mill.main +import mill.define.ExternalModule +import mill.eval.{Evaluator, PathRef} +import mill.util.ParseArgs + +object MagicScopt{ + + + case class Tasks[T](value: Seq[mill.define.NamedTask[T]]) +} +class EvaluatorScopt[T]() + extends scopt.Read[mill.eval.Evaluator[T]]{ + def arity = 0 + def reads = s => try{ + Evaluator.currentEvaluator.get.asInstanceOf[mill.eval.Evaluator[T]] + } +} +class TargetScopt[T]() + extends scopt.Read[MagicScopt.Tasks[T]]{ + def arity = 0 + def reads = s => { + val rootModule = Evaluator.currentEvaluator.get.rootModule + val d = rootModule.millDiscover + val (expanded, leftover) = ParseArgs(Seq(s)).fold(e => throw new Exception(e), identity) + val resolved = expanded.map{ + case (Some(scoping), segments) => + val moduleCls = rootModule.getClass.getClassLoader.loadClass(scoping.render + "$") + val externalRootModule = moduleCls.getField("MODULE$").get(moduleCls).asInstanceOf[ExternalModule] + val crossSelectors = segments.value.map { + case mill.define.Segment.Cross(x) => x.toList.map(_.toString) + case _ => Nil + } + mill.main.Resolve.resolve(segments.value.toList, externalRootModule, d, leftover, crossSelectors.toList, Nil) + case (None, segments) => + val crossSelectors = segments.value.map { + case mill.define.Segment.Cross(x) => x.toList.map(_.toString) + case _ => Nil + } + mill.main.Resolve.resolve(segments.value.toList, rootModule, d, leftover, crossSelectors.toList, Nil) + } + mill.util.EitherOps.sequence(resolved) match{ + case Left(s) => throw new Exception(s) + case Right(ts) => MagicScopt.Tasks(ts.flatten).asInstanceOf[MagicScopt.Tasks[T]] + } + } +}
\ No newline at end of file diff --git a/main/src/mill/main/MainModule.scala b/main/src/mill/main/MainModule.scala new file mode 100644 index 00000000..012ecce5 --- /dev/null +++ b/main/src/mill/main/MainModule.scala @@ -0,0 +1,56 @@ +package mill.main + +import mill.util +import mill.main.RunScript +import mill.util.Watched +import pprint.{Renderer, Truncated} + +trait MainModule extends mill.Module{ + + implicit def millDiscover: mill.define.Discover[_] + implicit def millScoptTargetReads[T] = new mill.main.TargetScopt[T]() + implicit def millScoptEvaluatorReads[T] = new mill.main.EvaluatorScopt[T]() + mill.define.Ctx.make + def resolve(targets: mill.main.MagicScopt.Tasks[Any]*) = mill.T.command{ + targets.flatMap(_.value).foreach(println) + } + def describe(evaluator: mill.eval.Evaluator[Any], + targets: mill.main.MagicScopt.Tasks[Any]*) = mill.T.command{ + for{ + t <- targets + target <- t.value + tree = ReplApplyHandler.pprintTask(target, evaluator) + val defaults = pprint.PPrinter() + val renderer = new Renderer( + defaults.defaultWidth, + defaults.colorApplyPrefix, + defaults.colorLiteral, + defaults.defaultIndent + ) + val rendered = renderer.rec(tree, 0, 0).iter + val truncated = new Truncated(rendered, defaults.defaultWidth, defaults.defaultHeight) + str <- truncated ++ Iterator("\n") + } { + print(str) + } + } + def all(evaluator: mill.eval.Evaluator[Any], + targets: mill.main.MagicScopt.Tasks[Any]*) = mill.T.command{ + val (watched, res) = RunScript.evaluate( + evaluator, + mill.util.Strict.Agg.from(targets.flatMap(_.value)) + ) + Watched((), watched) + } + def show(evaluator: mill.eval.Evaluator[Any], + targets: mill.main.MagicScopt.Tasks[Any]*) = mill.T.command{ + val (watched, res) = mill.main.RunScript.evaluate( + evaluator, + mill.util.Strict.Agg.from(targets.flatMap(_.value)) + ) + for(json <- res.right.get.flatMap(_._2)){ + println(json) + } + Watched((), watched) + } +} diff --git a/main/src/mill/main/MainRunner.scala b/main/src/mill/main/MainRunner.scala new file mode 100644 index 00000000..c073e583 --- /dev/null +++ b/main/src/mill/main/MainRunner.scala @@ -0,0 +1,120 @@ +package mill.main +import java.io.{InputStream, OutputStream, PrintStream} + +import ammonite.Main +import ammonite.interp.{Interpreter, Preprocessor} +import ammonite.ops.Path +import ammonite.util._ +import mill.define.Discover +import mill.eval.{Evaluator, PathRef} +import mill.util.PrintLogger +import mill.main.RunScript +import upickle.Js + +/** + * Customized version of [[ammonite.MainRunner]], allowing us to run Mill + * `build.sc` scripts with mill-specific tweaks such as a custom + * `scriptCodeWrapper` or with a persistent evaluator between runs. + */ +class MainRunner(config: ammonite.main.Cli.Config, + show: Boolean, + outprintStream: PrintStream, + errPrintStream: PrintStream, + stdIn: InputStream) + extends ammonite.MainRunner( + config, outprintStream, errPrintStream, + stdIn, outprintStream, errPrintStream + ){ + + var lastEvaluator: Option[(Seq[(Path, Long)], Evaluator[Any])] = None + + override def runScript(scriptPath: Path, scriptArgs: List[String]) = + watchLoop( + isRepl = false, + printing = true, + mainCfg => { + val (result, interpWatched) = RunScript.runScript( + mainCfg.wd, + scriptPath, + mainCfg.instantiateInterpreter(), + scriptArgs, + lastEvaluator, + new PrintLogger( + colors != ammonite.util.Colors.BlackWhite, + colors, + if (show) errPrintStream else outprintStream, + errPrintStream, + errPrintStream + ) + ) + + result match{ + case Res.Success(data) => + val (eval, evaluationWatches, res) = data + + lastEvaluator = Some((interpWatched, eval)) + + (Res(res), interpWatched ++ evaluationWatches) + case _ => (result, interpWatched) + } + } + ) + + override def handleWatchRes[T](res: Res[T], printing: Boolean) = { + res match{ + case Res.Success(value) => +// if (show){ +// for(json <- value.asInstanceOf[Seq[Js.Value]]){ +// outprintStream.println(json) +// } +// } + + true + + case _ => super.handleWatchRes(res, printing) + } + + } + override def initMain(isRepl: Boolean) = { + super.initMain(isRepl).copy( + scriptCodeWrapper = CustomCodeWrapper, + // Ammonite does not properly forward the wd from CliConfig to Main, so + // force forward it outselves + wd = config.wd + ) + } + object CustomCodeWrapper extends Preprocessor.CodeWrapper { + def top(pkgName: Seq[Name], imports: Imports, indexedWrapperName: Name) = { + val wrapName = indexedWrapperName.backticked + val literalPath = pprint.Util.literalize(config.wd.toString) + s""" + |package ${pkgName.head.encoded} + |package ${Util.encodeScalaSourcePath(pkgName.tail)} + |$imports + |import mill._ + |object $wrapName + |extends mill.define.BaseModule(ammonite.ops.Path($literalPath)) + |with $wrapName{ + | // Stub to make sure Ammonite has something to call after it evaluates a script, + | // even if it does nothing... + | def $$main() = Iterator[String]() + | + | implicit def millDiscover: mill.define.Discover[this.type] = mill.define.Discover[this.type] + | // Need to wrap the returned Module in Some(...) to make sure it + | // doesn't get picked up during reflective child-module discovery + | val millSelf = Some(this) + |} + | + |sealed trait $wrapName extends mill.main.MainModule{ + |""".stripMargin + } + + + def bottom(printCode: String, indexedWrapperName: Name, extraCode: String) = { + // We need to disable the `$main` method definition inside the wrapper class, + // because otherwise it might get picked up by Ammonite and run as a static + // class method, which blows up since it's defined as an instance method + "\n}" + } + } +} diff --git a/main/src/mill/main/ReplApplyHandler.scala b/main/src/mill/main/ReplApplyHandler.scala new file mode 100644 index 00000000..76ef354f --- /dev/null +++ b/main/src/mill/main/ReplApplyHandler.scala @@ -0,0 +1,137 @@ +package mill.main + + +import mill.define.Applicative.ApplyHandler +import mill.define.Segment.Label +import mill.define._ +import mill.eval.{Evaluator, Result} +import mill.util.Strict.Agg + +import scala.collection.mutable +object ReplApplyHandler{ + def apply[T](colors: ammonite.util.Colors, + pprinter0: pprint.PPrinter, + rootModule: mill.define.BaseModule, + discover: Discover[_]) = { + new ReplApplyHandler( + pprinter0, + new Evaluator( + ammonite.ops.pwd / 'out, + ammonite.ops.pwd / 'out, + rootModule, + discover, + new mill.util.PrintLogger( + colors != ammonite.util.Colors.BlackWhite, + colors, + System.out, + System.err, + System.err + ) + ) + ) + } + def pprintCross(c: mill.define.Cross[_], evaluator: Evaluator[_]) = { + pprint.Tree.Lazy( ctx => + Iterator(c.millOuterCtx.enclosing , ":", c.millOuterCtx.lineNum.toString, ctx.applyPrefixColor("\nChildren:").toString) ++ + c.items.iterator.map(x => + "\n (" + x._1.map(pprint.PPrinter.BlackWhite.apply(_)).mkString(", ") + ")" + ) + ) + } + def pprintModule(m: mill.define.Module, evaluator: Evaluator[_]) = { + pprint.Tree.Lazy( ctx => + Iterator(m.millInternal.millModuleEnclosing, ":", m.millInternal.millModuleLine.toString) ++ + (if (m.millInternal.reflect[mill.Module].isEmpty) Nil + else + ctx.applyPrefixColor("\nChildren:").toString +: + m.millInternal.reflect[mill.Module].map("\n ." + _.millOuterCtx.segment.pathSegments.mkString("."))) ++ + (evaluator.discover.value.get(m.getClass) match{ + case None => Nil + case Some(commands) => + ctx.applyPrefixColor("\nCommands:").toString +: commands.map{c => + "\n ." + c._2.name + "(" + + c._2.argSignatures.map(s => s.name + ": " + s.typeString).mkString(", ") + + ")()" + } + }) ++ + (if (m.millInternal.reflect[Target[_]].isEmpty) Nil + else { + Seq(ctx.applyPrefixColor("\nTargets:").toString) ++ + m.millInternal.reflect[Target[_]].sortBy(_.label).map(t => + "\n ." + t.label + "()" + ) + }) + + ) + } + def pprintTask(t: NamedTask[_], evaluator: Evaluator[_]) = { + val seen = mutable.Set.empty[Task[_]] + def rec(t: Task[_]): Seq[Segments] = { + if (seen(t)) Nil // do nothing + else t match { + case t: Target[_] if evaluator.rootModule.millInternal.targets.contains(t) => + Seq(t.ctx.segments) + case _ => + seen.add(t) + t.inputs.flatMap(rec) + } + } + pprint.Tree.Lazy(ctx => + Iterator( + t.toString, "(", t.ctx.fileName, ":", t.ctx.lineNum.toString, ")", + t.ctx.lineNum.toString, "\n", ctx.applyPrefixColor("Inputs:").toString + ) ++ t.inputs.iterator.flatMap(rec).map("\n " + _.render) + ) + } + +} +class ReplApplyHandler(pprinter0: pprint.PPrinter, + evaluator: Evaluator[_]) extends ApplyHandler[Task] { + // Evaluate classLoaderSig only once in the REPL to avoid busting caches + // as the user enters more REPL commands and changes the classpath + val classLoaderSig = Evaluator.classLoaderSig + override def apply[V](t: Task[V]) = { + val res = evaluator.evaluate(Agg(t)) + res.values match{ + case Seq(head: V) => head + case Nil => + val msg = new mutable.StringBuilder() + msg.append(res.failing.keyCount + " targets failed\n") + for((k, vs) <- res.failing.items){ + msg.append(k match{ + case Left(t) => "Anonymous Task\n" + case Right(k) => k.segments.render + "\n" + }) + + for(v <- vs){ + v match{ + case Result.Failure(m, _) => msg.append(m + "\n") + case Result.Exception(t, outerStack) => + msg.append( + t.toString + + t.getStackTrace.dropRight(outerStack.value.length).map("\n " + _).mkString + + "\n" + ) + + } + } + } + throw new Exception(msg.toString) + } + } + + val generatedEval = new EvalGenerated(evaluator) + + val millHandlers: PartialFunction[Any, pprint.Tree] = { + case c: Cross[_] => + ReplApplyHandler.pprintCross(c, evaluator) + case m: mill.Module if evaluator.rootModule.millInternal.modules.contains(m) => + ReplApplyHandler.pprintModule(m, evaluator) + case t: mill.define.Target[_] if evaluator.rootModule.millInternal.targets.contains(t) => + ReplApplyHandler.pprintTask(t, evaluator) + + } + val pprinter = pprinter0.copy( + additionalHandlers = millHandlers orElse pprinter0.additionalHandlers + ) +} diff --git a/main/src/mill/main/Resolve.scala b/main/src/mill/main/Resolve.scala new file mode 100644 index 00000000..c134d70d --- /dev/null +++ b/main/src/mill/main/Resolve.scala @@ -0,0 +1,144 @@ +package mill.main + +import mill.define._ +import mill.define.TaskModule +import ammonite.util.Res +import mill.util.Router.EntryPoint +import mill.util.Scripts + +object Resolve { + def resolve[T, V](remainingSelector: List[Segment], + obj: mill.Module, + discover: Discover[_], + rest: Seq[String], + remainingCrossSelectors: List[List[String]], + revSelectorsSoFar: List[Segment]): Either[String, Seq[NamedTask[Any]]] = { + + remainingSelector match{ + case Segment.Cross(_) :: Nil => Left("Selector cannot start with a [cross] segment") + case Segment.Label(last) :: Nil => + val target = + obj + .millInternal + .reflect[Target[_]] + .find(_.label == last) + .map(Right(_)) + + def shimArgsig[T](a: mill.util.Router.ArgSig[T, _]) = { + ammonite.main.Router.ArgSig[T]( + a.name, + a.typeString, + a.doc, + a.default + ) + } + def invokeCommand(target: mill.Module, name: String) = for{ + (cls, entryPoints) <- discover.value + if cls.isAssignableFrom(target.getClass) + ep <- entryPoints + if ep._2.name == name + } yield Scripts.runMainMethod( + target, + ep._2.asInstanceOf[EntryPoint[mill.Module]], + ammonite.main.Scripts.groupArgs(rest.toList) + ) match{ + case Res.Success(v: Command[_]) => Right(v) + case Res.Failure(msg) => Left(msg) + case Res.Exception(ex, msg) => + val sw = new java.io.StringWriter() + ex.printStackTrace(new java.io.PrintWriter(sw)) + val prefix = if (msg.nonEmpty) msg + "\n" else msg + Left(prefix + sw.toString) + + } + + val runDefault = for{ + child <- obj.millInternal.reflectNestedObjects[mill.Module] + if child.millOuterCtx.segment == Segment.Label(last) + res <- child match{ + case taskMod: TaskModule => Some(invokeCommand(child, taskMod.defaultCommandName()).headOption) + case _ => None + } + } yield res + + val command = invokeCommand(obj, last).headOption + + command orElse target orElse runDefault.flatten.headOption match{ + case None => Left("Cannot resolve task " + + Segments((Segment.Label(last) :: revSelectorsSoFar).reverse:_*).render + ) + // Contents of `either` *must* be a `Task`, because we only select + // methods returning `Task` in the discovery process + case Some(either) => either.right.map(Seq(_)) + } + + + case head :: tail => + val newRevSelectorsSoFar = head :: revSelectorsSoFar + head match{ + case Segment.Label(singleLabel) => + if (singleLabel == "__") { + + val matching = + obj.millInternal.modules + .map(resolve(tail, _, discover, rest, remainingCrossSelectors, newRevSelectorsSoFar)) + .collect{case Right(vs) => vs}.flatten + + if (matching.nonEmpty)Right(matching) + else Left("Cannot resolve module " + Segments(newRevSelectorsSoFar.reverse:_*).render) + } else if (singleLabel == "_") { + + val matching = + obj.millModuleDirectChildren + .map(resolve(tail, _, discover, rest, remainingCrossSelectors, newRevSelectorsSoFar)) + .collect{case Right(vs) => vs}.flatten + + if (matching.nonEmpty)Right(matching) + else Left("Cannot resolve module " + Segments(newRevSelectorsSoFar.reverse:_*).render) + }else{ + + obj.millInternal.reflectNestedObjects[mill.Module].find{ + _.millOuterCtx.segment == Segment.Label(singleLabel) + } match{ + case Some(child: mill.Module) => resolve(tail, child, discover, rest, remainingCrossSelectors, newRevSelectorsSoFar) + case None => Left("Cannot resolve module " + Segments(newRevSelectorsSoFar.reverse:_*).render) + } + } + + case Segment.Cross(cross) => + obj match{ + case c: Cross[_] => + if(cross == Seq("__")){ + val matching = + for ((k, v) <- c.items) + yield resolve(tail, v.asInstanceOf[mill.Module], discover, rest, remainingCrossSelectors, newRevSelectorsSoFar) + + val results = matching.collect{case Right(res) => res}.flatten + + if (results.isEmpty) Left("Cannot resolve cross " + Segments(newRevSelectorsSoFar.reverse:_*).render) + else Right(results) + } else if (cross.contains("_")){ + val matching = for { + (k, v) <- c.items + if k.length == cross.length + if k.zip(cross).forall { case (l, r) => l == r || r == "_" } + } yield resolve(tail, v.asInstanceOf[mill.Module], discover, rest, remainingCrossSelectors, newRevSelectorsSoFar) + + val results = matching.collect{case Right(res) => res}.flatten + + if (results.isEmpty) Left("Cannot resolve cross " + Segments(newRevSelectorsSoFar.reverse:_*).render) + else Right(results) + }else{ + c.itemMap.get(cross.toList) match{ + case Some(m: mill.Module) => resolve(tail, m, discover, rest, remainingCrossSelectors, newRevSelectorsSoFar) + case None => Left("Cannot resolve cross " + Segments(newRevSelectorsSoFar.reverse:_*).render) + } + } + case _ => Left("Cannot resolve cross " + Segments(newRevSelectorsSoFar.reverse:_*).render) + } + } + + case Nil => Left("Selector cannot be empty") + } + } +} diff --git a/main/src/mill/main/RunScript.scala b/main/src/mill/main/RunScript.scala new file mode 100644 index 00000000..bd21a821 --- /dev/null +++ b/main/src/mill/main/RunScript.scala @@ -0,0 +1,240 @@ +package mill.main + +import java.nio.file.NoSuchFileException + +import ammonite.interp.Interpreter +import ammonite.ops.{Path, read} +import ammonite.runtime.SpecialClassLoader +import ammonite.util.Util.CodeSource +import ammonite.util.{Name, Res, Util} +import mill.define +import mill.define.{Discover, ExternalModule, Segment, Task} +import mill.eval.{Evaluator, PathRef, Result} +import mill.util.{EitherOps, Logger, ParseArgs, Watched} +import mill.util.Strict.Agg +import upickle.Js + +/** + * Custom version of ammonite.main.Scripts, letting us run the build.sc script + * directly without going through Ammonite's main-method/argument-parsing + * subsystem + */ +object RunScript{ + def runScript(wd: Path, + path: Path, + instantiateInterpreter: => Either[(Res.Failing, Seq[(Path, Long)]), ammonite.interp.Interpreter], + scriptArgs: Seq[String], + lastEvaluator: Option[(Seq[(Path, Long)], Evaluator[Any])], + log: Logger) + : (Res[(Evaluator[Any], Seq[(Path, Long)], Either[String, Seq[Js.Value]])], Seq[(Path, Long)]) = { + + val (evalRes, interpWatched) = lastEvaluator match{ + case Some((prevInterpWatchedSig, prevEvaluator)) + if watchedSigUnchanged(prevInterpWatchedSig) => + + (Res.Success(prevEvaluator), prevInterpWatchedSig) + + case _ => + instantiateInterpreter match{ + case Left((res, watched)) => (res, watched) + case Right(interp) => + interp.watch(path) + val eval = + for((mapping, discover) <- evaluateMapping(wd, path, interp)) + yield new Evaluator[Any]( + wd / 'out, wd / 'out, mapping, discover, log, + mapping.getClass.getClassLoader.asInstanceOf[SpecialClassLoader].classpathSignature + ) + + (eval, interp.watchedFiles) + } + } + + val evaluated = for{ + evaluator <- evalRes + (evalWatches, res) <- Res(evaluateTarget(evaluator, scriptArgs)) + } yield { + val alreadyStale = evalWatches.exists(p => p.sig != new PathRef(p.path, p.quick).sig) + // If the file changed between the creation of the original + // `PathRef` and the current moment, use random junk .sig values + // to force an immediate re-run. Otherwise calculate the + // pathSignatures the same way Ammonite would and hand over the + // values, so Ammonite can watch them and only re-run if they + // subsequently change + val evaluationWatches = + if (alreadyStale) evalWatches.map(_.path -> util.Random.nextLong()) + else evalWatches.map(p => p.path -> Interpreter.pathSignature(p.path)) + + (evaluator, evaluationWatches, res.map(_.flatMap(_._2))) + } + (evaluated, interpWatched) + } + + def watchedSigUnchanged(sig: Seq[(Path, Long)]) = { + sig.forall{case (p, l) => Interpreter.pathSignature(p) == l} + } + + def evaluateMapping(wd: Path, + path: Path, + interp: ammonite.interp.Interpreter): Res[(mill.define.BaseModule, Discover[Any])] = { + + val (pkg, wrapper) = Util.pathToPackageWrapper(Seq(), path relativeTo wd) + + for { + scriptTxt <- + try Res.Success(Util.normalizeNewlines(read(path))) + catch { case e: NoSuchFileException => Res.Failure("Script file not found: " + path) } + + processed <- interp.processModule( + scriptTxt, + CodeSource(wrapper, pkg, Seq(Name("ammonite"), Name("$file")), Some(path)), + autoImport = true, + extraCode = "", + hardcoded = true + ) + + buildClsName <- processed.blockInfo.lastOption match { + case Some(meta) => Res.Success(meta.id.wrapperPath) + case None => Res.Skip + } + + buildCls = interp + .evalClassloader + .loadClass(buildClsName) + + module <- try { + Util.withContextClassloader(interp.evalClassloader) { + Res.Success( + buildCls.getMethod("millSelf") + .invoke(null) + .asInstanceOf[Some[mill.define.BaseModule]] + .get + ) + } + } catch { + case e: Throwable => Res.Exception(e, "") + } + discover <- try { + Util.withContextClassloader(interp.evalClassloader) { + Res.Success( + buildCls.getMethod("millDiscover") + .invoke(module) + .asInstanceOf[Discover[Any]] + ) + } + } catch { + case e: Throwable => Res.Exception(e, "") + } +// _ <- Res(consistencyCheck(mapping)) + } yield (module, discover) + } + + def evaluateTarget[T](evaluator: Evaluator[T], scriptArgs: Seq[String]) = { + for { + parsed <- ParseArgs(scriptArgs) + (selectors, args) = parsed + targets <- { + val selected = selectors.map { case (scopedSel, sel) => + val (rootModule, discover) = scopedSel match{ + case None => (evaluator.rootModule, evaluator.discover) + case Some(scoping) => + val moduleCls = + evaluator.rootModule.getClass.getClassLoader.loadClass(scoping.render + "$") + + val rootModule = moduleCls.getField("MODULE$").get(moduleCls).asInstanceOf[ExternalModule] + (rootModule, rootModule.millDiscover) + } + val crossSelectors = sel.value.map { + case Segment.Cross(x) => x.toList.map(_.toString) + case _ => Nil + } + + try { + // We inject the `evaluator.rootModule` into the TargetScopt, rather + // than the `rootModule`, because even if you are running an external + // module we still want you to be able to resolve targets from your + // main build. Resolving targets from external builds as CLI arguments + // is not currently supported + mill.eval.Evaluator.currentEvaluator.set(evaluator) + mill.main.Resolve.resolve( + sel.value.toList, rootModule, + discover, + args, crossSelectors.toList, Nil + ) + } finally{ + mill.eval.Evaluator.currentEvaluator.set(null) + } + } + EitherOps.sequence(selected) + } + } yield { + val (watched, res) = evaluate( + evaluator, + Agg.from(targets.flatten.distinct) + ) + + val watched2 = for{ + x <- res.right.toSeq + (Watched(_, extraWatched), _) <- x + w <- extraWatched + } yield w + + (watched ++ watched2, res) + } + } + + def evaluate(evaluator: Evaluator[_], + targets: Agg[Task[Any]]): (Seq[PathRef], Either[String, Seq[(Any, Option[upickle.Js.Value])]]) = { + val evaluated = evaluator.evaluate(targets) + val watched = evaluated.results + .iterator + .collect { + case (t: define.Sources, Result.Success(p: Seq[PathRef])) => p + } + .flatten + .toSeq + + val errorStr = + (for((k, fs) <- evaluated.failing.items()) yield { + val ks = k match{ + case Left(t) => t.toString + case Right(t) => t.segments.render + } + val fss = fs.map{ + case Result.Exception(t, outerStack) => + t.toString + + t.getStackTrace.dropRight(outerStack.value.length).map("\n " + _).mkString + case Result.Failure(t, _) => t + } + s"$ks ${fss.mkString(", ")}" + }).mkString("\n") + + evaluated.failing.keyCount match { + case 0 => + val json = for(t <- targets.toSeq) yield { + t match { + case t: mill.define.NamedTask[_] => + val jsonFile = Evaluator + .resolveDestPaths(evaluator.outPath, t.ctx.segments) + .meta + val metadata = upickle.json.read(jsonFile.toIO) + Some(metadata(1)) + + case _ => None + } + } + + watched -> Right(evaluated.values.zip(json)) + case n => watched -> Left(s"$n targets failed\n$errorStr") + } + } + +// def consistencyCheck[T](mapping: Discovered.Mapping[T]): Either[String, Unit] = { +// val consistencyErrors = Discovered.consistencyCheck(mapping) +// if (consistencyErrors.nonEmpty) { +// Left(s"Failed Discovered.consistencyCheck: ${consistencyErrors.map(_.render)}") +// } else { +// Right(()) +// } +// } +} diff --git a/main/src/mill/modules/Jvm.scala b/main/src/mill/modules/Jvm.scala new file mode 100644 index 00000000..297dcf1f --- /dev/null +++ b/main/src/mill/modules/Jvm.scala @@ -0,0 +1,257 @@ +package mill.modules + +import java.io.FileOutputStream +import java.lang.reflect.Modifier +import java.net.URLClassLoader +import java.nio.file.attribute.PosixFilePermission +import java.util.jar.{JarEntry, JarFile, JarOutputStream} + +import ammonite.ops._ +import mill.define.Task +import mill.eval.PathRef +import mill.util.{Ctx, Loose} +import mill.util.Ctx.Log +import mill.util.Loose.Agg +import upickle.default.{Reader, Writer} + +import scala.annotation.tailrec +import scala.collection.mutable +import scala.reflect.ClassTag + + +object Jvm { + + def interactiveSubprocess(mainClass: String, + classPath: Agg[Path], + jvmArgs: Seq[String] = Seq.empty, + envArgs: Map[String, String] = Map.empty, + mainArgs: Seq[String] = Seq.empty, + workingDir: Path = null): Unit = { + import ammonite.ops.ImplicitWd._ + val commandArgs = + Vector("java") ++ + jvmArgs ++ + Vector("-cp", classPath.mkString(":"), mainClass) ++ + mainArgs + + %.copy(envArgs = envArgs)(commandArgs)(workingDir) + } + + def runLocal(mainClass: String, + classPath: Agg[Path], + mainArgs: Seq[String] = Seq.empty) + (implicit ctx: Ctx): Unit = { + inprocess(classPath, classLoaderOverrideSbtTesting = false, cl => { + getMainMethod(mainClass, cl).invoke(null, mainArgs.toArray) + }) + } + + private def getMainMethod(mainClassName: String, cl: ClassLoader) = { + val mainClass = cl.loadClass(mainClassName) + val method = mainClass.getMethod("main", classOf[Array[String]]) + // jvm allows the actual main class to be non-public and to run a method in the non-public class, + // we need to make it accessible + method.setAccessible(true) + val modifiers = method.getModifiers + if (!Modifier.isPublic(modifiers)) + throw new NoSuchMethodException(mainClassName + ".main is not public") + if (!Modifier.isStatic(modifiers)) + throw new NoSuchMethodException(mainClassName + ".main is not static") + method + } + + + + def inprocess[T](classPath: Agg[Path], + classLoaderOverrideSbtTesting: Boolean, + body: ClassLoader => T): T = { + val cl = if (classLoaderOverrideSbtTesting) { + val outerClassLoader = getClass.getClassLoader + new URLClassLoader(classPath.map(_.toIO.toURI.toURL).toArray, null){ + override def findClass(name: String) = { + if (name.startsWith("sbt.testing.")){ + outerClassLoader.loadClass(name) + }else{ + super.findClass(name) + } + } + } + } else { + new URLClassLoader(classPath.map(_.toIO.toURI.toURL).toArray, null) + } + val oldCl = Thread.currentThread().getContextClassLoader + Thread.currentThread().setContextClassLoader(cl) + try { + body(cl) + }finally{ + Thread.currentThread().setContextClassLoader(oldCl) + cl.close() + } + } + + def subprocess(mainClass: String, + classPath: Agg[Path], + jvmArgs: Seq[String] = Seq.empty, + envArgs: Map[String, String] = Map.empty, + mainArgs: Seq[String] = Seq.empty, + workingDir: Path = null) + (implicit ctx: Ctx) = { + + val commandArgs = + Vector("java") ++ + jvmArgs ++ + Vector("-cp", classPath.mkString(":"), mainClass) ++ + mainArgs + + val workingDir1 = Option(workingDir).getOrElse(ctx.dest) + mkdir(workingDir1) + val builder = + new java.lang.ProcessBuilder() + .directory(workingDir1.toIO) + .command(commandArgs:_*) + .redirectOutput(ProcessBuilder.Redirect.PIPE) + .redirectError(ProcessBuilder.Redirect.PIPE) + + for((k, v) <- envArgs) builder.environment().put(k, v) + val proc = builder.start() + val stdout = proc.getInputStream + val stderr = proc.getErrorStream + val sources = Seq( + (stdout, Left(_: Bytes), ctx.log.outputStream), + (stderr, Right(_: Bytes),ctx.log.errorStream ) + ) + val chunks = mutable.Buffer.empty[Either[Bytes, Bytes]] + while( + // Process.isAlive doesn't exist on JDK 7 =/ + util.Try(proc.exitValue).isFailure || + stdout.available() > 0 || + stderr.available() > 0 + ){ + var readSomething = false + for ((subStream, wrapper, parentStream) <- sources){ + while (subStream.available() > 0){ + readSomething = true + val array = new Array[Byte](subStream.available()) + val actuallyRead = subStream.read(array) + chunks.append(wrapper(new ammonite.ops.Bytes(array))) + parentStream.write(array, 0, actuallyRead) + } + } + // if we did not read anything sleep briefly to avoid spinning + if(!readSomething) + Thread.sleep(2) + } + + if (proc.exitValue() != 0) throw new InteractiveShelloutException() + else ammonite.ops.CommandResult(proc.exitValue(), chunks) + } + + private def createManifest(mainClass: Option[String]) = { + val m = new java.util.jar.Manifest() + m.getMainAttributes.put(java.util.jar.Attributes.Name.MANIFEST_VERSION, "1.0") + m.getMainAttributes.putValue( "Created-By", "Scala mill" ) + mainClass.foreach( + m.getMainAttributes.put(java.util.jar.Attributes.Name.MAIN_CLASS, _) + ) + m + } + + def createJar(inputPaths: Agg[Path], mainClass: Option[String] = None) + (implicit ctx: Ctx.Dest): PathRef = { + val outputPath = ctx.dest / "out.jar" + rm(outputPath) + + val seen = mutable.Set.empty[RelPath] + seen.add("META-INF" / "MANIFEST.MF") + val jar = new JarOutputStream( + new FileOutputStream(outputPath.toIO), + createManifest(mainClass) + ) + + try{ + assert(inputPaths.forall(exists(_))) + for{ + p <- inputPaths + (file, mapping) <- + if (p.isFile) Iterator(p -> empty/p.last) + else ls.rec(p).filter(_.isFile).map(sub => sub -> sub.relativeTo(p)) + if !seen(mapping) + } { + seen.add(mapping) + val entry = new JarEntry(mapping.toString) + entry.setTime(file.mtime.toMillis) + jar.putNextEntry(entry) + jar.write(read.bytes(file)) + jar.closeEntry() + } + } finally { + jar.close() + } + + PathRef(outputPath) + } + + def createAssembly(inputPaths: Agg[Path], + mainClass: Option[String] = None, + prependShellScript: String = "") + (implicit ctx: Ctx.Dest): PathRef = { + val outputPath = ctx.dest / "out.jar" + rm(outputPath) + + if(inputPaths.nonEmpty) { + + val output = new FileOutputStream(outputPath.toIO) + + // Prepend shell script and make it executable + if (prependShellScript.nonEmpty) { + output.write((prependShellScript + "\n").getBytes) + val perms = java.nio.file.Files.getPosixFilePermissions(outputPath.toNIO) + perms.add(PosixFilePermission.GROUP_EXECUTE) + perms.add(PosixFilePermission.OWNER_EXECUTE) + perms.add(PosixFilePermission.OTHERS_EXECUTE) + java.nio.file.Files.setPosixFilePermissions(outputPath.toNIO, perms) + } + + val jar = new JarOutputStream( + output, + createManifest(mainClass) + ) + + val seen = mutable.Set("META-INF/MANIFEST.MF") + try{ + + + for{ + p <- inputPaths + if exists(p) + (file, mapping) <- + if (p.isFile) { + val jf = new JarFile(p.toIO) + import collection.JavaConverters._ + for(entry <- jf.entries().asScala if !entry.isDirectory) yield { + read.bytes(jf.getInputStream(entry)) -> entry.getName + } + } + else { + ls.rec(p).iterator + .filter(_.isFile) + .map(sub => read.bytes(sub) -> sub.relativeTo(p).toString) + } + if !seen(mapping) + } { + seen.add(mapping) + val entry = new JarEntry(mapping.toString) + jar.putNextEntry(entry) + jar.write(file) + jar.closeEntry() + } + } finally { + jar.close() + output.close() + } + + } + PathRef(outputPath) + } + +} diff --git a/main/src/mill/modules/Util.scala b/main/src/mill/modules/Util.scala new file mode 100644 index 00000000..cef11859 --- /dev/null +++ b/main/src/mill/modules/Util.scala @@ -0,0 +1,65 @@ +package mill.modules + + +import ammonite.ops.{Path, RelPath, empty, mkdir, read} +import mill.eval.PathRef +import mill.util.Ctx + +object Util { + def download(url: String, dest: RelPath = "download")(implicit ctx: Ctx.Dest) = { + val out = ctx.dest / dest + + val website = new java.net.URI(url).toURL + val rbc = java.nio.channels.Channels.newChannel(website.openStream) + try{ + val fos = new java.io.FileOutputStream(out.toIO) + try{ + fos.getChannel.transferFrom(rbc, 0, java.lang.Long.MAX_VALUE) + PathRef(out) + } finally{ + fos.close() + } + } finally{ + rbc.close() + } + } + + def downloadUnpackZip(url: String, dest: RelPath = "unpacked") + (implicit ctx: Ctx.Dest) = { + + val tmpName = if (dest == empty / "tmp.zip") "tmp2.zip" else "tmp.zip" + val downloaded = download(url, tmpName) + unpackZip(downloaded.path, dest) + } + + def unpackZip(src: Path, dest: RelPath = "unpacked") + (implicit ctx: Ctx.Dest) = { + + val byteStream = read.getInputStream(src) + val zipStream = new java.util.zip.ZipInputStream(byteStream) + while({ + zipStream.getNextEntry match{ + case null => false + case entry => + if (!entry.isDirectory) { + val entryDest = ctx.dest / dest / RelPath(entry.getName) + mkdir(entryDest / ammonite.ops.up) + val fileOut = new java.io.FileOutputStream(entryDest.toString) + val buffer = new Array[Byte](4096) + while ( { + zipStream.read(buffer) match { + case -1 => false + case n => + fileOut.write(buffer, 0, n) + true + } + }) () + fileOut.close() + } + zipStream.closeEntry() + true + } + })() + PathRef(ctx.dest / dest) + } +} diff --git a/main/src/mill/package.scala b/main/src/mill/package.scala new file mode 100644 index 00000000..93916c8b --- /dev/null +++ b/main/src/mill/package.scala @@ -0,0 +1,12 @@ +import mill.util.JsonFormatters + +package object mill extends JsonFormatters{ + val T = define.Target + type T[T] = define.Target[T] + val PathRef = mill.eval.PathRef + type PathRef = mill.eval.PathRef + type Module = define.Module + type Cross[T] = define.Cross[T] + type Agg[T] = util.Loose.Agg[T] + val Agg = util.Loose.Agg +} diff --git a/main/test/resources/examples/javac/build.sc b/main/test/resources/examples/javac/build.sc new file mode 100644 index 00000000..0783ac17 --- /dev/null +++ b/main/test/resources/examples/javac/build.sc @@ -0,0 +1,23 @@ +import ammonite.ops._ +import mill.T +import mill.eval.JavaCompileJarTests.compileAll +import mill.eval.PathRef +import mill.modules.Jvm +import mill.util.Loose + +def sourceRootPath = millSourcePath / 'src +def resourceRootPath = millSourcePath / '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)(T.ctx().dest) +} diff --git a/main/test/resources/examples/javac/resources/hello.txt b/main/test/resources/examples/javac/resources/hello.txt new file mode 100644 index 00000000..5e1c309d --- /dev/null +++ b/main/test/resources/examples/javac/resources/hello.txt @@ -0,0 +1 @@ +Hello World
\ No newline at end of file diff --git a/main/test/resources/examples/javac/src/Bar.java b/main/test/resources/examples/javac/src/Bar.java new file mode 100644 index 00000000..4e30c89b --- /dev/null +++ b/main/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/main/test/resources/examples/javac/src/Foo.java b/main/test/resources/examples/javac/src/Foo.java new file mode 100644 index 00000000..e694f9fa --- /dev/null +++ b/main/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/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)) + } + } + } +} |