/* test_job_array_apiserver_push.c — job-array-single-wal-record task 5.2 * * Unit tests for the Compact Scheduler/API-Server Push component's * controller-side API-server push (design.md "Compact Scheduler/API-Server * Push"; Req 6.2, 6.4), mirroring task 5.1's test_job_array_sched_push.c * construction for sched.c's xcesched_push_jobarray() -- same discriminator * (l->count > 1), same direct-xce_request() transport -- but for * apiserver.c's apiserver_job_new_fun()/apiserver_push_joblist(). * * Fan-out difference from sched.c: sched.c has a single c->sched.ep, so * task 5.1 could only prove "one push per notification". apiserver.c * already fans a push out over an array of connected servers * (c->apiservers[N_APISERVER_MAX]), exactly like apiserver_push_joblist()'s * existing per-server loop -- so this file simulates TWO connected API * servers to prove the new compact push is genuinely per-connected-server, * not just per-notification (task 5.2's explicit observable: "exactly one * compact push PER CONNECTED API SERVER"). * * - A synthetic multi-element HOOK_DETAIL_JOB_NEW fire (the exact shape * apply_committed_entry()'s WAL_JOB_ARRAY_SUBMIT case produces, task 3.2 * -- one AFTER_EVENT fire, one xlist_t carrying every constructed * element) produces EXACTLY ONE compact Data.jobArray push on EACH of * the two connected servers' endpoints (two pushes total) -- never N * individually-encoded Job structs, and never fewer than one push per * connected server. * - A synthetic single-element fire (ordinary single-job submission) is * byte-for-byte unaffected on every connected server: still the * existing Data.jobs push via apiserver_push_joblist(), unchanged * (Req 8.1). * * Fixture: apiserver_job_new_fun()/apiserver_push_joblist()/ * apiserver_push_jobarray() are static to apiserver.c, so this file drives * them through the same public entry point production code uses: * apiserver_init() installs the HOOK_DETAIL_JOB_NEW hook (guarded locally * with a static flag -- unlike sched.c's on_scheduler_join(), * apiserver_init() has no idempotency guard of its own, so calling it twice * would double-register hooks and double-fire pushes) and xce_run_hooks() * fires it exactly as apply_committed_entry() does. Each connected server's * outbound push is observed the same way test_job_array_sched_push.c * observes them: a zeroed, fd=-1 stack endpoint_t queues packets on * ep->outq without touching a real socket (endpoint_update_epoll no-ops, * zeroed pthread mutexes are valid); this test drains and raw-capnp-decodes * those packets. * * Test-binary strategy (verified against this task's own dependency graph, * NOT assumed identical to sched.c's): apiserver.c calls notify(), * collect_resval(), and check_controller() -- all defined in dataex.c, the * same real-vs-fake collision task 5.1 hit (test_commit_barrier_reply.c and * test_worker_join.c already define their OWN fake notify()/ * on_scheduler_join(), so linking the real dataex.c into the shared * controller_unit_test binary collides at link time). UNLIKE sched.c, * apiserver.c never calls job_submit_committed()/collect_jobs_committed() * (egress.c) -- apiserver_push_joblist() has no WAL-commit filter, a * known, pre-existing, out-of-scope gap (see apiserver.c's own history) -- * so this standalone binary links apiserver.c + dataex.c only, WITHOUT * egress.c (confirmed unnecessary by grep, not assumed). Mirrors * sched_push_unit_test's standalone-binary strategy (meson.build). */ #include #include #include #include #include #include #include "cluster.h" #include "controller.capnp.h" #include "hook.h" #include "netio.h" #include "request.h" #include "xce/cluster_def.h" #include "xce/types.h" #include "xlist.h" extern int apiserver_init(cluster_t*); /* apiserver.c's xce_apiserver_push_workers() (reached only through * on_apiserver_join(), which this file's fixture never calls -- see * ap_setup) references opget.c's check_worker_() as a callback function * pointer, so the symbol must still resolve at link time even though it is * never invoked at runtime here. Stubbed locally rather than linking all of * opget.c, mirroring test_job_array_sched_push.c's identical stub for the * identical reason (sched.c's xcesched_push_workers() has the same * reference). */ int check_worker_(xce_worker_t* w) { return (w != NULL) ? 1 : 0; } #define APISRV_TEST_N 2 /* ------------------------------------------------------------------ */ /* Fixture */ /* ------------------------------------------------------------------ */ static cluster_t g_ap_c; static endpoint_t g_ap_ep[APISRV_TEST_N]; static int g_ap_hook_wired; static void ap_drain_ep(endpoint_t* ep) { data_item_t* it; while ((it = endpoint_deq(ep)) != NULL) { io_packet_t* pkt = ITPTR_TO_PACKET(it); free_packet(pkt); free(pkt); } } static void ap_setup(void) { int i; if (!g_ap_hook_wired) { memset(&g_ap_c, 0, sizeof(g_ap_c)); TEST_ASSERT_EQUAL_INT( 0, xce_hook_init(&g_ap_c.hooksys, XCE_CONTROLLER_HOOK_MAX)); /* apiserver_init() mylock_inits every c->apiservers[] slot AND * installs the HOOK_DETAIL_JOB_NEW (+ sibling) hooks. It has no own * idempotency guard (unlike on_scheduler_join()), so this file * guards the call itself: exactly once, process-wide, shared by * every test below. */ TEST_ASSERT_EQUAL_INT(0, apiserver_init(&g_ap_c)); g_ap_hook_wired = 1; } for (i = 0; i < APISRV_TEST_N; i++) { memset(&g_ap_ep[i], 0, sizeof(g_ap_ep[i])); g_ap_ep[i].fd = -1; g_ap_c.apiservers[i].name = (char*)"apisrv-jsp"; g_ap_c.apiservers[i].ep = &g_ap_ep[i]; } for (i = APISRV_TEST_N; i < N_APISERVER_MAX; i++) { g_ap_c.apiservers[i].name = NULL; g_ap_c.apiservers[i].ep = NULL; } g_ap_c.running = 1; g_ap_c.myhost = &g_ap_c.myhost_info; g_ap_c.myhost_info.name = (char*)"ctrl-jsp"; g_ap_c.myhost_info.key = (char*)""; } static void ap_teardown(void) { int i; for (i = 0; i < APISRV_TEST_N; i++) { ap_drain_ep(&g_ap_ep[i]); g_ap_c.apiservers[i].name = NULL; g_ap_c.apiservers[i].ep = NULL; } } /* ------------------------------------------------------------------ */ /* Helpers */ /* ------------------------------------------------------------------ */ static job_submit_data_t g_ap_submit_data; /* apiserver_push_joblist()/apiserver_push_jobarray() only ever read * xce_job_t's own .jobid/.submit fields directly (never CONTAINER_OF into a * wrapping job_t, unlike sched.c's job_submit_committed() filter) -- so * plain xce_job_t elements are the correct, minimal fixture here. */ static void ap_make_job(xce_job_t* out, uint64_t jobid) { memset(out, 0, sizeof(*out)); out->jobid = jobid; out->submit = &g_ap_submit_data; } static int ap_fire_job_new(xlist_t* l) { return xce_run_hooks(&g_ap_c.hooksys, XCE_CONTROLLER_HOOK_JOB, HOOK_AFTER_EVENT, HOOK_DETAIL_JOB_NEW, &g_ap_c, l); } typedef struct { int n_pushes; int last_data_which; uint64_t last_jobid; uint64_t last_array_size; } ap_push_obs_t; /* Raw-capnp-parse every queued push packet on ep->outq (mirrors * test_job_array_sched_push.c's jsp_scan_pushes). Drains the queue. */ static void ap_scan_pushes(endpoint_t* ep, ap_push_obs_t* obs) { data_item_t* it; memset(obs, 0, sizeof(*obs)); while ((it = endpoint_deq(ep)) != NULL) { io_packet_t* pkt = ITPTR_TO_PACKET(it); struct capn c; struct Request rq; Request_ptr rp; capn_init_mem(&c, (const uint8_t*)pkt->data, pkt->sz, 0); rp.p = capn_getp(capn_root(&c), 0, 1); read_Request(&rq, rp); if (rq.body_which == Request_body_pushmsg) { struct XceData d; read_XceData(&d, rq.body.pushmsg); obs->n_pushes += 1; obs->last_data_which = (int)d.data_which; if (d.data_which == XceData_data_jobArray) { struct JobSubmit js; read_JobSubmit(&js, d.data.jobArray); obs->last_jobid = js.jobid; obs->last_array_size = js.arraySize; } } capn_free(&c); free_packet(pkt); free(pkt); } } /* ------------------------------------------------------------------ */ /* Test 1: multi-element fire -> one compact jobArray push PER connected */ /* API server */ /* ------------------------------------------------------------------ */ static void test_multi_element_job_new_produces_one_compact_jobarray_push_per_server(void) { xce_job_t jobs[5]; void* storage[5]; xlist_t l = { .count = 5, .caps = 5, .delta = 5, .data = storage }; ap_push_obs_t obs; int rc; int i; ap_setup(); for (i = 0; i < 5; i++) { ap_make_job(&jobs[i], 6000 + (uint64_t)i); storage[i] = &jobs[i]; } rc = ap_fire_job_new(&l); TEST_ASSERT_EQUAL_INT(0, rc); for (i = 0; i < APISRV_TEST_N; i++) { ap_scan_pushes(&g_ap_ep[i], &obs); TEST_ASSERT_EQUAL_INT_MESSAGE( 1, obs.n_pushes, "a 5-element array notification must produce exactly ONE push " "packet per connected API server, not N individually-encoded " "job structs and not fewer than one per connected server (Req " "6.2, 6.4)"); TEST_ASSERT_EQUAL_INT_MESSAGE( XceData_data_jobArray, obs.last_data_which, "the push must encode the compact Data.jobArray descriptor, not " "Data.jobs (N individually-encoded job structs)"); TEST_ASSERT_EQUAL_UINT64_MESSAGE( 6000, obs.last_jobid, "base jobid must be the array's first (lowest) jobid"); TEST_ASSERT_EQUAL_UINT64_MESSAGE( 5, obs.last_array_size, "arraySize must be the notification's element count"); } ap_teardown(); printf( "test_multi_element_job_new_produces_one_compact_jobarray_push_per_" "server: PASS\n"); } /* ------------------------------------------------------------------ */ /* Test 2: single-element fire -> unmodified per-job Data.jobs push, */ /* still fanned out to every connected server */ /* ------------------------------------------------------------------ */ static void test_single_element_job_new_is_unaffected(void) { xce_job_t job; void* storage[1]; xlist_t l = { .count = 1, .caps = 1, .delta = 1, .data = storage }; ap_push_obs_t obs; int rc; int i; ap_setup(); ap_make_job(&job, 9101); storage[0] = &job; rc = ap_fire_job_new(&l); TEST_ASSERT_EQUAL_INT(0, rc); for (i = 0; i < APISRV_TEST_N; i++) { ap_scan_pushes(&g_ap_ep[i], &obs); TEST_ASSERT_EQUAL_INT_MESSAGE( 1, obs.n_pushes, "an ordinary single-job notification must still produce exactly " "one push packet per connected API server, unchanged"); TEST_ASSERT_EQUAL_INT_MESSAGE( XceData_data_jobs, obs.last_data_which, "a single-element notification must go through the EXISTING, " "UNMODIFIED apiserver_push_joblist() path (Data.jobs), never " "the new compact jobArray path (Req 8.1)"); } ap_teardown(); printf("test_single_element_job_new_is_unaffected: PASS\n"); } /* ------------------------------------------------------------------ */ /* Standalone Unity main (this binary is its own test executable -- see */ /* meson.build's apiserver_push_unit_test) */ /* ------------------------------------------------------------------ */ void setUp(void) { return; } void tearDown(void) { return; } int main(void) { printf("\n>> job-array-single-wal-record task 5.2: compact API-server " "push\n\n"); UNITY_BEGIN(); RUN_TEST( test_multi_element_job_new_produces_one_compact_jobarray_push_per_server); RUN_TEST(test_single_element_job_new_is_unaffected); return UNITY_END(); }