Skip to content

Commit 20e8485

Browse files
feat: improvements to line level granularity in per-test coverage
1 parent 5ab378f commit 20e8485

File tree

7 files changed

+88
-23
lines changed

7 files changed

+88
-23
lines changed

dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/coverage/line/ExecutionDataAdapter.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ public String getClassName() {
1818
return className;
1919
}
2020

21+
boolean[] getProbeActivations() {
22+
return probeActivations;
23+
}
24+
2125
void record(int probeId) {
2226
probeActivations[probeId] = true;
2327
}

dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/coverage/line/LineCoverageStore.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import datadog.trace.civisibility.source.SourcePathResolver;
1414
import datadog.trace.civisibility.source.Utils;
1515
import java.io.InputStream;
16+
import java.lang.reflect.Field;
1617
import java.util.ArrayList;
1718
import java.util.BitSet;
1819
import java.util.Collection;
@@ -63,6 +64,10 @@ protected TestReport report(
6364
combinedNonCodeResources.addAll(probe.getNonCodeResources());
6465
}
6566

67+
// Copy per-test probe data back into JaCoCo's shared $jacocoData arrays so that
68+
// JaCoCo's aggregate coverage reports remain accurate.
69+
copyProbeDataToJacoco(combinedExecutionData);
70+
6671
if (combinedExecutionData.isEmpty() && combinedNonCodeResources.isEmpty()) {
6772
return null;
6873
}
@@ -132,6 +137,26 @@ protected TestReport report(
132137
return report;
133138
}
134139

140+
private static void copyProbeDataToJacoco(Map<Class<?>, ExecutionDataAdapter> executionData) {
141+
for (Map.Entry<Class<?>, ExecutionDataAdapter> e : executionData.entrySet()) {
142+
Class<?> clazz = e.getKey();
143+
boolean[] probeActivations = e.getValue().getProbeActivations();
144+
try {
145+
Field jacocoDataField = clazz.getDeclaredField("$jacocoData");
146+
jacocoDataField.setAccessible(true);
147+
boolean[] jacocoData = (boolean[]) jacocoDataField.get(null);
148+
if (jacocoData != null) {
149+
for (int i = 0; i < probeActivations.length && i < jacocoData.length; i++) {
150+
jacocoData[i] |= probeActivations[i];
151+
}
152+
}
153+
} catch (Exception ex) {
154+
// Class may not have $jacocoData field (e.g. not instrumented by JaCoCo agent),
155+
// or field may not be accessible — silently ignore
156+
}
157+
}
158+
}
159+
135160
public static final class Factory implements CoverageStore.Factory {
136161

137162
private final Map<String, Integer> probeCounts = new ConcurrentHashMap<>();

dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/coverage/line/LineProbes.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,22 @@ public void record(Class<?> clazz, long classId, int probeId) {
5858
}
5959
}
6060

61+
@Override
62+
public boolean[] resolveProbeArray(Class<?> clazz, long classId, int probeCount) {
63+
try {
64+
if (lastCoveredClass != clazz) {
65+
lastCoveredExecutionData =
66+
executionData.computeIfAbsent(
67+
lastCoveredClass = clazz,
68+
k -> new ExecutionDataAdapter(classId, k.getName(), probeCount));
69+
}
70+
return lastCoveredExecutionData.getProbeActivations();
71+
} catch (Exception e) {
72+
metrics.add(CiVisibilityCountMetric.CODE_COVERAGE_ERRORS, 1, CoverageErrorType.RECORD);
73+
return null;
74+
}
75+
}
76+
6177
@Override
6278
public void recordNonCodeResource(String absolutePath) {
6379
nonCodeResources.put(absolutePath, absolutePath);

dd-java-agent/instrumentation/jacoco-0.8.9/src/main/java/datadog/trace/instrumentation/jacoco/MethodVisitorWrapper.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ public class MethodVisitorWrapper {
1818
private static final MethodHandle visitInsnHandle;
1919
private static final MethodHandle visitIntInsnHandle;
2020
private static final MethodHandle visitLdcInsnHandle;
21+
private static final MethodHandle visitVarInsnHandle;
2122
private static final MethodHandle getTypeHandle;
2223

2324
static {
@@ -45,6 +46,8 @@ public class MethodVisitorWrapper {
4546
accessMethod(lookup, shadedMethodVisitorClass, "visitIntInsn", int.class, int.class);
4647
visitLdcInsnHandle =
4748
accessMethod(lookup, shadedMethodVisitorClass, "visitLdcInsn", Object.class);
49+
visitVarInsnHandle =
50+
accessMethod(lookup, shadedMethodVisitorClass, "visitVarInsn", int.class, int.class);
4851

4952
Class<?> shadedTypeClass = getJacocoClass(jacocoClassLoader, jacocoPackageName, ".asm.Type");
5053
getTypeHandle = accessMethod(lookup, shadedTypeClass, "getType", String.class);
@@ -119,6 +122,10 @@ public void push(int value) throws Throwable {
119122
}
120123
}
121124

125+
public void visitVarInsn(int opcode, int var) throws Throwable {
126+
visitVarInsnHandle.invoke(mv, opcode, var);
127+
}
128+
122129
public void pushClass(String className) throws Throwable {
123130
Object clazz = getTypeHandle.invoke('L' + className + ';');
124131
visitLdcInsnHandle.invoke(mv, clazz);

dd-java-agent/instrumentation/jacoco-0.8.9/src/main/java/datadog/trace/instrumentation/jacoco/ProbeInserterInstrumentation.java

Lines changed: 25 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,13 @@ private ElementMatcher<FieldDescription> methodVisitor() {
9393
declaresMethod(
9494
named("visitLdcInsn")
9595
.and(takesArguments(1))
96-
.and(takesArgument(0, Object.class))))));
96+
.and(takesArgument(0, Object.class))))
97+
.and(
98+
declaresMethod(
99+
named("visitVarInsn")
100+
.and(takesArguments(2))
101+
.and(takesArgument(0, int.class))
102+
.and(takesArgument(1, int.class))))));
97103
}
98104

99105
@Override
@@ -112,29 +118,17 @@ public ElementMatcher<TypeDescription> hierarchyMatcher() {
112118
@Override
113119
public void methodAdvice(MethodTransformer transformer) {
114120
transformer.applyAdvice(
115-
isMethod().and(named("visitMaxs")).and(takesArguments(2)).and(takesArgument(0, int.class)),
116-
getClass().getName() + "$VisitMaxsAdvice");
117-
transformer.applyAdvice(
118-
isMethod()
119-
.and(named("insertProbe"))
120-
.and(takesArguments(1))
121-
.and(takesArgument(0, int.class)),
122-
getClass().getName() + "$InsertProbeAdvice");
121+
isMethod().and(named("visitCode")).and(takesArguments(0)),
122+
getClass().getName() + "$VisitCodeAdvice");
123123
}
124124

125-
public static class VisitMaxsAdvice {
126-
@Advice.OnMethodEnter(suppress = Throwable.class)
127-
static void enter(@Advice.Argument(value = 0, readOnly = false) int maxStack) {
128-
maxStack = maxStack + 2;
129-
}
130-
}
131-
132-
public static class InsertProbeAdvice {
125+
public static class VisitCodeAdvice {
133126
@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
134127
static void exit(
135128
@Advice.FieldValue(value = "mv") final Object mv,
136129
@Advice.FieldValue(value = "arrayStrategy") final Object arrayStrategy,
137-
@Advice.Argument(0) final int id)
130+
@Advice.FieldValue(value = "variable") final int variable,
131+
@Advice.FieldValue(value = "accessorStackSize", readOnly = false) int accessorStackSize)
138132
throws Throwable {
139133
Field classNameField = arrayStrategy.getClass().getDeclaredField("className");
140134
classNameField.setAccessible(true);
@@ -163,20 +157,28 @@ static void exit(
163157

164158
Field classIdField = arrayStrategy.getClass().getDeclaredField("classId");
165159
classIdField.setAccessible(true);
166-
Long classId = classIdField.getLong(arrayStrategy);
160+
long classId = classIdField.getLong(arrayStrategy);
167161

168162
MethodVisitorWrapper methodVisitor = MethodVisitorWrapper.wrap(mv);
169163

164+
// ALOAD variable — load JaCoCo's shared boolean[] array
165+
methodVisitor.visitVarInsn(Opcodes.ALOAD, variable);
166+
// LDC ClassName.class — push Class reference
170167
methodVisitor.pushClass(className);
168+
// LDC classId — push long classId
171169
methodVisitor.visitLdcInsn(classId);
172-
methodVisitor.push(id);
173-
170+
// INVOKESTATIC resolveProbeArray(boolean[], Class, long) → boolean[]
174171
methodVisitor.visitMethodInsn(
175172
Opcodes.INVOKESTATIC,
176173
"datadog/trace/api/civisibility/coverage/CoveragePerTestBridge",
177-
"recordCoverage",
178-
"(Ljava/lang/Class;JI)V",
174+
"resolveProbeArray",
175+
"([ZLjava/lang/Class;J)[Z",
179176
false);
177+
// ASTORE variable — replace local variable with per-test array
178+
methodVisitor.visitVarInsn(Opcodes.ASTORE, variable);
179+
180+
// Ensure enough stack space for our bytecodes (1 ref + 1 ref + 1 long = 4 slots)
181+
accessorStackSize = Math.max(accessorStackSize, 4);
180182
}
181183
}
182184
}

internal-api/src/main/java/datadog/trace/api/civisibility/coverage/CoveragePerTestBridge.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,13 @@ private TotalProbeCount(String className, int count) {
6666
}
6767
}
6868

69+
/* This method is referenced by name in bytecode added in jacoco instrumentation module (see datadog.trace.instrumentation.jacoco.ProbeInserterInstrumentation.VisitCodeAdvice) */
70+
public static boolean[] resolveProbeArray(boolean[] jacocoArray, Class<?> clazz, long classId) {
71+
CoverageProbes probes = getCurrentCoverageProbes();
72+
boolean[] result = probes.resolveProbeArray(clazz, classId, jacocoArray.length);
73+
return result != null ? result : jacocoArray;
74+
}
75+
6976
/* This method is referenced by name in bytecode added in jacoco instrumentation module (see datadog.trace.instrumentation.jacoco.ProbeInserterInstrumentation.InsertProbeAdvice) */
7077
public static void recordCoverage(Class<?> clazz, long classId, int probeId) {
7178
getCurrentCoverageProbes().record(clazz, classId, probeId);

internal-api/src/main/java/datadog/trace/api/civisibility/coverage/CoverageProbes.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,8 @@ public interface CoverageProbes {
66
void record(Class<?> clazz, long classId, int probeId);
77

88
void recordNonCodeResource(String absolutePath);
9+
10+
default boolean[] resolveProbeArray(Class<?> clazz, long classId, int probeCount) {
11+
return null;
12+
}
913
}

0 commit comments

Comments
 (0)