Skip to content

Commit bbb9d66

Browse files
dfa1claude
andcommitted
test(reader): cover AlpEncodingDecoder broadcast + patch-index paths
Was 66% line coverage — the encoder round-trips only drive the dense U32-indexed paths. Adds accepts, the non-primitive-dtype throw, the missing-metadata zero-exponent fallback, f64/f32 constant-broadcast (capacity < n, no patches), a U8 patch-index decode (readUnsigned U8 arm), and the non-unsigned patch-index-ptype rejection. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent d487f1a commit bbb9d66

1 file changed

Lines changed: 174 additions & 0 deletions

File tree

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
package io.github.dfa1.vortex.reader.decode;
2+
3+
import io.github.dfa1.vortex.core.DType;
4+
import io.github.dfa1.vortex.core.PType;
5+
import io.github.dfa1.vortex.encoding.EncodingId;
6+
import io.github.dfa1.vortex.proto.ALPMetadata;
7+
import io.github.dfa1.vortex.proto.PatchesMetadata;
8+
import io.github.dfa1.vortex.reader.ReadRegistry;
9+
import io.github.dfa1.vortex.reader.array.DoubleArray;
10+
import io.github.dfa1.vortex.reader.array.FloatArray;
11+
import org.junit.jupiter.api.Test;
12+
13+
import java.lang.foreign.Arena;
14+
import java.lang.foreign.MemorySegment;
15+
import java.nio.ByteBuffer;
16+
import java.nio.ByteOrder;
17+
18+
import static org.assertj.core.api.Assertions.assertThat;
19+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
20+
import static org.assertj.core.api.Assertions.within;
21+
22+
class AlpEncodingDecoderTest {
23+
24+
private static final AlpEncodingDecoder SUT = new AlpEncodingDecoder();
25+
private static final ReadRegistry REGISTRY = TestRegistry.ofDecoders(SUT, new PrimitiveEncodingDecoder());
26+
27+
private static final DType F64 = new DType.Primitive(PType.F64, false);
28+
private static final DType F32 = new DType.Primitive(PType.F32, false);
29+
30+
private static MemorySegment leLongs(long... vs) {
31+
byte[] b = new byte[vs.length * 8];
32+
ByteBuffer bb = ByteBuffer.wrap(b).order(ByteOrder.LITTLE_ENDIAN);
33+
for (long v : vs) {
34+
bb.putLong(v);
35+
}
36+
return MemorySegment.ofArray(b);
37+
}
38+
39+
private static MemorySegment leInts(int... vs) {
40+
byte[] b = new byte[vs.length * 4];
41+
ByteBuffer bb = ByteBuffer.wrap(b).order(ByteOrder.LITTLE_ENDIAN);
42+
for (int v : vs) {
43+
bb.putInt(v);
44+
}
45+
return MemorySegment.ofArray(b);
46+
}
47+
48+
private static MemorySegment leDoubles(double... vs) {
49+
byte[] b = new byte[vs.length * 8];
50+
ByteBuffer bb = ByteBuffer.wrap(b).order(ByteOrder.LITTLE_ENDIAN);
51+
for (double v : vs) {
52+
bb.putDouble(v);
53+
}
54+
return MemorySegment.ofArray(b);
55+
}
56+
57+
@Test
58+
void accepts_floatsTrue_otherFalse() {
59+
// Given / When / Then
60+
assertThat(SUT.accepts(F64)).isTrue();
61+
assertThat(SUT.accepts(F32)).isTrue();
62+
assertThat(SUT.accepts(new DType.Primitive(PType.I64, false))).isFalse();
63+
assertThat(SUT.accepts(new DType.Utf8(false))).isFalse();
64+
}
65+
66+
@Test
67+
void decode_nonPrimitiveDtype_throws() {
68+
// Given a Utf8 dtype on an ALP node
69+
ArrayNode node = ArrayNode.of(EncodingId.VORTEX_ALP, ByteBuffer.wrap(new ALPMetadata(0, 0, null).encode()),
70+
new ArrayNode[0], new int[0]);
71+
DecodeContext ctx = new DecodeContext(node, new DType.Utf8(false), 1,
72+
new MemorySegment[0], REGISTRY, Arena.global());
73+
74+
// When / Then
75+
assertThatThrownBy(() -> SUT.decode(ctx)).hasMessageContaining("expected primitive dtype");
76+
}
77+
78+
@Test
79+
void decode_missingMetadata_defaultsToZeroExponents() {
80+
// Given no metadata — decoder falls back to exp_e=0, exp_f=0 (scale 1.0)
81+
ArrayNode enc = ArrayNode.of(EncodingId.VORTEX_PRIMITIVE, null, new ArrayNode[0], new int[]{0});
82+
ArrayNode node = ArrayNode.of(EncodingId.VORTEX_ALP, null, new ArrayNode[]{enc}, new int[0]);
83+
DecodeContext ctx = new DecodeContext(node, F64, 2, new MemorySegment[]{leLongs(5L, 7L)}, REGISTRY, Arena.global());
84+
85+
// When
86+
DoubleArray result = (DoubleArray) SUT.decode(ctx);
87+
88+
// Then
89+
assertThat(result.getDouble(0)).isCloseTo(5.0, within(1e-9));
90+
assertThat(result.getDouble(1)).isCloseTo(7.0, within(1e-9));
91+
}
92+
93+
@Test
94+
void decode_f64_broadcastNoPatches_returnsConstant() {
95+
// Given a single encoded value but 4 logical rows (capacity < n) and no patches:
96+
// the decoder broadcasts it into a constant array
97+
ArrayNode enc = ArrayNode.of(EncodingId.VORTEX_PRIMITIVE, null, new ArrayNode[0], new int[]{0});
98+
byte[] meta = new ALPMetadata(2, 0, null).encode(); // exp_e=2 -> *0.01
99+
ArrayNode node = ArrayNode.of(EncodingId.VORTEX_ALP, ByteBuffer.wrap(meta), new ArrayNode[]{enc}, new int[0]);
100+
DecodeContext ctx = new DecodeContext(node, F64, 4, new MemorySegment[]{leLongs(123L)}, REGISTRY, Arena.global());
101+
102+
// When
103+
DoubleArray result = (DoubleArray) SUT.decode(ctx);
104+
105+
// Then
106+
assertThat(result.length()).isEqualTo(4);
107+
for (int i = 0; i < 4; i++) {
108+
assertThat(result.getDouble(i)).as("index %d", i).isCloseTo(1.23, within(1e-9));
109+
}
110+
}
111+
112+
@Test
113+
void decode_f32_broadcastNoPatches_returnsConstant() {
114+
// Given single value, 3 rows, no patches
115+
ArrayNode enc = ArrayNode.of(EncodingId.VORTEX_PRIMITIVE, null, new ArrayNode[0], new int[]{0});
116+
byte[] meta = new ALPMetadata(1, 0, null).encode(); // exp_e=1 -> *0.1
117+
ArrayNode node = ArrayNode.of(EncodingId.VORTEX_ALP, ByteBuffer.wrap(meta), new ArrayNode[]{enc}, new int[0]);
118+
DecodeContext ctx = new DecodeContext(node, F32, 3, new MemorySegment[]{leInts(25)}, REGISTRY, Arena.global());
119+
120+
// When
121+
FloatArray result = (FloatArray) SUT.decode(ctx);
122+
123+
// Then
124+
assertThat(result.length()).isEqualTo(3);
125+
for (int i = 0; i < 3; i++) {
126+
assertThat(result.getFloat(i)).as("index %d", i).isCloseTo(2.5f, within(1e-6f));
127+
}
128+
}
129+
130+
@Test
131+
void decode_f64_patches_withU8Indices() {
132+
// Given patches whose index child uses U8 storage — exercises the U8 arm of
133+
// readUnsigned (the encoder always emits U32 indices)
134+
PatchesMetadata pm = new PatchesMetadata(1L, 0L, io.github.dfa1.vortex.proto.PType.U8, null, null, null);
135+
byte[] meta = new ALPMetadata(2, 0, pm).encode(); // *0.01
136+
137+
ArrayNode enc = ArrayNode.of(EncodingId.VORTEX_PRIMITIVE, null, new ArrayNode[0], new int[]{0});
138+
ArrayNode idx = ArrayNode.of(EncodingId.VORTEX_PRIMITIVE, null, new ArrayNode[0], new int[]{1});
139+
ArrayNode val = ArrayNode.of(EncodingId.VORTEX_PRIMITIVE, null, new ArrayNode[0], new int[]{2});
140+
ArrayNode node = ArrayNode.of(EncodingId.VORTEX_ALP, ByteBuffer.wrap(meta),
141+
new ArrayNode[]{enc, idx, val}, new int[0]);
142+
143+
MemorySegment idxSeg = MemorySegment.ofArray(new byte[]{1}); // patch row 1
144+
MemorySegment[] segs = {leLongs(100L, 0L, 300L), idxSeg, leDoubles(9.0)};
145+
DecodeContext ctx = new DecodeContext(node, F64, 3, segs, REGISTRY, Arena.global());
146+
147+
// When
148+
DoubleArray result = (DoubleArray) SUT.decode(ctx);
149+
150+
// Then
151+
assertThat(result.getDouble(0)).isCloseTo(1.0, within(1e-9));
152+
assertThat(result.getDouble(1)).isCloseTo(9.0, within(1e-9)); // patched
153+
assertThat(result.getDouble(2)).isCloseTo(3.0, within(1e-9));
154+
}
155+
156+
@Test
157+
void decode_patches_nonUnsignedIndexPtype_throws() {
158+
// Given a signed (I32) patch-index ptype — readUnsigned rejects it
159+
PatchesMetadata pm = new PatchesMetadata(1L, 0L, io.github.dfa1.vortex.proto.PType.I32, null, null, null);
160+
byte[] meta = new ALPMetadata(2, 0, pm).encode();
161+
162+
ArrayNode enc = ArrayNode.of(EncodingId.VORTEX_PRIMITIVE, null, new ArrayNode[0], new int[]{0});
163+
ArrayNode idx = ArrayNode.of(EncodingId.VORTEX_PRIMITIVE, null, new ArrayNode[0], new int[]{1});
164+
ArrayNode val = ArrayNode.of(EncodingId.VORTEX_PRIMITIVE, null, new ArrayNode[0], new int[]{2});
165+
ArrayNode node = ArrayNode.of(EncodingId.VORTEX_ALP, ByteBuffer.wrap(meta),
166+
new ArrayNode[]{enc, idx, val}, new int[0]);
167+
168+
MemorySegment[] segs = {leLongs(100L, 0L), leInts(1), leDoubles(9.0)};
169+
DecodeContext ctx = new DecodeContext(node, F64, 2, segs, REGISTRY, Arena.global());
170+
171+
// When / Then
172+
assertThatThrownBy(() -> SUT.decode(ctx)).hasMessageContaining("non-unsigned patch index ptype");
173+
}
174+
}

0 commit comments

Comments
 (0)