clitest_spawn/send_line/send_partial/send_tab/send_signal/wait_for_text/ wait_for_exit/kill_and_reap launch the real compiled xce_test behind a genuine pty (so isatty/g_interactive/libedit behave as a real terminal), with stderr routed to a separate pipe for real stream-separation testing. All waits are poll()-bounded with an explicit timeout so a future regression can't hang the whole test suite. This is the shared infrastructure tasks 2.3, 3.5, and all of Validation phase (4.1-4.4) will build their black-box scenarios on top of. One smoke test proves launch/send/capture/exit-code/stream-separation end to end. 1290/1290 controller suite passing (+1). Task 1.7 of .kiro/specs/xce-test-cli-ux/tasks.md. Foundation phase (1.1-1.7) now complete. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019jaMf9QEwpugH2W9KAuc7f
249 lines
8.5 KiB
C
249 lines
8.5 KiB
C
/*
|
|
* 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 <fcntl.h>
|
|
#include <libgen.h>
|
|
#include <limits.h>
|
|
#include <signal.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/wait.h>
|
|
#include <unistd.h>
|
|
|
|
#include <unity/unity.h>
|
|
|
|
#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 <timestamp>"
|
|
* 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);
|
|
}
|