From f165e3a7516cedbdf290e94d9c141dc57ecf4d69 Mon Sep 17 00:00:00 2001 From: Rongsong Shen Date: Wed, 2 Sep 2026 07:31:31 +0000 Subject: [PATCH] feat(scheduler-sim-repl-verbosity): task 1.1 - RED-phase tests for job identifier range compression Adds failing unit tests for repl_fmt_jobid_range() covering contiguous ranges, non-contiguous sub-ranges, multiple array bases, mixed array/ non-array ids, and a too-small output buffer (Requirement 4, AC 4.1-4.5). The function itself is not yet implemented (task 1.2) - build fails at link time with undefined reference to repl_fmt_jobid_range, confirming the RED phase. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01YAWMhfRJFnmmnG8h97yUDd --- .../scheduler-sim-repl-verbosity/tasks.md | 76 ++++- src/scheduler/testsuite/meson.build | 20 ++ .../testsuite/test_repl_output_compression.c | 317 ++++++++++++++++++ 3 files changed, 401 insertions(+), 12 deletions(-) create mode 100644 src/scheduler/testsuite/test_repl_output_compression.c diff --git a/.kiro/specs/scheduler-sim-repl-verbosity/tasks.md b/.kiro/specs/scheduler-sim-repl-verbosity/tasks.md index 72973746..e7df9ce9 100644 --- a/.kiro/specs/scheduler-sim-repl-verbosity/tasks.md +++ b/.kiro/specs/scheduler-sim-repl-verbosity/tasks.md @@ -1,7 +1,7 @@ # Implementation Plan - [ ] 1. Foundation: job identifier range compression -- [ ] 1.1 Write failing unit tests for compressing job-array identifier listings into index ranges +- [x] 1.1 Write failing unit tests for compressing job-array identifier listings into index ranges - Cover a single contiguous range, non-contiguous indices within one array submission, multiple distinct array submissions in one listing, a mix of array and non-array identifiers, and an output buffer too small to hold the full listing - Observable: the new test cases execute and fail against the current codebase, which has no compression behavior yet - _Requirements: 4.1, 4.2, 4.3, 4.4, 4.5_ @@ -14,9 +14,9 @@ - [ ] 2. Core: per-round decision summary, repetitive-round collapsing, and the shared range-flush capability - [ ] 2.1 Write failing tests for round-level decision summaries, collapsing, and the shared flush capability - - Cover: a round with at least one decision produces one summary line naming decision counts and affected job identifiers; a round with zero decisions produces no individual line; two or more consecutive idle rounds collapse into a single range notification; two or more consecutive rounds with an identical decision outcome collapse into a single range notification; a collapsed range notification states its round bounds, round count, and covered time span; a change in outcome ends an active collapsed range; a job's status change between rounds is reflected in the following round's summary even without a dedicated dispatch event to observe; the shared flush capability safely does nothing when no range is active and is safe to call more than once in a row + - Cover: a round with at least one decision produces one summary line naming decision counts and affected job identifiers; a round with zero decisions produces no individual line; two or more consecutive idle rounds collapse into a single range notification; two or more consecutive rounds with an identical decision outcome collapse into a single range notification; a collapsed range notification states its round bounds, round count, and covered time span; a change in outcome ends an active collapsed range; a job's status change between rounds is reflected in the following round's summary even without a dedicated dispatch event to observe; the shared flush capability safely does nothing when no range is active and is safe to call more than once in a row; an active collapsed range open at replay completion is flushed and printed before `xce_repl_on_replay_complete()`'s existing completion banner - Observable: the new test cases fail against today's unconditional per-decision echo, which has no summarizing, collapsing, or flush behavior - - _Requirements: 2.1, 2.2, 2.4, 3.1, 3.2, 3.3, 3.4_ + - _Requirements: 2.1, 2.2, 2.4, 3.1, 3.2, 3.3, 3.4, 3.6_ - _Boundary: Round Rollup_ - _Depends: 1.2_ @@ -30,7 +30,7 @@ - [ ] 3. (P) Core: mode-aware event echo, including ending an active collapsed range on structural events or breakpoint hits — Depends: 2.2 - [ ] 3.1 (P) Write failing tests for suppressing routine event chatter and for ending an active collapsed range at the right moments - - Cover: a routine per-tick event (job progress, resource usage, or resource value update) produces no line under continuous execution; the same event still produces a line while single-stepping; a structural change (new jobs, workers, resources, or configuration arriving) still produces a line under continuous execution and ends any active collapsed range first; an event matching a breakpoint still interrupts continuous execution regardless of its classification, and ends any active collapsed range before the interruption is shown + - Cover: a routine per-tick event (a job status change, job progress update, job resource-usage update, queue update, or partial/delta resource-value update) produces no line under continuous execution; the same event still produces a line while single-stepping; a structural change (new jobs, workers, or resources arriving, a full resource-value snapshot arriving, or configuration pushed) still produces a line under continuous execution and ends any active collapsed range first; an event matching a breakpoint still interrupts continuous execution regardless of its classification, and ends any active collapsed range before the interruption is shown - Observable: the new test cases fail against today's unconditional per-event echo, which has no classification or range-ending behavior - _Requirements: 1.1, 1.2, 1.3, 1.4, 3.5, 3.6_ - _Boundary: Event Echo Gate_ @@ -58,12 +58,64 @@ - _Requirements: 2.3, 5.1, 5.4_ - _Boundary: sim_print_decision gate_ -- [ ] 5. Validation: regression and real-data verification — Depends: 3.2, 4.2 -- [ ] 5.1 Verify single-stepping and every other existing REPL command remain unchanged - - Confirm single-stepping output, and the inspection/breakpoint/parameter/snapshot commands' behavior and output, match their pre-feature baseline exactly; confirm no new command was introduced - - Observable: the existing REPL test suite's pre-existing assertions all still pass unmodified, and a step-mode transcript comparison shows no textual difference from the pre-feature baseline - - _Requirements: 5.1, 5.2, 5.3_ +- [ ] 6. (P) REPL prompt shows current session and timestamp +- [ ] 6.1 (P) Write failing tests for the dynamic REPL prompt + - Cover: the prompt is the plain `(sim) ` before any session has begun; once at least one session has run, the prompt includes the current session count and a human-readable timestamp; this holds in both `step` and `continue` modes; the prompt never alters any command's own output + - Observable: the new test cases fail against today's static `XCE_REPL_PROMPT` macro, which never varies + - _Requirements: 5.5_ + - _Boundary: REPL Prompt Context_ -- [ ] 5.2 Run the full scheduler test suite and re-verify against the real replay capture that originally motivated this feature - - Observable: the full test suite passes with no regressions, and re-running the interactive REPL's continuous execution against the real capture reproduces the collapsed output shape validated during design, cutting a several-thousand-line transcript down to a handful of lines while preserving every state change - - _Requirements: 2.1, 2.2, 3.1, 3.2, 3.3, 3.4_ +- [ ] 6.2 (P) Implement the dynamic prompt + - Build the prompt fresh on every `repl_command_loop()` iteration from the current session count and `xce_core_now()`/`repl_format_ts_human()`, falling back to the original plain prompt before any session begins + - Observable: all tests from 6.1 pass; a live `step`/`continue` session shows the session/timestamp in the prompt starting from the first completed session, with no change to any command's own printed output + - _Requirements: 5.5_ + - _Boundary: REPL Prompt Context_ + +- [ ] 7. (P) Resource value classification, human-readable scaling, and the `set units` option +- [ ] 7.1 (P) Write failing tests for byte/megabyte classification, adaptive scaling, and `set units` parsing + - Cover: the bytes-valued classifier returns true only for `mem`/`tmp`/`maxmem`/`maxtmp`/`maxswap` and false for `swp`, unitless resources, and unknown names; the scaling helper adaptively renders across B/KB/MB/GB/TB, including exact unit-boundary values; `swp`'s value is converted MB→bytes before scaling; `set units raw`/`human`/`both` each update the display-mode setting, defaulting to `both`; an unrecognized `set units` value is rejected the same way other unrecognized `set` parameters already are + - Observable: the new test cases fail against the current codebase, which has neither the classifier/scaling helpers nor a `units` setting + - _Requirements: 6.1, 6.2, 6.3_ + - _Boundary: Resource Value Formatter_ + +- [ ] 7.2 (P) Implement the classification/scaling helpers, the `units_mode` setting, and `set units` parsing + - Add the bytes-valued classifier and the adaptive human-scaling formatter as pure functions; add the `units_mode` field to the REPL session state, explicitly defaulted to `both` (not left to zero-initialization); wire `set units raw|human|both` into the existing `set` command dispatch alongside `speed`/`output`/`verify` + - Observable: all tests from 7.1 pass; the helpers and the new setting are ready for the resource-listing rendering work to adopt + - _Requirements: 6.1, 6.2, 6.3_ + - _Boundary: Resource Value Formatter_ + +- [ ] 8. (P) `info worker`/`info resource` grouped table layout and units-mode-aware rendering — Depends: 1.2, 7.2 +- [ ] 8.1 (P) Write failing tests for the resource-listing table layout and units-mode rendering + - Cover: `info worker`'s and `info resource`'s resource listings render as an aligned table grouped by category (consumable/capacity, load metrics, static hardware attributes, where applicable to that listing) instead of one flat list; a whole-number unitless value renders with no trailing `.00`, a fractional one keeps its decimals; each `units_mode` (`raw`/`human`/`both`) selects the corresponding rendering for a byte/megabyte-valued resource; the underlying stored/wire value and replay determinism are unaffected; `info worker`'s assigned-job listing renders using the existing jobid-range-compression helper + - Observable: the new test cases fail against today's unconditional flat per-resource `fprintf` loop and un-compressed assigned-job listing + - _Requirements: 6.4, 6.5, 6.6, 4.5_ + - _Boundary: Resource Value Formatter, Jobid Range Compressor_ + +- [ ] 8.2 (P) Implement the grouped table rendering for `info worker` and `info resource` + - Replace the flat per-resource print loop with a shared, grouped, aligned-column table renderer reading `rs->units_mode`; adopt the jobid-range-compression helper for the assigned-job listing + - Observable: all tests from 8.1 pass; a live `info worker all` against a real captured worker reproduces the hand-validated grouped-table shape instead of 20 unstructured lines + - _Requirements: 6.4, 6.5, 6.6, 4.5_ + - _Boundary: Resource Value Formatter, Jobid Range Compressor_ + +- [ ] 9. (P) Discoverable `help` listing and `break` event types — Depends: 7.2 +- [ ] 9.1 (P) Write failing tests for the reorganized `help` listing and `break`'s event-type discoverability + - Cover: `help`'s output groups commands into the four labeled sections (execution control, breakpoints, state inspection, snapshot and parameters), uses one consistent column width throughout, lists `info config` alongside the other `info` subcommands, and lists `set units` alongside `set speed`/`set output`/`set verify` in the snapshot-and-parameters section; `help`'s `break` line and `break`'s unrecognized-event-type error both list the complete, current set of valid event types from the same source; no command's existence, argument parsing, or other output changes + - Observable: the new test cases fail against today's unordered, hand-padded `help` listing (with `info config` disconnected from its siblings) and `break`'s error message, which names no valid alternatives + - _Requirements: 7.1, 7.2, 7.3, 7.4, 7.5, 7.6_ + - _Boundary: Help & Breakpoint Discoverability_ + +- [ ] 9.2 (P) Implement the shared event-type accessor, the reorganized `help`, and `break`'s enriched error message + - Add one shared accessor rendering `BP_KIND_TABLE`'s complete token set; rewrite `repl_print_help()` into the four labeled, consistently-aligned sections with `info config` moved next to its siblings, `set units` listed alongside `set speed`/`set output`/`set verify`, and the accessor's output appended to the `break` line; call the same accessor from `repl_cmd_break()`'s unrecognized-event-type error + - Observable: all tests from 9.1 pass; `help` and `break`'s error message can never disagree with each other or with `BP_KIND_TABLE`, since both read the same accessor + - _Requirements: 7.1, 7.2, 7.3, 7.4, 7.5, 7.6_ + - _Boundary: Help & Breakpoint Discoverability_ + +- [ ] 10. Validation: regression and real-data verification — Depends: 3.2, 4.2, 6.2, 8.2, 9.2 +- [ ] 10.1 Verify single-stepping and every other existing REPL command remain unchanged outside this feature's explicit exceptions + - Confirm single-stepping's per-event/per-decision output, and the inspection/breakpoint/parameter/snapshot commands' behavior and output, match their pre-feature baseline exactly except for the prompt content (Requirement 5.5), `info`'s job-identifier/resource-value listings (Requirements 4.5, 6), and `help`/`break`'s discoverability changes (Requirement 7); confirm no new top-level command was introduced + - Observable: the existing REPL test suite's pre-existing assertions all still pass unmodified, and a step-mode transcript comparison shows no textual difference from the pre-feature baseline outside the prompt line itself + - _Requirements: 5.1, 5.2, 5.3, 5.4_ + +- [ ] 10.2 Run the full scheduler test suite and re-verify against the real replay capture that originally motivated this feature + - Observable: the full test suite passes with no regressions, and re-running the interactive REPL's continuous execution against the real capture reproduces the collapsed output shape validated during design, cutting a several-thousand-line transcript down to a handful of lines while preserving every state change; a `step`/`info worker`/`help`/`break` pass over the same capture shows the new prompt content, grouped resource table, reorganized help listing, and enriched break error, with no change to any scheduling decision + - _Requirements: 2.1, 2.2, 3.1, 3.2, 3.3, 3.4, 5.5, 6.6, 7.1, 7.2, 7.3, 7.4, 7.5_ diff --git a/src/scheduler/testsuite/meson.build b/src/scheduler/testsuite/meson.build index dac82ba9..08b91a58 100644 --- a/src/scheduler/testsuite/meson.build +++ b/src/scheduler/testsuite/meson.build @@ -974,6 +974,23 @@ sched_repl_equivalence_test = executable( dependencies: [scheduler_test_deps, libedit_dep], install: false) +# RED-phase contract test for the REPL's job-identifier range compressor +# (scheduler-sim-repl-verbosity spec, task 1.1, Requirement 4). Unlike every +# other sched_repl_*_test above, this is NOT a white-box #include of +# ../src/repl.c and friends -- repl_fmt_jobid_range() is a pure formatter +# with no xce_sched_sim_driver_t/xce_sched_core_t dependency, so this file +# declares it as a plain extern instead (see test_repl_output_compression.c's +# own header comment for the full rationale). As of task 1.1, the function +# has no definition anywhere in this codebase (task 1.2 adds it), so this +# target is EXPECTED to fail at link time -- that is task 1.1's own +# Observable evidence, not a defect in this build file. +sched_repl_output_compression_test = executable( + 'sched_repl_output_compression_test', + ['test_repl_output_compression.c'], + include_directories:[incdir], + dependencies: [scheduler_test_deps], + install: false) + # Tests for the scheduler's unified candidate-list connect/rejoin pipeline # (component-failover-adoption, task 3.1, boundary: SchedulerControllerClient). # Unlike the white-box #include-the-.c-file tests above, this target links @@ -1324,6 +1341,9 @@ test('scheduler repl snapshot params test', test('scheduler repl equivalence test', sched_repl_equivalence_test, suite: 'scheduler', env: ['ASAN_OPTIONS=detect_leaks=0']) +test('scheduler repl output compression test', + sched_repl_output_compression_test, suite: 'scheduler', + env: ['ASAN_OPTIONS=detect_leaks=0']) test('scheduler connect pipeline test', sched_scheduler_connect_test, suite: 'scheduler', env: ['ASAN_OPTIONS=detect_leaks=0']) diff --git a/src/scheduler/testsuite/test_repl_output_compression.c b/src/scheduler/testsuite/test_repl_output_compression.c new file mode 100644 index 00000000..f6ea5ff3 --- /dev/null +++ b/src/scheduler/testsuite/test_repl_output_compression.c @@ -0,0 +1,317 @@ +/* + * test_repl_output_compression.c - RED-phase contract test for the REPL's + * job-identifier range compressor (scheduler-sim-repl-verbosity spec, task + * 1.1, Requirement 4 "Job Identifier Range Compression", ACs 4.1-4.5). + * + * THIS TASK (1.1) IS RED-PHASE ONLY: `repl_fmt_jobid_range()` does not exist + * anywhere in this codebase yet -- task 1.2 (a separate, later task) + * implements it. Building this file is therefore expected to FAIL AT LINK + * TIME (undefined reference to `repl_fmt_jobid_range`), not to fail a + * runtime assertion -- that link failure IS this task's Observable evidence + * per tasks.md. Once task 1.2 lands a real definition, this same file + * (unmodified) becomes the GREEN-phase proof. + * + * Declaring the function under test: every existing test_repl_*.c file in + * this directory white-box #includes ../src/repl.c (plus repl_break.c/ + * repl_info.c/repl_params.c/core.c/sim.c/...) to reach the REPL functions it + * exercises, because those functions need a real xce_sched_sim_driver_t/ + * xce_sched_core_t fixture. repl_fmt_jobid_range() does not: per design.md's + * "Jobid Range Compressor" component, it is a pure formatter over a raw + * uint64_t jobid array with no dependency on sim/core/repl state. Pulling in + * repl.c wholesale here would ALSO drag in repl_internal.h's other extern + * cross-references (repl_cmd_break/repl_cmd_info/repl_cmd_set/..., + * implemented in sibling files this test does not include), producing a + * pile of unrelated undefined-reference errors and burying the one this + * task actually wants to prove. So this file declares the function locally + * as a plain `extern` instead -- once task 1.2 gives it a shared + * declaration in repl_internal.h (needed at that point because Round Rollup + * and repl_info.c's `info worker` will call it from other files), this + * local prototype becomes redundant but harmless (identical signature). + * + * Jobid fixtures: real, correctly bit-packed jobids built via this + * codebase's own MAKE_JOBID/TAG_JOB_ARRAY/SEQUENCE_HIGH_SHIFT macros + * (src/include/config.h, src/include/utils.h) -- the SAME encoding + * fmt_jobid_ex() (src/lib/utils.c) decodes: a 64-bit jobid packs + * (cluster:6 | apiserver:4 | timestamp:32 | tag:2 | sequence:20); for an + * array-tagged jobid (tag=TAG_JOB_ARRAY), the 20-bit sequence field splits + * further into a 5-bit "array base" (bits 15-19, EXTRACT_ARRAY_JOB) and a + * 15-bit "array index" (bits 0-14, EXTRACT_ARRAY_IDX) -- mirrored here by + * mk_array_jobid()'s `(array_base << SEQUENCE_HIGH_SHIFT) | idx` construction + * (the same shape idgen.c's own idgen() builds for a real array submission). + * A non-array jobid (tag=0) uses the full 20-bit sequence field as one plain + * value (mk_plain_jobid()). + * + * Assumed output contract (design.md's Service Interface documents the + * grouping/compression algorithm and postconditions, but not a byte-exact + * example -- this header comment fixes the literal format these tests pin, + * for task 1.2 to satisfy): + * - Groups (one per distinct (cluster,apiserver,timestamp,array-base) + * array submission, or one per standalone non-array jobid) are + * comma-joined ("," bare, no space -- matching this file family's own + * existing comma-list convention: repl_break.c's JOB_PUSH jobid listing + * and info-break filter listing, repl_info.c's repl_resource_flags_str()), + * in the input order each group is FIRST seen (design.md's own + * "distinct array bases appear in input order" postcondition). + * - A non-array jobid renders as "---" + * -- fmt_jobid_ex()'s own non-array format, verbatim. + * - An array submission's compressed group renders as + * "---[,,...]", + * each run being "-" for a contiguous run of >= 2 indices or a + * bare "" for a run of exactly 1, runs ascending and comma-joined + * inside the brackets (design.md: "compresses contiguous runs into + * [lo-hi] form, comma-joining non-contiguous runs WITHIN ONE BASE"). + * - Buffer-too-small (Error Handling section): returns a negative value, + * and writes NOTHING into the caller's buf -- mirroring fmt_jobid_ex()'s + * own established pattern in this exact codebase (src/lib/utils.c): + * build the full rendering into a local scratch buffer first, and only + * ever touch the caller's buf if the complete result is proven to fit; + * "never truncates silently" (design.md's Error Handling section) rules + * out writing any partial/truncated prefix into the caller's buffer. + */ +#include +#include +#include +#include + +#include + +#include "utils.h" /* MAKE_JOBID, TAG_JOB_ARRAY, SEQUENCE_HIGH_SHIFT (via config.h) */ + +/* + * repl_fmt_jobid_range (task 1.2, NOT YET IMPLEMENTED -- see this file's own + * header comment above for why this is a local extern rather than a + * white-box #include). Signature copied verbatim from design.md's "Jobid + * Range Compressor" Service Interface. + */ +extern int repl_fmt_jobid_range(char* buf, size_t bufsz, + const uint64_t* jobids, int njobids); + +void setUp(void) { return; } +void tearDown(void) { return; } + +/* ---------------------------------------------------------------------- */ +/* Fixture helpers -- real, correctly bit-packed jobids (see header */ +/* comment above for the exact encoding each mirrors). */ +/* ---------------------------------------------------------------------- */ + +static uint64_t mk_array_jobid(uint32_t cluster, uint32_t apiserver, + uint32_t timestamp, uint32_t array_base, + uint32_t idx) +{ + uint64_t seq = (((uint64_t)array_base) << SEQUENCE_HIGH_SHIFT) + | (uint64_t)idx; + + return MAKE_JOBID(cluster, apiserver, timestamp, TAG_JOB_ARRAY, seq); +} + +static uint64_t mk_plain_jobid(uint32_t cluster, uint32_t apiserver, + uint32_t timestamp, uint32_t seq) +{ + return MAKE_JOBID(cluster, apiserver, timestamp, 0x0, (uint64_t)seq); +} + +/* ---------------------------------------------------------------------- */ +/* Test 1 (AC 4.1): a single contiguous range from one array submission. */ +/* ---------------------------------------------------------------------- */ + +/* + * Three array jobids sharing one submission (cluster=3, apiserver=2, + * ts=1700000000, array base=1 -> job field 1<<15=32768), indices 0,1,2 + * contiguous, must compress into a single "[0-2]" range on that one base + * prefix -- not three separate entries. + */ +static void test_contiguous_array_range_compresses_to_single_range(void) +{ + uint64_t jobids[3]; + char buf[256]; + int rc; + + jobids[0] = mk_array_jobid(3, 2, 1700000000, 1, 0); + jobids[1] = mk_array_jobid(3, 2, 1700000000, 1, 1); + jobids[2] = mk_array_jobid(3, 2, 1700000000, 1, 2); + + memset(buf, 0, sizeof(buf)); + rc = repl_fmt_jobid_range(buf, sizeof(buf), jobids, 3); + + TEST_ASSERT_EQUAL_STRING("3-2-1700000000-32768[0-2]", buf); + TEST_ASSERT_EQUAL_INT((int)strlen("3-2-1700000000-32768[0-2]"), rc); + + printf("test_contiguous_array_range_compresses_to_single_range: PASS\n"); +} + +/* ---------------------------------------------------------------------- */ +/* Test 2 (AC 4.2): non-contiguous indices within one array submission. */ +/* ---------------------------------------------------------------------- */ + +/* + * One array submission (same base as Test 1) with indices 0,1,2,5,6,7,10 -- + * three contiguous sub-runs ([0-2], [5-7]) plus one lone index (10) -- must + * render as comma-joined sub-ranges INSIDE that one base's brackets, not as + * three/four separately-prefixed base entries. + */ +static void test_noncontiguous_indices_compress_to_commajoined_subranges( + void) +{ + uint64_t jobids[7]; + char buf[256]; + int rc; + + jobids[0] = mk_array_jobid(3, 2, 1700000000, 1, 0); + jobids[1] = mk_array_jobid(3, 2, 1700000000, 1, 1); + jobids[2] = mk_array_jobid(3, 2, 1700000000, 1, 2); + jobids[3] = mk_array_jobid(3, 2, 1700000000, 1, 5); + jobids[4] = mk_array_jobid(3, 2, 1700000000, 1, 6); + jobids[5] = mk_array_jobid(3, 2, 1700000000, 1, 7); + jobids[6] = mk_array_jobid(3, 2, 1700000000, 1, 10); + + memset(buf, 0, sizeof(buf)); + rc = repl_fmt_jobid_range(buf, sizeof(buf), jobids, 7); + + TEST_ASSERT_EQUAL_STRING("3-2-1700000000-32768[0-2,5-7,10]", buf); + TEST_ASSERT_EQUAL_INT( + (int)strlen("3-2-1700000000-32768[0-2,5-7,10]"), rc); + + printf("test_noncontiguous_indices_compress_to_commajoined_subranges: " + "PASS\n"); +} + +/* ---------------------------------------------------------------------- */ +/* Test 3 (AC 4.3): more than one distinct array submission in one listing.*/ +/* ---------------------------------------------------------------------- */ + +/* + * Two distinct array submissions -- base A (ts=1700000000) and base B + * (ts=1700000500, deliberately sharing the SAME numeric array-base value 1 + * as base A, so this only passes if grouping honors the full + * (cluster,apiserver,timestamp,base) key, not just the array-base number) + * -- interleaved in the input array (A0,B0,A1,B1,A2) to prove grouping does + * not depend on adjacency. Each submission's indices must compress + * independently, and the two groups must appear in first-seen order (A + * first, since jobids[0] is A's first index). + */ +static void test_multiple_array_submissions_compress_independently(void) +{ + uint64_t jobids[5]; + char buf[256]; + int rc; + + jobids[0] = mk_array_jobid(3, 2, 1700000000, 1, 0); /* A0 */ + jobids[1] = mk_array_jobid(3, 2, 1700000500, 1, 0); /* B0 */ + jobids[2] = mk_array_jobid(3, 2, 1700000000, 1, 1); /* A1 */ + jobids[3] = mk_array_jobid(3, 2, 1700000500, 1, 1); /* B1 */ + jobids[4] = mk_array_jobid(3, 2, 1700000000, 1, 2); /* A2 */ + + memset(buf, 0, sizeof(buf)); + rc = repl_fmt_jobid_range(buf, sizeof(buf), jobids, 5); + + TEST_ASSERT_EQUAL_STRING( + "3-2-1700000000-32768[0-2],3-2-1700000500-32768[0-1]", buf); + TEST_ASSERT_EQUAL_INT( + (int)strlen( + "3-2-1700000000-32768[0-2],3-2-1700000500-32768[0-1]"), + rc); + + printf( + "test_multiple_array_submissions_compress_independently: PASS\n"); +} + +/* ---------------------------------------------------------------------- */ +/* Test 4 (AC 4.4): a listing mixing array and non-array job identifiers. */ +/* ---------------------------------------------------------------------- */ + +/* + * A listing interleaving two standalone (non-array) jobids around one + * array submission's indices: plain1, arr0, arr1, plain2, arr2. The two + * plain ids must each render individually at their own first-seen listing + * position, while the array indices -- though non-adjacent in the input -- + * still collapse into one compressed "[0-2]" group at ITS first-seen + * position (right after plain1). + */ +static void test_mixed_array_and_nonarray_ids_shows_nonarray_individually( + void) +{ + uint64_t jobids[5]; + char buf[256]; + int rc; + + jobids[0] = mk_plain_jobid(3, 2, 1700000000, 10); /* plain1 */ + jobids[1] = mk_array_jobid(3, 2, 1700000000, 1, 0); /* arr0 */ + jobids[2] = mk_array_jobid(3, 2, 1700000000, 1, 1); /* arr1 */ + jobids[3] = mk_plain_jobid(3, 2, 1700000000, 20); /* plain2 */ + jobids[4] = mk_array_jobid(3, 2, 1700000000, 1, 2); /* arr2 */ + + memset(buf, 0, sizeof(buf)); + rc = repl_fmt_jobid_range(buf, sizeof(buf), jobids, 5); + + TEST_ASSERT_EQUAL_STRING( + "3-2-1700000000-10,3-2-1700000000-32768[0-2],3-2-1700000000-20", + buf); + TEST_ASSERT_EQUAL_INT( + (int)strlen("3-2-1700000000-10,3-2-1700000000-32768[0-2]," + "3-2-1700000000-20"), + rc); + + printf( + "test_mixed_array_and_nonarray_ids_shows_nonarray_individually: " + "PASS\n"); +} + +/* ---------------------------------------------------------------------- */ +/* Test 5 (AC 4.5 / design.md Error Handling): buffer too small to hold */ +/* the full listing. */ +/* ---------------------------------------------------------------------- */ + +/* + * The same three-jobid contiguous-range fixture as Test 1 (whose full + * rendering is 26 bytes, "3-2-1700000000-32768[0-2]" + NUL) passed with a + * caller bufsz of 4 -- far too small. Must fail safely: a negative return, + * and NOT ONE byte of the caller's buffer touched (see this file's header + * comment for why "never truncates silently" is read as "writes nothing on + * failure", mirroring fmt_jobid_ex()'s own local-scratch-buffer-first + * pattern in this codebase). buf is allocated larger (64 bytes) than the + * bufsz actually passed (4) specifically so a bug that ignores bufsz and + * writes into the full physical buffer is caught too, not just a bug that + * overflows past the physical allocation. + */ +static void test_buffer_too_small_fails_safely_without_truncating(void) +{ + uint64_t jobids[3]; + char buf[64]; + char sentinel[64]; + int rc; + + jobids[0] = mk_array_jobid(3, 2, 1700000000, 1, 0); + jobids[1] = mk_array_jobid(3, 2, 1700000000, 1, 1); + jobids[2] = mk_array_jobid(3, 2, 1700000000, 1, 2); + + memset(buf, 0xAA, sizeof(buf)); + memset(sentinel, 0xAA, sizeof(sentinel)); + + rc = repl_fmt_jobid_range(buf, 4, jobids, 3); + + TEST_ASSERT_TRUE_MESSAGE(rc < 0, + "a buffer too small for the full listing must " + "return a negative value, never a truncated " + "success"); + TEST_ASSERT_EQUAL_MEMORY_MESSAGE( + sentinel, buf, sizeof(buf), + "on failure, no byte of the caller's buffer may be written -- " + "silent truncation is explicitly disallowed (design.md Error " + "Handling)"); + + printf( + "test_buffer_too_small_fails_safely_without_truncating: PASS\n"); +} + +int main(void) +{ + UNITY_BEGIN(); + + RUN_TEST(test_contiguous_array_range_compresses_to_single_range); + RUN_TEST(test_noncontiguous_indices_compress_to_commajoined_subranges); + RUN_TEST(test_multiple_array_submissions_compress_independently); + RUN_TEST(test_mixed_array_and_nonarray_ids_shows_nonarray_individually); + RUN_TEST(test_buffer_too_small_fails_safely_without_truncating); + + return UNITY_END(); +}