/* test_apply_seam.c — raft-apply-decoupling task 2.1 * * Proves the guarded state-transition cores extracted from mark_job_start/ * finish/fail/update (job.c) are directly callable WITHOUT going through the * worker-report receipt path (on_job_start/on_job_finish/on_job_fail) and * reproduce the exact same guard behavior mark_job_* had before extraction: * - reject a backward transition (terminal -> non-terminal), state * preserved (Req 5.2 guard, START/UPDATE) * - no-op a duplicate terminal transition, state preserved (Req 5.1 guard, * FINISH/UPDATE) * - apply a forward transition normally, including a forward merge onto an * already-terminal job (postdone-style OR, and the FAIL PERR-merge, * neither of which the guard may over-reject) * * Fixture note: apply_job_*_transition() take only a job_t* plus the fact's * payload struct — never a cluster_t* — because task 2.1 decouples hook- * firing (the only thing that needed cluster_t->hooksys) out of these cores * entirely. So the fixture here needs just a bare jobmgr + job_t, none of * test_commit_barrier_reply.c's full cluster_t/persist/repl harness. */ #include #include #include #include #include #include #include "cluster.h" #include "jobj.h" #include "xce/types.h" #include "xstring.h" /* jobj.c's jobsub/startinfo tables and the process-global xstring table are * shared across every test file in the controller_unit_test binary — guard * idempotently so this file runs standalone or alongside the others. */ static int g_tas_jobobj_ready; static var g_tas_jobmgr; static job_submit_data_t g_tas_submit; /* Bare jobmgr fixture: no cluster_t, no hooksys, no persist — the extracted * apply_job_*_transition() cores need none of that. */ static void tas_setup(void) { kvmgr_params_t int_params = { .key_type = KVMGR_KEY_INT, .nslots = 16, .delta = 16 }; if (makeString((char*)"tas-probe") == NULL) xstring_table_init(64); if (!g_tas_jobobj_ready) { TEST_ASSERT_EQUAL_INT(0, job_obj_init(16, 16)); g_tas_jobobj_ready = 1; } g_tas_jobmgr = makeInstance(&xkvmgr_type, &int_params); TEST_ASSERT_NOT_NULL(g_tas_jobmgr); } static void tas_teardown(void) { if (g_tas_jobmgr != NULL) { dereference(g_tas_jobmgr); g_tas_jobmgr = NULL; } } /* Creates a job directly in the bare jobmgr at the given initial state, * bypassing handle_jobsubmit/create_job/on_job_* entirely. */ static job_t* tas_make_job(uint64_t jobid, uint32_t state) { xce_job_t jdef; memset(&g_tas_submit, 0, sizeof(g_tas_submit)); g_tas_submit.queue = (char*)"q"; g_tas_submit.user = (char*)"tester"; memset(&jdef, 0, sizeof(jdef)); jdef.jobid = jobid; jdef.state = state; jdef.submit = &g_tas_submit; return makeJob(g_tas_jobmgr, &jdef); } /* ── apply_job_start_transition ──────────────────────────────────────── */ static void test_apply_start_transition_applies_forward(void) { static unsigned int pids[2] = { 111, 112 }; job_pidinfo_t info; job_t* j; job_transition_result_t rc; tas_setup(); j = tas_make_job(1, JOB_STAT_PEND); TEST_ASSERT_NOT_NULL(j); memset(&info, 0, sizeof(info)); info.jobid = 1; info.npids = 2; info.pids = pids; /* Act directly on the job_t — no cluster_t, no on_job_start, no jobmgr * lookup/lock dance. */ rc = apply_job_start_transition(j, &info); TEST_ASSERT_EQUAL_INT_MESSAGE(JOB_TRANSITION_APPLIED, rc, "a forward START on a non-terminal job must apply"); TEST_ASSERT_EQUAL_UINT32(JOB_STAT_RUN, j->job.state); TEST_ASSERT_NOT_NULL(j->job.pidinfo); TEST_ASSERT_EQUAL_INT(2, j->job.pidinfo->npids); tas_teardown(); printf("test_apply_start_transition_applies_forward: PASS\n"); } static void test_apply_start_transition_rejects_backward_on_terminal(void) { job_pidinfo_t info; job_t* j; job_transition_result_t rc; tas_setup(); j = tas_make_job(2, JOB_STAT_DONE); TEST_ASSERT_NOT_NULL(j); memset(&info, 0, sizeof(info)); info.jobid = 2; rc = apply_job_start_transition(j, &info); TEST_ASSERT_EQUAL_INT_MESSAGE(JOB_TRANSITION_REJECTED_BACKWARD, rc, "a START on an already-terminal job must be rejected as backward " "(Req 5.2)"); TEST_ASSERT_EQUAL_UINT32_MESSAGE(JOB_STAT_DONE, j->job.state, "a rejected backward START must preserve the terminal state"); TEST_ASSERT_NULL_MESSAGE(j->job.pidinfo, "a rejected backward START must perform no mutation at all"); tas_teardown(); printf("test_apply_start_transition_rejects_backward_on_terminal: PASS\n"); } /* ── apply_job_finish_transition ─────────────────────────────────────── */ static void test_apply_finish_transition_applies_forward(void) { xce_jobfinish_t fin; job_t* j; job_transition_result_t rc; tas_setup(); j = tas_make_job(3, JOB_STAT_RUN); TEST_ASSERT_NOT_NULL(j); j->job.start_time = time(NULL) - 5; memset(&fin, 0, sizeof(fin)); fin.jobid = 3; fin.exit_code = 0; rc = apply_job_finish_transition(j, &fin); TEST_ASSERT_EQUAL_INT_MESSAGE(JOB_TRANSITION_APPLIED, rc, "a forward FINISH on a running job must apply"); TEST_ASSERT_EQUAL_UINT32(JOB_STAT_DONE, j->job.state); TEST_ASSERT_NOT_NULL(j->job.exit_info); TEST_ASSERT_EQUAL_INT(0, j->job.exit_info->exitcode); tas_teardown(); printf("test_apply_finish_transition_applies_forward: PASS\n"); } static void test_apply_finish_transition_noop_on_terminal_duplicate(void) { xce_jobfinish_t fin; job_t* j; time_t finish_time_before; job_transition_result_t rc; tas_setup(); j = tas_make_job(4, JOB_STAT_DONE); TEST_ASSERT_NOT_NULL(j); j->job.finish_time = 12345; /* sentinel — must survive the no-op */ finish_time_before = j->job.finish_time; memset(&fin, 0, sizeof(fin)); fin.jobid = 4; fin.exit_code = 0; rc = apply_job_finish_transition(j, &fin); TEST_ASSERT_EQUAL_INT_MESSAGE(JOB_TRANSITION_NOOP_DUPLICATE, rc, "a duplicate FINISH on an already-terminal job must no-op (Req 5.1)"); TEST_ASSERT_EQUAL_UINT32_MESSAGE(JOB_STAT_DONE, j->job.state, "a no-op duplicate FINISH must leave the job's state unchanged"); TEST_ASSERT_EQUAL_INT_MESSAGE((int)finish_time_before, (int)j->job.finish_time, "a no-op duplicate FINISH must perform no mutation at all"); tas_teardown(); printf("test_apply_finish_transition_noop_on_terminal_duplicate: PASS\n"); } /* ── apply_job_update_transition ─────────────────────────────────────── */ static void test_apply_update_transition_rejects_backward_on_terminal(void) { job_t* j; job_transition_result_t rc; tas_setup(); j = tas_make_job(5, JOB_STAT_DONE); TEST_ASSERT_NOT_NULL(j); /* An UPDATE that clears the terminal bit is backward (shared predicate * with START, Req 5.2). */ rc = apply_job_update_transition(j, JOB_STAT_RUN); TEST_ASSERT_EQUAL_INT_MESSAGE(JOB_TRANSITION_REJECTED_BACKWARD, rc, "an UPDATE clearing a terminal bit must be rejected as backward"); TEST_ASSERT_EQUAL_UINT32_MESSAGE(JOB_STAT_DONE, j->job.state, "a rejected backward UPDATE must preserve the terminal state"); tas_teardown(); printf("test_apply_update_transition_rejects_backward_on_terminal: PASS\n"); } static void test_apply_update_transition_noop_on_duplicate(void) { job_t* j; job_transition_result_t rc; tas_setup(); j = tas_make_job(6, JOB_STAT_RUN); TEST_ASSERT_NOT_NULL(j); rc = apply_job_update_transition(j, JOB_STAT_RUN); /* state == cur */ TEST_ASSERT_EQUAL_INT_MESSAGE(JOB_TRANSITION_NOOP_DUPLICATE, rc, "an UPDATE requesting the already-current state must no-op " "(Req 5.1)"); TEST_ASSERT_EQUAL_UINT32(JOB_STAT_RUN, j->job.state); tas_teardown(); printf("test_apply_update_transition_noop_on_duplicate: PASS\n"); } static void test_apply_update_transition_forward_merge_on_terminal_applies( void) { job_t* j; job_transition_result_t rc; tas_setup(); j = tas_make_job(7, JOB_STAT_DONE); TEST_ASSERT_NOT_NULL(j); /* A forward merge that keeps the terminal bit (postdone OR'ing PDONE) * must NOT be over-rejected by the backward guard. */ rc = apply_job_update_transition(j, JOB_STAT_DONE | JOB_STAT_PDONE); TEST_ASSERT_EQUAL_INT_MESSAGE(JOB_TRANSITION_APPLIED, rc, "a forward merge preserving the terminal bit must apply, not be " "rejected as backward"); TEST_ASSERT_EQUAL_UINT32(JOB_STAT_DONE | JOB_STAT_PDONE, j->job.state); tas_teardown(); printf("test_apply_update_transition_forward_merge_on_terminal_applies: " "PASS\n"); } /* ── apply_job_fail_transition (no monotonic-rank guard, pin) ────────── */ static void test_apply_fail_transition_merges_perr_on_terminal(void) { job_failinfo_t failinfo; job_t* j; job_transition_result_t rc; tas_setup(); j = tas_make_job(8, JOB_STAT_DONE); TEST_ASSERT_NOT_NULL(j); memset(&failinfo, 0, sizeof(failinfo)); failinfo.jobid = 8; failinfo.stage = JOB_EXEC_STAGE_POST_EXEC; failinfo.code = 1; failinfo.detail = (char*)"post-exec fail after finish"; rc = apply_job_fail_transition(j, &failinfo); TEST_ASSERT_EQUAL_INT_MESSAGE(JOB_TRANSITION_APPLIED, rc, "mark_job_fail has no monotonic-rank guard — a FAIL always applies"); TEST_ASSERT_TRUE_MESSAGE((j->job.state & JOB_STAT_DONE) != 0, "fail-after-finish must keep the DONE terminal bit (PERR-merge, " "unchanged by this extraction)"); TEST_ASSERT_TRUE_MESSAGE((j->job.state & JOB_STAT_PERR) != 0, "fail-after-finish must OR JOB_STAT_PERR onto the existing terminal " "state"); tas_teardown(); printf("test_apply_fail_transition_merges_perr_on_terminal: PASS\n"); } /* ── Test group entry point ──────────────────────────────────────────── */ void test_apply_seam(void) { UnitySetTestFile(__FILE__); RUN_TEST(test_apply_start_transition_applies_forward); RUN_TEST(test_apply_start_transition_rejects_backward_on_terminal); RUN_TEST(test_apply_finish_transition_applies_forward); RUN_TEST(test_apply_finish_transition_noop_on_terminal_duplicate); RUN_TEST(test_apply_update_transition_rejects_backward_on_terminal); RUN_TEST(test_apply_update_transition_noop_on_duplicate); RUN_TEST(test_apply_update_transition_forward_merge_on_terminal_applies); RUN_TEST(test_apply_fail_transition_merges_perr_on_terminal); }