Merge branch 'develop' into fix/1469-junit-xml-fork-stacktrace

This commit is contained in:
volcano303 2026-04-18 00:04:40 +02:00
commit ee6137da2a
39 changed files with 631 additions and 84 deletions

View File

@ -31,7 +31,7 @@ object LockFile {
val json = Converter.toJson(data).get
val content = PrettyPrinter(json)
lockFile.getParentFile.mkdirs()
Files.write(lockFile.toPath, content.getBytes(StandardCharsets.UTF_8))
Files.writeString(lockFile.toPath, content)
} match {
case Success(_) => Right(())
case Failure(ex) => Left(s"Failed to write lock file: ${ex.getMessage}")

View File

@ -194,6 +194,9 @@ private class React(
) extends WorkerResponseListener:
val g = WorkerMain.mkGson()
val promise: Promise[Int] = Promise()
/** Events per test group, accumulated for [[SuiteResult]] (listeners get each event immediately). */
private val progressEvents = mutable.Map.empty[String, mutable.ArrayBuffer[testing.Event]]
override def apply(line: String): Unit =
try
val o = JsonParser.parseString(line).getAsJsonObject()
@ -235,15 +238,30 @@ private class React(
case ForkTags.Debug => log.debug(info.message)
case _ => ()
else ()
case "testEvents" =>
case "startTestGroup" =>
val params = o.getAsJsonObject("params")
val info =
g.fromJson[ForkTestMain.ForkGroupStart](params, classOf[ForkTestMain.ForkGroupStart])
if info.id == id then
progressEvents(info.group) = mutable.ArrayBuffer.empty
listeners.foreach(_.startGroup(info.group))
else ()
case "testProgress" =>
val params = o.getAsJsonObject("params")
val info =
g.fromJson[ForkTestMain.ForkEventsInfo](params, classOf[ForkTestMain.ForkEventsInfo])
if info.id == id then
val events = info.events.asScala.toSeq
listeners.foreach(_.startGroup(info.group))
val event = TestEvent(events)
listeners.foreach(_.testEvent(event))
val buf = progressEvents.getOrElseUpdate(info.group, mutable.ArrayBuffer.empty)
for e <- info.events.asScala do
buf += e
listeners.foreach(_.testEvent(TestEvent(Seq(e))))
else ()
case "endTestGroup" =>
val params = o.getAsJsonObject("params")
val info =
g.fromJson[ForkTestMain.ForkGroupEnd](params, classOf[ForkTestMain.ForkGroupEnd])
if info.id == id then
val events = progressEvents.remove(info.group).getOrElse(mutable.ArrayBuffer.empty).toSeq
val suiteResult = SuiteResult(events)
results += info.group -> suiteResult
listeners.foreach(_.endGroup(info.group, suiteResult.result))

View File

@ -50,6 +50,8 @@ object WorkerExchange:
val scanner = Scanner(socket.getInputStream(), "UTF-8")
while scanner.hasNextLine() do notifyListeners(scanner.nextLine())
})
accepter.setName("sbt-fork-test-response-reader")
accepter.setPriority(Thread.NORM_PRIORITY + 1)
accepter.start()
Some(serverSocket)
case _ => None

View File

@ -790,6 +790,28 @@ object Defaults extends BuildCommon with DefExtra {
ScalaArtifacts.Organization,
appConfiguration.value.provider.scalaProvider.version
),
// consoleProject compiles the *build definition*, which targets sbt's own
// Scala version (the launcher's scalaProvider.version), not the project's.
// The default `scalaCompilerBridgeBin` resolves via the `update` report,
// which uses the project's scalaVersion; resolve the pre-built Scala 3
// bridge directly for sbt's Scala version instead. See sbt/sbt#7722.
consoleProject / scalaCompilerBridgeBin := Def.uncached {
val sv = appConfiguration.value.provider.scalaProvider.version
val st = state.value
val g = BuildPaths.getGlobalBase(st)
val zincDir = BuildPaths.getZincDirectory(st, g)
val conv = fileConverter.value
Compiler
.scala3ConsoleProjectBridgeJar(
scalaVersion = sv,
scalaOrg = ScalaArtifacts.Organization,
dr = (LocalRootProject / dependencyResolution).value,
retrieveDir = zincDir,
log = streams.value.log,
)
.toVector
.map(jar => conv.toVirtualFile(jar.toPath()): HashedVirtualFileRef)
},
classpathOptions := ClasspathOptionsUtil.noboot(scalaVersion.value),
console / classpathOptions := ClasspathOptionsUtil.replNoboot(scalaVersion.value),
)
@ -811,22 +833,35 @@ object Defaults extends BuildCommon with DefExtra {
),
consoleProject := ConsoleProject.consoleProjectTask.value,
consoleProject / scalaInstance := {
val topLoader = classOf[org.jline.terminal.Terminal].getClassLoader
// Use the classloader that has `ConsoleProjectBindings` as the top so
// that code generated by `initialCommands` in the REPL sees the same
// class instance that sbt has set fields on. See sbt/sbt#7722.
val topLoader = classOf[ConsoleProjectBindings.type].getClassLoader
val scalaProvider = appConfiguration.value.provider.scalaProvider
val allJars = scalaProvider.jars
val libraryJars = allJars.filter { jar =>
val launcherJars = scalaProvider.jars
val libraryJars = launcherJars.filter { jar =>
jar.getName == "scala-library.jar" || jar.getName.startsWith("scala3-library_3")
}
val compilerJar = allJars.filter { jar =>
jar.getName == "scala-compiler.jar" || jar.getName.startsWith("scala3-compiler_3")
}
ScalaInstance(scalaProvider.version, scalaProvider.launcher)
// Scala 3.8+ extracted ReplDriver from scala3-compiler_3 into scala3-repl_3,
// which the sbt launcher does not ship. Resolve it explicitly so that
// consoleProject can start the Scala 3 REPL. See sbt/sbt#7722.
val st = state.value
val g = BuildPaths.getGlobalBase(st)
val zincDir = BuildPaths.getZincDirectory(st, g)
val replToolJars = Compiler.scala3ReplToolJars(
scalaVersion = scalaProvider.version,
scalaOrg = ScalaArtifacts.Organization,
dr = (LocalRootProject / dependencyResolution).value,
retrieveDir = zincDir,
log = streams.value.log,
)
val allJars = (launcherJars.toSeq ++ replToolJars).distinct
Compiler.makeScalaInstance(
scalaProvider.version,
libraryJars,
allJars.toSeq,
Seq.empty,
state.value,
allJars,
replToolJars,
st,
topLoader,
)
},

View File

@ -27,6 +27,8 @@ import sbt.librarymanagement.{
Configuration,
Configurations,
ConfigurationReport,
CrossVersion,
DependencyResolution,
ModuleID,
ScalaArtifacts,
SemanticSelector,
@ -110,6 +112,73 @@ object Compiler:
case _ => ScalaInstance(sv, scalaProvider)
}
/**
* Resolves extra REPL tool jars required for Scala 3.8+.
*
* In Scala 3.8+, `dotty.tools.repl.ReplDriver` was extracted from
* `scala3-compiler_3` into a new artifact `scala3-repl_3`. The sbt launcher's
* Scala provider only includes `scala3-compiler_3`, so we need to resolve
* `scala3-repl_3` explicitly for `consoleProject` to work.
*
* Returns an empty sequence for Scala versions < 3.8.
*
* @see https://github.com/sbt/sbt/issues/7722
* @see https://github.com/scala/scala3/pull/24243
*/
private[sbt] def scala3ReplToolJars(
scalaVersion: String,
scalaOrg: String,
dr: DependencyResolution,
retrieveDir: File,
log: Logger
): Seq[File] =
if !ScalaArtifacts.isScala3_8Plus(scalaVersion) then Nil
else
// Scala 3 artifacts use the `_3` suffix, not `_3.8`. Bake the suffix into
// the artifact name and disable cross-version resolution, because the
// caller's scalaModuleInfo may be None or reflect a different project
// Scala version than the one we actually need.
val replModule = ModuleID(scalaOrg, s"${ScalaArtifacts.Scala3ReplID}_3", scalaVersion)
.withCrossVersion(CrossVersion.disabled)
dr.retrieve(replModule, scalaModuleInfo = None, retrieveDir, log) match
case Right(resolved) => resolved.toSeq
case Left(unresolved) =>
log.warn(
s"Could not resolve $replModule for consoleProject; REPL may fail to start: ${unresolved.resolveException.getMessage}"
)
Nil
/**
* Resolves the pre-built Scala 3 compiler bridge jar for `consoleProject`.
*
* The default `scalaCompilerBridgeBin` uses the project's Scala version, but
* `consoleProject` compiles the build definition with sbt's own Scala
* version. Resolve the pre-built bridge directly for that version to keep
* the bridge consistent with `consoleProject / scalaInstance`.
*
* The bridge is a Java-compiled jar (cross-version disabled).
*
* @see https://github.com/sbt/sbt/issues/7722
*/
private[sbt] def scala3ConsoleProjectBridgeJar(
scalaVersion: String,
scalaOrg: String,
dr: DependencyResolution,
retrieveDir: File,
log: Logger
): Option[File] =
if !ScalaArtifacts.isScala3(scalaVersion) then None
else
val bridgeModule = ModuleID(scalaOrg, "scala3-sbt-bridge", scalaVersion)
.withCrossVersion(CrossVersion.disabled)
dr.retrieve(bridgeModule, scalaModuleInfo = None, retrieveDir, log) match
case Right(resolved) => resolved.find(_.getName.startsWith("scala3-sbt-bridge"))
case Left(unresolved) =>
log.warn(
s"Could not resolve $bridgeModule for consoleProject: ${unresolved.resolveException.getMessage}"
)
None
def scalaInstanceConfigFromHome(dir: File): Def.Initialize[Task[ScalaInstanceConfig]] =
Def.task {
val dummy = ScalaInstance(dir)(Keys.state.value.classLoaderCache.apply)

View File

@ -53,7 +53,9 @@ object ConsoleProject:
): Unit = {
val extracted = Project.extract(state)
val cpImports = new Imports(extracted, state)
// Bindings are blocked by https://github.com/scala/scala3/issues/5069
// Bindings are ignored by Scala 3 bridge: https://github.com/scala/scala3/issues/5069
// Workaround: vals are injected via initialCommands from ConsoleProjectBindings holder.
// bindings are still passed to Console for Scala 2 backward compatibility.
val bindings =
("currentState" -> state) :: ("extracted" -> extracted) :: ("cpHelpers" -> cpImports) :: Nil
val unit = extracted.currentUnit
@ -86,22 +88,120 @@ object ConsoleProject:
classLoaderCache = state.get(BasicKeys.classLoaderCache),
log = log
)
val imports = BuildUtil.getImports(unit.unit) ++ BuildUtil.importAll(bindings.map(_._1))
val importString = imports.mkString("", ";\n", ";\n\n")
val initCommands = importString + extra
val loader = ClasspathUtil.makeLoader(unit.classpath, si, tempDir)
ConsoleProjectBindings.set(state, extracted, cpImports)
val baseImports = BuildUtil.getImports(unit.unit)
val bindingDefs = Seq(
"val currentState = _root_.sbt.internal.ConsoleProjectBindings.state",
"val extracted = _root_.sbt.internal.ConsoleProjectBindings.extracted",
"val cpHelpers = _root_.sbt.internal.ConsoleProjectBindings.cpHelpers",
)
val bindingImports = BuildUtil.importAll(bindings.map(_._1))
val allLines = baseImports ++ bindingDefs ++ bindingImports
val initCommands = allLines.mkString("", ";\n", ";\n\n") + extra
// Two things are required so the REPL resolves `sbt.*` (e.g.
// `sbt.TaskKey`, `sbt.Keys`, `sbt.State`, `sbt.internal.ConsoleProjectBindings`)
// and `scala.*` (e.g. `scala.Function2`) via the *same* class objects
// that sbt itself uses. See sbt/sbt#7722.
//
// 1. Remove sbt's own module jars from the runtime URL classloader,
// so that `sbt.*` references resolve via parent delegation back
// to sbt's `MetaBuildLoader` rather than being defined a second
// time by the REPL's URL classloader (which would break the
// `ConsoleProjectBindings` singleton's static state and
// trigger `LinkageError: loader constraint violation` when REPL
// code touches a method whose signature mentions a duplicated
// type e.g. `sbt.TaskKey.zipWith(_, scala.Function2)`).
//
// 2. On Scala 3.8+ switch the REPL's bytecode interrupt
// instrumentation to `local` mode. The default (`true`) for
// `dotty.tools.repl.AbstractFileClassLoader` (added in 3.8)
// reads every class's bytes from the parent loader via
// `getResourceAsStream` and `defineClass`-es them a *second*
// time inside the REPL loader, producing duplicate `Class`
// objects for every `sbt.*` and `scala.*` class. `local` skips
// that re-definition and falls through to standard parent-first
// delegation (so the REPL sees the same singleton classes as
// the surrounding sbt process) while still keeping interrupt
// support for REPL-defined code preserving Ctrl+C for long-
// running expressions like `(Compile / compile).eval`. The flag
// does not exist on Scala 3.7 and earlier (which use the older
// AFClassLoader without instrumentation), so we only pass it
// when the consoleProject scala instance is 3.8+ to avoid a
// "bad option" warning.
//
// The full classpath is still passed to `Console` below so the REPL's
// compile-time classpath is unchanged.
val runtimeClasspath = unit.classpath.filterNot(isSbtModuleJar)
val loader = ClasspathUtil.makeLoader(runtimeClasspath, si, tempDir)
val replOptions =
if needsInterruptInstrumentationOptOut(si.version) then
"-Xrepl-interrupt-instrumentation:local" +: options
else options
val terminal = Terminal.get
// TODO - Hook up dsl classpath correctly...
(new Console(compiler))(
unit.classpath.map(_.toFile),
options,
initCommands,
cleanupCommands,
terminal
)(Some(loader), bindings).get
()
try
(new Console(compiler))(
unit.classpath.map(_.toFile),
replOptions,
initCommands,
cleanupCommands,
terminal
)(Some(loader), bindings).get
()
finally ConsoleProjectBindings.clear()
}
/**
* `dotty.tools.repl.AbstractFileClassLoader`'s bytecode interrupt
* instrumentation was added in Scala 3.8 and is enabled by default
* see Scala 3 PR scala/scala3#22720. The setting that disables it is
* also a 3.8+ addition (see `Xrepl-interrupt-instrumentation` in
* `dotty.tools.dotc.config.ScalaSettings`). For 3.7 and earlier the
* REPL classloader doesn't re-define classes locally, so the flag is
* unnecessary and would only produce a "bad option" warning.
*/
private def needsInterruptInstrumentationOptOut(scalaVersion: String): Boolean =
scalaVersion match
case s"3.$rest" =>
rest.takeWhile(_.isDigit).toIntOption.exists(_ >= 8)
case _ => false
/**
* Returns true when a `Path` refers to a jar published by
* `org.scala-sbt`. These jars ship sbt's own classes (e.g. `sbt.State`,
* `sbt.TaskKey`, `sbt.Keys`) that are already reachable via the parent
* class loader used by `consoleProject`. They must be excluded from the
* REPL's runtime classloader so that `sbt.*` references resolve via
* parent delegation and reach sbt's singleton copies rather than
* being defined fresh by the URL classloader from `unit.classpath`,
* which would trigger a `LinkageError: loader constraint violation`
* whenever those classes are used from the REPL. See sbt/sbt#7722.
*
* Detection is done via `META-INF/MANIFEST.MF`'s `Implementation-Vendor-Id`
* attribute, which all sbt module jars set to `org.scala-sbt`. This is
* more robust than checking for specific class entries, because it
* uniformly catches every sbt module (main, main-settings, command, io,
* util-*, etc.) without enumerating them.
*/
private def isSbtModuleJar(p: java.nio.file.Path): Boolean =
val name = p.getFileName.toString
if !name.endsWith(".jar") || !java.nio.file.Files.isRegularFile(p) then false
else
try
val zf = new java.util.zip.ZipFile(p.toFile)
try
val entry = zf.getEntry("META-INF/MANIFEST.MF")
if entry eq null then false
else
val is = zf.getInputStream(entry)
try
val manifest = new java.util.jar.Manifest(is)
val attrs = manifest.getMainAttributes
attrs != null && "org.scala-sbt" == attrs.getValue("Implementation-Vendor-Id")
finally is.close()
finally zf.close()
catch case _: java.io.IOException => false
/** Conveniences for consoleProject that shouldn't normally be used for builds. */
final class Imports private[sbt] (extracted: Extracted, state: State) {
import extracted.*

View File

@ -0,0 +1,45 @@
/*
* sbt
* Copyright 2023, Scala center
* Copyright 2011 - 2022, Lightbend, Inc.
* Copyright 2008 - 2010, Mark Harrah
* Licensed under Apache License 2.0 (see LICENSE)
*/
package sbt
package internal
import scala.compiletime.uninitialized
/**
* Static holder for consoleProject bindings.
*
* Scala 3 compiler bridge does not implement REPL binding injection
* (https://github.com/scala/scala3/issues/5069), so sbt generates `val`
* definitions in `initialCommands` that read from this holder.
*
* The holder has to be resolved to the same JVM `Class` object from both
* sbt itself and the Scala 3 REPL's `AbstractFileClassLoader`. sbt's
* own sbt module jars are removed from the REPL runtime classloader in
* `ConsoleProject.apply`, so that all `sbt.*` references in the REPL go
* through the parent chain and reach the sbt singleton.
*/
object ConsoleProjectBindings:
@volatile private var _state: State = uninitialized
@volatile private var _extracted: Extracted = uninitialized
@volatile private var _cpHelpers: ConsoleProject.Imports = uninitialized
def set(state: State, extracted: Extracted, cpHelpers: ConsoleProject.Imports): Unit =
_state = state
_extracted = extracted
_cpHelpers = cpHelpers
def clear(): Unit =
_state = null
_extracted = null
_cpHelpers = null
def state: State = _state
def extracted: Extracted = _extracted
def cpHelpers: ConsoleProject.Imports = _cpHelpers
end ConsoleProjectBindings

View File

@ -3,6 +3,6 @@ import java.nio.file.Files
object Main {
def main(args: Array[String]): Unit = {
Files.write(new File("output").toPath, "OK".getBytes("UTF-8"))
Files.writeString(new File("output").toPath, "OK")
}
}

View File

@ -29,6 +29,6 @@ object Main {
"Expected not to find classes from argonaut"
)
Files.write(new File("output").toPath, "OK".getBytes("UTF-8"))
Files.writeString(new File("output").toPath, "OK")
}
}

View File

@ -34,6 +34,6 @@ object Main {
"Expected not to find class from cats-mtl"
)
Files.write(new File("output").toPath, "OK".getBytes("UTF-8"))
Files.writeString(new File("output").toPath, "OK")
}
}

View File

@ -5,6 +5,6 @@ import org.apache.zookeeper.ZooKeeper
object Main {
def main(args: Array[String]): Unit = {
Files.write(new File("output").toPath, classOf[ZooKeeper].getSimpleName.getBytes("UTF-8"))
Files.writeString(new File("output").toPath, classOf[ZooKeeper].getSimpleName)
}
}

View File

@ -4,6 +4,6 @@ import java.nio.file.Paths
object Main {
def main(args: Array[String]): Unit = {
val msg = Bar.value
Files.write(Paths.get("baz/output"), msg.getBytes("UTF-8"))
Files.writeString(Paths.get("baz/output"), msg)
}
}

View File

@ -10,6 +10,6 @@ object Main {
def main(args: Array[String]): Unit = {
val msg = CC(2, A.msg).asJson.spaces2
Files.write(new File("output").toPath, msg.getBytes("UTF-8"))
Files.writeString(new File("output").toPath, msg)
}
}

View File

@ -16,6 +16,6 @@ object Main {
assert(hadoopVersion == "2.6.0")
Files.write(new File("output").toPath, "OK".getBytes("UTF-8"))
Files.writeString(new File("output").toPath, "OK")
}
}

View File

@ -8,6 +8,6 @@ object Main {
// assert(Thread.currentThread.getContextClassLoader.getResource("org/nlogo/nvm/Task.class") != null)
// Thread.currentThread.getContextClassLoader.getResource("org/nlogo/nvm/Task.class")
def main(args: Array[String]): Unit = {
Files.write(new File("output").toPath, "OK".getBytes("UTF-8"))
Files.writeString(new File("output").toPath, "OK")
}
}

View File

@ -13,7 +13,7 @@ csrExtraCredentials += {
|foo.https-only=false
""".stripMargin
val dest = (ThisBuild / baseDirectory).value / "project" / "target" / "cred"
Files.write(dest.toPath, content.getBytes("UTF-8"))
Files.writeString(dest.toPath, content)
lmcoursier.credentials.FileCredentials(dest.toString)
}

View File

@ -8,6 +8,6 @@ object Main {
// assert(Thread.currentThread.getContextClassLoader.getResource("org/nlogo/nvm/Task.class") != null)
// Thread.currentThread.getContextClassLoader.getResource("org/nlogo/nvm/Task.class")
def main(args: Array[String]): Unit = {
Files.write(new File("output").toPath, "OK".getBytes("UTF-8"))
Files.writeString(new File("output").toPath, "OK")
}
}

View File

@ -11,6 +11,6 @@ object Main {
val l = Generic[CC].to(cc)
val msg = l.head
Files.write(new File("output").toPath, msg.getBytes("UTF-8"))
Files.writeString(new File("output").toPath, msg)
}
}

View File

@ -11,6 +11,6 @@ object Main {
val l = Generic[CC].to(cc)
val msg = l.head
Files.write(new File("output").toPath, msg.getBytes("UTF-8"))
Files.writeString(new File("output").toPath, msg)
}
}

View File

@ -6,6 +6,6 @@ object Main {
// TODO Use some jvm-repr stuff as a test
def main(args: Array[String]): Unit = {
Files.write(new File("output").toPath, A.default.msg.getBytes("UTF-8"))
Files.writeString(new File("output").toPath, A.default.msg)
}
}

View File

@ -10,6 +10,6 @@ import java.nio.file.Files
*/
object Main {
def main(args: Array[String]): Unit = {
Files.write(new File("output").toPath, "OK".getBytes("UTF-8"))
Files.writeString(new File("output").toPath, "OK")
}
}

View File

@ -6,6 +6,6 @@ object Main {
// TODO Use some jvm-repr stuff
def main(args: Array[String]): Unit = {
Files.write(new File("output").toPath, "OK".getBytes("UTF-8"))
Files.writeString(new File("output").toPath, "OK")
}
}

View File

@ -3,6 +3,6 @@ import java.nio.file.Files
object Main {
def main(args: Array[String]): Unit = {
Files.write(new File("output").toPath, "OK".getBytes("UTF-8"))
Files.writeString(new File("output").toPath, "OK")
}
}

View File

@ -63,6 +63,6 @@ object Main {
notFromCoursierCache("scala-library")
assert(props.lengthCompare(1) == 0, s"Found several library.properties files in classpath: $props")
Files.write(new File("output").toPath, "OK".getBytes("UTF-8"))
Files.writeString(new File("output").toPath, "OK")
}
}

View File

@ -3,6 +3,6 @@ import java.nio.file.Files
object Main {
def main(args: Array[String]): Unit = {
Files.write(new File("output").toPath, "OK".getBytes("UTF-8"))
Files.writeString(new File("output").toPath, "OK")
}
}

View File

@ -3,6 +3,6 @@ import java.nio.file.Files
object Main {
def main(args: Array[String]): Unit = {
Files.write(new File("output").toPath, "OK".getBytes("UTF-8"))
Files.writeString(new File("output").toPath, "OK")
}
}

View File

@ -5,6 +5,6 @@ import org.apache.zookeeper.ZooKeeper
object Main {
def main(args: Array[String]): Unit = {
Files.write(new File("output").toPath, classOf[ZooKeeper].getSimpleName.getBytes("UTF-8"))
Files.writeString(new File("output").toPath, classOf[ZooKeeper].getSimpleName)
}
}

View File

@ -2,4 +2,4 @@ import java.io.File
import java.nio.file.Files
@main def hello() =
Files.write(new File("output").toPath, "OK".getBytes("UTF-8"))
Files.writeString(new File("output").toPath, "OK")

View File

@ -1,3 +1,22 @@
ThisBuild / scalaVersion := "3.0.0-M2"
ThisBuild / scalaVersion := "3.8.3"
lazy val root = project.in(file("."))
lazy val markerFile = settingKey[java.io.File]("marker file written by consoleProject REPL when bindings resolve")
lazy val root = project.in(file(".")).settings(
markerFile := target.value / "console-bindings-ok",
Global / initialCommands := {
val path = markerFile.value.getAbsolutePath.replace("\\", "\\\\")
// Exercise the exact code paths that hit `LinkageError: loader
// constraint violation` in earlier iterations of sbt/sbt#7722:
// 1. Resolving `sbt.Keys.compile` loads `sbt.TaskKey` through the
// REPL's classloader chain (first regression, pre-#9073).
// 2. Calling `TaskKey.zipWith(_, Function2)` tripped on
// `scala.Function2` being defined twice (second regression,
// reported on PR #9073 after the initial fix).
// If either resolution fails, the marker file is never written.
s"""val _compileKey = _root_.sbt.Keys.compile
|val _zipped = _compileKey.zipWith(_compileKey)((_, _) => 0)
|_root_.java.nio.file.Files.writeString(_root_.java.nio.file.Paths.get("$path"), currentState.toString.length.toString + "/" + extracted.toString.length.toString + "/" + cpHelpers.toString.length.toString + "/" + _compileKey.key.label + "/" + _zipped.getClass.getName)
|""".stripMargin
},
)

View File

@ -1 +0,0 @@
> consoleProject

View File

@ -0,0 +1,2 @@
> consoleProject
$ exists target/out/**/console-bindings-ok

View File

@ -0,0 +1,80 @@
import java.util.concurrent.atomic.AtomicLong
import sbt.protocol.testing.TestResult
val startAtNs = collection.concurrent.TrieMap.empty[String, AtomicLong]
val eventCallbackCount = collection.concurrent.TrieMap.empty[String, AtomicLong]
val maxDetailSize = collection.concurrent.TrieMap.empty[String, AtomicLong]
val endSeen = collection.concurrent.TrieMap.empty[String, AtomicLong]
def counter(map: collection.concurrent.TrieMap[String, AtomicLong], key: String): AtomicLong =
map.getOrElseUpdate(key, new AtomicLong(0L))
def resetCounters(key: String): Unit = {
counter(startAtNs, key).set(0L)
counter(eventCallbackCount, key).set(0L)
counter(maxDetailSize, key).set(0L)
counter(endSeen, key).set(0L)
}
def streamingListener(label: String): TestReportListener = new TestReportListener {
def startGroup(name: String): Unit =
counter(startAtNs, label).compareAndSet(0L, System.nanoTime())
def testEvent(event: TestEvent): Unit = {
counter(eventCallbackCount, label).incrementAndGet()
val detailSize = event.detail.size.toLong
val maxSeen = counter(maxDetailSize, label)
var done = false
while (!done) {
val current = maxSeen.get()
if (detailSize <= current) done = true
else done = maxSeen.compareAndSet(current, detailSize)
}
}
def endGroup(name: String, t: Throwable): Unit =
counter(endSeen, label).set(1L)
def endGroup(name: String, result: TestResult): Unit =
counter(endSeen, label).set(1L)
}
lazy val resetListener = taskKey[Unit]("Reset listener state for timing checks")
lazy val checkStreaming = taskKey[Unit]("Assert test events are received before endGroup")
ThisBuild / scalaVersion := "2.12.21"
def commonSettings(label: String): Seq[Def.Setting[_]] =
Seq(
libraryDependencies += "org.scala-sbt" % "test-interface" % "1.0" % Test,
Test / testFrameworks := Seq(new TestFramework("custom.StreamingFramework")),
Test / parallelExecution := false,
testListeners += streamingListener(label),
resetListener := resetCounters(label),
checkStreaming := {
val startNs = counter(startAtNs, label).get()
val callbacks = counter(eventCallbackCount, label).get()
val largestDetail = counter(maxDetailSize, label).get()
val endWasSeen = counter(endSeen, label).get()
if (startNs == 0L) sys.error("startGroup was never called")
if (endWasSeen == 0L) sys.error("endGroup was never called")
if (callbacks < 2L)
sys.error("Expected at least two testEvent callbacks, saw " + callbacks)
if (largestDetail > 1L)
sys.error(
"Expected streamed test events with detail size 1, largest detail size was " + largestDetail
)
}
)
lazy val inproc = (project in file("inproc"))
.settings(commonSettings("inproc"): _*)
.settings(Test / fork := false)
lazy val forked = (project in file("forked"))
.settings(commonSettings("forked"): _*)
.settings(Test / fork := true)
lazy val root = (project in file("."))
.aggregate(inproc, forked)
.settings(publish / skip := true)

View File

@ -0,0 +1,68 @@
package custom
import sbt.testing._
trait StreamTest
final class SampleTest extends StreamTest
final class StreamingFramework extends Framework {
def name(): String = "StreamingFramework"
def fingerprints(): Array[Fingerprint] =
Array(
new SubclassFingerprint {
def isModule(): Boolean = false
def superclassName(): String = "custom.StreamTest"
def requireNoArgConstructor(): Boolean = true
}
)
def runner(
args: Array[String],
remoteArgs: Array[String],
testClassLoader: ClassLoader
): Runner =
new StreamingRunner
}
final class StreamingRunner extends Runner {
def tasks(taskDefs: Array[TaskDef]): Array[Task] =
taskDefs.map(new StreamingTask(_))
def done(): String = ""
def args(): Array[String] = Array.empty
def remoteArgs(): Array[String] = Array.empty
def receiveMessage(msg: String): Option[String] = None
def serializeTask(task: Task, serializer: TaskDef => String): String =
serializer(task.taskDef())
def deserializeTask(task: String, deserializer: String => TaskDef): Task =
new StreamingTask(deserializer(task))
}
final class StreamingTask(td: TaskDef) extends Task {
def taskDef(): TaskDef = td
def tags(): Array[String] = Array.empty
def execute(handler: EventHandler, loggers: Array[Logger]): Array[Task] = {
handler.handle(new StreamingEvent(td, "first"))
Thread.sleep(1200L)
handler.handle(new StreamingEvent(td, "second"))
Array.empty
}
}
final class StreamingEvent(td: TaskDef, testName: String) extends Event {
def fullyQualifiedName(): String = td.fullyQualifiedName()
def fingerprint(): Fingerprint = td.fingerprint()
def selector(): Selector = new TestSelector(testName)
def status(): Status = Status.Success
def throwable(): OptionalThrowable = new OptionalThrowable()
def duration(): Long = 0L
}

View File

@ -0,0 +1,68 @@
package custom
import sbt.testing._
trait StreamTest
final class SampleTest extends StreamTest
final class StreamingFramework extends Framework {
def name(): String = "StreamingFramework"
def fingerprints(): Array[Fingerprint] =
Array(
new SubclassFingerprint {
def isModule(): Boolean = false
def superclassName(): String = "custom.StreamTest"
def requireNoArgConstructor(): Boolean = true
}
)
def runner(
args: Array[String],
remoteArgs: Array[String],
testClassLoader: ClassLoader
): Runner =
new StreamingRunner
}
final class StreamingRunner extends Runner {
def tasks(taskDefs: Array[TaskDef]): Array[Task] =
taskDefs.map(new StreamingTask(_))
def done(): String = ""
def args(): Array[String] = Array.empty
def remoteArgs(): Array[String] = Array.empty
def receiveMessage(msg: String): Option[String] = None
def serializeTask(task: Task, serializer: TaskDef => String): String =
serializer(task.taskDef())
def deserializeTask(task: String, deserializer: String => TaskDef): Task =
new StreamingTask(deserializer(task))
}
final class StreamingTask(td: TaskDef) extends Task {
def taskDef(): TaskDef = td
def tags(): Array[String] = Array.empty
def execute(handler: EventHandler, loggers: Array[Logger]): Array[Task] = {
handler.handle(new StreamingEvent(td, "first"))
Thread.sleep(1200L)
handler.handle(new StreamingEvent(td, "second"))
Array.empty
}
}
final class StreamingEvent(td: TaskDef, testName: String) extends Event {
def fullyQualifiedName(): String = td.fullyQualifiedName()
def fingerprint(): Fingerprint = td.fingerprint()
def selector(): Selector = new TestSelector(testName)
def status(): Status = Status.Success
def throwable(): OptionalThrowable = new OptionalThrowable()
def duration(): Long = 0L
}

View File

@ -0,0 +1,6 @@
> inproc/resetListener
> inproc/test
> inproc/checkStreaming
> forked/resetListener
> forked/test
> forked/checkStreaming

View File

@ -10,7 +10,6 @@ package sbt
package internal
package librarymanagement
import java.nio.charset.StandardCharsets.UTF_8
import java.nio.file.Files
import lmcoursier.definitions.{ Configuration, Project }
@ -66,7 +65,7 @@ object IvyXml {
val content0 = rawContent(currentProject, shadedConfigOpt, bomForcedDeps)
cacheIvyFile.getParentFile.mkdirs()
log.debug(s"writing Ivy file $cacheIvyFile")
Files.write(cacheIvyFile.toPath, content0.getBytes(UTF_8))
Files.writeString(cacheIvyFile.toPath, content0)
// Just writing an empty file here... Are these only used?
cacheIvyPropertiesFile.getParentFile.mkdirs()

View File

@ -135,7 +135,12 @@ private[sbt] final class TestRunner(
// Thread-safe collection so AsyncFunSuite (and other async frameworks) can call
// handle() from multiple threads without corrupting results (fixes #5245).
val results = new CopyOnWriteArrayList[Event]
val handler = new EventHandler { def handle(e: Event): Unit = { results.add(e) } }
val handler = new EventHandler {
def handle(e: Event): Unit = {
results.add(e)
safeListenersCall(_.testEvent(TestEvent(Seq(e))))
}
}
val loggers: Vector[ContentLogger] = listeners.flatMap(_.contentLogger(testDefinition))
def errorEvents(e: Throwable): Array[sbt.testing.Task] = {
val taskDef = testTask.taskDef
@ -148,6 +153,7 @@ private[sbt] final class TestRunner(
val duration = -1L
}
results.add(event)
safeListenersCall(_.testEvent(TestEvent(Seq(event))))
Array.empty
}
val nestedTasks =
@ -160,8 +166,6 @@ private[sbt] final class TestRunner(
loggers.foreach(_.flush())
}
val resultsList = results.asScala.toList
val event = TestEvent(resultsList)
safeListenersCall(_.testEvent(event))
(SuiteResult(resultsList), nestedTasks.toSeq)
}

View File

@ -12,7 +12,7 @@ import java.io.PrintStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.concurrent.*;
@ -140,6 +140,26 @@ public class ForkTestMain {
}
}
public static class ForkGroupStart implements Serializable {
public long id;
public String group;
public ForkGroupStart(long id, String group) {
this.id = id;
this.group = group;
}
}
public static class ForkGroupEnd implements Serializable {
public long id;
public String group;
public ForkGroupEnd(long id, String group) {
this.id = id;
this.group = group;
}
}
// -----------------------------------------------------------------------------
public static final class ForkError extends Exception {
@ -306,16 +326,38 @@ public class ForkTestMain {
};
}
private void writeEvents(final TaskDef taskDef, final ForkEvent[] events) {
private void writeGroupStart(final TaskDef taskDef) {
ForkGroupStart info = new ForkGroupStart(this.id, taskDef.fullyQualifiedName());
String params = this.gson.toJson(info, ForkGroupStart.class);
String notification =
String.format(
"{ \"jsonrpc\": \"2.0\", \"method\": \"startTestGroup\", \"params\": %s, \"re\": %d }",
params, this.id);
this.originalOut.println(notification);
this.originalOut.flush();
}
private void writeTestProgress(final TaskDef taskDef, final ForkEvent event) {
ForkEventsInfo info =
new ForkEventsInfo(
this.id,
taskDef.fullyQualifiedName(),
new ArrayList<ForkEvent>(Arrays.asList(events)));
new ArrayList<ForkEvent>(Collections.singletonList(event)));
String params = this.gson.toJson(info, ForkEventsInfo.class);
String notification =
String.format(
"{ \"jsonrpc\": \"2.0\", \"method\": \"testEvents\", \"params\": %s, \"re\": %d }",
"{ \"jsonrpc\": \"2.0\", \"method\": \"testProgress\", \"params\": %s, \"re\": %d }",
params, this.id);
this.originalOut.println(notification);
this.originalOut.flush();
}
private void writeGroupEnd(final TaskDef taskDef) {
ForkGroupEnd info = new ForkGroupEnd(this.id, taskDef.fullyQualifiedName());
String params = this.gson.toJson(info, ForkGroupEnd.class);
String notification =
String.format(
"{ \"jsonrpc\": \"2.0\", \"method\": \"endTestGroup\", \"params\": %s, \"re\": %d }",
params, this.id);
this.originalOut.println(notification);
this.originalOut.flush();
@ -424,41 +466,32 @@ public class ForkTestMain {
final ExecutorService executor, final Task task, final Logger[] loggers) {
return executor.submit(
() -> {
ForkEvent[] events;
Task[] nestedTasks;
final TaskDef taskDef = task.taskDef();
writeGroupStart(taskDef);
try {
final Collection<ForkEvent> eventList = new ConcurrentLinkedDeque<>();
final EventHandler handler =
new EventHandler() {
public void handle(final Event e) {
eventList.add(new ForkEvent(e));
writeTestProgress(taskDef, new ForkEvent(e));
}
};
logDebug(" Running " + taskDef);
nestedTasks = task.execute(handler, loggers);
if (nestedTasks.length > 0 || eventList.size() > 0)
logDebug(
" Produced "
+ nestedTasks.length
+ " nested tasks and "
+ eventList.size()
+ " events.");
events = eventList.toArray(new ForkEvent[eventList.size()]);
logDebug(" Produced " + nestedTasks.length + " nested tasks (events streamed).");
} catch (final Throwable t) {
nestedTasks = new Task[0];
events =
new ForkEvent[] {
testError(
taskDef,
"Uncaught exception when running "
+ taskDef.fullyQualifiedName()
+ ": "
+ t.toString(),
t)
};
writeTestProgress(
taskDef,
testError(
taskDef,
"Uncaught exception when running "
+ taskDef.fullyQualifiedName()
+ ": "
+ t.toString(),
t));
}
writeEvents(taskDef, events);
writeGroupEnd(taskDef);
return nestedTasks;
});
}