From f8423ebea413605b42de23f5dd06c5479d3fa1dd Mon Sep 17 00:00:00 2001 From: Ethan Werner Date: Sat, 10 Oct 2026 20:55:33 +0000 Subject: initial commit --- tests/test_shell.cpp | 131 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 tests/test_shell.cpp (limited to 'tests/test_shell.cpp') diff --git a/tests/test_shell.cpp b/tests/test_shell.cpp new file mode 100644 index 0000000..bd10642 --- /dev/null +++ b/tests/test_shell.cpp @@ -0,0 +1,131 @@ +#include "shell.h" + +#include +#include +#include +#include + +static int failures = 0; +static int checks = 0; + +#define CHECK(cond, msg) \ + do { \ + checks++; \ + if (!(cond)) { \ + failures++; \ + fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, (msg)); \ + } \ + } while (0) + +/* Spawn a shell, confirm it is a live process, then reap and free it. */ +static void test_spawn_and_reap(void) +{ + interactive_shell *s = shell_spawn(); + CHECK(s != NULL, "shell_spawn returns a shell"); + if (!s) + return; + CHECK(s->pid > 0, "shell has a live pid"); + CHECK(s->master >= 0, "shell has a master fd"); + + /* Let it settle, then confirm it is still alive (not yet reaped). */ + usleep(100 * 1000); + CHECK(s->pid > 0, "shell still alive before reap"); + + /* Free kills it and reaps; afterwards the pid is 0. */ + shell_free(s); +} + +/* Writing to a freed/closed shell must not crash. */ +static void test_write_after_free_is_safe(void) +{ + interactive_shell *s = shell_spawn(); + CHECK(s != NULL, "shell_spawn"); + if (!s) + return; + shell_free(s); + /* shell_write on a NULL shell is guarded by the caller; on a freed one we + * cannot call it (use-after-free), so just confirm free was clean. */ +} + +/* A shell that `cd`s and then exits must leave its final directory recorded + * (regression: the cwd was read after waitpid, when /proc//cwd is gone, + * so `shell_final_cwd` returned NULL and llsh stayed in the old directory). */ +static void test_final_cwd_after_exit(void) +{ + setenv("SHELL", "/bin/bash", 1); + interactive_shell *s = shell_spawn(); + CHECK(s != NULL, "shell_spawn"); + if (!s) + return; + /* The sleep holds the shell alive after the cd, so the cwd poll (which + * only sees a live process) is guaranteed to observe /tmp. */ + const char *cmd = "cd /tmp\nsleep 0.3\nexit\n"; + for (size_t i = 0; i < strlen(cmd); i++) + CHECK(shell_write(s, cmd + i, 1) == 0, "write to shell"); + bool reaped = false; + for (int i = 0; i < 200 && !reaped; i++) { + if (shell_reap_if_dead(s)) + reaped = true; + else + usleep(25 * 1000); + } + CHECK(reaped, "shell reaped after exit"); + char *cwd = shell_final_cwd(s); + CHECK(cwd != NULL, "final cwd is known"); + if (cwd) { + CHECK(xstreq(cwd, "/tmp"), "final cwd is the shell's last directory"); + free(cwd); + } + shell_free(s); +} + +/* Output the shell prints is captured (sanitized, terminal escapes stripped) + * alongside streaming to stdout, so llsh can record a shell-mode visit + * (commands and output) in the conversation context. `shell_captured` + * returns the text and resets the buffer. */ +static void test_output_captured(void) +{ + setenv("SHELL", "/bin/bash", 1); + interactive_shell *s = shell_spawn(); + CHECK(s != NULL, "shell_spawn"); + if (!s) + return; + /* `echo OUTMARKER-$((1+1))` prints `OUTMARKER-2`, which only appears in + * the command's *output* (the input echo shows the unexpanded form), so a + * match proves the output was captured, not just the typed command. */ + const char *cmd = "echo OUTMARKER-$((1+1))\n"; + for (size_t i = 0; i < strlen(cmd); i++) + CHECK(shell_write(s, cmd + i, 1) == 0, "write to shell"); + char *cap = NULL; + for (int i = 0; i < 200 && !cap; i++) { + usleep(50 * 1000); + char *c = shell_captured(s); + if (strstr(c, "OUTMARKER-2")) + cap = c; /* keep it */ + else + free(c); + } + CHECK(cap != NULL, "captured output contains the command's output"); + if (cap) { + /* The sanitized text must have no terminal escapes left. */ + CHECK(strchr(cap, '\x1B') == NULL, "captured output has no ESC"); + fprintf(stderr, "\n=== CAPTURED (%zu bytes) ===\n%s\n=== END ===\n", + strlen(cap), cap); + } + free(cap); + shell_free(s); +} + +int main(int argc, char **argv) +{ + if (argc > 1 && xstreq(argv[1], "dump")) { + test_output_captured(); + return 0; + } + test_spawn_and_reap(); + test_write_after_free_is_safe(); + test_final_cwd_after_exit(); + test_output_captured(); + printf("shell: %d checks, %d failures\n", checks, failures); + return failures == 0 ? 0 : 1; +} -- cgit v1.3.1