#include #include #include #include #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); }