feat(xce-test-cli-ux): generate and consolidate every command's help content

Wire flagspec_render_help() into cmd_help via new command_t fields
(flagspec/flagspec_count/usage_tail), so every flag-accepting command's
help composes description -> generated flag block -> example/related
notes, with hand-written per-flag lines removed as a duplicate source of
truth. Add missing purpose descriptions, examples, and confirmation-gate
related notes across ~20 commands; fix bug-1711's "matchs" grammar
(Req 11.2); confirm join's corrected maxmem default (bug-1557, Req 12.2)
and the --json flag (Req 5.4) now render correctly end-to-end.

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 23:55:56 +00:00
parent 0d67f5cbde
commit 96746af3eb
No known key found for this signature in database
GPG key ID: 5BCDAAE0123E9FE2
5 changed files with 755 additions and 192 deletions

View file

@ -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_<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)

View file

@ -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)
{

View file

@ -17,8 +17,36 @@
* behavior change to the network-call phase, extraction only.
*/
#include <stddef.h> /* 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

View file

@ -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 <cmd>` 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 <N>] <memberA> <memberB> [more...]\n"
" --report also emit a full per-member (lsn,term,pcrc,status)\n"
" machine-checkable table, not just the verdict\n"
" --committed <N> use LSN N as the shared committed boundary instead\n"
" of deriving it as the minimum CRC-intact tip across\n"
" members\n"
" <memberX> 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 <cpufactor>] [-c <maxcpus>] [-m <maxmem>] [-s <maxswap>]\n"
" [-t <maxtmp>] [-d <ndisks>]\n"
" -f <cpufactor> relative CPU speed factor (default 1.0)\n"
" -c <maxcpus> max CPU count (default 4)\n"
" -m <maxmem> max memory in bytes (default 64)\n"
" -s <maxswap> max swap in bytes (default 1024)\n"
" -t <maxtmp> max tmp space in bytes (default 2048)\n"
" -d <ndisks> 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 <jobid> [-p <pid>]\n"
" -j <jobid> jobid to report a synthetic job-start fact for\n"
" (required)\n"
" -p <pid> 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 <jobid> [-c <exitcode>]\n"
" -j <jobid> jobid to report a synthetic job-finish fact for\n"
" (required)\n"
" -c <exitcode> 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 <jobid> [-c <code>] [-s <stage>]\n"
" -j <jobid> jobid to report a synthetic job-fail fact for\n"
" (required)\n"
" -c <code> failure code to report (default 0)\n"
" -s <stage> 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 <resname> -v <value> [-t <numeric|string>]\n"
" [-n <resname> -v <value> [-t <numeric|string>] ...]\n"
" -n <resname> 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> value for the immediately preceding -n (numeric for\n"
" most resources, string for type/model)\n"
" -t <numeric|string> 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 <count>] [-m <message>]\n"
" -c <count> number of times to send the hello (default 1)\n"
" -m <message> 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 <queue> [-a] [-i] [-o] [-c]\n"
" -q <queue> 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 <hostname> (-o|-c)\n"
" -h <hostname> 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 <cluster.bin>\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] <cluster.bin>\n"
" <cluster.bin> 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>\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 <jobid> [-m <host>[:<ntasks>]] [-m ...]\n"
" -j <jobid> jobid to dispatch (required, must be > 0)\n"
" -m <host>[:<n>] 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 <user> -q <queue> -c <cmdline> [-R <resreq>]\n"
" [-n <min>[,<max>]] [-x <exdata>] [-a <arraysize>]\n"
" -u <user> submitting user (required)\n"
" -q <queue> target queue (required)\n"
" -c <cmdline> command line to run (required)\n"
" -R <resreq> resource requirement expression (default\n"
" \"type == any\")\n"
" -n <min>[,<max>] task count, or a min,max range (default 1)\n"
" -x <exdata> raw exdata JSON string (overrides the built-in\n"
" PATH env)\n"
" -a <arraysize> 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 <resname> [-m <host>]\n"
" -n <resname> resource name to query (required)\n"
" -m <host> 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 <jobid> [-s <signo>]\n"
" -j <jobid> jobid to signal (required)\n"
" -s <signo> 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 <jobid> [-k] [-m <method>]\n"
" -j <jobid> jobid to checkpoint (required)\n"
" -k kill the job immediately after checkpointing\n"
" -m <method> 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 <jobid> [-j <jobid> ...]\n"
" -j <jobid> 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 <jobid>] [-m <host>[:<n>]]... [-u <user>] [-c <cmdline>]\n"
" [-x <exdata>] [-d <dir>] [-t <launch_type>]\n"
" -j <jobid> jobid this task run belongs to\n"
" -m <host>[:<n>] host to run on, optional :ntasks count\n"
" (default 1); may be repeated up to 5 times\n"
" -u <user> exec user, applied to every host\n"
" -c <cmdline> command line, applied to every host\n"
" -x <exdata> exdata string, applied to every host\n"
" -d <dir> exec home/cwd directory, applied to every host\n"
" -t <launch_type> 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 <jobid> -p <reason> [-p <reason> ...]\n"
" -j <jobid> jobid to set the pend reason for (required)\n"
" -p <reason> pend reason spec, may repeat: `<code>` sets the main\n"
" reason; `<host>:<code>` adds a host-based reason;\n"
" `<res>:<host>:<code>` 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: `<code>`\n"
" sets the main reason; `<host>:<code>` adds a host-based reason;\n"
" `<res>:<host>:<code>` 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 <cmd>` 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

View file

@ -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 <name-not-a-command>` 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 <cmd>` 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);
}