Nicolò Andronio

Nicolò Andronio

Full-stack developer, computer scientist, engineer
Evil Genius in the spare time

Finding uncovered lines in a feature branch with git+Jacoco

At my current workplace, we use jacoco to compute code coverage, and we have a minimum coverage threshold to satisfy in order for our CI pipeline to pass. Sometimes the building process fails because code that has been introduced or removed causes some lines to be uncovered. Consulting jacoco reports is painful, because they are scattered across a huge codebase and the html interface offers poor search capabilities. On top of that, it is often hard to remember all the files that have changed on the current feature branch when compared to master or develop.

Thus I created a simple script to quickly help me find those lines and analyze them. Notice that this is just a small hack, rather than a fully functional program. Use it at your discretion to speed up developing and testing :)

As a premise, note that it’s possible to get a list of all files on the current branch that differ from a target branch with a compact git command:

git diff --name-status target-branch

Given that we know what files changed, it’s now sufficient to cross that information with the uncovered lines in those files, which can be retrieved directly from jacoco reports. The following script requires two paths in input:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
const fs = require('fs');
const path = require('path');

const GIT_FILE_DIFFS_PATH = process.argv[2];
const JACOCO_REPORTS_DIRECTORY = process.argv[3];

// Only includes source files in the analyzed file names.
// The git diff command also returns non-source files that have been changed,
// but we are not interested in them because they won't appear in the jacoco reports
const DIFF_REGEX = /\w\s+.*\/src\/main\/java\/([\w/]+)\/(\w+\.java)/i;
const parseDiffLine = (line) => {
const match = line.match(DIFF_REGEX);
return match
? { package: match[1].replace(/\//g, '.'), className: match[2] }
: null;
};

// Quick hack to get the uncovered lines from jacoco html reports.
// Reports can be generated in various formats but we still have to parse them
// in order to cross them over with git information, so it doesn't really matter.
// Besides, you may want to avoid fiddling your maven goals if that's just for
// your local development copy
const COVERAGE_HTML_HINT = '<span class="nc"';
const UNCOVERED_LINE_REGEX = /<span class="nc" id="L(\d+)">/i;
const parseReportLine = (line) => {
const match = line.match(UNCOVERED_LINE_REGEX);
return match ? Number(match[1]) : null;
};

// Obtain java classes and package names included in the diff
const nonFalsey = (arg) => !!arg;
const fileDiffs = fs.readFileSync(GIT_FILE_DIFFS_PATH, { encoding: 'utf-8' })
.split('\n')
.map(parseDiffLine)
.filter(nonFalsey);

for (const fileDiff of fileDiffs) {
const jacocoReportPath = path.resolve(JACOCO_REPORTS_DIRECTORY, fileDiff.package, `${fileDiff.className}.html`);

// Check whether the current java class has a jacoco report
if (!fs.existsSync(jacocoReportPath)) {
continue;
}

// If it does, extract the uncovered lines from the report.
// Improvement for the future: only returns those lines that were actually
// modified in the current java class
const jacocoReportContent = fs.readFileSync(jacocoReportPath, { encoding: 'utf-8' });
const uncoveredLines = jacocoReportContent.split('\n')
.filter(line => line.includes(COVERAGE_HTML_HINT))
.map(parseReportLine)
.filter(nonFalsey);

if (uncoveredLines.length > 0) {
console.log(`${fileDiff.package}.${fileDiff.className}:`);
console.log(' ', uncoveredLines.join(', '));
}
}

Finally, saving the script above in a file named retrieve-changed-uncovered-lines.js, we put everything together in a tiny bash script, to be executed in the project root (where you have your pom.xml):

1
2
3
4
5
6
7
8
9
FILE_DIFFS_PATH="./file-diffs.txt"
JACOCO_REPORTS_DIR="./target/site/jacoco/"

mvn package
mvn jacoco:report

git diff --name-status master > $FILE_DIFFS_PATH
node retrieve-changed-uncovered-lines.js $FILE_DIFFS_PATH $JACOCO_REPORTS_DIR
rm $FILE_DIFFS_PATH

I used the default jacoco reporting directory. Yours may be different. In that case, remember to change it in the script!