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:
parent
0ef232a013
commit
c709eba813
3 changed files with 467 additions and 1 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue