|
|
/**
|
|
|
* @file mcp_module-t.cpp
|
|
|
* @brief TAP test for the MCP module
|
|
|
*
|
|
|
* This test verifies the functionality of the MCP (Model Context Protocol) module in ProxySQL.
|
|
|
* It tests:
|
|
|
* - LOAD/SAVE commands for MCP variables across all variants
|
|
|
* - Variable access (SET and SELECT) for MCP variables
|
|
|
* - Variable persistence across storage layers (memory, disk, runtime)
|
|
|
* - CHECKSUM commands for MCP variables
|
|
|
* - SHOW VARIABLES for MCP module
|
|
|
*
|
|
|
* @date 2025-01-11
|
|
|
*/
|
|
|
|
|
|
#include <algorithm>
|
|
|
#include <string>
|
|
|
#include <string.h>
|
|
|
#include <stdio.h>
|
|
|
#include <unistd.h>
|
|
|
#include <vector>
|
|
|
#include <tuple>
|
|
|
|
|
|
#include "mysql.h"
|
|
|
#include "mysqld_error.h"
|
|
|
|
|
|
#include "tap.h"
|
|
|
#include "command_line.h"
|
|
|
#include "utils.h"
|
|
|
|
|
|
using std::string;
|
|
|
|
|
|
/**
|
|
|
* @brief Helper function to add LOAD/SAVE command variants for MCP module
|
|
|
*
|
|
|
* This function generates all the standard LOAD/SAVE command variants that
|
|
|
* ProxySQL supports for module variables.
|
|
|
*
|
|
|
* @param queries Vector to append the generated commands to
|
|
|
*/
|
|
|
void add_mcp_load_save_commands(std::vector<std::string>& queries) {
|
|
|
// LOAD commands - Memory variants
|
|
|
queries.push_back("LOAD MCP VARIABLES TO MEMORY");
|
|
|
queries.push_back("LOAD MCP VARIABLES TO MEM");
|
|
|
|
|
|
// LOAD from disk
|
|
|
queries.push_back("LOAD MCP VARIABLES FROM DISK");
|
|
|
|
|
|
// LOAD from memory
|
|
|
queries.push_back("LOAD MCP VARIABLES FROM MEMORY");
|
|
|
queries.push_back("LOAD MCP VARIABLES FROM MEM");
|
|
|
|
|
|
// LOAD to runtime
|
|
|
queries.push_back("LOAD MCP VARIABLES TO RUNTIME");
|
|
|
queries.push_back("LOAD MCP VARIABLES TO RUN");
|
|
|
|
|
|
// SAVE from memory
|
|
|
queries.push_back("SAVE MCP VARIABLES FROM MEMORY");
|
|
|
queries.push_back("SAVE MCP VARIABLES FROM MEM");
|
|
|
|
|
|
// SAVE to disk
|
|
|
queries.push_back("SAVE MCP VARIABLES TO DISK");
|
|
|
|
|
|
// SAVE to memory
|
|
|
queries.push_back("SAVE MCP VARIABLES TO MEMORY");
|
|
|
queries.push_back("SAVE MCP VARIABLES TO MEM");
|
|
|
|
|
|
// SAVE from runtime
|
|
|
queries.push_back("SAVE MCP VARIABLES FROM RUNTIME");
|
|
|
queries.push_back("SAVE MCP VARIABLES FROM RUN");
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* @brief Helper function to add LOAD/SAVE command variants for MCP PROFILES
|
|
|
*/
|
|
|
void add_mcp_profile_commands(std::vector<std::string>& queries) {
|
|
|
queries.push_back("LOAD MCP PROFILES TO MEMORY");
|
|
|
queries.push_back("LOAD MCP PROFILES FROM DISK");
|
|
|
queries.push_back("LOAD MCP PROFILES FROM MEMORY");
|
|
|
queries.push_back("LOAD MCP PROFILES FROM MEM");
|
|
|
queries.push_back("LOAD MCP PROFILES TO RUNTIME");
|
|
|
queries.push_back("LOAD MCP PROFILES TO RUN");
|
|
|
queries.push_back("SAVE MCP PROFILES TO MEMORY");
|
|
|
queries.push_back("SAVE MCP PROFILES TO MEM");
|
|
|
queries.push_back("SAVE MCP PROFILES FROM RUNTIME");
|
|
|
queries.push_back("SAVE MCP PROFILES FROM RUN");
|
|
|
queries.push_back("SAVE MCP PROFILES TO DISK");
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* @brief Get the value of an MCP variable as a string
|
|
|
*
|
|
|
* @param admin MySQL connection to admin interface
|
|
|
* @param var_name Variable name (without mcp- prefix)
|
|
|
* @return std::string The variable value, or empty string on error
|
|
|
*/
|
|
|
std::string get_mcp_variable(MYSQL* admin, const std::string& var_name) {
|
|
|
std::string query = "SELECT @@mcp-" + var_name;
|
|
|
if (mysql_query(admin, query.c_str()) != 0) {
|
|
|
return "";
|
|
|
}
|
|
|
|
|
|
MYSQL_RES* res = mysql_store_result(admin);
|
|
|
if (!res) {
|
|
|
return "";
|
|
|
}
|
|
|
|
|
|
MYSQL_ROW row = mysql_fetch_row(res);
|
|
|
std::string value = row && row[0] ? row[0] : "";
|
|
|
|
|
|
mysql_free_result(res);
|
|
|
return value;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* @brief Test variable access operations (SET and SELECT)
|
|
|
*
|
|
|
* Tests setting and retrieving MCP variables to ensure they work correctly.
|
|
|
*/
|
|
|
int test_variable_access(MYSQL* admin) {
|
|
|
int test_num = 0;
|
|
|
|
|
|
// Test 1: Get default value of mcp_enabled
|
|
|
std::string enabled_default = get_mcp_variable(admin, "enabled");
|
|
|
ok(enabled_default == "false",
|
|
|
"Default value of mcp_enabled is 'false', got '%s'", enabled_default.c_str());
|
|
|
|
|
|
// Test 2: Get default value of mcp_port
|
|
|
std::string port_default = get_mcp_variable(admin, "port");
|
|
|
ok(port_default == "6071",
|
|
|
"Default value of mcp_port is '6071', got '%s'", port_default.c_str());
|
|
|
|
|
|
// Test 3: Set mcp_enabled to true
|
|
|
MYSQL_QUERY(admin, "SET mcp-enabled=true");
|
|
|
std::string enabled_new = get_mcp_variable(admin, "enabled");
|
|
|
ok(enabled_new == "true",
|
|
|
"After SET, mcp_enabled is 'true', got '%s'", enabled_new.c_str());
|
|
|
|
|
|
// Test 4: Set mcp_port to a new value
|
|
|
MYSQL_QUERY(admin, "SET mcp-port=8080");
|
|
|
std::string port_new = get_mcp_variable(admin, "port");
|
|
|
ok(port_new == "8080",
|
|
|
"After SET, mcp_port is '8080', got '%s'", port_new.c_str());
|
|
|
|
|
|
// Test 5: Set mcp_config_endpoint_auth
|
|
|
MYSQL_QUERY(admin, "SET mcp-config_endpoint_auth='token123'");
|
|
|
std::string auth_config = get_mcp_variable(admin, "config_endpoint_auth");
|
|
|
ok(auth_config == "token123",
|
|
|
"After SET, mcp_config_endpoint_auth is 'token123', got '%s'", auth_config.c_str());
|
|
|
|
|
|
// Test 6: Set mcp_timeout_ms
|
|
|
MYSQL_QUERY(admin, "SET mcp-timeout_ms=60000");
|
|
|
std::string timeout = get_mcp_variable(admin, "timeout_ms");
|
|
|
ok(timeout == "60000",
|
|
|
"After SET, mcp_timeout_ms is '60000', got '%s'", timeout.c_str());
|
|
|
|
|
|
// Test 7: Verify SHOW VARIABLES LIKE pattern
|
|
|
MYSQL_QUERY(admin, "SHOW VARIABLES LIKE 'mcp-%'");
|
|
|
MYSQL_RES* res = mysql_store_result(admin);
|
|
|
int num_rows = mysql_num_rows(res);
|
|
|
ok(num_rows == 14,
|
|
|
"SHOW VARIABLES LIKE 'mcp-%%' returns 14 rows, got %d", num_rows);
|
|
|
mysql_free_result(res);
|
|
|
|
|
|
// Test 8: Restore default values
|
|
|
MYSQL_QUERY(admin, "SET mcp-enabled=false");
|
|
|
MYSQL_QUERY(admin, "SET mcp-port=6071");
|
|
|
MYSQL_QUERY(admin, "SET mcp-config_endpoint_auth=''");
|
|
|
MYSQL_QUERY(admin, "SET mcp-timeout_ms=30000");
|
|
|
MYSQL_QUERY(admin, "SET mcp-catalog_path='mcp_catalog.db'");
|
|
|
ok(1, "Restored default values for MCP variables");
|
|
|
|
|
|
return test_num;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* @brief Test variable persistence across storage layers
|
|
|
*
|
|
|
* Tests that variables are correctly copied between:
|
|
|
* - Memory (main.global_variables)
|
|
|
* - Disk (disk.global_variables)
|
|
|
* - Runtime (GloMCPH handler object)
|
|
|
*/
|
|
|
int test_variable_persistence(MYSQL* admin) {
|
|
|
int test_num = 0;
|
|
|
|
|
|
diag("=== Part 3: Testing variable persistence across storage layers ===");
|
|
|
diag("Testing variable persistence: Set values, save to disk, modify, load from disk");
|
|
|
|
|
|
// Test 1: Set values and save to disk
|
|
|
diag("Test 1: Setting mcp-enabled=true, mcp-port=7070, mcp-timeout_ms=90000");
|
|
|
MYSQL_QUERY(admin, "SET mcp-enabled=true");
|
|
|
MYSQL_QUERY(admin, "SET mcp-port=7070");
|
|
|
MYSQL_QUERY(admin, "SET mcp-timeout_ms=90000");
|
|
|
diag("Test 1: Saving variables to disk with 'SAVE MCP VARIABLES TO DISK'");
|
|
|
MYSQL_QUERY(admin, "SAVE MCP VARIABLES TO DISK");
|
|
|
ok(1, "Set mcp_enabled=true, mcp_port=7070, mcp_timeout_ms=90000 and saved to disk");
|
|
|
|
|
|
// Test 2: Modify values in memory
|
|
|
diag("Test 2: Modifying values in memory (mcp-enabled=false, mcp-port=8080)");
|
|
|
MYSQL_QUERY(admin, "SET mcp-enabled=false");
|
|
|
MYSQL_QUERY(admin, "SET mcp-port=8080");
|
|
|
std::string enabled_mem = get_mcp_variable(admin, "enabled");
|
|
|
std::string port_mem = get_mcp_variable(admin, "port");
|
|
|
diag("Test 2: After modification - mcp_enabled='%s', mcp_port='%s'", enabled_mem.c_str(), port_mem.c_str());
|
|
|
ok(enabled_mem == "false" && port_mem == "8080",
|
|
|
"Modified in memory: mcp_enabled='false', mcp_port='8080'");
|
|
|
|
|
|
// Test 3: Load from disk and verify original values restored
|
|
|
diag("Test 3: Loading variables from disk with 'LOAD MCP VARIABLES FROM DISK'");
|
|
|
MYSQL_QUERY(admin, "LOAD MCP VARIABLES FROM DISK");
|
|
|
std::string enabled_disk = get_mcp_variable(admin, "enabled");
|
|
|
std::string port_disk = get_mcp_variable(admin, "port");
|
|
|
std::string timeout_disk = get_mcp_variable(admin, "timeout_ms");
|
|
|
diag("Test 3: After LOAD FROM DISK - mcp_enabled='%s', mcp_port='%s', mcp_timeout_ms='%s'",
|
|
|
enabled_disk.c_str(), port_disk.c_str(), timeout_disk.c_str());
|
|
|
ok(enabled_disk == "true" && port_disk == "7070" && timeout_disk == "90000",
|
|
|
"After LOAD FROM DISK: mcp_enabled='true', mcp_port='7070', mcp_timeout_ms='90000'");
|
|
|
|
|
|
// Test 4: Save to memory and verify
|
|
|
diag("Test 4: Executing 'SAVE MCP VARIABLES TO MEMORY'");
|
|
|
MYSQL_QUERY(admin, "SAVE MCP VARIABLES TO MEMORY");
|
|
|
ok(1, "SAVE MCP VARIABLES TO MEMORY executed");
|
|
|
|
|
|
// Test 5: Load from memory
|
|
|
diag("Test 5: Executing 'LOAD MCP VARIABLES FROM MEMORY'");
|
|
|
MYSQL_QUERY(admin, "LOAD MCP VARIABLES FROM MEMORY");
|
|
|
ok(1, "LOAD MCP VARIABLES FROM MEMORY executed");
|
|
|
|
|
|
// Test 6: Test SAVE from runtime
|
|
|
diag("Test 6: Executing 'SAVE MCP VARIABLES FROM RUNTIME'");
|
|
|
MYSQL_QUERY(admin, "SAVE MCP VARIABLES FROM RUNTIME");
|
|
|
ok(1, "SAVE MCP VARIABLES FROM RUNTIME executed");
|
|
|
|
|
|
// Test 7: Test LOAD to runtime
|
|
|
diag("Test 7: Executing 'LOAD MCP VARIABLES TO RUNTIME'");
|
|
|
MYSQL_QUERY(admin, "LOAD MCP VARIABLES TO RUNTIME");
|
|
|
ok(1, "LOAD MCP VARIABLES TO RUNTIME executed");
|
|
|
|
|
|
// Test 8: Restore default values
|
|
|
diag("Test 8: Restoring default values");
|
|
|
MYSQL_QUERY(admin, "SET mcp-enabled=false");
|
|
|
MYSQL_QUERY(admin, "SET mcp-port=6071");
|
|
|
MYSQL_QUERY(admin, "SET mcp-config_endpoint_auth=''");
|
|
|
MYSQL_QUERY(admin, "SET mcp-observe_endpoint_auth=''");
|
|
|
MYSQL_QUERY(admin, "SET mcp-query_endpoint_auth=''");
|
|
|
MYSQL_QUERY(admin, "SET mcp-admin_endpoint_auth=''");
|
|
|
MYSQL_QUERY(admin, "SET mcp-cache_endpoint_auth=''");
|
|
|
MYSQL_QUERY(admin, "SET mcp-timeout_ms=30000");
|
|
|
MYSQL_QUERY(admin, "SET mcp-catalog_path='mcp_catalog.db'");
|
|
|
MYSQL_QUERY(admin, "SAVE MCP VARIABLES TO DISK");
|
|
|
ok(1, "Restored default values and saved to disk");
|
|
|
|
|
|
return test_num;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* @brief Test CHECKSUM commands for MCP variables
|
|
|
*
|
|
|
* Tests all CHECKSUM variants to ensure they work correctly.
|
|
|
*/
|
|
|
int test_checksum_commands(MYSQL* admin) {
|
|
|
int test_num = 0;
|
|
|
|
|
|
diag("=== Part 4: Testing CHECKSUM commands ===");
|
|
|
diag("Testing CHECKSUM commands for MCP variables");
|
|
|
|
|
|
// Test 1: CHECKSUM DISK MCP VARIABLES
|
|
|
diag("Test 1: Executing 'CHECKSUM DISK MCP VARIABLES'");
|
|
|
int rc1 = mysql_query(admin, "CHECKSUM DISK MCP VARIABLES");
|
|
|
diag("Test 1: Query returned with rc=%d", rc1);
|
|
|
ok(rc1 == 0, "CHECKSUM DISK MCP VARIABLES");
|
|
|
if (rc1 == 0) {
|
|
|
MYSQL_RES* res = mysql_store_result(admin);
|
|
|
int num_rows = mysql_num_rows(res);
|
|
|
diag("Test 1: Result has %d row(s)", num_rows);
|
|
|
ok(num_rows == 1, "CHECKSUM DISK MCP VARIABLES returns 1 row");
|
|
|
mysql_free_result(res);
|
|
|
} else {
|
|
|
diag("Test 1: Query failed with error: %s", mysql_error(admin));
|
|
|
skip(1, "Skipping row count check due to error");
|
|
|
}
|
|
|
|
|
|
// Test 2: CHECKSUM MEM MCP VARIABLES
|
|
|
diag("Test 2: Executing 'CHECKSUM MEM MCP VARIABLES'");
|
|
|
int rc2 = mysql_query(admin, "CHECKSUM MEM MCP VARIABLES");
|
|
|
diag("Test 2: Query returned with rc=%d", rc2);
|
|
|
ok(rc2 == 0, "CHECKSUM MEM MCP VARIABLES");
|
|
|
if (rc2 == 0) {
|
|
|
MYSQL_RES* res = mysql_store_result(admin);
|
|
|
int num_rows = mysql_num_rows(res);
|
|
|
diag("Test 2: Result has %d row(s)", num_rows);
|
|
|
ok(num_rows == 1, "CHECKSUM MEM MCP VARIABLES returns 1 row");
|
|
|
mysql_free_result(res);
|
|
|
} else {
|
|
|
diag("Test 2: Query failed with error: %s", mysql_error(admin));
|
|
|
skip(1, "Skipping row count check due to error");
|
|
|
}
|
|
|
|
|
|
// Test 3: CHECKSUM MEMORY MCP VARIABLES (alias for MEM)
|
|
|
diag("Test 3: Executing 'CHECKSUM MEMORY MCP VARIABLES' (alias for MEM)");
|
|
|
int rc3 = mysql_query(admin, "CHECKSUM MEMORY MCP VARIABLES");
|
|
|
diag("Test 3: Query returned with rc=%d", rc3);
|
|
|
ok(rc3 == 0, "CHECKSUM MEMORY MCP VARIABLES");
|
|
|
if (rc3 == 0) {
|
|
|
MYSQL_RES* res = mysql_store_result(admin);
|
|
|
int num_rows = mysql_num_rows(res);
|
|
|
diag("Test 3: Result has %d row(s)", num_rows);
|
|
|
ok(num_rows == 1, "CHECKSUM MEMORY MCP VARIABLES returns 1 row");
|
|
|
mysql_free_result(res);
|
|
|
} else {
|
|
|
diag("Test 3: Query failed with error: %s", mysql_error(admin));
|
|
|
skip(1, "Skipping row count check due to error");
|
|
|
}
|
|
|
|
|
|
// Test 4: CHECKSUM MCP VARIABLES (defaults to DISK)
|
|
|
diag("Test 4: Executing 'CHECKSUM MCP VARIABLES' (defaults to DISK)");
|
|
|
int rc4 = mysql_query(admin, "CHECKSUM MCP VARIABLES");
|
|
|
diag("Test 4: Query returned with rc=%d", rc4);
|
|
|
ok(rc4 == 0, "CHECKSUM MCP VARIABLES");
|
|
|
if (rc4 == 0) {
|
|
|
MYSQL_RES* res = mysql_store_result(admin);
|
|
|
int num_rows = mysql_num_rows(res);
|
|
|
diag("Test 4: Result has %d row(s)", num_rows);
|
|
|
ok(num_rows == 1, "CHECKSUM MCP VARIABLES returns 1 row");
|
|
|
mysql_free_result(res);
|
|
|
} else {
|
|
|
diag("Test 4: Query failed with error: %s", mysql_error(admin));
|
|
|
skip(1, "Skipping row count check due to error");
|
|
|
}
|
|
|
|
|
|
return test_num;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* @brief Main test function
|
|
|
*
|
|
|
* Orchestrates all MCP module tests.
|
|
|
*/
|
|
|
int main() {
|
|
|
CommandLine cl;
|
|
|
|
|
|
if (cl.getEnv()) {
|
|
|
diag("Failed to get the required environmental variables.");
|
|
|
return EXIT_FAILURE;
|
|
|
}
|
|
|
|
|
|
// Initialize connection to admin interface
|
|
|
MYSQL* admin = mysql_init(NULL);
|
|
|
if (!admin) {
|
|
|
fprintf(stderr, "File %s, line %d, Error: mysql_init failed\n", __FILE__, __LINE__);
|
|
|
return EXIT_FAILURE;
|
|
|
}
|
|
|
|
|
|
if (!mysql_real_connect(admin, cl.host, cl.admin_username, cl.admin_password,
|
|
|
NULL, cl.admin_port, NULL, 0)) {
|
|
|
fprintf(stderr, "File %s, line %d, Error: %s\n", __FILE__, __LINE__, mysql_error(admin));
|
|
|
return EXIT_FAILURE;
|
|
|
}
|
|
|
|
|
|
diag("Connected to ProxySQL admin interface at %s:%d", cl.host, cl.admin_port);
|
|
|
|
|
|
// Build the list of LOAD/SAVE commands to test
|
|
|
std::vector<std::string> queries;
|
|
|
add_mcp_load_save_commands(queries);
|
|
|
add_mcp_profile_commands(queries);
|
|
|
|
|
|
// Each command test = 2 tests (execution + optional result check)
|
|
|
// LOAD/SAVE commands: variables + profiles
|
|
|
// Variable access tests: 8 tests
|
|
|
// Persistence tests: 8 tests
|
|
|
// CHECKSUM tests: 8 tests (4 commands × 2)
|
|
|
int num_load_save_tests = (int)queries.size() * 2; // Each command + result check
|
|
|
int total_tests = num_load_save_tests + 8 + 8 + 8;
|
|
|
|
|
|
plan(total_tests);
|
|
|
|
|
|
int test_count = 0;
|
|
|
|
|
|
// ============================================================================
|
|
|
// Part 1: Test LOAD/SAVE commands
|
|
|
// ============================================================================
|
|
|
diag("=== Part 1: Testing LOAD/SAVE MCP VARIABLES commands ===");
|
|
|
for (const auto& query : queries) {
|
|
|
MYSQL* admin_local = mysql_init(NULL);
|
|
|
if (!admin_local) {
|
|
|
diag("Failed to initialize MySQL connection");
|
|
|
continue;
|
|
|
}
|
|
|
|
|
|
if (!mysql_real_connect(admin_local, cl.host, cl.admin_username, cl.admin_password,
|
|
|
NULL, cl.admin_port, NULL, 0)) {
|
|
|
diag("Failed to connect to admin interface");
|
|
|
mysql_close(admin_local);
|
|
|
continue;
|
|
|
}
|
|
|
|
|
|
int rc = run_q(admin_local, query.c_str());
|
|
|
ok(rc == 0, "Command executed successfully: %s", query.c_str());
|
|
|
|
|
|
// For SELECT/SHOW/CHECKSUM style commands, verify result set
|
|
|
if (strncasecmp(query.c_str(), "SELECT ", 7) == 0 ||
|
|
|
strncasecmp(query.c_str(), "SHOW ", 5) == 0 ||
|
|
|
strncasecmp(query.c_str(), "CHECKSUM ", 9) == 0) {
|
|
|
MYSQL_RES* res = mysql_store_result(admin_local);
|
|
|
unsigned long long num_rows = mysql_num_rows(res);
|
|
|
ok(num_rows != 0, "Command returned rows: %s", query.c_str());
|
|
|
mysql_free_result(res);
|
|
|
} else {
|
|
|
// For non-query commands, just mark the test as passed
|
|
|
ok(1, "Command completed: %s", query.c_str());
|
|
|
}
|
|
|
|
|
|
mysql_close(admin_local);
|
|
|
}
|
|
|
|
|
|
// ============================================================================
|
|
|
// Part 2: Test variable access (SET and SELECT)
|
|
|
// ============================================================================
|
|
|
diag("=== Part 2: Testing variable access (SET and SELECT) ===");
|
|
|
test_count += test_variable_access(admin);
|
|
|
|
|
|
// ============================================================================
|
|
|
// Part 3: Test variable persistence across layers
|
|
|
// ============================================================================
|
|
|
diag("=== Part 3: Testing variable persistence across storage layers ===");
|
|
|
test_count += test_variable_persistence(admin);
|
|
|
|
|
|
// ============================================================================
|
|
|
// Part 4: Test CHECKSUM commands
|
|
|
// ============================================================================
|
|
|
diag("=== Part 4: Testing CHECKSUM commands ===");
|
|
|
test_count += test_checksum_commands(admin);
|
|
|
|
|
|
// ============================================================================
|
|
|
// Cleanup
|
|
|
// ============================================================================
|
|
|
mysql_close(admin);
|
|
|
|
|
|
diag("=== All MCP module tests completed ===");
|
|
|
|
|
|
return exit_status();
|
|
|
}
|