/* * 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(); }