diff --git a/CHANGELOG.md b/CHANGELOG.md index 626b6d6f..d9592ca7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - Renamed `BASHUNIT_TESTS_ENV` to `BASHUNIT_LOAD_FILE` - Better handler runtime errors - Display failing tests after running the entire suite +- Added defer expressions with `eval` when using standalone assertions ## [0.16.0](https://github.com/TypedDevs/bashunit/compare/0.15.0...0.16.0) - 2024-09-15 diff --git a/docs/standalone.md b/docs/standalone.md index 52b44741..16f80791 100644 --- a/docs/standalone.md +++ b/docs/standalone.md @@ -43,3 +43,53 @@ The prefix `assert_` is optional. ``` ::: +## Lazy evaluations + +You can evaluate the `exit_code` for your scripts using the command to call as raw-string instead of +executing them with `$(...)` to avoid interrupting the CI when encountering a potential error (anything but `0`). + +::: code-group +```bash [Example] +./bashunit -a exit_code "1" "$PHPSTAN_PATH analyze \ + --no-progress --level 8 \ + --error-format raw ./" +``` +```[Output] +Testing.php:3:Method Testing::bar() has no return type specified. +``` +::: + +This is useful to get control over the output of your "callable": + +::: code-group +```bash [Example] +OUTPUT=$(./bashunit -a exit_code "1" "$PHPSTAN_PATH analyze \ + --no-progress --level 8 \ + --error-format raw ./") +./bashunit -a line_count 1 "$OUTPUT" +``` +```[Output] +# No output +``` +::: + +### Full control over the stdout and stderr + +The stdout will be used for the callable result, while bashunit output will be on stderr. +This way you can control the FD and redirect the output as you need. + +::: code-group +```bash [Example] +./bashunit -a exit_code "0" "$PHPSTAN_PATH analyze \ + --no-progress --level 8 \ + --error-format raw ./" 2> /tmp/error.log +``` +```[Output] +Testing.php:3:Method Testing::bar() has no return type specified. +``` +```[/tmp/error.log] +✗ Failed: Main::exec assert + Expected '0' + but got '1' +``` +::: diff --git a/src/assert.sh b/src/assert.sh index d9fdade3..07a54ba9 100755 --- a/src/assert.sh +++ b/src/assert.sh @@ -213,7 +213,7 @@ function assert_general_error() { local actual_exit_code=${3-"$?"} local expected_exit_code=1 - if [[ $actual_exit_code -ne "$expected_exit_code" ]]; then + if [[ "$actual_exit_code" -ne "$expected_exit_code" ]]; then local label label="$(helper::normalize_test_function_name "${FUNCNAME[1]}")" state::add_assertions_failed diff --git a/src/main.sh b/src/main.sh index 9f7733d7..613d09b6 100644 --- a/src/main.sh +++ b/src/main.sh @@ -34,21 +34,76 @@ function main::exec_tests() { function main::exec_assert() { local original_assert_fn=$1 - local assert_fn=$original_assert_fn local args=("${@:2}") + local assert_fn=$original_assert_fn + + # Check if the function exists if ! type "$assert_fn" > /dev/null 2>&1; then - # try again using prefix `assert_` assert_fn="assert_$assert_fn" if ! type "$assert_fn" > /dev/null 2>&1; then - echo "Function $original_assert_fn does not exist." + echo "Function $original_assert_fn does not exist." 1>&2 exit 127 fi fi - "$assert_fn" "${args[@]}" + # Get the last argument safely by calculating the array length + local last_index=$((${#args[@]} - 1)) + local last_arg="${args[$last_index]}" + local output="" + local inner_exit_code=0 + local bashunit_exit_code=0 + + # Handle different assert_* functions + case "$assert_fn" in + assert_exit_code) + output=$(main::handle_assert_exit_code "$last_arg") + inner_exit_code=$? + # Remove the last argument and append the exit code + args=("${args[@]:0:last_index}") + args+=("$inner_exit_code") + ;; + *) + # Add more cases here for other assert_* handlers if needed + ;; + esac + + if [[ -n "$output" ]]; then + echo "$output" 1>&1 + assert_fn="assert_same" + fi + + # Run the assertion function and write into stderr + "$assert_fn" "${args[@]}" 1>&2 + bashunit_exit_code=$? if [[ "$(state::get_tests_failed)" -gt 0 ]] || [[ "$(state::get_assertions_failed)" -gt 0 ]]; then - exit 1 + return 1 + fi + + return "$bashunit_exit_code" +} + +function main::handle_assert_exit_code() { + local cmd="$1" + local output + local inner_exit_code=0 + + if [[ $(command -v "${cmd%% *}") ]]; then + output=$(eval "$cmd" 2>&1 || echo "inner_exit_code:$?") + local last_line + last_line=$(echo "$output" | tail -n 1) + if echo "$last_line" | grep -q 'inner_exit_code:[0-9]*'; then + inner_exit_code=$(echo "$last_line" | grep -o 'inner_exit_code:[0-9]*' | cut -d':' -f2) + if ! [[ $inner_exit_code =~ ^[0-9]+$ ]]; then + inner_exit_code=1 + fi + output=$(echo "$output" | sed '$d') + fi + echo "$output" + return "$inner_exit_code" + else + echo "Command not found: $cmd" 1>&2 + return 127 fi } diff --git a/tests/acceptance/bashunit_direct_fn_call_test.sh b/tests/acceptance/bashunit_direct_fn_call_test.sh index 8fe89796..4f99f7aa 100644 --- a/tests/acceptance/bashunit_direct_fn_call_test.sh +++ b/tests/acceptance/bashunit_direct_fn_call_test.sh @@ -74,11 +74,73 @@ function test_bashunit_direct_fn_call_failure() { local expected="foo" local actual="bar" - assert_match_snapshot "$(./bashunit -a assert_same --env "$TEST_ENV_FILE" "$expected" $actual)" + assert_match_snapshot "$(./bashunit -a assert_same --env "$TEST_ENV_FILE" "$expected" $actual 2>&1)" assert_general_error "$(./bashunit -a assert_same --env "$TEST_ENV_FILE" "$expected" $actual)" } function test_bashunit_direct_fn_call_non_existing_fn() { - assert_match_snapshot "$(./bashunit -a non_existing_fn --env "$TEST_ENV_FILE")" + assert_match_snapshot "$(./bashunit -a non_existing_fn --env "$TEST_ENV_FILE" 2>&1)" assert_command_not_found "$(./bashunit -a non_existing_fn --env "$TEST_ENV_FILE")" } + +# shellcheck disable=SC2155 +function test_bashunit_assert_exit_code_successful_with_inner_func() { + local temp=$(mktemp) + # shellcheck disable=SC2116 + local output="$(./bashunit -a exit_code "0" "$(echo "unknown command")" 2> "$temp")" + + assert_empty "$output" + assert_same "Command not found: unknown command" "$(cat "$temp")" + + rm "$temp" +} + +# shellcheck disable=SC2155 +function test_bashunit_assert_exit_code_error_with_inner_func() { + local temp=$(mktemp) + # shellcheck disable=SC2116 + local output="$(./bashunit -a exit_code "1" "$(echo "unknown command")" 2> "$temp")" + + assert_empty "$output" + + assert_contains\ + "$(console_results::print_failed_test "Main::exec assert" "0" "to be" "1")"\ + "$(cat "$temp")" + + rm "$temp" +} + +function test_bashunit_assert_exit_code_str_successful_code() { + ./bashunit -a exit_code "0" "./bashunit -a same 1 1" + assert_successful_code +} + +function test_bashunit_assert_exit_code_str_general_error() { + ./bashunit -a exit_code "1" "./bashunit -a same 1 2" + assert_successful_code +} + +# shellcheck disable=SC2155 +function test_bashunit_assert_exit_code_str_successful_but_exit_code_error() { + local temp=$(mktemp) + local output="$(./bashunit -a exit_code "1" "echo something to stdout" 2> "$temp")" + + assert_same "something to stdout" "$output" + + assert_contains\ + "$(console_results::print_failed_test "Main::exec assert" "1" "but got " "0")"\ + "$(cat "$temp")" + + rm "$temp" +} + +# shellcheck disable=SC2155 +function test_bashunit_assert_exit_code_str_successful_and_exit_code_ok() { + local temp=$(mktemp) + local output="$(./bashunit -a exit_code "0" "echo something to stdout" 2> "$temp")" + + assert_same "something to stdout" "$output" + assert_empty "$(cat "$temp")" + + rm "$temp" +}