feat(worker-rejoin-resync): task 4.2 - worker answers job-state query with extras

Classify each queried job ID as running/finished-ok/finished-fail/
unrecognized (Req 3.1-3.3, 3.6), report unqueried running/recently-
finished jobs as extras, and send both via task 1.4's paginated wire
helper (Req 8.2, 8.3). Composes correctly with the already-implemented
controller-side consumer (resync_apply_report).

Adds xcecd_finished_job_retention_snapshot() to xcecd_worker_utils, a
small additive companion needed to enumerate all retained jobs for the
extras set (task 4.1 only exposed single-jobid lookup).

Folds the still-open OPCode_jobStateQuery receive-dispatch wiring gap
into task 4.3's scope in tasks.md, since 4.3 already needs the real
receive path to implement its post-join gate.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y4M5MJM2zK6nU43da267Ku
This commit is contained in:
Rongsong Shen 2026-09-01 16:35:28 +00:00
parent e31012e8a7
commit cfe58c3dcf
No known key found for this signature in database
GPG key ID: 5BCDAAE0123E9FE2
7 changed files with 1084 additions and 4 deletions

View file

@ -107,3 +107,19 @@ test('worker finished_job_retention test', finished_job_retention_test,
suite: 'worker',
timeout: 60,
env: ['ASAN_OPTIONS=detect_leaks=0'])
# worker-rejoin-resync task 4.2 (Requirements 3.1, 3.2, 3.3, 3.6, 8.2, 8.3):
# answer the controller's job-state query (running list + task 4.1's
# finished-job retention), including extras -- white-box #include of both
# ../src/xcecd_worker_utils.c and ../src/xcecd_resync.c, same convention
# as finished_job_retention_test above.
xcecd_resync_test = executable('xcecd_resync_test',
'test_xcecd_resync.c',
include_directories: [worker_test_incdir],
dependencies: worker_test_deps,
install: false)
test('worker xcecd_resync test', xcecd_resync_test,
suite: 'worker',
timeout: 60,
env: ['ASAN_OPTIONS=detect_leaks=0'])

View file

@ -0,0 +1,686 @@
/*
* test_xcecd_resync.c -- worker-rejoin-resync task 4.2 (Requirements 3.1,
* 3.2, 3.3, 3.6, 8.2, 8.3): answer the controller's job-state query from
* the worker's own live running list and the new finished-job retention
* (task 4.1), including extra jobs the worker knows about that were not
* part of the query, sent as one or more paginated OPCode_jobStateReport
* messages.
*
* White-box #include of both ../src/xcecd_worker_utils.c (task 4.1's
* retention + running-list lookups that xcecd_resync.c's classify logic
* calls into for real, not against reimplemented stand-ins) and
* ../src/xcecd_resync.c (this task's own production code under test) --
* same convention as test_finished_job_retention.c and
* test_controller_connector.c. Link-satisfying stubs for
* xcecd_worker_utils.c's OTHER external dependencies are copied verbatim
* from test_finished_job_retention.c's own header comment/stub set; none
* of them are exercised by the assertions below.
*
* Wire-level assertions (paginated OPCode_jobStateReport sends) use
* test_job_state_wire.c's own no-live-socket technique: a real (heap)
* endpoint_t via endpoint_get() with ep->iomgr == NULL, so
* xce_job_state_report_req() can be exercised end-to-end -- real
* xce_encode() on send, real xce_decode() on "receive" -- without a
* socket. Each queued page is popped with endpoint_deq()/ITPTR_TO_PACKET()
* and decoded with xce_decode(), which takes ownership of (frees) the
* packet.
*/
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unity/unity.h>
#include "../src/xcecd_resync.c"
#include "../src/xcecd_worker_utils.c"
/* ════════════════════════════════════════════════════════════════════════
* Link-satisfying stubs -- none exercised by the assertions below; they
* only need to let xcecd_worker_utils.c's other, unrelated code paths
* link and (where reached at all) run to completion without touching
* real sqlite3/dbus/IPC. Copied from test_finished_job_retention.c.
* ════════════════════════════════════════════════════════════════════════ */
bool xcecd_reply_startjob(xcecd_job_t* job)
{
(void)job;
return true;
}
int xcecd_job_stopped(uint64_t job_id)
{
(void)job_id;
return 0;
}
int xcecd_job_resumed(uint64_t job_id)
{
(void)job_id;
return 0;
}
int xcecd_job_finished(xcecd_job_t* job)
{
(void)job;
return 0;
}
bool xcecd_db_update_worker(sqlite3* db, int worker_pid, int worker_status,
int job_status)
{
(void)db;
(void)worker_pid;
(void)worker_status;
(void)job_status;
return true;
}
bool xcecd_db_delete_worker(sqlite3* db, int worker_pid)
{
(void)db;
(void)worker_pid;
return true;
}
bool dbus_worker_clean(xcecd_worker_t* worker, xcecd_job_t* job)
{
(void)worker;
(void)job;
return true;
}
bool ipc_master_check_connection() { return true; }
void block_signal(int sig) { (void)sig; }
void unblock_signal(int sig) { (void)sig; }
/* ════════════════════════════════════════════════════════════════════════
* Fixtures
* ════════════════════════════════════════════════════════════════════════ */
static xcecd_worker_t* make_worker(int pid, uint64_t jobid, int worker_status)
{
xcecd_worker_t* w = (xcecd_worker_t*)calloc(1, sizeof(*w));
w->worker_pid = pid;
w->worker_status = worker_status;
w->job_status
= (worker_status == WORKER_RUNNING) ? JOB_RUNNING : JOB_EXITED;
w->job_info.jobid = jobid;
return w;
}
static void free_worker_list(xcecd_worker_t* list)
{
xcecd_worker_t* w = list;
while (w) {
xcecd_worker_t* next = w->next;
free(w);
w = next;
}
}
static void link_worker(xcecd_worker_t** list, xcecd_worker_t* w)
{
if (*list == NULL) {
*list = w;
return;
}
xcecd_worker_t* cur = *list;
while (cur->next) {
cur = cur->next;
}
cur->next = w;
w->prev = cur;
}
static endpoint_t* new_test_endpoint(void)
{
endpoint_t* ep = endpoint_get();
TEST_ASSERT_NOT_NULL(ep);
return ep;
}
static void pop_and_decode(endpoint_t* ep, xce_request_t* out)
{
data_item_t* it = endpoint_deq(ep);
TEST_ASSERT_NOT_NULL(it);
io_packet_t* pkt = ITPTR_TO_PACKET(it);
memset(out, 0, sizeof(*out));
TEST_ASSERT_EQUAL_INT(0, xce_decode(pkt, out));
}
static void assert_queue_empty(endpoint_t* ep)
{
TEST_ASSERT_NULL(endpoint_deq(ep));
}
void setUp(void) { xcecd_finished_job_retention_reset(); }
void tearDown(void) { xcecd_finished_job_retention_reset(); }
/* ════════════════════════════════════════════════════════════════════════
* xcecd_resync_classify_job: per-ID classification (Requirements 3.1, 3.3)
* ════════════════════════════════════════════════════════════════════════ */
static void test_classify_running_job(void)
{
xcecd_worker_t* w = make_worker(1, 111, WORKER_RUNNING);
xce_job_state_entry_t out;
xcecd_resync_classify_job(w, 111, 5000, &out);
TEST_ASSERT_EQUAL_UINT64(111, out.jobid);
TEST_ASSERT_EQUAL_INT(JobStateStatus_running, out.status);
free(w);
}
static void test_classify_finished_ok_job(void)
{
xcecd_retain_finished_job(222, WORKER_DONE, 0, 90000);
xce_job_state_entry_t out;
xcecd_resync_classify_job(NULL, 222, 95000, &out);
TEST_ASSERT_EQUAL_UINT64(222, out.jobid);
TEST_ASSERT_EQUAL_INT(JobStateStatus_finishedOk, out.status);
TEST_ASSERT_EQUAL_UINT64(90000, out.finish_time_ms);
}
static void test_classify_finished_fail_job(void)
{
xcecd_retain_finished_job(333, WORKER_EXITED, 137, 90500);
xce_job_state_entry_t out;
xcecd_resync_classify_job(NULL, 333, 95000, &out);
TEST_ASSERT_EQUAL_UINT64(333, out.jobid);
TEST_ASSERT_EQUAL_INT(JobStateStatus_finishedFail, out.status);
TEST_ASSERT_EQUAL_INT32(137, out.exit_code);
TEST_ASSERT_EQUAL_UINT64(90500, out.finish_time_ms);
}
static void test_classify_unrecognized_job(void)
{
xce_job_state_entry_t out;
xcecd_resync_classify_job(NULL, 444, 95000, &out);
TEST_ASSERT_EQUAL_UINT64(444, out.jobid);
TEST_ASSERT_EQUAL_INT(JobStateStatus_unrecognized, out.status);
}
/* A job present in the worker list but already terminal (worker_status
* WORKER_DONE/EXITED, lingering before cleanup_worker_list() reaps it)
* must NOT be reported as running -- it must fall through to retention
* (populated by the same update_worker_status() hook at the same
* transition, task 4.1), matching design.md's "check the running list
* first; if not running, check retention" order exactly. */
static void test_classify_terminal_worker_entry_falls_through_to_retention(void)
{
xcecd_worker_t* w = make_worker(5, 555, WORKER_DONE);
xcecd_retain_finished_job(555, WORKER_DONE, 0, 80000);
xce_job_state_entry_t out;
xcecd_resync_classify_job(w, 555, 85000, &out);
TEST_ASSERT_EQUAL_INT(JobStateStatus_finishedOk, out.status);
free(w);
}
/* Requirement 3.3: a job whose process state is inconclusive (here,
* WORKER_UNKNOWN -- the one worker_status update_worker_status() never
* feeds into retention) must be reported unrecognized, not guessed. */
static void test_classify_worker_unknown_status_is_unrecognized(void)
{
xcecd_worker_t* w = make_worker(6, 666, WORKER_UNKNOWN);
xce_job_state_entry_t out;
xcecd_resync_classify_job(w, 666, 85000, &out);
TEST_ASSERT_EQUAL_INT(JobStateStatus_unrecognized, out.status);
free(w);
}
/* Expiry boundary: a retained job older than the configured window must
* classify as unrecognized, not finished (task 4.1's own boundary
* semantics, consumed here through the classify layer). */
static void test_classify_expired_retention_is_unrecognized(void)
{
xcecd_finished_job_retention_set_window_ms(1000);
xcecd_retain_finished_job(777, WORKER_DONE, 0, 100000);
xce_job_state_entry_t out;
xcecd_resync_classify_job(NULL, 777, 100000 + 1000 + 1, &out);
TEST_ASSERT_EQUAL_INT(JobStateStatus_unrecognized, out.status);
}
/* ════════════════════════════════════════════════════════════════════════
* xcecd_resync_build_report: query answers + extras (Requirements 3.2, 3.6)
* ════════════════════════════════════════════════════════════════════════ */
static xce_job_state_entry_t* find_entry(xce_job_state_entry_t** entries, int n,
uint64_t jobid)
{
int i;
for (i = 0; i < n; i++) {
if (entries[i]->jobid == jobid) {
return entries[i];
}
}
return NULL;
}
/* Every queried ID gets exactly one entry, none silently skipped, mixing
* all four outcome kinds (Requirement 3.2, and the task's own Observable
* Completion: "a query covering a mix of running, finished, and unknown
* job IDs produces one correctly-classified entry per ID"). */
static void test_build_report_answers_every_queried_id_exactly_once(void)
{
xcecd_worker_t* list = NULL;
link_worker(&list, make_worker(1, 1001, WORKER_RUNNING));
xcecd_retain_finished_job(1002, WORKER_DONE, 0, 50000);
xcecd_retain_finished_job(1003, WORKER_EXITED, 9, 50500);
/* 1004 is queried but the worker has no record of it at all. */
uint64_t jobids[4] = { 1001, 1002, 1003, 1004 };
xce_job_state_entry_t** entries = NULL;
int n = 0;
int rc = xcecd_resync_build_report(list, jobids, 4, 60000, &entries, &n);
TEST_ASSERT_EQUAL_INT(0, rc);
TEST_ASSERT_EQUAL_INT(4, n);
xce_job_state_entry_t* e1001 = find_entry(entries, n, 1001);
xce_job_state_entry_t* e1002 = find_entry(entries, n, 1002);
xce_job_state_entry_t* e1003 = find_entry(entries, n, 1003);
xce_job_state_entry_t* e1004 = find_entry(entries, n, 1004);
TEST_ASSERT_NOT_NULL(e1001);
TEST_ASSERT_EQUAL_INT(JobStateStatus_running, e1001->status);
TEST_ASSERT_EQUAL_INT(1, e1001->queried);
TEST_ASSERT_NOT_NULL(e1002);
TEST_ASSERT_EQUAL_INT(JobStateStatus_finishedOk, e1002->status);
TEST_ASSERT_EQUAL_UINT64(50000, e1002->finish_time_ms);
TEST_ASSERT_EQUAL_INT(1, e1002->queried);
TEST_ASSERT_NOT_NULL(e1003);
TEST_ASSERT_EQUAL_INT(JobStateStatus_finishedFail, e1003->status);
TEST_ASSERT_EQUAL_INT32(9, e1003->exit_code);
TEST_ASSERT_EQUAL_INT(1, e1003->queried);
TEST_ASSERT_NOT_NULL(e1004);
TEST_ASSERT_EQUAL_INT(JobStateStatus_unrecognized, e1004->status);
TEST_ASSERT_EQUAL_INT(1, e1004->queried);
xcecd_resync_free_entries(entries, n);
free_worker_list(list);
}
/* Requirement 3.6: a job the worker is running that was NOT part of the
* query is reported as an extra (queried=0). */
static void test_build_report_includes_running_extra(void)
{
xcecd_worker_t* list = NULL;
link_worker(&list, make_worker(1, 2001, WORKER_RUNNING));
link_worker(&list, make_worker(2, 2002, WORKER_RUNNING)); /* extra */
uint64_t jobids[1] = { 2001 };
xce_job_state_entry_t** entries = NULL;
int n = 0;
int rc = xcecd_resync_build_report(list, jobids, 1, 1000, &entries, &n);
TEST_ASSERT_EQUAL_INT(0, rc);
TEST_ASSERT_EQUAL_INT(2, n);
xce_job_state_entry_t* queried = find_entry(entries, n, 2001);
xce_job_state_entry_t* extra = find_entry(entries, n, 2002);
TEST_ASSERT_NOT_NULL(queried);
TEST_ASSERT_EQUAL_INT(1, queried->queried);
TEST_ASSERT_NOT_NULL(extra);
TEST_ASSERT_EQUAL_INT(0, extra->queried);
TEST_ASSERT_EQUAL_INT(JobStateStatus_running, extra->status);
xcecd_resync_free_entries(entries, n);
free_worker_list(list);
}
/* Requirement 3.6: a job the worker recently finished that was NOT part
* of the query is reported as an extra, with its real finish time. */
static void test_build_report_includes_finished_extra(void)
{
xcecd_retain_finished_job(3001, WORKER_EXITED, 5, 70000);
xce_job_state_entry_t** entries = NULL;
int n = 0;
int rc = xcecd_resync_build_report(NULL, NULL, 0, 75000, &entries, &n);
TEST_ASSERT_EQUAL_INT(0, rc);
TEST_ASSERT_EQUAL_INT(1, n);
TEST_ASSERT_EQUAL_UINT64(3001, entries[0]->jobid);
TEST_ASSERT_EQUAL_INT(JobStateStatus_finishedFail, entries[0]->status);
TEST_ASSERT_EQUAL_INT32(5, entries[0]->exit_code);
TEST_ASSERT_EQUAL_UINT64(70000, entries[0]->finish_time_ms);
TEST_ASSERT_EQUAL_INT(0, entries[0]->queried);
xcecd_resync_free_entries(entries, n);
}
/* A job that IS part of the query must never also appear a second time
* as an extra -- the queried answer is the only entry for it. */
static void test_build_report_queried_job_not_duplicated_as_extra(void)
{
xcecd_worker_t* list = NULL;
link_worker(&list, make_worker(1, 4001, WORKER_RUNNING));
xcecd_retain_finished_job(4002, WORKER_DONE, 0, 10000);
uint64_t jobids[2] = { 4001, 4002 };
xce_job_state_entry_t** entries = NULL;
int n = 0;
int rc = xcecd_resync_build_report(list, jobids, 2, 20000, &entries, &n);
TEST_ASSERT_EQUAL_INT(0, rc);
TEST_ASSERT_EQUAL_INT(2, n);
int count4001 = 0, count4002 = 0;
int i;
for (i = 0; i < n; i++) {
if (entries[i]->jobid == 4001)
count4001++;
if (entries[i]->jobid == 4002)
count4002++;
}
TEST_ASSERT_EQUAL_INT(1, count4001);
TEST_ASSERT_EQUAL_INT(1, count4002);
xcecd_resync_free_entries(entries, n);
free_worker_list(list);
}
/* Empty query, nothing running or retained -> a valid, empty (zero-entry)
* result, not an error. */
static void test_build_report_empty_query_no_state_yields_empty_result(void)
{
xce_job_state_entry_t** entries
= (xce_job_state_entry_t**)0x1; /* sentinel */
int n = -1;
int rc = xcecd_resync_build_report(NULL, NULL, 0, 1000, &entries, &n);
TEST_ASSERT_EQUAL_INT(0, rc);
TEST_ASSERT_EQUAL_INT(0, n);
TEST_ASSERT_NULL(entries);
}
static void test_build_report_null_out_params_rejected(void)
{
uint64_t jobids[1] = { 1 };
xce_job_state_entry_t** entries = NULL;
int n = 0;
TEST_ASSERT_EQUAL_INT(
-EINVAL, xcecd_resync_build_report(NULL, jobids, 1, 1000, NULL, &n));
TEST_ASSERT_EQUAL_INT(-EINVAL, xcecd_resync_build_report(
NULL, jobids, 1, 1000, &entries, NULL));
}
/* ════════════════════════════════════════════════════════════════════════
* xcecd_resync_answer_query: end-to-end paginated send (Requirements 8.2,
* 8.3)
* ════════════════════════════════════════════════════════════════════════ */
static void test_answer_query_single_page(void)
{
xcecd_worker_t* list = NULL;
link_worker(&list, make_worker(1, 5001, WORKER_RUNNING));
xcecd_retain_finished_job(5002, WORKER_DONE, 0, 30000);
uint64_t jobids[2] = { 5001, 5002 };
char nm[] = "worker1";
char ky[] = "key";
xce_peerinfo_t from = { .name = nm, .key = ky };
endpoint_t* ep = new_test_endpoint();
int rc = xcecd_resync_answer_query(list, ep, &from, jobids, 2, 40000, 0);
TEST_ASSERT_EQUAL_INT(0, rc);
xce_request_t decoded;
pop_and_decode(ep, &decoded);
TEST_ASSERT_EQUAL_INT(OPCode_jobStateReport, decoded.opcode);
TEST_ASSERT_EQUAL_INT(Request_body_jobstatereportmsg, decoded.body_which);
TEST_ASSERT_EQUAL_INT(2, decoded.body.jobstatereportmsg->n_total);
TEST_ASSERT_EQUAL_INT(2, decoded.body.jobstatereportmsg->count);
xce_free_request(&decoded);
assert_queue_empty(ep);
endpoint_fini(ep);
free(ep);
free_worker_list(list);
}
/* Requirement 8.3: nothing truncated/dropped across multiple pages, for
* a mix of queried answers and extras. */
static void test_answer_query_multipage_covers_every_job(void)
{
xcecd_worker_t* list = NULL;
int i;
/* 3 queried running jobs. */
uint64_t jobids[3] = { 6001, 6002, 6003 };
for (i = 0; i < 3; i++) {
link_worker(&list, make_worker(100 + i, jobids[i], WORKER_RUNNING));
}
/* 2 extra running jobs, not queried. */
link_worker(&list, make_worker(200, 6004, WORKER_RUNNING));
link_worker(&list, make_worker(201, 6005, WORKER_RUNNING));
/* 2 extra finished jobs, not queried. */
xcecd_retain_finished_job(6006, WORKER_DONE, 0, 90000);
xcecd_retain_finished_job(6007, WORKER_EXITED, 3, 90500);
char nm[] = "worker1";
char ky[] = "key";
xce_peerinfo_t from = { .name = nm, .key = ky };
endpoint_t* ep = new_test_endpoint();
/* 7 total entries, pktlimit=3 -> pages of 3, 3, 1. */
int rc = xcecd_resync_answer_query(list, ep, &from, jobids, 3, 95000, 3);
TEST_ASSERT_EQUAL_INT(0, rc);
uint64_t rebuilt[7];
int queried_flags[7];
int rebuilt_n = 0;
int page;
for (page = 0; page < 3; page++) {
xce_request_t decoded;
pop_and_decode(ep, &decoded);
TEST_ASSERT_EQUAL_INT(OPCode_jobStateReport, decoded.opcode);
TEST_ASSERT_EQUAL_INT(7, decoded.body.jobstatereportmsg->n_total);
int c = decoded.body.jobstatereportmsg->count;
int i2;
for (i2 = 0; i2 < c; i2++) {
xce_job_state_entry_t* got
= decoded.body.jobstatereportmsg->entries[i2];
rebuilt[rebuilt_n] = got->jobid;
queried_flags[rebuilt_n] = got->queried;
rebuilt_n++;
}
xce_free_request(&decoded);
}
assert_queue_empty(ep);
TEST_ASSERT_EQUAL_INT(7, rebuilt_n);
/* Every job from 6001..6007 must appear exactly once across all
* pages, with the correct queried flag. */
uint64_t expected[7] = { 6001, 6002, 6003, 6004, 6005, 6006, 6007 };
int expected_q[7] = { 1, 1, 1, 0, 0, 0, 0 };
int j;
for (j = 0; j < 7; j++) {
int found = 0;
int k;
for (k = 0; k < 7; k++) {
if (rebuilt[k] == expected[j]) {
TEST_ASSERT_EQUAL_INT(expected_q[j], queried_flags[k]);
found = 1;
}
}
TEST_ASSERT_TRUE_MESSAGE(found, "every job must appear exactly once "
"across all report pages, none "
"dropped");
}
endpoint_fini(ep);
free(ep);
free_worker_list(list);
}
/* An empty query against a worker with no running/retained jobs still
* sends exactly one (empty) report page -- not skipped. */
static void test_answer_query_empty_still_sends_one_page(void)
{
uint64_t* jobids = NULL;
char nm[] = "worker1";
char ky[] = "key";
xce_peerinfo_t from = { .name = nm, .key = ky };
endpoint_t* ep = new_test_endpoint();
int rc = xcecd_resync_answer_query(NULL, ep, &from, jobids, 0, 1000, 0);
TEST_ASSERT_EQUAL_INT(0, rc);
xce_request_t decoded;
pop_and_decode(ep, &decoded);
TEST_ASSERT_EQUAL_INT(OPCode_jobStateReport, decoded.opcode);
TEST_ASSERT_EQUAL_INT(0, decoded.body.jobstatereportmsg->n_total);
TEST_ASSERT_EQUAL_INT(0, decoded.body.jobstatereportmsg->count);
xce_free_request(&decoded);
assert_queue_empty(ep);
endpoint_fini(ep);
free(ep);
}
/* ════════════════════════════════════════════════════════════════════════
* xcecd_finished_job_retention_snapshot: task 4.2's companion addition to
* xcecd_worker_utils.c/h (needed to enumerate retention for extras, since
* task 4.1's own public API was jobid-keyed lookup only).
* ════════════════════════════════════════════════════════════════════════ */
static void test_retention_snapshot_returns_all_unexpired_entries(void)
{
xcecd_retain_finished_job(9001, WORKER_DONE, 0, 10000);
xcecd_retain_finished_job(9002, WORKER_EXITED, 1, 10500);
xcecd_finished_job_t* snap = NULL;
int n = 0;
int rc = xcecd_finished_job_retention_snapshot(20000, &snap, &n);
TEST_ASSERT_EQUAL_INT(0, rc);
TEST_ASSERT_EQUAL_INT(2, n);
TEST_ASSERT_NOT_NULL(snap);
free(snap);
}
static void test_retention_snapshot_excludes_expired_entries(void)
{
xcecd_finished_job_retention_set_window_ms(1000);
xcecd_retain_finished_job(9101, WORKER_DONE, 0, 10000);
xcecd_finished_job_t* snap = NULL;
int n = 0;
int rc = xcecd_finished_job_retention_snapshot(10000 + 1000 + 1, &snap, &n);
TEST_ASSERT_EQUAL_INT(0, rc);
TEST_ASSERT_EQUAL_INT(0, n);
TEST_ASSERT_NULL(snap);
}
static void test_retention_snapshot_empty_store(void)
{
xcecd_finished_job_t* snap = (xcecd_finished_job_t*)0x1;
int n = -1;
int rc = xcecd_finished_job_retention_snapshot(1000, &snap, &n);
TEST_ASSERT_EQUAL_INT(0, rc);
TEST_ASSERT_EQUAL_INT(0, n);
TEST_ASSERT_NULL(snap);
}
static void test_retention_snapshot_null_out_params_rejected(void)
{
xcecd_finished_job_t* snap = NULL;
int n = 0;
TEST_ASSERT_EQUAL_INT(
-EINVAL, xcecd_finished_job_retention_snapshot(1000, NULL, &n));
TEST_ASSERT_EQUAL_INT(
-EINVAL, xcecd_finished_job_retention_snapshot(1000, &snap, NULL));
}
/* ════════════════════════════════════════════════════════════════════════
* main
* ════════════════════════════════════════════════════════════════════════ */
int main(void)
{
xce_init_xcecd_log("/tmp/test_xcecd_resync.log", 1024 * 1024, 2);
UNITY_BEGIN();
RUN_TEST(test_classify_running_job);
RUN_TEST(test_classify_finished_ok_job);
RUN_TEST(test_classify_finished_fail_job);
RUN_TEST(test_classify_unrecognized_job);
RUN_TEST(test_classify_terminal_worker_entry_falls_through_to_retention);
RUN_TEST(test_classify_worker_unknown_status_is_unrecognized);
RUN_TEST(test_classify_expired_retention_is_unrecognized);
RUN_TEST(test_build_report_answers_every_queried_id_exactly_once);
RUN_TEST(test_build_report_includes_running_extra);
RUN_TEST(test_build_report_includes_finished_extra);
RUN_TEST(test_build_report_queried_job_not_duplicated_as_extra);
RUN_TEST(test_build_report_empty_query_no_state_yields_empty_result);
RUN_TEST(test_build_report_null_out_params_rejected);
RUN_TEST(test_answer_query_single_page);
RUN_TEST(test_answer_query_multipage_covers_every_job);
RUN_TEST(test_answer_query_empty_still_sends_one_page);
RUN_TEST(test_retention_snapshot_returns_all_unexpired_entries);
RUN_TEST(test_retention_snapshot_excludes_expired_entries);
RUN_TEST(test_retention_snapshot_empty_store);
RUN_TEST(test_retention_snapshot_null_out_params_rejected);
return UNITY_END();
}