Skip to content

Commit f20e80d

Browse files
dfa1claude
andcommitted
test(cli): cover ExportCommand, SelectCommand, StatsCommand, InspectCommand, ImportCommand
All five previously had 0% direct coverage. New tests follow the shared CliTestSupport harness (writes a 3-row Vortex fixture, captures stdout/ stderr, asserts exit status + output shape): - ExportCommandTest (3): usage, missing file, CSV header + 3 rows. - SelectCommandTest (3): usage, missing file, projection emits header. - StatsCommandTest (3): usage, missing file, row count + column table. - InspectCommandTest (4): usage, missing file (null handle path), invalid URL branch, valid file → Inspector report header. - ImportCommandTest (8, two @nested groups): - ArgParsing: no args, --delimiter missing value, --delimiter multi-char, too many positional args - Execution: missing input file, valid CSV (default output path), explicit output path, custom delimiter (--delimiter <TAB>) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent d9cff37 commit f20e80d

5 files changed

Lines changed: 360 additions & 0 deletions

File tree

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package io.github.dfa1.vortex.cli;
2+
3+
import org.junit.jupiter.api.Test;
4+
import org.junit.jupiter.api.io.TempDir;
5+
6+
import java.io.IOException;
7+
import java.nio.file.Path;
8+
9+
import static io.github.dfa1.vortex.cli.CliTestSupport.capture;
10+
import static io.github.dfa1.vortex.cli.CliTestSupport.writeSmallVortex;
11+
import static org.assertj.core.api.Assertions.assertThat;
12+
13+
class ExportCommandTest {
14+
15+
@Test
16+
void wrongArity_returnsUsageError() {
17+
// Given / When
18+
CliTestSupport.Captured got = capture(() -> ExportCommand.run(new String[]{"export"}));
19+
20+
// Then
21+
assertThat(got.status()).isEqualTo(ExitStatus.USAGE_ERROR);
22+
assertThat(got.stderr()).contains("usage:");
23+
}
24+
25+
@Test
26+
void missingFile_returnsFileNotFound(@TempDir Path tmp) {
27+
// Given
28+
Path missing = tmp.resolve("nope.vortex");
29+
30+
// When
31+
CliTestSupport.Captured got = capture(() -> ExportCommand.run(new String[]{"export", missing.toString()}));
32+
33+
// Then
34+
assertThat(got.status()).isEqualTo(ExitStatus.FILE_NOT_FOUND);
35+
assertThat(got.stderr()).contains("file not found");
36+
}
37+
38+
@Test
39+
void validFile_emitsCsvHeaderAndRows(@TempDir Path tmp) throws IOException {
40+
// Given — 3-row Vortex file with column "id" = [1, 2, 3]
41+
Path file = writeSmallVortex(tmp, "export.vortex");
42+
43+
// When
44+
CliTestSupport.Captured got = capture(() -> ExportCommand.run(new String[]{"export", file.toString()}));
45+
46+
// Then — header + 3 data rows
47+
assertThat(got.status()).isEqualTo(ExitStatus.OK);
48+
assertThat(got.stdout()).startsWith("id");
49+
assertThat(got.stdout().lines().count()).isEqualTo(4);
50+
}
51+
}
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
package io.github.dfa1.vortex.cli;
2+
3+
import org.junit.jupiter.api.Nested;
4+
import org.junit.jupiter.api.Test;
5+
import org.junit.jupiter.api.io.TempDir;
6+
7+
import java.io.IOException;
8+
import java.nio.charset.StandardCharsets;
9+
import java.nio.file.Files;
10+
import java.nio.file.Path;
11+
12+
import static io.github.dfa1.vortex.cli.CliTestSupport.capture;
13+
import static org.assertj.core.api.Assertions.assertThat;
14+
15+
class ImportCommandTest {
16+
17+
@Nested
18+
class ArgParsing {
19+
20+
@Test
21+
void noArgs_returnsUsageError() {
22+
// Given / When
23+
CliTestSupport.Captured got = capture(() -> ImportCommand.run(new String[]{"import"}));
24+
25+
// Then
26+
assertThat(got.status()).isEqualTo(ExitStatus.USAGE_ERROR);
27+
assertThat(got.stderr())
28+
.contains("usage:")
29+
.contains("missing import arguments");
30+
}
31+
32+
@Test
33+
void delimiterMissingValue_returnsUsageError() {
34+
// Given / When — {@code --delimiter} at the tail with no value
35+
CliTestSupport.Captured got = capture(() ->
36+
ImportCommand.run(new String[]{"import", "--delimiter"}));
37+
38+
// Then
39+
assertThat(got.status()).isEqualTo(ExitStatus.USAGE_ERROR);
40+
assertThat(got.stderr()).contains("missing value for --delimiter");
41+
}
42+
43+
@Test
44+
void delimiterMultiCharValue_returnsUsageError() {
45+
// Given / When — delimiter must be exactly one character
46+
CliTestSupport.Captured got = capture(() ->
47+
ImportCommand.run(new String[]{"import", "--delimiter", "||", "file.csv"}));
48+
49+
// Then
50+
assertThat(got.status()).isEqualTo(ExitStatus.USAGE_ERROR);
51+
assertThat(got.stderr()).contains("--delimiter must be exactly one character");
52+
}
53+
54+
@Test
55+
void tooManyPositional_returnsUsageError() {
56+
// Given / When — three positional arguments (only input + optional output allowed)
57+
CliTestSupport.Captured got = capture(() ->
58+
ImportCommand.run(new String[]{"import", "a.csv", "b.vortex", "extra"}));
59+
60+
// Then
61+
assertThat(got.status()).isEqualTo(ExitStatus.USAGE_ERROR);
62+
assertThat(got.stderr()).contains("input path");
63+
}
64+
}
65+
66+
@Nested
67+
class Execution {
68+
69+
@Test
70+
void missingInputFile_returnsFileNotFound(@TempDir Path tmp) {
71+
// Given
72+
Path missing = tmp.resolve("nope.csv");
73+
74+
// When
75+
CliTestSupport.Captured got = capture(() ->
76+
ImportCommand.run(new String[]{"import", missing.toString()}));
77+
78+
// Then
79+
assertThat(got.status()).isEqualTo(ExitStatus.FILE_NOT_FOUND);
80+
assertThat(got.stderr()).contains("file not found");
81+
}
82+
83+
@Test
84+
void validCsv_writesVortexAndReportsResult(@TempDir Path tmp) throws IOException {
85+
// Given — two-row CSV with a string-typed header inferred by the importer
86+
Path csv = tmp.resolve("data.csv");
87+
Files.writeString(csv, "id,value\n1,a\n2,b\n", StandardCharsets.UTF_8);
88+
89+
// When
90+
CliTestSupport.Captured got = capture(() ->
91+
ImportCommand.run(new String[]{"import", csv.toString()}));
92+
93+
// Then — output path derives from input filename, command prints the result line
94+
Path out = tmp.resolve("data.vortex");
95+
assertThat(got.status()).isEqualTo(ExitStatus.OK);
96+
assertThat(out).exists();
97+
assertThat(got.stdout()).contains("written:").contains(out.toString());
98+
}
99+
100+
@Test
101+
void csvWithExplicitOutputPath_usesIt(@TempDir Path tmp) throws IOException {
102+
// Given
103+
Path csv = tmp.resolve("in.csv");
104+
Files.writeString(csv, "id\n1\n2\n", StandardCharsets.UTF_8);
105+
Path out = tmp.resolve("custom.vortex");
106+
107+
// When
108+
CliTestSupport.Captured got = capture(() ->
109+
ImportCommand.run(new String[]{"import", csv.toString(), out.toString()}));
110+
111+
// Then
112+
assertThat(got.status()).isEqualTo(ExitStatus.OK);
113+
assertThat(out).exists();
114+
assertThat(got.stdout()).contains(out.toString());
115+
}
116+
117+
@Test
118+
void csvWithCustomDelimiter_imports(@TempDir Path tmp) throws IOException {
119+
// Given — tab-separated values
120+
Path csv = tmp.resolve("data.tsv");
121+
Files.writeString(csv, "id\tvalue\n1\ta\n2\tb\n", StandardCharsets.UTF_8);
122+
123+
// When
124+
CliTestSupport.Captured got = capture(() ->
125+
ImportCommand.run(new String[]{"import", "--delimiter", "\t", csv.toString()}));
126+
127+
// Then — without the explicit delimiter the import would treat the whole row as one
128+
// column. Success here confirms the {@code --delimiter} flag plumbs through.
129+
// Output filename: input is {@code data.tsv}, deriveOutputPath only strips
130+
// {@code .csv}/{@code .parquet} suffixes, so the result is {@code data.tsv.vortex}.
131+
assertThat(got.status()).isEqualTo(ExitStatus.OK);
132+
assertThat(tmp.resolve("data.tsv.vortex")).exists();
133+
}
134+
}
135+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package io.github.dfa1.vortex.cli;
2+
3+
import org.junit.jupiter.api.Test;
4+
import org.junit.jupiter.api.io.TempDir;
5+
6+
import java.io.IOException;
7+
import java.nio.file.Path;
8+
9+
import static io.github.dfa1.vortex.cli.CliTestSupport.capture;
10+
import static io.github.dfa1.vortex.cli.CliTestSupport.writeSmallVortex;
11+
import static org.assertj.core.api.Assertions.assertThat;
12+
13+
class InspectCommandTest {
14+
15+
@Test
16+
void wrongArity_returnsUsageError() {
17+
// Given / When
18+
CliTestSupport.Captured got = capture(() -> InspectCommand.run(new String[]{"inspect"}));
19+
20+
// Then
21+
assertThat(got.status()).isEqualTo(ExitStatus.USAGE_ERROR);
22+
assertThat(got.stderr()).contains("usage:");
23+
}
24+
25+
@Test
26+
void missingFile_returnsFileNotFound(@TempDir Path tmp) {
27+
// Given
28+
Path missing = tmp.resolve("nope.vortex");
29+
30+
// When
31+
CliTestSupport.Captured got = capture(() ->
32+
InspectCommand.run(new String[]{"inspect", missing.toString()}));
33+
34+
// Then — open() returns null which the run() loop maps to FILE_NOT_FOUND
35+
assertThat(got.status()).isEqualTo(ExitStatus.FILE_NOT_FOUND);
36+
assertThat(got.stderr()).contains("file not found");
37+
}
38+
39+
@Test
40+
void invalidUrl_returnsFileNotFound() {
41+
// Given — URI parser rejects whitespace
42+
// When
43+
CliTestSupport.Captured got = capture(() ->
44+
InspectCommand.run(new String[]{"inspect", "http://bad host/x"}));
45+
46+
// Then — invalid-URL branch returns null too, mapped to FILE_NOT_FOUND
47+
assertThat(got.status()).isEqualTo(ExitStatus.FILE_NOT_FOUND);
48+
assertThat(got.stderr()).contains("invalid URL");
49+
}
50+
51+
@Test
52+
void validFile_printsInspectorReport(@TempDir Path tmp) throws IOException {
53+
// Given
54+
Path file = writeSmallVortex(tmp, "inspect.vortex");
55+
56+
// When
57+
CliTestSupport.Captured got = capture(() ->
58+
InspectCommand.run(new String[]{"inspect", file.toString()}));
59+
60+
// Then — the VortexInspector report header is "Vortex v<n>" then schema + layout
61+
assertThat(got.status()).isEqualTo(ExitStatus.OK);
62+
assertThat(got.stdout())
63+
.startsWith("Vortex v")
64+
.contains("Schema:")
65+
.contains("Layout:");
66+
}
67+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package io.github.dfa1.vortex.cli;
2+
3+
import org.junit.jupiter.api.Test;
4+
import org.junit.jupiter.api.io.TempDir;
5+
6+
import java.io.IOException;
7+
import java.nio.file.Path;
8+
9+
import static io.github.dfa1.vortex.cli.CliTestSupport.capture;
10+
import static io.github.dfa1.vortex.cli.CliTestSupport.writeSmallVortex;
11+
import static org.assertj.core.api.Assertions.assertThat;
12+
13+
class SelectCommandTest {
14+
15+
@Test
16+
void wrongArity_returnsUsageError() {
17+
// Given / When — only "select" + file, no column list
18+
CliTestSupport.Captured got = capture(() -> SelectCommand.run(new String[]{"select", "file"}));
19+
20+
// Then
21+
assertThat(got.status()).isEqualTo(ExitStatus.USAGE_ERROR);
22+
assertThat(got.stderr()).contains("usage:");
23+
}
24+
25+
@Test
26+
void missingFile_returnsFileNotFound(@TempDir Path tmp) {
27+
// Given
28+
Path missing = tmp.resolve("nope.vortex");
29+
30+
// When
31+
CliTestSupport.Captured got = capture(() ->
32+
SelectCommand.run(new String[]{"select", missing.toString(), "id"}));
33+
34+
// Then
35+
assertThat(got.status()).isEqualTo(ExitStatus.FILE_NOT_FOUND);
36+
assertThat(got.stderr()).contains("file not found");
37+
}
38+
39+
@Test
40+
void validFile_projectsRequestedColumn(@TempDir Path tmp) throws IOException {
41+
// Given — single I64 column "id" = [1, 2, 3]
42+
Path file = writeSmallVortex(tmp, "select.vortex");
43+
44+
// When
45+
CliTestSupport.Captured got = capture(() ->
46+
SelectCommand.run(new String[]{"select", file.toString(), "id"}));
47+
48+
// Then — header is the projected column, 3 data rows follow
49+
assertThat(got.status()).isEqualTo(ExitStatus.OK);
50+
assertThat(got.stdout().lines().findFirst()).hasValue("id");
51+
assertThat(got.stdout().lines().count()).isEqualTo(4);
52+
}
53+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package io.github.dfa1.vortex.cli;
2+
3+
import org.junit.jupiter.api.Test;
4+
import org.junit.jupiter.api.io.TempDir;
5+
6+
import java.io.IOException;
7+
import java.nio.file.Path;
8+
9+
import static io.github.dfa1.vortex.cli.CliTestSupport.capture;
10+
import static io.github.dfa1.vortex.cli.CliTestSupport.writeSmallVortex;
11+
import static org.assertj.core.api.Assertions.assertThat;
12+
13+
class StatsCommandTest {
14+
15+
@Test
16+
void wrongArity_returnsUsageError() {
17+
// Given / When
18+
CliTestSupport.Captured got = capture(() -> StatsCommand.run(new String[]{"stats"}));
19+
20+
// Then
21+
assertThat(got.status()).isEqualTo(ExitStatus.USAGE_ERROR);
22+
assertThat(got.stderr()).contains("usage:");
23+
}
24+
25+
@Test
26+
void missingFile_returnsFileNotFound(@TempDir Path tmp) {
27+
// Given
28+
Path missing = tmp.resolve("nope.vortex");
29+
30+
// When
31+
CliTestSupport.Captured got = capture(() -> StatsCommand.run(new String[]{"stats", missing.toString()}));
32+
33+
// Then
34+
assertThat(got.status()).isEqualTo(ExitStatus.FILE_NOT_FOUND);
35+
assertThat(got.stderr()).contains("file not found");
36+
}
37+
38+
@Test
39+
void validFile_printsRowCountAndColumnTable(@TempDir Path tmp) throws IOException {
40+
// Given — 3-row file with column "id" of type I64
41+
Path file = writeSmallVortex(tmp, "stats.vortex");
42+
43+
// When
44+
CliTestSupport.Captured got = capture(() -> StatsCommand.run(new String[]{"stats", file.toString()}));
45+
46+
// Then — header line + column row with the type and observed min/max
47+
assertThat(got.status()).isEqualTo(ExitStatus.OK);
48+
assertThat(got.stdout())
49+
.contains("rows: 3")
50+
.contains("column")
51+
.contains("id")
52+
.contains("i64");
53+
}
54+
}

0 commit comments

Comments
 (0)