Skip to content

Commit 8fc59ea

Browse files
dfa1claude
andcommitted
test(reader,writer): make the zstd binding guard testable
The optional-binding guards added in 0.12.0 had branches no in- process test could reach — the binding is always on the test classpath. Factor the probe into bindingPresent(className) and the throw into requireBinding(present), both package-private, so both branches are covered by a plain unit test passing an absent class name / present=false. Behavior unchanged; greens 6 of the new-code coverage gaps the post-0.12.0 quality gate flagged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent fdbb352 commit 8fc59ea

4 files changed

Lines changed: 147 additions & 18 deletions

File tree

reader/src/main/java/io/github/dfa1/vortex/reader/decode/ZstdEncodingDecoder.java

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,30 +32,44 @@ public final class ZstdEncodingDecoder implements EncodingDecoder {
3232
// The io.github.dfa1.zstd binding is an OPTIONAL dependency: probe once so a reader
3333
// touching vortex.zstd without it gets an actionable message instead of a raw
3434
// NoClassDefFoundError surfacing from the first binding call.
35-
private static final boolean ZSTD_BINDING_PRESENT = zstdBindingPresent();
35+
private static final boolean ZSTD_BINDING_PRESENT =
36+
bindingPresent("io.github.dfa1.zstd.ZstdDecompressContext");
3637

37-
private static boolean zstdBindingPresent() {
38+
/// Reports whether the named binding class is loadable — package-private and
39+
/// class-name-parameterized so both branches are unit-testable without the binding
40+
/// having to actually be absent from the classpath.
41+
///
42+
/// @param className fully qualified class name to probe
43+
/// @return `true` if the class resolves, `false` if it is absent
44+
static boolean bindingPresent(String className) {
3845
try {
39-
Class.forName("io.github.dfa1.zstd.ZstdDecompressContext", false,
40-
ZstdEncodingDecoder.class.getClassLoader());
46+
Class.forName(className, false, ZstdEncodingDecoder.class.getClassLoader());
4147
return true;
4248
} catch (ClassNotFoundException e) {
4349
return false;
4450
}
4551
}
4652

53+
/// Fails with an actionable [VortexException] when the optional zstd binding is absent.
54+
///
55+
/// @param present whether the binding is on the classpath
56+
/// @throws VortexException naming the two artifacts to add, if `present` is `false`
57+
static void requireBinding(boolean present) {
58+
if (!present) {
59+
throw new VortexException(EncodingId.VORTEX_ZSTD, "this file uses vortex.zstd, but the "
60+
+ "optional zstd binding is not on the classpath — add io.github.dfa1.zstd:zstd "
61+
+ "and io.github.dfa1.zstd:zstd-platform (versions pinned by the vortex BOM)");
62+
}
63+
}
64+
4765
@Override
4866
public EncodingId encodingId() {
4967
return EncodingId.VORTEX_ZSTD;
5068
}
5169

5270
@Override
5371
public Array decode(DecodeContext ctx) {
54-
if (!ZSTD_BINDING_PRESENT) {
55-
throw new VortexException(EncodingId.VORTEX_ZSTD, "this file uses vortex.zstd, but the "
56-
+ "optional zstd binding is not on the classpath — add io.github.dfa1.zstd:zstd "
57-
+ "and io.github.dfa1.zstd:zstd-platform (versions pinned by the vortex BOM)");
58-
}
72+
requireBinding(ZSTD_BINDING_PRESENT);
5973
MemorySegment rawMeta = ctx.metadata();
6074
if (rawMeta == null) {
6175
throw new VortexException(EncodingId.VORTEX_ZSTD, "missing metadata");
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package io.github.dfa1.vortex.reader.decode;
2+
3+
import io.github.dfa1.vortex.core.error.VortexException;
4+
import org.junit.jupiter.api.Test;
5+
6+
import static org.assertj.core.api.Assertions.assertThat;
7+
import static org.assertj.core.api.Assertions.assertThatCode;
8+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
9+
10+
/// Covers the optional-zstd-binding guard on the decode side. The binding is always on the test
11+
/// classpath, so the "absent" branches are unreachable through a real decode; the guard logic is
12+
/// factored into class-name- and boolean-parameterized helpers precisely so both branches are
13+
/// exercised here without the jar having to be missing.
14+
class ZstdBindingGuardTest {
15+
16+
@Test
17+
void bindingPresent_resolvableClass_returnsTrue() {
18+
// Given the real binding class name
19+
// When
20+
boolean result = ZstdEncodingDecoder.bindingPresent("io.github.dfa1.zstd.ZstdDecompressContext");
21+
22+
// Then
23+
assertThat(result).isTrue();
24+
}
25+
26+
@Test
27+
void bindingPresent_absentClass_returnsFalse() {
28+
// Given a class name no classpath provides — exercises the ClassNotFoundException branch
29+
// without the binding actually being absent
30+
// When
31+
boolean result = ZstdEncodingDecoder.bindingPresent("io.github.dfa1.zstd.NoSuchBindingClass");
32+
33+
// Then
34+
assertThat(result).isFalse();
35+
}
36+
37+
@Test
38+
void requireBinding_absent_throwsActionableVortexException() {
39+
// Given / When / Then — the message names the two artifacts a consumer must add
40+
assertThatThrownBy(() -> ZstdEncodingDecoder.requireBinding(false))
41+
.isInstanceOf(VortexException.class)
42+
.hasMessageContaining("io.github.dfa1.zstd:zstd")
43+
.hasMessageContaining("zstd-platform");
44+
}
45+
46+
@Test
47+
void requireBinding_present_doesNotThrow() {
48+
// Given / When / Then
49+
assertThatCode(() -> ZstdEncodingDecoder.requireBinding(true)).doesNotThrowAnyException();
50+
}
51+
}

writer/src/main/java/io/github/dfa1/vortex/writer/encode/ZstdEncodingEncoder.java

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,18 +29,36 @@ public final class ZstdEncodingEncoder implements EncodingEncoder {
2929
// The io.github.dfa1.zstd binding is an OPTIONAL dependency: probe once so a writer
3030
// touching vortex.zstd without it gets an actionable message instead of a raw
3131
// NoClassDefFoundError surfacing from the first binding call.
32-
private static final boolean ZSTD_BINDING_PRESENT = zstdBindingPresent();
32+
private static final boolean ZSTD_BINDING_PRESENT =
33+
bindingPresent("io.github.dfa1.zstd.ZstdCompressContext");
3334

34-
private static boolean zstdBindingPresent() {
35+
/// Reports whether the named binding class is loadable — package-private and
36+
/// class-name-parameterized so both branches are unit-testable without the binding
37+
/// having to actually be absent from the classpath.
38+
///
39+
/// @param className fully qualified class name to probe
40+
/// @return `true` if the class resolves, `false` if it is absent
41+
static boolean bindingPresent(String className) {
3542
try {
36-
Class.forName("io.github.dfa1.zstd.ZstdCompressContext", false,
37-
ZstdEncodingEncoder.class.getClassLoader());
43+
Class.forName(className, false, ZstdEncodingEncoder.class.getClassLoader());
3844
return true;
3945
} catch (ClassNotFoundException e) {
4046
return false;
4147
}
4248
}
4349

50+
/// Fails with an actionable [VortexException] when the optional zstd binding is absent.
51+
///
52+
/// @param present whether the binding is on the classpath
53+
/// @throws VortexException naming the two artifacts to add, if `present` is `false`
54+
static void requireBinding(boolean present) {
55+
if (!present) {
56+
throw new VortexException(EncodingId.VORTEX_ZSTD, "vortex.zstd encoding requires the "
57+
+ "optional zstd binding — add io.github.dfa1.zstd:zstd and "
58+
+ "io.github.dfa1.zstd:zstd-platform (versions pinned by the vortex BOM)");
59+
}
60+
}
61+
4462
/// Values per zstd frame; `0` (or any non-positive value) means a single frame for the whole array.
4563
private final long valuesPerFrame;
4664

@@ -77,11 +95,7 @@ public boolean acceptsNullable(DType dtype) {
7795

7896
@Override
7997
public EncodeResult encode(DType dtype, Object data, EncodeContext ctx) {
80-
if (!ZSTD_BINDING_PRESENT) {
81-
throw new VortexException(EncodingId.VORTEX_ZSTD, "vortex.zstd encoding requires the "
82-
+ "optional zstd binding — add io.github.dfa1.zstd:zstd and "
83-
+ "io.github.dfa1.zstd:zstd-platform (versions pinned by the vortex BOM)");
84-
}
98+
requireBinding(ZSTD_BINDING_PRESENT);
8599
if (data instanceof NullableData nd) {
86100
if (dtype instanceof DType.Primitive dt) {
87101
return encodeNullablePrimitive(dt, nd, ctx);
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package io.github.dfa1.vortex.writer.encode;
2+
3+
import io.github.dfa1.vortex.core.error.VortexException;
4+
import org.junit.jupiter.api.Test;
5+
6+
import static org.assertj.core.api.Assertions.assertThat;
7+
import static org.assertj.core.api.Assertions.assertThatCode;
8+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
9+
10+
/// Covers the optional-zstd-binding guard on the encode side. The binding is always on the test
11+
/// classpath, so the "absent" branches are unreachable through a real encode; the guard logic is
12+
/// factored into class-name- and boolean-parameterized helpers precisely so both branches are
13+
/// exercised here without the jar having to be missing.
14+
class ZstdBindingGuardTest {
15+
16+
@Test
17+
void bindingPresent_resolvableClass_returnsTrue() {
18+
// Given the real binding class name
19+
// When
20+
boolean result = ZstdEncodingEncoder.bindingPresent("io.github.dfa1.zstd.ZstdCompressContext");
21+
22+
// Then
23+
assertThat(result).isTrue();
24+
}
25+
26+
@Test
27+
void bindingPresent_absentClass_returnsFalse() {
28+
// Given a class name no classpath provides — exercises the ClassNotFoundException branch
29+
// When
30+
boolean result = ZstdEncodingEncoder.bindingPresent("io.github.dfa1.zstd.NoSuchBindingClass");
31+
32+
// Then
33+
assertThat(result).isFalse();
34+
}
35+
36+
@Test
37+
void requireBinding_absent_throwsActionableVortexException() {
38+
// Given / When / Then — the message names the two artifacts a consumer must add
39+
assertThatThrownBy(() -> ZstdEncodingEncoder.requireBinding(false))
40+
.isInstanceOf(VortexException.class)
41+
.hasMessageContaining("io.github.dfa1.zstd:zstd")
42+
.hasMessageContaining("zstd-platform");
43+
}
44+
45+
@Test
46+
void requireBinding_present_doesNotThrow() {
47+
// Given / When / Then
48+
assertThatCode(() -> ZstdEncodingEncoder.requireBinding(true)).doesNotThrowAnyException();
49+
}
50+
}

0 commit comments

Comments
 (0)