From e9fbfad0a4c2dd5e9763ff291fd1c5a524644276 Mon Sep 17 00:00:00 2001 From: smanes0213 Date: Thu, 16 Apr 2026 14:51:44 +0530 Subject: [PATCH 01/22] WIP: test support library --- .../workflows/Test_Thunder_Test_Support.yml | 112 +++++ Tests/CMakeLists.txt | 5 + Tests/test_support/CMakeLists.txt | 98 ++++ Tests/test_support/Module.cpp | 9 + Tests/test_support/ThunderTestRuntime.cpp | 455 ++++++++++++++++++ Tests/test_support/ThunderTestRuntime.h | 182 +++++++ Tests/test_support/tests/CMakeLists.txt | 23 + Tests/test_support/tests/Module.cpp | 5 + Tests/test_support/tests/SmokeTest.cpp | 123 +++++ 9 files changed, 1012 insertions(+) create mode 100644 .github/workflows/Test_Thunder_Test_Support.yml 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 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..a624b00932 --- /dev/null +++ b/.github/workflows/Test_Thunder_Test_Support.yml @@ -0,0 +1,112 @@ +name: Test Thunder Test Support Library + +permissions: + contents: read + +on: + workflow_dispatch: + push: + branches: ["R5.3"] + paths: + - 'Tests/test_support/**' + - 'Source/Thunder/**' + - 'Source/core/**' + - 'Source/plugins/**' + pull_request: + branches: ["R5.3"] + 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 + ref: ${{ github.event.pull_request.head.sha || github.sha }} + + - 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_CXX17=OFF \ + -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/Tests/CMakeLists.txt b/Tests/CMakeLists.txt index 79a328ea9c..220f102291 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 the in-process Thunder test runtime library" 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..e2c483d351 --- /dev/null +++ b/Tests/test_support/CMakeLists.txt @@ -0,0 +1,98 @@ +# ========================================================================== +# thunder_test_support — static library for in-process Thunder plugin testing +# +# Compiles the test runtime together with the subset of Source/Thunder +# objects needed to host an embedded PluginHost::Server. The resulting +# archive is linked via whole-archive so that MODULE_NAME_DECLARATION +# statics are not dropped by the linker. +# ========================================================================== + +set(TARGET thunder_test_support) + +add_library(${TARGET} STATIC + ThunderTestRuntime.cpp + Module.cpp + ${CMAKE_SOURCE_DIR}/Source/Thunder/PluginServer.cpp + ${CMAKE_SOURCE_DIR}/Source/Thunder/Controller.cpp + ${CMAKE_SOURCE_DIR}/Source/Thunder/SystemInfo.cpp + ${CMAKE_SOURCE_DIR}/Source/Thunder/PostMortem.cpp + ${CMAKE_SOURCE_DIR}/Source/Thunder/Probe.cpp +) + +target_include_directories(${TARGET} + PUBLIC + $ + PRIVATE + ${CMAKE_SOURCE_DIR}/Source/Thunder +) + +target_compile_definitions(${TARGET} + PUBLIC + NAMESPACE=${NAMESPACE} + APPLICATION_NAME=ThunderTestRuntime + MODULE_NAME=ThunderTestRuntime + THREADPOOL_COUNT=${THREADPOOL_COUNT} + PRIVATE + DEFAULT_SYSTEM_PATH="${SYSTEM_PATH}" + DEFAULT_PROXYSTUB_PATH="${PROXYSTUB_PATH}" +) + +target_link_libraries(${TARGET} + 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 + CompileSettingsDebug::CompileSettingsDebug + COMProcess::COMProcess + Threads::Threads +) + +# ------------------------------------------------------------------ +# Whole-archive link. Ensures that the MODULE_NAME_DECLARATION +# constructors from the Server objects are not discarded. +# ------------------------------------------------------------------ +if(APPLE) + target_link_options(${TARGET} INTERFACE + "SHELL:-Wl,-force_load,$" + ) +else() + target_link_options(${TARGET} INTERFACE + "SHELL:-Wl,--whole-archive $ -Wl,--no-whole-archive" + ) +endif() + +# ------------------------------------------------------------------ +# Optional features – only link when enabled in the main build. +# ------------------------------------------------------------------ +if(WARNING_REPORTING) + target_link_libraries(${TARGET} + PRIVATE + ${NAMESPACE}WarningReporting::${NAMESPACE}WarningReporting + ) + target_sources(${TARGET} + PRIVATE + ${CMAKE_SOURCE_DIR}/Source/Thunder/WarningReportingCategories.cpp + ) +endif() + +if(PROCESSCONTAINERS) + target_link_libraries(${TARGET} + PRIVATE + ${NAMESPACE}ProcessContainers::${NAMESPACE}ProcessContainers + ) +endif() + +if(HIBERNATESUPPORT) + target_link_libraries(${TARGET} + PRIVATE + ${NAMESPACE}Hibernate::${NAMESPACE}Hibernate + ) +endif() + +# ------------------------------------------------------------------ +# Smoke test sub-directory +# ------------------------------------------------------------------ +add_subdirectory(tests) diff --git a/Tests/test_support/Module.cpp b/Tests/test_support/Module.cpp new file mode 100644 index 0000000000..4202ae8bf2 --- /dev/null +++ b/Tests/test_support/Module.cpp @@ -0,0 +1,9 @@ +// Module definition for the thunder_test_support static library. +// +// 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. + +#include + +MODULE_NAME_ARCHIVE_DECLARATION diff --git a/Tests/test_support/ThunderTestRuntime.cpp b/Tests/test_support/ThunderTestRuntime.cpp new file mode 100644 index 0000000000..34884e2512 --- /dev/null +++ b/Tests/test_support/ThunderTestRuntime.cpp @@ -0,0 +1,455 @@ +#include "ThunderTestRuntime.h" + +#include +#include +#include +#include + +// ========================================================================== +// ThunderTestRuntime implementation +// +// Lifecycle: Initialize() -> [run tests] -> Deinitialize() +// +// 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 configured plugins. +// +// Deinitialize reverses the process: Server::Close(), cleanup temp files. +// ========================================================================== + +namespace Thunder { +namespace TestCore { + + // ================================================================== + // JSONRPCLink implementation + // ================================================================== + + ThunderTestRuntime::JSONRPCLink::JSONRPCLink(ThunderTestRuntime& runtime, const string& callsign) + : _runtime(runtime) + , _callsign(callsign) + , _dispatcher(nullptr) + { + Core::ProxyType shell = _runtime.GetShell(_callsign); + if (shell.IsValid()) { + _dispatcher = shell->QueryInterface(); + } + } + + ThunderTestRuntime::JSONRPCLink::~JSONRPCLink() + { + // Unsubscribe from all active events + if (_dispatcher != nullptr) { + std::lock_guard guard(_lock); + for (const auto& entry : _handlers) { + _dispatcher->Unsubscribe(this, entry.first, _callsign, string()); + } + _handlers.clear(); + _dispatcher->Release(); + } + } + + uint32_t ThunderTestRuntime::JSONRPCLink::Invoke(const string& method, + const string& params, + string& response) + { + uint32_t result = Core::ERROR_UNAVAILABLE; + + if (_dispatcher != nullptr) { + + string fullMethod = _callsign + '.' + method; + + if (_runtime.MethodExists(_dispatcher, _callsign, method) == false) { + result = Core::ERROR_UNKNOWN_KEY; + } else { + result = _dispatcher->Invoke(0, 0, string(), fullMethod, params, response); + } + } + + return result; + } + + uint32_t ThunderTestRuntime::JSONRPCLink::Subscribe(const string& event, EventHandler handler) + { + uint32_t result = Core::ERROR_UNAVAILABLE; + + if (_dispatcher != nullptr) { + + result = _dispatcher->Subscribe(this, event, _callsign, string()); + + if (result == Core::ERROR_NONE) { + std::lock_guard guard(_lock); + _handlers[event] = std::move(handler); + } + } + + return result; + } + + uint32_t ThunderTestRuntime::JSONRPCLink::Unsubscribe(const string& event) + { + uint32_t result = Core::ERROR_UNAVAILABLE; + + if (_dispatcher != nullptr) { + + result = _dispatcher->Unsubscribe(this, event, _callsign, string()); + + std::lock_guard guard(_lock); + _handlers.erase(event); + } + + return result; + } + + Core::hresult ThunderTestRuntime::JSONRPCLink::Event(const string& event, + const string& /* designator */, + const string& /* index */, + const string& parameters) + { + std::lock_guard guard(_lock); + + auto it = _handlers.find(event); + if (it != _handlers.end()) { + it->second(parameters); + } + + return Core::ERROR_NONE; + } + + // ================================================================== + // ThunderTestRuntime implementation + // ================================================================== + + ThunderTestRuntime::~ThunderTestRuntime() + { + Deinitialize(); + } + + bool ThunderTestRuntime::CreateDirectories() const + { + 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() == false) { + Core::Directory(_tempDir.c_str()).Destroy(); + + // Core::Directory::Destroy() does not remove the directory if the path + // ends with a trailing separator. Strip it before the final call. + string path = _tempDir; + while ((path.empty() == false) && (path.back() == '/' || path.back() == '\\')) { + path.pop_back(); + } + if (path.empty() == false) { + Core::Directory(path.c_str()).Destroy(); + } + } + } + + static string TemporaryRootPath() + { + 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); + } + } + +#ifdef __WINDOWS__ + return Core::Directory::Normalize("c:/temp"); +#else + return Core::Directory::Normalize("/tmp"); +#endif + } + + static bool CreateUniqueTemporaryDirectory(string& path) + { + 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"; + + 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(); + } + + pluginList.Add(pluginValue); + } + + config["plugins"] = pluginList; + + string json; + config.ToString(json); + + return json; + } + + uint32_t ThunderTestRuntime::Initialize(const std::vector& plugins, + const string& systemPath, + const string& proxyStubPath) + { + if (_server != nullptr) { + return Core::ERROR_ALREADY_CONNECTED; + } + + // Create a unique temp directory for this test run using Thunder Core helpers. + if (CreateUniqueTemporaryDirectory(_tempDir) == false) { + return Core::ERROR_GENERAL; + } + + if (CreateDirectories() == false) { + CleanupDirectories(); + _tempDir.clear(); + return Core::ERROR_OPENING_FAILED; + } + + // Determine system path for plugin .so files + string sysPath = systemPath.empty() + ? Core::Directory::Normalize(DEFAULT_SYSTEM_PATH) + : Core::Directory::Normalize(systemPath); + + // Determine proxy stub path + _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"; + + { + 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; + } + + // 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(); + + return Core::ERROR_NONE; + } + + ThunderTestRuntime::JSONRPCLink* ThunderTestRuntime::JSONRPCLink(const string& callsign) + { + return new class JSONRPCLink(*this, callsign); + } + + // Invoke a JSON-RPC method synchronously via the in-process dispatcher. + // Bypasses HTTP/WebSocket — calls IDispatcher::Invoke() directly. + // Callsign and method are parsed using Core::JSONRPC::Message helpers. + uint32_t ThunderTestRuntime::Invoke(const string& method, + const string& params, string& response) + { + uint32_t result = Core::ERROR_ILLEGAL_STATE; + + if (_server != nullptr) { + + string callsign = Core::JSONRPC::Message::Callsign(method); + string methodName = Core::JSONRPC::Message::Method(method); + + 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 { + + 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; + 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::Deinitialize() + { + if (_server != nullptr) { + _server->Close(); + delete _server; + _server = nullptr; + } + + if (_config != nullptr) { + delete _config; + _config = nullptr; + } + + Messaging::MessageUnit::Instance().Close(); + + if (_configFilePath.empty() == false) { + 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..e5447d32f9 --- /dev/null +++ b/Tests/test_support/ThunderTestRuntime.h @@ -0,0 +1,182 @@ +#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, event handling, +// and COM-RPC queries +// - Tears everything down cleanly on Deinitialize() +// +// Typical usage in a GTest fixture: +// +// static ThunderTestRuntime _runtime; +// +// static void SetUpTestSuite() { +// std::vector plugins = { ... }; +// _runtime.Initialize(plugins, pluginPath, proxyStubPath); +// } +// static void TearDownTestSuite() { _runtime.Deinitialize(); } +// +// TEST_F(MyTest, JsonRpc) { +// string resp; +// auto link = _runtime.JSONRPCLink("MyPlugin"); +// link.Invoke("someMethod", R"({"param":1})", resp); +// } +// +// TEST_F(MyTest, Events) { +// auto link = _runtime.JSONRPCLink("MyPlugin"); +// link.Subscribe("onSomethingChanged", +// [](const string& params) { /* handle event */ }); +// // ... trigger event ... +// link.Unsubscribe("onSomethingChanged"); +// } +// +// TEST_F(MyTest, ComRpc) { +// auto* iface = _runtime.GetInterface("MyPlugin"); +// } +// ========================================================================== + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Thunder { + +namespace PluginHost { + class Server; + class Config; +} + +namespace TestCore { + + class ThunderTestRuntime { + public: + // Reuse the real Thunder plugin configuration type. + // Key fields: Callsign, Locator, ClassName, StartupOrder, StartMode, Configuration. + using PluginConfig = Plugin::Config; + + // ------------------------------------------------------------------ + // JSONRPCLink — callsign-bound helper for JSON-RPC calls and events + // ------------------------------------------------------------------ + class JSONRPCLink : public PluginHost::IDispatcher::ICallback { + public: + using EventHandler = std::function; + + JSONRPCLink(ThunderTestRuntime& runtime, const string& callsign); + ~JSONRPCLink() override; + + JSONRPCLink(const JSONRPCLink&) = delete; + JSONRPCLink& operator=(const JSONRPCLink&) = delete; + + // JSON-RPC method invocation (callsign is implicit). + // Method is the bare method name (e.g. "getLogLevel"), not "Callsign.getLogLevel". + uint32_t Invoke(const string& method, const string& params, string& response); + + // Subscribe to a JSON-RPC event with a callback. + uint32_t Subscribe(const string& event, EventHandler handler); + + // Unsubscribe from a previously subscribed event. + uint32_t Unsubscribe(const string& event); + + const string& Callsign() const { return _callsign; } + + // IDispatcher::ICallback + void AddRef() const override {} + uint32_t Release() const override { return Core::ERROR_NONE; } + + private: + // IDispatcher::ICallback + Core::hresult Event(const string& event, const string& designator, + const string& index, const string& parameters) override; + + BEGIN_INTERFACE_MAP(JSONRPCLink) + INTERFACE_ENTRY(PluginHost::IDispatcher::ICallback) + END_INTERFACE_MAP + + ThunderTestRuntime& _runtime; + string _callsign; + PluginHost::IDispatcher* _dispatcher; + + mutable std::mutex _lock; + std::unordered_map _handlers; + }; + + 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 = ""); + + // Create a callsign-bound JSON-RPC link for invoke and event operations. + // Caller owns the returned object. + JSONRPCLink* JSONRPCLink(const string& callsign); + + // Invoke a JSON-RPC method on a loaded plugin (full designator form). + // 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. + 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 domain socket path used by the communicator. + string CommunicatorPath() const; + + // Stop the server, release config, and clean up temp directories. + void Deinitialize(); + + 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; + + PluginHost::Config* _config = nullptr; + PluginHost::Server* _server = nullptr; + string _tempDir; + string _configFilePath; + string _proxyStubPath; + }; + +} // namespace TestCore +} // namespace Thunder diff --git a/Tests/test_support/tests/CMakeLists.txt b/Tests/test_support/tests/CMakeLists.txt new file mode 100644 index 0000000000..5e6463e706 --- /dev/null +++ b/Tests/test_support/tests/CMakeLists.txt @@ -0,0 +1,23 @@ +set(TARGET thunder_test_runtime_smoke) + +find_package(GTest REQUIRED) + +add_executable(${TARGET} + SmokeTest.cpp + Module.cpp +) + +target_compile_definitions(${TARGET} + PRIVATE + MODULE_NAME=SmokeTest + BUILD_REFERENCE=${BUILD_REFERENCE} +) + +target_link_libraries(${TARGET} + PRIVATE + thunder_test_support + GTest::GTest + GTest::Main +) + +add_test(NAME ${TARGET} COMMAND ${TARGET}) diff --git a/Tests/test_support/tests/Module.cpp b/Tests/test_support/tests/Module.cpp new file mode 100644 index 0000000000..5f7e111ab5 --- /dev/null +++ b/Tests/test_support/tests/Module.cpp @@ -0,0 +1,5 @@ +#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..27332b9ee3 --- /dev/null +++ b/Tests/test_support/tests/SmokeTest.cpp @@ -0,0 +1,123 @@ +// ========================================================================== +// SmokeTest — validates the ThunderTestRuntime boots and tears down cleanly. +// +// These tests exercise: +// 1. Invoke() with the full "Callsign.method" form +// 2. JSONRPCLink (callsign-bound) invocation +// 3. GetShell() COM-RPC path +// ========================================================================== + +#include "ThunderTestRuntime.h" +#include +#include +#include + +namespace Thunder { +namespace TestCore { +namespace Tests { + + class SmokeTest : public ::testing::Test { + protected: + static ThunderTestRuntime _runtime; + + static void SetUpTestSuite() + { + 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(); + } + }; + + ThunderTestRuntime SmokeTest::_runtime; + + // ------------------------------------------------------------------ + // Full-designator Invoke: "Controller.status" + // ------------------------------------------------------------------ + TEST_F(SmokeTest, ControllerStatusViaFullDesignator) + { + string response; + uint32_t result = _runtime.Invoke("Controller.status", "{}", response); + EXPECT_EQ(result, Core::ERROR_NONE) << "Controller.status returned: " << result; + EXPECT_FALSE(response.empty()); + } + + // ------------------------------------------------------------------ + // Full-designator Invoke: "Controller.subsystems" + // ------------------------------------------------------------------ + TEST_F(SmokeTest, ControllerSubsystemsViaFullDesignator) + { + string response; + uint32_t result = _runtime.Invoke("Controller.subsystems", "{}", response); + EXPECT_EQ(result, Core::ERROR_NONE) << "Controller.subsystems returned: " << result; + EXPECT_FALSE(response.empty()); + } + + // ------------------------------------------------------------------ + // JSONRPCLink (callsign-bound): invoke without repeating callsign + // ------------------------------------------------------------------ + TEST_F(SmokeTest, ControllerStatusViaJSONRPCLink) + { + auto* controller = _runtime.JSONRPCLink("Controller"); + ASSERT_NE(controller, nullptr); + + string response; + uint32_t result = controller->Invoke("status", "{}", response); + EXPECT_EQ(result, Core::ERROR_NONE) << "status via link returned: " << result; + EXPECT_FALSE(response.empty()); + + delete controller; + } + + // ------------------------------------------------------------------ + // GetShell: obtain the IShell for the Controller + // ------------------------------------------------------------------ + TEST_F(SmokeTest, GetControllerShell) + { + auto shell = _runtime.GetShell("Controller"); + EXPECT_TRUE(shell.IsValid()) << "Controller IShell must be available"; + } + + // ------------------------------------------------------------------ + // Unknown method returns ERROR_UNKNOWN_KEY + // ------------------------------------------------------------------ + TEST_F(SmokeTest, UnknownMethodReturnsError) + { + string response; + uint32_t result = _runtime.Invoke("Controller.thisMethodDoesNotExist", "{}", response); + EXPECT_EQ(result, Core::ERROR_UNKNOWN_KEY); + } + + // ------------------------------------------------------------------ + // Unknown method via JSONRPCLink + // ------------------------------------------------------------------ + TEST_F(SmokeTest, UnknownMethodViaJSONRPCLinkReturnsError) + { + auto* controller = _runtime.JSONRPCLink("Controller"); + ASSERT_NE(controller, nullptr); + + string response; + uint32_t result = controller->Invoke("thisMethodDoesNotExist", "{}", response); + EXPECT_EQ(result, Core::ERROR_UNKNOWN_KEY); + + delete controller; + } + + // ------------------------------------------------------------------ + // Missing callsign in full-designator form returns error + // ------------------------------------------------------------------ + TEST_F(SmokeTest, MissingCallsignReturnsError) + { + string response; + uint32_t result = _runtime.Invoke("noCallsignDot", "{}", response); + EXPECT_NE(result, Core::ERROR_NONE); + } + +} // namespace Tests +} // namespace TestCore +} // namespace Thunder From 5f0f65c64f45c6228a1057463e54de02398861ca Mon Sep 17 00:00:00 2001 From: smanes0213 Date: Fri, 17 Apr 2026 10:16:39 +0530 Subject: [PATCH 02/22] Resolve compilation issues --- .../workflows/Test_Thunder_Test_Support.yml | 3 +- Tests/test_support/CMakeLists.txt | 63 +++++++++++++------ Tests/test_support/ThunderTestRuntime.cpp | 2 +- Tests/test_support/ThunderTestRuntime.h | 4 +- Tests/test_support/tests/SmokeTest.cpp | 4 +- 5 files changed, 51 insertions(+), 25 deletions(-) diff --git a/.github/workflows/Test_Thunder_Test_Support.yml b/.github/workflows/Test_Thunder_Test_Support.yml index a624b00932..8d8f05f21b 100644 --- a/.github/workflows/Test_Thunder_Test_Support.yml +++ b/.github/workflows/Test_Thunder_Test_Support.yml @@ -50,7 +50,7 @@ jobs: uses: actions/checkout@v4 with: path: Thunder - ref: ${{ github.event.pull_request.head.sha || github.sha }} + # ref: ${{ github.event.pull_request.head.sha || github.sha }} - name: Checkout ThunderTools - default if: ${{ !contains(github.event.pull_request.body, '[DependsOn=ThunderTools:') }} @@ -93,7 +93,6 @@ 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 diff --git a/Tests/test_support/CMakeLists.txt b/Tests/test_support/CMakeLists.txt index e2c483d351..01a8edfee9 100644 --- a/Tests/test_support/CMakeLists.txt +++ b/Tests/test_support/CMakeLists.txt @@ -7,36 +7,60 @@ # statics are not dropped by the linker. # ========================================================================== +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") + set(TARGET thunder_test_support) add_library(${TARGET} STATIC ThunderTestRuntime.cpp Module.cpp - ${CMAKE_SOURCE_DIR}/Source/Thunder/PluginServer.cpp - ${CMAKE_SOURCE_DIR}/Source/Thunder/Controller.cpp - ${CMAKE_SOURCE_DIR}/Source/Thunder/SystemInfo.cpp - ${CMAKE_SOURCE_DIR}/Source/Thunder/PostMortem.cpp - ${CMAKE_SOURCE_DIR}/Source/Thunder/Probe.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 ) target_include_directories(${TARGET} PUBLIC - $ + ${CMAKE_CURRENT_SOURCE_DIR} PRIVATE - ${CMAKE_SOURCE_DIR}/Source/Thunder + ${THUNDER_SOURCE_DIR} + $ + $ ) target_compile_definitions(${TARGET} - PUBLIC + PRIVATE NAMESPACE=${NAMESPACE} APPLICATION_NAME=ThunderTestRuntime MODULE_NAME=ThunderTestRuntime THREADPOOL_COUNT=${THREADPOOL_COUNT} - PRIVATE DEFAULT_SYSTEM_PATH="${SYSTEM_PATH}" DEFAULT_PROXYSTUB_PATH="${PROXYSTUB_PATH}" ) +target_compile_options(${TARGET} PRIVATE -Wno-psabi) + +set_target_properties(${TARGET} PROPERTIES + CXX_STANDARD 11 + CXX_STANDARD_REQUIRED YES +) + +target_link_libraries(${TARGET} + PRIVATE + CompileSettings::CompileSettings + CompileSettingsDebug::CompileSettingsDebug +) + +if(EXCEPTION_CATCHING) + set_source_files_properties(${THUNDER_SOURCE_DIR}/PluginServer.cpp PROPERTIES COMPILE_FLAGS "-fexceptions") +endif() + target_link_libraries(${TARGET} PUBLIC ${NAMESPACE}Core::${NAMESPACE}Core @@ -45,8 +69,7 @@ target_link_libraries(${TARGET} ${NAMESPACE}Messaging::${NAMESPACE}Messaging ${NAMESPACE}WebSocket::${NAMESPACE}WebSocket ${NAMESPACE}Plugins::${NAMESPACE}Plugins - CompileSettingsDebug::CompileSettingsDebug - COMProcess::COMProcess + ${NAMESPACE}COMProcess::${NAMESPACE}COMProcess Threads::Threads ) @@ -68,28 +91,32 @@ endif() # Optional features – only link when enabled in the main build. # ------------------------------------------------------------------ if(WARNING_REPORTING) - target_link_libraries(${TARGET} - PRIVATE - ${NAMESPACE}WarningReporting::${NAMESPACE}WarningReporting - ) target_sources(${TARGET} PRIVATE - ${CMAKE_SOURCE_DIR}/Source/Thunder/WarningReportingCategories.cpp + ${THUNDER_SOURCE_DIR}/WarningReportingCategories.cpp ) endif() if(PROCESSCONTAINERS) target_link_libraries(${TARGET} - PRIVATE + PUBLIC ${NAMESPACE}ProcessContainers::${NAMESPACE}ProcessContainers ) + target_compile_definitions(${TARGET} + PUBLIC + PROCESSCONTAINERS_ENABLED=1 + ) endif() if(HIBERNATESUPPORT) target_link_libraries(${TARGET} - PRIVATE + PUBLIC ${NAMESPACE}Hibernate::${NAMESPACE}Hibernate ) + target_compile_definitions(${TARGET} + PUBLIC + HIBERNATE_SUPPORT_ENABLED=1 + ) endif() # ------------------------------------------------------------------ diff --git a/Tests/test_support/ThunderTestRuntime.cpp b/Tests/test_support/ThunderTestRuntime.cpp index 34884e2512..5623582eb1 100644 --- a/Tests/test_support/ThunderTestRuntime.cpp +++ b/Tests/test_support/ThunderTestRuntime.cpp @@ -334,7 +334,7 @@ namespace TestCore { return Core::ERROR_NONE; } - ThunderTestRuntime::JSONRPCLink* ThunderTestRuntime::JSONRPCLink(const string& callsign) + ThunderTestRuntime::JSONRPCLink* ThunderTestRuntime::CreateJSONRPCLink(const string& callsign) { return new class JSONRPCLink(*this, callsign); } diff --git a/Tests/test_support/ThunderTestRuntime.h b/Tests/test_support/ThunderTestRuntime.h index e5447d32f9..0c38e3800c 100644 --- a/Tests/test_support/ThunderTestRuntime.h +++ b/Tests/test_support/ThunderTestRuntime.h @@ -92,7 +92,7 @@ namespace TestCore { const string& Callsign() const { return _callsign; } // IDispatcher::ICallback - void AddRef() const override {} + uint32_t AddRef() const override { return Core::ERROR_NONE; } uint32_t Release() const override { return Core::ERROR_NONE; } private: @@ -129,7 +129,7 @@ namespace TestCore { // Create a callsign-bound JSON-RPC link for invoke and event operations. // Caller owns the returned object. - JSONRPCLink* JSONRPCLink(const string& callsign); + class JSONRPCLink* CreateJSONRPCLink(const string& callsign); // Invoke a JSON-RPC method on a loaded plugin (full designator form). // Method format: "Callsign.method" (e.g. "MyPlugin.doSomething") diff --git a/Tests/test_support/tests/SmokeTest.cpp b/Tests/test_support/tests/SmokeTest.cpp index 27332b9ee3..5ed057a14a 100644 --- a/Tests/test_support/tests/SmokeTest.cpp +++ b/Tests/test_support/tests/SmokeTest.cpp @@ -63,7 +63,7 @@ namespace Tests { // ------------------------------------------------------------------ TEST_F(SmokeTest, ControllerStatusViaJSONRPCLink) { - auto* controller = _runtime.JSONRPCLink("Controller"); + auto* controller = _runtime.CreateJSONRPCLink("Controller"); ASSERT_NE(controller, nullptr); string response; @@ -98,7 +98,7 @@ namespace Tests { // ------------------------------------------------------------------ TEST_F(SmokeTest, UnknownMethodViaJSONRPCLinkReturnsError) { - auto* controller = _runtime.JSONRPCLink("Controller"); + auto* controller = _runtime.CreateJSONRPCLink("Controller"); ASSERT_NE(controller, nullptr); string response; From e03f7a2cb784f19b567f92b2d00ae5b8099b7c1d Mon Sep 17 00:00:00 2001 From: smanes0213 Date: Mon, 20 Apr 2026 13:55:21 +0530 Subject: [PATCH 03/22] Add Test plugin test to workflow --- .../workflows/Test_Thunder_Test_Support.yml | 91 ++++++++- Tests/test_support/Module.cpp | 8 +- Tests/test_support/Module.h | 10 + Tests/test_support/ThunderTestRuntime.cpp | 2 +- Tests/test_support/tests/CMakeLists.txt | 33 +++ Tests/test_support/tests/Module.cpp | 4 +- Tests/test_support/tests/Module.h | 10 + Tests/test_support/tests/SmokeTest.cpp | 2 +- Tests/test_support/tests/TestPluginTest.cpp | 190 ++++++++++++++++++ 9 files changed, 334 insertions(+), 16 deletions(-) create mode 100644 Tests/test_support/Module.h create mode 100644 Tests/test_support/tests/Module.h create mode 100644 Tests/test_support/tests/TestPluginTest.cpp diff --git a/.github/workflows/Test_Thunder_Test_Support.yml b/.github/workflows/Test_Thunder_Test_Support.yml index 8d8f05f21b..2340e320b8 100644 --- a/.github/workflows/Test_Thunder_Test_Support.yml +++ b/.github/workflows/Test_Thunder_Test_Support.yml @@ -76,6 +76,58 @@ jobs: repository: rdkcentral/ThunderTools ref: ${{steps.tools.outputs.first_match}} + - name: Checkout ThunderInterfaces - default + if: ${{ !contains(github.event.pull_request.body, '[DependsOn=ThunderInterfaces:') }} + uses: actions/checkout@v4 + with: + path: ThunderInterfaces + repository: rdkcentral/ThunderInterfaces + # TODO: Remove ref once qa_interfaces changes are merged to master + ref: development/test-support + + - name: Regex ThunderInterfaces + if: ${{ contains(github.event.pull_request.body, '[DependsOn=ThunderInterfaces:') }} + id: interfaces + uses: AsasInnab/regex-action@v1 + with: + regex_pattern: '(?<=\[DependsOn=ThunderInterfaces:).*(?=\])' + regex_flags: 'gim' + search_string: ${{github.event.pull_request.body}} + + - name: Checkout ThunderInterfaces - ${{steps.interfaces.outputs.first_match}} + if: ${{ contains(github.event.pull_request.body, '[DependsOn=ThunderInterfaces:') }} + uses: actions/checkout@v4 + with: + path: ThunderInterfaces + repository: rdkcentral/ThunderInterfaces + ref: ${{steps.interfaces.outputs.first_match}} + + - name: Checkout ThunderNanoServices - default + if: ${{ !contains(github.event.pull_request.body, '[DependsOn=ThunderNanoServices:') }} + uses: actions/checkout@v4 + with: + path: ThunderNanoServices + repository: rdkcentral/ThunderNanoServices + # TODO: Remove ref once TestPlugin changes are merged to master + ref: development/test-support + + - name: Regex ThunderNanoServices + if: ${{ contains(github.event.pull_request.body, '[DependsOn=ThunderNanoServices:') }} + id: nanoservices + uses: AsasInnab/regex-action@v1 + with: + regex_pattern: '(?<=\[DependsOn=ThunderNanoServices:).*(?=\])' + regex_flags: 'gim' + search_string: ${{github.event.pull_request.body}} + + - name: Checkout ThunderNanoServices - ${{steps.nanoservices.outputs.first_match}} + if: ${{ contains(github.event.pull_request.body, '[DependsOn=ThunderNanoServices:') }} + uses: actions/checkout@v4 + with: + path: ThunderNanoServices + repository: rdkcentral/ThunderNanoServices + ref: ${{steps.nanoservices.outputs.first_match}} + # ----- Build ----- - name: Build ThunderTools run: | @@ -91,11 +143,32 @@ jobs: -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" \ + -DCMAKE_MODULE_PATH="${PWD}/${{matrix.build_type}}/install/usr/include/Thunder/Modules" \ -DPORT="0" \ - -DENABLE_TEST_RUNTIME=ON + -DENABLE_CXX17=OFF \ + -DENABLE_TEST_RUNTIME=ON \ + -DTEST_PLUGIN_PATH="${PWD}/${{matrix.build_type}}/install/usr/lib/thunder/plugins" cmake --build ${{matrix.build_type}}/build/Thunder --target install + - name: Build ThunderInterfaces + run: | + source venv/bin/activate + cmake -G Ninja -S ThunderInterfaces -B ${{matrix.build_type}}/build/ThunderInterfaces \ + -DCMAKE_BUILD_TYPE=${{matrix.build_type}} \ + -DCMAKE_INSTALL_PREFIX="${{matrix.build_type}}/install/usr" \ + -DCMAKE_MODULE_PATH="${PWD}/${{matrix.build_type}}/install/usr/include/Thunder/Modules" + cmake --build ${{matrix.build_type}}/build/ThunderInterfaces --target install + + - name: Build ThunderNanoServices TestPlugin + run: | + source venv/bin/activate + cmake -G Ninja -S ThunderNanoServices/tests -B ${{matrix.build_type}}/build/ThunderNanoServicesTests \ + -DCMAKE_BUILD_TYPE=${{matrix.build_type}} \ + -DCMAKE_INSTALL_PREFIX="${{matrix.build_type}}/install/usr" \ + -DCMAKE_MODULE_PATH="${PWD}/${{matrix.build_type}}/install/usr/include/Thunder/Modules" \ + -DPLUGIN_TESTPLUGIN=ON + cmake --build ${{matrix.build_type}}/build/ThunderNanoServicesTests --target install + # ----- Run smoke test ----- - name: Run smoke test run: | @@ -104,8 +177,18 @@ jobs: --gtest_output="xml:smoke-test-results.xml" \ --gtest_color=yes +# ----- Run plugin test ----- + - name: Run plugin test (COM-RPC + JSON-RPC) + run: | + LD_LIBRARY_PATH="${{matrix.build_type}}/install/usr/lib:${PWD}/${{matrix.build_type}}/install/usr/lib/thunder/plugins:$LD_LIBRARY_PATH" \ + ${{matrix.build_type}}/build/Thunder/Tests/test_support/tests/thunder_test_runtime_plugin \ + --gtest_output="xml:plugin-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 + name: test-results-${{matrix.build_type}} + path: | + smoke-test-results.xml + plugin-test-results.xml diff --git a/Tests/test_support/Module.cpp b/Tests/test_support/Module.cpp index 4202ae8bf2..e328ce1e4d 100644 --- a/Tests/test_support/Module.cpp +++ b/Tests/test_support/Module.cpp @@ -1,9 +1,3 @@ -// Module definition for the thunder_test_support static library. -// -// 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. - -#include +#include "Module.h" MODULE_NAME_ARCHIVE_DECLARATION diff --git a/Tests/test_support/Module.h b/Tests/test_support/Module.h new file mode 100644 index 0000000000..a54bdb5637 --- /dev/null +++ b/Tests/test_support/Module.h @@ -0,0 +1,10 @@ +#pragma once + +#ifndef MODULE_NAME +#define MODULE_NAME ThunderTestRuntime +#endif + +#include + +#undef EXTERNAL +#define EXTERNAL diff --git a/Tests/test_support/ThunderTestRuntime.cpp b/Tests/test_support/ThunderTestRuntime.cpp index 5623582eb1..c8eb660c2f 100644 --- a/Tests/test_support/ThunderTestRuntime.cpp +++ b/Tests/test_support/ThunderTestRuntime.cpp @@ -1,7 +1,7 @@ +#include "Module.h" #include "ThunderTestRuntime.h" #include -#include #include #include diff --git a/Tests/test_support/tests/CMakeLists.txt b/Tests/test_support/tests/CMakeLists.txt index 5e6463e706..67b7f32e44 100644 --- a/Tests/test_support/tests/CMakeLists.txt +++ b/Tests/test_support/tests/CMakeLists.txt @@ -21,3 +21,36 @@ target_link_libraries(${TARGET} ) add_test(NAME ${TARGET} COMMAND ${TARGET}) + +# ------------------------------------------------------------------ +# TestPlugin integration test +# +# Requires: +# - ThunderInterfaces qa_interfaces headers installed (for ITestPlugin.h) +# - ThunderNanoServices TestPlugin .so built and path passed via +# TEST_PLUGIN_PATH (defaults to the Thunder install plugins dir) +# ------------------------------------------------------------------ +set(PLUGIN_TEST_TARGET thunder_test_runtime_plugin) + +set(TEST_PLUGIN_PATH "" CACHE PATH "Directory containing libThunderTestPlugin.so") + +add_executable(${PLUGIN_TEST_TARGET} + TestPluginTest.cpp + Module.cpp +) + +target_compile_definitions(${PLUGIN_TEST_TARGET} + PRIVATE + MODULE_NAME=TestPluginTest + BUILD_REFERENCE=${BUILD_REFERENCE} + TEST_PLUGIN_PATH="${TEST_PLUGIN_PATH}" +) + +target_link_libraries(${PLUGIN_TEST_TARGET} + PRIVATE + thunder_test_support + GTest::GTest + GTest::Main +) + +add_test(NAME ${PLUGIN_TEST_TARGET} COMMAND ${PLUGIN_TEST_TARGET}) diff --git a/Tests/test_support/tests/Module.cpp b/Tests/test_support/tests/Module.cpp index 5f7e111ab5..2d85ed902b 100644 --- a/Tests/test_support/tests/Module.cpp +++ b/Tests/test_support/tests/Module.cpp @@ -1,5 +1,3 @@ -#define MODULE_NAME SmokeTest - -#include +#include "Module.h" MODULE_NAME_DECLARATION(BUILD_REFERENCE) diff --git a/Tests/test_support/tests/Module.h b/Tests/test_support/tests/Module.h new file mode 100644 index 0000000000..c8b64442c0 --- /dev/null +++ b/Tests/test_support/tests/Module.h @@ -0,0 +1,10 @@ +#pragma once + +#ifndef MODULE_NAME +#define MODULE_NAME SmokeTest +#endif + +#include + +#undef EXTERNAL +#define EXTERNAL diff --git a/Tests/test_support/tests/SmokeTest.cpp b/Tests/test_support/tests/SmokeTest.cpp index 5ed057a14a..ef75e81797 100644 --- a/Tests/test_support/tests/SmokeTest.cpp +++ b/Tests/test_support/tests/SmokeTest.cpp @@ -7,9 +7,9 @@ // 3. GetShell() COM-RPC path // ========================================================================== +#include "Module.h" #include "ThunderTestRuntime.h" #include -#include #include namespace Thunder { diff --git a/Tests/test_support/tests/TestPluginTest.cpp b/Tests/test_support/tests/TestPluginTest.cpp new file mode 100644 index 0000000000..3104e550fa --- /dev/null +++ b/Tests/test_support/tests/TestPluginTest.cpp @@ -0,0 +1,190 @@ +// ========================================================================== +// TestPluginTest — validates that the test runtime can load a plugin +// and interact with it via both COM-RPC and JSON-RPC. +// +// COM-RPC tests use QueryInterface to call methods directly. +// JSON-RPC tests use ThunderTestRuntime::Invoke() and JSONRPCLink. +// +// The TestPlugin is built as a shared library and placed in +// ${CMAKE_BINARY_DIR}/test_plugins/. The test passes that directory as +// the systemPath so the embedded server can dlopen it. +// ========================================================================== + +#include "Module.h" +#include "ThunderTestRuntime.h" +#include +#include +#include + +#ifndef TEST_PLUGIN_PATH +#error "TEST_PLUGIN_PATH must be defined by CMake" +#endif + +namespace Thunder { +namespace TestCore { +namespace Tests { + + class TestPluginTest : public ::testing::Test { + protected: + static ThunderTestRuntime _runtime; + + static void SetUpTestSuite() + { + ThunderTestRuntime::PluginConfig dummyConfig; + dummyConfig.Callsign = "TestPlugin"; + dummyConfig.ClassName = "TestPlugin"; + dummyConfig.Locator = "libThunderTestPlugin.so"; + dummyConfig.StartMode = PluginHost::IShell::startmode::ACTIVATED; + + std::vector plugins; + plugins.push_back(dummyConfig); + + uint32_t result = _runtime.Initialize(plugins, TEST_PLUGIN_PATH); + ASSERT_EQ(result, Core::ERROR_NONE) << "Failed to initialize runtime with TestPlugin"; + } + + static void TearDownTestSuite() + { + _runtime.Deinitialize(); + } + }; + + ThunderTestRuntime TestPluginTest::_runtime; + + // ================================================================== + // Plugin lifecycle + // ================================================================== + + TEST_F(TestPluginTest, PluginIsActivated) + { + auto shell = _runtime.GetShell("TestPlugin"); + EXPECT_TRUE(shell.IsValid()) << "TestPlugin IShell must be available"; + if (shell.IsValid()) { + EXPECT_EQ(shell->State(), PluginHost::IShell::state::ACTIVATED); + } + } + + // ================================================================== + // COM-RPC path (QueryInterface) + // ================================================================== + + TEST_F(TestPluginTest, COMRPC_QueryInterfaceSucceeds) + { + auto* iface = _runtime.GetInterface("TestPlugin"); + ASSERT_NE(iface, nullptr) << "QueryInterface must succeed"; + iface->Release(); + } + + TEST_F(TestPluginTest, COMRPC_EchoReturnsInput) + { + auto* iface = _runtime.GetInterface("TestPlugin"); + ASSERT_NE(iface, nullptr); + + string output; + uint32_t result = iface->Echo("hello", output); + EXPECT_EQ(result, Core::ERROR_NONE); + EXPECT_EQ(output, "hello"); + + iface->Release(); + } + + TEST_F(TestPluginTest, COMRPC_GreetReturnsMessage) + { + auto* iface = _runtime.GetInterface("TestPlugin"); + ASSERT_NE(iface, nullptr); + + string message; + uint32_t result = iface->Greet("Thunder", message); + EXPECT_EQ(result, Core::ERROR_NONE); + EXPECT_EQ(message, "Hello, Thunder!"); + + iface->Release(); + } + + TEST_F(TestPluginTest, COMRPC_GreetDefaultsToWorld) + { + auto* iface = _runtime.GetInterface("TestPlugin"); + ASSERT_NE(iface, nullptr); + + string message; + uint32_t result = iface->Greet("", message); + EXPECT_EQ(result, Core::ERROR_NONE); + EXPECT_EQ(message, "Hello, World!"); + + iface->Release(); + } + + TEST_F(TestPluginTest, COMRPC_EchoEmptyString) + { + auto* iface = _runtime.GetInterface("TestPlugin"); + ASSERT_NE(iface, nullptr); + + string output; + uint32_t result = iface->Echo("", output); + EXPECT_EQ(result, Core::ERROR_NONE); + EXPECT_TRUE(output.empty()); + + iface->Release(); + } + + // ================================================================== + // JSON-RPC path (full designator via Invoke) + // ================================================================== + + TEST_F(TestPluginTest, JSONRPC_EchoReturnsInput) + { + string response; + uint32_t result = _runtime.Invoke("TestPlugin.echo", R"({"input":"hello"})", response); + EXPECT_EQ(result, Core::ERROR_NONE) << "echo returned: " << result; + EXPECT_FALSE(response.empty()); + EXPECT_NE(response.find("hello"), string::npos) << "response: " << response; + } + + TEST_F(TestPluginTest, JSONRPC_GreetReturnsMessage) + { + string response; + uint32_t result = _runtime.Invoke("TestPlugin.greet", R"({"name":"Thunder"})", response); + EXPECT_EQ(result, Core::ERROR_NONE) << "greet returned: " << result; + EXPECT_NE(response.find("Hello, Thunder!"), string::npos) << "response: " << response; + } + + TEST_F(TestPluginTest, JSONRPC_UnknownMethodReturnsError) + { + string response; + uint32_t result = _runtime.Invoke("TestPlugin.nonexistent", "{}", response); + EXPECT_EQ(result, Core::ERROR_UNKNOWN_KEY); + } + + // ================================================================== + // JSON-RPC path (JSONRPCLink — callsign-bound) + // ================================================================== + + TEST_F(TestPluginTest, JSONRPC_EchoViaLink) + { + auto* link = _runtime.CreateJSONRPCLink("TestPlugin"); + ASSERT_NE(link, nullptr); + + string response; + uint32_t result = link->Invoke("echo", R"({"input":"linked"})", response); + EXPECT_EQ(result, Core::ERROR_NONE); + EXPECT_NE(response.find("linked"), string::npos) << "response: " << response; + + delete link; + } + + TEST_F(TestPluginTest, JSONRPC_GreetViaLink) + { + auto* link = _runtime.CreateJSONRPCLink("TestPlugin"); + ASSERT_NE(link, nullptr); + + string response; + uint32_t result = link->Invoke("greet", R"({"name":"Link"})", response); + EXPECT_EQ(result, Core::ERROR_NONE); + EXPECT_NE(response.find("Hello, Link!"), string::npos) << "response: " << response; + + delete link; + } + +} // namespace Tests +} // namespace TestCore +} // namespace Thunder From 68f35e93c14a717bafe62bee7ee7b83de5fcd833 Mon Sep 17 00:00:00 2001 From: Sankalp Maneshwar Date: Mon, 20 Apr 2026 14:31:52 +0530 Subject: [PATCH 04/22] 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 2340e320b8..73f7326c6e 100644 --- a/.github/workflows/Test_Thunder_Test_Support.yml +++ b/.github/workflows/Test_Thunder_Test_Support.yml @@ -6,7 +6,7 @@ permissions: on: workflow_dispatch: push: - branches: ["R5.3"] + branches: ["R5.3", "development/test-support"] paths: - 'Tests/test_support/**' - 'Source/Thunder/**' From deeda11b800afb00df61c0e8413a54d62398d190 Mon Sep 17 00:00:00 2001 From: Sankalp Maneshwar Date: Mon, 20 Apr 2026 14:33:01 +0530 Subject: [PATCH 05/22] Update Test_Thunder_Test_Support.yml From 9be539e0c8c70e73c9305ef360b1bed71db77b0d Mon Sep 17 00:00:00 2001 From: Sankalp Maneshwar Date: Mon, 20 Apr 2026 14:58:19 +0530 Subject: [PATCH 06/22] Update Test_Thunder_Test_Support.yml --- .github/workflows/Test_Thunder_Test_Support.yml | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/.github/workflows/Test_Thunder_Test_Support.yml b/.github/workflows/Test_Thunder_Test_Support.yml index 73f7326c6e..9c38731ef3 100644 --- a/.github/workflows/Test_Thunder_Test_Support.yml +++ b/.github/workflows/Test_Thunder_Test_Support.yml @@ -7,18 +7,9 @@ on: workflow_dispatch: push: branches: ["R5.3", "development/test-support"] - paths: - - 'Tests/test_support/**' - - 'Source/Thunder/**' - - 'Source/core/**' - - 'Source/plugins/**' + pull_request: branches: ["R5.3"] - paths: - - 'Tests/test_support/**' - - 'Source/Thunder/**' - - 'Source/core/**' - - 'Source/plugins/**' jobs: SmokeTest: From 689d4c873939b915028e563d05160a651e703576 Mon Sep 17 00:00:00 2001 From: Sankalp Maneshwar Date: Mon, 20 Apr 2026 14:58:37 +0530 Subject: [PATCH 07/22] Update Test_Thunder_Test_Support.yml --- .github/workflows/Test_Thunder_Test_Support.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/Test_Thunder_Test_Support.yml b/.github/workflows/Test_Thunder_Test_Support.yml index 9c38731ef3..f10c749f01 100644 --- a/.github/workflows/Test_Thunder_Test_Support.yml +++ b/.github/workflows/Test_Thunder_Test_Support.yml @@ -6,10 +6,10 @@ permissions: on: workflow_dispatch: push: - branches: ["R5.3", "development/test-support"] + branches: ["R5.3"] pull_request: - branches: ["R5.3"] + branches: ["R5.3", "development/test-support"] jobs: SmokeTest: From 8912b88b5f070fe699abbfcabc9277142487e646 Mon Sep 17 00:00:00 2001 From: Sankalp Maneshwar Date: Mon, 20 Apr 2026 15:00:36 +0530 Subject: [PATCH 08/22] 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 f10c749f01..0ce4c8e1cb 100644 --- a/.github/workflows/Test_Thunder_Test_Support.yml +++ b/.github/workflows/Test_Thunder_Test_Support.yml @@ -6,7 +6,7 @@ permissions: on: workflow_dispatch: push: - branches: ["R5.3"] + branches: ["R5.3", "development/test-support"] pull_request: branches: ["R5.3", "development/test-support"] From 30bab109f3592293a4df521195564ed94cd5569d Mon Sep 17 00:00:00 2001 From: Sankalp Maneshwar Date: Mon, 20 Apr 2026 16:19:29 +0530 Subject: [PATCH 09/22] Update Test_Thunder_Test_Support.yml --- .github/workflows/Test_Thunder_Test_Support.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/Test_Thunder_Test_Support.yml b/.github/workflows/Test_Thunder_Test_Support.yml index 0ce4c8e1cb..885052ae20 100644 --- a/.github/workflows/Test_Thunder_Test_Support.yml +++ b/.github/workflows/Test_Thunder_Test_Support.yml @@ -6,10 +6,10 @@ permissions: on: workflow_dispatch: push: - branches: ["R5.3", "development/test-support"] + branches: ["R5_3", "development/test-support"] pull_request: - branches: ["R5.3", "development/test-support"] + branches: ["R5_3", "development/test-support"] jobs: SmokeTest: From 4c7ce48a0665cf92d29bd6bb1f703130bafd4b86 Mon Sep 17 00:00:00 2001 From: Sankalp Maneshwar Date: Mon, 20 Apr 2026 16:19:54 +0530 Subject: [PATCH 10/22] Update TestPluginTest.cpp --- Tests/test_support/tests/TestPluginTest.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_support/tests/TestPluginTest.cpp b/Tests/test_support/tests/TestPluginTest.cpp index 3104e550fa..366e474e64 100644 --- a/Tests/test_support/tests/TestPluginTest.cpp +++ b/Tests/test_support/tests/TestPluginTest.cpp @@ -12,7 +12,7 @@ #include "Module.h" #include "ThunderTestRuntime.h" -#include +#include #include #include From 8468c7dc10cd1d815e2b1bab6f005a4e99ebeb44 Mon Sep 17 00:00:00 2001 From: smanes0213 Date: Mon, 20 Apr 2026 16:27:06 +0530 Subject: [PATCH 11/22] Resolve header include error --- Tests/test_support/tests/CMakeLists.txt | 5 +++++ Tests/test_support/tests/TestPluginTest.cpp | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/Tests/test_support/tests/CMakeLists.txt b/Tests/test_support/tests/CMakeLists.txt index 67b7f32e44..d25dac524b 100644 --- a/Tests/test_support/tests/CMakeLists.txt +++ b/Tests/test_support/tests/CMakeLists.txt @@ -46,6 +46,11 @@ target_compile_definitions(${PLUGIN_TEST_TARGET} TEST_PLUGIN_PATH="${TEST_PLUGIN_PATH}" ) +target_include_directories(${PLUGIN_TEST_TARGET} + PRIVATE + ${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_INCLUDEDIR}/${NAMESPACE} +) + target_link_libraries(${PLUGIN_TEST_TARGET} PRIVATE thunder_test_support diff --git a/Tests/test_support/tests/TestPluginTest.cpp b/Tests/test_support/tests/TestPluginTest.cpp index 366e474e64..3104e550fa 100644 --- a/Tests/test_support/tests/TestPluginTest.cpp +++ b/Tests/test_support/tests/TestPluginTest.cpp @@ -12,7 +12,7 @@ #include "Module.h" #include "ThunderTestRuntime.h" -#include +#include #include #include From 3f4d329df4c00db753177558d37bfd0470c36638 Mon Sep 17 00:00:00 2001 From: smanes0213 Date: Mon, 20 Apr 2026 16:35:13 +0530 Subject: [PATCH 12/22] Resolve workflow issue --- .../workflows/Test_Thunder_Test_Support.yml | 14 +++-- Tests/test_support/tests/CMakeLists.txt | 56 ++++++++++--------- 2 files changed, 38 insertions(+), 32 deletions(-) diff --git a/.github/workflows/Test_Thunder_Test_Support.yml b/.github/workflows/Test_Thunder_Test_Support.yml index 885052ae20..cfed396c5e 100644 --- a/.github/workflows/Test_Thunder_Test_Support.yml +++ b/.github/workflows/Test_Thunder_Test_Support.yml @@ -5,9 +5,7 @@ permissions: on: workflow_dispatch: - push: - branches: ["R5_3", "development/test-support"] - + pull_request: branches: ["R5_3", "development/test-support"] @@ -137,8 +135,7 @@ jobs: -DCMAKE_MODULE_PATH="${PWD}/${{matrix.build_type}}/install/usr/include/Thunder/Modules" \ -DPORT="0" \ -DENABLE_CXX17=OFF \ - -DENABLE_TEST_RUNTIME=ON \ - -DTEST_PLUGIN_PATH="${PWD}/${{matrix.build_type}}/install/usr/lib/thunder/plugins" + -DENABLE_TEST_RUNTIME=ON cmake --build ${{matrix.build_type}}/build/Thunder --target install - name: Build ThunderInterfaces @@ -160,6 +157,13 @@ jobs: -DPLUGIN_TESTPLUGIN=ON cmake --build ${{matrix.build_type}}/build/ThunderNanoServicesTests --target install + - name: Build Thunder plugin test + run: | + source venv/bin/activate + cmake -G Ninja -S Thunder -B ${{matrix.build_type}}/build/Thunder \ + -DTEST_PLUGIN_PATH="${PWD}/${{matrix.build_type}}/install/usr/lib/thunder/plugins" + cmake --build ${{matrix.build_type}}/build/Thunder --target thunder_test_runtime_plugin + # ----- Run smoke test ----- - name: Run smoke test run: | diff --git a/Tests/test_support/tests/CMakeLists.txt b/Tests/test_support/tests/CMakeLists.txt index d25dac524b..0059158c1a 100644 --- a/Tests/test_support/tests/CMakeLists.txt +++ b/Tests/test_support/tests/CMakeLists.txt @@ -30,32 +30,34 @@ add_test(NAME ${TARGET} COMMAND ${TARGET}) # - ThunderNanoServices TestPlugin .so built and path passed via # TEST_PLUGIN_PATH (defaults to the Thunder install plugins dir) # ------------------------------------------------------------------ -set(PLUGIN_TEST_TARGET thunder_test_runtime_plugin) - set(TEST_PLUGIN_PATH "" CACHE PATH "Directory containing libThunderTestPlugin.so") -add_executable(${PLUGIN_TEST_TARGET} - TestPluginTest.cpp - Module.cpp -) - -target_compile_definitions(${PLUGIN_TEST_TARGET} - PRIVATE - MODULE_NAME=TestPluginTest - BUILD_REFERENCE=${BUILD_REFERENCE} - TEST_PLUGIN_PATH="${TEST_PLUGIN_PATH}" -) - -target_include_directories(${PLUGIN_TEST_TARGET} - PRIVATE - ${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_INCLUDEDIR}/${NAMESPACE} -) - -target_link_libraries(${PLUGIN_TEST_TARGET} - PRIVATE - thunder_test_support - GTest::GTest - GTest::Main -) - -add_test(NAME ${PLUGIN_TEST_TARGET} COMMAND ${PLUGIN_TEST_TARGET}) +if(TEST_PLUGIN_PATH) + set(PLUGIN_TEST_TARGET thunder_test_runtime_plugin) + + add_executable(${PLUGIN_TEST_TARGET} + TestPluginTest.cpp + Module.cpp + ) + + target_compile_definitions(${PLUGIN_TEST_TARGET} + PRIVATE + MODULE_NAME=TestPluginTest + BUILD_REFERENCE=${BUILD_REFERENCE} + TEST_PLUGIN_PATH="${TEST_PLUGIN_PATH}" + ) + + target_include_directories(${PLUGIN_TEST_TARGET} + PRIVATE + ${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_INCLUDEDIR}/${NAMESPACE} + ) + + target_link_libraries(${PLUGIN_TEST_TARGET} + PRIVATE + thunder_test_support + GTest::GTest + GTest::Main + ) + + add_test(NAME ${PLUGIN_TEST_TARGET} COMMAND ${PLUGIN_TEST_TARGET}) +endif() From df16a4712c6e43080df6d17df568fa981c8e9842 Mon Sep 17 00:00:00 2001 From: smanes0213 Date: Mon, 20 Apr 2026 16:44:00 +0530 Subject: [PATCH 13/22] Resolve workflow issue --- .github/workflows/Test_Thunder_Test_Support.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/Test_Thunder_Test_Support.yml b/.github/workflows/Test_Thunder_Test_Support.yml index cfed396c5e..ecc9efefca 100644 --- a/.github/workflows/Test_Thunder_Test_Support.yml +++ b/.github/workflows/Test_Thunder_Test_Support.yml @@ -144,7 +144,8 @@ jobs: cmake -G Ninja -S ThunderInterfaces -B ${{matrix.build_type}}/build/ThunderInterfaces \ -DCMAKE_BUILD_TYPE=${{matrix.build_type}} \ -DCMAKE_INSTALL_PREFIX="${{matrix.build_type}}/install/usr" \ - -DCMAKE_MODULE_PATH="${PWD}/${{matrix.build_type}}/install/usr/include/Thunder/Modules" + -DCMAKE_MODULE_PATH="${PWD}/${{matrix.build_type}}/install/usr/include/Thunder/Modules" \ + -DCMAKE_PREFIX_PATH="${PWD}/${{matrix.build_type}}/install/usr" cmake --build ${{matrix.build_type}}/build/ThunderInterfaces --target install - name: Build ThunderNanoServices TestPlugin @@ -154,6 +155,7 @@ jobs: -DCMAKE_BUILD_TYPE=${{matrix.build_type}} \ -DCMAKE_INSTALL_PREFIX="${{matrix.build_type}}/install/usr" \ -DCMAKE_MODULE_PATH="${PWD}/${{matrix.build_type}}/install/usr/include/Thunder/Modules" \ + -DCMAKE_PREFIX_PATH="${PWD}/${{matrix.build_type}}/install/usr" \ -DPLUGIN_TESTPLUGIN=ON cmake --build ${{matrix.build_type}}/build/ThunderNanoServicesTests --target install From 57f70075aa6079fe14a5fc354f45a110171aebef Mon Sep 17 00:00:00 2001 From: Sankalp Maneshwar Date: Mon, 20 Apr 2026 17:26:25 +0530 Subject: [PATCH 14/22] 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 ecc9efefca..505fe5f361 100644 --- a/.github/workflows/Test_Thunder_Test_Support.yml +++ b/.github/workflows/Test_Thunder_Test_Support.yml @@ -47,6 +47,7 @@ jobs: with: path: ThunderTools repository: rdkcentral/ThunderTools + ref: R5.3.0 - name: Regex ThunderTools if: ${{ contains(github.event.pull_request.body, '[DependsOn=ThunderTools:') }} From f801490127ab32c8ec5e624e8e8bc8b906ee59e6 Mon Sep 17 00:00:00 2001 From: smanes0213 Date: Tue, 21 Apr 2026 10:33:16 +0530 Subject: [PATCH 15/22] Move out TestPlugin Test file --- .../workflows/Test_Thunder_Test_Support.yml | 16 +- Tests/test_support/CMakeLists.txt | 18 ++ Tests/test_support/tests/CMakeLists.txt | 40 ---- Tests/test_support/tests/TestPluginTest.cpp | 190 ------------------ 4 files changed, 23 insertions(+), 241 deletions(-) delete mode 100644 Tests/test_support/tests/TestPluginTest.cpp diff --git a/.github/workflows/Test_Thunder_Test_Support.yml b/.github/workflows/Test_Thunder_Test_Support.yml index 505fe5f361..ba980ef081 100644 --- a/.github/workflows/Test_Thunder_Test_Support.yml +++ b/.github/workflows/Test_Thunder_Test_Support.yml @@ -149,7 +149,7 @@ jobs: -DCMAKE_PREFIX_PATH="${PWD}/${{matrix.build_type}}/install/usr" cmake --build ${{matrix.build_type}}/build/ThunderInterfaces --target install - - name: Build ThunderNanoServices TestPlugin + - name: Build ThunderNanoServices TestPlugin + test run: | source venv/bin/activate cmake -G Ninja -S ThunderNanoServices/tests -B ${{matrix.build_type}}/build/ThunderNanoServicesTests \ @@ -157,15 +157,9 @@ jobs: -DCMAKE_INSTALL_PREFIX="${{matrix.build_type}}/install/usr" \ -DCMAKE_MODULE_PATH="${PWD}/${{matrix.build_type}}/install/usr/include/Thunder/Modules" \ -DCMAKE_PREFIX_PATH="${PWD}/${{matrix.build_type}}/install/usr" \ - -DPLUGIN_TESTPLUGIN=ON - cmake --build ${{matrix.build_type}}/build/ThunderNanoServicesTests --target install - - - name: Build Thunder plugin test - run: | - source venv/bin/activate - cmake -G Ninja -S Thunder -B ${{matrix.build_type}}/build/Thunder \ + -DPLUGIN_TESTPLUGIN=ON \ -DTEST_PLUGIN_PATH="${PWD}/${{matrix.build_type}}/install/usr/lib/thunder/plugins" - cmake --build ${{matrix.build_type}}/build/Thunder --target thunder_test_runtime_plugin + cmake --build ${{matrix.build_type}}/build/ThunderNanoServicesTests --target install # ----- Run smoke test ----- - name: Run smoke test @@ -176,10 +170,10 @@ jobs: --gtest_color=yes # ----- Run plugin test ----- - - name: Run plugin test (COM-RPC + JSON-RPC) + - name: Run plugin test (COM-RPC + JSON-RPC + events) run: | LD_LIBRARY_PATH="${{matrix.build_type}}/install/usr/lib:${PWD}/${{matrix.build_type}}/install/usr/lib/thunder/plugins:$LD_LIBRARY_PATH" \ - ${{matrix.build_type}}/build/Thunder/Tests/test_support/tests/thunder_test_runtime_plugin \ + ${{matrix.build_type}}/build/ThunderNanoServicesTests/TestPlugin/test/thunder_testplugin_test \ --gtest_output="xml:plugin-test-results.xml" \ --gtest_color=yes diff --git a/Tests/test_support/CMakeLists.txt b/Tests/test_support/CMakeLists.txt index 01a8edfee9..16a3e5aaa0 100644 --- a/Tests/test_support/CMakeLists.txt +++ b/Tests/test_support/CMakeLists.txt @@ -119,6 +119,24 @@ if(HIBERNATESUPPORT) ) endif() +# ------------------------------------------------------------------ +# Install the library and header so external projects can use it +# via find_package(thunder_test_support) +# ------------------------------------------------------------------ +install( + FILES ThunderTestRuntime.h + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/${NAMESPACE}/test_support + COMPONENT ${NAMESPACE}_Development +) + +install( + TARGETS ${TARGET} EXPORT ${TARGET}Targets + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} COMPONENT ${NAMESPACE}_Development + INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/${NAMESPACE} +) + +InstallCMakeConfig(TARGETS ${TARGET}) + # ------------------------------------------------------------------ # Smoke test sub-directory # ------------------------------------------------------------------ diff --git a/Tests/test_support/tests/CMakeLists.txt b/Tests/test_support/tests/CMakeLists.txt index 0059158c1a..5e6463e706 100644 --- a/Tests/test_support/tests/CMakeLists.txt +++ b/Tests/test_support/tests/CMakeLists.txt @@ -21,43 +21,3 @@ target_link_libraries(${TARGET} ) add_test(NAME ${TARGET} COMMAND ${TARGET}) - -# ------------------------------------------------------------------ -# TestPlugin integration test -# -# Requires: -# - ThunderInterfaces qa_interfaces headers installed (for ITestPlugin.h) -# - ThunderNanoServices TestPlugin .so built and path passed via -# TEST_PLUGIN_PATH (defaults to the Thunder install plugins dir) -# ------------------------------------------------------------------ -set(TEST_PLUGIN_PATH "" CACHE PATH "Directory containing libThunderTestPlugin.so") - -if(TEST_PLUGIN_PATH) - set(PLUGIN_TEST_TARGET thunder_test_runtime_plugin) - - add_executable(${PLUGIN_TEST_TARGET} - TestPluginTest.cpp - Module.cpp - ) - - target_compile_definitions(${PLUGIN_TEST_TARGET} - PRIVATE - MODULE_NAME=TestPluginTest - BUILD_REFERENCE=${BUILD_REFERENCE} - TEST_PLUGIN_PATH="${TEST_PLUGIN_PATH}" - ) - - target_include_directories(${PLUGIN_TEST_TARGET} - PRIVATE - ${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_INCLUDEDIR}/${NAMESPACE} - ) - - target_link_libraries(${PLUGIN_TEST_TARGET} - PRIVATE - thunder_test_support - GTest::GTest - GTest::Main - ) - - add_test(NAME ${PLUGIN_TEST_TARGET} COMMAND ${PLUGIN_TEST_TARGET}) -endif() diff --git a/Tests/test_support/tests/TestPluginTest.cpp b/Tests/test_support/tests/TestPluginTest.cpp deleted file mode 100644 index 3104e550fa..0000000000 --- a/Tests/test_support/tests/TestPluginTest.cpp +++ /dev/null @@ -1,190 +0,0 @@ -// ========================================================================== -// TestPluginTest — validates that the test runtime can load a plugin -// and interact with it via both COM-RPC and JSON-RPC. -// -// COM-RPC tests use QueryInterface to call methods directly. -// JSON-RPC tests use ThunderTestRuntime::Invoke() and JSONRPCLink. -// -// The TestPlugin is built as a shared library and placed in -// ${CMAKE_BINARY_DIR}/test_plugins/. The test passes that directory as -// the systemPath so the embedded server can dlopen it. -// ========================================================================== - -#include "Module.h" -#include "ThunderTestRuntime.h" -#include -#include -#include - -#ifndef TEST_PLUGIN_PATH -#error "TEST_PLUGIN_PATH must be defined by CMake" -#endif - -namespace Thunder { -namespace TestCore { -namespace Tests { - - class TestPluginTest : public ::testing::Test { - protected: - static ThunderTestRuntime _runtime; - - static void SetUpTestSuite() - { - ThunderTestRuntime::PluginConfig dummyConfig; - dummyConfig.Callsign = "TestPlugin"; - dummyConfig.ClassName = "TestPlugin"; - dummyConfig.Locator = "libThunderTestPlugin.so"; - dummyConfig.StartMode = PluginHost::IShell::startmode::ACTIVATED; - - std::vector plugins; - plugins.push_back(dummyConfig); - - uint32_t result = _runtime.Initialize(plugins, TEST_PLUGIN_PATH); - ASSERT_EQ(result, Core::ERROR_NONE) << "Failed to initialize runtime with TestPlugin"; - } - - static void TearDownTestSuite() - { - _runtime.Deinitialize(); - } - }; - - ThunderTestRuntime TestPluginTest::_runtime; - - // ================================================================== - // Plugin lifecycle - // ================================================================== - - TEST_F(TestPluginTest, PluginIsActivated) - { - auto shell = _runtime.GetShell("TestPlugin"); - EXPECT_TRUE(shell.IsValid()) << "TestPlugin IShell must be available"; - if (shell.IsValid()) { - EXPECT_EQ(shell->State(), PluginHost::IShell::state::ACTIVATED); - } - } - - // ================================================================== - // COM-RPC path (QueryInterface) - // ================================================================== - - TEST_F(TestPluginTest, COMRPC_QueryInterfaceSucceeds) - { - auto* iface = _runtime.GetInterface("TestPlugin"); - ASSERT_NE(iface, nullptr) << "QueryInterface must succeed"; - iface->Release(); - } - - TEST_F(TestPluginTest, COMRPC_EchoReturnsInput) - { - auto* iface = _runtime.GetInterface("TestPlugin"); - ASSERT_NE(iface, nullptr); - - string output; - uint32_t result = iface->Echo("hello", output); - EXPECT_EQ(result, Core::ERROR_NONE); - EXPECT_EQ(output, "hello"); - - iface->Release(); - } - - TEST_F(TestPluginTest, COMRPC_GreetReturnsMessage) - { - auto* iface = _runtime.GetInterface("TestPlugin"); - ASSERT_NE(iface, nullptr); - - string message; - uint32_t result = iface->Greet("Thunder", message); - EXPECT_EQ(result, Core::ERROR_NONE); - EXPECT_EQ(message, "Hello, Thunder!"); - - iface->Release(); - } - - TEST_F(TestPluginTest, COMRPC_GreetDefaultsToWorld) - { - auto* iface = _runtime.GetInterface("TestPlugin"); - ASSERT_NE(iface, nullptr); - - string message; - uint32_t result = iface->Greet("", message); - EXPECT_EQ(result, Core::ERROR_NONE); - EXPECT_EQ(message, "Hello, World!"); - - iface->Release(); - } - - TEST_F(TestPluginTest, COMRPC_EchoEmptyString) - { - auto* iface = _runtime.GetInterface("TestPlugin"); - ASSERT_NE(iface, nullptr); - - string output; - uint32_t result = iface->Echo("", output); - EXPECT_EQ(result, Core::ERROR_NONE); - EXPECT_TRUE(output.empty()); - - iface->Release(); - } - - // ================================================================== - // JSON-RPC path (full designator via Invoke) - // ================================================================== - - TEST_F(TestPluginTest, JSONRPC_EchoReturnsInput) - { - string response; - uint32_t result = _runtime.Invoke("TestPlugin.echo", R"({"input":"hello"})", response); - EXPECT_EQ(result, Core::ERROR_NONE) << "echo returned: " << result; - EXPECT_FALSE(response.empty()); - EXPECT_NE(response.find("hello"), string::npos) << "response: " << response; - } - - TEST_F(TestPluginTest, JSONRPC_GreetReturnsMessage) - { - string response; - uint32_t result = _runtime.Invoke("TestPlugin.greet", R"({"name":"Thunder"})", response); - EXPECT_EQ(result, Core::ERROR_NONE) << "greet returned: " << result; - EXPECT_NE(response.find("Hello, Thunder!"), string::npos) << "response: " << response; - } - - TEST_F(TestPluginTest, JSONRPC_UnknownMethodReturnsError) - { - string response; - uint32_t result = _runtime.Invoke("TestPlugin.nonexistent", "{}", response); - EXPECT_EQ(result, Core::ERROR_UNKNOWN_KEY); - } - - // ================================================================== - // JSON-RPC path (JSONRPCLink — callsign-bound) - // ================================================================== - - TEST_F(TestPluginTest, JSONRPC_EchoViaLink) - { - auto* link = _runtime.CreateJSONRPCLink("TestPlugin"); - ASSERT_NE(link, nullptr); - - string response; - uint32_t result = link->Invoke("echo", R"({"input":"linked"})", response); - EXPECT_EQ(result, Core::ERROR_NONE); - EXPECT_NE(response.find("linked"), string::npos) << "response: " << response; - - delete link; - } - - TEST_F(TestPluginTest, JSONRPC_GreetViaLink) - { - auto* link = _runtime.CreateJSONRPCLink("TestPlugin"); - ASSERT_NE(link, nullptr); - - string response; - uint32_t result = link->Invoke("greet", R"({"name":"Link"})", response); - EXPECT_EQ(result, Core::ERROR_NONE); - EXPECT_NE(response.find("Hello, Link!"), string::npos) << "response: " << response; - - delete link; - } - -} // namespace Tests -} // namespace TestCore -} // namespace Thunder From bfcdfcee48a1c204e2ca7d0c05adad5afb22b208 Mon Sep 17 00:00:00 2001 From: smanes0213 Date: Tue, 21 Apr 2026 10:40:14 +0530 Subject: [PATCH 16/22] Resolve workflow failures --- .github/workflows/Test_Thunder_Test_Support.yml | 1 - Tests/test_support/CMakeLists.txt | 6 ++++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/Test_Thunder_Test_Support.yml b/.github/workflows/Test_Thunder_Test_Support.yml index ba980ef081..98e7722cb6 100644 --- a/.github/workflows/Test_Thunder_Test_Support.yml +++ b/.github/workflows/Test_Thunder_Test_Support.yml @@ -135,7 +135,6 @@ jobs: -DCMAKE_INSTALL_PREFIX="${{matrix.build_type}}/install/usr" \ -DCMAKE_MODULE_PATH="${PWD}/${{matrix.build_type}}/install/usr/include/Thunder/Modules" \ -DPORT="0" \ - -DENABLE_CXX17=OFF \ -DENABLE_TEST_RUNTIME=ON cmake --build ${{matrix.build_type}}/build/Thunder --target install diff --git a/Tests/test_support/CMakeLists.txt b/Tests/test_support/CMakeLists.txt index 16a3e5aaa0..2f7d807cae 100644 --- a/Tests/test_support/CMakeLists.txt +++ b/Tests/test_support/CMakeLists.txt @@ -27,7 +27,8 @@ add_library(${TARGET} STATIC target_include_directories(${TARGET} PUBLIC - ${CMAKE_CURRENT_SOURCE_DIR} + $ + $ PRIVATE ${THUNDER_SOURCE_DIR} $ @@ -69,8 +70,9 @@ target_link_libraries(${TARGET} ${NAMESPACE}Messaging::${NAMESPACE}Messaging ${NAMESPACE}WebSocket::${NAMESPACE}WebSocket ${NAMESPACE}Plugins::${NAMESPACE}Plugins - ${NAMESPACE}COMProcess::${NAMESPACE}COMProcess Threads::Threads + PRIVATE + ${NAMESPACE}COMProcess::${NAMESPACE}COMProcess ) # ------------------------------------------------------------------ From ed48229fa747a8c6f50b7dd6b8d40353e9920556 Mon Sep 17 00:00:00 2001 From: smanes0213 Date: Tue, 21 Apr 2026 10:43:32 +0530 Subject: [PATCH 17/22] Resolve workflow failures --- Tests/test_support/CMakeLists.txt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Tests/test_support/CMakeLists.txt b/Tests/test_support/CMakeLists.txt index 2f7d807cae..0f0b2700a8 100644 --- a/Tests/test_support/CMakeLists.txt +++ b/Tests/test_support/CMakeLists.txt @@ -71,10 +71,12 @@ target_link_libraries(${TARGET} ${NAMESPACE}WebSocket::${NAMESPACE}WebSocket ${NAMESPACE}Plugins::${NAMESPACE}Plugins Threads::Threads - PRIVATE - ${NAMESPACE}COMProcess::${NAMESPACE}COMProcess ) +# COMProcess is an INTERFACE library carrying only a compile definition. +# It is not in any export set, so we absorb its effect directly. +target_compile_definitions(${TARGET} PRIVATE HOSTING_COMPROCESS=ThunderPlugin) + # ------------------------------------------------------------------ # Whole-archive link. Ensures that the MODULE_NAME_DECLARATION # constructors from the Server objects are not discarded. From 5f0e7b662f6f4081b83b7e03a73bd13cca10fa09 Mon Sep 17 00:00:00 2001 From: smanes0213 Date: Tue, 21 Apr 2026 10:55:29 +0530 Subject: [PATCH 18/22] Resolve workflow failures --- Tests/test_support/CMakeLists.txt | 11 +++++++--- .../thunder_test_supportConfig.cmake.in | 20 +++++++++++++++++++ 2 files changed, 28 insertions(+), 3 deletions(-) create mode 100644 Tests/test_support/thunder_test_supportConfig.cmake.in diff --git a/Tests/test_support/CMakeLists.txt b/Tests/test_support/CMakeLists.txt index 0f0b2700a8..2f91a64bc1 100644 --- a/Tests/test_support/CMakeLists.txt +++ b/Tests/test_support/CMakeLists.txt @@ -80,14 +80,16 @@ target_compile_definitions(${TARGET} PRIVATE HOSTING_COMPROCESS=ThunderPlugin) # ------------------------------------------------------------------ # Whole-archive link. Ensures that the MODULE_NAME_DECLARATION # constructors from the Server objects are not discarded. +# BUILD_INTERFACE only — installed consumers get this via the +# custom Config.cmake.in template instead. # ------------------------------------------------------------------ if(APPLE) target_link_options(${TARGET} INTERFACE - "SHELL:-Wl,-force_load,$" + "$>" ) else() target_link_options(${TARGET} INTERFACE - "SHELL:-Wl,--whole-archive $ -Wl,--no-whole-archive" + "$ -Wl,--no-whole-archive>" ) endif() @@ -139,7 +141,10 @@ install( INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/${NAMESPACE} ) -InstallCMakeConfig(TARGETS ${TARGET}) +InstallCMakeConfig( + TARGETS ${TARGET} + TEMPLATE ${CMAKE_CURRENT_SOURCE_DIR}/thunder_test_supportConfig.cmake.in +) # ------------------------------------------------------------------ # Smoke test sub-directory diff --git a/Tests/test_support/thunder_test_supportConfig.cmake.in b/Tests/test_support/thunder_test_supportConfig.cmake.in new file mode 100644 index 0000000000..ae044d95b3 --- /dev/null +++ b/Tests/test_support/thunder_test_supportConfig.cmake.in @@ -0,0 +1,20 @@ +set(dependencies @dependencies@) + +foreach(dependency ${dependencies}) + find_package(${dependency} REQUIRED) +endforeach() + +get_filename_component(_DIR "${CMAKE_CURRENT_LIST_FILE}" PATH) +include(${_DIR}/@_name@Targets.cmake) + +# Whole-archive linking ensures MODULE_NAME_DECLARATION static constructors +# from the embedded PluginHost::Server objects are not discarded by the linker. +if(TARGET @_name@::@_name@) + if(APPLE) + set_property(TARGET @_name@::@_name@ APPEND PROPERTY + INTERFACE_LINK_OPTIONS "SHELL:-Wl,-force_load,$") + else() + set_property(TARGET @_name@::@_name@ APPEND PROPERTY + INTERFACE_LINK_OPTIONS "SHELL:-Wl,--whole-archive $ -Wl,--no-whole-archive") + endif() +endif() From b45292ea19ee6ebd63b00433c9c2e33738da3057 Mon Sep 17 00:00:00 2001 From: smanes0213 Date: Tue, 21 Apr 2026 11:33:50 +0530 Subject: [PATCH 19/22] Add conditional gaurd to initialize and deinitialize --- Tests/test_support/ThunderTestRuntime.cpp | 7 ++++++- Tests/test_support/ThunderTestRuntime.h | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/Tests/test_support/ThunderTestRuntime.cpp b/Tests/test_support/ThunderTestRuntime.cpp index c8eb660c2f..56dbf6803b 100644 --- a/Tests/test_support/ThunderTestRuntime.cpp +++ b/Tests/test_support/ThunderTestRuntime.cpp @@ -331,6 +331,8 @@ namespace TestCore { _server = new PluginHost::Server(*_config, false); _server->Open(); + _initialized = true; + return Core::ERROR_NONE; } @@ -440,7 +442,10 @@ namespace TestCore { _config = nullptr; } - Messaging::MessageUnit::Instance().Close(); + if (_initialized == true) { + Messaging::MessageUnit::Instance().Close(); + _initialized = false; + } if (_configFilePath.empty() == false) { Core::File(_configFilePath).Destroy(); diff --git a/Tests/test_support/ThunderTestRuntime.h b/Tests/test_support/ThunderTestRuntime.h index 0c38e3800c..ac7433cd98 100644 --- a/Tests/test_support/ThunderTestRuntime.h +++ b/Tests/test_support/ThunderTestRuntime.h @@ -173,6 +173,7 @@ namespace TestCore { PluginHost::Config* _config = nullptr; PluginHost::Server* _server = nullptr; + bool _initialized = false; string _tempDir; string _configFilePath; string _proxyStubPath; From 5689025bb5de85722961de8d4a388c0237cd0d87 Mon Sep 17 00:00:00 2001 From: smanes0213 Date: Tue, 21 Apr 2026 11:57:31 +0530 Subject: [PATCH 20/22] Update ThunderTestRuntime.cpp --- Tests/test_support/ThunderTestRuntime.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/Tests/test_support/ThunderTestRuntime.cpp b/Tests/test_support/ThunderTestRuntime.cpp index 56dbf6803b..94913385db 100644 --- a/Tests/test_support/ThunderTestRuntime.cpp +++ b/Tests/test_support/ThunderTestRuntime.cpp @@ -444,6 +444,7 @@ namespace TestCore { if (_initialized == true) { Messaging::MessageUnit::Instance().Close(); + Core::Singleton::Dispose(); _initialized = false; } From 8e977abbed0014c6c54c93490d7b87a757e8165a Mon Sep 17 00:00:00 2001 From: smanes0213 Date: Tue, 21 Apr 2026 12:27:35 +0530 Subject: [PATCH 21/22] Add debug comments --- Tests/test_support/ThunderTestRuntime.cpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Tests/test_support/ThunderTestRuntime.cpp b/Tests/test_support/ThunderTestRuntime.cpp index 94913385db..11aac53021 100644 --- a/Tests/test_support/ThunderTestRuntime.cpp +++ b/Tests/test_support/ThunderTestRuntime.cpp @@ -122,7 +122,9 @@ namespace TestCore { ThunderTestRuntime::~ThunderTestRuntime() { + fprintf(stderr, "[TestRuntime] ~ThunderTestRuntime() - begin\n"); fflush(stderr); Deinitialize(); + fprintf(stderr, "[TestRuntime] ~ThunderTestRuntime() - done\n"); fflush(stderr); } bool ThunderTestRuntime::CreateDirectories() const @@ -431,20 +433,29 @@ namespace TestCore { void ThunderTestRuntime::Deinitialize() { + fprintf(stderr, "[TestRuntime] Deinitialize - begin\n"); fflush(stderr); + if (_server != nullptr) { + fprintf(stderr, "[TestRuntime] Server::Close() - begin\n"); fflush(stderr); _server->Close(); + fprintf(stderr, "[TestRuntime] Server::Close() - done\n"); fflush(stderr); delete _server; + fprintf(stderr, "[TestRuntime] delete _server - done\n"); fflush(stderr); _server = nullptr; } if (_config != nullptr) { delete _config; + fprintf(stderr, "[TestRuntime] delete _config - done\n"); fflush(stderr); _config = nullptr; } if (_initialized == true) { + fprintf(stderr, "[TestRuntime] MessageUnit::Close() - begin\n"); fflush(stderr); Messaging::MessageUnit::Instance().Close(); + fprintf(stderr, "[TestRuntime] Singleton::Dispose() - begin\n"); fflush(stderr); Core::Singleton::Dispose(); + fprintf(stderr, "[TestRuntime] Singleton::Dispose() - done\n"); fflush(stderr); _initialized = false; } @@ -455,6 +466,7 @@ namespace TestCore { CleanupDirectories(); _tempDir.clear(); + fprintf(stderr, "[TestRuntime] Deinitialize - done\n"); fflush(stderr); } } // namespace TestCore From 4770e550e45e0e2fbdeafd52cb7ba661e4564834 Mon Sep 17 00:00:00 2001 From: smanes0213 Date: Tue, 21 Apr 2026 15:06:08 +0530 Subject: [PATCH 22/22] Add thunder_test_main static library providing a GTest main() that calls _Exit() after RUN_ALL_TESTS() to avoid static destruction order crashes when external plugin .so files are loaded. --- Tests/test_support/CMakeLists.txt | 32 +++++++++++++++++++++++ Tests/test_support/ThunderTestMain.cpp | 28 ++++++++++++++++++++ Tests/test_support/ThunderTestRuntime.cpp | 13 --------- 3 files changed, 60 insertions(+), 13 deletions(-) create mode 100644 Tests/test_support/ThunderTestMain.cpp diff --git a/Tests/test_support/CMakeLists.txt b/Tests/test_support/CMakeLists.txt index 2f91a64bc1..5e6a5149b6 100644 --- a/Tests/test_support/CMakeLists.txt +++ b/Tests/test_support/CMakeLists.txt @@ -141,11 +141,43 @@ install( INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/${NAMESPACE} ) +# ------------------------------------------------------------------ +# thunder_test_main — GTest main() that calls _Exit() after +# RUN_ALL_TESTS() to avoid static destruction order crashes when +# external plugin .so files are loaded. +# +# Plugin tests link this instead of GTest::Main. +# ------------------------------------------------------------------ +set(MAIN_TARGET thunder_test_main) + +add_library(${MAIN_TARGET} STATIC + ThunderTestMain.cpp +) + +target_link_libraries(${MAIN_TARGET} + PUBLIC + GTest::GTest +) + +set_target_properties(${MAIN_TARGET} PROPERTIES + CXX_STANDARD 11 + CXX_STANDARD_REQUIRED YES +) + +install( + TARGETS ${MAIN_TARGET} EXPORT ${MAIN_TARGET}Targets + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} COMPONENT ${NAMESPACE}_Development +) + InstallCMakeConfig( TARGETS ${TARGET} TEMPLATE ${CMAKE_CURRENT_SOURCE_DIR}/thunder_test_supportConfig.cmake.in ) +InstallCMakeConfig( + TARGETS ${MAIN_TARGET} +) + # ------------------------------------------------------------------ # Smoke test sub-directory # ------------------------------------------------------------------ diff --git a/Tests/test_support/ThunderTestMain.cpp b/Tests/test_support/ThunderTestMain.cpp new file mode 100644 index 0000000000..b9ba67adea --- /dev/null +++ b/Tests/test_support/ThunderTestMain.cpp @@ -0,0 +1,28 @@ +// ========================================================================== +// ThunderTestMain — GTest main() for plugin integration tests. +// +// When a test binary loads external plugin shared libraries (.so) via the +// ThunderTestRuntime, those libraries may register static objects whose +// destruction order at process exit conflicts with Thunder's own +// singletons (WorkerPool, ResourceMonitor, etc.). +// +// This main() calls _Exit() after RUN_ALL_TESTS() to skip static +// destruction entirely, matching the real Thunder daemon which calls +// exit(0) after its CloseDown() sequence. +// +// Link against thunder_test_main instead of GTest::Main: +// target_link_libraries(my_test PRIVATE +// thunder_test_support::thunder_test_support +// thunder_test_main::thunder_test_main +// GTest::GTest) +// ========================================================================== + +#include +#include + +int main(int argc, char** argv) +{ + ::testing::InitGoogleTest(&argc, argv); + int result = RUN_ALL_TESTS(); + _Exit(result); +} diff --git a/Tests/test_support/ThunderTestRuntime.cpp b/Tests/test_support/ThunderTestRuntime.cpp index 11aac53021..56dbf6803b 100644 --- a/Tests/test_support/ThunderTestRuntime.cpp +++ b/Tests/test_support/ThunderTestRuntime.cpp @@ -122,9 +122,7 @@ namespace TestCore { ThunderTestRuntime::~ThunderTestRuntime() { - fprintf(stderr, "[TestRuntime] ~ThunderTestRuntime() - begin\n"); fflush(stderr); Deinitialize(); - fprintf(stderr, "[TestRuntime] ~ThunderTestRuntime() - done\n"); fflush(stderr); } bool ThunderTestRuntime::CreateDirectories() const @@ -433,29 +431,19 @@ namespace TestCore { void ThunderTestRuntime::Deinitialize() { - fprintf(stderr, "[TestRuntime] Deinitialize - begin\n"); fflush(stderr); - if (_server != nullptr) { - fprintf(stderr, "[TestRuntime] Server::Close() - begin\n"); fflush(stderr); _server->Close(); - fprintf(stderr, "[TestRuntime] Server::Close() - done\n"); fflush(stderr); delete _server; - fprintf(stderr, "[TestRuntime] delete _server - done\n"); fflush(stderr); _server = nullptr; } if (_config != nullptr) { delete _config; - fprintf(stderr, "[TestRuntime] delete _config - done\n"); fflush(stderr); _config = nullptr; } if (_initialized == true) { - fprintf(stderr, "[TestRuntime] MessageUnit::Close() - begin\n"); fflush(stderr); Messaging::MessageUnit::Instance().Close(); - fprintf(stderr, "[TestRuntime] Singleton::Dispose() - begin\n"); fflush(stderr); - Core::Singleton::Dispose(); - fprintf(stderr, "[TestRuntime] Singleton::Dispose() - done\n"); fflush(stderr); _initialized = false; } @@ -466,7 +454,6 @@ namespace TestCore { CleanupDirectories(); _tempDir.clear(); - fprintf(stderr, "[TestRuntime] Deinitialize - done\n"); fflush(stderr); } } // namespace TestCore