test(job-array-single-wal-record): close remaining array expansion gaps

Adds 2 targeted tests after confirming tasks 3.1/3.2/3.3 already
cover most of this task's observable criteria:
- N-way expansion swept across sizes {2,3,100,1000} with exact-
  endpoint non-leakage checks (existing coverage only tested size 5)
- Requirement 5.2 (individually addressable exactly as a per-element
  job) exercised through the real production command dispatch
  (job_ctrl(), not the raw key_find() every existing test used) --
  confirms an array-expanded element can be cancelled by identity,
  siblings stay untouched, and an out-of-range jobid is rejected

Idempotent no-op, forced-failure-then-retry, and partial-reapply
were already fully covered by 3.1/3.2 -- no new tests needed.
Requirement 9.4's node-fencing behavior is confirmed out of this
task's boundary (Live Apply Resource Fence, task 4.x); restore_job_
array()'s own atomicity contract that the fence depends on is
already proven. No production code changed -- test-only.

Task 3.4 of job-array-single-wal-record.
This commit is contained in:
Rongsong Shen 2026-07-15 02:56:20 +00:00
parent b0c7cfe548
commit 107e3839d9
2 changed files with 158 additions and 1 deletions

View file

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

View file

@ -47,6 +47,7 @@
#include <capnp_c.h>
#include <errno.h>
#include <signal.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stdint.h>
@ -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();