From 849441a602cee415e85a4df80b606db6e7778cd0 Mon Sep 17 00:00:00 2001 From: Davide Angelocola Date: Fri, 19 Jun 2026 09:35:52 +0200 Subject: [PATCH 1/2] test(cli): cover remaining extension + decimal render branches MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GridRender: vortex.time (I32 ms), vortex.timestamp (I64 ms) and vortex.uuid (FixedSizeList(U8,16)) extension rendering via the decoder dtype factories. InspectorRender: GenericArray-backed decimal (single-buffer LE mantissa) and the bad-shape fallback path through tryDecimal. Leaves only the DecimalByteParts "null cell" branch uncovered — it needs a masked byte-parts fixture not worth the weight for one return. Co-Authored-By: Claude Opus 4.8 --- .../dfa1/vortex/cli/tui/GridRenderTest.java | 45 +++++++++++++++++++ .../vortex/cli/tui/InspectorRenderTest.java | 30 +++++++++++++ 2 files changed, 75 insertions(+) diff --git a/cli/src/test/java/io/github/dfa1/vortex/cli/tui/GridRenderTest.java b/cli/src/test/java/io/github/dfa1/vortex/cli/tui/GridRenderTest.java index c56f8791..d6a7ca31 100644 --- a/cli/src/test/java/io/github/dfa1/vortex/cli/tui/GridRenderTest.java +++ b/cli/src/test/java/io/github/dfa1/vortex/cli/tui/GridRenderTest.java @@ -2,12 +2,18 @@ import io.github.dfa1.vortex.core.DType; import io.github.dfa1.vortex.core.PType; +import io.github.dfa1.vortex.encoding.TimeUnit; import io.github.dfa1.vortex.reader.array.Array; +import io.github.dfa1.vortex.reader.array.ByteArray; +import io.github.dfa1.vortex.reader.array.FixedSizeListArray; import io.github.dfa1.vortex.reader.array.IntArray; import io.github.dfa1.vortex.reader.array.LazyConstantDecimalArray; import io.github.dfa1.vortex.reader.array.LongArray; import io.github.dfa1.vortex.reader.array.MaskedArray; import io.github.dfa1.vortex.reader.array.StructArray; +import io.github.dfa1.vortex.reader.extension.TimeExtensionDecoder; +import io.github.dfa1.vortex.reader.extension.TimestampExtensionDecoder; +import io.github.dfa1.vortex.reader.extension.UuidExtensionDecoder; import org.junit.jupiter.api.Test; import java.lang.foreign.Arena; @@ -80,6 +86,45 @@ void rendersDateExtensionFromIntStorage() { } } + @Test + void rendersTimeExtensionFromIntStorage() { + try (Arena arena = Arena.ofConfined()) { + // Given — vortex.time (ms unit) over I32 storage; 0 ms-of-day = midnight + DType timeExt = TimeExtensionDecoder.INSTANCE.dtype(TimeUnit.Milliseconds, false); + IntArray storage = ArrayFixtures.ints(arena, 0); + + // When / Then + assertThat(GridRender.formatCell(storage, 0, timeExt)).isEqualTo("00:00"); + } + } + + @Test + void rendersTimestampExtensionFromLongStorage() { + try (Arena arena = Arena.ofConfined()) { + // Given — vortex.timestamp (ms unit, no tz) over I64 storage; 0 ms = the epoch + DType tsExt = TimestampExtensionDecoder.INSTANCE.dtype(TimeUnit.Milliseconds, null, false); + LongArray storage = ArrayFixtures.longs(arena, 0L); + + // When / Then + assertThat(GridRender.formatCell(storage, 0, tsExt)).isEqualTo("1970-01-01T00:00:00Z"); + } + } + + @Test + void rendersUuidExtensionFromFixedSizeListStorage() { + try (Arena arena = Arena.ofConfined()) { + // Given — vortex.uuid over FixedSizeList(U8,16); all-zero bytes = the nil UUID + DType uuidExt = UuidExtensionDecoder.INSTANCE.dtype(false); + ByteArray elems = ArrayFixtures.bytes(arena, new byte[16]); + FixedSizeListArray storage = new FixedSizeListArray( + new DType.FixedSizeList(new DType.Primitive(PType.U8, false), 16, false), 1, elems); + + // When / Then + assertThat(GridRender.formatCell(storage, 0, uuidExt)) + .isEqualTo("00000000-0000-0000-0000-000000000000"); + } + } + @Test void maskedNullCellRendersEmpty() { try (Arena arena = Arena.ofConfined()) { diff --git a/cli/src/test/java/io/github/dfa1/vortex/cli/tui/InspectorRenderTest.java b/cli/src/test/java/io/github/dfa1/vortex/cli/tui/InspectorRenderTest.java index 55c1cf2d..69b2374e 100644 --- a/cli/src/test/java/io/github/dfa1/vortex/cli/tui/InspectorRenderTest.java +++ b/cli/src/test/java/io/github/dfa1/vortex/cli/tui/InspectorRenderTest.java @@ -3,6 +3,7 @@ import io.github.dfa1.vortex.core.DType; import io.github.dfa1.vortex.core.PType; import io.github.dfa1.vortex.reader.array.Array; +import io.github.dfa1.vortex.reader.array.GenericArray; import io.github.dfa1.vortex.reader.array.IntArray; import io.github.dfa1.vortex.reader.array.LazyConstantDecimalArray; import io.github.dfa1.vortex.reader.array.MaskedArray; @@ -11,6 +12,8 @@ import org.junit.jupiter.api.Test; import java.lang.foreign.Arena; +import java.lang.foreign.MemorySegment; +import java.lang.foreign.ValueLayout; import java.math.BigDecimal; import java.util.List; @@ -57,6 +60,33 @@ void rendersDecimal() { assertThat(InspectorRender.formatValue(sut, 0, decimal)).isEqualTo("3.14"); } + @Test + void rendersGenericArrayDecimal() { + try (Arena arena = Arena.ofConfined()) { + // Given — GenericArray over an 8-byte LE mantissa 123 at scale 2 → 1.23 + DType decimal = new DType.Decimal((byte) 10, (byte) 2, false); + MemorySegment buf = arena.allocate(8, 8); + buf.setAtIndex(ValueLayout.JAVA_LONG, 0, 123L); + Array sut = new GenericArray(decimal, 1, buf.asReadOnly()); + + // When / Then + assertThat(InspectorRender.formatValue(sut, 0, decimal)).isEqualTo("1.23"); + } + } + + @Test + void genericArrayDecimalBadShapeRendersFallback() { + try (Arena arena = Arena.ofConfined()) { + // Given — two buffers: getDecimal rejects the shape with a non-"null cell" error + DType decimal = new DType.Decimal((byte) 10, (byte) 2, false); + MemorySegment buf = arena.allocate(8, 8); + Array sut = new GenericArray(decimal, 1, new MemorySegment[]{buf, buf}, new Array[0]); + + // When / Then — tryDecimal swallows it into the fallback + assertThat(InspectorRender.formatValue(sut, 0, decimal)).startsWith(" Date: Fri, 19 Jun 2026 14:56:35 +0200 Subject: [PATCH 2/2] test(cli): cover FilterCommand parser + per-type compare branches MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a mixed-type fixture (I64/I32/F64/Utf8) to CliTestSupport and exercise the previously-uncovered FilterCommand paths: - parser: Double literal, unknown operator ('!'), empty value, operator at index 0 (empty column), invalid column name. - compareValue: Int, Double (compareDouble) and VarBin (lexicographic) columns, plus compareNumeric's Double branch via a fractional threshold on an I64 column. Short/Byte/Float/Bool compare branches and the RowFilter.And predicate stay uncovered — the writer takes no such arrays and the parser emits no AND syntax. Co-Authored-By: Claude Opus 4.8 --- .../dfa1/vortex/cli/CliTestSupport.java | 24 ++++ .../dfa1/vortex/cli/FilterCommandTest.java | 104 ++++++++++++++++++ 2 files changed, 128 insertions(+) diff --git a/cli/src/test/java/io/github/dfa1/vortex/cli/CliTestSupport.java b/cli/src/test/java/io/github/dfa1/vortex/cli/CliTestSupport.java index 04a4aff9..622256b9 100644 --- a/cli/src/test/java/io/github/dfa1/vortex/cli/CliTestSupport.java +++ b/cli/src/test/java/io/github/dfa1/vortex/cli/CliTestSupport.java @@ -36,6 +36,30 @@ static Path writeSmallVortex(Path dir, String name) throws IOException { return file; } + /// Writes a 3-row file with mixed column types: `id` (I64), `qty` (I32), + /// `price` (F64), and `name` (Utf8). Used to exercise per-type formatting and + /// comparison branches that the single-column [#writeSmallVortex] cannot reach. + static Path writeTypedVortex(Path dir, String name) throws IOException { + Path file = dir.resolve(name); + DType.Struct schema = new DType.Struct( + List.of("id", "qty", "price", "name"), + List.of( + new DType.Primitive(PType.I64, false), + new DType.Primitive(PType.I32, false), + new DType.Primitive(PType.F64, false), + new DType.Utf8(false)), + false); + try (FileChannel ch = FileChannel.open(file, StandardOpenOption.CREATE, StandardOpenOption.WRITE); + VortexWriter writer = VortexWriter.create(ch, schema, WriteOptions.defaults())) { + writer.writeChunk(Map.of( + "id", new long[]{1L, 2L, 3L}, + "qty", new int[]{10, 20, 30}, + "price", new double[]{100.0, 200.0, 300.0}, + "name", new String[]{"alice", "bob", "carol"})); + } + return file; + } + /// Captured streams + exit status from one CLI invocation. record Captured(int status, String stdout, String stderr) { } diff --git a/cli/src/test/java/io/github/dfa1/vortex/cli/FilterCommandTest.java b/cli/src/test/java/io/github/dfa1/vortex/cli/FilterCommandTest.java index 7a06fbd6..0428849e 100644 --- a/cli/src/test/java/io/github/dfa1/vortex/cli/FilterCommandTest.java +++ b/cli/src/test/java/io/github/dfa1/vortex/cli/FilterCommandTest.java @@ -11,6 +11,7 @@ import static io.github.dfa1.vortex.cli.CliTestSupport.capture; import static io.github.dfa1.vortex.cli.CliTestSupport.writeSmallVortex; +import static io.github.dfa1.vortex.cli.CliTestSupport.writeTypedVortex; import static org.assertj.core.api.Assertions.assertThat; class FilterCommandTest { @@ -90,6 +91,109 @@ void unknownColumn_returnsErrorNotCrash() { assertThat(result.stderr()).contains("error:"); } + @Test + void doubleValueAgainstLongColumn_returnsOk() { + // Given — a fractional literal parses as Double, compared against an I64 column + // (exercises parseValue's Double branch and compareNumeric's Double path) + // When + CliTestSupport.Captured result = capture(() -> + FilterCommand.run(new String[]{"filter", file.toString(), "id", ">", "1.5"})); + + // Then — id in {2, 3} match + assertThat(result.status()).isEqualTo(ExitStatus.OK); + assertThat(result.stdout()).startsWith("id"); + } + + @Test + void unknownOperator_returnsUsageError() { + // Given — a lone '!' is a recognised operator char but not a valid operator + // When + CliTestSupport.Captured result = capture(() -> + FilterCommand.run(new String[]{"filter", file.toString(), "id", "!", "1"})); + + // Then + assertThat(result.status()).isEqualTo(ExitStatus.USAGE_ERROR); + assertThat(result.stderr()).contains("unknown operator"); + } + + @Test + void emptyValue_returnsUsageError() { + // Given — operator present but no value after it + // When + CliTestSupport.Captured result = capture(() -> + FilterCommand.run(new String[]{"filter", file.toString(), "id", ">"})); + + // Then + assertThat(result.status()).isEqualTo(ExitStatus.USAGE_ERROR); + assertThat(result.stderr()).contains("invalid filter expression"); + } + + @Test + void operatorAtStart_returnsUsageError() { + // Given — operator at index 0 means an empty column name + // When + CliTestSupport.Captured result = capture(() -> + FilterCommand.run(new String[]{"filter", file.toString(), "=", "1"})); + + // Then + assertThat(result.status()).isEqualTo(ExitStatus.USAGE_ERROR); + assertThat(result.stderr()).contains("invalid filter expression"); + } + + @Test + void invalidColumnName_returnsUsageError() { + // Given — '-' is not a legal column-name character + // When + CliTestSupport.Captured result = capture(() -> + FilterCommand.run(new String[]{"filter", file.toString(), "a-b", "=", "1"})); + + // Then + assertThat(result.status()).isEqualTo(ExitStatus.USAGE_ERROR); + assertThat(result.stderr()).contains("invalid filter expression"); + } + + @Test + void filtersDoubleColumn_returnsOk() throws IOException { + // Given — F64 column with a fractional threshold (compareDouble path) + Path typed = writeTypedVortex(tmp, "typed.vortex"); + + // When — price in {200.0, 300.0} match + CliTestSupport.Captured result = capture(() -> + FilterCommand.run(new String[]{"filter", typed.toString(), "price", ">", "150.0"})); + + // Then + assertThat(result.status()).isEqualTo(ExitStatus.OK); + assertThat(result.stdout()).startsWith("id"); + } + + @Test + void filtersIntColumn_returnsOk() throws IOException { + // Given — I32 column (compareValue's IntArray branch) + Path typed = writeTypedVortex(tmp, "typed.vortex"); + + // When + CliTestSupport.Captured result = capture(() -> + FilterCommand.run(new String[]{"filter", typed.toString(), "qty", ">=", "20"})); + + // Then + assertThat(result.status()).isEqualTo(ExitStatus.OK); + assertThat(result.stdout()).startsWith("id"); + } + + @Test + void filtersStringColumn_returnsOk() throws IOException { + // Given — Utf8 column (compareValue's VarBinArray branch, lexicographic compare) + Path typed = writeTypedVortex(tmp, "typed.vortex"); + + // When + CliTestSupport.Captured result = capture(() -> + FilterCommand.run(new String[]{"filter", typed.toString(), "name", "==", "bob"})); + + // Then + assertThat(result.status()).isEqualTo(ExitStatus.OK); + assertThat(result.stdout()).startsWith("id"); + } + @Test void operatorPrefixAttack_doesNotCauseBacktracking() { // Given — input designed to trigger polynomial backtracking under the previous regex