[2.x] fix: Include test arg into hash (#9222)

**Problem**
test -- foo passes on MUnit, allowing failing tests to be validated.

**Solution**
Include test args into the cache input.
This commit is contained in:
eugene yokota 2026-05-16 04:12:33 -04:00 committed by GitHub
parent 68dde13fdc
commit 57495c28dc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 73 additions and 26 deletions

View File

@ -782,6 +782,7 @@ lazy val mainProj = (project in file("main"))
exclude[MissingClassProblem]("sbt.Resolvers$DistributedVCS"),
exclude[DirectMissingMethodProblem]("sbt.internal.ClassStamper.stampVf"),
exclude[DirectMissingMethodProblem]("sbt.internal.CompileInputs2.*"),
exclude[DirectMissingMethodProblem]("sbt.internal.IncrementalTest.cacheInput"),
),
)
.dependsOn(lmCore, lmCoursierShadedPublishing)

View File

@ -1440,8 +1440,23 @@ object Defaults extends BuildCommon with DefExtra {
val st = state.value
given display: Show[ScopedKey[?]] = Project.showContextKey(st)
val modifiedOpts =
Tests.ExplicitlyRequestedNames(selected) +: Tests.Filters(filter(selected)) +:
Tests.ExplicitlyRequestedNames(selected) +:
Tests.Filters(
filter(
selected ++ (if frameworkOptions.nonEmpty then Seq("--") ++ frameworkOptions else Nil)
)
) +:
Tests.Argument(frameworkOptions*) +: config.options
if frameworkOptions.nonEmpty then
modifiedOpts.foreach: opt =>
opt match
case Tests.Listeners(listeners) =>
listeners.toList.foreach: l =>
l match
case r: TestStatusReporter =>
r.setArguments(frameworkOptions)
case _ => ()
case _ => ()
val newConfig = config.copy(options = modifiedOpts)
val output = allTestGroupsTask(
s,

View File

@ -32,15 +32,22 @@ object IncrementalTest:
val cp = (Keys.test / fullClasspath).value
val digests = (Keys.definedTestDigests).value
val config = Def.cacheConfiguration.value
def hasCachedSuccess(ts: Digest): Boolean =
val input = cacheInput(ts)
def hasCachedSuccess(ts: Digest, options: Seq[String]): Boolean =
val input = cacheInput(ts, options)
ActionCache.exists(input._1, input._2, input._3, config)
def hasSucceeded(className: String): Boolean = digests.get(className) match
case None => false
case Some(ts) => hasCachedSuccess(ts)
def hasSucceeded(className: String, options: Seq[String]): Boolean =
digests.get(className) match
case None => false
case Some(ts) => hasCachedSuccess(ts, options)
args =>
for filter <- selectedFilter(args)
yield (test: String) => filter(test) && !hasSucceeded(test)
val (pattern, options) =
args.indexOf("--") match
case idx if idx >= 0 =>
val (s1, s2) = args.splitAt(idx)
(s1, s2.drop(1))
case _ => (args, Nil)
for filter <- selectedFilter(pattern)
yield (test: String) => filter(test) && !hasSucceeded(test, options)
}
// cache the test digests against the fullClasspath.
@ -84,17 +91,23 @@ object IncrementalTest:
case _ =>
includeFilters.map(f => (s: String) => (f.accept(s) && !matches(excludeFilters, s)))
private[sbt] def cacheInput(value: Digest): (Unit, Digest, Digest) =
((), value, Digest.zero)
private[sbt] def cacheInput(
value: Digest,
frameworkOptions: Seq[String]
): (Seq[String], Digest, Digest) = (frameworkOptions, value, Digest.zero)
end IncrementalTest
private[sbt] class TestStatusReporter(
private[sbt] case class TestStatusReporter(
digests: Map[String, Digest],
cacheConfiguration: BuildWideCacheConfiguration,
) extends TestsListener:
// int value to represent success
private final val successfulTest = 0
private var _args: Seq[String] = Nil
def getArgs: Seq[String] = _args
def setArguments(args: Seq[String]): Unit =
_args = args
def doInit(): Unit = ()
def startGroup(name: String): Unit = ()
def testEvent(event: TestEvent): Unit = ()
@ -109,7 +122,7 @@ private[sbt] class TestStatusReporter(
digests.get(name) match
case Some(ts) =>
// treat each test suite as a successful action that returns 0
val input = IncrementalTest.cacheInput(ts)
val input = IncrementalTest.cacheInput(ts, getArgs)
ActionCache.cache(
key = input._1,
codeContentHash = input._2,

View File

@ -7,11 +7,13 @@ testFrameworks := new TestFramework("build.MyFramework") :: Nil
fork := true
Test / definedTests += new sbt.TestDefinition(
"my",
// marker fingerprint since there are no test classes
// to be discovered by sbt:
new sbt.testing.AnnotatedFingerprint {
def isModule = true
def annotationName = "my"
}, true, Array()
)
"my",
// marker fingerprint since there are no test classes
// to be discovered by sbt:
new sbt.testing.AnnotatedFingerprint {
def isModule = true
def annotationName = "my"
},
true,
Array()
)

View File

@ -9,11 +9,14 @@ class MyFramework extends sbt.testing.Framework {
new MyRunner(args, remoteArgs, testClassLoader)
}
class MyRunner(val args: Array[String], val remoteArgs: Array[String],
val testClassLoader: ClassLoader) extends sbt.testing.Runner {
class MyRunner(
val args: Array[String],
val remoteArgs: Array[String],
val testClassLoader: ClassLoader) extends sbt.testing.Runner {
def tasks(taskDefs: Array[TaskDef]): Array[Task] =
if (args contains "task-boom") taskDefs map BoomTask else throw new Throwable()
if (args.contains("task-boom")) taskDefs.map(BoomTask)
else throw new Throwable()
def done(): String = ""
private case class BoomTask(taskDef: TaskDef) extends Task {
@ -21,4 +24,3 @@ class MyRunner(val args: Array[String], val remoteArgs: Array[String],
def execute(handler: EventHandler, loggers: Array[Logger]) = throw new Throwable()
}
}

View File

@ -1,2 +1,2 @@
-> test
-> testOnly -- task-boom
> testOnly -- task-boom

View File

@ -0,0 +1,2 @@
scalaVersion := "3.8.3"
libraryDependencies += "org.scalameta" %% "munit" % "0.7.29" % Test

View File

@ -0,0 +1,5 @@
package example
class ATest extends munit.FunSuite:
test("sum"):
assertEquals(3, 1 + 1)

View File

@ -0,0 +1,3 @@
-> test
> test -- foo
-> test

View File

@ -259,6 +259,10 @@ public class ForkTestMain {
params, this.id);
this.originalOut.println(notification);
this.originalOut.flush();
try {
Thread.sleep(10);
} catch (final Exception e) {
}
}
private void log(final String message, final ForkTags level) {