android-junit5
android-junit5 copied to clipboard
Minimum SDK < 24 and coreLibraryDesugaring causes NoSuchMethodError in ConcurrentHashMap
First off, I want to thank the maintainers of this repo for doing the herculean work of getting JUnit 5 up and running on Android!
I'm running into an issue where I can run my instrumentation tests just fine within Android Studio, but if I try to run them from the command line I get two sets of errors.
First off, these are the errors I'm seeing:
First error:
module.myModule.myAndroidTest > initializationError[emulator-5554 - 8.1.0] FAILED
java.lang.NoSuchMethodError: No static method newKeySet()Lj$/util/concurrent/ConcurrentHashMap$KeySetView; in class Lj$/util/concurrent/ConcurrentHashMap; or its super classes (declaration of 'j$.util.concurrent.ConcurrentHashMap' appears in /data/app/module.ins.test-TnAh7dSYSDIdYT5oUAsyiQ==/base.apk!classes6.dex)
at org.junit.platform.commons.logging.LoggerFactory.<clinit>(LoggerFactory.java:36)
Second error:
module.myModule.myPackage.myOtherAndroidTest > initializationError[emulator-5554 - 8.1.0] FAILED
java.lang.IllegalStateException: junit-platform-runner not found on runtime classpath of instrumentation tests; please review your androidTest dependencies or raise an issue.
at de.mannodermaus.junit5.AndroidJUnit5Builder.runnerForClass(RunnerBuilder.kt:72)
The second error is repeated for each class in my androidTest
folder.
Here's the build.gradle
for the module I'm attempting to test:
apply plugin: 'com.android.library'
apply plugin: "de.mannodermaus.android-junit5"
android {
compileSdkVersion ...
defaultConfig {
...
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
testInstrumentationRunnerArgument "runnerBuilder", "de.mannodermaus.junit5.AndroidJUnit5Builder"
}
...
compileOptions {
coreLibraryDesugaringEnabled=true
sourceCompatibility = "1.8"
targetCompatibility = "1.8"
}
packagingOptions {
exclude "META-INF/LICENSE*"
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
...
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.0.10'
testImplementation "junit:junit:${rootProject.ext.junitVer}"
testImplementation "org.junit.jupiter:junit-jupiter-api:5.6.2"
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:5.6.2"
testRuntimeOnly "org.junit.vintage:junit-vintage-engine:5.6.2"
androidTestImplementation "androidx.test:runner:1.3.0"
androidTestImplementation "org.junit.jupiter:junit-jupiter-api:5.6.2"
androidTestImplementation "de.mannodermaus.junit5:android-test-core:1.2.0"
androidTestRuntimeOnly "de.mannodermaus.junit5:android-test-runner:1.2.0"
androidTestImplementation "org.mockito:mockito-core:3.3.3"
testImplementation "org.mockito:mockito-core:3.3.3"
}
...
The errors I'm seeing are awfully close to the errors in this stack overflow post, but we're not using Kotlin so the proposed solution doesn't work for us (unless the issue is some library we're using is using Kotlin under the hood?)
Any help anyone could be provide would be much appreciated. Thank you!
EDIT: Tried to add Kotlin to the project so I could specify the jvmTarget
in the kotlinOptions
block and add a dependency on the jdk8 standard library as outlined in the linked StackOverflow post but alas no luck.
EDIT: It looks like updating the minSdk
to >= 24 fixes the issue.
I've reopened this issue primarily to ask if this is expected behavior. Is a minSdk
>= 24 required for instrumentation tests?
Thanks for bringing this up. The two test failure messages seem unrelated to each other, let's tackle them one by one.
The NoSuchMethodError
going into j$/util/concurrent/ConcurrentHashMap$KeySetView
makes me think that there's a problem with L8 and androidTest
sources. You seem to be using core library desugaring - I vaguely remember reading somewhere that the desugared types don't currently work in instrumentation tests. Since the JUnit 5 libs fundamentally build on top of Java 8, it's possible that L8's rewrite accidentally repackages these libraries, causing the failure at runtime.
As for the second error, it's weird that you'd need the explicit minSdk
here, since the runtime should automatically deactivate itself when running on an older device. (In fact, you'd techncally need API 26 to run JUnit 5 tests on device, 24 isn't enough.) I'm wondering if it's possible that the aforementioned L8 rewrite is at fault here after all. If it's not too much of a hassle, how does this reproduce in a project without coreLibraryDesugaringEnabled
? Also, I don't know if this Gradle configuration is actually a thing, but I'm wondering if we'd have to explicitly define something like androidTestCoreLibraryDesugaring
for the lib to be active for tests..?
Ahhh I tried it while removing the coreLibraryDesugaringEnabled
call and everything worked as expected. Interesting. I couldn't find any sort of androidTestCoreLibaryDesugaring
method to employ. The technical details of this are a bit out of my wheelhouse - @mannodermaus do you feel like this is a bug within this project, or something to raise with Google?
I'm assuming that L8's rewriting of core Java classes for maintaining Android compatibility clashes with the JUnit 5 stuff that uses the actual Java classes. By giving a higher minSdk
, you effectively turn off the desugaring for classes that can be used anyway. The desugaring tool would need to rewrite the JUnit's JAR code from java.util...
to j$.util
as well in order to fix this, I suppose. Let me do some research and discovery here.
What I'd like to know is: If you use minSdk < 24
here, will you encounter the above runtime error on any device, no matter if it's newer or older than API 24? Or will only the old devices crash with it?
Edit: Re-read the question just now. You're saying that your tests execute fine from within AS, but fail from command line. What's the Gradle task you use to run from command line? Maybe the task graph is different between the two, and Android Studio has some internal knowledge of running the desugaring whereas the command line does not.
After more experimentation, I am convinced that this behavior is a bug inside the Android Gradle Plugin and/or L8 desugaring stack. I have raised an issue on the Google bug tracker for it and will lock down this ticket until there is some feedback from the Android team.
https://issuetracker.google.com/issues/195786468
After more experimentation, I am convinced that this behavior is a bug inside the Android Gradle Plugin and/or L8 desugaring stack. I have raised an issue on the Google bug tracker for it and will lock down this ticket until there is some feedback from the Android team.
https://issuetracker.google.com/issues/195786468
Having the same issue on Arctic Fox, AGP 7.0.3
with Min SDK 21 on module,
worked with id("com.android.application")
failed with id("com.android.library")
?
included the log here, in case you need this
java.lang.NoSuchMethodError: No static method newKeySet()Lj$/util/concurrent/ConcurrentHashMap$KeySetView; in class Lj$/util/concurrent/ConcurrentHashMap; or its super classes (declaration of 'j$.util.concurrent.ConcurrentHashMap' appears in /data/app/~~iyR0exDW6dOtiOLN7wTyow==/feature.playground.deviant.test-b8BabFAg4DsvcymQy-0Q-g==/base.apk!classes5.dex)
at org.junit.platform.commons.logging.LoggerFactory.<clinit>(LoggerFactory.java:36)
at org.junit.platform.commons.logging.LoggerFactory.getLogger(LoggerFactory.java:47)
at org.junit.platform.launcher.core.ServiceLoaderRegistry.<clinit>(ServiceLoaderRegistry.java:27)
at org.junit.platform.launcher.core.LauncherFactory.<clinit>(LauncherFactory.java:66)
at org.junit.platform.launcher.core.LauncherFactory.create(LauncherFactory.java:109)
at de.mannodermaus.junit5.internal.runners.AndroidJUnit5.<init>(AndroidJUnit5.kt:32)
at de.mannodermaus.junit5.internal.runners.AndroidJUnit5.<init>(AndroidJUnit5.kt:27)
at de.mannodermaus.junit5.internal.runners.JUnit5RunnerFactory.createJUnit5Runner$runner_release(JUnit5RunnerFactory.kt:16)
at de.mannodermaus.junit5.AndroidJUnit5Builder.runnerForClass(AndroidJUnit5Builder.kt:71)
at org.junit.runners.model.RunnerBuilder.safeRunnerForClass(RunnerBuilder.java:70)
at androidx.test.internal.runner.AndroidRunnerBuilder.runnerForClass(AndroidRunnerBuilder.java:147)
at org.junit.runners.model.RunnerBuilder.safeRunnerForClass(RunnerBuilder.java:70)
at androidx.test.internal.runner.TestLoader.doCreateRunner(TestLoader.java:73)
at androidx.test.internal.runner.TestLoader.getRunnersFor(TestLoader.java:105)
at androidx.test.internal.runner.TestRequestBuilder.build(TestRequestBuilder.java:804)
at androidx.test.runner.AndroidJUnitRunner.buildRequest(AndroidJUnitRunner.java:613)
at androidx.test.runner.AndroidJUnitRunner.onStart(AndroidJUnitRunner.java:411)
at android.app.Instrumentation$InstrumentationThread.run(Instrumentation.java:2205)
However setting Minimum SDK to API 26 also resolve the issue
Thanks @shawnthye! This does reflect the behavior that I've seen when the minimum SDK is below 26. The desugaring part of the Android Gradle Plugin doesn't kick in for this method if minSdk >= 26
, since at that point the "real" API is available on devices, so no desugaring is required. Quite interesting that it worked for you when using the application
plugin. Maybe the L8 bug is specific to libraries then..?
Unfortunately I haven't heard back from the ticket on the Google issue tracker above, causing this to remain at stalemate for the time being.
Thanks @shawnthye! This does reflect the behavior that I've seen when the minimum SDK is below 26. The desugaring part of the Android Gradle Plugin doesn't kick in for this method if
minSdk >= 26
, since at that point the "real" API is available on devices, so no desugaring is required. Quite interesting that it worked for you when using theapplication
plugin. Maybe the L8 bug is specific to libraries then..?Unfortunately I haven't heard back from the ticket on the Google issue tracker above, causing this to remain at stalemate for the time being.
yea @mannodermaus , seem like only for Library module
@alexsullivan114 error also written with some package name module.myModule.myAndroidTest
😄
Any progress on this? I encountered the same issue, and switching from JUnit5 to JUnit4 fixed it, without any other changes.
Nothing to share, sorry. Feel free to star the ticket on the Google issue tracker (linked above) to give some visibility to it. It's not in the realm of possibility for the plugin to address this problem, unfortunately.
I found this line in the release notes of the desugaring library 1.2.0:
Support for all methods on java.util.concurrent.ConcurrentHashMap.
Sounds promising. Note to self to check out this particular issue against version 1.2.0!
JLYK, v1.2.0 fixes the issue.
Though, if after upgrade and running your tests you see another error: java.lang.ClassCastException: j$.util.stream.ReferencePipeline$Head cannot be cast to java.util.stream.Stream
, then you need desugar lib v2.0.0 and AGP v7.4.0-rc03 (which comes with Android Studio 2022.1.1 Electric Eel). See: https://issuetracker.google.com/issues/243636261.
Thanks for letting me know and the additional pointer, @kronstein! Kind of unfortunate that there doesn't seem to be a good way to backport that second fix to the desugar 1.x
line, but at least it works on 2.x.