test(scheduler-dispatch-commit): terminal dispatch failure e2e coverage (task 5.3)

Adds test_handle_jobdispatch_terminal_no_notify_no_leak to
test_jobdispatch_no_double_notify.c, closing a gap neither existing
test fully covered: test_dispatch_fail_terminal.c (task 3.2) bypasses
handle_jobdispatch() entirely via dispatch_a_job_test(), so it can't
exercise the double-notify race at all; test_jobdispatch_no_double_notify.c
(task 4.1) only proves an aggregate 3-job-batch notify count, not a
direct per-job zero-notify proof.

The new test drives a single terminal-classified job through the real
handle_jobdispatch() entry point and asserts, in one run: JOB_STAT_EXIT
with allocs left NULL, a direct (non-aggregate) zero-count read from
the notify-capture spy proving no jobDispatchFail was produced for this
job, and (via task 3.2's canary-reference technique, armed before the
call under test) no reference over-release.

Test-only; job.c/handler.c are untouched. RED phase verified via an
actual revert-rebuild-rerun of handle_jobdispatch() to its pre-task-4.1
shape. Independently reviewed and approved -- the reviewer notably
declined to trust a relayed build report and re-ran verification
first-hand before approving.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cxa1PmqAB5PyXfJaNvj8TN
This commit is contained in:
Rongsong Shen 2026-08-23 20:47:15 +00:00
parent 16d90319db
commit bea956997a
No known key found for this signature in database
GPG key ID: 5BCDAAE0123E9FE2
2 changed files with 127 additions and 1 deletions

View file

@ -46,6 +46,21 @@
* for the full rationale -- this binary's sole notify() definition lives in
* test_commit_barrier_reply.c, exposed read-only via its
* cbrp_notify_capture_*_ext() accessors (added by task 3.3).
*
* scheduler-dispatch-commit task 5.3 (Req 4.2, 4.5) adds
* test_handle_jobdispatch_terminal_no_notify_no_leak, below. It is a
* sharper-focused sibling of this file's original (task 4.1) 3-job-batch
* test: that test proves the BATCH's total notify_dispatch() count is 1,
* attributable to the retryable job (an aggregate proof the terminal job
* contributed zero calls only by subtraction/arithmetic against the other
* two jobs' known outcomes) and never exercises reference-count cleanliness
* for any job. The new test instead drives a SINGLE terminal-classified job
* through this same real handle_jobdispatch() entry point (not
* dispatch_a_job_test()'s bypass, unlike test_dispatch_fail_terminal.c's
* task-3.2 tests) and combines, in ONE test run: (a) JOB_STAT_EXIT
* resolution, (b) a direct (not subtracted) zero-notify assertion for this
* job's own batch, and (c) test_dispatch_fail_terminal.c's canary-reference
* technique proving the reference count returns to its pre-attempt value.
*/
#include <rtti.h>
@ -347,6 +362,115 @@ static void test_handle_jobdispatch_matches_dispatch_a_job_notify_count(void)
"PASS\n");
}
#define JND_JOBID_TERMINAL_SOLO 92201ULL
/* Canary-reference technique, reused verbatim from
* test_dispatch_fail_terminal.c's dft_arm_ref_canary()/dft_assert_ref_
* not_over_released() (task 3.2): armed immediately BEFORE the call under
* test (not after -- arming after would rely incidentally on other
* assertions crashing first, which doesn't independently prove anything),
* released and the job re-touched immediately after. An over-release during
* the call would free j while the canary reference is still held, making
* the post-call touch a real, ASAN-caught heap-use-after-free. job_t's real
* refcount lives in jobj.c's file-local job_private_t with no accessor
* exposed to a test, so this indirect technique is the only available
* proof. */
static void jnd_arm_ref_canary(job_t* j)
{
reference(j);
}
static void jnd_assert_ref_not_over_released(job_t* j, uint64_t expect_jobid)
{
dereference(j);
TEST_ASSERT_EQUAL_UINT64_MESSAGE(expect_jobid, j->job.jobid,
"Req 4.5: j must still be safely readable after releasing the "
"canary reference -- an over-release inside "
"handle_jobdispatch()/dispatch_a_job_() would have freed j while "
"the canary was still held, making this a heap-use-after-free "
"(this terminal site already dereferences correctly even before "
"bug-1627's fix, which was scoped to the four retryable sites -- "
"this check guards the same class of risk, not that specific bug)");
}
/* Task 5.3's focused proof (Req 4.2, 4.5) -- see this file's header for how
* this differs from test_handle_jobdispatch_matches_dispatch_a_job_notify_
* count above. A single-job batch, terminal-classified (nhosts == 0),
* driven through the real handle_jobdispatch() entry point. */
static void test_handle_jobdispatch_terminal_no_notify_no_leak(void)
{
xce_job_alloc_t* allocs[1];
xce_job_dispatch_t dispatchmsg;
xce_request_t req;
job_t* jterm;
int rc;
jnd_setup();
jterm = jnd_seed_job(JND_JOBID_TERMINAL_SOLO, "jnd-q-solo");
/* nhosts == 0: XCE_DISPATCH_FAIL_SITE_INVALID_ALLOC, terminal --
* mirrors this file's own JND_JOBID_TERMINAL allocation and
* test_dispatch_fail_terminal.c's test_invalid_alloc_resolves_job_
* to_exit, but reached this time through the real handle_jobdispatch()
* entry point instead of dispatch_a_job_test()'s bypass. */
allocs[0] = make_job_alloc(JND_JOBID_TERMINAL_SOLO, 0, NULL);
TEST_ASSERT_NOT_NULL(allocs[0]);
memset(&dispatchmsg, 0, sizeof(dispatchmsg));
dispatchmsg.njobs = 1;
dispatchmsg.allocs = allocs;
memset(&req, 0, sizeof(req));
req.opcode = OPCode_jobdispatch;
req.seq = 43;
req.from = &g_jnd_req_from;
req.body_which = Request_body_dispatchmsg;
req.body.dispatchmsg = &dispatchmsg;
cbrp_notify_capture_reset_ext();
jnd_arm_ref_canary(jterm);
/* Act: handle_jobdispatch() consumes/frees allocs[0] itself via
* MOVE_PTR + free_JobAlloc_ptr() in its own loop -- nothing left for
* this test to free afterward. */
rc = handle_jobdispatch(&g_jnd, &g_jnd_req_ep, &req);
TEST_ASSERT_EQUAL_INT_MESSAGE(0, rc, "handle_jobdispatch's own return "
"is unconditionally 0, unaffected by any per-job outcome");
TEST_ASSERT_EQUAL_UINT32_MESSAGE(JOB_STAT_EXIT, jterm->job.state,
"Req 4.2: a terminally-classified dispatch failure must resolve "
"the job to EXIT, reached this time through the real "
"handle_jobdispatch() entry point rather than the "
"dispatch_a_job_test() bypass test_dispatch_fail_terminal.c uses");
TEST_ASSERT_NULL_MESSAGE(jterm->job.allocs,
"Req 4.5: no partial allocation data may be left behind");
/* THE assertion this task exists for: a terminal dispatch failure must
* not ALSO produce a retry-eligible jobDispatchFail report for the same
* failure -- mark_job_fail()'s own schedFailJob hook chain (already
* fired above) is the sole notification. Unlike test_handle_jobdispatch_
* matches_dispatch_a_job_notify_count's 3-job batch (where the terminal
* job's zero contribution is inferred by the total equaling exactly the
* retryable job's own 1 call), this single-job batch makes the zero
* count direct: any notify_dispatch() call surfacing here can only have
* come from this one terminal job, whether from handle_jobdispatch()'s
* old per-job loop call (task 4.1's regression) or from dispatch_a_
* job_() itself misclassifying/misrouting (task 3.2's regression). */
TEST_ASSERT_EQUAL_INT_MESSAGE(0, cbrp_notify_capture_count_ext(),
"a terminal dispatch failure must not also produce a "
"notify_dispatch() report for this same failure (regression test "
"for the round-2 double-notify finding, design.md's Integration "
"Tests third bullet)");
jnd_assert_ref_not_over_released(jterm, JND_JOBID_TERMINAL_SOLO);
jnd_teardown();
printf("test_handle_jobdispatch_terminal_no_notify_no_leak: PASS\n");
}
void test_jobdispatch_no_double_notify(void)
{
UnitySetTestFile(__FILE__);
@ -354,4 +478,5 @@ void test_jobdispatch_no_double_notify(void)
"4.2)\n\n");
RUN_TEST(test_handle_jobdispatch_matches_dispatch_a_job_notify_count);
RUN_TEST(test_handle_jobdispatch_terminal_no_notify_no_leak);
}