Skip to content

Commit 02fa713

Browse files
committed
feat(runner): show output for assertion failures
Closes #637
1 parent 2015010 commit 02fa713

7 files changed

Lines changed: 195 additions & 7 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
## Unreleased
44

5+
### Added
6+
- Display captured test output on assertion failures when `--show-output` is enabled (#637)
7+
58
## [0.35.0](https://github.com/TypedDevs/bashunit/compare/0.34.1...0.35.0) - 2026-04-26
69

710
### Added

docs/command-line.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -371,11 +371,12 @@ The `--log-gha` flag writes GitHub Actions workflow commands (`::error`, `::warn
371371
> `bashunit test --show-output`
372372
> `bashunit test --no-output-on-failure`
373373
374-
Control whether test output (stdout/stderr) is displayed when tests fail with runtime errors.
374+
Control whether test output (stdout/stderr) is displayed when tests fail with runtime errors or assertion failures.
375375

376376
By default (`--show-output`), when a test fails due to a runtime error (command not found,
377-
unbound variable, permission denied, etc.), bashunit displays the captured output in an
378-
"Output:" section to help debug the failure.
377+
unbound variable, permission denied, etc.) or a failed assertion after the test printed
378+
diagnostics, bashunit displays the captured output in an "Output:" section to help debug
379+
the failure.
379380

380381
Use `--no-output-on-failure` to suppress this output.
381382

docs/configuration.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -424,10 +424,11 @@ BASHUNIT_NO_PROGRESS=true
424424

425425
> `BASHUNIT_SHOW_OUTPUT_ON_FAILURE=true|false`
426426

427-
Display captured stdout/stderr output when tests fail with runtime errors. `true` by default.
427+
Display captured stdout/stderr output when tests fail with runtime errors or assertion failures. `true` by default.
428428

429-
When a test fails due to a runtime error (command not found, unbound variable, etc.),
430-
bashunit displays the test's output in an "Output:" section to help debug the failure.
429+
When a test fails due to a runtime error (command not found, unbound variable, etc.) or
430+
a failed assertion after the test printed diagnostics, bashunit displays the test's output
431+
in an "Output:" section to help debug the failure.
431432

432433
Similar as using `--show-output` or `--no-output-on-failure` options on the [command line](/command-line#show-output-on-failure).
433434

src/runner.sh

Lines changed: 111 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -814,7 +814,12 @@ function bashunit::runner::run_test() {
814814
if [ "$current_assertions_failed" != "$_BASHUNIT_ASSERTIONS_FAILED" ]; then
815815
bashunit::state::add_tests_failed
816816
bashunit::reports::add_test_failed "$test_file" "$label" "$duration" "$total_assertions" "$subshell_output"
817-
bashunit::runner::write_failure_result_output "$test_file" "$fn_name" "$subshell_output"
817+
local assertion_runtime_output
818+
assertion_runtime_output="$(
819+
bashunit::runner::extract_assertion_runtime_output "$runtime_output" "$subshell_output"
820+
)"
821+
bashunit::runner::write_failure_result_output \
822+
"$test_file" "$fn_name" "$subshell_output" "$assertion_runtime_output"
818823

819824
bashunit::internal_log "Test failed" "$label"
820825

@@ -949,6 +954,111 @@ function bashunit::runner::decode_subshell_output() {
949954
bashunit::helper::decode_base64 "$test_output_base64"
950955
}
951956

957+
function bashunit::runner::is_simple_progress_output() {
958+
local output="$1"
959+
960+
[ -n "$output" ] || return 1
961+
962+
local color
963+
for color in \
964+
"$_BASHUNIT_COLOR_DEFAULT" \
965+
"$_BASHUNIT_COLOR_PASSED" \
966+
"$_BASHUNIT_COLOR_FAILED" \
967+
"$_BASHUNIT_COLOR_SKIPPED" \
968+
"$_BASHUNIT_COLOR_INCOMPLETE" \
969+
"$_BASHUNIT_COLOR_SNAPSHOT" \
970+
"$_BASHUNIT_COLOR_RISKY"; do
971+
[ -n "$color" ] && output="${output//"$color"/}"
972+
done
973+
974+
local i
975+
local char
976+
for ((i = 0; i < ${#output}; i++)); do
977+
char="${output:$i:1}"
978+
case "$char" in
979+
"." | "F" | "S" | "I" | "N" | "R" | "E" | "?") ;;
980+
*) return 1 ;;
981+
esac
982+
done
983+
984+
return 0
985+
}
986+
987+
function bashunit::runner::line_starts_with_result_marker() {
988+
local line="$1"
989+
local marker
990+
local -a result_markers
991+
result_markers=(
992+
"${_BASHUNIT_COLOR_PASSED}✓ Passed"
993+
"${_BASHUNIT_COLOR_FAILED}✗ Failed"
994+
"${_BASHUNIT_COLOR_FAILED}✗ Error"
995+
"${_BASHUNIT_COLOR_SKIPPED}↷ Skipped"
996+
"${_BASHUNIT_COLOR_INCOMPLETE}✒ Incomplete"
997+
"${_BASHUNIT_COLOR_SNAPSHOT}✎ Snapshot"
998+
"${_BASHUNIT_COLOR_RISKY}⚠ Risky"
999+
"✓ Passed"
1000+
"✗ Failed"
1001+
"✗ Error"
1002+
"↷ Skipped"
1003+
"✒ Incomplete"
1004+
"✎ Snapshot"
1005+
"⚠ Risky"
1006+
)
1007+
1008+
for marker in "${result_markers[@]}"; do
1009+
case "$line" in
1010+
"$marker"*) return 0 ;;
1011+
esac
1012+
done
1013+
1014+
return 1
1015+
}
1016+
1017+
function bashunit::runner::line_exists_in_output() {
1018+
local needle="$1"
1019+
local haystack="$2"
1020+
local line
1021+
1022+
while IFS= read -r line || [ -n "$line" ]; do
1023+
[ "$line" = "$needle" ] && return 0
1024+
done <<<"$haystack"
1025+
1026+
return 1
1027+
}
1028+
1029+
function bashunit::runner::extract_assertion_runtime_output() {
1030+
local runtime_output="$1"
1031+
local rendered_assertion_output="$2"
1032+
local filtered_output=""
1033+
local line
1034+
1035+
while IFS= read -r line || [ -n "$line" ]; do
1036+
if bashunit::runner::line_exists_in_output "$line" "$rendered_assertion_output"; then
1037+
continue
1038+
fi
1039+
if bashunit::runner::is_simple_progress_output "$line"; then
1040+
continue
1041+
fi
1042+
if bashunit::runner::line_starts_with_result_marker "$line"; then
1043+
continue
1044+
fi
1045+
1046+
[ -n "$filtered_output" ] && filtered_output="$filtered_output"$'\n'
1047+
filtered_output="$filtered_output$line"
1048+
done <<<"$runtime_output"
1049+
1050+
runtime_output="$filtered_output"
1051+
1052+
while [ -n "$runtime_output" ]; do
1053+
case "$runtime_output" in
1054+
*$'\n') runtime_output="${runtime_output%$'\n'}" ;;
1055+
*) break ;;
1056+
esac
1057+
done
1058+
1059+
echo "$runtime_output"
1060+
}
1061+
9521062
function bashunit::runner::parse_result() {
9531063
local fn_name=$1
9541064
shift

tests/acceptance/bashunit_show_output_on_failure_test.sh

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,23 @@ function test_show_output_flag_overrides_env() {
5151
assert_contains "Output:" "$actual"
5252
assert_contains "Debug: Starting test" "$actual"
5353
}
54+
55+
function test_show_output_on_assertion_failure_enabled_by_default() {
56+
local test_file=./tests/acceptance/fixtures/test_bashunit_show_assertion_failure_output.sh
57+
58+
local actual
59+
actual="$(./bashunit --no-parallel --env "$TEST_ENV_FILE" "$test_file" 2>&1 || true)"
60+
61+
assert_contains "Output:" "$actual"
62+
assert_contains "function_being_tested requires at least 3 arguments." "$actual"
63+
}
64+
65+
function test_show_output_on_assertion_failure_disabled_via_flag() {
66+
local test_file=./tests/acceptance/fixtures/test_bashunit_show_assertion_failure_output.sh
67+
68+
local actual
69+
actual="$(./bashunit --no-parallel --env "$TEST_ENV_FILE" --no-output-on-failure "$test_file" 2>&1 || true)"
70+
71+
assert_not_contains "Output:" "$actual"
72+
assert_not_contains "function_being_tested requires at least 3 arguments." "$actual"
73+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#!/usr/bin/env bash
2+
3+
function function_being_tested() {
4+
if [ "$#" -lt 3 ]; then
5+
echo "function_being_tested requires at least 3 arguments." >&2
6+
return 1
7+
fi
8+
9+
return 0
10+
}
11+
12+
function test_assertion_failure_with_stderr_output() {
13+
function_being_tested 1 2
14+
15+
assert_exit_code 0
16+
}

tests/unit/runner_test.sh

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
#!/usr/bin/env bash
2+
3+
function test_extract_assertion_runtime_output_keeps_user_output() {
4+
local runtime_output
5+
runtime_output=$'diagnostic from stderr\n✗ Failed: Example\n Expected '\''1'\'''
6+
local rendered_assertion_output
7+
rendered_assertion_output=$'✗ Failed: Example\n Expected '\''1'\'''
8+
9+
local actual
10+
actual="$(bashunit::runner::extract_assertion_runtime_output "$runtime_output" "$rendered_assertion_output")"
11+
12+
assert_same "diagnostic from stderr" "$actual"
13+
}
14+
15+
function test_extract_assertion_runtime_output_ignores_bashunit_status_output_before_failure() {
16+
local runtime_output
17+
runtime_output=$'✒ Incomplete: Example pending\n✗ Failed: Example\n Expected '\''1'\'''
18+
local rendered_assertion_output
19+
rendered_assertion_output=$'✒ Incomplete: Example pending\n✗ Failed: Example\n Expected '\''1'\'''
20+
21+
local actual
22+
actual="$(bashunit::runner::extract_assertion_runtime_output "$runtime_output" "$rendered_assertion_output")"
23+
24+
assert_empty "$actual"
25+
}
26+
27+
function test_extract_assertion_runtime_output_keeps_user_output_after_status_output() {
28+
local runtime_output
29+
runtime_output=$'✓ Passed: Previous assertion\ndiagnostic after pass\n✗ Failed: Example'
30+
local rendered_assertion_output
31+
rendered_assertion_output=$'✓ Passed: Previous assertion\n✗ Failed: Example'
32+
33+
local actual
34+
actual="$(bashunit::runner::extract_assertion_runtime_output "$runtime_output" "$rendered_assertion_output")"
35+
36+
assert_same "diagnostic after pass" "$actual"
37+
}

0 commit comments

Comments
 (0)