Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cucumber_cpp/Steps.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#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/engine/Step.hpp"
#include "cucumber_cpp/library/util/DocString.hpp"
#include "cucumber_cpp/library/util/Table.hpp"

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
@expose_scenario_info
Feature: Scenario begin and end hooks can expose scenario information
@store_scenario_info
Rule: Scenario begin and end hooks store the scenario information
Background:
Then the "ScenarioInfoHook" has the name "Scenario with @tag-a" or "Scenario with @tag-b"
And the "ScenarioInfoHook" has the tag "@tag-a" or "@tag-b"
And the "StepHookInfo" has the name "Scenario with @tag-a" or "Scenario with @tag-b"
And the "StepHookInfo" has the tag "@tag-a" or "@tag-b"

Scenario Outline: Scenario with <tag>
Then the "<scope>" has the name "Scenario with <tag>"
And the "<scope>" has the tags "@expose_scenario_info" and "<tag>"

@tag-a
Examples:
| scope | tag |
| ScenarioInfoHook | @tag-a |
| StepHookInfo | @tag-a |

@tag-b
Examples:
| scope | tag |
| ScenarioInfoHook | @tag-b |
| StepHookInfo | @tag-b |

@no_store_scenario_info
Rule: scenario begin and end hooks do not store the scenario information
Scenario: Scenario information is available
Then the "ScenarioInfoHook" is not available
And the "StepHookInfo" is not available
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Feature: a parse error is printed to cerr
Scenario: a parse error is printed to cerr
Given a feature file with a parse error
when this line is a parse error (when should be When)
Then this should be skipped
19 changes: 19 additions & 0 deletions cucumber_cpp/acceptance_test/hooks/Hooks.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include "cucumber_cpp/CucumberCpp.hpp"
#include "cucumber_cpp/Steps.hpp"
#include "gmock/gmock.h"
#include <gtest/gtest.h>
#include <iostream>
Expand Down Expand Up @@ -57,3 +58,21 @@ HOOK_BEFORE_SCENARIO(.name = "fail if --failprogramhook is set")
if (context.Contains("--failprogramhook") && context.Get<bool>("--failprogramhook"))
std::cout << "should not be executed\n";
}

HOOK_BEFORE_SCENARIO("@expose_scenario_info")
{
const auto& scenarioInfo = ScenarioInfo();
if (scenarioInfo.tags.contains("@store_scenario_info"))
{
context.InsertAt("ScenarioInfoHook", scenarioInfo);
}
}

HOOK_BEFORE_STEP("@expose_scenario_info")
{
const auto& scenarioInfo = ScenarioInfo();
if (scenarioInfo.tags.contains("@store_scenario_info"))
{
context.InsertAt("StepHookInfo", scenarioInfo);
}
}
30 changes: 30 additions & 0 deletions cucumber_cpp/acceptance_test/steps/Steps.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include "cucumber_cpp/Steps.hpp"
#include "cucumber_cpp/library/util/ScenarioInfo.hpp"
#include "fmt/format.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
Expand Down Expand Up @@ -167,3 +168,32 @@ GIVEN("a step calls another step that will fail")
{
Step("a nested step that fails");
}

THEN("the {string} has the name {string} or {string}", (const std::string& hookType, const std::string& expectedName1, const std::string& expectedName2))
{
const auto& scenarioInfo = context.Get<cucumber_cpp::library::util::ScenarioInfo>(hookType);
ASSERT_THAT(scenarioInfo.name, testing::AnyOf(testing::StrEq(expectedName1), testing::StrEq(expectedName2)));
}

THEN("the {string} has the name {string}", (const std::string& hookType, const std::string& expectedName))
{
const auto& scenarioInfo = context.Get<cucumber_cpp::library::util::ScenarioInfo>(hookType);
ASSERT_THAT(scenarioInfo.name, testing::StrEq(expectedName));
}

THEN("the {string} has the tag {string} or {string}", (const std::string& hookType, const std::string& expectedTag1, const std::string& expectedTag2))
{
const auto& scenarioInfo = context.Get<cucumber_cpp::library::util::ScenarioInfo>(hookType);
ASSERT_THAT(scenarioInfo.tags, testing::AnyOf(testing::Contains(expectedTag1), testing::Contains(expectedTag2)));
}

THEN("the {string} has the tags {string} and {string}", (const std::string& hookType, const std::string& expectedTag1, const std::string& expectedTag2))
{
const auto& scenarioInfo = context.Get<cucumber_cpp::library::util::ScenarioInfo>(hookType);
ASSERT_THAT(scenarioInfo.tags, testing::AllOf(testing::Contains(expectedTag1), testing::Contains(expectedTag2)));
}

THEN("the {string} is not available", (const std::string& hookType))
{
ASSERT_THAT(context.Contains(hookType), testing::IsFalse());
}
12 changes: 12 additions & 0 deletions cucumber_cpp/acceptance_test/test.bats
Original file line number Diff line number Diff line change
Expand Up @@ -240,3 +240,15 @@ teardown() {
assert_output --partial "Actual: false (of type bool)"
assert_output --partial "↷ Then this should be skipped"
}

@test "Test providing access to scenario info in scenario and step hooks" {
run $acceptance_test --tags "@expose_scenario_info" -- cucumber_cpp/acceptance_test/features
assert_success
}

@test "Test parse errors are printed to cerr" {
run $acceptance_test cucumber_cpp/acceptance_test/features_with_parse_error/test_parse_error.feature
assert_failure
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)'"
}
Comment thread
Copilot marked this conversation as resolved.
26 changes: 17 additions & 9 deletions cucumber_cpp/library/Hooks.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,46 +8,54 @@
#include "cucumber_cpp/library/engine/Hook.hpp"
#include "cucumber_cpp/library/support/DefinitionRegistration.hpp"

#define HOOK_(matcher, type) BODY(matcher, type, (), cucumber_cpp::library::support::DefinitionRegistration::Register, cucumber_cpp::library::engine::HookBase)
#define HOOK_(matcher, type, base) BODY(matcher, type, (), cucumber_cpp::library::support::DefinitionRegistration::Register, base)

#define HOOK_BEFORE_ALL(...) \
HOOK_( \
(cucumber_cpp::library::support::GlobalHook{ __VA_ARGS__ }), \
cucumber_cpp::library::util::HookType::beforeAll)
cucumber_cpp::library::util::HookType::beforeAll, \
cucumber_cpp::library::engine::GlobalHookImpl)

#define HOOK_AFTER_ALL(...) \
HOOK_( \
(cucumber_cpp::library::support::GlobalHook{ __VA_ARGS__ }), \
cucumber_cpp::library::util::HookType::afterAll)
cucumber_cpp::library::util::HookType::afterAll, \
cucumber_cpp::library::engine::GlobalHookImpl)

#define HOOK_BEFORE_FEATURE(...) \
HOOK_( \
(cucumber_cpp::library::support::Hook{ __VA_ARGS__ }), \
cucumber_cpp::library::util::HookType::beforeFeature)
cucumber_cpp::library::util::HookType::beforeFeature, \
cucumber_cpp::library::engine::GlobalHookImpl)

#define HOOK_AFTER_FEATURE(...) \
HOOK_( \
(cucumber_cpp::library::support::Hook{ __VA_ARGS__ }), \
cucumber_cpp::library::util::HookType::afterFeature)
cucumber_cpp::library::util::HookType::afterFeature, \
cucumber_cpp::library::engine::GlobalHookImpl)

#define HOOK_BEFORE_SCENARIO(...) \
HOOK_( \
(cucumber_cpp::library::support::Hook{ __VA_ARGS__ }), \
cucumber_cpp::library::util::HookType::before)
cucumber_cpp::library::util::HookType::before, \
cucumber_cpp::library::engine::HookImpl)

#define HOOK_AFTER_SCENARIO(...) \
HOOK_( \
(cucumber_cpp::library::support::Hook{ __VA_ARGS__ }), \
cucumber_cpp::library::util::HookType::after)
cucumber_cpp::library::util::HookType::after, \
cucumber_cpp::library::engine::HookImpl)

#define HOOK_BEFORE_STEP(...) \
HOOK_( \
(cucumber_cpp::library::support::Hook{ __VA_ARGS__ }), \
cucumber_cpp::library::util::HookType::beforeStep)
cucumber_cpp::library::util::HookType::beforeStep, \
cucumber_cpp::library::engine::HookImpl)

#define HOOK_AFTER_STEP(...) \
HOOK_( \
(cucumber_cpp::library::support::Hook{ __VA_ARGS__ }), \
cucumber_cpp::library::util::HookType::afterStep)
cucumber_cpp::library::util::HookType::afterStep, \
cucumber_cpp::library::engine::HookImpl)

#endif
62 changes: 55 additions & 7 deletions cucumber_cpp/library/api/RunCucumber.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include "cucumber/messages/envelope.hpp"
#include "cucumber/messages/location.hpp"
#include "cucumber/messages/parameter_type.hpp"
#include "cucumber/messages/parse_error.hpp"
#include "cucumber/messages/pickle_tag.hpp"
#include "cucumber/messages/source_reference.hpp"
#include "cucumber/messages/step_definition.hpp"
Expand All @@ -23,15 +24,19 @@
#include "cucumber_cpp/library/util/Broadcaster.hpp"
#include "cucumber_cpp/library/util/HookData.hpp"
#include "cucumber_cpp/library/util/TransformHookData.hpp"
#include "fmt/format.h"
#include "fmt/ostream.h"
#include "nlohmann/json.hpp"
#include "nlohmann/json_fwd.hpp"
#include <functional>
#include <iostream>
#include <iterator>
#include <list>
#include <memory>
#include <ranges>
#include <set>
#include <string>
#include <utility>
#include <vector>

namespace cucumber_cpp::library::api
Expand Down Expand Up @@ -92,25 +97,25 @@ namespace cucumber_cpp::library::api
{
auto beforeAllHooks = supportCodeLibrary.hookRegistry.HooksByType(util::HookType::before);

for (auto& hook : beforeAllHooks)
for (const auto& hook : beforeAllHooks)
broadcaster.BroadcastEvent(cucumber::messages::envelope{ .hook = util::TransformHookData(hook) });

auto afterAllHooks = supportCodeLibrary.hookRegistry.HooksByType(util::HookType::after);

for (auto& hook : afterAllHooks)
for (const auto& hook : afterAllHooks)
broadcaster.BroadcastEvent(cucumber::messages::envelope{ .hook = util::TransformHookData(hook) });
}

void EmitTestRunHooks(const support::SupportCodeLibrary& supportCodeLibrary, util::Broadcaster& broadcaster)
{
auto beforeAllHooks = supportCodeLibrary.hookRegistry.HooksByType(util::HookType::beforeAll);

for (auto& hook : beforeAllHooks)
for (const auto& hook : beforeAllHooks)
broadcaster.BroadcastEvent(cucumber::messages::envelope{ .hook = util::TransformHookData(hook) });

auto afterAllHooks = supportCodeLibrary.hookRegistry.HooksByType(util::HookType::afterAll);

for (auto& hook : afterAllHooks)
for (const auto& hook : afterAllHooks)
broadcaster.BroadcastEvent(cucumber::messages::envelope{ .hook = util::TransformHookData(hook) });
}

Expand Down Expand Up @@ -156,9 +161,33 @@ namespace cucumber_cpp::library::api
};

if (sources.ordering == support::RunOptions::Ordering::defined)
return createOrderedPickleList(pickles);
return createOrderedPickleList(std::move(pickles));
else
return createOrderedPickleList(pickles | std::views::reverse);
return createOrderedPickleList(std::move(pickles) | std::views::reverse);
Comment thread
daantimmer marked this conversation as resolved.
};

struct ParseErrorListener : util::Listener
{
explicit ParseErrorListener(util::Broadcaster& broadcaster)
: Listener{ broadcaster, [this](const cucumber::messages::envelope& envelope)
{
OnEvent(envelope);
} }
{}

void OnEvent(const cucumber::messages::envelope& envelope)
{
if (envelope.parse_error)
parseErrors.push_back(*envelope.parse_error);
}

[[nodiscard]] const std::vector<cucumber::messages::parse_error>& GetParseErrors() const
{
return parseErrors;
}

private:
std::vector<cucumber::messages::parse_error> parseErrors;
};
}

Expand All @@ -182,7 +211,26 @@ namespace cucumber_cpp::library::api
const auto formatOptionsJson = formatOptions.empty() ? nlohmann::json::object() : nlohmann::json::parse(formatOptions);
const auto activeFormatters = formatters.EnableFormatters(format, formatOptionsJson, supportCodeLibrary, query);

const auto pickleSources = CollectPickles(options.sources, idGenerator, broadcaster);
ParseErrorListener parseErrorListener{ broadcaster };
auto pickleSources = CollectPickles(options.sources, idGenerator, broadcaster);

if (const auto& parseErrors = parseErrorListener.GetParseErrors(); !parseErrors.empty())
{
for (const auto& parseError : parseErrors)
{
const auto uri = parseError.source.uri.value_or("unknown source");
const auto line = parseError.source.location.has_value() ? fmt::format(":{}", parseError.source.location->line) : "";
const auto column = parseError.source.location.has_value() && parseError.source.location->column.has_value() ? fmt::format(":{}", parseError.source.location->column.value()) : "";

const auto messageStart = parseError.message.find(": ");
const auto message = messageStart != std::string::npos ? parseError.message.substr(messageStart + 2) : parseError.message;

fmt::println(std::cerr, "Parse error in: \"{}{}{}\" {}", uri, line, column, message);
}

return false;
}

const auto orderedPickles = OrderPickles(options.sources, pickleSources | std::views::filter(FilterByTagExpression(options.sources)));

EmitSupportCodeMessages(supportCodeLibrary, broadcaster, idGenerator);
Expand Down
16 changes: 16 additions & 0 deletions cucumber_cpp/library/engine/Hook.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
#include "cucumber_cpp/library/Context.hpp"
#include "cucumber_cpp/library/engine/ExecutionContext.hpp"
#include "cucumber_cpp/library/util/Broadcaster.hpp"
#include "cucumber_cpp/library/util/ScenarioInfo.hpp"
#include "cucumber_cpp/library/util/StepOrHookStarted.hpp"
#include <optional>
#include <utility>

namespace cucumber_cpp::library::engine
Expand All @@ -11,4 +13,18 @@ namespace cucumber_cpp::library::engine
: engine::ExecutionContext{ broadCaster, context, std::move(stepOrHookStarted) }
, hasError{ hasError }
{}

GlobalHookImpl::GlobalHookImpl(util::Broadcaster& broadCaster, Context& context, util::StepOrHookStarted stepOrHookStarted, [[maybe_unused]] const std::optional<util::ScenarioInfo>& scenarioInfo, bool hasError)
: HookBase{ broadCaster, context, std::move(stepOrHookStarted), hasError }
{}

HookImpl::HookImpl(util::Broadcaster& broadCaster, Context& context, util::StepOrHookStarted stepOrHookStarted, std::optional<util::ScenarioInfo> scenarioInfo, bool hasError)
: HookBase{ broadCaster, context, std::move(stepOrHookStarted), hasError }
, scenarioInfo{ std::move(scenarioInfo.value()) }
Comment thread
daantimmer marked this conversation as resolved.
{}
Comment thread
daantimmer marked this conversation as resolved.
Comment thread
daantimmer marked this conversation as resolved.
Comment thread
daantimmer marked this conversation as resolved.

const util::ScenarioInfo& HookImpl::ScenarioInfo() const
{
return scenarioInfo;
}
}
18 changes: 18 additions & 0 deletions cucumber_cpp/library/engine/Hook.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@

#include "cucumber_cpp/library/Context.hpp"
#include "cucumber_cpp/library/engine/ExecutionContext.hpp"
#include "cucumber_cpp/library/util/ScenarioInfo.hpp"
#include "cucumber_cpp/library/util/StepOrHookStarted.hpp"
#include <optional>

namespace cucumber_cpp::library::util
{
Expand Down Expand Up @@ -31,6 +33,22 @@ namespace cucumber_cpp::library::engine
protected:
const bool hasError;
};

struct GlobalHookImpl : HookBase
{
GlobalHookImpl(util::Broadcaster& broadCaster, Context& context, util::StepOrHookStarted stepOrHookStarted, const std::optional<util::ScenarioInfo>& scenarioInfo, bool hasError);
};

struct HookImpl : HookBase // NOSONAR(cpp:S3656) -
{
HookImpl(util::Broadcaster& broadCaster, Context& context, util::StepOrHookStarted stepOrHookStarted, std::optional<util::ScenarioInfo> scenarioInfo, bool hasError);

protected:
[[nodiscard]] const util::ScenarioInfo& ScenarioInfo() const;

private:
util::ScenarioInfo scenarioInfo;
};
}

#endif
Loading
Loading