Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Kotlin JaCoCo - IllegalClassFormatException. Please supply original non-instrumented classes

I'm trying to get test coverage report of our Android application module and executing the testVariantBuildTypeUnitTest task.

Although all tests are passed, TEST-classNameTest.xml file contains error message below. This error is only given for methods contain calls Java from Kotlin. Is there any solution for this issue ?

**Caused by: java.lang.IllegalStateException: Cannot process instrumented class... Please supply original non-instrumented classes.
at org.jacoco.agent.rt.internal_f3994fa.core.internal.instr.InstrSupport.assertNotInstrumented(InstrSupport.java:238)
at org.jacoco.agent.rt.internal_f3994fa.core.internal.instr.ClassInstrumenter.visitField(ClassInstrumenter.java:56)
at org.jacoco.agent.rt.internal_f3994fa.asm.ClassVisitor.visitField(ClassVisitor.java:339)
at org.jacoco.agent.rt.internal_f3994fa.asm.ClassReader.readField(ClassReader.java:1111)
at org.jacoco.agent.rt.internal_f3994fa.asm.ClassReader.accept(ClassReader.java:713)
at org.jacoco.agent.rt.internal_f3994fa.asm.ClassReader.accept(ClassReader.java:401)
at org.jacoco.agent.rt.internal_f3994fa.core.instr.Instrumenter.instrument(Instrumenter.java:90)
at org.jacoco.agent.rt.internal_f3994fa.core.instr.Instrumenter.instrument(Instrumenter.java:108)

We have multimodule android project, because of this we're using this custom Jacoco task to get coverage report:

project.afterEvaluate {

    (android.hasProperty('applicationVariants')
            ? android.'applicationVariants'
            : android.'libraryVariants')
            .all { variant ->
                def variantName = variant.name
                def unitTestTask = "test${variantName.capitalize()}UnitTest"

                def jacocoReportName = unitTestTask + project.name

                tasks.create(name: jacocoReportName, type: JacocoReport, dependsOn: [
                        "$unitTestTask"
                ]) {
                    group = "Reporting"
                    description = "Generate Jacoco coverage reports for the ${variantName.capitalize()} build"

                    reports {
                        html.enabled = true
                        xml.enabled = true
                    }

                    def fileFilter = [
                            // data binding
                            'android/databinding/**/*.class',
                            '**/android/databinding/*Binding.class',
                            '**/android/databinding/*',
                            '**/androidx/databinding/*',
                            '**/databinding',
                            '**/BR.*',
                            // android
                            '**/R.class',
                            '**/R$*.class',
                            '**/BuildConfig.*',
                            '**/Manifest*.*',
                            '**/*Test*.*'
                    ]

                    def javaClasses = fileTree(dir: variant.javaCompileProvider.get().destinationDir,
                            excludes: fileFilter)
                    def kotlinClasses = fileTree(dir: "${buildDir}/tmp/kotlin-classes/${variantName}",
                            excludes: fileFilter)

                    classDirectories.setFrom(files([
                            javaClasses,
                            kotlinClasses
                    ]))

                    def variantSourceSets = variant.sourceSets.java.srcDirs.collect { it.path }.flatten()
                    sourceDirectories.setFrom(project.files(variantSourceSets))

                    if (isAndroidLibrary(project)) {
                        executionData(files([
                                "${projectDir}/jacoco.exec"
                        ]))
                    }else{
                        executionData(files([
                                "$project.buildDir/jacoco/${project.name}.exec"
                        ]))
                    }

                }

            }
}

Regards,

like image 461
tugceaktepe Avatar asked Apr 28 '21 11:04

tugceaktepe


1 Answers

Solution

The problem most likely lies in your module level build.gradle file. You need to remove testCoverageEnabled or set it to false.

android {
 ...
  buildTypes {
     release {
        minifyEnabled true
     }
     debug {
        // testCoverageEnabled true 
        minifyEnabled false
     }
  }

Explanation and References

  • Firstly I've been struggling with unit test coverage on android with JaCoCo for years and repeatedly encountered the issues multiple times as the AGP (Android Gradle Plugin) team enhanced the AGP (e.g. Similar issue) therefore this solution is subject to change in future releases of the AGP.

  • Come the release of AGP 4.1 the AGP team changed the way the plugin deals with unit tests. I raised an enter link description here issue with the AGP team and they explained that the plugin (AGP 4.1+) does it's own form of instrumentation to get the coverage which is undoubtedly incompatible with JaCoCo. It was in this issue where the solution was highlighted to me.

Note 1: This issue is not documented in the DSL documentation!

Note 2: It should also be noted that as of when I wrote this AGP 7 is in development where the same issue occurs.

Considerations

This solution will cause a problem generating coverage for instrumented tests as the AGP generates coverage for instrumented tests (in the form of a .ec file) meaning to get instrumented test coverage you must have testCoverageEnabled true. As a workaround you could:

  • Put your unit tests in the release build variant
  • Put your instrumented tests in the debug build variant. It's still possible to combine the coverage into one report: for example:
task jacocoCombinedUnitTestAndroidTestReport(type: JacocoReport) {
        group = "Reporting"

        reports {
            xml.enabled = true
            html.enabled = true
        }

        def mainSrc = "$project.projectDir/src/main/java"
        def releaseKotlinClasses = fileTree(dir: "${buildDir}/tmp/kotlin-classes/release", excludes:["com/example/ui**"])
        def debugKotlinClasses = fileTree(dir: "${buildDir}/tmp/kotlin-classes/debug/com/example/ui/")
        def releaseJavaClasses = fileTree(dir: "${buildDir}/intermediates/javac/release", excludes: ["com/example/ui**"])
        def debugJavaClasses = fileTree(dir: "${buildDir}/intermediates/javac/debug/com/example/ui/" )

        def mainClasses = releaseKotlinClasses + debugKotlinClasses
        def javaClasses = releaseJavaClasses + debugJavaClasses

        sourceDirectories.from = files([mainSrc])
        classDirectories.from = files([mainClasses, javaClasses])
        executionData.from = fileTree(dir: buildDir, include: ["jacoco/testReleaseUnitTest.exec",
                                                                    "outputs/code_coverage/debugAndroidTest/connected/**.ec"])
}
  • This assumes you have any UI code in a ui directory.
  • You include all release classes excluding the ui directory
  • You only include your ui debug classes.

Please note this is only valid as of when this answer was written and hopefully Google will fix the issue in the future.

like image 53
goldy1992 Avatar answered Oct 19 '22 11:10

goldy1992