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:
parent
a57c04716b
commit
0d67f5cbde
7 changed files with 996 additions and 16 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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'])
|
||||
|
||||
|
|
|
|||
62
src/controller/testsuite/listrow_json.c
Normal file
62
src/controller/testsuite/listrow_json.c
Normal 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);
|
||||
}
|
||||
113
src/controller/testsuite/listrow_json.h
Normal file
113
src/controller/testsuite/listrow_json.h
Normal 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);
|
||||
|
|
@ -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 "
|
||||
|
|
|
|||
378
src/controller/testsuite/test_listrow_json.c
Normal file
378
src/controller/testsuite/test_listrow_json.c
Normal 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();
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue