diff --git a/test/tap/tests/Makefile b/test/tap/tests/Makefile index 4541bd1e5..6aaeca794 100644 --- a/test/tap/tests/Makefile +++ b/test/tap/tests/Makefile @@ -101,7 +101,8 @@ OPT=-O2 $(WGCOV) -Wl,--no-as-needed debug: OPT=-O0 -DDEBUG -ggdb -Wl,--no-as-needed $(WGCOV) $(WASAN) debug: tests -tests: $(patsubst %.cpp,%,$(wildcard *-t.cpp)) setparser_test reg_test_3504-change_user_libmariadb_helper reg_test_3504-change_user_libmysql_helper set_testing-240.csv test_clickhouse_server_libmysql clickhouse_php_conn-t +tests: $(patsubst %.cpp,%,$(wildcard *-t.cpp)) setparser_test reg_test_3504-change_user_libmariadb_helper reg_test_3504-change_user_libmysql_helper set_testing-240.csv test_clickhouse_server_libmysql clickhouse_php_conn-t \ + reg_test_stmt_resultset_err_no_rows_libmysql prepare_statement_err3024_libmysql prepare_statement_err3024_async reg_test_mariadb_stmt_store_result_libmysql reg_test_mariadb_stmt_store_result_async testgalera: galera_1_timeout_count galera_2_timeout_no_count testaurora: aurora @@ -156,3 +157,18 @@ reg_test_3504-change_user_libmysql_helper: reg_test_3504-change_user_helper.cpp test_clickhouse_server_libmysql: test_clickhouse_server-t.cpp $(CXX) -DLIBMYSQL_HELPER -DDEBUG test_clickhouse_server-t.cpp -I/usr/include/mysql -I$(IDIR) -I$(JSON_IDIR) -I../tap -L$(TAP_LIBDIR) -lpthread -ldl -std=c++11 -ltap -lmysqlclient -o test_clickhouse_server_libmysql-t -DGITVERSION=\"$(GIT_VERSION)\" + +reg_test_stmt_resultset_err_no_rows_libmysql: reg_test_stmt_resultset_err_no_rows-t.cpp + $(CXX) -DLIBMYSQL_HELPER reg_test_stmt_resultset_err_no_rows-t.cpp -I/usr/include/mysql -I$(IDIR) -I$(JSON_IDIR) -I../tap $(OPT) -L$(TAP_LIBDIR) -lpthread -ldl -std=c++11 -ltap -lmysqlclient -o reg_test_stmt_resultset_err_no_rows_libmysql-t -DGITVERSION=\"$(GIT_VERSION)\" + +reg_test_mariadb_stmt_store_result_libmysql: reg_test_mariadb_stmt_store_result-t.cpp $(TAP_LIBDIR)/libtap.a + $(CXX) -DLIBMYSQL_HELPER reg_test_mariadb_stmt_store_result-t.cpp -I/usr/include/mysql -I$(IDIR) -I$(JSON_IDIR) -I../tap $(OPT) -L$(TAP_LIBDIR) -lpthread -ldl -std=c++11 -ltap -lmysqlclient -o reg_test_mariadb_stmt_store_result_libmysql-t -DGITVERSION=\"$(GIT_VERSION)\" + +reg_test_mariadb_stmt_store_result_async: reg_test_mariadb_stmt_store_result-t.cpp $(TAP_LIBDIR)/libtap.a + $(CXX) -DASYNC_API reg_test_mariadb_stmt_store_result-t.cpp $(INCLUDEDIRS) $(LDIRS) $(OPT) $(MYLIBS) -lpthread -ldl -std=c++11 -ltap $(STATIC_LIBS) -o reg_test_mariadb_stmt_store_result_async-t -DGITVERSION=\"$(GIT_VERSION)\" + +prepare_statement_err3024_libmysql: prepare_statement_err3024-t.cpp $(TAP_LIBDIR)/libtap.a + $(CXX) -DLIBMYSQL_HELPER prepare_statement_err3024-t.cpp -I/usr/include/mysql -I$(IDIR) -I$(JSON_IDIR) -I../tap $(OPT) -L$(TAP_LIBDIR) -lpthread -ldl -std=c++11 -ltap -lmysqlclient -o prepare_statement_err3024_libmysql-t -DGITVERSION=\"$(GIT_VERSION)\" + +prepare_statement_err3024_async: prepare_statement_err3024-t.cpp $(TAP_LIBDIR)/libtap.a + $(CXX) -DASYNC_API prepare_statement_err3024-t.cpp $(INCLUDEDIRS) $(LDIRS) $(OPT) $(MYLIBS) -lpthread -ldl -std=c++11 -ltap $(STATIC_LIBS) -o prepare_statement_err3024_async-t -DGITVERSION=\"$(GIT_VERSION)\" diff --git a/test/tap/tests/prepare_statement_err3024-t.cpp b/test/tap/tests/prepare_statement_err3024-t.cpp index 15da38318..51eea03e2 100644 --- a/test/tap/tests/prepare_statement_err3024-t.cpp +++ b/test/tap/tests/prepare_statement_err3024-t.cpp @@ -1,12 +1,21 @@ -#include +/** + * @file prepare_statement_err3024-t.cpp + * @brief Checks proper handling by ProxySQL of binary resultset holding errors. + * @details This test, performs a mixed set of queries, some of them expecting errors. The test is also + * compiled against 'libmysqlclient' and also makes use of both ASYNC and SYNC APIs. + */ + +#include +#include #include #include -#include #include +#include #include #include "tap.h" #include "command_line.h" +#include "proxysql_utils.h" #include "utils.h" const int NUM_EXECUTIONS = 5; @@ -17,10 +26,108 @@ std::string select_query[3] = { "SELECT a.* FROM test.sbtest1 a JOIN test.sbtest1 b WHERE (a.id+b.id)%2 LIMIT 10000" }; +#ifndef LIBMYSQL_HELPER +/* Helper function to do the waiting for events on the socket. */ +static int wait_for_mysql(MYSQL *mysql, int status) { + struct pollfd pfd; + int timeout, res; + + pfd.fd = mysql_get_socket(mysql); + pfd.events = + (status & MYSQL_WAIT_READ ? POLLIN : 0) | + (status & MYSQL_WAIT_WRITE ? POLLOUT : 0) | + (status & MYSQL_WAIT_EXCEPT ? POLLPRI : 0); + if (status & MYSQL_WAIT_TIMEOUT) + timeout = 1000*mysql_get_timeout_value(mysql); + else + timeout = -1; + res = poll(&pfd, 1, timeout); + if (res == 0) + return MYSQL_WAIT_TIMEOUT; + else if (res < 0) + return MYSQL_WAIT_TIMEOUT; + else { + int status = 0; + if (pfd.revents & POLLIN) status |= MYSQL_WAIT_READ; + if (pfd.revents & POLLOUT) status |= MYSQL_WAIT_WRITE; + if (pfd.revents & POLLPRI) status |= MYSQL_WAIT_EXCEPT; + return status; + } +} +#endif + +/** + * @brief Function is required to be duplicated in the test because multiple compilations using + * 'libmysqlclient' and 'libmariadbclient'. TODO: Being able to share helper functions targetting different + * connector libraries between tap tests. + */ +int add_more_rows_test_sbtest1(int num_rows, MYSQL *mysql, bool sqlite) { + std::random_device rd; + std::mt19937 mt(rd()); + std::uniform_int_distribution dist(0.0, 9.0); + + diag("Creating %d rows in sbtest1", num_rows); + while (num_rows) { + std::stringstream q; + + if (sqlite==false) { + q << "INSERT INTO test.sbtest1 (k, c, pad) values "; + } else { + q << "INSERT INTO sbtest1 (k, c, pad) values "; + } + bool put_comma = false; + int i=0; + unsigned int cnt=5+rand()%50; + if (cnt > num_rows) cnt = num_rows; + for (i=0; i +#include +#include +#include +#include +#include +#include + +#include "tap.h" +#include "command_line.h" +#include "utils.h" + +#ifndef LIBMYSQL_HELPER +/* Helper function to do the waiting for events on the socket. */ +static int wait_for_mysql(MYSQL *mysql, int status) { + struct pollfd pfd; + int timeout, res; + + pfd.fd = mysql_get_socket(mysql); + pfd.events = + (status & MYSQL_WAIT_READ ? POLLIN : 0) | + (status & MYSQL_WAIT_WRITE ? POLLOUT : 0) | + (status & MYSQL_WAIT_EXCEPT ? POLLPRI : 0); + if (status & MYSQL_WAIT_TIMEOUT) + timeout = 1000*mysql_get_timeout_value(mysql); + else + timeout = -1; + res = poll(&pfd, 1, timeout); + if (res == 0) + return MYSQL_WAIT_TIMEOUT; + else if (res < 0) + return MYSQL_WAIT_TIMEOUT; + else { + int status = 0; + if (pfd.revents & POLLIN) status |= MYSQL_WAIT_READ; + if (pfd.revents & POLLOUT) status |= MYSQL_WAIT_WRITE; + if (pfd.revents & POLLPRI) status |= MYSQL_WAIT_EXCEPT; + return status; + } +} +#endif + +/** + * @brief Function is required to be duplicated in the test because multiple compilations using + * 'libmysqlclient' and 'libmariadbclient'. TODO: Being able to share helper functions targetting different + * connector libraries between tap tests. + */ +int add_more_rows_test_sbtest1(int num_rows, MYSQL *mysql, bool sqlite) { + std::random_device rd; + std::mt19937 mt(rd()); + std::uniform_int_distribution dist(0.0, 9.0); + + diag("Creating %d rows in sbtest1", num_rows); + while (num_rows) { + std::stringstream q; + + if (sqlite==false) { + q << "INSERT INTO test.sbtest1 (k, c, pad) values "; + } else { + q << "INSERT INTO sbtest1 (k, c, pad) values "; + } + bool put_comma = false; + int i=0; + unsigned int cnt=5+rand()%50; + if (cnt > num_rows) cnt = num_rows; + for (i=0; i +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "proxysql_utils.h" +#include "tap.h" +#include "command_line.h" +#include "utils.h" + +using std::string; +using std::vector; +using std::tuple; + +using test_case_t = tuple; + +const uint32_t STRING_SIZE = 1024; +const vector TEST_CASES { + {true, "$.", ""}, {false, "$", "[\"a\", \"b\"]"}, {true, "$.", ""}, {false, "$.b", "[\"c\"]"} +}; + +int main(int argc, char** argv) { + int res = EXIT_SUCCESS; + + plan(TEST_CASES.size()); + + CommandLine cl; + + if (cl.getEnv()) { + diag("Failed to get the required environmental variables."); + return EXIT_FAILURE; + } + + MYSQL* proxy = mysql_init(NULL); + MYSQL* admin = mysql_init(NULL); + +#ifdef LIBMYSQL_HELPER + enum mysql_ssl_mode ssl_mode = SSL_MODE_DISABLED; + mysql_options(proxy, MYSQL_OPT_SSL_MODE, &ssl_mode); +#endif + + proxy->options.client_flag &= ~CLIENT_DEPRECATE_EOF; + + if (!mysql_real_connect(proxy, cl.host, cl.username, cl.password, NULL, cl.port, NULL, 0)) { + fprintf(stderr, "File %s, line %d, Error: %s\n", __FILE__, __LINE__, mysql_error(proxy)); + return EXIT_FAILURE; + } + + if (!mysql_real_connect(admin, cl.host, cl.admin_username, cl.admin_password, NULL, cl.admin_port, NULL, 0)) { + fprintf(stderr, "File %s, line %d, Error: %s\n", __FILE__, __LINE__, mysql_error(admin)); + return EXIT_FAILURE; + } + + const string stmt_query { "SELECT json_keys('{\"a\": 0, \"b\": {\"c\": 1}}', ?)" }; + + for (const test_case_t& test_case : TEST_CASES) { + const bool exp_fail { std::get<0>(test_case) }; + const string& param { std::get<1>(test_case) }; + const string& exp_res { std::get<2>(test_case) }; + + MYSQL_STMT* stmt = mysql_stmt_init(proxy); + + if (!stmt) { + diag("mysql_stmt_init(), out of memory"); + res = EXIT_FAILURE; + goto exit; + } + + if (mysql_stmt_prepare(stmt, stmt_query.c_str(), strlen(stmt_query.c_str()))) { + diag("mysql_stmt_prepare at line %d failed: %s", __LINE__ , mysql_error(proxy)); + mysql_close(proxy); + res = EXIT_FAILURE; + goto exit; + } + + MYSQL_BIND bind_params; + memset(&bind_params, 0, sizeof(MYSQL_BIND)); + char str_data[STRING_SIZE] = { 0 }; + uint64_t str_length = 0; + + bind_params.buffer_type = MYSQL_TYPE_STRING; + bind_params.buffer = static_cast(str_data); + bind_params.buffer_length = STRING_SIZE; + bind_params.is_null = 0; + bind_params.length = &str_length; + + if (mysql_stmt_bind_param(stmt, &bind_params)) { + diag("mysql_stmt_bind_result at line %d failed: '%s'", __LINE__ , mysql_stmt_error(stmt)); + res = EXIT_FAILURE; + goto exit; + } + + strncpy(str_data, param.c_str(), STRING_SIZE); + str_length = strlen(str_data); + + int exec_res = mysql_stmt_execute(stmt); + if (exec_res) { + ok(exp_fail, "'mysql_stmt_execute' returned error: '%s'", mysql_stmt_error(stmt)); + if (exp_fail) { + diag("mysql_stmt_execute at line %d failed: '%s'", __LINE__, mysql_stmt_error(stmt)); + mysql_stmt_close(stmt); + continue; + } else { + diag("mysql_stmt_execute at line %d failed: '%s'", __LINE__, mysql_stmt_error(stmt)); + res = EXIT_FAILURE; + goto exit; + } + } + + MYSQL_BIND bind; + memset(&bind, 0, sizeof(bind)); + + char data_c2[STRING_SIZE] = { 0 }; +#ifdef LIBMYSQL_HELPER + bool is_null[1]; + bool error[1]; + long unsigned int length[1]; +#else + char is_null[1]; + char error[1]; + long unsigned int length[1]; +#endif + + bind.buffer_type = MYSQL_TYPE_STRING; + bind.buffer = (char *)&data_c2; + bind.buffer_length = STRING_SIZE; + bind.is_null = &is_null[0]; + bind.length = &length[0]; + bind.error = &error[0]; + + if (mysql_stmt_bind_result(stmt, &bind)) { + diag("mysql_stmt_bind_result at line %d failed: %s", __LINE__, mysql_stmt_error(stmt)); + res = EXIT_FAILURE; + goto exit; + } + + int res_err = mysql_stmt_store_result(stmt); + if (res_err) { + ok(exp_fail, "'mysql_stmt_store_result' returned error: '%s'", mysql_stmt_error(stmt)); + + if (exp_fail) { + mysql_stmt_close(stmt); + continue; + } else { + res = EXIT_FAILURE; + goto exit; + } + } else { + int field_count = mysql_stmt_field_count(stmt); + + if (mysql_stmt_fetch(stmt) == 1 && field_count == 1) { + diag("mysql_stmt_fetch at line %d failed: %s", __LINE__, mysql_stmt_error(stmt)); + res = EXIT_FAILURE; + goto exit; + } + + ok( + string { data_c2 } == exp_res && field_count == 1, + "Prepared statement SELECT matched expected - FieldCount: '%d', Exp: '%s', Act: '%s'", + field_count, exp_res.c_str(), data_c2 + ); + } + + mysql_stmt_close(stmt); + } + +exit: + mysql_close(proxy); + mysql_close(admin); + + if (res == EXIT_SUCCESS) { + return exit_status(); + } else { + return res; + } +} diff --git a/test/tap/tests/reg_test_stmt_resultset_err_no_rows.php b/test/tap/tests/reg_test_stmt_resultset_err_no_rows.php new file mode 100755 index 000000000..38056ba42 --- /dev/null +++ b/test/tap/tests/reg_test_stmt_resultset_err_no_rows.php @@ -0,0 +1,111 @@ +#!/usr/bin/env php +connect_errno) { + die("PorxySQL connect failed: " . $proxy->connect_error); +} +echo ":: ProxySQL Admin connection completed".PHP_EOL; + +echo ":: Creating ProxySQL connection...".PHP_EOL; +$proxy = new mysqli("127.0.0.1", $username, $password, "", $port); +if ($proxy->connect_errno) { + die("PorxySQL connect failed: " . $proxy->connect_error); +} +echo ":: ProxySQL connection completed".PHP_EOL; + +$stmt = $proxy->prepare("SELECT json_keys('{\"a\": 1, \"b\": {\"c\": 30}}', ?)"); + +foreach ($test_cases as $test_case) { + [$exp_fail, $param, $exp_res] = $test_case; + + try { + echo ":: Binding param: '", $param, "'\n"; + + $sql_select_limit = $param; + $stmt->bind_param('s', $sql_select_limit); + + $stmt->execute(); + + $result = $stmt->get_result(); + + if ($result->num_rows == 1) { + $row = $result->fetch_array(MYSQLI_NUM); + $field_count = $result->field_count; + $field_val = $row[0]; + + ok( + $field_count == 1 && $field_val == $exp_res, + "Fetch value should match expected - ". + "FieldCount: '".$field_count."', Exp: '".$exp_res."', Act: '".$field_val + ); + } else { + diag("Received invalid number of rows '".$result->num_rows."'"); + } + } catch (Exception $e) { + ok($exp_fail == true, "Operation failed with error: '".$e->getMessage()."'"); + } +} + +$success = ($plan_tests == $cases && $res == 0); +$exit_code = $success == false ? 1 : 0; + +exit($exit_code); +?> diff --git a/test/tap/tests/reg_test_stmt_resultset_err_no_rows_php-t.cpp b/test/tap/tests/reg_test_stmt_resultset_err_no_rows_php-t.cpp new file mode 100644 index 000000000..2f1602d21 --- /dev/null +++ b/test/tap/tests/reg_test_stmt_resultset_err_no_rows_php-t.cpp @@ -0,0 +1,46 @@ +/** + * @file reg_test_stmt_resultset_err_no_rows_php-t.cpp + * @brief Checks handling of STMT binary resultsets with errors by means of PHP test + * 'reg_test_stmt_resultset_err_no_rows.php'. + */ + +#include +#include +#include + +#include +#include + +#include "proxysql_utils.h" +#include "tap.h" +#include "command_line.h" +#include "utils.h" + +using std::string; + +int main(int argc, char** argv) { + plan(1); + + CommandLine cl {}; + + if (cl.getEnv()) { + diag("Failed to get the required environmental variables."); + return EXIT_FAILURE; + } + + string php_stdout {}; + string php_stderr {}; + const string php_path { string{ cl.workdir } + "./reg_test_stmt_resultset_err_no_rows.php" }; + + to_opts opts {2 * 1000 * 1000, 0, 0, 0}; + int exec_res = wexecvp(php_path, {}, &opts, php_stdout, php_stderr); + + diag("Output from executed test: '%s'", php_path.c_str()); + diag("========================================================================"); + + std::cout << php_stdout; + + diag("========================================================================"); + + ok(exec_res == EXIT_SUCCESS, "Test exited with code '%d'", exec_res); +}