|
14 | 14 | import org.junit.jupiter.params.provider.MethodSource; |
15 | 15 |
|
16 | 16 | import java.lang.foreign.Arena; |
| 17 | +import java.lang.foreign.MemorySegment; |
| 18 | +import java.util.Random; |
17 | 19 | import java.util.stream.LongStream; |
18 | 20 | import java.util.stream.Stream; |
19 | 21 |
|
@@ -257,11 +259,176 @@ void encodeDecode_f64_isLossless(String name, double[] data) { |
257 | 259 |
|
258 | 260 | /// IntMult-favorable data: base 100 + small random adjustment ([0,100)). |
259 | 261 | private static long[] intMultPriceData(int n) { |
260 | | - java.util.Random rng = new java.util.Random(42L); |
| 262 | + Random rng = new Random(42L); |
261 | 263 | long[] arr = new long[n]; |
262 | 264 | for (int i = 0; i < n; i++) { |
263 | 265 | arr[i] = (10L + rng.nextInt(1000)) * 100L + rng.nextInt(100); |
264 | 266 | } |
265 | 267 | return arr; |
266 | 268 | } |
| 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 | + } |
267 | 434 | } |
0 commit comments