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
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ BASHUNIT_FAIL_ON_RISKY= # Default: false (treat no-assertion tests a
BASHUNIT_PROFILE= # Default: false (report slowest tests after a run)
BASHUNIT_PROFILE_COUNT= # Default: 10 (how many slowest tests to report)
BASHUNIT_NO_COLOR= # Default: false (disable colors)
BASHUNIT_NO_DIFF= # Default: false (disable unified diff on multiline assert failures)

#───────────────────────────────────────────────────────────────────────────────
# Test Execution
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
- `--jobs auto` (and `-j auto`) caps parallel concurrency at the detected CPU core count, portable across Linux/macOS/BSD (`nproc`, then `sysctl`, then `getconf`, falling back to 4). The default stays unlimited (`--jobs 0`) (#766)

### Changed
- `assert_equals`/`assert_same` failures with multiline values now render a git word-diff below the `Expected/but got` header, making differences in command output or file contents easy to spot (the inline values are truncated to their first line while the diff is shown). Requires git; single-line failures are unchanged. Opt out with `BASHUNIT_NO_DIFF=true`, and it respects `--no-color`. Machine reports (JUnit/TAP/JSON) keep the raw values (#777)
- Faster test execution: `assert_match_snapshot` no longer forks on the success path. Carriage-return stripping, the test-function-name and snapshot-path resolution, and the snapshot-file read are now pure-bash (return slots + `$(<file)`), and `assert_match_snapshot_ignore_colors` only forks `sed` when the input actually contains an escape sequence. On-disk `.snapshot` files stay byte-compatible. 500 matching snapshot assertions ~7.5s -> ~3.0s on bash 3.2. No behaviour change (#775)
- Faster test execution with `--tag`/`--exclude-tag`: each test file's `# @tag` annotations are scanned once in a single `awk` pass and cached, replacing a per-function backward walk that forked `grep`+`sed` on every comment line (and ran twice per function). 100 tagged tests with `--tag` ~2.92s -> ~0.68s on bash 3.2. No behaviour change (#773)
- Faster test execution: removed the remaining `run_test` hot-path forks. Test-label normalization and the per-test clock capture use fork-free return slots (`bashunit::clock::now_to_slot` reads `EPOCHREALTIME` directly on Bash 5.0+), parallel mode is resolved once instead of re-checking env + OS on every `is_enabled` call, and the `no-parallel-tests` opt-out is detected in the existing single-pass file scan rather than a separate per-file `grep`. No behaviour change (#774)
Expand Down
26 changes: 26 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -562,6 +562,32 @@ BASHUNIT_SHOW_OUTPUT_ON_FAILURE=false
```
:::

## Diff on multiline failures

> `BASHUNIT_NO_DIFF=true|false`

When an `assert_equals`/`assert_same` failure involves a multiline value,
bashunit renders a git word-diff below the `Expected/but got` header so the
difference is easy to spot; the inline values are truncated to their first line
while the diff is shown. Requires git (falls back to the plain output when it is
unavailable), respects `--no-color`, and never affects single-line failures or
machine reports (JUnit/TAP/JSON). `false` by default — set `BASHUNIT_NO_DIFF=true`
to always print the full raw values instead.

::: code-group
```[Output example]
✗ Failed: My test function
Expected 'alpha…'
but got 'alpha…'
alpha
[-beta-]{+DELTA+}
gamma
```
```bash [.env to disable]
BASHUNIT_NO_DIFF=true
```
:::

## Color output

> `NO_COLOR=1`
Expand Down
81 changes: 72 additions & 9 deletions src/console_results.sh
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,47 @@ ${_BASHUNIT_COLOR_BOLD}'%s'${_BASHUNIT_COLOR_DEFAULT}\n" \
bashunit::state::print_line "failure" "$line"
}

##
# Renders a git word-diff of two files, indented, and echoes it. Colorized
# unless --no-color is active. Empty when git is unavailable or files match.
# Shared by the snapshot-failure and multiline assert-failure renderers.
# Arguments: $1 expected file path, $2 actual file path
##
function bashunit::console_results::render_diff() {
local expected_file=$1
local actual_file=$2

if ! bashunit::dependencies::has_git; then
return 0
fi

local color_flag="--color=always"
if bashunit::env::is_no_color_enabled; then
color_flag="--color=never"
fi

# `git diff` exits non-zero when the files differ; the `|| true` keeps that
# from tripping `set -e`/`pipefail` under --strict. `tail -n +6` drops git's
# header lines; `sed` indents the body.
git diff --no-index --word-diff "$color_flag" \
"$expected_file" "$actual_file" 2>/dev/null |
tail -n +6 | sed "s/^/ /" || true
}

##
# Echoes a value's first line, appending an ellipsis when it spans several
# lines. Used to keep the inline quoted value on one line when a diff follows.
##
function bashunit::console_results::first_line_ellipsis() {
local text=$1
local first="${text%%$'\n'*}"
if [ "$first" != "$text" ]; then
printf '%s…' "$first"
else
printf '%s' "$text"
fi
}

function bashunit::console_results::print_failed_test() {
local function_name=$1
local expected=$2
Expand All @@ -310,12 +351,41 @@ function bashunit::console_results::print_failed_test() {
local extra_key=${5-}
local extra_value=${6-}

# For multiline values, render a unified diff below the header (git required,
# opt out with BASHUNIT_NO_DIFF). Single-line output stays byte-identical.
local show_diff=false
case "$expected$actual" in
*$'\n'*)
if bashunit::env::is_diff_enabled && bashunit::dependencies::has_git; then
show_diff=true
fi
;;
esac

local display_expected=$expected
local display_actual=$actual
if [ "$show_diff" = true ]; then
display_expected="$(bashunit::console_results::first_line_ellipsis "$expected")"
display_actual="$(bashunit::console_results::first_line_ellipsis "$actual")"
fi

local line
line="$(printf "\
${_BASHUNIT_COLOR_FAILED}✗ Failed${_BASHUNIT_COLOR_DEFAULT}: %s
${_BASHUNIT_COLOR_FAINT}Expected${_BASHUNIT_COLOR_DEFAULT} ${_BASHUNIT_COLOR_BOLD}'%s'${_BASHUNIT_COLOR_DEFAULT}
${_BASHUNIT_COLOR_FAINT}%s${_BASHUNIT_COLOR_DEFAULT} ${_BASHUNIT_COLOR_BOLD}'%s'${_BASHUNIT_COLOR_DEFAULT}\n" \
"${function_name}" "${expected}" "${failure_condition_message}" "${actual}")"
"${function_name}" "${display_expected}" "${failure_condition_message}" "${display_actual}")"

if [ "$show_diff" = true ]; then
local _expected_file _actual_file
_expected_file="$(bashunit::temp_file diff_expected)"
_actual_file="$(bashunit::temp_file diff_actual)"
printf '%s\n' "$expected" >"$_expected_file"
printf '%s\n' "$actual" >"$_actual_file"
line="$line
$(bashunit::console_results::render_diff "$_expected_file" "$_actual_file")"
rm -f "$_expected_file" "$_actual_file"
fi

if [ -n "$extra_key" ]; then
line="$line$(printf "\
Expand All @@ -342,14 +412,7 @@ function bashunit::console_results::print_failed_snapshot_test() {
local actual_file="${snapshot_file}.tmp"
echo "$actual_content" >"$actual_file"

# `git diff` exits non-zero when the files differ; guard with `|| true` so
# the assignment does not trip `set -e`/`pipefail` under --strict.
local git_diff_output
git_diff_output="$(git diff --no-index --word-diff --color=always \
"$snapshot_file" "$actual_file" 2>/dev/null |
tail -n +6 | sed "s/^/ /")" || true

line="$line$git_diff_output"
line="$line$(bashunit::console_results::render_diff "$snapshot_file" "$actual_file")"
rm "$actual_file"
else
line="$line$(bashunit::console_results::snapshot_line_diff \
Expand Down
6 changes: 6 additions & 0 deletions src/env.sh
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ _BASHUNIT_DEFAULT_SKIP_ENV_FILE="false"
_BASHUNIT_DEFAULT_LOGIN_SHELL="false"
_BASHUNIT_DEFAULT_FAILURES_ONLY="false"
_BASHUNIT_DEFAULT_NO_COLOR="false"
_BASHUNIT_DEFAULT_NO_DIFF="false"
_BASHUNIT_DEFAULT_SHOW_OUTPUT_ON_FAILURE="true"
_BASHUNIT_DEFAULT_NO_PROGRESS="false"
_BASHUNIT_DEFAULT_OUTPUT_FORMAT=""
Expand Down Expand Up @@ -151,6 +152,7 @@ _BASHUNIT_DEFAULT_SHARD_TOTAL=""
: "${BASHUNIT_LOGIN_SHELL:=${LOGIN_SHELL:=$_BASHUNIT_DEFAULT_LOGIN_SHELL}}"
: "${BASHUNIT_FAILURES_ONLY:=${FAILURES_ONLY:=$_BASHUNIT_DEFAULT_FAILURES_ONLY}}"
: "${BASHUNIT_SHOW_OUTPUT_ON_FAILURE:=${SHOW_OUTPUT_ON_FAILURE:=$_BASHUNIT_DEFAULT_SHOW_OUTPUT_ON_FAILURE}}"
: "${BASHUNIT_NO_DIFF:=${NO_DIFF:=$_BASHUNIT_DEFAULT_NO_DIFF}}"
: "${BASHUNIT_NO_PROGRESS:=${NO_PROGRESS:=$_BASHUNIT_DEFAULT_NO_PROGRESS}}"
: "${BASHUNIT_OUTPUT_FORMAT:=${OUTPUT_FORMAT:=$_BASHUNIT_DEFAULT_OUTPUT_FORMAT}}"
: "${BASHUNIT_FAIL_ON_RISKY:=${FAIL_ON_RISKY:=$_BASHUNIT_DEFAULT_FAIL_ON_RISKY}}"
Expand Down Expand Up @@ -329,6 +331,10 @@ function bashunit::env::is_no_color_enabled() {
[ "$BASHUNIT_NO_COLOR" = "true" ]
}

function bashunit::env::is_diff_enabled() {
[ "$BASHUNIT_NO_DIFF" != "true" ]
}

##
# Whether the current terminal can render ANSI color sequences.
# Returns 1 when TERM=dumb or when `tput colors` reports fewer than 8.
Expand Down
38 changes: 38 additions & 0 deletions tests/acceptance/bashunit_diff_test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#!/usr/bin/env bash
set -euo pipefail

function set_up_before_script() {
FIXTURE="./tests/acceptance/fixtures/diff/multiline_diff.sh"
}

function test_multiline_failure_renders_a_word_diff() {
if ! bashunit::dependencies::has_git; then
bashunit::skip "git not available" && return
fi

local output
output=$(./bashunit --no-parallel --no-color --skip-env-file --filter multiline "$FIXTURE" 2>&1) || true

assert_contains "[-beta-]{+DELTA+}" "$output"
assert_contains "alpha…" "$output"
}

function test_no_diff_env_keeps_full_multiline_values() {
local output
output=$(BASHUNIT_NO_DIFF=true ./bashunit --no-parallel --no-color --skip-env-file \
--filter multiline "$FIXTURE" 2>&1) || true

assert_not_contains "[-beta-]" "$output"
assert_not_contains "alpha…" "$output"
assert_contains "beta" "$output"
assert_contains "gamma" "$output"
}

function test_single_line_failure_stays_inline_without_diff() {
local output
output=$(./bashunit --no-parallel --no-color --skip-env-file --filter single_line "$FIXTURE" 2>&1) || true

assert_contains "'expected_value'" "$output"
assert_contains "'actual_value'" "$output"
assert_not_contains "[-" "$output"
}
9 changes: 9 additions & 0 deletions tests/acceptance/fixtures/diff/multiline_diff.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#!/usr/bin/env bash

function test_diff_multiline_mismatch() {
assert_same "$(printf 'alpha\nbeta\ngamma')" "$(printf 'alpha\nDELTA\ngamma')"
}

function test_diff_single_line_mismatch() {
assert_same "expected_value" "actual_value"
}
64 changes: 64 additions & 0 deletions tests/unit/console_results_diff_test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
#!/usr/bin/env bash
# shellcheck disable=SC2317

function tear_down() {
unset BASHUNIT_NO_DIFF
}

function test_first_line_ellipsis_leaves_single_line_untouched() {
assert_same "hello" "$(bashunit::console_results::first_line_ellipsis "hello")"
}

function test_first_line_ellipsis_truncates_multiline_to_first_line() {
local value
value=$(printf 'first\nsecond\nthird')
assert_same "first…" "$(bashunit::console_results::first_line_ellipsis "$value")"
}

function test_is_diff_enabled_by_default() {
unset BASHUNIT_NO_DIFF
local rc=0
bashunit::env::is_diff_enabled || rc=$?
assert_same 0 "$rc"
}

function test_is_diff_disabled_with_no_diff_env() {
export BASHUNIT_NO_DIFF=true
local rc=0
bashunit::env::is_diff_enabled || rc=$?
assert_same 1 "$rc"
}

function test_render_diff_is_empty_for_identical_files() {
if ! bashunit::dependencies::has_git; then
bashunit::skip "git not available" && return
fi
local a b
a=$(bashunit::temp_file diff_a)
b=$(bashunit::temp_file diff_b)
printf 'same\ncontent\n' >"$a"
printf 'same\ncontent\n' >"$b"

assert_empty "$(bashunit::console_results::render_diff "$a" "$b")"
rm -f "$a" "$b"
}

function test_render_diff_shows_changed_tokens_without_color() {
if ! bashunit::dependencies::has_git; then
bashunit::skip "git not available" && return
fi
export BASHUNIT_NO_COLOR=true
local a b
a=$(bashunit::temp_file diff_a)
b=$(bashunit::temp_file diff_b)
printf 'alpha\nbeta\ngamma\n' >"$a"
printf 'alpha\nDELTA\ngamma\n' >"$b"

local output
output=$(bashunit::console_results::render_diff "$a" "$b")

assert_contains "beta" "$output"
assert_contains "DELTA" "$output"
rm -f "$a" "$b"
unset BASHUNIT_NO_COLOR
}
Loading