From 307d70acb99597b396b1f410697baccc899b7bee Mon Sep 17 00:00:00 2001 From: Rene Cannao Date: Sun, 5 Apr 2026 03:16:10 +0000 Subject: [PATCH] Add unit tests for GTID wire protocol parsing (I1/I2/I3/I4) Tests read_next_gtid() by buffer-stuffing, covering: - ST= bootstrap with single trxids and ranges - I1/I2 single trxid messages - I3/I4 range-based messages - I1 rejects range input (atoll parses only first number) - Unknown message type sets active=false and returns false - read_all_gtids() stops processing on unknown message - Empty buffer and incomplete message edge cases - Mixed message sequence with full GTID state verification --- test/tap/groups/groups.json | 3 +- test/tap/tests/unit/Makefile | 1 + .../tests/unit/gtid_server_data_unit-t.cpp | 306 ++++++++++++++++++ 3 files changed, 309 insertions(+), 1 deletion(-) create mode 100644 test/tap/tests/unit/gtid_server_data_unit-t.cpp diff --git a/test/tap/groups/groups.json b/test/tap/groups/groups.json index eefc3fe3f..8320bec49 100644 --- a/test/tap/groups/groups.json +++ b/test/tap/groups/groups.json @@ -371,5 +371,6 @@ "vector_db_performance-t" : [ "ai-g1" ], "vector_features-t" : [ "ai-g1" ], "gtid_trxid_interval_unit-t" : [ "unit-tests-g1" ], - "gtid_set_unit-t" : [ "unit-tests-g1" ] + "gtid_set_unit-t" : [ "unit-tests-g1" ], + "gtid_server_data_unit-t" : [ "unit-tests-g1" ] } diff --git a/test/tap/tests/unit/Makefile b/test/tap/tests/unit/Makefile index 7306a52b5..cda92c980 100644 --- a/test/tap/tests/unit/Makefile +++ b/test/tap/tests/unit/Makefile @@ -287,6 +287,7 @@ UNIT_TESTS := smoke_test-t query_cache_unit-t query_processor_unit-t \ genai_discovery_schema_unit-t \ gtid_trxid_interval_unit-t \ gtid_set_unit-t \ + gtid_server_data_unit-t \ genai_mysql_catalog_unit-t \ admin_disk_upgrade_unit-t \ glovars_unit-t diff --git a/test/tap/tests/unit/gtid_server_data_unit-t.cpp b/test/tap/tests/unit/gtid_server_data_unit-t.cpp new file mode 100644 index 000000000..7b4432670 --- /dev/null +++ b/test/tap/tests/unit/gtid_server_data_unit-t.cpp @@ -0,0 +1,306 @@ +/** + * @file gtid_server_data_unit-t.cpp + * @brief Unit tests for GTID_Server_Data wire protocol parsing. + * + * Tests read_next_gtid() by stuffing messages directly into the internal + * buffer, bypassing the network layer. Covers: + * - ST= bootstrap messages (single trxid and ranges) + * - I1/I2 single trxid incremental messages + * - I3/I4 range-based incremental messages + * - Unknown message type triggers disconnect (active = false) + * - events_read counter accuracy + */ + +#include "tap.h" + +#include "GTID_Server_Data.h" + +#include +#include + +static const char *UUID_A = "aaaaaaaa-0000-1111-2222-aaaaaaaaaaaa"; +static const char *UUID_A_STRIPPED = "aaaaaaaa000011112222aaaaaaaaaaaa"; +static const char *UUID_B = "bbbbbbbb-3333-4444-5555-bbbbbbbbbbbb"; +static const char *UUID_B_STRIPPED = "bbbbbbbb333344445555bbbbbbbbbbbb"; + +/** + * @brief Helper: stuff a message string into sd's buffer and reset pos. + * + * Replaces the entire buffer content. The caller can stuff multiple + * newline-delimited messages in a single call. + */ +static void stuff_buffer(GTID_Server_Data &sd, const std::string &msg) { + size_t needed = msg.size(); + if (needed > sd.size) { + sd.resize(needed); + } + memcpy(sd.data, msg.c_str(), needed); + sd.len = needed; + sd.pos = 0; +} + +/** + * @brief ST= bootstrap with single trxids. + */ +static void test_bootstrap_single() { + GTID_Server_Data sd(nullptr, (char *)"127.0.0.1", 0, 3306); + + std::string msg = std::string("ST=") + UUID_A + ":100," + UUID_B + ":200\n"; + stuff_buffer(sd, msg); + + ok(sd.read_next_gtid() == true, "ST= bootstrap: returns true"); + ok(sd.active == true, "ST= bootstrap: active remains true"); + ok(sd.events_read == 1, "ST= bootstrap: events_read incremented"); + ok(sd.gtid_exists((char *)UUID_A_STRIPPED, 100) == true, "ST= bootstrap: UUID_A trxid 100 exists"); + ok(sd.gtid_exists((char *)UUID_B_STRIPPED, 200) == true, "ST= bootstrap: UUID_B trxid 200 exists"); + ok(sd.gtid_exists((char *)UUID_A_STRIPPED, 101) == false, "ST= bootstrap: UUID_A trxid 101 does not exist"); +} + +/** + * @brief ST= bootstrap with trxid ranges. + */ +static void test_bootstrap_range() { + GTID_Server_Data sd(nullptr, (char *)"127.0.0.1", 0, 3306); + + std::string msg = std::string("ST=") + UUID_A + ":1-100," + UUID_B + ":50-200\n"; + stuff_buffer(sd, msg); + + ok(sd.read_next_gtid() == true, "ST= range: returns true"); + ok(sd.gtid_exists((char *)UUID_A_STRIPPED, 1) == true, "ST= range: UUID_A trxid 1 exists"); + ok(sd.gtid_exists((char *)UUID_A_STRIPPED, 50) == true, "ST= range: UUID_A trxid 50 exists"); + ok(sd.gtid_exists((char *)UUID_A_STRIPPED, 100) == true, "ST= range: UUID_A trxid 100 exists"); + ok(sd.gtid_exists((char *)UUID_A_STRIPPED, 101) == false, "ST= range: UUID_A trxid 101 does not exist"); + ok(sd.gtid_exists((char *)UUID_B_STRIPPED, 49) == false, "ST= range: UUID_B trxid 49 does not exist"); + ok(sd.gtid_exists((char *)UUID_B_STRIPPED, 50) == true, "ST= range: UUID_B trxid 50 exists"); + ok(sd.gtid_exists((char *)UUID_B_STRIPPED, 200) == true, "ST= range: UUID_B trxid 200 exists"); +} + +/** + * @brief I1= single trxid with UUID. + */ +static void test_i1_single_trxid() { + GTID_Server_Data sd(nullptr, (char *)"127.0.0.1", 0, 3306); + + std::string msg = std::string("I1=") + UUID_A_STRIPPED + ":42\n"; + stuff_buffer(sd, msg); + + ok(sd.read_next_gtid() == true, "I1: returns true"); + ok(sd.active == true, "I1: active remains true"); + ok(sd.events_read == 1, "I1: events_read incremented"); + ok(sd.gtid_exists((char *)UUID_A_STRIPPED, 42) == true, "I1: trxid 42 exists"); + ok(sd.gtid_exists((char *)UUID_A_STRIPPED, 43) == false, "I1: trxid 43 does not exist"); +} + +/** + * @brief I1= must parse only a single trxid, not a range. + */ +static void test_i1_ignores_range() { + GTID_Server_Data sd(nullptr, (char *)"127.0.0.1", 0, 3306); + + // If someone sends a range via I1, atoll() parses only the first number + std::string msg = std::string("I1=") + UUID_A_STRIPPED + ":10-20\n"; + stuff_buffer(sd, msg); + + ok(sd.read_next_gtid() == true, "I1 range: returns true"); + ok(sd.gtid_exists((char *)UUID_A_STRIPPED, 10) == true, "I1 range: trxid 10 exists (atoll parses first number)"); + ok(sd.gtid_exists((char *)UUID_A_STRIPPED, 15) == false, "I1 range: trxid 15 does not exist (range not parsed)"); + ok(sd.gtid_exists((char *)UUID_A_STRIPPED, 20) == false, "I1 range: trxid 20 does not exist (range not parsed)"); +} + +/** + * @brief I2= single trxid, reusing UUID from previous I1. + */ +static void test_i2_reuse_uuid() { + GTID_Server_Data sd(nullptr, (char *)"127.0.0.1", 0, 3306); + + // First set uuid_server via I1 + std::string msg1 = std::string("I1=") + UUID_A_STRIPPED + ":10\n"; + stuff_buffer(sd, msg1); + sd.read_next_gtid(); + + // Now I2 reuses uuid_server + std::string msg2 = "I2=20\n"; + stuff_buffer(sd, msg2); + + ok(sd.read_next_gtid() == true, "I2: returns true"); + ok(sd.active == true, "I2: active remains true"); + ok(sd.events_read == 2, "I2: events_read incremented to 2"); + ok(sd.gtid_exists((char *)UUID_A_STRIPPED, 20) == true, "I2: trxid 20 exists under UUID_A"); +} + +/** + * @brief I3= trxid range with UUID. + */ +static void test_i3_range() { + GTID_Server_Data sd(nullptr, (char *)"127.0.0.1", 0, 3306); + + std::string msg = std::string("I3=") + UUID_A_STRIPPED + ":100-200\n"; + stuff_buffer(sd, msg); + + ok(sd.read_next_gtid() == true, "I3: returns true"); + ok(sd.active == true, "I3: active remains true"); + ok(sd.events_read == 1, "I3: events_read incremented"); + ok(sd.gtid_exists((char *)UUID_A_STRIPPED, 99) == false, "I3: trxid 99 does not exist"); + ok(sd.gtid_exists((char *)UUID_A_STRIPPED, 100) == true, "I3: trxid 100 exists"); + ok(sd.gtid_exists((char *)UUID_A_STRIPPED, 150) == true, "I3: trxid 150 exists"); + ok(sd.gtid_exists((char *)UUID_A_STRIPPED, 200) == true, "I3: trxid 200 exists"); + ok(sd.gtid_exists((char *)UUID_A_STRIPPED, 201) == false, "I3: trxid 201 does not exist"); +} + +/** + * @brief I4= trxid range, reusing UUID from previous I3. + */ +static void test_i4_range_reuse_uuid() { + GTID_Server_Data sd(nullptr, (char *)"127.0.0.1", 0, 3306); + + // Set uuid_server via I3 + std::string msg1 = std::string("I3=") + UUID_B_STRIPPED + ":10-20\n"; + stuff_buffer(sd, msg1); + sd.read_next_gtid(); + + // I4 reuses uuid_server + std::string msg2 = "I4=30-40\n"; + stuff_buffer(sd, msg2); + + ok(sd.read_next_gtid() == true, "I4: returns true"); + ok(sd.active == true, "I4: active remains true"); + ok(sd.events_read == 2, "I4: events_read incremented to 2"); + ok(sd.gtid_exists((char *)UUID_B_STRIPPED, 30) == true, "I4: trxid 30 exists under UUID_B"); + ok(sd.gtid_exists((char *)UUID_B_STRIPPED, 35) == true, "I4: trxid 35 exists under UUID_B"); + ok(sd.gtid_exists((char *)UUID_B_STRIPPED, 40) == true, "I4: trxid 40 exists under UUID_B"); + ok(sd.gtid_exists((char *)UUID_B_STRIPPED, 41) == false, "I4: trxid 41 does not exist"); +} + +/** + * @brief Unknown message type sets active=false and returns false. + */ +static void test_unknown_message_disconnects() { + GTID_Server_Data sd(nullptr, (char *)"127.0.0.1", 0, 3306); + + // First, send a valid message to confirm baseline + std::string msg1 = std::string("I1=") + UUID_A_STRIPPED + ":10\n"; + stuff_buffer(sd, msg1); + sd.read_next_gtid(); + ok(sd.active == true, "unknown: baseline active is true"); + ok(sd.events_read == 1, "unknown: baseline events_read is 1"); + + // Now send an unknown message type I9 + std::string msg2 = "I9=garbage\n"; + stuff_buffer(sd, msg2); + + ok(sd.read_next_gtid() == false, "unknown: returns false"); + ok(sd.active == false, "unknown: active set to false (disconnect)"); + ok(sd.events_read == 1, "unknown: events_read NOT incremented"); +} + +/** + * @brief Multiple messages in sequence: ST bootstrap, then I1, I3, I2, I4. + */ +static void test_mixed_sequence() { + GTID_Server_Data sd(nullptr, (char *)"127.0.0.1", 0, 3306); + + // Bootstrap + std::string boot = std::string("ST=") + UUID_A + ":1-5\n"; + stuff_buffer(sd, boot); + sd.read_next_gtid(); + ok(sd.events_read == 1, "mixed: bootstrap events_read=1"); + + // I1: single trxid, sets UUID to A + std::string m1 = std::string("I1=") + UUID_A_STRIPPED + ":6\n"; + stuff_buffer(sd, m1); + sd.read_next_gtid(); + ok(sd.gtid_exists((char *)UUID_A_STRIPPED, 6) == true, "mixed: I1 trxid 6 exists"); + + // I2: single trxid, reuses UUID A + std::string m2 = "I2=7\n"; + stuff_buffer(sd, m2); + sd.read_next_gtid(); + ok(sd.gtid_exists((char *)UUID_A_STRIPPED, 7) == true, "mixed: I2 trxid 7 exists"); + + // I3: range, sets UUID to B + std::string m3 = std::string("I3=") + UUID_B_STRIPPED + ":100-110\n"; + stuff_buffer(sd, m3); + sd.read_next_gtid(); + ok(sd.gtid_exists((char *)UUID_B_STRIPPED, 105) == true, "mixed: I3 trxid 105 exists"); + + // I4: range, reuses UUID B + std::string m4 = "I4=111-120\n"; + stuff_buffer(sd, m4); + sd.read_next_gtid(); + ok(sd.gtid_exists((char *)UUID_B_STRIPPED, 115) == true, "mixed: I4 trxid 115 exists"); + + ok(sd.events_read == 5, "mixed: events_read=5 after all messages"); + ok(sd.active == true, "mixed: still active after valid sequence"); + + // Verify the full GTID state + ok(sd.gtid_exists((char *)UUID_A_STRIPPED, 1) == true, "mixed: UUID_A range start from bootstrap"); + ok(sd.gtid_exists((char *)UUID_A_STRIPPED, 5) == true, "mixed: UUID_A range end from bootstrap"); + ok(sd.gtid_exists((char *)UUID_B_STRIPPED, 100) == true, "mixed: UUID_B range start from I3"); + ok(sd.gtid_exists((char *)UUID_B_STRIPPED, 120) == true, "mixed: UUID_B range end from I4"); + ok(sd.gtid_exists((char *)UUID_B_STRIPPED, 99) == false, "mixed: UUID_B before range"); + ok(sd.gtid_exists((char *)UUID_B_STRIPPED, 121) == false, "mixed: UUID_B after range"); +} + +/** + * @brief read_all_gtids() stops on unknown message; earlier messages are processed. + */ +static void test_read_all_stops_on_unknown() { + GTID_Server_Data sd(nullptr, (char *)"127.0.0.1", 0, 3306); + + // Three messages: two valid, one unknown + std::string msgs = std::string("I1=") + UUID_A_STRIPPED + ":10\n" + + "I2=11\n" + + "I9=bad\n"; + stuff_buffer(sd, msgs); + + sd.read_all_gtids(); + + ok(sd.gtid_exists((char *)UUID_A_STRIPPED, 10) == true, "read_all: first message processed"); + ok(sd.gtid_exists((char *)UUID_A_STRIPPED, 11) == true, "read_all: second message processed"); + ok(sd.active == false, "read_all: active=false after unknown message"); + ok(sd.events_read == 2, "read_all: events_read=2 (unknown not counted)"); +} + +/** + * @brief Empty buffer returns false, active stays true. + */ +static void test_empty_buffer() { + GTID_Server_Data sd(nullptr, (char *)"127.0.0.1", 0, 3306); + + ok(sd.read_next_gtid() == false, "empty: returns false"); + ok(sd.active == true, "empty: active remains true"); + ok(sd.events_read == 0, "empty: events_read stays 0"); +} + +/** + * @brief Incomplete message (no newline) returns false, active stays true. + */ +static void test_incomplete_message() { + GTID_Server_Data sd(nullptr, (char *)"127.0.0.1", 0, 3306); + + std::string msg = std::string("I1=") + UUID_A_STRIPPED + ":42"; // no newline + stuff_buffer(sd, msg); + + ok(sd.read_next_gtid() == false, "incomplete: returns false (no newline)"); + ok(sd.active == true, "incomplete: active remains true"); + ok(sd.events_read == 0, "incomplete: events_read stays 0"); +} + +int main() { + plan(70); + + test_bootstrap_single(); // 6 assertions + test_bootstrap_range(); // 8 assertions + test_i1_single_trxid(); // 5 assertions + test_i1_ignores_range(); // 4 assertions + test_i2_reuse_uuid(); // 4 assertions + test_i3_range(); // 8 assertions + test_i4_range_reuse_uuid(); // 7 assertions + test_unknown_message_disconnects(); // 5 assertions + test_mixed_sequence(); // 14 assertions + test_read_all_stops_on_unknown(); // 4 assertions + test_empty_buffer(); // 3 assertions + test_incomplete_message(); // 3 assertions + + return exit_status(); +}