Only 21 of 86 test_*.c group functions called their own nested UNITY_BEGIN()/UNITY_END(). Each nested call reset Unity's global NumberOfTests/TestFailures counters, so controller_test.c's final `return UNITY_END();` -- the value meson test actually gates on -- only ever reflected whichever small window of tests ran since the LAST nested reset, silently discarding pass/fail evidence for everything before it (measured: 16 of 972 total tests in one run). The same nested-BEGIN calls also caused PASS/FAIL banners to show the wrong source file for tests in files that don't reset it themselves (Unity.TestFile just carries over from whichever group last called UNITY_BEGIN()). Fix: replace every nested UNITY_BEGIN() with UnitySetTestFile(__FILE__) (sets file attribution only, no counter reset) and drop the matching UNITY_END(); add the same UnitySetTestFile(__FILE__) as the first statement to the other group functions that had neither. The 2 genuinely standalone binaries (test_job_array_sched_push.c, test_job_array_apiserver_push.c -- each has its own int main(), not registered via controller_test.c's TEST() macro) are untouched. Verified: single consolidated summary now reads "972 Tests 0 Failures 0 Ignored OK", exactly matching the ground-truth RUN_TEST call-site count across all 83 registered group files (976 total minus 4 belonging to the 2 standalone binaries). Every PASS/FAIL banner now names its own true source file. Full xce-build.sh all regression pass clean (Ok: 9, Fail: 0). No production code touched -- test-harness plumbing only.
758 lines
29 KiB
C
758 lines
29 KiB
C
/* test_apply_committed_entry.c — raft-apply-decoupling task 2.2
|
|
*
|
|
* Proves apply_committed_entry() (persist.c) is the single dispatch seam
|
|
* that turns ONE committed WAL entry into an in-memory mutation for every
|
|
* classified entry type, and fires that entry type's post-mutation
|
|
* notification hook exactly once, strictly after the mutation completes:
|
|
*
|
|
* - WAL_JOB_SUBMIT -> restore_job() + HOOK_DETAIL_JOB_NEW
|
|
* - WAL_JOB_START/... -> apply_job_update_transition() (2.1's guarded
|
|
* core) + HOOK_DETAIL_JOB_UPDATE
|
|
* - WAL_QUEUE_CTRL -> restore_queue_state() + HOOK_DETAIL_QUEUE_UPDATE
|
|
* - WAL_WORKER_CTRL -> worker_from_capnp() + HOOK_DETAIL_WORKER_JOIN
|
|
*
|
|
* and that a guard-rejected or idempotent-duplicate application performs no
|
|
* mutation and fires NO notification (Req 7.1/7.2).
|
|
*
|
|
* This task does NOT wire apply_committed_entry into any live call site
|
|
* (replay / apply pump are later tasks 2.3/6.1) — every test here calls the
|
|
* dispatch function directly.
|
|
*
|
|
* Fixture: a minimal cluster_t (hooksys + jobmgr/qmgr/workermgr) and a bare
|
|
* persist_t whose ONLY field apply_committed_entry reads is ->cluster — no
|
|
* real WAL/disk/repl/commit-engine setup needed (mirrors test_apply_seam.c's
|
|
* "bare fixture, no more than the function under test needs" philosophy).
|
|
*/
|
|
|
|
#include <rtti.h>
|
|
|
|
#include <capnp_c.h>
|
|
#include <errno.h>
|
|
#include <stdarg.h>
|
|
#include <stdbool.h>
|
|
#include <string.h>
|
|
|
|
#include <unity/unity.h>
|
|
|
|
#include "cluster.h"
|
|
#include "controller.capnp.h"
|
|
#include "hook.h"
|
|
#include "jobj.h"
|
|
#include "persist.h"
|
|
#include "qobj.h"
|
|
#include "wal_types.h"
|
|
#include "wobj.h"
|
|
#include "xce/cluster_def.h"
|
|
#include "xce/types.h"
|
|
#include "xstring.h"
|
|
|
|
/* jobj.c's jobsub table and the process-global xstring table are shared
|
|
* across every test file in the controller_unit_test binary — guard
|
|
* idempotently, mirroring test_apply_seam.c. */
|
|
static int g_tace_jobobj_ready;
|
|
|
|
static cluster_t g_tace_c;
|
|
static persist_t g_tace_p;
|
|
|
|
/* ── Spy hook: counts AFTER_EVENT firings, ignores all payload args ─────── */
|
|
static int g_tace_hook_fires;
|
|
|
|
static int tace_spy_hook(int type, int situation, int detail, void* data,
|
|
va_list ap)
|
|
{
|
|
(void)type; (void)detail; (void)data; (void)ap;
|
|
if (situation == HOOK_AFTER_EVENT)
|
|
g_tace_hook_fires++;
|
|
return 0;
|
|
}
|
|
|
|
/* ── Array-aware spy hook: like tace_spy_hook (counts every AFTER_EVENT
|
|
* firing into the same g_tace_hook_fires counter), additionally recording
|
|
* the xlist_t element count seen on the most recent HOOK_DETAIL_JOB_NEW
|
|
* firing -- this is what lets a test distinguish "fired once carrying N
|
|
* elements" from "fired N times carrying one element each" (task 3.2,
|
|
* Req 3.4). va_arg order mirrors test_apply_pump.c's tap_spy_hook: the
|
|
* WAL_JOB_SUBMIT/WAL_JOB_ARRAY_SUBMIT HOOK_DETAIL_JOB_NEW call shape is
|
|
* (cluster_t*, xlist_t*) -- consume the leading cluster_t* first. ────────── */
|
|
static int g_tace_last_new_list_count;
|
|
|
|
static int tace_array_spy_hook(int type, int situation, int detail,
|
|
void* data, va_list ap)
|
|
{
|
|
(void)type; (void)data;
|
|
if (situation != HOOK_AFTER_EVENT)
|
|
return 0;
|
|
g_tace_hook_fires++;
|
|
if (detail == HOOK_DETAIL_JOB_NEW) {
|
|
(void)va_arg(ap, cluster_t*);
|
|
xlist_t* jlist = va_arg(ap, xlist_t*);
|
|
g_tace_last_new_list_count = jlist ? jlist->count : -1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static void tace_setup(void)
|
|
{
|
|
kvmgr_params_t int_params = { .key_type = KVMGR_KEY_INT,
|
|
.nslots = 32,
|
|
.delta = 16 };
|
|
kvmgr_params_t str_params = { .key_type = KVMGR_KEY_STRING,
|
|
.nslots = 32,
|
|
.delta = 16 };
|
|
|
|
if (makeString((char*)"tace-probe") == NULL)
|
|
xstring_table_init(64);
|
|
|
|
if (!g_tace_jobobj_ready) {
|
|
TEST_ASSERT_EQUAL_INT(0, job_obj_init(32, 32));
|
|
g_tace_jobobj_ready = 1;
|
|
}
|
|
|
|
memset(&g_tace_c, 0, sizeof(g_tace_c));
|
|
g_tace_c.myhost = &g_tace_c.myhost_info;
|
|
g_tace_c.myhost_info.name = (char*)"ctrl-test";
|
|
g_tace_c.myhost_info.key = (char*)"";
|
|
|
|
TEST_ASSERT_EQUAL_INT(0,
|
|
xce_hook_init(&g_tace_c.hooksys, XCE_CONTROLLER_HOOK_MAX));
|
|
|
|
g_tace_c.jobmgr = makeInstance(&xkvmgr_type, &int_params);
|
|
g_tace_c.qmgr = makeInstance(&xkvmgr_type, &str_params);
|
|
g_tace_c.workermgr = makeInstance(&xkvmgr_type, &str_params);
|
|
g_tace_c.resmgr = makeInstance(&xkvmgr_type, &str_params);
|
|
TEST_ASSERT_NOT_NULL(g_tace_c.jobmgr);
|
|
TEST_ASSERT_NOT_NULL(g_tace_c.qmgr);
|
|
TEST_ASSERT_NOT_NULL(g_tace_c.workermgr);
|
|
TEST_ASSERT_NOT_NULL(g_tace_c.resmgr);
|
|
zarray_init(&g_tace_c.sharedinst, 16, NULL);
|
|
|
|
memset(&g_tace_p, 0, sizeof(g_tace_p));
|
|
g_tace_p.cluster = &g_tace_c;
|
|
|
|
g_tace_hook_fires = 0;
|
|
}
|
|
|
|
static void tace_teardown(void)
|
|
{
|
|
zarray_fini(&g_tace_c.sharedinst);
|
|
if (g_tace_c.resmgr) dereference(g_tace_c.resmgr);
|
|
if (g_tace_c.workermgr) dereference(g_tace_c.workermgr);
|
|
if (g_tace_c.qmgr) dereference(g_tace_c.qmgr);
|
|
if (g_tace_c.jobmgr) dereference(g_tace_c.jobmgr);
|
|
xce_hook_fini(&g_tace_c.hooksys);
|
|
}
|
|
|
|
/* ── Encode helpers (mirror the static capnp_encode_{job,worker} in
|
|
* persist.c — those are file-static, so tests keep local copies, exactly
|
|
* as test_egress_filter.c's ef_encode_job already does). ────────────── */
|
|
|
|
static int tace_encode_job(const xce_job_t* j, uint8_t** out, size_t* out_len)
|
|
{
|
|
struct capn c;
|
|
struct capn_segment* cs;
|
|
struct Job cj;
|
|
Job_ptr jp;
|
|
|
|
capn_init_malloc(&c);
|
|
cs = capn_root(&c).seg;
|
|
encode_Job(cs, &cj, (xce_job_t*)j);
|
|
jp = new_Job(cs);
|
|
write_Job(&cj, jp);
|
|
capn_setp(capn_root(&c), 0, jp.p);
|
|
|
|
*out_len = capn_size(&c);
|
|
*out = malloc(*out_len);
|
|
if (!*out) { capn_free(&c); return -ENOMEM; }
|
|
capn_write_mem(&c, *out, *out_len, 0);
|
|
capn_free(&c);
|
|
return 0;
|
|
}
|
|
|
|
static int tace_encode_worker(const xce_worker_t* w, uint8_t** out,
|
|
size_t* out_len)
|
|
{
|
|
struct capn c;
|
|
struct capn_segment* cs;
|
|
struct Worker cw;
|
|
Worker_ptr wp;
|
|
|
|
capn_init_malloc(&c);
|
|
cs = capn_root(&c).seg;
|
|
encode_Worker(cs, &cw, (xce_worker_t*)w);
|
|
wp = new_Worker(cs);
|
|
write_Worker(&cw, wp);
|
|
capn_setp(capn_root(&c), 0, wp.p);
|
|
|
|
*out_len = capn_size(&c);
|
|
*out = malloc(*out_len);
|
|
if (!*out) { capn_free(&c); return -ENOMEM; }
|
|
capn_write_mem(&c, *out, *out_len, 0);
|
|
capn_free(&c);
|
|
return 0;
|
|
}
|
|
|
|
/* Encodes a (base_jobid, array_size, spec) JobSubmit payload -- the exact
|
|
* shape a committed WAL_JOB_ARRAY_SUBMIT record carries (mirrors
|
|
* test_job_array_apply.c's taj_encode_job_submit and this file's own
|
|
* tace_encode_job/tace_encode_worker pattern above). Caller frees *out. */
|
|
static int tace_encode_job_submit_array(uint64_t base_jobid,
|
|
uint64_t array_size,
|
|
job_submit_data_t* spec,
|
|
uint8_t** out, size_t* out_len)
|
|
{
|
|
struct capn c;
|
|
struct capn_segment* cs;
|
|
struct JobSubmit cjs;
|
|
JobSubmit_ptr jsp;
|
|
xce_job_submit_t s;
|
|
|
|
memset(&s, 0, sizeof(s));
|
|
s.jobid = base_jobid;
|
|
s.array_size = array_size;
|
|
s.submit = spec;
|
|
|
|
capn_init_malloc(&c);
|
|
cs = capn_root(&c).seg;
|
|
encode_JobSubmit(cs, &cjs, &s);
|
|
jsp = new_JobSubmit(cs);
|
|
write_JobSubmit(&cjs, jsp);
|
|
capn_setp(capn_root(&c), 0, jsp.p);
|
|
|
|
*out_len = capn_size(&c);
|
|
*out = malloc(*out_len);
|
|
if (!*out) {
|
|
capn_free(&c);
|
|
return -ENOMEM;
|
|
}
|
|
capn_write_mem(&c, *out, *out_len, 0);
|
|
capn_free(&c);
|
|
return 0;
|
|
}
|
|
|
|
/* Look up a job_t wrapper by jobid (NULL if absent). */
|
|
static job_t* tace_find_job(uint64_t jobid)
|
|
{
|
|
job_t* j = NULL;
|
|
lock(g_tace_c.jobmgr, RTTI_LOCK_READ);
|
|
int found = key_find(g_tace_c.jobmgr, (void*)jobid, (void**)&j);
|
|
unlock(g_tace_c.jobmgr);
|
|
return (found > 0) ? j : NULL;
|
|
}
|
|
|
|
/* Creates a job directly in the fixture's jobmgr, bypassing
|
|
* handle_jobsubmit/restore_job entirely — for seeding fact-transition tests. */
|
|
static job_submit_data_t g_tace_seed_submit;
|
|
|
|
static job_t* tace_seed_job(uint64_t jobid, uint32_t state)
|
|
{
|
|
xce_job_t jdef;
|
|
|
|
memset(&g_tace_seed_submit, 0, sizeof(g_tace_seed_submit));
|
|
g_tace_seed_submit.queue = (char*)"q";
|
|
g_tace_seed_submit.user = (char*)"tester";
|
|
|
|
memset(&jdef, 0, sizeof(jdef));
|
|
jdef.jobid = jobid;
|
|
jdef.state = state;
|
|
jdef.submit = &g_tace_seed_submit;
|
|
|
|
return makeJob(g_tace_c.jobmgr, &jdef);
|
|
}
|
|
|
|
/* ── WAL_JOB_SUBMIT ───────────────────────────────────────────────────── */
|
|
|
|
static void test_ace_job_submit_constructs_and_notifies(void)
|
|
{
|
|
tace_setup();
|
|
|
|
job_submit_data_t submit;
|
|
memset(&submit, 0, sizeof(submit));
|
|
submit.queue = (char*)"q";
|
|
submit.user = (char*)"tester";
|
|
|
|
xce_job_t jdef;
|
|
memset(&jdef, 0, sizeof(jdef));
|
|
jdef.jobid = 501;
|
|
jdef.state = JOB_STAT_PEND;
|
|
jdef.submit = &submit;
|
|
|
|
uint8_t* payload = NULL;
|
|
size_t plen = 0;
|
|
TEST_ASSERT_EQUAL_INT(0, tace_encode_job(&jdef, &payload, &plen));
|
|
|
|
TEST_ASSERT_EQUAL_INT(0,
|
|
xce_add_hook(&g_tace_c.hooksys, XCE_CONTROLLER_HOOK_JOB,
|
|
tace_spy_hook, NULL));
|
|
|
|
int rc = apply_committed_entry(&g_tace_p, WAL_JOB_SUBMIT, 10, payload,
|
|
(uint32_t)plen);
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(1, rc,
|
|
"a genuine new job creation must return 1 (bug-1419: distinguishes "
|
|
"creation from an idempotent duplicate no-op)");
|
|
|
|
job_t* j = tace_find_job(501);
|
|
TEST_ASSERT_NOT_NULL_MESSAGE(j,
|
|
"WAL_JOB_SUBMIT must construct the job via restore_job");
|
|
TEST_ASSERT_EQUAL_UINT32(JOB_STAT_PEND, j->job.state);
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(1, g_tace_hook_fires,
|
|
"a fresh job submit must fire exactly one post-mutation notification");
|
|
|
|
free(payload);
|
|
tace_teardown();
|
|
printf("test_ace_job_submit_constructs_and_notifies: PASS\n");
|
|
}
|
|
|
|
static void test_ace_job_submit_duplicate_is_noop_no_extra_notify(void)
|
|
{
|
|
tace_setup();
|
|
|
|
xce_job_t jdef;
|
|
job_submit_data_t submit;
|
|
memset(&submit, 0, sizeof(submit));
|
|
submit.queue = (char*)"q";
|
|
submit.user = (char*)"tester";
|
|
memset(&jdef, 0, sizeof(jdef));
|
|
jdef.jobid = 502;
|
|
jdef.state = JOB_STAT_PEND;
|
|
jdef.submit = &submit;
|
|
|
|
uint8_t* payload = NULL;
|
|
size_t plen = 0;
|
|
TEST_ASSERT_EQUAL_INT(0, tace_encode_job(&jdef, &payload, &plen));
|
|
|
|
xce_add_hook(&g_tace_c.hooksys, XCE_CONTROLLER_HOOK_JOB, tace_spy_hook,
|
|
NULL);
|
|
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(1, apply_committed_entry(&g_tace_p,
|
|
WAL_JOB_SUBMIT, 11,
|
|
payload,
|
|
(uint32_t)plen),
|
|
"a genuine new job creation must return 1");
|
|
TEST_ASSERT_EQUAL_INT(1, g_tace_hook_fires);
|
|
|
|
/* Re-apply the SAME entry (replay / resync redelivery) — must be a
|
|
* no-op: no error, no second job, no second notification (Req 7.1). */
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(0, apply_committed_entry(&g_tace_p,
|
|
WAL_JOB_SUBMIT, 11,
|
|
payload,
|
|
(uint32_t)plen),
|
|
"an idempotent duplicate must return 0, not 1");
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(1, g_tace_hook_fires,
|
|
"an idempotent duplicate submit must not fire a second notification");
|
|
|
|
free(payload);
|
|
tace_teardown();
|
|
printf("test_ace_job_submit_duplicate_is_noop_no_extra_notify: PASS\n");
|
|
}
|
|
|
|
/* ── WAL_JOB_START / job-state fact family ───────────────────────────── */
|
|
|
|
static void test_ace_job_state_transition_applies_forward_and_notifies(void)
|
|
{
|
|
tace_setup();
|
|
|
|
job_t* j = tace_seed_job(601, JOB_STAT_PEND);
|
|
TEST_ASSERT_NOT_NULL(j);
|
|
|
|
wal_jobstate_t js = { .jobid = 601, .state = JOB_STAT_RUN };
|
|
|
|
xce_add_hook(&g_tace_c.hooksys, XCE_CONTROLLER_HOOK_JOB, tace_spy_hook,
|
|
NULL);
|
|
|
|
int rc = apply_committed_entry(&g_tace_p, WAL_JOB_START, 20, &js,
|
|
sizeof(js));
|
|
TEST_ASSERT_EQUAL_INT(0, rc);
|
|
TEST_ASSERT_EQUAL_UINT32_MESSAGE(JOB_STAT_RUN, j->job.state,
|
|
"a forward job-state fact must apply via the 2.1 guarded core");
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(1, g_tace_hook_fires,
|
|
"a forward job-state transition must fire exactly one notification");
|
|
|
|
tace_teardown();
|
|
printf("test_ace_job_state_transition_applies_forward_and_notifies: "
|
|
"PASS\n");
|
|
}
|
|
|
|
static void
|
|
test_ace_job_state_transition_rejects_backward_suppresses_notify(void)
|
|
{
|
|
tace_setup();
|
|
|
|
/* Job already terminal (as if FINISH already applied); a stale,
|
|
* previously-unapplied START must not regress it (Req 7.1/7.2). */
|
|
job_t* j = tace_seed_job(602, JOB_STAT_DONE);
|
|
TEST_ASSERT_NOT_NULL(j);
|
|
|
|
wal_jobstate_t js = { .jobid = 602, .state = JOB_STAT_RUN };
|
|
|
|
xce_add_hook(&g_tace_c.hooksys, XCE_CONTROLLER_HOOK_JOB, tace_spy_hook,
|
|
NULL);
|
|
|
|
int rc = apply_committed_entry(&g_tace_p, WAL_JOB_START, 21, &js,
|
|
sizeof(js));
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(0, rc,
|
|
"a guard-rejected backward transition is a success no-op, not an "
|
|
"apply failure");
|
|
TEST_ASSERT_EQUAL_UINT32_MESSAGE(JOB_STAT_DONE, j->job.state,
|
|
"a rejected backward transition must preserve the terminal state");
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(0, g_tace_hook_fires,
|
|
"a guard-rejected transition must fire NO notification");
|
|
|
|
tace_teardown();
|
|
printf("test_ace_job_state_transition_rejects_backward_suppresses_"
|
|
"notify: PASS\n");
|
|
}
|
|
|
|
/* ── WAL_QUEUE_CTRL ───────────────────────────────────────────────────── */
|
|
|
|
static void test_ace_queue_ctrl_updates_and_notifies(void)
|
|
{
|
|
tace_setup();
|
|
|
|
xce_queue_t qdef;
|
|
memset(&qdef, 0, sizeof(qdef));
|
|
qdef.queue = (char*)"q1";
|
|
TEST_ASSERT_NOT_NULL(makeQueue(g_tace_c.qmgr, &qdef));
|
|
|
|
const char* name = "q1";
|
|
size_t name_len = strlen(name) + 1;
|
|
size_t total = sizeof(wal_queuehdr_t) + name_len;
|
|
uint8_t* pkt = malloc(total);
|
|
TEST_ASSERT_NOT_NULL(pkt);
|
|
wal_queuehdr_t qh = { .status = QUEUE_STAT_CLOSED };
|
|
memcpy(pkt, &qh, sizeof(qh));
|
|
memcpy(pkt + sizeof(qh), name, name_len);
|
|
|
|
xce_add_hook(&g_tace_c.hooksys, XCE_CONTROLLER_HOOK_QUEUE, tace_spy_hook,
|
|
NULL);
|
|
|
|
int rc = apply_committed_entry(&g_tace_p, WAL_QUEUE_CTRL, 30, pkt,
|
|
(uint32_t)total);
|
|
TEST_ASSERT_EQUAL_INT(0, rc);
|
|
|
|
queue_t* q = NULL;
|
|
lock(g_tace_c.qmgr, RTTI_LOCK_READ);
|
|
key_find(g_tace_c.qmgr, (void*)name, (void**)&q);
|
|
unlock(g_tace_c.qmgr);
|
|
TEST_ASSERT_NOT_NULL(q);
|
|
TEST_ASSERT_TRUE_MESSAGE((q->queue.status & QUEUE_STAT_CLOSED) != 0,
|
|
"WAL_QUEUE_CTRL must apply via restore_queue_state");
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(1, g_tace_hook_fires,
|
|
"a queue control apply must fire exactly one notification");
|
|
|
|
free(pkt);
|
|
tace_teardown();
|
|
printf("test_ace_queue_ctrl_updates_and_notifies: PASS\n");
|
|
}
|
|
|
|
/* ── WAL_WORKER_CTRL ──────────────────────────────────────────────────── */
|
|
|
|
static void test_ace_worker_ctrl_join_restores_and_notifies(void)
|
|
{
|
|
tace_setup();
|
|
|
|
/* copy_worker (wobj.c) rejects a NULL src->attrs with -EINVAL — a real
|
|
* WAL_WORKER_CTRL JOIN payload always carries real host attributes, so
|
|
* the fixture must too. */
|
|
hostattrs_t attrs;
|
|
memset(&attrs, 0, sizeof(attrs));
|
|
attrs.hosttype = (char*)"unknown";
|
|
attrs.hostmodel = (char*)"unknown";
|
|
|
|
xce_worker_t wdef;
|
|
memset(&wdef, 0, sizeof(wdef));
|
|
wdef.name = (char*)"w1";
|
|
wdef.attrs = &attrs;
|
|
|
|
uint8_t* wbuf = NULL;
|
|
size_t wsize = 0;
|
|
TEST_ASSERT_EQUAL_INT(0, tace_encode_worker(&wdef, &wbuf, &wsize));
|
|
|
|
uint32_t disc = (uint32_t)HOOK_DETAIL_WORKER_JOIN;
|
|
size_t total = sizeof(disc) + wsize;
|
|
uint8_t* pkt = malloc(total);
|
|
TEST_ASSERT_NOT_NULL(pkt);
|
|
memcpy(pkt, &disc, sizeof(disc));
|
|
memcpy(pkt + sizeof(disc), wbuf, wsize);
|
|
free(wbuf);
|
|
|
|
xce_add_hook(&g_tace_c.hooksys, XCE_CONTROLLER_HOOK_WORKER, tace_spy_hook,
|
|
NULL);
|
|
|
|
int rc = apply_committed_entry(&g_tace_p, WAL_WORKER_CTRL, 40, pkt,
|
|
(uint32_t)total);
|
|
TEST_ASSERT_EQUAL_INT(0, rc);
|
|
|
|
worker_t* w = NULL;
|
|
lock(g_tace_c.workermgr, RTTI_LOCK_READ);
|
|
key_find(g_tace_c.workermgr, (void*)"w1", (void**)&w);
|
|
unlock(g_tace_c.workermgr);
|
|
TEST_ASSERT_NOT_NULL_MESSAGE(w,
|
|
"WAL_WORKER_CTRL must restore the worker via worker_from_capnp");
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(1, g_tace_hook_fires,
|
|
"a worker control apply must fire exactly one notification");
|
|
|
|
free(pkt);
|
|
tace_teardown();
|
|
printf("test_ace_worker_ctrl_join_restores_and_notifies: PASS\n");
|
|
}
|
|
|
|
/* ── WAL_JOB_ARRAY_SUBMIT (job-array-single-wal-record task 3.2) ────────── */
|
|
|
|
static job_submit_data_t tace_array_make_spec(void)
|
|
{
|
|
job_submit_data_t s;
|
|
memset(&s, 0, sizeof(s));
|
|
s.queue = (char*)"normal";
|
|
s.user = (char*)"tester";
|
|
s.mintasks = 1;
|
|
s.maxtasks = 1;
|
|
s.job_name = (char*)"array-job";
|
|
return s;
|
|
}
|
|
|
|
static void test_ace_job_array_submit_constructs_all_and_notifies_once(void)
|
|
{
|
|
tace_setup();
|
|
|
|
const uint64_t base_jobid = 5100;
|
|
const uint64_t array_size = 4;
|
|
job_submit_data_t spec = tace_array_make_spec();
|
|
uint8_t* payload = NULL;
|
|
size_t plen = 0;
|
|
|
|
TEST_ASSERT_EQUAL_INT(0, tace_encode_job_submit_array(base_jobid,
|
|
array_size, &spec,
|
|
&payload, &plen));
|
|
|
|
TEST_ASSERT_EQUAL_INT(0,
|
|
xce_add_hook(&g_tace_c.hooksys, XCE_CONTROLLER_HOOK_JOB,
|
|
tace_array_spy_hook, NULL));
|
|
|
|
int rc = apply_committed_entry(&g_tace_p, WAL_JOB_ARRAY_SUBMIT, 60,
|
|
payload, (uint32_t)plen);
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE((int)array_size, rc,
|
|
"a genuine full-array creation must return the count of newly "
|
|
"created elements (restore_job_array(), not restore_job(), was "
|
|
"invoked)");
|
|
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(1, g_tace_hook_fires,
|
|
"applying one committed array record must fire exactly one "
|
|
"notification event, never one per element");
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE((int)array_size, g_tace_last_new_list_count,
|
|
"the single notification must carry all N newly created elements "
|
|
"in one list, not a 1-element list fired N times");
|
|
|
|
for (uint64_t i = 0; i < array_size; i++) {
|
|
job_t* j = tace_find_job(base_jobid + i);
|
|
TEST_ASSERT_NOT_NULL_MESSAGE(j,
|
|
"every array index must be constructed via restore_job_array");
|
|
TEST_ASSERT_EQUAL_UINT32(JOB_STAT_PEND, j->job.state);
|
|
TEST_ASSERT_EQUAL_UINT64_MESSAGE(60, j->submit_lsn,
|
|
"each constructed element must be tagged with the entry's lsn");
|
|
}
|
|
|
|
free(payload);
|
|
tace_teardown();
|
|
printf("test_ace_job_array_submit_constructs_all_and_notifies_once: "
|
|
"PASS\n");
|
|
}
|
|
|
|
static void test_ace_job_array_submit_idempotent_replay_no_extra_notify(void)
|
|
{
|
|
tace_setup();
|
|
|
|
const uint64_t base_jobid = 5200;
|
|
const uint64_t array_size = 3;
|
|
job_submit_data_t spec = tace_array_make_spec();
|
|
uint8_t* payload = NULL;
|
|
size_t plen = 0;
|
|
|
|
TEST_ASSERT_EQUAL_INT(0, tace_encode_job_submit_array(base_jobid,
|
|
array_size, &spec,
|
|
&payload, &plen));
|
|
|
|
xce_add_hook(&g_tace_c.hooksys, XCE_CONTROLLER_HOOK_JOB,
|
|
tace_array_spy_hook, NULL);
|
|
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE((int)array_size,
|
|
apply_committed_entry(&g_tace_p, WAL_JOB_ARRAY_SUBMIT, 61, payload,
|
|
(uint32_t)plen),
|
|
"the first apply must genuinely construct every element");
|
|
TEST_ASSERT_EQUAL_INT(1, g_tace_hook_fires);
|
|
|
|
/* Re-apply the SAME committed record (replay / resync redelivery): every
|
|
* index already exists, so this is a whole-record idempotent no-op --
|
|
* restore_job_array() returns -EEXIST, apply_committed_entry() must
|
|
* translate that to 0 and fire NO further notification (Req 3.3). */
|
|
int rc2 = apply_committed_entry(&g_tace_p, WAL_JOB_ARRAY_SUBMIT, 61,
|
|
payload, (uint32_t)plen);
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(0, rc2,
|
|
"an idempotent whole-record re-apply must return 0, not a genuine "
|
|
"creation count");
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(1, g_tace_hook_fires,
|
|
"an idempotent duplicate array re-apply must not fire a second "
|
|
"notification");
|
|
|
|
for (uint64_t i = 0; i < array_size; i++) {
|
|
TEST_ASSERT_NOT_NULL(tace_find_job(base_jobid + i));
|
|
}
|
|
|
|
free(payload);
|
|
tace_teardown();
|
|
printf("test_ace_job_array_submit_idempotent_replay_no_extra_notify: "
|
|
"PASS\n");
|
|
}
|
|
|
|
#ifdef XCE_TESTING
|
|
static void test_ace_job_array_submit_forced_failure_fires_no_hook(void)
|
|
{
|
|
tace_setup();
|
|
|
|
const uint64_t base_jobid = 5300;
|
|
const uint64_t array_size = 5;
|
|
job_submit_data_t spec = tace_array_make_spec();
|
|
uint8_t* payload = NULL;
|
|
size_t plen = 0;
|
|
|
|
TEST_ASSERT_EQUAL_INT(0, tace_encode_job_submit_array(base_jobid,
|
|
array_size, &spec,
|
|
&payload, &plen));
|
|
|
|
xce_add_hook(&g_tace_c.hooksys, XCE_CONTROLLER_HOOK_JOB,
|
|
tace_array_spy_hook, NULL);
|
|
|
|
/* Force restore_job_array()'s scratch-build loop to fail constructing
|
|
* element index 2 -- simulates a resource-exhaustion apply failure
|
|
* (Req 9.4). apply_committed_entry() must propagate the negative errno
|
|
* and must NOT fire HOOK_DETAIL_JOB_NEW at all. */
|
|
job_array_apply_test_set_force_fail_at(2);
|
|
|
|
int rc = apply_committed_entry(&g_tace_p, WAL_JOB_ARRAY_SUBMIT, 62,
|
|
payload, (uint32_t)plen);
|
|
TEST_ASSERT_TRUE_MESSAGE(rc < 0,
|
|
"a genuine construction failure must propagate as a negative errno");
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(0, g_tace_hook_fires,
|
|
"a failed array expansion must fire NO notification");
|
|
|
|
for (uint64_t i = 0; i < array_size; i++) {
|
|
TEST_ASSERT_NULL_MESSAGE(tace_find_job(base_jobid + i),
|
|
"an all-or-nothing failed attempt must leave nothing visible");
|
|
}
|
|
|
|
/* Clear the seam and retry the identical committed record: this time
|
|
* construction succeeds fully, the hook fires exactly once with all N
|
|
* elements, and the earlier failed attempt did not poison this one. */
|
|
job_array_apply_test_clear_force_fail();
|
|
|
|
int rc2 = apply_committed_entry(&g_tace_p, WAL_JOB_ARRAY_SUBMIT, 62,
|
|
payload, (uint32_t)plen);
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE((int)array_size, rc2,
|
|
"a retry after the injected failure clears must construct every "
|
|
"element");
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(1, g_tace_hook_fires,
|
|
"the successful retry must fire exactly one notification");
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE((int)array_size, g_tace_last_new_list_count,
|
|
"the retry's single notification must carry all N elements");
|
|
|
|
for (uint64_t i = 0; i < array_size; i++) {
|
|
TEST_ASSERT_NOT_NULL(tace_find_job(base_jobid + i));
|
|
}
|
|
|
|
free(payload);
|
|
tace_teardown();
|
|
printf("test_ace_job_array_submit_forced_failure_fires_no_hook: PASS\n");
|
|
}
|
|
|
|
/* ── job-array-single-wal-record task 7.1 (bug-1443 partial-prefix fix):
|
|
* a bulk-insert-phase failure (the narrower residual gap, distinct from the
|
|
* scratch-build-phase failure above) must STILL fire HOOK_DETAIL_JOB_NEW --
|
|
* for exactly the genuinely-inserted prefix -- while still returning the
|
|
* negative rc so persist_apply_pump_run() halts/fences for the remainder.
|
|
* Before this task, this exact scenario fired NO hook at all (see
|
|
* test_ace_job_array_submit_forced_failure_fires_no_hook above, which forces
|
|
* the OTHER, scratch-build-phase seam and correctly still expects no hook --
|
|
* that one never inserts anything real, so no hook is correct there; this
|
|
* test's bulk-insert-phase seam DOES insert a real, permanent prefix, so a
|
|
* hook is now required). */
|
|
static void test_ace_job_array_submit_bulk_insert_failure_fires_hook_for_prefix(void)
|
|
{
|
|
tace_setup();
|
|
|
|
const uint64_t base_jobid = 5400;
|
|
const uint64_t array_size = 5;
|
|
job_submit_data_t spec = tace_array_make_spec();
|
|
uint8_t* payload = NULL;
|
|
size_t plen = 0;
|
|
|
|
TEST_ASSERT_EQUAL_INT(0, tace_encode_job_submit_array(base_jobid,
|
|
array_size, &spec,
|
|
&payload, &plen));
|
|
|
|
xce_add_hook(&g_tace_c.hooksys, XCE_CONTROLLER_HOOK_JOB,
|
|
tace_array_spy_hook, NULL);
|
|
|
|
/* Force the bulk-insert pass (NOT the scratch-build pass) to fail at
|
|
* index 2 -- indices 0 and 1 are genuinely, permanently inserted into
|
|
* c->jobmgr via restore_job_array()'s real key_add() calls before this
|
|
* fires. */
|
|
job_array_apply_test_set_bulk_insert_force_fail_at(2);
|
|
|
|
int rc = apply_committed_entry(&g_tace_p, WAL_JOB_ARRAY_SUBMIT, 63,
|
|
payload, (uint32_t)plen);
|
|
TEST_ASSERT_TRUE_MESSAGE(rc < 0,
|
|
"a bulk-insert-phase failure must still propagate as a negative"
|
|
" errno so the caller halts/fences the LSN");
|
|
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(1, g_tace_hook_fires,
|
|
"the genuinely-inserted prefix must still get exactly ONE"
|
|
" HOOK_DETAIL_JOB_NEW fire -- task 7.1's bug-1443 partial-prefix fix"
|
|
" -- even though the overall call reports failure");
|
|
TEST_ASSERT_EQUAL_INT_MESSAGE(2, g_tace_last_new_list_count,
|
|
"the fire must carry exactly the 2 genuinely-inserted elements, not"
|
|
" the full array_size and not zero");
|
|
|
|
for (uint64_t i = 0; i < 2; i++) {
|
|
TEST_ASSERT_NOT_NULL_MESSAGE(tace_find_job(base_jobid + i),
|
|
"the genuinely-inserted prefix must be visible in c->jobmgr");
|
|
}
|
|
for (uint64_t i = 2; i < array_size; i++) {
|
|
TEST_ASSERT_NULL_MESSAGE(tace_find_job(base_jobid + i),
|
|
"indices never reached by key_add() must not be visible");
|
|
}
|
|
|
|
job_array_apply_test_clear_bulk_insert_force_fail();
|
|
free(payload);
|
|
tace_teardown();
|
|
printf("test_ace_job_array_submit_bulk_insert_failure_fires_hook_for_"
|
|
"prefix: PASS\n");
|
|
}
|
|
#endif /* XCE_TESTING */
|
|
|
|
/* ── Defensive / boundary ─────────────────────────────────────────────── */
|
|
|
|
static void test_ace_null_persist_returns_error(void)
|
|
{
|
|
int rc = apply_committed_entry(NULL, WAL_JOB_UPDATE, 1, NULL, 0);
|
|
TEST_ASSERT_NOT_EQUAL(0, rc);
|
|
printf("test_ace_null_persist_returns_error: PASS\n");
|
|
}
|
|
|
|
/* ── Test group entry point ───────────────────────────────────────────── */
|
|
|
|
void test_apply_committed_entry(void)
|
|
{
|
|
UnitySetTestFile(__FILE__);
|
|
RUN_TEST(test_ace_job_submit_constructs_and_notifies);
|
|
RUN_TEST(test_ace_job_submit_duplicate_is_noop_no_extra_notify);
|
|
RUN_TEST(test_ace_job_state_transition_applies_forward_and_notifies);
|
|
RUN_TEST(test_ace_job_state_transition_rejects_backward_suppresses_notify);
|
|
RUN_TEST(test_ace_queue_ctrl_updates_and_notifies);
|
|
RUN_TEST(test_ace_worker_ctrl_join_restores_and_notifies);
|
|
RUN_TEST(test_ace_job_array_submit_constructs_all_and_notifies_once);
|
|
RUN_TEST(test_ace_job_array_submit_idempotent_replay_no_extra_notify);
|
|
#ifdef XCE_TESTING
|
|
RUN_TEST(test_ace_job_array_submit_forced_failure_fires_no_hook);
|
|
RUN_TEST(test_ace_job_array_submit_bulk_insert_failure_fires_hook_for_prefix);
|
|
#endif
|
|
RUN_TEST(test_ace_null_persist_returns_error);
|
|
}
|