[2.x] fix: Fixes stale resources in test (#9469)

**Problem**
sbt 2.x currently constructs the test classloader using symlinks
to the JARs in CAS (content-addressable storage).
This causes issues because JVM apparently caches opened JAR files by path,
and since the symlink itself doesn't change it could end up
serving stale resource files.

**Solution**
We can workaround this issue by resolving the symlinks to the real path.
This commit is contained in:
eugene yokota 2026-07-18 09:24:09 -04:00 committed by GitHub
parent b76ac09d44
commit 204468b84c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 49 additions and 6 deletions

View File

@ -9,7 +9,7 @@
package sbt
package internal
import java.io.File
import java.io.{ IOException, File }
import java.net.URL
import java.nio.file.Path
import sbt.ClassLoaderLayeringStrategy.*
@ -29,8 +29,22 @@ import xsbti.ArtifactInfo
import xsbti.HashedVirtualFileRef
private[sbt] object ClassLoaders {
/**
* https://github.com/sbt/sbt/issues/9468
* Resolves symlinks before converting to a URL. Classpath entries backed by a
* CAS cache would otherwise all share one URL across builds;
* the JVM's jar/URL connection caching is keyed by path, so once a jar: URL for that
* stable path has been opened, later reads can keep returning the first content seen\
* even after the symlink target (and the underlying file content) changes.
*/
private[sbt] def toRealURL(f: File): URL =
val real = try f.toPath.toRealPath().toFile
catch case _: IOException => f
real.toURI.toURL
extension (files: Seq[File]) {
def urls: Array[URL] = files.toArray.map(_.toURI.toURL)
def urls: Array[URL] = files.toArray.map(toRealURL)
}
private val interfaceLoader = classOf[sbt.testing.Framework].getClassLoader
/*

View File

@ -45,10 +45,10 @@ private[internal] final class ReverseLookupClassLoaderHolder(
val closeThis: Boolean,
val allowZombies: Boolean,
val logger: Logger
) extends URLClassLoader(Array.empty, null) {
) extends URLClassLoader(Array.empty, null):
private val cached: AtomicReference[ReverseLookupClassLoader] = new AtomicReference
private val closed = new AtomicBoolean(false)
private val urls = classpath.map(_.toURI.toURL).toArray
private val urls = classpath.map(ClassLoaders.toRealURL).toArray
/**
* Get a classloader. If there is a loader available in the cache, it will use that loader,
@ -69,7 +69,7 @@ private[internal] final class ReverseLookupClassLoaderHolder(
reverseLookupClassLoader.setup(tempDir)
new BottomClassLoader(
ReverseLookupClassLoaderHolder.this,
fullClasspath.map(_.toURI.toURL).toArray,
fullClasspath.map(ClassLoaders.toRealURL).toArray,
reverseLookupClassLoader,
tempDir,
closeThis,
@ -97,7 +97,7 @@ private[internal] final class ReverseLookupClassLoaderHolder(
case c => c.close()
}
}
}
end ReverseLookupClassLoaderHolder
/**
* This is more or less copied from the NativeCopyLoader in zinc. It differs from the zinc

View File

@ -6,3 +6,5 @@ libraryDependencies ++= Seq(
"commons-io" % "commons-io" % "2.5" % Runtime,
)
libraryDependencies += scalaVersion("org.scala-lang" % "scala-compiler" % _ ).value
Test / fork := true

View File

@ -0,0 +1,7 @@
val munit = "org.scalameta" %% "munit" % "1.0.4"
scalaVersion := "3.8.4"
lazy val root = rootProject
.settings(
libraryDependencies += munit % Test
)

View File

@ -0,0 +1 @@
bad

View File

@ -0,0 +1 @@
hello

View File

@ -0,0 +1,13 @@
package testpkg
import munit.*
import scala.io.Source
import scala.util.Using
class ATest extends FunSuite:
test("hello.txt should contain hello") {
Using.resource(Source.fromResource("hello.txt")): r =>
val actual = r.mkString.trim
assert(actual == "hello", s"actual: $actual")
}
end ATest

View File

@ -0,0 +1,5 @@
> test
$ copy-file changes/bad.txt src/test/resources/hello.txt
-> test