Skip to content

Commit d828954

Browse files
dfa1claude
andcommitted
test: add jqwik property-based integration tests for dict utf8 + i64
Add jqwik 1.9.3 and four @Property tests in JavaWritesRustReadsIntegrationTest that validate Java-writer → Rust-JNI-reader round-trips across randomly generated inputs: - prop_dictUtf8_ascii_roundTripsViaRust (20 tries, U8 codes range) - prop_dictUtf8_u16Codes_roundTripsViaRust (10 tries, 257+ unique → U16 codes) - prop_dictUtf8_unicode_roundTripsViaRust (20 tries, multi-byte UTF-8) - prop_i64_roundTripsViaRust (20 tries, arbitrary long[]) Unit tests only cover Java→Java roundtrips; these properties catch wire-format divergence between the Java writer and the Rust JNI reader that unit tests miss. Fix stale CLAUDE.md note claiming jqwik doesn't work with JUnit Platform 1.x. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent ec81cab commit d828954

4 files changed

Lines changed: 132 additions & 5 deletions

File tree

CLAUDE.md

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -147,9 +147,8 @@ methods in the Rust source to get the exact protobuf schema, then implement from
147147

148148
## Property-Based Testing (jqwik)
149149

150-
**Known issue:** jqwik 1.9.3 targets JUnit Platform 1.x; project uses JUnit 6 (Platform 6.x). `@Property` tests compile
151-
and are structurally correct but the jqwik engine does not execute them at runtime.
152-
Track https://github.com/jqwik-team/jqwik/issues for jqwik 2.x.
150+
jqwik 1.9.3 works with JUnit Jupiter 5.11.x (JUnit Platform 1.x). Use `@Property` + `@ForAll` for parameters,
151+
`@Provide` for custom arbitraries, `Assume.that(...)` for preconditions.
153152

154-
Write property tests: `@Property` + `@ForAll` for parameters, `@Provide` for custom arbitraries, `Assume.that(...)` for
155-
preconditions.
153+
Keep `tries` low (10–20) for integration tests that involve file I/O or JNI; unit-level properties can use the
154+
default (100).

integration/pom.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,10 @@
5454
<groupId>org.assertj</groupId>
5555
<artifactId>assertj-core</artifactId>
5656
</dependency>
57+
<dependency>
58+
<groupId>net.jqwik</groupId>
59+
<artifactId>jqwik</artifactId>
60+
</dependency>
5761
</dependencies>
5862

5963
<build>

integration/src/test/java/io/github/dfa1/vortex/integration/JavaWritesRustReadsIntegrationTest.java

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,23 @@
2020
import org.apache.arrow.vector.VarCharVector;
2121
import org.apache.arrow.vector.VectorSchemaRoot;
2222
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;
2329
import org.junit.jupiter.api.Test;
2430
import org.junit.jupiter.api.io.TempDir;
2531

2632
import java.io.IOException;
2733
import java.nio.channels.FileChannel;
34+
import java.nio.file.Files;
2835
import java.nio.file.Path;
2936
import java.nio.file.StandardOpenOption;
3037
import java.util.ArrayList;
3138
import java.util.Arrays;
39+
import java.util.Comparator;
3240
import java.util.List;
3341
import java.util.Map;
3442

@@ -352,4 +360,113 @@ void javaWriter_jniReader_cascading_ohlc_columnProjection(@TempDir Path tmp) thr
352360
long[] expected = batches.stream().flatMapToLong(b -> Arrays.stream(b.volume())).toArray();
353361
assertThat(volumes).containsExactlyInAnyOrder(expected);
354362
}
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+
}
355472
}

pom.xml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
<junit.version>5.11.4</junit.version>
2626
<assertj.version>3.26.3</assertj.version>
2727
<mockito.version>5.14.2</mockito.version>
28+
<jqwik.version>1.9.3</jqwik.version>
2829
</properties>
2930

3031
<dependencyManagement>
@@ -72,6 +73,12 @@
7273
<version>${mockito.version}</version>
7374
<scope>test</scope>
7475
</dependency>
76+
<dependency>
77+
<groupId>net.jqwik</groupId>
78+
<artifactId>jqwik</artifactId>
79+
<version>${jqwik.version}</version>
80+
<scope>test</scope>
81+
</dependency>
7582
</dependencies>
7683
</dependencyManagement>
7784

0 commit comments

Comments
 (0)