// filter of files that shouldn't be part of the report def androidFileFilter = [ //jdk 'jdk.internal.*', // data binding '**/databinding/*.class', '**/BR.*', // android '**/R.class', '**/R$*.class', '**/BuildConfig.*', '**/Manifest*.*', '**/*Test*.*', 'android/**/*.*', // kotlin '**/META-INF/*', '**/*MapperImpl*.*', '**/*$ViewInjector*.*', '**/*$ViewBinder*.*', '**/BuildConfig.*', '**/*Component*.*', '**/*BR*.*', '**/Manifest*.*', '**/*$Lambda$*.*', '**/*Companion*.*', '**/*Module*.*', '**/*Dagger*.*', '**/*Hilt*.*', '**/*MembersInjector*.*', '**/*_MembersInjector.class', '**/*_Factory*.*', '**/*_Provide*Factory*.*', '**/*Extensions*.*', // sealed and data classes '**/*$Result.*', '**/*$Result$*.*', // adapters generated by moshi '**/*JsonAdapter.*', // room '**/*_Impl.class', '**/*_Impl*.*', // hilt '**/hilt_aggregated_deps/*' ] / ** * Setup Jacoco for Android module. * - applies Plugin to the module. * - sets up the "debug" build type for jacoco * - creates tasks: * - if androidTest folder exists creates a jacocoAndroidTestReport alias for the jacoco coverageReport, otherwise an empty task. * - if test folder exists creates a jacocoUnitTestReport alias for the jacoco coverageReport, otherwise an empty task. * - creates jacocoTestReport which runs both alias tasks created before. * * Note: "jacocoTestReport" is the task name which is default for Java Modules. * / def setupAndroidJacoco(Project module, ArrayList fileFilter, String jacocoVersion) { configure(module) { apply plugin: "jacoco" module.android.testOptions.unitTests.all { jacoco.includeNoLocationClasses = true jacoco.excludes = fileFilter } android.buildTypes.debug.enableAndroidTestCoverage true android.buildTypes.debug.enableUnitTestCoverage true jacoco.toolVersion = "$jacocoVersion" def hasAndroidTests = new File("${module.projectDir}/src/androidTest").exists() def hasUnitTests = new File("${module.projectDir}/src/test").exists() if (hasUnitTests) { task jacocoUnitTestReport(dependsOn: ["createDebugUnitTestCoverageReport"]) { group = "verification" } } else { task jacocoUnitTestReport() { group = "verification" } } if (hasAndroidTests) { task jacocoAndroidTestReport(dependsOn: ["createDebugAndroidTestCoverageReport"]) { group = "verification" } } else { task jacocoAndroidTestReport() { group = "verification" } } task jacocoTestReport(dependsOn: ["jacocoUnitTestReport", "jacocoAndroidTestReport"]) { group = "verification" } } } / ** * Setup Jacoco for Java module. * - applies Plugin to the module. * - updates the `jacocoTestReport` task to ignore the `androidFileFilter` * - ensures tests run before `jacocoTestReport` * / def setupJavaJacoco(Project module, ArrayList fileFilter) { configure(module) { apply plugin: "jacoco" jacocoTestReport { dependsOn test // tests are required to run before generating the report afterEvaluate { classDirectories.setFrom(files(classDirectories.files.collect { fileTree(dir: it, exclude: fileFilter) })) } } } } / ** * Setup Jacoco for submodules based on their android or java module type */ subprojects { module -> plugins.withType(JavaPlugin).whenPluginAdded { setupJavaJacoco(module, androidFileFilter) } plugins.withId("com.android.application") { setupAndroidJacoco(module, androidFileFilter, "$jacoco_version") } plugins.withId("com.android.library") { setupAndroidJacoco(module, androidFileFilter, "$jacoco_version") } } / ** * Setup Aggregation tasks for Jacoco. * - jacocoRootReport: can be used to generate the report after submodules `jacocoTestReport` has been ran at least once. * - runTestAndJacocoRootReport: calls the `jacocoTestReport` of each submodule then calls `jacocoRootReport` for aggregation. * * Context, how the aggregated report works: * The jacoco tasks created by the plugin generate .ec and .exec Execution-Data files in specific locations. * - These Execution-Data files are all pulled into one `JacocoReport` task (`executionData.from`). * - All the source files from all the submodules are pulled into the same `JacocoReport` task (`sourceDirectories.from`) * - All the class files from all the submodules are pulled into the same `JacocoReport` task (`classDirectories.from`) * Then finally the report is configured to be generated at root `build\coverage-report` * / configure(rootProject) { apply plugin: "jacoco" def testTypeName = "debug" task runTestAndJacocoRootReport(type: JacocoReport, group: 'Coverage reports') { description = 'Run Tests and Generates report from all subprojects' // add all non empty subProjects `jacocoTestReport` task as a dependency. // note: these tasks are default Jacoco Task for Java and have been added above for Android modules. def codeProjects = subprojects.findAll({ it.subprojects.isEmpty() }) codeProjects.forEach { dependsOn += ["$it.path:jacocoTestReport"] } finalizedBy("jacocoRootReport") } task jacocoRootReport(type: JacocoReport, group: 'Coverage reports') { description = 'Generates report from all subprojects' def codeProjects = subprojects.findAll({ it.subprojects.isEmpty() }) sourceDirectories.from = files(codeProjects.collect { "${it.projectDir}/src/main/java" }) def classFileTrees = codeProjects.collect { def javaClassFilesInJavaModuleTree = fileTree( dir: "${it.buildDir}/classes/java/main", excludes: androidFileFilter ) def kotlinClassFilesInJavaModuleTree = fileTree( dir: "${it.buildDir}/classes/kotlin/main", excludes: androidFileFilter ) def javaClassFilesInAndroidModuleTree = fileTree( dir: "${it.buildDir}/intermediates/javac/${testTypeName}/classes", excludes: androidFileFilter ) def kotlinClassFilesInAndroidModuleTree = fileTree( dir: "${it.buildDir}/tmp/kotlin-classes/${testTypeName}", excludes: androidFileFilter ) files([javaClassFilesInJavaModuleTree, kotlinClassFilesInJavaModuleTree, javaClassFilesInAndroidModuleTree, kotlinClassFilesInAndroidModuleTree]) }.flatten() classDirectories.from = files(classFileTrees) def executionDataFiles = codeProjects.collect { def androidTestExecutionData = fileTree( dir: "${it.buildDir}/outputs/code_coverage/${testTypeName}AndroidTest/connected/", includes: ["**/*.ec", "**/*.exec"] ) def androidUnitTestExecutionData = fileTree( dir: "${it.buildDir}/outputs/unit_test_code_coverage/", includes: ["**/*.ec", "**/*.exec"] ) def javaUnitTestExecutionData = "${it.buildDir}/jacoco/test.exec" [androidTestExecutionData, androidUnitTestExecutionData, javaUnitTestExecutionData] }.flatten() executionData.from = files(executionDataFiles) reports { html { required = true destination file("${buildDir}/coverage-report") } } } }