/* * test_libxce_retry.c — reusable libxce-failover test harness + harness * self-tests (spec libxce-failover, task 1.2 ADAPTED). * * This file builds the GREEN, reusable harness that the later component tasks * consume to write their own RED→GREEN tests: * - task 2.1 (StatusClassifier) → drives FO_STATUS_TABLE * - task 2.2 (EndpointRotator) → drives the fake-master cursor * - task 2.3 (reply-capture) → drives the redirect-hint rows * - task 3.1/3.2 (decision helper) → drives the table + cursor * - task 5.1/6.1 (concurrency) → drive fo_run_threads() * * It deliberately references NO not-yet-existing failover production symbols * (xce_failover_classify / xcecli_retry_next / xce_failover_rotate); those are * added by the component tasks. The lib build+test MUST stay GREEN after this * task, so every fixture here is exercised by at least one real assertion. * * Requirements covered by the harness: 2.5, 4.1, 4.2, 4.3, 4.4, 4.5, 5.2, 8.3, * 8.4 (status taxonomy); 2.1, 2.4 (master cursor); 6.3 (concurrency reuse). * The retained retry-defaults test still covers 6.1, 6.2. */ #define _GNU_SOURCE #include #include #include #include #include #include #include #include "client_internal.h" /* api_ctx_t, xce_failover_state_t */ #include "failover_harness.h" #include "request.h" #include "xce/client_failover.h" #include "xce/errnox.h" /* ════════════════════════════════════════════════════════════════════════ * Harness implementation * ════════════════════════════════════════════════════════════════════════ */ /* ── 1. Fake multi-master config + cursor ─────────────────────────────── */ int fo_make_fake_masters(xce_cluster_config_t *cfg) { static const struct { const char *host; int port; } masters[FO_FAKE_MASTERS] = { { "10.20.0.1", 7001 }, { "10.20.0.2", 7002 }, { "10.20.0.3", 7003 }, }; memset(cfg, 0, sizeof(*cfg)); for (int i = 0; i < FO_FAKE_MASTERS && i < MAX_HA_CANDIDATES; i++) { strncpy(cfg->candidates[i].hostname, masters[i].host, XCE_MAXHOSTNAME_LEN - 1); cfg->candidates[i].hostname[XCE_MAXHOSTNAME_LEN - 1] = '\0'; cfg->candidates[i].port = masters[i].port; } cfg->candidates_count = (FO_FAKE_MASTERS < MAX_HA_CANDIDATES) ? FO_FAKE_MASTERS : MAX_HA_CANDIDATES; return cfg->candidates_count; } void fo_cursor_init(fo_master_cursor_t *cur, const xce_cluster_config_t *cfg) { cur->cfg = cfg; cur->cur = 0; } int fo_cursor_advance(fo_master_cursor_t *cur) { if (!cur->cfg || cur->cfg->candidates_count <= 0) return 0; cur->cur = (cur->cur + 1) % cur->cfg->candidates_count; return cur->cur; } const xce_candidate_t *fo_cursor_current(const fo_master_cursor_t *cur) { if (!cur->cfg || cur->cfg->candidates_count <= 0) return NULL; return &cur->cfg->candidates[cur->cur]; } /* ── 2. Controller-status taxonomy table ──────────────────────────────── */ const fo_status_row_t FO_STATUS_TABLE[] = { /* success */ { "OK", 0, false, false, FO_ACT_SUCCESS, "request served by primary" }, /* redirect (a primary exists elsewhere) — rotate now, hint first if any */ { "E_XCE_REDIRECT+hint", E_XCE_REDIRECT, true, false, FO_ACT_RETRY_NOW, "standby redirects with primary address hint (8.3)" }, { "E_XCE_REDIRECT", E_XCE_REDIRECT, false, false, FO_ACT_RETRY_NOW, "standby redirects without address — rotate master list (8.4)" }, /* standby (op is primary-only) — treat as redirect, rotate now (4.3) */ { "E_XCE_STANDBY", E_XCE_STANDBY, false, false, FO_ACT_RETRY_NOW, "controller in standby; op is primary-only (4.3)" }, /* no-primary / not-ready — rotate after backoff (4.1) */ { "E_XCE_NOMASTER", E_XCE_NOMASTER, false, false, FO_ACT_RETRY_BACKOFF, "standby knows no primary yet — backoff + rotate (4.1)" }, { "E_XCE_NOT_READY", E_XCE_NOT_READY, false, false, FO_ACT_RETRY_BACKOFF, "controller initializing/electing — backoff + rotate (4.1)" }, /* idempotent-replay duplicate (write replay) — DUP/EEXIST (5.2) */ { "E_XCE_DUP_JOB", E_XCE_DUP_JOB, false, false, FO_ACT_TERMINAL, "duplicate job; SUCCESS only on a re-sent write (5.2, attempt-aware)" }, { "-EEXIST", -EEXIST, false, true, FO_ACT_TERMINAL, "duplicate via -EEXIST; SUCCESS only on a re-sent write (5.2)" }, /* transport errors — reconnect/rotate now (4.4) */ { "-ECONNREFUSED", -ECONNREFUSED, false, true, FO_ACT_RETRY_NOW, "connection refused — reconnect/rotate (4.4)" }, { "-ETIMEDOUT", -ETIMEDOUT, false, true, FO_ACT_RETRY_NOW, "connection timeout — reconnect/rotate (4.4)" }, { "-ECONNRESET", -ECONNRESET, false, true, FO_ACT_RETRY_NOW, "peer reset — reconnect/rotate (4.4)" }, /* terminal / non-retriable — surface immediately (4.5) */ { "-EINVAL", -EINVAL, false, true, FO_ACT_TERMINAL, "invalid request — surface, no retry (4.5)" }, }; const size_t FO_STATUS_TABLE_LEN = sizeof(FO_STATUS_TABLE) / sizeof(FO_STATUS_TABLE[0]); const fo_status_row_t *fo_status_lookup(int status) { for (size_t i = 0; i < FO_STATUS_TABLE_LEN; i++) { if (FO_STATUS_TABLE[i].status == status) return &FO_STATUS_TABLE[i]; } return NULL; } /* ── 3. Generic N-thread concurrency harness ──────────────────────────── */ typedef struct { fo_thread_fn fn; void *ctx; int idx; } fo_thread_arg_t; static void *fo_thread_trampoline(void *p) { fo_thread_arg_t *a = (fo_thread_arg_t *)p; a->fn(a->ctx, a->idx); return NULL; } int fo_run_threads(int n, fo_thread_fn fn, void *ctx) { if (n <= 0 || !fn) return -EINVAL; pthread_t *tids = calloc((size_t)n, sizeof(*tids)); fo_thread_arg_t *args = calloc((size_t)n, sizeof(*args)); if (!tids || !args) { free(tids); free(args); return -ENOMEM; } int spawned = 0; int rc = 0; for (int i = 0; i < n; i++) { args[i].fn = fn; args[i].ctx = ctx; args[i].idx = i; int e = pthread_create(&tids[i], NULL, fo_thread_trampoline, &args[i]); if (e != 0) { rc = -e; break; } spawned++; } for (int i = 0; i < spawned; i++) pthread_join(tids[i], NULL); free(tids); free(args); return rc; } /* ════════════════════════════════════════════════════════════════════════ * Harness self-tests (GREEN) — each fixture proven by real assertions * ════════════════════════════════════════════════════════════════════════ */ /* Req 6.1, 6.2: the retry-tunables struct + safe production defaults are * retained and reachable for the failover loop. */ static void test_retry_defaults_are_reachable(void) { xce_retry_config_t cfg = XCE_RETRY_DEFAULTS; TEST_ASSERT_EQUAL_INT(20, cfg.max_retries); TEST_ASSERT_EQUAL_INT(50, cfg.initial_retry_ms); TEST_ASSERT_EQUAL_INT(5000, cfg.max_retry_ms); TEST_ASSERT_TRUE(cfg.backoff_multiplier > 1.0); printf("test_retry_defaults_are_reachable: PASS\n"); } /* Fixture 1: fake multi-master config holds 3 distinct candidates. */ static void test_fake_masters_hold_three_candidates(void) { xce_cluster_config_t cfg; int n = fo_make_fake_masters(&cfg); TEST_ASSERT_EQUAL_INT(FO_FAKE_MASTERS, n); TEST_ASSERT_EQUAL_INT(FO_FAKE_MASTERS, cfg.candidates_count); TEST_ASSERT_EQUAL_STRING("10.20.0.1", cfg.candidates[0].hostname); TEST_ASSERT_EQUAL_INT(7001, cfg.candidates[0].port); TEST_ASSERT_EQUAL_STRING("10.20.0.3", cfg.candidates[2].hostname); TEST_ASSERT_EQUAL_INT(7003, cfg.candidates[2].port); printf("test_fake_masters_hold_three_candidates: PASS\n"); } /* Fixture 1: cursor advances through masters and wraps (2.1, 2.4, 8.4). */ static void test_master_cursor_advances_and_wraps(void) { xce_cluster_config_t cfg; fo_make_fake_masters(&cfg); fo_master_cursor_t cur; fo_cursor_init(&cur, &cfg); TEST_ASSERT_EQUAL_INT(0, cur.cur); TEST_ASSERT_EQUAL_STRING("10.20.0.1", fo_cursor_current(&cur)->hostname); TEST_ASSERT_EQUAL_INT(1, fo_cursor_advance(&cur)); TEST_ASSERT_EQUAL_STRING("10.20.0.2", fo_cursor_current(&cur)->hostname); TEST_ASSERT_EQUAL_INT(2, fo_cursor_advance(&cur)); TEST_ASSERT_EQUAL_STRING("10.20.0.3", fo_cursor_current(&cur)->hostname); /* wrap back to master 0 */ TEST_ASSERT_EQUAL_INT(0, fo_cursor_advance(&cur)); TEST_ASSERT_EQUAL_STRING("10.20.0.1", fo_cursor_current(&cur)->hostname); printf("test_master_cursor_advances_and_wraps: PASS\n"); } /* Fixture 2: taxonomy table is complete — every status the classifier must * handle is present, lookups resolve, and the status codes equal the real * errnox.h values (2.5, 4.1-4.5, 5.2, 8.3, 8.4). */ static void test_status_table_is_complete_and_resolves(void) { /* every mandated controller status + transport error is represented */ TEST_ASSERT_NOT_NULL(fo_status_lookup(0)); TEST_ASSERT_NOT_NULL(fo_status_lookup(E_XCE_REDIRECT)); TEST_ASSERT_NOT_NULL(fo_status_lookup(E_XCE_STANDBY)); TEST_ASSERT_NOT_NULL(fo_status_lookup(E_XCE_NOMASTER)); TEST_ASSERT_NOT_NULL(fo_status_lookup(E_XCE_NOT_READY)); TEST_ASSERT_NOT_NULL(fo_status_lookup(E_XCE_DUP_JOB)); TEST_ASSERT_NOT_NULL(fo_status_lookup(-EEXIST)); TEST_ASSERT_NOT_NULL(fo_status_lookup(-ECONNREFUSED)); TEST_ASSERT_NOT_NULL(fo_status_lookup(-ETIMEDOUT)); TEST_ASSERT_NOT_NULL(fo_status_lookup(-ECONNRESET)); /* status codes resolve to the real errnox.h values (ordered enum) */ TEST_ASSERT_EQUAL_INT(XCE_ERRNO_BASE, E_XCE_BAD_JOB); TEST_ASSERT_TRUE(E_XCE_REDIRECT == E_XCE_NOMASTER + 1); TEST_ASSERT_TRUE(E_XCE_NOT_READY == E_XCE_REDIRECT + 1); TEST_ASSERT_TRUE(E_XCE_STANDBY == E_XCE_NOT_READY + 1); /* expected actions line up with the design's StatusClassifier (first send) */ TEST_ASSERT_EQUAL_INT(FO_ACT_SUCCESS, fo_status_lookup(0)->expect); TEST_ASSERT_EQUAL_INT(FO_ACT_RETRY_NOW, fo_status_lookup(E_XCE_REDIRECT)->expect); TEST_ASSERT_EQUAL_INT(FO_ACT_RETRY_NOW, fo_status_lookup(E_XCE_STANDBY)->expect); TEST_ASSERT_EQUAL_INT(FO_ACT_RETRY_BACKOFF, fo_status_lookup(E_XCE_NOMASTER)->expect); TEST_ASSERT_EQUAL_INT(FO_ACT_RETRY_BACKOFF, fo_status_lookup(E_XCE_NOT_READY)->expect); TEST_ASSERT_EQUAL_INT(FO_ACT_RETRY_NOW, fo_status_lookup(-ECONNRESET)->expect); printf("test_status_table_is_complete_and_resolves: PASS\n"); } /* Fixture 2: the redirect-hint flag distinguishes hint vs. no-hint rows (8.3). */ static void test_status_table_redirect_hint_flag(void) { bool saw_hint = false; bool saw_no_hint = false; for (size_t i = 0; i < FO_STATUS_TABLE_LEN; i++) { if (FO_STATUS_TABLE[i].status == E_XCE_REDIRECT) { if (FO_STATUS_TABLE[i].has_hint) saw_hint = true; else saw_no_hint = true; } } TEST_ASSERT_TRUE(saw_hint); /* REDIRECT+hint row present (8.3) */ TEST_ASSERT_TRUE(saw_no_hint); /* REDIRECT no-hint row present (8.4) */ printf("test_status_table_redirect_hint_flag: PASS\n"); } /* Fixture 3: concurrency harness runs N threads to completion (6.3 reuse). */ #define FO_TEST_NTHREADS 16 typedef struct { pthread_mutex_t lock; int counter; } fo_selftest_ctx_t; static void fo_selftest_worker(void *ctx, int idx) { (void)idx; fo_selftest_ctx_t *c = (fo_selftest_ctx_t *)ctx; pthread_mutex_lock(&c->lock); c->counter++; pthread_mutex_unlock(&c->lock); } static void test_thread_harness_runs_all_threads(void) { fo_selftest_ctx_t ctx; pthread_mutex_init(&ctx.lock, NULL); ctx.counter = 0; int rc = fo_run_threads(FO_TEST_NTHREADS, fo_selftest_worker, &ctx); pthread_mutex_destroy(&ctx.lock); TEST_ASSERT_EQUAL_INT(0, rc); TEST_ASSERT_EQUAL_INT(FO_TEST_NTHREADS, ctx.counter); printf("test_thread_harness_runs_all_threads: PASS\n"); } /* Fixture 3: harness rejects bad arguments. */ static void test_thread_harness_rejects_bad_args(void) { TEST_ASSERT_EQUAL_INT(-EINVAL, fo_run_threads(0, fo_selftest_worker, NULL)); TEST_ASSERT_EQUAL_INT(-EINVAL, fo_run_threads(4, NULL, NULL)); printf("test_thread_harness_rejects_bad_args: PASS\n"); } /* ════════════════════════════════════════════════════════════════════════ * Task 2.1 — StatusClassifier (xce_failover_classify) RED→GREEN tests * ════════════════════════════════════════════════════════════════════════ */ /* Map the harness' expected-action category to the real classifier enum. The * two enums share order by design, but we map explicitly so a future reorder of * either enum is caught here rather than silently passing. */ static xce_fo_action_t fo_expected_to_action(fo_expected_action_t e) { switch (e) { case FO_ACT_SUCCESS: return XCE_FO_SUCCESS; case FO_ACT_RETRY_NOW: return XCE_FO_RETRY_NOW; case FO_ACT_RETRY_BACKOFF: return XCE_FO_RETRY_BACKOFF; case FO_ACT_TERMINAL: return XCE_FO_TERMINAL; } return XCE_FO_TERMINAL; } /* Req 2.5, 4.1, 4.2, 4.3, 4.5: every taxonomy row's first-send classification * (is_write=false, attempt=0) matches the design's StatusClassifier mapping. */ static void test_classify_matches_taxonomy_table(void) { for (size_t i = 0; i < FO_STATUS_TABLE_LEN; i++) { const fo_status_row_t *row = &FO_STATUS_TABLE[i]; xce_fo_action_t want = fo_expected_to_action(row->expect); xce_fo_action_t got = xce_failover_classify(row->status, /*is_write=*/false, /*attempt=*/0); char msg[128]; snprintf(msg, sizeof(msg), "row '%s' (status=%d): want %d got %d", row->name, row->status, (int)want, (int)got); TEST_ASSERT_EQUAL_INT_MESSAGE(want, got, msg); } printf("test_classify_matches_taxonomy_table: PASS\n"); } /* Req 2.5: REDIRECT/STANDBY/NOMASTER/NOT_READY are never reported as success, * regardless of is_write/attempt. */ static void test_classify_control_codes_never_success(void) { const int codes[] = { E_XCE_REDIRECT, E_XCE_STANDBY, E_XCE_NOMASTER, E_XCE_NOT_READY }; for (size_t i = 0; i < sizeof(codes) / sizeof(codes[0]); i++) { TEST_ASSERT_NOT_EQUAL(XCE_FO_SUCCESS, xce_failover_classify(codes[i], false, 0)); TEST_ASSERT_NOT_EQUAL(XCE_FO_SUCCESS, xce_failover_classify(codes[i], true, 1)); /* accepted in either sign */ TEST_ASSERT_NOT_EQUAL(XCE_FO_SUCCESS, xce_failover_classify(-codes[i], true, 1)); } printf("test_classify_control_codes_never_success: PASS\n"); } /* Req 5.2: idempotent-replay rule — DUP_JOB/EEXIST on a RE-SENT WRITE is a * success; the first send (or a read) is a genuine duplicate → terminal. */ static void test_classify_dup_replay_rule(void) { /* re-sent write (is_write && attempt>0) → SUCCESS */ TEST_ASSERT_EQUAL_INT(XCE_FO_SUCCESS, xce_failover_classify(-E_XCE_DUP_JOB, true, 1)); TEST_ASSERT_EQUAL_INT(XCE_FO_SUCCESS, xce_failover_classify(E_XCE_DUP_JOB, true, 1)); TEST_ASSERT_EQUAL_INT(XCE_FO_SUCCESS, xce_failover_classify(-EEXIST, true, 1)); /* first send → genuine duplicate → TERMINAL */ TEST_ASSERT_EQUAL_INT(XCE_FO_TERMINAL, xce_failover_classify(-E_XCE_DUP_JOB, true, 0)); /* non-write replay → TERMINAL */ TEST_ASSERT_EQUAL_INT(XCE_FO_TERMINAL, xce_failover_classify(-E_XCE_DUP_JOB, false, 1)); TEST_ASSERT_EQUAL_INT(XCE_FO_TERMINAL, xce_failover_classify(-EEXIST, false, 1)); printf("test_classify_dup_replay_rule: PASS\n"); } /* Req 3.2 / general: status 0 is always success. */ static void test_classify_zero_is_success(void) { TEST_ASSERT_EQUAL_INT(XCE_FO_SUCCESS, xce_failover_classify(0, false, 0)); TEST_ASSERT_EQUAL_INT(XCE_FO_SUCCESS, xce_failover_classify(0, true, 3)); printf("test_classify_zero_is_success: PASS\n"); } /* Req 4.5: unknown / unrecognized statuses are conservatively terminal. */ static void test_classify_unknown_is_terminal(void) { TEST_ASSERT_EQUAL_INT(XCE_FO_TERMINAL, xce_failover_classify(-EACCES, false, 0)); TEST_ASSERT_EQUAL_INT(XCE_FO_TERMINAL, xce_failover_classify(-ENOENT, true, 1)); TEST_ASSERT_EQUAL_INT(XCE_FO_TERMINAL, xce_failover_classify(-12345, false, 0)); printf("test_classify_unknown_is_terminal: PASS\n"); } /* ════════════════════════════════════════════════════════════════════════ * Task 2.2 — EndpointRotator target selection (xce_failover_next_target) * RED→GREEN tests. Pure, socket-free target-selection logic; the * live connect path is covered by E2E task 6.2. * ════════════════════════════════════════════════════════════════════════ */ /* Build a 3-master client config (distinct host:port) the rotator selects over. * Strings are static-duration so the api_ctx may borrow them by pointer. */ static void fo_make_client_masters(xce_client_config_t *conf) { static char *hosts[N_MAX_MASTERS] = { "10.30.0.1", "10.30.0.2", "10.30.0.3" }; static int ports[N_MAX_MASTERS] = { 8001, 8002, 8003 }; memset(conf, 0, sizeof(*conf)); conf->nmasters = N_MAX_MASTERS; for (int i = 0; i < N_MAX_MASTERS; i++) { conf->masters[i] = hosts[i]; conf->master_ports[i] = ports[i]; } } /* Minimal api_ctx for target-selection tests: only conf, the failover cursor, * and the ctx redirect-hint fields are exercised. No endpoint/socket touched. */ static void fo_make_api_ctx(api_ctx_t *ac, xce_client_config_t *conf) { memset(ac, 0, sizeof(*ac)); ac->conf = conf; ac->failover.cur_master = 0; } static void fo_set_hint(api_ctx_t *ac, const char *host, uint16_t port) { strncpy(ac->ctx.redirect_hint_host, host, XCE_MAXHOSTNAME_LEN - 1); ac->ctx.redirect_hint_host[XCE_MAXHOSTNAME_LEN - 1] = '\0'; ac->ctx.redirect_hint_port = port; } /* Req 2.2, 8.4: with a redirect hint set, next_target returns the hint * host/port and CONSUMES (clears) it; the following call ignores the stale * hint and advances the master cursor instead. */ static void test_next_target_hint_first_then_consumed(void) { xce_client_config_t conf; api_ctx_t ac; fo_make_client_masters(&conf); fo_make_api_ctx(&ac, &conf); fo_set_hint(&ac, "10.99.0.7", 9100); char host[XCE_MAXHOSTNAME_LEN] = { 0 }; uint16_t port = 0; int rc = xce_failover_next_target(&ac, host, sizeof(host), &port); TEST_ASSERT_EQUAL_INT(0, rc); TEST_ASSERT_EQUAL_STRING("10.99.0.7", host); TEST_ASSERT_EQUAL_INT(9100, port); /* hint consumed: cleared on the ctx, cursor untouched */ TEST_ASSERT_EQUAL_STRING("", ac.ctx.redirect_hint_host); TEST_ASSERT_EQUAL_INT(0, ac.failover.cur_master); /* next call has no hint → advances cursor to master 1 */ rc = xce_failover_next_target(&ac, host, sizeof(host), &port); TEST_ASSERT_EQUAL_INT(0, rc); TEST_ASSERT_EQUAL_STRING("10.30.0.2", host); TEST_ASSERT_EQUAL_INT(8002, port); TEST_ASSERT_EQUAL_INT(1, ac.failover.cur_master); printf("test_next_target_hint_first_then_consumed: PASS\n"); } /* Req 2.1, 8.4: with no hint, successive next_target calls advance through the * configured masters and WRAP around; the cursor lives on failover.cur_master. */ static void test_next_target_rotates_and_wraps(void) { xce_client_config_t conf; api_ctx_t ac; fo_make_client_masters(&conf); fo_make_api_ctx(&ac, &conf); char host[XCE_MAXHOSTNAME_LEN] = { 0 }; uint16_t port = 0; /* from cur_master=0, first rotation advances to master 1 */ TEST_ASSERT_EQUAL_INT(0, xce_failover_next_target(&ac, host, sizeof(host), &port)); TEST_ASSERT_EQUAL_STRING("10.30.0.2", host); TEST_ASSERT_EQUAL_INT(8002, port); TEST_ASSERT_EQUAL_INT(1, ac.failover.cur_master); TEST_ASSERT_EQUAL_INT(0, xce_failover_next_target(&ac, host, sizeof(host), &port)); TEST_ASSERT_EQUAL_STRING("10.30.0.3", host); TEST_ASSERT_EQUAL_INT(8003, port); TEST_ASSERT_EQUAL_INT(2, ac.failover.cur_master); /* wrap back to master 0 */ TEST_ASSERT_EQUAL_INT(0, xce_failover_next_target(&ac, host, sizeof(host), &port)); TEST_ASSERT_EQUAL_STRING("10.30.0.1", host); TEST_ASSERT_EQUAL_INT(8001, port); TEST_ASSERT_EQUAL_INT(0, ac.failover.cur_master); printf("test_next_target_rotates_and_wraps: PASS\n"); } /* Req 4.4, 8.4: after a hint is consumed, the very next call advances to the * next master — correctness is independent of the (one-shot) hint. */ static void test_next_target_hint_then_fallback_rotation(void) { xce_client_config_t conf; api_ctx_t ac; fo_make_client_masters(&conf); fo_make_api_ctx(&ac, &conf); /* start the cursor at master 1 to prove fallback advances from current */ ac.failover.cur_master = 1; fo_set_hint(&ac, "10.99.0.9", 9200); char host[XCE_MAXHOSTNAME_LEN] = { 0 }; uint16_t port = 0; /* hint first */ TEST_ASSERT_EQUAL_INT(0, xce_failover_next_target(&ac, host, sizeof(host), &port)); TEST_ASSERT_EQUAL_STRING("10.99.0.9", host); TEST_ASSERT_EQUAL_INT(9200, port); TEST_ASSERT_EQUAL_INT(1, ac.failover.cur_master); /* cursor untouched */ /* fallback: advances from master 1 → master 2 */ TEST_ASSERT_EQUAL_INT(0, xce_failover_next_target(&ac, host, sizeof(host), &port)); TEST_ASSERT_EQUAL_STRING("10.30.0.3", host); TEST_ASSERT_EQUAL_INT(8003, port); TEST_ASSERT_EQUAL_INT(2, ac.failover.cur_master); printf("test_next_target_hint_then_fallback_rotation: PASS\n"); } /* Guard: NULL/invalid arguments are rejected without touching state. */ static void test_next_target_rejects_bad_args(void) { xce_client_config_t conf; api_ctx_t ac; fo_make_client_masters(&conf); fo_make_api_ctx(&ac, &conf); char host[XCE_MAXHOSTNAME_LEN] = { 0 }; uint16_t port = 0; TEST_ASSERT_EQUAL_INT(-EINVAL, xce_failover_next_target(NULL, host, sizeof(host), &port)); TEST_ASSERT_EQUAL_INT(-EINVAL, xce_failover_next_target(&ac, NULL, sizeof(host), &port)); TEST_ASSERT_EQUAL_INT(-EINVAL, xce_failover_next_target(&ac, host, 0, &port)); TEST_ASSERT_EQUAL_INT(-EINVAL, xce_failover_next_target(&ac, host, sizeof(host), NULL)); printf("test_next_target_rejects_bad_args: PASS\n"); } /* ── Suite entry point ──────────────────────────────────────────────── */ void test_libxce_retry(void) { UNITY_BEGIN(); RUN_TEST(test_retry_defaults_are_reachable); RUN_TEST(test_fake_masters_hold_three_candidates); RUN_TEST(test_master_cursor_advances_and_wraps); RUN_TEST(test_status_table_is_complete_and_resolves); RUN_TEST(test_status_table_redirect_hint_flag); RUN_TEST(test_thread_harness_runs_all_threads); RUN_TEST(test_thread_harness_rejects_bad_args); RUN_TEST(test_classify_matches_taxonomy_table); RUN_TEST(test_classify_control_codes_never_success); RUN_TEST(test_classify_dup_replay_rule); RUN_TEST(test_classify_zero_is_success); RUN_TEST(test_classify_unknown_is_terminal); RUN_TEST(test_next_target_hint_first_then_consumed); RUN_TEST(test_next_target_rotates_and_wraps); RUN_TEST(test_next_target_hint_then_fallback_rotation); RUN_TEST(test_next_target_rejects_bad_args); UNITY_END(); }