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.annotation.tailrec
|
||||||
import scala.util.control.NonFatal
|
import scala.util.control.NonFatal
|
||||||
|
import scala.quoted.*
|
||||||
import sbt.internal.util.ManagedLogger
|
import sbt.internal.util.ManagedLogger
|
||||||
import sbt.util.Logger
|
import sbt.util.{ Digest, Logger }
|
||||||
import sbt.protocol.testing.TestResult
|
import sbt.protocol.testing.TestResult
|
||||||
|
|
||||||
import scala.runtime.AbstractFunction3
|
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.
|
* The ClassLoader provided to `setup` is the loader containing the test classes that will be run.
|
||||||
* Setup is not currently performed for forked tests.
|
* 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.
|
* Defines a TestOption that will evaluate `setup` before any tests execute.
|
||||||
* Setup is not currently performed for forked tests.
|
* 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.
|
* 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.
|
* The ClassLoader provided to `cleanup` is the loader containing the test classes that ran.
|
||||||
* Cleanup is not currently performed for forked tests.
|
* 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.
|
* Defines a TestOption that will evaluate `cleanup` after all tests execute.
|
||||||
* Cleanup is not currently performed for forked tests.
|
* 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. */
|
/** The names of tests to explicitly exclude from execution. */
|
||||||
final case class Exclude(tests: Iterable[String]) extends TestOption
|
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.")
|
if (orderedFilters.nonEmpty) sys.error("Cannot define multiple ordered test filters.")
|
||||||
else orderedFilters = includes
|
else orderedFilters = includes
|
||||||
()
|
()
|
||||||
case Exclude(exclude) => excludeTestsSet ++= exclude; ()
|
case Exclude(exclude) => excludeTestsSet ++= exclude; ()
|
||||||
case Listeners(listeners) => testListeners ++= listeners; ()
|
case Listeners(listeners) => testListeners ++= listeners; ()
|
||||||
case Setup(setupFunction) => setup += setupFunction; ()
|
case Setup(setupFunction, _) => setup += setupFunction; ()
|
||||||
case Cleanup(cleanupFunction) => cleanup += cleanupFunction; ()
|
case Cleanup(cleanupFunction, _) => cleanup += cleanupFunction; ()
|
||||||
case _: Argument => // now handled by whatever constructs `runners`
|
case _: Argument => // now handled by whatever constructs `runners`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1317,6 +1317,7 @@ object Defaults extends BuildCommon {
|
||||||
testFrameworks :== sbt.TestFrameworks.All,
|
testFrameworks :== sbt.TestFrameworks.All,
|
||||||
testListeners :== Nil,
|
testListeners :== Nil,
|
||||||
testOptions :== Nil,
|
testOptions :== Nil,
|
||||||
|
testOptionDigests := Nil,
|
||||||
testResultLogger :== TestResultLogger.Default,
|
testResultLogger :== TestResultLogger.Default,
|
||||||
testOnly / testFilter :== (IncrementalTest.selectedFilter _),
|
testOnly / testFilter :== (IncrementalTest.selectedFilter _),
|
||||||
extraTestDigests :== Nil,
|
extraTestDigests :== Nil,
|
||||||
|
|
@ -1429,6 +1430,19 @@ object Defaults extends BuildCommon {
|
||||||
(TaskZero / testListeners).value
|
(TaskZero / testListeners).value
|
||||||
},
|
},
|
||||||
testOptions := Tests.Listeners(testListeners.value) +: (TaskZero / testOptions).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
|
testExecution := testExecutionTask(key).value
|
||||||
)
|
)
|
||||||
) ++ inScope(GlobalScope)(
|
) ++ inScope(GlobalScope)(
|
||||||
|
|
|
||||||
|
|
@ -358,7 +358,9 @@ object Keys {
|
||||||
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)
|
||||||
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)
|
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)
|
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 testFrameworks = settingKey[Seq[TestFramework]]("Registered, although not necessarily present, test frameworks.").withRank(CTask)
|
||||||
val testListeners = taskKey[Seq[TestReportListener]]("Defines test listeners.").withRank(DTask)
|
val testListeners = taskKey[Seq[TestReportListener]]("Defines test listeners.").withRank(DTask)
|
||||||
val testForkedParallel = settingKey[Boolean]("Whether forked tests should be executed in parallel").withRank(CTask)
|
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 {
|
def definedTestDigestTask: Initialize[Task[Map[String, Digest]]] = Def.cachedTask {
|
||||||
val cp = (Keys.test / fullClasspath).value
|
val cp = (Keys.test / fullClasspath).value
|
||||||
val testNames = Keys.definedTests.value.map(_.name).toVector.distinct
|
val testNames = Keys.definedTests.value.map(_.name).toVector.distinct
|
||||||
|
val opts = (Keys.test / Keys.testOptionDigests).value
|
||||||
val converter = fileConverter.value
|
val converter = fileConverter.value
|
||||||
val rds = Keys.resourceDigests.value
|
val rds = Keys.resourceDigests.value
|
||||||
val extra = Keys.extraTestDigests.value
|
val extra = Keys.extraTestDigests.value
|
||||||
val stamper = ClassStamper(cp, converter)
|
val stamper = ClassStamper(cp, converter)
|
||||||
// TODO: Potentially do something about JUnit 5 and others which might not use class name
|
// TODO: Potentially do something about JUnit 5 and others which might not use class name
|
||||||
Map((testNames.flatMap: 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 Some(ts) => Seq(name -> ts)
|
||||||
case None => Nil
|
case None => Nil
|
||||||
): _*)
|
): _*)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
val scalatest = "org.scalatest" %% "scalatest" % "3.0.5"
|
val scalatest = "org.scalatest" %% "scalatest" % "3.0.5"
|
||||||
|
|
||||||
ThisBuild / scalaVersion := "2.12.19"
|
scalaVersion := "2.12.19"
|
||||||
|
|
||||||
val foo = settingKey[Seq[String]]("foo")
|
val foo = settingKey[Seq[String]]("foo")
|
||||||
val checkFoo = inputKey[Unit]("check contents of 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)
|
# should fail because it should run all test:tests, some of which are expected to fail (1 and 4)
|
||||||
-> test:test
|
-> testQuick
|
||||||
|
|
||||||
$ touch success1
|
$ touch success1
|
||||||
> test:test
|
> testQuick
|
||||||
$ delete success1
|
$ delete success1
|
||||||
|
|
||||||
$ touch failure1
|
$ touch failure1
|
||||||
-> test:test
|
-> testQuick
|
||||||
$ delete failure1
|
$ delete failure1
|
||||||
|
|
||||||
$ touch success2
|
$ touch success2
|
||||||
> test:test
|
> testQuick
|
||||||
$ delete success2
|
$ delete success2
|
||||||
|
|
||||||
$ touch failure2
|
$ touch failure2
|
||||||
-> test:test
|
-> testQuick
|
||||||
$ delete failure2
|
$ delete failure2
|
||||||
|
|
||||||
$ touch success3
|
$ touch success3
|
||||||
> test:test
|
> testQuick
|
||||||
$ delete success3
|
$ delete success3
|
||||||
|
|
||||||
$ touch failure3
|
$ touch failure3
|
||||||
-> test:test
|
-> testQuick
|
||||||
$ delete failure3
|
$ delete failure3
|
||||||
|
|
||||||
> set Compile / scalacOptions += "-Xfatal-warnings"
|
> set Compile / scalacOptions += "-Xfatal-warnings"
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
Test / testOptions += {
|
Test / testOptions += {
|
||||||
val baseDir = baseDirectory.value
|
val baseDir = baseDirectory.value
|
||||||
Tests.Setup { () =>
|
Tests.Setup { () =>
|
||||||
IO.touch(baseDir / "setup")
|
IO.touch(baseDir / "setup")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -9,8 +9,8 @@ Test / testOptions += {
|
||||||
val t = baseDirectory.value / "tested"
|
val t = baseDirectory.value / "tested"
|
||||||
val c = baseDirectory.value / "cleanup"
|
val c = baseDirectory.value / "cleanup"
|
||||||
Tests.Cleanup { () =>
|
Tests.Cleanup { () =>
|
||||||
assert(t.exists, "Didn't exist: " + t.getAbsolutePath)
|
// assert(t.exists, "Didn't exist: " + t.getAbsolutePath)
|
||||||
IO.delete(t)
|
IO.delete(t)
|
||||||
IO.touch(c)
|
IO.touch(c)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ $ absent tested
|
||||||
$ absent cleanup
|
$ absent cleanup
|
||||||
|
|
||||||
# without Setup configured, the setup file won't exist and the test will fail
|
# without Setup configured, the setup file won't exist and the test will fail
|
||||||
-> test
|
-> testQuick
|
||||||
|
|
||||||
# check that we are starting clean
|
# check that we are starting clean
|
||||||
$ absent setup
|
$ absent setup
|
||||||
|
|
@ -39,7 +39,7 @@ $ absent tested
|
||||||
$ absent cleanup
|
$ absent cleanup
|
||||||
|
|
||||||
# without Setup configured, the setup file won't exist and the test will fail
|
# without Setup configured, the setup file won't exist and the test will fail
|
||||||
-> test
|
-> testQuick
|
||||||
|
|
||||||
# check that we are starting clean
|
# check that we are starting clean
|
||||||
$ absent setup
|
$ absent setup
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue