diff --git a/.kiro/specs/job-array-single-wal-record/tasks.md b/.kiro/specs/job-array-single-wal-record/tasks.md index ce2c106f..c29fc39b 100644 --- a/.kiro/specs/job-array-single-wal-record/tasks.md +++ b/.kiro/specs/job-array-single-wal-record/tasks.md @@ -73,7 +73,7 @@ - _Requirements: 3.1, 3.2, 4.1 — delivered by task 3.2 alone (see Implementation Notes); this task is defensive parity, not an independent delivery path_ - _Boundary: Array Apply Expansion_ -- [ ] 3.4 Unit tests for array expansion +- [x] 3.4 Unit tests for array expansion - Verify N-way expansion produces the correct contiguous set of element identities for several array sizes - Verify idempotent re-apply of an already-fully-applied record is a no-op that creates nothing new - Verify, using 3.1's forceable failure point, that a failure partway through leaves nothing visible, and that a subsequent successful retry produces the complete, non-duplicated set diff --git a/src/controller/testsuite/test_job_array_apply.c b/src/controller/testsuite/test_job_array_apply.c index 2ea94c34..0505446a 100644 --- a/src/controller/testsuite/test_job_array_apply.c +++ b/src/controller/testsuite/test_job_array_apply.c @@ -47,6 +47,7 @@ #include #include +#include #include #include #include @@ -235,6 +236,79 @@ static void test_rja_expands_contiguous_set_and_tags_lsn(void) printf("test_rja_expands_contiguous_set_and_tags_lsn: PASS\n"); } +/* ── N-way expansion across SEVERAL array sizes (task 3.4 gap-closer) ─── + * The test above only proves correctness at ONE size (5). Task 3.4's own + * observable explicitly asks for "several array sizes" -- mirroring the + * WAL-append/commit-continuation size sweep task 2.3 already added + * (test_job_array_submit_one_entry_one_continuation_across_sizes, + * test_commit_barrier_rollout.c) for the encode/commit side. This is the + * expansion-side analog: same size set {2, 3, 100, 1000}, proving the + * CONTIGUOUS jobid set (and its exact endpoints -- no off-by-one leakage + * into an adjacent, never-submitted jobid) holds at small, medium, and + * large array sizes, not just one. */ +static void test_rja_expands_contiguous_set_across_sizes(void) +{ + static const uint64_t sizes[] = { 2, 3, 100, 1000 }; + size_t idx; + + taj_setup(); + + for (idx = 0; idx < sizeof(sizes) / sizeof(sizes[0]); idx++) { + const uint64_t array_size = sizes[idx]; + const uint64_t base_jobid = 90000 + (uint64_t)idx * 2000000; + const uint64_t entry_lsn = 900 + (uint64_t)idx; + job_submit_data_t spec = taj_make_spec(); + uint8_t* payload = NULL; + size_t len = 0; + char msg[160]; + + TEST_ASSERT_EQUAL_INT(0, + taj_encode_job_submit(base_jobid, array_size, &spec, &payload, + &len)); + + xlist_t out; + memset(&out, 0, sizeof(out)); + int rc = restore_job_array(&g_taj_c, payload, len, entry_lsn, &out); + + snprintf(msg, sizeof(msg), + "array_size=%llu: must report array_size newly created elements", + (unsigned long long)array_size); + TEST_ASSERT_EQUAL_INT_MESSAGE((int)array_size, rc, msg); + + snprintf(msg, sizeof(msg), + "array_size=%llu: out_new_jobs count must equal array_size", + (unsigned long long)array_size); + TEST_ASSERT_EQUAL_INT_MESSAGE((int)array_size, out.count, msg); + + for (uint64_t i = 0; i < array_size; i++) { + job_t* j = taj_find_job(base_jobid + i); + snprintf(msg, sizeof(msg), + "array_size=%llu index=%llu: element must be visible", + (unsigned long long)array_size, (unsigned long long)i); + TEST_ASSERT_NOT_NULL_MESSAGE(j, msg); + TEST_ASSERT_EQUAL_UINT64(base_jobid + i, j->job.jobid); + TEST_ASSERT_EQUAL_UINT64(entry_lsn, j->submit_lsn); + } + + /* Exact endpoints -- no off-by-one leakage into an adjacent, + * never-submitted jobid on either side of the range. */ + snprintf(msg, sizeof(msg), + "array_size=%llu: one-below-base must not exist", + (unsigned long long)array_size); + TEST_ASSERT_NULL_MESSAGE(taj_find_job(base_jobid - 1), msg); + snprintf(msg, sizeof(msg), + "array_size=%llu: one-past-the-end must not exist", + (unsigned long long)array_size); + TEST_ASSERT_NULL_MESSAGE(taj_find_job(base_jobid + array_size), msg); + + xlist_fini(&out); + free(payload); + } + + taj_teardown(); + printf("test_rja_expands_contiguous_set_across_sizes: PASS\n"); +} + /* ── Idempotent whole-record re-apply is a no-op (Req 3.3) ─────────────── */ static void test_rja_idempotent_whole_record_replay_is_noop(void) @@ -564,6 +638,87 @@ static job_t* tajd_find_job(cluster_t* c, uint64_t jobid) return (found > 0) ? j : NULL; } +/* ── job_ctrl() addresses an array-expanded element by its per-element + * identity, exactly as it does for a single-submitted job (Req 5.2, task + * 3.4 gap-closer) ─────────────────────────────────────────────────────── + * Every test above only proves an array element is FOUND via the raw + * key_find(c->jobmgr, jobid) primitive -- the SAME primitive job_ctrl() + * itself uses internally (job.c:880) to resolve a real client cancel/ + * signal request. This test drives job_ctrl() itself (the real production + * command entry point, not a bypass) against ONE array-expanded jobid, to + * prove Requirement 5.2's "querying, cancelling ... exactly as it does for + * job elements created via the current per-element path" holds for the + * actual command path -- not only for the lookup primitive it happens to + * share with every other test's assertions. */ +static void test_tajd_job_ctrl_cancels_array_expanded_element_by_identity(void) +{ + tajd_setup(); + + const uint64_t base_jobid = 95000; + const uint64_t array_size = 4; + job_submit_data_t spec = taj_make_spec(); + uint8_t* payload = NULL; + size_t len = 0; + + TEST_ASSERT_EQUAL_INT(0, + taj_encode_job_submit(base_jobid, array_size, &spec, &payload, &len)); + + xlist_t out; + memset(&out, 0, sizeof(out)); + int rc = restore_job_array(&g_tajd_c, payload, len, 500, &out); + TEST_ASSERT_EQUAL_INT((int)array_size, rc); + xlist_fini(&out); + free(payload); + + /* Cancel exactly ONE element (index 2 of 4) via the real command entry + * point job_ctrl() -- the same function a client cancel/signal request + * dispatches through for a single-submitted job. */ + xce_signal_t sig = { .signo = SIGKILL, .force = 0 }; + xce_jobctrl_t ctrinfo = { + .action = JobControl_u_siginfo, + .jobid = base_jobid + 2, + .u = { .siginfo = &sig }, + }; + + int ctrl_rc = job_ctrl(&g_tajd_c, &ctrinfo); + TEST_ASSERT_EQUAL_INT_MESSAGE(0, ctrl_rc, + "job_ctrl must resolve an array-expanded jobid exactly as it does" + " for a single-submitted job"); + + job_t* cancelled = tajd_find_job(&g_tajd_c, base_jobid + 2); + TEST_ASSERT_NOT_NULL(cancelled); + TEST_ASSERT_EQUAL_UINT32_MESSAGE(JOB_STAT_EXIT, cancelled->job.state, + "the targeted array-expanded element must transition to EXIT via" + " job_ctrl's real not-yet-started-job SIGKILL path"); + + /* Every OTHER array-expanded element must be untouched -- proving + * job_ctrl addresses exactly the requested per-element identity, not + * the whole array. */ + for (uint64_t i = 0; i < array_size; i++) { + if (i == 2) + continue; + job_t* j = tajd_find_job(&g_tajd_c, base_jobid + i); + TEST_ASSERT_NOT_NULL(j); + TEST_ASSERT_EQUAL_UINT32_MESSAGE(JOB_STAT_PEND, j->job.state, + "an untargeted sibling array element must be unaffected by a" + " job_ctrl call against a different index"); + } + + /* A jobid one past the array's committed range must not resolve. */ + xce_jobctrl_t missing_ctrinfo = { + .action = JobControl_u_siginfo, + .jobid = base_jobid + array_size, + .u = { .siginfo = &sig }, + }; + TEST_ASSERT_EQUAL_INT_MESSAGE(-ENOENT, + job_ctrl(&g_tajd_c, &missing_ctrinfo), + "a jobid one past the array's committed range must not resolve"); + + tajd_teardown(); + printf("test_tajd_job_ctrl_cancels_array_expanded_element_by_identity:" + " PASS\n"); +} + /* apply_wal_entry()'s WAL_JOB_ARRAY_SUBMIT case constructs every element * through restore_job_array() and fires NO notification -- mirroring the * existing WAL_JOB_SUBMIT case's hook-free behavior at this same dispatch @@ -707,6 +862,7 @@ void test_job_array_apply(void) { UNITY_BEGIN(); RUN_TEST(test_rja_expands_contiguous_set_and_tags_lsn); + RUN_TEST(test_rja_expands_contiguous_set_across_sizes); RUN_TEST(test_rja_idempotent_whole_record_replay_is_noop); RUN_TEST(test_rja_partial_reapply_skips_existing_indices); #ifdef XCE_TESTING @@ -715,6 +871,7 @@ void test_job_array_apply(void) #endif RUN_TEST(test_rja_null_or_empty_payload_returns_error); RUN_TEST(test_rja_out_new_jobs_is_optional); + RUN_TEST(test_tajd_job_ctrl_cancels_array_expanded_element_by_identity); RUN_TEST(test_tajd_apply_wal_entry_constructs_all_and_fires_no_hook); RUN_TEST(test_tajd_apply_committed_entry_vs_apply_wal_entry_byte_identical); UNITY_END();