From 9aba51cea229262f0ab883c046f43dadb9e42e22 Mon Sep 17 00:00:00 2001 From: Christian Zahl Date: Wed, 12 Feb 2025 11:35:11 +0100 Subject: [PATCH 1/9] feat: parser to handle gherkin like macros --- bash_unit | 81 ++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 75 insertions(+), 6 deletions(-) diff --git a/bash_unit b/bash_unit index adc949d..7b14100 100755 --- a/bash_unit +++ b/bash_unit @@ -208,7 +208,12 @@ stacktrace() { local i=1 while [ -n "${BASH_SOURCE[$i]:-}" ] do - echo "${BASH_SOURCE[$i]}:${BASH_LINENO[$((i-1))]}:${FUNCNAME[$i]}()" + if [ "$gherkin" = 1 ] && [[ ${BASH_SOURCE[$i]} =~ ^/dev/fd ]] + then + echo "$test_file:${BASH_LINENO[$((i-1))]}:${FUNCNAME[$i]}()" + else + echo "${BASH_SOURCE[$i]}:${BASH_LINENO[$((i-1))]}:${FUNCNAME[$i]}()" + fi i=$((i + 1)) done | "$GREP" -v "^$BASH_SOURCE" } @@ -504,6 +509,54 @@ count() { fi } +#--------------------- +gherkin_strip_arg() +{ + local s + s=${1## } + s=${s#\"} + s=${s%\"} + echo "$s" +} + +gherkin_FEATURE() { + gherkin_last_feature=$(gherkin_strip_arg "$*") +} + +gherkin_SCENARIO() { + local func_name + gherkin_last_scenario=$(gherkin_strip_arg "$*") + func_name="${gherkin_last_scenario//[^a-zA-Z0-9_]/_}" + echo "test_$func_name ()" +} + +gherkin_GIVEN() { + gherkin_last_given=$(gherkin_strip_arg "$*") +} + +gherkin_WHEN() { + gherkin_last_when=$(gherkin_strip_arg "$*") +} + +gherkin_THEN() { + gherkin_last_then=$(gherkin_strip_arg "$*") + gherkin_last_msg="" + gherkin_last_msg="${gherkin_last_msg}${YELLOW}SCENARIO${NOCOLOR} $gherkin_last_scenario\n" + gherkin_last_msg="${gherkin_last_msg} ${YELLOW}GIVEN${NOCOLOR} $gherkin_last_given\n" + gherkin_last_msg="${gherkin_last_msg} ${YELLOW}WHEN${NOCOLOR} $gherkin_last_when\n" + gherkin_last_msg="${gherkin_last_msg} ${YELLOW}THEN${NOCOLOR} $gherkin_last_then" +} + +gherkin_parse() +{ + while IFS= read -r line; do + # echo "$line" + [[ $line =~ [:blank:]*@([A-Z]*)\ (.*) ]] && { "gherkin_${BASH_REMATCH[1]}" "${BASH_REMATCH[2]}"; continue; } + line="${line/@MSG/\"$gherkin_last_msg\"}" + echo "$line" + done < <(cat "$1") +} +#---------------------- output_format=text verbosity=normal test_pattern="" @@ -511,7 +564,8 @@ test_pattern_separator="" skip_pattern="" skip_pattern_separator="" randomize=0 -while getopts "vp:s:f:rq" option +gherkin=0 +while getopts "vp:s:f:rqg" option do case "$option" in p) @@ -535,6 +589,9 @@ do q) verbosity=quiet ;; + g) + gherkin=1 + ;; ?) usage ;; @@ -579,11 +636,23 @@ do if [[ "${STICK_TO_CWD:-}" != true ]] then cd "$(dirname "$test_file")" - # shellcheck disable=1090 - source "$(basename "$test_file")" + if [ "$gherkin" = 1 ] + then + # shellcheck disable=1090 + source <(gherkin_parse "$(basename "$test_file")") + else + # shellcheck disable=1090 + source "$(basename "$test_file")" + fi else - # shellcheck disable=1090 - source "$test_file" + if [ "$gherkin" = 1 ] + then + # shellcheck disable=1090 + source <(gherkin_parse "$test_file") + else + # shellcheck disable=1090 + source "$test_file" + fi fi set +e run_test_suite From caf6c81f2c1ba4d05ff524018632dded42e3905a Mon Sep 17 00:00:00 2001 From: Christian Zahl Date: Tue, 25 Mar 2025 11:25:02 +0000 Subject: [PATCH 2/9] doc: add -q option to synopsis --- README.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.adoc b/README.adoc index bbc2aab..46edd49 100644 --- a/README.adoc +++ b/README.adoc @@ -12,7 +12,7 @@ bash_unit - bash unit testing enterprise edition framework for professionals! == Synopsis -*bash_unit* [-f tap] [-p ] [-s ] [-r] [test_file] +*bash_unit* [-f tap] [-p ] [-s ] [-r] [test_file] [-q] == Description From 859c13fe3eeac0711d27e016351aa9640617acad Mon Sep 17 00:00:00 2001 From: Christian Zahl Date: Tue, 25 Mar 2025 15:10:20 +0000 Subject: [PATCH 3/9] fix: use colors in terminal mode only --- bash_unit | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/bash_unit b/bash_unit index 7b14100..03dc7e9 100755 --- a/bash_unit +++ b/bash_unit @@ -539,12 +539,19 @@ gherkin_WHEN() { } gherkin_THEN() { + local COL="" + local NCOL="" + if is_terminal; then + COL="${YELLOW}" + NCOL="${NOCOLOR}" + fi + gherkin_last_then=$(gherkin_strip_arg "$*") gherkin_last_msg="" - gherkin_last_msg="${gherkin_last_msg}${YELLOW}SCENARIO${NOCOLOR} $gherkin_last_scenario\n" - gherkin_last_msg="${gherkin_last_msg} ${YELLOW}GIVEN${NOCOLOR} $gherkin_last_given\n" - gherkin_last_msg="${gherkin_last_msg} ${YELLOW}WHEN${NOCOLOR} $gherkin_last_when\n" - gherkin_last_msg="${gherkin_last_msg} ${YELLOW}THEN${NOCOLOR} $gherkin_last_then" + gherkin_last_msg="${gherkin_last_msg}${COL}SCENARIO${NCOL} $gherkin_last_scenario\n" + gherkin_last_msg="${gherkin_last_msg} ${COL}GIVEN${NCOL} $gherkin_last_given\n" + gherkin_last_msg="${gherkin_last_msg} ${COL}WHEN${NCOL} $gherkin_last_when\n" + gherkin_last_msg="${gherkin_last_msg} ${COL}THEN${NCOL} $gherkin_last_then" } gherkin_parse() From ab62848d17f102a2689910287de9cb3407b63f84 Mon Sep 17 00:00:00 2001 From: Christian Zahl Date: Tue, 25 Mar 2025 15:10:49 +0000 Subject: [PATCH 4/9] feat: add @AND decorator --- bash_unit | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bash_unit b/bash_unit index 03dc7e9..349fee2 100755 --- a/bash_unit +++ b/bash_unit @@ -554,6 +554,8 @@ gherkin_THEN() { gherkin_last_msg="${gherkin_last_msg} ${COL}THEN${NCOL} $gherkin_last_then" } +gherkin_AND() { gherkin_THEN "$@"; } + gherkin_parse() { while IFS= read -r line; do From ed59b673c208a9b63c19abd4c26366090831436f Mon Sep 17 00:00:00 2001 From: Christian Zahl Date: Tue, 25 Mar 2025 15:12:14 +0000 Subject: [PATCH 5/9] feat: handle gherkin based tests --- tests/test_doc.sh | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/tests/test_doc.sh b/tests/test_doc.sh index 47d086b..00aea21 100644 --- a/tests/test_doc.sh +++ b/tests/test_doc.sh @@ -8,9 +8,13 @@ unset LC_ALL LANGUAGE export STICK_TO_CWD=true BASH_UNIT="eval FORCE_COLOR=false ./bash_unit" +TEST_PATTERN_GHERKIN='```test-g' +OUTPUT_PATTERN_GHERKIN='```output-g' +BASH_UNIT_GHERKIN="eval FORCE_COLOR=false ./bash_unit -g" +block=0 + prepare_tests() { - mkdir /tmp/$$ - local block=0 + [ -d /tmp/$$ ] || mkdir /tmp/$$ local remaining=/tmp/$$/remaining local swap=/tmp/$$/swap local test_output=/tmp/$$/test_output @@ -28,6 +32,14 @@ prepare_tests() { done } +prepare_gherkin_tests() +{ + BASH_UNIT="$BASH_UNIT_GHERKIN" + TEST_PATTERN="$TEST_PATTERN_GHERKIN" + OUTPUT_PATTERN="$OUTPUT_PATTERN_GHERKIN" + prepare_tests +} + function run_doc_test() { local remaining="$1" local swap="$2" @@ -81,3 +93,4 @@ function _next_quote_section() { # test subdirectory cd .. prepare_tests +prepare_gherkin_tests From 004cbbb2e8f756d3d2f77df5da1af8cd9b5f8f0b Mon Sep 17 00:00:00 2001 From: Christian Zahl Date: Tue, 25 Mar 2025 15:27:09 +0000 Subject: [PATCH 6/9] doc: add usage of gherkin like test style --- README.adoc | 125 +++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 124 insertions(+), 1 deletion(-) diff --git a/README.adoc b/README.adoc index 46edd49..a723311 100644 --- a/README.adoc +++ b/README.adoc @@ -12,7 +12,7 @@ bash_unit - bash unit testing enterprise edition framework for professionals! == Synopsis -*bash_unit* [-f tap] [-p ] [-s ] [-r] [test_file] [-q] +*bash_unit* [-f tap] [-p ] [-s ] [-r] [test_file] [-q] [-g] == Description @@ -76,6 +76,10 @@ _(by the way, the documentation you are reading is itself tested with bash-unit) Will only output the status of each test with no further information even in case of failure. +*-g*:: + gherkin style. + Accept tests written in a Gherkin like style (see "Gherkin" section below). + ifndef::backend-manpage[] == How to install *bash_unit* @@ -994,3 +998,122 @@ test_get_data_from_fake() { expected [ax] but was [a] doc:13:test_get_data_from_fake() ``` + +=== Write tests in a Gherkin like style +*bash_unit* supports to tests written in a +[Gherkin](https://cucumber.io/docs/gherkin/reference) like style. You need +to run *bash_unit* with the option _-g_ to enable the Gherkin like test style. + +```test-g +@FEATURE wc can count the number of lines + +@SCENARIO calling wc without any arguments should also count the # of lines +{ + @GIVEN wc is installed + @WHEN running (echo BDD; echo is; echo so; echo cool) | wc + output=$((echo BDD; echo is; echo so; echo cool) | wc) + @THEN it should print three results + assert_equals 3 "$(echo $output | wc -w)" @MSG + @AND the first should match the number of lines (4) + assert_equals 4 "$(echo $output | cut -d " " -f 1)" @MSG +} +``` + +```output-g + Running test_calling_wc_without_any_arguments_should_also_count_the___of_lines ... SUCCESS +``` +In case of a failure (`wc -c` counts the number of characters instead of lines), +the resulting message is a merge of the individual `@XXX` decorators: + +```test-g +@SCENARIO calling wc with the argument -c it should count the # of lines +{ + @GIVEN wc is installed + @WHEN running (echo BDD; echo is; echo so; echo cool) | wc -c + output=$((echo BDD; echo is; echo so; echo cool) | wc -c) + @THEN the counted number of lines should be 4 + assert_equals 4 "$(echo $output)" @MSG +} +``` + +```output-g + Running test_calling_wc_with_the_argument__c_it_should_count_the___of_lines ... FAILURE +SCENARIO calling wc with the argument -c it should count the # of lines + GIVEN wc is installed + WHEN running (echo BDD; echo is; echo so; echo cool) | wc -c + THEN the counted number of lines should be 4 + expected [4] but was [15] +doc:4:test_calling_wc_with_the_argument__c_it_should_count_the___of_lines() +``` + +The Gherkin like test style is implemented by a simple DSL. The test script +is preprocessed and lines with the `@XXX` decorators are getting replaced. +The resulting script is then tested by *bash_unit* as usual. + +Key concepts you need to keep in mind when using the Gherin stype DSL: + +* The preprocessor parses the test script line by line and substitues the +lines starting with a `@XXX` decorator. Leading whitespaces are ignorred. +* The `@MSG` decorator is handled in-line +* The text following an `@XXX` decorator (with the exception of the `@MSG` decorator) +is used as an argument to the decorator. +* A decorator entry ends at the lineend. There is no line continuation. +* Gherkin like tests can be mixed with the standard *bash_uni* style. +* Only upper case decorators are handled. +* Lines with an unknown `@XXX`decorator are filtered out. + +==== Recommended structure +The decorators helps to structure your test suites. Using the decorators +give ou some guidelines to your hand how to document your tests. The +structured approach is much more readable then looking into some comments. +When following a behavior driven develoment approach, the decorators can also +be used to derive the documentation of the behavior. + +```text +$ grep '^[[:blank:]]*@[[:upper:]]*' ../README.adoc +@FEATURE wc can count the number of lines +@SCENARIO calling wc without any arguments should also count the # of lines + @GIVEN wc is installed + @WHEN running (echo BDD; echo is; echo so; echo cool) | wc + @THEN it should print three results + @AND the first should match the number of lines (4) +... +``` + +Begin your tests with a `@FEATURE` decorator to express that the following +set of test functions are used to verify the implemntation of a given feature. +The argument is stored in an internal variable but not used anymore. + +Beginning groups of tests with a `@FEATURE` helps to structure and to understand +the test by others or after some time. + +The `@SCENARIO` decorator is mainly the wrapper to define a test function. The +argument is any aspect to test given as clear text. It is used to generate name +of the test function. Each character other than a letter or digit is replayed by +an `_`. The resulting string is prefixed by `test_`. + +A line like +```text +@SCENARIO calling wc without any arguments should also count the # of lines +``` + +will be raplaced by +```text +test_calling_wc_without_any_arguments_should_also_count_the___of_lines () +``` + +Use the `@GIVEN` decorator to describe a precondition to be met. The line +will be filtered out and the argument will be used by the `@MSG` decorator. + +The `@WHEN` decorator should be used before running the code to be tested. +It should describe what is getting tested. The ine is filtered out and +used by the `@MSG` decorator. + +Usually, after calling the code to test the test expression follows. You +should add a `@THEN` decorator to express the expected behaviour of the +code to test. Again, the line is getting filtered out and the argument is +used by the `@MSG` decorator. + +If you have multiple test expression for a given call of the code to test +then you should use the `@AND` decorator. It is mainly the same as the `@THEN` +decorator but extends the readability of the test. From f950946c64d4a05e393004f38f5ffc79a0063585 Mon Sep 17 00:00:00 2001 From: Christian Zahl Date: Thu, 27 Mar 2025 06:38:43 +0000 Subject: [PATCH 7/9] fix: replace filtered @XXX decoratory by an empty line to keep original linecount. --- bash_unit | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/bash_unit b/bash_unit index 349fee2..aa6d39d 100755 --- a/bash_unit +++ b/bash_unit @@ -521,6 +521,7 @@ gherkin_strip_arg() gherkin_FEATURE() { gherkin_last_feature=$(gherkin_strip_arg "$*") + echo "" } gherkin_SCENARIO() { @@ -532,10 +533,12 @@ gherkin_SCENARIO() { gherkin_GIVEN() { gherkin_last_given=$(gherkin_strip_arg "$*") + echo "" } gherkin_WHEN() { gherkin_last_when=$(gherkin_strip_arg "$*") + echo "" } gherkin_THEN() { @@ -552,6 +555,7 @@ gherkin_THEN() { gherkin_last_msg="${gherkin_last_msg} ${COL}GIVEN${NCOL} $gherkin_last_given\n" gherkin_last_msg="${gherkin_last_msg} ${COL}WHEN${NCOL} $gherkin_last_when\n" gherkin_last_msg="${gherkin_last_msg} ${COL}THEN${NCOL} $gherkin_last_then" + echo "" } gherkin_AND() { gherkin_THEN "$@"; } From 73434764f43cae14427613b1e9c2f891f8714a9b Mon Sep 17 00:00:00 2001 From: Christian Zahl Date: Thu, 27 Mar 2025 06:39:50 +0000 Subject: [PATCH 8/9] fix: keep previous returncode when using @THEN --- bash_unit | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bash_unit b/bash_unit index aa6d39d..aa87960 100755 --- a/bash_unit +++ b/bash_unit @@ -542,6 +542,7 @@ gherkin_WHEN() { } gherkin_THEN() { + local rc=$? local COL="" local NCOL="" if is_terminal; then @@ -556,6 +557,7 @@ gherkin_THEN() { gherkin_last_msg="${gherkin_last_msg} ${COL}WHEN${NCOL} $gherkin_last_when\n" gherkin_last_msg="${gherkin_last_msg} ${COL}THEN${NCOL} $gherkin_last_then" echo "" + return $rc } gherkin_AND() { gherkin_THEN "$@"; } From c00e2c4fb28505c7e81e8a1ae67e7349191f7ca8 Mon Sep 17 00:00:00 2001 From: Christian Zahl Date: Thu, 27 Mar 2025 08:50:30 +0000 Subject: [PATCH 9/9] Revert "fix: keep previous returncode when using @THEN" This reverts commit 73434764f43cae14427613b1e9c2f891f8714a9b. --- bash_unit | 2 -- 1 file changed, 2 deletions(-) diff --git a/bash_unit b/bash_unit index aa87960..aa6d39d 100755 --- a/bash_unit +++ b/bash_unit @@ -542,7 +542,6 @@ gherkin_WHEN() { } gherkin_THEN() { - local rc=$? local COL="" local NCOL="" if is_terminal; then @@ -557,7 +556,6 @@ gherkin_THEN() { gherkin_last_msg="${gherkin_last_msg} ${COL}WHEN${NCOL} $gherkin_last_when\n" gherkin_last_msg="${gherkin_last_msg} ${COL}THEN${NCOL} $gherkin_last_then" echo "" - return $rc } gherkin_AND() { gherkin_THEN "$@"; }