From 7697d4f43ea8e30ad8358a9d2d6826afdbf84e28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Canna=C3=B2?= Date: Sun, 22 Mar 2026 08:53:14 +0100 Subject: [PATCH] Add server selection unit tests (Phase 3.4, #5492) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 21 test cases covering: - Eligibility: all 5 status types, max_connections, latency, repl lag, disabled limits (max=0) - Selection: single server, empty list, all offline, weight=0 - Weighted distribution: equal weights → ~50/50, 3:1 → ~75/25 (statistical verification over 10000 iterations) - Determinism: same seed produces same result - Mixed eligibility: only eligible server selected 100% of the time --- test/tap/tests/unit/Makefile | 50 +--- .../tests/unit/server_selection_unit-t.cpp | 223 ++++++++++++++++++ 2 files changed, 231 insertions(+), 42 deletions(-) create mode 100644 test/tap/tests/unit/server_selection_unit-t.cpp diff --git a/test/tap/tests/unit/Makefile b/test/tap/tests/unit/Makefile index 0d56f1758..205b01fee 100644 --- a/test/tap/tests/unit/Makefile +++ b/test/tap/tests/unit/Makefile @@ -231,7 +231,10 @@ $(ODIR)/test_init.o: $(TEST_HELPERS_DIR)/test_init.cpp | $(ODIR) # Unit test targets # =========================================================================== -UNIT_TESTS := smoke_test-t query_cache_unit-t query_processor_unit-t protocol_unit-t auth_unit-t connection_pool_unit-t rule_matching_unit-t hostgroups_unit-t monitor_health_unit-t +UNIT_TESTS := smoke_test-t query_cache_unit-t query_processor_unit-t \ + protocol_unit-t auth_unit-t connection_pool_unit-t \ + rule_matching_unit-t hostgroups_unit-t monitor_health_unit-t \ + server_selection_unit-t .PHONY: all all: $(UNIT_TESTS) @@ -245,47 +248,10 @@ ifneq ($(UNAME_S),Darwin) ALLOW_MULTI_DEF := -Wl,--allow-multiple-definition endif -smoke_test-t: smoke_test-t.cpp $(TEST_HELPERS_OBJ) $(LIBPROXYSQLAR) - $(CXX) $< $(TEST_HELPERS_OBJ) $(IDIRS) $(LDIRS) $(OPT) \ - $(LIBPROXYSQLAR_FULL) $(STATIC_LIBS) $(MYLIBS) \ - $(ALLOW_MULTI_DEF) -o $@ - -query_cache_unit-t: query_cache_unit-t.cpp $(TEST_HELPERS_OBJ) $(LIBPROXYSQLAR) - $(CXX) $< $(TEST_HELPERS_OBJ) $(IDIRS) $(LDIRS) $(OPT) \ - $(LIBPROXYSQLAR_FULL) $(STATIC_LIBS) $(MYLIBS) \ - $(ALLOW_MULTI_DEF) -o $@ - -query_processor_unit-t: query_processor_unit-t.cpp $(TEST_HELPERS_OBJ) $(LIBPROXYSQLAR) - $(CXX) $< $(TEST_HELPERS_OBJ) $(IDIRS) $(LDIRS) $(OPT) \ - $(LIBPROXYSQLAR_FULL) $(STATIC_LIBS) $(MYLIBS) \ - $(ALLOW_MULTI_DEF) -o $@ - -protocol_unit-t: protocol_unit-t.cpp $(TEST_HELPERS_OBJ) $(LIBPROXYSQLAR) - $(CXX) $< $(TEST_HELPERS_OBJ) $(IDIRS) $(LDIRS) $(OPT) \ - $(LIBPROXYSQLAR_FULL) $(STATIC_LIBS) $(MYLIBS) \ - $(ALLOW_MULTI_DEF) -o $@ - -auth_unit-t: auth_unit-t.cpp $(TEST_HELPERS_OBJ) $(LIBPROXYSQLAR) - $(CXX) $< $(TEST_HELPERS_OBJ) $(IDIRS) $(LDIRS) $(OPT) \ - $(LIBPROXYSQLAR_FULL) $(STATIC_LIBS) $(MYLIBS) \ - $(ALLOW_MULTI_DEF) -o $@ - -connection_pool_unit-t: connection_pool_unit-t.cpp $(TEST_HELPERS_OBJ) $(LIBPROXYSQLAR) - $(CXX) $< $(TEST_HELPERS_OBJ) $(IDIRS) $(LDIRS) $(OPT) \ - $(LIBPROXYSQLAR_FULL) $(STATIC_LIBS) $(MYLIBS) \ - $(ALLOW_MULTI_DEF) -o $@ - -rule_matching_unit-t: rule_matching_unit-t.cpp $(TEST_HELPERS_OBJ) $(LIBPROXYSQLAR) - $(CXX) $< $(TEST_HELPERS_OBJ) $(IDIRS) $(LDIRS) $(OPT) \ - $(LIBPROXYSQLAR_FULL) $(STATIC_LIBS) $(MYLIBS) \ - $(ALLOW_MULTI_DEF) -o $@ - -hostgroups_unit-t: hostgroups_unit-t.cpp $(TEST_HELPERS_OBJ) $(LIBPROXYSQLAR) - $(CXX) $< $(TEST_HELPERS_OBJ) $(IDIRS) $(LDIRS) $(OPT) \ - $(LIBPROXYSQLAR_FULL) $(STATIC_LIBS) $(MYLIBS) \ - $(ALLOW_MULTI_DEF) -o $@ - -monitor_health_unit-t: monitor_health_unit-t.cpp $(TEST_HELPERS_OBJ) $(LIBPROXYSQLAR) +# Pattern rule: all unit tests use the same compile + link flags. +# Each test binary is built from its .cpp source, linked against +# the test harness objects and libproxysql.a with all dependencies. +%-t: %-t.cpp $(TEST_HELPERS_OBJ) $(LIBPROXYSQLAR) $(CXX) $< $(TEST_HELPERS_OBJ) $(IDIRS) $(LDIRS) $(OPT) \ $(LIBPROXYSQLAR_FULL) $(STATIC_LIBS) $(MYLIBS) \ $(ALLOW_MULTI_DEF) -o $@ diff --git a/test/tap/tests/unit/server_selection_unit-t.cpp b/test/tap/tests/unit/server_selection_unit-t.cpp new file mode 100644 index 000000000..33538fe18 --- /dev/null +++ b/test/tap/tests/unit/server_selection_unit-t.cpp @@ -0,0 +1,223 @@ +/** + * @file server_selection_unit-t.cpp + * @brief Unit tests for the server selection algorithm. + * + * Tests the pure selection functions extracted from get_random_MySrvC(): + * - is_candidate_eligible() + * - select_server_from_candidates() + * + * @see Phase 3.4 (GitHub issue #5492) + */ + +#include "tap.h" +#include "test_globals.h" +#include "test_init.h" + +#include "proxysql.h" +#include "ServerSelection.h" + +// ============================================================================ +// Helper: create a default ONLINE server candidate +// ============================================================================ +static ServerCandidate make_candidate(int idx, int64_t weight = 1, + unsigned int max_conns = 1000) +{ + ServerCandidate c {}; + c.index = idx; + c.weight = weight; + c.status = SERVER_ONLINE; + c.current_connections = 0; + c.max_connections = max_conns; + c.current_latency_us = 0; + c.max_latency_us = 0; + c.current_repl_lag = 0; + c.max_repl_lag = 0; + return c; +} + +// ============================================================================ +// 1. is_candidate_eligible +// ============================================================================ + +static void test_eligibility() { + ServerCandidate online = make_candidate(0); + ok(is_candidate_eligible(online) == true, "eligible: ONLINE server"); + + ServerCandidate shunned = make_candidate(1); + shunned.status = SERVER_SHUNNED; + ok(is_candidate_eligible(shunned) == false, "ineligible: SHUNNED"); + + ServerCandidate off_soft = make_candidate(2); + off_soft.status = SERVER_OFFLINE_SOFT; + ok(is_candidate_eligible(off_soft) == false, "ineligible: OFFLINE_SOFT"); + + ServerCandidate off_hard = make_candidate(3); + off_hard.status = SERVER_OFFLINE_HARD; + ok(is_candidate_eligible(off_hard) == false, "ineligible: OFFLINE_HARD"); + + ServerCandidate lag_shunned = make_candidate(4); + lag_shunned.status = SERVER_SHUNNED_REPLICATION_LAG; + ok(is_candidate_eligible(lag_shunned) == false, "ineligible: SHUNNED_REPL_LAG"); + + ServerCandidate at_max = make_candidate(5, 1, 10); + at_max.current_connections = 10; + ok(is_candidate_eligible(at_max) == false, "ineligible: at max_connections"); + + ServerCandidate below_max = make_candidate(6, 1, 10); + below_max.current_connections = 9; + ok(is_candidate_eligible(below_max) == true, "eligible: below max_connections"); + + ServerCandidate high_latency = make_candidate(7); + high_latency.max_latency_us = 5000; + high_latency.current_latency_us = 6000; + ok(is_candidate_eligible(high_latency) == false, "ineligible: high latency"); + + ServerCandidate ok_latency = make_candidate(8); + ok_latency.max_latency_us = 5000; + ok_latency.current_latency_us = 4000; + ok(is_candidate_eligible(ok_latency) == true, "eligible: acceptable latency"); + + ServerCandidate no_limit = make_candidate(9); + no_limit.max_latency_us = 0; + no_limit.current_latency_us = 999999; + ok(is_candidate_eligible(no_limit) == true, "eligible: latency limit disabled (max=0)"); + + ServerCandidate high_lag = make_candidate(10); + high_lag.max_repl_lag = 10; + high_lag.current_repl_lag = 15; + ok(is_candidate_eligible(high_lag) == false, "ineligible: high repl lag"); + + ServerCandidate ok_lag = make_candidate(11); + ok_lag.max_repl_lag = 10; + ok_lag.current_repl_lag = 5; + ok(is_candidate_eligible(ok_lag) == true, "eligible: acceptable repl lag"); +} + +// ============================================================================ +// 2. select_server_from_candidates — basic +// ============================================================================ + +static void test_select_single() { + ServerCandidate c = make_candidate(42); + int result = select_server_from_candidates(&c, 1, 12345); + ok(result == 42, "single server: always selected (idx=42)"); +} + +static void test_select_empty() { + ok(select_server_from_candidates(nullptr, 0, 0) == -1, + "empty list: returns -1"); +} + +static void test_select_all_offline() { + ServerCandidate candidates[3]; + candidates[0] = make_candidate(0); candidates[0].status = SERVER_OFFLINE_HARD; + candidates[1] = make_candidate(1); candidates[1].status = SERVER_SHUNNED; + candidates[2] = make_candidate(2); candidates[2].status = SERVER_OFFLINE_SOFT; + + ok(select_server_from_candidates(candidates, 3, 999) == -1, + "all offline: returns -1"); +} + +static void test_select_weight_zero() { + ServerCandidate c = make_candidate(0, 0); + ok(select_server_from_candidates(&c, 1, 12345) == -1, + "weight=0: never selected"); +} + +// ============================================================================ +// 3. Weighted distribution (statistical) +// ============================================================================ + +static void test_equal_weight_distribution() { + ServerCandidate candidates[2]; + candidates[0] = make_candidate(0, 1); + candidates[1] = make_candidate(1, 1); + + int count[2] = {0, 0}; + const int N = 10000; + for (int seed = 0; seed < N; seed++) { + int result = select_server_from_candidates(candidates, 2, seed); + if (result >= 0 && result <= 1) count[result]++; + } + + double pct0 = (double)count[0] / N * 100; + ok(pct0 > 30 && pct0 < 70, + "equal weight: server 0 selected %.1f%% (expect ~50%%)", pct0); +} + +static void test_weighted_distribution() { + ServerCandidate candidates[2]; + candidates[0] = make_candidate(0, 3); // weight 3 + candidates[1] = make_candidate(1, 1); // weight 1 + + int count[2] = {0, 0}; + const int N = 10000; + for (int seed = 0; seed < N; seed++) { + int result = select_server_from_candidates(candidates, 2, seed); + if (result >= 0 && result <= 1) count[result]++; + } + + double pct0 = (double)count[0] / N * 100; + ok(pct0 > 60 && pct0 < 90, + "3:1 weight: server 0 selected %.1f%% (expect ~75%%)", pct0); +} + +// ============================================================================ +// 4. Determinism +// ============================================================================ + +static void test_determinism() { + ServerCandidate candidates[3]; + candidates[0] = make_candidate(0, 2); + candidates[1] = make_candidate(1, 3); + candidates[2] = make_candidate(2, 5); + + int r1 = select_server_from_candidates(candidates, 3, 42); + int r2 = select_server_from_candidates(candidates, 3, 42); + ok(r1 == r2, "determinism: same seed → same result"); +} + +// ============================================================================ +// 5. Mixed eligible/ineligible +// ============================================================================ + +static void test_mixed_eligibility() { + ServerCandidate candidates[4]; + candidates[0] = make_candidate(0, 1); candidates[0].status = SERVER_SHUNNED; + candidates[1] = make_candidate(1, 1); candidates[1].status = SERVER_OFFLINE_HARD; + candidates[2] = make_candidate(2, 1); // ONLINE + candidates[3] = make_candidate(3, 1); candidates[3].status = SERVER_OFFLINE_SOFT; + + // Only candidate[2] is eligible — must always be selected + int pass = 0; + for (int seed = 0; seed < 100; seed++) { + if (select_server_from_candidates(candidates, 4, seed) == 2) pass++; + } + ok(pass == 100, + "mixed: only eligible server selected 100/100 times"); +} + +// ============================================================================ +// Main +// ============================================================================ + +int main() { + plan(21); + + int rc = test_init_minimal(); + ok(rc == 0, "test_init_minimal() succeeds"); + + test_eligibility(); // 12 + test_select_single(); // 1 + test_select_empty(); // 1 + test_select_all_offline(); // 1 + test_select_weight_zero(); // 1 + test_equal_weight_distribution(); // 1 + test_weighted_distribution(); // 1 + test_determinism(); // 1 + test_mixed_eligibility(); // 1 + // Total: 1+12+1+1+1+1+1+1+1+1 = 21... fix + + test_cleanup_minimal(); + return exit_status(); +}