From 051764f7b8459e61329272dbbad54e73b0f00293 Mon Sep 17 00:00:00 2001 From: Daan Timmer <8293597+daantimmer@users.noreply.github.com> Date: Wed, 24 Jan 2024 16:52:27 +0100 Subject: [PATCH] feat: add custom report handler (#32) * feat: add custom report handler Fixes #28 Fixes #29 * chore: made TableValue:As const * chore: fixed TestSteps * chore: added ambiguous step test * chore: resolved sonar warnings * chore: resolved sonar warnings * chore: resolved sonar warnings * chore: resolved sonar warnings * chore: resolved sonar warnings * chore: resolved sonar warnings * chore: resolved sonar warnings * chore: resolved sonar warnings * chore: resolved sonar warnings * chore: resolved sonar warnings --- .devcontainer/devcontainer.json | 3 +- .vscode/settings.json | 1 + cucumber-cpp-runner/main.cpp | 1 - cucumber-cpp/Application.cpp | 102 ++++----- cucumber-cpp/Application.hpp | 27 ++- cucumber-cpp/Body.hpp | 15 +- cucumber-cpp/BodyMacro.hpp | 10 +- cucumber-cpp/CMakeLists.txt | 4 +- cucumber-cpp/CucumberRunner.cpp | 70 +++--- cucumber-cpp/CucumberRunner.hpp | 30 ++- cucumber-cpp/FeatureRunner.cpp | 124 ++++++---- cucumber-cpp/FeatureRunner.hpp | 58 ++++- cucumber-cpp/HookRegistry.cpp | 17 +- cucumber-cpp/HookRegistry.hpp | 8 +- cucumber-cpp/HookScopes.cpp | 13 +- cucumber-cpp/HookScopes.hpp | 24 +- cucumber-cpp/JsonTagToSet.cpp | 24 -- cucumber-cpp/JsonTagToSet.hpp | 13 -- cucumber-cpp/ScenarioRunner.cpp | 127 +++++++---- cucumber-cpp/ScenarioRunner.hpp | 58 ++++- cucumber-cpp/StepRegistry.cpp | 42 ++-- cucumber-cpp/StepRegistry.hpp | 77 +++++-- cucumber-cpp/StepRunner.cpp | 270 +++++++++++++++++----- cucumber-cpp/StepRunner.hpp | 53 ++++- cucumber-cpp/TagExpression.cpp | 19 +- cucumber-cpp/TagExpression.hpp | 4 +- cucumber-cpp/TagsToSet.hpp | 22 ++ cucumber-cpp/TraceTime.cpp | 27 ++- cucumber-cpp/TraceTime.hpp | 26 ++- cucumber-cpp/report/CMakeLists.txt | 4 +- cucumber-cpp/report/JsonReport.cpp | 11 - cucumber-cpp/report/JsonReport.hpp | 14 -- cucumber-cpp/report/JunitReport.cpp | 291 +++++++++++++----------- cucumber-cpp/report/JunitReport.hpp | 40 +++- cucumber-cpp/report/Report.cpp | 81 +++++++ cucumber-cpp/report/Report.hpp | 81 ++++++- cucumber-cpp/report/StdOutReport.cpp | 189 ++++++++------- cucumber-cpp/report/StdOutReport.hpp | 18 +- cucumber-cpp/test/TestSteps.cpp | 65 ++++-- cucumber-cpp/test/TestTagExpression.cpp | 8 +- 40 files changed, 1352 insertions(+), 719 deletions(-) delete mode 100644 cucumber-cpp/JsonTagToSet.cpp delete mode 100644 cucumber-cpp/JsonTagToSet.hpp create mode 100644 cucumber-cpp/TagsToSet.hpp delete mode 100644 cucumber-cpp/report/JsonReport.cpp delete mode 100644 cucumber-cpp/report/JsonReport.hpp create mode 100644 cucumber-cpp/report/Report.cpp diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 104ea0f..36d2224 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -21,7 +21,8 @@ "twxs.cmake", "ms-vscode.cpptools", "akiramiyakoda.cppincludeguard", - "alexkrechik.cucumberautocomplete" + "alexkrechik.cucumberautocomplete", + "SonarSource.sonarlint-vscode" ] } } diff --git a/.vscode/settings.json b/.vscode/settings.json index 6140bb1..db3a3bf 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -21,4 +21,5 @@ "C/C++ Include Guard.Comment Style": "None", "C/C++ Include Guard.Path Skip": 0, "testMate.cpp.test.executables": "${workspaceFolder}/.build/**/*{test,Test,TEST}*", + "sonarlint.pathToCompileCommands": "${workspaceFolder}/.build/compile_commands.json", } diff --git a/cucumber-cpp-runner/main.cpp b/cucumber-cpp-runner/main.cpp index fb16c5a..b078881 100644 --- a/cucumber-cpp-runner/main.cpp +++ b/cucumber-cpp-runner/main.cpp @@ -8,7 +8,6 @@ int main(int argc, char** argv) auto args = application.GetForwardArgs(); application.RunFeatures(); - application.GenerateReports(); return application.GetExitCode(); } diff --git a/cucumber-cpp/Application.cpp b/cucumber-cpp/Application.cpp index a4471bc..bee10d5 100644 --- a/cucumber-cpp/Application.cpp +++ b/cucumber-cpp/Application.cpp @@ -1,11 +1,11 @@ #include "cucumber-cpp/Application.hpp" #include "cucumber-cpp/Context.hpp" #include "cucumber-cpp/CucumberRunner.hpp" -#include "cucumber-cpp/ResultStates.hpp" -#include "cucumber-cpp/report/JsonReport.hpp" #include "cucumber-cpp/report/JunitReport.hpp" +#include "cucumber-cpp/report/Report.hpp" #include "cucumber-cpp/report/StdOutReport.hpp" #include "cucumber/gherkin/file.hpp" +#include "cucumber/gherkin/parse_error.hpp" #include #include #include @@ -74,44 +74,34 @@ namespace cucumber_cpp { const auto arg = *current; if (arg == "--tag") - { while (std::next(current) != view.end() && (!(*std::next(current)).starts_with("-"))) { current = std::next(current); tags.push_back(*current); } - } else if (arg == "--feature") - { while (std::next(current) != view.end() && (!(*std::next(current)).starts_with("-"))) { current = std::next(current); features.push_back(*current); } - } else if (arg == "--report") - { while (std::next(current) != view.end() && (!(*std::next(current)).starts_with("-"))) { current = std::next(current); reports.push_back(*current); } - } else if (arg.starts_with("--Xapp,")) { const auto param = arg.substr(std::string_view("--Xapp,").size()); for (const auto xArg : std::views::split(param, ',')) - { forwardArgs.push_back(subrange_to_sv(xArg)); - } } else { if (!(arg == "--help" && arg == "-h")) - { std::cout << "\nUnkown argument: " << std::quoted(arg) << "\n"; - } ExitWithHelp(name); } @@ -152,37 +142,43 @@ namespace cucumber_cpp validateArguments(); } - Application::Application(std::span args) - : options{ args } + GherkinParser::GherkinParser(CucumberRunnerV2& cucumberRunner) + : cucumberRunner{ cucumberRunner } , cbs{ .ast = [&](const cucumber::gherkin::app::parser_result& ast) { - nlohmann::json astJson; - - ast.to_json(astJson); - - root["features"].push_back({ { "ast", astJson } }); + featureRunner = cucumberRunner.StartFeature(ast); }, .pickle = [&](const cucumber::messages::pickle& pickle) { - nlohmann::json scenarioJson; - - pickle.to_json(scenarioJson); - - root["features"].back()["scenarios"].push_back(scenarioJson); + featureRunner->StartScenario(pickle); }, - .error = [&](const auto& m) - { - std::cout << m.to_json() << std::endl; - } + .error = [&](const cucumber::gherkin::parse_error& m) {} } { - root["tagexpression"] = options.tags; app.include_source(false); app.include_ast(true); app.include_pickles(true); } + report::ReportHandler::Result GherkinParser::RunFeatureFile(const std::filesystem::path& path) + { + app.parse(cucumber::gherkin::file{ path.string() }, cbs); + auto result = featureRunner->Result(); + featureRunner = nullptr; + return result; + } + + Application::Application(std::span args) + : options{ args } + { + if (std::ranges::find(options.reports, "console") != options.reports.end()) + reporters.Add(std::make_unique()); + + if (std::ranges::find(options.reports, "junit-xml") != options.reports.end()) + reporters.Add(std::make_unique()); + } + const std::vector& Application::GetForwardArgs() const { return options.forwardArgs; @@ -190,48 +186,36 @@ namespace cucumber_cpp void Application::RunFeatures(std::shared_ptr contextStorageFactory) { - for (const auto& featurePath : GetFeatureFiles()) - app.parse(cucumber::gherkin::file{ featurePath.string() }, cbs); - - const auto tagExpression = options.tags.empty() ? std::string{} : std::accumulate(std::next(options.tags.begin()), options.tags.end(), std::string(options.tags.front()), JoinStringWithSpace); + using Result = report::ReportHandler::Result; - CucumberRunner cucumberRunner{ GetForwardArgs(), tagExpression, std::move(contextStorageFactory) }; - cucumberRunner.Run(root); + auto tagExpression = options.tags.empty() ? std::string{} : std::accumulate(std::next(options.tags.begin()), options.tags.end(), std::string(options.tags.front()), JoinStringWithSpace); - if (!root.contains("result")) - { - std::cout << "\nError: no features have been executed"; - } - } + CucumberRunnerV2 cucumberRunner{ GetForwardArgs(), std::move(tagExpression), reporters, std::move(contextStorageFactory) }; + GherkinParser gherkinParser{ cucumberRunner }; - void Application::GenerateReports(const std::map& /*unused*/) - { - if (std::ranges::find(options.reports, "json") != options.reports.end()) + for (const auto& featurePath : GetFeatureFiles()) { - cucumber_cpp::report::JsonReport report; - report.GenerateReport(root); - } + auto featureResult = gherkinParser.RunFeatureFile(featurePath); - if (std::ranges::find(options.reports, "console") != options.reports.end()) - { - cucumber_cpp::report::StdOutReport report; - report.GenerateReport(root); + if (result == Result::undefined || result == Result::success) + result = featureResult; } - if (std::ranges::find(options.reports, "junit-xml") != options.reports.end()) - { - cucumber_cpp::report::JunitReport junitReport; - junitReport.GenerateReport(root); - } + if (result == Result::undefined) + std::cout << "\nError: no features have been executed"; } int Application::GetExitCode() const { - if (root.contains("result") && root["result"] == result::success) - { + if (result == decltype(result)::success) return 0; - } - return 1; + else + return 1; + } + + [[nodiscard]] report::Reporters& Application::Reporters() + { + return reporters; } std::vector Application::GetFeatureFiles() const diff --git a/cucumber-cpp/Application.hpp b/cucumber-cpp/Application.hpp index dc48423..8300881 100644 --- a/cucumber-cpp/Application.hpp +++ b/cucumber-cpp/Application.hpp @@ -2,16 +2,35 @@ #define CUCUMBER_CPP_APPLICATION_HPP #include "cucumber-cpp/Context.hpp" +#include "cucumber-cpp/CucumberRunner.hpp" +#include "cucumber-cpp/FeatureRunner.hpp" #include "cucumber-cpp/report/Report.hpp" #include "cucumber/gherkin/app.hpp" #include #include +#include +#include #include #include +#include #include namespace cucumber_cpp { + struct GherkinParser + { + explicit GherkinParser(CucumberRunnerV2& cucumberRunner); + + [[nodiscard]] report::ReportHandler::Result RunFeatureFile(const std::filesystem::path& path); + + private: + CucumberRunnerV2& cucumberRunner; + std::unique_ptr featureRunner; + + cucumber::gherkin::app app; + cucumber::gherkin::app::callbacks cbs; + }; + struct Application { Application(std::span args); @@ -19,10 +38,11 @@ namespace cucumber_cpp [[nodiscard]] const std::vector& GetForwardArgs() const; void RunFeatures(std::shared_ptr contextStorageFactory = std::make_shared()); - void GenerateReports(const std::map& additionalReports = {}); [[nodiscard]] int GetExitCode() const; + [[nodiscard]] report::Reporters& Reporters(); + private: [[nodiscard]] std::vector GetFeatureFiles() const; @@ -37,10 +57,9 @@ namespace cucumber_cpp }; Options options; - nlohmann::json root; - cucumber::gherkin::app app; - cucumber::gherkin::app::callbacks cbs; + report::ReportForwarder reporters; + report::ReportHandler::Result result{ report::ReportHandler::Result::undefined }; }; } diff --git a/cucumber-cpp/Body.hpp b/cucumber-cpp/Body.hpp index 032ca5b..4353148 100644 --- a/cucumber-cpp/Body.hpp +++ b/cucumber-cpp/Body.hpp @@ -1,10 +1,13 @@ #ifndef CUCUMBER_CPP_BODY_HPP #define CUCUMBER_CPP_BODY_HPP -#include "nlohmann/json.hpp" #include +#include +#include +#include #include #include +#include namespace cucumber_cpp { @@ -31,19 +34,19 @@ namespace cucumber_cpp { virtual ~Body() = default; - virtual void Execute(const nlohmann::json& parameters = {}) = 0; + virtual void Execute(const std::vector& args = {}) = 0; protected: template - void InvokeWithArgImpl(T* t, const nlohmann::json& json, void (T::*ptr)(Args...), std::index_sequence) + void InvokeWithArgImpl(T* t, const std::vector& args, void (T::*ptr)(Args...), std::index_sequence) const { - (t->*ptr)(StringTo>(json[I])...); + (t->*ptr)(StringTo>(args[I])...); } template - void InvokeWithArg(T* t, const nlohmann::json& json, void (T::*ptr)(Args...)) + void InvokeWithArg(T* t, const std::vector& args, void (T::*ptr)(Args...)) const { - InvokeWithArgImpl(t, json, ptr, std::make_index_sequence{}); + InvokeWithArgImpl(t, args, ptr, std::make_index_sequence{}); } }; } diff --git a/cucumber-cpp/BodyMacro.hpp b/cucumber-cpp/BodyMacro.hpp index d74a1e4..f5f8e72 100644 --- a/cucumber-cpp/BodyMacro.hpp +++ b/cucumber-cpp/BodyMacro.hpp @@ -11,7 +11,7 @@ #define BODY_STRUCT CONCAT(BodyImpl, __LINE__) -#define BODY(matcher, type, args, registration, base) \ +#define BODY(matcher, type, targs, registration, base) \ namespace \ { \ struct BODY_STRUCT : cucumber_cpp::Body \ @@ -19,17 +19,17 @@ { \ using myBase = base; \ using myBase::myBase; \ - void Execute(const nlohmann::json& parameters = {}) override \ + void Execute(const std::vector& args) override \ { \ - InvokeWithArg(this, parameters, &BODY_STRUCT::ExecuteWithArgs); \ + InvokeWithArg(this, args, &BODY_STRUCT::ExecuteWithArgs); \ } \ \ private: \ - void ExecuteWithArgs args; \ + void ExecuteWithArgs targs; \ static const std::size_t ID; \ }; \ } \ const std::size_t BODY_STRUCT::ID = cucumber_cpp::registration(matcher, type); \ - void BODY_STRUCT::ExecuteWithArgs args + void BODY_STRUCT::ExecuteWithArgs targs #endif diff --git a/cucumber-cpp/CMakeLists.txt b/cucumber-cpp/CMakeLists.txt index 7f51edb..d465483 100644 --- a/cucumber-cpp/CMakeLists.txt +++ b/cucumber-cpp/CMakeLists.txt @@ -17,8 +17,6 @@ target_sources(cucumber-cpp PRIVATE Hooks.hpp HookScopes.cpp HookScopes.hpp - JsonTagToSet.cpp - JsonTagToSet.hpp OnTestPartResultEventListener.cpp OnTestPartResultEventListener.hpp Rtrim.cpp @@ -32,6 +30,7 @@ target_sources(cucumber-cpp PRIVATE StepRunner.hpp TagExpression.cpp TagExpression.hpp + TagsToSet.hpp TraceTime.cpp TraceTime.hpp ) @@ -43,7 +42,6 @@ target_include_directories(cucumber-cpp PUBLIC target_link_libraries(cucumber-cpp PUBLIC gtest cucumber_gherkin_lib - nlohmann_json::nlohmann_json PRIVATE cucumber-cpp.report diff --git a/cucumber-cpp/CucumberRunner.cpp b/cucumber-cpp/CucumberRunner.cpp index 9e039b2..532168e 100644 --- a/cucumber-cpp/CucumberRunner.cpp +++ b/cucumber-cpp/CucumberRunner.cpp @@ -1,56 +1,46 @@ #include "cucumber-cpp/CucumberRunner.hpp" +#include "cucumber-cpp/Context.hpp" #include "cucumber-cpp/FeatureRunner.hpp" #include "cucumber-cpp/HookScopes.hpp" -#include "cucumber-cpp/ResultStates.hpp" -#include "cucumber-cpp/ScenarioRunner.hpp" -#include "cucumber-cpp/TraceTime.hpp" -#include "nlohmann/json.hpp" -#include "nlohmann/json_fwd.hpp" +#include "cucumber/gherkin/app.hpp" +#include +#include +#include namespace cucumber_cpp { - CucumberRunner::CucumberRunner(const std::vector& args, const std::string& tagExpr, std::shared_ptr contextStorageFactory) - : tagExpr{ tagExpr } - , programContext(contextStorageFactory) + CucumberRunnerV2::InsertArgsToContext::InsertArgsToContext(Context& context, const std::vector& args) { - programContext.InsertAt("args", args); + + context.InsertAt("args", args); } - void CucumberRunner::Run(nlohmann::json& json) + CucumberRunnerV2::CucumberRunnerV2(const std::vector& args, std::string tagExpression, report::ReportHandler& reportHandler, std::shared_ptr contextStorageFactory) + : tagExpression{ std::move(tagExpression) } + , reportHandler{ reportHandler } + , programContext{ std::move(contextStorageFactory) } + , insertArgsToContext{ programContext, args } + , programHookeScope{ programContext } { - BeforeAfterAllScope programHookeScope{ programContext }; - - double totalTime = 0.0; - - std::ranges::for_each(json["features"], [this, &json, &totalTime](nlohmann::json& featureJson) - { - FeatureRunner featureRunner{ programContext, tagExpr }; - featureRunner.Run(featureJson); + } - totalTime += featureJson.value("elapsed", 0.0); - }); + std::string CucumberRunnerV2::TagExpression() const + { + return tagExpression; + } - json["elapsed"] = totalTime; + report::ReportHandler& CucumberRunnerV2::ReportHandler() + { + return reportHandler; + } - auto onlyFeaturesWithResult = json["features"] | std::views::filter([](const auto& json) - { - return json.contains("result"); - }); + Context& CucumberRunnerV2::GetContext() + { + return programContext; + } - if (onlyFeaturesWithResult.empty()) - { - // don't set result - } - else if (std::ranges::all_of(onlyFeaturesWithResult, [](const auto& featureJson) - { - return featureJson["result"] == result::success; - })) - { - json["result"] = result::success; - } - else - { - json["result"] = result::failed; - } + std::unique_ptr CucumberRunnerV2::StartFeature(const cucumber::gherkin::app::parser_result& ast) + { + return std::make_unique(*this, ast); } } diff --git a/cucumber-cpp/CucumberRunner.hpp b/cucumber-cpp/CucumberRunner.hpp index a2c87b0..b0ba241 100644 --- a/cucumber-cpp/CucumberRunner.hpp +++ b/cucumber-cpp/CucumberRunner.hpp @@ -2,22 +2,40 @@ #define CUCUMBER_CPP_CUCUMBERRUNNER_HPP #include "cucumber-cpp/Context.hpp" -#include "cucumber-cpp/Hooks.hpp" -#include "nlohmann/json_fwd.hpp" +#include "cucumber-cpp/HookScopes.hpp" +#include "cucumber-cpp/report/Report.hpp" +#include "cucumber/gherkin/app.hpp" #include #include namespace cucumber_cpp { - struct CucumberRunner + struct FeatureRunnerV2; + + struct CucumberRunnerV2 { - CucumberRunner(const std::vector& args, const std::string& tagExpr, std::shared_ptr contextStorageFactory); + public: + CucumberRunnerV2(const std::vector& args, std::string tagExpression, report::ReportHandler& reportHandler, std::shared_ptr contextStorageFactory); + ~CucumberRunnerV2() = default; + + [[nodiscard]] std::string TagExpression() const; + [[nodiscard]] report::ReportHandler& ReportHandler(); + [[nodiscard]] Context& GetContext(); - void Run(nlohmann::json& json); + std::unique_ptr StartFeature(const cucumber::gherkin::app::parser_result& ast); private: - std::string tagExpr; + struct InsertArgsToContext + { + InsertArgsToContext(Context& context, const std::vector& args); + }; + + const std::string tagExpression; + report::ReportHandler& reportHandler; + Context programContext; + InsertArgsToContext insertArgsToContext; + BeforeAfterAllScope programHookeScope; }; } diff --git a/cucumber-cpp/FeatureRunner.cpp b/cucumber-cpp/FeatureRunner.cpp index 1676f68..ddd31ae 100644 --- a/cucumber-cpp/FeatureRunner.cpp +++ b/cucumber-cpp/FeatureRunner.cpp @@ -1,62 +1,104 @@ + #include "cucumber-cpp/FeatureRunner.hpp" -#include "cucumber-cpp/HookScopes.hpp" -#include "cucumber-cpp/JsonTagToSet.hpp" -#include "cucumber-cpp/ResultStates.hpp" +#include "cucumber-cpp/Context.hpp" +#include "cucumber-cpp/CucumberRunner.hpp" #include "cucumber-cpp/ScenarioRunner.hpp" #include "cucumber-cpp/TagExpression.hpp" -#include "cucumber-cpp/TraceTime.hpp" -#include "nlohmann/json.hpp" -#include -#include +#include "cucumber-cpp/TagsToSet.hpp" +#include "cucumber/gherkin/app.hpp" +#include "cucumber/messages/feature.hpp" +#include +#include +#include +#include namespace cucumber_cpp { - FeatureRunner::FeatureRunner(Context& programContext, const std::string& tagExpr) - : tagExpr{ tagExpr } - , featureContext{ &programContext } - {} + FeatureSource FeatureSource::FromAst(const cucumber::gherkin::app::parser_result& ast) + { + return { ast.feature->name, ast.uri.value_or("unknown"), ast.feature->location.line, ast.feature->location.column.value_or(0) }; + } - void FeatureRunner::Run(nlohmann::json& json) + FeatureRunnerV2::FeatureRunnerV2(CucumberRunnerV2& cucumberRunner, const cucumber::gherkin::app::parser_result& ast) + : cucumberRunner{ cucumberRunner } + , ast{ ast } + , featureSource{ FeatureSource::FromAst(ast) } + , featureContext{ &cucumberRunner.GetContext() } + , featureHookScope{ featureContext, TagsToSet(ast.feature->tags) } { - BeforeAfterFeatureHookScope featureHookScope{ featureContext, JsonTagsToSet(json["ast"]["feature"]["tags"]) }; + } - const auto isTagExprSelected = [this](const auto& json) + FeatureRunnerV2::~FeatureRunnerV2() + { + try { - return IsTagExprSelected(tagExpr, json["tags"]); - }; + stopFeatureOnDestruction(); + } + catch (...) + { + std::cout << "\nexception caught during FeatureRunner dtor"; + } + } - double totalTime = 0.0; - std::ranges::for_each(json["scenarios"] | std::views::filter(isTagExprSelected), [this, &json, &totalTime](nlohmann::json& scenarioJson) - { - ScenarioRunner scenarioRunner{ featureContext }; - scenarioRunner.Run(scenarioJson); + const FeatureSource& FeatureRunnerV2::Source() const + { + return featureSource; + } - totalTime += scenarioJson.value("elapsed", 0.0); - }); + report::ReportHandler& FeatureRunnerV2::ReportHandler() + { + return cucumberRunner.ReportHandler(); + } + + Context& FeatureRunnerV2::GetContext() + { + return featureContext; + } - json["elapsed"] = totalTime; + const cucumber::messages::feature& FeatureRunnerV2::Feature() const + { + return ast.feature.value(); + } - const auto containsResult = [](const auto& json) + void FeatureRunnerV2::StartScenario(const cucumber::messages::pickle& pickle) + { + if (ast.feature && IsTagExprSelected(cucumberRunner.TagExpression(), TagsToSet(pickle.tags))) { - return json.contains("result"); - }; + std::call_once(startFeatureOnceFlag, &FeatureRunnerV2::StartFeatureOnce, *this); - auto onlyScenariosWithResult = json["scenarios"] | std::views::filter(containsResult); + ScenarioRunnerV2 scenarioRunner{ *this, pickle }; - if (onlyScenariosWithResult.empty()) - { - // don't set result - } - else if (std::ranges::all_of(onlyScenariosWithResult, [](const nlohmann::json& scenarioJson) - { - return scenarioJson["result"] == result::success; - })) - { - json["result"] = result::success; + scenarioRunner.Run(); + + duration += scenarioRunner.Duration(); + + if (result == decltype(result)::undefined || result == decltype(result)::success) + result = scenarioRunner.Result(); } - else + } + + report::ReportHandler::Result FeatureRunnerV2::Result() const + { + return result; + } + + TraceTime::Duration FeatureRunnerV2::Duration() const + { + return duration; + } + + void FeatureRunnerV2::StartFeatureOnce() + { + ReportHandler().FeatureStart(Source()); + + stopFeatureOnDestruction = [this] { - json["result"] = result::failed; - } + StopFeatureOnDestruction(); + }; + } + + void FeatureRunnerV2::StopFeatureOnDestruction() noexcept + { + ReportHandler().FeatureEnd(Source(), report::ReportHandler::Result::undefined, Duration()); } } diff --git a/cucumber-cpp/FeatureRunner.hpp b/cucumber-cpp/FeatureRunner.hpp index 4e5c764..f738f8d 100644 --- a/cucumber-cpp/FeatureRunner.hpp +++ b/cucumber-cpp/FeatureRunner.hpp @@ -1,20 +1,66 @@ #ifndef CUCUMBER_CPP_FEATURERUNNER_HPP #define CUCUMBER_CPP_FEATURERUNNER_HPP -#include "cucumber-cpp/Hooks.hpp" -#include +#include "cucumber-cpp/Context.hpp" +#include "cucumber-cpp/HookScopes.hpp" +#include "cucumber-cpp/report/Report.hpp" +#include "cucumber/gherkin/app.hpp" +#include "cucumber/messages/feature.hpp" +#include "cucumber/messages/pickle.hpp" +#include +#include +#include +#include namespace cucumber_cpp { - struct FeatureRunner + struct CucumberRunnerV2; + + struct FeatureSource { - FeatureRunner(Context& programContext, const std::string& tagExpr); + std::string name; + std::filesystem::path path; + std::size_t line; + std::size_t column; + + static FeatureSource FromAst(const cucumber::gherkin::app::parser_result& ast); + }; + + struct FeatureRunnerV2 + { + public: + FeatureRunnerV2(CucumberRunnerV2& cucumberRunner, const cucumber::gherkin::app::parser_result& ast); + ~FeatureRunnerV2(); + + [[nodiscard]] const FeatureSource& Source() const; + + [[nodiscard]] report::ReportHandler& ReportHandler(); + [[nodiscard]] Context& GetContext(); - void Run(nlohmann::json& json); + [[nodiscard]] const cucumber::messages::feature& Feature() const; + + [[nodiscard]] report::ReportHandler::Result Result() const; + [[nodiscard]] TraceTime::Duration Duration() const; + + void StartScenario(const cucumber::messages::pickle& pickle); private: - std::string tagExpr; + void StartFeatureOnce(); + void StopFeatureOnDestruction() noexcept; + + CucumberRunnerV2& cucumberRunner; + const cucumber::gherkin::app::parser_result& ast; + + std::once_flag startFeatureOnceFlag; + std::function stopFeatureOnDestruction{ [] { /* do nothing */ } }; + + FeatureSource featureSource; + + report::ReportHandler::Result result{ report::ReportHandler::Result::undefined }; + TraceTime::Duration duration{ 0 }; + Context featureContext; + BeforeAfterFeatureHookScope featureHookScope; }; } diff --git a/cucumber-cpp/HookRegistry.cpp b/cucumber-cpp/HookRegistry.cpp index 4197c1f..8440609 100644 --- a/cucumber-cpp/HookRegistry.cpp +++ b/cucumber-cpp/HookRegistry.cpp @@ -2,6 +2,7 @@ #include "cucumber-cpp/HookRegistry.hpp" #include "cucumber-cpp/TagExpression.hpp" #include +#include namespace cucumber_cpp { @@ -9,7 +10,7 @@ namespace cucumber_cpp { auto TypeFilter(HookType hookType) { - return [hookType](const HookRegistry::Entry& entry) -> bool + return [hookType](const HookRegistry::Entry& entry) { return entry.type == hookType; }; @@ -20,7 +21,7 @@ namespace cucumber_cpp : context{ context } {} - TagExpressionMatch::TagExpressionMatch(const std::string& tagExpression, const std::set& tags) + TagExpressionMatch::TagExpressionMatch(const std::string& tagExpression, const std::set>& tags) : matched{ IsTagExprSelected(tagExpression, tags) } {} @@ -29,11 +30,11 @@ namespace cucumber_cpp return matched; } - HookTagExpression::HookTagExpression(const std::string& tagExpression) - : tagExpression{ tagExpression } + HookTagExpression::HookTagExpression(std::string tagExpression) + : tagExpression{ std::move(tagExpression) } {} - std::unique_ptr HookTagExpression::Match(const std::set& tags) const + std::unique_ptr HookTagExpression::Match(const std::set>& tags) const { return std::make_unique(tagExpression, tags); } @@ -43,17 +44,13 @@ namespace cucumber_cpp return tagExpression; } - std::vector HookRegistryBase::Query(HookType hookType, const std::set& tags) const + std::vector HookRegistryBase::Query(HookType hookType, const std::set>& tags) const { std::vector matches; for (const Entry& entry : registry | std::views::filter(TypeFilter(hookType))) - { if (auto match = entry.hookTagExpression.Match(tags); match->Matched()) - { matches.emplace_back(std::move(match), entry.factory, entry.hookTagExpression); - } - } return matches; } diff --git a/cucumber-cpp/HookRegistry.hpp b/cucumber-cpp/HookRegistry.hpp index ceea9b4..ebc4a6f 100644 --- a/cucumber-cpp/HookRegistry.hpp +++ b/cucumber-cpp/HookRegistry.hpp @@ -31,7 +31,7 @@ namespace cucumber_cpp struct TagExpressionMatch { - TagExpressionMatch(const std::string& tagExpression, const std::set& tags); + TagExpressionMatch(const std::string& tagExpression, const std::set>& tags); bool Matched() const; @@ -41,9 +41,9 @@ namespace cucumber_cpp struct HookTagExpression { - explicit HookTagExpression(const std::string& tagExpression); + explicit HookTagExpression(std::string tagExpression); - std::unique_ptr Match(const std::set& tags) const; + std::unique_ptr Match(const std::set>& tags) const; std::string TagExpression() const; private: @@ -66,7 +66,7 @@ namespace cucumber_cpp std::unique_ptr (&factory)(Context& context); }; - std::vector Query(HookType hookType, const std::set& tags) const; + std::vector Query(HookType hookType, const std::set>& tags) const; std::size_t Size() const; std::size_t Size(HookType hookType) const; diff --git a/cucumber-cpp/HookScopes.cpp b/cucumber-cpp/HookScopes.cpp index ac71414..f95b14f 100644 --- a/cucumber-cpp/HookScopes.cpp +++ b/cucumber-cpp/HookScopes.cpp @@ -2,16 +2,19 @@ #include "cucumber-cpp/HookScopes.hpp" #include "cucumber-cpp/Context.hpp" #include "cucumber-cpp/HookRegistry.hpp" -#include "nlohmann/json.hpp" #include namespace cucumber_cpp { - void BeforeAfterScopeExecuter::ExecuteAll(const std::vector& matches, Context& context) + BeforeAfterScopeExecuter::BeforeAfterScopeExecuter(Context& context, const std::set>& tags) + : context{ context } + , tags{ tags } { - for (const HookMatch& match : matches) - { + } + + void BeforeAfterScopeExecuter::ExecuteAll(HookType hook) + { + for (const auto& match : HookRegistry::Instance().Query(hook, tags)) match.factory(context)->Execute(); - } } } diff --git a/cucumber-cpp/HookScopes.hpp b/cucumber-cpp/HookScopes.hpp index cc366f7..44f7866 100644 --- a/cucumber-cpp/HookScopes.hpp +++ b/cucumber-cpp/HookScopes.hpp @@ -3,7 +3,6 @@ #include "cucumber-cpp/Context.hpp" #include "cucumber-cpp/HookRegistry.hpp" -#include "nlohmann/json_fwd.hpp" #include #include @@ -12,21 +11,23 @@ namespace cucumber_cpp struct BeforeAfterScopeExecuter { - void ExecuteAll(const std::vector& matches, Context& context); + BeforeAfterScopeExecuter(Context& context, const std::set>& tags); + + void ExecuteAll(HookType hook); + + private: + Context& context; + std::set> tags; }; template struct BeforeAfterScope : BeforeAfterScopeExecuter { public: - BeforeAfterScope(Context& context, const std::set& tags = {}); + BeforeAfterScope(Context& context, const std::set>& tags = {}); protected: ~BeforeAfterScope(); - - private: - Context& context; - std::set tags; }; struct BeforeAfterAllScope : BeforeAfterScope @@ -54,17 +55,16 @@ namespace cucumber_cpp ////////////////////////// template - BeforeAfterScope::BeforeAfterScope(Context& context, const std::set& tags) - : context{ context } - , tags{ tags } + BeforeAfterScope::BeforeAfterScope(Context& context, const std::set>& tags) + : BeforeAfterScopeExecuter{ context, tags } { - ExecuteAll(HookRegistry::Instance().Query(BeforeHook, tags), context); + BeforeAfterScopeExecuter::ExecuteAll(BeforeHook); } template BeforeAfterScope::~BeforeAfterScope() { - ExecuteAll(HookRegistry::Instance().Query(AfterHook, tags), context); + BeforeAfterScopeExecuter::ExecuteAll(AfterHook); } } diff --git a/cucumber-cpp/JsonTagToSet.cpp b/cucumber-cpp/JsonTagToSet.cpp deleted file mode 100644 index 337b740..0000000 --- a/cucumber-cpp/JsonTagToSet.cpp +++ /dev/null @@ -1,24 +0,0 @@ -#include "cucumber-cpp/JsonTagToSet.hpp" -#include "nlohmann/json.hpp" -#include -#include -#include -#include -#include -#include - -namespace cucumber_cpp -{ - std::set JsonTagsToSet(const nlohmann::json& json) - { - static const auto getName = [](const nlohmann::json& json) - { - return json["name"]; - }; - - std::set tagSet; - std::ranges::copy(json | std::views::transform(getName), std::inserter(tagSet, tagSet.end())); - - return tagSet; - } -} diff --git a/cucumber-cpp/JsonTagToSet.hpp b/cucumber-cpp/JsonTagToSet.hpp deleted file mode 100644 index 8ca9136..0000000 --- a/cucumber-cpp/JsonTagToSet.hpp +++ /dev/null @@ -1,13 +0,0 @@ -#ifndef CUCUMBER_CPP_JSONTAGTOSET_HPP -#define CUCUMBER_CPP_JSONTAGTOSET_HPP - -#include "nlohmann/json_fwd.hpp" -#include -#include - -namespace cucumber_cpp -{ - std::set JsonTagsToSet(const nlohmann::json& json); -} - -#endif diff --git a/cucumber-cpp/ScenarioRunner.cpp b/cucumber-cpp/ScenarioRunner.cpp index 72c68a5..44090d8 100644 --- a/cucumber-cpp/ScenarioRunner.cpp +++ b/cucumber-cpp/ScenarioRunner.cpp @@ -1,78 +1,109 @@ #include "cucumber-cpp/ScenarioRunner.hpp" +#include "cucumber-cpp/FeatureRunner.hpp" #include "cucumber-cpp/HookScopes.hpp" -#include "cucumber-cpp/JsonTagToSet.hpp" -#include "cucumber-cpp/ResultStates.hpp" #include "cucumber-cpp/StepRunner.hpp" -#include "cucumber-cpp/TraceTime.hpp" -#include "nlohmann/json.hpp" -#include "gtest/gtest.h" +#include "cucumber-cpp/TagsToSet.hpp" +#include "cucumber-cpp/report/Report.hpp" #include +#include #include namespace cucumber_cpp { namespace { - struct StepRunnerStrategy : RunStepStrategy + [[nodiscard]] ScenarioSource LookupScenarioSource(const FeatureSource& featureSource, const cucumber::messages::feature& feature, const std::string& id) { - void Run(StepRunner& stepRunner, nlohmann::json& json, nlohmann::json& scenarioTags) override - { - stepRunner.Run(json, scenarioTags); - } - }; + for (const auto& child : feature.children) + if (child.background && child.background->id == id) + return ScenarioSource::FromAst(featureSource, *child.background); + else if (child.scenario && child.scenario->id == id) + return ScenarioSource::FromAst(featureSource, *child.scenario); - struct SkipStepStrategy : RunStepStrategy - { - void Run([[maybe_unused]] StepRunner& stepRunner, nlohmann::json& json, nlohmann::json& scenarioTags) override + struct ScenarioSourceNotFoundError : std::out_of_range { - json["result"] = result::skipped; - } - }; + using std::out_of_range::out_of_range; + }; + + throw ScenarioSourceNotFoundError{ "ScenarioSource not found" }; + } } - ScenarioRunner::ScenarioRunner(Context& programContext) - : scenarioContext{ &programContext } - , runStepStrategy{ std::make_unique() } + ScenarioSource ScenarioSource::FromAst(const FeatureSource& featureSource, const cucumber::messages::scenario& scenario) { + return { featureSource, scenario.name, scenario.location.line, scenario.location.column.value_or(0) }; } - void ScenarioRunner::Run(nlohmann::json& scenarioJson) + ScenarioSource ScenarioSource::FromAst(const FeatureSource& featureSource, const cucumber::messages::background& background) { - BeforeAfterHookScope scenarioHookScope{ scenarioContext, JsonTagsToSet(scenarioJson["tags"]) }; - double totalTime = 0.0; + return { featureSource, background.name, background.location.line, background.location.column.value_or(0) }; + } - std::ranges::for_each(scenarioJson["steps"], [&scenarioJson, this, &totalTime](nlohmann::json& stepJson) - { - StepRunner stepRunner{ scenarioContext }; - runStepStrategy->Run(stepRunner, stepJson, scenarioJson["tags"]); + ScenarioRunnerV2::ScenarioRunnerV2(FeatureRunnerV2& featureRunner, const cucumber::messages::pickle& scenarioPickle) + : featureRunner{ featureRunner } + , pickle{ scenarioPickle } + , scenarioTags{ TagsToSet(scenarioPickle.tags) } + , scenarioSource{ LookupScenarioSource(featureRunner.Source(), Ast(), pickle.ast_node_ids[0]) } + , scenarioContext{ &featureRunner.GetContext() } + , scenarioHookScope{ scenarioContext, scenarioTags } + { + ReportHandler().ScenarioStart(scenarioSource); + } - if (auto result = stepJson["result"]; result != result::success && result != result::skipped) - { - runStepStrategy = std::make_unique(); - } + ScenarioRunnerV2::~ScenarioRunnerV2() + { + ReportHandler().ScenarioEnd(scenarioSource, Result(), Duration()); + } - totalTime += stepJson.value("elapsed", 0.0); - }); + const ScenarioSource& ScenarioRunnerV2::Source() const + { + return scenarioSource; + } - scenarioJson["elapsed"] = totalTime; + report::ReportHandler& ScenarioRunnerV2::ReportHandler() + { + return featureRunner.ReportHandler(); + } - auto iter = std::ranges::find_if_not(scenarioJson["steps"], [](auto stepJson) - { - return stepJson["result"] == result::success; - }); + Context& ScenarioRunnerV2::GetContext() + { + return scenarioContext; + } - if (iter == scenarioJson["steps"].end()) - { - scenarioJson["result"] = result::success; - } - else - { - scenarioJson["result"] = (*iter)["result"]; - } + const std::set>& ScenarioRunnerV2::GetScenarioTags() const + { + return scenarioTags; + } + + const cucumber::messages::feature& ScenarioRunnerV2::Ast() const + { + return featureRunner.Feature(); + } + + report::ReportHandler::Result ScenarioRunnerV2::Result() const + { + return result; + } + + TraceTime::Duration ScenarioRunnerV2::Duration() const + { + return duration; } - void ScenarioRunner::OnTestPartResult(const testing::TestPartResult& testPartResult) + void ScenarioRunnerV2::Run() { - runStepStrategy = std::make_unique(); + for (const auto& step : pickle.steps) + if (result == decltype(result)::success || result == decltype(result)::undefined) + { + StepRunnerV2 stepRunner{ *this, step }; + stepRunner.Run(); + + duration += stepRunner.Duration(); + + if (const auto stepResult = stepRunner.Result(); stepResult != decltype(stepResult)::success || stepResult != decltype(stepResult)::skipped) + result = stepResult; + } + else + SkipStepRunnerV2{ *this, step }; } } diff --git a/cucumber-cpp/ScenarioRunner.hpp b/cucumber-cpp/ScenarioRunner.hpp index 2077bc0..5436325 100644 --- a/cucumber-cpp/ScenarioRunner.hpp +++ b/cucumber-cpp/ScenarioRunner.hpp @@ -2,31 +2,65 @@ #define CUCUMBER_CPP_SCENARIORUNNER_HPP #include "cucumber-cpp/Context.hpp" -#include "cucumber-cpp/Hooks.hpp" -#include "cucumber-cpp/OnTestPartResultEventListener.hpp" -#include "cucumber-cpp/StepRunner.hpp" +#include "cucumber-cpp/HookScopes.hpp" +#include "cucumber-cpp/TraceTime.hpp" +#include "cucumber-cpp/report/Report.hpp" +#include "cucumber/messages/background.hpp" +#include "cucumber/messages/feature.hpp" +#include "cucumber/messages/pickle.hpp" +#include "cucumber/messages/scenario.hpp" +#include +#include namespace cucumber_cpp { + struct FeatureSource; - struct RunStepStrategy + struct ScenarioSource { - virtual ~RunStepStrategy() = default; - virtual void Run(StepRunner& stepRunner, nlohmann::json& json, nlohmann::json& scenarioTags) = 0; + const FeatureSource& featureSource; + const std::string name; + const std::size_t line; + const std::size_t column; + + static ScenarioSource FromAst(const FeatureSource& featureSource, const cucumber::messages::scenario& scenario); + static ScenarioSource FromAst(const FeatureSource& featureSource, const cucumber::messages::background& background); }; - struct ScenarioRunner : OnTestPartResultEventListener + struct FeatureRunnerV2; + + struct ScenarioRunnerV2 { - ScenarioRunner(Context& programContext); + public: + ScenarioRunnerV2(FeatureRunnerV2& featureRunner, const cucumber::messages::pickle& pickle); + ~ScenarioRunnerV2(); + + [[nodiscard]] const ScenarioSource& Source() const; + + [[nodiscard]] report::ReportHandler& ReportHandler(); + [[nodiscard]] Context& GetContext(); + [[nodiscard]] const std::set>& GetScenarioTags() const; + + [[nodiscard]] const cucumber::messages::feature& Ast() const; - void Run(nlohmann::json& json); + [[nodiscard]] report::ReportHandler::Result Result() const; + [[nodiscard]] TraceTime::Duration Duration() const; - // implementation of OnTestPartResultEventListener - void OnTestPartResult(const testing::TestPartResult& testPartResult) override; + void Run(); private: + FeatureRunnerV2& featureRunner; + const cucumber::messages::pickle& pickle; + + std::set> scenarioTags; + + report::ReportHandler::Result result{ report::ReportHandler::Result::undefined }; + TraceTime::Duration duration{ 0 }; + + ScenarioSource scenarioSource; + Context scenarioContext; - std::unique_ptr runStepStrategy; + BeforeAfterHookScope scenarioHookScope; }; } diff --git a/cucumber-cpp/StepRegistry.cpp b/cucumber-cpp/StepRegistry.cpp index 3de4866..64f1188 100644 --- a/cucumber-cpp/StepRegistry.cpp +++ b/cucumber-cpp/StepRegistry.cpp @@ -1,11 +1,12 @@ #include "cucumber-cpp/StepRegistry.hpp" #include "cucumber-cpp/Context.hpp" -#include "nlohmann/json.hpp" +#include "cucumber-cpp/StepRunner.hpp" #include #include #include #include +#include #include #include @@ -18,15 +19,6 @@ namespace cucumber_cpp return toString; } - void SetStepParameters(nlohmann::json& json, const std::smatch& matchResults) - { - std::ranges::copy(matchResults - // the first element is the whole matched string, - // we are only interested in the actual matches - | std::views::drop(1) | std::views::transform(ToString), - std::back_inserter(json["parameters"])); - } - std::vector ToVector(const std::smatch& matchResults) { auto range = matchResults | std::views::drop(1) | std::views::transform(ToString); @@ -62,14 +54,14 @@ namespace cucumber_cpp auto TypeFilter(StepType stepType) { - return [stepType](const StepRegistry::Entry& entry) -> bool + return [stepType](const StepRegistry::Entry& entry) { return entry.type == stepType || entry.type == StepType::any; }; }; } - Step::Step(Context& context, const nlohmann::json& table) + Step::Step(Context& context, const Table& table) : context{ context } , table{ table } {} @@ -91,10 +83,13 @@ namespace cucumber_cpp void Step::Any(StepType type, const std::string& step) { - const auto stepMatches = StepRegistry::Instance().Query(type, step); + const auto stepMatch = StepRegistry::Instance().Query(type, step); + stepMatch.factory(context, {})->Execute(stepMatch.regexMatch->Matches()); + } - if (const auto& step = stepMatches.front(); stepMatches.size() == 1) - step.factory(context, {})->Execute(step.regexMatch->Matches()); + void Step::Pending(const std::string& message, std::source_location current) const noexcept(false) + { + throw StepPending{ message, current }; } RegexMatch::RegexMatch(const std::regex& regex, const std::string& expression) @@ -135,24 +130,21 @@ namespace cucumber_cpp return instance; } - std::vector StepRegistryBase::Query(StepType stepType, std::string expression) const + StepMatch StepRegistryBase::Query(StepType stepType, const std::string& expression) const { std::vector matches; for (const Entry& entry : registry | std::views::filter(TypeFilter(stepType))) - { if (auto match = entry.regex.Match(expression); match->Matched()) - { matches.emplace_back(std::move(match), entry.factory, entry.regex); - } - } - if (matches.size() == 0) - { - throw StepNotFound{ "Step: \"" + expression + "\" not found" }; - } + if (matches.empty()) + throw StepNotFoundError{}; - return matches; + if (matches.size() > 1) + throw AmbiguousStepError{ std::move(matches) }; + + return std::move(matches.front()); } std::size_t StepRegistryBase::Size() const diff --git a/cucumber-cpp/StepRegistry.hpp b/cucumber-cpp/StepRegistry.hpp index f83d7c5..10d38ff 100644 --- a/cucumber-cpp/StepRegistry.hpp +++ b/cucumber-cpp/StepRegistry.hpp @@ -3,11 +3,12 @@ #include "cucumber-cpp/Body.hpp" #include "cucumber-cpp/Context.hpp" -#include "nlohmann/json_fwd.hpp" +#include #include #include #include #include +#include #include #include @@ -21,29 +22,53 @@ namespace cucumber_cpp any }; + struct TableValue + { + template + T As() const; + + std::string value; + }; + + using Table = std::vector>; + struct Step { - Step(Context& context, const nlohmann::json& table); + struct StepPending : std::exception + { + StepPending(std::string message, std::source_location sourceLocation) + : message{ std::move(message) } + , sourceLocation{ sourceLocation } + { + } + + std::string message; + std::source_location sourceLocation; + }; + + Step(Context& context, const Table& table); protected: void Given(const std::string& step); void When(const std::string& step); void Then(const std::string& step); + [[noreturn]] void Pending(const std::string& message, std::source_location current = std::source_location::current()) const noexcept(false); + private: void Any(StepType type, const std::string& step); protected: Context& context; - const nlohmann::json& table; + const Table& table; }; struct RegexMatch { RegexMatch(const std::regex& regex, const std::string& expression); - bool Matched() const; - std::vector Matches() const; + [[nodiscard]] bool Matched() const; + [[nodiscard]] std::vector Matches() const; private: bool matched; @@ -54,8 +79,8 @@ namespace cucumber_cpp { explicit StepRegex(const std::string& string); - std::unique_ptr Match(const std::string& expression) const; - std::string String() const; + [[nodiscard]] std::unique_ptr Match(const std::string& expression) const; + [[nodiscard]] std::string String() const; private: std::string string; @@ -65,28 +90,40 @@ namespace cucumber_cpp struct StepMatch { std::unique_ptr regexMatch; - std::unique_ptr (&factory)(Context& context, const nlohmann::json& table); + std::unique_ptr (&factory)(Context& context, const Table& table); const StepRegex& stepRegex; }; struct StepRegistryBase { - struct StepNotFound : std::out_of_range + struct StepNotFoundError : std::exception { - using std::out_of_range::out_of_range; + using std::exception::exception; + }; + + struct AmbiguousStepError : std::exception + { + explicit AmbiguousStepError(std::vector&& matches) + : matches{ std::move(matches) } + {} + + AmbiguousStepError(const AmbiguousStepError&) = delete; + AmbiguousStepError& operator=(const AmbiguousStepError&) = delete; + + const std::vector matches; }; struct Entry { - StepType type; + StepType type{}; StepRegex regex; - std::unique_ptr (&factory)(Context& context, const nlohmann::json& table); + std::unique_ptr (&factory)(Context& context, const Table& table); }; - std::vector Query(StepType stepType, std::string expression) const; + [[nodiscard]] StepMatch Query(StepType stepType, const std::string& expression) const; - std::size_t Size() const; - std::size_t Size(StepType stepType) const; + [[nodiscard]] std::size_t Size() const; + [[nodiscard]] std::size_t Size(StepType stepType) const; protected: template @@ -94,7 +131,7 @@ namespace cucumber_cpp private: template - static std::unique_ptr Construct(Context& context, const nlohmann::json& table); + static std::unique_ptr Construct(Context& context, const Table& table); std::vector registry; }; @@ -115,6 +152,12 @@ namespace cucumber_cpp // implementation // ////////////////////////// + template + T TableValue::As() const + { + return StringTo(value); + } + template std::size_t StepRegistryBase::Register(const std::string& matcher, StepType stepType) { @@ -123,7 +166,7 @@ namespace cucumber_cpp } template - std::unique_ptr StepRegistryBase::Construct(Context& context, const nlohmann::json& table) + std::unique_ptr StepRegistryBase::Construct(Context& context, const Table& table) { return std::make_unique(context, table); } diff --git a/cucumber-cpp/StepRunner.cpp b/cucumber-cpp/StepRunner.cpp index e8da28f..bc1d3f0 100644 --- a/cucumber-cpp/StepRunner.cpp +++ b/cucumber-cpp/StepRunner.cpp @@ -1,17 +1,19 @@ #include "cucumber-cpp/StepRunner.hpp" #include "cucumber-cpp/HookScopes.hpp" -#include "cucumber-cpp/JsonTagToSet.hpp" #include "cucumber-cpp/OnTestPartResultEventListener.hpp" -#include "cucumber-cpp/ResultStates.hpp" #include "cucumber-cpp/Rtrim.hpp" #include "cucumber-cpp/StepRegistry.hpp" #include "cucumber-cpp/TraceTime.hpp" -#include "nlohmann/json_fwd.hpp" +#include "cucumber-cpp/report/Report.hpp" +#include "cucumber/messages/pickle_step_type.hpp" #include "gtest/gtest.h" #include #include #include #include +#include +#include +#include #include #include #include @@ -19,97 +21,253 @@ namespace cucumber_cpp { + namespace { - struct AppendFailureOnTestPartResultEvent : OnTestPartResultEventListener + struct AppendFailureOnTestPartResultEvent + : OnTestPartResultEventListener { - explicit AppendFailureOnTestPartResultEvent(nlohmann::json& json) - : json{ json } + explicit AppendFailureOnTestPartResultEvent(report::ReportHandler& reportHandler) + : reportHandler{ reportHandler } + { + } + + ~AppendFailureOnTestPartResultEvent() override { + for (const auto& error : errors) + reportHandler.Failure(error.message(), error.file_name(), error.line_number(), 0); } + AppendFailureOnTestPartResultEvent(const AppendFailureOnTestPartResultEvent&) = delete; + AppendFailureOnTestPartResultEvent& operator=(const AppendFailureOnTestPartResultEvent&) = delete; + void OnTestPartResult(const testing::TestPartResult& testPartResult) override { - json["errors"].push_back({ { "file", testPartResult.file_name() }, - { "line", testPartResult.line_number() }, - { "message", testPartResult.message() } }); + errors.emplace_back(testPartResult); + } + + [[nodiscard]] bool HasFailures() const + { + return !errors.empty(); + } + + private: + report::ReportHandler& reportHandler; + std::vector errors; + }; + + struct CaptureAndTraceStdOut + { + explicit CaptureAndTraceStdOut(report::ReportHandler& reportHandler) + : reportHandler{ reportHandler } + { + testing::internal::CaptureStdout(); + testing::internal::CaptureStderr(); } + ~CaptureAndTraceStdOut() + { + if (auto str = testing::internal::GetCapturedStdout(); !str.empty()) + { + Rtrim(str); + reportHandler.Trace(str); + } + + if (auto str = testing::internal::GetCapturedStderr(); !str.empty()) + { + Rtrim(str); + reportHandler.Trace(str); + } + } + + CaptureAndTraceStdOut(const CaptureAndTraceStdOut&) = delete; + CaptureAndTraceStdOut& operator=(const CaptureAndTraceStdOut&) = delete; + private: - nlohmann::json& json; + report::ReportHandler& reportHandler; + }; + + const std::map stepTypeLut{ + { cucumber::messages::pickle_step_type::CONTEXT, StepType::given }, + { cucumber::messages::pickle_step_type::ACTION, StepType::when }, + { cucumber::messages::pickle_step_type::OUTCOME, StepType::then }, }; - const std::map stepTypeLut{ - { "Context", StepType::given }, - { "Action", StepType::when }, - { "Outcome", StepType::then }, + const std::map stepTypeToString{ + { cucumber::messages::pickle_step_type::CONTEXT, "Given" }, + { cucumber::messages::pickle_step_type::ACTION, "When" }, + { cucumber::messages::pickle_step_type::OUTCOME, "Then" }, }; + + std::vector> PickleArgumentToTable(const std::optional& optionalPickleStepArgument) + { + try + { + const auto& pickleStepArgument = optionalPickleStepArgument.value(); + const auto& optionalDataTable = pickleStepArgument.data_table; + const auto& dataTable = optionalDataTable.value(); + + std::vector> table; + + for (const auto& row : dataTable.rows) + { + table.emplace_back(); + + for (const auto& cols : row.cells) + table.back().emplace_back(cols.value); + } + return table; + } + catch (const std::bad_optional_access&) + { + return {}; + } + } + + [[nodiscard]] StepSource LookupStepSource(const ScenarioSource& scenarioSource, const cucumber::messages::feature& feature, const cucumber::messages::pickle_step& pickleStep, const std::string& id) + { + for (const auto& child : feature.children) + if (child.background) + { + const auto iter = std::ranges::find(child.background->steps, id, &cucumber::messages::step::id); + if (iter != child.background->steps.end()) + return StepSource::FromAst(scenarioSource, *iter, pickleStep); + } + else if (child.scenario) + { + const auto iter = std::ranges::find(child.scenario->steps, id, &cucumber::messages::step::id); + if (iter != child.scenario->steps.end()) + return StepSource::FromAst(scenarioSource, *iter, pickleStep); + } + + struct StepSourceNotFoundError : std::out_of_range + { + using std::out_of_range::out_of_range; + }; + + throw StepSourceNotFoundError{ "StepSource not found" }; + } } - StepRunner::StepRunner(Context& context) - : context{ context } + StepSource StepSource::FromAst(const ScenarioSource& scenarioSource, const cucumber::messages::pickle_step& pickleStep) { + return { scenarioSource, pickleStep.text, stepTypeToString.at(pickleStep.type.value()), 0, 0 }; } - void StepRunner::Run(nlohmann::json& json, nlohmann::json& scenarioTags) + StepSource StepSource::FromAst(const ScenarioSource& scenarioSource, const cucumber::messages::step& step, const cucumber::messages::pickle_step& pickleStep) { - testing::internal::CaptureStdout(); - testing::internal::CaptureStderr(); + return { scenarioSource, pickleStep.text, stepTypeToString.at(pickleStep.type.value()), step.location.line, step.location.column.value_or(0) }; + } + SkipStepRunnerV2::SkipStepRunnerV2(ScenarioRunnerV2& scenarioRunner, const cucumber::messages::pickle_step& pickleStep) + : scenarioRunner{ scenarioRunner } + , stepSource{ LookupStepSource(scenarioRunner.Source(), scenarioRunner.Ast(), pickleStep, pickleStep.ast_node_ids[0]) } + { + scenarioRunner.ReportHandler().StepStart(stepSource); + } + + SkipStepRunnerV2::~SkipStepRunnerV2() + { + scenarioRunner.ReportHandler().StepEnd(stepSource, Result(), Duration()); + } + + report::ReportHandler::Result SkipStepRunnerV2::Result() const + { + return report::ReportHandler::Result::skipped; + } + + TraceTime::Duration SkipStepRunnerV2::Duration() const + { + return TraceTime::Duration{ 0 }; + } + + StepRunnerV2::StepRunnerV2(ScenarioRunnerV2& scenarioRunner, const cucumber::messages::pickle_step& pickleStep) + : scenarioRunner{ scenarioRunner } + , pickleStep{ pickleStep } + , stepSource{ LookupStepSource(scenarioRunner.Source(), scenarioRunner.Ast(), pickleStep, pickleStep.ast_node_ids[0]) } + { + ReportHandler().StepStart(Source()); + } + + StepRunnerV2::~StepRunnerV2() + { + ReportHandler().StepEnd(Source(), Result(), Duration()); + } + + const StepSource& StepRunnerV2::Source() const + { + return stepSource; + } + + report::ReportHandler& StepRunnerV2::ReportHandler() + { + return scenarioRunner.ReportHandler(); + } + + report::ReportHandler::Result StepRunnerV2::Result() const + { + return result; + } + + TraceTime::Duration StepRunnerV2::Duration() const + { + return traceTime.Delta(); + } + + void StepRunnerV2::Run() + { try { - AppendFailureOnTestPartResultEvent appendFailureOnTestPartResultEvent{ json }; + const auto stepMatch = StepRegistry::Instance().Query(stepTypeLut.at(*pickleStep.type), pickleStep.text); - const auto stepTypeStr = json["type"].get(); - const auto stepMatches = StepRegistry::Instance().Query(stepTypeLut.at(stepTypeStr), json["text"]); + AppendFailureOnTestPartResultEvent appendFailureOnTestPartResultEvent{ ReportHandler() }; + CaptureAndTraceStdOut captureAndTraceStdOut{ ReportHandler() }; + TraceTime::Scoped scopedTime{ traceTime }; - if (const auto& step = stepMatches.front(); stepMatches.size() == 1) - { - TraceTime traceTime{ json }; + BeforeAfterStepHookScope stepHookScope{ scenarioRunner.GetContext(), scenarioRunner.GetScenarioTags() }; - BeforeAfterStepHookScope stepHookScope{ context, JsonTagsToSet(scenarioTags) }; + stepMatch.factory(scenarioRunner.GetContext(), PickleArgumentToTable(pickleStep.argument))->Execute(stepMatch.regexMatch->Matches()); - step.factory(context, json["argument"]["dataTable"])->Execute(step.regexMatch->Matches()); - } - - if (json.count("errors") == 0) - { - json["result"] = result::success; - } + if (appendFailureOnTestPartResultEvent.HasFailures()) + result = decltype(result)::failed; else - { - json["result"] = result::failed; - } + result = decltype(result)::success; } - catch ([[maybe_unused]] const StepRegistry::StepNotFound& e) + catch (const StepRegistryBase::StepNotFoundError& /* e */) { - json["result"] = result::undefined; + result = decltype(result)::error; + ReportHandler().Error("Step \"" + stepSource.type + " " + stepSource.name + "\" not found"); } - catch (const std::exception& e) + catch (const StepRegistryBase::AmbiguousStepError& e) { - json["error"].push_back({ { "file", "unknown" }, - { "line", 0 }, - { "message", std::string{ "Exception thrown: " } + e.what() } }); - json["result"] = result::error; + std::ostringstream out; + out << "Ambiguous step: " << pickleStep.text << "\nMatches:"; + + for (const auto& step : e.matches) + out << "\n\t" << step.stepRegex.String(); + + result = decltype(result)::ambiguous; + ReportHandler().Error(out.str()); } - catch (...) + catch (const Step::StepPending& e) { - json["error"].push_back({ { "file", "unknown" }, - { "line", 0 }, - { "message", "Unknown exception thrown" } }); - json["result"] = result::error; + result = decltype(result)::pending; + ReportHandler().Error(e.message, e.sourceLocation.file_name(), e.sourceLocation.line(), e.sourceLocation.column()); } - - if (auto str = testing::internal::GetCapturedStdout(); !str.empty()) + catch (const std::source_location& loc) { - Rtrim(str); - json["stdout"] = str; + result = decltype(result)::error; + ReportHandler().Error("Unknown error", loc.file_name(), loc.line(), loc.column()); } - - if (auto str = testing::internal::GetCapturedStderr(); !str.empty()) + catch (const std::exception& e) + { + result = decltype(result)::error; + ReportHandler().Error(std::string{ "Exception thrown: " } + e.what()); + } + catch (...) { - Rtrim(str); - json["stderr"] = str; + result = decltype(result)::error; + ReportHandler().Error("Unknown exception thrown"); } } } diff --git a/cucumber-cpp/StepRunner.hpp b/cucumber-cpp/StepRunner.hpp index 5b52d66..86e5ccc 100644 --- a/cucumber-cpp/StepRunner.hpp +++ b/cucumber-cpp/StepRunner.hpp @@ -3,17 +3,62 @@ #include "cucumber-cpp/Context.hpp" #include "cucumber-cpp/Hooks.hpp" +#include "cucumber-cpp/ScenarioRunner.hpp" +#include "cucumber-cpp/TraceTime.hpp" +#include "cucumber/messages/pickle_step.hpp" +#include namespace cucumber_cpp { - struct StepRunner + struct StepSource { - StepRunner(Context& context); + const ScenarioSource& scenarioSource; + const std::string name; + const std::string type; + const std::size_t line; + const std::size_t column; - void Run(nlohmann::json& json, nlohmann::json& scenarioTags); + static StepSource FromAst(const ScenarioSource& scenarioSource, const cucumber::messages::pickle_step& pickleStep); + static StepSource FromAst(const ScenarioSource& scenarioSource, const cucumber::messages::step& step, const cucumber::messages::pickle_step& pickleStep); + }; + + struct SkipStepRunnerV2 + { + SkipStepRunnerV2(ScenarioRunnerV2& scenarioRunner, const cucumber::messages::pickle_step& pickleStep); + virtual ~SkipStepRunnerV2(); + + [[nodiscard]] report::ReportHandler::Result Result() const; + [[nodiscard]] TraceTime::Duration Duration() const; + + private: + ScenarioRunnerV2& scenarioRunner; + StepSource stepSource; + }; + + struct StepRunnerV2 + { + public: + StepRunnerV2(ScenarioRunnerV2& scenarioRunner, const cucumber::messages::pickle_step& pickleStep); + ~StepRunnerV2(); + + [[nodiscard]] const StepSource& Source() const; + + [[nodiscard]] report::ReportHandler& ReportHandler(); + + [[nodiscard]] report::ReportHandler::Result Result() const; + [[nodiscard]] TraceTime::Duration Duration() const; + + void Run(); private: - Context& context; + ScenarioRunnerV2& scenarioRunner; + const cucumber::messages::pickle_step& pickleStep; + + TraceTime traceTime; + + report::ReportHandler::Result result{ report::ReportHandler::Result::undefined }; + + StepSource stepSource; }; } diff --git a/cucumber-cpp/TagExpression.cpp b/cucumber-cpp/TagExpression.cpp index efe9082..d048d60 100644 --- a/cucumber-cpp/TagExpression.cpp +++ b/cucumber-cpp/TagExpression.cpp @@ -1,14 +1,12 @@ #ifndef CUCUMBER_CPP_TAGEXPRESSION_CPP #define CUCUMBER_CPP_TAGEXPRESSION_CPP #include "cucumber-cpp/TagExpression.hpp" -#include "nlohmann/json.hpp" #include #include #include #include #include #include -#include #include namespace cucumber_cpp @@ -21,7 +19,7 @@ namespace cucumber_cpp } } - bool IsTagExprSelected(const std::string& tagExpr, const std::set& tags) + bool IsTagExprSelected(const std::string& tagExpr, const std::set>& tags) { if (tagExpr.empty()) { @@ -37,7 +35,7 @@ namespace cucumber_cpp for (std::smatch matches; std::regex_search(eval, matches, std::regex(R"((@[^ \)]+))"));) { - Replace(eval, matches[1], tags.count(matches[1]) ? "1" : "0"); + Replace(eval, matches[1], tags.contains(matches[1]) ? "1" : "0"); } Replace(eval, "not", "!"); @@ -81,19 +79,6 @@ namespace cucumber_cpp return eval == std::string("1"); } - - bool IsTagExprSelected(const std::string& tagExpr, const nlohmann::json& json) - { - static const auto getName = [](const nlohmann::json& json) - { - return json["name"]; - }; - - std::set tagSet; - std::ranges::copy(json | std::views::transform(getName), std::inserter(tagSet, tagSet.end())); - - return IsTagExprSelected(tagExpr, tagSet); - } } #endif diff --git a/cucumber-cpp/TagExpression.hpp b/cucumber-cpp/TagExpression.hpp index 2c5445a..51ec572 100644 --- a/cucumber-cpp/TagExpression.hpp +++ b/cucumber-cpp/TagExpression.hpp @@ -1,15 +1,13 @@ #ifndef CUCUMBER_CPP_TAGEXPRESSION_HPP #define CUCUMBER_CPP_TAGEXPRESSION_HPP -#include "nlohmann/json_fwd.hpp" #include #include #include namespace cucumber_cpp { - bool IsTagExprSelected(const std::string& tagExpr, const std::set& tags); - bool IsTagExprSelected(const std::string& tagExpr, const nlohmann::json& json); + bool IsTagExprSelected(const std::string& tagExpr, const std::set>& tags); } #endif diff --git a/cucumber-cpp/TagsToSet.hpp b/cucumber-cpp/TagsToSet.hpp new file mode 100644 index 0000000..9b1b38b --- /dev/null +++ b/cucumber-cpp/TagsToSet.hpp @@ -0,0 +1,22 @@ +#ifndef CUCUMBER_CPP_TAGSTOSET_HPP +#define CUCUMBER_CPP_TAGSTOSET_HPP + +#include +#include +#include +#include + +namespace cucumber_cpp +{ + std::set> TagsToSet(const auto& tags) + { + std::set> result; + + for (const auto& entry : tags) + result.insert(entry.name); + + return result; + } +} + +#endif diff --git a/cucumber-cpp/TraceTime.cpp b/cucumber-cpp/TraceTime.cpp index d1d3320..1c1d3bb 100644 --- a/cucumber-cpp/TraceTime.cpp +++ b/cucumber-cpp/TraceTime.cpp @@ -1,19 +1,32 @@ #include "cucumber-cpp/TraceTime.hpp" -#include "nlohmann/json.hpp" namespace cucumber_cpp { - TraceTime::TraceTime(nlohmann::json& json) - : json{ json } - , timeStart{ std::chrono::high_resolution_clock::now() } + + TraceTime::Scoped::Scoped(TraceTime& traceTime) + : traceTime{ traceTime } { + traceTime.Start(); } - TraceTime::~TraceTime() + TraceTime::Scoped::~Scoped() { - const auto timeEnd = std::chrono::high_resolution_clock::now(); + traceTime.Stop(); + } + + void TraceTime::Start() + { + timeStart = std::chrono::high_resolution_clock::now(); + } - json["elapsed"] = std::chrono::duration>(timeEnd - timeStart).count(); + void TraceTime::Stop() + { + timeStop = std::chrono::high_resolution_clock::now(); + } + + TraceTime::Duration TraceTime::Delta() const + { + return timeStop - timeStart; } } diff --git a/cucumber-cpp/TraceTime.hpp b/cucumber-cpp/TraceTime.hpp index 387fc46..c3ee6fd 100644 --- a/cucumber-cpp/TraceTime.hpp +++ b/cucumber-cpp/TraceTime.hpp @@ -1,7 +1,6 @@ #ifndef CUCUMBER_CPP_TRACETIME_HPP #define CUCUMBER_CPP_TRACETIME_HPP -#include "nlohmann/json_fwd.hpp" #include #include @@ -9,12 +8,29 @@ namespace cucumber_cpp { struct TraceTime { - TraceTime(nlohmann::json& json); - ~TraceTime(); + using TimePoint = std::chrono::time_point; + using Duration = TimePoint::duration; + + struct Scoped + { + explicit Scoped(TraceTime& traceTime); + ~Scoped(); + + Scoped(const Scoped&) = delete; + Scoped& operator=(const Scoped&) = delete; + + private: + TraceTime& traceTime; + }; + + void Start(); + void Stop(); + + [[nodiscard]] Duration Delta() const; private: - nlohmann::json& json; - std::chrono::time_point timeStart; + TimePoint timeStart; + TimePoint timeStop; }; } diff --git a/cucumber-cpp/report/CMakeLists.txt b/cucumber-cpp/report/CMakeLists.txt index 6518eb1..cf17c56 100644 --- a/cucumber-cpp/report/CMakeLists.txt +++ b/cucumber-cpp/report/CMakeLists.txt @@ -1,10 +1,9 @@ add_library(cucumber-cpp.report STATIC ${CCR_EXCLUDE_FROM_ALL}) target_sources(cucumber-cpp.report PRIVATE - JsonReport.cpp - JsonReport.hpp JunitReport.cpp JunitReport.hpp + Report.cpp Report.hpp StdOutReport.cpp StdOutReport.hpp @@ -15,7 +14,6 @@ target_include_directories(cucumber-cpp.report PUBLIC ) target_link_libraries(cucumber-cpp.report PUBLIC - nlohmann_json pugixml PRIVATE diff --git a/cucumber-cpp/report/JsonReport.cpp b/cucumber-cpp/report/JsonReport.cpp deleted file mode 100644 index 2400bed..0000000 --- a/cucumber-cpp/report/JsonReport.cpp +++ /dev/null @@ -1,11 +0,0 @@ -#include "cucumber-cpp/report/JsonReport.hpp" -#include "nlohmann/json.hpp" -#include - -namespace cucumber_cpp::report -{ - void JsonReport::GenerateReport(const nlohmann::json& json) - { - std::ofstream("out.json") << json.dump(2) << "\n"; - } -} diff --git a/cucumber-cpp/report/JsonReport.hpp b/cucumber-cpp/report/JsonReport.hpp deleted file mode 100644 index b84861d..0000000 --- a/cucumber-cpp/report/JsonReport.hpp +++ /dev/null @@ -1,14 +0,0 @@ -#ifndef REPORT_JSONREPORT_HPP -#define REPORT_JSONREPORT_HPP - -#include "cucumber-cpp/report/Report.hpp" - -namespace cucumber_cpp::report -{ - struct JsonReport : Report - { - void GenerateReport(const nlohmann::json& json) override; - }; -} - -#endif diff --git a/cucumber-cpp/report/JunitReport.cpp b/cucumber-cpp/report/JunitReport.cpp index 9735797..527f2ad 100644 --- a/cucumber-cpp/report/JunitReport.cpp +++ b/cucumber-cpp/report/JunitReport.cpp @@ -1,9 +1,13 @@ + #include "cucumber-cpp/report/JunitReport.hpp" -#include "cucumber-cpp/Rtrim.hpp" -#include "nlohmann/json.hpp" -#include "pugixml.hpp" +#include "cucumber-cpp/FeatureRunner.hpp" +#include "cucumber-cpp/ScenarioRunner.hpp" +#include "cucumber-cpp/StepRunner.hpp" +#include "cucumber-cpp/report/Report.hpp" +#include +#include #include -#include +#include #include #include @@ -11,170 +15,189 @@ namespace cucumber_cpp::report { namespace { - auto ViewAllCapturedStdStream(const nlohmann::json& json, std::string what) - { - return json["steps"] | std::views::filter([what](const nlohmann::json& step) - { - return step.contains(what); - }) | - std::views::transform([what](const nlohmann::json& step) - { - return step[what].get(); - }); - } + constexpr double precision = 0.0000001; + + const std::map successLut{ + { report::ReportHandler::Result::success, "done" }, + { report::ReportHandler::Result::skipped, "skipped" }, + { report::ReportHandler::Result::failed, "failed" }, + { report::ReportHandler::Result::error, "error" }, + { report::ReportHandler::Result::pending, "pending" }, + { report::ReportHandler::Result::ambiguous, "ambiguous" }, + { report::ReportHandler::Result::undefined, "undefined" }, + }; - void AppendStdStreamOutput(auto& parent, std::string element, auto view) + std::string RoundTo(double value, double roundToPrecision) { - if (!view.empty()) - { + const auto d = std::round(value / roundToPrecision) * roundToPrecision; - auto elem = parent.append_child(element.c_str()); - auto str = std::accumulate(std::next(view.begin()), view.end(), view.front()); - Rtrim(str); - elem.text() = str.c_str(); - } + std::ostringstream out; + out << std::fixed << d; + return out.str(); } + } - std::string AllErrors(const nlohmann::json& json) - { - auto stepsWithErrors = json["steps"] | std::views::filter([](const nlohmann::json& step) - { - return step.contains("errors"); - }); - auto allMessages = stepsWithErrors | std::views::transform([](const nlohmann::json& step) - { - auto messages = step["errors"] | std::views::transform([](const nlohmann::json& json) - { - return json["message"].get(); - }); - return std::accumulate(messages.begin(), messages.end(), std::string{}); - }); - - return std::accumulate(allMessages.begin(), allMessages.end(), std::string{}); - } + JunitReportV2::JunitReportV2() + { + testsuites = doc.append_child("testsuites"); + testsuites.append_attribute("name").set_value("Test run"); + testsuites.append_attribute("time").set_value(0.0); } - void JunitReport::GenerateReport(const nlohmann::json& json) + JunitReportV2::~JunitReportV2() { - struct Statistics - { - std::uint32_t tests = 0; - std::uint32_t failures = 0; - std::uint32_t errors = 0; - std::uint32_t skipped = 0; + testsuites.append_attribute("tests").set_value(totalTests); + testsuites.append_attribute("failures").set_value(totalFailures); + testsuites.append_attribute("errors").set_value(totalErrors); + testsuites.append_attribute("skipped").set_value(totalSkipped); - Statistics& operator+=(Statistics& rhs) - { - tests += rhs.tests; - failures += rhs.failures; - errors += rhs.errors; - skipped += rhs.skipped; + const auto doubleTime = std::chrono::duration>(totalTime).count(); + testsuites.append_attribute("time").set_value(RoundTo(doubleTime, precision).c_str()); - return *this; - } - }; + doc.save_file("out.xml"); + } - pugi::xml_document doc; + void JunitReportV2::FeatureStart(const FeatureSource& featureSource) + { + testsuite = testsuites.append_child("testsuite"); + testsuite.append_attribute("name").set_value(featureSource.name.c_str()); + testsuite.append_attribute("file").set_value(featureSource.path.string().c_str()); + + scenarioTests = 0; + scenarioFailures = 0; + scenarioErrors = 0; + scenarioSkipped = 0; + } - auto testsuites = doc.append_child("testsuites"); + void JunitReportV2::FeatureEnd(const FeatureSource& /*featureSource*/, Result /*result*/, TraceTime::Duration duration) + { + const auto doubleTime = std::chrono::duration>(duration).count(); + testsuite.append_attribute("time").set_value(RoundTo(doubleTime, precision).c_str()); + + totalTests += scenarioTests; + totalFailures += scenarioFailures; + totalErrors += scenarioErrors; + totalSkipped += scenarioSkipped; + + testsuite.append_attribute("tests").set_value(scenarioTests); + testsuite.append_attribute("failures").set_value(scenarioFailures); + testsuite.append_attribute("errors").set_value(scenarioErrors); + testsuite.append_attribute("skipped").set_value(scenarioSkipped); + } - Statistics allStats; + void JunitReportV2::ScenarioStart(const ScenarioSource& scenarioSource) + { + testcase = testsuite.append_child("testcase"); - testsuites.append_attribute("name").set_value("Test run"); - testsuites.append_attribute("time").set_value(json.value("elapsed", 0.0)); + testcase.append_attribute("name").set_value(scenarioSource.name.c_str()); - for (const auto& feature : json["features"]) - { - auto testsuite = testsuites.append_child("testsuite"); - Statistics featureStatistics; + ++scenarioTests; + } - auto featureName = feature["ast"]["feature"]["name"].get(); - testsuite.append_attribute("name").set_value(featureName.c_str()); - testsuite.append_attribute("time").set_value(feature.value("elapsed", 0.0)); + void JunitReportV2::ScenarioEnd(const ScenarioSource& /*scenarioSource*/, Result result, TraceTime::Duration duration) + { + const auto doubleTime = std::chrono::duration>(duration).count(); + testcase.append_attribute("time").set_value(RoundTo(doubleTime, precision).c_str()); - for (const auto& scenario : feature["scenarios"]) + switch (result) + { + case ReportHandler::Result::skipped: + case ReportHandler::Result::pending: + case ReportHandler::Result::ambiguous: + case ReportHandler::Result::undefined: { - if (!scenario.contains("result")) - { - continue; - } - - auto testcase = testsuite.append_child("testcase"); - - auto scenarioName = scenario["name"].get(); - testcase.append_attribute("name").set_value(scenarioName.c_str()); - testcase.append_attribute("time").set_value(scenario.value("elapsed", 0.0)); - - ++featureStatistics.tests; + ++scenarioSkipped; + auto skipped = testcase.append_child("skipped"); - if (const auto result = scenario["result"]; result == "success") + if (result == ReportHandler::Result::skipped) { + skipped.append_attribute("message").set_value("Test is skipped due to previous errors."); } - else if (result == "failed") + else if (result == ReportHandler::Result::undefined) { - ++featureStatistics.failures; - - auto failure = testcase.append_child("failure"); - failure.append_attribute("message").set_value(AllErrors(scenario).c_str()); + skipped.append_attribute("message").set_value("Test is undefined."); } - else if (result == "error") + else if (result == ReportHandler::Result::pending) { - ++featureStatistics.errors; - - auto error = testcase.append_child("error"); - error.append_attribute("message").set_value(AllErrors(scenario).c_str()); + skipped.append_attribute("message").set_value("Test is pending."); } else { - ++featureStatistics.skipped; - - auto skipped = testcase.append_child("skipped"); - - if (result == "skipped") - { - skipped.append_attribute("message").set_value("Test is skipped due to previous errors."); - } - else if (result == "undefined") - { - skipped.append_attribute("message").set_value("Test is undefined."); - } - else if (result == "pending") - { - skipped.append_attribute("message").set_value("Test is pending."); - } - else - { - skipped.append_attribute("message").set_value("Test result unkown."); - } + skipped.append_attribute("message").set_value("Test result unkown."); } + } - auto stepsWithStdout = ViewAllCapturedStdStream(scenario, "stdout"); - AppendStdStreamOutput(testcase, "system-out", stepsWithStdout); + break; - auto stepsWithStderr = ViewAllCapturedStdStream(scenario, "stderr"); - AppendStdStreamOutput(testcase, "system-err", stepsWithStderr); - } + case ReportHandler::Result::failed: + ++scenarioFailures; + break; - if (featureStatistics.tests == 0) - { - testsuites.remove_child(testsuite); - } - else - { - testsuite.append_attribute("tests").set_value(featureStatistics.tests); - testsuite.append_attribute("failures").set_value(featureStatistics.failures); - testsuite.append_attribute("errors").set_value(featureStatistics.errors); - testsuite.append_attribute("skipped").set_value(featureStatistics.skipped); + case ReportHandler::Result::error: + ++scenarioErrors; + break; - allStats += featureStatistics; - } + default: + break; } - testsuites.append_attribute("tests").set_value(allStats.tests); - testsuites.append_attribute("failures").set_value(allStats.failures); - testsuites.append_attribute("errors").set_value(allStats.errors); - testsuites.append_attribute("skipped").set_value(allStats.skipped); + totalTime += duration; + } - doc.save_file("out.xml"); + void JunitReportV2::StepStart(const StepSource& stepSource) + { + /* do nothing */ + } + + void JunitReportV2::StepEnd(const StepSource& stepSource, Result result, TraceTime::Duration duration) + { + /* do nothing */ + } + + void JunitReportV2::Failure(const std::string& error, std::optional path, std::optional line, std::optional column) + { + auto failure = testcase.append_child("failure"); + + failure.append_attribute("message").set_value(error.c_str()); + + std::ostringstream out; + + if (path && line && column) + out + << "\n" + << path.value().string() << ":" << line.value() << ":" << column.value() << ": Failure\n" + << error; + else + out + << "\n" + << error; + + failure.text() = out.str().c_str(); + } + + void JunitReportV2::Error(const std::string& error, std::optional path, std::optional line, std::optional column) + { + auto errorNode = testcase.append_child("error"); + + errorNode.append_attribute("message").set_value(error.c_str()); + + std::ostringstream out; + + if (path && line && column) + out + << "\n" + << path.value().string() << ":" << line.value() << ":" << column.value() << ": Error\n" + << error; + else + out + << "\n" + << error; + + errorNode.text() = out.str().c_str(); + } + + void JunitReportV2::Trace(const std::string& trace) + { + /* do nothing */ } } diff --git a/cucumber-cpp/report/JunitReport.hpp b/cucumber-cpp/report/JunitReport.hpp index d110b5e..a58eeb7 100644 --- a/cucumber-cpp/report/JunitReport.hpp +++ b/cucumber-cpp/report/JunitReport.hpp @@ -2,12 +2,48 @@ #define REPORT_JUNITREPORT_HPP #include "cucumber-cpp/report/Report.hpp" +#include "pugixml.hpp" +#include +#include +#include namespace cucumber_cpp::report { - struct JunitReport : Report + struct JunitReportV2 : ReportHandler { - void GenerateReport(const nlohmann::json& json) override; + JunitReportV2(); + ~JunitReportV2() override; + + void FeatureStart(const FeatureSource& featureSource) override; + void FeatureEnd(const FeatureSource& featureSource, Result result, TraceTime::Duration duration) override; + + void ScenarioStart(const ScenarioSource& scenarioSource) override; + void ScenarioEnd(const ScenarioSource& scenarioSource, Result result, TraceTime::Duration duration) override; + + void StepStart(const StepSource& stepSource) override; + void StepEnd(const StepSource& stepSource, Result result, TraceTime::Duration duration) override; + + void Failure(const std::string& error, std::optional path, std::optional line, std::optional column) override; + void Error(const std::string& error, std::optional path, std::optional line, std::optional column) override; + + void Trace(const std::string& trace) override; + + private: + pugi::xml_document doc; + pugi::xml_node testsuites; + pugi::xml_node testsuite; + pugi::xml_node testcase; + + std::size_t totalTests{ 0 }; + std::size_t totalFailures{ 0 }; + std::size_t totalErrors{ 0 }; + std::size_t totalSkipped{ 0 }; + TraceTime::Duration totalTime{ 0 }; + + std::size_t scenarioTests{ 0 }; + std::size_t scenarioFailures{ 0 }; + std::size_t scenarioErrors{ 0 }; + std::size_t scenarioSkipped{ 0 }; }; } diff --git a/cucumber-cpp/report/Report.cpp b/cucumber-cpp/report/Report.cpp new file mode 100644 index 0000000..7342c02 --- /dev/null +++ b/cucumber-cpp/report/Report.cpp @@ -0,0 +1,81 @@ +#include "cucumber-cpp/report/Report.hpp" +#include "cucumber-cpp/FeatureRunner.hpp" +#include "cucumber-cpp/ScenarioRunner.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace cucumber_cpp::report +{ + namespace + { + template + void ForwardCall(auto& reporters, TFn fn, TArgs&&... args) + { + std::ranges::for_each( + reporters, [&](const auto& reportHandler) + { + (*reportHandler.*fn)(std::forward(args)...); + }); + } + } + + void Reporters::Add(std::unique_ptr&& report) + { + reporters.push_back(std::move(report)); + } + + std::vector>& Reporters::Storage() + { + return reporters; + } + + void ReportForwarder::FeatureStart(const FeatureSource& featureSource) + { + ForwardCall(Storage(), &ReportHandler::FeatureStart, featureSource); + } + + void ReportForwarder::FeatureEnd(const FeatureSource& featureSource, Result result, TraceTime::Duration duration) + { + ForwardCall(Storage(), &ReportHandler::FeatureEnd, featureSource, result, duration); + } + + void ReportForwarder::ScenarioStart(const ScenarioSource& scenarioSource) + { + ForwardCall(Storage(), &ReportHandler::ScenarioStart, scenarioSource); + } + + void ReportForwarder::ScenarioEnd(const ScenarioSource& scenarioSource, Result result, TraceTime::Duration duration) + { + ForwardCall(Storage(), &ReportHandler::ScenarioEnd, scenarioSource, result, duration); + } + + void ReportForwarder::StepStart(const StepSource& stepSource) + { + ForwardCall(Storage(), &ReportHandler::StepStart, stepSource); + } + + void ReportForwarder::StepEnd(const StepSource& stepSource, Result result, TraceTime::Duration duration) + { + ForwardCall(Storage(), &ReportHandler::StepEnd, stepSource, result, duration); + } + + void ReportForwarder::Failure(const std::string& error, std::optional path, std::optional line, std::optional column) + { + ForwardCall(Storage(), &ReportHandler::Failure, error, path, line, column); + } + + void ReportForwarder::Error(const std::string& error, std::optional path, std::optional line, std::optional column) + { + ForwardCall(Storage(), &ReportHandler::Error, error, path, line, column); + } + + void ReportForwarder::Trace(const std::string& trace) + { + ForwardCall(Storage(), &ReportHandler::Trace, trace); + } +} diff --git a/cucumber-cpp/report/Report.hpp b/cucumber-cpp/report/Report.hpp index 3e6056b..f80bbb5 100644 --- a/cucumber-cpp/report/Report.hpp +++ b/cucumber-cpp/report/Report.hpp @@ -1,16 +1,83 @@ -#ifndef D5276DD0_2B76_4493_83B4_DC4A0849A343 -#define D5276DD0_2B76_4493_83B4_DC4A0849A343 +#ifndef REPORT_REPORT_HPP +#define REPORT_REPORT_HPP -#include "nlohmann/json_fwd.hpp" +#include "cucumber-cpp/TraceTime.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace cucumber_cpp +{ + struct FeatureSource; + struct ScenarioSource; + struct StepSource; +} namespace cucumber_cpp::report { - struct Report + struct ReportHandler + { + enum struct Result + { + success, + skipped, + failed, + error, + pending, + ambiguous, + undefined, + }; + + virtual ~ReportHandler() = default; + + virtual void FeatureStart(const FeatureSource& featureSource) = 0; + virtual void FeatureEnd(const FeatureSource& featureSource, Result result, TraceTime::Duration duration) = 0; + + virtual void ScenarioStart(const ScenarioSource& scenarioSource) = 0; + virtual void ScenarioEnd(const ScenarioSource& scenarioSource, Result result, TraceTime::Duration duration) = 0; + + virtual void StepStart(const StepSource& stepSource) = 0; + virtual void StepEnd(const StepSource& stepSource, Result result, TraceTime::Duration duration) = 0; + + virtual void Failure(const std::string& error, std::optional path = {}, std::optional line = {}, std::optional column = {}) = 0; + virtual void Error(const std::string& error, std::optional path = {}, std::optional line = {}, std::optional column = {}) = 0; + + virtual void Trace(const std::string& trace) = 0; + }; + + struct Reporters + { + void Add(std::unique_ptr&& report); + + protected: + std::vector>& Storage(); + + private: + std::vector> reporters; + }; + + struct ReportForwarder + : Reporters + , ReportHandler { - virtual ~Report() = default; + void FeatureStart(const FeatureSource& featureSource) override; + void FeatureEnd(const FeatureSource& featureSource, Result result, TraceTime::Duration duration) override; + + void ScenarioStart(const ScenarioSource& scenarioSource) override; + void ScenarioEnd(const ScenarioSource& scenarioSource, Result result, TraceTime::Duration duration) override; + + void StepStart(const StepSource& stepSource) override; + void StepEnd(const StepSource& stepSource, Result result, TraceTime::Duration duration) override; + + void Failure(const std::string& error, std::optional path, std::optional line, std::optional column) override; + void Error(const std::string& error, std::optional path, std::optional line, std::optional column) override; - virtual void GenerateReport(const nlohmann::json& json) = 0; + void Trace(const std::string& trace) override; }; } -#endif /* D5276DD0_2B76_4493_83B4_DC4A0849A343 */ +#endif diff --git a/cucumber-cpp/report/StdOutReport.cpp b/cucumber-cpp/report/StdOutReport.cpp index 3820105..c541098 100644 --- a/cucumber-cpp/report/StdOutReport.cpp +++ b/cucumber-cpp/report/StdOutReport.cpp @@ -1,115 +1,138 @@ #include "cucumber-cpp/report/StdOutReport.hpp" -#include "nlohmann/json.hpp" +#include "cucumber-cpp/FeatureRunner.hpp" +#include "cucumber-cpp/ScenarioRunner.hpp" +#include "cucumber-cpp/StepRunner.hpp" +#include "cucumber-cpp/TraceTime.hpp" +#include "cucumber-cpp/report/Report.hpp" #include +#include #include #include +#include +#include namespace cucumber_cpp::report { namespace { - void PrintLineWithResult(const std::string& text, const std::string& result, std::uint32_t indent, std::uint32_t spacing) + std::string ConstructLineWithResult(const std::string& text, const std::string& result, std::uint32_t indent, std::uint32_t spacing) { + std::ostringstream out; + const auto size = text.size() + indent; auto dots = std::max(static_cast(spacing - size), 5); - std::cout << "\n"; + out << "\n"; for (; indent != 0; --indent) - std::cout << ' '; + out << ' '; - std::cout << text; + out << text; for (; dots != 0; --dots) - std::cout << '.'; + out << '.'; - std::cout << result; + out << result; + + return out.str(); } - void PrintTable(const nlohmann::json& step) + void PrintLineWithResult(const std::string& text, const std::string& result, std::uint32_t indent, std::uint32_t spacing) { - if (step.contains("argument") && !step["argument"]["dataTable"].empty()) - { - const auto& table = step["argument"]["dataTable"]; - - auto colSize = [&table](int colIndex) - { - return std::ranges::max(table["rows"] | std::views::transform( - [colIndex](const nlohmann::json& row) - { - return row["cells"][colIndex]["value"].get().size(); - })); - }; - - for (auto row : table["rows"]) - { - std::cout << "\n |"; - auto colIndex{ 0 }; - for (auto col : row["cells"]) - { - const auto value = col["value"].get(); - std::cout << " " << value << std::string(colSize(colIndex) - value.size(), ' '); - std::cout << " |"; - ++colIndex; - } - } - } + std::cout << ConstructLineWithResult(text, result, indent, spacing); } - const std::map stepTypeLut{ - { "Context", "Given" }, - { "Action", "When" }, - { "Outcome", "Then" }, + const std::map successLut{ + { report::ReportHandler::Result::success, "done" }, + { report::ReportHandler::Result::skipped, "skipped" }, + { report::ReportHandler::Result::failed, "failed" }, + { report::ReportHandler::Result::error, "error" }, + { report::ReportHandler::Result::pending, "pending" }, + { report::ReportHandler::Result::ambiguous, "ambiguous" }, + { report::ReportHandler::Result::undefined, "undefined" }, }; - std::string CreateStepText(const nlohmann::json& stepJson) + std::string ScaledDuration(TraceTime::Duration duration) { - return std::string{ stepTypeLut.at(stepJson["type"].get()) } + " " + stepJson["text"].get(); + std::ostringstream out; + + if (duration < std::chrono::microseconds{ 1 }) + out << std::chrono::duration(duration); + else if (duration < std::chrono::milliseconds{ 1 }) + out << std::chrono::duration(duration); + else if (duration < std::chrono::seconds{ 1 }) + out << std::chrono::duration(duration); + else if (duration < std::chrono::minutes{ 1 }) + out << std::chrono::duration(duration); + else if (duration < std::chrono::hours{ 1 }) + out << std::chrono::duration(duration); + else + out << std::chrono::duration(duration); + + return out.str(); } + } - double RoundTo(double value, double precision) - { - return std::round(value / precision) * precision; - } + void StdOutReportV2::FeatureStart(const FeatureSource& featureSource) + { + /* do nothing */ } - void StdOutReport::GenerateReport(const nlohmann::json& json) + void StdOutReportV2::FeatureEnd(const FeatureSource& featureSource, Result result, TraceTime::Duration duration) { - for (const auto& feature : json["features"]) - { - for (const auto& scenario : feature["scenarios"]) - { - if (!scenario.contains("result")) - { - continue; - } - - std::cout << "\n"; - PrintLineWithResult(scenario["name"], scenario["result"], 0, 60); - std::cout << " " << RoundTo(scenario.value("elapsed", 0.0), 0.001) << "s"; - - for (const auto& step : scenario["steps"]) - { - if (!step.contains("result")) - { - continue; - } - - PrintLineWithResult(CreateStepText(step), step["result"], 4, 60); - std::cout << " " << RoundTo(step.value("elapsed", 0.0), 0.001) << "s"; - PrintTable(step); - - if (step.contains("stdout")) - { - std::cout << "\nstdout:\n" - << step["stdout"].get(); - } - - if (step.contains("stderr")) - { - std::cout << "\nstderr:\n" - << step["stderr"].get(); - } - } - } - } + /* do nothing */ + } + + void StdOutReportV2::ScenarioStart(const ScenarioSource& scenarioSource) + { + std::cout << "\n" + << scenarioSource.name; + } + + void StdOutReportV2::ScenarioEnd(const ScenarioSource& scenarioSource, Result result, TraceTime::Duration duration) + { + std::cout << "\n"; + } + + void StdOutReportV2::StepStart(const StepSource& stepSource) + { + std::cout << "\n " << stepSource.type + " " + stepSource.name; + } + + void StdOutReportV2::StepEnd(const StepSource& stepSource, Result result, TraceTime::Duration duration) + { + if (result == decltype(result)::success || result == decltype(result)::skipped) + std::cout << "\n -> " << successLut.at(result) << " (" << ScaledDuration(duration) << ")"; + else + std::cout << "\n -> " << stepSource.scenarioSource.featureSource.path << ":" << stepSource.line << ":" << stepSource.column << ": " << successLut.at(result) << " (" << ScaledDuration(duration) << ")"; + } + + void StdOutReportV2::Failure(const std::string& error, std::optional path, std::optional line, std::optional column) + { + if (path && line && column) + std::cout + << "\n" + << path.value().string() << ":" << line.value() << ":" << column.value() << ": Failure\n" + << error; + else + std::cout + << "\n" + << error; + } + + void StdOutReportV2::Error(const std::string& error, std::optional path, std::optional line, std::optional column) + { + if (path && line && column) + std::cout + << "\n" + << path.value().string() << ":" << line.value() << ":" << column.value() << ": Error\n" + << error; + else + std::cout + << "\n" + << error; + } + + void StdOutReportV2::Trace(const std::string& trace) + { + /* do nothing */ } } diff --git a/cucumber-cpp/report/StdOutReport.hpp b/cucumber-cpp/report/StdOutReport.hpp index c68a36b..452cfd1 100644 --- a/cucumber-cpp/report/StdOutReport.hpp +++ b/cucumber-cpp/report/StdOutReport.hpp @@ -2,12 +2,26 @@ #define REPORT_STDOUTREPORT_HPP #include "cucumber-cpp/report/Report.hpp" +#include +#include namespace cucumber_cpp::report { - struct StdOutReport : Report + struct StdOutReportV2 : ReportHandler { - void GenerateReport(const nlohmann::json& json) override; + void FeatureStart(const FeatureSource& featureSource) override; + void FeatureEnd(const FeatureSource& featureSource, Result result, TraceTime::Duration duration) override; + + void ScenarioStart(const ScenarioSource& scenarioSource) override; + void ScenarioEnd(const ScenarioSource& scenarioSource, Result result, TraceTime::Duration duration) override; + + void StepStart(const StepSource& stepSource) override; + void StepEnd(const StepSource& stepSource, Result result, TraceTime::Duration duration) override; + + void Failure(const std::string& error, std::optional path, std::optional line, std::optional column) override; + void Error(const std::string& error, std::optional path, std::optional line, std::optional column) override; + + void Trace(const std::string& trace) override; }; } diff --git a/cucumber-cpp/test/TestSteps.cpp b/cucumber-cpp/test/TestSteps.cpp index 6507665..b423344 100644 --- a/cucumber-cpp/test/TestSteps.cpp +++ b/cucumber-cpp/test/TestSteps.cpp @@ -10,22 +10,40 @@ namespace cucumber_cpp { GIVEN("This is a GIVEN step") - {} + { + /* do nothing */ + } WHEN("This is a WHEN step") - {} + { + /* do nothing */ + } THEN("This is a THEN step") - {} + { + /* do nothing */ + } STEP("This is a STEP step") - {} + { + /* do nothing */ + } STEP("This is a step with a ([0-9]+)s delay", (std::uint32_t delay)) { context.InsertAt("std::uint32_t", delay); } + THEN("an ambiguous step") + { + /* do nothing */ + } + + STEP("an ambiguous step") + { + /* do nothing */ + } + STEP("Step with cucumber expression syntax {float} {string} {int}", (float fl, std::string str, std::uint32_t nr)) { context.InsertAt("float", fl); @@ -44,71 +62,72 @@ namespace cucumber_cpp TEST_F(TestSteps, RegisterThroughPreregistration) { - EXPECT_THAT(stepRegistry.Size(), testing::Eq(6)); + EXPECT_THAT(stepRegistry.Size(), testing::Eq(8)); EXPECT_THAT(stepRegistry.Size(StepType::given), testing::Eq(1)); EXPECT_THAT(stepRegistry.Size(StepType::when), testing::Eq(1)); - EXPECT_THAT(stepRegistry.Size(StepType::then), testing::Eq(1)); - EXPECT_THAT(stepRegistry.Size(StepType::any), testing::Eq(3)); + EXPECT_THAT(stepRegistry.Size(StepType::then), testing::Eq(2)); + EXPECT_THAT(stepRegistry.Size(StepType::any), testing::Eq(4)); } TEST_F(TestSteps, GetGivenStep) { const auto matches = stepRegistry.Query(StepType::given, "This is a GIVEN step"); - ASSERT_THAT(matches.size(), testing::Eq(1)); - EXPECT_THAT(matches.front().stepRegex.String(), testing::StrEq("This is a GIVEN step")); + EXPECT_THAT(matches.stepRegex.String(), testing::StrEq("This is a GIVEN step")); } TEST_F(TestSteps, GetWhenStep) { const auto matches = stepRegistry.Query(StepType::when, "This is a WHEN step"); - ASSERT_THAT(matches.size(), testing::Eq(1)); - EXPECT_THAT(matches.front().stepRegex.String(), testing::StrEq("This is a WHEN step")); + EXPECT_THAT(matches.stepRegex.String(), testing::StrEq("This is a WHEN step")); } TEST_F(TestSteps, GetThenStep) { const auto matches = stepRegistry.Query(StepType::then, "This is a THEN step"); - ASSERT_THAT(matches.size(), testing::Eq(1)); - EXPECT_THAT(matches.front().stepRegex.String(), testing::StrEq("This is a THEN step")); + EXPECT_THAT(matches.stepRegex.String(), testing::StrEq("This is a THEN step")); } TEST_F(TestSteps, GetAnyStep) { const auto matches = stepRegistry.Query(StepType::given, "This is a STEP step"); - ASSERT_THAT(matches.size(), testing::Eq(1)); - EXPECT_THAT(matches.front().stepRegex.String(), testing::StrEq("This is a STEP step")); + EXPECT_THAT(matches.stepRegex.String(), testing::StrEq("This is a STEP step")); } TEST_F(TestSteps, GetStepWithMatches) { const auto matches = stepRegistry.Query(StepType::when, "This is a step with a 10s delay"); - ASSERT_THAT(matches.size(), testing::Eq(1)); - EXPECT_THAT(matches.front().stepRegex.String(), testing::StrEq("This is a step with a ([0-9]+)s delay")); + EXPECT_THAT(matches.stepRegex.String(), testing::StrEq("This is a step with a ([0-9]+)s delay")); - EXPECT_THAT(matches.front().regexMatch->Matches().size(), testing::Eq(1)); - EXPECT_THAT(matches.front().regexMatch->Matches()[0], testing::StrEq("10")); + EXPECT_THAT(matches.regexMatch->Matches().size(), testing::Eq(1)); + EXPECT_THAT(matches.regexMatch->Matches()[0], testing::StrEq("10")); } TEST_F(TestSteps, GetInvalidStep) { - EXPECT_THROW(stepRegistry.Query(StepType::when, "This step does not exist"), std::out_of_range); + EXPECT_THROW((void)stepRegistry.Query(StepType::when, "This step does not exist"), StepRegistryBase::StepNotFoundError); + } + + TEST_F(TestSteps, GetAmbiguousStep) + { + EXPECT_NO_THROW((void)stepRegistry.Query(StepType::given, "an ambiguous step")); + EXPECT_NO_THROW((void)stepRegistry.Query(StepType::when, "an ambiguous step")); + + EXPECT_THROW((void)stepRegistry.Query(StepType::then, "an ambiguous step"), StepRegistryBase::AmbiguousStepError); } TEST_F(TestSteps, InvokeTestWithCucumberExpressions) { const auto matches = stepRegistry.Query(StepType::when, R"(Step with cucumber expression syntax 1.5 """abcdef"" 10)"); - ASSERT_THAT(matches.size(), testing::Eq(1)); - auto contextStorage{ std::make_shared() }; Context context{ contextStorage }; - matches.front().factory(context, {})->Execute(matches.front().regexMatch->Matches()); + matches.factory(context, {})->Execute(matches.regexMatch->Matches()); EXPECT_THAT(context.Contains("float"), testing::IsTrue()); EXPECT_THAT(context.Contains("std::string"), testing::IsTrue()); diff --git a/cucumber-cpp/test/TestTagExpression.cpp b/cucumber-cpp/test/TestTagExpression.cpp index 89fbdc7..549f8fe 100644 --- a/cucumber-cpp/test/TestTagExpression.cpp +++ b/cucumber-cpp/test/TestTagExpression.cpp @@ -1,5 +1,4 @@ #include "cucumber-cpp/TagExpression.hpp" -#include "nlohmann/json.hpp" #include "gmock/gmock.h" #include @@ -7,11 +6,8 @@ namespace cucumber_cpp { struct TestTagExpression : testing::Test { - nlohmann::json inputTags = { { { "name", "@abc" } }, - { { "name", "@def" } }, - { { "name", "@efg" } } }; - - nlohmann::json noTags = nlohmann::json::value_t::array; + std::set> inputTags = { "@abc", "@def", "@efg" }; + std::set> noTags = {}; }; TEST_F(TestTagExpression, EmptyTagExpression)