Refactor to definedTestDigests task

This commit is contained in:
Eugene Yokota 2024-09-08 02:24:47 -04:00
parent 6952d3c46d
commit 0021c3a0bd
5 changed files with 33 additions and 13 deletions

View File

@ -307,6 +307,7 @@ object Tests {
in.filter(t => seen.add(f(t)))
}
// Called by Defaults
def apply(
frameworks: Map[TestFramework, Framework],
testLoader: ClassLoader,
@ -340,7 +341,7 @@ object Tests {
apply(frameworks, testLoader, runners, o, config, log)
}
def testTask(
private[sbt] def testTask(
loader: ClassLoader,
frameworks: Map[TestFramework, Framework],
runners: Map[TestFramework, Runner],

View File

@ -1329,6 +1329,9 @@ object Defaults extends BuildCommon {
.storeAs(definedTestNames)
.triggeredBy(compile)
.value,
definedTestDigests := IncrementalTest.definedTestDigestTask
.triggeredBy(compile)
.value,
testQuick / testFilter := IncrementalTest.filterTask.value,
executeTests := {
import sbt.TupleSyntax.*
@ -1411,8 +1414,7 @@ object Defaults extends BuildCommon {
) +:
TestStatusReporter(
IncrementalTest.succeededFile((test / streams).value.cacheDirectory),
(Keys.test / fullClasspath).value,
fileConverter.value,
definedTestDigests.value,
) +:
(TaskZero / testListeners).value
},

View File

@ -37,7 +37,7 @@ import sbt.librarymanagement._
import sbt.librarymanagement.ivy.{ Credentials, IvyConfiguration, IvyPaths, UpdateOptions }
import sbt.nio.file.Glob
import sbt.testing.Framework
import sbt.util.{ cacheLevel, ActionCacheStore, Level, Logger, LoggerContext }
import sbt.util.{ cacheLevel, ActionCacheStore, Digest, Level, Logger, LoggerContext }
import xsbti.{ HashedVirtualFileRef, VirtualFile, VirtualFileRef }
import xsbti.compile._
import xsbti.compile.analysis.ReadStamps
@ -349,8 +349,10 @@ object Keys {
// Test Keys
val testLoader = taskKey[ClassLoader]("Provides the class loader used for testing.").withRank(DTask)
val loadedTestFrameworks = taskKey[Map[TestFramework, Framework]]("Loads Framework definitions from the test loader.").withRank(DTask)
@cacheLevel(include = Array.empty)
val definedTests = taskKey[Seq[TestDefinition]]("Provides the list of defined tests.").withRank(BMinusTask)
val definedTestNames = taskKey[Seq[String]]("Provides the set of defined test names.").withRank(BMinusTask)
val definedTestDigests = taskKey[Map[String, Digest]]("Provides a unique digest of defined tests.").withRank(DTask)
val executeTests = taskKey[Tests.Output]("Executes all tests, producing a report.").withRank(CTask)
val test = taskKey[Unit]("Executes all tests.").withRank(APlusTask)
val testOnly = inputKey[Unit]("Executes the tests provided as arguments or all tests if no arguments are provided.").withRank(ATask)

View File

@ -11,7 +11,7 @@ package internal
import java.io.File
import java.util.concurrent.ConcurrentHashMap
import Keys.{ test, compileInputs, fileConverter, fullClasspath, streams }
import Keys.{ test, fileConverter, fullClasspath, streams }
import sbt.Def.Initialize
import sbt.internal.inc.Analysis
import sbt.internal.util.Attributed
@ -32,17 +32,30 @@ object IncrementalTest:
Def.task {
val cp = (Keys.test / fullClasspath).value
val s = (Keys.test / streams).value
val converter = fileConverter.value
val stamper = ClassStamper(cp, converter)
val digests = (Keys.definedTestDigests).value
val succeeded = TestStatus.read(succeededFile(s.cacheDirectory))
def hasSucceeded(className: String): Boolean = succeeded.get(className) match
case None => false
case Some(ts) => ts == stamper.transitiveStamp(className)
case Some(ts) => Some(ts) == digests.get(className)
args =>
for filter <- selectedFilter(args)
yield (test: String) => filter(test) && !hasSucceeded(test)
}
// cache the test digests against the fullClasspath.
def definedTestDigestTask: Initialize[Task[Map[String, Digest]]] = Def.cachedTask {
val cp = (Keys.test / fullClasspath).value
val testNames = Keys.definedTests.value.map(_.name).toVector.distinct
val converter = fileConverter.value
val stamper = ClassStamper(cp, converter)
// TODO: Potentially do something about JUnit 5 and others which might not use class name
Map((testNames.flatMap: name =>
stamper.transitiveStamp(name) match
case Some(ts) => Seq(name -> ts)
case None => Nil
): _*)
}
def succeededFile(dir: File): File = dir / "succeeded_tests.txt"
def selectedFilter(args: Seq[String]): Seq[String => Boolean] =
@ -123,9 +136,10 @@ class ClassStamper(
/**
* Given a classpath and a class name, this tries to create a SHA-256 digest.
*/
def transitiveStamp(className: String): Digest =
def transitiveStamp(className: String): Option[Digest] =
val digests = SortedSet(analyses.flatMap(internalStamp(className, _, Set.empty)): _*)
Digest.sha256Hash(digests.toSeq: _*)
if digests.nonEmpty then Some(Digest.sha256Hash(digests.toSeq: _*))
else None
private def internalStamp(
className: String,
@ -144,7 +158,7 @@ class ClassStamper(
internalStamp(otherCN, analysis, alreadySeen + className)
val internalJarDeps = relations
.externalDeps(className)
.map: libClassName =>
.flatMap: libClassName =>
transitiveStamp(libClassName)
val externalDeps = relations
.externalDeps(className)

View File

@ -93,6 +93,7 @@ final class TestFramework(val implClassNames: String*) extends Serializable {
def create(loader: ClassLoader, log: ManagedLogger): Option[Framework] =
createFramework(loader, log, implClassNames.toList)
}
final class TestDefinition(
val name: String,
val fingerprint: Fingerprint,
@ -108,7 +109,7 @@ final class TestDefinition(
override def hashCode: Int = (name.hashCode, TestFramework.hashCode(fingerprint)).hashCode
}
final class TestRunner(
private[sbt] final class TestRunner(
delegate: Runner,
listeners: Vector[TestReportListener],
log: ManagedLogger
@ -214,7 +215,7 @@ object TestFramework {
case _ => f.toString
}
def testTasks(
private[sbt] def testTasks(
frameworks: Map[TestFramework, Framework],
runners: Map[TestFramework, Runner],
testLoader: ClassLoader,