feat(scheduler-controller-msg-gap): store job rusage on the job + free leaked op payloads (tasks 5.5, 5.8; bug-765 G-5/G-3)

5.5 (Req 6 / G-5): the JOB_RUSAGE notify path called the plugin but never
stored the usage data — xce_sched_job_t had no rusage field (UB-6.1 unmet).
- Add xce_job_rusages_t* rusages to xce_sched_job_t.
- New sched_apply_job_rusage() stores the report ON the job (ownership
  transferred), replacing any prior via free_JobRusages_ptr; -ENOENT unknown,
  -EINVAL NULL. Dequeue NULLs the request pointer on success; kill_sched_job
  releases job->rusages.

5.8 (G-3): sched_pend_update_killer left five new-op payloads on empty
break cases — a per-message leak since ownership is transferred on enqueue.
- Free each with the exact generated deep-freer: free_QueueConf_ptr,
  free_Resource_ptr[], free_ResourceVal_ptr[], free_ResValList_ptr,
  free_JobRusages_ptr. Safe: handlers nullify on enqueue, makeResource copies
  via makeInstance, dequeue copies values (no aliasing).

Tests: sched_job_update_test 7/7 (3 update + 4 rusage), regression
sched_test 15/15, ASAN clean (no bad-free/double-free).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Rongsong Shen 2026-06-19 03:33:28 +00:00
parent 5c1f715ccf
commit 93124c5601
6 changed files with 155 additions and 13 deletions

View file

@ -64,10 +64,10 @@ Wire the five new op types into the `switch (req->op)` in `sched_update()`, impl
- [x] 5.2 Add `XCE_SCHED_OP_UPDATE_RESOURCES` case — iterate `resdata->resources[]`, call `makeResource(sched->resmgr, r)` for each non-NULL entry - [x] 5.2 Add `XCE_SCHED_OP_UPDATE_RESOURCES` case — iterate `resdata->resources[]`, call `makeResource(sched->resmgr, r)` for each non-NULL entry
- [x] 5.3 Add `XCE_SCHED_OP_UPDATE_RESVALS_FULL` case — iterate `resvaldata->vals[]`, find resource in `sched->resmgr` by name, update value; log WARN and skip unknown resources - [x] 5.3 Add `XCE_SCHED_OP_UPDATE_RESVALS_FULL` case — iterate `resvaldata->vals[]`, find resource in `sched->resmgr` by name, update value; log WARN and skip unknown resources
- [x] 5.4 Add `XCE_SCHED_OP_UPDATE_RESVALS` case — iterate `resvals->vals[]`, find worker by host name via `sched_get_worker()`, update worker's `resvals` zarray; silently skip unknown hosts - [x] 5.4 Add `XCE_SCHED_OP_UPDATE_RESVALS` case — iterate `resvals->vals[]`, find worker by host name via `sched_get_worker()`, update worker's `resvals` zarray; silently skip unknown hosts
- [ ] 5.5 Add `XCE_SCHED_OP_JOB_RUSAGE` case — find job by jobid, **store rusage data ON THE JOB** (`xce_job_rusages_t`/`xce_sched_job_rusages_t` field added to `xce_sched_job_t`, replacing any prior, freed in `kill_sched_job`), call `plugin_job_rusage_update()`; return `-ENOENT` if job not found. _Remediation (bug-765, gap G-5): rusage was never stored — `xce_sched_job_t` has no rusage field; Req 6 UB-6.1 unmet. See research.md §Gap Re-Audit._ - [x] 5.5 Add `XCE_SCHED_OP_JOB_RUSAGE` case — find job by jobid, **store rusage data ON THE JOB** (`xce_job_rusages_t`/`xce_sched_job_rusages_t` field added to `xce_sched_job_t`, replacing any prior, freed in `kill_sched_job`), call `plugin_job_rusage_update()`; return `-ENOENT` if job not found. _Remediation (bug-765, gap G-5): rusage was never stored — `xce_sched_job_t` has no rusage field; Req 6 UB-6.1 unmet. See research.md §Gap Re-Audit._
- [x] 5.6 Implement AND WIRE `sched_update_existing_job()` — find job by jobid via `sched_get_job()`, update ALL mutable fields per Req 3 (state, priority, queue, resource request — resreq string realloc like `sched_update_existing_queue`), call `plugin_job_update()`; return `-ENOENT` when jobid unknown, `-EINVAL` on NULL. **CRITICAL routing fix:** the notify `schedUpdateJob` handler (`sched_handle_sched_update_job`, handler.c:200) and the dataset bulk-load (`sched_on_jobs`, handler.c:31) BOTH enqueue `XCE_SCHED_OP_UPDATE_JOB`, and the dequeue (scheduler.c:755) routes BOTH to `sched_update_jobs()` (create/replace). Distinguish the notify path (introduce a distinct op, e.g. `XCE_SCHED_OP_UPDATE_JOB_FIELDS`, enqueued only by the notify handler, dequeued to `sched_update_existing_job`) so a notify update mutates-in-place / returns `-ENOENT` and NEVER creates a job (Req 3: jobs created only via the dataset path). _Remediation (bug-765, gap G-8+G-4): `sched_update_existing_job` was dead code (0 callers) and copied only `state`; UB-3.1/UB-3.2 unmet. See research.md §Gap Re-Audit._ - [x] 5.6 Implement AND WIRE `sched_update_existing_job()` — find job by jobid via `sched_get_job()`, update ALL mutable fields per Req 3 (state, priority, queue, resource request — resreq string realloc like `sched_update_existing_queue`), call `plugin_job_update()`; return `-ENOENT` when jobid unknown, `-EINVAL` on NULL. **CRITICAL routing fix:** the notify `schedUpdateJob` handler (`sched_handle_sched_update_job`, handler.c:200) and the dataset bulk-load (`sched_on_jobs`, handler.c:31) BOTH enqueue `XCE_SCHED_OP_UPDATE_JOB`, and the dequeue (scheduler.c:755) routes BOTH to `sched_update_jobs()` (create/replace). Distinguish the notify path (introduce a distinct op, e.g. `XCE_SCHED_OP_UPDATE_JOB_FIELDS`, enqueued only by the notify handler, dequeued to `sched_update_existing_job`) so a notify update mutates-in-place / returns `-ENOENT` and NEVER creates a job (Req 3: jobs created only via the dataset path). _Remediation (bug-765, gap G-8+G-4): `sched_update_existing_job` was dead code (0 callers) and copied only `state`; UB-3.1/UB-3.2 unmet. See research.md §Gap Re-Audit._
- [x] 5.7 Add WARN log in `sched_update()` after switch block for non-zero `rc` — include symbolic op name via `xce_sched_op_name()` and `strerror(-rc)` - [x] 5.7 Add WARN log in `sched_update()` after switch block for non-zero `rc` — include symbolic op name via `xce_sched_op_name()` and `strerror(-rc)`
- [ ] 5.8 Free new-op payloads in `sched_pend_update_killer` (and any dequeue early-exit) for all new op types (`UPDATE_QUEUE`/`UPDATE_RESOURCES`/`UPDATE_RESVALS_FULL`/`UPDATE_RESVALS`/`JOB_RUSAGE`/the new `UPDATE_JOB_FIELDS`) — ownership IS transferred to the request (handler nullifies the notify/dataset payload pointers), so the empty `break;` cases leak. Use the lib deep-free helpers; validate ASAN-clean (no double-free). _Remediation (bug-765, gap G-3): killer had no-op `break` cases → confirmed per-message leak. See research.md §Gap Re-Audit._ - [x] 5.8 Free new-op payloads in `sched_pend_update_killer` (and any dequeue early-exit) for all new op types (`UPDATE_QUEUE`/`UPDATE_RESOURCES`/`UPDATE_RESVALS_FULL`/`UPDATE_RESVALS`/`JOB_RUSAGE`/the new `UPDATE_JOB_FIELDS`) — ownership IS transferred to the request (handler nullifies the notify/dataset payload pointers), so the empty `break;` cases leak. Use the lib deep-free helpers; validate ASAN-clean (no double-free). _Remediation (bug-765, gap G-3): killer had no-op `break` cases → confirmed per-message leak. See research.md §Gap Re-Audit._
--- ---
@ -128,3 +128,5 @@ Extend `src/scheduler/testsuite/test.c` with unit tests covering every acceptanc
## Implementation Notes ## Implementation Notes
- 5.6 (bug-765 remediation, gaps G-8+G-4, done 2026-06-18): Introduced a distinct op `XCE_SCHED_OP_UPDATE_JOB_FIELDS` (+ `jobfieldsupdate.job` union member) so the notify `schedUpdateJob` handler routes to `sched_update_existing_job` (now in schedobj.c) which mutates state/priority/queue/resreq IN PLACE and returns `-ENOENT` for unknown jobid — never creating a job. The dataset bulk-create path (`sched_on_jobs` → `XCE_SCHED_OP_UPDATE_JOB` → `sched_update_jobs`) is unchanged. Killer frees the new op's payload via `free_Job_ptr`. New white-box test `testsuite/test_job_update.c` (separate meson executable `sched_job_update_test`, white-box `#include "../src/schedobj.c"` + stubs for scheduler-internal externs): 3/3 PASS, regression `sched_test` 15/15, ASAN clean (leaks-off per suite convention). VERIFY NOTES: (1) the handler→dequeue ROUTING is verified by code review, NOT an automated integration test (an end-to-end test through `sched_handle_notify` needs NotifyData capnp plumbing — follow-up); (2) `free(job->resreq)` on replace is shallow but consistent with the codebase — `kill_sched_job` is a no-op stub, so sched jobs are not deep-freed anywhere; the inner-resreq leak is pre-existing, not introduced here. (3) test_job_update.c originally had an accidental `*/` inside a comment (`make_*/kill_*`) + non-ASCII chars + a `make_sched_obj` stub conflicting with the static def in schedobj.c — rewritten ASCII-clean with correct stub set. Task 9.4 (test sched_update_existing_job) is now genuinely satisfied by this real test. - 5.6 (bug-765 remediation, gaps G-8+G-4, done 2026-06-18): Introduced a distinct op `XCE_SCHED_OP_UPDATE_JOB_FIELDS` (+ `jobfieldsupdate.job` union member) so the notify `schedUpdateJob` handler routes to `sched_update_existing_job` (now in schedobj.c) which mutates state/priority/queue/resreq IN PLACE and returns `-ENOENT` for unknown jobid — never creating a job. The dataset bulk-create path (`sched_on_jobs` → `XCE_SCHED_OP_UPDATE_JOB` → `sched_update_jobs`) is unchanged. Killer frees the new op's payload via `free_Job_ptr`. New white-box test `testsuite/test_job_update.c` (separate meson executable `sched_job_update_test`, white-box `#include "../src/schedobj.c"` + stubs for scheduler-internal externs): 3/3 PASS, regression `sched_test` 15/15, ASAN clean (leaks-off per suite convention). VERIFY NOTES: (1) the handler→dequeue ROUTING is verified by code review, NOT an automated integration test (an end-to-end test through `sched_handle_notify` needs NotifyData capnp plumbing — follow-up); (2) `free(job->resreq)` on replace is shallow but consistent with the codebase — `kill_sched_job` is a no-op stub, so sched jobs are not deep-freed anywhere; the inner-resreq leak is pre-existing, not introduced here. (3) test_job_update.c originally had an accidental `*/` inside a comment (`make_*/kill_*`) + non-ASCII chars + a `make_sched_obj` stub conflicting with the static def in schedobj.c — rewritten ASCII-clean with correct stub set. Task 9.4 (test sched_update_existing_job) is now genuinely satisfied by this real test.
- 5.5 (bug-765 remediation, gap G-5, done 2026-06-18): Added `xce_job_rusages_t* rusages` to `xce_sched_job_t` (sched/types.h). New `sched_apply_job_rusage()` (schedobj.c) stores the report ON the job (ownership transferred), replacing any prior via the generated `free_JobRusages_ptr`; returns `-ENOENT` unknown / `-EINVAL` NULL. The dequeue `JOB_RUSAGE` case calls it and NULLs `req->u.job_rusage.rusages` on success so the killer doesn't double-free; `kill_sched_job` (was a no-op stub) now releases `job->rusages`. NOTE: used the generated capnp deep-freer `free_JobRusages_ptr` (NOT a hand-rolled freer — it correctly frees pids/pgids/usages incl. resname per the capnp allocation model; an earlier hand-rolled `free_job_rusages` was removed). 4 new white-box tests (store / replace-frees-prior / -ENOENT / -EINVAL): sched_job_update_test 7/7.
- 5.8 (bug-765 remediation, gap G-3, done 2026-06-18): `sched_pend_update_killer` now frees all five previously-leaked op payloads with the exact generated deep-freers — `free_QueueConf_ptr` (xce_queue_t), `free_Resource_ptr[]`+free(arr) (xce_resource_t), `free_ResourceVal_ptr[]`+free(arr) (xce_resval_t), `free_ResValList_ptr` (xce_resval_list_t), `free_JobRusages_ptr` (xce_job_rusages_t). Safe because: ownership is transferred (notify `sched_handle_notify` and dataset `sched_handle_dataset` nullify their pointers on enqueue), `makeResource` constructs a fresh instance via `makeInstance` (no alias), and the dequeue copies values (no alias). VERIFY NOTE: the JOB_RUSAGE killer path interacts with 5.5's ownership; the other four killer frees are review-verified (type-exact generated freers + confirmed transfer/non-alias) but NOT runtime-ASAN-exercised — those notify/dataset ops are not emitted to the scheduler in production today (research R-3 dormant paths) and constructing capnp payloads for a dequeue test is a follow-up. regression sched_test 15/15; no bad-free/double-free under ASAN.

View file

@ -93,6 +93,10 @@ typedef struct xce_sched_job_ {
uint32_t maxtasks; uint32_t maxtasks;
xce_job_alloc_t* allocs; xce_job_alloc_t* allocs;
xce_job_account_t* accounts; xce_job_account_t* accounts;
/* Latest resource-usage report from the controller (Req 6 / bug-765 G-5).
* Owned by the job: replaced on each JOB_RUSAGE update, freed in
* kill_sched_job. NULL until the first rusage report. */
xce_job_rusages_t* rusages;
} xce_sched_job_t; } xce_sched_job_t;
typedef struct { typedef struct {

View file

@ -139,6 +139,7 @@ extern int sched_add_job(xce_scheduler_t*, xce_sched_job_t*);
extern int sched_add_queue(xce_scheduler_t*, xce_sched_queue_t*); extern int sched_add_queue(xce_scheduler_t*, xce_sched_queue_t*);
extern int sched_update_existing_queue(xce_scheduler_t*, xce_queue_t*); extern int sched_update_existing_queue(xce_scheduler_t*, xce_queue_t*);
extern int sched_update_existing_job(xce_scheduler_t*, xce_job_t*); extern int sched_update_existing_job(xce_scheduler_t*, xce_job_t*);
extern int sched_apply_job_rusage(xce_scheduler_t*, xce_job_rusages_t*);
extern int sched_add_user(xce_scheduler_t*, xce_sched_user_t*); extern int sched_add_user(xce_scheduler_t*, xce_sched_user_t*);
extern int sched_add_worker(xce_scheduler_t*, xce_sched_worker_t*); extern int sched_add_worker(xce_scheduler_t*, xce_sched_worker_t*);
extern int sched_add_project(xce_scheduler_t*, xce_sched_project_t*); extern int sched_add_project(xce_scheduler_t*, xce_sched_project_t*);

View file

@ -16,9 +16,15 @@
void kill_sched_job(void* arg) void kill_sched_job(void* arg)
{ {
if (arg == NULL) { xce_sched_job_t* job = (xce_sched_job_t*)arg;
if (job == NULL) {
return; return;
} }
/* release the stored rusage report, if any (Req 6 / bug-765 G-5).
* free_JobRusages_ptr is NULL-safe and clears the field. */
free_JobRusages_ptr(&job->rusages);
} }
request_res_t* make_resource_request(char* res, float value) request_res_t* make_resource_request(char* res, float value)
@ -584,3 +590,38 @@ int sched_update_existing_job(xce_scheduler_t* sched, xce_job_t* updated)
return 0; return 0;
} }
/*
* sched_apply_job_rusage — store a controller rusage report ON the matching
* job (Req 6, UB-6.1/6.2/6.3; bug-765 G-5). On success the job TAKES OWNERSHIP
* of `rusages` (the caller must not free it); the previous report, if any, is
* released. Returns:
* -EINVAL rusages is NULL
* -ENOENT no job with rusages->jobid (no state modified, ownership retained
* by the caller so it can free the payload)
* 0 stored on the job and plugins notified
*/
int sched_apply_job_rusage(xce_scheduler_t* sched, xce_job_rusages_t* rusages)
{
xce_sched_job_t* job;
if (rusages == NULL) {
return -EINVAL;
}
job = sched_get_job(sched, rusages->jobid);
if (job == NULL) {
return -ENOENT;
}
/* replace any prior report; the job now owns this one. free_JobRusages_ptr
* is NULL-safe and clears the field before we reassign. */
if (job->rusages != rusages) {
free_JobRusages_ptr(&job->rusages);
}
job->rusages = rusages;
plugin_job_rusage_update(sched, job, rusages);
return 0;
}

View file

@ -311,15 +311,33 @@ static void sched_pend_update_killer(data_item_t* it)
break; break;
case XCE_SCHED_OP_JSTATE_CHANGE: case XCE_SCHED_OP_JSTATE_CHANGE:
break; break;
/* The five notify/dataset payloads below are owned by the request (the
* handler/dataset path nullifies its own pointer on enqueue). The dequeue
* copies values / makeResource constructs a fresh instance, so none of
* these are aliased into live state — free them here (bug-765 G-3). */
case XCE_SCHED_OP_UPDATE_QUEUE: case XCE_SCHED_OP_UPDATE_QUEUE:
free_QueueConf_ptr(&(req->u.queueupdate.queue));
break; break;
case XCE_SCHED_OP_UPDATE_RESOURCES: case XCE_SCHED_OP_UPDATE_RESOURCES:
for (i = 0; i < req->u.resourceupdate.nresources; i++) {
free_Resource_ptr(&(req->u.resourceupdate.resources[i]));
}
free(req->u.resourceupdate.resources);
break; break;
case XCE_SCHED_OP_UPDATE_RESVALS_FULL: case XCE_SCHED_OP_UPDATE_RESVALS_FULL:
for (i = 0; i < req->u.resvalfullupdate.nvals; i++) {
free_ResourceVal_ptr(&(req->u.resvalfullupdate.vals[i]));
}
free(req->u.resvalfullupdate.vals);
break; break;
case XCE_SCHED_OP_UPDATE_RESVALS: case XCE_SCHED_OP_UPDATE_RESVALS:
free_ResValList_ptr(&(req->u.resvalupdate.resvals));
break; break;
case XCE_SCHED_OP_JOB_RUSAGE: case XCE_SCHED_OP_JOB_RUSAGE:
/* NULL after a successful sched_apply_job_rusage (job took ownership);
* non-NULL on the -ENOENT/-EINVAL path, freed here (bug-765 G-5).
* free_JobRusages_ptr is NULL-safe. */
free_JobRusages_ptr(&(req->u.job_rusage.rusages));
break; break;
default: default:
break; break;
@ -820,18 +838,16 @@ int sched_update(xce_scheduler_t* sched)
rc = 0; rc = 0;
break; break;
} }
case XCE_SCHED_OP_JOB_RUSAGE: { case XCE_SCHED_OP_JOB_RUSAGE:
xce_sched_job_t* job = NULL; /* Store the rusage report ON the job (Req 6 / bug-765 G-5). On
xce_job_rusages_t* rusages = req->u.job_rusage.rusages; * success the job takes ownership; clear the request pointer so the
job = sched_get_job(sched, rusages->jobid); * killer does not free it. On failure (-ENOENT/-EINVAL) ownership
if (job == NULL) { * stays with the request and the killer frees it. */
rc = -ENOENT; rc = sched_apply_job_rusage(sched, req->u.job_rusage.rusages);
} else { if (rc == 0) {
plugin_job_rusage_update(sched, job, rusages); req->u.job_rusage.rusages = NULL;
rc = 0;
} }
break; break;
}
default: default:
rc = -EINVAL; rc = -EINVAL;
break; break;

View file

@ -42,6 +42,10 @@ xce_sched_project_t* sched_get_project(xce_scheduler_t* s, char* n)
} }
int plugin_job_update(void* s, xce_sched_job_t* j) { (void)s; (void)j; return 0; } int plugin_job_update(void* s, xce_sched_job_t* j) { (void)s; (void)j; return 0; }
int plugin_queue_update(void* s, xce_sched_queue_t* q) { (void)s; (void)q; return 0; } int plugin_queue_update(void* s, xce_sched_queue_t* q) { (void)s; (void)q; return 0; }
int plugin_job_rusage_update(void* s, xce_sched_job_t* j, xce_job_rusages_t* r)
{
(void)s; (void)j; (void)r; return 0;
}
xce_job_account_t* make_job_account(xce_job_alloc_t* a) { (void)a; return NULL; } xce_job_account_t* make_job_account(xce_job_alloc_t* a) { (void)a; return NULL; }
void free_job_account(xce_job_account_t* a) { (void)a; } void free_job_account(xce_job_account_t* a) { (void)a; }
int sched_add_user(xce_scheduler_t* s, xce_sched_user_t* u) { (void)s; (void)u; return 0; } int sched_add_user(xce_scheduler_t* s, xce_sched_user_t* u) { (void)s; (void)u; return 0; }
@ -221,6 +225,76 @@ test_update_null_payload_returns_einval(void)
TEST_ASSERT_EQUAL_INT(-EINVAL, rc); TEST_ASSERT_EQUAL_INT(-EINVAL, rc);
} }
/* Job rusage storage (Req 6 / bug-765 G-5) */
static xce_job_rusages_t* make_rusages(uint64_t jobid)
{
/* count==0 / usages==NULL keeps free_job_rusages a clean structural free. */
xce_job_rusages_t* r = (xce_job_rusages_t*)calloc(1, sizeof(*r));
r->jobid = jobid;
return r;
}
static void
test_rusage_stored_on_existing_job(void)
{
xce_scheduler_t sched;
fixture_init(&sched);
xce_sched_job_t* job = seed_job(&sched, 42);
xce_job_rusages_t* ru = make_rusages(42);
int rc = sched_apply_job_rusage(&sched, ru);
TEST_ASSERT_EQUAL_INT(0, rc);
/* stored ON the job (ownership transferred to the job) */
TEST_ASSERT_EQUAL_PTR(ru, job->rusages);
/* the job object itself is unchanged identity */
TEST_ASSERT_EQUAL_PTR(job, sched_get_job(&sched, 42));
/* job owns ru now; kill_sched_job/free_job_rusages will release it */
}
static void
test_rusage_replace_frees_prior(void)
{
xce_scheduler_t sched;
fixture_init(&sched);
xce_sched_job_t* job = seed_job(&sched, 7);
xce_job_rusages_t* ru1 = make_rusages(7);
xce_job_rusages_t* ru2 = make_rusages(7);
TEST_ASSERT_EQUAL_INT(0, sched_apply_job_rusage(&sched, ru1));
TEST_ASSERT_EQUAL_PTR(ru1, job->rusages);
/* second report replaces the first (prior freed, no leak/crash) */
TEST_ASSERT_EQUAL_INT(0, sched_apply_job_rusage(&sched, ru2));
TEST_ASSERT_EQUAL_PTR(ru2, job->rusages);
}
static void
test_rusage_unknown_job_returns_enoent(void)
{
xce_scheduler_t sched;
fixture_init(&sched);
xce_job_rusages_t* ru = make_rusages(999); /* no such job */
int rc = sched_apply_job_rusage(&sched, ru);
TEST_ASSERT_EQUAL_INT(-ENOENT, rc);
/* on -ENOENT the caller retains ownership */
free_JobRusages_ptr(&ru);
}
static void
test_rusage_null_returns_einval(void)
{
xce_scheduler_t sched;
fixture_init(&sched);
TEST_ASSERT_EQUAL_INT(-EINVAL, sched_apply_job_rusage(&sched, NULL));
}
int main(int argc, char* argv[]) int main(int argc, char* argv[])
{ {
(void)argc; (void)argv; (void)argc; (void)argv;
@ -228,5 +302,9 @@ int main(int argc, char* argv[])
RUN_TEST(test_update_existing_job_mutates_in_place, 0); RUN_TEST(test_update_existing_job_mutates_in_place, 0);
RUN_TEST(test_update_unknown_job_returns_enoent_no_create, 0); RUN_TEST(test_update_unknown_job_returns_enoent_no_create, 0);
RUN_TEST(test_update_null_payload_returns_einval, 0); RUN_TEST(test_update_null_payload_returns_einval, 0);
RUN_TEST(test_rusage_stored_on_existing_job, 0);
RUN_TEST(test_rusage_replace_frees_prior, 0);
RUN_TEST(test_rusage_unknown_job_returns_enoent, 0);
RUN_TEST(test_rusage_null_returns_einval, 0);
return UNITY_END(); return UNITY_END();
} }