diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index de2ec5664..4730be9fb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -286,6 +286,7 @@ jobs: -DARK_BUILD_BENCHMARKS=ON -DARK_BUILD_PLUGIN_HTTP=ON -DARK_BUILD_PLUGIN_CONSUL=ON + -DARK_BUILD_PLUGIN_ETCD_CLIENT=ON -DARK_BUILD_PROTO=ON -DARK_BUILD_TESTS=ON -DARK_BUILD_CORE_TESTS=ON diff --git a/docs/guides/plugins.md b/docs/guides/plugins.md index cd3fbcd5d..7afc46cf7 100644 --- a/docs/guides/plugins.md +++ b/docs/guides/plugins.md @@ -213,3 +213,5 @@ Net constructs its HTTP client through a private factory returning `std::unique_ Kernel also uses typed construction while retaining its public `*_impl` and `kernel_plugin` types and header paths. Its private factory header is not installed. Rebuild Kernel and consumers together because those public class layouts change; the [Kernel migration table](../reference/kernel.md#typed-kernel-construction) records unchanged interface keys, update selection and registration order. Consul follows the same construction pattern while retaining all three public headers and types. Its private factory is not installed; the module keeps its interface RTTI key and disabled updates. Rebuild Consul and consumers together because its public implementation/plugin layouts change. See [Consul construction migration](../reference/directory-structure.md#typed-consul-construction) for test entry points and dependency conditions. + +Etcd Client also retains all five public headers and types while using typed bases, a private interface-returning factory and explicit entry/exit functions. Its interface RTTI key and disabled updates are unchanged. Rebuild the plugin and consumers together because the public implementation/plugin layouts change. The [Etcd Client construction migration](../reference/directory-structure.md#typed-etcd-client-construction) describes optional JSON dependencies and tests without an Etcd service. diff --git a/docs/guides/plugins_CN.md b/docs/guides/plugins_CN.md index b66b700d4..ef5d6defb 100644 --- a/docs/guides/plugins_CN.md +++ b/docs/guides/plugins_CN.md @@ -200,3 +200,5 @@ Net 通过私有工厂返回 `std::unique_ptr`,并启用 Kernel 同样使用类型化构造,并保留公开 `*_impl`、`kernel_plugin` 类型与头路径;私有工厂头不安装。公开类布局发生变化,Kernel 与消费者需要一起重建。[Kernel 迁移表](../reference/kernel_CN.md#kernel-类型化构造)记录保持不变的接口键、update 选择与注册顺序。 Consul 使用相同构造方式,保留全部三个公开头与类型。私有工厂不安装;模块保留接口 RTTI 键和禁用 update 的选择。公开实现/插件布局发生变化,Consul 与消费者必须一起重建。测试入口与依赖条件见 [Consul 构造迁移](../reference/directory-structure_CN.md#consul-类型化构造)。 + +Etcd Client 使用类型化基类、返回接口的私有工厂与显式入口/出口,保留全部五个公开头与类型。接口 RTTI 键和禁用 update 的选择不变。公开实现/插件布局发生变化,插件与消费者必须一起重建。可选 JSON 依赖与无需 Etcd 服务的测试见 [Etcd Client 构造迁移](../reference/directory-structure_CN.md#etcd-client-类型化构造)。 diff --git a/docs/reference/build-options.md b/docs/reference/build-options.md index f6be18c32..1825ad741 100644 --- a/docs/reference/build-options.md +++ b/docs/reference/build-options.md @@ -39,9 +39,9 @@ cmake -S . -B build -DARK_BUILD_APPLICATIONS=ON \ cmake --build build --target ark_mysql_plugin ark_redis_plugin ``` -All official plugin public headers are copied from explicit lists at configuration time into each build tree's generated plugin header mapping. Each `ark::` API target carries that include root even with runtime plugins disabled. MySQL, Redis, Utility, Net, Bus and HTTP exclude private assembly and factories. Kernel retains all 38 existing headers, including `kernel_plugin.hpp`; Consul, Etcd Client, Kafka, OSS SDK and Physic retain all 22 existing public headers, including implementation and assembly headers. Registration keys, public type visibility and optional plugin policies are retained; [Kernel typed construction](kernel.md#typed-kernel-construction) and [Consul typed construction](directory-structure.md#typed-consul-construction) change public class layouts and require rebuilding consumers. Their private factory headers are excluded from mapping and installation; header mapping does not validate optional driver integrations. Each public header has one co-located source; its generated include mapping belongs to the selected build tree, rather than a second source layout. +All official plugin public headers are copied from explicit lists at configuration time into each build tree's generated plugin header mapping. Each `ark::` API target carries that include root even with runtime plugins disabled. MySQL, Redis, Utility, Net, Bus and HTTP exclude private assembly and factories. Kernel retains all 38 existing headers, including `kernel_plugin.hpp`; Consul, Etcd Client, Kafka, OSS SDK and Physic retain all 22 existing public headers, including implementation and assembly headers. Registration keys, public type visibility and optional plugin policies are retained; [Kernel typed construction](kernel.md#typed-kernel-construction), [Consul typed construction](directory-structure.md#typed-consul-construction) and [Etcd Client typed construction](directory-structure.md#typed-etcd-client-construction) change public class layouts and require rebuilding consumers. Their private factory headers are excluded from mapping and installation; header mapping does not validate optional driver integrations. Each public header has one co-located source; its generated include mapping belongs to the selected build tree, rather than a second source layout. -Applications CI enables the optional Consul plugin on Linux, macOS and Windows. Its `check-core` run includes construction, DLL lifecycle and production export checks; these checks require no Consul service and do not establish service interoperability. +Applications CI explicitly enables Consul and Etcd Client on Linux, macOS and Windows. Its `check-core` run includes their construction, DLL lifecycle and production export checks; these checks require no Consul or Etcd service and do not establish service interoperability. Etcd Client keeps its existing default `ON` selection and can be disabled with `ARK_BUILD_PLUGIN_ETCD_CLIENT=OFF`. Building it requires the existing `nlohmann_json` package (`etcd-client` vcpkg feature); disabling it does not add that JSON dependency to core tests. Link a consumer to `ark::mysql` and/or `ark::redis` for the public headers and usage requirements. These public interface targets do not load a runtime library. Deploy the selected libraries with the launcher and add their stems to the application's manifest: diff --git a/docs/reference/build-options_CN.md b/docs/reference/build-options_CN.md index b57abfb08..d9d1cd082 100644 --- a/docs/reference/build-options_CN.md +++ b/docs/reference/build-options_CN.md @@ -39,9 +39,9 @@ cmake -S . -B build -DARK_BUILD_APPLICATIONS=ON \ cmake --build build --target ark_mysql_plugin ark_redis_plugin ``` -全部官方插件的公开头通过显式清单在配置阶段复制到各构建树生成的插件公开头映射。各 `ark::` API target 传递该目录,插件关闭时仍可使用。MySQL、Redis、Utility、Net、Bus、HTTP 的私有装配头和工厂不进入映射;Kernel 保留包括 `kernel_plugin.hpp` 在内的原有 38 个头;Consul、Etcd Client、Kafka、OSS SDK、Physic 保留包括实现头与装配头在内的原有 22 个公开头。注册键、公开类型可见性和可选插件策略继续保留;[Kernel 类型化构造](kernel_CN.md#kernel-类型化构造)与 [Consul 类型化构造](directory-structure_CN.md#consul-类型化构造)改变公开类布局,需要重建消费者。两者的私有工厂头均不映射、不安装;头文件映射不代表可选驱动集成已经验证。每个公共头仅有一份共置源码;生成的头文件映射属于选定构建树,不构成第二棵源码目录。 +全部官方插件的公开头通过显式清单在配置阶段复制到各构建树生成的插件公开头映射。各 `ark::` API target 传递该目录,插件关闭时仍可使用。MySQL、Redis、Utility、Net、Bus、HTTP 的私有装配头和工厂不进入映射;Kernel 保留包括 `kernel_plugin.hpp` 在内的原有 38 个头;Consul、Etcd Client、Kafka、OSS SDK、Physic 保留包括实现头与装配头在内的原有 22 个公开头。注册键、公开类型可见性和可选插件策略继续保留;[Kernel 类型化构造](kernel_CN.md#kernel-类型化构造)、[Consul 类型化构造](directory-structure_CN.md#consul-类型化构造)与 [Etcd Client 类型化构造](directory-structure_CN.md#etcd-client-类型化构造)改变公开类布局,需要重建消费者。这些私有工厂头均不映射、不安装;头文件映射不代表可选驱动集成已经验证。每个公共头仅有一份共置源码;生成的头文件映射属于选定构建树,不构成第二棵源码目录。 -应用 CI 在 Linux、macOS、Windows 启用可选 Consul 插件,`check-core` 包含构造、动态库生命周期与生产库导出检查。这些检查不需要 Consul 服务,也不代表服务互通验收。 +应用 CI 在 Linux、macOS、Windows 显式启用 Consul 与 Etcd Client,`check-core` 包含两者的构造、动态库生命周期与生产库导出检查。这些检查不需要 Consul 或 Etcd 服务,也不代表服务互通验收。Etcd Client 保留现有默认 `ON` 选择,可通过 `ARK_BUILD_PLUGIN_ETCD_CLIENT=OFF` 关闭。构建它需要现有 `nlohmann_json` 软件包(vcpkg 的 `etcd-client` feature);关闭它不会向核心测试增加该 JSON 依赖。 业务插件链接 `ark::mysql` 和/或 `ark::redis`,以获取公开头文件和编译要求;公共接口目标不会加载运行时动态库。将所需动态库与启动器一起部署,并在应用清单中填写库名称: diff --git a/docs/reference/directory-structure.md b/docs/reference/directory-structure.md index dd56dd187..56ac10a5c 100644 --- a/docs/reference/directory-structure.md +++ b/docs/reference/directory-structure.md @@ -180,3 +180,19 @@ cmake --build build --target check-consul-construction --parallel 2 ``` `ARK_BUILD_PLUGIN_CONSUL=ON` also requires `ARK_BUILD_APPLICATIONS=ON` and remains off by default. It links the generated protocol target `ark::proto` directly. With the core tests enabled, `check-consul-plugin` additionally inspects the actual Consul DSO exports in a separate process; `check-core` includes the applicable Consul checks. The controlled factory does not exercise the production HTTP client or a Consul server. These construction checks do not establish live Consul interoperability, protocol acceptance or arbitrary hot-update safety. + +## Typed Etcd Client construction + +**Breaking ABI/layout change:** `etcd_client_module_impl` now inherits `module_base` and `etcd_client_plugin` inherits `plugin_base`. Rebuild Etcd Client and consumers together. All five public headers, their `` paths and public types remain available through `ark::etcd_client`; the `etcd_client_module` and `etcd_client` interfaces are unchanged. C++ plugin boundaries do not promise compiler-independent ABI compatibility. + +The private `etcd_client_factory.hpp` returns `std::unique_ptr` and is excluded from public mappings and installation. Explicit `DllEntryPlugin` / `DllExitPlugin` retain the existing platform exports and logger setup. Registration uses the original interface RTTI key and `module_update::disabled`; unregistration destroys the owned module. HTTP-client lookup, endpoint handling, timeout argument order, lease requests and callbacks are unchanged. The existing `keepalive` stub remains incomplete; this construction change adds no protocol or Registry capability. + +Etcd Client remains selected by default for application builds, with `ARK_BUILD_PLUGIN_ETCD_CLIENT=OFF` available to disable it. Its existing `nlohmann_json` package is required only when building the plugin (`etcd-client` vcpkg feature). With `ARK_BUILD_APPLICATIONS=ON`, `ARK_BUILD_PLUGIN_ETCD_CLIENT=ON`, `ARK_BUILD_PROTO=ON` and `ARK_BUILD_CORE_TESTS=ON`, run: + +```sh +cmake --build build --target check-etcd-client-plugin --parallel 1 +``` + +This target includes `check-etcd-client-construction`: actual factory/assembly doctest cases, plus a controlled-factory DSO using the production entry/exit source to check configuration handoff, interface metadata, disabled updates, exactly-once ownership and repeated stop. A separate process loads the actual production plugin, checks its entry/exit exports and unloads it. `check-core` includes both checks when the runtime plugin is selected; disabled-plugin core builds do not acquire the JSON dependency. Applications CI explicitly enables the plugin and runs these checks through its existing `check-core` job. + +These tests do not run an Etcd service or exercise production HTTP/Etcd interoperability. They establish construction and DSO boundaries, not a completed Registry or arbitrary hot-update safety. diff --git a/docs/reference/directory-structure_CN.md b/docs/reference/directory-structure_CN.md index 5a6f80e73..25d79a69f 100644 --- a/docs/reference/directory-structure_CN.md +++ b/docs/reference/directory-structure_CN.md @@ -170,3 +170,19 @@ cmake --build build --target check-consul-construction --parallel 2 ``` `ARK_BUILD_PLUGIN_CONSUL=ON` 还要求 `ARK_BUILD_APPLICATIONS=ON`,默认保持关闭。插件直接链接生成协议目标 `ark::proto`。开启核心测试后,`check-consul-plugin` 还在独立进程中检查真实 Consul DSO 的导出符号;`check-core` 包含当前条件下可用的 Consul 检查。受控工厂不执行生产 HTTP 客户端或访问 Consul 服务。这些构造检查不代表真实 Consul 互通、协议验收或任意热更新安全。 + +## Etcd Client 类型化构造 + +**ABI/布局不兼容变更:** `etcd_client_module_impl` 现在继承 `module_base`,`etcd_client_plugin` 继承 `plugin_base`。Etcd Client 与消费者必须一起重建。全部五个公开头、`` 路径和公开类型继续通过 `ark::etcd_client` 提供,`etcd_client_module` 与 `etcd_client` 接口不变。C++ 插件边界不承诺跨编译器的 ABI 兼容性。 + +私有 `etcd_client_factory.hpp` 返回 `std::unique_ptr`,不映射、不安装。显式 `DllEntryPlugin`/`DllExitPlugin` 保留原有平台导出和日志初始化。注册沿用原接口 RTTI 键,显式使用 `module_update::disabled`;注销时销毁拥有的模块。HTTP 客户端查找、端点处理、超时参数顺序、租约请求与回调保持不变。原有 `keepalive` 空实现仍未完成,本次构造调整不增加协议或 Registry 能力。 + +Etcd Client 保留应用构建默认选中的策略,可通过 `ARK_BUILD_PLUGIN_ETCD_CLIENT=OFF` 关闭。仅构建该插件时需要现有 `nlohmann_json` 软件包(vcpkg 的 `etcd-client` feature)。开启 `ARK_BUILD_APPLICATIONS=ON`、`ARK_BUILD_PLUGIN_ETCD_CLIENT=ON`、`ARK_BUILD_PROTO=ON` 与 `ARK_BUILD_CORE_TESTS=ON` 后,运行: + +```sh +cmake --build build --target check-etcd-client-plugin --parallel 1 +``` + +该目标包含 `check-etcd-client-construction`:真实工厂/装配的 doctest 用例,以及使用生产入口/出口源码与受控工厂的 DSO 测试,覆盖配置传递、接口元数据、禁用 update、所有权恰好销毁一次和重复 stop。另一个独立进程加载真实生产插件,检查入口/出口导出后卸载。选中运行时插件时,`check-core` 包含这两项检查;关闭插件的核心构建不增加 JSON 依赖。应用 CI 显式启用该插件,通过现有 `check-core` 任务执行检查。 + +这些测试不启动 Etcd 服务,不执行生产 HTTP/Etcd 互通验收,只验证构造与 DSO 边界,不代表 Registry 已完成,也不证明任意热更新安全。 diff --git a/src/plugins/etcd_client/etcd_client_factory.hpp b/src/plugins/etcd_client/etcd_client_factory.hpp new file mode 100644 index 000000000..e521550f8 --- /dev/null +++ b/src/plugins/etcd_client/etcd_client_factory.hpp @@ -0,0 +1,13 @@ +// Copyright (c) 2026 OpenArkStudio. Licensed under Apache-2.0. See LICENSE. +#pragma once + +#include + +#include "ark/etcd_client/etcd_client_module.hpp" + +// Plugin-private construction; deliberately absent from public header mappings. +namespace ark::detail { + +std::unique_ptr make_etcd_client_module(); + +} // namespace ark::detail diff --git a/src/plugins/etcd_client/etcd_client_module_impl.cpp b/src/plugins/etcd_client/etcd_client_module_impl.cpp index 7c5007fb1..4edfc4b98 100644 --- a/src/plugins/etcd_client/etcd_client_module_impl.cpp +++ b/src/plugins/etcd_client/etcd_client_module_impl.cpp @@ -1,8 +1,13 @@ #include "ark/etcd_client/etcd_client_impl.hpp" #include "ark/etcd_client/etcd_client_module_impl.hpp" +#include "etcd_client_factory.hpp" namespace ark { +std::unique_ptr detail::make_etcd_client_module() { + return std::make_unique(); +} + bool etcd_client_module_impl::init() { http_client_module_ = find_module(); return http_client_module_ != nullptr; diff --git a/src/plugins/etcd_client/etcd_client_module_impl.hpp b/src/plugins/etcd_client/etcd_client_module_impl.hpp index 88e659ece..8296b0aed 100644 --- a/src/plugins/etcd_client/etcd_client_module_impl.hpp +++ b/src/plugins/etcd_client/etcd_client_module_impl.hpp @@ -1,19 +1,17 @@ #pragma once #include "ark/etcd_client/etcd_client_module.hpp" +#include "ark/module_base.hpp" #include "ark/net/http_client_module.hpp" -#include "ark/plugin_manager.hpp" namespace ark { -class etcd_client_module_impl final : public etcd_client_module +class etcd_client_module_impl final : public module_base { - ARK_DECLARE_MODULE_FUNCTIONS - public: - virtual bool init() override; + bool init() override; - virtual std::shared_ptr create_etcd_client( + std::shared_ptr create_etcd_client( std::vector endpoints, int io_timeout = 5, int connect_timeout = 2) override; private: diff --git a/src/plugins/etcd_client/etcd_client_plugin.cpp b/src/plugins/etcd_client/etcd_client_plugin.cpp index a9f2edf23..ac6ccaef3 100644 --- a/src/plugins/etcd_client/etcd_client_plugin.cpp +++ b/src/plugins/etcd_client/etcd_client_plugin.cpp @@ -1,16 +1,31 @@ -#include "ark/etcd_client/etcd_client_module_impl.hpp" +#include + #include "ark/etcd_client/etcd_client_plugin.hpp" +#include "etcd_client_factory.hpp" namespace ark { -ARK_DECLARE_PLUGIN_DLL_FUNCTION(etcd_client_plugin) +ARK_EXPORT_FUNC void DllEntryPlugin(plugin_manager* manager, const std::string& name, logger* log) { + logger::init(log); + manager->register_plugin(name); +} + +ARK_EXPORT_FUNC void DllExitPlugin(plugin_manager* manager) { + manager->unregister_plugin(); +} void etcd_client_plugin::install() { - ARK_REGISTER_MODULE(etcd_client_module, etcd_client_module_impl); + if (!register_module(detail::make_etcd_client_module(), module_update::disabled)) + { + throw std::runtime_error("Etcd Client module registration rejected"); + } } void etcd_client_plugin::uninstall() { - ARK_UNREGISTER_MODULE(etcd_client_module, etcd_client_module_impl); + if (!unregister_module()) + { + throw std::runtime_error("Etcd Client module unregistration rejected"); + } } } // namespace ark diff --git a/src/plugins/etcd_client/etcd_client_plugin.hpp b/src/plugins/etcd_client/etcd_client_plugin.hpp index a199b52fb..684317dbb 100755 --- a/src/plugins/etcd_client/etcd_client_plugin.hpp +++ b/src/plugins/etcd_client/etcd_client_plugin.hpp @@ -1,8 +1,14 @@ -#include "ark/plugin.hpp" -#include "ark/plugin_manager.hpp" +#pragma once + +#include "ark/plugin_base.hpp" namespace ark { -ARK_DECLARE_PLUGIN(etcd_client_plugin) +class etcd_client_plugin final : public plugin_base +{ +public: + void install() override; + void uninstall() override; +}; } // namespace ark diff --git a/tests/ci/install-package.test.mjs b/tests/ci/install-package.test.mjs index 0b5daeec1..43eb8591d 100644 --- a/tests/ci/install-package.test.mjs +++ b/tests/ci/install-package.test.mjs @@ -72,10 +72,10 @@ include(cmake/ark_install.cmake) const text = readFileSync(file, 'utf8'); for (const forbidden of [root, producer, build, prefix]) assert.ok(!text.includes(forbidden), `${file}: ${forbidden}`); } - for (const name of ['mysql/mysql_module.hpp', 'kernel/kernel_plugin.hpp', 'physic/physic_module_impl.hpp', 'application_manifest.hpp']) { + for (const name of ['mysql/mysql_module.hpp', 'kernel/kernel_plugin.hpp', 'etcd_client/etcd_client_plugin.hpp', 'etcd_client/etcd_client_module_impl.hpp', 'physic/physic_module_impl.hpp', 'application_manifest.hpp']) { assert.ok(existsSync(join(relocated, 'include/ark', name)), name); } - for (const name of ['mysql/mysql_plugin.hpp', 'bus/bus_config_parser.hpp', 'http/http_plugin.hpp', 'net/http_client_module_impl.hpp', 'net/http_client_factory.hpp', 'kernel/kernel_factory.hpp', 'configuration_reader.hpp']) { + for (const name of ['mysql/mysql_plugin.hpp', 'bus/bus_config_parser.hpp', 'http/http_plugin.hpp', 'net/http_client_module_impl.hpp', 'net/http_client_factory.hpp', 'kernel/kernel_factory.hpp', 'etcd_client/etcd_client_factory.hpp', 'configuration_reader.hpp']) { assert.equal(existsSync(join(relocated, 'include/ark', name)), false, name); } rmSync(producer, { recursive: true }); diff --git a/tests/ci/package-dependencies.test.mjs b/tests/ci/package-dependencies.test.mjs index 9fee41e8e..840a16bc9 100644 --- a/tests/ci/package-dependencies.test.mjs +++ b/tests/ci/package-dependencies.test.mjs @@ -87,3 +87,20 @@ test('Applications CI enables Consul construction and production export checks w assert.match(checks, /add_dependencies\(check-core check-consul-plugin\)/); assert.match(checks, /add_dependencies\(check-core check-consul-construction\)/); }); + +test('Applications CI explicitly enables Etcd Client ownership and production DSO checks', () => { + const applications = read('.github/workflows/ci.yml').split(' applications:\n')[1].split('\n database:')[0]; + assert.match(applications, /-DARK_BUILD_PLUGIN_ETCD_CLIENT=ON/); + assert.match(applications, /run: cmake --build ci-build --target check-core/); + const core = read('tests/core/CMakeLists.txt'); + assert.match(core, /if\(TARGET ark_etcd_client_plugin\)[\s\S]*add_subdirectory\(\.\.\/etcd_client/); + const checks = read('tests/etcd_client/CMakeLists.txt'); + assert.match(checks, /add_dependencies\(check-core check-etcd-client-plugin check-etcd-client-construction\)/); + assert.match(checks, /etcd_client_impl\.cpp/); + const mappings = read('src/plugins/CMakeLists.txt').split('set(ark_etcd_client_public_headers')[1].split('\n)')[0]; + assert.doesNotMatch(mappings, /etcd_client_factory/); + const plugin = read('src/plugins/etcd_client/etcd_client_plugin.cpp'); + assert.match(plugin, /DllEntryPlugin/); + assert.match(plugin, /DllExitPlugin/); + assert.doesNotMatch(plugin, /ARK_DECLARE_PLUGIN|ARK_REGISTER_MODULE|ARK_UNREGISTER_MODULE/); +}); diff --git a/tests/consumer/etcd_client/main.cpp b/tests/consumer/etcd_client/main.cpp index ea35b4e3e..8b4071e70 100644 --- a/tests/consumer/etcd_client/main.cpp +++ b/tests/consumer/etcd_client/main.cpp @@ -8,8 +8,12 @@ #include static_assert(std::is_base_of_v); -static_assert(std::is_base_of_v); -static_assert(std::is_base_of_v); +static_assert(std::is_base_of_v, ark::etcd_client_module_impl>); +static_assert(std::is_base_of_v, ark::etcd_client_plugin>); + +#if __has_include() +#error "Etcd Client private factory must not be mapped or installed" +#endif int main() { return 0; diff --git a/tests/core/CMakeLists.txt b/tests/core/CMakeLists.txt index 0d324c919..927446e32 100644 --- a/tests/core/CMakeLists.txt +++ b/tests/core/CMakeLists.txt @@ -228,6 +228,11 @@ if(ARK_BUILD_PROTO AND ARK_BUILD_CONFIG) add_subdirectory(../consul "${CMAKE_CURRENT_BINARY_DIR}/consul") endif() +# Keep JSON optional: construction checks follow the selected runtime plugin. +if(TARGET ark_etcd_client_plugin) + add_subdirectory(../etcd_client "${CMAKE_CURRENT_BINARY_DIR}/etcd_client") +endif() + if(ARK_BUILD_PROTO AND TARGET ark_kernel_plugin AND TARGET ark_utility_plugin) add_executable(ark_kernel_plugin_tests ../app/kernel_plugin.cpp) set_property(TARGET ark_kernel_plugin_tests PROPERTY LINK_LIBRARIES ark::kernel) diff --git a/tests/etcd_client/CMakeLists.txt b/tests/etcd_client/CMakeLists.txt new file mode 100644 index 000000000..66e391ace --- /dev/null +++ b/tests/etcd_client/CMakeLists.txt @@ -0,0 +1,35 @@ +# Copyright (c) 2026 OpenArkStudio. Licensed under Apache-2.0. See LICENSE. +find_package(nlohmann_json CONFIG REQUIRED) +set(etcd_client_source "${PROJECT_SOURCE_DIR}/src/plugins/etcd_client") + +# Compile real assembly/factory locally too: private C++ symbols are not DLL exports. +add_executable(ark_etcd_client_construction_tests ../core/main.cpp construction.cpp + "${etcd_client_source}/etcd_client_plugin.cpp" "${etcd_client_source}/etcd_client_module_impl.cpp" "${etcd_client_source}/etcd_client_impl.cpp") +set_property(TARGET ark_etcd_client_construction_tests PROPERTY LINK_LIBRARIES + ark::etcd_client nlohmann_json::nlohmann_json) +target_include_directories(ark_etcd_client_construction_tests PRIVATE "${etcd_client_source}") + +# A controlled module exercises the actual entry/exit functions without HTTP or Etcd Client services. +add_library(ark_etcd_client_entry_probe SHARED entry_factory.cpp "${etcd_client_source}/etcd_client_plugin.cpp") +set_property(TARGET ark_etcd_client_entry_probe PROPERTY LINK_LIBRARIES ark::etcd_client) +target_include_directories(ark_etcd_client_entry_probe PRIVATE "${etcd_client_source}") +set_target_properties(ark_etcd_client_entry_probe PROPERTIES PREFIX "") +add_executable(ark_etcd_client_entry_tests entry.cpp) +set_property(TARGET ark_etcd_client_entry_tests PROPERTY LINK_LIBRARIES ark::etcd_client) + +add_custom_target(check-etcd-client-construction + COMMAND $ + COMMAND $ "$/" "ark_etcd_client_entry_probe" + DEPENDS ark_etcd_client_construction_tests ark_etcd_client_entry_tests ark_etcd_client_entry_probe + USES_TERMINAL VERBATIM) + +if(TARGET ark_etcd_client_plugin) + # Keep real DSO export inspection separate from the host linking static AFProto. + add_executable(ark_etcd_client_exports_tests exports.cpp) + set_property(TARGET ark_etcd_client_exports_tests PROPERTY LINK_LIBRARIES ark::core) + add_custom_target(check-etcd-client-plugin + COMMAND $ "$/" "ark_etcd_client_plugin" + DEPENDS ark_etcd_client_exports_tests ark_etcd_client_plugin check-etcd-client-construction + USES_TERMINAL VERBATIM) + add_dependencies(check-core check-etcd-client-plugin check-etcd-client-construction) +endif() diff --git a/tests/etcd_client/construction.cpp b/tests/etcd_client/construction.cpp new file mode 100644 index 000000000..5d600fca3 --- /dev/null +++ b/tests/etcd_client/construction.cpp @@ -0,0 +1,56 @@ +// Copyright (c) 2026 OpenArkStudio. Licensed under Apache-2.0. See LICENSE. +#include +#include +#include + +#include "../network/doctest.h" + +#include +#include +#include "etcd_client_factory.hpp" + +#if __has_include() +#error "Etcd Client public mappings must exclude the private factory" +#endif + +static_assert(std::is_base_of_v, ark::etcd_client_module_impl>); +static_assert(std::is_base_of_v, ark::etcd_client_plugin>); +static_assert(std::is_same_v); +static_assert( + std::is_same_v>); + +TEST_CASE("Etcd Client real factory retains its public implementation and unpublished metadata") { + auto candidate = ark::detail::make_etcd_client_module(); + REQUIRE(candidate != nullptr); + const auto* instance = candidate.get(); + CHECK(typeid(*instance) == typeid(ark::etcd_client_module_impl)); + CHECK(candidate->get_plugin_manager() == nullptr); + CHECK(candidate->get_name().empty()); +} + +TEST_CASE("Etcd Client assembly retains the interface key, disabled updates and explicit ownership") { + ark::plugin_manager manager; + ark::etcd_client_plugin owner; + owner.set_plugin_manager(&manager); + CHECK(owner.get_plugin_version() == 0); + CHECK(owner.get_plugin_name() == typeid(ark::etcd_client_plugin).name()); + owner.set_plugin_conf("etcd-client-construction.toml"); + CHECK(ark::etcd_client_plugin::get_plugin_conf() == "etcd-client-construction.toml"); + owner.install(); + auto* instance = manager.find_module(); + REQUIRE(instance != nullptr); + CHECK(typeid(*instance) == typeid(ark::etcd_client_module_impl)); + CHECK(instance->get_plugin_manager() == &manager); + CHECK(instance->get_name() == typeid(ark::etcd_client_module).name()); + // Insertion succeeds only when assembly left the module out of the update set. + CHECK(manager.add_update_module(instance)); + manager.remove_update_module(instance->get_name()); + CHECK_THROWS_AS(owner.install(), std::runtime_error); + CHECK(manager.find_module() == instance); + owner.uninstall(); + // Reinstallation proves the public registration key and ownership were released. + CHECK_NOTHROW(owner.install()); + CHECK_NOTHROW(owner.uninstall()); + CHECK(manager.update(1)); + CHECK_THROWS_AS(owner.uninstall(), std::runtime_error); +} diff --git a/tests/etcd_client/entry.cpp b/tests/etcd_client/entry.cpp new file mode 100644 index 000000000..7420feeca --- /dev/null +++ b/tests/etcd_client/entry.cpp @@ -0,0 +1,45 @@ +// Copyright (c) 2026 OpenArkStudio. Licensed under Apache-2.0. See LICENSE. +#include +#include +#include + +#include +#include +#include + +namespace { + +void require(bool condition, const char* message) { + if (!condition) + { + std::cerr << message << std::endl; + std::_Exit(1); + } +} + +} // namespace + +int main(int argc, char* argv[]) { + require(argc == 3, "Expected Etcd Client entry-probe directory and library stem"); + ark::logger log; + ark::logger::init(&log); + log.set_log_func([](spdlog::level::level_enum, const std::string&) {}); + ark::dynamic_library library(argv[2]); + require(library.load(argv[1]), "Cannot load Etcd Client entry-probe library"); + auto events = reinterpret_cast(library.get_symbol("etcd_client_entry_events")); + require(events && events(0) == 0 && events(1) == 0, "Entry-probe state is not fresh"); + require(library.get_symbol("DllEntryPlugin") && library.get_symbol("DllExitPlugin"), + "Etcd Client explicit entry/exit exports are missing"); + ark::plugin_manager manager; + require(manager.start(0, argv[1], "conf/", {{argv[2], "entry-probe.toml"}}), "Etcd Client explicit entry failed"); + auto* instance = manager.find_module(); + require(instance && instance->get_plugin_manager() == &manager && + instance->get_name() == typeid(ark::etcd_client_module).name(), + "Etcd Client explicit entry lost interface registration metadata"); + require(events(0) == 1 && events(1) == 0, "Etcd Client entry did not transfer factory ownership"); + require(manager.update(1) && events(2) == 0, "Etcd Client entry changed disabled update selection"); + require(manager.stop(2) && events(1) == 1, "Etcd Client explicit exit did not destroy its module exactly once"); + require(manager.stop(3) && events(1) == 1, "Repeated stop invoked Etcd Client exit twice"); + require(library.unload(), "Cannot release Etcd Client observation handle"); + std::cout << "Etcd Client real assembly entry/exit, configuration, interface metadata and ownership passed\n"; +} diff --git a/tests/etcd_client/entry_factory.cpp b/tests/etcd_client/entry_factory.cpp new file mode 100644 index 000000000..ab69b24a0 --- /dev/null +++ b/tests/etcd_client/entry_factory.cpp @@ -0,0 +1,59 @@ +// Copyright (c) 2026 OpenArkStudio. Licensed under Apache-2.0. See LICENSE. +#include + +#include +#include +#include "etcd_client_factory.hpp" + +namespace { + +unsigned created{}; +unsigned destroyed{}; +unsigned updated{}; + +class entry_etcd_client final : public ark::module_base +{ +public: + entry_etcd_client() { + ++created; + } + + ~entry_etcd_client() override { + ++destroyed; + } + + bool init() override { + return ark::etcd_client_plugin::get_plugin_conf() == "conf/entry-probe.toml"; + } + + bool update() override { + ++updated; + return true; + } + + std::shared_ptr create_etcd_client(std::vector, int, int) override { + return {}; + } +}; + +} // namespace + +namespace ark::detail { + +std::unique_ptr make_etcd_client_module() { + return std::make_unique(); +} + +} // namespace ark::detail + +ARK_EXPORT_FUNC unsigned etcd_client_entry_events(unsigned event) { + switch (event) + { + case 0: + return created; + case 1: + return destroyed; + default: + return updated; + } +} diff --git a/tests/etcd_client/exports.cpp b/tests/etcd_client/exports.cpp new file mode 100644 index 000000000..f6ef4cb20 --- /dev/null +++ b/tests/etcd_client/exports.cpp @@ -0,0 +1,31 @@ +// Copyright (c) 2026 OpenArkStudio. Licensed under Apache-2.0. See LICENSE. +#include +#include +#include + +#include + +namespace { + +void require(bool condition, const char* message) { + if (!condition) + { + std::cerr << message << std::endl; + std::_Exit(1); + } +} + +} // namespace + +int main(int argc, char* argv[]) { + require(argc == 3, "Expected plugin directory and library stem"); + ark::logger log; + ark::logger::init(&log); + log.set_log_func([](spdlog::level::level_enum, const std::string&) {}); + ark::dynamic_library library(argv[2]); + require(library.load(argv[1]), "Cannot load actual Etcd Client shared library"); + require(library.get_symbol("DllEntryPlugin") != nullptr, "Missing explicit DllEntryPlugin export"); + require(library.get_symbol("DllExitPlugin") != nullptr, "Missing explicit DllExitPlugin export"); + require(library.unload(), "Cannot unload Etcd Client shared library"); + std::cout << argv[2] << ": actual DSO load, explicit entry/exit exports and unload passed\n"; +}