feat: shade JaCoCo into codeflash-runtime as one fat JAR

Merge JaCoCo agent and CLI into the runtime JAR instead of shipping 3
separate JARs. JaCoCo already self-shades its internals with a version
hash, so no relocation is needed.

- Add AgentDispatcher premain that routes to profiler (config=) or
  JaCoCo (destfile=) based on agent args
- Update shade plugin: Premain-Class → AgentDispatcher, add
  ServicesResourceTransformer and DontIncludeResourceTransformer
- Rewrite build_jacoco_agent_arg() and generate_jacoco_report() to use
  the runtime JAR instead of separate JaCoCo JARs
- Delete org.jacoco.agent-0.8.13-runtime.jar and
  org.jacoco.cli-0.8.13-nodeps.jar from resources/

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Mohamed Ashraf 2026-03-07 01:06:58 +00:00
parent b6acd6b7ce
commit 24ad61aa5c
8 changed files with 106 additions and 31 deletions

View file

@ -40,6 +40,7 @@
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<jacoco.version>0.8.13</jacoco.version>
</properties>
<dependencies>
@ -83,6 +84,22 @@
<version>9.7.1</version>
</dependency>
<!-- JaCoCo agent (coverage instrumentation at runtime) -->
<dependency>
<groupId>org.jacoco</groupId>
<artifactId>org.jacoco.agent</artifactId>
<version>${jacoco.version}</version>
<classifier>runtime</classifier>
</dependency>
<!-- JaCoCo CLI (report generation from .exec files) -->
<dependency>
<groupId>org.jacoco</groupId>
<artifactId>org.jacoco.cli</artifactId>
<version>${jacoco.version}</version>
<classifier>nodeps</classifier>
</dependency>
<!-- JUnit 5 for testing -->
<dependency>
<groupId>org.junit.jupiter</groupId>
@ -145,10 +162,14 @@
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>com.codeflash.Comparator</mainClass>
<manifestEntries>
<Premain-Class>com.codeflash.profiler.ProfilerAgent</Premain-Class>
<Premain-Class>com.codeflash.AgentDispatcher</Premain-Class>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</transformer>
<transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
<transformer implementation="org.apache.maven.plugins.shade.resource.DontIncludeResourceTransformer">
<resource>about.html</resource>
</transformer>
</transformers>
<filters>
<filter>

View file

@ -0,0 +1,33 @@
package com.codeflash;
import java.lang.instrument.Instrumentation;
/**
* Premain dispatcher that routes to either the CodeFlash line profiler or the
* JaCoCo coverage agent based on the agent arguments.
*
* <p>Detection logic:
* <ul>
* <li>Args contain {@code config=} line profiler mode delegate to
* {@link com.codeflash.profiler.ProfilerAgent}</li>
* <li>Otherwise JaCoCo mode delegate to JaCoCo's PreMain</li>
* </ul>
*
* <p>This is reliable because our profiler always receives
* {@code config=/path/to/config.json} while JaCoCo always receives
* {@code destfile=/path/to/jacoco.exec}.
*/
public class AgentDispatcher {
static boolean isProfilerMode(String agentArgs) {
return agentArgs != null && agentArgs.contains("config=");
}
public static void premain(String agentArgs, Instrumentation inst) throws Exception {
if (isProfilerMode(agentArgs)) {
com.codeflash.profiler.ProfilerAgent.premain(agentArgs, inst);
} else {
org.jacoco.agent.rt.internal_0e20598.PreMain.premain(agentArgs, inst);
}
}
}

View file

@ -0,0 +1,33 @@
package com.codeflash;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;
class AgentDispatcherTest {
@Test
void profilerModeWhenConfigPresent() {
assertTrue(AgentDispatcher.isProfilerMode("config=/tmp/config.json"));
}
@Test
void profilerModeWithMultipleArgs() {
assertTrue(AgentDispatcher.isProfilerMode("config=/tmp/config.json,output=results"));
}
@Test
void jacocoModeWhenDestfilePresent() {
assertFalse(AgentDispatcher.isProfilerMode("destfile=/tmp/jacoco.exec"));
}
@Test
void jacocoModeWhenNullArgs() {
assertFalse(AgentDispatcher.isProfilerMode(null));
}
@Test
void jacocoModeWhenEmptyArgs() {
assertFalse(AgentDispatcher.isProfilerMode(""));
}
}

View file

@ -22,8 +22,6 @@ CODEFLASH_RUNTIME_VERSION = "1.0.0"
CODEFLASH_RUNTIME_JAR_NAME = f"codeflash-runtime-{CODEFLASH_RUNTIME_VERSION}.jar"
JACOCO_PLUGIN_VERSION = "0.8.13"
JACOCO_AGENT_JAR = f"org.jacoco.agent-{JACOCO_PLUGIN_VERSION}-runtime.jar"
JACOCO_CLI_JAR = f"org.jacoco.cli-{JACOCO_PLUGIN_VERSION}-nodeps.jar"
_JAVA_RESOURCES_DIR = Path(__file__).parent / "resources"
@ -72,21 +70,6 @@ def restore_all_pom_backups() -> None:
_pom_backups.clear()
def find_jacoco_agent_jar() -> Path | None:
"""Find the bundled JaCoCo agent JAR in package resources."""
jar = _JAVA_RESOURCES_DIR / JACOCO_AGENT_JAR
if jar.exists():
return jar
return None
def find_jacoco_cli_jar() -> Path | None:
"""Find the bundled JaCoCo CLI JAR in package resources."""
jar = _JAVA_RESOURCES_DIR / JACOCO_CLI_JAR
if jar.exists():
return jar
return None
# MVN_CENTRAL_TODO: Uncomment once codeflash-runtime is published to Maven Central.
# Steps:

View file

@ -28,8 +28,6 @@ from codeflash.languages.java.build_tools import (
CODEFLASH_RUNTIME_VERSION,
add_codeflash_dependency_to_pom,
backup_pom,
find_jacoco_agent_jar,
find_jacoco_cli_jar,
find_maven_executable,
get_jacoco_xml_path,
install_codeflash_runtime,
@ -179,23 +177,29 @@ def _validate_java_class_name(class_name: str) -> bool:
def build_jacoco_agent_arg(exec_dest: Path) -> str | None:
"""Build the -javaagent arg for standalone JaCoCo coverage collection.
Returns None if the bundled JaCoCo agent JAR is not found.
The codeflash-runtime JAR includes the shaded JaCoCo agent. The AgentDispatcher
routes to JaCoCo when the args contain ``destfile=`` (no ``config=``).
Returns None if the runtime JAR is not found.
"""
agent_jar = find_jacoco_agent_jar()
if agent_jar is None:
logger.warning("Bundled JaCoCo agent JAR not found — coverage will not be collected")
runtime_jar = _find_runtime_jar()
if runtime_jar is None:
logger.warning("codeflash-runtime JAR not found — coverage will not be collected")
return None
return f"-javaagent:{agent_jar}=destfile={exec_dest}"
return f"-javaagent:{runtime_jar}=destfile={exec_dest}"
def generate_jacoco_report(exec_file: Path, classfiles_dir: Path, sourcefiles_dir: Path, xml_output: Path) -> bool:
"""Generate a JaCoCo XML report from a .exec file using the bundled CLI JAR.
"""Generate a JaCoCo XML report from a .exec file.
Uses the JaCoCo CLI classes shaded into the codeflash-runtime JAR
(invoked via ``java -cp <runtime_jar> org.jacoco.cli.internal.Main``).
Returns True if the report was generated successfully.
"""
cli_jar = find_jacoco_cli_jar()
if cli_jar is None:
logger.error("Bundled JaCoCo CLI JAR not found — cannot generate coverage report")
runtime_jar = _find_runtime_jar()
if runtime_jar is None:
logger.error("codeflash-runtime JAR not found — cannot generate coverage report")
return False
if not exec_file.exists():
@ -206,8 +210,9 @@ def generate_jacoco_report(exec_file: Path, classfiles_dir: Path, sourcefiles_di
cmd = [
shutil.which("java") or "java",
"-jar",
str(cli_jar),
"-cp",
str(runtime_jar),
"org.jacoco.cli.internal.Main",
"report",
str(exec_file),
"--classfiles",