feat(xce-test-cli-ux): add in-session source command with cycle detection

cmd_source reuses rcfile_load() unmodified for line execution, adding
an explicit missing-file check (bypassing rcfile_load's silent-missing
policy, which is correct for its one existing startup-rc-file caller
but wrong for an explicit user command) and a realpath()-canonicalized
in-flight path stack that rejects direct or transitive cycles instead
of hanging. Registered in all four command contexts. Translates
rcfile_load's raw positive "aborted" return to -1 so an enclosing
source line correctly sees a transitively-aborted nested source as
failure, not success (preprocess_and_run only treats negative returns
as failure). Proven via real fixture files driven through the black-box
pty harness: a genuine two-file mutual cycle, a missing path, and
ordered execution across a non-cyclic nested source. 1310/1310
controller suite passing (+3).

Task 2.4 of .kiro/specs/xce-test-cli-ux/tasks.md.
Requirements: 13.1, 13.2, 13.3, 13.4, 13.5, 13.6

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019jaMf9QEwpugH2W9KAuc7f
This commit is contained in:
Rongsong Shen 2026-09-09 14:25:37 +00:00
parent 0ef232a013
commit c709eba813
No known key found for this signature in database
GPG key ID: 5BCDAAE0123E9FE2
3 changed files with 467 additions and 1 deletions

View file

@ -74,7 +74,7 @@
- _Boundary: Tab Completion_
- _Depends: 1.7_
- [ ] 2.4 (P) Build the in-session source command with missing-file and cycle detection
- [x] 2.4 (P) Build the in-session source command with missing-file and cycle detection
- Implement a command that reads and executes another file's lines in order, reusing the existing rc-file loading primitive, reporting an explicit error (rather than silently doing nothing) when the file does not exist or cannot be read
- Add cycle detection using a canonicalized in-flight path stack so a file that sources itself, directly or transitively, is rejected with an error instead of hanging
- Ensure a failing line inside the sourced file follows the session's current abort-on-failure/continue-on-error setting, and that nested sourcing (one file sourcing another, non-cyclically) composes correctly
@ -178,6 +178,7 @@
## Implementation Notes
- `src/tools/xce-build.sh controller asan xce.build --incremental` writes its build dir at `src/xce.build/` (relative to the script's own location), not a repo-root `xce.build/` — meson logs are at `src/xce.build/meson-logs/testlog.txt`. (task 1.1)
- New Unity test files need two wiring points, not just meson.build's source list: also register an `extern void test_<module>(void);` + `TEST(test_<module>);` in `src/controller/testsuite/controller_test.c`, mirroring the existing `test_rcfile` registration, or the suite silently never runs. (task 1.1)
- `cmd_source` translates `rcfile_load`'s raw positive "aborted" return to `-1` rather than forwarding it verbatim, because `preprocess_and_run` only treats a *negative* return as failure (`if (rc < 0)`) — forwarding the raw positive value would let an enclosing `source` line silently read a transitively-aborted nested source as success. No test yet exercises this specific nested-abort-propagation case (all 3 required scenarios pass either way) — worth a dedicated test in Validation phase (4.3 or nearby). (task 2.4)
- libedit (this project's pinned NetBSD libedit 3.1) provides GNU-readline-compatible `rl_attempted_completion_function`/`rl_completion_matches` verbatim — confirmed against the actual linked header, not assumed. An ambiguous match needs a SECOND consecutive Tab to list candidates (standard readline/bash double-Tab convention) — later black-box tests using Tab must account for this. Also: `test_test_cli.c`'s `clitest_spawn()` disabling kernel ECHO also silently suppressed libedit's own completion feedback in this libedit build (bug-1792) — use the new `clitest_spawn_ex(sess, path, argv, disable_echo)` with `disable_echo=false` for any future test that needs to observe libedit UI behavior (completion, etc.), not just raw command output. (task 2.3)
- `listfmt_json.c`'s tests only exercise power-of-two double values (Unity's `TEST_ASSERT_EQUAL_DOUBLE` is disabled project-wide, matching `test_host_runusage.c`/`test_e2e_capnp.c`); cJSON's real print path uses shortest-round-trippable-double formatting so fractional values are fine, but task 3.4 should add a real fractional-value (e.g. a resource value like 12.34) round-trip test once actual command fields are wired in, since nothing here proves that end-to-end yet. (task 2.2)
- `confirm_destructive()`'s EINTR-as-decline (9.9) works because it's a single, non-looping `fgets()` call — a naive "robust I/O" retry-on-EINTR wrapper would silently defeat this requirement. `g_interactive` is exposed via a new `xcetest_is_interactive()` accessor (declared in `confirm.h`, mirroring `xcetest_interrupted()`'s pattern) for task 3.3 to reuse. Task 3.3 must short-circuit around `confirm_destructive()` entirely (not pass a parameter into it) when a command's own `--yes` flag was given — the function has no bypass-flag parameter by design. (task 2.1)

View file

@ -2,6 +2,8 @@
#include <errno.h>
#include <getopt.h>
#include <inttypes.h>
#include <limits.h> /* PATH_MAX -- cmd_source's realpath() canonicalization
* buffer (xce-test-cli-ux task 2.4) */
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
@ -144,6 +146,7 @@ extern int cmd_walcompare(int, char*[]);
extern int cmd_snapverify(int, char*[]);
extern int cmd_snapdiff(int, char*[]);
extern int cmd_configcompare(int, char*[]);
extern int cmd_source(int, char*[]);
static int worker_house_keeping(void*);
/* Session verdict / loop state (xce-test-e2e-primitives task 1.1).
@ -287,6 +290,21 @@ static const char USAGE_CONFIGCOMPARE[] =
" member is unreadable or divergent.\n"
"example: configcompare /var/xce/ctrl-1 /var/xce/ctrl-2 /var/xce/ctrl-3\n";
static const char USAGE_SOURCE[] =
"source <file>\n"
" <file> path to a file containing xce_test commands, one per line\n"
" (required)\n"
" Reads and executes each line of <file> in order, exactly as if it\n"
" had been typed at the current prompt -- including a nested `source`\n"
" command, so one script file can compose others. A file that does\n"
" not exist or cannot be read is rejected with an explicit error and\n"
" no line of it is executed; a file that would source itself,\n"
" directly or transitively, is rejected as a cycle instead of\n"
" hanging. A failing line inside the file follows the session's\n"
" current -e/+e/continue-on-error setting (see `help set`), exactly\n"
" as it does for lines typed at the top-level prompt.\n"
"example: source /path/to/script.xt\n";
static const char USAGE_WORKER[] =
"worker\n"
" (no arguments)\n"
@ -567,6 +585,7 @@ static command_t top_commands[] = {
USAGE_SNAPDIFF },
{ "configcompare", cmd_configcompare, "Compare active config hash across members",
USAGE_CONFIGCOMPARE },
{ "source", cmd_source, "Run commands from a file", USAGE_SOURCE },
{ "quit", cmd_quit, "Quit xce test", USAGE_QUIT },
{ (char*)NULL, (cmd_func_t)NULL, (char*)NULL, (const char*)NULL }
};
@ -586,6 +605,7 @@ static command_t worker_commands[] = {
{ "jfail", cmd_jobfail, "tell controller jobfail", USAGE_JOBFAIL },
{ "resupdate", cmd_resupdate, "update res vals", USAGE_RESUPDATE },
{ "echo", cmd_echo, "Print a message", USAGE_ECHO },
{ "source", cmd_source, "Run commands from a file", USAGE_SOURCE },
{ (char*)NULL, (cmd_func_t)NULL, (char*)NULL, (const char*)NULL }
};
@ -628,6 +648,7 @@ static command_t client_commands[] = {
{ "taskrun", cmd_runtasks, "run tasks", USAGE_TASKRUN },
{ "help", cmd_help, "Display this text", USAGE_HELP },
{ "echo", cmd_echo, "Print a message", USAGE_ECHO },
{ "source", cmd_source, "Run commands from a file", USAGE_SOURCE },
{ "quit", cmd_quit, "Quit xce test", USAGE_QUIT },
{ (char*)NULL, (cmd_func_t)NULL, (char*)NULL, (const char*)NULL }
};
@ -645,6 +666,7 @@ static command_t scheduler_commands[] = {
{ "getconf", cmd_getconf, "Get conf of cluster", USAGE_GETCONF },
{ "setpend", cmd_setpend, "set job pend reason", USAGE_SETPEND },
{ "echo", cmd_echo, "Print a message", USAGE_ECHO },
{ "source", cmd_source, "Run commands from a file", USAGE_SOURCE },
{ (char*)NULL, (cmd_func_t)NULL, (char*)NULL, (const char*)NULL }
};
@ -1418,6 +1440,157 @@ int cmd_configcompare(int argc, char* argv[])
return (rc == 0) ? 0 : -1;
}
/* --------------------------------------------------------------------
* source <file> -- in-session script sourcing (xce-test-cli-ux task 2.4,
* Requirements 13.1-13.6; 13.7's TTY-condition clarification relating
* `source` to the Confirmation Gate is a separate, later validation task
* and is NOT implemented here).
*
* Reuses rcfile_load() (rcfile.c/h, xce-test-rc-file spec) UNCHANGED as the
* actual line-reading/dispatch loop, passing preprocess_and_run as its
* runner -- the same per-line command dispatcher the interactive REPL loop
* and the startup rc-file loader already use. rcfile.c itself is not
* touched: its own missing-file-is-silent-success policy is correct for
* its one other caller (the startup rc file) but wrong for an explicit
* user command, so cmd_source layers its own missing-file check (access())
* and cycle detection (a small in-flight canonical-path stack) on top,
* entirely in this file -- per research.md's "source built directly on
* rcfile_load" decision.
*
* Cycle detection: source_push_path()/source_pop_path() maintain a static
* LIFO stack of realpath()-canonicalized in-flight source paths.
* source_push_path() scans the stack for the same canonical path already
* in flight (a real cycle -- including through symlinks or a differently-
* spelled relative path, since realpath() runs before the scan) and
* returns -ELOOP without pushing if found; otherwise it pushes and returns
* 0. cmd_source calls source_pop_path() on EVERY exit path once the push
* succeeded (success, or rcfile_load returning nonzero because a line
* inside the file failed under -e/abort-on-failure semantics) -- this is
* the "path stack popped exactly once per successful push, on every exit
* path" invariant design.md requires. When source_push_path() itself
* fails (cycle detected), nothing was pushed, so cmd_source returns
* immediately WITHOUT calling source_pop_path() -- there is nothing to
* pop.
*
* Abort/continue-on-error composition (Req 13.3): a failing line inside
* the sourced file makes preprocess_and_run (the runner) return 1 once
* g_continue_on_error is false, which is exactly rcfile_load's own
* "stop on nonzero runner return" mechanism -- no extra code is needed
* here for that part; it falls out of reusing rcfile_load() unchanged.
* cmd_source's own return value then translates rcfile_load's result
* (0 == every line ran without requesting an abort) into this codebase's
* negative-errno failure convention (nonzero -> a generic -1 failure,
* mirroring cmd_configcompare's/cmd_waldump's own "rc == 0 ? 0 : -1"
* pattern) rather than forwarding rcfile_load's raw positive "stop" signal
* verbatim: forwarding it unchanged would be silently treated as SUCCESS
* by an enclosing preprocess_and_run call (only rc<0 counts as failure
* there), which would incorrectly let an enclosing script's later lines
* keep running past a `source` that itself aborted under -e, and would
* break that composition for nested sourcing (source A containing
* "source B", where B aborts). Translating to a negative value here makes
* a failed `source` compose correctly at every nesting depth, exactly
* like any other failing command.
*
* Nested sourcing (Req 13.4): "source" is registered in every command
* table (see the four command_t[] tables below); a line inside a sourced
* file that itself reads "source <otherfile>" is dispatched by
* preprocess_and_run/execute_line exactly like any other line, so nested
* sourcing falls out of dispatch being generic. The path stack is a
* single process-wide stack (not scoped per active context), so a cycle
* is caught regardless of which context each nesting level was sourced
* from. */
#define SOURCE_MAX_DEPTH 64
static char* g_source_stack[SOURCE_MAX_DEPTH];
static int g_source_depth = 0;
/* Returns 0 and pushes `canonical_path` if it is not already in flight, or
* -ELOOP without pushing if it is (a cycle -- the same canonicalized path
* sourcing itself, directly or transitively). Returns -ENOMEM if the stack
* is exhausted (SOURCE_MAX_DEPTH levels of DISTINCT, non-cyclic nested
* sourcing) or a heap copy of the path could not be made -- either way,
* nothing is pushed, so the caller must not call source_pop_path(). */
static int source_push_path(const char* canonical_path)
{
int i;
for (i = 0; i < g_source_depth; i++) {
if (strcmp(g_source_stack[i], canonical_path) == 0) {
return -ELOOP;
}
}
if (g_source_depth >= SOURCE_MAX_DEPTH) {
return -ENOMEM;
}
g_source_stack[g_source_depth] = strdup(canonical_path);
if (g_source_stack[g_source_depth] == NULL) {
return -ENOMEM;
}
g_source_depth++;
return 0;
}
/* Pops the most recently pushed path (LIFO). A no-op if the stack is
* already empty (defensive; every real call site only calls this after a
* source_push_path() call that itself returned 0). */
static void source_pop_path(void)
{
if (g_source_depth <= 0) {
return;
}
g_source_depth--;
free(g_source_stack[g_source_depth]);
g_source_stack[g_source_depth] = NULL;
}
/* source <file> -- reads and executes each line of <file> as if it had
* been typed at the current prompt, in order (Req 13.1). A missing or
* unreadable file is rejected with an explicit stderr error BEFORE
* rcfile_load() is ever called, bypassing its silent-missing-file
* behavior entirely (Req 13.2) -- rcfile.c itself is unmodified. */
int cmd_source(int argc, char* argv[])
{
char canonical[PATH_MAX];
int rc;
if (argc != 2) {
fprintf(stderr, "usage: source <file>\n");
return -EINVAL;
}
if (access(argv[1], R_OK) != 0) {
fprintf(stderr, "source: %s: %s\n", argv[1], strerror(errno));
return -errno;
}
if (realpath(argv[1], canonical) == NULL) {
fprintf(stderr, "source: %s: %s\n", argv[1], strerror(errno));
return -errno;
}
rc = source_push_path(canonical);
if (rc == -ELOOP) {
fprintf(stderr,
"source: %s: cycle detected (file sources itself, directly "
"or transitively)\n",
canonical);
return rc;
} else if (rc != 0) {
fprintf(stderr, "source: %s: %s\n", canonical, strerror(-rc));
return rc;
}
rc = rcfile_load(argv[1], preprocess_and_run);
source_pop_path(); /* popped on EVERY exit path past the push above */
return (rc == 0) ? 0 : -1;
}
int cmd_up(int argc, char* argv[])
{
if (active_ctx->parent != NULL) {

View file

@ -785,9 +785,301 @@ static void test_tab_completion_context_scoped_single_and_multi_match(void)
clitest_buf_free(&err);
}
/* ------------------------------------------------------------------ */
/* source <file> (task 2.4, Requirements 13.1-13.6) */
/* ------------------------------------------------------------------ */
/*
* cmd_source lives in test.c itself, which -- like every earlier
* xce_test-CLI-surface task -- is NOT linked into controller_unit_test
* (it pulls in libedit/cJSON/libxce/Cap'n Proto/OpenSSL and defines its
* own main()). The black-box pty harness above (task 1.7) is therefore
* the right vehicle here too: these tests spawn the REAL xce_test binary,
* write REAL fixture files to disk, and send `source <path>` as a real
* REPL command, exactly as the task's own guidance anticipated.
*
* Fixture helpers below mirror test_rcfile.c's fresh_dir()/cleanup_dir()/
* write_file() convention (a per-test mkdtemp() dir, removed afterward)
* so these tests don't leak fixture files into the shared /tmp this
* sandbox already tracks (bug-924).
*/
static char clitest_fixture_dir[256];
static void clitest_fixture_fresh_dir(void)
{
strcpy(clitest_fixture_dir, "/tmp/xce_source_test_XXXXXX");
TEST_ASSERT_NOT_NULL(mkdtemp(clitest_fixture_dir));
}
static void clitest_fixture_cleanup_dir(void)
{
char cmd[512];
snprintf(cmd, sizeof(cmd), "rm -rf %s", clitest_fixture_dir);
system(cmd);
}
static void clitest_fixture_write_file(const char* path, const char* content)
{
FILE* f = fopen(path, "w");
TEST_ASSERT_NOT_NULL(f);
TEST_ASSERT_EQUAL_INT((int)strlen(content),
(int)fwrite(content, 1, strlen(content), f));
fclose(f);
}
/* Requirement 13.5: a two-file MUTUAL cycle (A sources B, B sources A)
* must be rejected with an explicit error -- never hang or exhaust
* resources. Also proves the session survives the rejection (still
* responsive to the next command) and that the failed `source` marks the
* session failed (exit code 1), exactly like any other failing command. */
static void test_source_mutual_cycle_rejected_with_error(void)
{
char xce_test_path[PATH_MAX];
char* const argv[] = { "xce_test", "--norc", NULL };
clitest_session_t sess;
clitest_buf_t out;
clitest_buf_t err;
int exit_code = -999;
char path_a[300];
char path_b[300];
char source_cmd[600];
TEST_ASSERT_EQUAL_INT_MESSAGE(
0, resolve_xce_test_path(xce_test_path, sizeof(xce_test_path)),
"must be able to resolve the sibling xce_test binary's path");
clitest_fixture_fresh_dir();
snprintf(path_a, sizeof(path_a), "%s/a.txt", clitest_fixture_dir);
snprintf(path_b, sizeof(path_b), "%s/b.txt", clitest_fixture_dir);
{
char content_a[350];
char content_b[350];
snprintf(content_a, sizeof(content_a), "source %s\n", path_b);
snprintf(content_b, sizeof(content_b), "source %s\n", path_a);
clitest_fixture_write_file(path_a, content_a);
clitest_fixture_write_file(path_b, content_b);
}
clitest_buf_init(&out);
clitest_buf_init(&err);
TEST_ASSERT_EQUAL_INT_MESSAGE(
0, clitest_spawn(&sess, xce_test_path, argv),
"clitest_spawn must launch xce_test behind a real pty");
TEST_ASSERT_TRUE_MESSAGE(
clitest_wait_for_text(&sess, &out, &err, "xce>", false, 5000),
"expected the initial 'xce>' interactive prompt on stdout");
snprintf(source_cmd, sizeof(source_cmd), "source %s", path_a);
TEST_ASSERT_EQUAL_INT(0, clitest_send_line(&sess, source_cmd));
TEST_ASSERT_TRUE_MESSAGE(
clitest_wait_for_text(&sess, &out, &err, "cycle detected", true,
5000),
"a mutual source cycle must be rejected with an explicit 'cycle "
"detected' error on stderr, not a hang (Requirement 13.5)");
/* The session must still be responsive -- the cycle rejection must not
* have hung or corrupted it. */
TEST_ASSERT_EQUAL_INT(
0, clitest_send_line(&sess, "echo cycle_test_still_alive_marker"));
TEST_ASSERT_TRUE_MESSAGE(
clitest_wait_for_text(&sess, &out, &err,
"cycle_test_still_alive_marker", false, 5000),
"the session must remain usable after a rejected source cycle");
TEST_ASSERT_EQUAL_INT(0, clitest_send_line(&sess, "quit"));
TEST_ASSERT_EQUAL_INT_MESSAGE(
0, clitest_wait_for_exit(&sess, &out, &err, 5000, &exit_code),
"xce_test did not exit within the harness's safety timeout after "
"'quit'");
TEST_ASSERT_EQUAL_INT_MESSAGE(
1, exit_code,
"the rejected source cycle must mark the session failed, exactly "
"like any other failing command");
clitest_kill_and_reap(&sess);
clitest_close(&sess);
clitest_buf_free(&out);
clitest_buf_free(&err);
clitest_fixture_cleanup_dir();
}
/* Requirement 13.2: `source`ing a path that does not exist must report an
* explicit error and must NOT treat any line of it as executed -- the
* opposite of rcfile_load()'s own silent-missing-file policy, which is
* correct for the startup rc file but wrong for this explicit command. */
static void test_source_missing_file_rejected_with_error(void)
{
char xce_test_path[PATH_MAX];
char* const argv[] = { "xce_test", "--norc", NULL };
clitest_session_t sess;
clitest_buf_t out;
clitest_buf_t err;
int exit_code = -999;
char missing_path[300];
char source_cmd[400];
TEST_ASSERT_EQUAL_INT_MESSAGE(
0, resolve_xce_test_path(xce_test_path, sizeof(xce_test_path)),
"must be able to resolve the sibling xce_test binary's path");
clitest_fixture_fresh_dir();
snprintf(missing_path, sizeof(missing_path), "%s/does_not_exist.txt",
clitest_fixture_dir);
clitest_buf_init(&out);
clitest_buf_init(&err);
TEST_ASSERT_EQUAL_INT_MESSAGE(
0, clitest_spawn(&sess, xce_test_path, argv),
"clitest_spawn must launch xce_test behind a real pty");
TEST_ASSERT_TRUE_MESSAGE(
clitest_wait_for_text(&sess, &out, &err, "xce>", false, 5000),
"expected the initial 'xce>' interactive prompt on stdout");
snprintf(source_cmd, sizeof(source_cmd), "source %s", missing_path);
TEST_ASSERT_EQUAL_INT(0, clitest_send_line(&sess, source_cmd));
TEST_ASSERT_TRUE_MESSAGE(
clitest_wait_for_text(&sess, &out, &err, "does_not_exist.txt", true,
5000),
"a missing source file must be reported with an explicit error "
"naming the path, on stderr (Requirement 13.2)");
TEST_ASSERT_EQUAL_INT(
0, clitest_send_line(&sess, "echo missing_test_still_alive_marker"));
TEST_ASSERT_TRUE_MESSAGE(
clitest_wait_for_text(&sess, &out, &err,
"missing_test_still_alive_marker", false,
5000),
"the session must remain usable after a rejected missing-file "
"source");
TEST_ASSERT_EQUAL_INT(0, clitest_send_line(&sess, "quit"));
TEST_ASSERT_EQUAL_INT_MESSAGE(
0, clitest_wait_for_exit(&sess, &out, &err, 5000, &exit_code),
"xce_test did not exit within the harness's safety timeout after "
"'quit'");
TEST_ASSERT_EQUAL_INT_MESSAGE(
1, exit_code,
"the rejected missing-file source must mark the session failed");
clitest_kill_and_reap(&sess);
clitest_close(&sess);
clitest_buf_free(&out);
clitest_buf_free(&err);
clitest_fixture_cleanup_dir();
}
/* Requirement 13.1/13.4: nested, NON-cyclic sourcing (A sources B, B does
* not source back to A) must compose correctly -- every line of BOTH files
* runs, in order, exactly as if all of it had been typed at the prompt. */
static void test_source_nested_noncyclic_executes_every_line(void)
{
char xce_test_path[PATH_MAX];
char* const argv[] = { "xce_test", "--norc", NULL };
clitest_session_t sess;
clitest_buf_t out;
clitest_buf_t err;
int exit_code = -999;
char path_a[300];
char path_b[300];
char source_cmd[600];
TEST_ASSERT_EQUAL_INT_MESSAGE(
0, resolve_xce_test_path(xce_test_path, sizeof(xce_test_path)),
"must be able to resolve the sibling xce_test binary's path");
clitest_fixture_fresh_dir();
snprintf(path_a, sizeof(path_a), "%s/a.txt", clitest_fixture_dir);
snprintf(path_b, sizeof(path_b), "%s/b.txt", clitest_fixture_dir);
clitest_fixture_write_file(
path_b, "echo nested_marker_b_line1\necho nested_marker_b_line2\n");
{
char content_a[400];
snprintf(content_a, sizeof(content_a),
"echo nested_marker_a_line1\nsource %s\necho "
"nested_marker_a_line2\n",
path_b);
clitest_fixture_write_file(path_a, content_a);
}
clitest_buf_init(&out);
clitest_buf_init(&err);
TEST_ASSERT_EQUAL_INT_MESSAGE(
0, clitest_spawn(&sess, xce_test_path, argv),
"clitest_spawn must launch xce_test behind a real pty");
TEST_ASSERT_TRUE_MESSAGE(
clitest_wait_for_text(&sess, &out, &err, "xce>", false, 5000),
"expected the initial 'xce>' interactive prompt on stdout");
snprintf(source_cmd, sizeof(source_cmd), "source %s", path_a);
TEST_ASSERT_EQUAL_INT(0, clitest_send_line(&sess, source_cmd));
TEST_ASSERT_TRUE_MESSAGE(
clitest_wait_for_text(&sess, &out, &err, "nested_marker_a_line2",
false, 5000),
"every line of the outer file (A) must run, including the one "
"after the nested `source` (Requirement 13.1/13.4)");
TEST_ASSERT_NOT_NULL_MESSAGE(
strstr(out.data, "nested_marker_a_line1"),
"A's line before the nested source must have run");
TEST_ASSERT_NOT_NULL_MESSAGE(
strstr(out.data, "nested_marker_b_line1"),
"B's first line must have run via the nested source");
TEST_ASSERT_NOT_NULL_MESSAGE(
strstr(out.data, "nested_marker_b_line2"),
"B's second line must have run via the nested source");
/* Order matters too: A's pre-source line, then both of B's lines (in
* order), then A's post-source line. */
{
const char* p1 = strstr(out.data, "nested_marker_a_line1");
const char* p2 = strstr(out.data, "nested_marker_b_line1");
const char* p3 = strstr(out.data, "nested_marker_b_line2");
const char* p4 = strstr(out.data, "nested_marker_a_line2");
TEST_ASSERT_NOT_NULL(p1);
TEST_ASSERT_NOT_NULL(p2);
TEST_ASSERT_NOT_NULL(p3);
TEST_ASSERT_NOT_NULL(p4);
TEST_ASSERT_TRUE_MESSAGE(p1 < p2 && p2 < p3 && p3 < p4,
"nested source's lines must execute in "
"the correct relative order");
}
TEST_ASSERT_EQUAL_INT(0, clitest_send_line(&sess, "quit"));
TEST_ASSERT_EQUAL_INT_MESSAGE(
0, clitest_wait_for_exit(&sess, &out, &err, 5000, &exit_code),
"xce_test did not exit within the harness's safety timeout after "
"'quit'");
TEST_ASSERT_EQUAL_INT_MESSAGE(
0, exit_code,
"a fully successful nested source (every line is a succeeding "
"echo) must not mark the session failed");
clitest_kill_and_reap(&sess);
clitest_close(&sess);
clitest_buf_free(&out);
clitest_buf_free(&err);
clitest_fixture_cleanup_dir();
}
void test_test_cli(void)
{
UnitySetTestFile(__FILE__);
RUN_TEST(test_smoke_launch_send_echo_capture_stdout_and_exit_code);
RUN_TEST(test_tab_completion_context_scoped_single_and_multi_match);
RUN_TEST(test_source_mutual_cycle_rejected_with_error);
RUN_TEST(test_source_missing_file_rejected_with_error);
RUN_TEST(test_source_nested_noncyclic_executes_every_line);
}