/** * @file statistics_unit-t.cpp * @brief Unit tests for ProxySQL_Statistics. * * Tests cover: * - Constructor / init / table creation * - Timer functions (MySQL_Threads_Handler_timetoget, system_cpu_timetoget, * MySQL_Query_Cache_timetoget, eventslog timer functions) * - TSDB variable management (set_variable, get_variable, has_variable, * get_variables_list) — guarded by PROXYSQLTSDB * - TSDB timer functions (tsdb_sampler_timetoget, tsdb_downsample_timetoget, * tsdb_monitor_timetoget, tsdb_retention_timetoget) — guarded by PROXYSQLTSDB * - TSDB metric insertion and query (insert_tsdb_metric, query_tsdb_metrics) * which indirectly exercises the anonymous-namespace helpers * escape_sql_string_literal and valid_label_key * - TSDB backend health insertion and query * - TSDB downsampling and retention cleanup * - TSDB status retrieval * * All tests use in-memory or /tmp SQLite3 databases — no external deps. */ #include "tap.h" #include "test_globals.h" #include "test_init.h" #include "proxysql.h" #include "cpp.h" #include "ProxySQL_Statistics.hpp" #include #include #include #include #include #include // Forward-declare the extern that the constructor reads. // GloVars is defined in test_globals.cpp via the PROXYSQL_EXTERN mechanism. static ProxySQL_Statistics *stats = nullptr; static std::string tmpdb_path; // ============================================================ // Setup / teardown // ============================================================ static void setup_stats() { // ProxySQL_Statistics constructor opens GloVars.statsdb_disk as a file path. // Use /tmp directly to avoid datadir issues in CI environments. const char *tmpdir = getenv("TMPDIR"); if (tmpdir == nullptr) tmpdir = "/tmp"; tmpdb_path = std::string(tmpdir) + "/proxysql_test_statsdb_" + std::to_string(getpid()) + ".db"; // Ensure datadir exists (needed by other parts of ProxySQL_Statistics) if (GloVars.datadir != nullptr) mkdir(GloVars.datadir, 0755); // Set the global so the constructor can open it GloVars.statsdb_disk = strdup(tmpdb_path.c_str()); stats = new ProxySQL_Statistics(); if (stats->statsdb_disk == nullptr) { diag("statsdb_disk is NULL after construction — possible struct layout mismatch"); diag("(rebuild with: make clean && make debug in test/tap/tests/unit/)"); diag("GloVars.statsdb_disk=%s", GloVars.statsdb_disk ? GloVars.statsdb_disk : "NULL"); } if (stats->statsdb_disk != nullptr) stats->init(); } static void teardown_stats() { delete stats; stats = nullptr; // Clean up temp database files unlink(tmpdb_path.c_str()); std::string wal = tmpdb_path + "-wal"; std::string shm = tmpdb_path + "-shm"; unlink(wal.c_str()); unlink(shm.c_str()); if (GloVars.statsdb_disk != nullptr) { free(GloVars.statsdb_disk); GloVars.statsdb_disk = nullptr; } } // ============================================================ // Constructor and init // ============================================================ static bool statsdb_ok = false; static void test_constructor_creates_databases() { ok(stats != nullptr, "ProxySQL_Statistics object created successfully"); statsdb_ok = (stats != nullptr && stats->statsdb_disk != nullptr); ok(statsdb_ok, "statsdb_disk is not null after construction"); } static void test_init_creates_tables() { if (!statsdb_ok) { skip(3, "statsdb_disk is NULL — struct layout mismatch (clean rebuild needed)"); return; } // After init(), standard tables should exist. Verify by querying them. SQLite3_result *resultset = nullptr; char *error = nullptr; int cols = 0, affected_rows = 0; stats->statsdb_disk->execute_statement( "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='mysql_connections'", &error, &cols, &affected_rows, &resultset); if (error) { free(error); error = nullptr; } ok(resultset != nullptr && resultset->rows_count > 0 && strcmp(resultset->rows[0]->fields[0], "1") == 0, "init: mysql_connections table exists on disk"); delete resultset; resultset = nullptr; stats->statsdb_disk->execute_statement( "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='system_cpu'", &error, &cols, &affected_rows, &resultset); if (error) { free(error); error = nullptr; } ok(resultset != nullptr && resultset->rows_count > 0 && strcmp(resultset->rows[0]->fields[0], "1") == 0, "init: system_cpu table exists on disk"); delete resultset; resultset = nullptr; stats->statsdb_disk->execute_statement( "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='history_mysql_query_digest'", &error, &cols, &affected_rows, &resultset); if (error) { free(error); error = nullptr; } ok(resultset != nullptr && resultset->rows_count > 0 && strcmp(resultset->rows[0]->fields[0], "1") == 0, "init: history_mysql_query_digest table exists on disk"); delete resultset; } #ifdef PROXYSQLTSDB static void test_init_creates_tsdb_tables() { SQLite3_result *resultset = nullptr; char *error = nullptr; int cols = 0, affected_rows = 0; stats->statsdb_disk->execute_statement( "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='tsdb_metrics'", &error, &cols, &affected_rows, &resultset); if (error) { free(error); error = nullptr; } ok(resultset != nullptr && resultset->rows_count > 0 && strcmp(resultset->rows[0]->fields[0], "1") == 0, "init: tsdb_metrics table exists on disk"); delete resultset; resultset = nullptr; stats->statsdb_disk->execute_statement( "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='tsdb_metrics_hour'", &error, &cols, &affected_rows, &resultset); if (error) { free(error); error = nullptr; } ok(resultset != nullptr && resultset->rows_count > 0 && strcmp(resultset->rows[0]->fields[0], "1") == 0, "init: tsdb_metrics_hour table exists on disk"); delete resultset; resultset = nullptr; stats->statsdb_disk->execute_statement( "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='tsdb_backend_health'", &error, &cols, &affected_rows, &resultset); if (error) { free(error); error = nullptr; } ok(resultset != nullptr && resultset->rows_count > 0 && strcmp(resultset->rows[0]->fields[0], "1") == 0, "init: tsdb_backend_health table exists on disk"); delete resultset; } #endif // ============================================================ // Timer functions — MySQL_Threads_Handler_timetoget // ============================================================ static void test_timer_returns_false_when_disabled() { // When stats_mysql_connections is 0, timetoget should return false stats->variables.stats_mysql_connections = 0; bool result = stats->MySQL_Threads_Handler_timetoget(1000000ULL); ok(result == false, "MySQL_Threads_Handler_timetoget: returns false when interval is 0"); } static void test_timer_returns_true_on_first_call() { // With interval > 0 and curtime > next_timer (initially 0), should return true stats->variables.stats_mysql_connections = 5; bool result = stats->MySQL_Threads_Handler_timetoget(5000000ULL); ok(result == true, "MySQL_Threads_Handler_timetoget: returns true on first call (timer was 0)"); } static void test_timer_returns_false_before_interval() { // After a successful trigger, calling again immediately should return false stats->variables.stats_mysql_connections = 60; // 60 second interval // First call triggers and sets next_timer stats->MySQL_Threads_Handler_timetoget(1000000000ULL); // 1000 seconds in usec // Call again at almost the same time — should not trigger bool result = stats->MySQL_Threads_Handler_timetoget(1000000001ULL); ok(result == false, "MySQL_Threads_Handler_timetoget: returns false before interval elapses"); } static void test_timer_returns_true_after_interval() { stats->variables.stats_mysql_connections = 5; // 5 second interval // First call at t=1000s triggers stats->MySQL_Threads_Handler_timetoget(1000000000ULL); // Call again at t=1006s — should trigger (5s+ elapsed) bool result = stats->MySQL_Threads_Handler_timetoget(1006000000ULL); ok(result == true, "MySQL_Threads_Handler_timetoget: returns true after interval elapses"); } // ============================================================ // Timer functions — MySQL_Query_Cache_timetoget // ============================================================ static void test_query_cache_timer_disabled() { stats->variables.stats_mysql_query_cache = 0; ok(stats->MySQL_Query_Cache_timetoget(5000000ULL) == false, "MySQL_Query_Cache_timetoget: returns false when interval is 0"); } static void test_query_cache_timer_triggers() { stats->variables.stats_mysql_query_cache = 10; ok(stats->MySQL_Query_Cache_timetoget(2000000000ULL) == true, "MySQL_Query_Cache_timetoget: returns true on first call"); } // ============================================================ // Timer functions — system_cpu_timetoget // ============================================================ static void test_system_cpu_timer_disabled() { stats->variables.stats_system_cpu = 0; ok(stats->system_cpu_timetoget(5000000ULL) == false, "system_cpu_timetoget: returns false when interval is 0"); } static void test_system_cpu_timer_triggers() { stats->variables.stats_system_cpu = 10; ok(stats->system_cpu_timetoget(3000000000ULL) == true, "system_cpu_timetoget: returns true on first call"); } // ============================================================ // Timer functions — eventslog timers // ============================================================ static void test_mysql_eventslog_timer_disabled() { stats->variables.stats_mysql_eventslog_sync_buffer_to_disk = 0; ok(stats->MySQL_Logger_dump_eventslog_timetoget(5000000ULL) == false, "MySQL_Logger_dump_eventslog_timetoget: returns false when interval is 0"); } static void test_mysql_eventslog_timer_triggers() { stats->variables.stats_mysql_eventslog_sync_buffer_to_disk = 5; ok(stats->MySQL_Logger_dump_eventslog_timetoget(6000000ULL) == true, "MySQL_Logger_dump_eventslog_timetoget: returns true on first call"); } static void test_mysql_eventslog_timer_no_retrigger() { stats->variables.stats_mysql_eventslog_sync_buffer_to_disk = 10; stats->MySQL_Logger_dump_eventslog_timetoget(10000000ULL); // triggers, sets last_timer ok(stats->MySQL_Logger_dump_eventslog_timetoget(15000000ULL) == false, "MySQL_Logger_dump_eventslog_timetoget: returns false before 10s elapses"); } static void test_pgsql_eventslog_timer_disabled() { stats->variables.stats_pgsql_eventslog_sync_buffer_to_disk = 0; ok(stats->PgSQL_Logger_dump_eventslog_timetoget(5000000ULL) == false, "PgSQL_Logger_dump_eventslog_timetoget: returns false when interval is 0"); } static void test_pgsql_eventslog_timer_triggers() { stats->variables.stats_pgsql_eventslog_sync_buffer_to_disk = 5; ok(stats->PgSQL_Logger_dump_eventslog_timetoget(6000000ULL) == true, "PgSQL_Logger_dump_eventslog_timetoget: returns true on first call"); } // ============================================================ // Timer functions — mysql_query_digest_to_disk_timetoget // ============================================================ static void test_digest_to_disk_timer_disabled() { stats->variables.stats_mysql_query_digest_to_disk = 0; ok(stats->mysql_query_digest_to_disk_timetoget(5000000ULL) == false, "mysql_query_digest_to_disk_timetoget: returns false when interval is 0"); } static void test_digest_to_disk_timer_triggers() { stats->variables.stats_mysql_query_digest_to_disk = 10; ok(stats->mysql_query_digest_to_disk_timetoget(4000000000ULL) == true, "mysql_query_digest_to_disk_timetoget: returns true on first call"); } // ============================================================ // TSDB variable management (PROXYSQLTSDB only) // ============================================================ #ifdef PROXYSQLTSDB static void test_set_variable_enabled() { ok(stats->set_variable("enabled", "1") == true, "set_variable: 'enabled' accepts value '1'"); ok(stats->set_variable("enabled", "0") == true, "set_variable: 'enabled' accepts value '0'"); } static void test_set_variable_enabled_out_of_range() { ok(stats->set_variable("enabled", "2") == false, "set_variable: 'enabled' rejects value '2' (max=1)"); ok(stats->set_variable("enabled", "-1") == false, "set_variable: 'enabled' rejects value '-1' (min=0)"); } static void test_set_variable_sample_interval() { ok(stats->set_variable("sample_interval", "1") == true, "set_variable: 'sample_interval' accepts min value '1'"); ok(stats->set_variable("sample_interval", "3600") == true, "set_variable: 'sample_interval' accepts max value '3600'"); ok(stats->set_variable("sample_interval", "0") == false, "set_variable: 'sample_interval' rejects value '0' (below min)"); ok(stats->set_variable("sample_interval", "3601") == false, "set_variable: 'sample_interval' rejects value '3601' (above max)"); } static void test_set_variable_retention_days() { ok(stats->set_variable("retention_days", "1") == true, "set_variable: 'retention_days' accepts value '1'"); ok(stats->set_variable("retention_days", "3650") == true, "set_variable: 'retention_days' accepts max value '3650'"); ok(stats->set_variable("retention_days", "0") == false, "set_variable: 'retention_days' rejects '0'"); ok(stats->set_variable("retention_days", "3651") == false, "set_variable: 'retention_days' rejects '3651'"); } static void test_set_variable_monitor_enabled() { ok(stats->set_variable("monitor_enabled", "0") == true, "set_variable: 'monitor_enabled' accepts '0'"); ok(stats->set_variable("monitor_enabled", "1") == true, "set_variable: 'monitor_enabled' accepts '1'"); ok(stats->set_variable("monitor_enabled", "3") == false, "set_variable: 'monitor_enabled' rejects '3'"); } static void test_set_variable_monitor_interval() { ok(stats->set_variable("monitor_interval", "10") == true, "set_variable: 'monitor_interval' accepts '10'"); ok(stats->set_variable("monitor_interval", "0") == false, "set_variable: 'monitor_interval' rejects '0'"); } static void test_set_variable_invalid_name() { ok(stats->set_variable("nonexistent_var", "1") == false, "set_variable: unknown variable name returns false"); } static void test_set_variable_null_inputs() { ok(stats->set_variable(nullptr, "1") == false, "set_variable: null name returns false"); ok(stats->set_variable("enabled", nullptr) == false, "set_variable: null value returns false"); ok(stats->set_variable("enabled", "") == false, "set_variable: empty value returns false"); } static void test_set_variable_non_integer_value() { ok(stats->set_variable("enabled", "abc") == false, "set_variable: non-integer value returns false"); ok(stats->set_variable("enabled", "1.5") == false, "set_variable: float value returns false"); ok(stats->set_variable("enabled", "1abc") == false, "set_variable: trailing garbage returns false"); } static void test_set_variable_case_insensitive() { ok(stats->set_variable("ENABLED", "1") == true, "set_variable: variable name is case-insensitive (uppercase)"); ok(stats->set_variable("Enabled", "0") == true, "set_variable: variable name is case-insensitive (mixed case)"); } static void test_get_variable() { stats->set_variable("enabled", "1"); char *val = stats->get_variable("enabled"); ok(val != nullptr && strcmp(val, "1") == 0, "get_variable: returns '1' after set_variable('enabled', '1')"); free(val); stats->set_variable("sample_interval", "42"); val = stats->get_variable("sample_interval"); ok(val != nullptr && strcmp(val, "42") == 0, "get_variable: returns '42' after set_variable('sample_interval', '42')"); free(val); stats->set_variable("retention_days", "365"); val = stats->get_variable("retention_days"); ok(val != nullptr && strcmp(val, "365") == 0, "get_variable: returns '365' after set_variable('retention_days', '365')"); free(val); stats->set_variable("monitor_enabled", "1"); val = stats->get_variable("monitor_enabled"); ok(val != nullptr && strcmp(val, "1") == 0, "get_variable: returns '1' for monitor_enabled"); free(val); stats->set_variable("monitor_interval", "30"); val = stats->get_variable("monitor_interval"); ok(val != nullptr && strcmp(val, "30") == 0, "get_variable: returns '30' for monitor_interval"); free(val); } static void test_get_variable_null_and_unknown() { ok(stats->get_variable(nullptr) == nullptr, "get_variable: null name returns null"); ok(stats->get_variable("nonexistent") == nullptr, "get_variable: unknown name returns null"); } static void test_has_variable() { ok(stats->has_variable("enabled") == true, "has_variable: 'enabled' exists"); ok(stats->has_variable("sample_interval") == true, "has_variable: 'sample_interval' exists"); ok(stats->has_variable("retention_days") == true, "has_variable: 'retention_days' exists"); ok(stats->has_variable("monitor_enabled") == true, "has_variable: 'monitor_enabled' exists"); ok(stats->has_variable("monitor_interval") == true, "has_variable: 'monitor_interval' exists"); ok(stats->has_variable("nonexistent") == false, "has_variable: 'nonexistent' does not exist"); ok(stats->has_variable(nullptr) == false, "has_variable: null returns false"); } static void test_get_variables_list() { char **list = stats->get_variables_list(); ok(list != nullptr, "get_variables_list: returns non-null"); int count = 0; if (list) { while (list[count] != nullptr) count++; } ok(count == 5, "get_variables_list: returns 5 variables (got %d)", count); // Free the list if (list) { for (int i = 0; list[i]; i++) free(list[i]); free(list); } } // ============================================================ // TSDB timer functions // ============================================================ static void test_tsdb_sampler_timer_disabled() { stats->set_variable("enabled", "0"); ok(stats->tsdb_sampler_timetoget(5000000ULL) == false, "tsdb_sampler_timetoget: returns false when TSDB disabled"); } static void test_tsdb_sampler_timer_triggers() { stats->set_variable("enabled", "1"); stats->set_variable("sample_interval", "5"); // next_timer starts at 0, curtime > 0 should trigger ok(stats->tsdb_sampler_timetoget(1000000ULL) == true, "tsdb_sampler_timetoget: returns true on first call when enabled"); } static void test_tsdb_sampler_timer_no_retrigger() { stats->set_variable("enabled", "1"); stats->set_variable("sample_interval", "10"); stats->tsdb_sampler_timetoget(1000000ULL); // triggers, sets next = 1 + 10s = 11000000 ok(stats->tsdb_sampler_timetoget(5000000ULL) == false, "tsdb_sampler_timetoget: returns false before interval elapses"); } static void test_tsdb_downsample_timer() { stats->set_variable("enabled", "0"); ok(stats->tsdb_downsample_timetoget(5000000ULL) == false, "tsdb_downsample_timetoget: returns false when disabled"); stats->set_variable("enabled", "1"); ok(stats->tsdb_downsample_timetoget(1000000ULL) == true, "tsdb_downsample_timetoget: returns true on first call when enabled"); } static void test_tsdb_monitor_timer() { stats->set_variable("enabled", "1"); stats->set_variable("monitor_enabled", "0"); ok(stats->tsdb_monitor_timetoget(5000000ULL) == false, "tsdb_monitor_timetoget: returns false when monitor disabled"); stats->set_variable("monitor_enabled", "1"); stats->set_variable("monitor_interval", "10"); ok(stats->tsdb_monitor_timetoget(1000000ULL) == true, "tsdb_monitor_timetoget: returns true on first call when both enabled"); } static void test_tsdb_retention_timer() { stats->set_variable("enabled", "0"); ok(stats->tsdb_retention_timetoget(5000000ULL) == false, "tsdb_retention_timetoget: returns false when disabled"); stats->set_variable("enabled", "1"); ok(stats->tsdb_retention_timetoget(1000000ULL) == true, "tsdb_retention_timetoget: returns true on first call when enabled"); } // ============================================================ // TSDB metric insertion and query // ============================================================ static void test_insert_and_query_tsdb_metric() { stats->set_variable("enabled", "1"); std::map labels; labels["instance"] = "localhost:6032"; time_t now = time(nullptr); stats->insert_tsdb_metric("test_metric", labels, 42.5, now); stats->insert_tsdb_metric("test_metric", labels, 43.0, now + 1); // Query back std::map no_filter; SQLite3_result *result = stats->query_tsdb_metrics("test_metric", no_filter, now - 10, now + 10); ok(result != nullptr, "query_tsdb_metrics: returns non-null result for inserted metric"); if (result) { ok(result->rows_count == 2, "query_tsdb_metrics: returns 2 rows (got %lu)", (unsigned long)result->rows_count); delete result; } else { ok(false, "query_tsdb_metrics: returns 2 rows (null result)"); } } static void test_query_tsdb_metric_with_label_filter() { time_t now = time(nullptr) + 100; // offset to not collide with previous test std::map labels_a; labels_a["host"] = "server_a"; stats->insert_tsdb_metric("cpu_usage", labels_a, 75.0, now); std::map labels_b; labels_b["host"] = "server_b"; stats->insert_tsdb_metric("cpu_usage", labels_b, 90.0, now); // Filter by label std::map filter; filter["host"] = "server_a"; SQLite3_result *result = stats->query_tsdb_metrics("cpu_usage", filter, now - 10, now + 10); ok(result != nullptr, "query_tsdb_metrics: label filter returns non-null"); if (result) { ok(result->rows_count == 1, "query_tsdb_metrics: label filter returns 1 row (got %lu)", (unsigned long)result->rows_count); delete result; } else { ok(false, "query_tsdb_metrics: label filter returns 1 row (null result)"); } } static void test_query_tsdb_metric_with_single_quote_in_name() { // This exercises escape_sql_string_literal indirectly time_t now = time(nullptr) + 200; std::map labels; stats->insert_tsdb_metric("metric_with'quote", labels, 10.0, now); std::map no_filter; SQLite3_result *result = stats->query_tsdb_metrics("metric_with'quote", no_filter, now - 10, now + 10); ok(result != nullptr, "query_tsdb_metrics: metric name with single quote works (escape_sql_string_literal)"); if (result) { ok(result->rows_count == 1, "query_tsdb_metrics: single-quote metric returns 1 row (got %lu)", (unsigned long)result->rows_count); delete result; } else { ok(false, "query_tsdb_metrics: single-quote metric returns 1 row (null result)"); } } static void test_query_tsdb_metric_swapped_time_range() { // query_tsdb_metrics should swap from/to if to < from time_t now = time(nullptr) + 300; std::map labels; stats->insert_tsdb_metric("swap_test", labels, 1.0, now); std::map no_filter; SQLite3_result *result = stats->query_tsdb_metrics("swap_test", no_filter, now + 10, now - 10); ok(result != nullptr, "query_tsdb_metrics: swapped time range still returns result"); if (result) { ok(result->rows_count == 1, "query_tsdb_metrics: swapped range returns correct row count"); delete result; } else { ok(false, "query_tsdb_metrics: swapped range returns correct row count (null)"); } } static void test_query_tsdb_metric_invalid_label_key() { // valid_label_key rejects keys with special chars — query should return NULL time_t now = time(nullptr) + 400; std::map bad_filter; bad_filter["bad key!"] = "value"; // space and ! are invalid SQLite3_result *result = stats->query_tsdb_metrics("some_metric", bad_filter, now - 10, now + 10); ok(result == nullptr, "query_tsdb_metrics: invalid label key returns null (valid_label_key rejects it)"); } // ============================================================ // TSDB backend health // ============================================================ static void test_insert_and_query_backend_health() { time_t now = time(nullptr) + 500; stats->insert_backend_health(1, "db1.example.com", 3306, true, 5, now); stats->insert_backend_health(1, "db2.example.com", 3306, false, -1, now); stats->insert_backend_health(2, "db3.example.com", 5432, true, 2, now); SQLite3_result *result = stats->get_backend_health_metrics(now - 10, now + 10); ok(result != nullptr, "get_backend_health_metrics: returns non-null for inserted data"); if (result) { ok(result->rows_count == 3, "get_backend_health_metrics: returns 3 rows (got %lu)", (unsigned long)result->rows_count); delete result; } else { ok(false, "get_backend_health_metrics: returns 3 rows (null result)"); } // Filter by hostgroup result = stats->get_backend_health_metrics(now - 10, now + 10, 1); ok(result != nullptr, "get_backend_health_metrics: hostgroup filter returns non-null"); if (result) { ok(result->rows_count == 2, "get_backend_health_metrics: hostgroup=1 returns 2 rows (got %lu)", (unsigned long)result->rows_count); delete result; } else { ok(false, "get_backend_health_metrics: hostgroup=1 returns 2 rows (null result)"); } } // ============================================================ // TSDB status // ============================================================ static void test_get_tsdb_status() { // We have already inserted some metrics above ProxySQL_Statistics::tsdb_status_t st = stats->get_tsdb_status(); ok(st.total_datapoints > 0, "get_tsdb_status: total_datapoints > 0 (got %zu)", st.total_datapoints); ok(st.total_series > 0, "get_tsdb_status: total_series > 0 (got %zu)", st.total_series); ok(st.newest_datapoint > 0, "get_tsdb_status: newest_datapoint is set"); } // ============================================================ // TSDB downsampling // ============================================================ static void test_tsdb_downsample_disabled() { stats->set_variable("enabled", "0"); // Should just return without error stats->tsdb_downsample_metrics(); ok(true, "tsdb_downsample_metrics: does not crash when TSDB disabled"); } static void test_tsdb_retention_cleanup_disabled() { stats->set_variable("enabled", "0"); stats->tsdb_retention_cleanup(); ok(true, "tsdb_retention_cleanup: does not crash when TSDB disabled"); } static void test_tsdb_downsample_runs() { stats->set_variable("enabled", "1"); // Insert some metrics at specific bucket-aligned times time_t hour_start = (time(nullptr) / 3600) * 3600 - 7200; // 2 hours ago std::map labels; for (int i = 0; i < 10; i++) { stats->insert_tsdb_metric("downsample_test", labels, (double)i, hour_start + i * 60); } stats->tsdb_downsample_metrics(); ok(true, "tsdb_downsample_metrics: runs without error on data"); // Check that hourly data was created SQLite3_result *resultset = nullptr; char *error = nullptr; int cols = 0, affected_rows = 0; stats->statsdb_disk->execute_statement( "SELECT COUNT(*) FROM tsdb_metrics_hour WHERE metric_name='downsample_test'", &error, &cols, &affected_rows, &resultset); if (error) { free(error); error = nullptr; } bool has_hourly = false; if (resultset && resultset->rows_count > 0 && resultset->rows[0]->fields[0]) { has_hourly = atoi(resultset->rows[0]->fields[0]) > 0; } ok(has_hourly, "tsdb_downsample_metrics: creates hourly aggregated data"); delete resultset; } static void test_tsdb_retention_cleanup_runs() { stats->set_variable("enabled", "1"); stats->set_variable("retention_days", "1"); // Insert old data time_t old_time = time(nullptr) - 86400 * 5; // 5 days ago std::map labels; stats->insert_tsdb_metric("old_metric", labels, 1.0, old_time); stats->tsdb_retention_cleanup(); // Verify old data was removed SQLite3_result *resultset = nullptr; char *error = nullptr; int cols = 0, affected_rows = 0; char buf[256]; snprintf(buf, sizeof(buf), "SELECT COUNT(*) FROM tsdb_metrics WHERE metric_name='old_metric' AND timestamp < %ld", (long)(time(nullptr) - 86400)); stats->statsdb_disk->execute_statement(buf, &error, &cols, &affected_rows, &resultset); if (error) { free(error); error = nullptr; } bool old_removed = false; if (resultset && resultset->rows_count > 0 && resultset->rows[0]->fields[0]) { old_removed = atoi(resultset->rows[0]->fields[0]) == 0; } ok(old_removed, "tsdb_retention_cleanup: removes data older than retention_days"); delete resultset; } #endif // PROXYSQLTSDB // ============================================================ // Main // ============================================================ int main() { // Count tests int num_tests = 0; // Constructor + init tests: 5 num_tests += 5; // Timer tests: 15 num_tests += 15; #ifdef PROXYSQLTSDB // TSDB table init: 3 num_tests += 3; // Variable management: 42 num_tests += 42; // TSDB timers: 9 num_tests += 9; // TSDB metric insert/query: 9 num_tests += 9; // TSDB backend health: 4 num_tests += 4; // TSDB status: 3 num_tests += 3; // TSDB downsampling/retention: 5 num_tests += 5; #endif plan(num_tests); test_init_minimal(); setup_stats(); // Constructor + init test_constructor_creates_databases(); test_init_creates_tables(); // Timer tests test_timer_returns_false_when_disabled(); test_timer_returns_true_on_first_call(); test_timer_returns_false_before_interval(); test_timer_returns_true_after_interval(); test_query_cache_timer_disabled(); test_query_cache_timer_triggers(); test_system_cpu_timer_disabled(); test_system_cpu_timer_triggers(); test_mysql_eventslog_timer_disabled(); test_mysql_eventslog_timer_triggers(); test_mysql_eventslog_timer_no_retrigger(); test_pgsql_eventslog_timer_disabled(); test_pgsql_eventslog_timer_triggers(); test_digest_to_disk_timer_disabled(); test_digest_to_disk_timer_triggers(); #ifdef PROXYSQLTSDB // TSDB table init test_init_creates_tsdb_tables(); // Variable management test_set_variable_enabled(); test_set_variable_enabled_out_of_range(); test_set_variable_sample_interval(); test_set_variable_retention_days(); test_set_variable_monitor_enabled(); test_set_variable_monitor_interval(); test_set_variable_invalid_name(); test_set_variable_null_inputs(); test_set_variable_non_integer_value(); test_set_variable_case_insensitive(); test_get_variable(); test_get_variable_null_and_unknown(); test_has_variable(); test_get_variables_list(); // TSDB timers test_tsdb_sampler_timer_disabled(); test_tsdb_sampler_timer_triggers(); test_tsdb_sampler_timer_no_retrigger(); test_tsdb_downsample_timer(); test_tsdb_monitor_timer(); test_tsdb_retention_timer(); // TSDB metric insert/query test_insert_and_query_tsdb_metric(); test_query_tsdb_metric_with_label_filter(); test_query_tsdb_metric_with_single_quote_in_name(); test_query_tsdb_metric_swapped_time_range(); test_query_tsdb_metric_invalid_label_key(); // TSDB backend health test_insert_and_query_backend_health(); // TSDB status test_get_tsdb_status(); // TSDB downsampling/retention test_tsdb_downsample_disabled(); test_tsdb_retention_cleanup_disabled(); test_tsdb_downsample_runs(); test_tsdb_retention_cleanup_runs(); #endif teardown_stats(); test_cleanup_minimal(); return exit_status(); }