Skip to content

Commit fbbc54d

Browse files
dfa1claude
andcommitted
test(reader): kill convertLayout/convertDType boundary mutants (pilot iteration 3)
PostscriptParserParseBlobsBoundsTest drives the package-private parseBlobs directly with crafted footer/layout/dtype FlatBuffers (no file or segment), pinning the validation guards at their exact edges: - layout encoding index: last valid index parses, index == size throws - layout depth: a chain exactly MAX_LAYOUT_DEPTH deep parses, one deeper throws - decimal precision: 1 and 38 parse, 0 and 39 throw - decimal scale: 0 and scale == precision parse, scale > precision throws Reader mutation score 90% -> 95%, test strength 96%. Remaining reader survivors are out of scope or accepted: checkBlobBounds `length < 0` (unreachable — blob length is u32 off the wire), the 4 MiB metadata-size boundary (cosmetic limit, not worth a 4 MB allocation in a unit test), and the Trailer / FlatSegmentDecoder edges in other classes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent ed8c98a commit fbbc54d

1 file changed

Lines changed: 193 additions & 0 deletions

File tree

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
package io.github.dfa1.vortex.reader;
2+
3+
import com.google.flatbuffers.FlatBufferBuilder;
4+
import io.github.dfa1.vortex.core.DType;
5+
import io.github.dfa1.vortex.core.VortexException;
6+
import io.github.dfa1.vortex.fbs.ArraySpec;
7+
import io.github.dfa1.vortex.fbs.Decimal;
8+
import io.github.dfa1.vortex.fbs.Footer;
9+
import io.github.dfa1.vortex.fbs.Layout;
10+
import io.github.dfa1.vortex.fbs.LayoutSpec;
11+
import io.github.dfa1.vortex.fbs.Type;
12+
import org.junit.jupiter.api.Test;
13+
import org.junit.jupiter.params.ParameterizedTest;
14+
import org.junit.jupiter.params.provider.ValueSource;
15+
16+
import java.nio.ByteBuffer;
17+
18+
import static org.assertj.core.api.Assertions.assertThat;
19+
import static org.assertj.core.api.Assertions.assertThatCode;
20+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
21+
22+
/// Boundary coverage for the validation guards inside `PostscriptParser.convertLayout` (layout
23+
/// depth, encoding index) and `convertDType` (decimal precision / scale). Drives the
24+
/// package-private [PostscriptParser#parseBlobs] directly with crafted footer / layout / dtype
25+
/// FlatBuffers — no file or segment needed — so each guard is hit at its exact edge: the largest
26+
/// legal value must parse, the first illegal value must throw a [VortexException].
27+
class PostscriptParserParseBlobsBoundsTest {
28+
29+
// ── Layout encoding-index bound: encIdx < 0 || encIdx >= layoutSpecs.size() ──
30+
31+
@Test
32+
void parseBlobs_layoutEncodingIndex_atLastSpec_parses() {
33+
// Given — one layout spec, layout references index 0 (== size - 1, the last valid index)
34+
ByteBuffer footer = footerWithLayoutSpecs("vortex.flat");
35+
ByteBuffer layout = flatLayout(0);
36+
37+
// When / Then — the top valid index must be accepted (kills `>=` relaxed to `>`)
38+
assertThatCode(() -> PostscriptParser.parseBlobs(footer, layout, null))
39+
.doesNotThrowAnyException();
40+
}
41+
42+
@Test
43+
void parseBlobs_layoutEncodingIndex_equalToSize_throws() {
44+
// Given — one layout spec (size 1), layout references index 1 (first out-of-range index)
45+
ByteBuffer footer = footerWithLayoutSpecs("vortex.flat");
46+
ByteBuffer layout = flatLayout(1);
47+
48+
// When / Then
49+
assertThatThrownBy(() -> PostscriptParser.parseBlobs(footer, layout, null))
50+
.isInstanceOf(VortexException.class)
51+
.hasMessageContaining("encoding index");
52+
}
53+
54+
// ── Layout depth bound: depth > MAX_LAYOUT_DEPTH (64) ────────────────────────
55+
56+
@Test
57+
void parseBlobs_layoutDepth_atLimit_parses() {
58+
// Given — a single-child chain whose deepest node sits at exactly MAX_LAYOUT_DEPTH.
59+
// convertLayout is called with depth == 64 there, and `64 > 64` is false. Kills the
60+
// `depth >` relaxed to `depth >=` mutant, which would reject the legal max-depth tree.
61+
ByteBuffer footer = footerWithLayoutSpecs("vortex.flat");
62+
ByteBuffer layout = nestedLayout(PostscriptParser.MAX_LAYOUT_DEPTH);
63+
64+
// When / Then
65+
assertThatCode(() -> PostscriptParser.parseBlobs(footer, layout, null))
66+
.doesNotThrowAnyException();
67+
}
68+
69+
@Test
70+
void parseBlobs_layoutDepth_oneOverLimit_throws() {
71+
// Given — one level deeper: the deepest node reaches depth 65, tripping `65 > 64`
72+
ByteBuffer footer = footerWithLayoutSpecs("vortex.flat");
73+
ByteBuffer layout = nestedLayout(PostscriptParser.MAX_LAYOUT_DEPTH + 1);
74+
75+
// When / Then
76+
assertThatThrownBy(() -> PostscriptParser.parseBlobs(footer, layout, null))
77+
.isInstanceOf(VortexException.class)
78+
.hasMessageContaining("depth");
79+
}
80+
81+
// ── Decimal precision bound: precision < 1 || precision > 38 ─────────────────
82+
83+
@ParameterizedTest
84+
@ValueSource(ints = {1, 38})
85+
void parseBlobs_decimalPrecision_atEdges_parses(int precision) {
86+
// Given — precision at the inclusive edges 1 and 38 (scale 0 keeps the scale guard happy).
87+
// Kills `precision < 1` -> `<= 1` (would reject precision 1) and `precision > 38` ->
88+
// `>= 38` (would reject precision 38).
89+
ByteBuffer dtype = decimalDtype(precision, (byte) 0);
90+
91+
// When
92+
DType result = parseDtype(dtype);
93+
94+
// Then
95+
assertThat(result).isInstanceOf(DType.Decimal.class);
96+
}
97+
98+
@ParameterizedTest
99+
@ValueSource(ints = {0, 39})
100+
void parseBlobs_decimalPrecision_outOfRange_throws(int precision) {
101+
// Given — precision just outside [1, 38]
102+
ByteBuffer dtype = decimalDtype(precision, (byte) 0);
103+
104+
// When / Then
105+
assertThatThrownBy(() -> parseDtype(dtype))
106+
.isInstanceOf(VortexException.class)
107+
.hasMessageContaining("precision");
108+
}
109+
110+
// ── Decimal scale bound: scale < 0 || scale > precision ──────────────────────
111+
112+
@Test
113+
void parseBlobs_decimalScale_atEdges_parses() {
114+
// Given — scale 0 (lower edge) and scale == precision (upper edge) must both pass.
115+
// Kills `scale < 0` -> `<= 0` (would reject scale 0) and `scale > precision` ->
116+
// `>= precision` (would reject scale == precision).
117+
118+
// When / Then
119+
assertThatCode(() -> parseDtype(decimalDtype(10, (byte) 0))).doesNotThrowAnyException();
120+
assertThatCode(() -> parseDtype(decimalDtype(10, (byte) 10))).doesNotThrowAnyException();
121+
}
122+
123+
@Test
124+
void parseBlobs_decimalScale_abovePrecision_throws() {
125+
// Given — scale one past precision
126+
ByteBuffer dtype = decimalDtype(10, (byte) 11);
127+
128+
// When / Then
129+
assertThatThrownBy(() -> parseDtype(dtype))
130+
.isInstanceOf(VortexException.class)
131+
.hasMessageContaining("scale");
132+
}
133+
134+
// ── helpers ──────────────────────────────────────────────────────────────────
135+
136+
/// Parses a dtype blob through the full parseBlobs path, paired with a minimal valid
137+
/// footer + flat layout so convertDType is reached. Returns the decoded [DType].
138+
private static DType parseDtype(ByteBuffer dtype) {
139+
ByteBuffer footer = footerWithLayoutSpecs("vortex.flat");
140+
ByteBuffer layout = flatLayout(0);
141+
return PostscriptParser.parseBlobs(footer, layout, dtype).dtype();
142+
}
143+
144+
private static ByteBuffer footerWithLayoutSpecs(String... layoutSpecs) {
145+
var fbb = new FlatBufferBuilder(256);
146+
int asv = Footer.createArraySpecsVector(fbb, new int[]{
147+
ArraySpec.createArraySpec(fbb, fbb.createString("vortex.primitive"))});
148+
int[] ls = new int[layoutSpecs.length];
149+
for (int i = 0; i < layoutSpecs.length; i++) {
150+
ls[i] = LayoutSpec.createLayoutSpec(fbb, fbb.createString(layoutSpecs[i]));
151+
}
152+
int lsv = Footer.createLayoutSpecsVector(fbb, ls);
153+
Footer.startSegmentSpecsVector(fbb, 0);
154+
int ssv = fbb.endVector();
155+
int footOff = Footer.createFooter(fbb, asv, lsv, ssv, 0, 0);
156+
fbb.finish(footOff);
157+
return slice(fbb);
158+
}
159+
160+
private static ByteBuffer flatLayout(int encodingIdx) {
161+
var fbb = new FlatBufferBuilder(128);
162+
int segV = Layout.createSegmentsVector(fbb, new long[]{0});
163+
int off = Layout.createLayout(fbb, encodingIdx, 1L, 0, 0, segV);
164+
Layout.finishLayoutBuffer(fbb, off);
165+
return slice(fbb);
166+
}
167+
168+
private static ByteBuffer nestedLayout(int depth) {
169+
var fbb = new FlatBufferBuilder(depth * 32 + 64);
170+
int segV = Layout.createSegmentsVector(fbb, new long[]{0});
171+
int current = Layout.createLayout(fbb, 0, 1L, 0, 0, segV);
172+
// Wrap `depth` times: the innermost leaf ends up at recursion depth == `depth`.
173+
for (int i = 0; i < depth; i++) {
174+
int childV = Layout.createChildrenVector(fbb, new int[]{current});
175+
current = Layout.createLayout(fbb, 0, 1L, 0, childV, 0);
176+
}
177+
Layout.finishLayoutBuffer(fbb, current);
178+
return slice(fbb);
179+
}
180+
181+
private static ByteBuffer decimalDtype(int precision, byte scale) {
182+
var fbb = new FlatBufferBuilder(64);
183+
int dec = Decimal.createDecimal(fbb, precision, scale, false);
184+
int off = io.github.dfa1.vortex.fbs.DType.createDType(fbb, Type.Decimal, dec);
185+
io.github.dfa1.vortex.fbs.DType.finishDTypeBuffer(fbb, off);
186+
return slice(fbb);
187+
}
188+
189+
private static ByteBuffer slice(FlatBufferBuilder fbb) {
190+
ByteBuffer data = fbb.dataBuffer();
191+
return data.slice(data.position(), data.remaining());
192+
}
193+
}

0 commit comments

Comments
 (0)