Skip to content

Commit 31fea8e

Browse files
dfa1claude
andcommitted
test(compute): cover the kernel branches the oracles never reached
The Sonar quality gate fails on new-code coverage (75.25% < 80%), and the gap sits in exactly the branches the randomized oracles never generate: FusedFilterSum's ShortArray/ByteArray and FloatArray filter loops, the generic boxing path (composite and null-test predicates), the empty-range boundary-constant guards of the range lowering, the length-mismatch fail-fast, and Values' narrow-unsigned zero-extension and non-numeric boxes. FusedFilterSumTest's random filter/aggregate domains now include i16, i8 (each randomly unsigned) and f32 columns, and the predicate sweep adds And/Or composites and the null tests, driving the whole random space through the generic path as well. Pinned tests cover the mismatched-length throw and the Lt(min)/Gt(max) empty-range lowering. The new ValuesTest pins the boxing accessor directly: u8/u16/u32 zero-extension (0xFF must box as 255, never -1), signed sign-extension, the extension-dtype passthrough, Utf8/bool boxes, the no-scalar- accessor fail-fast, and masked reads. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 00fc0da commit 31fea8e

2 files changed

Lines changed: 224 additions & 7 deletions

File tree

reader/src/test/java/io/github/dfa1/vortex/reader/compute/FusedFilterSumTest.java

Lines changed: 79 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,42 @@
11
package io.github.dfa1.vortex.reader.compute;
22

3+
import io.github.dfa1.vortex.core.error.VortexException;
34
import io.github.dfa1.vortex.core.io.PTypeIO;
45
import io.github.dfa1.vortex.core.model.DType;
56
import io.github.dfa1.vortex.reader.array.Array;
67
import io.github.dfa1.vortex.reader.array.MaskedArray;
8+
import io.github.dfa1.vortex.reader.array.MaterializedByteArray;
79
import io.github.dfa1.vortex.reader.array.MaterializedDoubleArray;
10+
import io.github.dfa1.vortex.reader.array.MaterializedFloatArray;
811
import io.github.dfa1.vortex.reader.array.MaterializedIntArray;
912
import io.github.dfa1.vortex.reader.array.MaterializedLongArray;
13+
import io.github.dfa1.vortex.reader.array.MaterializedShortArray;
1014
import org.junit.jupiter.api.Test;
1115
import org.junit.jupiter.params.ParameterizedTest;
1216
import org.junit.jupiter.params.provider.MethodSource;
1317

1418
import java.lang.foreign.Arena;
1519
import java.lang.foreign.MemorySegment;
20+
import java.lang.foreign.ValueLayout;
1621
import java.util.Random;
1722
import java.util.stream.IntStream;
1823
import java.util.stream.Stream;
1924

2025
import static org.assertj.core.api.Assertions.assertThat;
26+
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
2127

2228
/// Oracle test for the fused [Compute#filteredSum(Array, Predicate, Array)] kernel: the one-pass
2329
/// fused result must be bit-identical to an independent boxing reference — a plain per-element
2430
/// `if (predicate) acc += value` loop over the same columns. The fused hot lane shares no code with
2531
/// that boxing reference, so any divergence is a correctness bug.
2632
///
27-
/// The randomized cases pair a random primitive filter column (`i64` / `i32` / `f64`, with and
28-
/// without nulls) against a random primitive aggregate column, under each comparison predicate, so
29-
/// every filter/aggregate domain crossing (long/double, hot lane and the narrow-int generic
30-
/// fall-back) is exercised. The explicit cases pin the corners — empty, an all-null filter, an
31-
/// all-null aggregate, zero matches, and a full match.
33+
/// The randomized cases pair a random primitive filter column (`i64` / `i32` / `i16` / `i8` — each
34+
/// narrow width randomly unsigned — plus `f32` / `f64`, with and without nulls) against a random
35+
/// primitive aggregate column, under each comparison predicate and under composite / null-test
36+
/// predicates (the generic boxing path), so every filter/aggregate domain crossing (long/double,
37+
/// signed/unsigned, hot lane and the narrow-int generic fall-back) is exercised. The explicit cases
38+
/// pin the corners — empty, an all-null filter, an all-null aggregate, zero matches, a full match,
39+
/// mismatched column lengths, and the empty-range boundary constants of the range lowering.
3240
class FusedFilterSumTest {
3341

3442
private static final Arena ARENA = Arena.ofAuto();
@@ -137,6 +145,37 @@ void doubleFilterLongAggregateCrossesDomains() {
137145
assertFusedMatchesTwoPass(filter, new Predicate.Gt(1.0), agg);
138146
}
139147

148+
@Test
149+
void mismatchedColumnLengthsFailFast() {
150+
// Given columns of different lengths — the kernel must reject them before any scan
151+
Array filter = longColumn(new long[]{1, 2, 3}, null);
152+
Array agg = longColumn(new long[]{1, 2}, null);
153+
154+
// When the fused kernel is asked to fold misaligned columns
155+
// Then it fails fast instead of reading out of bounds
156+
assertThatExceptionOfType(VortexException.class)
157+
.isThrownBy(() -> Compute.filteredSum(filter, new Predicate.Gt(0L), agg))
158+
.withMessageContaining("filter length");
159+
}
160+
161+
@Test
162+
void boundaryConstantsLowerToEmptyRanges() {
163+
// Given the two range-lowering guards: Lt(min long) and Gt(max long) can match no value,
164+
// so both must lower to the canonical empty range instead of under/overflowing the bound
165+
Array filter = longColumn(new long[]{Long.MIN_VALUE, -1, 0, 1, Long.MAX_VALUE}, null);
166+
Array agg = longColumn(new long[]{1, 2, 4, 8, 16}, null);
167+
168+
// When the fused kernel filters with each boundary constant
169+
Number ltMin = Compute.filteredSum(filter, new Predicate.Lt(Long.MIN_VALUE), agg);
170+
Number gtMax = Compute.filteredSum(filter, new Predicate.Gt(Long.MAX_VALUE), agg);
171+
172+
// Then nothing is selected either way, matching the boxing reference
173+
assertThat(ltMin).isEqualTo(0L);
174+
assertThat(gtMax).isEqualTo(0L);
175+
assertFusedMatchesTwoPass(filter, new Predicate.Lt(Long.MIN_VALUE), agg);
176+
assertFusedMatchesTwoPass(filter, new Predicate.Gt(Long.MAX_VALUE), agg);
177+
}
178+
140179
private static void assertFusedMatchesTwoPass(Array filter, Predicate predicate, Array agg) {
141180
Number reference = referenceFilteredSum(filter, predicate, agg);
142181
Number fused = Compute.filteredSum(filter, predicate, agg);
@@ -182,7 +221,13 @@ private static Predicate[] everyComparison(Random random, Array filter) {
182221
new Predicate.Gt(a),
183222
new Predicate.Lte(a),
184223
new Predicate.Gte(a),
185-
new Predicate.Between(lo, hi)
224+
new Predicate.Between(lo, hi),
225+
// Composite and null-test predicates have no fast lane, so these drive the whole
226+
// sweep through the generic boxing path (PredicateEvaluator + Values + Compare).
227+
new Predicate.And(new Predicate.Gte(lo), new Predicate.Lte(hi)),
228+
new Predicate.Or(new Predicate.Lt(lo), new Predicate.Gt(hi)),
229+
new Predicate.IsNull(),
230+
new Predicate.IsNotNull()
186231
};
187232
}
188233

@@ -204,9 +249,12 @@ private static Array randomColumn(Random random, boolean nullable) {
204249

205250
private static Array randomColumn(Random random, boolean nullable, int n) {
206251
boolean[] valid = nullable ? randomValidity(random, n) : null;
207-
return switch (random.nextInt(3)) {
252+
return switch (random.nextInt(6)) {
208253
case 0 -> longColumn(randomLongs(random, n), valid);
209254
case 1 -> intColumn(randomLongs(random, n), valid);
255+
case 2 -> shortColumn(randomLongs(random, n), valid, random.nextBoolean());
256+
case 3 -> byteColumn(randomLongs(random, n), valid, random.nextBoolean());
257+
case 4 -> floatColumn(randomDoubles(random, n), valid);
210258
default -> doubleColumn(randomDoubles(random, n), valid);
211259
};
212260
}
@@ -251,6 +299,30 @@ private static Array intColumn(long[] values, boolean[] valid) {
251299
return wrap(new MaterializedIntArray(DType.I32, values.length, seg), valid);
252300
}
253301

302+
private static Array shortColumn(long[] values, boolean[] valid, boolean unsigned) {
303+
MemorySegment seg = ARENA.allocate(Math.max(2L, values.length * 2L), 2);
304+
for (int i = 0; i < values.length; i++) {
305+
seg.setAtIndex(PTypeIO.LE_SHORT, i, (short) values[i]);
306+
}
307+
return wrap(new MaterializedShortArray(unsigned ? DType.U16 : DType.I16, values.length, seg), valid);
308+
}
309+
310+
private static Array byteColumn(long[] values, boolean[] valid, boolean unsigned) {
311+
MemorySegment seg = ARENA.allocate(Math.max(1L, values.length));
312+
for (int i = 0; i < values.length; i++) {
313+
seg.set(ValueLayout.JAVA_BYTE, i, (byte) values[i]);
314+
}
315+
return wrap(new MaterializedByteArray(unsigned ? DType.U8 : DType.I8, values.length, seg), valid);
316+
}
317+
318+
private static Array floatColumn(double[] values, boolean[] valid) {
319+
MemorySegment seg = ARENA.allocate(Math.max(4L, values.length * 4L), 4);
320+
for (int i = 0; i < values.length; i++) {
321+
seg.setAtIndex(PTypeIO.LE_FLOAT, i, (float) values[i]);
322+
}
323+
return wrap(new MaterializedFloatArray(DType.F32, values.length, seg), valid);
324+
}
325+
254326
private static Array doubleColumn(double[] values, boolean[] valid) {
255327
MemorySegment seg = ARENA.allocate(Math.max(8L, values.length * 8L), 8);
256328
for (int i = 0; i < values.length; i++) {
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
package io.github.dfa1.vortex.reader.compute;
2+
3+
import io.github.dfa1.vortex.core.error.VortexException;
4+
import io.github.dfa1.vortex.core.io.PTypeIO;
5+
import io.github.dfa1.vortex.core.model.DType;
6+
import io.github.dfa1.vortex.reader.array.Array;
7+
import io.github.dfa1.vortex.reader.array.GenericArray;
8+
import io.github.dfa1.vortex.reader.array.MaskedArray;
9+
import io.github.dfa1.vortex.reader.array.MaterializedByteArray;
10+
import io.github.dfa1.vortex.reader.array.MaterializedIntArray;
11+
import io.github.dfa1.vortex.reader.array.MaterializedShortArray;
12+
import org.junit.jupiter.api.Test;
13+
14+
import java.lang.foreign.Arena;
15+
import java.lang.foreign.MemorySegment;
16+
import java.lang.foreign.ValueLayout;
17+
18+
import static org.assertj.core.api.Assertions.assertThat;
19+
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
20+
21+
/// Unit test for the boxing value accessor [Values] — the correctness baseline every generic
22+
/// compute path reads through. Pins the branches the kernel oracles reach only incidentally: the
23+
/// zero-extension of narrow unsigned columns (a `u8` `0xFF` must box as `255`, never `-1`), the
24+
/// non-numeric boxes (Utf8 → [String], bool → [Boolean]), the extension-dtype passthrough of the
25+
/// integer widening, and the fail-fast on an array type with no scalar accessor.
26+
class ValuesTest {
27+
28+
private static final Arena ARENA = Arena.ofAuto();
29+
30+
@Test
31+
void unsignedNarrowColumnsZeroExtend() {
32+
// Given the all-ones bit pattern in u8 / u16 / u32 columns — sign-extended by the raw
33+
// accessors, so boxing must zero-extend or the value reads back negative
34+
Array u8 = byteArray(DType.U8, (byte) 0xFF);
35+
Array u16 = shortArray(DType.U16, (short) 0xFFFF);
36+
Array u32 = intArray(DType.U32, 0xFFFFFFFF);
37+
38+
// When each value is boxed
39+
Object result8 = Values.valueAt(u8, 0);
40+
Object result16 = Values.valueAt(u16, 0);
41+
Object result32 = Values.valueAt(u32, 0);
42+
43+
// Then the boxes carry the unsigned magnitude
44+
assertThat(result8).isEqualTo(255L);
45+
assertThat(result16).isEqualTo(65535L);
46+
assertThat(result32).isEqualTo(4294967295L);
47+
}
48+
49+
@Test
50+
void signedNarrowColumnsSignExtend() {
51+
// Given the same all-ones bit pattern in the signed narrow columns
52+
Array i8 = byteArray(DType.I8, (byte) 0xFF);
53+
Array i16 = shortArray(DType.I16, (short) 0xFFFF);
54+
55+
// When each value is boxed
56+
Object result8 = Values.valueAt(i8, 0);
57+
Object result16 = Values.valueAt(i16, 0);
58+
59+
// Then the boxes keep the signed value
60+
assertThat(result8).isEqualTo(-1L);
61+
assertThat(result16).isEqualTo(-1L);
62+
}
63+
64+
@Test
65+
void extensionDtypePassesIntegerThrough() {
66+
// Given an integer column whose dtype is an Extension (not a Primitive) — the unsigned
67+
// widening must pass the raw value through rather than assume a primitive ptype
68+
DType extension = new DType.Extension("test.ext", DType.I32, null, false);
69+
MemorySegment seg = ARENA.allocate(4, 4);
70+
seg.setAtIndex(PTypeIO.LE_INT, 0, -7);
71+
Array column = new MaterializedIntArray(extension, 1, seg);
72+
73+
// When the value is boxed
74+
Object result = Values.valueAt(column, 0);
75+
76+
// Then it carries the raw signed value unchanged
77+
assertThat(result).isEqualTo(-7L);
78+
}
79+
80+
@Test
81+
void utf8AndBoolBoxNaturally() {
82+
// Given a Utf8 column and a bool column
83+
Array utf8 = ComputeArrays.utf8Array(ARENA, "alpha", "beta");
84+
Array bool = ComputeArrays.boolArray(ARENA, true, false);
85+
86+
// When the values are boxed
87+
Object result = Values.valueAt(utf8, 1);
88+
Object resultBool = Values.valueAt(bool, 1);
89+
90+
// Then Utf8 boxes as String and bool as Boolean
91+
assertThat(result).isEqualTo("beta");
92+
assertThat(resultBool).isEqualTo(false);
93+
}
94+
95+
@Test
96+
void arrayWithoutScalarAccessorFailsFast() {
97+
// Given a GenericArray — the fallback array type with no scalar accessor for compute
98+
Array generic = new GenericArray(DType.I64, 1, ARENA.allocate(8, 8));
99+
100+
// When a value read is attempted
101+
// Then the accessor fails fast instead of returning garbage
102+
assertThatExceptionOfType(VortexException.class)
103+
.isThrownBy(() -> Values.valueAt(generic, 0))
104+
.withMessageContaining("no scalar accessor");
105+
}
106+
107+
@Test
108+
void maskedArrayReadsThroughItsPayload() {
109+
// Given a masked narrow column with one null position
110+
Array masked = new MaskedArray(byteArray(DType.U8, (byte) 42, (byte) 7),
111+
ComputeArrays.boolArray(ARENA, true, false));
112+
113+
// When the valid position is boxed and both positions are null-tested
114+
Object result = Values.valueAt(masked, 0);
115+
116+
// Then the payload boxes through the mask and the validity drives the null test
117+
assertThat(result).isEqualTo(42L);
118+
assertThat(Values.isNullAt(masked, 0)).isFalse();
119+
assertThat(Values.isNullAt(masked, 1)).isTrue();
120+
}
121+
122+
private static Array byteArray(DType dtype, byte... values) {
123+
MemorySegment seg = ARENA.allocate(Math.max(1L, values.length));
124+
for (int i = 0; i < values.length; i++) {
125+
seg.set(ValueLayout.JAVA_BYTE, i, values[i]);
126+
}
127+
return new MaterializedByteArray(dtype, values.length, seg);
128+
}
129+
130+
private static Array shortArray(DType dtype, short... values) {
131+
MemorySegment seg = ARENA.allocate(Math.max(2L, values.length * 2L), 2);
132+
for (int i = 0; i < values.length; i++) {
133+
seg.setAtIndex(PTypeIO.LE_SHORT, i, values[i]);
134+
}
135+
return new MaterializedShortArray(dtype, values.length, seg);
136+
}
137+
138+
private static Array intArray(DType dtype, int... values) {
139+
MemorySegment seg = ARENA.allocate(Math.max(4L, values.length * 4L), 4);
140+
for (int i = 0; i < values.length; i++) {
141+
seg.setAtIndex(PTypeIO.LE_INT, i, values[i]);
142+
}
143+
return new MaterializedIntArray(dtype, values.length, seg);
144+
}
145+
}

0 commit comments

Comments
 (0)