diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index 9287dcc6..2022c33d 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -44,7 +44,7 @@ jobs: - name: Run acceptance tests run: | - bats --formatter junit cucumber_cpp/acceptance_test/coverage.bats | tee test-report.xml + bats --formatter junit cucumber_cpp/acceptance_test/test.bats | tee test-report.xml - name: Collect coverage run: | diff --git a/.gitignore b/.gitignore index 7cf593e7..398c432a 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,10 @@ .xwin-cache .vs/ megalinter-reports/ + +# compatibility generated files +actual.ndjson +expected.ndjson + +# bats task generated files +test-report.xml diff --git a/.ls-lint.yml b/.ls-lint.yml index a40b0114..65d82f84 100644 --- a/.ls-lint.yml +++ b/.ls-lint.yml @@ -12,3 +12,4 @@ ignore: - external - megalinter-reports - testdata + - compatibility diff --git a/.vscode/launch.json b/.vscode/launch.json index 32bb5ea0..a043d84a 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -15,7 +15,13 @@ "@result:UNDEFINED", "@result:FAILED", "@result:OK", - "@fail_feature" + "@fail_feature", + "@ex:2", + "@substep", + "@table_argument", + "@thishasarule", + "@nested_steps", + "@rules" ] }, { @@ -24,7 +30,10 @@ "type": "pickString", "options": [ "cucumber_cpp/example/features", - "cucumber_cpp/acceptance_test/features" + "cucumber_cpp/example/features/rule.feature", + "cucumber_cpp/example/features/substep.feature", + "cucumber_cpp/acceptance_test/features", + "cucumber_cpp/devkit/empty/features" ] } ], @@ -35,17 +44,77 @@ "request": "launch", "program": "${command:cmake.launchTargetPath}", "args": [ - "run", - "--feature", - "${input:features}", - "--report", - "console", - "junit-xml", - // "--com", - // "COMx", - // "--nordic", - "--tag", - "${input:tag}" + "--format", + "pretty", + "summary", + "--retry", + "1", + "--tags", + "${input:tag}", + "--", + "${input:features}" + ], + "stopAtEntry": false, + "cwd": "${workspaceFolder}", + "environment": [ + { + "name": "ASAN_OPTIONS", + "value": "detect_leaks=0" + } + ], + "externalConsole": false, + "MIMode": "gdb", + "setupCommands": [ + { + "description": "Enable pretty-printing for gdb", + "text": "-enable-pretty-printing", + "ignoreFailures": true + } + ] + }, + { + "name": "(gdb) Launch dry run", + "type": "cppdbg", + "request": "launch", + "program": "${command:cmake.launchTargetPath}", + "args": [ + "--format", + "usage", + "--dry-run", + "--", + "${input:features}" + ], + "stopAtEntry": false, + "cwd": "${workspaceFolder}", + "environment": [ + { + "name": "ASAN_OPTIONS", + "value": "detect_leaks=0" + } + ], + "externalConsole": false, + "MIMode": "gdb", + "setupCommands": [ + { + "description": "Enable pretty-printing for gdb", + "text": "-enable-pretty-printing", + "ignoreFailures": true + } + ] + }, + { + "name": "(gdb) Launch - no tags", + "type": "cppdbg", + "request": "launch", + "program": "${command:cmake.launchTargetPath}", + "args": [ + "--com", + "COMx", + "--nordic", + "--format", + "summary", + "--", + "${input:features}" ], "stopAtEntry": false, "cwd": "${workspaceFolder}", diff --git a/.vscode/settings.json b/.vscode/settings.json index 6b729d44..1d82165e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,7 @@ { "cmake.useCMakePresets": "always", "cucumberautocomplete.steps": [ + "compatibility/*/*.cpp", "cucumber_cpp/example/steps/*.cpp", "cucumber_cpp/acceptance_test/steps/*.cpp" ], diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 9008a9df..afed183e 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -6,7 +6,7 @@ { "label": "bats", "type": "shell", - "command": "bats --formatter junit cucumber_cpp/acceptance_test/test.bats | tee test-report.xml", + "command": "bats --formatter junit cucumber_cpp/acceptance_test/test.bats", "problemMatcher": [] } ] diff --git a/CMakeLists.txt b/CMakeLists.txt index c30a33e2..91519a98 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,20 +11,26 @@ endif() if (CCR_STANDALONE) set(CCR_DEFAULTOPT On) -else() - set(CCR_DEFAULTOPT Off) -endif() - -if (CCR_STANDALONE) set(CCR_EXCLUDE_FROM_ALL "") + set(CMAKE_COMPILE_WARNING_AS_ERROR On) else() + set(CCR_DEFAULTOPT Off) set(CCR_EXCLUDE_FROM_ALL "EXCLUDE_FROM_ALL") endif() -option(CCR_FETCH_DEPS "Fetch dependencies via FetchContent." On ) +option(CCR_FETCH_DEPS "Fetch dependencies via FetchContent." ${CCR_DEFAULTOPT} ) option(CCR_BUILD_TESTS "Enable build of the tests" ${CCR_DEFAULTOPT}) option(CCR_ENABLE_COVERAGE "Enable compiler flags for code coverage measurements" Off) +set(CMAKE_POSITION_INDEPENDENT_CODE ON) + +if (CCR_STANDALONE AND NOT CCR_ENABLE_COVERAGE) + if (NOT (CMAKE_CXX_COMPILER_ID MATCHES "MSVC" OR CMAKE_CXX_SIMULATE_ID MATCHES "MSVC")) + add_compile_options(-fsanitize=address -fsanitize=undefined) + add_link_options(-fsanitize=address -fsanitize=undefined) + endif() +endif() + if (CCR_BUILD_TESTS) ccr_enable_testing() endif() @@ -53,7 +59,6 @@ set_directory_properties(PROPERTY USE_FOLDERS ON) include(FetchContent) include(GNUInstallDirs) include(CTest) -include(GoogleTest) if (CCR_FETCH_DEPS) add_subdirectory(external) @@ -64,7 +69,15 @@ else() find_package(pugixml REQUIRED) find_package(cucumber_messages REQUIRED) find_package(cucumber_gherkin REQUIRED) - find_package(yaml-cpp REQUIRED) + find_package(fmt REQUIRED) + + if (CCR_BUILD_TESTS) + find_package(yaml-cpp REQUIRED) + endif() endif() add_subdirectory(cucumber_cpp) + +if (CCR_STANDALONE) + add_subdirectory(compatibility) +endif() diff --git a/CMakePresets.json b/CMakePresets.json index 2eb2c925..2fb21843 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -13,7 +13,9 @@ "installDir": "${sourceDir}/.install/${presetName}", "cacheVariables": { "CMAKE_EXPORT_COMPILE_COMMANDS": "On", - "CMAKE_CONFIGURATION_TYPES": "Debug;RelWithDebInfo;Release" + "CMAKE_CONFIGURATION_TYPES": "Debug;RelWithDebInfo;Release", + "CMAKE_C_COMPILER_LAUNCHER": "ccache", + "CMAKE_CXX_COMPILER_LAUNCHER": "ccache" } }, { diff --git a/README.md b/README.md index 5a026dd7..298a2173 100644 --- a/README.md +++ b/README.md @@ -18,11 +18,148 @@ amp-cucumber-cpp-runner is known to build using: amp-cucumber-cpp-runner is not supposed to be used standalone. It is possible to add amp-cucumber-cpp-runner as a submodule to your own project or to use cmake's FetchContent to add amp-cucumber-cpp-runner as a cmake build dependency. En example project is provided which shows most features of amp-cucumber-cpp-runner and how to configure a cmake project to use amp-cucumber-cpp-runner. The simplest solution is to simply add a dependency on `cucumber-cpp-runner` like so: -`cmake -target_link_libraries(cucumber_cpp.example PRIVATE + +```cmake +FetchContent_Declare(cucumber_cpp + GIT_REPOSITORY https://github.com/philips-software/amp-cucumber-cpp-runner.git + GIT_TAG main +) + +FetchContent_MakeAvailable(cucumber_cpp) + +add_executable(my_test_runner) +target_sources(my_test_runner PRIVATE + steps/mysteps.cpp + hooks/myhooks.cpp +) + +target_link_libraries(my_test_runner PRIVATE cucumber_cpp.runner ) -` +``` + +## Writing step definitions + +Steps are written using the `GIVEN`, `WHEN`, `THEN` or `STEP` macro's. The syntax for step definitions is: + +```cpp +// using cucumber expressions + +STEP("cucumber expression without captures"){ + // body +} + +STEP("cucumber expression with an {int}", (std::int32_t arg)){ + // body +} + +// or as a regular expression: + +STEP(R"(^a regular\?? expression without captures$)"){ + +} + +STEP(R"(^a (regular|unregular) expression with ([1-2]) capture groups$)", (const std::string& type, const std::string& nrOfGroups)){ + +} +``` + +### Builtin Parameter Types + +The argument list has to match with the cucumber expression type: + +| Cucumber Expression | Parsed as | Notes | +|---------------------|----------------|----------------------------------| +| `{int}` | `std::int32_t` | | +| `{float}` | `float` | | +| `{word}` | `std::string` | words without whitespace | +| `{string}` | `std::string` | "banana split" or 'banana split' | +| `{}` | `std::string` | | +| `{bigdecimal}` | `double` | non-standard | +| `{biginteger}` | `std::int64_t` | non-standard | +| `{byte}` | `std::int8_t` | | +| `{short}` | `std::int16_t` | | +| `{long}` | `std::int64_t` | | +| `{double}` | `double` | | + + +See the [Cucumber Expressions documentation](https://github.com/cucumber/cucumber-expressions#readme) for more details on supported types. amp-cucumber-cpp-runner has full support for cucumber expressions. + +> ℹ️ except for biginteger, but it's optional + +### Custom Parameter Types + +There is also support for custom parameter types. They are written and used like: + +```cpp +struct Flight +{ + std::string from; + std::string to; +}; + +PARAMETER(Flight, "flight", "([A-Z]{3})-([A-Z]{3})", true) +{ + return { group.children[0].value.value(), group.children[1].value.value() }; +} + +STEP(R"({flight} has been delayed)", (const Flight& flight)) +{ + EXPECT_THAT(flight.from, testing::StrEq("LHR")); + EXPECT_THAT(flight.to, testing::StrEq("CDG")); +} +``` + +The `PARAMETER` macro API is as follows: +``` +PARAMETER(, , , ) +``` +> ℹ️ \ is not supported yet + +The `PARAMETER` function body receives an [`const cucumber::messages::group& group`](https://github.com/cucumber/messages/blob/37af3aa8c54ccaf4e61c7cd168d05e39a19c1959/cpp/include/messages/cucumber/messages/group.hpp#L21) as argument. + +### Custom step fixtures + +amp-cucumber-cpp-runner has support for custom base classes for step definitions using the `GIVEN_F`, `WHEN_F`, `THEN_F` or `STEP_F` macros. These give additional flexibility for re-using common patterns for steps. For example: + +```cpp +struct LoadMeOnConstruction +{ + void Tadaa() const + {} +}; + +struct CustomFixture : cucumber_cpp::library::engine::Step +{ + using Step::Step; + + const LoadMeOnConstruction& alwaysAvailable{ context.Get() }; +}; + +GIVEN_F(CustomFixture, R"(a custom fixture background step)") +{ + alwaysAvailable.Tadaa(); +} +``` + +### Assertion Framework + +amp-cucumber-cpp-runner uses GoogleTest and GoogleMock as the basis for the assertion framework. For example: +```cpp +STEP(R"(the value should be {string})", (const std::string& expected_value)) +{ + const auto& actual = context.Get("cell"); + + ASSERT_THAT(actual, testing::StrEq(expected_value)); +} +``` + +If asserting (or expecting) is not an option then it is also safe to throw exceptions. + +## Writing hook definitions + +\<\> + ## How to test the software diff --git a/compatibility/CMakeLists.txt b/compatibility/CMakeLists.txt new file mode 100644 index 00000000..b0258437 --- /dev/null +++ b/compatibility/CMakeLists.txt @@ -0,0 +1,47 @@ +function(add_compatibility_kit name) + set(libname compatibility.${name}.test) + add_executable(${libname}) + + if(CCR_BUILD_TESTS) + add_test(NAME ${libname} COMMAND ${libname}) + endif() + + target_link_libraries(${libname} PRIVATE + cucumber_cpp + gtest_main + ) + + target_include_directories(${libname} PUBLIC + $ + ) + + target_sources(${libname} PRIVATE + compatibility.cpp + ) + + + if (EXISTS ${CMAKE_CURRENT_LIST_DIR}/${name}/${name}.cpp) + target_sources(${libname} PRIVATE + ${name}/${name}.cpp + ) + else() + set(SKIP_TEST On) + endif() + + string(REPLACE "-" "_" KITNAME ${name}) + + target_compile_definitions(${libname} PRIVATE + KIT_NAME=${KITNAME} + KIT_STRING="${name}" + KIT_FOLDER="${CMAKE_CURRENT_SOURCE_DIR}/${name}" + KIT_NDJSON_FILE="${CMAKE_CURRENT_SOURCE_DIR}/${name}/${name}.ndjson" + $<$:SKIP_TEST> + ) +endfunction() + +file(GLOB kits RELATIVE ${CMAKE_CURRENT_LIST_DIR} ${CMAKE_CURRENT_LIST_DIR}/*) +foreach(kit ${kits}) + if (IS_DIRECTORY ${CMAKE_CURRENT_LIST_DIR}/${kit}) + add_compatibility_kit(${kit}) + endif() +endforeach() diff --git a/compatibility/ambiguous/ambiguous.cpp b/compatibility/ambiguous/ambiguous.cpp new file mode 100644 index 00000000..f21b5b41 --- /dev/null +++ b/compatibility/ambiguous/ambiguous.cpp @@ -0,0 +1,12 @@ +#include "cucumber_cpp/CucumberCpp.hpp" +#include + +STEP(R"(^a (.*?) with (.*?)$)", (const std::string& arg1, const std::string& arg2)) +{ + // no-op +} + +STEP(R"(^a step with (.*?)$)", (const std::string& arg1)) +{ + // no-op +} diff --git a/compatibility/ambiguous/ambiguous.feature b/compatibility/ambiguous/ambiguous.feature new file mode 100644 index 00000000..f021e17c --- /dev/null +++ b/compatibility/ambiguous/ambiguous.feature @@ -0,0 +1,6 @@ +Feature: Ambiguous steps + Multiple step definitions that match a pickle step result in an AMBIGUOUS status, since Cucumnber cannot determine + which one to execute. + + Scenario: Multiple step definitions for a step + Given a step with multiple definitions diff --git a/compatibility/ambiguous/ambiguous.ndjson b/compatibility/ambiguous/ambiguous.ndjson new file mode 100644 index 00000000..5cbe118b --- /dev/null +++ b/compatibility/ambiguous/ambiguous.ndjson @@ -0,0 +1,13 @@ +{"meta":{"protocolVersion":"31.0.0","implementation":{"name":"fake-cucumber","version":"123.45.6"},"cpu":{"name":"arm64"},"os":{"name":"darwin","version":"24.5.0"},"runtime":{"name":"Node.js","version":"24.4.1"},"ci":{"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429","buildNumber":"154666429","git":{"revision":"99684bcacf01d95875834d87903dcb072306c9ad","remote":"https://github.com/cucumber-ltd/shouty.rb.git","branch":"main"}}}} +{"source":{"data":"Feature: Ambiguous steps\n Multiple step definitions that match a pickle step result in an AMBIGUOUS status, since Cucumnber cannot determine\n which one to execute.\n\n Scenario: Multiple step definitions for a step\n Given a step with multiple definitions\n","uri":"samples/ambiguous/ambiguous.feature","mediaType":"text/x.cucumber.gherkin+plain"}} +{"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"Ambiguous steps","description":" Multiple step definitions that match a pickle step result in an AMBIGUOUS status, since Cucumnber cannot determine\n which one to execute.","children":[{"scenario":{"id":"1","tags":[],"location":{"line":5,"column":3},"keyword":"Scenario","name":"Multiple step definitions for a step","description":"","steps":[{"id":"0","location":{"line":6,"column":5},"keyword":"Given ","keywordType":"Context","text":"a step with multiple definitions"}],"examples":[]}}]},"comments":[],"uri":"samples/ambiguous/ambiguous.feature"}} +{"pickle":{"id":"3","uri":"samples/ambiguous/ambiguous.feature","location":{"line":5,"column":3},"astNodeIds":["1"],"tags":[],"name":"Multiple step definitions for a step","language":"en","steps":[{"id":"2","text":"a step with multiple definitions","type":"Context","astNodeIds":["0"]}]}} +{"stepDefinition":{"id":"4","pattern":{"type":"REGULAR_EXPRESSION","source":"^a (.*?) with (.*?)$"},"sourceReference":{"uri":"samples/ambiguous/ambiguous.ts","location":{"line":3}}}} +{"stepDefinition":{"id":"5","pattern":{"type":"REGULAR_EXPRESSION","source":"^a step with (.*?)$"},"sourceReference":{"uri":"samples/ambiguous/ambiguous.ts","location":{"line":7}}}} +{"testRunStarted":{"id":"6","timestamp":{"seconds":0,"nanos":0}}} +{"testCase":{"id":"7","pickleId":"3","testSteps":[{"id":"8","pickleStepId":"2","stepDefinitionIds":["4","5"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":2,"value":"step","children":[]}},{"group":{"start":12,"value":"multiple definitions","children":[]}}]},{"stepMatchArguments":[{"group":{"start":12,"value":"multiple definitions","children":[]}}]}]}],"testRunStartedId":"6"}} +{"testCaseStarted":{"id":"9","testCaseId":"7","timestamp":{"seconds":0,"nanos":1000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"9","testStepId":"8","timestamp":{"seconds":0,"nanos":2000000}}} +{"testStepFinished":{"testCaseStartedId":"9","testStepId":"8","testStepResult":{"status":"AMBIGUOUS","duration":{"seconds":0,"nanos":0}},"timestamp":{"seconds":0,"nanos":3000000}}} +{"testCaseFinished":{"testCaseStartedId":"9","timestamp":{"seconds":0,"nanos":4000000},"willBeRetried":false}} +{"testRunFinished":{"testRunStartedId":"6","timestamp":{"seconds":0,"nanos":5000000},"success":false}} diff --git a/compatibility/ambiguous/ambiguous.ts b/compatibility/ambiguous/ambiguous.ts new file mode 100644 index 00000000..d80f1727 --- /dev/null +++ b/compatibility/ambiguous/ambiguous.ts @@ -0,0 +1,9 @@ +import { Given } from '@cucumber/fake-cucumber' + +Given(/^a (.*?) with (.*?)$/, function () { + // first one +}) + +Given(/^a step with (.*)$/, function () { + // second one +}) diff --git a/compatibility/attachments/attachments.cpp b/compatibility/attachments/attachments.cpp new file mode 100644 index 00000000..76eaa31b --- /dev/null +++ b/compatibility/attachments/attachments.cpp @@ -0,0 +1,54 @@ +#include "cucumber_cpp/CucumberCpp.hpp" +#include "cucumber_cpp/library/engine/ExecutionContext.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ + const std::filesystem::path currentCompileDir = std::filesystem::path{ std::source_location::current().file_name() }.parent_path(); +} + +WHEN(R"(the string {string} is attached as {string})", (const std::string& text, const std::string& mediaType)) +{ + Attach(text, mediaType); +} + +WHEN(R"(the string {string} is logged)", (const std::string& text)) +{ + Log(text); +} + +WHEN(R"(text with ANSI escapes is logged)") +{ + Log("This displays a \x1b[31mr\x1b[0m\x1b[91ma\x1b[0m\x1b[33mi\x1b[0m\x1b[32mn\x1b[0m\x1b[34mb\x1b[0m\x1b[95mo\x1b[0m\x1b[35mw\x1b[0m"); +} + +WHEN(R"(the following string is attached as {string}:)", (const std::string& mediaType)) +{ + Attach(docString->content, mediaType); +} + +WHEN(R"(an array with {int} bytes is attached as {string})", (std::int32_t size, const std::string& mediaType)) +{ + std::string data(size, '\0'); + std::iota(data.begin(), data.end(), 0); + std::stringstream stream{ data }; + Attach(stream, mediaType); +} + +WHEN(R"(a PDF document is attached and renamed)") +{ + std::ifstream pdfFile{ currentCompileDir / "document.pdf", std::ios::binary }; + Attach(pdfFile, cucumber_cpp::library::engine::AttachOptions{ "application/pdf", "renamed.pdf" }); +} + +WHEN(R"(a link to {string} is attached)", (const std::string& url)) +{ + Link(url); +} diff --git a/compatibility/attachments/attachments.feature b/compatibility/attachments/attachments.feature new file mode 100644 index 00000000..aa44b74d --- /dev/null +++ b/compatibility/attachments/attachments.feature @@ -0,0 +1,36 @@ +Feature: Attachments + It is sometimes useful to take a screenshot while a scenario runs or capture some logs. + + Cucumber lets you `attach` arbitrary files during execution, and you can + specify a content type for the contents. + + Formatters can then render these attachments in reports. + + Attachments must have a body and a content type. + + Scenario: Strings can be attached with a media type + Beware that some formatters such as @cucumber/react use the media type + to determine how to display an attachment. + + When the string "hello" is attached as "application/octet-stream" + + Scenario: Log text + When the string "hello" is logged + + Scenario: Log ANSI coloured text + When text with ANSI escapes is logged + + Scenario: Log JSON + When the following string is attached as "application/json": + ``` + {"message": "The big question", "foo": "bar"} + ``` + + Scenario: Byte arrays are base64-encoded regardless of media type + When an array with 10 bytes is attached as "text/plain" + + Scenario: Attaching PDFs with a different filename + When a PDF document is attached and renamed + + Scenario: Attaching URIs + When a link to "https://cucumber.io" is attached diff --git a/compatibility/attachments/attachments.ndjson b/compatibility/attachments/attachments.ndjson new file mode 100644 index 00000000..7973606e --- /dev/null +++ b/compatibility/attachments/attachments.ndjson @@ -0,0 +1,61 @@ +{"meta":{"protocolVersion":"31.0.0","implementation":{"name":"fake-cucumber","version":"123.45.6"},"cpu":{"name":"arm64"},"os":{"name":"darwin","version":"24.5.0"},"runtime":{"name":"Node.js","version":"24.4.1"},"ci":{"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429","buildNumber":"154666429","git":{"revision":"99684bcacf01d95875834d87903dcb072306c9ad","remote":"https://github.com/cucumber-ltd/shouty.rb.git","branch":"main"}}}} +{"source":{"data":"Feature: Attachments\n It is sometimes useful to take a screenshot while a scenario runs or capture some logs.\n\n Cucumber lets you `attach` arbitrary files during execution, and you can\n specify a content type for the contents.\n\n Formatters can then render these attachments in reports.\n\n Attachments must have a body and a content type.\n\n Scenario: Strings can be attached with a media type\n Beware that some formatters such as @cucumber/react use the media type\n to determine how to display an attachment.\n\n When the string \"hello\" is attached as \"application/octet-stream\"\n\n Scenario: Log text\n When the string \"hello\" is logged\n\n Scenario: Log ANSI coloured text\n When text with ANSI escapes is logged\n\n Scenario: Log JSON\n When the following string is attached as \"application/json\":\n ```\n {\"message\": \"The big question\", \"foo\": \"bar\"}\n ```\n\n Scenario: Byte arrays are base64-encoded regardless of media type\n When an array with 10 bytes is attached as \"text/plain\"\n\n Scenario: Attaching PDFs with a different filename\n When a PDF document is attached and renamed\n\n Scenario: Attaching URIs\n When a link to \"https://cucumber.io\" is attached\n","uri":"samples/attachments/attachments.feature","mediaType":"text/x.cucumber.gherkin+plain"}} +{"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"Attachments","description":" It is sometimes useful to take a screenshot while a scenario runs or capture some logs.\n\n Cucumber lets you `attach` arbitrary files during execution, and you can\n specify a content type for the contents.\n\n Formatters can then render these attachments in reports.\n\n Attachments must have a body and a content type.","children":[{"scenario":{"id":"1","tags":[],"location":{"line":11,"column":3},"keyword":"Scenario","name":"Strings can be attached with a media type","description":" Beware that some formatters such as @cucumber/react use the media type\n to determine how to display an attachment.","steps":[{"id":"0","location":{"line":15,"column":5},"keyword":"When ","keywordType":"Action","text":"the string \"hello\" is attached as \"application/octet-stream\""}],"examples":[]}},{"scenario":{"id":"3","tags":[],"location":{"line":17,"column":3},"keyword":"Scenario","name":"Log text","description":"","steps":[{"id":"2","location":{"line":18,"column":5},"keyword":"When ","keywordType":"Action","text":"the string \"hello\" is logged"}],"examples":[]}},{"scenario":{"id":"5","tags":[],"location":{"line":20,"column":3},"keyword":"Scenario","name":"Log ANSI coloured text","description":"","steps":[{"id":"4","location":{"line":21,"column":5},"keyword":"When ","keywordType":"Action","text":"text with ANSI escapes is logged"}],"examples":[]}},{"scenario":{"id":"7","tags":[],"location":{"line":23,"column":3},"keyword":"Scenario","name":"Log JSON","description":"","steps":[{"id":"6","location":{"line":24,"column":6},"keyword":"When ","keywordType":"Action","text":"the following string is attached as \"application/json\":","docString":{"location":{"line":25,"column":8},"content":"{\"message\": \"The big question\", \"foo\": \"bar\"}","delimiter":"```"}}],"examples":[]}},{"scenario":{"id":"9","tags":[],"location":{"line":29,"column":3},"keyword":"Scenario","name":"Byte arrays are base64-encoded regardless of media type","description":"","steps":[{"id":"8","location":{"line":30,"column":5},"keyword":"When ","keywordType":"Action","text":"an array with 10 bytes is attached as \"text/plain\""}],"examples":[]}},{"scenario":{"id":"11","tags":[],"location":{"line":32,"column":3},"keyword":"Scenario","name":"Attaching PDFs with a different filename","description":"","steps":[{"id":"10","location":{"line":33,"column":5},"keyword":"When ","keywordType":"Action","text":"a PDF document is attached and renamed"}],"examples":[]}},{"scenario":{"id":"13","tags":[],"location":{"line":35,"column":3},"keyword":"Scenario","name":"Attaching URIs","description":"","steps":[{"id":"12","location":{"line":36,"column":5},"keyword":"When ","keywordType":"Action","text":"a link to \"https://cucumber.io\" is attached"}],"examples":[]}}]},"comments":[],"uri":"samples/attachments/attachments.feature"}} +{"pickle":{"id":"15","uri":"samples/attachments/attachments.feature","location":{"line":11,"column":3},"astNodeIds":["1"],"tags":[],"name":"Strings can be attached with a media type","language":"en","steps":[{"id":"14","text":"the string \"hello\" is attached as \"application/octet-stream\"","type":"Action","astNodeIds":["0"]}]}} +{"pickle":{"id":"17","uri":"samples/attachments/attachments.feature","location":{"line":17,"column":3},"astNodeIds":["3"],"tags":[],"name":"Log text","language":"en","steps":[{"id":"16","text":"the string \"hello\" is logged","type":"Action","astNodeIds":["2"]}]}} +{"pickle":{"id":"19","uri":"samples/attachments/attachments.feature","location":{"line":20,"column":3},"astNodeIds":["5"],"tags":[],"name":"Log ANSI coloured text","language":"en","steps":[{"id":"18","text":"text with ANSI escapes is logged","type":"Action","astNodeIds":["4"]}]}} +{"pickle":{"id":"21","uri":"samples/attachments/attachments.feature","location":{"line":23,"column":3},"astNodeIds":["7"],"tags":[],"name":"Log JSON","language":"en","steps":[{"id":"20","text":"the following string is attached as \"application/json\":","type":"Action","argument":{"docString":{"content":"{\"message\": \"The big question\", \"foo\": \"bar\"}"}},"astNodeIds":["6"]}]}} +{"pickle":{"id":"23","uri":"samples/attachments/attachments.feature","location":{"line":29,"column":3},"astNodeIds":["9"],"tags":[],"name":"Byte arrays are base64-encoded regardless of media type","language":"en","steps":[{"id":"22","text":"an array with 10 bytes is attached as \"text/plain\"","type":"Action","astNodeIds":["8"]}]}} +{"pickle":{"id":"25","uri":"samples/attachments/attachments.feature","location":{"line":32,"column":3},"astNodeIds":["11"],"tags":[],"name":"Attaching PDFs with a different filename","language":"en","steps":[{"id":"24","text":"a PDF document is attached and renamed","type":"Action","astNodeIds":["10"]}]}} +{"pickle":{"id":"27","uri":"samples/attachments/attachments.feature","location":{"line":35,"column":3},"astNodeIds":["13"],"tags":[],"name":"Attaching URIs","language":"en","steps":[{"id":"26","text":"a link to \"https://cucumber.io\" is attached","type":"Action","astNodeIds":["12"]}]}} +{"stepDefinition":{"id":"28","pattern":{"type":"CUCUMBER_EXPRESSION","source":"the string {string} is attached as {string}"},"sourceReference":{"uri":"samples/attachments/attachments.ts","location":{"line":4}}}} +{"stepDefinition":{"id":"29","pattern":{"type":"CUCUMBER_EXPRESSION","source":"the string {string} is logged"},"sourceReference":{"uri":"samples/attachments/attachments.ts","location":{"line":11}}}} +{"stepDefinition":{"id":"30","pattern":{"type":"CUCUMBER_EXPRESSION","source":"text with ANSI escapes is logged"},"sourceReference":{"uri":"samples/attachments/attachments.ts","location":{"line":15}}}} +{"stepDefinition":{"id":"31","pattern":{"type":"CUCUMBER_EXPRESSION","source":"the following string is attached as {string}:"},"sourceReference":{"uri":"samples/attachments/attachments.ts","location":{"line":21}}}} +{"stepDefinition":{"id":"32","pattern":{"type":"CUCUMBER_EXPRESSION","source":"an array with {int} bytes is attached as {string}"},"sourceReference":{"uri":"samples/attachments/attachments.ts","location":{"line":28}}}} +{"stepDefinition":{"id":"33","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a PDF document is attached and renamed"},"sourceReference":{"uri":"samples/attachments/attachments.ts","location":{"line":37}}}} +{"stepDefinition":{"id":"34","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a link to {string} is attached"},"sourceReference":{"uri":"samples/attachments/attachments.ts","location":{"line":47}}}} +{"testRunStarted":{"id":"35","timestamp":{"seconds":0,"nanos":0}}} +{"testCase":{"id":"36","pickleId":"15","testSteps":[{"id":"37","pickleStepId":"14","stepDefinitionIds":["28"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":11,"value":"\"hello\"","children":[{"start":12,"value":"hello","children":[{"children":[]}]},{"children":[{"children":[]}]}]},"parameterTypeName":"string"},{"group":{"start":34,"value":"\"application/octet-stream\"","children":[{"start":35,"value":"application/octet-stream","children":[{"children":[]}]},{"children":[{"children":[]}]}]},"parameterTypeName":"string"}]}]}],"testRunStartedId":"35"}} +{"testCase":{"id":"38","pickleId":"17","testSteps":[{"id":"39","pickleStepId":"16","stepDefinitionIds":["29"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":11,"value":"\"hello\"","children":[{"start":12,"value":"hello","children":[{"children":[]}]},{"children":[{"children":[]}]}]},"parameterTypeName":"string"}]}]}],"testRunStartedId":"35"}} +{"testCase":{"id":"40","pickleId":"19","testSteps":[{"id":"41","pickleStepId":"18","stepDefinitionIds":["30"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}],"testRunStartedId":"35"}} +{"testCase":{"id":"42","pickleId":"21","testSteps":[{"id":"43","pickleStepId":"20","stepDefinitionIds":["31"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":36,"value":"\"application/json\"","children":[{"start":37,"value":"application/json","children":[{"children":[]}]},{"children":[{"children":[]}]}]},"parameterTypeName":"string"}]}]}],"testRunStartedId":"35"}} +{"testCase":{"id":"44","pickleId":"23","testSteps":[{"id":"45","pickleStepId":"22","stepDefinitionIds":["32"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":14,"value":"10","children":[]},"parameterTypeName":"int"},{"group":{"start":38,"value":"\"text/plain\"","children":[{"start":39,"value":"text/plain","children":[{"children":[]}]},{"children":[{"children":[]}]}]},"parameterTypeName":"string"}]}]}],"testRunStartedId":"35"}} +{"testCase":{"id":"46","pickleId":"25","testSteps":[{"id":"47","pickleStepId":"24","stepDefinitionIds":["33"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}],"testRunStartedId":"35"}} +{"testCase":{"id":"48","pickleId":"27","testSteps":[{"id":"49","pickleStepId":"26","stepDefinitionIds":["34"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":10,"value":"\"https://cucumber.io\"","children":[{"start":11,"value":"https://cucumber.io","children":[{"children":[]}]},{"children":[{"children":[]}]}]},"parameterTypeName":"string"}]}]}],"testRunStartedId":"35"}} +{"testCaseStarted":{"id":"50","testCaseId":"36","timestamp":{"seconds":0,"nanos":1000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"50","testStepId":"37","timestamp":{"seconds":0,"nanos":2000000}}} +{"attachment":{"testCaseStartedId":"50","testStepId":"37","body":"hello","contentEncoding":"IDENTITY","mediaType":"application/octet-stream","timestamp":{"seconds":0,"nanos":3000000}}} +{"testStepFinished":{"testCaseStartedId":"50","testStepId":"37","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":4000000}}} +{"testCaseFinished":{"testCaseStartedId":"50","timestamp":{"seconds":0,"nanos":5000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"51","testCaseId":"38","timestamp":{"seconds":0,"nanos":6000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"51","testStepId":"39","timestamp":{"seconds":0,"nanos":7000000}}} +{"attachment":{"testCaseStartedId":"51","testStepId":"39","body":"hello","contentEncoding":"IDENTITY","mediaType":"text/x.cucumber.log+plain","timestamp":{"seconds":0,"nanos":8000000}}} +{"testStepFinished":{"testCaseStartedId":"51","testStepId":"39","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":9000000}}} +{"testCaseFinished":{"testCaseStartedId":"51","timestamp":{"seconds":0,"nanos":10000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"52","testCaseId":"40","timestamp":{"seconds":0,"nanos":11000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"52","testStepId":"41","timestamp":{"seconds":0,"nanos":12000000}}} +{"attachment":{"testCaseStartedId":"52","testStepId":"41","body":"This displays a \u001b[31mr\u001b[0m\u001b[91ma\u001b[0m\u001b[33mi\u001b[0m\u001b[32mn\u001b[0m\u001b[34mb\u001b[0m\u001b[95mo\u001b[0m\u001b[35mw\u001b[0m","contentEncoding":"IDENTITY","mediaType":"text/x.cucumber.log+plain","timestamp":{"seconds":0,"nanos":13000000}}} +{"testStepFinished":{"testCaseStartedId":"52","testStepId":"41","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":14000000}}} +{"testCaseFinished":{"testCaseStartedId":"52","timestamp":{"seconds":0,"nanos":15000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"53","testCaseId":"42","timestamp":{"seconds":0,"nanos":16000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"53","testStepId":"43","timestamp":{"seconds":0,"nanos":17000000}}} +{"attachment":{"testCaseStartedId":"53","testStepId":"43","body":"{\"message\": \"The big question\", \"foo\": \"bar\"}","contentEncoding":"IDENTITY","mediaType":"application/json","timestamp":{"seconds":0,"nanos":18000000}}} +{"testStepFinished":{"testCaseStartedId":"53","testStepId":"43","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":19000000}}} +{"testCaseFinished":{"testCaseStartedId":"53","timestamp":{"seconds":0,"nanos":20000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"54","testCaseId":"44","timestamp":{"seconds":0,"nanos":21000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"54","testStepId":"45","timestamp":{"seconds":0,"nanos":22000000}}} +{"attachment":{"testCaseStartedId":"54","testStepId":"45","body":"AAECAwQFBgcICQ==","contentEncoding":"BASE64","mediaType":"text/plain","timestamp":{"seconds":0,"nanos":23000000}}} +{"testStepFinished":{"testCaseStartedId":"54","testStepId":"45","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":24000000}}} +{"testCaseFinished":{"testCaseStartedId":"54","timestamp":{"seconds":0,"nanos":25000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"55","testCaseId":"46","timestamp":{"seconds":0,"nanos":26000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"55","testStepId":"47","timestamp":{"seconds":0,"nanos":27000000}}} +{"attachment":{"testCaseStartedId":"55","testStepId":"47","body":"JVBERi0xLjQKJdPr6eEKMSAwIG9iago8PC9UaXRsZSAoVW50aXRsZWQgZG9jdW1lbnQpCi9Qcm9kdWNlciAoU2tpYS9QREYgbTExNiBHb29nbGUgRG9jcyBSZW5kZXJlcik+PgplbmRvYmoKMyAwIG9iago8PC9jYSAxCi9CTSAvTm9ybWFsPj4KZW5kb2JqCjUgMCBvYmoKPDwvRmlsdGVyIC9GbGF0ZURlY29kZQovTGVuZ3RoIDE2Nz4+IHN0cmVhbQp4nF2P0QrCMAxF3/MV+YF1TdM2LYgPgu5Z6R+oGwg+bP4/mK64gU1Jw73cQ0potTrSlrzD+xtmMBJW9feqSFjrNmAblgn6gXH6QPUleyRyjMsTRrj+EcTVqwy7Sspow844FegvivAm1iNYRqB9L+MlJxLOWCqkIzZOhD0nLA88WMtyxPICMexijoE10wyfViMZCkRW0maEuCUSubDrjXQu+osv96M5GgplbmRzdHJlYW0KZW5kb2JqCjIgMCBvYmoKPDwvVHlwZSAvUGFnZQovUmVzb3VyY2VzIDw8L1Byb2NTZXQgWy9QREYgL1RleHQgL0ltYWdlQiAvSW1hZ2VDIC9JbWFnZUldCi9FeHRHU3RhdGUgPDwvRzMgMyAwIFI+PgovRm9udCA8PC9GNCA0IDAgUj4+Pj4KL01lZGlhQm94IFswIDAgNTk2IDg0Ml0KL0NvbnRlbnRzIDUgMCBSCi9TdHJ1Y3RQYXJlbnRzIDAKL1BhcmVudCA2IDAgUj4+CmVuZG9iago2IDAgb2JqCjw8L1R5cGUgL1BhZ2VzCi9Db3VudCAxCi9LaWRzIFsyIDAgUl0+PgplbmRvYmoKNyAwIG9iago8PC9UeXBlIC9DYXRhbG9nCi9QYWdlcyA2IDAgUj4+CmVuZG9iago4IDAgb2JqCjw8L0xlbmd0aDEgMTY5OTYKL0ZpbHRlciAvRmxhdGVEZWNvZGUKL0xlbmd0aCA4MDA5Pj4gc3RyZWFtCnic7XoJeFRF9u+pureXrN0J2TrppG+nkw6kA4EECEtMOhugkT1gwiSSAJGAIEtAQVGaGVCJKI4LDuiI+6CO0lnADi4wMjojLjDquAsIjOLMIOgoruS+X1V3gIj65sv7z3uf75u+Ob86derUqapTp869N93EiKgPQKWBo8srRtFH9C4R80Pad/SE8ZN9g357HRE/gvrq0ZOnlIY/Y1qH9rdQHzh+cm7esjHbj6F9Ner1U8vHVk+4Ze4XaNpHFHPbzPkNCxlny9DuRXv5zMuXaPfa3/wHkXEXqOqShbPnv7S8ZhNRVBzql81uaF5ISRQG+4XQt86et/ySu6oLu4jsOUTmQ02z5i97puTkEkwY45m3NDU2zDoY9zzscTP0hzZBEJsf5kR/zJEymuYvWRa/nu0nMtRDVj9vwcyGRE885qc0ob1tfsOyhYb2KB/aLkRdu6xhfmNi/aD34Qw7ZOULFzQv0bNpA/h5on3h4saFmW+M3UmUaSWKeAYyhczEKYaYroMXvqymz6iQfksmyK2US1Nh7ffQNaCukPzoWcLmD3zQ31TUNY7KrPTN1m+utEpJj0+1lESGahy7FuxXgIvRGFwMI14EFHrhNACXoWFxwwzSZi5fPI+02YsbLyWtqXHGYtLmNSy5jLQzY5PBtmmRI6Z9uqXwC3OKWYrvO5yVLcoXJ4zc/s3WU7OtZBajh501My79QBQX8kCciCWUZukboipqpCXwT5Br1nX9sLjOsqAo17Ob4SGzYZMhH1NJCZbKX+gSHms28AijysVHpe95ZOz4cePJC7tLDK91TWT5piLW5hWbgdFUt+FJsWuYTdAXpVRLivRCTtALcv1xQR+iB+v2p+TZWTymcmnjYuiejaG5CD2OlTJJkRScY6y0UICWMXoqTQURxf9fvTb87y52549fylPqIulgE00Tu6riTNJc8oV4Bm9eHuI5RVNTiFewF31DvHqWjoGSoRXkjeCISmgxzaEGmkdjsXtTEReLqRmSBSQicgiidhBiqAGtQrKAltByWggtjc6n+ZDPhu5lQI36g85Y02gStGbTUvANkPasndF7GJp5GGEQLg0zaJK2zx2tDLXF4AU2QB6c4QA55rzQeHMwQhPamkOjN8vVXA6cRQOM5xzh/38+6mF5zv/PbDRTZa/6ERXz4ZRh2EE2ULLhd2RT3bh7kP4R6Kgou+boR0W7KPnf0SkQIqIt9BibQ4/RTnqWnUCvrdRJHfRnSqRyuotW0G10HSJ1GiRrsaeTEMHldBuz6R3I6Pciku+ll6F7EV1DOyiBJekf00pao7yGXmsoitIRHRMQKTeyC/WlyDoH1F8hF1yIyFnIfHq1fpN+i/4APUidyp/1UxSB0zET18v6J4a39PcQ0bV0O22kA+yWsG04URfh3HUqv0VMbVLqVKbP1r/BDJx0BeagImZfZru4B9Yb6SOWxFYoZbByv+7X/wgtO9UhNjfRDjaEjeZOQ60+Vn+ZEjDGMljdSG20HVeAnqZ3WKThhP6AfoJslINTthL+eIXtUrpOreoqhscM8FI/Go6WBfQM/Yn2MRf7A19giDTkGbyGK/XXkREH0RTM9nfo+SH7kl+Da6XyvDpKL8WZX0O/Ft6m5+gDlsxy2Xg2lffjC/jdymJkzhx5EmfhLK2l38D6fuZh23kk36vcrz6qfmtM7TqoR2NH3HQn7q1/YFFYqcaa2S/ZG+wwL+PT+Z38kHKb+rD6qqkBq74YWeJGepS+ZLFsGJvIfsGa2Ap2Hfs128heZvvYUV7Cq/il/LjSpCxSnlZLcU1Wm9VfGa413GA82lXd9ceuv3R9qefp19JExMMqzP52uhsr66S99DauA3SIGVgEi8alMSebwq7CdQ27kd3HtrCHWQdG2ccOsY/ZZ+wL9i1HouRGnsKdPB2Xiy/mV/Db+F18L659/J/8ayVRSVc8yhClUKlRFmBW1yk349qmfKAmq3tVHX7OM2wwbDZsMTxqeNZwwhhp+iVusS99d/+p7FP7u6jr+q4NXW1dHfoHyP42xJSdHHgSmYi81YDcvQw5/0HE+WssEr5LZtmsiF0Iz0xnc9kitgyeXM02sQfl3B9nT8FLb7LjmHMUt8s5D+BDeCkfj+ti3sgX8Zv5LbyDv8G/UUxKhGJR4pVsZbRSpzQqS5TlygbFr7ykvK8cUk4q3+HS1XDVoaarbtWjjlanq0vVu9WP1I8MtYYXDX8zhhvnG681BoyfmoaaikwTTBNNdab1pu2m1831iM7dtI2eOPvss4PKKqVC2UY38XzVxl/hryCep9MsZSxHpPIt7Hp+NevgGYZlxpF8JBtHJ1Q3fP0838xP8pHKWFbJJtNcPihozRinPoKiUN1Nx9SnsLZXYHmZMZJdw48bI6kNjwXDMeZzykDVo7xI7ygHmEm9l95Vw1kiO8Z/p0xAFDytFhmqyancRY8ri9jVtI1X4JHjW/M6xPE49gjyQhXLY18peErk4xBFBcph+hVdyt+iYzjH19MdbJY6m26ifLYCT+AP4VT0M1xmzDbGsxf4HLWF92EdxNWHsbrhLIMphjhazeqUTcbj/G3c3faq4bRf+T1mv5c/roxVTxgmsSacgKvpWlqkr6Llhmr1VTabFDaVMtWDyG4rlDzViXIlskotctp2nO4dyAMlylhIkhA5FyIupiBDbML1G+QJFRE0B2f8ImSxV6jDWMUDNNsQzZB1kI1f7JpE0/SHaKM+my7Tb6H+yAfX6StgcQv9jdbTFram6yrcR9NwcvazCw2j+F7DKL0/b+Fv88l8Q8/9hbczWRL9HdfjqBThOa5FfZMmU7G+Tv8rorsvMuxGmkEX0BGs8hOMMEbZRfld43irPkpZiPUeoIn673QHC6cmfR6Np6foQZOBGkwe7LGfvYr1XkWNfJK+RGnsmgM/rIcXvPDWUuSftd6yKVUl3uKi8wpHjhg+rGDI4Py8QQNzB/TP8WT365vlzsxwpTs1R1qqPSXZlpSYEB/XJzbGaomOiowIDzObjAZV4YxyKlyj6jW/u96vul1jxvQXdVcDBA1nCer9GkSjeur4tXqppvXU9ELzku9peoOa3tOazKoVUmH/HK3CpflfLndpATZtYjX4G8tdNZr/mOTHSv5myUeBdzrRQatIairX/Kxeq/CPuryppaK+HOZaI8LLXGWN4f1zqDU8AmwEOH+ia2ErSyxikuGJFSNa8QQchUn5k13lFX6bq1zMwK9kVjTM8k+YWF1RnuJ01vTP8bOyma4ZfnKV+i0eqUJlchi/scxvksNoc8Rq6AatNWdXy7qAlWbUeyJnuWY11Fb7lYYaMUaMB+OW+xOvPJJ0pgrjsWXV153dmqK0VCTN0US1peU6zX/PxOqzW50Ca2pgA3155qj6llEYeh2cWDlZw2h8TU21n63BkJpYiVhVcH2NrgohqZ+r+cNcpa6mlrn12JrkFj9NWu5sS072duoHKblCa6mqdjn9xSmumoZye2sctUxa3m7zaraeLf1zWq0xQce2RltCTGTU2Uzj6TbJSXXBVU467VkmZuQ6HwHh12ZqmEm1C2saJqBxGLXMHAY1fGoYevlnYUfm+MPK6lusI4Rc9PcbMq0ureULQgS4jv2zp6QhJDFmWr8gwYo4OR1qaO/m/R6PPztbhIipDHuKORbJ+pD+OZcHuMu10KqhgPtoAnzbUDMiF+53OsUG3xDw0gxU/L6J1cG6RjNS2sib66nx83rRsqu7JX6KaPF1t5zuXu9CJHfIJ+54v9l9+s9iTehT0TTCzxJ+orkx2F452VU5cVq1VtFSH/JtZVWPWrB92Om2EOfvU1atpPAQx1MU2YqgrD2tLCrVkX41E39GGdSzAiYzolJKmDbKb60fE8SacKfz3+wU0E+IXrI40y00Tf8IT8/6yB71HtOLbFEwYdwqK6umtbSE92hDqAUHPD9UIOKpqtqplflpCk5mJv4C+q5hgmpS/F64rEwoIP6ColC1h2JKiK/BR0Rn/5xRSHQtLaNc2qiW+paGgO6b4dKsrpZO/ix/tmVhRX134AT0HTek+Eetq4GvmtgIHApOpa0udv3EVi+7fvK06k4r3vyvr6pu44yX1ZfWtGagrbpTI/JKKRdSIRQVTVSokmGRbdws9VM6vUQ+2apKgazPDDCSMnO3jNHMAA/KrN0yDpkalHmlTHxEjimrqj47euSRrOkvb3h4b6HaCLO5N69CeIT5aYFRIYoMC+udbdNPC0ywHRUe/p+xjZc8S0RE72yfs9yevjXDtjUy8vtKvbTdUyBsx0RF/cds94mO7p3tc5bb07fhBiRGq/V/yHZPQQRCMik2tne2z1luT99GImxS4uJ6Z/uc5Vp6Do2wSU1I6J3tPj89mAW2taSk/yHbMT1HQtg4bbbe2Y7/adsxsJ1pt/fOduL3BT33LRapJFvTemc7+acHi0NIDnC5emf7nOX2HCwRIZnndvfOtuOnB7Mh/of269c7287vC9J61FIQ7iNycnpnO+P7Aq1HLRXhXpaX1zvb5yw3s0ctHfFfOWxY72z3/74gu0fNjfifXFTUO9uDvy8Y0HMkhGRtRUXvbA//viC/50gIyVmVvfp3Kt6yvy/o6ds8EZJcfkmEixRxq3bGOGMyAeIrkO80Zdd3XgN9S5q6S3wDMpBI3WHYAb39XpuRR0aWTjFJNJoiIsBLZAH96w7BEBhvjOCMhsgoNEtE87cdgkHzt94YwRl4Gl6vSb5mhwV4c7umMjXA2BNGjfFchSngtzGmYQYB/ag3wmrlU8hssXBh47OOyEjJHOqIipLMd5AYBdMFiWBg0bx9Y5LHetIjP3WF1s9Bp47UfWgttBZScXHhqcJBA5nn9AcOGOKMd8bwPl2paktXiiHqsce++ReeAiv1o2qaWoRsmsru9iY6yB7Ppyh1hrqwKRGNyqWGBWGNEeb4gH5EDh0DxjtJcKl2gVmxbxu+iTuZrA6KHWEbZC+JHZtcYp8YW2ubZG+InZ/cYF9mXBZ/kp9MslICs0QlJk5IqE9YmKAk2C03W++xcqtVTbGHm2gHf4SYvqtDOAL+3OWNtlqNU6yMsdv72NWIRLw3dIhtSRTuERsA5qvtUXB1ojcqoL8nPQXmEzlLMH+XLosSpsKysgf7o1hUsgO19kz3YFE+keYaPNDBHAnwrrdWGErIt5rFENZoYd9qFjJrhsmbkT3YYSo2jTcppkgZH5GixaRFRPAppiSxVSa7GN2EfkbwYlxTgpiGyZY2uCDJM876efcu1HnGnkJxBLJFHs/JRUI29hiAio+dqkND8bHY4bl1hacWFbKY2OHDY4djE+sILR62aDFLNBpd6RRjpfw8iokzORMS8vOGMqc7y+1KNyoX78j5pPPjruMs7r2/smj23dHwtjUz1516h0+MHDZ17YqH2dTE+zuYgykskvXt2t/1tVXbuqOJ3X5tWdND4iwU60eVVkTCQKXV2ydReiFJok1i34D+udyDrG7G3c1kdjMZ3Yyrm0nvZpzdjAbGu1Jwanpc+oiwC8LKM6amN6avCLspbHXGQ30ezXlWiQpLTE5KHFiZ80aiIYVP4dyax8KTas21YbXhtRG1kbVRc81zw+aGz42YGzk3qsPdkWXJcmdkZfQbmjEtvCZilntW3yWuJRm+jFvD74q8pe8dObcPfCD84cj7sx7o2+5+zp0g1yK2KL2bcXUzGd1MaL3G7iUYuxdl7F4mDkFA3++NTRs+zZyVGRmuJmvueDViQGpygD/iTbfliBBx2Ipt423TbVtte21Gi81hW2A7YFMdtvU2bnsapxtZPBj73jihbmVexq1sH+PErIyLs9AelzBYnglrdMxgxgbUps5L5an2eJMqpiE6gfmwQxwYwXj7WCzg7AMiHMksOcPm7ZM0OE90HyLyiy0piCJibQkiem2a6GnTRC+bVazKJqNXtGLvd/BfkEn/bLtMhxnZMLTNPnxfNssWY4r+YI52CKOSEf2zxfETJsB8vl1YyU6WM3DiJNbn7crjxXm+PJ4njncGyamQVSY2Leh8LoNErkhGi0PMTZNRqGVYrGLJFjl3iyaULQH9G69bTMESLca3RApjFqMY2ZJ+gFgxjUemsw0Knca6RWO7T6Q4ex4rysXjrHWLPMF0ukicyc/P5M5ji3E8URYfW4TTiVO8aLHniPWULHBK8YfDmoijWrbc683qn+YyxOW4Y6yx1j5WxZgepaVQWF9TCjP0B6TFoeqMdqVQuisq0twvPIX1zQoLN3rUFHJYU1MYYT5I4UGQCTzbs2rVKjo9m7pFrG7xorozAqHUp0DmgiGDs9xZA/iQwUMLhg7Nz0tISDS5RW6Ij0tMwJXG4+NECnEXt1nWXrVi2ZDMW5/fOL5kWPavJ1/99LQYf2TznBVzExJyU1bvvGPqnOev3vs2O89+6eLG8vNcSZl5568aN3p5X4dnzFWzkybVTipw2VP7hGfkl6yonbb5ot+LDJKhf8azDRspkTk6KRJ3K7EDEYEQY+5mTN2MsZsJF2Hucg8OE1EyGYzPxohFRoUzhRKsYR5LuDHBrkRYrOmUzqJiZW6OlfEQGy76x2ZGMt1krgirqDctNPlMN+Ol3KSZ7jH5TbtM+0xGk7gziHuLScSViBSTuJFER0vmKxlykpHpHOEkYw/MCW+EiD2TUWZ1EeAyse/gcymJDW295MwtWO7M50esxwpFhi+0Hvkct+Fj4j4cgzQek59vfUHk8pBqZqLYBveQGNeQ/JiCmPx4V0yc2EFuTb6wcMa8nNWr27dt6+Ppm3bvZmtR43185jpmmtd147pTt47NwfNTJ1UpyGRJjn1PKf3oIIgr/do8qY5OJUtJbRvp8AYUV3tsfJ6lpL8injJyJWrABaCtoJ2K+M3JdCUNcitwJcgH2graCdoHwtswULRqoAWgzaCDokVJVextmsNakqXY0NeG82VREuk4SAcp5ADmgsaDpoPWgzaDjFJPSBaAVoJ2gk7IFq+S2HZLPuae2HaDLNrnzsuT1YZgtbZOVtsvqgmWYycGy/Lzg2ojgmqDBgfFA0qDZVZOsIzNzPOJMjwqb1cJHkKwyARMfCGQ8T+ShTG85NyjxJMfxBVjSOJVYtsz3HmbdyoqMYUrjGaRQ9+lsLaomLyScK7z4xRLDv4JPxZs4cfao2PyNpdcwA/RVtBOkMIP4fqAf0Ar+UHhc2AxaDNoJ2gv6DjIyA/iOoBrP99PFv4+5YKKQdNBm0E7QcdBJv4+0MrfE/8rlij4YhDn7wGt/F0s612ghb8D7h3+Dqb2WlvB8LxOyXhyQ4wjM8QkpoSY2IS8AH+17et+iCg3dhoR9aSSjsfvfCW9LXOQI6AktRXOcQT44XbN47inZCB/nfwgjpm8jpFfJw00AVQPWggygnsD3BvkA90MugfkByHKgFaQxveAXgK9QQNBXtAEkJnva8MwAb63zV3qKEngr/A/4a3ZwV/mf5blS/x5Wb7In5PlCyjTUO7hz7elOagkAu2EPlaUVpS5aDfwP7RnxDr0khi+E75zAHNBxaDxoOmg9SAj38nT22Y5YmHkSdpjxnswb6OPZfkQ3Wcm71yH112GANQEuEecBw6wWdvs5l73ho2oCnDfdAs4Ae7V68AJcF+5CpwA97zLwQlwz5oLToB72nRwAtzjq8ABAvzuJzKyHAXjL2VaiYVfAS9dAS9dAS9dQSq/Qlz0tSrmdmdbdjY8tsnr6Zft8O1gvqeYbxLz3cd8jcx3DfOtYr5C5ruY+TzMZ2e+NObzMt+TbBhc4WPejh7V4d4k5tvDfI8xXzPzuZkvk/kymE9jBd4Ad7adny+LClm0l4hDh/K8ImQfC3fCo07EvBM5YSdwL0iXNS+UtPSgsi1NlOnt2cXB+oAReQtKxvDd6Lgb27CbDoBUbNBuhNFuGNkNAxZgMWg6aBfoOEgHGaGdjomvl2gB5oKKQdNBK0HHQUY5neMgTgtCU9wqJ5YbmvR4UeO7cYkfQzi505tqtVs91jHKejuzpLHxaXoaLyD5f7fYGHNMgEVt/zLqqy+jKKwkjN/E11MqNuLmULm+7etUR4D9ps39pKMknt1BaSqijg0nN8tEOYyaZX0I2c2iHEx2/ijKvDb7VHSztLlzHDtYtOi13fG1/YjjY3uAgz1qf9LxphZQWZvjr5A8ut3xun2t44XcgBmSp9x40Wxz7NCkaqd9mOOxPVJ1FRo2tTmuEcV2x9X20Y5L7bKhMdhwcTNqXotjknuaYwzsldtnOLzNsLndUWy/2FEY1Boi+mx3DMQUPEE2G5PtZ5eDutKkwSkFAdbkzTFtMFXjHWqoKc+UY3KaHKZUU4opzhxrtpqjzZHmcLPZbDSrZm4mc1xAP+j1iOeJOKP8calRlT9glLyVk/wJpPxZI2dmTheQv49SySsnl7JK/66ZVDlD85+c7Aqw8InT/AZXKfPHVlJlVal/mKcyYNIn+Qs8lX7ThF9UtzJ2Uw2kfn59gFFVdYDpQrQmRXxH20mMxay5MUWUfdfcWFNDSQmXFycVxxbFDB9V/gNQH8Izj42epB58qn9D5eRq/yOpNf48weipNZX+W8WXuJ3sM3aioryTfSqKmupOpYh9VjFJyJWi8pqaygCbKvVIY59CDxHzqdQz48Ys9EgzpwX1NgX1MtEfehmigF5YGGVKvcywMKmnMqHX2pxRUd6akSF1EjVqljrNidrZOnsyoZOZKXUSfLRH6uxJ8Akdf5FUsduhkmaXKiyZ7FLFzpKlytQzKrkhlbWnVdbKkRR2Rsce1Ik62K0TdRA6nn/301iK5+H2kTUza8UX4PWuikZQvf+Gy5uS/L4ZmtY6syb0zbi7fsbMJlE2NPprXI3l/pmucq11ZO0PNNeK5pGu8laqraiqbq31Npa3jfSOrHA1lNe0j54wuKDHWGtPjzV4wg8YmyCMDRZjjS74geYC0TxajFUgxioQY432jpZjkYzxCdWtZiqtKasNlu08IhzxWp/irClNsC4sksE70pl0TcoOPK1soQhPjT/SVeqPAomm/iX9S0QTzpRoiha/cgg1JV0z0pmyg20JNVkhjnGVkmfJ0uallFQxpzz414wPREuWCocH0dP8Yx+0Vfi9DeXNS4gq/dmTK/3FE6dVt5pMkNaLJflHdMsiIirw+B8UDoBwhBAqymlFISsUsrCwkOK5+780VJaJU+DjT7YzbxpbQs01ij+tsoojFVSFvk7egWcpcXtorsECm5mHNXfbCE3b4wm9YpFYczctWRriQr5YEiqDPdGludslpz/CWZ7THlsCg+KjkMLEx6AoeM1nlGT4Z8Qu+sqsi1+k610URmH6KQqncPnbywhgJF6pTlEURQGjJVooGmglCzAG+B0eQ2OAfSgWGEd9gPHAbymB4oCJFA9MAn5DNkoEn0w28CmUDLRLTKUUYBrZ9a/x6CtQo1SgEw+2X1M6aUAX8CvKICcwk9KBbuCXlEUuYF+8B35J/cgNzJbooSz9JOVQX2B/iQMoG5hLHuBA6g8cBPyC8mgAMJ9ygYNpoP45DZE4lAYBCygfOIwG6/+i4RJH0BDgSImFNBR4HhUAi2gYsJiG65+Rl0YAS2gksJQKgWXAT6mczgNWUBFwFBXrJ2g0eYFjqAR4PpUCL5BYSWXAC6kcOJZG6cdpnMTxNBo4gcYAJ9L5+ic0SeJkugBYRZX6MZpCY4FTJV5E44DVNF7/J9XQBOA04DH6BU0EX0uTgXVUBbxY4nSaov+D6mkqsIEuAs4A/p1mUg1wFk0DNtIvgJdQrf4xzZbYRHXAOXSxfpTmUj34SyXOowbgfJoB+WU0E7hA4kKapX9Ei6gRuJhmA5slLqEm/UNaSnOAl9Nc4BXAv9EyuhS4nOYDr6TLgFdJXEELgFfTQuA1tEg/Qisl+qgZuIqWAH9JS3Xxm8LLgaslrqEr9EN0LS0DXkfLgdfTlcC1dJX+AbXQCuANdDUk64Af0I10DfAmWglcT6uANwMP0q/pl8Bb6FfAW2m1foBuk3g7rQFuoOuAd9D1aP0N8ABtpLXATdSi76c76QbgXbQO+FuJd9NNwM20HngP3Qy8F/g+3Ue/Bt5PtwAfoFuBD9Jt+nv0EN2uv0u/ow3ALXQH8GGJj9BvgI/SRuDv6U7gYxIfp7uAW+m3QD/dDWwFvkNttBnYTvcAO+g+/W3aRvfrb9F2iU/QA8AAPQjspIeAOyQ+SVuAT9HD+pv0ND0CfEbiTnoUuIt+D/wDPQZ8lh4H7qat+hv0R/IDn6NW/a/0vMQ/URvwz9Suv04vUAdwD20DvkjbgS/RE8CXKQB8hTqBeyXuox3Av9BTwFfpaf01eg34Kr1OzwD/SjuBb9Au/S/0psS36Fng27Qb+A79EfiuxPfoOeD79DxwP/1J30cHJB6kF/S99AHtAR6iF4GHJR6hl4B/o5eBH9IrwI9on/4KHZX4Mf0F+Hd6VX+Z/kGvAf8p8Ri9DvyE3tBfouP0JvCExE/pLeBn9DbwX/QO8HOJX9B7+ot0kt4Hfkn7gV8B99DXdAD4DR0EfksfAL+TeIoO6y9QFx0B6vQ34H9z+n8+p3/6M8/p//i3c/rHP5LTPz4npx/9kZz+0Tk5/cN/I6cfOZ3TF/fI6Yd/JKcfljn98Dk5/ZDM6YfOyumHZE4/JHP6obNy+gfn5PSDMqcflDn94M8wp7/9/yinv/7fnP7fnP6zy+k/9+f0n29O/7Hn9P/m9P/m9B/O6X/++ef0/wVVj3DwCmVuZHN0cmVhbQplbmRvYmoKOSAwIG9iago8PC9UeXBlIC9Gb250RGVzY3JpcHRvcgovRm9udE5hbWUgL0FBQUFBQStBcmlhbE1UCi9GbGFncyA0Ci9Bc2NlbnQgOTA1LjI3MzQ0Ci9EZXNjZW50IC0yMTEuOTE0MDYKL1N0ZW1WIDQ1Ljg5ODQzOAovQ2FwSGVpZ2h0IDcxNS44MjAzMQovSXRhbGljQW5nbGUgMAovRm9udEJCb3ggWy02NjQuNTUwNzggLTMyNC43MDcwMyAyMDAwIDEwMDUuODU5MzhdCi9Gb250RmlsZTIgOCAwIFI+PgplbmRvYmoKMTAgMCBvYmoKPDwvVHlwZSAvRm9udAovRm9udERlc2NyaXB0b3IgOSAwIFIKL0Jhc2VGb250IC9BQUFBQUErQXJpYWxNVAovU3VidHlwZSAvQ0lERm9udFR5cGUyCi9DSURUb0dJRE1hcCAvSWRlbnRpdHkKL0NJRFN5c3RlbUluZm8gPDwvUmVnaXN0cnkgKEFkb2JlKQovT3JkZXJpbmcgKElkZW50aXR5KQovU3VwcGxlbWVudCAwPj4KL1cgWzAgWzc1MF0gNTUgWzYxMC44Mzk4NF0gNzIgWzU1Ni4xNTIzNF0gODcgWzI3Ny44MzIwM11dCi9EVyA1MDA+PgplbmRvYmoKMTEgMCBvYmoKPDwvRmlsdGVyIC9GbGF0ZURlY29kZQovTGVuZ3RoIDI1MD4+IHN0cmVhbQp4nF2Qy2rEIBSG9z7FWU4Xg0lmMtNFEMqUQha90LQPYPQkFRoVYxZ5+3pJU6ig8PP/n+dCb+1jq5UH+uaM6NDDoLR0OJvFCYQeR6VJWYFUwm8qvWLiltAAd+vscWr1YEjTAND34M7erXB4kKbHO0JfnUSn9AiHz1sXdLdY+40Tag8FYQwkDuGnZ25f+IRAE3ZsZfCVX4+B+Ut8rBahSrrM3QgjcbZcoON6RNIU4TBonsJhBLX851eZ6gfxxV1Mn64hXRT1mUV1vk/qUid2S5W/zF6ivmQos9fTls5+LBqXs08kFufCMGmDaYrYv9K4L9kaG6l4fwAdQH9hCmVuZHN0cmVhbQplbmRvYmoKNCAwIG9iago8PC9UeXBlIC9Gb250Ci9TdWJ0eXBlIC9UeXBlMAovQmFzZUZvbnQgL0FBQUFBQStBcmlhbE1UCi9FbmNvZGluZyAvSWRlbnRpdHktSAovRGVzY2VuZGFudEZvbnRzIFsxMCAwIFJdCi9Ub1VuaWNvZGUgMTEgMCBSPj4KZW5kb2JqCnhyZWYKMCAxMgowMDAwMDAwMDAwIDY1NTM1IGYgCjAwMDAwMDAwMTUgMDAwMDAgbiAKMDAwMDAwMDM4MiAwMDAwMCBuIAowMDAwMDAwMTA4IDAwMDAwIG4gCjAwMDAwMDk2MDYgMDAwMDAgbiAKMDAwMDAwMDE0NSAwMDAwMCBuIAowMDAwMDAwNTkwIDAwMDAwIG4gCjAwMDAwMDA2NDUgMDAwMDAgbiAKMDAwMDAwMDY5MiAwMDAwMCBuIAowMDAwMDA4Nzg3IDAwMDAwIG4gCjAwMDAwMDkwMjEgMDAwMDAgbiAKMDAwMDAwOTI4NSAwMDAwMCBuIAp0cmFpbGVyCjw8L1NpemUgMTIKL1Jvb3QgNyAwIFIKL0luZm8gMSAwIFI+PgpzdGFydHhyZWYKOTc0NQolJUVPRgo=","contentEncoding":"BASE64","mediaType":"application/pdf","fileName":"renamed.pdf","timestamp":{"seconds":0,"nanos":28000000}}} +{"testStepFinished":{"testCaseStartedId":"55","testStepId":"47","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":29000000}}} +{"testCaseFinished":{"testCaseStartedId":"55","timestamp":{"seconds":0,"nanos":30000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"56","testCaseId":"48","timestamp":{"seconds":0,"nanos":31000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"56","testStepId":"49","timestamp":{"seconds":0,"nanos":32000000}}} +{"attachment":{"testCaseStartedId":"56","testStepId":"49","body":"https://cucumber.io","contentEncoding":"IDENTITY","mediaType":"text/uri-list","timestamp":{"seconds":0,"nanos":33000000}}} +{"testStepFinished":{"testCaseStartedId":"56","testStepId":"49","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":34000000}}} +{"testCaseFinished":{"testCaseStartedId":"56","timestamp":{"seconds":0,"nanos":35000000},"willBeRetried":false}} +{"testRunFinished":{"testRunStartedId":"35","timestamp":{"seconds":0,"nanos":36000000},"success":true}} diff --git a/compatibility/attachments/attachments.ts b/compatibility/attachments/attachments.ts new file mode 100644 index 00000000..e0e9310a --- /dev/null +++ b/compatibility/attachments/attachments.ts @@ -0,0 +1,49 @@ +import { When } from '@cucumber/fake-cucumber' +import fs from 'node:fs' + +When( + 'the string {string} is attached as {string}', + async function (text: string, mediaType: string) { + await this.attach(text, mediaType) + } +) + +When('the string {string} is logged', async function (text: string) { + await this.log(text) +}) + +When('text with ANSI escapes is logged', async function () { + await this.log( + 'This displays a \x1b[31mr\x1b[0m\x1b[91ma\x1b[0m\x1b[33mi\x1b[0m\x1b[32mn\x1b[0m\x1b[34mb\x1b[0m\x1b[95mo\x1b[0m\x1b[35mw\x1b[0m' + ) +}) + +When( + 'the following string is attached as {string}:', + async function (mediaType: string, text: string) { + await this.attach(text, mediaType) + } +) + +When( + 'an array with {int} bytes is attached as {string}', + async function (size: number, mediaType: string) { + const data = [...Array(size).keys()] + const buffer = Buffer.from(data) + await this.attach(buffer, mediaType) + } +) + +When('a PDF document is attached and renamed', async function () { + await this.attach( + fs.createReadStream(import.meta.dirname + '/document.pdf'), + { + mediaType: 'application/pdf', + fileName: 'renamed.pdf', + } + ) +}) + +When('a link to {string} is attached', async function (uri: string) { + await this.link(uri) +}) diff --git a/compatibility/attachments/cucumber.jpeg b/compatibility/attachments/cucumber.jpeg new file mode 100644 index 00000000..e833d6c7 Binary files /dev/null and b/compatibility/attachments/cucumber.jpeg differ diff --git a/compatibility/attachments/cucumber.png b/compatibility/attachments/cucumber.png new file mode 100644 index 00000000..2760899a Binary files /dev/null and b/compatibility/attachments/cucumber.png differ diff --git a/compatibility/attachments/document.pdf b/compatibility/attachments/document.pdf new file mode 100644 index 00000000..4647f3c9 Binary files /dev/null and b/compatibility/attachments/document.pdf differ diff --git a/compatibility/backgrounds/backgrounds.cpp b/compatibility/backgrounds/backgrounds.cpp new file mode 100644 index 00000000..72fffe8f --- /dev/null +++ b/compatibility/backgrounds/backgrounds.cpp @@ -0,0 +1,17 @@ +#include "cucumber_cpp/CucumberCpp.hpp" +#include + +STEP(R"(an order for {string})", ([[maybe_unused]] const std::string& item)) +{ + // no-op +} + +STEP(R"(an action)") +{ + // no-op +} + +STEP(R"(an outcome)") +{ + // no-op +} diff --git a/compatibility/backgrounds/backgrounds.feature b/compatibility/backgrounds/backgrounds.feature new file mode 100644 index 00000000..a99e739b --- /dev/null +++ b/compatibility/backgrounds/backgrounds.feature @@ -0,0 +1,17 @@ +Feature: Backgrounds + Though not recommended, Backgrounds can be used to share context steps between Scenarios. The Background steps + are prepended to the steps in each Scenario when they are compiled to Pickles. Only one Background at the Feature + level is supported. + + Background: + Given an order for "eggs" + And an order for "milk" + And an order for "bread" + + Scenario: one scenario + When an action + Then an outcome + + Scenario: another scenario + When an action + Then an outcome \ No newline at end of file diff --git a/compatibility/backgrounds/backgrounds.ndjson b/compatibility/backgrounds/backgrounds.ndjson new file mode 100644 index 00000000..dd1446a6 --- /dev/null +++ b/compatibility/backgrounds/backgrounds.ndjson @@ -0,0 +1,36 @@ +{"meta":{"protocolVersion":"31.0.0","implementation":{"name":"fake-cucumber","version":"123.45.6"},"cpu":{"name":"arm64"},"os":{"name":"darwin","version":"24.5.0"},"runtime":{"name":"Node.js","version":"24.4.1"},"ci":{"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429","buildNumber":"154666429","git":{"revision":"99684bcacf01d95875834d87903dcb072306c9ad","remote":"https://github.com/cucumber-ltd/shouty.rb.git","branch":"main"}}}} +{"source":{"data":"Feature: Backgrounds\n Though not recommended, Backgrounds can be used to share context steps between Scenarios. The Background steps\n are prepended to the steps in each Scenario when they are compiled to Pickles. Only one Background at the Feature\n level is supported.\n\n Background:\n Given an order for \"eggs\"\n And an order for \"milk\"\n And an order for \"bread\"\n\n Scenario: one scenario\n When an action\n Then an outcome\n\n Scenario: another scenario\n When an action\n Then an outcome","uri":"samples/backgrounds/backgrounds.feature","mediaType":"text/x.cucumber.gherkin+plain"}} +{"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"Backgrounds","description":" Though not recommended, Backgrounds can be used to share context steps between Scenarios. The Background steps\n are prepended to the steps in each Scenario when they are compiled to Pickles. Only one Background at the Feature\n level is supported.","children":[{"background":{"id":"3","location":{"line":6,"column":3},"keyword":"Background","name":"","description":"","steps":[{"id":"0","location":{"line":7,"column":5},"keyword":"Given ","keywordType":"Context","text":"an order for \"eggs\""},{"id":"1","location":{"line":8,"column":5},"keyword":"And ","keywordType":"Conjunction","text":"an order for \"milk\""},{"id":"2","location":{"line":9,"column":5},"keyword":"And ","keywordType":"Conjunction","text":"an order for \"bread\""}]}},{"scenario":{"id":"6","tags":[],"location":{"line":11,"column":3},"keyword":"Scenario","name":"one scenario","description":"","steps":[{"id":"4","location":{"line":12,"column":5},"keyword":"When ","keywordType":"Action","text":"an action"},{"id":"5","location":{"line":13,"column":5},"keyword":"Then ","keywordType":"Outcome","text":"an outcome"}],"examples":[]}},{"scenario":{"id":"9","tags":[],"location":{"line":15,"column":3},"keyword":"Scenario","name":"another scenario","description":"","steps":[{"id":"7","location":{"line":16,"column":5},"keyword":"When ","keywordType":"Action","text":"an action"},{"id":"8","location":{"line":17,"column":5},"keyword":"Then ","keywordType":"Outcome","text":"an outcome"}],"examples":[]}}]},"comments":[],"uri":"samples/backgrounds/backgrounds.feature"}} +{"pickle":{"id":"15","uri":"samples/backgrounds/backgrounds.feature","location":{"line":11,"column":3},"astNodeIds":["6"],"tags":[],"name":"one scenario","language":"en","steps":[{"id":"10","text":"an order for \"eggs\"","type":"Context","astNodeIds":["0"]},{"id":"11","text":"an order for \"milk\"","type":"Context","astNodeIds":["1"]},{"id":"12","text":"an order for \"bread\"","type":"Context","astNodeIds":["2"]},{"id":"13","text":"an action","type":"Action","astNodeIds":["4"]},{"id":"14","text":"an outcome","type":"Outcome","astNodeIds":["5"]}]}} +{"pickle":{"id":"21","uri":"samples/backgrounds/backgrounds.feature","location":{"line":15,"column":3},"astNodeIds":["9"],"tags":[],"name":"another scenario","language":"en","steps":[{"id":"16","text":"an order for \"eggs\"","type":"Context","astNodeIds":["0"]},{"id":"17","text":"an order for \"milk\"","type":"Context","astNodeIds":["1"]},{"id":"18","text":"an order for \"bread\"","type":"Context","astNodeIds":["2"]},{"id":"19","text":"an action","type":"Action","astNodeIds":["7"]},{"id":"20","text":"an outcome","type":"Outcome","astNodeIds":["8"]}]}} +{"stepDefinition":{"id":"22","pattern":{"type":"CUCUMBER_EXPRESSION","source":"an order for {string}"},"sourceReference":{"uri":"samples/backgrounds/backgrounds.ts","location":{"line":3}}}} +{"stepDefinition":{"id":"23","pattern":{"type":"CUCUMBER_EXPRESSION","source":"an action"},"sourceReference":{"uri":"samples/backgrounds/backgrounds.ts","location":{"line":7}}}} +{"stepDefinition":{"id":"24","pattern":{"type":"CUCUMBER_EXPRESSION","source":"an outcome"},"sourceReference":{"uri":"samples/backgrounds/backgrounds.ts","location":{"line":11}}}} +{"testRunStarted":{"id":"25","timestamp":{"seconds":0,"nanos":0}}} +{"testCase":{"id":"26","pickleId":"15","testSteps":[{"id":"27","pickleStepId":"10","stepDefinitionIds":["22"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":13,"value":"\"eggs\"","children":[{"start":14,"value":"eggs","children":[{"children":[]}]},{"children":[{"children":[]}]}]},"parameterTypeName":"string"}]}]},{"id":"28","pickleStepId":"11","stepDefinitionIds":["22"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":13,"value":"\"milk\"","children":[{"start":14,"value":"milk","children":[{"children":[]}]},{"children":[{"children":[]}]}]},"parameterTypeName":"string"}]}]},{"id":"29","pickleStepId":"12","stepDefinitionIds":["22"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":13,"value":"\"bread\"","children":[{"start":14,"value":"bread","children":[{"children":[]}]},{"children":[{"children":[]}]}]},"parameterTypeName":"string"}]}]},{"id":"30","pickleStepId":"13","stepDefinitionIds":["23"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"31","pickleStepId":"14","stepDefinitionIds":["24"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}],"testRunStartedId":"25"}} +{"testCase":{"id":"32","pickleId":"21","testSteps":[{"id":"33","pickleStepId":"16","stepDefinitionIds":["22"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":13,"value":"\"eggs\"","children":[{"start":14,"value":"eggs","children":[{"children":[]}]},{"children":[{"children":[]}]}]},"parameterTypeName":"string"}]}]},{"id":"34","pickleStepId":"17","stepDefinitionIds":["22"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":13,"value":"\"milk\"","children":[{"start":14,"value":"milk","children":[{"children":[]}]},{"children":[{"children":[]}]}]},"parameterTypeName":"string"}]}]},{"id":"35","pickleStepId":"18","stepDefinitionIds":["22"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":13,"value":"\"bread\"","children":[{"start":14,"value":"bread","children":[{"children":[]}]},{"children":[{"children":[]}]}]},"parameterTypeName":"string"}]}]},{"id":"36","pickleStepId":"19","stepDefinitionIds":["23"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"37","pickleStepId":"20","stepDefinitionIds":["24"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}],"testRunStartedId":"25"}} +{"testCaseStarted":{"id":"38","testCaseId":"26","timestamp":{"seconds":0,"nanos":1000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"38","testStepId":"27","timestamp":{"seconds":0,"nanos":2000000}}} +{"testStepFinished":{"testCaseStartedId":"38","testStepId":"27","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":3000000}}} +{"testStepStarted":{"testCaseStartedId":"38","testStepId":"28","timestamp":{"seconds":0,"nanos":4000000}}} +{"testStepFinished":{"testCaseStartedId":"38","testStepId":"28","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":5000000}}} +{"testStepStarted":{"testCaseStartedId":"38","testStepId":"29","timestamp":{"seconds":0,"nanos":6000000}}} +{"testStepFinished":{"testCaseStartedId":"38","testStepId":"29","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":7000000}}} +{"testStepStarted":{"testCaseStartedId":"38","testStepId":"30","timestamp":{"seconds":0,"nanos":8000000}}} +{"testStepFinished":{"testCaseStartedId":"38","testStepId":"30","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":9000000}}} +{"testStepStarted":{"testCaseStartedId":"38","testStepId":"31","timestamp":{"seconds":0,"nanos":10000000}}} +{"testStepFinished":{"testCaseStartedId":"38","testStepId":"31","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":11000000}}} +{"testCaseFinished":{"testCaseStartedId":"38","timestamp":{"seconds":0,"nanos":12000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"39","testCaseId":"32","timestamp":{"seconds":0,"nanos":13000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"39","testStepId":"33","timestamp":{"seconds":0,"nanos":14000000}}} +{"testStepFinished":{"testCaseStartedId":"39","testStepId":"33","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":15000000}}} +{"testStepStarted":{"testCaseStartedId":"39","testStepId":"34","timestamp":{"seconds":0,"nanos":16000000}}} +{"testStepFinished":{"testCaseStartedId":"39","testStepId":"34","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":17000000}}} +{"testStepStarted":{"testCaseStartedId":"39","testStepId":"35","timestamp":{"seconds":0,"nanos":18000000}}} +{"testStepFinished":{"testCaseStartedId":"39","testStepId":"35","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":19000000}}} +{"testStepStarted":{"testCaseStartedId":"39","testStepId":"36","timestamp":{"seconds":0,"nanos":20000000}}} +{"testStepFinished":{"testCaseStartedId":"39","testStepId":"36","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":21000000}}} +{"testStepStarted":{"testCaseStartedId":"39","testStepId":"37","timestamp":{"seconds":0,"nanos":22000000}}} +{"testStepFinished":{"testCaseStartedId":"39","testStepId":"37","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":23000000}}} +{"testCaseFinished":{"testCaseStartedId":"39","timestamp":{"seconds":0,"nanos":24000000},"willBeRetried":false}} +{"testRunFinished":{"testRunStartedId":"25","timestamp":{"seconds":0,"nanos":25000000},"success":true}} diff --git a/compatibility/backgrounds/backgrounds.ts b/compatibility/backgrounds/backgrounds.ts new file mode 100644 index 00000000..cb475f6e --- /dev/null +++ b/compatibility/backgrounds/backgrounds.ts @@ -0,0 +1,13 @@ +import { Given, When, Then } from '@cucumber/fake-cucumber' + +Given('an order for {string}', () => { + // no-op +}) + +When('an action', () => { + // no-op +}) + +Then('an outcome', () => { + // no-op +}) diff --git a/compatibility/cdata/cdata.cpp b/compatibility/cdata/cdata.cpp new file mode 100644 index 00000000..5477f624 --- /dev/null +++ b/compatibility/cdata/cdata.cpp @@ -0,0 +1,5 @@ +#include "cucumber_cpp/CucumberCpp.hpp" +#include + +GIVEN(R"(I have {int} in my belly)", ([[maybe_unused]] std::int32_t number)) +{} diff --git a/compatibility/cdata/cdata.feature b/compatibility/cdata/cdata.feature new file mode 100644 index 00000000..ca75bff7 --- /dev/null +++ b/compatibility/cdata/cdata.feature @@ -0,0 +1,5 @@ +Feature: cdata + Cucumber xml formatters should be able to handle xml cdata elements. + + Scenario: cdata + Given I have 42 in my belly diff --git a/compatibility/cdata/cdata.ndjson b/compatibility/cdata/cdata.ndjson new file mode 100644 index 00000000..fe2f10f5 --- /dev/null +++ b/compatibility/cdata/cdata.ndjson @@ -0,0 +1,12 @@ +{"meta":{"protocolVersion":"31.0.0","implementation":{"name":"fake-cucumber","version":"123.45.6"},"cpu":{"name":"arm64"},"os":{"name":"darwin","version":"24.5.0"},"runtime":{"name":"Node.js","version":"24.4.1"},"ci":{"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429","buildNumber":"154666429","git":{"revision":"99684bcacf01d95875834d87903dcb072306c9ad","remote":"https://github.com/cucumber-ltd/shouty.rb.git","branch":"main"}}}} +{"source":{"data":"Feature: cdata\n Cucumber xml formatters should be able to handle xml cdata elements.\n\n Scenario: cdata\n Given I have 42 in my belly\n","uri":"samples/cdata/cdata.feature","mediaType":"text/x.cucumber.gherkin+plain"}} +{"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"cdata","description":" Cucumber xml formatters should be able to handle xml cdata elements.","children":[{"scenario":{"id":"1","tags":[],"location":{"line":4,"column":3},"keyword":"Scenario","name":"cdata","description":"","steps":[{"id":"0","location":{"line":5,"column":5},"keyword":"Given ","keywordType":"Context","text":"I have 42 in my belly"}],"examples":[]}}]},"comments":[],"uri":"samples/cdata/cdata.feature"}} +{"pickle":{"id":"3","uri":"samples/cdata/cdata.feature","location":{"line":4,"column":3},"astNodeIds":["1"],"tags":[],"name":"cdata","language":"en","steps":[{"id":"2","text":"I have 42 in my belly","type":"Context","astNodeIds":["0"]}]}} +{"stepDefinition":{"id":"4","pattern":{"type":"CUCUMBER_EXPRESSION","source":"I have {int} in my belly"},"sourceReference":{"uri":"samples/cdata/cdata.ts","location":{"line":3}}}} +{"testRunStarted":{"id":"5","timestamp":{"seconds":0,"nanos":0}}} +{"testCase":{"id":"6","pickleId":"3","testSteps":[{"id":"7","pickleStepId":"2","stepDefinitionIds":["4"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":7,"value":"42","children":[]},"parameterTypeName":"int"}]}]}],"testRunStartedId":"5"}} +{"testCaseStarted":{"id":"8","testCaseId":"6","timestamp":{"seconds":0,"nanos":1000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"8","testStepId":"7","timestamp":{"seconds":0,"nanos":2000000}}} +{"testStepFinished":{"testCaseStartedId":"8","testStepId":"7","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":3000000}}} +{"testCaseFinished":{"testCaseStartedId":"8","timestamp":{"seconds":0,"nanos":4000000},"willBeRetried":false}} +{"testRunFinished":{"testRunStartedId":"5","timestamp":{"seconds":0,"nanos":5000000},"success":true}} diff --git a/compatibility/cdata/cdata.ts b/compatibility/cdata/cdata.ts new file mode 100644 index 00000000..aebf8e5a --- /dev/null +++ b/compatibility/cdata/cdata.ts @@ -0,0 +1,5 @@ +import { Given } from '@cucumber/fake-cucumber' + +Given('I have {int} in my belly', function (cukeCount: number) { + // no-op +}) diff --git a/compatibility/compatibility.cpp b/compatibility/compatibility.cpp new file mode 100644 index 00000000..c035a95b --- /dev/null +++ b/compatibility/compatibility.cpp @@ -0,0 +1,291 @@ +#include "cucumber/messages/envelope.hpp" +#include "cucumber_cpp/CucumberCpp.hpp" +#include "cucumber_cpp/library/api/Formatters.hpp" +#include "cucumber_cpp/library/api/RunCucumber.hpp" +#include "cucumber_cpp/library/cucumber_expression/ParameterRegistry.hpp" +#include "cucumber_cpp/library/support/SupportCodeLibrary.hpp" +#include "cucumber_cpp/library/support/Types.hpp" +#include "cucumber_cpp/library/tag_expression/Parser.hpp" +#include "cucumber_cpp/library/util/Broadcaster.hpp" +#include "cucumber_cpp/library/util/Duration.hpp" +#include "cucumber_cpp/library/util/Timestamp.hpp" +#include "nlohmann/json.hpp" +#include "nlohmann/json_fwd.hpp" +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef KIT_FOLDER +#error KIT_FOLDER is not defined +#define KIT_FOLDER "" +#endif + +#ifndef KIT_NDJSON_FILE +#error KIT_NDJSON_FILE is not defined +#define KIT_NDJSON_FILE "" +#endif + +namespace compatibility +{ + namespace + { + struct Devkit + { + std::set> paths; + std::string tagExpression; + std::size_t retry; + std::filesystem::path ndjsonFile; + }; + + Devkit LoadDevkit() + { + return { + .paths = { KIT_FOLDER }, + .tagExpression = "", + .retry = 0, + .ndjsonFile = KIT_NDJSON_FILE + }; + } + + void RemoveIncompatibilities(nlohmann::json& json) + { + for (auto jsonIter = json.begin(); jsonIter != json.end();) + { + const auto& key = jsonIter.key(); + auto& value = jsonIter.value(); + + if (key == "exception") + jsonIter = json.erase(jsonIter); + else if (key == "message") + jsonIter = json.erase(jsonIter); + else if (key == "line") + jsonIter = json.erase(jsonIter); + else if (key == "snippets") + jsonIter = json.erase(jsonIter); + else if (value.is_object()) + { + RemoveIncompatibilities(value); + ++jsonIter; + } + else if (value.is_array()) + { + for (auto valueIter = value.begin(); valueIter != value.end();) + { + if (valueIter->is_object()) + RemoveIncompatibilities(*valueIter); + + ++valueIter; + } + + ++jsonIter; + } + else if (key == "data") + { + json[key] = std::regex_replace(json[key].get(), std::regex(R"(\r\n)"), "\n"); + ++jsonIter; + } + else if (key == "uri") + { + auto uri = value.get(); + + uri = std::regex_replace(uri, std::regex(R"(samples\/[^\/]+)"), KIT_FOLDER); + uri = std::regex_replace(uri, std::regex(R"(\.ts$)"), ".cpp"); + + json[key] = std::filesystem::canonical(uri).string(); + + ++jsonIter; + } + else + ++jsonIter; + } + } + + struct BroadcastListener + { + BroadcastListener(std::filesystem::path ndjsonin, std::filesystem::path expectedndjson, std::filesystem::path ndout, cucumber_cpp::library::util::Broadcaster& broadcaster) + : listener(broadcaster, [this](const cucumber::messages::envelope& envelope) + { + OnEvent(envelope); + }) + , ndjsonin{ std::move(ndjsonin) } + , expectedndjson{ std::move(expectedndjson) } + , actualndjson(std::move(ndout)) + { + while (!ifs.eof()) + { + std::string line; + std::getline(ifs, line); + + if (line.empty()) + continue; + + auto json = nlohmann::json::parse(line); + + if (json.contains("meta")) + continue; + + expectedEnvelopes.emplace_back(std::move(json)); + } + } + + void OnEvent(const cucumber::messages::envelope& envelope) + { + nlohmann::json actualJson{}; + to_json(actualJson, envelope); + + actualEnvelopes.emplace_back(std::move(actualJson)); + } + + void CompareEnvelopes() + { + EXPECT_THAT(actualEnvelopes.size(), testing::Eq(expectedEnvelopes.size())); + + for (auto& json : actualEnvelopes) + { + RemoveIncompatibilities(json); + actualOfs << json.dump() << "\n"; + } + + for (auto& json : expectedEnvelopes) + { + RemoveIncompatibilities(json); + expectedOfs << json.dump() << "\n"; + } + + while (!actualEnvelopes.empty() && !expectedEnvelopes.empty()) + { + auto actualJson = actualEnvelopes.front(); + actualEnvelopes.pop_front(); + + auto expectedJson = expectedEnvelopes.front(); + expectedEnvelopes.pop_front(); + + EXPECT_THAT(actualJson, testing::Eq(expectedJson)); + } + } + + private: + cucumber_cpp::library::util::Listener listener; + std::filesystem::path ndjsonin; + std::ifstream ifs{ ndjsonin }; + + std::filesystem::path expectedndjson; + std::ofstream expectedOfs{ expectedndjson }; + + std::filesystem::path actualndjson; + std::ofstream actualOfs{ actualndjson }; + + std::list expectedEnvelopes; + std::list actualEnvelopes; + }; + + bool IsFeatureFile(const std::filesystem::directory_entry& entry) + { + return std::filesystem::is_regular_file(entry) && entry.path().has_extension() && entry.path().extension() == ".feature"; + } + + std::set> GetFeatureFiles(std::set> paths) + { + std::set> files; + + for (const auto feature : paths) + if (std::filesystem::is_directory(feature)) + for (const auto& entry : std::filesystem::directory_iterator{ feature } | std::views::filter(IsFeatureFile)) + { + std::cout << " found feature file: " << entry.path() << "\n"; + files.insert(entry.path()); + } + else + files.insert(feature); + + return files; + } + + struct StopwatchIncremental : cucumber_cpp::library::util::Stopwatch + { + virtual ~StopwatchIncremental() = default; + + std::chrono::high_resolution_clock::time_point Start() override + { + return {}; + } + + std::chrono::nanoseconds Duration([[maybe_unused]] std::chrono::high_resolution_clock::time_point timePoint) override + { + return current; + } + + std::chrono::nanoseconds current{ std::chrono::milliseconds{ 1 } }; + }; + + struct TimestampGeneratorIncremental : cucumber_cpp::library::util::TimestampGenerator + { + virtual ~TimestampGeneratorIncremental() = default; + + std::chrono::milliseconds Now() override + { + return current++; + } + + std::chrono::milliseconds current{ 0 }; + }; + + void RunDevkit(Devkit devkit) + { + devkit.paths = GetFeatureFiles(devkit.paths); + const auto isReversed = std::string{ KIT_STRING }.ends_with("-reversed"); + + cucumber_cpp::library::support::RunOptions runOptions{ + .sources = { + .paths = devkit.paths, + .tagExpression = cucumber_cpp::library::tag_expression::Parse(devkit.tagExpression), + .ordering = isReversed ? cucumber_cpp::library::support::RunOptions::Ordering::reverse : cucumber_cpp::library::support::RunOptions::Ordering::defined, + }, + .runtime = { + .retry = std::string{ KIT_STRING }.starts_with("retry") ? 2u : 0u, + .strict = true, + .retryTagExpression = cucumber_cpp::library::tag_expression::Parse(""), + }, + }; + + cucumber_cpp::library::cucumber_expression::ParameterRegistry parameterRegistry{ cucumber_cpp::library::support::DefinitionRegistration::Instance().GetRegisteredParameters() }; + + auto contextStorageFactory{ std::make_shared() }; + auto programContext{ std::make_unique(contextStorageFactory) }; + + cucumber_cpp::library::util::Broadcaster broadcaster; + + BroadcastListener broadcastListener{ devkit.ndjsonFile, devkit.ndjsonFile.parent_path() / "expected.ndjson", devkit.ndjsonFile.parent_path() / "actual.ndjson", broadcaster }; + + cucumber_cpp::library::api::Formatters formatters; + cucumber_cpp::library::api::RunCucumber(runOptions, parameterRegistry, *programContext, broadcaster, formatters, { "junit", "message", "pretty", "summary", "usage" }, {}); + + broadcastListener.CompareEnvelopes(); + } + } +} + +TEST(CompatibilityTest, KIT_NAME) +{ +#ifdef SKIP_TEST + GTEST_SKIP(); +#endif + + compatibility::StopwatchIncremental stopwatch; + compatibility::TimestampGeneratorIncremental timestampGenerator; + compatibility::RunDevkit(compatibility::LoadDevkit()); +} diff --git a/compatibility/data-tables/data-tables.cpp b/compatibility/data-tables/data-tables.cpp new file mode 100644 index 00000000..2222bb90 --- /dev/null +++ b/compatibility/data-tables/data-tables.cpp @@ -0,0 +1,39 @@ +#include "cucumber/messages/pickle_table.hpp" +#include "cucumber_cpp/CucumberCpp.hpp" +#include +#include +#include +#include + +STEP(R"(the following table is transposed:)") +{ + auto transposedTable = context.Emplace(); + transposedTable->rows.reserve(this->dataTable->rows[0].cells.size()); + for (std::size_t colIdx = 0; colIdx < this->dataTable->rows[0].cells.size(); ++colIdx) + transposedTable->rows.emplace_back().cells.resize(this->dataTable->rows.size()); + + for (std::size_t rowIdx = 0; rowIdx < this->dataTable->rows.size(); ++rowIdx) + for (std::size_t colIdx = 0; colIdx < this->dataTable->rows[rowIdx].cells.size(); ++colIdx) + transposedTable->rows[colIdx].cells[rowIdx] = this->dataTable->rows[rowIdx].cells[colIdx]; +} + +STEP(R"(it should be:)") +{ + const auto& actualTable = context.Get(); + const auto& expectedTalbe = dataTable; + + const auto rows = actualTable.rows.size(); + ASSERT_THAT(rows, testing::Eq(expectedTalbe->rows.size())); + + for (auto rowIdx = 0; rowIdx < rows; ++rowIdx) + { + const auto columns = expectedTalbe->rows[rowIdx].cells.size(); + ASSERT_THAT(columns, testing::Eq(actualTable.rows[rowIdx].cells.size())); + for (auto colIdx = 0; colIdx < columns; ++colIdx) + { + const auto& expectedCell = expectedTalbe->rows[rowIdx].cells[colIdx]; + const auto& actualCell = actualTable.rows[rowIdx].cells[colIdx]; + EXPECT_THAT(expectedCell.value, testing::StrEq(actualCell.value)) << "at row " << rowIdx << " column " << colIdx; + } + } +} diff --git a/compatibility/data-tables/data-tables.feature b/compatibility/data-tables/data-tables.feature new file mode 100644 index 00000000..2822419e --- /dev/null +++ b/compatibility/data-tables/data-tables.feature @@ -0,0 +1,13 @@ +Feature: Data Tables + Data Tables can be placed underneath a step and will be passed as the last + argument to the step definition. + + They can be used to represent richer data structures, and can be transformed to other data-types. + + Scenario: transposed table + When the following table is transposed: + | a | b | + | 1 | 2 | + Then it should be: + | a | 1 | + | b | 2 | diff --git a/compatibility/data-tables/data-tables.ndjson b/compatibility/data-tables/data-tables.ndjson new file mode 100644 index 00000000..de951db0 --- /dev/null +++ b/compatibility/data-tables/data-tables.ndjson @@ -0,0 +1,15 @@ +{"meta":{"protocolVersion":"31.0.0","implementation":{"name":"fake-cucumber","version":"123.45.6"},"cpu":{"name":"arm64"},"os":{"name":"darwin","version":"24.5.0"},"runtime":{"name":"Node.js","version":"24.4.1"},"ci":{"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429","buildNumber":"154666429","git":{"revision":"99684bcacf01d95875834d87903dcb072306c9ad","remote":"https://github.com/cucumber-ltd/shouty.rb.git","branch":"main"}}}} +{"source":{"data":"Feature: Data Tables\n Data Tables can be placed underneath a step and will be passed as the last\n argument to the step definition.\n\n They can be used to represent richer data structures, and can be transformed to other data-types.\n\n Scenario: transposed table\n When the following table is transposed:\n | a | b |\n | 1 | 2 |\n Then it should be:\n | a | 1 |\n | b | 2 |\n","uri":"samples/data-tables/data-tables.feature","mediaType":"text/x.cucumber.gherkin+plain"}} +{"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"Data Tables","description":" Data Tables can be placed underneath a step and will be passed as the last\n argument to the step definition.\n\n They can be used to represent richer data structures, and can be transformed to other data-types.","children":[{"scenario":{"id":"6","tags":[],"location":{"line":7,"column":3},"keyword":"Scenario","name":"transposed table","description":"","steps":[{"id":"2","location":{"line":8,"column":5},"keyword":"When ","keywordType":"Action","text":"the following table is transposed:","dataTable":{"location":{"line":9,"column":7},"rows":[{"id":"0","location":{"line":9,"column":7},"cells":[{"location":{"line":9,"column":9},"value":"a"},{"location":{"line":9,"column":13},"value":"b"}]},{"id":"1","location":{"line":10,"column":7},"cells":[{"location":{"line":10,"column":9},"value":"1"},{"location":{"line":10,"column":13},"value":"2"}]}]}},{"id":"5","location":{"line":11,"column":5},"keyword":"Then ","keywordType":"Outcome","text":"it should be:","dataTable":{"location":{"line":12,"column":7},"rows":[{"id":"3","location":{"line":12,"column":7},"cells":[{"location":{"line":12,"column":9},"value":"a"},{"location":{"line":12,"column":13},"value":"1"}]},{"id":"4","location":{"line":13,"column":7},"cells":[{"location":{"line":13,"column":9},"value":"b"},{"location":{"line":13,"column":13},"value":"2"}]}]}}],"examples":[]}}]},"comments":[],"uri":"samples/data-tables/data-tables.feature"}} +{"pickle":{"id":"9","uri":"samples/data-tables/data-tables.feature","location":{"line":7,"column":3},"astNodeIds":["6"],"tags":[],"name":"transposed table","language":"en","steps":[{"id":"7","text":"the following table is transposed:","type":"Action","argument":{"dataTable":{"rows":[{"cells":[{"value":"a"},{"value":"b"}]},{"cells":[{"value":"1"},{"value":"2"}]}]}},"astNodeIds":["2"]},{"id":"8","text":"it should be:","type":"Outcome","argument":{"dataTable":{"rows":[{"cells":[{"value":"a"},{"value":"1"}]},{"cells":[{"value":"b"},{"value":"2"}]}]}},"astNodeIds":["5"]}]}} +{"stepDefinition":{"id":"10","pattern":{"type":"CUCUMBER_EXPRESSION","source":"the following table is transposed:"},"sourceReference":{"uri":"samples/data-tables/data-tables.ts","location":{"line":4}}}} +{"stepDefinition":{"id":"11","pattern":{"type":"CUCUMBER_EXPRESSION","source":"it should be:"},"sourceReference":{"uri":"samples/data-tables/data-tables.ts","location":{"line":8}}}} +{"testRunStarted":{"id":"12","timestamp":{"seconds":0,"nanos":0}}} +{"testCase":{"id":"13","pickleId":"9","testSteps":[{"id":"14","pickleStepId":"7","stepDefinitionIds":["10"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"15","pickleStepId":"8","stepDefinitionIds":["11"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}],"testRunStartedId":"12"}} +{"testCaseStarted":{"id":"16","testCaseId":"13","timestamp":{"seconds":0,"nanos":1000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"16","testStepId":"14","timestamp":{"seconds":0,"nanos":2000000}}} +{"testStepFinished":{"testCaseStartedId":"16","testStepId":"14","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":3000000}}} +{"testStepStarted":{"testCaseStartedId":"16","testStepId":"15","timestamp":{"seconds":0,"nanos":4000000}}} +{"testStepFinished":{"testCaseStartedId":"16","testStepId":"15","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":5000000}}} +{"testCaseFinished":{"testCaseStartedId":"16","timestamp":{"seconds":0,"nanos":6000000},"willBeRetried":false}} +{"testRunFinished":{"testRunStartedId":"12","timestamp":{"seconds":0,"nanos":7000000},"success":true}} diff --git a/compatibility/data-tables/data-tables.ts b/compatibility/data-tables/data-tables.ts new file mode 100644 index 00000000..e778898a --- /dev/null +++ b/compatibility/data-tables/data-tables.ts @@ -0,0 +1,10 @@ +import assert from 'node:assert' +import { DataTable, When, Then } from '@cucumber/fake-cucumber' + +When('the following table is transposed:', function (table: DataTable) { + this.transposed = table.transpose() +}) + +Then('it should be:', function (expected: DataTable) { + assert.deepStrictEqual(this.transposed.raw(), expected.raw()) +}) diff --git a/compatibility/doc-strings/doc-strings.cpp b/compatibility/doc-strings/doc-strings.cpp new file mode 100644 index 00000000..b30c03ff --- /dev/null +++ b/compatibility/doc-strings/doc-strings.cpp @@ -0,0 +1,9 @@ +#include "cucumber_cpp/CucumberCpp.hpp" +#include "gmock/gmock.h" +#include + +STEP(R"(a doc string:)") +{ + ASSERT_THAT(docString, testing::IsTrue()); + ASSERT_THAT(docString->content, testing::Not(testing::IsEmpty())); +} diff --git a/compatibility/doc-strings/doc-strings.feature b/compatibility/doc-strings/doc-strings.feature new file mode 100644 index 00000000..c3917189 --- /dev/null +++ b/compatibility/doc-strings/doc-strings.feature @@ -0,0 +1,31 @@ +Feature: Doc strings + Doc strings are a way to supply long, sometimes multi-line, text to a step. They are passed as the last argument + to the step definition. + + Scenario: a doc string with standard delimiter + Three double quotes above and below are the standard delimiter for doc strings. + + Given a doc string: + """ + Here is some content + And some more on another line + """ + + Scenario: a doc string with backticks delimiter + Backticks can also be used, like Markdown, but are less widely supported by editors. + + Given a doc string: + ``` + Here is some content + And some more on another line + ``` + + Scenario: a doc string with media type + The media type can be optionally specified too, following the opening delimiter. + + Given a doc string: + """application/json + { + "foo": "bar" + } + """ \ No newline at end of file diff --git a/compatibility/doc-strings/doc-strings.ndjson b/compatibility/doc-strings/doc-strings.ndjson new file mode 100644 index 00000000..95b8c7b4 --- /dev/null +++ b/compatibility/doc-strings/doc-strings.ndjson @@ -0,0 +1,24 @@ +{"meta":{"protocolVersion":"31.0.0","implementation":{"name":"fake-cucumber","version":"123.45.6"},"cpu":{"name":"arm64"},"os":{"name":"darwin","version":"24.5.0"},"runtime":{"name":"Node.js","version":"24.4.1"},"ci":{"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429","buildNumber":"154666429","git":{"revision":"99684bcacf01d95875834d87903dcb072306c9ad","remote":"https://github.com/cucumber-ltd/shouty.rb.git","branch":"main"}}}} +{"source":{"data":"Feature: Doc strings\n Doc strings are a way to supply long, sometimes multi-line, text to a step. They are passed as the last argument\n to the step definition.\n\n Scenario: a doc string with standard delimiter\n Three double quotes above and below are the standard delimiter for doc strings.\n\n Given a doc string:\n \"\"\"\n Here is some content\n And some more on another line\n \"\"\"\n\n Scenario: a doc string with backticks delimiter\n Backticks can also be used, like Markdown, but are less widely supported by editors.\n\n Given a doc string:\n ```\n Here is some content\n And some more on another line\n ```\n\n Scenario: a doc string with media type\n The media type can be optionally specified too, following the opening delimiter.\n\n Given a doc string:\n \"\"\"application/json\n {\n \"foo\": \"bar\"\n }\n \"\"\"","uri":"samples/doc-strings/doc-strings.feature","mediaType":"text/x.cucumber.gherkin+plain"}} +{"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"Doc strings","description":" Doc strings are a way to supply long, sometimes multi-line, text to a step. They are passed as the last argument\n to the step definition.","children":[{"scenario":{"id":"1","tags":[],"location":{"line":5,"column":3},"keyword":"Scenario","name":"a doc string with standard delimiter","description":" Three double quotes above and below are the standard delimiter for doc strings.","steps":[{"id":"0","location":{"line":8,"column":5},"keyword":"Given ","keywordType":"Context","text":"a doc string:","docString":{"location":{"line":9,"column":5},"content":"Here is some content\nAnd some more on another line","delimiter":"\"\"\""}}],"examples":[]}},{"scenario":{"id":"3","tags":[],"location":{"line":14,"column":3},"keyword":"Scenario","name":"a doc string with backticks delimiter","description":" Backticks can also be used, like Markdown, but are less widely supported by editors.","steps":[{"id":"2","location":{"line":17,"column":5},"keyword":"Given ","keywordType":"Context","text":"a doc string:","docString":{"location":{"line":18,"column":5},"content":"Here is some content\nAnd some more on another line","delimiter":"```"}}],"examples":[]}},{"scenario":{"id":"5","tags":[],"location":{"line":23,"column":3},"keyword":"Scenario","name":"a doc string with media type","description":" The media type can be optionally specified too, following the opening delimiter.","steps":[{"id":"4","location":{"line":26,"column":5},"keyword":"Given ","keywordType":"Context","text":"a doc string:","docString":{"location":{"line":27,"column":5},"content":"{\n \"foo\": \"bar\"\n}","delimiter":"\"\"\"","mediaType":"application/json"}}],"examples":[]}}]},"comments":[],"uri":"samples/doc-strings/doc-strings.feature"}} +{"pickle":{"id":"7","uri":"samples/doc-strings/doc-strings.feature","location":{"line":5,"column":3},"astNodeIds":["1"],"tags":[],"name":"a doc string with standard delimiter","language":"en","steps":[{"id":"6","text":"a doc string:","type":"Context","argument":{"docString":{"content":"Here is some content\nAnd some more on another line"}},"astNodeIds":["0"]}]}} +{"pickle":{"id":"9","uri":"samples/doc-strings/doc-strings.feature","location":{"line":14,"column":3},"astNodeIds":["3"],"tags":[],"name":"a doc string with backticks delimiter","language":"en","steps":[{"id":"8","text":"a doc string:","type":"Context","argument":{"docString":{"content":"Here is some content\nAnd some more on another line"}},"astNodeIds":["2"]}]}} +{"pickle":{"id":"11","uri":"samples/doc-strings/doc-strings.feature","location":{"line":23,"column":3},"astNodeIds":["5"],"tags":[],"name":"a doc string with media type","language":"en","steps":[{"id":"10","text":"a doc string:","type":"Context","argument":{"docString":{"content":"{\n \"foo\": \"bar\"\n}","mediaType":"application/json"}},"astNodeIds":["4"]}]}} +{"stepDefinition":{"id":"12","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a doc string:"},"sourceReference":{"uri":"samples/doc-strings/doc-strings.ts","location":{"line":3}}}} +{"testRunStarted":{"id":"13","timestamp":{"seconds":0,"nanos":0}}} +{"testCase":{"id":"14","pickleId":"7","testSteps":[{"id":"15","pickleStepId":"6","stepDefinitionIds":["12"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}],"testRunStartedId":"13"}} +{"testCase":{"id":"16","pickleId":"9","testSteps":[{"id":"17","pickleStepId":"8","stepDefinitionIds":["12"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}],"testRunStartedId":"13"}} +{"testCase":{"id":"18","pickleId":"11","testSteps":[{"id":"19","pickleStepId":"10","stepDefinitionIds":["12"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}],"testRunStartedId":"13"}} +{"testCaseStarted":{"id":"20","testCaseId":"14","timestamp":{"seconds":0,"nanos":1000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"20","testStepId":"15","timestamp":{"seconds":0,"nanos":2000000}}} +{"testStepFinished":{"testCaseStartedId":"20","testStepId":"15","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":3000000}}} +{"testCaseFinished":{"testCaseStartedId":"20","timestamp":{"seconds":0,"nanos":4000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"21","testCaseId":"16","timestamp":{"seconds":0,"nanos":5000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"21","testStepId":"17","timestamp":{"seconds":0,"nanos":6000000}}} +{"testStepFinished":{"testCaseStartedId":"21","testStepId":"17","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":7000000}}} +{"testCaseFinished":{"testCaseStartedId":"21","timestamp":{"seconds":0,"nanos":8000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"22","testCaseId":"18","timestamp":{"seconds":0,"nanos":9000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"22","testStepId":"19","timestamp":{"seconds":0,"nanos":10000000}}} +{"testStepFinished":{"testCaseStartedId":"22","testStepId":"19","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":11000000}}} +{"testCaseFinished":{"testCaseStartedId":"22","timestamp":{"seconds":0,"nanos":12000000},"willBeRetried":false}} +{"testRunFinished":{"testRunStartedId":"13","timestamp":{"seconds":0,"nanos":13000000},"success":true}} diff --git a/compatibility/doc-strings/doc-strings.ts b/compatibility/doc-strings/doc-strings.ts new file mode 100644 index 00000000..ba72d417 --- /dev/null +++ b/compatibility/doc-strings/doc-strings.ts @@ -0,0 +1,5 @@ +import { Given } from '@cucumber/fake-cucumber' + +Given('a doc string:', (docString: string) => { + // no-op +}) diff --git a/compatibility/empty/empty.cpp b/compatibility/empty/empty.cpp new file mode 100644 index 00000000..e69de29b diff --git a/compatibility/empty/empty.feature b/compatibility/empty/empty.feature new file mode 100644 index 00000000..e9767eff --- /dev/null +++ b/compatibility/empty/empty.feature @@ -0,0 +1,7 @@ +Feature: Empty Scenarios + Sometimes we want to quickly jot down a new scenario without specifying any actual steps + for what should be executed. + + In this instance we want to stipulate what should / shouldn't run and what the output is. + + Scenario: Blank Scenario diff --git a/compatibility/empty/empty.ndjson b/compatibility/empty/empty.ndjson new file mode 100644 index 00000000..f90ca578 --- /dev/null +++ b/compatibility/empty/empty.ndjson @@ -0,0 +1,9 @@ +{"meta":{"protocolVersion":"31.0.0","implementation":{"name":"fake-cucumber","version":"123.45.6"},"cpu":{"name":"arm64"},"os":{"name":"darwin","version":"24.5.0"},"runtime":{"name":"Node.js","version":"24.4.1"},"ci":{"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429","buildNumber":"154666429","git":{"revision":"99684bcacf01d95875834d87903dcb072306c9ad","remote":"https://github.com/cucumber-ltd/shouty.rb.git","branch":"main"}}}} +{"source":{"data":"Feature: Empty Scenarios\n Sometimes we want to quickly jot down a new scenario without specifying any actual steps\n for what should be executed.\n\n In this instance we want to stipulate what should / shouldn't run and what the output is.\n\n Scenario: Blank Scenario\n","uri":"samples/empty/empty.feature","mediaType":"text/x.cucumber.gherkin+plain"}} +{"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"Empty Scenarios","description":" Sometimes we want to quickly jot down a new scenario without specifying any actual steps\n for what should be executed.\n\n In this instance we want to stipulate what should / shouldn't run and what the output is.","children":[{"scenario":{"id":"0","tags":[],"location":{"line":7,"column":3},"keyword":"Scenario","name":"Blank Scenario","description":"","steps":[],"examples":[]}}]},"comments":[],"uri":"samples/empty/empty.feature"}} +{"pickle":{"id":"1","uri":"samples/empty/empty.feature","location":{"line":7,"column":3},"astNodeIds":["0"],"tags":[],"name":"Blank Scenario","language":"en","steps":[]}} +{"testRunStarted":{"id":"2","timestamp":{"seconds":0,"nanos":0}}} +{"testCase":{"id":"3","pickleId":"1","testSteps":[],"testRunStartedId":"2"}} +{"testCaseStarted":{"id":"4","testCaseId":"3","timestamp":{"seconds":0,"nanos":1000000},"attempt":0}} +{"testCaseFinished":{"testCaseStartedId":"4","timestamp":{"seconds":0,"nanos":2000000},"willBeRetried":false}} +{"testRunFinished":{"testRunStartedId":"2","timestamp":{"seconds":0,"nanos":3000000},"success":true}} diff --git a/compatibility/examples-tables-attachment/cucumber.jpeg b/compatibility/examples-tables-attachment/cucumber.jpeg new file mode 100644 index 00000000..e833d6c7 Binary files /dev/null and b/compatibility/examples-tables-attachment/cucumber.jpeg differ diff --git a/compatibility/examples-tables-attachment/cucumber.png b/compatibility/examples-tables-attachment/cucumber.png new file mode 100644 index 00000000..2760899a Binary files /dev/null and b/compatibility/examples-tables-attachment/cucumber.png differ diff --git a/compatibility/examples-tables-attachment/examples-tables-attachment.cpp b/compatibility/examples-tables-attachment/examples-tables-attachment.cpp new file mode 100644 index 00000000..1588ef0a --- /dev/null +++ b/compatibility/examples-tables-attachment/examples-tables-attachment.cpp @@ -0,0 +1,24 @@ +#include "cucumber_cpp/CucumberCpp.hpp" +#include +#include +#include +#include +#include +#include + +namespace +{ + const std::filesystem::path currentCompileDir = std::filesystem::path{ std::source_location::current().file_name() }.parent_path(); +} + +WHEN(R"(a JPEG image is attached)") +{ + std::ifstream jpegFile{ currentCompileDir / "cucumber.jpeg", std::ios::binary }; + Attach(jpegFile, "image/jpeg"); +} + +WHEN(R"(a PNG image is attached)") +{ + std::ifstream pngFile{ currentCompileDir / "cucumber.png", std::ios::binary }; + Attach(pngFile, "image/png"); +} diff --git a/compatibility/examples-tables-attachment/examples-tables-attachment.feature b/compatibility/examples-tables-attachment/examples-tables-attachment.feature new file mode 100644 index 00000000..b8e6466a --- /dev/null +++ b/compatibility/examples-tables-attachment/examples-tables-attachment.feature @@ -0,0 +1,10 @@ +Feature: Examples Tables - With attachments + It is sometimes useful to take a screenshot while a scenario runs or capture some logs. + + Scenario Outline: Attaching images in an examples table + When a image is attached + + Examples: + | type | + | JPEG | + | PNG | diff --git a/compatibility/examples-tables-attachment/examples-tables-attachment.ndjson b/compatibility/examples-tables-attachment/examples-tables-attachment.ndjson new file mode 100644 index 00000000..1de68a68 --- /dev/null +++ b/compatibility/examples-tables-attachment/examples-tables-attachment.ndjson @@ -0,0 +1,21 @@ +{"meta":{"protocolVersion":"31.0.0","implementation":{"name":"fake-cucumber","version":"123.45.6"},"cpu":{"name":"arm64"},"os":{"name":"darwin","version":"24.5.0"},"runtime":{"name":"Node.js","version":"24.4.1"},"ci":{"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429","buildNumber":"154666429","git":{"revision":"99684bcacf01d95875834d87903dcb072306c9ad","remote":"https://github.com/cucumber-ltd/shouty.rb.git","branch":"main"}}}} +{"source":{"data":"Feature: Examples Tables - With attachments\n It is sometimes useful to take a screenshot while a scenario runs or capture some logs.\n\n Scenario Outline: Attaching images in an examples table\n When a image is attached\n\n Examples:\n | type |\n | JPEG |\n | PNG |\n","uri":"samples/examples-tables-attachment/examples-tables-attachment.feature","mediaType":"text/x.cucumber.gherkin+plain"}} +{"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"Examples Tables - With attachments","description":" It is sometimes useful to take a screenshot while a scenario runs or capture some logs.","children":[{"scenario":{"id":"5","tags":[],"location":{"line":4,"column":3},"keyword":"Scenario Outline","name":"Attaching images in an examples table","description":"","steps":[{"id":"0","location":{"line":5,"column":5},"keyword":"When ","keywordType":"Action","text":"a image is attached"}],"examples":[{"id":"4","tags":[],"location":{"line":7,"column":5},"keyword":"Examples","name":"","description":"","tableHeader":{"id":"1","location":{"line":8,"column":7},"cells":[{"location":{"line":8,"column":9},"value":"type"}]},"tableBody":[{"id":"2","location":{"line":9,"column":7},"cells":[{"location":{"line":9,"column":9},"value":"JPEG"}]},{"id":"3","location":{"line":10,"column":7},"cells":[{"location":{"line":10,"column":9},"value":"PNG"}]}]}]}}]},"comments":[],"uri":"samples/examples-tables-attachment/examples-tables-attachment.feature"}} +{"pickle":{"id":"7","uri":"samples/examples-tables-attachment/examples-tables-attachment.feature","location":{"line":9,"column":7},"astNodeIds":["5","2"],"name":"Attaching images in an examples table","language":"en","steps":[{"id":"6","text":"a JPEG image is attached","type":"Action","astNodeIds":["0","2"]}],"tags":[]}} +{"pickle":{"id":"9","uri":"samples/examples-tables-attachment/examples-tables-attachment.feature","location":{"line":10,"column":7},"astNodeIds":["5","3"],"name":"Attaching images in an examples table","language":"en","steps":[{"id":"8","text":"a PNG image is attached","type":"Action","astNodeIds":["0","3"]}],"tags":[]}} +{"stepDefinition":{"id":"10","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a JPEG image is attached"},"sourceReference":{"uri":"samples/examples-tables-attachment/examples-tables-attachment.ts","location":{"line":4}}}} +{"stepDefinition":{"id":"11","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a PNG image is attached"},"sourceReference":{"uri":"samples/examples-tables-attachment/examples-tables-attachment.ts","location":{"line":8}}}} +{"testRunStarted":{"id":"12","timestamp":{"seconds":0,"nanos":0}}} +{"testCase":{"id":"13","pickleId":"7","testSteps":[{"id":"14","pickleStepId":"6","stepDefinitionIds":["10"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}],"testRunStartedId":"12"}} +{"testCase":{"id":"15","pickleId":"9","testSteps":[{"id":"16","pickleStepId":"8","stepDefinitionIds":["11"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}],"testRunStartedId":"12"}} +{"testCaseStarted":{"id":"17","testCaseId":"13","timestamp":{"seconds":0,"nanos":1000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"17","testStepId":"14","timestamp":{"seconds":0,"nanos":2000000}}} +{"attachment":{"testCaseStartedId":"17","testStepId":"14","body":"/9j/4AAQSkZJRgABAQAAAQABAAD//gAfQ29tcHJlc3NlZCBieSBqcGVnLXJlY29tcHJlc3P/2wCEAAQEBAQEBAQEBAQGBgUGBggHBwcHCAwJCQkJCQwTDA4MDA4MExEUEA8QFBEeFxUVFx4iHRsdIiolJSo0MjRERFwBBAQEBAQEBAQEBAYGBQYGCAcHBwcIDAkJCQkJDBMMDgwMDgwTERQQDxAUER4XFRUXHiIdGx0iKiUlKjQyNEREXP/CABEIAC4AKQMBIgACEQEDEQH/xAAcAAABBAMBAAAAAAAAAAAAAAAIBAUGBwABAwL/2gAIAQEAAAAAOESYe+lPPw0bK2mvU5gRhNkM/tNMGeuJM5msiEjujvC+s0ApSWvn/8QAFgEBAQEAAAAAAAAAAAAAAAAABQME/9oACAECEAAAADs6pclK4E//xAAWAQEBAQAAAAAAAAAAAAAAAAAHBgT/2gAIAQMQAAAAMJZbKcF1XHit/8QANhAAAQQBAgQDBAcJAAAAAAAAAgEDBAUGABEHEiExEyJREEFCUhRTYXFzgZIVFiMyMzRVY3L/2gAIAQEAAT8AzLMqPBKOReXb6gy3sDbYdXXnS/labH3mWrrMOIWdGb063fxyoPq1XVp8klQ/3v8Aff7E0eCY86fjPtynn99/GclOq5v6782quZnOGmEnEcrmPNN96y1cWTFcH5BUurf5a4bcTKzP6x9QjlBuIKo1YVzq7mwfuJF+IC9y+zPLc8z4kWiuHz1GLuLAht/AU3u+6qfMK+XUuV4TbrTBtFNVoyYZM0RTJE6dO+2+oGcWZY1fzp0URsq5wGuXkUU3dLlHmH1FdYvMs59HCmW7SBKdQiVEHl3Hfyqqe7dNFbOYRlNDnkQlBth9uHaoPZ2C+SCSl9oL1HX0qN9c3+pNY6pkeSG9/XO/sie9fEV5d9Z5FxdbKNKsbeREsUbHZGAVxeQV6Lt8K6gtMPQYzhD43istETjzaC45sm6EaeulzOgC1Kmdkm1KF3wvO2Qjz+m+syECxe7Q+30ZV/NF3TX7dyv5nv06zGpPDOJd/WvAoV+QvHb1znwk8f8AcN/9c3XUuhp5s1qyl17L0poUQDNN+3VN07LqDTZdNg5fLsFdanyxAI4c/wBUSnsGy9B9w6x+kWwrq2blFW2VtHVUF11P4qiC+RT27r9+r6E9kUyiwmDusq8nNMny924zZc7rv3Cia/dSg/xTH6dcQMDpc/oSqbLmZeaNHoUxro9GfHs4C6uoGZYC4cXM6Z+TCb6BdV7avRjH1dEerRagWEO0iNToDyOx3N+Q0RU32XZehbLq4u4VMyByFI33VQI8ZpOZ5416IICnVdcHuHNjUOSs3y5lByGwaRpiL3Svid0b/EL4vavbXDDBM5ymjjRKi3qK2vZ5lOSYOvykRw1Lyhsgawbg9jGGSUtzJ63v1TzWU/zuB+CPZtPb/8QAJREAAgEDBAEEAwAAAAAAAAAAAQIDAAQRBRITIVEUMTJhI0Fx/9oACAECAQE/ALy8eNxb2/z63N4zTy6hbbpJJ9wV9uCdwPWaglFxEkqDGeiPBFSv6bUZJXLhXGQVx3kfdPBbpyvLNyDOAEbsEjOfsVpJ4rUlx83JH8FSwxTqElTI/R9iKGkBJm5X/GGO1R7kV0AABgAYA8Cv/8QAJREAAgIBBAEDBQAAAAAAAAAAAQIDBAUABhESMSFRcRMVIjJB/9oACAEDAQE/AN1bpuJcbFYt+hXgSSDzydG9uLFF7T3yekwjKl+wY8dvHtrAZlMzjo7RAWQHrIvsw1k+2I3LdksmZVcsymPjlg/z/NTU6MIsy2bf1x26hYnHKsy9ufXyB41sWnN9rmlPKrJNyvwBxrL4LH5mMLbj/Nf1dfRhqjsKaa27WZgtRZD1APLsuq1aGpBHXgQLGihVA1//2Q==","contentEncoding":"BASE64","mediaType":"image/jpeg","timestamp":{"seconds":0,"nanos":3000000}}} +{"testStepFinished":{"testCaseStartedId":"17","testStepId":"14","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":4000000}}} +{"testCaseFinished":{"testCaseStartedId":"17","timestamp":{"seconds":0,"nanos":5000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"18","testCaseId":"15","timestamp":{"seconds":0,"nanos":6000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"18","testStepId":"16","timestamp":{"seconds":0,"nanos":7000000}}} +{"attachment":{"testCaseStartedId":"18","testStepId":"16","body":"iVBORw0KGgoAAAANSUhEUgAAACkAAAAuCAYAAAC1ZTBOAAAABmJLR0QA/wD/AP+gvaeTAAAGgElEQVRYw81ZeWwUVRgfNF4xalDo7Oy92yYmEkm0nZ22olYtM7Pbbu8t24Ntl960Eo0HRCsW5BCIRLyDQK0pFqt/iCdVPIISQvEIVSxg4h8mEhPEqNE/jNLn972dmd1Ztruz3W11kpftdue995vv+H2/7w3DzPBatChwKcvLd7GCvJn1SG+YPNIp+PwFxm8wzrO89CPrEY/A36/keKRuc4F8PTNX18IC700AaAg2/x0GSXN8B8AfNuf7F8wKuBxBXgybHIzdlKvxE2v/MmLf00Kc77QT16ddxH2sh346320nzn1hYtvcSMyhKsIukWPB/sny4iZ2sXhlVsBZiwJXmHh5Gyz8N25gKvES29ogcX3USXJP9RkfE73EMRgiXF1FLNjTbKEoZATwuqJyC+uRj1FwhTKxPrKM5H7Zkx64+HGyjzj2honJV64ChYcX7565e3npDAVY6Seu9zoyAxc33F+tJNZ766JW5eX+9JKjSMpjBfEnnGxpq6ELZhNg7LBta9SAmjzyA4YAssViDkz4ngLsqSW5J3pnDaAGdEeTCvSfHGGpmBokL+3HCebmSpL7zewDVId1Tb0K9NxC3meaHqBHbqNmLy2jVDJXAOkAj3HBCsXt0lBCgAtuqbiKFaSzeJMD+M1Q8E8CrewKEfvzy0nu1xda3THcQiz3B4hjqMXQeq6xDgIYEOhUDi8WJ3Cz3E/jsL3auIse0lwUmXcy+ptzf5uu2jjfakvX7W/rAObleS+DJziHP7oOtBsGyVX79UBGV2i/mcNVut+wKhmy5mddqjXPI8tEOdEjVtFkgfKVVrCvrtcBQdeq1YUtjKnZ8DdubnRdS1cNnQfCZEtMwkij9GlfWJ4eIUNymcSyaC2vr4hY41CnDjyW0XTWdQy3qnNPqBjnwZezaGL3eHfScmZ/uplYVtUS26YG4j4Sudf9cSfh/OU6kFg6FZcRy31g3cn0q5GpKCJIuGKfI1JdMO2r/MmfbqRVL7tA1WiWh8y2P9VM7M9GPWF7vIE4Xw3PmJLMzZGYhixvYkyCWEefuK826SQM/EQa0fFiaHbIXYl3KJUDAFLqxS/W9cGUZIuJobpRq7e3ezNXRomMsl0tlfIwZvajNGmeaDJMuLYNDcRyT4Bymn13iGZz1kEqnoPqcwAzeyMFCTE1p2UwVYYPKuHFS+8zgHQ1pYmtjcYy72g3LXOYNOgSfGL38eRSzvVhJ00q9Jb9mWbi/iS1qne8pOXAQQY7ORqT0KsknQg0YtvYQNhiWZ888D0ZdbkhXjFudXOA3DExkslApDvqbl56naFtqYGa7Xi5NWF2ozU1QN8m3hStnpAZdk3PDNZ1QTVxtjP2JWXzUXWY7vTpBEJKCoIst22JhggmECf5aLWhAgOUFH0ARZOisFUJWgM5OH09x45AKY3dalk8TQXC2PR9DFoJVQ9XX0ksvXW0ZdWIG8NA2zhiHbNSf81Qhdyfr1TKZRdt5hAAVq1pKxH8n73DF5lfKN2sCoytNHlgs7SzcCSckNy5Cq0bJOaW6qReih9oAGXur0x+/iUUJCeI+bROgrvS7WkukGtvRnQjWlAH/rUVxqvNeiUeeXFE38Ly0hc0EXaG0lJBuuoDca0mD7pVp4QGgobVvqqscgSpVq/MBaky0t/4DJc5umC0ySe2J6MFwX24i5hujVJPrPhIGj5DWoKe0Vwdc6FkG6ec+WDAsDUxGdBKtM+JSwRU+bbHgoZ7HJzPVflVK65N3C0W+W6EG/5CejHajGW1Xj+n8enP1wreq5P03eIaVS8abZ6ycuwyDvFd4lWPXFalOB4YuAhu3EtvBq7CujvrICej5A1ePMoEAhcbO8UVpA/Uoz7n6Oy6HoldcfMfJsF7g+FDK2dJyeUAdJ9WAqGZck9k/+AK67cqpGmrMINrHqiQdXiQRK0ql0V4NEuHWFQPRJX+howOUznP0gJY5LhG2kC2qFJcY+1pd4Kai4FTtd5ckHaiQTI/lwZihX4oDAtO6qoMJJe5o4bkGjzDxJChvZK2BkixrACMy35Q82Ug6/fQfl3ZTO3DkwoHOPzHU2PtGDo11WThAqqg5J8CJCp32qJGj15+4Hjxtjl7r5MMJNZvZIWY1yNTMHbPzy+9hpnLKx4k9jSYteaOav2hlUc6nPHrkExBojvNTZXxLcIU9s0Qv6XMf3mpIHWDFydQxcD7GRfzf7hQ90GzdAheqeyAzxC+oMr2Hv8Cf7uNwHUHEgMAAAAASUVORK5CYII=","contentEncoding":"BASE64","mediaType":"image/png","timestamp":{"seconds":0,"nanos":8000000}}} +{"testStepFinished":{"testCaseStartedId":"18","testStepId":"16","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":9000000}}} +{"testCaseFinished":{"testCaseStartedId":"18","timestamp":{"seconds":0,"nanos":10000000},"willBeRetried":false}} +{"testRunFinished":{"testRunStartedId":"12","timestamp":{"seconds":0,"nanos":11000000},"success":true}} diff --git a/compatibility/examples-tables-attachment/examples-tables-attachment.ts b/compatibility/examples-tables-attachment/examples-tables-attachment.ts new file mode 100644 index 00000000..0e29e9f2 --- /dev/null +++ b/compatibility/examples-tables-attachment/examples-tables-attachment.ts @@ -0,0 +1,10 @@ +import { When } from '@cucumber/fake-cucumber' +import fs from 'node:fs' + +When('a JPEG image is attached', async function () { + await this.attach(fs.createReadStream(import.meta.dirname + '/cucumber.jpeg'), 'image/jpeg') +}) + +When('a PNG image is attached', async function () { + await this.attach(fs.createReadStream(import.meta.dirname + '/cucumber.png'), 'image/png') +}) diff --git a/compatibility/examples-tables-undefined/examples-tables-undefined.cpp b/compatibility/examples-tables-undefined/examples-tables-undefined.cpp new file mode 100644 index 00000000..13a3e619 --- /dev/null +++ b/compatibility/examples-tables-undefined/examples-tables-undefined.cpp @@ -0,0 +1,19 @@ +#include "cucumber_cpp/CucumberCpp.hpp" +#include "gtest/gtest.h" +#include +#include + +GIVEN(R"(there are {int} cucumbers)", (std::int32_t initialCount)) +{ + context.Insert(initialCount); +} + +WHEN(R"(I eat {int} cucumbers)", (std::int32_t eatCount)) +{ + context.Get() -= eatCount; +} + +THEN(R"(I should have {int} cucumbers)", (std::int32_t expectedCount)) +{ + ASSERT_THAT(context.Get(), testing::Eq(expectedCount)); +} diff --git a/compatibility/examples-tables-undefined/examples-tables-undefined.ndjson b/compatibility/examples-tables-undefined/examples-tables-undefined.ndjson new file mode 100644 index 00000000..114aaf5a --- /dev/null +++ b/compatibility/examples-tables-undefined/examples-tables-undefined.ndjson @@ -0,0 +1,41 @@ +{"meta":{"protocolVersion":"31.0.0","implementation":{"name":"fake-cucumber","version":"123.45.6"},"cpu":{"name":"arm64"},"os":{"name":"darwin","version":"24.5.0"},"runtime":{"name":"Node.js","version":"24.4.1"},"ci":{"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429","buildNumber":"154666429","git":{"revision":"99684bcacf01d95875834d87903dcb072306c9ad","remote":"https://github.com/cucumber-ltd/shouty.rb.git","branch":"main"}}}} +{"source":{"data":"Feature: Examples Tables - With Undefined Steps\n The replacement pattern used in scenario outlines does not influence how steps\n are matched. The replacement pattern is replaced, and step definitions are\n matched against that text. Because of that the following results in one\n undefined step for each example and a suggested snippet to implement it. \n\n Scenario Outline: Eating cucumbers\n Given there are cucumbers\n When I eat cucumbers\n Then I should have cucumbers\n\n @undefined\n Examples: These are undefined because the value is not an {int}\n | start | eat | left |\n | pear | 1 | 12 |\n | 12 | banana | 12 |\n | 0 | 1 | apple |\n","uri":"samples/examples-tables-undefined/examples-undefined.feature","mediaType":"text/x.cucumber.gherkin+plain"}} +{"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"Examples Tables - With Undefined Steps","description":" The replacement pattern used in scenario outlines does not influence how steps\n are matched. The replacement pattern is replaced, and step definitions are\n matched against that text. Because of that the following results in one\n undefined step for each example and a suggested snippet to implement it. ","children":[{"scenario":{"id":"9","tags":[],"location":{"line":7,"column":3},"keyword":"Scenario Outline","name":"Eating cucumbers","description":"","steps":[{"id":"0","location":{"line":8,"column":5},"keyword":"Given ","keywordType":"Context","text":"there are cucumbers"},{"id":"1","location":{"line":9,"column":5},"keyword":"When ","keywordType":"Action","text":"I eat cucumbers"},{"id":"2","location":{"line":10,"column":5},"keyword":"Then ","keywordType":"Outcome","text":"I should have cucumbers"}],"examples":[{"id":"8","tags":[{"location":{"line":12,"column":5},"name":"@undefined","id":"7"}],"location":{"line":13,"column":5},"keyword":"Examples","name":"These are undefined because the value is not an {int}","description":"","tableHeader":{"id":"3","location":{"line":14,"column":7},"cells":[{"location":{"line":14,"column":9},"value":"start"},{"location":{"line":14,"column":17},"value":"eat"},{"location":{"line":14,"column":26},"value":"left"}]},"tableBody":[{"id":"4","location":{"line":15,"column":7},"cells":[{"location":{"line":15,"column":9},"value":"pear"},{"location":{"line":15,"column":17},"value":"1"},{"location":{"line":15,"column":26},"value":"12"}]},{"id":"5","location":{"line":16,"column":7},"cells":[{"location":{"line":16,"column":9},"value":"12"},{"location":{"line":16,"column":17},"value":"banana"},{"location":{"line":16,"column":26},"value":"12"}]},{"id":"6","location":{"line":17,"column":7},"cells":[{"location":{"line":17,"column":9},"value":"0"},{"location":{"line":17,"column":17},"value":"1"},{"location":{"line":17,"column":26},"value":"apple"}]}]}]}}]},"comments":[],"uri":"samples/examples-tables-undefined/examples-undefined.feature"}} +{"pickle":{"id":"13","uri":"samples/examples-tables-undefined/examples-undefined.feature","location":{"line":15,"column":7},"astNodeIds":["9","4"],"name":"Eating cucumbers","language":"en","steps":[{"id":"10","text":"there are pear cucumbers","type":"Context","astNodeIds":["0","4"]},{"id":"11","text":"I eat 1 cucumbers","type":"Action","astNodeIds":["1","4"]},{"id":"12","text":"I should have 12 cucumbers","type":"Outcome","astNodeIds":["2","4"]}],"tags":[{"name":"@undefined","astNodeId":"7"}]}} +{"pickle":{"id":"17","uri":"samples/examples-tables-undefined/examples-undefined.feature","location":{"line":16,"column":7},"astNodeIds":["9","5"],"name":"Eating cucumbers","language":"en","steps":[{"id":"14","text":"there are 12 cucumbers","type":"Context","astNodeIds":["0","5"]},{"id":"15","text":"I eat banana cucumbers","type":"Action","astNodeIds":["1","5"]},{"id":"16","text":"I should have 12 cucumbers","type":"Outcome","astNodeIds":["2","5"]}],"tags":[{"name":"@undefined","astNodeId":"7"}]}} +{"pickle":{"id":"21","uri":"samples/examples-tables-undefined/examples-undefined.feature","location":{"line":17,"column":7},"astNodeIds":["9","6"],"name":"Eating cucumbers","language":"en","steps":[{"id":"18","text":"there are 0 cucumbers","type":"Context","astNodeIds":["0","6"]},{"id":"19","text":"I eat 1 cucumbers","type":"Action","astNodeIds":["1","6"]},{"id":"20","text":"I should have apple cucumbers","type":"Outcome","astNodeIds":["2","6"]}],"tags":[{"name":"@undefined","astNodeId":"7"}]}} +{"stepDefinition":{"id":"22","pattern":{"type":"CUCUMBER_EXPRESSION","source":"there are {int} cucumbers"},"sourceReference":{"uri":"samples/examples-tables-undefined/examples-tables-undefined.ts","location":{"line":4}}}} +{"stepDefinition":{"id":"23","pattern":{"type":"CUCUMBER_EXPRESSION","source":"I eat {int} cucumbers"},"sourceReference":{"uri":"samples/examples-tables-undefined/examples-tables-undefined.ts","location":{"line":8}}}} +{"stepDefinition":{"id":"24","pattern":{"type":"CUCUMBER_EXPRESSION","source":"I should have {int} cucumbers"},"sourceReference":{"uri":"samples/examples-tables-undefined/examples-tables-undefined.ts","location":{"line":12}}}} +{"testRunStarted":{"id":"25","timestamp":{"seconds":0,"nanos":0}}} +{"testCase":{"id":"26","pickleId":"13","testSteps":[{"id":"27","pickleStepId":"10","stepDefinitionIds":[],"stepMatchArgumentsLists":[]},{"id":"28","pickleStepId":"11","stepDefinitionIds":["23"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":6,"value":"1","children":[]},"parameterTypeName":"int"}]}]},{"id":"29","pickleStepId":"12","stepDefinitionIds":["24"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":14,"value":"12","children":[]},"parameterTypeName":"int"}]}]}],"testRunStartedId":"25"}} +{"testCase":{"id":"30","pickleId":"17","testSteps":[{"id":"31","pickleStepId":"14","stepDefinitionIds":["22"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":10,"value":"12","children":[]},"parameterTypeName":"int"}]}]},{"id":"32","pickleStepId":"15","stepDefinitionIds":[],"stepMatchArgumentsLists":[]},{"id":"33","pickleStepId":"16","stepDefinitionIds":["24"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":14,"value":"12","children":[]},"parameterTypeName":"int"}]}]}],"testRunStartedId":"25"}} +{"testCase":{"id":"34","pickleId":"21","testSteps":[{"id":"35","pickleStepId":"18","stepDefinitionIds":["22"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":10,"value":"0","children":[]},"parameterTypeName":"int"}]}]},{"id":"36","pickleStepId":"19","stepDefinitionIds":["23"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":6,"value":"1","children":[]},"parameterTypeName":"int"}]}]},{"id":"37","pickleStepId":"20","stepDefinitionIds":[],"stepMatchArgumentsLists":[]}],"testRunStartedId":"25"}} +{"testCaseStarted":{"id":"38","testCaseId":"26","timestamp":{"seconds":0,"nanos":1000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"38","testStepId":"27","timestamp":{"seconds":0,"nanos":2000000}}} +{"suggestion":{"id":"39","pickleStepId":"10","snippets":[{"language":"typescript","code":"Given(\"there are pear cucumbers\", () => {\n return \"pending\"\n})"}]}} +{"testStepFinished":{"testCaseStartedId":"38","testStepId":"27","testStepResult":{"status":"UNDEFINED","duration":{"seconds":0,"nanos":0}},"timestamp":{"seconds":0,"nanos":3000000}}} +{"testStepStarted":{"testCaseStartedId":"38","testStepId":"28","timestamp":{"seconds":0,"nanos":4000000}}} +{"testStepFinished":{"testCaseStartedId":"38","testStepId":"28","testStepResult":{"status":"SKIPPED","duration":{"seconds":0,"nanos":0}},"timestamp":{"seconds":0,"nanos":5000000}}} +{"testStepStarted":{"testCaseStartedId":"38","testStepId":"29","timestamp":{"seconds":0,"nanos":6000000}}} +{"testStepFinished":{"testCaseStartedId":"38","testStepId":"29","testStepResult":{"status":"SKIPPED","duration":{"seconds":0,"nanos":0}},"timestamp":{"seconds":0,"nanos":7000000}}} +{"testCaseFinished":{"testCaseStartedId":"38","timestamp":{"seconds":0,"nanos":8000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"40","testCaseId":"30","timestamp":{"seconds":0,"nanos":9000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"40","testStepId":"31","timestamp":{"seconds":0,"nanos":10000000}}} +{"testStepFinished":{"testCaseStartedId":"40","testStepId":"31","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":11000000}}} +{"testStepStarted":{"testCaseStartedId":"40","testStepId":"32","timestamp":{"seconds":0,"nanos":12000000}}} +{"suggestion":{"id":"41","pickleStepId":"15","snippets":[{"language":"typescript","code":"When(\"I eat banana cucumbers\", () => {\n return \"pending\"\n})"}]}} +{"testStepFinished":{"testCaseStartedId":"40","testStepId":"32","testStepResult":{"status":"UNDEFINED","duration":{"seconds":0,"nanos":0}},"timestamp":{"seconds":0,"nanos":13000000}}} +{"testStepStarted":{"testCaseStartedId":"40","testStepId":"33","timestamp":{"seconds":0,"nanos":14000000}}} +{"testStepFinished":{"testCaseStartedId":"40","testStepId":"33","testStepResult":{"status":"SKIPPED","duration":{"seconds":0,"nanos":0}},"timestamp":{"seconds":0,"nanos":15000000}}} +{"testCaseFinished":{"testCaseStartedId":"40","timestamp":{"seconds":0,"nanos":16000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"42","testCaseId":"34","timestamp":{"seconds":0,"nanos":17000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"42","testStepId":"35","timestamp":{"seconds":0,"nanos":18000000}}} +{"testStepFinished":{"testCaseStartedId":"42","testStepId":"35","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":19000000}}} +{"testStepStarted":{"testCaseStartedId":"42","testStepId":"36","timestamp":{"seconds":0,"nanos":20000000}}} +{"testStepFinished":{"testCaseStartedId":"42","testStepId":"36","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":21000000}}} +{"testStepStarted":{"testCaseStartedId":"42","testStepId":"37","timestamp":{"seconds":0,"nanos":22000000}}} +{"suggestion":{"id":"43","pickleStepId":"20","snippets":[{"language":"typescript","code":"Then(\"I should have apple cucumbers\", () => {\n return \"pending\"\n})"}]}} +{"testStepFinished":{"testCaseStartedId":"42","testStepId":"37","testStepResult":{"status":"UNDEFINED","duration":{"seconds":0,"nanos":0}},"timestamp":{"seconds":0,"nanos":23000000}}} +{"testCaseFinished":{"testCaseStartedId":"42","timestamp":{"seconds":0,"nanos":24000000},"willBeRetried":false}} +{"testRunFinished":{"testRunStartedId":"25","timestamp":{"seconds":0,"nanos":25000000},"success":false}} diff --git a/compatibility/examples-tables-undefined/examples-tables-undefined.ts b/compatibility/examples-tables-undefined/examples-tables-undefined.ts new file mode 100644 index 00000000..80d45547 --- /dev/null +++ b/compatibility/examples-tables-undefined/examples-tables-undefined.ts @@ -0,0 +1,14 @@ +import assert from 'node:assert' +import { Given, When, Then } from '@cucumber/fake-cucumber' + +Given('there are {int} cucumbers', function (initialCount) { + this.count = initialCount +}) + +When('I eat {int} cucumbers', function (eatCount) { + this.count -= eatCount +}) + +Then('I should have {int} cucumbers', function (expectedCount) { + assert.strictEqual(this.count, expectedCount) +}) diff --git a/compatibility/examples-tables-undefined/examples-undefined.feature b/compatibility/examples-tables-undefined/examples-undefined.feature new file mode 100644 index 00000000..0e35aaf1 --- /dev/null +++ b/compatibility/examples-tables-undefined/examples-undefined.feature @@ -0,0 +1,17 @@ +Feature: Examples Tables - With Undefined Steps + The replacement pattern used in scenario outlines does not influence how steps + are matched. The replacement pattern is replaced, and step definitions are + matched against that text. Because of that the following results in one + undefined step for each example and a suggested snippet to implement it. + + Scenario Outline: Eating cucumbers + Given there are cucumbers + When I eat cucumbers + Then I should have cucumbers + + @undefined + Examples: These are undefined because the value is not an {int} + | start | eat | left | + | pear | 1 | 12 | + | 12 | banana | 12 | + | 0 | 1 | apple | diff --git a/compatibility/examples-tables/examples-tables.cpp b/compatibility/examples-tables/examples-tables.cpp new file mode 100644 index 00000000..000fd1e2 --- /dev/null +++ b/compatibility/examples-tables/examples-tables.cpp @@ -0,0 +1,31 @@ +#include "cucumber_cpp/CucumberCpp.hpp" +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include +#include + +GIVEN(R"(there are {int} cucumbers)", (std::int32_t cucumbers)) +{ + context.InsertAt("cucumbers", cucumbers); +} + +GIVEN(R"(there are {int} friends)", (std::int32_t friends)) +{ + context.InsertAt("friends", friends); +} + +WHEN(R"(I eat {int} cucumbers)", (std::int32_t eatCount)) +{ + context.Get("cucumbers") -= eatCount; +} + +THEN(R"(I should have {int} cucumbers)", (std::int32_t expectedCount)) +{ + ASSERT_THAT(context.Get("cucumbers"), testing::Eq(expectedCount)); +} + +THEN(R"(each person can eat {int} cucumbers)", (std::int32_t expectedShare)) +{ + const auto share = context.Get("cucumbers") / (1 + context.Get("friends")); + ASSERT_THAT(share, testing::Eq(expectedShare)); +} diff --git a/compatibility/examples-tables/examples-tables.feature b/compatibility/examples-tables/examples-tables.feature new file mode 100644 index 00000000..523309dc --- /dev/null +++ b/compatibility/examples-tables/examples-tables.feature @@ -0,0 +1,37 @@ +Feature: Examples Tables + Sometimes it can be desirable to run the same scenario multiple times with + different data each time - this can be done by placing an Examples table + underneath a Scenario, and use in the Scenario which match the + table headers. + + The Scenario Outline name can also be parameterized. The name of the resulting + pickle will have the replaced with the value from the examples + table. + + Scenario Outline: Eating cucumbers + Given there are cucumbers + When I eat cucumbers + Then I should have cucumbers + + @passing + Examples: These are passing + | start | eat | left | + | 12 | 5 | 7 | + | 20 | 5 | 15 | + + @failing + Examples: These are failing + | start | eat | left | + | 12 | 20 | 0 | + | 0 | 1 | 0 | + + Scenario Outline: Eating cucumbers with friends + Given there are friends + And there are cucumbers + Then each person can eat cucumbers + + Examples: + | friends | start | share | + | 11 | 12 | 1 | + | 1 | 4 | 2 | + | 0 | 4 | 4 | diff --git a/compatibility/examples-tables/examples-tables.ndjson b/compatibility/examples-tables/examples-tables.ndjson new file mode 100644 index 00000000..f1db9992 --- /dev/null +++ b/compatibility/examples-tables/examples-tables.ndjson @@ -0,0 +1,80 @@ +{"meta":{"protocolVersion":"31.0.0","implementation":{"name":"fake-cucumber","version":"123.45.6"},"cpu":{"name":"arm64"},"os":{"name":"darwin","version":"24.5.0"},"runtime":{"name":"Node.js","version":"24.4.1"},"ci":{"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429","buildNumber":"154666429","git":{"revision":"99684bcacf01d95875834d87903dcb072306c9ad","remote":"https://github.com/cucumber-ltd/shouty.rb.git","branch":"main"}}}} +{"source":{"data":"Feature: Examples Tables\n Sometimes it can be desirable to run the same scenario multiple times with\n different data each time - this can be done by placing an Examples table\n underneath a Scenario, and use in the Scenario which match the\n table headers.\n\n The Scenario Outline name can also be parameterized. The name of the resulting\n pickle will have the replaced with the value from the examples\n table.\n\n Scenario Outline: Eating cucumbers\n Given there are cucumbers\n When I eat cucumbers\n Then I should have cucumbers\n\n @passing\n Examples: These are passing\n | start | eat | left |\n | 12 | 5 | 7 |\n | 20 | 5 | 15 |\n\n @failing\n Examples: These are failing\n | start | eat | left |\n | 12 | 20 | 0 |\n | 0 | 1 | 0 |\n\n Scenario Outline: Eating cucumbers with friends\n Given there are friends\n And there are cucumbers\n Then each person can eat cucumbers\n\n Examples:\n | friends | start | share |\n | 11 | 12 | 1 |\n | 1 | 4 | 2 |\n | 0 | 4 | 4 |\n","uri":"samples/examples-tables/examples-tables.feature","mediaType":"text/x.cucumber.gherkin+plain"}} +{"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"Examples Tables","description":" Sometimes it can be desirable to run the same scenario multiple times with\n different data each time - this can be done by placing an Examples table\n underneath a Scenario, and use in the Scenario which match the\n table headers.\n\n The Scenario Outline name can also be parameterized. The name of the resulting\n pickle will have the replaced with the value from the examples\n table.","children":[{"scenario":{"id":"13","tags":[],"location":{"line":11,"column":3},"keyword":"Scenario Outline","name":"Eating cucumbers","description":"","steps":[{"id":"0","location":{"line":12,"column":5},"keyword":"Given ","keywordType":"Context","text":"there are cucumbers"},{"id":"1","location":{"line":13,"column":5},"keyword":"When ","keywordType":"Action","text":"I eat cucumbers"},{"id":"2","location":{"line":14,"column":5},"keyword":"Then ","keywordType":"Outcome","text":"I should have cucumbers"}],"examples":[{"id":"7","tags":[{"location":{"line":16,"column":5},"name":"@passing","id":"6"}],"location":{"line":17,"column":5},"keyword":"Examples","name":"These are passing","description":"","tableHeader":{"id":"3","location":{"line":18,"column":7},"cells":[{"location":{"line":18,"column":9},"value":"start"},{"location":{"line":18,"column":17},"value":"eat"},{"location":{"line":18,"column":23},"value":"left"}]},"tableBody":[{"id":"4","location":{"line":19,"column":7},"cells":[{"location":{"line":19,"column":12},"value":"12"},{"location":{"line":19,"column":19},"value":"5"},{"location":{"line":19,"column":26},"value":"7"}]},{"id":"5","location":{"line":20,"column":7},"cells":[{"location":{"line":20,"column":12},"value":"20"},{"location":{"line":20,"column":19},"value":"5"},{"location":{"line":20,"column":25},"value":"15"}]}]},{"id":"12","tags":[{"location":{"line":22,"column":5},"name":"@failing","id":"11"}],"location":{"line":23,"column":5},"keyword":"Examples","name":"These are failing","description":"","tableHeader":{"id":"8","location":{"line":24,"column":7},"cells":[{"location":{"line":24,"column":9},"value":"start"},{"location":{"line":24,"column":17},"value":"eat"},{"location":{"line":24,"column":23},"value":"left"}]},"tableBody":[{"id":"9","location":{"line":25,"column":7},"cells":[{"location":{"line":25,"column":12},"value":"12"},{"location":{"line":25,"column":18},"value":"20"},{"location":{"line":25,"column":26},"value":"0"}]},{"id":"10","location":{"line":26,"column":7},"cells":[{"location":{"line":26,"column":13},"value":"0"},{"location":{"line":26,"column":19},"value":"1"},{"location":{"line":26,"column":26},"value":"0"}]}]}]}},{"scenario":{"id":"22","tags":[],"location":{"line":28,"column":3},"keyword":"Scenario Outline","name":"Eating cucumbers with friends","description":"","steps":[{"id":"14","location":{"line":29,"column":5},"keyword":"Given ","keywordType":"Context","text":"there are friends"},{"id":"15","location":{"line":30,"column":5},"keyword":"And ","keywordType":"Conjunction","text":"there are cucumbers"},{"id":"16","location":{"line":31,"column":5},"keyword":"Then ","keywordType":"Outcome","text":"each person can eat cucumbers"}],"examples":[{"id":"21","tags":[],"location":{"line":33,"column":5},"keyword":"Examples","name":"","description":"","tableHeader":{"id":"17","location":{"line":34,"column":7},"cells":[{"location":{"line":34,"column":9},"value":"friends"},{"location":{"line":34,"column":19},"value":"start"},{"location":{"line":34,"column":27},"value":"share"}]},"tableBody":[{"id":"18","location":{"line":35,"column":7},"cells":[{"location":{"line":35,"column":14},"value":"11"},{"location":{"line":35,"column":22},"value":"12"},{"location":{"line":35,"column":31},"value":"1"}]},{"id":"19","location":{"line":36,"column":7},"cells":[{"location":{"line":36,"column":15},"value":"1"},{"location":{"line":36,"column":23},"value":"4"},{"location":{"line":36,"column":31},"value":"2"}]},{"id":"20","location":{"line":37,"column":7},"cells":[{"location":{"line":37,"column":15},"value":"0"},{"location":{"line":37,"column":23},"value":"4"},{"location":{"line":37,"column":31},"value":"4"}]}]}]}}]},"comments":[],"uri":"samples/examples-tables/examples-tables.feature"}} +{"pickle":{"id":"26","uri":"samples/examples-tables/examples-tables.feature","location":{"line":19,"column":7},"astNodeIds":["13","4"],"name":"Eating cucumbers","language":"en","steps":[{"id":"23","text":"there are 12 cucumbers","type":"Context","astNodeIds":["0","4"]},{"id":"24","text":"I eat 5 cucumbers","type":"Action","astNodeIds":["1","4"]},{"id":"25","text":"I should have 7 cucumbers","type":"Outcome","astNodeIds":["2","4"]}],"tags":[{"name":"@passing","astNodeId":"6"}]}} +{"pickle":{"id":"30","uri":"samples/examples-tables/examples-tables.feature","location":{"line":20,"column":7},"astNodeIds":["13","5"],"name":"Eating cucumbers","language":"en","steps":[{"id":"27","text":"there are 20 cucumbers","type":"Context","astNodeIds":["0","5"]},{"id":"28","text":"I eat 5 cucumbers","type":"Action","astNodeIds":["1","5"]},{"id":"29","text":"I should have 15 cucumbers","type":"Outcome","astNodeIds":["2","5"]}],"tags":[{"name":"@passing","astNodeId":"6"}]}} +{"pickle":{"id":"34","uri":"samples/examples-tables/examples-tables.feature","location":{"line":25,"column":7},"astNodeIds":["13","9"],"name":"Eating cucumbers","language":"en","steps":[{"id":"31","text":"there are 12 cucumbers","type":"Context","astNodeIds":["0","9"]},{"id":"32","text":"I eat 20 cucumbers","type":"Action","astNodeIds":["1","9"]},{"id":"33","text":"I should have 0 cucumbers","type":"Outcome","astNodeIds":["2","9"]}],"tags":[{"name":"@failing","astNodeId":"11"}]}} +{"pickle":{"id":"38","uri":"samples/examples-tables/examples-tables.feature","location":{"line":26,"column":7},"astNodeIds":["13","10"],"name":"Eating cucumbers","language":"en","steps":[{"id":"35","text":"there are 0 cucumbers","type":"Context","astNodeIds":["0","10"]},{"id":"36","text":"I eat 1 cucumbers","type":"Action","astNodeIds":["1","10"]},{"id":"37","text":"I should have 0 cucumbers","type":"Outcome","astNodeIds":["2","10"]}],"tags":[{"name":"@failing","astNodeId":"11"}]}} +{"pickle":{"id":"42","uri":"samples/examples-tables/examples-tables.feature","location":{"line":35,"column":7},"astNodeIds":["22","18"],"name":"Eating cucumbers with 11 friends","language":"en","steps":[{"id":"39","text":"there are 11 friends","type":"Context","astNodeIds":["14","18"]},{"id":"40","text":"there are 12 cucumbers","type":"Context","astNodeIds":["15","18"]},{"id":"41","text":"each person can eat 1 cucumbers","type":"Outcome","astNodeIds":["16","18"]}],"tags":[]}} +{"pickle":{"id":"46","uri":"samples/examples-tables/examples-tables.feature","location":{"line":36,"column":7},"astNodeIds":["22","19"],"name":"Eating cucumbers with 1 friends","language":"en","steps":[{"id":"43","text":"there are 1 friends","type":"Context","astNodeIds":["14","19"]},{"id":"44","text":"there are 4 cucumbers","type":"Context","astNodeIds":["15","19"]},{"id":"45","text":"each person can eat 2 cucumbers","type":"Outcome","astNodeIds":["16","19"]}],"tags":[]}} +{"pickle":{"id":"50","uri":"samples/examples-tables/examples-tables.feature","location":{"line":37,"column":7},"astNodeIds":["22","20"],"name":"Eating cucumbers with 0 friends","language":"en","steps":[{"id":"47","text":"there are 0 friends","type":"Context","astNodeIds":["14","20"]},{"id":"48","text":"there are 4 cucumbers","type":"Context","astNodeIds":["15","20"]},{"id":"49","text":"each person can eat 4 cucumbers","type":"Outcome","astNodeIds":["16","20"]}],"tags":[]}} +{"stepDefinition":{"id":"51","pattern":{"type":"CUCUMBER_EXPRESSION","source":"there are {int} cucumbers"},"sourceReference":{"uri":"samples/examples-tables/examples-tables.ts","location":{"line":4}}}} +{"stepDefinition":{"id":"52","pattern":{"type":"CUCUMBER_EXPRESSION","source":"there are {int} friends"},"sourceReference":{"uri":"samples/examples-tables/examples-tables.ts","location":{"line":8}}}} +{"stepDefinition":{"id":"53","pattern":{"type":"CUCUMBER_EXPRESSION","source":"I eat {int} cucumbers"},"sourceReference":{"uri":"samples/examples-tables/examples-tables.ts","location":{"line":12}}}} +{"stepDefinition":{"id":"54","pattern":{"type":"CUCUMBER_EXPRESSION","source":"I should have {int} cucumbers"},"sourceReference":{"uri":"samples/examples-tables/examples-tables.ts","location":{"line":16}}}} +{"stepDefinition":{"id":"55","pattern":{"type":"CUCUMBER_EXPRESSION","source":"each person can eat {int} cucumbers"},"sourceReference":{"uri":"samples/examples-tables/examples-tables.ts","location":{"line":20}}}} +{"testRunStarted":{"id":"56","timestamp":{"seconds":0,"nanos":0}}} +{"testCase":{"id":"57","pickleId":"26","testSteps":[{"id":"58","pickleStepId":"23","stepDefinitionIds":["51"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":10,"value":"12","children":[]},"parameterTypeName":"int"}]}]},{"id":"59","pickleStepId":"24","stepDefinitionIds":["53"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":6,"value":"5","children":[]},"parameterTypeName":"int"}]}]},{"id":"60","pickleStepId":"25","stepDefinitionIds":["54"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":14,"value":"7","children":[]},"parameterTypeName":"int"}]}]}],"testRunStartedId":"56"}} +{"testCase":{"id":"61","pickleId":"30","testSteps":[{"id":"62","pickleStepId":"27","stepDefinitionIds":["51"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":10,"value":"20","children":[]},"parameterTypeName":"int"}]}]},{"id":"63","pickleStepId":"28","stepDefinitionIds":["53"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":6,"value":"5","children":[]},"parameterTypeName":"int"}]}]},{"id":"64","pickleStepId":"29","stepDefinitionIds":["54"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":14,"value":"15","children":[]},"parameterTypeName":"int"}]}]}],"testRunStartedId":"56"}} +{"testCase":{"id":"65","pickleId":"34","testSteps":[{"id":"66","pickleStepId":"31","stepDefinitionIds":["51"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":10,"value":"12","children":[]},"parameterTypeName":"int"}]}]},{"id":"67","pickleStepId":"32","stepDefinitionIds":["53"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":6,"value":"20","children":[]},"parameterTypeName":"int"}]}]},{"id":"68","pickleStepId":"33","stepDefinitionIds":["54"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":14,"value":"0","children":[]},"parameterTypeName":"int"}]}]}],"testRunStartedId":"56"}} +{"testCase":{"id":"69","pickleId":"38","testSteps":[{"id":"70","pickleStepId":"35","stepDefinitionIds":["51"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":10,"value":"0","children":[]},"parameterTypeName":"int"}]}]},{"id":"71","pickleStepId":"36","stepDefinitionIds":["53"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":6,"value":"1","children":[]},"parameterTypeName":"int"}]}]},{"id":"72","pickleStepId":"37","stepDefinitionIds":["54"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":14,"value":"0","children":[]},"parameterTypeName":"int"}]}]}],"testRunStartedId":"56"}} +{"testCase":{"id":"73","pickleId":"42","testSteps":[{"id":"74","pickleStepId":"39","stepDefinitionIds":["52"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":10,"value":"11","children":[]},"parameterTypeName":"int"}]}]},{"id":"75","pickleStepId":"40","stepDefinitionIds":["51"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":10,"value":"12","children":[]},"parameterTypeName":"int"}]}]},{"id":"76","pickleStepId":"41","stepDefinitionIds":["55"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":20,"value":"1","children":[]},"parameterTypeName":"int"}]}]}],"testRunStartedId":"56"}} +{"testCase":{"id":"77","pickleId":"46","testSteps":[{"id":"78","pickleStepId":"43","stepDefinitionIds":["52"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":10,"value":"1","children":[]},"parameterTypeName":"int"}]}]},{"id":"79","pickleStepId":"44","stepDefinitionIds":["51"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":10,"value":"4","children":[]},"parameterTypeName":"int"}]}]},{"id":"80","pickleStepId":"45","stepDefinitionIds":["55"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":20,"value":"2","children":[]},"parameterTypeName":"int"}]}]}],"testRunStartedId":"56"}} +{"testCase":{"id":"81","pickleId":"50","testSteps":[{"id":"82","pickleStepId":"47","stepDefinitionIds":["52"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":10,"value":"0","children":[]},"parameterTypeName":"int"}]}]},{"id":"83","pickleStepId":"48","stepDefinitionIds":["51"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":10,"value":"4","children":[]},"parameterTypeName":"int"}]}]},{"id":"84","pickleStepId":"49","stepDefinitionIds":["55"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":20,"value":"4","children":[]},"parameterTypeName":"int"}]}]}],"testRunStartedId":"56"}} +{"testCaseStarted":{"id":"85","testCaseId":"57","timestamp":{"seconds":0,"nanos":1000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"85","testStepId":"58","timestamp":{"seconds":0,"nanos":2000000}}} +{"testStepFinished":{"testCaseStartedId":"85","testStepId":"58","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":3000000}}} +{"testStepStarted":{"testCaseStartedId":"85","testStepId":"59","timestamp":{"seconds":0,"nanos":4000000}}} +{"testStepFinished":{"testCaseStartedId":"85","testStepId":"59","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":5000000}}} +{"testStepStarted":{"testCaseStartedId":"85","testStepId":"60","timestamp":{"seconds":0,"nanos":6000000}}} +{"testStepFinished":{"testCaseStartedId":"85","testStepId":"60","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":7000000}}} +{"testCaseFinished":{"testCaseStartedId":"85","timestamp":{"seconds":0,"nanos":8000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"86","testCaseId":"61","timestamp":{"seconds":0,"nanos":9000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"86","testStepId":"62","timestamp":{"seconds":0,"nanos":10000000}}} +{"testStepFinished":{"testCaseStartedId":"86","testStepId":"62","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":11000000}}} +{"testStepStarted":{"testCaseStartedId":"86","testStepId":"63","timestamp":{"seconds":0,"nanos":12000000}}} +{"testStepFinished":{"testCaseStartedId":"86","testStepId":"63","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":13000000}}} +{"testStepStarted":{"testCaseStartedId":"86","testStepId":"64","timestamp":{"seconds":0,"nanos":14000000}}} +{"testStepFinished":{"testCaseStartedId":"86","testStepId":"64","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":15000000}}} +{"testCaseFinished":{"testCaseStartedId":"86","timestamp":{"seconds":0,"nanos":16000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"87","testCaseId":"65","timestamp":{"seconds":0,"nanos":17000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"87","testStepId":"66","timestamp":{"seconds":0,"nanos":18000000}}} +{"testStepFinished":{"testCaseStartedId":"87","testStepId":"66","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":19000000}}} +{"testStepStarted":{"testCaseStartedId":"87","testStepId":"67","timestamp":{"seconds":0,"nanos":20000000}}} +{"testStepFinished":{"testCaseStartedId":"87","testStepId":"67","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":21000000}}} +{"testStepStarted":{"testCaseStartedId":"87","testStepId":"68","timestamp":{"seconds":0,"nanos":22000000}}} +{"testStepFinished":{"testCaseStartedId":"87","testStepId":"68","testStepResult":{"message":"Expected values to be strictly equal:\n\n-8 !== 0\n","exception":{"type":"AssertionError","message":"Expected values to be strictly equal:\n\n-8 !== 0\n","stackTrace":"AssertionError: Expected values to be strictly equal:\n\n-8 !== 0\n\nsamples/examples-tables/examples-tables.feature:14"},"status":"FAILED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":23000000}}} +{"testCaseFinished":{"testCaseStartedId":"87","timestamp":{"seconds":0,"nanos":24000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"88","testCaseId":"69","timestamp":{"seconds":0,"nanos":25000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"88","testStepId":"70","timestamp":{"seconds":0,"nanos":26000000}}} +{"testStepFinished":{"testCaseStartedId":"88","testStepId":"70","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":27000000}}} +{"testStepStarted":{"testCaseStartedId":"88","testStepId":"71","timestamp":{"seconds":0,"nanos":28000000}}} +{"testStepFinished":{"testCaseStartedId":"88","testStepId":"71","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":29000000}}} +{"testStepStarted":{"testCaseStartedId":"88","testStepId":"72","timestamp":{"seconds":0,"nanos":30000000}}} +{"testStepFinished":{"testCaseStartedId":"88","testStepId":"72","testStepResult":{"message":"Expected values to be strictly equal:\n\n-1 !== 0\n","exception":{"type":"AssertionError","message":"Expected values to be strictly equal:\n\n-1 !== 0\n","stackTrace":"AssertionError: Expected values to be strictly equal:\n\n-1 !== 0\n\nsamples/examples-tables/examples-tables.feature:14"},"status":"FAILED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":31000000}}} +{"testCaseFinished":{"testCaseStartedId":"88","timestamp":{"seconds":0,"nanos":32000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"89","testCaseId":"73","timestamp":{"seconds":0,"nanos":33000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"89","testStepId":"74","timestamp":{"seconds":0,"nanos":34000000}}} +{"testStepFinished":{"testCaseStartedId":"89","testStepId":"74","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":35000000}}} +{"testStepStarted":{"testCaseStartedId":"89","testStepId":"75","timestamp":{"seconds":0,"nanos":36000000}}} +{"testStepFinished":{"testCaseStartedId":"89","testStepId":"75","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":37000000}}} +{"testStepStarted":{"testCaseStartedId":"89","testStepId":"76","timestamp":{"seconds":0,"nanos":38000000}}} +{"testStepFinished":{"testCaseStartedId":"89","testStepId":"76","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":39000000}}} +{"testCaseFinished":{"testCaseStartedId":"89","timestamp":{"seconds":0,"nanos":40000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"90","testCaseId":"77","timestamp":{"seconds":0,"nanos":41000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"90","testStepId":"78","timestamp":{"seconds":0,"nanos":42000000}}} +{"testStepFinished":{"testCaseStartedId":"90","testStepId":"78","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":43000000}}} +{"testStepStarted":{"testCaseStartedId":"90","testStepId":"79","timestamp":{"seconds":0,"nanos":44000000}}} +{"testStepFinished":{"testCaseStartedId":"90","testStepId":"79","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":45000000}}} +{"testStepStarted":{"testCaseStartedId":"90","testStepId":"80","timestamp":{"seconds":0,"nanos":46000000}}} +{"testStepFinished":{"testCaseStartedId":"90","testStepId":"80","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":47000000}}} +{"testCaseFinished":{"testCaseStartedId":"90","timestamp":{"seconds":0,"nanos":48000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"91","testCaseId":"81","timestamp":{"seconds":0,"nanos":49000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"91","testStepId":"82","timestamp":{"seconds":0,"nanos":50000000}}} +{"testStepFinished":{"testCaseStartedId":"91","testStepId":"82","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":51000000}}} +{"testStepStarted":{"testCaseStartedId":"91","testStepId":"83","timestamp":{"seconds":0,"nanos":52000000}}} +{"testStepFinished":{"testCaseStartedId":"91","testStepId":"83","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":53000000}}} +{"testStepStarted":{"testCaseStartedId":"91","testStepId":"84","timestamp":{"seconds":0,"nanos":54000000}}} +{"testStepFinished":{"testCaseStartedId":"91","testStepId":"84","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":55000000}}} +{"testCaseFinished":{"testCaseStartedId":"91","timestamp":{"seconds":0,"nanos":56000000},"willBeRetried":false}} +{"testRunFinished":{"testRunStartedId":"56","timestamp":{"seconds":0,"nanos":57000000},"success":false}} diff --git a/compatibility/examples-tables/examples-tables.ts b/compatibility/examples-tables/examples-tables.ts new file mode 100644 index 00000000..38a157fd --- /dev/null +++ b/compatibility/examples-tables/examples-tables.ts @@ -0,0 +1,23 @@ +import assert from 'node:assert' +import { Given, When, Then } from '@cucumber/fake-cucumber' + +Given('there are {int} cucumbers', function (initialCount) { + this.count = initialCount +}) + +Given('there are {int} friends', function (initialFriends) { + this.friends = initialFriends +}) + +When('I eat {int} cucumbers', function (eatCount) { + this.count -= eatCount +}) + +Then('I should have {int} cucumbers', function (expectedCount) { + assert.strictEqual(this.count, expectedCount) +}) + +Then('each person can eat {int} cucumbers', function (expectedShare) { + let share = Math.floor(this.count / (1 + this.friends)); + assert.strictEqual(share, expectedShare) +}) diff --git a/compatibility/global-hooks-afterall-error/global-hooks-afterall-error.cpp b/compatibility/global-hooks-afterall-error/global-hooks-afterall-error.cpp new file mode 100644 index 00000000..ea4e948f --- /dev/null +++ b/compatibility/global-hooks-afterall-error/global-hooks-afterall-error.cpp @@ -0,0 +1,32 @@ +#include "cucumber_cpp/CucumberCpp.hpp" +#include + +HOOK_BEFORE_ALL() +{ + // no-op +} + +HOOK_BEFORE_ALL() +{ + // no-op +} + +WHEN(R"(a step passes)") +{ + // no-op +} + +HOOK_AFTER_ALL() +{ + // no-op +} + +HOOK_AFTER_ALL() +{ + ASSERT_THAT(true, false); +} + +HOOK_AFTER_ALL() +{ + // no-op +} diff --git a/compatibility/global-hooks-afterall-error/global-hooks-afterall-error.feature b/compatibility/global-hooks-afterall-error/global-hooks-afterall-error.feature new file mode 100644 index 00000000..0ef1bfed --- /dev/null +++ b/compatibility/global-hooks-afterall-error/global-hooks-afterall-error.feature @@ -0,0 +1,6 @@ +Feature: Global hooks - AfterAll error + Errors in AfterAll hooks cause the whole test run to fail. The remaining AfterAll hooks will still run, in an + effort to clean up resources as well as possible. + + Scenario: A passing scenario + When a step passes diff --git a/compatibility/global-hooks-afterall-error/global-hooks-afterall-error.ndjson b/compatibility/global-hooks-afterall-error/global-hooks-afterall-error.ndjson new file mode 100644 index 00000000..25b9a8b5 --- /dev/null +++ b/compatibility/global-hooks-afterall-error/global-hooks-afterall-error.ndjson @@ -0,0 +1,27 @@ +{"meta":{"protocolVersion":"31.0.0","implementation":{"name":"fake-cucumber","version":"123.45.6"},"cpu":{"name":"arm64"},"os":{"name":"darwin","version":"24.5.0"},"runtime":{"name":"Node.js","version":"24.4.1"},"ci":{"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429","buildNumber":"154666429","git":{"revision":"99684bcacf01d95875834d87903dcb072306c9ad","remote":"https://github.com/cucumber-ltd/shouty.rb.git","branch":"main"}}}} +{"source":{"data":"Feature: Global hooks - AfterAll error\n Errors in AfterAll hooks cause the whole test run to fail. The remaining AfterAll hooks will still run, in an\n effort to clean up resources as well as possible.\n\n Scenario: A passing scenario\n When a step passes\n","uri":"samples/global-hooks-afterall-error/global-hooks-afterall-error.feature","mediaType":"text/x.cucumber.gherkin+plain"}} +{"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"Global hooks - AfterAll error","description":" Errors in AfterAll hooks cause the whole test run to fail. The remaining AfterAll hooks will still run, in an\n effort to clean up resources as well as possible.","children":[{"scenario":{"id":"1","tags":[],"location":{"line":5,"column":3},"keyword":"Scenario","name":"A passing scenario","description":"","steps":[{"id":"0","location":{"line":6,"column":5},"keyword":"When ","keywordType":"Action","text":"a step passes"}],"examples":[]}}]},"comments":[],"uri":"samples/global-hooks-afterall-error/global-hooks-afterall-error.feature"}} +{"pickle":{"id":"3","uri":"samples/global-hooks-afterall-error/global-hooks-afterall-error.feature","location":{"line":5,"column":3},"astNodeIds":["1"],"tags":[],"name":"A passing scenario","language":"en","steps":[{"id":"2","text":"a step passes","type":"Action","astNodeIds":["0"]}]}} +{"stepDefinition":{"id":"6","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a step passes"},"sourceReference":{"uri":"samples/global-hooks-afterall-error/global-hooks-afterall-error.ts","location":{"line":11}}}} +{"hook":{"id":"4","type":"BEFORE_TEST_RUN","sourceReference":{"uri":"samples/global-hooks-afterall-error/global-hooks-afterall-error.ts","location":{"line":3}}}} +{"hook":{"id":"5","type":"BEFORE_TEST_RUN","sourceReference":{"uri":"samples/global-hooks-afterall-error/global-hooks-afterall-error.ts","location":{"line":7}}}} +{"hook":{"id":"7","type":"AFTER_TEST_RUN","sourceReference":{"uri":"samples/global-hooks-afterall-error/global-hooks-afterall-error.ts","location":{"line":15}}}} +{"hook":{"id":"8","type":"AFTER_TEST_RUN","sourceReference":{"uri":"samples/global-hooks-afterall-error/global-hooks-afterall-error.ts","location":{"line":19}}}} +{"hook":{"id":"9","type":"AFTER_TEST_RUN","sourceReference":{"uri":"samples/global-hooks-afterall-error/global-hooks-afterall-error.ts","location":{"line":23}}}} +{"testRunStarted":{"id":"10","timestamp":{"seconds":0,"nanos":0}}} +{"testRunHookStarted":{"testRunStartedId":"10","id":"11","hookId":"4","timestamp":{"seconds":0,"nanos":1000000}}} +{"testRunHookFinished":{"testRunHookStartedId":"11","timestamp":{"seconds":0,"nanos":2000000},"result":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}}}} +{"testRunHookStarted":{"testRunStartedId":"10","id":"12","hookId":"5","timestamp":{"seconds":0,"nanos":3000000}}} +{"testRunHookFinished":{"testRunHookStartedId":"12","timestamp":{"seconds":0,"nanos":4000000},"result":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}}}} +{"testCase":{"id":"13","pickleId":"3","testSteps":[{"id":"14","pickleStepId":"2","stepDefinitionIds":["6"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}],"testRunStartedId":"10"}} +{"testCaseStarted":{"id":"15","testCaseId":"13","timestamp":{"seconds":0,"nanos":5000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"15","testStepId":"14","timestamp":{"seconds":0,"nanos":6000000}}} +{"testStepFinished":{"testCaseStartedId":"15","testStepId":"14","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":7000000}}} +{"testCaseFinished":{"testCaseStartedId":"15","timestamp":{"seconds":0,"nanos":8000000},"willBeRetried":false}} +{"testRunHookStarted":{"testRunStartedId":"10","id":"16","hookId":"9","timestamp":{"seconds":0,"nanos":9000000}}} +{"testRunHookFinished":{"testRunHookStartedId":"16","timestamp":{"seconds":0,"nanos":10000000},"result":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}}}} +{"testRunHookStarted":{"testRunStartedId":"10","id":"17","hookId":"8","timestamp":{"seconds":0,"nanos":11000000}}} +{"testRunHookFinished":{"testRunHookStartedId":"17","timestamp":{"seconds":0,"nanos":12000000},"result":{"message":"AfterAll hook went wrong","exception":{"type":"Error","message":"AfterAll hook went wrong","stackTrace":"Error: AfterAll hook went wrong\nsamples/global-hooks-afterall-error/global-hooks-afterall-error.ts:19"},"status":"FAILED","duration":{"seconds":0,"nanos":1000000}}}} +{"testRunHookStarted":{"testRunStartedId":"10","id":"18","hookId":"7","timestamp":{"seconds":0,"nanos":13000000}}} +{"testRunHookFinished":{"testRunHookStartedId":"18","timestamp":{"seconds":0,"nanos":14000000},"result":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}}}} +{"testRunFinished":{"testRunStartedId":"10","timestamp":{"seconds":0,"nanos":15000000},"success":false}} diff --git a/compatibility/global-hooks-afterall-error/global-hooks-afterall-error.ts b/compatibility/global-hooks-afterall-error/global-hooks-afterall-error.ts new file mode 100644 index 00000000..3fe24b79 --- /dev/null +++ b/compatibility/global-hooks-afterall-error/global-hooks-afterall-error.ts @@ -0,0 +1,25 @@ +import { When, BeforeAll, AfterAll } from '@cucumber/fake-cucumber' + +BeforeAll({}, function () { + // no-op +}) + +BeforeAll({}, function () { + // no-op +}) + +When('a step passes', function () { + // no-op +}) + +AfterAll({}, function () { + // no-op +}) + +AfterAll({}, function () { + throw new Error('AfterAll hook went wrong') +}) + +AfterAll({}, function () { + // no-op +}) diff --git a/compatibility/global-hooks-attachments/global-hooks-attachments.cpp b/compatibility/global-hooks-attachments/global-hooks-attachments.cpp new file mode 100644 index 00000000..e4469a18 --- /dev/null +++ b/compatibility/global-hooks-attachments/global-hooks-attachments.cpp @@ -0,0 +1,17 @@ +#include "cucumber_cpp/CucumberCpp.hpp" +#include + +HOOK_BEFORE_ALL() +{ + Attach("Attachment from BeforeAll hook", "text/plain"); +} + +WHEN(R"(a step passes)") +{ + // no-op +} + +HOOK_AFTER_ALL() +{ + Attach("Attachment from AfterAll hook", "text/plain"); +} diff --git a/compatibility/global-hooks-attachments/global-hooks-attachments.feature b/compatibility/global-hooks-attachments/global-hooks-attachments.feature new file mode 100644 index 00000000..d96ccf2b --- /dev/null +++ b/compatibility/global-hooks-attachments/global-hooks-attachments.feature @@ -0,0 +1,5 @@ +Feature: Global hooks with attachments + Attachments can be captured in BeforeAll and AfterAll hooks. + + Scenario: A scenario + When a step passes diff --git a/compatibility/global-hooks-attachments/global-hooks-attachments.ndjson b/compatibility/global-hooks-attachments/global-hooks-attachments.ndjson new file mode 100644 index 00000000..0c174212 --- /dev/null +++ b/compatibility/global-hooks-attachments/global-hooks-attachments.ndjson @@ -0,0 +1,20 @@ +{"meta":{"protocolVersion":"31.0.0","implementation":{"name":"fake-cucumber","version":"123.45.6"},"cpu":{"name":"arm64"},"os":{"name":"darwin","version":"24.5.0"},"runtime":{"name":"Node.js","version":"24.4.1"},"ci":{"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429","buildNumber":"154666429","git":{"revision":"99684bcacf01d95875834d87903dcb072306c9ad","remote":"https://github.com/cucumber-ltd/shouty.rb.git","branch":"main"}}}} +{"source":{"data":"Feature: Global hooks with attachments\n Attachments can be captured in BeforeAll and AfterAll hooks.\n\n Scenario: A scenario\n When a step passes\n","uri":"samples/global-hooks-attachments/global-hooks-attachments.feature","mediaType":"text/x.cucumber.gherkin+plain"}} +{"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"Global hooks with attachments","description":" Attachments can be captured in BeforeAll and AfterAll hooks.","children":[{"scenario":{"id":"1","tags":[],"location":{"line":4,"column":3},"keyword":"Scenario","name":"A scenario","description":"","steps":[{"id":"0","location":{"line":5,"column":5},"keyword":"When ","keywordType":"Action","text":"a step passes"}],"examples":[]}}]},"comments":[],"uri":"samples/global-hooks-attachments/global-hooks-attachments.feature"}} +{"pickle":{"id":"3","uri":"samples/global-hooks-attachments/global-hooks-attachments.feature","location":{"line":4,"column":3},"astNodeIds":["1"],"tags":[],"name":"A scenario","language":"en","steps":[{"id":"2","text":"a step passes","type":"Action","astNodeIds":["0"]}]}} +{"stepDefinition":{"id":"5","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a step passes"},"sourceReference":{"uri":"samples/global-hooks-attachments/global-hooks-attachments.ts","location":{"line":7}}}} +{"hook":{"id":"4","type":"BEFORE_TEST_RUN","sourceReference":{"uri":"samples/global-hooks-attachments/global-hooks-attachments.ts","location":{"line":3}}}} +{"hook":{"id":"6","type":"AFTER_TEST_RUN","sourceReference":{"uri":"samples/global-hooks-attachments/global-hooks-attachments.ts","location":{"line":11}}}} +{"testRunStarted":{"id":"7","timestamp":{"seconds":0,"nanos":0}}} +{"testRunHookStarted":{"testRunStartedId":"7","id":"8","hookId":"4","timestamp":{"seconds":0,"nanos":1000000}}} +{"attachment":{"testRunHookStartedId":"8","body":"Attachment from BeforeAll hook","contentEncoding":"IDENTITY","mediaType":"text/plain","timestamp":{"seconds":0,"nanos":2000000}}} +{"testRunHookFinished":{"testRunHookStartedId":"8","timestamp":{"seconds":0,"nanos":3000000},"result":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}}}} +{"testCase":{"id":"9","pickleId":"3","testSteps":[{"id":"10","pickleStepId":"2","stepDefinitionIds":["5"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}],"testRunStartedId":"7"}} +{"testCaseStarted":{"id":"11","testCaseId":"9","timestamp":{"seconds":0,"nanos":4000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"11","testStepId":"10","timestamp":{"seconds":0,"nanos":5000000}}} +{"testStepFinished":{"testCaseStartedId":"11","testStepId":"10","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":6000000}}} +{"testCaseFinished":{"testCaseStartedId":"11","timestamp":{"seconds":0,"nanos":7000000},"willBeRetried":false}} +{"testRunHookStarted":{"testRunStartedId":"7","id":"12","hookId":"6","timestamp":{"seconds":0,"nanos":8000000}}} +{"attachment":{"testRunHookStartedId":"12","body":"Attachment from AfterAll hook","contentEncoding":"IDENTITY","mediaType":"text/plain","timestamp":{"seconds":0,"nanos":9000000}}} +{"testRunHookFinished":{"testRunHookStartedId":"12","timestamp":{"seconds":0,"nanos":10000000},"result":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}}}} +{"testRunFinished":{"testRunStartedId":"7","timestamp":{"seconds":0,"nanos":11000000},"success":true}} diff --git a/compatibility/global-hooks-attachments/global-hooks-attachments.ts b/compatibility/global-hooks-attachments/global-hooks-attachments.ts new file mode 100644 index 00000000..77bbdbee --- /dev/null +++ b/compatibility/global-hooks-attachments/global-hooks-attachments.ts @@ -0,0 +1,13 @@ +import { When, BeforeAll, AfterAll } from '@cucumber/fake-cucumber' + +BeforeAll({}, async function () { + await this.attach('Attachment from BeforeAll hook', 'text/plain') +}) + +When('a step passes', function () { + // no-op +}) + +AfterAll({}, async function () { + await this.attach('Attachment from AfterAll hook', 'text/plain') +}) diff --git a/compatibility/global-hooks-beforeall-error/global-hooks-beforeall-error.cpp b/compatibility/global-hooks-beforeall-error/global-hooks-beforeall-error.cpp new file mode 100644 index 00000000..ca642edd --- /dev/null +++ b/compatibility/global-hooks-beforeall-error/global-hooks-beforeall-error.cpp @@ -0,0 +1,32 @@ +#include "cucumber_cpp/CucumberCpp.hpp" +#include + +HOOK_BEFORE_ALL() +{ + // no-op +} + +HOOK_BEFORE_ALL() +{ + ASSERT_THAT(true, false); +} + +HOOK_BEFORE_ALL() +{ + // no-op +} + +WHEN(R"(a step passes)") +{ + // no-op +} + +HOOK_AFTER_ALL() +{ + // no-op +} + +HOOK_AFTER_ALL() +{ + // no-op +} diff --git a/compatibility/global-hooks-beforeall-error/global-hooks-beforeall-error.feature b/compatibility/global-hooks-beforeall-error/global-hooks-beforeall-error.feature new file mode 100644 index 00000000..2b42678e --- /dev/null +++ b/compatibility/global-hooks-beforeall-error/global-hooks-beforeall-error.feature @@ -0,0 +1,6 @@ +Feature: Global hooks - BeforeAll error + Errors in BeforeAll hooks cause the whole test run to fail. Test cases will not be executed. The remaining BeforeAll + hooks will still run, along with all AfterAll hooks, in an effort to clean up resources as well as possible. + + Scenario: A passing scenario + When a step passes diff --git a/compatibility/global-hooks-beforeall-error/global-hooks-beforeall-error.ndjson b/compatibility/global-hooks-beforeall-error/global-hooks-beforeall-error.ndjson new file mode 100644 index 00000000..a8a2635e --- /dev/null +++ b/compatibility/global-hooks-beforeall-error/global-hooks-beforeall-error.ndjson @@ -0,0 +1,22 @@ +{"meta":{"protocolVersion":"31.0.0","implementation":{"name":"fake-cucumber","version":"123.45.6"},"cpu":{"name":"arm64"},"os":{"name":"darwin","version":"24.5.0"},"runtime":{"name":"Node.js","version":"24.4.1"},"ci":{"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429","buildNumber":"154666429","git":{"revision":"99684bcacf01d95875834d87903dcb072306c9ad","remote":"https://github.com/cucumber-ltd/shouty.rb.git","branch":"main"}}}} +{"source":{"data":"Feature: Global hooks - BeforeAll error\n Errors in BeforeAll hooks cause the whole test run to fail. Test cases will not be executed. The remaining BeforeAll\n hooks will still run, along with all AfterAll hooks, in an effort to clean up resources as well as possible.\n\n Scenario: A passing scenario\n When a step passes\n","uri":"samples/global-hooks-beforeall-error/global-hooks-beforeall-error.feature","mediaType":"text/x.cucumber.gherkin+plain"}} +{"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"Global hooks - BeforeAll error","description":" Errors in BeforeAll hooks cause the whole test run to fail. Test cases will not be executed. The remaining BeforeAll\n hooks will still run, along with all AfterAll hooks, in an effort to clean up resources as well as possible.","children":[{"scenario":{"id":"1","tags":[],"location":{"line":5,"column":3},"keyword":"Scenario","name":"A passing scenario","description":"","steps":[{"id":"0","location":{"line":6,"column":5},"keyword":"When ","keywordType":"Action","text":"a step passes"}],"examples":[]}}]},"comments":[],"uri":"samples/global-hooks-beforeall-error/global-hooks-beforeall-error.feature"}} +{"pickle":{"id":"3","uri":"samples/global-hooks-beforeall-error/global-hooks-beforeall-error.feature","location":{"line":5,"column":3},"astNodeIds":["1"],"tags":[],"name":"A passing scenario","language":"en","steps":[{"id":"2","text":"a step passes","type":"Action","astNodeIds":["0"]}]}} +{"stepDefinition":{"id":"7","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a step passes"},"sourceReference":{"uri":"samples/global-hooks-beforeall-error/global-hooks-beforeall-error.ts","location":{"line":15}}}} +{"hook":{"id":"4","type":"BEFORE_TEST_RUN","sourceReference":{"uri":"samples/global-hooks-beforeall-error/global-hooks-beforeall-error.ts","location":{"line":3}}}} +{"hook":{"id":"5","type":"BEFORE_TEST_RUN","sourceReference":{"uri":"samples/global-hooks-beforeall-error/global-hooks-beforeall-error.ts","location":{"line":7}}}} +{"hook":{"id":"6","type":"BEFORE_TEST_RUN","sourceReference":{"uri":"samples/global-hooks-beforeall-error/global-hooks-beforeall-error.ts","location":{"line":11}}}} +{"hook":{"id":"8","type":"AFTER_TEST_RUN","sourceReference":{"uri":"samples/global-hooks-beforeall-error/global-hooks-beforeall-error.ts","location":{"line":19}}}} +{"hook":{"id":"9","type":"AFTER_TEST_RUN","sourceReference":{"uri":"samples/global-hooks-beforeall-error/global-hooks-beforeall-error.ts","location":{"line":23}}}} +{"testRunStarted":{"id":"10","timestamp":{"seconds":0,"nanos":0}}} +{"testRunHookStarted":{"testRunStartedId":"10","id":"11","hookId":"4","timestamp":{"seconds":0,"nanos":1000000}}} +{"testRunHookFinished":{"testRunHookStartedId":"11","timestamp":{"seconds":0,"nanos":2000000},"result":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}}}} +{"testRunHookStarted":{"testRunStartedId":"10","id":"12","hookId":"5","timestamp":{"seconds":0,"nanos":3000000}}} +{"testRunHookFinished":{"testRunHookStartedId":"12","timestamp":{"seconds":0,"nanos":4000000},"result":{"message":"BeforeAll hook went wrong","exception":{"type":"Error","message":"BeforeAll hook went wrong","stackTrace":"Error: BeforeAll hook went wrong\nsamples/global-hooks-beforeall-error/global-hooks-beforeall-error.ts:7"},"status":"FAILED","duration":{"seconds":0,"nanos":1000000}}}} +{"testRunHookStarted":{"testRunStartedId":"10","id":"13","hookId":"6","timestamp":{"seconds":0,"nanos":5000000}}} +{"testRunHookFinished":{"testRunHookStartedId":"13","timestamp":{"seconds":0,"nanos":6000000},"result":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}}}} +{"testRunHookStarted":{"testRunStartedId":"10","id":"14","hookId":"9","timestamp":{"seconds":0,"nanos":7000000}}} +{"testRunHookFinished":{"testRunHookStartedId":"14","timestamp":{"seconds":0,"nanos":8000000},"result":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}}}} +{"testRunHookStarted":{"testRunStartedId":"10","id":"15","hookId":"8","timestamp":{"seconds":0,"nanos":9000000}}} +{"testRunHookFinished":{"testRunHookStartedId":"15","timestamp":{"seconds":0,"nanos":10000000},"result":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}}}} +{"testRunFinished":{"testRunStartedId":"10","timestamp":{"seconds":0,"nanos":11000000},"success":false}} diff --git a/compatibility/global-hooks-beforeall-error/global-hooks-beforeall-error.ts b/compatibility/global-hooks-beforeall-error/global-hooks-beforeall-error.ts new file mode 100644 index 00000000..3a526374 --- /dev/null +++ b/compatibility/global-hooks-beforeall-error/global-hooks-beforeall-error.ts @@ -0,0 +1,25 @@ +import { When, BeforeAll, AfterAll } from '@cucumber/fake-cucumber' + +BeforeAll({}, function () { + // no-op +}) + +BeforeAll({}, function () { + throw new Error('BeforeAll hook went wrong') +}) + +BeforeAll({}, function () { + // no-op +}) + +When('a step passes', function () { + // no-op +}) + +AfterAll({}, function () { + // no-op +}) + +AfterAll({}, function () { + // no-op +}) diff --git a/compatibility/global-hooks/global-hooks.cpp b/compatibility/global-hooks/global-hooks.cpp new file mode 100644 index 00000000..29ac1fd4 --- /dev/null +++ b/compatibility/global-hooks/global-hooks.cpp @@ -0,0 +1,32 @@ +#include "cucumber_cpp/CucumberCpp.hpp" +#include + +HOOK_BEFORE_ALL() +{ + // no-op +} + +HOOK_BEFORE_ALL() +{ + // no-op +} + +WHEN(R"(a step passes)") +{ + // no-op +} + +WHEN(R"(a step fails)") +{ + ASSERT_THAT(true, false); +} + +HOOK_AFTER_ALL() +{ + // no-op +} + +HOOK_AFTER_ALL() +{ + // no-op +} diff --git a/compatibility/global-hooks/global-hooks.feature b/compatibility/global-hooks/global-hooks.feature new file mode 100644 index 00000000..715a76f3 --- /dev/null +++ b/compatibility/global-hooks/global-hooks.feature @@ -0,0 +1,10 @@ +Feature: Global hooks + Hooks can be at the test run level, so they run once before or after all test cases. + + AfterAll hooks are executed in reverse order of definition. + + Scenario: A passing scenario + When a step passes + + Scenario: A failing scenario + When a step fails diff --git a/compatibility/global-hooks/global-hooks.ndjson b/compatibility/global-hooks/global-hooks.ndjson new file mode 100644 index 00000000..baa7783a --- /dev/null +++ b/compatibility/global-hooks/global-hooks.ndjson @@ -0,0 +1,31 @@ +{"meta":{"protocolVersion":"31.0.0","implementation":{"name":"fake-cucumber","version":"123.45.6"},"cpu":{"name":"arm64"},"os":{"name":"darwin","version":"24.5.0"},"runtime":{"name":"Node.js","version":"24.4.1"},"ci":{"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429","buildNumber":"154666429","git":{"revision":"99684bcacf01d95875834d87903dcb072306c9ad","remote":"https://github.com/cucumber-ltd/shouty.rb.git","branch":"main"}}}} +{"source":{"data":"Feature: Global hooks\n Hooks can be at the test run level, so they run once before or after all test cases.\n\n AfterAll hooks are executed in reverse order of definition.\n\n Scenario: A passing scenario\n When a step passes\n\n Scenario: A failing scenario\n When a step fails\n","uri":"samples/global-hooks/global-hooks.feature","mediaType":"text/x.cucumber.gherkin+plain"}} +{"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"Global hooks","description":" Hooks can be at the test run level, so they run once before or after all test cases.\n\n AfterAll hooks are executed in reverse order of definition.","children":[{"scenario":{"id":"1","tags":[],"location":{"line":6,"column":3},"keyword":"Scenario","name":"A passing scenario","description":"","steps":[{"id":"0","location":{"line":7,"column":5},"keyword":"When ","keywordType":"Action","text":"a step passes"}],"examples":[]}},{"scenario":{"id":"3","tags":[],"location":{"line":9,"column":3},"keyword":"Scenario","name":"A failing scenario","description":"","steps":[{"id":"2","location":{"line":10,"column":5},"keyword":"When ","keywordType":"Action","text":"a step fails"}],"examples":[]}}]},"comments":[],"uri":"samples/global-hooks/global-hooks.feature"}} +{"pickle":{"id":"5","uri":"samples/global-hooks/global-hooks.feature","location":{"line":6,"column":3},"astNodeIds":["1"],"tags":[],"name":"A passing scenario","language":"en","steps":[{"id":"4","text":"a step passes","type":"Action","astNodeIds":["0"]}]}} +{"pickle":{"id":"7","uri":"samples/global-hooks/global-hooks.feature","location":{"line":9,"column":3},"astNodeIds":["3"],"tags":[],"name":"A failing scenario","language":"en","steps":[{"id":"6","text":"a step fails","type":"Action","astNodeIds":["2"]}]}} +{"stepDefinition":{"id":"10","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a step passes"},"sourceReference":{"uri":"samples/global-hooks/global-hooks.ts","location":{"line":11}}}} +{"stepDefinition":{"id":"11","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a step fails"},"sourceReference":{"uri":"samples/global-hooks/global-hooks.ts","location":{"line":15}}}} +{"hook":{"id":"8","type":"BEFORE_TEST_RUN","sourceReference":{"uri":"samples/global-hooks/global-hooks.ts","location":{"line":3}}}} +{"hook":{"id":"9","type":"BEFORE_TEST_RUN","sourceReference":{"uri":"samples/global-hooks/global-hooks.ts","location":{"line":7}}}} +{"hook":{"id":"12","type":"AFTER_TEST_RUN","sourceReference":{"uri":"samples/global-hooks/global-hooks.ts","location":{"line":19}}}} +{"hook":{"id":"13","type":"AFTER_TEST_RUN","sourceReference":{"uri":"samples/global-hooks/global-hooks.ts","location":{"line":23}}}} +{"testRunStarted":{"id":"14","timestamp":{"seconds":0,"nanos":0}}} +{"testRunHookStarted":{"testRunStartedId":"14","id":"15","hookId":"8","timestamp":{"seconds":0,"nanos":1000000}}} +{"testRunHookFinished":{"testRunHookStartedId":"15","timestamp":{"seconds":0,"nanos":2000000},"result":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}}}} +{"testRunHookStarted":{"testRunStartedId":"14","id":"16","hookId":"9","timestamp":{"seconds":0,"nanos":3000000}}} +{"testRunHookFinished":{"testRunHookStartedId":"16","timestamp":{"seconds":0,"nanos":4000000},"result":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}}}} +{"testCase":{"id":"17","pickleId":"5","testSteps":[{"id":"18","pickleStepId":"4","stepDefinitionIds":["10"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}],"testRunStartedId":"14"}} +{"testCase":{"id":"19","pickleId":"7","testSteps":[{"id":"20","pickleStepId":"6","stepDefinitionIds":["11"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}],"testRunStartedId":"14"}} +{"testCaseStarted":{"id":"21","testCaseId":"17","timestamp":{"seconds":0,"nanos":5000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"21","testStepId":"18","timestamp":{"seconds":0,"nanos":6000000}}} +{"testStepFinished":{"testCaseStartedId":"21","testStepId":"18","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":7000000}}} +{"testCaseFinished":{"testCaseStartedId":"21","timestamp":{"seconds":0,"nanos":8000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"22","testCaseId":"19","timestamp":{"seconds":0,"nanos":9000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"22","testStepId":"20","timestamp":{"seconds":0,"nanos":10000000}}} +{"testStepFinished":{"testCaseStartedId":"22","testStepId":"20","testStepResult":{"message":"Exception in step","exception":{"type":"Error","message":"Exception in step","stackTrace":"Error: Exception in step\nsamples/global-hooks/global-hooks.feature:10"},"status":"FAILED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":11000000}}} +{"testCaseFinished":{"testCaseStartedId":"22","timestamp":{"seconds":0,"nanos":12000000},"willBeRetried":false}} +{"testRunHookStarted":{"testRunStartedId":"14","id":"23","hookId":"13","timestamp":{"seconds":0,"nanos":13000000}}} +{"testRunHookFinished":{"testRunHookStartedId":"23","timestamp":{"seconds":0,"nanos":14000000},"result":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}}}} +{"testRunHookStarted":{"testRunStartedId":"14","id":"24","hookId":"12","timestamp":{"seconds":0,"nanos":15000000}}} +{"testRunHookFinished":{"testRunHookStartedId":"24","timestamp":{"seconds":0,"nanos":16000000},"result":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}}}} +{"testRunFinished":{"testRunStartedId":"14","timestamp":{"seconds":0,"nanos":17000000},"success":false}} diff --git a/compatibility/global-hooks/global-hooks.ts b/compatibility/global-hooks/global-hooks.ts new file mode 100644 index 00000000..c86714a5 --- /dev/null +++ b/compatibility/global-hooks/global-hooks.ts @@ -0,0 +1,25 @@ +import { When, BeforeAll, AfterAll } from '@cucumber/fake-cucumber' + +BeforeAll({}, function () { + // no-op +}) + +BeforeAll({}, function () { + // no-op +}) + +When('a step passes', function () { + // no-op +}) + +When('a step fails', function () { + throw new Error('Exception in step') +}) + +AfterAll({}, function () { + // no-op +}) + +AfterAll({}, function () { + // no-op +}) diff --git a/compatibility/hooks-attachment/.gitattributes b/compatibility/hooks-attachment/.gitattributes new file mode 100644 index 00000000..6ea7b312 --- /dev/null +++ b/compatibility/hooks-attachment/.gitattributes @@ -0,0 +1,4 @@ +# SVG files are plain text. So git will change line endings on windows. +# Because we expect the image to have been encoded in base64 with lf rather than +# crlf this is not desirable. +cucumber.svg eol=lf diff --git a/compatibility/hooks-attachment/cucumber.svg b/compatibility/hooks-attachment/cucumber.svg new file mode 100644 index 00000000..e76ff7fa --- /dev/null +++ b/compatibility/hooks-attachment/cucumber.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/compatibility/hooks-attachment/hooks-attachment.cpp b/compatibility/hooks-attachment/hooks-attachment.cpp new file mode 100644 index 00000000..e2bca714 --- /dev/null +++ b/compatibility/hooks-attachment/hooks-attachment.cpp @@ -0,0 +1,28 @@ +#include "cucumber_cpp/CucumberCpp.hpp" +#include +#include +#include +#include +#include + +namespace +{ + const std::filesystem::path currentCompileDir = std::filesystem::path{ std::source_location::current().file_name() }.parent_path(); +} + +HOOK_BEFORE_SCENARIO() +{ + std::ifstream svgFile{ currentCompileDir / "cucumber.svg", std::ios::binary }; + Attach(svgFile, "image/svg+xml"); +} + +WHEN(R"(a step passes)") +{ + // no-op +} + +HOOK_AFTER_SCENARIO() +{ + std::ifstream svgFile{ currentCompileDir / "cucumber.svg", std::ios::binary }; + Attach(svgFile, "image/svg+xml"); +} diff --git a/compatibility/hooks-attachment/hooks-attachment.feature b/compatibility/hooks-attachment/hooks-attachment.feature new file mode 100644 index 00000000..721e3470 --- /dev/null +++ b/compatibility/hooks-attachment/hooks-attachment.feature @@ -0,0 +1,7 @@ +Feature: Hooks - Attachments + Hooks are special steps that run before or after each scenario's steps. + + Like regular steps, it is possible to attach a file to the output. + + Scenario: With an valid attachment in the hook and a passed step + When a step passes diff --git a/compatibility/hooks-attachment/hooks-attachment.ndjson b/compatibility/hooks-attachment/hooks-attachment.ndjson new file mode 100644 index 00000000..b8383ab0 --- /dev/null +++ b/compatibility/hooks-attachment/hooks-attachment.ndjson @@ -0,0 +1,20 @@ +{"meta":{"protocolVersion":"31.0.0","implementation":{"name":"fake-cucumber","version":"123.45.6"},"cpu":{"name":"arm64"},"os":{"name":"darwin","version":"24.5.0"},"runtime":{"name":"Node.js","version":"24.4.1"},"ci":{"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429","buildNumber":"154666429","git":{"revision":"99684bcacf01d95875834d87903dcb072306c9ad","remote":"https://github.com/cucumber-ltd/shouty.rb.git","branch":"main"}}}} +{"source":{"data":"Feature: Hooks - Attachments\n Hooks are special steps that run before or after each scenario's steps.\n\n Like regular steps, it is possible to attach a file to the output.\n\n Scenario: With an valid attachment in the hook and a passed step\n When a step passes\n","uri":"samples/hooks-attachment/hooks-attachment.feature","mediaType":"text/x.cucumber.gherkin+plain"}} +{"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"Hooks - Attachments","description":" Hooks are special steps that run before or after each scenario's steps.\n\n Like regular steps, it is possible to attach a file to the output.","children":[{"scenario":{"id":"1","tags":[],"location":{"line":6,"column":3},"keyword":"Scenario","name":"With an valid attachment in the hook and a passed step","description":"","steps":[{"id":"0","location":{"line":7,"column":5},"keyword":"When ","keywordType":"Action","text":"a step passes"}],"examples":[]}}]},"comments":[],"uri":"samples/hooks-attachment/hooks-attachment.feature"}} +{"pickle":{"id":"3","uri":"samples/hooks-attachment/hooks-attachment.feature","location":{"line":6,"column":3},"astNodeIds":["1"],"tags":[],"name":"With an valid attachment in the hook and a passed step","language":"en","steps":[{"id":"2","text":"a step passes","type":"Action","astNodeIds":["0"]}]}} +{"stepDefinition":{"id":"5","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a step passes"},"sourceReference":{"uri":"samples/hooks-attachment/hooks-attachment.ts","location":{"line":11}}}} +{"hook":{"id":"4","type":"BEFORE_TEST_CASE","sourceReference":{"uri":"samples/hooks-attachment/hooks-attachment.ts","location":{"line":4}}}} +{"hook":{"id":"6","type":"AFTER_TEST_CASE","sourceReference":{"uri":"samples/hooks-attachment/hooks-attachment.ts","location":{"line":15}}}} +{"testRunStarted":{"id":"7","timestamp":{"seconds":0,"nanos":0}}} +{"testCase":{"id":"8","pickleId":"3","testSteps":[{"id":"9","hookId":"4"},{"id":"10","pickleStepId":"2","stepDefinitionIds":["5"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"11","hookId":"6"}],"testRunStartedId":"7"}} +{"testCaseStarted":{"id":"12","testCaseId":"8","timestamp":{"seconds":0,"nanos":1000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"12","testStepId":"9","timestamp":{"seconds":0,"nanos":2000000}}} +{"attachment":{"testCaseStartedId":"12","testStepId":"9","body":"PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGNsYXNzPSJtbC0zIG1sLW1kLTAiIHZpZXdCb3g9IjAgMCA0MC41OSA0Ni4zMSIgd2lkdGg9IjQwLjU5IiBoZWlnaHQ9IjQ2LjMxIj4KICAgIDxnPgogICAgICAgIDxwYXRoIGZpbGw9IiMyM2Q5NmMiIGZpbGwtcnVsZT0iZXZlbm9kZCIgZD0iTTMwLjI4MyAzLjY0NXEtLjUyOC0uMzE3LTEuMDgtLjU5M2ExNi4xNjQgMTYuMTY0IDAgMDAtMS4xNTQtLjUxOGMtLjEyNC0uMDUyLS4yNDctLjEtLjM3Mi0uMTQ5LS4zNDMtLjEyNy0uNjg5LS4yNjgtMS4wNDItLjM3MWExOS40MjcgMTkuNDI3IDAgMTAtOS43OTIgMzcuNTF2NS41NmMxMS42NzYtMS43NTMgMjIuMDE2LTEwLjk3OSAyMi43ODctMjMuMDkzLjQ1OS03LjI4OS0zLjE5My0xNC43My05LjM0Ny0xOC4zNDZ6Ii8+CiAgICAgICAgPHBhdGggZmlsbD0iIzE3MzY0NyIgZD0iTTE1Ljc4NyA0Ni4zMDd2LTUuOTM1QTIwLjQ3MiAyMC40NzIgMCAxMTI2Ljk1OSAxLjAxNWMuMjc0LjA4LjU1Ny4xODcuODMyLjI5MWwuMjQ4LjA5M2MuMTY1LjA2NC4yOTEuMTEzLjQxNy4xNjcuMzQ4LjEzNy43MzkuMzEzIDEuMjA4LjU0M3EuNTg5LjI5NSAxLjE1My42MzNjNi4zOTMgMy43NTYgMTAuMzU0IDExLjUxOCA5Ljg1NyAxOS4zMTYtLjc2MyAxMi0xMC43MjIgMjIuMTIyLTIzLjY3OSAyNC4wNjd6bTQuOC00NC4yMTRoLS4wMjZhMTguMzY2IDE4LjM2NiAwIDAwLTMuNTI0IDM2LjQwOGwuODUuMTY1djUuMThjMTEuMzkyLTIuMjI0IDIwLjAwOS0xMS4yNzIgMjAuNjg2LTIxLjkyMi40NDgtNy4wMzMtMy4xLTE0LjAxOC04LjgzLTE3LjM4M2wtLjAwOC0uMDA1QTE0LjY5MSAxNC42OTEgMCAwMDI3LjY1NCAzLjVhNS43NCA1Ljc0IDAgMDAtLjM0NC0uMTM4bC0uMjctLjFhOS40OSA5LjQ5IDAgMDAtLjcwOC0uMjQ5IDE4LjQyNSAxOC40MjUgMCAwMC01Ljc0My0uOTJ6Ii8+CiAgICAgICAgPHBhdGggZmlsbD0iIzE3MzY0NyIgZmlsbC1ydWxlPSJldmVub2RkIiBkPSJNMTYuNjY2IDEwLjU4YTEuOCAxLjggMCAwMTEuNTgzLjYwOCA0LjE4NCA0LjE4NCAwIDAxLjcyOCAxLjEwN2MuNjQ1IDEuNDIyIDEuMDI3IDMuNDYxLjIzIDQuNjA1YTYuMzM0IDYuMzM0IDAgMDEtMy45ODEtMy4wODcgMy4yMzYgMy4yMzYgMCAwMS0uMzQ3LTEuMzM5IDEuOTU3IDEuOTU3IDAgMDExLjc4Ny0xLjg5NHptLTUuNjgzIDguMDI1YTcuNzQyIDcuNzQyIDAgMDAxLjIxOC43MzcgNS43ODkgNS43ODkgMCAwMDQuODgzLS4xMzggNi4xMTYgNi4xMTYgMCAwMC0zLjM0NS0zLjQ1IDMuNjY0IDMuNjY0IDAgMDAtMS40NDItLjMyMSAxLjg4NCAxLjg4NCAwIDAwLS4zMTkgMCAxLjc2NiAxLjc2NiAwIDAwLS45OTUgMy4xNzJ6bTYuMSAzLjQzM2MtLjc3Ny0uNTE4LTIuMzc5LS4zMDktMy4zMTItLjI5MmE0LjQxNiA0LjQxNiAwIDAwLTEuNjY2LjM1MiAzLjUgMy41IDAgMDAtMS4yMTguNzM4IDEuODE3IDEuODE3IDAgMDAxLjQwOSAzLjE3MSAzLjMgMy4zIDAgMDAxLjQ0Mi0uMzIxYzEuNDM2LS42MiAzLjE0MS0yLjMyIDMuMzQ2LTMuNjQ4em0yLjYxIDJhNi41NTYgNi41NTYgMCAwMC0zLjcyNCAzLjUwNiAzLjA5MSAzLjA5MSAwIDAwLS4zMjEgMS4zMTQgMS45MDcgMS45MDcgMCAwMDMuMyAxLjM0NiA3LjQyMiA3LjQyMiAwIDAwLjctMS4yMThjLjYyMS0xLjMzMy44NjYtMy43Mi4wNDYtNC45NDh6bTIuNTU3LTcuMTY3YTUuOTQxIDUuOTQxIDAgMDAzLjctMy4xNjcgMy4yNDMgMy4yNDMgMCAwMC4zMTktMS4zNDYgMS45MTUgMS45MTUgMCAwMC0xLjc5NC0xLjk1NCAxLjgzMiAxLjgzMiAwIDAwLTEuNi42NDEgNy4zODIgNy4zODIgMCAwMC0uNzA1IDEuMjE4Yy0uNjIgMS40MzQtLjg0MiAzLjQ4LjA4MSA0LjYwM3ptNC4yMDggMTIuMTE1YTMuMjQ0IDMuMjQ0IDAgMDAtLjMyMS0xLjM0NSA1Ljg2OSA1Ljg2OSAwIDAwLTMuNTU0LTMuMjY5IDUuMzg2IDUuMzg2IDAgMDAtLjIyNiA0LjcxMSA0LjE0NyA0LjE0NyAwIDAwLjcgMS4xMjFjMS4xMzMgMS4yMyAzLjUwNS4zMiAzLjQwMi0xLjIxOHptNC4yLTYuMjhhNy40NjYgNy40NjYgMCAwMC0xLjIxNy0uNyA0LjQyNSA0LjQyNSAwIDAwLTEuNjY2LS4zNTIgNi40IDYuNCAwIDAwLTMuMTg4LjU1NSA1Ljk1OSA1Ljk1OSAwIDAwMy4zMTYgMy4zODYgMy42NzIgMy42NzIgMCAwMDEuNDQyLjMyIDEuOCAxLjggMCAwMDEuMzEtMy4yMDl6Ii8+CiAgICA8L2c+Cjwvc3ZnPg==","contentEncoding":"BASE64","mediaType":"image/svg+xml","timestamp":{"seconds":0,"nanos":3000000}}} +{"testStepFinished":{"testCaseStartedId":"12","testStepId":"9","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":4000000}}} +{"testStepStarted":{"testCaseStartedId":"12","testStepId":"10","timestamp":{"seconds":0,"nanos":5000000}}} +{"testStepFinished":{"testCaseStartedId":"12","testStepId":"10","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":6000000}}} +{"testStepStarted":{"testCaseStartedId":"12","testStepId":"11","timestamp":{"seconds":0,"nanos":7000000}}} +{"attachment":{"testCaseStartedId":"12","testStepId":"11","body":"PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGNsYXNzPSJtbC0zIG1sLW1kLTAiIHZpZXdCb3g9IjAgMCA0MC41OSA0Ni4zMSIgd2lkdGg9IjQwLjU5IiBoZWlnaHQ9IjQ2LjMxIj4KICAgIDxnPgogICAgICAgIDxwYXRoIGZpbGw9IiMyM2Q5NmMiIGZpbGwtcnVsZT0iZXZlbm9kZCIgZD0iTTMwLjI4MyAzLjY0NXEtLjUyOC0uMzE3LTEuMDgtLjU5M2ExNi4xNjQgMTYuMTY0IDAgMDAtMS4xNTQtLjUxOGMtLjEyNC0uMDUyLS4yNDctLjEtLjM3Mi0uMTQ5LS4zNDMtLjEyNy0uNjg5LS4yNjgtMS4wNDItLjM3MWExOS40MjcgMTkuNDI3IDAgMTAtOS43OTIgMzcuNTF2NS41NmMxMS42NzYtMS43NTMgMjIuMDE2LTEwLjk3OSAyMi43ODctMjMuMDkzLjQ1OS03LjI4OS0zLjE5My0xNC43My05LjM0Ny0xOC4zNDZ6Ii8+CiAgICAgICAgPHBhdGggZmlsbD0iIzE3MzY0NyIgZD0iTTE1Ljc4NyA0Ni4zMDd2LTUuOTM1QTIwLjQ3MiAyMC40NzIgMCAxMTI2Ljk1OSAxLjAxNWMuMjc0LjA4LjU1Ny4xODcuODMyLjI5MWwuMjQ4LjA5M2MuMTY1LjA2NC4yOTEuMTEzLjQxNy4xNjcuMzQ4LjEzNy43MzkuMzEzIDEuMjA4LjU0M3EuNTg5LjI5NSAxLjE1My42MzNjNi4zOTMgMy43NTYgMTAuMzU0IDExLjUxOCA5Ljg1NyAxOS4zMTYtLjc2MyAxMi0xMC43MjIgMjIuMTIyLTIzLjY3OSAyNC4wNjd6bTQuOC00NC4yMTRoLS4wMjZhMTguMzY2IDE4LjM2NiAwIDAwLTMuNTI0IDM2LjQwOGwuODUuMTY1djUuMThjMTEuMzkyLTIuMjI0IDIwLjAwOS0xMS4yNzIgMjAuNjg2LTIxLjkyMi40NDgtNy4wMzMtMy4xLTE0LjAxOC04LjgzLTE3LjM4M2wtLjAwOC0uMDA1QTE0LjY5MSAxNC42OTEgMCAwMDI3LjY1NCAzLjVhNS43NCA1Ljc0IDAgMDAtLjM0NC0uMTM4bC0uMjctLjFhOS40OSA5LjQ5IDAgMDAtLjcwOC0uMjQ5IDE4LjQyNSAxOC40MjUgMCAwMC01Ljc0My0uOTJ6Ii8+CiAgICAgICAgPHBhdGggZmlsbD0iIzE3MzY0NyIgZmlsbC1ydWxlPSJldmVub2RkIiBkPSJNMTYuNjY2IDEwLjU4YTEuOCAxLjggMCAwMTEuNTgzLjYwOCA0LjE4NCA0LjE4NCAwIDAxLjcyOCAxLjEwN2MuNjQ1IDEuNDIyIDEuMDI3IDMuNDYxLjIzIDQuNjA1YTYuMzM0IDYuMzM0IDAgMDEtMy45ODEtMy4wODcgMy4yMzYgMy4yMzYgMCAwMS0uMzQ3LTEuMzM5IDEuOTU3IDEuOTU3IDAgMDExLjc4Ny0xLjg5NHptLTUuNjgzIDguMDI1YTcuNzQyIDcuNzQyIDAgMDAxLjIxOC43MzcgNS43ODkgNS43ODkgMCAwMDQuODgzLS4xMzggNi4xMTYgNi4xMTYgMCAwMC0zLjM0NS0zLjQ1IDMuNjY0IDMuNjY0IDAgMDAtMS40NDItLjMyMSAxLjg4NCAxLjg4NCAwIDAwLS4zMTkgMCAxLjc2NiAxLjc2NiAwIDAwLS45OTUgMy4xNzJ6bTYuMSAzLjQzM2MtLjc3Ny0uNTE4LTIuMzc5LS4zMDktMy4zMTItLjI5MmE0LjQxNiA0LjQxNiAwIDAwLTEuNjY2LjM1MiAzLjUgMy41IDAgMDAtMS4yMTguNzM4IDEuODE3IDEuODE3IDAgMDAxLjQwOSAzLjE3MSAzLjMgMy4zIDAgMDAxLjQ0Mi0uMzIxYzEuNDM2LS42MiAzLjE0MS0yLjMyIDMuMzQ2LTMuNjQ4em0yLjYxIDJhNi41NTYgNi41NTYgMCAwMC0zLjcyNCAzLjUwNiAzLjA5MSAzLjA5MSAwIDAwLS4zMjEgMS4zMTQgMS45MDcgMS45MDcgMCAwMDMuMyAxLjM0NiA3LjQyMiA3LjQyMiAwIDAwLjctMS4yMThjLjYyMS0xLjMzMy44NjYtMy43Mi4wNDYtNC45NDh6bTIuNTU3LTcuMTY3YTUuOTQxIDUuOTQxIDAgMDAzLjctMy4xNjcgMy4yNDMgMy4yNDMgMCAwMC4zMTktMS4zNDYgMS45MTUgMS45MTUgMCAwMC0xLjc5NC0xLjk1NCAxLjgzMiAxLjgzMiAwIDAwLTEuNi42NDEgNy4zODIgNy4zODIgMCAwMC0uNzA1IDEuMjE4Yy0uNjIgMS40MzQtLjg0MiAzLjQ4LjA4MSA0LjYwM3ptNC4yMDggMTIuMTE1YTMuMjQ0IDMuMjQ0IDAgMDAtLjMyMS0xLjM0NSA1Ljg2OSA1Ljg2OSAwIDAwLTMuNTU0LTMuMjY5IDUuMzg2IDUuMzg2IDAgMDAtLjIyNiA0LjcxMSA0LjE0NyA0LjE0NyAwIDAwLjcgMS4xMjFjMS4xMzMgMS4yMyAzLjUwNS4zMiAzLjQwMi0xLjIxOHptNC4yLTYuMjhhNy40NjYgNy40NjYgMCAwMC0xLjIxNy0uNyA0LjQyNSA0LjQyNSAwIDAwLTEuNjY2LS4zNTIgNi40IDYuNCAwIDAwLTMuMTg4LjU1NSA1Ljk1OSA1Ljk1OSAwIDAwMy4zMTYgMy4zODYgMy42NzIgMy42NzIgMCAwMDEuNDQyLjMyIDEuOCAxLjggMCAwMDEuMzEtMy4yMDl6Ii8+CiAgICA8L2c+Cjwvc3ZnPg==","contentEncoding":"BASE64","mediaType":"image/svg+xml","timestamp":{"seconds":0,"nanos":8000000}}} +{"testStepFinished":{"testCaseStartedId":"12","testStepId":"11","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":9000000}}} +{"testCaseFinished":{"testCaseStartedId":"12","timestamp":{"seconds":0,"nanos":10000000},"willBeRetried":false}} +{"testRunFinished":{"testRunStartedId":"7","timestamp":{"seconds":0,"nanos":11000000},"success":true}} diff --git a/compatibility/hooks-attachment/hooks-attachment.ts b/compatibility/hooks-attachment/hooks-attachment.ts new file mode 100644 index 00000000..2cc47c73 --- /dev/null +++ b/compatibility/hooks-attachment/hooks-attachment.ts @@ -0,0 +1,20 @@ +import { When, Before, After } from '@cucumber/fake-cucumber' +import fs from 'node:fs' + +Before({}, async function () { + await this.attach( + fs.createReadStream(import.meta.dirname + '/cucumber.svg'), + 'image/svg+xml' + ) +}) + +When('a step passes', function () { + // no-op +}) + +After({}, async function () { + await this.attach( + fs.createReadStream(import.meta.dirname + '/cucumber.svg'), + 'image/svg+xml' + ) +}) diff --git a/compatibility/hooks-conditional/hooks-conditional.cpp b/compatibility/hooks-conditional/hooks-conditional.cpp new file mode 100644 index 00000000..bf2430ee --- /dev/null +++ b/compatibility/hooks-conditional/hooks-conditional.cpp @@ -0,0 +1,27 @@ +#include "cucumber_cpp/CucumberCpp.hpp" +#include + +HOOK_BEFORE_SCENARIO("@passing-hook") +{ + // no-op +} + +HOOK_BEFORE_SCENARIO("@fail-before") +{ + ASSERT_THAT(true, false); +} + +WHEN(R"(a step passes)") +{ + // no-op +} + +HOOK_AFTER_SCENARIO("@fail-after") +{ + ASSERT_THAT(true, false); +} + +HOOK_AFTER_SCENARIO("@passing-hook") +{ + // no-op +} diff --git a/compatibility/hooks-conditional/hooks-conditional.feature b/compatibility/hooks-conditional/hooks-conditional.feature new file mode 100644 index 00000000..86aa9fb3 --- /dev/null +++ b/compatibility/hooks-conditional/hooks-conditional.feature @@ -0,0 +1,16 @@ +Feature: Hooks - Conditional execution + Hooks are special steps that run before or after each scenario's steps. + + They can also conditionally target specific scenarios, using tag expressions. + + @fail-before + Scenario: A failure in the before hook and a skipped step + When a step passes + + @fail-after + Scenario: A failure in the after hook and a passed step + When a step passes + + @passing-hook + Scenario: With an tag, a passed step and hook + When a step passes diff --git a/compatibility/hooks-conditional/hooks-conditional.ndjson b/compatibility/hooks-conditional/hooks-conditional.ndjson new file mode 100644 index 00000000..773dd04b --- /dev/null +++ b/compatibility/hooks-conditional/hooks-conditional.ndjson @@ -0,0 +1,36 @@ +{"meta":{"protocolVersion":"31.0.0","implementation":{"name":"fake-cucumber","version":"123.45.6"},"cpu":{"name":"arm64"},"os":{"name":"darwin","version":"24.5.0"},"runtime":{"name":"Node.js","version":"24.4.1"},"ci":{"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429","buildNumber":"154666429","git":{"revision":"99684bcacf01d95875834d87903dcb072306c9ad","remote":"https://github.com/cucumber-ltd/shouty.rb.git","branch":"main"}}}} +{"source":{"data":"Feature: Hooks - Conditional execution\n Hooks are special steps that run before or after each scenario's steps.\n\n They can also conditionally target specific scenarios, using tag expressions.\n\n @fail-before\n Scenario: A failure in the before hook and a skipped step\n When a step passes\n\n @fail-after\n Scenario: A failure in the after hook and a passed step\n When a step passes\n\n @passing-hook\n Scenario: With an tag, a passed step and hook\n When a step passes\n","uri":"samples/hooks-conditional/hooks-conditional.feature","mediaType":"text/x.cucumber.gherkin+plain"}} +{"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"Hooks - Conditional execution","description":" Hooks are special steps that run before or after each scenario's steps.\n\n They can also conditionally target specific scenarios, using tag expressions.","children":[{"scenario":{"id":"2","tags":[{"location":{"line":6,"column":3},"name":"@fail-before","id":"1"}],"location":{"line":7,"column":3},"keyword":"Scenario","name":"A failure in the before hook and a skipped step","description":"","steps":[{"id":"0","location":{"line":8,"column":5},"keyword":"When ","keywordType":"Action","text":"a step passes"}],"examples":[]}},{"scenario":{"id":"5","tags":[{"location":{"line":10,"column":3},"name":"@fail-after","id":"4"}],"location":{"line":11,"column":3},"keyword":"Scenario","name":"A failure in the after hook and a passed step","description":"","steps":[{"id":"3","location":{"line":12,"column":5},"keyword":"When ","keywordType":"Action","text":"a step passes"}],"examples":[]}},{"scenario":{"id":"8","tags":[{"location":{"line":14,"column":3},"name":"@passing-hook","id":"7"}],"location":{"line":15,"column":3},"keyword":"Scenario","name":"With an tag, a passed step and hook","description":"","steps":[{"id":"6","location":{"line":16,"column":5},"keyword":"When ","keywordType":"Action","text":"a step passes"}],"examples":[]}}]},"comments":[],"uri":"samples/hooks-conditional/hooks-conditional.feature"}} +{"pickle":{"id":"10","uri":"samples/hooks-conditional/hooks-conditional.feature","location":{"line":7,"column":3},"astNodeIds":["2"],"tags":[{"name":"@fail-before","astNodeId":"1"}],"name":"A failure in the before hook and a skipped step","language":"en","steps":[{"id":"9","text":"a step passes","type":"Action","astNodeIds":["0"]}]}} +{"pickle":{"id":"12","uri":"samples/hooks-conditional/hooks-conditional.feature","location":{"line":11,"column":3},"astNodeIds":["5"],"tags":[{"name":"@fail-after","astNodeId":"4"}],"name":"A failure in the after hook and a passed step","language":"en","steps":[{"id":"11","text":"a step passes","type":"Action","astNodeIds":["3"]}]}} +{"pickle":{"id":"14","uri":"samples/hooks-conditional/hooks-conditional.feature","location":{"line":15,"column":3},"astNodeIds":["8"],"tags":[{"name":"@passing-hook","astNodeId":"7"}],"name":"With an tag, a passed step and hook","language":"en","steps":[{"id":"13","text":"a step passes","type":"Action","astNodeIds":["6"]}]}} +{"stepDefinition":{"id":"17","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a step passes"},"sourceReference":{"uri":"samples/hooks-conditional/hooks-conditional.ts","location":{"line":11}}}} +{"hook":{"id":"15","type":"BEFORE_TEST_CASE","tagExpression":"@passing-hook","sourceReference":{"uri":"samples/hooks-conditional/hooks-conditional.ts","location":{"line":3}}}} +{"hook":{"id":"16","type":"BEFORE_TEST_CASE","tagExpression":"@fail-before","sourceReference":{"uri":"samples/hooks-conditional/hooks-conditional.ts","location":{"line":7}}}} +{"hook":{"id":"18","type":"AFTER_TEST_CASE","tagExpression":"@fail-after","sourceReference":{"uri":"samples/hooks-conditional/hooks-conditional.ts","location":{"line":15}}}} +{"hook":{"id":"19","type":"AFTER_TEST_CASE","tagExpression":"@passing-hook","sourceReference":{"uri":"samples/hooks-conditional/hooks-conditional.ts","location":{"line":19}}}} +{"testRunStarted":{"id":"20","timestamp":{"seconds":0,"nanos":0}}} +{"testCase":{"id":"21","pickleId":"10","testSteps":[{"id":"22","hookId":"16"},{"id":"23","pickleStepId":"9","stepDefinitionIds":["17"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}],"testRunStartedId":"20"}} +{"testCase":{"id":"24","pickleId":"12","testSteps":[{"id":"25","pickleStepId":"11","stepDefinitionIds":["17"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"26","hookId":"18"}],"testRunStartedId":"20"}} +{"testCase":{"id":"27","pickleId":"14","testSteps":[{"id":"28","hookId":"15"},{"id":"29","pickleStepId":"13","stepDefinitionIds":["17"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"30","hookId":"19"}],"testRunStartedId":"20"}} +{"testCaseStarted":{"id":"31","testCaseId":"21","timestamp":{"seconds":0,"nanos":1000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"31","testStepId":"22","timestamp":{"seconds":0,"nanos":2000000}}} +{"testStepFinished":{"testCaseStartedId":"31","testStepId":"22","testStepResult":{"message":"Exception in conditional hook","exception":{"type":"Error","message":"Exception in conditional hook","stackTrace":"Error: Exception in conditional hook\nsamples/hooks-conditional/hooks-conditional.feature:7"},"status":"FAILED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":3000000}}} +{"testStepStarted":{"testCaseStartedId":"31","testStepId":"23","timestamp":{"seconds":0,"nanos":4000000}}} +{"testStepFinished":{"testCaseStartedId":"31","testStepId":"23","testStepResult":{"status":"SKIPPED","duration":{"seconds":0,"nanos":0}},"timestamp":{"seconds":0,"nanos":5000000}}} +{"testCaseFinished":{"testCaseStartedId":"31","timestamp":{"seconds":0,"nanos":6000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"32","testCaseId":"24","timestamp":{"seconds":0,"nanos":7000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"32","testStepId":"25","timestamp":{"seconds":0,"nanos":8000000}}} +{"testStepFinished":{"testCaseStartedId":"32","testStepId":"25","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":9000000}}} +{"testStepStarted":{"testCaseStartedId":"32","testStepId":"26","timestamp":{"seconds":0,"nanos":10000000}}} +{"testStepFinished":{"testCaseStartedId":"32","testStepId":"26","testStepResult":{"message":"Exception in conditional hook","exception":{"type":"Error","message":"Exception in conditional hook","stackTrace":"Error: Exception in conditional hook\nsamples/hooks-conditional/hooks-conditional.feature:11"},"status":"FAILED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":11000000}}} +{"testCaseFinished":{"testCaseStartedId":"32","timestamp":{"seconds":0,"nanos":12000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"33","testCaseId":"27","timestamp":{"seconds":0,"nanos":13000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"33","testStepId":"28","timestamp":{"seconds":0,"nanos":14000000}}} +{"testStepFinished":{"testCaseStartedId":"33","testStepId":"28","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":15000000}}} +{"testStepStarted":{"testCaseStartedId":"33","testStepId":"29","timestamp":{"seconds":0,"nanos":16000000}}} +{"testStepFinished":{"testCaseStartedId":"33","testStepId":"29","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":17000000}}} +{"testStepStarted":{"testCaseStartedId":"33","testStepId":"30","timestamp":{"seconds":0,"nanos":18000000}}} +{"testStepFinished":{"testCaseStartedId":"33","testStepId":"30","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":19000000}}} +{"testCaseFinished":{"testCaseStartedId":"33","timestamp":{"seconds":0,"nanos":20000000},"willBeRetried":false}} +{"testRunFinished":{"testRunStartedId":"20","timestamp":{"seconds":0,"nanos":21000000},"success":false}} diff --git a/compatibility/hooks-conditional/hooks-conditional.ts b/compatibility/hooks-conditional/hooks-conditional.ts new file mode 100644 index 00000000..b47008e3 --- /dev/null +++ b/compatibility/hooks-conditional/hooks-conditional.ts @@ -0,0 +1,21 @@ +import { When, Before, After } from '@cucumber/fake-cucumber' + +Before({tags: '@passing-hook'}, async function () { + // no-op +}) + +Before({tags: '@fail-before'}, function () { + throw new Error('Exception in conditional hook') +}) + +When('a step passes', function () { + // no-op +}) + +After({tags: '@fail-after'}, function () { + throw new Error('Exception in conditional hook') +}) + +After({tags: '@passing-hook'}, async function () { + // no-op +}) diff --git a/compatibility/hooks-named/hooks-named.cpp b/compatibility/hooks-named/hooks-named.cpp new file mode 100644 index 00000000..2e385f9b --- /dev/null +++ b/compatibility/hooks-named/hooks-named.cpp @@ -0,0 +1,17 @@ +#include "cucumber_cpp/CucumberCpp.hpp" +#include + +HOOK_BEFORE_SCENARIO(.name = "A named before hook") +{ + // no-op +} + +WHEN(R"(a step passes)") +{ + // no-op +} + +HOOK_AFTER_SCENARIO(.name = "A named after hook") +{ + // no-op +} diff --git a/compatibility/hooks-named/hooks-named.feature b/compatibility/hooks-named/hooks-named.feature new file mode 100644 index 00000000..41007112 --- /dev/null +++ b/compatibility/hooks-named/hooks-named.feature @@ -0,0 +1,8 @@ +Feature: Hooks - Named + Hooks are special steps that run before or after each scenario's steps. + + Hooks can be given a name. Which is nice for reporting. Otherwise they work + exactly the same as regular hooks. + + Scenario: With a named before and after hook + When a step passes diff --git a/compatibility/hooks-named/hooks-named.ndjson b/compatibility/hooks-named/hooks-named.ndjson new file mode 100644 index 00000000..16e20a1d --- /dev/null +++ b/compatibility/hooks-named/hooks-named.ndjson @@ -0,0 +1,18 @@ +{"meta":{"protocolVersion":"31.0.0","implementation":{"name":"fake-cucumber","version":"123.45.6"},"cpu":{"name":"arm64"},"os":{"name":"darwin","version":"24.5.0"},"runtime":{"name":"Node.js","version":"24.4.1"},"ci":{"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429","buildNumber":"154666429","git":{"revision":"99684bcacf01d95875834d87903dcb072306c9ad","remote":"https://github.com/cucumber-ltd/shouty.rb.git","branch":"main"}}}} +{"source":{"data":"Feature: Hooks - Named\n Hooks are special steps that run before or after each scenario's steps.\n\n Hooks can be given a name. Which is nice for reporting. Otherwise they work\n exactly the same as regular hooks.\n\n Scenario: With a named before and after hook\n When a step passes\n","uri":"samples/hooks-named/hooks-named.feature","mediaType":"text/x.cucumber.gherkin+plain"}} +{"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"Hooks - Named","description":" Hooks are special steps that run before or after each scenario's steps.\n\n Hooks can be given a name. Which is nice for reporting. Otherwise they work\n exactly the same as regular hooks.","children":[{"scenario":{"id":"1","tags":[],"location":{"line":7,"column":3},"keyword":"Scenario","name":"With a named before and after hook","description":"","steps":[{"id":"0","location":{"line":8,"column":5},"keyword":"When ","keywordType":"Action","text":"a step passes"}],"examples":[]}}]},"comments":[],"uri":"samples/hooks-named/hooks-named.feature"}} +{"pickle":{"id":"3","uri":"samples/hooks-named/hooks-named.feature","location":{"line":7,"column":3},"astNodeIds":["1"],"tags":[],"name":"With a named before and after hook","language":"en","steps":[{"id":"2","text":"a step passes","type":"Action","astNodeIds":["0"]}]}} +{"stepDefinition":{"id":"5","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a step passes"},"sourceReference":{"uri":"samples/hooks-named/hooks-named.ts","location":{"line":7}}}} +{"hook":{"id":"4","type":"BEFORE_TEST_CASE","name":"A named before hook","sourceReference":{"uri":"samples/hooks-named/hooks-named.ts","location":{"line":3}}}} +{"hook":{"id":"6","type":"AFTER_TEST_CASE","name":"A named after hook","sourceReference":{"uri":"samples/hooks-named/hooks-named.ts","location":{"line":11}}}} +{"testRunStarted":{"id":"7","timestamp":{"seconds":0,"nanos":0}}} +{"testCase":{"id":"8","pickleId":"3","testSteps":[{"id":"9","hookId":"4"},{"id":"10","pickleStepId":"2","stepDefinitionIds":["5"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"11","hookId":"6"}],"testRunStartedId":"7"}} +{"testCaseStarted":{"id":"12","testCaseId":"8","timestamp":{"seconds":0,"nanos":1000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"12","testStepId":"9","timestamp":{"seconds":0,"nanos":2000000}}} +{"testStepFinished":{"testCaseStartedId":"12","testStepId":"9","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":3000000}}} +{"testStepStarted":{"testCaseStartedId":"12","testStepId":"10","timestamp":{"seconds":0,"nanos":4000000}}} +{"testStepFinished":{"testCaseStartedId":"12","testStepId":"10","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":5000000}}} +{"testStepStarted":{"testCaseStartedId":"12","testStepId":"11","timestamp":{"seconds":0,"nanos":6000000}}} +{"testStepFinished":{"testCaseStartedId":"12","testStepId":"11","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":7000000}}} +{"testCaseFinished":{"testCaseStartedId":"12","timestamp":{"seconds":0,"nanos":8000000},"willBeRetried":false}} +{"testRunFinished":{"testRunStartedId":"7","timestamp":{"seconds":0,"nanos":9000000},"success":true}} diff --git a/compatibility/hooks-named/hooks-named.ts b/compatibility/hooks-named/hooks-named.ts new file mode 100644 index 00000000..a21cf9a2 --- /dev/null +++ b/compatibility/hooks-named/hooks-named.ts @@ -0,0 +1,13 @@ +import { When, Before, After } from '@cucumber/fake-cucumber' + +Before({name: 'A named before hook'}, function () { + // no-op +}) + +When('a step passes', function () { + // no-op +}) + +After({name: 'A named after hook'}, function () { + // no-op +}) diff --git a/compatibility/hooks-skipped/hooks-skipped.cpp b/compatibility/hooks-skipped/hooks-skipped.cpp new file mode 100644 index 00000000..38b435a4 --- /dev/null +++ b/compatibility/hooks-skipped/hooks-skipped.cpp @@ -0,0 +1,42 @@ +#include "cucumber_cpp/CucumberCpp.hpp" +#include + +HOOK_BEFORE_SCENARIO() +{ + // no-op +} + +HOOK_BEFORE_SCENARIO("@skip-before") +{ + Skipped(); +} + +HOOK_BEFORE_SCENARIO() +{ + // no-op +} + +WHEN(R"(a normal step)") +{ + // no-op +} + +WHEN(R"(a step that skips)") +{ + Skipped(); +} + +HOOK_AFTER_SCENARIO() +{ + // no-op +} + +HOOK_AFTER_SCENARIO("@skip-after") +{ + Skipped(); +} + +HOOK_AFTER_SCENARIO() +{ + // no-op +} diff --git a/compatibility/hooks-skipped/hooks-skipped.feature b/compatibility/hooks-skipped/hooks-skipped.feature new file mode 100644 index 00000000..a721b409 --- /dev/null +++ b/compatibility/hooks-skipped/hooks-skipped.feature @@ -0,0 +1,26 @@ +Feature: Hooks and skipped + + Before and After hooks behave a little differently when it comes to skipping. + + Scenario: Skip from a step + + Skipping from a step causes all subsequent steps to be skipped, but any After hooks + will still be run normally. + + Given a step that skips + + @skip-before + Scenario: Skip from a Before hook + + Skipping from a Before hook will cause all subsequent Before hooks and steps to be skipped, + but any After hooks will still be run normally. + + Given a normal step + + @skip-after + Scenario: Skip from an After hook + + Skipping from a After hook will only mark that hook as skipped. Any subsequent After hooks + will still be run normally. + + Given a normal step diff --git a/compatibility/hooks-skipped/hooks-skipped.ndjson b/compatibility/hooks-skipped/hooks-skipped.ndjson new file mode 100644 index 00000000..d05380be --- /dev/null +++ b/compatibility/hooks-skipped/hooks-skipped.ndjson @@ -0,0 +1,59 @@ +{"meta":{"protocolVersion":"31.0.0","implementation":{"name":"fake-cucumber","version":"123.45.6"},"cpu":{"name":"arm64"},"os":{"name":"darwin","version":"24.5.0"},"runtime":{"name":"Node.js","version":"24.4.1"},"ci":{"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429","buildNumber":"154666429","git":{"revision":"99684bcacf01d95875834d87903dcb072306c9ad","remote":"https://github.com/cucumber-ltd/shouty.rb.git","branch":"main"}}}} +{"source":{"data":"Feature: Hooks and skipped\n\n Before and After hooks behave a little differently when it comes to skipping.\n\n Scenario: Skip from a step\n\n Skipping from a step causes all subsequent steps to be skipped, but any After hooks\n will still be run normally.\n\n Given a step that skips\n\n @skip-before\n Scenario: Skip from a Before hook\n\n Skipping from a Before hook will cause all subsequent Before hooks and steps to be skipped,\n but any After hooks will still be run normally.\n\n Given a normal step\n\n @skip-after\n Scenario: Skip from an After hook\n\n Skipping from a After hook will only mark that hook as skipped. Any subsequent After hooks\n will still be run normally.\n\n Given a normal step\n","uri":"samples/hooks-skipped/hooks-skipped.feature","mediaType":"text/x.cucumber.gherkin+plain"}} +{"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"Hooks and skipped","description":" Before and After hooks behave a little differently when it comes to skipping.","children":[{"scenario":{"id":"1","tags":[],"location":{"line":5,"column":3},"keyword":"Scenario","name":"Skip from a step","description":" Skipping from a step causes all subsequent steps to be skipped, but any After hooks\n will still be run normally.","steps":[{"id":"0","location":{"line":10,"column":5},"keyword":"Given ","keywordType":"Context","text":"a step that skips"}],"examples":[]}},{"scenario":{"id":"4","tags":[{"location":{"line":12,"column":3},"name":"@skip-before","id":"3"}],"location":{"line":13,"column":3},"keyword":"Scenario","name":"Skip from a Before hook","description":" Skipping from a Before hook will cause all subsequent Before hooks and steps to be skipped,\n but any After hooks will still be run normally.","steps":[{"id":"2","location":{"line":18,"column":5},"keyword":"Given ","keywordType":"Context","text":"a normal step"}],"examples":[]}},{"scenario":{"id":"7","tags":[{"location":{"line":20,"column":3},"name":"@skip-after","id":"6"}],"location":{"line":21,"column":3},"keyword":"Scenario","name":"Skip from an After hook","description":" Skipping from a After hook will only mark that hook as skipped. Any subsequent After hooks\n will still be run normally.","steps":[{"id":"5","location":{"line":26,"column":5},"keyword":"Given ","keywordType":"Context","text":"a normal step"}],"examples":[]}}]},"comments":[],"uri":"samples/hooks-skipped/hooks-skipped.feature"}} +{"pickle":{"id":"9","uri":"samples/hooks-skipped/hooks-skipped.feature","location":{"line":5,"column":3},"astNodeIds":["1"],"tags":[],"name":"Skip from a step","language":"en","steps":[{"id":"8","text":"a step that skips","type":"Context","astNodeIds":["0"]}]}} +{"pickle":{"id":"11","uri":"samples/hooks-skipped/hooks-skipped.feature","location":{"line":13,"column":3},"astNodeIds":["4"],"tags":[{"name":"@skip-before","astNodeId":"3"}],"name":"Skip from a Before hook","language":"en","steps":[{"id":"10","text":"a normal step","type":"Context","astNodeIds":["2"]}]}} +{"pickle":{"id":"13","uri":"samples/hooks-skipped/hooks-skipped.feature","location":{"line":21,"column":3},"astNodeIds":["7"],"tags":[{"name":"@skip-after","astNodeId":"6"}],"name":"Skip from an After hook","language":"en","steps":[{"id":"12","text":"a normal step","type":"Context","astNodeIds":["5"]}]}} +{"stepDefinition":{"id":"17","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a normal step"},"sourceReference":{"uri":"samples/hooks-skipped/hooks-skipped.ts","location":{"line":15}}}} +{"stepDefinition":{"id":"18","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a step that skips"},"sourceReference":{"uri":"samples/hooks-skipped/hooks-skipped.ts","location":{"line":19}}}} +{"hook":{"id":"14","type":"BEFORE_TEST_CASE","sourceReference":{"uri":"samples/hooks-skipped/hooks-skipped.ts","location":{"line":3}}}} +{"hook":{"id":"15","type":"BEFORE_TEST_CASE","tagExpression":"@skip-before","sourceReference":{"uri":"samples/hooks-skipped/hooks-skipped.ts","location":{"line":7}}}} +{"hook":{"id":"16","type":"BEFORE_TEST_CASE","sourceReference":{"uri":"samples/hooks-skipped/hooks-skipped.ts","location":{"line":11}}}} +{"hook":{"id":"19","type":"AFTER_TEST_CASE","sourceReference":{"uri":"samples/hooks-skipped/hooks-skipped.ts","location":{"line":23}}}} +{"hook":{"id":"20","type":"AFTER_TEST_CASE","tagExpression":"@skip-after","sourceReference":{"uri":"samples/hooks-skipped/hooks-skipped.ts","location":{"line":27}}}} +{"hook":{"id":"21","type":"AFTER_TEST_CASE","sourceReference":{"uri":"samples/hooks-skipped/hooks-skipped.ts","location":{"line":31}}}} +{"testRunStarted":{"id":"22","timestamp":{"seconds":0,"nanos":0}}} +{"testCase":{"id":"23","pickleId":"9","testSteps":[{"id":"24","hookId":"14"},{"id":"25","hookId":"16"},{"id":"26","pickleStepId":"8","stepDefinitionIds":["18"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"27","hookId":"21"},{"id":"28","hookId":"19"}],"testRunStartedId":"22"}} +{"testCase":{"id":"29","pickleId":"11","testSteps":[{"id":"30","hookId":"14"},{"id":"31","hookId":"15"},{"id":"32","hookId":"16"},{"id":"33","pickleStepId":"10","stepDefinitionIds":["17"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"34","hookId":"21"},{"id":"35","hookId":"19"}],"testRunStartedId":"22"}} +{"testCase":{"id":"36","pickleId":"13","testSteps":[{"id":"37","hookId":"14"},{"id":"38","hookId":"16"},{"id":"39","pickleStepId":"12","stepDefinitionIds":["17"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"40","hookId":"21"},{"id":"41","hookId":"20"},{"id":"42","hookId":"19"}],"testRunStartedId":"22"}} +{"testCaseStarted":{"id":"43","testCaseId":"23","timestamp":{"seconds":0,"nanos":1000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"43","testStepId":"24","timestamp":{"seconds":0,"nanos":2000000}}} +{"testStepFinished":{"testCaseStartedId":"43","testStepId":"24","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":3000000}}} +{"testStepStarted":{"testCaseStartedId":"43","testStepId":"25","timestamp":{"seconds":0,"nanos":4000000}}} +{"testStepFinished":{"testCaseStartedId":"43","testStepId":"25","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":5000000}}} +{"testStepStarted":{"testCaseStartedId":"43","testStepId":"26","timestamp":{"seconds":0,"nanos":6000000}}} +{"testStepFinished":{"testCaseStartedId":"43","testStepId":"26","testStepResult":{"status":"SKIPPED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":7000000}}} +{"testStepStarted":{"testCaseStartedId":"43","testStepId":"27","timestamp":{"seconds":0,"nanos":8000000}}} +{"testStepFinished":{"testCaseStartedId":"43","testStepId":"27","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":9000000}}} +{"testStepStarted":{"testCaseStartedId":"43","testStepId":"28","timestamp":{"seconds":0,"nanos":10000000}}} +{"testStepFinished":{"testCaseStartedId":"43","testStepId":"28","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":11000000}}} +{"testCaseFinished":{"testCaseStartedId":"43","timestamp":{"seconds":0,"nanos":12000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"44","testCaseId":"29","timestamp":{"seconds":0,"nanos":13000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"44","testStepId":"30","timestamp":{"seconds":0,"nanos":14000000}}} +{"testStepFinished":{"testCaseStartedId":"44","testStepId":"30","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":15000000}}} +{"testStepStarted":{"testCaseStartedId":"44","testStepId":"31","timestamp":{"seconds":0,"nanos":16000000}}} +{"testStepFinished":{"testCaseStartedId":"44","testStepId":"31","testStepResult":{"status":"SKIPPED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":17000000}}} +{"testStepStarted":{"testCaseStartedId":"44","testStepId":"32","timestamp":{"seconds":0,"nanos":18000000}}} +{"testStepFinished":{"testCaseStartedId":"44","testStepId":"32","testStepResult":{"status":"SKIPPED","duration":{"seconds":0,"nanos":0}},"timestamp":{"seconds":0,"nanos":19000000}}} +{"testStepStarted":{"testCaseStartedId":"44","testStepId":"33","timestamp":{"seconds":0,"nanos":20000000}}} +{"testStepFinished":{"testCaseStartedId":"44","testStepId":"33","testStepResult":{"status":"SKIPPED","duration":{"seconds":0,"nanos":0}},"timestamp":{"seconds":0,"nanos":21000000}}} +{"testStepStarted":{"testCaseStartedId":"44","testStepId":"34","timestamp":{"seconds":0,"nanos":22000000}}} +{"testStepFinished":{"testCaseStartedId":"44","testStepId":"34","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":23000000}}} +{"testStepStarted":{"testCaseStartedId":"44","testStepId":"35","timestamp":{"seconds":0,"nanos":24000000}}} +{"testStepFinished":{"testCaseStartedId":"44","testStepId":"35","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":25000000}}} +{"testCaseFinished":{"testCaseStartedId":"44","timestamp":{"seconds":0,"nanos":26000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"45","testCaseId":"36","timestamp":{"seconds":0,"nanos":27000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"45","testStepId":"37","timestamp":{"seconds":0,"nanos":28000000}}} +{"testStepFinished":{"testCaseStartedId":"45","testStepId":"37","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":29000000}}} +{"testStepStarted":{"testCaseStartedId":"45","testStepId":"38","timestamp":{"seconds":0,"nanos":30000000}}} +{"testStepFinished":{"testCaseStartedId":"45","testStepId":"38","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":31000000}}} +{"testStepStarted":{"testCaseStartedId":"45","testStepId":"39","timestamp":{"seconds":0,"nanos":32000000}}} +{"testStepFinished":{"testCaseStartedId":"45","testStepId":"39","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":33000000}}} +{"testStepStarted":{"testCaseStartedId":"45","testStepId":"40","timestamp":{"seconds":0,"nanos":34000000}}} +{"testStepFinished":{"testCaseStartedId":"45","testStepId":"40","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":35000000}}} +{"testStepStarted":{"testCaseStartedId":"45","testStepId":"41","timestamp":{"seconds":0,"nanos":36000000}}} +{"testStepFinished":{"testCaseStartedId":"45","testStepId":"41","testStepResult":{"status":"SKIPPED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":37000000}}} +{"testStepStarted":{"testCaseStartedId":"45","testStepId":"42","timestamp":{"seconds":0,"nanos":38000000}}} +{"testStepFinished":{"testCaseStartedId":"45","testStepId":"42","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":39000000}}} +{"testCaseFinished":{"testCaseStartedId":"45","timestamp":{"seconds":0,"nanos":40000000},"willBeRetried":false}} +{"testRunFinished":{"testRunStartedId":"22","timestamp":{"seconds":0,"nanos":41000000},"success":true}} diff --git a/compatibility/hooks-skipped/hooks-skipped.ts b/compatibility/hooks-skipped/hooks-skipped.ts new file mode 100644 index 00000000..36ec26c5 --- /dev/null +++ b/compatibility/hooks-skipped/hooks-skipped.ts @@ -0,0 +1,33 @@ +import { After, Before, When } from '@cucumber/fake-cucumber' + +Before({}, function () { + // no-op +}) + +Before({ tags: '@skip-before' }, function () { + return 'skipped' +}) + +Before({}, function () { + // no-op +}) + +When('a normal step', function () { + // no-op +}) + +When('a step that skips', function () { + return 'skipped' +}) + +After({}, function () { + // no-op +}) + +After({ tags: '@skip-after' }, function () { + return 'skipped' +}) + +After({}, function () { + // no-op +}) diff --git a/compatibility/hooks-undefined/hooks-undefined.cpp b/compatibility/hooks-undefined/hooks-undefined.cpp new file mode 100644 index 00000000..5d96d8b1 --- /dev/null +++ b/compatibility/hooks-undefined/hooks-undefined.cpp @@ -0,0 +1,12 @@ +#include "cucumber_cpp/CucumberCpp.hpp" +#include + +HOOK_BEFORE_SCENARIO() +{ + // no-op +} + +HOOK_AFTER_SCENARIO() +{ + // no-op +} diff --git a/compatibility/hooks-undefined/hooks-undefined.feature b/compatibility/hooks-undefined/hooks-undefined.feature new file mode 100644 index 00000000..a13e450b --- /dev/null +++ b/compatibility/hooks-undefined/hooks-undefined.feature @@ -0,0 +1,5 @@ +Feature: Hooks - With Undefined Steps + Hooks also run before and after undefined steps + + Scenario: No tags and a undefined step + When a step does not exist diff --git a/compatibility/hooks-undefined/hooks-undefined.ndjson b/compatibility/hooks-undefined/hooks-undefined.ndjson new file mode 100644 index 00000000..ebd0b7fe --- /dev/null +++ b/compatibility/hooks-undefined/hooks-undefined.ndjson @@ -0,0 +1,18 @@ +{"meta":{"protocolVersion":"31.0.0","implementation":{"name":"fake-cucumber","version":"123.45.6"},"cpu":{"name":"arm64"},"os":{"name":"darwin","version":"24.5.0"},"runtime":{"name":"Node.js","version":"24.4.1"},"ci":{"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429","buildNumber":"154666429","git":{"revision":"99684bcacf01d95875834d87903dcb072306c9ad","remote":"https://github.com/cucumber-ltd/shouty.rb.git","branch":"main"}}}} +{"source":{"data":"Feature: Hooks - With Undefined Steps\n Hooks also run before and after undefined steps\n\n Scenario: No tags and a undefined step\n When a step does not exist\n","uri":"samples/hooks-undefined/hooks-undefined.feature","mediaType":"text/x.cucumber.gherkin+plain"}} +{"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"Hooks - With Undefined Steps","description":" Hooks also run before and after undefined steps","children":[{"scenario":{"id":"1","tags":[],"location":{"line":4,"column":3},"keyword":"Scenario","name":"No tags and a undefined step","description":"","steps":[{"id":"0","location":{"line":5,"column":5},"keyword":"When ","keywordType":"Action","text":"a step does not exist"}],"examples":[]}}]},"comments":[],"uri":"samples/hooks-undefined/hooks-undefined.feature"}} +{"pickle":{"id":"3","uri":"samples/hooks-undefined/hooks-undefined.feature","location":{"line":4,"column":3},"astNodeIds":["1"],"tags":[],"name":"No tags and a undefined step","language":"en","steps":[{"id":"2","text":"a step does not exist","type":"Action","astNodeIds":["0"]}]}} +{"hook":{"id":"4","type":"BEFORE_TEST_CASE","sourceReference":{"uri":"samples/hooks-undefined/hooks-undefined.ts","location":{"line":3}}}} +{"hook":{"id":"5","type":"AFTER_TEST_CASE","sourceReference":{"uri":"samples/hooks-undefined/hooks-undefined.ts","location":{"line":7}}}} +{"testRunStarted":{"id":"6","timestamp":{"seconds":0,"nanos":0}}} +{"testCase":{"id":"7","pickleId":"3","testSteps":[{"id":"8","hookId":"4"},{"id":"9","pickleStepId":"2","stepDefinitionIds":[],"stepMatchArgumentsLists":[]},{"id":"10","hookId":"5"}],"testRunStartedId":"6"}} +{"testCaseStarted":{"id":"11","testCaseId":"7","timestamp":{"seconds":0,"nanos":1000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"11","testStepId":"8","timestamp":{"seconds":0,"nanos":2000000}}} +{"testStepFinished":{"testCaseStartedId":"11","testStepId":"8","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":3000000}}} +{"testStepStarted":{"testCaseStartedId":"11","testStepId":"9","timestamp":{"seconds":0,"nanos":4000000}}} +{"suggestion":{"id":"12","pickleStepId":"2","snippets":[{"language":"typescript","code":"When(\"a step does not exist\", () => {\n return \"pending\"\n})"}]}} +{"testStepFinished":{"testCaseStartedId":"11","testStepId":"9","testStepResult":{"status":"UNDEFINED","duration":{"seconds":0,"nanos":0}},"timestamp":{"seconds":0,"nanos":5000000}}} +{"testStepStarted":{"testCaseStartedId":"11","testStepId":"10","timestamp":{"seconds":0,"nanos":6000000}}} +{"testStepFinished":{"testCaseStartedId":"11","testStepId":"10","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":7000000}}} +{"testCaseFinished":{"testCaseStartedId":"11","timestamp":{"seconds":0,"nanos":8000000},"willBeRetried":false}} +{"testRunFinished":{"testRunStartedId":"6","timestamp":{"seconds":0,"nanos":9000000},"success":false}} diff --git a/compatibility/hooks-undefined/hooks-undefined.ts b/compatibility/hooks-undefined/hooks-undefined.ts new file mode 100644 index 00000000..9a669601 --- /dev/null +++ b/compatibility/hooks-undefined/hooks-undefined.ts @@ -0,0 +1,9 @@ +import { When, Before, After } from '@cucumber/fake-cucumber' + +Before({}, function () { + // no-op +}) + +After({}, function () { + // no-op +}) diff --git a/compatibility/hooks/hooks.cpp b/compatibility/hooks/hooks.cpp new file mode 100644 index 00000000..07f078b4 --- /dev/null +++ b/compatibility/hooks/hooks.cpp @@ -0,0 +1,22 @@ +#include "cucumber_cpp/CucumberCpp.hpp" +#include + +HOOK_BEFORE_SCENARIO() +{ + // no-op +} + +WHEN(R"(a step passes)") +{ + // no-op +} + +WHEN(R"(a step fails)") +{ + ASSERT_THAT(true, false); +} + +HOOK_AFTER_SCENARIO() +{ + // no-op +} diff --git a/compatibility/hooks/hooks.feature b/compatibility/hooks/hooks.feature new file mode 100644 index 00000000..5a29a802 --- /dev/null +++ b/compatibility/hooks/hooks.feature @@ -0,0 +1,8 @@ +Feature: Hooks + Hooks are special steps that run before or after each scenario's steps. + + Scenario: No tags and a passed step + When a step passes + + Scenario: No tags and a failed step + When a step fails diff --git a/compatibility/hooks/hooks.ndjson b/compatibility/hooks/hooks.ndjson new file mode 100644 index 00000000..ec2f6186 --- /dev/null +++ b/compatibility/hooks/hooks.ndjson @@ -0,0 +1,29 @@ +{"meta":{"protocolVersion":"31.0.0","implementation":{"name":"fake-cucumber","version":"123.45.6"},"cpu":{"name":"arm64"},"os":{"name":"darwin","version":"24.5.0"},"runtime":{"name":"Node.js","version":"24.4.1"},"ci":{"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429","buildNumber":"154666429","git":{"revision":"99684bcacf01d95875834d87903dcb072306c9ad","remote":"https://github.com/cucumber-ltd/shouty.rb.git","branch":"main"}}}} +{"source":{"data":"Feature: Hooks\n Hooks are special steps that run before or after each scenario's steps.\n\n Scenario: No tags and a passed step\n When a step passes\n\n Scenario: No tags and a failed step\n When a step fails\n","uri":"samples/hooks/hooks.feature","mediaType":"text/x.cucumber.gherkin+plain"}} +{"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"Hooks","description":" Hooks are special steps that run before or after each scenario's steps.","children":[{"scenario":{"id":"1","tags":[],"location":{"line":4,"column":3},"keyword":"Scenario","name":"No tags and a passed step","description":"","steps":[{"id":"0","location":{"line":5,"column":5},"keyword":"When ","keywordType":"Action","text":"a step passes"}],"examples":[]}},{"scenario":{"id":"3","tags":[],"location":{"line":7,"column":3},"keyword":"Scenario","name":"No tags and a failed step","description":"","steps":[{"id":"2","location":{"line":8,"column":5},"keyword":"When ","keywordType":"Action","text":"a step fails"}],"examples":[]}}]},"comments":[],"uri":"samples/hooks/hooks.feature"}} +{"pickle":{"id":"5","uri":"samples/hooks/hooks.feature","location":{"line":4,"column":3},"astNodeIds":["1"],"tags":[],"name":"No tags and a passed step","language":"en","steps":[{"id":"4","text":"a step passes","type":"Action","astNodeIds":["0"]}]}} +{"pickle":{"id":"7","uri":"samples/hooks/hooks.feature","location":{"line":7,"column":3},"astNodeIds":["3"],"tags":[],"name":"No tags and a failed step","language":"en","steps":[{"id":"6","text":"a step fails","type":"Action","astNodeIds":["2"]}]}} +{"stepDefinition":{"id":"9","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a step passes"},"sourceReference":{"uri":"samples/hooks/hooks.ts","location":{"line":7}}}} +{"stepDefinition":{"id":"10","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a step fails"},"sourceReference":{"uri":"samples/hooks/hooks.ts","location":{"line":11}}}} +{"hook":{"id":"8","type":"BEFORE_TEST_CASE","sourceReference":{"uri":"samples/hooks/hooks.ts","location":{"line":3}}}} +{"hook":{"id":"11","type":"AFTER_TEST_CASE","sourceReference":{"uri":"samples/hooks/hooks.ts","location":{"line":15}}}} +{"testRunStarted":{"id":"12","timestamp":{"seconds":0,"nanos":0}}} +{"testCase":{"id":"13","pickleId":"5","testSteps":[{"id":"14","hookId":"8"},{"id":"15","pickleStepId":"4","stepDefinitionIds":["9"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"16","hookId":"11"}],"testRunStartedId":"12"}} +{"testCase":{"id":"17","pickleId":"7","testSteps":[{"id":"18","hookId":"8"},{"id":"19","pickleStepId":"6","stepDefinitionIds":["10"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"20","hookId":"11"}],"testRunStartedId":"12"}} +{"testCaseStarted":{"id":"21","testCaseId":"13","timestamp":{"seconds":0,"nanos":1000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"21","testStepId":"14","timestamp":{"seconds":0,"nanos":2000000}}} +{"testStepFinished":{"testCaseStartedId":"21","testStepId":"14","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":3000000}}} +{"testStepStarted":{"testCaseStartedId":"21","testStepId":"15","timestamp":{"seconds":0,"nanos":4000000}}} +{"testStepFinished":{"testCaseStartedId":"21","testStepId":"15","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":5000000}}} +{"testStepStarted":{"testCaseStartedId":"21","testStepId":"16","timestamp":{"seconds":0,"nanos":6000000}}} +{"testStepFinished":{"testCaseStartedId":"21","testStepId":"16","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":7000000}}} +{"testCaseFinished":{"testCaseStartedId":"21","timestamp":{"seconds":0,"nanos":8000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"22","testCaseId":"17","timestamp":{"seconds":0,"nanos":9000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"22","testStepId":"18","timestamp":{"seconds":0,"nanos":10000000}}} +{"testStepFinished":{"testCaseStartedId":"22","testStepId":"18","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":11000000}}} +{"testStepStarted":{"testCaseStartedId":"22","testStepId":"19","timestamp":{"seconds":0,"nanos":12000000}}} +{"testStepFinished":{"testCaseStartedId":"22","testStepId":"19","testStepResult":{"message":"Exception in step","exception":{"type":"Error","message":"Exception in step","stackTrace":"Error: Exception in step\nsamples/hooks/hooks.feature:8"},"status":"FAILED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":13000000}}} +{"testStepStarted":{"testCaseStartedId":"22","testStepId":"20","timestamp":{"seconds":0,"nanos":14000000}}} +{"testStepFinished":{"testCaseStartedId":"22","testStepId":"20","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":15000000}}} +{"testCaseFinished":{"testCaseStartedId":"22","timestamp":{"seconds":0,"nanos":16000000},"willBeRetried":false}} +{"testRunFinished":{"testRunStartedId":"12","timestamp":{"seconds":0,"nanos":17000000},"success":false}} diff --git a/compatibility/hooks/hooks.ts b/compatibility/hooks/hooks.ts new file mode 100644 index 00000000..30b6e27f --- /dev/null +++ b/compatibility/hooks/hooks.ts @@ -0,0 +1,17 @@ +import { When, Before, After } from '@cucumber/fake-cucumber' + +Before({}, function () { + // no-op +}) + +When('a step passes', function () { + // no-op +}) + +When('a step fails', function () { + throw new Error('Exception in step') +}) + +After({}, function () { + // no-op +}) diff --git a/compatibility/markdown/markdown.feature.md b/compatibility/markdown/markdown.feature.md new file mode 100644 index 00000000..e0cdec5b --- /dev/null +++ b/compatibility/markdown/markdown.feature.md @@ -0,0 +1,46 @@ +# Feature: Cheese + +This table is not picked up by Gherkin (not indented 2+ spaces) + +| foo | bar | +| --- | --- | +| boz | boo | + + +## Rule: Nom nom nom + +I love cheese, especially fromage macaroni cheese. Rubber cheese ricotta caerphilly blue castello who moved my cheese queso bavarian bergkase melted cheese. + +### Scenario Outline: Ylajali! + +* Given some TypeScript code: + ```typescript + type Cheese = 'reblochon' | 'roquefort' | 'rocamadour' + ``` +* And some classic Gherkin: + ```gherkin + Given there are 24 apples in Mary's basket + ``` +* When we use a data table and attach something and then + | name | age | + | ---- | --: | + | Bill | 3 | + | Jane | 6 | + | Isla | 5 | +* Then this might or might not run + +#### Examples: because we need more tables + +This table is indented 2 spaces, so Gherkin will pick it up + + | what | + | ---- | + | fail | + | pass | + +And oh by the way, this table is also ignored by Gherkin because it doesn't have 2+ space indent: + +| cheese | +| -------- | +| gouda | +| gamalost | diff --git a/compatibility/markdown/markdown.ndjson b/compatibility/markdown/markdown.ndjson new file mode 100644 index 00000000..2d485ed1 --- /dev/null +++ b/compatibility/markdown/markdown.ndjson @@ -0,0 +1,35 @@ +{"meta":{"protocolVersion":"31.0.0","implementation":{"name":"fake-cucumber","version":"123.45.6"},"cpu":{"name":"arm64"},"os":{"name":"darwin","version":"24.5.0"},"runtime":{"name":"Node.js","version":"24.4.1"},"ci":{"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429","buildNumber":"154666429","git":{"revision":"99684bcacf01d95875834d87903dcb072306c9ad","remote":"https://github.com/cucumber-ltd/shouty.rb.git","branch":"main"}}}} +{"source":{"data":"# Feature: Cheese\n\nThis table is not picked up by Gherkin (not indented 2+ spaces)\n\n| foo | bar |\n| --- | --- |\n| boz | boo |\n\n\n## Rule: Nom nom nom\n\nI love cheese, especially fromage macaroni cheese. Rubber cheese ricotta caerphilly blue castello who moved my cheese queso bavarian bergkase melted cheese.\n\n### Scenario Outline: Ylajali!\n\n* Given some TypeScript code:\n ```typescript\n type Cheese = 'reblochon' | 'roquefort' | 'rocamadour'\n ```\n* And some classic Gherkin:\n ```gherkin\n Given there are 24 apples in Mary's basket\n ```\n* When we use a data table and attach something and then \n | name | age |\n | ---- | --: |\n | Bill | 3 |\n | Jane | 6 |\n | Isla | 5 |\n* Then this might or might not run\n\n#### Examples: because we need more tables\n\nThis table is indented 2 spaces, so Gherkin will pick it up\n\n | what |\n | ---- |\n | fail |\n | pass |\n\nAnd oh by the way, this table is also ignored by Gherkin because it doesn't have 2+ space indent:\n\n| cheese |\n| -------- |\n| gouda |\n| gamalost |\n","uri":"samples/markdown/markdown.feature.md","mediaType":"text/x.cucumber.gherkin+markdown"}} +{"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":3},"language":"en","keyword":"Feature","name":"Cheese","description":"| boz | boo |","children":[{"rule":{"id":"13","location":{"line":10,"column":4},"keyword":"Rule","name":"Nom nom nom","description":"","children":[{"scenario":{"id":"12","tags":[],"location":{"line":14,"column":5},"keyword":"Scenario Outline","name":"Ylajali!","description":"","steps":[{"id":"0","location":{"line":16,"column":3},"keyword":"Given ","keywordType":"Context","text":"some TypeScript code:","docString":{"location":{"line":17,"column":3},"content":"type Cheese = 'reblochon' | 'roquefort' | 'rocamadour'","delimiter":"```","mediaType":"typescript"}},{"id":"1","location":{"line":20,"column":3},"keyword":"And ","keywordType":"Conjunction","text":"some classic Gherkin:","docString":{"location":{"line":21,"column":3},"content":"Given there are 24 apples in Mary's basket","delimiter":"```","mediaType":"gherkin"}},{"id":"6","location":{"line":24,"column":3},"keyword":"When ","keywordType":"Action","text":"we use a data table and attach something and then ","dataTable":{"location":{"line":25,"column":3},"rows":[{"id":"2","location":{"line":25,"column":3},"cells":[{"location":{"line":25,"column":5},"value":"name"},{"location":{"line":25,"column":12},"value":"age"}]},{"id":"3","location":{"line":27,"column":3},"cells":[{"location":{"line":27,"column":5},"value":"Bill"},{"location":{"line":27,"column":14},"value":"3"}]},{"id":"4","location":{"line":28,"column":3},"cells":[{"location":{"line":28,"column":5},"value":"Jane"},{"location":{"line":28,"column":14},"value":"6"}]},{"id":"5","location":{"line":29,"column":3},"cells":[{"location":{"line":29,"column":5},"value":"Isla"},{"location":{"line":29,"column":14},"value":"5"}]}]}},{"id":"7","location":{"line":30,"column":3},"keyword":"Then ","keywordType":"Outcome","text":"this might or might not run"}],"examples":[{"id":"11","tags":[],"location":{"line":32,"column":6},"keyword":"Examples","name":"because we need more tables","description":"","tableHeader":{"id":"8","location":{"line":36,"column":3},"cells":[{"location":{"line":36,"column":5},"value":"what"}]},"tableBody":[{"id":"9","location":{"line":38,"column":3},"cells":[{"location":{"line":38,"column":5},"value":"fail"}]},{"id":"10","location":{"line":39,"column":3},"cells":[{"location":{"line":39,"column":5},"value":"pass"}]}]}]}}],"tags":[]}}]},"comments":[],"uri":"samples/markdown/markdown.feature.md"}} +{"pickle":{"id":"18","uri":"samples/markdown/markdown.feature.md","location":{"line":38,"column":3},"astNodeIds":["12","9"],"name":"Ylajali!","language":"en","steps":[{"id":"14","text":"some TypeScript code:","type":"Context","argument":{"docString":{"content":"type Cheese = 'reblochon' | 'roquefort' | 'rocamadour'","mediaType":"typescript"}},"astNodeIds":["0","9"]},{"id":"15","text":"some classic Gherkin:","type":"Context","argument":{"docString":{"content":"Given there are 24 apples in Mary's basket","mediaType":"gherkin"}},"astNodeIds":["1","9"]},{"id":"16","text":"we use a data table and attach something and then fail","type":"Action","argument":{"dataTable":{"rows":[{"cells":[{"value":"name"},{"value":"age"}]},{"cells":[{"value":"Bill"},{"value":"3"}]},{"cells":[{"value":"Jane"},{"value":"6"}]},{"cells":[{"value":"Isla"},{"value":"5"}]}]}},"astNodeIds":["6","9"]},{"id":"17","text":"this might or might not run","type":"Outcome","astNodeIds":["7","9"]}],"tags":[]}} +{"pickle":{"id":"23","uri":"samples/markdown/markdown.feature.md","location":{"line":39,"column":3},"astNodeIds":["12","10"],"name":"Ylajali!","language":"en","steps":[{"id":"19","text":"some TypeScript code:","type":"Context","argument":{"docString":{"content":"type Cheese = 'reblochon' | 'roquefort' | 'rocamadour'","mediaType":"typescript"}},"astNodeIds":["0","10"]},{"id":"20","text":"some classic Gherkin:","type":"Context","argument":{"docString":{"content":"Given there are 24 apples in Mary's basket","mediaType":"gherkin"}},"astNodeIds":["1","10"]},{"id":"21","text":"we use a data table and attach something and then pass","type":"Action","argument":{"dataTable":{"rows":[{"cells":[{"value":"name"},{"value":"age"}]},{"cells":[{"value":"Bill"},{"value":"3"}]},{"cells":[{"value":"Jane"},{"value":"6"}]},{"cells":[{"value":"Isla"},{"value":"5"}]}]}},"astNodeIds":["6","10"]},{"id":"22","text":"this might or might not run","type":"Outcome","astNodeIds":["7","10"]}],"tags":[]}} +{"stepDefinition":{"id":"24","pattern":{"type":"CUCUMBER_EXPRESSION","source":"some TypeScript code:"},"sourceReference":{"uri":"samples/markdown/markdown.ts","location":{"line":4}}}} +{"stepDefinition":{"id":"25","pattern":{"type":"CUCUMBER_EXPRESSION","source":"some classic Gherkin:"},"sourceReference":{"uri":"samples/markdown/markdown.ts","location":{"line":8}}}} +{"stepDefinition":{"id":"26","pattern":{"type":"CUCUMBER_EXPRESSION","source":"we use a data table and attach something and then {word}"},"sourceReference":{"uri":"samples/markdown/markdown.ts","location":{"line":12}}}} +{"stepDefinition":{"id":"27","pattern":{"type":"CUCUMBER_EXPRESSION","source":"this might or might not run"},"sourceReference":{"uri":"samples/markdown/markdown.ts","location":{"line":23}}}} +{"testRunStarted":{"id":"28","timestamp":{"seconds":0,"nanos":0}}} +{"testCase":{"id":"29","pickleId":"18","testSteps":[{"id":"30","pickleStepId":"14","stepDefinitionIds":["24"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"31","pickleStepId":"15","stepDefinitionIds":["25"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"32","pickleStepId":"16","stepDefinitionIds":["26"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":50,"value":"fail","children":[]},"parameterTypeName":"word"}]}]},{"id":"33","pickleStepId":"17","stepDefinitionIds":["27"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}],"testRunStartedId":"28"}} +{"testCase":{"id":"34","pickleId":"23","testSteps":[{"id":"35","pickleStepId":"19","stepDefinitionIds":["24"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"36","pickleStepId":"20","stepDefinitionIds":["25"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"37","pickleStepId":"21","stepDefinitionIds":["26"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":50,"value":"pass","children":[]},"parameterTypeName":"word"}]}]},{"id":"38","pickleStepId":"22","stepDefinitionIds":["27"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}],"testRunStartedId":"28"}} +{"testCaseStarted":{"id":"39","testCaseId":"29","timestamp":{"seconds":0,"nanos":1000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"39","testStepId":"30","timestamp":{"seconds":0,"nanos":2000000}}} +{"testStepFinished":{"testCaseStartedId":"39","testStepId":"30","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":3000000}}} +{"testStepStarted":{"testCaseStartedId":"39","testStepId":"31","timestamp":{"seconds":0,"nanos":4000000}}} +{"testStepFinished":{"testCaseStartedId":"39","testStepId":"31","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":5000000}}} +{"testStepStarted":{"testCaseStartedId":"39","testStepId":"32","timestamp":{"seconds":0,"nanos":6000000}}} +{"attachment":{"testCaseStartedId":"39","testStepId":"32","body":"We are logging some plain text (fail)","contentEncoding":"IDENTITY","mediaType":"text/x.cucumber.log+plain","timestamp":{"seconds":0,"nanos":7000000}}} +{"testStepFinished":{"testCaseStartedId":"39","testStepId":"32","testStepResult":{"message":"You asked me to fail","exception":{"type":"Error","message":"You asked me to fail","stackTrace":"Error: You asked me to fail\nsamples/markdown/markdown.feature.md:24"},"status":"FAILED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":8000000}}} +{"testStepStarted":{"testCaseStartedId":"39","testStepId":"33","timestamp":{"seconds":0,"nanos":9000000}}} +{"testStepFinished":{"testCaseStartedId":"39","testStepId":"33","testStepResult":{"status":"SKIPPED","duration":{"seconds":0,"nanos":0}},"timestamp":{"seconds":0,"nanos":10000000}}} +{"testCaseFinished":{"testCaseStartedId":"39","timestamp":{"seconds":0,"nanos":11000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"40","testCaseId":"34","timestamp":{"seconds":0,"nanos":12000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"40","testStepId":"35","timestamp":{"seconds":0,"nanos":13000000}}} +{"testStepFinished":{"testCaseStartedId":"40","testStepId":"35","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":14000000}}} +{"testStepStarted":{"testCaseStartedId":"40","testStepId":"36","timestamp":{"seconds":0,"nanos":15000000}}} +{"testStepFinished":{"testCaseStartedId":"40","testStepId":"36","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":16000000}}} +{"testStepStarted":{"testCaseStartedId":"40","testStepId":"37","timestamp":{"seconds":0,"nanos":17000000}}} +{"attachment":{"testCaseStartedId":"40","testStepId":"37","body":"We are logging some plain text (pass)","contentEncoding":"IDENTITY","mediaType":"text/x.cucumber.log+plain","timestamp":{"seconds":0,"nanos":18000000}}} +{"testStepFinished":{"testCaseStartedId":"40","testStepId":"37","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":19000000}}} +{"testStepStarted":{"testCaseStartedId":"40","testStepId":"38","timestamp":{"seconds":0,"nanos":20000000}}} +{"testStepFinished":{"testCaseStartedId":"40","testStepId":"38","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":21000000}}} +{"testCaseFinished":{"testCaseStartedId":"40","timestamp":{"seconds":0,"nanos":22000000},"willBeRetried":false}} +{"testRunFinished":{"testRunStartedId":"28","timestamp":{"seconds":0,"nanos":23000000},"success":false}} diff --git a/compatibility/markdown/markdown.ts b/compatibility/markdown/markdown.ts new file mode 100644 index 00000000..7af55d90 --- /dev/null +++ b/compatibility/markdown/markdown.ts @@ -0,0 +1,25 @@ +import assert from 'node:assert' +import { Given, When, Then } from '@cucumber/fake-cucumber' + +Given('some TypeScript code:', function (dataTable: string[][]) { + assert(dataTable) +}) + +Given('some classic Gherkin:', function (gherkin: string) { + assert(gherkin) +}) + +When( + 'we use a data table and attach something and then {word}', + async function (word: string, dataTable: string[][]) { + assert(dataTable) + await this.log(`We are logging some plain text (${word})`) + if (word === 'fail') { + throw new Error('You asked me to fail') + } + } +) + +Then('this might or might not run', function () { + // no-op +}) diff --git a/compatibility/minimal/minimal.cpp b/compatibility/minimal/minimal.cpp new file mode 100644 index 00000000..805969b1 --- /dev/null +++ b/compatibility/minimal/minimal.cpp @@ -0,0 +1,7 @@ +#include "cucumber_cpp/CucumberCpp.hpp" +#include + +STEP(R"(I have {int} cukes in my belly)", (std::int32_t number)) +{ + // no-op +} diff --git a/compatibility/minimal/minimal.feature b/compatibility/minimal/minimal.feature new file mode 100644 index 00000000..158fde29 --- /dev/null +++ b/compatibility/minimal/minimal.feature @@ -0,0 +1,10 @@ +Feature: minimal + + Cucumber doesn't execute this markdown, but @cucumber/react renders it. + + * This is + * a bullet + * list + + Scenario: cukes + Given I have 42 cukes in my belly diff --git a/compatibility/minimal/minimal.ndjson b/compatibility/minimal/minimal.ndjson new file mode 100644 index 00000000..f58addeb --- /dev/null +++ b/compatibility/minimal/minimal.ndjson @@ -0,0 +1,12 @@ +{"meta":{"protocolVersion":"31.0.0","implementation":{"name":"fake-cucumber","version":"123.45.6"},"cpu":{"name":"arm64"},"os":{"name":"darwin","version":"24.5.0"},"runtime":{"name":"Node.js","version":"24.4.1"},"ci":{"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429","buildNumber":"154666429","git":{"revision":"99684bcacf01d95875834d87903dcb072306c9ad","remote":"https://github.com/cucumber-ltd/shouty.rb.git","branch":"main"}}}} +{"source":{"data":"Feature: minimal\n \n Cucumber doesn't execute this markdown, but @cucumber/react renders it.\n \n * This is\n * a bullet\n * list\n \n Scenario: cukes\n Given I have 42 cukes in my belly\n","uri":"samples/minimal/minimal.feature","mediaType":"text/x.cucumber.gherkin+plain"}} +{"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"minimal","description":" Cucumber doesn't execute this markdown, but @cucumber/react renders it.\n \n * This is\n * a bullet\n * list","children":[{"scenario":{"id":"1","tags":[],"location":{"line":9,"column":3},"keyword":"Scenario","name":"cukes","description":"","steps":[{"id":"0","location":{"line":10,"column":5},"keyword":"Given ","keywordType":"Context","text":"I have 42 cukes in my belly"}],"examples":[]}}]},"comments":[],"uri":"samples/minimal/minimal.feature"}} +{"pickle":{"id":"3","uri":"samples/minimal/minimal.feature","location":{"line":9,"column":3},"astNodeIds":["1"],"tags":[],"name":"cukes","language":"en","steps":[{"id":"2","text":"I have 42 cukes in my belly","type":"Context","astNodeIds":["0"]}]}} +{"stepDefinition":{"id":"4","pattern":{"type":"CUCUMBER_EXPRESSION","source":"I have {int} cukes in my belly"},"sourceReference":{"uri":"samples/minimal/minimal.ts","location":{"line":3}}}} +{"testRunStarted":{"id":"5","timestamp":{"seconds":0,"nanos":0}}} +{"testCase":{"id":"6","pickleId":"3","testSteps":[{"id":"7","pickleStepId":"2","stepDefinitionIds":["4"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":7,"value":"42","children":[]},"parameterTypeName":"int"}]}]}],"testRunStartedId":"5"}} +{"testCaseStarted":{"id":"8","testCaseId":"6","timestamp":{"seconds":0,"nanos":1000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"8","testStepId":"7","timestamp":{"seconds":0,"nanos":2000000}}} +{"testStepFinished":{"testCaseStartedId":"8","testStepId":"7","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":3000000}}} +{"testCaseFinished":{"testCaseStartedId":"8","timestamp":{"seconds":0,"nanos":4000000},"willBeRetried":false}} +{"testRunFinished":{"testRunStartedId":"5","timestamp":{"seconds":0,"nanos":5000000},"success":true}} diff --git a/compatibility/minimal/minimal.ts b/compatibility/minimal/minimal.ts new file mode 100644 index 00000000..969e74a0 --- /dev/null +++ b/compatibility/minimal/minimal.ts @@ -0,0 +1,5 @@ +import { Given } from '@cucumber/fake-cucumber' + +Given('I have {int} cukes in my belly', function (cukeCount: number) { + // no-op +}) diff --git a/compatibility/multiple-features-reversed/multiple-features-reversed-1.feature b/compatibility/multiple-features-reversed/multiple-features-reversed-1.feature new file mode 100644 index 00000000..faf03eee --- /dev/null +++ b/compatibility/multiple-features-reversed/multiple-features-reversed-1.feature @@ -0,0 +1,10 @@ +Feature: First feature + + Scenario: First scenario + Given an order for "eggs" + + Scenario: Second scenario + Given an order for "milk" + + Scenario: Third scenario + Given an order for "bread" \ No newline at end of file diff --git a/compatibility/multiple-features-reversed/multiple-features-reversed-2.feature b/compatibility/multiple-features-reversed/multiple-features-reversed-2.feature new file mode 100644 index 00000000..0b0f7569 --- /dev/null +++ b/compatibility/multiple-features-reversed/multiple-features-reversed-2.feature @@ -0,0 +1,10 @@ +Feature: Second feature + + Scenario: First scenario + Given an order for "batteries" + + Scenario: Second scenario + Given an order for "light bulbs" + + Scenario: Third scenario + Given an order for "fuses" \ No newline at end of file diff --git a/compatibility/multiple-features-reversed/multiple-features-reversed-3.feature b/compatibility/multiple-features-reversed/multiple-features-reversed-3.feature new file mode 100644 index 00000000..4c306c6c --- /dev/null +++ b/compatibility/multiple-features-reversed/multiple-features-reversed-3.feature @@ -0,0 +1,10 @@ +Feature: Third feature + + Scenario: First scenario + Given an order for "pencils" + + Scenario: Second scenario + Given an order for "rulers" + + Scenario: Third scenario + Given an order for "paperclips" \ No newline at end of file diff --git a/compatibility/multiple-features-reversed/multiple-features-reversed.arguments.txt b/compatibility/multiple-features-reversed/multiple-features-reversed.arguments.txt new file mode 100644 index 00000000..97072c0c --- /dev/null +++ b/compatibility/multiple-features-reversed/multiple-features-reversed.arguments.txt @@ -0,0 +1 @@ +--order reverse diff --git a/compatibility/multiple-features-reversed/multiple-features-reversed.cpp b/compatibility/multiple-features-reversed/multiple-features-reversed.cpp new file mode 100644 index 00000000..08abbd51 --- /dev/null +++ b/compatibility/multiple-features-reversed/multiple-features-reversed.cpp @@ -0,0 +1,7 @@ +#include "cucumber_cpp/CucumberCpp.hpp" +#include + +STEP(R"(an order for {string})", ([[maybe_unused]] const std::string& order)) +{ + // no-op +} diff --git a/compatibility/multiple-features-reversed/multiple-features-reversed.ndjson b/compatibility/multiple-features-reversed/multiple-features-reversed.ndjson new file mode 100644 index 00000000..7516fb80 --- /dev/null +++ b/compatibility/multiple-features-reversed/multiple-features-reversed.ndjson @@ -0,0 +1,64 @@ +{"meta":{"protocolVersion":"31.0.0","implementation":{"name":"fake-cucumber","version":"123.45.6"},"cpu":{"name":"arm64"},"os":{"name":"darwin","version":"24.5.0"},"runtime":{"name":"Node.js","version":"24.4.1"},"ci":{"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429","buildNumber":"154666429","git":{"revision":"99684bcacf01d95875834d87903dcb072306c9ad","remote":"https://github.com/cucumber-ltd/shouty.rb.git","branch":"main"}}}} +{"source":{"data":"Feature: First feature\n\n Scenario: First scenario\n Given an order for \"eggs\"\n\n Scenario: Second scenario\n Given an order for \"milk\"\n\n Scenario: Third scenario\n Given an order for \"bread\"","uri":"samples/multiple-features-reversed/multiple-features-reversed-1.feature","mediaType":"text/x.cucumber.gherkin+plain"}} +{"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"First feature","description":"","children":[{"scenario":{"id":"1","tags":[],"location":{"line":3,"column":3},"keyword":"Scenario","name":"First scenario","description":"","steps":[{"id":"0","location":{"line":4,"column":5},"keyword":"Given ","keywordType":"Context","text":"an order for \"eggs\""}],"examples":[]}},{"scenario":{"id":"3","tags":[],"location":{"line":6,"column":3},"keyword":"Scenario","name":"Second scenario","description":"","steps":[{"id":"2","location":{"line":7,"column":5},"keyword":"Given ","keywordType":"Context","text":"an order for \"milk\""}],"examples":[]}},{"scenario":{"id":"5","tags":[],"location":{"line":9,"column":3},"keyword":"Scenario","name":"Third scenario","description":"","steps":[{"id":"4","location":{"line":10,"column":5},"keyword":"Given ","keywordType":"Context","text":"an order for \"bread\""}],"examples":[]}}]},"comments":[],"uri":"samples/multiple-features-reversed/multiple-features-reversed-1.feature"}} +{"pickle":{"id":"7","uri":"samples/multiple-features-reversed/multiple-features-reversed-1.feature","location":{"line":3,"column":3},"astNodeIds":["1"],"tags":[],"name":"First scenario","language":"en","steps":[{"id":"6","text":"an order for \"eggs\"","type":"Context","astNodeIds":["0"]}]}} +{"pickle":{"id":"9","uri":"samples/multiple-features-reversed/multiple-features-reversed-1.feature","location":{"line":6,"column":3},"astNodeIds":["3"],"tags":[],"name":"Second scenario","language":"en","steps":[{"id":"8","text":"an order for \"milk\"","type":"Context","astNodeIds":["2"]}]}} +{"pickle":{"id":"11","uri":"samples/multiple-features-reversed/multiple-features-reversed-1.feature","location":{"line":9,"column":3},"astNodeIds":["5"],"tags":[],"name":"Third scenario","language":"en","steps":[{"id":"10","text":"an order for \"bread\"","type":"Context","astNodeIds":["4"]}]}} +{"source":{"data":"Feature: Second feature\n\n Scenario: First scenario\n Given an order for \"batteries\"\n\n Scenario: Second scenario\n Given an order for \"light bulbs\"\n\n Scenario: Third scenario\n Given an order for \"fuses\"","uri":"samples/multiple-features-reversed/multiple-features-reversed-2.feature","mediaType":"text/x.cucumber.gherkin+plain"}} +{"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"Second feature","description":"","children":[{"scenario":{"id":"13","tags":[],"location":{"line":3,"column":3},"keyword":"Scenario","name":"First scenario","description":"","steps":[{"id":"12","location":{"line":4,"column":5},"keyword":"Given ","keywordType":"Context","text":"an order for \"batteries\""}],"examples":[]}},{"scenario":{"id":"15","tags":[],"location":{"line":6,"column":3},"keyword":"Scenario","name":"Second scenario","description":"","steps":[{"id":"14","location":{"line":7,"column":5},"keyword":"Given ","keywordType":"Context","text":"an order for \"light bulbs\""}],"examples":[]}},{"scenario":{"id":"17","tags":[],"location":{"line":9,"column":3},"keyword":"Scenario","name":"Third scenario","description":"","steps":[{"id":"16","location":{"line":10,"column":5},"keyword":"Given ","keywordType":"Context","text":"an order for \"fuses\""}],"examples":[]}}]},"comments":[],"uri":"samples/multiple-features-reversed/multiple-features-reversed-2.feature"}} +{"pickle":{"id":"19","uri":"samples/multiple-features-reversed/multiple-features-reversed-2.feature","location":{"line":3,"column":3},"astNodeIds":["13"],"tags":[],"name":"First scenario","language":"en","steps":[{"id":"18","text":"an order for \"batteries\"","type":"Context","astNodeIds":["12"]}]}} +{"pickle":{"id":"21","uri":"samples/multiple-features-reversed/multiple-features-reversed-2.feature","location":{"line":6,"column":3},"astNodeIds":["15"],"tags":[],"name":"Second scenario","language":"en","steps":[{"id":"20","text":"an order for \"light bulbs\"","type":"Context","astNodeIds":["14"]}]}} +{"pickle":{"id":"23","uri":"samples/multiple-features-reversed/multiple-features-reversed-2.feature","location":{"line":9,"column":3},"astNodeIds":["17"],"tags":[],"name":"Third scenario","language":"en","steps":[{"id":"22","text":"an order for \"fuses\"","type":"Context","astNodeIds":["16"]}]}} +{"source":{"data":"Feature: Third feature\n\n Scenario: First scenario\n Given an order for \"pencils\"\n\n Scenario: Second scenario\n Given an order for \"rulers\"\n\n Scenario: Third scenario\n Given an order for \"paperclips\"","uri":"samples/multiple-features-reversed/multiple-features-reversed-3.feature","mediaType":"text/x.cucumber.gherkin+plain"}} +{"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"Third feature","description":"","children":[{"scenario":{"id":"25","tags":[],"location":{"line":3,"column":3},"keyword":"Scenario","name":"First scenario","description":"","steps":[{"id":"24","location":{"line":4,"column":5},"keyword":"Given ","keywordType":"Context","text":"an order for \"pencils\""}],"examples":[]}},{"scenario":{"id":"27","tags":[],"location":{"line":6,"column":3},"keyword":"Scenario","name":"Second scenario","description":"","steps":[{"id":"26","location":{"line":7,"column":5},"keyword":"Given ","keywordType":"Context","text":"an order for \"rulers\""}],"examples":[]}},{"scenario":{"id":"29","tags":[],"location":{"line":9,"column":3},"keyword":"Scenario","name":"Third scenario","description":"","steps":[{"id":"28","location":{"line":10,"column":5},"keyword":"Given ","keywordType":"Context","text":"an order for \"paperclips\""}],"examples":[]}}]},"comments":[],"uri":"samples/multiple-features-reversed/multiple-features-reversed-3.feature"}} +{"pickle":{"id":"31","uri":"samples/multiple-features-reversed/multiple-features-reversed-3.feature","location":{"line":3,"column":3},"astNodeIds":["25"],"tags":[],"name":"First scenario","language":"en","steps":[{"id":"30","text":"an order for \"pencils\"","type":"Context","astNodeIds":["24"]}]}} +{"pickle":{"id":"33","uri":"samples/multiple-features-reversed/multiple-features-reversed-3.feature","location":{"line":6,"column":3},"astNodeIds":["27"],"tags":[],"name":"Second scenario","language":"en","steps":[{"id":"32","text":"an order for \"rulers\"","type":"Context","astNodeIds":["26"]}]}} +{"pickle":{"id":"35","uri":"samples/multiple-features-reversed/multiple-features-reversed-3.feature","location":{"line":9,"column":3},"astNodeIds":["29"],"tags":[],"name":"Third scenario","language":"en","steps":[{"id":"34","text":"an order for \"paperclips\"","type":"Context","astNodeIds":["28"]}]}} +{"stepDefinition":{"id":"36","pattern":{"type":"CUCUMBER_EXPRESSION","source":"an order for {string}"},"sourceReference":{"uri":"samples/multiple-features-reversed/multiple-features-reversed.ts","location":{"line":3}}}} +{"testRunStarted":{"id":"37","timestamp":{"seconds":0,"nanos":0}}} +{"testCase":{"id":"38","pickleId":"35","testSteps":[{"id":"39","pickleStepId":"34","stepDefinitionIds":["36"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":13,"value":"\"paperclips\"","children":[{"start":14,"value":"paperclips","children":[{"children":[]}]},{"children":[{"children":[]}]}]},"parameterTypeName":"string"}]}]}],"testRunStartedId":"37"}} +{"testCase":{"id":"40","pickleId":"33","testSteps":[{"id":"41","pickleStepId":"32","stepDefinitionIds":["36"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":13,"value":"\"rulers\"","children":[{"start":14,"value":"rulers","children":[{"children":[]}]},{"children":[{"children":[]}]}]},"parameterTypeName":"string"}]}]}],"testRunStartedId":"37"}} +{"testCase":{"id":"42","pickleId":"31","testSteps":[{"id":"43","pickleStepId":"30","stepDefinitionIds":["36"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":13,"value":"\"pencils\"","children":[{"start":14,"value":"pencils","children":[{"children":[]}]},{"children":[{"children":[]}]}]},"parameterTypeName":"string"}]}]}],"testRunStartedId":"37"}} +{"testCase":{"id":"44","pickleId":"23","testSteps":[{"id":"45","pickleStepId":"22","stepDefinitionIds":["36"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":13,"value":"\"fuses\"","children":[{"start":14,"value":"fuses","children":[{"children":[]}]},{"children":[{"children":[]}]}]},"parameterTypeName":"string"}]}]}],"testRunStartedId":"37"}} +{"testCase":{"id":"46","pickleId":"21","testSteps":[{"id":"47","pickleStepId":"20","stepDefinitionIds":["36"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":13,"value":"\"light bulbs\"","children":[{"start":14,"value":"light bulbs","children":[{"children":[]}]},{"children":[{"children":[]}]}]},"parameterTypeName":"string"}]}]}],"testRunStartedId":"37"}} +{"testCase":{"id":"48","pickleId":"19","testSteps":[{"id":"49","pickleStepId":"18","stepDefinitionIds":["36"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":13,"value":"\"batteries\"","children":[{"start":14,"value":"batteries","children":[{"children":[]}]},{"children":[{"children":[]}]}]},"parameterTypeName":"string"}]}]}],"testRunStartedId":"37"}} +{"testCase":{"id":"50","pickleId":"11","testSteps":[{"id":"51","pickleStepId":"10","stepDefinitionIds":["36"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":13,"value":"\"bread\"","children":[{"start":14,"value":"bread","children":[{"children":[]}]},{"children":[{"children":[]}]}]},"parameterTypeName":"string"}]}]}],"testRunStartedId":"37"}} +{"testCase":{"id":"52","pickleId":"9","testSteps":[{"id":"53","pickleStepId":"8","stepDefinitionIds":["36"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":13,"value":"\"milk\"","children":[{"start":14,"value":"milk","children":[{"children":[]}]},{"children":[{"children":[]}]}]},"parameterTypeName":"string"}]}]}],"testRunStartedId":"37"}} +{"testCase":{"id":"54","pickleId":"7","testSteps":[{"id":"55","pickleStepId":"6","stepDefinitionIds":["36"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":13,"value":"\"eggs\"","children":[{"start":14,"value":"eggs","children":[{"children":[]}]},{"children":[{"children":[]}]}]},"parameterTypeName":"string"}]}]}],"testRunStartedId":"37"}} +{"testCaseStarted":{"id":"56","testCaseId":"38","timestamp":{"seconds":0,"nanos":1000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"56","testStepId":"39","timestamp":{"seconds":0,"nanos":2000000}}} +{"testStepFinished":{"testCaseStartedId":"56","testStepId":"39","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":3000000}}} +{"testCaseFinished":{"testCaseStartedId":"56","timestamp":{"seconds":0,"nanos":4000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"57","testCaseId":"40","timestamp":{"seconds":0,"nanos":5000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"57","testStepId":"41","timestamp":{"seconds":0,"nanos":6000000}}} +{"testStepFinished":{"testCaseStartedId":"57","testStepId":"41","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":7000000}}} +{"testCaseFinished":{"testCaseStartedId":"57","timestamp":{"seconds":0,"nanos":8000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"58","testCaseId":"42","timestamp":{"seconds":0,"nanos":9000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"58","testStepId":"43","timestamp":{"seconds":0,"nanos":10000000}}} +{"testStepFinished":{"testCaseStartedId":"58","testStepId":"43","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":11000000}}} +{"testCaseFinished":{"testCaseStartedId":"58","timestamp":{"seconds":0,"nanos":12000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"59","testCaseId":"44","timestamp":{"seconds":0,"nanos":13000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"59","testStepId":"45","timestamp":{"seconds":0,"nanos":14000000}}} +{"testStepFinished":{"testCaseStartedId":"59","testStepId":"45","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":15000000}}} +{"testCaseFinished":{"testCaseStartedId":"59","timestamp":{"seconds":0,"nanos":16000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"60","testCaseId":"46","timestamp":{"seconds":0,"nanos":17000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"60","testStepId":"47","timestamp":{"seconds":0,"nanos":18000000}}} +{"testStepFinished":{"testCaseStartedId":"60","testStepId":"47","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":19000000}}} +{"testCaseFinished":{"testCaseStartedId":"60","timestamp":{"seconds":0,"nanos":20000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"61","testCaseId":"48","timestamp":{"seconds":0,"nanos":21000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"61","testStepId":"49","timestamp":{"seconds":0,"nanos":22000000}}} +{"testStepFinished":{"testCaseStartedId":"61","testStepId":"49","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":23000000}}} +{"testCaseFinished":{"testCaseStartedId":"61","timestamp":{"seconds":0,"nanos":24000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"62","testCaseId":"50","timestamp":{"seconds":0,"nanos":25000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"62","testStepId":"51","timestamp":{"seconds":0,"nanos":26000000}}} +{"testStepFinished":{"testCaseStartedId":"62","testStepId":"51","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":27000000}}} +{"testCaseFinished":{"testCaseStartedId":"62","timestamp":{"seconds":0,"nanos":28000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"63","testCaseId":"52","timestamp":{"seconds":0,"nanos":29000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"63","testStepId":"53","timestamp":{"seconds":0,"nanos":30000000}}} +{"testStepFinished":{"testCaseStartedId":"63","testStepId":"53","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":31000000}}} +{"testCaseFinished":{"testCaseStartedId":"63","timestamp":{"seconds":0,"nanos":32000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"64","testCaseId":"54","timestamp":{"seconds":0,"nanos":33000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"64","testStepId":"55","timestamp":{"seconds":0,"nanos":34000000}}} +{"testStepFinished":{"testCaseStartedId":"64","testStepId":"55","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":35000000}}} +{"testCaseFinished":{"testCaseStartedId":"64","timestamp":{"seconds":0,"nanos":36000000},"willBeRetried":false}} +{"testRunFinished":{"testRunStartedId":"37","timestamp":{"seconds":0,"nanos":37000000},"success":true}} diff --git a/compatibility/multiple-features-reversed/multiple-features-reversed.ts b/compatibility/multiple-features-reversed/multiple-features-reversed.ts new file mode 100644 index 00000000..e41dae39 --- /dev/null +++ b/compatibility/multiple-features-reversed/multiple-features-reversed.ts @@ -0,0 +1,5 @@ +import { Given } from '@cucumber/fake-cucumber' + +Given('an order for {string}', () => { + // no-op +}) diff --git a/compatibility/multiple-features/multiple-features-1.feature b/compatibility/multiple-features/multiple-features-1.feature new file mode 100644 index 00000000..faf03eee --- /dev/null +++ b/compatibility/multiple-features/multiple-features-1.feature @@ -0,0 +1,10 @@ +Feature: First feature + + Scenario: First scenario + Given an order for "eggs" + + Scenario: Second scenario + Given an order for "milk" + + Scenario: Third scenario + Given an order for "bread" \ No newline at end of file diff --git a/compatibility/multiple-features/multiple-features-2.feature b/compatibility/multiple-features/multiple-features-2.feature new file mode 100644 index 00000000..0b0f7569 --- /dev/null +++ b/compatibility/multiple-features/multiple-features-2.feature @@ -0,0 +1,10 @@ +Feature: Second feature + + Scenario: First scenario + Given an order for "batteries" + + Scenario: Second scenario + Given an order for "light bulbs" + + Scenario: Third scenario + Given an order for "fuses" \ No newline at end of file diff --git a/compatibility/multiple-features/multiple-features-3.feature b/compatibility/multiple-features/multiple-features-3.feature new file mode 100644 index 00000000..4c306c6c --- /dev/null +++ b/compatibility/multiple-features/multiple-features-3.feature @@ -0,0 +1,10 @@ +Feature: Third feature + + Scenario: First scenario + Given an order for "pencils" + + Scenario: Second scenario + Given an order for "rulers" + + Scenario: Third scenario + Given an order for "paperclips" \ No newline at end of file diff --git a/compatibility/multiple-features/multiple-features.cpp b/compatibility/multiple-features/multiple-features.cpp new file mode 100644 index 00000000..08abbd51 --- /dev/null +++ b/compatibility/multiple-features/multiple-features.cpp @@ -0,0 +1,7 @@ +#include "cucumber_cpp/CucumberCpp.hpp" +#include + +STEP(R"(an order for {string})", ([[maybe_unused]] const std::string& order)) +{ + // no-op +} diff --git a/compatibility/multiple-features/multiple-features.ndjson b/compatibility/multiple-features/multiple-features.ndjson new file mode 100644 index 00000000..e3fbf473 --- /dev/null +++ b/compatibility/multiple-features/multiple-features.ndjson @@ -0,0 +1,64 @@ +{"meta":{"protocolVersion":"31.0.0","implementation":{"name":"fake-cucumber","version":"123.45.6"},"cpu":{"name":"arm64"},"os":{"name":"darwin","version":"24.5.0"},"runtime":{"name":"Node.js","version":"24.4.1"},"ci":{"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429","buildNumber":"154666429","git":{"revision":"99684bcacf01d95875834d87903dcb072306c9ad","remote":"https://github.com/cucumber-ltd/shouty.rb.git","branch":"main"}}}} +{"source":{"data":"Feature: First feature\n\n Scenario: First scenario\n Given an order for \"eggs\"\n\n Scenario: Second scenario\n Given an order for \"milk\"\n\n Scenario: Third scenario\n Given an order for \"bread\"","uri":"samples/multiple-features/multiple-features-1.feature","mediaType":"text/x.cucumber.gherkin+plain"}} +{"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"First feature","description":"","children":[{"scenario":{"id":"1","tags":[],"location":{"line":3,"column":3},"keyword":"Scenario","name":"First scenario","description":"","steps":[{"id":"0","location":{"line":4,"column":5},"keyword":"Given ","keywordType":"Context","text":"an order for \"eggs\""}],"examples":[]}},{"scenario":{"id":"3","tags":[],"location":{"line":6,"column":3},"keyword":"Scenario","name":"Second scenario","description":"","steps":[{"id":"2","location":{"line":7,"column":5},"keyword":"Given ","keywordType":"Context","text":"an order for \"milk\""}],"examples":[]}},{"scenario":{"id":"5","tags":[],"location":{"line":9,"column":3},"keyword":"Scenario","name":"Third scenario","description":"","steps":[{"id":"4","location":{"line":10,"column":5},"keyword":"Given ","keywordType":"Context","text":"an order for \"bread\""}],"examples":[]}}]},"comments":[],"uri":"samples/multiple-features/multiple-features-1.feature"}} +{"pickle":{"id":"7","uri":"samples/multiple-features/multiple-features-1.feature","location":{"line":3,"column":3},"astNodeIds":["1"],"tags":[],"name":"First scenario","language":"en","steps":[{"id":"6","text":"an order for \"eggs\"","type":"Context","astNodeIds":["0"]}]}} +{"pickle":{"id":"9","uri":"samples/multiple-features/multiple-features-1.feature","location":{"line":6,"column":3},"astNodeIds":["3"],"tags":[],"name":"Second scenario","language":"en","steps":[{"id":"8","text":"an order for \"milk\"","type":"Context","astNodeIds":["2"]}]}} +{"pickle":{"id":"11","uri":"samples/multiple-features/multiple-features-1.feature","location":{"line":9,"column":3},"astNodeIds":["5"],"tags":[],"name":"Third scenario","language":"en","steps":[{"id":"10","text":"an order for \"bread\"","type":"Context","astNodeIds":["4"]}]}} +{"source":{"data":"Feature: Second feature\n\n Scenario: First scenario\n Given an order for \"batteries\"\n\n Scenario: Second scenario\n Given an order for \"light bulbs\"\n\n Scenario: Third scenario\n Given an order for \"fuses\"","uri":"samples/multiple-features/multiple-features-2.feature","mediaType":"text/x.cucumber.gherkin+plain"}} +{"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"Second feature","description":"","children":[{"scenario":{"id":"13","tags":[],"location":{"line":3,"column":3},"keyword":"Scenario","name":"First scenario","description":"","steps":[{"id":"12","location":{"line":4,"column":5},"keyword":"Given ","keywordType":"Context","text":"an order for \"batteries\""}],"examples":[]}},{"scenario":{"id":"15","tags":[],"location":{"line":6,"column":3},"keyword":"Scenario","name":"Second scenario","description":"","steps":[{"id":"14","location":{"line":7,"column":5},"keyword":"Given ","keywordType":"Context","text":"an order for \"light bulbs\""}],"examples":[]}},{"scenario":{"id":"17","tags":[],"location":{"line":9,"column":3},"keyword":"Scenario","name":"Third scenario","description":"","steps":[{"id":"16","location":{"line":10,"column":5},"keyword":"Given ","keywordType":"Context","text":"an order for \"fuses\""}],"examples":[]}}]},"comments":[],"uri":"samples/multiple-features/multiple-features-2.feature"}} +{"pickle":{"id":"19","uri":"samples/multiple-features/multiple-features-2.feature","location":{"line":3,"column":3},"astNodeIds":["13"],"tags":[],"name":"First scenario","language":"en","steps":[{"id":"18","text":"an order for \"batteries\"","type":"Context","astNodeIds":["12"]}]}} +{"pickle":{"id":"21","uri":"samples/multiple-features/multiple-features-2.feature","location":{"line":6,"column":3},"astNodeIds":["15"],"tags":[],"name":"Second scenario","language":"en","steps":[{"id":"20","text":"an order for \"light bulbs\"","type":"Context","astNodeIds":["14"]}]}} +{"pickle":{"id":"23","uri":"samples/multiple-features/multiple-features-2.feature","location":{"line":9,"column":3},"astNodeIds":["17"],"tags":[],"name":"Third scenario","language":"en","steps":[{"id":"22","text":"an order for \"fuses\"","type":"Context","astNodeIds":["16"]}]}} +{"source":{"data":"Feature: Third feature\n\n Scenario: First scenario\n Given an order for \"pencils\"\n\n Scenario: Second scenario\n Given an order for \"rulers\"\n\n Scenario: Third scenario\n Given an order for \"paperclips\"","uri":"samples/multiple-features/multiple-features-3.feature","mediaType":"text/x.cucumber.gherkin+plain"}} +{"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"Third feature","description":"","children":[{"scenario":{"id":"25","tags":[],"location":{"line":3,"column":3},"keyword":"Scenario","name":"First scenario","description":"","steps":[{"id":"24","location":{"line":4,"column":5},"keyword":"Given ","keywordType":"Context","text":"an order for \"pencils\""}],"examples":[]}},{"scenario":{"id":"27","tags":[],"location":{"line":6,"column":3},"keyword":"Scenario","name":"Second scenario","description":"","steps":[{"id":"26","location":{"line":7,"column":5},"keyword":"Given ","keywordType":"Context","text":"an order for \"rulers\""}],"examples":[]}},{"scenario":{"id":"29","tags":[],"location":{"line":9,"column":3},"keyword":"Scenario","name":"Third scenario","description":"","steps":[{"id":"28","location":{"line":10,"column":5},"keyword":"Given ","keywordType":"Context","text":"an order for \"paperclips\""}],"examples":[]}}]},"comments":[],"uri":"samples/multiple-features/multiple-features-3.feature"}} +{"pickle":{"id":"31","uri":"samples/multiple-features/multiple-features-3.feature","location":{"line":3,"column":3},"astNodeIds":["25"],"tags":[],"name":"First scenario","language":"en","steps":[{"id":"30","text":"an order for \"pencils\"","type":"Context","astNodeIds":["24"]}]}} +{"pickle":{"id":"33","uri":"samples/multiple-features/multiple-features-3.feature","location":{"line":6,"column":3},"astNodeIds":["27"],"tags":[],"name":"Second scenario","language":"en","steps":[{"id":"32","text":"an order for \"rulers\"","type":"Context","astNodeIds":["26"]}]}} +{"pickle":{"id":"35","uri":"samples/multiple-features/multiple-features-3.feature","location":{"line":9,"column":3},"astNodeIds":["29"],"tags":[],"name":"Third scenario","language":"en","steps":[{"id":"34","text":"an order for \"paperclips\"","type":"Context","astNodeIds":["28"]}]}} +{"stepDefinition":{"id":"36","pattern":{"type":"CUCUMBER_EXPRESSION","source":"an order for {string}"},"sourceReference":{"uri":"samples/multiple-features/multiple-features.ts","location":{"line":3}}}} +{"testRunStarted":{"id":"37","timestamp":{"seconds":0,"nanos":0}}} +{"testCase":{"id":"38","pickleId":"7","testSteps":[{"id":"39","pickleStepId":"6","stepDefinitionIds":["36"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":13,"value":"\"eggs\"","children":[{"start":14,"value":"eggs","children":[{"children":[]}]},{"children":[{"children":[]}]}]},"parameterTypeName":"string"}]}]}],"testRunStartedId":"37"}} +{"testCase":{"id":"40","pickleId":"9","testSteps":[{"id":"41","pickleStepId":"8","stepDefinitionIds":["36"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":13,"value":"\"milk\"","children":[{"start":14,"value":"milk","children":[{"children":[]}]},{"children":[{"children":[]}]}]},"parameterTypeName":"string"}]}]}],"testRunStartedId":"37"}} +{"testCase":{"id":"42","pickleId":"11","testSteps":[{"id":"43","pickleStepId":"10","stepDefinitionIds":["36"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":13,"value":"\"bread\"","children":[{"start":14,"value":"bread","children":[{"children":[]}]},{"children":[{"children":[]}]}]},"parameterTypeName":"string"}]}]}],"testRunStartedId":"37"}} +{"testCase":{"id":"44","pickleId":"19","testSteps":[{"id":"45","pickleStepId":"18","stepDefinitionIds":["36"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":13,"value":"\"batteries\"","children":[{"start":14,"value":"batteries","children":[{"children":[]}]},{"children":[{"children":[]}]}]},"parameterTypeName":"string"}]}]}],"testRunStartedId":"37"}} +{"testCase":{"id":"46","pickleId":"21","testSteps":[{"id":"47","pickleStepId":"20","stepDefinitionIds":["36"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":13,"value":"\"light bulbs\"","children":[{"start":14,"value":"light bulbs","children":[{"children":[]}]},{"children":[{"children":[]}]}]},"parameterTypeName":"string"}]}]}],"testRunStartedId":"37"}} +{"testCase":{"id":"48","pickleId":"23","testSteps":[{"id":"49","pickleStepId":"22","stepDefinitionIds":["36"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":13,"value":"\"fuses\"","children":[{"start":14,"value":"fuses","children":[{"children":[]}]},{"children":[{"children":[]}]}]},"parameterTypeName":"string"}]}]}],"testRunStartedId":"37"}} +{"testCase":{"id":"50","pickleId":"31","testSteps":[{"id":"51","pickleStepId":"30","stepDefinitionIds":["36"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":13,"value":"\"pencils\"","children":[{"start":14,"value":"pencils","children":[{"children":[]}]},{"children":[{"children":[]}]}]},"parameterTypeName":"string"}]}]}],"testRunStartedId":"37"}} +{"testCase":{"id":"52","pickleId":"33","testSteps":[{"id":"53","pickleStepId":"32","stepDefinitionIds":["36"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":13,"value":"\"rulers\"","children":[{"start":14,"value":"rulers","children":[{"children":[]}]},{"children":[{"children":[]}]}]},"parameterTypeName":"string"}]}]}],"testRunStartedId":"37"}} +{"testCase":{"id":"54","pickleId":"35","testSteps":[{"id":"55","pickleStepId":"34","stepDefinitionIds":["36"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":13,"value":"\"paperclips\"","children":[{"start":14,"value":"paperclips","children":[{"children":[]}]},{"children":[{"children":[]}]}]},"parameterTypeName":"string"}]}]}],"testRunStartedId":"37"}} +{"testCaseStarted":{"id":"56","testCaseId":"38","timestamp":{"seconds":0,"nanos":1000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"56","testStepId":"39","timestamp":{"seconds":0,"nanos":2000000}}} +{"testStepFinished":{"testCaseStartedId":"56","testStepId":"39","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":3000000}}} +{"testCaseFinished":{"testCaseStartedId":"56","timestamp":{"seconds":0,"nanos":4000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"57","testCaseId":"40","timestamp":{"seconds":0,"nanos":5000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"57","testStepId":"41","timestamp":{"seconds":0,"nanos":6000000}}} +{"testStepFinished":{"testCaseStartedId":"57","testStepId":"41","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":7000000}}} +{"testCaseFinished":{"testCaseStartedId":"57","timestamp":{"seconds":0,"nanos":8000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"58","testCaseId":"42","timestamp":{"seconds":0,"nanos":9000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"58","testStepId":"43","timestamp":{"seconds":0,"nanos":10000000}}} +{"testStepFinished":{"testCaseStartedId":"58","testStepId":"43","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":11000000}}} +{"testCaseFinished":{"testCaseStartedId":"58","timestamp":{"seconds":0,"nanos":12000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"59","testCaseId":"44","timestamp":{"seconds":0,"nanos":13000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"59","testStepId":"45","timestamp":{"seconds":0,"nanos":14000000}}} +{"testStepFinished":{"testCaseStartedId":"59","testStepId":"45","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":15000000}}} +{"testCaseFinished":{"testCaseStartedId":"59","timestamp":{"seconds":0,"nanos":16000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"60","testCaseId":"46","timestamp":{"seconds":0,"nanos":17000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"60","testStepId":"47","timestamp":{"seconds":0,"nanos":18000000}}} +{"testStepFinished":{"testCaseStartedId":"60","testStepId":"47","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":19000000}}} +{"testCaseFinished":{"testCaseStartedId":"60","timestamp":{"seconds":0,"nanos":20000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"61","testCaseId":"48","timestamp":{"seconds":0,"nanos":21000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"61","testStepId":"49","timestamp":{"seconds":0,"nanos":22000000}}} +{"testStepFinished":{"testCaseStartedId":"61","testStepId":"49","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":23000000}}} +{"testCaseFinished":{"testCaseStartedId":"61","timestamp":{"seconds":0,"nanos":24000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"62","testCaseId":"50","timestamp":{"seconds":0,"nanos":25000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"62","testStepId":"51","timestamp":{"seconds":0,"nanos":26000000}}} +{"testStepFinished":{"testCaseStartedId":"62","testStepId":"51","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":27000000}}} +{"testCaseFinished":{"testCaseStartedId":"62","timestamp":{"seconds":0,"nanos":28000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"63","testCaseId":"52","timestamp":{"seconds":0,"nanos":29000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"63","testStepId":"53","timestamp":{"seconds":0,"nanos":30000000}}} +{"testStepFinished":{"testCaseStartedId":"63","testStepId":"53","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":31000000}}} +{"testCaseFinished":{"testCaseStartedId":"63","timestamp":{"seconds":0,"nanos":32000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"64","testCaseId":"54","timestamp":{"seconds":0,"nanos":33000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"64","testStepId":"55","timestamp":{"seconds":0,"nanos":34000000}}} +{"testStepFinished":{"testCaseStartedId":"64","testStepId":"55","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":35000000}}} +{"testCaseFinished":{"testCaseStartedId":"64","timestamp":{"seconds":0,"nanos":36000000},"willBeRetried":false}} +{"testRunFinished":{"testRunStartedId":"37","timestamp":{"seconds":0,"nanos":37000000},"success":true}} diff --git a/compatibility/multiple-features/multiple-features.ts b/compatibility/multiple-features/multiple-features.ts new file mode 100644 index 00000000..e41dae39 --- /dev/null +++ b/compatibility/multiple-features/multiple-features.ts @@ -0,0 +1,5 @@ +import { Given } from '@cucumber/fake-cucumber' + +Given('an order for {string}', () => { + // no-op +}) diff --git a/compatibility/parameter-types/parameter-types.cpp b/compatibility/parameter-types/parameter-types.cpp new file mode 100644 index 00000000..460d587e --- /dev/null +++ b/compatibility/parameter-types/parameter-types.cpp @@ -0,0 +1,21 @@ +#include "cucumber/messages/group.hpp" +#include "cucumber_cpp/CucumberCpp.hpp" +#include "gmock/gmock.h" +#include + +struct Flight +{ + std::string from; + std::string to; +}; + +PARAMETER(Flight, "flight", "([A-Z]{3})-([A-Z]{3})", true) +{ + return { group.children[0].value.value(), group.children[1].value.value() }; +} + +STEP(R"({flight} has been delayed)", (const Flight& flight)) +{ + EXPECT_THAT(flight.from, testing::StrEq("LHR")); + EXPECT_THAT(flight.to, testing::StrEq("CDG")); +} diff --git a/compatibility/parameter-types/parameter-types.feature b/compatibility/parameter-types/parameter-types.feature new file mode 100644 index 00000000..67e09946 --- /dev/null +++ b/compatibility/parameter-types/parameter-types.feature @@ -0,0 +1,11 @@ +Feature: Parameter Types + Cucumber lets you define your own parameter types, which can be used + in Cucumber Expressions. + + This lets you define a precise domain-specific vocabulary which can be used to + generate a glossary with examples taken from your scenarios. + + Parameter types also enable you to transform strings and tables into different types. + + Scenario: Flight transformer + Given LHR-CDG has been delayed diff --git a/compatibility/parameter-types/parameter-types.ndjson b/compatibility/parameter-types/parameter-types.ndjson new file mode 100644 index 00000000..0923b944 --- /dev/null +++ b/compatibility/parameter-types/parameter-types.ndjson @@ -0,0 +1,13 @@ +{"meta":{"protocolVersion":"31.0.0","implementation":{"name":"fake-cucumber","version":"123.45.6"},"cpu":{"name":"arm64"},"os":{"name":"darwin","version":"24.5.0"},"runtime":{"name":"Node.js","version":"24.4.1"},"ci":{"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429","buildNumber":"154666429","git":{"revision":"99684bcacf01d95875834d87903dcb072306c9ad","remote":"https://github.com/cucumber-ltd/shouty.rb.git","branch":"main"}}}} +{"source":{"data":"Feature: Parameter Types\n Cucumber lets you define your own parameter types, which can be used\n in Cucumber Expressions.\n\n This lets you define a precise domain-specific vocabulary which can be used to\n generate a glossary with examples taken from your scenarios.\n\n Parameter types also enable you to transform strings and tables into different types.\n\n Scenario: Flight transformer\n Given LHR-CDG has been delayed\n","uri":"samples/parameter-types/parameter-types.feature","mediaType":"text/x.cucumber.gherkin+plain"}} +{"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"Parameter Types","description":" Cucumber lets you define your own parameter types, which can be used\n in Cucumber Expressions.\n\n This lets you define a precise domain-specific vocabulary which can be used to\n generate a glossary with examples taken from your scenarios.\n\n Parameter types also enable you to transform strings and tables into different types.","children":[{"scenario":{"id":"1","tags":[],"location":{"line":10,"column":3},"keyword":"Scenario","name":"Flight transformer","description":"","steps":[{"id":"0","location":{"line":11,"column":5},"keyword":"Given ","keywordType":"Context","text":"LHR-CDG has been delayed"}],"examples":[]}}]},"comments":[],"uri":"samples/parameter-types/parameter-types.feature"}} +{"pickle":{"id":"3","uri":"samples/parameter-types/parameter-types.feature","location":{"line":10,"column":3},"astNodeIds":["1"],"tags":[],"name":"Flight transformer","language":"en","steps":[{"id":"2","text":"LHR-CDG has been delayed","type":"Context","astNodeIds":["0"]}]}} +{"parameterType":{"id":"4","name":"flight","regularExpressions":["([A-Z]{3})-([A-Z]{3})"],"preferForRegularExpressionMatch":false,"useForSnippets":true,"sourceReference":{"uri":"samples/parameter-types/parameter-types.ts","location":{"line":8}}}} +{"stepDefinition":{"id":"5","pattern":{"type":"CUCUMBER_EXPRESSION","source":"{flight} has been delayed"},"sourceReference":{"uri":"samples/parameter-types/parameter-types.ts","location":{"line":16}}}} +{"testRunStarted":{"id":"6","timestamp":{"seconds":0,"nanos":0}}} +{"testCase":{"id":"7","pickleId":"3","testSteps":[{"id":"8","pickleStepId":"2","stepDefinitionIds":["5"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":0,"value":"LHR-CDG","children":[{"start":0,"value":"LHR","children":[]},{"start":4,"value":"CDG","children":[]}]},"parameterTypeName":"flight"}]}]}],"testRunStartedId":"6"}} +{"testCaseStarted":{"id":"9","testCaseId":"7","timestamp":{"seconds":0,"nanos":1000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"9","testStepId":"8","timestamp":{"seconds":0,"nanos":2000000}}} +{"testStepFinished":{"testCaseStartedId":"9","testStepId":"8","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":3000000}}} +{"testCaseFinished":{"testCaseStartedId":"9","timestamp":{"seconds":0,"nanos":4000000},"willBeRetried":false}} +{"testRunFinished":{"testRunStartedId":"6","timestamp":{"seconds":0,"nanos":5000000},"success":true}} diff --git a/compatibility/parameter-types/parameter-types.ts b/compatibility/parameter-types/parameter-types.ts new file mode 100644 index 00000000..ca8833a5 --- /dev/null +++ b/compatibility/parameter-types/parameter-types.ts @@ -0,0 +1,19 @@ +import assert from 'node:assert' +import { Given, ParameterType } from '@cucumber/fake-cucumber' + +class Flight { + constructor(public readonly from: string, public readonly to: string) {} +} + +ParameterType({ + name: 'flight', + regexp: /([A-Z]{3})-([A-Z]{3})/, + transformer(from: string, to: string) { + return new Flight(from, to) + }, +}) + +Given('{flight} has been delayed', function (flight: Flight) { + assert.strictEqual(flight.from, 'LHR') + assert.strictEqual(flight.to, 'CDG') +}) diff --git a/compatibility/pending/pending.cpp b/compatibility/pending/pending.cpp new file mode 100644 index 00000000..8908bdbc --- /dev/null +++ b/compatibility/pending/pending.cpp @@ -0,0 +1,16 @@ +#include "cucumber_cpp/CucumberCpp.hpp" + +GIVEN(R"(an implemented non-pending step)") +{ + // no-op +} + +GIVEN(R"(an implemented step that is skipped)") +{ + // no-op +} + +GIVEN(R"(an unimplemented pending step)") +{ + Pending(); +} diff --git a/compatibility/pending/pending.feature b/compatibility/pending/pending.feature new file mode 100644 index 00000000..767ece53 --- /dev/null +++ b/compatibility/pending/pending.feature @@ -0,0 +1,18 @@ +Feature: Pending steps + During development, step definitions can signal at runtime that they are + not yet implemented (or "pending") by returning or throwing a particular + value. + + This causes subsequent steps in the scenario to be skipped, and the overall + result to be treated as a failure. + + Scenario: Unimplemented step signals pending status + Given an unimplemented pending step + + Scenario: Steps before unimplemented steps are executed + Given an implemented non-pending step + And an unimplemented pending step + + Scenario: Steps after unimplemented steps are skipped + Given an unimplemented pending step + And an implemented step that is skipped diff --git a/compatibility/pending/pending.ndjson b/compatibility/pending/pending.ndjson new file mode 100644 index 00000000..a7891bcf --- /dev/null +++ b/compatibility/pending/pending.ndjson @@ -0,0 +1,30 @@ +{"meta":{"protocolVersion":"31.0.0","implementation":{"name":"fake-cucumber","version":"123.45.6"},"cpu":{"name":"arm64"},"os":{"name":"darwin","version":"24.5.0"},"runtime":{"name":"Node.js","version":"24.4.1"},"ci":{"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429","buildNumber":"154666429","git":{"revision":"99684bcacf01d95875834d87903dcb072306c9ad","remote":"https://github.com/cucumber-ltd/shouty.rb.git","branch":"main"}}}} +{"source":{"data":"Feature: Pending steps\n During development, step definitions can signal at runtime that they are\n not yet implemented (or \"pending\") by returning or throwing a particular\n value.\n\n This causes subsequent steps in the scenario to be skipped, and the overall\n result to be treated as a failure.\n\n Scenario: Unimplemented step signals pending status\n Given an unimplemented pending step\n\n Scenario: Steps before unimplemented steps are executed\n Given an implemented non-pending step\n And an unimplemented pending step\n\n Scenario: Steps after unimplemented steps are skipped\n Given an unimplemented pending step\n And an implemented step that is skipped\n","uri":"samples/pending/pending.feature","mediaType":"text/x.cucumber.gherkin+plain"}} +{"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"Pending steps","description":" During development, step definitions can signal at runtime that they are\n not yet implemented (or \"pending\") by returning or throwing a particular\n value.\n\n This causes subsequent steps in the scenario to be skipped, and the overall\n result to be treated as a failure.","children":[{"scenario":{"id":"1","tags":[],"location":{"line":9,"column":3},"keyword":"Scenario","name":"Unimplemented step signals pending status","description":"","steps":[{"id":"0","location":{"line":10,"column":5},"keyword":"Given ","keywordType":"Context","text":"an unimplemented pending step"}],"examples":[]}},{"scenario":{"id":"4","tags":[],"location":{"line":12,"column":3},"keyword":"Scenario","name":"Steps before unimplemented steps are executed","description":"","steps":[{"id":"2","location":{"line":13,"column":5},"keyword":"Given ","keywordType":"Context","text":"an implemented non-pending step"},{"id":"3","location":{"line":14,"column":5},"keyword":"And ","keywordType":"Conjunction","text":"an unimplemented pending step"}],"examples":[]}},{"scenario":{"id":"7","tags":[],"location":{"line":16,"column":3},"keyword":"Scenario","name":"Steps after unimplemented steps are skipped","description":"","steps":[{"id":"5","location":{"line":17,"column":5},"keyword":"Given ","keywordType":"Context","text":"an unimplemented pending step"},{"id":"6","location":{"line":18,"column":5},"keyword":"And ","keywordType":"Conjunction","text":"an implemented step that is skipped"}],"examples":[]}}]},"comments":[],"uri":"samples/pending/pending.feature"}} +{"pickle":{"id":"9","uri":"samples/pending/pending.feature","location":{"line":9,"column":3},"astNodeIds":["1"],"tags":[],"name":"Unimplemented step signals pending status","language":"en","steps":[{"id":"8","text":"an unimplemented pending step","type":"Context","astNodeIds":["0"]}]}} +{"pickle":{"id":"12","uri":"samples/pending/pending.feature","location":{"line":12,"column":3},"astNodeIds":["4"],"tags":[],"name":"Steps before unimplemented steps are executed","language":"en","steps":[{"id":"10","text":"an implemented non-pending step","type":"Context","astNodeIds":["2"]},{"id":"11","text":"an unimplemented pending step","type":"Context","astNodeIds":["3"]}]}} +{"pickle":{"id":"15","uri":"samples/pending/pending.feature","location":{"line":16,"column":3},"astNodeIds":["7"],"tags":[],"name":"Steps after unimplemented steps are skipped","language":"en","steps":[{"id":"13","text":"an unimplemented pending step","type":"Context","astNodeIds":["5"]},{"id":"14","text":"an implemented step that is skipped","type":"Context","astNodeIds":["6"]}]}} +{"stepDefinition":{"id":"16","pattern":{"type":"CUCUMBER_EXPRESSION","source":"an implemented non-pending step"},"sourceReference":{"uri":"samples/pending/pending.ts","location":{"line":3}}}} +{"stepDefinition":{"id":"17","pattern":{"type":"CUCUMBER_EXPRESSION","source":"an implemented step that is skipped"},"sourceReference":{"uri":"samples/pending/pending.ts","location":{"line":7}}}} +{"stepDefinition":{"id":"18","pattern":{"type":"CUCUMBER_EXPRESSION","source":"an unimplemented pending step"},"sourceReference":{"uri":"samples/pending/pending.ts","location":{"line":11}}}} +{"testRunStarted":{"id":"19","timestamp":{"seconds":0,"nanos":0}}} +{"testCase":{"id":"20","pickleId":"9","testSteps":[{"id":"21","pickleStepId":"8","stepDefinitionIds":["18"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}],"testRunStartedId":"19"}} +{"testCase":{"id":"22","pickleId":"12","testSteps":[{"id":"23","pickleStepId":"10","stepDefinitionIds":["16"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"24","pickleStepId":"11","stepDefinitionIds":["18"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}],"testRunStartedId":"19"}} +{"testCase":{"id":"25","pickleId":"15","testSteps":[{"id":"26","pickleStepId":"13","stepDefinitionIds":["18"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"27","pickleStepId":"14","stepDefinitionIds":["17"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}],"testRunStartedId":"19"}} +{"testCaseStarted":{"id":"28","testCaseId":"20","timestamp":{"seconds":0,"nanos":1000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"28","testStepId":"21","timestamp":{"seconds":0,"nanos":2000000}}} +{"testStepFinished":{"testCaseStartedId":"28","testStepId":"21","testStepResult":{"status":"PENDING","message":"TODO","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":3000000}}} +{"testCaseFinished":{"testCaseStartedId":"28","timestamp":{"seconds":0,"nanos":4000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"29","testCaseId":"22","timestamp":{"seconds":0,"nanos":5000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"29","testStepId":"23","timestamp":{"seconds":0,"nanos":6000000}}} +{"testStepFinished":{"testCaseStartedId":"29","testStepId":"23","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":7000000}}} +{"testStepStarted":{"testCaseStartedId":"29","testStepId":"24","timestamp":{"seconds":0,"nanos":8000000}}} +{"testStepFinished":{"testCaseStartedId":"29","testStepId":"24","testStepResult":{"status":"PENDING","message":"TODO","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":9000000}}} +{"testCaseFinished":{"testCaseStartedId":"29","timestamp":{"seconds":0,"nanos":10000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"30","testCaseId":"25","timestamp":{"seconds":0,"nanos":11000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"30","testStepId":"26","timestamp":{"seconds":0,"nanos":12000000}}} +{"testStepFinished":{"testCaseStartedId":"30","testStepId":"26","testStepResult":{"status":"PENDING","message":"TODO","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":13000000}}} +{"testStepStarted":{"testCaseStartedId":"30","testStepId":"27","timestamp":{"seconds":0,"nanos":14000000}}} +{"testStepFinished":{"testCaseStartedId":"30","testStepId":"27","testStepResult":{"status":"SKIPPED","duration":{"seconds":0,"nanos":0}},"timestamp":{"seconds":0,"nanos":15000000}}} +{"testCaseFinished":{"testCaseStartedId":"30","timestamp":{"seconds":0,"nanos":16000000},"willBeRetried":false}} +{"testRunFinished":{"testRunStartedId":"19","timestamp":{"seconds":0,"nanos":17000000},"success":false}} diff --git a/compatibility/pending/pending.ts b/compatibility/pending/pending.ts new file mode 100644 index 00000000..431d643a --- /dev/null +++ b/compatibility/pending/pending.ts @@ -0,0 +1,13 @@ +import { Given } from '@cucumber/fake-cucumber' + +Given('an implemented non-pending step', function () { + // no-op +}) + +Given('an implemented step that is skipped', function () { + // no-op +}) + +Given('an unimplemented pending step', function () { + return 'pending' +}) diff --git a/compatibility/regular-expression/regular-expression.cpp b/compatibility/regular-expression/regular-expression.cpp new file mode 100644 index 00000000..d42e34ad --- /dev/null +++ b/compatibility/regular-expression/regular-expression.cpp @@ -0,0 +1,7 @@ +#include "cucumber_cpp/CucumberCpp.hpp" +#include + +STEP(R"(^a (.*?)(?: and a (.*?))?(?: and a (.*?))?$)", (const std::string& vegtable1, const std::string& vegtable2, const std::string& vegtable3)) +{ + // no-op +} diff --git a/compatibility/regular-expression/regular-expression.feature b/compatibility/regular-expression/regular-expression.feature new file mode 100644 index 00000000..fcf7ae59 --- /dev/null +++ b/compatibility/regular-expression/regular-expression.feature @@ -0,0 +1,9 @@ +Feature: regular expression + + Cucumber supports both Cucumber and regular expressions in step + definitions. This shows a sample that uses a regular expression. + + Scenario: regular expression + Given a cucumber + Given a cucumber and a zucchini + Given a cucumber and a zucchini and a gourd diff --git a/compatibility/regular-expression/regular-expression.ndjson b/compatibility/regular-expression/regular-expression.ndjson new file mode 100644 index 00000000..e5ceaf8f --- /dev/null +++ b/compatibility/regular-expression/regular-expression.ndjson @@ -0,0 +1,16 @@ +{"meta":{"protocolVersion":"31.0.0","implementation":{"name":"fake-cucumber","version":"123.45.6"},"cpu":{"name":"arm64"},"os":{"name":"darwin","version":"24.5.0"},"runtime":{"name":"Node.js","version":"24.4.1"},"ci":{"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429","buildNumber":"154666429","git":{"revision":"99684bcacf01d95875834d87903dcb072306c9ad","remote":"https://github.com/cucumber-ltd/shouty.rb.git","branch":"main"}}}} +{"source":{"data":"Feature: regular expression\n \n Cucumber supports both Cucumber and regular expressions in step\n definitions. This shows a sample that uses a regular expression.\n \n Scenario: regular expression\n Given a cucumber\n Given a cucumber and a zucchini\n Given a cucumber and a zucchini and a gourd\n","uri":"samples/regular-expression/regular-expression.feature","mediaType":"text/x.cucumber.gherkin+plain"}} +{"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"regular expression","description":" Cucumber supports both Cucumber and regular expressions in step\n definitions. This shows a sample that uses a regular expression.","children":[{"scenario":{"id":"3","tags":[],"location":{"line":6,"column":3},"keyword":"Scenario","name":"regular expression","description":"","steps":[{"id":"0","location":{"line":7,"column":5},"keyword":"Given ","keywordType":"Context","text":"a cucumber"},{"id":"1","location":{"line":8,"column":5},"keyword":"Given ","keywordType":"Context","text":"a cucumber and a zucchini"},{"id":"2","location":{"line":9,"column":5},"keyword":"Given ","keywordType":"Context","text":"a cucumber and a zucchini and a gourd"}],"examples":[]}}]},"comments":[],"uri":"samples/regular-expression/regular-expression.feature"}} +{"pickle":{"id":"7","uri":"samples/regular-expression/regular-expression.feature","location":{"line":6,"column":3},"astNodeIds":["3"],"tags":[],"name":"regular expression","language":"en","steps":[{"id":"4","text":"a cucumber","type":"Context","astNodeIds":["0"]},{"id":"5","text":"a cucumber and a zucchini","type":"Context","astNodeIds":["1"]},{"id":"6","text":"a cucumber and a zucchini and a gourd","type":"Context","astNodeIds":["2"]}]}} +{"stepDefinition":{"id":"8","pattern":{"type":"REGULAR_EXPRESSION","source":"^a (.*?)(?: and a (.*?))?(?: and a (.*?))?$"},"sourceReference":{"uri":"samples/regular-expression/regular-expression.ts","location":{"line":3}}}} +{"testRunStarted":{"id":"9","timestamp":{"seconds":0,"nanos":0}}} +{"testCase":{"id":"10","pickleId":"7","testSteps":[{"id":"11","pickleStepId":"4","stepDefinitionIds":["8"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":2,"value":"cucumber","children":[]}},{"group":{"children":[]}},{"group":{"children":[]}}]}]},{"id":"12","pickleStepId":"5","stepDefinitionIds":["8"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":2,"value":"cucumber","children":[]}},{"group":{"start":17,"value":"zucchini","children":[]}},{"group":{"children":[]}}]}]},{"id":"13","pickleStepId":"6","stepDefinitionIds":["8"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":2,"value":"cucumber","children":[]}},{"group":{"start":17,"value":"zucchini","children":[]}},{"group":{"start":32,"value":"gourd","children":[]}}]}]}],"testRunStartedId":"9"}} +{"testCaseStarted":{"id":"14","testCaseId":"10","timestamp":{"seconds":0,"nanos":1000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"14","testStepId":"11","timestamp":{"seconds":0,"nanos":2000000}}} +{"testStepFinished":{"testCaseStartedId":"14","testStepId":"11","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":3000000}}} +{"testStepStarted":{"testCaseStartedId":"14","testStepId":"12","timestamp":{"seconds":0,"nanos":4000000}}} +{"testStepFinished":{"testCaseStartedId":"14","testStepId":"12","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":5000000}}} +{"testStepStarted":{"testCaseStartedId":"14","testStepId":"13","timestamp":{"seconds":0,"nanos":6000000}}} +{"testStepFinished":{"testCaseStartedId":"14","testStepId":"13","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":7000000}}} +{"testCaseFinished":{"testCaseStartedId":"14","timestamp":{"seconds":0,"nanos":8000000},"willBeRetried":false}} +{"testRunFinished":{"testRunStartedId":"9","timestamp":{"seconds":0,"nanos":9000000},"success":true}} diff --git a/compatibility/regular-expression/regular-expression.ts b/compatibility/regular-expression/regular-expression.ts new file mode 100644 index 00000000..49f66a2b --- /dev/null +++ b/compatibility/regular-expression/regular-expression.ts @@ -0,0 +1,6 @@ +import { Given } from '@cucumber/fake-cucumber' + +Given(/^a (.*?)(?: and a (.*?))?(?: and a (.*?))?$/, + function (vegtable1: string, vegtable2: string, vegtable3: string) { + // no-op +}) diff --git a/compatibility/retry-ambiguous/retry-ambiguous.arguments.txt b/compatibility/retry-ambiguous/retry-ambiguous.arguments.txt new file mode 100644 index 00000000..cf83a555 --- /dev/null +++ b/compatibility/retry-ambiguous/retry-ambiguous.arguments.txt @@ -0,0 +1 @@ +--retry 2 diff --git a/compatibility/retry-ambiguous/retry-ambiguous.cpp b/compatibility/retry-ambiguous/retry-ambiguous.cpp new file mode 100644 index 00000000..71944084 --- /dev/null +++ b/compatibility/retry-ambiguous/retry-ambiguous.cpp @@ -0,0 +1,11 @@ +#include "cucumber_cpp/CucumberCpp.hpp" + +GIVEN(R"(an ambiguous step)") +{ + // no-op +} + +GIVEN(R"(an ambiguous step)") +{ + // no-op +} diff --git a/compatibility/retry-ambiguous/retry-ambiguous.feature b/compatibility/retry-ambiguous/retry-ambiguous.feature new file mode 100644 index 00000000..2769d2f4 --- /dev/null +++ b/compatibility/retry-ambiguous/retry-ambiguous.feature @@ -0,0 +1,3 @@ +Feature: Retry - With Ambiguous Steps + Scenario: Test cases won't retry when the status is AMBIGUOUS + Given an ambiguous step diff --git a/compatibility/retry-ambiguous/retry-ambiguous.ndjson b/compatibility/retry-ambiguous/retry-ambiguous.ndjson new file mode 100644 index 00000000..70771be9 --- /dev/null +++ b/compatibility/retry-ambiguous/retry-ambiguous.ndjson @@ -0,0 +1,13 @@ +{"meta":{"protocolVersion":"31.0.0","implementation":{"name":"fake-cucumber","version":"123.45.6"},"cpu":{"name":"arm64"},"os":{"name":"darwin","version":"24.5.0"},"runtime":{"name":"Node.js","version":"24.4.1"},"ci":{"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429","buildNumber":"154666429","git":{"revision":"99684bcacf01d95875834d87903dcb072306c9ad","remote":"https://github.com/cucumber-ltd/shouty.rb.git","branch":"main"}}}} +{"source":{"data":"Feature: Retry - With Ambiguous Steps\n Scenario: Test cases won't retry when the status is AMBIGUOUS\n Given an ambiguous step\n","uri":"samples/retry-ambiguous/retry-ambiguous.feature","mediaType":"text/x.cucumber.gherkin+plain"}} +{"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"Retry - With Ambiguous Steps","description":"","children":[{"scenario":{"id":"1","tags":[],"location":{"line":2,"column":3},"keyword":"Scenario","name":"Test cases won't retry when the status is AMBIGUOUS","description":"","steps":[{"id":"0","location":{"line":3,"column":5},"keyword":"Given ","keywordType":"Context","text":"an ambiguous step"}],"examples":[]}}]},"comments":[],"uri":"samples/retry-ambiguous/retry-ambiguous.feature"}} +{"pickle":{"id":"3","uri":"samples/retry-ambiguous/retry-ambiguous.feature","location":{"line":2,"column":3},"astNodeIds":["1"],"tags":[],"name":"Test cases won't retry when the status is AMBIGUOUS","language":"en","steps":[{"id":"2","text":"an ambiguous step","type":"Context","astNodeIds":["0"]}]}} +{"stepDefinition":{"id":"4","pattern":{"type":"CUCUMBER_EXPRESSION","source":"an ambiguous step"},"sourceReference":{"uri":"samples/retry-ambiguous/retry-ambiguous.ts","location":{"line":3}}}} +{"stepDefinition":{"id":"5","pattern":{"type":"CUCUMBER_EXPRESSION","source":"an ambiguous step"},"sourceReference":{"uri":"samples/retry-ambiguous/retry-ambiguous.ts","location":{"line":7}}}} +{"testRunStarted":{"id":"6","timestamp":{"seconds":0,"nanos":0}}} +{"testCase":{"id":"7","pickleId":"3","testSteps":[{"id":"8","pickleStepId":"2","stepDefinitionIds":["4","5"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]},{"stepMatchArguments":[]}]}],"testRunStartedId":"6"}} +{"testCaseStarted":{"id":"9","testCaseId":"7","timestamp":{"seconds":0,"nanos":1000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"9","testStepId":"8","timestamp":{"seconds":0,"nanos":2000000}}} +{"testStepFinished":{"testCaseStartedId":"9","testStepId":"8","testStepResult":{"status":"AMBIGUOUS","duration":{"seconds":0,"nanos":0}},"timestamp":{"seconds":0,"nanos":3000000}}} +{"testCaseFinished":{"testCaseStartedId":"9","timestamp":{"seconds":0,"nanos":4000000},"willBeRetried":false}} +{"testRunFinished":{"testRunStartedId":"6","timestamp":{"seconds":0,"nanos":5000000},"success":false}} diff --git a/compatibility/retry-ambiguous/retry-ambiguous.ts b/compatibility/retry-ambiguous/retry-ambiguous.ts new file mode 100644 index 00000000..e08a7b17 --- /dev/null +++ b/compatibility/retry-ambiguous/retry-ambiguous.ts @@ -0,0 +1,9 @@ +import { Given } from '@cucumber/fake-cucumber' + +Given('an ambiguous step', function () { + // first one +}) + +Given('an ambiguous step', function () { + // second one +}) diff --git a/compatibility/retry-pending/retry-pending.arguments.txt b/compatibility/retry-pending/retry-pending.arguments.txt new file mode 100644 index 00000000..cf83a555 --- /dev/null +++ b/compatibility/retry-pending/retry-pending.arguments.txt @@ -0,0 +1 @@ +--retry 2 diff --git a/compatibility/retry-pending/retry-pending.cpp b/compatibility/retry-pending/retry-pending.cpp new file mode 100644 index 00000000..09b3099e --- /dev/null +++ b/compatibility/retry-pending/retry-pending.cpp @@ -0,0 +1,6 @@ +#include "cucumber_cpp/CucumberCpp.hpp" + +GIVEN(R"(a pending step)") +{ + Pending(); +} diff --git a/compatibility/retry-pending/retry-pending.feature b/compatibility/retry-pending/retry-pending.feature new file mode 100644 index 00000000..bacc607e --- /dev/null +++ b/compatibility/retry-pending/retry-pending.feature @@ -0,0 +1,3 @@ +Feature: Retry - With Pending Steps + Scenario: Test cases won't retry when the status is PENDING + Given a pending step diff --git a/compatibility/retry-pending/retry-pending.ndjson b/compatibility/retry-pending/retry-pending.ndjson new file mode 100644 index 00000000..11265a05 --- /dev/null +++ b/compatibility/retry-pending/retry-pending.ndjson @@ -0,0 +1,12 @@ +{"meta":{"protocolVersion":"31.0.0","implementation":{"name":"fake-cucumber","version":"123.45.6"},"cpu":{"name":"arm64"},"os":{"name":"darwin","version":"24.5.0"},"runtime":{"name":"Node.js","version":"24.4.1"},"ci":{"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429","buildNumber":"154666429","git":{"revision":"99684bcacf01d95875834d87903dcb072306c9ad","remote":"https://github.com/cucumber-ltd/shouty.rb.git","branch":"main"}}}} +{"source":{"data":"Feature: Retry - With Pending Steps\n Scenario: Test cases won't retry when the status is PENDING\n Given a pending step\n","uri":"samples/retry-pending/retry-pending.feature","mediaType":"text/x.cucumber.gherkin+plain"}} +{"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"Retry - With Pending Steps","description":"","children":[{"scenario":{"id":"1","tags":[],"location":{"line":2,"column":3},"keyword":"Scenario","name":"Test cases won't retry when the status is PENDING","description":"","steps":[{"id":"0","location":{"line":3,"column":5},"keyword":"Given ","keywordType":"Context","text":"a pending step"}],"examples":[]}}]},"comments":[],"uri":"samples/retry-pending/retry-pending.feature"}} +{"pickle":{"id":"3","uri":"samples/retry-pending/retry-pending.feature","location":{"line":2,"column":3},"astNodeIds":["1"],"tags":[],"name":"Test cases won't retry when the status is PENDING","language":"en","steps":[{"id":"2","text":"a pending step","type":"Context","astNodeIds":["0"]}]}} +{"stepDefinition":{"id":"4","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a pending step"},"sourceReference":{"uri":"samples/retry-pending/retry-pending.ts","location":{"line":3}}}} +{"testRunStarted":{"id":"5","timestamp":{"seconds":0,"nanos":0}}} +{"testCase":{"id":"6","pickleId":"3","testSteps":[{"id":"7","pickleStepId":"2","stepDefinitionIds":["4"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}],"testRunStartedId":"5"}} +{"testCaseStarted":{"id":"8","testCaseId":"6","timestamp":{"seconds":0,"nanos":1000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"8","testStepId":"7","timestamp":{"seconds":0,"nanos":2000000}}} +{"testStepFinished":{"testCaseStartedId":"8","testStepId":"7","testStepResult":{"status":"PENDING","message":"TODO","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":3000000}}} +{"testCaseFinished":{"testCaseStartedId":"8","timestamp":{"seconds":0,"nanos":4000000},"willBeRetried":false}} +{"testRunFinished":{"testRunStartedId":"5","timestamp":{"seconds":0,"nanos":5000000},"success":false}} diff --git a/compatibility/retry-pending/retry-pending.ts b/compatibility/retry-pending/retry-pending.ts new file mode 100644 index 00000000..cab68569 --- /dev/null +++ b/compatibility/retry-pending/retry-pending.ts @@ -0,0 +1,5 @@ +import { Given } from '@cucumber/fake-cucumber' + +Given('a pending step', function () { + return 'pending' +}) diff --git a/compatibility/retry-undefined/retry-undefined.arguments.txt b/compatibility/retry-undefined/retry-undefined.arguments.txt new file mode 100644 index 00000000..cf83a555 --- /dev/null +++ b/compatibility/retry-undefined/retry-undefined.arguments.txt @@ -0,0 +1 @@ +--retry 2 diff --git a/compatibility/retry-undefined/retry-undefined.cpp b/compatibility/retry-undefined/retry-undefined.cpp new file mode 100644 index 00000000..e69de29b diff --git a/compatibility/retry-undefined/retry-undefined.feature b/compatibility/retry-undefined/retry-undefined.feature new file mode 100644 index 00000000..10f7eab6 --- /dev/null +++ b/compatibility/retry-undefined/retry-undefined.feature @@ -0,0 +1,3 @@ +Feature: Retry - With Undefined Steps + Scenario: Test cases won't retry when the status is UNDEFINED + Given a non-existent step diff --git a/compatibility/retry-undefined/retry-undefined.ndjson b/compatibility/retry-undefined/retry-undefined.ndjson new file mode 100644 index 00000000..c2a0619d --- /dev/null +++ b/compatibility/retry-undefined/retry-undefined.ndjson @@ -0,0 +1,12 @@ +{"meta":{"protocolVersion":"31.0.0","implementation":{"name":"fake-cucumber","version":"123.45.6"},"cpu":{"name":"arm64"},"os":{"name":"darwin","version":"24.5.0"},"runtime":{"name":"Node.js","version":"24.4.1"},"ci":{"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429","buildNumber":"154666429","git":{"revision":"99684bcacf01d95875834d87903dcb072306c9ad","remote":"https://github.com/cucumber-ltd/shouty.rb.git","branch":"main"}}}} +{"source":{"data":"Feature: Retry - With Undefined Steps\n Scenario: Test cases won't retry when the status is UNDEFINED\n Given a non-existent step\n","uri":"samples/retry-undefined/retry-undefined.feature","mediaType":"text/x.cucumber.gherkin+plain"}} +{"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"Retry - With Undefined Steps","description":"","children":[{"scenario":{"id":"1","tags":[],"location":{"line":2,"column":3},"keyword":"Scenario","name":"Test cases won't retry when the status is UNDEFINED","description":"","steps":[{"id":"0","location":{"line":3,"column":5},"keyword":"Given ","keywordType":"Context","text":"a non-existent step"}],"examples":[]}}]},"comments":[],"uri":"samples/retry-undefined/retry-undefined.feature"}} +{"pickle":{"id":"3","uri":"samples/retry-undefined/retry-undefined.feature","location":{"line":2,"column":3},"astNodeIds":["1"],"tags":[],"name":"Test cases won't retry when the status is UNDEFINED","language":"en","steps":[{"id":"2","text":"a non-existent step","type":"Context","astNodeIds":["0"]}]}} +{"testRunStarted":{"id":"4","timestamp":{"seconds":0,"nanos":0}}} +{"testCase":{"id":"5","pickleId":"3","testSteps":[{"id":"6","pickleStepId":"2","stepDefinitionIds":[],"stepMatchArgumentsLists":[]}],"testRunStartedId":"4"}} +{"testCaseStarted":{"id":"7","testCaseId":"5","timestamp":{"seconds":0,"nanos":1000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"7","testStepId":"6","timestamp":{"seconds":0,"nanos":2000000}}} +{"suggestion":{"id":"8","pickleStepId":"2","snippets":[{"language":"typescript","code":"Given(\"a non-existent step\", () => {\n return \"pending\"\n})"}]}} +{"testStepFinished":{"testCaseStartedId":"7","testStepId":"6","testStepResult":{"status":"UNDEFINED","duration":{"seconds":0,"nanos":0}},"timestamp":{"seconds":0,"nanos":3000000}}} +{"testCaseFinished":{"testCaseStartedId":"7","timestamp":{"seconds":0,"nanos":4000000},"willBeRetried":false}} +{"testRunFinished":{"testRunStartedId":"4","timestamp":{"seconds":0,"nanos":5000000},"success":false}} diff --git a/compatibility/retry-undefined/retry-undefined.ts b/compatibility/retry-undefined/retry-undefined.ts new file mode 100644 index 00000000..7b8d0a62 --- /dev/null +++ b/compatibility/retry-undefined/retry-undefined.ts @@ -0,0 +1 @@ +// There are intentionally no steps defined for this sample \ No newline at end of file diff --git a/compatibility/retry/retry.arguments.txt b/compatibility/retry/retry.arguments.txt new file mode 100644 index 00000000..cf83a555 --- /dev/null +++ b/compatibility/retry/retry.arguments.txt @@ -0,0 +1 @@ +--retry 2 diff --git a/compatibility/retry/retry.cpp b/compatibility/retry/retry.cpp new file mode 100644 index 00000000..94ee443b --- /dev/null +++ b/compatibility/retry/retry.cpp @@ -0,0 +1,33 @@ +#include "cucumber_cpp/CucumberCpp.hpp" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +GIVEN(R"(a step that always passes)") +{ + // no-op +} + +namespace +{ + auto secondTimePass = 0; + auto thirdTimePass = 0; +} + +GIVEN(R"(a step that passes the second time)") +{ + secondTimePass++; + EXPECT_THAT(secondTimePass, testing::Ge(2)); +} + +GIVEN(R"(a step that passes the third time)") +{ + // no-op + thirdTimePass++; + EXPECT_THAT(thirdTimePass, testing::Ge(3)); +} + +GIVEN(R"(a step that always fails)") +{ + // no-op + FAIL(); +} diff --git a/compatibility/retry/retry.feature b/compatibility/retry/retry.feature new file mode 100644 index 00000000..e48671dd --- /dev/null +++ b/compatibility/retry/retry.feature @@ -0,0 +1,18 @@ +Feature: Retry + Some Cucumber implementations support a Retry mechanism, where test cases that fail + can be retried up to a limited number of attempts in the same test run. + + Non-passing statuses other than FAILED won't trigger a retry, as they are not + going to pass however many times we attempt them. + + Scenario: Test cases that pass aren't retried + Given a step that always passes + + Scenario: Test cases that fail are retried if within the --retry limit + Given a step that passes the second time + + Scenario: Test cases that fail will continue to retry up to the --retry limit + Given a step that passes the third time + + Scenario: Test cases won't retry after failing more than the --retry limit + Given a step that always fails diff --git a/compatibility/retry/retry.ndjson b/compatibility/retry/retry.ndjson new file mode 100644 index 00000000..d3c2d9cb --- /dev/null +++ b/compatibility/retry/retry.ndjson @@ -0,0 +1,53 @@ +{"meta":{"protocolVersion":"31.0.0","implementation":{"name":"fake-cucumber","version":"123.45.6"},"cpu":{"name":"arm64"},"os":{"name":"darwin","version":"24.5.0"},"runtime":{"name":"Node.js","version":"24.4.1"},"ci":{"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429","buildNumber":"154666429","git":{"revision":"99684bcacf01d95875834d87903dcb072306c9ad","remote":"https://github.com/cucumber-ltd/shouty.rb.git","branch":"main"}}}} +{"source":{"data":"Feature: Retry\n Some Cucumber implementations support a Retry mechanism, where test cases that fail\n can be retried up to a limited number of attempts in the same test run.\n\n Non-passing statuses other than FAILED won't trigger a retry, as they are not\n going to pass however many times we attempt them.\n\n Scenario: Test cases that pass aren't retried\n Given a step that always passes\n\n Scenario: Test cases that fail are retried if within the --retry limit\n Given a step that passes the second time\n\n Scenario: Test cases that fail will continue to retry up to the --retry limit\n Given a step that passes the third time\n\n Scenario: Test cases won't retry after failing more than the --retry limit\n Given a step that always fails\n","uri":"samples/retry/retry.feature","mediaType":"text/x.cucumber.gherkin+plain"}} +{"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"Retry","description":" Some Cucumber implementations support a Retry mechanism, where test cases that fail\n can be retried up to a limited number of attempts in the same test run.\n\n Non-passing statuses other than FAILED won't trigger a retry, as they are not\n going to pass however many times we attempt them.","children":[{"scenario":{"id":"1","tags":[],"location":{"line":8,"column":3},"keyword":"Scenario","name":"Test cases that pass aren't retried","description":"","steps":[{"id":"0","location":{"line":9,"column":5},"keyword":"Given ","keywordType":"Context","text":"a step that always passes"}],"examples":[]}},{"scenario":{"id":"3","tags":[],"location":{"line":11,"column":3},"keyword":"Scenario","name":"Test cases that fail are retried if within the --retry limit","description":"","steps":[{"id":"2","location":{"line":12,"column":5},"keyword":"Given ","keywordType":"Context","text":"a step that passes the second time"}],"examples":[]}},{"scenario":{"id":"5","tags":[],"location":{"line":14,"column":3},"keyword":"Scenario","name":"Test cases that fail will continue to retry up to the --retry limit","description":"","steps":[{"id":"4","location":{"line":15,"column":5},"keyword":"Given ","keywordType":"Context","text":"a step that passes the third time"}],"examples":[]}},{"scenario":{"id":"7","tags":[],"location":{"line":17,"column":3},"keyword":"Scenario","name":"Test cases won't retry after failing more than the --retry limit","description":"","steps":[{"id":"6","location":{"line":18,"column":5},"keyword":"Given ","keywordType":"Context","text":"a step that always fails"}],"examples":[]}}]},"comments":[],"uri":"samples/retry/retry.feature"}} +{"pickle":{"id":"9","uri":"samples/retry/retry.feature","location":{"line":8,"column":3},"astNodeIds":["1"],"tags":[],"name":"Test cases that pass aren't retried","language":"en","steps":[{"id":"8","text":"a step that always passes","type":"Context","astNodeIds":["0"]}]}} +{"pickle":{"id":"11","uri":"samples/retry/retry.feature","location":{"line":11,"column":3},"astNodeIds":["3"],"tags":[],"name":"Test cases that fail are retried if within the --retry limit","language":"en","steps":[{"id":"10","text":"a step that passes the second time","type":"Context","astNodeIds":["2"]}]}} +{"pickle":{"id":"13","uri":"samples/retry/retry.feature","location":{"line":14,"column":3},"astNodeIds":["5"],"tags":[],"name":"Test cases that fail will continue to retry up to the --retry limit","language":"en","steps":[{"id":"12","text":"a step that passes the third time","type":"Context","astNodeIds":["4"]}]}} +{"pickle":{"id":"15","uri":"samples/retry/retry.feature","location":{"line":17,"column":3},"astNodeIds":["7"],"tags":[],"name":"Test cases won't retry after failing more than the --retry limit","language":"en","steps":[{"id":"14","text":"a step that always fails","type":"Context","astNodeIds":["6"]}]}} +{"stepDefinition":{"id":"16","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a step that always passes"},"sourceReference":{"uri":"samples/retry/retry.ts","location":{"line":3}}}} +{"stepDefinition":{"id":"17","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a step that passes the second time"},"sourceReference":{"uri":"samples/retry/retry.ts","location":{"line":8}}}} +{"stepDefinition":{"id":"18","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a step that passes the third time"},"sourceReference":{"uri":"samples/retry/retry.ts","location":{"line":16}}}} +{"stepDefinition":{"id":"19","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a step that always fails"},"sourceReference":{"uri":"samples/retry/retry.ts","location":{"line":23}}}} +{"testRunStarted":{"id":"20","timestamp":{"seconds":0,"nanos":0}}} +{"testCase":{"id":"21","pickleId":"9","testSteps":[{"id":"22","pickleStepId":"8","stepDefinitionIds":["16"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}],"testRunStartedId":"20"}} +{"testCase":{"id":"23","pickleId":"11","testSteps":[{"id":"24","pickleStepId":"10","stepDefinitionIds":["17"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}],"testRunStartedId":"20"}} +{"testCase":{"id":"25","pickleId":"13","testSteps":[{"id":"26","pickleStepId":"12","stepDefinitionIds":["18"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}],"testRunStartedId":"20"}} +{"testCase":{"id":"27","pickleId":"15","testSteps":[{"id":"28","pickleStepId":"14","stepDefinitionIds":["19"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}],"testRunStartedId":"20"}} +{"testCaseStarted":{"id":"29","testCaseId":"21","timestamp":{"seconds":0,"nanos":1000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"29","testStepId":"22","timestamp":{"seconds":0,"nanos":2000000}}} +{"testStepFinished":{"testCaseStartedId":"29","testStepId":"22","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":3000000}}} +{"testCaseFinished":{"testCaseStartedId":"29","timestamp":{"seconds":0,"nanos":4000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"30","testCaseId":"23","timestamp":{"seconds":0,"nanos":5000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"30","testStepId":"24","timestamp":{"seconds":0,"nanos":6000000}}} +{"testStepFinished":{"testCaseStartedId":"30","testStepId":"24","testStepResult":{"message":"Exception in step","exception":{"type":"Error","message":"Exception in step","stackTrace":"Error: Exception in step\nsamples/retry/retry.feature:12"},"status":"FAILED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":7000000}}} +{"testCaseFinished":{"testCaseStartedId":"30","timestamp":{"seconds":0,"nanos":8000000},"willBeRetried":true}} +{"testCaseStarted":{"id":"31","testCaseId":"23","timestamp":{"seconds":0,"nanos":9000000},"attempt":1}} +{"testStepStarted":{"testCaseStartedId":"31","testStepId":"24","timestamp":{"seconds":0,"nanos":10000000}}} +{"testStepFinished":{"testCaseStartedId":"31","testStepId":"24","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":11000000}}} +{"testCaseFinished":{"testCaseStartedId":"31","timestamp":{"seconds":0,"nanos":12000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"32","testCaseId":"25","timestamp":{"seconds":0,"nanos":13000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"32","testStepId":"26","timestamp":{"seconds":0,"nanos":14000000}}} +{"testStepFinished":{"testCaseStartedId":"32","testStepId":"26","testStepResult":{"message":"Exception in step","exception":{"type":"Error","message":"Exception in step","stackTrace":"Error: Exception in step\nsamples/retry/retry.feature:15"},"status":"FAILED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":15000000}}} +{"testCaseFinished":{"testCaseStartedId":"32","timestamp":{"seconds":0,"nanos":16000000},"willBeRetried":true}} +{"testCaseStarted":{"id":"33","testCaseId":"25","timestamp":{"seconds":0,"nanos":17000000},"attempt":1}} +{"testStepStarted":{"testCaseStartedId":"33","testStepId":"26","timestamp":{"seconds":0,"nanos":18000000}}} +{"testStepFinished":{"testCaseStartedId":"33","testStepId":"26","testStepResult":{"message":"Exception in step","exception":{"type":"Error","message":"Exception in step","stackTrace":"Error: Exception in step\nsamples/retry/retry.feature:15"},"status":"FAILED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":19000000}}} +{"testCaseFinished":{"testCaseStartedId":"33","timestamp":{"seconds":0,"nanos":20000000},"willBeRetried":true}} +{"testCaseStarted":{"id":"34","testCaseId":"25","timestamp":{"seconds":0,"nanos":21000000},"attempt":2}} +{"testStepStarted":{"testCaseStartedId":"34","testStepId":"26","timestamp":{"seconds":0,"nanos":22000000}}} +{"testStepFinished":{"testCaseStartedId":"34","testStepId":"26","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":23000000}}} +{"testCaseFinished":{"testCaseStartedId":"34","timestamp":{"seconds":0,"nanos":24000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"35","testCaseId":"27","timestamp":{"seconds":0,"nanos":25000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"35","testStepId":"28","timestamp":{"seconds":0,"nanos":26000000}}} +{"testStepFinished":{"testCaseStartedId":"35","testStepId":"28","testStepResult":{"message":"Exception in step","exception":{"type":"Error","message":"Exception in step","stackTrace":"Error: Exception in step\nsamples/retry/retry.feature:18"},"status":"FAILED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":27000000}}} +{"testCaseFinished":{"testCaseStartedId":"35","timestamp":{"seconds":0,"nanos":28000000},"willBeRetried":true}} +{"testCaseStarted":{"id":"36","testCaseId":"27","timestamp":{"seconds":0,"nanos":29000000},"attempt":1}} +{"testStepStarted":{"testCaseStartedId":"36","testStepId":"28","timestamp":{"seconds":0,"nanos":30000000}}} +{"testStepFinished":{"testCaseStartedId":"36","testStepId":"28","testStepResult":{"message":"Exception in step","exception":{"type":"Error","message":"Exception in step","stackTrace":"Error: Exception in step\nsamples/retry/retry.feature:18"},"status":"FAILED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":31000000}}} +{"testCaseFinished":{"testCaseStartedId":"36","timestamp":{"seconds":0,"nanos":32000000},"willBeRetried":true}} +{"testCaseStarted":{"id":"37","testCaseId":"27","timestamp":{"seconds":0,"nanos":33000000},"attempt":2}} +{"testStepStarted":{"testCaseStartedId":"37","testStepId":"28","timestamp":{"seconds":0,"nanos":34000000}}} +{"testStepFinished":{"testCaseStartedId":"37","testStepId":"28","testStepResult":{"message":"Exception in step","exception":{"type":"Error","message":"Exception in step","stackTrace":"Error: Exception in step\nsamples/retry/retry.feature:18"},"status":"FAILED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":35000000}}} +{"testCaseFinished":{"testCaseStartedId":"37","timestamp":{"seconds":0,"nanos":36000000},"willBeRetried":false}} +{"testRunFinished":{"testRunStartedId":"20","timestamp":{"seconds":0,"nanos":37000000},"success":false}} diff --git a/compatibility/retry/retry.ts b/compatibility/retry/retry.ts new file mode 100644 index 00000000..077e37cd --- /dev/null +++ b/compatibility/retry/retry.ts @@ -0,0 +1,25 @@ +import { Given } from '@cucumber/fake-cucumber' + +Given('a step that always passes', function () { + // no-op +}) + +let secondTimePass = 0 +Given('a step that passes the second time', function () { + secondTimePass++ + if (secondTimePass < 2) { + throw new Error('Exception in step') + } +}) + +let thirdTimePass = 0 +Given('a step that passes the third time', function () { + thirdTimePass++ + if (thirdTimePass < 3) { + throw new Error('Exception in step') + } +}) + +Given('a step that always fails', function () { + throw new Error('Exception in step') +}) diff --git a/compatibility/rules-backgrounds/rules-backgrounds.cpp b/compatibility/rules-backgrounds/rules-backgrounds.cpp new file mode 100644 index 00000000..2cc1e047 --- /dev/null +++ b/compatibility/rules-backgrounds/rules-backgrounds.cpp @@ -0,0 +1,17 @@ +#include "cucumber_cpp/CucumberCpp.hpp" +#include + +GIVEN(R"(an order for {string})", (const std::string& order)) +{ + // no-op +} + +WHEN(R"(an action)") +{ + // no-op +} + +THEN(R"(an outcome)") +{ + // no-op +} diff --git a/compatibility/rules-backgrounds/rules-backgrounds.feature b/compatibility/rules-backgrounds/rules-backgrounds.feature new file mode 100644 index 00000000..d4f79667 --- /dev/null +++ b/compatibility/rules-backgrounds/rules-backgrounds.feature @@ -0,0 +1,23 @@ +Feature: Rules with Backgrounds + Like Features, Rules can also have Backgrounds, whose steps are prepended to those of each child Scenario. Only + one Background at the Rule level is supported. + + It's even possible to have a Background at both Feature and Rule level, in which case they are concatenated. + + Background: + Given an order for "eggs" + And an order for "milk" + And an order for "bread" + + Rule: + Background: + Given an order for "batteries" + And an order for "light bulbs" + + Example: one scenario + When an action + Then an outcome + + Example: another scenario + When an action + Then an outcome \ No newline at end of file diff --git a/compatibility/rules-backgrounds/rules-backgrounds.ndjson b/compatibility/rules-backgrounds/rules-backgrounds.ndjson new file mode 100644 index 00000000..c4ddf144 --- /dev/null +++ b/compatibility/rules-backgrounds/rules-backgrounds.ndjson @@ -0,0 +1,44 @@ +{"meta":{"protocolVersion":"31.0.0","implementation":{"name":"fake-cucumber","version":"123.45.6"},"cpu":{"name":"arm64"},"os":{"name":"darwin","version":"24.5.0"},"runtime":{"name":"Node.js","version":"24.4.1"},"ci":{"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429","buildNumber":"154666429","git":{"revision":"99684bcacf01d95875834d87903dcb072306c9ad","remote":"https://github.com/cucumber-ltd/shouty.rb.git","branch":"main"}}}} +{"source":{"data":"Feature: Rules with Backgrounds\n Like Features, Rules can also have Backgrounds, whose steps are prepended to those of each child Scenario. Only\n one Background at the Rule level is supported.\n\n It's even possible to have a Background at both Feature and Rule level, in which case they are concatenated.\n\n Background:\n Given an order for \"eggs\"\n And an order for \"milk\"\n And an order for \"bread\"\n\n Rule:\n Background:\n Given an order for \"batteries\"\n And an order for \"light bulbs\"\n\n Example: one scenario\n When an action\n Then an outcome\n\n Example: another scenario\n When an action\n Then an outcome","uri":"samples/rules-backgrounds/rules-backgrounds.feature","mediaType":"text/x.cucumber.gherkin+plain"}} +{"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"Rules with Backgrounds","description":" Like Features, Rules can also have Backgrounds, whose steps are prepended to those of each child Scenario. Only\n one Background at the Rule level is supported.\n\n It's even possible to have a Background at both Feature and Rule level, in which case they are concatenated.","children":[{"background":{"id":"3","location":{"line":7,"column":3},"keyword":"Background","name":"","description":"","steps":[{"id":"0","location":{"line":8,"column":5},"keyword":"Given ","keywordType":"Context","text":"an order for \"eggs\""},{"id":"1","location":{"line":9,"column":5},"keyword":"And ","keywordType":"Conjunction","text":"an order for \"milk\""},{"id":"2","location":{"line":10,"column":5},"keyword":"And ","keywordType":"Conjunction","text":"an order for \"bread\""}]}},{"rule":{"id":"13","location":{"line":12,"column":3},"keyword":"Rule","name":"","description":"","children":[{"background":{"id":"6","location":{"line":13,"column":5},"keyword":"Background","name":"","description":"","steps":[{"id":"4","location":{"line":14,"column":7},"keyword":"Given ","keywordType":"Context","text":"an order for \"batteries\""},{"id":"5","location":{"line":15,"column":7},"keyword":"And ","keywordType":"Conjunction","text":"an order for \"light bulbs\""}]}},{"scenario":{"id":"9","tags":[],"location":{"line":17,"column":5},"keyword":"Example","name":"one scenario","description":"","steps":[{"id":"7","location":{"line":18,"column":7},"keyword":"When ","keywordType":"Action","text":"an action"},{"id":"8","location":{"line":19,"column":7},"keyword":"Then ","keywordType":"Outcome","text":"an outcome"}],"examples":[]}},{"scenario":{"id":"12","tags":[],"location":{"line":21,"column":5},"keyword":"Example","name":"another scenario","description":"","steps":[{"id":"10","location":{"line":22,"column":7},"keyword":"When ","keywordType":"Action","text":"an action"},{"id":"11","location":{"line":23,"column":7},"keyword":"Then ","keywordType":"Outcome","text":"an outcome"}],"examples":[]}}],"tags":[]}}]},"comments":[],"uri":"samples/rules-backgrounds/rules-backgrounds.feature"}} +{"pickle":{"id":"21","uri":"samples/rules-backgrounds/rules-backgrounds.feature","location":{"line":17,"column":5},"astNodeIds":["9"],"tags":[],"name":"one scenario","language":"en","steps":[{"id":"14","text":"an order for \"eggs\"","type":"Context","astNodeIds":["0"]},{"id":"15","text":"an order for \"milk\"","type":"Context","astNodeIds":["1"]},{"id":"16","text":"an order for \"bread\"","type":"Context","astNodeIds":["2"]},{"id":"17","text":"an order for \"batteries\"","type":"Context","astNodeIds":["4"]},{"id":"18","text":"an order for \"light bulbs\"","type":"Context","astNodeIds":["5"]},{"id":"19","text":"an action","type":"Action","astNodeIds":["7"]},{"id":"20","text":"an outcome","type":"Outcome","astNodeIds":["8"]}]}} +{"pickle":{"id":"29","uri":"samples/rules-backgrounds/rules-backgrounds.feature","location":{"line":21,"column":5},"astNodeIds":["12"],"tags":[],"name":"another scenario","language":"en","steps":[{"id":"22","text":"an order for \"eggs\"","type":"Context","astNodeIds":["0"]},{"id":"23","text":"an order for \"milk\"","type":"Context","astNodeIds":["1"]},{"id":"24","text":"an order for \"bread\"","type":"Context","astNodeIds":["2"]},{"id":"25","text":"an order for \"batteries\"","type":"Context","astNodeIds":["4"]},{"id":"26","text":"an order for \"light bulbs\"","type":"Context","astNodeIds":["5"]},{"id":"27","text":"an action","type":"Action","astNodeIds":["10"]},{"id":"28","text":"an outcome","type":"Outcome","astNodeIds":["11"]}]}} +{"stepDefinition":{"id":"30","pattern":{"type":"CUCUMBER_EXPRESSION","source":"an order for {string}"},"sourceReference":{"uri":"samples/rules-backgrounds/rules-backgrounds.ts","location":{"line":3}}}} +{"stepDefinition":{"id":"31","pattern":{"type":"CUCUMBER_EXPRESSION","source":"an action"},"sourceReference":{"uri":"samples/rules-backgrounds/rules-backgrounds.ts","location":{"line":7}}}} +{"stepDefinition":{"id":"32","pattern":{"type":"CUCUMBER_EXPRESSION","source":"an outcome"},"sourceReference":{"uri":"samples/rules-backgrounds/rules-backgrounds.ts","location":{"line":11}}}} +{"testRunStarted":{"id":"33","timestamp":{"seconds":0,"nanos":0}}} +{"testCase":{"id":"34","pickleId":"21","testSteps":[{"id":"35","pickleStepId":"14","stepDefinitionIds":["30"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":13,"value":"\"eggs\"","children":[{"start":14,"value":"eggs","children":[{"children":[]}]},{"children":[{"children":[]}]}]},"parameterTypeName":"string"}]}]},{"id":"36","pickleStepId":"15","stepDefinitionIds":["30"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":13,"value":"\"milk\"","children":[{"start":14,"value":"milk","children":[{"children":[]}]},{"children":[{"children":[]}]}]},"parameterTypeName":"string"}]}]},{"id":"37","pickleStepId":"16","stepDefinitionIds":["30"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":13,"value":"\"bread\"","children":[{"start":14,"value":"bread","children":[{"children":[]}]},{"children":[{"children":[]}]}]},"parameterTypeName":"string"}]}]},{"id":"38","pickleStepId":"17","stepDefinitionIds":["30"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":13,"value":"\"batteries\"","children":[{"start":14,"value":"batteries","children":[{"children":[]}]},{"children":[{"children":[]}]}]},"parameterTypeName":"string"}]}]},{"id":"39","pickleStepId":"18","stepDefinitionIds":["30"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":13,"value":"\"light bulbs\"","children":[{"start":14,"value":"light bulbs","children":[{"children":[]}]},{"children":[{"children":[]}]}]},"parameterTypeName":"string"}]}]},{"id":"40","pickleStepId":"19","stepDefinitionIds":["31"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"41","pickleStepId":"20","stepDefinitionIds":["32"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}],"testRunStartedId":"33"}} +{"testCase":{"id":"42","pickleId":"29","testSteps":[{"id":"43","pickleStepId":"22","stepDefinitionIds":["30"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":13,"value":"\"eggs\"","children":[{"start":14,"value":"eggs","children":[{"children":[]}]},{"children":[{"children":[]}]}]},"parameterTypeName":"string"}]}]},{"id":"44","pickleStepId":"23","stepDefinitionIds":["30"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":13,"value":"\"milk\"","children":[{"start":14,"value":"milk","children":[{"children":[]}]},{"children":[{"children":[]}]}]},"parameterTypeName":"string"}]}]},{"id":"45","pickleStepId":"24","stepDefinitionIds":["30"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":13,"value":"\"bread\"","children":[{"start":14,"value":"bread","children":[{"children":[]}]},{"children":[{"children":[]}]}]},"parameterTypeName":"string"}]}]},{"id":"46","pickleStepId":"25","stepDefinitionIds":["30"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":13,"value":"\"batteries\"","children":[{"start":14,"value":"batteries","children":[{"children":[]}]},{"children":[{"children":[]}]}]},"parameterTypeName":"string"}]}]},{"id":"47","pickleStepId":"26","stepDefinitionIds":["30"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":13,"value":"\"light bulbs\"","children":[{"start":14,"value":"light bulbs","children":[{"children":[]}]},{"children":[{"children":[]}]}]},"parameterTypeName":"string"}]}]},{"id":"48","pickleStepId":"27","stepDefinitionIds":["31"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"49","pickleStepId":"28","stepDefinitionIds":["32"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}],"testRunStartedId":"33"}} +{"testCaseStarted":{"id":"50","testCaseId":"34","timestamp":{"seconds":0,"nanos":1000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"50","testStepId":"35","timestamp":{"seconds":0,"nanos":2000000}}} +{"testStepFinished":{"testCaseStartedId":"50","testStepId":"35","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":3000000}}} +{"testStepStarted":{"testCaseStartedId":"50","testStepId":"36","timestamp":{"seconds":0,"nanos":4000000}}} +{"testStepFinished":{"testCaseStartedId":"50","testStepId":"36","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":5000000}}} +{"testStepStarted":{"testCaseStartedId":"50","testStepId":"37","timestamp":{"seconds":0,"nanos":6000000}}} +{"testStepFinished":{"testCaseStartedId":"50","testStepId":"37","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":7000000}}} +{"testStepStarted":{"testCaseStartedId":"50","testStepId":"38","timestamp":{"seconds":0,"nanos":8000000}}} +{"testStepFinished":{"testCaseStartedId":"50","testStepId":"38","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":9000000}}} +{"testStepStarted":{"testCaseStartedId":"50","testStepId":"39","timestamp":{"seconds":0,"nanos":10000000}}} +{"testStepFinished":{"testCaseStartedId":"50","testStepId":"39","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":11000000}}} +{"testStepStarted":{"testCaseStartedId":"50","testStepId":"40","timestamp":{"seconds":0,"nanos":12000000}}} +{"testStepFinished":{"testCaseStartedId":"50","testStepId":"40","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":13000000}}} +{"testStepStarted":{"testCaseStartedId":"50","testStepId":"41","timestamp":{"seconds":0,"nanos":14000000}}} +{"testStepFinished":{"testCaseStartedId":"50","testStepId":"41","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":15000000}}} +{"testCaseFinished":{"testCaseStartedId":"50","timestamp":{"seconds":0,"nanos":16000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"51","testCaseId":"42","timestamp":{"seconds":0,"nanos":17000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"51","testStepId":"43","timestamp":{"seconds":0,"nanos":18000000}}} +{"testStepFinished":{"testCaseStartedId":"51","testStepId":"43","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":19000000}}} +{"testStepStarted":{"testCaseStartedId":"51","testStepId":"44","timestamp":{"seconds":0,"nanos":20000000}}} +{"testStepFinished":{"testCaseStartedId":"51","testStepId":"44","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":21000000}}} +{"testStepStarted":{"testCaseStartedId":"51","testStepId":"45","timestamp":{"seconds":0,"nanos":22000000}}} +{"testStepFinished":{"testCaseStartedId":"51","testStepId":"45","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":23000000}}} +{"testStepStarted":{"testCaseStartedId":"51","testStepId":"46","timestamp":{"seconds":0,"nanos":24000000}}} +{"testStepFinished":{"testCaseStartedId":"51","testStepId":"46","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":25000000}}} +{"testStepStarted":{"testCaseStartedId":"51","testStepId":"47","timestamp":{"seconds":0,"nanos":26000000}}} +{"testStepFinished":{"testCaseStartedId":"51","testStepId":"47","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":27000000}}} +{"testStepStarted":{"testCaseStartedId":"51","testStepId":"48","timestamp":{"seconds":0,"nanos":28000000}}} +{"testStepFinished":{"testCaseStartedId":"51","testStepId":"48","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":29000000}}} +{"testStepStarted":{"testCaseStartedId":"51","testStepId":"49","timestamp":{"seconds":0,"nanos":30000000}}} +{"testStepFinished":{"testCaseStartedId":"51","testStepId":"49","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":31000000}}} +{"testCaseFinished":{"testCaseStartedId":"51","timestamp":{"seconds":0,"nanos":32000000},"willBeRetried":false}} +{"testRunFinished":{"testRunStartedId":"33","timestamp":{"seconds":0,"nanos":33000000},"success":true}} diff --git a/compatibility/rules-backgrounds/rules-backgrounds.ts b/compatibility/rules-backgrounds/rules-backgrounds.ts new file mode 100644 index 00000000..cb475f6e --- /dev/null +++ b/compatibility/rules-backgrounds/rules-backgrounds.ts @@ -0,0 +1,13 @@ +import { Given, When, Then } from '@cucumber/fake-cucumber' + +Given('an order for {string}', () => { + // no-op +}) + +When('an action', () => { + // no-op +}) + +Then('an outcome', () => { + // no-op +}) diff --git a/compatibility/rules/rules.cpp b/compatibility/rules/rules.cpp new file mode 100644 index 00000000..709ae320 --- /dev/null +++ b/compatibility/rules/rules.cpp @@ -0,0 +1,42 @@ +#include "cucumber_cpp/CucumberCpp.hpp" +#include "gmock/gmock.h" +#include +#include +#include + +using Stock = std::stack; + +GIVEN(R"(the customer has {int} cents)", (std::int32_t money)) +{ + context.InsertAt("money", money); +} + +GIVEN(R"(there are chocolate bars in stock)") +{ + auto stock = context.Emplace(); + stock->push("Mars"); +} + +GIVEN(R"(there are no chocolate bars in stock)") +{ + context.Emplace(); +} + +WHEN(R"(the customer tries to buy a {int} cent chocolate bar)",(std::int32_t price)) +{ + if (context.Get("money") >= price && !context.Get().empty()) + { + context.InsertAt("chocolate", context.Get().top()); + context.Get().pop(); + } +} + +THEN(R"(the sale should not happen)") +{ + EXPECT_THAT(context.Contains("chocolate"), testing::IsFalse()); +} + +THEN(R"(the sale should happen)") +{ + EXPECT_THAT(context.Contains("chocolate"), testing::IsTrue()); +} diff --git a/compatibility/rules/rules.feature b/compatibility/rules/rules.feature new file mode 100644 index 00000000..5d576ac7 --- /dev/null +++ b/compatibility/rules/rules.feature @@ -0,0 +1,29 @@ +Feature: Usage of a `Rule` + You can place scenarios inside rules. This makes it possible to structure Gherkin documents + in the same way as [example maps](https://cucumber.io/blog/bdd/example-mapping-introduction/). + + You can also use the Examples synonym for Scenario to make them even similar. + + Rule: A sale cannot happen if the customer does not have enough money + # Unhappy path + Example: Not enough money + Given the customer has 100 cents + And there are chocolate bars in stock + When the customer tries to buy a 125 cent chocolate bar + Then the sale should not happen + + # Happy path + Example: Enough money + Given the customer has 100 cents + And there are chocolate bars in stock + When the customer tries to buy a 75 cent chocolate bar + Then the sale should happen + + @some-tag + Rule: a sale cannot happen if there is no stock + # Unhappy path + Example: No chocolates left + Given the customer has 100 cents + And there are no chocolate bars in stock + When the customer tries to buy a 1 cent chocolate bar + Then the sale should not happen diff --git a/compatibility/rules/rules.ndjson b/compatibility/rules/rules.ndjson new file mode 100644 index 00000000..8a8fd1b9 --- /dev/null +++ b/compatibility/rules/rules.ndjson @@ -0,0 +1,47 @@ +{"meta":{"protocolVersion":"31.0.0","implementation":{"name":"fake-cucumber","version":"123.45.6"},"cpu":{"name":"arm64"},"os":{"name":"darwin","version":"24.5.0"},"runtime":{"name":"Node.js","version":"24.4.1"},"ci":{"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429","buildNumber":"154666429","git":{"revision":"99684bcacf01d95875834d87903dcb072306c9ad","remote":"https://github.com/cucumber-ltd/shouty.rb.git","branch":"main"}}}} +{"source":{"data":"Feature: Usage of a `Rule`\n You can place scenarios inside rules. This makes it possible to structure Gherkin documents\n in the same way as [example maps](https://cucumber.io/blog/bdd/example-mapping-introduction/).\n\n You can also use the Examples synonym for Scenario to make them even similar.\n\n Rule: A sale cannot happen if the customer does not have enough money\n # Unhappy path\n Example: Not enough money\n Given the customer has 100 cents\n And there are chocolate bars in stock\n When the customer tries to buy a 125 cent chocolate bar\n Then the sale should not happen\n\n # Happy path\n Example: Enough money\n Given the customer has 100 cents\n And there are chocolate bars in stock\n When the customer tries to buy a 75 cent chocolate bar\n Then the sale should happen\n\n @some-tag\n Rule: a sale cannot happen if there is no stock\n # Unhappy path\n Example: No chocolates left\n Given the customer has 100 cents\n And there are no chocolate bars in stock\n When the customer tries to buy a 1 cent chocolate bar\n Then the sale should not happen\n","uri":"samples/rules/rules.feature","mediaType":"text/x.cucumber.gherkin+plain"}} +{"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"Usage of a `Rule`","description":" You can place scenarios inside rules. This makes it possible to structure Gherkin documents\n in the same way as [example maps](https://cucumber.io/blog/bdd/example-mapping-introduction/).\n\n You can also use the Examples synonym for Scenario to make them even similar.","children":[{"rule":{"id":"10","location":{"line":7,"column":3},"keyword":"Rule","name":"A sale cannot happen if the customer does not have enough money","description":"","children":[{"scenario":{"id":"4","tags":[],"location":{"line":9,"column":5},"keyword":"Example","name":"Not enough money","description":"","steps":[{"id":"0","location":{"line":10,"column":7},"keyword":"Given ","keywordType":"Context","text":"the customer has 100 cents"},{"id":"1","location":{"line":11,"column":7},"keyword":"And ","keywordType":"Conjunction","text":"there are chocolate bars in stock"},{"id":"2","location":{"line":12,"column":7},"keyword":"When ","keywordType":"Action","text":"the customer tries to buy a 125 cent chocolate bar"},{"id":"3","location":{"line":13,"column":7},"keyword":"Then ","keywordType":"Outcome","text":"the sale should not happen"}],"examples":[]}},{"scenario":{"id":"9","tags":[],"location":{"line":16,"column":5},"keyword":"Example","name":"Enough money","description":"","steps":[{"id":"5","location":{"line":17,"column":7},"keyword":"Given ","keywordType":"Context","text":"the customer has 100 cents"},{"id":"6","location":{"line":18,"column":7},"keyword":"And ","keywordType":"Conjunction","text":"there are chocolate bars in stock"},{"id":"7","location":{"line":19,"column":7},"keyword":"When ","keywordType":"Action","text":"the customer tries to buy a 75 cent chocolate bar"},{"id":"8","location":{"line":20,"column":7},"keyword":"Then ","keywordType":"Outcome","text":"the sale should happen"}],"examples":[]}}],"tags":[]}},{"rule":{"id":"17","location":{"line":23,"column":3},"keyword":"Rule","name":"a sale cannot happen if there is no stock","description":"","children":[{"scenario":{"id":"15","tags":[],"location":{"line":25,"column":5},"keyword":"Example","name":"No chocolates left","description":"","steps":[{"id":"11","location":{"line":26,"column":7},"keyword":"Given ","keywordType":"Context","text":"the customer has 100 cents"},{"id":"12","location":{"line":27,"column":7},"keyword":"And ","keywordType":"Conjunction","text":"there are no chocolate bars in stock"},{"id":"13","location":{"line":28,"column":7},"keyword":"When ","keywordType":"Action","text":"the customer tries to buy a 1 cent chocolate bar"},{"id":"14","location":{"line":29,"column":7},"keyword":"Then ","keywordType":"Outcome","text":"the sale should not happen"}],"examples":[]}}],"tags":[{"location":{"line":22,"column":3},"name":"@some-tag","id":"16"}]}}]},"comments":[{"location":{"line":8,"column":1},"text":" # Unhappy path"},{"location":{"line":15,"column":1},"text":" # Happy path"},{"location":{"line":24,"column":1},"text":" # Unhappy path"}],"uri":"samples/rules/rules.feature"}} +{"pickle":{"id":"22","uri":"samples/rules/rules.feature","location":{"line":9,"column":5},"astNodeIds":["4"],"tags":[],"name":"Not enough money","language":"en","steps":[{"id":"18","text":"the customer has 100 cents","type":"Context","astNodeIds":["0"]},{"id":"19","text":"there are chocolate bars in stock","type":"Context","astNodeIds":["1"]},{"id":"20","text":"the customer tries to buy a 125 cent chocolate bar","type":"Action","astNodeIds":["2"]},{"id":"21","text":"the sale should not happen","type":"Outcome","astNodeIds":["3"]}]}} +{"pickle":{"id":"27","uri":"samples/rules/rules.feature","location":{"line":16,"column":5},"astNodeIds":["9"],"tags":[],"name":"Enough money","language":"en","steps":[{"id":"23","text":"the customer has 100 cents","type":"Context","astNodeIds":["5"]},{"id":"24","text":"there are chocolate bars in stock","type":"Context","astNodeIds":["6"]},{"id":"25","text":"the customer tries to buy a 75 cent chocolate bar","type":"Action","astNodeIds":["7"]},{"id":"26","text":"the sale should happen","type":"Outcome","astNodeIds":["8"]}]}} +{"pickle":{"id":"32","uri":"samples/rules/rules.feature","location":{"line":25,"column":5},"astNodeIds":["15"],"tags":[{"name":"@some-tag","astNodeId":"16"}],"name":"No chocolates left","language":"en","steps":[{"id":"28","text":"the customer has 100 cents","type":"Context","astNodeIds":["11"]},{"id":"29","text":"there are no chocolate bars in stock","type":"Context","astNodeIds":["12"]},{"id":"30","text":"the customer tries to buy a 1 cent chocolate bar","type":"Action","astNodeIds":["13"]},{"id":"31","text":"the sale should not happen","type":"Outcome","astNodeIds":["14"]}]}} +{"stepDefinition":{"id":"33","pattern":{"type":"CUCUMBER_EXPRESSION","source":"the customer has {int} cents"},"sourceReference":{"uri":"samples/rules/rules.ts","location":{"line":4}}}} +{"stepDefinition":{"id":"34","pattern":{"type":"CUCUMBER_EXPRESSION","source":"there are chocolate bars in stock"},"sourceReference":{"uri":"samples/rules/rules.ts","location":{"line":8}}}} +{"stepDefinition":{"id":"35","pattern":{"type":"CUCUMBER_EXPRESSION","source":"there are no chocolate bars in stock"},"sourceReference":{"uri":"samples/rules/rules.ts","location":{"line":12}}}} +{"stepDefinition":{"id":"36","pattern":{"type":"CUCUMBER_EXPRESSION","source":"the customer tries to buy a {int} cent chocolate bar"},"sourceReference":{"uri":"samples/rules/rules.ts","location":{"line":16}}}} +{"stepDefinition":{"id":"37","pattern":{"type":"CUCUMBER_EXPRESSION","source":"the sale should not happen"},"sourceReference":{"uri":"samples/rules/rules.ts","location":{"line":22}}}} +{"stepDefinition":{"id":"38","pattern":{"type":"CUCUMBER_EXPRESSION","source":"the sale should happen"},"sourceReference":{"uri":"samples/rules/rules.ts","location":{"line":26}}}} +{"testRunStarted":{"id":"39","timestamp":{"seconds":0,"nanos":0}}} +{"testCase":{"id":"40","pickleId":"22","testSteps":[{"id":"41","pickleStepId":"18","stepDefinitionIds":["33"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":17,"value":"100","children":[]},"parameterTypeName":"int"}]}]},{"id":"42","pickleStepId":"19","stepDefinitionIds":["34"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"43","pickleStepId":"20","stepDefinitionIds":["36"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":28,"value":"125","children":[]},"parameterTypeName":"int"}]}]},{"id":"44","pickleStepId":"21","stepDefinitionIds":["37"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}],"testRunStartedId":"39"}} +{"testCase":{"id":"45","pickleId":"27","testSteps":[{"id":"46","pickleStepId":"23","stepDefinitionIds":["33"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":17,"value":"100","children":[]},"parameterTypeName":"int"}]}]},{"id":"47","pickleStepId":"24","stepDefinitionIds":["34"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"48","pickleStepId":"25","stepDefinitionIds":["36"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":28,"value":"75","children":[]},"parameterTypeName":"int"}]}]},{"id":"49","pickleStepId":"26","stepDefinitionIds":["38"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}],"testRunStartedId":"39"}} +{"testCase":{"id":"50","pickleId":"32","testSteps":[{"id":"51","pickleStepId":"28","stepDefinitionIds":["33"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":17,"value":"100","children":[]},"parameterTypeName":"int"}]}]},{"id":"52","pickleStepId":"29","stepDefinitionIds":["35"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"53","pickleStepId":"30","stepDefinitionIds":["36"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"start":28,"value":"1","children":[]},"parameterTypeName":"int"}]}]},{"id":"54","pickleStepId":"31","stepDefinitionIds":["37"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}],"testRunStartedId":"39"}} +{"testCaseStarted":{"id":"55","testCaseId":"40","timestamp":{"seconds":0,"nanos":1000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"55","testStepId":"41","timestamp":{"seconds":0,"nanos":2000000}}} +{"testStepFinished":{"testCaseStartedId":"55","testStepId":"41","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":3000000}}} +{"testStepStarted":{"testCaseStartedId":"55","testStepId":"42","timestamp":{"seconds":0,"nanos":4000000}}} +{"testStepFinished":{"testCaseStartedId":"55","testStepId":"42","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":5000000}}} +{"testStepStarted":{"testCaseStartedId":"55","testStepId":"43","timestamp":{"seconds":0,"nanos":6000000}}} +{"testStepFinished":{"testCaseStartedId":"55","testStepId":"43","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":7000000}}} +{"testStepStarted":{"testCaseStartedId":"55","testStepId":"44","timestamp":{"seconds":0,"nanos":8000000}}} +{"testStepFinished":{"testCaseStartedId":"55","testStepId":"44","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":9000000}}} +{"testCaseFinished":{"testCaseStartedId":"55","timestamp":{"seconds":0,"nanos":10000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"56","testCaseId":"45","timestamp":{"seconds":0,"nanos":11000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"56","testStepId":"46","timestamp":{"seconds":0,"nanos":12000000}}} +{"testStepFinished":{"testCaseStartedId":"56","testStepId":"46","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":13000000}}} +{"testStepStarted":{"testCaseStartedId":"56","testStepId":"47","timestamp":{"seconds":0,"nanos":14000000}}} +{"testStepFinished":{"testCaseStartedId":"56","testStepId":"47","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":15000000}}} +{"testStepStarted":{"testCaseStartedId":"56","testStepId":"48","timestamp":{"seconds":0,"nanos":16000000}}} +{"testStepFinished":{"testCaseStartedId":"56","testStepId":"48","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":17000000}}} +{"testStepStarted":{"testCaseStartedId":"56","testStepId":"49","timestamp":{"seconds":0,"nanos":18000000}}} +{"testStepFinished":{"testCaseStartedId":"56","testStepId":"49","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":19000000}}} +{"testCaseFinished":{"testCaseStartedId":"56","timestamp":{"seconds":0,"nanos":20000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"57","testCaseId":"50","timestamp":{"seconds":0,"nanos":21000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"57","testStepId":"51","timestamp":{"seconds":0,"nanos":22000000}}} +{"testStepFinished":{"testCaseStartedId":"57","testStepId":"51","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":23000000}}} +{"testStepStarted":{"testCaseStartedId":"57","testStepId":"52","timestamp":{"seconds":0,"nanos":24000000}}} +{"testStepFinished":{"testCaseStartedId":"57","testStepId":"52","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":25000000}}} +{"testStepStarted":{"testCaseStartedId":"57","testStepId":"53","timestamp":{"seconds":0,"nanos":26000000}}} +{"testStepFinished":{"testCaseStartedId":"57","testStepId":"53","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":27000000}}} +{"testStepStarted":{"testCaseStartedId":"57","testStepId":"54","timestamp":{"seconds":0,"nanos":28000000}}} +{"testStepFinished":{"testCaseStartedId":"57","testStepId":"54","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":29000000}}} +{"testCaseFinished":{"testCaseStartedId":"57","timestamp":{"seconds":0,"nanos":30000000},"willBeRetried":false}} +{"testRunFinished":{"testRunStartedId":"39","timestamp":{"seconds":0,"nanos":31000000},"success":true}} diff --git a/compatibility/rules/rules.ts b/compatibility/rules/rules.ts new file mode 100644 index 00000000..dbcbfe33 --- /dev/null +++ b/compatibility/rules/rules.ts @@ -0,0 +1,28 @@ +import assert from 'node:assert' +import { Given, When, Then } from '@cucumber/fake-cucumber' + +Given('the customer has {int} cents', function (money) { + this.money = money +}) + +Given('there are chocolate bars in stock', function () { + this.stock = ['Mars'] +}) + +Given('there are no chocolate bars in stock', function () { + this.stock = [] +}) + +When('the customer tries to buy a {int} cent chocolate bar', function (price) { + if(this.money >= price) { + this.chocolate = this.stock.pop() + } +}) + +Then('the sale should not happen', function () { + assert.strictEqual(this.chocolate, undefined) +}) + +Then('the sale should happen', function () { + assert.ok(this.chocolate) +}) diff --git a/compatibility/skipped/skipped.cpp b/compatibility/skipped/skipped.cpp new file mode 100644 index 00000000..6e0e9773 --- /dev/null +++ b/compatibility/skipped/skipped.cpp @@ -0,0 +1,16 @@ +#include "cucumber_cpp/CucumberCpp.hpp" + +GIVEN(R"(a step that does not skip)") +{ + // no-op +} + +WHEN(R"(a step that is skipped)") +{ + // no-op +} + +THEN(R"(I skip a step)") +{ + Skipped(); +} diff --git a/compatibility/skipped/skipped.feature b/compatibility/skipped/skipped.feature new file mode 100644 index 00000000..73d11ad0 --- /dev/null +++ b/compatibility/skipped/skipped.feature @@ -0,0 +1,15 @@ +Feature: Skipping scenarios + + Step definitions are able to signal at runtime that the scenario should + be skipped by raising a particular kind of exception status (For example PENDING or SKIPPED). + + This can be useful in certain situations e.g. the current environment doesn't have + the right conditions for running a particular scenario. + + Scenario: Skipping from a step doesn't affect the previous steps + Given a step that does not skip + And I skip a step + + Scenario: Skipping from a step causes the rest of the scenario to be skipped + Given I skip a step + And a step that is skipped diff --git a/compatibility/skipped/skipped.ndjson b/compatibility/skipped/skipped.ndjson new file mode 100644 index 00000000..fee16080 --- /dev/null +++ b/compatibility/skipped/skipped.ndjson @@ -0,0 +1,24 @@ +{"meta":{"protocolVersion":"31.0.0","implementation":{"name":"fake-cucumber","version":"123.45.6"},"cpu":{"name":"arm64"},"os":{"name":"darwin","version":"24.5.0"},"runtime":{"name":"Node.js","version":"24.4.1"},"ci":{"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429","buildNumber":"154666429","git":{"revision":"99684bcacf01d95875834d87903dcb072306c9ad","remote":"https://github.com/cucumber-ltd/shouty.rb.git","branch":"main"}}}} +{"source":{"data":"Feature: Skipping scenarios\n\n Step definitions are able to signal at runtime that the scenario should\n be skipped by raising a particular kind of exception status (For example PENDING or SKIPPED).\n\n This can be useful in certain situations e.g. the current environment doesn't have\n the right conditions for running a particular scenario.\n\n Scenario: Skipping from a step doesn't affect the previous steps\n Given a step that does not skip\n And I skip a step\n\n Scenario: Skipping from a step causes the rest of the scenario to be skipped\n Given I skip a step\n And a step that is skipped\n","uri":"samples/skipped/skipped.feature","mediaType":"text/x.cucumber.gherkin+plain"}} +{"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"Skipping scenarios","description":" Step definitions are able to signal at runtime that the scenario should\n be skipped by raising a particular kind of exception status (For example PENDING or SKIPPED).\n\n This can be useful in certain situations e.g. the current environment doesn't have\n the right conditions for running a particular scenario.","children":[{"scenario":{"id":"2","tags":[],"location":{"line":9,"column":3},"keyword":"Scenario","name":"Skipping from a step doesn't affect the previous steps","description":"","steps":[{"id":"0","location":{"line":10,"column":5},"keyword":"Given ","keywordType":"Context","text":"a step that does not skip"},{"id":"1","location":{"line":11,"column":5},"keyword":"And ","keywordType":"Conjunction","text":"I skip a step"}],"examples":[]}},{"scenario":{"id":"5","tags":[],"location":{"line":13,"column":3},"keyword":"Scenario","name":"Skipping from a step causes the rest of the scenario to be skipped","description":"","steps":[{"id":"3","location":{"line":14,"column":5},"keyword":"Given ","keywordType":"Context","text":"I skip a step"},{"id":"4","location":{"line":15,"column":5},"keyword":"And ","keywordType":"Conjunction","text":"a step that is skipped"}],"examples":[]}}]},"comments":[],"uri":"samples/skipped/skipped.feature"}} +{"pickle":{"id":"8","uri":"samples/skipped/skipped.feature","location":{"line":9,"column":3},"astNodeIds":["2"],"tags":[],"name":"Skipping from a step doesn't affect the previous steps","language":"en","steps":[{"id":"6","text":"a step that does not skip","type":"Context","astNodeIds":["0"]},{"id":"7","text":"I skip a step","type":"Context","astNodeIds":["1"]}]}} +{"pickle":{"id":"11","uri":"samples/skipped/skipped.feature","location":{"line":13,"column":3},"astNodeIds":["5"],"tags":[],"name":"Skipping from a step causes the rest of the scenario to be skipped","language":"en","steps":[{"id":"9","text":"I skip a step","type":"Context","astNodeIds":["3"]},{"id":"10","text":"a step that is skipped","type":"Context","astNodeIds":["4"]}]}} +{"stepDefinition":{"id":"12","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a step that does not skip"},"sourceReference":{"uri":"samples/skipped/skipped.ts","location":{"line":3}}}} +{"stepDefinition":{"id":"13","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a step that is skipped"},"sourceReference":{"uri":"samples/skipped/skipped.ts","location":{"line":7}}}} +{"stepDefinition":{"id":"14","pattern":{"type":"CUCUMBER_EXPRESSION","source":"I skip a step"},"sourceReference":{"uri":"samples/skipped/skipped.ts","location":{"line":11}}}} +{"testRunStarted":{"id":"15","timestamp":{"seconds":0,"nanos":0}}} +{"testCase":{"id":"16","pickleId":"8","testSteps":[{"id":"17","pickleStepId":"6","stepDefinitionIds":["12"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"18","pickleStepId":"7","stepDefinitionIds":["14"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}],"testRunStartedId":"15"}} +{"testCase":{"id":"19","pickleId":"11","testSteps":[{"id":"20","pickleStepId":"9","stepDefinitionIds":["14"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"21","pickleStepId":"10","stepDefinitionIds":["13"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}],"testRunStartedId":"15"}} +{"testCaseStarted":{"id":"22","testCaseId":"16","timestamp":{"seconds":0,"nanos":1000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"22","testStepId":"17","timestamp":{"seconds":0,"nanos":2000000}}} +{"testStepFinished":{"testCaseStartedId":"22","testStepId":"17","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":3000000}}} +{"testStepStarted":{"testCaseStartedId":"22","testStepId":"18","timestamp":{"seconds":0,"nanos":4000000}}} +{"testStepFinished":{"testCaseStartedId":"22","testStepId":"18","testStepResult":{"status":"SKIPPED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":5000000}}} +{"testCaseFinished":{"testCaseStartedId":"22","timestamp":{"seconds":0,"nanos":6000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"23","testCaseId":"19","timestamp":{"seconds":0,"nanos":7000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"23","testStepId":"20","timestamp":{"seconds":0,"nanos":8000000}}} +{"testStepFinished":{"testCaseStartedId":"23","testStepId":"20","testStepResult":{"status":"SKIPPED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":9000000}}} +{"testStepStarted":{"testCaseStartedId":"23","testStepId":"21","timestamp":{"seconds":0,"nanos":10000000}}} +{"testStepFinished":{"testCaseStartedId":"23","testStepId":"21","testStepResult":{"status":"SKIPPED","duration":{"seconds":0,"nanos":0}},"timestamp":{"seconds":0,"nanos":11000000}}} +{"testCaseFinished":{"testCaseStartedId":"23","timestamp":{"seconds":0,"nanos":12000000},"willBeRetried":false}} +{"testRunFinished":{"testRunStartedId":"15","timestamp":{"seconds":0,"nanos":13000000},"success":true}} diff --git a/compatibility/skipped/skipped.ts b/compatibility/skipped/skipped.ts new file mode 100644 index 00000000..fdb86d24 --- /dev/null +++ b/compatibility/skipped/skipped.ts @@ -0,0 +1,13 @@ +import { Given } from '@cucumber/fake-cucumber' + +Given('a step that does not skip', function () { + // no-op +}) + +Given('a step that is skipped', function () { + // no-op +}) + +Given('I skip a step', function () { + return 'skipped' +}) diff --git a/compatibility/stack-traces/stack-traces.feature b/compatibility/stack-traces/stack-traces.feature new file mode 100644 index 00000000..2f6ff485 --- /dev/null +++ b/compatibility/stack-traces/stack-traces.feature @@ -0,0 +1,10 @@ +Feature: Stack traces + Stack traces can help you diagnose the source of a bug. + + Cucumber provides helpful stack traces that includes the stack frames from the + Gherkin document and remove uninteresting frames by default. + + The first line of the stack trace will contain a reference to the feature file. + + Scenario: A failing step + When a step throws an exception diff --git a/compatibility/stack-traces/stack-traces.ndjson b/compatibility/stack-traces/stack-traces.ndjson new file mode 100644 index 00000000..0fb0ad95 --- /dev/null +++ b/compatibility/stack-traces/stack-traces.ndjson @@ -0,0 +1,12 @@ +{"meta":{"protocolVersion":"31.0.0","implementation":{"name":"fake-cucumber","version":"123.45.6"},"cpu":{"name":"arm64"},"os":{"name":"darwin","version":"24.5.0"},"runtime":{"name":"Node.js","version":"24.4.1"},"ci":{"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429","buildNumber":"154666429","git":{"revision":"99684bcacf01d95875834d87903dcb072306c9ad","remote":"https://github.com/cucumber-ltd/shouty.rb.git","branch":"main"}}}} +{"source":{"data":"Feature: Stack traces\n Stack traces can help you diagnose the source of a bug.\n\n Cucumber provides helpful stack traces that includes the stack frames from the\n Gherkin document and remove uninteresting frames by default.\n\n The first line of the stack trace will contain a reference to the feature file.\n\n Scenario: A failing step\n When a step throws an exception\n","uri":"samples/stack-traces/stack-traces.feature","mediaType":"text/x.cucumber.gherkin+plain"}} +{"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"Stack traces","description":" Stack traces can help you diagnose the source of a bug.\n\n Cucumber provides helpful stack traces that includes the stack frames from the\n Gherkin document and remove uninteresting frames by default.\n\n The first line of the stack trace will contain a reference to the feature file.","children":[{"scenario":{"id":"1","tags":[],"location":{"line":9,"column":3},"keyword":"Scenario","name":"A failing step","description":"","steps":[{"id":"0","location":{"line":10,"column":5},"keyword":"When ","keywordType":"Action","text":"a step throws an exception"}],"examples":[]}}]},"comments":[],"uri":"samples/stack-traces/stack-traces.feature"}} +{"pickle":{"id":"3","uri":"samples/stack-traces/stack-traces.feature","location":{"line":9,"column":3},"astNodeIds":["1"],"tags":[],"name":"A failing step","language":"en","steps":[{"id":"2","text":"a step throws an exception","type":"Action","astNodeIds":["0"]}]}} +{"stepDefinition":{"id":"4","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a step throws an exception"},"sourceReference":{"uri":"samples/stack-traces/stack-traces.ts","location":{"line":3}}}} +{"testRunStarted":{"id":"5","timestamp":{"seconds":0,"nanos":0}}} +{"testCase":{"id":"6","pickleId":"3","testSteps":[{"id":"7","pickleStepId":"2","stepDefinitionIds":["4"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}],"testRunStartedId":"5"}} +{"testCaseStarted":{"id":"8","testCaseId":"6","timestamp":{"seconds":0,"nanos":1000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"8","testStepId":"7","timestamp":{"seconds":0,"nanos":2000000}}} +{"testStepFinished":{"testCaseStartedId":"8","testStepId":"7","testStepResult":{"message":"BOOM","exception":{"type":"Error","message":"BOOM","stackTrace":"Error: BOOM\nsamples/stack-traces/stack-traces.feature:10"},"status":"FAILED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":3000000}}} +{"testCaseFinished":{"testCaseStartedId":"8","timestamp":{"seconds":0,"nanos":4000000},"willBeRetried":false}} +{"testRunFinished":{"testRunStartedId":"5","timestamp":{"seconds":0,"nanos":5000000},"success":false}} diff --git a/compatibility/stack-traces/stack-traces.ts b/compatibility/stack-traces/stack-traces.ts new file mode 100644 index 00000000..2ffd339b --- /dev/null +++ b/compatibility/stack-traces/stack-traces.ts @@ -0,0 +1,5 @@ +import { When } from '@cucumber/fake-cucumber' + +When('a step throws an exception', function () { + throw new Error('BOOM') +}) diff --git a/compatibility/test-run-exception/test-run-exception.arguments.txt b/compatibility/test-run-exception/test-run-exception.arguments.txt new file mode 100644 index 00000000..6c11c1b4 --- /dev/null +++ b/compatibility/test-run-exception/test-run-exception.arguments.txt @@ -0,0 +1 @@ +--error diff --git a/compatibility/test-run-exception/test-run-exception.feature b/compatibility/test-run-exception/test-run-exception.feature new file mode 100644 index 00000000..1410da17 --- /dev/null +++ b/compatibility/test-run-exception/test-run-exception.feature @@ -0,0 +1,8 @@ +Feature: Test run exceptions + + Sometimes an exception might happen during the test run, but outside of user-supplied hook + or step code. This might be bad configuration, a bug in Cucumber, or an environmental issue. + In such cases, Cucumber will abort the test run and include the exception in the final message. + + Scenario: exception during test run + Given a step \ No newline at end of file diff --git a/compatibility/test-run-exception/test-run-exception.ndjson b/compatibility/test-run-exception/test-run-exception.ndjson new file mode 100644 index 00000000..52988388 --- /dev/null +++ b/compatibility/test-run-exception/test-run-exception.ndjson @@ -0,0 +1,7 @@ +{"meta":{"protocolVersion":"31.0.0","implementation":{"name":"fake-cucumber","version":"123.45.6"},"cpu":{"name":"arm64"},"os":{"name":"darwin","version":"24.5.0"},"runtime":{"name":"Node.js","version":"24.4.1"},"ci":{"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429","buildNumber":"154666429","git":{"revision":"99684bcacf01d95875834d87903dcb072306c9ad","remote":"https://github.com/cucumber-ltd/shouty.rb.git","branch":"main"}}}} +{"source":{"data":"Feature: Test run exceptions\n\n Sometimes an exception might happen during the test run, but outside of user-supplied hook\n or step code. This might be bad configuration, a bug in Cucumber, or an environmental issue.\n In such cases, Cucumber will abort the test run and include the exception in the final message.\n\n Scenario: exception during test run\n Given a step","uri":"samples/test-run-exception/test-run-exception.feature","mediaType":"text/x.cucumber.gherkin+plain"}} +{"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"Test run exceptions","description":" Sometimes an exception might happen during the test run, but outside of user-supplied hook\n or step code. This might be bad configuration, a bug in Cucumber, or an environmental issue.\n In such cases, Cucumber will abort the test run and include the exception in the final message.","children":[{"scenario":{"id":"1","tags":[],"location":{"line":7,"column":3},"keyword":"Scenario","name":"exception during test run","description":"","steps":[{"id":"0","location":{"line":8,"column":5},"keyword":"Given ","keywordType":"Context","text":"a step"}],"examples":[]}}]},"comments":[],"uri":"samples/test-run-exception/test-run-exception.feature"}} +{"pickle":{"id":"3","uri":"samples/test-run-exception/test-run-exception.feature","location":{"line":7,"column":3},"astNodeIds":["1"],"tags":[],"name":"exception during test run","language":"en","steps":[{"id":"2","text":"a step","type":"Context","astNodeIds":["0"]}]}} +{"stepDefinition":{"id":"4","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a step"},"sourceReference":{"uri":"samples/test-run-exception/test-run-exception.ts","location":{"line":3}}}} +{"testRunStarted":{"id":"5","timestamp":{"seconds":0,"nanos":0}}} +{"testRunFinished":{"testRunStartedId":"5","timestamp":{"seconds":0,"nanos":1000000},"success":false,"message":"Whoops!","exception":{"type":"Error","message":"Whoops!","stackTrace":"Error: Whoops!\n"}}} diff --git a/compatibility/test-run-exception/test-run-exception.ts b/compatibility/test-run-exception/test-run-exception.ts new file mode 100644 index 00000000..4eae9604 --- /dev/null +++ b/compatibility/test-run-exception/test-run-exception.ts @@ -0,0 +1,3 @@ +import { Given } from '@cucumber/fake-cucumber' + +Given('a step', () => {}) diff --git a/compatibility/undefined/undefined.cpp b/compatibility/undefined/undefined.cpp new file mode 100644 index 00000000..292ca85c --- /dev/null +++ b/compatibility/undefined/undefined.cpp @@ -0,0 +1,11 @@ +#include "cucumber_cpp/CucumberCpp.hpp" + +GIVEN(R"(an implemented step)") +{ + // no-op +} + +GIVEN(R"(a step that will be skipped)") +{ + // no-op +} diff --git a/compatibility/undefined/undefined.feature b/compatibility/undefined/undefined.feature new file mode 100644 index 00000000..c1e19e9a --- /dev/null +++ b/compatibility/undefined/undefined.feature @@ -0,0 +1,20 @@ +Feature: Undefined steps + + At runtime, Cucumber may encounter a step in a scenario that it cannot match to a step definition. + + In these cases, the scenario is not able to run and so the step status will be UNDEFINED, with + subsequent steps being SKIPPED and the overall result will be FAILURE. + + Scenario: An undefined step causes a failure + Given a step that is yet to be defined + + Scenario: Steps before undefined steps are executed + Given an implemented step + And a step that is yet to be defined + + Scenario: Steps after undefined steps are skipped + Given a step that is yet to be defined + And a step that will be skipped + + Scenario: Snippets reflect parameter types + Given a list of 8 things diff --git a/compatibility/undefined/undefined.ndjson b/compatibility/undefined/undefined.ndjson new file mode 100644 index 00000000..57828456 --- /dev/null +++ b/compatibility/undefined/undefined.ndjson @@ -0,0 +1,39 @@ +{"meta":{"protocolVersion":"31.0.0","implementation":{"name":"fake-cucumber","version":"123.45.6"},"cpu":{"name":"arm64"},"os":{"name":"darwin","version":"24.5.0"},"runtime":{"name":"Node.js","version":"24.4.1"},"ci":{"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429","buildNumber":"154666429","git":{"revision":"99684bcacf01d95875834d87903dcb072306c9ad","remote":"https://github.com/cucumber-ltd/shouty.rb.git","branch":"main"}}}} +{"source":{"data":"Feature: Undefined steps\n\n At runtime, Cucumber may encounter a step in a scenario that it cannot match to a step definition.\n\n In these cases, the scenario is not able to run and so the step status will be UNDEFINED, with\n subsequent steps being SKIPPED and the overall result will be FAILURE.\n\n Scenario: An undefined step causes a failure\n Given a step that is yet to be defined\n\n Scenario: Steps before undefined steps are executed\n Given an implemented step\n And a step that is yet to be defined\n\n Scenario: Steps after undefined steps are skipped\n Given a step that is yet to be defined\n And a step that will be skipped\n\n Scenario: Snippets reflect parameter types\n Given a list of 8 things\n","uri":"samples/undefined/undefined.feature","mediaType":"text/x.cucumber.gherkin+plain"}} +{"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"Undefined steps","description":" At runtime, Cucumber may encounter a step in a scenario that it cannot match to a step definition.\n\n In these cases, the scenario is not able to run and so the step status will be UNDEFINED, with\n subsequent steps being SKIPPED and the overall result will be FAILURE.","children":[{"scenario":{"id":"1","tags":[],"location":{"line":8,"column":3},"keyword":"Scenario","name":"An undefined step causes a failure","description":"","steps":[{"id":"0","location":{"line":9,"column":5},"keyword":"Given ","keywordType":"Context","text":"a step that is yet to be defined"}],"examples":[]}},{"scenario":{"id":"4","tags":[],"location":{"line":11,"column":3},"keyword":"Scenario","name":"Steps before undefined steps are executed","description":"","steps":[{"id":"2","location":{"line":12,"column":5},"keyword":"Given ","keywordType":"Context","text":"an implemented step"},{"id":"3","location":{"line":13,"column":5},"keyword":"And ","keywordType":"Conjunction","text":"a step that is yet to be defined"}],"examples":[]}},{"scenario":{"id":"7","tags":[],"location":{"line":15,"column":3},"keyword":"Scenario","name":"Steps after undefined steps are skipped","description":"","steps":[{"id":"5","location":{"line":16,"column":5},"keyword":"Given ","keywordType":"Context","text":"a step that is yet to be defined"},{"id":"6","location":{"line":17,"column":5},"keyword":"And ","keywordType":"Conjunction","text":"a step that will be skipped"}],"examples":[]}},{"scenario":{"id":"9","tags":[],"location":{"line":19,"column":3},"keyword":"Scenario","name":"Snippets reflect parameter types","description":"","steps":[{"id":"8","location":{"line":20,"column":5},"keyword":"Given ","keywordType":"Context","text":"a list of 8 things"}],"examples":[]}}]},"comments":[],"uri":"samples/undefined/undefined.feature"}} +{"pickle":{"id":"11","uri":"samples/undefined/undefined.feature","location":{"line":8,"column":3},"astNodeIds":["1"],"tags":[],"name":"An undefined step causes a failure","language":"en","steps":[{"id":"10","text":"a step that is yet to be defined","type":"Context","astNodeIds":["0"]}]}} +{"pickle":{"id":"14","uri":"samples/undefined/undefined.feature","location":{"line":11,"column":3},"astNodeIds":["4"],"tags":[],"name":"Steps before undefined steps are executed","language":"en","steps":[{"id":"12","text":"an implemented step","type":"Context","astNodeIds":["2"]},{"id":"13","text":"a step that is yet to be defined","type":"Context","astNodeIds":["3"]}]}} +{"pickle":{"id":"17","uri":"samples/undefined/undefined.feature","location":{"line":15,"column":3},"astNodeIds":["7"],"tags":[],"name":"Steps after undefined steps are skipped","language":"en","steps":[{"id":"15","text":"a step that is yet to be defined","type":"Context","astNodeIds":["5"]},{"id":"16","text":"a step that will be skipped","type":"Context","astNodeIds":["6"]}]}} +{"pickle":{"id":"19","uri":"samples/undefined/undefined.feature","location":{"line":19,"column":3},"astNodeIds":["9"],"tags":[],"name":"Snippets reflect parameter types","language":"en","steps":[{"id":"18","text":"a list of 8 things","type":"Context","astNodeIds":["8"]}]}} +{"stepDefinition":{"id":"20","pattern":{"type":"CUCUMBER_EXPRESSION","source":"an implemented step"},"sourceReference":{"uri":"samples/undefined/undefined.ts","location":{"line":3}}}} +{"stepDefinition":{"id":"21","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a step that will be skipped"},"sourceReference":{"uri":"samples/undefined/undefined.ts","location":{"line":7}}}} +{"testRunStarted":{"id":"22","timestamp":{"seconds":0,"nanos":0}}} +{"testCase":{"id":"23","pickleId":"11","testSteps":[{"id":"24","pickleStepId":"10","stepDefinitionIds":[],"stepMatchArgumentsLists":[]}],"testRunStartedId":"22"}} +{"testCase":{"id":"25","pickleId":"14","testSteps":[{"id":"26","pickleStepId":"12","stepDefinitionIds":["20"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]},{"id":"27","pickleStepId":"13","stepDefinitionIds":[],"stepMatchArgumentsLists":[]}],"testRunStartedId":"22"}} +{"testCase":{"id":"28","pickleId":"17","testSteps":[{"id":"29","pickleStepId":"15","stepDefinitionIds":[],"stepMatchArgumentsLists":[]},{"id":"30","pickleStepId":"16","stepDefinitionIds":["21"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}],"testRunStartedId":"22"}} +{"testCase":{"id":"31","pickleId":"19","testSteps":[{"id":"32","pickleStepId":"18","stepDefinitionIds":[],"stepMatchArgumentsLists":[]}],"testRunStartedId":"22"}} +{"testCaseStarted":{"id":"33","testCaseId":"23","timestamp":{"seconds":0,"nanos":1000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"33","testStepId":"24","timestamp":{"seconds":0,"nanos":2000000}}} +{"suggestion":{"id":"34","pickleStepId":"10","snippets":[{"language":"typescript","code":"Given(\"a step that is yet to be defined\", () => {\n return \"pending\"\n})"}]}} +{"testStepFinished":{"testCaseStartedId":"33","testStepId":"24","testStepResult":{"status":"UNDEFINED","duration":{"seconds":0,"nanos":0}},"timestamp":{"seconds":0,"nanos":3000000}}} +{"testCaseFinished":{"testCaseStartedId":"33","timestamp":{"seconds":0,"nanos":4000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"35","testCaseId":"25","timestamp":{"seconds":0,"nanos":5000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"35","testStepId":"26","timestamp":{"seconds":0,"nanos":6000000}}} +{"testStepFinished":{"testCaseStartedId":"35","testStepId":"26","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":7000000}}} +{"testStepStarted":{"testCaseStartedId":"35","testStepId":"27","timestamp":{"seconds":0,"nanos":8000000}}} +{"suggestion":{"id":"36","pickleStepId":"13","snippets":[{"language":"typescript","code":"Given(\"a step that is yet to be defined\", () => {\n return \"pending\"\n})"}]}} +{"testStepFinished":{"testCaseStartedId":"35","testStepId":"27","testStepResult":{"status":"UNDEFINED","duration":{"seconds":0,"nanos":0}},"timestamp":{"seconds":0,"nanos":9000000}}} +{"testCaseFinished":{"testCaseStartedId":"35","timestamp":{"seconds":0,"nanos":10000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"37","testCaseId":"28","timestamp":{"seconds":0,"nanos":11000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"37","testStepId":"29","timestamp":{"seconds":0,"nanos":12000000}}} +{"suggestion":{"id":"38","pickleStepId":"15","snippets":[{"language":"typescript","code":"Given(\"a step that is yet to be defined\", () => {\n return \"pending\"\n})"}]}} +{"testStepFinished":{"testCaseStartedId":"37","testStepId":"29","testStepResult":{"status":"UNDEFINED","duration":{"seconds":0,"nanos":0}},"timestamp":{"seconds":0,"nanos":13000000}}} +{"testStepStarted":{"testCaseStartedId":"37","testStepId":"30","timestamp":{"seconds":0,"nanos":14000000}}} +{"testStepFinished":{"testCaseStartedId":"37","testStepId":"30","testStepResult":{"status":"SKIPPED","duration":{"seconds":0,"nanos":0}},"timestamp":{"seconds":0,"nanos":15000000}}} +{"testCaseFinished":{"testCaseStartedId":"37","timestamp":{"seconds":0,"nanos":16000000},"willBeRetried":false}} +{"testCaseStarted":{"id":"39","testCaseId":"31","timestamp":{"seconds":0,"nanos":17000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"39","testStepId":"32","timestamp":{"seconds":0,"nanos":18000000}}} +{"suggestion":{"id":"40","pickleStepId":"18","snippets":[{"language":"typescript","code":"Given(\"a list of {int} things\", (int: number) => {\n return \"pending\"\n})"},{"language":"typescript","code":"Given(\"a list of {float} things\", (float: number) => {\n return \"pending\"\n})"}]}} +{"testStepFinished":{"testCaseStartedId":"39","testStepId":"32","testStepResult":{"status":"UNDEFINED","duration":{"seconds":0,"nanos":0}},"timestamp":{"seconds":0,"nanos":19000000}}} +{"testCaseFinished":{"testCaseStartedId":"39","timestamp":{"seconds":0,"nanos":20000000},"willBeRetried":false}} +{"testRunFinished":{"testRunStartedId":"22","timestamp":{"seconds":0,"nanos":21000000},"success":false}} diff --git a/compatibility/undefined/undefined.ts b/compatibility/undefined/undefined.ts new file mode 100644 index 00000000..3429c26b --- /dev/null +++ b/compatibility/undefined/undefined.ts @@ -0,0 +1,9 @@ +import { Given } from '@cucumber/fake-cucumber' + +Given('an implemented step', function () { + // no-op +}) + +Given('a step that will be skipped', function () { + // no-op +}) diff --git a/compatibility/unknown-parameter-type/unknown-parameter-type.cpp b/compatibility/unknown-parameter-type/unknown-parameter-type.cpp new file mode 100644 index 00000000..bea9d047 --- /dev/null +++ b/compatibility/unknown-parameter-type/unknown-parameter-type.cpp @@ -0,0 +1,10 @@ +#include "cucumber_cpp/CucumberCpp.hpp" +#include + +struct Airport +{}; + +GIVEN(R"({airport} is closed because of a strike)", (const Airport& airport)) +{ + FAIL() << "Should not be called because airport parameter type has not been defined"; +} diff --git a/compatibility/unknown-parameter-type/unknown-parameter-type.feature b/compatibility/unknown-parameter-type/unknown-parameter-type.feature new file mode 100644 index 00000000..1d68f1e6 --- /dev/null +++ b/compatibility/unknown-parameter-type/unknown-parameter-type.feature @@ -0,0 +1,7 @@ +Feature: Parameter Types + Cucumber will generate an error message if a step definition registers + an unknown parameter type, but the suite will run. Additionally, because + the step is effectively undefined, a suggestion will also be created. + + Scenario: undefined parameter type + Given CDG is closed because of a strike diff --git a/compatibility/unknown-parameter-type/unknown-parameter-type.ndjson b/compatibility/unknown-parameter-type/unknown-parameter-type.ndjson new file mode 100644 index 00000000..2c3ceeb9 --- /dev/null +++ b/compatibility/unknown-parameter-type/unknown-parameter-type.ndjson @@ -0,0 +1,13 @@ +{"meta":{"protocolVersion":"31.0.0","implementation":{"name":"fake-cucumber","version":"123.45.6"},"cpu":{"name":"arm64"},"os":{"name":"darwin","version":"24.5.0"},"runtime":{"name":"Node.js","version":"24.4.1"},"ci":{"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429","buildNumber":"154666429","git":{"revision":"99684bcacf01d95875834d87903dcb072306c9ad","remote":"https://github.com/cucumber-ltd/shouty.rb.git","branch":"main"}}}} +{"source":{"data":"Feature: Parameter Types\n Cucumber will generate an error message if a step definition registers\n an unknown parameter type, but the suite will run. Additionally, because\n the step is effectively undefined, a suggestion will also be created.\n\n Scenario: undefined parameter type\n Given CDG is closed because of a strike\n","uri":"samples/unknown-parameter-type/unknown-parameter-type.feature","mediaType":"text/x.cucumber.gherkin+plain"}} +{"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"Parameter Types","description":" Cucumber will generate an error message if a step definition registers\n an unknown parameter type, but the suite will run. Additionally, because\n the step is effectively undefined, a suggestion will also be created.","children":[{"scenario":{"id":"1","tags":[],"location":{"line":6,"column":3},"keyword":"Scenario","name":"undefined parameter type","description":"","steps":[{"id":"0","location":{"line":7,"column":5},"keyword":"Given ","keywordType":"Context","text":"CDG is closed because of a strike"}],"examples":[]}}]},"comments":[],"uri":"samples/unknown-parameter-type/unknown-parameter-type.feature"}} +{"pickle":{"id":"3","uri":"samples/unknown-parameter-type/unknown-parameter-type.feature","location":{"line":6,"column":3},"astNodeIds":["1"],"tags":[],"name":"undefined parameter type","language":"en","steps":[{"id":"2","text":"CDG is closed because of a strike","type":"Context","astNodeIds":["0"]}]}} +{"undefinedParameterType":{"name":"airport","expression":"{airport} is closed because of a strike"}} +{"testRunStarted":{"id":"5","timestamp":{"seconds":0,"nanos":0}}} +{"testCase":{"id":"6","pickleId":"3","testSteps":[{"id":"7","pickleStepId":"2","stepDefinitionIds":[],"stepMatchArgumentsLists":[]}],"testRunStartedId":"5"}} +{"testCaseStarted":{"id":"8","testCaseId":"6","timestamp":{"seconds":0,"nanos":1000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"8","testStepId":"7","timestamp":{"seconds":0,"nanos":2000000}}} +{"suggestion":{"id":"9","pickleStepId":"2","snippets":[{"language":"typescript","code":"Given(\"CDG is closed because of a strike\", () => {\n return \"pending\"\n})"}]}} +{"testStepFinished":{"testCaseStartedId":"8","testStepId":"7","testStepResult":{"status":"UNDEFINED","duration":{"seconds":0,"nanos":0}},"timestamp":{"seconds":0,"nanos":3000000}}} +{"testCaseFinished":{"testCaseStartedId":"8","timestamp":{"seconds":0,"nanos":4000000},"willBeRetried":false}} +{"testRunFinished":{"testRunStartedId":"5","timestamp":{"seconds":0,"nanos":5000000},"success":false}} diff --git a/compatibility/unknown-parameter-type/unknown-parameter-type.ts b/compatibility/unknown-parameter-type/unknown-parameter-type.ts new file mode 100644 index 00000000..984130a6 --- /dev/null +++ b/compatibility/unknown-parameter-type/unknown-parameter-type.ts @@ -0,0 +1,6 @@ +import { Given } from '@cucumber/fake-cucumber' + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +Given('{airport} is closed because of a strike', function (airport) { + throw new Error('Should not be called because airport parameter type has not been defined') +}) diff --git a/compatibility/unused-steps/unused-steps.cpp b/compatibility/unused-steps/unused-steps.cpp new file mode 100644 index 00000000..681b2a08 --- /dev/null +++ b/compatibility/unused-steps/unused-steps.cpp @@ -0,0 +1,10 @@ +#include "cucumber_cpp/CucumberCpp.hpp" +#include + +GIVEN(R"(a step that is used)") +{ +} + +GIVEN(R"(a step that is not used)") +{ +} diff --git a/compatibility/unused-steps/unused-steps.feature b/compatibility/unused-steps/unused-steps.feature new file mode 100644 index 00000000..19c48a29 --- /dev/null +++ b/compatibility/unused-steps/unused-steps.feature @@ -0,0 +1,6 @@ +Feature: Unused steps + Depending on the run, some step definitions may not be used. This is valid, and the step definitions are still + includes in the stream of messages, which allows formatters to report on step usage if desired. + + Scenario: a scenario + Given a step that is used diff --git a/compatibility/unused-steps/unused-steps.ndjson b/compatibility/unused-steps/unused-steps.ndjson new file mode 100644 index 00000000..89cc480b --- /dev/null +++ b/compatibility/unused-steps/unused-steps.ndjson @@ -0,0 +1,13 @@ +{"meta":{"protocolVersion":"31.0.0","implementation":{"name":"fake-cucumber","version":"123.45.6"},"cpu":{"name":"arm64"},"os":{"name":"darwin","version":"24.5.0"},"runtime":{"name":"Node.js","version":"24.4.1"},"ci":{"name":"GitHub Actions","url":"https://github.com/cucumber-ltd/shouty.rb/actions/runs/154666429","buildNumber":"154666429","git":{"revision":"99684bcacf01d95875834d87903dcb072306c9ad","remote":"https://github.com/cucumber-ltd/shouty.rb.git","branch":"main"}}}} +{"source":{"data":"Feature: Unused steps\n Depending on the run, some step definitions may not be used. This is valid, and the step definitions are still\n includes in the stream of messages, which allows formatters to report on step usage if desired.\n\n Scenario: a scenario\n Given a step that is used\n","uri":"samples/unused-steps/unused-steps.feature","mediaType":"text/x.cucumber.gherkin+plain"}} +{"gherkinDocument":{"feature":{"tags":[],"location":{"line":1,"column":1},"language":"en","keyword":"Feature","name":"Unused steps","description":" Depending on the run, some step definitions may not be used. This is valid, and the step definitions are still\n includes in the stream of messages, which allows formatters to report on step usage if desired.","children":[{"scenario":{"id":"1","tags":[],"location":{"line":5,"column":3},"keyword":"Scenario","name":"a scenario","description":"","steps":[{"id":"0","location":{"line":6,"column":5},"keyword":"Given ","keywordType":"Context","text":"a step that is used"}],"examples":[]}}]},"comments":[],"uri":"samples/unused-steps/unused-steps.feature"}} +{"pickle":{"id":"3","uri":"samples/unused-steps/unused-steps.feature","location":{"line":5,"column":3},"astNodeIds":["1"],"tags":[],"name":"a scenario","language":"en","steps":[{"id":"2","text":"a step that is used","type":"Context","astNodeIds":["0"]}]}} +{"stepDefinition":{"id":"4","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a step that is used"},"sourceReference":{"uri":"samples/unused-steps/unused-steps.ts","location":{"line":3}}}} +{"stepDefinition":{"id":"5","pattern":{"type":"CUCUMBER_EXPRESSION","source":"a step that is not used"},"sourceReference":{"uri":"samples/unused-steps/unused-steps.ts","location":{"line":7}}}} +{"testRunStarted":{"id":"6","timestamp":{"seconds":0,"nanos":0}}} +{"testCase":{"id":"7","pickleId":"3","testSteps":[{"id":"8","pickleStepId":"2","stepDefinitionIds":["4"],"stepMatchArgumentsLists":[{"stepMatchArguments":[]}]}],"testRunStartedId":"6"}} +{"testCaseStarted":{"id":"9","testCaseId":"7","timestamp":{"seconds":0,"nanos":1000000},"attempt":0}} +{"testStepStarted":{"testCaseStartedId":"9","testStepId":"8","timestamp":{"seconds":0,"nanos":2000000}}} +{"testStepFinished":{"testCaseStartedId":"9","testStepId":"8","testStepResult":{"status":"PASSED","duration":{"seconds":0,"nanos":1000000}},"timestamp":{"seconds":0,"nanos":3000000}}} +{"testCaseFinished":{"testCaseStartedId":"9","timestamp":{"seconds":0,"nanos":4000000},"willBeRetried":false}} +{"testRunFinished":{"testRunStartedId":"6","timestamp":{"seconds":0,"nanos":5000000},"success":true}} diff --git a/compatibility/unused-steps/unused-steps.ts b/compatibility/unused-steps/unused-steps.ts new file mode 100644 index 00000000..1d1b8759 --- /dev/null +++ b/compatibility/unused-steps/unused-steps.ts @@ -0,0 +1,9 @@ +import { Given } from '@cucumber/fake-cucumber' + +Given('a step that is used', () => { + // no-op +}) + +Given('a step that is not used', () => { + // no-op +}) diff --git a/cucumber_cpp/CMakeLists.txt b/cucumber_cpp/CMakeLists.txt index eca2965c..6d6c51c0 100644 --- a/cucumber_cpp/CMakeLists.txt +++ b/cucumber_cpp/CMakeLists.txt @@ -1,9 +1,10 @@ - - add_subdirectory(library) add_subdirectory(runner) -add_subdirectory(example) -add_subdirectory(acceptance_test) + +if (CCR_STANDALONE) + add_subdirectory(example) + add_subdirectory(acceptance_test) +endif() add_library(cucumber_cpp INTERFACE) @@ -12,7 +13,7 @@ target_sources(cucumber_cpp INTERFACE ) target_include_directories(cucumber_cpp INTERFACE - ./ + $ ) target_link_libraries(cucumber_cpp INTERFACE diff --git a/cucumber_cpp/CucumberCpp.hpp b/cucumber_cpp/CucumberCpp.hpp index 1bad8e67..cc146b26 100644 --- a/cucumber_cpp/CucumberCpp.hpp +++ b/cucumber_cpp/CucumberCpp.hpp @@ -4,17 +4,17 @@ #include "cucumber_cpp/library/Application.hpp" #include "cucumber_cpp/library/Context.hpp" #include "cucumber_cpp/library/Hooks.hpp" +#include "cucumber_cpp/library/Parameter.hpp" #include "cucumber_cpp/library/Steps.hpp" +#include "cucumber_cpp/library/cucumber_expression/MatchRange.hpp" #include "cucumber_cpp/library/engine/StringTo.hpp" -#include "cucumber_cpp/library/report/Report.hpp" namespace cucumber_cpp { using cucumber_cpp::library::Application; using cucumber_cpp::library::Context; - using cucumber_cpp::library::engine::Step; + using cucumber_cpp::library::engine::StepBase; using cucumber_cpp::library::engine::StringTo; - using cucumber_cpp::library::report::ReportHandlerV2; } #endif diff --git a/cucumber_cpp/acceptance_test/CMakeLists.txt b/cucumber_cpp/acceptance_test/CMakeLists.txt index eda7ed36..b04cd4c8 100644 --- a/cucumber_cpp/acceptance_test/CMakeLists.txt +++ b/cucumber_cpp/acceptance_test/CMakeLists.txt @@ -21,5 +21,13 @@ target_link_libraries(cucumber_cpp.acceptance_test.custom PRIVATE cucumber_cpp.acceptance_test.steps ) +add_executable(cucumber_cpp.acceptance_test.unused ${CCR_EXCLUDE_FROM_ALL}) + +target_link_libraries(cucumber_cpp.acceptance_test.unused PRIVATE + cucumber_cpp + cucumber_cpp.runner + cucumber_cpp.acceptance_test.usedunused +) + add_subdirectory(hooks) add_subdirectory(steps) diff --git a/cucumber_cpp/acceptance_test/coverage.bats b/cucumber_cpp/acceptance_test/coverage.bats deleted file mode 100644 index e14cbe43..00000000 --- a/cucumber_cpp/acceptance_test/coverage.bats +++ /dev/null @@ -1,208 +0,0 @@ -#!/usr/bin/env bats - -setup() { - load '/usr/local/bats-support/load' - load '/usr/local/bats-assert/load' -} - -teardown() { - rm -rf ./out/ -} - -@test "Successful test" { - run .build/Coverage/cucumber_cpp/acceptance_test/cucumber_cpp.acceptance_test run --tag "@result:OK" --feature cucumber_cpp/acceptance_test/features --report console - assert_success -} - -@test "Parse tag expression" { - run .build/Coverage/cucumber_cpp/acceptance_test/cucumber_cpp.acceptance_test run --tag @smoke @result:OK --feature cucumber_cpp/acceptance_test/features --report console - assert_success -} - -@test "Failed tests" { - run .build/Coverage/cucumber_cpp/acceptance_test/cucumber_cpp.acceptance_test run --tag "@result:FAILED" --feature cucumber_cpp/acceptance_test/features --report console - assert_failure - assert_output --partial "failed \"cucumber_cpp/acceptance_test/features/test_scenarios.feature\"" - assert_output --partial "skipped Then a then step" -} - -@test "Undefined tests" { - run .build/Coverage/cucumber_cpp/acceptance_test/cucumber_cpp.acceptance_test run --tag "@result:UNDEFINED" --feature cucumber_cpp/acceptance_test/features --report console - assert_failure - assert_output --partial "undefined \"cucumber_cpp/acceptance_test/features/test_scenarios.feature\"" - assert_output --partial "skipped Then this should be skipped" -} - -@test "No tests" { - run .build/Coverage/cucumber_cpp/acceptance_test/cucumber_cpp.acceptance_test run --tag "@invalidtag" --feature cucumber_cpp/acceptance_test/features --report console - assert_success -} - -@test "All features in a folder" { - run .build/Coverage/cucumber_cpp/acceptance_test/cucumber_cpp.acceptance_test run --feature cucumber_cpp/acceptance_test/features/subfolder --report console - assert_success - assert_output --partial "test1 scenario" - assert_output --partial "test2 scenario" -} - -@test "Missing mandatory feature argument" { - run .build/Coverage/cucumber_cpp/acceptance_test/cucumber_cpp.acceptance_test run --report console - assert_failure - assert_output --partial "--feature is required" -} - -@test "Missing mandatory report argument" { - run .build/Coverage/cucumber_cpp/acceptance_test/cucumber_cpp.acceptance_test run --feature cucumber_cpp/acceptance_test/features - assert_failure - assert_output --partial "--report is required" -} - -@test "Missing mandatory custom argument" { - run .build/Coverage/cucumber_cpp/acceptance_test/cucumber_cpp.acceptance_test.custom run --feature cucumber_cpp/acceptance_test/features --report console - assert_failure - assert_output --partial "--required is required" -} - -@test "Second feature file does not overwrite success with an undefined status" { - run .build/Coverage/cucumber_cpp/acceptance_test/cucumber_cpp.acceptance_test run --tag "@undefinedsuccess and @result:success" --feature cucumber_cpp/acceptance_test/features/test_undefined_success_1.feature cucumber_cpp/acceptance_test/features/test_undefined_success_2.feature --report console - assert_success -} - -@test "Valid reporters only" { - run .build/Coverage/cucumber_cpp/acceptance_test/cucumber_cpp.acceptance_test run --feature cucumber_cpp/acceptance_test/features --report doesnotexist - assert_failure - assert_output --partial "--report: 'doesnotexist' is not a reporter" -} - -@test "Run Program hooks" { - run .build/Coverage/cucumber_cpp/acceptance_test/cucumber_cpp.acceptance_test run --feature cucumber_cpp/acceptance_test/features --tag @bats and @program_hooks --report console - assert_success - - assert_output --partial "HOOK_BEFORE_ALL" - assert_output --partial "HOOK_AFTER_ALL" -} - -@test "Run Scenario hooks" { - run .build/Coverage/cucumber_cpp/acceptance_test/cucumber_cpp.acceptance_test run --feature cucumber_cpp/acceptance_test/features --tag @bats and @scenariohook and not @stephook --report console - assert_success - - assert_output --partial "HOOK_BEFORE_SCENARIO" - assert_output --partial "HOOK_AFTER_SCENARIO" - - refute_output --partial "HOOK_BEFORE_STEP" - refute_output --partial "HOOK_AFTER_STEP" -} - -@test "Run Step hooks" { - run .build/Coverage/cucumber_cpp/acceptance_test/cucumber_cpp.acceptance_test run --feature cucumber_cpp/acceptance_test/features --tag @bats and @stephook and not @scenariohook --report console - assert_success - - refute_output --partial "HOOK_BEFORE_SCENARIO" - refute_output --partial "HOOK_AFTER_SCENARIO" - - assert_output --partial "HOOK_BEFORE_STEP" - assert_output --partial "HOOK_AFTER_STEP" -} - -@test "Run Scenario and Step hooks" { - run .build/Coverage/cucumber_cpp/acceptance_test/cucumber_cpp.acceptance_test run --feature cucumber_cpp/acceptance_test/features --tag "@bats and (@scenariohook or @stephook)" --report console - assert_success - - assert_output --partial "HOOK_BEFORE_SCENARIO" - assert_output --partial "HOOK_AFTER_SCENARIO" - - assert_output --partial "HOOK_BEFORE_STEP" - assert_output --partial "HOOK_AFTER_STEP" -} - -@test "Dry run with known failing steps" { - run .build/Coverage/cucumber_cpp/acceptance_test/cucumber_cpp.acceptance_test run --feature cucumber_cpp/acceptance_test/features --tag "@result:FAILED" --report console - assert_failure - - run .build/Coverage/cucumber_cpp/acceptance_test/cucumber_cpp.acceptance_test run --feature cucumber_cpp/acceptance_test/features --tag "@result:FAILED" --report console --dry - assert_success -} - -@test "Dry run with known missing steps" { - run .build/Coverage/cucumber_cpp/acceptance_test/cucumber_cpp.acceptance_test run --feature cucumber_cpp/acceptance_test/features --tag "@result:UNDEFINED" --report console --dry - assert_failure - assert_output --partial "undefined \"cucumber_cpp/acceptance_test/features/test_scenarios.feature\"" - assert_output --partial "skipped Then this should be skipped" -} - -@test "Test the and keyword" { - run .build/Coverage/cucumber_cpp/acceptance_test/cucumber_cpp.acceptance_test run --feature cucumber_cpp/acceptance_test/features --tag "@keyword-and" --report console - assert_success - assert_output --partial "--when--" - assert_output --partial "--and--" -} - -@test "Test the but keyword" { - run .build/Coverage/cucumber_cpp/acceptance_test/cucumber_cpp.acceptance_test run --feature cucumber_cpp/acceptance_test/features --tag "@keyword-but" --report console - assert_success - assert_output --partial "--when--" - assert_output --partial "--but--" -} - -@test "Test the asterisk keyword - will fail" { - run .build/Coverage/cucumber_cpp/acceptance_test/cucumber_cpp.acceptance_test run --feature cucumber_cpp/acceptance_test/features --tag "@keyword-asterisk" --report console - assert_failure -} - -@test "Test passing scenario after failed scenario reports feature as failed" { - run .build/Coverage/cucumber_cpp/acceptance_test/cucumber_cpp.acceptance_test run --feature cucumber_cpp/acceptance_test/features --tag "@fail_feature" --report console - assert_failure - assert_output --partial "tests : 1/2 passed" -} - -@test "Test failing hook before results in error" { - run .build/Coverage/cucumber_cpp/acceptance_test/cucumber_cpp.acceptance_test run --feature cucumber_cpp/acceptance_test/features --tag "@fail_scenariohook_before" --report console - assert_failure - assert_output --partial "skipped Given a given step" - assert_output --partial "tests : 0/1 passed" -} - -@test "Test failing hook after results in error" { - run .build/Coverage/cucumber_cpp/acceptance_test/cucumber_cpp.acceptance_test run --feature cucumber_cpp/acceptance_test/features --tag "@fail_scenariohook_after" --report console - assert_failure - assert_output --partial "Given a given step" - assert_output --partial "done" - assert_output --partial "failed" - assert_output --partial "tests : 0/1 passed" -} - -@test "Test throwing hook results in error" { - run .build/Coverage/cucumber_cpp/acceptance_test/cucumber_cpp.acceptance_test run --feature cucumber_cpp/acceptance_test/features --tag "@throw_scenariohook" --report console - assert_failure - assert_output --partial "skipped Given a given step" - assert_output --partial "tests : 0/1 passed" -} - - -@test "Test error program hook results in error and skipped steps" { - run .build/Coverage/cucumber_cpp/acceptance_test/cucumber_cpp.acceptance_test.custom run --feature cucumber_cpp/acceptance_test/features --tag "@smoke and @result:OK" --report console --required --failprogramhook - assert_failure - refute_output --partial "skipped Given a given step" - refute_output --partial "should not be executed" - assert_output --partial "tests : 0/0 passed" -} - -@test "Test unicode" { - run .build/Coverage/cucumber_cpp/acceptance_test/cucumber_cpp.acceptance_test run --feature cucumber_cpp/acceptance_test/features --tag "@unicode" --report console - assert_success - assert_output --partial "tests : 1/1 passed" -} - -@test "TestExceptionContinuesWithNextScenario" { - run .build/Coverage/cucumber_cpp/acceptance_test/cucumber_cpp.acceptance_test run --feature cucumber_cpp/acceptance_test/features --tag "@stepthrowcontinues" --report console - assert_failure - assert_output --partial "Exception thrown" - refute_output --partial "Should Not Be Thrown" - assert_output --partial "tests : 1/2 passed" -} - -@test "RunFeatureFileWithError" { - run .build/Coverage/cucumber_cpp/acceptance_test/cucumber_cpp.acceptance_test run --feature cucumber_cpp/acceptance_test/features --tag "@fail_scenario" --report console - assert_failure - assert_output --partial "tests : 1/2 passed" -} diff --git a/cucumber_cpp/acceptance_test/features/test_attachment.feature b/cucumber_cpp/acceptance_test/features/test_attachment.feature new file mode 100644 index 00000000..8dd0425c --- /dev/null +++ b/cucumber_cpp/acceptance_test/features/test_attachment.feature @@ -0,0 +1,7 @@ +@attachment +Feature: Simple feature file + Background: + Given a background step + + Scenario: An OK scenario + Given I attach a link to "my_url.txt" diff --git a/cucumber_cpp/acceptance_test/features/test_nested_steps.feature b/cucumber_cpp/acceptance_test/features/test_nested_steps.feature new file mode 100644 index 00000000..d4228606 --- /dev/null +++ b/cucumber_cpp/acceptance_test/features/test_nested_steps.feature @@ -0,0 +1,5 @@ +@nested_steps +Feature: Nested Steps + Scenario: Call other steps from within a step + Given a step calls another step with "cucumber" + Then the stored string is "cucumber" diff --git a/cucumber_cpp/acceptance_test/features/test_scenario_rules.feature b/cucumber_cpp/acceptance_test/features/test_scenario_rules.feature new file mode 100644 index 00000000..2e066607 --- /dev/null +++ b/cucumber_cpp/acceptance_test/features/test_scenario_rules.feature @@ -0,0 +1,23 @@ +@rules +Feature: Simple feature file + Background: + Given a background step + + @result:OK + Scenario: An OK scenario + Given a given step + When a when step + Then a then step + + Rule: a simple rule with two scenarios + @result:FAILED + Scenario: A failing scenario + Given a given step + When a when step + Then an assertion is raised + Then a then step + + @result:UNDEFINED + Scenario: A scenario with undefined step + Given a missing step + Then this should be skipped diff --git a/cucumber_cpp/acceptance_test/features/test_scenarios.feature b/cucumber_cpp/acceptance_test/features/test_scenarios.feature index 07a315ee..2eb4280c 100644 --- a/cucumber_cpp/acceptance_test/features/test_scenarios.feature +++ b/cucumber_cpp/acceptance_test/features/test_scenarios.feature @@ -16,7 +16,19 @@ Feature: Simple feature file Then an assertion is raised Then a then step + @result:FAILED + Scenario: A failing scenario + Given a given step + When a when step + Then an exception is thrown + Then a then step + @result:UNDEFINED Scenario: A scenario with undefined step Given a missing step Then this should be skipped + + @result:AMBIGUOUS + Scenario: A scenario with ambiguous step + Given an ambiguous step + Then this should be skipped diff --git a/cucumber_cpp/acceptance_test/features/test_scenarios_unused.feature b/cucumber_cpp/acceptance_test/features/test_scenarios_unused.feature new file mode 100644 index 00000000..c36fb3d9 --- /dev/null +++ b/cucumber_cpp/acceptance_test/features/test_scenarios_unused.feature @@ -0,0 +1,4 @@ +@unused +Feature: Simple feature file + Scenario: An unused scenario + Given this step is used diff --git a/cucumber_cpp/acceptance_test/features/test_undefined_success_1.feature b/cucumber_cpp/acceptance_test/features/test_undefined_success_1.feature index e690cef7..909e7c1f 100644 --- a/cucumber_cpp/acceptance_test/features/test_undefined_success_1.feature +++ b/cucumber_cpp/acceptance_test/features/test_undefined_success_1.feature @@ -1,7 +1,6 @@ @undefinedsuccess Feature: Undefined success results in success - @result:success Scenario: Results in success Given a given step When a when step - Then a then step + Then a then step is missing diff --git a/cucumber_cpp/acceptance_test/features/test_undefined_success_2.feature b/cucumber_cpp/acceptance_test/features/test_undefined_success_2.feature index 29104865..d9b9ff09 100644 --- a/cucumber_cpp/acceptance_test/features/test_undefined_success_2.feature +++ b/cucumber_cpp/acceptance_test/features/test_undefined_success_2.feature @@ -1,6 +1,5 @@ @undefinedsuccess Feature: Undefined success results in undefined - @result:undefined Scenario: Results in undefined Given a given step When a when step diff --git a/cucumber_cpp/acceptance_test/hooks/CMakeLists.txt b/cucumber_cpp/acceptance_test/hooks/CMakeLists.txt index ee263a66..2412ab7e 100644 --- a/cucumber_cpp/acceptance_test/hooks/CMakeLists.txt +++ b/cucumber_cpp/acceptance_test/hooks/CMakeLists.txt @@ -7,5 +7,5 @@ target_sources(cucumber_cpp.acceptance_test.hooks PRIVATE ) target_link_libraries(cucumber_cpp.acceptance_test.hooks PRIVATE - cucumber_cpp.library + cucumber_cpp ) diff --git a/cucumber_cpp/acceptance_test/hooks/Hooks.cpp b/cucumber_cpp/acceptance_test/hooks/Hooks.cpp index 056172f2..de7fc35e 100644 --- a/cucumber_cpp/acceptance_test/hooks/Hooks.cpp +++ b/cucumber_cpp/acceptance_test/hooks/Hooks.cpp @@ -1,5 +1,6 @@ #include "cucumber_cpp/CucumberCpp.hpp" #include "gmock/gmock.h" +#include #include #include @@ -8,7 +9,7 @@ HOOK_BEFORE_ALL() std::cout << "HOOK_BEFORE_ALL\n"; if (context.Contains("--failprogramhook") && context.Get("--failprogramhook")) - ASSERT_THAT(false, testing::IsTrue()); + FAIL(); } HOOK_AFTER_ALL() @@ -38,12 +39,12 @@ HOOK_AFTER_STEP("@stephook and @bats") HOOK_BEFORE_SCENARIO("@fail_scenariohook_before") { - ASSERT_THAT(false, testing::IsTrue()); + FAIL(); } HOOK_AFTER_SCENARIO("@fail_scenariohook_after") { - ASSERT_THAT(false, testing::IsTrue()); + FAIL(); } HOOK_BEFORE_SCENARIO("@throw_scenariohook") @@ -51,7 +52,7 @@ HOOK_BEFORE_SCENARIO("@throw_scenariohook") throw std::string{ "error" }; } -HOOK_BEFORE_SCENARIO() +HOOK_BEFORE_SCENARIO(.name = "fail if --failprogramhook is set") { if (context.Contains("--failprogramhook") && context.Get("--failprogramhook")) std::cout << "should not be executed\n"; diff --git a/cucumber_cpp/acceptance_test/steps/CMakeLists.txt b/cucumber_cpp/acceptance_test/steps/CMakeLists.txt index a5185d8c..046ef91c 100644 --- a/cucumber_cpp/acceptance_test/steps/CMakeLists.txt +++ b/cucumber_cpp/acceptance_test/steps/CMakeLists.txt @@ -7,5 +7,14 @@ target_sources(cucumber_cpp.acceptance_test.steps PRIVATE ) target_link_libraries(cucumber_cpp.acceptance_test.steps PRIVATE - cucumber_cpp.library + cucumber_cpp +) +add_library(cucumber_cpp.acceptance_test.usedunused OBJECT ${CCR_EXCLUDE_FROM_ALL}) + +target_sources(cucumber_cpp.acceptance_test.usedunused PRIVATE + UsedUnused.cpp +) + +target_link_libraries(cucumber_cpp.acceptance_test.usedunused PRIVATE + cucumber_cpp ) diff --git a/cucumber_cpp/acceptance_test/steps/Steps.cpp b/cucumber_cpp/acceptance_test/steps/Steps.cpp index 063e60b3..a6cc3119 100644 --- a/cucumber_cpp/acceptance_test/steps/Steps.cpp +++ b/cucumber_cpp/acceptance_test/steps/Steps.cpp @@ -1,4 +1,5 @@ #include "cucumber_cpp/CucumberCpp.hpp" +#include "fmt/format.h" #include "gmock/gmock.h" #include "gtest/gtest.h" #include @@ -6,6 +7,16 @@ #include #include +GIVEN("an ambiguous step") +{ + // empty +} + +GIVEN("a(n) ambiguous step") +{ + // empty +} + GIVEN("a background step") { // empty @@ -33,14 +44,25 @@ STEP("a step step") WHEN("I print {string}", (const std::string& str)) { - std::cout << "print: " << str; + std::cout << fmt::format("print: {}\n", str); } THEN("an assertion is raised") { + EXPECT_THAT(1, testing::Eq(2)); ASSERT_THAT(false, testing::IsTrue()); } +THEN("an exception is thrown") +{ + struct CustomException : public std::runtime_error + { + using runtime_error::runtime_error; + }; + + throw CustomException{ "This is a custom exception" }; +} + THEN("two expectations are raised") { EXPECT_THAT(0, testing::Eq(1)); @@ -59,8 +81,8 @@ THEN("this should be skipped") GIVEN("Next block of text enclosed in \"\"\" characters") { - - ASSERT_THAT(docString, testing::Eq("Multiline\nDocstring")); + ASSERT_THAT(docString, testing::IsTrue()); + ASSERT_THAT(docString->content, testing::StrEq("Multiline\nDocstring")); } WHEN("this step is being used") @@ -92,3 +114,29 @@ GIVEN("{int} and {int} are equal", (std::int32_t a, std::int32_t b)) { EXPECT_THAT(a, testing::Eq(b)); } + +GIVEN(R"(a step calls another step with {string})", (const std::string& str)) +{ + Step(fmt::format(R"(I store "{}")", str)); +} + +GIVEN(R"(I store {string})", (const std::string& str)) +{ + Step(fmt::format(R"(I store "{}" again)", str)); +} + +GIVEN(R"(I store {string} again)", (const std::string& str)) +{ + context.InsertAt("storedstring", str); +} + +THEN(R"(the stored string is {string})", (const std::string& expected)) +{ + const auto& stored = context.Get("storedstring"); + EXPECT_THAT(stored, testing::StrEq(expected)); +} + +GIVEN(R"(I attach a link to {string})", (const std::string& url)) +{ + Link(url, "title"); +} diff --git a/cucumber_cpp/acceptance_test/steps/UsedUnused.cpp b/cucumber_cpp/acceptance_test/steps/UsedUnused.cpp new file mode 100644 index 00000000..691b27c8 --- /dev/null +++ b/cucumber_cpp/acceptance_test/steps/UsedUnused.cpp @@ -0,0 +1,7 @@ +#include "cucumber_cpp/CucumberCpp.hpp" + +STEP("This step is unused") +{} + +STEP("this step is used") +{} diff --git a/cucumber_cpp/acceptance_test/test.bats b/cucumber_cpp/acceptance_test/test.bats index ed9ccbdf..d7ba7f2f 100644 --- a/cucumber_cpp/acceptance_test/test.bats +++ b/cucumber_cpp/acceptance_test/test.bats @@ -1,5 +1,10 @@ #!/usr/bin/env bats +setup_file() { + acceptance_test=$(find . -name "cucumber_cpp.acceptance_test" -print -quit) + export acceptance_test +} + setup() { load '/usr/local/bats-support/load' load '/usr/local/bats-assert/load' @@ -10,72 +15,58 @@ teardown() { } @test "Successful test" { - run .build/Host/cucumber_cpp/acceptance_test/Debug/cucumber_cpp.acceptance_test run --tag "@result:OK" --feature cucumber_cpp/acceptance_test/features --report console + run $acceptance_test --format summary pretty message junit --tags "@result:OK" --no-recursive -- cucumber_cpp/acceptance_test/features assert_success } @test "Parse tag expression" { - run .build/Host/cucumber_cpp/acceptance_test/Debug/cucumber_cpp.acceptance_test run --tag @smoke @result:OK --feature cucumber_cpp/acceptance_test/features --report console + run $acceptance_test --format summary pretty message junit --tags "@smoke and @result:OK" -- cucumber_cpp/acceptance_test/features assert_success } @test "Failed tests" { - run .build/Host/cucumber_cpp/acceptance_test/Debug/cucumber_cpp.acceptance_test run --tag "@result:FAILED" --feature cucumber_cpp/acceptance_test/features --report console + run $acceptance_test --format summary pretty message junit --tags "@smoke and @result:FAILED" -- cucumber_cpp/acceptance_test/features assert_failure - assert_output --partial "failed \"cucumber_cpp/acceptance_test/features/test_scenarios.feature\"" - assert_output --partial "skipped Then a then step" } -@test "Undefined tests" { - run .build/Host/cucumber_cpp/acceptance_test/Debug/cucumber_cpp.acceptance_test run --tag "@result:UNDEFINED" --feature cucumber_cpp/acceptance_test/features --report console +@test "■ tests" { + run $acceptance_test --format summary --format-options "{ \"summary\": {\"theme\":\"plain\"} }" --tags "@result:UNDEFINED" -- cucumber_cpp/acceptance_test/features assert_failure - assert_output --partial "undefined \"cucumber_cpp/acceptance_test/features/test_scenarios.feature\"" - assert_output --partial "skipped Then this should be skipped" + assert_output --partial "■ Given a missing step" + assert_output --partial "↷ Then this should be skipped" } @test "No tests" { - run .build/Host/cucumber_cpp/acceptance_test/Debug/cucumber_cpp.acceptance_test run --tag "@invalidtag" --feature cucumber_cpp/acceptance_test/features --report console + run $acceptance_test --format summary pretty message junit --tags "@invalidtag" -- cucumber_cpp/acceptance_test/features assert_success } @test "All features in a folder" { - run .build/Host/cucumber_cpp/acceptance_test/Debug/cucumber_cpp.acceptance_test run --feature cucumber_cpp/acceptance_test/features/subfolder --report console + run $acceptance_test --format summary pretty message junit -- cucumber_cpp/acceptance_test/features/subfolder assert_success - assert_output --partial "test1 scenario" - assert_output --partial "test2 scenario" -} - -@test "Missing mandatory feature argument" { - run .build/Host/cucumber_cpp/acceptance_test/Debug/cucumber_cpp.acceptance_test run --report console - assert_failure - assert_output --partial "--feature is required" -} - -@test "Missing mandatory report argument" { - run .build/Host/cucumber_cpp/acceptance_test/Debug/cucumber_cpp.acceptance_test run --feature cucumber_cpp/acceptance_test/features - assert_failure - assert_output --partial "--report is required" + assert_output --partial "2 scenarios" + assert_output --partial "2 passed" } @test "Missing mandatory custom argument" { - run .build/Host/cucumber_cpp/acceptance_test/Debug/cucumber_cpp.acceptance_test.custom run --feature cucumber_cpp/acceptance_test/features --report console + run $acceptance_test.custom --format summary pretty message junit -- cucumber_cpp/acceptance_test/features assert_failure assert_output --partial "--required is required" } -@test "Second feature file does not overwrite success with an undefined status" { - run .build/Host/cucumber_cpp/acceptance_test/Debug/cucumber_cpp.acceptance_test run --tag "@undefinedsuccess and @result:success" --feature cucumber_cpp/acceptance_test/features/test_undefined_success_1.feature cucumber_cpp/acceptance_test/features/test_undefined_success_2.feature --report console - assert_success +@test "Second feature file does not overwrite success with an ■ status" { + run $acceptance_test --format summary pretty message junit --tags @undefinedsuccess -- cucumber_cpp/acceptance_test/features/test_undefined_success_1.feature cucumber_cpp/acceptance_test/features/test_undefined_success_2.feature + assert_failure } @test "Valid reporters only" { - run .build/Host/cucumber_cpp/acceptance_test/Debug/cucumber_cpp.acceptance_test run --feature cucumber_cpp/acceptance_test/features --report doesnotexist + run $acceptance_test --format doesnotexist -- cucumber_cpp/acceptance_test/features assert_failure - assert_output --partial "--report: 'doesnotexist' is not a reporter" + assert_output --partial "--format: 'doesnotexist' is not a valid formatter" } @test "Run Program hooks" { - run .build/Host/cucumber_cpp/acceptance_test/Debug/cucumber_cpp.acceptance_test run --feature cucumber_cpp/acceptance_test/features --tag @bats and @program_hooks --report console + run $acceptance_test --format summary pretty message junit --tags @bats and @program_hooks -- cucumber_cpp/acceptance_test/features assert_success assert_output --partial "HOOK_BEFORE_ALL" @@ -83,7 +74,7 @@ teardown() { } @test "Run Scenario hooks" { - run .build/Host/cucumber_cpp/acceptance_test/Debug/cucumber_cpp.acceptance_test run --feature cucumber_cpp/acceptance_test/features --tag @bats and @scenariohook and not @stephook --report console + run $acceptance_test --format summary pretty message junit --tags @bats and @scenariohook and not @stephook -- cucumber_cpp/acceptance_test/features assert_success assert_output --partial "HOOK_BEFORE_SCENARIO" @@ -94,7 +85,7 @@ teardown() { } @test "Run Step hooks" { - run .build/Host/cucumber_cpp/acceptance_test/Debug/cucumber_cpp.acceptance_test run --feature cucumber_cpp/acceptance_test/features --tag @bats and @stephook and not @scenariohook --report console + run $acceptance_test --format summary pretty message junit --tags @bats and @stephook and not @scenariohook -- cucumber_cpp/acceptance_test/features assert_success refute_output --partial "HOOK_BEFORE_SCENARIO" @@ -105,7 +96,7 @@ teardown() { } @test "Run Scenario and Step hooks" { - run .build/Host/cucumber_cpp/acceptance_test/Debug/cucumber_cpp.acceptance_test run --feature cucumber_cpp/acceptance_test/features --tag "@bats and (@scenariohook or @stephook)" --report console + run $acceptance_test --format summary pretty message junit --tags "@bats and (@scenariohook or @stephook)" -- cucumber_cpp/acceptance_test/features assert_success assert_output --partial "HOOK_BEFORE_SCENARIO" @@ -116,92 +107,118 @@ teardown() { } @test "Dry run with known failing steps" { - run .build/Host/cucumber_cpp/acceptance_test/Debug/cucumber_cpp.acceptance_test run --feature cucumber_cpp/acceptance_test/features --tag "@result:FAILED" --report console + run $acceptance_test --format summary pretty message junit --tags "@result:FAILED" -- cucumber_cpp/acceptance_test/features assert_failure - run .build/Host/cucumber_cpp/acceptance_test/Debug/cucumber_cpp.acceptance_test run --feature cucumber_cpp/acceptance_test/features --tag "@result:FAILED" --report console --dry + run $acceptance_test --format summary pretty message junit --tags "@result:FAILED" --dry-run cucumber_cpp/acceptance_test/features assert_success } @test "Dry run with known missing steps" { - run .build/Host/cucumber_cpp/acceptance_test/Debug/cucumber_cpp.acceptance_test run --feature cucumber_cpp/acceptance_test/features --tag "@result:UNDEFINED" --report console --dry + run $acceptance_test --format summary pretty message junit --tags "@result:UNDEFINED" -- cucumber_cpp/acceptance_test/features assert_failure - assert_output --partial "undefined \"cucumber_cpp/acceptance_test/features/test_scenarios.feature\"" - assert_output --partial "skipped Then this should be skipped" + + run $acceptance_test --format summary --format-options "{ \"summary\": {\"theme\":\"plain\"} }" --tags "@result:UNDEFINED" --dry-run cucumber_cpp/acceptance_test/features + assert_success + assert_output --partial "■ Given a missing step" } @test "Test the and keyword" { - run .build/Host/cucumber_cpp/acceptance_test/Debug/cucumber_cpp.acceptance_test run --feature cucumber_cpp/acceptance_test/features --tag "@keyword-and" --report console + run $acceptance_test --format summary pretty message junit --tags "@keyword-and" -- cucumber_cpp/acceptance_test/features assert_success assert_output --partial "--when--" assert_output --partial "--and--" } @test "Test the but keyword" { - run .build/Host/cucumber_cpp/acceptance_test/Debug/cucumber_cpp.acceptance_test run --feature cucumber_cpp/acceptance_test/features --tag "@keyword-but" --report console + run $acceptance_test --format summary pretty message junit --tags "@keyword-but" -- cucumber_cpp/acceptance_test/features assert_success assert_output --partial "--when--" assert_output --partial "--but--" } -@test "Test the asterisk keyword - will fail" { - run .build/Host/cucumber_cpp/acceptance_test/Debug/cucumber_cpp.acceptance_test run --feature cucumber_cpp/acceptance_test/features --tag "@keyword-asterisk" --report console - assert_failure +@test "Test the asterisk keyword" { + run $acceptance_test --format summary pretty message junit --tags "@keyword-asterisk" -- cucumber_cpp/acceptance_test/features + assert_output --partial "print: --when--" + assert_output --partial "print: --asterisk--" + assert_success } @test "Test passing scenario after failed scenario reports feature as failed" { - run .build/Host/cucumber_cpp/acceptance_test/Debug/cucumber_cpp.acceptance_test run --feature cucumber_cpp/acceptance_test/features --tag "@fail_feature" --report console + run $acceptance_test --format summary pretty message junit --tags "@fail_feature" -- cucumber_cpp/acceptance_test/features assert_failure - assert_output --partial "tests : 1/2 passed" + assert_output --partial "2 scenarios" + assert_output --partial "1 passed" } @test "Test failing hook before results in error" { - run .build/Host/cucumber_cpp/acceptance_test/Debug/cucumber_cpp.acceptance_test run --feature cucumber_cpp/acceptance_test/features --tag "@fail_scenariohook_before" --report console + run $acceptance_test --format summary --format-options "{ \"summary\": {\"theme\":\"plain\"} }" --tags "@fail_scenariohook_before" -- cucumber_cpp/acceptance_test/features assert_failure - assert_output --partial "skipped Given a given step" - assert_output --partial "tests : 0/1 passed" + assert_output --partial "✘ Before" } @test "Test failing hook after results in error" { - run .build/Host/cucumber_cpp/acceptance_test/Debug/cucumber_cpp.acceptance_test run --feature cucumber_cpp/acceptance_test/features --tag "@fail_scenariohook_after" --report console + run $acceptance_test --format summary --format-options "{ \"summary\": {\"theme\":\"plain\"} }" --tags "@fail_scenariohook_after" -- cucumber_cpp/acceptance_test/features assert_failure - assert_output --partial "Given a given step" - assert_output --partial "done" - assert_output --partial "failed" - assert_output --partial "tests : 0/1 passed" + assert_output --partial "✘ After" } @test "Test throwing hook results in error" { - run .build/Host/cucumber_cpp/acceptance_test/Debug/cucumber_cpp.acceptance_test run --feature cucumber_cpp/acceptance_test/features --tag "@throw_scenariohook" --report console + run $acceptance_test --format summary --format-options "{ \"summary\": {\"theme\":\"plain\"} }" --tags "@throw_scenariohook" -- cucumber_cpp/acceptance_test/features assert_failure - assert_output --partial "skipped Given a given step" - assert_output --partial "tests : 0/1 passed" + assert_output --partial "✘ Before" } - @test "Test error program hook results in error and skipped steps" { - run .build/Host/cucumber_cpp/acceptance_test/Debug/cucumber_cpp.acceptance_test.custom run --feature cucumber_cpp/acceptance_test/features --tag "@smoke and @result:OK" --report console --required --failprogramhook + run $acceptance_test.custom --format summary pretty message junit --tags "@smoke and @result:OK" --required --failprogramhook cucumber_cpp/acceptance_test/features assert_failure - refute_output --partial "skipped Given a given step" - refute_output --partial "should not be executed" - assert_output --partial "tests : 0/0 passed" + assert_output --partial "HOOK_BEFORE_ALL" + assert_output --partial "HOOK_AFTER_ALL" + assert_output --partial "0 scenarios" + assert_output --partial "0 steps" } @test "Test unicode" { - run .build/Host/cucumber_cpp/acceptance_test/Debug/cucumber_cpp.acceptance_test run --feature cucumber_cpp/acceptance_test/features --tag "@unicode" --report console + run $acceptance_test --format summary pretty message junit --tags "@unicode" -- cucumber_cpp/acceptance_test/features assert_success - assert_output --partial "tests : 1/1 passed" + assert_output --partial "1 scenario" + assert_output --partial "1 passed" } -@test "Test unused step reporting" { - run .build/Host/cucumber_cpp/acceptance_test/Debug/cucumber_cpp.acceptance_test run --feature cucumber_cpp/acceptance_test/features --tag "@unused_steps" --report console --unused - assert_success - assert_output --regexp ".*The following steps have not been used:.*this step is not being used.*" - refute_output --regexp ".*The following steps have not been used:.*this step is being used.*" -} +# @test "Test unused step reporting" { +# run $acceptance_test --format summary pretty message junit --tags "@unused_steps" --report cucumber_cpp/acceptance_test/features +# assert_success +# assert_output --regexp ".*The following steps have not been used:.*this step is not being used.*" +# refute_output --regexp ".*The following steps have not been used:.*this step is being used.*" +# } @test "Test unused steps by default not reported" { - run .build/Host/cucumber_cpp/acceptance_test/Debug/cucumber_cpp.acceptance_test run --feature cucumber_cpp/acceptance_test/features --tag "@unused_steps" --report console + run $acceptance_test --format summary pretty message junit --tags "@unused_steps" -- cucumber_cpp/acceptance_test/features assert_success refute_output --partial "The following steps have not been used:" } + +@test "Test nested steps" { + run $acceptance_test --format summary pretty --tags "@nested_steps" -- cucumber_cpp/acceptance_test/features + assert_success +} + +@test "Test usage formatter" { + run $acceptance_test.unused --format usage --tags "@unused" -- cucumber_cpp/acceptance_test/features + assert_success + assert_output --partial "│ this step is used │ " + assert_output --partial "│ this step is used │ " + assert_output --partial "│ This step is unused │ UNUSED │" + + run $acceptance_test.unused --format usage --tags "@unused" --dry-run -- cucumber_cpp/acceptance_test/features + assert_success + assert_output --partial "│ this step is used │ - │" + assert_output --partial "│ this step is used │ - │" + assert_output --partial "│ This step is unused │ UNUSED │" + + run $acceptance_test.unused --format usage --tags "@unused" --dry-run --format-options "{ \"usage\" : {\"theme\": \"plain\"} }" -- cucumber_cpp/acceptance_test/features + assert_success + assert_output --partial "| this step is used | - |" + assert_output --partial "| this step is used | - |" + assert_output --partial "| This step is unused | UNUSED |" +} diff --git a/cucumber_cpp/example/features/2simple.feature b/cucumber_cpp/example/features/2simple.feature index 10edc3d6..8f5400d2 100644 --- a/cucumber_cpp/example/features/2simple.feature +++ b/cucumber_cpp/example/features/2simple.feature @@ -13,6 +13,13 @@ Feature: Simple feature file When I eat cucumbers Then I should have cucumbers + @ex:1 + Examples: + | x | y | z | + | 10 | 5 | 5 | + | 11 | 3 | 8 | + + @ex:2 Examples: | x | y | z | | 10 | 5 | 5 | @@ -32,6 +39,10 @@ Feature: Simple feature file | 10 | 4 | 5 | | 11 | 3 | 6 | + @result:FAILED + Scenario: Assert and Expect + Given expect and assert + @result:UNDEFINED Scenario: a scenario with a missing step Given there are cucumbers diff --git a/cucumber_cpp/example/features/3simple.feature b/cucumber_cpp/example/features/3simple.feature new file mode 100644 index 00000000..25b623ea --- /dev/null +++ b/cucumber_cpp/example/features/3simple.feature @@ -0,0 +1,86 @@ +@smoke +Feature: Simple feature file + This is a Simple feature file + + Background: + Given a background step + + Rule: Scenarios that have a rule applied + + @result:OK @printme + Scenario Outline: Can substract + Given there are cucumbers + | foo | bar | asdasd | ad | + | boz | boo | asd | asdasd | + When I eat cucumbers + Then I should have cucumbers + + Examples: + | x | y | z | + | 10 | 5 | 5 | + | 11 | 3 | 8 | + + @result:FAILED + Scenario Outline: Is a dingus + Given there are cucumbers + When I eat cucumbers + Then I should have cucumbers + And this step should be skipped + | foo | bar | + | boz | boo | + + Examples: + | x | y | z | + | 10 | 4 | 5 | + | 11 | 3 | 6 | + + @result:UNDEFINED + Scenario: a scenario with a missing step + Given there are cucumbers + When I eat cucumbers + Then I should have cucumbers left + And this step should be skipped + Examples: + | x | y | z | + | 10 | 5 | 5 | + | 11 | 3 | 8 | + + Rule: Scenarios that have another rule applied + + @result:OK @printme + Scenario Outline: Can substract + Given there are cucumbers + | foo | bar | asdasd | ad | + | boz | boo | asd | asdasd | + When I eat cucumbers + Then I should have cucumbers + + Examples: + | x | y | z | + | 10 | 5 | 5 | + | 11 | 3 | 8 | + + @result:FAILED + Scenario Outline: Is a dingus + Given there are cucumbers + When I eat cucumbers + Then I should have cucumbers + And this step should be skipped + | foo | bar | + | boz | boo | + + Examples: + | x | y | z | + | 10 | 4 | 5 | + | 11 | 3 | 6 | + + @result:UNDEFINED + Scenario: a scenario with a missing step + Given there are cucumbers + When I eat cucumbers + Then I should have cucumbers left + And this step should be skipped + Examples: + | x | y | z | + | 10 | 5 | 5 | + | 11 | 3 | 8 | diff --git a/cucumber_cpp/example/features/debug.feature b/cucumber_cpp/example/features/debug.feature index ba4cf7b1..1c4981f3 100644 --- a/cucumber_cpp/example/features/debug.feature +++ b/cucumber_cpp/example/features/debug.feature @@ -1,42 +1,42 @@ @debug -Feature: Simple feature file +Feature: Debug simple feature file - Background: feature background + Background: background Given a feature background 1 Given a feature background 2 - Scenario: feature scenario 1 + Scenario: scenario 1 Given a step - Scenario: feature scenario 2 + Scenario: scenario 2 Given a step @rule1 - Rule: feature rule 1 + Rule: rule 1 - Background: feature rule 1 background + Background: background Given a feature rule 1 background 1 Given a feature rule 1 background 2 @rule1scenario1 - Scenario: feature rule 1 scenario 1 + Scenario: scenario 1 Given a step @rule1scenario2 - Scenario: feature rule 1 scenario 2 + Scenario: scenario 2 Given a step @rule2 - Rule: feature rule 2 + Rule: rule 2 - Background: feature rule 2 background + Background: background Given a feature rule 2 background 1 Given a feature rule 2 background 2 @rule2scenario1 - Scenario: feature rule 2 scenario 1 + Scenario: scenario 1 Given a step @rule2scenario2 - Scenario: feature rule 2 scenario 2 + Scenario: scenario 2 Given a step diff --git a/cucumber_cpp/example/features/rule.feature b/cucumber_cpp/example/features/rule.feature new file mode 100644 index 00000000..50d4ff2a --- /dev/null +++ b/cucumber_cpp/example/features/rule.feature @@ -0,0 +1,16 @@ +@thishasarule +Feature: Feature with a rule + In order to group related scenarios and rules + As a Cucumber user + I want a clear, human-readable feature description + + Scenario: Scenario without a rule + Given a step + + Rule: Example rule + In order to demonstrate rules in Cucumber + As a developer + I want to see how rules work in feature files + + Scenario: Scenario under a rule + Given a step diff --git a/cucumber_cpp/example/features/substep.feature b/cucumber_cpp/example/features/substep.feature new file mode 100644 index 00000000..5a604b91 --- /dev/null +++ b/cucumber_cpp/example/features/substep.feature @@ -0,0 +1,6 @@ +@substep +Feature: Feature with a step that will call another step + + Scenario: Scenario under a rule + When a step calls another step + Then the called step is executed diff --git a/cucumber_cpp/example/features/table.feature b/cucumber_cpp/example/features/table.feature new file mode 100644 index 00000000..f0c30671 --- /dev/null +++ b/cucumber_cpp/example/features/table.feature @@ -0,0 +1,25 @@ +@table_argument +Feature: Steps can access step table arguments + + Scenario Outline: Step with table argument can access table data + """ + you can have a docstring + """ + When a step stores the value at row and column from the table: + | name | age | city | + | Alice | 30 | New York | + | Bob | 25 | Los Angeles | + | Charlie | 35 | Chicago | + Then the value should be "" + + Examples: + | row | column | expected_value | + | 1 | 0 | Alice | + | 1 | 1 | 30 | + | 1 | 2 | New York | + | 2 | 0 | Bob | + | 2 | 1 | 25 | + | 2 | 2 | Los Angeles | + | 3 | 0 | Charlie | + | 3 | 1 | 35 | + | 3 | 2 | Chicago | diff --git a/cucumber_cpp/example/hooks/Hooks.cpp b/cucumber_cpp/example/hooks/Hooks.cpp index d4e631f1..a11c4856 100644 --- a/cucumber_cpp/example/hooks/Hooks.cpp +++ b/cucumber_cpp/example/hooks/Hooks.cpp @@ -12,7 +12,42 @@ HOOK_BEFORE_ALL() context.Emplace(); } -HOOK_BEFORE_SCENARIO("@dingus") +HOOK_BEFORE_ALL(.name = "Initialize something") { - std::cout << "running only for dingus tests\n"; + /* no body, example only */ +} + +HOOK_BEFORE_SCENARIO(.name = "explicit name only") +{ + /* no body, example only */ +} + +HOOK_BEFORE_SCENARIO("@dingus", "name", 10) +{ + /* no body, example only */ +} + +HOOK_BEFORE_SCENARIO("@result:OK") +{ + /* no body, example only */ +} + +HOOK_BEFORE_SCENARIO() +{ + /* no body, example only */ +} + +HOOK_AFTER_SCENARIO() +{ + /* no body, example only */ +} + +HOOK_BEFORE_STEP() +{ + /* no body, example only */ +} + +HOOK_AFTER_STEP() +{ + /* no body, example only */ } diff --git a/cucumber_cpp/example/steps/Steps.cpp b/cucumber_cpp/example/steps/Steps.cpp index 9210568e..87ac0d88 100644 --- a/cucumber_cpp/example/steps/Steps.cpp +++ b/cucumber_cpp/example/steps/Steps.cpp @@ -1,49 +1,73 @@ #include "cucumber_cpp/library/Steps.hpp" +#include "cucumber_cpp/CucumberCpp.hpp" #include "cucumber_cpp/library/Context.hpp" #include "gmock/gmock.h" #include "gtest/gtest.h" #include -#include #include +struct LoadMeOnConstruction +{ + void Tadaa() const + {} +}; + +struct CustomFixture : cucumber_cpp::library::engine::StepBase +{ + using StepBase::StepBase; + + const LoadMeOnConstruction& alwaysAvailable{ context.Get() }; +}; + +GIVEN_F(CustomFixture, R"(a custom fixture background step)") +{ + alwaysAvailable.Tadaa(); +} + GIVEN(R"(a background step)") { - std::cout << "\nthis is a background step"; + /* no body, example only */ } GIVEN(R"(a simple data table)") { - std::cout << "row0.col0: " << table[0][0].As() << "\n"; - std::cout << "row0.col1: " << table[0][1].As() << "\n"; + [[maybe_unused]] const auto row0col0 = dataTable->rows[0].cells[0].value; + [[maybe_unused]] const auto row0col1 = dataTable->rows[0].cells[1].value; - std::cout << "row1.col0: " << table[1][0].As() << "\n"; - std::cout << "row1.col1: " << table[1][1].As() << "\n"; + [[maybe_unused]] const auto row1col0 = dataTable->rows[1].cells[0].value; + [[maybe_unused]] const auto row1col1 = dataTable->rows[1].cells[1].value; } -GIVEN(R"(there are {int} cucumbers)", (std::uint32_t num)) +GIVEN(R"(there are {int} cucumbers)", (std::int32_t num)) { context.InsertAt("cucumbers_before", num); } -STEP(R"(I eat {int} cucumbers)", (std::uint32_t num)) +STEP(R"(I eat {int} cucumbers)", (std::int32_t num)) { context.InsertAt("cucumbers_eaten", num); } -THEN(R"(^I should have ([0-9]+) cucumbers$)", (std::uint32_t num)) +STEP("expect and assert") +{ + EXPECT_THAT(false, testing::Eq(true)); + ASSERT_THAT(true, testing::Eq(false)); +} + +THEN(R"(I should have {int} cucumbers)", (std::int32_t num)) { - const auto& before = context.Get("cucumbers_before"); - const auto& eaten = context.Get("cucumbers_eaten"); + const auto& before = context.Get("cucumbers_before"); + const auto& eaten = context.Get("cucumbers_eaten"); const auto actual = before - eaten; ASSERT_THAT(actual, testing::Eq(num)); } -THEN(R"(I should have ([0-9]+) cucumbers left)", (std::uint32_t num)) +THEN(R"(I should have {int} cucumbers left)", (std::int32_t num)) { - const auto& before = context.Get("cucumbers_before"); - const auto& eaten = context.Get("cucumbers_eaten"); + const auto& before = context.Get("cucumbers_before"); + const auto& eaten = context.Get("cucumbers_eaten"); const auto actual = before - eaten; @@ -69,3 +93,36 @@ STEP(R"(a data table with comments and newlines inside)") { /* no body, example only */ } + +STEP(R"(^a step$)") +{ + context.EmplaceAt("substep", "was executed"); +} + +STEP(R"(^a step calls another step$)") +{ + Step(R"(a step)"); +} + +STEP(R"(^the called step is executed$)") +{ + ASSERT_THAT(context.Contains("substep"), testing::IsTrue()); + ASSERT_THAT(context.Get("substep"), testing::Eq("was executed")); +} + +STEP("this step should be skipped") +{ + /* no body, example only */ +} + +STEP(R"(a step stores the value at row {int} and column {int} from the table:)", (std::int32_t row, std::int32_t column)) +{ + context.EmplaceAt("cell", dataTable->rows[row].cells[column].value); +} + +STEP(R"(the value should be {string})", (const std::string& expected_value)) +{ + const auto& actual = context.Get("cell"); + + ASSERT_THAT(actual, testing::StrEq(expected_value)); +} diff --git a/cucumber_cpp/library/Application.cpp b/cucumber_cpp/library/Application.cpp index 934700eb..b86bb37b 100644 --- a/cucumber_cpp/library/Application.cpp +++ b/cucumber_cpp/library/Application.cpp @@ -1,139 +1,135 @@ #include "cucumber_cpp/library/Application.hpp" #include "cucumber_cpp/library/Context.hpp" #include "cucumber_cpp/library/Errors.hpp" -#include "cucumber_cpp/library/StepRegistry.hpp" +#include "cucumber_cpp/library/api/Formatters.hpp" +#include "cucumber_cpp/library/api/RunCucumber.hpp" #include "cucumber_cpp/library/cucumber_expression/Errors.hpp" -#include "cucumber_cpp/library/cucumber_expression/Matcher.hpp" #include "cucumber_cpp/library/cucumber_expression/ParameterRegistry.hpp" -#include "cucumber_cpp/library/engine/ContextManager.hpp" -#include "cucumber_cpp/library/engine/FeatureFactory.hpp" -#include "cucumber_cpp/library/engine/FeatureInfo.hpp" -#include "cucumber_cpp/library/engine/HookExecutor.hpp" -#include "cucumber_cpp/library/engine/Result.hpp" -#include "cucumber_cpp/library/engine/TestExecution.hpp" -#include "cucumber_cpp/library/engine/TestRunner.hpp" -#include "cucumber_cpp/library/report/JunitReport.hpp" -#include "cucumber_cpp/library/report/Report.hpp" -#include "cucumber_cpp/library/report/StdOutReport.hpp" +#include "cucumber_cpp/library/support/Types.hpp" +#include "cucumber_cpp/library/tag_expression/Parser.hpp" +#include "fmt/base.h" +#include "fmt/format.h" +#include "fmt/ranges.h" +#include #include #include #include #include #include #include +#include #include #include +#include #include +#include #include -#include +#include #include -#include -#include -#include #include +#include #include -#include -#include #include -#include -#include namespace cucumber_cpp::library { namespace { - template - std::string Join(const Range& range, std::string_view delim) - { - if (range.empty()) - return ""; - - return std::accumulate(std::next(range.begin()), range.end(), range.front(), [&delim](const auto& lhs, const auto& rhs) - { - return std::format("{}{}{}", lhs, delim, rhs); - }); - } - - std::filesystem::path ToFileSystemPath(const std::string_view& sv) + bool IsFeatureFile(const std::filesystem::directory_entry& entry) { - return { sv }; + return std::filesystem::is_regular_file(entry) && entry.path().has_extension() && entry.path().extension() == ".feature"; } - bool IsFeatureFile(const std::filesystem::directory_entry& entry) + void CollectFilesFromDirectory(std::set>& foundFiles, std::filesystem::path folder, Application::Options& options) { - return std::filesystem::is_regular_file(entry) && entry.path().has_extension() && entry.path().extension() == ".feature"; + if (options.recursive) + for (const auto& entry : std::filesystem::recursive_directory_iterator{ folder } | std::views::filter(IsFeatureFile)) + foundFiles.emplace(entry.path()); + else + for (const auto& entry : std::filesystem::directory_iterator{ folder } | std::views::filter(IsFeatureFile)) + foundFiles.emplace(entry.path()); } - std::vector GetFeatureFiles(Application::Options& options) + std::set> GetFeatureFiles(Application::Options& options) { - std::vector files; + std::set> foundFiles; - for (const auto feature : options.features | std::views::transform(ToFileSystemPath)) + for (const auto feature : options.paths) if (std::filesystem::is_directory(feature)) - for (const auto& entry : std::filesystem::directory_iterator{ feature } | std::views::filter(IsFeatureFile)) - files.emplace_back(entry.path()); - else - files.emplace_back(feature); + CollectFilesFromDirectory(foundFiles, feature, options); + else if (IsFeatureFile(std::filesystem::directory_entry{ feature })) + foundFiles.emplace(feature); - return files; + return foundFiles; } } - ReportHandlerValidator::ReportHandlerValidator(const report::Reporters& reporters) - : CLI::Validator("ReportHandler", [&reporters, cachedAvailableReporters = std::optional>{}](const std::string& str) mutable - { - if (!cachedAvailableReporters) - cachedAvailableReporters = reporters.AvailableReporters(); - - if (std::ranges::find(*cachedAvailableReporters, str) == cachedAvailableReporters->end()) - return std::string{ "'" + str + "' is not a reporter" }; - else - return std::string{}; - }) - {} - Application::Application(std::shared_ptr contextStorageFactory, bool removeDefaultGoogleTestListener) - : contextManager{ std::move(contextStorageFactory) } - , reporters{ contextManager } - , reportHandlerValidator{ reporters } + : contextStorageFactory{ contextStorageFactory } , removeDefaultGoogleTestListener{ removeDefaultGoogleTestListener } + { + cli.set_config("--config", "cucumber.toml"); + } + int Application::Run(int argc, const char* const* argv) { - gherkin.include_source(false); - gherkin.include_ast(true); - gherkin.include_pickles(true); + const auto formattersSet = formatters.GetAvailableFormatterNames(); + + const auto formatterDescription = fmt::format("{{{}}}", fmt::join(formattersSet | std::views::transform([](const auto& pair) + { + if (pair.second) + return fmt::format("{}<<:output>>", pair.first); + else + return pair.first; + }), + ",")); + + CLI::Validator formatValidator{ [&formattersSet](const std::string& str) -> std::string + { + const api::FormatterOption option{ str }; + const auto iter = std::ranges::find(formattersSet, option.name, &std::pair::first); - cli.require_subcommand(1); + if (iter == formattersSet.end()) + return fmt::format("'{}' is not a valid formatter", option.name); + else + return ""; + }, + formatterDescription }; - runCommand = cli.add_subcommand("run")->parse_complete_callback([this] - { - RunFeatures(); - }); + try + { + const std::map> orderingMap{ + { "defined", support::RunOptions::Ordering::defined }, + { "reverse", support::RunOptions::Ordering::reverse }, + }; - runCommand->add_option("-t,--tag", options.tags, "Cucumber tag expression"); - runCommand->add_option("-f,--feature", options.features, "Feature file or folder with feature files")->required()->check(CLI::ExistingPath); + cli.add_flag("--dump-config", options.dumpConfig, "Dump the configuration"); - runCommand->add_option("-r,--report", options.reporters, "Name of the report generator: ")->required()->group("report generation")->check(reportHandlerValidator); - runCommand->add_option("--outputfolder", options.outputfolder, "Specifies the output folder for generated report files")->group("report generation"); - runCommand->add_option("--reportfile", options.reportfile, "Specifies the output name for generated report files")->group("report generation"); - runCommand->add_flag("--dry", options.dryrun, "Generate report without running tests"); - runCommand->add_flag("--unused", options.printStepsNotUsed, "Show step definitions that were not used"); + cli.add_flag("-d,--dry-run", options.dryRun, "Perform a dry run without executing steps")->default_val(options.dryRun); + cli.add_flag("--fail-fast", options.failFast, "Stop execution on first failure")->default_val(options.failFast); + cli.add_option("--format", options.format, "specify the output format, optionally supply PATH to redirect formatter output.")->check(formatValidator)->default_val(options.format); + cli.add_option("--format-options", options.formatOptions, "provide options for formatters")->default_val(options.formatOptions); + cli.add_option("--language", options.language, "Default language for feature files, eg 'en'")->default_str(options.language); + cli.add_option("--order", options.ordering, "Run scenarios in specificed order")->transform(CLI::CheckedTransformer(orderingMap, CLI::ignore_case))->default_val(options.ordering); + auto* retryOpt = cli.add_option("--retry", options.retry, "Number of times to retry failed scenarios")->default_val(options.retry); + cli.add_option("--retry-tag-filter", options.retryTagFilter, "Only retry scenarios matching this tag expression")->needs(retryOpt); + cli.add_flag("--strict,!--no-strict", options.strict, "Fail if there are pending steps")->default_val(options.strict); + cli.add_flag("--recursive,!--no-recursive", options.recursive, "Search for feature files recursively")->default_val(options.recursive); - reporters.Add("console", std::make_unique()); - reporters.Add("junit-xml", std::make_unique(options.outputfolder, options.reportfile)); + CLI::deprecate_option(cli.add_option("--tag", options.tags, "Cucumber tag expression"), "-t,--tags"); + cli.add_option("-t,--tags", options.tags, "Cucumber tag expression"); - ProgramContext().InsertRef(options); - } + CLI::deprecate_option(cli.add_option("-f,--feature", options.paths, "Paths to where your feature files are"), "paths"); + cli.add_option("paths", options.paths, "Paths to where your feature files are, defaults to \"./features\"")->default_val(options.paths); - int Application::Run(int argc, const char* const* argv) - { - try - { - const auto reportDescription = runCommand->get_option("--report")->get_description(); - const auto joinedReporters = reportDescription + Join(reporters.AvailableReporters(), ", "); + ProgramContext().InsertRef(options); - runCommand->get_option("--report")->description(joinedReporters); cli.parse(argc, argv); + + if (options.dumpConfig) + std::ofstream{ "cucumber.toml" } << cli.config_to_str(true, true); + + return RunFeatures(); } catch (const CLI::ParseError& e) { @@ -141,127 +137,66 @@ namespace cucumber_cpp::library } catch (const InternalError& error) { - std::cout << "Internal error:\n\n"; - std::cout << error.what() << std::endl; - return GetExitCode(engine::Result::failed); - } - catch (const UnsupportedAsteriskError& error) - { - std::cout << "UnsupportedAsteriskError error:\n\n"; - std::cout << error.what() << std::endl; - return GetExitCode(engine::Result::failed); + std::cout << fmt::format("InternalError error:\n{}\n", error.what()); + return EXIT_FAILURE; } catch (const cucumber_expression::Error& error) { - std::cout << "Cucumber Expression error:\n\n"; - std::cout << error.what() << std::endl; - return GetExitCode(engine::Result::failed); + std::cout << fmt::format("Cucumber Expression error:\n{}\n", error.what()); + return EXIT_FAILURE; } catch (const std::exception& error) { - std::cout << "Generic error:\n\n"; - std::cout << error.what() << std::endl; - return GetExitCode(engine::Result::failed); + std::cout << fmt::format("Generic error:\n{}\n", error.what()); + return EXIT_FAILURE; } catch (...) { std::cout << "Unknown error"; - return GetExitCode(engine::Result::failed); + return EXIT_FAILURE; } - - return GetExitCode(); } CLI::App& Application::CliParser() { - return *runCommand; + return cli; } Context& Application::ProgramContext() { - return contextManager.ProgramContext(); + return programContextRef; } - cucumber_expression::ParameterRegistration& Application::ParameterRegistration() + cucumber_expression::ParameterRegistry& Application::ParameterRegistration() { return parameterRegistry; } - void Application::AddReportHandler(const std::string& name, std::unique_ptr&& reporter) - { - reporters.Add(name, std::move(reporter)); - } - - void Application::RunFeatures() + api::Formatters& Application::Formatters() { - for (const auto& selectedReporter : options.reporters) - reporters.Use(selectedReporter); - - auto tagExpression = Join(options.tags, " "); - engine::HookExecutorImpl hookExecution{ contextManager }; - - const auto& runPolicy = (options.dryrun) ? static_cast(engine::dryRunPolicy) - : static_cast(engine::executeRunPolicy); - - std::unique_ptr testExecution; - if (removeDefaultGoogleTestListener) - testExecution = std::make_unique(contextManager, reporters, hookExecution, runPolicy); - else - testExecution = std::make_unique(contextManager, reporters, hookExecution, runPolicy); - - StepRegistry stepRegistry{ parameterRegistry }; - engine::FeatureTreeFactory featureTreeFactory{ stepRegistry }; - - engine::TestRunnerImpl testRunner{ featureTreeFactory, *testExecution }; - - testRunner.Run(GetFeatureTree(featureTreeFactory, tagExpression)); - - if (options.printStepsNotUsed) - PrintStepsNotUsed(stepRegistry); - - std::cout << '\n' - << std::flush; + return formatters; } - void Application::PrintStepsNotUsed(const StepRegistry& stepRegistry) const + int Application::RunFeatures() { - auto isUnused = [](const StepRegistry::EntryView& entry) - { - return entry.used == 0; + fmt::println("Running with tags: {}", options.tags); + + const auto runOptions = support::RunOptions{ + .sources = { + .paths = GetFeatureFiles(options), + .tagExpression = tag_expression::Parse(fmt::to_string(fmt::join(options.tags, " "))), + .ordering = options.ordering, + }, + .runtime = { + .dryRun = options.dryRun, + .failFast = options.failFast, + .retry = options.retry, + .strict = options.strict, + .retryTagExpression = tag_expression::Parse(fmt::to_string(fmt::join(options.retryTagFilter, " "))), + }, }; - auto unusedSteps = stepRegistry.List() | std::views::filter(isUnused); - - if (std::ranges::empty(unusedSteps)) - std::cout << "\nAll steps have been used."; - else - { - std::cout << "\nThe following steps have not been used:"; - for (const auto& entry : unusedSteps) - std::cout << "\n - " << std::visit(cucumber_expression::SourceVisitor{}, entry.stepRegex); - } - } - - std::vector> Application::GetFeatureTree(const engine::FeatureTreeFactory& featureTreeFactory, std::string_view tagExpression) - { - - const auto featureFiles = GetFeatureFiles(options); - std::vector> vec; - vec.reserve(featureFiles.size()); - - for (const auto& featurePath : featureFiles) - vec.push_back(featureTreeFactory.Create(featurePath, tagExpression)); - - return vec; - } - - int Application::GetExitCode() const - { - return GetExitCode(contextManager.ProgramContext().ExecutionStatus()); - } - - int Application::GetExitCode(engine::Result result) const - { - return static_cast>(result) - static_cast>(engine::Result::passed); + const auto runPassed = api::RunCucumber(runOptions, parameterRegistry, *programContext, broadcaster, formatters, options.format, options.formatOptions); + return runPassed ? EXIT_SUCCESS : EXIT_FAILURE; } } diff --git a/cucumber_cpp/library/Application.hpp b/cucumber_cpp/library/Application.hpp index 989baea1..eba0ca84 100644 --- a/cucumber_cpp/library/Application.hpp +++ b/cucumber_cpp/library/Application.hpp @@ -4,79 +4,86 @@ // IWYU pragma: private, include "cucumber_cpp/CucumberCpp.hpp" // IWYU pragma: friend cucumber_cpp/.* -#include "cucumber/gherkin/app.hpp" #include "cucumber_cpp/library/Context.hpp" -#include "cucumber_cpp/library/StepRegistry.hpp" +#include "cucumber_cpp/library/api/Formatters.hpp" #include "cucumber_cpp/library/cucumber_expression/ParameterRegistry.hpp" -#include "cucumber_cpp/library/engine/ContextManager.hpp" -#include "cucumber_cpp/library/engine/FeatureFactory.hpp" -#include "cucumber_cpp/library/engine/FeatureInfo.hpp" -#include "cucumber_cpp/library/engine/Result.hpp" -#include "cucumber_cpp/library/report/Report.hpp" +#include "cucumber_cpp/library/support/SupportCodeLibrary.hpp" +#include "cucumber_cpp/library/support/Types.hpp" +#include "cucumber_cpp/library/util/Broadcaster.hpp" +#include "cucumber_cpp/library/util/Duration.hpp" +#include "cucumber_cpp/library/util/Timestamp.hpp" #include #include #include +#include #include +#include #include +#include #include -#include #include namespace cucumber_cpp::library { - struct ReportHandlerValidator : public CLI::Validator - { - explicit ReportHandlerValidator(const report::Reporters& reporters); - }; - struct Application { struct Options { - std::vector tags{}; - std::vector features{}; - std::vector reporters{}; + bool dumpConfig{ false }; + + std::set> paths{ { (std::filesystem::path(".") / "features").string() } }; + + bool dryRun{ false }; + bool failFast{ false }; + + std::set> format{ "summary" }; + std::string formatOptions{ R"({})" }; + + std::string language{ "en" }; - std::string outputfolder{ "./out" }; - std::string reportfile{ "TestReport" }; + enum support::RunOptions::Ordering ordering{ support::RunOptions::Ordering::defined }; - bool dryrun{ false }; - bool printStepsNotUsed{ false }; + std::size_t retry{ 0 }; + std::vector retryTagFilter{}; + + bool strict{ true }; + + bool recursive{ true }; + + std::vector tags{}; }; - explicit Application(std::shared_ptr contextStorageFactory = std::make_shared(), bool removeDefaultGoogleTestListener = true); + Application(std::shared_ptr contextStorageFactory = std::make_shared(), bool removeDefaultGoogleTestListener = true); - int Run(int argc, const char* const* argv); + [[nodiscard]] int Run(int argc, const char* const* argv); CLI::App& CliParser(); Context& ProgramContext(); - cucumber_expression::ParameterRegistration& ParameterRegistration(); - - void AddReportHandler(const std::string& name, std::unique_ptr&& reporter); + cucumber_expression::ParameterRegistry& ParameterRegistration(); + api::Formatters& Formatters(); private: void DryRunFeatures(); - void RunFeatures(); - [[nodiscard]] engine::Result RunFeature(const std::filesystem::path& path, std::string_view tagExpression, report::ReportHandlerV2& reportHandler); - void PrintStepsNotUsed(const StepRegistry& stepRegistry) const; - [[nodiscard]] std::vector> GetFeatureTree(const engine::FeatureTreeFactory& featureTreeFactory, std::string_view tagExpression); - - [[nodiscard]] int GetExitCode() const; - [[nodiscard]] int GetExitCode(engine::Result result) const; + [[nodiscard]] int RunFeatures(); Options options; + CLI::App cli; - CLI::App* runCommand; - engine::ContextManager contextManager; + std::shared_ptr contextStorageFactory; + std::unique_ptr programContext{ std::make_unique(contextStorageFactory) }; + Context& programContextRef{ *programContext }; - report::ReportForwarderImpl reporters; - ReportHandlerValidator reportHandlerValidator; + api::Formatters formatters; - cucumber::gherkin::app gherkin; + util::Broadcaster broadcaster; - cucumber_expression::ParameterRegistry parameterRegistry; + cucumber_expression::ParameterRegistry parameterRegistry{ cucumber_cpp::library::support::DefinitionRegistration::Instance().GetRegisteredParameters() }; bool removeDefaultGoogleTestListener; + util::StopWatchHighResolutionClock stopwatchHighResolutionClock; + util::TimestampGeneratorSystemClock timestampGeneratorSystemClock; + + bool runPassed{ false }; }; } diff --git a/cucumber_cpp/library/Body.hpp b/cucumber_cpp/library/Body.hpp deleted file mode 100644 index 49c0892a..00000000 --- a/cucumber_cpp/library/Body.hpp +++ /dev/null @@ -1,47 +0,0 @@ -#ifndef CUCUMBER_CPP_BODY_HPP -#define CUCUMBER_CPP_BODY_HPP - -#include -#include -#include -#include - -namespace cucumber_cpp::library -{ - struct Body - { - virtual ~Body() = default; - - virtual void Execute(const std::variant, std::vector>& args = {}) = 0; - }; - - template - concept HasSetUpTearDown = - requires(T t) { - { t.SetUp() } -> std::convertible_to; - { t.TearDown() } -> std::convertible_to; - }; - - template - struct SetUpTearDownWrapper - { - explicit SetUpTearDownWrapper(T& t) - : t{ t } - { - t.SetUp(); - } - - SetUpTearDownWrapper(const SetUpTearDownWrapper&) = delete; - SetUpTearDownWrapper(SetUpTearDownWrapper&&) = delete; - - ~SetUpTearDownWrapper() - { - t.TearDown(); - } - - private: - T& t; - }; -} - -#endif diff --git a/cucumber_cpp/library/BodyMacro.hpp b/cucumber_cpp/library/BodyMacro.hpp index 12609db3..60c03570 100644 --- a/cucumber_cpp/library/BodyMacro.hpp +++ b/cucumber_cpp/library/BodyMacro.hpp @@ -1,12 +1,19 @@ #ifndef CUCUMBER_CPP_BODYMACRO_HPP #define CUCUMBER_CPP_BODYMACRO_HPP -#include "cucumber_cpp/library/Body.hpp" -#include "cucumber_cpp/library/engine/FailureHandler.hpp" +#include "cucumber/messages/step_match_argument.hpp" +#include "cucumber_cpp/library/cucumber_expression/ParameterRegistry.hpp" #include "cucumber_cpp/library/engine/StringTo.hpp" +#include "cucumber_cpp/library/support/Body.hpp" #include #include +template +T TransformArg(const cucumber::messages::step_match_argument& match) +{ + return cucumber_cpp::library::cucumber_expression::ConverterTypeMap::Instance().at(match.parameter_type_name.value_or(""))(match.group); +} + #define BODY_MATCHER(matcher, ...) matcher #define BODY_ARGS(matcher, args, ...) args @@ -15,50 +22,41 @@ #define BODY_STRUCT CONCAT(BodyImpl, __LINE__) -#define BODY(matcher, type, targs, registration, base) \ - namespace \ - { \ - struct BODY_STRUCT : cucumber_cpp::library::Body \ - , base \ - { \ - /* Workaround namespaces in `base`. For example `base` = Foo::Bar. */ \ - /* Then the result would be Foo::Bar::Foo::Bar which is invalid */ \ - using myBase = base; \ - using myBase::myBase; \ - \ - void Execute(const std::variant, std::vector>& args) override \ - { \ - cucumber_cpp::library::SetUpTearDownWrapper wrapper{ *this }; \ - ASSERT_NO_THROW(ExecuteWithArgs(args, static_cast(nullptr))); \ - } \ - \ - template \ - void ExecuteWithArgs(const std::variant, std::vector>& args, void (* /* unused */)(TArgs...)) \ - { \ - if (std::holds_alternative>(args)) \ - ExecuteWithArgs(std::get>(args), std::make_index_sequence{}); \ - else \ - ExecuteWithArgs(std::get>(args), std::make_index_sequence{}); \ - } \ - \ - template \ - void ExecuteWithArgs(const std::vector& args, std::index_sequence /*unused*/) \ - { \ - ExecuteWithArgs(cucumber_cpp::library::engine::StringTo>(args[I])...); \ - } \ - \ - template \ - void ExecuteWithArgs(const std::vector& args, std::index_sequence /*unused*/) \ - { \ - ExecuteWithArgs(std::any_cast>(args[I])...); \ - } \ - \ - private: \ - void ExecuteWithArgs targs; \ - static const std::size_t ID; \ - }; \ - } \ - const std::size_t BODY_STRUCT::ID = registration(matcher, type); \ +#define BODY(matcher, type, targs, registration, base) \ + namespace \ + { \ + struct BODY_STRUCT : cucumber_cpp::library::support::Body \ + , base \ + { \ + /* Workaround namespaces in `base`. For example `base` = Foo::Bar. */ \ + /* Then the result would be Foo::Bar::Foo::Bar which is invalid */ \ + using myBase = base; \ + using myBase::myBase; \ + \ + void Execute(const cucumber::messages::step_match_arguments_list& args) override \ + { \ + cucumber_cpp::library::support::SetUpTearDownWrapper wrapper{ *this }; \ + ExecuteWithArgs(args, static_cast(nullptr)); \ + } \ + \ + template \ + void ExecuteWithArgs(const cucumber::messages::step_match_arguments_list& args, void (* /* unused */)(TArgs...)) \ + { \ + ExecuteWithArgs(args, std::make_index_sequence{}); \ + } \ + \ + template \ + void ExecuteWithArgs(const cucumber::messages::step_match_arguments_list& args, std::index_sequence /*unused*/) \ + { \ + ExecuteWithArgs(TransformArg>(args.step_match_arguments[I])...); \ + } \ + \ + private: \ + void ExecuteWithArgs targs; \ + static const std::size_t ID; \ + }; \ + } \ + const std::size_t BODY_STRUCT::ID = registration(matcher, type); \ void BODY_STRUCT::ExecuteWithArgs targs #endif diff --git a/cucumber_cpp/library/CMakeLists.txt b/cucumber_cpp/library/CMakeLists.txt index e7ccea79..6e12e776 100644 --- a/cucumber_cpp/library/CMakeLists.txt +++ b/cucumber_cpp/library/CMakeLists.txt @@ -1,27 +1,14 @@ -set(CMAKE_COMPILE_WARNING_AS_ERROR On) - -add_library(cucumber_cpp.library STATIC ${CCR_EXCLUDE_FROM_ALL}) +add_library(cucumber_cpp.library ${CCR_EXCLUDE_FROM_ALL}) target_sources(cucumber_cpp.library PRIVATE Application.cpp Application.hpp - Body.hpp BodyMacro.hpp Context.hpp Errors.hpp - HookRegistry.cpp - HookRegistry.hpp Hooks.hpp - Rtrim.cpp - Rtrim.hpp - StepRegistry.cpp - StepRegistry.hpp + Parameter.hpp Steps.hpp - TagExpression.cpp - TagExpression.hpp - TagsToSet.hpp - TraceTime.cpp - TraceTime.hpp ) target_include_directories(cucumber_cpp.library PUBLIC @@ -32,11 +19,13 @@ target_link_libraries(cucumber_cpp.library PUBLIC GTest::gtest GTest::gmock cucumber_gherkin_lib - cucumber_cpp.library.report + cucumber_cpp.library.api cucumber_cpp.library.engine cucumber_cpp.library.tag_expression cucumber_cpp.library.util - CLI11 + cucumber_cpp.library.formatter + cucumber_cpp.library.support + CLI11::CLI11 ) target_compile_options(cucumber_cpp.library @@ -47,12 +36,17 @@ target_compile_options(cucumber_cpp.library $<$:/Zc:preprocessor> ) +add_subdirectory(api) +add_subdirectory(assemble) add_subdirectory(cucumber_expression) -add_subdirectory(tag_expression) add_subdirectory(engine) -add_subdirectory(report) +add_subdirectory(formatter) +add_subdirectory(query) +add_subdirectory(runtime) +add_subdirectory(support) +add_subdirectory(tag_expression) add_subdirectory(util) if (CCR_BUILD_TESTS) - add_subdirectory(test) + # add_subdirectory(test) endif() diff --git a/cucumber_cpp/library/Errors.hpp b/cucumber_cpp/library/Errors.hpp index 08a66120..57444af1 100644 --- a/cucumber_cpp/library/Errors.hpp +++ b/cucumber_cpp/library/Errors.hpp @@ -9,11 +9,6 @@ namespace cucumber_cpp::library { using runtime_error::runtime_error; }; - - struct UnsupportedAsteriskError : std::runtime_error - { - using runtime_error::runtime_error; - }; } #endif diff --git a/cucumber_cpp/library/HookRegistry.cpp b/cucumber_cpp/library/HookRegistry.cpp deleted file mode 100644 index 529761ff..00000000 --- a/cucumber_cpp/library/HookRegistry.cpp +++ /dev/null @@ -1,65 +0,0 @@ - -#include "cucumber_cpp/library/HookRegistry.hpp" -#include "cucumber_cpp/library/Context.hpp" -#include "cucumber_cpp/library/TagExpression.hpp" -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace cucumber_cpp::library -{ - namespace - { - auto TypeFilter(HookType hookType) - { - return [hookType](const HookRegistry::Entry& entry) - { - return entry.type == hookType; - }; - }; - - auto Matches(const std::set>& tags) - { - return [&tags](const HookRegistryBase::Entry& entry) - { - return entry.tagExpression->Evaluate(tags); - }; - } - } - - HookBase::HookBase(Context& context) - : context{ context } - {} - - std::vector HookRegistryBase::Query(HookType hookType, const std::set>& tags) const - { - std::vector matches; - - for (const Entry& entry : registry | std::views::filter(TypeFilter(hookType)) | std::views::filter(Matches(tags))) - matches.emplace_back(entry.factory); - - return matches; - } - - std::size_t HookRegistryBase::Size() const - { - return registry.size(); - } - - std::size_t HookRegistryBase::Size(HookType hookType) const - { - return std::ranges::count(registry, hookType, &Entry::type); - } - - HookRegistry& HookRegistry::Instance() - { - static HookRegistry instance; - return instance; - } -} diff --git a/cucumber_cpp/library/HookRegistry.hpp b/cucumber_cpp/library/HookRegistry.hpp deleted file mode 100644 index 973e2e28..00000000 --- a/cucumber_cpp/library/HookRegistry.hpp +++ /dev/null @@ -1,126 +0,0 @@ -#ifndef CUCUMBER_CPP_HOOKREGISTRY_HPP -#define CUCUMBER_CPP_HOOKREGISTRY_HPP - -#include "cucumber_cpp/library/Body.hpp" -#include "cucumber_cpp/library/Context.hpp" -#include "cucumber_cpp/library/tag_expression/Model.hpp" -#include "cucumber_cpp/library/tag_expression/Parser.hpp" -#include -#include -#include -#include -#include -#include -#include - -namespace cucumber_cpp::library -{ - enum struct HookType - { - beforeAll, - afterAll, - beforeFeature, - afterFeature, - before, - after, - beforeStep, - afterStep, - }; - - struct HookBase - { - explicit HookBase(Context& context); - - virtual ~HookBase() = default; - - virtual void SetUp() - { - /* nothing to do */ - } - - virtual void TearDown() - { - /* nothing to do */ - } - - protected: - Context& context; - }; - - struct HookMatch - { - explicit HookMatch(std::unique_ptr (&factory)(Context& context)) - : factory(factory) - {} - - std::unique_ptr (&factory)(Context& context); - }; - - struct HookRegistryBase - { - struct Entry - { - Entry(HookType type, std::string_view expression, std::unique_ptr (&factory)(Context& context)) - : type(type) - , tagExpression{ tag_expression::Parse(expression) } - , factory(factory) - {} - - HookType type; - std::unique_ptr tagExpression; - std::unique_ptr (&factory)(Context& context); - }; - - [[nodiscard]] std::vector Query(HookType hookType, const std::set>& tags) const; - - [[nodiscard]] std::size_t Size() const; - [[nodiscard]] std::size_t Size(HookType hookType) const; - - protected: - template - std::size_t Register(const std::string& tagExpression, HookType hookType); - - private: - template - static std::unique_ptr Construct(Context& context); - - std::vector registry; - }; - - struct HookRegistry : HookRegistryBase - { - private: - HookRegistry() = default; - - public: - static HookRegistry& Instance(); - - template - static std::size_t Register(const std::string& tagExpression, HookType hookType); - }; - - ////////////////////////// - // implementation // - ////////////////////////// - - template - std::size_t HookRegistryBase::Register(const std::string& tagExpression, HookType hookType) - { - registry.emplace_back(hookType, tagExpression, Construct); - return registry.size(); - } - - template - std::unique_ptr HookRegistryBase::Construct(Context& context) - { - return std::make_unique(context); - } - - template - std::size_t HookRegistry::Register(const std::string& tagExpression, HookType hookType) - { - return Instance().HookRegistryBase::Register(tagExpression, hookType); - } -} - -#endif diff --git a/cucumber_cpp/library/Hooks.hpp b/cucumber_cpp/library/Hooks.hpp index eaa4098a..d895a1ef 100644 --- a/cucumber_cpp/library/Hooks.hpp +++ b/cucumber_cpp/library/Hooks.hpp @@ -5,48 +5,49 @@ // IWYU pragma: friend cucumber_cpp/.* #include "cucumber_cpp/library/BodyMacro.hpp" -#include "cucumber_cpp/library/HookRegistry.hpp" - -#define HOOK_(matcher, type) BODY(matcher, type, (), cucumber_cpp::library::HookRegistry::Register, cucumber_cpp::library::HookBase) - -#define HOOK_BEFORE_ALL() \ - HOOK_( \ - "", \ - cucumber_cpp::library::HookType::beforeAll) - -#define HOOK_AFTER_ALL() \ - HOOK_( \ - "", \ - cucumber_cpp::library::HookType::afterAll) - -#define HOOK_BEFORE_FEATURE(...) \ - HOOK_( \ - BODY_MATCHER(__VA_ARGS__ __VA_OPT__(, ) ""), \ - cucumber_cpp::library::HookType::beforeFeature) - -#define HOOK_AFTER_FEATURE(...) \ - HOOK_( \ - BODY_MATCHER(__VA_ARGS__ __VA_OPT__(, ) ""), \ - cucumber_cpp::library::HookType::afterFeature) - -#define HOOK_BEFORE_SCENARIO(...) \ - HOOK_( \ - BODY_MATCHER(__VA_ARGS__ __VA_OPT__(, ) ""), \ - cucumber_cpp::library::HookType::before) - -#define HOOK_AFTER_SCENARIO(...) \ - HOOK_( \ - BODY_MATCHER(__VA_ARGS__ __VA_OPT__(, ) ""), \ - cucumber_cpp::library::HookType::after) - -#define HOOK_BEFORE_STEP(...) \ - HOOK_( \ - BODY_MATCHER(__VA_ARGS__ __VA_OPT__(, ) ""), \ - cucumber_cpp::library::HookType::beforeStep) - -#define HOOK_AFTER_STEP(...) \ - HOOK_( \ - BODY_MATCHER(__VA_ARGS__ __VA_OPT__(, ) ""), \ - cucumber_cpp::library::HookType::afterStep) +#include "cucumber_cpp/library/engine/Hook.hpp" +#include "cucumber_cpp/library/support/SupportCodeLibrary.hpp" + +#define HOOK_(matcher, type) BODY(matcher, type, (), cucumber_cpp::library::support::DefinitionRegistration::Register, cucumber_cpp::library::engine::HookBase) + +#define HOOK_BEFORE_ALL(...) \ + HOOK_( \ + (cucumber_cpp::library::support::GlobalHook{ __VA_ARGS__ }), \ + cucumber_cpp::library::support::HookType::beforeAll) + +#define HOOK_AFTER_ALL(...) \ + HOOK_( \ + (cucumber_cpp::library::support::GlobalHook{ __VA_ARGS__ }), \ + cucumber_cpp::library::support::HookType::afterAll) + +#define HOOK_BEFORE_FEATURE(...) \ + HOOK_( \ + (cucumber_cpp::library::support::Hook{ __VA_ARGS__ }), \ + cucumber_cpp::library::support::HookType::beforeFeature) + +#define HOOK_AFTER_FEATURE(...) \ + HOOK_( \ + (cucumber_cpp::library::support::Hook{ __VA_ARGS__ }), \ + cucumber_cpp::library::support::HookType::afterFeature) + +#define HOOK_BEFORE_SCENARIO(...) \ + HOOK_( \ + (cucumber_cpp::library::support::Hook{ __VA_ARGS__ }), \ + cucumber_cpp::library::support::HookType::before) + +#define HOOK_AFTER_SCENARIO(...) \ + HOOK_( \ + (cucumber_cpp::library::support::Hook{ __VA_ARGS__ }), \ + cucumber_cpp::library::support::HookType::after) + +#define HOOK_BEFORE_STEP(...) \ + HOOK_( \ + (cucumber_cpp::library::support::Hook{ __VA_ARGS__ }), \ + cucumber_cpp::library::support::HookType::beforeStep) + +#define HOOK_AFTER_STEP(...) \ + HOOK_( \ + (cucumber_cpp::library::support::Hook{ __VA_ARGS__ }), \ + cucumber_cpp::library::support::HookType::afterStep) #endif diff --git a/cucumber_cpp/library/Parameter.hpp b/cucumber_cpp/library/Parameter.hpp new file mode 100644 index 00000000..f7444cef --- /dev/null +++ b/cucumber_cpp/library/Parameter.hpp @@ -0,0 +1,24 @@ +#ifndef LIBRARY_PARAMETER_HPP +#define LIBRARY_PARAMETER_HPP + +// IWYU pragma: private, include "cucumber_cpp/CucumberCpp.hpp" +// IWYU pragma: friend cucumber_cpp/.* + +#include "cucumber/messages/group.hpp" +#include "cucumber_cpp/library/support/SupportCodeLibrary.hpp" + +#define PARAMETER_STRUCT CONCAT(ParameterImpl, __LINE__) + +#define PARAMETER(Type, ...) \ + namespace \ + { \ + struct PARAMETER_STRUCT \ + { \ + static Type Transform(const cucumber::messages::group& group); \ + static const std::size_t ID; \ + }; \ + } \ + const std::size_t PARAMETER_STRUCT::ID = cucumber_cpp::library::support::DefinitionRegistration::Instance().Register({ __VA_ARGS__ }); \ + Type PARAMETER_STRUCT::Transform(const cucumber::messages::group& group) + +#endif diff --git a/cucumber_cpp/library/Rtrim.cpp b/cucumber_cpp/library/Rtrim.cpp deleted file mode 100644 index 5acd7591..00000000 --- a/cucumber_cpp/library/Rtrim.cpp +++ /dev/null @@ -1,17 +0,0 @@ -#include "cucumber_cpp/library/Rtrim.hpp" -#include -#include -#include - -namespace cucumber_cpp::library -{ - void Rtrim(std::string& s) - { - s.erase(std::find_if(s.rbegin(), s.rend(), [](unsigned char ch) - { - return std::isspace(ch) == 0; - }) - .base(), - s.end()); - } -} diff --git a/cucumber_cpp/library/Rtrim.hpp b/cucumber_cpp/library/Rtrim.hpp deleted file mode 100644 index afd0bbe0..00000000 --- a/cucumber_cpp/library/Rtrim.hpp +++ /dev/null @@ -1,11 +0,0 @@ -#ifndef CUCUMBER_CPP_RTRIM_HPP -#define CUCUMBER_CPP_RTRIM_HPP - -#include - -namespace cucumber_cpp::library -{ - void Rtrim(std::string& s); -} - -#endif diff --git a/cucumber_cpp/library/StepRegistry.cpp b/cucumber_cpp/library/StepRegistry.cpp deleted file mode 100644 index 0e9ef169..00000000 --- a/cucumber_cpp/library/StepRegistry.cpp +++ /dev/null @@ -1,90 +0,0 @@ -#include "cucumber_cpp/library/StepRegistry.hpp" -#include "cucumber_cpp/library/Body.hpp" -#include "cucumber_cpp/library/Context.hpp" -#include "cucumber_cpp/library/cucumber_expression/Expression.hpp" -#include "cucumber_cpp/library/cucumber_expression/Matcher.hpp" -#include "cucumber_cpp/library/cucumber_expression/ParameterRegistry.hpp" -#include "cucumber_cpp/library/cucumber_expression/RegularExpression.hpp" -#include "cucumber_cpp/library/engine/StepType.hpp" -#include "cucumber_cpp/library/engine/Table.hpp" -#include -#include -#include -#include -#include -#include -#include - -namespace cucumber_cpp::library -{ - StepRegistry::StepRegistry(cucumber_expression::ParameterRegistry& parameterRegistry) - : parameterRegistry{ parameterRegistry } - { - for (const auto& matcher : StepStringRegistration::Instance().GetEntries()) - Register(matcher.regex, matcher.type, matcher.factory); - } - - StepMatch StepRegistry::Query(const std::string& expression) - { - std::vector matches; - - for (Entry& entry : registry) - { - auto match = std::visit(cucumber_expression::MatchVisitor{ expression }, entry.regex); - if (match) - { - matches.emplace_back(entry.factory, *match, std::visit(cucumber_expression::PatternVisitor{}, entry.regex)); - ++entry.used; - } - } - - if (matches.empty()) - throw StepNotFoundError{}; - - if (matches.size() > 1) - throw AmbiguousStepError{ std::move(matches) }; - - return std::move(matches.front()); - } - - std::size_t StepRegistry::Size() const - { - return registry.size(); - } - - std::vector StepRegistry::List() const - { - std::vector list; - - list.reserve(registry.size()); - - for (const Entry& entry : registry) - list.emplace_back(entry.regex, entry.used); - - return list; - } - - void StepRegistry::Register(const std::string& matcher, engine::StepType stepType, std::unique_ptr (&factory)(Context& context, const engine::Table& table, const std::string& docString)) - { - if (matcher.starts_with('^') || matcher.ends_with('$')) - registry.emplace_back(stepType, cucumber_expression::Matcher{ std::in_place_type, matcher }, factory); - else - registry.emplace_back(stepType, cucumber_expression::Matcher{ std::in_place_type, matcher, parameterRegistry }, factory); - } - - StepStringRegistration& StepStringRegistration::Instance() - { - static StepStringRegistration instance; - return instance; - } - - std::span StepStringRegistration::GetEntries() - { - return registry; - } - - std::span StepStringRegistration::GetEntries() const - { - return registry; - } -} diff --git a/cucumber_cpp/library/StepRegistry.hpp b/cucumber_cpp/library/StepRegistry.hpp deleted file mode 100644 index e6dc4746..00000000 --- a/cucumber_cpp/library/StepRegistry.hpp +++ /dev/null @@ -1,143 +0,0 @@ -#ifndef CUCUMBER_CPP_STEPREGISTRY_HPP -#define CUCUMBER_CPP_STEPREGISTRY_HPP - -#include "cucumber_cpp/library/Body.hpp" -#include "cucumber_cpp/library/Context.hpp" -#include "cucumber_cpp/library/cucumber_expression/Matcher.hpp" -#include "cucumber_cpp/library/cucumber_expression/ParameterRegistry.hpp" -#include "cucumber_cpp/library/engine/StepType.hpp" -#include "cucumber_cpp/library/engine/Table.hpp" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace cucumber_cpp::library -{ - template - std::unique_ptr StepBodyFactory(Context& context, const engine::Table& table, const std::string& docString) - { - return std::make_unique(context, table, docString); - } - - struct StepMatch - { - StepMatch(std::unique_ptr (&factory)(Context& context, const engine::Table& table, const std::string& docString), std::variant, std::vector> matches, std::string_view stepRegexStr) - : factory(factory) - , matches(std::move(matches)) - , stepRegexStr(stepRegexStr) - {} - - std::unique_ptr (&factory)(Context& context, const engine::Table& table, const std::string& docString); - std::variant, std::vector> matches{}; - std::string_view stepRegexStr{}; - }; - - struct StepRegistry - { - struct StepNotFoundError : std::exception - { - using std::exception::exception; - }; - - struct AmbiguousStepError : std::exception - { - explicit AmbiguousStepError(std::vector&& matches) - : matches{ std::move(matches) } - {} - - std::vector matches; - }; - - struct Entry - { - Entry(engine::StepType type, cucumber_expression::Matcher regex, std::unique_ptr (&factory)(Context& context, const engine::Table& table, const std::string& docString)) - : type(type) - , regex(std::move(regex)) - , factory(factory) - {} - - engine::StepType type{}; - cucumber_expression::Matcher regex; - std::unique_ptr (&factory)(Context& context, const engine::Table& table, const std::string& docString); - - std::uint32_t used{ 0 }; - }; - - struct EntryView - { - EntryView(const cucumber_expression::Matcher& stepRegex, const std::uint32_t& used) - : stepRegex(stepRegex) - , used(used) - {} - - const cucumber_expression::Matcher& stepRegex; - const std::uint32_t& used; - }; - - explicit StepRegistry(cucumber_expression::ParameterRegistry& parameterRegistry); - - [[nodiscard]] StepMatch Query(const std::string& expression); - - [[nodiscard]] std::size_t Size() const; - - [[nodiscard]] std::vector List() const; - - private: - void Register(const std::string& matcher, engine::StepType stepType, std::unique_ptr (&factory)(Context& context, const engine::Table& table, const std::string& docString)); - - std::vector registry; - cucumber_expression::ParameterRegistry& parameterRegistry; - }; - - struct StepStringRegistration - { - private: - StepStringRegistration() = default; - - public: - static StepStringRegistration& Instance(); - - struct Entry - { - Entry(engine::StepType type, std::string regex, std::unique_ptr (&factory)(Context& context, const engine::Table& table, const std::string& docString)) - : type(type) - , regex(std::move(regex)) - , factory(factory) - {} - - engine::StepType type{}; - std::string regex; - std::unique_ptr (&factory)(Context& context, const engine::Table& table, const std::string& docString); - }; - - template - static std::size_t Register(const std::string& matcher, engine::StepType stepType); - - std::span GetEntries(); - [[nodiscard]] std::span GetEntries() const; - - private: - std::vector registry; - }; - - ////////////////////////// - // implementation // - ////////////////////////// - - template - std::size_t StepStringRegistration::Register(const std::string& matcher, engine::StepType stepType) - { - Instance().registry.emplace_back(stepType, matcher, StepBodyFactory); - - return Instance().registry.size(); - } -} - -#endif diff --git a/cucumber_cpp/library/Steps.hpp b/cucumber_cpp/library/Steps.hpp index ad1b459f..5e8cdf3c 100644 --- a/cucumber_cpp/library/Steps.hpp +++ b/cucumber_cpp/library/Steps.hpp @@ -5,10 +5,11 @@ // IWYU pragma: friend cucumber_cpp/.* #include "cucumber_cpp/library/BodyMacro.hpp" -#include "cucumber_cpp/library/StepRegistry.hpp" #include "cucumber_cpp/library/engine/Step.hpp" +#include "cucumber_cpp/library/support/StepRegistry.hpp" +#include "cucumber_cpp/library/support/SupportCodeLibrary.hpp" -#define STEP_(matcher, type, args, fixture) BODY(matcher, type, args, cucumber_cpp::library::StepStringRegistration::Register, fixture) +#define STEP_(matcher, type, args, fixture) BODY(matcher, type, args, cucumber_cpp::library::support::DefinitionRegistration::Register, fixture) #define STEP_TYPE_(fixture, type, ...) \ STEP_( \ @@ -17,14 +18,14 @@ BODY_ARGS(__VA_ARGS__, (), ()), \ fixture) -#define GIVEN_F(fixture, ...) STEP_TYPE_(fixture, cucumber_cpp::library::engine::StepType::given, __VA_ARGS__) -#define WHEN_F(fixture, ...) STEP_TYPE_(fixture, cucumber_cpp::library::engine::StepType::when, __VA_ARGS__) -#define THEN_F(fixture, ...) STEP_TYPE_(fixture, cucumber_cpp::library::engine::StepType::then, __VA_ARGS__) -#define STEP_F(fixture, ...) STEP_TYPE_(fixture, cucumber_cpp::library::engine::StepType::any, __VA_ARGS__) +#define GIVEN_F(fixture, ...) STEP_TYPE_(fixture, cucumber_cpp::library::support::StepType::given, __VA_ARGS__) +#define WHEN_F(fixture, ...) STEP_TYPE_(fixture, cucumber_cpp::library::support::StepType::when, __VA_ARGS__) +#define THEN_F(fixture, ...) STEP_TYPE_(fixture, cucumber_cpp::library::support::StepType::then, __VA_ARGS__) +#define STEP_F(fixture, ...) STEP_TYPE_(fixture, cucumber_cpp::library::support::StepType::any, __VA_ARGS__) -#define GIVEN(...) GIVEN_F(cucumber_cpp::library::engine::Step, __VA_ARGS__) -#define WHEN(...) WHEN_F(cucumber_cpp::library::engine::Step, __VA_ARGS__) -#define THEN(...) THEN_F(cucumber_cpp::library::engine::Step, __VA_ARGS__) -#define STEP(...) STEP_F(cucumber_cpp::library::engine::Step, __VA_ARGS__) +#define GIVEN(...) GIVEN_F(cucumber_cpp::library::engine::StepBase, __VA_ARGS__) +#define WHEN(...) WHEN_F(cucumber_cpp::library::engine::StepBase, __VA_ARGS__) +#define THEN(...) THEN_F(cucumber_cpp::library::engine::StepBase, __VA_ARGS__) +#define STEP(...) STEP_F(cucumber_cpp::library::engine::StepBase, __VA_ARGS__) #endif diff --git a/cucumber_cpp/library/TagExpression.cpp b/cucumber_cpp/library/TagExpression.cpp deleted file mode 100644 index 42370d77..00000000 --- a/cucumber_cpp/library/TagExpression.cpp +++ /dev/null @@ -1,74 +0,0 @@ -#include "cucumber_cpp/library/TagExpression.hpp" -#include "cucumber_cpp/library/Errors.hpp" -#include -#include -#include -#include -#include - -namespace cucumber_cpp::library -{ - namespace - { - void Replace(std::string& str, const std::string& from, const std::string& to) - { - str = std::regex_replace(str, std::regex(from), to); - } - } - - bool IsTagExprSelected(std::string_view tagExpr, const std::set>& tags) - { - if (tagExpr.empty()) - { - return true; - } - - std::string eval{ tagExpr }; - - for (std::smatch matches; std::regex_search(eval, matches, std::regex(R"((@[^ \)]+))"));) - { - Replace(eval, matches[1], tags.contains(matches[1]) ? "1" : "0"); - } - - Replace(eval, "not", "!"); - Replace(eval, "or", "|"); - Replace(eval, "and", "&"); - Replace(eval, " ", ""); - - for (;;) - { - auto s = eval.size(); - - Replace(eval, R"(\(0\))", "0"); - Replace(eval, R"(\(1\))", "1"); - Replace(eval, "!0", "1"); - Replace(eval, "!1", "0"); - - Replace(eval, "0&0", "0"); - Replace(eval, "0&1", "0"); - Replace(eval, "1&0", "0"); - Replace(eval, "1&1", "1"); - Replace(eval, "00", "0"); - Replace(eval, "01", "0"); - Replace(eval, "10", "0"); - Replace(eval, "11", "1"); - - Replace(eval, R"(0\|0)", "0"); - Replace(eval, R"(0\|1)", "1"); - Replace(eval, R"(1\|0)", "1"); - Replace(eval, R"(1\|1)", "1"); - - if (s == eval.size()) - { - break; - } - } - - if (eval.size() != 1) - { - throw InternalError("Could not parse tag expression: \"" + std::string{ tagExpr } + "\""); - } - - return eval == std::string("1"); - } -} diff --git a/cucumber_cpp/library/TagExpression.hpp b/cucumber_cpp/library/TagExpression.hpp deleted file mode 100644 index 3734f0bb..00000000 --- a/cucumber_cpp/library/TagExpression.hpp +++ /dev/null @@ -1,14 +0,0 @@ -#ifndef CUCUMBER_CPP_TAGEXPRESSION_HPP -#define CUCUMBER_CPP_TAGEXPRESSION_HPP - -#include -#include -#include -#include - -namespace cucumber_cpp::library -{ - bool IsTagExprSelected(std::string_view tagExpr, const std::set>& tags); -} - -#endif diff --git a/cucumber_cpp/library/TagsToSet.hpp b/cucumber_cpp/library/TagsToSet.hpp deleted file mode 100644 index 58665f3b..00000000 --- a/cucumber_cpp/library/TagsToSet.hpp +++ /dev/null @@ -1,21 +0,0 @@ -#ifndef CUCUMBER_CPP_TAGSTOSET_HPP -#define CUCUMBER_CPP_TAGSTOSET_HPP - -#include -#include -#include - -namespace cucumber_cpp::library -{ - auto TagsToSet(const auto& tags) - { - std::set> result; - - for (const auto& entry : tags) - result.insert(entry.name); - - return result; - } -} - -#endif diff --git a/cucumber_cpp/library/TraceTime.cpp b/cucumber_cpp/library/TraceTime.cpp deleted file mode 100644 index 722076c5..00000000 --- a/cucumber_cpp/library/TraceTime.cpp +++ /dev/null @@ -1,23 +0,0 @@ -#include "cucumber_cpp/library/TraceTime.hpp" -#include - -namespace cucumber_cpp::library -{ - void TraceTime::Start() - { - timeStart = std::chrono::high_resolution_clock::now(); - } - - void TraceTime::Stop() - { - timeStop = std::chrono::high_resolution_clock::now(); - } - - TraceTime::Duration TraceTime::Delta() const - { - if (timeStop != TimePoint{}) - return timeStop - timeStart; - - return std::chrono::high_resolution_clock::now() - timeStart; - } -} diff --git a/cucumber_cpp/library/TraceTime.hpp b/cucumber_cpp/library/TraceTime.hpp deleted file mode 100644 index 9e5c5f54..00000000 --- a/cucumber_cpp/library/TraceTime.hpp +++ /dev/null @@ -1,24 +0,0 @@ -#ifndef CUCUMBER_CPP_TRACETIME_HPP -#define CUCUMBER_CPP_TRACETIME_HPP - -#include - -namespace cucumber_cpp::library -{ - struct TraceTime - { - using TimePoint = std::chrono::time_point; - using Duration = TimePoint::duration; - - void Start(); - void Stop(); - - [[nodiscard]] Duration Delta() const; - - private: - TimePoint timeStart{}; - TimePoint timeStop{}; - }; -} - -#endif diff --git a/cucumber_cpp/library/api/CMakeLists.txt b/cucumber_cpp/library/api/CMakeLists.txt new file mode 100644 index 00000000..08282f68 --- /dev/null +++ b/cucumber_cpp/library/api/CMakeLists.txt @@ -0,0 +1,27 @@ +add_library(cucumber_cpp.library.api ${CCR_EXCLUDE_FROM_ALL}) + +target_sources(cucumber_cpp.library.api PRIVATE + Formatters.cpp + Formatters.hpp + Gherkin.cpp + Gherkin.hpp + RunCucumber.cpp + RunCucumber.hpp +) + +target_include_directories(cucumber_cpp.library.api PUBLIC + ../../.. +) + +target_link_libraries(cucumber_cpp.library.api PUBLIC + cucumber_cpp.library.cucumber_expression + cucumber_cpp.library.formatter + cucumber_cpp.library.runtime + cucumber_cpp.library.support + cucumber_cpp.library.util +) + +if (CCR_BUILD_TESTS) + add_subdirectory(test) + # add_subdirectory(test_helper) +endif() diff --git a/cucumber_cpp/library/api/Formatters.cpp b/cucumber_cpp/library/api/Formatters.cpp new file mode 100644 index 00000000..cb8bd025 --- /dev/null +++ b/cucumber_cpp/library/api/Formatters.cpp @@ -0,0 +1,73 @@ +#include "cucumber_cpp/library/api/Formatters.hpp" +#include "cucumber_cpp/library/formatter/Formatter.hpp" +#include "cucumber_cpp/library/formatter/JunitXmlFormatter.hpp" +#include "cucumber_cpp/library/formatter/MessageFormatter.hpp" +#include "cucumber_cpp/library/formatter/PrettyFormatter.hpp" +#include "cucumber_cpp/library/formatter/SummaryFormatter.hpp" +#include "cucumber_cpp/library/formatter/UsageFormatter.hpp" +#include "cucumber_cpp/library/query/Query.hpp" +#include "cucumber_cpp/library/support/SupportCodeLibrary.hpp" +#include "nlohmann/json_fwd.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace cucumber_cpp::library::api +{ + FormatterOption::FormatterOption(std::string_view str) + { + const auto colon = str.find(':'); + name = str.substr(0, colon); + output = colon == std::string::npos ? "" : str.substr(colon + 1); + } + + Formatters::Formatters() + { + RegisterFormatter(); + RegisterFormatter(); + RegisterFormatter(); + RegisterFormatter(); + RegisterFormatter(); + } + + std::set> Formatters::GetAvailableFormatterNames() const + { + auto view = availableFormatters | std::views::transform([](const auto& pair) -> std::pair + { + return { pair.first, pair.second.hasOutput }; + }); + return { view.begin(), view.end() }; + } + + std::list> Formatters::EnableFormatters(const std::set>& format, const nlohmann::json& formatOptions, support::SupportCodeLibrary& supportCodeLibrary, query::Query& query, std::ostream& output) + { + std::list> activeFormatters; + + for (const auto& formatterName : format) + { + const FormatterOption option{ formatterName }; + + if (option.output.empty()) + activeFormatters.emplace_back(availableFormatters.at(option.name).factory(supportCodeLibrary, query, formatOptions, output)); + else + { + const auto absolutePath = std::filesystem::absolute(std::filesystem::path{ option.output }).string(); + if (!customOutputFiles.contains(absolutePath)) + customOutputFiles.try_emplace(absolutePath, std::make_unique(absolutePath)); + + activeFormatters.emplace_back(availableFormatters.at(option.name).factory(supportCodeLibrary, query, formatOptions, *customOutputFiles.at(absolutePath))); + } + } + + return activeFormatters; + } +} diff --git a/cucumber_cpp/library/api/Formatters.hpp b/cucumber_cpp/library/api/Formatters.hpp new file mode 100644 index 00000000..268e9feb --- /dev/null +++ b/cucumber_cpp/library/api/Formatters.hpp @@ -0,0 +1,66 @@ +#ifndef API_FORMATTERS_HPP +#define API_FORMATTERS_HPP + +#include "cucumber_cpp/library/formatter/Formatter.hpp" +#include "cucumber_cpp/library/query/Query.hpp" +#include "cucumber_cpp/library/support/SupportCodeLibrary.hpp" +#include "nlohmann/json_fwd.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace cucumber_cpp::library::api +{ + struct FormatterOption + { + explicit FormatterOption(std::string_view str); + + std::string name; + std::string output; + }; + + struct RegisteredFormatter + { + std::function(support::SupportCodeLibrary&, query::Query&, const nlohmann::json& formatOptions, std::ostream&)> factory; + bool hasOutput{ false }; + }; + + struct Formatters + { + Formatters(); + + template + void RegisterFormatter(bool hasOutput = false); + + std::set> GetAvailableFormatterNames() const; + + [[nodiscard]] std::list> EnableFormatters(const std::set>& format, const nlohmann::json& formatOptions, support::SupportCodeLibrary& supportCodeLibrary, query::Query& query, std::ostream& output = std::cout); + + private: + std::map> availableFormatters; + std::map, std::less<>> customOutputFiles; + }; + + //////////////////// + // Implementation // + //////////////////// + + template + void Formatters::RegisterFormatter(bool hasOutput) + { + availableFormatters.try_emplace(T::name, [](support::SupportCodeLibrary& supportCodeLibrary, query::Query& query, const nlohmann::json& formatOptions, std::ostream& output) + { + return std::make_unique(supportCodeLibrary, query, formatOptions, output); + }, + hasOutput); + } +} + +#endif diff --git a/cucumber_cpp/library/api/Gherkin.cpp b/cucumber_cpp/library/api/Gherkin.cpp new file mode 100644 index 00000000..e8733425 --- /dev/null +++ b/cucumber_cpp/library/api/Gherkin.cpp @@ -0,0 +1,78 @@ +#include "cucumber_cpp/library/api/Gherkin.hpp" +#include "cucumber/gherkin/app.hpp" +#include "cucumber/gherkin/exceptions.hpp" +#include "cucumber/gherkin/id_generator.hpp" +#include "cucumber/gherkin/pickle_compiler.hpp" +#include "cucumber/gherkin/utils.hpp" +#include "cucumber/messages/envelope.hpp" +#include "cucumber/messages/gherkin_document.hpp" +#include "cucumber/messages/parse_error.hpp" +#include "cucumber/messages/pickle.hpp" +#include "cucumber/messages/source_reference.hpp" +#include "cucumber_cpp/library/support/Types.hpp" +#include "cucumber_cpp/library/util/Broadcaster.hpp" +#include +#include + +namespace cucumber_cpp::library::api +{ + std::list CollectPickles(const support::RunOptions::Sources& sources, cucumber::gherkin::id_generator_ptr idGenerator, util::Broadcaster& broadcaster) + { + std::list pickleSources; + + cucumber::gherkin::parser<> parser{ idGenerator }; + + cucumber::messages::envelope envelope; + + for (const auto& path : sources.paths) + { + envelope.source = { + .uri = path.string(), + .data = cucumber::gherkin::slurp(path.string()), + }; + + broadcaster.BroadcastEvent(envelope); + + try + { + auto ast = std::make_shared(parser.parse(envelope.source->uri, envelope.source->data)); + broadcaster.BroadcastEvent({ .gherkin_document = *ast }); + + cucumber::gherkin::pickle_compiler pc(idGenerator); + pc.compile(*ast, envelope.source->uri, [&pickleSources, ast, &broadcaster](const auto& pickle) + { + pickleSources.emplace_back( + std::make_shared(pickle), + ast); + + broadcaster.BroadcastEvent({ .pickle = pickle }); + }); + } + catch (const cucumber::gherkin::composite_parser_error& compositeError) + { + for (const auto& error : compositeError.errors()) + { + broadcaster.BroadcastEvent({ .parse_error = cucumber::messages::parse_error{ + .source = cucumber::messages::source_reference{ + .uri = envelope.source->uri, + .location = error->location(), + }, + .message = error->what(), + } }); + } + } + catch (const cucumber::gherkin::parser_error& error) + { + broadcaster.BroadcastEvent({ .parse_error = cucumber::messages::parse_error{ + .source = cucumber::messages::source_reference{ + .uri = envelope.source->uri, + .location = error.location(), + }, + .message = error.what(), + } }); + } + } + + return pickleSources; + } +} diff --git a/cucumber_cpp/library/api/Gherkin.hpp b/cucumber_cpp/library/api/Gherkin.hpp new file mode 100644 index 00000000..3bbc7b63 --- /dev/null +++ b/cucumber_cpp/library/api/Gherkin.hpp @@ -0,0 +1,14 @@ +#ifndef API_GHERKIN_HPP +#define API_GHERKIN_HPP + +#include "cucumber/gherkin/id_generator.hpp" +#include "cucumber_cpp/library/support/Types.hpp" +#include "cucumber_cpp/library/util/Broadcaster.hpp" +#include + +namespace cucumber_cpp::library::api +{ + std::list CollectPickles(const support::RunOptions::Sources& sources, cucumber::gherkin::id_generator_ptr idGenerator, util::Broadcaster& broadcaster); +} + +#endif diff --git a/cucumber_cpp/library/api/RunCucumber.cpp b/cucumber_cpp/library/api/RunCucumber.cpp new file mode 100644 index 00000000..bfcf3274 --- /dev/null +++ b/cucumber_cpp/library/api/RunCucumber.cpp @@ -0,0 +1,199 @@ +#include "cucumber_cpp/library/api/RunCucumber.hpp" +#include "cucumber/gherkin/id_generator.hpp" +#include "cucumber/messages/location.hpp" +#include "cucumber/messages/parameter_type.hpp" +#include "cucumber/messages/source_reference.hpp" +#include "cucumber/messages/step_definition.hpp" +#include "cucumber/messages/step_definition_pattern.hpp" +#include "cucumber_cpp/library/Context.hpp" +#include "cucumber_cpp/library/api/Formatters.hpp" +#include "cucumber_cpp/library/api/Gherkin.hpp" +#include "cucumber_cpp/library/cucumber_expression/ParameterRegistry.hpp" +#include "cucumber_cpp/library/query/Query.hpp" +#include "cucumber_cpp/library/runtime/MakeRuntime.hpp" +#include "cucumber_cpp/library/support/HookRegistry.hpp" +#include "cucumber_cpp/library/support/StepRegistry.hpp" +#include "cucumber_cpp/library/support/SupportCodeLibrary.hpp" +#include "cucumber_cpp/library/support/Types.hpp" +#include "cucumber_cpp/library/support/UndefinedParameters.hpp" +#include "cucumber_cpp/library/util/Broadcaster.hpp" +#include "nlohmann/json_fwd.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace cucumber_cpp::library::api +{ + namespace + { + void EmitParameters(const support::SupportCodeLibrary& supportCodeLibrary, util::Broadcaster& broadcaster, cucumber::gherkin::id_generator_ptr idGenerator) + { + for (const auto& [name, parameter] : supportCodeLibrary.parameterRegistry.GetParameters()) + { + if (parameter.isBuiltin) + continue; + + broadcaster.BroadcastEvent({ + .parameter_type = cucumber::messages::parameter_type{ + .name = parameter.name, + .regular_expressions = parameter.regex, + .use_for_snippets = parameter.useForSnippets, + .id = idGenerator->next_id(), + .source_reference = cucumber::messages::source_reference{ + .uri = parameter.location.file_name(), + .location = cucumber::messages::location{ + .line = parameter.location.line(), + }, + }, + }, + }); + } + } + + void EmitUndefinedParameters(const support::SupportCodeLibrary& supportCodeLibrary, util::Broadcaster& broadcaster) + { + for (const auto& parameter : supportCodeLibrary.undefinedParameters.definitions) + broadcaster.BroadcastEvent({ .undefined_parameter_type = parameter }); + } + + void EmitStepDefinitions(const support::SupportCodeLibrary& supportCodeLibrary, util::Broadcaster& broadcaster) + { + for (const auto& stepDefinition : supportCodeLibrary.stepRegistry.StepDefinitions()) + { + broadcaster.BroadcastEvent({ .step_definition = cucumber::messages::step_definition{ + .id = stepDefinition.id, + .pattern = cucumber::messages::step_definition_pattern{ + .source = stepDefinition.pattern, + .type = stepDefinition.patternType, + }, + .source_reference = { + .uri = stepDefinition.uri.string(), + .location = cucumber::messages::location{ + .line = stepDefinition.line, + }, + }, + } }); + } + } + + void EmitTestCaseHooks(const support::SupportCodeLibrary& supportCodeLibrary, util::Broadcaster& broadcaster) + { + auto beforeAllHooks = supportCodeLibrary.hookRegistry.HooksByType(support::HookType::before); + + for (auto& hook : beforeAllHooks) + broadcaster.BroadcastEvent({ .hook = std::move(hook) }); + + auto afterAllHooks = supportCodeLibrary.hookRegistry.HooksByType(support::HookType::after); + + for (auto& hook : afterAllHooks) + broadcaster.BroadcastEvent({ .hook = std::move(hook) }); + } + + void EmitTestRunHooks(const support::SupportCodeLibrary& supportCodeLibrary, util::Broadcaster& broadcaster) + { + auto beforeAllHooks = supportCodeLibrary.hookRegistry.HooksByType(support::HookType::beforeAll); + + for (auto& hook : beforeAllHooks) + broadcaster.BroadcastEvent({ .hook = std::move(hook) }); + + auto afterAllHooks = supportCodeLibrary.hookRegistry.HooksByType(support::HookType::afterAll); + + for (auto& hook : afterAllHooks) + broadcaster.BroadcastEvent({ .hook = std::move(hook) }); + } + + void EmitSupportCodeMessages(const support::SupportCodeLibrary& supportCodeLibrary, util::Broadcaster& broadcaster, cucumber::gherkin::id_generator_ptr idGenerator) + { + EmitParameters(supportCodeLibrary, broadcaster, idGenerator); + + support::DefinitionRegistration::Instance().LoadIds(idGenerator); + supportCodeLibrary.stepRegistry.LoadSteps(); + + EmitUndefinedParameters(supportCodeLibrary, broadcaster); + EmitStepDefinitions(supportCodeLibrary, broadcaster); + + supportCodeLibrary.hookRegistry.LoadHooks(); + EmitTestCaseHooks(supportCodeLibrary, broadcaster); + EmitTestRunHooks(supportCodeLibrary, broadcaster); + } + + auto FilterByTagExpression(const support::RunOptions::Sources& sources) + { + return [&sources](const support::PickleSource& pickle) + { + return sources.tagExpression->Evaluate(pickle.pickle->tags); + }; + } + + std::list OrderPickles(const support::RunOptions::Sources& sources, auto pickles) + { + const auto createOrderedPickleList = [](auto ordered) -> std::list + { + return { ordered.begin(), ordered.end() }; + }; + + if (sources.ordering == support::RunOptions::Ordering::defined) + return createOrderedPickleList(pickles); + else + return createOrderedPickleList(pickles | std::views::reverse); + }; + + void signal_handler(int signal) + { + if (signal == SIGABRT) + std::cerr << "SIGABRT received\n"; + else + std::cerr << "Unexpected signal " << signal << " received\n"; + std::_Exit(EXIT_FAILURE); + } + + struct OverrideAbortSignalHandler + { + ~OverrideAbortSignalHandler() + { + std::signal(SIGABRT, original); + } + + using signal_handler_t = void (*)(int); + signal_handler_t original{ std::signal(SIGABRT, signal_handler) }; + }; + } + + bool RunCucumber(const support::RunOptions& options, cucumber_expression::ParameterRegistry& parameterRegistry, Context& programContext, util::Broadcaster& broadcaster, Formatters& formatters, const std::set>& format, const std::string& formatOptions) + { + OverrideAbortSignalHandler overrideSignalHandler; + + cucumber::gherkin::id_generator_ptr idGenerator = std::make_shared(); + + support::UndefinedParameters undefinedParameters; + support::StepRegistry stepRegistry{ parameterRegistry, undefinedParameters, idGenerator }; + support::HookRegistry hookRegistry{ idGenerator }; + + support::SupportCodeLibrary supportCodeLibrary{ + .hookRegistry = hookRegistry, + .stepRegistry = stepRegistry, + .parameterRegistry = parameterRegistry, + .undefinedParameters = undefinedParameters, + }; + + query::Query query{ broadcaster }; + + const auto formatOptionsJson = formatOptions.empty() ? nlohmann::json::object() : nlohmann::json::parse(formatOptions); + const auto activeFormatters = formatters.EnableFormatters(format, formatOptionsJson, supportCodeLibrary, query); + + const auto pickleSources = CollectPickles(options.sources, idGenerator, broadcaster); + const auto orderedPickles = OrderPickles(options.sources, pickleSources | std::views::filter(FilterByTagExpression(options.sources))); + + EmitSupportCodeMessages(supportCodeLibrary, broadcaster, idGenerator); + + const auto runtime = runtime::MakeRuntime(options.runtime, broadcaster, orderedPickles, supportCodeLibrary, idGenerator, programContext); + return runtime->Run(); + } +} diff --git a/cucumber_cpp/library/api/RunCucumber.hpp b/cucumber_cpp/library/api/RunCucumber.hpp new file mode 100644 index 00000000..34ac1976 --- /dev/null +++ b/cucumber_cpp/library/api/RunCucumber.hpp @@ -0,0 +1,18 @@ +#ifndef API_RUN_CUCUMBER_HPP +#define API_RUN_CUCUMBER_HPP + +#include "cucumber_cpp/library/Context.hpp" +#include "cucumber_cpp/library/api/Formatters.hpp" +#include "cucumber_cpp/library/cucumber_expression/ParameterRegistry.hpp" +#include "cucumber_cpp/library/support/Types.hpp" +#include "cucumber_cpp/library/util/Broadcaster.hpp" +#include +#include +#include + +namespace cucumber_cpp::library::api +{ + bool RunCucumber(const support::RunOptions& options, cucumber_expression::ParameterRegistry& parameterRegistry, Context& programContext, util::Broadcaster& broadcaster, Formatters& formatters, const std::set>& format, const std::string& formatOptions); +} + +#endif diff --git a/cucumber_cpp/library/api/test/CMakeLists.txt b/cucumber_cpp/library/api/test/CMakeLists.txt new file mode 100644 index 00000000..6f6e4700 --- /dev/null +++ b/cucumber_cpp/library/api/test/CMakeLists.txt @@ -0,0 +1,12 @@ +add_executable(cucumber_cpp.library.api.test) +add_test(NAME cucumber_cpp.library.api.test COMMAND cucumber_cpp.library.api.test) + +target_link_libraries(cucumber_cpp.library.api.test PUBLIC + gmock_main + cucumber_cpp.library.api + GTest::gmock +) + +target_sources(cucumber_cpp.library.api.test PRIVATE + TestDummy.cpp +) diff --git a/cucumber_cpp/library/api/test/TestDummy.cpp b/cucumber_cpp/library/api/test/TestDummy.cpp new file mode 100644 index 00000000..c81abaf1 --- /dev/null +++ b/cucumber_cpp/library/api/test/TestDummy.cpp @@ -0,0 +1,8 @@ +#include + +namespace cucumber_cpp::library::support +{ + TEST(Dummy, dummy) + { + } +} diff --git a/cucumber_cpp/library/assemble/AssembleTestSuites.cpp b/cucumber_cpp/library/assemble/AssembleTestSuites.cpp new file mode 100644 index 00000000..524fb29f --- /dev/null +++ b/cucumber_cpp/library/assemble/AssembleTestSuites.cpp @@ -0,0 +1,125 @@ +#include "cucumber_cpp/library/assemble/AssembleTestSuites.hpp" +#include "cucumber/gherkin/id_generator.hpp" +#include "cucumber/messages/envelope.hpp" +#include "cucumber/messages/step_match_arguments_list.hpp" +#include "cucumber/messages/test_case.hpp" +#include "cucumber_cpp/library/assemble/AssembledTestSuite.hpp" +#include "cucumber_cpp/library/cucumber_expression/Argument.hpp" +#include "cucumber_cpp/library/cucumber_expression/Matcher.hpp" +#include "cucumber_cpp/library/support/HookRegistry.hpp" +#include "cucumber_cpp/library/support/StepRegistry.hpp" +#include "cucumber_cpp/library/support/SupportCodeLibrary.hpp" +#include "cucumber_cpp/library/support/Types.hpp" +#include "cucumber_cpp/library/util/Broadcaster.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace cucumber_cpp::library::assemble +{ + namespace + { + auto TransformToMatch(const std::string& text) + { + return [&text](const support::StepRegistry::Definition& definition) -> std::pair>> + { + const auto match = std::visit(cucumber_expression::MatchVisitor{ text }, definition.regex); + return { definition.id, match }; + }; + } + + bool HasMatch(const std::pair>>& pair) + { + return pair.second.has_value(); + } + + void AssembleSteps(const support::SupportCodeLibrary& supportCodeLibrary, const support::PickleSource& pickleSource, cucumber::messages::test_case& testCase, cucumber::gherkin::id_generator_ptr idGenerator) + { + for (const auto& step : pickleSource.pickle->steps) + { + const auto& stepDefinitions = supportCodeLibrary.stepRegistry.StepDefinitions(); + + auto& testStep = testCase.test_steps.emplace_back( + std::nullopt, + idGenerator->next_id(), + step.id, + std::vector{}, + std::vector{}); + + for (const auto& [id, match] : stepDefinitions | + std::views::transform(TransformToMatch(step.text)) | + std::views::filter(HasMatch)) + { + testStep.step_definition_ids.value().push_back(id); + auto& argumentList = testStep.step_match_arguments_lists.value().emplace_back(); + for (const auto& result : *match) + argumentList.step_match_arguments.emplace_back(result.Group(), result.Name().empty() ? std::nullopt : std::make_optional(result.Name())); + } + } + } + + void AssembleTestSteps(support::SupportCodeLibrary& supportCodeLibrary, const support::PickleSource& pickleSource, cucumber::messages::test_case& testCase, cucumber::gherkin::id_generator_ptr idGenerator) + { + auto beforeHooks = supportCodeLibrary.hookRegistry.FindIds(support::HookType::before, pickleSource.pickle->tags); + auto afterHooks = supportCodeLibrary.hookRegistry.FindIds(support::HookType::after, pickleSource.pickle->tags); + + testCase.test_steps.reserve(beforeHooks.size() + pickleSource.pickle->steps.size() + afterHooks.size()); + + for (const auto& hookId : beforeHooks) + testCase.test_steps.emplace_back(hookId, idGenerator->next_id()); + + AssembleSteps(supportCodeLibrary, pickleSource, testCase, idGenerator); + + for (const auto& hookId : afterHooks | std::views::reverse) + testCase.test_steps.emplace_back(hookId, idGenerator->next_id()); + } + } + + std::vector AssembleTestSuites(support::SupportCodeLibrary& supportCodeLibrary, + std::string_view testRunStartedId, + util::Broadcaster& broadcaster, + const std::list& sourcedPickles, + cucumber::gherkin::id_generator_ptr idGenerator) + { + std::list testUris; + std::map> assembledTestSuiteMap; + + for (const auto& pickleSource : sourcedPickles) + { + cucumber::messages::test_case testCase{ + .id = idGenerator->next_id(), + .pickle_id = pickleSource.pickle->id, + .test_steps = {}, + .test_run_started_id = std::make_optional(testRunStartedId) + }; + + AssembleTestSteps(supportCodeLibrary, pickleSource, testCase, idGenerator); + + broadcaster.BroadcastEvent(cucumber::messages::envelope{ .test_case = testCase }); + + if (!assembledTestSuiteMap.contains(pickleSource.gherkinDocument->uri.value())) + { + testUris.emplace_back(pickleSource.gherkinDocument->uri.value()); + assembledTestSuiteMap.try_emplace(pickleSource.gherkinDocument->uri.value(), *pickleSource.gherkinDocument); + } + + assembledTestSuiteMap.at(pickleSource.gherkinDocument->uri.value()).testCases.emplace_back(*pickleSource.pickle, testCase); + } + + std::vector assembledTestSuites; + assembledTestSuites.reserve(assembledTestSuiteMap.size()); + + for (const auto& uri : testUris) + assembledTestSuites.emplace_back(std::move(assembledTestSuiteMap.at(uri))); + + return assembledTestSuites; + } +} diff --git a/cucumber_cpp/library/assemble/AssembleTestSuites.hpp b/cucumber_cpp/library/assemble/AssembleTestSuites.hpp new file mode 100644 index 00000000..90bc52c3 --- /dev/null +++ b/cucumber_cpp/library/assemble/AssembleTestSuites.hpp @@ -0,0 +1,23 @@ +#ifndef ASSEMBLE_ASSEMBLE_TEST_SUITES_HPP +#define ASSEMBLE_ASSEMBLE_TEST_SUITES_HPP + +#include "cucumber/gherkin/id_generator.hpp" +#include "cucumber_cpp/library/assemble/AssembledTestSuite.hpp" +#include "cucumber_cpp/library/support/SupportCodeLibrary.hpp" +#include "cucumber_cpp/library/support/Types.hpp" +#include "cucumber_cpp/library/util/Broadcaster.hpp" +#include +#include +#include + +namespace cucumber_cpp::library::assemble +{ + std::vector AssembleTestSuites( + support::SupportCodeLibrary& supportCodeLibrary, + std::string_view testRunStartedId, + util::Broadcaster& broadcaster, + const std::list& sourcedPickles, + cucumber::gherkin::id_generator_ptr idGenerator); +} + +#endif diff --git a/cucumber_cpp/library/assemble/AssembledTestCase.hpp b/cucumber_cpp/library/assemble/AssembledTestCase.hpp new file mode 100644 index 00000000..abcbe947 --- /dev/null +++ b/cucumber_cpp/library/assemble/AssembledTestCase.hpp @@ -0,0 +1,16 @@ +#ifndef ASSEMBLE_ASSEMBLED_TEST_CASE_HPP +#define ASSEMBLE_ASSEMBLED_TEST_CASE_HPP + +#include "cucumber/messages/pickle.hpp" +#include "cucumber/messages/test_case.hpp" + +namespace cucumber_cpp::library::assemble +{ + struct AssembledTestCase + { + const cucumber::messages::pickle& pickle; + const cucumber::messages::test_case testCase; + }; +} + +#endif diff --git a/cucumber_cpp/library/assemble/AssembledTestSuite.hpp b/cucumber_cpp/library/assemble/AssembledTestSuite.hpp new file mode 100644 index 00000000..814fcc3e --- /dev/null +++ b/cucumber_cpp/library/assemble/AssembledTestSuite.hpp @@ -0,0 +1,17 @@ +#ifndef ASSEMBLE_ASSEMBLED_TEST_SUITE_HPP +#define ASSEMBLE_ASSEMBLED_TEST_SUITE_HPP + +#include "cucumber/messages/gherkin_document.hpp" +#include "cucumber_cpp/library/assemble/AssembledTestCase.hpp" +#include + +namespace cucumber_cpp::library::assemble +{ + struct AssembledTestSuite + { + const cucumber::messages::gherkin_document& gherkinDocument; + std::list testCases; + }; +} + +#endif diff --git a/cucumber_cpp/library/assemble/CMakeLists.txt b/cucumber_cpp/library/assemble/CMakeLists.txt new file mode 100644 index 00000000..1c038f88 --- /dev/null +++ b/cucumber_cpp/library/assemble/CMakeLists.txt @@ -0,0 +1,23 @@ +add_library(cucumber_cpp.library.assemble ${CCR_EXCLUDE_FROM_ALL}) + +target_sources(cucumber_cpp.library.assemble PRIVATE + AssembledTestCase.hpp + AssembledTestSuite.hpp + AssembleTestSuites.cpp + AssembleTestSuites.hpp +) + +target_include_directories(cucumber_cpp.library.assemble PUBLIC + ../../.. +) + +target_link_libraries(cucumber_cpp.library.assemble PUBLIC + cucumber_cpp.library.cucumber_expression + cucumber_cpp.library.support + cucumber_cpp.library.util +) + +if (CCR_BUILD_TESTS) + add_subdirectory(test) + # add_subdirectory(test_helper) +endif() diff --git a/cucumber_cpp/library/assemble/test/CMakeLists.txt b/cucumber_cpp/library/assemble/test/CMakeLists.txt new file mode 100644 index 00000000..d837451e --- /dev/null +++ b/cucumber_cpp/library/assemble/test/CMakeLists.txt @@ -0,0 +1,12 @@ +add_executable(cucumber_cpp.library.assemble.test) +add_test(NAME cucumber_cpp.library.assemble.test COMMAND cucumber_cpp.library.assemble.test) + +target_link_libraries(cucumber_cpp.library.assemble.test PUBLIC + gmock_main + cucumber_cpp.library.assemble + GTest::gmock +) + +target_sources(cucumber_cpp.library.assemble.test PRIVATE + TestDummy.cpp +) diff --git a/cucumber_cpp/library/assemble/test/TestDummy.cpp b/cucumber_cpp/library/assemble/test/TestDummy.cpp new file mode 100644 index 00000000..c81abaf1 --- /dev/null +++ b/cucumber_cpp/library/assemble/test/TestDummy.cpp @@ -0,0 +1,8 @@ +#include + +namespace cucumber_cpp::library::support +{ + TEST(Dummy, dummy) + { + } +} diff --git a/cucumber_cpp/library/cucumber_expression/Argument.cpp b/cucumber_cpp/library/cucumber_expression/Argument.cpp new file mode 100644 index 00000000..1f872143 --- /dev/null +++ b/cucumber_cpp/library/cucumber_expression/Argument.cpp @@ -0,0 +1,45 @@ +#include "cucumber_cpp/library/cucumber_expression/Argument.hpp" +#include "cucumber/messages/group.hpp" +#include "cucumber_cpp/library/cucumber_expression/ParameterRegistry.hpp" +#include "fmt/format.h" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace cucumber_cpp::library::cucumber_expression +{ + Argument::Argument(cucumber::messages::group group, const Parameter& parameter) + : group{ std::move(group) } + , parameter{ parameter } + {} + + std::vector Argument::BuildArguments(const cucumber::messages::group& group, std::span parameters) + { + if (group.children.size() != parameters.size()) + throw std::runtime_error(fmt::format("Mismatch between number of groups ({}) and parameters ({})", group.children.size(), parameters.size())); + + std::size_t index{ 0 }; + auto converted = parameters | std::views::transform([&group, &index](const Parameter& parameter) -> Argument + { + return { group.children[index++], parameter }; + }); + + return { converted.begin(), converted.end() }; + } + + cucumber::messages::group Argument::Group() const + { + return group; + } + + std::string Argument::Name() const + { + return parameter.name; + } + +} diff --git a/cucumber_cpp/library/cucumber_expression/Argument.hpp b/cucumber_cpp/library/cucumber_expression/Argument.hpp new file mode 100644 index 00000000..a4bd55a5 --- /dev/null +++ b/cucumber_cpp/library/cucumber_expression/Argument.hpp @@ -0,0 +1,34 @@ +#ifndef CUCUMBER_EXPRESSION_ARGUMENT_HPP +#define CUCUMBER_EXPRESSION_ARGUMENT_HPP + +#include "cucumber/messages/group.hpp" +#include "cucumber_cpp/library/cucumber_expression/ParameterRegistry.hpp" +#include +#include + +namespace cucumber_cpp::library::cucumber_expression +{ + struct Argument + { + private: + Argument(cucumber::messages::group group, const Parameter& parameter); + + public: + static std::vector BuildArguments(const cucumber::messages::group& group, std::span parameters); + + template + T GetValue() const + { + return ConverterTypeMap::Instance().at(parameter.name)(group); + } + + cucumber::messages::group Group() const; + std::string Name() const; + + private: + cucumber::messages::group group; + const Parameter& parameter; + }; +} + +#endif diff --git a/cucumber_cpp/library/cucumber_expression/CMakeLists.txt b/cucumber_cpp/library/cucumber_expression/CMakeLists.txt index c68f546c..66002367 100644 --- a/cucumber_cpp/library/cucumber_expression/CMakeLists.txt +++ b/cucumber_cpp/library/cucumber_expression/CMakeLists.txt @@ -1,6 +1,8 @@ -add_library(cucumber_cpp.library.cucumber_expression STATIC ${CCR_EXCLUDE_FROM_ALL}) +add_library(cucumber_cpp.library.cucumber_expression ${CCR_EXCLUDE_FROM_ALL}) target_sources(cucumber_cpp.library.cucumber_expression PRIVATE + Argument.cpp + Argument.hpp Ast.cpp Ast.hpp Errors.cpp @@ -11,14 +13,24 @@ target_sources(cucumber_cpp.library.cucumber_expression PRIVATE ExpressionParser.hpp ExpressionTokenizer.cpp ExpressionTokenizer.hpp + MatchRange.cpp + MatchRange.hpp ParameterRegistry.cpp ParameterRegistry.hpp + RegularExpression.hpp + TreeRegexp.cpp + TreeRegexp.hpp ) target_include_directories(cucumber_cpp.library.cucumber_expression PUBLIC ../../.. ) +target_link_libraries(cucumber_cpp.library.cucumber_expression PUBLIC + cucumber_gherkin_lib + fmt::fmt +) + if (CCR_BUILD_TESTS) add_subdirectory(test) endif() diff --git a/cucumber_cpp/library/cucumber_expression/Errors.cpp b/cucumber_cpp/library/cucumber_expression/Errors.cpp index d16500a7..47b0f1c3 100644 --- a/cucumber_cpp/library/cucumber_expression/Errors.cpp +++ b/cucumber_cpp/library/cucumber_expression/Errors.cpp @@ -1,12 +1,13 @@ #include "cucumber_cpp/library/cucumber_expression/Errors.hpp" #include "cucumber_cpp/library/cucumber_expression/Ast.hpp" +#include "fmt/format.h" #include -#include #include #include #include #include #include +#include namespace cucumber_cpp::library::cucumber_expression { @@ -28,7 +29,7 @@ namespace cucumber_cpp::library::cucumber_expression Error::Error(std::size_t column, std::string_view expression, std::string_view pointer, std::string_view problem, std::string_view solution) : std::runtime_error{ - std::format( + fmt::format( "This Cucumber Expression has a problem at column {}:\n" "\n" "{}\n" @@ -89,14 +90,14 @@ Otherwise rephrase your expression or consider using a regular expression instea token.Start(), expression, PointAtLocated(token), - std::format(R"(The '{}' does not have a matching '{}')", Token::SymbolOf(beginToken), Token::SymbolOf(endToken)), - std::format(R"(If you did not intend to use {} you can use '\\{}' to escape the {})", Token::PurposeOf(beginToken), Token::SymbolOf(beginToken), Token::PurposeOf(beginToken)), + fmt::format(R"(The '{}' does not have a matching '{}')", Token::SymbolOf(beginToken), Token::SymbolOf(endToken)), + fmt::format(R"(If you did not intend to use {} you can use '\\{}' to escape the {})", Token::PurposeOf(beginToken), Token::SymbolOf(beginToken), Token::PurposeOf(beginToken)), } {} NoEligibleParsers::NoEligibleParsers(std::span tokens) : std::runtime_error{ - std::format("No eligible parsers for [{}]", std::accumulate(tokens.begin() + 1, tokens.end(), Token::NameOf(tokens.begin()->Type()), + fmt::format("No eligible parsers for [{}]", std::accumulate(tokens.begin() + 1, tokens.end(), Token::NameOf(tokens.begin()->Type()), [](const auto& acc, const auto& token) -> std::string { return acc + ", " + Token::NameOf(token.Type()); @@ -155,13 +156,15 @@ For more complicated expressions consider using a regular expression instead.)", } {} - UndefinedParameterTypeError::UndefinedParameterTypeError(const Node& node, std::string_view expression, std::string_view undefinedParameterName) + UndefinedParameterTypeError::UndefinedParameterTypeError(const Node& node, std::string expression, std::string undefinedParameterName) : Error{ node.Start(), expression, PointAtLocated(node), - std::format(R"(Undefined parameter type '{}')", undefinedParameterName), - std::format(R"(Please register a ParameterType for '{}')", undefinedParameterName), + fmt::format(R"(Undefined parameter type '{}')", undefinedParameterName), + fmt::format(R"(Please register a ParameterType for '{}')", undefinedParameterName), } + , expression{ std::move(expression) } + , undefinedParameterName{ std::move(undefinedParameterName) } {} } diff --git a/cucumber_cpp/library/cucumber_expression/Errors.hpp b/cucumber_cpp/library/cucumber_expression/Errors.hpp index 61e3d607..7ed9a217 100644 --- a/cucumber_cpp/library/cucumber_expression/Errors.hpp +++ b/cucumber_cpp/library/cucumber_expression/Errors.hpp @@ -5,6 +5,7 @@ #include #include #include +#include #include namespace cucumber_cpp::library::cucumber_expression @@ -90,7 +91,10 @@ namespace cucumber_cpp::library::cucumber_expression struct UndefinedParameterTypeError : Error { - UndefinedParameterTypeError(const Node& node, std::string_view expression, std::string_view undefinedParameterName); + UndefinedParameterTypeError(const Node& node, std::string expression, std::string undefinedParameterName); + + std::string expression; + std::string undefinedParameterName; }; struct InvalidTokenType diff --git a/cucumber_cpp/library/cucumber_expression/Expression.cpp b/cucumber_cpp/library/cucumber_expression/Expression.cpp index b578eaff..9be0a3c7 100644 --- a/cucumber_cpp/library/cucumber_expression/Expression.cpp +++ b/cucumber_cpp/library/cucumber_expression/Expression.cpp @@ -1,17 +1,14 @@ #include "cucumber_cpp/library/cucumber_expression/Expression.hpp" +#include "cucumber_cpp/library/cucumber_expression/Argument.hpp" #include "cucumber_cpp/library/cucumber_expression/Ast.hpp" #include "cucumber_cpp/library/cucumber_expression/Errors.hpp" #include "cucumber_cpp/library/cucumber_expression/ExpressionParser.hpp" #include "cucumber_cpp/library/cucumber_expression/ParameterRegistry.hpp" +#include "fmt/format.h" #include -#include -#include -#include -#include #include #include -#include #include #include #include @@ -24,8 +21,9 @@ namespace cucumber_cpp::library::cucumber_expression : expression{ std::move(expression) } , parameterRegistry{ parameterRegistry } , pattern{ RewriteToRegex(ExpressionParser{}.Parse(this->expression)) } - , regex{ pattern } - {} + , treeRegexp{ pattern } + { + } std::string_view Expression::Source() const { @@ -37,27 +35,13 @@ namespace cucumber_cpp::library::cucumber_expression return pattern; } - std::optional> Expression::Match(const std::string& text) const + std::optional> Expression::MatchToArguments(const std::string& text) const { - std::smatch smatch; - if (!std::regex_search(text, smatch, regex)) + auto group = treeRegexp.MatchToGroup(text); + if (!group.has_value()) return std::nullopt; - std::vector result; - result.reserve(converters.size()); - - auto converterIter = converters.begin(); - auto matchIter = smatch.begin() + 1; - - while (matchIter != smatch.end() && converterIter != converters.end()) - { - result.emplace_back(converterIter->converter({ matchIter, matchIter + converterIter->matches })); - - matchIter = std::next(matchIter, converterIter->matches); - converterIter = std::next(converterIter); - } - - return result; + return Argument::BuildArguments(group.value(), parameters); } std::string Expression::RewriteToRegex(const Node& node) @@ -114,7 +98,7 @@ namespace cucumber_cpp::library::cucumber_expression for (const auto& child : node.Children()) partialRegex += RewriteToRegex(child); - return std::format(R"((?:{})?)", partialRegex); + return fmt::format(R"((?:{})?)", partialRegex); } std::string Expression::RewriteAlternation(const Node& node) @@ -133,7 +117,7 @@ namespace cucumber_cpp::library::cucumber_expression for (const auto& child : node.Children() | std::views::drop(1)) partialRegex += '|' + RewriteToRegex(child); - return std::format(R"((?:{}))", partialRegex); + return fmt::format(R"((?:{}))", partialRegex); } std::string Expression::RewriteAlternative(const Node& node) @@ -148,25 +132,30 @@ namespace cucumber_cpp::library::cucumber_expression std::string Expression::RewriteParameter(const Node& node) { - auto parameter = parameterRegistry.Lookup(node.Text()); - if (parameter.regex.empty()) - throw UndefinedParameterTypeError(node, expression, node.Text()); - - converters.emplace_back(0u, parameter.converter); - - std::string partialRegex{}; - if (parameter.regex.size() == 1) - partialRegex = std::format(R"(({}))", parameter.regex.front()); - else + try { - partialRegex = { parameter.regex.front() }; - for (const auto& parameterRegex : parameter.regex | std::views::drop(1)) - partialRegex += R"()|(?:)" + parameterRegex; - partialRegex = std::format(R"(((?:{})))", partialRegex); + auto parameter = parameterRegistry.Lookup(node.Text()); + if (parameter.regex.empty()) + throw UndefinedParameterTypeError(node, expression, node.Text()); + + parameters.push_back(parameter); + + std::string partialRegex{}; + if (parameter.regex.size() == 1) + partialRegex = fmt::format(R"(({}))", parameter.regex.front()); + else + { + partialRegex = { parameter.regex.front() }; + for (const auto& parameterRegex : parameter.regex | std::views::drop(1)) + partialRegex += R"()|(?:)" + parameterRegex; + partialRegex = fmt::format(R"(((?:{})))", partialRegex); + } + return partialRegex; + } + catch (const std::out_of_range&) + { + throw UndefinedParameterTypeError(node, expression, node.Text()); } - - converters.back().matches += std::regex{ partialRegex }.mark_count(); - return partialRegex; } std::string Expression::RewriteExpression(const Node& node) @@ -176,7 +165,7 @@ namespace cucumber_cpp::library::cucumber_expression for (const auto& child : node.Children()) partialRegex += RewriteToRegex(child); - return std::format("^{}$", partialRegex); + return fmt::format("^{}$", partialRegex); } std::string Expression::CreateEmptyRegexString(const Node& node) const diff --git a/cucumber_cpp/library/cucumber_expression/Expression.hpp b/cucumber_cpp/library/cucumber_expression/Expression.hpp index b07fbeab..44d5e6d3 100644 --- a/cucumber_cpp/library/cucumber_expression/Expression.hpp +++ b/cucumber_cpp/library/cucumber_expression/Expression.hpp @@ -1,15 +1,14 @@ #ifndef CUCUMBER_EXPRESSION_EXPRESSION_HPP #define CUCUMBER_EXPRESSION_EXPRESSION_HPP +#include "cucumber_cpp/library/cucumber_expression/Argument.hpp" #include "cucumber_cpp/library/cucumber_expression/Ast.hpp" #include "cucumber_cpp/library/cucumber_expression/ParameterRegistry.hpp" -#include +#include "cucumber_cpp/library/cucumber_expression/TreeRegexp.hpp" #include #include -#include #include #include -#include #include #include #include @@ -22,10 +21,12 @@ namespace cucumber_cpp::library::cucumber_expression std::string_view Source() const; std::string_view Pattern() const; - std::optional> Match(const std::string& text) const; + + std::optional> MatchToArguments(const std::string& text) const; private: - std::string RewriteToRegex(const Node& node); + std::string + RewriteToRegex(const Node& node); std::string EscapeRegex(std::string_view text) const; std::string RewriteOptional(const Node& node); std::string RewriteAlternation(const Node& node); @@ -51,9 +52,10 @@ namespace cucumber_cpp::library::cucumber_expression std::string expression; ParameterRegistry& parameterRegistry; - std::vector converters; + std::vector parameters; std::string pattern; - std::regex regex; + + TreeRegexp treeRegexp; }; } diff --git a/cucumber_cpp/library/cucumber_expression/MatchRange.cpp b/cucumber_cpp/library/cucumber_expression/MatchRange.cpp new file mode 100644 index 00000000..9394c9b0 --- /dev/null +++ b/cucumber_cpp/library/cucumber_expression/MatchRange.cpp @@ -0,0 +1,23 @@ + +#include "cucumber_cpp/library/cucumber_expression/MatchRange.hpp" +#include +#include + +namespace cucumber_cpp::library::cucumber_expression +{ + + std::smatch::const_iterator MatchRange::begin() const + { + return first; + } + + std::smatch::const_iterator MatchRange::end() const + { + return second; + } + + const std::ssub_match& MatchRange::operator[](std::size_t index) const + { + return *std::next(begin(), index); + } +} diff --git a/cucumber_cpp/library/cucumber_expression/MatchRange.hpp b/cucumber_cpp/library/cucumber_expression/MatchRange.hpp new file mode 100644 index 00000000..c8936a85 --- /dev/null +++ b/cucumber_cpp/library/cucumber_expression/MatchRange.hpp @@ -0,0 +1,23 @@ +#ifndef CUCUMBER_EXPRESSION_MATCH_RANGE_HPP +#define CUCUMBER_EXPRESSION_MATCH_RANGE_HPP + +// IWYU pragma: private, include "cucumber_cpp/CucumberCpp.hpp" +// IWYU pragma: friend cucumber_cpp/.* + +#include +#include + +namespace cucumber_cpp::library::cucumber_expression +{ + struct MatchRange : std::pair + { + using std::pair::pair; + + std::smatch::const_iterator begin() const; + std::smatch::const_iterator end() const; + + const std::ssub_match& operator[](std::size_t index) const; + }; +} + +#endif diff --git a/cucumber_cpp/library/cucumber_expression/Matcher.hpp b/cucumber_cpp/library/cucumber_expression/Matcher.hpp index 214611be..2a98c6ba 100644 --- a/cucumber_cpp/library/cucumber_expression/Matcher.hpp +++ b/cucumber_cpp/library/cucumber_expression/Matcher.hpp @@ -1,13 +1,12 @@ #ifndef CUCUMBER_EXPRESSION_MATCHER_HPP #define CUCUMBER_EXPRESSION_MATCHER_HPP +#include "cucumber_cpp/library/cucumber_expression/Argument.hpp" #include "cucumber_cpp/library/cucumber_expression/Expression.hpp" #include "cucumber_cpp/library/cucumber_expression/RegularExpression.hpp" -#include #include #include #include -#include #include #include @@ -33,9 +32,9 @@ namespace cucumber_cpp::library::cucumber_expression struct MatchVisitor { - std::optional, std::vector>> operator()(const auto& expression) const + std::optional> operator()(const auto& expression) const { - return expression.Match(text); + return expression.MatchToArguments(text); } const std::string& text; diff --git a/cucumber_cpp/library/cucumber_expression/ParameterRegistry.cpp b/cucumber_cpp/library/cucumber_expression/ParameterRegistry.cpp index 670e86f2..05aeea97 100644 --- a/cucumber_cpp/library/cucumber_expression/ParameterRegistry.cpp +++ b/cucumber_cpp/library/cucumber_expression/ParameterRegistry.cpp @@ -1,18 +1,19 @@ - #include "cucumber_cpp/library/cucumber_expression/ParameterRegistry.hpp" +#include "cucumber/messages/group.hpp" #include "cucumber_cpp/library/cucumber_expression/Errors.hpp" -#include +#include "cucumber_cpp/library/support/SupportCodeLibrary.hpp" +#include "fmt/format.h" #include -#include +#include #include #include -#include #include -#include #include +#include #include +#include #include -#include +#include #include namespace cucumber_cpp::library::cucumber_expression @@ -20,88 +21,99 @@ namespace cucumber_cpp::library::cucumber_expression namespace { template - std::function CreateStreamConverter() + std::function CreateStreamConverter() { - return [](const MatchRange& matches) + return [](const cucumber::messages::group& matches) -> T { - return StringTo(matches.begin()->str()); + if (matches.value.has_value()) + return StringTo(matches.value.value()); + return {}; }; - } + }; - std::function CreateStringConverter() + std::function CreateStringConverter() { - return [](const MatchRange& matches) + return [](const cucumber::messages::group& matches) { - std::string str = matches[1].matched ? matches[1].str() : matches[3].str(); + std::string str = matches.children.front().value.has_value() + ? matches.children.front().value.value() + : matches.children.back().value.value(); + str = std::regex_replace(str, std::regex(R"__(\\")__"), "\""); str = std::regex_replace(str, std::regex(R"__(\\')__"), "'"); + return str; }; } - } - std::smatch::const_iterator MatchRange::begin() const - { - return first; - } - - std::smatch::const_iterator MatchRange::end() const - { - return second; + std::function CreateByteConverter() + { + return [](const cucumber::messages::group& matches) -> std::int8_t + { + if (matches.value.has_value()) + return static_cast(StringTo(matches.value.value())); + return {}; + }; + } } - const std::ssub_match& MatchRange::operator[](std::size_t index) const + std::strong_ordering CustomParameterEntry::operator<=>(const CustomParameterEntry& other) const { - return *std::next(begin(), index); + return std::tie(params.name, params.regex) <=> std::tie(other.params.name, other.params.regex); } - Converter::Converter(std::size_t matches, std::function converter) - : matches{ matches } - , converter{ std::move(converter) } - {} - - ParameterRegistry::ParameterRegistry() + ParameterRegistry::ParameterRegistry(const std::set>& customParameters) { const static std::string integerNegativeRegex{ R"__(-?\d+)__" }; const static std::string integerPositiveRegex{ R"__(\d+)__" }; const static std::string floatRegex{ R"__((?=.*\d.*)[-+]?\d*(?:\.(?=\d.*))?\d*(?:\d+[E][+-]?\d+)?)__" }; - const static std::string stringDoubleRegex{ R"__("([^\"\\]*(\\.[^\"\\]*)*)")__" }; - const static std::string stringSingleRegex{ R"__('([^'\\]*(\\.[^'\\]*)*)')__" }; + const static std::string stringRegex{ R"__("([^"\\]*(\\.[^"\\]*)*)"|'([^'\\]*(\\.[^'\\]*)*)')__" }; const static std::string wordRegex{ R"__([^\s]+)__" }; - AddParameter("int", { integerNegativeRegex, integerPositiveRegex }, CreateStreamConverter()); - AddParameter("float", { floatRegex }, CreateStreamConverter()); - AddParameter("word", { wordRegex }, CreateStreamConverter()); - AddParameter("string", { stringDoubleRegex, stringSingleRegex }, CreateStringConverter()); - AddParameter("", { ".*" }, CreateStreamConverter()); - AddParameter("bigdecimal", { floatRegex }, CreateStreamConverter()); - AddParameter("biginteger", { { integerNegativeRegex, integerPositiveRegex } }, CreateStreamConverter()); - AddParameter("byte", { { integerNegativeRegex, integerPositiveRegex } }, CreateStreamConverter()); - AddParameter("short", { { integerNegativeRegex, integerPositiveRegex } }, CreateStreamConverter()); - AddParameter("long", { { integerNegativeRegex, integerPositiveRegex } }, CreateStreamConverter()); - AddParameter("double", { floatRegex }, CreateStreamConverter()); + AddBuiltinParameter("int", { integerNegativeRegex, integerPositiveRegex }, CreateStreamConverter()); + AddBuiltinParameter("float", { floatRegex }, CreateStreamConverter()); + AddBuiltinParameter("word", { wordRegex }, CreateStreamConverter()); + AddBuiltinParameter("string", { stringRegex }, CreateStringConverter()); + AddBuiltinParameter("", { ".*" }, CreateStreamConverter()); + AddBuiltinParameter("bigdecimal", { floatRegex }, CreateStreamConverter()); + AddBuiltinParameter("biginteger", { { integerNegativeRegex, integerPositiveRegex } }, CreateStreamConverter()); + AddBuiltinParameter("byte", { { integerNegativeRegex, integerPositiveRegex } }, CreateByteConverter()); + AddBuiltinParameter("short", { { integerNegativeRegex, integerPositiveRegex } }, CreateStreamConverter()); + AddBuiltinParameter("long", { { integerNegativeRegex, integerPositiveRegex } }, CreateStreamConverter()); + AddBuiltinParameter("double", { floatRegex }, CreateStreamConverter()); // extension - AddParameter("bool", { wordRegex }, CreateStreamConverter()); + AddBuiltinParameter("bool", { wordRegex }, CreateStreamConverter()); + + for (const auto& parameter : customParameters) + AddParameter(Parameter{ parameter.params.name, { std::string(parameter.params.regex) }, false, parameter.params.useForSnippets, parameter.location }); + } + + const std::map& ParameterRegistry::GetParameters() const + { + return parametersByName; } - Parameter ParameterRegistry::Lookup(const std::string& name) const + const Parameter& ParameterRegistry::Lookup(const std::string& name) const { - if (parameters.contains(name)) - return parameters.at(name); - return {}; + return parametersByName.at(name); } - void ParameterRegistry::AddParameter(std::string name, std::vector regex, std::function converter) + void ParameterRegistry::AssertParameterIsUnique(const std::string& name) const { - if (parameters.contains(name)) + if (parametersByName.contains(name)) { if (name.empty()) throw CucumberExpressionError{ "The anonymous parameter type has already been defined" }; else - throw CucumberExpressionError{ std::format("There is already a parameter with name {}", name) }; + throw CucumberExpressionError{ fmt::format("There is already a parameter with name {}", name) }; } + } + + void ParameterRegistry::AddParameter(Parameter parameter) + { + AssertParameterIsUnique(parameter.name); - parameters[name] = Parameter{ name, std::move(regex), std::move(converter) }; + parametersByName.emplace(parameter.name, parameter); } } diff --git a/cucumber_cpp/library/cucumber_expression/ParameterRegistry.hpp b/cucumber_cpp/library/cucumber_expression/ParameterRegistry.hpp index fcd2c44b..cf65eb73 100644 --- a/cucumber_cpp/library/cucumber_expression/ParameterRegistry.hpp +++ b/cucumber_cpp/library/cucumber_expression/ParameterRegistry.hpp @@ -1,28 +1,46 @@ #ifndef CUCUMBER_EXPRESSION_PARAMETERREGISTRY_HPP #define CUCUMBER_EXPRESSION_PARAMETERREGISTRY_HPP +#include "cucumber/messages/group.hpp" +#include "fmt/format.h" #include -#include #include +#include #include #include #include -#include #include -#include #include -#include +#include +#include #include #include #include #include -#include #include namespace cucumber_cpp::library::cucumber_expression { using namespace std::literals; + struct CustomParameterEntryParams + { + std::string name; + std::string regex; + bool useForSnippets; + }; + + struct CustomParameterEntry + { + CustomParameterEntryParams params; + + std::size_t localId; + + std::source_location location; + + std::strong_ordering operator<=>(const CustomParameterEntry& other) const; + }; + struct ConversionError : std::runtime_error { using std::runtime_error::runtime_error; @@ -38,7 +56,7 @@ namespace cucumber_cpp::library::cucumber_expression To to{}; stream >> to; if (stream.fail()) - throw ConversionError{ std::format("Cannot convert parameter {} in to {}", s, typeid(To).name()) }; + throw ConversionError{ fmt::format("Cannot convert parameter {} in to {}", s, typeid(To).name()) }; return to; } @@ -95,52 +113,79 @@ namespace cucumber_cpp::library::cucumber_expression return iequals(s, "true") || iequals(s, "1") || iequals(s, "yes") || iequals(s, "on") || iequals(s, "enabled") || iequals(s, "active"); } - struct MatchRange : std::pair - { - using std::pair::pair; - - std::smatch::const_iterator begin() const; - std::smatch::const_iterator end() const; - - const std::ssub_match& operator[](std::size_t index) const; - }; - - struct Converter - { - Converter(std::size_t matches, std::function converter); - - std::size_t matches; - std::function converter; - }; - struct Parameter { std::string name; std::vector regex; - std::function converter; + bool isBuiltin{ false }; + bool useForSnippets{ false }; + std::source_location location; }; - struct ParameterRegistration - { - protected: - ~ParameterRegistration() = default; + template + using TypeMap = std::map>; - public: - virtual void AddParameter(std::string name, std::vector regex, std::function converter) = 0; + template + struct ConverterTypeMap + { + static std::map>& Instance(); }; - struct ParameterRegistry : ParameterRegistration + template + std::map>& ConverterTypeMap::Instance() { - ParameterRegistry(); + static std::map> typeMap; + return typeMap; + } + + struct ParameterRegistry + { + explicit ParameterRegistry(const std::set>& customParameters); virtual ~ParameterRegistry() = default; - Parameter Lookup(const std::string& name) const; - void AddParameter(std::string name, std::vector regex, std::function converter) override; + const std::map& GetParameters() const; + + const Parameter& Lookup(const std::string& name) const; + + template + void AddParameter(std::string name, std::vector regex, std::function converter, std::source_location location = std::source_location::current()); + + void AssertParameterIsUnique(const std::string& name) const; private: - std::map> parameters{}; + void AddParameter(Parameter parameter); + + template + void AddBuiltinParameter(std::string name, std::vector regex, std::function converter, std::source_location location = std::source_location::current()); + + template + void AddParameter(Parameter parameter, std::function converter); + + std::map parametersByName; }; + + template + void ParameterRegistry::AddParameter(std::string name, std::vector regex, std::function converter, std::source_location location) + { + AddParameter(Parameter{ name, regex, false, false, location }, converter); + } + + template + void ParameterRegistry::AddBuiltinParameter(std::string name, std::vector regex, std::function converter, std::source_location location) + { + AddParameter(Parameter{ name, regex, true, false, location }, converter); + } + + template + void ParameterRegistry::AddParameter(Parameter parameter, std::function converter) + { + AssertParameterIsUnique(parameter.name); + + AddParameter(parameter); + + ConverterTypeMap::Instance().emplace(parameter.name, converter); + } } #endif diff --git a/cucumber_cpp/library/cucumber_expression/RegularExpression.hpp b/cucumber_cpp/library/cucumber_expression/RegularExpression.hpp index 271b1b3f..ee69854d 100644 --- a/cucumber_cpp/library/cucumber_expression/RegularExpression.hpp +++ b/cucumber_cpp/library/cucumber_expression/RegularExpression.hpp @@ -1,6 +1,11 @@ #ifndef CUCUMBER_EXPRESSION_REGULAREXPRESSION_HPP #define CUCUMBER_EXPRESSION_REGULAREXPRESSION_HPP +#include "cucumber/messages/group.hpp" +#include "cucumber/messages/step_match_arguments_list.hpp" +#include "cucumber_cpp/library/cucumber_expression/Argument.hpp" +#include "cucumber_cpp/library/cucumber_expression/ParameterRegistry.hpp" +#include "cucumber_cpp/library/cucumber_expression/TreeRegexp.hpp" #include #include #include @@ -13,10 +18,18 @@ namespace cucumber_cpp::library::cucumber_expression { struct RegularExpression { - explicit RegularExpression(std::string expression) + explicit RegularExpression(std::string expression, ParameterRegistry& parameterRegistry) : expression{ std::move(expression) } , regex{ this->expression } - {} + , treeRegexp{ this->expression } + { + auto parameterIters = treeRegexp.RootBuilder().Children() | std::views::transform([¶meterRegistry](const GroupBuilder& groupBuilder) -> Parameter + { + return parameterRegistry.Lookup(""); + }); + + parameters = { parameterIters.begin(), parameterIters.end() }; + } std::string_view Source() const { @@ -28,24 +41,21 @@ namespace cucumber_cpp::library::cucumber_expression return expression; } - std::optional> Match(const std::string& text) const + std::optional> MatchToArguments(const std::string& text) const { - std::smatch smatch; - if (!std::regex_search(text, smatch, regex)) + auto group = treeRegexp.MatchToGroup(text); + if (!group.has_value()) return std::nullopt; - std::vector result{}; - result.reserve(smatch.size() - 1); - - for (const auto& match : smatch | std::views::drop(1)) - result.emplace_back(match.str()); - - return result; + return Argument::BuildArguments(group.value(), parameters); } private: std::string expression; std::regex regex; + + TreeRegexp treeRegexp; + std::vector parameters; }; } diff --git a/cucumber_cpp/library/cucumber_expression/TreeRegexp.cpp b/cucumber_cpp/library/cucumber_expression/TreeRegexp.cpp new file mode 100644 index 00000000..029f7eb2 --- /dev/null +++ b/cucumber_cpp/library/cucumber_expression/TreeRegexp.cpp @@ -0,0 +1,200 @@ +#include "cucumber_cpp/library/cucumber_expression/TreeRegexp.hpp" +#include "cucumber/messages/group.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace cucumber_cpp::library::cucumber_expression +{ + namespace + { + bool IsNonCapturing(std::string_view pattern, std::size_t pos) + { + if (pattern[pos + 1] != '?') + return false; + if (pattern[pos + 2] != '<') + return true; + + return pattern[pos + 3] == '=' || pattern[pos + 3] == '!'; + } + + void StartGroup(std::deque& stack, std::deque& groupStartStack, std::string_view pattern, std::size_t patternIndex) + { + groupStartStack.emplace_back(patternIndex); + auto& groupBuilder = stack.emplace_back(); + if (IsNonCapturing(pattern, patternIndex)) + groupBuilder.SetNonCapturing(); + } + + void FinalizeGroup(std::deque& stack, std::deque& groupStartStack, std::string_view pattern, std::size_t patternIndex) + { + if (stack.empty()) + throw std::runtime_error("Empty stack"); + + auto groupBuilder = stack.back(); + stack.pop_back(); + + auto groupStart = groupStartStack.empty() ? 0 : groupStartStack.back(); + groupStart += 1; + + if (!groupStartStack.empty()) + groupStartStack.pop_back(); + + if (groupBuilder.IsCapturing()) + { + groupBuilder.SetPattern(pattern.substr(groupStart, patternIndex - groupStart)); + stack.back().Add(groupBuilder); + } + else + groupBuilder.MoveChildrenTo(stack.back()); + } + + struct PatternGroupParser + { + enum class State + { + nonGroup, + groupStart, + groupClose + }; + + State Parse(char c) + { + State state{}; + + if (c == '[' && !escaping) + charClass = true; + else if (c == ']' && !escaping) + charClass = false; + else if (c == '(' && !escaping && !charClass) + state = State::groupStart; + else if (c == ')' && !escaping && !charClass) + state = State::groupClose; + + escaping = (c == '\\' && !escaping); + + return state; + } + + private: + bool escaping{ false }; + bool charClass{ false }; + }; + } + + void GroupBuilder::Add(GroupBuilder groupBuilder) + { + children.push_back(std::move(groupBuilder)); + } + + void GroupBuilder::SetNonCapturing() + { + capturing = false; + } + + bool GroupBuilder::IsCapturing() const + { + return capturing; + } + + void GroupBuilder::SetPattern(std::string_view pattern) + { + this->pattern = pattern; + } + + void GroupBuilder::MoveChildrenTo(GroupBuilder& target) + { + for (auto& child : children) + target.Add(std::move(child)); + children.clear(); + } + + const std::list& GroupBuilder::Children() const + { + return children; + } + + std::string_view GroupBuilder::Pattern() const + { + return pattern; + } + + cucumber::messages::group GroupBuilder::Build(const std::smatch& match, std::size_t& index) const + { + const auto groupIndex = index++; + const auto children = this->children | std::views::transform([&match, &index](const auto& child) + { + return child.Build(match, index); + }); + const auto value = match[groupIndex].matched ? std::make_optional(match[groupIndex].str()) : std::nullopt; + + return { + .children = std::vector(children.begin(), children.end()), + .start = match[groupIndex].matched ? std::make_optional(match.position(groupIndex)) : std::nullopt, + .value = value, + }; + } + + TreeRegexp::TreeRegexp(std::string_view pattern) + : rootGroupBuilder{ CreateGroupBuilder(pattern) } + , regex{ std::string(pattern) } + { + } + + const GroupBuilder& TreeRegexp::RootBuilder() const + { + return rootGroupBuilder; + } + + std::optional TreeRegexp::MatchToGroup(const std::string& text) const + { + std::smatch match; + if (!std::regex_search(text, match, regex)) + return std::nullopt; + + std::size_t index = 0; + return rootGroupBuilder.Build(match, index); + } + + GroupBuilder TreeRegexp::CreateGroupBuilder(std::string_view pattern) + { + std::deque stack; + std::deque groupStartStack; + PatternGroupParser patternParser; + + stack.emplace_back(); + + for (std::size_t i = 0; i < pattern.size(); ++i) + { + const char c = pattern[i]; + + switch (patternParser.Parse(c)) + { + case PatternGroupParser::State::groupStart: + StartGroup(stack, groupStartStack, pattern, i); + break; + + case PatternGroupParser::State::groupClose: + FinalizeGroup(stack, groupStartStack, pattern, i); + break; + + case PatternGroupParser::State::nonGroup: + break; + } + } + + if (stack.empty()) + throw std::runtime_error("Empty stack"); + + return stack.back(); + } + +} diff --git a/cucumber_cpp/library/cucumber_expression/TreeRegexp.hpp b/cucumber_cpp/library/cucumber_expression/TreeRegexp.hpp new file mode 100644 index 00000000..453e4299 --- /dev/null +++ b/cucumber_cpp/library/cucumber_expression/TreeRegexp.hpp @@ -0,0 +1,52 @@ +#ifndef CUCUMBER_EXPRESSION_TREE_REGEXP_HPP +#define CUCUMBER_EXPRESSION_TREE_REGEXP_HPP + +#include "cucumber/messages/group.hpp" +#include +#include +#include +#include +#include +#include + +namespace cucumber_cpp::library::cucumber_expression +{ + struct GroupBuilder + { + void Add(GroupBuilder groupBuilder); + + void SetNonCapturing(); + bool IsCapturing() const; + + void SetPattern(std::string_view pattern); + + void MoveChildrenTo(GroupBuilder& target); + + const std::list& Children() const; + std::string_view Pattern() const; + + cucumber::messages::group Build(const std::smatch& match, std::size_t& index) const; + + private: + std::string_view pattern; + bool capturing{ true }; + std::list children; + }; + + struct TreeRegexp + { + explicit TreeRegexp(std::string_view pattern); + + const GroupBuilder& RootBuilder() const; + + std::optional MatchToGroup(const std::string& text) const; + + private: + GroupBuilder CreateGroupBuilder(std::string_view pattern); + + GroupBuilder rootGroupBuilder; + std::regex regex; + }; +} + +#endif diff --git a/cucumber_cpp/library/cucumber_expression/test/CMakeLists.txt b/cucumber_cpp/library/cucumber_expression/test/CMakeLists.txt index 94ffce35..da802baa 100644 --- a/cucumber_cpp/library/cucumber_expression/test/CMakeLists.txt +++ b/cucumber_cpp/library/cucumber_expression/test/CMakeLists.txt @@ -2,10 +2,11 @@ add_executable(cucumber_cpp.library.cucumber_expression.test) add_test(NAME cucumber_cpp.library.cucumber_expression.test COMMAND cucumber_cpp.library.cucumber_expression.test) target_link_libraries(cucumber_cpp.library.cucumber_expression.test PUBLIC - gmock_main + GTest::gmock_main + GTest::gmock + cucumber_cpp.library cucumber_cpp.library.cucumber_expression yaml-cpp::yaml-cpp - GTest::gmock ) target_sources(cucumber_cpp.library.cucumber_expression.test PRIVATE @@ -13,6 +14,7 @@ target_sources(cucumber_cpp.library.cucumber_expression.test PRIVATE TestExpressionParser.cpp TestExpressionTokenizer.cpp TestTransformation.cpp + TestTreeRegexp.cpp ) add_custom_command( diff --git a/cucumber_cpp/library/cucumber_expression/test/TestExpression.cpp b/cucumber_cpp/library/cucumber_expression/test/TestExpression.cpp index a13f0bb7..110a9bfc 100644 --- a/cucumber_cpp/library/cucumber_expression/test/TestExpression.cpp +++ b/cucumber_cpp/library/cucumber_expression/test/TestExpression.cpp @@ -1,24 +1,21 @@ +#include "cucumber/messages/group.hpp" +#include "cucumber_cpp/library/cucumber_expression/Argument.hpp" #include "cucumber_cpp/library/cucumber_expression/Errors.hpp" #include "cucumber_cpp/library/cucumber_expression/Expression.hpp" #include "cucumber_cpp/library/cucumber_expression/ParameterRegistry.hpp" +#include "fmt/format.h" #include "yaml-cpp/node/node.h" #include "yaml-cpp/node/parse.h" #include "yaml-cpp/yaml.h" #include "gmock/gmock.h" -#include #include #include #include #include -#include -#include #include -#include -#include #include #include -#include #include #include #include @@ -38,23 +35,30 @@ namespace cucumber_cpp::library::cucumber_expression return testdata; } - std::string FormatMessage(const YAML::Node& node, const Expression& expression) + std::string FormatTestFailureMessage(const std::string& file, const YAML::Node& node, const Expression& expression) { - return std::format("failed to match {}\n" + return fmt::format("file: {}\n" + "failed to match {}\n" "regex {}\n" "against {}", - node["expression"].as(), expression.Pattern(), node["text"].as()); + file, node["expression"].as(), expression.Pattern(), node["text"].as()); } } struct TestExpression : testing::Test { - ParameterRegistry parameterRegistry{}; + ParameterRegistry parameterRegistry{ {} }; - std::optional> Match(std::string expr, std::string text) + template + std::optional Match(std::string expr, std::string text) { Expression expression{ std::move(expr), parameterRegistry }; - return expression.Match(std::move(text)); + auto args = expression.MatchToArguments(std::move(text)); + + if (!args.has_value()) + return std::nullopt; + else + return args.value()[0].GetValue(); } }; @@ -65,35 +69,62 @@ namespace cucumber_cpp::library::cucumber_expression for (const auto& [file, testdata] : GetTestData(testdataPath)) { if (testdata["exception"]) - ASSERT_ANY_THROW(Match(testdata["expression"].as(), testdata["text"].as())) - << std::format("Test failed for file: {}", file); + ASSERT_ANY_THROW(Match(testdata["expression"].as(), testdata["text"].as())) + << fmt::format("Test failed for file: {}", file); else { if (testdata["expected_args"].IsNull()) - ASSERT_THAT(Match(testdata["expression"].as(), testdata["text"].as()), testing::IsFalse()); + ASSERT_THAT(Match(testdata["expression"].as(), testdata["text"].as()), testing::IsFalse()); else { const auto expression = Expression{ testdata["expression"].as(), parameterRegistry }; - const auto matchOpt = expression.Match(testdata["text"].as()); + const auto matchOpt = expression.MatchToArguments(testdata["text"].as()); + + const auto arguments = expression.MatchToArguments(testdata["text"].as()); - ASSERT_THAT(matchOpt, testing::IsTrue()) << FormatMessage(testdata, expression); + ASSERT_THAT(matchOpt, testing::IsTrue()) << FormatTestFailureMessage(file, testdata, expression); - const auto match = *matchOpt; + const auto& match = *matchOpt; for (std::size_t i = 0; i < testdata["expected_args"].size(); ++i) { - if (match[i].type() == typeid(std::string)) - ASSERT_THAT(std::any_cast(match[i]), testdata["expected_args"][i].as()) << FormatMessage(testdata, expression); - else if (match[i].type() == typeid(std::int32_t)) - ASSERT_THAT(std::any_cast(match[i]), testdata["expected_args"][i].as()) << FormatMessage(testdata, expression); - else if (match[i].type() == typeid(std::int64_t)) - ASSERT_THAT(std::any_cast(match[i]), testdata["expected_args"][i].as()) << FormatMessage(testdata, expression); - else if (match[i].type() == typeid(float)) - ASSERT_THAT(std::any_cast(match[i]), testdata["expected_args"][i].as()) << FormatMessage(testdata, expression); - else if (match[i].type() == typeid(double)) - ASSERT_THAT(std::any_cast(match[i]), testdata["expected_args"][i].as()) << FormatMessage(testdata, expression); + const auto& argument = match[i]; + + if (argument.Name() == "") + EXPECT_THAT(argument.GetValue(), testdata["expected_args"][i].as()) << FormatTestFailureMessage(file, testdata, expression); + + else if (argument.Name() == "int") + EXPECT_THAT(argument.GetValue(), testdata["expected_args"][i].as()) << FormatTestFailureMessage(file, testdata, expression); + + else if (argument.Name() == "float") + EXPECT_THAT(argument.GetValue(), testdata["expected_args"][i].as()) << FormatTestFailureMessage(file, testdata, expression); + + else if (argument.Name() == "word") + EXPECT_THAT(argument.GetValue(), testdata["expected_args"][i].as()) << FormatTestFailureMessage(file, testdata, expression); + + else if (argument.Name() == "string") + EXPECT_THAT(argument.GetValue(), testdata["expected_args"][i].as()) << FormatTestFailureMessage(file, testdata, expression); + + else if (argument.Name() == "bigdecimal") + EXPECT_THAT(argument.GetValue(), testdata["expected_args"][i].as()) << FormatTestFailureMessage(file, testdata, expression); + + else if (argument.Name() == "biginteger") + EXPECT_THAT(argument.GetValue(), testdata["expected_args"][i].as()) << FormatTestFailureMessage(file, testdata, expression); + + else if (argument.Name() == "byte") + EXPECT_THAT(argument.GetValue(), testdata["expected_args"][i].as()) << FormatTestFailureMessage(file, testdata, expression); + + else if (argument.Name() == "short") + EXPECT_THAT(argument.GetValue(), testdata["expected_args"][i].as()) << FormatTestFailureMessage(file, testdata, expression); + + else if (argument.Name() == "long") + EXPECT_THAT(argument.GetValue(), testdata["expected_args"][i].as()) << FormatTestFailureMessage(file, testdata, expression); + + else if (argument.Name() == "double") + EXPECT_THAT(argument.GetValue(), testdata["expected_args"][i].as()) << FormatTestFailureMessage(file, testdata, expression); + else - FAIL() << "Unknown type: " << match[i].type().name() << " for:\n" - << FormatMessage(testdata, expression); + FAIL() << "Unknown type: " << argument.Name() << " for:\n" + << FormatTestFailureMessage(file, testdata, expression); } } } @@ -102,49 +133,50 @@ namespace cucumber_cpp::library::cucumber_expression TEST_F(TestExpression, MatchFloat) { - EXPECT_THAT(Match(R"__({float})__", R"__()__"), testing::IsFalse()); - EXPECT_THAT(Match(R"__({float})__", R"__(.)__"), testing::IsFalse()); - EXPECT_THAT(Match(R"__({float})__", R"__(,)__"), testing::IsFalse()); - EXPECT_THAT(Match(R"__({float})__", R"__(-)__"), testing::IsFalse()); - EXPECT_THAT(Match(R"__({float})__", R"__(E)__"), testing::IsFalse()); - EXPECT_THAT(Match(R"__({float})__", R"__(1,)__"), testing::IsFalse()); - EXPECT_THAT(Match(R"__({float})__", R"__(,1)__"), testing::IsFalse()); - EXPECT_THAT(Match(R"__({float})__", R"__(1.)__"), testing::IsFalse()); - - EXPECT_THAT(std::any_cast((*Match(R"__({float})__", R"__(1)__"))[0]), testing::FloatNear(1.0f, std::numeric_limits::epsilon())); - EXPECT_THAT(std::any_cast((*Match(R"__({float})__", R"__(-1)__"))[0]), testing::FloatNear(-1.0f, std::numeric_limits::epsilon())); - EXPECT_THAT(std::any_cast((*Match(R"__({float})__", R"__(1.1)__"))[0]), testing::FloatNear(1.1f, std::numeric_limits::epsilon())); - EXPECT_THAT(Match(R"__({float})__", R"__(1,000)__"), testing::IsFalse()); - EXPECT_THAT(Match(R"__({float})__", R"__(1,000,0)__"), testing::IsFalse()); - EXPECT_THAT(Match(R"__({float})__", R"__(1,000.1)__"), testing::IsFalse()); - EXPECT_THAT(Match(R"__({float})__", R"__(1,000,10)__"), testing::IsFalse()); - EXPECT_THAT(Match(R"__({float})__", R"__(1,0.1)__"), testing::IsFalse()); - EXPECT_THAT(Match(R"__({float})__", R"__(1,000,000.1)__"), testing::IsFalse()); - EXPECT_THAT(std::any_cast((*Match(R"__({float})__", R"__(-1.1)__"))[0]), testing::FloatNear(-1.1f, std::numeric_limits::epsilon())); - - EXPECT_THAT(std::any_cast((*Match(R"__({float})__", R"__(.1)__"))[0]), testing::FloatNear(0.1f, std::numeric_limits::epsilon())); - EXPECT_THAT(std::any_cast((*Match(R"__({float})__", R"__(-.1)__"))[0]), testing::FloatNear(-0.1f, std::numeric_limits::epsilon())); - EXPECT_THAT(std::any_cast((*Match(R"__({float})__", R"__(-.1000001)__"))[0]), testing::FloatNear(-0.1000001f, std::numeric_limits::epsilon())); - EXPECT_THAT(std::any_cast((*Match(R"__({float})__", R"__(1E1)__"))[0]), testing::FloatNear(10.0, std::numeric_limits::epsilon())); - EXPECT_THAT(std::any_cast((*Match(R"__({float})__", R"__(.1E1)__"))[0]), testing::FloatNear(1, std::numeric_limits::epsilon())); - EXPECT_THAT(Match(R"__({float})__", R"__(1,E1)__"), testing::IsFalse()); - EXPECT_THAT(std::any_cast((*Match(R"__({float})__", R"__(-.01)__"))[0]), testing::FloatNear(-0.01, std::numeric_limits::epsilon())); - EXPECT_THAT(std::any_cast((*Match(R"__({float})__", R"__(-.1E-1)__"))[0]), testing::FloatNear(-0.01, std::numeric_limits::epsilon())); - EXPECT_THAT(std::any_cast((*Match(R"__({float})__", R"__(-.1E-2)__"))[0]), testing::FloatNear(-0.001, std::numeric_limits::epsilon())); - EXPECT_THAT(std::any_cast((*Match(R"__({float})__", R"__(-.1E+1)__"))[0]), testing::FloatNear(-1, std::numeric_limits::epsilon())); - EXPECT_THAT(std::any_cast((*Match(R"__({float})__", R"__(-.1E+2)__"))[0]), testing::FloatNear(-10, std::numeric_limits::epsilon())); - EXPECT_THAT(std::any_cast((*Match(R"__({float})__", R"__(-.1E1)__"))[0]), testing::FloatNear(-1, std::numeric_limits::epsilon())); - EXPECT_THAT(std::any_cast((*Match(R"__({float})__", R"__(-.1E2)__"))[0]), testing::FloatNear(-10, std::numeric_limits::epsilon())); + EXPECT_THAT(Match(R"__({float})__", R"__()__"), testing::IsFalse()); + EXPECT_THAT(Match(R"__({float})__", R"__(.)__"), testing::IsFalse()); + EXPECT_THAT(Match(R"__({float})__", R"__(,)__"), testing::IsFalse()); + EXPECT_THAT(Match(R"__({float})__", R"__(-)__"), testing::IsFalse()); + EXPECT_THAT(Match(R"__({float})__", R"__(E)__"), testing::IsFalse()); + EXPECT_THAT(Match(R"__({float})__", R"__(1,)__"), testing::IsFalse()); + EXPECT_THAT(Match(R"__({float})__", R"__(,1)__"), testing::IsFalse()); + EXPECT_THAT(Match(R"__({float})__", R"__(1.)__"), testing::IsFalse()); + + EXPECT_THAT(Match(R"__({float})__", R"__(1)__").value(), testing::FloatNear(1.0f, std::numeric_limits::epsilon())); + EXPECT_THAT(Match(R"__({float})__", R"__(-1)__").value(), testing::FloatNear(-1.0f, std::numeric_limits::epsilon())); + EXPECT_THAT(Match(R"__({float})__", R"__(1.1)__").value(), testing::FloatNear(1.1f, std::numeric_limits::epsilon())); + + EXPECT_THAT(Match(R"__({float})__", R"__(1,000)__"), testing::IsFalse()); + EXPECT_THAT(Match(R"__({float})__", R"__(1,000,0)__"), testing::IsFalse()); + EXPECT_THAT(Match(R"__({float})__", R"__(1,000.1)__"), testing::IsFalse()); + EXPECT_THAT(Match(R"__({float})__", R"__(1,000,10)__"), testing::IsFalse()); + EXPECT_THAT(Match(R"__({float})__", R"__(1,0.1)__"), testing::IsFalse()); + EXPECT_THAT(Match(R"__({float})__", R"__(1,000,000.1)__"), testing::IsFalse()); + EXPECT_THAT(Match(R"__({float})__", R"__(-1.1)__").value(), testing::FloatNear(-1.1f, std::numeric_limits::epsilon())); + + EXPECT_THAT(Match(R"__({float})__", R"__(.1)__").value(), testing::FloatNear(0.1f, std::numeric_limits::epsilon())); + EXPECT_THAT(Match(R"__({float})__", R"__(-.1)__").value(), testing::FloatNear(-0.1f, std::numeric_limits::epsilon())); + EXPECT_THAT(Match(R"__({float})__", R"__(-.1000001)__").value(), testing::FloatNear(-0.1000001f, std::numeric_limits::epsilon())); + EXPECT_THAT(Match(R"__({float})__", R"__(1E1)__").value(), testing::FloatNear(10.0, std::numeric_limits::epsilon())); + EXPECT_THAT(Match(R"__({float})__", R"__(.1E1)__").value(), testing::FloatNear(1, std::numeric_limits::epsilon())); + EXPECT_THAT(Match(R"__({float})__", R"__(1,E1)__"), testing::IsFalse()); + EXPECT_THAT(Match(R"__({float})__", R"__(-.01)__").value(), testing::FloatNear(-0.01, std::numeric_limits::epsilon())); + EXPECT_THAT(Match(R"__({float})__", R"__(-.1E-1)__").value(), testing::FloatNear(-0.01, std::numeric_limits::epsilon())); + EXPECT_THAT(Match(R"__({float})__", R"__(-.1E-2)__").value(), testing::FloatNear(-0.001, std::numeric_limits::epsilon())); + EXPECT_THAT(Match(R"__({float})__", R"__(-.1E+1)__").value(), testing::FloatNear(-1, std::numeric_limits::epsilon())); + EXPECT_THAT(Match(R"__({float})__", R"__(-.1E+2)__").value(), testing::FloatNear(-10, std::numeric_limits::epsilon())); + EXPECT_THAT(Match(R"__({float})__", R"__(-.1E1)__").value(), testing::FloatNear(-1, std::numeric_limits::epsilon())); + EXPECT_THAT(Match(R"__({float})__", R"__(-.1E2)__").value(), testing::FloatNear(-10, std::numeric_limits::epsilon())); } TEST_F(TestExpression, FloatWithZero) { - EXPECT_THAT(std::any_cast((*Match(R"__({float})__", R"__(0)__"))[0]), testing::FloatNear(0.0f, std::numeric_limits::epsilon())); + EXPECT_THAT(Match(R"__({float})__", R"__(0)__").value(), testing::FloatNear(0.0f, std::numeric_limits::epsilon())); } TEST_F(TestExpression, MatchAnonymous) { - EXPECT_THAT(std::any_cast((*Match(R"__({})__", R"__(0.22)__"))[0]), testing::StrEq("0.22")); + EXPECT_THAT(Match(R"__({})__", R"__(0.22)__").value(), testing::StrEq("0.22")); } TEST_F(TestExpression, MatchCustom) @@ -155,28 +187,28 @@ namespace cucumber_cpp::library::cucumber_expression std::optional number; }; - parameterRegistry.AddParameter("textAndOrNumber", { R"(([A-Z]+)?(?: )?([0-9]+)?)" }, [](MatchRange matches) -> std::any + parameterRegistry.AddParameter("textAndOrNumber", { R"(([A-Z]+)?(?: )?([0-9]+)?)" }, + [](const cucumber::messages::group& matches) -> CustomType { - std::optional text{ matches[1].matched ? StringTo(matches[1].str()) : std::optional{ std::nullopt } }; - std::optional number{ matches[2].matched ? StringTo(matches[2].str()) : std::optional{ std::nullopt } }; - + std::optional text{ matches.children[0].value }; + std::optional number{ matches.children[1].value ? StringTo(matches.children[1].value.value()) : std::optional{ std::nullopt } }; return CustomType{ text, number }; }); - auto matchString{ Match(R"__({textAndOrNumber})__", R"__(ABC)__") }; + auto matchString{ Match(R"__({textAndOrNumber})__", R"__(ABC)__") }; EXPECT_THAT(matchString, testing::IsTrue()); - EXPECT_THAT(std::any_cast((*matchString)[0]).text.value(), testing::StrEq("ABC")); - EXPECT_THAT(std::any_cast((*matchString)[0]).number, testing::IsFalse()); + EXPECT_THAT(matchString.value().text.value(), testing::StrEq("ABC")); + EXPECT_THAT(matchString.value().number, testing::IsFalse()); - auto matchInt{ Match(R"__({textAndOrNumber})__", R"__(123)__") }; + auto matchInt{ Match(R"__({textAndOrNumber})__", R"__(123)__") }; EXPECT_THAT(matchInt, testing::IsTrue()); - EXPECT_THAT(std::any_cast((*matchInt)[0]).text, testing::IsFalse()); - EXPECT_THAT(std::any_cast((*matchInt)[0]).number.value(), testing::Eq(123)); + EXPECT_THAT(matchInt.value().text, testing::IsFalse()); + EXPECT_THAT(matchInt.value().number.value(), testing::Eq(123)); - auto matchStringAndInt{ Match(R"__({textAndOrNumber})__", R"__(ABC 123)__") }; + auto matchStringAndInt{ Match(R"__({textAndOrNumber})__", R"__(ABC 123)__") }; EXPECT_THAT(matchStringAndInt, testing::IsTrue()); - EXPECT_THAT(std::any_cast((*matchStringAndInt)[0]).text.value(), testing::StrEq("ABC")); - EXPECT_THAT(std::any_cast((*matchStringAndInt)[0]).number.value(), testing::Eq(123)); + EXPECT_THAT(matchStringAndInt.value().text.value(), testing::StrEq("ABC")); + EXPECT_THAT(matchStringAndInt.value().number.value(), testing::Eq(123)); } TEST_F(TestExpression, ExposeSource) @@ -188,18 +220,18 @@ namespace cucumber_cpp::library::cucumber_expression TEST_F(TestExpression, MatchBoolean) { - EXPECT_THAT(std::any_cast((*Match(R"__({bool})__", R"__(true)__"))[0]), testing::IsTrue()); - EXPECT_THAT(std::any_cast((*Match(R"__({bool})__", R"__(1)__"))[0]), testing::IsTrue()); - EXPECT_THAT(std::any_cast((*Match(R"__({bool})__", R"__(yes)__"))[0]), testing::IsTrue()); - EXPECT_THAT(std::any_cast((*Match(R"__({bool})__", R"__(on)__"))[0]), testing::IsTrue()); - EXPECT_THAT(std::any_cast((*Match(R"__({bool})__", R"__(enabled)__"))[0]), testing::IsTrue()); - EXPECT_THAT(std::any_cast((*Match(R"__({bool})__", R"__(active)__"))[0]), testing::IsTrue()); - - EXPECT_THAT(std::any_cast((*Match(R"__({bool})__", R"__(false)__"))[0]), testing::IsFalse()); - EXPECT_THAT(std::any_cast((*Match(R"__({bool})__", R"__(0)__"))[0]), testing::IsFalse()); - EXPECT_THAT(std::any_cast((*Match(R"__({bool})__", R"__(2)__"))[0]), testing::IsFalse()); - EXPECT_THAT(std::any_cast((*Match(R"__({bool})__", R"__(off)__"))[0]), testing::IsFalse()); - EXPECT_THAT(std::any_cast((*Match(R"__({bool})__", R"__(foo)__"))[0]), testing::IsFalse()); + EXPECT_THAT(Match(R"__({bool})__", R"__(true)__").value(), testing::IsTrue()); + EXPECT_THAT(Match(R"__({bool})__", R"__(1)__").value(), testing::IsTrue()); + EXPECT_THAT(Match(R"__({bool})__", R"__(yes)__").value(), testing::IsTrue()); + EXPECT_THAT(Match(R"__({bool})__", R"__(on)__").value(), testing::IsTrue()); + EXPECT_THAT(Match(R"__({bool})__", R"__(enabled)__").value(), testing::IsTrue()); + EXPECT_THAT(Match(R"__({bool})__", R"__(active)__").value(), testing::IsTrue()); + + EXPECT_THAT(Match(R"__({bool})__", R"__(false)__").value(), testing::IsFalse()); + EXPECT_THAT(Match(R"__({bool})__", R"__(0)__").value(), testing::IsFalse()); + EXPECT_THAT(Match(R"__({bool})__", R"__(2)__").value(), testing::IsFalse()); + EXPECT_THAT(Match(R"__({bool})__", R"__(off)__").value(), testing::IsFalse()); + EXPECT_THAT(Match(R"__({bool})__", R"__(foo)__").value(), testing::IsFalse()); } TEST_F(TestExpression, ThrowUnknownParameterType) @@ -226,9 +258,10 @@ namespace cucumber_cpp::library::cucumber_expression { try { - parameterRegistry.AddParameter("", { ".*" }, [](const MatchRange& matches) + parameterRegistry.AddParameter("", { ".*" }, + [](const cucumber::messages::group& matches) -> std::string { - return StringTo(matches.begin()->str()); + return matches.value.value(); }); FAIL() << "Expected CucumberExpressionError to be thrown"; } @@ -242,9 +275,10 @@ namespace cucumber_cpp::library::cucumber_expression { try { - parameterRegistry.AddParameter("word", { ".*" }, [](const MatchRange& matches) + parameterRegistry.AddParameter("word", { ".*" }, + [](const cucumber::messages::group& matches) -> std::string { - return StringTo(matches.begin()->str()); + return matches.value.value(); }); FAIL() << "Expected CucumberExpressionError to be thrown"; } diff --git a/cucumber_cpp/library/cucumber_expression/test/TestExpressionParser.cpp b/cucumber_cpp/library/cucumber_expression/test/TestExpressionParser.cpp index 5267c96f..3b3890c4 100644 --- a/cucumber_cpp/library/cucumber_expression/test/TestExpressionParser.cpp +++ b/cucumber_cpp/library/cucumber_expression/test/TestExpressionParser.cpp @@ -1,11 +1,11 @@ #include "cucumber_cpp/library/cucumber_expression/Ast.hpp" #include "cucumber_cpp/library/cucumber_expression/ExpressionParser.hpp" +#include "fmt/format.h" #include "yaml-cpp/node/node.h" #include "yaml-cpp/node/parse.h" #include "yaml-cpp/yaml.h" #include #include -#include #include #include #include @@ -107,13 +107,13 @@ namespace cucumber_cpp::library::cucumber_expression { if (testdata["exception"]) ASSERT_ANY_THROW(ExpressionParser{}.Parse(testdata["expression"].as())) - << std::format("Test failed for file: {}", file); + << fmt::format("Test failed for file: {}", file); else { const auto actual = ExpressionParser{}.Parse(testdata["expression"].as()); const auto expected = CreateNode(testdata["expected_ast"]); ASSERT_THAT(actual, testing::Eq(expected)) - << std::format("Test failed for file: {}", file); + << fmt::format("Test failed for file: {}", file); } } } diff --git a/cucumber_cpp/library/cucumber_expression/test/TestExpressionTokenizer.cpp b/cucumber_cpp/library/cucumber_expression/test/TestExpressionTokenizer.cpp index 62fe1cf8..b9c12cd2 100644 --- a/cucumber_cpp/library/cucumber_expression/test/TestExpressionTokenizer.cpp +++ b/cucumber_cpp/library/cucumber_expression/test/TestExpressionTokenizer.cpp @@ -1,12 +1,12 @@ #include "cucumber_cpp/library/cucumber_expression/Ast.hpp" #include "cucumber_cpp/library/cucumber_expression/ExpressionTokenizer.hpp" +#include "fmt/format.h" #include "yaml-cpp/node/node.h" #include "yaml-cpp/node/parse.h" #include "yaml-cpp/yaml.h" #include "gmock/gmock.h" #include #include -#include #include #include #include @@ -62,13 +62,13 @@ namespace cucumber_cpp::library::cucumber_expression for (const auto& [file, testdata] : GetTestData(testdataPath)) if (testdata["exception"]) ASSERT_ANY_THROW(ExpressionTokenizer{}.Tokenize(testdata["expression"].as())) - << std::format("Test failed for file: {}", file); + << fmt::format("Test failed for file: {}", file); else { const auto actual = ExpressionTokenizer{}.Tokenize(testdata["expression"].as()); const auto expected = CreateTokens(testdata["expected_tokens"]); ASSERT_THAT(actual, testing::ElementsAreArray(expected)) - << std::format("Test failed for file: {}", file); + << fmt::format("Test failed for file: {}", file); } } diff --git a/cucumber_cpp/library/cucumber_expression/test/TestTransformation.cpp b/cucumber_cpp/library/cucumber_expression/test/TestTransformation.cpp index 2f5fed80..82210908 100644 --- a/cucumber_cpp/library/cucumber_expression/test/TestTransformation.cpp +++ b/cucumber_cpp/library/cucumber_expression/test/TestTransformation.cpp @@ -1,24 +1,14 @@ - -#include "cucumber_cpp/library/cucumber_expression/Errors.hpp" #include "cucumber_cpp/library/cucumber_expression/Expression.hpp" #include "cucumber_cpp/library/cucumber_expression/ParameterRegistry.hpp" +#include "fmt/format.h" #include "yaml-cpp/node/node.h" #include "yaml-cpp/node/parse.h" #include "yaml-cpp/yaml.h" #include "gmock/gmock.h" -#include #include -#include #include #include -#include -#include #include -#include -#include -#include -#include -#include #include #include #include @@ -40,7 +30,7 @@ namespace cucumber_cpp::library::cucumber_expression std::string FormatMessage(const YAML::Node& node, const Expression& expression) { - return std::format("failed to match {}\n" + return fmt::format("failed to match {}\n" "regex {}\n" "against {}", node["expression"].as(), expression.Pattern(), node["text"].as()); @@ -50,7 +40,7 @@ namespace cucumber_cpp::library::cucumber_expression TEST(TestTransformation, TestFromFiles) { std::filesystem::path testdataPath = "testdata/cucumber-expression/transformation"; - ParameterRegistry parameterRegistry{}; + ParameterRegistry parameterRegistry{ {} }; for (const auto& [file, testdata] : GetTestData(testdataPath)) { @@ -58,7 +48,7 @@ namespace cucumber_cpp::library::cucumber_expression const auto actualRegex = expression.Pattern(); EXPECT_THAT(actualRegex, testing::StrEq(testdata["expected_regex"].as())) - << std::format("Test failed for {}", testdata["expression"].as()); + << fmt::format("Test failed for {}", testdata["expression"].as()); } } } diff --git a/cucumber_cpp/library/cucumber_expression/test/TestTreeRegexp.cpp b/cucumber_cpp/library/cucumber_expression/test/TestTreeRegexp.cpp new file mode 100644 index 00000000..bec78ae8 --- /dev/null +++ b/cucumber_cpp/library/cucumber_expression/test/TestTreeRegexp.cpp @@ -0,0 +1,185 @@ + +#include "cucumber_cpp/library/cucumber_expression/TreeRegexp.hpp" +#include "gmock/gmock.h" +#include +#include +#include + +namespace cucumber_cpp::library::cucumber_expression +{ + using namespace std::string_view_literals; + + namespace + { + std::string_view BuilderPattern(const GroupBuilder& builder) + { + return builder.Pattern(); + } + } + + TEST(TestTreeRegexp, exposes_group_source) + { + TreeRegexp treeRegexp{ R"__((a(?:b)?)(c))__" }; + EXPECT_THAT(treeRegexp.RootBuilder().Children().begin()->Pattern(), testing::StrEq("a(?:b)?"sv)); + EXPECT_THAT(std::next(treeRegexp.RootBuilder().Children().begin())->Pattern(), testing::StrEq("c"sv)); + } + + TEST(TestTreeRegexp, builds_tree) + { + TreeRegexp treeRegexp{ R"__((a(?:b)?)(c))__" }; + const auto group = *treeRegexp.MatchToGroup("ac"); + EXPECT_THAT(group.value.value(), testing::StrEq("ac")); + EXPECT_THAT(group.children[0].value.value(), testing::StrEq("a")); + EXPECT_THAT(group.children[0].children.empty(), testing::IsTrue()); + EXPECT_THAT(group.children[1].value.value(), testing::StrEq("c")); + } + + TEST(TestTreeRegexp, ignores_non_capture_group_as_a_non_capturing_group) + { + TreeRegexp treeRegexp{ R"__(a(?:b)(c))__" }; + const auto group = *treeRegexp.MatchToGroup("abc"); + EXPECT_THAT(group.value.value(), testing::StrEq("abc")); + EXPECT_THAT(group.children.size(), testing::Eq(1)); + } + + TEST(TestTreeRegexp, ignores_negative_lookahead_as_a_non_capturing_group) + { + TreeRegexp treeRegexp{ R"__(a(?!b)(.+))__" }; + const auto group = *treeRegexp.MatchToGroup("aBc"); + EXPECT_THAT(group.value.value(), testing::StrEq("aBc")); + EXPECT_THAT(group.children.size(), testing::Eq(1)); + } + + TEST(TestTreeRegexp, ignores_positive_lookahead_as_a_non_capturing_group) + { + TreeRegexp treeRegexp{ R"__(a(?=[b])(.+))__" }; + const auto group = *treeRegexp.MatchToGroup("abc"); + EXPECT_THAT(group.value.value(), testing::StrEq("abc")); + EXPECT_THAT(group.children.size(), 1); + EXPECT_THAT(group.children[0].value.value(), testing::StrEq("bc")); + } + + TEST(TestTreeRegexp, DISABLED_ignores_positive_lookbehind_as_a_non_capturing_group) + { + // std::regex does not support positive lookbehind + TreeRegexp treeRegexp{ R"__(a(.+)(?<=c)$)__" }; + const auto group = *treeRegexp.MatchToGroup("abc"); + EXPECT_THAT(group.value.value(), testing::StrEq("abc")); + EXPECT_THAT(group.children.size(), 1); + EXPECT_THAT(group.children[0].value.value(), testing::StrEq("bc")); + } + + TEST(TestTreeRegexp, DISABLED_ignores_negative_lookbehind_as_a_non_capturing_group) + { + // std::regex does not support negative lookbehind + TreeRegexp treeRegexp{ R"__(a(.+?)(?b)c)__" }; + const auto group = *treeRegexp.MatchToGroup("abc"); + EXPECT_THAT(group.value.value(), testing::StrEq("abc")); + EXPECT_THAT(group.children.size(), 1); + EXPECT_THAT(group.children[0].value.value(), testing::StrEq("b")); + } + + TEST(TestTreeRegexp, matches_optional_group) + { + TreeRegexp treeRegexp{ R"__(^Something( with an optional argument)?)__" }; + const auto group = *treeRegexp.MatchToGroup("Something"); + EXPECT_THAT(group.children[0].value, testing::IsFalse()); + } + + TEST(TestTreeRegexp, matches_nested_groups) + { + TreeRegexp treeRegexp{ R"__(^A (\d+) thick line from ((\d+),\s*(\d+),\s*(\d+)) to ((\d+),\s*(\d+),\s*(\d+)))__" }; + const auto group = *treeRegexp.MatchToGroup("A 5 thick line from 10,20,30 to 40,50,60"); + EXPECT_THAT(group.children[0].value.value(), testing::StrEq("5")); + EXPECT_THAT(group.children[1].value.value(), testing::StrEq("10,20,30")); + EXPECT_THAT(group.children[1].children[0].value.value(), testing::StrEq("10")); + EXPECT_THAT(group.children[1].children[1].value.value(), testing::StrEq("20")); + EXPECT_THAT(group.children[1].children[2].value.value(), testing::StrEq("30")); + EXPECT_THAT(group.children[2].value.value(), testing::StrEq("40,50,60")); + EXPECT_THAT(group.children[2].children[0].value.value(), testing::StrEq("40")); + EXPECT_THAT(group.children[2].children[1].value.value(), testing::StrEq("50")); + EXPECT_THAT(group.children[2].children[2].value.value(), testing::StrEq("60")); + } + + TEST(TestTreeRegexp, detects_multiple_non_capturing_groups) + { + TreeRegexp treeRegexp{ R"__((?:a)(:b)(\?c)(d))__" }; + const auto group = *treeRegexp.MatchToGroup("a:b?cd"); + EXPECT_THAT(group.children.size(), testing::Eq(3)); + } + + TEST(TestTreeRegexp, works_with_escaped_backslash) + { + TreeRegexp treeRegexp{ R"__(foo\\(bar|baz))__" }; + const auto group = *treeRegexp.MatchToGroup("foo\\bar"); + EXPECT_THAT(group.children.size(), testing::Eq(1)); + } + + TEST(TestTreeRegexp, works_with_escaped_slash) + { + TreeRegexp treeRegexp{ R"__(^I go to '\/(.+)'$)__" }; + const auto group = *treeRegexp.MatchToGroup("I go to '/hello'"); + EXPECT_THAT(group.children.size(), testing::Eq(1)); + } + + TEST(TestTreeRegexp, works_with_digit_and_word) + { + TreeRegexp treeRegexp{ R"__(^(\d) (\w+)$)__" }; + const auto group = *treeRegexp.MatchToGroup("2 you"); + EXPECT_THAT(group.children.size(), testing::Eq(2)); + } + + TEST(TestTreeRegexp, captures_non_capturing_groups_with_capturing_groups_inside) + { + TreeRegexp treeRegexp{ R"__(the stdout(?: from "(.*?)")?)__" }; + const auto group = *treeRegexp.MatchToGroup("the stdout"); + + EXPECT_THAT(group.value.value(), testing::StrEq("the stdout")); + EXPECT_THAT(group.children[0].value, testing::IsFalse()); + EXPECT_THAT(group.children.size(), testing::Eq(1)); + } + + TEST(TestTreeRegexp, DISABLED_works_with_case_insensitive_flag) + { + // std::regex does not support inline case insensitive flag + TreeRegexp treeRegexp{ R"__(HELLO/i)__" }; + const auto group = *treeRegexp.MatchToGroup("hello"); + EXPECT_THAT(group.value.value(), testing::StrEq("hello")); + } + + TEST(TestTreeRegexp, empty_capturing_group) + { + TreeRegexp treeRegexp{ R"__(())__" }; + const auto group = *treeRegexp.MatchToGroup(""); + EXPECT_THAT(group.value.value(), testing::StrEq("")); + EXPECT_THAT(group.children.size(), testing::Eq(1)); + } + + TEST(TestTreeRegexp, DISABLED_empty_look_ahead) + { + // std::regex does not support positive lookbehind + TreeRegexp treeRegexp{ R"__((?<=))__" }; + const auto group = *treeRegexp.MatchToGroup(""); + EXPECT_THAT(group.value.value(), testing::StrEq("")); + EXPECT_THAT(group.children.size(), testing::Eq(0)); + } + + TEST(TestTreeRegexp, does_not_consider_parenthesis_in_character_class_as_group) + { + TreeRegexp treeRegexp{ R"__(^drawings: ([A-Z, ()]+)$)__" }; + const auto group = *treeRegexp.MatchToGroup("drawings: ONE(TWO)"); + EXPECT_THAT(group.value.value(), testing::StrEq("drawings: ONE(TWO)")); + EXPECT_THAT(group.children.size(), testing::Eq(1)); + EXPECT_THAT(group.children[0].value.value(), testing::StrEq("ONE(TWO)")); + } +} diff --git a/cucumber_cpp/library/engine/CMakeLists.txt b/cucumber_cpp/library/engine/CMakeLists.txt index 06594974..24b25d47 100644 --- a/cucumber_cpp/library/engine/CMakeLists.txt +++ b/cucumber_cpp/library/engine/CMakeLists.txt @@ -1,32 +1,13 @@ -add_library(cucumber_cpp.library.engine STATIC ${CCR_EXCLUDE_FROM_ALL}) +add_library(cucumber_cpp.library.engine ${CCR_EXCLUDE_FROM_ALL}) target_sources(cucumber_cpp.library.engine PRIVATE - ContextManager.cpp - ContextManager.hpp - FeatureFactory.cpp - FeatureFactory.hpp - FeatureInfo.cpp - FeatureInfo.hpp - HookExecutor.cpp - HookExecutor.hpp - RuleInfo.cpp - RuleInfo.hpp - ScenarioInfo.cpp - ScenarioInfo.hpp + ExecutionContext.cpp + ExecutionContext.hpp + Hook.cpp + Hook.hpp Step.cpp Step.hpp - StepInfo.cpp - StepInfo.hpp - StepType.hpp StringTo.hpp - Table.cpp - Table.hpp - TestExecution.cpp - TestExecution.hpp - FailureHandler.cpp - FailureHandler.hpp - TestRunner.cpp - TestRunner.hpp ) target_include_directories(cucumber_cpp.library.engine PUBLIC @@ -34,12 +15,16 @@ target_include_directories(cucumber_cpp.library.engine PUBLIC ) target_link_libraries(cucumber_cpp.library.engine PUBLIC - cucumber_cpp.library + base64 + # cucumber_cpp.library cucumber_cpp.library.util cucumber_cpp.library.cucumber_expression + cucumber_cpp.library.support + cucumber_cpp.library.assemble + cucumber_cpp.library.runtime ) if (CCR_BUILD_TESTS) add_subdirectory(test) - add_subdirectory(test_helper) + # add_subdirectory(test_helper) endif() diff --git a/cucumber_cpp/library/engine/ContextManager.cpp b/cucumber_cpp/library/engine/ContextManager.cpp deleted file mode 100644 index 1d45637b..00000000 --- a/cucumber_cpp/library/engine/ContextManager.cpp +++ /dev/null @@ -1,225 +0,0 @@ -#include "cucumber_cpp/library/engine/ContextManager.hpp" -#include "cucumber_cpp/library/Context.hpp" -#include "cucumber_cpp/library/TraceTime.hpp" -#include "cucumber_cpp/library/engine/Result.hpp" -#include "cucumber_cpp/library/engine/RuleInfo.hpp" -#include "cucumber_cpp/library/engine/ScenarioInfo.hpp" -#include "cucumber_cpp/library/engine/StepInfo.hpp" -#include -#include -#include -#include - -namespace cucumber_cpp::library::engine -{ - namespace - { - std::string ConstructErrorString(std::string_view typeName, std::string_view postfix) - { - std::string error; - - error.reserve(typeName.size() + postfix.size()); - error.append(typeName); - error.append(postfix); - - return error; - } - - auto& GetOrThrow(auto& ptr, std::string_view typeName) - { - if (ptr) - return *ptr; - - throw ContextNotAvailable{ ConstructErrorString(typeName, " not available") }; - } - } - - CurrentContext::CurrentContext(std::shared_ptr contextStorageFactory) - : Context{ std::move(contextStorageFactory) } - { - Start(); - } - - CurrentContext::CurrentContext(CurrentContext* parent) - : Context{ parent } - , parent{ parent } - - { - Start(); - } - - [[nodiscard]] Result CurrentContext::ExecutionStatus() const - { - return executionStatus; - } - - void CurrentContext::Start() - { - traceTime.Start(); - } - - TraceTime::Duration CurrentContext::Duration() const - { - return traceTime.Delta(); - } - - void CurrentContext::ExecutionStatus(Result result) - { - if (result > executionStatus) - executionStatus = result; - - if (parent != nullptr) - parent->ExecutionStatus(result); - } - - ProgramContext::ProgramContext(std::shared_ptr contextStorageFactory) - : RunnerContext{ std::move(contextStorageFactory) } - { - } - - ContextManager::ScopedFeatureContext::ScopedFeatureContext(ContextManager& contextManager) - : contextManager{ contextManager } - {} - - ContextManager::ScopedFeatureContext::~ScopedFeatureContext() - { - contextManager.DisposeFeatureContext(); - } - - const cucumber_cpp::library::engine::CurrentContext& ContextManager::ScopedFeatureContext::CurrentContext() const - { - return contextManager.FeatureContext(); - } - - ContextManager::ScopedRuleContext::ScopedRuleContext(ContextManager& contextManager) - : contextManager{ contextManager } - {} - - ContextManager::ScopedRuleContext::~ScopedRuleContext() - { - contextManager.DisposeRuleContext(); - } - - ContextManager::ScopedScenarioContext::ScopedScenarioContext(ContextManager& contextManager) - : contextManager{ contextManager } - {} - - ContextManager::ScopedScenarioContext::~ScopedScenarioContext() - { - contextManager.DisposeScenarioContext(); - } - - ContextManager::ScopedStepContext::ScopedStepContext(ContextManager& contextManager) - : contextManager{ contextManager } - {} - - ContextManager::ScopedStepContext::~ScopedStepContext() - { - contextManager.DisposeStepContext(); - } - - ContextManager::ContextManager(std::shared_ptr contextStorageFactory) - { - programContext = std::make_shared(std::move(contextStorageFactory)); - runnerContext.push(programContext); - } - - cucumber_cpp::library::engine::ProgramContext& ContextManager::ProgramContext() - { - return *programContext; - } - - cucumber_cpp::library::engine::ProgramContext& ContextManager::ProgramContext() const - { - return *programContext; - } - - ContextManager::ScopedFeatureContext ContextManager::CreateFeatureContext(const FeatureInfo& featureInfo) - { - featureContext = std::make_shared(*programContext, featureInfo); - runnerContext.push(featureContext); - - return ScopedFeatureContext{ *this }; - } - - void ContextManager::DisposeFeatureContext() - { - runnerContext.pop(); - featureContext.reset(); - } - - FeatureContext& ContextManager::FeatureContext() - { - return GetOrThrow(featureContext, "FeatureContext"); - } - - ContextManager::ScopedRuleContext ContextManager::CreateRuleContext(const RuleInfo& ruleInfo) - { - ruleContext = std::make_shared(*featureContext, ruleInfo); - runnerContext.push(ruleContext); - - return ScopedRuleContext{ *this }; - } - - void ContextManager::DisposeRuleContext() - { - runnerContext.pop(); - ruleContext.reset(); - } - - RuleContext& ContextManager::RuleContext() - { - return GetOrThrow(ruleContext, "RuleContext"); - } - - ContextManager::ScopedScenarioContext ContextManager::CreateScenarioContext(const ScenarioInfo& scenarioInfo) - { - scenarioContext = std::make_shared(CurrentContext(), scenarioInfo); - runnerContext.push(scenarioContext); - - return ScopedScenarioContext{ *this }; - } - - void ContextManager::DisposeScenarioContext() - { - runnerContext.pop(); - scenarioContext.reset(); - } - - ScenarioContext& ContextManager::ScenarioContext() - { - return GetOrThrow(scenarioContext, "ScenarioContext"); - } - - ContextManager::ScopedStepContext ContextManager::CreateStepContext(const StepInfo& stepInfo) - { - stepContext.push(std::make_shared(*scenarioContext, stepInfo)); - return ScopedStepContext{ *this }; - } - - void ContextManager::DisposeStepContext() - { - stepContext.pop(); - } - - StepContext& ContextManager::StepContext() - { - if (stepContext.empty()) - throw ContextNotAvailable{ "StepContext not available" }; - - return *stepContext.top(); - } - - cucumber_cpp::library::engine::RunnerContext& ContextManager::CurrentContext() - { - return *runnerContext.top(); - } - - cucumber_cpp::library::engine::RunnerContext* ContextManager::CurrentStepContext() - { - if (stepContext.empty()) - return nullptr; - - return stepContext.top().get(); - } -} diff --git a/cucumber_cpp/library/engine/ContextManager.hpp b/cucumber_cpp/library/engine/ContextManager.hpp deleted file mode 100644 index e6c076eb..00000000 --- a/cucumber_cpp/library/engine/ContextManager.hpp +++ /dev/null @@ -1,152 +0,0 @@ -#ifndef ENGINE_CONTEXTMANAGER_HPP -#define ENGINE_CONTEXTMANAGER_HPP - -#include "cucumber_cpp/library/Context.hpp" -#include "cucumber_cpp/library/TraceTime.hpp" -#include "cucumber_cpp/library/engine/Result.hpp" -#include "cucumber_cpp/library/engine/RuleInfo.hpp" -#include "cucumber_cpp/library/engine/ScenarioInfo.hpp" -#include "cucumber_cpp/library/engine/StepInfo.hpp" -#include "cucumber_cpp/library/util/Immoveable.hpp" -#include -#include -#include - -namespace cucumber_cpp::library::engine -{ - struct CurrentContext : Context - { - explicit CurrentContext(std::shared_ptr contextStorageFactory); - explicit CurrentContext(CurrentContext* parent); - - [[nodiscard]] Result ExecutionStatus() const; - - void Start(); - [[nodiscard]] TraceTime::Duration Duration() const; - - protected: - void ExecutionStatus(Result result); - - private: - CurrentContext* parent{ nullptr }; - - Result executionStatus{ Result::passed }; - TraceTime traceTime; - }; - - struct RunnerContext : CurrentContext - { - using CurrentContext::CurrentContext; - using CurrentContext::ExecutionStatus; - }; - - struct ProgramContext : RunnerContext - { - explicit ProgramContext(std::shared_ptr contextStorageFactory); - }; - - template - struct NestedContext : RunnerContext - { - NestedContext(RunnerContext& parent, const T& info) - : RunnerContext{ &parent } - , info{ info } - {} - - const T& info; - }; - - using FeatureContext = NestedContext; - using RuleContext = NestedContext; - using ScenarioContext = NestedContext; - using StepContext = NestedContext; - using NestedStepContext = NestedContext; - - struct ContextNotAvailable : std::logic_error - { - using std::logic_error::logic_error; - }; - - struct ContextManager - { - explicit ContextManager(std::shared_ptr contextStorageFactory); - - cucumber_cpp::library::engine::ProgramContext& ProgramContext(); - [[nodiscard]] cucumber_cpp::library::engine::ProgramContext& ProgramContext() const; - - struct ScopedFeatureContext; - struct ScopedRuleContext; - struct ScopedScenarioContext; - struct ScopedStepContext; - - [[nodiscard]] ScopedFeatureContext CreateFeatureContext(const FeatureInfo& featureInfo); - cucumber_cpp::library::engine::FeatureContext& FeatureContext(); - - [[nodiscard]] ScopedRuleContext CreateRuleContext(const RuleInfo& ruleInfo); - cucumber_cpp::library::engine::RuleContext& RuleContext(); - - [[nodiscard]] ScopedScenarioContext CreateScenarioContext(const ScenarioInfo& scenarioInfo); - cucumber_cpp::library::engine::ScenarioContext& ScenarioContext(); - - [[nodiscard]] ScopedStepContext CreateStepContext(const StepInfo& stepInfo); - - cucumber_cpp::library::engine::StepContext& StepContext(); - - cucumber_cpp::library::engine::RunnerContext& CurrentContext(); - cucumber_cpp::library::engine::RunnerContext* CurrentStepContext(); - - private: - void DisposeFeatureContext(); - void DisposeRuleContext(); - void DisposeScenarioContext(); - void DisposeStepContext(); - - std::shared_ptr programContext; - std::shared_ptr featureContext; - std::shared_ptr ruleContext; - std::shared_ptr scenarioContext; - - std::stack> runnerContext; - std::stack> stepContext; - }; - - struct ContextManager::ScopedFeatureContext : library::util::Immoveable - { - explicit ScopedFeatureContext(ContextManager& contextManager); - ~ScopedFeatureContext(); - - const cucumber_cpp::library::engine::CurrentContext& CurrentContext() const; - - private: - ContextManager& contextManager; - }; - - struct ContextManager::ScopedRuleContext : library::util::Immoveable - { - explicit ScopedRuleContext(ContextManager& contextManager); - ~ScopedRuleContext(); - - private: - ContextManager& contextManager; - }; - - struct ContextManager::ScopedScenarioContext : library::util::Immoveable - { - explicit ScopedScenarioContext(ContextManager& contextManager); - ~ScopedScenarioContext(); - - private: - ContextManager& contextManager; - }; - - struct ContextManager::ScopedStepContext : library::util::Immoveable - { - explicit ScopedStepContext(ContextManager& contextManager); - ~ScopedStepContext(); - - private: - ContextManager& contextManager; - }; -} - -#endif diff --git a/cucumber_cpp/library/engine/ExecutionContext.cpp b/cucumber_cpp/library/engine/ExecutionContext.cpp new file mode 100644 index 00000000..04f4e2e0 --- /dev/null +++ b/cucumber_cpp/library/engine/ExecutionContext.cpp @@ -0,0 +1,134 @@ +#include "cucumber_cpp/library/engine/ExecutionContext.hpp" +#include "base64.hpp" +#include "cucumber/messages/attachment.hpp" +#include "cucumber/messages/attachment_content_encoding.hpp" +#include "cucumber/messages/test_run_hook_started.hpp" +#include "cucumber/messages/test_step_started.hpp" +#include "cucumber_cpp/library/Context.hpp" +#include "cucumber_cpp/library/util/Broadcaster.hpp" +#include "cucumber_cpp/library/util/Timestamp.hpp" +#include +#include +#include +#include +#include +#include +#include + +/* + +std::optional read_file( std::istream & is ) { + if( not is.seekg( 0, std::ios_base::seek_dir::end ) ) { + return std::nullopt; + } + auto const sz = is.tellg( ); + if( not is.seekg( 0 ) ) { + return std::nullopt; + } + auto result = std::string( '\0', static_cast( sz ) ); + if( not is.read( result.data( ), sz ) ) { + return std::nullopt; + } + if( sz != is.gcount( ) ) { + return std::nullopt; + } + return result; +} + +*/ + +namespace cucumber_cpp::library::engine +{ + namespace + { + constexpr auto LogMediaType{ "text/x.cucumber.log+plain" }; + constexpr auto LinkMediaType{ "text/uri-list" }; + + std::pair, std::optional> ReadTestStepStartedIds(StepOrHookStarted stepOrHookStarted) + { + if (std::holds_alternative(stepOrHookStarted)) + { + return { + std::get(stepOrHookStarted).test_case_started_id, + std::get(stepOrHookStarted).test_step_id, + }; + } + + return { std::nullopt, std::nullopt }; + } + + std::optional ReadTestRunHookStartedIds(StepOrHookStarted stepOrHookStarted) + { + if (std::holds_alternative(stepOrHookStarted)) + return std::get(stepOrHookStarted).id; + + return std::nullopt; + } + } + + ExecutionContext::ExecutionContext(util::Broadcaster& broadCaster, Context& context, StepOrHookStarted stepOrHookStarted) + : context{ context } + , broadCaster{ broadCaster } + , stepOrHookStarted{ std::move(stepOrHookStarted) } + {} + + void ExecutionContext::Attach(std::string data, OptionsOrMediaType mediaType) + { + Attach(std::move(data), cucumber::messages::attachment_content_encoding::IDENTITY, std::move(mediaType)); + } + + void ExecutionContext::Attach(std::istream& data, OptionsOrMediaType mediaType) + { + std::string buffer{ std::istreambuf_iterator{ data }, std::istreambuf_iterator{} }; + + buffer = base64::to_base64(buffer); + + Attach(std::move(buffer), cucumber::messages::attachment_content_encoding::BASE64, mediaType); + } + + void ExecutionContext::Log(std::string text) + { + Attach(std::move(text), std::string{ LogMediaType }); + } + + void ExecutionContext::Link(std::string url, std::optional title) + { + Attach(std::move(url), AttachOptions{ + .mediaType = LinkMediaType, + .fileName = std::move(title), + }); + } + + void ExecutionContext::Skipped(const std::string& message, std::source_location current) noexcept(false) + { + throw StepSkipped{ message, current }; + } + + void ExecutionContext::Pending(const std::string& message, std::source_location current) noexcept(false) + { + throw StepPending{ message, current }; + } + + void ExecutionContext::Attach(std::string data, cucumber::messages::attachment_content_encoding encoding, OptionsOrMediaType mediaType) + { + const auto options = std::holds_alternative(mediaType) + ? AttachOptions{ .mediaType = std::get(mediaType) } + : std::get(mediaType); + + auto [test_case_started_id, test_step_id] = ReadTestStepStartedIds(stepOrHookStarted); + auto test_run_hook_started_id = ReadTestRunHookStartedIds(stepOrHookStarted); + + broadCaster.BroadcastEvent({ + .attachment = cucumber::messages::attachment{ + .body = std::move(data), + .content_encoding = encoding, + .file_name = std::move(options.fileName), + .media_type = std::move(options.mediaType), + .test_case_started_id = std::move(test_case_started_id), + .test_step_id = std::move(test_step_id), + .test_run_hook_started_id = std::move(test_run_hook_started_id), + .timestamp = util::TimestampNow(), + }, + }); + } +} diff --git a/cucumber_cpp/library/engine/ExecutionContext.hpp b/cucumber_cpp/library/engine/ExecutionContext.hpp new file mode 100644 index 00000000..71ffa7ef --- /dev/null +++ b/cucumber_cpp/library/engine/ExecutionContext.hpp @@ -0,0 +1,53 @@ +#ifndef ENGINE_EXECUTION_CONTEXT_HPP +#define ENGINE_EXECUTION_CONTEXT_HPP + +#include "cucumber/messages/attachment_content_encoding.hpp" +#include "cucumber/messages/test_run_hook_started.hpp" +#include "cucumber/messages/test_step_started.hpp" +#include "cucumber_cpp/library/Context.hpp" +#include "cucumber_cpp/library/support/Body.hpp" +#include "cucumber_cpp/library/util/Broadcaster.hpp" +#include +#include +#include +#include +#include + +namespace cucumber_cpp::library::engine +{ + using StepSkipped = support::StepSkipped; + using StepPending = support::StepPending; + + struct AttachOptions + { + std::string mediaType; + std::optional fileName; + }; + + using OptionsOrMediaType = std::variant; + using StepOrHookStarted = std::variant; + + struct ExecutionContext + { + ExecutionContext(util::Broadcaster& broadCaster, Context& context, StepOrHookStarted stepOrHookStarted); + + protected: + void Attach(std::string data, OptionsOrMediaType mediaType); + void Attach(std::istream& data, OptionsOrMediaType mediaType); + void Log(std::string text); + void Link(std::string url, std::optional title = std::nullopt); + + [[noreturn]] static void Skipped(const std::string& message = "", std::source_location current = std::source_location::current()) noexcept(false); + [[noreturn]] static void Pending(const std::string& message = "", std::source_location current = std::source_location::current()) noexcept(false); + + Context& context; + + private: + void Attach(std::string data, cucumber::messages::attachment_content_encoding encoding, OptionsOrMediaType mediaType); + + util::Broadcaster& broadCaster; + StepOrHookStarted stepOrHookStarted; + }; +} + +#endif diff --git a/cucumber_cpp/library/engine/FailureHandler.cpp b/cucumber_cpp/library/engine/FailureHandler.cpp deleted file mode 100644 index 872aa81f..00000000 --- a/cucumber_cpp/library/engine/FailureHandler.cpp +++ /dev/null @@ -1,43 +0,0 @@ -#include "cucumber_cpp/library/engine/FailureHandler.hpp" -#include "cucumber_cpp/library/engine/ContextManager.hpp" -#include "cucumber_cpp/library/engine/Result.hpp" -#include "cucumber_cpp/library/report/Report.hpp" -#include "gtest/gtest.h" -#include -#include -#include - -namespace cucumber_cpp::library::engine -{ - TestAssertionHandlerImpl::TestAssertionHandlerImpl(cucumber_cpp::library::engine::ContextManager& contextManager, report::ReportForwarder& reportHandler) - : contextManager{ contextManager } - , reportHandler{ reportHandler } - {} - - void TestAssertionHandlerImpl::AddAssertionError(const char* file, int line, const std::string& message) - { - std::filesystem::path relativeFilePath = std::filesystem::relative(file); - - contextManager.CurrentContext().ExecutionStatus(cucumber_cpp::library::engine::Result::failed); - - if (auto* stepContext = contextManager.CurrentStepContext(); stepContext != nullptr) - stepContext->ExecutionStatus(cucumber_cpp::library::engine::Result::failed); - - reportHandler.Failure(message, relativeFilePath, line); - } - - GoogleTestEventListener::GoogleTestEventListener(TestAssertionHandler& testAssertionHandler) - : testAssertionHandler(testAssertionHandler) - {} - - void GoogleTestEventListener::OnTestPartResult(const testing::TestPartResult& testPartResult) - { - if (testPartResult.failed()) - { - testAssertionHandler.AddAssertionError( - testPartResult.file_name() != nullptr ? testPartResult.file_name() : "", - testPartResult.line_number(), - testPartResult.message()); - } - } -} diff --git a/cucumber_cpp/library/engine/FailureHandler.hpp b/cucumber_cpp/library/engine/FailureHandler.hpp deleted file mode 100644 index 89c0f401..00000000 --- a/cucumber_cpp/library/engine/FailureHandler.hpp +++ /dev/null @@ -1,41 +0,0 @@ -#ifndef ENGINE_FAILUREHANDLER_HPP -#define ENGINE_FAILUREHANDLER_HPP - -#include "cucumber_cpp/library/engine/ContextManager.hpp" -#include "cucumber_cpp/library/report/Report.hpp" -#include -#include -#include - -namespace cucumber_cpp::library::engine -{ - struct TestAssertionHandler - { - virtual ~TestAssertionHandler() = default; - - virtual void AddAssertionError(const char* file, int line, const std::string& message) = 0; - }; - - struct TestAssertionHandlerImpl : TestAssertionHandler - { - explicit TestAssertionHandlerImpl(cucumber_cpp::library::engine::ContextManager& contextManager, report::ReportForwarder& reportHandler); - - void AddAssertionError(const char* file, int line, const std::string& message) override; - - private: - cucumber_cpp::library::engine::ContextManager& contextManager; - report::ReportForwarder& reportHandler; - }; - - struct GoogleTestEventListener : testing::EmptyTestEventListener - { - explicit GoogleTestEventListener(TestAssertionHandler& testAssertionHandler); - - void OnTestPartResult(const testing::TestPartResult& testPartResult) override; - - private: - TestAssertionHandler& testAssertionHandler; - }; -} - -#endif diff --git a/cucumber_cpp/library/engine/FeatureFactory.cpp b/cucumber_cpp/library/engine/FeatureFactory.cpp deleted file mode 100644 index b254e627..00000000 --- a/cucumber_cpp/library/engine/FeatureFactory.cpp +++ /dev/null @@ -1,346 +0,0 @@ -#include "cucumber_cpp/library/engine/FeatureFactory.hpp" -#include "cucumber/gherkin/app.hpp" -#include "cucumber/gherkin/file.hpp" -#include "cucumber/gherkin/parse_error.hpp" -#include "cucumber/messages/background.hpp" -#include "cucumber/messages/feature.hpp" -#include "cucumber/messages/pickle.hpp" -#include "cucumber/messages/pickle_step.hpp" -#include "cucumber/messages/pickle_step_argument.hpp" -#include "cucumber/messages/pickle_step_type.hpp" -#include "cucumber/messages/pickle_tag.hpp" -#include "cucumber/messages/rule.hpp" -#include "cucumber/messages/scenario.hpp" -#include "cucumber/messages/step.hpp" -#include "cucumber/messages/tag.hpp" -#include "cucumber_cpp/library/Errors.hpp" -#include "cucumber_cpp/library/StepRegistry.hpp" -#include "cucumber_cpp/library/TagExpression.hpp" -#include "cucumber_cpp/library/engine/FeatureInfo.hpp" -#include "cucumber_cpp/library/engine/RuleInfo.hpp" -#include "cucumber_cpp/library/engine/ScenarioInfo.hpp" -#include "cucumber_cpp/library/engine/StepInfo.hpp" -#include "cucumber_cpp/library/engine/StepType.hpp" -#include "cucumber_cpp/library/engine/Table.hpp" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace cucumber_cpp::library::engine -{ - namespace - { - struct ScenarioWithRule - { - const cucumber::messages::rule* rule; - const cucumber::messages::scenario* scenario; - }; - - using AllTypes = std::variant< - const cucumber::messages::background*, - const cucumber::messages::scenario*, - const cucumber::messages::step*, - ScenarioWithRule>; - - using FlatAst = std::map>; - - void FlattenBackgroundAst(FlatAst& flatAst, const cucumber::messages::background& background) - { - flatAst[background.id] = &background; - for (const auto& step : background.steps) - flatAst[step.id] = &step; - } - - void FlattenScenarioAst(FlatAst& flatAst, const cucumber::messages::scenario& scenario) - { - flatAst[scenario.id] = &scenario; - for (const auto& step : scenario.steps) - flatAst[step.id] = &step; - } - - void FlattenRuleAst(FlatAst& flatAst, const cucumber::messages::rule& rule) - { - for (const auto& ruleChild : rule.children) - { - if (ruleChild.background) - { - flatAst[ruleChild.background->id] = &*ruleChild.background; - for (const auto& step : ruleChild.background->steps) - flatAst[step.id] = &step; - } - else if (ruleChild.scenario) - { - flatAst[ruleChild.scenario->id] = ScenarioWithRule{ &rule, &*ruleChild.scenario }; - for (const auto& step : ruleChild.scenario->steps) - flatAst[step.id] = &step; - } - } - } - - FlatAst FlattenAst(const cucumber::messages::feature& feature) - { - FlatAst flatAst; - - for (const auto& child : feature.children) - { - if (child.background) - FlattenBackgroundAst(flatAst, *child.background); - else if (child.rule) - FlattenRuleAst(flatAst, *child.rule); - else if (child.scenario) - FlattenScenarioAst(flatAst, *child.scenario); - } - - return flatAst; - } - - 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 }, - }; - - auto TableFactory(const std::optional& optionalPickleStepArgument) - { - std::vector> table{}; - - if (!optionalPickleStepArgument) - return table; - - if (!optionalPickleStepArgument.value().data_table) - return table; - - for (const auto& dataTable = optionalPickleStepArgument.value().data_table.value(); - const auto& row : dataTable.rows) - { - table.emplace_back(); - auto& back = table.back(); - - for (const auto& cols : row.cells) - back.emplace_back(cols.value); - } - - return table; - } - - std::string DocStringFactory(const std::optional& optionalPickleStepArgument) - { - if (!optionalPickleStepArgument) - return ""; - - if (!optionalPickleStepArgument.value().doc_string.has_value()) - return ""; - - return optionalPickleStepArgument.value().doc_string.value().content; - } - - std::set> TagsFactory(const std::vector& tags) - { - const auto range = tags | std::views::transform(&cucumber::messages::tag::name); - return { range.begin(), range.end() }; - } - - std::set> TagsFactory(const std::vector& tags) - { - const auto range = tags | std::views::transform(&cucumber::messages::pickle_tag::name); - return { range.begin(), range.end() }; - } - - void ConstructStep(const FeatureTreeFactory& featureTreeFactory, ScenarioInfo& scenarioInfo, const cucumber::messages::step& step, const cucumber::messages::pickle_step& pickleStep) - { - auto table = TableFactory(pickleStep.argument); - auto docString = DocStringFactory(pickleStep.argument); - - try - { - scenarioInfo.Children().push_back(featureTreeFactory.CreateStepInfo(stepTypeLut.at(*pickleStep.type), pickleStep.text, scenarioInfo, step.location.line, step.location.column.value_or(0), std::move(table), std::move(docString))); - } - catch (const std::out_of_range&) - { - throw UnsupportedAsteriskError{ std::format("{}:{}: * steps are not supported", scenarioInfo.FeatureInfo().Path().string(), step.location.line) }; - } - } - - void ConstructSteps(const FeatureTreeFactory& featureTreeFactory, ScenarioInfo& scenarioInfo, const FlatAst& flatAst, const std::vector& pickleSteps) - { - for (const auto& pickleStep : pickleSteps) - { - const auto* astStep = std::get(flatAst.at(pickleStep.ast_node_ids.front())); - - ConstructStep(featureTreeFactory, scenarioInfo, *astStep, pickleStep); - } - } - - void ConstructScenario(const FeatureTreeFactory& featureTreeFactory, FeatureInfo& featureInfo, const FlatAst& flatAst, const cucumber::messages::scenario& scenario, const cucumber::messages::pickle& pickle, std::string_view tagExpression) - { - auto tags = TagsFactory(pickle.tags); - - if (!IsTagExprSelected(tagExpression, tags)) - return; - - featureInfo.Scenarios().push_back(std::make_unique( - featureInfo, - std::move(tags), - pickle.name, - scenario.description, - scenario.location.line, - scenario.location.column.value_or(0))); - - ConstructSteps(featureTreeFactory, *featureInfo.Scenarios().back(), flatAst, pickle.steps); - } - - void ConstructScenario(const FeatureTreeFactory& featureTreeFactory, RuleInfo& ruleInfo, const FlatAst& flatAst, const cucumber::messages::scenario& scenario, const cucumber::messages::pickle& pickle, std::set> tags) - { - ruleInfo.Scenarios().push_back(std::make_unique( - ruleInfo, - std::move(tags), - pickle.name, - scenario.description, - scenario.location.line, - scenario.location.column.value_or(0))); - - ConstructSteps(featureTreeFactory, *ruleInfo.Scenarios().back(), flatAst, pickle.steps); - } - - RuleInfo& GetOrConstructRule(FeatureInfo& featureInfo, const cucumber::messages::rule& rule) - { - if (auto iter = std::ranges::find(featureInfo.Rules(), rule.id, &RuleInfo::Id); iter != featureInfo.Rules().end()) - return **iter; - - featureInfo.Rules().push_back( - std::make_unique(featureInfo, - rule.id, - rule.name, - rule.description, - rule.location.line, - rule.location.column.value_or(0))); - - return *featureInfo.Rules().back(); - } - - void ConstructScenarioWithRule(const FeatureTreeFactory& featureTreeFactory, FeatureInfo& featureInfo, const FlatAst& flatAst, const cucumber::messages::rule& rule, const cucumber::messages::scenario& scenario, const cucumber::messages::pickle& pickle, std::string_view tagExpression) - { - auto tags = TagsFactory(pickle.tags); - - if (!IsTagExprSelected(tagExpression, tags)) - return; - - auto& ruleInfo = GetOrConstructRule(featureInfo, rule); - - ConstructScenario(featureTreeFactory, ruleInfo, flatAst, scenario, pickle, std::move(tags)); - } - - struct ConstructScenarioVisitor - { - ConstructScenarioVisitor(const FeatureTreeFactory& featureTreeFactory, FeatureInfo& featureInfo, const FlatAst& flatAst, const cucumber::messages::pickle& pickle, std::string_view tagExpression) - : featureTreeFactory{ featureTreeFactory } - , featureInfo{ featureInfo } - , flatAst{ flatAst } - , pickle{ pickle } - , tagExpression{ tagExpression } - {} - - void operator()(const cucumber::messages::scenario* scenario) const - { - ConstructScenario(featureTreeFactory, featureInfo, flatAst, *scenario, pickle, tagExpression); - } - - void operator()(ScenarioWithRule ruleWithScenario) const - { - ConstructScenarioWithRule(featureTreeFactory, featureInfo, flatAst, *ruleWithScenario.rule, *ruleWithScenario.scenario, pickle, tagExpression); - } - - void operator()(auto /* ignore */) const - { - /* ignore */ - } - - private: - const FeatureTreeFactory& featureTreeFactory; - FeatureInfo& featureInfo; - const FlatAst& flatAst; - const cucumber::messages::pickle& pickle; - std::string_view tagExpression; - }; - - void ConstructScenario(const FeatureTreeFactory& featureTreeFactory, FeatureInfo& featureInfo, const FlatAst& flatAst, const cucumber::messages::pickle& pickle, std::string_view tagExpression) - { - ConstructScenarioVisitor visitor{ featureTreeFactory, featureInfo, flatAst, pickle, tagExpression }; - - std::visit(visitor, flatAst.at(pickle.ast_node_ids.front())); - } - - std::unique_ptr FeatureFactory(std::filesystem::path path, const cucumber::gherkin::app::parser_result& ast) - { - return std::make_unique( - TagsFactory(ast.feature->tags), - ast.feature->name, - ast.feature->description, - std::move(path), - ast.feature->location.line, - ast.feature->location.column.value_or(0)); - } - } - - FeatureTreeFactory::FeatureTreeFactory(StepRegistry& stepRegistry) - : stepRegistry{ stepRegistry } - {} - - std::unique_ptr FeatureTreeFactory::CreateStepInfo(StepType stepType, std::string stepText, const ScenarioInfo& scenarioInfo, std::size_t line, std::size_t column, std::vector> table, std::string docString) const - { - try - { - auto stepMatch = stepRegistry.Query(stepText); - return std::make_unique(scenarioInfo, std::move(stepText), stepType, line, column, std::move(table), std::move(docString), std::move(stepMatch)); - } - catch (const StepRegistry::StepNotFoundError&) - { - return std::make_unique(scenarioInfo, std::move(stepText), stepType, line, column, std::move(table), std::move(docString)); - } - catch (StepRegistry::AmbiguousStepError& ase) - { - return std::make_unique(scenarioInfo, std::move(stepText), stepType, line, column, std::move(table), std::move(docString), std::move(ase.matches)); - } - } - - std::unique_ptr FeatureTreeFactory::Create(const std::filesystem::path& path, std::string_view tagExpression) const - { - std::unique_ptr featureInfo; - std::optional flatAst; - - cucumber::gherkin::app::callbacks callbacks{ - .ast = [&path, &flatAst, &featureInfo](const cucumber::gherkin::app::parser_result& ast) - { - featureInfo = FeatureFactory(path, ast); - flatAst = FlattenAst(*ast.feature); - }, - .pickle = [this, &featureInfo, &flatAst, &tagExpression](const cucumber::messages::pickle& pickle) - { - ConstructScenario(*this, *featureInfo, *flatAst, pickle, tagExpression); - }, - .error = [](const cucumber::gherkin::parse_error& /* _ */) - { - /* not handled yet */ - } - }; - - cucumber::gherkin::app gherkin; - - gherkin.parse(cucumber::gherkin::file{ path.string() }, callbacks); - - return featureInfo; - } -} diff --git a/cucumber_cpp/library/engine/FeatureFactory.hpp b/cucumber_cpp/library/engine/FeatureFactory.hpp deleted file mode 100644 index a064f366..00000000 --- a/cucumber_cpp/library/engine/FeatureFactory.hpp +++ /dev/null @@ -1,32 +0,0 @@ -#ifndef ENGINE_FEATUREFACTORY_HPP -#define ENGINE_FEATUREFACTORY_HPP - -#include "cucumber/gherkin/app.hpp" -#include "cucumber_cpp/library/StepRegistry.hpp" -#include "cucumber_cpp/library/engine/FeatureInfo.hpp" -#include "cucumber_cpp/library/engine/StepInfo.hpp" -#include "cucumber_cpp/library/engine/StepType.hpp" -#include "cucumber_cpp/library/engine/Table.hpp" -#include -#include -#include -#include -#include -#include - -namespace cucumber_cpp::library::engine -{ - struct FeatureTreeFactory - { - explicit FeatureTreeFactory(StepRegistry& stepRegistry); - - [[nodiscard]] std::unique_ptr CreateStepInfo(StepType stepType, std::string stepText, const ScenarioInfo& scenarioInfo, std::size_t line, std::size_t column, std::vector> table, std::string docString) const; - - [[nodiscard]] std::unique_ptr Create(const std::filesystem::path& path, std::string_view tagExpression) const; - - private: - StepRegistry& stepRegistry; - }; -} - -#endif diff --git a/cucumber_cpp/library/engine/FeatureInfo.cpp b/cucumber_cpp/library/engine/FeatureInfo.cpp deleted file mode 100644 index 016f964c..00000000 --- a/cucumber_cpp/library/engine/FeatureInfo.cpp +++ /dev/null @@ -1,75 +0,0 @@ -#include "cucumber_cpp/library/engine/FeatureInfo.hpp" -#include "cucumber_cpp/library/engine/RuleInfo.hpp" -#include "cucumber_cpp/library/engine/ScenarioInfo.hpp" -#include -#include -#include -#include -#include -#include -#include -#include - -namespace cucumber_cpp::library::engine -{ - FeatureInfo::FeatureInfo(std::set> tags, std::string title, std::string description, std::filesystem::path path, std::size_t line, std::size_t column) - : tags{ std::move(tags) } - , title{ std::move(title) } - , description{ std::move(description) } - , path{ std::move(path) } - , line{ line } - , column{ column } - { - } - - const std::set>& FeatureInfo::Tags() const - { - return tags; - } - - const std::string& FeatureInfo::Title() const - { - return title; - } - - const std::string& FeatureInfo::Description() const - { - return description; - } - - const std::filesystem::path& FeatureInfo::Path() const - { - return path; - } - - std::size_t FeatureInfo::Line() const - { - return line; - } - - std::size_t FeatureInfo::Column() const - { - return column; - } - - std::vector>& FeatureInfo::Rules() - { - return rules; - } - - const std::vector>& FeatureInfo::Rules() const - { - return rules; - } - - std::vector>& FeatureInfo::Scenarios() - { - return scenarios; - } - - const std::vector>& FeatureInfo::Scenarios() const - { - return scenarios; - } - -} diff --git a/cucumber_cpp/library/engine/FeatureInfo.hpp b/cucumber_cpp/library/engine/FeatureInfo.hpp deleted file mode 100644 index 1cf18401..00000000 --- a/cucumber_cpp/library/engine/FeatureInfo.hpp +++ /dev/null @@ -1,50 +0,0 @@ -#ifndef ENGINE_FEATUREINFO_HPP -#define ENGINE_FEATUREINFO_HPP - -#include "cucumber_cpp/library/engine/RuleInfo.hpp" -#include "cucumber_cpp/library/engine/ScenarioInfo.hpp" -#include -#include -#include -#include -#include -#include -#include - -namespace cucumber_cpp::library::engine -{ - struct FeatureInfo - { - FeatureInfo(std::set> tags, std::string title, std::string description, std::filesystem::path path, std::size_t line, std::size_t column); - - [[nodiscard]] const std::set>& Tags() const; - [[nodiscard]] const std::string& Title() const; - [[nodiscard]] const std::string& Description() const; - - [[nodiscard]] const std::filesystem::path& Path() const; - - [[nodiscard]] std::size_t Line() const; - [[nodiscard]] std::size_t Column() const; - - [[nodiscard]] std::vector>& Rules(); - [[nodiscard]] const std::vector>& Rules() const; - - [[nodiscard]] std::vector>& Scenarios(); - [[nodiscard]] const std::vector>& Scenarios() const; - - private: - std::set> tags; - std::string title; - std::string description; - - std::filesystem::path path; - - std::size_t line; - std::size_t column; - - std::vector> rules; - std::vector> scenarios; - }; -} - -#endif diff --git a/cucumber_cpp/library/engine/Hook.cpp b/cucumber_cpp/library/engine/Hook.cpp new file mode 100644 index 00000000..3a51685e --- /dev/null +++ b/cucumber_cpp/library/engine/Hook.cpp @@ -0,0 +1,12 @@ +#include "cucumber_cpp/library/engine/Hook.hpp" +#include "cucumber_cpp/library/Context.hpp" +#include "cucumber_cpp/library/engine/ExecutionContext.hpp" +#include "cucumber_cpp/library/util/Broadcaster.hpp" +#include + +namespace cucumber_cpp::library::engine +{ + HookBase::HookBase(util::Broadcaster& broadCaster, Context& context, engine::StepOrHookStarted stepOrHookStarted) + : engine::ExecutionContext{ broadCaster, context, std::move(stepOrHookStarted) } + {} +} diff --git a/cucumber_cpp/library/engine/Hook.hpp b/cucumber_cpp/library/engine/Hook.hpp new file mode 100644 index 00000000..27340646 --- /dev/null +++ b/cucumber_cpp/library/engine/Hook.hpp @@ -0,0 +1,28 @@ +#ifndef ENGINE_HOOK_HPP +#define ENGINE_HOOK_HPP + +#include "cucumber_cpp/library/Context.hpp" +#include "cucumber_cpp/library/engine/ExecutionContext.hpp" +#include "cucumber_cpp/library/util/Broadcaster.hpp" + +namespace cucumber_cpp::library::engine +{ + struct HookBase : engine::ExecutionContext + { + HookBase(util::Broadcaster& broadCaster, Context& context, StepOrHookStarted stepOrHookStarted); + + virtual ~HookBase() = default; + + virtual void SetUp() + { + /* nothing to do */ + } + + virtual void TearDown() + { + /* nothing to do */ + } + }; +} + +#endif diff --git a/cucumber_cpp/library/engine/HookExecutor.cpp b/cucumber_cpp/library/engine/HookExecutor.cpp deleted file mode 100644 index 1c2f4fbd..00000000 --- a/cucumber_cpp/library/engine/HookExecutor.cpp +++ /dev/null @@ -1,85 +0,0 @@ -#include "cucumber_cpp/library/engine/HookExecutor.hpp" -#include "cucumber_cpp/library/HookRegistry.hpp" -#include "cucumber_cpp/library/engine/ContextManager.hpp" -#include "cucumber_cpp/library/engine/FeatureInfo.hpp" -#include "cucumber_cpp/library/engine/Result.hpp" -#include "cucumber_cpp/library/engine/ScenarioInfo.hpp" -#include "cucumber_cpp/library/engine/StepInfo.hpp" -#include -#include -#include - -namespace cucumber_cpp::library::engine -{ - namespace - { - constexpr HookPair programHooks{ HookType::beforeAll, HookType::afterAll }; - constexpr HookPair featureHooks{ HookType::beforeFeature, HookType::afterFeature }; - constexpr HookPair scenarioHooks{ HookType::before, HookType::after }; - constexpr HookPair stepHooks{ HookType::beforeStep, HookType::afterStep }; - - void ExecuteHook(cucumber_cpp::library::engine::RunnerContext& runnerContext, HookType hook, const std::set>& tags) - { - for (const auto& match : HookRegistry::Instance().Query(hook, tags)) - { - match.factory(runnerContext)->Execute(); - - if (runnerContext.ExecutionStatus() != cucumber_cpp::library::engine::Result::passed) - return; - } - } - } - - HookExecutor::ScopedHook::ScopedHook(cucumber_cpp::library::engine::RunnerContext& runnerContext, HookPair hookPair, const std::set>& tags) - : runnerContext{ runnerContext } - , hookPair{ hookPair } - , tags{ tags } - { - ExecuteHook(runnerContext, hookPair.before, tags); - } - - HookExecutor::ScopedHook::~ScopedHook() - { - ExecuteHook(runnerContext, hookPair.after, tags); - } - - HookExecutor::ProgramScope::ProgramScope(cucumber_cpp::library::engine::ContextManager& contextManager) - : ScopedHook{ contextManager.ProgramContext(), programHooks, {} } - {} - - HookExecutor::FeatureScope::FeatureScope(cucumber_cpp::library::engine::ContextManager& contextManager) - : ScopedHook{ contextManager.FeatureContext(), featureHooks, contextManager.FeatureContext().info.Tags() } - {} - - HookExecutor::ScenarioScope::ScenarioScope(cucumber_cpp::library::engine::ContextManager& contextManager) - : ScopedHook{ contextManager.ScenarioContext(), scenarioHooks, contextManager.ScenarioContext().info.Tags() } - {} - - HookExecutor::StepScope::StepScope(cucumber_cpp::library::engine::ContextManager& contextManager) - : ScopedHook{ contextManager.ScenarioContext(), stepHooks, contextManager.ScenarioContext().info.Tags() } - {} - - HookExecutorImpl::HookExecutorImpl(cucumber_cpp::library::engine::ContextManager& contextManager) - : contextManager{ contextManager } - {} - - HookExecutor::ProgramScope HookExecutorImpl::BeforeAll() - { - return ProgramScope{ contextManager }; - } - - HookExecutor::FeatureScope HookExecutorImpl::FeatureStart() - { - return FeatureScope{ contextManager }; - } - - HookExecutor::ScenarioScope HookExecutorImpl::ScenarioStart() - { - return ScenarioScope{ contextManager }; - } - - HookExecutor::StepScope HookExecutorImpl::StepStart() - { - return StepScope{ contextManager }; - } -} diff --git a/cucumber_cpp/library/engine/HookExecutor.hpp b/cucumber_cpp/library/engine/HookExecutor.hpp deleted file mode 100644 index e74dfcf7..00000000 --- a/cucumber_cpp/library/engine/HookExecutor.hpp +++ /dev/null @@ -1,88 +0,0 @@ -#ifndef ENGINE_HOOKEXECUTOR_HPP -#define ENGINE_HOOKEXECUTOR_HPP - -#include "cucumber_cpp/library/HookRegistry.hpp" -#include "cucumber_cpp/library/engine/ContextManager.hpp" -#include "cucumber_cpp/library/util/Immoveable.hpp" -#include -#include -#include -#include - -namespace cucumber_cpp::library::engine -{ - - struct HookExecutor - { - protected: - ~HookExecutor() = default; - - public: - struct ProgramScope; - struct FeatureScope; - struct ScenarioScope; - struct StepScope; - - [[nodiscard]] virtual ProgramScope BeforeAll() = 0; - [[nodiscard]] virtual FeatureScope FeatureStart() = 0; - [[nodiscard]] virtual ScenarioScope ScenarioStart() = 0; - [[nodiscard]] virtual StepScope StepStart() = 0; - - private: - struct ScopedHook; - }; - - struct HookPair - { - const HookType before; - const HookType after; - }; - - struct HookExecutor::ScopedHook : util::Immoveable - { - ScopedHook(cucumber_cpp::library::engine::RunnerContext& runnerContext, HookPair hookPair, const std::set>& tags); - ~ScopedHook(); - - private: - cucumber_cpp::library::engine::RunnerContext& runnerContext; - HookPair hookPair; - const std::set>& tags; - }; - - struct HookExecutor::ProgramScope : private ScopedHook - { - explicit ProgramScope(cucumber_cpp::library::engine::ContextManager& contextManager); - }; - - struct HookExecutor::FeatureScope : private ScopedHook - { - explicit FeatureScope(cucumber_cpp::library::engine::ContextManager& contextManager); - }; - - struct HookExecutor::ScenarioScope : private ScopedHook - { - explicit ScenarioScope(cucumber_cpp::library::engine::ContextManager& contextManager); - }; - - struct HookExecutor::StepScope : private ScopedHook - { - explicit StepScope(cucumber_cpp::library::engine::ContextManager& contextManager); - }; - - struct HookExecutorImpl : HookExecutor - { - explicit HookExecutorImpl(cucumber_cpp::library::engine::ContextManager& contextManager); - virtual ~HookExecutorImpl() = default; - - [[nodiscard]] ProgramScope - BeforeAll() override; - [[nodiscard]] FeatureScope FeatureStart() override; - [[nodiscard]] ScenarioScope ScenarioStart() override; - [[nodiscard]] StepScope StepStart() override; - - private: - cucumber_cpp::library::engine::ContextManager& contextManager; - }; -} - -#endif diff --git a/cucumber_cpp/library/engine/Result.hpp b/cucumber_cpp/library/engine/Result.hpp deleted file mode 100644 index a850ba3f..00000000 --- a/cucumber_cpp/library/engine/Result.hpp +++ /dev/null @@ -1,17 +0,0 @@ -#ifndef ENGINE_RESULT_HPP -#define ENGINE_RESULT_HPP - -namespace cucumber_cpp::library::engine -{ - enum struct Result - { - passed, - skipped, - pending, - undefined, - ambiguous, - failed - }; -} - -#endif diff --git a/cucumber_cpp/library/engine/RuleInfo.cpp b/cucumber_cpp/library/engine/RuleInfo.cpp deleted file mode 100644 index 72a20781..00000000 --- a/cucumber_cpp/library/engine/RuleInfo.cpp +++ /dev/null @@ -1,67 +0,0 @@ -#include "cucumber_cpp/library/engine/RuleInfo.hpp" -#include "cucumber_cpp/library/engine/FeatureInfo.hpp" -#include "cucumber_cpp/library/engine/ScenarioInfo.hpp" -#include -#include -#include -#include -#include -#include -#include - -namespace cucumber_cpp::library::engine -{ - RuleInfo::RuleInfo(const struct FeatureInfo& featureInfo, std::string id, std::string title, std::string description, std::size_t line, std::size_t column) - : featureInfo{ featureInfo } - , id{ std::move(id) } - , title{ std::move(title) } - , description{ std::move(description) } - , line{ line } - , column{ column } - {} - - const FeatureInfo& RuleInfo::FeatureInfo() const - { - return featureInfo; - } - - [[nodiscard]] std::string_view RuleInfo::Id() const - { - return id; - } - - const std::string& RuleInfo::Title() const - { - return title; - } - - const std::string& RuleInfo::Description() const - { - return description; - } - - const std::filesystem::path& RuleInfo::Path() const - { - return FeatureInfo().Path(); - } - - std::size_t RuleInfo::Line() const - { - return line; - } - - std::size_t RuleInfo::Column() const - { - return column; - } - - std::vector>& RuleInfo::Scenarios() - { - return scenarios; - } - - const std::vector>& RuleInfo::Scenarios() const - { - return scenarios; - } -} diff --git a/cucumber_cpp/library/engine/RuleInfo.hpp b/cucumber_cpp/library/engine/RuleInfo.hpp deleted file mode 100644 index 2dabfc21..00000000 --- a/cucumber_cpp/library/engine/RuleInfo.hpp +++ /dev/null @@ -1,50 +0,0 @@ -#ifndef ENGINE_RULEINFO_HPP -#define ENGINE_RULEINFO_HPP - -#include "cucumber_cpp/library/engine/StepInfo.hpp" -#include -#include -#include -#include -#include -#include - -namespace cucumber_cpp::library::engine -{ - struct FeatureInfo; - - struct RuleInfo - { - RuleInfo(const FeatureInfo& featureInfo, std::string id, std::string title, std::string description, std::size_t line, std::size_t column); - - [[nodiscard]] const struct FeatureInfo& FeatureInfo() const; - - [[nodiscard]] std::string_view Id() const; - - [[nodiscard]] const std::string& Title() const; - [[nodiscard]] const std::string& Description() const; - - [[nodiscard]] const std::filesystem::path& Path() const; - - [[nodiscard]] std::size_t Line() const; - [[nodiscard]] std::size_t Column() const; - - [[nodiscard]] std::vector>& Scenarios(); - [[nodiscard]] const std::vector>& Scenarios() const; - - private: - const struct FeatureInfo& featureInfo; - - std::string id; - - std::string title; - std::string description; - - std::size_t line; - std::size_t column; - - std::vector> scenarios; - }; -} - -#endif diff --git a/cucumber_cpp/library/engine/ScenarioInfo.cpp b/cucumber_cpp/library/engine/ScenarioInfo.cpp deleted file mode 100644 index 655a93b4..00000000 --- a/cucumber_cpp/library/engine/ScenarioInfo.cpp +++ /dev/null @@ -1,96 +0,0 @@ -#include "cucumber_cpp/library/engine/ScenarioInfo.hpp" -#include "cucumber_cpp/library/engine/FeatureInfo.hpp" -#include "cucumber_cpp/library/engine/RuleInfo.hpp" -#include "cucumber_cpp/library/engine/StepInfo.hpp" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace cucumber_cpp::library::engine -{ - ScenarioInfo::ScenarioInfo(const struct RuleInfo& ruleInfo, std::set> tags, std::string title, std::string description, std::size_t line, std::size_t column) - : parentInfo{ &ruleInfo } - , tags{ std::move(tags) } - , title{ std::move(title) } - , description{ std::move(description) } - , line{ line } - , column{ column } - {} - - ScenarioInfo::ScenarioInfo(const struct FeatureInfo& featureInfo, std::set> tags, std::string title, std::string description, std::size_t line, std::size_t column) - : parentInfo{ &featureInfo } - , tags{ std::move(tags) } - , title{ std::move(title) } - , description{ std::move(description) } - , line{ line } - , column{ column } - { - } - - const FeatureInfo& ScenarioInfo::FeatureInfo() const - { - if (std::holds_alternative(parentInfo)) - return *std::get(parentInfo); - - if (std::holds_alternative(parentInfo)) - return std::get(parentInfo)->FeatureInfo(); - - std::abort(); - } - - std::optional> ScenarioInfo::RuleInfo() const - { - if (std::holds_alternative(parentInfo)) - return *std::get(parentInfo); - - return std::nullopt; - } - - const std::set>& ScenarioInfo::Tags() const - { - return tags; - } - - const std::string& ScenarioInfo::Title() const - { - return title; - } - - const std::string& ScenarioInfo::Description() const - { - return description; - } - - const std::filesystem::path& ScenarioInfo::Path() const - { - return FeatureInfo().Path(); - } - - std::size_t ScenarioInfo::Line() const - { - return line; - } - - std::size_t ScenarioInfo::Column() const - { - return column; - } - - std::vector>& ScenarioInfo::Children() - { - return children; - } - - const std::vector>& ScenarioInfo::Children() const - { - return children; - } -} diff --git a/cucumber_cpp/library/engine/ScenarioInfo.hpp b/cucumber_cpp/library/engine/ScenarioInfo.hpp deleted file mode 100644 index c502da49..00000000 --- a/cucumber_cpp/library/engine/ScenarioInfo.hpp +++ /dev/null @@ -1,54 +0,0 @@ -#ifndef ENGINE_SCENARIOINFO_HPP -#define ENGINE_SCENARIOINFO_HPP - -#include "cucumber_cpp/library/engine/RuleInfo.hpp" -#include "cucumber_cpp/library/engine/StepInfo.hpp" -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace cucumber_cpp::library::engine -{ - struct FeatureInfo; - - struct ScenarioInfo - { - ScenarioInfo(const RuleInfo& ruleInfo, std::set> tags, std::string title, std::string description, std::size_t line, std::size_t column); - ScenarioInfo(const FeatureInfo& featureInfo, std::set> tags, std::string title, std::string description, std::size_t line, std::size_t column); - - [[nodiscard]] const struct FeatureInfo& FeatureInfo() const; - [[nodiscard]] std::optional> RuleInfo() const; - - [[nodiscard]] const std::set>& Tags() const; - [[nodiscard]] const std::string& Title() const; - [[nodiscard]] const std::string& Description() const; - - [[nodiscard]] const std::filesystem::path& Path() const; - - [[nodiscard]] std::size_t Line() const; - [[nodiscard]] std::size_t Column() const; - - [[nodiscard]] std::vector>& Children(); - [[nodiscard]] const std::vector>& Children() const; - - private: - std::variant parentInfo; - - std::set> tags; - std::string title; - std::string description; - - std::size_t line; - std::size_t column; - - std::vector> children; - }; -} - -#endif diff --git a/cucumber_cpp/library/engine/Step.cpp b/cucumber_cpp/library/engine/Step.cpp index 140b26eb..095d383a 100644 --- a/cucumber_cpp/library/engine/Step.cpp +++ b/cucumber_cpp/library/engine/Step.cpp @@ -1,36 +1,39 @@ #include "cucumber_cpp/library/engine/Step.hpp" +#include "cucumber/messages/pickle_doc_string.hpp" +#include "cucumber/messages/pickle_table.hpp" #include "cucumber_cpp/library/Context.hpp" -#include "cucumber_cpp/library/engine/StepType.hpp" -#include "cucumber_cpp/library/engine/Table.hpp" -#include "cucumber_cpp/library/engine/TestRunner.hpp" -#include +#include "cucumber_cpp/library/engine/ExecutionContext.hpp" +#include "cucumber_cpp/library/runtime/NestedTestCaseRunner.hpp" +#include "cucumber_cpp/library/util/Broadcaster.hpp" +#include #include namespace cucumber_cpp::library::engine { - Step::Step(Context& context, const Table& table, const std::string& docString) - : context{ context } - , table{ table } + StepBase::StepBase(const runtime::NestedTestCaseRunner& nestedTestCaseRunner, util::Broadcaster& broadCaster, Context& context, engine::StepOrHookStarted stepOrHookStarted, const std::optional& dataTable, const std::optional& docString) + : ExecutionContext{ broadCaster, context, stepOrHookStarted } + , nestedTestCaseRunner{ nestedTestCaseRunner } + , dataTable{ dataTable } , docString{ docString } {} - void Step::Given(const std::string& step) const + void StepBase::Step(const std::string& step) const { - TestRunner::Instance().NestedStep(StepType::given, step); + nestedTestCaseRunner.Step(step); } - void Step::When(const std::string& step) const + void StepBase::Step(const std::string& step, const std::optional& docString) const { - TestRunner::Instance().NestedStep(StepType::when, step); + nestedTestCaseRunner.Step(step, docString); } - void Step::Then(const std::string& step) const + void StepBase::Step(const std::string& step, const std::optional& dataTable) const { - TestRunner::Instance().NestedStep(StepType::then, step); + nestedTestCaseRunner.Step(step, dataTable); } - void Step::Pending(const std::string& message, std::source_location current) noexcept(false) + void StepBase::Step(const std::string& step, const std::optional& dataTable, const std::optional& docString) const { - throw StepPending{ message, current }; + nestedTestCaseRunner.Step(step, dataTable, docString); } } diff --git a/cucumber_cpp/library/engine/Step.hpp b/cucumber_cpp/library/engine/Step.hpp index de95fcd2..33188b58 100644 --- a/cucumber_cpp/library/engine/Step.hpp +++ b/cucumber_cpp/library/engine/Step.hpp @@ -4,31 +4,21 @@ // IWYU pragma: private, include "cucumber_cpp/CucumberCpp.hpp" // IWYU pragma: friend cucumber_cpp/.* +#include "cucumber/messages/pickle_doc_string.hpp" +#include "cucumber/messages/pickle_table.hpp" #include "cucumber_cpp/library/Context.hpp" -#include "cucumber_cpp/library/engine/Table.hpp" -#include -#include +#include "cucumber_cpp/library/engine/ExecutionContext.hpp" +#include "cucumber_cpp/library/runtime/NestedTestCaseRunner.hpp" +#include "cucumber_cpp/library/util/Broadcaster.hpp" +#include #include -#include namespace cucumber_cpp::library::engine { - struct Step + struct StepBase : ExecutionContext { - 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, const std::string& docString); - virtual ~Step() = default; + StepBase(const runtime::NestedTestCaseRunner& nestedTestCaseRunner, util::Broadcaster& broadCaster, Context& context, engine::StepOrHookStarted stepOrHookStarted, const std::optional& dataTable, const std::optional& docString); + virtual ~StepBase() = default; virtual void SetUp() { @@ -41,15 +31,15 @@ namespace cucumber_cpp::library::engine } protected: - void Given(const std::string& step) const; - void When(const std::string& step) const; - void Then(const std::string& step) const; + void Step(const std::string& step) const; + void Step(const std::string& step, const std::optional& docString) const; + void Step(const std::string& step, const std::optional& dataTable) const; + void Step(const std::string& step, const std::optional& dataTable, const std::optional& docString) const; - [[noreturn]] static void Pending(const std::string& message, std::source_location current = std::source_location::current()) noexcept(false); + const runtime::NestedTestCaseRunner& nestedTestCaseRunner; - Context& context; - const Table& table; - const std::string& docString; + const std::optional& dataTable; + const std::optional& docString; }; } diff --git a/cucumber_cpp/library/engine/StepInfo.cpp b/cucumber_cpp/library/engine/StepInfo.cpp deleted file mode 100644 index f21b6f24..00000000 --- a/cucumber_cpp/library/engine/StepInfo.cpp +++ /dev/null @@ -1,87 +0,0 @@ -#include "cucumber_cpp/library/engine/StepInfo.hpp" -#include "cucumber_cpp/library/StepRegistry.hpp" -#include "cucumber_cpp/library/engine/StepType.hpp" -#include "cucumber_cpp/library/engine/Table.hpp" -#include -#include -#include -#include -#include - -namespace cucumber_cpp::library::engine -{ - StepInfo::StepInfo(const struct ScenarioInfo& scenarioInfo, std::string text, StepType type, std::size_t line, std::size_t column, std::vector> table, std::string docString) - : scenarioInfo{ scenarioInfo } - , text{ std::move(text) } - , type{ type } - , line{ line } - , column{ column } - , table{ std::move(table) } - , docString{ std::move(docString) } - { - } - - StepInfo::StepInfo(const struct ScenarioInfo& scenarioInfo, std::string text, StepType type, std::size_t line, std::size_t column, std::vector> table, std::string docString, struct StepMatch stepMatch) - : scenarioInfo{ scenarioInfo } - , text{ std::move(text) } - , type{ type } - , line{ line } - , column{ column } - , table{ std::move(table) } - , docString{ std::move(docString) } - , stepMatch{ std::move(stepMatch) } - { - } - - StepInfo::StepInfo(const struct ScenarioInfo& scenarioInfo, std::string text, StepType type, std::size_t line, std::size_t column, std::vector> table, std::string docString, std::vector stepMatches) - : scenarioInfo{ scenarioInfo } - , text{ std::move(text) } - , type{ type } - , line{ line } - , column{ column } - , table{ std::move(table) } - , docString{ std::move(docString) } - , stepMatch{ std::move(stepMatches) } - { - } - - const struct ScenarioInfo& StepInfo::ScenarioInfo() const - { - return scenarioInfo; - } - - const std::string& StepInfo::Text() const - { - return text; - } - - StepType StepInfo::Type() const - { - return type; - } - - std::size_t StepInfo::Line() const - { - return line; - } - - std::size_t StepInfo::Column() const - { - return column; - } - - [[nodiscard]] const std::vector>& StepInfo::Table() const - { - return table; - } - - const std::string& StepInfo::DocString() const - { - return docString; - } - - const std::variant>& StepInfo::StepMatch() const - { - return stepMatch; - } -} diff --git a/cucumber_cpp/library/engine/StepInfo.hpp b/cucumber_cpp/library/engine/StepInfo.hpp deleted file mode 100644 index 2e3b841c..00000000 --- a/cucumber_cpp/library/engine/StepInfo.hpp +++ /dev/null @@ -1,80 +0,0 @@ -#ifndef ENGINE_STEPINFO_HPP -#define ENGINE_STEPINFO_HPP - -#include "cucumber_cpp/library/StepRegistry.hpp" -#include "cucumber_cpp/library/engine/StepType.hpp" -#include "cucumber_cpp/library/engine/Table.hpp" -#include -#include -#include -#include -#include - -namespace cucumber_cpp::library::engine -{ - struct ScenarioInfo; - - struct StepInfo - { - StepInfo(const struct ScenarioInfo& scenarioInfo, std::string text, StepType type, std::size_t line, std::size_t column, std::vector> table, std::string docString); - StepInfo(const struct ScenarioInfo& scenarioInfo, std::string text, StepType type, std::size_t line, std::size_t column, std::vector> table, std::string docString, StepMatch); - StepInfo(const struct ScenarioInfo& scenarioInfo, std::string text, StepType type, std::size_t line, std::size_t column, std::vector> table, std::string docString, std::vector); - - [[nodiscard]] const struct ScenarioInfo& ScenarioInfo() const; - - [[nodiscard]] const std::string& Text() const; - [[nodiscard]] StepType Type() const; - - [[nodiscard]] std::size_t Line() const; - [[nodiscard]] std::size_t Column() const; - - [[nodiscard]] const std::vector>& Table() const; - [[nodiscard]] const std::string& DocString() const; - [[nodiscard]] const std::variant>& StepMatch() const; - - private: - const struct ScenarioInfo& scenarioInfo; - - std::string text; - StepType type; - - std::size_t line; - std::size_t column; - - std::vector> table; - std::string docString; - std::variant> stepMatch; - }; - - struct NestedStepInfo - { - NestedStepInfo(std::string text, StepType type, std::filesystem::path, std::size_t line, std::size_t column, std::vector> table); - NestedStepInfo(std::string text, StepType type, std::filesystem::path, std::size_t line, std::size_t column, std::vector> table, StepMatch); - NestedStepInfo(std::string text, StepType type, std::filesystem::path, std::size_t line, std::size_t column, std::vector> table, std::vector); - - [[nodiscard]] const std::filesystem::path& FilePath() const; - - [[nodiscard]] const std::string& Text() const; - [[nodiscard]] StepType Type() const; - - [[nodiscard]] std::size_t Line() const; - [[nodiscard]] std::size_t Column() const; - - [[nodiscard]] const std::vector>& Table() const; - [[nodiscard]] const std::variant>& StepMatch() const; - - private: - const std::filesystem::path filePath; - - std::string text; - StepType type; - - std::size_t line; - std::size_t column; - - std::vector> table; - std::variant> stepMatch; - }; -} - -#endif diff --git a/cucumber_cpp/library/engine/Table.cpp b/cucumber_cpp/library/engine/Table.cpp deleted file mode 100644 index 65db3680..00000000 --- a/cucumber_cpp/library/engine/Table.cpp +++ /dev/null @@ -1,27 +0,0 @@ -#ifndef ENGINE_TABLE_HPP -#define ENGINE_TABLE_HPP - -#include -#include -#include -#include - -namespace cucumber_cpp::library::engine -{ - struct TableValue - { - template - T As(std::source_location sourceLocation = std::source_location::current()) const; - - explicit TableValue(std::string value) - : value(std::move(value)) - {} - - private: - std::string value; - }; - - using Table = std::vector>; -} - -#endif diff --git a/cucumber_cpp/library/engine/Table.hpp b/cucumber_cpp/library/engine/Table.hpp deleted file mode 100644 index 4df18c47..00000000 --- a/cucumber_cpp/library/engine/Table.hpp +++ /dev/null @@ -1,30 +0,0 @@ -#ifndef ENGINE_TABLE_HPP -#define ENGINE_TABLE_HPP - -#include "cucumber_cpp/library/engine/StringTo.hpp" -#include -#include -#include - -namespace cucumber_cpp::library::engine -{ - struct TableValue - { - template - T As() const - { - return StringTo(value); - } - - explicit TableValue(std::string value) - : value(std::move(value)) - {} - - private: - std::string value; - }; - - using Table = std::vector>; -} - -#endif diff --git a/cucumber_cpp/library/engine/TestExecution.cpp b/cucumber_cpp/library/engine/TestExecution.cpp deleted file mode 100644 index ae1c86dc..00000000 --- a/cucumber_cpp/library/engine/TestExecution.cpp +++ /dev/null @@ -1,154 +0,0 @@ -#include "cucumber_cpp/library/engine/TestExecution.hpp" -#include "cucumber_cpp/library/StepRegistry.hpp" -#include "cucumber_cpp/library/engine/ContextManager.hpp" -#include "cucumber_cpp/library/engine/FeatureInfo.hpp" -#include "cucumber_cpp/library/engine/HookExecutor.hpp" -#include "cucumber_cpp/library/engine/Result.hpp" -#include "cucumber_cpp/library/engine/RuleInfo.hpp" -#include "cucumber_cpp/library/engine/StepInfo.hpp" -#include "cucumber_cpp/library/report/Report.hpp" -#include "gtest/gtest.h" -#include -#include -#include - -namespace cucumber_cpp::library::engine -{ - const DryRunPolicy dryRunPolicy; - const ExecuteRunPolicy executeRunPolicy; - - TestExecution::ProgramScope::ProgramScope(cucumber_cpp::library::engine::ContextManager& contextManager, report::ReportForwarder& reportHandler, HookExecutor& hookExecution) - : contextManager{ contextManager } - , scopedProgramReport{ reportHandler.ProgramStart() } - , scopedProgramHook{ hookExecution.BeforeAll() } - { - } - - Result TestExecution::ProgramScope::ExecutionStatus() const - { - return contextManager.ProgramContext().ExecutionStatus(); - } - - TestExecution::FeatureScope::FeatureScope(cucumber_cpp::library::engine::ContextManager& contextManager, report::ReportForwarder& reportHandler, HookExecutor& hookExecution, const cucumber_cpp::library::engine::FeatureInfo& featureInfo) - : scopedFeatureContext{ contextManager.CreateFeatureContext(featureInfo) } - , scopedFeatureReport{ reportHandler.FeatureStart() } - , scopedFeatureHook{ hookExecution.FeatureStart() } - { - } - - Result TestExecution::FeatureScope::ExecutionStatus() const - { - return scopedFeatureContext.CurrentContext().ExecutionStatus(); - } - - TestExecution::RuleScope::RuleScope(cucumber_cpp::library::engine::ContextManager& contextManager, report::ReportForwarder& reportHandler, const cucumber_cpp::library::engine::RuleInfo& ruleInfo) - : scopedRuleContext{ contextManager.CreateRuleContext(ruleInfo) } - , scopedRuleReport{ reportHandler.RuleStart() } - { - } - - TestExecution::ScenarioScope::ScenarioScope(cucumber_cpp::library::engine::ContextManager& contextManager, report::ReportForwarder& reportHandler, HookExecutor& hookExecution, const cucumber_cpp::library::engine::ScenarioInfo& scenarioInfo) - : scopedScenarioContext{ contextManager.CreateScenarioContext(scenarioInfo) } - , scopedScenarioReport{ reportHandler.ScenarioStart() } - , scopedScenarioHook{ hookExecution.ScenarioStart() } - { - } - - void DryRunPolicy::RunStep(cucumber_cpp::library::engine::ContextManager& /* contextManager */, const StepMatch& /* stepMatch */) const - { - /* don't execute actual steps */ - } - - void ExecuteRunPolicy::RunStep(cucumber_cpp::library::engine::ContextManager& contextManager, const StepMatch& stepMatch) const - { - const auto& stepContext = contextManager.StepContext(); - auto& scenarioContext = contextManager.ScenarioContext(); - - stepMatch.factory(scenarioContext, stepContext.info.Table(), stepContext.info.DocString())->Execute(stepMatch.matches); - } - - TestExecutionImpl::TestExecutionImpl(cucumber_cpp::library::engine::ContextManager& contextManager, report::ReportForwarder& reportHandler, HookExecutor& hookExecution, const Policy& executionPolicy) - : contextManager{ contextManager } - , reportHandler{ reportHandler } - , hookExecution{ hookExecution } - , executionPolicy{ executionPolicy } - { - auto& listeners = testing::UnitTest::GetInstance()->listeners(); - listeners.Append(&googleTestEventListener); - } - - TestExecutionImpl::~TestExecutionImpl() - { - auto& listeners = testing::UnitTest::GetInstance()->listeners(); - listeners.Release(&googleTestEventListener); - } - - TestExecution::ProgramScope TestExecutionImpl::StartRun() - { - return ProgramScope{ contextManager, reportHandler, hookExecution }; - } - - TestExecution::FeatureScope TestExecutionImpl::StartFeature(const cucumber_cpp::library::engine::FeatureInfo& featureInfo) - { - return FeatureScope{ contextManager, reportHandler, hookExecution, featureInfo }; - } - - TestExecution::RuleScope TestExecutionImpl::StartRule(const cucumber_cpp::library::engine::RuleInfo& ruleInfo) - { - return RuleScope{ contextManager, reportHandler, ruleInfo }; - } - - TestExecution::ScenarioScope TestExecutionImpl::StartScenario(const cucumber_cpp::library::engine::ScenarioInfo& scenarioInfo) - { - return ScenarioScope{ contextManager, reportHandler, hookExecution, scenarioInfo }; - } - - void TestExecutionImpl::RunStep(const cucumber_cpp::library::engine::StepInfo& stepInfo) - { - auto scopedContext = contextManager.CreateStepContext(stepInfo); - if (contextManager.ScenarioContext().ExecutionStatus() == Result::passed) - { - const auto scopedStepReport = reportHandler.StepStart(); - const auto scopedStepHook = hookExecution.StepStart(); - - std::visit([this](const auto& value) - { - RunStepMatch(value); - }, - stepInfo.StepMatch()); - } - else - { - contextManager.StepContext().ExecutionStatus(cucumber_cpp::library::engine::Result::skipped); - reportHandler.StepSkipped(); - } - } - - void TestExecutionImpl::RunStepMatch(std::monostate /* not used */) - { - contextManager.StepContext().ExecutionStatus(cucumber_cpp::library::engine::Result::undefined); - } - - void TestExecutionImpl::RunStepMatch(const std::vector& /* not used */) - { - contextManager.StepContext().ExecutionStatus(cucumber_cpp::library::engine::Result::ambiguous); - } - - void TestExecutionImpl::RunStepMatch(const StepMatch& stepMatch) - { - executionPolicy.RunStep(contextManager, stepMatch); - } - - TestExecutionImplWithoutDefaultGoogleListener::TestExecutionImplWithoutDefaultGoogleListener(cucumber_cpp::library::engine::ContextManager& contextManager, report::ReportForwarder& reportHandler, HookExecutor& hookExecution, const Policy& executionPolicy) - : TestExecutionImpl{ contextManager, reportHandler, hookExecution, executionPolicy } - { - auto& listeners = testing::UnitTest::GetInstance()->listeners(); - defaultEventListener = listeners.Release(listeners.default_result_printer()); - } - - TestExecutionImplWithoutDefaultGoogleListener::~TestExecutionImplWithoutDefaultGoogleListener() - { - auto& listeners = testing::UnitTest::GetInstance()->listeners(); - listeners.Append(defaultEventListener); - } -} diff --git a/cucumber_cpp/library/engine/TestExecution.hpp b/cucumber_cpp/library/engine/TestExecution.hpp deleted file mode 100644 index 904d8961..00000000 --- a/cucumber_cpp/library/engine/TestExecution.hpp +++ /dev/null @@ -1,153 +0,0 @@ -#ifndef ENGINE_TESTEXECUTION_HPP -#define ENGINE_TESTEXECUTION_HPP - -#include "cucumber_cpp/library/StepRegistry.hpp" -#include "cucumber_cpp/library/engine/ContextManager.hpp" -#include "cucumber_cpp/library/engine/FailureHandler.hpp" -#include "cucumber_cpp/library/engine/FeatureInfo.hpp" -#include "cucumber_cpp/library/engine/HookExecutor.hpp" -#include "cucumber_cpp/library/engine/Result.hpp" -#include "cucumber_cpp/library/engine/RuleInfo.hpp" -#include "cucumber_cpp/library/engine/ScenarioInfo.hpp" -#include "cucumber_cpp/library/engine/StepInfo.hpp" -#include "cucumber_cpp/library/report/Report.hpp" -#include "cucumber_cpp/library/util/Immoveable.hpp" -#include -#include -#include -#include - -namespace cucumber_cpp::library::engine -{ - struct TestExecution - { - protected: - ~TestExecution() = default; - - public: - struct ProgramScope; - struct FeatureScope; - struct RuleScope; - struct ScenarioScope; - - struct Policy; - - [[nodiscard]] virtual ProgramScope StartRun() = 0; - [[nodiscard]] virtual FeatureScope StartFeature(const cucumber_cpp::library::engine::FeatureInfo& featureInfo) = 0; - [[nodiscard]] virtual RuleScope StartRule(const cucumber_cpp::library::engine::RuleInfo& ruleInfo) = 0; - [[nodiscard]] virtual ScenarioScope StartScenario(const cucumber_cpp::library::engine::ScenarioInfo& scenarioInfo) = 0; - - virtual void RunStep(const cucumber_cpp::library::engine::StepInfo& stepInfo) = 0; - }; - - struct TestExecution::ProgramScope : library::util::Immoveable - { - ProgramScope(cucumber_cpp::library::engine::ContextManager& contextManager, report::ReportForwarder& reportHandler, HookExecutor& hookExecution); - - Result ExecutionStatus() const; - - private: - cucumber_cpp::library::engine::ContextManager& contextManager; - report::ReportForwarder::ProgramScope scopedProgramReport; - HookExecutor::ProgramScope scopedProgramHook; - }; - - struct TestExecution::FeatureScope : library::util::Immoveable - { - FeatureScope(cucumber_cpp::library::engine::ContextManager& contextManager, report::ReportForwarder& reportHandler, HookExecutor& hookExecution, const cucumber_cpp::library::engine::FeatureInfo& featureInfo); - - Result ExecutionStatus() const; - - private: - cucumber_cpp::library::engine::ContextManager::ScopedFeatureContext scopedFeatureContext; - report::ReportForwarder::FeatureScope scopedFeatureReport; - HookExecutor::FeatureScope scopedFeatureHook; - }; - - struct TestExecution::RuleScope : library::util::Immoveable - { - RuleScope(cucumber_cpp::library::engine::ContextManager& contextManager, report::ReportForwarder& reportHandler, const cucumber_cpp::library::engine::RuleInfo& ruleInfo); - - private: - cucumber_cpp::library::engine::ContextManager::ScopedRuleContext scopedRuleContext; - report::ReportForwarder::RuleScope scopedRuleReport; - }; - - struct TestExecution::ScenarioScope : library::util::Immoveable - { - ScenarioScope(cucumber_cpp::library::engine::ContextManager& contextManager, report::ReportForwarder& reportHandler, HookExecutor& hookExecution, const cucumber_cpp::library::engine::ScenarioInfo& scenarioInfo); - - private: - cucumber_cpp::library::engine::ContextManager::ScopedScenarioContext scopedScenarioContext; - report::ReportForwarder::ScenarioScope scopedScenarioReport; - HookExecutor::ScenarioScope scopedScenarioHook; - }; - - struct TestExecution::Policy - { - protected: - ~Policy() = default; - - public: - virtual void RunStep(cucumber_cpp::library::engine::ContextManager& contextManager, const StepMatch& stepMatch) const = 0; - }; - - struct DryRunPolicy : TestExecution::Policy - { - virtual ~DryRunPolicy() = default; - - void RunStep(cucumber_cpp::library::engine::ContextManager& contextManager, const StepMatch& stepMatch) const override; - }; - - struct ExecuteRunPolicy : TestExecution::Policy - { - virtual ~ExecuteRunPolicy() = default; - - void RunStep(cucumber_cpp::library::engine::ContextManager& contextManager, const StepMatch& stepMatch) const override; - }; - - extern const DryRunPolicy dryRunPolicy; - extern const ExecuteRunPolicy executeRunPolicy; - - struct TestExecutionImpl : TestExecution - { - TestExecutionImpl(cucumber_cpp::library::engine::ContextManager& contextManager, report::ReportForwarder& reportHandler, HookExecutor& hookExecution, const Policy& executionPolicy = executeRunPolicy); - - TestExecutionImpl(const TestExecutionImpl&) = delete; - TestExecutionImpl& operator=(const TestExecutionImpl&) = delete; - - virtual ~TestExecutionImpl(); - - ProgramScope StartRun() override; - FeatureScope StartFeature(const cucumber_cpp::library::engine::FeatureInfo& featureInfo) override; - RuleScope StartRule(const cucumber_cpp::library::engine::RuleInfo& ruleInfo) override; - ScenarioScope StartScenario(const cucumber_cpp::library::engine::ScenarioInfo& scenarioInfo) override; - - void RunStep(const cucumber_cpp::library::engine::StepInfo& stepInfo) override; - - private: - void RunStepMatch(std::monostate); - void RunStepMatch(const std::vector&); - void RunStepMatch(const StepMatch& stepMatch); - - cucumber_cpp::library::engine::ContextManager& contextManager; - report::ReportForwarder& reportHandler; - HookExecutor& hookExecution; - - const Policy& executionPolicy; - - TestAssertionHandlerImpl testAssertionHandler{ contextManager, reportHandler }; - GoogleTestEventListener googleTestEventListener{ testAssertionHandler }; - }; - - struct TestExecutionImplWithoutDefaultGoogleListener : TestExecutionImpl - { - TestExecutionImplWithoutDefaultGoogleListener(cucumber_cpp::library::engine::ContextManager& contextManager, report::ReportForwarder& reportHandler, HookExecutor& hookExecution, const Policy& executionPolicy = executeRunPolicy); - ~TestExecutionImplWithoutDefaultGoogleListener() override; - - private: - testing::TestEventListener* defaultEventListener; - }; -} - -#endif diff --git a/cucumber_cpp/library/engine/TestRunner.cpp b/cucumber_cpp/library/engine/TestRunner.cpp deleted file mode 100644 index c56709ca..00000000 --- a/cucumber_cpp/library/engine/TestRunner.cpp +++ /dev/null @@ -1,104 +0,0 @@ - -#include "cucumber_cpp/library/engine/TestRunner.hpp" -#include "cucumber_cpp/library/engine/FeatureFactory.hpp" -#include "cucumber_cpp/library/engine/FeatureInfo.hpp" -#include "cucumber_cpp/library/engine/Result.hpp" -#include "cucumber_cpp/library/engine/RuleInfo.hpp" -#include "cucumber_cpp/library/engine/StepInfo.hpp" -#include "cucumber_cpp/library/engine/StepType.hpp" -#include "cucumber_cpp/library/engine/TestExecution.hpp" -#include -#include -#include -#include - -namespace cucumber_cpp::library::engine -{ - TestRunner* TestRunner::instance = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) - - TestRunner::TestRunner() - { - TestRunner::instance = this; - } - - TestRunner::~TestRunner() - { - TestRunner::instance = nullptr; - } - - TestRunner& TestRunner::Instance() - { - return *instance; - } - - TestRunnerImpl::TestRunnerImpl(FeatureTreeFactory& featureTreeFactory, cucumber_cpp::library::engine::TestExecution& testExecution) - : featureTreeFactory{ featureTreeFactory } - , testExecution{ testExecution } - { - } - - void TestRunnerImpl::Run(const std::vector>& features) - { - auto scope = testExecution.StartRun(); - - if (scope.ExecutionStatus() != Result::passed) - return; - - for (const auto& featurePtr : features) - RunFeature(*featurePtr); - } - - void TestRunnerImpl::NestedStep(StepType type, std::string step) - { - const auto nestedStep = featureTreeFactory.CreateStepInfo(type, std::move(step), *currentScenario, 0, 0, {}, ""); - testExecution.RunStep(*nestedStep); - } - - void TestRunnerImpl::RunFeature(const FeatureInfo& feature) - { - if (feature.Rules().empty() && feature.Scenarios().empty()) - return; - - const auto featureScope = testExecution.StartFeature(feature); - - if (featureScope.ExecutionStatus() != Result::passed) - return; - - RunRules(feature.Rules()); - RunScenarios(feature.Scenarios()); - } - - void TestRunnerImpl::RunRule(const RuleInfo& rule) - { - const auto ruleScope = testExecution.StartRule(rule); - - RunScenarios(rule.Scenarios()); - } - - void TestRunnerImpl::RunRules(const std::vector>& rules) - { - for (const auto& rule : rules) - RunRule(*rule); - } - - void TestRunnerImpl::RunScenario(const ScenarioInfo& scenario) - { - const auto scenarioScope = testExecution.StartScenario(scenario); - - currentScenario = &scenario; - - ExecuteSteps(scenario); - } - - void TestRunnerImpl::RunScenarios(const std::vector>& scenarios) - { - for (const auto& scenario : scenarios) - RunScenario(*scenario); - } - - void TestRunnerImpl::ExecuteSteps(const ScenarioInfo& scenario) - { - for (const auto& step : scenario.Children()) - testExecution.RunStep(*step); - } -} diff --git a/cucumber_cpp/library/engine/TestRunner.hpp b/cucumber_cpp/library/engine/TestRunner.hpp deleted file mode 100644 index bf813f8e..00000000 --- a/cucumber_cpp/library/engine/TestRunner.hpp +++ /dev/null @@ -1,73 +0,0 @@ -#ifndef ENGINE_TESTRUNNER_HPP -#define ENGINE_TESTRUNNER_HPP - -#include "cucumber_cpp/library/engine/FeatureFactory.hpp" -#include "cucumber_cpp/library/engine/FeatureInfo.hpp" -#include "cucumber_cpp/library/engine/RuleInfo.hpp" -#include "cucumber_cpp/library/engine/ScenarioInfo.hpp" -#include "cucumber_cpp/library/engine/StepInfo.hpp" -#include "cucumber_cpp/library/engine/StepType.hpp" -#include "cucumber_cpp/library/engine/TestExecution.hpp" -#include -#include -#include - -namespace cucumber_cpp::library::report -{ - struct ReportHandlerV2; -} - -namespace cucumber_cpp::library::engine -{ - struct TestRunner - { - struct PendingException - {}; - - struct StepNotFoundException - {}; - - struct AmbiguousStepException - {}; - - protected: - TestRunner(); - ~TestRunner(); - - public: - TestRunner(const TestRunner&) = delete; - - static TestRunner& Instance(); - - virtual void Run(const std::vector>& feature) = 0; - virtual void NestedStep(StepType type, std::string step) = 0; - - private: - static TestRunner* instance; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) - }; - - struct TestRunnerImpl : TestRunner - { - TestRunnerImpl(FeatureTreeFactory& featureTreeFactory, TestExecution& testExecution); - virtual ~TestRunnerImpl() = default; - - void Run(const std::vector>& feature) override; - - void NestedStep(StepType type, std::string step) override; - - private: - void ExecuteSteps(const ScenarioInfo& scenario); - void RunScenario(const ScenarioInfo& scenario); - void RunScenarios(const std::vector>& scenarios); - void RunRule(const RuleInfo& rule); - void RunRules(const std::vector>& rules); - void RunFeature(const FeatureInfo& feature); - - FeatureTreeFactory& featureTreeFactory; - TestExecution& testExecution; - - const ScenarioInfo* currentScenario = nullptr; - }; -} - -#endif diff --git a/cucumber_cpp/library/engine/test/CMakeLists.txt b/cucumber_cpp/library/engine/test/CMakeLists.txt index 39b9330e..e224f962 100644 --- a/cucumber_cpp/library/engine/test/CMakeLists.txt +++ b/cucumber_cpp/library/engine/test/CMakeLists.txt @@ -2,19 +2,14 @@ add_executable(cucumber_cpp.library.engine.test) add_test(NAME cucumber_cpp.library.engine.test COMMAND cucumber_cpp.library.engine.test) target_link_libraries(cucumber_cpp.library.engine.test PUBLIC + cucumber_cpp.library cucumber_cpp.library.engine gmock_main - cucumber_cpp.library.engine.test_helper - cucumber_cpp.library.engine.test_helper.steps + # cucumber_cpp.library.engine.test_helper + # cucumber_cpp.library.engine.test_helper.steps ) target_sources(cucumber_cpp.library.engine.test PRIVATE - TestContextManager.cpp - TestFailureHandler.cpp - TestFeatureFactory.cpp - TestHookExecutor.cpp - TestHookExecutorHooks.cpp TestStep.cpp TestStringTo.cpp - TestTestRunner.cpp ) diff --git a/cucumber_cpp/library/engine/test/TestContextManager.cpp b/cucumber_cpp/library/engine/test/TestContextManager.cpp deleted file mode 100644 index b048917a..00000000 --- a/cucumber_cpp/library/engine/test/TestContextManager.cpp +++ /dev/null @@ -1,139 +0,0 @@ - -#include "cucumber_cpp/library/Context.hpp" -#include "cucumber_cpp/library/engine/ContextManager.hpp" -#include "cucumber_cpp/library/engine/FeatureInfo.hpp" -#include "cucumber_cpp/library/engine/Result.hpp" -#include "cucumber_cpp/library/engine/RuleInfo.hpp" -#include "cucumber_cpp/library/engine/ScenarioInfo.hpp" -#include "cucumber_cpp/library/engine/StepInfo.hpp" -#include -#include -#include - -namespace cucumber_cpp::library::engine -{ - struct TestContextManager : testing::Test - { - std::shared_ptr contextStorageFactory = std::make_shared(); - cucumber_cpp::library::engine::ContextManager contextManager{ contextStorageFactory }; - - cucumber_cpp::library::engine::FeatureInfo feature{ {}, {}, {}, {}, {}, {} }; - cucumber_cpp::library::engine::RuleInfo rule{ feature, {}, {}, {}, {}, {} }; - cucumber_cpp::library::engine::ScenarioInfo scenario{ rule, {}, {}, {}, {}, {} }; - cucumber_cpp::library::engine::StepInfo step{ scenario, {}, {}, {}, {}, {}, {} }; - }; - - TEST_F(TestContextManager, Construct) - {} - - TEST_F(TestContextManager, ProgramContext) - { - auto& programContext = contextManager.ProgramContext(); - EXPECT_THAT(programContext.ExecutionStatus(), testing::Eq(cucumber_cpp::library::engine::Result::passed)); - } - - TEST_F(TestContextManager, FeatureContextNotAvailableAfterConstruction) - { - ASSERT_THROW(contextManager.FeatureContext(), cucumber_cpp::library::engine::ContextNotAvailable); - - try - { - contextManager.FeatureContext(); - FAIL() << "Expected to throw ContextNotAvailable"; - } - catch (const cucumber_cpp::library::engine::ContextNotAvailable& e) - { - EXPECT_THAT(e.what(), testing::StrEq(R"(FeatureContext not available)")); - } - } - - TEST_F(TestContextManager, RuleContextNotAvailableAfterConstruction) - { - ASSERT_THROW(contextManager.RuleContext(), cucumber_cpp::library::engine::ContextNotAvailable); - - try - { - contextManager.RuleContext(); - FAIL() << "Expected to throw ContextNotAvailable"; - } - catch (const cucumber_cpp::library::engine::ContextNotAvailable& e) - { - EXPECT_THAT(e.what(), testing::StrEq(R"(RuleContext not available)")); - } - } - - TEST_F(TestContextManager, ScenarioContextNotAvailableAfterConstruction) - { - ASSERT_THROW(contextManager.ScenarioContext(), cucumber_cpp::library::engine::ContextNotAvailable); - - try - { - contextManager.ScenarioContext(); - FAIL() << "Expected to throw ContextNotAvailable"; - } - catch (const cucumber_cpp::library::engine::ContextNotAvailable& e) - { - EXPECT_THAT(e.what(), testing::StrEq(R"(ScenarioContext not available)")); - } - } - - TEST_F(TestContextManager, StepContextNotAvailableAfterConstruction) - { - ASSERT_THROW(contextManager.StepContext(), cucumber_cpp::library::engine::ContextNotAvailable); - - try - { - contextManager.StepContext(); - FAIL() << "Expected to throw ContextNotAvailable"; - } - catch (const cucumber_cpp::library::engine::ContextNotAvailable& e) - { - EXPECT_THAT(e.what(), testing::StrEq(R"(StepContext not available)")); - } - } - - TEST_F(TestContextManager, FeatureContextLifetimeManagement) - { - { - auto scoped = contextManager.CreateFeatureContext(feature); - ASSERT_NO_THROW(contextManager.FeatureContext()); - } - ASSERT_ANY_THROW(contextManager.FeatureContext()); - } - - TEST_F(TestContextManager, RuleContextLifetimeManagement) - { - { - auto scopedFeature = contextManager.CreateFeatureContext(feature); - auto scopedRule = contextManager.CreateRuleContext(rule); - - ASSERT_NO_THROW(contextManager.RuleContext()); - } - ASSERT_ANY_THROW(contextManager.RuleContext()); - } - - TEST_F(TestContextManager, ScenarioContextLifetimeManagement) - { - { - auto scopedFeature = contextManager.CreateFeatureContext(feature); - auto scopedRule = contextManager.CreateRuleContext(rule); - auto scopedScenario = contextManager.CreateScenarioContext(scenario); - - ASSERT_NO_THROW(contextManager.ScenarioContext()); - } - ASSERT_ANY_THROW(contextManager.ScenarioContext()); - } - - TEST_F(TestContextManager, StepContextLifetimeManagement) - { - { - auto scopedFeature = contextManager.CreateFeatureContext(feature); - auto scopedRule = contextManager.CreateRuleContext(rule); - auto scopedScenario = contextManager.CreateScenarioContext(scenario); - auto stepContextScope = contextManager.CreateStepContext(step); - - ASSERT_NO_THROW(contextManager.StepContext()); - } - ASSERT_ANY_THROW(contextManager.StepContext()); - } -} diff --git a/cucumber_cpp/library/engine/test/TestFailureHandler.cpp b/cucumber_cpp/library/engine/test/TestFailureHandler.cpp deleted file mode 100644 index 3efde06d..00000000 --- a/cucumber_cpp/library/engine/test/TestFailureHandler.cpp +++ /dev/null @@ -1,57 +0,0 @@ - -#include "cucumber_cpp/library/engine/FailureHandler.hpp" -#include "cucumber_cpp/library/engine/Result.hpp" -#include "cucumber_cpp/library/engine/test_helper/ContextManagerInstance.hpp" -#include "cucumber_cpp/library/report/test_helper/ReportForwarderMock.hpp" -#include "gtest/gtest.h" -#include -#include - -namespace cucumber_cpp::library::engine -{ - namespace - { - - struct TestFailureHandler : testing::Test - { - - void Error(const char* failure) - { - googleTestEventListener.OnTestPartResult({ testing::TestPartResult::Type::kFatalFailure, "custom_file.cpp", 0, failure }); - } - - test_helper::ContextManagerInstance contextManager; - report::test_helper::ReportForwarderMock reportHandler{ contextManager }; - TestAssertionHandlerImpl testAssertionHandler{ contextManager, reportHandler }; - - private: - GoogleTestEventListener googleTestEventListener{ testAssertionHandler }; - }; - } - - TEST_F(TestFailureHandler, SetContextToFailed) - { - ASSERT_THAT(contextManager.CurrentContext().ExecutionStatus(), testing::Eq(Result::passed)); - ASSERT_THAT(contextManager.ProgramContext().ExecutionStatus(), testing::Eq(Result::passed)); - ASSERT_THAT(contextManager.FeatureContext().ExecutionStatus(), testing::Eq(Result::passed)); - ASSERT_THAT(contextManager.RuleContext().ExecutionStatus(), testing::Eq(Result::passed)); - ASSERT_THAT(contextManager.ScenarioContext().ExecutionStatus(), testing::Eq(Result::passed)); - ASSERT_THAT(contextManager.StepContext().ExecutionStatus(), testing::Eq(Result::passed)); - - Error("failure"); - - EXPECT_THAT(contextManager.CurrentContext().ExecutionStatus(), testing::Eq(Result::failed)); - EXPECT_THAT(contextManager.ProgramContext().ExecutionStatus(), testing::Eq(Result::failed)); - EXPECT_THAT(contextManager.FeatureContext().ExecutionStatus(), testing::Eq(Result::failed)); - EXPECT_THAT(contextManager.RuleContext().ExecutionStatus(), testing::Eq(Result::failed)); - EXPECT_THAT(contextManager.ScenarioContext().ExecutionStatus(), testing::Eq(Result::failed)); - EXPECT_THAT(contextManager.StepContext().ExecutionStatus(), testing::Eq(Result::failed)); - } - - TEST_F(TestFailureHandler, ReportFailureMessage) - { - EXPECT_CALL(reportHandler, Failure("Failure Message", testing::_, testing::_, testing::_)); - - Error("Failure Message"); - } -} diff --git a/cucumber_cpp/library/engine/test/TestFeatureFactory.cpp b/cucumber_cpp/library/engine/test/TestFeatureFactory.cpp deleted file mode 100644 index 65f4d16d..00000000 --- a/cucumber_cpp/library/engine/test/TestFeatureFactory.cpp +++ /dev/null @@ -1,312 +0,0 @@ -#include "cucumber_cpp/library/StepRegistry.hpp" -#include "cucumber_cpp/library/cucumber_expression/ParameterRegistry.hpp" -#include "cucumber_cpp/library/engine/FeatureFactory.hpp" -#include "cucumber_cpp/library/engine/StepType.hpp" -#include "cucumber_cpp/library/engine/test_helper/TemporaryFile.hpp" -#include -#include -#include -#include -#include -#include - -namespace cucumber_cpp::library::engine -{ - - struct TestFeatureFactory : testing::Test - { - cucumber_expression::ParameterRegistry parameterRegistry; - StepRegistry stepRegistry{ parameterRegistry }; - FeatureTreeFactory featureTreeFactory{ stepRegistry }; - }; - - TEST_F(TestFeatureFactory, CreateEmptyFeature) - { - auto tmp = test_helper::TemporaryFile{ "tmpfile.feature" }; - - tmp << "Feature: Test feature\n" - "Custom\n" - "Description\n"; - - auto feature = featureTreeFactory.Create(tmp.Path(), ""); - EXPECT_THAT(feature->Title(), testing::StrEq("Test feature")); - EXPECT_THAT(feature->Description(), testing::StrEq("Custom\nDescription")); - EXPECT_THAT(feature->Tags(), testing::Eq(std::set>{})); - EXPECT_THAT(feature->Path(), testing::Eq(tmp.Path())); - EXPECT_THAT(feature->Line(), testing::Eq(1)); - EXPECT_THAT(feature->Column(), testing::Eq(1)); - EXPECT_THAT(feature->Rules().size(), testing::Eq(0)); - EXPECT_THAT(feature->Scenarios().size(), testing::Eq(0)); - } - - TEST_F(TestFeatureFactory, CreateScenario) - { - auto tmp = test_helper::TemporaryFile{ "tmpfile.feature" }; - - tmp << "Feature: Test feature\n" - "Custom\n" - "Description\n" - " Scenario: Test scenario\n" - " Custom Scenario\n" - " Description\n"; - - const auto feature = featureTreeFactory.Create(tmp.Path(), ""); - EXPECT_THAT(feature->Rules().size(), testing::Eq(0)); - EXPECT_THAT(feature->Scenarios().size(), testing::Eq(1)); - - const auto& scenario = feature->Scenarios().front(); - EXPECT_THAT(scenario->Title(), testing::StrEq("Test scenario")); - EXPECT_THAT(scenario->Description(), testing::StrEq(" Custom Scenario\n Description")); - EXPECT_THAT(scenario->Tags(), testing::Eq(std::set>{})); - EXPECT_THAT(scenario->Path(), testing::Eq(tmp.Path())); - EXPECT_THAT(scenario->Line(), testing::Eq(4)); - EXPECT_THAT(scenario->Column(), testing::Eq(3)); - EXPECT_THAT(scenario->Children().size(), testing::Eq(0)); - } - - TEST_F(TestFeatureFactory, CreateRules) - { - auto tmp = test_helper::TemporaryFile{ "tmpfile.feature" }; - - tmp << "Feature: Test feature\n" - "Custom\n" - "Description\n" - " Rule: Test rule\n" - " Custom Rule\n" - " Description1\n" - " Scenario: Test scenario1\n" - " Custom Scenario\n" - " Description\n" - " Rule: Test rule\n" - " Custom Rule\n" - " Description2\n" - " Scenario: Test scenario2\n" - " Custom Scenario\n" - " Description\n"; - - const auto feature = featureTreeFactory.Create(tmp.Path(), ""); - EXPECT_THAT(feature->Rules().size(), testing::Eq(2)); - EXPECT_THAT(feature->Scenarios().size(), testing::Eq(0)); - - const auto& rule1 = feature->Rules().front(); - EXPECT_THAT(rule1->Title(), testing::StrEq("Test rule")); - EXPECT_THAT(rule1->Description(), testing::StrEq(" Custom Rule\n Description1")); - EXPECT_THAT(rule1->Line(), testing::Eq(4)); - EXPECT_THAT(rule1->Column(), testing::Eq(3)); - EXPECT_THAT(rule1->Scenarios().size(), testing::Eq(1)); - - const auto& rule1scenario1 = rule1->Scenarios().front(); - EXPECT_THAT(rule1scenario1->Title(), testing::StrEq("Test scenario1")); - EXPECT_THAT(rule1scenario1->Line(), testing::Eq(7)); - - const auto& rule2 = feature->Rules().back(); - EXPECT_THAT(rule2->Title(), testing::StrEq("Test rule")); - EXPECT_THAT(rule2->Description(), testing::StrEq(" Custom Rule\n Description2")); - EXPECT_THAT(rule2->Line(), testing::Eq(10)); - EXPECT_THAT(rule2->Column(), testing::Eq(3)); - EXPECT_THAT(rule2->Scenarios().size(), testing::Eq(1)); - - const auto& rule2scenario1 = rule2->Scenarios().front(); - EXPECT_THAT(rule2scenario1->Title(), testing::StrEq("Test scenario2")); - EXPECT_THAT(rule2scenario1->Line(), testing::Eq(13)); - } - - TEST_F(TestFeatureFactory, CreateSteps) - { - auto tmp = test_helper::TemporaryFile{ "tmpfile.feature" }; - - tmp << "Feature: Test feature\n" - " Scenario: Test scenario1\n" - " Given I have a step1\n" - " When I do something1\n" - " Then I expect something1\n" - " Rule: Test rule\n" - " Scenario: Test scenario2\n" - " Given I have a step2\n" - " When I do something2\n" - " Then I expect something2\n" - " Rule: Test rule\n" - " Scenario: Test scenario3\n" - " Given I have a step3\n" - " When I do something3\n" - " Then I expect something3\n"; - - const auto feature = featureTreeFactory.Create(tmp.Path(), ""); - EXPECT_THAT(feature->Rules().size(), testing::Eq(2)); - EXPECT_THAT(feature->Scenarios().size(), testing::Eq(1)); - - const auto& scenario1 = feature->Scenarios()[0]; - EXPECT_THAT(scenario1->Title(), testing::StrEq("Test scenario1")); - EXPECT_THAT(scenario1->Children().size(), testing::Eq(3)); - EXPECT_THAT(scenario1->Children()[0]->Type(), testing::Eq(StepType::given)); - EXPECT_THAT(scenario1->Children()[1]->Type(), testing::Eq(StepType::when)); - EXPECT_THAT(scenario1->Children()[2]->Type(), testing::Eq(StepType::then)); - EXPECT_THAT(scenario1->Children()[0]->Text(), testing::StrEq("I have a step1")); - EXPECT_THAT(scenario1->Children()[1]->Text(), testing::StrEq("I do something1")); - EXPECT_THAT(scenario1->Children()[2]->Text(), testing::StrEq("I expect something1")); - - const auto& scenario2 = feature->Rules()[0]->Scenarios()[0]; - EXPECT_THAT(scenario2->Title(), testing::StrEq("Test scenario2")); - EXPECT_THAT(scenario2->Children().size(), testing::Eq(3)); - EXPECT_THAT(scenario2->Children()[0]->Type(), testing::Eq(StepType::given)); - EXPECT_THAT(scenario2->Children()[1]->Type(), testing::Eq(StepType::when)); - EXPECT_THAT(scenario2->Children()[2]->Type(), testing::Eq(StepType::then)); - EXPECT_THAT(scenario2->Children()[0]->Text(), testing::StrEq("I have a step2")); - EXPECT_THAT(scenario2->Children()[1]->Text(), testing::StrEq("I do something2")); - EXPECT_THAT(scenario2->Children()[2]->Text(), testing::StrEq("I expect something2")); - - const auto& scenario3 = feature->Rules()[1]->Scenarios()[0]; - EXPECT_THAT(scenario3->Title(), testing::StrEq("Test scenario3")); - EXPECT_THAT(scenario3->Children().size(), testing::Eq(3)); - EXPECT_THAT(scenario3->Children()[0]->Type(), testing::Eq(StepType::given)); - EXPECT_THAT(scenario3->Children()[1]->Type(), testing::Eq(StepType::when)); - EXPECT_THAT(scenario3->Children()[2]->Type(), testing::Eq(StepType::then)); - EXPECT_THAT(scenario3->Children()[0]->Text(), testing::StrEq("I have a step3")); - EXPECT_THAT(scenario3->Children()[1]->Text(), testing::StrEq("I do something3")); - EXPECT_THAT(scenario3->Children()[2]->Text(), testing::StrEq("I expect something3")); - } - - TEST_F(TestFeatureFactory, CreateBackground) - { - auto tmp = test_helper::TemporaryFile{ "tmpfile.feature" }; - - tmp << "Feature: Test feature\n" - " Background: Test background\n" - " Given a background step1\n" - " Scenario: Test scenario1\n" - " Given I have a step1\n" - " Rule: Test rule\n" - " Background: Test background\n" - " Given a background step2\n" - " Scenario: Test scenario2\n" - " Given I have a step2\n" - " Rule: Test rule\n" - " Scenario: Test scenario3\n" - " Given I have a step3\n"; - - const auto feature = featureTreeFactory.Create(tmp.Path(), ""); - - const auto& scenario1 = feature->Scenarios()[0]; - EXPECT_THAT(scenario1->Title(), testing::StrEq("Test scenario1")); - EXPECT_THAT(scenario1->Children().size(), testing::Eq(2)); - EXPECT_THAT(scenario1->Children()[0]->Type(), testing::Eq(StepType::given)); - EXPECT_THAT(scenario1->Children()[1]->Type(), testing::Eq(StepType::given)); - EXPECT_THAT(scenario1->Children()[0]->Text(), testing::StrEq("a background step1")); - EXPECT_THAT(scenario1->Children()[1]->Text(), testing::StrEq("I have a step1")); - - const auto& scenario2 = feature->Rules()[0]->Scenarios()[0]; - EXPECT_THAT(scenario2->Title(), testing::StrEq("Test scenario2")); - EXPECT_THAT(scenario2->Children().size(), testing::Eq(3)); - EXPECT_THAT(scenario2->Children()[0]->Type(), testing::Eq(StepType::given)); - EXPECT_THAT(scenario2->Children()[1]->Type(), testing::Eq(StepType::given)); - EXPECT_THAT(scenario2->Children()[0]->Text(), testing::StrEq("a background step1")); - EXPECT_THAT(scenario2->Children()[1]->Text(), testing::StrEq("a background step2")); - EXPECT_THAT(scenario2->Children()[2]->Text(), testing::StrEq("I have a step2")); - - const auto& scenario3 = feature->Rules()[1]->Scenarios()[0]; - EXPECT_THAT(scenario3->Title(), testing::StrEq("Test scenario3")); - EXPECT_THAT(scenario3->Children().size(), testing::Eq(2)); - EXPECT_THAT(scenario3->Children()[0]->Type(), testing::Eq(StepType::given)); - EXPECT_THAT(scenario3->Children()[1]->Type(), testing::Eq(StepType::given)); - EXPECT_THAT(scenario3->Children()[0]->Text(), testing::StrEq("a background step1")); - EXPECT_THAT(scenario3->Children()[1]->Text(), testing::StrEq("I have a step3")); - } - - TEST_F(TestFeatureFactory, CreateTagsTags) - { - auto tmp = test_helper::TemporaryFile{ "tmpfile.feature" }; - - tmp << "@feature\n" - "Feature: Test feature\n" - " @scenario1\n" - " Scenario: Test scenario1\n" - " Given I have a step1\n" - " @rule\n" - " Rule: Test rule\n" - " @scenario2\n" - " Scenario: Test scenario2\n" - " Given I have a step2\n"; - - const auto feature = featureTreeFactory.Create(tmp.Path(), ""); - - const auto& scenario1 = feature->Scenarios()[0]; - EXPECT_THAT(scenario1->Tags(), testing::Eq(std::set>{ "@feature", "@scenario1" })); - - const auto& scenario2 = feature->Rules()[0]->Scenarios()[0]; - EXPECT_THAT(scenario2->Tags(), testing::Eq(std::set>{ "@feature", "@rule", "@scenario2" })); - } - - TEST_F(TestFeatureFactory, SelectTags) - { - auto tmp = test_helper::TemporaryFile{ "tmpfile.feature" }; - - tmp << "@feature\n" - "Feature: Test feature\n" - " Rule: Test rule1\n" - " @scenario1 @debug\n" - " Scenario: Test scenario1\n" - " Given I have a step1\n" - " @rule\n" - " Rule: Test rule2\n" - " @scenario2 @debug\n" - " Scenario: Test scenario2\n" - " Given I have a step2\n"; - - const auto feature = featureTreeFactory.Create(tmp.Path(), "@debug & @rule"); - EXPECT_THAT(feature->Scenarios().size(), testing::Eq(0)); - EXPECT_THAT(feature->Rules().size(), testing::Eq(1)); - - const auto& scenario1 = feature->Rules()[0]->Scenarios()[0]; - EXPECT_THAT(scenario1->Title(), testing::StrEq("Test scenario2")); - } - - TEST_F(TestFeatureFactory, CreateMultipleScenariosInOneRule) - { - auto tmp = test_helper::TemporaryFile{ "tmpfile.feature" }; - - tmp << "Feature: Test feature\n" - " Rule: Test rule\n" - " Scenario: Test scenario1\n" - " Given I have a step1\n" - " Scenario: Test scenario2\n" - " Given I have a step2\n"; - - const auto feature = featureTreeFactory.Create(tmp.Path(), ""); - EXPECT_THAT(feature->Rules().size(), testing::Eq(1)); - - const auto& rule = feature->Rules()[0]; - EXPECT_THAT(rule->Scenarios().size(), testing::Eq(2)); - - const auto& scenario1 = rule->Scenarios()[0]; - EXPECT_THAT(scenario1->Title(), testing::StrEq("Test scenario1")); - EXPECT_THAT(scenario1->Children().size(), testing::Eq(1)); - EXPECT_THAT(scenario1->Children()[0]->Type(), testing::Eq(StepType::given)); - EXPECT_THAT(scenario1->Children()[0]->Text(), testing::StrEq("I have a step1")); - - const auto& scenario2 = rule->Scenarios()[1]; - EXPECT_THAT(scenario2->Title(), testing::StrEq("Test scenario2")); - EXPECT_THAT(scenario2->Children().size(), testing::Eq(1)); - EXPECT_THAT(scenario2->Children()[0]->Type(), testing::Eq(StepType::given)); - EXPECT_THAT(scenario2->Children()[0]->Text(), testing::StrEq("I have a step2")); - } - - TEST_F(TestFeatureFactory, CreateTable) - { - auto tmp = test_helper::TemporaryFile{ "tmpfile.feature" }; - - tmp << "Feature: Test feature\n" - " Scenario: Test scenario1\n" - " Given I have a step1\n" - " | a | b |\n" - " | c | d |\n"; - - const auto feature = featureTreeFactory.Create(tmp.Path(), ""); - - const auto& scenario1 = feature->Scenarios()[0]; - EXPECT_THAT(scenario1->Children().size(), testing::Eq(1)); - EXPECT_THAT(scenario1->Children()[0]->Table()[0][0].As(), testing::StrEq("a")); - EXPECT_THAT(scenario1->Children()[0]->Table()[0][1].As(), testing::StrEq("b")); - EXPECT_THAT(scenario1->Children()[0]->Table()[1][0].As(), testing::StrEq("c")); - EXPECT_THAT(scenario1->Children()[0]->Table()[1][1].As(), testing::StrEq("d")); - } -} diff --git a/cucumber_cpp/library/engine/test/TestHookExecutor.cpp b/cucumber_cpp/library/engine/test/TestHookExecutor.cpp deleted file mode 100644 index db1f2314..00000000 --- a/cucumber_cpp/library/engine/test/TestHookExecutor.cpp +++ /dev/null @@ -1,98 +0,0 @@ -#include "cucumber_cpp/library/engine/FailureHandler.hpp" -#include "cucumber_cpp/library/engine/HookExecutor.hpp" -#include "cucumber_cpp/library/engine/Result.hpp" -#include "cucumber_cpp/library/engine/test_helper/ContextManagerInstance.hpp" -#include "cucumber_cpp/library/engine/test_helper/FailureHandlerFixture.hpp" -#include "cucumber_cpp/library/report/test_helper/ReportForwarderMock.hpp" -#include -#include -#include -#include -#include -#include -#include -#include - -namespace cucumber_cpp::library::engine -{ - namespace - { - std::function expectFatalStatement; - } - - struct TestHookExecutor : testing::Test - { - std::optional contextManagerInstance{ std::in_place }; - std::optional hookExecutor{ *contextManagerInstance }; - - test_helper::FailureHandlerFixture failureHandlerFixture{ *contextManagerInstance }; - }; - - TEST_F(TestHookExecutor, Construct) - { - } - - TEST_F(TestHookExecutor, ExecuteProgramHooks) - { - ASSERT_FALSE(contextManagerInstance->ProgramContext().Contains("hookBeforeAll")); - ASSERT_FALSE(contextManagerInstance->ProgramContext().Contains("hookAfterAll")); - - { - auto scope = hookExecutor->BeforeAll(); - EXPECT_TRUE(contextManagerInstance->ProgramContext().Contains("hookBeforeAll")); - } - EXPECT_TRUE(contextManagerInstance->ProgramContext().Contains("hookAfterAll")); - } - - TEST_F(TestHookExecutor, ExecuteBeforeFeature) - { - ASSERT_FALSE(contextManagerInstance->FeatureContext().Contains("hookBeforeFeature")); - ASSERT_FALSE(contextManagerInstance->FeatureContext().Contains("hookAfterFeature")); - - { - auto scope = hookExecutor->FeatureStart(); - EXPECT_TRUE(contextManagerInstance->FeatureContext().Contains("hookBeforeFeature")); - } - EXPECT_TRUE(contextManagerInstance->FeatureContext().Contains("hookAfterFeature")); - } - - TEST_F(TestHookExecutor, ExecuteBeforeScenario) - { - ASSERT_FALSE(contextManagerInstance->ScenarioContext().Contains("hookBeforeScenario")); - ASSERT_FALSE(contextManagerInstance->ScenarioContext().Contains("hookAfterScenario")); - - { - auto scope = hookExecutor->ScenarioStart(); - EXPECT_TRUE(contextManagerInstance->ScenarioContext().Contains("hookBeforeScenario")); - } - EXPECT_TRUE(contextManagerInstance->ScenarioContext().Contains("hookAfterScenario")); - } - - TEST_F(TestHookExecutor, ExecuteBeforeStep) - { - ASSERT_FALSE(contextManagerInstance->ScenarioContext().Contains("hookBeforeStep")); - ASSERT_FALSE(contextManagerInstance->ScenarioContext().Contains("hookAfterStep")); - - { - auto scope = hookExecutor->StepStart(); - EXPECT_TRUE(contextManagerInstance->ScenarioContext().Contains("hookBeforeStep")); - } - EXPECT_TRUE(contextManagerInstance->ScenarioContext().Contains("hookAfterStep")); - } - - TEST_F(TestHookExecutor, BeforeHookError) - { - contextManagerInstance.emplace(std::set>{ "@errorbefore" }); - hookExecutor.emplace(*contextManagerInstance); - - report::test_helper::ReportForwarderMock reportHandler{ *contextManagerInstance }; - TestAssertionHandlerImpl assertionHandler{ *contextManagerInstance, reportHandler }; - - expectFatalStatement = [this] - { - auto hook = hookExecutor->StepStart(); - }; - - EXPECT_FATAL_FAILURE(expectFatalStatement(), "Expected: false"); - } -} diff --git a/cucumber_cpp/library/engine/test/TestHookExecutorHooks.cpp b/cucumber_cpp/library/engine/test/TestHookExecutorHooks.cpp deleted file mode 100644 index 15888377..00000000 --- a/cucumber_cpp/library/engine/test/TestHookExecutorHooks.cpp +++ /dev/null @@ -1,51 +0,0 @@ -#include "cucumber_cpp/library/Hooks.hpp" -#include -#include - -namespace cucumber_cpp::library::engine -{ - HOOK_BEFORE_ALL() - { - context.InsertAt("hookBeforeAll", std::string{ "hookBeforeAll" }); - } - - HOOK_AFTER_ALL() - { - context.InsertAt("hookAfterAll", std::string{ "hookAfterAll" }); - } - - HOOK_BEFORE_FEATURE() - { - context.InsertAt("hookBeforeFeature", std::string{ "hookBeforeFeature" }); - } - - HOOK_AFTER_FEATURE() - { - context.InsertAt("hookAfterFeature", std::string{ "hookAfterFeature" }); - } - - HOOK_BEFORE_SCENARIO() - { - context.InsertAt("hookBeforeScenario", std::string{ "hookBeforeScenario" }); - } - - HOOK_AFTER_SCENARIO() - { - context.InsertAt("hookAfterScenario", std::string{ "hookAfterScenario" }); - } - - HOOK_BEFORE_STEP() - { - context.InsertAt("hookBeforeStep", std::string{ "hookBeforeStep" }); - } - - HOOK_AFTER_STEP() - { - context.InsertAt("hookAfterStep", std::string{ "hookAfterStep" }); - } - - HOOK_BEFORE_STEP("@errorbefore") - { - ASSERT_FALSE(true); - } -} diff --git a/cucumber_cpp/library/engine/test/TestStep.cpp b/cucumber_cpp/library/engine/test/TestStep.cpp index a8f7e141..646eea6f 100644 --- a/cucumber_cpp/library/engine/test/TestStep.cpp +++ b/cucumber_cpp/library/engine/test/TestStep.cpp @@ -1,77 +1,108 @@ +#include "cucumber/gherkin/id_generator.hpp" +#include "cucumber/messages/pickle_step_argument.hpp" +#include "cucumber/messages/test_step_started.hpp" #include "cucumber_cpp/library/Context.hpp" -#include "cucumber_cpp/library/engine/ContextManager.hpp" +#include "cucumber_cpp/library/cucumber_expression/ParameterRegistry.hpp" +#include "cucumber_cpp/library/engine/ExecutionContext.hpp" #include "cucumber_cpp/library/engine/Step.hpp" -#include "cucumber_cpp/library/engine/Table.hpp" -#include "cucumber_cpp/library/engine/test_helper/ContextManagerInstance.hpp" -#include "cucumber_cpp/library/engine/test_helper/TestRunnerMock.hpp" +#include "cucumber_cpp/library/runtime/NestedTestCaseRunner.hpp" +#include "cucumber_cpp/library/support/HookRegistry.hpp" +#include "cucumber_cpp/library/support/StepRegistry.hpp" +#include "cucumber_cpp/library/support/SupportCodeLibrary.hpp" +#include "cucumber_cpp/library/support/UndefinedParameters.hpp" +#include "cucumber_cpp/library/util/Broadcaster.hpp" #include "gmock/gmock.h" -#include +#include "gtest/gtest.h" +#include #include -#include namespace cucumber_cpp::library::engine { - struct StepMock : Step + struct StepMock : StepBase { - using Step::Step; + using StepBase::StepBase; MOCK_METHOD(void, SetUp, (), (override)); MOCK_METHOD(void, TearDown, (), (override)); - using Step::context; - using Step::Given; - using Step::Pending; - using Step::table; - using Step::Then; - using Step::When; + using StepBase::Pending; + using StepBase::Skipped; + + using StepBase::Step; + + using StepBase::context; + using StepBase::dataTable; + using StepBase::docString; }; struct TestStep : testing::Test { - Table table{ - std::vector{ TableValue{ "header1" }, TableValue{ "header2}" } }, - std::vector{ TableValue{ "value1" }, TableValue{ "value2}" } } + util::Broadcaster broadcaster; + std::shared_ptr contextStorageFactory{ std::make_shared() }; + Context context{ contextStorageFactory }; + engine::StepOrHookStarted stepOrHookStarted; + cucumber::messages::pickle_step_argument pickleStepArgument; + + cucumber_expression::ParameterRegistry parameterRegistry{ cucumber_cpp::library::support::DefinitionRegistration::Instance().GetRegisteredParameters() }; + cucumber::gherkin::id_generator_ptr idGenerator = std::make_shared(); + support::UndefinedParameters undefinedParameters; + support::StepRegistry stepRegistry{ parameterRegistry, undefinedParameters, idGenerator }; + support::HookRegistry hookRegistry{ idGenerator }; + + support::SupportCodeLibrary supportCodeLibrary{ + hookRegistry, + stepRegistry, + parameterRegistry, + undefinedParameters + }; + runtime::NestedTestCaseRunner nestedTestCaseRunner{ + 0, + supportCodeLibrary, + broadcaster, + context, + std::get(stepOrHookStarted), }; - const std::string docString = "multiline \n string"; - - library::engine::test_helper::ContextManagerInstance contextManager; - - engine::test_helper::TestRunnerMock testRunnerMock; - StepMock step{ contextManager.StepContext(), table, docString }; + StepMock step{ + nestedTestCaseRunner, + broadcaster, + context, + stepOrHookStarted, + pickleStepArgument.data_table, + pickleStepArgument.doc_string, + }; }; TEST_F(TestStep, StepProvidesAccessToSetUpFunction) { EXPECT_CALL(step, SetUp()); - static_cast(step).SetUp(); + static_cast(step).SetUp(); } TEST_F(TestStep, StepProvidesAccessToTearDownFunction) { EXPECT_CALL(step, TearDown()); - static_cast(step).TearDown(); + static_cast(step).TearDown(); } TEST_F(TestStep, ProvidesAccessToCurrentContext) { - ASSERT_THAT(contextManager.StepContext().Contains("top level value"), testing::Eq(false)); + ASSERT_THAT(context.Contains("top level value"), testing::Eq(false)); step.context.InsertAt("top level value", "value"); - ASSERT_THAT(contextManager.StepContext().Contains("top level value"), testing::Eq(true)); + ASSERT_THAT(context.Contains("top level value"), testing::Eq(true)); } TEST_F(TestStep, ProvidesAccessToTable) { - ASSERT_THAT(step.table[0][0].As(), testing::Eq(table[0][0].As())); - ASSERT_THAT(step.table[1][1].As(), testing::Eq(table[1][1].As())); + ASSERT_THAT(step.dataTable.has_value(), testing::Eq(false)); } TEST_F(TestStep, ThrowsStepPendingExceptionOnPending) { - ASSERT_THROW(step.Pending("message"), Step::StepPending); + ASSERT_THROW(step.Pending("message"), StepPending); } } diff --git a/cucumber_cpp/library/engine/test/TestTestRunner.cpp b/cucumber_cpp/library/engine/test/TestTestRunner.cpp deleted file mode 100644 index e733f269..00000000 --- a/cucumber_cpp/library/engine/test/TestTestRunner.cpp +++ /dev/null @@ -1,223 +0,0 @@ - -#include "cucumber_cpp/library/Context.hpp" -#include "cucumber_cpp/library/StepRegistry.hpp" -#include "cucumber_cpp/library/cucumber_expression/ParameterRegistry.hpp" -#include "cucumber_cpp/library/engine/ContextManager.hpp" -#include "cucumber_cpp/library/engine/FeatureFactory.hpp" -#include "cucumber_cpp/library/engine/FeatureInfo.hpp" -#include "cucumber_cpp/library/engine/HookExecutor.hpp" -#include "cucumber_cpp/library/engine/Result.hpp" -#include "cucumber_cpp/library/engine/RuleInfo.hpp" -#include "cucumber_cpp/library/engine/ScenarioInfo.hpp" -#include "cucumber_cpp/library/engine/StepInfo.hpp" -#include "cucumber_cpp/library/engine/TestExecution.hpp" -#include "cucumber_cpp/library/engine/TestRunner.hpp" -#include "cucumber_cpp/library/engine/test_helper/TemporaryFile.hpp" -#include "cucumber_cpp/library/report/Report.hpp" -#include "cucumber_cpp/library/report/StdOutReport.hpp" -#include "gmock/gmock.h" -#include -#include -#include -#include - -namespace cucumber_cpp::library::engine -{ - struct TestExecutionMockInstance : TestExecution - { - TestExecutionMockInstance() - { - reporters.Add("console", std::make_unique()); - reporters.Use("console"); - } - - virtual ~TestExecutionMockInstance() = default; - - MOCK_METHOD(void, StartRunMock, ()); - MOCK_METHOD(void, StartFeatureMock, (const FeatureInfo& featureInfo)); - MOCK_METHOD(void, StartRuleMock, (const RuleInfo& ruleInfo)); - MOCK_METHOD(void, StartScenarioMock, (const ScenarioInfo& scenarioInfo)); - - MOCK_METHOD(void, RunStepMock, (const StepInfo& stepInfo)); - - [[nodiscard]] ProgramScope StartRun() override - { - StartRunMock(); - return testExecutionImpl.StartRun(); - } - - [[nodiscard]] FeatureScope StartFeature(const cucumber_cpp::library::engine::FeatureInfo& featureInfo) override - { - StartFeatureMock(featureInfo); - return testExecutionImpl.StartFeature(featureInfo); - } - - [[nodiscard]] RuleScope StartRule(const cucumber_cpp::library::engine::RuleInfo& ruleInfo) override - { - StartRuleMock(ruleInfo); - return testExecutionImpl.StartRule(ruleInfo); - } - - [[nodiscard]] ScenarioScope StartScenario(const cucumber_cpp::library::engine::ScenarioInfo& scenarioInfo) override - { - StartScenarioMock(scenarioInfo); - return testExecutionImpl.StartScenario(scenarioInfo); - } - - void RunStep(const cucumber_cpp::library::engine::StepInfo& stepInfo) override - { - RunStepMock(stepInfo); - testExecutionImpl.RunStep(stepInfo); - } - - ContextManager contextManager{ std::make_shared() }; - - HookExecutorImpl hookExecutor{ contextManager }; - report::ReportForwarderImpl reporters{ contextManager }; - - TestExecutionImpl testExecutionImpl{ contextManager, reporters, hookExecutor }; - }; - - struct TestTestRunner : testing::Test - { - cucumber_expression::ParameterRegistry parameterRegistry; - StepRegistry stepRegistry{ parameterRegistry }; - FeatureTreeFactory featureTreeFactory{ stepRegistry }; - - testing::StrictMock testExecutionMock; - TestRunnerImpl runner{ featureTreeFactory, testExecutionMock }; - - std::vector> features; - - ContextManager& contextManager = testExecutionMock.contextManager; - }; - - TEST_F(TestTestRunner, StartProgramContext) - { - EXPECT_CALL(testExecutionMock, StartRunMock()); - - runner.Run({}); - } - - TEST_F(TestTestRunner, StartFeatureAndScenarioContext) - { - EXPECT_CALL(testExecutionMock, StartRunMock); - EXPECT_CALL(testExecutionMock, StartFeatureMock); - EXPECT_CALL(testExecutionMock, StartScenarioMock); - - auto tmp = test_helper::TemporaryFile{ "tmpfile.feature" }; - tmp << "Feature: Test feature\n" - " Scenario: Test scenario\n"; - features.push_back(featureTreeFactory.Create(tmp.Path(), "")); - - runner.Run(features); - } - - TEST_F(TestTestRunner, DontRunEmptyFeature) - { - EXPECT_CALL(testExecutionMock, StartRunMock); - EXPECT_CALL(testExecutionMock, StartFeatureMock).Times(0); - - auto tmp = test_helper::TemporaryFile{ "tmpfile.feature" }; - tmp << "Feature: Test feature\n"; - features.push_back(featureTreeFactory.Create(tmp.Path(), "")); - - runner.Run(features); - } - - TEST_F(TestTestRunner, StartFeatureContextForEveryFeature) - { - EXPECT_CALL(testExecutionMock, StartRunMock); - EXPECT_CALL(testExecutionMock, StartFeatureMock); - EXPECT_CALL(testExecutionMock, StartScenarioMock).Times(2); - - auto tmp = test_helper::TemporaryFile{ "tmpfile.feature" }; - tmp << "Feature: Test feature\n" - " Scenario: Test scenario\n" - " Scenario: Test scenario\n"; - - features.push_back(featureTreeFactory.Create(tmp.Path(), "")); - - runner.Run(features); - } - - TEST_F(TestTestRunner, StartScenarioContextForEveryScenario) - { - EXPECT_CALL(testExecutionMock, StartRunMock); - EXPECT_CALL(testExecutionMock, StartFeatureMock).Times(2); - EXPECT_CALL(testExecutionMock, StartScenarioMock).Times(2); - - auto tmp = test_helper::TemporaryFile{ "tmpfile.feature" }; - tmp << "Feature: Test feature\n" - " Scenario: Test scenario\n"; - - features.push_back(featureTreeFactory.Create(tmp.Path(), "")); - features.push_back(featureTreeFactory.Create(tmp.Path(), "")); - - runner.Run(features); - } - - TEST_F(TestTestRunner, StartRuleAndScenarioForEveryRuleAndScenario) - { - EXPECT_CALL(testExecutionMock, StartRunMock); - EXPECT_CALL(testExecutionMock, StartFeatureMock).Times(2); - EXPECT_CALL(testExecutionMock, StartRuleMock).Times(4); - EXPECT_CALL(testExecutionMock, StartScenarioMock).Times(4); - - auto tmp = test_helper::TemporaryFile{ "tmpfile.feature" }; - tmp << "Feature: Test feature\n" - " Rule: Test rule\n" - " Scenario: Test scenario\n" - " Rule: Test rule\n" - " Scenario: Test scenario\n"; - - features.push_back(featureTreeFactory.Create(tmp.Path(), "")); - features.push_back(featureTreeFactory.Create(tmp.Path(), "")); - - runner.Run(features); - } - - TEST_F(TestTestRunner, RunEveryStep) - { - EXPECT_CALL(testExecutionMock, StartRunMock); - EXPECT_CALL(testExecutionMock, StartFeatureMock); - EXPECT_CALL(testExecutionMock, StartRuleMock).Times(1); - EXPECT_CALL(testExecutionMock, StartScenarioMock).Times(2); - EXPECT_CALL(testExecutionMock, RunStepMock).Times(4); - - auto tmp = test_helper::TemporaryFile{ "tmpfile.feature" }; - tmp << "Feature: Test feature\n" - " Scenario: Test scenario\n" - " Given I have a step\n" - " Given I have a step\n" - " Rule: Test rule\n" - " Scenario: Test scenario\n" - " Given I have a step\n" - " Given I have a step\n"; - - features.push_back(featureTreeFactory.Create(tmp.Path(), "")); - runner.Run(features); - } - - TEST_F(TestTestRunner, RunNestedSteps) - { - EXPECT_CALL(testExecutionMock, StartRunMock); - EXPECT_CALL(testExecutionMock, StartFeatureMock); - EXPECT_CALL(testExecutionMock, StartScenarioMock).Times(1); - EXPECT_CALL(testExecutionMock, RunStepMock).Times(3); - - auto tmp = test_helper::TemporaryFile{ "tmpfile.feature" }; - tmp << "Feature: Test feature\n" - " Scenario: Test scenario\n" - " When I call a nested step\n" - " Then the nested step was called\n"; - - features.push_back(featureTreeFactory.Create(tmp.Path(), "")); - - ASSERT_THAT(contextManager.ProgramContext().ExecutionStatus(), testing::Eq(Result::passed)); - - runner.Run(features); - - ASSERT_THAT(contextManager.ProgramContext().ExecutionStatus(), testing::Eq(Result::passed)); - } -} diff --git a/cucumber_cpp/library/engine/test_helper/CMakeLists.txt b/cucumber_cpp/library/engine/test_helper/CMakeLists.txt index 03105f77..8888e8f4 100644 --- a/cucumber_cpp/library/engine/test_helper/CMakeLists.txt +++ b/cucumber_cpp/library/engine/test_helper/CMakeLists.txt @@ -23,5 +23,4 @@ target_sources(cucumber_cpp.library.engine.test_helper PRIVATE TemporaryFile.cpp TemporaryFile.hpp TestExecutionInstance.hpp - TestRunnerMock.hpp ) diff --git a/cucumber_cpp/library/engine/test_helper/ContextManagerInstance.hpp b/cucumber_cpp/library/engine/test_helper/ContextManagerInstance.hpp deleted file mode 100644 index fecbcc0a..00000000 --- a/cucumber_cpp/library/engine/test_helper/ContextManagerInstance.hpp +++ /dev/null @@ -1,48 +0,0 @@ -#ifndef TEST_HELPER_CONTEXTMANAGERHELPER_HPP -#define TEST_HELPER_CONTEXTMANAGERHELPER_HPP - -#include "cucumber_cpp/library/Context.hpp" -#include "cucumber_cpp/library/engine/ContextManager.hpp" -#include "cucumber_cpp/library/engine/FeatureInfo.hpp" -#include "cucumber_cpp/library/engine/RuleInfo.hpp" -#include "cucumber_cpp/library/engine/ScenarioInfo.hpp" -#include "cucumber_cpp/library/engine/StepInfo.hpp" -#include -#include -#include -#include - -namespace cucumber_cpp::library::engine::test_helper -{ - struct ContextManagerInstanceStorage - { - protected: - std::shared_ptr contextStorageFactory = std::make_shared(); - }; - - struct ContextManagerInstance : private ContextManagerInstanceStorage - , cucumber_cpp::library::engine::ContextManager - { - ContextManagerInstance(std::set> tags = {}) - : cucumber_cpp::library::engine::ContextManager{ contextStorageFactory } - , feature{ tags, {}, {}, {}, {}, {} } - , rule{ feature, {}, {}, {}, {}, {} } - , scenario{ rule, tags, {}, {}, {}, {} } - , step{ scenario, {}, {}, {}, {}, {}, {} } - { - } - - private: - cucumber_cpp::library::engine::FeatureInfo feature; - cucumber_cpp::library::engine::RuleInfo rule; - cucumber_cpp::library::engine::ScenarioInfo scenario; - cucumber_cpp::library::engine::StepInfo step; - - ContextManager::ScopedFeatureContext featureContextScope{ CreateFeatureContext(feature) }; - ContextManager::ScopedRuleContext ruleContextScope{ CreateRuleContext(rule) }; - ContextManager::ScopedScenarioContext scenarioContextScope{ CreateScenarioContext(scenario) }; - ContextManager::ScopedStepContext stepContextScope{ CreateStepContext(step) }; - }; -} - -#endif diff --git a/cucumber_cpp/library/engine/test_helper/FailureHandlerFixture.cpp b/cucumber_cpp/library/engine/test_helper/FailureHandlerFixture.cpp deleted file mode 100644 index 26a7bb2a..00000000 --- a/cucumber_cpp/library/engine/test_helper/FailureHandlerFixture.cpp +++ /dev/null @@ -1,19 +0,0 @@ -#include "cucumber_cpp/library/engine/test_helper/FailureHandlerFixture.hpp" -#include "cucumber_cpp/library/engine/ContextManager.hpp" -#include "gtest/gtest.h" - -namespace cucumber_cpp::library::engine::test_helper -{ - FailureHandlerFixture::FailureHandlerFixture(ContextManager& contextManager) - : contextManager{ contextManager } - { - auto& listeners = testing::UnitTest::GetInstance()->listeners(); - listeners.Append(&googleTestEventListener); - } - - FailureHandlerFixture::~FailureHandlerFixture() - { - auto& listeners = testing::UnitTest::GetInstance()->listeners(); - listeners.Release(&googleTestEventListener); - } -} diff --git a/cucumber_cpp/library/engine/test_helper/FailureHandlerFixture.hpp b/cucumber_cpp/library/engine/test_helper/FailureHandlerFixture.hpp deleted file mode 100644 index 13530a15..00000000 --- a/cucumber_cpp/library/engine/test_helper/FailureHandlerFixture.hpp +++ /dev/null @@ -1,24 +0,0 @@ -#ifndef TEST_HELPER_FAILURE_HANDLER_FIXTURE_HPP -#define TEST_HELPER_FAILURE_HANDLER_FIXTURE_HPP - -#include "cucumber_cpp/CucumberCpp.hpp" -#include "cucumber_cpp/library/engine/ContextManager.hpp" -#include "cucumber_cpp/library/engine/FailureHandler.hpp" - -namespace cucumber_cpp::library::engine::test_helper -{ - struct FailureHandlerFixture - { - explicit FailureHandlerFixture(ContextManager& contextManager); - ~FailureHandlerFixture(); - - ContextManager& contextManager; - - report::ReportForwarderImpl reportHandler{ contextManager }; - TestAssertionHandlerImpl testAssertionHandler{ contextManager, reportHandler }; - GoogleTestEventListener googleTestEventListener{ testAssertionHandler }; - }; - -} - -#endif diff --git a/cucumber_cpp/library/engine/test_helper/TestExecutionInstance.hpp b/cucumber_cpp/library/engine/test_helper/TestExecutionInstance.hpp deleted file mode 100644 index 55c42c46..00000000 --- a/cucumber_cpp/library/engine/test_helper/TestExecutionInstance.hpp +++ /dev/null @@ -1,66 +0,0 @@ -#ifndef TEST_HELPER_TESTEXECUTIONINSTANCE_HPP -#define TEST_HELPER_TESTEXECUTIONINSTANCE_HPP - -#include "cucumber_cpp/library/engine/FeatureInfo.hpp" -#include "cucumber_cpp/library/engine/HookExecutor.hpp" -#include "cucumber_cpp/library/engine/RuleInfo.hpp" -#include "cucumber_cpp/library/engine/ScenarioInfo.hpp" -#include "cucumber_cpp/library/engine/StepInfo.hpp" -#include "cucumber_cpp/library/engine/TestExecution.hpp" -#include "cucumber_cpp/library/engine/test_helper/ContextManagerInstance.hpp" -#include "cucumber_cpp/library/report/Report.hpp" -#include "gmock/gmock.h" - -namespace cucumber_cpp::library::engine::test_helper -{ - struct TestExecutionMockInstance : TestExecution - { - virtual ~TestExecutionMockInstance() = default; - - MOCK_METHOD(void, StartRunMock, ()); - MOCK_METHOD(void, StartFeatureMock, (const FeatureInfo& featureInfo)); - MOCK_METHOD(void, StartRuleMock, (const RuleInfo& ruleInfo)); - MOCK_METHOD(void, StartScenarioMock, (const ScenarioInfo& scenarioInfo)); - - MOCK_METHOD(void, RunStepMock, (const StepInfo& stepInfo)); - - private: - [[nodiscard]] ProgramScope StartRun() override - { - StartRunMock(); - return testExecutionImpl.StartRun(); - } - - [[nodiscard]] FeatureScope StartFeature(const cucumber_cpp::library::engine::FeatureInfo& featureInfo) override - { - StartFeatureMock(featureInfo); - return testExecutionImpl.StartFeature(featureInfo); - } - - [[nodiscard]] RuleScope StartRule(const cucumber_cpp::library::engine::RuleInfo& ruleInfo) override - { - StartRuleMock(ruleInfo); - return testExecutionImpl.StartRule(ruleInfo); - } - - [[nodiscard]] ScenarioScope StartScenario(const cucumber_cpp::library::engine::ScenarioInfo& scenarioInfo) override - { - StartScenarioMock(scenarioInfo); - return testExecutionImpl.StartScenario(scenarioInfo); - } - - void RunStep(const cucumber_cpp::library::engine::StepInfo& stepInfo) override - { - RunStepMock(stepInfo); - testExecutionImpl.RunStep(stepInfo); - } - - ContextManagerInstance contextManager; - HookExecutorImpl hookExecutor{ contextManager }; - report::ReportForwarderImpl reporters{ contextManager }; - - TestExecutionImpl testExecutionImpl{ contextManager, reporters, hookExecutor }; - }; -} - -#endif diff --git a/cucumber_cpp/library/engine/test_helper/TestRunnerMock.hpp b/cucumber_cpp/library/engine/test_helper/TestRunnerMock.hpp deleted file mode 100644 index 40c4b207..00000000 --- a/cucumber_cpp/library/engine/test_helper/TestRunnerMock.hpp +++ /dev/null @@ -1,23 +0,0 @@ -#ifndef TEST_HELPER_TESTRUNNERMOCK_HPP -#define TEST_HELPER_TESTRUNNERMOCK_HPP - -#include "cucumber_cpp/library/engine/ScenarioInfo.hpp" -#include "cucumber_cpp/library/engine/StepType.hpp" -#include "cucumber_cpp/library/engine/TestRunner.hpp" -#include "gmock/gmock.h" -#include -#include -#include - -namespace cucumber_cpp::library::engine::test_helper -{ - struct TestRunnerMock : TestRunner - { - virtual ~TestRunnerMock() = default; - - MOCK_METHOD(void, Run, (const std::vector>& feature), (override)); - MOCK_METHOD(void, NestedStep, (StepType type, std::string step), (override)); - }; -} - -#endif diff --git a/cucumber_cpp/library/formatter/CMakeLists.txt b/cucumber_cpp/library/formatter/CMakeLists.txt new file mode 100644 index 00000000..da6e145a --- /dev/null +++ b/cucumber_cpp/library/formatter/CMakeLists.txt @@ -0,0 +1,38 @@ +add_library(cucumber_cpp.library.formatter ${CCR_EXCLUDE_FROM_ALL}) + +target_sources(cucumber_cpp.library.formatter PRIVATE + Formatter.cpp + Formatter.hpp + JunitXmlFormatter.cpp + JunitXmlFormatter.hpp + MessageFormatter.cpp + MessageFormatter.hpp + PrettyFormatter.cpp + PrettyFormatter.hpp + SummaryFormatter.cpp + SummaryFormatter.hpp + UsageFormatter.cpp + UsageFormatter.hpp +) + +target_include_directories(cucumber_cpp.library.formatter PUBLIC + ../../.. +) + +target_link_libraries(cucumber_cpp.library.formatter PUBLIC + cucumber_cpp.library.cucumber_expression + cucumber_cpp.library.formatter.helper + cucumber_cpp.library.query + cucumber_cpp.library.support + cucumber_cpp.library.util + fmt::fmt + nlohmann_json::nlohmann_json + pugixml::pugixml +) + +add_subdirectory(helper) + +if (CCR_BUILD_TESTS) + add_subdirectory(test) + # add_subdirectory(test_helper) +endif() diff --git a/cucumber_cpp/library/formatter/Formatter.cpp b/cucumber_cpp/library/formatter/Formatter.cpp new file mode 100644 index 00000000..4e97ce02 --- /dev/null +++ b/cucumber_cpp/library/formatter/Formatter.cpp @@ -0,0 +1,23 @@ +#include "cucumber_cpp/library/formatter/Formatter.hpp" +#include "cucumber/messages/envelope.hpp" +#include "cucumber_cpp/library/query/Query.hpp" +#include "cucumber_cpp/library/support/SupportCodeLibrary.hpp" +#include "cucumber_cpp/library/util/Broadcaster.hpp" +#include "nlohmann/json_fwd.hpp" +#include +#include + +namespace cucumber_cpp::library::formatter +{ + Formatter::Formatter(support::SupportCodeLibrary& supportCodeLibrary, query::Query& query, const nlohmann::json& formatOptions, std::ostream& outputStream) + : util::Listener{ query, [this](const cucumber::messages::envelope& envelope) + { + OnEnvelope(envelope); + } } + , supportCodeLibrary{ supportCodeLibrary } + , query{ query } + , formatOptions{ formatOptions } + , outputStream{ outputStream } + { + } +} diff --git a/cucumber_cpp/library/formatter/Formatter.hpp b/cucumber_cpp/library/formatter/Formatter.hpp new file mode 100644 index 00000000..97145494 --- /dev/null +++ b/cucumber_cpp/library/formatter/Formatter.hpp @@ -0,0 +1,31 @@ +#ifndef FORMATTER_FORMATTER_HPP +#define FORMATTER_FORMATTER_HPP + +#include "cucumber/messages/envelope.hpp" +#include "cucumber_cpp/library/query/Query.hpp" +#include "cucumber_cpp/library/support/SupportCodeLibrary.hpp" +#include "cucumber_cpp/library/util/Broadcaster.hpp" +#include "nlohmann/json_fwd.hpp" +#include +#include +#include + +namespace cucumber_cpp::library::formatter +{ + struct Formatter + : util::Listener + { + Formatter(support::SupportCodeLibrary& supportCodeLibrary, query::Query& query, const nlohmann::json& formatOptions, std::ostream& outputStream = std::cout); + virtual ~Formatter() = default; + + protected: + virtual void OnEnvelope(const cucumber::messages::envelope& envelope) = 0; + + support::SupportCodeLibrary& supportCodeLibrary; + query::Query& query; + const nlohmann::json& formatOptions; + std::ostream& outputStream; + }; +} + +#endif diff --git a/cucumber_cpp/library/formatter/JunitXmlFormatter.cpp b/cucumber_cpp/library/formatter/JunitXmlFormatter.cpp new file mode 100644 index 00000000..b0c62879 --- /dev/null +++ b/cucumber_cpp/library/formatter/JunitXmlFormatter.cpp @@ -0,0 +1,212 @@ + +#include "cucumber_cpp/library/formatter/JunitXmlFormatter.hpp" +#include "cucumber/messages/envelope.hpp" +#include "cucumber/messages/pickle_step.hpp" +#include "cucumber/messages/step.hpp" +#include "cucumber/messages/test_case_started.hpp" +#include "cucumber/messages/test_step.hpp" +#include "cucumber/messages/test_step_finished.hpp" +#include "cucumber/messages/test_step_result_status.hpp" +#include "cucumber_cpp/library/query/Query.hpp" +#include "cucumber_cpp/library/util/Duration.hpp" +#include "cucumber_cpp/library/util/Timestamp.hpp" +#include "cucumber_cpp/library/util/Trim.hpp" +#include "fmt/format.h" +#include "fmt/ranges.h" +#include "nlohmann/json_fwd.hpp" +#include "pugixml.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace cucumber_cpp::library::formatter +{ + namespace + { + enum class FailureKind + { + failure, + skipped + }; + + struct ReportFailure + { + FailureKind kind; + std::optional type; + std::optional message; + std::optional stack; + }; + + struct ReportTestCase + { + std::string classname; + std::string name; + std::int64_t time; + std::optional failure; + std::string output; + }; + + struct ReportSuite + { + std::int64_t time; + std::size_t tests; + std::size_t skipped; + std::size_t failures; + std::size_t errors; + std::list testCases; + std::optional timestamp; + }; + + std::optional MakeFailure(query::Query& query, const cucumber::messages::test_case_started& testCaseStarted) + { + const auto result = query.FindMostSevereTestStepResultBy(testCaseStarted); + if (!result.has_value() || result.value()->status == cucumber::messages::test_step_result_status::PASSED) + return std::nullopt; + + const auto& testStepResultStatus = result.value(); + + return std::optional{ + std::in_place, + testStepResultStatus->status == cucumber::messages::test_step_result_status::SKIPPED ? FailureKind::skipped : FailureKind::failure, + testStepResultStatus->exception ? std::make_optional(testStepResultStatus->exception->type) : std::nullopt, + testStepResultStatus->exception ? testStepResultStatus->exception->message : std::nullopt, + testStepResultStatus->exception ? testStepResultStatus->exception->stack_trace : testStepResultStatus->message, + }; + } + + std::string FormatStep(const cucumber::messages::step& gherkinStep, const cucumber::messages::pickle_step& pickleStep, cucumber::messages::test_step_result_status status) + { + auto statusString = std::string{ cucumber::messages::to_string(status) }; + std::transform(statusString.begin(), statusString.end(), statusString.begin(), [](unsigned char c) + { + return std::tolower(c); + }); + + return fmt::format("{:.<76}{}", util::Trim(gherkinStep.keyword) + " " + util::Trim(pickleStep.text), statusString); + } + + std::string MakeOutput(query::Query& query, const cucumber::messages::test_case_started& testCaseStarted) + { + const auto& testStepFinishedAndTestStep = query.FindTestStepFinishedAndTestStepBy(testCaseStarted); + auto outputView = testStepFinishedAndTestStep | + std::views::filter([](const std::pair& pair) + { + const auto [_, testStep] = pair; + return testStep->pickle_step_id.has_value(); + }) | + std::views::transform([&query](const std::pair& pair) + { + const auto [testStepFinished, testStep] = pair; + const auto& pickleStep = *query.FindPickleStepBy(*testStep); + const auto& gherkinStep = query.FindStepBy(pickleStep); + return FormatStep(gherkinStep, pickleStep, testStepFinished->test_step_result.status); + }); + + return fmt::format("\n{}\n", fmt::join(outputView, "\n")); + } + + std::list MakeTestCases(query::Query& query, const std::optional& testClassName) + { + std::list testCases; + + const auto allTestCaseStarted = query.FindAllTestCaseStarted(); + for (const auto testCaseStartedPtr : allTestCaseStarted) + { + const auto& pickle = query.FindPickleBy(*testCaseStartedPtr); + const auto& lineage = query.FindLineageByPickle(pickle); + const auto& testStepFinishedAndTestStep = query.FindTestStepFinishedAndTestStepBy(*testCaseStartedPtr); + + testCases.emplace_back(testClassName.has_value() + ? testClassName.value() + : lineage.feature + ? lineage.feature->name + : pickle.uri, + query::NamingStrategy::Reduce(lineage, pickle), + util::DurationToMilliseconds(query.FindTestCaseDurationBy(*testCaseStartedPtr)).count(), + MakeFailure(query, *testCaseStartedPtr), + MakeOutput(query, *testCaseStartedPtr)); + } + + return testCases; + } + + ReportSuite MakeReport(query::Query& query, const std::optional& testClassName) + { + const auto& statuses = query.CountMostSevereTestStepResultStatus(); + + return { + .time = util::DurationToMilliseconds(query.FindTestRunDuration()).count(), + .tests = query.CountTestCasesStarted(), + .skipped = statuses.at(cucumber::messages::test_step_result_status::SKIPPED), + .failures = statuses.at(cucumber::messages::test_step_result_status::UNKNOWN) + + statuses.at(cucumber::messages::test_step_result_status::PENDING) + + statuses.at(cucumber::messages::test_step_result_status::UNDEFINED) + + statuses.at(cucumber::messages::test_step_result_status::AMBIGUOUS) + + statuses.at(cucumber::messages::test_step_result_status::FAILED), + .errors = 0, + .testCases = MakeTestCases(query, testClassName), + .timestamp = util::MakeIso8601Timestamp(query.FindTestRunStarted().timestamp), + }; + } + } + + JunitXmlFormatter::Options::Options(const nlohmann::json& formatOptions) + : suiteName{ formatOptions.value("suite_name", "Cucumber") } + , testClassName{ formatOptions.contains("test_class_name") ? std::make_optional(formatOptions.at("test_class_name").get()) : std::nullopt } + { + } + + void JunitXmlFormatter::OnEnvelope(const cucumber::messages::envelope& envelope) + { + if (envelope.test_run_finished) + { + const auto& report = MakeReport(query, options.testClassName); + + testSuite.append_attribute("name").set_value(options.suiteName.c_str()); + testSuite.append_attribute("time").set_value(report.time / 1000.0f); + testSuite.append_attribute("tests").set_value(static_cast(report.tests)); + testSuite.append_attribute("skipped").set_value(static_cast(report.skipped)); + testSuite.append_attribute("failures").set_value(static_cast(report.failures)); + testSuite.append_attribute("errors").set_value(static_cast(report.errors)); + + if (report.timestamp) + testSuite.append_attribute("timestamp").set_value(report.timestamp->c_str()); + + for (const auto& testCase : report.testCases) + { + auto testCaseNode = testSuite.append_child("testcase"); + testCaseNode.append_attribute("classname").set_value(testCase.classname.c_str()); + testCaseNode.append_attribute("name").set_value(testCase.name.c_str()); + testCaseNode.append_attribute("time").set_value(testCase.time / 1000.0f); + + if (testCase.failure) + { + auto failureNode = testCaseNode.append_child(testCase.failure->kind == FailureKind::failure ? "failure" : "skipped"); + + if (testCase.failure->kind == FailureKind::failure && testCase.failure->type) + failureNode.append_attribute("type").set_value(testCase.failure->type->c_str()); + + if (testCase.failure->message) + failureNode.append_attribute("message").set_value(testCase.failure->message->c_str()); + + if (testCase.failure->stack) + failureNode.append_child(pugi::node_cdata).set_value(testCase.failure->stack->c_str()); + } + + if (!testCase.output.empty()) + { + auto systemOutNode = testCaseNode.append_child("system-out"); + systemOutNode.append_child(pugi::node_cdata).set_value(testCase.output.c_str()); + } + } + + doc.save(outputStream); + } + } +} diff --git a/cucumber_cpp/library/formatter/JunitXmlFormatter.hpp b/cucumber_cpp/library/formatter/JunitXmlFormatter.hpp new file mode 100644 index 00000000..0f6c3d9a --- /dev/null +++ b/cucumber_cpp/library/formatter/JunitXmlFormatter.hpp @@ -0,0 +1,38 @@ +#ifndef FORMATTER_JUNIT_XML_FORMATTER_HPP +#define FORMATTER_JUNIT_XML_FORMATTER_HPP + +#include "cucumber/messages/envelope.hpp" +#include "cucumber_cpp/library/formatter/Formatter.hpp" +#include "nlohmann/json_fwd.hpp" +#include "pugixml.hpp" +#include +#include + +namespace cucumber_cpp::library::formatter +{ + struct JunitXmlFormatter + : Formatter + { + using Formatter::Formatter; + + constexpr static auto name = "junit"; + + private: + struct Options + { + explicit Options(const nlohmann::json& formatOptions); + + const std::string suiteName; + const std::optional testClassName; + }; + + void OnEnvelope(const cucumber::messages::envelope& envelope) override; + + Options options{ formatOptions.contains(name) ? formatOptions.at(name) : nlohmann::json::object() }; + + pugi::xml_document doc; + pugi::xml_node testSuite{ doc.append_child("testsuite") }; + }; +} + +#endif diff --git a/cucumber_cpp/library/formatter/MessageFormatter.cpp b/cucumber_cpp/library/formatter/MessageFormatter.cpp new file mode 100644 index 00000000..35ab691d --- /dev/null +++ b/cucumber_cpp/library/formatter/MessageFormatter.cpp @@ -0,0 +1,10 @@ +#include "cucumber_cpp/library/formatter/MessageFormatter.hpp" +#include "cucumber/messages/envelope.hpp" + +namespace cucumber_cpp::library::formatter +{ + void MessageFormatter::OnEnvelope(const cucumber::messages::envelope& envelope) + { + outputStream << envelope.to_json() << "\n"; + } +} diff --git a/cucumber_cpp/library/formatter/MessageFormatter.hpp b/cucumber_cpp/library/formatter/MessageFormatter.hpp new file mode 100644 index 00000000..b7d69493 --- /dev/null +++ b/cucumber_cpp/library/formatter/MessageFormatter.hpp @@ -0,0 +1,24 @@ +#ifndef FORMATTER_MESSAGE_FORMATTER_HPP +#define FORMATTER_MESSAGE_FORMATTER_HPP + +#include "cucumber/messages/envelope.hpp" +#include "cucumber_cpp/library/formatter/Formatter.hpp" +#include "nlohmann/json_fwd.hpp" +#include +#include + +namespace cucumber_cpp::library::formatter +{ + struct MessageFormatter + : Formatter + { + using Formatter::Formatter; + + constexpr static auto name = "message"; + + private: + void OnEnvelope(const cucumber::messages::envelope& envelope) override; + }; +} + +#endif diff --git a/cucumber_cpp/library/formatter/PrettyFormatter.cpp b/cucumber_cpp/library/formatter/PrettyFormatter.cpp new file mode 100644 index 00000000..6e12da5b --- /dev/null +++ b/cucumber_cpp/library/formatter/PrettyFormatter.cpp @@ -0,0 +1,155 @@ +#include "cucumber_cpp/library/formatter/PrettyFormatter.hpp" +#include "cucumber/messages/attachment.hpp" +#include "cucumber/messages/envelope.hpp" +#include "cucumber/messages/test_case_started.hpp" +#include "cucumber/messages/test_run_finished.hpp" +#include "cucumber/messages/test_step.hpp" +#include "cucumber/messages/test_step_finished.hpp" +#include "cucumber/messages/test_step_result_status.hpp" +#include "cucumber_cpp/library/formatter/helper/FormatMessages.hpp" +#include "cucumber_cpp/library/formatter/helper/PrintMessages.hpp" +#include "cucumber_cpp/library/formatter/helper/Theme.hpp" +#include "fmt/ostream.h" +#include "nlohmann/json_fwd.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace cucumber_cpp::library::formatter +{ + PrettyFormatter::Options::Options(const nlohmann::json& formatOptions) + : includeAttachments{ formatOptions.value("include_attachments", true) } + , includeFeatureLine{ formatOptions.value("include_feature_line", true) } + , includeRuleLine{ formatOptions.value("include_rule_line", true) } + , useStatusIcon{ formatOptions.value("use_status_icon", true) } + , theme{ helper::CreateTheme(formatOptions.value("theme", std::string_view{ "cucumber" })) } + { + } + + void PrettyFormatter::OnEnvelope(const cucumber::messages::envelope& envelope) + { + if (envelope.test_case_started) + { + CalculateIndent(envelope.test_case_started.value()); + HandleTestCaseStarted(envelope.test_case_started.value()); + } + + if (envelope.attachment) + HandleAttachment(envelope.attachment.value()); + + if (envelope.test_step_finished) + HandleTestStepFinished(envelope.test_step_finished.value()); + + if (envelope.test_run_finished) + HandleTestRunFinished(envelope.test_run_finished.value()); + } + + void PrettyFormatter::CalculateIndent(const cucumber::messages::test_case_started& testCaseStarted) + { + const auto& pickle = query.FindPickleBy(testCaseStarted); + const auto& lineage = query.FindLineageByPickle(pickle); + const auto& scenario = *lineage.scenario; + const auto scenarioLength = helper::Unstyled(helper::FormatPickleTitle(pickle, scenario, options.theme)).length(); + + const auto& testCase = query.FindTestCaseBy(testCaseStarted); + + const auto hasPickleStepId = [](const cucumber::messages::test_step& testStep) + { + return testStep.pickle_step_id.has_value(); + }; + const auto toLength = [this](const cucumber::messages::test_step& testStep) + { + const auto* pickleStep = query.FindPickleStepBy(testStep); + const auto& step = query.FindStepBy(*pickleStep); + return helper::Unstyled(helper::FormatStepTitle(testStep, *pickleStep, step, cucumber::messages::test_step_result_status::UNKNOWN, options.useStatusIcon, options.theme)).length(); + }; + + auto steplengths = testCase.test_steps | std::views::filter(hasPickleStepId) | std::views::transform(toLength); + + const auto maxStepLengthIter = std::ranges::max_element(steplengths); + const auto maxStepLength = (maxStepLengthIter != steplengths.end()) ? *maxStepLengthIter : 0; + + maxContentLengthByTestCaseStartedId[testCaseStarted.id] = std::max(scenarioLength, options.useStatusIcon ? maxStepLength + 2 : maxStepLength); + + std::size_t scenarioIndent{ 0 }; + if (options.includeFeatureLine) + { + scenarioIndent += helper::gherkinIndentLength; + if (options.includeRuleLine && lineage.rule) + scenarioIndent += helper::gherkinIndentLength; + } + + scenarioIndentByTestCaseStartedId[testCaseStarted.id] = scenarioIndent; + } + + void PrettyFormatter::HandleTestCaseStarted(const cucumber::messages::test_case_started& testCaseStarted) + { + const auto& pickle = query.FindPickleBy(testCaseStarted); + const auto& lineage = query.FindLineageByPickle(pickle); + const auto& scenario = lineage.scenario; + const auto& rule = lineage.rule; + const auto& feature = lineage.feature; + + const auto scenarioIndent = scenarioIndentByTestCaseStartedId.at(testCaseStarted.id); + const auto maxContentLength = maxContentLengthByTestCaseStartedId.at(testCaseStarted.id); + + if (options.includeFeatureLine && rule && !printedFeatureUris.contains(feature.get())) + helper::PrintFeatureLine(outputStream, *feature, options.theme); + + if (options.includeRuleLine && rule && !printedRuleIds.contains(rule.get())) + helper::PrintRuleLine(outputStream, *rule, options.theme); + + outputStream << "\n"; + + helper::PrintTags(outputStream, pickle, scenarioIndent, options.theme); + helper::PrintScenarioLine(outputStream, pickle, *scenario, scenarioIndent, maxContentLength, options.theme); + + printedFeatureUris.insert(feature.get()); + printedRuleIds.insert(rule.get()); + } + + void PrettyFormatter::HandleAttachment(const cucumber::messages::attachment& attachment) + { + if (!options.includeAttachments) + return; + + if (attachment.test_case_started_id.has_value()) + helper::PrintAttachment(outputStream, attachment, scenarioIndentByTestCaseStartedId.at(attachment.test_case_started_id.value()), options.useStatusIcon, options.theme); + } + + void PrettyFormatter::HandleTestStepFinished(const cucumber::messages::test_step_finished& testStepFinished) + { + const auto scenarioIndent = scenarioIndentByTestCaseStartedId.at(testStepFinished.test_case_started_id); + const auto maxContentLength = maxContentLengthByTestCaseStartedId.at(testStepFinished.test_case_started_id); + + const auto& testStep = query.FindTestStepBy(testStepFinished); + const auto* pickleStep = query.FindPickleStepBy(testStep); + + if (pickleStep != nullptr) + { + const auto& step = query.FindStepBy(*pickleStep); + const auto* stepDefinition = (testStep.step_definition_ids && !testStep.step_definition_ids->empty()) ? &query.FindStepDefinitionById(testStep.step_definition_ids->front()) : nullptr; + + helper::PrintStepLine(outputStream, testStepFinished, testStep, *pickleStep, step, stepDefinition, scenarioIndent, maxContentLength, options.useStatusIcon, options.theme); + helper::PrintStepArgument(outputStream, *pickleStep, scenarioIndent, options.useStatusIcon, options.theme); + helper::PrintAmbiguousStep(outputStream, query, testStepFinished, testStep, scenarioIndent, options.useStatusIcon, options.theme); + } + helper::PrintError(outputStream, testStepFinished, scenarioIndent, options.useStatusIcon, options.theme); + } + + void PrettyFormatter::HandleTestRunFinished(const cucumber::messages::test_run_finished& testRunFinished) + { + const auto content = FormatTestRunFinishedError(testRunFinished, options.theme); + + if (!content.empty()) + fmt::println(outputStream, "{}", content); + } + +} diff --git a/cucumber_cpp/library/formatter/PrettyFormatter.hpp b/cucumber_cpp/library/formatter/PrettyFormatter.hpp new file mode 100644 index 00000000..05a32313 --- /dev/null +++ b/cucumber_cpp/library/formatter/PrettyFormatter.hpp @@ -0,0 +1,66 @@ +#ifndef FORMATTER_PRETTY_PRINTER_HPP +#define FORMATTER_PRETTY_PRINTER_HPP + +#include "cucumber/messages/attachment.hpp" +#include "cucumber/messages/envelope.hpp" +#include "cucumber/messages/feature.hpp" +#include "cucumber/messages/pickle.hpp" +#include "cucumber/messages/pickle_step.hpp" +#include "cucumber/messages/rule.hpp" +#include "cucumber/messages/scenario.hpp" +#include "cucumber/messages/step.hpp" +#include "cucumber/messages/step_definition.hpp" +#include "cucumber/messages/test_case_started.hpp" +#include "cucumber/messages/test_run_finished.hpp" +#include "cucumber/messages/test_step.hpp" +#include "cucumber/messages/test_step_finished.hpp" +#include "cucumber_cpp/library/formatter/Formatter.hpp" +#include "cucumber_cpp/library/formatter/helper/Theme.hpp" +#include +#include +#include +#include +#include +#include + +namespace cucumber_cpp::library::formatter +{ + struct PrettyFormatter + : Formatter + { + using Formatter::Formatter; + + constexpr static auto name = "pretty"; + + struct Options + { + explicit Options(const nlohmann::json& formatOptions); + + const bool includeAttachments; + const bool includeFeatureLine; + const bool includeRuleLine; + const bool useStatusIcon; + const helper::Theme theme; + }; + + private: + void OnEnvelope(const cucumber::messages::envelope& envelope) override; + + void CalculateIndent(const cucumber::messages::test_case_started& testCaseStarted); + + void HandleTestCaseStarted(const cucumber::messages::test_case_started& testCaseStarted); + void HandleAttachment(const cucumber::messages::attachment& attachment); + void HandleTestStepFinished(const cucumber::messages::test_step_finished& testStepFinished); + void HandleTestRunFinished(const cucumber::messages::test_run_finished& testRunFinished); + + Options options{ formatOptions.contains(name) ? formatOptions.at(name) : nlohmann::json::object() }; + + std::map maxContentLengthByTestCaseStartedId; + std::map scenarioIndentByTestCaseStartedId; + + std::set printedFeatureUris; + std::set printedRuleIds; + }; +} + +#endif diff --git a/cucumber_cpp/library/formatter/SummaryFormatter.cpp b/cucumber_cpp/library/formatter/SummaryFormatter.cpp new file mode 100644 index 00000000..3b170254 --- /dev/null +++ b/cucumber_cpp/library/formatter/SummaryFormatter.cpp @@ -0,0 +1,226 @@ +#include "cucumber_cpp/library/formatter/SummaryFormatter.hpp" +#include "cucumber/messages/duration.hpp" +#include "cucumber/messages/envelope.hpp" +#include "cucumber/messages/pickle.hpp" +#include "cucumber/messages/scenario.hpp" +#include "cucumber/messages/test_case.hpp" +#include "cucumber/messages/test_case_finished.hpp" +#include "cucumber/messages/test_case_started.hpp" +#include "cucumber/messages/test_step.hpp" +#include "cucumber/messages/test_step_finished.hpp" +#include "cucumber/messages/test_step_result.hpp" +#include "cucumber/messages/test_step_result_status.hpp" +#include "cucumber_cpp/library/formatter/helper/FormatMessages.hpp" +#include "cucumber_cpp/library/formatter/helper/PrintMessages.hpp" +#include "cucumber_cpp/library/formatter/helper/TextBuilder.hpp" +#include "cucumber_cpp/library/formatter/helper/Theme.hpp" +#include "cucumber_cpp/library/query/Query.hpp" +#include "cucumber_cpp/library/util/Duration.hpp" +#include "cucumber_cpp/library/util/ToLower.hpp" +#include "fmt/ostream.h" +#include "fmt/ranges.h" +#include "nlohmann/json_fwd.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace cucumber_cpp::library::formatter +{ + namespace + { + bool IsFailure(cucumber::messages::test_step_result_status status, bool willBeRetried) + { + return status == cucumber::messages::test_step_result_status::AMBIGUOUS || + status == cucumber::messages::test_step_result_status::UNDEFINED || + (status == cucumber::messages::test_step_result_status::FAILED && !willBeRetried); + } + + bool IsWarning(cucumber::messages::test_step_result_status status, bool willBeRetried) + { + return status == cucumber::messages::test_step_result_status::PENDING || + (status == cucumber::messages::test_step_result_status::FAILED && willBeRetried); + } + + std::size_t CalculateLength(const query::Query& query, const cucumber::messages::pickle& pickle, const cucumber::messages::test_case_started& testCaseStarted, const cucumber::messages::test_case_finished& testCaseFinished, const cucumber::messages::scenario& scenario, const cucumber::messages::test_case& testCase, bool useStatusIcon, const helper::Theme& theme) + { + const auto scenarioLength = helper::Unstyled(helper::FormatPickleAttemptTitle(pickle, testCaseStarted.attempt, testCaseFinished.will_be_retried, scenario, theme)).length(); + + const auto toLength = [&query, useStatusIcon, &theme, isBeforeHook = true](const cucumber::messages::test_step& testStep) mutable -> std::size_t + { + if (testStep.hook_id.has_value()) + { + const auto& hook = query.FindHookById(testStep.hook_id.value()); + return helper::Unstyled(helper::FormatHookTitle(hook, cucumber::messages::test_step_result_status::UNKNOWN, isBeforeHook, useStatusIcon, theme)).length(); + } + else if (testStep.hook_id.has_value()) + { + isBeforeHook = false; + + const auto* pickleStep = query.FindPickleStepBy(testStep); + const auto& step = query.FindStepBy(*pickleStep); + return helper::Unstyled(helper::FormatStepTitle(testStep, *pickleStep, step, cucumber::messages::test_step_result_status::UNKNOWN, useStatusIcon, theme)).length(); + } + return 0; + }; + + auto steplengths = testCase.test_steps | std::views::transform(toLength); + + const auto maxStepLengthIter = std::ranges::max_element(steplengths); + const auto maxStepLength = (maxStepLengthIter != steplengths.end()) ? *maxStepLengthIter : 0; + const auto maxContentLength = std::max(scenarioLength, useStatusIcon ? maxStepLength + 2 : maxStepLength); + + return maxContentLength; + } + + void HandleHookStep(std::ostream& stream, const query::Query& query, const cucumber::messages::test_step_finished& testStepFinished, const cucumber::messages::test_step& testStep, std::size_t scenarioIndent, std::size_t maxContentLength, bool isBeforeHook, bool useStatusIcon, const helper::Theme& theme) + { + const auto& hook = query.FindHookById(testStep.hook_id.value()); + helper::PrintHookLine(stream, testStepFinished, hook, scenarioIndent, maxContentLength, isBeforeHook, useStatusIcon, theme); + } + + void HandleTestStep(std::ostream& stream, const query::Query& query, const cucumber::messages::test_step_finished& testStepFinished, const cucumber::messages::test_step& testStep, std::size_t scenarioIndent, std::size_t maxContentLength, bool useStatusIcon, const helper::Theme& theme) + { + const auto* pickleStep = query.FindPickleStepBy(testStep); + const auto& step = query.FindStepBy(*pickleStep); + const auto* stepDefinition = (testStep.step_definition_ids && !testStep.step_definition_ids->empty()) ? &query.FindStepDefinitionById(testStep.step_definition_ids->front()) : nullptr; + + helper::PrintStepLine(stream, testStepFinished, testStep, *pickleStep, step, stepDefinition, scenarioIndent, maxContentLength, useStatusIcon, theme); + + helper::PrintStepArgument(stream, *pickleStep, scenarioIndent, useStatusIcon, theme); + helper::PrintAmbiguousStep(stream, query, testStepFinished, testStep, scenarioIndent, useStatusIcon, theme); + + helper::PrintError(stream, testStepFinished, scenarioIndent, useStatusIcon, theme); + } + + void HandleTestSteps(std::ostream& stream, const query::Query& query, const cucumber::messages::test_case_started& testCaseStarted, std::size_t scenarioIndent, std::size_t maxContentLength, bool useStatusIcon, const helper::Theme& theme) + { + const auto& testStepFinishedAndTestStep = query.FindTestStepFinishedAndTestStepBy(testCaseStarted); + + auto isBeforeHook = true; + for (const auto [testStepFinished, testStep] : testStepFinishedAndTestStep) + { + if (testStep->hook_id.has_value()) + HandleHookStep(stream, query, *testStepFinished, *testStep, scenarioIndent, maxContentLength, isBeforeHook, useStatusIcon, theme); + else + { + isBeforeHook = false; + HandleTestStep(stream, query, *testStepFinished, *testStep, scenarioIndent, maxContentLength, useStatusIcon, theme); + } + } + } + + void HandleTestCaseStarted(std::ostream& stream, const query::Query& query, const cucumber::messages::test_case_started& testCaseStarted, bool useStatusIcon, const helper::Theme& theme) + { + auto scenarioIndent = 0; + + const auto& testCaseFinished = query.TestCaseFinishedByTestCaseStartedId().at(testCaseStarted.id); + const auto& pickle = query.FindPickleBy(testCaseStarted); + const auto& lineage = query.FindLineageByPickle(pickle); + const auto& scenario = lineage.scenario; + const auto& rule = lineage.rule; + const auto& feature = lineage.feature; + const auto& testCase = query.FindTestCaseBy(testCaseStarted); + + const auto maxContentLength = CalculateLength(query, pickle, testCaseStarted, testCaseFinished, *scenario, testCase, useStatusIcon, theme); + + fmt::println(stream, ""); + helper::PrintScenarioAttemptLine(stream, pickle, testCaseStarted.attempt, testCaseFinished.will_be_retried, *scenario, scenarioIndent, maxContentLength, theme); + HandleTestSteps(stream, query, testCaseStarted, scenarioIndent, maxContentLength, useStatusIcon, theme); + } + + void HandleTestCaseStartedList(std::ostream& stream, const query::Query& query, const std::string& title, const std::map testCaseStartedList, bool useStatusIcon, const helper::Theme& theme) + { + if (testCaseStartedList.empty()) + return; + + fmt::println(stream, "\n{}:", title); + + for (const auto& [id, testCaseStarted] : testCaseStartedList) + HandleTestCaseStarted(stream, query, *testCaseStarted, useStatusIcon, theme); + } + + void HandleSummary(std::ostream& stream, const std::string& summary, const std::map& counts, const helper::Theme& theme) + { + const auto countToStatusString = [&theme](const auto& pair) + { + const auto& [status, count] = pair; + return helper::TextBuilder{} + .Append(std::to_string(count)) + .Space() + .Append(util::ToLower(std::string{ cucumber::messages::to_string(status) })) + .Build(theme.status.All(status)); + }; + + auto countsValues = counts | std::views::values; + const auto totalCount = std::accumulate(countsValues.begin(), countsValues.end(), std::size_t{ 0u }); + + fmt::println(stream, "{} {} {}", totalCount, summary, + fmt::join(counts | std::views::transform(countToStatusString), ", ")); + } + } + + SummaryFormatter::Options::Options(const nlohmann::json& formatOptions) + : useStatusIcon{ formatOptions.value("use_status_icon", true) } + , theme{ helper::CreateTheme(formatOptions.value("theme", std::string_view{ "cucumber" })) } + { + } + + void SummaryFormatter::OnEnvelope(const cucumber::messages::envelope& envelope) + { + if (envelope.test_run_finished) + LogSummary(query.FindTestRunDuration()); + } + + void SummaryFormatter::LogSummary(const cucumber::messages::duration& testRunDuration) + { + std::map warningTestStepResults{}; + std::map failedTestStepResults{}; + + std::map scenarioCounts; + std::map stepCounts; + + cucumber::messages::duration totalStepDuration{}; + + for (const auto& [id, testCaseStarted] : query.TestCaseStarted()) + { + const auto& testCaseFinished = query.TestCaseFinishedByTestCaseStartedId().at(testCaseStarted.id); + const auto* testStepResult = query.FindMostSevereTestStepResultBy(testCaseStarted).value_or(nullptr); + + if (testStepResult != nullptr && IsWarning(testStepResult->status, testCaseFinished.will_be_retried)) + warningTestStepResults[id] = &testCaseStarted; + + if (testStepResult != nullptr && IsFailure(testStepResult->status, testCaseFinished.will_be_retried)) + failedTestStepResults[id] = &testCaseStarted; + + if (!testCaseFinished.will_be_retried) + { + if (const auto& testStepResultPtr = query.FindMostSevereTestStepResultBy(testCaseFinished); testStepResultPtr.has_value()) + ++scenarioCounts[testStepResultPtr.value()->status]; + else + ++scenarioCounts[cucumber::messages::test_step_result_status::PASSED]; + + const auto& testStepFinishedAndTestStep = query.FindTestStepFinishedAndTestStepBy(testCaseStarted); + for (const auto& [testStepFinished, testStep] : testStepFinishedAndTestStep) + { + if (testStep->pickle_step_id.has_value()) + { + ++stepCounts[testStepFinished->test_step_result.status]; + totalStepDuration += testStepFinished->test_step_result.duration; + } + } + } + } + + HandleTestCaseStartedList(outputStream, query, "Warnings", warningTestStepResults, options.useStatusIcon, options.theme); + HandleTestCaseStartedList(outputStream, query, "Failures", failedTestStepResults, options.useStatusIcon, options.theme); + HandleSummary(outputStream, "scenarios", scenarioCounts, options.theme); + HandleSummary(outputStream, "steps", stepCounts, options.theme); + + fmt::println(outputStream, "{:%Mm %S}s (executing steps: {:%Mm %S}s)", util::DurationToMilliseconds(testRunDuration), util::DurationToMilliseconds(totalStepDuration)); + } +} diff --git a/cucumber_cpp/library/formatter/SummaryFormatter.hpp b/cucumber_cpp/library/formatter/SummaryFormatter.hpp new file mode 100644 index 00000000..7d85bebb --- /dev/null +++ b/cucumber_cpp/library/formatter/SummaryFormatter.hpp @@ -0,0 +1,36 @@ +#ifndef FORMATTER_SUMMARY_FORMATTER_HPP +#define FORMATTER_SUMMARY_FORMATTER_HPP + +#include "cucumber/messages/duration.hpp" +#include "cucumber/messages/envelope.hpp" +#include "cucumber_cpp/library/formatter/Formatter.hpp" +#include "cucumber_cpp/library/formatter/helper/Theme.hpp" +#include "nlohmann/json_fwd.hpp" +#include + +namespace cucumber_cpp::library::formatter +{ + struct SummaryFormatter + : Formatter + { + using Formatter::Formatter; + + constexpr static auto name = "summary"; + + private: + struct Options + { + explicit Options(const nlohmann::json& formatOptions); + + const bool useStatusIcon; + const helper::Theme theme; + }; + + void OnEnvelope(const cucumber::messages::envelope& envelope) override; + void LogSummary(const cucumber::messages::duration& testRunDuration); + + Options options{ formatOptions.contains(name) ? formatOptions.at(name) : nlohmann::json::object() }; + }; +} + +#endif diff --git a/cucumber_cpp/library/formatter/UsageFormatter.cpp b/cucumber_cpp/library/formatter/UsageFormatter.cpp new file mode 100644 index 00000000..dbe6cf5a --- /dev/null +++ b/cucumber_cpp/library/formatter/UsageFormatter.cpp @@ -0,0 +1,225 @@ +#include "cucumber_cpp/library/formatter/UsageFormatter.hpp" +#include "cucumber/messages/envelope.hpp" +#include "cucumber/messages/location.hpp" +#include "cucumber/messages/step_definition_pattern_type.hpp" +#include "cucumber/messages/test_step_result_status.hpp" +#include "cucumber_cpp/library/formatter/helper/Theme.hpp" +#include "cucumber_cpp/library/query/Query.hpp" +#include "cucumber_cpp/library/util/Duration.hpp" +#include "fmt/base.h" +#include "fmt/format.h" +#include "fmt/ostream.h" +#include "nlohmann/json_fwd.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace cucumber_cpp::library::formatter +{ + namespace + { + bool HasExecuted(cucumber::messages::test_step_result_status status) + { + return status == cucumber::messages::test_step_result_status::UNKNOWN || + status == cucumber::messages::test_step_result_status::PASSED || + status == cucumber::messages::test_step_result_status::PENDING || + status == cucumber::messages::test_step_result_status::FAILED; + } + + struct UsageMatch + { + std::optional duration; + std::size_t line; + std::string text; + std::string uri; + }; + + struct Usage + { + std::size_t line; + std::list matches; + std::string pattern; + std::string patternType; + std::string uri; + std::optional meanDuration; + }; + + std::map> BuildEmptyMapping(const query::Query& query) + { + std::map> mapping; + + for (const auto& [id, stepDefinition] : query.StepDefinitions()) + { + mapping[id] = Usage{ + .line = stepDefinition.source_reference.location.value_or(cucumber::messages::location{}).line, + .matches = {}, + .pattern = stepDefinition.pattern.source, + .patternType = std::string{ cucumber::messages::to_string(stepDefinition.pattern.type) }, + .uri = stepDefinition.source_reference.uri.value_or(""), + .meanDuration = {}, + }; + } + + return mapping; + } + + std::map> BuildMapping(const query::Query& query) + { + auto mapping = BuildEmptyMapping(query); + + for (const auto& [testCaseStartedId, testCaseFinished] : query.TestCaseFinishedByTestCaseStartedId()) + { + const auto& testCaseStarted = query.FindTestCaseStartedById(testCaseStartedId); + const auto& testCase = query.FindTestCaseBy(testCaseStarted); + const auto testStepFinishedAndTestStep = query.FindTestStepFinishedAndTestStepBy(testCaseStarted); + + for (const auto [testStepFinished, testStep] : testStepFinishedAndTestStep) + { + if (!testStep->step_definition_ids.has_value() || testStep->step_definition_ids.value().size() != 1) + continue; + + const auto& pickleStep = query.FindPickleStepById(testStep->pickle_step_id.value()); + const auto& pickle = query.FindPickleById(testCase.pickle_id); + const auto& step = query.FindStepBy(pickleStep); + const auto& stepDefinitionId = testStep->step_definition_ids.value().front(); + const auto& lineage = query.FindLineageByPickle(pickle); + + std::optional duration{}; + if (HasExecuted(testStepFinished->test_step_result.status)) + duration = util::DurationToNanoSeconds(query.FindTestStepDurationByTestStepId(testStepFinished->test_step_id)); + + mapping.at(stepDefinitionId).matches.emplace_back(duration, step.location.line, pickleStep.text, lineage.gherkinDocument->uri.value_or("")); + } + } + + for (auto& usage : mapping | std::views::values) + { + + if (usage.matches.empty()) + usage.meanDuration = std::chrono::nanoseconds{ -1 }; + else + { + std::chrono::nanoseconds totalDuration{}; + std::size_t countDuration{}; + + for (const auto& match : usage.matches) + { + if (match.duration.has_value()) + { + ++countDuration; + totalDuration += match.duration.value_or(std::chrono::nanoseconds{}); + } + } + + if (countDuration != 0) + usage.meanDuration = totalDuration / countDuration; + } + } + + return mapping; + } + + std::list GetUsage(const query::Query& query, bool unusedOnly) + { + const auto& mapping = BuildMapping(query); + auto mapValues = mapping | std::views::values | (std::views::filter([unusedOnly](const Usage& usage) + { + return !unusedOnly || usage.matches.empty(); + })); + + std::list usageList{ mapValues.begin(), mapValues.end() }; + + usageList.sort([](const Usage& lhs, const Usage& rhs) + { + if (lhs.matches.empty() && !rhs.matches.empty()) + return false; + if (!lhs.matches.empty() && rhs.matches.empty()) + return true; + + if (lhs.meanDuration == rhs.meanDuration) + return std::make_tuple(lhs.uri, lhs.line) < std::make_tuple(rhs.uri, rhs.line); + + return lhs.meanDuration > rhs.meanDuration; + }); + + for (auto& usage : usageList) + usage.matches.sort([](const UsageMatch& lhs, const UsageMatch& rhs) + { + if (lhs.duration == rhs.duration) + return std::make_tuple(lhs.uri, lhs.line, lhs.text) < std::make_tuple(rhs.uri, rhs.line, rhs.text); + + return lhs.duration > rhs.duration; + }); + + return usageList; + } + } + + UsageFormatter::Options::Options(const nlohmann::json& json) + : unusedOnly{ json.value("unused_only", false) } + , theme{ helper::CreateTheme(json.value("theme", "cucumber")) } + { + } + + void UsageFormatter::OnEnvelope(const cucumber::messages::envelope& envelope) + { + if (envelope.test_run_finished.has_value()) + { + const auto& mapping = GetUsage(query, options.unusedOnly); + + auto patternWidth = std::string("Pattern / Text").size(); + auto durationWidth = std::string("Duration").size(); + auto locationWidth = std::string("Location").size(); + + for (const auto& usage : mapping) + { + patternWidth = std::max(patternWidth, usage.pattern.size()); + locationWidth = std::max(locationWidth, fmt::format("{}:{}", usage.uri, usage.line).size()); + + if (usage.meanDuration.has_value()) + durationWidth = std::max(durationWidth, fmt::format("{}", std::chrono::duration_cast(usage.meanDuration.value())).size()); + else + durationWidth = std::max(durationWidth, std::string("-").size()); + + for (const auto& match : usage.matches) + { + patternWidth = std::max(patternWidth, match.text.size() + 2); + locationWidth = std::max(locationWidth, fmt::format("{}:{}", match.uri, match.line).size()); + } + } + + fmt::println(outputStream, fmt::runtime(topRow), "", patternWidth, "", durationWidth, "", locationWidth); + fmt::println(outputStream, "{0} {1:<{2}} {0} {3:<{4}} {0} {5:<{6}} {0}", options.theme.table.vertical, "Pattern / Text", patternWidth, "Duration", durationWidth, "Location", locationWidth); + + const auto horizontalDivider = fmt::format(fmt::runtime(middleRow), "", patternWidth, "", durationWidth, "", locationWidth); + + for (const auto& usage : mapping) + { + fmt::println(outputStream, "{}", horizontalDivider); + + if (usage.matches.empty()) + fmt::println(outputStream, "{0} {1:<{2}} {0} {3:<{4}} {0} {5:<{6}} {0}", options.theme.table.vertical, usage.pattern, patternWidth, "UNUSED", durationWidth, fmt::format("{}:{}", usage.uri, usage.line), locationWidth); + else if (usage.meanDuration.has_value()) + fmt::println(outputStream, "{0} {1:<{2}} {0} {3:<{4}} {0} {5:<{6}} {0}", options.theme.table.vertical, usage.pattern, patternWidth, std::chrono::duration_cast(usage.meanDuration.value()), durationWidth, fmt::format("{}:{}", usage.uri, usage.line), locationWidth); + else + fmt::println(outputStream, "{0} {1:<{2}} {0} {3:<{4}} {0} {5:<{6}} {0}", options.theme.table.vertical, usage.pattern, patternWidth, "-", durationWidth, fmt::format("{}:{}", usage.uri, usage.line), locationWidth); + + for (const auto& match : usage.matches) + if (match.duration.has_value()) + fmt::println(outputStream, "{0} {1:<{2}} {0} {3:<{4}} {0} {5:<{6}} {0}", options.theme.table.vertical, match.text, patternWidth - 2, std::chrono::duration_cast(match.duration.value()), durationWidth, fmt::format("{}:{}", match.uri, match.line), locationWidth); + else + fmt::println(outputStream, "{0} {1:<{2}} {0} {3:<{4}} {0} {5:<{6}} {0}", options.theme.table.vertical, match.text, patternWidth - 2, "-", durationWidth, fmt::format("{}:{}", match.uri, match.line), locationWidth); + } + + fmt::println(outputStream, fmt::runtime(bottomRow), "", patternWidth, "", durationWidth, "", locationWidth); + } + } +} diff --git a/cucumber_cpp/library/formatter/UsageFormatter.hpp b/cucumber_cpp/library/formatter/UsageFormatter.hpp new file mode 100644 index 00000000..90315851 --- /dev/null +++ b/cucumber_cpp/library/formatter/UsageFormatter.hpp @@ -0,0 +1,41 @@ +#ifndef FORMATTER_USAGE_FORMATTER_HPP +#define FORMATTER_USAGE_FORMATTER_HPP + +#include "cucumber/messages/envelope.hpp" +#include "cucumber_cpp/library/formatter/Formatter.hpp" +#include "cucumber_cpp/library/formatter/helper/Theme.hpp" +#include "fmt/base.h" +#include "fmt/format.h" +#include "nlohmann/json_fwd.hpp" +#include + +namespace cucumber_cpp::library::formatter +{ + struct UsageFormatter + : Formatter + { + using Formatter::Formatter; + + constexpr static auto name = "usage"; + + private: + struct Options + { + explicit Options(const nlohmann::json& json); + + bool unusedOnly; + helper::Theme theme; + }; + + void OnEnvelope(const cucumber::messages::envelope& envelope) override; + + Options options{ formatOptions.value(name, nlohmann::json::object()) }; + + const std::string rowFormat = "{0}{1}{{:{1}<{{}}}}{1}{2}{1}{{:{1}<{{}}}}{1}{2}{1}{{:{1}<{{}}}}{1}{3}"; + const std::string topRow = fmt::format(fmt::runtime(rowFormat), options.theme.table.cornerTopLeft, options.theme.table.dash, options.theme.table.edgeTopT, options.theme.table.cornerTopRight); + const std::string middleRow = fmt::format(fmt::runtime(rowFormat), options.theme.table.edgeLeftT, options.theme.table.dash, options.theme.table.cross, options.theme.table.edgeRightT); + const std::string bottomRow = fmt::format(fmt::runtime(rowFormat), options.theme.table.cornerBottomLeft, options.theme.table.dash, options.theme.table.edgeBottomT, options.theme.table.cornerBottomRight); + }; +} + +#endif diff --git a/cucumber_cpp/library/formatter/helper/CMakeLists.txt b/cucumber_cpp/library/formatter/helper/CMakeLists.txt new file mode 100644 index 00000000..3db158fe --- /dev/null +++ b/cucumber_cpp/library/formatter/helper/CMakeLists.txt @@ -0,0 +1,30 @@ +add_library(cucumber_cpp.library.formatter.helper ${CCR_EXCLUDE_FROM_ALL}) + +target_sources(cucumber_cpp.library.formatter.helper PRIVATE + FormatMessages.cpp + FormatMessages.hpp + IndentString.cpp + IndentString.hpp + PrintMessages.cpp + PrintMessages.hpp + TextBuilder.cpp + TextBuilder.hpp + Theme.cpp + Theme.hpp +) + +target_include_directories(cucumber_cpp.library.formatter.helper PUBLIC + ../../../.. +) + +target_link_libraries(cucumber_cpp.library.formatter.helper PUBLIC + cucumber_cpp.library.cucumber_expression + cucumber_cpp.library.support + cucumber_cpp.library.query + fmt::fmt +) + +if (CCR_BUILD_TESTS) + add_subdirectory(test) + # add_subdirectory(test_helper) +endif() diff --git a/cucumber_cpp/library/formatter/helper/FormatMessages.cpp b/cucumber_cpp/library/formatter/helper/FormatMessages.cpp new file mode 100644 index 00000000..ec91cb78 --- /dev/null +++ b/cucumber_cpp/library/formatter/helper/FormatMessages.cpp @@ -0,0 +1,374 @@ +#include "cucumber_cpp/library/formatter/helper/FormatMessages.hpp" +#include "cucumber/messages/attachment.hpp" +#include "cucumber/messages/attachment_content_encoding.hpp" +#include "cucumber/messages/feature.hpp" +#include "cucumber/messages/hook.hpp" +#include "cucumber/messages/location.hpp" +#include "cucumber/messages/pickle.hpp" +#include "cucumber/messages/pickle_doc_string.hpp" +#include "cucumber/messages/pickle_step.hpp" +#include "cucumber/messages/pickle_table.hpp" +#include "cucumber/messages/rule.hpp" +#include "cucumber/messages/scenario.hpp" +#include "cucumber/messages/source_reference.hpp" +#include "cucumber/messages/step.hpp" +#include "cucumber/messages/step_definition.hpp" +#include "cucumber/messages/test_run_finished.hpp" +#include "cucumber/messages/test_step.hpp" +#include "cucumber/messages/test_step_result.hpp" +#include "cucumber/messages/test_step_result_status.hpp" +#include "cucumber_cpp/library/formatter/helper/TextBuilder.hpp" +#include "cucumber_cpp/library/formatter/helper/Theme.hpp" +#include "cucumber_cpp/library/util/Trim.hpp" +#include "fmt/color.h" +#include "fmt/format.h" +#include "fmt/ranges.h" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace cucumber_cpp::library::formatter::helper +{ + namespace + { + std::vector CalculateColumnWidths(const cucumber::messages::pickle_table& pickleDataTable) + { + std::vector columnWidths(pickleDataTable.rows.empty() ? 0 : pickleDataTable.rows.front().cells.size(), 0); + + for (const auto& row : pickleDataTable.rows) + for (std::size_t colIndex = 0; colIndex < row.cells.size(); ++colIndex) + { + const auto cellContentLength = row.cells[colIndex].value.length(); + columnWidths[colIndex] = std::max(columnWidths[colIndex], cellContentLength); + } + + return columnWidths; + } + + const auto transformToString = [](auto subrange) + { + return std::string_view{ subrange.begin(), subrange.end() }; + }; + + std::string GetAttemptText(std::size_t attempt, bool willBeRetried) + { + if (attempt > 0 || willBeRetried) + return fmt::format("(attempt {}{})", attempt + 1, willBeRetried ? ", retried" : ""); + return ""; + } + } + + std::string FormatPickleTitle(const cucumber::messages::pickle& pickle, const cucumber::messages::scenario& scenario, const Theme& theme) + { + return TextBuilder{} + .Append(scenario.keyword + ":", theme.scenario.keyword) + .Space() + .Append(pickle.name, theme.scenario.name) + .Build(theme.scenario.all); + } + + std::string FormatPickleAttemptTitle(const cucumber::messages::pickle& pickle, std::size_t attempt, bool retry, const cucumber::messages::scenario& scenario, const Theme& theme) + { + auto attemptText = GetAttemptText(attempt, retry); + + TextBuilder builder{}; + builder.Append(scenario.keyword + ":", theme.scenario.keyword) + .Space() + .Append(pickle.name, theme.scenario.name); + + if (!attemptText.empty()) + builder.Space().Append(attemptText, theme.scenario.attempt); + + return builder.Build(theme.scenario.all); + } + + std::string FormatPickleLocation(const cucumber::messages::pickle& pickle, const std::optional& location, const Theme& theme) + { + TextBuilder builder{}; + + builder.Append("#") + .Space() + .Append(pickle.uri); + if (location.has_value()) + builder.Append(":") + .Append(std::to_string(location.value().line)); + + return builder.Build(theme.location); + } + + std::string FormatStepText(const cucumber::messages::test_step& testStep, const cucumber::messages::pickle_step& pickleStep, cucumber::messages::test_step_result_status status, const Theme& theme) + { + TextBuilder builder{}; + const auto& stepMatchArgumentsLists = testStep.step_match_arguments_lists; + + if (stepMatchArgumentsLists && stepMatchArgumentsLists->size() == 1) + { + const auto& stepMatchArguments = stepMatchArgumentsLists->front().step_match_arguments; + std::size_t currentIndex = 0; + + for (const auto& argument : stepMatchArguments) + { + const auto& group = argument.group; + + if (group.value.has_value() && group.start.has_value()) + { + const auto text = pickleStep.text.substr(currentIndex, group.start.value() - currentIndex); + currentIndex = group.start.value() + group.value->size(); + builder.Append(text, theme.step.text.value_or(fmt::text_style{}) | theme.status.All(status)) + .Append(group.value.value(), theme.step.argument.value_or(fmt::text_style{}) | theme.status.All(status)); + } + } + if (currentIndex != pickleStep.text.size()) + { + const auto remainingText = pickleStep.text.substr(currentIndex); + builder.Append(remainingText, theme.step.text.value_or(fmt::text_style{}) | theme.status.All(status)); + } + } + else + builder.Append(pickleStep.text, theme.step.text.value_or(fmt::text_style{}) | theme.status.All(status)); + + return builder.Build(); + } + + std::string FormatCodeLocation(const cucumber::messages::source_reference& sourceReference, const Theme& theme) + { + if (sourceReference.uri.has_value()) + { + TextBuilder builder{}; + + builder.Append("#") + .Space() + .Append(sourceReference.uri.value()); + + if (sourceReference.location.has_value()) + builder.Append(":") + .Append(std::to_string(sourceReference.location.value().line)); + return builder.Build(theme.location); + } + + return ""; + } + + std::string FormatCodeLocation(const cucumber::messages::step_definition* stepDefinition, const Theme& theme) + { + if (stepDefinition != nullptr) + return FormatCodeLocation(stepDefinition->source_reference, theme); + + return ""; + } + + std::string FormatFeatureTitle(const cucumber::messages::feature& feature, const Theme& theme) + { + return TextBuilder{} + .Append(feature.keyword + ":", theme.feature.keyword) + .Space() + .Append(feature.name, theme.feature.name) + .Build(theme.feature.all); + } + + std::string FormatRuleTitle(const cucumber::messages::rule& rule, const Theme& theme) + { + return TextBuilder{} + .Append(rule.keyword + ":", theme.rule.keyword) + .Space() + .Append(rule.name, theme.rule.name) + .Build(theme.rule.all); + } + + std::string FormatPickleTags(const cucumber::messages::pickle& pickle, const Theme& theme) + { + if (!pickle.tags.empty()) + { + return TextBuilder{} + .Append(fmt::to_string(fmt::join(pickle.tags | std::views::transform([](const auto& tag) + { + return tag.name; + }), + " "))) + .Build(theme.tag); + } + return ""; + } + + std::string FormatHookTitle(const cucumber::messages::hook& hook, cucumber::messages::test_step_result_status status, bool isBeforeHook, bool useStatusIcon, const Theme& theme) + { + TextBuilder builder{}; + + if (useStatusIcon) + builder.Append(theme.status.Icon(status, " "), theme.status.All(status)).Space(); + + builder.Append((isBeforeHook ? "Before" : "After"), theme.step.keyword.value_or(fmt::text_style{}) | theme.status.All(status)); + + if (hook.name.has_value()) + builder.Space().Append('(' + hook.name.value() + ')', theme.status.All(status)); + + return builder.Build(theme.status.All(status)); + } + + std::string FormatStepTitle(const cucumber::messages::test_step& testStep, const cucumber::messages::pickle_step& pickleStep, const cucumber::messages::step& step, cucumber::messages::test_step_result_status status, bool useStatusIcon, const Theme& theme) + { + auto builder = TextBuilder{}; + if (useStatusIcon) + builder.Append(theme.status.Icon(status, " "), theme.status.All(status)).Space(); + + return builder.Append(TextBuilder{} + .Append(step.keyword, theme.step.keyword) + .Append(FormatStepText(testStep, pickleStep, status, theme), theme.status.All(status)) + .Build(theme.status.All(status))) + .Build(); + } + + std::string FormatDocString(const cucumber::messages::pickle_doc_string& pickleDocString, const Theme& theme) + { + TextBuilder builder{}; + builder.Append(R"(""")", theme.docString.delimiter); + if (pickleDocString.media_type.has_value()) + builder.Append(pickleDocString.media_type.value(), theme.docString.mediaType); + builder.Line(); + + for (const auto& line : pickleDocString.content | std::views::split('\n') | std::views::transform(transformToString)) + builder.Append(line).Line(); + + builder.Append(R"(""")", theme.docString.delimiter); + + return builder.Build(theme.docString.all, true); + } + + std::string FormatDataTable(const cucumber::messages::pickle_table& pickleDataTable, const Theme& theme) + { + const auto columnWidths = CalculateColumnWidths(pickleDataTable); + TextBuilder builder{}; + + for (auto rowIndex = 0; rowIndex != pickleDataTable.rows.size(); ++rowIndex) + { + const auto& row = pickleDataTable.rows[rowIndex]; + + if (rowIndex > 0) + builder.Line(); + builder.Append("|", theme.dataTable.border); + + for (auto colIndex = 0; colIndex != pickleDataTable.rows[rowIndex].cells.size(); ++colIndex) + { + const auto& cell = row.cells[colIndex]; + builder.Append(fmt::format(" {:<{}} ", cell.value, columnWidths[colIndex]), theme.dataTable.content) + .Append("|", theme.dataTable.border); + } + } + + return builder.Build(theme.dataTable.all, true); + } + + std::string FormatPickleStepArgument(const cucumber::messages::pickle_step& pickleStep, const Theme& theme) + { + if (pickleStep.argument && pickleStep.argument->doc_string.has_value()) + return FormatDocString(pickleStep.argument->doc_string.value(), theme); + + if (pickleStep.argument && pickleStep.argument->data_table.has_value()) + return FormatDataTable(pickleStep.argument->data_table.value(), theme); + + return ""; + } + + std::string FormatAmbiguousStep(const std::list& stepDefinitions, const Theme& theme) + { + TextBuilder builder{}; + builder.Append("Multiple matching step definitions found:"); + for (const auto* stepDefinition : stepDefinitions) + { + builder.Line().Append(" " + theme.symbol.bullet + " "); + + if (!stepDefinition->pattern.source.empty()) + builder.Append(stepDefinition->pattern.source); + + const auto location = FormatCodeLocation(stepDefinition, theme); + if (!location.empty()) + builder.Space().Append(location); + } + return builder.Build({}, true); + } + + std::string FormatTestStepResultError(const cucumber::messages::test_step_result& testStepResult, const Theme& theme) + { + if (testStepResult.exception.has_value() && testStepResult.exception.value().stack_trace.has_value()) + { + return TextBuilder{} + .Append(util::Trim(testStepResult.exception.value().stack_trace.value())) + .Build(theme.status.All(testStepResult.status), true); + } + + if (testStepResult.exception.has_value() && testStepResult.exception.value().message.has_value()) + { + return TextBuilder{} + .Append(util::Trim(testStepResult.exception.value().type)) + .Append(": ") + .Append(util::Trim(testStepResult.exception.value().message.value())) + .Build(theme.status.All(testStepResult.status), true); + } + + if (testStepResult.message.has_value()) + { + return TextBuilder{} + .Append(util::Trim(testStepResult.message.value())) + .Build(theme.status.All(testStepResult.status), true); + } + + return ""; + } + + std::string FormatTestRunFinishedError(const cucumber::messages::test_run_finished& testRunFinished, const Theme& theme) + { + if (testRunFinished.exception && testRunFinished.exception->stack_trace) + { + return TextBuilder{} + .Append(util::Trim(testRunFinished.exception->stack_trace.value())) + .Build(theme.status.All(cucumber::messages::test_step_result_status::FAILED)); + } + + if (testRunFinished.exception && testRunFinished.exception->message) + { + return TextBuilder{} + .Append(util::Trim(testRunFinished.exception->message.value())) + .Build(theme.status.All(cucumber::messages::test_step_result_status::FAILED)); + } + + return ""; + } + + std::string FormatBase64Attachment(const std::string& body, const std::string& mediaType, const std::optional& filename, const Theme& theme) + { + TextBuilder builder{}; + builder.Append("Embedding").Space(); + + if (filename) + builder.Append(filename.value()).Space(); + + builder + .Append("[") + .Append(mediaType) + .Space() + .Append(std::to_string(body.length() / 4 * 3)) + .Space() + .Append("bytes]"); + + return builder.Build(theme.attachment); + } + + std::string FormatTextAttachment(const std::string& body, const Theme& theme) + { + return TextBuilder{}.Append(body).Build(theme.attachment); + } + + std::string FormatAttachment(const cucumber::messages::attachment& attachment, const Theme& theme) + { + if (attachment.content_encoding == cucumber::messages::attachment_content_encoding::BASE64) + return FormatBase64Attachment(attachment.body, attachment.media_type, attachment.file_name, theme); + else + return FormatTextAttachment(attachment.body, theme); + return ""; + } +} diff --git a/cucumber_cpp/library/formatter/helper/FormatMessages.hpp b/cucumber_cpp/library/formatter/helper/FormatMessages.hpp new file mode 100644 index 00000000..8775a2fa --- /dev/null +++ b/cucumber_cpp/library/formatter/helper/FormatMessages.hpp @@ -0,0 +1,51 @@ +#ifndef HELPER_FORMAT_MESSAGES_HPP +#define HELPER_FORMAT_MESSAGES_HPP + +#include "cucumber/messages/attachment.hpp" +#include "cucumber/messages/feature.hpp" +#include "cucumber/messages/hook.hpp" +#include "cucumber/messages/location.hpp" +#include "cucumber/messages/pickle.hpp" +#include "cucumber/messages/pickle_doc_string.hpp" +#include "cucumber/messages/pickle_step.hpp" +#include "cucumber/messages/pickle_table.hpp" +#include "cucumber/messages/rule.hpp" +#include "cucumber/messages/scenario.hpp" +#include "cucumber/messages/source_reference.hpp" +#include "cucumber/messages/step.hpp" +#include "cucumber/messages/step_definition.hpp" +#include "cucumber/messages/test_run_finished.hpp" +#include "cucumber/messages/test_step.hpp" +#include "cucumber/messages/test_step_result.hpp" +#include "cucumber/messages/test_step_result_status.hpp" +#include "cucumber_cpp/library/formatter/helper/Theme.hpp" +#include +#include +#include +#include + +namespace cucumber_cpp::library::formatter::helper +{ + std::string FormatPickleTitle(const cucumber::messages::pickle& pickle, const cucumber::messages::scenario& scenario, const Theme& theme); + std::string FormatPickleAttemptTitle(const cucumber::messages::pickle& pickle, std::size_t attempt, bool retry, const cucumber::messages::scenario& scenario, const Theme& theme); + std::string FormatPickleLocation(const cucumber::messages::pickle& pickle, const std::optional& location, const Theme& theme); + std::string FormatStepText(const cucumber::messages::test_step& testStep, const cucumber::messages::pickle_step& pickleStep, cucumber::messages::test_step_result_status status, const Theme& theme); + std::string FormatCodeLocation(const cucumber::messages::source_reference& sourceReference, const Theme& theme); + std::string FormatCodeLocation(const cucumber::messages::step_definition* stepDefinition, const Theme& theme); + std::string FormatFeatureTitle(const cucumber::messages::feature& feature, const Theme& theme); + std::string FormatRuleTitle(const cucumber::messages::rule& rule, const Theme& theme); + std::string FormatPickleTags(const cucumber::messages::pickle& pickle, const Theme& theme); + std::string FormatHookTitle(const cucumber::messages::hook& hook, cucumber::messages::test_step_result_status status, bool isBeforeHook, bool useStatusIcon, const Theme& theme); + std::string FormatStepTitle(const cucumber::messages::test_step& testStep, const cucumber::messages::pickle_step& pickleStep, const cucumber::messages::step& step, cucumber::messages::test_step_result_status status, bool useStatusIcon, const Theme& theme); + std::string FormatDocString(const cucumber::messages::pickle_doc_string& pickleDocString, const Theme& theme); + std::string FormatDataTable(const cucumber::messages::pickle_table& pickleDataTable, const Theme& theme); + std::string FormatPickleStepArgument(const cucumber::messages::pickle_step& pickleStep, const Theme& theme); + std::string FormatAmbiguousStep(const std::list& stepDefinitions, const Theme& theme); + std::string FormatTestStepResultError(const cucumber::messages::test_step_result& testStepResult, const Theme& theme); + std::string FormatTestRunFinishedError(const cucumber::messages::test_run_finished& testRunFinished, const Theme& theme); + std::string FormatBase64Attachment(const std::string& body, const std::string& mediaType, const std::optional& filename, const Theme& theme); + std::string FormatTextAttachment(const std::string& body, const Theme& theme); + std::string FormatAttachment(const cucumber::messages::attachment& attachment, const Theme& theme); +} + +#endif diff --git a/cucumber_cpp/library/formatter/helper/IndentString.cpp b/cucumber_cpp/library/formatter/helper/IndentString.cpp new file mode 100644 index 00000000..f1f48221 --- /dev/null +++ b/cucumber_cpp/library/formatter/helper/IndentString.cpp @@ -0,0 +1,21 @@ +#include "cucumber_cpp/library/formatter/helper/IndentString.hpp" +#include "fmt/format.h" +#include "fmt/ranges.h" +#include +#include +#include + +namespace cucumber_cpp::library::formatter::helper +{ + std::string IndentString(const std::string& str, std::size_t indentSize) + { + auto lines = str | + std::views::split('\n') | + std::views::transform([indent = std::string(indentSize, ' ')](const auto& line) + { + return indent + std::string{ line.begin(), line.end() }; + }); + + return fmt::to_string(fmt::join(lines, "\n")); + } +} diff --git a/cucumber_cpp/library/formatter/helper/IndentString.hpp b/cucumber_cpp/library/formatter/helper/IndentString.hpp new file mode 100644 index 00000000..3ae582c9 --- /dev/null +++ b/cucumber_cpp/library/formatter/helper/IndentString.hpp @@ -0,0 +1,12 @@ +#ifndef HELPER_INDENT_STRING_HPP +#define HELPER_INDENT_STRING_HPP + +#include +#include + +namespace cucumber_cpp::library::formatter::helper +{ + std::string IndentString(const std::string& str, std::size_t indentSize); +} + +#endif diff --git a/cucumber_cpp/library/formatter/helper/PrintMessages.cpp b/cucumber_cpp/library/formatter/helper/PrintMessages.cpp new file mode 100644 index 00000000..fba75dcf --- /dev/null +++ b/cucumber_cpp/library/formatter/helper/PrintMessages.cpp @@ -0,0 +1,146 @@ +#include "cucumber_cpp/library/formatter/helper/PrintMessages.hpp" +#include "cucumber/messages/attachment.hpp" +#include "cucumber/messages/feature.hpp" +#include "cucumber/messages/hook.hpp" +#include "cucumber/messages/pickle.hpp" +#include "cucumber/messages/pickle_step.hpp" +#include "cucumber/messages/rule.hpp" +#include "cucumber/messages/scenario.hpp" +#include "cucumber/messages/step.hpp" +#include "cucumber/messages/step_definition.hpp" +#include "cucumber/messages/test_step.hpp" +#include "cucumber/messages/test_step_finished.hpp" +#include "cucumber/messages/test_step_result_status.hpp" +#include "cucumber_cpp/library/formatter/helper/FormatMessages.hpp" +#include "cucumber_cpp/library/formatter/helper/Theme.hpp" +#include "cucumber_cpp/library/query/Query.hpp" +#include "fmt/format.h" +#include "fmt/ostream.h" +#include "fmt/ranges.h" +#include +#include +#include +#include +#include +#include +#include + +namespace cucumber_cpp::library::formatter::helper +{ + namespace + { + const auto transformToString = [](auto subrange) + { + return std::string_view{ subrange.begin(), subrange.end() }; + }; + + void PrintlnIndentedContent(std::ostream& os, std::string_view content, std::size_t indent) + { + const std::string indentStr(indent, ' '); + fmt::println(os, "{}{}", indentStr, fmt::join(content | std::views::split('\n') | std::views::transform(transformToString), "\n" + indentStr)); + } + } + + void PrintFeatureLine(std::ostream& stream, const cucumber::messages::feature& feature, const Theme& theme) + { + fmt::println(stream, "\n{}", FormatFeatureTitle(feature, theme)); + } + + void PrintRuleLine(std::ostream& stream, const cucumber::messages::rule& rule, const Theme& theme) + { + fmt::println(stream, "\n{}{}", std::string(gherkinIndentLength, ' '), FormatRuleTitle(rule, theme)); + } + + void PrintTags(std::ostream& stream, const cucumber::messages::pickle& pickle, std::size_t scenarioIndent, const Theme& theme) + { + if (pickle.tags.empty()) + return; + + fmt::println(stream, "{}{}", std::string(scenarioIndent, ' '), FormatPickleTags(pickle, theme)); + } + + void PrintScenarioLine(std::ostream& stream, const cucumber::messages::pickle& pickle, const cucumber::messages::scenario& scenario, std::size_t scenarioIndent, std::size_t maxContentLength, const Theme& theme) + { + PrintGherkinLine(stream, + FormatPickleTitle(pickle, scenario, theme), + FormatPickleLocation(pickle, scenario.location, theme), + scenarioIndent, + maxContentLength, theme); + } + + void PrintScenarioAttemptLine(std::ostream& stream, const cucumber::messages::pickle& pickle, std::size_t attempt, bool retry, const cucumber::messages::scenario& scenario, std::size_t scenarioIndent, std::size_t maxContentLength, const Theme& theme) + { + PrintGherkinLine(stream, + FormatPickleAttemptTitle(pickle, attempt, retry, scenario, theme), + FormatPickleLocation(pickle, scenario.location, theme), + scenarioIndent, + maxContentLength, theme); + } + + void PrintHookLine(std::ostream& stream, const cucumber::messages::test_step_finished& testStepFinished, const cucumber::messages::hook& hook, std::size_t scenarioIndent, std::size_t maxContentLength, bool isBeforeHook, bool useStatusIcon, const Theme& theme) + { + PrintGherkinLine(stream, + helper::FormatHookTitle(hook, testStepFinished.test_step_result.status, isBeforeHook, useStatusIcon, theme), + helper::FormatCodeLocation(hook.source_reference, theme), + scenarioIndent + 2, maxContentLength, theme); + } + + void PrintStepLine(std::ostream& stream, const cucumber::messages::test_step_finished& testStepFinished, const cucumber::messages::test_step& testStep, const cucumber::messages::pickle_step& pickleStep, const cucumber::messages::step& step, const cucumber::messages::step_definition* stepDefinition, std::size_t scenarioIndent, std::size_t maxContentLength, bool useStatusIcon, const Theme& theme) + { + PrintGherkinLine(stream, + helper::FormatStepTitle(testStep, pickleStep, step, testStepFinished.test_step_result.status, useStatusIcon, theme), + helper::FormatCodeLocation(stepDefinition, theme), + scenarioIndent + 2, maxContentLength, theme); + } + + void PrintStepArgument(std::ostream& stream, const cucumber::messages::pickle_step& pickleStep, std::size_t scenarioIndent, bool useStatusIcon, const helper::Theme& theme) + { + const auto content = FormatPickleStepArgument(pickleStep, theme); + if (content.empty()) + return; + + PrintlnIndentedContent(stream, content, scenarioIndent + gherkinIndentLength + stepArgumentIndentLength + (useStatusIcon ? gherkinIndentLength : 0)); + } + + void PrintAmbiguousStep(std::ostream& stream, const query::Query& query, const cucumber::messages::test_step_finished& testStepFinished, const cucumber::messages::test_step& testStep, std::size_t scenarioIndent, bool useStatusIcon, const Theme& theme) + { + if (testStepFinished.test_step_result.status != cucumber::messages::test_step_result_status::AMBIGUOUS) + return; + + const auto list = query.FindStepDefinitionsById(testStep); + const auto content = FormatAmbiguousStep(list, theme); + + if (content.empty()) + return; + + PrintlnIndentedContent(stream, content, scenarioIndent + gherkinIndentLength + errorIndentLength + (useStatusIcon ? gherkinIndentLength : 0)); + } + + void PrintError(std::ostream& stream, const cucumber::messages::test_step_finished& testStepFinished, std::size_t scenarioIndent, bool useStatusIcon, const Theme& theme) + { + const auto content = FormatTestStepResultError(testStepFinished.test_step_result, theme); + if (content.empty()) + return; + + PrintlnIndentedContent(stream, content, scenarioIndent + gherkinIndentLength + errorIndentLength + (useStatusIcon ? gherkinIndentLength : 0)); + } + + void PrintGherkinLine(std::ostream& stream, const std::string& title, const std::optional& location, std::size_t indent, std::size_t maxContentLength, const Theme& theme) + { + const auto unstyledLength = helper::Unstyled(title).length(); + const auto padding = location.has_value() ? (maxContentLength - std::min(unstyledLength, maxContentLength)) + 1 : 0; + + const std::string paddingStr(padding, ' '); + const auto content = fmt::format("{}{}{}", title, paddingStr, location.value_or("")); + + PrintlnIndentedContent(stream, content, indent); + } + + void PrintAttachment(std::ostream& stream, const cucumber::messages::attachment& attachment, std::size_t scenarioIndent, bool useStatusIcon, const Theme& theme) + { + const auto content = FormatAttachment(attachment, theme); + const std::string indentStr(scenarioIndent + helper::gherkinIndentLength + helper::attachmentIndentLength + (useStatusIcon ? helper::gherkinIndentLength : 0), ' '); + + fmt::println(stream, "\n{}{}\n", indentStr, content); + } +} diff --git a/cucumber_cpp/library/formatter/helper/PrintMessages.hpp b/cucumber_cpp/library/formatter/helper/PrintMessages.hpp new file mode 100644 index 00000000..5598d645 --- /dev/null +++ b/cucumber_cpp/library/formatter/helper/PrintMessages.hpp @@ -0,0 +1,44 @@ +#ifndef HELPER_PRINT_MESSAGES_HPP +#define HELPER_PRINT_MESSAGES_HPP + +#include "cucumber/messages/attachment.hpp" +#include "cucumber/messages/feature.hpp" +#include "cucumber/messages/hook.hpp" +#include "cucumber/messages/pickle.hpp" +#include "cucumber/messages/pickle_step.hpp" +#include "cucumber/messages/rule.hpp" +#include "cucumber/messages/scenario.hpp" +#include "cucumber/messages/step.hpp" +#include "cucumber/messages/step_definition.hpp" +#include "cucumber/messages/test_step.hpp" +#include "cucumber/messages/test_step_finished.hpp" +#include "cucumber_cpp/library/formatter/helper/Theme.hpp" +#include "cucumber_cpp/library/query/Query.hpp" +#include +#include +#include +#include + +namespace cucumber_cpp::library::formatter::helper +{ + + static constexpr auto gherkinIndentLength = 2; + static constexpr auto stepArgumentIndentLength = 2; + static constexpr auto attachmentIndentLength = 4; + static constexpr auto errorIndentLength = 4; + + void PrintFeatureLine(std::ostream& stream, const cucumber::messages::feature& feature, const Theme& theme); + void PrintRuleLine(std::ostream& stream, const cucumber::messages::rule& rule, const Theme& theme); + void PrintTags(std::ostream& stream, const cucumber::messages::pickle& pickle, std::size_t scenarioIndent, const Theme& theme); + void PrintScenarioLine(std::ostream& stream, const cucumber::messages::pickle& pickle, const cucumber::messages::scenario& scenario, std::size_t scenarioIndent, std::size_t maxContentLength, const Theme& theme); + void PrintScenarioAttemptLine(std::ostream& stream, const cucumber::messages::pickle& pickle, std::size_t attempt, bool retry, const cucumber::messages::scenario& scenario, std::size_t scenarioIndent, std::size_t maxContentLength, const Theme& theme); + void PrintHookLine(std::ostream& stream, const cucumber::messages::test_step_finished& testStepFinished, const cucumber::messages::hook& hook, std::size_t scenarioIndent, std::size_t maxContentLength, bool isBeforeHook, bool useStatusIcon, const Theme& theme); + void PrintStepLine(std::ostream& stream, const cucumber::messages::test_step_finished& testStepFinished, const cucumber::messages::test_step& testStep, const cucumber::messages::pickle_step& pickleStep, const cucumber::messages::step& step, const cucumber::messages::step_definition* stepDefinition, std::size_t scenarioIndent, std::size_t maxContentLength, bool useStatusIcon, const Theme& theme); + void PrintStepArgument(std::ostream& stream, const cucumber::messages::pickle_step& pickleStep, std::size_t scenarioIndent, bool useStatusIcon, const helper::Theme& theme); + void PrintAmbiguousStep(std::ostream& stream, const query::Query& query, const cucumber::messages::test_step_finished& testStepFinished, const cucumber::messages::test_step& testStep, std::size_t scenarioIndent, bool useStatusIcon, const Theme& theme); + void PrintError(std::ostream& stream, const cucumber::messages::test_step_finished& testStepFinished, std::size_t scenarioIndent, bool useStatusIcon, const Theme& theme); + void PrintGherkinLine(std::ostream& stream, const std::string& title, const std::optional& location, std::size_t indent, std::size_t maxContentLength, const Theme& theme); + void PrintAttachment(std::ostream& stream, const cucumber::messages::attachment& attachment, std::size_t scenarioIndent, bool useStatusIcon, const Theme& theme); +} + +#endif diff --git a/cucumber_cpp/library/formatter/helper/TextBuilder.cpp b/cucumber_cpp/library/formatter/helper/TextBuilder.cpp new file mode 100644 index 00000000..1ea51551 --- /dev/null +++ b/cucumber_cpp/library/formatter/helper/TextBuilder.cpp @@ -0,0 +1,58 @@ + +#include "cucumber_cpp/library/formatter/helper/TextBuilder.hpp" +#include "fmt/color.h" +#include "fmt/format.h" +#include "fmt/ranges.h" +#include +#include +#include +#include + +namespace cucumber_cpp::library::formatter::helper +{ + namespace + { + std::string ApplyStyle(std::string_view text, std::optional style) + { + if (!style) + return std::string{ text }; + + return fmt::format(*style, "{}", text); + } + } + + TextBuilder& TextBuilder::Space() + { + text += ' '; + return *this; + } + + TextBuilder& TextBuilder::Line() + { + text += '\n'; + return *this; + } + + TextBuilder& TextBuilder::Append(std::string_view text, std::optional style) + { + this->text += ApplyStyle(text, style); + return *this; + } + + std::string TextBuilder::Build(std::optional style, bool styleEachLine) const + { + if (styleEachLine) + { + auto styled = text | + std::views::split('\n') | + std::views::transform([&style](const auto& line) + { + return ApplyStyle(std::string{ line.begin(), line.end() }, style); + }); + + return fmt::to_string(fmt::join(styled, "\n")); + } + + return ApplyStyle(text, style); + } +} diff --git a/cucumber_cpp/library/formatter/helper/TextBuilder.hpp b/cucumber_cpp/library/formatter/helper/TextBuilder.hpp new file mode 100644 index 00000000..fd746f8c --- /dev/null +++ b/cucumber_cpp/library/formatter/helper/TextBuilder.hpp @@ -0,0 +1,23 @@ +#ifndef HELPER_TEXT_BUILDER_HPP +#define HELPER_TEXT_BUILDER_HPP + +#include "fmt/color.h" +#include +#include +#include + +namespace cucumber_cpp::library::formatter::helper +{ + struct TextBuilder + { + TextBuilder& Space(); + TextBuilder& Line(); + TextBuilder& Append(std::string_view text, std::optional style = std::nullopt); + std::string Build(std::optional style = std::nullopt, bool styleEachLine = false) const; + + private: + std::string text; + }; +} + +#endif diff --git a/cucumber_cpp/library/formatter/helper/Theme.cpp b/cucumber_cpp/library/formatter/helper/Theme.cpp new file mode 100644 index 00000000..0021d621 --- /dev/null +++ b/cucumber_cpp/library/formatter/helper/Theme.cpp @@ -0,0 +1,130 @@ +#include "cucumber_cpp/library/formatter/helper/Theme.hpp" +#include "cucumber/messages/test_step_result_status.hpp" +#include "fmt/color.h" +#include +#include +#include +#include +#include +#include + +namespace cucumber_cpp::library::formatter::helper +{ + namespace + { + const std::map> statusColors{ + { cucumber::messages::test_step_result_status::AMBIGUOUS, fmt::fg(fmt::color::red) }, + { cucumber::messages::test_step_result_status::FAILED, fmt::fg(fmt::color::red) }, + { cucumber::messages::test_step_result_status::PASSED, fmt::fg(fmt::color::green) }, + { cucumber::messages::test_step_result_status::PENDING, fmt::fg(fmt::color::yellow) }, + { cucumber::messages::test_step_result_status::SKIPPED, fmt::fg(fmt::color::cyan) }, + { cucumber::messages::test_step_result_status::UNDEFINED, fmt::fg(fmt::color::yellow) }, + { cucumber::messages::test_step_result_status::UNKNOWN, fmt::fg(fmt::color::gray) }, + }; + + const std::map> iconMap{ + { cucumber::messages::test_step_result_status::AMBIGUOUS, "✘" }, + { cucumber::messages::test_step_result_status::FAILED, "✘" }, + { cucumber::messages::test_step_result_status::PASSED, "✔" }, + { cucumber::messages::test_step_result_status::PENDING, "■" }, + { cucumber::messages::test_step_result_status::SKIPPED, "↷" }, + { cucumber::messages::test_step_result_status::UNDEFINED, "■" }, + { cucumber::messages::test_step_result_status::UNKNOWN, " " }, + }; + + const std::map> progressIcons{ + { cucumber::messages::test_step_result_status::AMBIGUOUS, "A" }, + { cucumber::messages::test_step_result_status::FAILED, "F" }, + { cucumber::messages::test_step_result_status::PASSED, "." }, + { cucumber::messages::test_step_result_status::PENDING, "P" }, + { cucumber::messages::test_step_result_status::SKIPPED, "-" }, + { cucumber::messages::test_step_result_status::UNDEFINED, "U" }, + { cucumber::messages::test_step_result_status::UNKNOWN, "?" }, + }; + + std::optional GetColorStyle(std::optional def) + { + return def; + } + + const std::regex ansiEscape{ "\033\\[[^m]+m" }; + } + + Theme CreateEmptyTheme() + { + return {}; + } + + Theme CreateCucumberTheme() + { + static const Theme theme{ + .attachment = fmt::fg(fmt::color::cyan), + .feature = { + .keyword = fmt::emphasis::bold, + }, + .location = fmt::fg(fmt::terminal_color::bright_black), + .rule = { + .keyword = fmt::emphasis::bold, + }, + .scenario = { + .keyword = fmt::emphasis::bold, + .attempt = fmt::emphasis::italic, + }, + .status = { + .all = statusColors, + .icon = iconMap, + .progress = progressIcons, + }, + .step = { + .argument = fmt::emphasis::bold, + .keyword = fmt::emphasis::bold, + }, + .symbol = { .bullet = "•" }, + .table{ + .cornerTopLeft{ "┌" }, + .cornerTopRight{ "┐" }, + .cornerBottomLeft{ "└" }, + .cornerBottomRight{ "┘" }, + .edgeTopT{ "┬" }, + .edgeBottomT{ "┴" }, + .edgeLeftT{ "├" }, + .edgeRightT{ "┤" }, + .dash{ "─" }, + .vertical{ "│" }, + .cross{ "┼" }, + }, + }; + + return theme; + } + + Theme CreatePlainTheme() + { + static const Theme theme{ + .status = { + .icon = iconMap, + .progress = progressIcons, + }, + .symbol = { .bullet = "-" }, + }; + + return theme; + } + + Theme CreateTheme(std::string_view name) + { + if (name == "cucumber") + return CreateCucumberTheme(); + else if (name == "plain") + return CreatePlainTheme(); + else if (name == "none") + return CreateEmptyTheme(); + else + return CreateEmptyTheme(); + } + + std::string Unstyled(const std::string& str) + { + return std::regex_replace(str, ansiEscape, ""); + } +} diff --git a/cucumber_cpp/library/formatter/helper/Theme.hpp b/cucumber_cpp/library/formatter/helper/Theme.hpp new file mode 100644 index 00000000..43bdd84b --- /dev/null +++ b/cucumber_cpp/library/formatter/helper/Theme.hpp @@ -0,0 +1,127 @@ +#ifndef HELPER_THEME_HPP +#define HELPER_THEME_HPP + +#include "cucumber/messages/test_step_result_status.hpp" +#include "fmt/color.h" +#include +#include +#include +#include +#include + +namespace cucumber_cpp::library::formatter::helper +{ + struct Theme + { + std::optional attachment{}; + + struct + { + std::optional all{}; + std::optional border{}; + std::optional content{}; + } dataTable; + + struct + { + std::optional all{}; + std::optional content{}; + std::optional delimiter{}; + std::optional mediaType{}; + } docString; + + struct + { + std::optional all{}; + std::optional keyword{}; + std::optional name{}; + } feature; + + std::optional location{}; + + struct + { + std::optional all{}; + std::optional keyword{}; + std::optional name{}; + } rule; + + struct + { + std::optional all{}; + std::optional keyword{}; + std::optional name{}; + std::optional attempt{}; + } scenario; + + struct + { + std::optional>> all{}; + std::optional>> icon{}; + std::optional>> progress{}; + + fmt::text_style All(cucumber::messages::test_step_result_status status, fmt::text_style defaultStyle = {}) const + { + if (all && all->contains(status)) + return all->at(status); + return defaultStyle; + } + + std::string Icon(cucumber::messages::test_step_result_status status, std::string defaultIcon = {}) const + { + if (icon && icon->contains(status)) + return icon->at(status); + return defaultIcon; + } + + std::string Progress(cucumber::messages::test_step_result_status status, std::string defaultProgress = {}) const + { + if (progress && progress->contains(status)) + return progress->at(status); + return defaultProgress; + } + } status{}; + + struct + { + std::optional argument{}; + std::optional keyword{}; + std::optional text{}; + } step; + + std::optional tag{}; + + struct + { + std::string bullet{ " " }; + } symbol; + + struct + { + std::string cornerTopLeft{ "|" }; + std::string cornerTopRight{ "|" }; + std::string cornerBottomLeft{ "|" }; + std::string cornerBottomRight{ "|" }; + + std::string edgeTopT{ "|" }; + std::string edgeBottomT{ "|" }; + std::string edgeLeftT{ "|" }; + std::string edgeRightT{ "|" }; + + std::string dash{ "-" }; + std::string vertical{ "|" }; + + std::string cross{ "+" }; + } table; + }; + + Theme CreateEmptyTheme(); + Theme CreateCucumberTheme(); + Theme CreatePlainTheme(); + + Theme CreateTheme(std::string_view name); + + std::string Unstyled(const std::string& str); +} + +#endif diff --git a/cucumber_cpp/library/formatter/helper/test/CMakeLists.txt b/cucumber_cpp/library/formatter/helper/test/CMakeLists.txt new file mode 100644 index 00000000..7dfec4b3 --- /dev/null +++ b/cucumber_cpp/library/formatter/helper/test/CMakeLists.txt @@ -0,0 +1,12 @@ +add_executable(cucumber_cpp.library.formatter.helper.test) +add_test(NAME cucumber_cpp.library.formatter.helper.test COMMAND cucumber_cpp.library.formatter.helper.test) + +target_link_libraries(cucumber_cpp.library.formatter.helper.test PUBLIC + gmock_main + cucumber_cpp.library.formatter.helper + GTest::gmock +) + +target_sources(cucumber_cpp.library.formatter.helper.test PRIVATE + TestDummy.cpp +) diff --git a/cucumber_cpp/library/formatter/helper/test/TestDummy.cpp b/cucumber_cpp/library/formatter/helper/test/TestDummy.cpp new file mode 100644 index 00000000..c81abaf1 --- /dev/null +++ b/cucumber_cpp/library/formatter/helper/test/TestDummy.cpp @@ -0,0 +1,8 @@ +#include + +namespace cucumber_cpp::library::support +{ + TEST(Dummy, dummy) + { + } +} diff --git a/cucumber_cpp/library/formatter/test/CMakeLists.txt b/cucumber_cpp/library/formatter/test/CMakeLists.txt new file mode 100644 index 00000000..ca355d3a --- /dev/null +++ b/cucumber_cpp/library/formatter/test/CMakeLists.txt @@ -0,0 +1,12 @@ +add_executable(cucumber_cpp.library.formatter.test) +add_test(NAME cucumber_cpp.library.formatter.test COMMAND cucumber_cpp.library.formatter.test) + +target_link_libraries(cucumber_cpp.library.formatter.test PUBLIC + gmock_main + cucumber_cpp.library.formatter + GTest::gmock +) + +target_sources(cucumber_cpp.library.formatter.test PRIVATE + TestDummy.cpp +) diff --git a/cucumber_cpp/library/formatter/test/TestDummy.cpp b/cucumber_cpp/library/formatter/test/TestDummy.cpp new file mode 100644 index 00000000..c81abaf1 --- /dev/null +++ b/cucumber_cpp/library/formatter/test/TestDummy.cpp @@ -0,0 +1,8 @@ +#include + +namespace cucumber_cpp::library::support +{ + TEST(Dummy, dummy) + { + } +} diff --git a/cucumber_cpp/library/query/CMakeLists.txt b/cucumber_cpp/library/query/CMakeLists.txt new file mode 100644 index 00000000..8a0554c4 --- /dev/null +++ b/cucumber_cpp/library/query/CMakeLists.txt @@ -0,0 +1,20 @@ +add_library(cucumber_cpp.library.query ${CCR_EXCLUDE_FROM_ALL}) + +target_sources(cucumber_cpp.library.query PRIVATE + Query.cpp + Query.hpp +) + +target_include_directories(cucumber_cpp.library.query PUBLIC + ../../.. +) + +target_link_libraries(cucumber_cpp.library.query PUBLIC + cucumber_gherkin_lib + cucumber_cpp.library.util + fmt::fmt +) + +if (CCR_BUILD_TESTS) + add_subdirectory(test) +endif() diff --git a/cucumber_cpp/library/query/Query.cpp b/cucumber_cpp/library/query/Query.cpp new file mode 100644 index 00000000..fc568730 --- /dev/null +++ b/cucumber_cpp/library/query/Query.cpp @@ -0,0 +1,592 @@ +#include "cucumber_cpp/library/query/Query.hpp" +#include "cucumber/messages/attachment.hpp" +#include "cucumber/messages/background.hpp" +#include "cucumber/messages/duration.hpp" +#include "cucumber/messages/envelope.hpp" +#include "cucumber/messages/examples.hpp" +#include "cucumber/messages/feature.hpp" +#include "cucumber/messages/gherkin_document.hpp" +#include "cucumber/messages/hook.hpp" +#include "cucumber/messages/location.hpp" +#include "cucumber/messages/meta.hpp" +#include "cucumber/messages/parameter_type.hpp" +#include "cucumber/messages/pickle.hpp" +#include "cucumber/messages/pickle_step.hpp" +#include "cucumber/messages/rule.hpp" +#include "cucumber/messages/scenario.hpp" +#include "cucumber/messages/step.hpp" +#include "cucumber/messages/step_definition.hpp" +#include "cucumber/messages/suggestion.hpp" +#include "cucumber/messages/table_row.hpp" +#include "cucumber/messages/test_case.hpp" +#include "cucumber/messages/test_case_finished.hpp" +#include "cucumber/messages/test_case_started.hpp" +#include "cucumber/messages/test_run_finished.hpp" +#include "cucumber/messages/test_run_hook_finished.hpp" +#include "cucumber/messages/test_run_hook_started.hpp" +#include "cucumber/messages/test_run_started.hpp" +#include "cucumber/messages/test_step.hpp" +#include "cucumber/messages/test_step_finished.hpp" +#include "cucumber/messages/test_step_result.hpp" +#include "cucumber/messages/test_step_result_status.hpp" +#include "cucumber/messages/test_step_started.hpp" +#include "cucumber/messages/undefined_parameter_type.hpp" +#include "cucumber_cpp/library/util/Broadcaster.hpp" +#include "cucumber_cpp/library/util/Timestamp.hpp" +#include "fmt/format.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace cucumber_cpp::library::query +{ + namespace + { + struct SortByStatus + { + bool operator()(const std::pair& a, + const std::pair& b) const + { + const auto& statusA = a.first->test_step_result.status; + const auto& statusB = b.first->test_step_result.status; + return static_cast(statusA) < static_cast(statusB); + } + }; + } + + std::string Lineage::GetUniqueFeatureName() const + { + return fmt::format("{}/{}", feature->name, featureIndex); + } + + std::string Lineage::GetScenarioAndOrRuleName() const + { + if (rule) + return fmt::format("{}/{}", rule->name, scenario->name); + return scenario->name; + } + + std::string NamingStrategy::Reduce(const Lineage& lineage, const cucumber::messages::pickle& pickle) + { + std::vector parts; + parts.reserve(10); + + if (!lineage.feature) + parts.emplace_back(lineage.feature->name); + + if (lineage.rule) + parts.emplace_back(lineage.rule->name); + + if (lineage.scenario) + parts.emplace_back(lineage.scenario->name); + else + parts.emplace_back(pickle.name); + + if (lineage.examples) + parts.emplace_back(lineage.examples->name); + + if (parts.size() == 1) + return parts.front(); + + return std::accumulate(std::next(parts.begin()), parts.end(), parts.front(), [](const std::string& a, const std::string& b) + { + if (b.empty()) + return a; + else + return a + " - " + b; + }); + } + + Query::Query(util::Broadcaster& broadcaster) + : util::Listener{ broadcaster, [this](const cucumber::messages::envelope& envelope) + { + operator+=(envelope); + } } + {} + + const Lineage& Query::FindLineageByPickle(const cucumber::messages::pickle& pickle) const + { + return lineageById.at(pickle.ast_node_ids[0]); + } + + const Lineage& Query::FindLineageByUri(const std::string& uri) const + { + return lineageByUri.at(uri); + } + + const cucumber::messages::parameter_type& Query::FindParameterTypeById(const std::string& id) const + { + return parameterTypeById.at(id); + } + + const cucumber::messages::parameter_type& Query::FindParameterTypeByName(const std::string& name) const + { + return parameterTypeByName.at(name); + } + + bool Query::ContainsParameterTypeByName(const std::string& name) const + { + return parameterTypeByName.contains(name); + } + + const cucumber::messages::test_case& Query::FindTestCaseBy(const cucumber::messages::test_case_started& testCaseStarted) const + { + return testCaseById.at(testCaseStarted.test_case_id); + } + + const cucumber::messages::test_case& Query::FindTestCaseById(const std::string& id) const + { + return testCaseById.at(id); + } + + const cucumber::messages::pickle& Query::FindPickleBy(const cucumber::messages::test_case_started& testCaseStarted) const + { + const auto& testCase = FindTestCaseById(testCaseStarted.test_case_id); + return FindPickleById(testCase.pickle_id); + } + + const cucumber::messages::pickle& Query::FindPickleById(const std::string& id) const + { + return pickleById.at(id); + } + + const cucumber::messages::pickle_step* Query::FindPickleStepBy(const cucumber::messages::test_step& testStep) const + { + if (!testStep.pickle_step_id.has_value()) + return nullptr; + return &pickleStepById.at(testStep.pickle_step_id.value()); + } + + const cucumber::messages::pickle_step& Query::FindPickleStepById(const std::string& id) const + { + return pickleStepById.at(id); + } + + const cucumber::messages::test_step& Query::FindTestStepBy(const cucumber::messages::test_step_finished& testStepFinished) const + { + return testStepById.at(testStepFinished.test_step_id); + } + + const cucumber::messages::step& Query::FindStepBy(const cucumber::messages::pickle_step& pickleStep) const + { + return stepById.at(pickleStep.ast_node_ids[0]); + } + + const cucumber::messages::step_definition& Query::FindStepDefinitionById(const std::string& id) const + { + return stepDefinitionById.at(id); + } + + std::list Query::FindStepDefinitionsById(const cucumber::messages::test_step& testStep) const + { + if (!testStep.step_definition_ids.has_value()) + return {}; + + auto view = testStep.step_definition_ids.value() | std::views::transform([this](const std::string& id) + { + return &FindStepDefinitionById(id); + }); + + return { view.begin(), view.end() }; + } + + const cucumber::messages::hook& Query::FindHookById(const std::string& id) const + { + return hooksById.at(id); + } + + std::optional Query::FindLocationOf(const cucumber::messages::pickle& pickle) const + { + const auto& lineage = FindLineageByUri(pickle.uri); + if (lineage.tableRow) + return lineage.tableRow->location; + if (lineage.scenario) + return lineage.scenario->location; + return std::nullopt; + } + + const cucumber::messages::test_case_started& Query::FindTestCaseStartedById(const std::string& id) const + { + return testCaseStartedById.at(id); + } + + const std::map>& Query::StepDefinitions() const + { + return stepDefinitionById; + } + + const std::map>& Query::TestCaseStarted() const + { + return testCaseStartedById; + } + + const std::map>& Query::TestCaseFinishedByTestCaseStartedId() const + { + return testCaseFinishedByTestCaseStartedId; + } + + std::size_t Query::CountTestCasesStarted() const + { + return FindAllTestCaseStarted().size(); + } + + std::map> Query::CountMostSevereTestStepResultStatus() const + { + std::map> result{ + { cucumber::messages::test_step_result_status::UNKNOWN, 0 }, + { cucumber::messages::test_step_result_status::PASSED, 0 }, + { cucumber::messages::test_step_result_status::SKIPPED, 0 }, + { cucumber::messages::test_step_result_status::PENDING, 0 }, + { cucumber::messages::test_step_result_status::UNDEFINED, 0 }, + { cucumber::messages::test_step_result_status::AMBIGUOUS, 0 }, + { cucumber::messages::test_step_result_status::FAILED, 0 }, + }; + + for (const auto* testCaseStarted : FindAllTestCaseStarted()) + { + const auto testStepFinishedAndTestSteps = FindTestStepFinishedAndTestStepBy(*testCaseStarted); + if (testStepFinishedAndTestSteps.empty()) + continue; + const auto [testStepFinished, testStep] = *std::ranges::max_element(testStepFinishedAndTestSteps, SortByStatus{}); + ++result[testStepFinished->test_step_result.status]; + } + + return result; + } + + std::optional Query::FindMostSevereTestStepResultBy(const cucumber::messages::test_case_started& testCaseStarted) const + { + auto list = FindTestStepFinishedAndTestStepBy(testCaseStarted); + if (list.empty()) + return std::nullopt; + + list.sort(SortByStatus{}); + + const auto [testStepFinished, testStep] = list.back(); + return &testStepFinished->test_step_result; + } + + std::optional Query::FindMostSevereTestStepResultBy(const cucumber::messages::test_case_finished& testCaseFinished) const + { + return FindMostSevereTestStepResultBy(testCaseStartedById.at(testCaseFinished.test_case_started_id)); + } + + std::list Query::FindAllTestCaseStarted() const + { + auto view = testCaseStartedById | + std::views::values | + std::views::filter([this](const cucumber::messages::test_case_started& testCaseStarted) + { + return !testCaseFinishedByTestCaseStartedId.at(testCaseStarted.id).will_be_retried; + }) | + std::views::transform([](const cucumber::messages::test_case_started& testCaseStarted) + { + return &testCaseStarted; + }); + return { view.begin(), view.end() }; + } + + std::list> Query::FindTestStepFinishedAndTestStepBy(const cucumber::messages::test_case_started& testCaseStarted) const + { + if (!testStepFinishedByTestCaseStartedId.contains(testCaseStarted.id)) + return {}; + + const auto& testStepsFinished = testStepFinishedByTestCaseStartedId.at(testCaseStarted.id); + auto view = testStepsFinished | std::views::transform([this](const auto& testStepFinished) + { + return std::make_pair(&testStepFinished, &FindTestStepBy(testStepFinished)); + }); + return { view.begin(), view.end() }; + } + + const cucumber::messages::test_run_started& Query::FindTestRunStarted() const + { + return *testRunStarted; + } + + cucumber::messages::duration Query::FindTestRunDuration() const + { + return testRunFinished->timestamp - testRunStarted->timestamp; + } + + cucumber::messages::duration Query::FindTestCaseDurationBy(const cucumber::messages::test_case_started& testCaseStarted) const + { + return FindTestCaseDurationBy(testCaseStarted, testCaseFinishedByTestCaseStartedId.at(testCaseStarted.id)); + } + + cucumber::messages::duration Query::FindTestCaseDurationBy(const cucumber::messages::test_case_finished& testCaseFinished) const + { + return FindTestCaseDurationBy(testCaseStartedById.at(testCaseFinished.test_case_started_id), testCaseFinished); + } + + cucumber::messages::duration Query::FindTestCaseDurationBy(const cucumber::messages::test_case_started& testCaseStarted, const cucumber::messages::test_case_finished& testCaseFinished) const + { + return testCaseFinished.timestamp - testCaseStarted.timestamp; + } + + cucumber::messages::duration Query::FindTestStepDurationByTestStepId(const std::string& testStepId) const + { + return testStepFinishedByTestStepId.at(testStepId)->timestamp - testStepStartedByTestStepId.at(testStepId)->timestamp; + } + + void Query::operator+=(const cucumber::messages::envelope& envelope) + { + if (envelope.meta) + meta = std::make_unique(*envelope.meta); + + if (envelope.gherkin_document) + *this += *envelope.gherkin_document; + + if (envelope.pickle) + *this += *envelope.pickle; + + if (envelope.hook) + *this += *envelope.hook; + + if (envelope.step_definition) + *this += *envelope.step_definition; + + if (envelope.test_run_started) + *this += *envelope.test_run_started; + + if (envelope.test_run_hook_started) + *this += *envelope.test_run_hook_started; + + if (envelope.test_run_hook_finished) + *this += *envelope.test_run_hook_finished; + + if (envelope.test_case) + *this += *envelope.test_case; + + if (envelope.test_case_started) + *this += *envelope.test_case_started; + + if (envelope.test_step_started) + *this += *envelope.test_step_started; + + if (envelope.attachment) + *this += *envelope.attachment; + + if (envelope.test_step_finished) + *this += *envelope.test_step_finished; + + if (envelope.test_case_finished) + *this += *envelope.test_case_finished; + + if (envelope.test_run_finished) + *this += *envelope.test_run_finished; + + if (envelope.suggestion) + *this += *envelope.suggestion; + + if (envelope.undefined_parameter_type) + *this += *envelope.undefined_parameter_type; + + if (envelope.parameter_type) + *this += *envelope.parameter_type; + + BroadcastEvent(envelope); + } + + void Query::operator+=(const cucumber::messages::gherkin_document& gherkinDocument) + { + if (gherkinDocument.feature) + *this += { *gherkinDocument.feature, Lineage{ std::make_unique(gherkinDocument) } }; + } + + void Query::operator+=(const cucumber::messages::pickle& pickle) + { + pickleById.try_emplace(pickle.id, pickle); + for (const auto& pickleStep : pickle.steps) + pickleStepById.try_emplace(pickleStep.id, pickleStep); + } + + void Query::operator+=(const cucumber::messages::hook& hook) + { + hooksById.try_emplace(hook.id, hook); + } + + void Query::operator+=(const cucumber::messages::step_definition& stepDefinition) + { + stepDefinitionById.try_emplace(stepDefinition.id, stepDefinition); + } + + void Query::operator+=(const cucumber::messages::test_run_started& testRunStarted) + { + this->testRunStarted = std::make_unique(testRunStarted); + } + + void Query::operator+=(const cucumber::messages::test_run_hook_started& testRunHookStarted) + { + testRunHookStartedById.try_emplace(testRunHookStarted.id, testRunHookStarted); + } + + void Query::operator+=(const cucumber::messages::test_run_hook_finished& testRunHookFinished) + { + testRunHookFinishedByTestRunHookStartedId.try_emplace(testRunHookFinished.test_run_hook_started_id, testRunHookFinished); + } + + void Query::operator+=(const cucumber::messages::test_case& testCase) + { + auto& testCaseRef = testCaseById.try_emplace(testCase.id, testCase).first->second; + testCaseByPickleId.try_emplace(testCase.pickle_id, testCaseRef); + + for (const auto& testStep : testCase.test_steps) + { + testStepById.try_emplace(testStep.id, testStep); + pickleIdByTestStepId.try_emplace(testStep.id, testCase.pickle_id); + + if (testStep.pickle_step_id) + { + pickleStepIdByTestStepId[testStep.id] = testStep.pickle_step_id.value(); + testStepIdsByPickleStepId[testStep.pickle_step_id.value()].push_back(testStep.id); + + if (testStep.step_match_arguments_lists) + stepMatchArgumentsListsByPickleStepId[testStep.pickle_step_id.value()] = testStep.step_match_arguments_lists.value(); + } + } + } + + void Query::operator+=(const cucumber::messages::test_case_started& testCaseStarted) + { + testCaseStartedById.try_emplace(testCaseStarted.id, testCaseStarted); + + /* reset data? https://github.dev/cucumber/query/blob/f31732e5972c1815614f1d83928a7065e3080dc4/javascript/src/Query.ts#L249 */ + } + + void Query::operator+=(const cucumber::messages::test_step_started& testStepStarted) + { + testStepStartedByTestCaseStartedId[testStepStarted.test_case_started_id].push_back(testStepStarted); + testStepStartedByTestStepId[testStepStarted.test_step_id] = &testStepStartedByTestCaseStartedId[testStepStarted.test_case_started_id].back(); + } + + void Query::operator+=(const cucumber::messages::attachment& attachment) + { + auto* ptr = &attachments.emplace_front(attachment); + if (attachment.test_step_id) + attachmentsByTestStepId[attachment.test_step_id.value()].push_back(ptr); + if (attachment.test_case_started_id) + attachmentsByTestCaseStartedId[attachment.test_case_started_id.value()].push_back(ptr); + } + + void Query::operator+=(const cucumber::messages::test_step_finished& testStepFinished) + { + auto* testStepResultPtr = &testStepResults.emplace_front(testStepFinished.test_step_result); + + testStepFinishedByTestCaseStartedId[testStepFinished.test_case_started_id].push_back(testStepFinished); + testStepFinishedByTestStepId[testStepFinished.test_step_id] = &testStepFinishedByTestCaseStartedId[testStepFinished.test_case_started_id].back(); + + const auto& pickleId = pickleIdByTestStepId.at(testStepFinished.test_step_id); + testStepResultByPickleId[pickleId].push_back(testStepResultPtr); + + const auto& testStep = testStepById.at(testStepFinished.test_step_id); + testStepResultsbyTestStepId[testStep.id].push_back(testStepResultPtr); + if (testStep.pickle_step_id) + testStepResultsByPickleStepId[testStep.pickle_step_id.value()].push_back(testStepResultPtr); + } + + void Query::operator+=(const cucumber::messages::test_case_finished& testCaseFinished) + { + testCaseFinishedByTestCaseStartedId.try_emplace(testCaseFinished.test_case_started_id, testCaseFinished); + } + + void Query::operator+=(const cucumber::messages::test_run_finished& testRunFinished) + { + this->testRunFinished = std::make_unique(testRunFinished); + } + + void Query::operator+=(const cucumber::messages::suggestion& suggestion) + { + suggestionsByPickleStepId[suggestion.pickle_step_id].push_back(suggestion); + } + + void Query::operator+=(const cucumber::messages::undefined_parameter_type& undefinedParameterType) + { + undefinedParameterTypes.emplace_front(undefinedParameterType); + } + + void Query::operator+=(const std::pair& feature) + { + auto featurePtr = std::make_shared(feature.first); + + ++featureCountByName[feature.first.name]; + + auto lineage = feature.second + featurePtr + featureCountByName[feature.first.name]; + lineageByUri[*lineage.gherkinDocument->uri] = lineage; + + for (const auto& child : feature.first.children) + { + if (child.background) + *this += child.background->steps; + if (child.scenario) + *this += { *child.scenario, lineage }; + if (child.rule) + *this += { *child.rule, lineage }; + } + } + + void Query::operator+=(const std::pair& scenario) + { + auto scenarioPtr = std::make_shared(scenario.first); + lineageByUri[*scenario.second.gherkinDocument->uri] = scenario.second; + + lineageById[scenarioPtr->id] = scenario.second + scenarioPtr; + + for (const auto& examples : scenario.first.examples) + { + auto examplesPtr = std::make_shared(examples); + + lineageById[examples.id] = scenario.second + + scenarioPtr + + examplesPtr; + + for (const auto& tableRow : examples.table_body) + { + auto tableRowPtr = std::make_shared(tableRow); + + lineageById[tableRow.id] = scenario.second + + scenarioPtr + + examplesPtr + + tableRowPtr; + } + } + + *this += scenarioPtr->steps; + } + + void Query::operator+=(const std::pair& rule) + { + auto rulePtr = std::make_shared(rule.first); + + for (const auto& child : rule.first.children) + { + if (child.background) + *this += child.background->steps; + if (child.scenario) + *this += { *child.scenario, rule.second + rulePtr }; + } + } + + void Query::operator+=(std::span steps) + { + for (const auto& step : steps) + stepById.try_emplace(step.id, step); + } + + void Query::operator+=(const cucumber::messages::parameter_type& parameterType) + { + auto& ref = parameterTypeById.try_emplace(parameterType.id, parameterType).first->second; + parameterTypeByName.try_emplace(parameterType.name, ref); + } +} diff --git a/cucumber_cpp/library/query/Query.hpp b/cucumber_cpp/library/query/Query.hpp new file mode 100644 index 00000000..e5acf6e0 --- /dev/null +++ b/cucumber_cpp/library/query/Query.hpp @@ -0,0 +1,255 @@ +#ifndef LIBRARY_QUERY_HPP +#define LIBRARY_QUERY_HPP + +#include "cucumber/messages/attachment.hpp" +#include "cucumber/messages/duration.hpp" +#include "cucumber/messages/envelope.hpp" +#include "cucumber/messages/examples.hpp" +#include "cucumber/messages/feature.hpp" +#include "cucumber/messages/gherkin_document.hpp" +#include "cucumber/messages/hook.hpp" +#include "cucumber/messages/location.hpp" +#include "cucumber/messages/meta.hpp" +#include "cucumber/messages/parameter_type.hpp" +#include "cucumber/messages/pickle.hpp" +#include "cucumber/messages/pickle_step.hpp" +#include "cucumber/messages/rule.hpp" +#include "cucumber/messages/scenario.hpp" +#include "cucumber/messages/step.hpp" +#include "cucumber/messages/step_definition.hpp" +#include "cucumber/messages/step_match_arguments_list.hpp" +#include "cucumber/messages/suggestion.hpp" +#include "cucumber/messages/table_row.hpp" +#include "cucumber/messages/test_case.hpp" +#include "cucumber/messages/test_case_finished.hpp" +#include "cucumber/messages/test_case_started.hpp" +#include "cucumber/messages/test_run_finished.hpp" +#include "cucumber/messages/test_run_hook_finished.hpp" +#include "cucumber/messages/test_run_hook_started.hpp" +#include "cucumber/messages/test_run_started.hpp" +#include "cucumber/messages/test_step.hpp" +#include "cucumber/messages/test_step_finished.hpp" +#include "cucumber/messages/test_step_result.hpp" +#include "cucumber/messages/test_step_result_status.hpp" +#include "cucumber/messages/test_step_started.hpp" +#include "cucumber/messages/undefined_parameter_type.hpp" +#include "cucumber_cpp/library/util/Broadcaster.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace cucumber_cpp::library::query +{ + struct Lineage + { + std::string GetUniqueFeatureName() const; + std::string GetScenarioAndOrRuleName() const; + + std::shared_ptr gherkinDocument; + std::shared_ptr feature; + std::shared_ptr rule; + std::shared_ptr scenario; + std::shared_ptr examples; + std::shared_ptr tableRow; + + std::uint32_t featureIndex{ 0 }; + + friend Lineage operator+(Lineage lineage, std::shared_ptr gherkinDocument) + { + lineage.gherkinDocument = gherkinDocument; + return std::move(lineage); + } + + friend Lineage operator+(Lineage lineage, std::shared_ptr feature) + { + lineage.feature = feature; + return std::move(lineage); + } + + friend Lineage operator+(Lineage lineage, std::shared_ptr rule) + { + lineage.rule = rule; + return std::move(lineage); + } + + friend Lineage operator+(Lineage lineage, std::shared_ptr scenario) + { + lineage.scenario = scenario; + return std::move(lineage); + } + + friend Lineage operator+(Lineage lineage, std::shared_ptr examples) + { + lineage.examples = examples; + return std::move(lineage); + } + + friend Lineage operator+(Lineage lineage, std::shared_ptr tableRow) + { + lineage.tableRow = tableRow; + return std::move(lineage); + } + + friend Lineage operator+(Lineage lineage, std::uint32_t featureIndex) + { + lineage.featureIndex = featureIndex; + return std::move(lineage); + } + }; + + struct NamingStrategy + { + static std::string Reduce(const Lineage& lineage, const cucumber::messages::pickle& pickle); + }; + + struct Query + : util::Broadcaster + , util::Listener + { + explicit Query(util::Broadcaster& broadcaster); + + auto GetPickles() const + { + return pickleById | std::views::values; + } + + const Lineage& FindLineageByPickle(const cucumber::messages::pickle& pickle) const; + const Lineage& FindLineageByUri(const std::string&) const; + + const cucumber::messages::parameter_type& FindParameterTypeById(const std::string& id) const; + const cucumber::messages::parameter_type& FindParameterTypeByName(const std::string& name) const; + bool ContainsParameterTypeByName(const std::string& name) const; + + const cucumber::messages::test_case& FindTestCaseBy(const cucumber::messages::test_case_started& testCaseStarted) const; + const cucumber::messages::test_case& FindTestCaseById(const std::string& id) const; + + const cucumber::messages::pickle& FindPickleBy(const cucumber::messages::test_case_started& testCaseStarted) const; + const cucumber::messages::pickle& FindPickleById(const std::string& id) const; + + const cucumber::messages::pickle_step* FindPickleStepBy(const cucumber::messages::test_step& testStep) const; + const cucumber::messages::pickle_step& FindPickleStepById(const std::string& id) const; + + const cucumber::messages::test_step& FindTestStepBy(const cucumber::messages::test_step_finished& testStepFinished) const; + + const cucumber::messages::step& FindStepBy(const cucumber::messages::pickle_step& pickleStep) const; + + const cucumber::messages::step_definition& FindStepDefinitionById(const std::string& id) const; + std::list FindStepDefinitionsById(const cucumber::messages::test_step& testStep) const; + + const cucumber::messages::hook& FindHookById(const std::string& id) const; + + std::optional FindLocationOf(const cucumber::messages::pickle& pickle) const; + + const cucumber::messages::test_case_started& FindTestCaseStartedById(const std::string& id) const; + + const std::map>& StepDefinitions() const; + const std::map>& TestCaseStarted() const; + const std::map>& TestCaseFinishedByTestCaseStartedId() const; + + std::size_t CountTestCasesStarted() const; + + std::map> CountMostSevereTestStepResultStatus() const; + std::optional FindMostSevereTestStepResultBy(const cucumber::messages::test_case_started& testCaseStarted) const; + std::optional FindMostSevereTestStepResultBy(const cucumber::messages::test_case_finished& testCaseFinished) const; + + std::list FindAllTestCaseStarted() const; + std::list> FindTestStepFinishedAndTestStepBy(const cucumber::messages::test_case_started& testCaseStarted) const; + + const cucumber::messages::test_run_started& FindTestRunStarted() const; + cucumber::messages::duration FindTestRunDuration() const; + + cucumber::messages::duration FindTestCaseDurationBy(const cucumber::messages::test_case_started& testCaseStarted) const; + cucumber::messages::duration FindTestCaseDurationBy(const cucumber::messages::test_case_finished& testCaseFinished) const; + cucumber::messages::duration FindTestCaseDurationBy(const cucumber::messages::test_case_started& testCaseStarted, const cucumber::messages::test_case_finished& testCaseFinished) const; + + cucumber::messages::duration FindTestStepDurationByTestStepId(const std::string& testStepId) const; + + private: + void operator+=(const cucumber::messages::envelope& envelope); + + void operator+=(const cucumber::messages::gherkin_document& gherkinDocument); + void operator+=(const cucumber::messages::pickle& pickle); + void operator+=(const cucumber::messages::hook& hook); + void operator+=(const cucumber::messages::step_definition& stepDefinition); + void operator+=(const cucumber::messages::test_run_started& testRunStarted); + void operator+=(const cucumber::messages::test_run_hook_started& testRunHookStarted); + void operator+=(const cucumber::messages::test_run_hook_finished& testRunHookFinished); + void operator+=(const cucumber::messages::test_case& testCase); + void operator+=(const cucumber::messages::test_case_started& testCaseStarted); + void operator+=(const cucumber::messages::test_step_started& testStepStarted); + void operator+=(const cucumber::messages::attachment& attachment); + void operator+=(const cucumber::messages::test_step_finished& testStepFinished); + void operator+=(const cucumber::messages::test_case_finished& testCaseFinished); + void operator+=(const cucumber::messages::test_run_finished& testRunFinished); + void operator+=(const cucumber::messages::suggestion& suggestion); + void operator+=(const cucumber::messages::undefined_parameter_type& undefinedParameterType); + + void operator+=(const std::pair& feature); + void operator+=(const std::pair& scenario); + void operator+=(const std::pair& rule); + void operator+=(std::span steps); + + void operator+=(const cucumber::messages::parameter_type& parameterType); + + std::map> featureCountByName; + + std::list testStepResults; + std::map, std::less<>> testStepResultByPickleId; + std::map, std::less<>> testStepResultsByPickleStepId; + std::map, std::less<>> testStepResultsbyTestStepId; + + std::map> testCaseById; + std::map> testCaseByPickleId; + + std::map> pickleIdByTestStepId; + std::map> pickleStepIdByTestStepId; + std::map, std::less<>> testStepIdsByPickleStepId; + std::map> hooksById; + std::list attachments; + std::map, std::less<>> attachmentsByTestStepId; + std::map, std::less<>> attachmentsByTestCaseStartedId; + // std::map>, std::less<>> attachmentsByTestRunHookStartedId; + + std::map, std::less<>> stepMatchArgumentsListsByPickleStepId; + + std::unique_ptr meta; + std::unique_ptr testRunStarted; + std::unique_ptr testRunFinished; + + std::map> testCaseStartedById; + std::map> testCaseFinishedByTestCaseStartedId; + + std::map> lineageById; + std::map> lineageByUri; + + std::map> stepById; + std::map> pickleById; + std::map> pickleStepById; + std::map> stepDefinitionById; + std::map> testStepById; + std::map> testRunHookStartedById; + std::map> testRunHookFinishedByTestRunHookStartedId; + std::map, std::less<>> testStepStartedByTestCaseStartedId; + std::map, std::less<>> testStepFinishedByTestCaseStartedId; + + std::map> testStepStartedByTestStepId; + std::map> testStepFinishedByTestStepId; + + std::map, std::less<>> suggestionsByPickleStepId; + std::list undefinedParameterTypes; + + std::map> parameterTypeById; + std::map> parameterTypeByName; + }; +} + +#endif diff --git a/cucumber_cpp/library/query/test/CMakeLists.txt b/cucumber_cpp/library/query/test/CMakeLists.txt new file mode 100644 index 00000000..70c5cc12 --- /dev/null +++ b/cucumber_cpp/library/query/test/CMakeLists.txt @@ -0,0 +1,12 @@ +add_executable(cucumber_cpp.library.query.test) +add_test(NAME cucumber_cpp.library.query.test COMMAND cucumber_cpp.library.query.test) + +target_link_libraries(cucumber_cpp.library.query.test PUBLIC + gmock_main + cucumber_cpp.library.query + GTest::gmock +) + +target_sources(cucumber_cpp.library.query.test PRIVATE + TestDummy.cpp +) diff --git a/cucumber_cpp/library/query/test/TestDummy.cpp b/cucumber_cpp/library/query/test/TestDummy.cpp new file mode 100644 index 00000000..c81abaf1 --- /dev/null +++ b/cucumber_cpp/library/query/test/TestDummy.cpp @@ -0,0 +1,8 @@ +#include + +namespace cucumber_cpp::library::support +{ + TEST(Dummy, dummy) + { + } +} diff --git a/cucumber_cpp/library/report/CMakeLists.txt b/cucumber_cpp/library/report/CMakeLists.txt deleted file mode 100644 index bba15987..00000000 --- a/cucumber_cpp/library/report/CMakeLists.txt +++ /dev/null @@ -1,23 +0,0 @@ -add_library(cucumber_cpp.library.report STATIC ${CCR_EXCLUDE_FROM_ALL}) - -target_sources(cucumber_cpp.library.report PRIVATE - JunitReport.cpp - JunitReport.hpp - Report.cpp - Report.hpp - StdOutReport.cpp - StdOutReport.hpp -) - -target_include_directories(cucumber_cpp.library.report PUBLIC - ../../.. -) - -target_link_libraries(cucumber_cpp.library.report PUBLIC - pugixml::pugixml - cucumber_cpp.library -) - -if (CCR_BUILD_TESTS) - add_subdirectory(test_helper) -endif() diff --git a/cucumber_cpp/library/report/JunitReport.cpp b/cucumber_cpp/library/report/JunitReport.cpp deleted file mode 100644 index 3b321169..00000000 --- a/cucumber_cpp/library/report/JunitReport.cpp +++ /dev/null @@ -1,238 +0,0 @@ - -#include "cucumber_cpp/library/report/JunitReport.hpp" -#include "cucumber_cpp/library/TraceTime.hpp" -#include "cucumber_cpp/library/engine/FeatureInfo.hpp" -#include "cucumber_cpp/library/engine/Result.hpp" -#include "cucumber_cpp/library/engine/RuleInfo.hpp" -#include "cucumber_cpp/library/engine/ScenarioInfo.hpp" -#include "cucumber_cpp/library/engine/StepInfo.hpp" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace cucumber_cpp::library::report -{ - namespace - { - constexpr double precision = 0.0000001; - - const std::map successLut{ - { engine::Result::passed, "done" }, - { engine::Result::skipped, "skipped" }, - { engine::Result::failed, "failed" }, - { engine::Result::pending, "pending" }, - { engine::Result::ambiguous, "ambiguous" }, - { engine::Result::undefined, "undefined" }, - }; - - std::string RoundTo(double value, double roundToPrecision) - { - const auto d = std::round(value / roundToPrecision) * roundToPrecision; - - std::ostringstream out; - out << std::fixed << d; - return out.str(); - } - } - - JunitReport::JunitReport(const std::string& outputfolder, const std::string& reportfile) - : outputfolder{ outputfolder } - , reportfile{ reportfile } - { - testsuites = doc.append_child("testsuites"); - testsuites.append_attribute("name").set_value("Test run"); - testsuites.append_attribute("time").set_value(0.0); - } - - JunitReport::~JunitReport() - { - testsuites.append_attribute("tests").set_value(totalTests); - testsuites.append_attribute("failures").set_value(totalFailures); - testsuites.append_attribute("skipped").set_value(totalSkipped); - - const auto doubleTime = std::chrono::duration>(totalTime).count(); - testsuites.append_attribute("time").set_value(RoundTo(doubleTime, precision).c_str()); - - try - { - if (!std::filesystem::exists(outputfolder)) - std::filesystem::create_directories(outputfolder); - - const auto outputfile = std::filesystem::path{ outputfolder }.append(reportfile + ".xml"); - doc.save_file(outputfile.c_str()); - } - catch (const std::filesystem::filesystem_error& ex) - { - std::cout << "\nwhat(): " << ex.what() << '\n' - << "path1(): " << ex.path1() << '\n' - << "path2(): " << ex.path2() << '\n' - << "code().value(): " << ex.code().value() << '\n' - << "code().message(): " << ex.code().message() << '\n' - << "code().category(): " << ex.code().category().name() << '\n'; - } - } - - void JunitReport::FeatureStart(const engine::FeatureInfo& featureInfo) - { - testsuite = testsuites.append_child("testsuite"); - testsuite.append_attribute("name").set_value(featureInfo.Title().c_str()); - testsuite.append_attribute("file").set_value(featureInfo.Path().string().c_str()); - - scenarioTests = 0; - scenarioFailures = 0; - scenarioSkipped = 0; - } - - void JunitReport::FeatureEnd(engine::Result /*result*/, const engine::FeatureInfo& /*featureInfo*/, 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; - totalSkipped += scenarioSkipped; - - testsuite.append_attribute("tests").set_value(scenarioTests); - testsuite.append_attribute("failures").set_value(scenarioFailures); - testsuite.append_attribute("skipped").set_value(scenarioSkipped); - } - - void JunitReport::RuleStart(const engine::RuleInfo& ruleInfo) - { - /* do nothing */ - } - - void JunitReport::RuleEnd(engine::Result result, const engine::RuleInfo& ruleInfo, TraceTime::Duration duration) - { - /* do nothing */ - } - - void JunitReport::ScenarioStart(const engine::ScenarioInfo& scenarioInfo) - { - testcase = testsuite.append_child("testcase"); - - testcase.append_attribute("name").set_value(scenarioInfo.Title().c_str()); - - ++scenarioTests; - } - - void JunitReport::ScenarioEnd(engine::Result result, const engine::ScenarioInfo& /*scenarioInfo*/, TraceTime::Duration duration) - { - const auto doubleTime = std::chrono::duration>(duration).count(); - testcase.append_attribute("time").set_value(RoundTo(doubleTime, precision).c_str()); - - switch (result) - { - case engine::Result::passed: - break; - - case engine::Result::skipped: - case engine::Result::pending: - case engine::Result::ambiguous: - case engine::Result::undefined: - { - ++scenarioSkipped; - auto skipped = testcase.append_child("skipped"); - - if (result == engine::Result::skipped) - { - skipped.append_attribute("message").set_value("Test is skipped due to previous errors."); - } - else if (result == engine::Result::undefined) - { - skipped.append_attribute("message").set_value("Test is undefined."); - } - else if (result == engine::Result::pending) - { - skipped.append_attribute("message").set_value("Test is pending."); - } - else - { - skipped.append_attribute("message").set_value("Test result unkown."); - } - } - - break; - - case engine::Result::failed: - ++scenarioFailures; - break; - } - - totalTime += duration; - } - - void JunitReport::StepSkipped(const engine::StepInfo& stepInfo) - { - /* do nothing */ - } - - void JunitReport::StepStart(const engine::StepInfo& stepInfo) - { - /* do nothing */ - } - - void JunitReport::StepEnd(engine::Result result, const engine::StepInfo& stepInfo, TraceTime::Duration duration) - { - /* do nothing */ - } - - void JunitReport::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 JunitReport::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 JunitReport::Trace(const std::string& trace) - { - /* do nothing */ - } - - void JunitReport::Summary(TraceTime::Duration duration) - { - /* do nothing */} -} diff --git a/cucumber_cpp/library/report/JunitReport.hpp b/cucumber_cpp/library/report/JunitReport.hpp deleted file mode 100644 index 8c528f4e..00000000 --- a/cucumber_cpp/library/report/JunitReport.hpp +++ /dev/null @@ -1,64 +0,0 @@ -#ifndef REPORT_JUNITREPORT_HPP -#define REPORT_JUNITREPORT_HPP - -#include "cucumber_cpp/library/TraceTime.hpp" -#include "cucumber_cpp/library/engine/FeatureInfo.hpp" -#include "cucumber_cpp/library/engine/Result.hpp" -#include "cucumber_cpp/library/engine/RuleInfo.hpp" -#include "cucumber_cpp/library/engine/ScenarioInfo.hpp" -#include "cucumber_cpp/library/engine/StepInfo.hpp" -#include "cucumber_cpp/library/report/Report.hpp" -#include "pugixml.hpp" -#include -#include -#include -#include - -namespace cucumber_cpp::library::report -{ - struct JunitReport : ReportHandlerV2 - { - JunitReport(const std::string& outputfolder, const std::string& reportfile); - ~JunitReport() override; - - void FeatureStart(const engine::FeatureInfo& featureInfo) override; - void FeatureEnd(engine::Result result, const engine::FeatureInfo& featureInfo, TraceTime::Duration duration) override; - - void RuleStart(const engine::RuleInfo& ruleInfo) override; - void RuleEnd(engine::Result result, const engine::RuleInfo& ruleInfo, TraceTime::Duration duration) override; - - void ScenarioStart(const engine::ScenarioInfo& scenarioInfo) override; - void ScenarioEnd(engine::Result result, const engine::ScenarioInfo& scenarioInfo, TraceTime::Duration duration) override; - - void StepSkipped(const engine::StepInfo& stepInfo) override; - void StepStart(const engine::StepInfo& stepInfo) override; - void StepEnd(engine::Result result, const engine::StepInfo& stepInfo, 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; - - void Summary(TraceTime::Duration duration) override; - - private: - const std::string& outputfolder; - const std::string& reportfile; - - 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 totalSkipped{ 0 }; - TraceTime::Duration totalTime{ 0 }; - - std::size_t scenarioTests{ 0 }; - std::size_t scenarioFailures{ 0 }; - std::size_t scenarioSkipped{ 0 }; - }; -} - -#endif diff --git a/cucumber_cpp/library/report/Report.cpp b/cucumber_cpp/library/report/Report.cpp deleted file mode 100644 index 3ca74a18..00000000 --- a/cucumber_cpp/library/report/Report.cpp +++ /dev/null @@ -1,167 +0,0 @@ -#include "cucumber_cpp/library/report/Report.hpp" -#include "cucumber_cpp/library/TraceTime.hpp" -#include "cucumber_cpp/library/engine/ContextManager.hpp" -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace cucumber_cpp::library::report -{ - namespace - { - template - void ForwardCall(auto& reporters, TFn fn, TArgs&&... args) - { - std::ranges::for_each( - reporters, [&](const auto& reportHandlerV2) - { - (*reportHandlerV2.*fn)(std::forward(args)...); - }); - } - } - - void Reporters::Add(const std::string& name, std::unique_ptr reporter) - { - availableReporters[name] = std::move(reporter); - } - - void Reporters::Use(const std::string& name) - { - if (availableReporters[name]) - Add(std::move(availableReporters[name])); - } - - void Reporters::Add(std::unique_ptr report) - { - reporters.push_back(std::move(report)); - } - - std::vector Reporters::AvailableReporters() const - { - auto range = std::views::keys(availableReporters); - return { range.begin(), range.end() }; - } - - std::vector>& Reporters::Storage() - { - return reporters; - } - - ReportForwarder::ProgramScope::ProgramScope(cucumber_cpp::library::engine::ProgramContext& programContext, std::vector>& reporters) - : programContext{ programContext } - , reporters{ reporters } - {} - - ReportForwarder::ProgramScope::~ProgramScope() - { - ForwardCall(reporters, &ReportHandlerV2::Summary, programContext.Duration()); - } - - ReportForwarder::FeatureScope::FeatureScope(cucumber_cpp::library::engine::FeatureContext& featureContext, std::vector>& reporters) - : featureContext{ featureContext } - , reporters{ reporters } - { - ForwardCall(reporters, &ReportHandlerV2::FeatureStart, featureContext.info); - } - - ReportForwarder::FeatureScope::~FeatureScope() - { - ForwardCall(reporters, &ReportHandlerV2::FeatureEnd, featureContext.ExecutionStatus(), featureContext.info, featureContext.Duration()); - } - - ReportForwarder::RuleScope::RuleScope(cucumber_cpp::library::engine::RuleContext& ruleContext, std::vector>& reporters) - : ruleContext{ ruleContext } - , reporters{ reporters } - { - ForwardCall(reporters, &ReportHandlerV2::RuleStart, ruleContext.info); - } - - ReportForwarder::RuleScope::~RuleScope() - { - ForwardCall(reporters, &ReportHandlerV2::RuleEnd, ruleContext.ExecutionStatus(), ruleContext.info, ruleContext.Duration()); - } - - ReportForwarder::ScenarioScope::ScenarioScope(cucumber_cpp::library::engine::ScenarioContext& scenarioContext, std::vector>& reporters) - : scenarioContext{ scenarioContext } - , reporters{ reporters } - { - ForwardCall(reporters, &ReportHandlerV2::ScenarioStart, scenarioContext.info); - } - - ReportForwarder::ScenarioScope::~ScenarioScope() - { - ForwardCall(reporters, &ReportHandlerV2::ScenarioEnd, scenarioContext.ExecutionStatus(), scenarioContext.info, scenarioContext.Duration()); - } - - ReportForwarder::StepScope::StepScope(cucumber_cpp::library::engine::StepContext& stepContext, std::vector>& reporters) - : stepContext{ stepContext } - , reporters{ reporters } - { - ForwardCall(reporters, &ReportHandlerV2::StepStart, stepContext.info); - } - - ReportForwarder::StepScope::~StepScope() - { - ForwardCall(reporters, &ReportHandlerV2::StepEnd, stepContext.ExecutionStatus(), stepContext.info, stepContext.Duration()); - } - - ReportForwarderImpl::ReportForwarderImpl(cucumber_cpp::library::engine::ContextManager& contextManager) - : contextManager{ contextManager } - {} - - [[nodiscard]] ReportForwarder::ProgramScope ReportForwarderImpl::ProgramStart() - { - return ProgramScope{ contextManager.ProgramContext(), Storage() }; - } - - ReportForwarder::FeatureScope ReportForwarderImpl::FeatureStart() - { - return FeatureScope{ contextManager.FeatureContext(), Storage() }; - } - - ReportForwarder::RuleScope ReportForwarderImpl::RuleStart() - { - return RuleScope{ contextManager.RuleContext(), Storage() }; - } - - ReportForwarder::ScenarioScope ReportForwarderImpl::ScenarioStart() - { - return ScenarioScope{ contextManager.ScenarioContext(), Storage() }; - } - - ReportForwarder::StepScope ReportForwarderImpl::StepStart() - { - return StepScope{ contextManager.StepContext(), Storage() }; - } - - void ReportForwarderImpl::StepSkipped() - { - ForwardCall(Storage(), &ReportHandlerV2::StepSkipped, contextManager.StepContext().info); - } - - void ReportForwarderImpl::Failure(const std::string& error, std::optional path, std::optional line, std::optional column) - { - ForwardCall(Storage(), &ReportHandlerV2::Failure, error, path, line, column); - } - - void ReportForwarderImpl::Error(const std::string& error, std::optional path, std::optional line, std::optional column) - { - ForwardCall(Storage(), &ReportHandlerV2::Error, error, path, line, column); - } - - void ReportForwarderImpl::Trace(const std::string& trace) - { - ForwardCall(Storage(), &ReportHandlerV2::Trace, trace); - } - - void ReportForwarderImpl::Summary(TraceTime::Duration duration) - { - ForwardCall(Storage(), &ReportHandlerV2::Summary, duration); - } -} diff --git a/cucumber_cpp/library/report/Report.hpp b/cucumber_cpp/library/report/Report.hpp deleted file mode 100644 index a70b6f15..00000000 --- a/cucumber_cpp/library/report/Report.hpp +++ /dev/null @@ -1,178 +0,0 @@ -#ifndef REPORT_REPORT_HPP -#define REPORT_REPORT_HPP - -// IWYU pragma: private, include "cucumber_cpp/CucumberCpp.hpp" -// IWYU pragma: friend cucumber_cpp/.* - -#include "cucumber_cpp/library/TraceTime.hpp" -#include "cucumber_cpp/library/engine/ContextManager.hpp" -#include "cucumber_cpp/library/engine/FeatureInfo.hpp" -#include "cucumber_cpp/library/engine/Result.hpp" -#include "cucumber_cpp/library/engine/RuleInfo.hpp" -#include "cucumber_cpp/library/engine/StepInfo.hpp" -#include "cucumber_cpp/library/util/Immoveable.hpp" -#include -#include -#include -#include -#include -#include -#include -#include - -namespace cucumber_cpp -{ - struct FeatureSource; - struct ScenarioSource; - struct StepSource; -} - -namespace cucumber_cpp::library::report -{ - struct ReportHandlerV2 - { - virtual ~ReportHandlerV2() = default; - - virtual void FeatureStart(const engine::FeatureInfo& featureInfo) = 0; - virtual void FeatureEnd(engine::Result result, const engine::FeatureInfo& featureInfo, TraceTime::Duration duration) = 0; - - virtual void RuleStart(const engine::RuleInfo& ruleInfo) = 0; - virtual void RuleEnd(engine::Result result, const engine::RuleInfo& ruleInfo, TraceTime::Duration duration) = 0; - - virtual void ScenarioStart(const engine::ScenarioInfo& scenarioInfo) = 0; - virtual void ScenarioEnd(engine::Result result, const engine::ScenarioInfo& scenarioInfo, TraceTime::Duration duration) = 0; - - virtual void StepSkipped(const engine::StepInfo& stepInfo) = 0; - virtual void StepStart(const engine::StepInfo& stepInfo) = 0; - virtual void StepEnd(engine::Result result, const engine::StepInfo& stepInfo, 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; - - virtual void Summary(TraceTime::Duration duration) = 0; - }; - - struct Reporters - { - void Add(const std::string& name, std::unique_ptr reporter); - void Use(const std::string& name); - - [[nodiscard]] std::vector AvailableReporters() const; - - protected: - void Add(std::unique_ptr report); - std::vector>& Storage(); - - private: - std::map, std::less<>> availableReporters; - std::vector> reporters; - }; - - struct ReportForwarder - { - protected: - ~ReportForwarder() = default; - - public: - struct ProgramScope; - struct FeatureScope; - struct RuleScope; - struct ScenarioScope; - struct StepScope; - - [[nodiscard]] virtual ProgramScope ProgramStart() = 0; - [[nodiscard]] virtual FeatureScope FeatureStart() = 0; - [[nodiscard]] virtual RuleScope RuleStart() = 0; - [[nodiscard]] virtual ScenarioScope ScenarioStart() = 0; - [[nodiscard]] virtual StepScope StepStart() = 0; - - virtual void StepSkipped() = 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; - - virtual void Summary(TraceTime::Duration duration) = 0; - }; - - struct ReportForwarder::ProgramScope : util::Immoveable - { - ProgramScope(cucumber_cpp::library::engine::ProgramContext& programContext, std::vector>& reporters); - ~ProgramScope(); - - private: - cucumber_cpp::library::engine::ProgramContext& programContext; - std::vector>& reporters; - }; - - struct ReportForwarder::FeatureScope : util::Immoveable - { - FeatureScope(cucumber_cpp::library::engine::FeatureContext& featureContext, std::vector>& reporters); - ~FeatureScope(); - - private: - cucumber_cpp::library::engine::FeatureContext& featureContext; - std::vector>& reporters; - }; - - struct ReportForwarder::RuleScope : util::Immoveable - { - RuleScope(cucumber_cpp::library::engine::RuleContext& ruleContext, std::vector>& reporters); - ~RuleScope(); - - private: - cucumber_cpp::library::engine::RuleContext& ruleContext; - std::vector>& reporters; - }; - - struct ReportForwarder::ScenarioScope : util::Immoveable - { - ScenarioScope(cucumber_cpp::library::engine::ScenarioContext& scenarioContext, std::vector>& reporters); - ~ScenarioScope(); - - private: - cucumber_cpp::library::engine::ScenarioContext& scenarioContext; - std::vector>& reporters; - }; - - struct ReportForwarder::StepScope : util::Immoveable - { - StepScope(cucumber_cpp::library::engine::StepContext& stepContext, std::vector>& reporters); - ~StepScope(); - - private: - cucumber_cpp::library::engine::StepContext& stepContext; - std::vector>& reporters; - }; - - struct ReportForwarderImpl - : Reporters - , ReportForwarder - { - explicit ReportForwarderImpl(cucumber_cpp::library::engine::ContextManager& contextManager); - virtual ~ReportForwarderImpl() = default; - - [[nodiscard]] ProgramScope ProgramStart() override; - [[nodiscard]] FeatureScope FeatureStart() override; - [[nodiscard]] RuleScope RuleStart() override; - [[nodiscard]] ScenarioScope ScenarioStart() override; - [[nodiscard]] StepScope StepStart() override; - - void StepSkipped() 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; - - void Summary(TraceTime::Duration duration) override; - - private: - cucumber_cpp::library::engine::ContextManager& contextManager; - }; -} - -#endif diff --git a/cucumber_cpp/library/report/StdOutReport.cpp b/cucumber_cpp/library/report/StdOutReport.cpp deleted file mode 100644 index f17c17d5..00000000 --- a/cucumber_cpp/library/report/StdOutReport.cpp +++ /dev/null @@ -1,295 +0,0 @@ -#include "cucumber_cpp/library/report/StdOutReport.hpp" -#include "cucumber_cpp/library/TraceTime.hpp" -#include "cucumber_cpp/library/engine/FeatureInfo.hpp" -#include "cucumber_cpp/library/engine/Result.hpp" -#include "cucumber_cpp/library/engine/RuleInfo.hpp" -#include "cucumber_cpp/library/engine/ScenarioInfo.hpp" -#include "cucumber_cpp/library/engine/StepInfo.hpp" -#include "cucumber_cpp/library/engine/StepType.hpp" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#ifdef _MSC_VER -// clang-format off -#include -#include -#include -#include -#include -#include -// clang-format on -#endif - -namespace cucumber_cpp::library::report -{ - namespace - { -#ifndef _MSC_VER - inline std::ostream& TcRed(std::ostream& o) - { - o << "\o{33}[1m\o{33}[31m"; - return o; - } - - inline std::ostream& TcGreen(std::ostream& o) - { - o << "\o{33}[1m\o{33}[32m"; - return o; - } - - inline std::ostream& TcCyan(std::ostream& o) - { - o << "\o{33}[1m\o{33}[36m"; - return o; - } - - inline std::ostream& TcDefault(std::ostream& o) - { - o << "\o{33}[0m\o{33}[39m"; - return o; - } -#else - WORD GetDefaultConsoleValue() - { - CONSOLE_SCREEN_BUFFER_INFO info; - GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &info); - return info.wAttributes; - } - - WORD GetDefaultConsole() - { - static WORD defaultValue = GetDefaultConsoleValue(); - return defaultValue; - } - - void SetColorConsole(WORD color) - { - SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), color | (GetDefaultConsole() & ~(FOREGROUND_INTENSITY | FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE))); - } - - inline std::ostream& TcRed(std::ostream& o) - { - SetColorConsole(FOREGROUND_INTENSITY | FOREGROUND_RED); - return o; - } - - inline std::ostream& TcGreen(std::ostream& o) - { - SetColorConsole(FOREGROUND_INTENSITY | FOREGROUND_GREEN); - return o; - } - - inline std::ostream& TcCyan(std::ostream& o) - { - SetColorConsole(FOREGROUND_INTENSITY | FOREGROUND_GREEN | FOREGROUND_BLUE); - return o; - } - - inline std::ostream& TcDefault(std::ostream& o) - { - SetColorConsole(GetDefaultConsole()); - return o; - } -#endif - - const std::map successLut{ - { engine::Result ::passed, "done" }, - { engine::Result ::skipped, "skipped" }, - { engine::Result ::failed, "failed" }, - { engine::Result ::pending, "pending" }, - { engine::Result ::ambiguous, "ambiguous" }, - { engine::Result ::undefined, "undefined" }, - }; - - const std::map stepTypeLut{ - { engine::StepType::given, "Given" }, - { engine::StepType::when, "When" }, - { engine::StepType::then, "Then" } - }; - - std::string ScaledDuration(TraceTime::Duration duration) - { - 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(); - } - - std::string Repeat(std::string_view input, std::size_t count) - { - std::string result; - result.reserve(input.size() * count); - for (std::size_t i = 0; i < count; ++i) - result += input; - return result; - } - } - - StdOutReport::StdOutReport() - { -#ifdef _MSC_VER - SetConsoleOutputCP(CP_UTF8); - SetConsoleCP(CP_UTF8); -#endif - } - - void StdOutReport::FeatureStart(const engine::FeatureInfo& featureInfo) - { - // not required - } - - void StdOutReport::FeatureEnd(engine::Result result, const engine::FeatureInfo& featureInfo, TraceTime::Duration duration) - { - // not required - } - - void StdOutReport::RuleStart(const engine::RuleInfo& ruleInfo) - { - std::cout << "\n" - << ruleInfo.Title(); - } - - void StdOutReport::RuleEnd(engine::Result result, const engine::RuleInfo& ruleInfo, TraceTime::Duration duration) - { - // not required - } - - void StdOutReport::ScenarioStart(const engine::ScenarioInfo& scenarioInfo) - { - ++nrOfScenarios; - std::cout << "\n" - << scenarioInfo.Title(); - } - - void StdOutReport::ScenarioEnd(engine::Result result, const engine::ScenarioInfo& scenarioInfo, TraceTime::Duration duration) - { - - using enum engine::Result; - - std::cout << "\n" - << "\\-> "; - - auto& coloured = ((result == passed) ? std::cout << TcGreen : std::cout << TcRed); - - coloured << successLut.at(result) - << " (" << ScaledDuration(duration) << ")" - << TcDefault << '\n'; - - if (result != engine::Result::passed) - { - failedScenarios.emplace_back(&scenarioInfo); - } - } - - void StdOutReport::StepSkipped(const engine::StepInfo& stepInfo) - { - std::cout << "\n" - << Repeat("| ", nestedSteps) - << TcCyan; - std::cout << successLut.at(engine::Result::skipped) << " " << stepTypeLut.at(stepInfo.Type()) << " " << stepInfo.Text(); - std::cout << TcDefault; - } - - void StdOutReport::StepStart(const engine::StepInfo& stepInfo) - { - std::cout << "\n" - << Repeat("| ", nestedSteps) - << stepTypeLut.at(stepInfo.Type()) << " " << stepInfo.Text() - << std::flush; - ++nestedSteps; - } - - void StdOutReport::StepEnd(engine::Result result, const engine::StepInfo& stepInfo, TraceTime::Duration duration) - { - --nestedSteps; - - using enum engine::Result; - - std::cout << "\n" - << Repeat("| ", nestedSteps) << "\\-> "; - - if (result == passed) - std::cout << TcGreen; - else - std::cout << TcRed; - - std::cout << successLut.at(result); - - if (result != passed) - std::cout << " " << stepInfo.ScenarioInfo().FeatureInfo().Path() << ":" << stepInfo.Line() << ":" << stepInfo.Column(); - - std::cout << " (" << ScaledDuration(duration) << ")"; - - std::cout << TcDefault; - } - - void StdOutReport::Failure(const std::string& error, std::optional path, std::optional line, std::optional column) - { - std::cout << TcRed; - - if (path && line && column) - std::cout << std::format("\nFailure @ ./{}:{}:{}:", path.value().string(), line.value(), column.value()); - else if (path && line) - std::cout << std::format("\nFailure @ ./{}:{}:", path.value().string(), line.value()); - - std::cout << std::format("\n{}", error); - - std::cout << TcDefault; - } - - void StdOutReport::Error(const std::string& error, std::optional path, std::optional line, std::optional column) - { - std::cout << TcRed; - - if (path && line && column) - std::cout << std::format("\nError @ ./{}:{}:{}:", path.value().string(), line.value(), column.value()); - else if (path && line) - std::cout << std::format("\nError @ ./{}:{}:", path.value().string(), line.value()); - - std::cout << std::format("\n{}", error); - - std::cout << TcDefault; - } - - void StdOutReport::Trace(const std::string& trace) - { - std::cout << trace; - } - - void StdOutReport::Summary(TraceTime::Duration duration) - { - std::cout << "\n====================summary===================="; - std::cout << "\nduration: " << ScaledDuration(duration); - std::cout << "\ntests : " << (nrOfScenarios - failedScenarios.size()) << "/" << nrOfScenarios << " passed"; - - if (!failedScenarios.empty()) - { - std::cout << "\n\nfailed tests:"; - - for (const auto* scenarioInfo : failedScenarios) - std::cout << "\n" - << scenarioInfo->Path() << ":" << scenarioInfo->Line() << ":" << scenarioInfo->Column() << " : " << std::quoted(scenarioInfo->Title()); - } - } -} diff --git a/cucumber_cpp/library/report/StdOutReport.hpp b/cucumber_cpp/library/report/StdOutReport.hpp deleted file mode 100644 index ab392b3e..00000000 --- a/cucumber_cpp/library/report/StdOutReport.hpp +++ /dev/null @@ -1,52 +0,0 @@ -#ifndef REPORT_STDOUTREPORT_HPP -#define REPORT_STDOUTREPORT_HPP - -#include "cucumber_cpp/library/TraceTime.hpp" -#include "cucumber_cpp/library/engine/FeatureInfo.hpp" -#include "cucumber_cpp/library/engine/Result.hpp" -#include "cucumber_cpp/library/engine/RuleInfo.hpp" -#include "cucumber_cpp/library/engine/ScenarioInfo.hpp" -#include "cucumber_cpp/library/engine/StepInfo.hpp" -#include "cucumber_cpp/library/report/Report.hpp" -#include -#include -#include -#include -#include -#include - -namespace cucumber_cpp::library::report -{ - struct StdOutReport : ReportHandlerV2 - { - StdOutReport(); - - void FeatureStart(const engine::FeatureInfo& featureInfo) override; - void FeatureEnd(engine::Result result, const engine::FeatureInfo& featureInfo, TraceTime::Duration duration) override; - - void RuleStart(const engine::RuleInfo& ruleInfo) override; - void RuleEnd(engine::Result result, const engine::RuleInfo& ruleInfo, TraceTime::Duration duration) override; - - void ScenarioStart(const engine::ScenarioInfo& scenarioInfo) override; - void ScenarioEnd(engine::Result result, const engine::ScenarioInfo& scenarioInfo, TraceTime::Duration duration) override; - - void StepSkipped(const engine::StepInfo& stepInfo) override; - void StepStart(const engine::StepInfo& stepInfo) override; - void StepEnd(engine::Result result, const engine::StepInfo& stepInfo, 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; - - void Summary(TraceTime::Duration duration) override; - - private: - std::uint32_t nrOfScenarios{ 0 }; - std::vector failedScenarios; - - std::size_t nestedSteps{ 1 }; - }; -} - -#endif diff --git a/cucumber_cpp/library/report/test_helper/CMakeLists.txt b/cucumber_cpp/library/report/test_helper/CMakeLists.txt deleted file mode 100644 index 8791d028..00000000 --- a/cucumber_cpp/library/report/test_helper/CMakeLists.txt +++ /dev/null @@ -1,10 +0,0 @@ -add_library(cucumber_cpp.library.report.test_helper INTERFACE) - -target_link_libraries(cucumber_cpp.library.report.test_helper INTERFACE - cucumber_cpp.library.report - gmock_main -) - -target_sources(cucumber_cpp.library.report.test_helper INTERFACE - ReportForwarderMock.hpp -) diff --git a/cucumber_cpp/library/report/test_helper/ReportForwarderMock.hpp b/cucumber_cpp/library/report/test_helper/ReportForwarderMock.hpp deleted file mode 100644 index 8451dbfc..00000000 --- a/cucumber_cpp/library/report/test_helper/ReportForwarderMock.hpp +++ /dev/null @@ -1,23 +0,0 @@ -#ifndef TEST_HELPER_REPORTFORWARDERMOCK_HPP -#define TEST_HELPER_REPORTFORWARDERMOCK_HPP - -#include "cucumber_cpp/library/report/Report.hpp" -#include -#include -#include -#include -#include - -namespace cucumber_cpp::library::report::test_helper -{ - struct ReportForwarderMock : report::ReportForwarderImpl - { - using ReportForwarderImpl::ReportForwarderImpl; - virtual ~ReportForwarderMock() = default; - - MOCK_METHOD(void, Failure, (const std::string& error, std::optional path, std::optional line, std::optional column), (override)); - MOCK_METHOD(void, Error, (const std::string& error, std::optional path, std::optional line, std::optional column), (override)); - }; -} - -#endif diff --git a/cucumber_cpp/library/runtime/CMakeLists.txt b/cucumber_cpp/library/runtime/CMakeLists.txt new file mode 100644 index 00000000..ce295e84 --- /dev/null +++ b/cucumber_cpp/library/runtime/CMakeLists.txt @@ -0,0 +1,33 @@ +add_library(cucumber_cpp.library.runtime ${CCR_EXCLUDE_FROM_ALL}) + +target_sources(cucumber_cpp.library.runtime PRIVATE + Coordinator.cpp + Coordinator.hpp + MakeRuntime.cpp + MakeRuntime.hpp + NestedTestCaseRunner.cpp + NestedTestCaseRunner.hpp + SerialRuntimeAdapter.cpp + SerialRuntimeAdapter.hpp + TestCaseRunner.cpp + TestCaseRunner.hpp + Worker.cpp + Worker.hpp +) + +target_include_directories(cucumber_cpp.library.runtime PUBLIC + ../../.. +) + +target_link_libraries(cucumber_cpp.library.runtime PUBLIC + cucumber_cpp.library.assemble + cucumber_cpp.library.cucumber_expression + cucumber_cpp.library.support + cucumber_cpp.library.util + fmt::fmt +) + +if (CCR_BUILD_TESTS) + add_subdirectory(test) + # add_subdirectory(test_helper) +endif() diff --git a/cucumber_cpp/library/runtime/Coordinator.cpp b/cucumber_cpp/library/runtime/Coordinator.cpp new file mode 100644 index 00000000..59defbe4 --- /dev/null +++ b/cucumber_cpp/library/runtime/Coordinator.cpp @@ -0,0 +1,46 @@ +#include "cucumber_cpp/library/runtime/Coordinator.hpp" +#include "cucumber/gherkin/id_generator.hpp" +#include "cucumber/messages/test_run_finished.hpp" +#include "cucumber/messages/test_run_started.hpp" +#include "cucumber_cpp/library/assemble/AssembleTestSuites.hpp" +#include "cucumber_cpp/library/support/SupportCodeLibrary.hpp" +#include "cucumber_cpp/library/support/Types.hpp" +#include "cucumber_cpp/library/util/Broadcaster.hpp" +#include "cucumber_cpp/library/util/Timestamp.hpp" +#include +#include +#include +#include + +namespace cucumber_cpp::library::runtime +{ + Coordinator::Coordinator(std::string testRunStartedId, + util::Broadcaster& broadcaster, + cucumber::gherkin::id_generator_ptr idGenerator, + std::unique_ptr&& runtimeAdapter, + support::SupportCodeLibrary& supportCodeLibrary) + : testRunStartedId{ testRunStartedId } + , broadcaster{ broadcaster } + , idGenerator{ idGenerator } + , runtimeAdapter{ std::move(runtimeAdapter) } + , supportCodeLibrary{ supportCodeLibrary } + {} + + bool Coordinator::Run() + { + broadcaster.BroadcastEvent({ .test_run_started = cucumber::messages::test_run_started{ + .timestamp = util::TimestampNow(), + .id = std::string{ testRunStartedId }, + } }); + + const auto success = runtimeAdapter->Run(); + + broadcaster.BroadcastEvent({ .test_run_finished = cucumber::messages::test_run_finished{ + .success = success, + .timestamp = util::TimestampNow(), + .test_run_started_id = std::string{ testRunStartedId }, + } }); + + return success; + } +} diff --git a/cucumber_cpp/library/runtime/Coordinator.hpp b/cucumber_cpp/library/runtime/Coordinator.hpp new file mode 100644 index 00000000..5124b3eb --- /dev/null +++ b/cucumber_cpp/library/runtime/Coordinator.hpp @@ -0,0 +1,33 @@ +#ifndef RUNTIME_COORDINATOR_HPP +#define RUNTIME_COORDINATOR_HPP + +#include "cucumber/gherkin/id_generator.hpp" +#include "cucumber_cpp/library/support/SupportCodeLibrary.hpp" +#include "cucumber_cpp/library/support/Types.hpp" +#include "cucumber_cpp/library/util/Broadcaster.hpp" +#include +#include +#include + +namespace cucumber_cpp::library::runtime +{ + struct Coordinator : support::Runtime + { + Coordinator(std::string testRunStartedId, + util::Broadcaster& broadcaster, + cucumber::gherkin::id_generator_ptr idGenerator, + std::unique_ptr&& runtimeAdapter, + support::SupportCodeLibrary& supportCodeLibrary); + + bool Run() override; + + private: + std::string testRunStartedId; + util::Broadcaster& broadcaster; + cucumber::gherkin::id_generator_ptr idGenerator; + std::unique_ptr runtimeAdapter; + support::SupportCodeLibrary& supportCodeLibrary; + }; +} + +#endif diff --git a/cucumber_cpp/library/runtime/MakeRuntime.cpp b/cucumber_cpp/library/runtime/MakeRuntime.cpp new file mode 100644 index 00000000..04b88d83 --- /dev/null +++ b/cucumber_cpp/library/runtime/MakeRuntime.cpp @@ -0,0 +1,37 @@ +#include "cucumber_cpp/library/runtime/MakeRuntime.hpp" +#include "cucumber/gherkin/id_generator.hpp" +#include "cucumber_cpp/library/Context.hpp" +#include "cucumber_cpp/library/runtime/Coordinator.hpp" +#include "cucumber_cpp/library/runtime/SerialRuntimeAdapter.hpp" +#include "cucumber_cpp/library/support/SupportCodeLibrary.hpp" +#include "cucumber_cpp/library/support/Types.hpp" +#include "cucumber_cpp/library/util/Broadcaster.hpp" +#include +#include +#include + +namespace cucumber_cpp::library::runtime +{ + std::unique_ptr MakeAdapter(const support::RunOptions::Runtime& options, std::string testRunStartedId, util::Broadcaster& broadcaster, const std::list& sourcedPickles, support::SupportCodeLibrary& supportCodeLibrary, cucumber::gherkin::id_generator_ptr idGenerator, Context& programContext) + { + return std::make_unique( + testRunStartedId, + broadcaster, + idGenerator, + sourcedPickles, + options, + supportCodeLibrary, + programContext); + } + + std::unique_ptr MakeRuntime(const support::RunOptions::Runtime& options, util::Broadcaster& broadcaster, const std::list& sourcedPickles, support::SupportCodeLibrary& supportCodeLibrary, cucumber::gherkin::id_generator_ptr idGenerator, Context& programContext) + { + const auto testRunStartedId{ idGenerator->next_id() }; + return std::make_unique( + testRunStartedId, + broadcaster, + idGenerator, + MakeAdapter(options, testRunStartedId, broadcaster, sourcedPickles, supportCodeLibrary, idGenerator, programContext), + supportCodeLibrary); + } +} diff --git a/cucumber_cpp/library/runtime/MakeRuntime.hpp b/cucumber_cpp/library/runtime/MakeRuntime.hpp new file mode 100644 index 00000000..3b3f5953 --- /dev/null +++ b/cucumber_cpp/library/runtime/MakeRuntime.hpp @@ -0,0 +1,20 @@ +#ifndef RUNTIME_MAKE_RUNTIME_HPP +#define RUNTIME_MAKE_RUNTIME_HPP + +#include "cucumber/gherkin/id_generator.hpp" +#include "cucumber_cpp/library/Context.hpp" +#include "cucumber_cpp/library/support/SupportCodeLibrary.hpp" +#include "cucumber_cpp/library/support/Types.hpp" +#include "cucumber_cpp/library/util/Broadcaster.hpp" +#include +#include +#include + +namespace cucumber_cpp::library::runtime +{ + std::unique_ptr MakeAdapter(const support::RunOptions::Runtime& options, std::string testRunStartedId, util::Broadcaster& broadcaster, support::SupportCodeLibrary& supportCodeLibrary, cucumber::gherkin::id_generator_ptr idGenerator, Context& programContext); + + std::unique_ptr MakeRuntime(const support::RunOptions::Runtime& options, util::Broadcaster& broadcaster, const std::list& sourcedPickles, support::SupportCodeLibrary& supportCodeLibrary, cucumber::gherkin::id_generator_ptr idGenerator, Context& programContext); +} + +#endif diff --git a/cucumber_cpp/library/runtime/NestedTestCaseRunner.cpp b/cucumber_cpp/library/runtime/NestedTestCaseRunner.cpp new file mode 100644 index 00000000..7823b9f0 --- /dev/null +++ b/cucumber_cpp/library/runtime/NestedTestCaseRunner.cpp @@ -0,0 +1,133 @@ +#include "cucumber_cpp/library/runtime/NestedTestCaseRunner.hpp" +#include "cucumber/messages/duration.hpp" +#include "cucumber/messages/pickle_doc_string.hpp" +#include "cucumber/messages/pickle_table.hpp" +#include "cucumber/messages/step_match_arguments_list.hpp" +#include "cucumber/messages/test_step.hpp" +#include "cucumber/messages/test_step_result.hpp" +#include "cucumber/messages/test_step_result_status.hpp" +#include "cucumber/messages/test_step_started.hpp" +#include "cucumber_cpp/library/Context.hpp" +#include "cucumber_cpp/library/cucumber_expression/Argument.hpp" +#include "cucumber_cpp/library/cucumber_expression/Matcher.hpp" +#include "cucumber_cpp/library/support/Body.hpp" +#include "cucumber_cpp/library/support/StepRegistry.hpp" +#include "cucumber_cpp/library/support/SupportCodeLibrary.hpp" +#include "cucumber_cpp/library/util/Broadcaster.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace cucumber_cpp::library::runtime +{ + namespace + { + auto TransformToMatch(const std::string& text) + { + return [&text](const support::StepRegistry::Definition& definition) -> std::pair>> + { + const auto match = std::visit(cucumber_expression::MatchVisitor{ text }, definition.regex); + return { definition.id, match }; + }; + } + + bool HasMatch(const std::pair>>& pair) + { + return pair.second.has_value(); + } + + cucumber::messages::test_step Assemble(const std::string& step, const support::SupportCodeLibrary& supportCodeLibrary, const cucumber::messages::test_step_started& testStepStarted) + { + cucumber::messages::test_step testStep{ + .id = testStepStarted.test_step_id, + .step_definition_ids = std::vector{}, + .step_match_arguments_lists = std::vector{} + }; + + const auto& stepDefinitions = supportCodeLibrary.stepRegistry.StepDefinitions(); + + for (const auto& [id, match] : stepDefinitions | + std::views::transform(TransformToMatch(step)) | + std::views::filter(HasMatch)) + { + testStep.step_definition_ids.value().push_back(id); + auto& argumentList = testStep.step_match_arguments_lists.value().emplace_back(); + for (const auto& result : *match) + argumentList.step_match_arguments.emplace_back(result.Group(), result.Name().empty() ? std::nullopt : std::make_optional(result.Name())); + } + + return testStep; + } + + void Invoke(std::size_t nesting, const std::string& step, std::unique_ptr body, const cucumber::messages::step_match_arguments_list& args) + { + const auto status = body->ExecuteAndCatchExceptions(args); + if (status.status != cucumber::messages::test_step_result_status::PASSED) + throw NestedTestCaseRunnerError{ nesting, status, step }; + } + + void Run(std::size_t nesting, const std::string& step, const cucumber::messages::test_step& testStep, const support::SupportCodeLibrary& supportCodeLibrary, util::Broadcaster& broadcaster, Context& testCaseContext, const cucumber::messages::test_step_started& testStepStarted, const std::optional& dataTable, const std::optional& docString) + { + auto stepDefinitions = (*testStep.step_definition_ids) | std::views::transform([&supportCodeLibrary](const std::string& id) + { + return supportCodeLibrary.stepRegistry.GetDefinitionById(id); + }); + + if (testStep.step_definition_ids->size() == 0) + throw NestedTestCaseRunnerError{ nesting, { + .duration = cucumber::messages::duration{}, + .status = cucumber::messages::test_step_result_status::UNDEFINED, + }, + step }; + + else if (testStep.step_definition_ids->size() > 1) + throw NestedTestCaseRunnerError{ nesting, { + .duration = cucumber::messages::duration{}, + .message = "Ambiguous step definitions", + .status = cucumber::messages::test_step_result_status::AMBIGUOUS, + }, + step }; + else + { + const auto& definition = stepDefinitions.front(); + Invoke(nesting, step, definition.factory(NestedTestCaseRunner{ nesting, supportCodeLibrary, broadcaster, testCaseContext, testStepStarted }, broadcaster, testCaseContext, testStepStarted, dataTable, docString), testStep.step_match_arguments_lists->front()); + } + } + } + + NestedTestCaseRunner::NestedTestCaseRunner(std::size_t nesting, const support::SupportCodeLibrary& supportCodeLibrary, util::Broadcaster& broadcaster, Context& testCaseContext, const cucumber::messages::test_step_started& testStepStarted) + : nesting{ nesting + 1 } + , supportCodeLibrary{ supportCodeLibrary } + , broadcaster{ broadcaster } + , testCaseContext{ testCaseContext } + , testStepStarted{ testStepStarted } + {} + + void NestedTestCaseRunner::Step(const std::string& step) const + { + Step(step, std::nullopt, std::nullopt); + } + + void NestedTestCaseRunner::Step(const std::string& step, const std::optional& docString) const + { + Step(step, std::nullopt, docString); + } + + void NestedTestCaseRunner::Step(const std::string& step, const std::optional& dataTable) const + { + Step(step, dataTable, std::nullopt); + } + + void NestedTestCaseRunner::Step(const std::string& step, const std::optional& dataTable, const std::optional& docString) const + { + const auto testStep = Assemble(step, supportCodeLibrary, testStepStarted); + Run(nesting, step, testStep, supportCodeLibrary, broadcaster, testCaseContext, testStepStarted, dataTable, docString); + } +} diff --git a/cucumber_cpp/library/runtime/NestedTestCaseRunner.hpp b/cucumber_cpp/library/runtime/NestedTestCaseRunner.hpp new file mode 100644 index 00000000..341196d8 --- /dev/null +++ b/cucumber_cpp/library/runtime/NestedTestCaseRunner.hpp @@ -0,0 +1,42 @@ +#ifndef RUNTIME_NESTED_TEST_CASE_RUNNER_HPP +#define RUNTIME_NESTED_TEST_CASE_RUNNER_HPP + +#include "cucumber/messages/pickle_doc_string.hpp" +#include "cucumber/messages/pickle_table.hpp" +#include "cucumber/messages/test_step_result.hpp" +#include "cucumber/messages/test_step_started.hpp" +#include "cucumber_cpp/library/Context.hpp" +#include "cucumber_cpp/library/support/SupportCodeLibrary.hpp" +#include "cucumber_cpp/library/util/Broadcaster.hpp" +#include +#include +#include + +namespace cucumber_cpp::library::runtime +{ + struct NestedTestCaseRunnerError + { + std::size_t nesting; + cucumber::messages::test_step_result status; + std::string text; + }; + + struct NestedTestCaseRunner + { + NestedTestCaseRunner(std::size_t nesting, const support::SupportCodeLibrary& supportCodeLibrary, util::Broadcaster& broadcaster, Context& testCaseContext, const cucumber::messages::test_step_started& testStepStarted); + + void Step(const std::string& step) const; + void Step(const std::string& step, const std::optional& docString) const; + void Step(const std::string& step, const std::optional& dataTable) const; + void Step(const std::string& step, const std::optional& dataTable, const std::optional& docString) const; + + private: + std::size_t nesting; + const support::SupportCodeLibrary& supportCodeLibrary; + util::Broadcaster& broadcaster; + Context& testCaseContext; + const cucumber::messages::test_step_started& testStepStarted; + }; +} + +#endif diff --git a/cucumber_cpp/library/runtime/SerialRuntimeAdapter.cpp b/cucumber_cpp/library/runtime/SerialRuntimeAdapter.cpp new file mode 100644 index 00000000..c78f0cd9 --- /dev/null +++ b/cucumber_cpp/library/runtime/SerialRuntimeAdapter.cpp @@ -0,0 +1,69 @@ +#include "cucumber_cpp/library/runtime/SerialRuntimeAdapter.hpp" +#include "cucumber/gherkin/id_generator.hpp" +#include "cucumber/messages/test_step_result_status.hpp" +#include "cucumber_cpp/library/Context.hpp" +#include "cucumber_cpp/library/assemble/AssembleTestSuites.hpp" +#include "cucumber_cpp/library/runtime/Worker.hpp" +#include "cucumber_cpp/library/support/SupportCodeLibrary.hpp" +#include "cucumber_cpp/library/support/Types.hpp" +#include "cucumber_cpp/library/util/Broadcaster.hpp" +#include "cucumber_cpp/library/util/GetWorstTestStepResult.hpp" +#include +#include +#include + +namespace cucumber_cpp::library::runtime +{ + SerialRuntimeAdapter::SerialRuntimeAdapter(std::string testRunStartedId, + util::Broadcaster& broadcaster, + cucumber::gherkin::id_generator_ptr idGenerator, + const std::list& sourcedPickles, + const support::RunOptions::Runtime& options, + support::SupportCodeLibrary& supportCodeLibrary, + Context& programContext) + : testRunStartedId{ testRunStartedId } + , broadcaster{ broadcaster } + , idGenerator{ idGenerator } + , sourcedPickles{ sourcedPickles } + , options{ options } + , supportCodeLibrary{ supportCodeLibrary } + , programContext{ programContext } + {} + + bool SerialRuntimeAdapter::Run() + { + bool failing = false; + runtime::Worker worker{ testRunStartedId, broadcaster, idGenerator, options, supportCodeLibrary, programContext }; + + const auto beforeHookResults = worker.RunBeforeAllHooks(); + + if (util::GetWorstTestStepResult(beforeHookResults).status != cucumber::messages::test_step_result_status::PASSED) + failing = true; + + if (!failing) + { + auto assembledTestSuites = assemble::AssembleTestSuites(supportCodeLibrary, testRunStartedId, broadcaster, sourcedPickles, idGenerator); + + for (const auto& assembledTestSuite : assembledTestSuites) + { + try + { + const auto success = worker.RunTestSuite(assembledTestSuite, failing); + if (!success) + failing = true; + } + catch (...) + { + failing = true; + } + } + } + + const auto afterHookResults = worker.RunAfterAllHooks(); + + if (util::GetWorstTestStepResult(afterHookResults).status != cucumber::messages::test_step_result_status::PASSED) + failing = true; + + return !failing; + } +} diff --git a/cucumber_cpp/library/runtime/SerialRuntimeAdapter.hpp b/cucumber_cpp/library/runtime/SerialRuntimeAdapter.hpp new file mode 100644 index 00000000..0321b3ec --- /dev/null +++ b/cucumber_cpp/library/runtime/SerialRuntimeAdapter.hpp @@ -0,0 +1,37 @@ +#ifndef RUNTIME_SERIAL_RUNTIME_ADAPTER_HPP +#define RUNTIME_SERIAL_RUNTIME_ADAPTER_HPP + +#include "cucumber/gherkin/id_generator.hpp" +#include "cucumber_cpp/library/Context.hpp" +#include "cucumber_cpp/library/support/SupportCodeLibrary.hpp" +#include "cucumber_cpp/library/support/Types.hpp" +#include "cucumber_cpp/library/util/Broadcaster.hpp" +#include +#include + +namespace cucumber_cpp::library::runtime +{ + struct SerialRuntimeAdapter : support::RuntimeAdapter + { + SerialRuntimeAdapter(std::string testRunStartedId, + util::Broadcaster& broadcaster, + cucumber::gherkin::id_generator_ptr idGenerator, + const std::list& sourcedPickles, + const support::RunOptions::Runtime& options, + support::SupportCodeLibrary& supportCodeLibrary, + Context& programContext); + + bool Run() override; + + private: + std::string testRunStartedId; + util::Broadcaster& broadcaster; + cucumber::gherkin::id_generator_ptr idGenerator; + const std::list& sourcedPickles; + const support::RunOptions::Runtime& options; + support::SupportCodeLibrary& supportCodeLibrary; + Context& programContext; + }; +} + +#endif diff --git a/cucumber_cpp/library/runtime/TestCaseRunner.cpp b/cucumber_cpp/library/runtime/TestCaseRunner.cpp new file mode 100644 index 00000000..75703f58 --- /dev/null +++ b/cucumber_cpp/library/runtime/TestCaseRunner.cpp @@ -0,0 +1,249 @@ + +#include "cucumber_cpp/library/runtime/TestCaseRunner.hpp" +#include "cucumber/gherkin/id_generator.hpp" +#include "cucumber/messages/duration.hpp" +#include "cucumber/messages/gherkin_document.hpp" +#include "cucumber/messages/pickle.hpp" +#include "cucumber/messages/pickle_step.hpp" +#include "cucumber/messages/step_match_arguments_list.hpp" +#include "cucumber/messages/suggestion.hpp" +#include "cucumber/messages/test_case.hpp" +#include "cucumber/messages/test_case_finished.hpp" +#include "cucumber/messages/test_case_started.hpp" +#include "cucumber/messages/test_step.hpp" +#include "cucumber/messages/test_step_finished.hpp" +#include "cucumber/messages/test_step_result.hpp" +#include "cucumber/messages/test_step_result_status.hpp" +#include "cucumber/messages/test_step_started.hpp" +#include "cucumber_cpp/library/Context.hpp" +#include "cucumber_cpp/library/runtime/NestedTestCaseRunner.hpp" +#include "cucumber_cpp/library/support/Body.hpp" +#include "cucumber_cpp/library/support/HookRegistry.hpp" +#include "cucumber_cpp/library/support/StepRegistry.hpp" +#include "cucumber_cpp/library/support/SupportCodeLibrary.hpp" +#include "cucumber_cpp/library/util/Broadcaster.hpp" +#include "cucumber_cpp/library/util/Duration.hpp" +#include "cucumber_cpp/library/util/GetWorstTestStepResult.hpp" +#include "cucumber_cpp/library/util/Timestamp.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace cucumber_cpp::library::runtime +{ + TestCaseRunner::TestCaseRunner(util::Broadcaster& broadcaster, + cucumber::gherkin::id_generator_ptr idGenerator, + const cucumber::messages::gherkin_document& gherkinDocument, + const cucumber::messages::pickle& pickle, + const cucumber::messages::test_case& testCase, + std::size_t retries, + bool skip, + support::SupportCodeLibrary& supportCodeLibrary, + Context& testSuiteContext) + : broadcaster{ broadcaster } + , idGenerator{ idGenerator } + , gherkinDocument{ gherkinDocument } + , pickle{ pickle } + , testCase{ testCase } + , maximumAttempts{ 1 + (skip ? 0 : retries) } + , skip{ skip } + , supportCodeLibrary{ supportCodeLibrary } + , testSuiteContext{ testSuiteContext } + {} + + cucumber::messages::test_step_result_status TestCaseRunner::Run() + { + for (std::size_t attempt = 0; attempt < maximumAttempts; ++attempt) + { + testStepResults.clear(); + + const auto willRetry = RunAttempt(attempt, (attempt + 1) < maximumAttempts); + + if (willRetry) + continue; + + return GetWorstStepResult().status; + } + + return cucumber::messages::test_step_result_status::UNKNOWN; + } + + bool TestCaseRunner::RunAttempt(std::size_t attempt, bool moreAttemptsAvailable) + { + Context testCaseContext{ &testSuiteContext }; + const auto currentTestCaseStartedId = idGenerator->next_id(); + bool willRetry = false; + + broadcaster.BroadcastEvent({ .test_case_started = cucumber::messages::test_case_started{ + .attempt = attempt, + .id = currentTestCaseStartedId, + .test_case_id = testCase.id, + .timestamp = util::TimestampNow(), + } }); + + bool seenSteps = false; + bool error = false; + + for (const auto& testStep : testCase.test_steps) + { + auto testStepStarted = cucumber::messages::test_step_started{ + .test_case_started_id = currentTestCaseStartedId, + .test_step_id = testStep.id, + .timestamp = util::TimestampNow(), + }; + broadcaster.BroadcastEvent({ .test_step_started = testStepStarted }); + + cucumber::messages::test_step_result testStepResult; + + if (testStep.hook_id) + { + testStepResult = RunHook(supportCodeLibrary.hookRegistry.GetDefinitionById(testStep.hook_id.value()), !seenSteps, testCaseContext, testStepStarted); + } + else + { + auto pickleStepIter = std::ranges::find(pickle.steps, testStep.pickle_step_id.value(), &cucumber::messages::pickle_step::id); + testStepResult = RunStep(*pickleStepIter, testStep, testCaseContext, testStepStarted); + seenSteps = true; + } + testStepResults.emplace_back(testStepResult); + + broadcaster.BroadcastEvent({ .test_step_finished = cucumber::messages::test_step_finished{ + .test_case_started_id = currentTestCaseStartedId, + .test_step_id = testStep.id, + .test_step_result = testStepResult, + .timestamp = util::TimestampNow(), + } }); + } + + willRetry = GetWorstStepResult().status == cucumber::messages::test_step_result_status::FAILED && moreAttemptsAvailable; + + broadcaster.BroadcastEvent({ .test_case_finished = cucumber::messages::test_case_finished{ + .test_case_started_id = currentTestCaseStartedId, + .timestamp = util::TimestampNow(), + .will_be_retried = willRetry, + } }); + + return willRetry; + } + + cucumber::messages::test_step_result TestCaseRunner::RunHook(const support::HookRegistry::Definition& hookDefinition, bool isBeforeHook, Context& testCaseContext, cucumber::messages::test_step_started testStepStarted) + { + if (ShouldSkipHook(isBeforeHook)) + return { + .duration = cucumber::messages::duration{}, + .status = cucumber::messages::test_step_result_status::SKIPPED, + }; + + return InvokeStep(hookDefinition.factory(broadcaster, testCaseContext, testStepStarted)); + } + + std::vector TestCaseRunner::RunStepHooks(const cucumber::messages::pickle_step& pickleStep, support::HookType hookType, Context& testCaseContext, cucumber::messages::test_step_started testStepStarted) + { + auto ids = supportCodeLibrary.hookRegistry.FindIds(hookType, pickle.tags); + std::vector results; + results.reserve(ids.size()); + + for (const auto& id : ids) + { + const auto& definition = supportCodeLibrary.hookRegistry.GetDefinitionById(id); + results.emplace_back(InvokeStep(definition.factory(broadcaster, testCaseContext, testStepStarted))); + } + + return results; + } + + cucumber::messages::test_step_result TestCaseRunner::RunStep(const cucumber::messages::pickle_step& pickleStep, const cucumber::messages::test_step& testStep, Context& testCaseContext, cucumber::messages::test_step_started testStepStarted) + { + auto stepDefinitions = (*testStep.step_definition_ids) | std::views::transform([this](const std::string& id) + { + return supportCodeLibrary.stepRegistry.GetDefinitionById(id); + }); + + if (const auto count = testStep.step_definition_ids->size(); count == 0) + { + broadcaster.BroadcastEvent({ .suggestion = cucumber::messages::suggestion{ + .id = idGenerator->next_id(), + .pickle_step_id = pickleStep.id, + .snippets = {}, + } }); + + return { + .duration = cucumber::messages::duration{}, + .status = cucumber::messages::test_step_result_status::UNDEFINED, + }; + } + else if (count > 1) + { + return { + .duration = cucumber::messages::duration{}, + .message = "Ambiguous step definitions", + .status = cucumber::messages::test_step_result_status::AMBIGUOUS, + }; + } + else if (IsSkippingSteps()) + { + return { + .duration = cucumber::messages::duration{}, + .status = cucumber::messages::test_step_result_status::SKIPPED, + }; + } + + auto stepResults = RunStepHooks(pickleStep, support::HookType::beforeStep, testCaseContext, testStepStarted); + + if (util::GetWorstTestStepResult(stepResults).status != cucumber::messages::test_step_result_status::FAILED) + { + const auto& dataTable = pickleStep.argument ? pickleStep.argument->data_table : std::nullopt; + const auto& docString = pickleStep.argument ? pickleStep.argument->doc_string : std::nullopt; + + const auto& definition = stepDefinitions.front(); + const auto result = InvokeStep(definition.factory( + NestedTestCaseRunner{ 0, supportCodeLibrary, broadcaster, testCaseContext, testStepStarted }, + broadcaster, testCaseContext, testStepStarted, dataTable, docString), + testStep.step_match_arguments_lists->front()); + stepResults.push_back(result); + } + + const auto afterStepHookResults = RunStepHooks(pickleStep, support::HookType::afterStep, testCaseContext, testStepStarted); + stepResults.reserve(stepResults.size() + afterStepHookResults.size()); + stepResults.insert(stepResults.end(), afterStepHookResults.begin(), afterStepHookResults.end()); + + auto finalStepResult = util::GetWorstTestStepResult(stepResults); + + cucumber::messages::duration finalDuration{}; + for (const auto& stepResult : stepResults) + finalDuration += stepResult.duration; + + finalStepResult.duration = finalDuration; + return finalStepResult; + } + + cucumber::messages::test_step_result TestCaseRunner::InvokeStep(std::unique_ptr body, const cucumber::messages::step_match_arguments_list& args) + { + return body->ExecuteAndCatchExceptions(args); + } + + cucumber::messages::test_step_result TestCaseRunner::GetWorstStepResult() const + { + if (testStepResults.empty()) + return { + .status = skip ? cucumber::messages::test_step_result_status::SKIPPED : cucumber::messages::test_step_result_status::PASSED, + }; + + return util::GetWorstTestStepResult(testStepResults); + } + + bool TestCaseRunner::ShouldSkipHook(bool isBeforeHook) + { + return skip || (IsSkippingSteps() && isBeforeHook); + } + + bool TestCaseRunner::IsSkippingSteps() + { + return GetWorstStepResult().status != cucumber::messages::test_step_result_status::PASSED; + } +} diff --git a/cucumber_cpp/library/runtime/TestCaseRunner.hpp b/cucumber_cpp/library/runtime/TestCaseRunner.hpp new file mode 100644 index 00000000..2cc8d709 --- /dev/null +++ b/cucumber_cpp/library/runtime/TestCaseRunner.hpp @@ -0,0 +1,68 @@ +#ifndef RUNTIME_TEST_CASE_RUNNER_HPP +#define RUNTIME_TEST_CASE_RUNNER_HPP + +#include "cucumber/gherkin/id_generator.hpp" +#include "cucumber/messages/gherkin_document.hpp" +#include "cucumber/messages/pickle.hpp" +#include "cucumber/messages/pickle_step.hpp" +#include "cucumber/messages/step_match_arguments_list.hpp" +#include "cucumber/messages/test_case.hpp" +#include "cucumber/messages/test_step.hpp" +#include "cucumber/messages/test_step_result.hpp" +#include "cucumber/messages/test_step_result_status.hpp" +#include "cucumber/messages/test_step_started.hpp" +#include "cucumber_cpp/library/Context.hpp" +#include "cucumber_cpp/library/support/Body.hpp" +#include "cucumber_cpp/library/support/HookRegistry.hpp" +#include "cucumber_cpp/library/support/SupportCodeLibrary.hpp" +#include "cucumber_cpp/library/util/Broadcaster.hpp" +#include +#include +#include + +namespace cucumber_cpp::library::runtime +{ + struct TestCaseRunner + { + TestCaseRunner(util::Broadcaster& broadcaster, + cucumber::gherkin::id_generator_ptr idGenerator, + const cucumber::messages::gherkin_document& gherkinDocument, + const cucumber::messages::pickle& pickle, + const cucumber::messages::test_case& testCase, + std::size_t retries, + bool skip, + support::SupportCodeLibrary& supportCodeLibrary, + Context& testSuiteContext); + + cucumber::messages::test_step_result_status Run(); + + bool RunAttempt(std::size_t attempt, bool moreAttemptsAvailable); + + cucumber::messages::test_step_result RunHook(const support::HookRegistry::Definition& hookDefinition, bool isBeforeHook, Context& testCaseContext, cucumber::messages::test_step_started testStepStarted); + + std::vector RunStepHooks(const cucumber::messages::pickle_step& pickleStep, support::HookType hookType, Context& testCaseContext, cucumber::messages::test_step_started testStepStarted); + + cucumber::messages::test_step_result RunStep(const cucumber::messages::pickle_step& pickleStep, const cucumber::messages::test_step& testStep, Context& testCaseContext, cucumber::messages::test_step_started testStepStarted); + + cucumber::messages::test_step_result InvokeStep(std::unique_ptr body, const cucumber::messages::step_match_arguments_list& args = {}); + cucumber::messages::test_step_result GetWorstStepResult() const; + + bool ShouldSkipHook(bool isBeforeHook); + bool IsSkippingSteps(); + + private: + util::Broadcaster& broadcaster; + cucumber::gherkin::id_generator_ptr idGenerator; + const cucumber::messages::gherkin_document& gherkinDocument; + const cucumber::messages::pickle& pickle; + const cucumber::messages::test_case& testCase; + std::size_t maximumAttempts; + bool skip; + support::SupportCodeLibrary& supportCodeLibrary; + Context& testSuiteContext; + + std::vector testStepResults; + }; +} + +#endif diff --git a/cucumber_cpp/library/runtime/Worker.cpp b/cucumber_cpp/library/runtime/Worker.cpp new file mode 100644 index 00000000..adb95c9e --- /dev/null +++ b/cucumber_cpp/library/runtime/Worker.cpp @@ -0,0 +1,182 @@ +#include "cucumber_cpp/library/runtime/Worker.hpp" +#include "cucumber/gherkin/id_generator.hpp" +#include "cucumber/messages/feature.hpp" +#include "cucumber/messages/gherkin_document.hpp" +#include "cucumber/messages/pickle.hpp" +#include "cucumber/messages/test_run_hook_finished.hpp" +#include "cucumber/messages/test_run_hook_started.hpp" +#include "cucumber/messages/test_step_result.hpp" +#include "cucumber/messages/test_step_result_status.hpp" +#include "cucumber_cpp/library/Context.hpp" +#include "cucumber_cpp/library/assemble/AssembledTestCase.hpp" +#include "cucumber_cpp/library/assemble/AssembledTestSuite.hpp" +#include "cucumber_cpp/library/runtime/TestCaseRunner.hpp" +#include "cucumber_cpp/library/support/HookRegistry.hpp" +#include "cucumber_cpp/library/support/SupportCodeLibrary.hpp" +#include "cucumber_cpp/library/support/Types.hpp" +#include "cucumber_cpp/library/util/Broadcaster.hpp" +#include "cucumber_cpp/library/util/GetWorstTestStepResult.hpp" +#include "cucumber_cpp/library/util/Timestamp.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace cucumber_cpp::library::runtime +{ + namespace + { + const inline std::set failingStatuses{ + cucumber::messages::test_step_result_status::AMBIGUOUS, + cucumber::messages::test_step_result_status::FAILED, + cucumber::messages::test_step_result_status::UNDEFINED, + }; + + std::size_t RetriesForPickle(const cucumber::messages::pickle& pickle, const support::RunOptions::Runtime& options) + { + if (options.retry == 0) + return 0; + else if (options.retryTagExpression->Evaluate(pickle.tags)) + return options.retry; + else + return 0; + } + } + + Worker::Worker(std::string_view testRunStartedId, + util::Broadcaster& broadcaster, + cucumber::gherkin::id_generator_ptr idGenerator, + const support::RunOptions::Runtime& options, + support::SupportCodeLibrary& supportCodeLibrary, + Context& programContext) + : testRunStartedId{ testRunStartedId } + , broadcaster{ broadcaster } + , idGenerator{ idGenerator } + , options{ options } + , supportCodeLibrary{ supportCodeLibrary } + , programContext{ programContext } + {} + + std::vector Worker::RunBeforeAllHooks() + { + std::vector results; + const auto ids = supportCodeLibrary.hookRegistry.FindIds(support::HookType::beforeAll); + for (const auto& id : ids) + results.emplace_back(RunTestHook(id, programContext)); + + return results; + } + + std::vector Worker::RunAfterAllHooks() + { + std::vector results; + auto ids = supportCodeLibrary.hookRegistry.FindIds(support::HookType::afterAll); + for (const auto& id : ids | std::views::reverse) + results.emplace_back(RunTestHook(id, programContext)); + + return results; + } + + bool Worker::RunTestSuite(const assemble::AssembledTestSuite& assembledTestSuite, bool failing) + { + Context testSuiteContext{ &programContext }; + + RunBeforeTestSuiteHooks(*assembledTestSuite.gherkinDocument.feature, testSuiteContext); + + auto failed = false; + for (const auto& assembledTestCase : assembledTestSuite.testCases) + failed |= !RunTestCase(assembledTestSuite.gherkinDocument, assembledTestCase, testSuiteContext, failing); + + RunAfterTestSuiteHooks(*assembledTestSuite.gherkinDocument.feature, testSuiteContext); + + return !failed; + } + + bool Worker::RunTestCase(const cucumber::messages::gherkin_document& gherkinDocument, const assemble::AssembledTestCase& assembledTestCase, Context& testSuiteContext, bool failing) + { + TestCaseRunner testCaseRunner{ + broadcaster, + idGenerator, + gherkinDocument, + assembledTestCase.pickle, + assembledTestCase.testCase, + RetriesForPickle(assembledTestCase.pickle, options), + options.dryRun || (options.failFast && failing), + supportCodeLibrary, + testSuiteContext, + }; + + const auto status = testCaseRunner.Run(); + + return !IsStatusFailed(status); + } + + std::vector Worker::RunBeforeTestSuiteHooks(const cucumber::messages::feature& feature, Context& context) + { + std::vector results; + const auto ids = supportCodeLibrary.hookRegistry.FindIds(support::HookType::beforeFeature, feature.tags); + + for (const auto& id : ids) + results.emplace_back(RunTestHook(id, context)); + + if (util::GetWorstTestStepResult(results).status != cucumber::messages::test_step_result_status::PASSED) + throw FeatureHookError{ "Failed before feature hook" }; + + return results; + } + + std::vector Worker::RunAfterTestSuiteHooks(const cucumber::messages::feature& feature, Context& context) + { + std::vector results; + const auto ids = supportCodeLibrary.hookRegistry.FindIds(support::HookType::afterFeature, feature.tags); + + for (const auto& id : ids) + results.emplace_back(RunTestHook(id, context)); + + if (util::GetWorstTestStepResult(results).status != cucumber::messages::test_step_result_status::PASSED) + throw FeatureHookError{ "Failed after feature hook" }; + + return results; + } + + cucumber::messages::test_step_result Worker::RunTestHook(const std::string& id, Context& context) + { + const auto& definition = supportCodeLibrary.hookRegistry.GetDefinitionById(id); + const auto testRunHookStartedId = idGenerator->next_id(); + + const auto testRunHookStarted = cucumber::messages::test_run_hook_started{ + .id = testRunHookStartedId, + .test_run_started_id = std::string{ testRunStartedId }, + .hook_id = definition.hook.id, + .timestamp = util::TimestampNow(), + }; + + broadcaster.BroadcastEvent({ .test_run_hook_started = testRunHookStarted }); + + auto result = definition.factory(broadcaster, context, testRunHookStarted)->ExecuteAndCatchExceptions(); + + broadcaster.BroadcastEvent({ .test_run_hook_finished = cucumber::messages::test_run_hook_finished{ + .test_run_hook_started_id = testRunHookStartedId, + .result = result, + .timestamp = util::TimestampNow(), + } }); + + return result; + } + + bool Worker::IsStatusFailed(cucumber::messages::test_step_result_status status) const + { + if (options.dryRun) + return false; + + if (options.strict && status == cucumber::messages::test_step_result_status::PENDING) + return true; + + return failingStatuses.contains(status); + } +} diff --git a/cucumber_cpp/library/runtime/Worker.hpp b/cucumber_cpp/library/runtime/Worker.hpp new file mode 100644 index 00000000..b58bf66b --- /dev/null +++ b/cucumber_cpp/library/runtime/Worker.hpp @@ -0,0 +1,60 @@ +#ifndef RUNTIME_WORKER_HPP +#define RUNTIME_WORKER_HPP + +#include "cucumber/gherkin/id_generator.hpp" +#include "cucumber/messages/feature.hpp" +#include "cucumber/messages/gherkin_document.hpp" +#include "cucumber/messages/test_step_result.hpp" +#include "cucumber/messages/test_step_result_status.hpp" +#include "cucumber_cpp/library/Context.hpp" +#include "cucumber_cpp/library/assemble/AssembledTestCase.hpp" +#include "cucumber_cpp/library/assemble/AssembledTestSuite.hpp" +#include "cucumber_cpp/library/support/SupportCodeLibrary.hpp" +#include "cucumber_cpp/library/support/Types.hpp" +#include "cucumber_cpp/library/util/Broadcaster.hpp" +#include +#include +#include +#include +#include + +namespace cucumber_cpp::library::runtime +{ + struct FeatureHookError : std::runtime_error + { + using std::runtime_error::runtime_error; + }; + + struct Worker + { + Worker(std::string_view testRunStartedId, + util::Broadcaster& broadcaster, + cucumber::gherkin::id_generator_ptr idGenerator, + const support::RunOptions::Runtime& options, + support::SupportCodeLibrary& supportCodeLibrary, + Context& programContext); + + std::vector RunBeforeAllHooks(); + std::vector RunAfterAllHooks(); + + bool RunTestSuite(const assemble::AssembledTestSuite& assembledTestSuite, bool failing); + bool RunTestCase(const cucumber::messages::gherkin_document& gherkinDocument, const assemble::AssembledTestCase& assembledTestCase, Context& testSuiteContext, bool failing); + + private: + std::vector RunBeforeTestSuiteHooks(const cucumber::messages::feature& feature, Context& context); + std::vector RunAfterTestSuiteHooks(const cucumber::messages::feature& feature, Context& context); + + cucumber::messages::test_step_result RunTestHook(const std::string& id, Context& context); + + bool IsStatusFailed(cucumber::messages::test_step_result_status status) const; + + std::string_view testRunStartedId; + util::Broadcaster& broadcaster; + cucumber::gherkin::id_generator_ptr idGenerator; + const support::RunOptions::Runtime& options; + support::SupportCodeLibrary& supportCodeLibrary; + Context& programContext; + }; +} + +#endif diff --git a/cucumber_cpp/library/runtime/test/CMakeLists.txt b/cucumber_cpp/library/runtime/test/CMakeLists.txt new file mode 100644 index 00000000..b1c57c9d --- /dev/null +++ b/cucumber_cpp/library/runtime/test/CMakeLists.txt @@ -0,0 +1,12 @@ +add_executable(cucumber_cpp.library.runtime.test) +add_test(NAME cucumber_cpp.library.runtime.test COMMAND cucumber_cpp.library.runtime.test) + +target_link_libraries(cucumber_cpp.library.runtime.test PUBLIC + gmock_main + cucumber_cpp.library.runtime + GTest::gmock +) + +target_sources(cucumber_cpp.library.runtime.test PRIVATE + TestDummy.cpp +) diff --git a/cucumber_cpp/library/runtime/test/TestDummy.cpp b/cucumber_cpp/library/runtime/test/TestDummy.cpp new file mode 100644 index 00000000..c81abaf1 --- /dev/null +++ b/cucumber_cpp/library/runtime/test/TestDummy.cpp @@ -0,0 +1,8 @@ +#include + +namespace cucumber_cpp::library::support +{ + TEST(Dummy, dummy) + { + } +} diff --git a/cucumber_cpp/library/support/Body.cpp b/cucumber_cpp/library/support/Body.cpp new file mode 100644 index 00000000..4c306a10 --- /dev/null +++ b/cucumber_cpp/library/support/Body.cpp @@ -0,0 +1,125 @@ +#include "cucumber_cpp/library/support/Body.hpp" +#include "cucumber/gherkin/demangle.hpp" +#include "cucumber/messages/exception.hpp" +#include "cucumber/messages/step_match_arguments_list.hpp" +#include "cucumber/messages/test_step_result.hpp" +#include "cucumber/messages/test_step_result_status.hpp" +#include "cucumber_cpp/library/runtime/NestedTestCaseRunner.hpp" +#include "cucumber_cpp/library/util/Duration.hpp" +#include "fmt/format.h" +#include "gtest/gtest.h" +#include +#include +#include +#include +#include +#include +#include + +namespace cucumber_cpp::library::support +{ + struct CucumberResultReporter : public testing::ScopedFakeTestPartResultReporter + { + explicit CucumberResultReporter(cucumber::messages::test_step_result& testStepResult) + : testing::ScopedFakeTestPartResultReporter{ nullptr } + , testStepResult{ testStepResult } + { + } + + void ReportTestPartResult(const testing::TestPartResult& testPartResult) override + { + if (testPartResult.failed()) + { + testStepResult.status = cucumber::messages::test_step_result_status::FAILED; + + auto fileName = std::filesystem::relative(testPartResult.file_name(), std::filesystem::current_path()).string(); + + if (testStepResult.message) + testStepResult.message = fmt::format("{}\n{}:{}: Failure\n{}", testStepResult.message.value(), fileName, testPartResult.line_number(), testPartResult.message()); + else + testStepResult.message = fmt::format("{}:{}: Failure\n{}", fileName, testPartResult.line_number(), testPartResult.message()); + } + + if (testPartResult.fatally_failed()) + throw FatalError{ testPartResult.message() }; + } + + private: + cucumber::messages::test_step_result& testStepResult; + }; + + cucumber::messages::test_step_result Body::ExecuteAndCatchExceptions(const cucumber::messages::step_match_arguments_list& args) + { + cucumber::messages::test_step_result testStepResult{ .status = cucumber::messages::test_step_result_status::PASSED }; + CucumberResultReporter reportListener{ testStepResult }; + + const auto startTime = util::Stopwatch::Instance().Start(); + try + { + Execute(args); + } + catch (const runtime::NestedTestCaseRunnerError& e) + { + testStepResult.status = cucumber::messages::test_step_result_status::FAILED; + + if (e.status.status != cucumber::messages::test_step_result_status::PASSED) + { + const auto offset = std::string(e.nesting, ' '); + + if (e.status.message.has_value()) + testStepResult.message = fmt::format(R"({0} {1} nested step: "* {2}")" + "\n{0} {3}", + offset, + cucumber::messages::to_string(e.status.status), + e.text, + e.status.message.value()); + else + testStepResult.message = fmt::format(R"({0} {1} nested step: "* {2}")", + offset, + cucumber::messages::to_string(e.status.status), + e.text); + } + } + catch (const StepSkipped& e) + { + testStepResult.status = cucumber::messages::test_step_result_status::SKIPPED; + if (!e.message.empty()) + testStepResult.message = e.message; + } + catch (const StepPending& e) + { + testStepResult.status = cucumber::messages::test_step_result_status::PENDING; + if (!e.message.empty()) + testStepResult.message = e.message; + } + catch ([[maybe_unused]] const FatalError& error) + { + testStepResult.status = cucumber::messages::test_step_result_status::FAILED; + } + catch (std::exception& e) + { + testStepResult.status = cucumber::messages::test_step_result_status::FAILED; + testStepResult.exception = cucumber::messages::exception{ + .type = cucumber::gherkin::detail::demangle(typeid(e).name()).get(), + .message = e.what(), + }; + } + catch (...) + { + testStepResult.status = cucumber::messages::test_step_result_status::FAILED; + testStepResult.exception = cucumber::messages::exception{ + .type = "unknown", + .message = "unknown exception", + }; + } + + auto nanoseconds = util::Stopwatch::Instance().Duration(startTime); + static constexpr std::size_t nanosecondsPerSecond = 1e9; + testStepResult.duration = { + .seconds = nanoseconds.count() / nanosecondsPerSecond, + .nanos = nanoseconds.count() % nanosecondsPerSecond, + }; + + return testStepResult; + } +} diff --git a/cucumber_cpp/library/support/Body.hpp b/cucumber_cpp/library/support/Body.hpp new file mode 100644 index 00000000..8cc9ac03 --- /dev/null +++ b/cucumber_cpp/library/support/Body.hpp @@ -0,0 +1,88 @@ +#ifndef CUCUMBER_CPP_BODY_HPP +#define CUCUMBER_CPP_BODY_HPP + +#include "cucumber/messages/step_match_arguments_list.hpp" +#include "cucumber/messages/test_step_result.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace cucumber_cpp::library::support +{ + using ExecuteArgs = std::variant, std::vector>; + + struct FatalError : std::runtime_error + { + using std::runtime_error::runtime_error; + }; + + struct StepSkipped : std::exception + { + StepSkipped(std::string message, std::source_location sourceLocation) + : message{ std::move(message) } + , sourceLocation{ sourceLocation } + { + } + + std::string message; + std::source_location sourceLocation; + }; + + 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; + }; + + struct Body + { + virtual ~Body() = default; + + cucumber::messages::test_step_result ExecuteAndCatchExceptions(const cucumber::messages::step_match_arguments_list& args = {}); + + protected: + virtual void Execute(const cucumber::messages::step_match_arguments_list& args) = 0; + }; + + template + concept HasSetUpTearDown = + requires(T t) { + { t.SetUp() } -> std::convertible_to; + { t.TearDown() } -> std::convertible_to; + }; + + template + struct SetUpTearDownWrapper + { + explicit SetUpTearDownWrapper(T& t) + : t{ t } + { + t.SetUp(); + } + + SetUpTearDownWrapper(const SetUpTearDownWrapper&) = delete; + SetUpTearDownWrapper(SetUpTearDownWrapper&&) = delete; + + ~SetUpTearDownWrapper() + { + t.TearDown(); + } + + private: + T& t; + }; +} + +#endif diff --git a/cucumber_cpp/library/support/CMakeLists.txt b/cucumber_cpp/library/support/CMakeLists.txt new file mode 100644 index 00000000..86bb415f --- /dev/null +++ b/cucumber_cpp/library/support/CMakeLists.txt @@ -0,0 +1,38 @@ +add_library(cucumber_cpp.library.support ${CCR_EXCLUDE_FROM_ALL}) + +if (CCR_STANDALONE) + target_compile_definitions(cucumber_cpp.library.support PRIVATE + CCR_STANDALONE + ) +endif() + +target_sources(cucumber_cpp.library.support PRIVATE + Body.cpp + Body.hpp + HookRegistry.cpp + HookRegistry.hpp + StepRegistry.cpp + StepRegistry.hpp + StepType.hpp + SupportCodeLibrary.cpp + SupportCodeLibrary.hpp + Types.hpp +) + +target_include_directories(cucumber_cpp.library.support PUBLIC + ../../.. +) + +target_link_libraries(cucumber_cpp.library.support PUBLIC + GTest::gtest + GTest::gmock + cucumber_cpp.library.cucumber_expression + cucumber_cpp.library.tag_expression + cucumber_cpp.library.util + fmt::fmt +) + +if (CCR_BUILD_TESTS) + add_subdirectory(test) + # add_subdirectory(test_helper) +endif() diff --git a/cucumber_cpp/library/support/HookRegistry.cpp b/cucumber_cpp/library/support/HookRegistry.cpp new file mode 100644 index 00000000..a78896ee --- /dev/null +++ b/cucumber_cpp/library/support/HookRegistry.cpp @@ -0,0 +1,131 @@ + +#include "cucumber_cpp/library/support/HookRegistry.hpp" +#include "cucumber/gherkin/id_generator.hpp" +#include "cucumber/messages/hook_type.hpp" +#include "cucumber/messages/location.hpp" +#include "cucumber/messages/pickle_tag.hpp" +#include "cucumber/messages/source_reference.hpp" +#include "cucumber/messages/tag.hpp" +#include "cucumber_cpp/library/Context.hpp" +#include "cucumber_cpp/library/engine/ExecutionContext.hpp" +#include "cucumber_cpp/library/support/SupportCodeLibrary.hpp" +#include "cucumber_cpp/library/tag_expression/Parser.hpp" +#include "cucumber_cpp/library/util/Broadcaster.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace cucumber_cpp::library::support +{ + namespace + { + auto TypeFilter(HookType hookType) + { + return [hookType](const auto& keyValue) + { + return keyValue.second.type == hookType; + }; + }; + + auto Matches(std::span tags) + { + return [tags](const auto& keyValue) + { + return keyValue.second.tagExpression->Evaluate(tags); + }; + } + + auto Matches(std::span tags) + { + return [tags](const auto& keyValue) + { + return keyValue.second.tagExpression->Evaluate(tags); + }; + } + + std::map HookTypeMap{ + { HookType::beforeAll, cucumber::messages::hook_type::BEFORE_TEST_RUN }, + { HookType::afterAll, cucumber::messages::hook_type::AFTER_TEST_RUN }, + // { HookType::beforeFeature, cucumber::messages::hook_type::BEFORE_TEST_CASE }, + // { HookType::afterFeature, cucumber::messages::hook_type::AFTER_TEST_CASE }, + { HookType::before, cucumber::messages::hook_type::BEFORE_TEST_CASE }, + { HookType::after, cucumber::messages::hook_type::AFTER_TEST_CASE }, + { HookType::beforeStep, cucumber::messages::hook_type::BEFORE_TEST_STEP }, + { HookType::afterStep, cucumber::messages::hook_type::AFTER_TEST_STEP }, + }; + } + + HookRegistry::Definition::Definition(std::string id, HookType type, std::optional expression, std::optional name, HookFactory factory, std::source_location sourceLocation) + : type{ type } + , tagExpression{ tag_expression::Parse(expression.value_or("")) } + , factory{ factory } + , hook{ + .id = std::move(id), + .name = name.has_value() ? std::make_optional(name.value()) : std::nullopt, + .source_reference = cucumber::messages::source_reference{ + .uri = sourceLocation.file_name(), + .location = cucumber::messages::location{ + .line = sourceLocation.line(), + }, + }, + .tag_expression = expression.has_value() ? std::make_optional(expression.value()) : std::nullopt, + .type = HookTypeMap.contains(type) ? std::make_optional(HookTypeMap.at(type)) : std::nullopt, + } + {} + + HookRegistry::HookRegistry(cucumber::gherkin::id_generator_ptr idGenerator) + : idGenerator{ std::move(idGenerator) } + { + } + + void HookRegistry::LoadHooks() + { + for (const auto& matcher : support::DefinitionRegistration::Instance().GetHooks()) + Register(matcher.id, matcher.type, matcher.expression, matcher.name, matcher.factory, matcher.sourceLocation); + } + + std::vector HookRegistry::FindIds(HookType hookType, std::span tags) const + { + auto ids = registry | std::views::filter(TypeFilter(hookType)) | std::views::filter(Matches(tags)) | std::views::keys; + return { ids.begin(), ids.end() }; + } + + std::vector HookRegistry::FindIds(HookType hookType, std::span tags) const + { + auto ids = registry | std::views::filter(TypeFilter(hookType)) | std::views::filter(Matches(tags)) | std::views::keys; + return { ids.begin(), ids.end() }; + } + + std::size_t HookRegistry::Size() const + { + return registry.size(); + } + + std::size_t HookRegistry::Size(HookType hookType) const + { + return std::ranges::count(registry | std::views::values, hookType, &Definition::type); + } + + HookFactory HookRegistry::GetFactoryById(const std::string& id) const + { + return registry.at(id).factory; + } + + const HookRegistry::Definition& HookRegistry::GetDefinitionById(const std::string& id) const + { + return registry.at(id); + } + + void HookRegistry::Register(const std::string& id, HookType type, std::optional expression, std::optional name, HookFactory factory, std::source_location sourceLocation) + { + registry.try_emplace(id, id, type, expression, name, factory, sourceLocation); + } +} diff --git a/cucumber_cpp/library/support/HookRegistry.hpp b/cucumber_cpp/library/support/HookRegistry.hpp new file mode 100644 index 00000000..43a17919 --- /dev/null +++ b/cucumber_cpp/library/support/HookRegistry.hpp @@ -0,0 +1,106 @@ +#ifndef CUCUMBER_CPP_HOOKREGISTRY_HPP +#define CUCUMBER_CPP_HOOKREGISTRY_HPP + +#include "cucumber/gherkin/id_generator.hpp" +#include "cucumber/messages/hook.hpp" +#include "cucumber/messages/pickle_tag.hpp" +#include "cucumber/messages/tag.hpp" +#include "cucumber_cpp/library/Context.hpp" +#include "cucumber_cpp/library/engine/ExecutionContext.hpp" +#include "cucumber_cpp/library/support/Body.hpp" +#include "cucumber_cpp/library/tag_expression/Model.hpp" +#include "cucumber_cpp/library/util/Broadcaster.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace cucumber_cpp::library::support +{ + enum struct HookType + { + beforeAll, + afterAll, + beforeFeature, + afterFeature, + before, + after, + beforeStep, + afterStep, + }; + + using HookFactory = std::unique_ptr (&)(util::Broadcaster& broadCaster, Context& context, engine::StepOrHookStarted stepOrHookStarted); + + template + std::unique_ptr HookBodyFactory(util::Broadcaster& broadCaster, Context& context, engine::StepOrHookStarted stepOrHookStarted) + { + return std::make_unique(broadCaster, context, stepOrHookStarted); + } + + struct HookMatch + { + explicit HookMatch(HookFactory factory) + : factory(factory) + {} + + HookFactory factory; + }; + + struct HookRegistry + { + struct Definition + { + Definition(std::string id, HookType type, std::optional expression, std::optional name, HookFactory factory, std::source_location sourceLocation); + + HookType type; + std::unique_ptr tagExpression; + HookFactory factory; + cucumber::messages::hook hook; + }; + + explicit HookRegistry(cucumber::gherkin::id_generator_ptr idGenerator); + + void LoadHooks(); + + std::vector FindIds(HookType hookType, std::span tags = {}) const; + std::vector FindIds(HookType hookType, std::span tags) const; + + std::vector HooksByType(HookType hookType) const + { + auto filtered = registry | + std::views::values | + std::views::filter([hookType](const Definition& definition) + { + return definition.type == hookType; + }) | + std::views::transform([](const Definition& definition) + { + return definition.hook; + }); + + return { filtered.begin(), filtered.end() }; + } + + [[nodiscard]] std::size_t Size() const; + [[nodiscard]] std::size_t Size(HookType hookType) const; + + HookFactory GetFactoryById(const std::string& id) const; + const Definition& GetDefinitionById(const std::string& id) const; + + private: + void Register(const std::string& id, HookType type, std::optional expression, std::optional name, HookFactory factory, std::source_location sourceLocation); + + cucumber::gherkin::id_generator_ptr idGenerator; + std::map> registry; + }; +} + +#endif diff --git a/cucumber_cpp/library/support/ParameterConversionTypeMap.hpp b/cucumber_cpp/library/support/ParameterConversionTypeMap.hpp new file mode 100644 index 00000000..2cf7acc7 --- /dev/null +++ b/cucumber_cpp/library/support/ParameterConversionTypeMap.hpp @@ -0,0 +1,28 @@ +#ifndef SUPPORT_PARAMETER_CONVERSION_TYPE_MAP_HPP +#define SUPPORT_PARAMETER_CONVERSION_TYPE_MAP_HPP + +#include "cucumber/messages/group.hpp" +#include +#include +#include + +namespace cucumber_cpp::library::support +{ + // template + // using TypeMap = std::map>; + + // template + // struct ConverterTypeMap + // { + // static std::map>& Instance(); + // }; + + // template + // std::map>& ConverterTypeMap::Instance() + // { + // static std::map> typeMap; + // return typeMap; + // } +} + +#endif diff --git a/cucumber_cpp/library/support/StepRegistry.cpp b/cucumber_cpp/library/support/StepRegistry.cpp new file mode 100644 index 00000000..84f69f1a --- /dev/null +++ b/cucumber_cpp/library/support/StepRegistry.cpp @@ -0,0 +1,135 @@ +#include "cucumber_cpp/library/support/StepRegistry.hpp" +#include "cucumber/gherkin/id_generator.hpp" +#include "cucumber/messages/step_definition_pattern_type.hpp" +#include "cucumber_cpp/library/cucumber_expression/Argument.hpp" +#include "cucumber_cpp/library/cucumber_expression/Errors.hpp" +#include "cucumber_cpp/library/cucumber_expression/Expression.hpp" +#include "cucumber_cpp/library/cucumber_expression/Matcher.hpp" +#include "cucumber_cpp/library/cucumber_expression/ParameterRegistry.hpp" +#include "cucumber_cpp/library/cucumber_expression/RegularExpression.hpp" +#include "cucumber_cpp/library/support/StepType.hpp" +#include "cucumber_cpp/library/support/SupportCodeLibrary.hpp" +#include "cucumber_cpp/library/support/UndefinedParameters.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace cucumber_cpp::library::support +{ + StepRegistry::StepRegistry(cucumber_expression::ParameterRegistry& parameterRegistry, support::UndefinedParameters& undefinedParameters, cucumber::gherkin::id_generator_ptr idGenerator) + : parameterRegistry{ parameterRegistry } + , undefinedParameters{ undefinedParameters } + , idGenerator{ idGenerator } + { + } + + void StepRegistry::LoadSteps() + { + support::DefinitionRegistration::Instance().ForEachRegisteredStep([this](const StepStringRegistration::Entry& entry) + { + Register(entry.id, entry.regex, entry.type, entry.factory, entry.sourceLocation); + }); + } + + [[nodiscard]] std::pair, std::vector>> StepRegistry::FindDefinitions(const std::string& expression) const + { + std::pair, std::vector>> result; + result.first.reserve(idToDefinitionMap.size()); + result.second.reserve(idToDefinitionMap.size()); + + for (const auto& [id, iter] : idToDefinitionMap) + { + const auto match = std::visit(cucumber_expression::MatchVisitor{ expression }, iter->regex); + if (match) + { + result.first.push_back(id); + result.second.push_back(match.value()); + } + } + + return result; + } + + std::size_t StepRegistry::Size() const + { + return registry.size(); + } + + StepFactory StepRegistry::GetFactoryById(const std::string& id) const + { + return idToDefinitionMap.at(id)->factory; + } + + StepRegistry::Definition StepRegistry::GetDefinitionById(const std::string& id) const + { + return *idToDefinitionMap.at(id); + } + + const std::list& StepRegistry::StepDefinitions() const + { + return registry; + } + + void StepRegistry::Register(std::string id, const std::string& matcher, StepType stepType, StepFactory factory, std::source_location sourceLocation) + { + try + { + auto cucumberMatcher = (matcher.starts_with('^') || matcher.ends_with('$')) + ? cucumber_expression::Matcher{ + std::in_place_type, + matcher, + parameterRegistry, + } + : cucumber_expression::Matcher{ + std::in_place_type, + matcher, + parameterRegistry, + }; + auto cucumberMatcherType = std::holds_alternative(cucumberMatcher) + ? cucumber::messages::step_definition_pattern_type::REGULAR_EXPRESSION + : cucumber::messages::step_definition_pattern_type::CUCUMBER_EXPRESSION; + + registry.emplace_back(factory, + id, + sourceLocation.line(), + sourceLocation.file_name(), + stepType, + matcher, + cucumberMatcher, + cucumberMatcherType); + + idToDefinitionMap[id] = std::prev(registry.end()); + } + catch (const cucumber_expression::UndefinedParameterTypeError& e) + { + undefinedParameters.definitions.emplace_back( + std::string{ e.expression }, + std::string{ e.undefinedParameterName }); + } + } + + StepStringRegistration& StepStringRegistration::Instance() + { + static StepStringRegistration instance; + return instance; + } + + std::span StepStringRegistration::GetEntries() + { + return registry; + } + + std::span StepStringRegistration::GetEntries() const + { + return registry; + } +} diff --git a/cucumber_cpp/library/support/StepRegistry.hpp b/cucumber_cpp/library/support/StepRegistry.hpp new file mode 100644 index 00000000..8258b752 --- /dev/null +++ b/cucumber_cpp/library/support/StepRegistry.hpp @@ -0,0 +1,177 @@ +#ifndef CUCUMBER_CPP_STEPREGISTRY_HPP +#define CUCUMBER_CPP_STEPREGISTRY_HPP + +#include "cucumber/gherkin/id_generator.hpp" +#include "cucumber/messages/pickle_doc_string.hpp" +#include "cucumber/messages/pickle_table.hpp" +#include "cucumber/messages/step_definition_pattern_type.hpp" +#include "cucumber_cpp/library/Context.hpp" +#include "cucumber_cpp/library/cucumber_expression/Argument.hpp" +#include "cucumber_cpp/library/cucumber_expression/Matcher.hpp" +#include "cucumber_cpp/library/cucumber_expression/ParameterRegistry.hpp" +#include "cucumber_cpp/library/engine/ExecutionContext.hpp" +#include "cucumber_cpp/library/support/Body.hpp" +#include "cucumber_cpp/library/support/StepType.hpp" +#include "cucumber_cpp/library/support/UndefinedParameters.hpp" +#include "cucumber_cpp/library/util/Broadcaster.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace cucumber_cpp::library::runtime +{ + struct NestedTestCaseRunner; +} + +namespace cucumber_cpp::library::support +{ + + using StepFactory = std::unique_ptr (&)(const runtime::NestedTestCaseRunner&, util::Broadcaster& broadCaster, Context&, engine::StepOrHookStarted stepOrHookStarted, const std::optional&, const std::optional&); + + template + std::unique_ptr StepBodyFactory(const runtime::NestedTestCaseRunner& nestedTestCaseRunner, util::Broadcaster& broadCaster, Context& context, engine::StepOrHookStarted stepOrHookStarted, const std::optional& dataTable, const std::optional& docString) + { + return std::make_unique(nestedTestCaseRunner, broadCaster, context, stepOrHookStarted, dataTable, docString); + } + + struct StepMatch + { + StepMatch(StepFactory factory, std::variant, std::vector> matches, std::string_view stepRegexStr) + : factory(factory) + , matches(std::move(matches)) + , stepRegexStr(stepRegexStr) + {} + + StepFactory factory; + std::variant, std::vector> matches{}; + std::string_view stepRegexStr{}; + }; + + struct StepRegistry + { + struct StepNotFoundError : std::exception + { + using std::exception::exception; + }; + + struct AmbiguousStepError : std::exception + { + explicit AmbiguousStepError(std::vector&& matches) + : matches{ std::move(matches) } + {} + + std::vector matches; + }; + + struct Definition + { + StepFactory factory; + std::string id; + std::size_t line; + std::filesystem::path uri; + + StepType type; + std::string pattern; + cucumber_expression::Matcher regex; + cucumber::messages::step_definition_pattern_type patternType; + + std::uint32_t used{ 0 }; + }; + + struct EntryView + { + EntryView(const cucumber_expression::Matcher& stepRegex, const std::uint32_t& used) + : stepRegex(stepRegex) + , used(used) + {} + + const cucumber_expression::Matcher& stepRegex; + const std::uint32_t& used; + }; + + explicit StepRegistry(cucumber_expression::ParameterRegistry& parameterRegistry, support::UndefinedParameters& undefinedParameters, cucumber::gherkin::id_generator_ptr idGenerator); + + void LoadSteps(); + + [[nodiscard]] std::pair, std::vector>> FindDefinitions(const std::string& expression) const; + + [[nodiscard]] std::size_t Size() const; + + StepFactory GetFactoryById(const std::string& id) const; + Definition GetDefinitionById(const std::string& id) const; + + const std::list& StepDefinitions() const; + + private: + void Register(std::string id, const std::string& matcher, StepType stepType, StepFactory factory, std::source_location sourceLocation); + + cucumber_expression::ParameterRegistry& parameterRegistry; + support::UndefinedParameters& undefinedParameters; + cucumber::gherkin::id_generator_ptr idGenerator; + + std::list registry; + std::map::iterator, std::less<>> idToDefinitionMap; + }; + + struct StepStringRegistration + { + private: + StepStringRegistration() = default; + + public: + static StepStringRegistration& Instance(); + + struct Entry + { + Entry(StepType type, std::string regex, StepFactory factory, std::source_location sourceLocation) + : type{ type } + , regex{ std::move(regex) } + , factory{ factory } + , sourceLocation{ sourceLocation } + {} + + StepType type{}; + std::string regex; + StepFactory factory; + std::source_location sourceLocation; + std::string id{ "unassigned" }; + }; + + template + static std::size_t Register(const std::string& matcher, StepType stepType, std::source_location sourceLocation = std::source_location::current()); + + std::span GetEntries(); + [[nodiscard]] std::span GetEntries() const; + + private: + std::vector registry; + }; + + ////////////////////////// + // implementation // + ////////////////////////// + + template + std::size_t StepStringRegistration::Register(const std::string& matcher, StepType stepType, std::source_location sourceLocation) + { + Instance().registry.emplace_back(stepType, matcher, StepBodyFactory, sourceLocation); + + return Instance().registry.size(); + } +} + +#endif diff --git a/cucumber_cpp/library/engine/StepType.hpp b/cucumber_cpp/library/support/StepType.hpp similarity index 79% rename from cucumber_cpp/library/engine/StepType.hpp rename to cucumber_cpp/library/support/StepType.hpp index 2aa5a87a..c967bdb6 100644 --- a/cucumber_cpp/library/engine/StepType.hpp +++ b/cucumber_cpp/library/support/StepType.hpp @@ -1,7 +1,7 @@ #ifndef ENGINE_STEPTYPE_HPP #define ENGINE_STEPTYPE_HPP -namespace cucumber_cpp::library::engine +namespace cucumber_cpp::library::support { enum struct StepType { diff --git a/cucumber_cpp/library/support/SupportCodeLibrary.cpp b/cucumber_cpp/library/support/SupportCodeLibrary.cpp new file mode 100644 index 00000000..6063b9ea --- /dev/null +++ b/cucumber_cpp/library/support/SupportCodeLibrary.cpp @@ -0,0 +1,98 @@ +#include "cucumber_cpp/library/support/SupportCodeLibrary.hpp" +#include "cucumber/gherkin/id_generator.hpp" +#include "cucumber_cpp/library/cucumber_expression/ParameterRegistry.hpp" +#include "cucumber_cpp/library/support/HookRegistry.hpp" +#include "cucumber_cpp/library/support/StepRegistry.hpp" +#include "cucumber_cpp/library/support/StepType.hpp" +#include "fmt/format.h" +#include "fmt/ranges.h" +#include "fmt/std.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace cucumber_cpp::library::support +{ + bool SourceLocationOrder::operator()(const std::source_location& lhs, const std::source_location& rhs) const + { + return std::make_tuple(std::string_view{ lhs.file_name() }, lhs.line()) < std::make_tuple(std::string_view{ rhs.file_name() }, rhs.line()); + } + + DefinitionRegistration& DefinitionRegistration::Instance() + { + static DefinitionRegistration instance; + return instance; + } + + void DefinitionRegistration::LoadIds(cucumber::gherkin::id_generator_ptr idGenerator) + { + const auto assignGenerator = [&idGenerator](auto& entry) + { + entry.id = idGenerator->next_id(); + }; + + for (auto& [key, item] : registry) + std::visit(assignGenerator, item); + } + + std::vector DefinitionRegistration::GetHooks() + { + auto allSteps = registry | std::views::values | std::views::filter([](const Entry& entry) + { + return std::holds_alternative(entry); + }) | + std::views::transform([](const Entry& entry) + { + return std::get(entry); + }); + return { allSteps.begin(), allSteps.end() }; + } + + const std::set>& DefinitionRegistration::GetRegisteredParameters() const + { + return customParameters; + } + + namespace + { + void PrintContents(std::string_view type, std::source_location sourceLocation, const std::map& registry) + { +#if defined(CCR_STANDALONE) + fmt::println("Added ({}): {}:{}", type, std::filesystem::path{ sourceLocation.file_name() }, sourceLocation.line()); + fmt::println("Registry contents:"); + for (const auto& [key, item] : registry) + fmt::println(" {}:{}", std::filesystem::path{ key.file_name() }, key.line()); +#endif + } + } + + std::size_t DefinitionRegistration::Register(Hook hook, HookType hookType, HookFactory factory, std::source_location sourceLocation) + { + registry.emplace(sourceLocation, HookEntry{ hookType, hook, factory, sourceLocation }); + PrintContents("Hook", sourceLocation, registry); + return registry.size(); + } + + std::size_t DefinitionRegistration::Register(GlobalHook hook, HookType hookType, HookFactory factory, std::source_location sourceLocation) + { + registry.emplace(sourceLocation, HookEntry{ hookType, hook, factory, sourceLocation }); + PrintContents("GlobalHook", sourceLocation, registry); + return registry.size(); + } + + std::size_t DefinitionRegistration::Register(std::string_view matcher, StepType stepType, StepFactory factory, std::source_location sourceLocation) + { + registry.emplace(sourceLocation, StepStringRegistration::Entry{ stepType, std::string{ matcher }, factory, sourceLocation }); + PrintContents("Step", sourceLocation, registry); + return registry.size(); + } +} diff --git a/cucumber_cpp/library/support/SupportCodeLibrary.hpp b/cucumber_cpp/library/support/SupportCodeLibrary.hpp new file mode 100644 index 00000000..a047c9eb --- /dev/null +++ b/cucumber_cpp/library/support/SupportCodeLibrary.hpp @@ -0,0 +1,173 @@ +#ifndef SUPPORT_SUPPORT_CODE_LIBRARY_HPP +#define SUPPORT_SUPPORT_CODE_LIBRARY_HPP + +#include "cucumber/gherkin/id_generator.hpp" +#include "cucumber_cpp/library/cucumber_expression/ParameterRegistry.hpp" +#include "cucumber_cpp/library/support/HookRegistry.hpp" +#include "cucumber_cpp/library/support/StepRegistry.hpp" +#include "cucumber_cpp/library/support/StepType.hpp" +#include "cucumber_cpp/library/support/UndefinedParameters.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace cucumber_cpp::library::support +{ + struct GlobalHook + + { + std::string_view name{}; + std::int32_t order{ 0 }; + }; + + struct Hook + { + std::string_view expression{ "" }; + std::string_view name{}; + std::int32_t order{ 0 }; + }; + + struct HookEntry + { + HookEntry(HookType type, GlobalHook hook, HookFactory factory, std::source_location sourceLocation) + : type{ type } + , expression{ std::nullopt } + , name{ hook.name.empty() ? std::nullopt : std::make_optional(hook.name) } + , order{ hook.order } + , factory{ factory } + , sourceLocation{ sourceLocation } + {} + + HookEntry(HookType type, Hook hook, HookFactory factory, std::source_location sourceLocation) + : type{ type } + , expression{ hook.expression.empty() ? std::nullopt : std::make_optional(hook.expression) } + , name{ hook.name.empty() ? std::nullopt : std::make_optional(hook.name) } + , order{ hook.order } + , factory{ factory } + , sourceLocation{ sourceLocation } + {} + + HookType type; + std::optional expression; + std::optional name; + std::int32_t order; + HookFactory factory; + std::source_location sourceLocation; + std::string id{ "unassigned" }; + }; + + struct SupportCodeLibrary + { + HookRegistry& hookRegistry; + StepRegistry& stepRegistry; + cucumber_expression::ParameterRegistry& parameterRegistry; + UndefinedParameters& undefinedParameters; + }; + + using Entry = std::variant; + + struct SourceLocationOrder + { + bool operator()(const std::source_location& lhs, const std::source_location& rhs) const; + }; + + struct DefinitionRegistration + { + private: + DefinitionRegistration() = default; + + public: + static DefinitionRegistration& Instance(); + + void LoadIds(cucumber::gherkin::id_generator_ptr idGenerator); + + template + void ForEachRegisteredStep(const T& func); + + std::vector GetHooks(); + + const std::set>& GetRegisteredParameters() const; + + template + static std::size_t Register(Hook hook, HookType hookType, std::source_location sourceLocation = std::source_location::current()); + + template + static std::size_t Register(GlobalHook hook, HookType hookType, std::source_location sourceLocation = std::source_location::current()); + + template + static std::size_t Register(std::string_view matcher, StepType stepType, std::source_location sourceLocation = std::source_location::current()); + + template + static std::size_t Register(cucumber_expression::CustomParameterEntryParams params, std::source_location location = std::source_location::current()); + + private: + std::size_t Register(Hook hook, HookType hookType, HookFactory factory, std::source_location sourceLocation); + std::size_t Register(GlobalHook hook, HookType hookType, HookFactory factory, std::source_location sourceLocation); + std::size_t Register(std::string_view matcher, StepType stepType, StepFactory factory, std::source_location sourceLocation); + + std::map registry; + std::set> customParameters; + }; + + ////////////////////////// + // implementation // + ////////////////////////// + + template + void DefinitionRegistration::ForEachRegisteredStep(const T& func) + { + auto allSteps = registry | + std::views::values | + std::views::filter([](const Entry& entry) + { + return std::holds_alternative(entry); + }) | + std::views::transform([](const Entry& entry) + { + return std::get(entry); + }); + + for (const auto& step : allSteps) + func(step); + } + + template + std::size_t DefinitionRegistration::Register(Hook hook, HookType hookType, std::source_location sourceLocation) + { + return Instance().Register(hook, hookType, HookBodyFactory, sourceLocation); + } + + template + std::size_t DefinitionRegistration::Register(GlobalHook hook, HookType hookType, std::source_location sourceLocation) + { + return Instance().Register(hook, hookType, HookBodyFactory, sourceLocation); + } + + template + std::size_t DefinitionRegistration::Register(std::string_view matcher, StepType stepType, std::source_location sourceLocation) + { + return Instance().Register(matcher, stepType, StepBodyFactory, sourceLocation); + } + + template + std::size_t DefinitionRegistration::Register(cucumber_expression::CustomParameterEntryParams params, std::source_location location) + { + auto& instance = Instance(); + instance.customParameters.emplace(params, instance.customParameters.size() + 1, location); + + cucumber_expression::ConverterTypeMap::Instance()[params.name] = Transformer::Transform; + + return instance.customParameters.size(); + } +} + +#endif diff --git a/cucumber_cpp/library/support/Types.hpp b/cucumber_cpp/library/support/Types.hpp new file mode 100644 index 00000000..673d7793 --- /dev/null +++ b/cucumber_cpp/library/support/Types.hpp @@ -0,0 +1,71 @@ +#ifndef SUPPORT_TYPES_HPP +#define SUPPORT_TYPES_HPP + +#include "cucumber/messages/gherkin_document.hpp" +#include "cucumber/messages/location.hpp" +#include "cucumber/messages/pickle.hpp" +#include "cucumber_cpp/library/tag_expression/Model.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace cucumber_cpp::library::support +{ + struct RunOptions + { + enum class Ordering + { + defined, + reverse, + }; + + struct Sources + { + std::set> paths{}; + std::unique_ptr tagExpression; + Ordering ordering{ Ordering::defined }; + + } sources; + + struct Runtime + { + bool dryRun{ false }; + bool failFast{ false }; + std::size_t retry{ 0 }; + bool strict{ true }; + std::unique_ptr retryTagExpression{}; + } runtime; + + struct RunEnvironment + { + std::optional cwd; + std::optional> env; + } runEnvironment; + }; + + struct PickleSource + { + std::shared_ptr pickle; + std::shared_ptr gherkinDocument; + std::shared_ptr location; + }; + + struct Runtime + { + virtual ~Runtime() = default; + virtual bool Run() = 0; + }; + + struct RuntimeAdapter + { + virtual ~RuntimeAdapter() = default; + virtual bool Run() = 0; + }; + +} + +#endif diff --git a/cucumber_cpp/library/support/UndefinedParameters.hpp b/cucumber_cpp/library/support/UndefinedParameters.hpp new file mode 100644 index 00000000..77f33859 --- /dev/null +++ b/cucumber_cpp/library/support/UndefinedParameters.hpp @@ -0,0 +1,15 @@ +#ifndef SUPPORT_UNDEFINED_PARAMETERS_HPP +#define SUPPORT_UNDEFINED_PARAMETERS_HPP + +#include "cucumber/messages/undefined_parameter_type.hpp" +#include + +namespace cucumber_cpp::library::support +{ + struct UndefinedParameters + { + std::list definitions; + }; +} + +#endif diff --git a/cucumber_cpp/library/support/test/CMakeLists.txt b/cucumber_cpp/library/support/test/CMakeLists.txt new file mode 100644 index 00000000..c1ea2130 --- /dev/null +++ b/cucumber_cpp/library/support/test/CMakeLists.txt @@ -0,0 +1,12 @@ +add_executable(cucumber_cpp.library.support.test) +add_test(NAME cucumber_cpp.library.support.test COMMAND cucumber_cpp.library.support.test) + +target_link_libraries(cucumber_cpp.library.support.test PUBLIC + gmock_main + cucumber_cpp.library.support + GTest::gmock +) + +target_sources(cucumber_cpp.library.support.test PRIVATE + TestDummy.cpp +) diff --git a/cucumber_cpp/library/support/test/TestDummy.cpp b/cucumber_cpp/library/support/test/TestDummy.cpp new file mode 100644 index 00000000..c81abaf1 --- /dev/null +++ b/cucumber_cpp/library/support/test/TestDummy.cpp @@ -0,0 +1,8 @@ +#include + +namespace cucumber_cpp::library::support +{ + TEST(Dummy, dummy) + { + } +} diff --git a/cucumber_cpp/library/tag_expression/CMakeLists.txt b/cucumber_cpp/library/tag_expression/CMakeLists.txt index 479264ed..3848b4ed 100644 --- a/cucumber_cpp/library/tag_expression/CMakeLists.txt +++ b/cucumber_cpp/library/tag_expression/CMakeLists.txt @@ -1,6 +1,4 @@ -set(CMAKE_COMPILE_WARNING_AS_ERROR On) - -add_library(cucumber_cpp.library.tag_expression STATIC ${CCR_EXCLUDE_FROM_ALL}) +add_library(cucumber_cpp.library.tag_expression ${CCR_EXCLUDE_FROM_ALL}) target_sources(cucumber_cpp.library.tag_expression PRIVATE Model.cpp @@ -17,6 +15,11 @@ target_include_directories(cucumber_cpp.library.tag_expression PUBLIC ../../.. ) +target_link_libraries(cucumber_cpp.library.tag_expression PUBLIC + cucumber_gherkin_lib + fmt::fmt +) + if (CCR_BUILD_TESTS) add_subdirectory(test) endif() diff --git a/cucumber_cpp/library/tag_expression/Model.cpp b/cucumber_cpp/library/tag_expression/Model.cpp index 1fb0b62b..0c79905d 100644 --- a/cucumber_cpp/library/tag_expression/Model.cpp +++ b/cucumber_cpp/library/tag_expression/Model.cpp @@ -1,10 +1,14 @@ #include "cucumber_cpp/library/tag_expression/Model.hpp" +#include "cucumber/messages/pickle_tag.hpp" +#include "cucumber/messages/tag.hpp" +#include "fmt/format.h" +#include #include -#include #include #include #include #include +#include #include #include #include @@ -16,6 +20,16 @@ namespace cucumber_cpp::library::tag_expression return true; } + bool TrueExpression::Evaluate(std::span tags) const + { + return true; + } + + bool TrueExpression::Evaluate(std::span tags) const + { + return true; + } + TrueExpression::operator std::string() const { return "true"; @@ -30,6 +44,16 @@ namespace cucumber_cpp::library::tag_expression return tags.contains(name); } + bool LiteralExpression::Evaluate(std::span tags) const + { + return std::ranges::find(tags, name, &cucumber::messages::pickle_tag::name) != tags.end(); + } + + bool LiteralExpression::Evaluate(std::span tags) const + { + return std::ranges::find(tags, name, &cucumber::messages::tag::name) != tags.end(); + } + LiteralExpression::operator std::string() const { auto replaceAll = [](std::string& str, std::string_view from, std::string_view to) @@ -65,12 +89,22 @@ namespace cucumber_cpp::library::tag_expression return left->Evaluate(tags) && right->Evaluate(tags); } + bool AndExpression::Evaluate(std::span tags) const + { + return left->Evaluate(tags) && right->Evaluate(tags); + } + + bool AndExpression::Evaluate(std::span tags) const + { + return left->Evaluate(tags) && right->Evaluate(tags); + } + AndExpression::operator std::string() const { if (!left || !right) return ""; - return std::format(R"(( {} and {} ))", static_cast(*left), static_cast(*right)); + return fmt::format(R"(( {} and {} ))", static_cast(*left), static_cast(*right)); } OrExpression::OrExpression(std::unique_ptr left, std::unique_ptr right) @@ -83,12 +117,22 @@ namespace cucumber_cpp::library::tag_expression return left->Evaluate(tags) || right->Evaluate(tags); } + bool OrExpression::Evaluate(std::span tags) const + { + return left->Evaluate(tags) || right->Evaluate(tags); + } + + bool OrExpression::Evaluate(std::span tags) const + { + return left->Evaluate(tags) || right->Evaluate(tags); + } + OrExpression::operator std::string() const { if (!left || !right) return ""; - return std::format(R"(( {} or {} ))", static_cast(*left), static_cast(*right)); + return fmt::format(R"(( {} or {} ))", static_cast(*left), static_cast(*right)); } NotExpression::NotExpression(std::unique_ptr operand) @@ -100,14 +144,24 @@ namespace cucumber_cpp::library::tag_expression return !operand->Evaluate(tags); } + bool NotExpression::Evaluate(std::span tags) const + { + return !operand->Evaluate(tags); + } + + bool NotExpression::Evaluate(std::span tags) const + { + return !operand->Evaluate(tags); + } + NotExpression::operator std::string() const { if (!operand) return ""; if (const auto& ref = *operand.get(); typeid(ref) == typeid(AndExpression) || typeid(ref) == typeid(OrExpression)) - return std::format(R"(not {})", static_cast(*operand)); + return fmt::format(R"(not {})", static_cast(*operand)); - return std::format(R"(not ( {} ))", static_cast(*operand)); + return fmt::format(R"(not ( {} ))", static_cast(*operand)); } } diff --git a/cucumber_cpp/library/tag_expression/Model.hpp b/cucumber_cpp/library/tag_expression/Model.hpp index 9ae69a5a..b515b12e 100644 --- a/cucumber_cpp/library/tag_expression/Model.hpp +++ b/cucumber_cpp/library/tag_expression/Model.hpp @@ -1,9 +1,12 @@ #ifndef TAG_EXPRESSION_MODEL_HPP #define TAG_EXPRESSION_MODEL_HPP +#include "cucumber/messages/pickle_tag.hpp" +#include "cucumber/messages/tag.hpp" #include #include #include +#include #include namespace cucumber_cpp::library::tag_expression @@ -13,12 +16,18 @@ namespace cucumber_cpp::library::tag_expression virtual ~Expression() = default; virtual bool Evaluate(const std::set>& tags) const = 0; + virtual bool Evaluate(std::span tags) const = 0; + virtual bool Evaluate(std::span tags) const = 0; + virtual explicit operator std::string() const = 0; }; struct TrueExpression : Expression { bool Evaluate(const std::set>& tags) const override; + bool Evaluate(std::span tags) const override; + bool Evaluate(std::span tags) const override; + explicit operator std::string() const override; }; @@ -27,6 +36,9 @@ namespace cucumber_cpp::library::tag_expression explicit LiteralExpression(std::string name); bool Evaluate(const std::set>& tags) const override; + bool Evaluate(std::span tags) const override; + bool Evaluate(std::span tags) const override; + explicit operator std::string() const override; private: @@ -38,6 +50,9 @@ namespace cucumber_cpp::library::tag_expression AndExpression(std::unique_ptr left, std::unique_ptr right); bool Evaluate(const std::set>& tags) const override; + bool Evaluate(std::span tags) const override; + bool Evaluate(std::span tags) const override; + explicit operator std::string() const override; private: @@ -50,6 +65,9 @@ namespace cucumber_cpp::library::tag_expression OrExpression(std::unique_ptr left, std::unique_ptr right); bool Evaluate(const std::set>& tags) const override; + bool Evaluate(std::span tags) const override; + bool Evaluate(std::span tags) const override; + explicit operator std::string() const override; private: @@ -62,6 +80,9 @@ namespace cucumber_cpp::library::tag_expression explicit NotExpression(std::unique_ptr operand); bool Evaluate(const std::set>& tags) const override; + bool Evaluate(std::span tags) const override; + bool Evaluate(std::span tags) const override; + explicit operator std::string() const override; private: diff --git a/cucumber_cpp/library/tag_expression/Parser.cpp b/cucumber_cpp/library/tag_expression/Parser.cpp index 76e32a50..7f78b3e0 100644 --- a/cucumber_cpp/library/tag_expression/Parser.cpp +++ b/cucumber_cpp/library/tag_expression/Parser.cpp @@ -2,10 +2,10 @@ #include "cucumber_cpp/library/tag_expression/Error.hpp" #include "cucumber_cpp/library/tag_expression/Model.hpp" #include "cucumber_cpp/library/tag_expression/Token.hpp" +#include "fmt/format.h" #include #include #include -#include #include #include #include @@ -21,7 +21,7 @@ namespace cucumber_cpp::library::tag_expression void EnsureExpectedTokenType(TokenType tokenType, TokenType expected, std::string_view lastPart) { if (tokenType != expected) - throw Error(std::format(R"(Syntax error. Expected {} after {})", TokenTypeMap().at(expected), lastPart)); + throw Error(fmt::format(R"(Syntax error. Expected {} after {})", TokenTypeMap().at(expected), lastPart)); } void RequireArgCount(const Token& token, std::deque>& expressions, std::size_t number) @@ -37,7 +37,7 @@ namespace cucumber_cpp::library::tag_expression expressionsStr += static_cast(*expr); } - throw Error(std::format(R"({}: Too few operands (expressions={{{}}}))", token.keyword, expressionsStr)); + throw Error(fmt::format(R"({}: Too few operands (expressions={{{}}}))", token.keyword, expressionsStr)); } } @@ -74,7 +74,7 @@ namespace cucumber_cpp::library::tag_expression else if (token == NOT) PushUnary(token, expressions); else - throw Error(std::format("Unexpected token: {}", token.keyword)); + throw Error(fmt::format("Unexpected token: {}", token.keyword)); } std::vector Tokenize(std::string_view expression) @@ -88,7 +88,7 @@ namespace cucumber_cpp::library::tag_expression if (escaped) { if ((ch != '(' && ch != ')' && ch != '\\') && !std::isspace(ch, std::locale())) - throw Error(std::format(R"(Tag expression "{}" could not be parsed because of syntax error: Illegal escape before "{}".)", expression, ch)); + throw Error(fmt::format(R"(Tag expression "{}" could not be parsed because of syntax error: Illegal escape before "{}".)", expression, ch)); token += ch; escaped = false; } @@ -174,7 +174,7 @@ namespace cucumber_cpp::library::tag_expression } if (operations.empty()) - throw Error(std::format("Missing '(': Too few open-parens in: {}", expression)); + throw Error(fmt::format("Missing '(': Too few open-parens in: {}", expression)); else if (operations.top() == OPEN_PARENTHESIS) { @@ -192,7 +192,7 @@ namespace cucumber_cpp::library::tag_expression operations.pop(); if (lastOperation == OPEN_PARENTHESIS) - throw Error(std::format("Unclosed '(': Too many open-parens in: {}", expression)); + throw Error(fmt::format("Unclosed '(': Too many open-parens in: {}", expression)); PushExpression(lastOperation, expressions); } diff --git a/cucumber_cpp/library/tag_expression/Token.hpp b/cucumber_cpp/library/tag_expression/Token.hpp index e1f24a21..a45e22a6 100644 --- a/cucumber_cpp/library/tag_expression/Token.hpp +++ b/cucumber_cpp/library/tag_expression/Token.hpp @@ -4,9 +4,7 @@ #include #include #include -#include #include -#include namespace cucumber_cpp::library::tag_expression { diff --git a/cucumber_cpp/library/tag_expression/test/CMakeLists.txt b/cucumber_cpp/library/tag_expression/test/CMakeLists.txt index 384f8893..64c6cf26 100644 --- a/cucumber_cpp/library/tag_expression/test/CMakeLists.txt +++ b/cucumber_cpp/library/tag_expression/test/CMakeLists.txt @@ -6,6 +6,7 @@ target_link_libraries(cucumber_cpp.library.tag_expression.test PUBLIC cucumber_cpp.library.tag_expression yaml-cpp::yaml-cpp GTest::gmock + fmt::fmt ) target_sources(cucumber_cpp.library.tag_expression.test PRIVATE diff --git a/cucumber_cpp/library/tag_expression/test/TestEvaluations.cpp b/cucumber_cpp/library/tag_expression/test/TestEvaluations.cpp index 0f28472b..65dab7f5 100644 --- a/cucumber_cpp/library/tag_expression/test/TestEvaluations.cpp +++ b/cucumber_cpp/library/tag_expression/test/TestEvaluations.cpp @@ -1,11 +1,11 @@ #include "cucumber_cpp/library/tag_expression/Parser.hpp" +#include "fmt/format.h" #include "yaml-cpp/node/emit.h" #include "yaml-cpp/node/node.h" #include "yaml-cpp/node/parse.h" #include "yaml-cpp/yaml.h" #include #include -#include #include #include #include @@ -80,7 +80,7 @@ namespace cucumber_cpp::library::tag_expression return new TestEvaluations(node["expression"].as(), test["variables"], test["result"].as()); }; - auto* testInfo = testing::RegisterTest("TestEvaluations", std::format("Test_{}_{}", tests.size(), lineNumber).c_str(), nullptr, nullptr, testdataPath.string().c_str(), lineNumber, factory); + auto* testInfo = testing::RegisterTest("TestEvaluations", fmt::format("Test_{}_{}", tests.size(), lineNumber).c_str(), nullptr, nullptr, testdataPath.string().c_str(), lineNumber, factory); tests.push_back(testInfo); diff --git a/cucumber_cpp/library/tag_expression/test/TestParsing.cpp b/cucumber_cpp/library/tag_expression/test/TestParsing.cpp index edb3cd4a..0567a910 100644 --- a/cucumber_cpp/library/tag_expression/test/TestParsing.cpp +++ b/cucumber_cpp/library/tag_expression/test/TestParsing.cpp @@ -1,4 +1,5 @@ #include "cucumber_cpp/library/tag_expression/Parser.hpp" +#include "fmt/format.h" #include "yaml-cpp/node/emit.h" #include "yaml-cpp/node/node.h" #include "yaml-cpp/node/parse.h" @@ -6,7 +7,6 @@ #include #include #include -#include #include #include #include @@ -69,7 +69,7 @@ namespace cucumber_cpp::library::tag_expression return new TestParsingFixture(node["expression"].as(), node["formatted"].as()); }; - auto* testInfo = testing::RegisterTest("TestParsing", std::format("Test_{}_{}", tests.size(), lineNumber).c_str(), nullptr, nullptr, testdataPath.string().c_str(), lineNumber, factory); + auto* testInfo = testing::RegisterTest("TestParsing", fmt::format("Test_{}_{}", tests.size(), lineNumber).c_str(), nullptr, nullptr, testdataPath.string().c_str(), lineNumber, factory); tests.push_back(testInfo); diff --git a/cucumber_cpp/library/test/CMakeLists.txt b/cucumber_cpp/library/test/CMakeLists.txt index 998d657a..1191772a 100644 --- a/cucumber_cpp/library/test/CMakeLists.txt +++ b/cucumber_cpp/library/test/CMakeLists.txt @@ -13,6 +13,5 @@ target_link_libraries(cucumber_cpp.library.test PUBLIC target_sources(cucumber_cpp.library.test PRIVATE TestApplication.cpp TestContext.cpp - TestTagExpression.cpp TestSteps.cpp ) diff --git a/cucumber_cpp/library/test/TestSteps.cpp b/cucumber_cpp/library/test/TestSteps.cpp index 8b4fc419..4a3c8ea3 100644 --- a/cucumber_cpp/library/test/TestSteps.cpp +++ b/cucumber_cpp/library/test/TestSteps.cpp @@ -1,6 +1,8 @@ #include "cucumber_cpp/library/Context.hpp" #include "cucumber_cpp/library/StepRegistry.hpp" #include "cucumber_cpp/library/cucumber_expression/ParameterRegistry.hpp" +#include "cucumber_cpp/library/query/Query.hpp" +#include "cucumber_cpp/library/support/StepRegistry.hpp" #include "gtest/gtest.h" #include #include @@ -13,8 +15,7 @@ namespace cucumber_cpp::library { struct TestSteps : testing::Test { - - cucumber_expression::ParameterRegistry parameterRegistry; + cucumber_expression::ParameterRegistry parameterRegistry{ {} }; StepRegistry stepRegistry{ parameterRegistry }; }; @@ -43,12 +44,12 @@ namespace cucumber_cpp::library TEST_F(TestSteps, GetInvalidStep) { - EXPECT_THROW((void)stepRegistry.Query("This step does not exist"), StepRegistry::StepNotFoundError); + EXPECT_THROW((void)stepRegistry.Query("This step does not exist"), support::StepRegistry::StepNotFoundError); } TEST_F(TestSteps, GetAmbiguousStep) { - EXPECT_THROW((void)stepRegistry.Query("an ambiguous step"), StepRegistry::AmbiguousStepError); + EXPECT_THROW((void)stepRegistry.Query("an ambiguous step"), support::StepRegistry::AmbiguousStepError); } TEST_F(TestSteps, InvokeTestWithCucumberExpressions) @@ -58,7 +59,7 @@ namespace cucumber_cpp::library auto contextStorage{ std::make_shared() }; Context context{ contextStorage }; - matches.factory(context, {}, "")->Execute(matches.matches); + matches.factory(context, {}, "")->ExecuteAndCatchExceptions(matches.matches); EXPECT_THAT(context.Contains("float"), testing::IsTrue()); EXPECT_THAT(context.Contains("std::string"), testing::IsTrue()); @@ -85,7 +86,7 @@ namespace cucumber_cpp::library auto contextStorage{ std::make_shared() }; Context context{ contextStorage }; - matches.factory(context, {}, {})->Execute(matches.matches); + matches.factory(context, {}, {})->ExecuteAndCatchExceptions(matches.matches); } TEST_F(TestSteps, EscapedParenthesis) diff --git a/cucumber_cpp/library/test/TestTagExpression.cpp b/cucumber_cpp/library/test/TestTagExpression.cpp deleted file mode 100644 index 2e34a24f..00000000 --- a/cucumber_cpp/library/test/TestTagExpression.cpp +++ /dev/null @@ -1,84 +0,0 @@ -#include "cucumber_cpp/library/TagExpression.hpp" -#include "gmock/gmock.h" -#include "gtest/gtest.h" -#include -#include -#include - -namespace cucumber_cpp::library -{ - struct TestTagExpression : testing::Test - { - std::set> inputTags = { "@abc", "@def", "@efg" }; - std::set> noTags = {}; - std::set> ignoredTags = { "@abc", "@def", "@efg", "@ignore" }; - }; - - TEST_F(TestTagExpression, EmptyTagExpression) - { - EXPECT_THAT(IsTagExprSelected("", noTags), testing::IsTrue()); - EXPECT_THAT(IsTagExprSelected("", inputTags), testing::IsTrue()); - } - - TEST_F(TestTagExpression, EmptyTags) - { - EXPECT_THAT(IsTagExprSelected("", noTags), testing::IsTrue()); - EXPECT_THAT(IsTagExprSelected("@foo", noTags), testing::IsFalse()); - } - - TEST_F(TestTagExpression, SingleTag) - { - EXPECT_THAT(IsTagExprSelected("@abc", noTags), testing::IsFalse()); - EXPECT_THAT(IsTagExprSelected("@abc", inputTags), testing::IsTrue()); - EXPECT_THAT(IsTagExprSelected("@abc", ignoredTags), testing::IsTrue()); - - EXPECT_THAT(IsTagExprSelected("@foo", inputTags), testing::IsFalse()); - EXPECT_THAT(IsTagExprSelected("@foo", ignoredTags), testing::IsFalse()); - } - - TEST_F(TestTagExpression, AndTag) - { - EXPECT_THAT(IsTagExprSelected("@abc and @def", noTags), testing::IsFalse()); - EXPECT_THAT(IsTagExprSelected("@abc and @def", inputTags), testing::IsTrue()); - EXPECT_THAT(IsTagExprSelected("@abc and @def", ignoredTags), testing::IsTrue()); - - EXPECT_THAT(IsTagExprSelected("@abc and @foo", inputTags), testing::IsFalse()); - EXPECT_THAT(IsTagExprSelected("@abc and @foo", ignoredTags), testing::IsFalse()); - } - - TEST_F(TestTagExpression, OrTag) - { - EXPECT_THAT(IsTagExprSelected("@foo or @def", noTags), testing::IsFalse()); - EXPECT_THAT(IsTagExprSelected("@abc or @foo", inputTags), testing::IsTrue()); - EXPECT_THAT(IsTagExprSelected("@abc or @foo", ignoredTags), testing::IsTrue()); - - EXPECT_THAT(IsTagExprSelected("@fez or @foo", inputTags), testing::IsFalse()); - EXPECT_THAT(IsTagExprSelected("@fez or @foo", ignoredTags), testing::IsFalse()); - } - - TEST_F(TestTagExpression, AndOrGroupedTag) - { - EXPECT_THAT(IsTagExprSelected("@abc and (@def or @efg)", noTags), testing::IsFalse()); - EXPECT_THAT(IsTagExprSelected("@abc and (@def or @efg)", inputTags), testing::IsTrue()); - EXPECT_THAT(IsTagExprSelected("@abc and (@def or @efg)", ignoredTags), testing::IsTrue()); - - EXPECT_THAT(IsTagExprSelected("@fez and (@def or @efg)", inputTags), testing::IsFalse()); - EXPECT_THAT(IsTagExprSelected("@fez and (@def or @efg)", ignoredTags), testing::IsFalse()); - EXPECT_THAT(IsTagExprSelected("@abc and (@fez or @bar)", inputTags), testing::IsFalse()); - EXPECT_THAT(IsTagExprSelected("@abc and (@fez or @bar)", ignoredTags), testing::IsFalse()); - } - - TEST_F(TestTagExpression, Negating) - { - EXPECT_THAT(IsTagExprSelected("not @ignore", noTags), testing::IsTrue()); - EXPECT_THAT(IsTagExprSelected("not @ignore", inputTags), testing::IsTrue()); - EXPECT_THAT(IsTagExprSelected("not @ignore", ignoredTags), testing::IsFalse()); - } - - TEST_F(TestTagExpression, WithTagNegating) - { - EXPECT_THAT(IsTagExprSelected("@abc and not @ignore", noTags), testing::IsFalse()); - EXPECT_THAT(IsTagExprSelected("@abc and not @ignore", inputTags), testing::IsTrue()); - EXPECT_THAT(IsTagExprSelected("@abc and not @ignore", ignoredTags), testing::IsFalse()); - } -} diff --git a/cucumber_cpp/library/util/Broadcaster.cpp b/cucumber_cpp/library/util/Broadcaster.cpp new file mode 100644 index 00000000..a3932f20 --- /dev/null +++ b/cucumber_cpp/library/util/Broadcaster.cpp @@ -0,0 +1,41 @@ +#include "cucumber_cpp/library/util/Broadcaster.hpp" +#include "cucumber/messages/envelope.hpp" +#include +#include + +namespace cucumber_cpp::library::util +{ + Listener::Listener(Broadcaster& broadcaster, const std::function& onEvent) + : broadcaster{ broadcaster } + , onEvent{ onEvent } + { + broadcaster.AddListener(this); + } + + Listener::~Listener() + { + broadcaster.RemoveListener(this); + } + + void Listener::Invoke(const cucumber::messages::envelope& envelope) const + { + if (onEvent) + onEvent(envelope); + } + + void Broadcaster::AddListener(Listener* listener) + { + listeners.push_back(listener); + } + + void Broadcaster::RemoveListener(Listener* listener) + { + std::erase(listeners, listener); + } + + void Broadcaster::BroadcastEvent(const cucumber::messages::envelope& envelope) + { + for (auto& listener : listeners) + listener->Invoke(envelope); + } +} diff --git a/cucumber_cpp/library/util/Broadcaster.hpp b/cucumber_cpp/library/util/Broadcaster.hpp new file mode 100644 index 00000000..b16ea207 --- /dev/null +++ b/cucumber_cpp/library/util/Broadcaster.hpp @@ -0,0 +1,42 @@ +#ifndef LIBRARY_20EVENT_EMITTER_HPP +#define LIBRARY_20EVENT_EMITTER_HPP + +#include "cucumber/messages/envelope.hpp" +#include +#include + +namespace cucumber_cpp::library::util +{ + struct Broadcaster; + + struct Listener + { + explicit Listener(Broadcaster& broadcaster, const std::function& onEvent); + + Listener(const Listener&) = delete; + Listener& operator=(const Listener&) = delete; + Listener(Listener&&) = delete; + Listener& operator=(Listener&&) = delete; + + ~Listener(); + + void Invoke(const cucumber::messages::envelope& envelope) const; + + private: + Broadcaster& broadcaster; + std::function onEvent; + }; + + struct Broadcaster + { + void AddListener(Listener* listener); + void RemoveListener(Listener* listener); + + void BroadcastEvent(const cucumber::messages::envelope& envelope); + + private: + std::vector listeners; + }; +} + +#endif diff --git a/cucumber_cpp/library/util/CMakeLists.txt b/cucumber_cpp/library/util/CMakeLists.txt index ce0ae4d4..e897d0f3 100644 --- a/cucumber_cpp/library/util/CMakeLists.txt +++ b/cucumber_cpp/library/util/CMakeLists.txt @@ -1,9 +1,28 @@ -add_library(cucumber_cpp.library.util INTERFACE ${CCR_EXCLUDE_FROM_ALL}) +add_library(cucumber_cpp.library.util ${CCR_EXCLUDE_FROM_ALL}) target_sources(cucumber_cpp.library.util PRIVATE + Broadcaster.cpp + Broadcaster.hpp + Duration.cpp + Duration.hpp + GetWorstTestStepResult.cpp + GetWorstTestStepResult.hpp Immoveable.hpp + Timestamp.cpp + Timestamp.hpp + Trim.hpp ) -target_include_directories(cucumber_cpp.library.util INTERFACE +target_include_directories(cucumber_cpp.library.util PUBLIC ../../.. ) + +target_link_libraries(cucumber_cpp.library.util PUBLIC + cucumber_gherkin_lib + fmt::fmt +) + +if (CCR_BUILD_TESTS) + add_subdirectory(test) + # add_subdirectory(test_helper) +endif() diff --git a/cucumber_cpp/library/util/Duration.cpp b/cucumber_cpp/library/util/Duration.cpp new file mode 100644 index 00000000..8f0be59c --- /dev/null +++ b/cucumber_cpp/library/util/Duration.cpp @@ -0,0 +1,84 @@ + +#include "cucumber_cpp/library/util/Duration.hpp" +#include "cucumber/messages/duration.hpp" +#include "cucumber_cpp/library/util/Timestamp.hpp" +#include +#include + +namespace cucumber_cpp::library::util +{ + namespace + { + std::chrono::milliseconds ToMillis(std::chrono::seconds seconds, std::chrono::nanoseconds nanos) + { + return std::chrono::duration_cast(seconds) + + std::chrono::duration_cast(nanos); + } + + std::chrono::nanoseconds ToNanoSeconds(std::chrono::seconds seconds, std::chrono::nanoseconds nanos) + { + return std::chrono::duration_cast(seconds) + nanos; + } + + cucumber::messages::duration ToDuration(std::chrono::milliseconds millis) + { + return { + .seconds = millis.count() / millisecondsPerSecond, + .nanos = (millis.count() % millisecondsPerSecond) * nanosecondsPerMillisecond, + }; + } + } + + Stopwatch::Stopwatch() + { + Stopwatch::instance = this; + } + + Stopwatch& Stopwatch::Instance() + { + return *instance; + } + + std::chrono::high_resolution_clock::time_point StopWatchHighResolutionClock::Start() + { + return std::chrono::high_resolution_clock::now(); + } + + std::chrono::nanoseconds StopWatchHighResolutionClock::Duration(std::chrono::high_resolution_clock::time_point timePoint) + { + const auto timeStop = std::chrono::high_resolution_clock::now(); + return std::chrono::duration_cast(timeStop - timePoint); + } + + cucumber::messages::duration MillisecondsToDuration(std::chrono::milliseconds millis) + { + return ToDuration(millis); + } + + std::chrono::milliseconds DurationToMilliseconds(const cucumber::messages::duration& duration) + { + return ToMillis(std::chrono::seconds{ duration.seconds }, std::chrono::nanoseconds{ duration.nanos }); + } + + std::chrono::nanoseconds DurationToNanoSeconds(const cucumber::messages::duration& duration) + { + return ToNanoSeconds(std::chrono::seconds{ duration.seconds }, std::chrono::nanoseconds{ duration.nanos }); + } + + cucumber::messages::duration& operator+=(cucumber::messages::duration& lhs, const cucumber::messages::duration& rhs) + { + const auto totalNanos = lhs.nanos + rhs.nanos; + lhs.seconds += rhs.seconds; + lhs.seconds += totalNanos / nanosecondsPerSecond; + lhs.nanos = totalNanos % nanosecondsPerSecond; + + return lhs; + } + + cucumber::messages::duration operator+(const cucumber::messages::duration& lhs, const cucumber::messages::duration& rhs) + { + cucumber::messages::duration result = lhs; + + return result += rhs; + } +} diff --git a/cucumber_cpp/library/util/Duration.hpp b/cucumber_cpp/library/util/Duration.hpp new file mode 100644 index 00000000..fb1741b0 --- /dev/null +++ b/cucumber_cpp/library/util/Duration.hpp @@ -0,0 +1,46 @@ +#ifndef UTIL_DURATION_HPP +#define UTIL_DURATION_HPP + +#include "cucumber/messages/duration.hpp" +#include + +namespace cucumber_cpp::library::util +{ + cucumber::messages::duration MillisecondsToDuration(std::chrono::milliseconds millis); + + std::chrono::milliseconds DurationToMilliseconds(const cucumber::messages::duration& duration); + std::chrono::nanoseconds DurationToNanoSeconds(const cucumber::messages::duration& duration); + cucumber::messages::duration& operator+=(cucumber::messages::duration& lhs, const cucumber::messages::duration& rhs); + cucumber::messages::duration operator+(const cucumber::messages::duration& lhs, const cucumber::messages::duration& rhs); + + struct Stopwatch + { + protected: + Stopwatch(); + ~Stopwatch() = default; + + public: + static Stopwatch& Instance(); + + virtual std::chrono::high_resolution_clock::time_point Start() = 0; + virtual std::chrono::nanoseconds Duration(std::chrono::high_resolution_clock::time_point timepPoint) = 0; + + private: + static inline Stopwatch* instance{ nullptr }; + }; + + struct StopWatchHighResolutionClock : Stopwatch + { + virtual ~StopWatchHighResolutionClock() = default; + std::chrono::high_resolution_clock::time_point Start() override; + std::chrono::nanoseconds Duration(std::chrono::high_resolution_clock::time_point timePoint) override; + }; +} + +namespace cucumber::messages +{ + using cucumber_cpp::library::util::operator+=; + using cucumber_cpp::library::util::operator+; +}; + +#endif diff --git a/cucumber_cpp/library/util/GetWorstTestStepResult.cpp b/cucumber_cpp/library/util/GetWorstTestStepResult.cpp new file mode 100644 index 00000000..966b41ef --- /dev/null +++ b/cucumber_cpp/library/util/GetWorstTestStepResult.cpp @@ -0,0 +1,30 @@ +#include "cucumber_cpp/library/util/GetWorstTestStepResult.hpp" +#include "cucumber/messages/test_step_result.hpp" +#include "cucumber/messages/test_step_result_status.hpp" +#include +#include +#include + +namespace cucumber_cpp::library::util +{ + namespace + { + const auto to_underlying = [](const auto& value) + { + return static_cast>>(value); + }; + + const auto compare = [](const cucumber::messages::test_step_result& a, const cucumber::messages::test_step_result& b) + { + return to_underlying(a.status) < to_underlying(b.status); + }; + } + + cucumber::messages::test_step_result GetWorstTestStepResult(std::span testStepResults) + { + if (testStepResults.empty()) + return { .status = cucumber::messages::test_step_result_status::PASSED }; + + return *std::ranges::max_element(testStepResults, compare); + } +} diff --git a/cucumber_cpp/library/util/GetWorstTestStepResult.hpp b/cucumber_cpp/library/util/GetWorstTestStepResult.hpp new file mode 100644 index 00000000..6471c921 --- /dev/null +++ b/cucumber_cpp/library/util/GetWorstTestStepResult.hpp @@ -0,0 +1,12 @@ +#ifndef UTIL_GET_WORST_TEST_STEP_RESULT_HPP +#define UTIL_GET_WORST_TEST_STEP_RESULT_HPP + +#include "cucumber/messages/test_step_result.hpp" +#include + +namespace cucumber_cpp::library::util +{ + cucumber::messages::test_step_result GetWorstTestStepResult(std::span testStepResults); +} + +#endif diff --git a/cucumber_cpp/library/util/Timestamp.cpp b/cucumber_cpp/library/util/Timestamp.cpp new file mode 100644 index 00000000..d7cacd6d --- /dev/null +++ b/cucumber_cpp/library/util/Timestamp.cpp @@ -0,0 +1,71 @@ + +#include "cucumber_cpp/library/util/Timestamp.hpp" +#include "cucumber/messages/duration.hpp" +#include "cucumber/messages/timestamp.hpp" +#include "cucumber_cpp/library/util/Duration.hpp" +#include "fmt/chrono.h" +#include "fmt/format.h" +#include +#include + +namespace cucumber_cpp::library::util +{ + namespace + { + std::chrono::milliseconds ToMillis(std::chrono::seconds seconds, std::chrono::nanoseconds nanos) + { + return std::chrono::duration_cast(seconds) + + std::chrono::duration_cast(nanos); + } + + std::chrono::milliseconds TimestampToMillis(const cucumber::messages::timestamp& timestamp) + { + return ToMillis(std::chrono::seconds(timestamp.seconds), std::chrono::nanoseconds(timestamp.nanos)); + } + } + + TimestampGenerator::TimestampGenerator() + { + instance = this; + } + + TimestampGenerator::~TimestampGenerator() + { + instance = nullptr; + } + + TimestampGenerator& TimestampGenerator::Instance() + { + return *instance; + } + + std::chrono::milliseconds TimestampGeneratorSystemClock::Now() + { + const auto now = std::chrono::system_clock::now().time_since_epoch(); + return std::chrono::duration_cast(now); + } + + cucumber::messages::timestamp TimestampNow() + { + const auto nowMillis = TimestampGenerator::Instance().Now().count(); + const auto seconds = nowMillis / millisecondsPerSecond; + const auto nanos = (nowMillis % millisecondsPerSecond) * nanosecondsPerMillisecond; + return cucumber::messages::timestamp{ + .seconds = seconds, + .nanos = nanos, + }; + } + + cucumber::messages::duration operator-(const cucumber::messages::timestamp& lhs, const cucumber::messages::timestamp& rhs) + { + const auto durationMillis = TimestampToMillis(lhs) - TimestampToMillis(rhs); + return MillisecondsToDuration(durationMillis); + } + + std::string MakeIso8601Timestamp(const cucumber::messages::timestamp& timestamp) + { + const auto duration = std::chrono::duration_cast(std::chrono::seconds(timestamp.seconds) + std::chrono::nanoseconds(timestamp.nanos)); + const std::chrono::system_clock::time_point tp{ duration }; + return fmt::format("{:%FT%T%Z}", tp); + } +} diff --git a/cucumber_cpp/library/util/Timestamp.hpp b/cucumber_cpp/library/util/Timestamp.hpp new file mode 100644 index 00000000..a4ab865a --- /dev/null +++ b/cucumber_cpp/library/util/Timestamp.hpp @@ -0,0 +1,49 @@ +#ifndef UTIL_TIMESTAMP_HPP +#define UTIL_TIMESTAMP_HPP + +#include "cucumber/messages/duration.hpp" +#include "cucumber/messages/timestamp.hpp" +#include +#include +#include + +namespace cucumber_cpp::library::util +{ + constexpr std::size_t millisecondsPerSecond = 1e3; + constexpr std::size_t nanosecondsPerMillisecond = 1e6; + constexpr std::size_t nanosecondsPerSecond = 1e9; + + struct TimestampGenerator + { + protected: + TimestampGenerator(); + ~TimestampGenerator(); + + public: + static TimestampGenerator& Instance(); + virtual std::chrono::milliseconds Now() = 0; + + private: + static inline TimestampGenerator* instance; + }; + + struct TimestampGeneratorSystemClock : TimestampGenerator + { + virtual ~TimestampGeneratorSystemClock() = default; + + std::chrono::milliseconds Now() override; + }; + + cucumber::messages::timestamp TimestampNow(); + + cucumber::messages::duration operator-(const cucumber::messages::timestamp& lhs, const cucumber::messages::timestamp& rhs); + + std::string MakeIso8601Timestamp(const cucumber::messages::timestamp& timestamp); +} + +namespace cucumber::messages +{ + using cucumber_cpp::library::util::operator-; +}; + +#endif diff --git a/cucumber_cpp/library/util/ToLower.hpp b/cucumber_cpp/library/util/ToLower.hpp new file mode 100644 index 00000000..d7dacec5 --- /dev/null +++ b/cucumber_cpp/library/util/ToLower.hpp @@ -0,0 +1,20 @@ +#ifndef UTIL_TO_LOWER_HPP +#define UTIL_TO_LOWER_HPP + +#include +#include +#include + +namespace cucumber_cpp::library::util +{ + [[nodiscard]] inline std::string ToLower(std::string str) + { + std::transform(str.begin(), str.end(), str.begin(), [](unsigned char c) + { + return std::tolower(c); + }); + return str; + } +} + +#endif diff --git a/cucumber_cpp/library/util/Trim.hpp b/cucumber_cpp/library/util/Trim.hpp new file mode 100644 index 00000000..5155c68e --- /dev/null +++ b/cucumber_cpp/library/util/Trim.hpp @@ -0,0 +1,19 @@ +#ifndef UTIL_TRIM_HPP +#define UTIL_TRIM_HPP + +#include + +namespace cucumber_cpp::library::util +{ + [[nodiscard]] inline std::string Trim(const std::string& str, const char* whitespace = " \t\n\r") + { + const auto start = str.find_first_not_of(whitespace); + if (start == std::string::npos) + return ""; + + const auto end = str.find_last_not_of(whitespace); + return str.substr(start, end - start + 1); + } +} + +#endif diff --git a/cucumber_cpp/library/util/test/CMakeLists.txt b/cucumber_cpp/library/util/test/CMakeLists.txt new file mode 100644 index 00000000..0e714f3c --- /dev/null +++ b/cucumber_cpp/library/util/test/CMakeLists.txt @@ -0,0 +1,12 @@ +add_executable(cucumber_cpp.library.util.test) +add_test(NAME cucumber_cpp.library.util.test COMMAND cucumber_cpp.library.util.test) + +target_link_libraries(cucumber_cpp.library.util.test PUBLIC + gmock_main + cucumber_cpp.library.util + GTest::gmock +) + +target_sources(cucumber_cpp.library.util.test PRIVATE + TestDummy.cpp +) diff --git a/cucumber_cpp/library/util/test/TestDummy.cpp b/cucumber_cpp/library/util/test/TestDummy.cpp new file mode 100644 index 00000000..c81abaf1 --- /dev/null +++ b/cucumber_cpp/library/util/test/TestDummy.cpp @@ -0,0 +1,8 @@ +#include + +namespace cucumber_cpp::library::support +{ + TEST(Dummy, dummy) + { + } +} diff --git a/cucumber_cpp/runner/CMakeLists.txt b/cucumber_cpp/runner/CMakeLists.txt index b10dbdb2..97dafc8d 100644 --- a/cucumber_cpp/runner/CMakeLists.txt +++ b/cucumber_cpp/runner/CMakeLists.txt @@ -1,4 +1,4 @@ -add_library(cucumber_cpp.runner ${CCR_EXCLUDE_FROM_ALL}) +add_library(cucumber_cpp.runner STATIC ${CCR_EXCLUDE_FROM_ALL}) target_sources(cucumber_cpp.runner PRIVATE Main.cpp diff --git a/external/CMakeLists.txt b/external/CMakeLists.txt index 1b229006..6de30b50 100644 --- a/external/CMakeLists.txt +++ b/external/CMakeLists.txt @@ -1,9 +1,14 @@ set(CMAKE_CXX_INCLUDE_WHAT_YOU_USE "") +set(CMAKE_COMPILE_WARNING_AS_ERROR Off) add_subdirectory(nlohmann) # before cucumber add_subdirectory(cliutils) add_subdirectory(cucumber) -add_subdirectory(googletest) -add_subdirectory(jbeder) +add_subdirectory(fmtlib) +add_subdirectory(google) +if (CCR_BUILD_TESTS) + add_subdirectory(jbeder) +endif() +add_subdirectory(tobiaslocker) add_subdirectory(zeux) diff --git a/external/cucumber/gherkin/CMakeLists.txt b/external/cucumber/gherkin/CMakeLists.txt index fbaf6e59..4022c03f 100644 --- a/external/cucumber/gherkin/CMakeLists.txt +++ b/external/cucumber/gherkin/CMakeLists.txt @@ -1,6 +1,7 @@ -FetchContent_Declare(cucumber_gherkin +FetchContent_Declare( + cucumber_gherkin GIT_REPOSITORY https://github.com/cucumber/gherkin.git - GIT_TAG "7cd0304c39a51ac139a22264ebe2a7ce6c68819d" + GIT_TAG "v37.0.0" ) FetchContent_MakeAvailable(cucumber_gherkin) diff --git a/external/cucumber/messages/CMakeLists.txt b/external/cucumber/messages/CMakeLists.txt index 4b609947..1e8a68c0 100644 --- a/external/cucumber/messages/CMakeLists.txt +++ b/external/cucumber/messages/CMakeLists.txt @@ -1,7 +1,7 @@ -FetchContent_Declare(cucumber_messages +FetchContent_Declare( + cucumber_messages GIT_REPOSITORY https://github.com/cucumber/messages.git - GIT_TAG "v26.0.1" - + GIT_TAG "v31.0.0" OVERRIDE_FIND_PACKAGE ) diff --git a/external/fmtlib/CMakeLists.txt b/external/fmtlib/CMakeLists.txt new file mode 100644 index 00000000..468fe7de --- /dev/null +++ b/external/fmtlib/CMakeLists.txt @@ -0,0 +1 @@ +add_subdirectory(fmt) diff --git a/external/fmtlib/fmt/CMakeLists.txt b/external/fmtlib/fmt/CMakeLists.txt new file mode 100644 index 00000000..2e687af6 --- /dev/null +++ b/external/fmtlib/fmt/CMakeLists.txt @@ -0,0 +1,7 @@ +FetchContent_Declare( + libfmt + GIT_REPOSITORY https://github.com/fmtlib/fmt + GIT_TAG 12.1.0 +) + +FetchContent_MakeAvailable(libfmt) diff --git a/external/google/CMakeLists.txt b/external/google/CMakeLists.txt new file mode 100644 index 00000000..73deebc3 --- /dev/null +++ b/external/google/CMakeLists.txt @@ -0,0 +1 @@ +add_subdirectory(googletest) diff --git a/external/google/googletest/CMakeLists.txt b/external/google/googletest/CMakeLists.txt new file mode 100644 index 00000000..e569ea75 --- /dev/null +++ b/external/google/googletest/CMakeLists.txt @@ -0,0 +1,15 @@ +if(NOT TARGET gtest) + FetchContent_Declare( + googletest + GIT_REPOSITORY https://github.com/google/googletest + GIT_TAG v1.14.0 + ) + + set(gtest_force_shared_crt On CACHE BOOL "" FORCE) # For Windows: Prevent overriding the parent project's compiler/linker settings + set(INSTALL_GTEST Off CACHE BOOL "" FORCE) + + FetchContent_MakeAvailable(googletest) + + set_target_properties(gtest gtest_main gmock gmock_main PROPERTIES FOLDER External/GoogleTest) + mark_as_advanced(BUILD_GMOCK BUILD_GTEST BUILD_SHARED_LIBS gmock_build_tests gtest_build_samples test_build_tests gtest_disable_pthreads gtest_force_shared_crt gtest_hide_internal_symbols) +endif() diff --git a/external/googletest/CMakeLists.txt b/external/googletest/CMakeLists.txt deleted file mode 100644 index 853e49be..00000000 --- a/external/googletest/CMakeLists.txt +++ /dev/null @@ -1,15 +0,0 @@ - -include(FetchContent) -FetchContent_Declare( - googletest - GIT_REPOSITORY https://github.com/google/googletest - GIT_TAG v1.14.0 -) - -set(gtest_force_shared_crt On CACHE BOOL "" FORCE) # For Windows: Prevent overriding the parent project's compiler/linker settings -set(INSTALL_GTEST Off CACHE BOOL "" FORCE) - -FetchContent_MakeAvailable(googletest) - -set_target_properties(gtest gtest_main gmock gmock_main PROPERTIES FOLDER External/GoogleTest) -mark_as_advanced(BUILD_GMOCK BUILD_GTEST BUILD_SHARED_LIBS gmock_build_tests gtest_build_samples test_build_tests gtest_disable_pthreads gtest_force_shared_crt gtest_hide_internal_symbols) diff --git a/external/jbeder/yaml-cpp/CMakeLists.txt b/external/jbeder/yaml-cpp/CMakeLists.txt index 65d76cae..aaae6767 100644 --- a/external/jbeder/yaml-cpp/CMakeLists.txt +++ b/external/jbeder/yaml-cpp/CMakeLists.txt @@ -1,6 +1,9 @@ FetchContent_Declare( - yaml-cpp - GIT_REPOSITORY https://github.com/jbeder/yaml-cpp.git - GIT_TAG 2f86d13775d119edbb69af52e5f566fd65c6953b # Unreleased + yaml-cpp + GIT_REPOSITORY https://github.com/jbeder/yaml-cpp.git + GIT_TAG 2f86d13775d119edbb69af52e5f566fd65c6953b # Unreleased ) + +set(YAML_ENABLE_PIC OFF CACHE STRING "") + FetchContent_MakeAvailable(yaml-cpp) diff --git a/external/nlohmann/json/CMakeLists.txt b/external/nlohmann/json/CMakeLists.txt index 0c2c6973..2accb643 100644 --- a/external/nlohmann/json/CMakeLists.txt +++ b/external/nlohmann/json/CMakeLists.txt @@ -1,7 +1,6 @@ FetchContent_Declare(nlohmann_json GIT_REPOSITORY https://github.com/nlohmann/json.git GIT_TAG "79587f896ed4db2a0b63baa9151113f4da336599" # Unreleased - OVERRIDE_FIND_PACKAGE ) diff --git a/external/tobiaslocker/CMakeLists.txt b/external/tobiaslocker/CMakeLists.txt new file mode 100644 index 00000000..f453a035 --- /dev/null +++ b/external/tobiaslocker/CMakeLists.txt @@ -0,0 +1 @@ +add_subdirectory(base64) diff --git a/external/tobiaslocker/base64/CMakeLists.txt b/external/tobiaslocker/base64/CMakeLists.txt new file mode 100644 index 00000000..d678405f --- /dev/null +++ b/external/tobiaslocker/base64/CMakeLists.txt @@ -0,0 +1,4 @@ +# https://github.com/tobiaslocker/base64/tree/8d96a2a737ac1396304b1de289beb3a5ea0cb752 + +add_library(base64 INTERFACE) +target_include_directories(base64 INTERFACE include) diff --git a/external/tobiaslocker/base64/LICENSE b/external/tobiaslocker/base64/LICENSE new file mode 100644 index 00000000..ffa390dd --- /dev/null +++ b/external/tobiaslocker/base64/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Tobias Locker + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/external/tobiaslocker/base64/README.md b/external/tobiaslocker/base64/README.md new file mode 100644 index 00000000..2c1f4756 --- /dev/null +++ b/external/tobiaslocker/base64/README.md @@ -0,0 +1,48 @@ +# base64 + +A simple, header-only C++17 library for Base64 encoding and decoding. Uses modern C++ features when available +(e.g., `std::bit_cast` in C++20) while remaining portable and efficient. + +## Features + +- Header-only, single-file library +- Fast encoding/decoding using lookup tables +- Safe type punning using `std::bit_cast` (avoids undefined behavior with unions) +- Throws `std::runtime_error` for invalid Base64 input (size, padding, or characters) + +## Platform Support + +- Compilers: GCC, Clang, MSVC +- Architectures: x86, x64, ARM, AArch64 (little-endian) +- Requires C++17 (C++20 features optional) + +## Usage + +```cpp +#include +#include "base64.hpp" + +int main() { + auto encoded = base64::to_base64("Hello, World!"); + std::cout << encoded << std::endl; // SGVsbG8sIFdvcmxkIQ== + + auto decoded = base64::from_base64("SGVsbG8sIFdvcmxkIQ=="); + std::cout << decoded << std::endl; // Hello, World! +} +``` + +## Notes + +- Inspired by Nick Galbreath's modp_b64 (used by Chromium) for high performance +- Compatible with C++17; optionally uses C++20 features +- Avoids union-based type punning for safety +- Faster implementations exist using SIMD or multithreading but are not header-only + +## References + +- Benchmark of C/C++ Base64 libraries: https://github.com/gaspardpetit/base64/ +- Chromium modp_b64: https://github.com/chromium/chromium/tree/main/third_party/modp_b64 +- SIMD/optimized alternatives: + - https://github.com/aklomp/base64 + - https://github.com/simdutf/simdutf + - https://github.com/powturbo/Turbo-Base64 diff --git a/external/tobiaslocker/base64/include/base64.hpp b/external/tobiaslocker/base64/include/base64.hpp new file mode 100644 index 00000000..c7df5e43 --- /dev/null +++ b/external/tobiaslocker/base64/include/base64.hpp @@ -0,0 +1,737 @@ +#ifndef BASE64_HPP_ +#define BASE64_HPP_ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(__cpp_lib_bit_cast) +#include // For std::bit_cast. +#endif + +namespace base64 +{ + namespace detail + { + +#if defined(__cpp_lib_bit_cast) + using std::bit_cast; +#else + template + std::enable_if_t && + std::is_trivially_copyable_v, + To> + bit_cast(const From& src) noexcept + { + static_assert(std::is_trivially_constructible_v, + "This implementation additionally requires " + "destination type to be trivially constructible"); + + To dst; + std::memcpy(&dst, &src, sizeof(To)); + return dst; + } +#endif + + inline constexpr char padding_char{ '=' }; + inline constexpr uint32_t bad_char{ 0x01FFFFFF }; + +#if !defined(__LITTLE_ENDIAN__) && !defined(__BIG_ENDIAN__) +#if (defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__) || \ + (defined(__BYTE_ORDER) && __BYTE_ORDER == __BIG_ENDIAN) || \ + (defined(_BYTE_ORDER) && _BYTE_ORDER == _BIG_ENDIAN) || \ + (defined(BYTE_ORDER) && BYTE_ORDER == BIG_ENDIAN) || \ + (defined(__sun) && defined(__SVR4) && defined(_BIG_ENDIAN)) || \ + defined(__ARMEB__) || defined(__THUMBEB__) || defined(__AARCH64EB__) || \ + defined(_MIBSEB) || defined(__MIBSEB) || defined(__MIBSEB__) || \ + defined(_M_PPC) +#define __BIG_ENDIAN__ +#elif (defined(__BYTE_ORDER__) && \ + __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__) || /* gcc */ \ + (defined(__BYTE_ORDER) && \ + __BYTE_ORDER == __LITTLE_ENDIAN) /* linux header */ \ + || (defined(_BYTE_ORDER) && _BYTE_ORDER == _LITTLE_ENDIAN) || \ + (defined(BYTE_ORDER) && BYTE_ORDER == LITTLE_ENDIAN) /* mingw header */ || \ + (defined(__sun) && defined(__SVR4) && \ + defined(_LITTLE_ENDIAN)) || /* solaris */ \ + defined(__ARMEL__) || \ + defined(__THUMBEL__) || defined(__AARCH64EL__) || defined(_MIPSEL) || \ + defined(__MIPSEL) || defined(__MIPSEL__) || defined(_M_IX86) || \ + defined(_M_X64) || defined(_M_IA64) || /* msvc for intel processors */ \ + defined(_M_ARM) || \ + defined(_M_ARM64) /* msvc code on arm executes in little endian mode */ +#define __LITTLE_ENDIAN__ +#endif +#endif + +#if !defined(__LITTLE_ENDIAN__) & !defined(__BIG_ENDIAN__) +#error "UNKNOWN Platform / endianness. Configure endianness explicitly." +#endif + +#if defined(__LITTLE_ENDIAN__) + std::array constexpr decode_table_0 = { + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x000000f8, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x000000fc, + 0x000000d0, 0x000000d4, 0x000000d8, 0x000000dc, 0x000000e0, 0x000000e4, + 0x000000e8, 0x000000ec, 0x000000f0, 0x000000f4, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x00000000, + 0x00000004, 0x00000008, 0x0000000c, 0x00000010, 0x00000014, 0x00000018, + 0x0000001c, 0x00000020, 0x00000024, 0x00000028, 0x0000002c, 0x00000030, + 0x00000034, 0x00000038, 0x0000003c, 0x00000040, 0x00000044, 0x00000048, + 0x0000004c, 0x00000050, 0x00000054, 0x00000058, 0x0000005c, 0x00000060, + 0x00000064, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x00000068, 0x0000006c, 0x00000070, 0x00000074, 0x00000078, + 0x0000007c, 0x00000080, 0x00000084, 0x00000088, 0x0000008c, 0x00000090, + 0x00000094, 0x00000098, 0x0000009c, 0x000000a0, 0x000000a4, 0x000000a8, + 0x000000ac, 0x000000b0, 0x000000b4, 0x000000b8, 0x000000bc, 0x000000c0, + 0x000000c4, 0x000000c8, 0x000000cc, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff + }; + + std::array constexpr decode_table_1 = { + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x0000e003, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x0000f003, + 0x00004003, 0x00005003, 0x00006003, 0x00007003, 0x00008003, 0x00009003, + 0x0000a003, 0x0000b003, 0x0000c003, 0x0000d003, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x00000000, + 0x00001000, 0x00002000, 0x00003000, 0x00004000, 0x00005000, 0x00006000, + 0x00007000, 0x00008000, 0x00009000, 0x0000a000, 0x0000b000, 0x0000c000, + 0x0000d000, 0x0000e000, 0x0000f000, 0x00000001, 0x00001001, 0x00002001, + 0x00003001, 0x00004001, 0x00005001, 0x00006001, 0x00007001, 0x00008001, + 0x00009001, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x0000a001, 0x0000b001, 0x0000c001, 0x0000d001, 0x0000e001, + 0x0000f001, 0x00000002, 0x00001002, 0x00002002, 0x00003002, 0x00004002, + 0x00005002, 0x00006002, 0x00007002, 0x00008002, 0x00009002, 0x0000a002, + 0x0000b002, 0x0000c002, 0x0000d002, 0x0000e002, 0x0000f002, 0x00000003, + 0x00001003, 0x00002003, 0x00003003, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff + }; + + std::array constexpr decode_table_2 = { + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x00800f00, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x00c00f00, + 0x00000d00, 0x00400d00, 0x00800d00, 0x00c00d00, 0x00000e00, 0x00400e00, + 0x00800e00, 0x00c00e00, 0x00000f00, 0x00400f00, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x00000000, + 0x00400000, 0x00800000, 0x00c00000, 0x00000100, 0x00400100, 0x00800100, + 0x00c00100, 0x00000200, 0x00400200, 0x00800200, 0x00c00200, 0x00000300, + 0x00400300, 0x00800300, 0x00c00300, 0x00000400, 0x00400400, 0x00800400, + 0x00c00400, 0x00000500, 0x00400500, 0x00800500, 0x00c00500, 0x00000600, + 0x00400600, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x00800600, 0x00c00600, 0x00000700, 0x00400700, 0x00800700, + 0x00c00700, 0x00000800, 0x00400800, 0x00800800, 0x00c00800, 0x00000900, + 0x00400900, 0x00800900, 0x00c00900, 0x00000a00, 0x00400a00, 0x00800a00, + 0x00c00a00, 0x00000b00, 0x00400b00, 0x00800b00, 0x00c00b00, 0x00000c00, + 0x00400c00, 0x00800c00, 0x00c00c00, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff + }; + + std::array constexpr decode_table_3 = { + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x003e0000, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x003f0000, + 0x00340000, 0x00350000, 0x00360000, 0x00370000, 0x00380000, 0x00390000, + 0x003a0000, 0x003b0000, 0x003c0000, 0x003d0000, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x00000000, + 0x00010000, 0x00020000, 0x00030000, 0x00040000, 0x00050000, 0x00060000, + 0x00070000, 0x00080000, 0x00090000, 0x000a0000, 0x000b0000, 0x000c0000, + 0x000d0000, 0x000e0000, 0x000f0000, 0x00100000, 0x00110000, 0x00120000, + 0x00130000, 0x00140000, 0x00150000, 0x00160000, 0x00170000, 0x00180000, + 0x00190000, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x001a0000, 0x001b0000, 0x001c0000, 0x001d0000, 0x001e0000, + 0x001f0000, 0x00200000, 0x00210000, 0x00220000, 0x00230000, 0x00240000, + 0x00250000, 0x00260000, 0x00270000, 0x00280000, 0x00290000, 0x002a0000, + 0x002b0000, 0x002c0000, 0x002d0000, 0x002e0000, 0x002f0000, 0x00300000, + 0x00310000, 0x00320000, 0x00330000, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff + }; + + // TODO fix decoding tables to avoid the need for different indices in big + // endian? + inline constexpr size_t decidx0{ 0 }; + inline constexpr size_t decidx1{ 1 }; + inline constexpr size_t decidx2{ 2 }; + +#elif defined(__BIG_ENDIAN__) + + std::array constexpr decode_table_0 = { + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x00f80000, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x00fc0000, + 0x00d00000, 0x00d40000, 0x00d80000, 0x00dc0000, 0x00e00000, 0x00e40000, + 0x00e80000, 0x00ec0000, 0x00f00000, 0x00f40000, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x00000000, + 0x00040000, 0x00080000, 0x000c0000, 0x00100000, 0x00140000, 0x00180000, + 0x001c0000, 0x00200000, 0x00240000, 0x00280000, 0x002c0000, 0x00300000, + 0x00340000, 0x00380000, 0x003c0000, 0x00400000, 0x00440000, 0x00480000, + 0x004c0000, 0x00500000, 0x00540000, 0x00580000, 0x005c0000, 0x00600000, + 0x00640000, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x00680000, 0x006c0000, 0x00700000, 0x00740000, 0x00780000, + 0x007c0000, 0x00800000, 0x00840000, 0x00880000, 0x008c0000, 0x00900000, + 0x00940000, 0x00980000, 0x009c0000, 0x00a00000, 0x00a40000, 0x00a80000, + 0x00ac0000, 0x00b00000, 0x00b40000, 0x00b80000, 0x00bc0000, 0x00c00000, + 0x00c40000, 0x00c80000, 0x00cc0000, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff + }; + + std::array constexpr decode_table_1 = { + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x0003e000, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x0003f000, + 0x00034000, 0x00035000, 0x00036000, 0x00037000, 0x00038000, 0x00039000, + 0x0003a000, 0x0003b000, 0x0003c000, 0x0003d000, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x00000000, + 0x00001000, 0x00002000, 0x00003000, 0x00004000, 0x00005000, 0x00006000, + 0x00007000, 0x00008000, 0x00009000, 0x0000a000, 0x0000b000, 0x0000c000, + 0x0000d000, 0x0000e000, 0x0000f000, 0x00010000, 0x00011000, 0x00012000, + 0x00013000, 0x00014000, 0x00015000, 0x00016000, 0x00017000, 0x00018000, + 0x00019000, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x0001a000, 0x0001b000, 0x0001c000, 0x0001d000, 0x0001e000, + 0x0001f000, 0x00020000, 0x00021000, 0x00022000, 0x00023000, 0x00024000, + 0x00025000, 0x00026000, 0x00027000, 0x00028000, 0x00029000, 0x0002a000, + 0x0002b000, 0x0002c000, 0x0002d000, 0x0002e000, 0x0002f000, 0x00030000, + 0x00031000, 0x00032000, 0x00033000, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff + }; + + std::array constexpr decode_table_2 = { + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x00000f80, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x00000fc0, + 0x00000d00, 0x00000d40, 0x00000d80, 0x00000dc0, 0x00000e00, 0x00000e40, + 0x00000e80, 0x00000ec0, 0x00000f00, 0x00000f40, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x00000000, + 0x00000040, 0x00000080, 0x000000c0, 0x00000100, 0x00000140, 0x00000180, + 0x000001c0, 0x00000200, 0x00000240, 0x00000280, 0x000002c0, 0x00000300, + 0x00000340, 0x00000380, 0x000003c0, 0x00000400, 0x00000440, 0x00000480, + 0x000004c0, 0x00000500, 0x00000540, 0x00000580, 0x000005c0, 0x00000600, + 0x00000640, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x00000680, 0x000006c0, 0x00000700, 0x00000740, 0x00000780, + 0x000007c0, 0x00000800, 0x00000840, 0x00000880, 0x000008c0, 0x00000900, + 0x00000940, 0x00000980, 0x000009c0, 0x00000a00, 0x00000a40, 0x00000a80, + 0x00000ac0, 0x00000b00, 0x00000b40, 0x00000b80, 0x00000bc0, 0x00000c00, + 0x00000c40, 0x00000c80, 0x00000cc0, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff + }; + + std::array constexpr decode_table_3 = { + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x0000003e, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x0000003f, + 0x00000034, 0x00000035, 0x00000036, 0x00000037, 0x00000038, 0x00000039, + 0x0000003a, 0x0000003b, 0x0000003c, 0x0000003d, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x00000000, + 0x00000001, 0x00000002, 0x00000003, 0x00000004, 0x00000005, 0x00000006, + 0x00000007, 0x00000008, 0x00000009, 0x0000000a, 0x0000000b, 0x0000000c, + 0x0000000d, 0x0000000e, 0x0000000f, 0x00000010, 0x00000011, 0x00000012, + 0x00000013, 0x00000014, 0x00000015, 0x00000016, 0x00000017, 0x00000018, + 0x00000019, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x0000001a, 0x0000001b, 0x0000001c, 0x0000001d, 0x0000001e, + 0x0000001f, 0x00000020, 0x00000021, 0x00000022, 0x00000023, 0x00000024, + 0x00000025, 0x00000026, 0x00000027, 0x00000028, 0x00000029, 0x0000002a, + 0x0000002b, 0x0000002c, 0x0000002d, 0x0000002e, 0x0000002f, 0x00000030, + 0x00000031, 0x00000032, 0x00000033, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff, + 0x01ffffff, 0x01ffffff, 0x01ffffff, 0x01ffffff + }; + + // TODO fix decoding tables to avoid the need for different indices in big + // endian? + inline constexpr size_t decidx0{ 1 }; + inline constexpr size_t decidx1{ 2 }; + inline constexpr size_t decidx2{ 3 }; + +#endif + + std::array constexpr encode_table_0 = { + 'A', 'A', 'A', 'A', 'B', 'B', 'B', 'B', 'C', 'C', 'C', 'C', 'D', 'D', 'D', + 'D', 'E', 'E', 'E', 'E', 'F', 'F', 'F', 'F', 'G', 'G', 'G', 'G', 'H', 'H', + 'H', 'H', 'I', 'I', 'I', 'I', 'J', 'J', 'J', 'J', 'K', 'K', 'K', 'K', 'L', + 'L', 'L', 'L', 'M', 'M', 'M', 'M', 'N', 'N', 'N', 'N', 'O', 'O', 'O', 'O', + 'P', 'P', 'P', 'P', 'Q', 'Q', 'Q', 'Q', 'R', 'R', 'R', 'R', 'S', 'S', 'S', + 'S', 'T', 'T', 'T', 'T', 'U', 'U', 'U', 'U', 'V', 'V', 'V', 'V', 'W', 'W', + 'W', 'W', 'X', 'X', 'X', 'X', 'Y', 'Y', 'Y', 'Y', 'Z', 'Z', 'Z', 'Z', 'a', + 'a', 'a', 'a', 'b', 'b', 'b', 'b', 'c', 'c', 'c', 'c', 'd', 'd', 'd', 'd', + 'e', 'e', 'e', 'e', 'f', 'f', 'f', 'f', 'g', 'g', 'g', 'g', 'h', 'h', 'h', + 'h', 'i', 'i', 'i', 'i', 'j', 'j', 'j', 'j', 'k', 'k', 'k', 'k', 'l', 'l', + 'l', 'l', 'm', 'm', 'm', 'm', 'n', 'n', 'n', 'n', 'o', 'o', 'o', 'o', 'p', + 'p', 'p', 'p', 'q', 'q', 'q', 'q', 'r', 'r', 'r', 'r', 's', 's', 's', 's', + 't', 't', 't', 't', 'u', 'u', 'u', 'u', 'v', 'v', 'v', 'v', 'w', 'w', 'w', + 'w', 'x', 'x', 'x', 'x', 'y', 'y', 'y', 'y', 'z', 'z', 'z', 'z', '0', '0', + '0', '0', '1', '1', '1', '1', '2', '2', '2', '2', '3', '3', '3', '3', '4', + '4', '4', '4', '5', '5', '5', '5', '6', '6', '6', '6', '7', '7', '7', '7', + '8', '8', '8', '8', '9', '9', '9', '9', '+', '+', '+', '+', '/', '/', '/', + '/' + }; + + std::array constexpr encode_table_1 = { + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', + 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', + 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', + 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', + '8', '9', '+', '/', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', + 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', + 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', + '4', '5', '6', '7', '8', '9', '+', '/', 'A', 'B', 'C', 'D', 'E', 'F', 'G', + 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', + 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', + 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/', 'A', 'B', 'C', + 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', + 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', + 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', + 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', + '/' + }; + + } // namespace detail + + template + inline OutputBuffer encode_into(InputIterator begin, InputIterator end) + { + typedef std::decay_t input_value_type; + static_assert(std::is_same_v || + std::is_same_v || + std::is_same_v || + std::is_same_v); + typedef typename OutputBuffer::value_type output_value_type; + static_assert(std::is_same_v || + std::is_same_v || + std::is_same_v || + std::is_same_v); + const size_t binarytextsize = end - begin; + const size_t encodedsize = (binarytextsize / 3 + (binarytextsize % 3 > 0)) + << 2; + OutputBuffer encoded(encodedsize, detail::padding_char); + + const uint8_t* bytes = reinterpret_cast(&*begin); + char* currEncoding = reinterpret_cast(&encoded[0]); + + for (size_t i = binarytextsize / 3; i; --i) + { + const uint8_t t1 = *bytes++; + const uint8_t t2 = *bytes++; + const uint8_t t3 = *bytes++; + *currEncoding++ = detail::encode_table_0[t1]; + *currEncoding++ = + detail::encode_table_1[((t1 & 0x03) << 4) | ((t2 >> 4) & 0x0F)]; + *currEncoding++ = + detail::encode_table_1[((t2 & 0x0F) << 2) | ((t3 >> 6) & 0x03)]; + *currEncoding++ = detail::encode_table_1[t3]; + } + + switch (binarytextsize % 3) + { + case 0: + { + break; + } + case 1: + { + const uint8_t t1 = bytes[0]; + *currEncoding++ = detail::encode_table_0[t1]; + *currEncoding++ = detail::encode_table_1[(t1 & 0x03) << 4]; + break; + } + case 2: + { + const uint8_t t1 = bytes[0]; + const uint8_t t2 = bytes[1]; + *currEncoding++ = detail::encode_table_0[t1]; + *currEncoding++ = + detail::encode_table_1[((t1 & 0x03) << 4) | ((t2 >> 4) & 0x0F)]; + *currEncoding++ = detail::encode_table_1[(t2 & 0x0F) << 2]; + break; + } + default: + { + throw std::runtime_error{ "Invalid base64 encoded data" }; + } + } + + return encoded; + } + + template + inline OutputBuffer encode_into(std::string_view data) + { + return encode_into(std::begin(data), std::end(data)); + } + + inline std::string to_base64(std::string_view data) + { + return encode_into(std::begin(data), std::end(data)); + } + + template + inline OutputBuffer decode_into(std::string_view base64Text) + { + typedef typename OutputBuffer::value_type output_value_type; + static_assert(std::is_same_v || + std::is_same_v || + std::is_same_v || + std::is_same_v); + if (base64Text.empty()) + { + return OutputBuffer(); + } + + if ((base64Text.size() & 3) != 0) + { + throw std::runtime_error{ + "Invalid base64 encoded data - Size not divisible by 4" + }; + } + + const size_t numPadding = + std::count(base64Text.rbegin(), base64Text.rbegin() + 4, '='); + if (numPadding > 2) + { + throw std::runtime_error{ + "Invalid base64 encoded data - Found more than 2 padding signs" + }; + } + + const size_t decodedsize = (base64Text.size() * 3 >> 2) - numPadding; + OutputBuffer decoded(decodedsize, '.'); + + const uint8_t* bytes = reinterpret_cast(&base64Text[0]); + char* currDecoding = reinterpret_cast(&decoded[0]); + + for (size_t i = (base64Text.size() >> 2) - (numPadding != 0); i; --i) + { + const uint8_t t1 = *bytes++; + const uint8_t t2 = *bytes++; + const uint8_t t3 = *bytes++; + const uint8_t t4 = *bytes++; + + const uint32_t d1 = detail::decode_table_0[t1]; + const uint32_t d2 = detail::decode_table_1[t2]; + const uint32_t d3 = detail::decode_table_2[t3]; + const uint32_t d4 = detail::decode_table_3[t4]; + + const uint32_t temp = d1 | d2 | d3 | d4; + + if (temp >= detail::bad_char) + { + throw std::runtime_error{ + "Invalid base64 encoded data - Invalid character" + }; + } + + // Use bit_cast instead of union and type punning to avoid + // undefined behaviour risk: + // https://en.wikipedia.org/wiki/Type_punning#Use_of_union + const std::array tempBytes = + detail::bit_cast, uint32_t>(temp); + + *currDecoding++ = tempBytes[detail::decidx0]; + *currDecoding++ = tempBytes[detail::decidx1]; + *currDecoding++ = tempBytes[detail::decidx2]; + } + + switch (numPadding) + { + case 0: + { + break; + } + case 1: + { + const uint8_t t1 = *bytes++; + const uint8_t t2 = *bytes++; + const uint8_t t3 = *bytes++; + + const uint32_t d1 = detail::decode_table_0[t1]; + const uint32_t d2 = detail::decode_table_1[t2]; + const uint32_t d3 = detail::decode_table_2[t3]; + + const uint32_t temp = d1 | d2 | d3; + + if (temp >= detail::bad_char) + { + throw std::runtime_error{ + "Invalid base64 encoded data - Invalid character" + }; + } + + // Use bit_cast instead of union and type punning to avoid + // undefined behaviour risk: + // https://en.wikipedia.org/wiki/Type_punning#Use_of_union + const std::array tempBytes = + detail::bit_cast, uint32_t>(temp); + *currDecoding++ = tempBytes[detail::decidx0]; + *currDecoding++ = tempBytes[detail::decidx1]; + break; + } + case 2: + { + const uint8_t t1 = *bytes++; + const uint8_t t2 = *bytes++; + + const uint32_t d1 = detail::decode_table_0[t1]; + const uint32_t d2 = detail::decode_table_1[t2]; + + const uint32_t temp = d1 | d2; + + if (temp >= detail::bad_char) + { + throw std::runtime_error{ + "Invalid base64 encoded data - Invalid character" + }; + } + + const std::array tempBytes = + detail::bit_cast, uint32_t>(temp); + *currDecoding++ = tempBytes[detail::decidx0]; + break; + } + default: + { + throw std::runtime_error{ + "Invalid base64 encoded data - Invalid padding number" + }; + } + } + + return decoded; + } + + template + inline OutputBuffer decode_into(InputIterator begin, InputIterator end) + { + typedef std::decay_t input_value_type; + static_assert(std::is_same_v || + std::is_same_v || + std::is_same_v || + std::is_same_v); + std::string_view data(reinterpret_cast(&*begin), end - begin); + return decode_into(data); + } + + inline std::string from_base64(std::string_view data) + { + return decode_into(data); + } + +} // namespace base64 + +#endif // BASE64_HPP_ diff --git a/sonar-project.properties b/sonar-project.properties index b5f7fdd1..b0e8c28f 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -31,7 +31,13 @@ sonar.cpd.exclusions=${env.SONAR_DUPLICATION_EXCLUSIONS} # Project specific ignored rules # NOTE: Please update scan details in SCA document # when making updates here! -sonar.issue.ignore.multicriteria=e1 +sonar.issue.ignore.multicriteria=e1,e2 + +# Variables should not be shadowed [cpp:S1117] +# +# We allow shadowing of variables in case they need to be stored for later use. +sonar.issue.ignore.multicriteria.e1.ruleKey=cpp:S1117 +sonar.issue.ignore.multicriteria.e1.resourceKey=**/*.?pp # The "Rule-of-Zero" should be followed [cpp:S4963] # @@ -40,5 +46,5 @@ sonar.issue.ignore.multicriteria=e1 # should be relaxed and rule of five should be followed. # S3624 does exactly this: When the "Rule-of-Zero" is not applicable, the "Rule-of-Five" should be followed # See https://community.sonarsource.com/t/how-to-fix-a-the-rule-of-zero-should-be-followed/20656/6 -sonar.issue.ignore.multicriteria.e1.ruleKey=cpp:S4963 -sonar.issue.ignore.multicriteria.e1.resourceKey=**/*.?pp +sonar.issue.ignore.multicriteria.e2.ruleKey=cpp:S4963 +sonar.issue.ignore.multicriteria.e2.resourceKey=**/*.?pp