diff --git a/cli/src/test/java/io/github/dfa1/vortex/cli/tui/TuiTestSupport.java b/cli/src/test/java/io/github/dfa1/vortex/cli/tui/TuiTestSupport.java index bcc591f9..86217365 100644 --- a/cli/src/test/java/io/github/dfa1/vortex/cli/tui/TuiTestSupport.java +++ b/cli/src/test/java/io/github/dfa1/vortex/cli/tui/TuiTestSupport.java @@ -79,6 +79,33 @@ static Path writeMultiTypeVortex(Path dir, String name) throws IOException { return file; } + /// Writes a single-chunk file with a low-cardinality `label` Utf8 column and an + /// `n` I64 column. The repeated labels trip the global-dictionary encoder, so the + /// `label` column decodes to a `vortex.dict` layout — letting the inspector render + /// its dictionary-preview detail pane (alongside the I64 data-preview pane). + /// + /// The global dictionary only forms within a single chunk, so all `rows` are + /// written in one `writeChunk`. + static Path writeRichVortex(Path dir, String name, int rows) throws IOException { + Path file = dir.resolve(name); + DType.Struct schema = new DType.Struct( + List.of("label", "n"), + List.of(new DType.Utf8(false), new DType.Primitive(PType.I64, false)), + false); + String[] labels = {"red", "green", "blue"}; + String[] label = new String[rows]; + long[] n = new long[rows]; + for (int i = 0; i < rows; i++) { + label[i] = labels[i % labels.length]; + n[i] = i; + } + try (FileChannel ch = FileChannel.open(file, StandardOpenOption.CREATE, StandardOpenOption.WRITE); + VortexWriter writer = VortexWriter.create(ch, schema, WriteOptions.defaults())) { + writer.writeChunk(Map.of("label", label, "n", n)); + } + return file; + } + /// Opens `file` on the worker thread and returns the handle. The caller must /// close the returned handle on the same worker (see [#closeOnWorker]). /// diff --git a/cli/src/test/java/io/github/dfa1/vortex/cli/tui/VortexInspectorTuiTest.java b/cli/src/test/java/io/github/dfa1/vortex/cli/tui/VortexInspectorTuiTest.java index 4441b23b..187f0e21 100644 --- a/cli/src/test/java/io/github/dfa1/vortex/cli/tui/VortexInspectorTuiTest.java +++ b/cli/src/test/java/io/github/dfa1/vortex/cli/tui/VortexInspectorTuiTest.java @@ -3,12 +3,15 @@ import io.github.dfa1.vortex.cli.tui.term.Key; import io.github.dfa1.vortex.cli.tui.term.Terminal; import io.github.dfa1.vortex.inspect.InspectorTree; +import io.github.dfa1.vortex.reader.VortexHandle; import io.github.dfa1.vortex.reader.VortexReader; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import java.nio.file.Path; +import java.util.ArrayList; import java.util.List; +import java.util.concurrent.atomic.AtomicReference; import static org.assertj.core.api.Assertions.assertThat; @@ -67,4 +70,66 @@ void quitsOnEscapeWithoutRenderingDetails() throws Exception { assertThat(term.output()).contains("vortex-inspect"); } + + @Test + void deepExpand_rendersDictAndDataPanes_synchronously() throws Exception { + // Given — a rich fixture (low-cardinality dict column + an I64 column) and a + // script that expands the whole tree, then visits every row so each node's + // detail pane renders. worker == null runs all previews inline. + Path file = TuiTestSupport.writeRichVortex(tmp, "rich.vortex", 200); + FakeTerminal term = new FakeTerminal(new Terminal.Size(40, 120), expandAndVisitAll()); + + // When + try (VortexReader handle = VortexReader.open(file)) { + InspectorTree tree = InspectorTree.buildShallow(handle); + VortexInspectorTui.run(term, tree, handle, null); + } + + // Then — the dictionary-preview and data-preview panes both rendered + String out = term.output(); + assertThat(out).contains("Dictionary"); + assertThat(out).contains("Data (column"); + } + + @Test + void deepExpand_overWorker_drivesAsyncPreviews() throws Exception { + // Given — same rich fixture, but a real IoWorker so the async submit branches + // (dict/stats/data load via worker.submit, indexStatsChildrenOnWorker, peek + // prefetch) are exercised instead of the synchronous render-thread path. + Path file = TuiTestSupport.writeRichVortex(tmp, "rich.vortex", 200); + FakeTerminal term = new FakeTerminal(new Terminal.Size(40, 120), expandAndVisitAll(), 3); + + // When — handle and tree are built on the worker thread (confined arena) + try (IoWorker worker = new IoWorker("inspect-test-io")) { + VortexHandle handle = TuiTestSupport.openOnWorker(worker, file); + try { + AtomicReference tree = new AtomicReference<>(); + worker.runAndAwait(() -> tree.set(InspectorTree.buildShallow(handle))); + VortexInspectorTui.run(term, tree.get(), handle, worker); + } finally { + TuiTestSupport.closeOnWorker(worker, handle); + } + } + + // Then — completes and rendered the inspector chrome + assertThat(term.output()).contains("vortex-inspect"); + } + + /// Script: expand every node along the traversal, then return to the top and step + /// through all now-visible rows so each node is selected (and its detail pane + /// rendered) at least once. + private static List expandAndVisitAll() { + List script = new ArrayList<>(); + script.add(Key.Home.INSTANCE); + for (int i = 0; i < 40; i++) { + script.add(Key.ArrowRight.INSTANCE); + script.add(Key.ArrowDown.INSTANCE); + } + script.add(Key.Home.INSTANCE); + for (int i = 0; i < 80; i++) { + script.add(Key.ArrowDown.INSTANCE); + } + script.add(new Key.Char('q')); + return script; + } }