From 9ecae7902c3315864882bbe6da1f2f65cc2df0b6 Mon Sep 17 00:00:00 2001 From: "Timmer, Daan" Date: Wed, 10 Jun 2026 13:36:05 +0000 Subject: [PATCH 1/9] You should catch exceptions while constructing a StepBody/ExecutionBody Fixes #348 --- .../library/runtime/TestCaseRunner.cpp | 24 +++- cucumber_cpp/library/util/Body.cpp | 105 ++++-------------- cucumber_cpp/library/util/Body.hpp | 12 ++ .../util/ExecuteAndCatchExceptions.hpp | 94 ++++++++++++++++ 4 files changed, 144 insertions(+), 91 deletions(-) create mode 100644 cucumber_cpp/library/util/ExecuteAndCatchExceptions.hpp diff --git a/cucumber_cpp/library/runtime/TestCaseRunner.cpp b/cucumber_cpp/library/runtime/TestCaseRunner.cpp index 7de3dbf6..0d714109 100644 --- a/cucumber_cpp/library/runtime/TestCaseRunner.cpp +++ b/cucumber_cpp/library/runtime/TestCaseRunner.cpp @@ -24,8 +24,11 @@ #include "cucumber_cpp/library/util/Body.hpp" #include "cucumber_cpp/library/util/Broadcaster.hpp" #include "cucumber_cpp/library/util/Duration.hpp" +#include "cucumber_cpp/library/util/ExecuteAndCatchExceptions.hpp" #include "cucumber_cpp/library/util/GetWorstTestStepResult.hpp" #include "cucumber_cpp/library/util/HookData.hpp" +#include "cucumber_cpp/library/util/TestStepResult.hpp" +#include "cucumber_cpp/library/util/TestStepResultStatus.hpp" #include "cucumber_cpp/library/util/Timestamp.hpp" #include "cucumber_cpp/library/util/TransformDocString.hpp" #include "cucumber_cpp/library/util/TransformPickleTag.hpp" @@ -221,11 +224,22 @@ namespace cucumber_cpp::library::runtime const auto& docString = pickleStep.argument ? pickleStep.argument->doc_string : std::nullopt; const auto& definition = stepDefinitions.front(); - const auto result = InvokeStep(definition.factory( - NestedTestCaseRunner{ 0, supportCodeLibrary, broadcaster, testCaseContext, util::TransformTestStepStarted(testStepStarted) }, - broadcaster, testCaseContext, util::TransformTestStepStarted(testStepStarted), util::TransformTable(dataTable), util::TransformDocString(docString)), - testStep.step_match_arguments_lists->front()); - stepResults.push_back(result); + + util::TestStepResult testStepResult{ .status = util::TestStepResultStatus::PASSED }; + util::CucumberResultReporter reportListener{ testStepResult }; + + try + { + auto step = definition.factory( + NestedTestCaseRunner{ 0, supportCodeLibrary, broadcaster, testCaseContext, util::TransformTestStepStarted(testStepStarted) }, + broadcaster, testCaseContext, util::TransformTestStepStarted(testStepStarted), util::TransformTable(dataTable), util::TransformDocString(docString)); + + stepResults.push_back(InvokeStep(std::move(step))); + } + catch (...) + { + stepResults.push_back(util::TransformTestStepResult(util::HandleErrors(testStepResult))); + } } const auto afterStepHookResults = RunStepHooks(pickleStep, util::HookType::afterStep, testCaseContext, testStepStarted); diff --git a/cucumber_cpp/library/util/Body.cpp b/cucumber_cpp/library/util/Body.cpp index e3f1b35e..b770e583 100644 --- a/cucumber_cpp/library/util/Body.cpp +++ b/cucumber_cpp/library/util/Body.cpp @@ -3,6 +3,7 @@ #include "cucumber/gherkin/demangle.hpp" #include "cucumber/messages/test_step_result_status.hpp" #include "cucumber_cpp/library/util/Duration.hpp" +#include "cucumber_cpp/library/util/ExecuteAndCatchExceptions.hpp" #include "cucumber_cpp/library/util/NestedTestCaseRunnerError.hpp" #include "cucumber_cpp/library/util/TestException.hpp" #include "cucumber_cpp/library/util/TestStepResult.hpp" @@ -17,38 +18,28 @@ 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; + testStepResult.status = util::TestStepResultStatus::FAILED; - auto fileName = std::filesystem::relative(testPartResult.file_name(), std::filesystem::current_path()).string(); + auto fileName = std::filesystem::relative(testPartResult.file_name(), std::filesystem::current_path()).string(); - 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()); - } - - 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 Body::ExecuteAndCatchExceptions(const ExecuteArgs& args) @@ -56,73 +47,15 @@ namespace cucumber_cpp::library::util TestStepResult testStepResult{ .status = TestStepResultStatus::PASSED }; CucumberResultReporter reportListener{ testStepResult }; - const auto startTime = Stopwatch::Instance().Start(); try { Execute(args); - } - catch (const util::NestedTestCaseRunnerError& e) - { - testStepResult.status = TestStepResultStatus::FAILED; - if (e.status.status != cucumber::messages::test_step_result_status::PASSED) - { - const auto offset = std::string(e.nesting, ' '); - - if (e.status.message.has_value()) - testStepResult.message = fmt::format(R"({0} {1} nested step: "* {2}")" - "\n{0} {3}", - offset, - cucumber::messages::to_string(e.status.status), - e.text, - e.status.message.value()); - else - testStepResult.message = fmt::format(R"({0} {1} nested step: "* {2}")", - offset, - cucumber::messages::to_string(e.status.status), - e.text); - } - } - catch (const StepSkipped& e) - { - testStepResult.status = TestStepResultStatus::SKIPPED; - if (!e.message.empty()) - testStepResult.message = e.message; - } - catch (const StepPending& e) - { - testStepResult.status = TestStepResultStatus::PENDING; - if (!e.message.empty()) - testStepResult.message = e.message; - } - catch ([[maybe_unused]] const FatalError& error) - { - testStepResult.status = TestStepResultStatus::FAILED; - } - catch (std::exception& e) - { - testStepResult.status = TestStepResultStatus::FAILED; - testStepResult.exception = TestException{ - .type = cucumber::gherkin::detail::demangle(typeid(e).name()).get(), - .message = e.what(), - }; + return testStepResult; } catch (...) { - testStepResult.status = TestStepResultStatus::FAILED; - testStepResult.exception = TestException{ - .type = "unknown", - .message = "unknown exception", - }; + return HandleErrors(testStepResult); } - - auto nanoseconds = Stopwatch::Instance().Duration(startTime); - static constexpr std::size_t nanosecondsPerSecond = 1e9; - testStepResult.duration = { - .seconds = nanoseconds.count() / nanosecondsPerSecond, - .nanos = nanoseconds.count() % nanosecondsPerSecond, - }; - - return testStepResult; } } diff --git a/cucumber_cpp/library/util/Body.hpp b/cucumber_cpp/library/util/Body.hpp index 4c57b257..6ee3fc1b 100644 --- a/cucumber_cpp/library/util/Body.hpp +++ b/cucumber_cpp/library/util/Body.hpp @@ -3,7 +3,9 @@ #include "cucumber_cpp/library/cucumber_expression/ParameterRegistry.hpp" #include "cucumber_cpp/library/util/TestStepResult.hpp" +#include "gtest/gtest.h" #include +#include #include #include #include @@ -50,6 +52,16 @@ namespace cucumber_cpp::library::util using ExecuteArgs = std::vector; + struct CucumberResultReporter : public testing::ScopedFakeTestPartResultReporter + { + explicit CucumberResultReporter(util::TestStepResult& testStepResult); + + void ReportTestPartResult(const testing::TestPartResult& testPartResult) override; + + private: + util::TestStepResult& testStepResult; + }; + struct Body { virtual ~Body() = default; diff --git a/cucumber_cpp/library/util/ExecuteAndCatchExceptions.hpp b/cucumber_cpp/library/util/ExecuteAndCatchExceptions.hpp new file mode 100644 index 00000000..87152b65 --- /dev/null +++ b/cucumber_cpp/library/util/ExecuteAndCatchExceptions.hpp @@ -0,0 +1,94 @@ +#ifndef UTIL_EXECUTE_AND_CATCH_EXCEPTIONS_HPP +#define UTIL_EXECUTE_AND_CATCH_EXCEPTIONS_HPP + +#include "cucumber/gherkin/demangle.hpp" +#include "cucumber/messages/test_step_result_status.hpp" +#include "cucumber_cpp/library/util/Body.hpp" +#include "cucumber_cpp/library/util/Duration.hpp" +#include "cucumber_cpp/library/util/NestedTestCaseRunnerError.hpp" +#include "cucumber_cpp/library/util/TestException.hpp" +#include "cucumber_cpp/library/util/TestStepResult.hpp" +#include "cucumber_cpp/library/util/TestStepResultStatus.hpp" +#include "fmt/format.h" +#include +#include +#include +#include + +namespace cucumber_cpp::library::util +{ + [[nodiscard]] inline TestStepResult HandleErrors(TestStepResult testStepResult = { .status = TestStepResultStatus::PASSED }) noexcept + { + const auto startTime = Stopwatch::Instance().Start(); + + try + { + throw; + } + catch (const util::NestedTestCaseRunnerError& e) + { + testStepResult.status = TestStepResultStatus::FAILED; + + if (e.status.status != cucumber::messages::test_step_result_status::PASSED) + { + const auto offset = std::string(e.nesting, ' '); + + if (e.status.message.has_value()) + testStepResult.message = fmt::format(R"({0} {1} nested step: "* {2}")" + "\n{0} {3}", + offset, + cucumber::messages::to_string(e.status.status), + e.text, + e.status.message.value()); + else + testStepResult.message = fmt::format(R"({0} {1} nested step: "* {2}")", + offset, + cucumber::messages::to_string(e.status.status), + e.text); + } + } + catch (const StepSkipped& e) + { + testStepResult.status = TestStepResultStatus::SKIPPED; + if (!e.message.empty()) + testStepResult.message = e.message; + } + catch (const StepPending& e) + { + testStepResult.status = TestStepResultStatus::PENDING; + if (!e.message.empty()) + testStepResult.message = e.message; + } + catch ([[maybe_unused]] const FatalError& error) + { + testStepResult.status = TestStepResultStatus::FAILED; + } + catch (std::exception& e) + { + testStepResult.status = TestStepResultStatus::FAILED; + testStepResult.exception = TestException{ + .type = cucumber::gherkin::detail::demangle(typeid(e).name()).get(), + .message = e.what(), + }; + } + catch (...) + { + testStepResult.status = TestStepResultStatus::FAILED; + testStepResult.exception = TestException{ + .type = "unknown", + .message = "unknown exception", + }; + } + + auto nanoseconds = Stopwatch::Instance().Duration(startTime); + static constexpr std::size_t nanosecondsPerSecond = 1e9; + testStepResult.duration = { + .seconds = nanoseconds.count() / nanosecondsPerSecond, + .nanos = nanoseconds.count() % nanosecondsPerSecond, + }; + + return testStepResult; + } +} + +#endif From 4bbaa87419a0ffbbaab2344fbe26de7a0d7a7b8a Mon Sep 17 00:00:00 2001 From: "Timmer, Daan" Date: Thu, 2 Jul 2026 07:14:47 +0000 Subject: [PATCH 2/9] You should catch exceptions while constructing a StepBody/ExecutionBody Fixes #348 --- cucumber_cpp/Steps.hpp | 1 + .../test_step_fixture_failure.feature | 7 ++++ cucumber_cpp/acceptance_test/steps/Steps.cpp | 19 +++++++++- .../acceptance_test/steps/UsedUnused.cpp | 2 +- cucumber_cpp/acceptance_test/test.bats | 8 ++++ cucumber_cpp/example/steps/Steps.cpp | 2 +- .../library/runtime/NestedTestCaseRunner.cpp | 12 ++++-- .../library/runtime/TestCaseRunner.cpp | 38 +++++++++---------- cucumber_cpp/library/runtime/Worker.cpp | 9 ++++- cucumber_cpp/library/util/Body.cpp | 26 +++++++------ cucumber_cpp/library/util/Body.hpp | 13 +++++-- .../util/ExecuteAndCatchExceptions.hpp | 13 +------ 12 files changed, 98 insertions(+), 52 deletions(-) create mode 100644 cucumber_cpp/acceptance_test/features/test_step_fixture_failure.feature diff --git a/cucumber_cpp/Steps.hpp b/cucumber_cpp/Steps.hpp index 1db0a855..212ddd39 100644 --- a/cucumber_cpp/Steps.hpp +++ b/cucumber_cpp/Steps.hpp @@ -8,6 +8,7 @@ #include "cucumber_cpp/library/cucumber_expression/MatchRange.hpp" #include "cucumber_cpp/library/cucumber_expression/ParameterRegistry.hpp" #include "cucumber_cpp/library/engine/ExecutionContext.hpp" +#include "cucumber_cpp/library/engine/Hook.hpp" #include "cucumber_cpp/library/util/DocString.hpp" #include "cucumber_cpp/library/util/Table.hpp" diff --git a/cucumber_cpp/acceptance_test/features/test_step_fixture_failure.feature b/cucumber_cpp/acceptance_test/features/test_step_fixture_failure.feature new file mode 100644 index 00000000..91cee723 --- /dev/null +++ b/cucumber_cpp/acceptance_test/features/test_step_fixture_failure.feature @@ -0,0 +1,7 @@ +@fail_step_fixture +Feature: Simple feature file + Rule: Test rule + Scenario: Test scenario without failing step fixture + Given step fixture does not fail + Scenario: Test with failing step fixture + Given step fixture fails diff --git a/cucumber_cpp/acceptance_test/steps/Steps.cpp b/cucumber_cpp/acceptance_test/steps/Steps.cpp index a6cc3119..b3fd06fe 100644 --- a/cucumber_cpp/acceptance_test/steps/Steps.cpp +++ b/cucumber_cpp/acceptance_test/steps/Steps.cpp @@ -1,4 +1,4 @@ -#include "cucumber_cpp/CucumberCpp.hpp" +#include "cucumber_cpp/Steps.hpp" #include "fmt/format.h" #include "gmock/gmock.h" #include "gtest/gtest.h" @@ -140,3 +140,20 @@ GIVEN(R"(I attach a link to {string})", (const std::string& url)) { Link(url, "title"); } + +GIVEN(R"(step fixture does not fail)") +{ + // +} + +struct FailingStepFixture : cucumber_cpp::StepBase +{ + using StepBase::StepBase; + + bool nonExistentKey = context.Get("nonExistentKey"); +}; + +GIVEN_F(FailingStepFixture, R"(step fixture fails)") +{ + // empty +} diff --git a/cucumber_cpp/acceptance_test/steps/UsedUnused.cpp b/cucumber_cpp/acceptance_test/steps/UsedUnused.cpp index 3abf8193..0d444d51 100644 --- a/cucumber_cpp/acceptance_test/steps/UsedUnused.cpp +++ b/cucumber_cpp/acceptance_test/steps/UsedUnused.cpp @@ -1,4 +1,4 @@ -#include "cucumber_cpp/CucumberCpp.hpp" +#include "cucumber_cpp/Steps.hpp" STEP("This step is unused") { diff --git a/cucumber_cpp/acceptance_test/test.bats b/cucumber_cpp/acceptance_test/test.bats index d7ba7f2f..8803cfef 100644 --- a/cucumber_cpp/acceptance_test/test.bats +++ b/cucumber_cpp/acceptance_test/test.bats @@ -222,3 +222,11 @@ teardown() { assert_output --partial "| this step is used | - |" assert_output --partial "| This step is unused | UNUSED |" } + +@test "Test failure in step fixture results in error" { + run $acceptance_test --format summary --format-options "{ \"summary\": {\"theme\":\"plain\"} }" --tags "@fail_step_fixture" -- cucumber_cpp/acceptance_test/features + assert_failure + assert_output --partial "key not found: \"nonExistentKey\"" + assert_output --partial "2 scenarios 1 passed, 1 failed" + assert_output --partial "2 steps 1 passed, 1 failed" +} diff --git a/cucumber_cpp/example/steps/Steps.cpp b/cucumber_cpp/example/steps/Steps.cpp index 76591a60..e061cb33 100644 --- a/cucumber_cpp/example/steps/Steps.cpp +++ b/cucumber_cpp/example/steps/Steps.cpp @@ -1,5 +1,5 @@ #include "cucumber_cpp/library/Steps.hpp" -#include "cucumber_cpp/CucumberCpp.hpp" +#include "cucumber_cpp/Steps.hpp" #include "cucumber_cpp/library/Context.hpp" #include "gmock/gmock.h" #include "gtest/gtest.h" diff --git a/cucumber_cpp/library/runtime/NestedTestCaseRunner.cpp b/cucumber_cpp/library/runtime/NestedTestCaseRunner.cpp index 08e5a0d7..9cc1c6f8 100644 --- a/cucumber_cpp/library/runtime/NestedTestCaseRunner.cpp +++ b/cucumber_cpp/library/runtime/NestedTestCaseRunner.cpp @@ -70,9 +70,10 @@ namespace cucumber_cpp::library::runtime return testStep; } - void Invoke(std::size_t nesting, const std::string& step, std::unique_ptr body, const cucumber::messages::step_match_arguments_list& args) + 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 = body->ExecuteAndCatchExceptions(util::StepMatchArgumentsListToExecuteArgs(args)); + const auto status = util::ConstructAndExecute(bodyFactory, util::StepMatchArgumentsListToExecuteArgs(args)); + if (status.status != util::TestStepResultStatus::PASSED) throw util::NestedTestCaseRunnerError{ .nesting = nesting, @@ -105,7 +106,12 @@ namespace cucumber_cpp::library::runtime else { const auto& definition = stepDefinitions.front(); - Invoke(nesting, step, definition.factory(NestedTestCaseRunner{ nesting, supportCodeLibrary, broadcaster, testCaseContext, testStepStarted }, broadcaster, testCaseContext, testStepStarted, util::TransformTable(dataTable), util::TransformDocString(docString)), testStep.step_match_arguments_lists->front()); + NestedTestCaseRunner nestedTestCaseRunner{ nesting, supportCodeLibrary, broadcaster, testCaseContext, testStepStarted }; + const util::BodyFactory bodyFactory = [&nestedTestCaseRunner, nesting, &definition, &supportCodeLibrary, &broadcaster, &testCaseContext, &testStepStarted, &dataTable, &docString] + { + return definition.factory(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 0d714109..30b6d6b9 100644 --- a/cucumber_cpp/library/runtime/TestCaseRunner.cpp +++ b/cucumber_cpp/library/runtime/TestCaseRunner.cpp @@ -24,11 +24,8 @@ #include "cucumber_cpp/library/util/Body.hpp" #include "cucumber_cpp/library/util/Broadcaster.hpp" #include "cucumber_cpp/library/util/Duration.hpp" -#include "cucumber_cpp/library/util/ExecuteAndCatchExceptions.hpp" #include "cucumber_cpp/library/util/GetWorstTestStepResult.hpp" #include "cucumber_cpp/library/util/HookData.hpp" -#include "cucumber_cpp/library/util/TestStepResult.hpp" -#include "cucumber_cpp/library/util/TestStepResultStatus.hpp" #include "cucumber_cpp/library/util/Timestamp.hpp" #include "cucumber_cpp/library/util/TransformDocString.hpp" #include "cucumber_cpp/library/util/TransformPickleTag.hpp" @@ -50,9 +47,9 @@ namespace cucumber_cpp::library::runtime { namespace { - cucumber::messages::test_step_result InvokeStep(std::unique_ptr body, const cucumber::messages::step_match_arguments_list& args = {}) + cucumber::messages::test_step_result InvokeStep(const util::BodyFactory& bodyFactory, const cucumber::messages::step_match_arguments_list& args) { - return util::TransformTestStepResult(body->ExecuteAndCatchExceptions(util::StepMatchArgumentsListToExecuteArgs(args))); + return util::TransformTestStepResult(util::ConstructAndExecute(bodyFactory, util::StepMatchArgumentsListToExecuteArgs(args))); } } @@ -162,7 +159,12 @@ namespace cucumber_cpp::library::runtime .status = cucumber::messages::test_step_result_status::SKIPPED, }; - return InvokeStep(hookDefinition.factory(broadcaster, testCaseContext, util::TransformTestStepStarted(testStepStarted), hasError)); + const util::BodyFactory bodyFactory = [&hookDefinition, this, &testCaseContext, &testStepStarted, hasError] + { + return hookDefinition.factory(broadcaster, testCaseContext, util::TransformTestStepStarted(testStepStarted), hasError); + }; + + return InvokeStep(bodyFactory, {}); } std::vector TestCaseRunner::RunStepHooks(const cucumber::messages::pickle_step& /*pickleStep*/, util::HookType hookType, Context& testCaseContext, const cucumber::messages::test_step_started& testStepStarted) @@ -174,7 +176,12 @@ namespace cucumber_cpp::library::runtime for (const auto& id : ids) { const auto& definition = supportCodeLibrary.hookRegistry.GetDefinitionById(id); - results.emplace_back(InvokeStep(definition.factory(broadcaster, testCaseContext, util::TransformTestStepStarted(testStepStarted), false))); + const auto bodyFactory = [&definition, this, &testCaseContext, &testStepStarted] + { + return definition.factory(broadcaster, testCaseContext, util::TransformTestStepStarted(testStepStarted), false); + }; + + results.emplace_back(InvokeStep(bodyFactory, {})); } return results; @@ -225,21 +232,14 @@ namespace cucumber_cpp::library::runtime const auto& definition = stepDefinitions.front(); - util::TestStepResult testStepResult{ .status = util::TestStepResultStatus::PASSED }; - util::CucumberResultReporter reportListener{ testStepResult }; + NestedTestCaseRunner nestedTestCaseRunner{ 0, supportCodeLibrary, broadcaster, testCaseContext, util::TransformTestStepStarted(testStepStarted) }; - try + const util::BodyFactory bodyFactory = [this, &definition, &nestedTestCaseRunner, &testCaseContext, &testStepStarted, &dataTable, &docString] { - auto step = definition.factory( - NestedTestCaseRunner{ 0, supportCodeLibrary, broadcaster, testCaseContext, util::TransformTestStepStarted(testStepStarted) }, - broadcaster, testCaseContext, util::TransformTestStepStarted(testStepStarted), util::TransformTable(dataTable), util::TransformDocString(docString)); + return definition.factory(nestedTestCaseRunner, broadcaster, testCaseContext, util::TransformTestStepStarted(testStepStarted), util::TransformTable(dataTable), util::TransformDocString(docString)); + }; - stepResults.push_back(InvokeStep(std::move(step))); - } - catch (...) - { - stepResults.push_back(util::TransformTestStepResult(util::HandleErrors(testStepResult))); - } + stepResults.push_back(InvokeStep(bodyFactory, testStep.step_match_arguments_lists->front())); } const auto afterStepHookResults = RunStepHooks(pickleStep, util::HookType::afterStep, testCaseContext, testStepStarted); diff --git a/cucumber_cpp/library/runtime/Worker.cpp b/cucumber_cpp/library/runtime/Worker.cpp index 2729591d..9c591dc8 100644 --- a/cucumber_cpp/library/runtime/Worker.cpp +++ b/cucumber_cpp/library/runtime/Worker.cpp @@ -16,6 +16,7 @@ #include "cucumber_cpp/library/support/HookRegistry.hpp" #include "cucumber_cpp/library/support/SupportCodeLibrary.hpp" #include "cucumber_cpp/library/support/Types.hpp" +#include "cucumber_cpp/library/util/Body.hpp" #include "cucumber_cpp/library/util/Broadcaster.hpp" #include "cucumber_cpp/library/util/GetWorstTestStepResult.hpp" #include "cucumber_cpp/library/util/HookData.hpp" @@ -189,9 +190,15 @@ namespace cucumber_cpp::library::runtime broadcaster.BroadcastEvent({ .test_run_hook_started = testRunHookStarted }); cucumber::messages::test_step_result result{ .duration{ .seconds = 0, .nanos = 0 }, .status = cucumber::messages::test_step_result_status::SKIPPED }; + if (!options.dryRun) { - result = util::TransformTestStepResult(definition.factory(broadcaster, context, util::TransformTestRunHookStarted(testRunHookStarted), false)->ExecuteAndCatchExceptions()); + const util::BodyFactory bodyFactory = [&definition, this, &context, &testRunHookStarted] + { + return definition.factory(broadcaster, context, util::TransformTestRunHookStarted(testRunHookStarted), false); + }; + + result = util::TransformTestStepResult(util::ConstructAndExecute(bodyFactory)); if (result.status != cucumber::messages::test_step_result_status::PASSED && options.failGlobalHookFast) throw GlobalHookError{ fmt::format("Global Hook Failed: {}\nresult:{}", util::TransformHookData(definition.data).to_string(), result.to_string()) }; diff --git a/cucumber_cpp/library/util/Body.cpp b/cucumber_cpp/library/util/Body.cpp index b770e583..ad711fa1 100644 --- a/cucumber_cpp/library/util/Body.cpp +++ b/cucumber_cpp/library/util/Body.cpp @@ -1,20 +1,16 @@ #include "cucumber_cpp/library/util/Body.hpp" -#include "cucumber/gherkin/demangle.hpp" -#include "cucumber/messages/test_step_result_status.hpp" #include "cucumber_cpp/library/util/Duration.hpp" #include "cucumber_cpp/library/util/ExecuteAndCatchExceptions.hpp" -#include "cucumber_cpp/library/util/NestedTestCaseRunnerError.hpp" -#include "cucumber_cpp/library/util/TestException.hpp" #include "cucumber_cpp/library/util/TestStepResult.hpp" #include "cucumber_cpp/library/util/TestStepResultStatus.hpp" #include "fmt/format.h" #include "gtest/gtest.h" #include -#include #include +#include #include -#include +#include namespace cucumber_cpp::library::util { @@ -42,20 +38,28 @@ namespace cucumber_cpp::library::util throw FatalError{ testPartResult.message() }; } - TestStepResult Body::ExecuteAndCatchExceptions(const ExecuteArgs& args) + TestStepResult ConstructAndExecute(const std::function()>& bodyFactory, const ExecuteArgs& args) { + const auto startTime = Stopwatch::Instance().Start(); TestStepResult testStepResult{ .status = TestStepResultStatus::PASSED }; CucumberResultReporter reportListener{ testStepResult }; try { - Execute(args); - - return testStepResult; + bodyFactory()->Execute(args); } catch (...) { - return HandleErrors(testStepResult); + HandleErrors(testStepResult); } + + auto nanoseconds = Stopwatch::Instance().Duration(startTime); + static constexpr std::size_t nanosecondsPerSecond = 1e9; + testStepResult.duration = { + .seconds = nanoseconds.count() / nanosecondsPerSecond, + .nanos = nanoseconds.count() % nanosecondsPerSecond, + }; + + return testStepResult; } } diff --git a/cucumber_cpp/library/util/Body.hpp b/cucumber_cpp/library/util/Body.hpp index 6ee3fc1b..d99ede80 100644 --- a/cucumber_cpp/library/util/Body.hpp +++ b/cucumber_cpp/library/util/Body.hpp @@ -5,7 +5,9 @@ #include "cucumber_cpp/library/util/TestStepResult.hpp" #include "gtest/gtest.h" #include +#include #include +#include #include #include #include @@ -62,14 +64,19 @@ namespace cucumber_cpp::library::util util::TestStepResult& testStepResult; }; + struct Body; + + using BodyFactory = std::function()>; + TestStepResult ConstructAndExecute(const BodyFactory& bodyFactory, const ExecuteArgs& args = {}); + struct Body { virtual ~Body() = default; - TestStepResult ExecuteAndCatchExceptions(const ExecuteArgs& args = {}); - - protected: + private: virtual void Execute(const ExecuteArgs& args) = 0; + + friend TestStepResult ConstructAndExecute(const BodyFactory& bodyFactory, const ExecuteArgs& args); }; } diff --git a/cucumber_cpp/library/util/ExecuteAndCatchExceptions.hpp b/cucumber_cpp/library/util/ExecuteAndCatchExceptions.hpp index 87152b65..8fdcf570 100644 --- a/cucumber_cpp/library/util/ExecuteAndCatchExceptions.hpp +++ b/cucumber_cpp/library/util/ExecuteAndCatchExceptions.hpp @@ -17,10 +17,8 @@ namespace cucumber_cpp::library::util { - [[nodiscard]] inline TestStepResult HandleErrors(TestStepResult testStepResult = { .status = TestStepResultStatus::PASSED }) noexcept + inline void HandleErrors(TestStepResult& testStepResult) noexcept { - const auto startTime = Stopwatch::Instance().Start(); - try { throw; @@ -79,15 +77,6 @@ namespace cucumber_cpp::library::util .message = "unknown exception", }; } - - auto nanoseconds = Stopwatch::Instance().Duration(startTime); - static constexpr std::size_t nanosecondsPerSecond = 1e9; - testStepResult.duration = { - .seconds = nanoseconds.count() / nanosecondsPerSecond, - .nanos = nanoseconds.count() % nanosecondsPerSecond, - }; - - return testStepResult; } } From 41b37ed4939b203224d5532146969184900076b7 Mon Sep 17 00:00:00 2001 From: "Timmer, Daan" Date: Thu, 2 Jul 2026 14:29:48 +0000 Subject: [PATCH 3/9] fix: catch exceptions during StepBody construction and improve error handling --- cucumber_cpp/library/runtime/NestedTestCaseRunner.cpp | 4 ++-- cucumber_cpp/library/util/ExecuteAndCatchExceptions.hpp | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/cucumber_cpp/library/runtime/NestedTestCaseRunner.cpp b/cucumber_cpp/library/runtime/NestedTestCaseRunner.cpp index 9cc1c6f8..924ea075 100644 --- a/cucumber_cpp/library/runtime/NestedTestCaseRunner.cpp +++ b/cucumber_cpp/library/runtime/NestedTestCaseRunner.cpp @@ -89,7 +89,7 @@ namespace cucumber_cpp::library::runtime return supportCodeLibrary.stepRegistry.GetDefinitionById(id); }); - if (testStep.step_definition_ids->size() == 0) + if (testStep.step_definition_ids->empty()) throw util::NestedTestCaseRunnerError{ .nesting = nesting, .status = { .duration = cucumber::messages::duration{}, .status = cucumber::messages::test_step_result_status::UNDEFINED, @@ -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, nesting, &definition, &supportCodeLibrary, &broadcaster, &testCaseContext, &testStepStarted, &dataTable, &docString] + const util::BodyFactory bodyFactory = [&nestedTestCaseRunner, &definition, &broadcaster, &testCaseContext, &testStepStarted, &dataTable, &docString] { return definition.factory(nestedTestCaseRunner, broadcaster, testCaseContext, testStepStarted, util::TransformTable(dataTable), util::TransformDocString(docString)); }; diff --git a/cucumber_cpp/library/util/ExecuteAndCatchExceptions.hpp b/cucumber_cpp/library/util/ExecuteAndCatchExceptions.hpp index 8fdcf570..115ca20b 100644 --- a/cucumber_cpp/library/util/ExecuteAndCatchExceptions.hpp +++ b/cucumber_cpp/library/util/ExecuteAndCatchExceptions.hpp @@ -21,7 +21,7 @@ namespace cucumber_cpp::library::util { try { - throw; + throw; // NOSONAR(cpp:S1039) - HandleErrors is always called from a catch(...) block, so an active exception is guaranteed } catch (const util::NestedTestCaseRunnerError& e) { @@ -45,7 +45,7 @@ namespace cucumber_cpp::library::util e.text); } } - catch (const StepSkipped& e) + catch (const cucumber_cpp::library::util::StepSkipped& e) { testStepResult.status = TestStepResultStatus::SKIPPED; if (!e.message.empty()) @@ -61,7 +61,7 @@ namespace cucumber_cpp::library::util { testStepResult.status = TestStepResultStatus::FAILED; } - catch (std::exception& e) + catch (const std::exception& e) { testStepResult.status = TestStepResultStatus::FAILED; testStepResult.exception = TestException{ From 3d0d05ed4c8e11a1db7ded2fb9f3f28360977605 Mon Sep 17 00:00:00 2001 From: "Timmer, Daan" Date: Thu, 2 Jul 2026 15:23:12 +0000 Subject: [PATCH 4/9] add failing nested step test --- .../acceptance_test/features/test_nested_steps.feature | 7 ++++++- cucumber_cpp/acceptance_test/steps/Steps.cpp | 10 ++++++++++ cucumber_cpp/acceptance_test/test.bats | 10 ++++++++++ 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/cucumber_cpp/acceptance_test/features/test_nested_steps.feature b/cucumber_cpp/acceptance_test/features/test_nested_steps.feature index d4228606..e91b7029 100644 --- a/cucumber_cpp/acceptance_test/features/test_nested_steps.feature +++ b/cucumber_cpp/acceptance_test/features/test_nested_steps.feature @@ -1,5 +1,10 @@ -@nested_steps Feature: Nested Steps + @nested_steps Scenario: Call other steps from within a step Given a step calls another step with "cucumber" Then the stored string is "cucumber" + + @nested_failing_steps + Scenario: Call other steps from within a step + When a step calls another step that will fail + Then this should be skipped diff --git a/cucumber_cpp/acceptance_test/steps/Steps.cpp b/cucumber_cpp/acceptance_test/steps/Steps.cpp index b3fd06fe..d17bcc83 100644 --- a/cucumber_cpp/acceptance_test/steps/Steps.cpp +++ b/cucumber_cpp/acceptance_test/steps/Steps.cpp @@ -157,3 +157,13 @@ GIVEN_F(FailingStepFixture, R"(step fixture fails)") { // empty } + +GIVEN("a nested step that fails") +{ + ASSERT_THAT(false, testing::IsTrue()); +} + +GIVEN("a step calls another step that will fail") +{ + Step("a nested step that fails"); +} diff --git a/cucumber_cpp/acceptance_test/test.bats b/cucumber_cpp/acceptance_test/test.bats index 8803cfef..edf20970 100644 --- a/cucumber_cpp/acceptance_test/test.bats +++ b/cucumber_cpp/acceptance_test/test.bats @@ -230,3 +230,13 @@ teardown() { assert_output --partial "2 scenarios 1 passed, 1 failed" assert_output --partial "2 steps 1 passed, 1 failed" } + +@test "Test nested failures propagate properly" { + run $acceptance_test --format summary --format-options "{ \"summary\": {\"theme\":\"plain\"} }" --tags "@nested_failing_steps" -- cucumber_cpp/acceptance_test/features + assert_failure + assert_output --partial "FAILED nested step: \"* a nested step that fails\"" + assert_output --partial "Value of: false" + assert_output --partial "Expected: is true" + assert_output --partial "Actual: false (of type bool)" + assert_output --partial "↷ Then this should be skipped" +} From 123bb512ce6bbc95fd96860f2a8d94980e174446 Mon Sep 17 00:00:00 2001 From: "Timmer, Daan" Date: Mon, 6 Jul 2026 08:39:50 +0000 Subject: [PATCH 5/9] get library tests running again --- cucumber_cpp/library/CMakeLists.txt | 2 +- cucumber_cpp/library/engine/CMakeLists.txt | 2 +- .../test_helper/StepImplementations.cpp | 2 +- cucumber_cpp/library/test/CMakeLists.txt | 1 - cucumber_cpp/library/test/TestApplication.cpp | 61 +++--------- cucumber_cpp/library/test/TestSteps.cpp | 97 ------------------- external/cucumber/CMakeLists.txt | 2 +- external/cucumber/gherkin/CMakeLists.txt | 2 +- 8 files changed, 21 insertions(+), 148 deletions(-) delete mode 100644 cucumber_cpp/library/test/TestSteps.cpp diff --git a/cucumber_cpp/library/CMakeLists.txt b/cucumber_cpp/library/CMakeLists.txt index 6e12e776..7b81f688 100644 --- a/cucumber_cpp/library/CMakeLists.txt +++ b/cucumber_cpp/library/CMakeLists.txt @@ -48,5 +48,5 @@ add_subdirectory(tag_expression) add_subdirectory(util) if (CCR_BUILD_TESTS) - # add_subdirectory(test) + add_subdirectory(test) endif() diff --git a/cucumber_cpp/library/engine/CMakeLists.txt b/cucumber_cpp/library/engine/CMakeLists.txt index 1f22fb74..83a05986 100644 --- a/cucumber_cpp/library/engine/CMakeLists.txt +++ b/cucumber_cpp/library/engine/CMakeLists.txt @@ -24,5 +24,5 @@ target_link_libraries(cucumber_cpp.library.engine PUBLIC if (CCR_BUILD_TESTS) add_subdirectory(test) - # add_subdirectory(test_helper) + add_subdirectory(test_helper) endif() diff --git a/cucumber_cpp/library/engine/test_helper/StepImplementations.cpp b/cucumber_cpp/library/engine/test_helper/StepImplementations.cpp index 11ab5d6e..5fa91397 100644 --- a/cucumber_cpp/library/engine/test_helper/StepImplementations.cpp +++ b/cucumber_cpp/library/engine/test_helper/StepImplementations.cpp @@ -62,7 +62,7 @@ WHEN("I call a nested step") { ASSERT_THAT(context.Contains("nested"), testing::IsFalse()); - Given("I am a nested step"); + Step("I am a nested step"); } THEN("the nested step was called") diff --git a/cucumber_cpp/library/test/CMakeLists.txt b/cucumber_cpp/library/test/CMakeLists.txt index 1191772a..85bf0d30 100644 --- a/cucumber_cpp/library/test/CMakeLists.txt +++ b/cucumber_cpp/library/test/CMakeLists.txt @@ -13,5 +13,4 @@ target_link_libraries(cucumber_cpp.library.test PUBLIC target_sources(cucumber_cpp.library.test PRIVATE TestApplication.cpp TestContext.cpp - TestSteps.cpp ) diff --git a/cucumber_cpp/library/test/TestApplication.cpp b/cucumber_cpp/library/test/TestApplication.cpp index f040f65b..81fcf9a2 100644 --- a/cucumber_cpp/library/test/TestApplication.cpp +++ b/cucumber_cpp/library/test/TestApplication.cpp @@ -2,6 +2,7 @@ #include "cucumber_cpp/library/Application.hpp" #include "cucumber_cpp/library/engine/test_helper/TemporaryFile.hpp" #include "gmock/gmock.h" +#include "gtest/gtest.h" #include #include #include @@ -34,41 +35,10 @@ namespace cucumber_cpp::library return capturedStdout; } - TEST_F(TestApplication, RunHelpWithoutArguments) - { - - const std::array args{ "application" }; - - RunWithArgs(args, static_cast>(CLI::ExitCodes::RequiredError)); - } - - TEST_F(TestApplication, RunCommandWithoutArguments) - { - - const std::array args{ "application", "run" }; - - RunWithArgs(args, static_cast>(CLI::ExitCodes::RequiredError)); - } - - TEST_F(TestApplication, RunCommand) - { - - const std::array args{ "application", "run", "--feature", "./", "--report", "console" }; - - RunWithArgs(args, static_cast>(CLI::ExitCodes::Success)); - } - - TEST_F(TestApplication, DryRunCommand) - { - const std::array args{ "application", "run", "--feature", "./", "--report", "console", "--dry" }; - - RunWithArgs(args, static_cast>(CLI::ExitCodes::Success)); - } - TEST_F(TestApplication, InvalidArgument) { - const std::array args{ "application", "run", "--feature", "./", "--report", "console", "--doesntexist" }; + const std::array args{ "application", "--doesntexist" }; RunWithArgs(args, static_cast>(CLI::ExitCodes::ExtrasError)); } @@ -83,11 +53,12 @@ namespace cucumber_cpp::library " Scenario: Test scenario1\n" " Given 5 and 5 are equal\n"; - const std::array args{ "application", "run", "--feature", path.c_str(), "--report", "console", "--dry" }; + const std::array args{ "application", "--format-options", R"({ "summary": {"theme":"plain"} })", "--dry-run", path.c_str() }; std::string stdoutString = RunWithArgs(args, static_cast>(CLI::ExitCodes::Success)); - EXPECT_THAT(stdoutString, testing::HasSubstr("1/1 passed")); + EXPECT_THAT(stdoutString, testing::HasSubstr("1 scenarios 1 skipped")); + EXPECT_THAT(stdoutString, testing::HasSubstr("1 steps 1 skipped")); } TEST_F(TestApplication, RunFeatureFile) @@ -100,11 +71,12 @@ namespace cucumber_cpp::library " Scenario: Test scenario1\n" " Given 5 and 5 are equal\n"; - const std::array args{ "application", "run", "--feature", path.c_str(), "--report", "console" }; + const std::array args{ "application", "--format-options", R"({ "summary": {"theme":"plain"} })", path.c_str() }; std::string stdoutString = RunWithArgs(args, static_cast>(CLI::ExitCodes::Success)); - EXPECT_THAT(stdoutString, testing::HasSubstr("1/1 passed")); + EXPECT_THAT(stdoutString, testing::HasSubstr("1 scenarios 1 passed")); + EXPECT_THAT(stdoutString, testing::HasSubstr("1 steps 1 passed")); } TEST_F(TestApplication, ExposeParameterRegistration) @@ -126,18 +98,17 @@ namespace cucumber_cpp::library " And This is a THEN step\n" " And This is a STEP step\n"; - const std::array args{ "application", "run", "--feature", path.c_str(), "--report", "console", "--unused" }; + const std::array args{ "application", "--format", "usage", "--format-options", R"({ "usage": {"theme":"plain"} })", "--", path.c_str() }; std::string stdoutString = RunWithArgs(args, static_cast>(CLI::ExitCodes::Success)); - EXPECT_THAT(stdoutString, testing::HasSubstr("The following steps have not been used:")); - EXPECT_THAT(stdoutString, testing::HasSubstr("^This is a step with a ([0-9]+)s delay$")); - EXPECT_THAT(stdoutString, testing::HasSubstr("Step with cucumber expression syntax {float} {string} {int}")); + // used + EXPECT_THAT(stdoutString, testing::ContainsRegex(R"(This is a GIVEN step\s+\|\s+[0-9]+ms)")); - EXPECT_THAT(stdoutString, testing::Not(testing::HasSubstr("{int} and {int} are equal"))); - EXPECT_THAT(stdoutString, testing::Not(testing::HasSubstr("And This is a GIVEN step"))); - EXPECT_THAT(stdoutString, testing::Not(testing::HasSubstr("And This is a WHEN step"))); - EXPECT_THAT(stdoutString, testing::Not(testing::HasSubstr("And This is a THEN step"))); - EXPECT_THAT(stdoutString, testing::Not(testing::HasSubstr("And This is a STEP step"))); + // unused + EXPECT_THAT(stdoutString, testing::ContainsRegex(R"(\^This is a step with a \(\[0-9]\+\)s delay\$\s+\|\s+UNUSED)")); + EXPECT_THAT(stdoutString, testing::ContainsRegex(R"(I throw an exception\s+\|\s+UNUSED)")); + EXPECT_THAT(stdoutString, testing::ContainsRegex(R"(Step with cucumber expression syntax \{float\} \{string\} \{int\}\s+\|\s+UNUSED)")); + EXPECT_THAT(stdoutString, testing::ContainsRegex(R"(An expression that looks like a function func\\\(\{int\}, \{int\}\) should keep its parameters\s+\|\s+UNUSED)")); } } diff --git a/cucumber_cpp/library/test/TestSteps.cpp b/cucumber_cpp/library/test/TestSteps.cpp deleted file mode 100644 index 4a3c8ea3..00000000 --- a/cucumber_cpp/library/test/TestSteps.cpp +++ /dev/null @@ -1,97 +0,0 @@ -#include "cucumber_cpp/library/Context.hpp" -#include "cucumber_cpp/library/StepRegistry.hpp" -#include "cucumber_cpp/library/cucumber_expression/ParameterRegistry.hpp" -#include "cucumber_cpp/library/query/Query.hpp" -#include "cucumber_cpp/library/support/StepRegistry.hpp" -#include "gtest/gtest.h" -#include -#include -#include -#include -#include -#include - -namespace cucumber_cpp::library -{ - struct TestSteps : testing::Test - { - cucumber_expression::ParameterRegistry parameterRegistry{ {} }; - StepRegistry stepRegistry{ parameterRegistry }; - }; - - TEST_F(TestSteps, RegisterThroughPreregistration) - { - EXPECT_THAT(stepRegistry.Size(), testing::Ge(1)); - } - - TEST_F(TestSteps, GetGivenStep) - { - EXPECT_THAT(stepRegistry.Query("This is a GIVEN step").stepRegexStr, testing::StrEq("^This is a GIVEN step$")); - EXPECT_THAT(stepRegistry.Query("This is a WHEN step").stepRegexStr, testing::StrEq("^This is a WHEN step$")); - EXPECT_THAT(stepRegistry.Query("This is a THEN step").stepRegexStr, testing::StrEq("^This is a THEN step$")); - EXPECT_THAT(stepRegistry.Query("This is a STEP step").stepRegexStr, testing::StrEq("^This is a STEP step$")); - } - - TEST_F(TestSteps, GetStepWithMatches) - { - const auto matches = stepRegistry.Query("This is a step with a 10s delay"); - - EXPECT_THAT(matches.stepRegexStr, testing::StrEq("^This is a step with a ([0-9]+)s delay$")); - - EXPECT_THAT(std::get>(matches.matches).size(), testing::Eq(1)); - EXPECT_THAT(std::get>(matches.matches)[0], testing::StrEq("10")); - } - - TEST_F(TestSteps, GetInvalidStep) - { - EXPECT_THROW((void)stepRegistry.Query("This step does not exist"), support::StepRegistry::StepNotFoundError); - } - - TEST_F(TestSteps, GetAmbiguousStep) - { - EXPECT_THROW((void)stepRegistry.Query("an ambiguous step"), support::StepRegistry::AmbiguousStepError); - } - - TEST_F(TestSteps, InvokeTestWithCucumberExpressions) - { - const auto matches = stepRegistry.Query(R"(Step with cucumber expression syntax 1.5 "abcdef" 10)"); - - auto contextStorage{ std::make_shared() }; - Context context{ contextStorage }; - - matches.factory(context, {}, "")->ExecuteAndCatchExceptions(matches.matches); - - EXPECT_THAT(context.Contains("float"), testing::IsTrue()); - EXPECT_THAT(context.Contains("std::string"), testing::IsTrue()); - EXPECT_THAT(context.Contains("std::int32_t"), testing::IsTrue()); - - EXPECT_THAT(context.Get("float"), testing::FloatEq(1.5)); - EXPECT_THAT(context.Get("std::string"), testing::StrEq("abcdef")); - EXPECT_THAT(context.Get("std::int32_t"), testing::Eq(10)); - } - - TEST_F(TestSteps, EscapedCucumberExpression) - { - const auto matchesParens = stepRegistry.Query(R"(An expression with (parenthesis) should remain as is)"); - EXPECT_THAT(matchesParens.stepRegexStr, testing::StrEq(R"(^An expression with \(parenthesis\) should remain as is$)")); - - const auto matchesBrace = stepRegistry.Query(R"(An expression with {braces} should remain as is)"); - EXPECT_THAT(matchesBrace.stepRegexStr, testing::StrEq(R"(^An expression with \{braces\} should remain as is$)")); - } - - TEST_F(TestSteps, FunctionLikeStep) - { - const auto matches = stepRegistry.Query(R"(An expression that looks like a function func(1, 2) should keep its parameters)"); - - auto contextStorage{ std::make_shared() }; - Context context{ contextStorage }; - - matches.factory(context, {}, {})->ExecuteAndCatchExceptions(matches.matches); - } - - TEST_F(TestSteps, EscapedParenthesis) - { - const auto matches1 = stepRegistry.Query(R"(An expression with \(escaped parenthesis\) should keep the slash)"); - const auto matches2 = stepRegistry.Query(R"(An expression with \{escaped braces\} should keep the slash)"); - } -} diff --git a/external/cucumber/CMakeLists.txt b/external/cucumber/CMakeLists.txt index 6c00ddcd..ed70a4dc 100644 --- a/external/cucumber/CMakeLists.txt +++ b/external/cucumber/CMakeLists.txt @@ -1,2 +1,2 @@ -add_subdirectory(messages) # before gherkin +# add_subdirectory(messages) # before gherkin add_subdirectory(gherkin) diff --git a/external/cucumber/gherkin/CMakeLists.txt b/external/cucumber/gherkin/CMakeLists.txt index da3b0381..9d2843b5 100644 --- a/external/cucumber/gherkin/CMakeLists.txt +++ b/external/cucumber/gherkin/CMakeLists.txt @@ -1,7 +1,7 @@ FetchContent_Declare( cucumber_gherkin GIT_REPOSITORY https://github.com/cucumber/gherkin.git - GIT_TAG 6fe0a3be9df9388209cca37bc3f254be10a6014e # v39.0.0 + GIT_TAG 6f0040fee2783fdb455d345e5bdb50d425515b5d # cpp-remove-cmate-use-native-cmake-and-CPM ) FetchContent_MakeAvailable(cucumber_gherkin) From 6ec19005f22e6af7153997e19d74848d09beaaa6 Mon Sep 17 00:00:00 2001 From: "Timmer, Daan" Date: Mon, 6 Jul 2026 09:08:52 +0000 Subject: [PATCH 6/9] refactor: remove unused test and restructure CucumberResultReporter --- cucumber_cpp/library/test/TestApplication.cpp | 28 ------------ cucumber_cpp/library/util/Body.cpp | 45 +++++++++++-------- cucumber_cpp/library/util/Body.hpp | 12 ----- 3 files changed, 27 insertions(+), 58 deletions(-) diff --git a/cucumber_cpp/library/test/TestApplication.cpp b/cucumber_cpp/library/test/TestApplication.cpp index 81fcf9a2..532d932d 100644 --- a/cucumber_cpp/library/test/TestApplication.cpp +++ b/cucumber_cpp/library/test/TestApplication.cpp @@ -83,32 +83,4 @@ namespace cucumber_cpp::library { EXPECT_THAT(&Application{}.ParameterRegistration(), testing::NotNull()); } - - TEST_F(TestApplication, UnusedParameters) - { - auto tmp = engine::test_helper::TemporaryFile{ "tmpfile.feature" }; - const auto path = tmp.Path().string(); - - tmp << "Feature: Test feature\n" - " Rule: Test rule\n" - " Scenario: Test scenario1\n" - " Given 5 and 5 are equal\n" - " And This is a GIVEN step\n" - " And This is a WHEN step\n" - " And This is a THEN step\n" - " And This is a STEP step\n"; - - const std::array args{ "application", "--format", "usage", "--format-options", R"({ "usage": {"theme":"plain"} })", "--", path.c_str() }; - - std::string stdoutString = RunWithArgs(args, static_cast>(CLI::ExitCodes::Success)); - - // used - EXPECT_THAT(stdoutString, testing::ContainsRegex(R"(This is a GIVEN step\s+\|\s+[0-9]+ms)")); - - // unused - EXPECT_THAT(stdoutString, testing::ContainsRegex(R"(\^This is a step with a \(\[0-9]\+\)s delay\$\s+\|\s+UNUSED)")); - EXPECT_THAT(stdoutString, testing::ContainsRegex(R"(I throw an exception\s+\|\s+UNUSED)")); - EXPECT_THAT(stdoutString, testing::ContainsRegex(R"(Step with cucumber expression syntax \{float\} \{string\} \{int\}\s+\|\s+UNUSED)")); - EXPECT_THAT(stdoutString, testing::ContainsRegex(R"(An expression that looks like a function func\\\(\{int\}, \{int\}\) should keep its parameters\s+\|\s+UNUSED)")); - } } diff --git a/cucumber_cpp/library/util/Body.cpp b/cucumber_cpp/library/util/Body.cpp index ad711fa1..943dd8c4 100644 --- a/cucumber_cpp/library/util/Body.cpp +++ b/cucumber_cpp/library/util/Body.cpp @@ -1,41 +1,50 @@ - #include "cucumber_cpp/library/util/Body.hpp" #include "cucumber_cpp/library/util/Duration.hpp" #include "cucumber_cpp/library/util/ExecuteAndCatchExceptions.hpp" #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 #include -#include #include namespace cucumber_cpp::library::util { - CucumberResultReporter::CucumberResultReporter(util::TestStepResult& testStepResult) - : testing::ScopedFakeTestPartResultReporter{ nullptr } - , testStepResult{ testStepResult } + namespace { - } - void CucumberResultReporter::ReportTestPartResult(const testing::TestPartResult& testPartResult) - { - if (testPartResult.failed()) + struct CucumberResultReporter : public testing::ScopedFakeTestPartResultReporter { - testStepResult.status = util::TestStepResultStatus::FAILED; + explicit CucumberResultReporter(util::TestStepResult& testStepResult) + : testing::ScopedFakeTestPartResultReporter{ nullptr } + , testStepResult{ testStepResult } + { + } - auto fileName = std::filesystem::relative(testPartResult.file_name(), std::filesystem::current_path()).string(); + void ReportTestPartResult(const testing::TestPartResult& testPartResult) override + { + if (testPartResult.failed()) + { + 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()); + } + + if (testPartResult.fatally_failed()) + throw FatalError{ testPartResult.message() }; + } + + private: + util::TestStepResult& testStepResult; + }; } TestStepResult ConstructAndExecute(const std::function()>& bodyFactory, const ExecuteArgs& args) diff --git a/cucumber_cpp/library/util/Body.hpp b/cucumber_cpp/library/util/Body.hpp index d99ede80..130ba47a 100644 --- a/cucumber_cpp/library/util/Body.hpp +++ b/cucumber_cpp/library/util/Body.hpp @@ -3,10 +3,8 @@ #include "cucumber_cpp/library/cucumber_expression/ParameterRegistry.hpp" #include "cucumber_cpp/library/util/TestStepResult.hpp" -#include "gtest/gtest.h" #include #include -#include #include #include #include @@ -54,16 +52,6 @@ namespace cucumber_cpp::library::util using ExecuteArgs = std::vector; - struct CucumberResultReporter : public testing::ScopedFakeTestPartResultReporter - { - explicit CucumberResultReporter(util::TestStepResult& testStepResult); - - void ReportTestPartResult(const testing::TestPartResult& testPartResult) override; - - private: - util::TestStepResult& testStepResult; - }; - struct Body; using BodyFactory = std::function()>; From dfe4f7051b467332e6b58f33cdd55a0e829d3bff Mon Sep 17 00:00:00 2001 From: "Timmer, Daan" Date: Mon, 6 Jul 2026 11:07:07 +0000 Subject: [PATCH 7/9] fix: remove noexcept from HandleErrors to allow exception propagation --- cucumber_cpp/library/util/ExecuteAndCatchExceptions.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cucumber_cpp/library/util/ExecuteAndCatchExceptions.hpp b/cucumber_cpp/library/util/ExecuteAndCatchExceptions.hpp index 115ca20b..7a8413e3 100644 --- a/cucumber_cpp/library/util/ExecuteAndCatchExceptions.hpp +++ b/cucumber_cpp/library/util/ExecuteAndCatchExceptions.hpp @@ -17,7 +17,7 @@ namespace cucumber_cpp::library::util { - inline void HandleErrors(TestStepResult& testStepResult) noexcept + inline void HandleErrors(TestStepResult& testStepResult) { try { From e2ef773783a1e411585d76d14a371a8ebab8a421 Mon Sep 17 00:00:00 2001 From: "Timmer, Daan" Date: Tue, 7 Jul 2026 06:07:46 +0000 Subject: [PATCH 8/9] nitpick 1 --- cucumber_cpp/acceptance_test/steps/Steps.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cucumber_cpp/acceptance_test/steps/Steps.cpp b/cucumber_cpp/acceptance_test/steps/Steps.cpp index d17bcc83..a5712fdc 100644 --- a/cucumber_cpp/acceptance_test/steps/Steps.cpp +++ b/cucumber_cpp/acceptance_test/steps/Steps.cpp @@ -107,7 +107,7 @@ THEN("the exception is caught") THEN("the next scenario is executed") { - /* do nothing */ + // empty } GIVEN("{int} and {int} are equal", (std::int32_t a, std::int32_t b)) @@ -143,7 +143,7 @@ GIVEN(R"(I attach a link to {string})", (const std::string& url)) GIVEN(R"(step fixture does not fail)") { - // + // empty } struct FailingStepFixture : cucumber_cpp::StepBase From 85947a67b534f07853bb2a49825873edf06592cd Mon Sep 17 00:00:00 2001 From: "Timmer, Daan" Date: Tue, 7 Jul 2026 06:09:35 +0000 Subject: [PATCH 9/9] nitpick 2 --- external/cucumber/CMakeLists.txt | 2 +- external/cucumber/messages/CMakeLists.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/external/cucumber/CMakeLists.txt b/external/cucumber/CMakeLists.txt index ed70a4dc..6c00ddcd 100644 --- a/external/cucumber/CMakeLists.txt +++ b/external/cucumber/CMakeLists.txt @@ -1,2 +1,2 @@ -# add_subdirectory(messages) # before gherkin +add_subdirectory(messages) # before gherkin add_subdirectory(gherkin) diff --git a/external/cucumber/messages/CMakeLists.txt b/external/cucumber/messages/CMakeLists.txt index 8d6f34ef..30df692c 100644 --- a/external/cucumber/messages/CMakeLists.txt +++ b/external/cucumber/messages/CMakeLists.txt @@ -1,7 +1,7 @@ FetchContent_Declare( cucumber_messages GIT_REPOSITORY https://github.com/cucumber/messages.git - GIT_TAG 6582661d5b1c44a628f8eab65d3aca539f99c4ab # v32.2.0 + GIT_TAG c34b8cf6412f274ac6943075326440b7e6062c81 # refactor-cpp-codegen OVERRIDE_FIND_PACKAGE )