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

Unwanted tasks added as dependencies to JaCoCo task

Open ebrowne72 opened this issue 2 years ago • 16 comments

In our project (using JUnit 4) we define a jacocoTestReport Gradle task:

task jacocoTestReport(type: JacocoReport, dependsOn: ["testInternalDebugUnitTest"]) {
    group = "Reporting"
    description = "Generate Jacoco coverage reports"
    reports {
        xml.enabled = true
        html.enabled = true
        xml.destination file("${buildDir}/reports/jacoco/test/jacocoTestReport.xml")
    }

    sourceDirectories.from = files([mainSrc])
    classDirectories.from = files([debugTree], [kotlinClasses])
    executionData.from = fileTree(dir: "$buildDir", includes: ["**/**/*.exec", "**/**/*.ec"])
}

And when using JUnit 4 and checking the dependency graph we see it is indeed dependent on testInternalDebugUnitTest:

:app:jacocoTestReport
\--- :app:testInternalDebugUnitTest ...

BTW, we have two flavors, Internal and Production.

When I add the JUnit 5 plugin, however, the task is now dependent on all flavors of jacocoTestReport, which are dependent on their respective flavor of unit test task:

:app:jacocoTestReport
+--- :app:jacocoTestReportInternalDebug
|    \--- :app:testInternalDebugUnitTest ...
+--- :app:jacocoTestReportInternalRelease
|    \--- :app:testInternalReleaseUnitTest ...
+--- :app:jacocoTestReportProductionDebug
|    \--- :app:testProductionDebugUnitTest ...
+--- :app:jacocoTestReportProductionRelease
|    \--- :app:testProductionReleaseUnitTest ...
\--- :app:testInternalDebugUnitTest *

Which results in our unit tests running once for each flavor combination, when we only want to run them for InternalDebug.

ebrowne72 avatar Apr 25 '22 22:04 ebrowne72

I tried adding the following configuration to my build file but it didn't help.

junitPlatform {
    jacoco {
        onlyGenerateTasksForVariants("internalDebug")
    }
}

ebrowne72 avatar Apr 27 '22 22:04 ebrowne72

Mind the name of the nested object for configuring the integration between JUnit 5 and Jacoco - it's called jacocoOptions, not jacoco:

junitPlatform {
  jacocoOptions {
    onlyGenerateTasksForVariants("internalDebug")
  }
}

Alternatively, you can turn off Jacoco integration entirely using the taskGenerationEnabled property.

junitPlatform {
  jacocoOptions {
    taskGenerationEnabled = false
  }
}

Will any of these work for you?

mannodermaus avatar Apr 30 '22 12:04 mannodermaus

Neither worked. jacocoTestReport is still depending on all the flavors.

ebrowne72 avatar Apr 30 '22 18:04 ebrowne72

I'll look into options to further restrict the involvement of the JUnit 5 plugin with existing custom Jacoco tasks. It's supposed to not interfere when taskGenerationEnabled = false, but there might be a bug in there. In the meantime, there are two workarounds I can see for your setup:

  1. Rename your custom task to something other than jacocoTestReport so that the plugin for JUnit 5 doesn't interfere with it
  2. If you only want to run the unit tests and Jacoco for the internalDebug variant, invoke jacocoTestReportInternalDebug in your CI or build pipeline, instead of using the catch-all task

Regardless of the choice, I'll check out the hiccup with the DSL.

mannodermaus avatar Jun 24 '22 15:06 mannodermaus

Hmm, I can't seem to reproduce the problem with a reference project of my own: Both onlyGenerateTasksForVariants and taskGenerationEnabled are working as expected. Note that I'm using the Gradle Kotlin DSL, but the following setup works for me:

// app/build.gradle.kts
plugins {
  id("com.android.application")
  id("jacoco")
  id("de.mannodermaus.android-junit5")
}

// ...

tasks.register<JacocoReport>("jacocoTestReport") {
    group = "Reporting"
    // ...
}

junitPlatform {
    // After syncing, there are no tasks from the JUnit 5 plugin and the custom Jacoco task's dependency chain is clean, too
    jacocoOptions.taskGenerationEnabled = false
}

I wonder if it has to do with the way the custom task is defined in your build script. Could you share a little more about where this task is configured? Also, could you try defining it as a lazy TaskProvider instead of eagerly, as per Gradle's newer recommendation?

// My Groovy is a little rusty, but this should be working
tasks.register("jacocoTestReport", JacocoReport) {
    dependsOn("testInternalDebugUnitTest")

    group = "Reporting"
    description = "Generate Jacoco coverage reports"
    reports {
        xml.enabled = true
        html.enabled = true
        xml.destination file("${buildDir}/reports/jacoco/test/jacocoTestReport.xml")
    }

    sourceDirectories.from = files([mainSrc])
    classDirectories.from = files([debugTree], [kotlinClasses])
    executionData.from = fileTree(dir: "$buildDir", includes: ["**/**/*.exec", "**/**/*.ec"])
}

junitPlatform {
    // After syncing, there are no tasks from the JUnit 5 plugin and the custom Jacoco task's dependency chain is clean, too
    jacocoOptions.taskGenerationEnabled = false
}

mannodermaus avatar Jun 24 '22 16:06 mannodermaus

Ohh damn, I have been just struggling with the same problem for weeks and found the culprit fortunately! BTW I have a common Gradle script that I apply in every submodule and I added the aforementioned configuration block in there and it did the trick!

android {
  if (project.plugins.hasPlugin(libs.plugins.junit5.get().pluginId)) {
      junitPlatform {
          jacocoOptions.taskGenerationEnabled = false
      }
   }
}

nuhkoca avatar Jun 25 '22 22:06 nuhkoca

  1. Rename your custom task to something other than jacocoTestReport so that the plugin for JUnit 5 doesn't interfere with it

It is not a good idea to rename the task for Android modules as pure Kotlin/Java modules are shipped with built-in jacocoTestReport task when the jacoco plugin is applied. Therefore, it is better to have the namesake task for Android modules, too.

Is there any side effects of the configuration below?

junitPlatform {
  jacocoOptions.taskGenerationEnabled = false
}

nuhkoca avatar Jun 25 '22 22:06 nuhkoca

I'm glad it turned out well for you in the end! Closing this ticket. 🙏

Is there any side effects of the configuration below?

The only side effect is that the variant-specific Jacoco tasks aren't generated automatically - other than that, nothing else comes to mind. That being said, this integration with Jacoco stems from a very old requirement, before there was native support for JUnit 5 in Gradle. Now that the first-party tasks run it well, it might be time to deprecate the integration slowly...

mannodermaus avatar Jun 26 '22 19:06 mannodermaus

OP here.

I tried defining the task lazily, but that didn't work.

I tried @nuhkoca 's suggestion:

Ohh damn, I have been just struggling with the same problem for weeks and found the culprit fortunately! BTW I have a common Gradle script that I apply in every submodule and I added the aforementioned configuration block in there and it did the trick!

android {
  if (project.plugins.hasPlugin(libs.plugins.junit5.get().pluginId)) {
      junitPlatform {
          jacocoOptions.taskGenerationEnabled = false
      }
   }
}

but I get the following error: groovy.lang.MissingPropertyException: No such property: junit5 for class: org.gradle.accessors.dm.LibrariesForLibs$PluginAccessors


We define the task in a file called code-coverage.gradle. Before the task definition we have the following configration:

jacoco {
    toolVersion = '0.8.7'
    reportsDir = file("${buildDir}/jacocoReports")
}

tasks.withType(Test) {
    jacoco.includeNoLocationClasses = true
    jacoco.excludes = ['jdk.internal.*']
}

In the top-level build script we have:

buildscript {
    ext.gradlePlugins = [
            ...
            codeCoverage : rootProject.file('gradle/plugins/code-coverage.gradle'),
            ...
    ]
}

And then in the module build file we have:

apply from: gradlePlugins.codeCoverage

Gradle version is 7.4.2.

ebrowne72 avatar Jul 01 '22 21:07 ebrowne72

@ebrowne72 Sorry I use the Version Catalogs. If you don't use it replace

libs.plugins.junit5.get().pluginId

with

de.mannodermaus.android-junit5

So

if (project.plugins.hasPlugin("de.mannodermaus.android-junit5")) {
    junitPlatform {
        jacocoOptions.taskGenerationEnabled = false
    }
}

You can create a deferred task like

tasks.register('jacocoTestReport', JacocoReport) {
      dependsOn testDebugUnitTest

      group = "Reporting"
      description = "Generates code coverage report for the test task."

      reports {
          html.required.set(true)
          xml.required.set(true)
      }

      ...
}

nuhkoca avatar Jul 01 '22 22:07 nuhkoca

Still not working for me.

ebrowne72 avatar Jul 01 '22 22:07 ebrowne72

Sorry to hear that these suggestions aren't working for your setup. As I'm assuming that your project is not publicly available, would it be possible for you to strip it down to just the relevant pieces of the build logic and share it here so that I have a reproducer? No Android source code needed, just the parts of each Gradle file involved in the setup would do.

mannodermaus avatar Jul 02 '22 11:07 mannodermaus

JaCoCoJUnitTest.zip

Command to show task tree: ./gradlew jacocoTestReport taskTree --depth 2

Command output:

:app:jacocoTestReport
+--- :app:jacocoTestReportInternalDebug
|    \--- :app:testInternalDebugUnitTest ...
+--- :app:jacocoTestReportInternalRelease
|    \--- :app:testInternalReleaseUnitTest ...
+--- :app:jacocoTestReportProductionDebug
|    \--- :app:testProductionDebugUnitTest ...
+--- :app:jacocoTestReportProductionRelease
|    \--- :app:testProductionReleaseUnitTest ...
\--- :app:testInternalDebugUnitTest *

ebrowne72 avatar Jul 08 '22 19:07 ebrowne72

@ebrowne72 I added this in your app/build.gradle

if (project.plugins.hasPlugin("de.mannodermaus.android-junit5")) {
    junitPlatform {
        jacocoOptions.taskGenerationEnabled = false
    }
}

and output

:app:jacocoTestReport
\--- :app:testInternalDebugUnitTest
     +--- :app:bundleInternalDebugClassesToRuntimeJar ...
     +--- :app:compileInternalDebugUnitTestJavaWithJavac ...
     +--- :app:compileInternalDebugUnitTestKotlin ...
     +--- :app:preInternalDebugUnitTestBuild ...
     +--- :app:processInternalDebugJavaRes ...
     +--- :app:processInternalDebugResources ...
     \--- :app:processInternalDebugUnitTestJavaRes ...

seems to be working. But as I mentioned, if you have a multi-module app, you can move this to a centralized place in order to not apply in each Gradle file individually and manually.

nuhkoca avatar Jul 10 '22 09:07 nuhkoca

I swear I tried that before and it didn't work, but now it does. Turning off task generation works for my case.

ebrowne72 avatar Jul 12 '22 00:07 ebrowne72

It's fine, that's what happens to all of us 🙂 Glad it worked for you, too! But I am still experiencing longer CI times after JUnit, is there any further improvements?

nuhkoca avatar Jul 12 '22 07:07 nuhkoca

Glad to hear that it worked out eventually, and thanks @nuhkoca for chiming in, much appreciated! As for questions related to JUnit 5 execution performance on CI, please raise potential questions on their repo directly 🙏

mannodermaus avatar Oct 22 '22 11:10 mannodermaus