diff --git a/.kiro/specs/xce-test-cli-ux/tasks.md b/.kiro/specs/xce-test-cli-ux/tasks.md index 90b45e00..378bd8c0 100644 --- a/.kiro/specs/xce-test-cli-ux/tasks.md +++ b/.kiro/specs/xce-test-cli-ux/tasks.md @@ -130,7 +130,7 @@ - _Boundary: JSON List Formatter_ - _Depends: 1.1, 2.2, 2.5_ -- [ ] 3.5 Generate and consolidate every command's help content +- [x] 3.5 Generate and consolidate every command's help content - Regenerate each command's flag-listing help text from the shared descriptor table (built in 1.2), and revise each command's remaining help text to include a purpose description, at least one example invocation, which contexts it is valid in when shared across contexts, and any related command or scripting side effect - Where a shared command's behavior differs by active context, describe the current context's specific behavior rather than a generic description - Ensure a help lookup for a name not found in the active context reports it as not found there without checking other contexts, and correct the existing malformed not-found message text @@ -176,6 +176,7 @@ - _Depends: 1.6, 2.3, 3.5, 1.7_ ## Implementation Notes +- `command_t` gained `flagspec`/`flagspec_count`/`usage_tail` fields; `cmd_help` now composes description(`usage` head) -> generated `flagspec_render_help()` block -> `usage_tail` (example/related notes) for all 22 flag-accepting commands, with hand-written per-flag lines deleted from their `USAGE_X`/`USAGE_X_TAIL` constants so the flagspec table is the only place a flag's short/long/required/default lives. Since the four `command_t` tables (`top_commands` etc.) appear before the 22 `flagspec_t` tables in the file, each is forward-declared with an explicit literal size (e.g. `static const flagspec_t jobkill_flags[3];`) rather than the previous `#define X_NFLAGS (sizeof(x_flags)/sizeof(x_flags[0]))` self-computing macro — the literal must now be kept in sync by hand, though any mismatch between the forward declaration and the real definition is a compiler "conflicting types" build error, never a silent drift (review verified this is standard, portable C, and hand-counted several tables' entries against their literals). A follow-up could reorder the flagspec_t tables above the command_t tables to restore the sizeof-based macro and drop this literal-maintenance requirement entirely, but it wasn't required for this task. `join_flags`/`JOIN_FLAGS_COUNT` (a separate translation unit, join_worker_args.c/.h) uses the standard `extern const T arr[N]` header/source split instead, since `extern` against a `static` definition isn't valid — no literal-sync risk there. The five destructive commands' help text now names the confirmation gate and `-y`/`set -y` bypass, scoped accurately (`qctrl`'s note covers only `-i`/`-c`, `hostctrl`'s only `-c`, matching Requirement 9.5's exact enumeration, not their full flag sets). (task 3.5) - `workerlist`/`joblist`/`reslist` all take the identical long-only `--json` flag (via each command's own `flagspec_t` table); JSON rows emit RAW numeric values for fields the table displays in a humanized/formatted form (`flags` as an int not a hex string, timestamps as raw epoch seconds not `format_job_time`'s human string, `maxmem`/`maxswap`/`maxtmp` as raw bytes not `format_bytes_human`'s string) — deliberate, since Requirement 5.1 asks for "the same fields" (value present) not identical display formatting, and a raw value is strictly more script-consumable. The field-mapping logic was extracted to a new pure module, `listrow_json.c`/`.h` (no RPC/networking/global-state coupling), following the `joblist_display.c` extraction precedent, with full unit coverage in `test_listrow_json.c` (a new standalone `listrow_json_unit_test` meson target, not counted in the main suite's test total) including the fractional-value (`cpufactor` 12.34) real cJSON-print-then-reparse round-trip the task-2.2 note asked for. `joblist_query` gained a 5th `json_mode` parameter; its other two callers (`expect-jobs`/`expect-job`) now pass a literal `0` and are behaviorally unchanged (verified: both `print_table` and `json_mode` false skips both branches exactly as before). Live-cluster E2E visual inspection of real JSON output was not done — same "genuinely untestable without a live cluster" situation task 2.5 already documented for these commands' inlined fetch+print bodies; coverage instead comes from the extracted module's unit tests plus a black-box test proving the flag parses/executes without a flag-parse error. (task 3.4) - `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_(void);` + `TEST(test_);` in `src/controller/testsuite/controller_test.c`, mirroring the existing `test_rcfile` registration, or the suite silently never runs. (task 1.1) diff --git a/src/controller/testsuite/join_worker_args.c b/src/controller/testsuite/join_worker_args.c index 0ed93ad4..da8cf23f 100644 --- a/src/controller/testsuite/join_worker_args.c +++ b/src/controller/testsuite/join_worker_args.c @@ -22,7 +22,7 @@ * xce_test for the identical real-world concepts (e.g. "host"/"jobid" in * test.c's own migrated commands); the ones here (cpufactor, maxcpus, * maxmem, maxswap, maxtmp, ndisks) are unique to `join` itself. */ -static const flagspec_t join_flags[] = { +const flagspec_t join_flags[] = { { 'f', "cpufactor", "cpufactor", required_argument, 0, "1.0" }, { 'c', "maxcpus", "maxcpus", required_argument, 0, "4" }, { 'm', "maxmem", "maxmem", required_argument, 0, @@ -31,7 +31,13 @@ static const flagspec_t join_flags[] = { { 't', "maxtmp", "maxtmp", required_argument, 0, "2048" }, { 'd', "ndisks", "ndisks", required_argument, 0, "1" }, }; -#define JOIN_NFLAGS (sizeof(join_flags) / sizeof(join_flags[0])) +/* JOIN_NFLAGS is an internal alias of the header's JOIN_FLAGS_COUNT (a + * compile-time literal, not sizeof()-derived) -- see join_worker_args.h's + * doc comment for why. The array declaration above and the header's + * `extern const flagspec_t join_flags[JOIN_FLAGS_COUNT];` must agree on + * size, so this stays a hard build failure, not a silent drift, if a + * future edit changes the entry count here without updating the header. */ +#define JOIN_NFLAGS JOIN_FLAGS_COUNT int join_worker_parse_attrs(int argc, char* argv[], hostattrs_t* attrs) { diff --git a/src/controller/testsuite/join_worker_args.h b/src/controller/testsuite/join_worker_args.h index 4891b04a..fd8d3187 100644 --- a/src/controller/testsuite/join_worker_args.h +++ b/src/controller/testsuite/join_worker_args.h @@ -17,8 +17,36 @@ * behavior change to the network-call phase, extraction only. */ +#include /* size_t */ + +#include "flagspec.h" /* flagspec_t */ #include "xce/types.h" /* hostattrs_t */ +/* join's flagspec_t table, exposed so test.c's `join` command_t entry + * (worker context, cmd_join_worker) can wire it into the Help Renderer's + * generated flag block (xce-test-cli-ux task 3.5, Requirement 8.2, 12.2) -- + * the same table join_worker_parse_attrs() itself parses against, so the + * rendered help and the actual parsing/defaulting behavior can never drift + * apart. + * + * JOIN_FLAGS_COUNT is a compile-time literal (not a `const size_t` + * variable) specifically so test.c's file-scope command_t table + * initializers -- which the C standard requires to be constant + * expressions -- can use it directly for a `.flagspec_count` field; an + * extern variable's value is only known at link time and is not a + * constant expression. Declaring `join_flags[]` here with this explicit + * bound, then defining it in join_worker_args.c with a plain `[]` and a + * 6-entry initializer, gets a free safety net: the two declarations of + * the same array must agree on size, so a future edit that adds/removes a + * flag without updating JOIN_FLAGS_COUNT fails the build immediately + * ("conflicting types") instead of silently reporting a wrong count. + * `JOIN_NFLAGS` (join_worker_args.c) stays an internal alias of this same + * constant, used to size join_worker_parse_attrs()'s own getopt_long() + * option table -- unchanged behavior, just no longer computed via + * sizeof(). */ +#define JOIN_FLAGS_COUNT 6 +extern const flagspec_t join_flags[JOIN_FLAGS_COUNT]; + /* Parse join's CLI flags ("-f cpufactor -c maxcpus -m maxmem -s maxswap * -t maxtmp -d ndisks") into `*attrs`, first defaulting every field * (including the bug-1557-corrected maxmem default of 64 * 1024 * 1024 diff --git a/src/controller/testsuite/test.c b/src/controller/testsuite/test.c index e247e268..7b2b111d 100644 --- a/src/controller/testsuite/test.c +++ b/src/controller/testsuite/test.c @@ -81,11 +81,46 @@ typedef int (*cmd_func_t)(int, char*[]); +/* command_t (xce-test-cli-ux task 3.5, Help Renderer, Requirements 8.1-8.7, + * 11.2, 12.2): `usage`/`flagspec`/`flagspec_count`/`usage_tail` together let + * cmd_help() assemble a command's detailed `help ` text in the order + * design.md mandates: description -> flags -> example -> related-command/ + * context notes. + * + * usage For a command with a flagspec table (flagspec != NULL): + * its HEAD text only -- the usage-syntax line plus a + * 1-2 sentence purpose description (and any positional- + * argument documentation flagspec_render_help() can't + * express). For a command with NO flagspec table + * (flagspec == NULL): the ENTIRE detailed usage text, same + * as before this task -- printed as-is, nothing generated. + * flagspec NULL if this command takes no flags; otherwise the + * command's flagspec_t table (Flag Parsing Layer, tasks + * 1.1/1.2), rendered via flagspec_render_help() and + * printed immediately after `usage`. + * flagspec_count Element count of `flagspec`; meaningless when + * flagspec == NULL. + * usage_tail Printed after the generated flag block: the example + * invocation(s) and any related-command/scripting-state + * notes (Requirement 8.3/8.4). NULL when flagspec == NULL + * -- `usage` already carries everything for those + * commands, matching their pre-task-3.5 shape exactly. + * + * C aggregate-initializer rules zero-initialize any trailing members a + * `{ ... }` initializer omits, so every non-flag command_t entry (help, + * quit, echo, source, up, worker, scheduler, client, set, waldump, + * snapverify, snapdiff, configcompare, join_sched, getconf, expect-jobs, + * expect-job, wait-primary, ctrlstatus) needs NO changes at all: their + * initializers stay exactly 4 fields, and flagspec/flagspec_count/ + * usage_tail all come out NULL/0 automatically. */ typedef struct { - char* name; - cmd_func_t func; - char* doc; - const char* usage; + char* name; + cmd_func_t func; + char* doc; + const char* usage; + const flagspec_t* flagspec; + size_t flagspec_count; + const char* usage_tail; } command_t; typedef struct { @@ -285,17 +320,18 @@ static const char USAGE_WALDUMP[] = " found.\n" "example: waldump /var/xce/ctrl-1/wal\n"; -static const char USAGE_WALCOMPARE[] = +static const char USAGE_WALCOMPARE_HEAD[] = "walcompare [--report] [--committed ] [more...]\n" - " --report also emit a full per-member (lsn,term,pcrc,status)\n" - " machine-checkable table, not just the verdict\n" - " --committed use LSN N as the shared committed boundary instead\n" - " of deriving it as the minimum CRC-intact tip across\n" - " members\n" " 2-16 member paths (seg-*.wal file, wal/ dir, or\n" " node/data dir)\n" - " Returns 0 if all members' committed WAL prefixes are identical,\n" - " nonzero on divergence or load error.\n" + " Diffs each member's committed WAL prefix (by LSN, term, and CRC) to\n" + " detect a durability divergence across replicas; --report\n" + " additionally emits a full per-member (lsn,term,pcrc,status)\n" + " machine-checkable table, not just the verdict. Returns 0 if all\n" + " members' committed WAL prefixes are identical, nonzero on\n" + " divergence or load error.\n"; + +static const char USAGE_WALCOMPARE_TAIL[] = "example: walcompare --report /var/xce/ctrl-1/wal /var/xce/ctrl-2/wal\n"; static const char USAGE_SNAPVERIFY[] = @@ -384,117 +420,136 @@ static const char USAGE_SET[] = " commands again (default)\n" "example: set masters ctrl1=localhost:9991,ctrl2=localhost:9992\n"; -static const char USAGE_JOIN_WORKER[] = +static const char USAGE_JOIN_WORKER_HEAD[] = "join [-f ] [-c ] [-m ] [-s ]\n" " [-t ] [-d ]\n" - " -f relative CPU speed factor (default 1.0)\n" - " -c max CPU count (default 4)\n" - " -m max memory in bytes (default 64)\n" - " -s max swap in bytes (default 1024)\n" - " -t max tmp space in bytes (default 2048)\n" - " -d number of local disks (default 1)\n" - " Joins the cluster as a worker (hosttype LINUX64, hostmodel Intel64).\n" - "example: join -c 8 -m 16384\n"; + " Joins the cluster as a worker (hosttype LINUX64, hostmodel Intel64).\n"; -static const char USAGE_JOBSTART[] = +static const char USAGE_JOIN_WORKER_TAIL[] = "example: join -c 8 -m 16384\n"; + +static const char USAGE_JOBSTART_HEAD[] = "jstart -j [-p ]\n" - " -j jobid to report a synthetic job-start fact for\n" - " (required)\n" - " -p pid to report (default: this xce_test process's own\n" - " pid)\n" - "example: jstart -j 12345 -p 4242\n"; + " Reports a synthetic job-start fact to the controller, simulating\n" + " what a real worker would report after actually launching the job\n" + " -- this does NOT execute anything; it is a fake-worker test\n" + " primitive only.\n"; -static const char USAGE_JOBFINISH[] = +static const char USAGE_JOBSTART_TAIL[] = "example: jstart -j 12345 -p 4242\n"; + +static const char USAGE_JOBFINISH_HEAD[] = "jfinish -j [-c ]\n" - " -j jobid to report a synthetic job-finish fact for\n" - " (required)\n" - " -c exit code to report (default 0)\n" - "example: jfinish -j 12345 -c 0\n"; + " Reports a synthetic job-finish fact to the controller, simulating\n" + " what a real worker would report after the job actually exited --\n" + " this does NOT execute anything; it is a fake-worker test primitive\n" + " only.\n"; -static const char USAGE_JOBFAIL[] = +static const char USAGE_JOBFINISH_TAIL[] = "example: jfinish -j 12345 -c 0\n"; + +static const char USAGE_JOBFAIL_HEAD[] = "jfail -j [-c ] [-s ]\n" - " -j jobid to report a synthetic job-fail fact for\n" - " (required)\n" - " -c failure code to report (default 0)\n" - " -s exec-stage code to report (default\n" - " JOB_EXEC_STAGE_POST_EXEC)\n" - "example: jfail -j 12345 -c 1\n"; + " Reports a synthetic job-fail fact to the controller, simulating\n" + " what a real worker would report after the job actually failed --\n" + " this does NOT execute anything; it is a fake-worker test primitive\n" + " only.\n"; -static const char USAGE_RESUPDATE[] = +static const char USAGE_JOBFAIL_TAIL[] = "example: jfail -j 12345 -c 1\n"; + +static const char USAGE_RESUPDATE_HEAD[] = "resupdate -n -v [-t ]\n" " [-n -v [-t ] ...]\n" - " -n resource name to update; any name the cluster has\n" - " registered is accepted (e.g. slots, mem, cpuf,\n" - " maxswap, maxtmp, maxcpus, ndisks, type, model,\n" - " dynres1, dynres2, r1m, r15s, r15m, pg, ls, it, swp,\n" - " io, tmp); may repeat for multiple resources\n" - " -v value for the immediately preceding -n (numeric for\n" - " most resources, string for type/model)\n" - " -t optional: force the wire type for the\n" - " immediately preceding -n/-v pair. Only meaningful for\n" - " a resource name NOT already known to this tool -- a\n" - " known name (e.g. mem, type) always keeps its own\n" - " fixed type, and -t is ignored for it. An unrecognized\n" - " name defaults to numeric when -t is omitted.\n" - " Order matters: pair each -n with its -v (and optional -t) right\n" - " after it.\n" + " Injects or overrides a resource's reported value on this session's\n" + " simulated worker, for testing scheduler/resource-accounting\n" + " behavior without a real xcecd. Any resource name the cluster has\n" + " registered is accepted (e.g. slots, mem, cpuf, maxswap, maxtmp,\n" + " maxcpus, ndisks, type, model, dynres1, dynres2, r1m, r15s, r15m,\n" + " pg, ls, it, swp, io, tmp); -n may repeat to update multiple\n" + " resources in one call. -t forces the wire type for the\n" + " immediately preceding -n/-v pair; it is only meaningful for a\n" + " resource name NOT already known to this tool -- a known name\n" + " (e.g. mem, type) always keeps its own fixed type, and -t is\n" + " ignored for it, while an unrecognized name defaults to numeric\n" + " when -t is omitted. Order matters: pair each -n with its -v (and\n" + " optional -t) right after it.\n"; + +static const char USAGE_RESUPDATE_TAIL[] = "example: resupdate -n slots -v 4 -n mem -v 8192\n" "example: resupdate -n r15m -v 1.5\n" "example: resupdate -n mylabel -v foo -t string\n"; -static const char USAGE_HELLO[] = +static const char USAGE_HELLO_HEAD[] = "hello [-c ] [-m ]\n" - " -c number of times to send the hello (default 1)\n" - " -m greeting text to send (default \"Greeting from xce\n" - " client\")\n" - "example: hello -c 3 -m \"ping\"\n"; + " Sends a greeting RPC to the controller, optionally repeated, as a\n" + " basic connectivity smoke test for the active context's connection.\n"; -static const char USAGE_RESLIST[] = +static const char USAGE_HELLO_TAIL[] = "example: hello -c 3 -m \"ping\"\n"; + +static const char USAGE_RESLIST_HEAD[] = "reslist [--json]\n" - " --json emit each resource as a JSON object (same fields as the\n" - " table) instead of the human-readable table\n" " Prints all cluster-wide resources (name, flags, interval,\n" - " description).\n"; + " description). Use --json to emit the same fields as one JSON\n" + " object per resource instead of the human-readable table.\n"; -static const char USAGE_WORKERLIST[] = +static const char USAGE_RESLIST_TAIL[] = "example: reslist --json\n"; + +static const char USAGE_WORKERLIST_HEAD[] = "workerlist [--json]\n" - " --json emit each worker as a JSON object (same fields as the\n" - " table) instead of the human-readable table\n" " Prints all joined workers (host, status, host type/model, cpu\n" - " factor, max cpu/mem/swap/tmp, ndisks).\n"; + " factor, max cpu/mem/swap/tmp, ndisks). Use --json to emit the same\n" + " fields as one JSON object per worker instead of the human-readable\n" + " table.\n"; -static const char USAGE_QCTRL[] = +static const char USAGE_WORKERLIST_TAIL[] = "example: workerlist --json\n"; + +static const char USAGE_QCTRL_HEAD[] = "qctrl -q [-a] [-i] [-o] [-c]\n" - " -q queue name to control (required)\n" - " -a set queue ACTIVE (accepts dispatch)\n" - " -i set queue INACTIVE (rejects dispatch)\n" - " -o set queue OPEN (accepts new submissions)\n" - " -c set queue CLOSE (rejects new submissions)\n" - "example: qctrl -q normal -i\n"; + " Controls a queue's dispatch/submission state: -a sets it ACTIVE\n" + " (accepts dispatch), -i sets it INACTIVE (rejects dispatch), -o\n" + " sets it OPEN (accepts new submissions), and -c sets it CLOSE\n" + " (rejects new submissions). Exactly one of -a/-i/-o/-c is required.\n"; -static const char USAGE_HOSTCTRL[] = +static const char USAGE_QCTRL_TAIL[] = + "example: qctrl -q normal -i\n" + " This is a destructive action only for -i/--deactivate and\n" + " -c/--close, which prompt for confirmation by default (-a/--activate\n" + " and -o/--open never prompt); use -y/--yes to skip the prompt for\n" + " this invocation, or `set -y` to skip it for the rest of the\n" + " session (see `help set`).\n"; + +static const char USAGE_HOSTCTRL_HEAD[] = "hostctrl -h (-o|-c)\n" - " -h host to administratively control (required)\n" - " -o open the host (allow scheduling)\n" - " -c close the host (admin-close; shows CLOSED in\n" - " workerlist)\n" - "example: hostctrl -h node01 -c\n"; + " Administratively controls a host's scheduling eligibility: -o\n" + " opens it (allow scheduling), -c closes it (admin-close; shows\n" + " CLOSED in workerlist). Exactly one of -o/-c is required.\n"; -static const char USAGE_UPGRADECONFIG[] = - "upgradeconfig \n" +static const char USAGE_HOSTCTRL_TAIL[] = + "example: hostctrl -h node01 -c\n" + " This is a destructive action only for -c/--close, which prompts\n" + " for confirmation by default (-o/--open never prompts); use\n" + " -y/--yes to skip the prompt for this invocation, or `set -y` to\n" + " skip it for the rest of the session (see `help set`).\n"; + +static const char USAGE_UPGRADECONFIG_HEAD[] = + "upgradeconfig [-y|--yes] \n" " path to a capnp-encoded ClusterConfig blob (same\n" " format `xceadmin config build` produces and\n" " deploy-ha.sh installs)\n" " Computes its SHA-256 and submits it as a configuration upgrade via\n" - " OP_upgradeConfig.\n" - "example: upgradeconfig /var/xce/cluster.bin\n"; + " OP_upgradeConfig.\n"; -static const char USAGE_JOBLIST[] = +static const char USAGE_UPGRADECONFIG_TAIL[] = + "example: upgradeconfig /var/xce/cluster.bin\n" + " This is a destructive action and prompts for confirmation by\n" + " default; use -y/--yes to skip the prompt for this invocation, or\n" + " `set -y` to skip it for the rest of the session (see `help set`).\n"; + +static const char USAGE_JOBLIST_HEAD[] = "joblist [--json]\n" - " --json emit each job as a JSON object (same fields as the table)\n" - " instead of the human-readable table\n" " Prints all jobs in the cluster (JobId, user, queue, state, times,\n" - " mintasks/maxtasks, command) and captures the count into $jobcount.\n"; + " mintasks/maxtasks, command) and captures the count into $jobcount.\n" + " Use --json to emit the same fields as one JSON object per job\n" + " instead of the human-readable table.\n"; + +static const char USAGE_JOBLIST_TAIL[] = "example: joblist --json\n"; static const char USAGE_EXPECT_JOBS[] = "expect-jobs \n" @@ -530,74 +585,80 @@ static const char USAGE_CTRLSTATUS[] = " committedLsn, appliedLsn, fenced) from a single any-node status\n" " read.\n"; -static const char USAGE_SHUTDOWN[] = - "shutdown\n" - " (no arguments)\n" +static const char USAGE_SHUTDOWN_HEAD[] = + "shutdown [-y|--yes]\n" " Shuts down the xce controller and, on success, leaves the client\n" " context (same as running `..`).\n"; -static const char USAGE_JOBRUN[] = +static const char USAGE_SHUTDOWN_TAIL[] = + "example: shutdown\n" + " This is a destructive action and prompts for confirmation by\n" + " default; use -y/--yes to skip the prompt for this invocation, or\n" + " `set -y` to skip it for the rest of the session (see `help set`).\n"; + +static const char USAGE_JOBRUN_HEAD[] = "jobrun -j [-m [:]] [-m ...]\n" - " -j jobid to dispatch (required, must be > 0)\n" - " -m [:] host to run tasks on, optional :ntasks count\n" - " (default 1); may be repeated up to 5 times\n" + " Dispatches an already-submitted job (jobid must be > 0) to one or\n" + " more hosts, allocating the given task counts on each.\n"; + +static const char USAGE_JOBRUN_TAIL[] = "example: jobrun -j 12345 -m node01:2 -m node02\n"; -static const char USAGE_JOBSUB[] = +static const char USAGE_JOBSUB_HEAD[] = "jobsub -u -q -c [-R ]\n" " [-n [,]] [-x ] [-a ]\n" - " -u submitting user (required)\n" - " -q target queue (required)\n" - " -c command line to run (required)\n" - " -R resource requirement expression (default\n" - " \"type == any\")\n" - " -n [,] task count, or a min,max range (default 1)\n" - " -x raw exdata JSON string (overrides the built-in\n" - " PATH env)\n" - " -a job array size (default 1)\n" + " Submits a new job to the cluster under the given user and queue;\n" + " -x's exdata JSON, when given, overrides the command's built-in\n" + " PATH env instead of the default.\n"; + +static const char USAGE_JOBSUB_TAIL[] = " On success, captures the submitted JobId into the $jobid\n" " auto-variable.\n" "example: jobsub -u alice -q normal -c \"sleep 10\" -n 1,4\n"; -static const char USAGE_RESVAL[] = +static const char USAGE_RESVAL_HEAD[] = "resval -n [-m ]\n" - " -n resource name to query (required)\n" - " -m host to query (required unless the resource is a\n" - " shared/cluster-wide resource, not per-host)\n" - "example: resval -n slots -m myhost\n"; + " Queries the current value of one resource, cluster-wide or on a\n" + " specific host.\n"; -static const char USAGE_JOBKILL[] = +static const char USAGE_RESVAL_TAIL[] = "example: resval -n slots -m myhost\n"; + +static const char USAGE_JOBKILL_HEAD[] = "jobkill -j [-s ]\n" - " -j jobid to signal (required)\n" - " -s signal number to send (default SIGTERM)\n" - "example: jobkill -j 12345 -s 9\n"; + " Sends a signal to a running job, terminating it by default.\n"; -static const char USAGE_JOBCKPT[] = +static const char USAGE_JOBKILL_TAIL[] = + "example: jobkill -j 12345 -s 9\n" + " This is a destructive action and prompts for confirmation by\n" + " default; use -y/--yes to skip the prompt for this invocation, or\n" + " `set -y` to skip it for the rest of the session (see `help set`).\n"; + +static const char USAGE_JOBCKPT_HEAD[] = "jobckpt -j [-k] [-m ]\n" - " -j jobid to checkpoint (required)\n" - " -k kill the job immediately after checkpointing\n" - " -m checkpoint method name (default \"default\")\n" + " Requests a checkpoint of a running job; -k additionally kills the\n" + " job immediately after checkpointing.\n"; + +static const char USAGE_JOBCKPT_TAIL[] = "example: jobckpt -j 12345 -m default\n"; -static const char USAGE_JOBINFO[] = +static const char USAGE_JOBINFO_HEAD[] = "jobinfo -j [-j ...]\n" - " -j jobid to show info for; may be repeated to show\n" - " several jobs in one call\n" - "example: jobinfo -j 12345\n"; + " Prints detailed information (state, pend reasons, submit/exec\n" + " metadata) for one or more jobs; -j may be repeated to show\n" + " several jobs in one call.\n"; -static const char USAGE_TASKRUN[] = +static const char USAGE_JOBINFO_TAIL[] = "example: jobinfo -j 12345\n"; + +static const char USAGE_TASKRUN_HEAD[] = "taskrun [-j ] [-m [:]]... [-u ] [-c ]\n" " [-x ] [-d ] [-t ]\n" - " -j jobid this task run belongs to\n" - " -m [:] host to run on, optional :ntasks count\n" - " (default 1); may be repeated up to 5 times\n" - " -u exec user, applied to every host\n" - " -c command line, applied to every host\n" - " -x exdata string, applied to every host\n" - " -d exec home/cwd directory, applied to every host\n" - " -t launch type (default \"mpi\" if the total task\n" - " count across all -m hosts is > 1, else a\n" - " single-task default)\n" + " Reports one or more task launches for an existing job to the\n" + " controller -- the same call a real worker makes after actually\n" + " starting a task -- letting a test simulate multi-host task\n" + " placement without a live worker; -u/-c/-x/-d apply uniformly to\n" + " every -m host.\n"; + +static const char USAGE_TASKRUN_TAIL[] = "example: taskrun -j 12345 -m node01:2 -c \"hostname\" -u alice\n"; static const char USAGE_JOIN_SCHED[] = @@ -611,13 +672,81 @@ static const char USAGE_GETCONF[] = " Prints the full cluster configuration (name, resources, queues,\n" " workers, user/host groups, configs) known to the scheduler.\n"; -static const char USAGE_SETPEND[] = +static const char USAGE_SETPEND_HEAD[] = "setpend -j -p [-p ...]\n" - " -j jobid to set the pend reason for (required)\n" - " -p pend reason spec, may repeat: `` sets the main\n" - " reason; `:` adds a host-based reason;\n" - " `::` adds a resource-based reason\n" - "example: setpend -j 12345 -p 5\n"; + " Sets a job's pend reason(s), simulating what the scheduler reports\n" + " for a job that cannot yet be dispatched. -p may repeat: ``\n" + " sets the main reason; `:` adds a host-based reason;\n" + " `::` adds a resource-based reason.\n"; + +static const char USAGE_SETPEND_TAIL[] = "example: setpend -j 12345 -p 5\n"; + +/* xce-test-cli-ux task 3.5 (Help Renderer): forward-declare every + * flag-accepting command's flagspec_t table -- each one is actually + * DEFINED later in this file, immediately next to its cmd_* handler, as + * before this task -- with its current flag count as an explicit array + * bound, and give that count a name via the same NFLAGS macro each + * handler already uses to size its own getopt_long() option table + * (`struct option long_opts[X_NFLAGS + 1]`). This lets top_commands[]/ + * worker_commands[]/client_commands[]/scheduler_commands[] below wire + * each entry's `.flagspec`/`.flagspec_count` directly to its table and + * macro, without requiring the table's full definition to appear first -- + * a file-scope initializer only needs a constant expression, which a + * literal NFLAGS macro is (an array's true element count from a later + * sizeof() is not known this early). + * + * Safety net, not a hand-counted magic number left free to drift: C + * requires two declarations of the same array to agree on size once + * either one specifies a constant size, so if a future edit changes a + * table's entry count below without updating its bound here, the build + * fails immediately ("conflicting types for ...") instead of silently + * rendering a wrong flag count in generated help text. Each table's own + * `#define X_NFLAGS (sizeof(x_flags) / sizeof(x_flags[0]))` -- used + * exactly this way for tasks 1.1/3.1 -- is superseded by (and removed at) + * that same later definition site; this is the one and only place each + * NFLAGS macro is now defined. */ +#define WALCOMPARE_NFLAGS 2 +static const flagspec_t walcompare_flags[WALCOMPARE_NFLAGS]; +#define HELLO_NFLAGS 2 +static const flagspec_t hello_flags[HELLO_NFLAGS]; +#define SHUTDOWN_NFLAGS 1 +static const flagspec_t shutdown_flags[SHUTDOWN_NFLAGS]; +#define RESLIST_NFLAGS 1 +static const flagspec_t reslist_flags[RESLIST_NFLAGS]; +#define WORKERLIST_NFLAGS 1 +static const flagspec_t workerlist_flags[WORKERLIST_NFLAGS]; +#define JOBLIST_NFLAGS 1 +static const flagspec_t joblist_flags[JOBLIST_NFLAGS]; +#define JOBINFO_NFLAGS 1 +static const flagspec_t jobinfo_flags[JOBINFO_NFLAGS]; +#define JOBRUN_NFLAGS 2 +static const flagspec_t jobrun_flags[JOBRUN_NFLAGS]; +#define JOBSUB_NFLAGS 7 +static const flagspec_t jobsub_flags[JOBSUB_NFLAGS]; +#define RESVAL_NFLAGS 2 +static const flagspec_t resval_flags[RESVAL_NFLAGS]; +#define JOBKILL_NFLAGS 3 +static const flagspec_t jobkill_flags[JOBKILL_NFLAGS]; +#define JOBCKPT_NFLAGS 3 +static const flagspec_t jobckpt_flags[JOBCKPT_NFLAGS]; +#define QCTRL_NFLAGS 6 +static const flagspec_t qctrl_flags[QCTRL_NFLAGS]; +#define HOSTCTRL_NFLAGS 4 +static const flagspec_t hostctrl_flags[HOSTCTRL_NFLAGS]; +#define UPGRADECONFIG_NFLAGS 1 +static const flagspec_t upgradeconfig_flags[UPGRADECONFIG_NFLAGS]; +#define RUNTASKS_NFLAGS 7 +static const flagspec_t runtasks_flags[RUNTASKS_NFLAGS]; +#define SETPEND_NFLAGS 2 +static const flagspec_t setpend_flags[SETPEND_NFLAGS]; +#define JOBSTART_NFLAGS 2 +static const flagspec_t jobstart_flags[JOBSTART_NFLAGS]; +#define JOBFINISH_NFLAGS 2 +static const flagspec_t jobfinish_flags[JOBFINISH_NFLAGS]; +#define JOBFAIL_NFLAGS 3 +static const flagspec_t jobfail_flags[JOBFAIL_NFLAGS]; +#define RESUPDATE_NFLAGS 3 +static const flagspec_t resupdate_flags[RESUPDATE_NFLAGS]; static command_t top_commands[] = { { "help", cmd_help, "Display this text", USAGE_HELP }, @@ -628,7 +757,8 @@ static command_t top_commands[] = { { "echo", cmd_echo, "Print a message", USAGE_ECHO }, { "waldump", cmd_waldump, "Inspect WAL segments", USAGE_WALDUMP }, { "walcompare", cmd_walcompare, "Diff member committed WAL prefixes", - USAGE_WALCOMPARE }, + USAGE_WALCOMPARE_HEAD, walcompare_flags, WALCOMPARE_NFLAGS, + USAGE_WALCOMPARE_TAIL }, { "snapverify", cmd_snapverify, "Verify checkpoint file structure", USAGE_SNAPVERIFY }, { "snapdiff", cmd_snapdiff, "Byte-diff two checkpoint files", @@ -649,11 +779,20 @@ static command_t worker_commands[] = { { "..", cmd_up, "Goto upper context", USAGE_UP }, { "help", cmd_help, "Display this text", USAGE_HELP }, { "quit", cmd_quit, "Quit xce test", USAGE_QUIT }, - { "join", cmd_join_worker, "Join the cluster", USAGE_JOIN_WORKER }, - { "jstart", cmd_jobstart, "tell controller jobstart", USAGE_JOBSTART }, - { "jfinish", cmd_jobfinish, "tell controller jobfinish", USAGE_JOBFINISH }, - { "jfail", cmd_jobfail, "tell controller jobfail", USAGE_JOBFAIL }, - { "resupdate", cmd_resupdate, "update res vals", USAGE_RESUPDATE }, + { "join", cmd_join_worker, "Join the cluster", + USAGE_JOIN_WORKER_HEAD, join_flags, JOIN_FLAGS_COUNT, + USAGE_JOIN_WORKER_TAIL }, + { "jstart", cmd_jobstart, "tell controller jobstart", + USAGE_JOBSTART_HEAD, jobstart_flags, JOBSTART_NFLAGS, + USAGE_JOBSTART_TAIL }, + { "jfinish", cmd_jobfinish, "tell controller jobfinish", + USAGE_JOBFINISH_HEAD, jobfinish_flags, JOBFINISH_NFLAGS, + USAGE_JOBFINISH_TAIL }, + { "jfail", cmd_jobfail, "tell controller jobfail", + USAGE_JOBFAIL_HEAD, jobfail_flags, JOBFAIL_NFLAGS, USAGE_JOBFAIL_TAIL }, + { "resupdate", cmd_resupdate, "update res vals", + USAGE_RESUPDATE_HEAD, resupdate_flags, RESUPDATE_NFLAGS, + USAGE_RESUPDATE_TAIL }, { "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 } @@ -666,14 +805,24 @@ static command_ctx_t workerctx = { .parent = &topctx, static command_t client_commands[] = { { "..", cmd_up, "Goto upper context", USAGE_UP }, - { "hello", cmd_hello, "Say hello to xce controller", USAGE_HELLO }, - { "reslist", cmd_reslist, "Display cluster resources", USAGE_RESLIST }, - { "workerlist", cmd_workerlist, "list workers", USAGE_WORKERLIST }, - { "qctrl", cmd_qctrl, "queue control", USAGE_QCTRL }, - { "hostctrl", cmd_hostctrl, "host control", USAGE_HOSTCTRL }, + { "hello", cmd_hello, "Say hello to xce controller", + USAGE_HELLO_HEAD, hello_flags, HELLO_NFLAGS, USAGE_HELLO_TAIL }, + { "reslist", cmd_reslist, "Display cluster resources", + USAGE_RESLIST_HEAD, reslist_flags, RESLIST_NFLAGS, USAGE_RESLIST_TAIL }, + { "workerlist", cmd_workerlist, "list workers", + USAGE_WORKERLIST_HEAD, workerlist_flags, WORKERLIST_NFLAGS, + USAGE_WORKERLIST_TAIL }, + { "qctrl", cmd_qctrl, "queue control", + USAGE_QCTRL_HEAD, qctrl_flags, QCTRL_NFLAGS, USAGE_QCTRL_TAIL }, + { "hostctrl", cmd_hostctrl, "host control", + USAGE_HOSTCTRL_HEAD, hostctrl_flags, HOSTCTRL_NFLAGS, + USAGE_HOSTCTRL_TAIL }, { "upgradeconfig", cmd_upgradeconfig, - "submit a config upgrade (cluster.bin file)", USAGE_UPGRADECONFIG }, - { "joblist", cmd_joblist, "list jobs", USAGE_JOBLIST }, + "submit a config upgrade (cluster.bin file)", + USAGE_UPGRADECONFIG_HEAD, upgradeconfig_flags, UPGRADECONFIG_NFLAGS, + USAGE_UPGRADECONFIG_TAIL }, + { "joblist", cmd_joblist, "list jobs", + USAGE_JOBLIST_HEAD, joblist_flags, JOBLIST_NFLAGS, USAGE_JOBLIST_TAIL }, { "expect-jobs", cmd_expect_jobs, "assert current job count", USAGE_EXPECT_JOBS }, { "expect-job", cmd_expect_job, "assert a JobId is present", USAGE_EXPECT_JOB }, { "wait-primary", cmd_wait_primary, @@ -681,21 +830,32 @@ static command_t client_commands[] = { { "ctrlstatus", cmd_ctrlstatus, "show controller/cluster status", USAGE_CTRLSTATUS }, { "waldump", cmd_waldump, "Inspect WAL segments", USAGE_WALDUMP }, { "walcompare", cmd_walcompare, "Diff member committed WAL prefixes", - USAGE_WALCOMPARE }, + USAGE_WALCOMPARE_HEAD, walcompare_flags, WALCOMPARE_NFLAGS, + USAGE_WALCOMPARE_TAIL }, { "snapverify", cmd_snapverify, "Verify checkpoint file structure", USAGE_SNAPVERIFY }, { "snapdiff", cmd_snapdiff, "Byte-diff two checkpoint files", USAGE_SNAPDIFF }, { "configcompare", cmd_configcompare, "Compare active config hash across members", USAGE_CONFIGCOMPARE }, - { "shutdown", cmd_shutdown, "shutdown xce controller", USAGE_SHUTDOWN }, - { "jobrun", cmd_jobrun, "run a job", USAGE_JOBRUN }, - { "jobsub", cmd_jobsub, "submit a job", USAGE_JOBSUB }, - { "jobkill", cmd_jobkill, "signal a job", USAGE_JOBKILL }, - { "jobckpt", cmd_jobckpt, "checkpoint a job", USAGE_JOBCKPT }, - { "jobinfo", cmd_jobinfo, "job information", USAGE_JOBINFO }, - { "resval", cmd_resval, "get value of res", USAGE_RESVAL }, - { "taskrun", cmd_runtasks, "run tasks", USAGE_TASKRUN }, + { "shutdown", cmd_shutdown, "shutdown xce controller", + USAGE_SHUTDOWN_HEAD, shutdown_flags, SHUTDOWN_NFLAGS, + USAGE_SHUTDOWN_TAIL }, + { "jobrun", cmd_jobrun, "run a job", + USAGE_JOBRUN_HEAD, jobrun_flags, JOBRUN_NFLAGS, USAGE_JOBRUN_TAIL }, + { "jobsub", cmd_jobsub, "submit a job", + USAGE_JOBSUB_HEAD, jobsub_flags, JOBSUB_NFLAGS, USAGE_JOBSUB_TAIL }, + { "jobkill", cmd_jobkill, "signal a job", + USAGE_JOBKILL_HEAD, jobkill_flags, JOBKILL_NFLAGS, USAGE_JOBKILL_TAIL }, + { "jobckpt", cmd_jobckpt, "checkpoint a job", + USAGE_JOBCKPT_HEAD, jobckpt_flags, JOBCKPT_NFLAGS, USAGE_JOBCKPT_TAIL }, + { "jobinfo", cmd_jobinfo, "job information", + USAGE_JOBINFO_HEAD, jobinfo_flags, JOBINFO_NFLAGS, USAGE_JOBINFO_TAIL }, + { "resval", cmd_resval, "get value of res", + USAGE_RESVAL_HEAD, resval_flags, RESVAL_NFLAGS, USAGE_RESVAL_TAIL }, + { "taskrun", cmd_runtasks, "run tasks", + USAGE_TASKRUN_HEAD, runtasks_flags, RUNTASKS_NFLAGS, + USAGE_TASKRUN_TAIL }, { "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 }, @@ -714,7 +874,8 @@ static command_t scheduler_commands[] = { { "quit", cmd_quit, "Quit xce test", USAGE_QUIT }, { "join", cmd_join_sched, "Join the cluster", USAGE_JOIN_SCHED }, { "getconf", cmd_getconf, "Get conf of cluster", USAGE_GETCONF }, - { "setpend", cmd_setpend, "set job pend reason", USAGE_SETPEND }, + { "setpend", cmd_setpend, "set job pend reason", + USAGE_SETPEND_HEAD, setpend_flags, SETPEND_NFLAGS, USAGE_SETPEND_TAIL }, { "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 } @@ -1312,6 +1473,9 @@ int cmd_help(int argc, char* argv[]) if (argc > 1) { char* cmd = argv[1]; + /* Requirement 8.7: scan ONLY active_ctx->commands -- a name valid in + * a different context must never be found here. Do not add a + * fallback lookup into another context's table. */ for (i = 0; active_ctx->commands[i].name != NULL; i++) { if (strcmp(argv[1], active_ctx->commands[i].name) == 0) { /* `help ` prints the detailed per-command usage (flags, @@ -1319,9 +1483,33 @@ int cmd_help(int argc, char* argv[]) * `doc` the bare `help` listing below still uses unchanged. * Every table entry is expected to carry a non-NULL `usage` * now; the `doc` fallback stays only as a defensive guard - * against a future entry that forgets to set it. */ + * against a future entry that forgets to set it. + * + * xce-test-cli-ux task 3.5 (Help Renderer, design.md's + * "description -> flags -> example -> related" assembly + * order): when this entry carries a flagspec table, `usage` + * holds only the HEAD (syntax + description) -- print the + * generated flag block right after it, then `usage_tail` + * (example + related-command/context notes). A command with + * no flagspec table is unchanged from before this task: + * `usage` alone already carries everything. */ if (active_ctx->commands[i].usage != NULL) { fputs(active_ctx->commands[i].usage, stdout); + + if (active_ctx->commands[i].flagspec != NULL) { + char* flags_block = flagspec_render_help( + active_ctx->commands[i].flagspec, + active_ctx->commands[i].flagspec_count); + + if (flags_block != NULL) { + fputs(flags_block, stdout); + free(flags_block); + } + + if (active_ctx->commands[i].usage_tail != NULL) { + fputs(active_ctx->commands[i].usage_tail, stdout); + } + } } else { printf("%s\t\t%s.\n", active_ctx->commands[i].name, active_ctx->commands[i].doc); @@ -1330,7 +1518,10 @@ int cmd_help(int argc, char* argv[]) } } - printf("No commands matchs `%s'. Possibilities are:\n", cmd); + /* bug-1711 / Requirement 11.2: grammar fix only ("match", not + * "matchs") -- does NOT add the cross-context lookup Requirement + * 8.7 explicitly excludes. */ + printf("No commands match `%s'. Possibilities are:\n", cmd); rc = -1; } @@ -1414,8 +1605,6 @@ static const flagspec_t walcompare_flags[] = { { 0, "committed", "committed", required_argument, 0, "derive the shared boundary from members' CRC-intact tips" }, }; -#define WALCOMPARE_NFLAGS \ - (sizeof(walcompare_flags) / sizeof(walcompare_flags[0])) int cmd_walcompare(int argc, char* argv[]) { @@ -1794,7 +1983,6 @@ static const flagspec_t hello_flags[] = { { 'm', "message", "message", required_argument, 0, "\"Greeting from xce client\"" }, }; -#define HELLO_NFLAGS (sizeof(hello_flags) / sizeof(hello_flags[0])) /* xce-test-cli-ux task 3.2: per-call-site heap block wrapping the blocking * xcecli_hello() call for xcetest_run_interruptible() (interruptcall.h, @@ -1922,7 +2110,6 @@ static int shutdown_thunk(void* ctxp) static const flagspec_t shutdown_flags[] = { { 'y', "yes", "yes", no_argument, 0, NULL }, }; -#define SHUTDOWN_NFLAGS (sizeof(shutdown_flags) / sizeof(shutdown_flags[0])) int cmd_shutdown(int argc, char* argv[]) { @@ -2022,7 +2209,6 @@ enum { RESLIST_OPT_JSON = 0 }; static const flagspec_t reslist_flags[] = { { 0, "json", "json-output", no_argument, 0, "human-readable table" }, }; -#define RESLIST_NFLAGS (sizeof(reslist_flags) / sizeof(reslist_flags[0])) int cmd_reslist(int argc, char* argv[]) { @@ -2166,8 +2352,6 @@ enum { WORKERLIST_OPT_JSON = 0 }; static const flagspec_t workerlist_flags[] = { { 0, "json", "json-output", no_argument, 0, "human-readable table" }, }; -#define WORKERLIST_NFLAGS \ - (sizeof(workerlist_flags) / sizeof(workerlist_flags[0])) int cmd_workerlist(int argc, char* argv[]) { @@ -2458,7 +2642,6 @@ enum { JOBLIST_OPT_JSON = 0 }; static const flagspec_t joblist_flags[] = { { 0, "json", "json-output", no_argument, 0, "human-readable table" }, }; -#define JOBLIST_NFLAGS (sizeof(joblist_flags) / sizeof(joblist_flags[0])) int cmd_joblist(int argc, char* argv[]) { @@ -3012,7 +3195,6 @@ static void print_jobinfo(xce_job_t* job) static const flagspec_t jobinfo_flags[] = { { 'j', "jobid", "jobid", required_argument, 1, NULL }, }; -#define JOBINFO_NFLAGS (sizeof(jobinfo_flags) / sizeof(jobinfo_flags[0])) typedef struct { xcecli_ctx_t conn; @@ -3150,7 +3332,6 @@ static const flagspec_t jobrun_flags[] = { { 'm', "host", "host", required_argument, 0, "1 task; repeatable up to 5" }, { 'j', "jobid", "jobid", required_argument, 1, NULL }, }; -#define JOBRUN_NFLAGS (sizeof(jobrun_flags) / sizeof(jobrun_flags[0])) typedef struct { xcecli_ctx_t conn; @@ -3315,7 +3496,6 @@ static const flagspec_t jobsub_flags[] = { { 'x', "exdata", "exdata", required_argument, 0, NULL }, { 'a', "array-size", "arraysize", required_argument, 0, "1" }, }; -#define JOBSUB_NFLAGS (sizeof(jobsub_flags) / sizeof(jobsub_flags[0])) typedef struct { xcecli_ctx_t conn; @@ -3668,7 +3848,6 @@ static const flagspec_t resval_flags[] = { { 'm', "host", "host", required_argument, 0, "required unless the resource is cluster-wide" }, }; -#define RESVAL_NFLAGS (sizeof(resval_flags) / sizeof(resval_flags[0])) typedef struct { xcecli_ctx_t conn; @@ -3816,7 +3995,6 @@ static const flagspec_t jobkill_flags[] = { * commands (Requirement 6.2 cross-command consistency). */ { 'y', "yes", "yes", no_argument, 0, NULL }, }; -#define JOBKILL_NFLAGS (sizeof(jobkill_flags) / sizeof(jobkill_flags[0])) typedef struct { xcecli_ctx_t conn; @@ -3949,7 +4127,6 @@ static const flagspec_t jobckpt_flags[] = { { 'm', "checkpoint-method", "checkpoint-method", required_argument, 0, "\"default\"" }, }; -#define JOBCKPT_NFLAGS (sizeof(jobckpt_flags) / sizeof(jobckpt_flags[0])) typedef struct { xcecli_ctx_t conn; @@ -4073,7 +4250,6 @@ static const flagspec_t qctrl_flags[] = { * the other four destructive commands (Requirement 6.2). */ { 'y', "yes", "yes", no_argument, 0, NULL }, }; -#define QCTRL_NFLAGS (sizeof(qctrl_flags) / sizeof(qctrl_flags[0])) typedef struct { xcecli_ctx_t conn; @@ -4234,7 +4410,6 @@ static const flagspec_t hostctrl_flags[] = { * spelling as the other four destructive commands (Requirement 6.2). */ { 'y', "yes", "yes", no_argument, 0, NULL }, }; -#define HOSTCTRL_NFLAGS (sizeof(hostctrl_flags) / sizeof(hostctrl_flags[0])) typedef struct { xcecli_ctx_t conn; @@ -4403,8 +4578,6 @@ static int upgradeconfig_thunk(void* ctxp) static const flagspec_t upgradeconfig_flags[] = { { 'y', "yes", "yes", no_argument, 0, NULL }, }; -#define UPGRADECONFIG_NFLAGS \ - (sizeof(upgradeconfig_flags) / sizeof(upgradeconfig_flags[0])) int cmd_upgradeconfig(int argc, char* argv[]) { @@ -4544,7 +4717,6 @@ static const flagspec_t runtasks_flags[] = { { 't', "launch-type", "launch-type", required_argument, 0, "\"mpi\" if >1 total tasks, else \"sequential\"" }, }; -#define RUNTASKS_NFLAGS (sizeof(runtasks_flags) / sizeof(runtasks_flags[0])) typedef struct { xcecli_ctx_t conn; @@ -4756,7 +4928,6 @@ static const flagspec_t setpend_flags[] = { { 'j', "jobid", "jobid", required_argument, 1, NULL }, { 'p', "reason", "reason", required_argument, 1, NULL }, }; -#define SETPEND_NFLAGS (sizeof(setpend_flags) / sizeof(setpend_flags[0])) typedef struct { xcecli_ctx_t conn; @@ -4955,7 +5126,6 @@ static const flagspec_t jobstart_flags[] = { { 'p', "pid", "pid", required_argument, 0, "this xce_test process's own pid" }, }; -#define JOBSTART_NFLAGS (sizeof(jobstart_flags) / sizeof(jobstart_flags[0])) typedef struct { xcecli_ctx_t conn; @@ -5060,7 +5230,6 @@ static const flagspec_t jobfinish_flags[] = { { 'j', "jobid", "jobid", required_argument, 1, NULL }, { 'c', "exit-code", "exit-code", required_argument, 0, "0" }, }; -#define JOBFINISH_NFLAGS (sizeof(jobfinish_flags) / sizeof(jobfinish_flags[0])) typedef struct { xcecli_ctx_t conn; @@ -5173,7 +5342,6 @@ static const flagspec_t jobfail_flags[] = { { 's', "stage", "stage", required_argument, 0, "JOB_EXEC_STAGE_POST_EXEC" }, }; -#define JOBFAIL_NFLAGS (sizeof(jobfail_flags) / sizeof(jobfail_flags[0])) typedef struct { xcecli_ctx_t conn; @@ -5497,7 +5665,6 @@ static const flagspec_t resupdate_flags[] = { { 't', "value-type", "value-type", required_argument, 0, "numeric unless overridden" }, }; -#define RESUPDATE_NFLAGS (sizeof(resupdate_flags) / sizeof(resupdate_flags[0])) /* Array sizes below are hardcoded to 16 to match cmd_resupdate's own * MAX_N_RES -- that macro is #define'd inside cmd_resupdate's own body diff --git a/src/controller/testsuite/test_test_cli.c b/src/controller/testsuite/test_test_cli.c index 2b31de4f..fa54b2e8 100644 --- a/src/controller/testsuite/test_test_cli.c +++ b/src/controller/testsuite/test_test_cli.c @@ -2480,6 +2480,362 @@ static void test_confirm_gate_session_toggle_skips_and_restores(void) clitest_buf_free(&err); } +/* + * test_help_jobkill_shows_generated_flag_and_confirmation_note -- + * xce-test-cli-ux task 3.5, Requirements 8.1, 8.2, 8.3, 8.4: `help jobkill` + * (client context) is the representative destructive command this task's + * own "Observable completion" line names -- its output must now show the + * GENERATED `-y, --yes` flag line (flagspec_render_help(), sourced from + * jobkill_flags, added by task 3.3 but never previously documented in + * jobkill's help text) AND a related-command/scripting note mentioning the + * confirmation gate and how to bypass it (task 3.5's own addition -- no + * prior version of USAGE_JOBKILL said anything about confirmation at all). + * Also spot-checks the retained description (8.1) and example (8.3) + * survive the HEAD/TAIL split. + */ +static void test_help_jobkill_shows_generated_flag_and_confirmation_note( + 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; + + 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_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"); + + TEST_ASSERT_EQUAL_INT(0, clitest_send_line(&sess, "client")); + TEST_ASSERT_TRUE_MESSAGE( + clitest_wait_for_text(&sess, &out, &err, "xce/client>", false, 5000), + "expected to enter the client context"); + + TEST_ASSERT_EQUAL_INT(0, clitest_send_line(&sess, "help jobkill")); + TEST_ASSERT_TRUE_MESSAGE( + clitest_wait_for_text(&sess, &out, &err, + "Sends a signal to a running job", false, + 5000), + "Requirement 8.1: help jobkill must include a purpose description"); + TEST_ASSERT_TRUE_MESSAGE( + clitest_wait_for_text(&sess, &out, &err, "-y, --yes", false, 5000), + "Requirement 8.2: help jobkill must show the generated -y/--yes " + "flag line (jobkill_flags, via flagspec_render_help)"); + TEST_ASSERT_TRUE_MESSAGE( + clitest_wait_for_text(&sess, &out, &err, "example: jobkill", false, + 5000), + "Requirement 8.3: help jobkill must retain its example invocation"); + TEST_ASSERT_TRUE_MESSAGE( + clitest_wait_for_text(&sess, &out, &err, + "destructive action and prompts for " + "confirmation", + false, 5000), + "Requirement 8.4: help jobkill must mention the confirmation gate " + "as a related scripting note"); + TEST_ASSERT_TRUE_MESSAGE( + clitest_wait_for_text(&sess, &out, &err, "set -y", false, 5000), + "the confirmation note must point at `set -y` as the session-wide " + "bypass (see `help set`)"); + + 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'"); + + clitest_kill_and_reap(&sess); + clitest_close(&sess); + clitest_buf_free(&out); + clitest_buf_free(&err); +} + +/* + * test_help_join_worker_shows_corrected_maxmem_default -- xce-test-cli-ux + * task 3.5, Requirement 12.2 (bug-1557): `help join` in the WORKER context + * (cmd_join_worker, join_flags) must show the corrected byte-canonical + * maxmem default (64 * 1024 * 1024, i.e. 64 MB in bytes) end-to-end in the + * actual rendered help text, and must NOT contain the old, stale + * "(default 64)" bytes-scale text USAGE_JOIN_WORKER hand-wrote before this + * task's split deleted the now-generated, now-driftable duplicate. + */ +static void test_help_join_worker_shows_corrected_maxmem_default(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; + + 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_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"); + + TEST_ASSERT_EQUAL_INT(0, clitest_send_line(&sess, "worker")); + TEST_ASSERT_TRUE_MESSAGE( + clitest_wait_for_text(&sess, &out, &err, "xce/worker>", false, 5000), + "expected to enter the worker context"); + + TEST_ASSERT_EQUAL_INT(0, clitest_send_line(&sess, "help join")); + TEST_ASSERT_TRUE_MESSAGE( + clitest_wait_for_text( + &sess, &out, &err, + "-m, --maxmem optional (default: 64 * 1024 * 1024 (64 MB in " + "bytes))", + false, 5000), + "bug-1557/Requirement 12.2: help join must show the byte-correct " + "maxmem default, generated from join_flags"); + + TEST_ASSERT_NULL_MESSAGE( + strstr(out.data, "default 64)"), + "help join must NOT contain the stale pre-bug-1557 " + "'(default 64)' bytes text -- USAGE_JOIN_WORKER's hand-written " + "duplicate must have been deleted, not merely superseded"); + + TEST_ASSERT_EQUAL_INT(0, clitest_send_line(&sess, "..")); + TEST_ASSERT_TRUE_MESSAGE( + clitest_wait_for_text(&sess, &out, &err, "xce>", false, 5000), + "expected to return to the top-level context after '..'"); + 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'"); + + clitest_kill_and_reap(&sess); + clitest_close(&sess); + clitest_buf_free(&out); + clitest_buf_free(&err); +} + +/* + * test_help_reslist_shows_json_example -- xce-test-cli-ux task 3.5, + * Requirements 5.4, 8.3: `help reslist` (one of the three listing + * commands whose --json mode was added fresh in task 3.4 with no example + * at all) must now show a concrete `--json` example invocation, and its + * description must state plainly that --json requests the machine- + * readable/JSON output mode. + */ +static void test_help_reslist_shows_json_example(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; + + 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_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"); + + TEST_ASSERT_EQUAL_INT(0, clitest_send_line(&sess, "client")); + TEST_ASSERT_TRUE_MESSAGE( + clitest_wait_for_text(&sess, &out, &err, "xce/client>", false, 5000), + "expected to enter the client context"); + + TEST_ASSERT_EQUAL_INT(0, clitest_send_line(&sess, "help reslist")); + TEST_ASSERT_TRUE_MESSAGE( + clitest_wait_for_text(&sess, &out, &err, "example: reslist --json", + false, 5000), + "Requirement 8.3: help reslist must show a --json example " + "invocation (it had none before this task)"); + TEST_ASSERT_TRUE_MESSAGE( + clitest_wait_for_text(&sess, &out, &err, + "Use --json to emit the same fields as one " + "JSON", + false, 5000), + "Requirement 5.4: help reslist must clearly document that --json " + "requests the machine-readable JSON output mode, not just list " + "the bare flag name"); + + 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'"); + + clitest_kill_and_reap(&sess); + clitest_close(&sess); + clitest_buf_free(&out); + clitest_buf_free(&err); +} + +/* + * test_help_unknown_command_shows_corrected_grammar -- xce-test-cli-ux + * task 3.5, Requirement 11.2 (bug-1711): `help ` must + * report "No commands match `%s'" (correct grammar), never the original + * "No commands matchs `%s'" typo. + */ +static void test_help_unknown_command_shows_corrected_grammar(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; + + 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_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"); + + TEST_ASSERT_EQUAL_INT( + 0, clitest_send_line(&sess, "help bogus_nonexistent_command")); + TEST_ASSERT_TRUE_MESSAGE( + clitest_wait_for_text( + &sess, &out, &err, + "No commands match `bogus_nonexistent_command'", false, 5000), + "bug-1711/Requirement 11.2: the not-found message must use " + "correct grammar ('match', not 'matchs')"); + TEST_ASSERT_NULL_MESSAGE( + strstr(out.data, "matchs"), + "the corrected message must not contain the old 'matchs' typo " + "anywhere in the captured output"); + + 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'"); + + clitest_kill_and_reap(&sess); + clitest_close(&sess); + clitest_buf_free(&out); + clitest_buf_free(&err); +} + +/* + * test_help_context_scoped_not_found_no_cross_context_hint -- + * xce-test-cli-ux task 3.5, Requirement 8.7: `help ` must report a + * name not found in the ACTIVE context as not found there, without ever + * checking another context's table -- proven here with `jobkill`, which + * exists only in client_commands[]. From the top-level context (where + * jobkill is not a command at all), `help jobkill` must be reported as + * not found -- never falling back to print client context's jobkill help + * text. The companion positive case (same command, correct context) is + * covered by test_help_jobkill_shows_generated_flag_and_confirmation_note + * above; this test is the negative/cross-context counterpart the task's + * own "Observable completion" line calls out explicitly. + */ +static void +test_help_context_scoped_not_found_no_cross_context_hint(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; + + 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_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"); + + /* Top-level context: jobkill is NOT a top-level command (only + * client_commands[] has it) -- must be reported not-found, never the + * client context's jobkill help text (which mentions "signal"). */ + TEST_ASSERT_EQUAL_INT(0, clitest_send_line(&sess, "help jobkill")); + TEST_ASSERT_TRUE_MESSAGE( + clitest_wait_for_text(&sess, &out, &err, + "No commands match `jobkill'", false, 5000), + "Requirement 8.7: from the top-level context, 'help jobkill' must " + "be reported as not found there, without checking the client " + "context's table"); + TEST_ASSERT_NULL_MESSAGE( + strstr(out.data, "Sends a signal to a running job"), + "the top-level not-found response must never leak client " + "context's jobkill help content"); + + /* Now enter the client context, where jobkill DOES exist -- the same + * lookup must now succeed and print jobkill's own detailed help. */ + TEST_ASSERT_EQUAL_INT(0, clitest_send_line(&sess, "client")); + TEST_ASSERT_TRUE_MESSAGE( + clitest_wait_for_text(&sess, &out, &err, "xce/client>", false, 5000), + "expected to enter the client context"); + + clitest_buf_free(&out); + clitest_buf_init(&out); + TEST_ASSERT_EQUAL_INT(0, clitest_send_line(&sess, "help jobkill")); + TEST_ASSERT_TRUE_MESSAGE( + clitest_wait_for_text(&sess, &out, &err, + "Sends a signal to a running job", false, + 5000), + "in the client context, 'help jobkill' must show jobkill's own " + "detailed help, not a not-found message"); + TEST_ASSERT_NULL_MESSAGE( + strstr(out.data, "No commands match"), + "a valid in-context lookup must not also print a not-found " + "message"); + + 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'"); + + clitest_kill_and_reap(&sess); + clitest_close(&sess); + clitest_buf_free(&out); + clitest_buf_free(&err); +} + void test_test_cli(void) { UnitySetTestFile(__FILE__); @@ -2499,4 +2855,9 @@ void test_test_cli(void) RUN_TEST(test_qctrl_hostctrl_confirm_gate_destructiveness_determination); RUN_TEST(test_destructive_commands_prompt_by_default_and_yes_bypasses); RUN_TEST(test_confirm_gate_session_toggle_skips_and_restores); + RUN_TEST(test_help_jobkill_shows_generated_flag_and_confirmation_note); + RUN_TEST(test_help_join_worker_shows_corrected_maxmem_default); + RUN_TEST(test_help_reslist_shows_json_example); + RUN_TEST(test_help_unknown_command_shows_corrected_grammar); + RUN_TEST(test_help_context_scoped_not_found_no_cross_context_hint); }