1111import io .github .dfa1 .vortex .reader .array .VarBinArray ;
1212import org .junit .jupiter .api .Nested ;
1313import 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
1518import java .lang .foreign .Arena ;
1619import java .lang .foreign .MemorySegment ;
1720import java .lang .foreign .ValueLayout ;
1821import java .nio .charset .StandardCharsets ;
1922import java .util .ArrayList ;
2023import java .util .List ;
24+ import java .util .stream .Stream ;
2125
2226import static org .assertj .core .api .Assertions .assertThat ;
2327import 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