Skip to content

Commit 308937f

Browse files
authored
Make unsuccessful tasks always run the subsequent time when --watch is active (#6263)
Fixes #6258. We keep track of the unsuccessful tasks in `SelectiveExecution.Metadata#forceRunTasks` during `--watch` and force those to re-run next evaluation This ensures that if a user does `--watch` and makes edits to code and config, they do not see a successful run until all the tasks selected by the `--watch` are successful. Previously, a user editing a single file will result in only a subset of tasks running, potentially resulting in previously-skipped tasks now being selected out and ending up never being run Also tested manually by setting up a `--watch __.compile` and then doing a find-and-replacec `new ([a-zA-Z]+)\(` to `$1(` with IntelliJ. Without this PR, after fixing the first few failures, the `--watch` appears to succeed, but if I press Enter to re-run everything I seem more failures. With this PR, the `--watch` fails consistently until I fix all failures, and then pressing `Enter` to re-run does not show any new failures
1 parent 15dd9fb commit 308937f

File tree

7 files changed

+326
-276
lines changed

7 files changed

+326
-276
lines changed

core/api/src/mill/api/SelectiveExecution.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ object SelectiveExecution {
4040
case class Metadata(
4141
inputHashes: Map[String, Int],
4242
codeSignatures: Map[String, Int],
43-
@com.lihaoyi.unroll buildOverrideSignatures: Map[String, Int] = Map()
43+
@com.lihaoyi.unroll buildOverrideSignatures: Map[String, Int] = Map(),
44+
@com.lihaoyi.unroll forceRunTasks: Set[String] = Set()
4445
)
4546
object Metadata {
4647
case class Computed(

core/eval/src/mill/eval/EvaluatorImpl.scala

Lines changed: 38 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -271,54 +271,46 @@ final class EvaluatorImpl(
271271
serialCommandExec = serialCommandExec
272272
)
273273

274-
@scala.annotation.nowarn("msg=cannot be checked at runtime")
275-
val watched = (evaluated.transitiveResults.iterator ++ selectiveResults)
276-
.collect {
277-
case (_: Task.Sources, ExecResult.Success(Val(ps: Seq[PathRef]))) =>
278-
ps.map(r => Watchable.Path(r.path.toNIO, r.quick, r.sig))
279-
case (_: Task.Source, ExecResult.Success(Val(p: PathRef))) =>
280-
Seq(Watchable.Path(p.path.toNIO, p.quick, p.sig))
281-
case (t: Task.Input[_], result) =>
282-
283-
val ctx = new mill.api.TaskCtx.Impl(
284-
args = Vector(),
285-
dest0 = () => null,
286-
log = logger,
287-
env = this.execution.env,
288-
reporter = reporter,
289-
testReporter = testReporter,
290-
workspace = workspace,
291-
_systemExitWithReason = (reason, exitCode) =>
292-
throw Exception(s"systemExit called: reason=$reason, exitCode=$exitCode"),
293-
fork = null,
294-
jobs = execution.effectiveThreadCount,
295-
offline = offline
296-
)
297-
val pretty = t.ctx0.fileName + ":" + t.ctx0.lineNum
298-
Seq(Watchable.Value(
299-
() => t.evaluate(ctx).hashCode(),
300-
result.map(_.value).hashCode(),
301-
pretty
302-
))
303-
}
304-
.flatten
305-
.toSeq
306-
307-
maybeNewMetadata.foreach { newMetadata =>
308-
val enclosingModules = PlanImpl
309-
.plan(tasks)
310-
.transitive
311-
.collect { case n: Task.Named[_] => n.ctx.enclosingModule }
312-
.distinct
313-
314-
val scriptBuildOverrides = enclosingModules.flatMap(_.moduleDynamicBuildOverrides)
274+
val allResults = evaluated.transitiveResults ++ selectiveResults
315275

316-
val allBuildOverrides = (staticBuildOverrides ++ scriptBuildOverrides)
317-
.map { case (k, v) => (k, v.##) }
276+
@scala.annotation.nowarn("msg=cannot be checked at runtime")
277+
val watched = allResults.collect {
278+
case (_: Task.Sources, ExecResult.Success(Val(ps: Seq[PathRef]))) =>
279+
ps.map(r => Watchable.Path(r.path.toNIO, r.quick, r.sig))
280+
case (_: Task.Source, ExecResult.Success(Val(p: PathRef))) =>
281+
Seq(Watchable.Path(p.path.toNIO, p.quick, p.sig))
282+
case (t: Task.Input[_], result) =>
283+
284+
val ctx = new mill.api.TaskCtx.Impl(
285+
args = Vector(),
286+
dest0 = () => null,
287+
log = logger,
288+
env = this.execution.env,
289+
reporter = reporter,
290+
testReporter = testReporter,
291+
workspace = workspace,
292+
_systemExitWithReason = (reason, exitCode) =>
293+
throw Exception(s"systemExit called: reason=$reason, exitCode=$exitCode"),
294+
fork = null,
295+
jobs = execution.effectiveThreadCount,
296+
offline = offline
297+
)
298+
val pretty = t.ctx0.fileName + ":" + t.ctx0.lineNum
299+
Seq(Watchable.Value(
300+
() => t.evaluate(ctx).hashCode(),
301+
result.map(_.value).hashCode(),
302+
pretty
303+
))
304+
}.flatten.toVector
305+
306+
for (newMetadata <- maybeNewMetadata) {
307+
val failingTaskNames = allResults
308+
.collect { case (t: Task.Named[_], r) if r.asSuccess.isEmpty => t.ctx.segments.render }
309+
.toSet
318310

319-
this.selective.saveMetadata(
320-
SelectiveExecution.Metadata(newMetadata.inputHashes, codeSignatures, allBuildOverrides)
321-
)
311+
// For tasks that were not successful, force them to re-run next time even
312+
// if not changed so the user can see that there are still failures remaining
313+
selective.saveMetadata(newMetadata.copy(forceRunTasks = failingTaskNames))
322314
}
323315

324316
val errorStr = ExecutionResultsApi.formatFailing(evaluated)

core/eval/src/mill/eval/SelectiveExecutionImpl.scala

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,9 @@ class SelectiveExecutionImpl(evaluator: Evaluator)
6363
newHashes.buildOverrideSignatures
6464
)
6565

66-
val changedRootTasks = (changedInputNames ++ changedCodeNames ++ changedBuildOverrides)
67-
.flatMap(namesToTasks.get(_): Option[Task[?]])
66+
val changedRootTasks =
67+
(changedInputNames ++ changedCodeNames ++ changedBuildOverrides ++ oldHashes.forceRunTasks)
68+
.flatMap(namesToTasks.get(_): Option[Task[?]])
6869

6970
val allNodes = breadthFirst(transitiveNamed.map(t => t: Task[?]))(_.inputs)
7071
val downstreamEdgeMap = SpanningForest.reverseEdges(allNodes.map(t => (t, t.inputs)))
@@ -117,7 +118,7 @@ class SelectiveExecutionImpl(evaluator: Evaluator)
117118
val transitiveNamed = PlanImpl.transitiveNamed(tasks)
118119
val oldMetadata = upickle.read[SelectiveExecution.Metadata](oldMetadataTxt)
119120
val (changedRootTasks, downstreamTasks) =
120-
evaluator.selective.computeDownstream(
121+
computeDownstream(
121122
transitiveNamed,
122123
oldMetadata,
123124
computedMetadata.metadata
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package mill.integration
2+
import mill.testkit.UtestIntegrationTestSuite
3+
import scala.concurrent.duration._
4+
5+
import utest._
6+
import utest.asserts.{RetryMax, RetryInterval}
7+
8+
object SelectiveExecutionChangedCodeTests extends UtestIntegrationTestSuite {
9+
implicit val retryMax: RetryMax = RetryMax(120.seconds)
10+
implicit val retryInterval: RetryInterval = RetryInterval(1.seconds)
11+
val tests: Tests = Tests {
12+
test("changed-code") - integrationTest { tester =>
13+
import tester._
14+
15+
// Check method body code changes correctly trigger downstream evaluation
16+
eval(
17+
("selective.prepare", "{foo.fooCommand,bar.barCommand}"),
18+
check = true,
19+
stderr = os.Inherit
20+
)
21+
modifyFile(workspacePath / "build.mill", _.replace("\"barHelper \"", "\"barHelper! \""))
22+
val cached1 = eval(
23+
("selective.run", "{foo.fooCommand,bar.barCommand}"),
24+
check = true,
25+
stderr = os.Inherit
26+
)
27+
28+
assert(!cached1.out.contains("Computing fooCommand"))
29+
assert(cached1.out.contains("Computing barCommand"))
30+
31+
// Check module body code changes correctly trigger downstream evaluation
32+
eval(
33+
("selective.prepare", "{foo.fooCommand,bar.barCommand}"),
34+
check = true,
35+
stderr = os.Inherit
36+
)
37+
modifyFile(
38+
workspacePath / "build.mill",
39+
_.replace("object foo extends Module {", "object foo extends Module { println(123)")
40+
)
41+
val cached2 = eval(
42+
("selective.run", "{foo.fooCommand,bar.barCommand}"),
43+
check = true,
44+
stderr = os.Inherit
45+
)
46+
47+
assert(cached2.out.contains("Computing fooCommand"))
48+
assert(!cached2.out.contains("Computing barCommand"))
49+
}
50+
51+
}
52+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package mill.integration
2+
import mill.testkit.UtestIntegrationTestSuite
3+
import scala.concurrent.duration._
4+
5+
import utest._
6+
import utest.asserts.{RetryMax, RetryInterval}
7+
8+
object SelectiveExecutionChangedInputsTests extends UtestIntegrationTestSuite {
9+
implicit val retryMax: RetryMax = RetryMax(120.seconds)
10+
implicit val retryInterval: RetryInterval = RetryInterval(1.seconds)
11+
val tests: Tests = Tests {
12+
test("changed-inputs") - integrationTest { tester =>
13+
import tester._
14+
15+
eval(
16+
("selective.prepare", "{foo.fooCommand,bar.barCommand}"),
17+
check = true,
18+
stderr = os.Inherit
19+
)
20+
21+
// no op
22+
val noOp = eval(
23+
("selective.run", "{foo.fooCommand,bar.barCommand}"),
24+
check = true,
25+
stderr = os.Inherit
26+
)
27+
28+
assert(!noOp.out.contains("Computing fooCommand"))
29+
assert(!noOp.out.contains("Computing barCommand"))
30+
31+
// After one input changed
32+
modifyFile(workspacePath / "bar/bar.txt", _ + "!")
33+
34+
val resolve = eval(("selective.resolve", "{foo.fooCommand,bar.barCommand}"), check = true)
35+
assert(resolve.out == "bar.barCommand")
36+
37+
val cached = eval(
38+
("selective.run", "{foo.fooCommand,bar.barCommand}"),
39+
check = true,
40+
stderr = os.Inherit
41+
)
42+
43+
assert(!cached.out.contains("Computing fooCommand"))
44+
assert(cached.out.contains("Computing barCommand"))
45+
46+
// zero out the `mill-selective-execution.json` file to run all tasks
47+
os.write.over(workspacePath / "out/mill-selective-execution.json", "")
48+
val runAll = eval(
49+
("selective.run", "{foo.fooCommand,bar.barCommand}"),
50+
check = true,
51+
stderr = os.Inherit
52+
)
53+
54+
assert(runAll.out.contains("Computing fooCommand"))
55+
assert(runAll.out.contains("Computing barCommand"))
56+
}
57+
58+
test("changed-inputs-generic") - integrationTest { tester =>
59+
// Make sure you can run `selective.prepare` on a broader set of tasks than
60+
// `selective.resolve` or `selective.run` and thingsstill work
61+
import tester._
62+
63+
// `selective.prepare` defaults to `__` if no selector is passed
64+
eval(("selective.prepare"), check = true)
65+
modifyFile(workspacePath / "bar/bar.txt", _ + "!")
66+
67+
val resolve = eval(("selective.resolve", "bar.barCommand"), check = true)
68+
assert(resolve.out == "bar.barCommand")
69+
val resolve2 = eval(("selective.resolve", "foo.fooCommand"), check = true)
70+
assert(resolve2.out == "")
71+
72+
val cached = eval(
73+
("selective.run", "bar.barCommand"),
74+
check = true,
75+
stderr = os.Inherit
76+
)
77+
78+
assert(!cached.out.contains("Computing fooCommand"))
79+
assert(cached.out.contains("Computing barCommand"))
80+
}
81+
}
82+
}

0 commit comments

Comments
 (0)