From 66908ed4a15d57d4f5644ff23fe44c6587e629a6 Mon Sep 17 00:00:00 2001 From: Danny Ben Shitrit Date: Thu, 9 Jul 2026 14:18:02 +0300 Subject: [PATCH] - Add support for `@BeforeAll` and `@AfterAll` hooks --- README.md | 20 ++- doc/shellkin-feature.5 | 2 - doc/shellkin-feature.md | 1 - doc/shellkin-stepdefs.5 | 19 +- doc/shellkin-stepdefs.md | 15 +- features/fixtures/hooks/hooks.feature | 4 +- .../fixtures/hooks/step_definitions/core.sh | 13 ++ features/step_definitions/core.sh | 3 + features/test.feature | 1 + shellkin | 99 ++++++++++- src/lib/feature/core.sh | 25 ++- src/lib/stepdef/hooks.sh | 45 ++++- src/lib/stepdef/parse.sh | 12 ++ src/root_command.sh | 17 ++ test/commands/test.bats | 166 ++++++++++++++++++ test/lib/feature/core.bats | 50 ++++++ test/lib/stepdef/files.bats | 15 ++ test/lib/stepdef/parse.bats | 30 ++++ 18 files changed, 511 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 1e0f0b7..bf719bc 100644 --- a/README.md +++ b/README.md @@ -85,15 +85,16 @@ Implemented pieces include: | Comments (`#`) | Supported | | Tags (`@tag`) | Supported | | `Before`, `After` hooks | Supported | +| `BeforeAll`, `AfterAll` hooks | Supported | | `Rule` | Unsupported | | `Scenario Outline` | Unsupported | | `Examples` | Unsupported | | Data tables | Unsupported | -| `BeforeAll`, `AfterAll` hooks | Unsupported | Tags can be selected with `--tag` / `-t` and skipped with `--exclude-tag` / `-x`. `@Before` and `@After` hooks are declared in step definition files and may be -limited to a tag. +limited to a tag. `@BeforeAll` and `@AfterAll` hooks are untagged and wrap the +selected scenario run. ## Usage @@ -264,10 +265,12 @@ file. ## Step Definition Hooks -Step definition files can also declare scenario hooks with `@Before` and -`@After`. +Step definition files can also declare hooks. ```bash +@BeforeAll + ./suite-setup + @Before mkdir -p tmp @@ -279,6 +282,9 @@ Step definition files can also declare scenario hooks with `@Before` and @After @needs-server ./server stop + +@AfterAll + ./suite-teardown ``` Hooks without a tag run for every scenario. Tagged hooks run only for scenarios @@ -286,6 +292,12 @@ with that tag, including tags inherited from the feature. `@Before` hooks run before background and scenario steps. `@After` hooks run after the scenario steps, even when a step or `@Before` hook fails. +`@BeforeAll` runs once before the first selected scenario executes. `@AfterAll` +runs once after the last selected scenario executes. If no scenario is selected, +neither all-hook runs. All-hooks do not accept tags. If `@BeforeAll` fails, the +run aborts immediately and `@AfterAll` does not run. If `@AfterAll` fails, the +run fails. + Hooks can call helper functions from `support.sh`: ```bash diff --git a/doc/shellkin-feature.5 b/doc/shellkin-feature.5 index 5397584..2be5dd0 100644 --- a/doc/shellkin-feature.5 +++ b/doc/shellkin-feature.5 @@ -134,8 +134,6 @@ The following common Gherkin constructs are not currently supported: \f[B]Examples\f[R] .IP \(bu 2 data tables -.IP \(bu 2 -\f[B]BeforeAll\f[R] and \f[B]AfterAll\f[R] hooks .SH EXAMPLE .IP .EX diff --git a/doc/shellkin-feature.md b/doc/shellkin-feature.md index 59214bb..86f0174 100644 --- a/doc/shellkin-feature.md +++ b/doc/shellkin-feature.md @@ -162,7 +162,6 @@ The following common Gherkin constructs are not currently supported: - **Scenario Outline** - **Examples** - data tables -- **BeforeAll** and **AfterAll** hooks EXAMPLE ================================================== diff --git a/doc/shellkin-stepdefs.5 b/doc/shellkin-stepdefs.5 index f22386a..7eddba9 100644 --- a/doc/shellkin-stepdefs.5 +++ b/doc/shellkin-stepdefs.5 @@ -75,10 +75,12 @@ Then the text should include \(aqJim \(dqJimbo\(dq Jackson\(aq The opening and closing quote in the feature step must match. Quoted token patterns do not match unquoted values. .SH HOOKS -Step definition files can also declare scenario hooks with -\f[B]\(atBefore\f[R] and \f[B]\(atAfter\f[R]. +Step definition files can also declare hooks. .IP .EX +\(atBeforeAll + ./suite\-setup + \(atBefore mkdir \-p tmp @@ -90,6 +92,9 @@ Step definition files can also declare scenario hooks with \(atAfter \(atneeds\-server ./server stop + +\(atAfterAll + ./suite\-teardown .EE .PP Hooks without a tag run for every scenario. @@ -104,6 +109,16 @@ remaining steps are skipped. \f[B]\(atBefore\f[R] hook fails. If an \f[B]\(atAfter\f[R] hook fails, the scenario fails. .PP +\f[B]\(atBeforeAll\f[R] runs once before the first selected scenario +executes. +\f[B]\(atAfterAll\f[R] runs once after the last selected scenario +executes. +If no scenario is selected, neither all\-hook runs. +All\-hooks do not accept tags. +If \f[B]\(atBeforeAll\f[R] fails, the run aborts immediately and +\f[B]\(atAfterAll\f[R] does not run. +If \f[B]\(atAfterAll\f[R] fails, the run fails. +.PP Passing hooks are quiet. Failing hooks are shown in the error report. .PP diff --git a/doc/shellkin-stepdefs.md b/doc/shellkin-stepdefs.md index 5fcb535..6e0a629 100644 --- a/doc/shellkin-stepdefs.md +++ b/doc/shellkin-stepdefs.md @@ -91,10 +91,12 @@ patterns do not match unquoted values. HOOKS ================================================== -Step definition files can also declare scenario hooks with **@Before** and -**@After**. +Step definition files can also declare hooks. ```bash +@BeforeAll + ./suite-setup + @Before mkdir -p tmp @@ -106,6 +108,9 @@ Step definition files can also declare scenario hooks with **@Before** and @After @needs-server ./server stop + +@AfterAll + ./suite-teardown ``` Hooks without a tag run for every scenario. Tagged hooks run only for scenarios @@ -117,6 +122,12 @@ hook fails, the scenario fails and the remaining steps are skipped. **@After** hooks run after scenario steps, even when a step or **@Before** hook fails. If an **@After** hook fails, the scenario fails. +**@BeforeAll** runs once before the first selected scenario executes. +**@AfterAll** runs once after the last selected scenario executes. If no +scenario is selected, neither all-hook runs. All-hooks do not accept tags. +If **@BeforeAll** fails, the run aborts immediately and **@AfterAll** does not +run. If **@AfterAll** fails, the run fails. + Passing hooks are quiet. Failing hooks are shown in the error report. Hooks can call helper functions from **support.sh**: diff --git a/features/fixtures/hooks/hooks.feature b/features/fixtures/hooks/hooks.feature index 9e889fb..6d712ef 100644 --- a/features/fixtures/hooks/hooks.feature +++ b/features/fixtures/hooks/hooks.feature @@ -1,11 +1,13 @@ Feature: hooks - Before and After hooks + Step definition hooks @needs-server Scenario: tagged hooks run before matching scenarios + Then the hook log should include 'before-all' Then the hook log should include 'before' And the hook log should include 'server-start' Scenario: after hooks run after scenarios + Then the hook log should include 'before-all' Then the hook log should include 'after' And the hook log should include 'server-stop' diff --git a/features/fixtures/hooks/step_definitions/core.sh b/features/fixtures/hooks/step_definitions/core.sh index ab1708b..dfdd9c9 100644 --- a/features/fixtures/hooks/step_definitions/core.sh +++ b/features/fixtures/hooks/step_definitions/core.sh @@ -1,3 +1,6 @@ +@BeforeAll + printf 'before-all\n' >>"$HOOK_LOG" + @Before printf 'before\n' >>"$HOOK_LOG" @@ -10,5 +13,15 @@ @After @needs-server printf 'server-stop\n' >>"$HOOK_LOG" +@AfterAll + printf 'after-all\n' >>"$HOOK_LOG" + printf 'HOOK_COUNTS before-all=%s before=%s after=%s after-all=%s server-start=%s server-stop=%s\n' \ + "$(grep -c '^before-all$' "$HOOK_LOG")" \ + "$(grep -c '^before$' "$HOOK_LOG")" \ + "$(grep -c '^after$' "$HOOK_LOG")" \ + "$(grep -c '^after-all$' "$HOOK_LOG")" \ + "$(grep -c '^server-start$' "$HOOK_LOG")" \ + "$(grep -c '^server-stop$' "$HOOK_LOG")" + @Then the hook log should include '{text}' [[ "$(cat "$HOOK_LOG")" == *"$text"* ]] diff --git a/features/step_definitions/core.sh b/features/step_definitions/core.sh index 09461b8..8fdbfc4 100644 --- a/features/step_definitions/core.sh +++ b/features/step_definitions/core.sh @@ -13,6 +13,9 @@ @Then the output should include '{text}' [[ "$(printf '%s' "$LAST_STDOUT" | strip_ansi)" == *"$text"* ]] +@Then the output should not include '{text}' + [[ "$(printf '%s' "$LAST_STDOUT" | strip_ansi)" != *"$text"* ]] + @Then the error output should include '{text}' [[ "$(printf '%s' "$LAST_STDERR" | strip_ansi)" == *"$text"* ]] diff --git a/features/test.feature b/features/test.feature index b15be7a..f69ad23 100644 --- a/features/test.feature +++ b/features/test.feature @@ -17,6 +17,7 @@ Scenario: Filtering scenarios by tag Scenario: Running hooks from step definition files When I run 'shellkin features/fixtures/hooks' Then the output should include 'Feature: hooks' + And the output should include 'HOOK_COUNTS before-all=1 before=2 after=2 after-all=1 server-start=1 server-stop=1' And the output should include '2 scenarios, 0 failing' And the exit code should mean success diff --git a/shellkin b/shellkin index 5935ba2..d693bc3 100755 --- a/shellkin +++ b/shellkin @@ -84,12 +84,16 @@ root_command() { STEPDEF_TOKENS_LIST=() STEPDEF_CAPTURE_INDEXES_LIST=() STEPDEF_BODIES=() + SHELLKIN_BEFORE_ALL_HOOK_HEADERS=() + SHELLKIN_BEFORE_ALL_HOOK_BODIES=() SHELLKIN_BEFORE_HOOK_TAGS=() SHELLKIN_BEFORE_HOOK_HEADERS=() SHELLKIN_BEFORE_HOOK_BODIES=() SHELLKIN_AFTER_HOOK_TAGS=() SHELLKIN_AFTER_HOOK_HEADERS=() SHELLKIN_AFTER_HOOK_BODIES=() + SHELLKIN_AFTER_ALL_HOOK_HEADERS=() + SHELLKIN_AFTER_ALL_HOOK_BODIES=() if [[ -f $TARGET_PATH ]]; then FEATURES_DIR="$(dirname "$TARGET_PATH")" @@ -176,6 +180,7 @@ root_command() { TEST_INCLUDE_TAGS=("${include_tags[@]}") TEST_EXCLUDE_TAGS=("${exclude_tags[@]}") TEST_ABORT_RUN=0 + ALL_HOOKS_ACTIVE=0 for feature_file in "${feature_files[@]}"; do feature_run "$feature_file" || feature_status=1 @@ -184,6 +189,18 @@ root_command() { fi done + if ((ALL_HOOKS_ACTIVE != 0)); then + if hooks__run_after_all; then + : + else + output_hook_failure "$HOOK_FAILED_HEADER" + feature_status=1 + if ((TEST_SCENARIOS_FAILED == 0 && TEST_SCENARIOS_TOTAL > 0)); then + TEST_SCENARIOS_FAILED=1 + fi + fi + fi + if [[ -n $target_scenario && $feature_status -eq 0 && $TARGET_SCENARIO_MATCHED -eq 0 ]]; then printf 'validation error in TARGET:\nscenario number out of range: %s\n' "$target_scenario" >&2 return 1 @@ -475,7 +492,7 @@ feature_run() { : else failed=1 - if ((TEST_FAIL_FAST != 0)); then + if ((TEST_FAIL_FAST != 0 || TEST_ABORT_RUN != 0)); then TEST_ABORT_RUN=1 scenario_seen=0 break @@ -509,7 +526,7 @@ feature_run() { : else failed=1 - if ((TEST_FAIL_FAST != 0)); then + if ((TEST_FAIL_FAST != 0 || TEST_ABORT_RUN != 0)); then TEST_ABORT_RUN=1 scenario_seen=0 break @@ -566,7 +583,7 @@ feature_run() { : else failed=1 - if ((TEST_FAIL_FAST != 0)); then + if ((TEST_FAIL_FAST != 0 || TEST_ABORT_RUN != 0)); then TEST_ABORT_RUN=1 fi fi @@ -576,7 +593,7 @@ feature_run() { : else failed=1 - if ((TEST_FAIL_FAST != 0)); then + if ((TEST_FAIL_FAST != 0 || TEST_ABORT_RUN != 0)); then TEST_ABORT_RUN=1 fi fi @@ -908,7 +925,20 @@ feature_scenario_run() { output_scenario_start "$scenario_number" "$scenario_name" set +e - if hooks__run_before_all; then + if ((${ALL_HOOKS_ACTIVE:-0} == 0)); then + if hooks__run_before_all; then + ALL_HOOKS_ACTIVE=1 + else + scenario_failed=1 + TEST_ABORT_RUN=1 + output_hook_failure "$HOOK_FAILED_HEADER" + set -e + ((TEST_SCENARIOS_FAILED += 1)) + return 1 + fi + fi + + if hooks__run_before_scenario; then : else scenario_failed=1 @@ -945,7 +975,7 @@ feature_scenario_run() { fi done - if hooks__run_after_all; then + if hooks__run_after_scenario; then : else scenario_failed=1 @@ -1663,6 +1693,10 @@ stepdef_hook_register() { local body=$1 case $STEPDEF_HOOK_TYPE in + BeforeAll) + SHELLKIN_BEFORE_ALL_HOOK_HEADERS+=("$STEPDEF_HOOK_HEADER") + SHELLKIN_BEFORE_ALL_HOOK_BODIES+=("$body") + ;; Before) SHELLKIN_BEFORE_HOOK_TAGS+=("$STEPDEF_HOOK_TAG") SHELLKIN_BEFORE_HOOK_HEADERS+=("$STEPDEF_HOOK_HEADER") @@ -1673,10 +1707,49 @@ stepdef_hook_register() { SHELLKIN_AFTER_HOOK_HEADERS+=("$STEPDEF_HOOK_HEADER") SHELLKIN_AFTER_HOOK_BODIES+=("$body") ;; + AfterAll) + SHELLKIN_AFTER_ALL_HOOK_HEADERS+=("$STEPDEF_HOOK_HEADER") + SHELLKIN_AFTER_ALL_HOOK_BODIES+=("$body") + ;; esac } hooks__run_before_all() { + local index + local hook_header + local body + + HOOK_FAILED_HEADER= + + for index in "${!SHELLKIN_BEFORE_ALL_HOOK_BODIES[@]}"; do + hook_header=${SHELLKIN_BEFORE_ALL_HOOK_HEADERS[$index]} + body=${SHELLKIN_BEFORE_ALL_HOOK_BODIES[$index]} + hooks__run_one "$hook_header" "$body" || return 1 + done +} + +hooks__run_after_all() { + local index + local hook_header + local body + local failed=0 + + HOOK_FAILED_HEADER= + + for index in "${!SHELLKIN_AFTER_ALL_HOOK_BODIES[@]}"; do + hook_header=${SHELLKIN_AFTER_ALL_HOOK_HEADERS[$index]} + body=${SHELLKIN_AFTER_ALL_HOOK_BODIES[$index]} + if hooks__run_one "$hook_header" "$body"; then + : + else + failed=1 + fi + done + + return "$failed" +} + +hooks__run_before_scenario() { local index local tag local hook_header @@ -1694,7 +1767,7 @@ hooks__run_before_all() { done } -hooks__run_after_all() { +hooks__run_after_scenario() { local index local tag local hook_header @@ -1822,6 +1895,7 @@ stepdef_parse() { if stepdef_hook_type_valid "$keyword"; then if [[ -n $remainder ]]; then + stepdef_hook_tag_allowed "$keyword" || return 1 stepdef_hook_tag_valid "$remainder" || return 1 fi @@ -1839,6 +1913,17 @@ stepdef_parse() { } stepdef_hook_type_valid() { + case $1 in + Before | After | BeforeAll | AfterAll) + return 0 + ;; + *) + return 1 + ;; + esac +} + +stepdef_hook_tag_allowed() { case $1 in Before | After) return 0 diff --git a/src/lib/feature/core.sh b/src/lib/feature/core.sh index da1be55..563f55f 100644 --- a/src/lib/feature/core.sh +++ b/src/lib/feature/core.sh @@ -78,7 +78,7 @@ feature_run() { : else failed=1 - if ((TEST_FAIL_FAST != 0)); then + if ((TEST_FAIL_FAST != 0 || TEST_ABORT_RUN != 0)); then TEST_ABORT_RUN=1 scenario_seen=0 break @@ -112,7 +112,7 @@ feature_run() { : else failed=1 - if ((TEST_FAIL_FAST != 0)); then + if ((TEST_FAIL_FAST != 0 || TEST_ABORT_RUN != 0)); then TEST_ABORT_RUN=1 scenario_seen=0 break @@ -169,7 +169,7 @@ feature_run() { : else failed=1 - if ((TEST_FAIL_FAST != 0)); then + if ((TEST_FAIL_FAST != 0 || TEST_ABORT_RUN != 0)); then TEST_ABORT_RUN=1 fi fi @@ -179,7 +179,7 @@ feature_run() { : else failed=1 - if ((TEST_FAIL_FAST != 0)); then + if ((TEST_FAIL_FAST != 0 || TEST_ABORT_RUN != 0)); then TEST_ABORT_RUN=1 fi fi @@ -518,7 +518,20 @@ feature_scenario_run() { output_scenario_start "$scenario_number" "$scenario_name" set +e - if hooks__run_before_all; then + if ((${ALL_HOOKS_ACTIVE:-0} == 0)); then + if hooks__run_before_all; then + ALL_HOOKS_ACTIVE=1 + else + scenario_failed=1 + TEST_ABORT_RUN=1 + output_hook_failure "$HOOK_FAILED_HEADER" + set -e + ((TEST_SCENARIOS_FAILED += 1)) + return 1 + fi + fi + + if hooks__run_before_scenario; then : else scenario_failed=1 @@ -555,7 +568,7 @@ feature_scenario_run() { fi done - if hooks__run_after_all; then + if hooks__run_after_scenario; then : else scenario_failed=1 diff --git a/src/lib/stepdef/hooks.sh b/src/lib/stepdef/hooks.sh index 2d5a418..ecc8143 100644 --- a/src/lib/stepdef/hooks.sh +++ b/src/lib/stepdef/hooks.sh @@ -3,6 +3,10 @@ stepdef_hook_register() { local body=$1 case $STEPDEF_HOOK_TYPE in + BeforeAll) + SHELLKIN_BEFORE_ALL_HOOK_HEADERS+=("$STEPDEF_HOOK_HEADER") + SHELLKIN_BEFORE_ALL_HOOK_BODIES+=("$body") + ;; Before) SHELLKIN_BEFORE_HOOK_TAGS+=("$STEPDEF_HOOK_TAG") SHELLKIN_BEFORE_HOOK_HEADERS+=("$STEPDEF_HOOK_HEADER") @@ -13,10 +17,49 @@ stepdef_hook_register() { SHELLKIN_AFTER_HOOK_HEADERS+=("$STEPDEF_HOOK_HEADER") SHELLKIN_AFTER_HOOK_BODIES+=("$body") ;; + AfterAll) + SHELLKIN_AFTER_ALL_HOOK_HEADERS+=("$STEPDEF_HOOK_HEADER") + SHELLKIN_AFTER_ALL_HOOK_BODIES+=("$body") + ;; esac } hooks__run_before_all() { + local index + local hook_header + local body + + HOOK_FAILED_HEADER= + + for index in "${!SHELLKIN_BEFORE_ALL_HOOK_BODIES[@]}"; do + hook_header=${SHELLKIN_BEFORE_ALL_HOOK_HEADERS[$index]} + body=${SHELLKIN_BEFORE_ALL_HOOK_BODIES[$index]} + hooks__run_one "$hook_header" "$body" || return 1 + done +} + +hooks__run_after_all() { + local index + local hook_header + local body + local failed=0 + + HOOK_FAILED_HEADER= + + for index in "${!SHELLKIN_AFTER_ALL_HOOK_BODIES[@]}"; do + hook_header=${SHELLKIN_AFTER_ALL_HOOK_HEADERS[$index]} + body=${SHELLKIN_AFTER_ALL_HOOK_BODIES[$index]} + if hooks__run_one "$hook_header" "$body"; then + : + else + failed=1 + fi + done + + return "$failed" +} + +hooks__run_before_scenario() { local index local tag local hook_header @@ -34,7 +77,7 @@ hooks__run_before_all() { done } -hooks__run_after_all() { +hooks__run_after_scenario() { local index local tag local hook_header diff --git a/src/lib/stepdef/parse.sh b/src/lib/stepdef/parse.sh index 58cf40d..7200856 100644 --- a/src/lib/stepdef/parse.sh +++ b/src/lib/stepdef/parse.sh @@ -61,6 +61,7 @@ stepdef_parse() { if stepdef_hook_type_valid "$keyword"; then if [[ -n $remainder ]]; then + stepdef_hook_tag_allowed "$keyword" || return 1 stepdef_hook_tag_valid "$remainder" || return 1 fi @@ -78,6 +79,17 @@ stepdef_parse() { } stepdef_hook_type_valid() { + case $1 in + Before | After | BeforeAll | AfterAll) + return 0 + ;; + *) + return 1 + ;; + esac +} + +stepdef_hook_tag_allowed() { case $1 in Before | After) return 0 diff --git a/src/root_command.sh b/src/root_command.sh index e5c2b82..2c841dc 100644 --- a/src/root_command.sh +++ b/src/root_command.sh @@ -70,12 +70,16 @@ STEPDEF_REGEXES=() STEPDEF_TOKENS_LIST=() STEPDEF_CAPTURE_INDEXES_LIST=() STEPDEF_BODIES=() +SHELLKIN_BEFORE_ALL_HOOK_HEADERS=() +SHELLKIN_BEFORE_ALL_HOOK_BODIES=() SHELLKIN_BEFORE_HOOK_TAGS=() SHELLKIN_BEFORE_HOOK_HEADERS=() SHELLKIN_BEFORE_HOOK_BODIES=() SHELLKIN_AFTER_HOOK_TAGS=() SHELLKIN_AFTER_HOOK_HEADERS=() SHELLKIN_AFTER_HOOK_BODIES=() +SHELLKIN_AFTER_ALL_HOOK_HEADERS=() +SHELLKIN_AFTER_ALL_HOOK_BODIES=() if [[ -f $TARGET_PATH ]]; then FEATURES_DIR="$(dirname "$TARGET_PATH")" @@ -162,6 +166,7 @@ else TEST_INCLUDE_TAGS=("${include_tags[@]}") TEST_EXCLUDE_TAGS=("${exclude_tags[@]}") TEST_ABORT_RUN=0 + ALL_HOOKS_ACTIVE=0 for feature_file in "${feature_files[@]}"; do feature_run "$feature_file" || feature_status=1 @@ -170,6 +175,18 @@ else fi done + if ((ALL_HOOKS_ACTIVE != 0)); then + if hooks__run_after_all; then + : + else + output_hook_failure "$HOOK_FAILED_HEADER" + feature_status=1 + if ((TEST_SCENARIOS_FAILED == 0 && TEST_SCENARIOS_TOTAL > 0)); then + TEST_SCENARIOS_FAILED=1 + fi + fi + fi + if [[ -n $target_scenario && $feature_status -eq 0 && $TARGET_SCENARIO_MATCHED -eq 0 ]]; then printf 'validation error in TARGET:\nscenario number out of range: %s\n' "$target_scenario" >&2 return 1 diff --git a/test/commands/test.bats b/test/commands/test.bats index e775bd3..69078b0 100644 --- a/test/commands/test.bats +++ b/test/commands/test.bats @@ -126,6 +126,76 @@ EOF assert_output_contains "1 scenario, 0 failing" } +@test "shellkin runs all hooks around tag-filtered scenarios" { + write_file features/sample.feature <<'EOF' +Feature: Tagged all hooks + +@selected +Scenario: First + Then I write 'first' + +Scenario: Skipped + Then I write 'skipped' + +@selected +Scenario: Second + Then I write 'second' +EOF + + write_file features/step_definitions/core.sh <<'EOF' +@BeforeAll +printf 'before-all\n' >> "$TEST_ROOT/result.txt" + +@Before +printf 'before\n' >> "$TEST_ROOT/result.txt" + +@After +printf 'after\n' >> "$TEST_ROOT/result.txt" + +@AfterAll +printf 'after-all\n' >> "$TEST_ROOT/result.txt" + +@Then I write '{text}' +printf 'step:%s\n' "$text" >> "$TEST_ROOT/result.txt" +EOF + + run "$SHELLKIN_REPO_ROOT/shellkin" -t @selected "$TEST_ROOT/features" + + [ "$status" -eq 0 ] + [ "$(cat "$TEST_ROOT/result.txt")" = $'before-all\nbefore\nstep:first\nafter\nbefore\nstep:second\nafter\nafter-all' ] + assert_output_contains "Scenario 1: First" + [[ "$output" != *"Scenario 2: Skipped"* ]] + assert_output_contains "Scenario 3: Second" + assert_output_contains "2 scenarios, 0 failing" +} + +@test "shellkin does not run all hooks when tag filters select no scenarios" { + write_file features/sample.feature <<'EOF' +Feature: Unmatched all hooks + +@selected +Scenario: Example + Then I write 'selected' +EOF + + write_file features/step_definitions/core.sh <<'EOF' +@BeforeAll +printf 'before-all\n' >> "$TEST_ROOT/result.txt" + +@AfterAll +printf 'after-all\n' >> "$TEST_ROOT/result.txt" + +@Then I write '{text}' +printf 'step:%s\n' "$text" >> "$TEST_ROOT/result.txt" +EOF + + run "$SHELLKIN_REPO_ROOT/shellkin" -t @missing "$TEST_ROOT/features" + + [ "$status" -eq 0 ] + [ ! -e "$TEST_ROOT/result.txt" ] + assert_output_contains "0 scenarios, 0 failing" +} + @test "shellkin rejects invalid CLI tag filters" { write_file features/sample.feature <<'EOF' Feature: Tagged feature @@ -496,6 +566,43 @@ EOF assert_output_contains "1 scenario, 0 failing" } +@test "shellkin runs all hooks around the selected scenario number" { + write_file features/sample.feature <<'EOF' +Feature: All hooks with scenario number + +Scenario: First + Then I write 'first' + +Scenario: Second + Then I write 'second' +EOF + + write_file features/step_definitions/core.sh <<'EOF' +@BeforeAll +printf 'before-all\n' >> "$TEST_ROOT/result.txt" + +@Before +printf 'before\n' >> "$TEST_ROOT/result.txt" + +@After +printf 'after\n' >> "$TEST_ROOT/result.txt" + +@AfterAll +printf 'after-all\n' >> "$TEST_ROOT/result.txt" + +@Then I write '{text}' +printf 'step:%s\n' "$text" >> "$TEST_ROOT/result.txt" +EOF + + run "$SHELLKIN_REPO_ROOT/shellkin" "$TEST_ROOT/features/sample.feature:2" + + [ "$status" -eq 0 ] + [ "$(cat "$TEST_ROOT/result.txt")" = $'before-all\nbefore\nstep:second\nafter\nafter-all' ] + [[ "$output" != *"Scenario 1: First"* ]] + assert_output_contains "Scenario 2: Second" + assert_output_contains "1 scenario, 0 failing" +} + @test "shellkin omits headings for feature files with no selected scenarios" { write_file features/first.feature <<'EOF' Feature: First feature @@ -570,3 +677,62 @@ EOF assert_output_contains "validation error in TARGET:" assert_output_contains "scenario number out of range: 9" } + +@test "shellkin aborts when BeforeAll fails and skips AfterAll" { + write_file features/sample.feature <<'EOF' +Feature: Failing BeforeAll + +Scenario: First + Then I pass + +Scenario: Second + Then I pass +EOF + + write_file features/step_definitions/core.sh <<'EOF' +@BeforeAll +fail "suite setup failed" + +@AfterAll +printf 'after-all\n' >> "$TEST_ROOT/result.txt" + +@Then I pass +true +EOF + + run "$SHELLKIN_REPO_ROOT/shellkin" "$TEST_ROOT/features" + + [ "$status" -eq 1 ] + [ ! -e "$TEST_ROOT/result.txt" ] + assert_output_contains "Scenario 1: First" + [[ "$output" != *"Scenario 2: Second"* ]] + assert_output_contains "✗ @BeforeAll" + assert_output_contains "FAIL_MESSAGE: suite setup failed" + assert_output_contains "1 scenario, 0 passing, 1 failing" +} + +@test "shellkin fails when AfterAll fails" { + write_file features/sample.feature <<'EOF' +Feature: Failing AfterAll + +Scenario: First + Then I pass +EOF + + write_file features/step_definitions/core.sh <<'EOF' +@AfterAll +fail "suite teardown failed" + +@Then I pass +true +EOF + + run "$SHELLKIN_REPO_ROOT/shellkin" "$TEST_ROOT/features" + + [ "$status" -eq 1 ] + assert_output_contains "Scenario 1: First" + assert_output_contains "✓ Then I pass" + assert_output_contains "✗ @AfterAll" + assert_output_contains "FAIL_MESSAGE: suite teardown failed" + assert_output_contains "1 scenario, 0 passing, 1 failing" +} diff --git a/test/lib/feature/core.bats b/test/lib/feature/core.bats index 3758eb0..fddb0ad 100644 --- a/test/lib/feature/core.bats +++ b/test/lib/feature/core.bats @@ -23,16 +23,21 @@ setup() { FEATURE_RECORDED_STEP_TEXT= FEATURE_RECORDED_STEP_DOC_STRING= SCENARIO_DEFERRED_COMMANDS=() + SHELLKIN_BEFORE_ALL_HOOK_HEADERS=() + SHELLKIN_BEFORE_ALL_HOOK_BODIES=() SHELLKIN_BEFORE_HOOK_TAGS=() SHELLKIN_BEFORE_HOOK_HEADERS=() SHELLKIN_BEFORE_HOOK_BODIES=() SHELLKIN_AFTER_HOOK_TAGS=() SHELLKIN_AFTER_HOOK_HEADERS=() SHELLKIN_AFTER_HOOK_BODIES=() + SHELLKIN_AFTER_ALL_HOOK_HEADERS=() + SHELLKIN_AFTER_ALL_HOOK_BODIES=() TEST_SCENARIOS_TOTAL=0 TEST_SCENARIOS_FAILED=0 TEST_FAIL_FAST=0 TEST_ABORT_RUN=0 + ALL_HOOKS_ACTIVE=0 cd "$TEST_ROOT" } @@ -83,6 +88,24 @@ teardown() { [ "$(cat "$TEST_ROOT/order.txt")" = $'before\nstep\nafter' ] } +@test "feature_scenario_run runs BeforeAll once before the first selected scenario" { + stepdef_parse "@BeforeAll" + stepdef_hook_register 'printf "before-all\n" >>"$TEST_ROOT/order.txt"' + stepdef_parse "@Before" + stepdef_hook_register 'printf "before\n" >>"$TEST_ROOT/order.txt"' + stepdef_parse "@When I record the step" + stepdef_register 'printf "step\n" >>"$TEST_ROOT/order.txt"' + + background_steps=() + scenario_steps=($'When\tI record the step') + + feature_scenario_run "First" background_steps scenario_steps + feature_scenario_run "Second" background_steps scenario_steps + + [ "$(cat "$TEST_ROOT/order.txt")" = $'before-all\nbefore\nstep\nbefore\nstep' ] + [ "$ALL_HOOKS_ACTIVE" -eq 1 ] +} + @test "feature_scenario_run runs tagged hooks only for matching scenarios" { stepdef_parse "@Before @needs-server" stepdef_hook_register 'printf "tagged\n" >>"$TEST_ROOT/hooks.txt"' @@ -97,6 +120,33 @@ teardown() { [ "$(cat "$TEST_ROOT/hooks.txt")" = "tagged" ] } +@test "feature_scenario_run aborts when BeforeAll fails" { + stepdef_parse "@BeforeAll" + stepdef_hook_register 'fail "suite setup failed"' + stepdef_parse "@Before" + stepdef_hook_register 'touch "$TEST_ROOT/before-scenario-ran"' + stepdef_parse "@When I create a file" + stepdef_register 'touch "$TEST_ROOT/should-not-exist"' + background_steps=() + scenario_steps=($'When\tI create a file') + + if feature_scenario_run "Example" background_steps scenario_steps >"$TEST_ROOT/output.txt"; then + status=0 + else + status=$? + fi + + [ "$status" -eq 1 ] + [ "$TEST_ABORT_RUN" -eq 1 ] + [ "$ALL_HOOKS_ACTIVE" -eq 0 ] + [ "$TEST_SCENARIOS_FAILED" -eq 1 ] + [ ! -e "$TEST_ROOT/before-scenario-ran" ] + [ ! -e "$TEST_ROOT/should-not-exist" ] + output=$(strip_ansi <"$TEST_ROOT/output.txt") + assert_output_contains "✗ @BeforeAll" + assert_output_contains "FAIL_MESSAGE: suite setup failed" +} + @test "feature_scenario_run skips steps when Before fails and still runs After" { stepdef_parse "@Before" stepdef_hook_register 'fail "setup failed"' diff --git a/test/lib/stepdef/files.bats b/test/lib/stepdef/files.bats index fe89505..03cbdec 100644 --- a/test/lib/stepdef/files.bats +++ b/test/lib/stepdef/files.bats @@ -12,12 +12,16 @@ setup() { STEPDEF_TOKENS_LIST=() STEPDEF_CAPTURE_INDEXES_LIST=() STEPDEF_BODIES=() + SHELLKIN_BEFORE_ALL_HOOK_HEADERS=() + SHELLKIN_BEFORE_ALL_HOOK_BODIES=() SHELLKIN_BEFORE_HOOK_TAGS=() SHELLKIN_BEFORE_HOOK_HEADERS=() SHELLKIN_BEFORE_HOOK_BODIES=() SHELLKIN_AFTER_HOOK_TAGS=() SHELLKIN_AFTER_HOOK_HEADERS=() SHELLKIN_AFTER_HOOK_BODIES=() + SHELLKIN_AFTER_ALL_HOOK_HEADERS=() + SHELLKIN_AFTER_ALL_HOOK_BODIES=() } teardown() { @@ -106,6 +110,9 @@ EOF @test "stepdefs_file_parse registers hook bodies from step definition files" { write_file stepdefs.sh <<'EOF' +@BeforeAll +setup_suite + @Before setup_each @@ -115,12 +122,18 @@ start_server @After @needs-server stop_server +@AfterAll +teardown_suite + @Then the hook log should include '{text}' [[ "$HOOK_LOG" == *"$text"* ]] EOF stepdefs_file_parse "$TEST_ROOT/stepdefs.sh" + [ "${#SHELLKIN_BEFORE_ALL_HOOK_BODIES[@]}" -eq 1 ] + [ "${SHELLKIN_BEFORE_ALL_HOOK_HEADERS[0]}" = "@BeforeAll" ] + [ "${SHELLKIN_BEFORE_ALL_HOOK_BODIES[0]}" = $'setup_suite\n' ] [ "${#SHELLKIN_BEFORE_HOOK_BODIES[@]}" -eq 2 ] [ "${SHELLKIN_BEFORE_HOOK_HEADERS[0]}" = "@Before" ] [ "${SHELLKIN_BEFORE_HOOK_BODIES[0]}" = $'setup_each\n' ] @@ -129,6 +142,8 @@ EOF [ "${SHELLKIN_BEFORE_HOOK_BODIES[1]}" = $'start_server\n' ] [ "${SHELLKIN_AFTER_HOOK_HEADERS[0]}" = "@After @needs-server" ] [ "${SHELLKIN_AFTER_HOOK_BODIES[0]}" = $'stop_server\n' ] + [ "${SHELLKIN_AFTER_ALL_HOOK_HEADERS[0]}" = "@AfterAll" ] + [ "${SHELLKIN_AFTER_ALL_HOOK_BODIES[0]}" = $'teardown_suite\n' ] [ "${STEPDEF_TYPES[0]}" = "Then" ] } diff --git a/test/lib/stepdef/parse.bats b/test/lib/stepdef/parse.bats index cab8d64..f402a79 100644 --- a/test/lib/stepdef/parse.bats +++ b/test/lib/stepdef/parse.bats @@ -75,6 +75,24 @@ teardown() { [ "$STEPDEF_HOOK_HEADER" = "@After @needs-server" ] } +@test "stepdef_parse reads an untagged BeforeAll hook" { + stepdef_parse "@BeforeAll" + + [ "$STEPDEF_HEADER_KIND" = "hook" ] + [ "$STEPDEF_HOOK_TYPE" = "BeforeAll" ] + [ "$STEPDEF_HOOK_TAG" = "" ] + [ "$STEPDEF_HOOK_HEADER" = "@BeforeAll" ] +} + +@test "stepdef_parse reads an untagged AfterAll hook" { + stepdef_parse "@AfterAll" + + [ "$STEPDEF_HEADER_KIND" = "hook" ] + [ "$STEPDEF_HOOK_TYPE" = "AfterAll" ] + [ "$STEPDEF_HOOK_TAG" = "" ] + [ "$STEPDEF_HOOK_HEADER" = "@AfterAll" ] +} + @test "stepdef_parse returns non-zero for a line that is not a step definition" { run stepdef_parse "echo hello" @@ -99,6 +117,18 @@ teardown() { [ "$status" -eq 1 ] } +@test "stepdef_parse returns non-zero for a tagged BeforeAll hook" { + run stepdef_parse "@BeforeAll @needs-server" + + [ "$status" -eq 1 ] +} + +@test "stepdef_parse returns non-zero for a tagged AfterAll hook" { + run stepdef_parse "@AfterAll @needs-server" + + [ "$status" -eq 1 ] +} + @test "stepdef_register stores the parsed definition and body" { stepdef_parse "@When I run '{command}'"