/* * test_xcetest_process_entry.c -- xce-test-cli-ux task 1.6, Requirements * 4.1, 4.2, 4.3 (Process-Level Help and Version Discovery). * * These are black-box, subprocess tests: they exec the REAL, built * xce_test binary (a sibling of this test binary in the same build * directory -- see resolve_xce_test_path()) with a process-level flag and * inspect its exit code and captured stdout. This is deliberately NOT a * plain unit test that calls argp_parse()/parse_opt() in-process: what's * under test is main()'s own process-level argv handling, including * whether it reaches the interactive/scripted REPL loop at all, which can * only be observed by actually running the process to completion. * * This is a small, self-contained subprocess harness scoped to this one * task's needs (process-level -h/--help/--version) -- NOT the reusable * pty-driven black-box CLI harness that task 1.7 builds separately (that * one will live in test_test_cli.c and is needed for interactive-only * surfaces like tab completion that a plain pipe can't exercise). * * resolve_xce_test_path() below is deliberately non-static (task 1.7): * test_test_cli.c's pty-driven harness needs the exact same "find the real * built xce_test binary next to this test binary" logic and reuses this * copy via an `extern` declaration rather than duplicating it -- see this * project's own consolidation-over-copy-paste convention. */ #define _GNU_SOURCE #include #include #include #include #include #include #include #include #include #include #define CAPTURE_BUF_SIZE 16384 #define CHILD_TIMEOUT_TENTHS 50 /* 5 seconds, polled every 100ms */ /* xce_test is always built as a sibling of this test binary in the same * meson build directory (src/controller/meson.build builds both `xce_test` * and `controller_unit_test` into the same target dir) -- resolving via * /proc/self/exe's directory avoids depending on the caller's cwd or an * env var that could go stale. */ int resolve_xce_test_path(char* out, size_t outsz) { char self[PATH_MAX]; ssize_t n; char* dir; n = readlink("/proc/self/exe", self, sizeof(self) - 1); if (n <= 0) { return -1; } self[n] = '\0'; dir = dirname(self); /* dirname() may modify its argument in place; * `self` is a disposable local copy, so that's * fine here. */ if (snprintf(out, outsz, "%s/xce_test", dir) >= (int)outsz) { return -1; } return 0; } /* Runs xce_test_path with argv (NULL-terminated; argv[0] conventionally * the program name) with stdin attached to /dev/null -- so that if a * regression makes it fall through into the interactive REPL instead of * exiting, readline() sees immediate EOF rather than hanging -- and * captures its stdout into out_buf. Kills and fails the test if the child * does not exit within CHILD_TIMEOUT_TENTHS/10 seconds, so a genuine hang * can't wedge the whole suite. */ static void run_and_capture(const char* xce_test_path, char* const argv[], char* out_buf, size_t out_buf_size, int* exit_code) { int outpipe[2]; pid_t pid; int status = 0; int elapsed = 0; pid_t waited; TEST_ASSERT_EQUAL_INT(0, pipe(outpipe)); pid = fork(); TEST_ASSERT_TRUE(pid >= 0); if (pid == 0) { int devnull = open("/dev/null", O_RDONLY); if (devnull >= 0) { dup2(devnull, STDIN_FILENO); close(devnull); } dup2(outpipe[1], STDOUT_FILENO); close(outpipe[0]); close(outpipe[1]); execv(xce_test_path, argv); _exit(127); /* only reached if execv() itself failed */ } close(outpipe[1]); for (;;) { waited = waitpid(pid, &status, WNOHANG); if (waited == pid) { break; } if (elapsed >= CHILD_TIMEOUT_TENTHS) { kill(pid, SIGKILL); waitpid(pid, &status, 0); close(outpipe[0]); TEST_FAIL_MESSAGE( "xce_test did not exit within timeout -- it likely fell " "through into the REPL instead of exiting after a " "process-level help/version flag"); } usleep(100000); elapsed++; } { ssize_t total = 0; ssize_t r; while (total < (ssize_t)out_buf_size - 1) { r = read(outpipe[0], out_buf + total, out_buf_size - 1 - (size_t)total); if (r <= 0) { break; } total += r; } out_buf[total] = '\0'; } close(outpipe[0]); TEST_ASSERT_TRUE(WIFEXITED(status)); *exit_code = WEXITSTATUS(status); } /* Word-wrapped help text can break a multi-word phrase across lines at any * space -- argp wraps the doc string to the terminal width. Folding * newlines to spaces before substring-matching makes assertions robust to * exactly where those wraps land. */ static void flatten_newlines(char* s) { for (; *s; s++) { if (*s == '\n') { *s = ' '; } } } static void test_help_flag_prints_usage_and_exits_without_repl(void) { char xce_test_path[PATH_MAX]; char out[CAPTURE_BUF_SIZE]; int exit_code; char* const argv[] = { "xce_test", "--help", NULL }; TEST_ASSERT_EQUAL_INT( 0, resolve_xce_test_path(xce_test_path, sizeof(xce_test_path))); run_and_capture(xce_test_path, argv, out, sizeof(out), &exit_code); TEST_ASSERT_EQUAL_INT(0, exit_code); TEST_ASSERT_NOT_NULL(strstr(out, "Usage:")); /* Never reached the interactive session's top-level prompt. */ TEST_ASSERT_NULL(strstr(out, "xce>")); } /* Requirement 4.3: the process-level help output must describe the * in-session `help` command as the way to list/learn available commands * -- not just document its own --help/-?/-h flags. */ static void test_help_flag_describes_insession_help_command(void) { char xce_test_path[PATH_MAX]; char out[CAPTURE_BUF_SIZE]; int exit_code; char* const argv[] = { "xce_test", "--help", NULL }; TEST_ASSERT_EQUAL_INT( 0, resolve_xce_test_path(xce_test_path, sizeof(xce_test_path))); run_and_capture(xce_test_path, argv, out, sizeof(out), &exit_code); TEST_ASSERT_EQUAL_INT(0, exit_code); flatten_newlines(out); TEST_ASSERT_NOT_NULL_MESSAGE( strstr(out, "type `help' at the prompt to list"), "xce_test --help must describe the in-session `help' command as " "the way to list/learn available commands (Requirement 4.3)"); } /* Requirement 4.1 explicitly calls out `-h` alongside `--help`. GNU argp's * OWN built-in short help alias is `-?`, not `-h` -- so unlike --help/-?, * this is not automatic and must be verified (and, if missing, added). */ static void test_dash_h_flag_prints_usage_and_exits_without_repl(void) { char xce_test_path[PATH_MAX]; char out[CAPTURE_BUF_SIZE]; int exit_code; char* const argv[] = { "xce_test", "-h", NULL }; TEST_ASSERT_EQUAL_INT( 0, resolve_xce_test_path(xce_test_path, sizeof(xce_test_path))); run_and_capture(xce_test_path, argv, out, sizeof(out), &exit_code); TEST_ASSERT_EQUAL_INT(0, exit_code); TEST_ASSERT_NOT_NULL(strstr(out, "Usage:")); TEST_ASSERT_NULL(strstr(out, "xce>")); } static void test_version_flag_prints_version_and_exits_without_repl(void) { char xce_test_path[PATH_MAX]; char out[CAPTURE_BUF_SIZE]; int exit_code; char* const argv[] = { "xce_test", "--version", NULL }; TEST_ASSERT_EQUAL_INT( 0, resolve_xce_test_path(xce_test_path, sizeof(xce_test_path))); run_and_capture(xce_test_path, argv, out, sizeof(out), &exit_code); TEST_ASSERT_EQUAL_INT(0, exit_code); TEST_ASSERT_NOT_NULL(strstr(out, "xce")); /* xce_compose_version_ex()'s output includes a "built " * segment -- distinguishes real version info from a coincidental * "xce" substring elsewhere. */ TEST_ASSERT_NOT_NULL(strstr(out, "built")); TEST_ASSERT_NULL(strstr(out, "Usage:")); TEST_ASSERT_NULL(strstr(out, "xce>")); } void test_xcetest_process_entry(void) { UnitySetTestFile(__FILE__); RUN_TEST(test_help_flag_prints_usage_and_exits_without_repl); RUN_TEST(test_help_flag_describes_insession_help_command); RUN_TEST(test_dash_h_flag_prints_usage_and_exits_without_repl); RUN_TEST(test_version_flag_prints_version_and_exits_without_repl); }