Skip to content

Commit f66247f

Browse files
committed
add testQuick command
1 parent 0c4bc5f commit f66247f

File tree

6 files changed

+235
-103
lines changed

6 files changed

+235
-103
lines changed

core/codesig/src/mill/codesig/ReachabilityAnalysis.scala

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@ package mill.codesig
22

33
import mill.codesig.JvmModel.*
44
import mill.internal.{SpanningForest, Tarjans}
5-
import ujson.Obj
5+
import ujson.{Arr, Obj}
66
import upickle.default.{Writer, writer}
77

88
import scala.collection.immutable.SortedMap
9+
import scala.collection.mutable
910

1011
class CallGraphAnalysis(
1112
localSummary: LocalSummary,
@@ -81,18 +82,19 @@ class CallGraphAnalysis(
8182
logger.mandatoryLog(transitiveCallGraphHashes0)
8283
logger.log(transitiveCallGraphHashes)
8384

84-
lazy val spanningInvalidationTree: Obj = prevTransitiveCallGraphHashesOpt() match {
85+
lazy val (spanningInvalidationTree: Obj, invalidClassNames: Arr) = prevTransitiveCallGraphHashesOpt() match {
8586
case Some(prevTransitiveCallGraphHashes) =>
8687
CallGraphAnalysis.spanningInvalidationTree(
8788
prevTransitiveCallGraphHashes,
8889
transitiveCallGraphHashes0,
8990
indexToNodes,
9091
indexGraphEdges
9192
)
92-
case None => ujson.Obj()
93+
case None => ujson.Obj() -> ujson.Arr()
9394
}
9495

9596
logger.mandatoryLog(spanningInvalidationTree)
97+
logger.mandatoryLog(invalidClassNames)
9698
}
9799

98100
object CallGraphAnalysis {
@@ -115,7 +117,7 @@ object CallGraphAnalysis {
115117
transitiveCallGraphHashes0: Array[(CallGraphAnalysis.Node, Int)],
116118
indexToNodes: Array[Node],
117119
indexGraphEdges: Array[Array[Int]]
118-
): ujson.Obj = {
120+
): (ujson.Obj, ujson.Arr) = {
119121
val transitiveCallGraphHashes0Map = transitiveCallGraphHashes0.toMap
120122

121123
val nodesWithChangedHashes = indexGraphEdges
@@ -135,10 +137,23 @@ object CallGraphAnalysis {
135137
val reverseGraphEdges =
136138
indexGraphEdges.indices.map(reverseGraphMap.getOrElse(_, Array[Int]())).toArray
137139

138-
SpanningForest.spanningTreeToJsonTree(
139-
SpanningForest.apply(reverseGraphEdges, nodesWithChangedHashes, false),
140+
val spanningForest = SpanningForest.apply(reverseGraphEdges, nodesWithChangedHashes, false)
141+
142+
val spanningInvalidationTree = SpanningForest.spanningTreeToJsonTree(
143+
spanningForest,
140144
k => indexToNodes(k).toString
141145
)
146+
147+
val invalidSet = invalidClassNameSet(
148+
spanningForest,
149+
indexToNodes.map {
150+
case LocalDef(call) => call.cls.name
151+
case Call(call) => call.cls.name
152+
case ExternalClsCall(cls) => cls.name
153+
}
154+
)
155+
156+
(spanningInvalidationTree, invalidSet)
142157
}
143158

144159
def indexGraphEdges(
@@ -263,6 +278,24 @@ object CallGraphAnalysis {
263278
}
264279
}
265280

281+
private def invalidClassNameSet(
282+
spanningForest: SpanningForest.Node,
283+
indexToClassName: Array[String]
284+
): Set[String] = {
285+
val queue = mutable.ArrayBuffer.empty[(Int, SpanningForest.Node)]
286+
val result = mutable.Set.empty[String]
287+
288+
queue.appendAll(spanningForest.values)
289+
290+
while (queue.nonEmpty) {
291+
val (index, node) = queue.remove(0)
292+
result += indexToClassName(index)
293+
queue.appendAll(node.values)
294+
}
295+
296+
result.toSet
297+
}
298+
266299
/**
267300
* Represents the three types of nodes in our call graph. These are kept heterogeneous
268301
* because flattening them out into a homogenous graph of MethodDef -> MethodDef edges

runner/meta/src/mill/runner/meta/MillBuildRootModule.scala

Lines changed: 6 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -157,87 +157,12 @@ class MillBuildRootModule()(implicit
157157
}
158158
}
159159

160-
def codeSignatures: T[Map[String, Int]] = Task(persistent = true) {
161-
os.remove.all(Task.dest / "previous")
162-
if (os.exists(Task.dest / "current"))
163-
os.move.over(Task.dest / "current", Task.dest / "previous")
164-
val debugEnabled = Task.log.debugEnabled
165-
val codesig = mill.codesig.CodeSig
166-
.compute(
167-
classFiles = os.walk(compile().classes.path).filter(_.ext == "class"),
168-
upstreamClasspath = compileClasspath().toSeq.map(_.path),
169-
ignoreCall = { (callSiteOpt, calledSig) =>
170-
// We can ignore all calls to methods that look like Targets when traversing
171-
// the call graph. We can do this because we assume `def` Targets are pure,
172-
// and so any changes in their behavior will be picked up by the runtime build
173-
// graph evaluator without needing to be accounted for in the post-compile
174-
// bytecode callgraph analysis.
175-
def isSimpleTarget(desc: mill.codesig.JvmModel.Desc) =
176-
(desc.ret.pretty == classOf[mill.define.Target[?]].getName ||
177-
desc.ret.pretty == classOf[mill.define.Worker[?]].getName) &&
178-
desc.args.isEmpty
179-
180-
// We avoid ignoring method calls that are simple trait forwarders, because
181-
// we need the trait forwarders calls to be counted in order to wire up the
182-
// method definition that a Target is associated with during evaluation
183-
// (e.g. `myModuleObject.myTarget`) with its implementation that may be defined
184-
// somewhere else (e.g. `trait MyModuleTrait{ def myTarget }`). Only that one
185-
// step is necessary, after that the runtime build graph invalidation logic can
186-
// take over
187-
def isForwarderCallsiteOrLambda =
188-
callSiteOpt.nonEmpty && {
189-
val callSiteSig = callSiteOpt.get.sig
190-
191-
(callSiteSig.name == (calledSig.name + "$") &&
192-
callSiteSig.static &&
193-
callSiteSig.desc.args.size == 1)
194-
|| (
195-
// In Scala 3, lambdas are implemented by private instance methods,
196-
// not static methods, so they fall through the crack of "isSimpleTarget".
197-
// Here make the assumption that a zero-arg lambda called from a simpleTarget,
198-
// should in fact be tracked. e.g. see `integration.invalidation[codesig-hello]`,
199-
// where the body of the `def foo` target is a zero-arg lambda i.e. the argument
200-
// of `Cacher.cachedTarget`.
201-
// To be more precise I think ideally we should capture more information in the signature
202-
isSimpleTarget(callSiteSig.desc) && calledSig.name.contains("$anonfun")
203-
)
204-
}
205-
206-
// We ignore Commands for the same reason as we ignore Targets, and also because
207-
// their implementations get gathered up all the via the `Discover` macro, but this
208-
// is primarily for use as external entrypoints and shouldn't really be counted as
209-
// part of the `millbuild.build#<init>` transitive call graph they would normally
210-
// be counted as
211-
def isCommand =
212-
calledSig.desc.ret.pretty == classOf[mill.define.Command[?]].getName
213-
214-
// Skip calls to `millDiscover`. `millDiscover` is bundled as part of `RootModule` for
215-
// convenience, but it should really never be called by any normal Mill module/task code,
216-
// and is only used by downstream code in `mill.eval`/`mill.resolve`. Thus although CodeSig's
217-
// conservative analysis considers potential calls from `build_.package_$#<init>` to
218-
// `millDiscover()`, we can safely ignore that possibility
219-
def isMillDiscover =
220-
calledSig.name == "millDiscover$lzyINIT1" ||
221-
calledSig.name == "millDiscover" ||
222-
callSiteOpt.exists(_.sig.name == "millDiscover")
223-
224-
(isSimpleTarget(calledSig.desc) && !isForwarderCallsiteOrLambda) ||
225-
isCommand ||
226-
isMillDiscover
227-
},
228-
logger = new mill.codesig.Logger(
229-
Task.dest / "current",
230-
Option.when(debugEnabled)(Task.dest / "current")
231-
),
232-
prevTransitiveCallGraphHashesOpt = () =>
233-
Option.when(os.exists(Task.dest / "previous/transitiveCallGraphHashes0.json"))(
234-
upickle.default.read[Map[String, Int]](
235-
os.read.stream(Task.dest / "previous/transitiveCallGraphHashes0.json")
236-
)
237-
)
238-
)
239-
240-
codesig.transitiveCallGraphHashes
160+
def codeSignatures: T[Map[String, Int]] = Task {
161+
val (analysisFolder, _) = callGraphAnalysis()
162+
val transitiveCallGraphHashes0 = upickle.default.read[Map[String, Int]](
163+
os.read.stream(analysisFolder / "transitiveCallGraphHashes0.json")
164+
)
165+
transitiveCallGraphHashes0
241166
}
242167

243168
override def sources: T[Seq[PathRef]] = Task {

scalalib/package.mill

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import mill.define.Cross
1616

1717
object `package` extends RootModule with build.MillStableScalaModule {
1818

19-
def moduleDeps = Seq(build.core.util, build.scalalib.api, build.testrunner)
19+
def moduleDeps = Seq(build.core.util, build.scalalib.api, build.testrunner, build.core.codesig)
2020
def mvnDeps = {
2121
Agg(build.Deps.scalaXml) ++ {
2222
// despite compiling with Scala 3, we need to include scala-reflect

scalalib/src/mill/scalalib/JavaModule.scala

Lines changed: 170 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ import mill.scalalib.publish.Artifact
4343
import mill.util.JarManifest
4444
import mill.util.Jvm
4545
import os.Path
46-
46+
import scala.util.Try
4747
/**
4848
* Core configuration required to compile a single Java compilation target
4949
*/
@@ -63,6 +63,7 @@ trait JavaModule
6363

6464
override def jvmWorker: ModuleRef[JvmWorkerModule] = super.jvmWorker
6565
trait JavaTests extends JavaModule with TestModule {
66+
import mill.testrunner.TestResult
6667
// Run some consistence checks
6768
hierarchyChecks()
6869

@@ -117,6 +118,90 @@ trait JavaModule
117118
case _: ClassNotFoundException => // if we can't find the classes, we certainly are not in a ScalaJSModule
118119
}
119120
}
121+
122+
private def quickTest(args: Seq[String]): Task[(String, Seq[TestResult])] =
123+
Task(persistent = true) {
124+
val quicktestFailedClassesLog = Task.dest / "quickTestFailedClasses.log"
125+
val (analysisFolder, previousAnalysisFolderOpt) = callGraphAnalysis()
126+
val globFilter: String => Boolean = {
127+
previousAnalysisFolderOpt.fold {
128+
// no previous analysis folder, we accept all classes
129+
TestRunnerUtils.globFilter(Seq.empty)
130+
} { _ =>
131+
132+
val failedTestClasses =
133+
if (!os.exists(quicktestFailedClassesLog)) {
134+
Seq.empty[String]
135+
} else {
136+
Try {
137+
upickle.default.read[Seq[String]](os.read.stream(quicktestFailedClassesLog))
138+
}.getOrElse(Seq.empty[String])
139+
}
140+
141+
val quiteTestingClasses =
142+
Try {
143+
upickle.default.read[Seq[String]](os.read.stream(analysisFolder / "invalidClassNames.json"))
144+
}.getOrElse(Seq.empty[String])
145+
146+
TestRunnerUtils.globFilter((failedTestClasses ++ quiteTestingClasses).distinct)
147+
}
148+
}
149+
150+
// Clean yp the directory
151+
os.walk(Task.dest).foreach { subPath => os.remove.all(subPath) }
152+
153+
val quickTestClassLists = testForkGrouping().map(_.filter(globFilter)).filter(_.nonEmpty)
154+
155+
val quickTestReportXml = testReportXml()
156+
157+
val testModuleUtil = new TestModuleUtil(
158+
testUseArgsFile(),
159+
forkArgs(),
160+
Seq.empty,
161+
zincWorker().scalalibClasspath(),
162+
resources(),
163+
testFramework(),
164+
runClasspath(),
165+
testClasspath(),
166+
args.toSeq,
167+
quickTestClassLists,
168+
zincWorker().testrunnerEntrypointClasspath(),
169+
forkEnv(),
170+
testSandboxWorkingDir(),
171+
forkWorkingDir(),
172+
quickTestReportXml,
173+
zincWorker().javaHome().map(_.path),
174+
testParallelism()
175+
)
176+
177+
val results = testModuleUtil.runTests()
178+
179+
val badTestClasses = results match {
180+
case Result.Failure(_) =>
181+
// Consider all quick testing classes as failed
182+
quickTestClassLists.flatten
183+
case Result.Success((_, results)) =>
184+
// Get all test classes that failed
185+
results
186+
.filter(testResult => Set("Error", "Failure").contains(testResult.status))
187+
.map(_.fullyQualifiedName)
188+
}
189+
190+
os.write.over(quicktestFailedClassesLog, upickle.default.write[Seq[String]](badTestClasses.distinct))
191+
192+
results match {
193+
case Result.Failure(errMsg) => Result.Failure(errMsg)
194+
case Result.Success((doneMsg, results)) =>
195+
try TestModule.handleResults(doneMsg, results, Task.ctx(), quickTestReportXml)
196+
catch {
197+
case e: Throwable => Result.Failure("Test reporting failed: " + e)
198+
}
199+
}
200+
}
201+
202+
def testQuick(args: String*): Command[(String, Seq[TestResult])] = Task.Command {
203+
quickTest(args.toSeq)()
204+
}
120205
}
121206

122207
def defaultCommandName(): String = "run"
@@ -1527,6 +1612,90 @@ trait JavaModule
15271612
}
15281613

15291614
}
1615+
1616+
// Return the directory containing the current call graph analysis results, and previous one too if it exists
1617+
def callGraphAnalysis: T[(Path, Option[Path])] = Task(persistent = true) {
1618+
os.remove.all(Task.dest / "previous")
1619+
if (os.exists(Task.dest / "current"))
1620+
os.move.over(Task.dest / "current", Task.dest / "previous")
1621+
val debugEnabled = Task.log.debugEnabled
1622+
mill.codesig.CodeSig
1623+
.compute(
1624+
classFiles = os.walk(compile().classes.path).filter(_.ext == "class"),
1625+
upstreamClasspath = compileClasspath().toSeq.map(_.path),
1626+
ignoreCall = { (callSiteOpt, calledSig) =>
1627+
// We can ignore all calls to methods that look like Targets when traversing
1628+
// the call graph. We can do this because we assume `def` Targets are pure,
1629+
// and so any changes in their behavior will be picked up by the runtime build
1630+
// graph evaluator without needing to be accounted for in the post-compile
1631+
// bytecode callgraph analysis.
1632+
def isSimpleTarget(desc: mill.codesig.JvmModel.Desc) =
1633+
(desc.ret.pretty == classOf[mill.define.Target[?]].getName ||
1634+
desc.ret.pretty == classOf[mill.define.Worker[?]].getName) &&
1635+
desc.args.isEmpty
1636+
1637+
// We avoid ignoring method calls that are simple trait forwarders, because
1638+
// we need the trait forwarders calls to be counted in order to wire up the
1639+
// method definition that a Target is associated with during evaluation
1640+
// (e.g. `myModuleObject.myTarget`) with its implementation that may be defined
1641+
// somewhere else (e.g. `trait MyModuleTrait{ def myTarget }`). Only that one
1642+
// step is necessary, after that the runtime build graph invalidation logic can
1643+
// take over
1644+
def isForwarderCallsiteOrLambda =
1645+
callSiteOpt.nonEmpty && {
1646+
val callSiteSig = callSiteOpt.get.sig
1647+
1648+
(callSiteSig.name == (calledSig.name + "$") &&
1649+
callSiteSig.static &&
1650+
callSiteSig.desc.args.size == 1)
1651+
|| (
1652+
// In Scala 3, lambdas are implemented by private instance methods,
1653+
// not static methods, so they fall through the crack of "isSimpleTarget".
1654+
// Here make the assumption that a zero-arg lambda called from a simpleTarget,
1655+
// should in fact be tracked. e.g. see `integration.invalidation[codesig-hello]`,
1656+
// where the body of the `def foo` target is a zero-arg lambda i.e. the argument
1657+
// of `Cacher.cachedTarget`.
1658+
// To be more precise I think ideally we should capture more information in the signature
1659+
isSimpleTarget(callSiteSig.desc) && calledSig.name.contains("$anonfun")
1660+
)
1661+
}
1662+
1663+
// We ignore Commands for the same reason as we ignore Targets, and also because
1664+
// their implementations get gathered up all the via the `Discover` macro, but this
1665+
// is primarily for use as external entrypoints and shouldn't really be counted as
1666+
// part of the `millbuild.build#<init>` transitive call graph they would normally
1667+
// be counted as
1668+
def isCommand =
1669+
calledSig.desc.ret.pretty == classOf[mill.define.Command[?]].getName
1670+
1671+
// Skip calls to `millDiscover`. `millDiscover` is bundled as part of `RootModule` for
1672+
// convenience, but it should really never be called by any normal Mill module/task code,
1673+
// and is only used by downstream code in `mill.eval`/`mill.resolve`. Thus although CodeSig's
1674+
// conservative analysis considers potential calls from `build_.package_$#<init>` to
1675+
// `millDiscover()`, we can safely ignore that possibility
1676+
def isMillDiscover =
1677+
calledSig.name == "millDiscover$lzyINIT1" ||
1678+
calledSig.name == "millDiscover" ||
1679+
callSiteOpt.exists(_.sig.name == "millDiscover")
1680+
1681+
(isSimpleTarget(calledSig.desc) && !isForwarderCallsiteOrLambda) ||
1682+
isCommand ||
1683+
isMillDiscover
1684+
},
1685+
logger = new mill.codesig.Logger(
1686+
Task.dest / "current",
1687+
Option.when(debugEnabled)(Task.dest / "current")
1688+
),
1689+
prevTransitiveCallGraphHashesOpt = () =>
1690+
Option.when(os.exists(Task.dest / "previous/transitiveCallGraphHashes0.json"))(
1691+
upickle.default.read[Map[String, Int]](
1692+
os.read.stream(Task.dest / "previous/transitiveCallGraphHashes0.json")
1693+
)
1694+
)
1695+
)
1696+
1697+
(Task.dest / "current", Option.when(os.exists(Task.dest / "previous"))(Task.dest / "previous"))
1698+
}
15301699
}
15311700

15321701
object JavaModule {

0 commit comments

Comments
 (0)