Skip to content

Commit a9bda35

Browse files
dfa1claude
andcommitted
test(writer): cover PatchedEncodingEncoder narrow-width + error paths
Add tests exercising every integer PType through toLongs/fromLongs/PTypeIO.set, the non-primitive and unsupported-ptype throw paths, and the no-bit-width-gain notApplicable branch. Coverage on PatchedEncodingEncoder goes from 42 missed lines to 1 (an unreachable defensive default in fromLongs). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 2b68f71 commit a9bda35

1 file changed

Lines changed: 163 additions & 0 deletions

File tree

writer/src/test/java/io/github/dfa1/vortex/writer/encode/PatchedEncodingEncoderTest.java

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,15 @@
22

33
import io.github.dfa1.vortex.core.DType;
44
import io.github.dfa1.vortex.core.PType;
5+
import io.github.dfa1.vortex.core.VortexException;
56
import io.github.dfa1.vortex.encoding.DTypes;
67
import io.github.dfa1.vortex.encoding.EncodingId;
78
import io.github.dfa1.vortex.reader.ReadRegistry;
89
import io.github.dfa1.vortex.reader.array.Array;
10+
import io.github.dfa1.vortex.reader.array.ByteArray;
911
import io.github.dfa1.vortex.reader.array.IntArray;
1012
import io.github.dfa1.vortex.reader.array.LongArray;
13+
import io.github.dfa1.vortex.reader.array.ShortArray;
1114
import io.github.dfa1.vortex.reader.decode.ArrayNode;
1215
import io.github.dfa1.vortex.reader.decode.DecodeContext;
1316
import io.github.dfa1.vortex.reader.decode.PatchedEncodingDecoder;
@@ -16,13 +19,15 @@
1619
import org.junit.jupiter.api.Nested;
1720
import org.junit.jupiter.api.Test;
1821
import org.junit.jupiter.params.ParameterizedTest;
22+
import org.junit.jupiter.params.provider.EnumSource;
1923
import org.junit.jupiter.params.provider.ValueSource;
2024

2125
import java.lang.foreign.Arena;
2226
import java.lang.foreign.MemorySegment;
2327
import java.util.List;
2428

2529
import static org.assertj.core.api.Assertions.assertThat;
30+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
2631

2732
class PatchedEncodingEncoderTest {
2833

@@ -119,6 +124,47 @@ void encode_roundTrip_i32_multipleChunks() {
119124
}
120125
}
121126

127+
@ParameterizedTest
128+
@EnumSource(value = PType.class, names = {"I8", "U8", "I16", "U16", "I32", "U32", "I64", "U64"})
129+
void encode_roundTrip_allIntegerPTypes(PType ptype) {
130+
// Given: 100 small values + 1 outlier — exercises toLongs/PTypeIO.set for every width
131+
int n = 100;
132+
DType dtype = dtypeOf(ptype);
133+
Object data = buildData(ptype, n);
134+
135+
// When
136+
EncodeResult result = SUT.encode(dtype, data, EncodeTestHelper.testCtx());
137+
Array decoded = decode(result, dtype, n);
138+
139+
// Then
140+
for (int i = 0; i < n; i++) {
141+
assertThat(readLong(decoded, i, ptype)).as("ptype %s index %d", ptype, i)
142+
.isEqualTo(expectedAt(ptype, i, n));
143+
}
144+
}
145+
146+
@Test
147+
void encode_throws_onUnsupportedPType() {
148+
// Given: a primitive float dtype slips past the instanceof check but has no integer mapping
149+
double[] data = {1.0, 2.0, 3.0};
150+
151+
// When / Then
152+
assertThatThrownBy(() -> SUT.encode(DTypes.F64, data, EncodeTestHelper.testCtx()))
153+
.isInstanceOf(VortexException.class)
154+
.hasMessageContaining("unsupported ptype");
155+
}
156+
157+
@Test
158+
void encode_throws_onNonPrimitiveDtype() {
159+
// Given
160+
long[] data = {1L, 2L, 3L};
161+
162+
// When / Then: non-primitive dtype is a programmer error on the encode path
163+
assertThatThrownBy(() -> SUT.encode(DTypes.UTF8, data, EncodeTestHelper.testCtx()))
164+
.isInstanceOf(VortexException.class)
165+
.hasMessageContaining("expected primitive");
166+
}
167+
122168
@ParameterizedTest
123169
@ValueSource(ints = {10, 100, 1023, 1024, 1025, 2048})
124170
void encode_roundTrip_i64_variousLengths(int n) {
@@ -212,5 +258,122 @@ void encodeCascade_notApplicable_whenEmpty() {
212258
// Then
213259
assertThat(step.applicable()).isFalse();
214260
}
261+
262+
@Test
263+
void encodeCascade_notApplicable_whenNonPrimitiveDtype() {
264+
// Given
265+
long[] data = {1L, 2L, 3L};
266+
267+
// When
268+
CascadeStep step = SUT.encodeCascade(DTypes.UTF8, data, EncodeTestHelper.testCtx());
269+
270+
// Then
271+
assertThat(step.applicable()).isFalse();
272+
}
273+
274+
@Test
275+
void encodeCascade_notApplicable_whenNoBitWidthGain() {
276+
// Given: every value needs the full 64 bits, so no target width beats the raw cost
277+
long[] data = new long[100];
278+
for (int i = 0; i < data.length; i++) {
279+
data[i] = Long.MIN_VALUE | i; // bit 63 set → width 64 for all
280+
}
281+
282+
// When
283+
CascadeStep step = SUT.encodeCascade(DTypes.I64, data, EncodeTestHelper.testCtx());
284+
285+
// Then
286+
assertThat(step.applicable()).isFalse();
287+
}
288+
289+
@ParameterizedTest
290+
@EnumSource(value = PType.class, names = {"I8", "U8", "I16", "U16", "I32", "U32"})
291+
void encodeCascade_applicable_smallIntegerPTypes(PType ptype) {
292+
// Given: 100 small values + 1 outlier — exercises fromLongs for every narrow width
293+
int n = 100;
294+
DType dtype = dtypeOf(ptype);
295+
Object data = buildData(ptype, n);
296+
297+
// When
298+
CascadeStep step = SUT.encodeCascade(dtype, data, EncodeTestHelper.testCtx());
299+
300+
// Then
301+
assertThat(step.applicable()).isTrue();
302+
assertThat(step.openChildren()).hasSize(4);
303+
assertThat(step.openChildren().get(0).childDtype()).isEqualTo(dtype);
304+
assertThat(step.openChildren().get(3).childDtype()).isEqualTo(dtype);
305+
}
306+
}
307+
308+
private static DType dtypeOf(PType ptype) {
309+
return new DType.Primitive(ptype, false);
310+
}
311+
312+
/// Builds an array of the given primitive type: small values everywhere except a
313+
/// single high outlier at the midpoint, which forces a patch.
314+
private static Object buildData(PType ptype, int n) {
315+
return switch (ptype) {
316+
case I8, U8 -> {
317+
byte[] a = new byte[n];
318+
for (int i = 0; i < n; i++) {
319+
a[i] = (byte) (i & 3);
320+
}
321+
a[n / 2] = (byte) (ptype == PType.U8 ? 0xC8 : 0x7F);
322+
yield a;
323+
}
324+
case I16, U16 -> {
325+
short[] a = new short[n];
326+
for (int i = 0; i < n; i++) {
327+
a[i] = (short) (i & 3);
328+
}
329+
a[n / 2] = (short) (ptype == PType.U16 ? 0xFFFF : 0x7FFF);
330+
yield a;
331+
}
332+
case I32, U32 -> {
333+
int[] a = new int[n];
334+
for (int i = 0; i < n; i++) {
335+
a[i] = i & 3;
336+
}
337+
a[n / 2] = ptype == PType.U32 ? 0xFFFF_FFFF : 0x7FFF_FFFF;
338+
yield a;
339+
}
340+
case I64, U64 -> {
341+
long[] a = new long[n];
342+
for (int i = 0; i < n; i++) {
343+
a[i] = i & 3;
344+
}
345+
a[n / 2] = ptype == PType.U64 ? -1L : Long.MAX_VALUE;
346+
yield a;
347+
}
348+
default -> throw new IllegalArgumentException("unsupported: " + ptype);
349+
};
350+
}
351+
352+
/// Expected raw element value for [#buildData], used to compare against the decoded array.
353+
private static long expectedAt(PType ptype, int i, int n) {
354+
if (i != n / 2) {
355+
return i & 3;
356+
}
357+
return switch (ptype) {
358+
case I8 -> 0x7F;
359+
case U8 -> (byte) 0xC8;
360+
case I16 -> 0x7FFF;
361+
case U16 -> (short) 0xFFFF;
362+
case I32 -> 0x7FFF_FFFF;
363+
case U32 -> 0xFFFF_FFFF;
364+
case I64 -> Long.MAX_VALUE;
365+
case U64 -> -1L;
366+
default -> throw new IllegalArgumentException("unsupported: " + ptype);
367+
};
368+
}
369+
370+
private static long readLong(Array array, int i, PType ptype) {
371+
return switch (ptype) {
372+
case I8, U8 -> ((ByteArray) array).getByte(i);
373+
case I16, U16 -> ((ShortArray) array).getShort(i);
374+
case I32, U32 -> ((IntArray) array).getInt(i);
375+
case I64, U64 -> ((LongArray) array).getLong(i);
376+
default -> throw new IllegalArgumentException("unsupported: " + ptype);
377+
};
215378
}
216379
}

0 commit comments

Comments
 (0)