Skip to content

Commit d6e5f22

Browse files
michalharakalclaude
andcommitted
Add ReleaseApiJavaTest covering 0.19.0 Java surface (#400)
New JUnit5 Java test in skainet-test-java exercising each of the three Kotlin surfaces polished in the earlier commits of this branch for Java-first-citizenship: - StableHloConverterFactory.createBasic/Extended/Fast() — must be reachable via the idiomatic `Factory.create*()` static form, never through `Factory.INSTANCE.create*()`. The test is effectively a compile-time smoke check: if someone drops the @JvmStatic annotations it fails to compile before any assertion runs. - TokenizerFactory.fromGguf(Map) / fromTokenizerJson(String) — same pattern. Passing empty inputs exercises the error path (UnsupportedTokenizerException), which is the cleanest way to prove static dispatch without needing a real GGUF fixture in the test classpath. - TensorSpecs (the new JvmName-bound class for TensorSpecEncoding.kt): getTensorEncoding / withTensorEncoding called via `TensorSpecs.<name>(spec, ...)` in Java syntax. Verifies the round-trip of TensorEncoding.Q8_0.INSTANCE and confirms withTensorEncoding does not mutate the source spec. Adds skainet-compile-hlo and skainet-io-core to the Java test module's `testImplementation` classpath so the new test can reference the factories + encoding helpers. Existing Java tests (SKaiNETTest, ModelBuilderTest, TensorJavaOpsTest) are untouched. Verified: `./gradlew :skainet-test:skainet-test-java:test` green — all 3 pre-existing tests plus the 4 new tests in ReleaseApiJavaTest. Fifth and final commit polishing the Java / JVM consumption story for the upcoming 0.19.0 release. See #400. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 76cfea2 commit d6e5f22

2 files changed

Lines changed: 123 additions & 0 deletions

File tree

skainet-test/skainet-test-java/build.gradle.kts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ dependencies {
1212
testImplementation(project(":skainet-lang:skainet-lang-core"))
1313
testImplementation(project(":skainet-backends:skainet-backend-cpu"))
1414
testImplementation(project(":skainet-data:skainet-data-simple"))
15+
// 0.19.0 Java consumption surface: converter factory, tokenizer
16+
// factory, and the TensorSpec encoding helper facade. Tested in
17+
// ReleaseApiJavaTest so a Java consumer of the upcoming release
18+
// has a reference invocation pattern for each.
19+
testImplementation(project(":skainet-compile:skainet-compile-hlo"))
20+
testImplementation(project(":skainet-io:skainet-io-core"))
1521
}
1622

1723
tasks.test {
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
package sk.ainet.java;
2+
3+
import org.junit.jupiter.api.Test;
4+
5+
import sk.ainet.compile.hlo.StableHloConverter;
6+
import sk.ainet.compile.hlo.StableHloConverterFactory;
7+
import sk.ainet.io.tokenizer.TokenizerFactory;
8+
import sk.ainet.io.tokenizer.UnsupportedTokenizerException;
9+
import sk.ainet.lang.tensor.ops.TensorSpec;
10+
import sk.ainet.lang.tensor.ops.TensorSpecs;
11+
import sk.ainet.lang.tensor.storage.TensorEncoding;
12+
13+
import java.util.Collections;
14+
import java.util.List;
15+
import java.util.Map;
16+
17+
import static org.junit.jupiter.api.Assertions.*;
18+
19+
/**
20+
* Java consumer smoke tests for the Kotlin surfaces polished in the
21+
* 0.19.0 release for Java-first-citizenship (#400). Each test is
22+
* deliberately close to the call sites a Java consumer of the BOM
23+
* would write so the patterns are self-documenting.
24+
*
25+
* If any of these lose their clean static form on a future Kotlin
26+
* refactor (e.g. `@JvmStatic` dropped), the tests fail at compile
27+
* time rather than at bytecode-verification time in production.
28+
*/
29+
class ReleaseApiJavaTest {
30+
31+
// --- StableHloConverterFactory -----------------------------------------
32+
33+
/**
34+
* The converter factory must be reachable via the idiomatic
35+
* static form, not through the Kotlin object's INSTANCE marker.
36+
* Written as a compile-time smoke test — if someone drops the
37+
* @JvmStatic annotations this fails to compile before any
38+
* assertion runs.
39+
*/
40+
@Test
41+
void stableHloConverterFactoryIsStatic() {
42+
StableHloConverter basic = StableHloConverterFactory.createBasic();
43+
StableHloConverter extended = StableHloConverterFactory.createExtended();
44+
StableHloConverter fast = StableHloConverterFactory.createFast();
45+
46+
assertNotNull(basic, "createBasic() must return a non-null converter");
47+
assertNotNull(extended, "createExtended() must return a non-null converter");
48+
assertNotNull(fast, "createFast() must return a non-null converter");
49+
}
50+
51+
// --- TokenizerFactory --------------------------------------------------
52+
53+
/**
54+
* TokenizerFactory's static form is the entry point Java consumers
55+
* hit when they load a GGUF or HuggingFace tokenizer.json. The
56+
* call shape must stay clean across releases.
57+
*
58+
* We pass an empty GGUF field map and expect an
59+
* UnsupportedTokenizerException — the point is to prove the
60+
* factory is dispatched via static, not to actually tokenize.
61+
*/
62+
@Test
63+
void tokenizerFactoryFromGgufIsStatic() {
64+
Map<String, Object> emptyFields = Collections.emptyMap();
65+
assertThrows(
66+
UnsupportedTokenizerException.class,
67+
() -> TokenizerFactory.fromGguf(emptyFields),
68+
"empty GGUF metadata map must trip UnsupportedTokenizerException"
69+
);
70+
}
71+
72+
@Test
73+
void tokenizerFactoryFromTokenizerJsonIsStatic() {
74+
String emptyJson = "{}";
75+
assertThrows(
76+
UnsupportedTokenizerException.class,
77+
() -> TokenizerFactory.fromTokenizerJson(emptyJson),
78+
"tokenizer.json with no model.type must trip UnsupportedTokenizerException"
79+
);
80+
}
81+
82+
// --- TensorSpecs (JvmName of TensorSpecEncoding.kt) --------------------
83+
84+
/**
85+
* The TensorEncoding accessor helpers live on
86+
* skainet-lang-core/.../ops/TensorSpecEncoding.kt, which now
87+
* compiles to a class named TensorSpecs (via @file:JvmName).
88+
* Java callers access read / copy via static-method syntax.
89+
*/
90+
@Test
91+
void tensorSpecsEncodingHelpers() {
92+
TensorSpec bare = new TensorSpec(
93+
/* name= */ "w",
94+
/* shape= */ List.of(8, 4),
95+
/* dtype= */ "FP32",
96+
/* requiresGrad= */ false,
97+
/* metadata= */ Collections.emptyMap()
98+
);
99+
100+
// Reader: an un-annotated spec has a null encoding.
101+
assertNull(TensorSpecs.getTensorEncoding(bare),
102+
"a fresh TensorSpec must have no tensorEncoding");
103+
104+
// Setter: returns a copy with the encoding attached.
105+
TensorSpec annotated = TensorSpecs.withTensorEncoding(
106+
bare, TensorEncoding.Q8_0.INSTANCE);
107+
assertNotNull(TensorSpecs.getTensorEncoding(annotated),
108+
"annotated spec must have a non-null tensorEncoding");
109+
assertSame(TensorEncoding.Q8_0.INSTANCE,
110+
TensorSpecs.getTensorEncoding(annotated),
111+
"annotated spec must carry the encoding we set");
112+
113+
// The original is unchanged — data-class copy semantics.
114+
assertNull(TensorSpecs.getTensorEncoding(bare),
115+
"withTensorEncoding must not mutate the source spec");
116+
}
117+
}

0 commit comments

Comments
 (0)