Skip to content

Commit a2cf344

Browse files
dfa1claude
andcommitted
test(writer): property-based round-trips for Delta/FoR/ZigZag/AlpRd
Seeded random inputs (+ curated edges: 0, ±1, MIN/MAX, near-extremes) through encode → decode asserting decode(encode(x)) == x. Integer codecs (Delta, FrameOfReference, ZigZag) verify value identity incl. the wrap-around cases where the internal subtraction/shift overflows; AlpRd is checked bit-exact (full IEEE-754 content, ±0 canonicalized to match the ALP family). ~100 cases per codec/width. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent dbe44aa commit a2cf344

1 file changed

Lines changed: 216 additions & 0 deletions

File tree

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
package io.github.dfa1.vortex.writer.encode;
2+
3+
import io.github.dfa1.vortex.core.DType;
4+
import io.github.dfa1.vortex.encoding.DTypes;
5+
import io.github.dfa1.vortex.reader.ReadRegistry;
6+
import io.github.dfa1.vortex.reader.array.Array;
7+
import io.github.dfa1.vortex.reader.array.DoubleArray;
8+
import io.github.dfa1.vortex.reader.array.FloatArray;
9+
import io.github.dfa1.vortex.reader.array.IntArray;
10+
import io.github.dfa1.vortex.reader.array.LongArray;
11+
import io.github.dfa1.vortex.reader.decode.AlpRdEncodingDecoder;
12+
import io.github.dfa1.vortex.reader.decode.BitpackedEncodingDecoder;
13+
import io.github.dfa1.vortex.reader.decode.DecodeContext;
14+
import io.github.dfa1.vortex.reader.decode.DeltaEncodingDecoder;
15+
import io.github.dfa1.vortex.reader.decode.EncodingDecoder;
16+
import io.github.dfa1.vortex.reader.decode.FrameOfReferenceEncodingDecoder;
17+
import io.github.dfa1.vortex.reader.decode.PrimitiveEncodingDecoder;
18+
import io.github.dfa1.vortex.reader.decode.TestRegistry;
19+
import io.github.dfa1.vortex.reader.decode.ZigZagEncodingDecoder;
20+
import org.junit.jupiter.params.ParameterizedTest;
21+
import org.junit.jupiter.params.provider.MethodSource;
22+
23+
import java.util.Random;
24+
import java.util.stream.Stream;
25+
26+
import static org.assertj.core.api.Assertions.assertThat;
27+
28+
/// Property-based `decode(encode(x)) == x` round-trips for the lossless transform encodings.
29+
///
30+
/// Seeded random inputs (plus curated edges: 0, ±1, MIN/MAX, near-extremes) flow through the
31+
/// real encoder → the writer's `EncodeResult` → the reader's decoder, asserting exact identity.
32+
/// Integer encodings (Delta, FrameOfReference, ZigZag) are checked for value identity including
33+
/// the wrap-around cases where the internal subtraction/shift overflows; AlpRd (the real-double
34+
/// bit-split codec) is checked **bit-exact** since it stores the full IEEE-754 content.
35+
class RoundTripPropertyTest {
36+
37+
private static final DeltaEncodingEncoder DELTA = new DeltaEncodingEncoder();
38+
private static final FrameOfReferenceEncodingEncoder FOR = new FrameOfReferenceEncodingEncoder();
39+
private static final ZigZagEncodingEncoder ZIGZAG = new ZigZagEncodingEncoder();
40+
private static final AlpRdEncodingEncoder ALPRD = new AlpRdEncodingEncoder();
41+
42+
private static final ReadRegistry REGISTRY = TestRegistry.ofDecoders(
43+
new DeltaEncodingDecoder(), new FrameOfReferenceEncodingDecoder(),
44+
new ZigZagEncodingDecoder(), new AlpRdEncodingDecoder(),
45+
new BitpackedEncodingDecoder(), new PrimitiveEncodingDecoder());
46+
47+
private static Array roundTrip(EncodingEncoder encoder, EncodingDecoder decoder, DType dtype, Object data, int n) {
48+
EncodeResult enc = encoder.encode(dtype, data, EncodeTestHelper.testCtx());
49+
DecodeContext ctx = DecodeTestHelper.toDecodeContext(enc, n, dtype, REGISTRY);
50+
return decoder.decode(ctx);
51+
}
52+
53+
// ── Delta ──────────────────────────────────────────────────────────────────
54+
55+
@ParameterizedTest
56+
@MethodSource("longArrays")
57+
void delta_i64(long[] data) {
58+
assertLongs(roundTrip(DELTA, new DeltaEncodingDecoder(), DTypes.I64, data, data.length), data);
59+
}
60+
61+
@ParameterizedTest
62+
@MethodSource("intArrays")
63+
void delta_i32(int[] data) {
64+
assertInts(roundTrip(DELTA, new DeltaEncodingDecoder(), DTypes.I32, data, data.length), data);
65+
}
66+
67+
// ── FrameOfReference ───────────────────────────────────────────────────────
68+
69+
@ParameterizedTest
70+
@MethodSource("longArrays")
71+
void frameOfReference_i64(long[] data) {
72+
assertLongs(roundTrip(FOR, new FrameOfReferenceEncodingDecoder(), DTypes.I64, data, data.length), data);
73+
}
74+
75+
@ParameterizedTest
76+
@MethodSource("intArrays")
77+
void frameOfReference_i32(int[] data) {
78+
assertInts(roundTrip(FOR, new FrameOfReferenceEncodingDecoder(), DTypes.I32, data, data.length), data);
79+
}
80+
81+
// ── ZigZag ─────────────────────────────────────────────────────────────────
82+
83+
@ParameterizedTest
84+
@MethodSource("longArrays")
85+
void zigzag_i64(long[] data) {
86+
assertLongs(roundTrip(ZIGZAG, new ZigZagEncodingDecoder(), DTypes.I64, data, data.length), data);
87+
}
88+
89+
@ParameterizedTest
90+
@MethodSource("intArrays")
91+
void zigzag_i32(int[] data) {
92+
assertInts(roundTrip(ZIGZAG, new ZigZagEncodingDecoder(), DTypes.I32, data, data.length), data);
93+
}
94+
95+
// ── AlpRd (bit-exact) ──────────────────────────────────────────────────────
96+
97+
@ParameterizedTest
98+
@MethodSource("doubleArrays")
99+
void alpRd_f64_bitExact(double[] data) {
100+
DoubleArray r = (DoubleArray) roundTrip(ALPRD, new AlpRdEncodingDecoder(), DTypes.F64, data, data.length);
101+
for (int i = 0; i < data.length; i++) {
102+
assertThat(canon(r.getDouble(i))).as("index %d value %s", i, data[i]).isEqualTo(canon(data[i]));
103+
}
104+
}
105+
106+
@ParameterizedTest
107+
@MethodSource("floatArrays")
108+
void alpRd_f32_bitExact(float[] data) {
109+
FloatArray r = (FloatArray) roundTrip(ALPRD, new AlpRdEncodingDecoder(), DTypes.F32, data, data.length);
110+
for (int i = 0; i < data.length; i++) {
111+
assertThat(canon(r.getFloat(i))).as("index %d value %s", i, data[i]).isEqualTo(canon(data[i]));
112+
}
113+
}
114+
115+
// ── assertions ─────────────────────────────────────────────────────────────
116+
117+
private static void assertLongs(Array result, long[] expected) {
118+
LongArray a = (LongArray) result;
119+
for (int i = 0; i < expected.length; i++) {
120+
assertThat(a.getLong(i)).as("index %d", i).isEqualTo(expected[i]);
121+
}
122+
}
123+
124+
private static void assertInts(Array result, int[] expected) {
125+
IntArray a = (IntArray) result;
126+
for (int i = 0; i < expected.length; i++) {
127+
assertThat(a.getInt(i)).as("index %d", i).isEqualTo(expected[i]);
128+
}
129+
}
130+
131+
private static long canon(double d) {
132+
return d == 0.0 ? 0L : Double.doubleToRawLongBits(d); // ±0 collapse, matches ALP family
133+
}
134+
135+
private static int canon(float f) {
136+
return f == 0.0f ? 0 : Float.floatToRawIntBits(f);
137+
}
138+
139+
// ── generators (seeded) ────────────────────────────────────────────────────
140+
141+
static Stream<long[]> longArrays() {
142+
Random rng = new Random(0xD17AL);
143+
Stream.Builder<long[]> b = Stream.builder();
144+
b.add(new long[]{0L, 1L, -1L, Long.MIN_VALUE, Long.MAX_VALUE, Long.MIN_VALUE + 1, Long.MAX_VALUE - 1});
145+
for (int n = 0; n < 50; n++) {
146+
int len = 1 + rng.nextInt(80);
147+
long[] a = new long[len];
148+
for (int i = 0; i < len; i++) {
149+
a[i] = switch (rng.nextInt(3)) {
150+
case 0 -> rng.nextLong(); // full-range: forces wrap-around
151+
case 1 -> (long) (rng.nextInt(2001) - 1000); // small near-constant: clean delta/FoR
152+
default -> 1_000_000L + rng.nextInt(1000); // tight cluster around a reference
153+
};
154+
}
155+
b.add(a);
156+
}
157+
return b.build();
158+
}
159+
160+
static Stream<int[]> intArrays() {
161+
Random rng = new Random(0xD17BL);
162+
Stream.Builder<int[]> b = Stream.builder();
163+
b.add(new int[]{0, 1, -1, Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE + 1, Integer.MAX_VALUE - 1});
164+
for (int n = 0; n < 50; n++) {
165+
int len = 1 + rng.nextInt(80);
166+
int[] a = new int[len];
167+
for (int i = 0; i < len; i++) {
168+
a[i] = switch (rng.nextInt(3)) {
169+
case 0 -> rng.nextInt();
170+
case 1 -> rng.nextInt(2001) - 1000;
171+
default -> 1_000_000 + rng.nextInt(1000);
172+
};
173+
}
174+
b.add(a);
175+
}
176+
return b.build();
177+
}
178+
179+
static Stream<double[]> doubleArrays() {
180+
Random rng = new Random(0xA1F64L);
181+
Stream.Builder<double[]> b = Stream.builder();
182+
b.add(new double[]{0.0, -0.0, 1.0, -1.0, 0.5, -0.25, 3.14159,
183+
Double.MIN_VALUE, Double.MAX_VALUE, Double.MIN_NORMAL, 1e-300, 1e300,
184+
Double.NaN, Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY});
185+
for (int n = 0; n < 50; n++) {
186+
int len = 1 + rng.nextInt(80);
187+
double[] a = new double[len];
188+
for (int i = 0; i < len; i++) {
189+
a[i] = rng.nextInt(2) == 0
190+
? Double.longBitsToDouble(rng.nextLong())
191+
: rng.nextGaussian() * Math.pow(10, rng.nextInt(20) - 10);
192+
}
193+
b.add(a);
194+
}
195+
return b.build();
196+
}
197+
198+
static Stream<float[]> floatArrays() {
199+
Random rng = new Random(0xA1F32L);
200+
Stream.Builder<float[]> b = Stream.builder();
201+
b.add(new float[]{0.0f, -0.0f, 1.0f, -1.0f, 0.5f, -0.25f, 3.14159f,
202+
Float.MIN_VALUE, Float.MAX_VALUE, Float.MIN_NORMAL, 1e-30f, 1e30f,
203+
Float.NaN, Float.POSITIVE_INFINITY, Float.NEGATIVE_INFINITY});
204+
for (int n = 0; n < 50; n++) {
205+
int len = 1 + rng.nextInt(80);
206+
float[] a = new float[len];
207+
for (int i = 0; i < len; i++) {
208+
a[i] = rng.nextInt(2) == 0
209+
? Float.intBitsToFloat(rng.nextInt())
210+
: (float) (rng.nextGaussian() * Math.pow(10, rng.nextInt(12) - 6));
211+
}
212+
b.add(a);
213+
}
214+
return b.build();
215+
}
216+
}

0 commit comments

Comments
 (0)