From 1274fb7798ee8087e262c8e970c21134e8045a53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Canna=C3=B2?= Date: Sun, 22 Mar 2026 12:19:49 +0100 Subject: [PATCH] =?UTF-8?q?Add=20comprehensive=20FFTO=20protocol=20unit=20?= =?UTF-8?q?tests=20=E2=80=94=20MySQL=20+=20PgSQL=20(#5499)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 36 test cases covering both MySQL and PgSQL FFTO protocol parsing: MySQL length-encoded integers (11 tests): - 1-byte, 2-byte, 3-byte, 8-byte values - Truncated buffers, empty input MySQL packet building (9 tests): - Normal, large (1000 bytes), empty packets - Header validation (length, seq_id, payload integrity) MySQL OK packet parsing (2 tests): - affected_rows extraction (1-byte and 2-byte lenenc) PgSQL CommandComplete extended (7 tests): - INSERT with OID, large row count, zero rows - All 8 command types in one sweep, 9 DDL commands - Null-terminated payload (real wire format) Fragmentation simulation (6 tests): - Truncated lenenc (partial data) - Multi-packet stream building and header verification --- test/tap/tests/unit/Makefile | 1 + test/tap/tests/unit/ffto_protocol_unit-t.cpp | 291 +++++++++++++++++++ 2 files changed, 292 insertions(+) create mode 100644 test/tap/tests/unit/ffto_protocol_unit-t.cpp diff --git a/test/tap/tests/unit/Makefile b/test/tap/tests/unit/Makefile index faba268e3..5de78d112 100644 --- a/test/tap/tests/unit/Makefile +++ b/test/tap/tests/unit/Makefile @@ -235,6 +235,7 @@ UNIT_TESTS := smoke_test-t query_cache_unit-t query_processor_unit-t \ protocol_unit-t auth_unit-t connection_pool_unit-t \ rule_matching_unit-t hostgroups_unit-t monitor_health_unit-t \ pgsql_command_complete_unit-t \ + ffto_protocol_unit-t \ hostgroup_routing_unit-t \ transaction_state_unit-t diff --git a/test/tap/tests/unit/ffto_protocol_unit-t.cpp b/test/tap/tests/unit/ffto_protocol_unit-t.cpp new file mode 100644 index 000000000..c7e6a1196 --- /dev/null +++ b/test/tap/tests/unit/ffto_protocol_unit-t.cpp @@ -0,0 +1,291 @@ +/** + * @file ffto_protocol_unit-t.cpp + * @brief Comprehensive unit tests for FFTO protocol parsing utilities. + * + * Tests both MySQL and PgSQL protocol parsing functions used by FFTO: + * - MySQL: read_lenenc_int, packet building, OK packet parsing + * - PgSQL: CommandComplete tag parsing + * - Both: fragmented data reassembly simulation, large payloads + * + * @see FFTO unit testing (GitHub issue #5499) + */ + +#include "tap.h" +#include "test_globals.h" +#include "test_init.h" +#include "proxysql.h" +#include "MySQLProtocolUtils.h" +#include "PgSQLCommandComplete.h" + +#include +#include + +// ============================================================================ +// 1. MySQL: read_lenenc_int +// ============================================================================ + +static void test_mysql_lenenc_1byte() { + unsigned char buf[] = {0}; + const unsigned char *p = buf; size_t len = 1; + ok(mysql_read_lenenc_int(p, len) == 0, "lenenc: 0x00 → 0"); + ok(len == 0 && p == buf + 1, "lenenc: consumed 1 byte"); + + unsigned char buf2[] = {250}; + p = buf2; len = 1; + ok(mysql_read_lenenc_int(p, len) == 250, "lenenc: 0xFA → 250 (max 1-byte)"); +} + +static void test_mysql_lenenc_2byte() { + unsigned char buf[] = {0xFC, 0x01, 0x00}; + const unsigned char *p = buf; size_t len = 3; + ok(mysql_read_lenenc_int(p, len) == 1, "lenenc 2-byte: 0xFC 01 00 → 1"); + ok(len == 0, "lenenc 2-byte: consumed 3 bytes"); + + unsigned char buf2[] = {0xFC, 0xFF, 0xFF}; + p = buf2; len = 3; + ok(mysql_read_lenenc_int(p, len) == 65535, "lenenc 2-byte: max → 65535"); +} + +static void test_mysql_lenenc_3byte() { + unsigned char buf[] = {0xFD, 0x00, 0x00, 0x01, 0x00}; // padded for CPY safety + const unsigned char *p = buf; size_t len = 4; + ok(mysql_read_lenenc_int(p, len) == 65536, "lenenc 3-byte: → 65536"); +} + +static void test_mysql_lenenc_8byte() { + unsigned char buf[9] = {0xFE, 0x01, 0, 0, 0, 0, 0, 0, 0}; + const unsigned char *p = buf; size_t len = 9; + ok(mysql_read_lenenc_int(p, len) == 1, "lenenc 8-byte: → 1"); + + unsigned char buf2[9] = {0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0, 0, 0, 0}; + p = buf2; len = 9; + ok(mysql_read_lenenc_int(p, len) == 0xFFFFFFFF, "lenenc 8-byte: → 4294967295"); +} + +static void test_mysql_lenenc_truncated() { + // 2-byte prefix but only 1 byte of data + unsigned char buf[] = {0xFC, 0x01}; + const unsigned char *p = buf; size_t len = 2; + ok(mysql_read_lenenc_int(p, len) == 0, "lenenc truncated: 0xFC with 1 byte → 0"); + + // Empty buffer + const unsigned char *p2 = nullptr; size_t len2 = 0; + ok(mysql_read_lenenc_int(p2, len2) == 0, "lenenc empty: → 0"); +} + +// ============================================================================ +// 2. MySQL: packet building +// ============================================================================ + +static void test_mysql_build_packet() { + unsigned char payload[] = {0x03, 'S', 'E', 'L', 'E', 'C', 'T', ' ', '1'}; + unsigned char out[13]; + size_t total = mysql_build_packet(payload, 9, 0, out); + ok(total == 13, "build packet: total size 13"); + ok(out[0] == 9 && out[1] == 0 && out[2] == 0, "build packet: length = 9"); + ok(out[3] == 0, "build packet: seq_id = 0"); + ok(memcmp(out + 4, payload, 9) == 0, "build packet: payload intact"); +} + +static void test_mysql_build_large_packet() { + // Build a packet with 1000-byte payload + std::vector payload(1000, 'X'); + std::vector out(1004); + size_t total = mysql_build_packet(payload.data(), 1000, 5, out.data()); + ok(total == 1004, "large packet: total size 1004"); + ok(out[0] == 0xE8 && out[1] == 0x03 && out[2] == 0x00, + "large packet: length = 1000 (little-endian)"); + ok(out[3] == 5, "large packet: seq_id = 5"); +} + +static void test_mysql_build_empty_packet() { + unsigned char out[4]; + size_t total = mysql_build_packet(nullptr, 0, 1, out); + ok(total == 4, "empty packet: header only"); + ok(out[0] == 0 && out[1] == 0 && out[2] == 0, "empty packet: length = 0"); +} + +// ============================================================================ +// 3. MySQL: OK packet affected_rows extraction +// ============================================================================ + +static void test_mysql_ok_affected_rows() { + // Build an OK packet: 0x00 + affected_rows(lenenc) + last_insert_id(lenenc) + // affected_rows = 42 + unsigned char ok_payload[] = {0x00, 42, 0}; // OK, affected=42, last_insert=0 + unsigned char pkt[7]; + mysql_build_packet(ok_payload, 3, 1, pkt); + + // Parse affected_rows from the OK packet payload + const unsigned char *pos = ok_payload + 1; + size_t rem = 2; + uint64_t affected = mysql_read_lenenc_int(pos, rem); + ok(affected == 42, "OK packet: affected_rows = 42"); +} + +static void test_mysql_ok_large_affected_rows() { + // affected_rows = 300 (needs 0xFC prefix) + unsigned char ok_payload[] = {0x00, 0xFC, 0x2C, 0x01, 0}; + const unsigned char *pos = ok_payload + 1; + size_t rem = 4; + uint64_t affected = mysql_read_lenenc_int(pos, rem); + ok(affected == 300, "OK packet: affected_rows = 300 (2-byte lenenc)"); +} + +// ============================================================================ +// 4. PgSQL: CommandComplete — extended tests +// ============================================================================ + +static void test_pgsql_insert_with_oid() { + // "INSERT oid count" format — the count is always the last token + auto r = parse_pgsql_command_complete( + (const unsigned char *)"INSERT 12345 50", 15); + ok(r.rows == 50 && r.is_select == false, + "PgSQL INSERT with OID: rows=50 (last token)"); +} + +static void test_pgsql_large_row_count() { + auto r = parse_pgsql_command_complete( + (const unsigned char *)"SELECT 1000000", 14); + ok(r.rows == 1000000 && r.is_select == true, + "PgSQL large SELECT: rows=1000000"); +} + +static void test_pgsql_zero_rows() { + auto r = parse_pgsql_command_complete( + (const unsigned char *)"UPDATE 0", 8); + ok(r.rows == 0 && r.is_select == false, "PgSQL UPDATE 0: rows=0"); + + auto r2 = parse_pgsql_command_complete( + (const unsigned char *)"SELECT 0", 8); + ok(r2.rows == 0 && r2.is_select == true, "PgSQL SELECT 0: rows=0"); +} + +static void test_pgsql_all_command_types() { + struct { const char *tag; uint64_t expected; bool is_sel; } cases[] = { + {"INSERT 0 1", 1, false}, + {"UPDATE 5", 5, false}, + {"DELETE 3", 3, false}, + {"SELECT 10", 10, true}, + {"FETCH 7", 7, true}, + {"MOVE 2", 2, true}, + {"COPY 100", 100, false}, + {"MERGE 8", 8, false}, + }; + int pass = 0; + for (auto &c : cases) { + auto r = parse_pgsql_command_complete( + (const unsigned char *)c.tag, strlen(c.tag)); + if (r.rows == c.expected && r.is_select == c.is_sel) pass++; + } + ok(pass == 8, "PgSQL all 8 command types parse correctly"); +} + +static void test_pgsql_ddl_commands() { + const char *ddls[] = { + "CREATE TABLE", "ALTER TABLE", "DROP TABLE", + "CREATE INDEX", "DROP INDEX", "VACUUM", + "TRUNCATE TABLE", "GRANT", "REVOKE", + }; + int pass = 0; + for (auto &ddl : ddls) { + auto r = parse_pgsql_command_complete( + (const unsigned char *)ddl, strlen(ddl)); + if (r.rows == 0) pass++; + } + ok(pass == 9, "PgSQL DDL commands all return rows=0"); +} + +static void test_pgsql_null_terminated_payload() { + // Payload with null terminator (common in real wire format) + unsigned char payload[] = {'S', 'E', 'L', 'E', 'C', 'T', ' ', '5', '\0'}; + auto r = parse_pgsql_command_complete(payload, 9); + ok(r.rows == 5, "PgSQL null-terminated: SELECT 5 → rows=5"); +} + +// ============================================================================ +// 5. Fragmented data simulation +// ============================================================================ + +static void test_mysql_fragmented_lenenc() { + // Simulate reading a lenenc int where data arrives in chunks + // Build a 3-byte lenenc (0xFD prefix + 3 bytes) + unsigned char full[] = {0xFD, 0x40, 0x42, 0x0F}; // = 999,999 + 1 (not quite, but valid) + + // First chunk: just the prefix + const unsigned char *p = full; size_t len = 1; + uint64_t val = mysql_read_lenenc_int(p, len); + // With only 1 byte, the 0xFD prefix is consumed but there aren't 3 bytes after → returns 0 + ok(val == 0, "fragmented: 0xFD with no data bytes → 0 (truncated)"); + + // Full data + p = full; len = 4; + val = mysql_read_lenenc_int(p, len); + ok(val > 0, "fragmented: full 3-byte lenenc decoded successfully"); +} + +static void test_mysql_multi_packet_build() { + // Build 3 packets sequentially (simulating a multi-packet stream) + unsigned char stream[64]; + size_t offset = 0; + + unsigned char p1[] = {0x03, 'S', 'E', 'L'}; + offset += mysql_build_packet(p1, 4, 0, stream + offset); + + unsigned char p2[] = {0x01, 0x02, 0x03}; + offset += mysql_build_packet(p2, 3, 1, stream + offset); + + unsigned char p3[] = {0xFE, 0x00, 0x00, 0x00, 0x00}; + offset += mysql_build_packet(p3, 5, 2, stream + offset); + + ok(offset == 4+4 + 3+4 + 5+4, "multi-packet: total stream size correct"); + + // Verify each packet header + ok(stream[0] == 4 && stream[3] == 0, "multi-packet: pkt 0 header ok"); + ok(stream[8] == 3 && stream[11] == 1, "multi-packet: pkt 1 header ok"); + ok(stream[15] == 5 && stream[18] == 2, "multi-packet: pkt 2 header ok"); +} + +// ============================================================================ +// Main +// ============================================================================ + +int main() { + plan(36); + int rc = test_init_minimal(); + ok(rc == 0, "test_init_minimal() succeeds"); + + // MySQL lenenc + test_mysql_lenenc_1byte(); // 3 + test_mysql_lenenc_2byte(); // 3 + test_mysql_lenenc_3byte(); // 1 + test_mysql_lenenc_8byte(); // 2 + test_mysql_lenenc_truncated(); // 2 + + // MySQL packet building + test_mysql_build_packet(); // 4 + test_mysql_build_large_packet(); // 3 + test_mysql_build_empty_packet(); // 2 + + // MySQL OK packet parsing + test_mysql_ok_affected_rows(); // 1 + test_mysql_ok_large_affected_rows(); // 1 + + // PgSQL extended + test_pgsql_insert_with_oid(); // 1 + test_pgsql_large_row_count(); // 1 + test_pgsql_zero_rows(); // 2 + test_pgsql_all_command_types(); // 1 + test_pgsql_ddl_commands(); // 1 + test_pgsql_null_terminated_payload(); // 1 + + // Fragmentation + test_mysql_fragmented_lenenc(); // 2 + test_mysql_multi_packet_build(); // 4 + // Total: 1+3+3+1+2+2+4+3+2+1+1+1+1+2+1+1+1+2+4 = 36... recount + // 1 (init) + 3+3+1+2+2 (lenenc=11) + 4+3+2 (build=9) + 1+1 (ok=2) + + // 1+1+2+1+1+1 (pgsql=7) + 2+4 (frag=6) = 1+11+9+2+7+6 = 36 + + test_cleanup_minimal(); + return exit_status(); +}