myxce/src/lib/testsuite/test_cluster.c
Rongsong Shen 210faa2b7a
feat(component-failover-adoption): add candidate-list-to-client-config helper
xce_cluster_config_to_client_config() converts a validated candidate
list into a fully-populated xce_client_config_t, copying candidates
in order with no special-casing for a single candidate. The
truncate-with-warning safety net stays in place as a defensive guard
now that N_MAX_MASTERS and MAX_HA_CANDIDATES match.

Also removes a WARN check in xce_cluster_config_validate() that
became unreachable dead code once task 1.1 reconciled the two
candidate-count constants (both checks tested the same field against
the same constant with no mutation between them).

Task 1.2 of component-failover-adoption (Requirement 1 AC1-AC7).
2026-08-11 04:57:31 +00:00

313 lines
12 KiB
C

#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <unity/unity.h>
#include "client.h"
#include "xce/cluster.h"
/* ── Test seam: mock connect function ───────────────────────────── */
static int s_connect_call_count = 0;
static int s_connect_fail_n = 0; /* fail first N attempts, then succeed */
static char s_last_host[256] = "";
static int s_last_port = 0;
static void* mock_connect(const char* host, int port)
{
strncpy(s_last_host, host, sizeof(s_last_host) - 1);
s_last_port = port;
s_connect_call_count++;
if (s_connect_call_count <= s_connect_fail_n)
return NULL;
return (void*)0xDEADBEEF; /* non-NULL = success */
}
static xce_cluster_config_t make_valid_config(void)
{
xce_cluster_config_t cfg = {0};
strncpy(cfg.candidates[0].hostname, "10.0.1.1",
XCE_MAXHOSTNAME_LEN - 1);
cfg.candidates[0].port = 7000;
strncpy(cfg.candidates[1].hostname, "10.0.1.2",
XCE_MAXHOSTNAME_LEN - 1);
cfg.candidates[1].port = 7001;
cfg.candidates_count = 2;
strncpy(cfg.identity, "worker-01", sizeof(cfg.identity) - 1);
return cfg;
}
/* ── Test cases ─────────────────────────────────────────────────── */
/* Test 1: validate rejects empty config */
static void test_validate_rejects_zero_candidates(void)
{
xce_cluster_config_t cfg = {0};
cfg.candidates_count = 0;
TEST_ASSERT_NOT_EQUAL(0, xce_cluster_config_validate(&cfg));
printf("test_validate_rejects_zero_candidates: PASS\n");
}
/* Test 2: validate rejects too many candidates */
static void test_validate_rejects_over_max_candidates(void)
{
xce_cluster_config_t cfg = {0};
cfg.candidates_count = MAX_HA_CANDIDATES + 1;
TEST_ASSERT_NOT_EQUAL(0, xce_cluster_config_validate(&cfg));
printf("test_validate_rejects_over_max_candidates: PASS\n");
}
/* Test 3: validate rejects missing identity */
static void test_validate_rejects_empty_identity(void)
{
xce_cluster_config_t cfg = make_valid_config();
cfg.identity[0] = '\0';
TEST_ASSERT_NOT_EQUAL(0, xce_cluster_config_validate(&cfg));
printf("test_validate_rejects_empty_identity: PASS\n");
}
/* Test 4: validate accepts valid config */
static void test_validate_accepts_valid_config(void)
{
xce_cluster_config_t cfg = make_valid_config();
TEST_ASSERT_EQUAL_INT(0, xce_cluster_config_validate(&cfg));
printf("test_validate_accepts_valid_config: PASS\n");
}
/* Test 5: connect succeeds on first candidate */
static void test_connect_succeeds_first_candidate(void)
{
xce_cluster_config_t cfg = make_valid_config();
s_connect_call_count = 0;
s_connect_fail_n = 0;
xce_cluster_set_connect_fn(mock_connect);
void* ep = xce_connect_cluster(&cfg);
TEST_ASSERT_NOT_NULL(ep);
TEST_ASSERT_EQUAL_INT(1, s_connect_call_count);
TEST_ASSERT_EQUAL_STRING("10.0.1.1", s_last_host);
TEST_ASSERT_EQUAL_INT(7000, s_last_port);
xce_cluster_set_connect_fn(NULL);
printf("test_connect_succeeds_first_candidate: PASS\n");
}
/* Test 6: connect falls back to second candidate when first fails */
static void test_connect_fallback_to_second_candidate(void)
{
xce_cluster_config_t cfg = make_valid_config();
s_connect_call_count = 0;
s_connect_fail_n = 1; /* first attempt fails */
xce_cluster_set_connect_fn(mock_connect);
void* ep = xce_connect_cluster(&cfg);
TEST_ASSERT_NOT_NULL(ep);
TEST_ASSERT_EQUAL_INT(2, s_connect_call_count);
TEST_ASSERT_EQUAL_STRING("10.0.1.2", s_last_host);
xce_cluster_set_connect_fn(NULL);
printf("test_connect_fallback_to_second_candidate: PASS\n");
}
/* Test 7: connect returns NULL when all candidates fail */
static void test_connect_returns_null_all_fail(void)
{
xce_cluster_config_t cfg = make_valid_config();
s_connect_call_count = 0;
s_connect_fail_n = 999; /* all fail */
xce_cluster_set_connect_fn(mock_connect);
void* ep = xce_connect_cluster(&cfg);
TEST_ASSERT_NULL(ep);
xce_cluster_set_connect_fn(NULL);
printf("test_connect_returns_null_all_fail: PASS\n");
}
/* Test 8: reconnect state machine — IDLE → CONNECTING → CONNECTED */
static void test_reconnect_step_reaches_connected(void)
{
xce_cluster_config_t cfg = make_valid_config();
xce_reconnect_ctx_t rctx;
s_connect_call_count = 0;
s_connect_fail_n = 0;
xce_cluster_set_connect_fn(mock_connect);
xce_reconnect_init(&rctx, &cfg);
TEST_ASSERT_EQUAL_INT(XCE_RECONNECT_IDLE, (int)rctx.state);
int steps = 0;
while (rctx.state != XCE_RECONNECT_CONNECTED &&
rctx.state != XCE_RECONNECT_FAILED && steps < 100) {
xce_reconnect_step(&rctx);
steps++;
}
TEST_ASSERT_EQUAL_INT(XCE_RECONNECT_CONNECTED, (int)rctx.state);
TEST_ASSERT_NOT_NULL(rctx.endpoint);
xce_reconnect_fini(&rctx);
xce_cluster_set_connect_fn(NULL);
printf("test_reconnect_step_reaches_connected: PASS\n");
}
/* Test 9: reconnect state machine backs off on failure */
static void test_reconnect_step_backs_off_on_failure(void)
{
xce_cluster_config_t cfg = make_valid_config();
cfg.candidates_count = 1; /* only one candidate so it hits backoff */
xce_reconnect_ctx_t rctx;
s_connect_call_count = 0;
s_connect_fail_n = 999; /* always fail */
xce_cluster_set_connect_fn(mock_connect);
xce_reconnect_init(&rctx, &cfg);
xce_reconnect_step(&rctx); /* IDLE → CONNECTING (attempt connect) */
xce_reconnect_step(&rctx); /* CONNECTING → BACKOFF (all candidates exhausted) */
/* When mock is set, sleep is skipped — state must be BACKOFF */
TEST_ASSERT_EQUAL_INT(XCE_RECONNECT_BACKOFF, (int)rctx.state);
TEST_ASSERT_TRUE(rctx.backoff_ms > 0);
xce_reconnect_fini(&rctx);
xce_cluster_set_connect_fn(NULL);
printf("test_reconnect_step_backs_off_on_failure: PASS\n");
}
/* ── xce_cluster_config_to_client_config() tests ──────────────────
* Builds a cfg with `n` candidates at distinct host/port values, so the
* copy-in-order behavior can be verified precisely. */
static xce_cluster_config_t make_config_with_n_candidates(int n)
{
xce_cluster_config_t cfg = {0};
for (int i = 0; i < n && i < MAX_HA_CANDIDATES; i++) {
char host[XCE_MAXHOSTNAME_LEN];
snprintf(host, sizeof(host), "10.0.2.%d", i + 1);
strncpy(cfg.candidates[i].hostname, host, XCE_MAXHOSTNAME_LEN - 1);
cfg.candidates[i].port = 9000 + i;
}
cfg.candidates_count = n;
strncpy(cfg.identity, "conv-test", sizeof(cfg.identity) - 1);
return cfg;
}
static void assert_converted_masters_match(const xce_cluster_config_t* cfg,
const xce_client_config_t* out,
int expected_count)
{
TEST_ASSERT_EQUAL_INT(expected_count, out->nmasters);
for (int i = 0; i < expected_count; i++) {
TEST_ASSERT_EQUAL_STRING(cfg->candidates[i].hostname,
out->masters[i]);
TEST_ASSERT_EQUAL_INT(cfg->candidates[i].port, out->master_ports[i]);
}
}
/* Test 10: one candidate goes through the identical code path as any other
* count -- no special-cased "single controller" branch (Req 1.5). */
static void test_convert_one_candidate(void)
{
xce_cluster_config_t cfg = make_config_with_n_candidates(1);
xce_client_config_t out = {0};
int rc = xce_cluster_config_to_client_config(&cfg, &out);
TEST_ASSERT_EQUAL_INT(0, rc);
assert_converted_masters_match(&cfg, &out, 1);
printf("test_convert_one_candidate: PASS\n");
}
/* Test 11: three candidates -- ordinary multi-candidate case. */
static void test_convert_three_candidates(void)
{
xce_cluster_config_t cfg = make_config_with_n_candidates(3);
xce_client_config_t out = {0};
int rc = xce_cluster_config_to_client_config(&cfg, &out);
TEST_ASSERT_EQUAL_INT(0, rc);
assert_converted_masters_match(&cfg, &out, 3);
printf("test_convert_three_candidates: PASS\n");
}
/* Test 12: four candidates -- the N_MAX_MASTERS boundary, still the
* ordinary (non-clamped) path since N_MAX_MASTERS == MAX_HA_CANDIDATES. */
static void test_convert_four_candidates(void)
{
xce_cluster_config_t cfg = make_config_with_n_candidates(4);
xce_client_config_t out = {0};
int rc = xce_cluster_config_to_client_config(&cfg, &out);
TEST_ASSERT_EQUAL_INT(0, rc);
TEST_ASSERT_EQUAL_INT(N_MAX_MASTERS, 4);
assert_converted_masters_match(&cfg, &out, 4);
printf("test_convert_four_candidates: PASS\n");
}
/* Test 13: NULL cfg is rejected. */
static void test_convert_rejects_null_cfg(void)
{
xce_client_config_t out = {0};
int rc = xce_cluster_config_to_client_config(NULL, &out);
TEST_ASSERT_EQUAL_INT(-EINVAL, rc);
printf("test_convert_rejects_null_cfg: PASS\n");
}
/* Test 14: NULL out is rejected. */
static void test_convert_rejects_null_out(void)
{
xce_cluster_config_t cfg = make_config_with_n_candidates(1);
int rc = xce_cluster_config_to_client_config(&cfg, NULL);
TEST_ASSERT_EQUAL_INT(-EINVAL, rc);
printf("test_convert_rejects_null_out: PASS\n");
}
/* Test 15: candidates_count < 1 is rejected. */
static void test_convert_rejects_zero_candidates(void)
{
xce_cluster_config_t cfg = make_config_with_n_candidates(0);
xce_client_config_t out = {0};
int rc = xce_cluster_config_to_client_config(&cfg, &out);
TEST_ASSERT_EQUAL_INT(-EINVAL, rc);
printf("test_convert_rejects_zero_candidates: PASS\n");
}
/* Test 16: an artificially invalid over-count input (candidates_count >
* N_MAX_MASTERS, bypassing xce_cluster_config_validate()) exercises the
* defensive truncation path -- clamps to N_MAX_MASTERS and still populates
* the first N_MAX_MASTERS candidates correctly. A properly validated input
* can never reach this path today since N_MAX_MASTERS == MAX_HA_CANDIDATES. */
static void test_convert_truncates_over_max_candidates(void)
{
xce_cluster_config_t cfg = make_config_with_n_candidates(MAX_HA_CANDIDATES);
cfg.candidates_count = MAX_HA_CANDIDATES + 1; /* artificially invalid */
xce_client_config_t out = {0};
int rc = xce_cluster_config_to_client_config(&cfg, &out);
TEST_ASSERT_EQUAL_INT(0, rc);
assert_converted_masters_match(&cfg, &out, N_MAX_MASTERS);
printf("test_convert_truncates_over_max_candidates: PASS\n");
}
/* Test 17: the helper does not set identity/auth fields -- caller sets
* out->from / out->key separately. */
static void test_convert_does_not_set_identity_fields(void)
{
xce_cluster_config_t cfg = make_config_with_n_candidates(2);
xce_client_config_t out = {0};
char sentinel_from[] = "sentinel-from";
char sentinel_key[] = "sentinel-key";
out.from = sentinel_from;
out.key = sentinel_key;
int rc = xce_cluster_config_to_client_config(&cfg, &out);
TEST_ASSERT_EQUAL_INT(0, rc);
TEST_ASSERT_EQUAL_PTR(sentinel_from, out.from);
TEST_ASSERT_EQUAL_PTR(sentinel_key, out.key);
printf("test_convert_does_not_set_identity_fields: PASS\n");
}
/* ── Test group entry point ─────────────────────────────────────── */
void test_cluster(void)
{
RUN_TEST(test_validate_rejects_zero_candidates);
RUN_TEST(test_validate_rejects_over_max_candidates);
RUN_TEST(test_validate_rejects_empty_identity);
RUN_TEST(test_validate_accepts_valid_config);
RUN_TEST(test_connect_succeeds_first_candidate);
RUN_TEST(test_connect_fallback_to_second_candidate);
RUN_TEST(test_connect_returns_null_all_fail);
RUN_TEST(test_reconnect_step_reaches_connected);
RUN_TEST(test_reconnect_step_backs_off_on_failure);
RUN_TEST(test_convert_one_candidate);
RUN_TEST(test_convert_three_candidates);
RUN_TEST(test_convert_four_candidates);
RUN_TEST(test_convert_rejects_null_cfg);
RUN_TEST(test_convert_rejects_null_out);
RUN_TEST(test_convert_rejects_zero_candidates);
RUN_TEST(test_convert_truncates_over_max_candidates);
RUN_TEST(test_convert_does_not_set_identity_fields);
}