mirror of https://github.com/sbt/sbt.git
[2.x] fix: Fixes stale resources in test
**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:
parent
df6c6c454e
commit
31d4e111fa
|
|
@ -9,7 +9,7 @@
|
||||||
package sbt
|
package sbt
|
||||||
package internal
|
package internal
|
||||||
|
|
||||||
import java.io.File
|
import java.io.{ IOException, File }
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import sbt.ClassLoaderLayeringStrategy.*
|
import sbt.ClassLoaderLayeringStrategy.*
|
||||||
|
|
@ -29,8 +29,22 @@ import xsbti.ArtifactInfo
|
||||||
import xsbti.HashedVirtualFileRef
|
import xsbti.HashedVirtualFileRef
|
||||||
|
|
||||||
private[sbt] object ClassLoaders {
|
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]) {
|
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
|
private val interfaceLoader = classOf[sbt.testing.Framework].getClassLoader
|
||||||
/*
|
/*
|
||||||
|
|
|
||||||
|
|
@ -45,10 +45,10 @@ private[internal] final class ReverseLookupClassLoaderHolder(
|
||||||
val closeThis: Boolean,
|
val closeThis: Boolean,
|
||||||
val allowZombies: Boolean,
|
val allowZombies: Boolean,
|
||||||
val logger: Logger
|
val logger: Logger
|
||||||
) extends URLClassLoader(Array.empty, null) {
|
) extends URLClassLoader(Array.empty, null):
|
||||||
private val cached: AtomicReference[ReverseLookupClassLoader] = new AtomicReference
|
private val cached: AtomicReference[ReverseLookupClassLoader] = new AtomicReference
|
||||||
private val closed = new AtomicBoolean(false)
|
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,
|
* 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)
|
reverseLookupClassLoader.setup(tempDir)
|
||||||
new BottomClassLoader(
|
new BottomClassLoader(
|
||||||
ReverseLookupClassLoaderHolder.this,
|
ReverseLookupClassLoaderHolder.this,
|
||||||
fullClasspath.map(_.toURI.toURL).toArray,
|
fullClasspath.map(ClassLoaders.toRealURL).toArray,
|
||||||
reverseLookupClassLoader,
|
reverseLookupClassLoader,
|
||||||
tempDir,
|
tempDir,
|
||||||
closeThis,
|
closeThis,
|
||||||
|
|
@ -97,7 +97,7 @@ private[internal] final class ReverseLookupClassLoaderHolder(
|
||||||
case c => c.close()
|
case c => c.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
end ReverseLookupClassLoaderHolder
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is more or less copied from the NativeCopyLoader in zinc. It differs from the zinc
|
* This is more or less copied from the NativeCopyLoader in zinc. It differs from the zinc
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
val munit = "org.scalameta" %% "munit" % "1.0.4"
|
||||||
|
scalaVersion := "3.8.4"
|
||||||
|
|
||||||
|
lazy val root = rootProject
|
||||||
|
.settings(
|
||||||
|
libraryDependencies += munit % Test
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
bad
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
hello
|
||||||
|
|
@ -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
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
> test
|
||||||
|
|
||||||
|
$ copy-file changes/bad.txt src/test/resources/hello.txt
|
||||||
|
|
||||||
|
-> test
|
||||||
Loading…
Reference in New Issue