diff --git a/scripts/test_databases.sh b/scripts/test_databases.sh index 47ea00cce..a0cae393d 100755 --- a/scripts/test_databases.sh +++ b/scripts/test_databases.sh @@ -3,14 +3,14 @@ set -euo pipefail if [[ $# -lt 1 || $# -gt 2 ]]; then - printf 'Usage: %s BUILD_DIR [local|redis|mysql|redis-awaitable|mysql-awaitable|all]\n' "$0" >&2 + printf 'Usage: %s BUILD_DIR [local|redis|mysql|redis-awaitable|mysql-awaitable|public-redis|public-mysql|public-services|all]\n' "$0" >&2 exit 2 fi database_test_dir=$(cd -- "$1" && pwd)/tests/database database_test_mode=${2:-local} case "$database_test_mode" in - local|redis|mysql|redis-awaitable|mysql-awaitable|all) ;; + local|redis|mysql|redis-awaitable|mysql-awaitable|public-redis|public-mysql|public-services|all) ;; *) printf 'Unknown database test mode: %s\n' "$database_test_mode" >&2; exit 2 ;; esac @@ -50,3 +50,9 @@ fi if [[ "$database_test_mode" == mysql-awaitable || "$database_test_mode" == all ]]; then run_database_test ark_database_service_awaitable_tests mysql fi +if [[ "$database_test_mode" == public-redis || "$database_test_mode" == public-services || "$database_test_mode" == all ]]; then + run_database_test ark_database_public_service_tests redis +fi +if [[ "$database_test_mode" == public-mysql || "$database_test_mode" == public-services || "$database_test_mode" == all ]]; then + run_database_test ark_database_public_service_tests mysql +fi diff --git a/tests/database/CMakeLists.txt b/tests/database/CMakeLists.txt index 7dc500a78..c29f11d48 100644 --- a/tests/database/CMakeLists.txt +++ b/tests/database/CMakeLists.txt @@ -1,4 +1,4 @@ -# Development-only native database bridges; no runtime plugin is enabled here. +# Development-only database checks; no runtime application plugin is enabled here. find_package(Threads REQUIRED) if(NOT EXISTS "${ARKNET_SOURCE_DIR}/include/arknet/redis/pool.hpp") message(FATAL_ERROR "Database development tests require a local arknet checkout with the native Redis pool") @@ -36,6 +36,10 @@ target_link_libraries(ark_database_redis_stop_tests PRIVATE arknet::arknet) ark_database_test(ark_database_public_redis_tests main.cpp public_redis_module.cpp "${ROOT_DIR}/src/plugins/redis/redis_module_impl.cpp") target_link_libraries(ark_database_public_redis_tests PRIVATE ark::core arknet::arknet) +# Explicit real-service acceptance; deliberately excluded from check-database. +ark_database_test(ark_database_public_service_tests public_service.cpp + "${ROOT_DIR}/src/plugins/redis/redis_module_impl.cpp") +target_link_libraries(ark_database_public_service_tests PRIVATE ark::core arknet::arknet) add_library(ark_database_redis_plugin SHARED "${ROOT_DIR}/src/plugins/redis/redis_plugin.cpp" "${ROOT_DIR}/src/plugins/redis/redis_module_impl.cpp") target_include_directories(ark_database_redis_plugin PRIVATE "${ROOT_DIR}/src/plugins") @@ -67,9 +71,12 @@ add_custom_target(check-database ark_database_task_contract_tests USES_TERMINAL) -add_dependencies(check-database ark_database_redis_sample_check) +# Compile real-service acceptance in ordinary CI without running any server work. +add_dependencies(check-database ark_database_redis_sample_check ark_database_public_service_tests) if(ARKNET_ENABLE_SSL) + target_sources(ark_database_public_service_tests PRIVATE + "${ROOT_DIR}/src/plugins/mysql/mysql_module_impl.cpp") target_compile_definitions(ark_database_public_redis_tests PRIVATE ARK_DATABASE_REDIS_CERT_DIR="${ARKNET_SOURCE_DIR}/tests/certs") ark_database_test(ark_database_public_mysql_tests main.cpp public_mysql_module.cpp diff --git a/tests/database/README.md b/tests/database/README.md index 59c121015..6a0044fef 100644 --- a/tests/database/README.md +++ b/tests/database/README.md @@ -186,3 +186,55 @@ peers. `ark_database_public_redis_plugin_tests` and `ark_database_public_mysql_plugin_tests` load actual hidden-symbol plugin libraries. The migrated samples are also compiled by `check-database`. MySQL checks require SSL; real-server targets still require explicit endpoint settings. + +## Public modules against real services + +`ark_database_public_service_tests` calls the public `mysql_module` and +`redis_module` interfaces with their real implementations, a registered consumer's +`task_group`, and the manager's shared `manual_runtime`. It is a separate C++ +executable; `check-database` compiles it without running it or connecting to a server. + +```sh +cmake --build database-build --target ark_database_public_service_tests --parallel 2 +# Configure the isolated endpoints first, then choose either protocol or both. +bash scripts/test_databases.sh database-build public-redis +bash scripts/test_databases.sh database-build public-mysql +bash scripts/test_databases.sh database-build public-services +# Equivalent direct invocations: +./database-build/tests/database/ark_database_public_service_tests redis +./database-build/tests/database/ark_database_public_service_tests mysql +``` + +Redis requires `ARK_DATABASE_REDIS_HOST` and `ARK_DATABASE_REDIS_PORT`. Optional +`ARK_DATABASE_REDIS_USER` and `ARK_DATABASE_REDIS_PASSWORD` configure authentication; +use an authenticated disposable service to verify that path. The check mutates the +caller's configuration after registration, verifies an ordered binary ECHO/ECHO/PING +pipeline and a follow-up command, and makes no writes. + +MySQL requires `ARK_DATABASE_MYSQL_HOST`, `ARK_DATABASE_MYSQL_PORT`, +`ARK_DATABASE_MYSQL_USER`, `ARK_DATABASE_MYSQL_PASSWORD` (present, possibly empty), +`ARK_DATABASE_MYSQL_DATABASE` and `ARK_DATABASE_MYSQL_CA`. TLS and peer verification +are required. Use a disposable schema and an account with CREATE, DROP, SELECT and +INSERT privileges. The check creates a unique `ark_public_service_...` InnoDB +table, exercises text queries, binary prepared results, transaction queries and +prepared inserts, then verifies committed data and the absence of rolled-back +data. It drops its table after the workflow, including assertion failures when the +connection remains usable. A failed connection, active transaction cleanup, or +forced process exit can leave the table for disposable-schema cleanup. + +Both protocols assert owner-thread/shared-executor coroutine resumption, input and +configuration ownership, and result ownership after subsequent operations and +module/runtime destruction. The manager enters natural drain with accepted work +pending; follow-up requests finish before successful, idempotent stop. There is no +cancellation or service fault injection in these real-service cases. Each protocol +also repeats its workflow with deliberate host-stack unwinding while IO is pending: +named coroutine functions own their input values, the owner destructor drains the +workflow, and the outer scope verifies its completion and retained results. +Protocol-peer and DSO regressions remain separate checks; these results do not establish dynamic-library +unloading, failure recovery or capacity acceptance. + +The executable requires explicit endpoints, returns nonzero for missing/invalid +configuration or failure, and does not print credentials or arbitrary native error +messages. It never starts or stops servers. `all` also includes both public +real-service modes and therefore requires an SSL-enabled build and both endpoint +configurations. diff --git a/tests/database/README_CN.md b/tests/database/README_CN.md index 80d38a50b..6a774b42e 100644 --- a/tests/database/README_CN.md +++ b/tests/database/README_CN.md @@ -148,3 +148,45 @@ bash scripts/test_databases.sh database-build redis 验证拥有型结果、取消、TLS 与正常停机。`ark_database_public_redis_plugin_tests` 和 `ark_database_public_mysql_plugin_tests` 加载实际隐藏符号动态库。 `check-database` 还编译迁移后的示例。MySQL 检查要求开启 SSL,真实服务目标仍需显式配置端点。 + +## 公开模块真实服务验收 + +`ark_database_public_service_tests` 通过公开的 `mysql_module`、`redis_module` +接口调用真实实现,使用已注册消费模块的 `task_group` 和管理器共享的 +`manual_runtime`。这是独立的 C++ 可执行程序;`check-database` 仅编译此程序,不运行,也不连接服务。 + +```sh +cmake --build database-build --target ark_database_public_service_tests --parallel 2 +# 先配置隔离服务的端点,再选择一种协议或同时运行两种协议。 +bash scripts/test_databases.sh database-build public-redis +bash scripts/test_databases.sh database-build public-mysql +bash scripts/test_databases.sh database-build public-services +# 也可以直接运行: +./database-build/tests/database/ark_database_public_service_tests redis +./database-build/tests/database/ark_database_public_service_tests mysql +``` + +Redis 要求设置 `ARK_DATABASE_REDIS_HOST` 和 `ARK_DATABASE_REDIS_PORT`。 +可选的 `ARK_DATABASE_REDIS_USER`、`ARK_DATABASE_REDIS_PASSWORD` 用于认证; +验证认证路径时,需使用启用了认证的临时服务。测试在注册后修改调用方配置, +验证有序的二进制 ECHO/ECHO/PING 流水线及后续命令,不执行写入。 + +MySQL 要求设置 `ARK_DATABASE_MYSQL_HOST`、`ARK_DATABASE_MYSQL_PORT`、 +`ARK_DATABASE_MYSQL_USER`、`ARK_DATABASE_MYSQL_PASSWORD`(必须存在,可以为空)、 +`ARK_DATABASE_MYSQL_DATABASE` 和 `ARK_DATABASE_MYSQL_CA`。连接必须使用 TLS 并验证证书。 +使用临时数据库,以及具备 CREATE、DROP、SELECT、INSERT 权限的账号。 +测试创建唯一的 `ark_public_service_...` InnoDB 表,执行文本查询、二进制预处理查询、 +事务查询和预处理插入,再验证提交数据可见、回滚数据不可见。流程结束后删除测试表; +断言失败且连接仍可用时,也会尝试删除。连接失败、活动事务清理或强制退出进程可能留下测试表, +需随临时数据库一起清理。 + +两种协议均检查协程在拥有线程及共享执行器上恢复、输入与配置的所有权,以及后续操作和 +模块/运行时销毁后结果仍然有效。管理器在已受理任务等待期间进入自然排空状态, +允许流程完成后续请求,再验证成功且可重复调用的停止操作。每种协议还会在 IO 等待期间主动 +触发宿主栈展开并重复整个流程:命名协程函数按值拥有输入,拥有者析构时排空流程,外层作用域 +再验证完成结果与保留的数据。这些真实服务用例不注入服务故障,也不测试取消。协议端点回归和动态库回归仍是独立检查;此处结果不代表动态库卸载、 +故障恢复或容量验收。 + +程序必须显式配置端点;配置缺失、无效或验收失败均返回非零状态,不打印凭据或任意原生错误文本, +也不会启动或停止服务。`all` 也运行这两种公开模块真实服务模式,因此要求启用 SSL, +并配置两种服务的端点。 diff --git a/tests/database/public_service.cpp b/tests/database/public_service.cpp new file mode 100644 index 000000000..d05e09b00 --- /dev/null +++ b/tests/database/public_service.cpp @@ -0,0 +1,407 @@ +// Copyright (c) 2026 OpenArkStudio. Licensed under Apache-2.0. See LICENSE. +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "ark/redis/redis_module_impl.hpp" + +#if defined(ARKNET_ENABLE_SSL) +#include "ark/mysql/mysql_module_impl.hpp" +#endif + +namespace { + +using namespace std::chrono_literals; +unsigned checks{}; + +class acceptance_failure : public std::runtime_error +{ +public: + using std::runtime_error::runtime_error; +}; + +void require(bool condition, const std::string& message) { + ++checks; + if (!condition) + { + throw acceptance_failure(message); + } +} + +std::string environment(const char* name, bool allow_empty = false) { + const auto* value = std::getenv(name); + require(value && (allow_empty || *value), std::string("Required test environment variable: ") + name); + return value; +} + +std::uint16_t port(const char* name) { + const auto text = environment(name); + unsigned number{}; + const auto parsed = std::from_chars(text.data(), text.data() + text.size(), number); + require(parsed.ec == std::errc{} && parsed.ptr == text.data() + text.size() && number > 0 && number <= 65535, + std::string(name) + " must be an integer between 1 and 65535"); + return static_cast(number); +} + +struct workflow_result +{ + std::exception_ptr failure; + unsigned completions{}; + bool completion_on_owner{}; +}; + +struct unwind_workflow +{ +}; + +void verify_workflow(const workflow_result& result) { + if (result.failure) + { + std::rethrow_exception(result.failure); + } + require(result.completions == 1 && result.completion_on_owner, + "Workflow completion was not delivered once on its owner"); +} + +// Register the real implementation exactly as a plugin does, with a consumer's +// manager-owned task group sharing the module's manual_runtime. +template +struct module_owner +{ + ark::plugin_manager manager; + module_type database; + ark::module consumer; + ark::task_group* tasks{}; + std::shared_ptr runtime; + const std::thread::id thread{std::this_thread::get_id()}; + workflow_result& result; + bool stopped{}; + + explicit module_owner(workflow_result& report) + : result(report) { + database.set_plugin_manager(&manager); + database.set_name("database"); + require(manager.add_module("database", &database) && manager.add_module("consumer", &consumer), + "Cannot install public database test modules"); + require(manager.add_update_module(&database), "Cannot install public database update hook"); + runtime = manager.get_task_runtime(); + tasks = &manager.create_task_group(consumer); + require(database.init(), "Cannot initialize public database module"); + } + + ~module_owner() { + if (!stopped) + { + // Never destroy business captures or a group with suspended tasks. + // Failure paths keep pumping accepted work before unwinding. + try + { finish(); } + catch (...) + { + std::cerr << "Public database cleanup could not drain\n" << std::flush; + std::_Exit(1); + } + } + } + + void check_executor() const { + require(std::this_thread::get_id() == thread && runtime->get_executor().running_in_this_thread(), + "Public database coroutine escaped its owner thread/shared manual runtime"); + } + + void start(ark::task<> workflow) { + require(tasks->start(std::move(workflow), + [this](std::exception_ptr error) { + result.completion_on_owner = + std::this_thread::get_id() == thread && runtime->get_executor().running_in_this_thread(); + result.failure = std::move(error); + ++result.completions; + }) != 0, + "Public database workflow was not accepted"); + require(manager.update(0), "Cannot start public database workflow"); + require(tasks->pending() == 1, "Public database workflow did not suspend for native IO"); + require(!manager.stop(0), "Manager stopped before its accepted database workflow drained"); + require(manager.get_shutdown_state() == ark::plugin_manager::shutdown_state::draining, + "Manager did not enter natural drain"); + require(!database.configure("late", {}), "Database admitted configuration during drain"); + } + + void finish() { + const auto deadline = std::chrono::steady_clock::now() + 30s; + while (!(stopped = manager.stop(0))) + { + if (manager.get_shutdown_state() == ark::plugin_manager::shutdown_state::failed || + std::chrono::steady_clock::now() >= deadline) + { + throw acceptance_failure("Public database manager did not drain/stop successfully"); + } + require(manager.update(0), "Public database manager update failed during drain"); + std::this_thread::sleep_for(1ms); + } + } + + void verify() { + finish(); + verify_workflow(result); + require(tasks->pending() == 0 && tasks->closed(), "Stopped manager retained public database tasks"); + require(manager.stop(0), "Manager stop was not idempotent"); + } +}; + +ark::task<> redis_workflow(module_owner& owner, ark::task pending, + std::string binary, std::optional& retained) { + ark::redis_module& database = owner.database; + retained = co_await std::move(pending); + owner.check_executor(); + require(bool(*retained) && retained->replies.size() == 3, "Public Redis binary pipeline failed"); + require( + retained->replies[0].text == binary && retained->replies[1].text.empty() && retained->replies[2].text == "PONG", + "Public Redis lost owning inputs or ordered binary replies"); + auto second = co_await database.command("primary", {"PING"}, 5s); + owner.check_executor(); + require(bool(second) && second.replies.size() == 1 && second.replies[0].text == "PONG", + "Public Redis follow-up command failed during natural drain"); + require(retained->replies[0].text == binary, "Public Redis prior result changed after the next command"); +} + +void check_redis(bool unwind) { + ark::redis_options options; + options.pool.connection.host = environment("ARK_DATABASE_REDIS_HOST"); + options.pool.connection.port = std::to_string(port("ARK_DATABASE_REDIS_PORT")); + if (const auto* user = std::getenv("ARK_DATABASE_REDIS_USER")) + { + options.pool.connection.username = user; + } + if (const auto* password = std::getenv("ARK_DATABASE_REDIS_PASSWORD")) + { + options.pool.connection.password = password; + } + options.pool.max_connections = 1; + options.pool.client_configuration.connect_timeout = 3s; + const std::string binary("public\0redis\r\nvalue", 19); + std::optional retained; + workflow_result report; + bool unwound{}; + try + { + module_owner owner(report); + ark::redis_module& database = owner.database; + require(database.configure("primary", options), "Cannot configure public Redis module"); + // The module owns endpoint and authentication strings after configure(). + options.pool.connection.host = "invalid.invalid"; + options.pool.connection.username = "changed"; + options.pool.connection.password = "changed"; + std::vector commands{{"ECHO", binary}, {"ECHO", ""}, {"PING"}}; + auto pending = database.pipeline("primary", commands, 5s); + commands[0][1] = "changed"; + commands.clear(); + owner.start(redis_workflow(owner, std::move(pending), binary, retained)); + if (unwind) + { + throw unwind_workflow{}; + } + owner.verify(); + } + catch (const unwind_workflow&) + { unwound = true; } + require(unwound == unwind, "Redis host stack did not unwind as requested"); + verify_workflow(report); + require(retained && retained->replies[0].text == binary, + "Public Redis owning result was lost after module/runtime destruction"); +} + +#if defined(ARKNET_ENABLE_SSL) +void success(const ark::mysql_reply& reply) { + require(bool(reply), "Public MySQL operation failed (code " + + std::to_string(static_cast(reply.result.status.code)) + ", server code " + + std::to_string(reply.result.status.server_code) + ")"); + require(!reply.statement_close_exception && (!reply.statement_close || bool(*reply.statement_close)), + "Public MySQL statement cleanup failed"); +} + +void success(const ark::mysql_transaction_reply& reply) { + require(bool(reply), "Public MySQL transaction operation failed (code " + + std::to_string(static_cast(reply.result.status.code)) + ", server code " + + std::to_string(reply.result.status.server_code) + ")"); + require(!reply.statement_close_exception && (!reply.statement_close || bool(*reply.statement_close)) && + !reply.transaction_status.cleanup_exception && !reply.transaction_status.lease_return_exception, + "Public MySQL transaction cleanup failed"); +} + +ark::task<> mysql_checks(module_owner& owner, ark::task prepared, + std::string table, std::string binary, std::optional& retained_text, + std::optional& retained_prepared) { + ark::mysql_module& database = owner.database; + retained_text = co_await database.query("primary", "SELECT -7 AS repeated, 'owned text' AS repeated, NULL", 5s); + owner.check_executor(); + success(*retained_text); + const auto& text = retained_text->result; + require(text.columns.size() == 3 && text.rows.size() == 1 && text.rows[0].size() == 3 && + text.columns[0].name == "repeated" && text.columns[1].name == "repeated", + "Public MySQL query lost shape or duplicate column names"); + require(std::get(text.rows[0][0]) == -7 && std::get(text.rows[0][1]) == "owned text" && + std::holds_alternative(text.rows[0][2]), + "Public MySQL query lost owning typed fields"); + retained_prepared = co_await std::move(prepared); + owner.check_executor(); + success(*retained_prepared); + const auto& result = retained_prepared->result; + require(result.rows.size() == 1 && result.rows[0].size() == 2 && std::get(result.rows[0][0]) == -9 && + std::get(result.rows[0][1]) == binary, + "Public MySQL prepared query lost owning inputs/binary results"); + require(retained_prepared->statement_close && bool(*retained_prepared->statement_close), + "Public MySQL prepared statement close was not reported"); + for (const bool commit : {true, false}) + { + auto begun = co_await database.begin("primary", {10s, 3s}); + owner.check_executor(); + success(begun); + require(begun.transaction && begun.transaction_status.outcome == ark::mysql_transaction_outcome::active, + "Public MySQL transaction did not begin"); + auto queried = co_await database.query(begun.transaction, "SELECT CONNECTION_ID()"); + owner.check_executor(); + success(queried); + require(queried.result.rows.size() == 1, "Public MySQL transaction query failed"); + std::vector values{std::int64_t{commit ? 1 : 2}, binary}; + auto inserted = co_await database.execute( + begun.transaction, "INSERT INTO " + table + " (id, payload) VALUES (?, ?)", std::move(values)); + owner.check_executor(); + success(inserted); + require(inserted.result.affected_rows == 1 && inserted.statement_close && bool(*inserted.statement_close), + "Public MySQL transaction prepared insert/cleanup failed"); + auto ended = + commit ? co_await database.commit(begun.transaction) : co_await database.rollback(begun.transaction); + owner.check_executor(); + success(ended); + require(ended.transaction_status.outcome == + (commit ? ark::mysql_transaction_outcome::committed : ark::mysql_transaction_outcome::rolled_back), + "Public MySQL commit/rollback terminal outcome changed"); + } + auto persisted = co_await database.query("primary", "SELECT id, payload FROM " + table + " ORDER BY id", 5s); + owner.check_executor(); + success(persisted); + require(persisted.result.rows.size() == 1 && persisted.result.rows[0].size() == 2 && + std::get(persisted.result.rows[0][0]) == 1 && + std::get(persisted.result.rows[0][1]) == binary, + "Public MySQL commit/rollback visibility or binary persistence failed"); + require(std::get(retained_text->result.rows[0][1]) == "owned text" && + std::get(retained_prepared->result.rows[0][1]) == binary, + "Public MySQL prior results changed after transaction reuse"); +} + +ark::task<> mysql_workflow(module_owner& owner, ark::task prepared, + std::string table, std::string binary, std::optional& retained_text, + std::optional& retained_prepared) { + ark::mysql_module& database = owner.database; + auto created = co_await database.query( + "primary", "CREATE TABLE " + table + " (id BIGINT PRIMARY KEY, payload VARBINARY(128)) ENGINE=InnoDB", 5s); + owner.check_executor(); + success(created); + std::exception_ptr error; + try + { co_await mysql_checks(owner, std::move(prepared), table, std::move(binary), retained_text, retained_prepared); } + catch (...) + { error = std::current_exception(); } + auto removed = co_await database.query("primary", "DROP TABLE " + table, 5s); + owner.check_executor(); + success(removed); + if (error) + { + std::rethrow_exception(error); + } +} + +void check_mysql(bool unwind) { + ark::mysql_options options; + options.pool.connection.host = environment("ARK_DATABASE_MYSQL_HOST"); + options.pool.connection.port = port("ARK_DATABASE_MYSQL_PORT"); + options.pool.connection.username = environment("ARK_DATABASE_MYSQL_USER"); + options.pool.connection.password = environment("ARK_DATABASE_MYSQL_PASSWORD", true); + options.pool.connection.database = environment("ARK_DATABASE_MYSQL_DATABASE"); + options.pool.connection.ca_file = environment("ARK_DATABASE_MYSQL_CA"); + options.pool.connection.tls = arknet::mysql::tls_mode::required; + options.pool.connection.timeout = 5s; + options.pool.max_connections = 1; + const std::string binary("public\0mysql\r\nvalue", 19); + const auto table = + "ark_public_service_" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()); + std::optional retained_text, retained_prepared; + workflow_result report; + bool unwound{}; + try + { + module_owner owner(report); + ark::mysql_module& database = owner.database; + require(database.configure("primary", options), "Cannot configure public MySQL module"); + options.pool.connection.host = "invalid.invalid"; + options.pool.connection.username = "changed"; + options.pool.connection.password = "changed"; + options.pool.connection.ca_file = "missing-certificate.pem"; + std::vector parameters{std::int64_t{-9}, binary}; + auto prepared = database.execute("primary", "SELECT CAST(? AS SIGNED), ?", parameters, 5s); + parameters[0] = std::int64_t{123}; + parameters[1] = std::string("changed"); + owner.start(mysql_workflow(owner, std::move(prepared), table, binary, retained_text, retained_prepared)); + if (unwind) + { + throw unwind_workflow{}; + } + owner.verify(); + } + catch (const unwind_workflow&) + { unwound = true; } + require(unwound == unwind, "MySQL host stack did not unwind as requested"); + verify_workflow(report); + require(retained_text && std::get(retained_text->result.rows[0][1]) == "owned text" && + retained_prepared && std::get(retained_prepared->result.rows[0][1]) == binary, + "Public MySQL owning results were lost after module/runtime destruction"); +} +#endif + +} // namespace + +int main(int argc, char** argv) { + try + { + require(argc == 2, "Usage: ark_database_public_service_tests redis|mysql"); + const std::string_view protocol(argv[1]); + if (protocol == "redis") + { + check_redis(false); + check_redis(true); + } +#if defined(ARKNET_ENABLE_SSL) + else if (protocol == "mysql") + { + check_mysql(false); + check_mysql(true); + } +#endif + else + { + throw acceptance_failure("Requested public database protocol is not enabled"); + } + std::cout << protocol << " public module real-service checks passed: " << checks << '\n'; + return 0; + } + catch (const acceptance_failure& error) + { std::cerr << error.what() << '\n'; } + catch (...) + { + // Native diagnostics can contain server-controlled text. Do not emit + // endpoint/authentication configuration or arbitrary exception messages. + std::cerr << "Public database service acceptance raised an unexpected exception\n"; + } + return 1; +}