Skip to content

Commit 5eac61c

Browse files
authored
Merge pull request #491 from TypedDevs/fix/489-setup-teardown-errors
Fix setup and teardown functions when errors
2 parents c0dc397 + 8d96d88 commit 5eac61c

16 files changed

+419
-22
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@
77
- Add two-way synchronization policy between `AGENTS.md` and `copilot-instructions.md` with automatic validation in task templates and PR checklist
88
- Add tasks storage policy clarifying `.tasks/` (versioned) vs `.task/` (private scratch, git-ignored)
99
- Include `set_test_title` helper in the single-file library
10+
- Fix lifecycle hooks capture-and-report flow errors
11+
- set_up
12+
- tear_down
13+
- set_up_before_script
14+
- tear_down_after_script
1015

1116
## [0.24.0](https://github.com/TypedDevs/bashunit/compare/0.23.0...0.24.0) - 2025-09-14
1217

docs/test-files.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ function tear_down() {
8585
The `set_up_before_script` auxiliary function is called, if it is present in the test file, only once before all tests functions in the test file begin.
8686
This is useful for global setup that applies to all test functions in the script, such as loading shared resources.
8787
88+
If any command inside `set_up_before_script` fails, bashunit halts the file immediately and reports the error (including the failing command and location) before any test functions run. This ensures misconfigured environments or missing dependencies surface clearly during setup.
89+
8890
::: code-group
8991
```bash [Example]
9092
function set_up_before_script() {
@@ -99,6 +101,8 @@ The `tear_down_after_script` auxiliary function is called, if it is present in t
99101
This auxiliary function is similar to how `set_up_before_script` works but at the end of the tests.
100102
It provides a hook for any cleanup that should occur after all tests have run, such as deleting temporary files or releasing resources.
101103
104+
Failures inside `tear_down_after_script` are also surfaced as dedicated errors after the final test output so cleanup problems (for example, missing tools or permissions) are visible in the run summary.
105+
102106
::: code-group
103107
```bash [Example]
104108
function tear_down_after_script() {

src/helpers.sh

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,9 +137,14 @@ function helper::get_functions_to_run() {
137137
# @param $1 string Eg: "do_something"
138138
#
139139
function helper::execute_function_if_exists() {
140-
if [[ "$(type -t "$1")" == "function" ]]; then
141-
"$1" 2>/dev/null
140+
local fn_name="$1"
141+
142+
if [[ "$(type -t "$fn_name")" == "function" ]]; then
143+
"$fn_name"
144+
return $?
142145
fi
146+
147+
return 0
143148
}
144149

145150
#

src/runner.sh

Lines changed: 183 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,19 @@ function runner::load_test_files() {
1717
internal_log "Loading file" "$test_file"
1818
# shellcheck source=/dev/null
1919
source "$test_file"
20-
runner::run_set_up_before_script
20+
if ! runner::run_set_up_before_script "$test_file"; then
21+
runner::clean_set_up_and_tear_down_after_script
22+
if ! parallel::is_enabled; then
23+
cleanup_script_temp_files
24+
fi
25+
continue
26+
fi
2127
if parallel::is_enabled; then
2228
runner::call_test_functions "$test_file" "$filter" 2>/dev/null &
2329
else
2430
runner::call_test_functions "$test_file" "$filter"
2531
fi
26-
runner::run_tear_down_after_script
32+
runner::run_tear_down_after_script "$test_file"
2733
runner::clean_set_up_and_tear_down_after_script
2834
if ! parallel::is_enabled; then
2935
cleanup_script_temp_files
@@ -57,9 +63,13 @@ function runner::load_bench_files() {
5763
export BASHUNIT_CURRENT_SCRIPT_ID="$(helper::generate_id "${test_file}")"
5864
# shellcheck source=/dev/null
5965
source "$bench_file"
60-
runner::run_set_up_before_script
66+
if ! runner::run_set_up_before_script "$bench_file"; then
67+
runner::clean_set_up_and_tear_down_after_script
68+
cleanup_script_temp_files
69+
continue
70+
fi
6171
runner::call_bench_functions "$bench_file" "$filter"
62-
runner::run_tear_down_after_script
72+
runner::run_tear_down_after_script "$bench_file"
6373
runner::clean_set_up_and_tear_down_after_script
6474
cleanup_script_temp_files
6575
done
@@ -284,18 +294,27 @@ function runner::run_test() {
284294
exec 3>&1
285295

286296
local test_execution_result=$(
297+
# shellcheck disable=SC2064
287298
# shellcheck disable=SC2154
288-
trap '
289-
exit_code=$?
299+
trap "
300+
exit_code=\$?
290301
set +e
291-
state::set_test_exit_code "$exit_code"
292-
runner::run_tear_down
302+
teardown_status=0
303+
runner::run_tear_down \"$test_file\" || teardown_status=\$?
293304
runner::clear_mocks
294305
cleanup_testcase_temp_files
306+
if [[ \$teardown_status -ne 0 ]]; then
307+
state::set_test_exit_code \"\$teardown_status\"
308+
else
309+
state::set_test_exit_code \"\$exit_code\"
310+
fi
295311
state::export_subshell_context
296-
' EXIT
312+
" EXIT
297313
state::initialize_assertions_count
298-
runner::run_set_up
314+
if ! runner::run_set_up "$test_file"; then
315+
status=$?
316+
exit "$status"
317+
fi
299318
300319
# 2>&1: Redirects the std-error (FD 2) to the std-output (FD 1).
301320
# points to the original std-output.
@@ -369,17 +388,46 @@ function runner::run_test() {
369388
local test_title=""
370389
[[ -n "$encoded_test_title" ]] && test_title="$(helper::decode_base64 "$encoded_test_title")"
371390

391+
local encoded_hook_failure
392+
encoded_hook_failure="${test_execution_result##*##TEST_HOOK_FAILURE=}"
393+
encoded_hook_failure="${encoded_hook_failure%%##*}"
394+
local hook_failure=""
395+
if [[ "$encoded_hook_failure" != "$test_execution_result" ]]; then
396+
hook_failure="$encoded_hook_failure"
397+
fi
398+
399+
local encoded_hook_message
400+
encoded_hook_message="${test_execution_result##*##TEST_HOOK_MESSAGE=}"
401+
encoded_hook_message="${encoded_hook_message%%##*}"
402+
local hook_message=""
403+
if [[ -n "$encoded_hook_message" ]]; then
404+
hook_message="$(helper::decode_base64 "$encoded_hook_message")"
405+
fi
406+
372407
state::set_test_title "$test_title"
373408
local label
374409
label="$(helper::normalize_test_function_name "$fn_name" "$interpolated_fn_name")"
375410
state::reset_test_title
376411

412+
local failure_label="$label"
413+
local failure_function="$fn_name"
414+
if [[ -n "$hook_failure" ]]; then
415+
failure_label="$(helper::normalize_test_function_name "$hook_failure")"
416+
failure_function="$hook_failure"
417+
fi
418+
377419
if [[ -n $runtime_error || $test_exit_code -ne 0 ]]; then
378420
state::add_tests_failed
379-
console_results::print_error_test "$label" "$runtime_error"
380-
reports::add_test_failed "$test_file" "$label" "$duration" "$total_assertions"
381-
runner::write_failure_result_output "$test_file" "$fn_name" "$runtime_error"
382-
internal_log "Test error" "$label" "$runtime_error"
421+
local error_message="$runtime_error"
422+
if [[ -n "$hook_failure" && -n "$hook_message" ]]; then
423+
error_message="$hook_message"
424+
elif [[ -z "$error_message" && -n "$hook_message" ]]; then
425+
error_message="$hook_message"
426+
fi
427+
console_results::print_error_test "$failure_function" "$error_message"
428+
reports::add_test_failed "$test_file" "$failure_label" "$duration" "$total_assertions"
429+
runner::write_failure_result_output "$test_file" "$failure_function" "$error_message"
430+
internal_log "Test error" "$failure_label" "$error_message"
383431
return
384432
fi
385433

@@ -555,19 +603,135 @@ function runner::write_failure_result_output() {
555603
echo -e "$test_nr) $test_file:$line_number\n$error_msg" >> "$FAILURES_OUTPUT_PATH"
556604
}
557605

606+
function runner::record_file_hook_failure() {
607+
local hook_name="$1"
608+
local test_file="$2"
609+
local hook_output="$3"
610+
local status="$4"
611+
local render_header="${5:-false}"
612+
613+
if [[ "$render_header" == true ]]; then
614+
runner::render_running_file_header "$test_file"
615+
fi
616+
617+
if [[ -z "$hook_output" ]]; then
618+
hook_output="Hook '$hook_name' failed with exit code $status"
619+
fi
620+
621+
state::add_tests_failed
622+
console_results::print_error_test "$hook_name" "$hook_output"
623+
reports::add_test_failed "$test_file" "$(helper::normalize_test_function_name "$hook_name")" 0 0
624+
runner::write_failure_result_output "$test_file" "$hook_name" "$hook_output"
625+
626+
return "$status"
627+
}
628+
629+
function runner::execute_file_hook() {
630+
local hook_name="$1"
631+
local test_file="$2"
632+
local render_header="${3:-false}"
633+
634+
if [[ "$(type -t "$hook_name")" != "function" ]]; then
635+
return 0
636+
fi
637+
638+
local hook_output=""
639+
local status=0
640+
local hook_output_file
641+
hook_output_file=$(temp_file "${hook_name}_output")
642+
643+
{
644+
"$hook_name"
645+
} >"$hook_output_file" 2>&1 || status=$?
646+
647+
if [[ -f "$hook_output_file" ]]; then
648+
hook_output=$(cat "$hook_output_file")
649+
rm -f "$hook_output_file"
650+
fi
651+
652+
if [[ $status -ne 0 ]]; then
653+
runner::record_file_hook_failure "$hook_name" "$test_file" "$hook_output" "$status" "$render_header"
654+
return $status
655+
fi
656+
657+
if [[ -n "$hook_output" ]]; then
658+
printf "%s\n" "$hook_output"
659+
fi
660+
661+
return 0
662+
}
663+
558664
function runner::run_set_up() {
665+
local _test_file="${1-}"
559666
internal_log "run_set_up"
560-
helper::execute_function_if_exists 'set_up'
667+
runner::execute_test_hook 'set_up'
561668
}
562669

563670
function runner::run_set_up_before_script() {
671+
local test_file="$1"
564672
internal_log "run_set_up_before_script"
565-
helper::execute_function_if_exists 'set_up_before_script'
673+
runner::execute_file_hook 'set_up_before_script' "$test_file" true
566674
}
567675

568676
function runner::run_tear_down() {
677+
local _test_file="${1-}"
569678
internal_log "run_tear_down"
570-
helper::execute_function_if_exists 'tear_down'
679+
runner::execute_test_hook 'tear_down'
680+
}
681+
682+
function runner::execute_test_hook() {
683+
local hook_name="$1"
684+
685+
if [[ "$(type -t "$hook_name")" != "function" ]]; then
686+
return 0
687+
fi
688+
689+
local hook_output=""
690+
local status=0
691+
local hook_output_file
692+
hook_output_file=$(temp_file "${hook_name}_output")
693+
694+
{
695+
"$hook_name"
696+
} >"$hook_output_file" 2>&1 || status=$?
697+
698+
if [[ -f "$hook_output_file" ]]; then
699+
hook_output=$(cat "$hook_output_file")
700+
rm -f "$hook_output_file"
701+
fi
702+
703+
if [[ $status -ne 0 ]]; then
704+
local message="$hook_output"
705+
if [[ -n "$hook_output" ]]; then
706+
printf "%s" "$hook_output"
707+
else
708+
message="Hook '$hook_name' failed with exit code $status"
709+
printf "%s\n" "$message" >&2
710+
fi
711+
runner::record_test_hook_failure "$hook_name" "$message" "$status"
712+
return "$status"
713+
fi
714+
715+
if [[ -n "$hook_output" ]]; then
716+
printf "%s" "$hook_output"
717+
fi
718+
719+
return 0
720+
}
721+
722+
function runner::record_test_hook_failure() {
723+
local hook_name="$1"
724+
local hook_message="$2"
725+
local status="$3"
726+
727+
if [[ -n "$(state::get_test_hook_failure)" ]]; then
728+
return "$status"
729+
fi
730+
731+
state::set_test_hook_failure "$hook_name"
732+
state::set_test_hook_message "$hook_message"
733+
734+
return "$status"
571735
}
572736

573737
function runner::clear_mocks() {
@@ -577,8 +741,9 @@ function runner::clear_mocks() {
577741
}
578742

579743
function runner::run_tear_down_after_script() {
744+
local test_file="$1"
580745
internal_log "run_tear_down_after_script"
581-
helper::execute_function_if_exists 'tear_down_after_script'
746+
runner::execute_file_hook 'tear_down_after_script' "$test_file"
582747
}
583748

584749
function runner::clean_set_up_and_tear_down_after_script() {

src/state.sh

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ _DUPLICATED_TEST_FUNCTIONS_FOUND=false
1616
_TEST_OUTPUT=""
1717
_TEST_TITLE=""
1818
_TEST_EXIT_CODE=0
19+
_TEST_HOOK_FAILURE=""
20+
_TEST_HOOK_MESSAGE=""
1921

2022
function state::get_tests_passed() {
2123
echo "$_TESTS_PASSED"
@@ -145,6 +147,30 @@ function state::reset_test_title() {
145147
_TEST_TITLE=""
146148
}
147149

150+
function state::get_test_hook_failure() {
151+
echo "$_TEST_HOOK_FAILURE"
152+
}
153+
154+
function state::set_test_hook_failure() {
155+
_TEST_HOOK_FAILURE="$1"
156+
}
157+
158+
function state::reset_test_hook_failure() {
159+
_TEST_HOOK_FAILURE=""
160+
}
161+
162+
function state::get_test_hook_message() {
163+
echo "$_TEST_HOOK_MESSAGE"
164+
}
165+
166+
function state::set_test_hook_message() {
167+
_TEST_HOOK_MESSAGE="$1"
168+
}
169+
170+
function state::reset_test_hook_message() {
171+
_TEST_HOOK_MESSAGE=""
172+
}
173+
148174
function state::set_duplicated_functions_merged() {
149175
state::set_duplicated_test_functions_found
150176
state::set_file_with_duplicated_function_names "$1"
@@ -159,20 +185,26 @@ function state::initialize_assertions_count() {
159185
_ASSERTIONS_SNAPSHOT=0
160186
_TEST_OUTPUT=""
161187
_TEST_TITLE=""
188+
_TEST_HOOK_FAILURE=""
189+
_TEST_HOOK_MESSAGE=""
162190
}
163191

164192
function state::export_subshell_context() {
165193
local encoded_test_output
166194
local encoded_test_title
167195

196+
local encoded_test_hook_message
197+
168198
if base64 --help 2>&1 | grep -q -- "-w"; then
169199
# Alpine requires the -w 0 option to avoid wrapping
170200
encoded_test_output=$(echo -n "$_TEST_OUTPUT" | base64 -w 0)
171201
encoded_test_title=$(echo -n "$_TEST_TITLE" | base64 -w 0)
202+
encoded_test_hook_message=$(echo -n "$_TEST_HOOK_MESSAGE" | base64 -w 0)
172203
else
173204
# macOS and others: default base64 without wrapping
174205
encoded_test_output=$(echo -n "$_TEST_OUTPUT" | base64)
175206
encoded_test_title=$(echo -n "$_TEST_TITLE" | base64)
207+
encoded_test_hook_message=$(echo -n "$_TEST_HOOK_MESSAGE" | base64)
176208
fi
177209

178210
cat <<EOF
@@ -182,6 +214,8 @@ function state::export_subshell_context() {
182214
##ASSERTIONS_INCOMPLETE=$_ASSERTIONS_INCOMPLETE\
183215
##ASSERTIONS_SNAPSHOT=$_ASSERTIONS_SNAPSHOT\
184216
##TEST_EXIT_CODE=$_TEST_EXIT_CODE\
217+
##TEST_HOOK_FAILURE=$_TEST_HOOK_FAILURE\
218+
##TEST_HOOK_MESSAGE=$encoded_test_hook_message\
185219
##TEST_TITLE=$encoded_test_title\
186220
##TEST_OUTPUT=$encoded_test_output\
187221
##

0 commit comments

Comments
 (0)