Skip to content

Commit 23f7171

Browse files
committed
test(fsst): cover LazyFsstVarBinArray's forEachByteLength ptype/broadcast branches
Sonar's new-code gate wants 80% coverage; the modulo broadcast path and the U16/U32/I64/unsupported-ptype branches of forEachByteLength were untested.
1 parent 733821d commit 23f7171

1 file changed

Lines changed: 117 additions & 5 deletions

File tree

reader/src/test/java/io/github/dfa1/vortex/reader/decode/FsstEncodingDecoderTest.java

Lines changed: 117 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,17 @@
1111
import io.github.dfa1.vortex.reader.array.VarBinArray;
1212
import org.junit.jupiter.api.Nested;
1313
import org.junit.jupiter.api.Test;
14+
import org.junit.jupiter.params.ParameterizedTest;
15+
import org.junit.jupiter.params.provider.Arguments;
16+
import org.junit.jupiter.params.provider.MethodSource;
1417

1518
import java.lang.foreign.Arena;
1619
import java.lang.foreign.MemorySegment;
1720
import java.lang.foreign.ValueLayout;
1821
import java.nio.charset.StandardCharsets;
1922
import java.util.ArrayList;
2023
import java.util.List;
24+
import java.util.stream.Stream;
2125

2226
import static org.assertj.core.api.Assertions.assertThat;
2327
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
@@ -182,6 +186,67 @@ void forEachByteLength_neverTouchesCodeOffsetsOrCompressedBytes() {
182186
// Then
183187
assertThat(lengths).containsExactly(2, 3);
184188
}
189+
190+
@Test
191+
void forEachByteLength_emptyChildWithNonZeroRows_throwsFromBulkPath() {
192+
// Given — n > 0 but the uncompressed-lengths child has zero physical elements,
193+
// exercised through the bulk forEachByteLength entry point rather than
194+
// Guards#emptyUncompressedLengthsChild_throws's single-row getByteLength call.
195+
long[] symbols = {};
196+
byte[] symbolLengths = {};
197+
byte[] compressed = {};
198+
long[] uncompLengths = {};
199+
long[] codeOffsets = {0};
200+
VarBinArray sut = decodeFsst(1, symbols, symbolLengths, compressed,
201+
PType.U8, uncompLengths, PType.U8, codeOffsets);
202+
203+
// When / Then
204+
assertThatExceptionOfType(VortexException.class)
205+
.isThrownBy(() -> sut.forEachByteLength(len -> { }))
206+
.withMessageContaining("empty");
207+
}
208+
209+
@Test
210+
void forEachByteLength_broadcastCapacityBetweenOneAndRowCount_readsWithModulo() {
211+
// Given — cap (2) is neither 0 nor >= n (5), so only the per-row modulo path can
212+
// produce the result; existing broadcast fixtures elsewhere use capacity 1 or
213+
// capacity == n, neither of which reaches this branch.
214+
long[] symbols = {};
215+
byte[] symbolLengths = {};
216+
byte[] compressed = {};
217+
long[] uncompLengths = {5, 7};
218+
long[] codeOffsets = {0};
219+
VarBinArray sut = decodeFsst(5, symbols, symbolLengths, compressed,
220+
PType.U8, uncompLengths, PType.U8, codeOffsets);
221+
222+
// When
223+
List<Integer> lengths = new ArrayList<>();
224+
sut.forEachByteLength(lengths::add);
225+
226+
// Then
227+
assertThat(lengths).containsExactly(5, 7, 5, 7, 5);
228+
}
229+
230+
@ParameterizedTest(name = "{0}")
231+
@MethodSource("io.github.dfa1.vortex.reader.decode.FsstEncodingDecoderTest#bulkPathLengthPtypes")
232+
void forEachByteLength_bulkPathSupportsEveryLengthsPtype(PType ptype, long[] values) {
233+
// Given — cap == n forces the fast bulk path (forEachClaimedLength), which switches
234+
// on uncompressedLengthsPType independently of readAt's similar-looking switch; only
235+
// U8 is exercised through this path elsewhere.
236+
long[] symbols = {};
237+
byte[] symbolLengths = {};
238+
byte[] compressed = {};
239+
long[] codeOffsets = {0};
240+
VarBinArray sut = decodeFsst(values.length, symbols, symbolLengths, compressed,
241+
ptype, values, PType.U8, codeOffsets);
242+
243+
// When
244+
List<Integer> lengths = new ArrayList<>();
245+
sut.forEachByteLength(lengths::add);
246+
247+
// Then
248+
assertThat(lengths).containsExactly(10, 20);
249+
}
185250
}
186251

187252
@Nested
@@ -537,10 +602,54 @@ void negativeTotalUncompressed_throws() {
537602
.isThrownBy(() -> result.getByteLength(0))
538603
.withMessageContaining("decoded length too large");
539604
}
605+
606+
@Test
607+
void forEachByteLength_unsupportedLengthsPtype_throwsFromBulkPath() {
608+
// Given — F32 is not one of forEachClaimedLength's supported cases; cap == n forces
609+
// the fast bulk path (not the per-row modulo path, which throws from readAt instead),
610+
// exercising forEachClaimedLength's own default branch.
611+
long[] symbols = {};
612+
byte[] symbolLengths = {};
613+
byte[] compressed = {};
614+
long[] codeOffsets = {0};
615+
VarBinArray result = decodeFsst(1, symbols, symbolLengths, compressed,
616+
PType.F32, new long[]{0}, PType.U8, codeOffsets);
617+
618+
// When / Then
619+
assertThatExceptionOfType(VortexException.class)
620+
.isThrownBy(() -> result.forEachByteLength(len -> { }))
621+
.withMessageContaining("unsupported ptype");
622+
}
623+
624+
@Test
625+
void getByteLength_unsupportedLengthsPtype_throwsFromReadAt() {
626+
// Given — getByteLength reads a single row's length via uncompressedLength()/readAt,
627+
// a different code path from forEachClaimedLength's own default branch above.
628+
long[] symbols = {};
629+
byte[] symbolLengths = {};
630+
byte[] compressed = {};
631+
long[] codeOffsets = {0, 0};
632+
VarBinArray result = decodeFsst(1, symbols, symbolLengths, compressed,
633+
PType.F32, new long[]{0}, PType.U8, codeOffsets);
634+
635+
// When / Then
636+
assertThatExceptionOfType(VortexException.class)
637+
.isThrownBy(() -> result.getByteLength(0))
638+
.withMessageContaining("unsupported ptype");
639+
}
540640
}
541641

542642
// ── decode harness ─────────────────────────────────────────────────────────
543643

644+
/// Length/value pairs used to exercise every ptype [LazyFsstVarBinArray]'s bulk
645+
/// `forEachClaimedLength` path supports beyond U8 (already covered elsewhere).
646+
static Stream<Arguments> bulkPathLengthPtypes() {
647+
return Stream.of(
648+
Arguments.of(PType.U16, new long[]{10, 20}),
649+
Arguments.of(PType.U32, new long[]{10, 20}),
650+
Arguments.of(PType.I64, new long[]{10, 20}));
651+
}
652+
544653
/// Builds and decodes a `vortex.fsst` node from the raw component arrays.
545654
///
546655
/// Segment layout matches the encoder: buffers 0-2 are the FSST node's own (symbols, symbol
@@ -621,10 +730,13 @@ private static MemorySegment bytes(byte[] values) {
621730
return seg;
622731
}
623732

624-
/// Writes `values` into a segment at the stride of `ptype`. Only the ptypes the FSST children
625-
/// can carry (U8, I32, I64) are needed by these tests. An empty `values` yields a zero-length
626-
/// segment (not a 1-byte floor) so the child decodes with capacity 0 — the shape the
627-
/// empty-child guards inspect.
733+
/// Writes `values` into a segment at the stride of `ptype`. Every ptype [LazyFsstVarBinArray]
734+
/// itself supports (U8, U16, U32, I32, I64, U64) is written faithfully; any other ptype (e.g.
735+
/// F32, used to drive LazyFsstVarBinArray's "unsupported ptype" guards) is left zero-filled —
736+
/// those tests only need a segment of the right size, never its content, since the guard
737+
/// throws before any value is read. An empty `values` yields a zero-length segment (not a
738+
/// 1-byte floor) so the child decodes with capacity 0 — the shape the empty-child guards
739+
/// inspect.
628740
private static MemorySegment typedSegment(PType ptype, long[] values) {
629741
long stride = ptype.byteSize();
630742
if (values.length == 0) {
@@ -637,7 +749,7 @@ private static MemorySegment typedSegment(PType ptype, long[] values) {
637749
case U16, I16 -> seg.set(VortexFormat.LE_SHORT, i * 2, (short) values[i]);
638750
case U32, I32 -> seg.setAtIndex(VortexFormat.LE_INT, i, (int) values[i]);
639751
case I64, U64 -> seg.setAtIndex(VortexFormat.LE_LONG, i, values[i]);
640-
default -> throw new IllegalArgumentException("unsupported ptype: " + ptype);
752+
default -> { /* unsupported-ptype guard tests: content is never read */ }
641753
}
642754
}
643755
return seg;

0 commit comments

Comments
 (0)