Skip to content

Commit dd847a1

Browse files
authored
Merge pull request #5215 from sysown/feature/add-tap-test-documentation
Add TAP test writing guide and GitHub automation improvements
2 parents 29a6b85 + 4989960 commit dd847a1

File tree

2 files changed

+187
-0
lines changed

2 files changed

+187
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,3 +151,4 @@ proxysql-save.cfg
151151
test/tap/tests/test_cluster_sync_config/cluster_sync_node_stderr.txt
152152
test/tap/tests/test_cluster_sync_config/proxysql*.pem
153153
test/tap/tests/test_cluster_sync_config/test_cluster_sync.cnf
154+
GEMINI.md

doc/TAP_TESTS_GUIDE.md

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
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

Comments
 (0)