@@ -18,6 +18,10 @@ import mill.scalalib.bsp.{BspBuildTarget, BspModule, BspUri, JvmBuildTarget}
18
18
import mill .scalalib .publish .Artifact
19
19
import mill .util .Jvm
20
20
import os .Path
21
+ import mill .testrunner .TestResult
22
+ import mill .scalalib .api .TransitiveSourceStampResults
23
+ import scala .collection .immutable .TreeMap
24
+ import scala .util .Try
21
25
22
26
/**
23
27
* Core configuration required to compile a single Java compilation target
@@ -92,6 +96,173 @@ trait JavaModule
92
96
case _ : ClassNotFoundException => // if we can't find the classes, we certainly are not in a ScalaJSModule
93
97
}
94
98
}
99
+
100
+ def testQuick (args : String * ): Command [(String , Seq [TestResult ])] = Task .Command (persistent = true ) {
101
+ val quicktestFailedClassesLog = Task .dest / " quickTestFailedClasses.json"
102
+ val invalidatedClassesLog = Task .dest / " invalidatedClasses.json"
103
+ val failedTestClasses =
104
+ if (! os.exists(quicktestFailedClassesLog)) {
105
+ Set .empty[String ]
106
+ } else {
107
+ Try {
108
+ upickle.default.read[Seq [String ]](os.read.stream(quicktestFailedClassesLog))
109
+ }.getOrElse(Seq .empty[String ]).toSet
110
+ }
111
+
112
+ val transitiveStampsFile = Task .dest / " transitiveStamps.json"
113
+ val previousStampsOpt = if (os.exists(transitiveStampsFile)) {
114
+ val previousStamps = upickle.default.read[TransitiveSourceStampResults ](
115
+ os.read.stream(transitiveStampsFile)
116
+ ).currentStamps
117
+ os.remove(transitiveStampsFile)
118
+ Some (previousStamps)
119
+ } else {
120
+ None
121
+ }
122
+
123
+ def getAnalysisStore (compileResult : CompilationResult ): Option [xsbti.compile.CompileAnalysis ] = {
124
+ val analysisStore = sbt.internal.inc.consistent.ConsistentFileAnalysisStore .binary(
125
+ file = compileResult.analysisFile.toIO,
126
+ mappers = xsbti.compile.analysis.ReadWriteMappers .getEmptyMappers(),
127
+ reproducible = true ,
128
+ parallelism = math.min(Runtime .getRuntime.availableProcessors(), 8 )
129
+ )
130
+ val analysisOptional = analysisStore.get()
131
+ if (analysisOptional.isPresent) Some (analysisOptional.get.getAnalysis) else None
132
+ }
133
+
134
+ val combinedAnalysis = (compile() +: upstreamCompileOutput())
135
+ .flatMap(getAnalysisStore)
136
+ .flatMap {
137
+ case analysis : sbt.internal.inc.Analysis => Some (analysis)
138
+ case _ => None
139
+ }
140
+ .foldLeft(sbt.internal.inc.Analysis .empty)(_ ++ _)
141
+
142
+ val result = TransitiveSourceStampResults (
143
+ currentStamps = TreeMap .from(
144
+ combinedAnalysis.stamps.sources.view.map { (source, stamp) =>
145
+ source.id() -> stamp.writeStamp()
146
+ }
147
+ ),
148
+ previousStamps = previousStampsOpt
149
+ )
150
+
151
+ def getInvalidatedClasspaths (
152
+ initialInvalidatedClassNames : Set [String ],
153
+ relations : sbt.internal.inc.Relations
154
+ ): Set [os.Path ] = {
155
+ val seen = collection.mutable.Set .empty[String ]
156
+ val seenList = collection.mutable.Buffer .empty[String ]
157
+ val queued = collection.mutable.Queue .from(initialInvalidatedClassNames)
158
+
159
+ while (queued.nonEmpty) {
160
+ val current = queued.dequeue()
161
+ seenList.append(current)
162
+ seen.add(current)
163
+
164
+ for (next <- relations.usesInternalClass(current)) {
165
+ if (! seen.contains(next)) {
166
+ seen.add(next)
167
+ queued.enqueue(next)
168
+ }
169
+ }
170
+
171
+ for (next <- relations.usesExternal(current)) {
172
+ if (! seen.contains(next)) {
173
+ seen.add(next)
174
+ queued.enqueue(next)
175
+ }
176
+ }
177
+ }
178
+
179
+ seenList
180
+ .iterator
181
+ .flatMap { invalidatedClassName =>
182
+ relations.definesClass(invalidatedClassName)
183
+ }
184
+ .flatMap { source =>
185
+ relations.products(source)
186
+ }
187
+ .map { product =>
188
+ os.Path (product.id)
189
+ }
190
+ .toSet
191
+ }
192
+
193
+ val relations = combinedAnalysis.relations
194
+
195
+ val invalidatedAbsoluteClasspaths = getInvalidatedClasspaths(
196
+ result.changedSources.flatMap { source =>
197
+ relations.classNames(xsbti.VirtualFileRef .of(source))
198
+ },
199
+ combinedAnalysis.relations
200
+ )
201
+
202
+ // We only care about testing class, so we can:
203
+ // - filter out all class path that start with `testClasspath()`
204
+ // - strip the prefix and safely turn them into module class path
205
+
206
+ val testClasspaths = testClasspath()
207
+ val invalidatedClassNames = invalidatedAbsoluteClasspaths.flatMap { absoluteClasspath =>
208
+ testClasspaths.collectFirst {
209
+ case path if absoluteClasspath.startsWith(path.path) =>
210
+ absoluteClasspath.relativeTo(path.path).segments.map(_.stripSuffix(" .class" )).mkString(" ." )
211
+ }
212
+ }
213
+ val testingClasses = invalidatedClassNames ++ failedTestClasses
214
+ val testClasses = testForkGrouping().map(_.filter(testingClasses.contains)).filter(_.nonEmpty)
215
+
216
+ // Clean up the directory for test runners
217
+ os.walk(Task .dest).foreach { subPath => os.remove.all(subPath) }
218
+
219
+ val quickTestReportXml = testReportXml()
220
+
221
+ val testModuleUtil = new TestModuleUtil (
222
+ testUseArgsFile(),
223
+ forkArgs(),
224
+ Seq .empty,
225
+ zincWorker().scalalibClasspath(),
226
+ resources(),
227
+ testFramework(),
228
+ runClasspath(),
229
+ testClasspaths,
230
+ args.toSeq,
231
+ testClasses,
232
+ zincWorker().testrunnerEntrypointClasspath(),
233
+ forkEnv(),
234
+ testSandboxWorkingDir(),
235
+ forkWorkingDir(),
236
+ quickTestReportXml,
237
+ zincWorker().javaHome().map(_.path),
238
+ testParallelism()
239
+ )
240
+
241
+ val results = testModuleUtil.runTests()
242
+
243
+ val badTestClasses = (results match {
244
+ case Result .Failure (_) =>
245
+ // Consider all quick testing classes as failed
246
+ testClasses.flatten
247
+ case Result .Success ((_, results)) =>
248
+ // Get all test classes that failed
249
+ results
250
+ .filter(testResult => Set (" Error" , " Failure" ).contains(testResult.status))
251
+ .map(_.fullyQualifiedName)
252
+ }).distinct
253
+
254
+ os.write.over(transitiveStampsFile, upickle.default.write(result))
255
+ os.write.over(quicktestFailedClassesLog, upickle.default.write(badTestClasses))
256
+ os.write.over(invalidatedClassesLog, upickle.default.write(invalidatedClassNames))
257
+ results match {
258
+ case Result .Failure (errMsg) => Result .Failure (errMsg)
259
+ case Result .Success ((doneMsg, results)) =>
260
+ try TestModule .handleResults(doneMsg, results, Task .ctx(), quickTestReportXml)
261
+ catch {
262
+ case e : Throwable => Result .Failure (" Test reporting failed: " + e)
263
+ }
264
+ }
265
+ }
95
266
}
96
267
97
268
def defaultCommandName (): String = " run"
0 commit comments