summaryrefslogtreecommitdiffstats
path: root/tests/test_shell.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'tests/test_shell.cpp')
-rw-r--r--tests/test_shell.cpp131
1 files changed, 131 insertions, 0 deletions
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 <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+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/<pid>/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;
+}