Skip to content

Commit a426c1d

Browse files
dfa1claude
andcommitted
test(writer): seeded-random property sweep for Pco encode/decode
Curated cases name one corner each; add seeded-random sweeps over mixed distributions (full-range, tiny-range, monotone, strided+jitter, sparse outliers, heavy repeats) for i64/i32/f64/f32 — exercising the bin optimizer, delta/IntMult mode pickers, and ANS/patch paths in combinations no single example covers. One i64 run crosses the 64K chunk boundary. Fixed seeds keep failures reproducible. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 115dd6f commit a426c1d

1 file changed

Lines changed: 168 additions & 1 deletion

File tree

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

Lines changed: 168 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
import org.junit.jupiter.params.provider.MethodSource;
1515

1616
import java.lang.foreign.Arena;
17+
import java.lang.foreign.MemorySegment;
18+
import java.util.Random;
1719
import java.util.stream.LongStream;
1820
import java.util.stream.Stream;
1921

@@ -257,11 +259,176 @@ void encodeDecode_f64_isLossless(String name, double[] data) {
257259

258260
/// IntMult-favorable data: base 100 + small random adjustment ([0,100)).
259261
private static long[] intMultPriceData(int n) {
260-
java.util.Random rng = new java.util.Random(42L);
262+
Random rng = new Random(42L);
261263
long[] arr = new long[n];
262264
for (int i = 0; i < n; i++) {
263265
arr[i] = (10L + rng.nextInt(1000)) * 100L + rng.nextInt(100);
264266
}
265267
return arr;
266268
}
269+
270+
// ── seeded-random property sweeps ───────────────────────────────────────────
271+
//
272+
// The curated cases above name one corner each; these sweep mixed distributions
273+
// so the bin optimizer, delta/IntMult mode pickers, and ANS/patch paths face
274+
// combinations no single example covers. Lengths vary up to a chunk-boundary
275+
// crossing. Seeds are fixed so failures reproduce.
276+
277+
static Stream<Arguments> i64Random() {
278+
Random rng = new Random(0x9C0L);
279+
Stream.Builder<Arguments> b = Stream.builder();
280+
for (int t = 0; t < 24; t++) {
281+
int len = 1 + rng.nextInt(t == 0 ? 70_000 : 4000); // one run crosses the 64K chunk boundary
282+
long[] a = new long[len];
283+
int mode = rng.nextInt(6);
284+
long base = rng.nextLong();
285+
long mult = 1L + rng.nextInt(1000);
286+
for (int i = 0; i < len; i++) {
287+
a[i] = switch (mode) {
288+
case 0 -> rng.nextLong(); // full-range: many bins, noOp
289+
case 1 -> rng.nextInt(16); // tiny range: tight bins
290+
case 2 -> base + i; // monotone: delta
291+
case 3 -> base + (long) i * mult + rng.nextInt(8); // strided + jitter: IntMult
292+
case 4 -> (rng.nextInt(20) == 0) ? rng.nextLong() : 1000L + rng.nextInt(4); // sparse outliers: patches/ANS
293+
default -> rng.nextInt(3) == 0 ? base : base + rng.nextInt(2); // heavy repeats: runs
294+
};
295+
}
296+
b.add(Arguments.of("i64-mode" + mode + "-len" + len, a));
297+
}
298+
return b.build();
299+
}
300+
301+
static Stream<Arguments> i32Random() {
302+
Random rng = new Random(0x9C1L);
303+
Stream.Builder<Arguments> b = Stream.builder();
304+
for (int t = 0; t < 24; t++) {
305+
int len = 1 + rng.nextInt(4000);
306+
int[] a = new int[len];
307+
int mode = rng.nextInt(6);
308+
int base = rng.nextInt();
309+
int mult = 1 + rng.nextInt(1000);
310+
for (int i = 0; i < len; i++) {
311+
a[i] = switch (mode) {
312+
case 0 -> rng.nextInt();
313+
case 1 -> rng.nextInt(16);
314+
case 2 -> base + i;
315+
case 3 -> base + i * mult + rng.nextInt(8);
316+
case 4 -> (rng.nextInt(20) == 0) ? rng.nextInt() : 1000 + rng.nextInt(4);
317+
default -> rng.nextInt(3) == 0 ? base : base + rng.nextInt(2);
318+
};
319+
}
320+
b.add(Arguments.of("i32-mode" + mode + "-len" + len, a));
321+
}
322+
return b.build();
323+
}
324+
325+
static Stream<Arguments> f64Random() {
326+
Random rng = new Random(0x9C2L);
327+
Stream.Builder<Arguments> b = Stream.builder();
328+
for (int t = 0; t < 20; t++) {
329+
int len = 1 + rng.nextInt(3000);
330+
double[] a = new double[len];
331+
int mode = rng.nextInt(3);
332+
for (int i = 0; i < len; i++) {
333+
a[i] = switch (mode) {
334+
case 0 -> rng.nextGaussian() * Math.pow(10, rng.nextInt(20) - 10); // wide magnitude
335+
case 1 -> 100.0 + (i % 50) * 0.01; // FloatMult-friendly decimals
336+
default -> rng.nextInt(8); // low cardinality
337+
};
338+
}
339+
b.add(Arguments.of("f64-mode" + mode + "-len" + len, a));
340+
}
341+
return b.build();
342+
}
343+
344+
static Stream<Arguments> f32Random() {
345+
Random rng = new Random(0x9C3L);
346+
Stream.Builder<Arguments> b = Stream.builder();
347+
for (int t = 0; t < 20; t++) {
348+
int len = 1 + rng.nextInt(3000);
349+
float[] a = new float[len];
350+
int mode = rng.nextInt(3);
351+
for (int i = 0; i < len; i++) {
352+
a[i] = switch (mode) {
353+
case 0 -> (float) (rng.nextGaussian() * Math.pow(10, rng.nextInt(12) - 6));
354+
case 1 -> 100.0f + (i % 50) * 0.01f;
355+
default -> rng.nextInt(8);
356+
};
357+
}
358+
b.add(Arguments.of("f32-mode" + mode + "-len" + len, a));
359+
}
360+
return b.build();
361+
}
362+
363+
@ParameterizedTest(name = "{0}")
364+
@MethodSource("i64Random")
365+
void encodeDecode_i64_random_isLossless(String name, long[] data) {
366+
// Given
367+
EncodeResult encoded = ENCODER.encode(I64, data, EncodeTestHelper.testCtx());
368+
369+
// When
370+
DecodeContext ctx = DecodeTestHelper.toDecodeContext(encoded, data.length, I64, REGISTRY);
371+
Array result = DECODER.decode(ctx);
372+
373+
// Then
374+
assertThat(result.length()).isEqualTo(data.length);
375+
MemorySegment m = result.materialize(Arena.ofAuto());
376+
for (int i = 0; i < data.length; i++) {
377+
assertThat(m.get(PTypeIO.LE_LONG, (long) i * 8)).as("index %d", i).isEqualTo(data[i]);
378+
}
379+
}
380+
381+
@ParameterizedTest(name = "{0}")
382+
@MethodSource("i32Random")
383+
void encodeDecode_i32_random_isLossless(String name, int[] data) {
384+
// Given
385+
EncodeResult encoded = ENCODER.encode(I32, data, EncodeTestHelper.testCtx());
386+
387+
// When
388+
DecodeContext ctx = DecodeTestHelper.toDecodeContext(encoded, data.length, I32, REGISTRY);
389+
Array result = DECODER.decode(ctx);
390+
391+
// Then
392+
assertThat(result.length()).isEqualTo(data.length);
393+
MemorySegment m = result.materialize(Arena.ofAuto());
394+
for (int i = 0; i < data.length; i++) {
395+
assertThat(m.get(PTypeIO.LE_INT, (long) i * 4)).as("index %d", i).isEqualTo(data[i]);
396+
}
397+
}
398+
399+
@ParameterizedTest(name = "{0}")
400+
@MethodSource("f64Random")
401+
void encodeDecode_f64_random_isLossless(String name, double[] data) {
402+
// Given
403+
EncodeResult encoded = ENCODER.encode(F64, data, EncodeTestHelper.testCtx());
404+
405+
// When
406+
DecodeContext ctx = DecodeTestHelper.toDecodeContext(encoded, data.length, F64, REGISTRY);
407+
Array result = DECODER.decode(ctx);
408+
409+
// Then
410+
assertThat(result.length()).isEqualTo(data.length);
411+
MemorySegment m = result.materialize(Arena.ofAuto());
412+
for (int i = 0; i < data.length; i++) {
413+
assertThat(m.get(PTypeIO.LE_DOUBLE, (long) i * 8)).as("index %d", i).isEqualTo(data[i]);
414+
}
415+
}
416+
417+
@ParameterizedTest(name = "{0}")
418+
@MethodSource("f32Random")
419+
void encodeDecode_f32_random_isLossless(String name, float[] data) {
420+
// Given
421+
EncodeResult encoded = ENCODER.encode(F32, data, EncodeTestHelper.testCtx());
422+
423+
// When
424+
DecodeContext ctx = DecodeTestHelper.toDecodeContext(encoded, data.length, F32, REGISTRY);
425+
Array result = DECODER.decode(ctx);
426+
427+
// Then
428+
assertThat(result.length()).isEqualTo(data.length);
429+
MemorySegment m = result.materialize(Arena.ofAuto());
430+
for (int i = 0; i < data.length; i++) {
431+
assertThat(m.get(PTypeIO.LE_FLOAT, (long) i * 4)).as("index %d", i).isEqualTo(data[i]);
432+
}
433+
}
267434
}

0 commit comments

Comments
 (0)