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

@ -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 "