Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions Tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ option(FILE_UNLINK_TEST "File unlink test" OFF)
option(REDIRECT_TEST "Test stream redirection" OFF)
option(MESSAGEBUFFER_TEST "Test message buffer" OFF)
option(UNRAVELLER "reveal thread details" OFF)
option(ENABLE_TEST_RUNTIME "Build Thunder test support library for plugin integration tests" OFF)

if(BUILD_TESTS)
add_subdirectory(unit)
Expand Down Expand Up @@ -40,4 +41,8 @@ endif()

if(UNRAVELLER)
add_subdirectory(unraveller)
endif()

if(ENABLE_TEST_RUNTIME)
add_subdirectory(test_support)
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ENABLE_TEST_RUNTIME can currently be enabled on any platform, but Tests/test_support/ThunderTestRuntime.cpp uses POSIX-only APIs (mkdtemp) and hardcoded /tmp paths. Consider adding a platform guard here (e.g., only add the test_support subdirectory on POSIX platforms, or emit a fatal error if the option is ON on unsupported systems) to avoid configuration/build failures.

Suggested change
add_subdirectory(test_support)
if(UNIX)
add_subdirectory(test_support)
else()
message(FATAL_ERROR "ENABLE_TEST_RUNTIME is only supported on POSIX/UNIX platforms because Tests/test_support uses POSIX-only APIs.")
endif()

Copilot uses AI. Check for mistakes.
endif()
105 changes: 105 additions & 0 deletions Tests/test_support/CMakeLists.txt
Comment thread
bramoosterhuis marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# ============================================================================
# thunder_test_support - Static library for Thunder plugin integration testing
#
# This library embeds the Thunder PluginHost server (PluginServer, Controller,
# SystemInfo, etc.) into a static archive so that GTest-based test binaries
# can spin up a real Thunder runtime in-process without launching the
# standalone Thunder daemon.
#
# Usage:
# 1. Link your test executable against thunder_test_support.
# 2. Use ThunderTestRuntime::Initialize() to start the embedded server.
# 3. Call JSON-RPC or COM-RPC methods directly against loaded plugins.
# 4. Call ThunderTestRuntime::Shutdown() when done.
#
# NOTE: PluginHost.cpp is deliberately excluded — it contains main().
# The test binary provides its own main() via GTest.
# ============================================================================

find_package(Threads REQUIRED)

set(THUNDER_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../Source/Thunder")

set(THREADPOOL_COUNT "4" CACHE STRING "The number of threads in the thread pool for test runtime")
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

THREADPOOL_COUNT is already defined as a CACHE variable in Source/Thunder/CMakeLists.txt, and this set(... CACHE ...) in a subdirectory will reuse/overwrite the same cache entry (including its help text). To avoid confusing cache UI/help strings, consider not re-declaring it here and just consuming the existing cache variable (or use a uniquely named cache var specific to test support).

Suggested change
set(THREADPOOL_COUNT "4" CACHE STRING "The number of threads in the thread pool for test runtime")
if(NOT DEFINED THREADPOOL_COUNT)
set(THREADPOOL_COUNT "4")
endif()

Copilot uses AI. Check for mistakes.

add_library(thunder_test_support STATIC
ThunderTestRuntime.cpp
Module.cpp
${THUNDER_SOURCE_DIR}/PluginServer.cpp
${THUNDER_SOURCE_DIR}/Controller.cpp
${THUNDER_SOURCE_DIR}/SystemInfo.cpp
${THUNDER_SOURCE_DIR}/PostMortem.cpp
${THUNDER_SOURCE_DIR}/Probe.cpp
)

# PluginHost.cpp is deliberately excluded — it contains main().
# The test binary supplies its own main via GTest/GMock.

target_compile_definitions(thunder_test_support
PRIVATE
NAMESPACE=${NAMESPACE}
APPLICATION_NAME=ThunderTestRuntime
MODULE_NAME=ThunderTestRuntime
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MODULE_NAME is set both via target_compile_definitions(... MODULE_NAME=ThunderTestRuntime) and again via #define MODULE_NAME ThunderTestRuntime in Tests/test_support/Module.cpp. This can trigger macro redefinition warnings and also forces the module name for all compiled Thunder server sources in this archive. Prefer defining MODULE_NAME only in the dedicated Module.cpp (and drop the compile definition here), similar to how the main Thunder target sets APPLICATION_NAME/THREADPOOL_COUNT but not MODULE_NAME.

Suggested change
MODULE_NAME=ThunderTestRuntime

Copilot uses AI. Check for mistakes.
THREADPOOL_COUNT=${THREADPOOL_COUNT}
)

target_compile_options(thunder_test_support PRIVATE -Wno-psabi)

target_link_libraries(thunder_test_support
PRIVATE
CompileSettings::CompileSettings
)

if(EXCEPTION_CATCHING)
set_source_files_properties(${THUNDER_SOURCE_DIR}/PluginServer.cpp PROPERTIES COMPILE_FLAGS "-fexceptions")
endif()

target_include_directories(thunder_test_support
PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}
PRIVATE
${THUNDER_SOURCE_DIR}
$<BUILD_INTERFACE:${CMAKE_BINARY_DIR}/Source/plugins/generated/jsonrpc>
$<BUILD_INTERFACE:${CMAKE_BINARY_DIR}/Source/Thunder/generated>
)

target_link_libraries(thunder_test_support
PUBLIC
${NAMESPACE}Core::${NAMESPACE}Core
${NAMESPACE}Cryptalgo::${NAMESPACE}Cryptalgo
${NAMESPACE}COM::${NAMESPACE}COM
${NAMESPACE}Messaging::${NAMESPACE}Messaging
${NAMESPACE}WebSocket::${NAMESPACE}WebSocket
${NAMESPACE}Plugins::${NAMESPACE}Plugins
${NAMESPACE}COMProcess::${NAMESPACE}COMProcess
Threads::Threads
)

if(WARNING_REPORTING)
target_sources(thunder_test_support PRIVATE ${THUNDER_SOURCE_DIR}/WarningReportingCategories.cpp)
endif()

if(PROCESSCONTAINERS)
target_link_libraries(thunder_test_support
PUBLIC
${NAMESPACE}ProcessContainers::${NAMESPACE}ProcessContainers)
target_compile_definitions(thunder_test_support
PUBLIC
PROCESSCONTAINERS_ENABLED=1)
endif()

if(HIBERNATESUPPORT)
target_link_libraries(thunder_test_support PUBLIC
${NAMESPACE}Hibernate::${NAMESPACE}Hibernate)
target_compile_definitions(thunder_test_support PUBLIC
HIBERNATE_SUPPORT_ENABLED=1)
endif()

# --- Install rules ---
install(TARGETS thunder_test_support
ARCHIVE DESTINATION lib
)

install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/ThunderTestRuntime.h
DESTINATION include/thunder_test_support
)
Comment on lines +134 to +140
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Install rules hardcode lib/include instead of using GNUInstallDirs variables (${CMAKE_INSTALL_LIBDIR}, ${CMAKE_INSTALL_INCLUDEDIR}) used elsewhere in the project. Using the standard variables improves portability across distros/multilib layouts.

Copilot uses AI. Check for mistakes.
Comment on lines +68 to +140
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docs and this CMake target describe consumers linking against the thunder_test_support CMake target to pick up INTERFACE whole-archive link options, but the install rules here only install the .a and header—there is no target export / CMake package config for thunder_test_support, and the include dirs also lack an INSTALL_INTERFACE. If this library is intended for external consumers via find_package, it should be exported and install-interface include dirs should be set; otherwise the docs should avoid implying the imported target exists / carries link options.

Copilot uses AI. Check for mistakes.
13 changes: 13 additions & 0 deletions Tests/test_support/Module.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Module definition for the thunder_test_support static library.
// MODULE_NAME is set to ThunderTestRuntime via -D in CMakeLists.txt,
// which overrides Source/Thunder/Module.h's default of "Application".
// This ensures all server sources and the MODULE_NAME_DECLARATION below
// use the same symbol, and trace output shows "ThunderTestRuntime".

#ifndef MODULE_NAME
#define MODULE_NAME ThunderTestRuntime
#endif

#include <core/core.h>

MODULE_NAME_DECLARATION(BUILD_REFERENCE)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thunder has a macro for static archives — MODULE_NAME_ARCHIVE_DECLARATION. Using it in Module.cpp instead of MODULE_NAME_DECLARATION(BUILD_REFERENCE) avoids conflicting definitions of ModuleBuildRef(), GetModuleServices() and SetModuleServices() with the consumer's own MODULE_NAME_DECLARATION. It also removes the need for the #ifndef guard here.

#define MODULE_NAME ThunderTestRuntime
#include <core/core.h>
MODULE_NAME_ARCHIVE_DECLARATION

231 changes: 231 additions & 0 deletions Tests/test_support/ThunderTestRuntime.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
#include "ThunderTestRuntime.h"

#include <PluginServer.h>
#include <core/core.h>
#include <fstream>
#include <sstream>

// ==========================================================================
// ThunderTestRuntime implementation
//
// Lifecycle: Initialize() -> [run tests] -> Shutdown()
//
// Initialize creates a unique /tmp/thunder_test_XXXXXX/ directory tree,
// writes a minimal Thunder config.json, parses it into PluginHost::Config,
// constructs PluginHost::Server, and calls Server::Open() which boots
// the controller and activates auto-start plugins.
//
// Shutdown reverses the process: Server::Close(), cleanup temp files.
// ==========================================================================

namespace Thunder {
namespace TestCore {

ThunderTestRuntime::~ThunderTestRuntime()
{
Shutdown();
}

void ThunderTestRuntime::CreateDirectories() const
{
Core::Directory(_tempDir.c_str()).Create();
Core::Directory((_tempDir + "persistent/").c_str()).Create();
Core::Directory((_tempDir + "volatile/").c_str()).Create();
Core::Directory((_tempDir + "data/").c_str()).Create();
}

void ThunderTestRuntime::CleanupDirectories() const
{
if (!_tempDir.empty()) {
Core::Directory(_tempDir.c_str()).Destroy();
}
}

// Build a minimal Thunder JSON config from the plugin list.
// Uses port 0 (OS-assigned) and binds to localhost only.
string ThunderTestRuntime::BuildConfigJSON(const std::vector<PluginConfig>& plugins,
const string& systemPath,
const string& proxyStubPath) const
{
Comment thread
bramoosterhuis marked this conversation as resolved.
std::ostringstream json;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use Thunder Core JSON for JSON manipulation!

json << "{"
<< "\"port\":0,"
<< "\"binding\":\"127.0.0.1\","
<< "\"idletime\":180,"
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using "port":0 will bind the listener to an OS-assigned port, but Thunder’s Config (and thus Accessor()/URLs derived from it) will still report port 0 because SocketPort::Open() does not update NodeId after binding. If any plugin/test relies on the configured accessor URL/port, this will be incorrect. Consider choosing a free port up front (or querying the bound port via getsockname() and updating the config/binder/URL accordingly) so the runtime reports the real port.

Copilot uses AI. Check for mistakes.
<< "\"persistentpath\":\"" << _tempDir << "persistent/\","
<< "\"volatilepath\":\"" << _tempDir << "volatile/\","
<< "\"datapath\":\"" << _tempDir << "data/\","
<< "\"systempath\":\"" << systemPath << "\","
<< "\"proxystubpath\":\"" << proxyStubPath << "\","
<< "\"communicator\":\"" << _tempDir << "communicator\","
<< "\"plugins\":[";

for (size_t i = 0; i < plugins.size(); ++i) {
const auto& p = plugins[i];
if (i > 0) json << ",";
json << "{"
<< "\"callsign\":\"" << p.callsign << "\","
<< "\"locator\":\"" << p.locator << "\","
<< "\"classname\":\"" << p.classname << "\","
<< "\"startuporder\":" << p.startuporder << ","
<< "\"autostart\":" << (p.autostart ? "true" : "false");
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Plugin::Config does not have an autostart field (it uses startmode), so this JSON key will be ignored during config parsing and plugins will always use the default startmode (Activated). This breaks the documented behavior of PluginConfig::autostart=false. Map this flag to "startmode":"Deactivated" (and optionally "startmode":"Activated" when true) or drop the field and expose startmode directly in PluginConfig.

Suggested change
<< "\"autostart\":" << (p.autostart ? "true" : "false");
<< "\"startmode\":" << JsonEscape(p.autostart ? "Activated" : "Deactivated");

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no autostart in 5.x in anymore. Perhaps it's then better to actually reuse the plugin Configuration as defined in plugns/Configuration.h, that would've eliminate such issues.


if (!p.configuration.empty()) {
json << ",\"configuration\":" << p.configuration;
}

json << "}";
}

json << "]}";
return json.str();
}

uint32_t ThunderTestRuntime::Initialize(const std::vector<PluginConfig>& plugins,
const string& systemPath,
const string& proxyStubPath)
{
if (_server != nullptr) {
return Core::ERROR_ALREADY_CONNECTED;
}

// Create unique temp directory for this test run
char tempTemplate[] = "/tmp/thunder_test_XXXXXX";
char* tempResult = mkdtemp(tempTemplate);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For OS-abstraction use Thunder Core functionality (also to remove posix dependency).

if (tempResult == nullptr) {
return Core::ERROR_GENERAL;
}
_tempDir = string(tempResult) + "/";
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Initialize() hardcodes a POSIX /tmp/... path and uses mkdtemp(), which will not build on non-POSIX platforms (e.g., Windows) if ENABLE_TEST_RUNTIME is enabled. Either gate this feature to POSIX in CMake (with a clear message when enabled on unsupported platforms) or provide a platform-specific temp directory implementation.

Copilot uses AI. Check for mistakes.

CreateDirectories();

// Determine system path for plugin .so files
string sysPath = systemPath;
if (sysPath.empty()) {
sysPath = "/usr/lib/wpeframework/plugins/";
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Such path must not be hardcoded! On 5.x the path is different.

}
if (sysPath.back() != '/') {
sysPath += '/';
}

// Determine proxy stub path
_proxyStubPath = proxyStubPath;
if (_proxyStubPath.empty()) {
// Default: look next to system path
_proxyStubPath = sysPath + "../proxystubs/";
}
if (_proxyStubPath.back() != '/') {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use Thunder-provided functionality for normalizing paths!

_proxyStubPath += '/';
}

// Build and write config to temp file
string configJSON = BuildConfigJSON(plugins, sysPath, _proxyStubPath);
_configFilePath = _tempDir + "config.json";

{
std::ofstream configFile(_configFilePath);
if (!configFile.is_open()) {
CleanupDirectories();
return Core::ERROR_OPENING_FAILED;
}
configFile << configJSON;
}

// Parse config
Core::File configFile(_configFilePath);
if (configFile.Open(true) == false) {
CleanupDirectories();
return Core::ERROR_OPENING_FAILED;
}

Core::OptionalType<Core::JSON::Error> error;
_config = new PluginHost::Config(configFile, false, error);
configFile.Close();

if (error.IsSet()) {
delete _config;
_config = nullptr;
CleanupDirectories();
return Core::ERROR_INCOMPLETE_CONFIG;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is missing messaging startup.

// Create and start the server
_server = new PluginHost::Server(*_config, false);
_server->Open();

return Core::ERROR_NONE;
}

// Invoke a JSON-RPC method synchronously via the in-process dispatcher.
// Bypasses HTTP/WebSocket — calls IDispatcher::Invoke() directly.
uint32_t ThunderTestRuntime::InvokeJSONRPC(const string& callsign, const string& method,
const string& params, string& response)
{
if (_server == nullptr) {
return Core::ERROR_ILLEGAL_STATE;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Multiple return points are not as per coding standards.

}

Core::ProxyType<PluginHost::IShell> shell;
uint32_t result = _server->Services().FromIdentifier(callsign, shell);
if (result != Core::ERROR_NONE) {
return result;
}

PluginHost::IDispatcher* dispatcher = shell->QueryInterface<PluginHost::IDispatcher>();
if (dispatcher == nullptr) {
return Core::ERROR_UNAVAILABLE;
}

result = dispatcher->Invoke(0, 0, string(), method, params, response);
dispatcher->Release();

return result;
}

Core::ProxyType<PluginHost::IShell> ThunderTestRuntime::GetShell(const string& callsign)
{
Core::ProxyType<PluginHost::IShell> shell;
if (_server != nullptr) {
_server->Services().FromIdentifier(callsign, shell);
}
return shell;
}

PluginHost::Server& ThunderTestRuntime::Server()
{
ASSERT(_server != nullptr);
return *_server;
}

string ThunderTestRuntime::CommunicatorPath() const
{
if (_config != nullptr) {
return _config->Communicator().HostName();
}
return string();
}

void ThunderTestRuntime::Shutdown()
{
if (_server != nullptr) {
_server->Close();
delete _server;
_server = nullptr;
}

if (_config != nullptr) {
delete _config;
_config = nullptr;
}

if (!_configFilePath.empty()) {
Core::File(_configFilePath).Destroy();
_configFilePath.clear();
}

CleanupDirectories();
_tempDir.clear();
}

} // namespace TestCore
} // namespace Thunder
Loading
Loading