@@ -43,7 +43,7 @@ import mill.scalalib.publish.Artifact
4343import mill .util .JarManifest
4444import mill .util .Jvm
4545import 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
15321701object JavaModule {
0 commit comments