|
| 1 | +# How to Write TAP Tests for ProxySQL |
| 2 | + |
| 3 | +This guide provides instructions and best practices for writing Test Anything Protocol (TAP) tests for ProxySQL. The tests are written in C++ and leverage a common framework to interact with ProxySQL instances, check for expected behavior, and report results. |
| 4 | + |
| 5 | +## Anatomy of a TAP Test File |
| 6 | + |
| 7 | +A typical TAP test file (`*-t.cpp`) has the following structure: |
| 8 | + |
| 9 | +1. **Includes:** Essential headers are included. |
| 10 | + * `"tap.h"`: The core TAP library for test reporting (`plan`, `ok`, `diag`, `exit_status`). |
| 11 | + * `"command_line.h"`: A helper for reading connection parameters (host, port, user, password) from environment variables. |
| 12 | + * `"utils.h"`: Provides various utility functions and macros, like `MYSQL_QUERY`. |
| 13 | + * Protocol-specific headers: `"mysql.h"` for MySQL or `"libpq-fe.h"` for PostgreSQL tests. |
| 14 | + * Standard C++ libraries (`<string>`, `<vector>`, `<chrono>`, etc.). |
| 15 | + |
| 16 | +2. **`main()` Function:** The entry point for the test. |
| 17 | + * **`plan(N)`:** The first thing you should do is declare how many tests you plan to run. `N` is the total number of `ok()` calls in your test. This can be calculated dynamically if the number of tests depends on other factors. |
| 18 | + * **`CommandLine cl;`:** An object to manage command-line and environment variables. `cl.getEnv()` reads the necessary configuration. |
| 19 | + * **Connections:** Establish one or more connections to ProxySQL. Typically, you need at least two: |
| 20 | + * An **admin connection** to configure ProxySQL, load settings, and check statistics tables. |
| 21 | + * A **client connection** to the standard proxy port to simulate application behavior. |
| 22 | + * **Setup (Arrange):** Prepare the test environment. This is a critical step to ensure your test is isolated and predictable. Common setup tasks include: |
| 23 | + * Deleting existing rules or data from previous test runs (e.g., `DELETE FROM mysql_query_rules`). |
| 24 | + * Setting global variables (`UPDATE global_variables SET ...`). |
| 25 | + * Inserting new configuration (e.g., `INSERT INTO mysql_users ...`). |
| 26 | + * Loading the new configuration into the running ProxySQL instance (e.g., `LOAD MYSQL USERS TO RUNTIME`). |
| 27 | + * **Execution (Act):** Perform the actions you want to test. This could be running a specific query, killing a connection, or triggering a cluster sync. |
| 28 | + * **Verification (Assert):** Check that the outcome of the action is what you expected. This is done using the `ok()` macro. |
| 29 | + * `ok(condition, "description of the test");` |
| 30 | + * The `condition` is a boolean expression. If true, the test passes. If false, it fails. |
| 31 | + * The description should clearly state what is being tested. |
| 32 | + * **`diag("message")`:** Print diagnostic messages to stderr. This is useful for showing the test's progress or debugging failures. These messages are not counted as test results. |
| 33 | + * **Cleanup:** Restore the original state if necessary (e.g., `LOAD MYSQL VARIABLES FROM DISK`). |
| 34 | + * **`return exit_status();`:** Finally, return the overall test status. The test runner uses this exit code. |
| 35 | + |
| 36 | +## Key Principles for Good Tests |
| 37 | + |
| 38 | +### 1. Be Isolated and Self-Contained |
| 39 | + |
| 40 | +A test should not depend on the state left behind by other tests. |
| 41 | + |
| 42 | +* **GOOD:** Start by deleting any existing configuration relevant to your test. |
| 43 | + ```cpp |
| 44 | + // test_firewall-t.cpp |
| 45 | + MYSQL_QUERY(mysqladmin, "delete from mysql_firewall_whitelist_users"); |
| 46 | + MYSQL_QUERY(mysqladmin, "delete from mysql_firewall_whitelist_rules"); |
| 47 | + MYSQL_QUERY(mysqladmin, "load mysql firewall to runtime"); |
| 48 | + ``` |
| 49 | +* **GOOD:** If you modify global variables, reload them from disk at the end of the test. |
| 50 | + ```cpp |
| 51 | + // test_firewall-t.cpp |
| 52 | + MYSQL_QUERY(mysqladmin, "load mysql variables from disk"); |
| 53 | + MYSQL_QUERY(mysqladmin, "load mysql variables to runtime"); |
| 54 | + ``` |
| 55 | + |
| 56 | +### 2. Verify State Through the Admin Interface |
| 57 | + |
| 58 | +The most reliable way to check ProxySQL's internal state is by querying the `stats` and `runtime` tables. |
| 59 | + |
| 60 | +* **GOOD:** To check if a connection was created, query `stats_mysql_connection_pool`. |
| 61 | + ```cpp |
| 62 | + // test_connection_annotation-t.cpp |
| 63 | + MYSQL_QUERY(proxysql_admin, "SELECT ConnUsed, ConnFree FROM stats.stats_mysql_connection_pool WHERE hostgroup=1"); |
| 64 | + // ... compare results before and after |
| 65 | + ``` |
| 66 | +* **GOOD:** To check if a query hit the cache, query `stats_mysql_query_digest`. |
| 67 | + ```cpp |
| 68 | + // test_query_cache_soft_ttl_pct-t.cpp |
| 69 | + const string STATS_QUERY_DIGEST = |
| 70 | + "SELECT hostgroup, SUM(count_star) FROM stats_mysql_query_digest " |
| 71 | + "WHERE digest_text = 'SELECT SLEEP(?)' GROUP BY hostgroup"; |
| 72 | + ``` |
| 73 | + |
| 74 | +### 3. Handle Asynchronicity |
| 75 | + |
| 76 | +Many operations in ProxySQL are asynchronous (e.g., connection killing, cluster synchronization). Your test must account for this. |
| 77 | + |
| 78 | +* **GOOD:** Use a polling loop with a timeout to wait for a condition to become true. This is more robust than a fixed `sleep()`. |
| 79 | + ```cpp |
| 80 | + // test_cluster1-t.cpp |
| 81 | + int module_in_sync(...) { |
| 82 | + while (i < num_retries && rc != 1) { |
| 83 | + // ... query stats_proxysql_servers_checksums and check if all nodes have the same checksum ... |
| 84 | + sleep(1); |
| 85 | + i++; |
| 86 | + } |
| 87 | + return (rc == 1 ? 0 : 1); // Return 0 on success |
| 88 | + } |
| 89 | + ``` |
| 90 | +* **ACCEPTABLE:** For simple cases where an action is expected to be fast, a short `sleep()` can be used. |
| 91 | + ```cpp |
| 92 | + // kill_connection-t.cpp |
| 93 | + std::string s = "KILL CONNECTION " + std::to_string(mythreadid[j]); |
| 94 | + MYSQL_QUERY(mysql, s.c_str()); |
| 95 | + sleep(1); // Give ProxySQL a moment to process the kill |
| 96 | + int rc = run_q(other_mysql_conn, "DO 1"); |
| 97 | + ok(rc != 0, "Connection should be killed"); |
| 98 | + ``` |
| 99 | + |
| 100 | +### 4. Use Helper Functions and Macros |
| 101 | + |
| 102 | +For complex or repetitive tasks, use helpers to make your test more readable and maintainable. |
| 103 | + |
| 104 | +* **GOOD:** The `test_cluster1-t.cpp` test defines `trigger_sync_and_check` to encapsulate the entire logic for testing one module's synchronization. |
| 105 | +* **GOOD:** The `pgsql-basic_tests-t.cpp` test defines a `PQEXEC` macro to wrap `PQexec` and add error checking, reducing boilerplate. |
| 106 | + |
| 107 | +### 5. Structure Complex Tests Clearly |
| 108 | + |
| 109 | +For features that require multiple steps or scenarios (like the REST API or PostgreSQL protocol tests), break the test into smaller functions. |
| 110 | + |
| 111 | +* **GOOD:** The `pgsql-basic_tests-t.cpp` has separate functions like `test_simple_query`, `test_insert_query`, `test_transaction_commit`, etc. This makes it easy to see what is being tested and to debug failures. |
| 112 | +* **GOOD:** The `reg_test_3223-restapi_return_codes-t.cpp` test defines its test cases in data structures (`std::vector` of structs) and then iterates over them. This data-driven approach is excellent for testing many variations of an input. |
| 113 | + |
| 114 | +## Example Template |
| 115 | + |
| 116 | +Here is a basic template to get you started. |
| 117 | + |
| 118 | +```cpp |
| 119 | +#include <string> |
| 120 | +#include <vector> |
| 121 | +#include <cstdio> |
| 122 | + |
| 123 | +#include "mysql.h" |
| 124 | +#include "tap.h" |
| 125 | +#include "command_line.h" |
| 126 | +#include "utils.h" |
| 127 | + |
| 128 | +int main(int argc, char** argv) { |
| 129 | + // 1. Declare the number of tests you will run. |
| 130 | + plan(3); |
| 131 | + |
| 132 | + CommandLine cl; |
| 133 | + if (cl.getEnv()) { |
| 134 | + diag("Failed to get the required environmental variables."); |
| 135 | + return exit_status(); |
| 136 | + } |
| 137 | + |
| 138 | + // 2. Establish connections. |
| 139 | + MYSQL* admin = mysql_init(NULL); |
| 140 | + if (!mysql_real_connect(admin, cl.host, cl.admin_username, cl.admin_password, NULL, cl.admin_port, NULL, 0)) { |
| 141 | + diag("Failed to connect to admin interface: %s", mysql_error(admin)); |
| 142 | + return exit_status(); |
| 143 | + } |
| 144 | + |
| 145 | + MYSQL* client = mysql_init(NULL); |
| 146 | + if (!mysql_real_connect(client, cl.host, cl.username, cl.password, NULL, cl.port, NULL, 0)) { |
| 147 | + diag("Failed to connect to client interface: %s", mysql_error(client)); |
| 148 | + mysql_close(admin); |
| 149 | + return exit_status(); |
| 150 | + } |
| 151 | + |
| 152 | + // 3. Arrange: Set up the test environment. |
| 153 | + diag("Setting up test: creating a new query rule."); |
| 154 | + MYSQL_QUERY(admin, "DELETE FROM mysql_query_rules WHERE rule_id=999"); |
| 155 | + MYSQL_QUERY(admin, "INSERT INTO mysql_query_rules (rule_id, active, match_pattern, destination_hostgroup) VALUES (999, 1, '^SELECT 123', 1)"); |
| 156 | + MYSQL_QUERY(admin, "LOAD MYSQL QUERY RULES TO RUNTIME"); |
| 157 | + |
| 158 | + // 4. Act & Assert: Run the test and verify the outcome. |
| 159 | + diag("Running a query that should match the rule."); |
| 160 | + int rc = mysql_query(client, "SELECT 123"); |
| 161 | + ok(rc == 0, "Query 'SELECT 123' should execute successfully."); |
| 162 | + |
| 163 | + // Verify state via statistics |
| 164 | + MYSQL_QUERY(admin, "SELECT hits FROM stats_mysql_query_rules WHERE rule_id=999"); |
| 165 | + MYSQL_RES* res = mysql_store_result(admin); |
| 166 | + ok(res && mysql_num_rows(res) == 1, "Rule 999 should exist in stats."); |
| 167 | + if (res && mysql_num_rows(res) == 1) { |
| 168 | + MYSQL_ROW row = mysql_fetch_row(res); |
| 169 | + ok(atoi(row[0]) == 1, "Rule 999 should have exactly 1 hit."); |
| 170 | + } |
| 171 | + mysql_free_result(res); |
| 172 | + |
| 173 | + |
| 174 | + // 5. Cleanup |
| 175 | + diag("Cleaning up test rule."); |
| 176 | + MYSQL_QUERY(admin, "DELETE FROM mysql_query_rules WHERE rule_id=999"); |
| 177 | + MYSQL_QUERY(admin, "LOAD MYSQL QUERY RULES TO RUNTIME"); |
| 178 | + |
| 179 | + |
| 180 | + // 6. Close connections and exit. |
| 181 | + mysql_close(admin); |
| 182 | + mysql_close(client); |
| 183 | + |
| 184 | + return exit_status(); |
| 185 | +} |
| 186 | +``` |
0 commit comments