diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 52d2a347..c8c8e542 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -82,6 +82,7 @@ jobs: os: [macos, windows, ubuntu] version: [latest] type: [synchronous] + preset_suffix: ["", "-no-re2"] fail-fast: false steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -89,14 +90,14 @@ jobs: persist-credentials: false - uses: hendrikmuhs/ccache-action@33522472633dbd32578e909b315f5ee43ba878ce # v1.2.22 with: - key: ${{ github.job }}-${{ matrix.os }}-${{ matrix.type }} + key: ${{ github.job }}-${{ matrix.os }}-${{ matrix.type }}${{ matrix.preset_suffix }} max-size: 2G - uses: lukka/run-cmake@5d55ea7949e25f69f0ecb516d8d572297e03a956 # v10.9 with: - workflowPreset: "${{ matrix.os }}-Debug-${{ matrix.type }}" + workflowPreset: "${{ matrix.os }}-Debug-${{ matrix.type }}${{ matrix.preset_suffix }}" - name: Upload test logs if: ${{ failure() }} uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: test-logs - path: .build/${{ matrix.os }}-Debug-${{ matrix.type }}/Testing/Temporary/ + path: .build/${{ matrix.os }}-Debug-${{ matrix.type }}${{ matrix.preset_suffix }}/Testing/Temporary/ diff --git a/CMakeLists.txt b/CMakeLists.txt index 29c0d02c..f97a7cad 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -20,18 +20,12 @@ endif() option(CCR_FETCH_DEPS "Fetch dependencies via FetchContent." ${CCR_DEFAULTOPT} ) option(CCR_BUILD_TESTS "Enable build of the tests" ${CCR_DEFAULTOPT}) +option(CCR_USE_RE2 "Use the RE2 regex engine (fetched when CCR_FETCH_DEPS is set, otherwise auto-detected)" ${CCR_DEFAULTOPT}) option(CCR_ENABLE_COVERAGE "Enable compiler flags for code coverage measurements" Off) option(CCR_ENABLE_TIME_PROFILE "Enable compiler flags for time profiling measurements" Off) set(CMAKE_POSITION_INDEPENDENT_CODE ON) -if (CCR_STANDALONE AND NOT CCR_ENABLE_COVERAGE) - if (NOT (CMAKE_CXX_COMPILER_ID MATCHES "MSVC" OR CMAKE_CXX_SIMULATE_ID MATCHES "MSVC")) - add_compile_options(-fsanitize=address -fsanitize=undefined) - add_link_options(-fsanitize=address -fsanitize=undefined) - endif() -endif() - if (CCR_BUILD_TESTS) ccr_enable_testing() endif() @@ -81,6 +75,28 @@ else() if (CCR_BUILD_TESTS) find_package(yaml-cpp REQUIRED) endif() + + if (CCR_USE_RE2) + find_package(absl) + find_package(re2) + endif() +endif() + +if (TARGET re2::re2) + set(CCR_HAS_RE2 On) +else() + set(CCR_HAS_RE2 Off) +endif() + +# Sanitizer flags are set after external dependencies so that fetched third-party +# libraries (abseil, re2) are not compiled with sanitizer instrumentation. +# Abseil's constexpr function-pointer comparisons are not compatible with GCC's +# ASan/UBSan instrumentation, which causes build failures. +if (CCR_STANDALONE AND NOT CCR_ENABLE_COVERAGE) + if (NOT (CMAKE_CXX_COMPILER_ID MATCHES "MSVC" OR CMAKE_CXX_SIMULATE_ID MATCHES "MSVC")) + add_compile_options(-fsanitize=address -fsanitize=undefined) + add_link_options(-fsanitize=address -fsanitize=undefined) + endif() endif() add_subdirectory(cucumber_cpp) diff --git a/CMakePresets.json b/CMakePresets.json index 76343f1c..4e9f5dca 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -57,6 +57,31 @@ "displayName": "Configuration for macOS Debug and Tests, Single Config Generator, synchronous", "inherits": "host-single-Debug-synchronous" }, + { + "name": "ubuntu-Debug-synchronous-no-re2", + "displayName": "Configuration for Linux Debug and Tests, Single Config Generator, synchronous, without RE2", + "inherits": "host-single-Debug-synchronous", + "cacheVariables": { + "CCR_USE_RE2": "Off" + } + }, + { + "name": "windows-Debug-synchronous-no-re2", + "displayName": "Configuration for Windows Debug and Tests, Single Config Generator, synchronous, without RE2", + "inherits": "host-single-Debug-synchronous", + "toolchainFile": "${sourceDir}/cmake/Windows.MSVC.toolchain.cmake", + "cacheVariables": { + "CCR_USE_RE2": "Off" + } + }, + { + "name": "macos-Debug-synchronous-no-re2", + "displayName": "Configuration for macOS Debug and Tests, Single Config Generator, synchronous, without RE2", + "inherits": "host-single-Debug-synchronous", + "cacheVariables": { + "CCR_USE_RE2": "Off" + } + }, { "name": "host-single-time-profile", "displayName": "Configuration time profiling", @@ -124,6 +149,21 @@ "configuration": "Debug", "configurePreset": "macos-Debug-synchronous" }, + { + "name": "ubuntu-Debug-synchronous-no-re2", + "configuration": "Debug", + "configurePreset": "ubuntu-Debug-synchronous-no-re2" + }, + { + "name": "windows-Debug-synchronous-no-re2", + "configuration": "Debug", + "configurePreset": "windows-Debug-synchronous-no-re2" + }, + { + "name": "macos-Debug-synchronous-no-re2", + "configuration": "Debug", + "configurePreset": "macos-Debug-synchronous-no-re2" + }, { "name": "host-single-time-profile", "configuration": "Debug", @@ -192,6 +232,24 @@ "configurePreset": "macos-Debug-synchronous", "configuration": "Debug", "inherits": "defaults" + }, + { + "name": "ubuntu-Debug-synchronous-no-re2", + "configurePreset": "ubuntu-Debug-synchronous-no-re2", + "configuration": "Debug", + "inherits": "defaults" + }, + { + "name": "windows-Debug-synchronous-no-re2", + "configurePreset": "windows-Debug-synchronous-no-re2", + "configuration": "Debug", + "inherits": "defaults" + }, + { + "name": "macos-Debug-synchronous-no-re2", + "configurePreset": "macos-Debug-synchronous-no-re2", + "configuration": "Debug", + "inherits": "defaults" } ], "workflowPresets": [ @@ -245,6 +303,57 @@ "name": "macos-Debug-synchronous" } ] + }, + { + "name": "ubuntu-Debug-synchronous-no-re2", + "steps": [ + { + "type": "configure", + "name": "ubuntu-Debug-synchronous-no-re2" + }, + { + "type": "build", + "name": "ubuntu-Debug-synchronous-no-re2" + }, + { + "type": "test", + "name": "ubuntu-Debug-synchronous-no-re2" + } + ] + }, + { + "name": "windows-Debug-synchronous-no-re2", + "steps": [ + { + "type": "configure", + "name": "windows-Debug-synchronous-no-re2" + }, + { + "type": "build", + "name": "windows-Debug-synchronous-no-re2" + }, + { + "type": "test", + "name": "windows-Debug-synchronous-no-re2" + } + ] + }, + { + "name": "macos-Debug-synchronous-no-re2", + "steps": [ + { + "type": "configure", + "name": "macos-Debug-synchronous-no-re2" + }, + { + "type": "build", + "name": "macos-Debug-synchronous-no-re2" + }, + { + "type": "test", + "name": "macos-Debug-synchronous-no-re2" + } + ] } ] } diff --git a/README.md b/README.md index a6b5e56d..bd56cbc1 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,21 @@ amp-cucumber-cpp-runner is known to build using: amp-cucumber-cpp-runner is not supposed to be used standalone. It is possible to add amp-cucumber-cpp-runner as a submodule to your own project or to use cmake's FetchContent to add amp-cucumber-cpp-runner as a cmake build dependency. -En example project is provided which shows most features of amp-cucumber-cpp-runner and how to configure a cmake project to use amp-cucumber-cpp-runner. The simplest solution is to simply add a dependency on `cucumber-cpp-runner` like so: +An example project is provided which shows most features of amp-cucumber-cpp-runner and how to configure a cmake project to use amp-cucumber-cpp-runner. + +### CMake options + +The following CMake options are supported: + +- `CCR_FETCH_DEPS` (default: `Off` when used as a dependency): fetch third-party dependencies via `FetchContent`. +- `CCR_BUILD_TESTS` (default: `Off` when used as a dependency): enable building unit/integration tests in this repository. +- `CCR_USE_RE2` (default: `Off` when used as a dependency): enable optional RE2 usage (`abseil-cpp` + `re2`). +- `CCR_ENABLE_COVERAGE` (default: `Off`): enable compiler flags for coverage measurements. +- `CCR_ENABLE_TIME_PROFILE` (default: `Off`): enable compiler time-trace profiling flags (Clang). + +### FetchContent example + +If you want this project to fetch its dependencies, set `CCR_FETCH_DEPS` explicitly to `On`: ```cmake FetchContent_Declare(cucumber_cpp @@ -25,6 +39,10 @@ FetchContent_Declare(cucumber_cpp GIT_TAG main ) +set(CCR_FETCH_DEPS On CACHE BOOL "Fetch amp-cucumber-cpp-runner dependencies") +# optional: +# set(CCR_USE_RE2 On CACHE BOOL "Enable RE2 backend") + FetchContent_MakeAvailable(cucumber_cpp) add_executable(my_test_runner) diff --git a/cucumber_cpp/library/cucumber_expression/CMakeLists.txt b/cucumber_cpp/library/cucumber_expression/CMakeLists.txt index ddb05d8a..c9dcd0b6 100644 --- a/cucumber_cpp/library/cucumber_expression/CMakeLists.txt +++ b/cucumber_cpp/library/cucumber_expression/CMakeLists.txt @@ -19,8 +19,13 @@ target_sources(cucumber_cpp.library.cucumber_expression PRIVATE MatchRange.hpp ParameterRegistry.cpp ParameterRegistry.hpp + RegexStrategy.hpp + RegexStrategyFactory.cpp + RegexStrategyFactory.hpp RegularExpression.cpp RegularExpression.hpp + StdRegexStrategy.cpp + StdRegexStrategy.hpp TreeRegexp.cpp TreeRegexp.hpp ) @@ -33,6 +38,21 @@ target_link_libraries(cucumber_cpp.library.cucumber_expression PUBLIC fmt::fmt ) +if (CCR_HAS_RE2) + target_sources(cucumber_cpp.library.cucumber_expression PRIVATE + Re2RegexStrategy.cpp + Re2RegexStrategy.hpp + ) + + target_compile_definitions(cucumber_cpp.library.cucumber_expression PRIVATE + CCR_HAS_RE2 + ) + + target_link_libraries(cucumber_cpp.library.cucumber_expression PUBLIC + re2::re2 + ) +endif() + if (CCR_BUILD_TESTS) add_subdirectory(test) endif() diff --git a/cucumber_cpp/library/cucumber_expression/ParameterRegistry.cpp b/cucumber_cpp/library/cucumber_expression/ParameterRegistry.cpp index f2113466..f163dc86 100644 --- a/cucumber_cpp/library/cucumber_expression/ParameterRegistry.cpp +++ b/cucumber_cpp/library/cucumber_expression/ParameterRegistry.cpp @@ -80,7 +80,7 @@ namespace cucumber_cpp::library::cucumber_expression { const static std::string integerNegativeRegex{ R"__(-?\d+)__" }; const static std::string integerPositiveRegex{ R"__(\d+)__" }; - const static std::string floatRegex{ R"__((?=.*\d.*)[-+]?\d*(?:\.(?=\d.*))?\d*(?:\d+[E][+-]?\d+)?)__" }; + const static std::string floatRegex{ R"__([-+]?(?:\d+(?:\.\d+)?|\.\d+)(?:[Ee][+-]?\d+)?)__" }; const static std::string stringRegex{ R"__("([^"\\]*(\\.[^"\\]*)*)"|'([^'\\]*(\\.[^'\\]*)*)')__" }; const static std::string wordRegex{ R"__([^\s]+)__" }; const static std::string anonymousRegex{ R"__(.*)__" }; diff --git a/cucumber_cpp/library/cucumber_expression/Re2RegexStrategy.cpp b/cucumber_cpp/library/cucumber_expression/Re2RegexStrategy.cpp new file mode 100644 index 00000000..a91318ef --- /dev/null +++ b/cucumber_cpp/library/cucumber_expression/Re2RegexStrategy.cpp @@ -0,0 +1,48 @@ +#include "cucumber_cpp/library/cucumber_expression/Re2RegexStrategy.hpp" +#include "absl/strings/string_view.h" +#include "cucumber_cpp/library/cucumber_expression/RegexStrategy.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace cucumber_cpp::library::cucumber_expression +{ + Re2RegexStrategy::Re2RegexStrategy(std::string_view pattern) + : re2{ absl::string_view{ pattern.data(), pattern.size() } } + { + if (!re2.ok()) + throw std::invalid_argument(re2.error()); + } + + std::optional Re2RegexStrategy::Match(std::string_view text) const + { + const int nCaptures = re2.NumberOfCapturingGroups(); + const int nSubmatch = nCaptures + 1; + std::vector submatch(static_cast(nSubmatch)); + + if (!re2.Match(text, 0, static_cast(text.size()), RE2::UNANCHORED, submatch.data(), nSubmatch)) + return std::nullopt; + + Matches result; + result.reserve(static_cast(nSubmatch)); + for (const auto& piece : submatch) + { + if (piece.data() == nullptr) + result.emplace_back(std::nullopt); + else + { + const auto start = static_cast(piece.data() - text.data()); + result.emplace_back(MatchGroup{ + .value = std::string(piece), + .start = start, + .end = start + piece.size(), + }); + } + } + return result; + } +} diff --git a/cucumber_cpp/library/cucumber_expression/Re2RegexStrategy.hpp b/cucumber_cpp/library/cucumber_expression/Re2RegexStrategy.hpp new file mode 100644 index 00000000..1b4c800a --- /dev/null +++ b/cucumber_cpp/library/cucumber_expression/Re2RegexStrategy.hpp @@ -0,0 +1,22 @@ +#ifndef CUCUMBER_EXPRESSION_RE2REGEXSTRATEGY_HPP +#define CUCUMBER_EXPRESSION_RE2REGEXSTRATEGY_HPP + +#include "cucumber_cpp/library/cucumber_expression/RegexStrategy.hpp" +#include "re2/re2.h" +#include +#include + +namespace cucumber_cpp::library::cucumber_expression +{ + struct Re2RegexStrategy : RegexStrategy + { + explicit Re2RegexStrategy(std::string_view pattern); + + [[nodiscard]] std::optional Match(std::string_view text) const override; + + private: + re2::RE2 re2; + }; +} + +#endif diff --git a/cucumber_cpp/library/cucumber_expression/RegexStrategy.hpp b/cucumber_cpp/library/cucumber_expression/RegexStrategy.hpp new file mode 100644 index 00000000..2c1f74a5 --- /dev/null +++ b/cucumber_cpp/library/cucumber_expression/RegexStrategy.hpp @@ -0,0 +1,30 @@ +#ifndef CUCUMBER_EXPRESSION_REGEXSTRATEGY_HPP +#define CUCUMBER_EXPRESSION_REGEXSTRATEGY_HPP + +#include +#include +#include +#include +#include + +namespace cucumber_cpp::library::cucumber_expression +{ + struct MatchGroup + { + std::string value; + std::size_t start; + std::size_t end; + }; + + using Matches = std::vector>; + + struct RegexStrategy + { + RegexStrategy() = default; + virtual ~RegexStrategy() = default; + + [[nodiscard]] virtual std::optional Match(std::string_view text) const = 0; + }; +} + +#endif diff --git a/cucumber_cpp/library/cucumber_expression/RegexStrategyFactory.cpp b/cucumber_cpp/library/cucumber_expression/RegexStrategyFactory.cpp new file mode 100644 index 00000000..fcf3baf0 --- /dev/null +++ b/cucumber_cpp/library/cucumber_expression/RegexStrategyFactory.cpp @@ -0,0 +1,30 @@ +#include "cucumber_cpp/library/cucumber_expression/RegexStrategyFactory.hpp" +#include "cucumber_cpp/library/cucumber_expression/RegexStrategy.hpp" +#include "cucumber_cpp/library/cucumber_expression/StdRegexStrategy.hpp" + +#ifdef CCR_HAS_RE2 +#include "cucumber_cpp/library/cucumber_expression/Re2RegexStrategy.hpp" +#include +#endif + +#include +#include + +namespace cucumber_cpp::library::cucumber_expression +{ + std::unique_ptr CreateRegexStrategy(std::string_view pattern) + { +#ifdef CCR_HAS_RE2 + try + { + return std::make_unique(pattern); + } + catch (const std::invalid_argument&) + { + return std::make_unique(pattern); + } +#else + return std::make_unique(pattern); +#endif + } +} diff --git a/cucumber_cpp/library/cucumber_expression/RegexStrategyFactory.hpp b/cucumber_cpp/library/cucumber_expression/RegexStrategyFactory.hpp new file mode 100644 index 00000000..ab0d7e73 --- /dev/null +++ b/cucumber_cpp/library/cucumber_expression/RegexStrategyFactory.hpp @@ -0,0 +1,13 @@ +#ifndef CUCUMBER_EXPRESSION_REGEXSTRATEGYFACTORY_HPP +#define CUCUMBER_EXPRESSION_REGEXSTRATEGYFACTORY_HPP + +#include "cucumber_cpp/library/cucumber_expression/RegexStrategy.hpp" +#include +#include + +namespace cucumber_cpp::library::cucumber_expression +{ + [[nodiscard]] std::unique_ptr CreateRegexStrategy(std::string_view pattern); +} + +#endif diff --git a/cucumber_cpp/library/cucumber_expression/StdRegexStrategy.cpp b/cucumber_cpp/library/cucumber_expression/StdRegexStrategy.cpp new file mode 100644 index 00000000..ecd527e8 --- /dev/null +++ b/cucumber_cpp/library/cucumber_expression/StdRegexStrategy.cpp @@ -0,0 +1,41 @@ +#include "cucumber_cpp/library/cucumber_expression/StdRegexStrategy.hpp" +#include "cucumber_cpp/library/cucumber_expression/RegexStrategy.hpp" +#include +#include +#include +#include +#include + +namespace cucumber_cpp::library::cucumber_expression +{ + StdRegexStrategy::StdRegexStrategy(std::string_view pattern) + : regex{ std::string(pattern) } + { + } + + std::optional StdRegexStrategy::Match(std::string_view text) const + { + std::smatch match; + const std::string textStr(text); + if (!std::regex_search(textStr, match, regex)) + return std::nullopt; + + Matches result; + result.reserve(match.size()); + for (std::smatch::size_type i = 0; i < match.size(); ++i) + { + if (const auto& m = match[i]; !m.matched) + result.emplace_back(std::nullopt); + else + { + const auto start = static_cast(match.position(i)); + result.emplace_back(MatchGroup{ + .value = m.str(), + .start = start, + .end = start + static_cast(m.length()), + }); + } + } + return result; + } +} diff --git a/cucumber_cpp/library/cucumber_expression/StdRegexStrategy.hpp b/cucumber_cpp/library/cucumber_expression/StdRegexStrategy.hpp new file mode 100644 index 00000000..4bea05ff --- /dev/null +++ b/cucumber_cpp/library/cucumber_expression/StdRegexStrategy.hpp @@ -0,0 +1,22 @@ +#ifndef CUCUMBER_EXPRESSION_STDREGEXSTRATEGY_HPP +#define CUCUMBER_EXPRESSION_STDREGEXSTRATEGY_HPP + +#include "cucumber_cpp/library/cucumber_expression/RegexStrategy.hpp" +#include +#include +#include + +namespace cucumber_cpp::library::cucumber_expression +{ + struct StdRegexStrategy : RegexStrategy + { + explicit StdRegexStrategy(std::string_view pattern); + + [[nodiscard]] std::optional Match(std::string_view text) const override; + + private: + std::regex regex; + }; +} + +#endif diff --git a/cucumber_cpp/library/cucumber_expression/TreeRegexp.cpp b/cucumber_cpp/library/cucumber_expression/TreeRegexp.cpp index 0a2b563f..c612dbd8 100644 --- a/cucumber_cpp/library/cucumber_expression/TreeRegexp.cpp +++ b/cucumber_cpp/library/cucumber_expression/TreeRegexp.cpp @@ -1,12 +1,14 @@ #include "cucumber_cpp/library/cucumber_expression/TreeRegexp.hpp" #include "cucumber_cpp/library/cucumber_expression/Group.hpp" +#include "cucumber_cpp/library/cucumber_expression/RegexStrategy.hpp" +#include "cucumber_cpp/library/cucumber_expression/RegexStrategyFactory.hpp" #include #include #include #include #include #include -#include +#include #include #include #include @@ -161,10 +163,10 @@ namespace cucumber_cpp::library::cucumber_expression return pattern; } - ArgumentGroup GroupBuilder::Build(const std::smatch& match, std::size_t& index) const + ArgumentGroup GroupBuilder::Build(std::span> match, std::size_t& index) const { const auto groupIndex = index++; - const auto& matchGroup = match[groupIndex]; + const auto& matchGroupOpt = match[groupIndex]; const auto children = this->children | std::views::transform([&match, &index](const auto& child) { @@ -172,18 +174,38 @@ namespace cucumber_cpp::library::cucumber_expression }); return { - .value = matchGroup.matched ? std::make_optional(matchGroup.str()) : std::nullopt, - .start = matchGroup.matched ? std::make_optional(match.position(groupIndex)) : std::nullopt, + .value = matchGroupOpt ? std::make_optional(matchGroupOpt->value) : std::nullopt, + .start = matchGroupOpt ? std::make_optional(matchGroupOpt->start) : std::nullopt, + .end = matchGroupOpt ? std::make_optional(matchGroupOpt->end) : std::nullopt, .children = std::vector(children.begin(), children.end()), }; } TreeRegexp::TreeRegexp(std::string_view pattern) - : rootGroupBuilder{ CreateGroupBuilder(pattern) } - , regex{ std::string(pattern) } + : storedPattern{ pattern } + , rootGroupBuilder{ CreateGroupBuilder(storedPattern) } + , regexStrategy{ CreateRegexStrategy(storedPattern) } { } + TreeRegexp::TreeRegexp(const TreeRegexp& other) + : storedPattern{ other.storedPattern } + , rootGroupBuilder{ other.rootGroupBuilder } + , regexStrategy{ CreateRegexStrategy(storedPattern) } + { + } + + TreeRegexp& TreeRegexp::operator=(const TreeRegexp& other) + { + if (this != &other) + { + storedPattern = other.storedPattern; + rootGroupBuilder = other.rootGroupBuilder; + regexStrategy = CreateRegexStrategy(storedPattern); + } + return *this; + } + const GroupBuilder& TreeRegexp::RootBuilder() const { return rootGroupBuilder; @@ -191,11 +213,11 @@ namespace cucumber_cpp::library::cucumber_expression std::optional TreeRegexp::MatchToGroup(const std::string& text) const { - std::smatch match; - if (!std::regex_search(text, match, regex)) + const auto matchResult = regexStrategy->Match(text); + if (!matchResult) return std::nullopt; std::size_t index = 0; - return rootGroupBuilder.Build(match, index); + return rootGroupBuilder.Build(*matchResult, index); } } diff --git a/cucumber_cpp/library/cucumber_expression/TreeRegexp.hpp b/cucumber_cpp/library/cucumber_expression/TreeRegexp.hpp index bda28e47..3b3b9ffa 100644 --- a/cucumber_cpp/library/cucumber_expression/TreeRegexp.hpp +++ b/cucumber_cpp/library/cucumber_expression/TreeRegexp.hpp @@ -2,10 +2,12 @@ #define CUCUMBER_EXPRESSION_TREE_REGEXP_HPP #include "cucumber_cpp/library/cucumber_expression/Group.hpp" +#include "cucumber_cpp/library/cucumber_expression/RegexStrategy.hpp" #include #include +#include #include -#include +#include #include #include @@ -25,7 +27,7 @@ namespace cucumber_cpp::library::cucumber_expression [[nodiscard]] const std::list& Children() const; [[nodiscard]] std::string_view Pattern() const; - [[nodiscard]] ArgumentGroup Build(const std::smatch& match, std::size_t& index) const; + [[nodiscard]] ArgumentGroup Build(std::span> match, std::size_t& index) const; private: std::string_view pattern; @@ -36,14 +38,20 @@ namespace cucumber_cpp::library::cucumber_expression struct TreeRegexp { explicit TreeRegexp(std::string_view pattern); + TreeRegexp(const TreeRegexp& other); + TreeRegexp& operator=(const TreeRegexp& other); + TreeRegexp(TreeRegexp&&) = default; + TreeRegexp& operator=(TreeRegexp&&) = default; + ~TreeRegexp() = default; [[nodiscard]] const GroupBuilder& RootBuilder() const; [[nodiscard]] std::optional MatchToGroup(const std::string& text) const; private: + std::string storedPattern; GroupBuilder rootGroupBuilder; - std::regex regex; + std::unique_ptr regexStrategy; }; } diff --git a/cucumber_cpp/library/cucumber_expression/test/CMakeLists.txt b/cucumber_cpp/library/cucumber_expression/test/CMakeLists.txt index 5c4e3254..651932c8 100644 --- a/cucumber_cpp/library/cucumber_expression/test/CMakeLists.txt +++ b/cucumber_cpp/library/cucumber_expression/test/CMakeLists.txt @@ -12,11 +12,19 @@ target_sources(cucumber_cpp.library.cucumber_expression.test PRIVATE TestExpression.cpp TestExpressionParser.cpp TestExpressionTokenizer.cpp + TestRegexStrategy.cpp + TestRegexStrategyFactory.cpp TestRegularExpression.cpp TestTransformation.cpp TestTreeRegexp.cpp ) +if (CCR_HAS_RE2) + target_compile_definitions(cucumber_cpp.library.cucumber_expression.test PRIVATE + CCR_HAS_RE2 + ) +endif() + add_custom_command( TARGET cucumber_cpp.library.cucumber_expression.test POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_directory diff --git a/cucumber_cpp/library/cucumber_expression/test/TestExpression.cpp b/cucumber_cpp/library/cucumber_expression/test/TestExpression.cpp index 987a5744..8dbb2f6a 100644 --- a/cucumber_cpp/library/cucumber_expression/test/TestExpression.cpp +++ b/cucumber_cpp/library/cucumber_expression/test/TestExpression.cpp @@ -49,10 +49,10 @@ namespace cucumber_cpp::library::cucumber_expression ParameterRegistry parameterRegistry{ {} }; template - std::optional Match(std::string expr, std::string text) + std::optional Match(const std::string& expr, const std::string& text) { - Expression expression{ std::move(expr), parameterRegistry }; - auto args = expression.MatchToArguments(std::move(text)); + Expression expression{ expr, parameterRegistry }; + auto args = expression.MatchToArguments(text); if (!args.has_value()) return std::nullopt; @@ -233,14 +233,15 @@ namespace cucumber_cpp::library::cucumber_expression TEST_F(TestExpression, ThrowUnknownParameterType) { - auto expr = "I have {doesnotexist} cuke(s)"; + using namespace std::string_literals; + const auto expr = "I have {doesnotexist} cuke(s)"s; try { Expression expression{ expr, parameterRegistry }; FAIL() << "Expected UndefinedParameterTypeError to be thrown"; } - catch (UndefinedParameterTypeError e) + catch (const UndefinedParameterTypeError& e) { EXPECT_THAT(e.what(), testing::StrEq("This Cucumber Expression has a problem at column 8:\n" "\n" diff --git a/cucumber_cpp/library/cucumber_expression/test/TestRegexStrategy.cpp b/cucumber_cpp/library/cucumber_expression/test/TestRegexStrategy.cpp new file mode 100644 index 00000000..63c8eecd --- /dev/null +++ b/cucumber_cpp/library/cucumber_expression/test/TestRegexStrategy.cpp @@ -0,0 +1,117 @@ +#include "cucumber_cpp/library/cucumber_expression/StdRegexStrategy.hpp" +#ifdef CCR_HAS_RE2 +#include "cucumber_cpp/library/cucumber_expression/Re2RegexStrategy.hpp" +#endif +#include +#include +#include +#include +#include +#include + +namespace cucumber_cpp::library::cucumber_expression +{ + template + struct TestRegexStrategy : testing::Test + {}; + +#ifdef CCR_HAS_RE2 + using RegexStrategyTypes = testing::Types; +#else + using RegexStrategyTypes = testing::Types; +#endif + TYPED_TEST_SUITE(TestRegexStrategy, RegexStrategyTypes); + + TYPED_TEST(TestRegexStrategy, ReturnsNulloptWhenNoMatch) + { + TypeParam strategy{ R"__(hello)__" }; + + EXPECT_THAT(strategy.Match("world"), testing::IsFalse()); + } + + TYPED_TEST(TestRegexStrategy, ReturnsVectorWhenPatternMatches) + { + TypeParam strategy{ R"__((\d+))__" }; + + const auto result = strategy.Match("abc 42 def"); + + ASSERT_THAT(result, testing::IsTrue()); + EXPECT_THAT(result->at(0)->value, testing::StrEq("42")); // whole match + EXPECT_THAT(result->at(1)->value, testing::StrEq("42")); // first capture group + } + + TYPED_TEST(TestRegexStrategy, WholeMatchIsAtIndexZero) + { + TypeParam strategy{ R"__((\w+)\s+(\w+))__" }; + + const auto result = strategy.Match("hello world"); + + ASSERT_THAT(result, testing::IsTrue()); + ASSERT_THAT(result->size(), testing::Eq(3)); + EXPECT_THAT(result->at(0)->value, testing::StrEq("hello world")); // whole match + EXPECT_THAT(result->at(1)->value, testing::StrEq("hello")); // first capture group + EXPECT_THAT(result->at(2)->value, testing::StrEq("world")); // second capture group + } + + TYPED_TEST(TestRegexStrategy, MatchesNegativeInteger) + { + TypeParam strategy{ R"__((-?\d+))__" }; + + const auto result = strategy.Match("-22"); + + ASSERT_THAT(result, testing::IsTrue()); + EXPECT_THAT(result->at(1)->value, testing::StrEq("-22")); + } + + TYPED_TEST(TestRegexStrategy, MatchesEmptyCapture) + { + TypeParam strategy{ R"__(^The value equals "([^"]*)"$)__" }; + + const auto result = strategy.Match(R"__(The value equals "")__"); + + ASSERT_THAT(result, testing::IsTrue()); + EXPECT_THAT(result->at(1)->value, testing::StrEq("")); + } + + TYPED_TEST(TestRegexStrategy, ReturnsStartPositionOfWholeMatch) + { + TypeParam strategy{ R"__((\d+))__" }; + + const auto result = strategy.Match("abc 42 def"); + + ASSERT_THAT(result, testing::IsTrue()); + EXPECT_THAT(result->at(0)->start, testing::Eq(4)); // "42" starts at index 4 + EXPECT_THAT(result->at(0)->end, testing::Eq(6)); // "42" ends after index 6 + } + + TYPED_TEST(TestRegexStrategy, ReturnsStartPositionOfCaptureGroup) + { + TypeParam strategy{ R"__((\w+)\s+(\w+))__" }; + + const auto result = strategy.Match("hello world"); + + ASSERT_THAT(result, testing::IsTrue()); + EXPECT_THAT(result->at(1)->start, testing::Eq(0)); // "hello" starts at 0 + EXPECT_THAT(result->at(1)->end, testing::Eq(5)); // "hello" ends at 5 + EXPECT_THAT(result->at(2)->start, testing::Eq(6)); // "world" starts at 6 + EXPECT_THAT(result->at(2)->end, testing::Eq(11)); // "world" ends at 11 + } + + TYPED_TEST(TestRegexStrategy, UnmatchedGroupIsNullopt) + { + TypeParam strategy{ R"__(^Something( with an optional argument)?)__" }; + + const auto result = strategy.Match("Something"); + + ASSERT_THAT(result, testing::IsTrue()); + EXPECT_THAT(result->at(1), testing::IsFalse()); + } + +#ifdef CCR_HAS_RE2 + TEST(Re2RegexStrategy, ThrowsOnInvalidPattern) + { + EXPECT_THROW(Re2RegexStrategy{ R"__([invalid)__" }, std::invalid_argument); + } +#endif +} + diff --git a/cucumber_cpp/library/cucumber_expression/test/TestRegexStrategyFactory.cpp b/cucumber_cpp/library/cucumber_expression/test/TestRegexStrategyFactory.cpp new file mode 100644 index 00000000..6e991c46 --- /dev/null +++ b/cucumber_cpp/library/cucumber_expression/test/TestRegexStrategyFactory.cpp @@ -0,0 +1,51 @@ +#include "cucumber_cpp/library/cucumber_expression/RegexStrategyFactory.hpp" +#include "cucumber_cpp/library/cucumber_expression/StdRegexStrategy.hpp" +#ifdef CCR_HAS_RE2 +#include "cucumber_cpp/library/cucumber_expression/Re2RegexStrategy.hpp" +#endif +#include +#include + +namespace cucumber_cpp::library::cucumber_expression +{ + TEST(RegexStrategyFactory, ReturnsNonNullStrategy) + { + const auto strategy = CreateRegexStrategy(R"__((\d+))__"); + + EXPECT_THAT(strategy, testing::NotNull()); + } + + TEST(RegexStrategyFactory, ReturnedStrategyCanMatch) + { + const auto strategy = CreateRegexStrategy(R"__((\d+))__"); + + const auto result = strategy->Match("abc 42 def"); + + ASSERT_THAT(result, testing::IsTrue()); + EXPECT_THAT(result->at(1)->value, testing::StrEq("42")); + } + +#ifdef CCR_HAS_RE2 + TEST(RegexStrategyFactory, ReturnsRe2StrategyForSupportedPattern) + { + const auto strategy = CreateRegexStrategy(R"__((\d+))__"); + + EXPECT_THAT(dynamic_cast(strategy.get()), testing::NotNull()); + } + + TEST(RegexStrategyFactory, FallsBackToStdStrategyForUnsupportedPattern) + { + // lookahead (?=) is not supported by RE2 + const auto strategy = CreateRegexStrategy(R"__((?=.*\d)\d+)__"); + + EXPECT_THAT(dynamic_cast(strategy.get()), testing::NotNull()); + } +#else + TEST(RegexStrategyFactory, ReturnsStdStrategy) + { + const auto strategy = CreateRegexStrategy(R"__((\d+))__"); + + EXPECT_THAT(dynamic_cast(strategy.get()), testing::NotNull()); + } +#endif +} diff --git a/cucumber_cpp/library/support/StepRegistry.cpp b/cucumber_cpp/library/support/StepRegistry.cpp index 123aed19..78a6410e 100644 --- a/cucumber_cpp/library/support/StepRegistry.cpp +++ b/cucumber_cpp/library/support/StepRegistry.cpp @@ -71,7 +71,7 @@ namespace cucumber_cpp::library::support return idToDefinitionMap.at(id)->factory; } - StepRegistry::Definition StepRegistry::GetDefinitionById(const std::string& id) const + const StepRegistry::Definition& StepRegistry::GetDefinitionById(const std::string& id) const { return *idToDefinitionMap.at(id); } diff --git a/cucumber_cpp/library/support/StepRegistry.hpp b/cucumber_cpp/library/support/StepRegistry.hpp index e9d96081..9b07c30d 100644 --- a/cucumber_cpp/library/support/StepRegistry.hpp +++ b/cucumber_cpp/library/support/StepRegistry.hpp @@ -106,7 +106,7 @@ namespace cucumber_cpp::library::support [[nodiscard]] std::size_t Size() const; [[nodiscard]] util::StepFactory GetFactoryById(const std::string& id) const; - [[nodiscard]] Definition GetDefinitionById(const std::string& id) const; + [[nodiscard]] const Definition& GetDefinitionById(const std::string& id) const; [[nodiscard]] const std::list& StepDefinitions() const; diff --git a/external/google/CMakeLists.txt b/external/google/CMakeLists.txt index 73deebc3..b307909f 100644 --- a/external/google/CMakeLists.txt +++ b/external/google/CMakeLists.txt @@ -1 +1,5 @@ +if (CCR_USE_RE2) + add_subdirectory(abseil) # before re2 + add_subdirectory(re2) +endif() add_subdirectory(googletest) diff --git a/external/google/abseil/CMakeLists.txt b/external/google/abseil/CMakeLists.txt new file mode 100644 index 00000000..1713d7d4 --- /dev/null +++ b/external/google/abseil/CMakeLists.txt @@ -0,0 +1,11 @@ +FetchContent_Declare( + absl + GIT_REPOSITORY https://github.com/abseil/abseil-cpp.git + GIT_TAG 0cf0a5c9d12cc3783363ab20f11613e69fd04c9a # 20250814.2 + OVERRIDE_FIND_PACKAGE +) + +set(ABSL_PROPAGATE_CXX_STD On CACHE BOOL "" FORCE) +set(ABSL_ENABLE_INSTALL On CACHE BOOL "" FORCE) + +FetchContent_MakeAvailable(absl) diff --git a/external/google/re2/CMakeLists.txt b/external/google/re2/CMakeLists.txt new file mode 100644 index 00000000..0cd9b7e0 --- /dev/null +++ b/external/google/re2/CMakeLists.txt @@ -0,0 +1,10 @@ +FetchContent_Declare( + re2 + GIT_REPOSITORY https://github.com/google/re2.git + GIT_TAG 0f6c07eae69151e606acb3d9232750c3442dff23 # 2025-08-12 + OVERRIDE_FIND_PACKAGE +) + +set(RE2_BUILD_TESTING Off CACHE BOOL "" FORCE) + +FetchContent_MakeAvailable(re2)