mirror of https://github.com/sbt/sbt.git
[2.x] fix: Invalidate testQuick on test argument changes (#7680)
**Problem** testQuick currently does not invalidate on argument changes. **Solution** This includes test argument digests. --------- Co-authored-by: adpi2 <adrien.piquerez@gmail.com>
This commit is contained in:
parent
cb9a455915
commit
97823b18b0
|
|
@ -32,8 +32,9 @@ import testing.{
|
|||
|
||||
import scala.annotation.tailrec
|
||||
import scala.util.control.NonFatal
|
||||
import scala.quoted.*
|
||||
import sbt.internal.util.ManagedLogger
|
||||
import sbt.util.Logger
|
||||
import sbt.util.{ Digest, Logger }
|
||||
import sbt.protocol.testing.TestResult
|
||||
|
||||
import scala.runtime.AbstractFunction3
|
||||
|
|
@ -68,26 +69,58 @@ object Tests {
|
|||
* The ClassLoader provided to `setup` is the loader containing the test classes that will be run.
|
||||
* Setup is not currently performed for forked tests.
|
||||
*/
|
||||
final case class Setup(setup: ClassLoader => Unit) extends TestOption
|
||||
final case class Setup(setup: ClassLoader => Unit, codeDigest: Digest) extends TestOption
|
||||
|
||||
/**
|
||||
* Defines a TestOption that will evaluate `setup` before any tests execute.
|
||||
* Setup is not currently performed for forked tests.
|
||||
*/
|
||||
def Setup(setup: () => Unit) = new Setup(_ => setup())
|
||||
inline def Setup(inline setup: () => Unit): Setup = ${ unitSetupMacro('setup) }
|
||||
|
||||
private def unitSetupMacro(fn: Expr[() => Unit])(using Quotes): Expr[Setup] =
|
||||
val codeDigest = Digest.sha256Hash(fn.show.getBytes("UTF-8"))
|
||||
val codeDigestStr = Expr(codeDigest.toString())
|
||||
'{
|
||||
new Setup(_ => $fn(), Digest($codeDigestStr))
|
||||
}
|
||||
|
||||
private inline def Setup(inline setup: ClassLoader => Unit): Setup = ${ clSetupMacro('setup) }
|
||||
|
||||
def clSetupMacro(fn: Expr[ClassLoader => Unit])(using Quotes): Expr[Setup] =
|
||||
val codeDigest = Digest.sha256Hash(fn.show.getBytes("UTF-8"))
|
||||
val codeDigestStr = Expr(codeDigest.toString())
|
||||
'{
|
||||
new Setup($fn, Digest($codeDigestStr))
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines a TestOption that will evaluate `cleanup` after all tests execute.
|
||||
* The ClassLoader provided to `cleanup` is the loader containing the test classes that ran.
|
||||
* Cleanup is not currently performed for forked tests.
|
||||
*/
|
||||
final case class Cleanup(cleanup: ClassLoader => Unit) extends TestOption
|
||||
final case class Cleanup(cleanup: ClassLoader => Unit, codeDigest: Digest) extends TestOption
|
||||
|
||||
/**
|
||||
* Defines a TestOption that will evaluate `cleanup` after all tests execute.
|
||||
* Cleanup is not currently performed for forked tests.
|
||||
*/
|
||||
def Cleanup(cleanup: () => Unit) = new Cleanup(_ => cleanup())
|
||||
inline def Cleanup(inline cleanup: () => Unit) = ${ unitCleanupMacro('cleanup) }
|
||||
|
||||
private def unitCleanupMacro(fn: Expr[() => Unit])(using Quotes): Expr[Cleanup] =
|
||||
val codeDigest = Digest.sha256Hash(fn.show.getBytes("UTF-8"))
|
||||
val codeDigestStr = Expr(codeDigest.toString())
|
||||
'{
|
||||
new Cleanup(_ => $fn(), Digest($codeDigestStr))
|
||||
}
|
||||
|
||||
inline def Cleanup(inline cleanup: ClassLoader => Unit): Cleanup = ${ clCleanupMacro('cleanup) }
|
||||
|
||||
private def clCleanupMacro(fn: Expr[ClassLoader => Unit])(using Quotes): Expr[Cleanup] =
|
||||
val codeDigest = Digest.sha256Hash(fn.show.getBytes("UTF-8"))
|
||||
val codeDigestStr = Expr(codeDigest.toString())
|
||||
'{
|
||||
new Cleanup($fn, Digest($codeDigestStr))
|
||||
}
|
||||
|
||||
/** The names of tests to explicitly exclude from execution. */
|
||||
final case class Exclude(tests: Iterable[String]) extends TestOption
|
||||
|
|
@ -271,11 +304,11 @@ object Tests {
|
|||
if (orderedFilters.nonEmpty) sys.error("Cannot define multiple ordered test filters.")
|
||||
else orderedFilters = includes
|
||||
()
|
||||
case Exclude(exclude) => excludeTestsSet ++= exclude; ()
|
||||
case Listeners(listeners) => testListeners ++= listeners; ()
|
||||
case Setup(setupFunction) => setup += setupFunction; ()
|
||||
case Cleanup(cleanupFunction) => cleanup += cleanupFunction; ()
|
||||
case _: Argument => // now handled by whatever constructs `runners`
|
||||
case Exclude(exclude) => excludeTestsSet ++= exclude; ()
|
||||
case Listeners(listeners) => testListeners ++= listeners; ()
|
||||
case Setup(setupFunction, _) => setup += setupFunction; ()
|
||||
case Cleanup(cleanupFunction, _) => cleanup += cleanupFunction; ()
|
||||
case _: Argument => // now handled by whatever constructs `runners`
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1317,6 +1317,7 @@ object Defaults extends BuildCommon {
|
|||
testFrameworks :== sbt.TestFrameworks.All,
|
||||
testListeners :== Nil,
|
||||
testOptions :== Nil,
|
||||
testOptionDigests := Nil,
|
||||
testResultLogger :== TestResultLogger.Default,
|
||||
testOnly / testFilter :== (IncrementalTest.selectedFilter _),
|
||||
extraTestDigests :== Nil,
|
||||
|
|
@ -1429,6 +1430,19 @@ object Defaults extends BuildCommon {
|
|||
(TaskZero / testListeners).value
|
||||
},
|
||||
testOptions := Tests.Listeners(testListeners.value) +: (TaskZero / testOptions).value,
|
||||
testOptionDigests := {
|
||||
(TaskZero / testOptions).value.flatMap {
|
||||
case Tests.Setup(_, digest) => Seq(digest)
|
||||
case Tests.Cleanup(_, digest) => Seq(digest)
|
||||
case Tests.Argument(fm, args) =>
|
||||
Seq(
|
||||
Digest.sha256Hash(
|
||||
(fm.toSeq.map(_.toString) ++ args).mkString("\n").getBytes("UTF-8")
|
||||
)
|
||||
)
|
||||
case _ => Nil
|
||||
}
|
||||
},
|
||||
testExecution := testExecutionTask(key).value
|
||||
)
|
||||
) ++ inScope(GlobalScope)(
|
||||
|
|
|
|||
|
|
@ -358,7 +358,9 @@ object Keys {
|
|||
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 testQuick = inputKey[Unit]("Executes the tests that either failed before, were not run or whose transitive dependencies changed, among those provided as arguments.").withRank(ATask)
|
||||
@cacheLevel(include = Array.empty)
|
||||
val testOptions = taskKey[Seq[TestOption]]("Options for running tests.").withRank(BPlusTask)
|
||||
private[sbt] val testOptionDigests = taskKey[Seq[Digest]]("Digest for testOptions").withRank(DTask)
|
||||
val testFrameworks = settingKey[Seq[TestFramework]]("Registered, although not necessarily present, test frameworks.").withRank(CTask)
|
||||
val testListeners = taskKey[Seq[TestReportListener]]("Defines test listeners.").withRank(DTask)
|
||||
val testForkedParallel = settingKey[Boolean]("Whether forked tests should be executed in parallel").withRank(CTask)
|
||||
|
|
|
|||
|
|
@ -48,13 +48,14 @@ object IncrementalTest:
|
|||
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 opts = (Keys.test / Keys.testOptionDigests).value
|
||||
val converter = fileConverter.value
|
||||
val rds = Keys.resourceDigests.value
|
||||
val extra = Keys.extraTestDigests.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, extra ++ rds) match
|
||||
stamper.transitiveStamp(name, extra ++ rds ++ opts) match
|
||||
case Some(ts) => Seq(name -> ts)
|
||||
case None => Nil
|
||||
): _*)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
val scalatest = "org.scalatest" %% "scalatest" % "3.0.5"
|
||||
|
||||
ThisBuild / scalaVersion := "2.12.19"
|
||||
scalaVersion := "2.12.19"
|
||||
|
||||
val foo = settingKey[Seq[String]]("foo")
|
||||
val checkFoo = inputKey[Unit]("check contents of foo")
|
||||
|
|
|
|||
|
|
@ -1,28 +1,28 @@
|
|||
# should fail because it should run all test:tests, some of which are expected to fail (1 and 4)
|
||||
-> test:test
|
||||
-> testQuick
|
||||
|
||||
$ touch success1
|
||||
> test:test
|
||||
> testQuick
|
||||
$ delete success1
|
||||
|
||||
$ touch failure1
|
||||
-> test:test
|
||||
-> testQuick
|
||||
$ delete failure1
|
||||
|
||||
$ touch success2
|
||||
> test:test
|
||||
> testQuick
|
||||
$ delete success2
|
||||
|
||||
$ touch failure2
|
||||
-> test:test
|
||||
-> testQuick
|
||||
$ delete failure2
|
||||
|
||||
$ touch success3
|
||||
> test:test
|
||||
> testQuick
|
||||
$ delete success3
|
||||
|
||||
$ touch failure3
|
||||
-> test:test
|
||||
-> testQuick
|
||||
$ delete failure3
|
||||
|
||||
> set Compile / scalacOptions += "-Xfatal-warnings"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
Test / testOptions += {
|
||||
val baseDir = baseDirectory.value
|
||||
Tests.Setup { () =>
|
||||
IO.touch(baseDir / "setup")
|
||||
IO.touch(baseDir / "setup")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -9,8 +9,8 @@ Test / testOptions += {
|
|||
val t = baseDirectory.value / "tested"
|
||||
val c = baseDirectory.value / "cleanup"
|
||||
Tests.Cleanup { () =>
|
||||
assert(t.exists, "Didn't exist: " + t.getAbsolutePath)
|
||||
IO.delete(t)
|
||||
IO.touch(c)
|
||||
// assert(t.exists, "Didn't exist: " + t.getAbsolutePath)
|
||||
IO.delete(t)
|
||||
IO.touch(c)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ $ absent tested
|
|||
$ absent cleanup
|
||||
|
||||
# without Setup configured, the setup file won't exist and the test will fail
|
||||
-> test
|
||||
-> testQuick
|
||||
|
||||
# check that we are starting clean
|
||||
$ absent setup
|
||||
|
|
@ -39,7 +39,7 @@ $ absent tested
|
|||
$ absent cleanup
|
||||
|
||||
# without Setup configured, the setup file won't exist and the test will fail
|
||||
-> test
|
||||
-> testQuick
|
||||
|
||||
# check that we are starting clean
|
||||
$ absent setup
|
||||
|
|
|
|||
Loading…
Reference in New Issue