From 47f15301ae6838fa131229c9e6e1acfc259e1199 Mon Sep 17 00:00:00 2001 From: smanes0213 Date: Tue, 17 Mar 2026 16:22:48 +0530 Subject: [PATCH 01/13] [Tests] Add thunder_test_support static library for plugin integration testing --- Tests/CMakeLists.txt | 5 + Tests/test_support/CMakeLists.txt | 122 ++++++ Tests/test_support/Module.cpp | 29 ++ Tests/test_support/ThunderTestRuntime.cpp | 247 ++++++++++++ Tests/test_support/ThunderTestRuntime.h | 135 +++++++ docs/ThunderTestSupport/ThunderTestSupport.md | 378 ++++++++++++++++++ 6 files changed, 916 insertions(+) create mode 100644 Tests/test_support/CMakeLists.txt create mode 100644 Tests/test_support/Module.cpp create mode 100644 Tests/test_support/ThunderTestRuntime.cpp create mode 100644 Tests/test_support/ThunderTestRuntime.h create mode 100644 docs/ThunderTestSupport/ThunderTestSupport.md diff --git a/Tests/CMakeLists.txt b/Tests/CMakeLists.txt index 79a328ea9c..15fac32899 100644 --- a/Tests/CMakeLists.txt +++ b/Tests/CMakeLists.txt @@ -5,6 +5,7 @@ option(FILE_UNLINK_TEST "File unlink test" OFF) option(REDIRECT_TEST "Test stream redirection" OFF) option(MESSAGEBUFFER_TEST "Test message buffer" OFF) option(UNRAVELLER "reveal thread details" OFF) +option(ENABLE_TEST_RUNTIME "Build Thunder test support library for plugin integration tests" OFF) if(BUILD_TESTS) add_subdirectory(unit) @@ -40,4 +41,8 @@ endif() if(UNRAVELLER) add_subdirectory(unraveller) +endif() + +if(ENABLE_TEST_RUNTIME) + add_subdirectory(test_support) endif() \ No newline at end of file diff --git a/Tests/test_support/CMakeLists.txt b/Tests/test_support/CMakeLists.txt new file mode 100644 index 0000000000..365baebb86 --- /dev/null +++ b/Tests/test_support/CMakeLists.txt @@ -0,0 +1,122 @@ +# If not stated otherwise in this file or this component's license file the +# following copyright and licenses apply: +# +# Copyright 2024 RDK Management +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# ============================================================================ +# thunder_test_support - Static library for Thunder plugin integration testing +# +# This library embeds the Thunder PluginHost server (PluginServer, Controller, +# SystemInfo, etc.) into a static archive so that GTest-based test binaries +# can spin up a real Thunder runtime in-process without launching the +# standalone Thunder daemon. +# +# Usage: +# 1. Link your test executable against thunder_test_support. +# 2. Use ThunderTestRuntime::Initialize() to start the embedded server. +# 3. Call JSON-RPC or COM-RPC methods directly against loaded plugins. +# 4. Call ThunderTestRuntime::Shutdown() when done. +# +# NOTE: PluginHost.cpp is deliberately excluded — it contains main(). +# The test binary provides its own main() via GTest. +# ============================================================================ + +find_package(Threads REQUIRED) + +set(THUNDER_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../Source/Thunder") + +set(THREADPOOL_COUNT "4" CACHE STRING "The number of threads in the thread pool for test runtime") + +add_library(thunder_test_support STATIC + ThunderTestRuntime.cpp + Module.cpp + ${THUNDER_SOURCE_DIR}/PluginServer.cpp + ${THUNDER_SOURCE_DIR}/Controller.cpp + ${THUNDER_SOURCE_DIR}/SystemInfo.cpp + ${THUNDER_SOURCE_DIR}/PostMortem.cpp + ${THUNDER_SOURCE_DIR}/Probe.cpp +) + +# PluginHost.cpp is deliberately excluded — it contains main(). +# The test binary supplies its own main via GTest/GMock. + +target_compile_definitions(thunder_test_support + PRIVATE + NAMESPACE=${NAMESPACE} + APPLICATION_NAME=ThunderTestRuntime + MODULE_NAME=ThunderTestRuntime + THREADPOOL_COUNT=${THREADPOOL_COUNT} +) + +target_compile_options(thunder_test_support PRIVATE -Wno-psabi) + +target_link_libraries(thunder_test_support + PRIVATE + CompileSettings::CompileSettings +) + +if(EXCEPTION_CATCHING) + set_source_files_properties(${THUNDER_SOURCE_DIR}/PluginServer.cpp PROPERTIES COMPILE_FLAGS "-fexceptions") +endif() + +target_include_directories(thunder_test_support + PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR} + PRIVATE + ${THUNDER_SOURCE_DIR} + $ + $ +) + +target_link_libraries(thunder_test_support + PUBLIC + ${NAMESPACE}Core::${NAMESPACE}Core + ${NAMESPACE}Cryptalgo::${NAMESPACE}Cryptalgo + ${NAMESPACE}COM::${NAMESPACE}COM + ${NAMESPACE}Messaging::${NAMESPACE}Messaging + ${NAMESPACE}WebSocket::${NAMESPACE}WebSocket + ${NAMESPACE}Plugins::${NAMESPACE}Plugins + ${NAMESPACE}COMProcess::${NAMESPACE}COMProcess + Threads::Threads +) + +if(WARNING_REPORTING) + target_sources(thunder_test_support PRIVATE ${THUNDER_SOURCE_DIR}/WarningReportingCategories.cpp) +endif() + +if(PROCESSCONTAINERS) + target_link_libraries(thunder_test_support + PUBLIC + ${NAMESPACE}ProcessContainers::${NAMESPACE}ProcessContainers) + target_compile_definitions(thunder_test_support + PUBLIC + PROCESSCONTAINERS_ENABLED=1) +endif() + +if(HIBERNATESUPPORT) + target_link_libraries(thunder_test_support PUBLIC + ${NAMESPACE}Hibernate::${NAMESPACE}Hibernate) + target_compile_definitions(thunder_test_support PUBLIC + HIBERNATE_SUPPORT_ENABLED=1) +endif() + +# --- Install rules --- +install(TARGETS thunder_test_support + ARCHIVE DESTINATION lib +) + +install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/ThunderTestRuntime.h + DESTINATION include/thunder_test_support +) diff --git a/Tests/test_support/Module.cpp b/Tests/test_support/Module.cpp new file mode 100644 index 0000000000..4ea265821d --- /dev/null +++ b/Tests/test_support/Module.cpp @@ -0,0 +1,29 @@ +/* + * Copyright 2024 RDK Management + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Module definition for the thunder_test_support static library. +// MODULE_NAME is set to ThunderTestRuntime via -D in CMakeLists.txt, +// which overrides Source/Thunder/Module.h's default of "Application". +// This ensures all server sources and the MODULE_NAME_DECLARATION below +// use the same symbol, and trace output shows "ThunderTestRuntime". + +#ifndef MODULE_NAME +#define MODULE_NAME ThunderTestRuntime +#endif + +#include + +MODULE_NAME_DECLARATION(BUILD_REFERENCE) diff --git a/Tests/test_support/ThunderTestRuntime.cpp b/Tests/test_support/ThunderTestRuntime.cpp new file mode 100644 index 0000000000..40a591121b --- /dev/null +++ b/Tests/test_support/ThunderTestRuntime.cpp @@ -0,0 +1,247 @@ +/* + * Copyright 2024 RDK Management + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ThunderTestRuntime.h" + +#include +#include +#include +#include + +// ========================================================================== +// ThunderTestRuntime implementation +// +// Lifecycle: Initialize() -> [run tests] -> Shutdown() +// +// Initialize creates a unique /tmp/thunder_test_XXXXXX/ directory tree, +// writes a minimal Thunder config.json, parses it into PluginHost::Config, +// constructs PluginHost::Server, and calls Server::Open() which boots +// the controller and activates auto-start plugins. +// +// Shutdown reverses the process: Server::Close(), cleanup temp files. +// ========================================================================== + +namespace Thunder { +namespace TestCore { + + ThunderTestRuntime::~ThunderTestRuntime() + { + Shutdown(); + } + + void ThunderTestRuntime::CreateDirectories() const + { + Core::Directory(_tempDir.c_str()).Create(); + Core::Directory((_tempDir + "persistent/").c_str()).Create(); + Core::Directory((_tempDir + "volatile/").c_str()).Create(); + Core::Directory((_tempDir + "data/").c_str()).Create(); + } + + void ThunderTestRuntime::CleanupDirectories() const + { + if (!_tempDir.empty()) { + Core::Directory(_tempDir.c_str()).Destroy(); + } + } + + // Build a minimal Thunder JSON config from the plugin list. + // Uses port 0 (OS-assigned) and binds to localhost only. + string ThunderTestRuntime::BuildConfigJSON(const std::vector& plugins, + const string& systemPath, + const string& proxyStubPath) const + { + std::ostringstream json; + json << "{" + << "\"port\":0," + << "\"binding\":\"127.0.0.1\"," + << "\"idletime\":180," + << "\"persistentpath\":\"" << _tempDir << "persistent/\"," + << "\"volatilepath\":\"" << _tempDir << "volatile/\"," + << "\"datapath\":\"" << _tempDir << "data/\"," + << "\"systempath\":\"" << systemPath << "\"," + << "\"proxystubpath\":\"" << proxyStubPath << "\"," + << "\"communicator\":\"" << _tempDir << "communicator\"," + << "\"plugins\":["; + + for (size_t i = 0; i < plugins.size(); ++i) { + const auto& p = plugins[i]; + if (i > 0) json << ","; + json << "{" + << "\"callsign\":\"" << p.callsign << "\"," + << "\"locator\":\"" << p.locator << "\"," + << "\"classname\":\"" << p.classname << "\"," + << "\"startuporder\":" << p.startuporder << "," + << "\"autostart\":" << (p.autostart ? "true" : "false"); + + if (!p.configuration.empty()) { + json << ",\"configuration\":" << p.configuration; + } + + json << "}"; + } + + json << "]}"; + return json.str(); + } + + uint32_t ThunderTestRuntime::Initialize(const std::vector& plugins, + const string& systemPath, + const string& proxyStubPath) + { + if (_server != nullptr) { + return Core::ERROR_ALREADY_CONNECTED; + } + + // Create unique temp directory for this test run + char tempTemplate[] = "/tmp/thunder_test_XXXXXX"; + char* tempResult = mkdtemp(tempTemplate); + if (tempResult == nullptr) { + return Core::ERROR_GENERAL; + } + _tempDir = string(tempResult) + "/"; + + CreateDirectories(); + + // Determine system path for plugin .so files + string sysPath = systemPath; + if (sysPath.empty()) { + sysPath = "/usr/lib/wpeframework/plugins/"; + } + if (sysPath.back() != '/') { + sysPath += '/'; + } + + // Determine proxy stub path + _proxyStubPath = proxyStubPath; + if (_proxyStubPath.empty()) { + // Default: look next to system path + _proxyStubPath = sysPath + "../proxystubs/"; + } + if (_proxyStubPath.back() != '/') { + _proxyStubPath += '/'; + } + + // Build and write config to temp file + string configJSON = BuildConfigJSON(plugins, sysPath, _proxyStubPath); + _configFilePath = _tempDir + "config.json"; + + { + std::ofstream configFile(_configFilePath); + if (!configFile.is_open()) { + CleanupDirectories(); + return Core::ERROR_OPENING_FAILED; + } + configFile << configJSON; + } + + // Parse config + Core::File configFile(_configFilePath); + if (configFile.Open(true) == false) { + CleanupDirectories(); + return Core::ERROR_OPENING_FAILED; + } + + Core::OptionalType error; + _config = new PluginHost::Config(configFile, false, error); + configFile.Close(); + + if (error.IsSet()) { + delete _config; + _config = nullptr; + CleanupDirectories(); + return Core::ERROR_INCOMPLETE_CONFIG; + } + + // Create and start the server + _server = new PluginHost::Server(*_config, false); + _server->Open(); + + return Core::ERROR_NONE; + } + + // Invoke a JSON-RPC method synchronously via the in-process dispatcher. + // Bypasses HTTP/WebSocket — calls IDispatcher::Invoke() directly. + uint32_t ThunderTestRuntime::InvokeJSONRPC(const string& callsign, const string& method, + const string& params, string& response) + { + if (_server == nullptr) { + return Core::ERROR_ILLEGAL_STATE; + } + + Core::ProxyType shell; + uint32_t result = _server->Services().FromIdentifier(callsign, shell); + if (result != Core::ERROR_NONE) { + return result; + } + + PluginHost::IDispatcher* dispatcher = shell->QueryInterface(); + if (dispatcher == nullptr) { + return Core::ERROR_UNAVAILABLE; + } + + result = dispatcher->Invoke(0, 0, string(), method, params, response); + dispatcher->Release(); + + return result; + } + + Core::ProxyType ThunderTestRuntime::GetShell(const string& callsign) + { + Core::ProxyType shell; + if (_server != nullptr) { + _server->Services().FromIdentifier(callsign, shell); + } + return shell; + } + + PluginHost::Server& ThunderTestRuntime::Server() + { + ASSERT(_server != nullptr); + return *_server; + } + + string ThunderTestRuntime::CommunicatorPath() const + { + if (_config != nullptr) { + return _config->Communicator().HostName(); + } + return string(); + } + + void ThunderTestRuntime::Shutdown() + { + if (_server != nullptr) { + _server->Close(); + delete _server; + _server = nullptr; + } + + if (_config != nullptr) { + delete _config; + _config = nullptr; + } + + if (!_configFilePath.empty()) { + Core::File(_configFilePath).Destroy(); + _configFilePath.clear(); + } + + CleanupDirectories(); + _tempDir.clear(); + } + +} // namespace TestCore +} // namespace Thunder diff --git a/Tests/test_support/ThunderTestRuntime.h b/Tests/test_support/ThunderTestRuntime.h new file mode 100644 index 0000000000..9f5d1805e2 --- /dev/null +++ b/Tests/test_support/ThunderTestRuntime.h @@ -0,0 +1,135 @@ +/* + * Copyright 2024 RDK Management + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +// ========================================================================== +// ThunderTestRuntime — Public API for in-process Thunder plugin testing +// +// Provides a lightweight wrapper around PluginHost::Server that: +// - Creates an isolated temp directory per test run +// - Generates a minimal Thunder config (JSON) on the fly +// - Boots the embedded server and activates the controller +// - Exposes helpers for JSON-RPC invocation and COM-RPC queries +// - Tears everything down cleanly on Shutdown() +// +// Typical usage in a GTest fixture: +// +// static ThunderTestRuntime _runtime; +// +// static void SetUpTestSuite() { +// std::vector plugins = { ... }; +// _runtime.Initialize(plugins, pluginPath, proxyStubPath); +// } +// static void TearDownTestSuite() { _runtime.Shutdown(); } +// +// TEST_F(MyTest, JsonRpc) { +// string resp; +// _runtime.InvokeJSONRPC("Callsign", "Method", params, resp); +// } +// +// TEST_F(MyTest, ComRpc) { +// auto* iface = _runtime.GetInterface("Callsign"); +// } +// ========================================================================== + +#ifndef MODULE_NAME +#define MODULE_NAME Application +#endif + +#include +#include +#include + +namespace Thunder { + +namespace PluginHost { + class Server; + class Config; +} + +namespace TestCore { + + class ThunderTestRuntime { + public: + // Describes a plugin to be loaded by the test runtime. + // Maps directly to a Thunder plugin JSON config entry. + struct PluginConfig { + string callsign; // Plugin callsign (e.g. "Counter") + string locator; // Shared library name (e.g. "libThunderCounterImplementation.so") + string classname; // Class name registered via SERVICE_REGISTRATION + bool autostart = true; // Whether to auto-activate on startup + int startuporder = 50; // Activation priority (lower = earlier) + string configuration; // Optional JSON object for plugin-specific config + }; + + ThunderTestRuntime() = default; + ~ThunderTestRuntime(); + + ThunderTestRuntime(const ThunderTestRuntime&) = delete; + ThunderTestRuntime& operator=(const ThunderTestRuntime&) = delete; + + // Boot the embedded Thunder server with the given plugins. + // Creates temp directories, generates config, parses it, and calls Server::Open(). + // systemPath — directory containing plugin .so files + // proxyStubPath — directory containing proxy stub .so files + // Returns Core::ERROR_NONE on success. + uint32_t Initialize(const std::vector& plugins, const string& systemPath = "", const string& proxyStubPath = ""); + + // Invoke a JSON-RPC method on a loaded plugin. + // Method format: "Callsign.Version.method" (e.g. "Counter.1.increment") + uint32_t InvokeJSONRPC(const string& callsign, const string& method, + const string& params, string& response); + + // Obtain a COM-RPC interface from a loaded plugin. + // Caller must Release() the returned pointer when done. + template + INTERFACE* GetInterface(const string& callsign) + { + INTERFACE* result = nullptr; + Core::ProxyType shell = GetShell(callsign); + if (shell.IsValid()) { + result = shell->QueryInterface(); + } + return result; + } + + // Get the IShell proxy for a plugin (for activation/deactivation control). + Core::ProxyType GetShell(const string& callsign); + + // Direct access to the underlying PluginHost::Server. + PluginHost::Server& Server(); + + // Returns the UNIX domain socket path used by the communicator. + string CommunicatorPath() const; + + // Stop the server, release config, and clean up temp directories. + void Shutdown(); + + private: + string BuildConfigJSON(const std::vector& plugins, const string& systemPath, const string& proxyStubPath) const; + void CreateDirectories() const; + void CleanupDirectories() const; + + PluginHost::Config* _config = nullptr; + PluginHost::Server* _server = nullptr; + string _tempDir; + string _configFilePath; + string _proxyStubPath; + }; + +} // namespace TestCore +} // namespace Thunder diff --git a/docs/ThunderTestSupport/ThunderTestSupport.md b/docs/ThunderTestSupport/ThunderTestSupport.md new file mode 100644 index 0000000000..a863cab25a --- /dev/null +++ b/docs/ThunderTestSupport/ThunderTestSupport.md @@ -0,0 +1,378 @@ +# Thunder Test Support Library + +## Overview + +The **thunder_test_support** library enables in-process integration testing of Thunder plugins without launching the Thunder daemon. It embeds the Thunder `PluginHost::Server` into a static archive (`.a`) that test binaries link against, allowing GTest-based tests to boot a real Thunder runtime, load plugins as shared libraries, and exercise both JSON-RPC and COM-RPC interfaces — all within a single process. + +### Key Properties + +| Property | Value | +|----------|-------| +| Library type | Static archive (`libthunder_test_support.a`) | +| CMake option | `ENABLE_TEST_RUNTIME=ON` | +| Location | `Tests/test_support/` | +| Public header | `ThunderTestRuntime.h` | +| Install paths | `lib/libthunder_test_support.a`, `include/thunder_test_support/ThunderTestRuntime.h` | + +--- + +## Architecture + +### Production Thunder vs Test Support + +``` +┌─────────────────────────────────────┐ ┌─────────────────────────────────────────┐ +│ PRODUCTION DEPLOYMENT │ │ TEST DEPLOYMENT │ +│ │ │ │ +│ ┌───────────────────────────────┐ │ │ ┌─────────────────────────────────┐ │ +│ │ Thunder Daemon (binary) │ │ │ │ GTest Binary (plugin_test) │ │ +│ │ │ │ │ │ │ │ +│ │ PluginHost.cpp (main) │ │ │ │ main() from gmock_main │ │ +│ │ ↓ │ │ │ │ ↓ │ │ +│ │ PluginHost::Server │ │ │ │ ThunderTestRuntime │ │ +│ │ ├── Controller │ │ │ │ ├── PluginHost::Server │ │ +│ │ ├── PluginServer │ │ │ │ │ ├── Controller │ │ +│ │ ├── SystemInfo │ │ │ │ │ ├── PluginServer │ │ +│ │ └── HTTP/WS Listener │ │ │ │ │ ├── SystemInfo │ │ +│ │ ↓ │ │ │ │ │ └── (no HTTP listener) │ │ +│ │ Plugin .so (dynamic load) │ │ │ │ └── Plugin .so (dynamic load) │ │ +│ └───────────────────────────────┘ │ │ └─────────────────────────────────┘ │ +│ │ │ │ +│ Communication: HTTP/WS/COMRPC │ │ Communication: Direct in-process calls │ +└─────────────────────────────────────┘ └─────────────────────────────────────────┘ +``` + +### How It Works + +In production, Thunder runs as a standalone daemon (`PluginHost.cpp` → `main()`). The test support library takes a different approach: + +1. **Excludes `PluginHost.cpp`** — this file contains `main()` and daemon lifecycle logic (signal handling, daemonization, etc.). The test binary provides its own `main()` via GTest. + +2. **Statically links server internals** — `PluginServer.cpp`, `Controller.cpp`, `SystemInfo.cpp`, `PostMortem.cpp`, and `Probe.cpp` are compiled directly into the static library. + +3. **Wraps server lifecycle** — `ThunderTestRuntime` provides `Initialize()` and `Shutdown()` to manage the server, replacing the daemon's startup/shutdown sequence. + +4. **Generates config on the fly** — instead of reading `/etc/Thunder/config.json`, the runtime builds a minimal JSON config programmatically and writes it to a temporary directory. + +5. **Plugins load normally** — plugin `.so` files are still loaded dynamically via `dlopen()`, exactly as in production. The `systempath` config entry points to the directory containing plugin shared libraries. + +--- + +## Files Added to Thunder + +### Directory Structure + +``` +Tests/ +├── CMakeLists.txt ← Modified: added ENABLE_TEST_RUNTIME option +└── test_support/ + ├── CMakeLists.txt ← Build definitions for the static library + ├── Module.cpp ← MODULE_NAME definition (ThunderTestRuntime) + ├── ThunderTestRuntime.h ← Public API header + └── ThunderTestRuntime.cpp ← Implementation +``` + +### 1. `Tests/CMakeLists.txt` (Modified) + +Two additions were made to the existing file: + +```cmake +# New option added alongside existing test options +option(ENABLE_TEST_RUNTIME "Build Thunder test support library for plugin integration tests" OFF) + +# New conditional block at the end of the file +if(ENABLE_TEST_RUNTIME) + add_subdirectory(test_support) +endif() +``` + +This follows the same pattern used by other test options (`LOADER_TEST`, `WORKERPOOL_TEST`, etc.) — off by default, enabled explicitly. + +### 2. `Tests/test_support/CMakeLists.txt` (New) + +Builds the `thunder_test_support` static library. Key design decisions: + +- **Source files**: Compiles `ThunderTestRuntime.cpp`, `Module.cpp`, plus Thunder server sources directly from `Source/Thunder/` (PluginServer, Controller, SystemInfo, PostMortem, Probe). +- **Excludes `PluginHost.cpp`**: This contains `main()` and would conflict with the test binary's entry point. +- **Compile definitions**: Sets `APPLICATION_NAME=ThunderTestRuntime` and `THREADPOOL_COUNT=4`. +- **Public dependencies**: Exposes `ThunderCore`, `ThunderCOM`, `ThunderPlugins`, `ThunderMessaging`, `ThunderWebSocket`, `ThunderCOMProcess`, and `Threads` as PUBLIC link dependencies, so consumers automatically get all required libraries. +- **Private dependencies**: `CompileSettings` is linked PRIVATE to apply Thunder's compile flags without propagating them. +- **Conditional features**: Supports `WARNING_REPORTING`, `PROCESSCONTAINERS`, and `HIBERNATESUPPORT` when enabled. +- **Install rules**: Installs the `.a` archive to `lib/` and the header to `include/thunder_test_support/`. + +### 3. `Tests/test_support/Module.cpp` (New) + +Module definition required by Thunder's internal logging/tracing macros. The `MODULE_NAME` value appears in trace output to identify which component emitted a message. Using `ThunderTestRuntime` makes it easy to distinguish test runtime traces from production daemon or plugin traces: + +```cpp +#ifndef MODULE_NAME +#define MODULE_NAME ThunderTestRuntime +#endif +``` + +### 4. `Tests/test_support/ThunderTestRuntime.h` (New) + +Public API header. Defines the `Thunder::TestCore::ThunderTestRuntime` class: + +| Member | Description | +|--------|-------------| +| `PluginConfig` (struct) | Describes a plugin to load: callsign, locator (.so name), classname, autostart, startuporder, configuration JSON | +| `Initialize()` | Boots the embedded server with given plugins, system path, and proxy stub path | +| `InvokeJSONRPC()` | Calls a JSON-RPC method synchronously via in-process `IDispatcher::Invoke()` | +| `GetInterface()` | Template: obtains a COM-RPC interface from a plugin via `QueryInterface()` | +| `GetShell()` | Returns the `IShell` proxy for a plugin (for activation/deactivation control) | +| `Server()` | Direct access to the underlying `PluginHost::Server` | +| `CommunicatorPath()` | Returns the UNIX domain socket path | +| `Shutdown()` | Stops the server, releases config, cleans up temp directories | + +### 5. `Tests/test_support/ThunderTestRuntime.cpp` (New) + +Implementation with the following lifecycle: + +``` +Initialize() + ├── Create temp dir: /tmp/thunder_test_XXXXXX/ + ├── Create subdirs: persistent/, volatile/, data/ + ├── Build JSON config string from PluginConfig list + ├── Write config to temp dir + ├── Parse config into PluginHost::Config + ├── Construct PluginHost::Server + └── Call Server::Open() + ├── Set up security + ├── ServiceMap::Open() + ├── Activate Controller plugin + └── Activate auto-start plugins + +Shutdown() + ├── Server::Close() + │ ├── Deactivate all plugins + │ └── Close connections + ├── Delete Server + ├── Delete Config + ├── Remove config file + └── Remove temp directories +``` + +#### JSON-RPC Invocation Path + +`InvokeJSONRPC()` bypasses HTTP/WebSocket entirely: + +``` +InvokeJSONRPC("", ".1.", params, response) + ├── Services().FromIdentifier("") → IShell proxy + ├── shell->QueryInterface() → dispatcher + └── dispatcher->Invoke(0, 0, "", method, params, response) +``` + +This calls the plugin's JSON-RPC handler directly in-process, with zero network overhead. + +#### COM-RPC Interface Access + +`GetInterface()` provides direct COM-RPC access: + +``` +GetInterface("") + ├── GetShell("") → IShell proxy + └── shell->QueryInterface() → interface pointer +``` + +The caller receives a real COM-RPC interface pointer and must call `Release()` when done. + +--- + +## Build Configuration + +### CMake Options + +| Option | Default | Description | +|--------|---------|-------------| +| `ENABLE_TEST_RUNTIME` | `OFF` | Build the thunder_test_support static library | +| `BUILD_TESTS` | `OFF` | Build Thunder unit tests (independent of test runtime) | + +### Build Command + +```bash +cmake -S Thunder -B build/Thunder \ + -DCMAKE_INSTALL_PREFIX=/path/to/install \ + -DCMAKE_MODULE_PATH=/path/to/install/tools/cmake \ + -DENABLE_TEST_RUNTIME=ON + +cmake --build build/Thunder --target install -j$(nproc) +``` + +After installation, the library and header are available at: +``` +/lib/libthunder_test_support.a +/include/thunder_test_support/ThunderTestRuntime.h +``` + +--- + +## Usage Guide + +### Writing a Plugin Integration Test + +#### 1. CMakeLists.txt for the test + +```cmake +find_package(Thunder REQUIRED) +find_package(ThunderDefinitions REQUIRED) + +# Build the plugin as a shared library +add_library(ThunderMyPluginImpl SHARED + plugin/MyPlugin.cpp + plugin/MyPluginImpl.cpp + plugin/Module.cpp +) +target_link_libraries(ThunderMyPluginImpl PRIVATE + ${NAMESPACE}Plugins::${NAMESPACE}Plugins + ${NAMESPACE}Definitions::${NAMESPACE}Definitions +) + +# Build the test executable +add_executable(my_plugin_test MyPluginTest.cpp) +target_link_libraries(my_plugin_test PRIVATE + -Wl,--whole-archive + thunder_test_support + -Wl,--no-whole-archive + gmock_main + ${NAMESPACE}Definitions::${NAMESPACE}Definitions +) +``` + +> **Note**: `--whole-archive` is required for `thunder_test_support` to ensure Thunder's static initializers (MODULE_NAME_DECLARATION, service registrations, etc.) are linked even though the test binary doesn't reference them directly. + +#### 2. Test fixture + +```cpp +#include +#include "ThunderTestRuntime.h" +#include + +using namespace Thunder; + +class MyPluginTest : public ::testing::Test { +protected: + static TestCore::ThunderTestRuntime _runtime; + + static void SetUpTestSuite() { + std::vector plugins; + + TestCore::ThunderTestRuntime::PluginConfig cfg; + cfg.callsign = "MyPlugin"; + cfg.locator = "libThunderMyPluginImpl.so"; + cfg.classname = "MyPlugin"; + cfg.autostart = true; + cfg.startuporder = 50; + cfg.configuration = R"({"root":{"mode":"Off","locator":"libThunderMyPluginImpl.so"}})"; + plugins.push_back(cfg); + + uint32_t result = _runtime.Initialize(plugins, + "/path/to/plugins/", + "/path/to/proxystubs/"); + ASSERT_EQ(result, Core::ERROR_NONE); + } + + static void TearDownTestSuite() { + _runtime.Shutdown(); + } +}; + +TestCore::ThunderTestRuntime MyPluginTest::_runtime; + +// JSON-RPC test +TEST_F(MyPluginTest, JsonRpcCall) { + string response; + EXPECT_EQ(Core::ERROR_NONE, + _runtime.InvokeJSONRPC("MyPlugin", "MyPlugin.1.someMethod", R"({"param":1})", response)); + // Validate response... +} + +// COM-RPC test +TEST_F(MyPluginTest, ComRpcCall) { + Exchange::IMyInterface* iface = _runtime.GetInterface("MyPlugin"); + ASSERT_NE(iface, nullptr); + // Call interface methods... + iface->Release(); +} + +// Lifecycle test +TEST_F(MyPluginTest, DeactivateReactivate) { + auto shell = _runtime.GetShell("MyPlugin"); + ASSERT_TRUE(shell.IsValid()); + EXPECT_EQ(Core::ERROR_NONE, shell->Deactivate(PluginHost::IShell::reason::REQUESTED)); + EXPECT_EQ(Core::ERROR_NONE, shell->Activate(PluginHost::IShell::reason::REQUESTED)); +} +``` + +#### 3. Running + +```bash +export LD_LIBRARY_PATH=/path/to/install/lib:/path/to/install/lib/thunder/plugins:/path/to/install/lib/thunder/proxystubs +export THUNDER_PLUGIN_PATH=/path/to/install/lib/thunder/plugins/ +export THUNDER_PROXYSTUB_PATH=/path/to/install/lib/thunder/proxystubs/ + +./my_plugin_test --gtest_color=yes +``` + +--- + +## CI Integration + +The library is designed for use in GitHub Actions workflows. A typical workflow: + +```yaml +- name: Build Thunder (with test runtime) + run: > + cmake -S Thunder -B build/Thunder + -DCMAKE_INSTALL_PREFIX=${{github.workspace}}/install/usr + -DCMAKE_MODULE_PATH=${{github.workspace}}/install/tools/cmake + -DENABLE_TEST_RUNTIME=ON + && + cmake --build build/Thunder --target install -j8 + +- name: Build plugin and tests + run: > + cmake -S my-plugin/tests -B build/tests + -DCMAKE_INSTALL_PREFIX=${{github.workspace}}/install/usr + -DCMAKE_MODULE_PATH=${{github.workspace}}/install/tools/cmake + && + cmake --build build/tests --target install -j8 + +- name: Run tests + run: | + export LD_LIBRARY_PATH=${{github.workspace}}/install/usr/lib:... + my_plugin_test --gtest_output="xml:test-results.xml" +``` + +--- + +## Design Decisions + +### Why a static library? + +A static archive ensures that all Thunder server symbols are available to the test binary at link time. Shared libraries would require careful `RPATH` management and could conflict with the installed Thunder daemon libraries. + +### Why `--whole-archive`? + +Thunder uses static initializers extensively (`MODULE_NAME_DECLARATION`, `SERVICE_REGISTRATION`, singletons). Without `--whole-archive`, the linker would discard these symbols because the test binary doesn't reference them directly. The `--whole-archive` flag forces all object files from the archive to be included. + +### Why exclude `PluginHost.cpp`? + +`PluginHost.cpp` contains: +- `main()` — conflicts with GTest's `main()` +- Signal handler setup — unwanted in test context +- Daemonization logic — not applicable to tests +- Command-line argument parsing — replaced by programmatic config + +### Why generate config programmatically? + +- **Isolation**: Each test run gets a unique temp directory, preventing interference between parallel test runs. +- **Simplicity**: Tests declare plugins as C++ structs rather than maintaining JSON config files. +- **Portability**: Paths are resolved at runtime, making tests work across different build environments and CI systems. + +### Why port 0? + +Using port 0 in the config tells the OS to assign an available port, avoiding conflicts when multiple test processes run simultaneously. + +--- \ No newline at end of file From 7840e23882685862414dc83befd1c934f10fee00 Mon Sep 17 00:00:00 2001 From: smanes0213 Date: Wed, 18 Mar 2026 16:40:57 +0530 Subject: [PATCH 02/13] Remove license header --- Tests/test_support/CMakeLists.txt | 17 ----------------- Tests/test_support/Module.cpp | 16 ---------------- Tests/test_support/ThunderTestRuntime.cpp | 16 ---------------- Tests/test_support/ThunderTestRuntime.h | 16 ---------------- 4 files changed, 65 deletions(-) diff --git a/Tests/test_support/CMakeLists.txt b/Tests/test_support/CMakeLists.txt index 365baebb86..fbd67b4c1a 100644 --- a/Tests/test_support/CMakeLists.txt +++ b/Tests/test_support/CMakeLists.txt @@ -1,20 +1,3 @@ -# If not stated otherwise in this file or this component's license file the -# following copyright and licenses apply: -# -# Copyright 2024 RDK Management -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - # ============================================================================ # thunder_test_support - Static library for Thunder plugin integration testing # diff --git a/Tests/test_support/Module.cpp b/Tests/test_support/Module.cpp index 4ea265821d..774bf97ba3 100644 --- a/Tests/test_support/Module.cpp +++ b/Tests/test_support/Module.cpp @@ -1,19 +1,3 @@ -/* - * Copyright 2024 RDK Management - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - // Module definition for the thunder_test_support static library. // MODULE_NAME is set to ThunderTestRuntime via -D in CMakeLists.txt, // which overrides Source/Thunder/Module.h's default of "Application". diff --git a/Tests/test_support/ThunderTestRuntime.cpp b/Tests/test_support/ThunderTestRuntime.cpp index 40a591121b..73e355855c 100644 --- a/Tests/test_support/ThunderTestRuntime.cpp +++ b/Tests/test_support/ThunderTestRuntime.cpp @@ -1,19 +1,3 @@ -/* - * Copyright 2024 RDK Management - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - #include "ThunderTestRuntime.h" #include diff --git a/Tests/test_support/ThunderTestRuntime.h b/Tests/test_support/ThunderTestRuntime.h index 9f5d1805e2..2967d29cce 100644 --- a/Tests/test_support/ThunderTestRuntime.h +++ b/Tests/test_support/ThunderTestRuntime.h @@ -1,19 +1,3 @@ -/* - * Copyright 2024 RDK Management - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - #pragma once // ========================================================================== From 8e0049842ae6f2825d828f0f4f6357c8d9b366e2 Mon Sep 17 00:00:00 2001 From: smanes0213 Date: Mon, 23 Mar 2026 12:01:15 +0530 Subject: [PATCH 03/13] Resolve review comments, add smoke test for the test lib --- .../workflows/Test_Thunder_Test_Support.yml | 110 ++++++++++++++++++ CMakeLists.txt | 2 +- Tests/test_support/CMakeLists.txt | 12 +- Tests/test_support/Module.cpp | 15 ++- Tests/test_support/ThunderTestRuntime.cpp | 46 +++++--- Tests/test_support/ThunderTestRuntime.h | 17 +-- Tests/test_support/tests/CMakeLists.txt | 17 +++ Tests/test_support/tests/Module.cpp | 4 + Tests/test_support/tests/SmokeTest.cpp | 55 +++++++++ cmake/common/CompileSettings.cmake | 3 +- docs/ThunderTestSupport/ThunderTestSupport.md | 64 +++++++--- 11 files changed, 296 insertions(+), 49 deletions(-) create mode 100644 .github/workflows/Test_Thunder_Test_Support.yml create mode 100644 Tests/test_support/tests/CMakeLists.txt create mode 100644 Tests/test_support/tests/Module.cpp create mode 100644 Tests/test_support/tests/SmokeTest.cpp diff --git a/.github/workflows/Test_Thunder_Test_Support.yml b/.github/workflows/Test_Thunder_Test_Support.yml new file mode 100644 index 0000000000..6059e56a9e --- /dev/null +++ b/.github/workflows/Test_Thunder_Test_Support.yml @@ -0,0 +1,110 @@ +name: Test Thunder Test Support Library + +permissions: + contents: read + +on: + workflow_dispatch: + push: + branches: ["master", "development/Thunder_Test_Lib"] + paths: + - 'Tests/test_support/**' + - 'Source/Thunder/**' + - 'Source/core/**' + - 'Source/plugins/**' + pull_request: + branches: ["master"] + paths: + - 'Tests/test_support/**' + - 'Source/Thunder/**' + - 'Source/core/**' + - 'Source/plugins/**' + +jobs: + SmokeTest: + runs-on: ubuntu-24.04 + + strategy: + matrix: + build_type: [Debug, Release] + + name: Smoke Test - ${{matrix.build_type}} + + steps: + - name: Install necessary packages + uses: nick-fields/retry@v3 + with: + timeout_minutes: 10 + max_attempts: 10 + command: | + sudo gem install apt-spy2 + sudo apt-spy2 fix --commit --launchpad --country=US + sudo apt-get update + sudo apt-get install -y python3-pip build-essential cmake ninja-build libusb-1.0-0-dev zlib1g-dev libssl-dev libgtest-dev + python3 -m venv venv + source venv/bin/activate + pip install jsonref + +# ----- Checkout ----- + - name: Checkout Thunder + uses: actions/checkout@v4 + with: + path: Thunder + + - name: Checkout ThunderTools - default + if: ${{ !contains(github.event.pull_request.body, '[DependsOn=ThunderTools:') }} + uses: actions/checkout@v4 + with: + path: ThunderTools + repository: rdkcentral/ThunderTools + + - name: Regex ThunderTools + if: ${{ contains(github.event.pull_request.body, '[DependsOn=ThunderTools:') }} + id: tools + uses: AsasInnab/regex-action@v1 + with: + regex_pattern: '(?<=\[DependsOn=ThunderTools:).*(?=\])' + regex_flags: 'gim' + search_string: ${{github.event.pull_request.body}} + + - name: Checkout ThunderTools - ${{steps.tools.outputs.first_match}} + if: ${{ contains(github.event.pull_request.body, '[DependsOn=ThunderTools:') }} + uses: actions/checkout@v4 + with: + path: ThunderTools + repository: rdkcentral/ThunderTools + ref: ${{steps.tools.outputs.first_match}} + +# ----- Build ----- + - name: Build ThunderTools + run: | + source venv/bin/activate + cmake -G Ninja -S ThunderTools -B ${{matrix.build_type}}/build/ThunderTools \ + -DCMAKE_INSTALL_PREFIX="${{matrix.build_type}}/install/usr" + cmake --build ${{matrix.build_type}}/build/ThunderTools --target install + + - name: Build Thunder with test support + run: | + source venv/bin/activate + cmake -G Ninja -S Thunder -B ${{matrix.build_type}}/build/Thunder \ + -DBINDING="127.0.0.1" \ + -DCMAKE_BUILD_TYPE=${{matrix.build_type}} \ + -DCMAKE_INSTALL_PREFIX="${{matrix.build_type}}/install/usr" \ + -DCMAKE_MODULE_PATH="${PWD}/${{matrix.build_type}}/install/usr/include/WPEFramework/Modules" \ + -DPORT="0" \ + -DENABLE_TEST_RUNTIME=ON + cmake --build ${{matrix.build_type}}/build/Thunder --target install + +# ----- Run smoke test ----- + - name: Run smoke test + run: | + LD_LIBRARY_PATH="${{matrix.build_type}}/install/usr/lib:$LD_LIBRARY_PATH" \ + ${{matrix.build_type}}/build/Thunder/Tests/test_support/tests/thunder_test_runtime_smoke \ + --gtest_output="xml:smoke-test-results.xml" \ + --gtest_color=yes + + - name: Upload test results + uses: actions/upload-artifact@v4 + with: + name: smoke-test-results-${{matrix.build_type}} + path: smoke-test-results.xml diff --git a/CMakeLists.txt b/CMakeLists.txt index 9aa20630c1..071ddc4d1f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -29,7 +29,7 @@ set(VERSION_MAJOR ${${PROJECT_NAME}_VERSION_MAJOR}) set(VERSION_MINOR ${${PROJECT_NAME}_VERSION_MINOR}) set(VERSION_PATCH ${${PROJECT_NAME}_VERSION_PATCH}) -option(ENABLE_CXX17 "Build with C++17 support" ON) +# option(ENABLE_CXX17 "Build with C++17 support" ON) if(ENABLE_CXX17) set(CXX_STD 17) diff --git a/Tests/test_support/CMakeLists.txt b/Tests/test_support/CMakeLists.txt index fbd67b4c1a..dfc5d96c80 100644 --- a/Tests/test_support/CMakeLists.txt +++ b/Tests/test_support/CMakeLists.txt @@ -10,7 +10,7 @@ # 1. Link your test executable against thunder_test_support. # 2. Use ThunderTestRuntime::Initialize() to start the embedded server. # 3. Call JSON-RPC or COM-RPC methods directly against loaded plugins. -# 4. Call ThunderTestRuntime::Shutdown() when done. +# 4. Call ThunderTestRuntime::Deinitialize() when done. # # NOTE: PluginHost.cpp is deliberately excluded — it contains main(). # The test binary provides its own main() via GTest. @@ -95,6 +95,14 @@ if(HIBERNATESUPPORT) HIBERNATE_SUPPORT_ENABLED=1) endif() +# Enforce --whole-archive for this target so consumers automatically pull in +# Thunder's static initializers (MODULE_NAME_DECLARATION, SERVICE_REGISTRATION). +# Scoped to this archive only via $ — won't affect other libs. +target_link_options(thunder_test_support + INTERFACE + -Wl,--whole-archive $ -Wl,--no-whole-archive +) + # --- Install rules --- install(TARGETS thunder_test_support ARCHIVE DESTINATION lib @@ -103,3 +111,5 @@ install(TARGETS thunder_test_support install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/ThunderTestRuntime.h DESTINATION include/thunder_test_support ) + +add_subdirectory(tests) diff --git a/Tests/test_support/Module.cpp b/Tests/test_support/Module.cpp index 774bf97ba3..5049edfd53 100644 --- a/Tests/test_support/Module.cpp +++ b/Tests/test_support/Module.cpp @@ -1,13 +1,12 @@ // Module definition for the thunder_test_support static library. -// MODULE_NAME is set to ThunderTestRuntime via -D in CMakeLists.txt, -// which overrides Source/Thunder/Module.h's default of "Application". -// This ensures all server sources and the MODULE_NAME_DECLARATION below -// use the same symbol, and trace output shows "ThunderTestRuntime". +// +// Uses MODULE_NAME_ARCHIVE_DECLARATION instead of MODULE_NAME_DECLARATION +// because this is a static archive, not a standalone binary. The archive +// macro only defines the MODULE_NAME string symbol. The full declaration +// (ModuleBuildRef, GetModuleServices, SetModuleServices) is left to the +// consumer's own MODULE_NAME_DECLARATION, avoiding duplicate definitions. -#ifndef MODULE_NAME #define MODULE_NAME ThunderTestRuntime -#endif - #include -MODULE_NAME_DECLARATION(BUILD_REFERENCE) +MODULE_NAME_ARCHIVE_DECLARATION diff --git a/Tests/test_support/ThunderTestRuntime.cpp b/Tests/test_support/ThunderTestRuntime.cpp index 73e355855c..f648bb0392 100644 --- a/Tests/test_support/ThunderTestRuntime.cpp +++ b/Tests/test_support/ThunderTestRuntime.cpp @@ -8,14 +8,14 @@ // ========================================================================== // ThunderTestRuntime implementation // -// Lifecycle: Initialize() -> [run tests] -> Shutdown() +// Lifecycle: Initialize() -> [run tests] -> Deinitialize() // // Initialize creates a unique /tmp/thunder_test_XXXXXX/ directory tree, // writes a minimal Thunder config.json, parses it into PluginHost::Config, // constructs PluginHost::Server, and calls Server::Open() which boots // the controller and activates auto-start plugins. // -// Shutdown reverses the process: Server::Close(), cleanup temp files. +// Deinitialize reverses the process: Server::Close(), cleanup temp files. // ========================================================================== namespace Thunder { @@ -23,7 +23,7 @@ namespace TestCore { ThunderTestRuntime::~ThunderTestRuntime() { - Shutdown(); + Deinitialize(); } void ThunderTestRuntime::CreateDirectories() const @@ -43,6 +43,17 @@ namespace TestCore { // Build a minimal Thunder JSON config from the plugin list. // Uses port 0 (OS-assigned) and binds to localhost only. + // Helper: JSON-escape a string value and return it quoted (e.g. "foo\"bar" → "\"foo\\\"bar\"") + // Uses Core::JSON::String serialization to handle escaping correctly. + static string JsonEscape(const string& value) + { + Core::JSON::String json; + json = value; + string result; + json.ToString(result); + return result; + } + string ThunderTestRuntime::BuildConfigJSON(const std::vector& plugins, const string& systemPath, const string& proxyStubPath) const @@ -52,21 +63,21 @@ namespace TestCore { << "\"port\":0," << "\"binding\":\"127.0.0.1\"," << "\"idletime\":180," - << "\"persistentpath\":\"" << _tempDir << "persistent/\"," - << "\"volatilepath\":\"" << _tempDir << "volatile/\"," - << "\"datapath\":\"" << _tempDir << "data/\"," - << "\"systempath\":\"" << systemPath << "\"," - << "\"proxystubpath\":\"" << proxyStubPath << "\"," - << "\"communicator\":\"" << _tempDir << "communicator\"," + << "\"persistentpath\":" << JsonEscape(_tempDir + "persistent/") << "," + << "\"volatilepath\":" << JsonEscape(_tempDir + "volatile/") << "," + << "\"datapath\":" << JsonEscape(_tempDir + "data/") << "," + << "\"systempath\":" << JsonEscape(systemPath) << "," + << "\"proxystubpath\":" << JsonEscape(proxyStubPath) << "," + << "\"communicator\":" << JsonEscape(_tempDir + "communicator") << "," << "\"plugins\":["; for (size_t i = 0; i < plugins.size(); ++i) { const auto& p = plugins[i]; if (i > 0) json << ","; json << "{" - << "\"callsign\":\"" << p.callsign << "\"," - << "\"locator\":\"" << p.locator << "\"," - << "\"classname\":\"" << p.classname << "\"," + << "\"callsign\":" << JsonEscape(p.callsign) << "," + << "\"locator\":" << JsonEscape(p.locator) << "," + << "\"classname\":" << JsonEscape(p.classname) << "," << "\"startuporder\":" << p.startuporder << "," << "\"autostart\":" << (p.autostart ? "true" : "false"); @@ -158,13 +169,20 @@ namespace TestCore { // Invoke a JSON-RPC method synchronously via the in-process dispatcher. // Bypasses HTTP/WebSocket — calls IDispatcher::Invoke() directly. - uint32_t ThunderTestRuntime::InvokeJSONRPC(const string& callsign, const string& method, + // Derives the callsign from the method string (text before the first '.'). + uint32_t ThunderTestRuntime::InvokeJSONRPC(const string& method, const string& params, string& response) { if (_server == nullptr) { return Core::ERROR_ILLEGAL_STATE; } + size_t dot = method.find('.'); + if (dot == string::npos) { + return Core::ERROR_INVALID_SIGNATURE; + } + string callsign = method.substr(0, dot); + Core::ProxyType shell; uint32_t result = _server->Services().FromIdentifier(callsign, shell); if (result != Core::ERROR_NONE) { @@ -205,7 +223,7 @@ namespace TestCore { return string(); } - void ThunderTestRuntime::Shutdown() + void ThunderTestRuntime::Deinitialize() { if (_server != nullptr) { _server->Close(); diff --git a/Tests/test_support/ThunderTestRuntime.h b/Tests/test_support/ThunderTestRuntime.h index 2967d29cce..2d750f30e2 100644 --- a/Tests/test_support/ThunderTestRuntime.h +++ b/Tests/test_support/ThunderTestRuntime.h @@ -8,7 +8,7 @@ // - Generates a minimal Thunder config (JSON) on the fly // - Boots the embedded server and activates the controller // - Exposes helpers for JSON-RPC invocation and COM-RPC queries -// - Tears everything down cleanly on Shutdown() +// - Tears everything down cleanly on Deinitialize() // // Typical usage in a GTest fixture: // @@ -18,11 +18,11 @@ // std::vector plugins = { ... }; // _runtime.Initialize(plugins, pluginPath, proxyStubPath); // } -// static void TearDownTestSuite() { _runtime.Shutdown(); } +// static void TearDownTestSuite() { _runtime.Deinitialize(); } // // TEST_F(MyTest, JsonRpc) { // string resp; -// _runtime.InvokeJSONRPC("Callsign", "Method", params, resp); +// _runtime.InvokeJSONRPC("Callsign.1.method", params, resp); // } // // TEST_F(MyTest, ComRpc) { @@ -34,7 +34,8 @@ #define MODULE_NAME Application #endif -#include +#include +#include #include #include @@ -74,9 +75,9 @@ namespace TestCore { uint32_t Initialize(const std::vector& plugins, const string& systemPath = "", const string& proxyStubPath = ""); // Invoke a JSON-RPC method on a loaded plugin. - // Method format: "Callsign.Version.method" (e.g. "Counter.1.increment") - uint32_t InvokeJSONRPC(const string& callsign, const string& method, - const string& params, string& response); + // The callsign is derived from the method string (text before the first '.'). + // Method format: "Callsign.Version.method" (e.g. "MyPlugin.1.doSomething") + uint32_t InvokeJSONRPC(const string& method, const string& params, string& response); // Obtain a COM-RPC interface from a loaded plugin. // Caller must Release() the returned pointer when done. @@ -101,7 +102,7 @@ namespace TestCore { string CommunicatorPath() const; // Stop the server, release config, and clean up temp directories. - void Shutdown(); + void Deinitialize(); private: string BuildConfigJSON(const std::vector& plugins, const string& systemPath, const string& proxyStubPath) const; diff --git a/Tests/test_support/tests/CMakeLists.txt b/Tests/test_support/tests/CMakeLists.txt new file mode 100644 index 0000000000..d6171889aa --- /dev/null +++ b/Tests/test_support/tests/CMakeLists.txt @@ -0,0 +1,17 @@ +# Smoke test for thunder_test_support +# Boots the embedded server and exercises JSON-RPC calls against the +# built-in Controller plugin. No external plugin .so files needed. + +find_package(GTest REQUIRED) + +add_executable(thunder_test_runtime_smoke + SmokeTest.cpp + Module.cpp +) + +target_link_libraries(thunder_test_runtime_smoke + PRIVATE + thunder_test_support + GTest::GTest + GTest::Main +) diff --git a/Tests/test_support/tests/Module.cpp b/Tests/test_support/tests/Module.cpp new file mode 100644 index 0000000000..9fa428a64c --- /dev/null +++ b/Tests/test_support/tests/Module.cpp @@ -0,0 +1,4 @@ +#define MODULE_NAME SmokeTest +#include + +MODULE_NAME_DECLARATION(BUILD_REFERENCE) diff --git a/Tests/test_support/tests/SmokeTest.cpp b/Tests/test_support/tests/SmokeTest.cpp new file mode 100644 index 0000000000..b2c878f570 --- /dev/null +++ b/Tests/test_support/tests/SmokeTest.cpp @@ -0,0 +1,55 @@ +// ============================================================================ +// Smoke test for thunder_test_support +// +// Verifies the library links, boots the embedded server, and exercises +// basic JSON-RPC calls against the built-in Controller plugin. +// No external plugin .so files are needed. +// ============================================================================ + +#include +#include "ThunderTestRuntime.h" + +using namespace Thunder; + +class SmokeTest : public ::testing::Test { +protected: + static TestCore::ThunderTestRuntime _runtime; + + static void SetUpTestSuite() { + // No plugins — only the built-in Controller is needed + std::vector plugins; + uint32_t result = _runtime.Initialize(plugins); + ASSERT_EQ(result, Core::ERROR_NONE) << "Failed to initialize Thunder runtime"; + } + + static void TearDownTestSuite() { + _runtime.Deinitialize(); + } +}; + +TestCore::ThunderTestRuntime SmokeTest::_runtime; + +// Verify the server booted and we can query Controller status +TEST_F(SmokeTest, ControllerStatus) { + string response; + uint32_t result = _runtime.InvokeJSONRPC("Controller.1.status", "", response); + EXPECT_EQ(result, Core::ERROR_NONE); + EXPECT_FALSE(response.empty()); + // Response should contain Controller's own entry + EXPECT_NE(response.find("Controller"), string::npos); +} + +// Verify we can query subsystems +TEST_F(SmokeTest, ControllerSubsystems) { + string response; + uint32_t result = _runtime.InvokeJSONRPC("Controller.1.subsystems", "", response); + EXPECT_EQ(result, Core::ERROR_NONE); + EXPECT_FALSE(response.empty()); +} + +// Verify we can get the IShell for Controller +TEST_F(SmokeTest, GetControllerShell) { + auto shell = _runtime.GetShell("Controller"); + ASSERT_TRUE(shell.IsValid()); + EXPECT_EQ(shell->State(), PluginHost::IShell::state::ACTIVATED); +} diff --git a/cmake/common/CompileSettings.cmake b/cmake/common/CompileSettings.cmake index 3c45e40459..f31b766a4e 100644 --- a/cmake/common/CompileSettings.cmake +++ b/cmake/common/CompileSettings.cmake @@ -62,7 +62,8 @@ endif() # target_compile_definitions(CompileSettings INTERFACE "THUNDER_PLATFORM=${THUNDER_PLATFORM}") # message(STATUS "Selected platform ${THUNDER_PLATFORM}") -target_compile_options(CompileSettings INTERFACE -Wno-psabi) +# TEMPORARY: Force C++11 to work around SocketServer.h IteratorType bug under C++17. +target_compile_options(CompileSettings INTERFACE -std=c++11 -Wno-psabi) if(BUILD_SHARED_LIBS) target_compile_definitions(CompileSettings INTERFACE BUILD_SHARED_LIBS) diff --git a/docs/ThunderTestSupport/ThunderTestSupport.md b/docs/ThunderTestSupport/ThunderTestSupport.md index a863cab25a..99b54373c1 100644 --- a/docs/ThunderTestSupport/ThunderTestSupport.md +++ b/docs/ThunderTestSupport/ThunderTestSupport.md @@ -50,7 +50,7 @@ In production, Thunder runs as a standalone daemon (`PluginHost.cpp` → `main() 2. **Statically links server internals** — `PluginServer.cpp`, `Controller.cpp`, `SystemInfo.cpp`, `PostMortem.cpp`, and `Probe.cpp` are compiled directly into the static library. -3. **Wraps server lifecycle** — `ThunderTestRuntime` provides `Initialize()` and `Shutdown()` to manage the server, replacing the daemon's startup/shutdown sequence. +3. **Wraps server lifecycle** — `ThunderTestRuntime` provides `Initialize()` and `Deinitialize()` to manage the server, replacing the daemon's startup/shutdown sequence. 4. **Generates config on the fly** — instead of reading `/etc/Thunder/config.json`, the runtime builds a minimal JSON config programmatically and writes it to a temporary directory. @@ -69,7 +69,11 @@ Tests/ ├── CMakeLists.txt ← Build definitions for the static library ├── Module.cpp ← MODULE_NAME definition (ThunderTestRuntime) ├── ThunderTestRuntime.h ← Public API header - └── ThunderTestRuntime.cpp ← Implementation + ├── ThunderTestRuntime.cpp ← Implementation + └── tests/ + ├── CMakeLists.txt ← Smoke test build + ├── Module.cpp ← MODULE_NAME_DECLARATION for smoke test binary + └── SmokeTest.cpp ← Self-contained Controller smoke test ``` ### 1. `Tests/CMakeLists.txt` (Modified) @@ -95,19 +99,29 @@ Builds the `thunder_test_support` static library. Key design decisions: - **Source files**: Compiles `ThunderTestRuntime.cpp`, `Module.cpp`, plus Thunder server sources directly from `Source/Thunder/` (PluginServer, Controller, SystemInfo, PostMortem, Probe). - **Excludes `PluginHost.cpp`**: This contains `main()` and would conflict with the test binary's entry point. - **Compile definitions**: Sets `APPLICATION_NAME=ThunderTestRuntime` and `THREADPOOL_COUNT=4`. -- **Public dependencies**: Exposes `ThunderCore`, `ThunderCOM`, `ThunderPlugins`, `ThunderMessaging`, `ThunderWebSocket`, `ThunderCOMProcess`, and `Threads` as PUBLIC link dependencies, so consumers automatically get all required libraries. +- **Public dependencies**: Exposes `ThunderCore`, `ThunderCryptalgo`, `ThunderCOM`, `ThunderPlugins`, `ThunderMessaging`, `ThunderWebSocket`, `ThunderCOMProcess`, and `Threads` as PUBLIC link dependencies, so consumers automatically get all required libraries. - **Private dependencies**: `CompileSettings` is linked PRIVATE to apply Thunder's compile flags without propagating them. - **Conditional features**: Supports `WARNING_REPORTING`, `PROCESSCONTAINERS`, and `HIBERNATESUPPORT` when enabled. - **Install rules**: Installs the `.a` archive to `lib/` and the header to `include/thunder_test_support/`. ### 3. `Tests/test_support/Module.cpp` (New) -Module definition required by Thunder's internal logging/tracing macros. The `MODULE_NAME` value appears in trace output to identify which component emitted a message. Using `ThunderTestRuntime` makes it easy to distinguish test runtime traces from production daemon or plugin traces: +Module definition required by Thunder's internal logging/tracing macros. Uses `MODULE_NAME_ARCHIVE_DECLARATION` instead of `MODULE_NAME_DECLARATION` because this is a static archive, not a standalone binary. The archive macro only defines the `MODULE_NAME` string symbol. The full declaration (`ModuleBuildRef`, `GetModuleServices`, `SetModuleServices`) is left to the consumer's own `MODULE_NAME_DECLARATION`, avoiding duplicate definitions at link time: ```cpp -#ifndef MODULE_NAME #define MODULE_NAME ThunderTestRuntime -#endif +#include + +MODULE_NAME_ARCHIVE_DECLARATION +``` + +Consumer binaries (test executables) must provide their own `Module.cpp` with the full declaration: + +```cpp +#define MODULE_NAME MyTestName +#include + +MODULE_NAME_DECLARATION(BUILD_REFERENCE) ``` ### 4. `Tests/test_support/ThunderTestRuntime.h` (New) @@ -118,12 +132,12 @@ Public API header. Defines the `Thunder::TestCore::ThunderTestRuntime` class: |--------|-------------| | `PluginConfig` (struct) | Describes a plugin to load: callsign, locator (.so name), classname, autostart, startuporder, configuration JSON | | `Initialize()` | Boots the embedded server with given plugins, system path, and proxy stub path | -| `InvokeJSONRPC()` | Calls a JSON-RPC method synchronously via in-process `IDispatcher::Invoke()` | +| `InvokeJSONRPC()` | Calls a JSON-RPC method synchronously via in-process `IDispatcher::Invoke()`. Callsign is derived from the method string. | | `GetInterface()` | Template: obtains a COM-RPC interface from a plugin via `QueryInterface()` | | `GetShell()` | Returns the `IShell` proxy for a plugin (for activation/deactivation control) | | `Server()` | Direct access to the underlying `PluginHost::Server` | | `CommunicatorPath()` | Returns the UNIX domain socket path | -| `Shutdown()` | Stops the server, releases config, cleans up temp directories | +| `Deinitialize()` | Stops the server, releases config, cleans up temp directories | ### 5. `Tests/test_support/ThunderTestRuntime.cpp` (New) @@ -143,7 +157,7 @@ Initialize() ├── Activate Controller plugin └── Activate auto-start plugins -Shutdown() +Deinitialize() ├── Server::Close() │ ├── Deactivate all plugins │ └── Close connections @@ -158,8 +172,9 @@ Shutdown() `InvokeJSONRPC()` bypasses HTTP/WebSocket entirely: ``` -InvokeJSONRPC("", ".1.", params, response) - ├── Services().FromIdentifier("") → IShell proxy +InvokeJSONRPC(".1.", params, response) + ├── Extract callsign from method string (text before first '.') + ├── Services().FromIdentifier(callsign) → IShell proxy ├── shell->QueryInterface() → dispatcher └── dispatcher->Invoke(0, 0, "", method, params, response) ``` @@ -232,15 +247,13 @@ target_link_libraries(ThunderMyPluginImpl PRIVATE # Build the test executable add_executable(my_plugin_test MyPluginTest.cpp) target_link_libraries(my_plugin_test PRIVATE - -Wl,--whole-archive thunder_test_support - -Wl,--no-whole-archive gmock_main ${NAMESPACE}Definitions::${NAMESPACE}Definitions ) ``` -> **Note**: `--whole-archive` is required for `thunder_test_support` to ensure Thunder's static initializers (MODULE_NAME_DECLARATION, service registrations, etc.) are linked even though the test binary doesn't reference them directly. +> **Note**: The `thunder_test_support` target carries `INTERFACE` link options that automatically apply `--whole-archive` scoped to its own archive. This ensures Thunder's static initializers (`MODULE_NAME_DECLARATION`, `SERVICE_REGISTRATION`) are preserved without the consumer needing to specify linker flags manually. This works when linking against the CMake target directly. When linking against the `.a` file by path (e.g. in external repos), manual `--whole-archive` / `--no-whole-archive` flags are still required. #### 2. Test fixture @@ -274,7 +287,7 @@ protected: } static void TearDownTestSuite() { - _runtime.Shutdown(); + _runtime.Deinitialize(); } }; @@ -284,7 +297,7 @@ TestCore::ThunderTestRuntime MyPluginTest::_runtime; TEST_F(MyPluginTest, JsonRpcCall) { string response; EXPECT_EQ(Core::ERROR_NONE, - _runtime.InvokeJSONRPC("MyPlugin", "MyPlugin.1.someMethod", R"({"param":1})", response)); + _runtime.InvokeJSONRPC("MyPlugin.1.someMethod", R"({"param":1})", response)); // Validate response... } @@ -357,6 +370,8 @@ A static archive ensures that all Thunder server symbols are available to the te Thunder uses static initializers extensively (`MODULE_NAME_DECLARATION`, `SERVICE_REGISTRATION`, singletons). Without `--whole-archive`, the linker would discard these symbols because the test binary doesn't reference them directly. The `--whole-archive` flag forces all object files from the archive to be included. +This is enforced automatically via `target_link_options(INTERFACE)` on the CMake target, scoped to the archive using `$` so it doesn't affect other libraries on the link line. Consumers linking against the CMake target get this for free. Consumers linking against the `.a` file by path must add the flags manually. + ### Why exclude `PluginHost.cpp`? `PluginHost.cpp` contains: @@ -375,4 +390,21 @@ Thunder uses static initializers extensively (`MODULE_NAME_DECLARATION`, `SERVIC Using port 0 in the config tells the OS to assign an available port, avoiding conflicts when multiple test processes run simultaneously. +--- + +## Smoke Test + +A self-contained smoke test (`Tests/test_support/tests/SmokeTest.cpp`) is included with the library. It verifies that the library links, boots, and can exercise JSON-RPC against the built-in Controller plugin — no external plugin `.so` files needed. + +The smoke test is built automatically when `ENABLE_TEST_RUNTIME=ON` and GTest is available. It includes a `Module.cpp` with `MODULE_NAME_DECLARATION(BUILD_REFERENCE)` — required because the library uses `MODULE_NAME_ARCHIVE_DECLARATION`. It covers: + +- **ControllerStatus** — calls `Controller.1.status` and verifies a non-empty response containing "Controller" +- **ControllerSubsystems** — calls `Controller.1.subsystems` and verifies a non-empty response +- **GetControllerShell** — obtains the Controller's `IShell` and verifies it is in `ACTIVATED` state + +Running: +```bash +./thunder_test_runtime_smoke --gtest_color=yes +``` + --- \ No newline at end of file From eb858a890289d123d018eaddc5a34d8432067d20 Mon Sep 17 00:00:00 2001 From: Sankalp Maneshwar Date: Mon, 23 Mar 2026 12:06:07 +0530 Subject: [PATCH 04/13] Update CMakeLists.txt --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 071ddc4d1f..9aa20630c1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -29,7 +29,7 @@ set(VERSION_MAJOR ${${PROJECT_NAME}_VERSION_MAJOR}) set(VERSION_MINOR ${${PROJECT_NAME}_VERSION_MINOR}) set(VERSION_PATCH ${${PROJECT_NAME}_VERSION_PATCH}) -# option(ENABLE_CXX17 "Build with C++17 support" ON) +option(ENABLE_CXX17 "Build with C++17 support" ON) if(ENABLE_CXX17) set(CXX_STD 17) From 4d5ebda641d3e84c2b0f220b403d9ce9039c0ab6 Mon Sep 17 00:00:00 2001 From: Sankalp Maneshwar Date: Mon, 23 Mar 2026 12:09:38 +0530 Subject: [PATCH 05/13] Add build flag to build with CXX11 support --- .github/workflows/Test_Thunder_Test_Support.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/Test_Thunder_Test_Support.yml b/.github/workflows/Test_Thunder_Test_Support.yml index 6059e56a9e..a0fc87f684 100644 --- a/.github/workflows/Test_Thunder_Test_Support.yml +++ b/.github/workflows/Test_Thunder_Test_Support.yml @@ -92,7 +92,8 @@ jobs: -DCMAKE_INSTALL_PREFIX="${{matrix.build_type}}/install/usr" \ -DCMAKE_MODULE_PATH="${PWD}/${{matrix.build_type}}/install/usr/include/WPEFramework/Modules" \ -DPORT="0" \ - -DENABLE_TEST_RUNTIME=ON + -DENABLE_TEST_RUNTIME=ON \ + -DENABLE_CXX17=OFF cmake --build ${{matrix.build_type}}/build/Thunder --target install # ----- Run smoke test ----- From 72d7c40d9df187ac94d142892fa4e760ff18993e Mon Sep 17 00:00:00 2001 From: smanes0213 Date: Wed, 25 Mar 2026 11:00:52 +0530 Subject: [PATCH 06/13] Remove hardcoded -std=c++11 from CompileSettings.cmake, restoring the original -Wno-psabi only setting set CXX_STANDARD on thunder_test_support target so it respects the ENABLE_CXX17 flag like all other Thunder targets Pass -DENABLE_CXX17=OFF in the smoke test CI workflow to avoid the SocketServer.h IteratorType move constructor bug --- .github/workflows/Test_Thunder_Test_Support.yml | 3 +-- Tests/test_support/CMakeLists.txt | 5 +++++ cmake/common/CompileSettings.cmake | 3 +-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/Test_Thunder_Test_Support.yml b/.github/workflows/Test_Thunder_Test_Support.yml index a0fc87f684..6059e56a9e 100644 --- a/.github/workflows/Test_Thunder_Test_Support.yml +++ b/.github/workflows/Test_Thunder_Test_Support.yml @@ -92,8 +92,7 @@ jobs: -DCMAKE_INSTALL_PREFIX="${{matrix.build_type}}/install/usr" \ -DCMAKE_MODULE_PATH="${PWD}/${{matrix.build_type}}/install/usr/include/WPEFramework/Modules" \ -DPORT="0" \ - -DENABLE_TEST_RUNTIME=ON \ - -DENABLE_CXX17=OFF + -DENABLE_TEST_RUNTIME=ON cmake --build ${{matrix.build_type}}/build/Thunder --target install # ----- Run smoke test ----- diff --git a/Tests/test_support/CMakeLists.txt b/Tests/test_support/CMakeLists.txt index dfc5d96c80..62bc113060 100644 --- a/Tests/test_support/CMakeLists.txt +++ b/Tests/test_support/CMakeLists.txt @@ -45,6 +45,11 @@ target_compile_definitions(thunder_test_support target_compile_options(thunder_test_support PRIVATE -Wno-psabi) +set_target_properties(thunder_test_support PROPERTIES + CXX_STANDARD ${CXX_STD} + CXX_STANDARD_REQUIRED YES +) + target_link_libraries(thunder_test_support PRIVATE CompileSettings::CompileSettings diff --git a/cmake/common/CompileSettings.cmake b/cmake/common/CompileSettings.cmake index f31b766a4e..3c45e40459 100644 --- a/cmake/common/CompileSettings.cmake +++ b/cmake/common/CompileSettings.cmake @@ -62,8 +62,7 @@ endif() # target_compile_definitions(CompileSettings INTERFACE "THUNDER_PLATFORM=${THUNDER_PLATFORM}") # message(STATUS "Selected platform ${THUNDER_PLATFORM}") -# TEMPORARY: Force C++11 to work around SocketServer.h IteratorType bug under C++17. -target_compile_options(CompileSettings INTERFACE -std=c++11 -Wno-psabi) +target_compile_options(CompileSettings INTERFACE -Wno-psabi) if(BUILD_SHARED_LIBS) target_compile_definitions(CompileSettings INTERFACE BUILD_SHARED_LIBS) From fe8854f40677f8507ed210d5b59f9a3bd4efe3ff Mon Sep 17 00:00:00 2001 From: Sankalp Maneshwar Date: Wed, 25 Mar 2026 11:05:39 +0530 Subject: [PATCH 07/13] Update Test_Thunder_Test_Support.yml --- .github/workflows/Test_Thunder_Test_Support.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/Test_Thunder_Test_Support.yml b/.github/workflows/Test_Thunder_Test_Support.yml index 6059e56a9e..d6c16bc9b8 100644 --- a/.github/workflows/Test_Thunder_Test_Support.yml +++ b/.github/workflows/Test_Thunder_Test_Support.yml @@ -92,6 +92,7 @@ jobs: -DCMAKE_INSTALL_PREFIX="${{matrix.build_type}}/install/usr" \ -DCMAKE_MODULE_PATH="${PWD}/${{matrix.build_type}}/install/usr/include/WPEFramework/Modules" \ -DPORT="0" \ + -DENABLE_CXX17=OFF -DENABLE_TEST_RUNTIME=ON cmake --build ${{matrix.build_type}}/build/Thunder --target install From a146f7670f653c3e0a257452f1c43a35d57f5185 Mon Sep 17 00:00:00 2001 From: Sankalp Maneshwar Date: Wed, 25 Mar 2026 11:06:03 +0530 Subject: [PATCH 08/13] Update Test_Thunder_Test_Support.yml --- .github/workflows/Test_Thunder_Test_Support.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/Test_Thunder_Test_Support.yml b/.github/workflows/Test_Thunder_Test_Support.yml index d6c16bc9b8..7468047f42 100644 --- a/.github/workflows/Test_Thunder_Test_Support.yml +++ b/.github/workflows/Test_Thunder_Test_Support.yml @@ -92,7 +92,7 @@ jobs: -DCMAKE_INSTALL_PREFIX="${{matrix.build_type}}/install/usr" \ -DCMAKE_MODULE_PATH="${PWD}/${{matrix.build_type}}/install/usr/include/WPEFramework/Modules" \ -DPORT="0" \ - -DENABLE_CXX17=OFF + -DENABLE_CXX17=OFF \ -DENABLE_TEST_RUNTIME=ON cmake --build ${{matrix.build_type}}/build/Thunder --target install From 2e76a987eeca33835eb73d7663360ca00cb35528 Mon Sep 17 00:00:00 2001 From: smanes0213 Date: Wed, 15 Apr 2026 12:26:16 +0530 Subject: [PATCH 09/13] Resolve copilot review comments --- Tests/test_support/CMakeLists.txt | 32 +++-- Tests/test_support/ThunderTestRuntime.cpp | 125 ++++++++++++++---- Tests/test_support/ThunderTestRuntime.h | 14 +- Tests/test_support/tests/SmokeTest.cpp | 2 + docs/ThunderTestSupport/ThunderTestSupport.md | 38 ++++-- 5 files changed, 157 insertions(+), 54 deletions(-) diff --git a/Tests/test_support/CMakeLists.txt b/Tests/test_support/CMakeLists.txt index 62bc113060..b2ad62fffe 100644 --- a/Tests/test_support/CMakeLists.txt +++ b/Tests/test_support/CMakeLists.txt @@ -100,21 +100,37 @@ if(HIBERNATESUPPORT) HIBERNATE_SUPPORT_ENABLED=1) endif() -# Enforce --whole-archive for this target so consumers automatically pull in +# Enforce whole-archive semantics for this target so consumers automatically pull in # Thunder's static initializers (MODULE_NAME_DECLARATION, SERVICE_REGISTRATION). -# Scoped to this archive only via $ — won't affect other libs. -target_link_options(thunder_test_support - INTERFACE - -Wl,--whole-archive $ -Wl,--no-whole-archive -) +# Use linker-specific flags only on supported platforms/toolchains. +if(APPLE) + target_link_options(thunder_test_support + INTERFACE + "-Wl,-force_load,$" + ) +elseif((CMAKE_SYSTEM_NAME STREQUAL "Linux" OR CMAKE_SYSTEM_NAME STREQUAL "Android") AND + (CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")) + # Scoped to this archive only via $ — won't affect other libs. + target_link_options(thunder_test_support + INTERFACE + -Wl,--whole-archive $ -Wl,--no-whole-archive + ) +else() + message(WARNING + "Automatic whole-archive linking for thunder_test_support is not configured for " + "${CMAKE_SYSTEM_NAME} with ${CMAKE_CXX_COMPILER_ID}. Consumers may need to link " + "the archive with platform-specific whole-archive/force-load options to ensure " + "Thunder static initializers are included." + ) +endif() # --- Install rules --- install(TARGETS thunder_test_support - ARCHIVE DESTINATION lib + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} ) install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/ThunderTestRuntime.h - DESTINATION include/thunder_test_support + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/thunder_test_support ) add_subdirectory(tests) diff --git a/Tests/test_support/ThunderTestRuntime.cpp b/Tests/test_support/ThunderTestRuntime.cpp index f648bb0392..97cc0f0bb1 100644 --- a/Tests/test_support/ThunderTestRuntime.cpp +++ b/Tests/test_support/ThunderTestRuntime.cpp @@ -26,60 +26,129 @@ namespace TestCore { Deinitialize(); } - void ThunderTestRuntime::CreateDirectories() const + bool ThunderTestRuntime::CreateDirectories() const { - Core::Directory(_tempDir.c_str()).Create(); - Core::Directory((_tempDir + "persistent/").c_str()).Create(); - Core::Directory((_tempDir + "volatile/").c_str()).Create(); - Core::Directory((_tempDir + "data/").c_str()).Create(); + const string persistentPath = _tempDir + "persistent/"; + const string volatilePath = _tempDir + "volatile/"; + const string dataPath = _tempDir + "data/"; + + const bool created = + Core::Directory(persistentPath.c_str()).Create() && + Core::Directory(volatilePath.c_str()).Create() && + Core::Directory(dataPath.c_str()).Create(); + + if (created == false) { + TRACE_L1("ThunderTestRuntime: Failed to create temp directory tree at '%s'", _tempDir.c_str()); + } + + return created; } void ThunderTestRuntime::CleanupDirectories() const { if (!_tempDir.empty()) { Core::Directory(_tempDir.c_str()).Destroy(); + + // Core::Directory::Destroy() does not remove the directory if the path + // ends with a trailing separator. Normalize the path before calling it. + string path = _tempDir; + while (!path.empty() && (path.back() == '/' || path.back() == '\\')) { + path.pop_back(); + } + if (!path.empty()) { + Core::Directory(path.c_str()).Destroy(); + } } } - // Build a minimal Thunder JSON config from the plugin list. - // Uses port 0 (OS-assigned) and binds to localhost only. - // Helper: JSON-escape a string value and return it quoted (e.g. "foo\"bar" → "\"foo\\\"bar\"") - // Uses Core::JSON::String serialization to handle escaping correctly. - static string JsonEscape(const string& value) + // Helper to escape a string for safe inclusion as a JSON string value. + // It escapes quotes, backslashes, and control characters (< 0x20). + static std::string JsonEscape(const std::string& input) { - Core::JSON::String json; - json = value; - string result; - json.ToString(result); - return result; + std::string output; + output.reserve(input.size()); + const char* hex = "0123456789abcdef"; + + for (unsigned char c : input) { + switch (c) { + case '"': + output += "\\\""; + break; + case '\\': + output += "\\\\"; + break; + case '\b': + output += "\\b"; + break; + case '\f': + output += "\\f"; + break; + case '\n': + output += "\\n"; + break; + case '\r': + output += "\\r"; + break; + case '\t': + output += "\\t"; + break; + default: + if (c < 0x20) { + output += "\\u00"; + output += hex[(c >> 4) & 0x0F]; + output += hex[c & 0x0F]; + } else { + output += static_cast(c); + } + break; + } + } + + return output; + } + + static const char* ToStartModeString(const ThunderTestRuntime::PluginConfig::StartMode startMode) + { + switch (startMode) { + case ThunderTestRuntime::PluginConfig::StartMode::Activated: + return "Activated"; + case ThunderTestRuntime::PluginConfig::StartMode::Deactivated: + return "Deactivated"; + case ThunderTestRuntime::PluginConfig::StartMode::Unavailable: + return "Unavailable"; + default: + return "Activated"; + } } string ThunderTestRuntime::BuildConfigJSON(const std::vector& plugins, const string& systemPath, const string& proxyStubPath) const { + const string communicatorPath = _tempDir + "communicator|0777"; + std::ostringstream json; json << "{" << "\"port\":0," << "\"binding\":\"127.0.0.1\"," << "\"idletime\":180," - << "\"persistentpath\":" << JsonEscape(_tempDir + "persistent/") << "," - << "\"volatilepath\":" << JsonEscape(_tempDir + "volatile/") << "," - << "\"datapath\":" << JsonEscape(_tempDir + "data/") << "," - << "\"systempath\":" << JsonEscape(systemPath) << "," - << "\"proxystubpath\":" << JsonEscape(proxyStubPath) << "," - << "\"communicator\":" << JsonEscape(_tempDir + "communicator") << "," + << "\"persistentpath\":\"" << JsonEscape(_tempDir + "persistent/") << "\"," + << "\"volatilepath\":\"" << JsonEscape(_tempDir + "volatile/") << "\"," + << "\"datapath\":\"" << JsonEscape(_tempDir + "data/") << "\"," + << "\"systempath\":\"" << JsonEscape(systemPath) << "\"," + << "\"proxystubpath\":\"" << JsonEscape(proxyStubPath) << "\"," + << "\"communicator\":\"" << JsonEscape(communicatorPath) << "\"," << "\"plugins\":["; for (size_t i = 0; i < plugins.size(); ++i) { const auto& p = plugins[i]; if (i > 0) json << ","; json << "{" - << "\"callsign\":" << JsonEscape(p.callsign) << "," - << "\"locator\":" << JsonEscape(p.locator) << "," - << "\"classname\":" << JsonEscape(p.classname) << "," + << "\"callsign\":\"" << JsonEscape(p.callsign) << "\"," + << "\"locator\":\"" << JsonEscape(p.locator) << "\"," + << "\"classname\":\"" << JsonEscape(p.classname) << "\"," << "\"startuporder\":" << p.startuporder << "," - << "\"autostart\":" << (p.autostart ? "true" : "false"); + << "\"startmode\":\"" << ToStartModeString(p.startmode) << "\""; if (!p.configuration.empty()) { json << ",\"configuration\":" << p.configuration; @@ -108,7 +177,11 @@ namespace TestCore { } _tempDir = string(tempResult) + "/"; - CreateDirectories(); + if (CreateDirectories() == false) { + CleanupDirectories(); + _tempDir.clear(); + return Core::ERROR_OPENING_FAILED; + } // Determine system path for plugin .so files string sysPath = systemPath; diff --git a/Tests/test_support/ThunderTestRuntime.h b/Tests/test_support/ThunderTestRuntime.h index 2d750f30e2..5b89884121 100644 --- a/Tests/test_support/ThunderTestRuntime.h +++ b/Tests/test_support/ThunderTestRuntime.h @@ -30,10 +30,6 @@ // } // ========================================================================== -#ifndef MODULE_NAME -#define MODULE_NAME Application -#endif - #include #include #include @@ -53,10 +49,16 @@ namespace TestCore { // Describes a plugin to be loaded by the test runtime. // Maps directly to a Thunder plugin JSON config entry. struct PluginConfig { + enum class StartMode { + Activated, + Deactivated, + Unavailable + }; + string callsign; // Plugin callsign (e.g. "Counter") string locator; // Shared library name (e.g. "libThunderCounterImplementation.so") string classname; // Class name registered via SERVICE_REGISTRATION - bool autostart = true; // Whether to auto-activate on startup + StartMode startmode = StartMode::Activated; // Thunder plugin start mode int startuporder = 50; // Activation priority (lower = earlier) string configuration; // Optional JSON object for plugin-specific config }; @@ -106,7 +108,7 @@ namespace TestCore { private: string BuildConfigJSON(const std::vector& plugins, const string& systemPath, const string& proxyStubPath) const; - void CreateDirectories() const; + bool CreateDirectories() const; void CleanupDirectories() const; PluginHost::Config* _config = nullptr; diff --git a/Tests/test_support/tests/SmokeTest.cpp b/Tests/test_support/tests/SmokeTest.cpp index b2c878f570..da85c4de56 100644 --- a/Tests/test_support/tests/SmokeTest.cpp +++ b/Tests/test_support/tests/SmokeTest.cpp @@ -6,6 +6,8 @@ // No external plugin .so files are needed. // ============================================================================ +#define MODULE_NAME SmokeTest + #include #include "ThunderTestRuntime.h" diff --git a/docs/ThunderTestSupport/ThunderTestSupport.md b/docs/ThunderTestSupport/ThunderTestSupport.md index 99b54373c1..e40866a985 100644 --- a/docs/ThunderTestSupport/ThunderTestSupport.md +++ b/docs/ThunderTestSupport/ThunderTestSupport.md @@ -12,7 +12,7 @@ The **thunder_test_support** library enables in-process integration testing of T | CMake option | `ENABLE_TEST_RUNTIME=ON` | | Location | `Tests/test_support/` | | Public header | `ThunderTestRuntime.h` | -| Install paths | `lib/libthunder_test_support.a`, `include/thunder_test_support/ThunderTestRuntime.h` | +| Install paths | `${CMAKE_INSTALL_LIBDIR}/libthunder_test_support.a`, `${CMAKE_INSTALL_INCLUDEDIR}/thunder_test_support/ThunderTestRuntime.h` | --- @@ -34,11 +34,12 @@ The **thunder_test_support** library enables in-process integration testing of T │ │ ├── PluginServer │ │ │ │ │ ├── Controller │ │ │ │ ├── SystemInfo │ │ │ │ │ ├── PluginServer │ │ │ │ └── HTTP/WS Listener │ │ │ │ │ ├── SystemInfo │ │ -│ │ ↓ │ │ │ │ │ └── (no HTTP listener) │ │ +│ │ ↓ │ │ │ │ │ └── HTTP/WS Listener │ │ │ │ Plugin .so (dynamic load) │ │ │ │ └── Plugin .so (dynamic load) │ │ │ └───────────────────────────────┘ │ │ └─────────────────────────────────┘ │ │ │ │ │ -│ Communication: HTTP/WS/COMRPC │ │ Communication: Direct in-process calls │ +│ Communication: HTTP/WS/COMRPC │ │ Communication: HTTP/WS/COMRPC available, +│ │ │ tests typically use direct in-process calls │ └─────────────────────────────────────┘ └─────────────────────────────────────────┘ ``` @@ -54,7 +55,9 @@ In production, Thunder runs as a standalone daemon (`PluginHost.cpp` → `main() 4. **Generates config on the fly** — instead of reading `/etc/Thunder/config.json`, the runtime builds a minimal JSON config programmatically and writes it to a temporary directory. -5. **Plugins load normally** — plugin `.so` files are still loaded dynamically via `dlopen()`, exactly as in production. The `systempath` config entry points to the directory containing plugin shared libraries. +5. **Still opens the listener** — `PluginHost::Server::Open()` still starts the HTTP/WebSocket listener, typically bound to `127.0.0.1` on an ephemeral port in the test runtime. + +6. **Plugins load normally** — plugin `.so` files are still loaded dynamically via `dlopen()`, exactly as in production. The `systempath` config entry points to the directory containing plugin shared libraries. --- @@ -102,7 +105,7 @@ Builds the `thunder_test_support` static library. Key design decisions: - **Public dependencies**: Exposes `ThunderCore`, `ThunderCryptalgo`, `ThunderCOM`, `ThunderPlugins`, `ThunderMessaging`, `ThunderWebSocket`, `ThunderCOMProcess`, and `Threads` as PUBLIC link dependencies, so consumers automatically get all required libraries. - **Private dependencies**: `CompileSettings` is linked PRIVATE to apply Thunder's compile flags without propagating them. - **Conditional features**: Supports `WARNING_REPORTING`, `PROCESSCONTAINERS`, and `HIBERNATESUPPORT` when enabled. -- **Install rules**: Installs the `.a` archive to `lib/` and the header to `include/thunder_test_support/`. +- **Install rules**: Installs the `.a` archive to `${CMAKE_INSTALL_LIBDIR}` and the header to `${CMAKE_INSTALL_INCLUDEDIR}/thunder_test_support/`. ### 3. `Tests/test_support/Module.cpp` (New) @@ -130,7 +133,7 @@ Public API header. Defines the `Thunder::TestCore::ThunderTestRuntime` class: | Member | Description | |--------|-------------| -| `PluginConfig` (struct) | Describes a plugin to load: callsign, locator (.so name), classname, autostart, startuporder, configuration JSON | +| `PluginConfig` (struct) | Describes a plugin to load: callsign, locator (.so name), classname, startmode, startuporder, configuration JSON. `startmode` supports Thunder's `Activated`, `Deactivated`, and `Unavailable` states. | | `Initialize()` | Boots the embedded server with given plugins, system path, and proxy stub path | | `InvokeJSONRPC()` | Calls a JSON-RPC method synchronously via in-process `IDispatcher::Invoke()`. Callsign is derived from the method string. | | `GetInterface()` | Template: obtains a COM-RPC interface from a plugin via `QueryInterface()` | @@ -155,6 +158,7 @@ Initialize() ├── Set up security ├── ServiceMap::Open() ├── Activate Controller plugin + ├── Open HTTP/WebSocket listener └── Activate auto-start plugins Deinitialize() @@ -179,7 +183,7 @@ InvokeJSONRPC(".1.", params, response) └── dispatcher->Invoke(0, 0, "", method, params, response) ``` -This calls the plugin's JSON-RPC handler directly in-process, with zero network overhead. +This calls the plugin's JSON-RPC handler directly in-process, with zero network overhead. The HTTP/WebSocket listener is still started by `Server::Open()`, but the helper API does not route JSON-RPC through it. #### COM-RPC Interface Access @@ -217,8 +221,8 @@ cmake --build build/Thunder --target install -j$(nproc) After installation, the library and header are available at: ``` -/lib/libthunder_test_support.a -/include/thunder_test_support/ThunderTestRuntime.h +/${CMAKE_INSTALL_LIBDIR}/libthunder_test_support.a +/${CMAKE_INSTALL_INCLUDEDIR}/thunder_test_support/ThunderTestRuntime.h ``` --- @@ -253,7 +257,7 @@ target_link_libraries(my_plugin_test PRIVATE ) ``` -> **Note**: The `thunder_test_support` target carries `INTERFACE` link options that automatically apply `--whole-archive` scoped to its own archive. This ensures Thunder's static initializers (`MODULE_NAME_DECLARATION`, `SERVICE_REGISTRATION`) are preserved without the consumer needing to specify linker flags manually. This works when linking against the CMake target directly. When linking against the `.a` file by path (e.g. in external repos), manual `--whole-archive` / `--no-whole-archive` flags are still required. +> **Note**: The `thunder_test_support` target carries `INTERFACE` link options that automatically enforce whole-archive semantics for its own archive. On Apple this uses `-Wl,-force_load,...`; on Linux/Android with GNU/Clang-family toolchains it uses `--whole-archive` / `--no-whole-archive`. This ensures Thunder's static initializers (`MODULE_NAME_DECLARATION`, `SERVICE_REGISTRATION`) are preserved without the consumer needing to specify linker flags manually when linking against the CMake target. Consumers linking against the `.a` file by path (for example in external repos) still need to apply the appropriate platform-specific whole-archive/force-load flags themselves. #### 2. Test fixture @@ -275,7 +279,7 @@ protected: cfg.callsign = "MyPlugin"; cfg.locator = "libThunderMyPluginImpl.so"; cfg.classname = "MyPlugin"; - cfg.autostart = true; + cfg.startmode = TestCore::ThunderTestRuntime::PluginConfig::StartMode::Activated; cfg.startuporder = 50; cfg.configuration = R"({"root":{"mode":"Off","locator":"libThunderMyPluginImpl.so"}})"; plugins.push_back(cfg); @@ -366,11 +370,11 @@ The library is designed for use in GitHub Actions workflows. A typical workflow: A static archive ensures that all Thunder server symbols are available to the test binary at link time. Shared libraries would require careful `RPATH` management and could conflict with the installed Thunder daemon libraries. -### Why `--whole-archive`? +### Why whole-archive semantics? -Thunder uses static initializers extensively (`MODULE_NAME_DECLARATION`, `SERVICE_REGISTRATION`, singletons). Without `--whole-archive`, the linker would discard these symbols because the test binary doesn't reference them directly. The `--whole-archive` flag forces all object files from the archive to be included. +Thunder uses static initializers extensively (`MODULE_NAME_DECLARATION`, `SERVICE_REGISTRATION`, singletons). Without whole-archive semantics, the linker would discard these symbols because the test binary doesn't reference them directly. Whole-archive/force-load style linker options force all object files from the archive to be included. -This is enforced automatically via `target_link_options(INTERFACE)` on the CMake target, scoped to the archive using `$` so it doesn't affect other libraries on the link line. Consumers linking against the CMake target get this for free. Consumers linking against the `.a` file by path must add the flags manually. +This is enforced automatically via `target_link_options(INTERFACE)` on the CMake target, scoped to the archive using `$` so it doesn't affect other libraries on the link line. The exact linker option is platform-dependent: Apple uses `-Wl,-force_load,...`, while Linux/Android with GNU/Clang-family toolchains use `--whole-archive` / `--no-whole-archive`. Consumers linking against the CMake target get this automatically; consumers linking against the `.a` file by path must add the appropriate platform-specific flags manually. ### Why exclude `PluginHost.cpp`? @@ -390,6 +394,12 @@ This is enforced automatically via `target_link_options(INTERFACE)` on the CMake Using port 0 in the config tells the OS to assign an available port, avoiding conflicts when multiple test processes run simultaneously. +This is convenient for parallel runs, but it has an important caveat in the current implementation: the assigned port is not propagated back into `PluginHost::Config`, so Thunder's configured accessor/URL can still report port `0` even though the listener is actually bound to an ephemeral port. + +In practice, this is usually acceptable for `thunder_test_support` because tests typically invoke JSON-RPC in-process via `IDispatcher::Invoke()` instead of routing through the HTTP listener. However, it can be confusing when inspecting the reported accessor URL and may break plugins or tests that depend on the configured port being accurate. + +A more complete solution would be to either choose a free port before building the config, or query the bound port after `Server::Open()` and update the config/binder/accessor state accordingly. + --- ## Smoke Test From 552ce6022651067c2c86288d3f444269a6dc7424 Mon Sep 17 00:00:00 2001 From: Sankalp Maneshwar Date: Wed, 15 Apr 2026 12:57:22 +0530 Subject: [PATCH 10/13] Update Test_Thunder_Test_Support.yml --- .github/workflows/Test_Thunder_Test_Support.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/Test_Thunder_Test_Support.yml b/.github/workflows/Test_Thunder_Test_Support.yml index 7468047f42..bf27e6cb09 100644 --- a/.github/workflows/Test_Thunder_Test_Support.yml +++ b/.github/workflows/Test_Thunder_Test_Support.yml @@ -50,6 +50,7 @@ jobs: uses: actions/checkout@v4 with: path: Thunder + ref: ${{ github.event.pull_request.head.sha || github.sha }} - name: Checkout ThunderTools - default if: ${{ !contains(github.event.pull_request.body, '[DependsOn=ThunderTools:') }} From 24fae29cef12c5768dc47f6487216000dc562692 Mon Sep 17 00:00:00 2001 From: smanes0213 Date: Wed, 15 Apr 2026 15:50:46 +0530 Subject: [PATCH 11/13] Resolve copilot review comments --- Tests/CMakeLists.txt | 10 ++- Tests/test_support/CMakeLists.txt | 6 +- Tests/test_support/Module.cpp | 9 +- Tests/test_support/ThunderTestRuntime.h | 3 +- Tests/test_support/tests/SmokeTest.cpp | 2 - docs/ThunderTestSupport/ThunderTestSupport.md | 83 ++++++++++++++++--- 6 files changed, 89 insertions(+), 24 deletions(-) diff --git a/Tests/CMakeLists.txt b/Tests/CMakeLists.txt index 15fac32899..c5aca48987 100644 --- a/Tests/CMakeLists.txt +++ b/Tests/CMakeLists.txt @@ -44,5 +44,13 @@ if(UNRAVELLER) endif() if(ENABLE_TEST_RUNTIME) - add_subdirectory(test_support) + if(UNIX) + add_subdirectory(test_support) + else() + message(FATAL_ERROR + "ENABLE_TEST_RUNTIME is supported only on POSIX platforms. " + "Tests/test_support currently depends on POSIX-specific temporary " + "directory handling." + ) + endif() endif() \ No newline at end of file diff --git a/Tests/test_support/CMakeLists.txt b/Tests/test_support/CMakeLists.txt index b2ad62fffe..69dc2c0327 100644 --- a/Tests/test_support/CMakeLists.txt +++ b/Tests/test_support/CMakeLists.txt @@ -7,13 +7,17 @@ # standalone Thunder daemon. # # Usage: -# 1. Link your test executable against thunder_test_support. +# 1. Link your test executable against thunder_test_support when the test is +# built in the same CMake build as Thunder. # 2. Use ThunderTestRuntime::Initialize() to start the embedded server. # 3. Call JSON-RPC or COM-RPC methods directly against loaded plugins. # 4. Call ThunderTestRuntime::Deinitialize() when done. # # NOTE: PluginHost.cpp is deliberately excluded — it contains main(). # The test binary provides its own main() via GTest. +# NOTE: The install rules below publish only the archive and public header. +# They do not export a CMake package target for external find_package() +# consumers. # ============================================================================ find_package(Threads REQUIRED) diff --git a/Tests/test_support/Module.cpp b/Tests/test_support/Module.cpp index 5049edfd53..4202ae8bf2 100644 --- a/Tests/test_support/Module.cpp +++ b/Tests/test_support/Module.cpp @@ -1,12 +1,9 @@ // Module definition for the thunder_test_support static library. // -// Uses MODULE_NAME_ARCHIVE_DECLARATION instead of MODULE_NAME_DECLARATION -// because this is a static archive, not a standalone binary. The archive -// macro only defines the MODULE_NAME string symbol. The full declaration -// (ModuleBuildRef, GetModuleServices, SetModuleServices) is left to the -// consumer's own MODULE_NAME_DECLARATION, avoiding duplicate definitions. +// MODULE_NAME is supplied by the target compile definitions so the embedded +// Source/Thunder objects and this archive-level declaration use the same +// module name. -#define MODULE_NAME ThunderTestRuntime #include MODULE_NAME_ARCHIVE_DECLARATION diff --git a/Tests/test_support/ThunderTestRuntime.h b/Tests/test_support/ThunderTestRuntime.h index 5b89884121..27bbc48152 100644 --- a/Tests/test_support/ThunderTestRuntime.h +++ b/Tests/test_support/ThunderTestRuntime.h @@ -30,7 +30,8 @@ // } // ========================================================================== -#include +#include +#include #include #include #include diff --git a/Tests/test_support/tests/SmokeTest.cpp b/Tests/test_support/tests/SmokeTest.cpp index da85c4de56..b2c878f570 100644 --- a/Tests/test_support/tests/SmokeTest.cpp +++ b/Tests/test_support/tests/SmokeTest.cpp @@ -6,8 +6,6 @@ // No external plugin .so files are needed. // ============================================================================ -#define MODULE_NAME SmokeTest - #include #include "ThunderTestRuntime.h" diff --git a/docs/ThunderTestSupport/ThunderTestSupport.md b/docs/ThunderTestSupport/ThunderTestSupport.md index e40866a985..0e68843dde 100644 --- a/docs/ThunderTestSupport/ThunderTestSupport.md +++ b/docs/ThunderTestSupport/ThunderTestSupport.md @@ -9,10 +9,28 @@ The **thunder_test_support** library enables in-process integration testing of T | Property | Value | |----------|-------| | Library type | Static archive (`libthunder_test_support.a`) | -| CMake option | `ENABLE_TEST_RUNTIME=ON` | +| CMake option | `ENABLE_TEST_RUNTIME=ON` (POSIX platforms only) | | Location | `Tests/test_support/` | | Public header | `ThunderTestRuntime.h` | | Install paths | `${CMAKE_INSTALL_LIBDIR}/libthunder_test_support.a`, `${CMAKE_INSTALL_INCLUDEDIR}/thunder_test_support/ThunderTestRuntime.h` | +| Installed CMake package | Not exported; installation publishes the archive and header only | + +--- + +## Why This Exists + +This library was added to eliminate the need for repo-local Thunder mock layers in tests. + +Without a reusable embedded Thunder runtime, repositories typically end up recreating enough Thunder behavior locally to get code under test running at all. That usually means carrying custom mocks or shims for host services, COM link behavior, worker-pool setup, factories, module plumbing, JSON-RPC wiring, and other framework-owned pieces. + +`thunder_test_support` replaces that Thunder-specific scaffolding with a reusable in-process Thunder runtime: + +- tests link one reusable library instead of maintaining local Thunder mocks +- code under test runs against a real embedded `PluginHost::Server` +- JSON-RPC and COM-RPC calls use real Thunder plumbing rather than hand-built host/service stubs +- consumers no longer need to recreate Thunder host behavior just to exercise their code + +The goal is to stop faking Thunder itself. Tests may still keep mocks for their own non-Thunder dependencies, but the Thunder runtime layer should be provided by this library rather than rebuilt in each repository. --- @@ -95,30 +113,31 @@ endif() This follows the same pattern used by other test options (`LOADER_TEST`, `WORKERPOOL_TEST`, etc.) — off by default, enabled explicitly. +`ENABLE_TEST_RUNTIME` is currently supported only on POSIX platforms. CMake rejects it on non-POSIX platforms instead of attempting a partial Windows build. + ### 2. `Tests/test_support/CMakeLists.txt` (New) Builds the `thunder_test_support` static library. Key design decisions: - **Source files**: Compiles `ThunderTestRuntime.cpp`, `Module.cpp`, plus Thunder server sources directly from `Source/Thunder/` (PluginServer, Controller, SystemInfo, PostMortem, Probe). - **Excludes `PluginHost.cpp`**: This contains `main()` and would conflict with the test binary's entry point. -- **Compile definitions**: Sets `APPLICATION_NAME=ThunderTestRuntime` and `THREADPOOL_COUNT=4`. +- **Compile definitions**: Sets `APPLICATION_NAME=ThunderTestRuntime`, `MODULE_NAME=ThunderTestRuntime`, and `THREADPOOL_COUNT=4`. - **Public dependencies**: Exposes `ThunderCore`, `ThunderCryptalgo`, `ThunderCOM`, `ThunderPlugins`, `ThunderMessaging`, `ThunderWebSocket`, `ThunderCOMProcess`, and `Threads` as PUBLIC link dependencies, so consumers automatically get all required libraries. - **Private dependencies**: `CompileSettings` is linked PRIVATE to apply Thunder's compile flags without propagating them. - **Conditional features**: Supports `WARNING_REPORTING`, `PROCESSCONTAINERS`, and `HIBERNATESUPPORT` when enabled. -- **Install rules**: Installs the `.a` archive to `${CMAKE_INSTALL_LIBDIR}` and the header to `${CMAKE_INSTALL_INCLUDEDIR}/thunder_test_support/`. +- **Install rules**: Installs the `.a` archive to `${CMAKE_INSTALL_LIBDIR}` and the header to `${CMAKE_INSTALL_INCLUDEDIR}/thunder_test_support/`. It does not export a `thunder_test_support` CMake package/target for `find_package()` consumers. ### 3. `Tests/test_support/Module.cpp` (New) -Module definition required by Thunder's internal logging/tracing macros. Uses `MODULE_NAME_ARCHIVE_DECLARATION` instead of `MODULE_NAME_DECLARATION` because this is a static archive, not a standalone binary. The archive macro only defines the `MODULE_NAME` string symbol. The full declaration (`ModuleBuildRef`, `GetModuleServices`, `SetModuleServices`) is left to the consumer's own `MODULE_NAME_DECLARATION`, avoiding duplicate definitions at link time: +Module definition required by Thunder's internal logging/tracing macros. Uses `MODULE_NAME_ARCHIVE_DECLARATION` instead of `MODULE_NAME_DECLARATION` because this is a static archive, not a standalone binary. The archive macro only defines the `MODULE_NAME` string symbol. The full declaration (`ModuleBuildRef`, `GetModuleServices`, `SetModuleServices`) is left to the consumer's own `MODULE_NAME_DECLARATION`, avoiding duplicate definitions at link time. The `thunder_test_support` target supplies `MODULE_NAME=ThunderTestRuntime` through target compile definitions, and this translation unit emits the archive-level symbol for that module name: ```cpp -#define MODULE_NAME ThunderTestRuntime #include MODULE_NAME_ARCHIVE_DECLARATION ``` -Consumer binaries (test executables) must provide their own `Module.cpp` with the full declaration: +Each final consumer binary (for example, each test executable) must provide exactly one dedicated `Module.cpp` with the full declaration: ```cpp #define MODULE_NAME MyTestName @@ -127,6 +146,8 @@ Consumer binaries (test executables) must provide their own `Module.cpp` with th MODULE_NAME_DECLARATION(BUILD_REFERENCE) ``` +This is a per-binary requirement, not a per-source-file requirement. Test sources that include `ThunderTestRuntime.h` do not need to define `MODULE_NAME` themselves as long as the executable includes one such `Module.cpp`. + ### 4. `Tests/test_support/ThunderTestRuntime.h` (New) Public API header. Defines the `Thunder::TestCore::ThunderTestRuntime` class: @@ -205,7 +226,7 @@ The caller receives a real COM-RPC interface pointer and must call `Release()` w | Option | Default | Description | |--------|---------|-------------| -| `ENABLE_TEST_RUNTIME` | `OFF` | Build the thunder_test_support static library | +| `ENABLE_TEST_RUNTIME` | `OFF` | Build the thunder_test_support static library on POSIX platforms | | `BUILD_TESTS` | `OFF` | Build Thunder unit tests (independent of test runtime) | ### Build Command @@ -225,13 +246,15 @@ After installation, the library and header are available at: /${CMAKE_INSTALL_INCLUDEDIR}/thunder_test_support/ThunderTestRuntime.h ``` +No `thunder_test_support` CMake package config or exported target is installed today. External consumers should treat this as an archive-plus-header install and add any required whole-archive/force-load flags themselves. + --- ## Usage Guide ### Writing a Plugin Integration Test -#### 1. CMakeLists.txt for the test +#### 1. CMakeLists.txt for the test built in the same Thunder build ```cmake find_package(Thunder REQUIRED) @@ -248,8 +271,11 @@ target_link_libraries(ThunderMyPluginImpl PRIVATE ${NAMESPACE}Definitions::${NAMESPACE}Definitions ) -# Build the test executable -add_executable(my_plugin_test MyPluginTest.cpp) +# Build the test executable. Include one dedicated Module.cpp for this binary. +add_executable(my_plugin_test + MyPluginTest.cpp + tests/Module.cpp +) target_link_libraries(my_plugin_test PRIVATE thunder_test_support gmock_main @@ -257,7 +283,38 @@ target_link_libraries(my_plugin_test PRIVATE ) ``` -> **Note**: The `thunder_test_support` target carries `INTERFACE` link options that automatically enforce whole-archive semantics for its own archive. On Apple this uses `-Wl,-force_load,...`; on Linux/Android with GNU/Clang-family toolchains it uses `--whole-archive` / `--no-whole-archive`. This ensures Thunder's static initializers (`MODULE_NAME_DECLARATION`, `SERVICE_REGISTRATION`) are preserved without the consumer needing to specify linker flags manually when linking against the CMake target. Consumers linking against the `.a` file by path (for example in external repos) still need to apply the appropriate platform-specific whole-archive/force-load flags themselves. +Example `tests/Module.cpp` for the test executable: + +```cpp +#define MODULE_NAME MyPluginTest +#include + +MODULE_NAME_DECLARATION(BUILD_REFERENCE) +``` + +> **Note**: The `thunder_test_support` target and its `INTERFACE` whole-archive link options are available only to targets built in the same CMake project as Thunder with `ENABLE_TEST_RUNTIME=ON`. They are not exported by the install rules. External repositories that consume the installed archive must link the `.a` file explicitly and apply the appropriate platform-specific whole-archive/force-load flags themselves. + +#### 1a. External consumer linking against the installed archive + +```cmake +find_package(Thunder REQUIRED) +find_package(ThunderDefinitions REQUIRED) + +add_executable(my_plugin_test MyPluginTest.cpp) + +target_include_directories(my_plugin_test PRIVATE + /path/to/install/${CMAKE_INSTALL_INCLUDEDIR}/thunder_test_support +) + +target_link_libraries(my_plugin_test PRIVATE + /path/to/install/${CMAKE_INSTALL_LIBDIR}/libthunder_test_support.a + gmock_main + ${NAMESPACE}Definitions::${NAMESPACE}Definitions +) + +# Add the platform-specific whole-archive / force-load option for +# libthunder_test_support.a here when linking from an external project. +``` #### 2. Test fixture @@ -374,7 +431,7 @@ A static archive ensures that all Thunder server symbols are available to the te Thunder uses static initializers extensively (`MODULE_NAME_DECLARATION`, `SERVICE_REGISTRATION`, singletons). Without whole-archive semantics, the linker would discard these symbols because the test binary doesn't reference them directly. Whole-archive/force-load style linker options force all object files from the archive to be included. -This is enforced automatically via `target_link_options(INTERFACE)` on the CMake target, scoped to the archive using `$` so it doesn't affect other libraries on the link line. The exact linker option is platform-dependent: Apple uses `-Wl,-force_load,...`, while Linux/Android with GNU/Clang-family toolchains use `--whole-archive` / `--no-whole-archive`. Consumers linking against the CMake target get this automatically; consumers linking against the `.a` file by path must add the appropriate platform-specific flags manually. +For in-tree Thunder builds, this is enforced automatically via `target_link_options(INTERFACE)` on the local CMake target, scoped to the archive using `$` so it doesn't affect other libraries on the link line. The exact linker option is platform-dependent: Apple uses `-Wl,-force_load,...`, while Linux/Android with GNU/Clang-family toolchains use `--whole-archive` / `--no-whole-archive`. Because the install rules do not export a `thunder_test_support` CMake target, external consumers of the installed `.a` file must add the appropriate platform-specific flags manually. ### Why exclude `PluginHost.cpp`? @@ -406,7 +463,7 @@ A more complete solution would be to either choose a free port before building t A self-contained smoke test (`Tests/test_support/tests/SmokeTest.cpp`) is included with the library. It verifies that the library links, boots, and can exercise JSON-RPC against the built-in Controller plugin — no external plugin `.so` files needed. -The smoke test is built automatically when `ENABLE_TEST_RUNTIME=ON` and GTest is available. It includes a `Module.cpp` with `MODULE_NAME_DECLARATION(BUILD_REFERENCE)` — required because the library uses `MODULE_NAME_ARCHIVE_DECLARATION`. It covers: +The smoke test is built automatically when `ENABLE_TEST_RUNTIME=ON` and GTest is available. The smoke-test executable includes one dedicated `Module.cpp` with `MODULE_NAME_DECLARATION(BUILD_REFERENCE)` — required because the library uses `MODULE_NAME_ARCHIVE_DECLARATION`. `SmokeTest.cpp` itself does not need to define `MODULE_NAME`. It covers: - **ControllerStatus** — calls `Controller.1.status` and verifies a non-empty response containing "Controller" - **ControllerSubsystems** — calls `Controller.1.subsystems` and verifies a non-empty response From c532dbb2e23c94f22d10ac2cf96e5801e844d95c Mon Sep 17 00:00:00 2001 From: smanes0213 Date: Thu, 16 Apr 2026 15:02:37 +0530 Subject: [PATCH 12/13] Resolve Review comments --- Tests/test_support/CMakeLists.txt | 2 + Tests/test_support/ThunderTestRuntime.cpp | 277 ++++++++++-------- Tests/test_support/ThunderTestRuntime.h | 28 +- Tests/test_support/tests/SmokeTest.cpp | 4 +- docs/ThunderTestSupport/ThunderTestSupport.md | 47 +-- 5 files changed, 193 insertions(+), 165 deletions(-) diff --git a/Tests/test_support/CMakeLists.txt b/Tests/test_support/CMakeLists.txt index 69dc2c0327..64aff14b4e 100644 --- a/Tests/test_support/CMakeLists.txt +++ b/Tests/test_support/CMakeLists.txt @@ -45,6 +45,8 @@ target_compile_definitions(thunder_test_support APPLICATION_NAME=ThunderTestRuntime MODULE_NAME=ThunderTestRuntime THREADPOOL_COUNT=${THREADPOOL_COUNT} + DEFAULT_SYSTEM_PATH="${SYSTEM_PATH}" + DEFAULT_PROXYSTUB_PATH="${PROXYSTUB_PATH}" ) target_compile_options(thunder_test_support PRIVATE -Wno-psabi) diff --git a/Tests/test_support/ThunderTestRuntime.cpp b/Tests/test_support/ThunderTestRuntime.cpp index 97cc0f0bb1..b7789c1f46 100644 --- a/Tests/test_support/ThunderTestRuntime.cpp +++ b/Tests/test_support/ThunderTestRuntime.cpp @@ -3,17 +3,17 @@ #include #include #include -#include +#include // ========================================================================== // ThunderTestRuntime implementation // // Lifecycle: Initialize() -> [run tests] -> Deinitialize() // -// Initialize creates a unique /tmp/thunder_test_XXXXXX/ directory tree, +// Initialize creates a unique temp directory tree under the platform temp root, // writes a minimal Thunder config.json, parses it into PluginHost::Config, // constructs PluginHost::Server, and calls Server::Open() which boots -// the controller and activates auto-start plugins. +// the controller and activates configured plugins. // // Deinitialize reverses the process: Server::Close(), cleanup temp files. // ========================================================================== @@ -61,104 +61,96 @@ namespace TestCore { } } - // Helper to escape a string for safe inclusion as a JSON string value. - // It escapes quotes, backslashes, and control characters (< 0x20). - static std::string JsonEscape(const std::string& input) + static string TemporaryRootPath() { - std::string output; - output.reserve(input.size()); - const char* hex = "0123456789abcdef"; - - for (unsigned char c : input) { - switch (c) { - case '"': - output += "\\\""; - break; - case '\\': - output += "\\\\"; - break; - case '\b': - output += "\\b"; - break; - case '\f': - output += "\\f"; - break; - case '\n': - output += "\\n"; - break; - case '\r': - output += "\\r"; - break; - case '\t': - output += "\\t"; - break; - default: - if (c < 0x20) { - output += "\\u00"; - output += hex[(c >> 4) & 0x0F]; - output += hex[c & 0x0F]; - } else { - output += static_cast(c); - } - break; + string path; + static const char* variables[] = { "TMPDIR", "TEMP", "TMP" }; + + for (const char* variable : variables) { + if ((Core::SystemInfo::GetEnvironment(variable, path) == true) && (path.empty() == false)) { + return Core::Directory::Normalize(path); } } - return output; +#ifdef __WINDOWS__ + return Core::Directory::Normalize("c:/temp"); +#else + return Core::Directory::Normalize("/tmp"); +#endif } - static const char* ToStartModeString(const ThunderTestRuntime::PluginConfig::StartMode startMode) + static bool CreateUniqueTemporaryDirectory(string& path) { - switch (startMode) { - case ThunderTestRuntime::PluginConfig::StartMode::Activated: - return "Activated"; - case ThunderTestRuntime::PluginConfig::StartMode::Deactivated: - return "Deactivated"; - case ThunderTestRuntime::PluginConfig::StartMode::Unavailable: - return "Unavailable"; - default: - return "Activated"; + const string root = TemporaryRootPath(); + + if (root.empty() == true) { + return false; + } + + Core::Directory rootDirectory(root.c_str()); + if ((rootDirectory.Exists() == false) && (rootDirectory.CreatePath() == false)) { + TRACE_L1("ThunderTestRuntime: Failed to create temporary root '%s'", root.c_str()); + return false; + } + + const string processId = Core::NumberType(static_cast(Core::ProcessInfo().Id())).Text(); + const string ticks = Core::NumberType(Core::Time::Now().Ticks()).Text(); + + for (uint8_t attempt = 0; attempt < 32; ++attempt) { + const string candidate = root + "thunder_test_" + processId + '_' + ticks + '_' + Core::NumberType(attempt).Text(); + Core::Directory directory(candidate.c_str()); + + if ((directory.Exists() == false) && (directory.Create() == true)) { + path = Core::Directory::Normalize(candidate); + return true; + } } + + TRACE_L1("ThunderTestRuntime: Failed to create unique temporary directory under '%s'", root.c_str()); + return false; } string ThunderTestRuntime::BuildConfigJSON(const std::vector& plugins, const string& systemPath, const string& proxyStubPath) const { - const string communicatorPath = _tempDir + "communicator|0777"; - - std::ostringstream json; - json << "{" - << "\"port\":0," - << "\"binding\":\"127.0.0.1\"," - << "\"idletime\":180," - << "\"persistentpath\":\"" << JsonEscape(_tempDir + "persistent/") << "\"," - << "\"volatilepath\":\"" << JsonEscape(_tempDir + "volatile/") << "\"," - << "\"datapath\":\"" << JsonEscape(_tempDir + "data/") << "\"," - << "\"systempath\":\"" << JsonEscape(systemPath) << "\"," - << "\"proxystubpath\":\"" << JsonEscape(proxyStubPath) << "\"," - << "\"communicator\":\"" << JsonEscape(communicatorPath) << "\"," - << "\"plugins\":["; - - for (size_t i = 0; i < plugins.size(); ++i) { - const auto& p = plugins[i]; - if (i > 0) json << ","; - json << "{" - << "\"callsign\":\"" << JsonEscape(p.callsign) << "\"," - << "\"locator\":\"" << JsonEscape(p.locator) << "\"," - << "\"classname\":\"" << JsonEscape(p.classname) << "\"," - << "\"startuporder\":" << p.startuporder << "," - << "\"startmode\":\"" << ToStartModeString(p.startmode) << "\""; - - if (!p.configuration.empty()) { - json << ",\"configuration\":" << p.configuration; + const string communicatorPath = _tempDir + "communicator|0777"; + + JsonObject config; + JsonArray pluginList; + + config["port"] = 0; + config["binding"] = "127.0.0.1"; + config["idletime"] = 180; + config["persistentpath"] = _tempDir + "persistent/"; + config["volatilepath"] = _tempDir + "volatile/"; + config["datapath"] = _tempDir + "data/"; + config["systempath"] = systemPath; + config["proxystubpath"] = proxyStubPath; + config["communicator"] = communicatorPath; + + for (const auto& plugin : plugins) { + + string serializedPluginConfig; + JsonValue pluginValue; + Core::OptionalType error; + + plugin.ToString(serializedPluginConfig); + + if ((pluginValue.FromString(serializedPluginConfig, error) == false) || (pluginValue.IsValid() == false)) { + TRACE_L1("ThunderTestRuntime: Failed to serialize configuration for plugin '%s'", plugin.Callsign.Value().c_str()); + return string(); } - json << "}"; + pluginList.Add(pluginValue); } - json << "]}"; - return json.str(); + config["plugins"] = pluginList; + + string json; + config.ToString(json); + + return json; } uint32_t ThunderTestRuntime::Initialize(const std::vector& plugins, @@ -169,13 +161,10 @@ namespace TestCore { return Core::ERROR_ALREADY_CONNECTED; } - // Create unique temp directory for this test run - char tempTemplate[] = "/tmp/thunder_test_XXXXXX"; - char* tempResult = mkdtemp(tempTemplate); - if (tempResult == nullptr) { + // Create a unique temp directory for this test run using Thunder Core helpers. + if (CreateUniqueTemporaryDirectory(_tempDir) == false) { return Core::ERROR_GENERAL; } - _tempDir = string(tempResult) + "/"; if (CreateDirectories() == false) { CleanupDirectories(); @@ -184,26 +173,22 @@ namespace TestCore { } // Determine system path for plugin .so files - string sysPath = systemPath; - if (sysPath.empty()) { - sysPath = "/usr/lib/wpeframework/plugins/"; - } - if (sysPath.back() != '/') { - sysPath += '/'; - } + string sysPath = systemPath.empty() + ? Core::Directory::Normalize(DEFAULT_SYSTEM_PATH) + : Core::Directory::Normalize(systemPath); // Determine proxy stub path - _proxyStubPath = proxyStubPath; - if (_proxyStubPath.empty()) { - // Default: look next to system path - _proxyStubPath = sysPath + "../proxystubs/"; - } - if (_proxyStubPath.back() != '/') { - _proxyStubPath += '/'; - } + _proxyStubPath = proxyStubPath.empty() + ? Core::Directory::Normalize(DEFAULT_PROXYSTUB_PATH) + : Core::Directory::Normalize(proxyStubPath); // Build and write config to temp file string configJSON = BuildConfigJSON(plugins, sysPath, _proxyStubPath); + if (configJSON.empty()) { + CleanupDirectories(); + _tempDir.clear(); + return Core::ERROR_INCOMPLETE_CONFIG; + } _configFilePath = _tempDir + "config.json"; { @@ -233,6 +218,16 @@ namespace TestCore { return Core::ERROR_INCOMPLETE_CONFIG; } + // Initialize the messaging subsystem (must happen before Server creation, + // mirrors the MessagingInitialization() call in the real PluginHost main()). + Messaging::MessageUnit::Settings::Config messagingConfig; + uint32_t messagingResult = Messaging::MessageUnit::Instance().Open( + _config->VolatilePath(), messagingConfig, false, Messaging::MessageUnit::flush::OFF); + + if (messagingResult != Core::ERROR_NONE) { + TRACE_L1("ThunderTestRuntime: Failed to initialize messaging subsystem (0x%08X)", messagingResult); + } + // Create and start the server _server = new PluginHost::Server(*_config, false); _server->Open(); @@ -243,36 +238,68 @@ namespace TestCore { // Invoke a JSON-RPC method synchronously via the in-process dispatcher. // Bypasses HTTP/WebSocket — calls IDispatcher::Invoke() directly. // Derives the callsign from the method string (text before the first '.'). - uint32_t ThunderTestRuntime::InvokeJSONRPC(const string& method, - const string& params, string& response) + uint32_t ThunderTestRuntime::Invoke(const string& method, + const string& params, string& response) { - if (_server == nullptr) { - return Core::ERROR_ILLEGAL_STATE; - } + uint32_t result = Core::ERROR_ILLEGAL_STATE; - size_t dot = method.find('.'); - if (dot == string::npos) { - return Core::ERROR_INVALID_SIGNATURE; - } - string callsign = method.substr(0, dot); + if (_server != nullptr) { - Core::ProxyType shell; - uint32_t result = _server->Services().FromIdentifier(callsign, shell); - if (result != Core::ERROR_NONE) { - return result; - } + string callsign = Core::JSONRPC::Message::Callsign(method); + string methodName = Core::JSONRPC::Message::Method(method); - PluginHost::IDispatcher* dispatcher = shell->QueryInterface(); - if (dispatcher == nullptr) { - return Core::ERROR_UNAVAILABLE; - } + if (callsign.empty() == true) { + result = Core::ERROR_INVALID_SIGNATURE; + } else { + + Core::ProxyType shell; + result = _server->Services().FromIdentifier(callsign, shell); + + if (result == Core::ERROR_NONE) { + + PluginHost::IDispatcher* dispatcher = shell->QueryInterface(); + + if (dispatcher == nullptr) { + result = Core::ERROR_UNAVAILABLE; + } else { - result = dispatcher->Invoke(0, 0, string(), method, params, response); - dispatcher->Release(); + if (MethodExists(dispatcher, callsign, methodName) == false) { + result = Core::ERROR_UNKNOWN_KEY; + } else { + result = dispatcher->Invoke(0, 0, string(), method, params, response); + } + + dispatcher->Release(); + } + } + } + } return result; } + bool ThunderTestRuntime::MethodExists(PluginHost::IDispatcher* dispatcher, + const string& callsign, + const string& methodName) const + { + bool found = false; + + JsonObject existsParams; + existsParams["method"] = methodName; + + string serializedParams; + existsParams.ToString(serializedParams); + + string existsResponse; + dispatcher->Invoke(0, 0, string(), callsign + ".exists", serializedParams, existsResponse); + + Core::JSON::Boolean available; + available.FromString(existsResponse); + found = available.Value(); + + return found; + } + Core::ProxyType ThunderTestRuntime::GetShell(const string& callsign) { Core::ProxyType shell; @@ -309,6 +336,8 @@ namespace TestCore { _config = nullptr; } + Messaging::MessageUnit::Instance().Close(); + if (!_configFilePath.empty()) { Core::File(_configFilePath).Destroy(); _configFilePath.clear(); diff --git a/Tests/test_support/ThunderTestRuntime.h b/Tests/test_support/ThunderTestRuntime.h index 27bbc48152..9967ef6c6a 100644 --- a/Tests/test_support/ThunderTestRuntime.h +++ b/Tests/test_support/ThunderTestRuntime.h @@ -22,7 +22,7 @@ // // TEST_F(MyTest, JsonRpc) { // string resp; -// _runtime.InvokeJSONRPC("Callsign.1.method", params, resp); +// _runtime.Invoke("Callsign.method", params, resp); // } // // TEST_F(MyTest, ComRpc) { @@ -33,6 +33,7 @@ #include #include #include +#include #include #include @@ -47,22 +48,9 @@ namespace TestCore { class ThunderTestRuntime { public: - // Describes a plugin to be loaded by the test runtime. - // Maps directly to a Thunder plugin JSON config entry. - struct PluginConfig { - enum class StartMode { - Activated, - Deactivated, - Unavailable - }; - - string callsign; // Plugin callsign (e.g. "Counter") - string locator; // Shared library name (e.g. "libThunderCounterImplementation.so") - string classname; // Class name registered via SERVICE_REGISTRATION - StartMode startmode = StartMode::Activated; // Thunder plugin start mode - int startuporder = 50; // Activation priority (lower = earlier) - string configuration; // Optional JSON object for plugin-specific config - }; + // Reuse the real Thunder plugin configuration type. + // Key fields: Callsign, Locator, ClassName, StartupOrder, StartMode, Configuration. + using PluginConfig = Plugin::Config; ThunderTestRuntime() = default; ~ThunderTestRuntime(); @@ -79,8 +67,9 @@ namespace TestCore { // Invoke a JSON-RPC method on a loaded plugin. // The callsign is derived from the method string (text before the first '.'). - // Method format: "Callsign.Version.method" (e.g. "MyPlugin.1.doSomething") - uint32_t InvokeJSONRPC(const string& method, const string& params, string& response); + // Method format: "Callsign.method" (e.g. "MyPlugin.doSomething") + // Returns Core::ERROR_UNAVAILABLE if the JSON-RPC endpoint is not available. + uint32_t Invoke(const string& method, const string& params, string& response); // Obtain a COM-RPC interface from a loaded plugin. // Caller must Release() the returned pointer when done. @@ -109,6 +98,7 @@ namespace TestCore { private: string BuildConfigJSON(const std::vector& plugins, const string& systemPath, const string& proxyStubPath) const; + bool MethodExists(PluginHost::IDispatcher* dispatcher, const string& callsign, const string& methodName) const; bool CreateDirectories() const; void CleanupDirectories() const; diff --git a/Tests/test_support/tests/SmokeTest.cpp b/Tests/test_support/tests/SmokeTest.cpp index b2c878f570..3a8bd90d87 100644 --- a/Tests/test_support/tests/SmokeTest.cpp +++ b/Tests/test_support/tests/SmokeTest.cpp @@ -32,7 +32,7 @@ TestCore::ThunderTestRuntime SmokeTest::_runtime; // Verify the server booted and we can query Controller status TEST_F(SmokeTest, ControllerStatus) { string response; - uint32_t result = _runtime.InvokeJSONRPC("Controller.1.status", "", response); + uint32_t result = _runtime.Invoke("Controller.status", "", response); EXPECT_EQ(result, Core::ERROR_NONE); EXPECT_FALSE(response.empty()); // Response should contain Controller's own entry @@ -42,7 +42,7 @@ TEST_F(SmokeTest, ControllerStatus) { // Verify we can query subsystems TEST_F(SmokeTest, ControllerSubsystems) { string response; - uint32_t result = _runtime.InvokeJSONRPC("Controller.1.subsystems", "", response); + uint32_t result = _runtime.Invoke("Controller.subsystems", "", response); EXPECT_EQ(result, Core::ERROR_NONE); EXPECT_FALSE(response.empty()); } diff --git a/docs/ThunderTestSupport/ThunderTestSupport.md b/docs/ThunderTestSupport/ThunderTestSupport.md index 0e68843dde..c55a9c5507 100644 --- a/docs/ThunderTestSupport/ThunderTestSupport.md +++ b/docs/ThunderTestSupport/ThunderTestSupport.md @@ -71,7 +71,7 @@ In production, Thunder runs as a standalone daemon (`PluginHost.cpp` → `main() 3. **Wraps server lifecycle** — `ThunderTestRuntime` provides `Initialize()` and `Deinitialize()` to manage the server, replacing the daemon's startup/shutdown sequence. -4. **Generates config on the fly** — instead of reading `/etc/Thunder/config.json`, the runtime builds a minimal JSON config programmatically and writes it to a temporary directory. +4. **Generates config on the fly** — instead of reading `/etc/Thunder/config.json`, the runtime builds a minimal JSON config using `Core::JSON` containers (`JsonObject`, `JsonArray`) and `Plugin::Config` serialization, then writes it to a temporary directory. 5. **Still opens the listener** — `PluginHost::Server::Open()` still starts the HTTP/WebSocket listener, typically bound to `127.0.0.1` on an ephemeral port in the test runtime. @@ -121,7 +121,7 @@ Builds the `thunder_test_support` static library. Key design decisions: - **Source files**: Compiles `ThunderTestRuntime.cpp`, `Module.cpp`, plus Thunder server sources directly from `Source/Thunder/` (PluginServer, Controller, SystemInfo, PostMortem, Probe). - **Excludes `PluginHost.cpp`**: This contains `main()` and would conflict with the test binary's entry point. -- **Compile definitions**: Sets `APPLICATION_NAME=ThunderTestRuntime`, `MODULE_NAME=ThunderTestRuntime`, and `THREADPOOL_COUNT=4`. +- **Compile definitions**: Sets `APPLICATION_NAME=ThunderTestRuntime`, `MODULE_NAME=ThunderTestRuntime`, `THREADPOOL_COUNT=4`, `DEFAULT_SYSTEM_PATH`, and `DEFAULT_PROXYSTUB_PATH` (derived from Thunder's `SYSTEM_PATH` and `PROXYSTUB_PATH` CMake variables). - **Public dependencies**: Exposes `ThunderCore`, `ThunderCryptalgo`, `ThunderCOM`, `ThunderPlugins`, `ThunderMessaging`, `ThunderWebSocket`, `ThunderCOMProcess`, and `Threads` as PUBLIC link dependencies, so consumers automatically get all required libraries. - **Private dependencies**: `CompileSettings` is linked PRIVATE to apply Thunder's compile flags without propagating them. - **Conditional features**: Supports `WARNING_REPORTING`, `PROCESSCONTAINERS`, and `HIBERNATESUPPORT` when enabled. @@ -154,14 +154,14 @@ Public API header. Defines the `Thunder::TestCore::ThunderTestRuntime` class: | Member | Description | |--------|-------------| -| `PluginConfig` (struct) | Describes a plugin to load: callsign, locator (.so name), classname, startmode, startuporder, configuration JSON. `startmode` supports Thunder's `Activated`, `Deactivated`, and `Unavailable` states. | -| `Initialize()` | Boots the embedded server with given plugins, system path, and proxy stub path | -| `InvokeJSONRPC()` | Calls a JSON-RPC method synchronously via in-process `IDispatcher::Invoke()`. Callsign is derived from the method string. | +| `PluginConfig` | Type alias for `Plugin::Config`. Reuses Thunder's own plugin configuration container — no custom struct needed. Key fields: `Callsign`, `Locator`, `ClassName`, `StartupOrder`, `StartMode`, `Configuration`. | +| `Initialize()` | Boots the embedded server with given plugins, system path, and proxy stub path. Initializes the messaging subsystem before server creation. | +| `Invoke()` | Calls a JSON-RPC method synchronously via in-process `IDispatcher::Invoke()`. Callsign and method are parsed using `Core::JSONRPC::Message` helpers. Checks method availability via the built-in `exists` endpoint before dispatch. | | `GetInterface()` | Template: obtains a COM-RPC interface from a plugin via `QueryInterface()` | | `GetShell()` | Returns the `IShell` proxy for a plugin (for activation/deactivation control) | | `Server()` | Direct access to the underlying `PluginHost::Server` | | `CommunicatorPath()` | Returns the UNIX domain socket path | -| `Deinitialize()` | Stops the server, releases config, cleans up temp directories | +| `Deinitialize()` | Stops the server, closes messaging, releases config, cleans up temp directories | ### 5. `Tests/test_support/ThunderTestRuntime.cpp` (New) @@ -169,11 +169,13 @@ Implementation with the following lifecycle: ``` Initialize() - ├── Create temp dir: /tmp/thunder_test_XXXXXX/ + ├── Create unique temp dir via Core helpers (process ID + ticks) ├── Create subdirs: persistent/, volatile/, data/ - ├── Build JSON config string from PluginConfig list + ├── Resolve system/proxystub paths from CMake defaults or caller args + ├── Build JSON config using Core::JSON + Plugin::Config serialization ├── Write config to temp dir ├── Parse config into PluginHost::Config + ├── Open Messaging::MessageUnit (before server creation) ├── Construct PluginHost::Server └── Call Server::Open() ├── Set up security @@ -188,22 +190,27 @@ Deinitialize() │ └── Close connections ├── Delete Server ├── Delete Config + ├── Close Messaging::MessageUnit ├── Remove config file └── Remove temp directories ``` #### JSON-RPC Invocation Path -`InvokeJSONRPC()` bypasses HTTP/WebSocket entirely: +`Invoke()` bypasses HTTP/WebSocket entirely: ``` -InvokeJSONRPC(".1.", params, response) - ├── Extract callsign from method string (text before first '.') +Invoke(".", params, response) + ├── Core::JSONRPC::Message::Callsign(method) → callsign + ├── Core::JSONRPC::Message::Method(method) → methodName ├── Services().FromIdentifier(callsign) → IShell proxy ├── shell->QueryInterface() → dispatcher + ├── MethodExists(dispatcher, callsign, methodName) via built-in "exists" └── dispatcher->Invoke(0, 0, "", method, params, response) ``` +The method string uses unversioned format: `"Callsign.method"` (e.g. `"Controller.status"`). Callsign and method name are parsed using Thunder's `Core::JSONRPC::Message` helpers. Before dispatch, the built-in `exists` endpoint is queried to verify the method is registered — returns `Core::ERROR_UNKNOWN_KEY` if not found. + This calls the plugin's JSON-RPC handler directly in-process, with zero network overhead. The HTTP/WebSocket listener is still started by `Server::Open()`, but the helper API does not route JSON-RPC through it. #### COM-RPC Interface Access @@ -333,12 +340,12 @@ protected: std::vector plugins; TestCore::ThunderTestRuntime::PluginConfig cfg; - cfg.callsign = "MyPlugin"; - cfg.locator = "libThunderMyPluginImpl.so"; - cfg.classname = "MyPlugin"; - cfg.startmode = TestCore::ThunderTestRuntime::PluginConfig::StartMode::Activated; - cfg.startuporder = 50; - cfg.configuration = R"({"root":{"mode":"Off","locator":"libThunderMyPluginImpl.so"}})"; + cfg.Callsign = "MyPlugin"; + cfg.Locator = "libThunderMyPluginImpl.so"; + cfg.ClassName = "MyPlugin"; + cfg.StartMode = PluginHost::IShell::startmode::ACTIVATED; + cfg.StartupOrder = 50; + cfg.Configuration = R"({"root":{"mode":"Off","locator":"libThunderMyPluginImpl.so"}})"; plugins.push_back(cfg); uint32_t result = _runtime.Initialize(plugins, @@ -358,7 +365,7 @@ TestCore::ThunderTestRuntime MyPluginTest::_runtime; TEST_F(MyPluginTest, JsonRpcCall) { string response; EXPECT_EQ(Core::ERROR_NONE, - _runtime.InvokeJSONRPC("MyPlugin.1.someMethod", R"({"param":1})", response)); + _runtime.Invoke("MyPlugin.someMethod", R"({"param":1})", response)); // Validate response... } @@ -465,8 +472,8 @@ A self-contained smoke test (`Tests/test_support/tests/SmokeTest.cpp`) is includ The smoke test is built automatically when `ENABLE_TEST_RUNTIME=ON` and GTest is available. The smoke-test executable includes one dedicated `Module.cpp` with `MODULE_NAME_DECLARATION(BUILD_REFERENCE)` — required because the library uses `MODULE_NAME_ARCHIVE_DECLARATION`. `SmokeTest.cpp` itself does not need to define `MODULE_NAME`. It covers: -- **ControllerStatus** — calls `Controller.1.status` and verifies a non-empty response containing "Controller" -- **ControllerSubsystems** — calls `Controller.1.subsystems` and verifies a non-empty response +- **ControllerStatus** — calls `Controller.status` and verifies a non-empty response containing "Controller" +- **ControllerSubsystems** — calls `Controller.subsystems` and verifies a non-empty response - **GetControllerShell** — obtains the Controller's `IShell` and verifies it is in `ACTIVATED` state Running: From 61cfb192d3ad6106c7ebc233e39881c4c2450e1b Mon Sep 17 00:00:00 2001 From: smanes0213 Date: Thu, 16 Apr 2026 15:14:48 +0530 Subject: [PATCH 13/13] Add missing header in the ThunderTestRuntime.h --- Tests/test_support/ThunderTestRuntime.h | 1 + 1 file changed, 1 insertion(+) diff --git a/Tests/test_support/ThunderTestRuntime.h b/Tests/test_support/ThunderTestRuntime.h index 9967ef6c6a..d0b86d9cc0 100644 --- a/Tests/test_support/ThunderTestRuntime.h +++ b/Tests/test_support/ThunderTestRuntime.h @@ -34,6 +34,7 @@ #include #include #include +#include #include #include