diff --git a/README.md b/README.md
index f0362d6..c254897 100644
--- a/README.md
+++ b/README.md
@@ -62,15 +62,12 @@ through the repository `features/` directory.
Implemented pieces include:
-- feature discovery from a directory or a single `.feature` file
-- optional support script loading with `--load`
-- step definition loading from `step_definitions/*.sh` and `*.bash`
-- step matching with `{token}` placeholders
-- `Background`, `Scenario`, `Given` / `When` / `Then`, `And` / `But`, and `*`
-- doc strings via Gherkin-style `"""` blocks exposed as `DOC_STRING`
-- data tables exposed as `TABLE_HEADER` and `TABLE_ROWS` arrays
-- `Scenario Outline` expansion from one `Examples` table
-- colored terminal output and scenario summary
+- feature discovery, validation, filtering, and scenario selection
+- English Gherkin features, rules, backgrounds, scenarios, and outlines
+- steps, tags, comments, doc strings, and data tables
+- step definitions with named placeholders and lifecycle hooks
+- optional support script loading
+- fail-fast execution, deferred cleanup, and scenario summaries
### Gherkin Feature Support
@@ -78,8 +75,11 @@ Implemented pieces include:
|:------------------------------|:------------|
| `Feature` | Supported |
| Feature description text | Supported |
-| `Scenario` | Supported |
+| `Rule` | Supported |
| `Background` | Supported |
+| `Scenario` | Supported |
+| `Scenario Outline` | Supported |
+| `Examples` | Supported |
| `Given`, `When`, `Then` | Supported |
| `And` , `But` | Supported |
| `*` step keyword | Supported |
@@ -89,9 +89,10 @@ Implemented pieces include:
| `Before`, `After` hooks | Supported |
| `BeforeAll`, `AfterAll` hooks | Supported |
| Data tables | Supported |
-| `Scenario Outline` | Supported |
-| `Examples` | Supported |
-| `Rule` | Unsupported |
+
+Shellkin intentionally implements a compact English Gherkin dialect. Keyword
+aliases and localization, multiple Examples blocks, tags on Examples blocks,
+and escaped data-table cells are not currently supported.
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
diff --git a/doc/shellkin-feature.5 b/doc/shellkin-feature.5
index 036bc1f..bc41e15 100644
--- a/doc/shellkin-feature.5
+++ b/doc/shellkin-feature.5
@@ -16,6 +16,8 @@ Shellkin currently supports these Gherkin keywords:
.IP \(bu 2
\f[B]Background\f[R]
.IP \(bu 2
+\f[B]Rule\f[R]
+.IP \(bu 2
\f[B]Scenario\f[R]
.IP \(bu 2
\f[B]Scenario Outline\f[R], \f[B]Examples\f[R]
@@ -55,6 +57,37 @@ in the feature.
Background:
Given I am in a temp directory
.EE
+.SS Rule
+Use \f[B]Rule:\f[R] to group scenarios that illustrate one business
+rule.
+Tags on a Rule are inherited by every scenario in that Rule.
+Free\-form description text may appear below the Rule header.
+.IP
+.EX
+\(atadmin
+Rule: Administrators can manage users
+ Administrators have access to user management.
+
+ Scenario: Deactivate a user
+ When I deactivate user \(aqAlice\(aq
+ Then user \(aqAlice\(aq should be inactive
+.EE
+.PP
+A Rule may have one \f[B]Background:\f[R] before its first scenario.
+Feature Background steps run first, followed by Rule Background steps.
+.IP
+.EX
+Background:
+ Given the current user is \(aqDana\(aq
+
+Rule: Administrators can manage users
+ Background:
+ Given the current role is \(aqadmin\(aq
+
+ Scenario: Deactivate a user
+ When I deactivate user \(aqAlice\(aq
+ Then user \(aqAlice\(aq should be inactive
+.EE
.SS Scenario Outline
Use \f[B]Scenario Outline:\f[R] with one \f[B]Examples:\f[R] table to
run the same scenario with several values.
@@ -169,10 +202,6 @@ Given these users exist
All rows must contain the same number of cells.
Cell values are trimmed.
Escaped pipe characters in cells are not currently supported.
-.SH UNSUPPORTED CONSTRUCTS
-The following common Gherkin constructs are not currently supported:
-.IP \(bu 2
-\f[B]Rule\f[R]
.SH EXAMPLE
.IP
.EX
diff --git a/doc/shellkin-feature.md b/doc/shellkin-feature.md
index 951e3a0..691b945 100644
--- a/doc/shellkin-feature.md
+++ b/doc/shellkin-feature.md
@@ -23,6 +23,7 @@ Shellkin currently supports these Gherkin keywords:
- **Feature**
- **Background**
+- **Rule**
- **Scenario**
- **Scenario Outline**, **Examples**
- **Given**, **When**, **Then**
@@ -73,6 +74,39 @@ Background:
Given I am in a temp directory
```
+Rule
+--------------------------------------------------
+
+Use **Rule:** to group scenarios that illustrate one business rule. Tags on a
+Rule are inherited by every scenario in that Rule. Free-form description text
+may appear below the Rule header.
+
+```gherkin
+@admin
+Rule: Administrators can manage users
+ Administrators have access to user management.
+
+ Scenario: Deactivate a user
+ When I deactivate user 'Alice'
+ Then user 'Alice' should be inactive
+```
+
+A Rule may have one **Background:** before its first scenario. Feature
+Background steps run first, followed by Rule Background steps.
+
+```gherkin
+Background:
+ Given the current user is 'Dana'
+
+Rule: Administrators can manage users
+ Background:
+ Given the current role is 'admin'
+
+ Scenario: Deactivate a user
+ When I deactivate user 'Alice'
+ Then user 'Alice' should be inactive
+```
+
Scenario Outline
--------------------------------------------------
@@ -196,13 +230,6 @@ Given these users exist
All rows must contain the same number of cells. Cell values are trimmed.
Escaped pipe characters in cells are not currently supported.
-UNSUPPORTED CONSTRUCTS
-==================================================
-
-The following common Gherkin constructs are not currently supported:
-
-- **Rule**
-
EXAMPLE
==================================================
diff --git a/features/fixtures/rules/rules.feature b/features/fixtures/rules/rules.feature
new file mode 100644
index 0000000..4a2e5ca
--- /dev/null
+++ b/features/fixtures/rules/rules.feature
@@ -0,0 +1,30 @@
+@access
+Feature: rules
+ Group examples by business rule
+
+Background:
+ Given the current user is 'Dana'
+
+@elevated
+Rule: Elevated roles can access protected areas
+ These roles have broader access.
+
+ Background:
+ Given the current role is 'admin'
+
+ Scenario Outline: Accessing
+ Then the user should have access to ''
+
+ Examples:
+ | area |
+ | settings |
+ | reports |
+
+@standard
+Rule: Standard roles cannot access protected areas
+
+ Background:
+ Given the current role is 'member'
+
+ Scenario: Accessing settings
+ Then the user should not have access to 'settings'
diff --git a/features/fixtures/rules/step_definitions/core.sh b/features/fixtures/rules/step_definitions/core.sh
new file mode 100644
index 0000000..64441a2
--- /dev/null
+++ b/features/fixtures/rules/step_definitions/core.sh
@@ -0,0 +1,15 @@
+@Given the current user is '{name}'
+ CURRENT_USER=$name
+
+@Given the current role is '{role}'
+ CURRENT_ROLE=$role
+
+@Then the user should have access to '{area}'
+ [[ $CURRENT_USER == Dana ]]
+ [[ $CURRENT_ROLE == admin ]]
+ [[ $area == settings || $area == reports ]]
+
+@Then the user should not have access to '{area}'
+ [[ $CURRENT_USER == Dana ]]
+ [[ $CURRENT_ROLE == member ]]
+ [[ $area == settings ]]
diff --git a/features/test.feature b/features/test.feature
index 4198217..4f5dcee 100644
--- a/features/test.feature
+++ b/features/test.feature
@@ -43,6 +43,22 @@ Scenario: Selecting one expanded scenario outline row
And the output should include '1 scenario, 0 failing'
And the exit code should mean success
+Scenario: Running scenarios grouped by Rules
+ When I run 'shellkin features/fixtures/rules'
+ Then the output should include 'Feature: rules'
+ And the output should include 'Scenario 1: Accessing settings'
+ And the output should include 'Scenario 2: Accessing reports'
+ And the output should include 'Scenario 3: Accessing settings'
+ And the output should include '3 scenarios, 0 failing'
+ And the exit code should mean success
+
+Scenario: Filtering scenarios by a Rule tag
+ When I run 'shellkin --tag @standard features/fixtures/rules'
+ Then the output should include 'Scenario 3: Accessing settings'
+ And the output should not include 'Scenario 1: Accessing settings'
+ And the output should include '1 scenario, 0 failing'
+ And the exit code should mean success
+
Scenario: Running a failing test
When I run 'shellkin features/fixtures/selective/failing.feature'
Then the output should include 'Feature: two'
diff --git a/shellkin b/shellkin
index 802dbbc..427e6a7 100755
--- a/shellkin
+++ b/shellkin
@@ -819,6 +819,13 @@ feature_parse() {
local line_number=0
local section=
local feature_seen=0
+ local feature_background_seen=0
+ local feature_scenario_seen=0
+ local rule_seen=0
+ local rule_background_seen=0
+ local rule_scenario_seen=0
+ local rule_line=0
+ local rule_name=
local scenario_seen=0
local scenario_outline=0
local scenario_line=0
@@ -834,11 +841,14 @@ feature_parse() {
local pending_tags_line=0
local pending_tags_context=
local -a feature_tags=()
+ local -a rule_tags=()
local -a pending_tags=()
local -a parsed_tags=()
local -a scenario_tags=()
local -a background_steps=()
local -a background_step_lines=()
+ local -a feature_background_steps=()
+ local -a feature_background_step_lines=()
local -a scenario_steps=()
local -a scenario_step_lines=()
local -a example_rows=()
@@ -934,8 +944,45 @@ feature_parse() {
fi
continue
;;
+ rule)
+ if ((feature_seen == 0)); then
+ feature_validation_set_error "$line_number" "Rule must appear after Feature" "$(trim "$line")"
+ failed=1
+ break
+ fi
+ if ((scenario_seen != 0)); then
+ feature__parsed_scenario_finish "$scenario_outline" "$scenario_line" "$examples_line" feature_tags scenario_tags background_steps background_step_lines scenario_steps scenario_step_lines example_rows example_row_lines || {
+ failed=1
+ break
+ }
+ scenario_seen=0
+ fi
+ if ((rule_seen != 0 && rule_scenario_seen == 0)); then
+ feature_validation_set_error "$rule_line" "Rule must contain at least one Scenario" "Rule: $rule_name"
+ failed=1
+ break
+ fi
+ if ((rule_seen == 0)); then
+ feature_background_steps=("${background_steps[@]}")
+ feature_background_step_lines=("${background_step_lines[@]}")
+ fi
+ background_steps=("${feature_background_steps[@]}")
+ background_step_lines=("${feature_background_step_lines[@]}")
+ rule_seen=1
+ rule_background_seen=0
+ rule_scenario_seen=0
+ rule_line=$line_number
+ rule_name=$FEATURE_LINE_NAME
+ rule_tags=("${pending_tags[@]}")
+ pending_tags=()
+ pending_tags_line=0
+ pending_tags_context=
+ section=rule
+ in_description=1
+ continue
+ ;;
background)
- if ((feature_seen == 0 || scenario_seen != 0)); then
+ if ((feature_seen == 0)); then
feature_validation_set_error "$line_number" "Background must appear after Feature and before the first Scenario" "$(trim "$line")"
failed=1
break
@@ -945,6 +992,21 @@ feature_parse() {
failed=1
break
fi
+ if ((rule_seen != 0)); then
+ if ((scenario_seen != 0 || rule_scenario_seen != 0 || rule_background_seen != 0)); then
+ feature_validation_set_error "$line_number" "Rule Background must appear before the first Scenario and only once" "$(trim "$line")"
+ failed=1
+ break
+ fi
+ rule_background_seen=1
+ else
+ if ((scenario_seen != 0 || feature_scenario_seen != 0 || feature_background_seen != 0)); then
+ feature_validation_set_error "$line_number" "Background must appear after Feature and before the first Scenario" "$(trim "$line")"
+ failed=1
+ break
+ fi
+ feature_background_seen=1
+ fi
section=background
in_description=0
continue
@@ -970,10 +1032,15 @@ feature_parse() {
scenario_line=$line_number
examples_seen=0
examples_line=0
+ if ((rule_seen != 0)); then
+ rule_scenario_seen=1
+ else
+ feature_scenario_seen=1
+ fi
section=scenario
in_description=0
FEATURE_SCENARIO_NAME=$FEATURE_LINE_NAME
- scenario_tags=("${pending_tags[@]}")
+ scenario_tags=("${rule_tags[@]}" "${pending_tags[@]}")
pending_tags=()
pending_tags_line=0
pending_tags_context=
@@ -1034,7 +1101,7 @@ feature_parse() {
continue
;;
other)
- if [[ $section == feature && $in_description == 1 ]]; then
+ if [[ $section == feature || $section == rule ]] && ((in_description == 1)); then
continue
fi
feature_validation_set_error "$line_number" "invalid feature syntax" "$FEATURE_LINE_NAME"
@@ -1059,6 +1126,11 @@ feature_parse() {
feature__parsed_scenario_finish "$scenario_outline" "$scenario_line" "$examples_line" feature_tags scenario_tags background_steps background_step_lines scenario_steps scenario_step_lines example_rows example_row_lines || failed=1
fi
+ if ((failed == 0 && rule_seen != 0 && rule_scenario_seen == 0)); then
+ feature_validation_set_error "$rule_line" "Rule must contain at least one Scenario" "Rule: $rule_name"
+ failed=1
+ fi
+
return "$failed"
}
@@ -1232,6 +1304,12 @@ feature_line_parse() {
return 0
fi
+ if [[ $line =~ ^Rule:[[:space:]]*(.*)$ ]]; then
+ FEATURE_LINE_KIND=rule
+ FEATURE_LINE_NAME=${BASH_REMATCH[1]}
+ return 0
+ fi
+
if [[ $line =~ ^Scenario:[[:space:]]*(.*)$ ]]; then
FEATURE_LINE_KIND=scenario
FEATURE_LINE_NAME=${BASH_REMATCH[1]}
diff --git a/src/lib/feature/parse.sh b/src/lib/feature/parse.sh
index 0817f52..c22ec23 100644
--- a/src/lib/feature/parse.sh
+++ b/src/lib/feature/parse.sh
@@ -5,6 +5,13 @@ feature_parse() {
local line_number=0
local section=
local feature_seen=0
+ local feature_background_seen=0
+ local feature_scenario_seen=0
+ local rule_seen=0
+ local rule_background_seen=0
+ local rule_scenario_seen=0
+ local rule_line=0
+ local rule_name=
local scenario_seen=0
local scenario_outline=0
local scenario_line=0
@@ -20,11 +27,14 @@ feature_parse() {
local pending_tags_line=0
local pending_tags_context=
local -a feature_tags=()
+ local -a rule_tags=()
local -a pending_tags=()
local -a parsed_tags=()
local -a scenario_tags=()
local -a background_steps=()
local -a background_step_lines=()
+ local -a feature_background_steps=()
+ local -a feature_background_step_lines=()
local -a scenario_steps=()
local -a scenario_step_lines=()
local -a example_rows=()
@@ -120,8 +130,45 @@ feature_parse() {
fi
continue
;;
+ rule)
+ if ((feature_seen == 0)); then
+ feature_validation_set_error "$line_number" "Rule must appear after Feature" "$(trim "$line")"
+ failed=1
+ break
+ fi
+ if ((scenario_seen != 0)); then
+ feature__parsed_scenario_finish "$scenario_outline" "$scenario_line" "$examples_line" feature_tags scenario_tags background_steps background_step_lines scenario_steps scenario_step_lines example_rows example_row_lines || {
+ failed=1
+ break
+ }
+ scenario_seen=0
+ fi
+ if ((rule_seen != 0 && rule_scenario_seen == 0)); then
+ feature_validation_set_error "$rule_line" "Rule must contain at least one Scenario" "Rule: $rule_name"
+ failed=1
+ break
+ fi
+ if ((rule_seen == 0)); then
+ feature_background_steps=("${background_steps[@]}")
+ feature_background_step_lines=("${background_step_lines[@]}")
+ fi
+ background_steps=("${feature_background_steps[@]}")
+ background_step_lines=("${feature_background_step_lines[@]}")
+ rule_seen=1
+ rule_background_seen=0
+ rule_scenario_seen=0
+ rule_line=$line_number
+ rule_name=$FEATURE_LINE_NAME
+ rule_tags=("${pending_tags[@]}")
+ pending_tags=()
+ pending_tags_line=0
+ pending_tags_context=
+ section=rule
+ in_description=1
+ continue
+ ;;
background)
- if ((feature_seen == 0 || scenario_seen != 0)); then
+ if ((feature_seen == 0)); then
feature_validation_set_error "$line_number" "Background must appear after Feature and before the first Scenario" "$(trim "$line")"
failed=1
break
@@ -131,6 +178,21 @@ feature_parse() {
failed=1
break
fi
+ if ((rule_seen != 0)); then
+ if ((scenario_seen != 0 || rule_scenario_seen != 0 || rule_background_seen != 0)); then
+ feature_validation_set_error "$line_number" "Rule Background must appear before the first Scenario and only once" "$(trim "$line")"
+ failed=1
+ break
+ fi
+ rule_background_seen=1
+ else
+ if ((scenario_seen != 0 || feature_scenario_seen != 0 || feature_background_seen != 0)); then
+ feature_validation_set_error "$line_number" "Background must appear after Feature and before the first Scenario" "$(trim "$line")"
+ failed=1
+ break
+ fi
+ feature_background_seen=1
+ fi
section=background
in_description=0
continue
@@ -156,10 +218,15 @@ feature_parse() {
scenario_line=$line_number
examples_seen=0
examples_line=0
+ if ((rule_seen != 0)); then
+ rule_scenario_seen=1
+ else
+ feature_scenario_seen=1
+ fi
section=scenario
in_description=0
FEATURE_SCENARIO_NAME=$FEATURE_LINE_NAME
- scenario_tags=("${pending_tags[@]}")
+ scenario_tags=("${rule_tags[@]}" "${pending_tags[@]}")
pending_tags=()
pending_tags_line=0
pending_tags_context=
@@ -220,7 +287,7 @@ feature_parse() {
continue
;;
other)
- if [[ $section == feature && $in_description == 1 ]]; then
+ if [[ $section == feature || $section == rule ]] && ((in_description == 1)); then
continue
fi
feature_validation_set_error "$line_number" "invalid feature syntax" "$FEATURE_LINE_NAME"
@@ -245,6 +312,11 @@ feature_parse() {
feature__parsed_scenario_finish "$scenario_outline" "$scenario_line" "$examples_line" feature_tags scenario_tags background_steps background_step_lines scenario_steps scenario_step_lines example_rows example_row_lines || failed=1
fi
+ if ((failed == 0 && rule_seen != 0 && rule_scenario_seen == 0)); then
+ feature_validation_set_error "$rule_line" "Rule must contain at least one Scenario" "Rule: $rule_name"
+ failed=1
+ fi
+
return "$failed"
}
diff --git a/src/lib/feature/syntax.sh b/src/lib/feature/syntax.sh
index ea7568e..984584f 100644
--- a/src/lib/feature/syntax.sh
+++ b/src/lib/feature/syntax.sh
@@ -32,6 +32,12 @@ feature_line_parse() {
return 0
fi
+ if [[ $line =~ ^Rule:[[:space:]]*(.*)$ ]]; then
+ FEATURE_LINE_KIND=rule
+ FEATURE_LINE_NAME=${BASH_REMATCH[1]}
+ return 0
+ fi
+
if [[ $line =~ ^Scenario:[[:space:]]*(.*)$ ]]; then
FEATURE_LINE_KIND=scenario
FEATURE_LINE_NAME=${BASH_REMATCH[1]}
diff --git a/test/lib/feature/parse.bats b/test/lib/feature/parse.bats
index 21b1ea3..f15e197 100644
--- a/test/lib/feature/parse.bats
+++ b/test/lib/feature/parse.bats
@@ -212,3 +212,83 @@ EOF
[ "$status" -eq 1 ]
[ "$FEATURE_VALIDATION_MESSAGE" = "Examples must appear once after a Scenario Outline" ]
}
+
+@test "feature_parse applies Rule tags and Background only within its Rule" {
+ write_file test.feature <<'EOF'
+@feature
+Feature: Rules
+
+Background:
+ Given feature setup
+
+@first-rule
+Rule: First rule
+ A useful description
+
+ Background:
+ Given first rule setup
+
+ @first-scenario
+ Scenario: First
+ Then first result
+
+@second-rule
+Rule: Second rule
+
+ Scenario: Second
+ Then second result
+EOF
+
+ feature_parse "$TEST_ROOT/test.feature"
+
+ [ "${FEATURE_PARSED_SCENARIO_NAMES[*]}" = "First Second" ]
+ [ "${FEATURE_PARSED_SCENARIO_TAGS[0]}" = "@feature @first-rule @first-scenario" ]
+ [ "${FEATURE_PARSED_SCENARIO_TAGS[1]}" = "@feature @second-rule" ]
+ [ "${FEATURE_PARSED_SCENARIO_STEP_COUNTS[*]}" = "3 2" ]
+ [ "${FEATURE_PARSED_STEPS[0]}" = $'Given\tfeature setup' ]
+ [ "${FEATURE_PARSED_STEPS[1]}" = $'Given\tfirst rule setup' ]
+ [ "${FEATURE_PARSED_STEPS[2]}" = $'Then\tfirst result' ]
+ [ "${FEATURE_PARSED_STEPS[3]}" = $'Given\tfeature setup' ]
+ [ "${FEATURE_PARSED_STEPS[4]}" = $'Then\tsecond result' ]
+}
+
+@test "feature_parse rejects a Rule without scenarios" {
+ write_file test.feature <<'EOF'
+Feature: Rules
+
+Rule: Empty rule
+ A description without scenarios
+EOF
+
+ if feature_parse "$TEST_ROOT/test.feature"; then
+ status=0
+ else
+ status=$?
+ fi
+
+ [ "$status" -eq 1 ]
+ [ "$FEATURE_VALIDATION_MESSAGE" = "Rule must contain at least one Scenario" ]
+}
+
+@test "feature_parse rejects a second Background in one Rule" {
+ write_file test.feature <<'EOF'
+Feature: Rules
+
+Rule: One rule
+ Background:
+ Given first setup
+ Background:
+ Given second setup
+ Scenario: Example
+ Then result
+EOF
+
+ if feature_parse "$TEST_ROOT/test.feature"; then
+ status=0
+ else
+ status=$?
+ fi
+
+ [ "$status" -eq 1 ]
+ [ "$FEATURE_VALIDATION_MESSAGE" = "Rule Background must appear before the first Scenario and only once" ]
+}
diff --git a/test/lib/feature/syntax.bats b/test/lib/feature/syntax.bats
index e097fab..0803b15 100644
--- a/test/lib/feature/syntax.bats
+++ b/test/lib/feature/syntax.bats
@@ -32,6 +32,13 @@ teardown() {
[ "$FEATURE_LINE_NAME" = "Create a file" ]
}
+@test "feature_line_parse recognizes a rule header" {
+ feature_line_parse "Rule: Administrators can manage users"
+
+ [ "$FEATURE_LINE_KIND" = rule ]
+ [ "$FEATURE_LINE_NAME" = "Administrators can manage users" ]
+}
+
@test "feature_line_parse recognizes a scenario outline header" {
feature_line_parse "Scenario Outline: Create a "