myxce/src/controller/testsuite/test_job_array_fence.c
Rongsong Shen a81f2a249c fix(controller-tests): make Unity test-file attribution and counters consistent suite-wide
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.
2026-07-17 14:49:12 +00:00

522 lines
23 KiB
C

/* test_job_array_fence.c — job-array-single-wal-record task 4.4
*
* Integration capstone for the Live Apply Resource Fence (design.md "Live
* Apply Resource Fence"; Requirements 9.4, 9.5, 9.6). This is the ONE test
* that drives the FULL engage-refuse-clear-recover sequence end-to-end
* through the real production seams built by tasks 3.1/4.1/4.2/4.3 — it adds
* no new mechanism, it only wires the existing pieces together the way a
* live cluster would exercise them:
*
* 1. Force a resource-exhaustion-style failure during array expansion.
* 2. The node fences itself (persist_apply_resource_fenced() reads true).
* 3. The node is refused promotion (ha.c's periodic fence-release check —
* task 4.3's site 1, ha_arm_promotion_fence_timer_for_test).
* 4. No partial or divergent set of elements was constructed.
* 5. The failure does not affect another (simulated) node's independent
* progress.
* 6. Clearing the forced condition and retrying succeeds: the fence
* self-clears and the node is eligible for promotion again.
*
* ── Seam choice (construction-phase, not bulk-insert-phase) ────────────────
* job.c exposes TWO distinct XCE_TESTING fault-injection points for
* restore_job_array() (see tasks.md Implementation Notes, task 3.1):
* - job_array_apply_test_set_force_fail_at(idx): fails the SCRATCH-BUILD
* phase (makeJob() at index idx). Proven fully atomic: nothing is ever
* written to c->jobmgr on this path (test_job_array_apply.c, test_apply_
* pump.c tests 2b-2d).
* - job_array_apply_test_set_bulk_insert_force_fail_at(idx): fails the
* final BULK-INSERT pass (key_add() at index idx). tasks.md's task 3.1
* Implementation Note documents this as a NARROWER, NON-atomic case by
* design: indices [0, idx) are already genuinely, permanently inserted
* into c->jobmgr (no key_del exists to undo them) before the failure is
* detected — proven directly by test_rja_bulk_insert_failure_reports_
* error_not_false_success in test_job_array_apply.c.
* Requirement 9.6 (and this task's own step 4) requires proving "constructs
* no partial or divergent set of elements" — a claim that is TRUE for the
* construction-phase seam and, by the task 3.1 Implementation Note's own
* documented residual gap, FALSE for the bulk-insert-phase seam. Using the
* bulk-insert seam here would either make this test's own step-4 assertion
* fail or force weakening it to hide a known gap — neither is acceptable for
* the capstone test that is supposed to PROVE Requirement 9.6. This file
* therefore drives the construction-phase seam for the full sequence. The
* bulk-insert-phase seam's own (already known, already tested at the
* restore_job_array() level) residual gap is a separate, pre-existing,
* documented finding — not something this task's Observable asks it to
* close.
*
* ── Fixture ──────────────────────────────────────────────────────────────
* A single, real cluster_t (g_tf_c) combining:
* - test_apply_pump.c's tap_setup shape (real persist_init/persist_start,
* jobmgr/qmgr/workermgr/resmgr, hooksys, a wired commit engine with two
* stub standbys) — needed to append a real WAL_JOB_ARRAY_SUBMIT entry
* and drive persist_apply_pump_run()/restore_job_array() for real.
* - test_commit_durability.c's dur_make_fence_cluster shape (ha_init +
* a real, started delaymgr) — needed to drive the SAME cluster_t's
* promotion-fence gate (ha_arm_promotion_fence_timer_for_test, task
* 4.3's site 1) through a real bounded-window timer.
* The commit path itself is driven white-box (persist_wal_async +
* atomic_store(committed_lsn) + a direct persist_apply_pump_run() call) —
* the exact pattern test_apply_pump.c's own task-4.1/4.2 tests
* (test_apply_pump_run_halts_at_failed_lsn_without_advancing,
* test_apply_pump_run_engages_live_fence_on_failed_lsn,
* test_apply_pump_run_self_clears_fence_on_successful_retry) already use for
* this exact WAL_JOB_ARRAY_SUBMIT/fence area — reused rather than driving
* the full multi-node repl_on_ack commit-engine path, which would add
* flakiness risk (see that file's own header comment on the fsync-thread
* race) without adding proof of anything this test needs: the seam under
* test is restore_job_array()'s construction failure and persist_apply_pump_
* run()'s halt/fence/self-heal, which behave identically once committed_lsn
* advances, however it advanced.
*
* Step 5 ("does not affect any other node's independent progress") uses a
* second, wholly separate cluster_t (node2, jobmgr-only, mirroring test_job_
* array_apply.c's taj_setup) and calls restore_job_array() on it directly —
* proving the fence state lives on g_tf_c's OWN persist_t (a parameter, not
* global state) and that a different cluster_t's independent apply succeeds
* untouched by g_tf_c's fenced condition. The XCE_TESTING fault-injection
* variables ARE process-global (job.c's own doc comment: "a later task
* needing independent multi-node fault injection can revisit this"), so this
* is the lighter-weight, sequential proof the task description explicitly
* sanctions rather than a true concurrent multi-process harness.
*/
#include <rtti.h>
#include <capnp_c.h>
#include <errno.h>
#include <stdatomic.h>
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <unity/unity.h>
#include "cluster.h"
#include "commit.h"
#include "controller.capnp.h"
#include "delay.h"
#include "ha.h"
#include "hook.h"
#include "jobj.h"
#include "persist.h"
#include "repl.h"
#include "wal_types.h"
#include "xce/cluster_def.h"
#include "xce/types.h"
#include "xlist.h"
#include "xstring.h"
#ifdef XCE_TESTING
/* jobj.c's jobsub table / the process-global xstring table are shared
* across every test file in the controller_unit_test binary — guard
* idempotently, mirroring every sibling fixture in this suite. */
static int g_tf_jobobj_ready;
static cluster_t g_tf_c; /* large — must not live on the test stack */
static commit_engine_t g_tf_hce;
static int g_tf_hep_a, g_tf_hep_b; /* dummy standby endpoints */
static char g_tf_tmpdir[256];
extern int (*repl_broadcast_send_hook)(void *ep, wal_catchup_entry_t **entries,
int n_entries);
static int tf_drop_send_hook(void *ep, wal_catchup_entry_t **entries,
int n_entries)
{
(void)ep; (void)entries; (void)n_entries;
return 0;
}
/* The delaymgr's task-run wrapper — a trivial synchronous pass-through
* (mirrors test_commit_durability.c's dur_run_delay_task); the real
* delaymgr worker thread still owns the actual wall-clock scheduling, this
* is only how each fired task is invoked. */
static int tf_run_delay_task(delay_task_entry_fn_t entry, void *arg)
{
return entry(arg);
}
static void tf_setup(void)
{
kvmgr_params_t str_params = { .key_type = KVMGR_KEY_STRING,
.nslots = 32,
.delta = 16 };
kvmgr_params_t int_params = { .key_type = KVMGR_KEY_INT,
.nslots = 32,
.delta = 16 };
if (makeString((char *)"tf-probe") == NULL)
xstring_table_init(64);
if (!g_tf_jobobj_ready) {
TEST_ASSERT_EQUAL_INT(0, job_obj_init(32, 32));
g_tf_jobobj_ready = 1;
}
memset(&g_tf_c, 0, sizeof(g_tf_c));
g_tf_c.myhost = &g_tf_c.myhost_info;
g_tf_c.myhost_info.name = (char *)"ctrl-test";
g_tf_c.myhost_info.key = (char *)"";
TEST_ASSERT_EQUAL_INT(0,
xce_hook_init(&g_tf_c.hooksys, XCE_CONTROLLER_HOOK_MAX));
g_tf_c.jobmgr = makeInstance(&xkvmgr_type, &int_params);
g_tf_c.qmgr = makeInstance(&xkvmgr_type, &str_params);
g_tf_c.workermgr = makeInstance(&xkvmgr_type, &str_params);
g_tf_c.resmgr = makeInstance(&xkvmgr_type, &str_params);
TEST_ASSERT_NOT_NULL(g_tf_c.jobmgr);
TEST_ASSERT_NOT_NULL(g_tf_c.qmgr);
TEST_ASSERT_NOT_NULL(g_tf_c.workermgr);
TEST_ASSERT_NOT_NULL(g_tf_c.resmgr);
zarray_init(&g_tf_c.sharedinst, 16, NULL);
persist_config_t cfg = persist_config_defaults();
cfg.wal_sync_ms = 5000;
cfg.sync_timeout_ms = 2000;
snprintf(g_tf_tmpdir, sizeof(g_tf_tmpdir), "/tmp/xce_tf44_XXXXXX");
TEST_ASSERT_NOT_NULL(mkdtemp(g_tf_tmpdir));
persist_set_workdir(&g_tf_c.persist, g_tf_tmpdir, "ctrl");
TEST_ASSERT_EQUAL_INT(0, persist_init(&g_tf_c.persist, &cfg, &g_tf_c));
TEST_ASSERT_EQUAL_INT(0, persist_start(&g_tf_c.persist));
g_tf_c.persist.recovery_verdict = RECOVER_CLEAN;
TEST_ASSERT_TRUE(
repl_add_standby(&g_tf_c.persist.repl, &g_tf_hep_a, "sb_a", 7500) >= 0);
TEST_ASSERT_TRUE(
repl_add_standby(&g_tf_c.persist.repl, &g_tf_hep_b, "sb_b", 7501) >= 0);
TEST_ASSERT_EQUAL_INT(0,
commit_engine_init(&g_tf_hce, &g_tf_c.persist, 3, 8));
repl_set_commit_engine(&g_tf_c.persist.repl, &g_tf_hce);
repl_broadcast_send_hook = tf_drop_send_hook;
ha_config_t hcfg = {
.heartbeat_interval_ms = 3000,
.heartbeat_timeout_ms = 10000,
.election_timeout_ms = 2000,
.probe_connect_timeout_ms = 500,
.max_promotion_retries = 5,
};
TEST_ASSERT_EQUAL_INT(0, ha_init(&g_tf_c.ha, &hcfg, 0));
g_tf_c.ha.raft_c = &g_tf_c;
g_tf_c.ha.repl = NULL; /* no healthy peer reachable at this level */
TEST_ASSERT_EQUAL_INT(0,
delaymgr_init(&g_tf_c.delaymgr, 5, tf_run_delay_task));
TEST_ASSERT_EQUAL_INT(0, delaymgr_start(&g_tf_c.delaymgr));
job_array_apply_test_clear_force_fail();
job_array_apply_test_clear_bulk_insert_force_fail();
}
static void tf_teardown(void)
{
job_array_apply_test_clear_force_fail();
job_array_apply_test_clear_bulk_insert_force_fail();
ha_cancel_promotion_fence_timer(&g_tf_c.ha);
delaymgr_stop(&g_tf_c.delaymgr);
delaymgr_fini(&g_tf_c.delaymgr);
ha_fini(&g_tf_c.ha);
repl_broadcast_send_hook = NULL;
commit_engine_fini(&g_tf_hce);
persist_stop(&g_tf_c.persist);
persist_fini(&g_tf_c.persist);
zarray_fini(&g_tf_c.sharedinst);
if (g_tf_c.resmgr) dereference(g_tf_c.resmgr);
if (g_tf_c.workermgr) dereference(g_tf_c.workermgr);
if (g_tf_c.qmgr) dereference(g_tf_c.qmgr);
if (g_tf_c.jobmgr) dereference(g_tf_c.jobmgr);
xce_hook_fini(&g_tf_c.hooksys);
}
static job_t *tf_find_job_in(cluster_t *c, uint64_t jobid)
{
job_t *j = NULL;
lock(c->jobmgr, RTTI_LOCK_READ);
int found = key_find(c->jobmgr, (void *)jobid, (void **)&j);
unlock(c->jobmgr);
return (found > 0) ? j : NULL;
}
static job_t *tf_find_job(uint64_t jobid)
{
return tf_find_job_in(&g_tf_c, jobid);
}
static job_submit_data_t tf_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-fence-e2e";
return s;
}
/* Encodes a (base_jobid, array_size, spec) JobSubmit payload — the exact
* WAL_JOB_ARRAY_SUBMIT payload shape (mirrors test_apply_pump.c's
* tap_encode_job_submit / test_job_array_apply.c's taj_encode_job_submit).
* Caller frees *out. */
static int tf_encode_job_submit(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;
}
/* ── The single capstone test: engage → refuse → no-partial-set →
* other-node-unaffected → clear+retry → self-heal → eligible again ──────── */
static void test_job_array_fence_engage_refuse_clear_recover_end_to_end(void)
{
tf_setup();
const uint64_t base_jobid = 970000;
const uint64_t array_size = 5;
const int fail_index = 2; /* mid-array: proves a genuine halt, not a
* lucky last-element edge case */
job_submit_data_t spec = tf_make_spec();
uint8_t *payload = NULL;
size_t len = 0;
int enc_rc = tf_encode_job_submit(base_jobid, array_size, &spec, &payload,
&len);
bool fenced_precondition = persist_apply_resource_fenced(&g_tf_c.persist);
/* ── Step 1: force a resource-exhaustion-style failure during array
* expansion (construction-phase seam — see file header). ──────────── */
job_array_apply_test_set_force_fail_at(fail_index);
int rc_array = persist_wal_async(&g_tf_c.persist, WAL_JOB_ARRAY_SUBMIT,
payload, (uint32_t)len);
uint64_t array_lsn = g_tf_c.persist.current_lsn - 1;
uint64_t baseline_applied = persist_last_applied_lsn(&g_tf_c.persist);
/* White-box commit (established test_apply_pump.c pattern for this exact
* WAL_JOB_ARRAY_SUBMIT/fence area — see file header). */
atomic_store(&g_tf_c.persist.committed_lsn, array_lsn);
int walk_rc = persist_apply_pump_run(&g_tf_c.persist);
uint64_t applied_after_failure = persist_last_applied_lsn(&g_tf_c.persist);
bool fenced_after_failure = persist_apply_resource_fenced(&g_tf_c.persist);
/* ── Step 4: no partial or divergent set of elements was constructed —
* checked now, before anything else touches g_tf_c's jobmgr. ──────── */
bool any_element_visible_after_failure = false;
for (uint64_t i = 0; i < array_size; i++) {
if (tf_find_job(base_jobid + i) != NULL)
any_element_visible_after_failure = true;
}
/* ── Step 3: refused promotion (task 4.3 site 1 — the periodic
* fence-release check, ha_arm_promotion_fence_timer_for_test). The
* structural conditions are made fully healthy (not truncated,
* durable_floor >= hwm) so the ONLY thing that can keep the member
* fenced is the live apply resource fence just engaged above.
*
* hwm=0 is deliberate, not a placeholder: this fixture runs persist
* with a REAL background fsync worker (needed for the real WAL append
* above), so durable_lsn is genuinely, monotonically advancing on its
* own schedule — any fixed non-zero value this test tried to inject
* white-box would race the real flusher and could be clobbered before
* promotion_fence_timeout_fn re-reads it at fire time (observed
* directly during development of this test). durable_lsn is monotonic
* and never negative, so hwm=0 makes "durable_floor >= hwm" true by
* construction at both arm time and fire time, regardless of the real
* flusher's timing — isolating the ONLY variable this test cares about
* (the live apply resource fence) exactly as the sibling precedent
* tests (test_commit_durability.c, test_convergence_verification.c)
* isolate it, just without relying on a fixture with no live WAL
* activity to keep an injected value stable. */
g_tf_c.persist.recovery_verdict = RECOVER_CLEAN;
ha_set_promotion_fence_window_ms(60); /* short window so the test is fast */
extern void ha_arm_promotion_fence_timer_for_test(cluster_t *, bool,
uint64_t, uint64_t);
ha_arm_promotion_fence_timer_for_test(&g_tf_c, /*truncated=*/false,
/*durable_floor=*/0,
/*hwm=*/0);
int in_catch_up_on_arm =
atomic_load_explicit(&g_tf_c.ha.in_catch_up, memory_order_acquire);
usleep(250 * 1000); /* let the bounded re-alert window elapse and fire */
int in_catch_up_after_window =
atomic_load_explicit(&g_tf_c.ha.in_catch_up, memory_order_acquire);
bool fenced_after_promotion_check =
persist_apply_resource_fenced(&g_tf_c.persist);
/* ── Step 5: the failure does not affect another node's independent
* progress. Clear the (process-global, per job.c's own doc comment)
* fault-injection seam, then apply the SAME committed array payload on a
* wholly separate cluster_t — g_tf_c's own fence state must be
* untouched by that node's independent success. ────────────────────── */
job_array_apply_test_clear_force_fail();
cluster_t node2;
memset(&node2, 0, sizeof(node2));
kvmgr_params_t node2_int_params = { .key_type = KVMGR_KEY_INT,
.nslots = 32,
.delta = 16 };
node2.jobmgr = makeInstance(&xkvmgr_type, &node2_int_params);
TEST_ASSERT_NOT_NULL(node2.jobmgr);
xlist_t node2_new;
memset(&node2_new, 0, sizeof(node2_new));
int node2_rc = restore_job_array(&node2, payload, len, /*entry_lsn=*/1,
&node2_new);
bool node2_all_visible = true;
for (uint64_t i = 0; i < array_size; i++) {
if (tf_find_job_in(&node2, base_jobid + i) == NULL)
node2_all_visible = false;
}
bool node1_still_fenced_after_node2_progress =
persist_apply_resource_fenced(&g_tf_c.persist);
xlist_fini(&node2_new);
dereference(node2.jobmgr);
/* ── Step 6: clear the forced condition and retry — the fence self-heals
* and the apply completes; then re-check promotion eligibility. ────── */
int walk_rc_retry = persist_apply_pump_run(&g_tf_c.persist);
uint64_t applied_after_retry = persist_last_applied_lsn(&g_tf_c.persist);
bool fenced_after_retry = persist_apply_resource_fenced(&g_tf_c.persist);
bool all_elements_visible_after_retry = true;
for (uint64_t i = 0; i < array_size; i++) {
if (tf_find_job(base_jobid + i) == NULL)
all_elements_visible_after_retry = false;
}
ha_arm_promotion_fence_timer_for_test(&g_tf_c, /*truncated=*/false,
/*durable_floor=*/0,
/*hwm=*/0);
usleep(250 * 1000);
int in_catch_up_after_self_heal =
atomic_load_explicit(&g_tf_c.ha.in_catch_up, memory_order_acquire);
ha_set_promotion_fence_window_ms(0); /* restore default */
free(payload);
tf_teardown();
/* ── Assertions ───────────────────────────────────────────────────── */
TEST_ASSERT_EQUAL_INT(0, enc_rc);
TEST_ASSERT_EQUAL_INT(0, rc_array);
TEST_ASSERT_FALSE_MESSAGE(fenced_precondition,
"a fresh persist_t must start with the Live Apply Resource Fence "
"CLEAR");
/* Step 1/2: forced failure genuinely halts the walk and engages the
* fence. */
TEST_ASSERT_TRUE_MESSAGE(walk_rc < 0,
"the forced construction-phase failure must halt the apply walk "
"with a negative return, not silently succeed");
TEST_ASSERT_EQUAL_UINT64_MESSAGE(baseline_applied, applied_after_failure,
"last_applied_lsn must NOT advance past the failed array LSN");
TEST_ASSERT_TRUE_MESSAGE(fenced_after_failure,
"the Live Apply Resource Fence must engage the moment the apply "
"walk halts on the forced resource-exhaustion failure (Req 9.4)");
/* Step 4: no partial/divergent set of elements. */
TEST_ASSERT_FALSE_MESSAGE(any_element_visible_after_failure,
"restore_job_array's scratch-build atomicity must leave ZERO array "
"elements visible after a construction-phase failure — no partial "
"or divergent set (Req 9.6)");
/* Step 3: refused promotion, structurally-healthy-but-for-the-fence. */
TEST_ASSERT_EQUAL_INT_MESSAGE(1, in_catch_up_on_arm,
"arming the promotion fence timer must immediately fence the "
"member (in_catch_up=1)");
TEST_ASSERT_EQUAL_INT_MESSAGE(1, in_catch_up_after_window,
"a node with the Live Apply Resource Fence engaged must remain "
"refused for promotion even though every OTHER structural "
"condition (not truncated, durable_floor >= hwm) is healthy — the "
"periodic fence-release check must not clear it (Req 9.4/9.5)");
TEST_ASSERT_TRUE_MESSAGE(fenced_after_promotion_check,
"the fence itself must still read engaged after the promotion "
"gate check — only the apply pump's own successful retry may "
"clear it");
/* Step 5: another node's independent progress is unaffected. */
TEST_ASSERT_EQUAL_INT_MESSAGE((int)array_size, node2_rc,
"a wholly separate node applying the SAME committed array record "
"(with the process-global fault-injection seam cleared) must "
"construct the full set independently of node 1's fenced state");
TEST_ASSERT_TRUE_MESSAGE(node2_all_visible,
"node 2's independent apply must produce every array element");
TEST_ASSERT_TRUE_MESSAGE(node1_still_fenced_after_node2_progress,
"node 1's own fence (its own persist_t's state) must be completely "
"unaffected by node 2's independent, unrelated progress (Req 9.5)");
/* Step 6: clear + retry + self-heal + eligible again. */
TEST_ASSERT_TRUE_MESSAGE(walk_rc_retry >= 0,
"once the forced condition is removed, the retry of the SAME "
"stuck LSN must succeed");
TEST_ASSERT_EQUAL_UINT64_MESSAGE(array_lsn, applied_after_retry,
"last_applied_lsn must advance to the array LSN once the retry "
"succeeds");
TEST_ASSERT_TRUE_MESSAGE(all_elements_visible_after_retry,
"every array element must be visible once the retry succeeds");
TEST_ASSERT_FALSE_MESSAGE(fenced_after_retry,
"the Live Apply Resource Fence must self-clear, from inside the "
"apply walk itself, the instant the retry of the same stuck LSN "
"succeeds — no external/admin action (Req 9.5)");
TEST_ASSERT_EQUAL_INT_MESSAGE(0, in_catch_up_after_self_heal,
"once the fence clears, the node must become eligible for "
"promotion again with no other action (task 4.3's Observable "
"clause)");
printf("test_job_array_fence_engage_refuse_clear_recover_end_to_end: "
"PASS\n");
}
#endif /* XCE_TESTING */
void test_job_array_fence(void)
{
UnitySetTestFile(__FILE__);
#ifdef XCE_TESTING
RUN_TEST(test_job_array_fence_engage_refuse_clear_recover_end_to_end);
#else
printf("test_job_array_fence: skipped (no XCE_TESTING)\n");
#endif
}