Skip to content

Commit fe0d537

Browse files
dfa1claude
andcommitted
test(integration): replace jqwik with seeded @ParameterizedTest + RandomArrays
jqwik team opposes AI-assisted development. Drop the dependency entirely. - Add RandomArrays utility with seeded Random generators for long[], double[], float[], String[] (ascii, varBinView, u16Dict, unicode) - Convert 7 @Property tests to @ParameterizedTest + @MethodSource - Remove jqwik from integration/pom.xml and root pom.xml (property + managed dep) - Remove .jqwik-database from .gitignore - Update CLAUDE.md and README.md to reflect the new approach Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent b30baf2 commit fe0d537

7 files changed

Lines changed: 189 additions & 99 deletions

File tree

.gitignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,3 @@ target/
55
*.iml
66
.DS_Store
77
dependency-reduced-pom.xml
8-
.jqwik-database

CLAUDE.md

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ The decoder reads back via `ctx.metadata()`, not `ctx.buffer(n)`.
163163
`given` and `then` — not `willReturn`/`willThrow`.
164164
- Prefer `@ParameterizedTest` over copy-pasting tests. Use `@ValueSource` when possible; `@ArgumentsSource` when more
165165
structure needed (test case must have a name).
166-
- Use property-based tests (`@Property`) for encoding/decoding logic where input space is large — they find corner cases that example tests miss.
166+
- Use `@ParameterizedTest` with seeded random generators for encoding/decoding logic where input space is large — they find corner cases that example tests miss.
167167
- Acceptance tests run the built jar end-to-end with hosh scripts.
168168
- Use `@Nested` to group related tests by scenario or feature within a test class:
169169
```java
@@ -175,10 +175,17 @@ The decoder reads back via `ctx.metadata()`, not `ctx.buffer(n)`.
175175
`@BeforeEach` inside a `@Nested` class applies only to that group. Private helpers go
176176
at the end of the class they serve, after all `@Test` methods.
177177

178-
## Property-Based Testing (jqwik)
178+
## Random-data parameterized tests
179179

180-
jqwik 1.9.3 works with JUnit Jupiter 5.11.x (JUnit Platform 1.x). Use `@Property` + `@ForAll` for parameters,
181-
`@Provide` for custom arbitraries, `Assume.that(...)` for preconditions.
180+
Use `@ParameterizedTest` + `@MethodSource` for random-input coverage. Put generators in `RandomArrays` (integration module)
181+
or a similar utility class. Static provider methods in the test class delegate to the generator:
182182

183-
Keep `tries` low (10–20) for integration tests that involve file I/O or JNI; unit-level properties can use the
184-
default (100).
183+
```java
184+
static Stream<long[]> i64ArrayProvider() { return RandomArrays.i64Arrays(30); }
185+
186+
@ParameterizedTest
187+
@MethodSource("i64ArrayProvider")
188+
void roundTrips(long[] data) { ... }
189+
```
190+
191+
Keep counts low (10–30) for integration tests that involve file I/O or JNI.

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ another implementation cannot decode.
237237
238238
The `integration` module addresses this by using the Rust JNI reader as a **test oracle**:
239239
Java writes a file, the Rust reader decodes it, and the values are compared exactly.
240-
[Property-based testing](https://jqwik.net/) (jqwik) generates large, diverse inputs automatically,
240+
Seeded random parameterized tests generate large, diverse inputs automatically,
241241
covering edge cases no hand-written test would anticipate.
242242
243243
This combination caught two real bugs in ALP floating-point encoding:

integration/pom.xml

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,6 @@
5252
<artifactId>assertj-core</artifactId>
5353
<scope>test</scope>
5454
</dependency>
55-
<dependency>
56-
<groupId>net.jqwik</groupId>
57-
<artifactId>jqwik</artifactId>
58-
<scope>test</scope>
59-
</dependency>
6055
<dependency>
6156
<groupId>org.slf4j</groupId>
6257
<artifactId>slf4j-simple</artifactId>

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

Lines changed: 50 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,6 @@
2828
import io.github.dfa1.vortex.encoding.ZstdEncoding;
2929
import io.github.dfa1.vortex.writer.VortexWriter;
3030
import io.github.dfa1.vortex.writer.WriteOptions;
31-
import net.jqwik.api.Arbitraries;
32-
import net.jqwik.api.Arbitrary;
33-
import net.jqwik.api.ForAll;
34-
import net.jqwik.api.Property;
35-
import net.jqwik.api.Provide;
3631
import org.apache.arrow.memory.BufferAllocator;
3732
import org.apache.arrow.vector.BigIntVector;
3833
import org.apache.arrow.vector.BitVector;
@@ -46,6 +41,8 @@
4641
import org.junit.jupiter.api.Disabled;
4742
import org.junit.jupiter.api.Test;
4843
import org.junit.jupiter.api.io.TempDir;
44+
import org.junit.jupiter.params.ParameterizedTest;
45+
import org.junit.jupiter.params.provider.MethodSource;
4946

5047
import java.io.IOException;
5148
import java.io.UncheckedIOException;
@@ -58,6 +55,7 @@
5855
import java.util.Comparator;
5956
import java.util.List;
6057
import java.util.Map;
58+
import java.util.stream.Stream;
6159

6260
import static org.assertj.core.api.Assertions.assertThat;
6361

@@ -452,9 +450,9 @@ void javaWriter_jniReader_varBinViewUtf8Column_mixed(@TempDir Path tmp) throws I
452450
}
453451

454452
/// VarBinView utf8 with arbitrary strings — exercises both inlined and referenced views.
455-
@Property(tries = 20)
456-
void prop_varBinView_utf8_roundTripsViaRust(
457-
@ForAll("varBinViewStringArrays") String[] data) throws IOException {
453+
@ParameterizedTest
454+
@MethodSource("varBinViewStringArrayProvider")
455+
void prop_varBinView_utf8_roundTripsViaRust(String[] data) throws IOException {
458456
Path tmp = Files.createTempDirectory("vortex-pbt-varbinview");
459457
try {
460458
Path file = tmp.resolve("pbt_varbinview_utf8.vtx");
@@ -551,13 +549,13 @@ void javaWriter_jniReader_cascading_ohlc_columnProjection(@TempDir Path tmp) thr
551549
assertThat(volumes).containsExactlyInAnyOrder(expected);
552550
}
553551

554-
// ── Property-based tests ──────────────────────────────────────────────────
552+
// ── Parameterized random-data tests ─────────────────────────────────────
555553

556554
/// Dict utf8 with arbitrary strings (small dict → U8 codes).
557555
/// Validates that random string data survives Java dict-encode → Rust JNI read.
558-
@Property(tries = 20)
559-
void prop_dictUtf8_ascii_roundTripsViaRust(
560-
@ForAll("asciiStringArrays") String[] data) throws IOException {
556+
@ParameterizedTest
557+
@MethodSource("asciiStringArrayProvider")
558+
void prop_dictUtf8_ascii_roundTripsViaRust(String[] data) throws IOException {
561559
Path tmp = Files.createTempDirectory("vortex-pbt-ascii");
562560
try {
563561
Path file = tmp.resolve("pbt_dict_utf8_ascii.vtx");
@@ -573,9 +571,9 @@ void prop_dictUtf8_ascii_roundTripsViaRust(
573571
}
574572

575573
/// Dict utf8 with 257+ unique strings → forces U16 codes (crosses U8→U16 boundary at 256).
576-
@Property(tries = 10)
577-
void prop_dictUtf8_u16Codes_roundTripsViaRust(
578-
@ForAll("u16DictStringArrays") String[] data) throws IOException {
574+
@ParameterizedTest
575+
@MethodSource("u16DictStringArrayProvider")
576+
void prop_dictUtf8_u16Codes_roundTripsViaRust(String[] data) throws IOException {
579577
Path tmp = Files.createTempDirectory("vortex-pbt-u16");
580578
try {
581579
Path file = tmp.resolve("pbt_dict_utf8_u16.vtx");
@@ -590,10 +588,10 @@ void prop_dictUtf8_u16Codes_roundTripsViaRust(
590588
}
591589
}
592590

593-
/// Dict utf8 with unicode strings (multi-byte UTF-8, emoji, CJK).
594-
@Property(tries = 20)
595-
void prop_dictUtf8_unicode_roundTripsViaRust(
596-
@ForAll("unicodeStringArrays") String[] data) throws IOException {
591+
/// Dict utf8 with unicode strings (multi-byte UTF-8, CJK).
592+
@ParameterizedTest
593+
@MethodSource("unicodeStringArrayProvider")
594+
void prop_dictUtf8_unicode_roundTripsViaRust(String[] data) throws IOException {
597595
Path tmp = Files.createTempDirectory("vortex-pbt-unicode");
598596
try {
599597
Path file = tmp.resolve("pbt_dict_utf8_unicode.vtx");
@@ -609,8 +607,9 @@ void prop_dictUtf8_unicode_roundTripsViaRust(
609607
}
610608

611609
/// I64 column: full Long range (MIN_VALUE, MAX_VALUE, negatives), empty and large arrays.
612-
@Property(tries = 30)
613-
void prop_i64_roundTripsViaRust(@ForAll("i64Arrays") long[] data) throws IOException {
610+
@ParameterizedTest
611+
@MethodSource("i64ArrayProvider")
612+
void prop_i64_roundTripsViaRust(long[] data) throws IOException {
614613
Path tmp = Files.createTempDirectory("vortex-pbt-i64");
615614
try {
616615
Path file = tmp.resolve("pbt_i64.vtx");
@@ -625,52 +624,11 @@ void prop_i64_roundTripsViaRust(@ForAll("i64Arrays") long[] data) throws IOExcep
625624
}
626625
}
627626

628-
@Provide
629-
Arbitrary<String[]> varBinViewStringArrays() {
630-
// Strings 0–30 chars: spans both inlined (≤12 bytes) and referenced (>12 bytes) paths
631-
Arbitrary<String> strings = Arbitraries.strings()
632-
.alpha()
633-
.ofMinLength(0).ofMaxLength(30);
634-
return strings.array(String[].class).ofMinSize(0).ofMaxSize(1_000);
635-
}
636-
637-
@Provide
638-
Arbitrary<String[]> asciiStringArrays() {
639-
Arbitrary<String> strings = Arbitraries.strings()
640-
.alpha()
641-
.ofMinLength(0).ofMaxLength(100);
642-
return strings.array(String[].class).ofMinSize(0).ofMaxSize(5_000);
643-
}
644-
645-
@Provide
646-
Arbitrary<String[]> u16DictStringArrays() {
647-
// 257–5000 unique strings → U16 codes territory
648-
Arbitrary<String> strings = Arbitraries.strings()
649-
.alpha()
650-
.ofMinLength(3).ofMaxLength(20);
651-
return strings.list().ofMinSize(257).ofMaxSize(5_000)
652-
.map(list -> list.stream().distinct().toArray(String[]::new))
653-
.filter(arr -> arr.length >= 257);
654-
}
655-
656-
@Provide
657-
Arbitrary<String[]> unicodeStringArrays() {
658-
Arbitrary<String> strings = Arbitraries.strings()
659-
.withCharRange('\u4E00', '\uD7FF')
660-
.ofMinLength(0).ofMaxLength(50);
661-
return strings.array(String[].class).ofMinSize(0).ofMaxSize(1_000);
662-
}
663-
664-
@Provide
665-
Arbitrary<long[]> i64Arrays() {
666-
return Arbitraries.longs()
667-
.array(long[].class)
668-
.ofMinSize(0).ofMaxSize(10_000);
669-
}
670627

671628
/// F64 column: finite doubles (ALP encoding), empty and large arrays.
672-
@Property(tries = 30)
673-
void prop_f64_roundTripsViaRust(@ForAll("f64Arrays") double[] data) throws IOException {
629+
@ParameterizedTest
630+
@MethodSource("f64ArrayProvider")
631+
void prop_f64_roundTripsViaRust(double[] data) throws IOException {
674632
Path tmp = Files.createTempDirectory("vortex-pbt-f64");
675633
try {
676634
Path file = tmp.resolve("pbt_f64.vtx");
@@ -686,8 +644,9 @@ void prop_f64_roundTripsViaRust(@ForAll("f64Arrays") double[] data) throws IOExc
686644
}
687645

688646
/// F32 column: finite floats, empty and large arrays.
689-
@Property(tries = 30)
690-
void prop_f32_roundTripsViaRust(@ForAll("f32Arrays") float[] data) throws IOException {
647+
@ParameterizedTest
648+
@MethodSource("f32ArrayProvider")
649+
void prop_f32_roundTripsViaRust(float[] data) throws IOException {
691650
Path tmp = Files.createTempDirectory("vortex-pbt-f32");
692651
try {
693652
Path file = tmp.resolve("pbt_f32.vtx");
@@ -702,20 +661,32 @@ void prop_f32_roundTripsViaRust(@ForAll("f32Arrays") float[] data) throws IOExce
702661
}
703662
}
704663

705-
@Provide
706-
Arbitrary<double[]> f64Arrays() {
707-
return Arbitraries.doubles()
708-
.filter(Double::isFinite)
709-
.array(double[].class)
710-
.ofMinSize(0).ofMaxSize(5_000);
664+
static Stream<String[]> varBinViewStringArrayProvider() {
665+
return RandomArrays.varBinViewStringArrays(20);
666+
}
667+
668+
static Stream<String[]> asciiStringArrayProvider() {
669+
return RandomArrays.asciiStringArrays(20);
670+
}
671+
672+
static Stream<String[]> u16DictStringArrayProvider() {
673+
return RandomArrays.u16DictStringArrays(10);
674+
}
675+
676+
static Stream<String[]> unicodeStringArrayProvider() {
677+
return RandomArrays.unicodeStringArrays(20);
678+
}
679+
680+
static Stream<long[]> i64ArrayProvider() {
681+
return RandomArrays.i64Arrays(30);
682+
}
683+
684+
static Stream<double[]> f64ArrayProvider() {
685+
return RandomArrays.f64Arrays(30);
711686
}
712687

713-
@Provide
714-
Arbitrary<float[]> f32Arrays() {
715-
return Arbitraries.floats()
716-
.filter(Float::isFinite)
717-
.array(float[].class)
718-
.ofMinSize(0).ofMaxSize(5_000);
688+
static Stream<float[]> f32ArrayProvider() {
689+
return RandomArrays.f32Arrays(30);
719690
}
720691

721692
/// F64: NaN and Inf go to ALP patches (raw bits). containsExactly fails for NaN — use bitwise comparison.
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
package io.github.dfa1.vortex.integration;
2+
3+
import java.util.LinkedHashSet;
4+
import java.util.Random;
5+
import java.util.Set;
6+
import java.util.stream.IntStream;
7+
import java.util.stream.Stream;
8+
9+
final class RandomArrays {
10+
11+
private static final long SEED = 0xCAFEBABEL;
12+
13+
static Stream<long[]> i64Arrays(int count) {
14+
Random rng = new Random(SEED);
15+
return IntStream.range(0, count).mapToObj(i -> {
16+
int size = rng.nextInt(10_001);
17+
long[] arr = new long[size];
18+
for (int j = 0; j < size; j++) {
19+
arr[j] = rng.nextLong();
20+
}
21+
return arr;
22+
});
23+
}
24+
25+
static Stream<double[]> f64Arrays(int count) {
26+
Random rng = new Random(SEED);
27+
return IntStream.range(0, count).mapToObj(i -> {
28+
int size = rng.nextInt(5_001);
29+
double[] arr = new double[size];
30+
for (int j = 0; j < size; j++) {
31+
double d;
32+
do {
33+
d = Double.longBitsToDouble(rng.nextLong());
34+
} while (!Double.isFinite(d));
35+
arr[j] = d;
36+
}
37+
return arr;
38+
});
39+
}
40+
41+
static Stream<float[]> f32Arrays(int count) {
42+
Random rng = new Random(SEED);
43+
return IntStream.range(0, count).mapToObj(i -> {
44+
int size = rng.nextInt(5_001);
45+
float[] arr = new float[size];
46+
for (int j = 0; j < size; j++) {
47+
float f;
48+
do {
49+
f = Float.intBitsToFloat(rng.nextInt());
50+
} while (!Float.isFinite(f));
51+
arr[j] = f;
52+
}
53+
return arr;
54+
});
55+
}
56+
57+
static Stream<String[]> asciiStringArrays(int count) {
58+
Random rng = new Random(SEED);
59+
return IntStream.range(0, count).mapToObj(i -> {
60+
int size = rng.nextInt(5_001);
61+
String[] arr = new String[size];
62+
for (int j = 0; j < size; j++) {
63+
arr[j] = randomAlphaString(rng, 0, 100);
64+
}
65+
return arr;
66+
});
67+
}
68+
69+
static Stream<String[]> varBinViewStringArrays(int count) {
70+
Random rng = new Random(SEED);
71+
return IntStream.range(0, count).mapToObj(i -> {
72+
int size = rng.nextInt(1_001);
73+
String[] arr = new String[size];
74+
for (int j = 0; j < size; j++) {
75+
arr[j] = randomAlphaString(rng, 0, 30);
76+
}
77+
return arr;
78+
});
79+
}
80+
81+
static Stream<String[]> u16DictStringArrays(int count) {
82+
Random rng = new Random(SEED);
83+
return IntStream.range(0, count).mapToObj(i -> {
84+
int targetSize = 257 + rng.nextInt(5_000 - 257 + 1);
85+
Set<String> unique = new LinkedHashSet<>();
86+
while (unique.size() < targetSize) {
87+
unique.add(randomAlphaString(rng, 3, 20));
88+
}
89+
return unique.toArray(String[]::new);
90+
});
91+
}
92+
93+
static Stream<String[]> unicodeStringArrays(int count) {
94+
Random rng = new Random(SEED);
95+
return IntStream.range(0, count).mapToObj(i -> {
96+
int size = rng.nextInt(1_001);
97+
StringBuilder sb = new StringBuilder();
98+
String[] arr = new String[size];
99+
for (int j = 0; j < size; j++) {
100+
arr[j] = randomCjkString(rng, 0, 50, sb);
101+
}
102+
return arr;
103+
});
104+
}
105+
106+
private static String randomAlphaString(Random rng, int minLen, int maxLen) {
107+
int len = minLen + (maxLen > minLen ? rng.nextInt(maxLen - minLen + 1) : 0);
108+
char[] chars = new char[len];
109+
for (int i = 0; i < len; i++) {
110+
chars[i] = (char) ('a' + rng.nextInt(26));
111+
}
112+
return new String(chars);
113+
}
114+
115+
private static String randomCjkString(Random rng, int minLen, int maxLen, StringBuilder sb) {
116+
int len = minLen + (maxLen > minLen ? rng.nextInt(maxLen - minLen + 1) : 0);
117+
sb.setLength(0);
118+
for (int i = 0; i < len; i++) {
119+
sb.appendCodePoint('一' + rng.nextInt('퟿' - '一' + 1));
120+
}
121+
return sb.toString();
122+
}
123+
124+
private RandomArrays() {}
125+
}

0 commit comments

Comments
 (0)