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

View File

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

View File

@ -37,7 +37,7 @@ import sbt.librarymanagement._
import sbt.librarymanagement.ivy.{ Credentials, IvyConfiguration, IvyPaths, UpdateOptions } import sbt.librarymanagement.ivy.{ Credentials, IvyConfiguration, IvyPaths, UpdateOptions }
import sbt.nio.file.Glob import sbt.nio.file.Glob
import sbt.testing.Framework 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.{ HashedVirtualFileRef, VirtualFile, VirtualFileRef }
import xsbti.compile._ import xsbti.compile._
import xsbti.compile.analysis.ReadStamps import xsbti.compile.analysis.ReadStamps
@ -349,8 +349,10 @@ object Keys {
// Test Keys // Test Keys
val testLoader = taskKey[ClassLoader]("Provides the class loader used for testing.").withRank(DTask) 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) 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 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 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 executeTests = taskKey[Tests.Output]("Executes all tests, producing a report.").withRank(CTask)
val test = taskKey[Unit]("Executes all tests.").withRank(APlusTask) 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) 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.io.File
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import Keys.{ test, compileInputs, fileConverter, fullClasspath, streams } import Keys.{ test, fileConverter, fullClasspath, streams }
import sbt.Def.Initialize import sbt.Def.Initialize
import sbt.internal.inc.Analysis import sbt.internal.inc.Analysis
import sbt.internal.util.Attributed import sbt.internal.util.Attributed
@ -32,17 +32,30 @@ object IncrementalTest:
Def.task { Def.task {
val cp = (Keys.test / fullClasspath).value val cp = (Keys.test / fullClasspath).value
val s = (Keys.test / streams).value val s = (Keys.test / streams).value
val converter = fileConverter.value val digests = (Keys.definedTestDigests).value
val stamper = ClassStamper(cp, converter)
val succeeded = TestStatus.read(succeededFile(s.cacheDirectory)) val succeeded = TestStatus.read(succeededFile(s.cacheDirectory))
def hasSucceeded(className: String): Boolean = succeeded.get(className) match def hasSucceeded(className: String): Boolean = succeeded.get(className) match
case None => false case None => false
case Some(ts) => ts == stamper.transitiveStamp(className) case Some(ts) => Some(ts) == digests.get(className)
args => args =>
for filter <- selectedFilter(args) for filter <- selectedFilter(args)
yield (test: String) => filter(test) && !hasSucceeded(test) 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 succeededFile(dir: File): File = dir / "succeeded_tests.txt"
def selectedFilter(args: Seq[String]): Seq[String => Boolean] = 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. * 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)): _*) 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( private def internalStamp(
className: String, className: String,
@ -144,7 +158,7 @@ class ClassStamper(
internalStamp(otherCN, analysis, alreadySeen + className) internalStamp(otherCN, analysis, alreadySeen + className)
val internalJarDeps = relations val internalJarDeps = relations
.externalDeps(className) .externalDeps(className)
.map: libClassName => .flatMap: libClassName =>
transitiveStamp(libClassName) transitiveStamp(libClassName)
val externalDeps = relations val externalDeps = relations
.externalDeps(className) .externalDeps(className)

View File

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