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
feature/gtid-range-update
Rene Cannao 1 month ago
parent dca01d4eaf
commit 307d70acb9

@ -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" ]
}

@ -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

@ -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 <cstring>
#include <string>
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();
}
Loading…
Cancel
Save