android-junit5 icon indicating copy to clipboard operation
android-junit5 copied to clipboard

Minimum SDK < 24 and coreLibraryDesugaring causes NoSuchMethodError in ConcurrentHashMap

Open alexsullivan114 opened this issue 3 years ago • 12 comments

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.

alexsullivan114 avatar Oct 07 '20 19:10 alexsullivan114

I've reopened this issue primarily to ask if this is expected behavior. Is a minSdk >= 24 required for instrumentation tests?

alexsullivan114 avatar Oct 08 '20 13:10 alexsullivan114

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..?

mannodermaus avatar Oct 11 '20 06:10 mannodermaus

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?

alexsullivan114 avatar Oct 12 '20 13:10 alexsullivan114

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.

mannodermaus avatar Oct 17 '20 13:10 mannodermaus

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

mannodermaus avatar Aug 07 '21 14:08 mannodermaus

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)

shawnthye avatar Nov 11 '21 18:11 shawnthye

However setting Minimum SDK to API 26 also resolve the issue

shawnthye avatar Nov 11 '21 18:11 shawnthye

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.

mannodermaus avatar Nov 12 '21 06:11 mannodermaus

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.

yea @mannodermaus , seem like only for Library module

@alexsullivan114 error also written with some package name module.myModule.myAndroidTest

😄

shawnthye-zalora avatar Nov 14 '21 15:11 shawnthye-zalora

Any progress on this? I encountered the same issue, and switching from JUnit5 to JUnit4 fixed it, without any other changes.

mattrob33 avatar May 05 '22 15:05 mattrob33

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.

mannodermaus avatar May 05 '22 18:05 mannodermaus

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!

mannodermaus avatar Aug 17 '22 06:08 mannodermaus

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.

kronstein avatar Dec 28 '22 06:12 kronstein

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.

mannodermaus avatar Jan 04 '23 10:01 mannodermaus