doc/mysqlx/TESTING.md
The mysqlx plugin has 600+ test assertions across 24 test files in three tiers:
| Tier | Scope | Dependencies | Count |
|---|---|---|---|
| Tier 1: Pure Unit Tests | No external dependencies, no database, no globals | None | 21 files |
| Tier 2: Integration Tests | In-memory SQLite3, plugin .so loading, TCP listener | Built plugin .so | 3 files |
| Tier 3: End-to-End Tests | Real MySQL 8.x backend with X Protocol | MySQL 8.x sandbox | 2 files |
| File | Tier | Assertions | What It Tests |
|---|---|---|---|
mysqlx_protocol_unit-t.cpp | Unit | 42 | Frame header encode/decode, MYSQL41 hash/scramble/verify, hex encode/decode, error/ok frame via socketpair, auth method validation |
mysqlx_protocol_socket_unit-t.cpp | Unit | 20 | Socket I/O roundtrip (socketpair), FIFO ordering, EOF/EPIPE detection, large frames (64KB/1MB/16MB), binary data preservation |
mysqlx_stats_unit-t.cpp | Unit | 22 | Counter operations, SQLite flush, concurrent multi-thread stress tests |
mysqlx_config_store_unit-t.cpp | Unit | 16 | Runtime loading, identity resolution, endpoint picking, topology generation |
mysqlx_config_store_pure_unit-t.cpp | Unit | 25 | Auth mode parsing, identity edge cases (inactive/backend users), endpoint edge cases, topology generation atomicity |
mysqlx_config_store_concurrent_unit-t.cpp | Unit | 15 | Thread safety: concurrent resolve/pick/reload, data consistency after reload, 10-thread stress |
mysqlx_route_store_unit-t.cpp | Unit | 26 | Routing strategies (first_available, round_robin, round_robin_with_fallback), inactive routes, endpoint overrides |
mysqlx_admin_schema_unit-t.cpp | Unit | 15 | DDL registration, command registration, null guards |
plugin_manager_unit-t.cpp | Unit | 20 | Plugin lifecycle (load/init/start/stop), error cases, double lifecycle |
plugin_registry_unit-t.cpp | Unit | 25 | Table/command registration, duplicate rejection, canonicalization |
plugin_config_unit-t.cpp | Unit | 20 | Config parsing, lifecycle free functions, dispatch after stop |
test_mysqlx_admin_tables-t.cpp | Integration | 42 | Full LOAD/SAVE roundtrip with real plugin .so, alias dispatch, data integrity |
test_mysqlx_listener_smoke-t.cpp | Integration | 15 | Listener lifecycle, multi-port, TCP connect verification |
test_mysqlx_plugin_load-t.cpp | Integration | 6 | Plugin .so loading, schema registration |
test_mysqlx_e2e_handshake-t.cpp | E2E | 10 | Full X Protocol handshake against real MySQL, auth failure cases |
test_mysqlx_e2e_routing-t.cpp | E2E | 10 | Query routing through ProxySQL X listener, DDL/DML roundtrip |
| File | Tier | Assertions | What It Tests |
|---|---|---|---|
mysqlx_data_stream_unit-t.cpp | Unit | 15 | Non-blocking frame header parsing, partial frame buffering, multiple concatenated frames, write buffer format, feed_bytes accumulation |
mysqlx_connection_unit-t.cpp | Unit | 10 | Connection state transitions, hostgroup/user/schema metadata, multiplexing eligibility (transaction/stmt disable reuse), reusable flag logic |
mysqlx_session_unit-t.cpp | Unit | 42 | 22 session state transitions, capabilities negotiation flow, MYSQL41 auth challenge-response, auth failure handling, session reset, backend connection flow, frame forwarding, session closing |
mysqlx_thread_unit-t.cpp | Unit | 22 | Thread initialization, listener add/remove, poll set management, session registration/unregistration, connection cache operations, pool matching (hostgroup/user/schema), pool exclusion (transaction/stmt), max cache eviction |
mysqlx_message_dispatch_unit-t.cpp | Unit | 49 | All 23 client message type dispatch (locally handled vs forwarded), multi-frame response forwarding, unknown message type error generation, SESS_RESET forwarding, CRUD operations, prepared statement tracking, cursor operations, expect operations |
mysqlx_concurrent_unit-t.cpp | Unit | 15 | Multi-thread session stress: concurrent handler invocations, session registration/deregistration, connection cache races |
mysqlx_backend_auth_unit-t.cpp | Unit | 15 | Backend authentication modes (mapped/service_account/pass_through), credential override, auth mode validation |
mysqlx_credential_verify_unit-t.cpp | Unit | 15 | MYSQL41 credential verification: correct/incorrect password, empty password, edge cases, constant-time comparison |
mysqlx_robustness_unit-t.cpp | Unit | 20 | Session robustness: unexpected disconnects, malformed frames, resource cleanup, error recovery |
mysqlx_tls_unit-t.cpp | Unit | 20 | TLS handshake states, Memory BIO pattern, encrypted read/write paths, TLS mode enforcement (DISABLED/PREFERRED/REQUIRED) |
mysqlx_admin_commands_unit-t.cpp | Unit | 15 | LOAD/SAVE admin command dispatch, alias resolution, error handling |
mysqlx_admin_disk_commands_unit-t.cpp | Unit | 15 | LOAD FROM DISK / SAVE TO DISK commands, disk-to-memory roundtrip |
mysqlx_admin_alias_resolution_unit-t.cpp | Unit | 10 | Admin command alias resolution: TO RUN/FROM MEM/FROM RUN aliases |
Total: 600+ assertions across 24 files.
Unit tests require no database, no ProxySQL binary, and no network. They link
against libproxysql.a via the custom test harness.
cd test/tap/tests/unit
make mysqlx_protocol_unit-t mysqlx_stats_unit-t mysqlx_config_store_unit-t \
mysqlx_config_store_pure_unit-t mysqlx_config_store_concurrent_unit-t \
mysqlx_route_store_unit-t mysqlx_admin_schema_unit-t \
mysqlx_admin_commands_unit-t mysqlx_admin_disk_commands_unit-t \
mysqlx_admin_alias_resolution_unit-t \
plugin_manager_unit-t plugin_registry_unit-t plugin_config_unit-t \
mysqlx_data_stream_unit-t mysqlx_connection_unit-t \
mysqlx_session_unit-t mysqlx_thread_unit-t \
mysqlx_message_dispatch_unit-t mysqlx_concurrent_unit-t \
mysqlx_backend_auth_unit-t mysqlx_credential_verify_unit-t \
mysqlx_robustness_unit-t mysqlx_tls_unit-t
# Run all unit tests
for t in mysqlx_*_unit-t plugin_*_unit-t; do
./$t
done
Unit tests are also runnable via the test group system:
cd test/tap
make group-tests GROUP=unit-tests-g1
Integration tests load the actual mysqlx plugin shared object and may start a TCP listener. The plugin must be built first.
# Build the plugin
cd plugins/mysqlx && make
# Build and run integration tests
cd ../../test/tap/tests
make test_mysqlx_admin_tables-t test_mysqlx_listener_smoke-t test_mysqlx_plugin_load-t
./test_mysqlx_admin_tables-t
./test_mysqlx_listener_smoke-t
./test_mysqlx_plugin_load-t
End-to-end tests connect to a real MySQL 8.x instance with X Protocol enabled. They skip gracefully when the required environment variables are not set.
# Using dbdeployer:
dbdeployer deploy single 8.4.8
export MYSQLX_E2E_HOST=127.0.0.1
export MYSQLX_E2E_PORT=33060
export MYSQLX_E2E_USER=mysqlx_test
export MYSQLX_E2E_PASS=mysqlx_test
cd test/tap/tests
make test_mysqlx_e2e_handshake-t test_mysqlx_e2e_routing-t
./test_mysqlx_e2e_handshake-t
./test_mysqlx_e2e_routing-t
E2E environment variables:
| Variable | Default | Description |
|---|---|---|
MYSQLX_E2E_HOST | (none — tests skip) | MySQL X Protocol host |
MYSQLX_E2E_PORT | 33060 | MySQL X Protocol port |
MYSQLX_E2E_USER | mysqlx_test | Username for authentication |
MYSQLX_E2E_PASS | mysqlx_test | Password for authentication |
If MYSQLX_E2E_HOST is not set, all E2E tests print a diagnostic and exit
with success (TAP skip).
E2E tests are also runnable via the test group system:
cd test/tap
make group-tests GROUP=mysqlx-e2e-g1
The unit test harness provides minimal initialization without requiring a full ProxySQL daemon:
| File | Purpose |
|---|---|
test/tap/test_helpers/test_globals.h/cpp | Minimal global state initialization ( jemalloc, threading ) |
test/tap/test_helpers/test_init.h/cpp | Component initialization helpers for SQLite3, protobuf, etc. |
test/tap/test_helpers/fake_plugin.cpp | Test plugin .so with env-var-driven behavior for integration tests |
Most unit tests use in-memory SQLite3 databases to avoid filesystem side effects:
SQLite3DB db;
db.open((char*)":memory:", SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX);
db.execute("CREATE TABLE mysqlx_endpoints (id INTEGER PRIMARY KEY, ...)");
db.execute("INSERT INTO mysqlx_endpoints VALUES (1, ...)");
This pattern is used by:
mysqlx_config_store_unit-t.cppmysqlx_config_store_pure_unit-t.cppmysqlx_config_store_concurrent_unit-t.cppmysqlx_route_store_unit-t.cppmysqlx_stats_unit-t.cppmysqlx_admin_schema_unit-t.cppProtocol tests use socketpair() to create connected file descriptor pairs
without real networking:
int fds[2];
socketpair(AF_UNIX, SOCK_STREAM, 0, fds);
// Write an error frame on one end
mysqlx_send_error(fds[0], 1045, "Access denied");
// Read and verify on the other end
mysqlx_read_frame(fds[1], header, payload);
close(fds[0]);
close(fds[1]);
This pattern is used by:
mysqlx_protocol_unit-t.cpp — error/ok frame roundtripsmysqlx_protocol_socket_unit-t.cpp — large frames, FIFO ordering, EOF detectionThread-safety tests spawn multiple threads and verify exact counter values after all threads join:
const int N_THREADS = 10;
const int N_ITER = 10000;
std::atomic<int> barrier{0};
auto worker = [&]() {
barrier.fetch_add(1);
while (barrier.load() < N_THREADS) { /* spin */ }
for (int i = 0; i < N_ITER; i++) {
store.resolve_identity(...);
}
};
std::vector<std::thread> threads;
for (int i = 0; i < N_THREADS; i++) threads.emplace_back(worker);
for (auto& t : threads) t.join();
ok(total_resolves == N_THREADS * N_ITER, "all resolves accounted for");
This pattern is used by:
mysqlx_config_store_concurrent_unit-t.cppmysqlx_stats_unit-t.cpp (multi-thread stress section)v2 data stream tests use the feed_bytes() method to simulate network input:
MysqlxDataStream ds;
// Feed a complete frame
uint8_t frame[] = {0x0a, 0x00, 0x00, 0x00, 0x01, /* body */};
ds.feed_bytes(frame, sizeof(frame));
ok(ds.has_complete_frame(), "complete frame detected");
auto& pkt = ds.front_frame();
ok(pkt[4] == 0x01, "message type is CON_CAPABILITIES_GET");
ds.pop_frame();
ok(!ds.has_complete_frame(), "no more frames after pop");
v2 session tests exercise state transitions by calling handlers directly:
MysqlxSession sess;
sess.set_status(MysqlxSession::CONNECTING_CLIENT);
sess.handler_connecting_client();
ok(sess.get_status() == MysqlxSession::X_CAPABILITIES_GET,
"transitioned to X_CAPABILITIES_GET");
File: .github/workflows/CI-mysqlx.yml
The CI workflow defines two jobs:
unit-tests.soe2e-testsmain and on pull requests (with manual trigger option)dbdeployer is used instead of Docker Compose for backend provisioning. MySQL 8.x enables X Protocol by default on port 33060.
# Install dbdeployer
curl -s https://raw.githubusercontent.com/ProxySQL/dbdeployer/master/scripts/dbdeployer-install.sh | bash
# Download and deploy MySQL 8.4
dbdeployer downloads get-by-version 8.4 --newest --minimal
dbdeployer unpack mysql-8.4.*.tar.xz
dbdeployer deploy single 8.4.8
# Verify X Protocol is active
mysql -u root -S ~/sandboxes/msb_8_4_8/socket.sock -e "SELECT @@mysqlx_port"
# Expected: 33060
Tests are registered in test/tap/groups/groups.json:
| Group | Tests | Infrastructure |
|---|---|---|
unit-tests-g1 | All 21 unit tests | None (SKIP_PROXYSQL=1) |
mysqlx-e2e-g1 | 2 E2E tests | dbdeployer MySQL 8.4 sandbox |
| Component | Unit | Integration | E2E | Concurrent |
|---|---|---|---|---|
| Protocol frames | 42 | — | 10 | — |
| Socket I/O | 20 | — | — | — |
| MYSQL41 auth | (within 42) | — | 10 | — |
| Config store | 41 | — | — | 15 |
| Route store | 26 | — | — | — |
| Stats | 22 | — | — | (within 22) |
| Admin schema | 15 | 42 | — | — |
| Plugin lifecycle | 45 | 6 | — | — |
| Listener | — | 15 | — | — |
| Full handshake | — | — | 10 | — |
| Query routing | — | — | 10 | — |
| Component | Unit | Integration | E2E | Concurrent |
|---|---|---|---|---|
| Data stream (frame I/O) | 15 | — | — | — |
| Connection (pooled object) | 10 | — | — | — |
| Session state machine | 42 | — | — | — |
| Thread (event loop) | 22 | — | — | — |
| Message dispatch (23 types) | 49 | — | — | — |
Unit: 500+ assertions (83%)
Integration: 63 assertions (11%)
E2E: 20 assertions ( 3%)
Concurrent: 30 assertions ( 3%)
─────────
Total: 600+ assertions
| Tier | Directory | Naming Pattern |
|---|---|---|
| Unit | test/tap/tests/unit/ | mysqlx_<component>_unit-t.cpp |
| Integration | test/tap/tests/ | test_mysqlx_<feature>-t.cpp |
| E2E | test/tap/tests/ | test_mysqlx_e2e_<feature>-t.cpp |
Every test file must:
plan(N) at the top of main() where N is the exact number of ok() callsdiag() for diagnostic output (visible on failure)BAIL_OUT() for fatal, unrecoverable failuresskip() or todo() when conditions are not metexit_status() at the end of main()#include "tap.h"
int main() {
plan(5);
ok(1 + 1 == 2, "basic arithmetic");
is_str("hello", "hello", "string equality");
ok(true, "simple boolean");
if (!some_condition) {
skip(2, "condition not met");
} else {
ok(test_a(), "test A");
ok(test_b(), "test B");
}
return exit_status();
}
New tests must be registered in two places:
Makefile target — add to test/tap/tests/unit/Makefile (for unit tests)
or test/tap/tests/Makefile (for integration/E2E tests)
Test group — add to the appropriate group in
test/tap/groups/groups.json:
{
"unit-tests-g1": {
"tests": [
"unit/mysqlx_my_new_component_unit-t"
]
}
}
":memory:") for any SQL needssocketpair() for any I/O testingfeed_bytes() for data stream testinglibproxysql.a via the test harnesstest_globals.h and test_init.h for minimal initialization.sofake_plugin.cpp from test_helpers when you need a controllable pluginMYSQLX_E2E_HOST and skip if unsetstd::thread (not pthread directly)std::atomic<int>) to ensure all threads start simultaneously// test/tap/tests/unit/mysqlx_pool_unit-t.cpp
#include "tap.h"
#include "test_globals.h"
#include "test_init.h"
#include "mysqlx_connection.h"
#include "mysqlx_thread.h"
int main() {
plan(6);
test_globals_init();
// Test pool matching
Mysqlx_Thread thr;
thr.init(0);
MysqlxConnection* conn = new MysqlxConnection();
conn->set_hostgroup(1);
conn->set_user("appuser");
conn->set_schema("appdb");
conn->set_state(MysqlxConnection::IDLE);
conn->set_reusable(true);
thr.return_connection_to_cache(conn);
MysqlxConnection* found = thr.get_connection_from_cache(1, "appuser", "appdb");
ok(found != nullptr, "connection found by matching hostgroup/user/schema");
ok(found == conn, "got the same connection back");
found = thr.get_connection_from_cache(1, "otheruser", "appdb");
ok(found == nullptr, "no match for different user");
found = thr.get_connection_from_cache(1, "appuser", "otherdb");
ok(found == nullptr, "no match for different schema");
found = thr.get_connection_from_cache(2, "appuser", "appdb");
ok(found == nullptr, "no match for different hostgroup");
ok(thr.get_cached_connection_count() == 0, "cache empty after all retrieved");
return exit_status();
}
Then register in test/tap/tests/unit/Makefile and test/tap/groups/groups.json.