feat(xce-test-cli-ux): add JSON output mode to the listing commands

Bring workerlist/joblist/reslist onto the shared flagspec_t library with an
identically-spelled --json flag, wired to the task-2.2 JSON list formatter
via a new pure field-mapping module (listrow_json.c/.h). Default table
output is unchanged; JSON rows carry raw numeric values (flags, timestamps,
memory sizes) rather than their humanized display strings, per Requirement
5.1's "same fields" contract.

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 22:42:37 +00:00
parent a57c04716b
commit 0d67f5cbde
No known key found for this signature in database
GPG key ID: 5BCDAAE0123E9FE2
7 changed files with 996 additions and 16 deletions

View file

@ -122,7 +122,7 @@
- _Boundary: Confirmation Gate_
- _Depends: 2.1, 3.1_
- [ ] 3.4 Add JSON output mode to the three listing commands
- [x] 3.4 Add JSON output mode to the three listing commands
- Bring the worker-listing, job-listing, and resource-listing commands onto the shared flag-descriptor library from 1.1 for the first time (none currently accepts any flag), adding a consistently named output-format flag that uses the JSON list-output library from 2.2 to emit every field present in each command's human-readable table, unabbreviated
- Leave each command's existing human-readable output unchanged when the new flag is not given
- Observable completion: running each of the three listing commands with the new flag against a fixture with a long field value produces valid JSON containing every table field with no truncation, while the default (no flag) invocation still prints the existing table
@ -176,6 +176,7 @@
- _Depends: 1.6, 2.3, 3.5, 1.7_
## Implementation Notes
- `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)
- Confirmed (review traced it concretely): a declined destructive command inside a `source`d file does NOT trip `set -e`'s abort-on-failure, since decline maps to `return 0` per confirm.h's own documented contract, not `execute_line`'s `rc<0` failure check. This only matters when a human is actively present declining (never a blind/unattended failure mode), and any later destructive line re-prompts independently — but task 4.3 should add an explicit `set -e` + `source <file with a declined destructive command>` case to prove/document this interaction, since it's currently untested. (task 3.3)

View file

@ -55,7 +55,7 @@ controller = executable('xce_controller',
link_args: wrap_link_args,
install: true)
xce_test_src = [ 'testsuite/test.c', 'testsuite/waldump.c', 'testsuite/walcompare.c', 'testsuite/snapverify.c', 'testsuite/snapdiff.c', 'testsuite/configcompare.c', 'testsuite/resval_typeresolve.c', 'testsuite/joblist_display.c', 'testsuite/join_worker_args.c', 'testsuite/rcfile.c', 'testsuite/flagspec.c', 'testsuite/interruptcall.c', 'testsuite/confirm.c', 'testsuite/listfmt_json.c']
xce_test_src = [ 'testsuite/test.c', 'testsuite/waldump.c', 'testsuite/walcompare.c', 'testsuite/snapverify.c', 'testsuite/snapdiff.c', 'testsuite/configcompare.c', 'testsuite/resval_typeresolve.c', 'testsuite/joblist_display.c', 'testsuite/join_worker_args.c', 'testsuite/rcfile.c', 'testsuite/flagspec.c', 'testsuite/interruptcall.c', 'testsuite/confirm.c', 'testsuite/listfmt_json.c', 'testsuite/listrow_json.c']
# BSD-licensed readline replacement (scheduler-core spec, task 12.1 review
# finding): GNU readline is GPL; xce_test's REPL only needs readline()/
@ -451,3 +451,31 @@ test('join worker args test', join_worker_args_unit_test,
timeout: 60,
env: ['ASAN_OPTIONS=detect_leaks=0'])
# xce-test-cli-ux task 3.4: listrow_json.c is a small, dependency-light
# module (no networking/REPL/global state) extracted from cmd_reslist/
# cmd_workerlist/joblist_query specifically so their new --json output
# mode's per-command field-mapping logic can be unit-tested in isolation
# -- test.c itself cannot be white-box included into a Unity test, same
# rationale as joblist_display_unit_test/resval_typeresolve_unit_test
# above. Standalone binary (not folded into controller_unit_test_src)
# mirroring those tests' own standalone-binary rationale. Links
# listfmt_json.c directly (not struct2json_dep alone) since listrow_json.c
# calls its add_string()/add_number()/new_row() helpers.
listrow_json_unit_test_src = [
'testsuite/test_listrow_json.c',
'testsuite/listrow_json.c',
'testsuite/listfmt_json.c',
]
listrow_json_unit_test = executable('listrow_json_unit_test',
listrow_json_unit_test_src,
include_directories: [incdir],
dependencies: controller_unit_test_deps,
c_args: ['-DXCE_TESTING'],
install: false)
test('listrow json test', listrow_json_unit_test,
suite: 'controller',
timeout: 60,
env: ['ASAN_OPTIONS=detect_leaks=0'])

View file

@ -0,0 +1,62 @@
/* listrow_json.c -- xce-test-cli-ux task 3.4 (Requirements 5.1, 5.2, 5.3)
*
* See listrow_json.h for the full rationale. Pure field-mapping only: each
* function below just calls listfmt_json_add_string()/add_number() (task
* 2.2, unmodified) once per table column.
*/
#include "listrow_json.h"
#include "listfmt_json.h"
void reslist_json_row(cJSON* row, const char* name, int flags, int interval,
const char* desc)
{
if (!row)
return;
listfmt_json_add_string(row, "name", name);
listfmt_json_add_number(row, "flags", flags);
listfmt_json_add_number(row, "interval", interval);
listfmt_json_add_string(row, "description", desc);
}
void workerlist_json_row(cJSON* row, const char* name, const char* status_str,
const char* hosttype, const char* hostmodel,
double cpufactor, int maxcpus, double maxmem,
double maxswap, double maxtmp, int ndisks)
{
if (!row)
return;
listfmt_json_add_string(row, "hostname", name);
listfmt_json_add_string(row, "status", status_str);
listfmt_json_add_string(row, "hosttype", hosttype);
listfmt_json_add_string(row, "hostmodel", hostmodel);
listfmt_json_add_number(row, "cpufactor", cpufactor);
listfmt_json_add_number(row, "maxcpu", maxcpus);
listfmt_json_add_number(row, "maxmem", maxmem);
listfmt_json_add_number(row, "maxswap", maxswap);
listfmt_json_add_number(row, "maxtmp", maxtmp);
listfmt_json_add_number(row, "ndisks", ndisks);
}
void joblist_json_row(cJSON* row, const char* jobid_str, const char* user,
const char* queue, const char* state_str,
time_t submit_time, time_t start_time,
time_t finish_time, uint32_t mintasks,
uint32_t maxtasks, const char* cmdline)
{
if (!row)
return;
listfmt_json_add_string(row, "jobid", jobid_str);
listfmt_json_add_string(row, "user", user);
listfmt_json_add_string(row, "queue", queue);
listfmt_json_add_string(row, "state", state_str);
listfmt_json_add_number(row, "submit_time", (double)submit_time);
listfmt_json_add_number(row, "start_time", (double)start_time);
listfmt_json_add_number(row, "finish_time", (double)finish_time);
listfmt_json_add_number(row, "mintasks", mintasks);
listfmt_json_add_number(row, "maxtasks", maxtasks);
listfmt_json_add_string(row, "command", cmdline);
}

View file

@ -0,0 +1,113 @@
#pragma once
/*
* listrow_json.h -- pure per-command JSON-row field-mapping helpers for
* cmd_reslist/cmd_workerlist/joblist_query's new --json output mode
* (xce-test-cli-ux task 3.4, Requirements 5.1, 5.2, 5.3).
*
* Each function here takes one already-fetched record's fields --
* already extracted as plain scalars by the caller, no xce/types.h struct
* dependency, no RPC, no networking, no global session state -- and
* populates one JSON row object (from listfmt_json_new_row(), task 2.2)
* via listfmt_json.h's add_string()/add_number() primitives, mapping
* EVERY column the corresponding command's human-readable table prints
* (Requirement 5.1) with no truncation regardless of value length
* (Requirement 5.3).
*
* Extracted out of test.c (rather than inlined in cmd_reslist/
* cmd_workerlist/joblist_query alongside their RPC calls) specifically so
* this field-mapping logic is unit-testable in isolation with plain
* literal inputs -- test.c itself cannot be white-box #include'd into a
* Unity test (thousands of lines, its own main(), heavy libedit/network
* dependencies), and cmd_reslist/joblist_query's fetch+print bodies were
* already found "genuinely untestable without a live cluster" for the
* SAME reason during task 2.5's review. This mirrors joblist_display.c's
* extraction precedent (task 2.5) for worker_status_str()/job_state_str()/
* format_job_time() -- pure, dependency-light display formatting pulled
* out specifically to get real automated coverage.
*
* The caller (test.c) remains responsible for: calling xcecli_reslist()/
* xcecli_workerlist()/xcecli_joblist(), iterating the result, calling
* listfmt_json_begin()/listfmt_json_new_row() around each call here, and
* listfmt_json_finish() exactly once when done -- this module never
* touches the array lifecycle itself (listfmt_json.h already owns that,
* task 2.2, and is not modified by this task).
*/
#include <stdint.h>
#include <time.h>
#include <cjson/cJSON.h>
/* Maps cmd_reslist's table columns -- Name, Flags, Interval, Description,
* see test.c's printf("%-10s%10s%10d %-64s\n", ...) row -- onto `row`.
*
* `flags` is emitted as a JSON NUMBER under the key "flags" (the raw
* xce_resource_t.flags value), not the table's "0x%x"-formatted hex
* string (Requirement 11.4's hex prefix is a display-only convention for
* the human-readable table; the underlying integer value is identical
* either way) -- a number already carries the same information the
* table's Flags column shows, so nothing is lost relative to the table
* (Requirement 5.1), and a script consuming the JSON output gets a
* directly-usable integer instead of having to re-parse a "0x..." string.
*
* No-op if `row` is NULL (mirrors listfmt_json_add_string/number's own
* NULL-row no-op contract). */
void reslist_json_row(cJSON* row, const char* name, int flags, int interval,
const char* desc);
/* Maps cmd_workerlist's table columns -- HostName, Status, HostType,
* HostModel, CPUfactor, MaxCPU, MaxMem, MaxSwap, MaxTmp, NDisks -- onto
* `row`.
*
* Unlike the table (which humanizes MaxMem/MaxSwap/MaxTmp via
* format_bytes_human() for display, e.g. "64.0 MB"), this emits their RAW
* numeric byte values (`maxmem`/`maxswap`/`maxtmp`, matching hostattrs_t's
* own `double` field types in xce/types.h) as JSON numbers under keys
* "maxmem"/"maxswap"/"maxtmp" -- the same underlying value the table's
* humanized string represents, but directly script-consumable without
* re-parsing a unit suffix.
*
* `status_str` is the caller's worker_status_str(workers[i]->status)
* result (joblist_display.h) -- passed in rather than recomputed here, so
* this module has no dependency on xce/types.h's XCE_WORKER_* bit
* definitions.
*
* `cpufactor` is `double` (not truncated to float/int) so a genuine
* fractional value -- not just a power-of-two -- round-trips exactly
* through the JSON text (see test_listrow_json.c's fractional round-trip
* test, closing the gap task 2.2's Implementation Notes flagged: its own
* tests only exercised power-of-two doubles).
*
* No-op if `row` is NULL. */
void workerlist_json_row(cJSON* row, const char* name, const char* status_str,
const char* hosttype, const char* hostmodel,
double cpufactor, int maxcpus, double maxmem,
double maxswap, double maxtmp, int ndisks);
/* Maps joblist_query's table columns -- JobId, User, Queue, State,
* SubmitTime, StartTime, FinishTime, MinTasks, MaxTasks, Command -- onto
* `row`.
*
* `jobid_str` is the caller's fmt_jobid(...) result and `state_str` the
* caller's job_state_str(jobs[i]->state) result (joblist_display.h) --
* both passed in rather than recomputed here, keeping this module free of
* test.c's own fmt_jobid()/RPC dependencies. `state_str` is stored
* verbatim (including job_state_str()'s Requirement-11.7 raw-state
* fallback text for an ambiguous RUN+EXIT state, e.g. "RUN(0x38)"), so no
* information the table's State column carries is lost.
*
* Unlike the table (which formats SubmitTime/StartTime/FinishTime as
* human-readable text via format_job_time(), Requirement 11.5), this
* emits their RAW epoch-second values as JSON numbers under keys
* "submit_time"/"start_time"/"finish_time" -- 0 meaning "has not happened
* yet", the same convention format_job_time() itself uses for its "-"
* display -- the same underlying value the table's formatted string
* represents, directly script-consumable without re-parsing a date
* string.
*
* No-op if `row` is NULL. */
void joblist_json_row(cJSON* row, const char* jobid_str, const char* user,
const char* queue, const char* state_str,
time_t submit_time, time_t start_time,
time_t finish_time, uint32_t mintasks,
uint32_t maxtasks, const char* cmdline);

View file

@ -67,6 +67,12 @@
* (xce-test-cli-ux task 2.5 remediation) */
#include "join_worker_args.h" /* join_worker_parse_attrs (xce-test-cli-ux
* task 2.6 remediation, bug-1557) */
#include "listfmt_json.h" /* listfmt_json_begin/new_row/finish -- JSON list
* output for workerlist/joblist/reslist
* (xce-test-cli-ux task 2.2, wired in task 3.4) */
#include "listrow_json.h" /* reslist_json_row/workerlist_json_row/
* joblist_json_row -- per-command JSON field
* mapping (xce-test-cli-ux task 3.4) */
#include "rcfile.h" /* startup rc-file path resolution + loading (xce-test-rc-file task 2.2) */
#include "resval_typeresolve.h" /* resval_resolve_type (resource-value-verification task 2.1) */
#include "xce/cluster.h" /* xce_parse_peers, xce_cluster_config_t */
@ -444,14 +450,16 @@ static const char USAGE_HELLO[] =
"example: hello -c 3 -m \"ping\"\n";
static const char USAGE_RESLIST[] =
"reslist\n"
" (no arguments)\n"
"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";
static const char USAGE_WORKERLIST[] =
"workerlist\n"
" (no arguments)\n"
"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";
@ -482,10 +490,11 @@ static const char USAGE_UPGRADECONFIG[] =
"example: upgradeconfig /var/xce/cluster.bin\n";
static const char USAGE_JOBLIST[] =
"joblist\n"
" (no arguments)\n"
"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"
" ntasks, command) and captures the count into $jobcount.\n";
" mintasks/maxtasks, command) and captures the count into $jobcount.\n";
static const char USAGE_EXPECT_JOBS[] =
"expect-jobs <N>\n"
@ -2002,12 +2011,53 @@ static int reslist_thunk(void* ctxp)
return call->rc;
}
/* xce-test-cli-ux task 3.4, Requirement 5.1/5.2, 6.2: reslist's first-ever
* flag, --json (long-only -- no short form is required by the
* requirements and none is added here). Same long_opt spelling ("json")
* and concept ("json-output") as workerlist_flags/joblist_flags below --
* Requirement 6.2 mandates identical naming for the same concept across
* every command that has it. */
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[])
{
int rc, i;
int opt;
int json_mode = 0;
xce_resource_t** res;
reslist_call_t* call = (reslist_call_t*)calloc(1, sizeof(*call));
reslist_call_t* call;
struct option long_opts[RESLIST_NFLAGS + 1];
char optstring[8];
optind = 1;
opterr = 0;
if (flagspec_build_opts(reslist_flags, RESLIST_NFLAGS, long_opts,
optstring, sizeof(optstring))
< 0) {
fprintf(stderr,
"xce_test: reslist: internal error building flag table\n");
return -EINVAL;
}
while ((opt = getopt_long(argc, argv, optstring, long_opts, NULL))
!= -1) {
switch (opt) {
case FLAGSPEC_LONG_ONLY_BASE + RESLIST_OPT_JSON:
json_mode = 1;
break;
case '?':
return flagspec_reject("reslist", reslist_flags, RESLIST_NFLAGS,
argv[optind - 1], "reslist [--json]");
}
}
call = (reslist_call_t*)calloc(1, sizeof(*call));
if (call == NULL) {
return -ENOMEM;
}
@ -2032,6 +2082,32 @@ int cmd_reslist(int argc, char* argv[])
return rc;
}
if (json_mode) {
/* xce-test-cli-ux task 3.4, Requirement 5.1/5.3: emit the SAME
* fields the table below prints (Name, Flags, Interval,
* Description), unabbreviated, as one JSON object per resource.
* reslist_json_row() (listrow_json.h) owns the field mapping;
* listfmt_json.h (task 2.2) owns the array lifecycle/printing. */
cJSON* array = listfmt_json_begin();
if (array == NULL) {
free_Resource_list(rc, res);
return -ENOMEM;
}
for (i = 0; i < rc; i++) {
cJSON* row = listfmt_json_new_row(array);
reslist_json_row(row, res[i]->name, res[i]->flags,
res[i]->interval, res[i]->desc);
}
listfmt_json_finish(array);
free_Resource_list(rc, res);
return 0;
}
/* Note the extra literal space before the Description specifier: unlike
* cmd_workerlist's all-left-justified text columns (which self-separate
* via their own trailing padding), Interval stays right-justified here,
@ -2082,12 +2158,52 @@ static int workerlist_thunk(void* ctxp)
return call->rc;
}
/* xce-test-cli-ux task 3.4, Requirement 5.1/5.2, 6.2: workerlist's
* first-ever flag, --json -- IDENTICAL long_opt/concept spelling to
* reslist_flags/joblist_flags (Requirement 6.2). */
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[])
{
int rc, i;
int opt;
int json_mode = 0;
xce_worker_t** workers;
workerlist_call_t* call = (workerlist_call_t*)calloc(1, sizeof(*call));
workerlist_call_t* call;
struct option long_opts[WORKERLIST_NFLAGS + 1];
char optstring[8];
optind = 1;
opterr = 0;
if (flagspec_build_opts(workerlist_flags, WORKERLIST_NFLAGS, long_opts,
optstring, sizeof(optstring))
< 0) {
fprintf(stderr,
"xce_test: workerlist: internal error building flag table\n");
return -EINVAL;
}
while ((opt = getopt_long(argc, argv, optstring, long_opts, NULL))
!= -1) {
switch (opt) {
case FLAGSPEC_LONG_ONLY_BASE + WORKERLIST_OPT_JSON:
json_mode = 1;
break;
case '?':
return flagspec_reject("workerlist", workerlist_flags,
WORKERLIST_NFLAGS, argv[optind - 1],
"workerlist [--json]");
}
}
call = (workerlist_call_t*)calloc(1, sizeof(*call));
if (call == NULL) {
return -ENOMEM;
}
@ -2111,6 +2227,40 @@ int cmd_workerlist(int argc, char* argv[])
return rc;
}
if (json_mode) {
/* xce-test-cli-ux task 3.4, Requirement 5.1/5.3: emit the SAME
* fields the table below prints, unabbreviated, as one JSON
* object per worker -- MaxMem/MaxSwap/MaxTmp as their RAW byte
* values (not the table's humanized "64.0 MB"-style strings), see
* workerlist_json_row()'s own doc comment (listrow_json.h). */
cJSON* array = listfmt_json_begin();
if (array == NULL) {
free_Worker_list(rc, workers);
return -ENOMEM;
}
for (i = 0; i < rc; i++) {
cJSON* row = listfmt_json_new_row(array);
workerlist_json_row(row, workers[i]->name,
worker_status_str(workers[i]->status),
workers[i]->attrs->hosttype,
workers[i]->attrs->hostmodel,
workers[i]->attrs->cpufactor,
workers[i]->attrs->maxcpus,
workers[i]->attrs->maxmem,
workers[i]->attrs->maxswap,
workers[i]->attrs->maxtmp,
workers[i]->attrs->ndisks);
}
listfmt_json_finish(array);
free_Worker_list(rc, workers);
return 0;
}
printf("%-20s%-8s%-25s%-25s%12s%12s%12s%12s%12s%12s\n", "HostName",
"Status", "HostType", "HostModel", "CPUfactor", "MaxCPU", "MaxMem",
"MaxSwap", "MaxTmp", "NDisks");
@ -2162,6 +2312,13 @@ int cmd_workerlist(int argc, char* argv[])
* up. When `print_table` is set, the human-readable table is printed (the
* `cmd_joblist` path). The job array is always freed internally.
*
* `json_mode` (xce-test-cli-ux task 3.4, Requirement 5.1/5.2): when set,
* emits the SAME fields `print_table` would have printed as one JSON
* object per job instead (joblist_json_row(), listrow_json.h) --
* mutually exclusive with `print_table` in practice: `cmd_joblist` (the
* only caller that ever sets either) passes exactly one of the two as
* true, never both; `expect-jobs`/`expect-job` always pass 0 for both.
*
* On error this returns the negative rc WITHOUT setting any auto-variable, so a
* failed joblist never advertises a fake `$jobcount` (Req 3.6). */
typedef struct {
@ -2180,10 +2337,11 @@ static int joblist_thunk(void* ctxp)
}
static int joblist_query(const char* find_jobid, int* contains, int print_table,
int quiet)
int quiet, int json_mode)
{
int rc, i;
xce_job_t** jobs;
cJSON* json_array = NULL;
joblist_call_t* call = (joblist_call_t*)calloc(1, sizeof(*call));
if (contains != NULL) {
@ -2224,6 +2382,16 @@ static int joblist_query(const char* find_jobid, int* contains, int print_table,
printf("%-26s%-10s%-10s%-8s%-24s%-24s%-24s%12s%12s%30s\n", "JobId",
"User", "Queue", "State", "SubmitTime", "StartTime",
"FinishTime", "MinTasks", "MaxTasks", "Command");
} else if (json_mode) {
json_array = listfmt_json_begin();
if (json_array == NULL) {
for (i = 0; i < rc; i++) {
free_Job_ptr(&(jobs[i]));
}
free(jobs);
return -ENOMEM;
}
}
for (i = 0; i < rc; i++) {
@ -2252,9 +2420,27 @@ static int joblist_query(const char* find_jobid, int* contains, int print_table,
sizeof(finish_buf)),
jobs[i]->submit->mintasks, jobs[i]->submit->maxtasks,
jobs[i]->submit->start_info->cmdline);
} else if (json_mode) {
/* xce-test-cli-ux task 3.4, Requirement 5.1/5.3: the SAME
* fields the table above prints, unabbreviated, as one JSON
* object per job -- joblist_json_row() (listrow_json.h) owns
* the field mapping. */
cJSON* row = listfmt_json_new_row(json_array);
joblist_json_row(row, buf, jobs[i]->submit->user,
jobs[i]->submit->queue,
job_state_str(jobs[i]->state),
jobs[i]->queue_time, jobs[i]->start_time,
jobs[i]->finish_time, jobs[i]->submit->mintasks,
jobs[i]->submit->maxtasks,
jobs[i]->submit->start_info->cmdline);
}
}
if (json_mode) {
listfmt_json_finish(json_array);
}
for (i = 0; i < rc; i++) {
free_Job_ptr(&(jobs[i]));
}
@ -2264,11 +2450,49 @@ static int joblist_query(const char* find_jobid, int* contains, int print_table,
return rc;
}
/* xce-test-cli-ux task 3.4, Requirement 5.1/5.2, 6.2: joblist's first-ever
* flag, --json -- IDENTICAL long_opt/concept spelling to reslist_flags/
* workerlist_flags above (Requirement 6.2). */
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[])
{
int count;
int count;
int opt;
int json_mode = 0;
struct option long_opts[JOBLIST_NFLAGS + 1];
char optstring[8];
count = joblist_query(NULL, NULL, 1, /*quiet=*/0);
optind = 1;
opterr = 0;
if (flagspec_build_opts(joblist_flags, JOBLIST_NFLAGS, long_opts,
optstring, sizeof(optstring))
< 0) {
fprintf(stderr,
"xce_test: joblist: internal error building flag table\n");
return -EINVAL;
}
while ((opt = getopt_long(argc, argv, optstring, long_opts, NULL))
!= -1) {
switch (opt) {
case FLAGSPEC_LONG_ONLY_BASE + JOBLIST_OPT_JSON:
json_mode = 1;
break;
case '?':
return flagspec_reject("joblist", joblist_flags, JOBLIST_NFLAGS,
argv[optind - 1], "joblist [--json]");
}
}
count = joblist_query(NULL, NULL, /*print_table=*/!json_mode,
/*quiet=*/0, json_mode);
if (count < 0) {
return count;
@ -2319,7 +2543,7 @@ int cmd_expect_jobs(int argc, char* argv[])
/* Print-free count via the shared helper. A negative rc means the query
* itself failed (e.g. no reachable master) — that is an assertion failure,
* not "0 jobs". */
count = joblist_query(NULL, NULL, 0, /*quiet=*/0);
count = joblist_query(NULL, NULL, 0, /*quiet=*/0, /*json_mode=*/0);
if (count < 0) {
printf("expect-jobs: could not determine job count (joblist query "
@ -2367,7 +2591,7 @@ int cmd_expect_job(int argc, char* argv[])
/* Print-free membership query via the shared helper. A negative rc means the
* query itself failed (e.g. no reachable master) — that is an assertion
* failure, distinct from "job absent". */
rc = joblist_query(argv[1], &contains, 0, /*quiet=*/0);
rc = joblist_query(argv[1], &contains, 0, /*quiet=*/0, /*json_mode=*/0);
if (rc < 0) {
printf("expect-job: could not determine membership of '%s' (joblist "

View file

@ -0,0 +1,378 @@
/* test_listrow_json.c -- xce-test-cli-ux task 3.4 (Requirements 5.1, 5.2,
* 5.3) unit tests for listrow_json.c's pure per-command JSON-row
* field-mapping helpers.
*
* Standalone Unity binary with its own main() -- mirrors
* test_joblist_display.c's/test_resval_typeresolve.c's precedent (task
* 2.5/resource-value-verification task 1.1): listrow_json.c has zero
* dependency on test.c or any networking, so it is built+tested in
* isolation rather than folded into controller_test.c's shared suite.
*
* Every test builds a JSON row directly with the module under test, then
* either inspects the in-memory cJSON tree (lifecycle-only assertions) or
* routes it through listfmt_json_finish() (task 2.2, unmodified) to prove
* a genuine JSON-text round trip -- this is where the fractional-value
* (cpufactor) round-trip test lives, closing the gap task 2.2's own
* Implementation Notes flagged ("listfmt_json.c's tests only exercise
* power-of-two doubles ... task 3.4 should add a real fractional-value
* round-trip test once actual command fields are wired in").
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <unity/unity.h>
#include "listfmt_json.h"
#include "listrow_json.h"
/* ---- stdout capture, for the listfmt_json_finish()-based round-trip
* tests -- mirrors test_listfmt_json.c's own dup/dup2/freopen technique
* exactly (that file's tests establish the pattern this one reuses). */
static char g_dir[256];
static void fresh_dir(void)
{
strcpy(g_dir, "/tmp/xce_listrow_json_test_XXXXXX");
TEST_ASSERT_NOT_NULL(mkdtemp(g_dir));
}
static void cleanup_dir(void)
{
char cmd[512];
snprintf(cmd, sizeof(cmd), "rm -rf %s", g_dir);
system(cmd);
}
typedef struct {
int saved_stdout_fd;
char stdout_path[300];
} capture_t;
static void capture_begin(capture_t* cap)
{
fflush(stdout);
cap->saved_stdout_fd = dup(fileno(stdout));
TEST_ASSERT_TRUE(cap->saved_stdout_fd >= 0);
snprintf(cap->stdout_path, sizeof(cap->stdout_path), "%s/out", g_dir);
TEST_ASSERT_NOT_NULL(freopen(cap->stdout_path, "w", stdout));
}
static void capture_end(capture_t* cap, char** out_stdout)
{
fflush(stdout);
TEST_ASSERT_TRUE(dup2(cap->saved_stdout_fd, fileno(stdout)) >= 0);
close(cap->saved_stdout_fd);
clearerr(stdout);
FILE* f = fopen(cap->stdout_path, "r");
TEST_ASSERT_NOT_NULL(f);
fseek(f, 0, SEEK_END);
long len = ftell(f);
fseek(f, 0, SEEK_SET);
*out_stdout = malloc((size_t)len + 1);
TEST_ASSERT_NOT_NULL(*out_stdout);
size_t n = fread(*out_stdout, 1, (size_t)len, f);
(*out_stdout)[n] = '\0';
fclose(f);
}
/* ---- reslist_json_row ---------------------------------------------- */
static void test_reslist_json_row_maps_every_table_field(void)
{
cJSON* array = listfmt_json_begin();
cJSON* row = listfmt_json_new_row(array);
reslist_json_row(row, "mem", 0x2a, 60, "physical memory");
/* Every column cmd_reslist's table header prints (Name, Flags,
* Interval, Description) must be present -- Requirement 5.1. */
TEST_ASSERT_EQUAL_STRING(
"mem", cJSON_GetStringValue(cJSON_GetObjectItem(row, "name")));
TEST_ASSERT_EQUAL_STRING("physical memory", cJSON_GetStringValue(
cJSON_GetObjectItem(
row, "description")));
TEST_ASSERT_EQUAL_FLOAT(
60.0f, (float)cJSON_GetNumberValue(cJSON_GetObjectItem(row, "interval")));
cJSON_Delete(array);
}
static void test_reslist_json_row_flags_preserves_raw_numeric_value(void)
{
/* bug-1711 (Requirement 11.4) formats Flags as "0x%x" for the TABLE
* only -- the JSON row must not lose the underlying value. 0x2a == 42
* decimal; assert the JSON "flags" field carries that same integer,
* not a hex string. */
cJSON* array = listfmt_json_begin();
cJSON* row = listfmt_json_new_row(array);
reslist_json_row(row, "mem", 0x2a, 60, "physical memory");
cJSON* flags = cJSON_GetObjectItem(row, "flags");
TEST_ASSERT_NOT_NULL(flags);
TEST_ASSERT_TRUE(cJSON_IsNumber(flags));
TEST_ASSERT_EQUAL_FLOAT(42.0f, (float)cJSON_GetNumberValue(flags));
cJSON_Delete(array);
}
static void test_reslist_json_row_long_description_not_truncated(void)
{
/* Requirement 5.3: no truncation regardless of value length -- the
* table's Description column is a fixed "%-64s" field; a value longer
* than 64 characters exercises exactly the scenario the table format
* would visually misalign but JSON must not lose or clip. */
enum { LONG_LEN = 300 };
char long_desc[LONG_LEN + 1];
for (int i = 0; i < LONG_LEN; i++)
long_desc[i] = (char)('a' + (i % 26));
long_desc[LONG_LEN] = '\0';
cJSON* array = listfmt_json_begin();
cJSON* row = listfmt_json_new_row(array);
reslist_json_row(row, "longres", 0, 1, long_desc);
const char* desc =
cJSON_GetStringValue(cJSON_GetObjectItem(row, "description"));
TEST_ASSERT_NOT_NULL(desc);
TEST_ASSERT_EQUAL_INT(LONG_LEN, (int)strlen(desc));
TEST_ASSERT_EQUAL_STRING(long_desc, desc);
cJSON_Delete(array);
}
static void test_reslist_json_row_noop_on_null_row(void)
{
/* Must not crash. */
reslist_json_row(NULL, "mem", 1, 1, "desc");
}
/* ---- workerlist_json_row --------------------------------------------- */
static void test_workerlist_json_row_maps_every_table_field(void)
{
cJSON* array = listfmt_json_begin();
cJSON* row = listfmt_json_new_row(array);
workerlist_json_row(row, "worker-07", "OK", "linux", "x86_64", 1.0, 8,
67108864.0, 134217728.0, 268435456.0, 2);
/* Every column cmd_workerlist's table header prints (HostName, Status,
* HostType, HostModel, CPUfactor, MaxCPU, MaxMem, MaxSwap, MaxTmp,
* NDisks) must be present -- Requirement 5.1. */
TEST_ASSERT_EQUAL_STRING(
"worker-07",
cJSON_GetStringValue(cJSON_GetObjectItem(row, "hostname")));
TEST_ASSERT_EQUAL_STRING(
"OK", cJSON_GetStringValue(cJSON_GetObjectItem(row, "status")));
TEST_ASSERT_EQUAL_STRING(
"linux", cJSON_GetStringValue(cJSON_GetObjectItem(row, "hosttype")));
TEST_ASSERT_EQUAL_STRING(
"x86_64",
cJSON_GetStringValue(cJSON_GetObjectItem(row, "hostmodel")));
TEST_ASSERT_EQUAL_FLOAT(
8.0f, (float)cJSON_GetNumberValue(cJSON_GetObjectItem(row, "maxcpu")));
TEST_ASSERT_EQUAL_FLOAT(
2.0f, (float)cJSON_GetNumberValue(cJSON_GetObjectItem(row, "ndisks")));
cJSON_Delete(array);
}
static void test_workerlist_json_row_maxmem_is_raw_bytes_not_humanized(void)
{
/* The table humanizes MaxMem/MaxSwap/MaxTmp via format_bytes_human()
* (e.g. "64.0 MB") for DISPLAY only; the JSON row must carry the raw
* byte value instead, so a script can consume it without re-parsing a
* unit suffix. Use a value that is NOT a round unit boundary to prove
* this is the raw byte count, not a rounded "MB" figure. */
cJSON* array = listfmt_json_begin();
cJSON* row = listfmt_json_new_row(array);
workerlist_json_row(row, "worker-01", "OK", "linux", "x86_64", 1.0, 4,
100000000.0, 0.0, 0.0, 1);
cJSON* maxmem = cJSON_GetObjectItem(row, "maxmem");
TEST_ASSERT_NOT_NULL(maxmem);
TEST_ASSERT_TRUE(cJSON_IsNumber(maxmem));
TEST_ASSERT_EQUAL_FLOAT(100000000.0f, (float)cJSON_GetNumberValue(maxmem));
cJSON_Delete(array);
}
/* This is the fractional-value round-trip test task 2.2's Implementation
* Note explicitly asked for: listfmt_json.c's own tests only exercised
* power-of-two doubles; cpufactor is a genuine command field that is
* commonly fractional (e.g. a machine rated at 1.5x or 0.75x a reference
* host), and this proves the value survives an ACTUAL JSON-text
* round-trip (via listfmt_json_finish()'s real cJSON_Print() path, then
* re-parsed with cJSON_Parse()) bit-for-bit, not just while sitting in the
* in-memory cJSON tree. */
static void test_workerlist_json_row_cpufactor_fractional_value_roundtrips(void)
{
cJSON* array = listfmt_json_begin();
cJSON* row = listfmt_json_new_row(array);
workerlist_json_row(row, "worker-09", "OK", "linux", "x86_64", 12.34, 4,
4294967296.0, 0.0, 0.0, 1);
fresh_dir();
capture_t cap;
capture_begin(&cap);
listfmt_json_finish(array); /* prints then frees `array`/`row` */
char* out;
capture_end(&cap, &out);
cJSON* parsed = cJSON_Parse(out);
TEST_ASSERT_NOT_NULL(parsed);
TEST_ASSERT_EQUAL_INT(1, cJSON_GetArraySize(parsed));
cJSON* p0 = cJSON_GetArrayItem(parsed, 0);
cJSON* cpufactor = cJSON_GetObjectItem(p0, "cpufactor");
TEST_ASSERT_NOT_NULL(cpufactor);
TEST_ASSERT_TRUE(cJSON_IsNumber(cpufactor));
/* Unity is built without UNITY_INCLUDE_DOUBLE (this codebase's
* convention) -- both sides are cast to float for comparison, same as
* test_listfmt_json.c's own pattern; since 12.34 round-trips exactly
* at double precision (cJSON's shortest-round-trippable-double
* formatting), the float cast of both sides is still an exact
* equality check, not merely "close enough". */
TEST_ASSERT_EQUAL_FLOAT(12.34f, (float)cJSON_GetNumberValue(cpufactor));
/* A non-power-of-two maxmem value in the SAME row must also survive,
* proving the fractional field didn't corrupt a neighboring one. */
TEST_ASSERT_EQUAL_FLOAT(
4294967296.0f,
(float)cJSON_GetNumberValue(cJSON_GetObjectItem(p0, "maxmem")));
cJSON_Delete(parsed);
free(out);
cleanup_dir();
}
static void test_workerlist_json_row_noop_on_null_row(void)
{
workerlist_json_row(NULL, "h", "OK", "t", "m", 1.0, 1, 1.0, 1.0, 1.0, 1);
}
/* ---- joblist_json_row -------------------------------------------------- */
static void test_joblist_json_row_maps_every_table_field(void)
{
cJSON* array = listfmt_json_begin();
cJSON* row = listfmt_json_new_row(array);
joblist_json_row(row, "1.0", "alice", "normal", "RUN",
(time_t)1000, (time_t)1010, (time_t)0, 2, 4,
"/bin/sleep 100");
/* Every column joblist_query's table header prints (JobId, User,
* Queue, State, SubmitTime, StartTime, FinishTime, MinTasks, MaxTasks,
* Command) must be present -- Requirement 5.1, including the
* mintasks/maxtasks pair as two distinct fields (mirroring
* Requirement 11.6's table-side fix, not a single ambiguous
* "ntasks"). */
TEST_ASSERT_EQUAL_STRING(
"1.0", cJSON_GetStringValue(cJSON_GetObjectItem(row, "jobid")));
TEST_ASSERT_EQUAL_STRING(
"alice", cJSON_GetStringValue(cJSON_GetObjectItem(row, "user")));
TEST_ASSERT_EQUAL_STRING(
"normal", cJSON_GetStringValue(cJSON_GetObjectItem(row, "queue")));
TEST_ASSERT_EQUAL_STRING(
"RUN", cJSON_GetStringValue(cJSON_GetObjectItem(row, "state")));
TEST_ASSERT_EQUAL_STRING(
"/bin/sleep 100",
cJSON_GetStringValue(cJSON_GetObjectItem(row, "command")));
TEST_ASSERT_EQUAL_FLOAT(
1000.0f,
(float)cJSON_GetNumberValue(cJSON_GetObjectItem(row, "submit_time")));
TEST_ASSERT_EQUAL_FLOAT(
1010.0f,
(float)cJSON_GetNumberValue(cJSON_GetObjectItem(row, "start_time")));
TEST_ASSERT_EQUAL_FLOAT(
0.0f,
(float)cJSON_GetNumberValue(cJSON_GetObjectItem(row, "finish_time")));
TEST_ASSERT_EQUAL_FLOAT(
2.0f,
(float)cJSON_GetNumberValue(cJSON_GetObjectItem(row, "mintasks")));
TEST_ASSERT_EQUAL_FLOAT(
4.0f,
(float)cJSON_GetNumberValue(cJSON_GetObjectItem(row, "maxtasks")));
cJSON_Delete(array);
}
static void test_joblist_json_row_ambiguous_state_fallback_kept_verbatim(void)
{
/* Requirement 11.7's raw-state fallback ("RUN(0x38)"-style text from
* job_state_str()) must not be re-interpreted or stripped -- the JSON
* row stores whatever string the caller passed in. */
cJSON* array = listfmt_json_begin();
cJSON* row = listfmt_json_new_row(array);
joblist_json_row(row, "2.0", "bob", "normal", "RUN(0x38)", (time_t)5,
(time_t)6, (time_t)7, 1, 1, "cmd");
TEST_ASSERT_EQUAL_STRING(
"RUN(0x38)",
cJSON_GetStringValue(cJSON_GetObjectItem(row, "state")));
cJSON_Delete(array);
}
static void test_joblist_json_row_unstarted_job_times_are_zero_not_dash(void)
{
/* format_job_time() renders a 0 time_t as "-" for the human table
* (Requirement 11.5); the JSON row instead keeps the raw 0 as a
* NUMBER (not the string "-") so a script can compare/sort on it
* without special-casing a sentinel string. */
cJSON* array = listfmt_json_begin();
cJSON* row = listfmt_json_new_row(array);
joblist_json_row(row, "3.0", "carol", "normal", "PEND", (time_t)0,
(time_t)0, (time_t)0, 1, 1, "cmd");
cJSON* start = cJSON_GetObjectItem(row, "start_time");
TEST_ASSERT_NOT_NULL(start);
TEST_ASSERT_TRUE(cJSON_IsNumber(start));
TEST_ASSERT_EQUAL_FLOAT(0.0f, (float)cJSON_GetNumberValue(start));
cJSON_Delete(array);
}
static void test_joblist_json_row_noop_on_null_row(void)
{
joblist_json_row(NULL, "1.0", "u", "q", "RUN", 1, 1, 1, 1, 1, "c");
}
void setUp(void) { return; }
void tearDown(void) { return; }
int main(void)
{
printf("\n>> xce-test-cli-ux task 3.4: listrow_json JSON-row "
"field-mapping helpers (reslist/workerlist/joblist)\n\n");
UNITY_BEGIN();
RUN_TEST(test_reslist_json_row_maps_every_table_field);
RUN_TEST(test_reslist_json_row_flags_preserves_raw_numeric_value);
RUN_TEST(test_reslist_json_row_long_description_not_truncated);
RUN_TEST(test_reslist_json_row_noop_on_null_row);
RUN_TEST(test_workerlist_json_row_maps_every_table_field);
RUN_TEST(test_workerlist_json_row_maxmem_is_raw_bytes_not_humanized);
RUN_TEST(test_workerlist_json_row_cpufactor_fractional_value_roundtrips);
RUN_TEST(test_workerlist_json_row_noop_on_null_row);
RUN_TEST(test_joblist_json_row_maps_every_table_field);
RUN_TEST(test_joblist_json_row_ambiguous_state_fallback_kept_verbatim);
RUN_TEST(test_joblist_json_row_unstarted_job_times_are_zero_not_dash);
RUN_TEST(test_joblist_json_row_noop_on_null_row);
return UNITY_END();
}

View file

@ -1438,6 +1438,179 @@ static void test_walcompare_unrecognized_flag_rejected_with_usage(void)
clitest_buf_free(&err);
}
/*
* test_listing_commands_json_flag_accepted_and_bogus_flag_rejected --
* xce-test-cli-ux task 3.4, Requirement 5.1, 5.2, 6.2, 2.1: workerlist/
* joblist/reslist are migrated onto the shared flagspec_t library for the
* FIRST time by this task (none accepted any flag before it) -- this
* proves the CLI surface of that migration end-to-end against the real
* built binary: a genuine typo is rejected via the same flagspec_reject()
* mechanism every other migrated command uses (Requirement 2.1), and the
* new --json flag itself is recognized (not confused with an unrecognized
* flag) by all three, using the IDENTICAL long_opt spelling across all
* three commands' flagspec_t tables (Requirement 6.2).
*
* No live controller is running in this test, so --json's underlying RPC
* still fails -- but per this task's own validation guidance, that
* failure must be a NORMAL command failure (the same perror()-based
* "xce_reslist"/"xce_workerlist"/"xce_joblist" label each command's
* pre-existing non-flag failure path already uses), never the
* "unrecognized flag" rejection text -- proving --json parsed
* successfully and execution proceeded past the getopt_long() loop into
* the RPC call. The JSON field-mapping/content correctness itself
* (Requirement 5.1 "same fields", 5.3 "no truncation") is covered at the
* unit level by test_listrow_json.c, which needs no live cluster and so
* can assert on the actual JSON text -- this black-box test is
* deliberately scoped to the CLI-parsing surface only.
*/
static void test_listing_commands_json_flag_accepted_and_bogus_flag_rejected(
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");
/* A genuine typo on each of the three commands must still be rejected
* exactly like every other migrated command (Requirement 2.1) -- this
* is the FIRST flag these three commands have ever accepted, so it is
* worth proving the migration itself (flagspec_build_opts/
* flagspec_reject wiring), not just the new --json flag in isolation. */
TEST_ASSERT_EQUAL_INT(0, clitest_send_line(&sess, "reslist --bogus"));
TEST_ASSERT_TRUE_MESSAGE(
clitest_wait_for_text(&sess, &out, &err,
"reslist: unrecognized flag '--bogus'", true,
5000),
"reslist must reject an unrecognized flag (Requirement 2.1)");
TEST_ASSERT_TRUE_MESSAGE(
clitest_wait_for_text(&sess, &out, &err, "usage: reslist", true,
5000),
"the rejection must include reslist's usage summary, mentioning "
"--json (Requirement 2.4, 5.4)");
TEST_ASSERT_EQUAL_INT(0, clitest_send_line(&sess, "workerlist --bogus"));
TEST_ASSERT_TRUE_MESSAGE(
clitest_wait_for_text(&sess, &out, &err,
"workerlist: unrecognized flag '--bogus'",
true, 5000),
"workerlist must reject an unrecognized flag (Requirement 2.1)");
TEST_ASSERT_EQUAL_INT(0, clitest_send_line(&sess, "joblist --bogus"));
TEST_ASSERT_TRUE_MESSAGE(
clitest_wait_for_text(&sess, &out, &err,
"joblist: unrecognized flag '--bogus'", true,
5000),
"joblist must reject an unrecognized flag (Requirement 2.1)");
/* Now the new --json flag itself: reset the captured buffers first so
* each check below is judged against only ITS OWN command's output,
* not contaminated by the "unrecognized flag" text already captured
* above for the deliberate typos. */
clitest_buf_free(&out);
clitest_buf_init(&out);
clitest_buf_free(&err);
clitest_buf_init(&err);
TEST_ASSERT_EQUAL_INT(0, clitest_send_line(&sess, "reslist --json"));
TEST_ASSERT_EQUAL_INT(
0, clitest_send_line(&sess, "echo marker_reslist_json_done"));
TEST_ASSERT_TRUE_MESSAGE(
clitest_wait_for_text(&sess, &out, &err, "marker_reslist_json_done",
false, 5000),
"reslist --json must run to completion (not hang) even without a "
"live cluster");
TEST_ASSERT_NULL_MESSAGE(
strstr(err.data, "unrecognized flag"),
"--json must be a recognized flag for reslist, not rejected "
"(Requirement 6.2: identical long_opt spelling across all three "
"listing commands)");
TEST_ASSERT_NOT_NULL_MESSAGE(
strstr(err.data, "xce_reslist"),
"with --json accepted, execution must reach reslist's normal "
"(non-flag-parse) RPC failure path, proving parsing succeeded");
clitest_buf_free(&out);
clitest_buf_init(&out);
clitest_buf_free(&err);
clitest_buf_init(&err);
TEST_ASSERT_EQUAL_INT(0, clitest_send_line(&sess, "workerlist --json"));
TEST_ASSERT_EQUAL_INT(
0, clitest_send_line(&sess, "echo marker_workerlist_json_done"));
TEST_ASSERT_TRUE_MESSAGE(
clitest_wait_for_text(&sess, &out, &err,
"marker_workerlist_json_done", false, 5000),
"workerlist --json must run to completion (not hang) even without "
"a live cluster");
TEST_ASSERT_NULL_MESSAGE(
strstr(err.data, "unrecognized flag"),
"--json must be a recognized flag for workerlist, not rejected "
"(Requirement 6.2)");
TEST_ASSERT_NOT_NULL_MESSAGE(
strstr(err.data, "xce_workerlist"),
"with --json accepted, execution must reach workerlist's normal "
"(non-flag-parse) RPC failure path, proving parsing succeeded");
clitest_buf_free(&out);
clitest_buf_init(&out);
clitest_buf_free(&err);
clitest_buf_init(&err);
TEST_ASSERT_EQUAL_INT(0, clitest_send_line(&sess, "joblist --json"));
TEST_ASSERT_EQUAL_INT(
0, clitest_send_line(&sess, "echo marker_joblist_json_done"));
TEST_ASSERT_TRUE_MESSAGE(
clitest_wait_for_text(&sess, &out, &err, "marker_joblist_json_done",
false, 5000),
"joblist --json must run to completion (not hang) even without a "
"live cluster");
TEST_ASSERT_NULL_MESSAGE(
strstr(err.data, "unrecognized flag"),
"--json must be a recognized flag for joblist, not rejected "
"(Requirement 6.2)");
TEST_ASSERT_NOT_NULL_MESSAGE(
strstr(err.data, "xce_joblist"),
"with --json accepted, execution must reach joblist's normal "
"(non-flag-parse) RPC failure path, proving parsing succeeded");
TEST_ASSERT_EQUAL_INT(0, clitest_send_line(&sess, "quit"));
TEST_ASSERT_EQUAL_INT_MESSAGE(
0, clitest_wait_for_exit(&sess, &out, &err, 5000, &exit_code),
"xce_test did not exit within the harness's safety timeout after "
"'quit'");
TEST_ASSERT_EQUAL_INT_MESSAGE(
1, exit_code,
"the three rejected --bogus invocations above must mark the "
"session failed");
clitest_kill_and_reap(&sess);
clitest_close(&sess);
clitest_buf_free(&out);
clitest_buf_free(&err);
}
/* ------------------------------------------------------------------ */
/* Interruptible Call Adapter end-to-end (task 3.2, Requirement 1) */
/* ------------------------------------------------------------------ */
@ -2319,6 +2492,7 @@ void test_test_cli(void)
RUN_TEST(test_qctrl_flag_forms_short_and_long_both_accepted);
RUN_TEST(test_migrated_commands_sample_reject_unrecognized_flag);
RUN_TEST(test_walcompare_unrecognized_flag_rejected_with_usage);
RUN_TEST(test_listing_commands_json_flag_accepted_and_bogus_flag_rejected);
RUN_TEST(test_interrupt_abandons_blocked_rpc_well_before_timeout);
RUN_TEST(test_interrupt_at_empty_prompt_discards_line_without_exiting);
RUN_TEST(test_interrupt_then_immediate_teardown_no_uaf);