|
20 | 20 | import org.apache.arrow.vector.VarCharVector; |
21 | 21 | import org.apache.arrow.vector.VectorSchemaRoot; |
22 | 22 | import org.apache.arrow.vector.ipc.ArrowReader; |
| 23 | +import net.jqwik.api.Arbitraries; |
| 24 | +import net.jqwik.api.Arbitrary; |
| 25 | +import net.jqwik.api.ForAll; |
| 26 | +import net.jqwik.api.Property; |
| 27 | +import net.jqwik.api.Provide; |
| 28 | +import net.jqwik.api.constraints.Size; |
23 | 29 | import org.junit.jupiter.api.Test; |
24 | 30 | import org.junit.jupiter.api.io.TempDir; |
25 | 31 |
|
26 | 32 | import java.io.IOException; |
27 | 33 | import java.nio.channels.FileChannel; |
| 34 | +import java.nio.file.Files; |
28 | 35 | import java.nio.file.Path; |
29 | 36 | import java.nio.file.StandardOpenOption; |
30 | 37 | import java.util.ArrayList; |
31 | 38 | import java.util.Arrays; |
| 39 | +import java.util.Comparator; |
32 | 40 | import java.util.List; |
33 | 41 | import java.util.Map; |
34 | 42 |
|
@@ -352,4 +360,113 @@ void javaWriter_jniReader_cascading_ohlc_columnProjection(@TempDir Path tmp) thr |
352 | 360 | long[] expected = batches.stream().flatMapToLong(b -> Arrays.stream(b.volume())).toArray(); |
353 | 361 | assertThat(volumes).containsExactlyInAnyOrder(expected); |
354 | 362 | } |
| 363 | + |
| 364 | + // ── Property-based tests ────────────────────────────────────────────────── |
| 365 | + |
| 366 | + /// Dict utf8 with arbitrary strings (small dict → U8 codes). |
| 367 | + /// Validates that random string data survives Java dict-encode → Rust JNI read. |
| 368 | + @Property(tries = 20) |
| 369 | + void prop_dictUtf8_ascii_roundTripsViaRust( |
| 370 | + @ForAll("asciiStringArrays") String[] data) throws IOException { |
| 371 | + Path tmp = Files.createTempDirectory("vortex-pbt-ascii"); |
| 372 | + try { |
| 373 | + Path file = tmp.resolve("pbt_dict_utf8_ascii.vtx"); |
| 374 | + try (var ch = FileChannel.open(file, StandardOpenOption.CREATE, StandardOpenOption.WRITE); |
| 375 | + var sut = VortexWriter.create(ch, STRING_SCHEMA, WriteOptions.defaults())) { |
| 376 | + sut.writeChunk(Map.of("s", data)); |
| 377 | + } |
| 378 | + String[] decoded = readStringColumn(file, "s"); |
| 379 | + assertThat(decoded).containsExactly(data); |
| 380 | + } finally { |
| 381 | + deleteDir(tmp); |
| 382 | + } |
| 383 | + } |
| 384 | + |
| 385 | + /// Dict utf8 with 257+ unique strings → forces U16 codes (crosses U8→U16 boundary at 256). |
| 386 | + @Property(tries = 10) |
| 387 | + void prop_dictUtf8_u16Codes_roundTripsViaRust( |
| 388 | + @ForAll("u16DictStringArrays") String[] data) throws IOException { |
| 389 | + Path tmp = Files.createTempDirectory("vortex-pbt-u16"); |
| 390 | + try { |
| 391 | + Path file = tmp.resolve("pbt_dict_utf8_u16.vtx"); |
| 392 | + try (var ch = FileChannel.open(file, StandardOpenOption.CREATE, StandardOpenOption.WRITE); |
| 393 | + var sut = VortexWriter.create(ch, STRING_SCHEMA, WriteOptions.defaults())) { |
| 394 | + sut.writeChunk(Map.of("s", data)); |
| 395 | + } |
| 396 | + String[] decoded = readStringColumn(file, "s"); |
| 397 | + assertThat(decoded).containsExactly(data); |
| 398 | + } finally { |
| 399 | + deleteDir(tmp); |
| 400 | + } |
| 401 | + } |
| 402 | + |
| 403 | + /// Dict utf8 with unicode strings (multi-byte UTF-8, emoji, CJK). |
| 404 | + @Property(tries = 20) |
| 405 | + void prop_dictUtf8_unicode_roundTripsViaRust( |
| 406 | + @ForAll("unicodeStringArrays") String[] data) throws IOException { |
| 407 | + Path tmp = Files.createTempDirectory("vortex-pbt-unicode"); |
| 408 | + try { |
| 409 | + Path file = tmp.resolve("pbt_dict_utf8_unicode.vtx"); |
| 410 | + try (var ch = FileChannel.open(file, StandardOpenOption.CREATE, StandardOpenOption.WRITE); |
| 411 | + var sut = VortexWriter.create(ch, STRING_SCHEMA, WriteOptions.defaults())) { |
| 412 | + sut.writeChunk(Map.of("s", data)); |
| 413 | + } |
| 414 | + String[] decoded = readStringColumn(file, "s"); |
| 415 | + assertThat(decoded).containsExactly(data); |
| 416 | + } finally { |
| 417 | + deleteDir(tmp); |
| 418 | + } |
| 419 | + } |
| 420 | + |
| 421 | + /// I64 column: arbitrary longs survive Java write → Rust JNI read. |
| 422 | + @Property(tries = 20) |
| 423 | + void prop_i64_roundTripsViaRust( |
| 424 | + @ForAll @Size(min = 1, max = 500) List<Long> values) throws IOException { |
| 425 | + Path tmp = Files.createTempDirectory("vortex-pbt-i64"); |
| 426 | + try { |
| 427 | + long[] data = values.stream().mapToLong(Long::longValue).toArray(); |
| 428 | + Path file = tmp.resolve("pbt_i64.vtx"); |
| 429 | + try (var ch = FileChannel.open(file, StandardOpenOption.CREATE, StandardOpenOption.WRITE); |
| 430 | + var sut = VortexWriter.create(ch, TS_SCHEMA, WriteOptions.defaults())) { |
| 431 | + sut.writeChunk(Map.of("ts", data)); |
| 432 | + } |
| 433 | + long[] decoded = readLongColumn(file, "ts"); |
| 434 | + assertThat(decoded).containsExactly(data); |
| 435 | + } finally { |
| 436 | + deleteDir(tmp); |
| 437 | + } |
| 438 | + } |
| 439 | + |
| 440 | + @Provide |
| 441 | + Arbitrary<String[]> asciiStringArrays() { |
| 442 | + Arbitrary<String> strings = Arbitraries.strings() |
| 443 | + .alpha() |
| 444 | + .ofMinLength(0).ofMaxLength(20); |
| 445 | + return strings.array(String[].class).ofMinSize(1).ofMaxSize(200); |
| 446 | + } |
| 447 | + |
| 448 | + @Provide |
| 449 | + Arbitrary<String[]> u16DictStringArrays() { |
| 450 | + // 257 unique strings guaranteed by @UniqueElements on a list, then a suffix of repeats |
| 451 | + Arbitrary<String> strings = Arbitraries.strings() |
| 452 | + .alpha() |
| 453 | + .ofMinLength(3).ofMaxLength(12); |
| 454 | + return strings.list().ofMinSize(257).ofMaxSize(300) |
| 455 | + .map(list -> list.stream().distinct().limit(300).toArray(String[]::new)) |
| 456 | + .filter(arr -> arr.length >= 257); |
| 457 | + } |
| 458 | + |
| 459 | + @Provide |
| 460 | + Arbitrary<String[]> unicodeStringArrays() { |
| 461 | + Arbitrary<String> strings = Arbitraries.strings() |
| 462 | + .withCharRange('', '') |
| 463 | + .ofMinLength(1).ofMaxLength(10); |
| 464 | + return strings.array(String[].class).ofMinSize(1).ofMaxSize(100); |
| 465 | + } |
| 466 | + |
| 467 | + private static void deleteDir(Path dir) throws IOException { |
| 468 | + try (var walk = Files.walk(dir)) { |
| 469 | + walk.sorted(Comparator.reverseOrder()).forEach(p -> p.toFile().delete()); |
| 470 | + } |
| 471 | + } |
355 | 472 | } |
0 commit comments