Skip to content

Commit 455fadb

Browse files
Consider targets used for test discovery in dependency checking
Before, we did test discovery at runtime, but we now do it at compile-time, for speed and to avoid having to serialize the analysis information returned by the compiler bridge. This commits marks targets used during test discovery as used, thereby subjecting them to depednency checking. In most cases, this won't make a difference as the target containing the implementation of the SBT test interface is the same one that the target being built would've depended on anyway. One important thing to note is that this will break a property of our dependency checker that's been thus-far guaranteed: that it only allows a dependency to be in `deps` or not in `deps`. When we do test discovery, we iterate through a list of test "frameworks". If we find a test framework in the classpath, we look for tests that use that framework; otherwise, we don't. This means that, for example, if specs2 isn't on the classpath, but the target contains specs2 tests, the tests will silently fail to execute. This means that now, having that a dependency in `deps` or not in `deps` are sometimes both valid states to be in. Prior to this change, that wasn't the case; there was a single, correct list of dependencies.
1 parent 4ff6c34 commit 455fadb

File tree

3 files changed

+66
-28
lines changed

3 files changed

+66
-28
lines changed

src/main/scala/higherkindness/rules_scala/common/sbt-testing/Test.scala

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package higherkindness.rules_scala.common.sbt_testing
22

3+
import java.nio.file.{Path, Paths}
34
import play.api.libs.json.{Format, Json}
45
import sbt.testing.{Event, Framework, Logger, Runner, Status, Task, TaskDef, TestWildcardSelector}
56
import scala.collection.mutable
@@ -12,17 +13,39 @@ object TestDefinition {
1213
}
1314

1415
class TestFrameworkLoader(loader: ClassLoader) {
16+
private val loadedJarsByFramework = mutable.Map.empty[String, mutable.ArrayBuffer[Path]]
17+
private def getClassJar(`class`: Class[?]): Option[Path] = for {
18+
codeSource <- Option(`class`.getProtectionDomain().getCodeSource())
19+
codeSourceUri = codeSource.getLocation().toURI()
20+
path <-
21+
try {
22+
Some(Paths.get(codeSourceUri))
23+
} catch {
24+
case _: IllegalArgumentException => None
25+
}
26+
} yield path
27+
28+
def getLoadedJarsByFramework(): Map[String, Array[Path]] =
29+
loadedJarsByFramework.view.map { case (frameworkName, loadedJars) => frameworkName -> loadedJars.toArray }.toMap
30+
1531
def load(className: String) = {
16-
val framework =
32+
val (framework, loadedJar) =
1733
try {
18-
Some(Class.forName(className, true, loader).getDeclaredConstructor().newInstance())
34+
val `class` = Class.forName(className, true, loader)
35+
val loadedJar = getClassJar(`class`)
36+
37+
(Some(`class`.getDeclaredConstructor().newInstance()), loadedJar)
1938
} catch {
20-
case _: ClassNotFoundException => None
39+
case _: ClassNotFoundException => (None, None)
2140
case NonFatal(e) => throw new Exception(s"Failed to load framework $className", e)
2241
}
2342
framework.map {
24-
case framework: Framework => framework
25-
case _ => throw new Exception(s"$className does not implement ${classOf[Framework].getName}")
43+
case framework: Framework =>
44+
loadedJar.foreach(loadedJarsByFramework.getOrElseUpdate(className, mutable.ArrayBuffer.empty) += _)
45+
46+
framework
47+
48+
case _ => throw new Exception(s"$className does not implement ${classOf[Framework].getName}")
2649
}
2750

2851
}

src/main/scala/higherkindness/rules_scala/workers/zinc/compile/ZincRunner.scala

Lines changed: 37 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -211,19 +211,33 @@ object ZincRunner extends WorkerMain[Unit] {
211211
}
212212
}
213213

214-
private def maybeWriteTestsFile(parsedArguments: CommonArguments, analysis: AnnexAnalysis): Unit =
215-
parsedArguments.testsFile.foreach { path =>
216-
val classloader = ClassLoaders.sbtTestClassLoader(parsedArguments.classpath.map(_.toUri.toURL))
217-
val frameworkLoader = new TestFrameworkLoader(classloader)
218-
val testsFileData = TestsFileData(
219-
parsedArguments.testFrameworks
220-
.flatMap(frameworkName => frameworkLoader.load(frameworkName).map((frameworkName, _)))
221-
.map { case (frameworkName, framework) => frameworkName -> new TestDiscovery(framework)(analysis.apis.toSet) }
222-
.toMap,
223-
)
214+
private def maybeWriteTestsFile(parsedArguments: CommonArguments, analysis: AnnexAnalysis): Array[Path] =
215+
parsedArguments.testsFile
216+
.map { path =>
217+
val classloader = ClassLoaders.sbtTestClassLoader(parsedArguments.classpath.map(_.toUri.toURL))
218+
val frameworkLoader = new TestFrameworkLoader(classloader)
219+
val testsFileData = TestsFileData(
220+
parsedArguments.testFrameworks
221+
.flatMap(frameworkName => frameworkLoader.load(frameworkName).map((frameworkName, _)))
222+
.map { case (frameworkName, framework) =>
223+
frameworkName -> new TestDiscovery(framework)(analysis.apis.toSet)
224+
}
225+
.toMap,
226+
)
224227

225-
Files.write(path, Json.stringify(Json.toJson(testsFileData)).getBytes(StandardCharsets.UTF_8))
226-
}
228+
val loadedJarsByFramework = frameworkLoader.getLoadedJarsByFramework()
229+
val loadedJars = testsFileData.testsByFramework.view
230+
.filter { case (_, tests) => tests.nonEmpty }
231+
.keys
232+
.flatMap(loadedJarsByFramework.get)
233+
.flatten
234+
.toArray
235+
236+
Files.write(path, Json.stringify(Json.toJson(testsFileData)).getBytes(StandardCharsets.UTF_8))
237+
238+
loadedJars
239+
}
240+
.getOrElse(Array.empty)
227241

228242
protected def init(args: Option[Array[String]]): Unit = ()
229243

@@ -298,7 +312,16 @@ object ZincRunner extends WorkerMain[Unit] {
298312

299313
InterruptUtil.throwIfInterrupted(task.isCancelled)
300314

301-
maybeWriteTestsFile(workRequest, analysis)
315+
/*
316+
* JARs on the classpath used during test discovery, but not used by compiler won't show up in `analysis.usedJars`, so
317+
* we need to consider them in addition to those used by the compiler.
318+
*
319+
* Test discovery works by iterating through the list of test frameworks provided via `workRequest.testFrameworks`,
320+
* attempting to classload each framework, and then using that framework to discover tests. If a given framework
321+
* successfully loads and we found a test with it we take note of the JAR it was loaded from and consider that JAR
322+
* (and therefore the target to which it belongs) used.
323+
*/
324+
val usedDepsDuringTestDiscovery = maybeWriteTestsFile(workRequest, analysis)
302325

303326
InterruptUtil.throwIfInterrupted(task.isCancelled)
304327

@@ -310,7 +333,7 @@ object ZincRunner extends WorkerMain[Unit] {
310333
val usedDeps = workRequest.classpath.view
311334
.map(_.normalize().toAbsolutePath)
312335
.toSet
313-
.intersect(analysis.usedJars.toSet)
336+
.intersect((analysis.usedJars.view ++ usedDepsDuringTestDiscovery.view).toSet)
314337
// Filter out the Scala standard library as they should always be implicitly available we shouldn't be
315338
// bookkeeping them
316339
.view

tests/test-frameworks/mixed/BUILD

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,12 @@ load("@rules_scala_annex//rules:scala.bzl", "scala_test")
33
scala_test(
44
name = "mixed_2_13",
55
srcs = glob(["*.scala"]),
6-
deps_used_whitelist = [
7-
"@annex_test//:org_hamcrest_hamcrest_core",
8-
"@annex_test//:com_novocode_junit_interface",
9-
],
106
scala_toolchain_name = "test_zinc_2_13",
117
shard_count = 2,
128
tags = ["manual"],
139
deps = [
1410
"@annex_test//:com_novocode_junit_interface",
1511
"@annex_test//:junit_junit",
16-
"@annex_test//:org_hamcrest_hamcrest_core",
1712
"@annex_test//:org_scalacheck_scalacheck_2_13",
1813
"@annex_test//:org_scalactic_scalactic_2_13",
1914
"@annex_test//:org_scalatest_scalatest_compatible",
@@ -31,11 +26,8 @@ scala_test(
3126
scala_toolchain_name = "test_zinc_2_12",
3227
shard_count = 2,
3328
tags = ["manual"],
34-
runtime_deps = [
35-
"@annex_test//:com_novocode_junit_interface",
36-
"@annex_test//:org_hamcrest_hamcrest_core",
37-
],
3829
deps = [
30+
"@annex_test//:com_novocode_junit_interface",
3931
"@annex_test//:junit_junit",
4032
"@annex_test_2_12//:org_scalacheck_scalacheck_2_12",
4133
"@annex_test_2_12//:org_scalactic_scalactic_2_12",

0 commit comments

Comments
 (0)