From b331c5e8ff7d71630854d04401763ef53b2f5a2c Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 2 Oct 2026 08:04:23 +0000 Subject: [PATCH 1/2] Give PIT the native library Surefire reads from src/main/natives The Java test job on main (run 36935020061) passed its 1831 tests and then failed the PIT gate before any mutant ran: "JsonEndpointParametersTest did not pass without mutation ... requires a green suite". Surefire finds the native library through additionalClasspathElements (src/main/natives). PIT builds its own classpath and does not read that, and since the natives left target/classes for the natives jars (#472) it had no library at all. JsonEndpointParametersTest is in a target-test package and model-backed: in CI its model is present, so it did not skip, and the library load failed. pitest-maven's own additionalClasspathElements adds the same directory. Without a native build the directory is missing and those tests skip themselves, as under Surefire. The earlier runs never got this far: run 36896497300 failed one step before, in the tests. Checked: a throwaway test in the parameters package that loads LlamaModel failed PIT's coverage run the same way before the change and passed after it; the full PIT gate then ran 388 mutations, 388 killed, test strength 100%. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AytmJF9faEiQEVt6eetQS2 --- llama/pom.xml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/llama/pom.xml b/llama/pom.xml index fa3d3fa43..46d3396ad 100644 --- a/llama/pom.xml +++ b/llama/pom.xml @@ -856,6 +856,18 @@ SPDX-License-Identifier: MIT org.slf4j:slf4j-simple + + + ${project.basedir}/src/main/natives + From 396feaa6a18eef6c7f18914de98c816d94dec3ec Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 2 Oct 2026 08:09:07 +0000 Subject: [PATCH 2/2] Guard the test classpath with TestClasspathTest The last two red runs on main came from the test classpath, not from the code: Surefire's module mode put Jackson on the module path (ExamplesTest, RouterModeIntegrationTest), and PIT had no native library (the PIT gate). Both were reachable only through model-backed tests, which locally skip, so they showed up in CI alone. TestClasspathTest pins the three properties, model-free: - the tests do not run in a named module; - Jackson can bind a private test type (ExamplesTest's failure shape) and is on java.class.path (what the router worker JVM is started with); - every native library under src/main/natives is a classpath resource (skips without a native build). It is listed in PIT's targetTests, so the same checks run on PIT's own classpath and fail its coverage run before any mutant. Checked, each by removing the setting it guards: without useModulePath=false three tests fail; without Surefire's additionalClasspathElements the native-library test fails; without PIT's additionalClasspathElements PIT aborts on it ("did not pass without mutation"). With all settings: 4/4 under Surefire, and the full PIT gate 388/388 killed, test strength 100%. CLAUDE.md says which Surefire settings PIT has to repeat and why. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AytmJF9faEiQEVt6eetQS2 --- CLAUDE.md | 9 ++ llama/pom.xml | 3 + .../ladenthin/llama/TestClasspathTest.java | 122 ++++++++++++++++++ 3 files changed, 134 insertions(+) create mode 100644 llama/src/test/java/net/ladenthin/llama/TestClasspathTest.java diff --git a/CLAUDE.md b/CLAUDE.md index f0c4fde16..dd5ff82b0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2135,6 +2135,15 @@ Run PIT with the lifecycle prefix — `mvn test-compile org.pitest:pitest-maven: needed: `ContentPartTest`'s `@TempDir` tests cover `value.ContentPart.audioFile(Path)` (verified 318/318 killed, 0 NO_COVERAGE, test strength 100% in a fixture-less sandbox; the former audio-fixture gotcha is resolved). +**PIT builds its own classpath and reads none of Surefire's settings**, so everything the tests need +from Surefire's configuration is repeated on the PIT plugin: the `slf4j-simple` exclusion (see "Java 8 +bytecode floor") and `src/main/natives` as `additionalClasspathElements` (the natives left +`target/classes` with the natives jars; without it a model-backed test in a target-test package aborts +the gate in CI, where its model exists). `TestClasspathTest` is in PIT's `targetTests` and fails under +both runners when the native library is missing from the classpath, when the tests run in a named +module (Surefire's default with a `module-info.class`, hence `useModulePath=false`), or when Jackson is +not on `java.class.path` (which the router worker JVM is started with). + **`net.ladenthin.llama.value.*` is a target at `mutationThreshold` 100**, so a new getter on a `value` type needs its own test or the gate reds — the `ServerMetrics` counters added for the `getMetrics()` merge are covered by `ServerMetricsTest`. diff --git a/llama/pom.xml b/llama/pom.xml index 46d3396ad..a8f11fd52 100644 --- a/llama/pom.xml +++ b/llama/pom.xml @@ -834,6 +834,9 @@ SPDX-License-Identifier: MIT net.ladenthin.llama.args.* net.ladenthin.llama.json.* net.ladenthin.llama.parameters.* + + net.ladenthin.llama.TestClasspathTest 100 30000 diff --git a/llama/src/test/java/net/ladenthin/llama/TestClasspathTest.java b/llama/src/test/java/net/ladenthin/llama/TestClasspathTest.java new file mode 100644 index 000000000..a547b88e6 --- /dev/null +++ b/llama/src/test/java/net/ladenthin/llama/TestClasspathTest.java @@ -0,0 +1,122 @@ +// SPDX-FileCopyrightText: 2026 Bernard Ladenthin +// +// SPDX-License-Identifier: MIT + +package net.ladenthin.llama; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.PropertyAccessor; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.File; +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.Test; + +/** + * Pins how the build lays out the test classpath, so a pom change that breaks it fails here, + * model-free and on every run, instead of in a model-gated test that only CI reaches. + * + *

Two of these failed on {@code main} after the natives moved out of {@code target/classes}: + * Surefire's default module mode put Jackson on the module path (ExamplesTest could not bind a + * test type, the router worker JVM found no Jackson on {@code java.class.path}), and PIT, which + * builds its own classpath, had no native library. The pom fixes are + * {@code useModulePath=false} on Surefire and {@code additionalClasspathElements} on PIT; this + * class is in PIT's {@code targetTests} so it runs under both.

+ */ +@ClaudeGenerated( + purpose = "Guard the Surefire and PIT test-classpath settings whose loss broke " + + "ExamplesTest, RouterModeIntegrationTest and the PIT gate on main.") +public class TestClasspathTest { + + /** The resource root of the native libraries, as {@code LlamaLoader} looks them up. */ + private static final String NATIVE_RESOURCE_ROOT = "net/ladenthin/llama"; + + /** Bound by Jackson below; private, so binding it needs a reflective access check. */ + private static final class Probe { + private String name; + + private Probe() {} + } + + @Test + public void testsRunOnTheClasspathNotInANamedModule() { + assertThat(TestClasspathTest.class.getModule().isNamed(), is(false)); + } + + @Test + public void jacksonBindsAPrivateTestType() throws IOException { + ObjectMapper mapper = new ObjectMapper(); + mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY); + + Probe probe = mapper.readValue("{\"name\":\"x\"}", Probe.class); + + assertThat(probe.name, is("x")); + } + + @Test + public void jacksonIsOnTheJavaClassPathAWorkerJvmIsStartedWith() throws URISyntaxException { + String jackson = location(ObjectMapper.class).toString(); + + assertThat(classPathEntries(), hasItem(jackson)); + } + + @Test + public void everyLocallyBuiltNativeLibraryIsAResourceOnTheClasspath() throws Exception { + Path nativesRoot = moduleBaseDir().resolve("src/main/natives"); + Path libraries = nativesRoot.resolve(NATIVE_RESOURCE_ROOT); + List built = nativeLibraries(libraries); + Assumptions.assumeFalse(built.isEmpty(), "no native build under " + libraries); + + ClassLoader loader = TestClasspathTest.class.getClassLoader(); + for (Path library : built) { + String resource = nativesRoot.relativize(library).toString().replace(File.separatorChar, '/'); + assertThat(resource, loader.getResource(resource), notNullValue()); + } + } + + private static List nativeLibraries(Path dir) throws IOException { + if (!Files.isDirectory(dir)) { + return Collections.emptyList(); + } + try (Stream files = Files.walk(dir)) { + return files.filter(Files::isRegularFile) + .filter(p -> p.getFileName().toString().contains("jllama")) + .collect(Collectors.toList()); + } + } + + /** + * The {@code llama} module directory, derived from where this class was loaded + * ({@code llama/target/test-classes}), so it holds whatever the working directory is: + * Surefire runs in the module, PIT and CI's {@code mvn -f llama/pom.xml} may not. + */ + private static Path moduleBaseDir() throws URISyntaxException { + return location(TestClasspathTest.class).getParent().getParent(); + } + + private static Path location(Class type) throws URISyntaxException { + return Paths.get( + type.getProtectionDomain().getCodeSource().getLocation().toURI()) + .toAbsolutePath(); + } + + private static List classPathEntries() { + return Arrays.stream(System.getProperty("java.class.path", "").split(File.pathSeparator)) + .map(entry -> Paths.get(entry).toAbsolutePath().toString()) + .collect(Collectors.toList()); + } +}