This repository contains the Java dynamic dedup coverage agent for Keploy Enterprise.
It collects per-testcase Java coverage during Keploy replay and sends that coverage back to Enterprise so duplicate testcases can be identified and removed.
The repository contains only the dedup-focused keploy-sdk module.
Supported runtimes in CI today are Java 8, 17, and 21.
Dynamic dedup works on applications compiled up to Java 26. See Supported Java versions for where that ceiling comes from and how to raise it.
Keploy Enterprise drives dynamic dedup per testcase.
- Enterprise sends
START <test-set>/<test-id>on/tmp/coverage_control.sock. - The Java agent resets JaCoCo coverage counters for that testcase.
- Enterprise replays the testcase.
- Enterprise sends
END <test-set>/<test-id>on/tmp/coverage_control.sock. - 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. - Enterprise writes the result to
dedupData.yamland uses it to identify duplicates.
Coverage is collected at per-testcase granularity, not process granularity.
Download the keploy-sdk jar and keep it outside your application dependencies. The jar is a Java agent and should be attached only when you run Keploy dynamic deduplication.
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>3.6.1</version>
<executions>
<execution>
<id>copy-keploy-java-agent</id>
<phase>package</phase>
<goals>
<goal>copy</goal>
</goals>
<configuration>
<artifactItems>
<artifactItem>
<groupId>io.keploy</groupId>
<artifactId>keploy-sdk</artifactId>
<version>2.0.5</version>
<outputDirectory>${project.build.directory}</outputDirectory>
<destFileName>keploy-sdk.jar</destFileName>
</artifactItem>
</artifactItems>
</configuration>
</execution>
</executions>
</plugin>The SDK no longer has to be added to dependencies, and application code should not import io.keploy.* classes for dynamic deduplication.
The dedup agent reads coverage in-process via JaCoCo's runtime API (org.jacoco.agent.rt.RT.getAgent()), so attaching the JaCoCo Java agent is the only runtime requirement in the common cases below:
- Maven/Gradle dev runs where application classes are under
target/classesorbuild/classes/java/main - packaged
java -jarruns where the application classes live inside the executable jar
java \
-javaagent:/path/to/keploy-sdk.jar \
-javaagent:/path/to/jacocoagent.jar \
-jar your-app.jarIf the in-process API is unavailable (for example because the JaCoCo agent is loaded into an isolated classloader), the SDK transparently falls back to JaCoCo's TCP server mode. To use the fallback explicitly, start JaCoCo in tcpserver mode and set KEPLOY_JACOCO_HOST / KEPLOY_JACOCO_PORT:
java \
-javaagent:/path/to/keploy-sdk.jar \
-javaagent:/path/to/jacocoagent.jar=address=127.0.0.1,port=36320,output=tcpserver \
-jar your-app.jarRun replay with dynamic dedup enabled:
keploy test -c "java -javaagent:/path/to/keploy-sdk.jar -javaagent:/path/to/jacocoagent.jar -jar your-app.jar" \
--dedup \
--language javaWhen using the TCP fallback, also pass --pass-through-ports <jacoco-port> so Keploy does not try to mock the JaCoCo control connection.
After replay, run:
keploy dedupTo remove duplicates:
keploy dedup --rmJava dedup works in native, Docker, and restricted Docker environments as long as /tmp is shared and writable between Keploy Enterprise and the Java process. In Docker Compose flows, Enterprise can inject that shared /tmp mount when it rewrites the Compose file for replay.
Keploy Enterprise and the Java SDK communicate over these Unix sockets:
/tmp/coverage_control.sock/tmp/coverage_data.sock
Without a shared /tmp, dedup will not work inside containers because Enterprise and the Java process will be writing to different socket paths.
Dynamic dedup can only handle bytecode that both of its jars can read, so the supported ceiling is the lower of two limits β not one:
| Component | Role | Reads up to |
|---|---|---|
jacocoagent.jar (k8s-proxy init image) |
analyzes coverage | Java 26 (JaCoCo 0.8.15) |
keploy-sdk.jar (this repo) |
instruments the app, via the ASM shaded in through org.jacoco:org.jacoco.core |
Java 26 (jacoco.core 0.8.15) |
Both currently sit at Java 26, so that is the effective ceiling.
It is easy to assume the JaCoCo agent alone decides this. It does not. The dedup
agent here does its own instrumentation through the ASM that jacoco.core brings
in, and ASM refuses class files newer than the release it was built against. So
the jacoco.core version in keploy-sdk/pom.xml is an independent limit, and
for a long time it was the lower one: while it sat on 0.8.12 it capped dedup at
Java 22 even though the agent could already read more.
Nothing crashes, which is what makes it worth documenting. The app is instrumented, the replay runs, and every test passes β coverage is simply never produced, so no duplicates are found. That is indistinguishable from "this app genuinely has no duplicates" unless something says otherwise, which is why k8s-proxy now detects the app's real class-file version from the classes this agent uploads and reports the reason instead of failing silently.
- Bump
org.jacoco:org.jacoco.coreinkeploy-sdk/pom.xml, and cut a release. - In k8s-proxy, bump
KEPLOY_SDK_VERSION(build/dedup-jars/build.sh) to that release, and setBundledJaCoCoVersion,JaCoCoOfficialMaxJavaandSDKShadedASMMaxJavainpkg/platform/dedupinject/jacocompat.go.
Take the Java number from JaCoCo's
release notes β the
officially supported version, not the "experimental support for Java N+1 class
files" one. Each JaCoCo release ships an ASM that declares one version beyond
what it accepts by default, so reading the highest declared Opcodes.V<n>
over-states real support by one. k8s-proxy's image build checks the constant
against the ASM actually inside the jar and fails the build if it over-promises.
A scheduled k8s-proxy pipeline also fails when a newer JaCoCo is published, so this ceiling does not quietly fall behind again.
KEPLOY_JACOCO_HOST: JaCoCo TCP host used when the in-process runtime API is unavailable. Default:127.0.0.1KEPLOY_JACOCO_PORT: JaCoCo TCP port used when the in-process runtime API is unavailable. Default:36320KEPLOY_JAVA_CLASS_DIRS: optional comma-separated class, jar, war, ear, or zip locations to analyze for executed lines when your build output lives outside the standard locationsKEPLOY_JAVA_CLASSPATH_FALLBACK: scans the full classpath if standard class roots and the executable archive do not provide application classes. Default:trueKEPLOY_JAVA_DEDUP_DISABLED: disables the Java dedup agent when set totrue,1, oryes
For a working reference, see the Java dedup sample in keploy/samples-java:
samples-java/java-dedup
That sample is used in CI to validate Java dynamic dedup for JDK 8, 17, and 21 across native, classpath, Docker, distroless, and restricted Docker runs. The CI matrix is narrower than the supported range: it pins the runtimes the sample is exercised on, not the ceiling β see Supported Java versions.