diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index b498d9eb..19526bf4 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -22,16 +22,25 @@ jobs: env: SONAR_SERVER_URL: "https://sonarcloud.io" steps: + - run: du -sh / || true + - run: du -sh . || true + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: fetch-depth: 0 # Disable shallow clone to enable blame information persist-credentials: false + - run: du -sh / || true + - run: du -sh . || true + - uses: hendrikmuhs/ccache-action@5ebbd400eff9e74630f759d94ddd7b6c26299639 # v1.2.20 with: key: ${{ github.job }} max-size: 2G + - run: du -sh / || true + - run: du -sh . || true + - name: Build for coverage uses: lukka/run-cmake@af1be47fd7c933593f687731bc6fdbee024d3ff4 # v10.8 with: @@ -42,28 +51,51 @@ jobs: env: GTEST_OUTPUT: "xml:${{ github.workspace }}/testresults/" + - run: du -sh / || true + - run: du -sh . || true + - 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 + + - run: du -sh / || true + - run: du -sh . || true - name: Collect coverage run: | gcovr --sonarqube=coverage.xml --exclude-lines-by-pattern '.*assert\(.*\);|.*really_assert\(.*\);|.*std::abort();' --exclude-unreachable-branches --exclude-throw-branches -j "$(nproc)" --exclude=.*/example/.* --exclude=.*/external/.* --exclude=.*/test/.* + - run: du -sh / || true + - run: du -sh . || true + - uses: philips-software/sonarqube-issue-conversion@9e9958764ba5fd1d302b039779dc902bedfa4d01 # v1.2.0 with: input: ${{ github.workspace }}/testresults/*.xml output: execution.xml transformation: gtest-to-generic-execution + - run: du -sh / || true + - run: du -sh . || true + - name: Convert results run: | cp .build/Coverage/compile_commands.json compile_commands.json - - uses: sonarsource/sonarqube-scan-action@fd88b7d7ccbaefd23d8f36f73b59db7a3d246602 # v6.0.0 + - run: du -sh / || true + - run: du -sh . || true + + - run: cmake --build --preset Coverage --target clean + + - run: du -sh / || true + - run: du -sh . || true + + - uses: sonarsource/sonarqube-scan-action@a31c9398be7ace6bbfaf30c0bd5d415f843d45e9 # v7.0.0 env: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + - run: du -sh / || true + - run: du -sh . || true + codeql: name: CodeQL runs-on: ubuntu-latest 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..46ed6118 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -15,7 +15,11 @@ "@result:UNDEFINED", "@result:FAILED", "@result:OK", - "@fail_feature" + "@fail_feature", + "@ex:2", + "@substep", + "@table_argument", + "@thishasarule" ] }, { @@ -24,7 +28,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 +42,47 @@ "request": "launch", "program": "${command:cmake.launchTargetPath}", "args": [ - "run", - "--feature", - "${input:features}", - "--report", - "console", - "junit-xml", - // "--com", - // "COMx", - // "--nordic", - "--tag", - "${input:tag}" + "--com", + "COMx", + "--nordic", + "--format", + "summary", + "--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 - 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..fb6a6387 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,13 +11,9 @@ endif() if (CCR_STANDALONE) set(CCR_DEFAULTOPT On) -else() - set(CCR_DEFAULTOPT Off) -endif() - -if (CCR_STANDALONE) set(CCR_EXCLUDE_FROM_ALL "") else() + set(CCR_DEFAULTOPT Off) set(CCR_EXCLUDE_FROM_ALL "EXCLUDE_FROM_ALL") endif() @@ -25,6 +21,13 @@ option(CCR_FETCH_DEPS "Fetch dependencies via FetchContent." On ) 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 (NOT (CMAKE_CXX_COMPILER_ID MATCHES "MSVC" OR CMAKE_CXX_SIMULATE_ID MATCHES "MSVC")) + add_compile_options(-fsanitize=address) + add_link_options(-fsanitize=address) +endif() + if (CCR_BUILD_TESTS) ccr_enable_testing() endif() @@ -68,3 +71,4 @@ else() endif() add_subdirectory(cucumber_cpp) +add_subdirectory(compatibility) 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/compatibility/CMakeLists.txt b/compatibility/CMakeLists.txt new file mode 100644 index 00000000..11237dbd --- /dev/null +++ b/compatibility/CMakeLists.txt @@ -0,0 +1,48 @@ +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}) + message(STATUS "Adding compatibility kit: ${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..655bf82d --- /dev/null +++ b/compatibility/compatibility.cpp @@ -0,0 +1,289 @@ +#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/Duration.hpp" +#include "cucumber_cpp/library/support/SupportCodeLibrary.hpp" +#include "cucumber_cpp/library/support/Timestamp.hpp" +#include "cucumber_cpp/library/support/Types.hpp" +#include "cucumber_cpp/library/tag_expression/Parser.hpp" +#include "cucumber_cpp/library/util/Broadcaster.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::support::Stopwatch + { + virtual ~StopwatchIncremental() = default; + + void Start() override + { + } + + std::chrono::nanoseconds Duration() override + { + return current; + } + + std::chrono::nanoseconds current{ std::chrono::milliseconds{ 1 } }; + }; + + struct TimestampGeneratorIncremental : cucumber_cpp::library::support::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, + }, + }; + + 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, { "summary" }, {}); + + 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..d6b5a532 100644 --- a/cucumber_cpp/CMakeLists.txt +++ b/cucumber_cpp/CMakeLists.txt @@ -1,5 +1,3 @@ - - add_subdirectory(library) add_subdirectory(runner) add_subdirectory(example) @@ -12,7 +10,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..6d78cf7e 100644 --- a/cucumber_cpp/CucumberCpp.hpp +++ b/cucumber_cpp/CucumberCpp.hpp @@ -4,9 +4,10 @@ #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 { @@ -14,7 +15,6 @@ namespace cucumber_cpp using cucumber_cpp::library::Context; using cucumber_cpp::library::engine::Step; using cucumber_cpp::library::engine::StringTo; - using cucumber_cpp::library::report::ReportHandlerV2; } #endif 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/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..3d4dc920 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") diff --git a/cucumber_cpp/acceptance_test/steps/CMakeLists.txt b/cucumber_cpp/acceptance_test/steps/CMakeLists.txt index a5185d8c..25d017d4 100644 --- a/cucumber_cpp/acceptance_test/steps/CMakeLists.txt +++ b/cucumber_cpp/acceptance_test/steps/CMakeLists.txt @@ -7,5 +7,5 @@ target_sources(cucumber_cpp.acceptance_test.steps PRIVATE ) target_link_libraries(cucumber_cpp.acceptance_test.steps PRIVATE - cucumber_cpp.library + cucumber_cpp ) diff --git a/cucumber_cpp/acceptance_test/steps/Steps.cpp b/cucumber_cpp/acceptance_test/steps/Steps.cpp index 063e60b3..dad2e57f 100644 --- a/cucumber_cpp/acceptance_test/steps/Steps.cpp +++ b/cucumber_cpp/acceptance_test/steps/Steps.cpp @@ -2,6 +2,7 @@ #include "gmock/gmock.h" #include "gtest/gtest.h" #include +#include #include #include #include @@ -33,7 +34,7 @@ STEP("a step step") WHEN("I print {string}", (const std::string& str)) { - std::cout << "print: " << str; + std::cout << std::format("print: {}\n", str); } THEN("an assertion is raised") @@ -59,8 +60,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") diff --git a/cucumber_cpp/acceptance_test/test.bats b/cucumber_cpp/acceptance_test/test.bats index ed9ccbdf..239ea416 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,64 @@ 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 --tags "@result:OK" -- 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 --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 --tags "@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 + run $acceptance_test --format summary pretty --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 "UNDEFINED Given a missing step" + assert_output --partial "SKIPPED 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 --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 -- cucumber_cpp/acceptance_test/features/subfolder assert_success - assert_output --partial "test1 scenario" - assert_output --partial "test2 scenario" + assert_output --partial "2 scenarios" + assert_output --partial "2 passed" } -@test "Missing mandatory feature argument" { - run .build/Host/cucumber_cpp/acceptance_test/Debug/cucumber_cpp.acceptance_test run --report console +@test "Missing mandatory paths argument" { + run $acceptance_test --format summary pretty -- 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 "paths is required" } @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 -- 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 + run $acceptance_test --format summary pretty --tags "@undefinedsuccess and @result:success" -- cucumber_cpp/acceptance_test/features/test_undefined_success_1.feature cucumber_cpp/acceptance_test/features/test_undefined_success_2.feature assert_success } @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 --tags @bats and @program_hooks -- cucumber_cpp/acceptance_test/features assert_success assert_output --partial "HOOK_BEFORE_ALL" @@ -83,7 +80,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 --tags @bats and @scenariohook and not @stephook -- cucumber_cpp/acceptance_test/features assert_success assert_output --partial "HOOK_BEFORE_SCENARIO" @@ -94,7 +91,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 --tags @bats and @stephook and not @scenariohook -- cucumber_cpp/acceptance_test/features assert_success refute_output --partial "HOOK_BEFORE_SCENARIO" @@ -105,7 +102,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 --tags "@bats and (@scenariohook or @stephook)" -- cucumber_cpp/acceptance_test/features assert_success assert_output --partial "HOOK_BEFORE_SCENARIO" @@ -116,92 +113,93 @@ 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 --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 --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 --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 pretty --tags "@result:UNDEFINED" --dry-run cucumber_cpp/acceptance_test/features + assert_success + assert_output --partial "UNDEFINED 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 --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 --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 --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 --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 pretty --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 "FAILED 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 pretty --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 "FAILED 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 pretty --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 "FAILED 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 --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 --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 --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 --tags "@unused_steps" -- cucumber_cpp/acceptance_test/features assert_success refute_output --partial "The following steps have not been used:" } 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..111f1025 100644 --- a/cucumber_cpp/example/steps/Steps.cpp +++ b/cucumber_cpp/example/steps/Steps.cpp @@ -3,47 +3,52 @@ #include "gmock/gmock.h" #include "gtest/gtest.h" #include -#include #include 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 +74,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$)") +{ + Given(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..466734f4 100644 --- a/cucumber_cpp/library/Application.cpp +++ b/cucumber_cpp/library/Application.cpp @@ -1,42 +1,39 @@ #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/StepRegistry.hpp" +#include "cucumber_cpp/library/support/Types.hpp" +#include "cucumber_cpp/library/tag_expression/Parser.hpp" +#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 { @@ -64,75 +61,80 @@ namespace cucumber_cpp::library return std::filesystem::is_regular_file(entry) && entry.path().has_extension() && entry.path().extension() == ".feature"; } - std::vector GetFeatureFiles(Application::Options& options) + std::set GetFeatureFiles(Application::Options& options) { - std::vector files; + std::set files; - for (const auto feature : options.features | std::views::transform(ToFileSystemPath)) + for (const auto feature : options.paths | std::views::transform(ToFileSystemPath)) if (std::filesystem::is_directory(feature)) for (const auto& entry : std::filesystem::directory_iterator{ feature } | std::views::filter(IsFeatureFile)) - files.emplace_back(entry.path()); + files.emplace(entry.path()); else - files.emplace_back(feature); + files.emplace(feature); return files; } } - 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 } - { - gherkin.include_source(false); - gherkin.include_ast(true); - gherkin.include_pickles(true); - - cli.require_subcommand(1); - - runCommand = cli.add_subcommand("run")->parse_complete_callback([this] + cli.parse_complete_callback([this] { RunFeatures(); }); - - 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); - - 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"); - - reporters.Add("console", std::make_unique()); - reporters.Add("junit-xml", std::make_unique(options.outputfolder, options.reportfile)); - - ProgramContext().InsertRef(options); } int Application::Run(int argc, const char* const* argv) { + const auto formattersSet = formatters.GetAvailableFormatterNames(); + const auto formatterDescription = std::format("{{{}}}", Join(formattersSet | std::views::transform([](const auto& pair) + { + if (pair.second) + return std::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); + + if (iter == formattersSet.end()) + return std::format("'{}' is not a valid formatter", option.name); + else + return ""; + }, + formatterDescription }; + try { - const auto reportDescription = runCommand->get_option("--report")->get_description(); - const auto joinedReporters = reportDescription + Join(reporters.AvailableReporters(), ", "); + const std::map> orderingMap{ + { "defined", support::RunOptions::Ordering::defined }, + { "reverse", support::RunOptions::Ordering::reverse }, + }; + + cli.add_flag("-d,--dry-run", options.dryRun, "Perform a dry run without executing steps"); + cli.add_flag("--fail-fast", options.failFast, "Stop execution on first failure"); + cli.add_option("--format", options.format, "specify the output format, optionally supply PATH to redirect formatter output.")->check(formatValidator); + cli.add_option("--format-options", options.formatOptions, "provide options for formatters"); + cli.add_option("--language", options.language, "Default langauge 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)); + 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::deprecate_option(cli.add_option("--tag", options.tags, "Cucumber tag expression"), "-t,--tags"); + cli.add_option("-t,--tags", options.tags, "Cucumber tag expression"); + + CLI::deprecate_option(cli.add_option("-f,--feature", options.paths, "Paths to where your feature files are")->check(CLI::ExistingPath), "paths"); + cli.add_option("paths", options.paths, "Paths to where your feature files are")->required()->check(CLI::ExistingPath); + + ProgramContext().InsertRef(options); - runCommand->get_option("--report")->description(joinedReporters); cli.parse(argc, argv); } catch (const CLI::ParseError& e) @@ -141,32 +143,23 @@ 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 << std::format("InternalError error:\n{}\n", error.what()); + return 1; } 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 << std::format("Cucumber Expression error:\n{}\n", error.what()); + return 1; } catch (const std::exception& error) { - std::cout << "Generic error:\n\n"; - std::cout << error.what() << std::endl; - return GetExitCode(engine::Result::failed); + std::cout << std::format("Generic error:\n{}\n", error.what()); + return 1; } catch (...) { std::cout << "Unknown error"; - return GetExitCode(engine::Result::failed); + return 1; } return GetExitCode(); @@ -174,94 +167,46 @@ namespace cucumber_cpp::library 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) + api::Formatters& Application::Formatters() { - reporters.Add(name, std::move(reporter)); + return formatters; } void Application::RunFeatures() { - 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; - } - - void Application::PrintStepsNotUsed(const StepRegistry& stepRegistry) const - { - auto isUnused = [](const StepRegistry::EntryView& entry) - { - return entry.used == 0; + const auto runOptions = support::RunOptions{ + .sources = { + .paths = GetFeatureFiles(options), + .tagExpression = tag_expression::Parse(Join(options.tags, " ")), + .ordering = options.ordering, + }, + .runtime = { + .dryRun = options.dryRun, + .failFast = options.failFast, + .retry = options.retry, + .strict = options.strict, + .retryTagExpression = tag_expression::Parse(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; + runPassed = api::RunCucumber(runOptions, parameterRegistry, *programContext, broadcaster, formatters, options.format, options.formatOptions); } 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); + return runPassed ? EXIT_SUCCESS : EXIT_FAILURE; } } diff --git a/cucumber_cpp/library/Application.hpp b/cucumber_cpp/library/Application.hpp index 989baea1..cebde23a 100644 --- a/cucumber_cpp/library/Application.hpp +++ b/cucumber_cpp/library/Application.hpp @@ -4,44 +4,49 @@ // 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/Duration.hpp" +#include "cucumber_cpp/library/support/StepRegistry.hpp" +#include "cucumber_cpp/library/support/SupportCodeLibrary.hpp" +#include "cucumber_cpp/library/support/Timestamp.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 { - struct ReportHandlerValidator : public CLI::Validator - { - explicit ReportHandlerValidator(const report::Reporters& reporters); - }; - struct Application { struct Options { - std::vector tags{}; - std::vector features{}; - std::vector reporters{}; + std::set> paths{}; + + bool dryRun{ false }; + bool failFast{ false }; + + std::set> format{}; + std::string formatOptions{}; + + 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 }; + + std::vector tags{}; }; explicit Application(std::shared_ptr contextStorageFactory = std::make_shared(), bool removeDefaultGoogleTestListener = true); @@ -50,33 +55,33 @@ namespace cucumber_cpp::library 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; 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; + support::StopWatchHighResolutionClock stopwatchHighResolutionClock; + support::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..ece95257 100644 --- a/cucumber_cpp/library/CMakeLists.txt +++ b/cucumber_cpp/library/CMakeLists.txt @@ -1,27 +1,18 @@ 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 + Parameter.hpp Rtrim.cpp Rtrim.hpp - StepRegistry.cpp - StepRegistry.hpp Steps.hpp - TagExpression.cpp - TagExpression.hpp - TagsToSet.hpp - TraceTime.cpp - TraceTime.hpp ) target_include_directories(cucumber_cpp.library PUBLIC @@ -32,10 +23,12 @@ 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 + cucumber_cpp.library.formatter + cucumber_cpp.library.support CLI11 ) @@ -47,12 +40,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/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..4bcd7410 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,10 +18,10 @@ 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__) 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..ba854f4b --- /dev/null +++ b/cucumber_cpp/library/api/Formatters.cpp @@ -0,0 +1,68 @@ +#include "cucumber_cpp/library/api/Formatters.hpp" +#include "cucumber_cpp/library/formatter/Formatter.hpp" +#include "cucumber_cpp/library/formatter/PrettyPrinter.hpp" +#include "cucumber_cpp/library/formatter/SummaryFormatter.hpp" +#include "cucumber_cpp/library/formatter/helper/EventDataCollector.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(); + } + + 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, const formatter::helper::EventDataCollector& eventDataCollector, 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, eventDataCollector, 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, eventDataCollector, 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..1ade2382 --- /dev/null +++ b/cucumber_cpp/library/api/Formatters.hpp @@ -0,0 +1,67 @@ +#ifndef API_FORMATTERS_HPP +#define API_FORMATTERS_HPP + +#include "cucumber_cpp/library/formatter/Formatter.hpp" +#include "cucumber_cpp/library/formatter/helper/EventDataCollector.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 formatter::helper::EventDataCollector&, 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, const formatter::helper::EventDataCollector& eventDataCollector, 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 formatter::helper::EventDataCollector& eventDataCollector, const nlohmann::json& formatOptions, std::ostream& output) + { + return std::make_unique(supportCodeLibrary, query, eventDataCollector, 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..3908dc27 --- /dev/null +++ b/cucumber_cpp/library/api/RunCucumber.cpp @@ -0,0 +1,201 @@ +#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/formatter/helper/EventDataCollector.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, + }; + + formatter::helper::EventDataCollector eventDataCollector{ broadcaster }; + 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, eventDataCollector); + + 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..a8ed1e7f --- /dev/null +++ b/cucumber_cpp/library/cucumber_expression/Argument.cpp @@ -0,0 +1,44 @@ +#include "cucumber_cpp/library/cucumber_expression/Argument.hpp" +#include "cucumber/messages/group.hpp" +#include "cucumber_cpp/library/cucumber_expression/ParameterRegistry.hpp" +#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(std::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..ce67f181 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,23 @@ 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 +) + 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..197a0120 100644 --- a/cucumber_cpp/library/cucumber_expression/Errors.cpp +++ b/cucumber_cpp/library/cucumber_expression/Errors.cpp @@ -7,6 +7,7 @@ #include #include #include +#include namespace cucumber_cpp::library::cucumber_expression { @@ -155,7 +156,7 @@ 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, @@ -163,5 +164,7 @@ For more complicated expressions consider using a regular expression instead.)", std::format(R"(Undefined parameter type '{}')", undefinedParameterName), std::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..79dfdfaa 100644 --- a/cucumber_cpp/library/cucumber_expression/Expression.cpp +++ b/cucumber_cpp/library/cucumber_expression/Expression.cpp @@ -1,14 +1,12 @@ #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 -#include #include -#include -#include #include #include #include @@ -24,8 +22,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 +36,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) @@ -148,25 +133,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 = std::format(R"(({}))", parameter.regex.front()); + else + { + partialRegex = { parameter.regex.front() }; + for (const auto& parameterRegex : parameter.regex | std::views::drop(1)) + partialRegex += R"()|(?:)" + parameterRegex; + partialRegex = std::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) 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..54feb15d 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 -#include +#include #include #include #include #include -#include #include +#include #include +#include #include -#include +#include #include namespace cucumber_cpp::library::cucumber_expression @@ -20,88 +21,89 @@ 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; - } - - 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 } }, CreateStreamConverter()); + 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 }); } - Parameter ParameterRegistry::Lookup(const std::string& name) const + const std::map& ParameterRegistry::GetParameters() const { - if (parameters.contains(name)) - return parameters.at(name); - return {}; + return parametersByName; } - void ParameterRegistry::AddParameter(std::string name, std::vector regex, std::function converter) + const Parameter& ParameterRegistry::Lookup(const std::string& name) const { - if (parameters.contains(name)) + return parametersByName.at(name); + } + + void ParameterRegistry::AssertParameterIsUnique(const std::string& name) const + { + 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) }; } + } + + 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..bd52ec46 100644 --- a/cucumber_cpp/library/cucumber_expression/ParameterRegistry.hpp +++ b/cucumber_cpp/library/cucumber_expression/ParameterRegistry.hpp @@ -1,28 +1,48 @@ #ifndef CUCUMBER_EXPRESSION_PARAMETERREGISTRY_HPP #define CUCUMBER_EXPRESSION_PARAMETERREGISTRY_HPP +#include "cucumber/messages/group.hpp" #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::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; @@ -95,52 +115,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..57a772d5 100644 --- a/cucumber_cpp/library/cucumber_expression/test/TestExpression.cpp +++ b/cucumber_cpp/library/cucumber_expression/test/TestExpression.cpp @@ -1,4 +1,6 @@ +#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" @@ -6,19 +8,14 @@ #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 std::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())) + ASSERT_ANY_THROW(Match(testdata["expression"].as(), testdata["text"].as())) << std::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/TestTransformation.cpp b/cucumber_cpp/library/cucumber_expression/test/TestTransformation.cpp index 2f5fed80..2295ab21 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 "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 @@ -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)) { 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..8a90a72c --- /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/support/Timestamp.hpp" +#include "cucumber_cpp/library/util/Broadcaster.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 = support::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..5a3738b6 100644 --- a/cucumber_cpp/library/engine/Step.cpp +++ b/cucumber_cpp/library/engine/Step.cpp @@ -1,36 +1,32 @@ #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/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 } + Step::Step(util::Broadcaster& broadCaster, Context& context, engine::StepOrHookStarted stepOrHookStarted, const std::optional& dataTable, const std::optional& docString) + : ExecutionContext{ broadCaster, context, stepOrHookStarted } + , dataTable{ dataTable } , docString{ docString } {} void Step::Given(const std::string& step) const { - TestRunner::Instance().NestedStep(StepType::given, step); + // CucumberTestServer::Instance()->RunStep(step, cucumber::messages::pickle_step_type::CONTEXT); } void Step::When(const std::string& step) const { - TestRunner::Instance().NestedStep(StepType::when, step); + // CucumberTestServer::Instance()->RunStep(step, cucumber::messages::pickle_step_type::ACTION); } void Step::Then(const std::string& step) const { - TestRunner::Instance().NestedStep(StepType::then, step); - } - - void Step::Pending(const std::string& message, std::source_location current) noexcept(false) - { - throw StepPending{ message, current }; + // CucumberTestServer::Instance()->RunStep(step, cucumber::messages::pickle_step_type::OUTCOME); } } diff --git a/cucumber_cpp/library/engine/Step.hpp b/cucumber_cpp/library/engine/Step.hpp index de95fcd2..34758c84 100644 --- a/cucumber_cpp/library/engine/Step.hpp +++ b/cucumber_cpp/library/engine/Step.hpp @@ -4,30 +4,24 @@ // 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/messages/pickle_table_row.hpp" #include "cucumber_cpp/library/Context.hpp" -#include "cucumber_cpp/library/engine/Table.hpp" +#include "cucumber_cpp/library/engine/ExecutionContext.hpp" +#include "cucumber_cpp/library/util/Broadcaster.hpp" #include +#include #include +#include #include #include namespace cucumber_cpp::library::engine { - struct Step + struct Step : 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); + Step(util::Broadcaster& broadCaster, Context& context, engine::StepOrHookStarted stepOrHookStarted, const std::optional& dataTable, const std::optional& docString); virtual ~Step() = default; virtual void SetUp() @@ -45,11 +39,8 @@ namespace cucumber_cpp::library::engine void When(const std::string& step) const; void Then(const std::string& step) const; - [[noreturn]] static void Pending(const std::string& message, std::source_location current = std::source_location::current()) noexcept(false); - - 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..c0d0e72a 100644 --- a/cucumber_cpp/library/engine/test/TestStep.cpp +++ b/cucumber_cpp/library/engine/test/TestStep.cpp @@ -1,13 +1,12 @@ +#include "cucumber/messages/pickle_step_argument.hpp" #include "cucumber_cpp/library/Context.hpp" -#include "cucumber_cpp/library/engine/ContextManager.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/util/Broadcaster.hpp" #include "gmock/gmock.h" -#include +#include "gtest/gtest.h" +#include #include -#include namespace cucumber_cpp::library::engine { @@ -18,27 +17,33 @@ namespace cucumber_cpp::library::engine MOCK_METHOD(void, SetUp, (), (override)); MOCK_METHOD(void, TearDown, (), (override)); - using Step::context; - using Step::Given; using Step::Pending; - using Step::table; + using Step::Skipped; + + using Step::Given; using Step::Then; using Step::When; + + using Step::context; + using Step::dataTable; + using Step::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; + + StepMock step{ + broadcaster, + context, + stepOrHookStarted, + pickleStepArgument.data_table, + pickleStepArgument.doc_string }; - - const std::string docString = "multiline \n string"; - - library::engine::test_helper::ContextManagerInstance contextManager; - - engine::test_helper::TestRunnerMock testRunnerMock; - StepMock step{ contextManager.StepContext(), table, docString }; }; TEST_F(TestStep, StepProvidesAccessToSetUpFunction) @@ -57,21 +62,21 @@ namespace cucumber_cpp::library::engine 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())); - } + // 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())); + // } 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..3ce3dd9e --- /dev/null +++ b/cucumber_cpp/library/formatter/CMakeLists.txt @@ -0,0 +1,32 @@ +add_library(cucumber_cpp.library.formatter ${CCR_EXCLUDE_FROM_ALL}) + +target_sources(cucumber_cpp.library.formatter PRIVATE + Formatter.cpp + Formatter.hpp + PrettyPrinter.cpp + PrettyPrinter.hpp + SummaryFormatter.cpp + SummaryFormatter.hpp +) + +target_include_directories(cucumber_cpp.library.formatter PUBLIC + ../../.. +) + +target_link_libraries(cucumber_cpp.library.formatter PUBLIC + cpp-terminal + cucumber_cpp.library.cucumber_expression + cucumber_cpp.library.formatter.helper + cucumber_cpp.library.query + cucumber_cpp.library.support + cucumber_cpp.library.util + nlohmann_json +) + +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..e172a82f --- /dev/null +++ b/cucumber_cpp/library/formatter/Formatter.cpp @@ -0,0 +1,25 @@ +#include "cucumber_cpp/library/formatter/Formatter.hpp" +#include "cucumber/messages/envelope.hpp" +#include "cucumber_cpp/library/formatter/helper/EventDataCollector.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 helper::EventDataCollector& eventDataCollector, const nlohmann::json& formatOptions, std::ostream& outputStream) + : util::Listener{ query, [this](const cucumber::messages::envelope& envelope) + { + OnEnvelope(envelope); + } } + , supportCodeLibrary{ supportCodeLibrary } + , query{ query } + , eventDataCollector{ eventDataCollector } + , 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..83447601 --- /dev/null +++ b/cucumber_cpp/library/formatter/Formatter.hpp @@ -0,0 +1,33 @@ +#ifndef FORMATTER_FORMATTER_HPP +#define FORMATTER_FORMATTER_HPP + +#include "cucumber/messages/envelope.hpp" +#include "cucumber_cpp/library/formatter/helper/EventDataCollector.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 helper::EventDataCollector& eventDataCollector, 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 helper::EventDataCollector& eventDataCollector; + const nlohmann::json& formatOptions; + std::ostream& outputStream; + }; +} + +#endif diff --git a/cucumber_cpp/library/formatter/PrettyPrinter.cpp b/cucumber_cpp/library/formatter/PrettyPrinter.cpp new file mode 100644 index 00000000..2283b45d --- /dev/null +++ b/cucumber_cpp/library/formatter/PrettyPrinter.cpp @@ -0,0 +1,203 @@ +#include "cucumber_cpp/library/formatter/PrettyPrinter.hpp" +#include "cucumber/messages/attachment.hpp" +#include "cucumber/messages/envelope.hpp" +#include "cucumber/messages/feature.hpp" +#include "cucumber/messages/location.hpp" +#include "cucumber/messages/pickle.hpp" +#include "cucumber/messages/pickle_step.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/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/helper/GetColorFunctions.hpp" +#include "cucumber_cpp/library/support/Join.hpp" +#include "cucumber_cpp/library/support/Polyfill.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace cucumber_cpp::library::formatter +{ + namespace + { + std::string FormatPickleTitle(const cucumber::messages::pickle& pickle, const cucumber::messages::scenario& scenario) + { + return std::format("{}: {}", scenario.keyword, pickle.name); + } + + std::string FormatStepTitle(const cucumber::messages::test_step& testStep, const cucumber::messages::pickle_step& pickleStep, const cucumber::messages::step& step) + { + return std::format("{}{}", step.keyword, pickleStep.text); + } + } + + void PrettyPrinter::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.test_step_finished) + { + HandleTestStepFinished(envelope.test_step_finished.value()); + } + } + + void PrettyPrinter::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 = FormatPickleTitle(pickle, scenario).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 FormatStepTitle(testStep, *pickleStep, step).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, maxStepLength); + + std::size_t scenarioIndent{ 0 }; + scenarioIndent += 2; + if (lineage.rule) + scenarioIndent += 2; + scenarioIndentByTestCaseStartedId[testCaseStarted.id] = scenarioIndent; + } + + void PrettyPrinter::HandleTestCaseStarted(const cucumber::messages::test_case_started& testCaseStarted) + { + const auto& pickle = query.FindPickleBy(testCaseStarted); + const auto& location = query.FindLocationOf(pickle); + 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); + + PrintFeatureLine(*feature); + if (rule) + PrintRuleLine(*rule); + outputStream << "\n"; + PrintTags(pickle, scenarioIndent); + PrintScenarioLine(pickle, *scenario, scenarioIndent, maxContentLength); + } + + void PrettyPrinter::HandleAttachment(const cucumber::messages::attachment& attachment) + { + /* TODO implement */ + } + + void PrettyPrinter::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; + + PrintStepLine(testStepFinished, testStep, *pickleStep, step, stepDefinition, scenarioIndent, maxContentLength); + } + } + + void PrettyPrinter::HandleTestRunFinished(const cucumber::messages::test_run_finished& testRunFinished) + { + /* TODO implement */ + } + + void PrettyPrinter::PrintFeatureLine(const cucumber::messages::feature& feature) + { + if (printedFeatureUris.contains(&feature)) + return; + + support::print(outputStream, "{}: {}\n", feature.keyword, feature.name); + printedFeatureUris.insert(&feature); + } + + void PrettyPrinter::PrintRuleLine(const cucumber::messages::rule& rule) + { + if (printedRuleIds.contains(&rule)) + return; + + support::print(outputStream, "{:{}}{}: {}\n", "", 2, rule.keyword, rule.name); + printedRuleIds.insert(&rule); + } + + void PrettyPrinter::PrintTags(const cucumber::messages::pickle& pickle, std::size_t scenarioIndent) + { + if (pickle.tags.empty()) + return; + + auto tags = pickle.tags | std::views::transform([](const cucumber::messages::pickle_tag& tag) + { + return tag.name; + }); + std::vector tagVec{ tags.begin(), tags.end() }; + support::print(outputStream, "{:{}}{}\n", "", scenarioIndent, helper::ColorFunctions::Tag(support::Join(tagVec, " "))); + } + + void PrettyPrinter::PrintScenarioLine(const cucumber::messages::pickle& pickle, const cucumber::messages::scenario& scenario, std::size_t scenarioIndent, std::size_t maxContentLength) + { + PrintGherkinLine(std::format("{}: {}", scenario.keyword, pickle.name), nullptr, pickle.uri, scenario.location.line, scenarioIndent, maxContentLength); + } + + void PrettyPrinter::PrintStepLine(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) + { + const auto uri = stepDefinition ? std::make_optional(*stepDefinition->source_reference.uri) : std::nullopt; + const auto line = stepDefinition ? std::make_optional(stepDefinition->source_reference.location->line) : std::nullopt; + + PrintGherkinLine(std::format("{}{}", step.keyword, pickleStep.text), helper::ColorFunctions::ForStatus(testStepFinished.test_step_result.status), uri, line, scenarioIndent + 2, maxContentLength); + } + + void PrettyPrinter::PrintGherkinLine(std::string_view title, std::function formatTitle, std::optional uri, std::optional line, std::size_t indent, std::size_t maxContentLength) + { + if (title.length() > maxContentLength) + throw std::logic_error("maxContentLength is smaller than title length"); + + const auto padding = maxContentLength - title.length(); + + if (!formatTitle) + formatTitle = [](std::string_view str) + { + return std::string{ str }; + }; + + if (uri.has_value() && line.has_value()) + support::print(outputStream, "{:{}}{}{:{}} {}\n", "", indent, formatTitle(title), "", padding, helper::ColorFunctions::Location(std::format("# {}:{}", *uri, *line))); + else + support::print(outputStream, "{:{}}{}\n", "", indent, formatTitle(title)); + } +} diff --git a/cucumber_cpp/library/formatter/PrettyPrinter.hpp b/cucumber_cpp/library/formatter/PrettyPrinter.hpp new file mode 100644 index 00000000..bf694e95 --- /dev/null +++ b/cucumber_cpp/library/formatter/PrettyPrinter.hpp @@ -0,0 +1,65 @@ +#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/GherkinDocumentParser.hpp" +#include "cucumber_cpp/library/query/Query.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace cucumber_cpp::library::formatter +{ + struct PrettyPrinter + : Formatter + { + using Formatter::Formatter; + + constexpr static auto name = "pretty"; + + 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); + + void PrintFeatureLine(const cucumber::messages::feature& feature); + void PrintRuleLine(const cucumber::messages::rule& rule); + void PrintTags(const cucumber::messages::pickle& pickle, std::size_t scenarioIndent); + void PrintScenarioLine(const cucumber::messages::pickle& pickle, const cucumber::messages::scenario& scenario, std::size_t scenarioIndent, std::size_t maxContentLength); + void PrintStepLine(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); + + void PrintGherkinLine(std::string_view title, std::function formatTitle, std::optional uri, std::optional line, std::size_t indent, std::size_t maxContentLength); + + std::map testCaseStartedIdToScenarioMap; + + 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..2374c36b --- /dev/null +++ b/cucumber_cpp/library/formatter/SummaryFormatter.cpp @@ -0,0 +1,83 @@ +#include "cucumber_cpp/library/formatter/SummaryFormatter.hpp" +#include "cucumber/messages/duration.hpp" +#include "cucumber/messages/envelope.hpp" +#include "cucumber/messages/test_step_result.hpp" +#include "cucumber/messages/test_step_result_status.hpp" +#include "cucumber_cpp/library/formatter/helper/EventDataCollector.hpp" +#include "cucumber_cpp/library/formatter/helper/IssueHelpers.hpp" +#include "cucumber_cpp/library/formatter/helper/SummaryHelpers.hpp" +#include "cucumber_cpp/library/support/Polyfill.hpp" +#include "cucumber_cpp/library/support/Timestamp.hpp" +#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); + } + } + + void SummaryFormatter::OnEnvelope(const cucumber::messages::envelope& envelope) + { + if (envelope.test_run_started) + { + testRunStartedAt = envelope.test_run_started->timestamp; + } + + if (envelope.test_run_finished) + { + const auto testRunFinishedAt = envelope.test_run_finished->timestamp; + const auto duration = testRunFinishedAt - testRunStartedAt; + + LogSummary(duration); + } + } + + void SummaryFormatter::LogSummary(const cucumber::messages::duration& testRunDuration) + { + std::list failures{}; + std::list warnings{}; + + const auto attempts = eventDataCollector.GetTestCaseAttempts(); + for (const auto& attempt : attempts) + { + if (IsFailure(attempt.worstTestStepResult.status, attempt.willBeRetried)) + failures.emplace_back(attempt); + + if (IsWarning(attempt.worstTestStepResult.status, attempt.willBeRetried)) + warnings.emplace_back(attempt); + } + + LogIssues(failures, "Failures"); + LogIssues(warnings, "Warnings"); + + outputStream << helper::FormatSummary(attempts, testRunDuration); + } + + void SummaryFormatter::LogIssues(const std::list& attempts, std::string_view title) + { + if (!attempts.empty()) + { + support::print(outputStream, "{}:\n\n", title); + + auto nr = 1; + for (const auto& issue : attempts) + helper::FormatIssue(outputStream, nr++, issue, supportCodeLibrary) << "\n"; + } + } +} diff --git a/cucumber_cpp/library/formatter/SummaryFormatter.hpp b/cucumber_cpp/library/formatter/SummaryFormatter.hpp new file mode 100644 index 00000000..795f8a74 --- /dev/null +++ b/cucumber_cpp/library/formatter/SummaryFormatter.hpp @@ -0,0 +1,30 @@ +#ifndef FORMATTER_SUMMARY_FORMATTER_HPP +#define FORMATTER_SUMMARY_FORMATTER_HPP + +#include "cucumber/messages/duration.hpp" +#include "cucumber/messages/envelope.hpp" +#include "cucumber/messages/timestamp.hpp" +#include "cucumber_cpp/library/formatter/Formatter.hpp" +#include "cucumber_cpp/library/formatter/helper/EventDataCollector.hpp" +#include +#include + +namespace cucumber_cpp::library::formatter +{ + struct SummaryFormatter + : Formatter + { + using Formatter::Formatter; + + constexpr static auto name = "summary"; + + private: + void OnEnvelope(const cucumber::messages::envelope& envelope) override; + void LogSummary(const cucumber::messages::duration& testRunDuration); + void LogIssues(const std::list& attempts, std::string_view title); + + cucumber::messages::timestamp testRunStartedAt{}; + }; +} + +#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..1f89b48d --- /dev/null +++ b/cucumber_cpp/library/formatter/helper/CMakeLists.txt @@ -0,0 +1,41 @@ +add_library(cucumber_cpp.library.formatter.helper ${CCR_EXCLUDE_FROM_ALL}) + +target_sources(cucumber_cpp.library.formatter.helper PRIVATE + EventDataCollector.cpp + EventDataCollector.hpp + GetColorFunctions.hpp + GetColorFunctions.cpp + GherkinDocumentParser.cpp + GherkinDocumentParser.hpp + IndentString.cpp + IndentString.hpp + IssueHelpers.cpp + IssueHelpers.hpp + KeywordType.cpp + KeywordType.hpp + LocationHelpers.cpp + LocationHelpers.hpp + PickleParser.cpp + PickleParser.hpp + SummaryHelpers.hpp + SummaryHelpers.cpp + TestCaseAttemptFormatter.cpp + TestCaseAttemptFormatter.hpp + TestCaseAttemptParser.cpp + TestCaseAttemptParser.hpp +) + +target_include_directories(cucumber_cpp.library.formatter.helper PUBLIC + ../../../.. +) + +target_link_libraries(cucumber_cpp.library.formatter.helper PUBLIC + cpp-terminal + cucumber_cpp.library.cucumber_expression + cucumber_cpp.library.support +) + +if (CCR_BUILD_TESTS) + add_subdirectory(test) + # add_subdirectory(test_helper) +endif() diff --git a/cucumber_cpp/library/formatter/helper/EventDataCollector.cpp b/cucumber_cpp/library/formatter/helper/EventDataCollector.cpp new file mode 100644 index 00000000..f2459bec --- /dev/null +++ b/cucumber_cpp/library/formatter/helper/EventDataCollector.cpp @@ -0,0 +1,141 @@ +#include "cucumber_cpp/library/formatter/helper/EventDataCollector.hpp" +#include "cucumber/messages/attachment.hpp" +#include "cucumber/messages/envelope.hpp" +#include "cucumber/messages/gherkin_document.hpp" +#include "cucumber/messages/pickle.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_finished.hpp" +#include "cucumber/messages/test_step_result.hpp" +#include "cucumber/messages/test_step_result_status.hpp" +#include "cucumber_cpp/library/util/Broadcaster.hpp" +#include "cucumber_cpp/library/util/GetWorstTestStepResult.hpp" +#include +#include +#include + +namespace cucumber_cpp::library::formatter::helper +{ + EventDataCollector::EventDataCollector(util::Broadcaster& broadcaster) + : util::Listener{ broadcaster, [this](const cucumber::messages::envelope& envelope) + { + OnEnvelope(envelope); + } } + {} + + const cucumber::messages::gherkin_document& EventDataCollector::GetGherkinDocument(std::string uri) const + { + return gherkinDocumentMap.at(uri); + } + + const cucumber::messages::pickle& EventDataCollector::GetPickle(std::string id) const + { + return pickleMap.at(id); + } + + const cucumber::messages::test_case& EventDataCollector::GetTestCase(std::string id) const + { + return testCaseMap.at(id); + } + + std::vector EventDataCollector::GetTestCaseAttempts() const + { + std::vector attempts{}; + for (const auto& key : testCaseAttemptDataMap | std::views::keys) + attempts.emplace_back(GetTestCaseAttempt(key)); + return attempts; + } + + void EventDataCollector::OnEnvelope(const cucumber::messages::envelope& envelope) + { + if (envelope.gherkin_document) + { + gherkinDocumentMap.emplace(envelope.gherkin_document->uri.value(), *envelope.gherkin_document); + } + else if (envelope.pickle) + { + pickleMap.emplace(envelope.pickle->id, *envelope.pickle); + } + else if (envelope.undefined_parameter_type) + { + undefinedParameterTypes.emplace_back(*envelope.undefined_parameter_type); + } + else if (envelope.test_case) + { + testCaseMap.emplace(envelope.test_case->id, *envelope.test_case); + } + else if (envelope.test_case_started) + { + InitTestCaseAttempt(*envelope.test_case_started); + } + else if (envelope.attachment) + { + StoreAttachment(*envelope.attachment); + } + else if (envelope.test_step_finished) + { + StoreTestStepResult(*envelope.test_step_finished); + } + else if (envelope.test_case_finished) + { + StoreTestCaseResult(*envelope.test_case_finished); + } + } + + void EventDataCollector::InitTestCaseAttempt(const cucumber::messages::test_case_started& testCaseStarted) + { + testCaseAttemptDataMap.emplace(testCaseStarted.id, + TestCaseAttemptData{ + .attempt = testCaseStarted.attempt, + .willBeRetried = false, + .testCaseId = testCaseStarted.test_case_id, + .worstTestStepResult = { + .status = cucumber::messages::test_step_result_status::UNKNOWN, + }, + }); + } + + void EventDataCollector::StoreAttachment(const cucumber::messages::attachment& attachment) + { + if (attachment.test_case_started_id && attachment.test_step_id) + { + auto& testCaseAttemptData = testCaseAttemptDataMap.at(*attachment.test_case_started_id); + testCaseAttemptData.stepAttachments[*attachment.test_step_id].emplace_back(attachment); + } + } + + void EventDataCollector::StoreTestStepResult(const cucumber::messages::test_step_finished& testStepFinished) + { + auto& testCaseAttemptData = testCaseAttemptDataMap.at(testStepFinished.test_case_started_id); + testCaseAttemptData.stepResults[testStepFinished.test_step_id] = testStepFinished.test_step_result; + } + + void EventDataCollector::StoreTestCaseResult(const cucumber::messages::test_case_finished& testCaseFinished) + { + auto& testCaseAttemptData = testCaseAttemptDataMap.at(testCaseFinished.test_case_started_id); + + const auto allStepResults = testCaseAttemptData.stepResults | std::views::values; + std::vector stepResults{ allStepResults.begin(), allStepResults.end() }; + + testCaseAttemptData.worstTestStepResult = util::GetWorstTestStepResult(stepResults); + testCaseAttemptData.willBeRetried = testCaseFinished.will_be_retried; + } + + TestCaseAttempt EventDataCollector::GetTestCaseAttempt(const std::string& testCaseStartedId) const + { + const auto& testCaseAttemptData = testCaseAttemptDataMap.at(testCaseStartedId); + const auto& testCase = testCaseMap.at(testCaseAttemptData.testCaseId); + const auto& pickle = pickleMap.at(testCase.pickle_id); + return { + .attempt = testCaseAttemptData.attempt, + .willBeRetried = testCaseAttemptData.willBeRetried, + .gherkinDocument = gherkinDocumentMap.at(pickle.uri), + .pickle = pickle, + .stepAttachments = testCaseAttemptData.stepAttachments, + .stepResults = testCaseAttemptData.stepResults, + .testCase = testCase, + .worstTestStepResult = testCaseAttemptData.worstTestStepResult, + }; + } +} diff --git a/cucumber_cpp/library/formatter/helper/EventDataCollector.hpp b/cucumber_cpp/library/formatter/helper/EventDataCollector.hpp new file mode 100644 index 00000000..a25ce4de --- /dev/null +++ b/cucumber_cpp/library/formatter/helper/EventDataCollector.hpp @@ -0,0 +1,72 @@ +#ifndef HELPER_EVENT_DATA_COLLECTOR_HPP +#define HELPER_EVENT_DATA_COLLECTOR_HPP + +#include "cucumber/messages/attachment.hpp" +#include "cucumber/messages/envelope.hpp" +#include "cucumber/messages/gherkin_document.hpp" +#include "cucumber/messages/pickle.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_finished.hpp" +#include "cucumber/messages/test_step_result.hpp" +#include "cucumber/messages/undefined_parameter_type.hpp" +#include "cucumber_cpp/library/util/Broadcaster.hpp" +#include +#include +#include +#include + +namespace cucumber_cpp::library::formatter::helper +{ + struct TestCaseAttemptData + { + std::size_t attempt; + bool willBeRetried; + std::string testCaseId; + std::map> stepAttachments; + std::map stepResults; + cucumber::messages::test_step_result worstTestStepResult; + }; + + struct TestCaseAttempt + { + std::size_t attempt; + bool willBeRetried; + const cucumber::messages::gherkin_document& gherkinDocument; + const cucumber::messages::pickle& pickle; + const std::map>& stepAttachments; + const std::map& stepResults; + const cucumber::messages::test_case& testCase; + cucumber::messages::test_step_result worstTestStepResult; + }; + + struct EventDataCollector : util::Listener + { + explicit EventDataCollector(util::Broadcaster& broadcaster); + + const cucumber::messages::gherkin_document& GetGherkinDocument(std::string uri) const; + const cucumber::messages::pickle& GetPickle(std::string id) const; + const cucumber::messages::test_case& GetTestCase(std::string id) const; + + std::vector GetTestCaseAttempts() const; + + private: + void OnEnvelope(const cucumber::messages::envelope& envelope); + + void InitTestCaseAttempt(const cucumber::messages::test_case_started& testCaseStarted); + void StoreAttachment(const cucumber::messages::attachment& attachment); + void StoreTestStepResult(const cucumber::messages::test_step_finished& testStepFinished); + void StoreTestCaseResult(const cucumber::messages::test_case_finished& testCaseFinished); + + TestCaseAttempt GetTestCaseAttempt(const std::string& testCaseStartedId) const; + + std::map gherkinDocumentMap; + std::map pickleMap; + std::map testCaseMap; + std::map testCaseAttemptDataMap; + std::vector undefinedParameterTypes; + }; +} + +#endif diff --git a/cucumber_cpp/library/formatter/helper/GetColorFunctions.cpp b/cucumber_cpp/library/formatter/helper/GetColorFunctions.cpp new file mode 100644 index 00000000..bdaec299 --- /dev/null +++ b/cucumber_cpp/library/formatter/helper/GetColorFunctions.cpp @@ -0,0 +1,72 @@ + +#include "cucumber_cpp/library/formatter/helper/GetColorFunctions.hpp" +#include "cpp-terminal/color.hpp" +#include "cucumber/messages/test_step_result_status.hpp" +#include +#include +#include +#include + +namespace cucumber_cpp::library::formatter::helper +{ + namespace + { + template + std::string ColorString(std::string_view sv) + { + return std::format("{}{}{}", Term::color_fg(colour), sv, Term::color_fg(Term::Color::Name::Default)); + } + } + + std::function + ColorFunctions::ForStatus(cucumber::messages::test_step_result_status status) + { + using enum cucumber::messages::test_step_result_status; + + switch (status) + { + case PASSED: + return ColorString; + case SKIPPED: + return ColorString; + case UNKNOWN: + case PENDING: + case UNDEFINED: + return ColorString; + case AMBIGUOUS: + case FAILED: + default: + return ColorString; + } + } + + std::string ColorFunctions::Location(std::string_view sv) + { + return ColorString(sv); + } + + std::string ColorFunctions::Tag(std::string_view sv) + { + return ColorString(sv); + } + + std::string ColorFunctions::DiffAdded(std::string_view sv) + { + return ColorString(sv); + } + + std::string ColorFunctions::DiffRemoved(std::string_view sv) + { + return ColorString(sv); + } + + std::string ColorFunctions::ErrorMessage(std::string_view sv) + { + return ColorString(sv); + } + + std::string ColorFunctions::ErrorStack(std::string_view sv) + { + return ColorString(sv); + } +} diff --git a/cucumber_cpp/library/formatter/helper/GetColorFunctions.hpp b/cucumber_cpp/library/formatter/helper/GetColorFunctions.hpp new file mode 100644 index 00000000..ce8cd4df --- /dev/null +++ b/cucumber_cpp/library/formatter/helper/GetColorFunctions.hpp @@ -0,0 +1,23 @@ +#ifndef FORMATTER_GET_COLOR_FUNCTIONS_HPP +#define FORMATTER_GET_COLOR_FUNCTIONS_HPP + +#include "cucumber/messages/test_step_result_status.hpp" +#include +#include +#include + +namespace cucumber_cpp::library::formatter::helper +{ + struct ColorFunctions + { + static std::function ForStatus(cucumber::messages::test_step_result_status status); + static std::string Location(std::string_view); + static std::string Tag(std::string_view); + static std::string DiffAdded(std::string_view); + static std::string DiffRemoved(std::string_view); + static std::string ErrorMessage(std::string_view); + static std::string ErrorStack(std::string_view); + }; +} + +#endif diff --git a/cucumber_cpp/library/formatter/helper/GherkinDocumentParser.cpp b/cucumber_cpp/library/formatter/helper/GherkinDocumentParser.cpp new file mode 100644 index 00000000..7572e805 --- /dev/null +++ b/cucumber_cpp/library/formatter/helper/GherkinDocumentParser.cpp @@ -0,0 +1,117 @@ +#include "cucumber_cpp/library/formatter/helper/GherkinDocumentParser.hpp" +#include "cucumber/messages/background.hpp" +#include "cucumber/messages/feature_child.hpp" +#include "cucumber/messages/gherkin_document.hpp" +#include "cucumber/messages/rule.hpp" +#include "cucumber/messages/rule_child.hpp" +#include "cucumber/messages/scenario.hpp" +#include "cucumber/messages/step.hpp" +#include +#include +#include + +namespace cucumber_cpp::library::formatter::helper +{ + namespace + { + const std::vector& ExtractSteps(const std::variant& scenarioOrBackground) + { + return std::visit([](const auto& item) -> const std::vector& + { + return item.steps; + }, + scenarioOrBackground); + } + + std::vector ExtractScenarioFromRuleChild(const cucumber::messages::rule_child& child) + { + if (child.scenario) + return { *child.scenario }; + return {}; + } + + std::vector ExtractRulesFeatureChild(const cucumber::messages::feature_child& child) + { + if (child.rule) + return { *child.rule }; + return {}; + } + + std::vector ExtractScenarioContainers(const cucumber::messages::feature_child& child) + { + if (child.rule) + { + std::vector result; + for (const auto& scenario : child.rule->children | std::views::transform(ExtractScenarioFromRuleChild) | std::views::join) + result.push_back(scenario); + return result; + } + + if (child.scenario) + return { *child.scenario }; + + return {}; + } + + std::variant ExtractRuleContainers(const cucumber::messages::rule_child& child) + { + if (child.background) + return *child.background; + else + return *child.scenario; + } + + std::vector> ExtractStepContainers(const cucumber::messages::feature_child& child) + { + if (child.background) + return { *child.background }; + if (child.rule) + { + auto iter = child.rule->children | std::views::transform(ExtractRuleContainers); + return { iter.begin(), iter.end() }; + } + + return { *child.scenario }; + } + } + + GherkinStepMap GetGherkinStepMap(const cucumber::messages::gherkin_document& gherkinDocument) + { + auto steps = gherkinDocument.feature->children | + std::views::transform(ExtractStepContainers) | std::views::join | + std::views::transform(ExtractSteps) | std::views::join; + + GherkinStepMap map; + + for (const auto& step : steps) + map.emplace(step.id, step); + + return map; + } + + GherkinScenarioMap GetGherkinScenarioMap(const cucumber::messages::gherkin_document& gherkinDocument) + { + GherkinScenarioMap map; + + for (const auto& scenario : gherkinDocument.feature->children | std::views::transform(ExtractScenarioContainers) | std::views::join) + map.emplace(scenario.id, scenario); + + return map; + } + + GherkinScenarioLocationMap GetGherkinScenarioLocationMap(const cucumber::messages::gherkin_document& gherkinDocument) + { + GherkinScenarioLocationMap locationMap; + GherkinScenarioMap scenarioMap = GetGherkinScenarioMap(gherkinDocument); + + for (const auto& [id, scenario] : scenarioMap) + { + locationMap.emplace(id, scenario.location); + for (const auto& example : scenario.examples) + for (const auto& tableRow : example.table_body) + locationMap.emplace(tableRow.id, tableRow.location); + } + + return locationMap; + } +} diff --git a/cucumber_cpp/library/formatter/helper/GherkinDocumentParser.hpp b/cucumber_cpp/library/formatter/helper/GherkinDocumentParser.hpp new file mode 100644 index 00000000..49106a7f --- /dev/null +++ b/cucumber_cpp/library/formatter/helper/GherkinDocumentParser.hpp @@ -0,0 +1,22 @@ +#ifndef HELPER_GHERKIN_DOCUMENT_PARSER_HPP +#define HELPER_GHERKIN_DOCUMENT_PARSER_HPP + +#include "cucumber/messages/gherkin_document.hpp" +#include "cucumber/messages/location.hpp" +#include "cucumber/messages/scenario.hpp" +#include "cucumber/messages/step.hpp" +#include +#include + +namespace cucumber_cpp::library::formatter::helper +{ + using GherkinStepMap = std::map; + using GherkinScenarioMap = std::map; + using GherkinScenarioLocationMap = std::map; + + GherkinStepMap GetGherkinStepMap(const cucumber::messages::gherkin_document& gherkinDocument); + GherkinScenarioMap GetGherkinScenarioMap(const cucumber::messages::gherkin_document& gherkinDocument); + GherkinScenarioLocationMap GetGherkinScenarioLocationMap(const cucumber::messages::gherkin_document& gherkinDocument); +} + +#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..5ede2363 --- /dev/null +++ b/cucumber_cpp/library/formatter/helper/IndentString.cpp @@ -0,0 +1,30 @@ +#include "cucumber_cpp/library/formatter/helper/IndentString.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace cucumber_cpp::library::formatter::helper +{ + std::string IndentString(const std::string& str, std::size_t indentSize) + { + using std::operator""sv; + + auto lines = str | std::views::split("\n"sv); + auto lineCount = std::distance(lines.begin(), lines.end()); + + if (lineCount == 0) + return ""; + + const auto indent = std::string(indentSize, ' '); + std::string indented = std::format("{}{}", indent, std::string_view{ lines.front().begin(), lines.front().end() }); + + for (const auto line : lines | std::views::drop(1)) + indented += std::format("\n{}{}", indent, std::string_view{ line.begin(), line.end() }); + + return indented; + } +} 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/IssueHelpers.cpp b/cucumber_cpp/library/formatter/helper/IssueHelpers.cpp new file mode 100644 index 00000000..64359580 --- /dev/null +++ b/cucumber_cpp/library/formatter/helper/IssueHelpers.cpp @@ -0,0 +1,38 @@ + +#include "cucumber_cpp/library/formatter/helper/IssueHelpers.hpp" +#include "cucumber_cpp/library/formatter/helper/EventDataCollector.hpp" +#include "cucumber_cpp/library/formatter/helper/TestCaseAttemptFormatter.hpp" +#include "cucumber_cpp/library/support/Polyfill.hpp" +#include "cucumber_cpp/library/support/SupportCodeLibrary.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace cucumber_cpp::library::formatter::helper +{ + std::ostream& FormatIssue(std::ostream& outputStream, std::size_t number, const TestCaseAttempt& testCaseAttempt, support::SupportCodeLibrary& supportCodeLibrary, bool printAttachments) + { + using std::operator""sv; + + const auto prefix = std::format("{}) ", number); + const auto formattedTestCaseAttempt = FormatTestCaseAttempt(supportCodeLibrary, testCaseAttempt, printAttachments); + + auto lines = formattedTestCaseAttempt | std::views::split("\n"sv); + + if (std::ranges::distance(lines) == 0) + return outputStream; + + support::print(outputStream, "{}{}\n", prefix, std::string_view{ lines.front().begin(), lines.front().end() }); + + for (const auto line : lines | std::views::drop(1)) + support::print(outputStream, "{:{}}{}\n", "", prefix.size(), std::string_view{ line.begin(), line.end() }); + + return outputStream; + } +} diff --git a/cucumber_cpp/library/formatter/helper/IssueHelpers.hpp b/cucumber_cpp/library/formatter/helper/IssueHelpers.hpp new file mode 100644 index 00000000..6ff37037 --- /dev/null +++ b/cucumber_cpp/library/formatter/helper/IssueHelpers.hpp @@ -0,0 +1,15 @@ +#ifndef HELPER_ISSUE_HELPERS_HPP +#define HELPER_ISSUE_HELPERS_HPP + +#include "cucumber_cpp/library/formatter/helper/EventDataCollector.hpp" +#include "cucumber_cpp/library/support/SupportCodeLibrary.hpp" +#include +#include +#include + +namespace cucumber_cpp::library::formatter::helper +{ + std::ostream& FormatIssue(std::ostream& outputStream, std::size_t number, const TestCaseAttempt& testCaseAttempt, support::SupportCodeLibrary& supportCodeLibrary, bool printAttachments = true); +} + +#endif diff --git a/cucumber_cpp/library/formatter/helper/KeywordType.cpp b/cucumber_cpp/library/formatter/helper/KeywordType.cpp new file mode 100644 index 00000000..44ba340d --- /dev/null +++ b/cucumber_cpp/library/formatter/helper/KeywordType.cpp @@ -0,0 +1,47 @@ +#include "cucumber_cpp/library/formatter/helper/KeywordType.hpp" +#include "cucumber/gherkin/dialect.hpp" +#include +#include +#include +#include +#include + +namespace cucumber_cpp::library::formatter::helper +{ + namespace + { + constexpr std::array stepKeywords{ + "given", + "when", + "then", + "and", + "but", + }; + } + + KeywordType GetStepKeywordType(std::string_view keyword, std::string_view language, std::optional previousKeywordType) + { + const auto& dialect = cucumber::gherkin::keywords(language); + const auto typeIter = std::ranges::find_if(stepKeywords, [&dialect, keyword](std::string_view stepKeyword) + { + return std::ranges::find(dialect.at(stepKeyword), keyword) != dialect.at(stepKeyword).end(); + }); + + switch (std::distance(stepKeywords.begin(), typeIter)) + { + case 0: // given + return KeywordType::precondition; + case 1: // when + return KeywordType::event; + case 2: // then + return KeywordType::outcome; + case 3: // and + case 4: // but + if (previousKeywordType.has_value()) + return *previousKeywordType; + [[fallthrough]]; + default: + return KeywordType::precondition; + } + } +} diff --git a/cucumber_cpp/library/formatter/helper/KeywordType.hpp b/cucumber_cpp/library/formatter/helper/KeywordType.hpp new file mode 100644 index 00000000..5fa9328f --- /dev/null +++ b/cucumber_cpp/library/formatter/helper/KeywordType.hpp @@ -0,0 +1,19 @@ +#ifndef HELPER_KEYWORD_TYPE_HPP +#define HELPER_KEYWORD_TYPE_HPP + +#include +#include + +namespace cucumber_cpp::library::formatter::helper +{ + enum class KeywordType + { + precondition, + event, + outcome + }; + + KeywordType GetStepKeywordType(std::string_view keyword, std::string_view language, std::optional previousKeywordType); +} + +#endif diff --git a/cucumber_cpp/library/formatter/helper/LocationHelpers.cpp b/cucumber_cpp/library/formatter/helper/LocationHelpers.cpp new file mode 100644 index 00000000..112f8f08 --- /dev/null +++ b/cucumber_cpp/library/formatter/helper/LocationHelpers.cpp @@ -0,0 +1,17 @@ +#include "cucumber_cpp/library/formatter/helper/LocationHelpers.hpp" +#include "cucumber_cpp/library/formatter/helper/TestCaseAttemptParser.hpp" +#include +#include +#include +#include + +namespace cucumber_cpp::library::formatter::helper +{ + std::string FormatLocation(LineAndUri obj, std::optional cwd) + { + std::string uri = obj.uri; + if (cwd) + uri = std::filesystem::relative(obj.uri, *cwd).string(); + return std::format("{}:{}", uri, obj.line); + } +} diff --git a/cucumber_cpp/library/formatter/helper/LocationHelpers.hpp b/cucumber_cpp/library/formatter/helper/LocationHelpers.hpp new file mode 100644 index 00000000..37453654 --- /dev/null +++ b/cucumber_cpp/library/formatter/helper/LocationHelpers.hpp @@ -0,0 +1,15 @@ +#ifndef HELPER_FORMAT_LOCATION_HPP +#define HELPER_FORMAT_LOCATION_HPP + +#include "cucumber_cpp/library/formatter/helper/TestCaseAttemptParser.hpp" +#include +#include +#include + +namespace cucumber_cpp::library::formatter::helper +{ + + std::string FormatLocation(LineAndUri obj, std::optional cwd = std::nullopt); +} + +#endif diff --git a/cucumber_cpp/library/formatter/helper/PickleParser.cpp b/cucumber_cpp/library/formatter/helper/PickleParser.cpp new file mode 100644 index 00000000..c81093f3 --- /dev/null +++ b/cucumber_cpp/library/formatter/helper/PickleParser.cpp @@ -0,0 +1,37 @@ +#include "cucumber_cpp/library/formatter/helper/PickleParser.hpp" +#include "cucumber/messages/pickle.hpp" +#include "cucumber/messages/pickle_step.hpp" +#include "cucumber_cpp/library/formatter/helper/GherkinDocumentParser.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace cucumber_cpp::library::formatter::helper +{ + namespace + { + constexpr auto toPair = [](const cucumber::messages::pickle_step& step) -> std::pair + { + return { step.id, step }; + }; + } + + std::map GetPickleStepMap(const cucumber::messages::pickle& pickle) + { + auto range = pickle.steps | std::views::transform(toPair); + return { range.begin(), range.end() }; + } + + std::string_view GetStepKeyword(const cucumber::messages::pickle_step& pickleStep, const GherkinStepMap& gherkinStepMap) + { + auto first = std::ranges::find_if(pickleStep.ast_node_ids, [&gherkinStepMap](const std::string& id) + { + return gherkinStepMap.contains(id); + }); + return gherkinStepMap.at(*first).keyword; + } +} diff --git a/cucumber_cpp/library/formatter/helper/PickleParser.hpp b/cucumber_cpp/library/formatter/helper/PickleParser.hpp new file mode 100644 index 00000000..f7cdab76 --- /dev/null +++ b/cucumber_cpp/library/formatter/helper/PickleParser.hpp @@ -0,0 +1,18 @@ +#ifndef HELPER_PICKLE_PARSER_HPP +#define HELPER_PICKLE_PARSER_HPP + +#include "cucumber/messages/pickle.hpp" +#include "cucumber/messages/pickle_step.hpp" +#include "cucumber_cpp/library/formatter/helper/GherkinDocumentParser.hpp" +#include +#include +#include + +namespace cucumber_cpp::library::formatter::helper +{ + std::map GetPickleStepMap(const cucumber::messages::pickle& pickle); + + std::string_view GetStepKeyword(const cucumber::messages::pickle_step& pickleStep, const GherkinStepMap& gherkinStepMap); +} + +#endif diff --git a/cucumber_cpp/library/formatter/helper/SummaryHelpers.cpp b/cucumber_cpp/library/formatter/helper/SummaryHelpers.cpp new file mode 100644 index 00000000..111d5597 --- /dev/null +++ b/cucumber_cpp/library/formatter/helper/SummaryHelpers.cpp @@ -0,0 +1,97 @@ +#include "cucumber_cpp/library/formatter/helper/SummaryHelpers.hpp" +#include "cucumber/messages/duration.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/EventDataCollector.hpp" +#include "cucumber_cpp/library/formatter/helper/GetColorFunctions.hpp" +#include "cucumber_cpp/library/support/Duration.hpp" +#include "cucumber_cpp/library/support/Join.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace cucumber_cpp::library::formatter::helper +{ + namespace + { + std::string GetCountSummary(std::span results, std::string_view type) + { + ColorFunctions colorFunctions{}; + std::map counts; + + for (const auto& result : results) + ++counts[result.status]; + + auto values = counts | std::views::values; + const auto total = std::accumulate(values.begin(), values.end(), std::size_t{ 0u }); + + std::string text = std::format("{} {}{}", total, type, (total == 1 ? "" : "s")); + + if (total > 0) + { + std::vector details; + for (const auto& [status, count] : counts) + { + auto statusStr = std::string{ cucumber::messages::to_string(status) }; + std::transform(statusStr.begin(), statusStr.end(), statusStr.begin(), [](unsigned char c) + { + return std::tolower(c); + }); + details.emplace_back(colorFunctions.ForStatus(status)(std::format("{} {}", count, statusStr))); + } + + text += " " + support::Join(details, ", "); + } + + return text; + } + + std::string GetDurationSummary(const cucumber::messages::duration& duration) + { + const auto total = support::DurationToMilliseconds(duration); + return std::format("{:%Mm %S}s", total); + } + + bool HasPickleStepId(const cucumber::messages::test_step& testStep) + { + return testStep.pickle_step_id.has_value(); + } + } + + std::string FormatSummary(std::span testCaseAttempts, cucumber::messages::duration testRunDuration) + { + std::vector testCaseResults; + std::vector testStepResults; + cucumber::messages::duration totalStepDuration{}; + + for (const auto& testCaseAttempt : testCaseAttempts) + { + for (const auto& [_, stepResult] : testCaseAttempt.stepResults) + totalStepDuration += stepResult.duration; + + if (!testCaseAttempt.willBeRetried) + { + testCaseResults.emplace_back(testCaseAttempt.worstTestStepResult); + + for (const auto& testStep : testCaseAttempt.testCase.test_steps | + std::views::filter(HasPickleStepId)) + testStepResults.emplace_back(testCaseAttempt.stepResults.at(testStep.id)); + } + } + + const auto scenarioSummary = GetCountSummary(testCaseResults, "scenario"); + const auto stepSummary = GetCountSummary(testStepResults, "step"); + const auto durationSummary = std::format("{} (executing steps: {})\n", GetDurationSummary(testRunDuration), GetDurationSummary(totalStepDuration)); + + return support::Join({ scenarioSummary, stepSummary, durationSummary }, "\n"); + } +} diff --git a/cucumber_cpp/library/formatter/helper/SummaryHelpers.hpp b/cucumber_cpp/library/formatter/helper/SummaryHelpers.hpp new file mode 100644 index 00000000..fea98149 --- /dev/null +++ b/cucumber_cpp/library/formatter/helper/SummaryHelpers.hpp @@ -0,0 +1,14 @@ +#ifndef HELPER_SUMMARY_HELPERS_HPP +#define HELPER_SUMMARY_HELPERS_HPP + +#include "cucumber/messages/duration.hpp" +#include "cucumber_cpp/library/formatter/helper/EventDataCollector.hpp" +#include +#include + +namespace cucumber_cpp::library::formatter::helper +{ + std::string FormatSummary(std::span testCaseAttempts, cucumber::messages::duration testRunDuration); +} + +#endif diff --git a/cucumber_cpp/library/formatter/helper/TestCaseAttemptFormatter.cpp b/cucumber_cpp/library/formatter/helper/TestCaseAttemptFormatter.cpp new file mode 100644 index 00000000..7007d3e0 --- /dev/null +++ b/cucumber_cpp/library/formatter/helper/TestCaseAttemptFormatter.cpp @@ -0,0 +1,86 @@ +#include "cucumber_cpp/library/formatter/helper/TestCaseAttemptFormatter.hpp" +#include "cucumber/messages/test_step_result_status.hpp" +#include "cucumber_cpp/library/formatter/helper/EventDataCollector.hpp" +#include "cucumber_cpp/library/formatter/helper/GetColorFunctions.hpp" +#include "cucumber_cpp/library/formatter/helper/IndentString.hpp" +#include "cucumber_cpp/library/formatter/helper/LocationHelpers.hpp" +#include "cucumber_cpp/library/formatter/helper/TestCaseAttemptParser.hpp" +#include "cucumber_cpp/library/support/SupportCodeLibrary.hpp" +#include +#include +#include +#include +#include + +namespace cucumber_cpp::library::formatter::helper +{ + namespace + { + std::string GetAttemptText(std::size_t attempt, bool willBeRetried) + { + if (attempt > 0 || willBeRetried) + return std::format(" (attempt {}{})", attempt + 1, willBeRetried ? ", retried" : ""); + return ""; + } + + std::string GetStepMessage(const ParsedTestStep& testStep) + { + using enum cucumber::messages::test_step_result_status; + + switch (testStep.result.status) + { + case AMBIGUOUS: + case FAILED: + return testStep.result.message.value_or(""); + + case UNDEFINED: + return "Undefined. Implement with the following snippet: ''"; + + case PENDING: + return "Pending"; + + default: + return {}; + } + } + + std::string FormatStep(const ParsedTestStep& testStep, bool printAttachments) + { + ColorFunctions colorFunctions; + const auto colorStatus = colorFunctions.ForStatus(testStep.result.status); + + auto identifier = std::format("{}{}", testStep.keyword, testStep.text.value_or("")); + auto text = colorStatus(std::format("{} {}", cucumber::messages::to_string(testStep.result.status), identifier)); + if (testStep.name) + text += colorStatus(*testStep.name); + + if (testStep.actionLocation) + text += std::format(" # {}", colorFunctions.Location(FormatLocation(*testStep.actionLocation, std::filesystem::current_path()))); + + text += '\n'; + + if (testStep.argument) + ; // indent 4 FormatStepArgument append \n + + // indent 4 print attachments append \n + + auto message = GetStepMessage(testStep); + if (!message.empty()) + text += IndentString(colorStatus(message), 4) + "\n"; + + return text; + } + } + + std::string FormatTestCaseAttempt(support::SupportCodeLibrary& supportCodeLibrary, const TestCaseAttempt& testCaseAttempt, bool printAttachments) + { + const auto parsed = ParseTestCaseAttempt(supportCodeLibrary, testCaseAttempt); + + auto text = std::format("Scenario: {}", parsed.parsedTestCase.name); + text += GetAttemptText(parsed.parsedTestCase.attempt, testCaseAttempt.willBeRetried); + text += std::format(" {}\n", FormatLocation(parsed.parsedTestCase.sourceLocation.value())); + for (auto const& testStep : parsed.parsedTestSteps) + text += FormatStep(testStep, printAttachments); + return text + '\n'; + } +} diff --git a/cucumber_cpp/library/formatter/helper/TestCaseAttemptFormatter.hpp b/cucumber_cpp/library/formatter/helper/TestCaseAttemptFormatter.hpp new file mode 100644 index 00000000..95fa7e1e --- /dev/null +++ b/cucumber_cpp/library/formatter/helper/TestCaseAttemptFormatter.hpp @@ -0,0 +1,13 @@ +#ifndef FORMATTER_TEST_CASE_ATTEMPT_FORMATTER_HPP +#define FORMATTER_TEST_CASE_ATTEMPT_FORMATTER_HPP + +#include "cucumber_cpp/library/formatter/helper/EventDataCollector.hpp" +#include "cucumber_cpp/library/support/SupportCodeLibrary.hpp" +#include + +namespace cucumber_cpp::library::formatter::helper +{ + std::string FormatTestCaseAttempt(support::SupportCodeLibrary& supportCodeLibrary, const TestCaseAttempt& testCaseAttempt, bool printAttachments); +} + +#endif diff --git a/cucumber_cpp/library/formatter/helper/TestCaseAttemptParser.cpp b/cucumber_cpp/library/formatter/helper/TestCaseAttemptParser.cpp new file mode 100644 index 00000000..69e197e7 --- /dev/null +++ b/cucumber_cpp/library/formatter/helper/TestCaseAttemptParser.cpp @@ -0,0 +1,139 @@ +#include "cucumber_cpp/library/formatter/helper/TestCaseAttemptParser.hpp" +#include "cucumber/messages/attachment.hpp" +#include "cucumber/messages/pickle_step.hpp" +#include "cucumber/messages/test_step.hpp" +#include "cucumber/messages/test_step_result.hpp" +#include "cucumber_cpp/library/formatter/helper/EventDataCollector.hpp" +#include "cucumber_cpp/library/formatter/helper/GherkinDocumentParser.hpp" +#include "cucumber_cpp/library/formatter/helper/KeywordType.hpp" +#include "cucumber_cpp/library/formatter/helper/PickleParser.hpp" +#include "cucumber_cpp/library/support/SupportCodeLibrary.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace cucumber_cpp::library::formatter::helper +{ + namespace + { + ParsedTestStep ParseStep(bool isBeforeHook, + const GherkinStepMap& gherkinStepMap, + std::string_view keyword, + KeywordType keywordType, + const std::map& pickleStepMap, + std::filesystem::path pickleUri, + support::SupportCodeLibrary supportCode, + const cucumber::messages::test_step& testStep, + const cucumber::messages::test_step_result& testStepResult, + std::span attachments) + { + ParsedTestStep parsedTestStep{ + .attachments = attachments, + .keyword = std::string{ testStep.pickle_step_id ? keyword : (isBeforeHook ? "Before" : "After") }, + .result = testStepResult, + }; + + if (testStep.hook_id) + { + const auto& definition = supportCode.hookRegistry.GetDefinitionById(testStep.hook_id.value()); + parsedTestStep.actionLocation = { + .uri = definition.hook.source_reference.uri.value(), + .line = definition.hook.source_reference.location->line, + }; + parsedTestStep.name = std::format(" [Hook]"); + } + + if (testStep.step_definition_ids && testStep.step_definition_ids->size() == 1) + { + const auto& definition = supportCode.stepRegistry.GetDefinitionById(testStep.step_definition_ids->front()); + parsedTestStep.actionLocation = { + .uri = definition.uri.string(), + .line = definition.line, + }; + } + + if (testStep.pickle_step_id) + { + const auto& pickleStep = pickleStepMap.at(*testStep.pickle_step_id); + + parsedTestStep.location = { + .uri = pickleUri.string(), + .line = gherkinStepMap.at(pickleStep.ast_node_ids.front()).location.line + }; + parsedTestStep.text = pickleStep.text; + if (pickleStep.argument) + parsedTestStep.argument = *pickleStep.argument; + } + + // if (testStepResult.status == cucumber::messages::test_step_result_status::UNDEFINED) + // parsedTestStep.snippet = "not supporting snippet generation yet"; + + return parsedTestStep; + } + } + + ParsedTestCaseAttempt ParseTestCaseAttempt(support::SupportCodeLibrary& supportCodeLibrary, const TestCaseAttempt& testCaseAttempt) + { + const auto& testCase = testCaseAttempt.testCase; + const auto& pickle = testCaseAttempt.pickle; + const auto& gherkinDocument = testCaseAttempt.gherkinDocument; + + const auto gherkinStepMap = GetGherkinStepMap(gherkinDocument); + const auto gherkinScenarioLocationMap = GetGherkinScenarioLocationMap(gherkinDocument); + const auto pickleStepMap = GetPickleStepMap(pickle); + const auto relativePickleUri = pickle.uri; + + const ParsedTestCase parsedTestCase{ + .attempt = testCaseAttempt.attempt, + .name = pickle.name, + .sourceLocation = LineAndUri{ + .uri = relativePickleUri, + .line = gherkinScenarioLocationMap.at(pickle.ast_node_ids[0]).line, + }, + .worstStepStepResult = testCaseAttempt.worstTestStepResult + }; + + std::vector parsedTestSteps{}; + parsedTestSteps.reserve(testCase.test_steps.size()); + + bool isBeforeHook = true; + std::optional previousKeyWordType = KeywordType::precondition; + + for (const auto& testStep : testCase.test_steps) + { + const auto& testStepResult = testCaseAttempt.stepResults.at(testStep.id); + isBeforeHook = isBeforeHook && testStep.hook_id.has_value(); + + std::string_view keyword{}; + KeywordType keywordType{}; + if (testStep.pickle_step_id) + { + const auto& pickleStep = pickleStepMap.at(*testStep.pickle_step_id); + keyword = GetStepKeyword(pickleStep, gherkinStepMap); + keywordType = GetStepKeywordType(keyword, gherkinDocument.feature->language, previousKeyWordType); + } + + parsedTestSteps.emplace_back(ParseStep(isBeforeHook, + gherkinStepMap, + keyword, + keywordType, + pickleStepMap, + relativePickleUri, + supportCodeLibrary, + testStep, + testStepResult, + testCaseAttempt.stepAttachments.contains(testStep.id) ? testCaseAttempt.stepAttachments.at(testStep.id) : std::span{})); + previousKeyWordType = keywordType; + } + + return { + .parsedTestCase = parsedTestCase, + .parsedTestSteps = parsedTestSteps, + }; + } +} diff --git a/cucumber_cpp/library/formatter/helper/TestCaseAttemptParser.hpp b/cucumber_cpp/library/formatter/helper/TestCaseAttemptParser.hpp new file mode 100644 index 00000000..8e786056 --- /dev/null +++ b/cucumber_cpp/library/formatter/helper/TestCaseAttemptParser.hpp @@ -0,0 +1,53 @@ +#ifndef FORMATTER_TEST_CASE_ATTEMPT_PARSER_HPP +#define FORMATTER_TEST_CASE_ATTEMPT_PARSER_HPP + +#include "cucumber/messages/attachment.hpp" +#include "cucumber/messages/pickle_step_argument.hpp" +#include "cucumber/messages/test_step_result.hpp" +#include "cucumber_cpp/library/formatter/helper/EventDataCollector.hpp" +#include "cucumber_cpp/library/support/SupportCodeLibrary.hpp" +#include +#include +#include +#include +#include +#include + +namespace cucumber_cpp::library::formatter::helper +{ + struct LineAndUri + { + std::string uri; + std::size_t line; + }; + + struct ParsedTestCase + { + std::size_t attempt; + std::string name; + std::optional sourceLocation; + cucumber::messages::test_step_result worstStepStepResult; + }; + + struct ParsedTestStep + { + std::optional actionLocation; + std::optional argument; + std::span attachments; + std::string keyword; + std::optional name; + const cucumber::messages::test_step_result& result; + std::optional location; + std::optional text; + }; + + struct ParsedTestCaseAttempt + { + ParsedTestCase parsedTestCase; + std::vector parsedTestSteps; + }; + + ParsedTestCaseAttempt ParseTestCaseAttempt(support::SupportCodeLibrary& supportCodeLibrary, const TestCaseAttempt& testCaseAttempt); +} + +#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..ec90d8bb --- /dev/null +++ b/cucumber_cpp/library/query/CMakeLists.txt @@ -0,0 +1,19 @@ +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 +) + +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..cd10dd5d --- /dev/null +++ b/cucumber_cpp/library/query/Query.cpp @@ -0,0 +1,398 @@ +#include "cucumber_cpp/library/query/Query.hpp" +#include "cucumber/messages/attachment.hpp" +#include "cucumber/messages/background.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_started.hpp" +#include "cucumber/messages/undefined_parameter_type.hpp" +#include "cucumber_cpp/library/util/Broadcaster.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace cucumber_cpp::library::query +{ + std::string Lineage::GetUniqueFeatureName() const + { + return std::format("{}/{}", feature->name, featureIndex); + } + + std::string Lineage::GetScenarioAndOrRuleName() const + { + if (rule) + return std::format("{}/{}", rule->name, scenario->name); + return scenario->name; + } + + 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); + } + + const cucumber::messages::location& Query::FindLocationOf(const cucumber::messages::pickle& pickle) const + { + const auto& lineage = FindLineageByUri(pickle.uri); + // if (lineage.examples) + return lineage.scenario->location; + } + + const std::map>& Query::TestCaseStarted() const + { + return testCaseStartedById; + } + + const std::map>& Query::TestCaseFinishedByTestCaseStartedId() const + { + return testCaseFinishedByTestCaseStartedId; + } + + 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_front(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_front(testStepStarted); + } + + 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_front(ptr); + if (attachment.test_case_started_id) + attachmentsByTestCaseStartedId[attachment.test_case_started_id.value()].push_front(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_front(testStepFinished); + + const auto& pickleId = pickleIdByTestStepId.at(testStepFinished.test_step_id); + testStepResultByPickleId[pickleId].push_front(testStepResultPtr); + + const auto& testStep = testStepById.at(testStepFinished.test_step_id); + testStepResultsbyTestStepId[testStep.id].push_front(testStepResultPtr); + if (testStep.pickle_step_id) + testStepResultsByPickleStepId[testStep.pickle_step_id.value()].push_front(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_front(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 linaege = feature.second + featurePtr + featureCountByName[feature.first.name]; + lineageByUri[*linaege.gherkinDocument->uri] = linaege; + + for (const auto& child : feature.first.children) + { + if (child.background) + *this += child.background->steps; + if (child.scenario) + *this += { *child.scenario, linaege }; + if (child.rule) + *this += { *child.rule, linaege }; + } + } + + 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..351fd3aa --- /dev/null +++ b/cucumber_cpp/library/query/Query.hpp @@ -0,0 +1,220 @@ +#ifndef LIBRARY_QUERY_HPP +#define LIBRARY_QUERY_HPP + +#include "cucumber/messages/attachment.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_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 + +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 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; + + const cucumber::messages::location& FindLocationOf(const cucumber::messages::pickle& pickle) const; + + const std::map>& TestCaseStarted() const; + const std::map>& TestCaseFinishedByTestCaseStartedId() 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::forward_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::forward_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, std::less<>> suggestionsByPickleStepId; + std::forward_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..e2a902a5 --- /dev/null +++ b/cucumber_cpp/library/runtime/CMakeLists.txt @@ -0,0 +1,30 @@ +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 + 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 +) + +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..79b91efc --- /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/Timestamp.hpp" +#include "cucumber_cpp/library/support/Types.hpp" +#include "cucumber_cpp/library/util/Broadcaster.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 = support::TimestampNow(), + .id = std::string{ testRunStartedId }, + } }); + + const auto success = runtimeAdapter->Run(); + + broadcaster.BroadcastEvent({ .test_run_finished = cucumber::messages::test_run_finished{ + .success = success, + .timestamp = support::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/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..b1b4a601 --- /dev/null +++ b/cucumber_cpp/library/runtime/TestCaseRunner.cpp @@ -0,0 +1,245 @@ + +#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/support/Body.hpp" +#include "cucumber_cpp/library/support/Duration.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/Timestamp.hpp" +#include "cucumber_cpp/library/util/Broadcaster.hpp" +#include "cucumber_cpp/library/util/GetWorstTestStepResult.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 = support::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 = support::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 = support::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 = support::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(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..a84d0ebf --- /dev/null +++ b/cucumber_cpp/library/runtime/Worker.cpp @@ -0,0 +1,206 @@ +#include "cucumber_cpp/library/runtime/Worker.hpp" +#include "cucumber/gherkin/id_generator.hpp" +#include "cucumber/messages/duration.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/Timestamp.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 +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace cucumber_cpp::library::runtime +{ + namespace + { + cucumber::messages::duration operator+=(cucumber::messages::duration durationA, cucumber::messages::duration durationB) + { + const auto seconds = durationA.seconds + durationB.seconds; + const auto nanos = durationA.nanos + durationB.nanos; + + if (nanos >= support::nanosecondsPerSecond) + return { seconds + 1, nanos - support::nanosecondsPerSecond }; + else + return { seconds, nanos }; + } + + 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); + }; + + 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, 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(std::move(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(std::move(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, + options.retry, + 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(std::move(RunTestHook(id, context))); + + if (util::GetWorstTestStepResult(results).status != cucumber::messages::test_step_result_status::PASSED) + throw std::runtime_error("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(std::move(RunTestHook(id, context))); + + if (util::GetWorstTestStepResult(results).status != cucumber::messages::test_step_result_status::PASSED) + throw std::runtime_error("Failed after feature hook"); + + return results; + } + + cucumber::messages::test_step_result Worker::RunTestHook(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 = support::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 = support::TimestampNow(), + } }); + + return result; + } + + bool Worker::IsStatusFailed(cucumber::messages::test_step_result_status status) + { + 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..5f6c547e --- /dev/null +++ b/cucumber_cpp/library/runtime/Worker.hpp @@ -0,0 +1,53 @@ +#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 + +namespace cucumber_cpp::library::runtime +{ + 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(std::string id, Context& context); + + bool IsStatusFailed(cucumber::messages::test_step_result_status status); + + 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..4b366a24 --- /dev/null +++ b/cucumber_cpp/library/support/Body.cpp @@ -0,0 +1,100 @@ +#include "cucumber_cpp/library/support/Body.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/support/Duration.hpp" +#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 = std::format("{}\n{}:{}: Failure\n{}", testStepResult.message.value(), fileName, testPartResult.line_number(), testPartResult.message()); + else + testStepResult.message = std::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 }; + + try + { + support::Stopwatch::Instance().Start(); + Execute(args); + } + 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 = typeid(e).name(), + .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 = support::Stopwatch::Instance().Duration(); + 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..d65d9877 --- /dev/null +++ b/cucumber_cpp/library/support/CMakeLists.txt @@ -0,0 +1,37 @@ +add_library(cucumber_cpp.library.support ${CCR_EXCLUDE_FROM_ALL}) + +target_sources(cucumber_cpp.library.support PRIVATE + Body.cpp + Body.hpp + Duration.cpp + Duration.hpp + HookRegistry.cpp + HookRegistry.hpp + Join.cpp + Join.hpp + StepRegistry.cpp + StepRegistry.hpp + StepType.hpp + SupportCodeLibrary.cpp + SupportCodeLibrary.hpp + Timestamp.cpp + Timestamp.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 +) + +if (CCR_BUILD_TESTS) + add_subdirectory(test) + # add_subdirectory(test_helper) +endif() diff --git a/cucumber_cpp/library/support/Duration.cpp b/cucumber_cpp/library/support/Duration.cpp new file mode 100644 index 00000000..566ce826 --- /dev/null +++ b/cucumber_cpp/library/support/Duration.cpp @@ -0,0 +1,67 @@ + +#include "cucumber/messages/duration.hpp" +#include "cucumber_cpp/library/support/Duration.hpp" +#include "cucumber_cpp/library/support/Timestamp.hpp" +#include +#include + +namespace cucumber_cpp::library::support +{ + namespace + { + std::chrono::milliseconds ToMillis(std::chrono::seconds seconds, std::chrono::nanoseconds nanos) + { + return std::chrono::duration_cast(seconds) + + std::chrono::duration_cast(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; + } + + void StopWatchHighResolutionClock::Start() + { + timeStart = std::chrono::high_resolution_clock::now(); + } + + std::chrono::nanoseconds StopWatchHighResolutionClock::Duration() + { + const auto timeStop = std::chrono::high_resolution_clock::now(); + return std::chrono::duration_cast(timeStop - timeStart); + } + + 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)); + } + + 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; + } +} diff --git a/cucumber_cpp/library/support/Duration.hpp b/cucumber_cpp/library/support/Duration.hpp new file mode 100644 index 00000000..7e52f33b --- /dev/null +++ b/cucumber_cpp/library/support/Duration.hpp @@ -0,0 +1,46 @@ +#ifndef SUPPORT_DURATION_HPP +#define SUPPORT_DURATION_HPP + +#include "cucumber/messages/duration.hpp" +#include + +namespace cucumber_cpp::library::support +{ + cucumber::messages::duration MillisecondsToDuration(std::chrono::milliseconds millis); + + std::chrono::milliseconds DurationToMilliseconds(const cucumber::messages::duration& duration); + cucumber::messages::duration& operator+=(cucumber::messages::duration& lhs, const cucumber::messages::duration& rhs); + + struct Stopwatch + { + protected: + Stopwatch(); + ~Stopwatch() = default; + + public: + static Stopwatch& Instance(); + + virtual void Start() = 0; + virtual std::chrono::nanoseconds Duration() = 0; + + private: + static inline Stopwatch* instance{ nullptr }; + }; + + struct StopWatchHighResolutionClock : Stopwatch + { + virtual ~StopWatchHighResolutionClock() = default; + void Start() override; + std::chrono::nanoseconds Duration() override; + + private: + std::chrono::high_resolution_clock::time_point timeStart{}; + }; +} + +namespace cucumber::messages +{ + using cucumber_cpp::library::support::operator+=; +}; + +#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/Join.cpp b/cucumber_cpp/library/support/Join.cpp new file mode 100644 index 00000000..9663e9f1 --- /dev/null +++ b/cucumber_cpp/library/support/Join.cpp @@ -0,0 +1,27 @@ +#include "cucumber_cpp/library/support/Join.hpp" +#include +#include +#include +#include +#include + +namespace cucumber_cpp::library::support +{ + std::string Join(std::initializer_list parts, const std::string& separator) + { + return Join({ parts.begin(), parts.end() }, separator); + } + + std::string Join(std::span parts, const std::string& separator) + { + if (parts.empty()) + return ""; + + std::string joined = *parts.begin(); + + for (const auto part : parts | std::views::drop(1)) + joined += std::format("{}{}", separator, part); + + return joined; + } +} diff --git a/cucumber_cpp/library/support/Join.hpp b/cucumber_cpp/library/support/Join.hpp new file mode 100644 index 00000000..07b185ac --- /dev/null +++ b/cucumber_cpp/library/support/Join.hpp @@ -0,0 +1,14 @@ +#ifndef SUPPORT_JOIN_HPP +#define SUPPORT_JOIN_HPP + +#include +#include +#include + +namespace cucumber_cpp::library::support +{ + std::string Join(std::initializer_list parts, const std::string& separator); + std::string Join(std::span parts, const std::string& separator); +} + +#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/Polyfill.hpp b/cucumber_cpp/library/support/Polyfill.hpp new file mode 100644 index 00000000..37792917 --- /dev/null +++ b/cucumber_cpp/library/support/Polyfill.hpp @@ -0,0 +1,18 @@ +#ifndef SUPPORT_POLYFILL_HPP +#define SUPPORT_POLYFILL_HPP + +#include +#include +#include +#include + +namespace cucumber_cpp::library::support +{ + template + void print(std::ostream& outputStream, std::format_string fmt, Args&&... args) + { + outputStream << std::format(fmt, std::forward(args)...); + } +} + +#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..1af1af30 --- /dev/null +++ b/cucumber_cpp/library/support/StepRegistry.hpp @@ -0,0 +1,171 @@ +#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::support +{ + using StepFactory = std::unique_ptr (&)(util::Broadcaster& broadCaster, Context&, engine::StepOrHookStarted stepOrHookStarted, const std::optional&, const std::optional&); + + template + std::unique_ptr StepBodyFactory(util::Broadcaster& broadCaster, Context& context, engine::StepOrHookStarted stepOrHookStarted, const std::optional& dataTable, const std::optional& docString) + { + return std::make_unique(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..213f1df6 --- /dev/null +++ b/cucumber_cpp/library/support/SupportCodeLibrary.cpp @@ -0,0 +1,92 @@ +#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 +#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::forward_as_tuple(lhs.file_name(), lhs.line()) < std::forward_as_tuple(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) + { + std::cout << std::format("Added ({}): {}:{}\n", type, sourceLocation.file_name(), sourceLocation.line()); + std::cout << "Registry contents:\n"; + for (const auto& [key, item] : registry) + std::cout << std::format(" {}:{}\n", key.file_name(), key.line()); + } + } + + 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/Timestamp.cpp b/cucumber_cpp/library/support/Timestamp.cpp new file mode 100644 index 00000000..c5d61047 --- /dev/null +++ b/cucumber_cpp/library/support/Timestamp.cpp @@ -0,0 +1,61 @@ + +#include "cucumber/messages/timestamp.hpp" +#include "cucumber/messages/duration.hpp" +#include "cucumber_cpp/library/support/Duration.hpp" +#include "cucumber_cpp/library/support/Timestamp.hpp" +#include + +namespace cucumber_cpp::library::support +{ + 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); + } +} diff --git a/cucumber_cpp/library/support/Timestamp.hpp b/cucumber_cpp/library/support/Timestamp.hpp new file mode 100644 index 00000000..1b432490 --- /dev/null +++ b/cucumber_cpp/library/support/Timestamp.hpp @@ -0,0 +1,47 @@ +#ifndef SUPPORT_TIMESTAMP_HPP +#define SUPPORT_TIMESTAMP_HPP + +#include "cucumber/messages/duration.hpp" +#include "cucumber/messages/timestamp.hpp" +#include +#include + +namespace cucumber_cpp::library::support +{ + 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); +} + +namespace cucumber::messages +{ + using cucumber_cpp::library::support::operator-; +}; + +#endif diff --git a/cucumber_cpp/library/support/Types.hpp b/cucumber_cpp/library/support/Types.hpp new file mode 100644 index 00000000..36f9255a --- /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..47076a8d 100644 --- a/cucumber_cpp/library/tag_expression/CMakeLists.txt +++ b/cucumber_cpp/library/tag_expression/CMakeLists.txt @@ -1,6 +1,6 @@ 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 +17,10 @@ target_include_directories(cucumber_cpp.library.tag_expression PUBLIC ../../.. ) +target_link_libraries(cucumber_cpp.library.tag_expression PUBLIC + cucumber_gherkin_lib +) + 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..8535d45c 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 #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,6 +89,16 @@ 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) @@ -83,6 +117,16 @@ 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) @@ -100,6 +144,16 @@ 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) 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/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/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..b3ff55cc 100644 --- a/cucumber_cpp/library/util/CMakeLists.txt +++ b/cucumber_cpp/library/util/CMakeLists.txt @@ -1,9 +1,22 @@ -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 + GetWorstTestStepResult.hpp + GetWorstTestStepResult.cpp Immoveable.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 +) + +if (CCR_BUILD_TESTS) + add_subdirectory(test) + # add_subdirectory(test_helper) +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/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..86bda483 100644 --- a/external/CMakeLists.txt +++ b/external/CMakeLists.txt @@ -6,4 +6,6 @@ add_subdirectory(cliutils) add_subdirectory(cucumber) add_subdirectory(googletest) add_subdirectory(jbeder) +add_subdirectory(jupyter-xeus) +add_subdirectory(tobiaslocker) add_subdirectory(zeux) diff --git a/external/cucumber/gherkin/CMakeLists.txt b/external/cucumber/gherkin/CMakeLists.txt index fbaf6e59..10c9a94f 100644 --- a/external/cucumber/gherkin/CMakeLists.txt +++ b/external/cucumber/gherkin/CMakeLists.txt @@ -1,6 +1,6 @@ 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..b645a475 100644 --- a/external/cucumber/messages/CMakeLists.txt +++ b/external/cucumber/messages/CMakeLists.txt @@ -1,6 +1,6 @@ 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/jbeder/yaml-cpp/CMakeLists.txt b/external/jbeder/yaml-cpp/CMakeLists.txt index 65d76cae..5de41095 100644 --- a/external/jbeder/yaml-cpp/CMakeLists.txt +++ b/external/jbeder/yaml-cpp/CMakeLists.txt @@ -3,4 +3,7 @@ FetchContent_Declare( 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/jupyter-xeus/CMakeLists.txt b/external/jupyter-xeus/CMakeLists.txt new file mode 100644 index 00000000..e886ef01 --- /dev/null +++ b/external/jupyter-xeus/CMakeLists.txt @@ -0,0 +1 @@ +add_subdirectory(cpp-terminal) diff --git a/external/jupyter-xeus/cpp-terminal/CMakeLists.txt b/external/jupyter-xeus/cpp-terminal/CMakeLists.txt new file mode 100644 index 00000000..5855aa30 --- /dev/null +++ b/external/jupyter-xeus/cpp-terminal/CMakeLists.txt @@ -0,0 +1,21 @@ +FetchContent_Declare( + cpp-terminal + GIT_REPOSITORY https://github.com/jupyter-xeus/cpp-terminal + GIT_TAG 48ae2f284084850901c45b6c10a9d68949c1b272 # unreleased main +) + +set_directory_properties(PROPERTIES EXCLUDE_FROM_ALL On) + +if (NOT (CMAKE_CXX_COMPILER_ID MATCHES "MSVC" OR CMAKE_CXX_SIMULATE_ID MATCHES "MSVC")) + add_compile_options( + -Wno-unused-variable + -Wno-unused-but-set-variable + ) +endif() + +set(CPPTERMINAL_BUILD_EXAMPLES Off CACHE STRING "") +set(CPPTERMINAL_ENABLE_INSTALL Off CACHE STRING "") +set(CPPTERMINAL_ENABLE_TESTING Off CACHE STRING "") +set(CPPTERMINAL_ENABLE_DOCS Off CACHE STRING "") + +FetchContent_MakeAvailable(cpp-terminal) 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