From 803eb27590e2d88fd015107086b7c3fc247fe2bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Canna=C3=B2?= Date: Sat, 21 Mar 2026 20:37:55 +0100 Subject: [PATCH] Add unit test harness: Glo* stubs and component init helpers Introduces the test infrastructure foundation for Phase 2.1 (#5473) of the unit testing framework (#5472). This enables unit test binaries to link against libproxysql.a without main.cpp, allowing individual components to be tested in isolation. New files: - test/tap/test_helpers/test_globals.h/.cpp: Stub definitions for all 25 Glo* global pointers and the PROXYSQL_EXTERN-based variable definitions (GloVars, MyHGM, PgHGM, __thread vars, tracked vars). Also stubs symbols from src/ (proxy_tls, SQLite3_Server, binary_sha1) and TAP noise testing functions. - test/tap/test_helpers/test_init.h/.cpp: Component initialization helpers (test_init_minimal, test_init_auth, test_init_query_cache, test_init_query_processor) with matching cleanup functions. All are idempotent and safe to call multiple times. --- test/tap/test_helpers/test_globals.cpp | 271 +++++++++++++++++++++++++ test/tap/test_helpers/test_globals.h | 46 +++++ test/tap/test_helpers/test_init.cpp | 129 ++++++++++++ test/tap/test_helpers/test_init.h | 112 ++++++++++ 4 files changed, 558 insertions(+) create mode 100644 test/tap/test_helpers/test_globals.cpp create mode 100644 test/tap/test_helpers/test_globals.h create mode 100644 test/tap/test_helpers/test_init.cpp create mode 100644 test/tap/test_helpers/test_init.h diff --git a/test/tap/test_helpers/test_globals.cpp b/test/tap/test_helpers/test_globals.cpp new file mode 100644 index 000000000..525f49bd1 --- /dev/null +++ b/test/tap/test_helpers/test_globals.cpp @@ -0,0 +1,271 @@ +/** + * @file test_globals.cpp + * @brief Stub definitions of ProxySQL global symbols for unit testing. + * + * This file serves as a replacement for both src/proxysql_global.cpp and + * the global variable definitions in src/main.cpp. It allows unit test + * binaries to link against libproxysql.a without pulling in main() or + * the full daemon initialization sequence. + * + * The PROXYSQL_EXTERN mechanism (defined in include/proxysql_structs.h) + * controls whether global variables are declared as 'extern' or defined. + * By defining PROXYSQL_EXTERN here, we cause the headers to emit actual + * definitions for: + * - GloVars (ProxySQL_GlobalVariables) + * - MyHGM, PgHGM (HostGroups Manager pointers) + * - glovars (global_variables struct) + * - All __thread per-thread variables (mysql_thread_*, pgsql_thread_*) + * - mysql_tracked_variables[], pgsql_tracked_variables[] + * + * Additionally, this file defines the Glo* pointers that are normally + * declared in main.cpp (GloMyQC, GloMyAuth, GloAdmin, GloMTH, etc.), + * all initialized to nullptr. + * + * @see test_globals.h for the public interface. + * @see Phase 2.1 of the Unit Testing Framework (GitHub issue #5473) + */ + +// Define PROXYSQL_EXTERN before including headers to emit global +// variable definitions (same mechanism as src/proxysql_global.cpp). +#define PROXYSQL_EXTERN + +#include "../deps/json/json.hpp" + +using json = nlohmann::json; +#define PROXYJSON + +#include +#include +#include +#include "btree_map.h" +#include "proxysql.h" +#include "cpp.h" + +#include "ProxySQL_Statistics.hpp" +#include "MySQL_PreparedStatement.h" +#include "PgSQL_PreparedStatement.h" +#include "ProxySQL_Cluster.hpp" +#include "MySQL_Logger.hpp" +#include "PgSQL_Logger.hpp" + +#ifdef PROXYSQLGENAI +#include "MCP_Thread.h" +#include "GenAI_Thread.h" +#include "AI_Features_Manager.h" +#endif /* PROXYSQLGENAI */ + +#include "SQLite3_Server.h" +#include "MySQL_Query_Processor.h" +#include "PgSQL_Query_Processor.h" +#include "MySQL_Authentication.hpp" +#include "PgSQL_Authentication.h" +#include "MySQL_LDAP_Authentication.hpp" +#include "MySQL_Query_Cache.h" +#include "PgSQL_Query_Cache.h" +#include "proxysql_restapi.h" +#include "Web_Interface.hpp" +#include "proxysql_utils.h" + +#include "test_globals.h" + +// ============================================================================ +// Glo* pointer stubs — normally defined in src/main.cpp +// +// These are the global singleton pointers that most ProxySQL classes +// reference directly. For unit tests, they start as nullptr and are +// selectively initialized by the test_init_*() helpers. +// ============================================================================ + +MySQL_Query_Cache *GloMyQC = nullptr; +PgSQL_Query_Cache *GloPgQC = nullptr; +MySQL_Authentication *GloMyAuth = nullptr; +PgSQL_Authentication *GloPgAuth = nullptr; +MySQL_LDAP_Authentication *GloMyLdapAuth = nullptr; + +#ifdef PROXYSQLCLICKHOUSE +ClickHouse_Authentication *GloClickHouseAuth = nullptr; +#endif /* PROXYSQLCLICKHOUSE */ + +MySQL_Query_Processor *GloMyQPro = nullptr; +PgSQL_Query_Processor *GloPgQPro = nullptr; +ProxySQL_Admin *GloAdmin = nullptr; +MySQL_Threads_Handler *GloMTH = nullptr; +PgSQL_Threads_Handler *GloPTH = nullptr; + +#ifdef PROXYSQLGENAI +MCP_Threads_Handler *GloMCPH = nullptr; +GenAI_Threads_Handler *GloGATH = nullptr; +AI_Features_Manager *GloAI = nullptr; +#endif /* PROXYSQLGENAI */ + +Web_Interface *GloWebInterface = nullptr; +MySQL_STMT_Manager_v14 *GloMyStmt = nullptr; +PgSQL_STMT_Manager *GloPgStmt = nullptr; + +MySQL_Monitor *GloMyMon = nullptr; +PgSQL_Monitor *GloPgMon = nullptr; +std::thread *MyMon_thread = nullptr; + +MySQL_Logger *GloMyLogger = nullptr; +PgSQL_Logger *GloPgSQL_Logger = nullptr; + +MySQL_Variables mysql_variables; +PgSQL_Variables pgsql_variables; + +SQLite3_Server *GloSQLite3Server = nullptr; + +#ifdef PROXYSQLCLICKHOUSE +ClickHouse_Server *GloClickHouseServer = nullptr; +#endif /* PROXYSQLCLICKHOUSE */ + +ProxySQL_Cluster *GloProxyCluster = nullptr; +ProxySQL_Statistics *GloProxyStats = nullptr; + +// ============================================================================ +// TAP noise testing stubs — normally defined in noise_utils.cpp. +// Unit tests do not use noise testing, so these are empty stubs to +// satisfy the extern references in tap.cpp. +// ============================================================================ + +std::vector noise_failures; +std::mutex noise_failure_mutex; + +// ============================================================================ +// Other symbols from main.cpp +// ============================================================================ + +/// Atomic load counter used during daemon startup. Unused in tests. +std::atomic load_{0}; + +/// File descriptors for the proxy listener sockets. Unused in tests. +int listen_fd = -1; +int socket_fd = -1; + +/// SHA1 checksum of the binary. Unused in tests. +char *binary_sha1 = nullptr; + +// ============================================================================ +// Stub functions for symbols referenced by libproxysql.a that are +// normally defined in src/ object files (proxy_tls.o, SQLite3_Server.o). +// These are no-ops since unit tests don't exercise TLS bootstrap or +// the SQLite3 server module. +// ============================================================================ + +#include "SQLite3_Server.h" + +int ProxySQL_create_or_load_TLS(bool, std::string &) { return 0; } + +char *SQLite3_Server::get_variable(char *) { return nullptr; } +bool SQLite3_Server::has_variable(const char *) { return false; } +bool SQLite3_Server::set_variable(char *, char *) { return false; } +char **SQLite3_Server::get_variables_list() { return nullptr; } +void SQLite3_Server::wrlock() {} +void SQLite3_Server::wrunlock() {} + +// ============================================================================ +// TAP noise testing stubs — exit_status() in tap.cpp calls these. +// Normally defined in noise_utils.cpp (test/tap/tap/utils.cpp). +// ============================================================================ + +extern "C" void stop_noise_tools() {} +extern "C" int get_noise_tools_count() { return 0; } + +/// jemalloc configuration string. Required at link time on non-FreeBSD. +#ifndef __FreeBSD__ +const char *malloc_conf = + "xmalloc:true,lg_tcache_max:16,prof:false"; +#endif + +// ============================================================================ +// ProxySQL_GlobalVariables SSL helpers (from src/proxysql_global.cpp) +// +// These methods access GloVars.global.ssl_ctx which will be nullptr +// in tests. The stubs return nullptr/noop to avoid crashes. +// ============================================================================ + +SSL_CTX *ProxySQL_GlobalVariables::get_SSL_ctx() { + std::lock_guard lock(global.ssl_mutex); + return global.ssl_ctx; +} + +SSL *ProxySQL_GlobalVariables::get_SSL_new() { + std::lock_guard lock(global.ssl_mutex); + if (global.ssl_ctx == nullptr) return nullptr; + return SSL_new(global.ssl_ctx); +} + +void ProxySQL_GlobalVariables::get_SSL_pem_mem(char **key, char **cert) { + std::lock_guard lock(global.ssl_mutex); + if (global.ssl_key_pem_mem != nullptr) + *key = strdup(global.ssl_key_pem_mem); + else + *key = nullptr; + if (global.ssl_cert_pem_mem != nullptr) + *cert = strdup(global.ssl_cert_pem_mem); + else + *cert = nullptr; +} + +// ============================================================================ +// test_globals_init / test_globals_cleanup +// ============================================================================ + +/** + * @brief Sets up GloVars with minimal safe defaults for unit testing. + * + * Configures GloVars to use a temporary directory as datadir, disables + * SSL, monitoring, and other features that require infrastructure. + * This function is idempotent. + * + * @return 0 on success, non-zero on failure. + */ +int test_globals_init() { + // Set safe defaults for the global configuration + GloVars.global.nostart = true; + GloVars.global.foreground = true; + GloVars.global.gdbg = false; + GloVars.global.my_monitor = false; + GloVars.global.pg_monitor = false; + GloVars.global.version_check = false; + GloVars.global.sqlite3_server = false; +#ifdef PROXYSQLCLICKHOUSE + GloVars.global.clickhouse_server = false; +#endif + GloVars.global.ssl_keylog_enabled = false; + GloVars.global.gr_bootstrap_mode = 0; + GloVars.global.gr_bootstrap_uri = nullptr; + GloVars.global.data_packets_history_size = 0; + + // SSL pointers — nullptr means no SSL + GloVars.global.ssl_ctx = nullptr; + GloVars.global.tmp_ssl_ctx = nullptr; + GloVars.global.ssl_key_pem_mem = nullptr; + GloVars.global.ssl_cert_pem_mem = nullptr; + + // File paths — use a temp directory so tests don't touch real data. + // These are strdup'd so cleanup can safely free() them. + const char *tmpdir = getenv("TMPDIR"); + if (tmpdir == nullptr) tmpdir = "/tmp"; + + char datadir_buf[256]; + snprintf(datadir_buf, sizeof(datadir_buf), "%s/proxysql_unit_test_%d", + tmpdir, getpid()); + + if (GloVars.datadir == nullptr) { + GloVars.datadir = strdup(datadir_buf); + } + + return 0; +} + +/** + * @brief Frees resources allocated by test_globals_init(). + * + * Safe to call multiple times or even if test_globals_init() was never called. + */ +void test_globals_cleanup() { + if (GloVars.datadir != nullptr) { + free(GloVars.datadir); + GloVars.datadir = nullptr; + } +} diff --git a/test/tap/test_helpers/test_globals.h b/test/tap/test_helpers/test_globals.h new file mode 100644 index 000000000..898f57131 --- /dev/null +++ b/test/tap/test_helpers/test_globals.h @@ -0,0 +1,46 @@ +/** + * @file test_globals.h + * @brief Stub global definitions for ProxySQL unit tests. + * + * This header is the entry point for unit tests that need to link against + * libproxysql.a without the real main.cpp. It provides: + * + * 1. All Glo* pointer stubs (initialized to nullptr) + * 2. GloVars (ProxySQL_GlobalVariables) with safe defaults + * 3. All __thread variable definitions via the PROXYSQL_EXTERN mechanism + * 4. Other extern symbols normally provided by main.cpp + * + * Usage in test files: + * @code + * #include "test_globals.h" + * #include "test_init.h" + * // ... test code ... + * @endcode + * + * @note This file must NOT be included by production code. + * @see test_globals.cpp for the corresponding definitions. + * @see Phase 2.1 of the Unit Testing Framework (GitHub issue #5473) + */ + +#ifndef TEST_GLOBALS_H +#define TEST_GLOBALS_H + +/** + * @brief Initialize minimal global state required for unit tests. + * + * Sets up GloVars with safe defaults (tmpdir-based datadir, no SSL, + * no pidfile). Must be called before any component initialization. + * + * @return 0 on success, non-zero on failure. + */ +int test_globals_init(); + +/** + * @brief Clean up global state allocated by test_globals_init(). + * + * Frees any memory allocated during initialization. Safe to call + * multiple times. + */ +void test_globals_cleanup(); + +#endif /* TEST_GLOBALS_H */ diff --git a/test/tap/test_helpers/test_init.cpp b/test/tap/test_helpers/test_init.cpp new file mode 100644 index 000000000..d555ba1ca --- /dev/null +++ b/test/tap/test_helpers/test_init.cpp @@ -0,0 +1,129 @@ +/** + * @file test_init.cpp + * @brief Implementation of component initialization helpers for unit tests. + * + * Each test_init_*() function creates real instances of ProxySQL components, + * bypassing the full daemon startup sequence. Components are assigned to + * their respective Glo* global pointers so that internal cross-references + * work correctly. + * + * @see test_init.h for the public interface and usage examples. + * @see Phase 2.1 of the Unit Testing Framework (GitHub issue #5473) + */ + +#include "proxysql.h" +#include "cpp.h" + +#include "MySQL_Authentication.hpp" +#include "PgSQL_Authentication.h" +#include "MySQL_Query_Cache.h" +#include "PgSQL_Query_Cache.h" +#include "MySQL_Query_Processor.h" +#include "PgSQL_Query_Processor.h" + +#include "test_globals.h" +#include "test_init.h" + +// Extern declarations for Glo* pointers defined in test_globals.cpp. +// These are normally defined in main.cpp and have no header declarations. +extern MySQL_Authentication *GloMyAuth; +extern PgSQL_Authentication *GloPgAuth; +extern MySQL_Query_Cache *GloMyQC; +extern PgSQL_Query_Cache *GloPgQC; +extern MySQL_Query_Processor *GloMyQPro; +extern PgSQL_Query_Processor *GloPgQPro; + +// ============================================================================ +// Minimal initialization +// ============================================================================ + +int test_init_minimal() { + return test_globals_init(); +} + +void test_cleanup_minimal() { + test_globals_cleanup(); +} + +// ============================================================================ +// Authentication +// ============================================================================ + +int test_init_auth() { + if (GloMyAuth != nullptr || GloPgAuth != nullptr) { + // Already initialized — idempotent + return 0; + } + + GloMyAuth = new MySQL_Authentication(); + GloPgAuth = new PgSQL_Authentication(); + + return 0; +} + +void test_cleanup_auth() { + if (GloMyAuth != nullptr) { + delete GloMyAuth; + GloMyAuth = nullptr; + } + if (GloPgAuth != nullptr) { + delete GloPgAuth; + GloPgAuth = nullptr; + } +} + +// ============================================================================ +// Query Cache +// ============================================================================ + +int test_init_query_cache() { + if (GloMyQC != nullptr || GloPgQC != nullptr) { + return 0; + } + + GloMyQC = new MySQL_Query_Cache(); + GloPgQC = new PgSQL_Query_Cache(); + + // NOTE: We intentionally do NOT start the purge thread here. + // Unit tests should call purgeHash() explicitly for deterministic + // behavior. + + return 0; +} + +void test_cleanup_query_cache() { + if (GloMyQC != nullptr) { + delete GloMyQC; + GloMyQC = nullptr; + } + if (GloPgQC != nullptr) { + delete GloPgQC; + GloPgQC = nullptr; + } +} + +// ============================================================================ +// Query Processor +// ============================================================================ + +int test_init_query_processor() { + if (GloMyQPro != nullptr || GloPgQPro != nullptr) { + return 0; + } + + GloMyQPro = new MySQL_Query_Processor(); + GloPgQPro = new PgSQL_Query_Processor(); + + return 0; +} + +void test_cleanup_query_processor() { + if (GloMyQPro != nullptr) { + delete GloMyQPro; + GloMyQPro = nullptr; + } + if (GloPgQPro != nullptr) { + delete GloPgQPro; + GloPgQPro = nullptr; + } +} diff --git a/test/tap/test_helpers/test_init.h b/test/tap/test_helpers/test_init.h new file mode 100644 index 000000000..4d57f86a7 --- /dev/null +++ b/test/tap/test_helpers/test_init.h @@ -0,0 +1,112 @@ +/** + * @file test_init.h + * @brief Component initialization helpers for ProxySQL unit tests. + * + * Provides functions to selectively initialize individual ProxySQL + * components for isolated testing, without requiring the full daemon + * startup sequence. Each init function has a matching cleanup function + * that frees all resources. + * + * Typical usage: + * @code + * #include "test_globals.h" + * #include "test_init.h" + * #include "tap.h" + * + * int main() { + * plan(3); + * test_init_minimal(); + * test_init_auth(); + * + * // ... run tests against GloMyAuth ... + * + * test_cleanup_auth(); + * test_cleanup_minimal(); + * return exit_status(); + * } + * @endcode + * + * @note All init functions are idempotent — calling them multiple + * times is safe (subsequent calls are no-ops). + * @note Always call cleanup functions in reverse init order. + * + * @see test_globals.h for the global stub definitions. + * @see Phase 2.1 of the Unit Testing Framework (GitHub issue #5473) + */ + +#ifndef TEST_INIT_H +#define TEST_INIT_H + +/** + * @brief Initialize minimal global state required by all unit tests. + * + * Sets up GloVars with safe defaults. This is the foundation that all + * other test_init_* functions build upon. Must be called first. + * + * @return 0 on success, non-zero on failure. + */ +int test_init_minimal(); + +/** + * @brief Clean up resources allocated by test_init_minimal(). + */ +void test_cleanup_minimal(); + +/** + * @brief Initialize MySQL and PostgreSQL Authentication components. + * + * Creates real MySQL_Authentication and PgSQL_Authentication objects + * (assigned to GloMyAuth and GloPgAuth). The auth stores are empty + * and ready for test data via add()/lookup()/del(). + * + * @pre test_init_minimal() must have been called. + * @return 0 on success, non-zero on failure. + */ +int test_init_auth(); + +/** + * @brief Clean up resources allocated by test_init_auth(). + * + * Destroys GloMyAuth and GloPgAuth, setting them back to nullptr. + */ +void test_cleanup_auth(); + +/** + * @brief Initialize MySQL and PostgreSQL Query Cache components. + * + * Creates real MySQL_Query_Cache and PgSQL_Query_Cache objects + * (assigned to GloMyQC and GloPgQC). The purge thread is NOT started; + * callers can invoke purgeHash() manually for deterministic testing. + * + * @pre test_init_minimal() must have been called. + * @return 0 on success, non-zero on failure. + */ +int test_init_query_cache(); + +/** + * @brief Clean up resources allocated by test_init_query_cache(). + * + * Destroys GloMyQC and GloPgQC, setting them back to nullptr. + */ +void test_cleanup_query_cache(); + +/** + * @brief Initialize MySQL and PostgreSQL Query Processor components. + * + * Creates real MySQL_Query_Processor and PgSQL_Query_Processor objects + * (assigned to GloMyQPro and GloPgQPro) with empty rulesets. Rules + * can be added via new_query_rule() for testing. + * + * @pre test_init_minimal() must have been called. + * @return 0 on success, non-zero on failure. + */ +int test_init_query_processor(); + +/** + * @brief Clean up resources allocated by test_init_query_processor(). + * + * Destroys GloMyQPro and GloPgQPro, setting them back to nullptr. + */ +void test_cleanup_query_processor(); + +#endif /* TEST_INIT_H */