From 3e6a101fe8c5e50903416dcb13a821cc815ae02b Mon Sep 17 00:00:00 2001 From: Alexandre Archambault Date: Wed, 25 Sep 2024 16:35:21 +0200 Subject: [PATCH 1/5] NIT Refactoring for readability --- main/eval/src/mill/eval/GroupEvaluator.scala | 43 ++++++++++---------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/main/eval/src/mill/eval/GroupEvaluator.scala b/main/eval/src/mill/eval/GroupEvaluator.scala index 7d1529801ea..a1f6d77dc71 100644 --- a/main/eval/src/mill/eval/GroupEvaluator.scala +++ b/main/eval/src/mill/eval/GroupEvaluator.scala @@ -364,28 +364,27 @@ private[mill] trait GroupEvaluator { inputsHash: Int, labelled: Terminal.Labelled[_] ): Unit = { - labelled.task.asWorker match { - case Some(w) => - workerCache.synchronized { - workerCache.update(w.ctx.segments, (workerCacheHash(inputsHash), v)) - } - case None => - val terminalResult = labelled - .task - .writerOpt - .asInstanceOf[Option[upickle.default.Writer[Any]]] - .map { w => upickle.default.writeJs(v.value)(w) } - - for (json <- terminalResult) { - os.write.over( - metaPath, - upickle.default.stream( - Evaluator.Cached(json, hashCode, inputsHash), - indent = 4 - ), - createFolders = true - ) - } + for (w <- labelled.task.asWorker) + workerCache.synchronized { + workerCache.update(w.ctx.segments, (workerCacheHash(inputsHash), v)) + } + + val terminalResult = labelled + .task + .writerOpt + .map { w => + upickle.default.writeJs(v.value)(w.asInstanceOf[upickle.default.Writer[Any]]) + } + + for (json <- terminalResult) { + os.write.over( + metaPath, + upickle.default.stream( + Evaluator.Cached(json, hashCode, inputsHash), + indent = 4 + ), + createFolders = true + ) } } From cfcd3a245cb070149529724f2b3a9ea2b05fe0ef Mon Sep 17 00:00:00 2001 From: Alexandre Archambault Date: Wed, 18 Sep 2024 17:56:52 +0200 Subject: [PATCH 2/5] Show workers in resolve / show / inspect too --- .../docannotations/resources/build.mill | 16 ++++++- .../src/DocAnnotationsTests.scala | 16 +++++++ main/eval/src/mill/eval/GroupEvaluator.scala | 9 ++++ main/resolve/src/mill/resolve/Resolve.scala | 15 ++++--- .../src/mill/resolve/ResolveCore.scala | 12 +++--- main/src/mill/main/RunScript.scala | 2 +- main/test/src/mill/main/MainModuleTests.scala | 43 +++++++++++++++++++ 7 files changed, 99 insertions(+), 14 deletions(-) diff --git a/integration/feature/docannotations/resources/build.mill b/integration/feature/docannotations/resources/build.mill index 6a12c87c5fb..d70236c0320 100644 --- a/integration/feature/docannotations/resources/build.mill +++ b/integration/feature/docannotations/resources/build.mill @@ -11,13 +11,27 @@ trait JUnitTests extends TestModule.Junit4 { def task = Task { "???" } + + /** + * *The worker* + */ + def theWorker = Task.Worker { + () + } } /** * The Core Module Docz! */ object core extends JavaModule { - object test extends JavaTests with JUnitTests + object test extends JavaTests with JUnitTests { + /** + * -> The worker <- + */ + def theWorker = Task.Worker { + () + } + } /** * Core Target Docz! diff --git a/integration/feature/docannotations/src/DocAnnotationsTests.scala b/integration/feature/docannotations/src/DocAnnotationsTests.scala index bfd237a9ae8..81ad43a2733 100644 --- a/integration/feature/docannotations/src/DocAnnotationsTests.scala +++ b/integration/feature/docannotations/src/DocAnnotationsTests.scala @@ -113,6 +113,22 @@ object DocAnnotationsTests extends UtestIntegrationTestSuite { ) ) + assert(eval(("inspect", "core.test.theWorker")).isSuccess) + val theWorkerInspect = out("inspect").json.str + + assert( + globMatches( + """core.test.theWorker(build.mill:...) + | -> The worker <- + | + | *The worker* + | + |Inputs: + |""".stripMargin, + theWorkerInspect + ) + ) + // Make sure both kebab-case and camelCase flags work, even though the // docs from `inspect` only show the kebab-case version assert(eval(("core.ivyDepsTree", "--withCompile", "--withRuntime")).isSuccess) diff --git a/main/eval/src/mill/eval/GroupEvaluator.scala b/main/eval/src/mill/eval/GroupEvaluator.scala index a1f6d77dc71..dcdcf2e7695 100644 --- a/main/eval/src/mill/eval/GroupEvaluator.scala +++ b/main/eval/src/mill/eval/GroupEvaluator.scala @@ -375,6 +375,15 @@ private[mill] trait GroupEvaluator { .map { w => upickle.default.writeJs(v.value)(w.asInstanceOf[upickle.default.Writer[Any]]) } + .orElse { + labelled.task.asWorker.map { w => + ujson.Obj( + "worker" -> ujson.Str(labelled.segments.render), + "toString" -> ujson.Str(v.value.toString), + "inputsHash" -> ujson.Num(inputsHash) + ) + } + } for (json <- terminalResult) { os.write.over( diff --git a/main/resolve/src/mill/resolve/Resolve.scala b/main/resolve/src/mill/resolve/Resolve.scala index 09c0edc6590..f011ca46b29 100644 --- a/main/resolve/src/mill/resolve/Resolve.scala +++ b/main/resolve/src/mill/resolve/Resolve.scala @@ -41,10 +41,10 @@ object Resolve { allowPositionalCommandArgs: Boolean ) = { val taskList = resolved.map { - case r: Resolved.Target => + case r: Resolved.NamedTask => val instantiated = ResolveCore .instantiateModule(rootModule, r.segments.init) - .flatMap(instantiateTarget(r, _)) + .flatMap(instantiateNamedTask(r, _)) instantiated.map(Some(_)) @@ -76,7 +76,7 @@ object Resolve { directChildrenOrErr.flatMap(directChildren => directChildren.head match { - case r: Resolved.Target => instantiateTarget(r, value).map(Some(_)) + case r: Resolved.NamedTask => instantiateNamedTask(r, value).map(Some(_)) case r: Resolved.Command => instantiateCommand( rootModule, @@ -104,13 +104,16 @@ object Resolve { items.distinctBy(_.ctx.segments) } - private def instantiateTarget(r: Resolved.Target, p: Module): Either[String, Target[_]] = { + private def instantiateNamedTask( + r: Resolved.NamedTask, + p: Module + ): Either[String, NamedTask[_]] = { val definition = Reflect - .reflect(p.getClass, classOf[Target[_]], _ == r.segments.parts.last, true) + .reflect(p.getClass, classOf[NamedTask[_]], _ == r.segments.parts.last, true) .head ResolveCore.catchWrapException( - definition.invoke(p).asInstanceOf[Target[_]] + definition.invoke(p).asInstanceOf[NamedTask[_]] ) } diff --git a/main/resolve/src/mill/resolve/ResolveCore.scala b/main/resolve/src/mill/resolve/ResolveCore.scala index 31a7d632f39..5c3a7396a74 100644 --- a/main/resolve/src/mill/resolve/ResolveCore.scala +++ b/main/resolve/src/mill/resolve/ResolveCore.scala @@ -29,7 +29,7 @@ private object ResolveCore { object Resolved { case class Module(segments: Segments, cls: Class[_]) extends Resolved - case class Target(segments: Segments) extends Resolved + case class NamedTask(segments: Segments) extends Resolved case class Command(segments: Segments) extends Resolved } @@ -327,7 +327,7 @@ private object ResolveCore { .map( _.map { case (Resolved.Module(s, cls), _) => Resolved.Module(segments ++ s, cls) - case (Resolved.Target(s), _) => Resolved.Target(segments ++ s) + case (Resolved.NamedTask(s), _) => Resolved.NamedTask(segments ++ s) case (Resolved.Command(s), _) => Resolved.Command(segments ++ s) } .toSet @@ -376,10 +376,10 @@ private object ResolveCore { } } - val targets = Reflect - .reflect(cls, classOf[Target[_]], namePred, noParams = true) + val namedTasks = Reflect + .reflect(cls, classOf[NamedTask[_]], namePred, noParams = true) .map { m => - Resolved.Target(Segments.labels(decode(m.getName))) -> + Resolved.NamedTask(Segments.labels(decode(m.getName))) -> None } @@ -388,7 +388,7 @@ private object ResolveCore { .map(m => decode(m.getName)) .map { name => Resolved.Command(Segments.labels(name)) -> None } - modulesOrErr.map(_ ++ targets ++ commands) + modulesOrErr.map(_ ++ namedTasks ++ commands) } def notFoundResult( diff --git a/main/src/mill/main/RunScript.scala b/main/src/mill/main/RunScript.scala index 4c2d7070bbb..0e48f14818a 100644 --- a/main/src/mill/main/RunScript.scala +++ b/main/src/mill/main/RunScript.scala @@ -62,7 +62,7 @@ object RunScript { case 0 => val nameAndJson = for (t <- targets.toSeq) yield { t match { - case t: mill.define.NamedTask[_] => + case t: mill.define.NamedTask[_] if t.writerOpt.isDefined || t.asWorker.isDefined => val jsonFile = EvaluatorPaths.resolveDestPaths(evaluator.outPath, t).meta val metadata = upickle.default.read[Evaluator.Cached](ujson.read(jsonFile.toIO)) Some((t.toString, metadata.value)) diff --git a/main/test/src/mill/main/MainModuleTests.scala b/main/test/src/mill/main/MainModuleTests.scala index 6f5b552ccdf..82d0790c7bc 100644 --- a/main/test/src/mill/main/MainModuleTests.scala +++ b/main/test/src/mill/main/MainModuleTests.scala @@ -27,6 +27,18 @@ object MainModuleTests extends TestSuite { Map("1" -> "hello", "2" -> "world") } def helloCommand(x: Int, y: Task[String]) = Task.Command { (x, y(), hello()) } + + /** + * The hello worker + */ + def helloWorker = Task.Worker { + // non-JSON-serializable, but everything should work fine nonetheless + new AutoCloseable { + def close() = () + override def toString = + "theHelloWorker" + } + } override lazy val millDiscover: Discover = Discover[this.type] } @@ -93,6 +105,17 @@ object MainModuleTests extends TestSuite { res.contains("hello") ) } + test("worker") - UnitTester(mainModule, null).scoped { eval => + val Right(result) = eval.apply("inspect", "helloWorker") + + val Seq(res: String) = result.value + assert( + res.startsWith("helloWorker("), + res.contains("MainModuleTests.scala:"), + res.contains("The hello worker"), + res.contains("hello") + ) + } } test("show") { @@ -173,6 +196,14 @@ object MainModuleTests extends TestSuite { val Seq(res) = result.value assert(res == ujson.Arr(1337, "lol", ujson.Arr("hello", "world"))) } + + test("worker") { + val Right(result) = evaluator.apply("show", "helloWorker") + val Seq(res: ujson.Obj) = result.value + assert(res("toString").str == "theHelloWorker") + assert(res("worker").str == "helloWorker") + assert(res("inputsHash").numOpt.isDefined) + } } test("showNamed") { @@ -209,6 +240,18 @@ object MainModuleTests extends TestSuite { } } + test("resolve") { + UnitTester(mainModule, null).scoped { eval => + val Right(result) = eval.apply("resolve", "_") + + val Seq(res: Seq[String]) = result.value + assert(res.contains("hello")) + assert(res.contains("hello2")) + assert(res.contains("helloCommand")) + assert(res.contains("helloWorker")) + } + } + test("clean") { val ev = UnitTester(cleanModule, null) val out = ev.evaluator.outPath From c2a9abc9c112ee65cff151487c9f540b58db9034 Mon Sep 17 00:00:00 2001 From: Alexandre Archambault Date: Wed, 25 Sep 2024 16:47:57 +0200 Subject: [PATCH 3/5] ? --- main/src/mill/main/RunScript.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/src/mill/main/RunScript.scala b/main/src/mill/main/RunScript.scala index 0e48f14818a..4c2d7070bbb 100644 --- a/main/src/mill/main/RunScript.scala +++ b/main/src/mill/main/RunScript.scala @@ -62,7 +62,7 @@ object RunScript { case 0 => val nameAndJson = for (t <- targets.toSeq) yield { t match { - case t: mill.define.NamedTask[_] if t.writerOpt.isDefined || t.asWorker.isDefined => + case t: mill.define.NamedTask[_] => val jsonFile = EvaluatorPaths.resolveDestPaths(evaluator.outPath, t).meta val metadata = upickle.default.read[Evaluator.Cached](ujson.read(jsonFile.toIO)) Some((t.toString, metadata.value)) From c941910ce665f2e4fa61a737ae1b48f75f2d39a4 Mon Sep 17 00:00:00 2001 From: Alexandre Archambault Date: Wed, 18 Sep 2024 17:54:56 +0200 Subject: [PATCH 4/5] Clean workers too from the clean command --- main/define/src/mill/define/Segments.scala | 3 + main/eval/src/mill/eval/Evaluator.scala | 7 ++ main/src/mill/main/MainModule.scala | 17 ++- main/test/src/mill/main/MainModuleTests.scala | 111 ++++++++++++++++++ 4 files changed, 134 insertions(+), 4 deletions(-) diff --git a/main/define/src/mill/define/Segments.scala b/main/define/src/mill/define/Segments.scala index 82ab529fd32..e5cd4d14260 100644 --- a/main/define/src/mill/define/Segments.scala +++ b/main/define/src/mill/define/Segments.scala @@ -14,6 +14,9 @@ case class Segments private (value: Seq[Segment]) { def ++(other: Seq[Segment]): Segments = Segments(value ++ other) def ++(other: Segments): Segments = Segments(value ++ other.value) + def startsWith(prefix: Segments): Boolean = + value.startsWith(prefix.value) + def parts: List[String] = value.toList match { case Nil => Nil case Segment.Label(head) :: rest => diff --git a/main/eval/src/mill/eval/Evaluator.scala b/main/eval/src/mill/eval/Evaluator.scala index 1a1d2853cb5..42829650626 100644 --- a/main/eval/src/mill/eval/Evaluator.scala +++ b/main/eval/src/mill/eval/Evaluator.scala @@ -21,7 +21,14 @@ trait Evaluator { def outPath: os.Path def externalOutPath: os.Path def pathsResolver: EvaluatorPathsResolver + // TODO In 0.13.0, workerCache should have the type of mutableWorkerCache, + // while the latter should be removed def workerCache: collection.Map[Segments, (Int, Val)] + private[mill] final def mutableWorkerCache: collection.mutable.Map[Segments, (Int, Val)] = + workerCache match { + case mut: collection.mutable.Map[Segments, (Int, Val)] => mut + case _ => sys.error("Evaluator#workerCache must be a mutable map") + } def disableCallgraphInvalidation: Boolean = false @deprecated( diff --git a/main/src/mill/main/MainModule.scala b/main/src/mill/main/MainModule.scala index 54421527574..109c37786d1 100644 --- a/main/src/mill/main/MainModule.scala +++ b/main/src/mill/main/MainModule.scala @@ -2,7 +2,7 @@ package mill.main import java.util.concurrent.LinkedBlockingQueue import mill.define.{BaseModule0, Command, NamedTask, Segments, Target, Task} -import mill.api.{Ctx, Logger, PathRef, Result} +import mill.api.{Ctx, Logger, PathRef, Result, Val} import mill.eval.{Evaluator, EvaluatorPaths, Terminal} import mill.resolve.{Resolve, SelectMode} import mill.resolve.SelectMode.Separated @@ -328,14 +328,14 @@ trait MainModule extends BaseModule0 { val pathsToRemove = if (targets.isEmpty) - Right(os.list(rootDir).filterNot(keepPath)) + Right((os.list(rootDir).filterNot(keepPath), List(mill.define.Segments()))) else mill.resolve.Resolve.Segments.resolve( evaluator.rootModule, targets, SelectMode.Multi ).map { ts => - ts.flatMap { segments => + val allPaths = ts.flatMap { segments => val evPaths = EvaluatorPaths.resolveDestPaths(rootDir, segments) val paths = Seq(evPaths.dest, evPaths.meta, evPaths.log) val potentialModulePath = rootDir / EvaluatorPaths.makeSegmentStrings(segments) @@ -348,12 +348,21 @@ trait MainModule extends BaseModule0 { paths :+ potentialModulePath } else paths } + (allPaths, ts) } pathsToRemove match { case Left(err) => Result.Failure(err) - case Right(paths) => + case Right((paths, allSegments)) => + for { + workerSegments <- evaluator.workerCache.keys.toList + if allSegments.exists(workerSegments.startsWith) + (_, Val(closeable: AutoCloseable)) <- evaluator.mutableWorkerCache.remove(workerSegments) + } { + closeable.close() + } + val existing = paths.filter(p => os.exists(p)) Target.log.debug(s"Cleaning ${existing.size} paths ...") existing.foreach(os.remove.all) diff --git a/main/test/src/mill/main/MainModuleTests.scala b/main/test/src/mill/main/MainModuleTests.scala index 82d0790c7bc..8faaa7009d7 100644 --- a/main/test/src/mill/main/MainModuleTests.scala +++ b/main/test/src/mill/main/MainModuleTests.scala @@ -9,6 +9,8 @@ import utest.{TestSuite, Tests, assert, test} import java.io.{ByteArrayOutputStream, PrintStream} +import scala.collection.mutable + object MainModuleTests extends TestSuite { object mainModule extends TestBaseModule with MainModule { @@ -72,6 +74,55 @@ object MainModuleTests extends TestSuite { } } + class TestWorker(val name: String, workers: mutable.HashSet[TestWorker]) extends AutoCloseable { + + workers.synchronized { + workers.add(this) + } + + var closed = false + def close(): Unit = + if (!closed) { + workers.synchronized { + workers.remove(this) + } + closed = true + } + + override def toString(): String = + s"TestWorker($name)@${Integer.toHexString(System.identityHashCode(this))}" + } + + class WorkerModule(workers: mutable.HashSet[TestWorker]) extends TestBaseModule with MainModule { + + trait Cleanable extends Module { + def theWorker = Task.Worker { + new TestWorker("shared", workers) + } + } + + object foo extends Cleanable { + object sub extends Cleanable + } + object bar extends Cleanable { + def theWorker = Task.Worker { + new TestWorker("bar", workers) + } + } + object bazz extends Cross[Bazz]("1", "2", "3") + trait Bazz extends Cleanable with Cross.Module[String] + + def all = Task { + foo.theWorker() + bar.theWorker() + bazz("1").theWorker() + bazz("2").theWorker() + bazz("3").theWorker() + + () + } + } + override def tests: Tests = Tests { test("inspect") { @@ -317,5 +368,65 @@ object MainModuleTests extends TestSuite { ) } } + + test("cleanWorker") { + test("all") { + val workers = new mutable.HashSet[TestWorker] + val workerModule = new WorkerModule(workers) + val ev = UnitTester(workerModule, null) + + val r1 = ev.evaluator.evaluate(Agg(workerModule.all)) + assert(r1.failing.keyCount == 0) + assert(workers.size == 5) + + val r2 = ev.evaluator.evaluate(Agg(workerModule.clean(ev.evaluator))) + assert(r2.failing.keyCount == 0) + assert(workers.isEmpty) + } + + test("single-target") { + val workers = new mutable.HashSet[TestWorker] + val workerModule = new WorkerModule(workers) + val ev = UnitTester(workerModule, null) + + val r1 = ev.evaluator.evaluate(Agg(workerModule.all)) + assert(r1.failing.keyCount == 0) + assert(workers.size == 5) + + val r2 = ev.evaluator.evaluate(Agg(workerModule.clean(ev.evaluator, "foo.theWorker"))) + assert(r2.failing.keyCount == 0) + assert(workers.size == 4) + + val r3 = ev.evaluator.evaluate(Agg(workerModule.clean(ev.evaluator, "bar.theWorker"))) + assert(r3.failing.keyCount == 0) + assert(workers.size == 3) + + val r4 = ev.evaluator.evaluate(Agg(workerModule.clean(ev.evaluator, "bazz[1].theWorker"))) + assert(r4.failing.keyCount == 0) + assert(workers.size == 2) + } + + test("single-module") { + val workers = new mutable.HashSet[TestWorker] + val workerModule = new WorkerModule(workers) + val ev = UnitTester(workerModule, null) + + val r1 = ev.evaluator.evaluate(Agg(workerModule.all)) + assert(r1.failing.keyCount == 0) + assert(workers.size == 5) + + val r2 = ev.evaluator.evaluate(Agg(workerModule.clean(ev.evaluator, "foo"))) + assert(r2.failing.keyCount == 0) + assert(workers.size == 4) + + val r3 = ev.evaluator.evaluate(Agg(workerModule.clean(ev.evaluator, "bar"))) + assert(r3.failing.keyCount == 0) + assert(workers.size == 3) + + val r4 = ev.evaluator.evaluate(Agg(workerModule.clean(ev.evaluator, "bazz[1]"))) + assert(r4.failing.keyCount == 0) + assert(workers.size == 2) + } + } } } From d155578e298473bb5b0de8eb7f55bddb79dcf840 Mon Sep 17 00:00:00 2001 From: Alexandre Archambault Date: Wed, 25 Sep 2024 17:58:09 +0200 Subject: [PATCH 5/5] Clean workers upon manual removal of their metadata in out dir --- main/eval/src/mill/eval/GroupEvaluator.scala | 14 ++++-- main/test/src/mill/main/MainModuleTests.scala | 50 +++++++++++++++++++ 2 files changed, 61 insertions(+), 3 deletions(-) diff --git a/main/eval/src/mill/eval/GroupEvaluator.scala b/main/eval/src/mill/eval/GroupEvaluator.scala index dcdcf2e7695..b2566465007 100644 --- a/main/eval/src/mill/eval/GroupEvaluator.scala +++ b/main/eval/src/mill/eval/GroupEvaluator.scala @@ -149,7 +149,14 @@ private[mill] trait GroupEvaluator { val cached = loadCachedJson(logger, inputsHash, labelled, paths) - val upToDateWorker = loadUpToDateWorker(logger, inputsHash, labelled) + val upToDateWorker = loadUpToDateWorker( + logger, + inputsHash, + labelled, + forceDiscard = + // worker metadata file removed by user, let's recompute the worker + cached.isEmpty + ) upToDateWorker.map((_, inputsHash)) orElse cached.flatMap(_._2) match { case Some((v, hashCode)) => @@ -444,7 +451,8 @@ private[mill] trait GroupEvaluator { private def loadUpToDateWorker( logger: ColorLogger, inputsHash: Int, - labelled: Terminal.Labelled[_] + labelled: Terminal.Labelled[_], + forceDiscard: Boolean ): Option[Val] = { labelled.task.asWorker .flatMap { w => @@ -454,7 +462,7 @@ private[mill] trait GroupEvaluator { } .flatMap { case (cachedHash, upToDate) - if cachedHash == workerCacheHash(inputsHash) => + if cachedHash == workerCacheHash(inputsHash) && !forceDiscard => Some(upToDate) // worker cached and up-to-date case (_, Val(obsolete: AutoCloseable)) => diff --git a/main/test/src/mill/main/MainModuleTests.scala b/main/test/src/mill/main/MainModuleTests.scala index 8faaa7009d7..8e7a05d9c0d 100644 --- a/main/test/src/mill/main/MainModuleTests.scala +++ b/main/test/src/mill/main/MainModuleTests.scala @@ -3,6 +3,7 @@ package mill.main import mill.api.{PathRef, Result, Val} import mill.{Agg, T, Task} import mill.define.{Cross, Discover, Module} +import mill.main.client.OutFiles import mill.testkit.UnitTester import mill.testkit.TestBaseModule import utest.{TestSuite, Tests, assert, test} @@ -406,6 +407,55 @@ object MainModuleTests extends TestSuite { assert(workers.size == 2) } + test("single-target via rm") { + val workers = new mutable.HashSet[TestWorker] + val workerModule = new WorkerModule(workers) + val ev = UnitTester(workerModule, null) + + ev.evaluator.evaluate(Agg(workerModule.foo.theWorker)) + .ensuring(_.failing.keyCount == 0) + assert(workers.size == 1) + + val originalFooWorker = workers.head + + ev.evaluator.evaluate(Agg(workerModule.bar.theWorker)) + .ensuring(_.failing.keyCount == 0) + assert(workers.size == 2) + assert(workers.exists(_ eq originalFooWorker)) + + val originalBarWorker = workers.filter(_ ne originalFooWorker).head + + ev.evaluator.evaluate(Agg(workerModule.foo.theWorker)) + .ensuring(_.failing.keyCount == 0) + assert(workers.size == 2) + assert(workers.exists(_ eq originalFooWorker)) + + ev.evaluator.evaluate(Agg(workerModule.bar.theWorker)) + .ensuring(_.failing.keyCount == 0) + assert(workers.size == 2) + assert(workers.exists(_ eq originalBarWorker)) + + val outDir = os.Path(OutFiles.out, workerModule.millSourcePath) + + assert(!originalFooWorker.closed) + os.remove(outDir / "foo/theWorker.json") + + ev.evaluator.evaluate(Agg(workerModule.foo.theWorker)) + .ensuring(_.failing.keyCount == 0) + assert(workers.size == 2) + assert(!workers.exists(_ eq originalFooWorker)) + assert(originalFooWorker.closed) + + assert(!originalBarWorker.closed) + os.remove(outDir / "bar/theWorker.json") + + ev.evaluator.evaluate(Agg(workerModule.bar.theWorker)) + .ensuring(_.failing.keyCount == 0) + assert(workers.size == 2) + assert(!workers.exists(_ eq originalBarWorker)) + assert(originalBarWorker.closed) + } + test("single-module") { val workers = new mutable.HashSet[TestWorker] val workerModule = new WorkerModule(workers)