Skip to content

Commit ed5a82a

Browse files
committed
test(loader): add model-free native-load smoke + document the procedure
Adds NativeLibraryLoadSmokeTest: forces LlamaModel.<clinit> (System.load -> JNI_OnLoad), which FindClass-es every JNI-referenced Java class, with no GGUF model required. It guards the two load-time failure modes that shipped on this branch and were invisible to a local `mvn test` (model-gated tests self-skip before the lib loads) and to the pure-Java unit tests: - wrong native-resource path in LlamaLoader (lib not found), and - a stale FindClass FQN in jllama.cpp after a Java package move (lib loads but JNI_OnLoad throws NoClassDefFoundError). The test self-skips when libjllama is not on the classpath (pure-Java checkout, no CMake build), so a build-less `mvn test` stays green; CI's test-java-* jobs and any local build run it for real. Presence is checked against the canonical /net/ladenthin/llama/<os>/<arch>/ layout directly (not via LlamaLoader.getNativeResourcePath()) so a regression there cannot silently skip the guard — instead the load itself fails the assertion. Verified locally both ways: lib present -> Tests run: 1 (loads, JNI_OnLoad OK); lib moved aside -> Skipped: 1, BUILD SUCCESS. CLAUDE.md: documents the model-free load-verification recipe under "Restricted-network environments" (build the lib via FetchContent — GitHub is reachable even when huggingface.co is not — then run the smoke test), and the rule to update jllama.cpp FindClass/signature FQNs + keep LlamaLoader.NATIVE_RESOURCE_BASE anchored when moving a JNI-referenced class.
1 parent e26e1ea commit ed5a82a

2 files changed

Lines changed: 105 additions & 0 deletions

File tree

CLAUDE.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,49 @@ be exercised either in CI (via `.github/workflows/publish.yml`) or on a
303303
developer machine with HF access; pre-staged models can also be uploaded
304304
into `models/` out-of-band.
305305

306+
**Verifying the native library *loads* without models (model-free smoke).**
307+
Even with HuggingFace blocked you can still do the one piece of *real native*
308+
verification that does not need a GGUF: confirm the library loads and its
309+
`JNI_OnLoad` resolves every Java class it looks up by name. The model-gated
310+
tests cannot do this in a restricted sandbox — they self-skip via
311+
`Assume.assumeTrue(model present)` **before** the lib is ever loaded, so a plain
312+
`mvn test` is silent on load-time breakage. The full local recipe:
313+
314+
```bash
315+
# 1. Build the native lib locally (FetchContent pulls llama.cpp from GitHub,
316+
# which is reachable even when huggingface.co is not):
317+
mvn -q compile
318+
cmake -B build -DBUILD_TESTING=ON
319+
cmake --build build --config Release -j$(nproc) # -> src/main/resources/.../<os>/<arch>/libjllama.so
320+
# 2. Force LlamaModel.<clinit> (System.load -> JNI_OnLoad) with no model:
321+
mvn test -Dtest=NativeLibraryLoadSmokeTest
322+
```
323+
324+
`NativeLibraryLoadSmokeTest` (in the `loader` package) calls
325+
`Class.forName("net.ladenthin.llama.LlamaModel")`, which runs
326+
`LlamaLoader.initialize() -> System.load() -> JNI_OnLoad`, which in turn calls
327+
`FindClass(...)` for every JNI-referenced Java class. It **passes** when the lib
328+
loads cleanly, **fails** if the native-resource path in `LlamaLoader` is wrong
329+
(lib not found) or a `FindClass`/field-signature FQN in
330+
`src/main/cpp/jllama.cpp` is stale after a Java package move (lib loads but
331+
`JNI_OnLoad` throws `NoClassDefFoundError: net/ladenthin/llama/...`), and
332+
**self-skips** when `libjllama` is not on the classpath (pure-Java checkout, no
333+
CMake build) so it never breaks a build-less `mvn test`.
334+
335+
Both of those failure modes shipped on a branch once — the layered-package
336+
restructure left (a) `LlamaLoader.getNativeResourcePath()` deriving the resource
337+
root from the loader's own package (which moved to `…loader`) and (b)
338+
`jllama.cpp` still `FindClass`-ing the old flat paths — and neither was visible
339+
to a local `mvn test` (model tests skipped) or to the pure-Java unit tests.
340+
**When you move a Java class the JNI layer references by name** (`LlamaModel`
341+
[root], `exception.LlamaException`, `value.LogLevel`, `args.LogFormat`,
342+
`callback.LoadProgressCallback`), update the matching `FindClass` / `"L…;"`
343+
signature string in `src/main/cpp/jllama.cpp` and keep the native-resource root
344+
anchored at `net/ladenthin/llama/` in `LlamaLoader.NATIVE_RESOURCE_BASE` (it must
345+
not track the loader's own Java package). This is the same
346+
"FQN/path not updated after a package move" class as the stale
347+
`spotbugs-exclude.xml`, PIT `targetClasses`, and `CMakeLists.txt` OSInfo repairs.
348+
306349
### Code Formatting
307350
```bash
308351
clang-format -i src/main/cpp/*.cpp src/main/cpp/*.hpp # Format C++ code
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// SPDX-FileCopyrightText: 2026 Bernard Ladenthin <bernard.ladenthin@gmail.com>
2+
//
3+
// SPDX-License-Identifier: MIT
4+
5+
package net.ladenthin.llama.loader;
6+
7+
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
8+
import static org.junit.jupiter.api.Assumptions.assumeTrue;
9+
10+
import net.ladenthin.llama.ClaudeGenerated;
11+
import org.junit.jupiter.api.Test;
12+
13+
/**
14+
* Model-free smoke test that the bundled native library actually loads and its
15+
* {@code JNI_OnLoad} resolves every Java class it looks up by name.
16+
*
17+
* <p>Forcing {@code LlamaModel.<clinit>} runs
18+
* {@code LlamaLoader.initialize() -> System.load() -> JNI_OnLoad}, which calls
19+
* {@code FindClass(...)} for the JNI-referenced classes ({@code LlamaException},
20+
* {@code LogLevel}, {@code LogFormat}, ...). No GGUF model is required, so this
21+
* catches the two failure modes that the model-gated tests cannot exercise when
22+
* models are absent (e.g. in a restricted-network sandbox):
23+
*
24+
* <ul>
25+
* <li>a wrong native-resource path in {@link LlamaLoader} (lib not found), and</li>
26+
* <li>a stale {@code FindClass} FQN in {@code jllama.cpp} after a Java package
27+
* move (lib loads but {@code JNI_OnLoad} throws
28+
* {@code NoClassDefFoundError}).</li>
29+
* </ul>
30+
*
31+
* <p>Both bugs shipped once on this branch precisely because they only surface
32+
* when the library is loaded — see the regression history in {@code CLAUDE.md}.
33+
*
34+
* <p>The test self-skips when {@code libjllama} is not on the classpath (a
35+
* pure-Java checkout with no native build), so a plain {@code mvn test} stays
36+
* green without a CMake build; CI's {@code test-java-*} jobs and any local build
37+
* have the library and run it for real. The presence check uses the canonical
38+
* resource layout directly (not {@link LlamaLoader#getNativeResourcePath()}) so
39+
* a regression in that method cannot silently skip this guard.
40+
*/
41+
@ClaudeGenerated(
42+
purpose = "Model-free native-load smoke: force LlamaModel.<clinit> so System.load + JNI_OnLoad "
43+
+ "run and resolve every FindClass'd Java class. Guards against native-resource-path and "
44+
+ "stale-JNI-FQN regressions that only appear when the library is actually loaded; skips "
45+
+ "cleanly when libjllama is not on the classpath.")
46+
class NativeLibraryLoadSmokeTest {
47+
48+
private static boolean nativeLibraryOnClasspath() {
49+
String resource = "/net/ladenthin/llama/" + OSInfo.getNativeLibFolderPathForCurrentOS() + "/"
50+
+ System.mapLibraryName("jllama");
51+
return NativeLibraryLoadSmokeTest.class.getResource(resource) != null;
52+
}
53+
54+
@Test
55+
void loadingNativeLibraryRunsJniOnLoadWithoutError() {
56+
assumeTrue(nativeLibraryOnClasspath(), "libjllama not on classpath — skipping native-load smoke");
57+
assertDoesNotThrow(
58+
() -> Class.forName("net.ladenthin.llama.LlamaModel"),
59+
"LlamaModel.<clinit> must load the native library and JNI_OnLoad must resolve "
60+
+ "every FindClass'd Java class");
61+
}
62+
}

0 commit comments

Comments
 (0)