simao
simao

Reputation: 15559

sbt using multiple classloaders

Sbt seems to be using different classloaders, making some tests failing when run more than once in an sbt session, with the following error:

[info]   java.lang.ClassCastException: net.i2p.crypto.eddsa.EdDSAPublicKey cannot be cast to net.i2p.crypto.eddsa.EdDSAPublicKey
[info]   at com.advancedtelematic.libtuf.crypt.EdcKeyPair$.generate(RsaKeyPair.scala:120)

I tried equivalent code using pattern matching instead of asInstanceOf and I get the same result.

How can I make sure sbt uses the same class loader for all test executions in the same session?

Upvotes: 1

Views: 754

Answers (1)

dk14
dk14

Reputation: 22374

I think it's related to this: Do security providers cause ClassLoader leaks in Java?. Basically Security is re-using providers from old class-loaders. So this could happen in any multi-classpath environment (like OSGi), not just SBT.

Fix for your build.sbt (without forking):

testOptions in Test += Tests.Cleanup(() => 
  java.security.Security.removeProvider("BC"))

Experiment:

sbt-classloader-issue$ sbt
> test
[success] Total time: 1 s, completed Jul 6, 2017 11:43:53 PM
> test
[success] Total time: 0 s, completed Jul 6, 2017 11:43:55 PM

Explanation:

As I can see from your code (published here):

Security.addProvider(new BouncyCastleProvider)

you're reusing the same BouncyCastleProvider provider every-time you run a test, as your Security.addProvider works only first time. As sbt creates new class-loader for every "test" run, but re-uses the same JVM - Security is kind-of JVM-scoped singleton as it was loaded by JVM-bootstrap, so classOf[java.security.Security].getClassLoader() == null and sbt cannot reload/reinitialize this class.

And you can easily check that

classOf[org.bouncycastle.jce.spec.ECParameterSpec].getClassLoader()
res30: ClassLoader = URLClassLoader with NativeCopyLoader with RawResources

org.bouncycastle classes are loaded with custom classloader (from sbt) which changes every-time you run test.

So this code:

val generator = KeyPairGenerator.getInstance("ECDSA", "BC")

gets instance of class loaded from old classloader (the one used for first "test" run) and you're trying to initialize it with spec from new classloader:

generator.initialize(ecSpec)

That's why you're getting "parameter object not a ECParameterSpec" exception. The reasoning around "net.i2p.crypto.eddsa.EdDSAPublicKey cannot be cast to net.i2p.crypto.eddsa.EdDSAPublicKey" is basically same.

Upvotes: 1

Related Questions