From aa282018d2a233df5137a16c4ea90b28f0b21ed2 Mon Sep 17 00:00:00 2001 From: Danny Ben Shitrit Date: Sat, 11 Jul 2026 10:18:47 +0300 Subject: [PATCH] - Add support for data tables --- README.md | 5 +- doc/shellkin-feature.5 | 21 ++- doc/shellkin-feature.md | 19 ++- doc/shellkin-stepdefs.5 | 18 +++ doc/shellkin-stepdefs.md | 16 ++ .../fixtures/data_tables/data_tables.feature | 10 ++ .../data_tables/step_definitions/core.sh | 20 +++ features/test.feature | 7 + shellkin | 149 +++++++++++++++++- src/lib/feature/core.sh | 33 ++++ src/lib/feature/syntax.sh | 7 + src/lib/feature/table.sh | 102 ++++++++++++ test/commands/validate.bats | 20 +++ test/lib/feature/core.bats | 32 +++- test/lib/feature/syntax.bats | 7 + 15 files changed, 460 insertions(+), 6 deletions(-) create mode 100644 features/fixtures/data_tables/data_tables.feature create mode 100644 features/fixtures/data_tables/step_definitions/core.sh create mode 100644 src/lib/feature/table.sh diff --git a/README.md b/README.md index bf719bc..d84144b 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,7 @@ Implemented pieces include: - 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 - colored terminal output and scenario summary ### Gherkin Feature Support @@ -86,10 +87,10 @@ Implemented pieces include: | Tags (`@tag`) | Supported | | `Before`, `After` hooks | Supported | | `BeforeAll`, `AfterAll` hooks | Supported | +| Data tables | Supported | | `Rule` | Unsupported | | `Scenario Outline` | Unsupported | | `Examples` | Unsupported | -| Data tables | 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 @@ -373,6 +374,8 @@ Shellkin exposes these variables to step definition bodies: | `LAST_STDOUT` | Standard output captured by the most recent `run` call | | `LAST_STDERR` | Standard error captured by the most recent `run` call | | `DOC_STRING` | Doc string attached to the current step, if any | +| `TABLE_HEADER` | Header cells from the current step's data table array | +| `TABLE_ROWS` | Tab-separated data rows from the current step's table | Example: diff --git a/doc/shellkin-feature.5 b/doc/shellkin-feature.5 index 2be5dd0..bc3c917 100644 --- a/doc/shellkin-feature.5 +++ b/doc/shellkin-feature.5 @@ -21,6 +21,8 @@ Shellkin currently supports these Gherkin keywords: \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] +.IP \(bu 2 +data tables .SH FORMAT .SS Feature Header Each feature file starts with a \f[B]Feature:\f[R] header. @@ -124,6 +126,23 @@ Scenario: Match multiline output .PP The doc string content is exposed to the matching step definition through the \f[B]DOC_STRING\f[R] environment variable. +.SH DATA TABLES +A data table may follow a step. +Its first row is exposed to the matching step definition as the +\f[B]TABLE_HEADER\f[R] array. +Each remaining row is stored as a tab\-separated value in the +\f[B]TABLE_ROWS\f[R] array. +.IP +.EX +Given these users exist + | name | role | + | Alice | admin | + | Bob | user | +.EE +.PP +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 @@ -132,8 +151,6 @@ The following common Gherkin constructs are not currently supported: \f[B]Scenario Outline\f[R] .IP \(bu 2 \f[B]Examples\f[R] -.IP \(bu 2 -data tables .SH EXAMPLE .IP .EX diff --git a/doc/shellkin-feature.md b/doc/shellkin-feature.md index 86f0174..c24028d 100644 --- a/doc/shellkin-feature.md +++ b/doc/shellkin-feature.md @@ -26,6 +26,7 @@ Shellkin currently supports these Gherkin keywords: - **Scenario** - **Given**, **When**, **Then** - **And**, **But**, **\*** +- data tables FORMAT ================================================== @@ -153,6 +154,23 @@ Scenario: Match multiline output The doc string content is exposed to the matching step definition through the **DOC_STRING** environment variable. +DATA TABLES +================================================== + +A data table may follow a step. Its first row is exposed to the matching step +definition as the **TABLE_HEADER** array. Each remaining row is stored as a +tab-separated value in the **TABLE_ROWS** array. + +```gherkin +Given these users exist + | name | role | + | Alice | admin | + | Bob | user | +``` + +All rows must contain the same number of cells. Cell values are trimmed. +Escaped pipe characters in cells are not currently supported. + UNSUPPORTED CONSTRUCTS ================================================== @@ -161,7 +179,6 @@ The following common Gherkin constructs are not currently supported: - **Rule** - **Scenario Outline** - **Examples** -- data tables EXAMPLE ================================================== diff --git a/doc/shellkin-stepdefs.5 b/doc/shellkin-stepdefs.5 index 7eddba9..fe6d72b 100644 --- a/doc/shellkin-stepdefs.5 +++ b/doc/shellkin-stepdefs.5 @@ -192,6 +192,12 @@ Shellkin exposes these variables to step definition bodies: \f[B]run\f[R] call .IP \(bu 2 \f[B]DOC_STRING\f[R] \- doc string attached to the current step, if any +.IP \(bu 2 +\f[B]TABLE_HEADER\f[R] \- header cells from the current step\(aqs data +table array +.IP \(bu 2 +\f[B]TABLE_ROWS\f[R] \- tab\-separated data rows from the current +step\(aqs data table .SH DOC STRINGS If a feature step is followed by a Gherkin doc string, its content is exposed to the step body through \f[B]DOC_STRING\f[R]. @@ -208,6 +214,18 @@ Then the output should match \(atThen the output should match \f[B][[\f[R] \(dq$LAST_STDOUT\(dq == \(dq$DOC_STRING\(dq \f[B]]]\f[R] .EE +.SH DATA TABLES +Data tables are exposed through the \f[B]TABLE_HEADER\f[R] and +\f[B]TABLE_ROWS\f[R] arrays. +Split each row on tabs to access its cells. +.IP +.EX +\(atGiven these users exist +\f[B]for\f[R] row \f[B]in\f[R] \(dq${TABLE_ROWS[\(at]}\(dq\f[B];\f[R] \f[B]do\f[R] + IFS=$\(aq\(rst\(aq read \-r name role <<<\(dq$row\(dq + create_user \(dq$name\(dq \(dq$role\(dq +\f[B]done\f[R] +.EE .SH EXAMPLE .IP .EX diff --git a/doc/shellkin-stepdefs.md b/doc/shellkin-stepdefs.md index 6e0a629..c18aa83 100644 --- a/doc/shellkin-stepdefs.md +++ b/doc/shellkin-stepdefs.md @@ -204,6 +204,8 @@ Shellkin exposes these variables to step definition bodies: - **LAST_STDOUT** - standard output captured by the most recent **run** call - **LAST_STDERR** - standard error captured by the most recent **run** call - **DOC_STRING** - doc string attached to the current step, if any +- **TABLE_HEADER** - header cells from the current step's data table array +- **TABLE_ROWS** - tab-separated data rows from the current step's data table DOC STRINGS ================================================== @@ -224,6 +226,20 @@ Then the output should match [[ "$LAST_STDOUT" == "$DOC_STRING" ]] ``` +DATA TABLES +================================================== + +Data tables are exposed through the **TABLE_HEADER** and **TABLE_ROWS** arrays. +Split each row on tabs to access its cells. + +```bash +@Given these users exist +for row in "${TABLE_ROWS[@]}"; do + IFS=$'\t' read -r name role <<<"$row" + create_user "$name" "$role" +done +``` + EXAMPLE ================================================== diff --git a/features/fixtures/data_tables/data_tables.feature b/features/fixtures/data_tables/data_tables.feature new file mode 100644 index 0000000..b17ed39 --- /dev/null +++ b/features/fixtures/data_tables/data_tables.feature @@ -0,0 +1,10 @@ +Feature: data tables + Pass structured examples to step definitions + +Scenario: Creating several users + Given these users exist + | name | role | + | Alice | admin | + | Bob | member | + Then user 'Alice' should have role 'admin' + And user 'Bob' should have role 'member' diff --git a/features/fixtures/data_tables/step_definitions/core.sh b/features/fixtures/data_tables/step_definitions/core.sh new file mode 100644 index 0000000..d99fb94 --- /dev/null +++ b/features/fixtures/data_tables/step_definitions/core.sh @@ -0,0 +1,20 @@ +@Given these users exist + USER_NAMES=() + USER_ROLES=() + [[ ${TABLE_HEADER[*]} == "name role" ]] || fail "expected name and role columns" + + for row in "${TABLE_ROWS[@]}"; do + IFS=$'\t' read -r name role <<<"$row" + USER_NAMES+=("$name") + USER_ROLES+=("$role") + done + +@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 f69ad23..0e4505c 100644 --- a/features/test.feature +++ b/features/test.feature @@ -21,6 +21,13 @@ Scenario: Running hooks from step definition files And the output should include '2 scenarios, 0 failing' And the exit code should mean success +Scenario: Running a feature that uses a data table + When I run 'shellkin features/fixtures/data_tables' + Then the output should include 'Feature: data tables' + And the output should include 'Scenario 1: Creating several users' + 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 18261e9..4253f33 100755 --- a/shellkin +++ b/shellkin @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# This script was generated by bashly 1.4.0.rc2 (https://bashly.dev) +# This script was generated by bashly 1.4.0 (https://bashly.dev) # Modifying it manually is not recommended # :wrapper.bash3_bouncer @@ -558,6 +558,15 @@ feature_run() { esac continue ;; + table_row) + in_description=0 + if feature_table_row_apply "$FEATURE_TABLE_TEXT" "$section" background_steps scenario_steps; then + : + else + failed=1 + fi + continue + ;; doc_string_fence) doc_string_indent=${line%%\"\"\"*} in_doc_string=1 @@ -607,6 +616,8 @@ feature__recorded_step_parse() { local recorded=$1 local remainder + recorded=${recorded%%$'\x1e'*} + FEATURE_RECORDED_STEP_KEYWORD=${recorded%%$'\t'*} remainder=${recorded#*$'\t'} FEATURE_RECORDED_STEP_DOC_STRING= @@ -624,6 +635,7 @@ feature_recorded_step_validate() { local previous_type=$2 local resolved_type + feature_table_validate "$recorded" || return 1 feature__recorded_step_parse "$recorded" resolved_type=$(feature_step_type_resolve "$previous_type" "$FEATURE_RECORDED_STEP_KEYWORD") || return 1 @@ -645,6 +657,7 @@ feature_recorded_step_run() { if [[ -n $FEATURE_RECORDED_STEP_DOC_STRING ]]; then export DOC_STRING=$FEATURE_RECORDED_STEP_DOC_STRING fi + feature_table_export "$recorded" || return 1 step_run "$resolved_type" "$FEATURE_RECORDED_STEP_TEXT" status=$? @@ -680,6 +693,11 @@ feature_scenario_validate() { for index in "${!background_steps_ref[@]}"; do step=${background_steps_ref[$index]} + if ! feature_table_validate "$step"; then + feature__recorded_step_parse "$step" + feature_validation_set_error "${background_lines_ref[$index]}" "data table rows must have the same number of cells for" "$FEATURE_RECORDED_STEP_KEYWORD $FEATURE_RECORDED_STEP_TEXT" + return 1 + fi if feature_recorded_step_validate "$step" "$FEATURE_PREVIOUS_STEP_TYPE"; then : else @@ -691,6 +709,11 @@ feature_scenario_validate() { for index in "${!scenario_steps_ref[@]}"; do step=${scenario_steps_ref[$index]} + if ! feature_table_validate "$step"; then + feature__recorded_step_parse "$step" + feature_validation_set_error "${scenario_lines_ref[$index]}" "data table rows must have the same number of cells for" "$FEATURE_RECORDED_STEP_KEYWORD $FEATURE_RECORDED_STEP_TEXT" + return 1 + fi if feature_recorded_step_validate "$step" "$FEATURE_PREVIOUS_STEP_TYPE"; then : else @@ -860,6 +883,16 @@ feature_validate() { esac continue ;; + table_row) + in_description=0 + if 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")" + failed=1 + fi + continue + ;; doc_string_fence) doc_string_indent=${line%%\"\"\"*} in_doc_string=1 @@ -1131,6 +1164,7 @@ feature_line_parse() { FEATURE_STEP_TYPE= FEATURE_STEP_TEXT= FEATURE_TAG_TEXT= + FEATURE_TABLE_TEXT= if [[ -z $line ]]; then FEATURE_LINE_KIND=blank @@ -1177,6 +1211,12 @@ feature_line_parse() { return 0 fi + if [[ $line == \|*\| ]]; then + FEATURE_LINE_KIND=table_row + FEATURE_TABLE_TEXT=$line + return 0 + fi + FEATURE_LINE_KIND=other FEATURE_LINE_NAME=$line } @@ -1196,6 +1236,106 @@ feature_step_type_resolve() { esac } +# src/lib/feature/table.sh +feature_table_row_parse() { + local line=$1 + local -n cells_ref=$2 + local content + local cell + local rest + + [[ $line == \|*\| ]] || return 1 + + content=${line#\|} + content=${content%\|} + cells_ref=() + rest=$content + while [[ $rest == *\|* ]]; do + cell=${rest%%\|*} + cells_ref+=("$(trim "$cell")") + rest=${rest#*\|} + done + cells_ref+=("$(trim "$rest")") +} + +feature_table_row_apply() { + local row=$1 + local section=$2 + # shellcheck disable=SC2178 # nameref to an array supplied by name + local -n background_steps_ref=$3 + # shellcheck disable=SC2178 # nameref to an array supplied by name + local -n scenario_steps_ref=$4 + local last_index + local recorded + + case $section in + background) + ((${#background_steps_ref[@]} > 0)) || return 1 + last_index=$((${#background_steps_ref[@]} - 1)) + recorded=${background_steps_ref[last_index]} + background_steps_ref[last_index]="$recorded"$'\x1e'"$row" + ;; + scenario) + ((${#scenario_steps_ref[@]} > 0)) || return 1 + last_index=$((${#scenario_steps_ref[@]} - 1)) + recorded=${scenario_steps_ref[last_index]} + scenario_steps_ref[last_index]="$recorded"$'\x1e'"$row" + ;; + *) + return 1 + ;; + esac +} + +feature_table_validate() { + local recorded=$1 + local table_data + local row + local width=0 + local -a cells=() + + [[ $recorded == *$'\x1e'* ]] || return 0 + table_data=${recorded#*$'\x1e'} + + while IFS= read -r row; do + feature_table_row_parse "$row" cells || return 1 + if ((width == 0)); then + width=${#cells[@]} + ((width > 0)) || return 1 + elif ((${#cells[@]} != width)); then + return 1 + fi + done <<<"${table_data//$'\x1e'/$'\n'}" +} + +feature_table_export() { + local recorded=$1 + local table_data + local row + local index=0 + local -a cells=() + + TABLE_HEADER=() + TABLE_ROWS=() + [[ $recorded == *$'\x1e'* ]] || return 0 + feature_table_validate "$recorded" || return 1 + + table_data=${recorded#*$'\x1e'} + while IFS= read -r row; do + feature_table_row_parse "$row" cells || return 1 + if ((index == 0)); then + # shellcheck disable=SC2034 # consumed by the evaluated step definition + TABLE_HEADER=("${cells[@]}") + else + TABLE_ROWS+=("$( + IFS=$'\t' + printf '%s' "${cells[*]}" + )") + fi + ((index += 1)) + done <<<"${table_data//$'\x1e'/$'\n'}" +} + # src/lib/init/files.sh init_files_create() { local target_dir=$1 @@ -2283,18 +2423,21 @@ parse_requirements() { [[ -n ${args['--fail-fast']+x} ]] || args['--fail-fast']=1 ;; + # :flag.argfile_case --validate | -v) # :flag.argfile_case_no_arg [[ -n ${args['--validate']+x} ]] || args['--validate']=1 ;; + # :flag.argfile_case --init) # :flag.argfile_case_no_arg [[ -n ${args['--init']+x} ]] || args['--init']=1 ;; + # :flag.argfile_case --default-target) # :flag.argfile_case_arg @@ -2302,6 +2445,7 @@ parse_requirements() { [[ -n ${args['--default-target']+x} ]] || args['--default-target']="$argfile_value" fi ;; + # :flag.argfile_case --tag | -t) # :flag.argfile_case_arg @@ -2314,6 +2458,7 @@ parse_requirements() { fi fi ;; + # :flag.argfile_case --exclude-tag | -x) # :flag.argfile_case_arg @@ -2326,6 +2471,7 @@ parse_requirements() { fi fi ;; + # :flag.argfile_case --stepdefs | -s) # :flag.argfile_case_arg @@ -2333,6 +2479,7 @@ parse_requirements() { [[ -n ${args['--stepdefs']+x} ]] || args['--stepdefs']="$argfile_value" fi ;; + # :flag.argfile_case --load | -l) # :flag.argfile_case_arg diff --git a/src/lib/feature/core.sh b/src/lib/feature/core.sh index 563f55f..5b100e0 100644 --- a/src/lib/feature/core.sh +++ b/src/lib/feature/core.sh @@ -144,6 +144,15 @@ feature_run() { esac continue ;; + table_row) + in_description=0 + if feature_table_row_apply "$FEATURE_TABLE_TEXT" "$section" background_steps scenario_steps; then + : + else + failed=1 + fi + continue + ;; doc_string_fence) doc_string_indent=${line%%\"\"\"*} in_doc_string=1 @@ -194,6 +203,8 @@ feature__recorded_step_parse() { local recorded=$1 local remainder + recorded=${recorded%%$'\x1e'*} + FEATURE_RECORDED_STEP_KEYWORD=${recorded%%$'\t'*} remainder=${recorded#*$'\t'} FEATURE_RECORDED_STEP_DOC_STRING= @@ -212,6 +223,7 @@ feature_recorded_step_validate() { local previous_type=$2 local resolved_type + feature_table_validate "$recorded" || return 1 feature__recorded_step_parse "$recorded" resolved_type=$(feature_step_type_resolve "$previous_type" "$FEATURE_RECORDED_STEP_KEYWORD") || return 1 @@ -234,6 +246,7 @@ feature_recorded_step_run() { if [[ -n $FEATURE_RECORDED_STEP_DOC_STRING ]]; then export DOC_STRING=$FEATURE_RECORDED_STEP_DOC_STRING fi + feature_table_export "$recorded" || return 1 step_run "$resolved_type" "$FEATURE_RECORDED_STEP_TEXT" status=$? @@ -271,6 +284,11 @@ feature_scenario_validate() { for index in "${!background_steps_ref[@]}"; do step=${background_steps_ref[$index]} + if ! feature_table_validate "$step"; then + feature__recorded_step_parse "$step" + feature_validation_set_error "${background_lines_ref[$index]}" "data table rows must have the same number of cells for" "$FEATURE_RECORDED_STEP_KEYWORD $FEATURE_RECORDED_STEP_TEXT" + return 1 + fi if feature_recorded_step_validate "$step" "$FEATURE_PREVIOUS_STEP_TYPE"; then : else @@ -282,6 +300,11 @@ feature_scenario_validate() { for index in "${!scenario_steps_ref[@]}"; do step=${scenario_steps_ref[$index]} + if ! feature_table_validate "$step"; then + feature__recorded_step_parse "$step" + feature_validation_set_error "${scenario_lines_ref[$index]}" "data table rows must have the same number of cells for" "$FEATURE_RECORDED_STEP_KEYWORD $FEATURE_RECORDED_STEP_TEXT" + return 1 + fi if feature_recorded_step_validate "$step" "$FEATURE_PREVIOUS_STEP_TYPE"; then : else @@ -452,6 +475,16 @@ feature_validate() { esac continue ;; + table_row) + in_description=0 + if 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")" + failed=1 + fi + continue + ;; doc_string_fence) doc_string_indent=${line%%\"\"\"*} in_doc_string=1 diff --git a/src/lib/feature/syntax.sh b/src/lib/feature/syntax.sh index b80bbcd..cc816af 100644 --- a/src/lib/feature/syntax.sh +++ b/src/lib/feature/syntax.sh @@ -9,6 +9,7 @@ feature_line_parse() { FEATURE_STEP_TYPE= FEATURE_STEP_TEXT= FEATURE_TAG_TEXT= + FEATURE_TABLE_TEXT= if [[ -z $line ]]; then FEATURE_LINE_KIND=blank @@ -55,6 +56,12 @@ feature_line_parse() { return 0 fi + if [[ $line == \|*\| ]]; then + FEATURE_LINE_KIND=table_row + FEATURE_TABLE_TEXT=$line + return 0 + fi + FEATURE_LINE_KIND=other FEATURE_LINE_NAME=$line } diff --git a/src/lib/feature/table.sh b/src/lib/feature/table.sh new file mode 100644 index 0000000..2ff5ffe --- /dev/null +++ b/src/lib/feature/table.sh @@ -0,0 +1,102 @@ +## Parses one data table row into a caller-provided array. +feature_table_row_parse() { + local line=$1 + local -n cells_ref=$2 + local content + local cell + local rest + + [[ $line == \|*\| ]] || return 1 + + content=${line#\|} + content=${content%\|} + cells_ref=() + rest=$content + while [[ $rest == *\|* ]]; do + cell=${rest%%\|*} + cells_ref+=("$(trim "$cell")") + rest=${rest#*\|} + done + cells_ref+=("$(trim "$rest")") +} + +## Attaches one parsed data table row to the previous recorded step. +feature_table_row_apply() { + local row=$1 + local section=$2 + # shellcheck disable=SC2178 # nameref to an array supplied by name + local -n background_steps_ref=$3 + # shellcheck disable=SC2178 # nameref to an array supplied by name + local -n scenario_steps_ref=$4 + local last_index + local recorded + + case $section in + background) + ((${#background_steps_ref[@]} > 0)) || return 1 + last_index=$((${#background_steps_ref[@]} - 1)) + recorded=${background_steps_ref[last_index]} + background_steps_ref[last_index]="$recorded"$'\x1e'"$row" + ;; + scenario) + ((${#scenario_steps_ref[@]} > 0)) || return 1 + last_index=$((${#scenario_steps_ref[@]} - 1)) + recorded=${scenario_steps_ref[last_index]} + scenario_steps_ref[last_index]="$recorded"$'\x1e'"$row" + ;; + *) + return 1 + ;; + esac +} + +## Validates the rows attached to one recorded step. +feature_table_validate() { + local recorded=$1 + local table_data + local row + local width=0 + local -a cells=() + + [[ $recorded == *$'\x1e'* ]] || return 0 + table_data=${recorded#*$'\x1e'} + + while IFS= read -r row; do + feature_table_row_parse "$row" cells || return 1 + if ((width == 0)); then + width=${#cells[@]} + ((width > 0)) || return 1 + elif ((${#cells[@]} != width)); then + return 1 + fi + done <<<"${table_data//$'\x1e'/$'\n'}" +} + +## Exposes an attached data table to a step definition. +feature_table_export() { + local recorded=$1 + local table_data + local row + local index=0 + local -a cells=() + + TABLE_HEADER=() + TABLE_ROWS=() + [[ $recorded == *$'\x1e'* ]] || return 0 + feature_table_validate "$recorded" || return 1 + + table_data=${recorded#*$'\x1e'} + while IFS= read -r row; do + feature_table_row_parse "$row" cells || return 1 + if ((index == 0)); then + # shellcheck disable=SC2034 # consumed by the evaluated step definition + TABLE_HEADER=("${cells[@]}") + else + TABLE_ROWS+=("$( + IFS=$'\t' + printf '%s' "${cells[*]}" + )") + fi + ((index += 1)) + done <<<"${table_data//$'\x1e'/$'\n'}" +} diff --git a/test/commands/validate.bats b/test/commands/validate.bats index 7838325..e03fcd6 100644 --- a/test/commands/validate.bats +++ b/test/commands/validate.bats @@ -120,3 +120,23 @@ EOF assert_output_contains "line 3: tag must appear before Feature or Scenario" assert_output_contains "@setup" } + +@test "shellkin --validate rejects data table rows with inconsistent widths" { + write_file features/example.feature <<'EOF' +Feature: Data tables + +Scenario: Invalid row + Given these users exist + | name | role | + | Alice | +EOF + write_file features/step_definitions/core.sh <<'EOF' +@Given these users exist +true +EOF + + run "$SHELLKIN_REPO_ROOT/shellkin" --validate "$TEST_ROOT/features" + + [ "$status" -ne 0 ] + assert_output_contains "data table rows must have the same number of cells" +} diff --git a/test/lib/feature/core.bats b/test/lib/feature/core.bats index fddb0ad..e863507 100644 --- a/test/lib/feature/core.bats +++ b/test/lib/feature/core.bats @@ -5,7 +5,7 @@ load ../../test_helper.bash setup() { setup_test_environment eval "$(declare -f run | sed '1s/^run /bats_run /')" - source_libs core/colors core/trim stepdef/pattern stepdef/parse stepdef/hooks feature/syntax stepdef/files user_helpers/run user_helpers/fail user_helpers/defer step/core output/test feature/core + source_libs core/colors core/trim stepdef/pattern stepdef/parse stepdef/hooks feature/syntax feature/table stepdef/files user_helpers/run user_helpers/fail user_helpers/defer step/core output/test feature/core STEPDEF_TYPES=() STEPDEF_PATTERNS=() @@ -331,6 +331,36 @@ EOF [ "$(cat "$TEST_ROOT/doc_string.txt")" = $'first line\nsecond line' ] } +@test "feature_recorded_step_run exposes an attached data table through arrays" { + stepdef_parse "@Given these users exist" + stepdef_register 'printf "%s\n" "${TABLE_HEADER[*]}" "${TABLE_ROWS[@]}" > "$TEST_ROOT/table.txt"' + recorded=$'Given\tthese users exist\x1e| name | role |\x1e| Alice | admin |\x1e| Bob | user |' + + feature_recorded_step_run "$recorded" "" + + [ "$(cat "$TEST_ROOT/table.txt")" = $'name role\nAlice\tadmin\nBob\tuser' ] +} + +@test "feature_run rejects data table rows with inconsistent widths" { + write_file stepdefs.sh <<'EOF' +@Given these users exist +true +EOF + write_file test.feature <<'EOF' +Feature: Data tables + +Scenario: Invalid row + Given these users exist + | name | role | + | Alice | +EOF + + stepdefs_file_parse "$TEST_ROOT/stepdefs.sh" + bats_run feature_run "$TEST_ROOT/test.feature" + + [ "$status" -eq 1 ] +} + @test "output_error_report_field indents multiline values inside the report frame" { output_error_report_field "LAST_STDOUT" $'first line\nsecond line' >"$TEST_ROOT/output.txt" diff --git a/test/lib/feature/syntax.bats b/test/lib/feature/syntax.bats index 57d2146..741b556 100644 --- a/test/lib/feature/syntax.bats +++ b/test/lib/feature/syntax.bats @@ -7,6 +7,13 @@ setup() { source_libs core/trim feature/syntax } +@test "feature_line_parse recognizes a data table row" { + feature_line_parse " | name | role |" + + [ "$FEATURE_LINE_KIND" = table_row ] + [ "$FEATURE_TABLE_TEXT" = "| name | role |" ] +} + teardown() { teardown_test_environment }