@@ -71,6 +71,8 @@ protected void commonTearDown() {
7171 protected static final String SQRL_CMD_IMAGE = "datasqrl/cmd" ;
7272 protected static final String SQRL_SERVER_IMAGE = "datasqrl/sqrl-server" ;
7373 protected static final String BUILD_DIR = "/build" ;
74+ protected static final String JACOCO_OUT_DIR = "/jacoco" ;
75+ protected static final String JACOCO_AGENT_PATH = "/jacoco-agent.jar" ;
7476 protected static final int HTTP_SERVER_PORT = 8888 ;
7577
7678 protected static Network sharedNetwork ;
@@ -115,6 +117,8 @@ protected GenericContainer<?> createCmdContainer(Path workingDir, boolean debug)
115117 .withFileSystemBind (workingDir .toString (), BUILD_DIR , BindMode .READ_WRITE )
116118 .withEnv ("TZ" , "America/Los_Angeles" );
117119
120+ container = configureJacocoCoverage (container , "cmd" );
121+
118122 if (debug ) {
119123 container = container .withEnv ("SQRL_DEBUG" , "1" );
120124 }
@@ -127,14 +131,19 @@ protected GenericContainer<?> createServerContainer(Path workingDir) {
127131 var deployPlanPath = workingDir .resolve ("build/deploy/plan" );
128132 assertThat (deployPlanPath ).exists ().isDirectory ();
129133
130- return new GenericContainer <>(DockerImageName .parse (SQRL_SERVER_IMAGE + ":" + getImageTag ()))
131- .withNetwork (sharedNetwork )
132- .withExposedPorts (HTTP_SERVER_PORT )
133- .withFileSystemBind (deployPlanPath .toString (), "/opt/sqrl/config" , BindMode .READ_ONLY )
134- .withEnv ("SQRL_DEBUG" , "1" )
135- .waitingFor (
136- Wait .forLogMessage (".*HTTP server listening on port 8888.*" , 1 )
137- .withStartupTimeout (Duration .ofSeconds (30 )));
134+ var container =
135+ new GenericContainer <>(DockerImageName .parse (SQRL_SERVER_IMAGE + ":" + getImageTag ()))
136+ .withNetwork (sharedNetwork )
137+ .withExposedPorts (HTTP_SERVER_PORT )
138+ .withFileSystemBind (deployPlanPath .toString (), "/opt/sqrl/config" , BindMode .READ_ONLY )
139+ .withEnv ("SQRL_DEBUG" , "1" )
140+ .waitingFor (
141+ Wait .forLogMessage (".*HTTP server listening on port 8888.*" , 1 )
142+ .withStartupTimeout (Duration .ofSeconds (30 )));
143+
144+ container = configureJacocoCoverage (container , "server" );
145+
146+ return container ;
138147 }
139148
140149 protected void compileSqrlProject (Path workingDir ) {
@@ -290,6 +299,75 @@ private String getImageTag() {
290299 return System .getProperty ("docker.image.tag" , "local" );
291300 }
292301
302+ @ SneakyThrows
303+ private GenericContainer <?> configureJacocoCoverage (GenericContainer <?> container , String component ) {
304+ if (!Boolean .parseBoolean (System .getProperty ("sqrl.container.coverage" , "false" ))) {
305+ return container ;
306+ }
307+
308+ var jacocoAgentJar = findJacocoAgentJar ();
309+ if (jacocoAgentJar == null ) {
310+ log .warn ("Container coverage enabled but JaCoCo agent jar not found in ~/.m2; skipping" );
311+ return container ;
312+ }
313+
314+ var jacocoOutDir = Paths .get ("target" , "jacoco" ).toAbsolutePath ();
315+ var hostDestFile = jacocoOutDir .resolve ("jacoco-container.exec" );
316+ Files .createDirectories (jacocoOutDir );
317+
318+ var containerDestFile = JACOCO_OUT_DIR + "/jacoco-container.exec" ;
319+ var agentArg =
320+ "-javaagent:"
321+ + JACOCO_AGENT_PATH
322+ + "=destfile="
323+ + containerDestFile
324+ + ",append=true,excludes=org/**" ;
325+
326+ var toolOptions = agentArg ;
327+
328+ log .info (
329+ "Enabling container JaCoCo for {}: agent={} destfile={}" ,
330+ component ,
331+ jacocoAgentJar ,
332+ hostDestFile );
333+
334+ return container
335+ .withFileSystemBind (jacocoOutDir .toString (), JACOCO_OUT_DIR , BindMode .READ_WRITE )
336+ .withFileSystemBind (jacocoAgentJar .toString (), JACOCO_AGENT_PATH , BindMode .READ_ONLY )
337+ .withEnv ("JAVA_TOOL_OPTIONS" , toolOptions );
338+ }
339+
340+ @ Nullable
341+ private Path findJacocoAgentJar () {
342+ var m2 = Paths .get (System .getProperty ("user.home" ), ".m2" , "repository" );
343+ var jacocoDir = m2 .resolve (Paths .get ("org" , "jacoco" , "org.jacoco.agent" ));
344+ if (!Files .exists (jacocoDir )) {
345+ return null ;
346+ }
347+
348+ try (var stream = Files .list (jacocoDir )) {
349+ var versions =
350+ stream
351+ .filter (Files ::isDirectory )
352+ .map (p -> p .getFileName ().toString ())
353+ .sorted ()
354+ .toList ();
355+ for (int i = versions .size () - 1 ; i >= 0 ; i --) {
356+ var version = versions .get (i );
357+ var candidate =
358+ jacocoDir .resolve (
359+ Paths .get (version , "org.jacoco.agent-" + version + "-runtime.jar" ));
360+ if (Files .exists (candidate )) {
361+ return candidate ;
362+ }
363+ }
364+ return null ;
365+ } catch (Exception e ) {
366+ log .warn ("Failed to locate JaCoCo agent jar under {}" , jacocoDir , e );
367+ return null ;
368+ }
369+ }
370+
293371 protected String getDockerRunCommand (GenericContainer <?> container , Path workingDir ) {
294372 var sb = new StringBuilder ();
295373 sb .append ("docker run -it --rm" );
0 commit comments