Skip to content

Commit 87a449c

Browse files
praagyajainclaude
andauthored
Feat/dynamic dedup tcp (#184)
* feat(dedup): TCP client transport for k8s (CoverageTcpClient) In k8s the dedup coverage collector runs in the k8s-proxy Deployment pod while the app JVM is in a separate pod, so the shared-/tmp unix sockets can't reach it. Add a TCP transport selected by KEPLOY_COVERAGE_ENDPOINT (host:port): the SDK dials the collector and keeps one bidirectional connection for the whole replay. Wire protocol (newline-delimited), matching the k8s-proxy collector: collector -> SDK : "START <id>" | "END <id>" SDK -> collector : "ACK" (after START reset) "COV <compact-json>" + "ACK" (after END dump) - CoverageTcpClient mirrors CommandServer's dispatch but inverts roles (SDK is the client); reuses CoverageCollector reset/capture unchanged. COV precedes ACK so the collector records the payload before the ACK releases the caller. - Connect loop retries (collector listens only once replay begins). No read timeout on the long-lived idle-between-tests connection. - Unix transport stays the default for local/docker (unchanged); the worker field is generalized to Closeable so stop() is transport-agnostic. Validated end-to-end: real JVM (this SDK as -javaagent, TCP client) against the k8s-proxy DedupCoverage TCP server — coverage flows correctly; the score>=90 pair dedupes (identical line sets), the score=40 case differs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(dedup): warm up app classes before first test for stable coverage A fresh JVM runs each class's static initializer (<clinit>) on first use. JaCoCo charged those one-time lines to whichever test ran first, so a test could look different from its true duplicates by luck of timing -> the duplicate set flip-flopped run to run (e.g. 2 vs 4). On the first START command (app fully started), eagerly Class.forName(..., initialize=true) every indexed application class so all <clinit> lines run once, then the normal reset clears them. Every test window then captures only the lines its own request executes -> deterministic dedup. Best-effort per class; disable via KEPLOY_JAVA_DEDUP_WARMUP_DISABLED=true. Go is unaffected (AOT, init() runs once at startup before the first reset). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(dedup): branch coverage via per-class JaCoCo probe fingerprints Fingerprint each test by the set of executed JaCoCo probes per class ({className -> [probeIdx]}) instead of executed source lines. Each branch is instrumented as a distinct probe, so the probe set distinguishes which branch a test took (true vs false) on a shared line — line status and even branch counts report identically for both paths. The probe set subsumes line coverage and uses canonical VM class-name keys (no path normalization). Only capture() changes; the wire payload, collector, store, and enterprise compute stay generic over map[string][]int. Removed the now-dead line-decode helpers (Analyzer/CoverageBuilder, executedLines, resolveSourcePath). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(dedup): address PR review + fix smoke test - TCP END always emits COV before ACK (even when empty), mirroring the unix transport, so the line-oriented collector reads exactly one COV per END and cannot desync (Copilot comment). - Reconnect failures log at INFO only on the first failure, then FINE, to avoid ~1/s log spam in k8s; reset on a successful connect (Copilot comment). - smoke-javaagent.sh asserts the VM class-name key ("smoke/Work") instead of the old source-file key ("Work.java"), matching the probe-based fingerprint contract; fixes the JDK 8/17/21 smoke jobs. * feat(dedup): CoverageReporter — line/branch coverage from probe-union + bytecode Option B for whole-test-set coverage: a pure Java tool that reconstructs ExecutionData from a fired-probe union (the dedupFingerprints we already persist) plus a build-constant {classId, probeCount} manifest, runs JaCoCo Analyzer over the app bytecode, and sums line/branch/instr/method counters over all classes. Reuses the fingerprints verbatim as input; no JaCoCo internal APIs (id + probeCount come from the SDK's live ExecutionData). CoverageReporterSelfTest cross-checks the reconstruct-from-union path against JaCoCo's direct .exec analysis — exact match on the sample app (lines 78/105, branches 22/42, instr 452/525). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: praagyajain <praagyajain2002@gmail.com> * feat(dedup): SDK exports bytecode+manifest once per build for coverage (Option B) After warmup (all app classes loaded), the SDK ships — best-effort, async, once-per-JVM — a zip of the indexed class bytecode plus a manifest {vmClassName -> {classId(hex), probeCount}} to k8s-proxy via HTTP multipart (KEPLOY_BYTECODE_UPLOAD_URL, tagged with KEPLOY_BUILD_TAG). classId/probeCount come straight from the live ExecutionData, so the offline CoverageReporter needs no JaCoCo internal APIs. HEAD exists-check uploads at most once per build tag. Gated by the two envs (webhook-injected); absent => no-op, so dedup-only deployments are unchanged. Fully non-fatal: never throws into the app or the per-test dedup path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: praagyajain <praagyajain2002@gmail.com> * feat(dedup): tolerate proxy self-signed cert on bytecode upload The bytecode/manifest upload targets k8s-proxy's in-cluster HTTPS control port (:8080), whose cert is self-signed and may not chain to the app JVM's truststore. Since the upload is a best-effort, cluster-internal data-plane call (like the raw-TCP coverage collector), install a trust-all SSLSocketFactory for that HTTPS call only. No effect on plain-HTTP endpoints. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: praagyajain <praagyajain2002@gmail.com> * feat(dedup): send bytecode manifest+zip over the collector channel Build a live manifest ({vmClassName -> {classId, probeCount}}) from the proven per-test capture() path and, when it grows, send the app class bytecode as a base64 "CLASSES ..." frame on the existing collector TCP connection (:36340) right after the COV frame. This survives the replay agent's outbound mock interception, which 502'd the earlier HTTPS upload. k8s-proxy uses this bytecode + the unioned fingerprints to compute whole-test-set coverage offline. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(dedup): address PR review — COV-on-exception, payload-name clarity, doc limits Resolves the still-open review findings on the current branch (several earlier ones — dead HTTP/trust-all TLS, reconnect log level, smoke-test assertion, uploadInFlight — were already fixed by prior commits on this branch): - COV-on-exception (MED): the TCP END path only emitted COV on success, so a capture() failure jumped to the catch and wrote ACK with no COV, desyncing the line-oriented collector onto the next test's frames. Restructured so exactly one COV (empty on failure) always precedes the ACK; a CLASSES-frame failure is isolated and never skips the COV/ACK. - Payload naming (HIGH): documented that DedupPayload.executedLinesByFile now carries probes-by-class ({vmClassName -> [probeIdx]}) under a HISTORICAL wire key kept for consumer compat (enterprise #2188, k8s-proxy #574), and renamed the local capture/publish variables to executedProbesByClass to match. - CoverageReporter (MED): documented the load-time-transform undercount (live CRC64 class id vs on-disk bytes) — dedup unaffected, only reported numbers. - Endpoint parsing (LOW): noted the IPv4/DNS host:port assumption of lastIndexOf(':'). Not locally compiled (no JVM/Maven in this environment) — validated by CI. Deferred (cross-repo / behavioral): send class-zip once vs on manifest growth (needs k8s-proxy #574 handleClassesFrame to agree), warmup opt-in default, JUnit fixture tests, and lazy/streamed class-bytes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(dedup): send class zip once per connection, manifest-only after The coverage bytecode path re-sent the full (constant) class zip on every manifest growth as one base64 CLASSES line. On a large app the cumulative class bytecode base64 exceeds k8s-proxy's per-line scanner cap, ending the collector stream mid-replay with no error — silently corrupting coverage AND the remaining tests' dedup. The indexed class set never changes (one-time classpath scan), so: - send the full CLASSES frame (tag+manifest+zip) ONCE per connection, then emit lightweight MANIFEST frames (tag+manifest, no zip) as the manifest grows; - reset the sent-once flag on every (re)connect so a fresh collector connection re-gets the zip (MinIO first-write-wins makes the re-store idempotent), preserving the reconnect recovery the old re-send-everything path gave for free. Removes the O(n^2) resend and keeps any single line small once the zip is out. Pairs with the k8s-proxy MANIFEST-frame handler. e2e-validated: zip sent once + 4 manifest-only updates per cycle, dedup 66->17 and coverage 81/105 unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(dedup): add wire-format unit tests; extract frame-encoding helper Addresses the review's "no automated tests for the new capture logic" item. Adds DedupWireFormatTest (JUnit) exercising the REAL serialize path: - COV payload via GSON — asserts the id + the historical `executedLinesByFile` key now carrying {vmClassName -> [probeIdx]}, plus the empty-coverage shape; - CLASSES frame — "CLASSES <b64tag> <b64manifest> <b64zip>", 3 whitespace-split base64 fields, decoded exactly as k8s-proxy's handleClassesFrame does; - MANIFEST frame — "MANIFEST <b64tag> <b64manifest>", 2 fields (no zip), as handleManifestFrame parses it; - both frames are single-line (a base64 field must not embed a newline). To test the serialize + frame contract without reaching into the private collector, extracts two package-private static helpers on KeployDedupAgent — toDedupJson and encodeBytecodeFrame — and routes pollBytecodeFrame through the latter (byte-identical frames; no behavior change). Adds junit 4.13.2 (test scope). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Signed-off-by: praagyajain <praagyajain2002@gmail.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent c1b96c1 commit 87a449c

7 files changed

Lines changed: 969 additions & 115 deletions

File tree

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ Keploy Enterprise drives dynamic dedup per testcase.
1616
2. The Java agent resets JaCoCo coverage counters for that testcase.
1717
3. Enterprise replays the testcase.
1818
4. Enterprise sends `END <test-set>/<test-id>` on `/tmp/coverage_control.sock`.
19-
5. The Java agent dumps JaCoCo execution data, resolves executed Java lines, and sends them as JSON on `/tmp/coverage_data.sock`.
19+
5. The Java agent dumps JaCoCo execution data and sends the executed probe indices per class (`{className -> [probeIdx]}`) as JSON on `/tmp/coverage_data.sock`. Probes capture branch-level coverage (which branch a test took), so they distinguish tests that run the same lines but take different branches.
2020
6. Enterprise writes the result to `dedupData.yaml` and uses it to identify duplicates.
2121

2222
Coverage is collected at per-testcase granularity, not process granularity.

‎keploy-sdk/pom.xml‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,12 @@
7474
<artifactId>junixsocket-native-common</artifactId>
7575
<version>2.10.1</version>
7676
</dependency>
77+
<dependency>
78+
<groupId>junit</groupId>
79+
<artifactId>junit</artifactId>
80+
<version>4.13.2</version>
81+
<scope>test</scope>
82+
</dependency>
7783
</dependencies>
7884

7985
<build>
Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
package io.keploy.dedup;
2+
3+
import com.google.gson.Gson;
4+
import com.google.gson.reflect.TypeToken;
5+
import org.jacoco.core.analysis.Analyzer;
6+
import org.jacoco.core.analysis.CoverageBuilder;
7+
import org.jacoco.core.analysis.IClassCoverage;
8+
import org.jacoco.core.analysis.ICounter;
9+
import org.jacoco.core.data.ExecutionData;
10+
import org.jacoco.core.data.ExecutionDataStore;
11+
12+
import java.io.File;
13+
import java.io.IOException;
14+
import java.lang.reflect.Type;
15+
import java.nio.charset.StandardCharsets;
16+
import java.nio.file.Files;
17+
import java.nio.file.Path;
18+
import java.nio.file.Paths;
19+
import java.util.List;
20+
import java.util.Map;
21+
22+
/**
23+
* Offline, one-shot line + branch coverage computation for the dynamic-dedup
24+
* "Option B" flow. Runs OUTSIDE the app JVM (as a short-lived job invoked by
25+
* k8s-proxy at recording stop), so it needs no live JaCoCo agent — only:
26+
*
27+
* <ul>
28+
* <li><b>classes</b> — the app's compiled {@code .class} files (or a jar),
29+
* i.e. the bytecode blob stored once per build tag;</li>
30+
* <li><b>union</b> — {@code {vmClassName -> [firedProbeIdx]}}: the union of
31+
* every kept test-case fingerprint for the test set (across all
32+
* auto-replay cycles). Reuses the exact {@code dedupFingerprints} probe
33+
* sets already persisted for cross-cycle dedup;</li>
34+
* <li><b>manifest</b> — {@code {vmClassName -> {id, probeCount}}}: the JaCoCo
35+
* class id (CRC64, as an unsigned-hex string) and total probe count per
36+
* class. These are build-constant and captured by the SDK from the live
37+
* {@code ExecutionData} at record time ({@code getId()} /
38+
* {@code getProbes().length}), so the reporter never has to touch JaCoCo
39+
* internal APIs to derive them.</li>
40+
* </ul>
41+
*
42+
* <p>Reconstruction: for each class in the union we build an
43+
* {@link ExecutionData} with the manifest's id + a {@code boolean[probeCount]}
44+
* whose fired indices are set, put it in an {@link ExecutionDataStore}, then run
45+
* the JaCoCo {@link Analyzer} over the classes. The analyzer computes each
46+
* class's CRC64 id from its bytecode and matches it against the store; a class
47+
* with no entry (never hit) is analyzed with all-missed probes so it still
48+
* contributes to the DENOMINATOR. Summing {@code getLineCounter()} /
49+
* {@code getBranchCounter()} across all classes yields the whole test set's
50+
* line + branch coverage.
51+
*
52+
* <p>Because dedup is coverage-preserving (it only drops exact/subset
53+
* duplicates), the union of the KEPT fingerprints equals the union of every
54+
* replayed test, so this number is the true whole-test-set coverage.
55+
*
56+
* <p>Output ({@code --out}) is a JSON object:
57+
* {@code {"lineCovered":N,"lineTotal":N,"branchCovered":N,"branchTotal":N,
58+
* "instructionCovered":N,"instructionTotal":N,"methodCovered":N,
59+
* "methodTotal":N,"classCount":N,"hitClassCount":N}}.
60+
*/
61+
public final class CoverageReporter {
62+
63+
private static final Gson GSON = new Gson();
64+
65+
private CoverageReporter() {
66+
}
67+
68+
/** JaCoCo class id + total probe count for one class (build-constant). */
69+
static final class ClassMeta {
70+
String id; // CRC64 class id, unsigned-hex (from ExecutionData.getId())
71+
int probeCount; // ExecutionData.getProbes().length
72+
}
73+
74+
/** Computed coverage counters, serialized to the --out file. */
75+
static final class CoverageResult {
76+
long lineCovered, lineTotal;
77+
long branchCovered, branchTotal;
78+
long instructionCovered, instructionTotal;
79+
long methodCovered, methodTotal;
80+
int classCount; // classes analyzed (denominator basis)
81+
int hitClassCount; // classes with at least one fired probe
82+
}
83+
84+
public static void main(String[] args) {
85+
try {
86+
Args parsed = Args.parse(args);
87+
CoverageResult result = compute(parsed.classesPath, parsed.unionPath, parsed.manifestPath);
88+
String json = GSON.toJson(result);
89+
if (parsed.outPath != null) {
90+
Files.write(parsed.outPath, json.getBytes(StandardCharsets.UTF_8));
91+
} else {
92+
System.out.println(json);
93+
}
94+
} catch (Exception e) {
95+
System.err.println("CoverageReporter failed: " + e.getMessage());
96+
e.printStackTrace();
97+
System.exit(1);
98+
}
99+
}
100+
101+
/**
102+
* Computes coverage from a classes dir/jar, a fired-probe union, and the
103+
* per-class id/probe-count manifest. Package-visible for unit testing.
104+
*/
105+
static CoverageResult compute(Path classesPath, Path unionPath, Path manifestPath) throws IOException {
106+
Map<String, List<Integer>> union = readUnion(unionPath);
107+
Map<String, ClassMeta> manifest = readManifest(manifestPath);
108+
109+
ExecutionDataStore store = new ExecutionDataStore();
110+
int hitClasses = 0;
111+
for (Map.Entry<String, ClassMeta> entry : manifest.entrySet()) {
112+
String className = entry.getKey();
113+
ClassMeta meta = entry.getValue();
114+
if (meta == null || meta.id == null || meta.probeCount < 0) {
115+
continue;
116+
}
117+
boolean[] probes = new boolean[meta.probeCount];
118+
List<Integer> fired = union.get(className);
119+
if (fired != null) {
120+
for (Integer idx : fired) {
121+
if (idx != null && idx >= 0 && idx < probes.length) {
122+
probes[idx] = true;
123+
}
124+
}
125+
if (!fired.isEmpty()) {
126+
hitClasses++;
127+
}
128+
}
129+
long id = Long.parseUnsignedLong(meta.id, 16);
130+
store.put(new ExecutionData(id, className, probes));
131+
}
132+
133+
CoverageBuilder builder = new CoverageBuilder();
134+
Analyzer analyzer = new Analyzer(store, builder);
135+
File classesFile = classesPath.toFile();
136+
if (!classesFile.exists()) {
137+
throw new IOException("classes path does not exist: " + classesPath);
138+
}
139+
// analyzeAll walks a dir tree (or a jar) and analyzes every .class,
140+
// computing each class's CRC64 id and matching it against the store.
141+
//
142+
// LIMITATION — load-time-transformed classes may undercount: the store id
143+
// comes from the LIVE ExecutionData at record time, but analyzeAll hashes
144+
// the ON-DISK .class bytes. If a class was transformed before JaCoCo saw it
145+
// (another -javaagent, CGLIB, load-time weaving — common in Spring apps),
146+
// the two CRC64 ids differ, so the analyzed class won't match its store
147+
// entry: its fired probes are dropped from the numerator while it still
148+
// counts toward the denominator, deflating the reported coverage. Dedup is
149+
// UNAFFECTED (probe sets are compared like-for-like, never against on-disk
150+
// bytes) — only the reported line/branch numbers can be low. To fully fix,
151+
// export the exact instrumented bytes (or detect+log id mismatches here).
152+
analyzer.analyzeAll(classesFile);
153+
154+
CoverageResult result = new CoverageResult();
155+
int classCount = 0;
156+
for (IClassCoverage cc : builder.getClasses()) {
157+
classCount++;
158+
result.lineCovered += cc.getLineCounter().getCoveredCount();
159+
result.lineTotal += cc.getLineCounter().getTotalCount();
160+
result.branchCovered += cc.getBranchCounter().getCoveredCount();
161+
result.branchTotal += cc.getBranchCounter().getTotalCount();
162+
result.instructionCovered += cc.getInstructionCounter().getCoveredCount();
163+
result.instructionTotal += cc.getInstructionCounter().getTotalCount();
164+
result.methodCovered += cc.getMethodCounter().getCoveredCount();
165+
result.methodTotal += cc.getMethodCounter().getTotalCount();
166+
}
167+
result.classCount = classCount;
168+
result.hitClassCount = hitClasses;
169+
return result;
170+
}
171+
172+
private static Map<String, List<Integer>> readUnion(Path path) throws IOException {
173+
Type type = new TypeToken<Map<String, List<Integer>>>() {
174+
}.getType();
175+
Map<String, List<Integer>> union = GSON.fromJson(readString(path), type);
176+
return union == null ? java.util.Collections.<String, List<Integer>>emptyMap() : union;
177+
}
178+
179+
private static Map<String, ClassMeta> readManifest(Path path) throws IOException {
180+
Type type = new TypeToken<Map<String, ClassMeta>>() {
181+
}.getType();
182+
Map<String, ClassMeta> manifest = GSON.fromJson(readString(path), type);
183+
if (manifest == null) {
184+
throw new IOException("manifest is empty or invalid: " + path);
185+
}
186+
return manifest;
187+
}
188+
189+
private static String readString(Path path) throws IOException {
190+
return new String(Files.readAllBytes(path), StandardCharsets.UTF_8);
191+
}
192+
193+
/** Minimal --flag value arg parser. */
194+
private static final class Args {
195+
Path classesPath;
196+
Path unionPath;
197+
Path manifestPath;
198+
Path outPath;
199+
200+
static Args parse(String[] argv) {
201+
Args a = new Args();
202+
for (int i = 0; i + 1 < argv.length; i += 2) {
203+
String flag = argv[i];
204+
String val = argv[i + 1];
205+
switch (flag) {
206+
case "--classes":
207+
a.classesPath = Paths.get(val);
208+
break;
209+
case "--union":
210+
a.unionPath = Paths.get(val);
211+
break;
212+
case "--manifest":
213+
a.manifestPath = Paths.get(val);
214+
break;
215+
case "--out":
216+
a.outPath = Paths.get(val);
217+
break;
218+
default:
219+
throw new IllegalArgumentException("unknown flag: " + flag);
220+
}
221+
}
222+
if (a.classesPath == null || a.unionPath == null || a.manifestPath == null) {
223+
throw new IllegalArgumentException(
224+
"usage: CoverageReporter --classes <dir|jar> --union <union.json> "
225+
+ "--manifest <manifest.json> [--out <cov.json>]");
226+
}
227+
return a;
228+
}
229+
}
230+
231+
// Referenced to keep the ICounter import meaningful for readers scanning
232+
// deps; getLineCounter()/getBranchCounter() return ICounter instances.
233+
@SuppressWarnings("unused")
234+
private static long coveredOf(ICounter counter) {
235+
return counter.getCoveredCount();
236+
}
237+
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
package io.keploy.dedup;
2+
3+
import org.jacoco.core.analysis.Analyzer;
4+
import org.jacoco.core.analysis.CoverageBuilder;
5+
import org.jacoco.core.analysis.IClassCoverage;
6+
import org.jacoco.core.data.ExecutionData;
7+
import org.jacoco.core.data.ExecutionDataStore;
8+
import org.jacoco.core.tools.ExecFileLoader;
9+
10+
import java.io.File;
11+
12+
/**
13+
* Standalone validation for {@link CoverageReporter}: proves that computing
14+
* coverage by <em>reconstructing</em> {@link ExecutionData} from a fired-probe
15+
* union + a per-class (id, probeCount) manifest produces the <em>identical</em>
16+
* line/branch numbers as JaCoCo's own analysis of a real {@code .exec} dump.
17+
*
18+
* <p>Given a real {@code .exec} (produced by running the app under
19+
* {@code -javaagent:jacocoagent.jar}) and the matching classes dir, it:
20+
* <ol>
21+
* <li>computes GROUND TRUTH: {@code Analyzer} over the loaded exec store;</li>
22+
* <li>derives a manifest (id hex + probeCount) and union (fired indices) from
23+
* that same exec — exactly what the SDK would persist;</li>
24+
* <li>REBUILDS an execution store from manifest + union (the
25+
* {@code CoverageReporter} path) and re-analyzes;</li>
26+
* <li>asserts the two coverage results are equal.</li>
27+
* </ol>
28+
*
29+
* <p>Usage: {@code java -cp keploy-sdk.jar
30+
* io.keploy.dedup.CoverageReporterSelfTest --exec <jacoco.exec> --classes <dir>}
31+
*/
32+
public final class CoverageReporterSelfTest {
33+
34+
private CoverageReporterSelfTest() {
35+
}
36+
37+
public static void main(String[] args) throws Exception {
38+
String execPath = null;
39+
String classesPath = null;
40+
for (int i = 0; i + 1 < args.length; i += 2) {
41+
if ("--exec".equals(args[i])) {
42+
execPath = args[i + 1];
43+
} else if ("--classes".equals(args[i])) {
44+
classesPath = args[i + 1];
45+
}
46+
}
47+
if (execPath == null || classesPath == null) {
48+
System.err.println("usage: CoverageReporterSelfTest --exec <jacoco.exec> --classes <dir>");
49+
System.exit(2);
50+
}
51+
52+
ExecFileLoader loader = new ExecFileLoader();
53+
loader.load(new File(execPath));
54+
ExecutionDataStore realStore = loader.getExecutionDataStore();
55+
56+
// (1) GROUND TRUTH — analyze the real exec directly.
57+
long[] truth = analyze(realStore, classesPath);
58+
59+
// (2) Derive manifest + union from the real exec (what the SDK captures),
60+
// then (3) REBUILD a fresh store from them (the CoverageReporter path).
61+
ExecutionDataStore rebuilt = new ExecutionDataStore();
62+
int classes = 0;
63+
int firedProbes = 0;
64+
for (ExecutionData data : realStore.getContents()) {
65+
classes++;
66+
boolean[] src = data.getProbes();
67+
boolean[] copy = new boolean[src.length]; // manifest.probeCount = src.length
68+
for (int i = 0; i < src.length; i++) {
69+
if (src[i]) { // union = fired indices
70+
copy[i] = true;
71+
firedProbes++;
72+
}
73+
}
74+
// manifest.id = data.getId() (round-tripped through unsigned-hex, like the real pipeline)
75+
long id = Long.parseUnsignedLong(Long.toHexString(data.getId()), 16);
76+
rebuilt.put(new ExecutionData(id, data.getName(), copy));
77+
}
78+
long[] rebuiltCov = analyze(rebuilt, classesPath);
79+
80+
System.out.println("classes in exec: " + classes + ", fired probes: " + firedProbes);
81+
printRow("GROUND TRUTH (direct exec)", truth);
82+
printRow("REBUILT (manifest+union) ", rebuiltCov);
83+
84+
boolean equal = true;
85+
for (int i = 0; i < truth.length; i++) {
86+
if (truth[i] != rebuiltCov[i]) {
87+
equal = false;
88+
break;
89+
}
90+
}
91+
if (equal) {
92+
System.out.println("RESULT: PASS — reconstruction matches JaCoCo's direct analysis exactly.");
93+
System.exit(0);
94+
} else {
95+
System.out.println("RESULT: FAIL — reconstruction diverges from ground truth.");
96+
System.exit(1);
97+
}
98+
}
99+
100+
/** Returns {lineCov, lineTot, branchCov, branchTot, instrCov, instrTot}. */
101+
private static long[] analyze(ExecutionDataStore store, String classesPath) throws Exception {
102+
CoverageBuilder builder = new CoverageBuilder();
103+
new Analyzer(store, builder).analyzeAll(new File(classesPath));
104+
long lc = 0, lt = 0, bc = 0, bt = 0, ic = 0, it = 0;
105+
for (IClassCoverage cc : builder.getClasses()) {
106+
lc += cc.getLineCounter().getCoveredCount();
107+
lt += cc.getLineCounter().getTotalCount();
108+
bc += cc.getBranchCounter().getCoveredCount();
109+
bt += cc.getBranchCounter().getTotalCount();
110+
ic += cc.getInstructionCounter().getCoveredCount();
111+
it += cc.getInstructionCounter().getTotalCount();
112+
}
113+
return new long[]{lc, lt, bc, bt, ic, it};
114+
}
115+
116+
private static void printRow(String label, long[] c) {
117+
System.out.printf("%s lines %d/%d branches %d/%d instr %d/%d%n",
118+
label, c[0], c[1], c[2], c[3], c[4], c[5]);
119+
}
120+
}

0 commit comments

Comments
 (0)