How does JaCoCo augment the code to capture the code coverage
3 min readFeb 10, 2024
There is the following presentation about mechanics behind Java code coverage. If you want to have the rough idea how the code is instrumented, check the code samples in this article and it should be more clear.
Instrumentation
Let’s consider the following example we want to instrument, so we can capture the code coverage.
public class TestTarget implements Runnable {
private boolean isPrime(final int n) {
for (int i = 2; i * i <= n; i++) {
if ((n ^ i) == 0) {
return false;
}
}
return true;
}
@Override
public void run() {
isPrime(7);
}
}
The code above gets instrumented by JaCoCo. JaCoCo adds blocks of code that put true
value into a synthetic array called $jacocoData
(synthetic variable is variable that is added by the compiler).
public class TestTarget implements Runnable {
public TestTarget() {
boolean[] var2 = (boolean[])"$jacocoData";
super();
var2[0] = true;
}
private boolean isPrime(int n) {
boolean[] var3 = (boolean[])"$jacocoData";
int i = 2;
for(var3[1] = true; i * i <= n; var3[3] = true) {
if ((n ^ i) == 0) {
var3[2] = true;
return false;
}
++i;
}
var3[4] = true;
return true;
}
public void run() {
boolean[] var2 = (boolean[])"$jacocoData";
this.isPrime(7);
var2[5] = true;
}
}
Here is the runnable code example to experiment with:
import org.jacoco.core.analysis.Analyzer;
import org.jacoco.core.analysis.CoverageBuilder;
import org.jacoco.core.analysis.IClassCoverage;
import org.jacoco.core.analysis.ICounter;
import org.jacoco.core.data.ExecutionDataStore;
import org.jacoco.core.data.SessionInfoStore;
import org.jacoco.core.instr.Instrumenter;
import org.jacoco.core.internal.InputStreams;
import org.jacoco.core.runtime.IRuntime;
import org.jacoco.core.runtime.LoggerRuntime;
import org.jacoco.core.runtime.RuntimeData;
import java.io.*;
import java.util.HashMap;
import java.util.Map;
public class CoreTutorial {
public static class TestTarget implements Runnable {
private boolean isPrime(final int n) {
for (int i = 2; i * i <= n; i++) {
if ((n ^ i) == 0) {
return false;
}
}
return true;
}
@Override
public void run() {
isPrime(7);
}
}
public static class MemoryClassLoader extends ClassLoader {
private final Map<String, byte[]> definitions = new HashMap<>();
public void addDefinition(String name, byte[] bytes) {
definitions.put(name, bytes);
}
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
byte[] bytes = definitions.get(name);
if (bytes != null) {
return defineClass(name, bytes, 0, bytes.length);
}
return super.loadClass(name, resolve);
}
}
private final PrintStream out;
public CoreTutorial(final PrintStream out) {
this.out = out;
}
public void execute() throws Exception {
var targetName = TestTarget.class.getName();
var runtime = new LoggerRuntime();
var instr = new Instrumenter(runtime);
InputStream original = getTargetClass(targetName);
final byte[] instrumented = instr.instrument(original, targetName);
// just to see how it was instrumented
FileOutputStream outputStream = new FileOutputStream("Instrumented.class");
outputStream.write(instrumented);
outputStream.close();
original.close();
var data = new RuntimeData();
runtime.startup(data);
var memoryClassLoader = new MemoryClassLoader();
memoryClassLoader.addDefinition(targetName, instrumented);
var targetClass = memoryClassLoader.loadClass(targetName);
var targetInstance = (Runnable) targetClass.newInstance();
targetInstance.run();
var executionData = new ExecutionDataStore();
var sessionInfos = new SessionInfoStore();
data.collect(executionData, sessionInfos, false);
runtime.shutdown();
var coverageBuilder = new CoverageBuilder();
var analyzer = new Analyzer(executionData, coverageBuilder);
original = getTargetClass(targetName);
analyzer.analyzeClass(original, targetName);
original.close();
for (final IClassCoverage cc : coverageBuilder.getClasses()) {
out.printf("Coverage of class %s%n", cc.getName());
printCounter("instructions", cc.getInstructionCounter());
printCounter("branches", cc.getBranchCounter());
printCounter("lines", cc.getLineCounter());
printCounter("methods", cc.getMethodCounter());
printCounter("complexity", cc.getComplexityCounter());
for (int i = cc.getFirstLine(); i <= cc.getLastLine(); i++) {
out.printf("Line %s: %s%n", Integer.valueOf(i),
getColor(cc.getLine(i).getStatus()));
}
}
}
private InputStream getTargetClass(String name) {
var resource = '/' + name.replace('.', '/') + ".class";
out.println(resource);
return getClass().getResourceAsStream(resource);
}
private void printCounter(String unit, ICounter counter) {
var missed = Integer.valueOf(counter.getMissedCount());
var total = Integer.valueOf(counter.getTotalCount());
out.printf("%s of %s %s missed%n", missed, total, unit);
}
private String getColor(int status) {
return switch (status) {
case ICounter.NOT_COVERED -> "red";
case ICounter.PARTLY_COVERED -> "yellow";
case ICounter.FULLY_COVERED -> "green";
default -> "";
};
}
public static void main(String[] args) throws Exception {
new CoreTutorial(System.out).execute();
}
}
The code above is modified version of CoreTutorial from JaCoCo documenation.