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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,6 +27,8 @@
- The `--parallel` unsupported-OS warning no longer claims Alpine is excluded

### Fixed
- A configured external git differ (`diff.external` / `GIT_EXTERNAL_DIFF`, e.g. difftastic) no longer blanks the multiline and snapshot failure diffs, which now render with `--no-ext-diff` (#912)
- Time reads and the JUnit report no longer set the locale with a temporary-environment prefix (`LC_ALL=C cmd`), which segfaults inside a command substitution on Bash 5.3.9 macOS (#912)
- Call assertions (`assert_not_called`, `assert_have_been_called*`) fail with `was never registered as a spy` instead of reporting zero calls when the name was never spied — a typo used to pass silently (#895)
- The per-argument form a spy records was written with a literal `$'\x1f'` separator instead of the byte, so it could not be compared against (#894)
- `--parallel` no longer discards worker stderr written outside a test body; it renders as a `Stderr from <file>` block (#864)
Expand Down
5 changes: 4 additions & 1 deletion src/clock.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -182,7 +182,10 @@ function bashunit::clock::now() {

function bashunit::clock::shell_time() {
# Get time directly from the shell variable EPOCHREALTIME (Bash 5+)
[ -n "${EPOCHREALTIME+x}" ] && [ -n "$EPOCHREALTIME" ] && LC_ALL=C echo "$EPOCHREALTIME"
# No `LC_ALL=C` prefix: the value is expanded before the temporary environment
# applies, so it cannot normalize the decimal separator (callers accept both),
# and that form segfaults inside `$()` on Bash 5.3 macOS (#912).
[ -n "${EPOCHREALTIME+x}" ] && [ -n "$EPOCHREALTIME" ] && echo "$EPOCHREALTIME"
}

function bashunit::clock::total_runtime_in_milliseconds() {
Expand Down
5 changes: 3 additions & 2 deletions src/console_results.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -312,8 +312,9 @@ function bashunit::console_results::render_diff() {

# `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" \
# header lines; `sed` indents the body. `--no-ext-diff` ignores a user's
# `diff.external`/`GIT_EXTERNAL_DIFF`, which would replace this word-diff.
git diff --no-index --no-ext-diff --word-diff "$color_flag" \
"$expected_file" "$actual_file" 2>/dev/null |
tail -n +6 | sed "s/^/ /" || true
}
Expand Down
6 changes: 4 additions & 2 deletions src/reports.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -102,7 +102,9 @@ function bashunit::reports::generate_junit_xml() {
local tests_failed=$(bashunit::state::get_tests_failed)
local time_ms=$(bashunit::clock::total_runtime_in_milliseconds)
local time
time=$(LC_ALL=C awk -v ms="$time_ms" 'BEGIN {printf "%.3f", ms/1000}')
# `env` rather than a bare `LC_ALL=C` prefix: C keeps awk's radix a dot for the
# XML, and that prefix form segfaults inside `$()` on Bash 5.3 macOS (#912).
time=$(env LC_ALL=C awk -v ms="$time_ms" 'BEGIN {printf "%.3f", ms/1000}')

{
echo "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
Expand All@@ -120,7 +122,7 @@ function bashunit::reports::generate_junit_xml() {
local test_time_ms="${_BASHUNIT_REPORTS_TEST_DURATIONS[$i]:-}"
local failure_message="${_BASHUNIT_REPORTS_TEST_FAILURES[$i]:-}"
local test_time
test_time=$(LC_ALL=C awk -v ms="$test_time_ms" 'BEGIN {printf "%.3f", ms/1000}')
test_time=$(env LC_ALL=C awk -v ms="$test_time_ms" 'BEGIN {printf "%.3f", ms/1000}')

echo " <testcase file=\"$file\""
echo " name=\"$name\""
Expand Down
10 changes: 5 additions & 5 deletions tests/acceptance/bashunit_init_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,23 +15,23 @@ function tear_down() {

function test_bashunit_init_creates_structure() {
pushd "$TMP_DIR" >/dev/null
"$BASHUNIT_PATH" init >/tmp/init.log
"$BASHUNIT_PATH" init >"$TMP_DIR/init.log"
assert_file_exists "tests/example_test.sh"
assert_file_exists "tests/bootstrap.sh"
popd >/dev/null
}

function test_bashunit_init_custom_directory() {
pushd "$TMP_DIR" >/dev/null
"$BASHUNIT_PATH" init custom >/tmp/init.log
"$BASHUNIT_PATH" init custom >"$TMP_DIR/init.log"
assert_file_exists "custom/example_test.sh"
assert_file_exists "custom/bootstrap.sh"
popd >/dev/null
}

function test_bashunit_init_creates_github_workflow() {
pushd "$TMP_DIR" >/dev/null
"$BASHUNIT_PATH" init >/tmp/init.log
"$BASHUNIT_PATH" init >"$TMP_DIR/init.log"
assert_file_exists ".github/workflows/tests.yml"
assert_file_contains ".github/workflows/tests.yml" "TypedDevs/bashunit@"
popd >/dev/null
Expand All@@ -41,7 +41,7 @@ function test_bashunit_init_does_not_overwrite_existing_workflow() {
pushd "$TMP_DIR" >/dev/null
mkdir -p ".github/workflows"
echo "custom-workflow" >".github/workflows/tests.yml"
"$BASHUNIT_PATH" init >/tmp/init.log
"$BASHUNIT_PATH" init >"$TMP_DIR/init.log"
assert_file_contains ".github/workflows/tests.yml" "custom-workflow"
popd >/dev/null
}
Expand All@@ -51,7 +51,7 @@ function test_bashunit_init_updates_env() {

pushd "$TMP_DIR" >/dev/null
echo "BASHUNIT_BOOTSTRAP=old/bootstrap.sh" >.env
"$BASHUNIT_PATH" init custom >/tmp/init.log
"$BASHUNIT_PATH" init custom >"$TMP_DIR/init.log"
assert_file_exists "custom/example_test.sh"
assert_file_exists "custom/bootstrap.sh"
assert_file_contains .env "#BASHUNIT_BOOTSTRAP=old/bootstrap.sh"
Expand Down
4 changes: 2 additions & 2 deletions tests/acceptance/bashunit_syntax_error_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,7 +10,7 @@ function test_bashunit_when_test_file_has_syntax_error() {

local actual_raw
set +e
actual_raw="$(LC_ALL=C LANG=C ./bashunit \
actual_raw="$(./bashunit \
--no-parallel --detailed --env "$TEST_ENV_FILE" "$test_file" 2>&1)"
set -e

Expand All@@ -19,6 +19,6 @@ function test_bashunit_when_test_file_has_syntax_error() {

assert_contains "failed" "$actual"
assert_contains "Error" "$actual"
assert_general_error "$(LC_ALL=C LANG=C ./bashunit \
assert_general_error "$(./bashunit \
--no-parallel --env "$TEST_ENV_FILE" "$test_file" 2>&1)"
}
11 changes: 11 additions & 0 deletions tests/unit/bash_compatibility_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -102,3 +102,14 @@ function test_src_has_no_coproc() {
function test_src_has_no_parameter_transformations() {
assert_empty "$(bashunit::compat::offenders '\$\{[A-Za-z_][A-Za-z0-9_]*@[QEPAKa]\}')"
}

# A temporary-environment locale prefix (`LC_ALL=C cmd`) makes bash change its
# own locale for that command. Bash 5.3.9 on macOS segfaults on that form inside
# a command substitution -- `x=$(LC_ALL=C echo hi)` exits 139 (#912) -- and no CI
# job runs that build. Use `env LC_ALL=C cmd` instead, which passes the locale
# straight to the child and never touches bash's own.
function test_src_has_no_temporary_locale_assignment_prefix() {
local pattern='(^|[;&|(])[[:space:]]*((LC_[A-Z_]+|LANG)=[^[:space:]]*[[:space:]]+)+[^[:space:]=]'

assert_empty "$(bashunit::compat::offenders "$pattern")"
}
23 changes: 23 additions & 0 deletions tests/unit/console_results_diff_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -62,3 +62,26 @@ function test_render_diff_shows_changed_tokens_without_color() {
rm -f "$a" "$b"
unset BASHUNIT_NO_COLOR
}

function test_render_diff_ignores_a_configured_external_diff() {
if ! bashunit::dependencies::has_git; then
bashunit::skip "git not available" && return
fi
export BASHUNIT_NO_COLOR=true
# An external differ (difftastic in #912) replaces git's own output, so without
# --no-ext-diff the rendered diff is whatever it prints -- here nothing at all.
export GIT_EXTERNAL_DIFF=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 GIT_EXTERNAL_DIFF
}
1 change: 1 addition & 0 deletions tests/unit/console_results_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -363,6 +363,7 @@ function test_render_execution_time_on_osx_with_perl() {

local render_result
mock_macos
_BASHUNIT_CLOCK_NOW_IMPL="perl"
bashunit::mock bashunit::dependencies::has_perl mock_true
_BASHUNIT_START_TIME="1726393394574382186"
bashunit::mock perl <<<"1726393394574372186"
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,6 +27,8 @@
- The `--parallel` unsupported-OS warning no longer claims Alpine is excluded

### Fixed
- A configured external git differ (`diff.external` / `GIT_EXTERNAL_DIFF`, e.g. difftastic) no longer blanks the multiline and snapshot failure diffs, which now render with `--no-ext-diff` (#912)
- Time reads and the JUnit report no longer set the locale with a temporary-environment prefix (`LC_ALL=C cmd`), which segfaults inside a command substitution on Bash 5.3.9 macOS (#912)
- Call assertions (`assert_not_called`, `assert_have_been_called*`) fail with `was never registered as a spy` instead of reporting zero calls when the name was never spied — a typo used to pass silently (#895)
- The per-argument form a spy records was written with a literal `$'\x1f'` separator instead of the byte, so it could not be compared against (#894)
- `--parallel` no longer discards worker stderr written outside a test body; it renders as a `Stderr from <file>` block (#864)
Expand Down
5 changes: 4 additions & 1 deletion src/clock.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -182,7 +182,10 @@ function bashunit::clock::now() {

function bashunit::clock::shell_time() {
# Get time directly from the shell variable EPOCHREALTIME (Bash 5+)
[ -n "${EPOCHREALTIME+x}" ] && [ -n "$EPOCHREALTIME" ] && LC_ALL=C echo "$EPOCHREALTIME"
# No `LC_ALL=C` prefix: the value is expanded before the temporary environment
# applies, so it cannot normalize the decimal separator (callers accept both),
# and that form segfaults inside `$()` on Bash 5.3 macOS (#912).
[ -n "${EPOCHREALTIME+x}" ] && [ -n "$EPOCHREALTIME" ] && echo "$EPOCHREALTIME"
}

function bashunit::clock::total_runtime_in_milliseconds() {
Expand Down
5 changes: 3 additions & 2 deletions src/console_results.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -312,8 +312,9 @@ function bashunit::console_results::render_diff() {

# `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" \
# header lines; `sed` indents the body. `--no-ext-diff` ignores a user's
# `diff.external`/`GIT_EXTERNAL_DIFF`, which would replace this word-diff.
git diff --no-index --no-ext-diff --word-diff "$color_flag" \
"$expected_file" "$actual_file" 2>/dev/null |
tail -n +6 | sed "s/^/ /" || true
}
Expand Down
6 changes: 4 additions & 2 deletions src/reports.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -102,7 +102,9 @@ function bashunit::reports::generate_junit_xml() {
local tests_failed=$(bashunit::state::get_tests_failed)
local time_ms=$(bashunit::clock::total_runtime_in_milliseconds)
local time
time=$(LC_ALL=C awk -v ms="$time_ms" 'BEGIN {printf "%.3f", ms/1000}')
# `env` rather than a bare `LC_ALL=C` prefix: C keeps awk's radix a dot for the
# XML, and that prefix form segfaults inside `$()` on Bash 5.3 macOS (#912).
time=$(env LC_ALL=C awk -v ms="$time_ms" 'BEGIN {printf "%.3f", ms/1000}')

{
echo "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
Expand All@@ -120,7 +122,7 @@ function bashunit::reports::generate_junit_xml() {
local test_time_ms="${_BASHUNIT_REPORTS_TEST_DURATIONS[$i]:-}"
local failure_message="${_BASHUNIT_REPORTS_TEST_FAILURES[$i]:-}"
local test_time
test_time=$(LC_ALL=C awk -v ms="$test_time_ms" 'BEGIN {printf "%.3f", ms/1000}')
test_time=$(env LC_ALL=C awk -v ms="$test_time_ms" 'BEGIN {printf "%.3f", ms/1000}')

echo " <testcase file=\"$file\""
echo " name=\"$name\""
Expand Down
10 changes: 5 additions & 5 deletions tests/acceptance/bashunit_init_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,23 +15,23 @@ function tear_down() {

function test_bashunit_init_creates_structure() {
pushd "$TMP_DIR" >/dev/null
"$BASHUNIT_PATH" init >/tmp/init.log
"$BASHUNIT_PATH" init >"$TMP_DIR/init.log"
assert_file_exists "tests/example_test.sh"
assert_file_exists "tests/bootstrap.sh"
popd >/dev/null
}

function test_bashunit_init_custom_directory() {
pushd "$TMP_DIR" >/dev/null
"$BASHUNIT_PATH" init custom >/tmp/init.log
"$BASHUNIT_PATH" init custom >"$TMP_DIR/init.log"
assert_file_exists "custom/example_test.sh"
assert_file_exists "custom/bootstrap.sh"
popd >/dev/null
}

function test_bashunit_init_creates_github_workflow() {
pushd "$TMP_DIR" >/dev/null
"$BASHUNIT_PATH" init >/tmp/init.log
"$BASHUNIT_PATH" init >"$TMP_DIR/init.log"
assert_file_exists ".github/workflows/tests.yml"
assert_file_contains ".github/workflows/tests.yml" "TypedDevs/bashunit@"
popd >/dev/null
Expand All@@ -41,7 +41,7 @@ function test_bashunit_init_does_not_overwrite_existing_workflow() {
pushd "$TMP_DIR" >/dev/null
mkdir -p ".github/workflows"
echo "custom-workflow" >".github/workflows/tests.yml"
"$BASHUNIT_PATH" init >/tmp/init.log
"$BASHUNIT_PATH" init >"$TMP_DIR/init.log"
assert_file_contains ".github/workflows/tests.yml" "custom-workflow"
popd >/dev/null
}
Expand All@@ -51,7 +51,7 @@ function test_bashunit_init_updates_env() {

pushd "$TMP_DIR" >/dev/null
echo "BASHUNIT_BOOTSTRAP=old/bootstrap.sh" >.env
"$BASHUNIT_PATH" init custom >/tmp/init.log
"$BASHUNIT_PATH" init custom >"$TMP_DIR/init.log"
assert_file_exists "custom/example_test.sh"
assert_file_exists "custom/bootstrap.sh"
assert_file_contains .env "#BASHUNIT_BOOTSTRAP=old/bootstrap.sh"
Expand Down
4 changes: 2 additions & 2 deletions tests/acceptance/bashunit_syntax_error_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,7 +10,7 @@ function test_bashunit_when_test_file_has_syntax_error() {

local actual_raw
set +e
actual_raw="$(LC_ALL=C LANG=C ./bashunit \
actual_raw="$(./bashunit \
--no-parallel --detailed --env "$TEST_ENV_FILE" "$test_file" 2>&1)"
set -e

Expand All@@ -19,6 +19,6 @@ function test_bashunit_when_test_file_has_syntax_error() {

assert_contains "failed" "$actual"
assert_contains "Error" "$actual"
assert_general_error "$(LC_ALL=C LANG=C ./bashunit \
assert_general_error "$(./bashunit \
--no-parallel --env "$TEST_ENV_FILE" "$test_file" 2>&1)"
}
11 changes: 11 additions & 0 deletions tests/unit/bash_compatibility_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -102,3 +102,14 @@ function test_src_has_no_coproc() {
function test_src_has_no_parameter_transformations() {
assert_empty "$(bashunit::compat::offenders '\$\{[A-Za-z_][A-Za-z0-9_]*@[QEPAKa]\}')"
}

# A temporary-environment locale prefix (`LC_ALL=C cmd`) makes bash change its
# own locale for that command. Bash 5.3.9 on macOS segfaults on that form inside
# a command substitution -- `x=$(LC_ALL=C echo hi)` exits 139 (#912) -- and no CI
# job runs that build. Use `env LC_ALL=C cmd` instead, which passes the locale
# straight to the child and never touches bash's own.
function test_src_has_no_temporary_locale_assignment_prefix() {
local pattern='(^|[;&|(])[[:space:]]*((LC_[A-Z_]+|LANG)=[^[:space:]]*[[:space:]]+)+[^[:space:]=]'

assert_empty "$(bashunit::compat::offenders "$pattern")"
}
23 changes: 23 additions & 0 deletions tests/unit/console_results_diff_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -62,3 +62,26 @@ function test_render_diff_shows_changed_tokens_without_color() {
rm -f "$a" "$b"
unset BASHUNIT_NO_COLOR
}

function test_render_diff_ignores_a_configured_external_diff() {
if ! bashunit::dependencies::has_git; then
bashunit::skip "git not available" && return
fi
export BASHUNIT_NO_COLOR=true
# An external differ (difftastic in #912) replaces git's own output, so without
# --no-ext-diff the rendered diff is whatever it prints -- here nothing at all.
export GIT_EXTERNAL_DIFF=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 GIT_EXTERNAL_DIFF
}
1 change: 1 addition & 0 deletions tests/unit/console_results_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -363,6 +363,7 @@ function test_render_execution_time_on_osx_with_perl() {

local render_result
mock_macos
_BASHUNIT_CLOCK_NOW_IMPL="perl"
bashunit::mock bashunit::dependencies::has_perl mock_true
_BASHUNIT_START_TIME="1726393394574382186"
bashunit::mock perl <<<"1726393394574372186"
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,6 +27,8 @@
- The `--parallel` unsupported-OS warning no longer claims Alpine is excluded

### Fixed
- A configured external git differ (`diff.external` / `GIT_EXTERNAL_DIFF`, e.g. difftastic) no longer blanks the multiline and snapshot failure diffs, which now render with `--no-ext-diff` (#912)
- Time reads and the JUnit report no longer set the locale with a temporary-environment prefix (`LC_ALL=C cmd`), which segfaults inside a command substitution on Bash 5.3.9 macOS (#912)
- Call assertions (`assert_not_called`, `assert_have_been_called*`) fail with `was never registered as a spy` instead of reporting zero calls when the name was never spied — a typo used to pass silently (#895)
- The per-argument form a spy records was written with a literal `$'\x1f'` separator instead of the byte, so it could not be compared against (#894)
- `--parallel` no longer discards worker stderr written outside a test body; it renders as a `Stderr from <file>` block (#864)
Expand Down
5 changes: 4 additions & 1 deletion src/clock.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -182,7 +182,10 @@ function bashunit::clock::now() {

function bashunit::clock::shell_time() {
# Get time directly from the shell variable EPOCHREALTIME (Bash 5+)
[ -n "${EPOCHREALTIME+x}" ] && [ -n "$EPOCHREALTIME" ] && LC_ALL=C echo "$EPOCHREALTIME"
# No `LC_ALL=C` prefix: the value is expanded before the temporary environment
# applies, so it cannot normalize the decimal separator (callers accept both),
# and that form segfaults inside `$()` on Bash 5.3 macOS (#912).
[ -n "${EPOCHREALTIME+x}" ] && [ -n "$EPOCHREALTIME" ] && echo "$EPOCHREALTIME"
}

function bashunit::clock::total_runtime_in_milliseconds() {
Expand Down
5 changes: 3 additions & 2 deletions src/console_results.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -312,8 +312,9 @@ function bashunit::console_results::render_diff() {

# `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" \
# header lines; `sed` indents the body. `--no-ext-diff` ignores a user's
# `diff.external`/`GIT_EXTERNAL_DIFF`, which would replace this word-diff.
git diff --no-index --no-ext-diff --word-diff "$color_flag" \
"$expected_file" "$actual_file" 2>/dev/null |
tail -n +6 | sed "s/^/ /" || true
}
Expand Down
6 changes: 4 additions & 2 deletions src/reports.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -102,7 +102,9 @@ function bashunit::reports::generate_junit_xml() {
local tests_failed=$(bashunit::state::get_tests_failed)
local time_ms=$(bashunit::clock::total_runtime_in_milliseconds)
local time
time=$(LC_ALL=C awk -v ms="$time_ms" 'BEGIN {printf "%.3f", ms/1000}')
# `env` rather than a bare `LC_ALL=C` prefix: C keeps awk's radix a dot for the
# XML, and that prefix form segfaults inside `$()` on Bash 5.3 macOS (#912).
time=$(env LC_ALL=C awk -v ms="$time_ms" 'BEGIN {printf "%.3f", ms/1000}')

{
echo "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
Expand All@@ -120,7 +122,7 @@ function bashunit::reports::generate_junit_xml() {
local test_time_ms="${_BASHUNIT_REPORTS_TEST_DURATIONS[$i]:-}"
local failure_message="${_BASHUNIT_REPORTS_TEST_FAILURES[$i]:-}"
local test_time
test_time=$(LC_ALL=C awk -v ms="$test_time_ms" 'BEGIN {printf "%.3f", ms/1000}')
test_time=$(env LC_ALL=C awk -v ms="$test_time_ms" 'BEGIN {printf "%.3f", ms/1000}')

echo " <testcase file=\"$file\""
echo " name=\"$name\""
Expand Down
10 changes: 5 additions & 5 deletions tests/acceptance/bashunit_init_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,23 +15,23 @@ function tear_down() {

function test_bashunit_init_creates_structure() {
pushd "$TMP_DIR" >/dev/null
"$BASHUNIT_PATH" init >/tmp/init.log
"$BASHUNIT_PATH" init >"$TMP_DIR/init.log"
assert_file_exists "tests/example_test.sh"
assert_file_exists "tests/bootstrap.sh"
popd >/dev/null
}

function test_bashunit_init_custom_directory() {
pushd "$TMP_DIR" >/dev/null
"$BASHUNIT_PATH" init custom >/tmp/init.log
"$BASHUNIT_PATH" init custom >"$TMP_DIR/init.log"
assert_file_exists "custom/example_test.sh"
assert_file_exists "custom/bootstrap.sh"
popd >/dev/null
}

function test_bashunit_init_creates_github_workflow() {
pushd "$TMP_DIR" >/dev/null
"$BASHUNIT_PATH" init >/tmp/init.log
"$BASHUNIT_PATH" init >"$TMP_DIR/init.log"
assert_file_exists ".github/workflows/tests.yml"
assert_file_contains ".github/workflows/tests.yml" "TypedDevs/bashunit@"
popd >/dev/null
Expand All@@ -41,7 +41,7 @@ function test_bashunit_init_does_not_overwrite_existing_workflow() {
pushd "$TMP_DIR" >/dev/null
mkdir -p ".github/workflows"
echo "custom-workflow" >".github/workflows/tests.yml"
"$BASHUNIT_PATH" init >/tmp/init.log
"$BASHUNIT_PATH" init >"$TMP_DIR/init.log"
assert_file_contains ".github/workflows/tests.yml" "custom-workflow"
popd >/dev/null
}
Expand All@@ -51,7 +51,7 @@ function test_bashunit_init_updates_env() {

pushd "$TMP_DIR" >/dev/null
echo "BASHUNIT_BOOTSTRAP=old/bootstrap.sh" >.env
"$BASHUNIT_PATH" init custom >/tmp/init.log
"$BASHUNIT_PATH" init custom >"$TMP_DIR/init.log"
assert_file_exists "custom/example_test.sh"
assert_file_exists "custom/bootstrap.sh"
assert_file_contains .env "#BASHUNIT_BOOTSTRAP=old/bootstrap.sh"
Expand Down
4 changes: 2 additions & 2 deletions tests/acceptance/bashunit_syntax_error_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,7 +10,7 @@ function test_bashunit_when_test_file_has_syntax_error() {

local actual_raw
set +e
actual_raw="$(LC_ALL=C LANG=C ./bashunit \
actual_raw="$(./bashunit \
--no-parallel --detailed --env "$TEST_ENV_FILE" "$test_file" 2>&1)"
set -e

Expand All@@ -19,6 +19,6 @@ function test_bashunit_when_test_file_has_syntax_error() {

assert_contains "failed" "$actual"
assert_contains "Error" "$actual"
assert_general_error "$(LC_ALL=C LANG=C ./bashunit \
assert_general_error "$(./bashunit \
--no-parallel --env "$TEST_ENV_FILE" "$test_file" 2>&1)"
}
11 changes: 11 additions & 0 deletions tests/unit/bash_compatibility_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -102,3 +102,14 @@ function test_src_has_no_coproc() {
function test_src_has_no_parameter_transformations() {
assert_empty "$(bashunit::compat::offenders '\$\{[A-Za-z_][A-Za-z0-9_]*@[QEPAKa]\}')"
}

# A temporary-environment locale prefix (`LC_ALL=C cmd`) makes bash change its
# own locale for that command. Bash 5.3.9 on macOS segfaults on that form inside
# a command substitution -- `x=$(LC_ALL=C echo hi)` exits 139 (#912) -- and no CI
# job runs that build. Use `env LC_ALL=C cmd` instead, which passes the locale
# straight to the child and never touches bash's own.
function test_src_has_no_temporary_locale_assignment_prefix() {
local pattern='(^|[;&|(])[[:space:]]*((LC_[A-Z_]+|LANG)=[^[:space:]]*[[:space:]]+)+[^[:space:]=]'

assert_empty "$(bashunit::compat::offenders "$pattern")"
}
23 changes: 23 additions & 0 deletions tests/unit/console_results_diff_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -62,3 +62,26 @@ function test_render_diff_shows_changed_tokens_without_color() {
rm -f "$a" "$b"
unset BASHUNIT_NO_COLOR
}

function test_render_diff_ignores_a_configured_external_diff() {
if ! bashunit::dependencies::has_git; then
bashunit::skip "git not available" && return
fi
export BASHUNIT_NO_COLOR=true
# An external differ (difftastic in #912) replaces git's own output, so without
# --no-ext-diff the rendered diff is whatever it prints -- here nothing at all.
export GIT_EXTERNAL_DIFF=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 GIT_EXTERNAL_DIFF
}
1 change: 1 addition & 0 deletions tests/unit/console_results_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -363,6 +363,7 @@ function test_render_execution_time_on_osx_with_perl() {

local render_result
mock_macos
_BASHUNIT_CLOCK_NOW_IMPL="perl"
bashunit::mock bashunit::dependencies::has_perl mock_true
_BASHUNIT_START_TIME="1726393394574382186"
bashunit::mock perl <<<"1726393394574372186"
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,6 +27,8 @@
- The `--parallel` unsupported-OS warning no longer claims Alpine is excluded

### Fixed
- A configured external git differ (`diff.external` / `GIT_EXTERNAL_DIFF`, e.g. difftastic) no longer blanks the multiline and snapshot failure diffs, which now render with `--no-ext-diff` (#912)
- Time reads and the JUnit report no longer set the locale with a temporary-environment prefix (`LC_ALL=C cmd`), which segfaults inside a command substitution on Bash 5.3.9 macOS (#912)
- Call assertions (`assert_not_called`, `assert_have_been_called*`) fail with `was never registered as a spy` instead of reporting zero calls when the name was never spied — a typo used to pass silently (#895)
- The per-argument form a spy records was written with a literal `$'\x1f'` separator instead of the byte, so it could not be compared against (#894)
- `--parallel` no longer discards worker stderr written outside a test body; it renders as a `Stderr from <file>` block (#864)
Expand Down
5 changes: 4 additions & 1 deletion src/clock.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -182,7 +182,10 @@ function bashunit::clock::now() {

function bashunit::clock::shell_time() {
# Get time directly from the shell variable EPOCHREALTIME (Bash 5+)
[ -n "${EPOCHREALTIME+x}" ] && [ -n "$EPOCHREALTIME" ] && LC_ALL=C echo "$EPOCHREALTIME"
# No `LC_ALL=C` prefix: the value is expanded before the temporary environment
# applies, so it cannot normalize the decimal separator (callers accept both),
# and that form segfaults inside `$()` on Bash 5.3 macOS (#912).
[ -n "${EPOCHREALTIME+x}" ] && [ -n "$EPOCHREALTIME" ] && echo "$EPOCHREALTIME"
}

function bashunit::clock::total_runtime_in_milliseconds() {
Expand Down
5 changes: 3 additions & 2 deletions src/console_results.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -312,8 +312,9 @@ function bashunit::console_results::render_diff() {

# `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" \
# header lines; `sed` indents the body. `--no-ext-diff` ignores a user's
# `diff.external`/`GIT_EXTERNAL_DIFF`, which would replace this word-diff.
git diff --no-index --no-ext-diff --word-diff "$color_flag" \
"$expected_file" "$actual_file" 2>/dev/null |
tail -n +6 | sed "s/^/ /" || true
}
Expand Down
6 changes: 4 additions & 2 deletions src/reports.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -102,7 +102,9 @@ function bashunit::reports::generate_junit_xml() {
local tests_failed=$(bashunit::state::get_tests_failed)
local time_ms=$(bashunit::clock::total_runtime_in_milliseconds)
local time
time=$(LC_ALL=C awk -v ms="$time_ms" 'BEGIN {printf "%.3f", ms/1000}')
# `env` rather than a bare `LC_ALL=C` prefix: C keeps awk's radix a dot for the
# XML, and that prefix form segfaults inside `$()` on Bash 5.3 macOS (#912).
time=$(env LC_ALL=C awk -v ms="$time_ms" 'BEGIN {printf "%.3f", ms/1000}')

{
echo "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
Expand All@@ -120,7 +122,7 @@ function bashunit::reports::generate_junit_xml() {
local test_time_ms="${_BASHUNIT_REPORTS_TEST_DURATIONS[$i]:-}"
local failure_message="${_BASHUNIT_REPORTS_TEST_FAILURES[$i]:-}"
local test_time
test_time=$(LC_ALL=C awk -v ms="$test_time_ms" 'BEGIN {printf "%.3f", ms/1000}')
test_time=$(env LC_ALL=C awk -v ms="$test_time_ms" 'BEGIN {printf "%.3f", ms/1000}')

echo " <testcase file=\"$file\""
echo " name=\"$name\""
Expand Down
10 changes: 5 additions & 5 deletions tests/acceptance/bashunit_init_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,23 +15,23 @@ function tear_down() {

function test_bashunit_init_creates_structure() {
pushd "$TMP_DIR" >/dev/null
"$BASHUNIT_PATH" init >/tmp/init.log
"$BASHUNIT_PATH" init >"$TMP_DIR/init.log"
assert_file_exists "tests/example_test.sh"
assert_file_exists "tests/bootstrap.sh"
popd >/dev/null
}

function test_bashunit_init_custom_directory() {
pushd "$TMP_DIR" >/dev/null
"$BASHUNIT_PATH" init custom >/tmp/init.log
"$BASHUNIT_PATH" init custom >"$TMP_DIR/init.log"
assert_file_exists "custom/example_test.sh"
assert_file_exists "custom/bootstrap.sh"
popd >/dev/null
}

function test_bashunit_init_creates_github_workflow() {
pushd "$TMP_DIR" >/dev/null
"$BASHUNIT_PATH" init >/tmp/init.log
"$BASHUNIT_PATH" init >"$TMP_DIR/init.log"
assert_file_exists ".github/workflows/tests.yml"
assert_file_contains ".github/workflows/tests.yml" "TypedDevs/bashunit@"
popd >/dev/null
Expand All@@ -41,7 +41,7 @@ function test_bashunit_init_does_not_overwrite_existing_workflow() {
pushd "$TMP_DIR" >/dev/null
mkdir -p ".github/workflows"
echo "custom-workflow" >".github/workflows/tests.yml"
"$BASHUNIT_PATH" init >/tmp/init.log
"$BASHUNIT_PATH" init >"$TMP_DIR/init.log"
assert_file_contains ".github/workflows/tests.yml" "custom-workflow"
popd >/dev/null
}
Expand All@@ -51,7 +51,7 @@ function test_bashunit_init_updates_env() {

pushd "$TMP_DIR" >/dev/null
echo "BASHUNIT_BOOTSTRAP=old/bootstrap.sh" >.env
"$BASHUNIT_PATH" init custom >/tmp/init.log
"$BASHUNIT_PATH" init custom >"$TMP_DIR/init.log"
assert_file_exists "custom/example_test.sh"
assert_file_exists "custom/bootstrap.sh"
assert_file_contains .env "#BASHUNIT_BOOTSTRAP=old/bootstrap.sh"
Expand Down
4 changes: 2 additions & 2 deletions tests/acceptance/bashunit_syntax_error_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,7 +10,7 @@ function test_bashunit_when_test_file_has_syntax_error() {

local actual_raw
set +e
actual_raw="$(LC_ALL=C LANG=C ./bashunit \
actual_raw="$(./bashunit \
--no-parallel --detailed --env "$TEST_ENV_FILE" "$test_file" 2>&1)"
set -e

Expand All@@ -19,6 +19,6 @@ function test_bashunit_when_test_file_has_syntax_error() {

assert_contains "failed" "$actual"
assert_contains "Error" "$actual"
assert_general_error "$(LC_ALL=C LANG=C ./bashunit \
assert_general_error "$(./bashunit \
--no-parallel --env "$TEST_ENV_FILE" "$test_file" 2>&1)"
}
11 changes: 11 additions & 0 deletions tests/unit/bash_compatibility_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -102,3 +102,14 @@ function test_src_has_no_coproc() {
function test_src_has_no_parameter_transformations() {
assert_empty "$(bashunit::compat::offenders '\$\{[A-Za-z_][A-Za-z0-9_]*@[QEPAKa]\}')"
}

# A temporary-environment locale prefix (`LC_ALL=C cmd`) makes bash change its
# own locale for that command. Bash 5.3.9 on macOS segfaults on that form inside
# a command substitution -- `x=$(LC_ALL=C echo hi)` exits 139 (#912) -- and no CI
# job runs that build. Use `env LC_ALL=C cmd` instead, which passes the locale
# straight to the child and never touches bash's own.
function test_src_has_no_temporary_locale_assignment_prefix() {
local pattern='(^|[;&|(])[[:space:]]*((LC_[A-Z_]+|LANG)=[^[:space:]]*[[:space:]]+)+[^[:space:]=]'

assert_empty "$(bashunit::compat::offenders "$pattern")"
}
23 changes: 23 additions & 0 deletions tests/unit/console_results_diff_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -62,3 +62,26 @@ function test_render_diff_shows_changed_tokens_without_color() {
rm -f "$a" "$b"
unset BASHUNIT_NO_COLOR
}

function test_render_diff_ignores_a_configured_external_diff() {
if ! bashunit::dependencies::has_git; then
bashunit::skip "git not available" && return
fi
export BASHUNIT_NO_COLOR=true
# An external differ (difftastic in #912) replaces git's own output, so without
# --no-ext-diff the rendered diff is whatever it prints -- here nothing at all.
export GIT_EXTERNAL_DIFF=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 GIT_EXTERNAL_DIFF
}
1 change: 1 addition & 0 deletions tests/unit/console_results_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -363,6 +363,7 @@ function test_render_execution_time_on_osx_with_perl() {

local render_result
mock_macos
_BASHUNIT_CLOCK_NOW_IMPL="perl"
bashunit::mock bashunit::dependencies::has_perl mock_true
_BASHUNIT_START_TIME="1726393394574382186"
bashunit::mock perl <<<"1726393394574372186"
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,6 +27,8 @@
- The `--parallel` unsupported-OS warning no longer claims Alpine is excluded

### Fixed
- A configured external git differ (`diff.external` / `GIT_EXTERNAL_DIFF`, e.g. difftastic) no longer blanks the multiline and snapshot failure diffs, which now render with `--no-ext-diff` (#912)
- Time reads and the JUnit report no longer set the locale with a temporary-environment prefix (`LC_ALL=C cmd`), which segfaults inside a command substitution on Bash 5.3.9 macOS (#912)
- Call assertions (`assert_not_called`, `assert_have_been_called*`) fail with `was never registered as a spy` instead of reporting zero calls when the name was never spied — a typo used to pass silently (#895)
- The per-argument form a spy records was written with a literal `$'\x1f'` separator instead of the byte, so it could not be compared against (#894)
- `--parallel` no longer discards worker stderr written outside a test body; it renders as a `Stderr from <file>` block (#864)
Expand Down
5 changes: 4 additions & 1 deletion src/clock.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -182,7 +182,10 @@ function bashunit::clock::now() {

function bashunit::clock::shell_time() {
# Get time directly from the shell variable EPOCHREALTIME (Bash 5+)
[ -n "${EPOCHREALTIME+x}" ] && [ -n "$EPOCHREALTIME" ] && LC_ALL=C echo "$EPOCHREALTIME"
# No `LC_ALL=C` prefix: the value is expanded before the temporary environment
# applies, so it cannot normalize the decimal separator (callers accept both),
# and that form segfaults inside `$()` on Bash 5.3 macOS (#912).
[ -n "${EPOCHREALTIME+x}" ] && [ -n "$EPOCHREALTIME" ] && echo "$EPOCHREALTIME"
}

function bashunit::clock::total_runtime_in_milliseconds() {
Expand Down
5 changes: 3 additions & 2 deletions src/console_results.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -312,8 +312,9 @@ function bashunit::console_results::render_diff() {

# `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" \
# header lines; `sed` indents the body. `--no-ext-diff` ignores a user's
# `diff.external`/`GIT_EXTERNAL_DIFF`, which would replace this word-diff.
git diff --no-index --no-ext-diff --word-diff "$color_flag" \
"$expected_file" "$actual_file" 2>/dev/null |
tail -n +6 | sed "s/^/ /" || true
}
Expand Down
6 changes: 4 additions & 2 deletions src/reports.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -102,7 +102,9 @@ function bashunit::reports::generate_junit_xml() {
local tests_failed=$(bashunit::state::get_tests_failed)
local time_ms=$(bashunit::clock::total_runtime_in_milliseconds)
local time
time=$(LC_ALL=C awk -v ms="$time_ms" 'BEGIN {printf "%.3f", ms/1000}')
# `env` rather than a bare `LC_ALL=C` prefix: C keeps awk's radix a dot for the
# XML, and that prefix form segfaults inside `$()` on Bash 5.3 macOS (#912).
time=$(env LC_ALL=C awk -v ms="$time_ms" 'BEGIN {printf "%.3f", ms/1000}')

{
echo "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
Expand All@@ -120,7 +122,7 @@ function bashunit::reports::generate_junit_xml() {
local test_time_ms="${_BASHUNIT_REPORTS_TEST_DURATIONS[$i]:-}"
local failure_message="${_BASHUNIT_REPORTS_TEST_FAILURES[$i]:-}"
local test_time
test_time=$(LC_ALL=C awk -v ms="$test_time_ms" 'BEGIN {printf "%.3f", ms/1000}')
test_time=$(env LC_ALL=C awk -v ms="$test_time_ms" 'BEGIN {printf "%.3f", ms/1000}')

echo " <testcase file=\"$file\""
echo " name=\"$name\""
Expand Down
10 changes: 5 additions & 5 deletions tests/acceptance/bashunit_init_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,23 +15,23 @@ function tear_down() {

function test_bashunit_init_creates_structure() {
pushd "$TMP_DIR" >/dev/null
"$BASHUNIT_PATH" init >/tmp/init.log
"$BASHUNIT_PATH" init >"$TMP_DIR/init.log"
assert_file_exists "tests/example_test.sh"
assert_file_exists "tests/bootstrap.sh"
popd >/dev/null
}

function test_bashunit_init_custom_directory() {
pushd "$TMP_DIR" >/dev/null
"$BASHUNIT_PATH" init custom >/tmp/init.log
"$BASHUNIT_PATH" init custom >"$TMP_DIR/init.log"
assert_file_exists "custom/example_test.sh"
assert_file_exists "custom/bootstrap.sh"
popd >/dev/null
}

function test_bashunit_init_creates_github_workflow() {
pushd "$TMP_DIR" >/dev/null
"$BASHUNIT_PATH" init >/tmp/init.log
"$BASHUNIT_PATH" init >"$TMP_DIR/init.log"
assert_file_exists ".github/workflows/tests.yml"
assert_file_contains ".github/workflows/tests.yml" "TypedDevs/bashunit@"
popd >/dev/null
Expand All@@ -41,7 +41,7 @@ function test_bashunit_init_does_not_overwrite_existing_workflow() {
pushd "$TMP_DIR" >/dev/null
mkdir -p ".github/workflows"
echo "custom-workflow" >".github/workflows/tests.yml"
"$BASHUNIT_PATH" init >/tmp/init.log
"$BASHUNIT_PATH" init >"$TMP_DIR/init.log"
assert_file_contains ".github/workflows/tests.yml" "custom-workflow"
popd >/dev/null
}
Expand All@@ -51,7 +51,7 @@ function test_bashunit_init_updates_env() {

pushd "$TMP_DIR" >/dev/null
echo "BASHUNIT_BOOTSTRAP=old/bootstrap.sh" >.env
"$BASHUNIT_PATH" init custom >/tmp/init.log
"$BASHUNIT_PATH" init custom >"$TMP_DIR/init.log"
assert_file_exists "custom/example_test.sh"
assert_file_exists "custom/bootstrap.sh"
assert_file_contains .env "#BASHUNIT_BOOTSTRAP=old/bootstrap.sh"
Expand Down
4 changes: 2 additions & 2 deletions tests/acceptance/bashunit_syntax_error_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,7 +10,7 @@ function test_bashunit_when_test_file_has_syntax_error() {

local actual_raw
set +e
actual_raw="$(LC_ALL=C LANG=C ./bashunit \
actual_raw="$(./bashunit \
--no-parallel --detailed --env "$TEST_ENV_FILE" "$test_file" 2>&1)"
set -e

Expand All@@ -19,6 +19,6 @@ function test_bashunit_when_test_file_has_syntax_error() {

assert_contains "failed" "$actual"
assert_contains "Error" "$actual"
assert_general_error "$(LC_ALL=C LANG=C ./bashunit \
assert_general_error "$(./bashunit \
--no-parallel --env "$TEST_ENV_FILE" "$test_file" 2>&1)"
}
11 changes: 11 additions & 0 deletions tests/unit/bash_compatibility_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -102,3 +102,14 @@ function test_src_has_no_coproc() {
function test_src_has_no_parameter_transformations() {
assert_empty "$(bashunit::compat::offenders '\$\{[A-Za-z_][A-Za-z0-9_]*@[QEPAKa]\}')"
}

# A temporary-environment locale prefix (`LC_ALL=C cmd`) makes bash change its
# own locale for that command. Bash 5.3.9 on macOS segfaults on that form inside
# a command substitution -- `x=$(LC_ALL=C echo hi)` exits 139 (#912) -- and no CI
# job runs that build. Use `env LC_ALL=C cmd` instead, which passes the locale
# straight to the child and never touches bash's own.
function test_src_has_no_temporary_locale_assignment_prefix() {
local pattern='(^|[;&|(])[[:space:]]*((LC_[A-Z_]+|LANG)=[^[:space:]]*[[:space:]]+)+[^[:space:]=]'

assert_empty "$(bashunit::compat::offenders "$pattern")"
}
23 changes: 23 additions & 0 deletions tests/unit/console_results_diff_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -62,3 +62,26 @@ function test_render_diff_shows_changed_tokens_without_color() {
rm -f "$a" "$b"
unset BASHUNIT_NO_COLOR
}

function test_render_diff_ignores_a_configured_external_diff() {
if ! bashunit::dependencies::has_git; then
bashunit::skip "git not available" && return
fi
export BASHUNIT_NO_COLOR=true
# An external differ (difftastic in #912) replaces git's own output, so without
# --no-ext-diff the rendered diff is whatever it prints -- here nothing at all.
export GIT_EXTERNAL_DIFF=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 GIT_EXTERNAL_DIFF
}
1 change: 1 addition & 0 deletions tests/unit/console_results_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -363,6 +363,7 @@ function test_render_execution_time_on_osx_with_perl() {

local render_result
mock_macos
_BASHUNIT_CLOCK_NOW_IMPL="perl"
bashunit::mock bashunit::dependencies::has_perl mock_true
_BASHUNIT_START_TIME="1726393394574382186"
bashunit::mock perl <<<"1726393394574372186"
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,6 +27,8 @@
- The `--parallel` unsupported-OS warning no longer claims Alpine is excluded

### Fixed
- A configured external git differ (`diff.external` / `GIT_EXTERNAL_DIFF`, e.g. difftastic) no longer blanks the multiline and snapshot failure diffs, which now render with `--no-ext-diff` (#912)
- Time reads and the JUnit report no longer set the locale with a temporary-environment prefix (`LC_ALL=C cmd`), which segfaults inside a command substitution on Bash 5.3.9 macOS (#912)
- Call assertions (`assert_not_called`, `assert_have_been_called*`) fail with `was never registered as a spy` instead of reporting zero calls when the name was never spied — a typo used to pass silently (#895)
- The per-argument form a spy records was written with a literal `$'\x1f'` separator instead of the byte, so it could not be compared against (#894)
- `--parallel` no longer discards worker stderr written outside a test body; it renders as a `Stderr from <file>` block (#864)
Expand Down
5 changes: 4 additions & 1 deletion src/clock.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -182,7 +182,10 @@ function bashunit::clock::now() {

function bashunit::clock::shell_time() {
# Get time directly from the shell variable EPOCHREALTIME (Bash 5+)
[ -n "${EPOCHREALTIME+x}" ] && [ -n "$EPOCHREALTIME" ] && LC_ALL=C echo "$EPOCHREALTIME"
# No `LC_ALL=C` prefix: the value is expanded before the temporary environment
# applies, so it cannot normalize the decimal separator (callers accept both),
# and that form segfaults inside `$()` on Bash 5.3 macOS (#912).
[ -n "${EPOCHREALTIME+x}" ] && [ -n "$EPOCHREALTIME" ] && echo "$EPOCHREALTIME"
}

function bashunit::clock::total_runtime_in_milliseconds() {
Expand Down
5 changes: 3 additions & 2 deletions src/console_results.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -312,8 +312,9 @@ function bashunit::console_results::render_diff() {

# `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" \
# header lines; `sed` indents the body. `--no-ext-diff` ignores a user's
# `diff.external`/`GIT_EXTERNAL_DIFF`, which would replace this word-diff.
git diff --no-index --no-ext-diff --word-diff "$color_flag" \
"$expected_file" "$actual_file" 2>/dev/null |
tail -n +6 | sed "s/^/ /" || true
}
Expand Down
6 changes: 4 additions & 2 deletions src/reports.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -102,7 +102,9 @@ function bashunit::reports::generate_junit_xml() {
local tests_failed=$(bashunit::state::get_tests_failed)
local time_ms=$(bashunit::clock::total_runtime_in_milliseconds)
local time
time=$(LC_ALL=C awk -v ms="$time_ms" 'BEGIN {printf "%.3f", ms/1000}')
# `env` rather than a bare `LC_ALL=C` prefix: C keeps awk's radix a dot for the
# XML, and that prefix form segfaults inside `$()` on Bash 5.3 macOS (#912).
time=$(env LC_ALL=C awk -v ms="$time_ms" 'BEGIN {printf "%.3f", ms/1000}')

{
echo "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
Expand All@@ -120,7 +122,7 @@ function bashunit::reports::generate_junit_xml() {
local test_time_ms="${_BASHUNIT_REPORTS_TEST_DURATIONS[$i]:-}"
local failure_message="${_BASHUNIT_REPORTS_TEST_FAILURES[$i]:-}"
local test_time
test_time=$(LC_ALL=C awk -v ms="$test_time_ms" 'BEGIN {printf "%.3f", ms/1000}')
test_time=$(env LC_ALL=C awk -v ms="$test_time_ms" 'BEGIN {printf "%.3f", ms/1000}')

echo " <testcase file=\"$file\""
echo " name=\"$name\""
Expand Down
10 changes: 5 additions & 5 deletions tests/acceptance/bashunit_init_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,23 +15,23 @@ function tear_down() {

function test_bashunit_init_creates_structure() {
pushd "$TMP_DIR" >/dev/null
"$BASHUNIT_PATH" init >/tmp/init.log
"$BASHUNIT_PATH" init >"$TMP_DIR/init.log"
assert_file_exists "tests/example_test.sh"
assert_file_exists "tests/bootstrap.sh"
popd >/dev/null
}

function test_bashunit_init_custom_directory() {
pushd "$TMP_DIR" >/dev/null
"$BASHUNIT_PATH" init custom >/tmp/init.log
"$BASHUNIT_PATH" init custom >"$TMP_DIR/init.log"
assert_file_exists "custom/example_test.sh"
assert_file_exists "custom/bootstrap.sh"
popd >/dev/null
}

function test_bashunit_init_creates_github_workflow() {
pushd "$TMP_DIR" >/dev/null
"$BASHUNIT_PATH" init >/tmp/init.log
"$BASHUNIT_PATH" init >"$TMP_DIR/init.log"
assert_file_exists ".github/workflows/tests.yml"
assert_file_contains ".github/workflows/tests.yml" "TypedDevs/bashunit@"
popd >/dev/null
Expand All@@ -41,7 +41,7 @@ function test_bashunit_init_does_not_overwrite_existing_workflow() {
pushd "$TMP_DIR" >/dev/null
mkdir -p ".github/workflows"
echo "custom-workflow" >".github/workflows/tests.yml"
"$BASHUNIT_PATH" init >/tmp/init.log
"$BASHUNIT_PATH" init >"$TMP_DIR/init.log"
assert_file_contains ".github/workflows/tests.yml" "custom-workflow"
popd >/dev/null
}
Expand All@@ -51,7 +51,7 @@ function test_bashunit_init_updates_env() {

pushd "$TMP_DIR" >/dev/null
echo "BASHUNIT_BOOTSTRAP=old/bootstrap.sh" >.env
"$BASHUNIT_PATH" init custom >/tmp/init.log
"$BASHUNIT_PATH" init custom >"$TMP_DIR/init.log"
assert_file_exists "custom/example_test.sh"
assert_file_exists "custom/bootstrap.sh"
assert_file_contains .env "#BASHUNIT_BOOTSTRAP=old/bootstrap.sh"
Expand Down
4 changes: 2 additions & 2 deletions tests/acceptance/bashunit_syntax_error_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,7 +10,7 @@ function test_bashunit_when_test_file_has_syntax_error() {

local actual_raw
set +e
actual_raw="$(LC_ALL=C LANG=C ./bashunit \
actual_raw="$(./bashunit \
--no-parallel --detailed --env "$TEST_ENV_FILE" "$test_file" 2>&1)"
set -e

Expand All@@ -19,6 +19,6 @@ function test_bashunit_when_test_file_has_syntax_error() {

assert_contains "failed" "$actual"
assert_contains "Error" "$actual"
assert_general_error "$(LC_ALL=C LANG=C ./bashunit \
assert_general_error "$(./bashunit \
--no-parallel --env "$TEST_ENV_FILE" "$test_file" 2>&1)"
}
11 changes: 11 additions & 0 deletions tests/unit/bash_compatibility_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -102,3 +102,14 @@ function test_src_has_no_coproc() {
function test_src_has_no_parameter_transformations() {
assert_empty "$(bashunit::compat::offenders '\$\{[A-Za-z_][A-Za-z0-9_]*@[QEPAKa]\}')"
}

# A temporary-environment locale prefix (`LC_ALL=C cmd`) makes bash change its
# own locale for that command. Bash 5.3.9 on macOS segfaults on that form inside
# a command substitution -- `x=$(LC_ALL=C echo hi)` exits 139 (#912) -- and no CI
# job runs that build. Use `env LC_ALL=C cmd` instead, which passes the locale
# straight to the child and never touches bash's own.
function test_src_has_no_temporary_locale_assignment_prefix() {
local pattern='(^|[;&|(])[[:space:]]*((LC_[A-Z_]+|LANG)=[^[:space:]]*[[:space:]]+)+[^[:space:]=]'

assert_empty "$(bashunit::compat::offenders "$pattern")"
}
23 changes: 23 additions & 0 deletions tests/unit/console_results_diff_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -62,3 +62,26 @@ function test_render_diff_shows_changed_tokens_without_color() {
rm -f "$a" "$b"
unset BASHUNIT_NO_COLOR
}

function test_render_diff_ignores_a_configured_external_diff() {
if ! bashunit::dependencies::has_git; then
bashunit::skip "git not available" && return
fi
export BASHUNIT_NO_COLOR=true
# An external differ (difftastic in #912) replaces git's own output, so without
# --no-ext-diff the rendered diff is whatever it prints -- here nothing at all.
export GIT_EXTERNAL_DIFF=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 GIT_EXTERNAL_DIFF
}
1 change: 1 addition & 0 deletions tests/unit/console_results_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -363,6 +363,7 @@ function test_render_execution_time_on_osx_with_perl() {

local render_result
mock_macos
_BASHUNIT_CLOCK_NOW_IMPL="perl"
bashunit::mock bashunit::dependencies::has_perl mock_true
_BASHUNIT_START_TIME="1726393394574382186"
bashunit::mock perl <<<"1726393394574372186"
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,6 +27,8 @@
- The `--parallel` unsupported-OS warning no longer claims Alpine is excluded

### Fixed
- A configured external git differ (`diff.external` / `GIT_EXTERNAL_DIFF`, e.g. difftastic) no longer blanks the multiline and snapshot failure diffs, which now render with `--no-ext-diff` (#912)
- Time reads and the JUnit report no longer set the locale with a temporary-environment prefix (`LC_ALL=C cmd`), which segfaults inside a command substitution on Bash 5.3.9 macOS (#912)
- Call assertions (`assert_not_called`, `assert_have_been_called*`) fail with `was never registered as a spy` instead of reporting zero calls when the name was never spied — a typo used to pass silently (#895)
- The per-argument form a spy records was written with a literal `$'\x1f'` separator instead of the byte, so it could not be compared against (#894)
- `--parallel` no longer discards worker stderr written outside a test body; it renders as a `Stderr from <file>` block (#864)
Expand Down
5 changes: 4 additions & 1 deletion src/clock.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -182,7 +182,10 @@ function bashunit::clock::now() {

function bashunit::clock::shell_time() {
# Get time directly from the shell variable EPOCHREALTIME (Bash 5+)
[ -n "${EPOCHREALTIME+x}" ] && [ -n "$EPOCHREALTIME" ] && LC_ALL=C echo "$EPOCHREALTIME"
# No `LC_ALL=C` prefix: the value is expanded before the temporary environment
# applies, so it cannot normalize the decimal separator (callers accept both),
# and that form segfaults inside `$()` on Bash 5.3 macOS (#912).
[ -n "${EPOCHREALTIME+x}" ] && [ -n "$EPOCHREALTIME" ] && echo "$EPOCHREALTIME"
}

function bashunit::clock::total_runtime_in_milliseconds() {
Expand Down
5 changes: 3 additions & 2 deletions src/console_results.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -312,8 +312,9 @@ function bashunit::console_results::render_diff() {

# `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" \
# header lines; `sed` indents the body. `--no-ext-diff` ignores a user's
# `diff.external`/`GIT_EXTERNAL_DIFF`, which would replace this word-diff.
git diff --no-index --no-ext-diff --word-diff "$color_flag" \
"$expected_file" "$actual_file" 2>/dev/null |
tail -n +6 | sed "s/^/ /" || true
}
Expand Down
6 changes: 4 additions & 2 deletions src/reports.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -102,7 +102,9 @@ function bashunit::reports::generate_junit_xml() {
local tests_failed=$(bashunit::state::get_tests_failed)
local time_ms=$(bashunit::clock::total_runtime_in_milliseconds)
local time
time=$(LC_ALL=C awk -v ms="$time_ms" 'BEGIN {printf "%.3f", ms/1000}')
# `env` rather than a bare `LC_ALL=C` prefix: C keeps awk's radix a dot for the
# XML, and that prefix form segfaults inside `$()` on Bash 5.3 macOS (#912).
time=$(env LC_ALL=C awk -v ms="$time_ms" 'BEGIN {printf "%.3f", ms/1000}')

{
echo "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
Expand All@@ -120,7 +122,7 @@ function bashunit::reports::generate_junit_xml() {
local test_time_ms="${_BASHUNIT_REPORTS_TEST_DURATIONS[$i]:-}"
local failure_message="${_BASHUNIT_REPORTS_TEST_FAILURES[$i]:-}"
local test_time
test_time=$(LC_ALL=C awk -v ms="$test_time_ms" 'BEGIN {printf "%.3f", ms/1000}')
test_time=$(env LC_ALL=C awk -v ms="$test_time_ms" 'BEGIN {printf "%.3f", ms/1000}')

echo " <testcase file=\"$file\""
echo " name=\"$name\""
Expand Down
10 changes: 5 additions & 5 deletions tests/acceptance/bashunit_init_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,23 +15,23 @@ function tear_down() {

function test_bashunit_init_creates_structure() {
pushd "$TMP_DIR" >/dev/null
"$BASHUNIT_PATH" init >/tmp/init.log
"$BASHUNIT_PATH" init >"$TMP_DIR/init.log"
assert_file_exists "tests/example_test.sh"
assert_file_exists "tests/bootstrap.sh"
popd >/dev/null
}

function test_bashunit_init_custom_directory() {
pushd "$TMP_DIR" >/dev/null
"$BASHUNIT_PATH" init custom >/tmp/init.log
"$BASHUNIT_PATH" init custom >"$TMP_DIR/init.log"
assert_file_exists "custom/example_test.sh"
assert_file_exists "custom/bootstrap.sh"
popd >/dev/null
}

function test_bashunit_init_creates_github_workflow() {
pushd "$TMP_DIR" >/dev/null
"$BASHUNIT_PATH" init >/tmp/init.log
"$BASHUNIT_PATH" init >"$TMP_DIR/init.log"
assert_file_exists ".github/workflows/tests.yml"
assert_file_contains ".github/workflows/tests.yml" "TypedDevs/bashunit@"
popd >/dev/null
Expand All@@ -41,7 +41,7 @@ function test_bashunit_init_does_not_overwrite_existing_workflow() {
pushd "$TMP_DIR" >/dev/null
mkdir -p ".github/workflows"
echo "custom-workflow" >".github/workflows/tests.yml"
"$BASHUNIT_PATH" init >/tmp/init.log
"$BASHUNIT_PATH" init >"$TMP_DIR/init.log"
assert_file_contains ".github/workflows/tests.yml" "custom-workflow"
popd >/dev/null
}
Expand All@@ -51,7 +51,7 @@ function test_bashunit_init_updates_env() {

pushd "$TMP_DIR" >/dev/null
echo "BASHUNIT_BOOTSTRAP=old/bootstrap.sh" >.env
"$BASHUNIT_PATH" init custom >/tmp/init.log
"$BASHUNIT_PATH" init custom >"$TMP_DIR/init.log"
assert_file_exists "custom/example_test.sh"
assert_file_exists "custom/bootstrap.sh"
assert_file_contains .env "#BASHUNIT_BOOTSTRAP=old/bootstrap.sh"
Expand Down
4 changes: 2 additions & 2 deletions tests/acceptance/bashunit_syntax_error_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,7 +10,7 @@ function test_bashunit_when_test_file_has_syntax_error() {

local actual_raw
set +e
actual_raw="$(LC_ALL=C LANG=C ./bashunit \
actual_raw="$(./bashunit \
--no-parallel --detailed --env "$TEST_ENV_FILE" "$test_file" 2>&1)"
set -e

Expand All@@ -19,6 +19,6 @@ function test_bashunit_when_test_file_has_syntax_error() {

assert_contains "failed" "$actual"
assert_contains "Error" "$actual"
assert_general_error "$(LC_ALL=C LANG=C ./bashunit \
assert_general_error "$(./bashunit \
--no-parallel --env "$TEST_ENV_FILE" "$test_file" 2>&1)"
}
11 changes: 11 additions & 0 deletions tests/unit/bash_compatibility_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -102,3 +102,14 @@ function test_src_has_no_coproc() {
function test_src_has_no_parameter_transformations() {
assert_empty "$(bashunit::compat::offenders '\$\{[A-Za-z_][A-Za-z0-9_]*@[QEPAKa]\}')"
}

# A temporary-environment locale prefix (`LC_ALL=C cmd`) makes bash change its
# own locale for that command. Bash 5.3.9 on macOS segfaults on that form inside
# a command substitution -- `x=$(LC_ALL=C echo hi)` exits 139 (#912) -- and no CI
# job runs that build. Use `env LC_ALL=C cmd` instead, which passes the locale
# straight to the child and never touches bash's own.
function test_src_has_no_temporary_locale_assignment_prefix() {
local pattern='(^|[;&|(])[[:space:]]*((LC_[A-Z_]+|LANG)=[^[:space:]]*[[:space:]]+)+[^[:space:]=]'

assert_empty "$(bashunit::compat::offenders "$pattern")"
}
23 changes: 23 additions & 0 deletions tests/unit/console_results_diff_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -62,3 +62,26 @@ function test_render_diff_shows_changed_tokens_without_color() {
rm -f "$a" "$b"
unset BASHUNIT_NO_COLOR
}

function test_render_diff_ignores_a_configured_external_diff() {
if ! bashunit::dependencies::has_git; then
bashunit::skip "git not available" && return
fi
export BASHUNIT_NO_COLOR=true
# An external differ (difftastic in #912) replaces git's own output, so without
# --no-ext-diff the rendered diff is whatever it prints -- here nothing at all.
export GIT_EXTERNAL_DIFF=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 GIT_EXTERNAL_DIFF
}
1 change: 1 addition & 0 deletions tests/unit/console_results_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -363,6 +363,7 @@ function test_render_execution_time_on_osx_with_perl() {

local render_result
mock_macos
_BASHUNIT_CLOCK_NOW_IMPL="perl"
bashunit::mock bashunit::dependencies::has_perl mock_true
_BASHUNIT_START_TIME="1726393394574382186"
bashunit::mock perl <<<"1726393394574372186"
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,6 +27,8 @@
- The `--parallel` unsupported-OS warning no longer claims Alpine is excluded

### Fixed
- A configured external git differ (`diff.external` / `GIT_EXTERNAL_DIFF`, e.g. difftastic) no longer blanks the multiline and snapshot failure diffs, which now render with `--no-ext-diff` (#912)
- Time reads and the JUnit report no longer set the locale with a temporary-environment prefix (`LC_ALL=C cmd`), which segfaults inside a command substitution on Bash 5.3.9 macOS (#912)
- Call assertions (`assert_not_called`, `assert_have_been_called*`) fail with `was never registered as a spy` instead of reporting zero calls when the name was never spied — a typo used to pass silently (#895)
- The per-argument form a spy records was written with a literal `$'\x1f'` separator instead of the byte, so it could not be compared against (#894)
- `--parallel` no longer discards worker stderr written outside a test body; it renders as a `Stderr from <file>` block (#864)
Expand Down
5 changes: 4 additions & 1 deletion src/clock.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -182,7 +182,10 @@ function bashunit::clock::now() {

function bashunit::clock::shell_time() {
# Get time directly from the shell variable EPOCHREALTIME (Bash 5+)
[ -n "${EPOCHREALTIME+x}" ] && [ -n "$EPOCHREALTIME" ] && LC_ALL=C echo "$EPOCHREALTIME"
# No `LC_ALL=C` prefix: the value is expanded before the temporary environment
# applies, so it cannot normalize the decimal separator (callers accept both),
# and that form segfaults inside `$()` on Bash 5.3 macOS (#912).
[ -n "${EPOCHREALTIME+x}" ] && [ -n "$EPOCHREALTIME" ] && echo "$EPOCHREALTIME"
}

function bashunit::clock::total_runtime_in_milliseconds() {
Expand Down
5 changes: 3 additions & 2 deletions src/console_results.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -312,8 +312,9 @@ function bashunit::console_results::render_diff() {

# `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" \
# header lines; `sed` indents the body. `--no-ext-diff` ignores a user's
# `diff.external`/`GIT_EXTERNAL_DIFF`, which would replace this word-diff.
git diff --no-index --no-ext-diff --word-diff "$color_flag" \
"$expected_file" "$actual_file" 2>/dev/null |
tail -n +6 | sed "s/^/ /" || true
}
Expand Down
6 changes: 4 additions & 2 deletions src/reports.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -102,7 +102,9 @@ function bashunit::reports::generate_junit_xml() {
local tests_failed=$(bashunit::state::get_tests_failed)
local time_ms=$(bashunit::clock::total_runtime_in_milliseconds)
local time
time=$(LC_ALL=C awk -v ms="$time_ms" 'BEGIN {printf "%.3f", ms/1000}')
# `env` rather than a bare `LC_ALL=C` prefix: C keeps awk's radix a dot for the
# XML, and that prefix form segfaults inside `$()` on Bash 5.3 macOS (#912).
time=$(env LC_ALL=C awk -v ms="$time_ms" 'BEGIN {printf "%.3f", ms/1000}')

{
echo "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
Expand All@@ -120,7 +122,7 @@ function bashunit::reports::generate_junit_xml() {
local test_time_ms="${_BASHUNIT_REPORTS_TEST_DURATIONS[$i]:-}"
local failure_message="${_BASHUNIT_REPORTS_TEST_FAILURES[$i]:-}"
local test_time
test_time=$(LC_ALL=C awk -v ms="$test_time_ms" 'BEGIN {printf "%.3f", ms/1000}')
test_time=$(env LC_ALL=C awk -v ms="$test_time_ms" 'BEGIN {printf "%.3f", ms/1000}')

echo " <testcase file=\"$file\""
echo " name=\"$name\""
Expand Down
10 changes: 5 additions & 5 deletions tests/acceptance/bashunit_init_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,23 +15,23 @@ function tear_down() {

function test_bashunit_init_creates_structure() {
pushd "$TMP_DIR" >/dev/null
"$BASHUNIT_PATH" init >/tmp/init.log
"$BASHUNIT_PATH" init >"$TMP_DIR/init.log"
assert_file_exists "tests/example_test.sh"
assert_file_exists "tests/bootstrap.sh"
popd >/dev/null
}

function test_bashunit_init_custom_directory() {
pushd "$TMP_DIR" >/dev/null
"$BASHUNIT_PATH" init custom >/tmp/init.log
"$BASHUNIT_PATH" init custom >"$TMP_DIR/init.log"
assert_file_exists "custom/example_test.sh"
assert_file_exists "custom/bootstrap.sh"
popd >/dev/null
}

function test_bashunit_init_creates_github_workflow() {
pushd "$TMP_DIR" >/dev/null
"$BASHUNIT_PATH" init >/tmp/init.log
"$BASHUNIT_PATH" init >"$TMP_DIR/init.log"
assert_file_exists ".github/workflows/tests.yml"
assert_file_contains ".github/workflows/tests.yml" "TypedDevs/bashunit@"
popd >/dev/null
Expand All@@ -41,7 +41,7 @@ function test_bashunit_init_does_not_overwrite_existing_workflow() {
pushd "$TMP_DIR" >/dev/null
mkdir -p ".github/workflows"
echo "custom-workflow" >".github/workflows/tests.yml"
"$BASHUNIT_PATH" init >/tmp/init.log
"$BASHUNIT_PATH" init >"$TMP_DIR/init.log"
assert_file_contains ".github/workflows/tests.yml" "custom-workflow"
popd >/dev/null
}
Expand All@@ -51,7 +51,7 @@ function test_bashunit_init_updates_env() {

pushd "$TMP_DIR" >/dev/null
echo "BASHUNIT_BOOTSTRAP=old/bootstrap.sh" >.env
"$BASHUNIT_PATH" init custom >/tmp/init.log
"$BASHUNIT_PATH" init custom >"$TMP_DIR/init.log"
assert_file_exists "custom/example_test.sh"
assert_file_exists "custom/bootstrap.sh"
assert_file_contains .env "#BASHUNIT_BOOTSTRAP=old/bootstrap.sh"
Expand Down
4 changes: 2 additions & 2 deletions tests/acceptance/bashunit_syntax_error_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,7 +10,7 @@ function test_bashunit_when_test_file_has_syntax_error() {

local actual_raw
set +e
actual_raw="$(LC_ALL=C LANG=C ./bashunit \
actual_raw="$(./bashunit \
--no-parallel --detailed --env "$TEST_ENV_FILE" "$test_file" 2>&1)"
set -e

Expand All@@ -19,6 +19,6 @@ function test_bashunit_when_test_file_has_syntax_error() {

assert_contains "failed" "$actual"
assert_contains "Error" "$actual"
assert_general_error "$(LC_ALL=C LANG=C ./bashunit \
assert_general_error "$(./bashunit \
--no-parallel --env "$TEST_ENV_FILE" "$test_file" 2>&1)"
}
11 changes: 11 additions & 0 deletions tests/unit/bash_compatibility_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -102,3 +102,14 @@ function test_src_has_no_coproc() {
function test_src_has_no_parameter_transformations() {
assert_empty "$(bashunit::compat::offenders '\$\{[A-Za-z_][A-Za-z0-9_]*@[QEPAKa]\}')"
}

# A temporary-environment locale prefix (`LC_ALL=C cmd`) makes bash change its
# own locale for that command. Bash 5.3.9 on macOS segfaults on that form inside
# a command substitution -- `x=$(LC_ALL=C echo hi)` exits 139 (#912) -- and no CI
# job runs that build. Use `env LC_ALL=C cmd` instead, which passes the locale
# straight to the child and never touches bash's own.
function test_src_has_no_temporary_locale_assignment_prefix() {
local pattern='(^|[;&|(])[[:space:]]*((LC_[A-Z_]+|LANG)=[^[:space:]]*[[:space:]]+)+[^[:space:]=]'

assert_empty "$(bashunit::compat::offenders "$pattern")"
}
23 changes: 23 additions & 0 deletions tests/unit/console_results_diff_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -62,3 +62,26 @@ function test_render_diff_shows_changed_tokens_without_color() {
rm -f "$a" "$b"
unset BASHUNIT_NO_COLOR
}

function test_render_diff_ignores_a_configured_external_diff() {
if ! bashunit::dependencies::has_git; then
bashunit::skip "git not available" && return
fi
export BASHUNIT_NO_COLOR=true
# An external differ (difftastic in #912) replaces git's own output, so without
# --no-ext-diff the rendered diff is whatever it prints -- here nothing at all.
export GIT_EXTERNAL_DIFF=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 GIT_EXTERNAL_DIFF
}
1 change: 1 addition & 0 deletions tests/unit/console_results_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -363,6 +363,7 @@ function test_render_execution_time_on_osx_with_perl() {

local render_result
mock_macos
_BASHUNIT_CLOCK_NOW_IMPL="perl"
bashunit::mock bashunit::dependencies::has_perl mock_true
_BASHUNIT_START_TIME="1726393394574382186"
bashunit::mock perl <<<"1726393394574372186"
Expand Down
Loading