From 206f4123953d29c8a02b0c517f2ea63cf41a757c Mon Sep 17 00:00:00 2001 From: "Timmer, Daan" Date: Fri, 24 Jul 2026 12:02:51 +0000 Subject: [PATCH 01/29] feat: add plugin support --- .vscode/settings.json | 2 +- cmake/ccr_add_plugin.cmake | 41 ++++++ cucumber_cpp/CucumberCpp.hpp | 1 + cucumber_cpp/example/CMakeLists.txt | 5 + cucumber_cpp/example/plugin/CMakeLists.txt | 8 ++ cucumber_cpp/example/plugin/PluginHooks.cpp | 13 ++ cucumber_cpp/example/plugin/PluginSteps.cpp | 24 ++++ .../example/plugin/features/plugin.feature | 7 + cucumber_cpp/library/Application.cpp | 17 +++ cucumber_cpp/library/Application.hpp | 7 + cucumber_cpp/library/CMakeLists.txt | 2 + cucumber_cpp/library/api/RunCucumber.cpp | 12 +- .../cucumber_expression/ParameterRegistry.hpp | 32 ++++- cucumber_cpp/library/plugin/CMakeLists.txt | 25 ++++ .../library/plugin/DynamicLibrary.cpp | 124 ++++++++++++++++++ .../library/plugin/DynamicLibrary.hpp | 51 +++++++ .../library/plugin/DynamicLibraryManager.cpp | 67 ++++++++++ .../library/plugin/DynamicLibraryManager.hpp | 30 +++++ cucumber_cpp/library/plugin/HookLoader.cpp | 10 ++ cucumber_cpp/library/plugin/HookLoader.hpp | 18 +++ .../library/plugin/ParameterLoader.cpp | 27 ++++ .../library/plugin/ParameterLoader.hpp | 19 +++ cucumber_cpp/library/plugin/PluginExport.hpp | 16 +++ cucumber_cpp/library/plugin/StepLoader.cpp | 10 ++ cucumber_cpp/library/plugin/StepLoader.hpp | 18 +++ .../support/DefinitionRegistration.cpp | 6 + .../support/DefinitionRegistration.hpp | 2 + cucumber_cpp/runner/CMakeLists.txt | 4 + 28 files changed, 594 insertions(+), 4 deletions(-) create mode 100644 cmake/ccr_add_plugin.cmake create mode 100644 cucumber_cpp/example/plugin/CMakeLists.txt create mode 100644 cucumber_cpp/example/plugin/PluginHooks.cpp create mode 100644 cucumber_cpp/example/plugin/PluginSteps.cpp create mode 100644 cucumber_cpp/example/plugin/features/plugin.feature create mode 100644 cucumber_cpp/library/plugin/CMakeLists.txt create mode 100644 cucumber_cpp/library/plugin/DynamicLibrary.cpp create mode 100644 cucumber_cpp/library/plugin/DynamicLibrary.hpp create mode 100644 cucumber_cpp/library/plugin/DynamicLibraryManager.cpp create mode 100644 cucumber_cpp/library/plugin/DynamicLibraryManager.hpp create mode 100644 cucumber_cpp/library/plugin/HookLoader.cpp create mode 100644 cucumber_cpp/library/plugin/HookLoader.hpp create mode 100644 cucumber_cpp/library/plugin/ParameterLoader.cpp create mode 100644 cucumber_cpp/library/plugin/ParameterLoader.hpp create mode 100644 cucumber_cpp/library/plugin/PluginExport.hpp create mode 100644 cucumber_cpp/library/plugin/StepLoader.cpp create mode 100644 cucumber_cpp/library/plugin/StepLoader.hpp diff --git a/.vscode/settings.json b/.vscode/settings.json index 1d82165e..785ed2f5 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,7 +2,7 @@ "cmake.useCMakePresets": "always", "cucumberautocomplete.steps": [ "compatibility/*/*.cpp", - "cucumber_cpp/example/steps/*.cpp", + "cucumber_cpp/example/*/*.cpp", "cucumber_cpp/acceptance_test/steps/*.cpp" ], "cucumberautocomplete.gherkinDefinitionPart": "(GIVEN|WHEN|THEN|STEP)\\(", diff --git a/cmake/ccr_add_plugin.cmake b/cmake/ccr_add_plugin.cmake new file mode 100644 index 00000000..4c29b34e --- /dev/null +++ b/cmake/ccr_add_plugin.cmake @@ -0,0 +1,41 @@ +# Helper function for creating cucumber-cpp-runner plugin targets +# +# Usage: +# ccr_add_plugin(my_plugin) +# ccr_add_plugin(my_plugin step1.cpp step2.cpp hooks.cpp) +# +# Creates a MODULE library target suitable for dynamic loading via --load. +# Plugins use the same STEP/HOOK/PARAMETER macros as statically-linked code. +# All symbols resolve from the host executable at runtime (ENABLE_EXPORTS). +# On macOS, -undefined dynamic_lookup suppresses linker errors for host symbols. +# On Windows, the plugin links against the host executable's import library. +# +# Sources can be passed directly (like add_library) or added later via +# target_sources. + +function(ccr_add_plugin TARGET_NAME) + add_library(${TARGET_NAME} MODULE ${ARGN}) + + # Plugins need the header include paths from cucumber_cpp and its + # transitive dependencies (fmt, gtest, gherkin, etc.) but must NOT link + # the actual libraries — symbols resolve from the host at runtime. + target_include_directories(${TARGET_NAME} PRIVATE + $ + $ + ) + + target_compile_definitions(${TARGET_NAME} PRIVATE + $ + ) + + if (WIN32) + target_link_libraries(${TARGET_NAME} PRIVATE cucumber_cpp) + elseif (APPLE) + target_link_options(${TARGET_NAME} PRIVATE -undefined dynamic_lookup) + endif() + + set_target_properties(${TARGET_NAME} PROPERTIES + PREFIX "" + CXX_VISIBILITY_PRESET default + ) +endfunction() diff --git a/cucumber_cpp/CucumberCpp.hpp b/cucumber_cpp/CucumberCpp.hpp index f7a56f7a..d13bca62 100644 --- a/cucumber_cpp/CucumberCpp.hpp +++ b/cucumber_cpp/CucumberCpp.hpp @@ -8,6 +8,7 @@ #include "cucumber_cpp/library/Steps.hpp" #include "cucumber_cpp/library/cucumber_expression/MatchRange.hpp" #include "cucumber_cpp/library/cucumber_expression/ParameterRegistry.hpp" +#include "cucumber_cpp/library/plugin/PluginExport.hpp" namespace cucumber_cpp { diff --git a/cucumber_cpp/example/CMakeLists.txt b/cucumber_cpp/example/CMakeLists.txt index f8d2ef00..8c72db82 100644 --- a/cucumber_cpp/example/CMakeLists.txt +++ b/cucumber_cpp/example/CMakeLists.txt @@ -13,5 +13,10 @@ target_link_libraries(cucumber_cpp.example PRIVATE cucumber_cpp.example.steps ) +set_target_properties(cucumber_cpp.example PROPERTIES + ENABLE_EXPORTS ON +) + add_subdirectory(hooks) +add_subdirectory(plugin) add_subdirectory(steps) diff --git a/cucumber_cpp/example/plugin/CMakeLists.txt b/cucumber_cpp/example/plugin/CMakeLists.txt new file mode 100644 index 00000000..177e339b --- /dev/null +++ b/cucumber_cpp/example/plugin/CMakeLists.txt @@ -0,0 +1,8 @@ +include(${PROJECT_SOURCE_DIR}/cmake/ccr_add_plugin.cmake) + +ccr_add_plugin(cucumber_cpp.example.plugin) + +target_sources(cucumber_cpp.example.plugin PRIVATE + PluginSteps.cpp + PluginHooks.cpp +) diff --git a/cucumber_cpp/example/plugin/PluginHooks.cpp b/cucumber_cpp/example/plugin/PluginHooks.cpp new file mode 100644 index 00000000..3936c86c --- /dev/null +++ b/cucumber_cpp/example/plugin/PluginHooks.cpp @@ -0,0 +1,13 @@ +#include "cucumber_cpp/library/Hooks.hpp" +#include "cucumber_cpp/library/plugin/PluginExport.hpp" +#include + +HOOK_BEFORE_SCENARIO() +{ + std::cout << "PLUGIN_HOOK_BEFORE_SCENARIO\n"; +} + +HOOK_AFTER_SCENARIO() +{ + std::cout << "PLUGIN_HOOK_AFTER_SCENARIO\n"; +} diff --git a/cucumber_cpp/example/plugin/PluginSteps.cpp b/cucumber_cpp/example/plugin/PluginSteps.cpp new file mode 100644 index 00000000..4d10f489 --- /dev/null +++ b/cucumber_cpp/example/plugin/PluginSteps.cpp @@ -0,0 +1,24 @@ +#include "cucumber_cpp/library/Context.hpp" +#include "cucumber_cpp/library/Steps.hpp" +#include "cucumber_cpp/library/plugin/PluginExport.hpp" +#include + +GIVEN(R"(the greeter says {string})", (std::string greeting)) +{ + context.InsertAt("greeting", greeting); +} + +WHEN(R"(the user responds)") +{ + const auto& greeting = context.Get("greeting"); + context.InsertAt("response", std::string{ "You said: " + greeting }); +} + +THEN(R"(the conversation is complete)") +{ + [[maybe_unused]] const auto& response = context.Get("response"); +} + +extern "C" CCR_EXPORT void ccr_register() +{ +} diff --git a/cucumber_cpp/example/plugin/features/plugin.feature b/cucumber_cpp/example/plugin/features/plugin.feature new file mode 100644 index 00000000..468d2b18 --- /dev/null +++ b/cucumber_cpp/example/plugin/features/plugin.feature @@ -0,0 +1,7 @@ +Feature: Plugin example + Demonstrates steps loaded from a dynamic library plugin. + + Scenario: Greeting + Given the greeter says "Hello" + When the user responds + Then the conversation is complete diff --git a/cucumber_cpp/library/Application.cpp b/cucumber_cpp/library/Application.cpp index 51516fcc..b72dc5a9 100644 --- a/cucumber_cpp/library/Application.cpp +++ b/cucumber_cpp/library/Application.cpp @@ -6,6 +6,8 @@ #include "cucumber_cpp/library/api/RunCucumber.hpp" #include "cucumber_cpp/library/cucumber_expression/Errors.hpp" #include "cucumber_cpp/library/cucumber_expression/ParameterRegistry.hpp" +#include "cucumber_cpp/library/plugin/DynamicLibraryManager.hpp" +#include "cucumber_cpp/library/support/DefinitionRegistration.hpp" #include "cucumber_cpp/library/support/Types.hpp" #include "cucumber_cpp/library/tag_expression/Parser.hpp" #include "fmt/base.h" @@ -72,6 +74,16 @@ namespace cucumber_cpp::library cli.set_config("--config", "cucumber.toml"); } + Application::~Application() + { + if (!dynamicLibraryManager.GetLoadedLibraries().empty()) + { + dynamicLibraryManager.UnloadAll(); + support::DefinitionRegistration::Instance().Clear(); + cucumber_expression::ConverterTypeMapClearer::ClearAll(); + } + } + int Application::Run(int argc, const char* const* argv) { const auto formattersSet = formatters.GetAvailableFormatterNames(); @@ -125,6 +137,8 @@ namespace cucumber_cpp::library CLI::deprecate_option(cli.add_option("-f,--feature", options.paths, "Paths to where your feature files are"), "paths"); cli.add_option("paths", options.paths, "Paths to where your feature files are, defaults to \"./features\"")->default_val(options.paths); + cli.add_option("--load", options.loadPaths, "Load cucumber step/hook/parameter dynamic libraries from files or directories"); + ProgramContext().InsertRef(options); cli.parse(argc, argv); @@ -132,6 +146,9 @@ namespace cucumber_cpp::library if (options.dumpConfig) std::ofstream{ "cucumber.toml" } << cli.config_to_str(true, true); + if (!options.loadPaths.empty()) + dynamicLibraryManager.Load(options.loadPaths); + return RunFeatures(); } catch (const CLI::ParseError& e) diff --git a/cucumber_cpp/library/Application.hpp b/cucumber_cpp/library/Application.hpp index 94cabed4..0a93c323 100644 --- a/cucumber_cpp/library/Application.hpp +++ b/cucumber_cpp/library/Application.hpp @@ -7,6 +7,7 @@ #include "cucumber_cpp/library/Context.hpp" #include "cucumber_cpp/library/api/Formatters.hpp" #include "cucumber_cpp/library/cucumber_expression/ParameterRegistry.hpp" +#include "cucumber_cpp/library/plugin/DynamicLibraryManager.hpp" #include "cucumber_cpp/library/support/DefinitionRegistration.hpp" #include "cucumber_cpp/library/support/SupportCodeLibrary.hpp" #include "cucumber_cpp/library/support/Types.hpp" @@ -55,10 +56,14 @@ namespace cucumber_cpp::library bool recursive{ true }; std::vector tags{}; + + std::vector loadPaths{}; }; explicit Application(std::shared_ptr contextStorageFactory = std::make_shared(), bool removeDefaultGoogleTestListener = true); + ~Application(); + [[nodiscard]] int Run(int argc, const char* const* argv); CLI::App& CliParser(); @@ -82,6 +87,8 @@ namespace cucumber_cpp::library util::Broadcaster broadcaster; + plugin::DynamicLibraryManager dynamicLibraryManager; + cucumber_expression::ParameterRegistry parameterRegistry{ cucumber_cpp::library::support::DefinitionRegistration::Instance().GetRegisteredParameters() }; bool removeDefaultGoogleTestListener; util::StopWatchHighResolutionClock stopwatchHighResolutionClock; diff --git a/cucumber_cpp/library/CMakeLists.txt b/cucumber_cpp/library/CMakeLists.txt index 7b81f688..3cab5c8b 100644 --- a/cucumber_cpp/library/CMakeLists.txt +++ b/cucumber_cpp/library/CMakeLists.txt @@ -21,6 +21,7 @@ target_link_libraries(cucumber_cpp.library PUBLIC cucumber_gherkin_lib cucumber_cpp.library.api cucumber_cpp.library.engine + cucumber_cpp.library.plugin cucumber_cpp.library.tag_expression cucumber_cpp.library.util cucumber_cpp.library.formatter @@ -41,6 +42,7 @@ add_subdirectory(assemble) add_subdirectory(cucumber_expression) add_subdirectory(engine) add_subdirectory(formatter) +add_subdirectory(plugin) add_subdirectory(query) add_subdirectory(runtime) add_subdirectory(support) diff --git a/cucumber_cpp/library/api/RunCucumber.cpp b/cucumber_cpp/library/api/RunCucumber.cpp index 4bb82dc7..8f2864e2 100644 --- a/cucumber_cpp/library/api/RunCucumber.cpp +++ b/cucumber_cpp/library/api/RunCucumber.cpp @@ -13,6 +13,9 @@ #include "cucumber_cpp/library/api/Formatters.hpp" #include "cucumber_cpp/library/api/Gherkin.hpp" #include "cucumber_cpp/library/cucumber_expression/ParameterRegistry.hpp" +#include "cucumber_cpp/library/plugin/HookLoader.hpp" +#include "cucumber_cpp/library/plugin/ParameterLoader.hpp" +#include "cucumber_cpp/library/plugin/StepLoader.hpp" #include "cucumber_cpp/library/query/Query.hpp" #include "cucumber_cpp/library/runtime/MakeRuntime.hpp" #include "cucumber_cpp/library/support/DefinitionRegistration.hpp" @@ -121,15 +124,20 @@ namespace cucumber_cpp::library::api void EmitSupportCodeMessages(const support::SupportCodeLibrary& supportCodeLibrary, util::Broadcaster& broadcaster, const cucumber::gherkin::id_generator_ptr& idGenerator) { + // Phase 1: Load parameters (must be first, steps reference parameter types) + plugin::ParameterLoader::Load(support::DefinitionRegistration::Instance(), supportCodeLibrary.parameterRegistry); EmitParameters(supportCodeLibrary, broadcaster, idGenerator); support::DefinitionRegistration::Instance().LoadIds(idGenerator); - supportCodeLibrary.stepRegistry.LoadSteps(); + + // Phase 2: Load steps (can now resolve parameter type expressions) + plugin::StepLoader::Load(supportCodeLibrary.stepRegistry); EmitUndefinedParameters(supportCodeLibrary, broadcaster); EmitStepDefinitions(supportCodeLibrary, broadcaster); - supportCodeLibrary.hookRegistry.LoadHooks(); + // Phase 3: Load hooks (last, no ordering dependency) + plugin::HookLoader::Load(supportCodeLibrary.hookRegistry); EmitTestCaseHooks(supportCodeLibrary, broadcaster); EmitTestRunHooks(supportCodeLibrary, broadcaster); } diff --git a/cucumber_cpp/library/cucumber_expression/ParameterRegistry.hpp b/cucumber_cpp/library/cucumber_expression/ParameterRegistry.hpp index 9843f833..78693f6a 100644 --- a/cucumber_cpp/library/cucumber_expression/ParameterRegistry.hpp +++ b/cucumber_cpp/library/cucumber_expression/ParameterRegistry.hpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -138,10 +139,39 @@ namespace cucumber_cpp::library::cucumber_expression static TypeMap& Instance(); }; + struct ConverterTypeMapClearer + { + static std::vector>& ClearFunctions() + { + static std::vector> fns; + return fns; + } + + static void Register(std::function fn) + { + ClearFunctions().push_back(std::move(fn)); + } + + static void ClearAll() + { + for (const auto& fn : ClearFunctions()) + fn(); + } + }; + template TypeMap& ConverterTypeMap::Instance() { static TypeMap typeMap; + [[maybe_unused]] static const bool registered = [] + { + auto* ptr = &typeMap; + ConverterTypeMapClearer::Register([ptr] + { + ptr->clear(); + }); + return true; + }(); return typeMap; } @@ -161,9 +191,9 @@ namespace cucumber_cpp::library::cucumber_expression void AssertParameterIsUnique(const std::string& name) const; - private: void AddParameter(ParameterType parameter); + private: template void AddBuiltinParameter(std::string name, std::vector regex, ConverterFunction converter, bool preferForRegexMatch = false, std::source_location location = std::source_location::current()); diff --git a/cucumber_cpp/library/plugin/CMakeLists.txt b/cucumber_cpp/library/plugin/CMakeLists.txt new file mode 100644 index 00000000..5d5b6e59 --- /dev/null +++ b/cucumber_cpp/library/plugin/CMakeLists.txt @@ -0,0 +1,25 @@ +add_library(cucumber_cpp.library.plugin ${CCR_EXCLUDE_FROM_ALL}) + +target_sources(cucumber_cpp.library.plugin PRIVATE + DynamicLibrary.cpp + DynamicLibrary.hpp + DynamicLibraryManager.cpp + DynamicLibraryManager.hpp + HookLoader.cpp + HookLoader.hpp + ParameterLoader.cpp + ParameterLoader.hpp + PluginExport.hpp + StepLoader.cpp + StepLoader.hpp +) + +target_include_directories(cucumber_cpp.library.plugin PUBLIC + ../../.. +) + +target_link_libraries(cucumber_cpp.library.plugin PUBLIC + cucumber_cpp.library.support + cucumber_cpp.library.cucumber_expression + ${CMAKE_DL_LIBS} +) diff --git a/cucumber_cpp/library/plugin/DynamicLibrary.cpp b/cucumber_cpp/library/plugin/DynamicLibrary.cpp new file mode 100644 index 00000000..92569695 --- /dev/null +++ b/cucumber_cpp/library/plugin/DynamicLibrary.cpp @@ -0,0 +1,124 @@ +#include "cucumber_cpp/library/plugin/DynamicLibrary.hpp" +#include +#include +#include +#include +#include + +#if defined(_WIN32) +#define WIN32_LEAN_AND_MEAN +#include +#else +#include +#endif + +namespace cucumber_cpp::library::plugin +{ + namespace + { +#if defined(_WIN32) + std::string GetLastErrorMessage() + { + const DWORD error = GetLastError(); + + if (error == 0) + return "Unknown error"; + + LPSTR buffer = nullptr; + const auto size = FormatMessageA( + FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, + nullptr, error, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), + reinterpret_cast(&buffer), 0, nullptr); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + + std::string message(buffer, size); + LocalFree(buffer); + return message; + } +#endif + } + + DynamicLibrary::DynamicLibrary(const std::filesystem::path& path) + : libraryPath{ path } + { +#if defined(_WIN32) + handle = LoadLibraryW(path.c_str()); + + if (handle == nullptr) + throw std::runtime_error("Failed to load library '" + path.string() + "': " + GetLastErrorMessage()); +#else + handle = dlopen(path.c_str(), RTLD_NOW | RTLD_GLOBAL); // NOLINT(hicpp-signed-bitwise) + + if (handle == nullptr) + throw std::runtime_error("Failed to load library '" + path.string() + "': " + dlerror()); +#endif + } + + DynamicLibrary::~DynamicLibrary() + { + if (handle != nullptr) + { +#if defined(_WIN32) + FreeLibrary(static_cast(handle)); +#else + dlclose(handle); +#endif + } + } + + DynamicLibrary::DynamicLibrary(DynamicLibrary&& other) noexcept + : libraryPath{ std::move(other.libraryPath) } + , handle{ std::exchange(other.handle, nullptr) } + {} + + DynamicLibrary& DynamicLibrary::operator=(DynamicLibrary&& other) noexcept + { + if (this != &other) + { + if (handle != nullptr) + { +#if defined(_WIN32) + FreeLibrary(static_cast(handle)); +#else + dlclose(handle); +#endif + } + + libraryPath = std::move(other.libraryPath); + handle = std::exchange(other.handle, nullptr); + } + + return *this; + } + + const std::filesystem::path& DynamicLibrary::Path() const + { + return libraryPath; + } + + std::string_view DynamicLibrary::PlatformExtension() + { +#if defined(_WIN32) + return ".dll"; +#elif defined(__APPLE__) + return ".dylib"; +#else + return ".so"; +#endif + } + + void* DynamicLibrary::GetSymbolAddress(std::string_view name) const + { + const std::string symbolName{ name }; + +#if defined(_WIN32) + void* symbol = reinterpret_cast(GetProcAddress(static_cast(handle), symbolName.c_str())); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) +#else + void* symbol = dlsym(handle, symbolName.c_str()); +#endif + + if (symbol == nullptr) + throw std::runtime_error("Symbol '" + symbolName + "' not found in library '" + libraryPath.string() + "'"); + + return symbol; + } +} diff --git a/cucumber_cpp/library/plugin/DynamicLibrary.hpp b/cucumber_cpp/library/plugin/DynamicLibrary.hpp new file mode 100644 index 00000000..f7562b3e --- /dev/null +++ b/cucumber_cpp/library/plugin/DynamicLibrary.hpp @@ -0,0 +1,51 @@ +#ifndef PLUGIN_DYNAMIC_LIBRARY_HPP +#define PLUGIN_DYNAMIC_LIBRARY_HPP +#ifndef CUCUMBER_CPP_PLUGIN_DYNAMIC_LIBRARY_HPP +#define CUCUMBER_CPP_PLUGIN_DYNAMIC_LIBRARY_HPP + +#include +#include +#include + +namespace cucumber_cpp::library::plugin +{ + struct DynamicLibrary + { + explicit DynamicLibrary(const std::filesystem::path& path); + + ~DynamicLibrary(); + + DynamicLibrary(const DynamicLibrary&) = delete; + DynamicLibrary& operator=(const DynamicLibrary&) = delete; + + DynamicLibrary(DynamicLibrary&& other) noexcept; + DynamicLibrary& operator=(DynamicLibrary&& other) noexcept; + + template + FnPtr GetSymbol(std::string_view name) const; + + [[nodiscard]] const std::filesystem::path& Path() const; + + static std::string_view PlatformExtension(); + + private: + void* GetSymbolAddress(std::string_view name) const; + + std::filesystem::path libraryPath; + void* handle{ nullptr }; + }; + + ////////////////////////// + // implementation // + ////////////////////////// + + template + FnPtr DynamicLibrary::GetSymbol(std::string_view name) const + { + return reinterpret_cast(GetSymbolAddress(name)); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + } +} + +#endif + +#endif diff --git a/cucumber_cpp/library/plugin/DynamicLibraryManager.cpp b/cucumber_cpp/library/plugin/DynamicLibraryManager.cpp new file mode 100644 index 00000000..ad3bfba9 --- /dev/null +++ b/cucumber_cpp/library/plugin/DynamicLibraryManager.cpp @@ -0,0 +1,67 @@ +#include "cucumber_cpp/library/plugin/DynamicLibraryManager.hpp" +#include "cucumber_cpp/library/plugin/DynamicLibrary.hpp" +#include "cucumber_cpp/library/plugin/PluginExport.hpp" +#include +#include +#include +#include +#include + +namespace cucumber_cpp::library::plugin +{ + void DynamicLibraryManager::Load(const std::vector& paths) + { + for (const auto& path : paths) + { + const std::filesystem::path fsPath{ path }; + + if (std::filesystem::is_directory(fsPath)) + LoadDirectory(fsPath); + else if (std::filesystem::is_regular_file(fsPath)) + LoadFile(fsPath); + else + throw std::runtime_error("Path '" + path + "' is not a file or directory"); + } + } + + void DynamicLibraryManager::UnloadAll() + { + while (!libraries.empty()) + libraries.pop_back(); + } + + std::vector DynamicLibraryManager::GetLoadedLibraries() const + { + std::vector paths; + paths.reserve(libraries.size()); + + for (const auto& lib : libraries) + paths.push_back(lib.Path()); + + return paths; + } + + void DynamicLibraryManager::LoadFile(const std::filesystem::path& path) + { + auto& lib = libraries.emplace_back(path); + + auto registerFn = lib.GetSymbol("ccr_register"); + registerFn(); + } + + void DynamicLibraryManager::LoadDirectory(const std::filesystem::path& directory) + { + const auto extension = DynamicLibrary::PlatformExtension(); + + std::vector sortedPaths; + + for (const auto& entry : std::filesystem::directory_iterator{ directory }) + if (std::filesystem::is_regular_file(entry) && entry.path().extension() == extension) + sortedPaths.push_back(entry.path()); + + std::sort(sortedPaths.begin(), sortedPaths.end()); + + for (const auto& path : sortedPaths) + LoadFile(path); + } +} diff --git a/cucumber_cpp/library/plugin/DynamicLibraryManager.hpp b/cucumber_cpp/library/plugin/DynamicLibraryManager.hpp new file mode 100644 index 00000000..256ab2b1 --- /dev/null +++ b/cucumber_cpp/library/plugin/DynamicLibraryManager.hpp @@ -0,0 +1,30 @@ +#ifndef PLUGIN_DYNAMIC_LIBRARY_MANAGER_HPP +#define PLUGIN_DYNAMIC_LIBRARY_MANAGER_HPP +#ifndef CUCUMBER_CPP_PLUGIN_DYNAMIC_LIBRARY_MANAGER_HPP +#define CUCUMBER_CPP_PLUGIN_DYNAMIC_LIBRARY_MANAGER_HPP + +#include "cucumber_cpp/library/plugin/DynamicLibrary.hpp" +#include +#include +#include + +namespace cucumber_cpp::library::plugin +{ + struct DynamicLibraryManager + { + void Load(const std::vector& paths); + void UnloadAll(); + + [[nodiscard]] std::vector GetLoadedLibraries() const; + + private: + void LoadFile(const std::filesystem::path& path); + void LoadDirectory(const std::filesystem::path& directory); + + std::vector libraries; + }; +} + +#endif + +#endif diff --git a/cucumber_cpp/library/plugin/HookLoader.cpp b/cucumber_cpp/library/plugin/HookLoader.cpp new file mode 100644 index 00000000..463dd9a1 --- /dev/null +++ b/cucumber_cpp/library/plugin/HookLoader.cpp @@ -0,0 +1,10 @@ +#include "cucumber_cpp/library/plugin/HookLoader.hpp" +#include "cucumber_cpp/library/support/HookRegistry.hpp" + +namespace cucumber_cpp::library::plugin +{ + void HookLoader::Load(support::HookRegistry& hookRegistry) + { + hookRegistry.LoadHooks(); + } +} diff --git a/cucumber_cpp/library/plugin/HookLoader.hpp b/cucumber_cpp/library/plugin/HookLoader.hpp new file mode 100644 index 00000000..c33f0390 --- /dev/null +++ b/cucumber_cpp/library/plugin/HookLoader.hpp @@ -0,0 +1,18 @@ +#ifndef PLUGIN_HOOK_LOADER_HPP +#define PLUGIN_HOOK_LOADER_HPP +#ifndef CUCUMBER_CPP_PLUGIN_HOOK_LOADER_HPP +#define CUCUMBER_CPP_PLUGIN_HOOK_LOADER_HPP + +#include "cucumber_cpp/library/support/HookRegistry.hpp" + +namespace cucumber_cpp::library::plugin +{ + struct HookLoader + { + static void Load(support::HookRegistry& hookRegistry); + }; +} + +#endif + +#endif diff --git a/cucumber_cpp/library/plugin/ParameterLoader.cpp b/cucumber_cpp/library/plugin/ParameterLoader.cpp new file mode 100644 index 00000000..0b03478a --- /dev/null +++ b/cucumber_cpp/library/plugin/ParameterLoader.cpp @@ -0,0 +1,27 @@ +#include "cucumber_cpp/library/plugin/ParameterLoader.hpp" +#include "cucumber_cpp/library/cucumber_expression/ParameterRegistry.hpp" +#include "cucumber_cpp/library/support/DefinitionRegistration.hpp" +#include + +namespace cucumber_cpp::library::plugin +{ + void ParameterLoader::Load(support::DefinitionRegistration& registration, cucumber_expression::ParameterRegistry& parameterRegistry) + { + const auto& existingParameters = parameterRegistry.GetParameters(); + + for (const auto& parameter : registration.GetRegisteredParameters()) + { + if (!existingParameters.contains(parameter.params.name)) + { + parameterRegistry.AddParameter( + cucumber_expression::ParameterType{ + .name = parameter.params.name, + .regex = { std::string(parameter.params.regex) }, + .isBuiltin = false, + .useForSnippets = parameter.params.useForSnippets, + .location = parameter.location, + }); + } + } + } +} diff --git a/cucumber_cpp/library/plugin/ParameterLoader.hpp b/cucumber_cpp/library/plugin/ParameterLoader.hpp new file mode 100644 index 00000000..8c1fd07d --- /dev/null +++ b/cucumber_cpp/library/plugin/ParameterLoader.hpp @@ -0,0 +1,19 @@ +#ifndef PLUGIN_PARAMETER_LOADER_HPP +#define PLUGIN_PARAMETER_LOADER_HPP +#ifndef CUCUMBER_CPP_PLUGIN_PARAMETER_LOADER_HPP +#define CUCUMBER_CPP_PLUGIN_PARAMETER_LOADER_HPP + +#include "cucumber_cpp/library/cucumber_expression/ParameterRegistry.hpp" +#include "cucumber_cpp/library/support/DefinitionRegistration.hpp" + +namespace cucumber_cpp::library::plugin +{ + struct ParameterLoader + { + static void Load(support::DefinitionRegistration& registration, cucumber_expression::ParameterRegistry& parameterRegistry); + }; +} + +#endif + +#endif diff --git a/cucumber_cpp/library/plugin/PluginExport.hpp b/cucumber_cpp/library/plugin/PluginExport.hpp new file mode 100644 index 00000000..9908bcf7 --- /dev/null +++ b/cucumber_cpp/library/plugin/PluginExport.hpp @@ -0,0 +1,16 @@ +#ifndef PLUGIN_PLUGIN_EXPORT_HPP +#define PLUGIN_PLUGIN_EXPORT_HPP +#ifndef CUCUMBER_CPP_PLUGIN_EXPORT_HPP +#define CUCUMBER_CPP_PLUGIN_EXPORT_HPP + +#if defined(_WIN32) || defined(__CYGWIN__) +#define CCR_EXPORT __declspec(dllexport) +#else +#define CCR_EXPORT __attribute__((visibility("default"))) +#endif + +using CcrRegisterFn = void (*)(); + +#endif + +#endif diff --git a/cucumber_cpp/library/plugin/StepLoader.cpp b/cucumber_cpp/library/plugin/StepLoader.cpp new file mode 100644 index 00000000..8531cd8c --- /dev/null +++ b/cucumber_cpp/library/plugin/StepLoader.cpp @@ -0,0 +1,10 @@ +#include "cucumber_cpp/library/plugin/StepLoader.hpp" +#include "cucumber_cpp/library/support/StepRegistry.hpp" + +namespace cucumber_cpp::library::plugin +{ + void StepLoader::Load(support::StepRegistry& stepRegistry) + { + stepRegistry.LoadSteps(); + } +} diff --git a/cucumber_cpp/library/plugin/StepLoader.hpp b/cucumber_cpp/library/plugin/StepLoader.hpp new file mode 100644 index 00000000..4e5f0322 --- /dev/null +++ b/cucumber_cpp/library/plugin/StepLoader.hpp @@ -0,0 +1,18 @@ +#ifndef PLUGIN_STEP_LOADER_HPP +#define PLUGIN_STEP_LOADER_HPP +#ifndef CUCUMBER_CPP_PLUGIN_STEP_LOADER_HPP +#define CUCUMBER_CPP_PLUGIN_STEP_LOADER_HPP + +#include "cucumber_cpp/library/support/StepRegistry.hpp" + +namespace cucumber_cpp::library::plugin +{ + struct StepLoader + { + static void Load(support::StepRegistry& stepRegistry); + }; +} + +#endif + +#endif diff --git a/cucumber_cpp/library/support/DefinitionRegistration.cpp b/cucumber_cpp/library/support/DefinitionRegistration.cpp index f331c71b..f2279629 100644 --- a/cucumber_cpp/library/support/DefinitionRegistration.cpp +++ b/cucumber_cpp/library/support/DefinitionRegistration.cpp @@ -32,6 +32,12 @@ namespace cucumber_cpp::library::support return instance; } + void DefinitionRegistration::Clear() + { + registry.clear(); + customParameters.clear(); + } + void DefinitionRegistration::LoadIds(cucumber::gherkin::id_generator_ptr idGenerator) { const auto assignGenerator = [&idGenerator](auto& entry) diff --git a/cucumber_cpp/library/support/DefinitionRegistration.hpp b/cucumber_cpp/library/support/DefinitionRegistration.hpp index 6f1b9a33..1fb016e6 100644 --- a/cucumber_cpp/library/support/DefinitionRegistration.hpp +++ b/cucumber_cpp/library/support/DefinitionRegistration.hpp @@ -29,6 +29,8 @@ namespace cucumber_cpp::library::support public: static DefinitionRegistration& Instance(); + void Clear(); + void LoadIds(cucumber::gherkin::id_generator_ptr idGenerator); template diff --git a/cucumber_cpp/runner/CMakeLists.txt b/cucumber_cpp/runner/CMakeLists.txt index 97dafc8d..624223a9 100644 --- a/cucumber_cpp/runner/CMakeLists.txt +++ b/cucumber_cpp/runner/CMakeLists.txt @@ -7,3 +7,7 @@ target_sources(cucumber_cpp.runner PRIVATE target_link_libraries(cucumber_cpp.runner PUBLIC cucumber_cpp ) + +# Note: executables that link cucumber_cpp.runner and use --load for plugins +# must set ENABLE_EXPORTS ON to export symbols for dynamic library resolution: +# set_target_properties(my_executable PROPERTIES ENABLE_EXPORTS ON) From 2e16fea74fa7a0cac923a0d271651fb99bb54fe3 Mon Sep 17 00:00:00 2001 From: "Timmer, Daan" Date: Fri, 24 Jul 2026 14:01:37 +0000 Subject: [PATCH 02/29] Rework compatibility tests in to plugins --- cmake/ccr_add_plugin.cmake | 20 +- compatibility/BaseCompatibility.cpp | 230 ++++++------------ compatibility/BaseCompatibility.hpp | 26 +- compatibility/CMakeLists.txt | 84 ++++--- compatibility/TestCompatibility.cpp | 99 ++++++-- compatibility/attachments/attachments.cpp | 2 +- compatibility/empty/empty.cpp | 1 + .../examples-tables-attachment.cpp | 2 +- .../hooks-attachment/hooks-attachment.cpp | 2 +- .../retry-undefined/retry-undefined.cpp | 1 + cucumber_cpp/example/plugin/PluginHooks.cpp | 1 - cucumber_cpp/example/plugin/PluginSteps.cpp | 5 - .../library/plugin/PluginRegister.cpp | 4 + 13 files changed, 236 insertions(+), 241 deletions(-) create mode 100644 cucumber_cpp/library/plugin/PluginRegister.cpp diff --git a/cmake/ccr_add_plugin.cmake b/cmake/ccr_add_plugin.cmake index 4c29b34e..9df38dfe 100644 --- a/cmake/ccr_add_plugin.cmake +++ b/cmake/ccr_add_plugin.cmake @@ -10,11 +10,29 @@ # On macOS, -undefined dynamic_lookup suppresses linker errors for host symbols. # On Windows, the plugin links against the host executable's import library. # +# The default ccr_register entry point is provided automatically via +# ccr_plugin_register. Plugins do not need to define it themselves. +# # Sources can be passed directly (like add_library) or added later via # target_sources. +# Object library providing the default ccr_register entry point. +# Built once and linked into every plugin created by ccr_add_plugin. +if (NOT TARGET ccr_plugin_register) + add_library(ccr_plugin_register OBJECT + ${CMAKE_CURRENT_LIST_DIR}/../cucumber_cpp/library/plugin/PluginRegister.cpp + ) + target_include_directories(ccr_plugin_register PRIVATE + $ + $ + ) + set_target_properties(ccr_plugin_register PROPERTIES + CXX_VISIBILITY_PRESET default + ) +endif() + function(ccr_add_plugin TARGET_NAME) - add_library(${TARGET_NAME} MODULE ${ARGN}) + add_library(${TARGET_NAME} MODULE ${ARGN} $) # Plugins need the header include paths from cucumber_cpp and its # transitive dependencies (fmt, gtest, gherkin, etc.) but must NOT link diff --git a/compatibility/BaseCompatibility.cpp b/compatibility/BaseCompatibility.cpp index fb1da6e8..152d7092 100644 --- a/compatibility/BaseCompatibility.cpp +++ b/compatibility/BaseCompatibility.cpp @@ -1,38 +1,23 @@ #include "BaseCompatibility.hpp" -#include "cucumber/messages/envelope.hpp" #include "cucumber_cpp/Steps.hpp" -#include "cucumber_cpp/library/api/Formatters.hpp" -#include "cucumber_cpp/library/api/RunCucumber.hpp" -#include "cucumber_cpp/library/cucumber_expression/ParameterRegistry.hpp" -#include "cucumber_cpp/library/support/DefinitionRegistration.hpp" -#include "cucumber_cpp/library/support/Types.hpp" -#include "cucumber_cpp/library/tag_expression/Parser.hpp" -#include "cucumber_cpp/library/util/Broadcaster.hpp" -#include "cucumber_cpp/library/util/Duration.hpp" -#include "cucumber_cpp/library/util/Timestamp.hpp" +#include "cucumber_cpp/library/Application.hpp" #include "nlohmann/json.hpp" #include "nlohmann/json_fwd.hpp" -#include +#include "gtest/gtest.h" #include #include -#include -#include -#include -#include -#include #include #include -#include #include -#include #include #include +#include namespace compatibility { namespace { - void RemoveIncompatibilities(nlohmann::json& json, const Devkit& devkit) + void RemoveIncompatibilities(nlohmann::json& json, const std::string& sourceDir) { for (auto jsonIter = json.begin(); jsonIter != json.end();) { @@ -41,21 +26,18 @@ namespace compatibility if (key == "exception" || key == "message" || key == "line" || key == "snippets") jsonIter = json.erase(jsonIter); + else if (key == "timestamp" || key == "duration") + jsonIter = json.erase(jsonIter); else if (value.is_object()) { - RemoveIncompatibilities(value, devkit); + RemoveIncompatibilities(value, sourceDir); ++jsonIter; } else if (value.is_array()) { - for (auto valueIter = value.begin(); valueIter != value.end();) - { - if (valueIter->is_object()) - RemoveIncompatibilities(*valueIter, devkit); - - ++valueIter; - } - + for (auto& element : value) + if (element.is_object()) + RemoveIncompatibilities(element, sourceDir); ++jsonIter; } else if (key == "data") @@ -67,7 +49,7 @@ namespace compatibility { auto uri = value.get(); - uri = std::regex_replace(uri, std::regex(R"(samples\/[^\/]+)"), devkit.folder); + uri = std::regex_replace(uri, std::regex(R"(samples\/[^\/]+)"), sourceDir); uri = std::regex_replace(uri, std::regex(R"(\.ts$)"), ".cpp"); std::filesystem::path path{ uri }; @@ -83,170 +65,94 @@ namespace compatibility } } - struct BroadcastListener + std::list LoadNdjson(const std::filesystem::path& path) { - BroadcastListener(std::filesystem::path ndjsonin, std::filesystem::path expectedndjson, std::filesystem::path ndout, cucumber_cpp::library::util::Broadcaster& broadcaster) - : listener(broadcaster, [this](const cucumber::messages::envelope& envelope) - { - OnEvent(envelope); - }) - , ndjsonin{ std::move(ndjsonin) } - , expectedndjson{ std::move(expectedndjson) } - , actualndjson(std::move(ndout)) - { - while (!ifs.eof()) - { - std::string line; - std::getline(ifs, line); - - if (line.empty()) - continue; - - auto json = nlohmann::json::parse(line); - - if (json.contains("meta")) - continue; - - expectedEnvelopes.emplace_back(std::move(json)); - } - } - - void OnEvent(const cucumber::messages::envelope& envelope) - { - nlohmann::json actualJson{}; - to_json(actualJson, envelope); - - actualEnvelopes.emplace_back(std::move(actualJson)); - } + std::list envelopes; + std::ifstream ifs{ path }; + std::string line; - void CompareEnvelopes(const Devkit& devkit) + while (std::getline(ifs, line)) { - EXPECT_THAT(actualEnvelopes.size(), testing::Eq(expectedEnvelopes.size())); + if (line.empty()) + continue; - for (auto& json : actualEnvelopes) - { - RemoveIncompatibilities(json, devkit); - actualOfs << json.dump() << "\n"; - } + auto json = nlohmann::json::parse(line); - for (auto& json : expectedEnvelopes) - { - RemoveIncompatibilities(json, devkit); - expectedOfs << json.dump() << "\n"; - } + if (json.contains("meta")) + continue; - while (!actualEnvelopes.empty() && !expectedEnvelopes.empty()) - { - auto actualJson = actualEnvelopes.front(); - actualEnvelopes.pop_front(); - - auto expectedJson = expectedEnvelopes.front(); - expectedEnvelopes.pop_front(); - - EXPECT_THAT(actualJson, testing::Eq(expectedJson)); - } + envelopes.emplace_back(std::move(json)); } - private: - cucumber_cpp::library::util::Listener listener; - std::filesystem::path ndjsonin; - std::ifstream ifs{ ndjsonin }; - - std::filesystem::path expectedndjson; - std::ofstream expectedOfs{ expectedndjson }; - - std::filesystem::path actualndjson; - std::ofstream actualOfs{ actualndjson }; - - std::list expectedEnvelopes; - std::list actualEnvelopes; - }; - - bool IsFeatureFile(const std::filesystem::directory_entry& entry) - { - return std::filesystem::is_regular_file(entry) && entry.path().has_extension() && entry.path().extension() == ".feature"; + return envelopes; } - std::set> GetFeatureFiles(const std::set>& paths) + void CompareEnvelopes(std::list& actual, std::list& expected, const std::string& sourceDir, const std::filesystem::path& kitDir) { - std::set> files; - - for (const auto& feature : paths) - if (std::filesystem::is_directory(feature)) - for (const auto& entry : std::filesystem::directory_iterator{ feature } | std::views::filter(IsFeatureFile)) - { - std::cout << " found feature file: " << entry.path() << "\n"; - files.insert(entry.path()); - } - else - files.insert(feature); + for (auto& json : actual) + RemoveIncompatibilities(json, sourceDir); + for (auto& json : expected) + RemoveIncompatibilities(json, sourceDir); - return files; - } - - struct StopwatchIncremental : cucumber_cpp::library::util::Stopwatch - { - virtual ~StopwatchIncremental() = default; - - std::chrono::high_resolution_clock::time_point Start() override + // Write out normalized envelopes for debugging { - return {}; + std::ofstream ofs{ kitDir / "actual.ndjson" }; + for (const auto& json : actual) + ofs << json.dump() << "\n"; } - - std::chrono::nanoseconds Duration([[maybe_unused]] std::chrono::high_resolution_clock::time_point timePoint) override { - return current; + std::ofstream ofs{ kitDir / "expected.ndjson" }; + for (const auto& json : expected) + ofs << json.dump() << "\n"; } - std::chrono::nanoseconds current{ std::chrono::milliseconds{ 1 } }; - }; + EXPECT_EQ(actual.size(), expected.size()); - struct TimestampGeneratorIncremental : cucumber_cpp::library::util::TimestampGenerator - { - virtual ~TimestampGeneratorIncremental() = default; + auto actualIter = actual.begin(); + auto expectedIter = expected.begin(); - std::chrono::milliseconds Now() override + while (actualIter != actual.end() && expectedIter != expected.end()) { - return current++; + EXPECT_EQ(*actualIter, *expectedIter); + ++actualIter; + ++expectedIter; } - - std::chrono::milliseconds current{ 0 }; - }; + } } - void RunDevkit(Devkit devkit) + void RunDevkit(const KitInfo& kit) { - compatibility::StopwatchIncremental stopwatch; - compatibility::TimestampGeneratorIncremental timestampGenerator; + const auto actualNdjsonPath = kit.sourceDir / "actual_run.ndjson"; - devkit.paths = GetFeatureFiles(devkit.paths); - const auto isReversed = devkit.kitString.ends_with("-reversed"); + // Build CLI args: program name, --load , --format message:, extra args, -- + std::vector argStrings; + argStrings.emplace_back("compatibility.test"); + argStrings.emplace_back("--load"); + argStrings.emplace_back(kit.pluginPath.string()); + argStrings.emplace_back("--format"); + argStrings.emplace_back("message:" + actualNdjsonPath.string()); - cucumber_cpp::library::support::RunOptions runOptions{ - .sources = { - .paths = devkit.paths, - .tagExpression = cucumber_cpp::library::tag_expression::Parse(devkit.tagExpression), - .ordering = isReversed ? cucumber_cpp::library::support::RunOptions::Ordering::reverse : cucumber_cpp::library::support::RunOptions::Ordering::defined, - }, - .runtime = { - .retry = devkit.kitString.starts_with("retry") ? 2u : 0u, - .strict = true, - .retryTagExpression = cucumber_cpp::library::tag_expression::Parse(""), - }, - }; + for (const auto& arg : kit.extraArgs) + argStrings.emplace_back(arg); - cucumber_cpp::library::cucumber_expression::ParameterRegistry parameterRegistry{ cucumber_cpp::library::support::DefinitionRegistration::Instance().GetRegisteredParameters() }; + argStrings.emplace_back("--no-recursive"); + argStrings.emplace_back(kit.sourceDir.string()); - auto contextStorageFactory{ std::make_shared() }; - auto programContext{ std::make_unique(contextStorageFactory) }; + std::vector argv; + argv.reserve(argStrings.size()); + for (const auto& s : argStrings) + argv.push_back(s.c_str()); - cucumber_cpp::library::util::Broadcaster broadcaster; + { + cucumber_cpp::library::Application app{ std::make_shared(), false }; + static_cast(app.Run(static_cast(argv.size()), argv.data())); + } - BroadcastListener broadcastListener{ devkit.ndjsonFile, devkit.ndjsonFile.parent_path() / "expected.ndjson", devkit.ndjsonFile.parent_path() / "actual.ndjson", broadcaster }; + auto actualEnvelopes = LoadNdjson(actualNdjsonPath); + auto expectedEnvelopes = LoadNdjson(kit.ndjsonFile); - cucumber_cpp::library::api::Formatters formatters; - cucumber_cpp::library::api::RunCucumber(runOptions, parameterRegistry, *programContext, broadcaster, formatters, { "junit", "message", "pretty", "summary", "usage" }, {}); + CompareEnvelopes(actualEnvelopes, expectedEnvelopes, kit.sourceDir.string(), kit.sourceDir); - broadcastListener.CompareEnvelopes(devkit); + std::filesystem::remove(actualNdjsonPath); } } diff --git a/compatibility/BaseCompatibility.hpp b/compatibility/BaseCompatibility.hpp index b594aa3f..248f32a0 100644 --- a/compatibility/BaseCompatibility.hpp +++ b/compatibility/BaseCompatibility.hpp @@ -1,27 +1,29 @@ #ifndef COMPATIBILITY_BASE_COMPATIBILITY_HPP #define COMPATIBILITY_BASE_COMPATIBILITY_HPP -#include #include -#include -#include -#include -#include +#include #include +#include namespace compatibility { - struct Devkit + struct KitInfo { - std::set> paths; - std::string tagExpression; - std::size_t retry; + std::string name; + std::filesystem::path sourceDir; std::filesystem::path ndjsonFile; - std::string folder; - std::string kitString; + std::filesystem::path pluginPath; + std::vector extraArgs; + bool hasPlugin; }; - void RunDevkit(Devkit devkit); + inline void PrintTo(const KitInfo& kit, std::ostream* os) + { + *os << kit.name; + } + + void RunDevkit(const KitInfo& kit); } #endif diff --git a/compatibility/CMakeLists.txt b/compatibility/CMakeLists.txt index b9372459..65fb627f 100644 --- a/compatibility/CMakeLists.txt +++ b/compatibility/CMakeLists.txt @@ -1,3 +1,5 @@ +include(${PROJECT_SOURCE_DIR}/cmake/ccr_add_plugin.cmake) + add_library(compatibility.base) target_sources(compatibility.base PRIVATE BaseCompatibility.cpp @@ -18,49 +20,57 @@ target_compile_definitions(compatibility.base PRIVATE CCR_BINARY_DIR="${CCR_BINARY_DIR_CMAKE}" ) -function(add_compatibility_kit name) - set(libname compatibility.${name}.test) - add_executable(${libname}) - - if(CCR_BUILD_TESTS) - add_test(NAME ${libname} COMMAND ${libname}) +# Build a plugin for each kit that has a .cpp file +file(GLOB kits RELATIVE ${CMAKE_CURRENT_LIST_DIR} ${CMAKE_CURRENT_LIST_DIR}/*) +foreach(kit ${kits}) + if (IS_DIRECTORY ${CMAKE_CURRENT_LIST_DIR}/${kit} AND EXISTS ${CMAKE_CURRENT_LIST_DIR}/${kit}/${kit}.cpp) + ccr_add_plugin(compatibility.${kit}.plugin ${kit}/${kit}.cpp) endif() +endforeach() - target_link_libraries(${libname} PRIVATE - GTest::gtest_main - compatibility.base - ) +# Single test executable that loads plugins at runtime +add_executable(compatibility.test) +target_sources(compatibility.test PRIVATE + TestCompatibility.cpp +) - target_include_directories(${libname} PUBLIC - $ - ) +target_link_libraries(compatibility.test PRIVATE + GTest::gtest_main + compatibility.base +) - target_sources(${libname} PRIVATE - TestCompatibility.cpp +# Plugins resolve symbols from this executable at runtime via dlopen. +# ENABLE_EXPORTS creates the dynamic symbol table, but the linker +# normally strips unreferenced symbols from static libraries. +# -rdynamic combined with --whole-archive ensures all cucumber_cpp +# symbols are linked and exported for plugin use. +if (NOT WIN32) + target_link_options(compatibility.test PRIVATE + "LINKER:--whole-archive" + $ + $ + $ + $ + $ + $ + $ + "LINKER:--no-whole-archive" ) +endif() - if (EXISTS ${CMAKE_CURRENT_LIST_DIR}/${name}/${name}.cpp) - target_sources(${libname} PRIVATE - ${name}/${name}.cpp - ) - else() - set(SKIP_TEST On) - endif() +set_target_properties(compatibility.test PROPERTIES + ENABLE_EXPORTS ON +) - string(REPLACE "-" "_" KITNAME ${name}) +file(TO_CMAKE_PATH "${CMAKE_CURRENT_SOURCE_DIR}" COMPAT_SOURCE_DIR_CMAKE) +file(TO_CMAKE_PATH "${CMAKE_BINARY_DIR}" COMPAT_BUILD_DIR_CMAKE) - target_compile_definitions(${libname} PRIVATE - KIT_NAME=${KITNAME} - KIT_STRING="${name}" - KIT_FOLDER="${CMAKE_CURRENT_SOURCE_DIR}/${name}" - KIT_NDJSON_FILE="${CMAKE_CURRENT_SOURCE_DIR}/${name}/${name}.ndjson" - $<$:SKIP_TEST> - ) -endfunction() +target_compile_definitions(compatibility.test PRIVATE + COMPAT_SOURCE_DIR="${COMPAT_SOURCE_DIR_CMAKE}" + COMPAT_BUILD_DIR="${COMPAT_BUILD_DIR_CMAKE}" + COMPAT_PLUGIN_DIR="$" +) -file(GLOB kits RELATIVE ${CMAKE_CURRENT_LIST_DIR} ${CMAKE_CURRENT_LIST_DIR}/*) -foreach(kit ${kits}) - if (IS_DIRECTORY ${CMAKE_CURRENT_LIST_DIR}/${kit}) - add_compatibility_kit(${kit}) - endif() -endforeach() +if(CCR_BUILD_TESTS) + gtest_discover_tests(compatibility.test) +endif() diff --git a/compatibility/TestCompatibility.cpp b/compatibility/TestCompatibility.cpp index 02ddc3bb..7b886669 100644 --- a/compatibility/TestCompatibility.cpp +++ b/compatibility/TestCompatibility.cpp @@ -1,26 +1,85 @@ #include "BaseCompatibility.hpp" +#include "cucumber_cpp/library/plugin/DynamicLibrary.hpp" +#include "gtest/gtest.h" +#include +#include +#include #include +#include -#ifndef KIT_FOLDER -#error KIT_FOLDER is not defined -#endif +namespace compatibility +{ + namespace + { + std::vector DiscoverKits() + { + std::vector kits; -#ifndef KIT_NDJSON_FILE -#error KIT_NDJSON_FILE is not defined -#endif + const std::filesystem::path sourceDir{ COMPAT_SOURCE_DIR }; + const std::filesystem::path pluginDir{ COMPAT_PLUGIN_DIR }; -TEST(CompatibilityTest, KIT_NAME) -{ -#ifdef SKIP_TEST - GTEST_SKIP(); -#endif - - compatibility::RunDevkit({ - .paths = { KIT_FOLDER }, - .tagExpression = "", - .retry = 0, - .ndjsonFile = KIT_NDJSON_FILE, - .folder = KIT_FOLDER, - .kitString = KIT_STRING, - }); + for (const auto& entry : std::filesystem::directory_iterator{ sourceDir }) + { + if (!entry.is_directory()) + continue; + + const auto name = entry.path().filename().string(); + const auto ndjsonFile = entry.path() / (name + ".ndjson"); + + if (!std::filesystem::exists(ndjsonFile)) + continue; + + const auto pluginName = "compatibility." + name + ".plugin" + std::string{ cucumber_cpp::library::plugin::DynamicLibrary::PlatformExtension() }; + const auto pluginPath = pluginDir / pluginName; + + std::vector extraArgs; + const auto argsFile = entry.path() / (name + ".arguments.txt"); + if (std::filesystem::exists(argsFile)) + { + std::ifstream ifs{ argsFile }; + std::string arg; + while (ifs >> arg) + extraArgs.push_back(arg); + } + + kits.push_back(KitInfo{ + .name = name, + .sourceDir = entry.path(), + .ndjsonFile = ndjsonFile, + .pluginPath = pluginPath, + .extraArgs = std::move(extraArgs), + .hasPlugin = std::filesystem::exists(entry.path() / (name + ".cpp")), + }); + } + + std::ranges::sort(kits, {}, &KitInfo::name); + return kits; + } + + std::string KitInfoToName(const testing::TestParamInfo& info) + { + auto name = info.param.name; + std::ranges::replace(name, '-', '_'); + return name; + } + } + + class CompatibilityTest : public testing::TestWithParam + {}; + + TEST_P(CompatibilityTest, Kit) + { + const auto& kit = GetParam(); + + if (!kit.hasPlugin) + GTEST_SKIP() << "No plugin for kit: " << kit.name; + + RunDevkit(kit); + } + + INSTANTIATE_TEST_SUITE_P( + Compatibility, + CompatibilityTest, + testing::ValuesIn(DiscoverKits()), + KitInfoToName); } diff --git a/compatibility/attachments/attachments.cpp b/compatibility/attachments/attachments.cpp index eb23409d..6687ad40 100644 --- a/compatibility/attachments/attachments.cpp +++ b/compatibility/attachments/attachments.cpp @@ -10,7 +10,7 @@ namespace { - const std::filesystem::path compatibilityPath = std::filesystem::path{ KIT_FOLDER }; + const std::filesystem::path compatibilityPath = std::filesystem::path{ std::source_location::current().file_name() }.parent_path(); } WHEN(R"(the string {string} is attached as {string})", (const std::string& text, const std::string& mediaType)) diff --git a/compatibility/empty/empty.cpp b/compatibility/empty/empty.cpp index e69de29b..f7d1ed55 100644 --- a/compatibility/empty/empty.cpp +++ b/compatibility/empty/empty.cpp @@ -0,0 +1 @@ +// intentionally empty - no steps or hooks for this kit diff --git a/compatibility/examples-tables-attachment/examples-tables-attachment.cpp b/compatibility/examples-tables-attachment/examples-tables-attachment.cpp index 155f9301..2e165df5 100644 --- a/compatibility/examples-tables-attachment/examples-tables-attachment.cpp +++ b/compatibility/examples-tables-attachment/examples-tables-attachment.cpp @@ -8,7 +8,7 @@ namespace { - const std::filesystem::path compatibilityPath = std::filesystem::path{ KIT_FOLDER }; + const std::filesystem::path compatibilityPath = std::filesystem::path{ std::source_location::current().file_name() }.parent_path(); } WHEN(R"(a JPEG image is attached)") diff --git a/compatibility/hooks-attachment/hooks-attachment.cpp b/compatibility/hooks-attachment/hooks-attachment.cpp index 6731236a..9643ae39 100644 --- a/compatibility/hooks-attachment/hooks-attachment.cpp +++ b/compatibility/hooks-attachment/hooks-attachment.cpp @@ -7,7 +7,7 @@ namespace { - const std::filesystem::path compatibilityPath = std::filesystem::path{ KIT_FOLDER }; + const std::filesystem::path compatibilityPath = std::filesystem::path{ std::source_location::current().file_name() }.parent_path(); } HOOK_BEFORE_SCENARIO() diff --git a/compatibility/retry-undefined/retry-undefined.cpp b/compatibility/retry-undefined/retry-undefined.cpp index e69de29b..f7d1ed55 100644 --- a/compatibility/retry-undefined/retry-undefined.cpp +++ b/compatibility/retry-undefined/retry-undefined.cpp @@ -0,0 +1 @@ +// intentionally empty - no steps or hooks for this kit diff --git a/cucumber_cpp/example/plugin/PluginHooks.cpp b/cucumber_cpp/example/plugin/PluginHooks.cpp index 3936c86c..263a879f 100644 --- a/cucumber_cpp/example/plugin/PluginHooks.cpp +++ b/cucumber_cpp/example/plugin/PluginHooks.cpp @@ -1,5 +1,4 @@ #include "cucumber_cpp/library/Hooks.hpp" -#include "cucumber_cpp/library/plugin/PluginExport.hpp" #include HOOK_BEFORE_SCENARIO() diff --git a/cucumber_cpp/example/plugin/PluginSteps.cpp b/cucumber_cpp/example/plugin/PluginSteps.cpp index 4d10f489..b335912a 100644 --- a/cucumber_cpp/example/plugin/PluginSteps.cpp +++ b/cucumber_cpp/example/plugin/PluginSteps.cpp @@ -1,6 +1,5 @@ #include "cucumber_cpp/library/Context.hpp" #include "cucumber_cpp/library/Steps.hpp" -#include "cucumber_cpp/library/plugin/PluginExport.hpp" #include GIVEN(R"(the greeter says {string})", (std::string greeting)) @@ -18,7 +17,3 @@ THEN(R"(the conversation is complete)") { [[maybe_unused]] const auto& response = context.Get("response"); } - -extern "C" CCR_EXPORT void ccr_register() -{ -} diff --git a/cucumber_cpp/library/plugin/PluginRegister.cpp b/cucumber_cpp/library/plugin/PluginRegister.cpp new file mode 100644 index 00000000..5462bf4f --- /dev/null +++ b/cucumber_cpp/library/plugin/PluginRegister.cpp @@ -0,0 +1,4 @@ +#include "cucumber_cpp/library/plugin/PluginExport.hpp" + +extern "C" CCR_EXPORT void ccr_register() +{} From 9a88e4c4af9448a40049a2c040950c57bc8d1896 Mon Sep 17 00:00:00 2001 From: "Timmer, Daan" Date: Fri, 24 Jul 2026 14:15:27 +0000 Subject: [PATCH 03/29] some cleanup --- compatibility/BaseCompatibility.cpp | 5 +++-- compatibility/TestCompatibility.cpp | 7 ++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/compatibility/BaseCompatibility.cpp b/compatibility/BaseCompatibility.cpp index 152d7092..7ce3e878 100644 --- a/compatibility/BaseCompatibility.cpp +++ b/compatibility/BaseCompatibility.cpp @@ -3,6 +3,7 @@ #include "cucumber_cpp/library/Application.hpp" #include "nlohmann/json.hpp" #include "nlohmann/json_fwd.hpp" +#include "gmock/gmock.h" #include "gtest/gtest.h" #include #include @@ -106,14 +107,14 @@ namespace compatibility ofs << json.dump() << "\n"; } - EXPECT_EQ(actual.size(), expected.size()); + EXPECT_THAT(actual.size(), testing::Eq(expected.size())); auto actualIter = actual.begin(); auto expectedIter = expected.begin(); while (actualIter != actual.end() && expectedIter != expected.end()) { - EXPECT_EQ(*actualIter, *expectedIter); + EXPECT_THAT(*actualIter, testing::Eq(*expectedIter)); ++actualIter; ++expectedIter; } diff --git a/compatibility/TestCompatibility.cpp b/compatibility/TestCompatibility.cpp index 7b886669..7fc50904 100644 --- a/compatibility/TestCompatibility.cpp +++ b/compatibility/TestCompatibility.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include namespace compatibility @@ -62,10 +63,10 @@ namespace compatibility std::ranges::replace(name, '-', '_'); return name; } - } - class CompatibilityTest : public testing::TestWithParam - {}; + class CompatibilityTest : public testing::TestWithParam + {}; + } TEST_P(CompatibilityTest, Kit) { From 47f8e64b865dc885508839012998a52a3cee3b3f Mon Sep 17 00:00:00 2001 From: "Timmer, Daan" Date: Fri, 24 Jul 2026 14:17:32 +0000 Subject: [PATCH 04/29] fix include guards --- cucumber_cpp/library/plugin/DynamicLibrary.hpp | 4 ---- cucumber_cpp/library/plugin/DynamicLibraryManager.hpp | 4 ---- cucumber_cpp/library/plugin/HookLoader.hpp | 4 ---- cucumber_cpp/library/plugin/ParameterLoader.hpp | 4 ---- cucumber_cpp/library/plugin/PluginExport.hpp | 4 ---- cucumber_cpp/library/plugin/StepLoader.hpp | 4 ---- 6 files changed, 24 deletions(-) diff --git a/cucumber_cpp/library/plugin/DynamicLibrary.hpp b/cucumber_cpp/library/plugin/DynamicLibrary.hpp index f7562b3e..faf4a58c 100644 --- a/cucumber_cpp/library/plugin/DynamicLibrary.hpp +++ b/cucumber_cpp/library/plugin/DynamicLibrary.hpp @@ -1,7 +1,5 @@ #ifndef PLUGIN_DYNAMIC_LIBRARY_HPP #define PLUGIN_DYNAMIC_LIBRARY_HPP -#ifndef CUCUMBER_CPP_PLUGIN_DYNAMIC_LIBRARY_HPP -#define CUCUMBER_CPP_PLUGIN_DYNAMIC_LIBRARY_HPP #include #include @@ -47,5 +45,3 @@ namespace cucumber_cpp::library::plugin } #endif - -#endif diff --git a/cucumber_cpp/library/plugin/DynamicLibraryManager.hpp b/cucumber_cpp/library/plugin/DynamicLibraryManager.hpp index 256ab2b1..335b6892 100644 --- a/cucumber_cpp/library/plugin/DynamicLibraryManager.hpp +++ b/cucumber_cpp/library/plugin/DynamicLibraryManager.hpp @@ -1,7 +1,5 @@ #ifndef PLUGIN_DYNAMIC_LIBRARY_MANAGER_HPP #define PLUGIN_DYNAMIC_LIBRARY_MANAGER_HPP -#ifndef CUCUMBER_CPP_PLUGIN_DYNAMIC_LIBRARY_MANAGER_HPP -#define CUCUMBER_CPP_PLUGIN_DYNAMIC_LIBRARY_MANAGER_HPP #include "cucumber_cpp/library/plugin/DynamicLibrary.hpp" #include @@ -26,5 +24,3 @@ namespace cucumber_cpp::library::plugin } #endif - -#endif diff --git a/cucumber_cpp/library/plugin/HookLoader.hpp b/cucumber_cpp/library/plugin/HookLoader.hpp index c33f0390..f012ddd9 100644 --- a/cucumber_cpp/library/plugin/HookLoader.hpp +++ b/cucumber_cpp/library/plugin/HookLoader.hpp @@ -1,7 +1,5 @@ #ifndef PLUGIN_HOOK_LOADER_HPP #define PLUGIN_HOOK_LOADER_HPP -#ifndef CUCUMBER_CPP_PLUGIN_HOOK_LOADER_HPP -#define CUCUMBER_CPP_PLUGIN_HOOK_LOADER_HPP #include "cucumber_cpp/library/support/HookRegistry.hpp" @@ -14,5 +12,3 @@ namespace cucumber_cpp::library::plugin } #endif - -#endif diff --git a/cucumber_cpp/library/plugin/ParameterLoader.hpp b/cucumber_cpp/library/plugin/ParameterLoader.hpp index 8c1fd07d..99ac2dfa 100644 --- a/cucumber_cpp/library/plugin/ParameterLoader.hpp +++ b/cucumber_cpp/library/plugin/ParameterLoader.hpp @@ -1,7 +1,5 @@ #ifndef PLUGIN_PARAMETER_LOADER_HPP #define PLUGIN_PARAMETER_LOADER_HPP -#ifndef CUCUMBER_CPP_PLUGIN_PARAMETER_LOADER_HPP -#define CUCUMBER_CPP_PLUGIN_PARAMETER_LOADER_HPP #include "cucumber_cpp/library/cucumber_expression/ParameterRegistry.hpp" #include "cucumber_cpp/library/support/DefinitionRegistration.hpp" @@ -15,5 +13,3 @@ namespace cucumber_cpp::library::plugin } #endif - -#endif diff --git a/cucumber_cpp/library/plugin/PluginExport.hpp b/cucumber_cpp/library/plugin/PluginExport.hpp index 9908bcf7..9d3ab028 100644 --- a/cucumber_cpp/library/plugin/PluginExport.hpp +++ b/cucumber_cpp/library/plugin/PluginExport.hpp @@ -1,7 +1,5 @@ #ifndef PLUGIN_PLUGIN_EXPORT_HPP #define PLUGIN_PLUGIN_EXPORT_HPP -#ifndef CUCUMBER_CPP_PLUGIN_EXPORT_HPP -#define CUCUMBER_CPP_PLUGIN_EXPORT_HPP #if defined(_WIN32) || defined(__CYGWIN__) #define CCR_EXPORT __declspec(dllexport) @@ -12,5 +10,3 @@ using CcrRegisterFn = void (*)(); #endif - -#endif diff --git a/cucumber_cpp/library/plugin/StepLoader.hpp b/cucumber_cpp/library/plugin/StepLoader.hpp index 4e5f0322..06eda5ba 100644 --- a/cucumber_cpp/library/plugin/StepLoader.hpp +++ b/cucumber_cpp/library/plugin/StepLoader.hpp @@ -1,7 +1,5 @@ #ifndef PLUGIN_STEP_LOADER_HPP #define PLUGIN_STEP_LOADER_HPP -#ifndef CUCUMBER_CPP_PLUGIN_STEP_LOADER_HPP -#define CUCUMBER_CPP_PLUGIN_STEP_LOADER_HPP #include "cucumber_cpp/library/support/StepRegistry.hpp" @@ -14,5 +12,3 @@ namespace cucumber_cpp::library::plugin } #endif - -#endif From 2e05c2e2b61b76d1e3322e76bc42f05bdffdbec7 Mon Sep 17 00:00:00 2001 From: "Timmer, Daan" Date: Fri, 24 Jul 2026 14:35:43 +0000 Subject: [PATCH 05/29] use libraries.clear() --- cucumber_cpp/library/plugin/DynamicLibraryManager.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/cucumber_cpp/library/plugin/DynamicLibraryManager.cpp b/cucumber_cpp/library/plugin/DynamicLibraryManager.cpp index ad3bfba9..a544be0e 100644 --- a/cucumber_cpp/library/plugin/DynamicLibraryManager.cpp +++ b/cucumber_cpp/library/plugin/DynamicLibraryManager.cpp @@ -26,8 +26,7 @@ namespace cucumber_cpp::library::plugin void DynamicLibraryManager::UnloadAll() { - while (!libraries.empty()) - libraries.pop_back(); + libraries.clear(); } std::vector DynamicLibraryManager::GetLoadedLibraries() const @@ -43,7 +42,7 @@ namespace cucumber_cpp::library::plugin void DynamicLibraryManager::LoadFile(const std::filesystem::path& path) { - auto& lib = libraries.emplace_back(path); + const auto& lib = libraries.emplace_back(path); auto registerFn = lib.GetSymbol("ccr_register"); registerFn(); @@ -59,7 +58,7 @@ namespace cucumber_cpp::library::plugin if (std::filesystem::is_regular_file(entry) && entry.path().extension() == extension) sortedPaths.push_back(entry.path()); - std::sort(sortedPaths.begin(), sortedPaths.end()); + std::ranges::sort(sortedPaths); for (const auto& path : sortedPaths) LoadFile(path); From 14beb6200774303e1795ba00317da04e67f9d1cb Mon Sep 17 00:00:00 2001 From: "Timmer, Daan" Date: Fri, 24 Jul 2026 14:35:47 +0000 Subject: [PATCH 06/29] Add flow and object diagramas --- .../library/plugin/PLUGIN_ARCHITECTURE.md | 208 ++++++++++++++++++ 1 file changed, 208 insertions(+) create mode 100644 cucumber_cpp/library/plugin/PLUGIN_ARCHITECTURE.md diff --git a/cucumber_cpp/library/plugin/PLUGIN_ARCHITECTURE.md b/cucumber_cpp/library/plugin/PLUGIN_ARCHITECTURE.md new file mode 100644 index 00000000..b2fbd771 --- /dev/null +++ b/cucumber_cpp/library/plugin/PLUGIN_ARCHITECTURE.md @@ -0,0 +1,208 @@ +# Plugin Architecture + +## Object Diagram + +```mermaid +classDiagram + class Application { + -Options options + -CLI::App cli + -Broadcaster broadcaster + -DynamicLibraryManager dynamicLibraryManager + -ParameterRegistry parameterRegistry + -Formatters formatters + +Run(argc, argv) int + +~Application() + } + + class DynamicLibraryManager { + -vector~DynamicLibrary~ libraries + +Load(paths) + +UnloadAll() + +GetLoadedLibraries() vector~path~ + -LoadFile(path) + -LoadDirectory(path) + } + + class DynamicLibrary { + -void* handle + -path libraryPath + +DynamicLibrary(path) + +~DynamicLibrary() + +GetSymbol~FnPtr~(name) FnPtr + +PlatformExtension()$ string_view + } + + class DefinitionRegistration { + -map registry + -set customParameters + +Instance()$ DefinitionRegistration& + +Register~Body~() size_t + +Clear() + +ForEachRegisteredStep() + +GetHooks() + +GetRegisteredParameters() + } + + class ParameterLoader { + +Load(defReg, paramReg)$ + } + + class StepLoader { + +Load(stepRegistry)$ + } + + class HookLoader { + +Load(hookRegistry)$ + } + + class PluginRegister { + +ccr_register() + } + + class ConverterTypeMapClearer { + +ClearAll()$ + } + + Application *-- DynamicLibraryManager + DynamicLibraryManager *-- "0..*" DynamicLibrary + DynamicLibrary ..> PluginRegister : resolves ccr_register + Application ..> DefinitionRegistration : clears on destroy + Application ..> ConverterTypeMapClearer : clears on destroy + ParameterLoader ..> DefinitionRegistration : reads parameters + StepLoader ..> DefinitionRegistration : reads steps + HookLoader ..> DefinitionRegistration : reads hooks +``` + +## Data Flow + +```mermaid +flowchart TB + subgraph build["Build Phase"] + src["Plugin Source\nSTEP / HOOK / PARAMETER macros"] + reg["ccr_plugin_register\nObject Library"] + mod["MODULE Library\n.so / .dylib / .dll"] + src --> mod + reg --> mod + end + + subgraph load["Load Phase — Application::Run"] + cli["Parse --load paths"] + mgr["DynamicLibraryManager::Load"] + dir{"Directory\nor File?"} + scan["Scan & sort by\nplatform extension"] + open["dlopen / LoadLibrary\nRTLD_NOW | RTLD_GLOBAL"] + sym["dlsym ccr_register\nValidation gate"] + call["Invoke ccr_register"] + static["Plugin static constructors\nalready ran on dlopen"] + singleton["DefinitionRegistration\nsingleton populated"] + + cli --> mgr --> dir + dir -->|directory| scan --> open + dir -->|file| open + open --> sym --> call + open -.->|side effect| static --> singleton + end + + subgraph run["Execution Phase — RunCucumber"] + p1["Phase 1: ParameterLoader\nCustom parameters → ParameterRegistry"] + p2["Phase 2: StepLoader\nStep definitions → StepRegistry"] + p3["Phase 3: HookLoader\nHooks → HookRegistry"] + exec["Runtime executes\nscenarios"] + + p1 --> p2 --> p3 --> exec + end + + subgraph cleanup["Cleanup Phase — ~Application"] + unload["DynamicLibraryManager::UnloadAll\nlibraries.clear → dlclose"] + clear["DefinitionRegistration::Clear\nregistry + customParameters"] + clearTypes["ConverterTypeMapClearer::ClearAll"] + + unload --> clear --> clearTypes + end + + build --> load --> run --> cleanup +``` + +## Sequence Diagram — Plugin Lifecycle + +```mermaid +sequenceDiagram + actor User + participant App as Application + participant CLI as CLI11 Parser + participant DLM as DynamicLibraryManager + participant DL as DynamicLibrary + participant OS as OS (dlopen) + participant Static as Plugin Static Constructors + participant DR as DefinitionRegistration + participant RC as RunCucumber + participant PL as ParameterLoader + participant SL as StepLoader + participant HL as HookLoader + + User ->> App: Run(argc, argv) + App ->> CLI: parse("--load plugin.so -- features/") + CLI -->> App: options.loadPaths = ["plugin.so"] + + rect rgb(220, 245, 220) + Note over App, DR: Plugin Loading + App ->> DLM: Load(["plugin.so"]) + DLM ->> DL: DynamicLibrary("plugin.so") + DL ->> OS: dlopen("plugin.so", RTLD_NOW | RTLD_GLOBAL) + OS -->> Static: Static constructors execute + Static ->> DR: Register("pattern", ...) + Static ->> DR: Register(hookType, ...) + OS -->> DL: handle + DL ->> DL: GetSymbol("ccr_register") + DL -->> DLM: ccr_register validated & called + end + + rect rgb(220, 230, 250) + Note over App, HL: Three-Phase Registration + App ->> RC: RunCucumber(options, paramReg, ...) + + RC ->> PL: Load(defReg, paramReg) + PL ->> DR: GetRegisteredParameters() + DR -->> PL: custom parameter entries + PL -->> RC: Parameters registered + + RC ->> SL: Load(stepRegistry) + SL ->> DR: ForEachRegisteredStep() + DR -->> SL: step entries with matchers + SL -->> RC: Steps registered + + RC ->> HL: Load(hookRegistry) + HL ->> DR: GetHooks() + DR -->> HL: hook entries + HL -->> RC: Hooks registered + end + + rect rgb(250, 230, 230) + Note over App, DR: Cleanup + App ->> DLM: UnloadAll() + DLM ->> DL: ~DynamicLibrary() + DL ->> OS: dlclose(handle) + App ->> DR: Clear() + App ->> App: ConverterTypeMapClearer::ClearAll() + end +``` + +## Cleanup Guard + +The `Application` destructor only clears registrations when plugins were loaded, +preserving statically-linked step definitions between runs: + +```mermaid +flowchart TD + dest["~Application()"] + check{"dynamicLibraryManager\n.GetLoadedLibraries()\nempty?"} + skip["No cleanup needed\nStatic registrations preserved"] + unload["UnloadAll()\nlibraries.clear()"] + clearDef["DefinitionRegistration\n::Instance().Clear()"] + clearConv["ConverterTypeMapClearer\n::ClearAll()"] + + dest --> check + check -->|yes| skip + check -->|no| unload --> clearDef --> clearConv +``` From 67d94697d88145c0602804cbb13bcb4eb3c174bd Mon Sep 17 00:00:00 2001 From: "Timmer, Daan" Date: Fri, 24 Jul 2026 15:14:54 +0000 Subject: [PATCH 07/29] Implement dynamic plugin support with snapshot management in DefinitionRegistration --- cucumber_cpp/library/Application.cpp | 18 +-- .../library/plugin/PLUGIN_ARCHITECTURE.md | 135 +++++++++++++++--- .../support/DefinitionRegistration.cpp | 39 +++-- .../support/DefinitionRegistration.hpp | 38 +++-- 4 files changed, 178 insertions(+), 52 deletions(-) diff --git a/cucumber_cpp/library/Application.cpp b/cucumber_cpp/library/Application.cpp index b72dc5a9..70a1bbaf 100644 --- a/cucumber_cpp/library/Application.cpp +++ b/cucumber_cpp/library/Application.cpp @@ -1,4 +1,10 @@ #include "cucumber_cpp/library/Application.hpp" +#include "CLI/App.hpp" +#include "CLI/CLI.hpp" +#include "CLI/Error.hpp" +#include "CLI/Option.hpp" +#include "CLI/Validators.hpp" +#include "CLI/impl/App_inl.hpp" #include "cucumber/gherkin/demangle.hpp" #include "cucumber_cpp/library/Context.hpp" #include "cucumber_cpp/library/Errors.hpp" @@ -13,19 +19,12 @@ #include "fmt/base.h" #include "fmt/format.h" #include "fmt/ranges.h" -#include -#include -#include -#include -#include -#include #include #include #include #include #include #include -#include #include #include #include @@ -53,7 +52,7 @@ namespace cucumber_cpp::library foundFiles.emplace(entry.path()); } - std::set> GetFeatureFiles(Application::Options& options) + std::set> GetFeatureFiles(const Application::Options& options) { std::set> foundFiles; @@ -68,10 +67,11 @@ namespace cucumber_cpp::library } Application::Application(std::shared_ptr contextStorageFactory, bool removeDefaultGoogleTestListener) - : contextStorageFactory{ contextStorageFactory } + : contextStorageFactory{ std::move(contextStorageFactory) } , removeDefaultGoogleTestListener{ removeDefaultGoogleTestListener } { cli.set_config("--config", "cucumber.toml"); + support::DefinitionRegistration::Instance().TakeSnapshot(); } Application::~Application() diff --git a/cucumber_cpp/library/plugin/PLUGIN_ARCHITECTURE.md b/cucumber_cpp/library/plugin/PLUGIN_ARCHITECTURE.md index b2fbd771..50557a35 100644 --- a/cucumber_cpp/library/plugin/PLUGIN_ARCHITECTURE.md +++ b/cucumber_cpp/library/plugin/PLUGIN_ARCHITECTURE.md @@ -1,5 +1,30 @@ # Plugin Architecture +This document describes the dynamic plugin loading system for cucumber-cpp-runner. +Plugins are shared libraries (`.so` / `.dylib` / `.dll`) that register steps, hooks, +and parameter types at runtime via the same macros used in statically-linked code. + +--- + +## Overview + +The plugin system enables loading cucumber step definitions, hooks, and custom +parameter types from shared libraries at runtime via the `--load` CLI option. +This allows test code to be compiled independently of the main runner executable +and loaded on demand. + +Key design principles: + +- **Transparent authoring** — plugin source files use the same `STEP`, `HOOK_*`, + and `PARAMETER` macros as statically-linked code. +- **Static registration preserved** — destroying an `Application` only removes + dynamically-loaded definitions; statically-linked registrations survive. +- **Cross-platform** — works on Linux (`.so`), macOS (`.dylib`), and Windows (`.dll`). +- **Validation** — each plugin must export a `ccr_register` symbol (provided + automatically by `ccr_add_plugin`) as an entry-point validation gate. + +--- + ## Object Diagram ```mermaid @@ -34,10 +59,13 @@ classDiagram } class DefinitionRegistration { + -map staticRegistry + -set staticParameters -map registry -set customParameters +Instance()$ DefinitionRegistration& +Register~Body~() size_t + +TakeSnapshot() +Clear() +ForEachRegisteredStep() +GetHooks() @@ -67,13 +95,33 @@ classDiagram Application *-- DynamicLibraryManager DynamicLibraryManager *-- "0..*" DynamicLibrary DynamicLibrary ..> PluginRegister : resolves ccr_register - Application ..> DefinitionRegistration : clears on destroy + Application ..> DefinitionRegistration : snapshot + restore Application ..> ConverterTypeMapClearer : clears on destroy ParameterLoader ..> DefinitionRegistration : reads parameters StepLoader ..> DefinitionRegistration : reads steps HookLoader ..> DefinitionRegistration : reads hooks ``` +**`Application`** owns the `DynamicLibraryManager` and orchestrates the full +lifecycle: CLI parsing → snapshot → plugin loading → execution → cleanup. + +**`DynamicLibraryManager`** manages plugin lifetime. It holds a vector of +`DynamicLibrary` RAII wrappers. `Load()` accepts file paths or directories; +directories are scanned for platform-matching shared libraries and loaded in +sorted order. + +**`DynamicLibrary`** is a move-only RAII wrapper around `dlopen`/`LoadLibrary`. +Construction opens the library; destruction closes it. `GetSymbol(name)` +resolves a typed function pointer. + +**`DefinitionRegistration`** is the global singleton holding all step, hook, and +parameter registrations. Entries are keyed by `std::source_location`. The +dual-container design (`TakeSnapshot` / `Clear`) separates static and +dynamic entries: `TakeSnapshot` moves current entries to dedicated static +containers, and `Clear` only removes the dynamic ones. + +--- + ## Data Flow ```mermaid @@ -88,20 +136,21 @@ flowchart TB subgraph load["Load Phase — Application::Run"] cli["Parse --load paths"] + snap["TakeSnapshot()\nMove entries to static containers"] mgr["DynamicLibraryManager::Load"] dir{"Directory\nor File?"} scan["Scan & sort by\nplatform extension"] open["dlopen / LoadLibrary\nRTLD_NOW | RTLD_GLOBAL"] sym["dlsym ccr_register\nValidation gate"] - call["Invoke ccr_register"] - static["Plugin static constructors\nalready ran on dlopen"] + invoke["Invoke ccr_register"] + pluginInit["Plugin static constructors\nalready ran on dlopen"] singleton["DefinitionRegistration\nsingleton populated"] - cli --> mgr --> dir + cli --> snap --> mgr --> dir dir -->|directory| scan --> open dir -->|file| open - open --> sym --> call - open -.->|side effect| static --> singleton + open --> sym --> invoke + open -.-> pluginInit --> singleton end subgraph run["Execution Phase — RunCucumber"] @@ -115,15 +164,37 @@ flowchart TB subgraph cleanup["Cleanup Phase — ~Application"] unload["DynamicLibraryManager::UnloadAll\nlibraries.clear → dlclose"] - clear["DefinitionRegistration::Clear\nregistry + customParameters"] - clearTypes["ConverterTypeMapClearer::ClearAll"] + restore["DefinitionRegistration::Clear()\nRemove only dynamic entries"] + clearTypes["ConverterTypeMapClearer::ClearAll\nMaps repopulated next run"] - unload --> clear --> clearTypes + unload --> restore --> clearTypes end build --> load --> run --> cleanup ``` +The data flow has four phases: + +1. **Build** — plugin sources are compiled into MODULE shared libraries. The + `ccr_add_plugin` CMake function automatically links the `ccr_plugin_register` + object library which provides the `ccr_register` entry point. + +2. **Load** — `Application::Run` parses `--load` paths, takes a registry + snapshot (capturing the static baseline), then delegates to + `DynamicLibraryManager`. Each library is opened with `RTLD_NOW | RTLD_GLOBAL`, + which triggers the plugin's static constructors — these register steps/hooks/ + parameters into the global `DefinitionRegistration` singleton. + +3. **Execution** — `RunCucumber` reads from `DefinitionRegistration` in three + ordered phases: parameters first (so step expressions can reference custom + types), then steps, then hooks. + +4. **Cleanup** — the `Application` destructor unloads plugins (`dlclose`), then + calls `Clear()` which removes only the dynamic entries (those registered + after `TakeSnapshot`), preserving statically-linked definitions. + +--- + ## Sequence Diagram — Plugin Lifecycle ```mermaid @@ -146,7 +217,9 @@ sequenceDiagram CLI -->> App: options.loadPaths = ["plugin.so"] rect rgb(220, 245, 220) - Note over App, DR: Plugin Loading + Note over App, DR: Snapshot & Plugin Loading + App ->> DR: TakeSnapshot() + Note right of DR: Move current entries
to static containers App ->> DLM: Load(["plugin.so"]) DLM ->> DL: DynamicLibrary("plugin.so") DL ->> OS: dlopen("plugin.so", RTLD_NOW | RTLD_GLOBAL) @@ -184,25 +257,51 @@ sequenceDiagram DLM ->> DL: ~DynamicLibrary() DL ->> OS: dlclose(handle) App ->> DR: Clear() + Note right of DR: Remove only dynamic entries
Static containers preserved App ->> App: ConverterTypeMapClearer::ClearAll() end ``` -## Cleanup Guard +--- + +## Snapshot & Cleanup -The `Application` destructor only clears registrations when plugins were loaded, -preserving statically-linked step definitions between runs: +The `Application` destructor uses a snapshot-based approach so that statically- +linked step/hook/parameter definitions are never lost — only plugin-injected +entries are removed. ```mermaid flowchart TD dest["~Application()"] check{"dynamicLibraryManager\n.GetLoadedLibraries()\nempty?"} - skip["No cleanup needed\nStatic registrations preserved"] - unload["UnloadAll()\nlibraries.clear()"] - clearDef["DefinitionRegistration\n::Instance().Clear()"] - clearConv["ConverterTypeMapClearer\n::ClearAll()"] + skip["No cleanup needed"] + unload["UnloadAll()\nlibraries.clear() → dlclose"] + restore["Clear()\nRemove dynamic registry\nand customParameters"] + clearConv["ConverterTypeMapClearer::ClearAll()\nType maps repopulated next run"] + done["Static registrations intact\nReady for next Application"] dest --> check check -->|yes| skip - check -->|no| unload --> clearDef --> clearConv + check -->|no| unload --> restore --> clearConv --> done ``` + +**How it works:** + +1. Before plugins are loaded, `TakeSnapshot()` moves the current `registry` + and `customParameters` into separate `staticRegistry` and `staticParameters` + containers. This represents the "static baseline" — everything registered + by code compiled directly into the executable. + +2. Plugins load and their static constructors add new entries to the (now + empty) `registry` and `customParameters` containers. + +3. On destruction, `Clear()` clears only the dynamic `registry` and + `customParameters`. The static containers are untouched. + +4. `ConverterTypeMapClearer::ClearAll()` clears all converter type maps. This + is safe because these maps are repopulated from `DefinitionRegistration` + contents at the start of each `RunCucumber` call (Phase 1: ParameterLoader). + +**Consequence**: Multiple sequential `Application` instances in the same process +will correctly share statically-linked definitions while each getting a fresh +set of plugin-loaded definitions. diff --git a/cucumber_cpp/library/support/DefinitionRegistration.cpp b/cucumber_cpp/library/support/DefinitionRegistration.cpp index f2279629..6001b7b0 100644 --- a/cucumber_cpp/library/support/DefinitionRegistration.cpp +++ b/cucumber_cpp/library/support/DefinitionRegistration.cpp @@ -38,6 +38,15 @@ namespace cucumber_cpp::library::support customParameters.clear(); } + void DefinitionRegistration::TakeSnapshot() + { + if (!staticRegistry.empty() || !staticParameters.empty()) + return; + + staticRegistry.merge(registry); + staticParameters.merge(customParameters); + } + void DefinitionRegistration::LoadIds(cucumber::gherkin::id_generator_ptr idGenerator) { const auto assignGenerator = [&idGenerator](auto& entry) @@ -45,26 +54,34 @@ namespace cucumber_cpp::library::support entry.id = idGenerator->next_id(); }; + for (auto& [key, item] : staticRegistry) + std::visit(assignGenerator, item); + for (auto& [key, item] : registry) std::visit(assignGenerator, item); } std::vector DefinitionRegistration::GetHooks() { - auto allSteps = registry | std::views::values | std::views::filter([](const Entry& entry) - { - return std::holds_alternative(entry); - }) | - std::views::transform([](const Entry& entry) - { - return std::get(entry); - }); - return { allSteps.begin(), allSteps.end() }; + std::vector result; + + auto collectHooks = [&result](auto& reg) + { + for (auto& [key, entry] : reg) + if (std::holds_alternative(entry)) + result.push_back(std::get(entry)); + }; + + collectHooks(staticRegistry); + collectHooks(registry); + return result; } - const std::set>& DefinitionRegistration::GetRegisteredParameters() const + std::set> DefinitionRegistration::GetRegisteredParameters() const { - return customParameters; + auto result = staticParameters; + result.insert(customParameters.begin(), customParameters.end()); + return result; } std::size_t DefinitionRegistration::Register(Hook hook, util::HookType hookType, util::HookFactory factory, std::source_location sourceLocation) diff --git a/cucumber_cpp/library/support/DefinitionRegistration.hpp b/cucumber_cpp/library/support/DefinitionRegistration.hpp index 1fb016e6..7c617ccd 100644 --- a/cucumber_cpp/library/support/DefinitionRegistration.hpp +++ b/cucumber_cpp/library/support/DefinitionRegistration.hpp @@ -30,6 +30,7 @@ namespace cucumber_cpp::library::support static DefinitionRegistration& Instance(); void Clear(); + void TakeSnapshot(); void LoadIds(cucumber::gherkin::id_generator_ptr idGenerator); @@ -38,7 +39,7 @@ namespace cucumber_cpp::library::support std::vector GetHooks(); - const std::set>& GetRegisteredParameters() const; + [[nodiscard]] std::set> GetRegisteredParameters() const; template static std::size_t Register(Hook hook, util::HookType hookType, std::source_location sourceLocation = std::source_location::current()); @@ -57,6 +58,9 @@ namespace cucumber_cpp::library::support std::size_t Register(GlobalHook hook, util::HookType hookType, util::HookFactory factory, std::source_location sourceLocation); std::size_t Register(std::string_view matcher, StepType stepType, util::StepFactory factory, std::source_location sourceLocation); + std::map staticRegistry; + std::set> staticParameters; + std::map registry; std::set> customParameters; }; @@ -68,19 +72,25 @@ namespace cucumber_cpp::library::support template void DefinitionRegistration::ForEachRegisteredStep(const T& func) { - auto allSteps = registry | - std::views::values | - std::views::filter([](const Entry& entry) - { - return std::holds_alternative(entry); - }) | - std::views::transform([](const Entry& entry) - { - return std::get(entry); - }); - - for (const auto& step : allSteps) - func(step); + auto forEachStep = [&func](auto& reg) + { + auto allSteps = reg | + std::views::values | + std::views::filter([](const Entry& entry) + { + return std::holds_alternative(entry); + }) | + std::views::transform([](const Entry& entry) + { + return std::get(entry); + }); + + for (const auto& step : allSteps) + func(step); + }; + + forEachStep(staticRegistry); + forEachStep(registry); } template From 50e9bdd4bf4a5ebd9239e5f05b9a18a3b28d0084 Mon Sep 17 00:00:00 2001 From: "Timmer, Daan" Date: Fri, 24 Jul 2026 15:20:57 +0000 Subject: [PATCH 08/29] Suppress character conversion warnings for Clang in GoogleTest targets --- cmake/dependencies.cpm.cmake | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cmake/dependencies.cpm.cmake b/cmake/dependencies.cpm.cmake index fc6369fa..d5ae3de2 100644 --- a/cmake/dependencies.cpm.cmake +++ b/cmake/dependencies.cpm.cmake @@ -106,6 +106,8 @@ if(CCR_FETCH_DEPS) set_target_properties(gtest gtest_main gmock gmock_main PROPERTIES FOLDER External/GoogleTest ) + target_compile_options(gtest PRIVATE $<$:-Wno-character-conversion>) + target_compile_options(gmock PRIVATE $<$:-Wno-character-conversion>) endif() # --------------------------------------------------------------------------- From 6ecc6d77ab7426fc8350af87199bc98e2d8f7379 Mon Sep 17 00:00:00 2001 From: "Timmer, Daan" Date: Fri, 24 Jul 2026 15:22:56 +0000 Subject: [PATCH 09/29] Fix gtest discovery condition to exclude cross-compiling --- compatibility/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compatibility/CMakeLists.txt b/compatibility/CMakeLists.txt index 65fb627f..b8f8118e 100644 --- a/compatibility/CMakeLists.txt +++ b/compatibility/CMakeLists.txt @@ -71,6 +71,6 @@ target_compile_definitions(compatibility.test PRIVATE COMPAT_PLUGIN_DIR="$" ) -if(CCR_BUILD_TESTS) +if(CCR_BUILD_TESTS AND NOT CMAKE_CROSSCOMPILING) gtest_discover_tests(compatibility.test) endif() From 35407490b9f23a125c8b0d054868c10f0e439b20 Mon Sep 17 00:00:00 2001 From: "Timmer, Daan" Date: Fri, 24 Jul 2026 15:24:10 +0000 Subject: [PATCH 10/29] Change execution behavior to continue on failure in CMake presets --- CMakePresets.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakePresets.json b/CMakePresets.json index 4e9f5dca..1a847897 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -194,7 +194,7 @@ }, "execution": { "noTestsAction": "error", - "stopOnFailure": true + "stopOnFailure": false } }, { From 5c3cb0a7d6c3b7042fceb1c18132a46a75311189 Mon Sep 17 00:00:00 2001 From: "Timmer, Daan" Date: Fri, 24 Jul 2026 15:27:31 +0000 Subject: [PATCH 11/29] Update include directories in ccr_plugin_register and ccr_add_plugin to use SYSTEM scope --- cmake/ccr_add_plugin.cmake | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmake/ccr_add_plugin.cmake b/cmake/ccr_add_plugin.cmake index 9df38dfe..6474c3c3 100644 --- a/cmake/ccr_add_plugin.cmake +++ b/cmake/ccr_add_plugin.cmake @@ -22,7 +22,7 @@ if (NOT TARGET ccr_plugin_register) add_library(ccr_plugin_register OBJECT ${CMAKE_CURRENT_LIST_DIR}/../cucumber_cpp/library/plugin/PluginRegister.cpp ) - target_include_directories(ccr_plugin_register PRIVATE + target_include_directories(ccr_plugin_register SYSTEM PRIVATE $ $ ) @@ -37,7 +37,7 @@ function(ccr_add_plugin TARGET_NAME) # Plugins need the header include paths from cucumber_cpp and its # transitive dependencies (fmt, gtest, gherkin, etc.) but must NOT link # the actual libraries — symbols resolve from the host at runtime. - target_include_directories(${TARGET_NAME} PRIVATE + target_include_directories(${TARGET_NAME} SYSTEM PRIVATE $ $ ) From 4a4e2e2f95ea504134eea181558cf0863069409d Mon Sep 17 00:00:00 2001 From: "Timmer, Daan" Date: Fri, 24 Jul 2026 15:53:58 +0000 Subject: [PATCH 12/29] solve loading plugins on windows --- cucumber_cpp/library/plugin/DynamicLibraryManager.cpp | 3 ++- cucumber_cpp/library/plugin/PluginExport.hpp | 2 +- cucumber_cpp/library/plugin/PluginRegister.cpp | 11 +++++++++-- .../library/support/DefinitionRegistration.cpp | 9 +++++++++ .../library/support/DefinitionRegistration.hpp | 1 + 5 files changed, 22 insertions(+), 4 deletions(-) diff --git a/cucumber_cpp/library/plugin/DynamicLibraryManager.cpp b/cucumber_cpp/library/plugin/DynamicLibraryManager.cpp index a544be0e..fc244124 100644 --- a/cucumber_cpp/library/plugin/DynamicLibraryManager.cpp +++ b/cucumber_cpp/library/plugin/DynamicLibraryManager.cpp @@ -1,6 +1,7 @@ #include "cucumber_cpp/library/plugin/DynamicLibraryManager.hpp" #include "cucumber_cpp/library/plugin/DynamicLibrary.hpp" #include "cucumber_cpp/library/plugin/PluginExport.hpp" +#include "cucumber_cpp/library/support/DefinitionRegistration.hpp" #include #include #include @@ -45,7 +46,7 @@ namespace cucumber_cpp::library::plugin const auto& lib = libraries.emplace_back(path); auto registerFn = lib.GetSymbol("ccr_register"); - registerFn(); + registerFn(&support::DefinitionRegistration::Instance()); } void DynamicLibraryManager::LoadDirectory(const std::filesystem::path& directory) diff --git a/cucumber_cpp/library/plugin/PluginExport.hpp b/cucumber_cpp/library/plugin/PluginExport.hpp index 9d3ab028..d4af94e7 100644 --- a/cucumber_cpp/library/plugin/PluginExport.hpp +++ b/cucumber_cpp/library/plugin/PluginExport.hpp @@ -7,6 +7,6 @@ #define CCR_EXPORT __attribute__((visibility("default"))) #endif -using CcrRegisterFn = void (*)(); +using CcrRegisterFn = void (*)(void*); #endif diff --git a/cucumber_cpp/library/plugin/PluginRegister.cpp b/cucumber_cpp/library/plugin/PluginRegister.cpp index 5462bf4f..0c8ae6bd 100644 --- a/cucumber_cpp/library/plugin/PluginRegister.cpp +++ b/cucumber_cpp/library/plugin/PluginRegister.cpp @@ -1,4 +1,11 @@ #include "cucumber_cpp/library/plugin/PluginExport.hpp" +#include "cucumber_cpp/library/support/DefinitionRegistration.hpp" -extern "C" CCR_EXPORT void ccr_register() -{} +extern "C" CCR_EXPORT void ccr_register(void* hostRegistration) +{ + if (hostRegistration == nullptr) + return; + + auto& host = *static_cast(hostRegistration); + cucumber_cpp::library::support::DefinitionRegistration::Instance().MergeInto(host); +} diff --git a/cucumber_cpp/library/support/DefinitionRegistration.cpp b/cucumber_cpp/library/support/DefinitionRegistration.cpp index 6001b7b0..3fd29754 100644 --- a/cucumber_cpp/library/support/DefinitionRegistration.cpp +++ b/cucumber_cpp/library/support/DefinitionRegistration.cpp @@ -47,6 +47,15 @@ namespace cucumber_cpp::library::support staticParameters.merge(customParameters); } + void DefinitionRegistration::MergeInto(DefinitionRegistration& target) + { + if (this == &target) + return; + + target.registry.merge(registry); + target.customParameters.merge(customParameters); + } + void DefinitionRegistration::LoadIds(cucumber::gherkin::id_generator_ptr idGenerator) { const auto assignGenerator = [&idGenerator](auto& entry) diff --git a/cucumber_cpp/library/support/DefinitionRegistration.hpp b/cucumber_cpp/library/support/DefinitionRegistration.hpp index 7c617ccd..56cff238 100644 --- a/cucumber_cpp/library/support/DefinitionRegistration.hpp +++ b/cucumber_cpp/library/support/DefinitionRegistration.hpp @@ -31,6 +31,7 @@ namespace cucumber_cpp::library::support void Clear(); void TakeSnapshot(); + void MergeInto(DefinitionRegistration& target); void LoadIds(cucumber::gherkin::id_generator_ptr idGenerator); From a4d5c7f105677f87f185ec78b7e358199f6a7275 Mon Sep 17 00:00:00 2001 From: "Timmer, Daan" Date: Fri, 24 Jul 2026 17:33:35 +0000 Subject: [PATCH 13/29] resolve parameter registration for plugins for windows --- cucumber_cpp/library/Application.cpp | 1 - cucumber_cpp/library/BodyMacro.hpp | 30 ++++++++++++++++--- .../cucumber_expression/ParameterRegistry.hpp | 9 ++++-- .../library/runtime/NestedTestCaseRunner.cpp | 6 +++- .../library/runtime/TestCaseRunner.cpp | 6 +++- .../support/DefinitionRegistration.cpp | 21 +++++++++++++ .../support/DefinitionRegistration.hpp | 10 ++++++- cucumber_cpp/library/util/Body.hpp | 1 + .../util/TransformStepMatchArgumentsList.cpp | 13 ++++++-- .../util/TransformStepMatchArgumentsList.hpp | 7 ++++- 10 files changed, 90 insertions(+), 14 deletions(-) diff --git a/cucumber_cpp/library/Application.cpp b/cucumber_cpp/library/Application.cpp index 70a1bbaf..e58020bb 100644 --- a/cucumber_cpp/library/Application.cpp +++ b/cucumber_cpp/library/Application.cpp @@ -80,7 +80,6 @@ namespace cucumber_cpp::library { dynamicLibraryManager.UnloadAll(); support::DefinitionRegistration::Instance().Clear(); - cucumber_expression::ConverterTypeMapClearer::ClearAll(); } } diff --git a/cucumber_cpp/library/BodyMacro.hpp b/cucumber_cpp/library/BodyMacro.hpp index 4790fa55..1af0a889 100644 --- a/cucumber_cpp/library/BodyMacro.hpp +++ b/cucumber_cpp/library/BodyMacro.hpp @@ -5,8 +5,10 @@ #include "cucumber_cpp/library/cucumber_expression/ParameterRegistry.hpp" #include "cucumber_cpp/library/support/Body.hpp" #include "cucumber_cpp/library/util/Body.hpp" +#include "gtest/gtest.h" +#include #include -#include +#include #include #include #include @@ -14,9 +16,29 @@ namespace cucumber_cpp::library::detail { template - T TransformArg(const std::string& parameterName, const cucumber_expression::ConvertFunctionArg& parameterArgs) + struct IsOptional : std::false_type + {}; + + template + struct IsOptional> : std::true_type + {}; + + template + T TransformArg(const std::string& parameterName, const cucumber_expression::ConvertFunctionArg& parameterArgs, const cucumber_expression::ErasedConverter& converter) { - return cucumber_expression::TransformArg(T{}, parameterName, parameterArgs); + if constexpr (IsOptional::value) + { + using Inner = typename T::value_type; + if (converter) + return std::any_cast>(converter(parameterArgs)); + return cucumber_expression::TransformArg(T{}, parameterName, parameterArgs); + } + else + { + if (converter) + return std::any_cast>(converter(parameterArgs)).value(); + return cucumber_expression::TransformArg(T{}, parameterName, parameterArgs); + } } template @@ -44,7 +66,7 @@ namespace cucumber_cpp::library::detail template void ExecuteImpl(const util::ExecuteArgs& args, std::index_sequence /*unused*/) { - static_cast(this)->ExecuteImpl(TransformArg>(args[I].converterName, args[I].converterArgs)...); + static_cast(this)->ExecuteImpl(TransformArg>(args[I].converterName, args[I].converterArgs, args[I].converter)...); } }; } diff --git a/cucumber_cpp/library/cucumber_expression/ParameterRegistry.hpp b/cucumber_cpp/library/cucumber_expression/ParameterRegistry.hpp index 78693f6a..e2dcb3e9 100644 --- a/cucumber_cpp/library/cucumber_expression/ParameterRegistry.hpp +++ b/cucumber_cpp/library/cucumber_expression/ParameterRegistry.hpp @@ -3,6 +3,7 @@ #include "fmt/format.h" #include +#include #include #include #include @@ -11,7 +12,6 @@ #include #include #include -#include #include #include #include @@ -32,6 +32,9 @@ namespace cucumber_cpp::library::cucumber_expression bool useForSnippets; }; + using ConvertFunctionArg = std::vector>; + using ErasedConverter = std::function; + struct CustomParameterEntry { CustomParameterEntryParams params; @@ -40,6 +43,8 @@ namespace cucumber_cpp::library::cucumber_expression std::source_location location; + ErasedConverter converter; + std::strong_ordering operator<=>(const CustomParameterEntry& other) const; }; @@ -125,8 +130,6 @@ namespace cucumber_cpp::library::cucumber_expression std::source_location location; }; - using ConvertFunctionArg = std::vector>; - template using ConverterFunction = std::function; diff --git a/cucumber_cpp/library/runtime/NestedTestCaseRunner.cpp b/cucumber_cpp/library/runtime/NestedTestCaseRunner.cpp index 924ea075..08d09908 100644 --- a/cucumber_cpp/library/runtime/NestedTestCaseRunner.cpp +++ b/cucumber_cpp/library/runtime/NestedTestCaseRunner.cpp @@ -8,6 +8,7 @@ #include "cucumber_cpp/library/Context.hpp" #include "cucumber_cpp/library/cucumber_expression/Argument.hpp" #include "cucumber_cpp/library/cucumber_expression/Matcher.hpp" +#include "cucumber_cpp/library/support/DefinitionRegistration.hpp" #include "cucumber_cpp/library/support/StepRegistry.hpp" #include "cucumber_cpp/library/support/SupportCodeLibrary.hpp" #include "cucumber_cpp/library/util/ArgumentGroupToMessageGroup.hpp" @@ -72,7 +73,10 @@ namespace cucumber_cpp::library::runtime void Invoke(std::size_t nesting, const std::string& step, const util::BodyFactory& bodyFactory, const cucumber::messages::step_match_arguments_list& args) { - const auto status = util::ConstructAndExecute(bodyFactory, util::StepMatchArgumentsListToExecuteArgs(args)); + const auto status = util::ConstructAndExecute(bodyFactory, util::StepMatchArgumentsListToExecuteArgs(args, [](const std::string& name) + { + return support::DefinitionRegistration::Instance().GetConverter(name); + })); if (status.status != util::TestStepResultStatus::PASSED) throw util::NestedTestCaseRunnerError{ diff --git a/cucumber_cpp/library/runtime/TestCaseRunner.cpp b/cucumber_cpp/library/runtime/TestCaseRunner.cpp index f7756ac5..1bf04d1a 100644 --- a/cucumber_cpp/library/runtime/TestCaseRunner.cpp +++ b/cucumber_cpp/library/runtime/TestCaseRunner.cpp @@ -18,6 +18,7 @@ #include "cucumber/messages/test_step_started.hpp" #include "cucumber_cpp/library/Context.hpp" #include "cucumber_cpp/library/runtime/NestedTestCaseRunner.hpp" +#include "cucumber_cpp/library/support/DefinitionRegistration.hpp" #include "cucumber_cpp/library/support/HookRegistry.hpp" #include "cucumber_cpp/library/support/StepRegistry.hpp" #include "cucumber_cpp/library/support/SupportCodeLibrary.hpp" @@ -50,7 +51,10 @@ namespace cucumber_cpp::library::runtime { cucumber::messages::test_step_result InvokeStep(const util::BodyFactory& bodyFactory, const cucumber::messages::step_match_arguments_list& args) { - return util::TransformTestStepResult(util::ConstructAndExecute(bodyFactory, util::StepMatchArgumentsListToExecuteArgs(args))); + return util::TransformTestStepResult(util::ConstructAndExecute(bodyFactory, util::StepMatchArgumentsListToExecuteArgs(args, [](const std::string& name) + { + return support::DefinitionRegistration::Instance().GetConverter(name); + }))); } std::optional MakeScenarioInfo(const cucumber::messages::pickle& pickle) diff --git a/cucumber_cpp/library/support/DefinitionRegistration.cpp b/cucumber_cpp/library/support/DefinitionRegistration.cpp index 3fd29754..a05d8a16 100644 --- a/cucumber_cpp/library/support/DefinitionRegistration.cpp +++ b/cucumber_cpp/library/support/DefinitionRegistration.cpp @@ -7,6 +7,7 @@ #include "cucumber_cpp/library/util/HookData.hpp" #include "cucumber_cpp/library/util/HookFactory.hpp" #include "cucumber_cpp/library/util/StepFactory.hpp" +#include #include #include #include @@ -93,6 +94,26 @@ namespace cucumber_cpp::library::support return result; } + cucumber_expression::ErasedConverter DefinitionRegistration::GetConverter(const std::string& name) const + { + auto findIn = [&name](const std::set>& params) -> cucumber_expression::ErasedConverter + { + auto it = std::ranges::find_if(params, [&name](const auto& entry) + { + return entry.params.name == name; + }); + if (it != params.end()) + return it->converter; + + return nullptr; + }; + + if (auto converter = findIn(staticParameters)) + return converter; + + return findIn(customParameters); + } + std::size_t DefinitionRegistration::Register(Hook hook, util::HookType hookType, util::HookFactory factory, std::source_location sourceLocation) { registry.try_emplace(sourceLocation, HookEntry{ hookType, hook, factory, sourceLocation }); diff --git a/cucumber_cpp/library/support/DefinitionRegistration.hpp b/cucumber_cpp/library/support/DefinitionRegistration.hpp index 56cff238..12c1866a 100644 --- a/cucumber_cpp/library/support/DefinitionRegistration.hpp +++ b/cucumber_cpp/library/support/DefinitionRegistration.hpp @@ -9,6 +9,7 @@ #include "cucumber_cpp/library/util/HookData.hpp" #include "cucumber_cpp/library/util/HookFactory.hpp" #include "cucumber_cpp/library/util/StepFactory.hpp" +#include #include #include #include @@ -42,6 +43,8 @@ namespace cucumber_cpp::library::support [[nodiscard]] std::set> GetRegisteredParameters() const; + [[nodiscard]] cucumber_expression::ErasedConverter GetConverter(const std::string& name) const; + template static std::size_t Register(Hook hook, util::HookType hookType, std::source_location sourceLocation = std::source_location::current()); @@ -116,7 +119,12 @@ namespace cucumber_cpp::library::support std::size_t DefinitionRegistration::Register(cucumber_expression::CustomParameterEntryParams params, std::source_location location) { auto& instance = Instance(); - instance.customParameters.emplace(params, instance.customParameters.size() + 1, location); + + cucumber_expression::ErasedConverter converter = [](const cucumber_expression::ConvertFunctionArg& args) -> std::any + { + return std::any(Transformer::Transform(args)); + }; + instance.customParameters.emplace(params, instance.customParameters.size() + 1, location, std::move(converter)); cucumber_expression::ConverterTypeMap::Instance()[params.name] = Transformer::Transform; diff --git a/cucumber_cpp/library/util/Body.hpp b/cucumber_cpp/library/util/Body.hpp index 130ba47a..4c2d4587 100644 --- a/cucumber_cpp/library/util/Body.hpp +++ b/cucumber_cpp/library/util/Body.hpp @@ -48,6 +48,7 @@ namespace cucumber_cpp::library::util { std::string converterName; cucumber_expression::ConvertFunctionArg converterArgs; + cucumber_expression::ErasedConverter converter; }; using ExecuteArgs = std::vector; diff --git a/cucumber_cpp/library/util/TransformStepMatchArgumentsList.cpp b/cucumber_cpp/library/util/TransformStepMatchArgumentsList.cpp index e7f26efb..3d043bde 100644 --- a/cucumber_cpp/library/util/TransformStepMatchArgumentsList.cpp +++ b/cucumber_cpp/library/util/TransformStepMatchArgumentsList.cpp @@ -4,12 +4,21 @@ #include "cucumber_cpp/library/util/Body.hpp" #include "cucumber_cpp/library/util/TransformArgument.hpp" #include +#include namespace cucumber_cpp::library::util { - ExecuteArgs StepMatchArgumentsListToExecuteArgs(const cucumber::messages::step_match_arguments_list& args) + ExecuteArgs StepMatchArgumentsListToExecuteArgs(const cucumber::messages::step_match_arguments_list& args, const ConverterResolver& resolveConverter) { - auto strings = args.step_match_arguments | std::views::transform(util::ToArgument); + auto transform = [&resolveConverter](const auto& arg) + { + auto result = ToArgument(arg); + if (resolveConverter && !result.converterName.empty()) + result.converter = resolveConverter(result.converterName); + return result; + }; + + auto strings = args.step_match_arguments | std::views::transform(transform); return { strings.begin(), strings.end() }; } } diff --git a/cucumber_cpp/library/util/TransformStepMatchArgumentsList.hpp b/cucumber_cpp/library/util/TransformStepMatchArgumentsList.hpp index 171df3d9..5a0eae53 100644 --- a/cucumber_cpp/library/util/TransformStepMatchArgumentsList.hpp +++ b/cucumber_cpp/library/util/TransformStepMatchArgumentsList.hpp @@ -2,11 +2,16 @@ #define UTIL_TRANSFORM_STEP_MATCH_ARGUMENTS_LIST_HPP #include "cucumber/messages/step_match_arguments_list.hpp" +#include "cucumber_cpp/library/cucumber_expression/ParameterRegistry.hpp" #include "cucumber_cpp/library/util/Body.hpp" +#include +#include namespace cucumber_cpp::library::util { - ExecuteArgs StepMatchArgumentsListToExecuteArgs(const cucumber::messages::step_match_arguments_list& args); + using ConverterResolver = std::function; + + ExecuteArgs StepMatchArgumentsListToExecuteArgs(const cucumber::messages::step_match_arguments_list& args, const ConverterResolver& resolveConverter = nullptr); } #endif From 7391e56035fd341f18174fe00956b30cb31b8208 Mon Sep 17 00:00:00 2001 From: "Timmer, Daan" Date: Fri, 24 Jul 2026 18:01:20 +0000 Subject: [PATCH 14/29] chain plugin registration for windows instead --- cucumber_cpp/CucumberCpp.hpp | 1 - cucumber_cpp/library/Application.cpp | 3 +- cucumber_cpp/library/BodyMacro.hpp | 28 +-- .../cucumber_expression/ParameterRegistry.hpp | 8 +- .../library/plugin/PLUGIN_ARCHITECTURE.md | 188 +++++++++++------- .../library/plugin/PluginRegister.cpp | 2 +- .../library/runtime/NestedTestCaseRunner.cpp | 6 +- .../library/runtime/TestCaseRunner.cpp | 6 +- .../support/DefinitionRegistration.cpp | 58 ++---- .../support/DefinitionRegistration.hpp | 24 +-- cucumber_cpp/library/util/Body.hpp | 1 - .../util/TransformStepMatchArgumentsList.cpp | 14 +- .../util/TransformStepMatchArgumentsList.hpp | 7 +- 13 files changed, 155 insertions(+), 191 deletions(-) diff --git a/cucumber_cpp/CucumberCpp.hpp b/cucumber_cpp/CucumberCpp.hpp index d13bca62..f7a56f7a 100644 --- a/cucumber_cpp/CucumberCpp.hpp +++ b/cucumber_cpp/CucumberCpp.hpp @@ -8,7 +8,6 @@ #include "cucumber_cpp/library/Steps.hpp" #include "cucumber_cpp/library/cucumber_expression/MatchRange.hpp" #include "cucumber_cpp/library/cucumber_expression/ParameterRegistry.hpp" -#include "cucumber_cpp/library/plugin/PluginExport.hpp" namespace cucumber_cpp { diff --git a/cucumber_cpp/library/Application.cpp b/cucumber_cpp/library/Application.cpp index e58020bb..870971b8 100644 --- a/cucumber_cpp/library/Application.cpp +++ b/cucumber_cpp/library/Application.cpp @@ -71,15 +71,14 @@ namespace cucumber_cpp::library , removeDefaultGoogleTestListener{ removeDefaultGoogleTestListener } { cli.set_config("--config", "cucumber.toml"); - support::DefinitionRegistration::Instance().TakeSnapshot(); } Application::~Application() { if (!dynamicLibraryManager.GetLoadedLibraries().empty()) { + support::DefinitionRegistration::Instance().UnregisterPlugins(); dynamicLibraryManager.UnloadAll(); - support::DefinitionRegistration::Instance().Clear(); } } diff --git a/cucumber_cpp/library/BodyMacro.hpp b/cucumber_cpp/library/BodyMacro.hpp index 1af0a889..36b1e2f3 100644 --- a/cucumber_cpp/library/BodyMacro.hpp +++ b/cucumber_cpp/library/BodyMacro.hpp @@ -6,9 +6,7 @@ #include "cucumber_cpp/library/support/Body.hpp" #include "cucumber_cpp/library/util/Body.hpp" #include "gtest/gtest.h" -#include #include -#include #include #include #include @@ -16,29 +14,9 @@ namespace cucumber_cpp::library::detail { template - struct IsOptional : std::false_type - {}; - - template - struct IsOptional> : std::true_type - {}; - - template - T TransformArg(const std::string& parameterName, const cucumber_expression::ConvertFunctionArg& parameterArgs, const cucumber_expression::ErasedConverter& converter) + T TransformArg(const std::string& parameterName, const cucumber_expression::ConvertFunctionArg& parameterArgs) { - if constexpr (IsOptional::value) - { - using Inner = typename T::value_type; - if (converter) - return std::any_cast>(converter(parameterArgs)); - return cucumber_expression::TransformArg(T{}, parameterName, parameterArgs); - } - else - { - if (converter) - return std::any_cast>(converter(parameterArgs)).value(); - return cucumber_expression::TransformArg(T{}, parameterName, parameterArgs); - } + return cucumber_expression::TransformArg(T{}, parameterName, parameterArgs); } template @@ -66,7 +44,7 @@ namespace cucumber_cpp::library::detail template void ExecuteImpl(const util::ExecuteArgs& args, std::index_sequence /*unused*/) { - static_cast(this)->ExecuteImpl(TransformArg>(args[I].converterName, args[I].converterArgs, args[I].converter)...); + static_cast(this)->ExecuteImpl(TransformArg>(args[I].converterName, args[I].converterArgs)...); } }; } diff --git a/cucumber_cpp/library/cucumber_expression/ParameterRegistry.hpp b/cucumber_cpp/library/cucumber_expression/ParameterRegistry.hpp index e2dcb3e9..69b59824 100644 --- a/cucumber_cpp/library/cucumber_expression/ParameterRegistry.hpp +++ b/cucumber_cpp/library/cucumber_expression/ParameterRegistry.hpp @@ -3,7 +3,6 @@ #include "fmt/format.h" #include -#include #include #include #include @@ -32,9 +31,6 @@ namespace cucumber_cpp::library::cucumber_expression bool useForSnippets; }; - using ConvertFunctionArg = std::vector>; - using ErasedConverter = std::function; - struct CustomParameterEntry { CustomParameterEntryParams params; @@ -43,8 +39,6 @@ namespace cucumber_cpp::library::cucumber_expression std::source_location location; - ErasedConverter converter; - std::strong_ordering operator<=>(const CustomParameterEntry& other) const; }; @@ -130,6 +124,8 @@ namespace cucumber_cpp::library::cucumber_expression std::source_location location; }; + using ConvertFunctionArg = std::vector>; + template using ConverterFunction = std::function; diff --git a/cucumber_cpp/library/plugin/PLUGIN_ARCHITECTURE.md b/cucumber_cpp/library/plugin/PLUGIN_ARCHITECTURE.md index 50557a35..c09400de 100644 --- a/cucumber_cpp/library/plugin/PLUGIN_ARCHITECTURE.md +++ b/cucumber_cpp/library/plugin/PLUGIN_ARCHITECTURE.md @@ -17,8 +17,9 @@ Key design principles: - **Transparent authoring** — plugin source files use the same `STEP`, `HOOK_*`, and `PARAMETER` macros as statically-linked code. -- **Static registration preserved** — destroying an `Application` only removes - dynamically-loaded definitions; statically-linked registrations survive. +- **Registrations stay in-place** — each plugin keeps its own + `DefinitionRegistration` instance; the host holds references and aggregates + lookups across all plugins. - **Cross-platform** — works on Linux (`.so`), macOS (`.dylib`), and Windows (`.dll`). - **Validation** — each plugin must export a `ccr_register` symbol (provided automatically by `ccr_add_plugin`) as an entry-point validation gate. @@ -59,14 +60,13 @@ classDiagram } class DefinitionRegistration { - -map staticRegistry - -set staticParameters -map registry -set customParameters + -vector~DefinitionRegistration*~ plugins +Instance()$ DefinitionRegistration& +Register~Body~() size_t - +TakeSnapshot() - +Clear() + +RegisterPlugin(plugin) + +UnregisterPlugins() +ForEachRegisteredStep() +GetHooks() +GetRegisteredParameters() @@ -88,22 +88,18 @@ classDiagram +ccr_register() } - class ConverterTypeMapClearer { - +ClearAll()$ - } - Application *-- DynamicLibraryManager DynamicLibraryManager *-- "0..*" DynamicLibrary DynamicLibrary ..> PluginRegister : resolves ccr_register - Application ..> DefinitionRegistration : snapshot + restore - Application ..> ConverterTypeMapClearer : clears on destroy + Application ..> DefinitionRegistration : unregister on destroy + DefinitionRegistration o-- "0..*" DefinitionRegistration : plugins ParameterLoader ..> DefinitionRegistration : reads parameters StepLoader ..> DefinitionRegistration : reads steps HookLoader ..> DefinitionRegistration : reads hooks ``` **`Application`** owns the `DynamicLibraryManager` and orchestrates the full -lifecycle: CLI parsing → snapshot → plugin loading → execution → cleanup. +lifecycle: CLI parsing → plugin loading → execution → cleanup. **`DynamicLibraryManager`** manages plugin lifetime. It holds a vector of `DynamicLibrary` RAII wrappers. `Load()` accepts file paths or directories; @@ -114,11 +110,11 @@ sorted order. Construction opens the library; destruction closes it. `GetSymbol(name)` resolves a typed function pointer. -**`DefinitionRegistration`** is the global singleton holding all step, hook, and -parameter registrations. Entries are keyed by `std::source_location`. The -dual-container design (`TakeSnapshot` / `Clear`) separates static and -dynamic entries: `TakeSnapshot` moves current entries to dedicated static -containers, and `Clear` only removes the dynamic ones. +**`DefinitionRegistration`** is a singleton holding step, hook, and parameter +registrations. Entries are keyed by `std::source_location`. Each plugin has its +own instance; the host's instance holds a `plugins` vector of pointers to +plugin instances. Query methods iterate the host's own entries first, then each +plugin's entries. --- @@ -136,17 +132,16 @@ flowchart TB subgraph load["Load Phase — Application::Run"] cli["Parse --load paths"] - snap["TakeSnapshot()\nMove entries to static containers"] mgr["DynamicLibraryManager::Load"] dir{"Directory\nor File?"} scan["Scan & sort by\nplatform extension"] open["dlopen / LoadLibrary\nRTLD_NOW | RTLD_GLOBAL"] sym["dlsym ccr_register\nValidation gate"] - invoke["Invoke ccr_register"] + invoke["Invoke ccr_register\nRegisterPlugin(local)"] pluginInit["Plugin static constructors\nalready ran on dlopen"] - singleton["DefinitionRegistration\nsingleton populated"] + singleton["Plugin DefinitionRegistration\ninstance populated"] - cli --> snap --> mgr --> dir + cli --> mgr --> dir dir -->|directory| scan --> open dir -->|file| open open --> sym --> invoke @@ -163,11 +158,10 @@ flowchart TB end subgraph cleanup["Cleanup Phase — ~Application"] + unreg["DefinitionRegistration::UnregisterPlugins\nClear plugin pointer list"] unload["DynamicLibraryManager::UnloadAll\nlibraries.clear → dlclose"] - restore["DefinitionRegistration::Clear()\nRemove only dynamic entries"] - clearTypes["ConverterTypeMapClearer::ClearAll\nMaps repopulated next run"] - unload --> restore --> clearTypes + unreg --> unload end build --> load --> run --> cleanup @@ -179,19 +173,21 @@ The data flow has four phases: `ccr_add_plugin` CMake function automatically links the `ccr_plugin_register` object library which provides the `ccr_register` entry point. -2. **Load** — `Application::Run` parses `--load` paths, takes a registry - snapshot (capturing the static baseline), then delegates to - `DynamicLibraryManager`. Each library is opened with `RTLD_NOW | RTLD_GLOBAL`, +2. **Load** — `Application::Run` parses `--load` paths and delegates to + `DynamicLibraryManager`. Each library is opened (`dlopen` with + `RTLD_NOW | RTLD_GLOBAL` on Linux/macOS, `LoadLibrary` on Windows), which triggers the plugin's static constructors — these register steps/hooks/ - parameters into the global `DefinitionRegistration` singleton. + parameters into the plugin's `DefinitionRegistration` instance. Then + `ccr_register` is called: on Linux/macOS this is a no-op (shared singleton), + on Windows it registers the plugin's separate instance with the host. 3. **Execution** — `RunCucumber` reads from `DefinitionRegistration` in three ordered phases: parameters first (so step expressions can reference custom - types), then steps, then hooks. + types), then steps, then hooks. Each query iterates the host's entries plus + all registered plugin entries. -4. **Cleanup** — the `Application` destructor unloads plugins (`dlclose`), then - calls `Clear()` which removes only the dynamic entries (those registered - after `TakeSnapshot`), preserving statically-linked definitions. +4. **Cleanup** — the `Application` destructor unregisters all plugin pointers, + then unloads the DLLs (`dlclose`). The host's own registrations remain intact. --- @@ -206,7 +202,8 @@ sequenceDiagram participant DL as DynamicLibrary participant OS as OS (dlopen) participant Static as Plugin Static Constructors - participant DR as DefinitionRegistration + participant PDR as Plugin DefinitionRegistration + participant HDR as Host DefinitionRegistration participant RC as RunCucumber participant PL as ParameterLoader participant SL as StepLoader @@ -217,18 +214,17 @@ sequenceDiagram CLI -->> App: options.loadPaths = ["plugin.so"] rect rgb(220, 245, 220) - Note over App, DR: Snapshot & Plugin Loading - App ->> DR: TakeSnapshot() - Note right of DR: Move current entries
to static containers + Note over App, HDR: Plugin Loading App ->> DLM: Load(["plugin.so"]) DLM ->> DL: DynamicLibrary("plugin.so") - DL ->> OS: dlopen("plugin.so", RTLD_NOW | RTLD_GLOBAL) + DL ->> OS: dlopen / LoadLibrary OS -->> Static: Static constructors execute - Static ->> DR: Register("pattern", ...) - Static ->> DR: Register(hookType, ...) + Static ->> PDR: Register("pattern", ...) + Static ->> PDR: Register(hookType, ...) OS -->> DL: handle DL ->> DL: GetSymbol("ccr_register") - DL -->> DLM: ccr_register validated & called + DL ->> HDR: ccr_register → RegisterPlugin(PDR) + Note right of HDR: Linux/macOS: &PDR == &HDR → no-op
Windows: stores pointer to plugin instance end rect rgb(220, 230, 250) @@ -236,71 +232,123 @@ sequenceDiagram App ->> RC: RunCucumber(options, paramReg, ...) RC ->> PL: Load(defReg, paramReg) - PL ->> DR: GetRegisteredParameters() - DR -->> PL: custom parameter entries + PL ->> HDR: GetRegisteredParameters() + Note right of HDR: Iterates host + plugins + HDR -->> PL: custom parameter entries PL -->> RC: Parameters registered RC ->> SL: Load(stepRegistry) - SL ->> DR: ForEachRegisteredStep() - DR -->> SL: step entries with matchers + SL ->> HDR: ForEachRegisteredStep() + Note right of HDR: Iterates host + plugins + HDR -->> SL: step entries with matchers SL -->> RC: Steps registered RC ->> HL: Load(hookRegistry) - HL ->> DR: GetHooks() - DR -->> HL: hook entries + HL ->> HDR: GetHooks() + Note right of HDR: Iterates host + plugins + HDR -->> HL: hook entries HL -->> RC: Hooks registered end rect rgb(250, 230, 230) - Note over App, DR: Cleanup + Note over App, HDR: Cleanup + App ->> HDR: UnregisterPlugins() + Note right of HDR: Clear plugin pointer list App ->> DLM: UnloadAll() DLM ->> DL: ~DynamicLibrary() DL ->> OS: dlclose(handle) - App ->> DR: Clear() - Note right of DR: Remove only dynamic entries
Static containers preserved - App ->> App: ConverterTypeMapClearer::ClearAll() end ``` --- -## Snapshot & Cleanup +## Platform Differences + +The plugin-list model adapts to each platform's shared library semantics: + +```mermaid +flowchart LR + subgraph linux["Linux / macOS"] + direction TB + lhost["Host Process"] + ldr["DefinitionRegistration\n(single shared instance)"] + lplugin["Plugin .so / .dylib"] + lstatic["Static constructors\nregister directly into\nshared instance"] + lccr["ccr_register:\nRegisterPlugin(self)\n→ self-guard skips"] + + lhost --> ldr + lplugin -.->|"RTLD_GLOBAL\nsymbols from host"| ldr + lplugin --> lstatic --> ldr + lplugin --> lccr + end + + subgraph windows["Windows"] + direction TB + whost["Host Process"] + whdr["Host DefinitionRegistration\nplugins: [&A, &B]"] + wpluginA["Plugin A .dll"] + wpdrA["Plugin A\nDefinitionRegistration"] + wpluginB["Plugin B .dll"] + wpdrB["Plugin B\nDefinitionRegistration"] + wccr["ccr_register:\nRegisterPlugin(local)\n→ host stores pointer"] + + whost --> whdr + wpluginA --> wpdrA + wpluginB --> wpdrB + whdr -.->|"iterates"| wpdrA + whdr -.->|"iterates"| wpdrB + wpluginA --> wccr + wpluginB --> wccr + end +``` + +| Aspect | Linux / macOS | Windows | +|--------|---------------|---------| +| Symbol resolution | `RTLD_GLOBAL` — plugin uses host symbols | Static link — plugin has own copy of `cucumber_cpp` | +| `DefinitionRegistration::Instance()` | Same address in host and plugin | Different instance per DLL | +| Static constructors | Register into the shared (host) instance | Register into plugin-local instance | +| `ccr_register` effect | No-op (self-registration guard) | Stores plugin instance pointer in host | +| `ConverterTypeMap` | Single shared instance | Separate per DLL (step executes in its own DLL context) | +| Plugin list (`plugins`) | Empty — all entries already in host | One entry per loaded plugin | +| CMake linking | No link (symbols from host via `-rdynamic`) | `target_link_libraries(plugin PRIVATE cucumber_cpp)` | +| macOS specifics | `-undefined dynamic_lookup` suppresses linker errors | — | + +--- + +## Plugin Registration Model -The `Application` destructor uses a snapshot-based approach so that statically- -linked step/hook/parameter definitions are never lost — only plugin-injected -entries are removed. +Each plugin has its own `DefinitionRegistration` singleton. The host's instance +maintains a list of plugin instance pointers. ```mermaid flowchart TD dest["~Application()"] check{"dynamicLibraryManager\n.GetLoadedLibraries()\nempty?"} skip["No cleanup needed"] + unreg["UnregisterPlugins()\nClear plugin pointer list"] unload["UnloadAll()\nlibraries.clear() → dlclose"] - restore["Clear()\nRemove dynamic registry\nand customParameters"] - clearConv["ConverterTypeMapClearer::ClearAll()\nType maps repopulated next run"] - done["Static registrations intact\nReady for next Application"] + done["Host registrations intact\nPlugin DLLs unloaded"] dest --> check check -->|yes| skip - check -->|no| unload --> restore --> clearConv --> done + check -->|no| unreg --> unload --> done ``` **How it works:** -1. Before plugins are loaded, `TakeSnapshot()` moves the current `registry` - and `customParameters` into separate `staticRegistry` and `staticParameters` - containers. This represents the "static baseline" — everything registered - by code compiled directly into the executable. +1. Plugins are loaded and their static constructors register steps/hooks/parameters + into the plugin's own `DefinitionRegistration::Instance()`. -2. Plugins load and their static constructors add new entries to the (now - empty) `registry` and `customParameters` containers. +2. `ccr_register` is called with a pointer to the host's `DefinitionRegistration`. + It calls `host.RegisterPlugin(local)`. A self-registration guard (`&plugin == this`) + skips registration on Linux/macOS where RTLD_GLOBAL makes both point to the + same singleton. -3. On destruction, `Clear()` clears only the dynamic `registry` and - `customParameters`. The static containers are untouched. +3. Query methods (`ForEachRegisteredStep`, `GetHooks`, `GetRegisteredParameters`, + `LoadIds`) iterate the host's own entries, then each plugin's entries. -4. `ConverterTypeMapClearer::ClearAll()` clears all converter type maps. This - is safe because these maps are repopulated from `DefinitionRegistration` - contents at the start of each `RunCucumber` call (Phase 1: ParameterLoader). +4. On destruction, `UnregisterPlugins()` clears the pointer list before `UnloadAll()` + closes the shared libraries (preventing dangling pointers). **Consequence**: Multiple sequential `Application` instances in the same process will correctly share statically-linked definitions while each getting a fresh diff --git a/cucumber_cpp/library/plugin/PluginRegister.cpp b/cucumber_cpp/library/plugin/PluginRegister.cpp index 0c8ae6bd..38881d3e 100644 --- a/cucumber_cpp/library/plugin/PluginRegister.cpp +++ b/cucumber_cpp/library/plugin/PluginRegister.cpp @@ -7,5 +7,5 @@ extern "C" CCR_EXPORT void ccr_register(void* hostRegistration) return; auto& host = *static_cast(hostRegistration); - cucumber_cpp::library::support::DefinitionRegistration::Instance().MergeInto(host); + host.RegisterPlugin(cucumber_cpp::library::support::DefinitionRegistration::Instance()); } diff --git a/cucumber_cpp/library/runtime/NestedTestCaseRunner.cpp b/cucumber_cpp/library/runtime/NestedTestCaseRunner.cpp index 08d09908..924ea075 100644 --- a/cucumber_cpp/library/runtime/NestedTestCaseRunner.cpp +++ b/cucumber_cpp/library/runtime/NestedTestCaseRunner.cpp @@ -8,7 +8,6 @@ #include "cucumber_cpp/library/Context.hpp" #include "cucumber_cpp/library/cucumber_expression/Argument.hpp" #include "cucumber_cpp/library/cucumber_expression/Matcher.hpp" -#include "cucumber_cpp/library/support/DefinitionRegistration.hpp" #include "cucumber_cpp/library/support/StepRegistry.hpp" #include "cucumber_cpp/library/support/SupportCodeLibrary.hpp" #include "cucumber_cpp/library/util/ArgumentGroupToMessageGroup.hpp" @@ -73,10 +72,7 @@ namespace cucumber_cpp::library::runtime void Invoke(std::size_t nesting, const std::string& step, const util::BodyFactory& bodyFactory, const cucumber::messages::step_match_arguments_list& args) { - const auto status = util::ConstructAndExecute(bodyFactory, util::StepMatchArgumentsListToExecuteArgs(args, [](const std::string& name) - { - return support::DefinitionRegistration::Instance().GetConverter(name); - })); + const auto status = util::ConstructAndExecute(bodyFactory, util::StepMatchArgumentsListToExecuteArgs(args)); if (status.status != util::TestStepResultStatus::PASSED) throw util::NestedTestCaseRunnerError{ diff --git a/cucumber_cpp/library/runtime/TestCaseRunner.cpp b/cucumber_cpp/library/runtime/TestCaseRunner.cpp index 1bf04d1a..f7756ac5 100644 --- a/cucumber_cpp/library/runtime/TestCaseRunner.cpp +++ b/cucumber_cpp/library/runtime/TestCaseRunner.cpp @@ -18,7 +18,6 @@ #include "cucumber/messages/test_step_started.hpp" #include "cucumber_cpp/library/Context.hpp" #include "cucumber_cpp/library/runtime/NestedTestCaseRunner.hpp" -#include "cucumber_cpp/library/support/DefinitionRegistration.hpp" #include "cucumber_cpp/library/support/HookRegistry.hpp" #include "cucumber_cpp/library/support/StepRegistry.hpp" #include "cucumber_cpp/library/support/SupportCodeLibrary.hpp" @@ -51,10 +50,7 @@ namespace cucumber_cpp::library::runtime { cucumber::messages::test_step_result InvokeStep(const util::BodyFactory& bodyFactory, const cucumber::messages::step_match_arguments_list& args) { - return util::TransformTestStepResult(util::ConstructAndExecute(bodyFactory, util::StepMatchArgumentsListToExecuteArgs(args, [](const std::string& name) - { - return support::DefinitionRegistration::Instance().GetConverter(name); - }))); + return util::TransformTestStepResult(util::ConstructAndExecute(bodyFactory, util::StepMatchArgumentsListToExecuteArgs(args))); } std::optional MakeScenarioInfo(const cucumber::messages::pickle& pickle) diff --git a/cucumber_cpp/library/support/DefinitionRegistration.cpp b/cucumber_cpp/library/support/DefinitionRegistration.cpp index a05d8a16..2fd0b40b 100644 --- a/cucumber_cpp/library/support/DefinitionRegistration.cpp +++ b/cucumber_cpp/library/support/DefinitionRegistration.cpp @@ -33,28 +33,17 @@ namespace cucumber_cpp::library::support return instance; } - void DefinitionRegistration::Clear() + void DefinitionRegistration::RegisterPlugin(DefinitionRegistration& plugin) { - registry.clear(); - customParameters.clear(); - } - - void DefinitionRegistration::TakeSnapshot() - { - if (!staticRegistry.empty() || !staticParameters.empty()) + if (&plugin == this) return; - staticRegistry.merge(registry); - staticParameters.merge(customParameters); + plugins.push_back(&plugin); } - void DefinitionRegistration::MergeInto(DefinitionRegistration& target) + void DefinitionRegistration::UnregisterPlugins() { - if (this == &target) - return; - - target.registry.merge(registry); - target.customParameters.merge(customParameters); + plugins.clear(); } void DefinitionRegistration::LoadIds(cucumber::gherkin::id_generator_ptr idGenerator) @@ -64,11 +53,12 @@ namespace cucumber_cpp::library::support entry.id = idGenerator->next_id(); }; - for (auto& [key, item] : staticRegistry) - std::visit(assignGenerator, item); - for (auto& [key, item] : registry) std::visit(assignGenerator, item); + + for (auto* plugin : plugins) + for (auto& [key, item] : plugin->registry) + std::visit(assignGenerator, item); } std::vector DefinitionRegistration::GetHooks() @@ -82,36 +72,22 @@ namespace cucumber_cpp::library::support result.push_back(std::get(entry)); }; - collectHooks(staticRegistry); collectHooks(registry); - return result; - } - std::set> DefinitionRegistration::GetRegisteredParameters() const - { - auto result = staticParameters; - result.insert(customParameters.begin(), customParameters.end()); + for (auto* plugin : plugins) + collectHooks(plugin->registry); + return result; } - cucumber_expression::ErasedConverter DefinitionRegistration::GetConverter(const std::string& name) const + std::set> DefinitionRegistration::GetRegisteredParameters() const { - auto findIn = [&name](const std::set>& params) -> cucumber_expression::ErasedConverter - { - auto it = std::ranges::find_if(params, [&name](const auto& entry) - { - return entry.params.name == name; - }); - if (it != params.end()) - return it->converter; - - return nullptr; - }; + auto result = customParameters; - if (auto converter = findIn(staticParameters)) - return converter; + for (const auto* plugin : plugins) + result.insert(plugin->customParameters.begin(), plugin->customParameters.end()); - return findIn(customParameters); + return result; } std::size_t DefinitionRegistration::Register(Hook hook, util::HookType hookType, util::HookFactory factory, std::source_location sourceLocation) diff --git a/cucumber_cpp/library/support/DefinitionRegistration.hpp b/cucumber_cpp/library/support/DefinitionRegistration.hpp index 12c1866a..0674d119 100644 --- a/cucumber_cpp/library/support/DefinitionRegistration.hpp +++ b/cucumber_cpp/library/support/DefinitionRegistration.hpp @@ -9,7 +9,6 @@ #include "cucumber_cpp/library/util/HookData.hpp" #include "cucumber_cpp/library/util/HookFactory.hpp" #include "cucumber_cpp/library/util/StepFactory.hpp" -#include #include #include #include @@ -30,9 +29,8 @@ namespace cucumber_cpp::library::support public: static DefinitionRegistration& Instance(); - void Clear(); - void TakeSnapshot(); - void MergeInto(DefinitionRegistration& target); + void RegisterPlugin(DefinitionRegistration& plugin); + void UnregisterPlugins(); void LoadIds(cucumber::gherkin::id_generator_ptr idGenerator); @@ -43,8 +41,6 @@ namespace cucumber_cpp::library::support [[nodiscard]] std::set> GetRegisteredParameters() const; - [[nodiscard]] cucumber_expression::ErasedConverter GetConverter(const std::string& name) const; - template static std::size_t Register(Hook hook, util::HookType hookType, std::source_location sourceLocation = std::source_location::current()); @@ -62,11 +58,10 @@ namespace cucumber_cpp::library::support std::size_t Register(GlobalHook hook, util::HookType hookType, util::HookFactory factory, std::source_location sourceLocation); std::size_t Register(std::string_view matcher, StepType stepType, util::StepFactory factory, std::source_location sourceLocation); - std::map staticRegistry; - std::set> staticParameters; - std::map registry; std::set> customParameters; + + std::vector plugins; }; ////////////////////////// @@ -93,8 +88,10 @@ namespace cucumber_cpp::library::support func(step); }; - forEachStep(staticRegistry); forEachStep(registry); + + for (auto* plugin : plugins) + forEachStep(plugin->registry); } template @@ -119,12 +116,7 @@ namespace cucumber_cpp::library::support std::size_t DefinitionRegistration::Register(cucumber_expression::CustomParameterEntryParams params, std::source_location location) { auto& instance = Instance(); - - cucumber_expression::ErasedConverter converter = [](const cucumber_expression::ConvertFunctionArg& args) -> std::any - { - return std::any(Transformer::Transform(args)); - }; - instance.customParameters.emplace(params, instance.customParameters.size() + 1, location, std::move(converter)); + instance.customParameters.emplace(params, instance.customParameters.size() + 1, location); cucumber_expression::ConverterTypeMap::Instance()[params.name] = Transformer::Transform; diff --git a/cucumber_cpp/library/util/Body.hpp b/cucumber_cpp/library/util/Body.hpp index 4c2d4587..130ba47a 100644 --- a/cucumber_cpp/library/util/Body.hpp +++ b/cucumber_cpp/library/util/Body.hpp @@ -48,7 +48,6 @@ namespace cucumber_cpp::library::util { std::string converterName; cucumber_expression::ConvertFunctionArg converterArgs; - cucumber_expression::ErasedConverter converter; }; using ExecuteArgs = std::vector; diff --git a/cucumber_cpp/library/util/TransformStepMatchArgumentsList.cpp b/cucumber_cpp/library/util/TransformStepMatchArgumentsList.cpp index 3d043bde..a208be8b 100644 --- a/cucumber_cpp/library/util/TransformStepMatchArgumentsList.cpp +++ b/cucumber_cpp/library/util/TransformStepMatchArgumentsList.cpp @@ -1,24 +1,14 @@ - #include "cucumber_cpp/library/util/TransformStepMatchArgumentsList.hpp" #include "cucumber/messages/step_match_arguments_list.hpp" #include "cucumber_cpp/library/util/Body.hpp" #include "cucumber_cpp/library/util/TransformArgument.hpp" #include -#include namespace cucumber_cpp::library::util { - ExecuteArgs StepMatchArgumentsListToExecuteArgs(const cucumber::messages::step_match_arguments_list& args, const ConverterResolver& resolveConverter) + ExecuteArgs StepMatchArgumentsListToExecuteArgs(const cucumber::messages::step_match_arguments_list& args) { - auto transform = [&resolveConverter](const auto& arg) - { - auto result = ToArgument(arg); - if (resolveConverter && !result.converterName.empty()) - result.converter = resolveConverter(result.converterName); - return result; - }; - - auto strings = args.step_match_arguments | std::views::transform(transform); + auto strings = args.step_match_arguments | std::views::transform(util::ToArgument); return { strings.begin(), strings.end() }; } } diff --git a/cucumber_cpp/library/util/TransformStepMatchArgumentsList.hpp b/cucumber_cpp/library/util/TransformStepMatchArgumentsList.hpp index 5a0eae53..171df3d9 100644 --- a/cucumber_cpp/library/util/TransformStepMatchArgumentsList.hpp +++ b/cucumber_cpp/library/util/TransformStepMatchArgumentsList.hpp @@ -2,16 +2,11 @@ #define UTIL_TRANSFORM_STEP_MATCH_ARGUMENTS_LIST_HPP #include "cucumber/messages/step_match_arguments_list.hpp" -#include "cucumber_cpp/library/cucumber_expression/ParameterRegistry.hpp" #include "cucumber_cpp/library/util/Body.hpp" -#include -#include namespace cucumber_cpp::library::util { - using ConverterResolver = std::function; - - ExecuteArgs StepMatchArgumentsListToExecuteArgs(const cucumber::messages::step_match_arguments_list& args, const ConverterResolver& resolveConverter = nullptr); + ExecuteArgs StepMatchArgumentsListToExecuteArgs(const cucumber::messages::step_match_arguments_list& args); } #endif From 4d43d691185745f86275358f8990caec89b82517 Mon Sep 17 00:00:00 2001 From: "Timmer, Daan" Date: Fri, 24 Jul 2026 18:02:07 +0000 Subject: [PATCH 15/29] fix macos build? --- compatibility/CMakeLists.txt | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/compatibility/CMakeLists.txt b/compatibility/CMakeLists.txt index b8f8118e..df6125fa 100644 --- a/compatibility/CMakeLists.txt +++ b/compatibility/CMakeLists.txt @@ -42,9 +42,19 @@ target_link_libraries(compatibility.test PRIVATE # Plugins resolve symbols from this executable at runtime via dlopen. # ENABLE_EXPORTS creates the dynamic symbol table, but the linker # normally strips unreferenced symbols from static libraries. -# -rdynamic combined with --whole-archive ensures all cucumber_cpp +# --whole-archive (Linux) or -force_load (macOS) ensures all cucumber_cpp # symbols are linked and exported for plugin use. -if (NOT WIN32) +if (APPLE) + target_link_options(compatibility.test PRIVATE + "LINKER:-force_load,$" + "LINKER:-force_load,$" + "LINKER:-force_load,$" + "LINKER:-force_load,$" + "LINKER:-force_load,$" + "LINKER:-force_load,$" + "LINKER:-force_load,$" + ) +elseif (NOT WIN32) target_link_options(compatibility.test PRIVATE "LINKER:--whole-archive" $ From 6aab179b3cfa54b0cfd0f796cedd0596ddb159e8 Mon Sep 17 00:00:00 2001 From: "Timmer, Daan" Date: Fri, 24 Jul 2026 18:09:47 +0000 Subject: [PATCH 16/29] Add missing link for fmt::fmt in ccr_plugin_register --- cmake/ccr_add_plugin.cmake | 1 + 1 file changed, 1 insertion(+) diff --git a/cmake/ccr_add_plugin.cmake b/cmake/ccr_add_plugin.cmake index 6474c3c3..364f8c02 100644 --- a/cmake/ccr_add_plugin.cmake +++ b/cmake/ccr_add_plugin.cmake @@ -26,6 +26,7 @@ if (NOT TARGET ccr_plugin_register) $ $ ) + target_link_libraries(ccr_plugin_register PRIVATE fmt::fmt) set_target_properties(ccr_plugin_register PROPERTIES CXX_VISIBILITY_PRESET default ) From 857f3e539c95f516f566692edb2b4aab8cd6dc9b Mon Sep 17 00:00:00 2001 From: "Timmer, Daan" Date: Fri, 24 Jul 2026 18:16:23 +0000 Subject: [PATCH 17/29] Set .dylib suffix for Apple plugin targets --- cmake/ccr_add_plugin.cmake | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cmake/ccr_add_plugin.cmake b/cmake/ccr_add_plugin.cmake index 364f8c02..8f81ea2e 100644 --- a/cmake/ccr_add_plugin.cmake +++ b/cmake/ccr_add_plugin.cmake @@ -57,4 +57,8 @@ function(ccr_add_plugin TARGET_NAME) PREFIX "" CXX_VISIBILITY_PRESET default ) + + if (APPLE) + set_target_properties(${TARGET_NAME} PROPERTIES SUFFIX ".dylib") + endif() endfunction() From 527ca600881ccc6cc8dfd1326ffc6847ba3ebbc0 Mon Sep 17 00:00:00 2001 From: "Timmer, Daan" Date: Fri, 24 Jul 2026 18:31:12 +0000 Subject: [PATCH 18/29] Clear converter type map and unregister plugins before unloading libraries --- cucumber_cpp/library/Application.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cucumber_cpp/library/Application.cpp b/cucumber_cpp/library/Application.cpp index 870971b8..7ac89cca 100644 --- a/cucumber_cpp/library/Application.cpp +++ b/cucumber_cpp/library/Application.cpp @@ -77,6 +77,8 @@ namespace cucumber_cpp::library { if (!dynamicLibraryManager.GetLoadedLibraries().empty()) { + cucumber_expression::ConverterTypeMapClearer::ClearAll(); + cucumber_expression::ConverterTypeMapClearer::ClearFunctions().clear(); support::DefinitionRegistration::Instance().UnregisterPlugins(); dynamicLibraryManager.UnloadAll(); } From bbb06e8e9669d2258911707ddfbd12610cc11739 Mon Sep 17 00:00:00 2001 From: "Timmer, Daan" Date: Fri, 24 Jul 2026 20:31:08 +0000 Subject: [PATCH 19/29] also test for timestamps and durations --- compatibility/BaseCompatibility.cpp | 40 +++++++++++++++++-- compatibility/CMakeLists.txt | 9 +++-- cucumber_cpp/library/Application.cpp | 9 ++++- cucumber_cpp/library/Application.hpp | 8 ++-- .../support/DefinitionRegistration.cpp | 21 ++++++++-- .../support/DefinitionRegistration.hpp | 6 +++ cucumber_cpp/library/util/Duration.hpp | 4 +- cucumber_cpp/library/util/Timestamp.hpp | 4 +- 8 files changed, 82 insertions(+), 19 deletions(-) diff --git a/compatibility/BaseCompatibility.cpp b/compatibility/BaseCompatibility.cpp index 7ce3e878..9be6d54d 100644 --- a/compatibility/BaseCompatibility.cpp +++ b/compatibility/BaseCompatibility.cpp @@ -1,10 +1,13 @@ #include "BaseCompatibility.hpp" #include "cucumber_cpp/Steps.hpp" #include "cucumber_cpp/library/Application.hpp" +#include "cucumber_cpp/library/util/Duration.hpp" +#include "cucumber_cpp/library/util/Timestamp.hpp" #include "nlohmann/json.hpp" #include "nlohmann/json_fwd.hpp" #include "gmock/gmock.h" #include "gtest/gtest.h" +#include #include #include #include @@ -27,8 +30,6 @@ namespace compatibility if (key == "exception" || key == "message" || key == "line" || key == "snippets") jsonIter = json.erase(jsonIter); - else if (key == "timestamp" || key == "duration") - jsonIter = json.erase(jsonIter); else if (value.is_object()) { RemoveIncompatibilities(value, sourceDir); @@ -119,6 +120,35 @@ namespace compatibility ++expectedIter; } } + + struct StopwatchIncremental : cucumber_cpp::library::util::Stopwatch + { + virtual ~StopwatchIncremental() = default; + + std::chrono::high_resolution_clock::time_point Start() override + { + return {}; + } + + std::chrono::nanoseconds Duration([[maybe_unused]] std::chrono::high_resolution_clock::time_point timePoint) override + { + return current; + } + + std::chrono::nanoseconds current{ std::chrono::milliseconds{ 1 } }; + }; + + struct TimestampGeneratorIncremental : cucumber_cpp::library::util::TimestampGenerator + { + virtual ~TimestampGeneratorIncremental() = default; + + std::chrono::milliseconds Now() override + { + return current++; + } + + std::chrono::milliseconds current{ 0 }; + }; } void RunDevkit(const KitInfo& kit) @@ -145,7 +175,11 @@ namespace compatibility argv.push_back(s.c_str()); { - cucumber_cpp::library::Application app{ std::make_shared(), false }; + cucumber_cpp::library::Application app{ + std::make_shared(), false, + std::make_unique(), + std::make_unique() + }; static_cast(app.Run(static_cast(argv.size()), argv.data())); } diff --git a/compatibility/CMakeLists.txt b/compatibility/CMakeLists.txt index df6125fa..91c4d8cb 100644 --- a/compatibility/CMakeLists.txt +++ b/compatibility/CMakeLists.txt @@ -33,7 +33,6 @@ add_executable(compatibility.test) target_sources(compatibility.test PRIVATE TestCompatibility.cpp ) - target_link_libraries(compatibility.test PRIVATE GTest::gtest_main compatibility.base @@ -81,6 +80,10 @@ target_compile_definitions(compatibility.test PRIVATE COMPAT_PLUGIN_DIR="$" ) -if(CCR_BUILD_TESTS AND NOT CMAKE_CROSSCOMPILING) - gtest_discover_tests(compatibility.test) +if(CCR_BUILD_TESTS) + if(NOT CMAKE_CROSSCOMPILING AND NOT CMAKE_HOST_SYSTEM_NAME STREQUAL "Windows") + gtest_discover_tests(compatibility.test) + else() + add_test(NAME compatibility.test COMMAND compatibility.test) + endif() endif() diff --git a/cucumber_cpp/library/Application.cpp b/cucumber_cpp/library/Application.cpp index 7ac89cca..feef4e32 100644 --- a/cucumber_cpp/library/Application.cpp +++ b/cucumber_cpp/library/Application.cpp @@ -66,9 +66,13 @@ namespace cucumber_cpp::library } } - Application::Application(std::shared_ptr contextStorageFactory, bool removeDefaultGoogleTestListener) + Application::Application(std::shared_ptr contextStorageFactory, bool removeDefaultGoogleTestListener, + std::unique_ptr stopwatch, + std::unique_ptr timestampGenerator) : contextStorageFactory{ std::move(contextStorageFactory) } , removeDefaultGoogleTestListener{ removeDefaultGoogleTestListener } + , stopwatch{ std::move(stopwatch) } + , timestampGenerator{ std::move(timestampGenerator) } { cli.set_config("--config", "cucumber.toml"); } @@ -147,7 +151,10 @@ namespace cucumber_cpp::library std::ofstream{ "cucumber.toml" } << cli.config_to_str(true, true); if (!options.loadPaths.empty()) + { + support::DefinitionRegistration::Instance().TakeSnapshot(); dynamicLibraryManager.Load(options.loadPaths); + } return RunFeatures(); } diff --git a/cucumber_cpp/library/Application.hpp b/cucumber_cpp/library/Application.hpp index 0a93c323..f2a75931 100644 --- a/cucumber_cpp/library/Application.hpp +++ b/cucumber_cpp/library/Application.hpp @@ -60,7 +60,9 @@ namespace cucumber_cpp::library std::vector loadPaths{}; }; - explicit Application(std::shared_ptr contextStorageFactory = std::make_shared(), bool removeDefaultGoogleTestListener = true); + explicit Application(std::shared_ptr contextStorageFactory = std::make_shared(), bool removeDefaultGoogleTestListener = true, + std::unique_ptr stopwatch = std::make_unique(), + std::unique_ptr timestampGenerator = std::make_unique()); ~Application(); @@ -91,8 +93,8 @@ namespace cucumber_cpp::library cucumber_expression::ParameterRegistry parameterRegistry{ cucumber_cpp::library::support::DefinitionRegistration::Instance().GetRegisteredParameters() }; bool removeDefaultGoogleTestListener; - util::StopWatchHighResolutionClock stopwatchHighResolutionClock; - util::TimestampGeneratorSystemClock timestampGeneratorSystemClock; + std::unique_ptr stopwatch; + std::unique_ptr timestampGenerator; bool runPassed{ false }; }; diff --git a/cucumber_cpp/library/support/DefinitionRegistration.cpp b/cucumber_cpp/library/support/DefinitionRegistration.cpp index 2fd0b40b..75ac543e 100644 --- a/cucumber_cpp/library/support/DefinitionRegistration.cpp +++ b/cucumber_cpp/library/support/DefinitionRegistration.cpp @@ -7,16 +7,15 @@ #include "cucumber_cpp/library/util/HookData.hpp" #include "cucumber_cpp/library/util/HookFactory.hpp" #include "cucumber_cpp/library/util/StepFactory.hpp" -#include #include #include #include -#include #include #include #include #include #include +#include #include #include @@ -44,6 +43,17 @@ namespace cucumber_cpp::library::support void DefinitionRegistration::UnregisterPlugins() { plugins.clear(); + registry.clear(); + customParameters.clear(); + } + + void DefinitionRegistration::TakeSnapshot() + { + if (staticRegistry.empty() && staticCustomParameters.empty()) + { + staticRegistry = std::move(registry); + staticCustomParameters = std::move(customParameters); + } } void DefinitionRegistration::LoadIds(cucumber::gherkin::id_generator_ptr idGenerator) @@ -53,6 +63,9 @@ namespace cucumber_cpp::library::support entry.id = idGenerator->next_id(); }; + for (auto& [key, item] : staticRegistry) + std::visit(assignGenerator, item); + for (auto& [key, item] : registry) std::visit(assignGenerator, item); @@ -72,6 +85,7 @@ namespace cucumber_cpp::library::support result.push_back(std::get(entry)); }; + collectHooks(staticRegistry); collectHooks(registry); for (auto* plugin : plugins) @@ -82,7 +96,8 @@ namespace cucumber_cpp::library::support std::set> DefinitionRegistration::GetRegisteredParameters() const { - auto result = customParameters; + auto result = staticCustomParameters; + result.insert(customParameters.begin(), customParameters.end()); for (const auto* plugin : plugins) result.insert(plugin->customParameters.begin(), plugin->customParameters.end()); diff --git a/cucumber_cpp/library/support/DefinitionRegistration.hpp b/cucumber_cpp/library/support/DefinitionRegistration.hpp index 0674d119..d94778a5 100644 --- a/cucumber_cpp/library/support/DefinitionRegistration.hpp +++ b/cucumber_cpp/library/support/DefinitionRegistration.hpp @@ -32,6 +32,8 @@ namespace cucumber_cpp::library::support void RegisterPlugin(DefinitionRegistration& plugin); void UnregisterPlugins(); + void TakeSnapshot(); + void LoadIds(cucumber::gherkin::id_generator_ptr idGenerator); template @@ -62,6 +64,9 @@ namespace cucumber_cpp::library::support std::set> customParameters; std::vector plugins; + + std::map staticRegistry; + std::set> staticCustomParameters; }; ////////////////////////// @@ -88,6 +93,7 @@ namespace cucumber_cpp::library::support func(step); }; + forEachStep(staticRegistry); forEachStep(registry); for (auto* plugin : plugins) diff --git a/cucumber_cpp/library/util/Duration.hpp b/cucumber_cpp/library/util/Duration.hpp index fb1741b0..30cfd43d 100644 --- a/cucumber_cpp/library/util/Duration.hpp +++ b/cucumber_cpp/library/util/Duration.hpp @@ -15,11 +15,9 @@ namespace cucumber_cpp::library::util struct Stopwatch { - protected: Stopwatch(); - ~Stopwatch() = default; + virtual ~Stopwatch() = default; - public: static Stopwatch& Instance(); virtual std::chrono::high_resolution_clock::time_point Start() = 0; diff --git a/cucumber_cpp/library/util/Timestamp.hpp b/cucumber_cpp/library/util/Timestamp.hpp index a4ab865a..8fc6e9d6 100644 --- a/cucumber_cpp/library/util/Timestamp.hpp +++ b/cucumber_cpp/library/util/Timestamp.hpp @@ -15,11 +15,9 @@ namespace cucumber_cpp::library::util struct TimestampGenerator { - protected: TimestampGenerator(); - ~TimestampGenerator(); + virtual ~TimestampGenerator(); - public: static TimestampGenerator& Instance(); virtual std::chrono::milliseconds Now() = 0; From 60ab2b13bef4ac918fd9c66fbe2ea17dad88ad97 Mon Sep 17 00:00:00 2001 From: "Timmer, Daan" Date: Fri, 24 Jul 2026 20:33:23 +0000 Subject: [PATCH 20/29] move ccache detection to root CMakeLists --- CMakeLists.txt | 13 +++++++++++++ CMakePresets.json | 4 +--- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index cbd60866..aa268b4a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -54,6 +54,19 @@ set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED On) set(CMAKE_CXX_EXTENSIONS Off) +# Compiler cache +find_program(CCACHE_PROGRAM ccache) +if(CCACHE_PROGRAM) + set(CMAKE_CXX_COMPILER_LAUNCHER "${CCACHE_PROGRAM}") + message(STATUS "Using ccache: ${CCACHE_PROGRAM}") +else() + find_program(SCCACHE_PROGRAM sccache) + if(SCCACHE_PROGRAM) + set(CMAKE_CXX_COMPILER_LAUNCHER "${SCCACHE_PROGRAM}") + message(STATUS "Using sccache: ${SCCACHE_PROGRAM}") + endif() +endif() + set_directory_properties(PROPERTY USE_FOLDERS ON) include(FetchContent) diff --git a/CMakePresets.json b/CMakePresets.json index 1a847897..ea3d2ed6 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -13,9 +13,7 @@ "installDir": "${sourceDir}/.install/${presetName}", "cacheVariables": { "CMAKE_EXPORT_COMPILE_COMMANDS": "On", - "CMAKE_CONFIGURATION_TYPES": "Debug;RelWithDebInfo;Release", - "CMAKE_C_COMPILER_LAUNCHER": "ccache", - "CMAKE_CXX_COMPILER_LAUNCHER": "ccache" + "CMAKE_CONFIGURATION_TYPES": "Debug;RelWithDebInfo;Release" } }, { From 308bdbd5319f1ecc2b88effffe3a54b6009cfcf2 Mon Sep 17 00:00:00 2001 From: "Timmer, Daan" Date: Fri, 24 Jul 2026 20:53:18 +0000 Subject: [PATCH 21/29] Add PluginHostContext and update registration mechanism for plugins --- .../library/plugin/DynamicLibraryManager.cpp | 11 ++++++++++- cucumber_cpp/library/plugin/PluginExport.hpp | 12 +++++++++++- cucumber_cpp/library/plugin/PluginRegister.cpp | 14 +++++++++++--- cucumber_cpp/library/util/Duration.cpp | 5 +++++ cucumber_cpp/library/util/Duration.hpp | 1 + cucumber_cpp/library/util/Timestamp.cpp | 5 +++++ cucumber_cpp/library/util/Timestamp.hpp | 1 + 7 files changed, 44 insertions(+), 5 deletions(-) diff --git a/cucumber_cpp/library/plugin/DynamicLibraryManager.cpp b/cucumber_cpp/library/plugin/DynamicLibraryManager.cpp index fc244124..95979c00 100644 --- a/cucumber_cpp/library/plugin/DynamicLibraryManager.cpp +++ b/cucumber_cpp/library/plugin/DynamicLibraryManager.cpp @@ -2,6 +2,8 @@ #include "cucumber_cpp/library/plugin/DynamicLibrary.hpp" #include "cucumber_cpp/library/plugin/PluginExport.hpp" #include "cucumber_cpp/library/support/DefinitionRegistration.hpp" +#include "cucumber_cpp/library/util/Duration.hpp" +#include "cucumber_cpp/library/util/Timestamp.hpp" #include #include #include @@ -46,7 +48,14 @@ namespace cucumber_cpp::library::plugin const auto& lib = libraries.emplace_back(path); auto registerFn = lib.GetSymbol("ccr_register"); - registerFn(&support::DefinitionRegistration::Instance()); + + PluginHostContext context{ + .registration = &support::DefinitionRegistration::Instance(), + .stopwatch = &util::Stopwatch::Instance(), + .timestampGenerator = &util::TimestampGenerator::Instance(), + }; + + registerFn(&context); } void DynamicLibraryManager::LoadDirectory(const std::filesystem::path& directory) diff --git a/cucumber_cpp/library/plugin/PluginExport.hpp b/cucumber_cpp/library/plugin/PluginExport.hpp index d4af94e7..48ed6b2c 100644 --- a/cucumber_cpp/library/plugin/PluginExport.hpp +++ b/cucumber_cpp/library/plugin/PluginExport.hpp @@ -7,6 +7,16 @@ #define CCR_EXPORT __attribute__((visibility("default"))) #endif -using CcrRegisterFn = void (*)(void*); +namespace cucumber_cpp::library::plugin +{ + struct PluginHostContext + { + void* registration; + void* stopwatch; + void* timestampGenerator; + }; +} + +using CcrRegisterFn = void (*)(cucumber_cpp::library::plugin::PluginHostContext*); #endif diff --git a/cucumber_cpp/library/plugin/PluginRegister.cpp b/cucumber_cpp/library/plugin/PluginRegister.cpp index 38881d3e..7b2ebd95 100644 --- a/cucumber_cpp/library/plugin/PluginRegister.cpp +++ b/cucumber_cpp/library/plugin/PluginRegister.cpp @@ -1,11 +1,19 @@ #include "cucumber_cpp/library/plugin/PluginExport.hpp" #include "cucumber_cpp/library/support/DefinitionRegistration.hpp" +#include "cucumber_cpp/library/util/Duration.hpp" +#include "cucumber_cpp/library/util/Timestamp.hpp" -extern "C" CCR_EXPORT void ccr_register(void* hostRegistration) +extern "C" CCR_EXPORT void ccr_register(cucumber_cpp::library::plugin::PluginHostContext* context) { - if (hostRegistration == nullptr) + if (context == nullptr || context->registration == nullptr) return; - auto& host = *static_cast(hostRegistration); + auto& host = *static_cast(context->registration); host.RegisterPlugin(cucumber_cpp::library::support::DefinitionRegistration::Instance()); + + if (context->stopwatch != nullptr) + cucumber_cpp::library::util::Stopwatch::SetInstance(*static_cast(context->stopwatch)); + + if (context->timestampGenerator != nullptr) + cucumber_cpp::library::util::TimestampGenerator::SetInstance(*static_cast(context->timestampGenerator)); } diff --git a/cucumber_cpp/library/util/Duration.cpp b/cucumber_cpp/library/util/Duration.cpp index 8f0be59c..d530077a 100644 --- a/cucumber_cpp/library/util/Duration.cpp +++ b/cucumber_cpp/library/util/Duration.cpp @@ -39,6 +39,11 @@ namespace cucumber_cpp::library::util return *instance; } + void Stopwatch::SetInstance(Stopwatch& inst) + { + instance = &inst; + } + std::chrono::high_resolution_clock::time_point StopWatchHighResolutionClock::Start() { return std::chrono::high_resolution_clock::now(); diff --git a/cucumber_cpp/library/util/Duration.hpp b/cucumber_cpp/library/util/Duration.hpp index 30cfd43d..e654b9ee 100644 --- a/cucumber_cpp/library/util/Duration.hpp +++ b/cucumber_cpp/library/util/Duration.hpp @@ -19,6 +19,7 @@ namespace cucumber_cpp::library::util virtual ~Stopwatch() = default; static Stopwatch& Instance(); + static void SetInstance(Stopwatch& inst); virtual std::chrono::high_resolution_clock::time_point Start() = 0; virtual std::chrono::nanoseconds Duration(std::chrono::high_resolution_clock::time_point timepPoint) = 0; diff --git a/cucumber_cpp/library/util/Timestamp.cpp b/cucumber_cpp/library/util/Timestamp.cpp index d7cacd6d..50b1839f 100644 --- a/cucumber_cpp/library/util/Timestamp.cpp +++ b/cucumber_cpp/library/util/Timestamp.cpp @@ -39,6 +39,11 @@ namespace cucumber_cpp::library::util return *instance; } + void TimestampGenerator::SetInstance(TimestampGenerator& inst) + { + instance = &inst; + } + std::chrono::milliseconds TimestampGeneratorSystemClock::Now() { const auto now = std::chrono::system_clock::now().time_since_epoch(); diff --git a/cucumber_cpp/library/util/Timestamp.hpp b/cucumber_cpp/library/util/Timestamp.hpp index 8fc6e9d6..05b4b9b2 100644 --- a/cucumber_cpp/library/util/Timestamp.hpp +++ b/cucumber_cpp/library/util/Timestamp.hpp @@ -19,6 +19,7 @@ namespace cucumber_cpp::library::util virtual ~TimestampGenerator(); static TimestampGenerator& Instance(); + static void SetInstance(TimestampGenerator& inst); virtual std::chrono::milliseconds Now() = 0; private: From 281ceb223d3a3a7532bd416c22ab8772d03b5d12 Mon Sep 17 00:00:00 2001 From: "Timmer, Daan" Date: Sat, 25 Jul 2026 01:16:24 +0200 Subject: [PATCH 22/29] working on getting error reporting to be shared between --- compatibility/BaseCompatibility.cpp | 1 + .../examples-tables/actual_run.ndjson | 18 +++ cucumber_cpp/library/Application.cpp | 1 - .../cucumber_expression/ParameterRegistry.hpp | 116 ++++++++++++++---- .../library/plugin/DynamicLibraryManager.cpp | 2 + cucumber_cpp/library/plugin/PluginExport.hpp | 1 + .../library/plugin/PluginRegister.cpp | 18 +++ .../library/runtime/NestedTestCaseRunner.cpp | 2 +- cucumber_cpp/library/util/Body.cpp | 53 ++++---- cucumber_cpp/library/util/Body.hpp | 15 ++- 10 files changed, 168 insertions(+), 59 deletions(-) create mode 100644 compatibility/examples-tables/actual_run.ndjson diff --git a/compatibility/BaseCompatibility.cpp b/compatibility/BaseCompatibility.cpp index 9be6d54d..e2223a8e 100644 --- a/compatibility/BaseCompatibility.cpp +++ b/compatibility/BaseCompatibility.cpp @@ -161,6 +161,7 @@ namespace compatibility argStrings.emplace_back("--load"); argStrings.emplace_back(kit.pluginPath.string()); argStrings.emplace_back("--format"); + argStrings.emplace_back("pretty"); argStrings.emplace_back("message:" + actualNdjsonPath.string()); for (const auto& arg : kit.extraArgs) diff --git a/compatibility/examples-tables/actual_run.ndjson b/compatibility/examples-tables/actual_run.ndjson new file mode 100644 index 00000000..cda4b3c8 --- /dev/null +++ b/compatibility/examples-tables/actual_run.ndjson @@ -0,0 +1,18 @@ +{"source":{"data":"Feature: Examples Tables\n Sometimes it can be desirable to run the same scenario multiple times with\n different data each time - this can be done by placing an Examples table\n underneath a Scenario, and use in the Scenario which match the\n table headers.\n\n The Scenario Outline name can also be parameterized. The name of the resulting\n pickle will have the replaced with the value from the examples\n table.\n\n Scenario Outline: Eating cucumbers\n Given there are cucumbers\n When I eat cucumbers\n Then I should have cucumbers\n\n @passing\n Examples: These are passing\n | start | eat | left |\n | 12 | 5 | 7 |\n | 20 | 5 | 15 |\n\n @failing\n Examples: These are failing\n | start | eat | left |\n | 12 | 20 | 0 |\n | 0 | 1 | 0 |\n\n Scenario Outline: Eating cucumbers with friends\n Given there are friends\n And there are cucumbers\n Then each person can eat cucumbers\n\n Examples:\n | friends | start | share |\n | 11 | 12 | 1 |\n | 1 | 4 | 2 |\n | 0 | 4 | 4 |\n","mediaType":"text/x.cucumber.gherkin+plain","uri":"D:/git/amp-cucumber-cpp-runner/compatibility\\examples-tables\\examples-tables.feature"}} +{"gherkinDocument":{"comments":[],"feature":{"children":[{"scenario":{"description":"","examples":[{"description":"","id":"7","keyword":"Examples","location":{"column":5,"line":17},"name":"These are passing","tableBody":[{"cells":[{"location":{"column":12,"line":19},"value":"12"},{"location":{"column":19,"line":19},"value":"5"},{"location":{"column":26,"line":19},"value":"7"}],"id":"4","location":{"column":7,"line":19}},{"cells":[{"location":{"column":12,"line":20},"value":"20"},{"location":{"column":19,"line":20},"value":"5"},{"location":{"column":25,"line":20},"value":"15"}],"id":"5","location":{"column":7,"line":20}}],"tableHeader":{"cells":[{"location":{"column":9,"line":18},"value":"start"},{"location":{"column":17,"line":18},"value":"eat"},{"location":{"column":23,"line":18},"value":"left"}],"id":"3","location":{"column":7,"line":18}},"tags":[{"id":"6","location":{"column":5,"line":16},"name":"@passing"}]},{"description":"","id":"12","keyword":"Examples","location":{"column":5,"line":23},"name":"These are failing","tableBody":[{"cells":[{"location":{"column":12,"line":25},"value":"12"},{"location":{"column":18,"line":25},"value":"20"},{"location":{"column":26,"line":25},"value":"0"}],"id":"9","location":{"column":7,"line":25}},{"cells":[{"location":{"column":13,"line":26},"value":"0"},{"location":{"column":19,"line":26},"value":"1"},{"location":{"column":26,"line":26},"value":"0"}],"id":"10","location":{"column":7,"line":26}}],"tableHeader":{"cells":[{"location":{"column":9,"line":24},"value":"start"},{"location":{"column":17,"line":24},"value":"eat"},{"location":{"column":23,"line":24},"value":"left"}],"id":"8","location":{"column":7,"line":24}},"tags":[{"id":"11","location":{"column":5,"line":22},"name":"@failing"}]}],"id":"13","keyword":"Scenario Outline","location":{"column":3,"line":11},"name":"Eating cucumbers","steps":[{"id":"0","keyword":"Given ","keywordType":"Context","location":{"column":5,"line":12},"text":"there are cucumbers"},{"id":"1","keyword":"When ","keywordType":"Action","location":{"column":5,"line":13},"text":"I eat cucumbers"},{"id":"2","keyword":"Then ","keywordType":"Outcome","location":{"column":5,"line":14},"text":"I should have cucumbers"}],"tags":[]}},{"scenario":{"description":"","examples":[{"description":"","id":"21","keyword":"Examples","location":{"column":5,"line":33},"name":"","tableBody":[{"cells":[{"location":{"column":14,"line":35},"value":"11"},{"location":{"column":22,"line":35},"value":"12"},{"location":{"column":31,"line":35},"value":"1"}],"id":"18","location":{"column":7,"line":35}},{"cells":[{"location":{"column":15,"line":36},"value":"1"},{"location":{"column":23,"line":36},"value":"4"},{"location":{"column":31,"line":36},"value":"2"}],"id":"19","location":{"column":7,"line":36}},{"cells":[{"location":{"column":15,"line":37},"value":"0"},{"location":{"column":23,"line":37},"value":"4"},{"location":{"column":31,"line":37},"value":"4"}],"id":"20","location":{"column":7,"line":37}}],"tableHeader":{"cells":[{"location":{"column":9,"line":34},"value":"friends"},{"location":{"column":19,"line":34},"value":"start"},{"location":{"column":27,"line":34},"value":"share"}],"id":"17","location":{"column":7,"line":34}},"tags":[]}],"id":"22","keyword":"Scenario Outline","location":{"column":3,"line":28},"name":"Eating cucumbers with friends","steps":[{"id":"14","keyword":"Given ","keywordType":"Context","location":{"column":5,"line":29},"text":"there are friends"},{"id":"15","keyword":"And ","keywordType":"Conjunction","location":{"column":5,"line":30},"text":"there are cucumbers"},{"id":"16","keyword":"Then ","keywordType":"Outcome","location":{"column":5,"line":31},"text":"each person can eat cucumbers"}],"tags":[]}}],"description":" Sometimes it can be desirable to run the same scenario multiple times with\n different data each time - this can be done by placing an Examples table\n underneath a Scenario, and use in the Scenario which match the\n table headers.\n\n The Scenario Outline name can also be parameterized. The name of the resulting\n pickle will have the replaced with the value from the examples\n table.","keyword":"Feature","language":"en","location":{"column":1,"line":1},"name":"Examples Tables","tags":[]},"uri":"D:/git/amp-cucumber-cpp-runner/compatibility\\examples-tables\\examples-tables.feature"}} +{"pickle":{"astNodeIds":["13","4"],"id":"26","language":"en","location":{"column":7,"line":19},"name":"Eating cucumbers","steps":[{"astNodeIds":["0","4"],"id":"23","text":"there are 12 cucumbers","type":"Context"},{"astNodeIds":["1","4"],"id":"24","text":"I eat 5 cucumbers","type":"Action"},{"astNodeIds":["2","4"],"id":"25","text":"I should have 7 cucumbers","type":"Outcome"}],"tags":[{"astNodeId":"6","name":"@passing"}],"uri":"D:/git/amp-cucumber-cpp-runner/compatibility\\examples-tables\\examples-tables.feature"}} +{"pickle":{"astNodeIds":["13","5"],"id":"30","language":"en","location":{"column":7,"line":20},"name":"Eating cucumbers","steps":[{"astNodeIds":["0","5"],"id":"27","text":"there are 20 cucumbers","type":"Context"},{"astNodeIds":["1","5"],"id":"28","text":"I eat 5 cucumbers","type":"Action"},{"astNodeIds":["2","5"],"id":"29","text":"I should have 15 cucumbers","type":"Outcome"}],"tags":[{"astNodeId":"6","name":"@passing"}],"uri":"D:/git/amp-cucumber-cpp-runner/compatibility\\examples-tables\\examples-tables.feature"}} +{"pickle":{"astNodeIds":["13","9"],"id":"34","language":"en","location":{"column":7,"line":25},"name":"Eating cucumbers","steps":[{"astNodeIds":["0","9"],"id":"31","text":"there are 12 cucumbers","type":"Context"},{"astNodeIds":["1","9"],"id":"32","text":"I eat 20 cucumbers","type":"Action"},{"astNodeIds":["2","9"],"id":"33","text":"I should have 0 cucumbers","type":"Outcome"}],"tags":[{"astNodeId":"11","name":"@failing"}],"uri":"D:/git/amp-cucumber-cpp-runner/compatibility\\examples-tables\\examples-tables.feature"}} +{"pickle":{"astNodeIds":["13","10"],"id":"38","language":"en","location":{"column":7,"line":26},"name":"Eating cucumbers","steps":[{"astNodeIds":["0","10"],"id":"35","text":"there are 0 cucumbers","type":"Context"},{"astNodeIds":["1","10"],"id":"36","text":"I eat 1 cucumbers","type":"Action"},{"astNodeIds":["2","10"],"id":"37","text":"I should have 0 cucumbers","type":"Outcome"}],"tags":[{"astNodeId":"11","name":"@failing"}],"uri":"D:/git/amp-cucumber-cpp-runner/compatibility\\examples-tables\\examples-tables.feature"}} +{"pickle":{"astNodeIds":["22","18"],"id":"42","language":"en","location":{"column":7,"line":35},"name":"Eating cucumbers with 11 friends","steps":[{"astNodeIds":["14","18"],"id":"39","text":"there are 11 friends","type":"Context"},{"astNodeIds":["15","18"],"id":"40","text":"there are 12 cucumbers","type":"Context"},{"astNodeIds":["16","18"],"id":"41","text":"each person can eat 1 cucumbers","type":"Outcome"}],"tags":[],"uri":"D:/git/amp-cucumber-cpp-runner/compatibility\\examples-tables\\examples-tables.feature"}} +{"pickle":{"astNodeIds":["22","19"],"id":"46","language":"en","location":{"column":7,"line":36},"name":"Eating cucumbers with 1 friends","steps":[{"astNodeIds":["14","19"],"id":"43","text":"there are 1 friends","type":"Context"},{"astNodeIds":["15","19"],"id":"44","text":"there are 4 cucumbers","type":"Context"},{"astNodeIds":["16","19"],"id":"45","text":"each person can eat 2 cucumbers","type":"Outcome"}],"tags":[],"uri":"D:/git/amp-cucumber-cpp-runner/compatibility\\examples-tables\\examples-tables.feature"}} +{"pickle":{"astNodeIds":["22","20"],"id":"50","language":"en","location":{"column":7,"line":37},"name":"Eating cucumbers with 0 friends","steps":[{"astNodeIds":["14","20"],"id":"47","text":"there are 0 friends","type":"Context"},{"astNodeIds":["15","20"],"id":"48","text":"there are 4 cucumbers","type":"Context"},{"astNodeIds":["16","20"],"id":"49","text":"each person can eat 4 cucumbers","type":"Outcome"}],"tags":[],"uri":"D:/git/amp-cucumber-cpp-runner/compatibility\\examples-tables\\examples-tables.feature"}} +{"stepDefinition":{"id":"51","pattern":{"source":"there are {int} cucumbers","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":7},"uri":"D:\\git\\amp-cucumber-cpp-runner\\compatibility\\examples-tables\\examples-tables.cpp"}}} +{"stepDefinition":{"id":"52","pattern":{"source":"there are {int} friends","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":12},"uri":"D:\\git\\amp-cucumber-cpp-runner\\compatibility\\examples-tables\\examples-tables.cpp"}}} +{"stepDefinition":{"id":"53","pattern":{"source":"I eat {int} cucumbers","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":17},"uri":"D:\\git\\amp-cucumber-cpp-runner\\compatibility\\examples-tables\\examples-tables.cpp"}}} +{"stepDefinition":{"id":"54","pattern":{"source":"I should have {int} cucumbers","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":22},"uri":"D:\\git\\amp-cucumber-cpp-runner\\compatibility\\examples-tables\\examples-tables.cpp"}}} +{"stepDefinition":{"id":"55","pattern":{"source":"each person can eat {int} cucumbers","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":27},"uri":"D:\\git\\amp-cucumber-cpp-runner\\compatibility\\examples-tables\\examples-tables.cpp"}}} +{"testRunStarted":{"id":"56","timestamp":{"nanos":0,"seconds":0}}} +{"testCase":{"id":"57","pickleId":"26","testRunStartedId":"56","testSteps":[{"id":"58","pickleStepId":"23","stepDefinitionIds":["51"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":10,"value":"12"},"parameterTypeName":"int"}]}]},{"id":"59","pickleStepId":"24","stepDefinitionIds":["53"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":6,"value":"5"},"parameterTypeName":"int"}]}]},{"id":"60","pickleStepId":"25","stepDefinitionIds":["54"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":14,"value":"7"},"parameterTypeName":"int"}]}]}]}} +{"testCase":{"id":"61","pickleId":"30","testRunStartedId":"56","testSteps":[{"id":"62","pickleStepId":"27","stepDefinitionIds":["51"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":10,"value":"20"},"parameterTypeName":"int"}]}]},{"id":"63","pickleStepId":"28","stepDefinitionIds":["53"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":6,"value":"5"},"parameterTypeName":"int"}]}]},{"id":"64","pickleStepId":"29","stepDefinitionIds":["54"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":14,"value":"15"},"parameterTypeName":"int"}]}]}]}} +{"testCase":{"id":"65","pickleId":"34","testRunStartedId":"56","testSteps":[{"id":"66","pickleStepId":"31","stepDefinitionIds":["51"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":10,"value":"12"},"param \ No newline at end of file diff --git a/cucumber_cpp/library/Application.cpp b/cucumber_cpp/library/Application.cpp index feef4e32..68cd526f 100644 --- a/cucumber_cpp/library/Application.cpp +++ b/cucumber_cpp/library/Application.cpp @@ -82,7 +82,6 @@ namespace cucumber_cpp::library if (!dynamicLibraryManager.GetLoadedLibraries().empty()) { cucumber_expression::ConverterTypeMapClearer::ClearAll(); - cucumber_expression::ConverterTypeMapClearer::ClearFunctions().clear(); support::DefinitionRegistration::Instance().UnregisterPlugins(); dynamicLibraryManager.UnloadAll(); } diff --git a/cucumber_cpp/library/cucumber_expression/ParameterRegistry.hpp b/cucumber_cpp/library/cucumber_expression/ParameterRegistry.hpp index 69b59824..33e85b57 100644 --- a/cucumber_cpp/library/cucumber_expression/ParameterRegistry.hpp +++ b/cucumber_cpp/library/cucumber_expression/ParameterRegistry.hpp @@ -3,6 +3,7 @@ #include "fmt/format.h" #include +#include #include #include #include @@ -129,50 +130,113 @@ namespace cucumber_cpp::library::cucumber_expression template using ConverterFunction = std::function; - template - using TypeMap = std::map>; + // Type-erased converter used at the host/plugin ABI boundary. The stored + // std::function returns std::any so a single non-templated map can hold + // converters for all return types. std::any_cast is performed at the call + // site (host or plugin), while the underlying storage can be shared. + using AnyConverterFunction = std::function; - template - struct ConverterTypeMap - { - static TypeMap& Instance(); - }; + using ConverterMap = std::map; - struct ConverterTypeMapClearer + // Central converter registry. Each DLL has its own default map, but the + // active pointer can be redirected to a shared (host-owned) map so + // registrations and lookups cross DLL boundaries safely. + struct ConverterRegistry { - static std::vector>& ClearFunctions() + static ConverterMap& Instance() { - static std::vector> fns; - return fns; + return *ActivePtr(); } - static void Register(std::function fn) + static ConverterMap& LocalInstance() { - ClearFunctions().push_back(std::move(fn)); + static ConverterMap map; + return map; } + // Redirect this DLL's active converter map to an externally owned map + // (typically the host's). Passing nullptr restores the local map. + static void SetInstance(ConverterMap* external) + { + ActivePtr() = external != nullptr ? external : &LocalInstance(); + } + + private: + static ConverterMap*& ActivePtr() + { + static ConverterMap* ptr = &LocalInstance(); + return ptr; + } + }; + + struct ConverterTypeMapClearer + { static void ClearAll() { - for (const auto& fn : ClearFunctions()) - fn(); + ConverterRegistry::LocalInstance().clear(); + ConverterRegistry::Instance().clear(); } }; + // Backwards-compatible typed accessor. Wraps/unwraps std::any so existing + // typed callers keep working, but storage is unified in ConverterRegistry. template - TypeMap& ConverterTypeMap::Instance() + struct ConverterTypeMap { - static TypeMap typeMap; - [[maybe_unused]] static const bool registered = [] + // Proxy allowing typed insertion/lookup against the underlying any map. + struct Proxy { - auto* ptr = &typeMap; - ConverterTypeMapClearer::Register([ptr] + ConverterMap& map; + + void emplace(const std::string& name, ConverterFunction fn) + { + map[name] = [fn = std::move(fn)](const ConvertFunctionArg& args) -> std::any { - ptr->clear(); - }); - return true; - }(); - return typeMap; - } + return std::any{ fn(args) }; + }; + } + + struct TypedAccessor + { + AnyConverterFunction& fn; + + T operator()(const ConvertFunctionArg& args) const + { + return std::any_cast(fn(args)); + } + }; + + TypedAccessor at(const std::string& name) + { + return TypedAccessor{ map.at(name) }; + } + + struct Assigner + { + ConverterMap& map; + std::string name; + + Assigner& operator=(ConverterFunction fn) + { + map[name] = [fn = std::move(fn)](const ConvertFunctionArg& args) -> std::any + { + return std::any{ fn(args) }; + }; + return *this; + } + }; + + Assigner operator[](const std::string& name) + { + return Assigner{ map, name }; + } + }; + + static Proxy Instance() + { + return Proxy{ ConverterRegistry::Instance() }; + } + }; struct ParameterRegistry { diff --git a/cucumber_cpp/library/plugin/DynamicLibraryManager.cpp b/cucumber_cpp/library/plugin/DynamicLibraryManager.cpp index 95979c00..2627b6b6 100644 --- a/cucumber_cpp/library/plugin/DynamicLibraryManager.cpp +++ b/cucumber_cpp/library/plugin/DynamicLibraryManager.cpp @@ -1,4 +1,5 @@ #include "cucumber_cpp/library/plugin/DynamicLibraryManager.hpp" +#include "cucumber_cpp/library/cucumber_expression/ParameterRegistry.hpp" #include "cucumber_cpp/library/plugin/DynamicLibrary.hpp" #include "cucumber_cpp/library/plugin/PluginExport.hpp" #include "cucumber_cpp/library/support/DefinitionRegistration.hpp" @@ -53,6 +54,7 @@ namespace cucumber_cpp::library::plugin .registration = &support::DefinitionRegistration::Instance(), .stopwatch = &util::Stopwatch::Instance(), .timestampGenerator = &util::TimestampGenerator::Instance(), + .converterMap = &cucumber_expression::ConverterRegistry::Instance(), }; registerFn(&context); diff --git a/cucumber_cpp/library/plugin/PluginExport.hpp b/cucumber_cpp/library/plugin/PluginExport.hpp index 48ed6b2c..dc5f6af0 100644 --- a/cucumber_cpp/library/plugin/PluginExport.hpp +++ b/cucumber_cpp/library/plugin/PluginExport.hpp @@ -14,6 +14,7 @@ namespace cucumber_cpp::library::plugin void* registration; void* stopwatch; void* timestampGenerator; + void* converterMap; }; } diff --git a/cucumber_cpp/library/plugin/PluginRegister.cpp b/cucumber_cpp/library/plugin/PluginRegister.cpp index 7b2ebd95..7885bafa 100644 --- a/cucumber_cpp/library/plugin/PluginRegister.cpp +++ b/cucumber_cpp/library/plugin/PluginRegister.cpp @@ -1,3 +1,4 @@ +#include "cucumber_cpp/library/cucumber_expression/ParameterRegistry.hpp" #include "cucumber_cpp/library/plugin/PluginExport.hpp" #include "cucumber_cpp/library/support/DefinitionRegistration.hpp" #include "cucumber_cpp/library/util/Duration.hpp" @@ -16,4 +17,21 @@ extern "C" CCR_EXPORT void ccr_register(cucumber_cpp::library::plugin::PluginHos if (context->timestampGenerator != nullptr) cucumber_cpp::library::util::TimestampGenerator::SetInstance(*static_cast(context->timestampGenerator)); + + if (context->converterMap != nullptr) + { + auto* hostMap = static_cast(context->converterMap); + + // Migrate any converters this plugin already registered into its + // local map (built-ins constructed during plugin startup) into the + // host-owned map, without overwriting existing host entries. + auto& local = cucumber_cpp::library::cucumber_expression::ConverterRegistry::LocalInstance(); + for (auto& [name, fn] : local) + hostMap->try_emplace(name, std::move(fn)); + local.clear(); + + // Redirect all future registrations and lookups in this plugin to the + // host-owned converter map so host and plugin share the same storage. + cucumber_cpp::library::cucumber_expression::ConverterRegistry::SetInstance(hostMap); + } } diff --git a/cucumber_cpp/library/runtime/NestedTestCaseRunner.cpp b/cucumber_cpp/library/runtime/NestedTestCaseRunner.cpp index 924ea075..c2af5b98 100644 --- a/cucumber_cpp/library/runtime/NestedTestCaseRunner.cpp +++ b/cucumber_cpp/library/runtime/NestedTestCaseRunner.cpp @@ -107,7 +107,7 @@ namespace cucumber_cpp::library::runtime { const auto& definition = stepDefinitions.front(); NestedTestCaseRunner nestedTestCaseRunner{ nesting, supportCodeLibrary, broadcaster, testCaseContext, testStepStarted }; - const util::BodyFactory bodyFactory = [&nestedTestCaseRunner, &definition, &broadcaster, &testCaseContext, &testStepStarted, &dataTable, &docString] + const util::BodyFactory bodyFactory = [&nestedTestCaseRunner, &definition, &broadcaster, &testCaseContext, &testStepStarted, &dataTable, &docString](util::TestStepResult& testStepResult) { return definition.factory(nestedTestCaseRunner, broadcaster, testCaseContext, testStepStarted, util::TransformTable(dataTable), util::TransformDocString(docString)); }; diff --git a/cucumber_cpp/library/util/Body.cpp b/cucumber_cpp/library/util/Body.cpp index 943dd8c4..f669280e 100644 --- a/cucumber_cpp/library/util/Body.cpp +++ b/cucumber_cpp/library/util/Body.cpp @@ -4,7 +4,6 @@ #include "cucumber_cpp/library/util/TestStepResult.hpp" #include "cucumber_cpp/library/util/TestStepResultStatus.hpp" #include "fmt/format.h" -#include "gtest/gtest-spi.h" #include "gtest/gtest.h" #include #include @@ -13,49 +12,38 @@ namespace cucumber_cpp::library::util { - namespace + CucumberResultReporter::CucumberResultReporter(util::TestStepResult& testStepResult) + : testing::ScopedFakeTestPartResultReporter{ nullptr } + , testStepResult{ testStepResult } { + } - struct CucumberResultReporter : public testing::ScopedFakeTestPartResultReporter + void CucumberResultReporter::ReportTestPartResult(const testing::TestPartResult& testPartResult) + { + if (testPartResult.failed()) { - explicit CucumberResultReporter(util::TestStepResult& testStepResult) - : testing::ScopedFakeTestPartResultReporter{ nullptr } - , testStepResult{ testStepResult } - { - } - - void ReportTestPartResult(const testing::TestPartResult& testPartResult) override - { - if (testPartResult.failed()) - { - testStepResult.status = util::TestStepResultStatus::FAILED; - - auto fileName = std::filesystem::relative(testPartResult.file_name(), std::filesystem::current_path()).string(); + testStepResult.status = util::TestStepResultStatus::FAILED; - if (testStepResult.message) - testStepResult.message = fmt::format("{}\n{}:{}: Failure\n{}", testStepResult.message.value(), fileName, testPartResult.line_number(), testPartResult.message()); - else - testStepResult.message = fmt::format("{}:{}: Failure\n{}", fileName, testPartResult.line_number(), testPartResult.message()); - } + auto fileName = std::filesystem::relative(testPartResult.file_name(), std::filesystem::current_path()).string(); - if (testPartResult.fatally_failed()) - throw FatalError{ testPartResult.message() }; - } + if (testStepResult.message) + testStepResult.message = fmt::format("{}\n{}:{}: Failure\n{}", testStepResult.message.value(), fileName, testPartResult.line_number(), testPartResult.message()); + else + testStepResult.message = fmt::format("{}:{}: Failure\n{}", fileName, testPartResult.line_number(), testPartResult.message()); + } - private: - util::TestStepResult& testStepResult; - }; + if (testPartResult.fatally_failed()) + throw FatalError{ testPartResult.message() }; } - TestStepResult ConstructAndExecute(const std::function()>& bodyFactory, const ExecuteArgs& args) + TestStepResult ConstructAndExecute(const BodyFactory& bodyFactory, const ExecuteArgs& args) { const auto startTime = Stopwatch::Instance().Start(); TestStepResult testStepResult{ .status = TestStepResultStatus::PASSED }; - CucumberResultReporter reportListener{ testStepResult }; try { - bodyFactory()->Execute(args); + bodyFactory(testStepResult)->Execute(args); } catch (...) { @@ -71,4 +59,9 @@ namespace cucumber_cpp::library::util return testStepResult; } + + + Body::Body(TestStepResult& testStepResult) + : reportListener{ testStepResult } + {} } diff --git a/cucumber_cpp/library/util/Body.hpp b/cucumber_cpp/library/util/Body.hpp index 130ba47a..a7fcfeee 100644 --- a/cucumber_cpp/library/util/Body.hpp +++ b/cucumber_cpp/library/util/Body.hpp @@ -3,6 +3,7 @@ #include "cucumber_cpp/library/cucumber_expression/ParameterRegistry.hpp" #include "cucumber_cpp/library/util/TestStepResult.hpp" +#include "gtest/gtest-spi.h" #include #include #include @@ -14,6 +15,15 @@ namespace cucumber_cpp::library::util { + struct CucumberResultReporter : public testing::ScopedFakeTestPartResultReporter + { + explicit CucumberResultReporter(util::TestStepResult& testStepResult); + + void ReportTestPartResult(const testing::TestPartResult& testPartResult) override; + + private: + util::TestStepResult& testStepResult; + }; struct FatalError : std::runtime_error { @@ -54,17 +64,20 @@ namespace cucumber_cpp::library::util struct Body; - using BodyFactory = std::function()>; + using BodyFactory = std::function(TestStepResult&)>; TestStepResult ConstructAndExecute(const BodyFactory& bodyFactory, const ExecuteArgs& args = {}); struct Body { + explicit Body(TestStepResult& testStepResult); virtual ~Body() = default; private: virtual void Execute(const ExecuteArgs& args) = 0; friend TestStepResult ConstructAndExecute(const BodyFactory& bodyFactory, const ExecuteArgs& args); + + CucumberResultReporter reportListener; }; } From 971e3af355bea934ad52b4fdc6945cc3ca95b784 Mon Sep 17 00:00:00 2001 From: "Timmer, Daan" Date: Fri, 24 Jul 2026 23:43:29 +0000 Subject: [PATCH 23/29] execute the CucumberResultReporter in the body of a step/hook --- cucumber_cpp/library/BodyMacro.hpp | 48 +++++++++++-------- .../library/runtime/NestedTestCaseRunner.cpp | 2 +- .../library/runtime/TestCaseRunner.cpp | 13 ++--- cucumber_cpp/library/runtime/Worker.cpp | 4 +- cucumber_cpp/library/util/HookFactory.hpp | 7 +-- cucumber_cpp/library/util/StepFactory.hpp | 7 +-- 6 files changed, 47 insertions(+), 34 deletions(-) diff --git a/cucumber_cpp/library/BodyMacro.hpp b/cucumber_cpp/library/BodyMacro.hpp index 36b1e2f3..c345d433 100644 --- a/cucumber_cpp/library/BodyMacro.hpp +++ b/cucumber_cpp/library/BodyMacro.hpp @@ -5,6 +5,7 @@ #include "cucumber_cpp/library/cucumber_expression/ParameterRegistry.hpp" #include "cucumber_cpp/library/support/Body.hpp" #include "cucumber_cpp/library/util/Body.hpp" +#include "cucumber_cpp/library/util/TestStepResult.hpp" #include "gtest/gtest.h" #include #include @@ -23,7 +24,11 @@ namespace cucumber_cpp::library::detail struct BodyCrtp : util::Body { private: - BodyCrtp() = default; + explicit BodyCrtp(util::TestStepResult& testStepResult) + : util::Body{ testStepResult } + { + } + friend Base; public: @@ -57,24 +62,29 @@ namespace cucumber_cpp::library::detail #define BODY_STRUCT CONCAT(BodyImpl, __LINE__) -#define BODY(matcher, type, targs, registration, base) \ - namespace \ - { \ - struct BODY_STRUCT : cucumber_cpp::library::detail::BodyCrtp \ - , base \ - { \ - /* Workaround namespaces in `base`. For example `base` = Foo::Bar. */ \ - /* Then the result would be Foo::Bar::Foo::Bar which is invalid */ \ - using myBase = base; \ - using myBase::myBase; \ - \ - private: \ - friend BodyCrtp; \ - void ExecuteImpl targs; \ - static const std::size_t ID; \ - }; \ - } \ - const std::size_t BODY_STRUCT::ID = registration(matcher, type); \ +#define BODY(matcher, type, targs, registration, base) \ + namespace \ + { \ + struct BODY_STRUCT : cucumber_cpp::library::detail::BodyCrtp \ + , base \ + { \ + /* Workaround namespaces in `base`. For example `base` = Foo::Bar. */ \ + /* Then the result would be Foo::Bar::Foo::Bar which is invalid */ \ + using myBase = base; \ + using myBase::myBase; \ + template \ + BODY_STRUCT(cucumber_cpp::library::util::TestStepResult& testStepResult, CTArgs&&... args) \ + : BodyCrtp{ testStepResult } \ + , myBase{ std::forward(args)... } \ + {} \ + \ + private: \ + friend BodyCrtp; \ + void ExecuteImpl targs; \ + static const std::size_t ID; \ + }; \ + } \ + const std::size_t BODY_STRUCT::ID = registration(matcher, type); \ void BODY_STRUCT::ExecuteImpl targs #endif diff --git a/cucumber_cpp/library/runtime/NestedTestCaseRunner.cpp b/cucumber_cpp/library/runtime/NestedTestCaseRunner.cpp index c2af5b98..5aba3bbe 100644 --- a/cucumber_cpp/library/runtime/NestedTestCaseRunner.cpp +++ b/cucumber_cpp/library/runtime/NestedTestCaseRunner.cpp @@ -109,7 +109,7 @@ namespace cucumber_cpp::library::runtime NestedTestCaseRunner nestedTestCaseRunner{ nesting, supportCodeLibrary, broadcaster, testCaseContext, testStepStarted }; const util::BodyFactory bodyFactory = [&nestedTestCaseRunner, &definition, &broadcaster, &testCaseContext, &testStepStarted, &dataTable, &docString](util::TestStepResult& testStepResult) { - return definition.factory(nestedTestCaseRunner, broadcaster, testCaseContext, testStepStarted, util::TransformTable(dataTable), util::TransformDocString(docString)); + return definition.factory(testStepResult, nestedTestCaseRunner, broadcaster, testCaseContext, testStepStarted, util::TransformTable(dataTable), util::TransformDocString(docString)); }; Invoke(nesting, step, bodyFactory, testStep.step_match_arguments_lists->front()); } diff --git a/cucumber_cpp/library/runtime/TestCaseRunner.cpp b/cucumber_cpp/library/runtime/TestCaseRunner.cpp index f7756ac5..e9201c2e 100644 --- a/cucumber_cpp/library/runtime/TestCaseRunner.cpp +++ b/cucumber_cpp/library/runtime/TestCaseRunner.cpp @@ -27,6 +27,7 @@ #include "cucumber_cpp/library/util/GetWorstTestStepResult.hpp" #include "cucumber_cpp/library/util/HookData.hpp" #include "cucumber_cpp/library/util/ScenarioInfo.hpp" +#include "cucumber_cpp/library/util/TestStepResult.hpp" #include "cucumber_cpp/library/util/Timestamp.hpp" #include "cucumber_cpp/library/util/TransformDocString.hpp" #include "cucumber_cpp/library/util/TransformPickleTag.hpp" @@ -165,9 +166,9 @@ namespace cucumber_cpp::library::runtime .status = cucumber::messages::test_step_result_status::SKIPPED, }; - const util::BodyFactory bodyFactory = [&hookDefinition, this, &testCaseContext, &testStepStarted, hasError] + const util::BodyFactory bodyFactory = [&hookDefinition, this, &testCaseContext, &testStepStarted, hasError](util::TestStepResult& testStepResult) { - return hookDefinition.factory(broadcaster, testCaseContext, util::TransformTestStepStarted(testStepStarted), MakeScenarioInfo(pickle), hasError); + return hookDefinition.factory(testStepResult, broadcaster, testCaseContext, util::TransformTestStepStarted(testStepStarted), MakeScenarioInfo(pickle), hasError); }; return InvokeStep(bodyFactory, {}); @@ -182,9 +183,9 @@ namespace cucumber_cpp::library::runtime for (const auto& id : ids) { const auto& definition = supportCodeLibrary.hookRegistry.GetDefinitionById(id); - const auto bodyFactory = [&definition, this, &testCaseContext, &testStepStarted] + const auto bodyFactory = [&definition, this, &testCaseContext, &testStepStarted](util::TestStepResult& testStepResult) { - return definition.factory(broadcaster, testCaseContext, util::TransformTestStepStarted(testStepStarted), MakeScenarioInfo(pickle), false); + return definition.factory(testStepResult, broadcaster, testCaseContext, util::TransformTestStepStarted(testStepStarted), MakeScenarioInfo(pickle), false); }; results.emplace_back(InvokeStep(bodyFactory, {})); @@ -240,9 +241,9 @@ namespace cucumber_cpp::library::runtime NestedTestCaseRunner nestedTestCaseRunner{ 0, supportCodeLibrary, broadcaster, testCaseContext, util::TransformTestStepStarted(testStepStarted) }; - const util::BodyFactory bodyFactory = [this, &definition, &nestedTestCaseRunner, &testCaseContext, &testStepStarted, &dataTable, &docString] + const util::BodyFactory bodyFactory = [this, &definition, &nestedTestCaseRunner, &testCaseContext, &testStepStarted, &dataTable, &docString](util::TestStepResult& testStepResult) { - return definition.factory(nestedTestCaseRunner, broadcaster, testCaseContext, util::TransformTestStepStarted(testStepStarted), util::TransformTable(dataTable), util::TransformDocString(docString)); + return definition.factory(testStepResult, nestedTestCaseRunner, broadcaster, testCaseContext, util::TransformTestStepStarted(testStepStarted), util::TransformTable(dataTable), util::TransformDocString(docString)); }; stepResults.push_back(InvokeStep(bodyFactory, testStep.step_match_arguments_lists->front())); diff --git a/cucumber_cpp/library/runtime/Worker.cpp b/cucumber_cpp/library/runtime/Worker.cpp index b1a6e999..0d871b3c 100644 --- a/cucumber_cpp/library/runtime/Worker.cpp +++ b/cucumber_cpp/library/runtime/Worker.cpp @@ -193,9 +193,9 @@ namespace cucumber_cpp::library::runtime if (!options.dryRun) { - const util::BodyFactory bodyFactory = [&definition, this, &context, &testRunHookStarted] + const util::BodyFactory bodyFactory = [&definition, this, &context, &testRunHookStarted](util::TestStepResult& testStepResult) { - return definition.factory(broadcaster, context, util::TransformTestRunHookStarted(testRunHookStarted), std::nullopt, false); + return definition.factory(testStepResult, broadcaster, context, util::TransformTestRunHookStarted(testRunHookStarted), std::nullopt, false); }; result = util::TransformTestStepResult(util::ConstructAndExecute(bodyFactory)); diff --git a/cucumber_cpp/library/util/HookFactory.hpp b/cucumber_cpp/library/util/HookFactory.hpp index 3663c897..7074e864 100644 --- a/cucumber_cpp/library/util/HookFactory.hpp +++ b/cucumber_cpp/library/util/HookFactory.hpp @@ -6,18 +6,19 @@ #include "cucumber_cpp/library/util/Broadcaster.hpp" #include "cucumber_cpp/library/util/ScenarioInfo.hpp" #include "cucumber_cpp/library/util/StepOrHookStarted.hpp" +#include "cucumber_cpp/library/util/TestStepResult.hpp" #include #include #include namespace cucumber_cpp::library::util { - using HookFactory = std::unique_ptr (&)(Broadcaster& broadCaster, Context& context, StepOrHookStarted stepOrHookStarted, std::optional scenarioInfo, bool hasError); + using HookFactory = std::unique_ptr (&)(util::TestStepResult& testStepResult, Broadcaster& broadCaster, Context& context, StepOrHookStarted stepOrHookStarted, std::optional scenarioInfo, bool hasError); template - std::unique_ptr HookBodyFactory(Broadcaster& broadCaster, Context& context, StepOrHookStarted stepOrHookStarted, std::optional scenarioInfo, bool hasError) + std::unique_ptr HookBodyFactory(util::TestStepResult& testStepResult, Broadcaster& broadCaster, Context& context, StepOrHookStarted stepOrHookStarted, std::optional scenarioInfo, bool hasError) { - return std::make_unique(broadCaster, context, std::move(stepOrHookStarted), std::move(scenarioInfo), hasError); + return std::make_unique(testStepResult, broadCaster, context, std::move(stepOrHookStarted), std::move(scenarioInfo), hasError); } } diff --git a/cucumber_cpp/library/util/StepFactory.hpp b/cucumber_cpp/library/util/StepFactory.hpp index 29c87204..81819bec 100644 --- a/cucumber_cpp/library/util/StepFactory.hpp +++ b/cucumber_cpp/library/util/StepFactory.hpp @@ -7,6 +7,7 @@ #include "cucumber_cpp/library/util/DocString.hpp" #include "cucumber_cpp/library/util/StepOrHookStarted.hpp" #include "cucumber_cpp/library/util/Table.hpp" +#include "cucumber_cpp/library/util/TestStepResult.hpp" #include #include @@ -17,12 +18,12 @@ namespace cucumber_cpp::library::runtime namespace cucumber_cpp::library::util { - using StepFactory = std::unique_ptr (&)(const runtime::NestedTestCaseRunner&, Broadcaster& broadCaster, Context&, StepOrHookStarted stepOrHookStarted, const std::optional&, const std::optional&); + using StepFactory = std::unique_ptr (&)(util::TestStepResult& testStepResult, const runtime::NestedTestCaseRunner& nestedTestCaseRunner, Broadcaster& broadCaster, Context&, StepOrHookStarted stepOrHookStarted, const std::optional
&, const std::optional&); template - std::unique_ptr StepBodyFactory(const runtime::NestedTestCaseRunner& nestedTestCaseRunner, Broadcaster& broadCaster, Context& context, StepOrHookStarted stepOrHookStarted, const std::optional
& dataTable, const std::optional& docString) + std::unique_ptr StepBodyFactory(util::TestStepResult& testStepResult, const runtime::NestedTestCaseRunner& nestedTestCaseRunner, Broadcaster& broadCaster, Context& context, StepOrHookStarted stepOrHookStarted, const std::optional
& dataTable, const std::optional& docString) { - return std::make_unique(nestedTestCaseRunner, broadCaster, context, stepOrHookStarted, dataTable, docString); + return std::make_unique(testStepResult, nestedTestCaseRunner, broadCaster, context, stepOrHookStarted, dataTable, docString); } } #endif From 9c46922b3d7d56f8cc93b4181f238232c6774992 Mon Sep 17 00:00:00 2001 From: "Timmer, Daan" Date: Sat, 25 Jul 2026 00:05:50 +0000 Subject: [PATCH 24/29] delete actual_run from examples-tables --- .../examples-tables/actual_run.ndjson | 18 ------------------ 1 file changed, 18 deletions(-) delete mode 100644 compatibility/examples-tables/actual_run.ndjson diff --git a/compatibility/examples-tables/actual_run.ndjson b/compatibility/examples-tables/actual_run.ndjson deleted file mode 100644 index cda4b3c8..00000000 --- a/compatibility/examples-tables/actual_run.ndjson +++ /dev/null @@ -1,18 +0,0 @@ -{"source":{"data":"Feature: Examples Tables\n Sometimes it can be desirable to run the same scenario multiple times with\n different data each time - this can be done by placing an Examples table\n underneath a Scenario, and use in the Scenario which match the\n table headers.\n\n The Scenario Outline name can also be parameterized. The name of the resulting\n pickle will have the replaced with the value from the examples\n table.\n\n Scenario Outline: Eating cucumbers\n Given there are cucumbers\n When I eat cucumbers\n Then I should have cucumbers\n\n @passing\n Examples: These are passing\n | start | eat | left |\n | 12 | 5 | 7 |\n | 20 | 5 | 15 |\n\n @failing\n Examples: These are failing\n | start | eat | left |\n | 12 | 20 | 0 |\n | 0 | 1 | 0 |\n\n Scenario Outline: Eating cucumbers with friends\n Given there are friends\n And there are cucumbers\n Then each person can eat cucumbers\n\n Examples:\n | friends | start | share |\n | 11 | 12 | 1 |\n | 1 | 4 | 2 |\n | 0 | 4 | 4 |\n","mediaType":"text/x.cucumber.gherkin+plain","uri":"D:/git/amp-cucumber-cpp-runner/compatibility\\examples-tables\\examples-tables.feature"}} -{"gherkinDocument":{"comments":[],"feature":{"children":[{"scenario":{"description":"","examples":[{"description":"","id":"7","keyword":"Examples","location":{"column":5,"line":17},"name":"These are passing","tableBody":[{"cells":[{"location":{"column":12,"line":19},"value":"12"},{"location":{"column":19,"line":19},"value":"5"},{"location":{"column":26,"line":19},"value":"7"}],"id":"4","location":{"column":7,"line":19}},{"cells":[{"location":{"column":12,"line":20},"value":"20"},{"location":{"column":19,"line":20},"value":"5"},{"location":{"column":25,"line":20},"value":"15"}],"id":"5","location":{"column":7,"line":20}}],"tableHeader":{"cells":[{"location":{"column":9,"line":18},"value":"start"},{"location":{"column":17,"line":18},"value":"eat"},{"location":{"column":23,"line":18},"value":"left"}],"id":"3","location":{"column":7,"line":18}},"tags":[{"id":"6","location":{"column":5,"line":16},"name":"@passing"}]},{"description":"","id":"12","keyword":"Examples","location":{"column":5,"line":23},"name":"These are failing","tableBody":[{"cells":[{"location":{"column":12,"line":25},"value":"12"},{"location":{"column":18,"line":25},"value":"20"},{"location":{"column":26,"line":25},"value":"0"}],"id":"9","location":{"column":7,"line":25}},{"cells":[{"location":{"column":13,"line":26},"value":"0"},{"location":{"column":19,"line":26},"value":"1"},{"location":{"column":26,"line":26},"value":"0"}],"id":"10","location":{"column":7,"line":26}}],"tableHeader":{"cells":[{"location":{"column":9,"line":24},"value":"start"},{"location":{"column":17,"line":24},"value":"eat"},{"location":{"column":23,"line":24},"value":"left"}],"id":"8","location":{"column":7,"line":24}},"tags":[{"id":"11","location":{"column":5,"line":22},"name":"@failing"}]}],"id":"13","keyword":"Scenario Outline","location":{"column":3,"line":11},"name":"Eating cucumbers","steps":[{"id":"0","keyword":"Given ","keywordType":"Context","location":{"column":5,"line":12},"text":"there are cucumbers"},{"id":"1","keyword":"When ","keywordType":"Action","location":{"column":5,"line":13},"text":"I eat cucumbers"},{"id":"2","keyword":"Then ","keywordType":"Outcome","location":{"column":5,"line":14},"text":"I should have cucumbers"}],"tags":[]}},{"scenario":{"description":"","examples":[{"description":"","id":"21","keyword":"Examples","location":{"column":5,"line":33},"name":"","tableBody":[{"cells":[{"location":{"column":14,"line":35},"value":"11"},{"location":{"column":22,"line":35},"value":"12"},{"location":{"column":31,"line":35},"value":"1"}],"id":"18","location":{"column":7,"line":35}},{"cells":[{"location":{"column":15,"line":36},"value":"1"},{"location":{"column":23,"line":36},"value":"4"},{"location":{"column":31,"line":36},"value":"2"}],"id":"19","location":{"column":7,"line":36}},{"cells":[{"location":{"column":15,"line":37},"value":"0"},{"location":{"column":23,"line":37},"value":"4"},{"location":{"column":31,"line":37},"value":"4"}],"id":"20","location":{"column":7,"line":37}}],"tableHeader":{"cells":[{"location":{"column":9,"line":34},"value":"friends"},{"location":{"column":19,"line":34},"value":"start"},{"location":{"column":27,"line":34},"value":"share"}],"id":"17","location":{"column":7,"line":34}},"tags":[]}],"id":"22","keyword":"Scenario Outline","location":{"column":3,"line":28},"name":"Eating cucumbers with friends","steps":[{"id":"14","keyword":"Given ","keywordType":"Context","location":{"column":5,"line":29},"text":"there are friends"},{"id":"15","keyword":"And ","keywordType":"Conjunction","location":{"column":5,"line":30},"text":"there are cucumbers"},{"id":"16","keyword":"Then ","keywordType":"Outcome","location":{"column":5,"line":31},"text":"each person can eat cucumbers"}],"tags":[]}}],"description":" Sometimes it can be desirable to run the same scenario multiple times with\n different data each time - this can be done by placing an Examples table\n underneath a Scenario, and use in the Scenario which match the\n table headers.\n\n The Scenario Outline name can also be parameterized. The name of the resulting\n pickle will have the replaced with the value from the examples\n table.","keyword":"Feature","language":"en","location":{"column":1,"line":1},"name":"Examples Tables","tags":[]},"uri":"D:/git/amp-cucumber-cpp-runner/compatibility\\examples-tables\\examples-tables.feature"}} -{"pickle":{"astNodeIds":["13","4"],"id":"26","language":"en","location":{"column":7,"line":19},"name":"Eating cucumbers","steps":[{"astNodeIds":["0","4"],"id":"23","text":"there are 12 cucumbers","type":"Context"},{"astNodeIds":["1","4"],"id":"24","text":"I eat 5 cucumbers","type":"Action"},{"astNodeIds":["2","4"],"id":"25","text":"I should have 7 cucumbers","type":"Outcome"}],"tags":[{"astNodeId":"6","name":"@passing"}],"uri":"D:/git/amp-cucumber-cpp-runner/compatibility\\examples-tables\\examples-tables.feature"}} -{"pickle":{"astNodeIds":["13","5"],"id":"30","language":"en","location":{"column":7,"line":20},"name":"Eating cucumbers","steps":[{"astNodeIds":["0","5"],"id":"27","text":"there are 20 cucumbers","type":"Context"},{"astNodeIds":["1","5"],"id":"28","text":"I eat 5 cucumbers","type":"Action"},{"astNodeIds":["2","5"],"id":"29","text":"I should have 15 cucumbers","type":"Outcome"}],"tags":[{"astNodeId":"6","name":"@passing"}],"uri":"D:/git/amp-cucumber-cpp-runner/compatibility\\examples-tables\\examples-tables.feature"}} -{"pickle":{"astNodeIds":["13","9"],"id":"34","language":"en","location":{"column":7,"line":25},"name":"Eating cucumbers","steps":[{"astNodeIds":["0","9"],"id":"31","text":"there are 12 cucumbers","type":"Context"},{"astNodeIds":["1","9"],"id":"32","text":"I eat 20 cucumbers","type":"Action"},{"astNodeIds":["2","9"],"id":"33","text":"I should have 0 cucumbers","type":"Outcome"}],"tags":[{"astNodeId":"11","name":"@failing"}],"uri":"D:/git/amp-cucumber-cpp-runner/compatibility\\examples-tables\\examples-tables.feature"}} -{"pickle":{"astNodeIds":["13","10"],"id":"38","language":"en","location":{"column":7,"line":26},"name":"Eating cucumbers","steps":[{"astNodeIds":["0","10"],"id":"35","text":"there are 0 cucumbers","type":"Context"},{"astNodeIds":["1","10"],"id":"36","text":"I eat 1 cucumbers","type":"Action"},{"astNodeIds":["2","10"],"id":"37","text":"I should have 0 cucumbers","type":"Outcome"}],"tags":[{"astNodeId":"11","name":"@failing"}],"uri":"D:/git/amp-cucumber-cpp-runner/compatibility\\examples-tables\\examples-tables.feature"}} -{"pickle":{"astNodeIds":["22","18"],"id":"42","language":"en","location":{"column":7,"line":35},"name":"Eating cucumbers with 11 friends","steps":[{"astNodeIds":["14","18"],"id":"39","text":"there are 11 friends","type":"Context"},{"astNodeIds":["15","18"],"id":"40","text":"there are 12 cucumbers","type":"Context"},{"astNodeIds":["16","18"],"id":"41","text":"each person can eat 1 cucumbers","type":"Outcome"}],"tags":[],"uri":"D:/git/amp-cucumber-cpp-runner/compatibility\\examples-tables\\examples-tables.feature"}} -{"pickle":{"astNodeIds":["22","19"],"id":"46","language":"en","location":{"column":7,"line":36},"name":"Eating cucumbers with 1 friends","steps":[{"astNodeIds":["14","19"],"id":"43","text":"there are 1 friends","type":"Context"},{"astNodeIds":["15","19"],"id":"44","text":"there are 4 cucumbers","type":"Context"},{"astNodeIds":["16","19"],"id":"45","text":"each person can eat 2 cucumbers","type":"Outcome"}],"tags":[],"uri":"D:/git/amp-cucumber-cpp-runner/compatibility\\examples-tables\\examples-tables.feature"}} -{"pickle":{"astNodeIds":["22","20"],"id":"50","language":"en","location":{"column":7,"line":37},"name":"Eating cucumbers with 0 friends","steps":[{"astNodeIds":["14","20"],"id":"47","text":"there are 0 friends","type":"Context"},{"astNodeIds":["15","20"],"id":"48","text":"there are 4 cucumbers","type":"Context"},{"astNodeIds":["16","20"],"id":"49","text":"each person can eat 4 cucumbers","type":"Outcome"}],"tags":[],"uri":"D:/git/amp-cucumber-cpp-runner/compatibility\\examples-tables\\examples-tables.feature"}} -{"stepDefinition":{"id":"51","pattern":{"source":"there are {int} cucumbers","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":7},"uri":"D:\\git\\amp-cucumber-cpp-runner\\compatibility\\examples-tables\\examples-tables.cpp"}}} -{"stepDefinition":{"id":"52","pattern":{"source":"there are {int} friends","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":12},"uri":"D:\\git\\amp-cucumber-cpp-runner\\compatibility\\examples-tables\\examples-tables.cpp"}}} -{"stepDefinition":{"id":"53","pattern":{"source":"I eat {int} cucumbers","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":17},"uri":"D:\\git\\amp-cucumber-cpp-runner\\compatibility\\examples-tables\\examples-tables.cpp"}}} -{"stepDefinition":{"id":"54","pattern":{"source":"I should have {int} cucumbers","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":22},"uri":"D:\\git\\amp-cucumber-cpp-runner\\compatibility\\examples-tables\\examples-tables.cpp"}}} -{"stepDefinition":{"id":"55","pattern":{"source":"each person can eat {int} cucumbers","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"location":{"line":27},"uri":"D:\\git\\amp-cucumber-cpp-runner\\compatibility\\examples-tables\\examples-tables.cpp"}}} -{"testRunStarted":{"id":"56","timestamp":{"nanos":0,"seconds":0}}} -{"testCase":{"id":"57","pickleId":"26","testRunStartedId":"56","testSteps":[{"id":"58","pickleStepId":"23","stepDefinitionIds":["51"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":10,"value":"12"},"parameterTypeName":"int"}]}]},{"id":"59","pickleStepId":"24","stepDefinitionIds":["53"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":6,"value":"5"},"parameterTypeName":"int"}]}]},{"id":"60","pickleStepId":"25","stepDefinitionIds":["54"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":14,"value":"7"},"parameterTypeName":"int"}]}]}]}} -{"testCase":{"id":"61","pickleId":"30","testRunStartedId":"56","testSteps":[{"id":"62","pickleStepId":"27","stepDefinitionIds":["51"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":10,"value":"20"},"parameterTypeName":"int"}]}]},{"id":"63","pickleStepId":"28","stepDefinitionIds":["53"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":6,"value":"5"},"parameterTypeName":"int"}]}]},{"id":"64","pickleStepId":"29","stepDefinitionIds":["54"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":14,"value":"15"},"parameterTypeName":"int"}]}]}]}} -{"testCase":{"id":"65","pickleId":"34","testRunStartedId":"56","testSteps":[{"id":"66","pickleStepId":"31","stepDefinitionIds":["51"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":10,"value":"12"},"param \ No newline at end of file From 43c02ebd8256a1df66ace564ae04912cf7ebb5eb Mon Sep 17 00:00:00 2001 From: "Timmer, Daan" Date: Sat, 25 Jul 2026 00:06:41 +0000 Subject: [PATCH 25/29] Refactor converter map handling in PluginRegister to avoid unnecessary migration on Linux platforms --- .../library/plugin/PluginRegister.cpp | 27 ++++++++++++------- cucumber_cpp/library/util/Body.hpp | 1 + 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/cucumber_cpp/library/plugin/PluginRegister.cpp b/cucumber_cpp/library/plugin/PluginRegister.cpp index 7885bafa..7bb52479 100644 --- a/cucumber_cpp/library/plugin/PluginRegister.cpp +++ b/cucumber_cpp/library/plugin/PluginRegister.cpp @@ -21,17 +21,24 @@ extern "C" CCR_EXPORT void ccr_register(cucumber_cpp::library::plugin::PluginHos if (context->converterMap != nullptr) { auto* hostMap = static_cast(context->converterMap); - - // Migrate any converters this plugin already registered into its - // local map (built-ins constructed during plugin startup) into the - // host-owned map, without overwriting existing host entries. auto& local = cucumber_cpp::library::cucumber_expression::ConverterRegistry::LocalInstance(); - for (auto& [name, fn] : local) - hostMap->try_emplace(name, std::move(fn)); - local.clear(); - // Redirect all future registrations and lookups in this plugin to the - // host-owned converter map so host and plugin share the same storage. - cucumber_cpp::library::cucumber_expression::ConverterRegistry::SetInstance(hostMap); + // On Linux (ELF unique symbols), host and plugin share the same + // static map, so migration is unnecessary and clearing would + // destroy the host's converters. Only migrate on platforms where + // they are separate (e.g. Windows DLLs). + if (&local != hostMap) + { + // Migrate any converters this plugin already registered into its + // local map (built-ins constructed during plugin startup) into the + // host-owned map, without overwriting existing host entries. + for (auto& [name, fn] : local) + hostMap->try_emplace(name, std::move(fn)); + local.clear(); + + // Redirect all future registrations and lookups in this plugin to the + // host-owned converter map so host and plugin share the same storage. + cucumber_cpp::library::cucumber_expression::ConverterRegistry::SetInstance(hostMap); + } } } diff --git a/cucumber_cpp/library/util/Body.hpp b/cucumber_cpp/library/util/Body.hpp index a7fcfeee..868938c5 100644 --- a/cucumber_cpp/library/util/Body.hpp +++ b/cucumber_cpp/library/util/Body.hpp @@ -4,6 +4,7 @@ #include "cucumber_cpp/library/cucumber_expression/ParameterRegistry.hpp" #include "cucumber_cpp/library/util/TestStepResult.hpp" #include "gtest/gtest-spi.h" +#include "gtest/gtest.h" #include #include #include From fe4a0d8cf802f9812b6229d5c330f0c4427adae9 Mon Sep 17 00:00:00 2001 From: "Timmer, Daan" Date: Sat, 25 Jul 2026 22:20:42 +0000 Subject: [PATCH 26/29] Add plugin support with ABI versioning and snapshot management - Introduced PluginHostContext struct with ABI version and size. - Implemented snapshot and restore functionality for converter registry. - Updated plugin registration to check ABI version for compatibility. - Enhanced dynamic library management with improved error reporting. - Refactored CMake configuration for plugin linking. - Added build directory handling in compatibility checks. --- .vscode/settings.json | 2 +- compatibility/BaseCompatibility.cpp | 3 +- compatibility/BaseCompatibility.hpp | 1 + compatibility/CMakeLists.txt | 28 ++------ compatibility/TestCompatibility.cpp | 2 + cucumber_cpp/acceptance_test/CMakeLists.txt | 1 + .../plugin_test/CMakeLists.txt | 36 ++++++++++ .../plugin_test/MainPlugin.cpp | 63 ++++++++++++++++ .../acceptance_test/plugin_test/PluginA.cpp | 39 ++++++++++ .../acceptance_test/plugin_test/PluginB.cpp | 25 +++++++ .../plugin_test/StaticSteps.cpp | 18 +++++ .../plugin_test/features/test_plugins.feature | 35 +++++++++ cucumber_cpp/acceptance_test/test.bats | 10 ++- cucumber_cpp/library/Application.cpp | 3 +- .../cucumber_expression/ParameterRegistry.hpp | 34 ++++++--- .../library/plugin/DynamicLibrary.cpp | 14 +++- .../library/plugin/DynamicLibraryManager.cpp | 2 + .../library/plugin/PLUGIN_ARCHITECTURE.md | 71 ++++++++++++++++++- cucumber_cpp/library/plugin/PluginExport.hpp | 10 ++- .../library/plugin/PluginRegister.cpp | 3 + .../support/DefinitionRegistration.cpp | 4 ++ cucumber_cpp/library/util/Body.cpp | 16 ++++- cucumber_cpp/library/util/Body.hpp | 15 +--- cucumber_cpp/library/util/Duration.cpp | 6 ++ cucumber_cpp/library/util/Duration.hpp | 2 +- cucumber_cpp/library/util/Timestamp.cpp | 3 +- 26 files changed, 387 insertions(+), 59 deletions(-) create mode 100644 cucumber_cpp/acceptance_test/plugin_test/CMakeLists.txt create mode 100644 cucumber_cpp/acceptance_test/plugin_test/MainPlugin.cpp create mode 100644 cucumber_cpp/acceptance_test/plugin_test/PluginA.cpp create mode 100644 cucumber_cpp/acceptance_test/plugin_test/PluginB.cpp create mode 100644 cucumber_cpp/acceptance_test/plugin_test/StaticSteps.cpp create mode 100644 cucumber_cpp/acceptance_test/plugin_test/features/test_plugins.feature diff --git a/.vscode/settings.json b/.vscode/settings.json index 785ed2f5..fdff978e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,7 +3,7 @@ "cucumberautocomplete.steps": [ "compatibility/*/*.cpp", "cucumber_cpp/example/*/*.cpp", - "cucumber_cpp/acceptance_test/steps/*.cpp" + "cucumber_cpp/acceptance_test/*/*.cpp" ], "cucumberautocomplete.gherkinDefinitionPart": "(GIVEN|WHEN|THEN|STEP)\\(", "cucumberautocomplete.customParameters": [ diff --git a/compatibility/BaseCompatibility.cpp b/compatibility/BaseCompatibility.cpp index e2223a8e..2c89dcc5 100644 --- a/compatibility/BaseCompatibility.cpp +++ b/compatibility/BaseCompatibility.cpp @@ -97,6 +97,7 @@ namespace compatibility RemoveIncompatibilities(json, sourceDir); // Write out normalized envelopes for debugging + std::filesystem::create_directories(kitDir); { std::ofstream ofs{ kitDir / "actual.ndjson" }; for (const auto& json : actual) @@ -187,7 +188,7 @@ namespace compatibility auto actualEnvelopes = LoadNdjson(actualNdjsonPath); auto expectedEnvelopes = LoadNdjson(kit.ndjsonFile); - CompareEnvelopes(actualEnvelopes, expectedEnvelopes, kit.sourceDir.string(), kit.sourceDir); + CompareEnvelopes(actualEnvelopes, expectedEnvelopes, kit.sourceDir.string(), kit.buildDir); std::filesystem::remove(actualNdjsonPath); } diff --git a/compatibility/BaseCompatibility.hpp b/compatibility/BaseCompatibility.hpp index 248f32a0..6006a0d0 100644 --- a/compatibility/BaseCompatibility.hpp +++ b/compatibility/BaseCompatibility.hpp @@ -12,6 +12,7 @@ namespace compatibility { std::string name; std::filesystem::path sourceDir; + std::filesystem::path buildDir; std::filesystem::path ndjsonFile; std::filesystem::path pluginPath; std::vector extraArgs; diff --git a/compatibility/CMakeLists.txt b/compatibility/CMakeLists.txt index 91c4d8cb..ee078142 100644 --- a/compatibility/CMakeLists.txt +++ b/compatibility/CMakeLists.txt @@ -41,29 +41,11 @@ target_link_libraries(compatibility.test PRIVATE # Plugins resolve symbols from this executable at runtime via dlopen. # ENABLE_EXPORTS creates the dynamic symbol table, but the linker # normally strips unreferenced symbols from static libraries. -# --whole-archive (Linux) or -force_load (macOS) ensures all cucumber_cpp -# symbols are linked and exported for plugin use. -if (APPLE) - target_link_options(compatibility.test PRIVATE - "LINKER:-force_load,$" - "LINKER:-force_load,$" - "LINKER:-force_load,$" - "LINKER:-force_load,$" - "LINKER:-force_load,$" - "LINKER:-force_load,$" - "LINKER:-force_load,$" - ) -elseif (NOT WIN32) - target_link_options(compatibility.test PRIVATE - "LINKER:--whole-archive" - $ - $ - $ - $ - $ - $ - $ - "LINKER:--no-whole-archive" +# WHOLE_ARCHIVE ensures all cucumber_cpp symbols are linked and exported +# for plugin use. +if (NOT WIN32) + target_link_libraries(compatibility.test PRIVATE + $ ) endif() diff --git a/compatibility/TestCompatibility.cpp b/compatibility/TestCompatibility.cpp index 7fc50904..620e5b4e 100644 --- a/compatibility/TestCompatibility.cpp +++ b/compatibility/TestCompatibility.cpp @@ -17,6 +17,7 @@ namespace compatibility std::vector kits; const std::filesystem::path sourceDir{ COMPAT_SOURCE_DIR }; + const std::filesystem::path buildDir{ COMPAT_BUILD_DIR }; const std::filesystem::path pluginDir{ COMPAT_PLUGIN_DIR }; for (const auto& entry : std::filesystem::directory_iterator{ sourceDir }) @@ -46,6 +47,7 @@ namespace compatibility kits.push_back(KitInfo{ .name = name, .sourceDir = entry.path(), + .buildDir = buildDir / "compatibility" / name, .ndjsonFile = ndjsonFile, .pluginPath = pluginPath, .extraArgs = std::move(extraArgs), diff --git a/cucumber_cpp/acceptance_test/CMakeLists.txt b/cucumber_cpp/acceptance_test/CMakeLists.txt index b04cd4c8..c99a6fd8 100644 --- a/cucumber_cpp/acceptance_test/CMakeLists.txt +++ b/cucumber_cpp/acceptance_test/CMakeLists.txt @@ -31,3 +31,4 @@ target_link_libraries(cucumber_cpp.acceptance_test.unused PRIVATE add_subdirectory(hooks) add_subdirectory(steps) +add_subdirectory(plugin_test) diff --git a/cucumber_cpp/acceptance_test/plugin_test/CMakeLists.txt b/cucumber_cpp/acceptance_test/plugin_test/CMakeLists.txt new file mode 100644 index 00000000..5d9885e4 --- /dev/null +++ b/cucumber_cpp/acceptance_test/plugin_test/CMakeLists.txt @@ -0,0 +1,36 @@ +include(${PROJECT_SOURCE_DIR}/cmake/ccr_add_plugin.cmake) + +# Test executable with statically linked step that loads plugins +add_executable(cucumber_cpp.acceptance_test.plugin ${CCR_EXCLUDE_FROM_ALL}) + +target_sources(cucumber_cpp.acceptance_test.plugin PRIVATE + StaticSteps.cpp + MainPlugin.cpp +) + +target_link_libraries(cucumber_cpp.acceptance_test.plugin PRIVATE + cucumber_cpp +) + +set_target_properties(cucumber_cpp.acceptance_test.plugin PROPERTIES + ENABLE_EXPORTS ON +) + +file(TO_CMAKE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/features" PLUGIN_TEST_FEATURES_DIR_CMAKE) + +target_compile_definitions(cucumber_cpp.acceptance_test.plugin PRIVATE + PLUGIN_TEST_FEATURES_DIR="${PLUGIN_TEST_FEATURES_DIR_CMAKE}" + PLUGIN_TEST_PLUGIN_DIR="$" +) + +if (NOT WIN32) + target_link_libraries(cucumber_cpp.acceptance_test.plugin PRIVATE + $ + ) +endif() + +# Plugin A: steps + custom parameter type +ccr_add_plugin(cucumber_cpp.acceptance_test.plugin_a PluginA.cpp) + +# Plugin B: hooks + steps +ccr_add_plugin(cucumber_cpp.acceptance_test.plugin_b PluginB.cpp) diff --git a/cucumber_cpp/acceptance_test/plugin_test/MainPlugin.cpp b/cucumber_cpp/acceptance_test/plugin_test/MainPlugin.cpp new file mode 100644 index 00000000..acd2acff --- /dev/null +++ b/cucumber_cpp/acceptance_test/plugin_test/MainPlugin.cpp @@ -0,0 +1,63 @@ +#include "cucumber_cpp/CucumberCpp.hpp" +#include "cucumber_cpp/library/plugin/DynamicLibrary.hpp" +#include +#include +#include +#include + +namespace +{ + const std::string featuresDir{ PLUGIN_TEST_FEATURES_DIR }; + const std::string pluginDir{ PLUGIN_TEST_PLUGIN_DIR }; + + std::string PluginPath(const std::string& name) + { + return pluginDir + "/" + name + std::string{ cucumber_cpp::library::plugin::DynamicLibrary::PlatformExtension() }; + } + + int RunApplication(const std::vector& extraArgs) + { + std::vector argStrings; + argStrings.emplace_back("plugin_test"); + argStrings.emplace_back("--format"); + argStrings.emplace_back("summary"); + + for (const auto& arg : extraArgs) + argStrings.emplace_back(arg); + + argStrings.emplace_back("--no-recursive"); + argStrings.emplace_back(featuresDir); + + std::vector argv; + argv.reserve(argStrings.size()); + for (const auto& s : argStrings) + argv.push_back(s.c_str()); + + cucumber_cpp::Application app{}; + return app.Run(static_cast(argv.size()), argv.data()); + } +} + +int main() +{ + const auto pluginA = PluginPath("cucumber_cpp.acceptance_test.plugin_a"); + const auto pluginB = PluginPath("cucumber_cpp.acceptance_test.plugin_b"); + + // First run: plugin A (steps + custom parameter) with statically linked step + auto result = RunApplication({ "--load", pluginA, "--tags", "@plugin_a and not @plugin_b_hook" }); + if (result != 0) + { + std::cerr << "FAILED: First run with plugin A\n"; + return EXIT_FAILURE; + } + + // Second run: plugin B (hooks + steps) with statically linked step + result = RunApplication({ "--load", pluginB, "--tags", "@plugin_b_hook and not @plugin_a" }); + if (result != 0) + { + std::cerr << "FAILED: Second run with plugin B\n"; + return EXIT_FAILURE; + } + + return EXIT_SUCCESS; +} diff --git a/cucumber_cpp/acceptance_test/plugin_test/PluginA.cpp b/cucumber_cpp/acceptance_test/plugin_test/PluginA.cpp new file mode 100644 index 00000000..22beb024 --- /dev/null +++ b/cucumber_cpp/acceptance_test/plugin_test/PluginA.cpp @@ -0,0 +1,39 @@ +#include "cucumber_cpp/Steps.hpp" +#include "gtest/gtest.h" +#include + +namespace +{ + struct Color + { + std::string name; + int r; + int g; + int b; + }; +} + +PARAMETER(Color, ("color", "(red|green|blue)", false), (const std::string& name)) +{ + if (name == "red") + return Color{ .name = "red", .r = 255, .g = 0, .b = 0 }; + if (name == "green") + return Color{ .name = "green", .r = 0, .g = 255, .b = 0 }; + return Color{ .name = "blue", .r = 0, .g = 0, .b = 255 }; +} + +THEN("plugin A can read the static context") +{ + ASSERT_TRUE(context.Get("static_step_ran")); + context.InsertAt("plugin_a_wrote", true); +} + +WHEN("a {color} is selected", (const Color& color)) +{ + context.InsertAt("selected_color", color.name); +} + +THEN("the color name is {string}", (const std::string& expected)) +{ + ASSERT_EQ(context.Get("selected_color"), expected); +} diff --git a/cucumber_cpp/acceptance_test/plugin_test/PluginB.cpp b/cucumber_cpp/acceptance_test/plugin_test/PluginB.cpp new file mode 100644 index 00000000..26668d22 --- /dev/null +++ b/cucumber_cpp/acceptance_test/plugin_test/PluginB.cpp @@ -0,0 +1,25 @@ +#include "cucumber_cpp/Steps.hpp" +#include "gtest/gtest.h" +#include + +HOOK_BEFORE_SCENARIO("@plugin_b_hook") +{ + std::cout << "PLUGIN_B_BEFORE_SCENARIO\n"; + context.InsertAt("plugin_b_hook_ran", true); +} + +HOOK_AFTER_SCENARIO("@plugin_b_hook") +{ + std::cout << "PLUGIN_B_AFTER_SCENARIO\n"; +} + +THEN("plugin B can read the static context") +{ + ASSERT_TRUE(context.Get("static_step_ran")); + context.InsertAt("plugin_b_wrote", true); +} + +THEN("plugin B hook was executed") +{ + ASSERT_TRUE(context.Get("plugin_b_hook_ran")); +} diff --git a/cucumber_cpp/acceptance_test/plugin_test/StaticSteps.cpp b/cucumber_cpp/acceptance_test/plugin_test/StaticSteps.cpp new file mode 100644 index 00000000..07f1c0ba --- /dev/null +++ b/cucumber_cpp/acceptance_test/plugin_test/StaticSteps.cpp @@ -0,0 +1,18 @@ +#include "cucumber_cpp/Steps.hpp" +#include "gtest/gtest.h" +#include + +GIVEN("a statically linked step") +{ + context.InsertAt("static_step_ran", true); +} + +THEN("the static step can read plugin A context") +{ + ASSERT_TRUE(context.Get("plugin_a_wrote")); +} + +THEN("the static step can read plugin B context") +{ + ASSERT_TRUE(context.Get("plugin_b_wrote")); +} diff --git a/cucumber_cpp/acceptance_test/plugin_test/features/test_plugins.feature b/cucumber_cpp/acceptance_test/plugin_test/features/test_plugins.feature new file mode 100644 index 00000000..f356f06c --- /dev/null +++ b/cucumber_cpp/acceptance_test/plugin_test/features/test_plugins.feature @@ -0,0 +1,35 @@ +@plugin +Feature: Plugin loading with static steps + + @plugin_a + Scenario: Plugin A provides custom parameter type + Given a statically linked step + Then plugin A can read the static context + When a red is selected + Then the color name is "red" + And the static step can read plugin A context + + @plugin_a + Scenario: Plugin A with different color + Given a statically linked step + Then plugin A can read the static context + When a blue is selected + Then the color name is "blue" + + @plugin_b_hook + Scenario: Plugin B provides hooks and steps + Given a statically linked step + Then plugin B can read the static context + And the static step can read plugin B context + And plugin B hook was executed + + @plugin_a @plugin_b_hook + Scenario: Both plugins loaded together + Given a statically linked step + Then plugin A can read the static context + And plugin B can read the static context + When a green is selected + Then the color name is "green" + And the static step can read plugin A context + And the static step can read plugin B context + And plugin B hook was executed diff --git a/cucumber_cpp/acceptance_test/test.bats b/cucumber_cpp/acceptance_test/test.bats index fa3a1a9c..40fbe2e7 100644 --- a/cucumber_cpp/acceptance_test/test.bats +++ b/cucumber_cpp/acceptance_test/test.bats @@ -1,8 +1,9 @@ #!/usr/bin/env bats setup_file() { - acceptance_test=$(find . -name "cucumber_cpp.acceptance_test" -print -quit) - export acceptance_test + acceptance_test=$(find . -name "cucumber_cpp.acceptance_test" -not -name "*.plugin*" -print -quit) + plugin_test=$(find . -name "cucumber_cpp.acceptance_test.plugin" -not -name "*.plugin_*" -print -quit) + export acceptance_test plugin_test } setup() { @@ -252,3 +253,8 @@ teardown() { assert_output --partial "Parse error in: \"cucumber_cpp/acceptance_test/features_with_parse_error/test_parse_error.feature:4:9\"" assert_output --partial "got 'when this line is a parse error (when should be When)'" } + +@test "Plugin test: load two plugins sequentially with static step" { + run $plugin_test + assert_success +} diff --git a/cucumber_cpp/library/Application.cpp b/cucumber_cpp/library/Application.cpp index 68cd526f..a5ecc18c 100644 --- a/cucumber_cpp/library/Application.cpp +++ b/cucumber_cpp/library/Application.cpp @@ -81,8 +81,8 @@ namespace cucumber_cpp::library { if (!dynamicLibraryManager.GetLoadedLibraries().empty()) { - cucumber_expression::ConverterTypeMapClearer::ClearAll(); support::DefinitionRegistration::Instance().UnregisterPlugins(); + cucumber_expression::ConverterRegistry::RestoreSnapshot(); dynamicLibraryManager.UnloadAll(); } } @@ -152,6 +152,7 @@ namespace cucumber_cpp::library if (!options.loadPaths.empty()) { support::DefinitionRegistration::Instance().TakeSnapshot(); + cucumber_expression::ConverterRegistry::TakeSnapshot(); dynamicLibraryManager.Load(options.loadPaths); } diff --git a/cucumber_cpp/library/cucumber_expression/ParameterRegistry.hpp b/cucumber_cpp/library/cucumber_expression/ParameterRegistry.hpp index 33e85b57..90897031 100644 --- a/cucumber_cpp/library/cucumber_expression/ParameterRegistry.hpp +++ b/cucumber_cpp/library/cucumber_expression/ParameterRegistry.hpp @@ -136,7 +136,7 @@ namespace cucumber_cpp::library::cucumber_expression // site (host or plugin), while the underlying storage can be shared. using AnyConverterFunction = std::function; - using ConverterMap = std::map; + using ConverterMap = std::map>; // Central converter registry. Each DLL has its own default map, but the // active pointer can be redirected to a shared (host-owned) map so @@ -161,20 +161,27 @@ namespace cucumber_cpp::library::cucumber_expression ActivePtr() = external != nullptr ? external : &LocalInstance(); } + static void TakeSnapshot() + { + Snapshot() = Instance(); + } + + static void RestoreSnapshot() + { + Instance() = Snapshot(); + } + private: static ConverterMap*& ActivePtr() { static ConverterMap* ptr = &LocalInstance(); return ptr; } - }; - struct ConverterTypeMapClearer - { - static void ClearAll() + static ConverterMap& Snapshot() { - ConverterRegistry::LocalInstance().clear(); - ConverterRegistry::Instance().clear(); + static ConverterMap snapshot; + return snapshot; } }; @@ -218,7 +225,7 @@ namespace cucumber_cpp::library::cucumber_expression Assigner& operator=(ConverterFunction fn) { - map[name] = [fn = std::move(fn)](const ConvertFunctionArg& args) -> std::any + map[name] = [fn = std::move(fn)](const ConvertFunctionArg& args) { return std::any{ fn(args) }; }; @@ -237,7 +244,15 @@ namespace cucumber_cpp::library::cucumber_expression return Proxy{ ConverterRegistry::Instance() }; } }; +} + +namespace cucumber_cpp::library::plugin +{ + struct ParameterLoader; +} +namespace cucumber_cpp::library::cucumber_expression +{ struct ParameterRegistry { explicit ParameterRegistry(const std::set>& customParameters); @@ -252,6 +267,9 @@ namespace cucumber_cpp::library::cucumber_expression template void AddParameter(std::string name, std::vector regex, ConverterFunction converter, std::source_location location = std::source_location::current()); + private: + friend struct plugin::ParameterLoader; + void AssertParameterIsUnique(const std::string& name) const; void AddParameter(ParameterType parameter); diff --git a/cucumber_cpp/library/plugin/DynamicLibrary.cpp b/cucumber_cpp/library/plugin/DynamicLibrary.cpp index 92569695..eb214d26 100644 --- a/cucumber_cpp/library/plugin/DynamicLibrary.cpp +++ b/cucumber_cpp/library/plugin/DynamicLibrary.cpp @@ -30,6 +30,9 @@ namespace cucumber_cpp::library::plugin nullptr, error, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), reinterpret_cast(&buffer), 0, nullptr); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + if (size == 0 || buffer == nullptr) + return "Unknown error (code " + std::to_string(error) + ")"; + std::string message(buffer, size); LocalFree(buffer); return message; @@ -112,12 +115,17 @@ namespace cucumber_cpp::library::plugin #if defined(_WIN32) void* symbol = reinterpret_cast(GetProcAddress(static_cast(handle), symbolName.c_str())); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + + if (symbol == nullptr) + throw std::runtime_error("Symbol '" + symbolName + "' not found in library '" + libraryPath.string() + "': " + GetLastErrorMessage()); #else + dlerror(); // clear previous error void* symbol = dlsym(handle, symbolName.c_str()); -#endif + const char* error = dlerror(); - if (symbol == nullptr) - throw std::runtime_error("Symbol '" + symbolName + "' not found in library '" + libraryPath.string() + "'"); + if (error != nullptr) + throw std::runtime_error("Symbol '" + symbolName + "' not found in library '" + libraryPath.string() + "': " + error); +#endif return symbol; } diff --git a/cucumber_cpp/library/plugin/DynamicLibraryManager.cpp b/cucumber_cpp/library/plugin/DynamicLibraryManager.cpp index 2627b6b6..ba20c545 100644 --- a/cucumber_cpp/library/plugin/DynamicLibraryManager.cpp +++ b/cucumber_cpp/library/plugin/DynamicLibraryManager.cpp @@ -51,6 +51,8 @@ namespace cucumber_cpp::library::plugin auto registerFn = lib.GetSymbol("ccr_register"); PluginHostContext context{ + .abiVersion = pluginAbiVersion, + .structSize = sizeof(PluginHostContext), .registration = &support::DefinitionRegistration::Instance(), .stopwatch = &util::Stopwatch::Instance(), .timestampGenerator = &util::TimestampGenerator::Instance(), diff --git a/cucumber_cpp/library/plugin/PLUGIN_ARCHITECTURE.md b/cucumber_cpp/library/plugin/PLUGIN_ARCHITECTURE.md index c09400de..43030421 100644 --- a/cucumber_cpp/library/plugin/PLUGIN_ARCHITECTURE.md +++ b/cucumber_cpp/library/plugin/PLUGIN_ARCHITECTURE.md @@ -347,9 +347,76 @@ flowchart TD 3. Query methods (`ForEachRegisteredStep`, `GetHooks`, `GetRegisteredParameters`, `LoadIds`) iterate the host's own entries, then each plugin's entries. -4. On destruction, `UnregisterPlugins()` clears the pointer list before `UnloadAll()` - closes the shared libraries (preventing dangling pointers). +4. On destruction, `UnregisterPlugins()` clears the pointer list, then + `ConverterRegistry::RestoreSnapshot()` restores the converter map to its + pre-plugin state, before `UnloadAll()` closes the shared libraries + (preventing dangling pointers). **Consequence**: Multiple sequential `Application` instances in the same process will correctly share statically-linked definitions while each getting a fresh set of plugin-loaded definitions. + +--- + +## ABI Versioning + +The `PluginHostContext` struct passed to `ccr_register` includes version +metadata to guard against host/plugin version skew: + +```cpp +struct PluginHostContext +{ + uint32_t abi_version; // pluginAbiVersion constant + uint32_t struct_size; // sizeof(PluginHostContext) at build time + void* registration; + void* stopwatch; + void* timestampGenerator; + void* converterMap; +}; +``` + +- **`abi_version`** — compared by the plugin against its own compiled-in + `pluginAbiVersion`. A mismatch causes `ccr_register` to return immediately + without registering, preventing undefined behaviour from incompatible structs. +- **`struct_size`** — allows future extensions to append fields to + `PluginHostContext` while old plugins safely ignore them. + +--- + +## Snapshot / Restore Cleanup + +Before loading plugins, the host takes snapshots of mutable global state: + +```cpp +support::DefinitionRegistration::Instance().TakeSnapshot(); +cucumber_expression::ConverterRegistry::TakeSnapshot(); +dynamicLibraryManager.Load(options.loadPaths); +``` + +On destruction, rather than clearing all state (which would destroy +statically-registered converters), the host restores to the snapshot: + +```cpp +support::DefinitionRegistration::Instance().UnregisterPlugins(); +cucumber_expression::ConverterRegistry::RestoreSnapshot(); +dynamicLibraryManager.UnloadAll(); +``` + +This ensures that converters and definitions registered by the statically-linked +test code survive across Application instances, while plugin-added entries are +properly removed. + +--- + +## Duplicate Plugin Guard + +`DefinitionRegistration::RegisterPlugin` includes a duplicate-pointer guard: + +```cpp +if (std::find(plugins.begin(), plugins.end(), &plugin) != plugins.end()) + return; +``` + +This prevents the same plugin from being registered twice if `--load` lists +the same path multiple times or if a directory scan includes a plugin that +was also explicitly specified. diff --git a/cucumber_cpp/library/plugin/PluginExport.hpp b/cucumber_cpp/library/plugin/PluginExport.hpp index dc5f6af0..0746aef5 100644 --- a/cucumber_cpp/library/plugin/PluginExport.hpp +++ b/cucumber_cpp/library/plugin/PluginExport.hpp @@ -1,6 +1,8 @@ #ifndef PLUGIN_PLUGIN_EXPORT_HPP #define PLUGIN_PLUGIN_EXPORT_HPP +#include + #if defined(_WIN32) || defined(__CYGWIN__) #define CCR_EXPORT __declspec(dllexport) #else @@ -9,15 +11,19 @@ namespace cucumber_cpp::library::plugin { + constexpr uint32_t pluginAbiVersion = 1; + struct PluginHostContext { + uint32_t abiVersion; + uint32_t structSize; void* registration; void* stopwatch; void* timestampGenerator; void* converterMap; }; -} -using CcrRegisterFn = void (*)(cucumber_cpp::library::plugin::PluginHostContext*); + using CcrRegisterFn = void (*)(PluginHostContext*); +} #endif diff --git a/cucumber_cpp/library/plugin/PluginRegister.cpp b/cucumber_cpp/library/plugin/PluginRegister.cpp index 7bb52479..9b67e06f 100644 --- a/cucumber_cpp/library/plugin/PluginRegister.cpp +++ b/cucumber_cpp/library/plugin/PluginRegister.cpp @@ -9,6 +9,9 @@ extern "C" CCR_EXPORT void ccr_register(cucumber_cpp::library::plugin::PluginHos if (context == nullptr || context->registration == nullptr) return; + if (context->abiVersion != cucumber_cpp::library::plugin::pluginAbiVersion) + return; + auto& host = *static_cast(context->registration); host.RegisterPlugin(cucumber_cpp::library::support::DefinitionRegistration::Instance()); diff --git a/cucumber_cpp/library/support/DefinitionRegistration.cpp b/cucumber_cpp/library/support/DefinitionRegistration.cpp index 75ac543e..828fe1c8 100644 --- a/cucumber_cpp/library/support/DefinitionRegistration.cpp +++ b/cucumber_cpp/library/support/DefinitionRegistration.cpp @@ -7,6 +7,7 @@ #include "cucumber_cpp/library/util/HookData.hpp" #include "cucumber_cpp/library/util/HookFactory.hpp" #include "cucumber_cpp/library/util/StepFactory.hpp" +#include #include #include #include @@ -37,6 +38,9 @@ namespace cucumber_cpp::library::support if (&plugin == this) return; + if (std::find(plugins.begin(), plugins.end(), &plugin) != plugins.end()) + return; + plugins.push_back(&plugin); } diff --git a/cucumber_cpp/library/util/Body.cpp b/cucumber_cpp/library/util/Body.cpp index f669280e..f7500711 100644 --- a/cucumber_cpp/library/util/Body.cpp +++ b/cucumber_cpp/library/util/Body.cpp @@ -4,6 +4,7 @@ #include "cucumber_cpp/library/util/TestStepResult.hpp" #include "cucumber_cpp/library/util/TestStepResultStatus.hpp" #include "fmt/format.h" +#include "gtest/gtest-spi.h" #include "gtest/gtest.h" #include #include @@ -12,6 +13,16 @@ namespace cucumber_cpp::library::util { + struct CucumberResultReporter : public testing::ScopedFakeTestPartResultReporter + { + explicit CucumberResultReporter(util::TestStepResult& testStepResult); + + void ReportTestPartResult(const testing::TestPartResult& testPartResult) override; + + private: + util::TestStepResult& testStepResult; + }; + CucumberResultReporter::CucumberResultReporter(util::TestStepResult& testStepResult) : testing::ScopedFakeTestPartResultReporter{ nullptr } , testStepResult{ testStepResult } @@ -60,8 +71,9 @@ namespace cucumber_cpp::library::util return testStepResult; } - Body::Body(TestStepResult& testStepResult) - : reportListener{ testStepResult } + : reportListener{ std::make_unique(testStepResult) } {} + + Body::~Body() = default; } diff --git a/cucumber_cpp/library/util/Body.hpp b/cucumber_cpp/library/util/Body.hpp index 868938c5..4667fedb 100644 --- a/cucumber_cpp/library/util/Body.hpp +++ b/cucumber_cpp/library/util/Body.hpp @@ -3,7 +3,6 @@ #include "cucumber_cpp/library/cucumber_expression/ParameterRegistry.hpp" #include "cucumber_cpp/library/util/TestStepResult.hpp" -#include "gtest/gtest-spi.h" #include "gtest/gtest.h" #include #include @@ -16,15 +15,7 @@ namespace cucumber_cpp::library::util { - struct CucumberResultReporter : public testing::ScopedFakeTestPartResultReporter - { - explicit CucumberResultReporter(util::TestStepResult& testStepResult); - - void ReportTestPartResult(const testing::TestPartResult& testPartResult) override; - - private: - util::TestStepResult& testStepResult; - }; + struct CucumberResultReporter; struct FatalError : std::runtime_error { @@ -71,14 +62,14 @@ namespace cucumber_cpp::library::util struct Body { explicit Body(TestStepResult& testStepResult); - virtual ~Body() = default; + virtual ~Body(); private: virtual void Execute(const ExecuteArgs& args) = 0; friend TestStepResult ConstructAndExecute(const BodyFactory& bodyFactory, const ExecuteArgs& args); - CucumberResultReporter reportListener; + std::unique_ptr reportListener; }; } diff --git a/cucumber_cpp/library/util/Duration.cpp b/cucumber_cpp/library/util/Duration.cpp index d530077a..519ea8bc 100644 --- a/cucumber_cpp/library/util/Duration.cpp +++ b/cucumber_cpp/library/util/Duration.cpp @@ -44,6 +44,12 @@ namespace cucumber_cpp::library::util instance = &inst; } + Stopwatch::~Stopwatch() + { + if (instance == this) + instance = nullptr; + } + std::chrono::high_resolution_clock::time_point StopWatchHighResolutionClock::Start() { return std::chrono::high_resolution_clock::now(); diff --git a/cucumber_cpp/library/util/Duration.hpp b/cucumber_cpp/library/util/Duration.hpp index e654b9ee..c07b9d61 100644 --- a/cucumber_cpp/library/util/Duration.hpp +++ b/cucumber_cpp/library/util/Duration.hpp @@ -16,7 +16,7 @@ namespace cucumber_cpp::library::util struct Stopwatch { Stopwatch(); - virtual ~Stopwatch() = default; + virtual ~Stopwatch(); static Stopwatch& Instance(); static void SetInstance(Stopwatch& inst); diff --git a/cucumber_cpp/library/util/Timestamp.cpp b/cucumber_cpp/library/util/Timestamp.cpp index 50b1839f..3e7a1659 100644 --- a/cucumber_cpp/library/util/Timestamp.cpp +++ b/cucumber_cpp/library/util/Timestamp.cpp @@ -31,7 +31,8 @@ namespace cucumber_cpp::library::util TimestampGenerator::~TimestampGenerator() { - instance = nullptr; + if (instance == this) + instance = nullptr; } TimestampGenerator& TimestampGenerator::Instance() From 0a1992e25d90bfb815ccb8aae3cfc251fcc4adc4 Mon Sep 17 00:00:00 2001 From: "Timmer, Daan" Date: Sat, 25 Jul 2026 22:20:54 +0000 Subject: [PATCH 27/29] Enhance readability settings in .clang-tidy for identifier naming conventions --- .clang-tidy | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.clang-tidy b/.clang-tidy index 73ff6a07..83b2c607 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -30,4 +30,17 @@ HeaderFilterRegex: '.*' FormatStyle: file CheckOptions: AllowSoleDefaultDtor: true + # readability-identifier-naming + readability-identifier-naming.NamespaceCase: lower_case + readability-identifier-naming.StructCase: CamelCase + readability-identifier-naming.ClassCase: CamelCase + readability-identifier-naming.EnumCase: CamelCase + readability-identifier-naming.EnumConstantCase: camelBack + readability-identifier-naming.MemberCase: camelBack + readability-identifier-naming.ParameterCase: camelBack + readability-identifier-naming.VariableCase: camelBack + readability-identifier-naming.FunctionCase: CamelCase + readability-identifier-naming.MethodCase: CamelCase + readability-identifier-naming.TypeAliasCase: CamelCase + readability-identifier-naming.TypedefCase: CamelCase ... From a466374bac8d16713ca31dbf29e212611a15a5b9 Mon Sep 17 00:00:00 2001 From: "Timmer, Daan" Date: Sat, 25 Jul 2026 22:27:50 +0000 Subject: [PATCH 28/29] Implement PluginSession management for dynamic library loading and unloading --- cucumber_cpp/library/Application.cpp | 40 ++++++++++++++++++---------- cucumber_cpp/library/Application.hpp | 4 +++ 2 files changed, 30 insertions(+), 14 deletions(-) diff --git a/cucumber_cpp/library/Application.cpp b/cucumber_cpp/library/Application.cpp index a5ecc18c..f485707a 100644 --- a/cucumber_cpp/library/Application.cpp +++ b/cucumber_cpp/library/Application.cpp @@ -35,6 +35,30 @@ namespace cucumber_cpp::library { + struct PluginSession + { + explicit PluginSession(plugin::DynamicLibraryManager& manager, const std::vector& paths) + : manager{ manager } + { + support::DefinitionRegistration::Instance().TakeSnapshot(); + cucumber_expression::ConverterRegistry::TakeSnapshot(); + manager.Load(paths); + } + + ~PluginSession() + { + support::DefinitionRegistration::Instance().UnregisterPlugins(); + cucumber_expression::ConverterRegistry::RestoreSnapshot(); + manager.UnloadAll(); + } + + PluginSession(const PluginSession&) = delete; + PluginSession& operator=(const PluginSession&) = delete; + + private: + plugin::DynamicLibraryManager& manager; + }; + namespace { bool IsFeatureFile(const std::filesystem::directory_entry& entry) @@ -77,15 +101,7 @@ namespace cucumber_cpp::library cli.set_config("--config", "cucumber.toml"); } - Application::~Application() - { - if (!dynamicLibraryManager.GetLoadedLibraries().empty()) - { - support::DefinitionRegistration::Instance().UnregisterPlugins(); - cucumber_expression::ConverterRegistry::RestoreSnapshot(); - dynamicLibraryManager.UnloadAll(); - } - } + Application::~Application() = default; int Application::Run(int argc, const char* const* argv) { @@ -150,11 +166,7 @@ namespace cucumber_cpp::library std::ofstream{ "cucumber.toml" } << cli.config_to_str(true, true); if (!options.loadPaths.empty()) - { - support::DefinitionRegistration::Instance().TakeSnapshot(); - cucumber_expression::ConverterRegistry::TakeSnapshot(); - dynamicLibraryManager.Load(options.loadPaths); - } + pluginSession = std::make_unique(dynamicLibraryManager, options.loadPaths); return RunFeatures(); } diff --git a/cucumber_cpp/library/Application.hpp b/cucumber_cpp/library/Application.hpp index f2a75931..460f01aa 100644 --- a/cucumber_cpp/library/Application.hpp +++ b/cucumber_cpp/library/Application.hpp @@ -27,6 +27,8 @@ namespace cucumber_cpp::library { + struct PluginSession; + struct Application { struct Options @@ -91,6 +93,8 @@ namespace cucumber_cpp::library plugin::DynamicLibraryManager dynamicLibraryManager; + std::unique_ptr pluginSession; + cucumber_expression::ParameterRegistry parameterRegistry{ cucumber_cpp::library::support::DefinitionRegistration::Instance().GetRegisteredParameters() }; bool removeDefaultGoogleTestListener; std::unique_ptr stopwatch; From dd77a978e6e05035234cde66cc28a6a51152f016 Mon Sep 17 00:00:00 2001 From: "Timmer, Daan" Date: Sat, 25 Jul 2026 22:46:54 +0000 Subject: [PATCH 29/29] Refactor method names in ConverterTypeMap for consistency and clarity --- compatibility/CMakeLists.txt | 2 +- cucumber_cpp/library/cucumber_expression/Argument.hpp | 4 ++-- .../library/cucumber_expression/ParameterRegistry.hpp | 9 ++++----- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/compatibility/CMakeLists.txt b/compatibility/CMakeLists.txt index ee078142..58e78605 100644 --- a/compatibility/CMakeLists.txt +++ b/compatibility/CMakeLists.txt @@ -64,7 +64,7 @@ target_compile_definitions(compatibility.test PRIVATE if(CCR_BUILD_TESTS) if(NOT CMAKE_CROSSCOMPILING AND NOT CMAKE_HOST_SYSTEM_NAME STREQUAL "Windows") - gtest_discover_tests(compatibility.test) + gtest_discover_tests(compatibility.test DISCOVERY_MODE PRE_TEST) else() add_test(NAME compatibility.test COMMAND compatibility.test) endif() diff --git a/cucumber_cpp/library/cucumber_expression/Argument.hpp b/cucumber_cpp/library/cucumber_expression/Argument.hpp index 7e146337..1b8ef2c2 100644 --- a/cucumber_cpp/library/cucumber_expression/Argument.hpp +++ b/cucumber_cpp/library/cucumber_expression/Argument.hpp @@ -13,13 +13,13 @@ namespace cucumber_cpp::library::cucumber_expression template T TransformArg([[maybe_unused]] const T& _, const std::string& name, const cucumber_expression::ConvertFunctionArg& match) { - return ConverterTypeMap>::Instance().at(name)(match).value(); + return ConverterTypeMap>::Instance().At(name)(match).value(); } template std::optional TransformArg([[maybe_unused]] const std::optional& _, const std::string& name, const cucumber_expression::ConvertFunctionArg& match) { - return ConverterTypeMap>::Instance().at(name)(match); + return ConverterTypeMap>::Instance().At(name)(match); } struct Argument diff --git a/cucumber_cpp/library/cucumber_expression/ParameterRegistry.hpp b/cucumber_cpp/library/cucumber_expression/ParameterRegistry.hpp index 90897031..b225dcae 100644 --- a/cucumber_cpp/library/cucumber_expression/ParameterRegistry.hpp +++ b/cucumber_cpp/library/cucumber_expression/ParameterRegistry.hpp @@ -195,9 +195,9 @@ namespace cucumber_cpp::library::cucumber_expression { ConverterMap& map; - void emplace(const std::string& name, ConverterFunction fn) + void Emplace(const std::string& name, ConverterFunction fn) { - map[name] = [fn = std::move(fn)](const ConvertFunctionArg& args) -> std::any + map[name] = [fn = std::move(fn)](const ConvertFunctionArg& args) { return std::any{ fn(args) }; }; @@ -213,7 +213,7 @@ namespace cucumber_cpp::library::cucumber_expression } }; - TypedAccessor at(const std::string& name) + TypedAccessor At(const std::string& name) { return TypedAccessor{ map.at(name) }; } @@ -274,7 +274,6 @@ namespace cucumber_cpp::library::cucumber_expression void AddParameter(ParameterType parameter); - private: template void AddBuiltinParameter(std::string name, std::vector regex, ConverterFunction converter, bool preferForRegexMatch = false, std::source_location location = std::source_location::current()); @@ -304,7 +303,7 @@ namespace cucumber_cpp::library::cucumber_expression AddParameter(parameter); - ConverterTypeMap::Instance().emplace(parameter.name, converter); + ConverterTypeMap::Instance().Emplace(parameter.name, converter); } }