diff --git a/.kiro/specs/controller-peers-env-fallback/tasks.md b/.kiro/specs/controller-peers-env-fallback/tasks.md index 7dd7f4b5..f17b3180 100644 --- a/.kiro/specs/controller-peers-env-fallback/tasks.md +++ b/.kiro/specs/controller-peers-env-fallback/tasks.md @@ -8,7 +8,7 @@ - Done when the libxce suite passes with the three new cases and every existing libxce test still passes - _Requirements: 1.2, 1.3, 1.7_ -- [ ] 1.2 Add the shared "which candidate is me" lookup +- [x] 1.2 Add the shared "which candidate is me" lookup - Write the failing libxce unit tests first: named match, unnamed entry matched by hostname, named entry not matched by its hostname, no match, first of duplicate names wins, no name given, and an empty name matching an unnamed empty-hostname entry exactly as the controller's existing loops do - Implement the lookup with the matching rule moved unchanged from the controller (candidate name when set, otherwise hostname; first match wins); it applies no default of its own - Done when the libxce suite passes with all seven cases diff --git a/src/include/xce/cluster.h b/src/include/xce/cluster.h index c41585e5..d4759384 100644 --- a/src/include/xce/cluster.h +++ b/src/include/xce/cluster.h @@ -148,6 +148,21 @@ int xce_parse_peers(const char* peers_str, xce_cluster_config_t* cfg); */ const char* xce_controller_peers_env(void); +/** + * xce_peers_find_identity() — the one "which candidate is me" rule. + * + * A candidate's key is its name when non-empty, otherwise its hostname + * (a named candidate is never matched by its hostname). The first + * candidate whose key matches name (compared over XCE_MAXNAME_LEN - 1 + * characters) wins. An empty name is compared like any other, so it + * matches an unnamed candidate with an empty hostname. + * + * Returns the matching candidate's index, or -1 when cfg or name is NULL + * or nothing matches. No default is applied; each caller keeps its own + * no-match handling. + */ +int xce_peers_find_identity(const xce_cluster_config_t* cfg, const char* name); + /* Test seam: set to non-NULL to override the real connect (unit tests only) */ typedef void* (*xce_connect_fn_t)(const char* host, int port); void xce_cluster_set_connect_fn(xce_connect_fn_t fn); diff --git a/src/lib/cluster.c b/src/lib/cluster.c index 0d54e66b..c963a0da 100644 --- a/src/lib/cluster.c +++ b/src/lib/cluster.c @@ -65,6 +65,20 @@ const char* xce_controller_peers_env(void) return (v && v[0] != '\0') ? v : NULL; } +int xce_peers_find_identity(const xce_cluster_config_t* cfg, const char* name) +{ + if (!cfg || !name) + return -1; + for (int i = 0; i < cfg->candidates_count; i++) { + const char* key = cfg->candidates[i].name[0] + ? cfg->candidates[i].name + : cfg->candidates[i].hostname; + if (strncmp(key, name, XCE_MAXNAME_LEN - 1) == 0) + return i; + } + return -1; +} + /* ── Test seam ─────────────────────────────────────────────────── */ static xce_connect_fn_t s_connect_fn = NULL; diff --git a/src/lib/testsuite/test_cluster.c b/src/lib/testsuite/test_cluster.c index edbc77ad..1f23a19b 100644 --- a/src/lib/testsuite/test_cluster.c +++ b/src/lib/testsuite/test_cluster.c @@ -326,6 +326,100 @@ static void test_peers_env_nonempty_returned_unchanged(void) printf("test_peers_env_nonempty_returned_unchanged: PASS\n"); } +/* ── xce_peers_find_identity() ──────────────────────────────────── */ + +/* Fill candidate i of cfg directly: name may be "" (unnamed entry), + * hostname may be "" (only reachable by filling the struct by hand). */ +static void set_candidate(xce_cluster_config_t* cfg, int i, const char* name, + const char* hostname, int port) +{ + snprintf(cfg->candidates[i].name, XCE_MAXNAME_LEN, "%s", name); + snprintf(cfg->candidates[i].hostname, XCE_MAXHOSTNAME_LEN, "%s", + hostname); + cfg->candidates[i].port = port; +} + +/* Req 1.6: a named candidate is found by its name. */ +static void test_find_identity_named_match(void) +{ + xce_cluster_config_t cfg = {0}; + set_candidate(&cfg, 0, "c1", "localhost", 9991); + set_candidate(&cfg, 1, "c2", "localhost", 9992); + set_candidate(&cfg, 2, "c3", "localhost", 9993); + cfg.candidates_count = 3; + TEST_ASSERT_EQUAL_INT(1, xce_peers_find_identity(&cfg, "c2")); + printf("test_find_identity_named_match: PASS\n"); +} + +/* Req 1.6: an unnamed candidate is found by its hostname. */ +static void test_find_identity_unnamed_matched_by_hostname(void) +{ + xce_cluster_config_t cfg = {0}; + set_candidate(&cfg, 0, "", "node1", 9990); + set_candidate(&cfg, 1, "", "node2", 9990); + cfg.candidates_count = 2; + TEST_ASSERT_EQUAL_INT(1, xce_peers_find_identity(&cfg, "node2")); + printf("test_find_identity_unnamed_matched_by_hostname: PASS\n"); +} + +/* Req 1.6: a named candidate's key is its name only -- its hostname + * does not identify it. */ +static void test_find_identity_named_not_matched_by_hostname(void) +{ + xce_cluster_config_t cfg = {0}; + set_candidate(&cfg, 0, "c1", "node1", 9991); + set_candidate(&cfg, 1, "c2", "node2", 9992); + cfg.candidates_count = 2; + TEST_ASSERT_EQUAL_INT(-1, xce_peers_find_identity(&cfg, "node2")); + printf("test_find_identity_named_not_matched_by_hostname: PASS\n"); +} + +/* Req 1.6: no candidate matches -> -1 (no default applied). */ +static void test_find_identity_no_match(void) +{ + xce_cluster_config_t cfg = {0}; + set_candidate(&cfg, 0, "c1", "localhost", 9991); + set_candidate(&cfg, 1, "", "node2", 9992); + cfg.candidates_count = 2; + TEST_ASSERT_EQUAL_INT(-1, xce_peers_find_identity(&cfg, "c9")); + printf("test_find_identity_no_match: PASS\n"); +} + +/* Req 1.6: the first of duplicate names wins (not the last). */ +static void test_find_identity_first_duplicate_wins(void) +{ + xce_cluster_config_t cfg = {0}; + set_candidate(&cfg, 0, "c0", "localhost", 9990); + set_candidate(&cfg, 1, "dup", "localhost", 9991); + set_candidate(&cfg, 2, "dup", "localhost", 9992); + cfg.candidates_count = 3; + TEST_ASSERT_EQUAL_INT(1, xce_peers_find_identity(&cfg, "dup")); + printf("test_find_identity_first_duplicate_wins: PASS\n"); +} + +/* Req 1.6: no name given -> -1, even against an entry with empty keys. */ +static void test_find_identity_null_name(void) +{ + xce_cluster_config_t cfg = {0}; + set_candidate(&cfg, 0, "c1", "localhost", 9991); + set_candidate(&cfg, 1, "", "", 9992); + cfg.candidates_count = 2; + TEST_ASSERT_EQUAL_INT(-1, xce_peers_find_identity(&cfg, NULL)); + printf("test_find_identity_null_name: PASS\n"); +} + +/* Req 1.6: an empty name is compared like any other, so it matches an + * unnamed empty-hostname entry exactly as the controller's loops do. */ +static void test_find_identity_empty_name_matches_empty_hostname(void) +{ + xce_cluster_config_t cfg = {0}; + set_candidate(&cfg, 0, "c1", "localhost", 9991); + set_candidate(&cfg, 1, "", "", 9992); + cfg.candidates_count = 2; + TEST_ASSERT_EQUAL_INT(1, xce_peers_find_identity(&cfg, "")); + printf("test_find_identity_empty_name_matches_empty_hostname: PASS\n"); +} + /* ── Test group entry point ─────────────────────────────────────── */ void test_cluster(void) { @@ -349,4 +443,11 @@ void test_cluster(void) RUN_TEST(test_peers_env_unset_returns_null); RUN_TEST(test_peers_env_empty_returns_null); RUN_TEST(test_peers_env_nonempty_returned_unchanged); + RUN_TEST(test_find_identity_named_match); + RUN_TEST(test_find_identity_unnamed_matched_by_hostname); + RUN_TEST(test_find_identity_named_not_matched_by_hostname); + RUN_TEST(test_find_identity_no_match); + RUN_TEST(test_find_identity_first_duplicate_wins); + RUN_TEST(test_find_identity_null_name); + RUN_TEST(test_find_identity_empty_name_matches_empty_hostname); }