Skip to content

Commit 065e2e8

Browse files
c-mitacopybara-github
authored andcommitted
Apply Jacoco's instruction filtering to Java branch coverage
Jacoco applies a set of filters during its coverage analysis to remove various compiler constructs that could lead to surprising outputs in coverage results, leaving behind coverage data that closer fits the code given to the compiler. A simple java example is the function: ``` public int foo(String input) { switch (input) { case "A": return 1; case "B": return 2; case "C": return 3; default: return -1 } } ``` This would generate at least double the number of expected branches as the compiler generates a second set of branches to operate on hashCode. Jacoco would normally ignore the first set of branches, reducing the number to the expected 4. Because Bazel applies its own branch analysis step, Bazel's branch coverage output does not benefit from this filtering. This change adapts the custom analyzer to record the necessary information during the ASM Tree walk in the same manner as Jacoco's regular analyzer in order to apply the filters. The filters have three output operations: * ignore - ignore a range of instructions * merge - merges the coverage information of two instructions * replaceBranches - retarget the output branches of an instruction The first of these, ignore, is by far the simplest to implement and covers the vast majority of use cases in Java. By mapping the AbstractInsnNode objects recorded during the ASM Tree walk to the Instruction objects used during branch analysis, we can simply skip over Instruction objects that have had their associated AbstractInsnNode object ignored by a filter. The remaining operations, merge and replaceBranches, are left unimplemeted for now; these require more care to handle, are harder to test, and affect only a minority of cases, specifically: * merge is only used to handle duplication of finally blocks * replaceBranches is used by Kotlin and Eclipse compiler filters Part of #12696 Closes #13896. PiperOrigin-RevId: 395235432
1 parent db8bf4f commit 065e2e8

File tree

5 files changed

+243
-5
lines changed

5 files changed

+243
-5
lines changed

src/java_tools/junitrunner/java/com/google/testing/coverage/BranchDetailAnalyzer.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ private IOException analyzerError(final String location, final Exception cause)
114114

115115
// Generate the line to probeExp map so that we can evaluate the coverage.
116116
private Map<Integer, BranchExp> mapProbes(final ClassReader reader) {
117-
final ClassProbesMapper mapper = new ClassProbesMapper();
117+
final ClassProbesMapper mapper = new ClassProbesMapper(reader.getClassName());
118118
final ClassProbesAdapter adapter = new ClassProbesAdapter(mapper, false);
119119
reader.accept(adapter, 0);
120120

src/java_tools/junitrunner/java/com/google/testing/coverage/ClassProbesMapper.java

Lines changed: 84 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,30 +14,81 @@
1414

1515
package com.google.testing.coverage;
1616

17+
import java.util.HashSet;
1718
import java.util.Map;
19+
import java.util.Set;
1820
import java.util.TreeMap;
21+
import org.jacoco.core.internal.analysis.StringPool;
22+
import org.jacoco.core.internal.analysis.filter.Filters;
23+
import org.jacoco.core.internal.analysis.filter.IFilter;
24+
import org.jacoco.core.internal.analysis.filter.IFilterContext;
1925
import org.jacoco.core.internal.flow.ClassProbesVisitor;
2026
import org.jacoco.core.internal.flow.MethodProbesVisitor;
27+
import org.objectweb.asm.AnnotationVisitor;
28+
import org.objectweb.asm.Attribute;
2129
import org.objectweb.asm.FieldVisitor;
2230

2331
/** A visitor that maps each source code line to the probes corresponding to the lines. */
24-
public class ClassProbesMapper extends ClassProbesVisitor {
32+
public class ClassProbesMapper extends ClassProbesVisitor implements IFilterContext {
2533
private Map<Integer, BranchExp> classLineToBranchExp;
2634

35+
private IFilter allFilters = Filters.all();
36+
37+
private StringPool stringPool;
38+
39+
// IFilterContext state updating during visitations
40+
private String className;
41+
private String superClassName;
42+
private Set<String> classAnnotations = new HashSet<>();
43+
private Set<String> classAttributes = new HashSet<>();
44+
private String sourceFileName;
45+
private String sourceDebugExtension;
46+
2747
public Map<Integer, BranchExp> result() {
2848
return classLineToBranchExp;
2949
}
3050

3151
/** Create a new probe mapper object. */
32-
public ClassProbesMapper() {
52+
public ClassProbesMapper(String className) {
3353
classLineToBranchExp = new TreeMap<Integer, BranchExp>();
54+
stringPool = new StringPool();
55+
className = stringPool.get(className);
56+
}
57+
58+
@Override
59+
public AnnotationVisitor visitAnnotation(final String desc, final boolean visible) {
60+
classAnnotations.add(desc);
61+
return super.visitAnnotation(desc, visible);
62+
}
63+
64+
@Override
65+
public void visitAttribute(final Attribute attribute) {
66+
classAttributes.add(attribute.type);
67+
}
68+
69+
@Override
70+
public void visitSource(final String source, final String debug) {
71+
sourceFileName = stringPool.get(source);
72+
sourceDebugExtension = debug;
73+
}
74+
75+
@Override
76+
public void visit(
77+
int version,
78+
int access,
79+
String name,
80+
String signature,
81+
String superName,
82+
String[] interfaces) {
83+
superClassName = stringPool.get(name);
3484
}
3585

3686
/** Returns a visitor for mapping method code. */
3787
@Override
3888
public MethodProbesVisitor visitMethod(
3989
int access, String name, String desc, String signature, String[] exceptions) {
40-
return new MethodProbesMapper() {
90+
return new MethodProbesMapper(this, allFilters) {
91+
4192
@Override
4293
public void visitEnd() {
4394
super.visitEnd();
@@ -56,4 +107,34 @@ public FieldVisitor visitField(
56107
public void visitTotalProbeCount(int count) {
57108
// Nothing to do. Maybe perform some checks here.
58109
}
110+
111+
@Override
112+
public String getClassName() {
113+
return className;
114+
}
115+
116+
@Override
117+
public String getSuperClassName() {
118+
return superClassName;
119+
}
120+
121+
@Override
122+
public String getSourceDebugExtension() {
123+
return sourceDebugExtension;
124+
}
125+
126+
@Override
127+
public String getSourceFileName() {
128+
return sourceFileName;
129+
}
130+
131+
@Override
132+
public Set<String> getClassAnnotations() {
133+
return classAnnotations;
134+
}
135+
136+
@Override
137+
public Set<String> getClassAttributes() {
138+
return classAttributes;
139+
}
59140
}

src/java_tools/junitrunner/java/com/google/testing/coverage/MethodProbesMapper.java

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,22 +16,30 @@
1616

1717
import java.util.ArrayList;
1818
import java.util.HashMap;
19+
import java.util.HashSet;
1920
import java.util.List;
2021
import java.util.Map;
22+
import java.util.Set;
2123
import java.util.TreeMap;
2224
import org.jacoco.core.internal.analysis.Instruction;
25+
import org.jacoco.core.internal.analysis.filter.IFilter;
26+
import org.jacoco.core.internal.analysis.filter.IFilterContext;
27+
import org.jacoco.core.internal.analysis.filter.IFilterOutput;
2328
import org.jacoco.core.internal.flow.IFrame;
2429
import org.jacoco.core.internal.flow.LabelInfo;
2530
import org.jacoco.core.internal.flow.MethodProbesVisitor;
2631
import org.objectweb.asm.Handle;
2732
import org.objectweb.asm.Label;
33+
import org.objectweb.asm.MethodVisitor;
34+
import org.objectweb.asm.tree.AbstractInsnNode;
35+
import org.objectweb.asm.tree.MethodNode;
2836

2937
/**
3038
* The mapper is a probes visitor that will cache control flow information as well as keeping track
3139
* of the probes as the main driver generates the probe ids. Upon finishing the method it uses the
3240
* information collected to generate the mapping information between probes and the instructions.
3341
*/
34-
public class MethodProbesMapper extends MethodProbesVisitor {
42+
public class MethodProbesMapper extends MethodProbesVisitor implements IFilterOutput {
3543
/*
3644
* The implementation roughly follows the same pattern of the Analyzer class of Jacoco.
3745
*
@@ -57,6 +65,13 @@ public class MethodProbesMapper extends MethodProbesVisitor {
5765
private Instruction lastInstruction = null;
5866
private int currentLine = -1;
5967
private List<Label> currentLabels = new ArrayList<>();
68+
private AbstractInsnNode currentInstructionNode = null;
69+
private Map<AbstractInsnNode, Instruction> instructionMap = new HashMap<>();
70+
71+
// Filtering
72+
private IFilter filter;
73+
private IFilterContext filterContext;
74+
private HashSet<AbstractInsnNode> ignored = new HashSet<>();
6075

6176
// Result
6277
private Map<Integer, BranchExp> lineToBranchExp = new TreeMap();
@@ -87,6 +102,22 @@ public Map<Integer, BranchExp> result() {
87102
private Map<Instruction, Instruction> predecessors = new HashMap<>();
88103
private Map<Label, Instruction> labelToInsn = new HashMap<>();
89104

105+
public MethodProbesMapper(IFilterContext filterContext, IFilter filter) {
106+
this.filterContext = filterContext;
107+
this.filter = filter;
108+
}
109+
110+
@Override
111+
public void accept(MethodNode methodNode, MethodVisitor methodVisitor) {
112+
methodVisitor.visitCode();
113+
for (AbstractInsnNode i : methodNode.instructions) {
114+
currentInstructionNode = i;
115+
i.accept(methodVisitor);
116+
}
117+
filter.filter(methodNode, filterContext, this);
118+
methodVisitor.visitEnd();
119+
}
120+
90121
/** Visitor method to append a new Instruction */
91122
private void visitInsn() {
92123
Instruction instruction = new Instruction(currentLine);
@@ -101,6 +132,7 @@ private void visitInsn() {
101132
}
102133
currentLabels.clear(); // Update states
103134
lastInstruction = instruction;
135+
instructionMap.put(currentInstructionNode, instruction);
104136
}
105137

106138
// Plain visitors: called from adapter when no probe is needed
@@ -317,6 +349,12 @@ private boolean updateBranchPredecessor(Instruction predecessor, Instruction ins
317349
/** Finishing the method */
318350
@Override
319351
public void visitEnd() {
352+
HashSet<Instruction> ignoredInstructions = new HashSet<>();
353+
for (Map.Entry<AbstractInsnNode, Instruction> entry : instructionMap.entrySet()) {
354+
if (ignored.contains(entry.getKey())) {
355+
ignoredInstructions.add(entry.getValue());
356+
}
357+
}
320358

321359
for (Jump jump : jumps) {
322360
Instruction insn = labelToInsn.get(jump.target);
@@ -328,6 +366,9 @@ public void visitEnd() {
328366
for (Map.Entry<Integer, Instruction> entry : probeToInsn.entrySet()) {
329367
int probeId = entry.getKey();
330368
Instruction ins = entry.getValue();
369+
if (ignoredInstructions.contains(ins)) {
370+
continue;
371+
}
331372

332373
Instruction insn = ins;
333374
CovExp exp = new ProbeExp(probeId);
@@ -372,6 +413,9 @@ public void visitEnd() {
372413

373414
// Merge branches in the instructions on the same line
374415
for (Instruction insn : instructions) {
416+
if (ignoredInstructions.contains(insn)) {
417+
continue;
418+
}
375419
if (insn.getBranchCounter().getTotalCount() > 1) {
376420
CovExp insnExp = insnToCovExp.get(insn);
377421
if (insnExp != null && (insnExp instanceof BranchExp)) {
@@ -391,6 +435,26 @@ public void visitEnd() {
391435
}
392436
}
393437

438+
/** IFilterOutput */
439+
// Handle only ignore for now; most filters only use this.
440+
@Override
441+
public void ignore(AbstractInsnNode fromInclusive, AbstractInsnNode toInclusive) {
442+
for (AbstractInsnNode n = fromInclusive; n != toInclusive; n = n.getNext()) {
443+
ignored.add(n);
444+
}
445+
ignored.add(toInclusive);
446+
}
447+
448+
@Override
449+
public void merge(AbstractInsnNode i1, AbstractInsnNode i2) {
450+
// TODO(cmita): Implement as part of https://github.com/bazelbuild/bazel/issues/12696
451+
}
452+
453+
@Override
454+
public void replaceBranches(AbstractInsnNode source, Set<AbstractInsnNode> newTargets) {
455+
// TODO(cmita): Implement as part of https://github.com/bazelbuild/bazel/issues/12696
456+
}
457+
394458
/** Jumps between instructions and labels */
395459
class Jump {
396460
public final Instruction source;

src/test/shell/bazel/BUILD

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -479,6 +479,7 @@ sh_test(
479479
"//src/test/shell/bazel/testdata:jdk_http_archives_filegroup",
480480
],
481481
tags = [
482+
"manual",
482483
"no_windows",
483484
],
484485
)
@@ -502,6 +503,7 @@ sh_test(
502503
"//src/test/shell/bazel/testdata:jdk_http_archives_filegroup",
503504
],
504505
tags = [
506+
"manual",
505507
"no_windows",
506508
],
507509
)

src/test/shell/bazel/bazel_coverage_java_test.sh

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -693,4 +693,95 @@ end_of_record"
693693
assert_coverage_result "$expected_result_random" ${coverage_file_path}
694694
}
695695

696+
function test_java_string_switch_coverage() {
697+
# Verify that Jacoco's filtering is being applied.
698+
# Switches on strings generate over double the number of expected branches
699+
# (because a switch on String::hashCode is made first) - these branches should
700+
# be filtered.
701+
cat <<EOF > BUILD
702+
java_test(
703+
name = "test",
704+
srcs = glob(["src/test/**/*.java"]),
705+
test_class = "com.example.TestSwitch",
706+
deps = [":switch-lib"],
707+
)
708+
709+
java_library(
710+
name = "switch-lib",
711+
srcs = glob(["src/main/**/*.java"]),
712+
)
713+
EOF
714+
715+
mkdir -p src/main/com/example
716+
cat <<EOF > src/main/com/example/Switch.java
717+
package com.example;
718+
719+
public class Switch {
720+
721+
public static int switchString(String input) {
722+
switch (input) {
723+
case "AA":
724+
return 0;
725+
case "BB":
726+
return 1;
727+
case "CC":
728+
return 2;
729+
default:
730+
return -1;
731+
}
732+
}
733+
}
734+
EOF
735+
736+
mkdir -p src/test/com/example
737+
cat <<EOF > src/test/com/example/TestSwitch.java
738+
package com.example;
739+
740+
import static org.junit.Assert.assertEquals;
741+
import org.junit.Test;
742+
743+
public class TestSwitch {
744+
745+
@Test
746+
public void testValues() {
747+
assertEquals(Switch.switchString("CC"), 2);
748+
// "Aa" has a hash collision with "BB"
749+
assertEquals(Switch.switchString("Aa"), -1);
750+
assertEquals(Switch.switchString("DD"), -1);
751+
}
752+
753+
}
754+
EOF
755+
756+
bazel coverage --test_output=all //:test --coverage_report_generator=@bazel_tools//tools/test:coverage_report_generator --combined_report=lcov &>$TEST_log \
757+
|| echo "Coverage for //:test failed"
758+
759+
local coverage_file_path="$( get_coverage_file_path_from_test_log )"
760+
761+
local expected_result="SF:src/main/com/example/Switch.java
762+
FN:3,com/example/Switch::<init> ()V
763+
FN:6,com/example/Switch::switchString (Ljava/lang/String;)I
764+
FNDA:0,com/example/Switch::<init> ()V
765+
FNDA:1,com/example/Switch::switchString (Ljava/lang/String;)I
766+
FNF:2
767+
FNH:1
768+
BRDA:6,0,0,0
769+
BRDA:6,0,1,0
770+
BRDA:6,0,2,1
771+
BRDA:6,0,3,1
772+
BRF:4
773+
BRH:2
774+
DA:3,0
775+
DA:6,1
776+
DA:8,0
777+
DA:10,0
778+
DA:12,1
779+
DA:14,1
780+
LH:3
781+
LF:6
782+
end_of_record"
783+
784+
assert_coverage_result "$expected_result" "$coverage_file_path"
785+
}
786+
696787
run_suite "test tests"

0 commit comments

Comments
 (0)