// Used for jacocoBranchReport task. Cmd line param to specify branch origin. Defaults to master. def jacoco_origin = project.hasProperty('jacoco.origin') ? project.getProperty('jacoco.origin') : "master" import groovy.io.FileType; if (project.jacocoEnabled) { def String jacocoRootExecPath = "$buildDir/jacoco/jacocoMerge.exec" def numFoundExecutionFiles = 0 // number of jacoco data files found in subprojects delete new File(jacocoRootExecPath) // If the merged exec file (output from jacocoMerge) exists, // jacocoReport & jacocoBranchReport tasks are skipped and // the report is not generated. // So always delete the merged file before the determination // to skip a task is made. List excludesList = generateExcludesList() /********************************************************************************* * Task to merge multiple jacoco execution data files into one. *********************************************************************************/ task jacocoMerge(type: JacocoMerge) { description = 'Task to merge multiple jacoco execution data files into one.' destinationFile = new File(jacocoRootExecPath) // Make this collection of execution data files empty during the configuration phase. // There may be new exec files generated during the execution phase // (ex: gradle test jacocoReport). So gather up these files in the execution phase // via doFirst below. executionData = project.files([]) // Before Task runs, update executionData by searching for files in each subproject. doFirst { logger.debug("jacocoMerge: Searching in " + subprojects.size() + " subproject(s)") subprojects.each { p -> logger.debug("jacocoMerge: Searching $p.name subproject in directory: $p.buildDir/jacoco/") File jacocoExecDir = new File("$p.buildDir/jacoco/") if (jacocoExecDir.exists()) { jacocoExecDir.eachFileRecurse (FileType.FILES) { file -> numFoundExecutionFiles++ logger.debug("jacocoMerge: Adding $p.name: $file") executionData file } } } println "jacocoMerge: Added $numFoundExecutionFiles execution data files to $destinationFile" } } /********************************************************************************* * Task to create a jacoco report based on changes from current branch and origin. * Default origin is 'master'. Specify -Pjacoco.origin=value to change the value of origin. *********************************************************************************/ task jacocoBranchReport(type: JacocoReport, group: 'Coverage reports') { description = 'Generates a Jacoco report based on changes from current branch and origin.' dependsOn ":jacocoMerge" executionData new File(jacocoRootExecPath) // Get current branch name String[] cmd = ["/bin/bash", "-c", "git rev-parse --abbrev-ref HEAD"] ProcessBuilder builder = new ProcessBuilder(); builder.command(cmd); Process process = builder.start(); def branchName = process.in.text process.waitFor(); branchName = branchName.trim() logger.debug("jacocoBranchReport: Current branchName is $branchName") // Find commit in origin before branching. See: https://stackoverflow.com/q/1527234 cmd = ["/bin/bash", "-c", "diff -u <(git rev-list --first-parent $branchName) <(git rev-list --first-parent $jacoco_origin) | sed -ne 's/^ //p' | head -1"] builder = new ProcessBuilder(); builder.command(cmd); process = builder.start(); def lastRevision = process.in.text process.waitFor(); lastRevision = lastRevision.trim() logger.debug("jacocoBranchReport: last revision before branching from $jacoco_origin is $lastRevision") // Find the files that were changed in the branch. builder = new ProcessBuilder(); cmd = ["/bin/bash", "-c", "git diff --name-only $lastRevision"] builder.command(cmd); process = builder.start(); def filesChanged = process.in.text process.waitFor(); logger.debug("jacocoBranchReport: files changed are:" + filesChanged) List filesToInclude = new ArrayList() filesChanged.split().each{ fileName -> // Filter out files not in src/main/java and create an inclusion pattern. if(fileName.endsWith(".java") && fileName.contains("/src/main/java/")) { String fqName = fileName.split("/src/main/java/")[1] fqName = fqName.replace(".java", ".class") filesToInclude.add(fqName) } } sourceDirectories = files(subprojects.sourceSets.main.allSource.srcDirs) classDirectories = files(subprojects.sourceSets.main.output) logger.debug("jacocoBranchReport: Files to include: " + filesToInclude) // Only include these src/main/java files in the report if (filesToInclude.size() > 0) { classDirectories = files(classDirectories.files.collect { fileTree(dir: it, include: filesToInclude.toArray(new String[filesToInclude.size()])) }) } // Turn on html reports, 'doFirst' may disable this later on. reports { html.enabled = true xml.enabled = false } // Output info before execution. doFirst { println "jacocoBranchReport: Found $filesToInclude.size Java files to filter on branch '$branchName' and revision $lastRevision from origin '$jacoco_origin'" println "jacocoBranchReport: Number of jacoco execution data files found from jacocoMerge: $numFoundExecutionFiles" // Turn off reports if no files to report or no jacoco data files found. Otherwise the jacoco task will create empty report. if (filesToInclude.size() == 0 || numFoundExecutionFiles == 0) { reports { html.enabled = false xml.enabled = false } println "jacocoBranchReport: Empty filter or no jacoco execution data found. Not writing report." } else { println "jacocoBranchReport: Writing report to file://$reports.html.destination/index.html" } } } /********************************************************************************* * Task to generate an aggregate jacoco report from all subprojects *********************************************************************************/ task jacocoReport(type: JacocoReport, group: 'Coverage reports') { description = 'Generates an aggregate Jacoco report from all subprojects' dependsOn ":jacocoMerge" executionData new File(jacocoRootExecPath) sourceDirectories = files(subprojects.sourceSets.main.allSource.srcDirs) classDirectories = files(subprojects.sourceSets.main.output) classDirectories = files(classDirectories.files.collect { fileTree(dir: it, exclude: excludesList) }) reports { html.enabled = true xml.enabled = false html.destination = new File(project.ext.reportDir + "/jacocoReport") } doFirst { if (numFoundExecutionFiles == 0) { println "jacocoReport: No execution data files found." println "jacocoReport: No report written to $reports.html.destination.absolutePath." reports { html.enabled = false xml.enabled = false } } else { println "jacocoReport: Writing report to $reports.html.destination.absolutePath" } } } } /********************************************************************************* * Generate the Jacoco excludes list from file (this will strip out comments and * whitespace). * * This uses 'gradleScripts/jacoco.excludes.src.txt' to generate list of * class exclusions for the 'jacocoReport' task. * *********************************************************************************/ def String[] generateExcludesList() { File inputFile = new File(rootProject.projectDir, "gradleScripts/jacoco.excludes.src.txt") def lines = inputFile.readLines() .findAll({ line -> !shouldIgnoreLine(line) }) .collect() println "Returning ${lines.size()} exclusion line(s) for jacocoReport." return lines } /* An ignorable line is one that is only whitespace or that starts with a comment marker */ def shouldIgnoreLine(line) { if (line.startsWith('#')){ return true } if (line.startsWith("//")) { return true } if (line.trim().isEmpty()) { return true } return false }