Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## Unreleased

### Added
- Display captured test output on assertion failures when `--show-output` is enabled (#637)

## [0.35.0](https://github.com/TypedDevs/bashunit/compare/0.34.1...0.35.0) - 2026-04-26

### Added
Expand Down
7 changes: 4 additions & 3 deletions docs/command-line.md
Original file line number Diff line number Diff line change
Expand Up @@ -371,11 +371,12 @@ The `--log-gha` flag writes GitHub Actions workflow commands (`::error`, `::warn
> `bashunit test --show-output`
> `bashunit test --no-output-on-failure`

Control whether test output (stdout/stderr) is displayed when tests fail with runtime errors.
Control whether test output (stdout/stderr) is displayed when tests fail with runtime errors or assertion failures.

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

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

Expand Down
7 changes: 4 additions & 3 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -424,10 +424,11 @@ BASHUNIT_NO_PROGRESS=true

> `BASHUNIT_SHOW_OUTPUT_ON_FAILURE=true|false`

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

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

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

Expand Down
112 changes: 111 additions & 1 deletion src/runner.sh
Original file line number Diff line number Diff line change
Expand Up @@ -814,7 +814,12 @@ function bashunit::runner::run_test() {
if [ "$current_assertions_failed" != "$_BASHUNIT_ASSERTIONS_FAILED" ]; then
bashunit::state::add_tests_failed
bashunit::reports::add_test_failed "$test_file" "$label" "$duration" "$total_assertions" "$subshell_output"
bashunit::runner::write_failure_result_output "$test_file" "$fn_name" "$subshell_output"
local assertion_runtime_output
assertion_runtime_output="$(
bashunit::runner::extract_assertion_runtime_output "$runtime_output" "$subshell_output"
)"
bashunit::runner::write_failure_result_output \
"$test_file" "$fn_name" "$subshell_output" "$assertion_runtime_output"

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

Expand Down Expand Up @@ -949,6 +954,111 @@ function bashunit::runner::decode_subshell_output() {
bashunit::helper::decode_base64 "$test_output_base64"
}

function bashunit::runner::is_simple_progress_output() {
local output="$1"

[ -n "$output" ] || return 1

local color
for color in \
"$_BASHUNIT_COLOR_DEFAULT" \
"$_BASHUNIT_COLOR_PASSED" \
"$_BASHUNIT_COLOR_FAILED" \
"$_BASHUNIT_COLOR_SKIPPED" \
"$_BASHUNIT_COLOR_INCOMPLETE" \
"$_BASHUNIT_COLOR_SNAPSHOT" \
"$_BASHUNIT_COLOR_RISKY"; do
[ -n "$color" ] && output="${output//"$color"/}"
done

local i
local char
for ((i = 0; i < ${#output}; i++)); do
char="${output:$i:1}"
case "$char" in
"." | "F" | "S" | "I" | "N" | "R" | "E" | "?") ;;
*) return 1 ;;
esac
done

return 0
}

function bashunit::runner::line_starts_with_result_marker() {
local line="$1"
local marker
local -a result_markers
result_markers=(
"${_BASHUNIT_COLOR_PASSED}✓ Passed"
"${_BASHUNIT_COLOR_FAILED}✗ Failed"
"${_BASHUNIT_COLOR_FAILED}✗ Error"
"${_BASHUNIT_COLOR_SKIPPED}↷ Skipped"
"${_BASHUNIT_COLOR_INCOMPLETE}✒ Incomplete"
"${_BASHUNIT_COLOR_SNAPSHOT}✎ Snapshot"
"${_BASHUNIT_COLOR_RISKY}⚠ Risky"
"✓ Passed"
"✗ Failed"
"✗ Error"
"↷ Skipped"
"✒ Incomplete"
"✎ Snapshot"
"⚠ Risky"
)

for marker in "${result_markers[@]}"; do
case "$line" in
"$marker"*) return 0 ;;
esac
done

return 1
}

function bashunit::runner::line_exists_in_output() {
local needle="$1"
local haystack="$2"
local line

while IFS= read -r line || [ -n "$line" ]; do
[ "$line" = "$needle" ] && return 0
done <<<"$haystack"

return 1
}

function bashunit::runner::extract_assertion_runtime_output() {
local runtime_output="$1"
local rendered_assertion_output="$2"
local filtered_output=""
local line

while IFS= read -r line || [ -n "$line" ]; do
if bashunit::runner::line_exists_in_output "$line" "$rendered_assertion_output"; then
continue
fi
if bashunit::runner::is_simple_progress_output "$line"; then
continue
fi
if bashunit::runner::line_starts_with_result_marker "$line"; then
continue
fi

[ -n "$filtered_output" ] && filtered_output="$filtered_output"$'\n'
filtered_output="$filtered_output$line"
done <<<"$runtime_output"

runtime_output="$filtered_output"

while [ -n "$runtime_output" ]; do
case "$runtime_output" in
*$'\n') runtime_output="${runtime_output%$'\n'}" ;;
*) break ;;
esac
done

echo "$runtime_output"
}

function bashunit::runner::parse_result() {
local fn_name=$1
shift
Expand Down
20 changes: 20 additions & 0 deletions tests/acceptance/bashunit_show_output_on_failure_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,23 @@ function test_show_output_flag_overrides_env() {
assert_contains "Output:" "$actual"
assert_contains "Debug: Starting test" "$actual"
}

function test_show_output_on_assertion_failure_enabled_by_default() {
local test_file=./tests/acceptance/fixtures/test_bashunit_show_assertion_failure_output.sh

local actual
actual="$(./bashunit --no-parallel --env "$TEST_ENV_FILE" "$test_file" 2>&1 || true)"

assert_contains "Output:" "$actual"
assert_contains "function_being_tested requires at least 3 arguments." "$actual"
}

function test_show_output_on_assertion_failure_disabled_via_flag() {
local test_file=./tests/acceptance/fixtures/test_bashunit_show_assertion_failure_output.sh

local actual
actual="$(./bashunit --no-parallel --env "$TEST_ENV_FILE" --no-output-on-failure "$test_file" 2>&1 || true)"

assert_not_contains "Output:" "$actual"
assert_not_contains "function_being_tested requires at least 3 arguments." "$actual"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#!/usr/bin/env bash

function function_being_tested() {
if [ "$#" -lt 3 ]; then
echo "function_being_tested requires at least 3 arguments." >&2
return 1
fi

return 0
}

function test_assertion_failure_with_stderr_output() {
function_being_tested 1 2

assert_exit_code 0
}
37 changes: 37 additions & 0 deletions tests/unit/runner_test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#!/usr/bin/env bash

function test_extract_assertion_runtime_output_keeps_user_output() {
local runtime_output
runtime_output=$'diagnostic from stderr\n✗ Failed: Example\n Expected '\''1'\'''
local rendered_assertion_output
rendered_assertion_output=$'✗ Failed: Example\n Expected '\''1'\'''

local actual
actual="$(bashunit::runner::extract_assertion_runtime_output "$runtime_output" "$rendered_assertion_output")"

assert_same "diagnostic from stderr" "$actual"
}

function test_extract_assertion_runtime_output_ignores_bashunit_status_output_before_failure() {
local runtime_output
runtime_output=$'✒ Incomplete: Example pending\n✗ Failed: Example\n Expected '\''1'\'''
local rendered_assertion_output
rendered_assertion_output=$'✒ Incomplete: Example pending\n✗ Failed: Example\n Expected '\''1'\'''

local actual
actual="$(bashunit::runner::extract_assertion_runtime_output "$runtime_output" "$rendered_assertion_output")"

assert_empty "$actual"
}

function test_extract_assertion_runtime_output_keeps_user_output_after_status_output() {
local runtime_output
runtime_output=$'✓ Passed: Previous assertion\ndiagnostic after pass\n✗ Failed: Example'
local rendered_assertion_output
rendered_assertion_output=$'✓ Passed: Previous assertion\n✗ Failed: Example'

local actual
actual="$(bashunit::runner::extract_assertion_runtime_output "$runtime_output" "$rendered_assertion_output")"

assert_same "diagnostic after pass" "$actual"
}
Loading