You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
proxysql/test/tap/tests/pgsql-options_startup_param...

473 lines
18 KiB

/**
* @file pgsql-options_startup_params-t.cpp
* @brief Comprehensive test for startup parameter handling in options
*
* Tests:
* - All ProxySQL tracked variables with spaces (success/failure)
* - Untracked parameters
* - Edge cases (escaped spaces, multiple spaces, trailing spaces)
* - Strict verification of error handling
*/
#include <unistd.h>
#include <string>
#include <sstream>
#include <vector>
#include <utility>
#include "libpq-fe.h"
#include "command_line.h"
#include "tap.h"
#include "utils.h"
CommandLine cl;
using PGConnPtr = std::unique_ptr<PGconn, decltype(&PQfinish)>;
struct TestCase {
std::string name;
std::string options;
bool should_succeed; // true = expect connection success
std::string expected_error; // substring expected in error message (if should_fail)
std::string description;
};
// Helper to create connection
PGConnPtr createConnection(const std::string& options, std::string& error_msg) {
std::stringstream ss;
ss << "host=" << cl.pgsql_host << " port=" << cl.pgsql_port;
ss << " user=" << cl.pgsql_username << " password=" << cl.pgsql_password;
ss << " sslmode=disable";
if (!options.empty()) {
ss << " options='" << options << "'";
}
PGconn* conn = PQconnectdb(ss.str().c_str());
if (PQstatus(conn) != CONNECTION_OK) {
error_msg = PQerrorMessage(conn);
PQfinish(conn);
return PGConnPtr(nullptr, &PQfinish);
}
error_msg.clear();
return PGConnPtr(conn, &PQfinish);
}
// Helper to get variable value
std::string getVar(PGconn* conn, const char* name) {
PGresult* res = PQexec(conn, (std::string("SHOW ") + name).c_str());
if (!res || PQresultStatus(res) != PGRES_TUPLES_OK) {
PQclear(res);
return "";
}
char* val = PQgetvalue(res, 0, 0);
std::string result = val ? val : "";
PQclear(res);
return result;
}
int main(int argc, char** argv) {
if (cl.getEnv()) return exit_status();
// Build comprehensive test cases
std::vector<TestCase> tests;
// 0. client_encoding (has alias "names")
tests.push_back({"client_encoding",
"-c client_encoding=LATIN1",
true, "", "client_encoding"});
tests.push_back({"names_alias",
"-c names=UTF8",
true, "", "names alias for client_encoding"});
// 1. DateStyle with escaped space
// For escaped space: C++ "\\\\" becomes actual "\\" which libpq sees as "\ " (escaped space)
tests.push_back({"datestyle_escaped",
"-c DateStyle=SQL,\\\\ DMY",
true, "", "DateStyle with escaped space"});
tests.push_back({"datestyle_postgres",
"-c DateStyle=Postgres,\\\\ DMY",
true, "", "DateStyle Postgres format"});
tests.push_back({"datestyle_iso_mdy",
"-c DateStyle=ISO,\\\\ MDY",
true, "", "DateStyle ISO MDY"});
// 2. IntervalStyle
tests.push_back({"intervalstyle_iso8601",
"-c IntervalStyle=iso_8601",
true, "", "IntervalStyle iso_8601"});
tests.push_back({"intervalstyle_postgres",
"-c IntervalStyle=postgres",
true, "", "IntervalStyle postgres"});
// 3. standard_conforming_strings
tests.push_back({"standard_conforming_strings_off",
"-c standard_conforming_strings=off",
true, "", "standard_conforming_strings=off"});
tests.push_back({"standard_conforming_strings_on",
"-c standard_conforming_strings=on",
true, "", "standard_conforming_strings=on"});
// 4. TimeZone (has alias "TIME ZONE")
tests.push_back({"timezone_pst",
"-c TimeZone=PST8PDT",
true, "", "TimeZone PST8PDT"});
tests.push_back({"timezone_est",
"-c TimeZone=EST5EDT",
true, "", "TimeZone EST5EDT"});
tests.push_back({"timezone_utc",
"-c TimeZone=UTC",
true, "", "TimeZone UTC"});
tests.push_back({"time_zone_alias",
"-c TimeZone=GMT",
true, "", "TimeZone alias"});
// Non-critical params (PGSQL_NAME_LAST_LOW_WM+1 to PGSQL_NAME_LAST_HIGH_WM-1)
// 6. allow_in_place_tablespaces
tests.push_back({"allow_in_place_tablespaces",
"-c allow_in_place_tablespaces=on",
true, "", "allow_in_place_tablespaces"});
// 7. bytea_output
tests.push_back({"bytea_output_escape",
"-c bytea_output=escape",
true, "", "bytea_output=escape"});
tests.push_back({"bytea_output_hex",
"-c bytea_output=hex",
true, "", "bytea_output=hex"});
// 8. client_min_messages
tests.push_back({"client_min_messages",
"-c client_min_messages=warning",
true, "", "client_min_messages"});
// 9-15. enable_* parameters
tests.push_back({"enable_bitmapscan",
"-c enable_bitmapscan=off",
true, "", "enable_bitmapscan"});
tests.push_back({"enable_hashjoin",
"-c enable_hashjoin=off",
true, "", "enable_hashjoin"});
tests.push_back({"enable_indexscan",
"-c enable_indexscan=off",
true, "", "enable_indexscan"});
tests.push_back({"enable_nestloop",
"-c enable_nestloop=off",
true, "", "enable_nestloop"});
tests.push_back({"enable_seqscan",
"-c enable_seqscan=off",
true, "", "enable_seqscan"});
tests.push_back({"enable_sort",
"-c enable_sort=off",
true, "", "enable_sort"});
// 16. escape_string_warning
tests.push_back({"escape_string_warning",
"-c escape_string_warning=off",
true, "", "escape_string_warning"});
// 17. extra_float_digits
tests.push_back({"extra_float_digits_2",
"-c extra_float_digits=2",
true, "", "extra_float_digits"});
// 18. maintenance_work_mem (with space in value, must escape)
tests.push_back({"maintenance_work_mem",
"-c maintenance_work_mem=64MB",
true, "", "maintenance_work_mem"});
// 19. search_path (has NO_STRIP_VALUE flag, handles commas specially)
tests.push_back({"searchpath_simple",
"-c search_path=public",
true, "", "search_path simple"});
tests.push_back({"searchpath_escaped_space",
"-c search_path=public,\\\\ private",
true, "", "search_path with escaped space"});
tests.push_back({"searchpath_quoted",
"-c search_path=\"\\$user\",public",
true, "", "search_path with quoted user"});
// 20. synchronous_commit
tests.push_back({"synchronous_commit",
"-c synchronous_commit=off",
true, "", "synchronous_commit"});
// Multiple parameters with proper escaping
tests.push_back({"multiple_critical",
"-c DateStyle=SQL,\\\\ DMY -c TimeZone=PST8PDT -c client_encoding=UTF8",
true, "", "Multiple critical params with escaped space"});
tests.push_back({"multiple_all",
"-c DateStyle=ISO,\\\\ MDY -c TimeZone=UTC -c client_encoding=UTF8 -c enable_seqscan=off -c bytea_output=escape",
true, "", "Multiple params: critical + non-critical"});
// ============================================================
// FAILURE CASES: Unescaped spaces (should fail)
// ============================================================
// DateStyle with unescaped space (the main bug case)
tests.push_back({"datestyle_unescaped",
"-c DateStyle=SQL, DMY",
false, "unescaped space", "DateStyle with unescaped space (should fail)"});
// TimeZone with unescaped space
tests.push_back({"timezone_unescaped",
"-c TimeZone=New York",
false, "unescaped space", "TimeZone with unescaped space"});
// search_path with unescaped space
tests.push_back({"searchpath_unescaped",
"-c search_path=public, private",
false, "unescaped space", "search_path with unescaped space"});
// maintenance_work_mem with unescaped space
tests.push_back({"maintenance_work_mem_unescaped",
"-c maintenance_work_mem=128 MB",
false, "unescaped space", "maintenance_work_mem with unescaped space"});
// Trailing space after value
tests.push_back({"trailing_space",
"-c DateStyle=ISO, MDY ",
false, "unescaped space", "Trailing space after value"});
// Multiple spaces between options (one unescaped in value)
tests.push_back({"multi_space_fail",
"-c DateStyle=Postgres, DMY -c TimeZone=PST",
false, "unescaped space", "Multiple spaces with unescaped space in first value"});
// bytea_output with unescaped space
tests.push_back({"bytea_unescaped",
"-c bytea_output=escape mode",
false, "unescaped space", "bytea_output with unescaped space"});
// client_min_messages with unescaped space
tests.push_back({"client_min_messages_unescaped",
"-c client_min_messages=log debug",
false, "unescaped space", "client_min_messages with unescaped space"});
// ============================================================
// UNTRACKED PARAMETERS: Should work but lock hostgroup
// ============================================================
// Valid untracked parameter
tests.push_back({"untracked_valid",
"-c geqo=off",
true, "", "Untracked parameter (geqo)"});
tests.push_back({"untracked_join_collapse_limit",
"-c join_collapse_limit=8",
true, "", "Untracked parameter (join_collapse_limit)"});
// ============================================================
// EDGE CASES: Various escape scenarios
// ============================================================
// Multiple spaces needing escape
tests.push_back({"multi_space_escape",
"-c search_path=a,\\\\ b,\\\\ c",
true, "", "Multiple escaped spaces"});
// Space at start of value (escaped)
tests.push_back({"space_at_start",
"-c search_path=\\\\ public",
true, "", "Escaped space at start of value"});
// ============================================================
// SPECIAL CASES: Empty and malformed
// ============================================================
// Empty options
tests.push_back({"empty_options",
"",
true, "", "Empty options (no options)"});
// Just spaces in options
tests.push_back({"only_spaces",
" ",
true, "", "Options with only spaces"});
// No equals sign (malformed - should FAIL now)
tests.push_back({"no_equals",
"-c DateStyle",
false, "missing '='", "Malformed option without ="});
// Empty value after equals (valid - empty value)
tests.push_back({"empty_value",
"-c DateStyle=",
true, "", "Empty value after equals"});
// ============================================================
// MALFORMED OPTIONS: Should be REJECTED (not skipped)
// ============================================================
// Token doesn't start with -c or --
tests.push_back({"malformed_no_prefix",
"foo=value",
false, "token must start with", "Malformed: no -c or -- prefix"});
// Space between key and =
tests.push_back({"malformed_space_before_equals",
"-c DateStyle =value",
false, "missing '='", "Malformed: space before ="});
// Empty key
tests.push_back({"malformed_empty_key",
"-c =value",
false, "empty key", "Malformed: empty key before ="});
// Multiple tokens without proper -c
tests.push_back({"malformed_multiple_tokens",
"-c foo -c bar=value",
false, "missing '='", "Malformed: space in key"});
// ============================================================
// PROXY VALIDATION: Verify values are actually applied
// ============================================================
// Verify DateStyle is actually set
// This will be verified separately after connection
const int num_tests = tests.size() + 4; // +4 for verification tests
plan(num_tests);
int pass_count = 0;
int fail_count = 0;
// Run all test cases
for (const auto& tc : tests) {
diag("\n=== Test: %s ===", tc.name.c_str());
diag("Options: '%s'", tc.options.c_str());
diag("Description: %s", tc.description.c_str());
std::string error_msg;
PGConnPtr conn = createConnection(tc.options, error_msg);
bool connected = (conn != nullptr);
bool test_passed;
if (tc.should_succeed) {
test_passed = connected;
if (!test_passed) {
diag("FAILED: Expected success but got error: %s", error_msg.c_str());
}
} else {
test_passed = !connected;
if (!test_passed) {
diag("FAILED: Expected failure but connection succeeded");
} else if (!tc.expected_error.empty()) {
// Verify error message contains expected text
if (error_msg.find(tc.expected_error) == std::string::npos) {
diag("WARNING: Error message doesn't contain '%s'", tc.expected_error.c_str());
diag("Actual error: %s", error_msg.c_str());
}
}
}
ok(test_passed, "%s", tc.description.c_str());
if (test_passed) pass_count++;
else fail_count++;
}
// Additional verification: Check that values are actually applied
diag("\n=== Verification: Check values are actually applied ===");
// Test 1: Verify escaped DateStyle is applied correctly
{
diag("\n--- Verifying DateStyle with escaped space is correct ---");
std::string error;
PGConnPtr conn = createConnection("-c DateStyle=SQL,\\\\ DMY", error);
if (conn) {
std::string val = getVar(conn.get(), "DateStyle");
bool correct = (val.find("SQL") != std::string::npos && val.find("DMY") != std::string::npos);
diag("Expected: SQL, DMY");
diag("Got: %s", val.c_str());
ok(correct, "DateStyle escaped space: value correctly applied");
} else {
diag("Connection failed: %s", error.c_str());
ok(false, "DateStyle escaped space: value correctly applied");
}
}
// Test 2: Verify partial value auto-completion works
{
diag("\n--- Verifying partial DateStyle (no escape needed) ---");
std::string error;
PGConnPtr conn = createConnection("-c DateStyle=MDY", error);
if (conn) {
std::string val = getVar(conn.get(), "DateStyle");
// Should be completed to "ISO, MDY" (format from default, order from param)
bool has_order = (val.find("MDY") != std::string::npos);
diag("Got: %s (should contain MDY)", val.c_str());
ok(has_order, "DateStyle partial value auto-completed");
} else {
diag("Connection failed: %s", error.c_str());
ok(false, "DateStyle partial value auto-completed");
}
}
// Test 3: Verify RESET reverts to startup value
{
diag("\n--- Verifying RESET reverts to startup value ---");
std::string error;
PGConnPtr conn = createConnection("-c DateStyle=SQL,\\\\ DMY", error);
if (conn) {
std::string startup_val = getVar(conn.get(), "DateStyle");
diag("Startup value: %s", startup_val.c_str());
// SET to different value
PGresult* set_res = PQexec(conn.get(), "SET DateStyle = 'Postgres, MDY'");
const bool set_ok = set_res && PQresultStatus(set_res) == PGRES_COMMAND_OK;
if (!set_ok) {
diag("SET failed: %s", set_res ? PQresultErrorMessage(set_res) : "null result");
}
PQclear(set_res);
std::string set_val = getVar(conn.get(), "DateStyle");
diag("After SET: %s", set_val.c_str());
// Verify SET actually changed the value
const bool set_changed = (set_val != startup_val);
if (!set_changed) {
diag("SET did not change value: startup='%s', after SET='%s'", startup_val.c_str(), set_val.c_str());
}
// RESET
PGresult* reset_res = PQexec(conn.get(), "RESET DateStyle");
const bool reset_ok = reset_res && PQresultStatus(reset_res) == PGRES_COMMAND_OK;
if (!reset_ok) {
diag("RESET failed: %s", reset_res ? PQresultErrorMessage(reset_res) : "null result");
}
PQclear(reset_res);
std::string reset_val = getVar(conn.get(), "DateStyle");
diag("After RESET: %s", reset_val.c_str());
// Verify all conditions: SET worked, RESET worked, and value reverted
bool reverted = set_ok && set_changed && reset_ok && (reset_val == startup_val);
ok(reverted, "RESET reverts to startup value");
} else {
diag("Connection failed: %s", error.c_str());
ok(false, "RESET reverts to startup value");
}
}
// Test 4: Verify search_path with escaped space
{
diag("\n--- Verifying search_path with escaped space ---");
std::string error;
PGConnPtr conn = createConnection("-c search_path=public,\\\\ private", error);
if (conn) {
std::string val = getVar(conn.get(), "search_path");
// search_path should contain both schemas
bool has_public = (val.find("public") != std::string::npos);
bool has_private = (val.find("private") != std::string::npos);
diag("Expected: public, private");
diag("Got: %s", val.c_str());
ok(has_public && has_private, "search_path with escaped space applied");
} else {
diag("Connection failed: %s", error.c_str());
ok(false, "search_path with escaped space applied");
}
}
// Summary
diag("\n=== SUMMARY ===");
diag("Passed: %d, Failed: %d", pass_count, fail_count);
return exit_status();
}