diff --git a/README.md b/README.md index d84144b..f0362d6 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,7 @@ Implemented pieces include: - `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 ### Gherkin Feature Support @@ -88,9 +89,9 @@ Implemented pieces include: | `Before`, `After` hooks | Supported | | `BeforeAll`, `AfterAll` hooks | Supported | | Data tables | Supported | +| `Scenario Outline` | Supported | +| `Examples` | Supported | | `Rule` | Unsupported | -| `Scenario Outline` | Unsupported | -| `Examples` | 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 diff --git a/doc/shellkin-feature.5 b/doc/shellkin-feature.5 index bc3c917..036bc1f 100644 --- a/doc/shellkin-feature.5 +++ b/doc/shellkin-feature.5 @@ -18,6 +18,8 @@ Shellkin currently supports these Gherkin keywords: .IP \(bu 2 \f[B]Scenario\f[R] .IP \(bu 2 +\f[B]Scenario Outline\f[R], \f[B]Examples\f[R] +.IP \(bu 2 \f[B]Given\f[R], \f[B]When\f[R], \f[B]Then\f[R] .IP \(bu 2 \f[B]And\f[R], \f[B]But\f[R], \f[B]*\f[R] @@ -53,6 +55,30 @@ in the feature. Background: Given I am in a temp directory .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. +Each Examples row is a separate scenario. +.IP +.EX +Scenario Outline: Creating as + When I register user \(aq\(aq with role \(aq\(aq + Then user \(aq\(aq should have role \(aq\(aq + +Examples: + | name | role | + | Alice | admin | + | Bob | member | +.EE +.PP +Placeholders may appear in the scenario name, steps, doc strings, and +data table cells. +Every placeholder must have a matching Examples column. +Examples headings must be unique, and all rows must have the same number +of cells. +.PP +Shellkin currently supports one Examples block per Scenario Outline. +Tags on Examples blocks are not supported. .SS Steps Supported step keywords are \f[B]Given\f[R], \f[B]When\f[R], \f[B]Then\f[R], \f[B]And\f[R], \f[B]But\f[R], and \f[B]*\f[R]. @@ -147,10 +173,6 @@ Escaped pipe characters in cells are not currently supported. The following common Gherkin constructs are not currently supported: .IP \(bu 2 \f[B]Rule\f[R] -.IP \(bu 2 -\f[B]Scenario Outline\f[R] -.IP \(bu 2 -\f[B]Examples\f[R] .SH EXAMPLE .IP .EX diff --git a/doc/shellkin-feature.md b/doc/shellkin-feature.md index c24028d..951e3a0 100644 --- a/doc/shellkin-feature.md +++ b/doc/shellkin-feature.md @@ -24,6 +24,7 @@ Shellkin currently supports these Gherkin keywords: - **Feature** - **Background** - **Scenario** +- **Scenario Outline**, **Examples** - **Given**, **When**, **Then** - **And**, **But**, **\*** - data tables @@ -72,6 +73,30 @@ Background: Given I am in a temp directory ``` +Scenario Outline +-------------------------------------------------- + +Use **Scenario Outline:** with one **Examples:** table to run the same scenario +with several values. Each Examples row is a separate scenario. + +```gherkin +Scenario Outline: Creating as + When I register user '' with role '' + Then user '' should have role '' + +Examples: + | name | role | + | Alice | admin | + | Bob | member | +``` + +Placeholders may appear in the scenario name, steps, doc strings, and data +table cells. Every placeholder must have a matching Examples column. Examples +headings must be unique, and all rows must have the same number of cells. + +Shellkin currently supports one Examples block per Scenario Outline. Tags on +Examples blocks are not supported. + Steps -------------------------------------------------- @@ -177,8 +202,6 @@ UNSUPPORTED CONSTRUCTS The following common Gherkin constructs are not currently supported: - **Rule** -- **Scenario Outline** -- **Examples** EXAMPLE ================================================== diff --git a/features/fixtures/scenario_outlines/scenario_outlines.feature b/features/fixtures/scenario_outlines/scenario_outlines.feature new file mode 100644 index 0000000..0e8a8a2 --- /dev/null +++ b/features/fixtures/scenario_outlines/scenario_outlines.feature @@ -0,0 +1,14 @@ +Feature: scenario outlines + Run the same scenario with several examples + +Background: + Given the user registry is empty + +Scenario Outline: Creating as + When I register user '' with role '' + Then user '' should have role '' + +Examples: + | name | role | + | Alice | admin | + | Bob | member | diff --git a/features/fixtures/scenario_outlines/step_definitions/core.sh b/features/fixtures/scenario_outlines/step_definitions/core.sh new file mode 100644 index 0000000..8fb540f --- /dev/null +++ b/features/fixtures/scenario_outlines/step_definitions/core.sh @@ -0,0 +1,17 @@ +@Given the user registry is empty + USER_NAMES=() + USER_ROLES=() + +@When I register user '{name}' with role '{role}' + USER_NAMES+=("$name") + USER_ROLES+=("$role") + +@Then user '{name}' should have role '{role}' + for index in "${!USER_NAMES[@]}"; do + if [[ ${USER_NAMES[$index]} == "$name" ]]; then + [[ ${USER_ROLES[$index]} == "$role" ]] + return + fi + done + + fail "user not found: $name" diff --git a/features/test.feature b/features/test.feature index 0e4505c..4198217 100644 --- a/features/test.feature +++ b/features/test.feature @@ -28,6 +28,21 @@ Scenario: Running a feature that uses a data table And the output should include '1 scenario, 0 failing' And the exit code should mean success +Scenario: Running a scenario outline + When I run 'shellkin features/fixtures/scenario_outlines' + Then the output should include 'Feature: scenario outlines' + And the output should include 'Scenario 1: Creating Alice as admin' + And the output should include 'Scenario 2: Creating Bob as member' + And the output should include '2 scenarios, 0 failing' + And the exit code should mean success + +Scenario: Selecting one expanded scenario outline row + When I run 'shellkin features/fixtures/scenario_outlines:2' + Then the output should include 'Scenario 2: Creating Bob as member' + And the output should not include 'Creating Alice as admin' + 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 5683208..802dbbc 100755 --- a/shellkin +++ b/shellkin @@ -820,6 +820,10 @@ feature_parse() { local section= local feature_seen=0 local scenario_seen=0 + local scenario_outline=0 + local scenario_line=0 + local examples_seen=0 + local examples_line=0 local in_description=0 local in_doc_string=0 local failed=0 @@ -837,6 +841,8 @@ feature_parse() { local -a background_step_lines=() local -a scenario_steps=() local -a scenario_step_lines=() + local -a example_rows=() + local -a example_row_lines=() FEATURE_NAME= FEATURE_VALIDATION_LINE= @@ -912,7 +918,10 @@ feature_parse() { pending_tags+=("${parsed_tags[@]}") else if ((scenario_seen != 0)); then - feature__parsed_scenario_add "$FEATURE_SCENARIO_NAME" feature_tags scenario_tags background_steps background_step_lines scenario_steps scenario_step_lines + 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 pending_tags+=("${parsed_tags[@]}") @@ -940,16 +949,27 @@ feature_parse() { in_description=0 continue ;; - scenario) + scenario | scenario_outline) if ((feature_seen == 0)); then feature_validation_set_error "$line_number" "Scenario must appear after Feature" "$(trim "$line")" failed=1 break fi if ((scenario_seen != 0)); then - feature__parsed_scenario_add "$FEATURE_SCENARIO_NAME" feature_tags scenario_tags background_steps background_step_lines scenario_steps scenario_step_lines + 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 + } fi scenario_seen=1 + if [[ $FEATURE_LINE_KIND == scenario_outline ]]; then + scenario_outline=1 + else + scenario_outline=0 + fi + scenario_line=$line_number + examples_seen=0 + examples_line=0 section=scenario in_description=0 FEATURE_SCENARIO_NAME=$FEATURE_LINE_NAME @@ -959,6 +979,20 @@ feature_parse() { pending_tags_context= scenario_steps=() scenario_step_lines=() + example_rows=() + example_row_lines=() + continue + ;; + examples) + if ((scenario_seen == 0 || scenario_outline == 0 || examples_seen != 0)); then + feature_validation_set_error "$line_number" "Examples must appear once after a Scenario Outline" "$(trim "$line")" + failed=1 + break + fi + examples_seen=1 + examples_line=$line_number + section=examples + in_description=0 continue ;; step) @@ -981,7 +1015,10 @@ feature_parse() { ;; table_row) in_description=0 - if feature_table_row_apply "$FEATURE_TABLE_TEXT" "$section" background_steps scenario_steps; then + if [[ $section == examples ]]; then + example_rows+=("$FEATURE_TABLE_TEXT") + example_row_lines+=("$line_number") + elif feature_table_row_apply "$FEATURE_TABLE_TEXT" "$section" background_steps scenario_steps; then : else feature_validation_set_error "$line_number" "data table must follow a step" "$(trim "$line")" @@ -1019,12 +1056,126 @@ feature_parse() { fi if ((failed == 0 && scenario_seen != 0)); then - feature__parsed_scenario_add "$FEATURE_SCENARIO_NAME" feature_tags scenario_tags background_steps background_step_lines scenario_steps scenario_step_lines + 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 return "$failed" } +feature__parsed_scenario_finish() { + local is_outline=$1 + local scenario_line=$2 + local examples_line=$3 + local feature_tags_name=$4 + local scenario_tags_name=$5 + local background_steps_name=$6 + local background_lines_name=$7 + local scenario_steps_name=$8 + local scenario_lines_name=$9 + local example_rows_name=${10} + local example_lines_name=${11} + + if ((is_outline == 0)); then + feature__parsed_scenario_add "$FEATURE_SCENARIO_NAME" "$feature_tags_name" "$scenario_tags_name" "$background_steps_name" "$background_lines_name" "$scenario_steps_name" "$scenario_lines_name" + return + fi + + feature__parsed_outline_add "$scenario_line" "$examples_line" "$feature_tags_name" "$scenario_tags_name" "$background_steps_name" "$background_lines_name" "$scenario_steps_name" "$scenario_lines_name" "$example_rows_name" "$example_lines_name" +} + +feature__parsed_outline_add() { + local scenario_line=$1 + local examples_line=$2 + local feature_tags_name=$3 + local scenario_tags_name=$4 + local background_steps_name=$5 + local background_lines_name=$6 + # shellcheck disable=SC2178 # nameref to an array supplied by name + local -n scenario_steps_ref=$7 + local scenario_lines_name=$8 + local -n scenario_lines_ref=$8 + local -n example_rows_ref=$9 + local -n example_lines_ref=${10} + local row_index + local step_index + local heading + local previous_heading + local expanded_name + local -a headings=() + local -a cells=() + local -a expanded_steps=() + local -a seen_headings=() + + if ((examples_line == 0)); then + feature_validation_set_error "$scenario_line" "Scenario Outline must have one Examples block" "Scenario Outline: $FEATURE_SCENARIO_NAME" + return 1 + fi + if ((${#example_rows_ref[@]} < 2)); then + feature_validation_set_error "$examples_line" "Examples must have a header and at least one data row" "Examples:" + return 1 + fi + + feature_table_row_parse "${example_rows_ref[0]}" headings + for heading in "${headings[@]}"; do + if [[ -z $heading ]]; then + feature_validation_set_error "${example_lines_ref[0]}" "Examples headings must not be empty" "${example_rows_ref[0]}" + return 1 + fi + for previous_heading in "${seen_headings[@]}"; do + if [[ $heading == "$previous_heading" ]]; then + feature_validation_set_error "${example_lines_ref[0]}" "Examples headings must be unique" "${example_rows_ref[0]}" + return 1 + fi + done + seen_headings+=("$heading") + done + + for ((row_index = 1; row_index < ${#example_rows_ref[@]}; row_index++)); do + feature_table_row_parse "${example_rows_ref[$row_index]}" cells + if ((${#cells[@]} != ${#headings[@]})); then + feature_validation_set_error "${example_lines_ref[$row_index]}" "Examples rows must have the same number of cells as the header" "${example_rows_ref[$row_index]}" + return 1 + fi + + feature__outline_substitute "$FEATURE_SCENARIO_NAME" headings cells || { + feature_validation_set_error "$scenario_line" "no Examples column for placeholder" "$FEATURE_OUTLINE_MISSING_PLACEHOLDER" + return 1 + } + expanded_name=$FEATURE_OUTLINE_SUBSTITUTED + expanded_steps=() + for step_index in "${!scenario_steps_ref[@]}"; do + feature__outline_substitute "${scenario_steps_ref[$step_index]}" headings cells || { + feature_validation_set_error "${scenario_lines_ref[$step_index]}" "no Examples column for placeholder" "$FEATURE_OUTLINE_MISSING_PLACEHOLDER" + return 1 + } + expanded_steps+=("$FEATURE_OUTLINE_SUBSTITUTED") + done + + feature__parsed_scenario_add "$expanded_name" "$feature_tags_name" "$scenario_tags_name" "$background_steps_name" "$background_lines_name" expanded_steps "$scenario_lines_name" + done +} + +feature__outline_substitute() { + local value=$1 + local -n headings_ref=$2 + local -n cells_ref=$3 + local placeholder_regex='<[^>]+>' + local pattern + local index + + for index in "${!headings_ref[@]}"; do + pattern="<${headings_ref[$index]}>" + value=${value//"$pattern"/"${cells_ref[$index]}"} + done + + FEATURE_OUTLINE_SUBSTITUTED=$value + FEATURE_OUTLINE_MISSING_PLACEHOLDER= + if [[ $value =~ $placeholder_regex ]]; then + FEATURE_OUTLINE_MISSING_PLACEHOLDER=${BASH_REMATCH[0]} + return 1 + fi +} + feature__parsed_scenario_add() { local scenario_name=$1 local feature_tags_name=$2 @@ -1087,6 +1238,17 @@ feature_line_parse() { return 0 fi + if [[ $line =~ ^Scenario[[:space:]]+Outline:[[:space:]]*(.*)$ ]]; then + FEATURE_LINE_KIND=scenario_outline + FEATURE_LINE_NAME=${BASH_REMATCH[1]} + return 0 + fi + + if [[ $line =~ ^Examples:[[:space:]]*$ ]]; then + FEATURE_LINE_KIND=examples + return 0 + fi + if [[ $line == @* ]]; then FEATURE_LINE_KIND=tag FEATURE_TAG_TEXT=$line diff --git a/src/lib/feature/parse.sh b/src/lib/feature/parse.sh index c5e0e51..0817f52 100644 --- a/src/lib/feature/parse.sh +++ b/src/lib/feature/parse.sh @@ -6,6 +6,10 @@ feature_parse() { local section= local feature_seen=0 local scenario_seen=0 + local scenario_outline=0 + local scenario_line=0 + local examples_seen=0 + local examples_line=0 local in_description=0 local in_doc_string=0 local failed=0 @@ -23,6 +27,8 @@ feature_parse() { local -a background_step_lines=() local -a scenario_steps=() local -a scenario_step_lines=() + local -a example_rows=() + local -a example_row_lines=() FEATURE_NAME= FEATURE_VALIDATION_LINE= @@ -98,7 +104,10 @@ feature_parse() { pending_tags+=("${parsed_tags[@]}") else if ((scenario_seen != 0)); then - feature__parsed_scenario_add "$FEATURE_SCENARIO_NAME" feature_tags scenario_tags background_steps background_step_lines scenario_steps scenario_step_lines + 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 pending_tags+=("${parsed_tags[@]}") @@ -126,16 +135,27 @@ feature_parse() { in_description=0 continue ;; - scenario) + scenario | scenario_outline) if ((feature_seen == 0)); then feature_validation_set_error "$line_number" "Scenario must appear after Feature" "$(trim "$line")" failed=1 break fi if ((scenario_seen != 0)); then - feature__parsed_scenario_add "$FEATURE_SCENARIO_NAME" feature_tags scenario_tags background_steps background_step_lines scenario_steps scenario_step_lines + 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 + } fi scenario_seen=1 + if [[ $FEATURE_LINE_KIND == scenario_outline ]]; then + scenario_outline=1 + else + scenario_outline=0 + fi + scenario_line=$line_number + examples_seen=0 + examples_line=0 section=scenario in_description=0 FEATURE_SCENARIO_NAME=$FEATURE_LINE_NAME @@ -145,6 +165,20 @@ feature_parse() { pending_tags_context= scenario_steps=() scenario_step_lines=() + example_rows=() + example_row_lines=() + continue + ;; + examples) + if ((scenario_seen == 0 || scenario_outline == 0 || examples_seen != 0)); then + feature_validation_set_error "$line_number" "Examples must appear once after a Scenario Outline" "$(trim "$line")" + failed=1 + break + fi + examples_seen=1 + examples_line=$line_number + section=examples + in_description=0 continue ;; step) @@ -167,7 +201,10 @@ feature_parse() { ;; table_row) in_description=0 - if feature_table_row_apply "$FEATURE_TABLE_TEXT" "$section" background_steps scenario_steps; then + if [[ $section == examples ]]; then + example_rows+=("$FEATURE_TABLE_TEXT") + example_row_lines+=("$line_number") + elif feature_table_row_apply "$FEATURE_TABLE_TEXT" "$section" background_steps scenario_steps; then : else feature_validation_set_error "$line_number" "data table must follow a step" "$(trim "$line")" @@ -205,12 +242,126 @@ feature_parse() { fi if ((failed == 0 && scenario_seen != 0)); then - feature__parsed_scenario_add "$FEATURE_SCENARIO_NAME" feature_tags scenario_tags background_steps background_step_lines scenario_steps scenario_step_lines + 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 return "$failed" } +feature__parsed_scenario_finish() { + local is_outline=$1 + local scenario_line=$2 + local examples_line=$3 + local feature_tags_name=$4 + local scenario_tags_name=$5 + local background_steps_name=$6 + local background_lines_name=$7 + local scenario_steps_name=$8 + local scenario_lines_name=$9 + local example_rows_name=${10} + local example_lines_name=${11} + + if ((is_outline == 0)); then + feature__parsed_scenario_add "$FEATURE_SCENARIO_NAME" "$feature_tags_name" "$scenario_tags_name" "$background_steps_name" "$background_lines_name" "$scenario_steps_name" "$scenario_lines_name" + return + fi + + feature__parsed_outline_add "$scenario_line" "$examples_line" "$feature_tags_name" "$scenario_tags_name" "$background_steps_name" "$background_lines_name" "$scenario_steps_name" "$scenario_lines_name" "$example_rows_name" "$example_lines_name" +} + +feature__parsed_outline_add() { + local scenario_line=$1 + local examples_line=$2 + local feature_tags_name=$3 + local scenario_tags_name=$4 + local background_steps_name=$5 + local background_lines_name=$6 + # shellcheck disable=SC2178 # nameref to an array supplied by name + local -n scenario_steps_ref=$7 + local scenario_lines_name=$8 + local -n scenario_lines_ref=$8 + local -n example_rows_ref=$9 + local -n example_lines_ref=${10} + local row_index + local step_index + local heading + local previous_heading + local expanded_name + local -a headings=() + local -a cells=() + local -a expanded_steps=() + local -a seen_headings=() + + if ((examples_line == 0)); then + feature_validation_set_error "$scenario_line" "Scenario Outline must have one Examples block" "Scenario Outline: $FEATURE_SCENARIO_NAME" + return 1 + fi + if ((${#example_rows_ref[@]} < 2)); then + feature_validation_set_error "$examples_line" "Examples must have a header and at least one data row" "Examples:" + return 1 + fi + + feature_table_row_parse "${example_rows_ref[0]}" headings + for heading in "${headings[@]}"; do + if [[ -z $heading ]]; then + feature_validation_set_error "${example_lines_ref[0]}" "Examples headings must not be empty" "${example_rows_ref[0]}" + return 1 + fi + for previous_heading in "${seen_headings[@]}"; do + if [[ $heading == "$previous_heading" ]]; then + feature_validation_set_error "${example_lines_ref[0]}" "Examples headings must be unique" "${example_rows_ref[0]}" + return 1 + fi + done + seen_headings+=("$heading") + done + + for ((row_index = 1; row_index < ${#example_rows_ref[@]}; row_index++)); do + feature_table_row_parse "${example_rows_ref[$row_index]}" cells + if ((${#cells[@]} != ${#headings[@]})); then + feature_validation_set_error "${example_lines_ref[$row_index]}" "Examples rows must have the same number of cells as the header" "${example_rows_ref[$row_index]}" + return 1 + fi + + feature__outline_substitute "$FEATURE_SCENARIO_NAME" headings cells || { + feature_validation_set_error "$scenario_line" "no Examples column for placeholder" "$FEATURE_OUTLINE_MISSING_PLACEHOLDER" + return 1 + } + expanded_name=$FEATURE_OUTLINE_SUBSTITUTED + expanded_steps=() + for step_index in "${!scenario_steps_ref[@]}"; do + feature__outline_substitute "${scenario_steps_ref[$step_index]}" headings cells || { + feature_validation_set_error "${scenario_lines_ref[$step_index]}" "no Examples column for placeholder" "$FEATURE_OUTLINE_MISSING_PLACEHOLDER" + return 1 + } + expanded_steps+=("$FEATURE_OUTLINE_SUBSTITUTED") + done + + feature__parsed_scenario_add "$expanded_name" "$feature_tags_name" "$scenario_tags_name" "$background_steps_name" "$background_lines_name" expanded_steps "$scenario_lines_name" + done +} + +feature__outline_substitute() { + local value=$1 + local -n headings_ref=$2 + local -n cells_ref=$3 + local placeholder_regex='<[^>]+>' + local pattern + local index + + for index in "${!headings_ref[@]}"; do + pattern="<${headings_ref[$index]}>" + value=${value//"$pattern"/"${cells_ref[$index]}"} + done + + FEATURE_OUTLINE_SUBSTITUTED=$value + FEATURE_OUTLINE_MISSING_PLACEHOLDER= + if [[ $value =~ $placeholder_regex ]]; then + FEATURE_OUTLINE_MISSING_PLACEHOLDER=${BASH_REMATCH[0]} + return 1 + fi +} + feature__parsed_scenario_add() { local scenario_name=$1 local feature_tags_name=$2 diff --git a/src/lib/feature/syntax.sh b/src/lib/feature/syntax.sh index cc816af..ea7568e 100644 --- a/src/lib/feature/syntax.sh +++ b/src/lib/feature/syntax.sh @@ -38,6 +38,17 @@ feature_line_parse() { return 0 fi + if [[ $line =~ ^Scenario[[:space:]]+Outline:[[:space:]]*(.*)$ ]]; then + FEATURE_LINE_KIND=scenario_outline + FEATURE_LINE_NAME=${BASH_REMATCH[1]} + return 0 + fi + + if [[ $line =~ ^Examples:[[:space:]]*$ ]]; then + FEATURE_LINE_KIND=examples + return 0 + fi + if [[ $line == @* ]]; then FEATURE_LINE_KIND=tag FEATURE_TAG_TEXT=$line diff --git a/test/lib/feature/parse.bats b/test/lib/feature/parse.bats index 0f0fec3..21b1ea3 100644 --- a/test/lib/feature/parse.bats +++ b/test/lib/feature/parse.bats @@ -62,3 +62,153 @@ EOF [[ ${FEATURE_PARSED_STEPS[0]} == *$'\x1e| name | role |'* ]] [[ ${FEATURE_PARSED_STEPS[1]} == *$'\thello\nworld' ]] } + +@test "feature_parse expands an outline into runnable scenarios" { + write_file test.feature <<'EOF' +@users +Feature: Outlines + +Background: + Given shared setup + +@roles +Scenario Outline: Creating + Given user '' exists + Then the user record should match + """ + : + """ + And these permissions exist + | user | permission | + | | | + +Examples: + | name | role | + | Alice | admin | + | Bob | member | +EOF + + feature_parse "$TEST_ROOT/test.feature" + + [ "${FEATURE_PARSED_SCENARIO_NAMES[*]}" = "Creating Alice Creating Bob" ] + [ "${FEATURE_PARSED_SCENARIO_TAGS[0]}" = "@users @roles" ] + [ "${FEATURE_PARSED_SCENARIO_TAGS[1]}" = "@users @roles" ] + [ "${FEATURE_PARSED_SCENARIO_STEP_COUNTS[*]}" = "4 4" ] + [ "${FEATURE_PARSED_STEPS[0]}" = $'Given\tshared setup' ] + [ "${FEATURE_PARSED_STEPS[1]}" = $'Given\tuser \'Alice\' exists' ] + [[ ${FEATURE_PARSED_STEPS[2]} == *$'\tAlice:admin' ]] + [[ ${FEATURE_PARSED_STEPS[3]} == *'| Alice | admin |'* ]] + [ "${FEATURE_PARSED_STEPS[4]}" = $'Given\tshared setup' ] + [ "${FEATURE_PARSED_STEPS[5]}" = $'Given\tuser \'Bob\' exists' ] +} + +@test "feature_parse rejects an outline without Examples" { + write_file test.feature <<'EOF' +Feature: Outlines + +Scenario Outline: Creating + Given user '' exists +EOF + + if feature_parse "$TEST_ROOT/test.feature"; then + status=0 + else + status=$? + fi + + [ "$status" -eq 1 ] + [ "$FEATURE_VALIDATION_MESSAGE" = "Scenario Outline must have one Examples block" ] +} + +@test "feature_parse rejects missing placeholder columns" { + write_file test.feature <<'EOF' +Feature: Outlines + +Scenario Outline: Creating + Given user '' has role '' + +Examples: + | name | + | Alice | +EOF + + if feature_parse "$TEST_ROOT/test.feature"; then + status=0 + else + status=$? + fi + + [ "$status" -eq 1 ] + [ "$FEATURE_VALIDATION_MESSAGE" = "no Examples column for placeholder" ] + [ "$FEATURE_VALIDATION_CONTEXT" = "" ] +} + +@test "feature_parse rejects duplicate Examples headings" { + write_file test.feature <<'EOF' +Feature: Outlines + +Scenario Outline: Creating + Given user '' exists + +Examples: + | name | name | + | Alice | admin | +EOF + + if feature_parse "$TEST_ROOT/test.feature"; then + status=0 + else + status=$? + fi + + [ "$status" -eq 1 ] + [ "$FEATURE_VALIDATION_MESSAGE" = "Examples headings must be unique" ] +} + +@test "feature_parse rejects uneven Examples rows" { + write_file test.feature <<'EOF' +Feature: Outlines + +Scenario Outline: Creating + Given user '' exists + +Examples: + | name | role | + | Alice | +EOF + + if feature_parse "$TEST_ROOT/test.feature"; then + status=0 + else + status=$? + fi + + [ "$status" -eq 1 ] + [ "$FEATURE_VALIDATION_MESSAGE" = "Examples rows must have the same number of cells as the header" ] +} + +@test "feature_parse rejects a second Examples block" { + write_file test.feature <<'EOF' +Feature: Outlines + +Scenario Outline: Creating + Given user '' exists + +Examples: + | name | + | Alice | + +Examples: + | name | + | Bob | +EOF + + if feature_parse "$TEST_ROOT/test.feature"; then + status=0 + else + status=$? + fi + + [ "$status" -eq 1 ] + [ "$FEATURE_VALIDATION_MESSAGE" = "Examples must appear once after a Scenario Outline" ] +} diff --git a/test/lib/feature/syntax.bats b/test/lib/feature/syntax.bats index 741b556..e097fab 100644 --- a/test/lib/feature/syntax.bats +++ b/test/lib/feature/syntax.bats @@ -32,6 +32,19 @@ teardown() { [ "$FEATURE_LINE_NAME" = "Create a file" ] } +@test "feature_line_parse recognizes a scenario outline header" { + feature_line_parse "Scenario Outline: Create a " + + [ "$FEATURE_LINE_KIND" = scenario_outline ] + [ "$FEATURE_LINE_NAME" = "Create a " ] +} + +@test "feature_line_parse recognizes an examples header" { + feature_line_parse "Examples:" + + [ "$FEATURE_LINE_KIND" = examples ] +} + @test "feature_line_parse recognizes a step line" { feature_line_parse "When I run 'touch somefile'"