Skip to content

Commit 7805824

Browse files
dfa1claude
andcommitted
test(cli): cover ViewCommand, TuiCommand, IoWorker
ViewCommand/TuiCommand: pin the pre-terminal control paths — wrong arity (USAGE_ERROR), missing local file and malformed URL (both FILE_NOT_FOUND). The TUI happy path needs a real terminal and stays covered by the TUI tests. IoWorker: cover the executor contract with no I/O — tasks run on the worker thread, runAndAwait flushes prior submits in FIFO order, a throwing task does not kill the loop, submit after close is dropped, and close is idempotent. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 7a18c61 commit 7805824

3 files changed

Lines changed: 209 additions & 0 deletions

File tree

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.nio.file.Path;
7+
8+
import static io.github.dfa1.vortex.cli.CliTestSupport.capture;
9+
import static org.assertj.core.api.Assertions.assertThat;
10+
11+
/// Covers the non-TUI control paths of `tui` — the happy path needs a real terminal
12+
/// and is exercised by the TUI tests; here we pin the argument/open failures that return
13+
/// before any terminal is touched.
14+
class TuiCommandTest {
15+
16+
@Test
17+
void wrongArity_returnsUsageError() {
18+
// Given / When — only the subcommand name, no target
19+
CliTestSupport.Captured result = capture(() -> TuiCommand.run(new String[]{"tui"}));
20+
21+
// Then
22+
assertThat(result.status()).isEqualTo(ExitStatus.USAGE_ERROR);
23+
assertThat(result.stderr()).contains("usage:");
24+
}
25+
26+
@Test
27+
void missingFile_returnsFileNotFound(@TempDir Path tmp) {
28+
// Given — a path that does not exist
29+
Path missing = tmp.resolve("nope.vortex");
30+
31+
// When
32+
CliTestSupport.Captured result = capture(() ->
33+
TuiCommand.run(new String[]{"tui", missing.toString()}));
34+
35+
// Then — open() returns null on a missing local file, run() maps that to FILE_NOT_FOUND
36+
assertThat(result.status()).isEqualTo(ExitStatus.FILE_NOT_FOUND);
37+
assertThat(result.stderr()).contains("file not found");
38+
}
39+
40+
@Test
41+
void malformedUrl_returnsFileNotFound() {
42+
// Given — http(s) prefix forces URI parsing; the space makes it syntactically invalid
43+
String badUrl = "http://a b";
44+
45+
// When
46+
CliTestSupport.Captured result = capture(() ->
47+
TuiCommand.run(new String[]{"tui", badUrl}));
48+
49+
// Then — URISyntaxException is caught and reported as a null handle
50+
assertThat(result.status()).isEqualTo(ExitStatus.FILE_NOT_FOUND);
51+
assertThat(result.stderr()).contains("invalid URL");
52+
}
53+
}
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.nio.file.Path;
7+
8+
import static io.github.dfa1.vortex.cli.CliTestSupport.capture;
9+
import static org.assertj.core.api.Assertions.assertThat;
10+
11+
/// Covers the non-TUI control paths of `view` — the happy path needs a real terminal
12+
/// and is exercised by the TUI tests; here we pin the argument/open failures that return
13+
/// before any terminal is touched.
14+
class ViewCommandTest {
15+
16+
@Test
17+
void wrongArity_returnsUsageError() {
18+
// Given / When — only the subcommand name, no target
19+
CliTestSupport.Captured result = capture(() -> ViewCommand.run(new String[]{"view"}));
20+
21+
// Then
22+
assertThat(result.status()).isEqualTo(ExitStatus.USAGE_ERROR);
23+
assertThat(result.stderr()).contains("usage:");
24+
}
25+
26+
@Test
27+
void missingFile_returnsFileNotFound(@TempDir Path tmp) {
28+
// Given — a path that does not exist
29+
Path missing = tmp.resolve("nope.vortex");
30+
31+
// When
32+
CliTestSupport.Captured result = capture(() ->
33+
ViewCommand.run(new String[]{"view", missing.toString()}));
34+
35+
// Then — open() returns null on a missing local file, run() maps that to FILE_NOT_FOUND
36+
assertThat(result.status()).isEqualTo(ExitStatus.FILE_NOT_FOUND);
37+
assertThat(result.stderr()).contains("file not found");
38+
}
39+
40+
@Test
41+
void malformedUrl_returnsFileNotFound() {
42+
// Given — http(s) prefix forces URI parsing; the space makes it syntactically invalid
43+
String badUrl = "http://a b";
44+
45+
// When
46+
CliTestSupport.Captured result = capture(() ->
47+
ViewCommand.run(new String[]{"view", badUrl}));
48+
49+
// Then — URISyntaxException is caught and reported as a null handle
50+
assertThat(result.status()).isEqualTo(ExitStatus.FILE_NOT_FOUND);
51+
assertThat(result.stderr()).contains("invalid URL");
52+
}
53+
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package io.github.dfa1.vortex.cli.tui;
2+
3+
import org.junit.jupiter.api.Test;
4+
5+
import java.util.List;
6+
import java.util.concurrent.CopyOnWriteArrayList;
7+
import java.util.concurrent.atomic.AtomicBoolean;
8+
import java.util.concurrent.atomic.AtomicReference;
9+
10+
import static org.assertj.core.api.Assertions.assertThat;
11+
import static org.assertj.core.api.Assertions.assertThatCode;
12+
13+
/// Exercises the single-thread executor contract without any file I/O: tasks run on the
14+
/// worker thread, `runAndAwait` flushes prior `submit`s in order, a throwing task does not
15+
/// kill the loop, and `close()` stops accepting work.
16+
class IoWorkerTest {
17+
18+
@Test
19+
void runAndAwait_runsTaskOnTheWorkerThread() throws InterruptedException {
20+
// Given
21+
try (IoWorker sut = new IoWorker("test-io")) {
22+
AtomicReference<Thread> ranOn = new AtomicReference<>();
23+
24+
// When
25+
sut.runAndAwait(() -> ranOn.set(Thread.currentThread()));
26+
27+
// Then — the task executed off the caller thread, on the named worker
28+
assertThat(ranOn.get()).isNotNull().isNotSameAs(Thread.currentThread());
29+
assertThat(ranOn.get().getName()).isEqualTo("test-io");
30+
}
31+
}
32+
33+
@Test
34+
void pending_isZeroAfterTaskCompletes() throws InterruptedException {
35+
// Given
36+
try (IoWorker sut = new IoWorker("test-io")) {
37+
// When
38+
sut.runAndAwait(() -> { });
39+
40+
// Then — the finally block decremented the in-flight counter
41+
assertThat(sut.pending()).isZero();
42+
}
43+
}
44+
45+
@Test
46+
void submit_runsTasksInFifoOrder() throws InterruptedException {
47+
// Given — a blocking runAndAwait at the end flushes the two prior submits
48+
try (IoWorker sut = new IoWorker("test-io")) {
49+
List<Integer> order = new CopyOnWriteArrayList<>();
50+
51+
// When
52+
sut.submit(() -> order.add(1));
53+
sut.submit(() -> order.add(2));
54+
sut.runAndAwait(() -> order.add(3));
55+
56+
// Then
57+
assertThat(order).containsExactly(1, 2, 3);
58+
assertThat(sut.pending()).isZero();
59+
}
60+
}
61+
62+
@Test
63+
void submit_throwingTask_doesNotKillTheWorker() throws InterruptedException {
64+
// Given — the loop must swallow a task's RuntimeException and keep draining
65+
try (IoWorker sut = new IoWorker("test-io")) {
66+
sut.submit(() -> {
67+
throw new IllegalStateException("boom");
68+
});
69+
AtomicBoolean laterRan = new AtomicBoolean(false);
70+
71+
// When
72+
sut.runAndAwait(() -> laterRan.set(true));
73+
74+
// Then — the subsequent task still executed
75+
assertThat(laterRan).isTrue();
76+
}
77+
}
78+
79+
@Test
80+
void submitAfterClose_isDroppedAndDoesNotCount() {
81+
// Given
82+
IoWorker sut = new IoWorker("test-io");
83+
sut.close();
84+
AtomicBoolean ran = new AtomicBoolean(false);
85+
86+
// When — submit observes the volatile closed flag and returns early
87+
sut.submit(() -> ran.set(true));
88+
89+
// Then — nothing enqueued, nothing counted
90+
assertThat(sut.pending()).isZero();
91+
assertThat(ran).isFalse();
92+
}
93+
94+
@Test
95+
void close_isIdempotent() {
96+
// Given
97+
IoWorker sut = new IoWorker("test-io");
98+
99+
// When / Then
100+
sut.close();
101+
assertThatCode(sut::close).doesNotThrowAnyException();
102+
}
103+
}

0 commit comments

Comments
 (0)