import groovy.transform.Memoized import groovy.xml.XmlParser apply plugin: 'jacoco' jacoco { toolVersion = "0.8.12" } android { testCoverage { jacocoVersion '0.8.12' } } @Memoized Properties getLocalProperties() { final propertiesFile = project.rootProject.file("local.properties") final properties = new Properties() if (propertiesFile.exists()) { properties.load(propertiesFile.newDataInputStream()) } return properties } def openReport(htmlOutDir) { final reportPath = "$htmlOutDir/index.html" println "HTML Report: $reportPath" if (!project.hasProperty("open-report")) { println "to open the report automatically in your default browser add '-Popen-report' cli argument" return } def os = org.gradle.internal.os.OperatingSystem.current() if (os.isWindows()) { exec { commandLine 'cmd', '/c', "start $reportPath" } } else if (os.isMacOsX()) { exec { commandLine 'open', "$reportPath" } } else if (os.isLinux()) { try { exec { commandLine 'xdg-open', "$reportPath" } } catch (Exception ignored) { if (localProperties.containsKey("linux-html-cmd")) { exec { commandLine properties.get("linux-html-cmd"), "$reportPath" } } else { println "'linux-html-cmd' property could not be found in 'local.properties'" } } } } tasks.withType(Test).configureEach { jacoco.includeNoLocationClasses = true jacoco.excludes = ['jdk.internal.*'] } // source: https://medium.com/jamf-engineering/android-kotlin-code-coverage-with-jacoco-sonar-and-gradle-plugin-6-x-3933ed503a6e def fileFilter = [ // android '**/R.class', '**/R$*.class', '**/BuildConfig.*', '**/Manifest*.*', '**/*Test*.*', 'android/**/*.*', // kotlin '**/*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$*.*' ] def classDir = 'tmp/kotlin-classes/play' if (rootProject.testReleaseBuild) classDir += "Release" else classDir += "Debug" // Our merge report task def testReport = tasks.register('jacocoTestReport', JacocoReport) { def htmlOutDir = project.layout.buildDirectory.dir("reports/jacoco/$name/html").get().asFile doLast { openReport htmlOutDir } reports { xml.required = true html.destination htmlOutDir } def kotlinClasses = fileTree(dir: project.layout.buildDirectory.dir(classDir), excludes: fileFilter) def mainSrc = "$project.projectDir/src/main/java" sourceDirectories.from = files([mainSrc]) classDirectories.from = files([kotlinClasses]) executionData.from = fileTree(dir: project.layout.buildDirectory, includes: [ '**/*.exec', '**/*.ec' ]) } testReport.configure { dependsOn('testPlayDebugUnitTest') dependsOn(rootProject.androidTestName) } // A unit-test only report task def unitTestReport = tasks.register('jacocoUnitTestReport', JacocoReport) { def htmlOutDir = layout.buildDirectory.dir("reports/jacoco/$name/html").get().asFile doLast { openReport htmlOutDir } reports { xml.required = true html.destination htmlOutDir } def kotlinClasses = fileTree(dir: project.layout.buildDirectory.dir(classDir), excludes: fileFilter) def mainSrc = "$project.projectDir/src/main/java" sourceDirectories.from = files([mainSrc]) classDirectories.from = files([kotlinClasses]) executionData.from = fileTree(dir: project.layout.buildDirectory, includes: [ '**/*.exec' ]) } unitTestReport.configure { dependsOn('testPlayDebugUnitTest') } // A connected android tests only report task def androidTestReport = tasks.register('jacocoAndroidTestReport', JacocoReport) { def htmlOutDir = layout.buildDirectory.dir("reports/jacoco/$name/html").get().asFile doLast { openReport htmlOutDir } reports { xml.required = true html.destination htmlOutDir } def kotlinClasses = fileTree(dir: project.layout.buildDirectory.dir(classDir), excludes: fileFilter) def mainSrc = "$project.projectDir/src/main/java" sourceDirectories.from = files([mainSrc]) classDirectories.from = files([kotlinClasses]) executionData.from = fileTree(dir: project.layout.buildDirectory, includes: [ '**/*.ec' ]) } androidTestReport.configure { dependsOn(rootProject.androidTestName) } // Issue 16640 - some emulators run, but register zero coverage tasks.register('assertNonzeroAndroidTestCoverage') { doLast { File jacocoReport = layout.buildDirectory.dir("reports/jacoco/jacocoAndroidTestReport/jacocoAndroidTestReport.xml").get().asFile if (!jacocoReport.exists()) throw new FileNotFoundException("jacocoAndroidTestReport.xml was not found after running jacocoAndroidTestReport") XmlParser xmlParser = new XmlParser() xmlParser.setFeature("http://apache.org/xml/features/disallow-doctype-decl", false) xmlParser.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false) Node reportRoot = xmlParser.parse(jacocoReport) boolean hasCovered = false // https://github.com/jacoco/jacoco/blob/5aabb2eb60bbcd05df968005f1746ba19dcd5361/org.jacoco.report/src/org/jacoco/report/internal/xml/ReportElement.java#L190 for (Node child : reportRoot.children()) { if (child.name() != "counter") continue; if (child.attribute("covered") == "0") { logger.warn("jacoco registered zero code coverage for counter type " + child.attribute("type"), null) } else { hasCovered = true } } if (!hasCovered) throw new GradleScriptException("androidTest registered zero code coverage in jacocoAndroidTestReport.xml. Probably some incompatibilities in the toolchain.", null) } } afterEvaluate { tasks.named('jacocoAndroidTestReport').configure { finalizedBy('assertNonzeroAndroidTestCoverage') } tasks.named('jacocoTestReport').configure { finalizedBy('assertNonzeroAndroidTestCoverage') } }