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
96 changes: 96 additions & 0 deletions src/assert/core.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,6 +20,18 @@ function bashunit::assert::should_skip() {
bashunit::env::is_stop_on_assertion_failure_enabled && ((_BASHUNIT_ASSERTION_FAILED_IN_TEST))
}

# Emits a machine-detectable assertion usage error. The runner strips the
# prefix and reports the message through the existing Error channel.
function bashunit::assert::usage_error() {
local assertion=$1
local required=$2
local signature=$3
local supplied=$4

printf 'bashunit: assertion usage error: %s expects %s arguments (%s), got %s\n' \
"$assertion" "$required" "$signature" "$supplied" >&2
}

_BASHUNIT_ASSERT_LABEL_OUT=""

# Resolve assertion label into the slot _BASHUNIT_ASSERT_LABEL_OUT with no fork:
Expand DownExpand Up@@ -207,6 +219,10 @@ function bashunit::handle_bool_assertion_failure() {

function assert_same() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi

local expected="$1"
local actual="$2"
Expand All@@ -222,6 +238,10 @@ function assert_same() {

function assert_equals() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi

local expected="$1"
local actual="$2"
Expand All@@ -242,6 +262,10 @@ function assert_equals() {

function assert_not_equals() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi

local expected="$1"
local actual="$2"
Expand DownExpand Up@@ -290,6 +314,10 @@ function assert_not_empty() {

function assert_not_same() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi

local expected="$1"
local actual="$2"
Expand All@@ -305,6 +333,10 @@ function assert_not_same() {

function assert_contains() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi
local IFS=$' \t\n'

local expected="$1"
Expand DownExpand Up@@ -339,6 +371,10 @@ function bashunit::assert::_supports_nocasematch() {

function assert_contains_ignore_case() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi

local expected="$1"
local actual="$2"
Expand DownExpand Up@@ -396,6 +432,10 @@ function assert_contains_ignore_case() {
function assert_not_contains() {
local label_override=""
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi
local IFS=$' \t\n'

local expected="$1"
Expand All@@ -416,6 +456,10 @@ function assert_not_contains() {

function assert_matches() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "pattern, actual" "$#"
return 2
fi
local IFS=$' \t\n'

local expected="$1"
Expand All@@ -442,6 +486,10 @@ function assert_matches() {
function assert_not_matches() {
local label_override=""
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "pattern, actual" "$#"
return 2
fi
local IFS=$' \t\n'

local expected="$1"
Expand DownExpand Up@@ -706,6 +754,10 @@ function assert_command_not_found() {
function assert_string_starts_with() {
local label_override=""
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi
local IFS=$' \t\n'

local expected="$1"
Expand All@@ -727,6 +779,10 @@ function assert_string_starts_with() {

function assert_string_not_starts_with() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi

local expected="$1"
local actual="$2"
Expand All@@ -745,6 +801,10 @@ function assert_string_not_starts_with() {
function assert_string_ends_with() {
local label_override=""
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi
local IFS=$' \t\n'

local expected="$1"
Expand All@@ -767,6 +827,10 @@ function assert_string_ends_with() {
function assert_string_not_ends_with() {
local label_override=""
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi
local IFS=$' \t\n'

local expected="$1"
Expand All@@ -787,6 +851,10 @@ function assert_string_not_ends_with() {

function assert_less_than() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi

local expected="$1"
local actual="$2"
Expand All@@ -802,6 +870,10 @@ function assert_less_than() {

function assert_less_or_equal_than() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi

local expected="$1"
local actual="$2"
Expand All@@ -817,6 +889,10 @@ function assert_less_or_equal_than() {

function assert_greater_than() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi

local expected="$1"
local actual="$2"
Expand All@@ -832,6 +908,10 @@ function assert_greater_than() {

function assert_greater_or_equal_than() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi

local expected="$1"
local actual="$2"
Expand DownExpand Up@@ -868,6 +948,10 @@ function bashunit::assert::_is_numeric() {
##
function assert_within_delta() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 3 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 3 "expected, actual, delta" "$#"
return 2
fi

local expected="$1"
local actual="$2"
Expand DownExpand Up@@ -948,6 +1032,10 @@ function assert_within_delta() {

function assert_line_count() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi
local IFS=$' \t\n'

local expected="$1"
Expand DownExpand Up@@ -1029,6 +1117,10 @@ function bashunit::format_to_regex() {

function assert_string_matches_format() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "format, actual" "$#"
return 2
fi

local format="$1"
local actual="$2"
Expand All@@ -1047,6 +1139,10 @@ function assert_string_matches_format() {

function assert_string_not_matches_format() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "format, actual" "$#"
return 2
fi

local format="$1"
local actual="$2"
Expand Down
40 changes: 39 additions & 1 deletion src/runner/diagnostics.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -31,12 +31,50 @@ function bashunit::runner::halt_if_stop_on_failure() {
}

# Writes the detected runtime-error message (empty when none) into
# _BASHUNIT_RUNNER_RUNTIME_ERROR_OUT. Return-slot form avoids a per-test fork
# _BASHUNIT_RUNNER_RUNTIME_ERROR_OUT and display-safe output into
# _BASHUNIT_RUNNER_RUNTIME_OUTPUT_OUT. Return-slot form avoids a per-test fork
# on the hot path (#764).
# Arguments: $1 runtime_output
function bashunit::runner::detect_runtime_error() {
local runtime_output=$1
_BASHUNIT_RUNNER_RUNTIME_ERROR_OUT=""
_BASHUNIT_RUNNER_RUNTIME_OUTPUT_OUT=$runtime_output

local usage_prefix="bashunit: assertion usage error: "
local usage_marker=$'\n'"$usage_prefix"
local usage_before=""
local usage_rest=""
local usage_found=false
case "$runtime_output" in
"$usage_prefix"*)
usage_rest=${runtime_output#"$usage_prefix"}
usage_found=true
;;
*"$usage_marker"*)
usage_before=${runtime_output%%"$usage_marker"*}
usage_rest=${runtime_output#*"$usage_marker"}
usage_found=true
;;
esac

if [ "$usage_found" = true ]; then
local usage_error=${usage_rest%%$'\n'*}
local usage_after=""
if [ "$usage_rest" != "$usage_error" ]; then
usage_after=${usage_rest#*$'\n'}
fi
_BASHUNIT_RUNNER_RUNTIME_ERROR_OUT=$usage_error
if [ -n "$usage_before" ] && [ -n "$usage_after" ]; then
_BASHUNIT_RUNNER_RUNTIME_OUTPUT_OUT="$usage_before
$usage_after"
elif [ -n "$usage_before" ]; then
_BASHUNIT_RUNNER_RUNTIME_OUTPUT_OUT=$usage_before
else
_BASHUNIT_RUNNER_RUNTIME_OUTPUT_OUT=$usage_after
fi
return
fi

case "$runtime_output" in
*"command not found"* | *"unbound variable"* | *"permission denied"* | \
*"no such file or directory"* | *"syntax error"* | *"bad substitution"* | \
Expand Down
3 changes: 2 additions & 1 deletion src/runner/exec.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -326,6 +326,7 @@ function bashunit::runner::run_test() {
local attempt_runtime_output="${test_execution_result%%##ASSERTIONS_*}"
bashunit::runner::detect_runtime_error "$attempt_runtime_output"
local attempt_runtime_error=$_BASHUNIT_RUNNER_RUNTIME_ERROR_OUT
local attempt_display_output=$_BASHUNIT_RUNNER_RUNTIME_OUTPUT_OUT
bashunit::runner::extract_result_counts "$test_execution_result"
# Mirror the commit-phase failure test exactly (runtime error, non-zero exit,
# or a failed assertion); snapshot/incomplete/skipped/risky are not failures.
Expand DownExpand Up@@ -373,7 +374,7 @@ function bashunit::runner::run_test() {
# Reuse the final attempt's values (the loop always runs at least once and
# its locals persist in this function scope), instead of recomputing and
# forking detect_runtime_error a second time (#764).
local runtime_output=$attempt_runtime_output
local runtime_output=$attempt_display_output
local runtime_error=$attempt_runtime_error

# parse_result accumulates _BASHUNIT_TEST_EXIT_CODE; reset it so each test's
Expand Down
14 changes: 14 additions & 0 deletions tests/acceptance/bashunit_assert_arity_test.sh
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
#!/usr/bin/env bash
set -euo pipefail

function test_missing_assertion_argument_is_a_usage_error() {
local fixture=tests/acceptance/fixtures/assert_arity/missing.sh
local output exit_code=0

output=$(NO_COLOR=1 ./bashunit --no-parallel --skip-env-file "$fixture" 2>&1) || exit_code=$?

assert_same 1 "$exit_code"
assert_contains "✗ Error: Wrong arg count" "$output"
assert_contains "assert_same expects 2 arguments (expected, actual), got 1" "$output"
assert_not_contains "but got" "$output"
}
7 changes: 4 additions & 3 deletions tests/acceptance/bashunit_strict_mode_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -44,14 +44,15 @@ function test_strict_mode_fails_on_unset_variable_in_set_up() {
assert_contains "failed" "$output"
}

function test_strict_mode_reports_an_omitted_variadic_actual_as_a_plain_failure() {
function test_strict_mode_reports_an_omitted_variadic_actual_as_a_usage_error() {
local output
output=$(BASHUNIT_STRICT_MODE=true ./bashunit --no-parallel --simple --skip-env-file --env "$TEST_ENV_FILE" \
tests/acceptance/fixtures/strict_mode_variadic_actual_omitted.sh 2>&1) || true

assert_not_contains "unbound variable" "$output"
assert_contains "to contain" "$output"
assert_contains "to start with" "$output"
assert_contains "✗ Error" "$output"
assert_contains "assert_contains expects 2 arguments (expected, actual), got 1" "$output"
assert_contains "assert_string_starts_with expects 2 arguments (expected, actual), got 1" "$output"
}

function test_cli_flag_overrides_env_var() {
Expand Down
6 changes: 6 additions & 0 deletions tests/acceptance/fixtures/assert_arity/missing.sh
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
#!/usr/bin/env bash

function test_wrong_arg_count() {
assert_same "only-one"
assert_same "later assertion" "later assertion"
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,8 +2,8 @@

# The "actual" operand of these assertions is variadic ("${@:2}"), so omitting
# it leaves an empty array. Expanding an empty array under `set -u` (--strict)
# is an unbound-variable error on Bash < 4.4, which used to abort the test with
# an internal error instead of reporting a normal assertion failure.
# is an unbound-variable error on Bash < 4.4, which used to abort the test. The
# assertions now reject the missing operand as a usage error first.
function test_contains_without_actual() {
assert_contains "needle"
}
Expand Down
Loading
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
96 changes: 96 additions & 0 deletions src/assert/core.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,6 +20,18 @@ function bashunit::assert::should_skip() {
bashunit::env::is_stop_on_assertion_failure_enabled && ((_BASHUNIT_ASSERTION_FAILED_IN_TEST))
}

# Emits a machine-detectable assertion usage error. The runner strips the
# prefix and reports the message through the existing Error channel.
function bashunit::assert::usage_error() {
local assertion=$1
local required=$2
local signature=$3
local supplied=$4

printf 'bashunit: assertion usage error: %s expects %s arguments (%s), got %s\n' \
"$assertion" "$required" "$signature" "$supplied" >&2
}

_BASHUNIT_ASSERT_LABEL_OUT=""

# Resolve assertion label into the slot _BASHUNIT_ASSERT_LABEL_OUT with no fork:
Expand DownExpand Up@@ -207,6 +219,10 @@ function bashunit::handle_bool_assertion_failure() {

function assert_same() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi

local expected="$1"
local actual="$2"
Expand All@@ -222,6 +238,10 @@ function assert_same() {

function assert_equals() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi

local expected="$1"
local actual="$2"
Expand All@@ -242,6 +262,10 @@ function assert_equals() {

function assert_not_equals() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi

local expected="$1"
local actual="$2"
Expand DownExpand Up@@ -290,6 +314,10 @@ function assert_not_empty() {

function assert_not_same() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi

local expected="$1"
local actual="$2"
Expand All@@ -305,6 +333,10 @@ function assert_not_same() {

function assert_contains() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi
local IFS=$' \t\n'

local expected="$1"
Expand DownExpand Up@@ -339,6 +371,10 @@ function bashunit::assert::_supports_nocasematch() {

function assert_contains_ignore_case() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi

local expected="$1"
local actual="$2"
Expand DownExpand Up@@ -396,6 +432,10 @@ function assert_contains_ignore_case() {
function assert_not_contains() {
local label_override=""
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi
local IFS=$' \t\n'

local expected="$1"
Expand All@@ -416,6 +456,10 @@ function assert_not_contains() {

function assert_matches() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "pattern, actual" "$#"
return 2
fi
local IFS=$' \t\n'

local expected="$1"
Expand All@@ -442,6 +486,10 @@ function assert_matches() {
function assert_not_matches() {
local label_override=""
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "pattern, actual" "$#"
return 2
fi
local IFS=$' \t\n'

local expected="$1"
Expand DownExpand Up@@ -706,6 +754,10 @@ function assert_command_not_found() {
function assert_string_starts_with() {
local label_override=""
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi
local IFS=$' \t\n'

local expected="$1"
Expand All@@ -727,6 +779,10 @@ function assert_string_starts_with() {

function assert_string_not_starts_with() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi

local expected="$1"
local actual="$2"
Expand All@@ -745,6 +801,10 @@ function assert_string_not_starts_with() {
function assert_string_ends_with() {
local label_override=""
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi
local IFS=$' \t\n'

local expected="$1"
Expand All@@ -767,6 +827,10 @@ function assert_string_ends_with() {
function assert_string_not_ends_with() {
local label_override=""
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi
local IFS=$' \t\n'

local expected="$1"
Expand All@@ -787,6 +851,10 @@ function assert_string_not_ends_with() {

function assert_less_than() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi

local expected="$1"
local actual="$2"
Expand All@@ -802,6 +870,10 @@ function assert_less_than() {

function assert_less_or_equal_than() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi

local expected="$1"
local actual="$2"
Expand All@@ -817,6 +889,10 @@ function assert_less_or_equal_than() {

function assert_greater_than() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi

local expected="$1"
local actual="$2"
Expand All@@ -832,6 +908,10 @@ function assert_greater_than() {

function assert_greater_or_equal_than() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi

local expected="$1"
local actual="$2"
Expand DownExpand Up@@ -868,6 +948,10 @@ function bashunit::assert::_is_numeric() {
##
function assert_within_delta() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 3 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 3 "expected, actual, delta" "$#"
return 2
fi

local expected="$1"
local actual="$2"
Expand DownExpand Up@@ -948,6 +1032,10 @@ function assert_within_delta() {

function assert_line_count() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi
local IFS=$' \t\n'

local expected="$1"
Expand DownExpand Up@@ -1029,6 +1117,10 @@ function bashunit::format_to_regex() {

function assert_string_matches_format() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "format, actual" "$#"
return 2
fi

local format="$1"
local actual="$2"
Expand All@@ -1047,6 +1139,10 @@ function assert_string_matches_format() {

function assert_string_not_matches_format() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "format, actual" "$#"
return 2
fi

local format="$1"
local actual="$2"
Expand Down
40 changes: 39 additions & 1 deletion src/runner/diagnostics.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -31,12 +31,50 @@ function bashunit::runner::halt_if_stop_on_failure() {
}

# Writes the detected runtime-error message (empty when none) into
# _BASHUNIT_RUNNER_RUNTIME_ERROR_OUT. Return-slot form avoids a per-test fork
# _BASHUNIT_RUNNER_RUNTIME_ERROR_OUT and display-safe output into
# _BASHUNIT_RUNNER_RUNTIME_OUTPUT_OUT. Return-slot form avoids a per-test fork
# on the hot path (#764).
# Arguments: $1 runtime_output
function bashunit::runner::detect_runtime_error() {
local runtime_output=$1
_BASHUNIT_RUNNER_RUNTIME_ERROR_OUT=""
_BASHUNIT_RUNNER_RUNTIME_OUTPUT_OUT=$runtime_output

local usage_prefix="bashunit: assertion usage error: "
local usage_marker=$'\n'"$usage_prefix"
local usage_before=""
local usage_rest=""
local usage_found=false
case "$runtime_output" in
"$usage_prefix"*)
usage_rest=${runtime_output#"$usage_prefix"}
usage_found=true
;;
*"$usage_marker"*)
usage_before=${runtime_output%%"$usage_marker"*}
usage_rest=${runtime_output#*"$usage_marker"}
usage_found=true
;;
esac

if [ "$usage_found" = true ]; then
local usage_error=${usage_rest%%$'\n'*}
local usage_after=""
if [ "$usage_rest" != "$usage_error" ]; then
usage_after=${usage_rest#*$'\n'}
fi
_BASHUNIT_RUNNER_RUNTIME_ERROR_OUT=$usage_error
if [ -n "$usage_before" ] && [ -n "$usage_after" ]; then
_BASHUNIT_RUNNER_RUNTIME_OUTPUT_OUT="$usage_before
$usage_after"
elif [ -n "$usage_before" ]; then
_BASHUNIT_RUNNER_RUNTIME_OUTPUT_OUT=$usage_before
else
_BASHUNIT_RUNNER_RUNTIME_OUTPUT_OUT=$usage_after
fi
return
fi

case "$runtime_output" in
*"command not found"* | *"unbound variable"* | *"permission denied"* | \
*"no such file or directory"* | *"syntax error"* | *"bad substitution"* | \
Expand Down
3 changes: 2 additions & 1 deletion src/runner/exec.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -326,6 +326,7 @@ function bashunit::runner::run_test() {
local attempt_runtime_output="${test_execution_result%%##ASSERTIONS_*}"
bashunit::runner::detect_runtime_error "$attempt_runtime_output"
local attempt_runtime_error=$_BASHUNIT_RUNNER_RUNTIME_ERROR_OUT
local attempt_display_output=$_BASHUNIT_RUNNER_RUNTIME_OUTPUT_OUT
bashunit::runner::extract_result_counts "$test_execution_result"
# Mirror the commit-phase failure test exactly (runtime error, non-zero exit,
# or a failed assertion); snapshot/incomplete/skipped/risky are not failures.
Expand DownExpand Up@@ -373,7 +374,7 @@ function bashunit::runner::run_test() {
# Reuse the final attempt's values (the loop always runs at least once and
# its locals persist in this function scope), instead of recomputing and
# forking detect_runtime_error a second time (#764).
local runtime_output=$attempt_runtime_output
local runtime_output=$attempt_display_output
local runtime_error=$attempt_runtime_error

# parse_result accumulates _BASHUNIT_TEST_EXIT_CODE; reset it so each test's
Expand Down
14 changes: 14 additions & 0 deletions tests/acceptance/bashunit_assert_arity_test.sh
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
#!/usr/bin/env bash
set -euo pipefail

function test_missing_assertion_argument_is_a_usage_error() {
local fixture=tests/acceptance/fixtures/assert_arity/missing.sh
local output exit_code=0

output=$(NO_COLOR=1 ./bashunit --no-parallel --skip-env-file "$fixture" 2>&1) || exit_code=$?

assert_same 1 "$exit_code"
assert_contains "✗ Error: Wrong arg count" "$output"
assert_contains "assert_same expects 2 arguments (expected, actual), got 1" "$output"
assert_not_contains "but got" "$output"
}
7 changes: 4 additions & 3 deletions tests/acceptance/bashunit_strict_mode_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -44,14 +44,15 @@ function test_strict_mode_fails_on_unset_variable_in_set_up() {
assert_contains "failed" "$output"
}

function test_strict_mode_reports_an_omitted_variadic_actual_as_a_plain_failure() {
function test_strict_mode_reports_an_omitted_variadic_actual_as_a_usage_error() {
local output
output=$(BASHUNIT_STRICT_MODE=true ./bashunit --no-parallel --simple --skip-env-file --env "$TEST_ENV_FILE" \
tests/acceptance/fixtures/strict_mode_variadic_actual_omitted.sh 2>&1) || true

assert_not_contains "unbound variable" "$output"
assert_contains "to contain" "$output"
assert_contains "to start with" "$output"
assert_contains "✗ Error" "$output"
assert_contains "assert_contains expects 2 arguments (expected, actual), got 1" "$output"
assert_contains "assert_string_starts_with expects 2 arguments (expected, actual), got 1" "$output"
}

function test_cli_flag_overrides_env_var() {
Expand Down
6 changes: 6 additions & 0 deletions tests/acceptance/fixtures/assert_arity/missing.sh
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
#!/usr/bin/env bash

function test_wrong_arg_count() {
assert_same "only-one"
assert_same "later assertion" "later assertion"
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,8 +2,8 @@

# The "actual" operand of these assertions is variadic ("${@:2}"), so omitting
# it leaves an empty array. Expanding an empty array under `set -u` (--strict)
# is an unbound-variable error on Bash < 4.4, which used to abort the test with
# an internal error instead of reporting a normal assertion failure.
# is an unbound-variable error on Bash < 4.4, which used to abort the test. The
# assertions now reject the missing operand as a usage error first.
function test_contains_without_actual() {
assert_contains "needle"
}
Expand Down
Loading
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
96 changes: 96 additions & 0 deletions src/assert/core.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,6 +20,18 @@ function bashunit::assert::should_skip() {
bashunit::env::is_stop_on_assertion_failure_enabled && ((_BASHUNIT_ASSERTION_FAILED_IN_TEST))
}

# Emits a machine-detectable assertion usage error. The runner strips the
# prefix and reports the message through the existing Error channel.
function bashunit::assert::usage_error() {
local assertion=$1
local required=$2
local signature=$3
local supplied=$4

printf 'bashunit: assertion usage error: %s expects %s arguments (%s), got %s\n' \
"$assertion" "$required" "$signature" "$supplied" >&2
}

_BASHUNIT_ASSERT_LABEL_OUT=""

# Resolve assertion label into the slot _BASHUNIT_ASSERT_LABEL_OUT with no fork:
Expand DownExpand Up@@ -207,6 +219,10 @@ function bashunit::handle_bool_assertion_failure() {

function assert_same() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi

local expected="$1"
local actual="$2"
Expand All@@ -222,6 +238,10 @@ function assert_same() {

function assert_equals() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi

local expected="$1"
local actual="$2"
Expand All@@ -242,6 +262,10 @@ function assert_equals() {

function assert_not_equals() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi

local expected="$1"
local actual="$2"
Expand DownExpand Up@@ -290,6 +314,10 @@ function assert_not_empty() {

function assert_not_same() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi

local expected="$1"
local actual="$2"
Expand All@@ -305,6 +333,10 @@ function assert_not_same() {

function assert_contains() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi
local IFS=$' \t\n'

local expected="$1"
Expand DownExpand Up@@ -339,6 +371,10 @@ function bashunit::assert::_supports_nocasematch() {

function assert_contains_ignore_case() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi

local expected="$1"
local actual="$2"
Expand DownExpand Up@@ -396,6 +432,10 @@ function assert_contains_ignore_case() {
function assert_not_contains() {
local label_override=""
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi
local IFS=$' \t\n'

local expected="$1"
Expand All@@ -416,6 +456,10 @@ function assert_not_contains() {

function assert_matches() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "pattern, actual" "$#"
return 2
fi
local IFS=$' \t\n'

local expected="$1"
Expand All@@ -442,6 +486,10 @@ function assert_matches() {
function assert_not_matches() {
local label_override=""
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "pattern, actual" "$#"
return 2
fi
local IFS=$' \t\n'

local expected="$1"
Expand DownExpand Up@@ -706,6 +754,10 @@ function assert_command_not_found() {
function assert_string_starts_with() {
local label_override=""
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi
local IFS=$' \t\n'

local expected="$1"
Expand All@@ -727,6 +779,10 @@ function assert_string_starts_with() {

function assert_string_not_starts_with() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi

local expected="$1"
local actual="$2"
Expand All@@ -745,6 +801,10 @@ function assert_string_not_starts_with() {
function assert_string_ends_with() {
local label_override=""
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi
local IFS=$' \t\n'

local expected="$1"
Expand All@@ -767,6 +827,10 @@ function assert_string_ends_with() {
function assert_string_not_ends_with() {
local label_override=""
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi
local IFS=$' \t\n'

local expected="$1"
Expand All@@ -787,6 +851,10 @@ function assert_string_not_ends_with() {

function assert_less_than() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi

local expected="$1"
local actual="$2"
Expand All@@ -802,6 +870,10 @@ function assert_less_than() {

function assert_less_or_equal_than() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi

local expected="$1"
local actual="$2"
Expand All@@ -817,6 +889,10 @@ function assert_less_or_equal_than() {

function assert_greater_than() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi

local expected="$1"
local actual="$2"
Expand All@@ -832,6 +908,10 @@ function assert_greater_than() {

function assert_greater_or_equal_than() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi

local expected="$1"
local actual="$2"
Expand DownExpand Up@@ -868,6 +948,10 @@ function bashunit::assert::_is_numeric() {
##
function assert_within_delta() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 3 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 3 "expected, actual, delta" "$#"
return 2
fi

local expected="$1"
local actual="$2"
Expand DownExpand Up@@ -948,6 +1032,10 @@ function assert_within_delta() {

function assert_line_count() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi
local IFS=$' \t\n'

local expected="$1"
Expand DownExpand Up@@ -1029,6 +1117,10 @@ function bashunit::format_to_regex() {

function assert_string_matches_format() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "format, actual" "$#"
return 2
fi

local format="$1"
local actual="$2"
Expand All@@ -1047,6 +1139,10 @@ function assert_string_matches_format() {

function assert_string_not_matches_format() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "format, actual" "$#"
return 2
fi

local format="$1"
local actual="$2"
Expand Down
40 changes: 39 additions & 1 deletion src/runner/diagnostics.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -31,12 +31,50 @@ function bashunit::runner::halt_if_stop_on_failure() {
}

# Writes the detected runtime-error message (empty when none) into
# _BASHUNIT_RUNNER_RUNTIME_ERROR_OUT. Return-slot form avoids a per-test fork
# _BASHUNIT_RUNNER_RUNTIME_ERROR_OUT and display-safe output into
# _BASHUNIT_RUNNER_RUNTIME_OUTPUT_OUT. Return-slot form avoids a per-test fork
# on the hot path (#764).
# Arguments: $1 runtime_output
function bashunit::runner::detect_runtime_error() {
local runtime_output=$1
_BASHUNIT_RUNNER_RUNTIME_ERROR_OUT=""
_BASHUNIT_RUNNER_RUNTIME_OUTPUT_OUT=$runtime_output

local usage_prefix="bashunit: assertion usage error: "
local usage_marker=$'\n'"$usage_prefix"
local usage_before=""
local usage_rest=""
local usage_found=false
case "$runtime_output" in
"$usage_prefix"*)
usage_rest=${runtime_output#"$usage_prefix"}
usage_found=true
;;
*"$usage_marker"*)
usage_before=${runtime_output%%"$usage_marker"*}
usage_rest=${runtime_output#*"$usage_marker"}
usage_found=true
;;
esac

if [ "$usage_found" = true ]; then
local usage_error=${usage_rest%%$'\n'*}
local usage_after=""
if [ "$usage_rest" != "$usage_error" ]; then
usage_after=${usage_rest#*$'\n'}
fi
_BASHUNIT_RUNNER_RUNTIME_ERROR_OUT=$usage_error
if [ -n "$usage_before" ] && [ -n "$usage_after" ]; then
_BASHUNIT_RUNNER_RUNTIME_OUTPUT_OUT="$usage_before
$usage_after"
elif [ -n "$usage_before" ]; then
_BASHUNIT_RUNNER_RUNTIME_OUTPUT_OUT=$usage_before
else
_BASHUNIT_RUNNER_RUNTIME_OUTPUT_OUT=$usage_after
fi
return
fi

case "$runtime_output" in
*"command not found"* | *"unbound variable"* | *"permission denied"* | \
*"no such file or directory"* | *"syntax error"* | *"bad substitution"* | \
Expand Down
3 changes: 2 additions & 1 deletion src/runner/exec.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -326,6 +326,7 @@ function bashunit::runner::run_test() {
local attempt_runtime_output="${test_execution_result%%##ASSERTIONS_*}"
bashunit::runner::detect_runtime_error "$attempt_runtime_output"
local attempt_runtime_error=$_BASHUNIT_RUNNER_RUNTIME_ERROR_OUT
local attempt_display_output=$_BASHUNIT_RUNNER_RUNTIME_OUTPUT_OUT
bashunit::runner::extract_result_counts "$test_execution_result"
# Mirror the commit-phase failure test exactly (runtime error, non-zero exit,
# or a failed assertion); snapshot/incomplete/skipped/risky are not failures.
Expand DownExpand Up@@ -373,7 +374,7 @@ function bashunit::runner::run_test() {
# Reuse the final attempt's values (the loop always runs at least once and
# its locals persist in this function scope), instead of recomputing and
# forking detect_runtime_error a second time (#764).
local runtime_output=$attempt_runtime_output
local runtime_output=$attempt_display_output
local runtime_error=$attempt_runtime_error

# parse_result accumulates _BASHUNIT_TEST_EXIT_CODE; reset it so each test's
Expand Down
14 changes: 14 additions & 0 deletions tests/acceptance/bashunit_assert_arity_test.sh
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
#!/usr/bin/env bash
set -euo pipefail

function test_missing_assertion_argument_is_a_usage_error() {
local fixture=tests/acceptance/fixtures/assert_arity/missing.sh
local output exit_code=0

output=$(NO_COLOR=1 ./bashunit --no-parallel --skip-env-file "$fixture" 2>&1) || exit_code=$?

assert_same 1 "$exit_code"
assert_contains "✗ Error: Wrong arg count" "$output"
assert_contains "assert_same expects 2 arguments (expected, actual), got 1" "$output"
assert_not_contains "but got" "$output"
}
7 changes: 4 additions & 3 deletions tests/acceptance/bashunit_strict_mode_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -44,14 +44,15 @@ function test_strict_mode_fails_on_unset_variable_in_set_up() {
assert_contains "failed" "$output"
}

function test_strict_mode_reports_an_omitted_variadic_actual_as_a_plain_failure() {
function test_strict_mode_reports_an_omitted_variadic_actual_as_a_usage_error() {
local output
output=$(BASHUNIT_STRICT_MODE=true ./bashunit --no-parallel --simple --skip-env-file --env "$TEST_ENV_FILE" \
tests/acceptance/fixtures/strict_mode_variadic_actual_omitted.sh 2>&1) || true

assert_not_contains "unbound variable" "$output"
assert_contains "to contain" "$output"
assert_contains "to start with" "$output"
assert_contains "✗ Error" "$output"
assert_contains "assert_contains expects 2 arguments (expected, actual), got 1" "$output"
assert_contains "assert_string_starts_with expects 2 arguments (expected, actual), got 1" "$output"
}

function test_cli_flag_overrides_env_var() {
Expand Down
6 changes: 6 additions & 0 deletions tests/acceptance/fixtures/assert_arity/missing.sh
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
#!/usr/bin/env bash

function test_wrong_arg_count() {
assert_same "only-one"
assert_same "later assertion" "later assertion"
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,8 +2,8 @@

# The "actual" operand of these assertions is variadic ("${@:2}"), so omitting
# it leaves an empty array. Expanding an empty array under `set -u` (--strict)
# is an unbound-variable error on Bash < 4.4, which used to abort the test with
# an internal error instead of reporting a normal assertion failure.
# is an unbound-variable error on Bash < 4.4, which used to abort the test. The
# assertions now reject the missing operand as a usage error first.
function test_contains_without_actual() {
assert_contains "needle"
}
Expand Down
Loading
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
96 changes: 96 additions & 0 deletions src/assert/core.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,6 +20,18 @@ function bashunit::assert::should_skip() {
bashunit::env::is_stop_on_assertion_failure_enabled && ((_BASHUNIT_ASSERTION_FAILED_IN_TEST))
}

# Emits a machine-detectable assertion usage error. The runner strips the
# prefix and reports the message through the existing Error channel.
function bashunit::assert::usage_error() {
local assertion=$1
local required=$2
local signature=$3
local supplied=$4

printf 'bashunit: assertion usage error: %s expects %s arguments (%s), got %s\n' \
"$assertion" "$required" "$signature" "$supplied" >&2
}

_BASHUNIT_ASSERT_LABEL_OUT=""

# Resolve assertion label into the slot _BASHUNIT_ASSERT_LABEL_OUT with no fork:
Expand DownExpand Up@@ -207,6 +219,10 @@ function bashunit::handle_bool_assertion_failure() {

function assert_same() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi

local expected="$1"
local actual="$2"
Expand All@@ -222,6 +238,10 @@ function assert_same() {

function assert_equals() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi

local expected="$1"
local actual="$2"
Expand All@@ -242,6 +262,10 @@ function assert_equals() {

function assert_not_equals() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi

local expected="$1"
local actual="$2"
Expand DownExpand Up@@ -290,6 +314,10 @@ function assert_not_empty() {

function assert_not_same() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi

local expected="$1"
local actual="$2"
Expand All@@ -305,6 +333,10 @@ function assert_not_same() {

function assert_contains() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi
local IFS=$' \t\n'

local expected="$1"
Expand DownExpand Up@@ -339,6 +371,10 @@ function bashunit::assert::_supports_nocasematch() {

function assert_contains_ignore_case() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi

local expected="$1"
local actual="$2"
Expand DownExpand Up@@ -396,6 +432,10 @@ function assert_contains_ignore_case() {
function assert_not_contains() {
local label_override=""
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi
local IFS=$' \t\n'

local expected="$1"
Expand All@@ -416,6 +456,10 @@ function assert_not_contains() {

function assert_matches() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "pattern, actual" "$#"
return 2
fi
local IFS=$' \t\n'

local expected="$1"
Expand All@@ -442,6 +486,10 @@ function assert_matches() {
function assert_not_matches() {
local label_override=""
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "pattern, actual" "$#"
return 2
fi
local IFS=$' \t\n'

local expected="$1"
Expand DownExpand Up@@ -706,6 +754,10 @@ function assert_command_not_found() {
function assert_string_starts_with() {
local label_override=""
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi
local IFS=$' \t\n'

local expected="$1"
Expand All@@ -727,6 +779,10 @@ function assert_string_starts_with() {

function assert_string_not_starts_with() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi

local expected="$1"
local actual="$2"
Expand All@@ -745,6 +801,10 @@ function assert_string_not_starts_with() {
function assert_string_ends_with() {
local label_override=""
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi
local IFS=$' \t\n'

local expected="$1"
Expand All@@ -767,6 +827,10 @@ function assert_string_ends_with() {
function assert_string_not_ends_with() {
local label_override=""
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi
local IFS=$' \t\n'

local expected="$1"
Expand All@@ -787,6 +851,10 @@ function assert_string_not_ends_with() {

function assert_less_than() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi

local expected="$1"
local actual="$2"
Expand All@@ -802,6 +870,10 @@ function assert_less_than() {

function assert_less_or_equal_than() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi

local expected="$1"
local actual="$2"
Expand All@@ -817,6 +889,10 @@ function assert_less_or_equal_than() {

function assert_greater_than() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi

local expected="$1"
local actual="$2"
Expand All@@ -832,6 +908,10 @@ function assert_greater_than() {

function assert_greater_or_equal_than() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi

local expected="$1"
local actual="$2"
Expand DownExpand Up@@ -868,6 +948,10 @@ function bashunit::assert::_is_numeric() {
##
function assert_within_delta() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 3 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 3 "expected, actual, delta" "$#"
return 2
fi

local expected="$1"
local actual="$2"
Expand DownExpand Up@@ -948,6 +1032,10 @@ function assert_within_delta() {

function assert_line_count() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi
local IFS=$' \t\n'

local expected="$1"
Expand DownExpand Up@@ -1029,6 +1117,10 @@ function bashunit::format_to_regex() {

function assert_string_matches_format() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "format, actual" "$#"
return 2
fi

local format="$1"
local actual="$2"
Expand All@@ -1047,6 +1139,10 @@ function assert_string_matches_format() {

function assert_string_not_matches_format() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "format, actual" "$#"
return 2
fi

local format="$1"
local actual="$2"
Expand Down
40 changes: 39 additions & 1 deletion src/runner/diagnostics.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -31,12 +31,50 @@ function bashunit::runner::halt_if_stop_on_failure() {
}

# Writes the detected runtime-error message (empty when none) into
# _BASHUNIT_RUNNER_RUNTIME_ERROR_OUT. Return-slot form avoids a per-test fork
# _BASHUNIT_RUNNER_RUNTIME_ERROR_OUT and display-safe output into
# _BASHUNIT_RUNNER_RUNTIME_OUTPUT_OUT. Return-slot form avoids a per-test fork
# on the hot path (#764).
# Arguments: $1 runtime_output
function bashunit::runner::detect_runtime_error() {
local runtime_output=$1
_BASHUNIT_RUNNER_RUNTIME_ERROR_OUT=""
_BASHUNIT_RUNNER_RUNTIME_OUTPUT_OUT=$runtime_output

local usage_prefix="bashunit: assertion usage error: "
local usage_marker=$'\n'"$usage_prefix"
local usage_before=""
local usage_rest=""
local usage_found=false
case "$runtime_output" in
"$usage_prefix"*)
usage_rest=${runtime_output#"$usage_prefix"}
usage_found=true
;;
*"$usage_marker"*)
usage_before=${runtime_output%%"$usage_marker"*}
usage_rest=${runtime_output#*"$usage_marker"}
usage_found=true
;;
esac

if [ "$usage_found" = true ]; then
local usage_error=${usage_rest%%$'\n'*}
local usage_after=""
if [ "$usage_rest" != "$usage_error" ]; then
usage_after=${usage_rest#*$'\n'}
fi
_BASHUNIT_RUNNER_RUNTIME_ERROR_OUT=$usage_error
if [ -n "$usage_before" ] && [ -n "$usage_after" ]; then
_BASHUNIT_RUNNER_RUNTIME_OUTPUT_OUT="$usage_before
$usage_after"
elif [ -n "$usage_before" ]; then
_BASHUNIT_RUNNER_RUNTIME_OUTPUT_OUT=$usage_before
else
_BASHUNIT_RUNNER_RUNTIME_OUTPUT_OUT=$usage_after
fi
return
fi

case "$runtime_output" in
*"command not found"* | *"unbound variable"* | *"permission denied"* | \
*"no such file or directory"* | *"syntax error"* | *"bad substitution"* | \
Expand Down
3 changes: 2 additions & 1 deletion src/runner/exec.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -326,6 +326,7 @@ function bashunit::runner::run_test() {
local attempt_runtime_output="${test_execution_result%%##ASSERTIONS_*}"
bashunit::runner::detect_runtime_error "$attempt_runtime_output"
local attempt_runtime_error=$_BASHUNIT_RUNNER_RUNTIME_ERROR_OUT
local attempt_display_output=$_BASHUNIT_RUNNER_RUNTIME_OUTPUT_OUT
bashunit::runner::extract_result_counts "$test_execution_result"
# Mirror the commit-phase failure test exactly (runtime error, non-zero exit,
# or a failed assertion); snapshot/incomplete/skipped/risky are not failures.
Expand DownExpand Up@@ -373,7 +374,7 @@ function bashunit::runner::run_test() {
# Reuse the final attempt's values (the loop always runs at least once and
# its locals persist in this function scope), instead of recomputing and
# forking detect_runtime_error a second time (#764).
local runtime_output=$attempt_runtime_output
local runtime_output=$attempt_display_output
local runtime_error=$attempt_runtime_error

# parse_result accumulates _BASHUNIT_TEST_EXIT_CODE; reset it so each test's
Expand Down
14 changes: 14 additions & 0 deletions tests/acceptance/bashunit_assert_arity_test.sh
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
#!/usr/bin/env bash
set -euo pipefail

function test_missing_assertion_argument_is_a_usage_error() {
local fixture=tests/acceptance/fixtures/assert_arity/missing.sh
local output exit_code=0

output=$(NO_COLOR=1 ./bashunit --no-parallel --skip-env-file "$fixture" 2>&1) || exit_code=$?

assert_same 1 "$exit_code"
assert_contains "✗ Error: Wrong arg count" "$output"
assert_contains "assert_same expects 2 arguments (expected, actual), got 1" "$output"
assert_not_contains "but got" "$output"
}
7 changes: 4 additions & 3 deletions tests/acceptance/bashunit_strict_mode_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -44,14 +44,15 @@ function test_strict_mode_fails_on_unset_variable_in_set_up() {
assert_contains "failed" "$output"
}

function test_strict_mode_reports_an_omitted_variadic_actual_as_a_plain_failure() {
function test_strict_mode_reports_an_omitted_variadic_actual_as_a_usage_error() {
local output
output=$(BASHUNIT_STRICT_MODE=true ./bashunit --no-parallel --simple --skip-env-file --env "$TEST_ENV_FILE" \
tests/acceptance/fixtures/strict_mode_variadic_actual_omitted.sh 2>&1) || true

assert_not_contains "unbound variable" "$output"
assert_contains "to contain" "$output"
assert_contains "to start with" "$output"
assert_contains "✗ Error" "$output"
assert_contains "assert_contains expects 2 arguments (expected, actual), got 1" "$output"
assert_contains "assert_string_starts_with expects 2 arguments (expected, actual), got 1" "$output"
}

function test_cli_flag_overrides_env_var() {
Expand Down
6 changes: 6 additions & 0 deletions tests/acceptance/fixtures/assert_arity/missing.sh
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
#!/usr/bin/env bash

function test_wrong_arg_count() {
assert_same "only-one"
assert_same "later assertion" "later assertion"
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,8 +2,8 @@

# The "actual" operand of these assertions is variadic ("${@:2}"), so omitting
# it leaves an empty array. Expanding an empty array under `set -u` (--strict)
# is an unbound-variable error on Bash < 4.4, which used to abort the test with
# an internal error instead of reporting a normal assertion failure.
# is an unbound-variable error on Bash < 4.4, which used to abort the test. The
# assertions now reject the missing operand as a usage error first.
function test_contains_without_actual() {
assert_contains "needle"
}
Expand Down
Loading
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
96 changes: 96 additions & 0 deletions src/assert/core.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,6 +20,18 @@ function bashunit::assert::should_skip() {
bashunit::env::is_stop_on_assertion_failure_enabled && ((_BASHUNIT_ASSERTION_FAILED_IN_TEST))
}

# Emits a machine-detectable assertion usage error. The runner strips the
# prefix and reports the message through the existing Error channel.
function bashunit::assert::usage_error() {
local assertion=$1
local required=$2
local signature=$3
local supplied=$4

printf 'bashunit: assertion usage error: %s expects %s arguments (%s), got %s\n' \
"$assertion" "$required" "$signature" "$supplied" >&2
}

_BASHUNIT_ASSERT_LABEL_OUT=""

# Resolve assertion label into the slot _BASHUNIT_ASSERT_LABEL_OUT with no fork:
Expand DownExpand Up@@ -207,6 +219,10 @@ function bashunit::handle_bool_assertion_failure() {

function assert_same() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi

local expected="$1"
local actual="$2"
Expand All@@ -222,6 +238,10 @@ function assert_same() {

function assert_equals() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi

local expected="$1"
local actual="$2"
Expand All@@ -242,6 +262,10 @@ function assert_equals() {

function assert_not_equals() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi

local expected="$1"
local actual="$2"
Expand DownExpand Up@@ -290,6 +314,10 @@ function assert_not_empty() {

function assert_not_same() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi

local expected="$1"
local actual="$2"
Expand All@@ -305,6 +333,10 @@ function assert_not_same() {

function assert_contains() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi
local IFS=$' \t\n'

local expected="$1"
Expand DownExpand Up@@ -339,6 +371,10 @@ function bashunit::assert::_supports_nocasematch() {

function assert_contains_ignore_case() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi

local expected="$1"
local actual="$2"
Expand DownExpand Up@@ -396,6 +432,10 @@ function assert_contains_ignore_case() {
function assert_not_contains() {
local label_override=""
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi
local IFS=$' \t\n'

local expected="$1"
Expand All@@ -416,6 +456,10 @@ function assert_not_contains() {

function assert_matches() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "pattern, actual" "$#"
return 2
fi
local IFS=$' \t\n'

local expected="$1"
Expand All@@ -442,6 +486,10 @@ function assert_matches() {
function assert_not_matches() {
local label_override=""
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "pattern, actual" "$#"
return 2
fi
local IFS=$' \t\n'

local expected="$1"
Expand DownExpand Up@@ -706,6 +754,10 @@ function assert_command_not_found() {
function assert_string_starts_with() {
local label_override=""
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi
local IFS=$' \t\n'

local expected="$1"
Expand All@@ -727,6 +779,10 @@ function assert_string_starts_with() {

function assert_string_not_starts_with() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi

local expected="$1"
local actual="$2"
Expand All@@ -745,6 +801,10 @@ function assert_string_not_starts_with() {
function assert_string_ends_with() {
local label_override=""
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi
local IFS=$' \t\n'

local expected="$1"
Expand All@@ -767,6 +827,10 @@ function assert_string_ends_with() {
function assert_string_not_ends_with() {
local label_override=""
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi
local IFS=$' \t\n'

local expected="$1"
Expand All@@ -787,6 +851,10 @@ function assert_string_not_ends_with() {

function assert_less_than() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi

local expected="$1"
local actual="$2"
Expand All@@ -802,6 +870,10 @@ function assert_less_than() {

function assert_less_or_equal_than() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi

local expected="$1"
local actual="$2"
Expand All@@ -817,6 +889,10 @@ function assert_less_or_equal_than() {

function assert_greater_than() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi

local expected="$1"
local actual="$2"
Expand All@@ -832,6 +908,10 @@ function assert_greater_than() {

function assert_greater_or_equal_than() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi

local expected="$1"
local actual="$2"
Expand DownExpand Up@@ -868,6 +948,10 @@ function bashunit::assert::_is_numeric() {
##
function assert_within_delta() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 3 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 3 "expected, actual, delta" "$#"
return 2
fi

local expected="$1"
local actual="$2"
Expand DownExpand Up@@ -948,6 +1032,10 @@ function assert_within_delta() {

function assert_line_count() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi
local IFS=$' \t\n'

local expected="$1"
Expand DownExpand Up@@ -1029,6 +1117,10 @@ function bashunit::format_to_regex() {

function assert_string_matches_format() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "format, actual" "$#"
return 2
fi

local format="$1"
local actual="$2"
Expand All@@ -1047,6 +1139,10 @@ function assert_string_matches_format() {

function assert_string_not_matches_format() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "format, actual" "$#"
return 2
fi

local format="$1"
local actual="$2"
Expand Down
40 changes: 39 additions & 1 deletion src/runner/diagnostics.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -31,12 +31,50 @@ function bashunit::runner::halt_if_stop_on_failure() {
}

# Writes the detected runtime-error message (empty when none) into
# _BASHUNIT_RUNNER_RUNTIME_ERROR_OUT. Return-slot form avoids a per-test fork
# _BASHUNIT_RUNNER_RUNTIME_ERROR_OUT and display-safe output into
# _BASHUNIT_RUNNER_RUNTIME_OUTPUT_OUT. Return-slot form avoids a per-test fork
# on the hot path (#764).
# Arguments: $1 runtime_output
function bashunit::runner::detect_runtime_error() {
local runtime_output=$1
_BASHUNIT_RUNNER_RUNTIME_ERROR_OUT=""
_BASHUNIT_RUNNER_RUNTIME_OUTPUT_OUT=$runtime_output

local usage_prefix="bashunit: assertion usage error: "
local usage_marker=$'\n'"$usage_prefix"
local usage_before=""
local usage_rest=""
local usage_found=false
case "$runtime_output" in
"$usage_prefix"*)
usage_rest=${runtime_output#"$usage_prefix"}
usage_found=true
;;
*"$usage_marker"*)
usage_before=${runtime_output%%"$usage_marker"*}
usage_rest=${runtime_output#*"$usage_marker"}
usage_found=true
;;
esac

if [ "$usage_found" = true ]; then
local usage_error=${usage_rest%%$'\n'*}
local usage_after=""
if [ "$usage_rest" != "$usage_error" ]; then
usage_after=${usage_rest#*$'\n'}
fi
_BASHUNIT_RUNNER_RUNTIME_ERROR_OUT=$usage_error
if [ -n "$usage_before" ] && [ -n "$usage_after" ]; then
_BASHUNIT_RUNNER_RUNTIME_OUTPUT_OUT="$usage_before
$usage_after"
elif [ -n "$usage_before" ]; then
_BASHUNIT_RUNNER_RUNTIME_OUTPUT_OUT=$usage_before
else
_BASHUNIT_RUNNER_RUNTIME_OUTPUT_OUT=$usage_after
fi
return
fi

case "$runtime_output" in
*"command not found"* | *"unbound variable"* | *"permission denied"* | \
*"no such file or directory"* | *"syntax error"* | *"bad substitution"* | \
Expand Down
3 changes: 2 additions & 1 deletion src/runner/exec.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -326,6 +326,7 @@ function bashunit::runner::run_test() {
local attempt_runtime_output="${test_execution_result%%##ASSERTIONS_*}"
bashunit::runner::detect_runtime_error "$attempt_runtime_output"
local attempt_runtime_error=$_BASHUNIT_RUNNER_RUNTIME_ERROR_OUT
local attempt_display_output=$_BASHUNIT_RUNNER_RUNTIME_OUTPUT_OUT
bashunit::runner::extract_result_counts "$test_execution_result"
# Mirror the commit-phase failure test exactly (runtime error, non-zero exit,
# or a failed assertion); snapshot/incomplete/skipped/risky are not failures.
Expand DownExpand Up@@ -373,7 +374,7 @@ function bashunit::runner::run_test() {
# Reuse the final attempt's values (the loop always runs at least once and
# its locals persist in this function scope), instead of recomputing and
# forking detect_runtime_error a second time (#764).
local runtime_output=$attempt_runtime_output
local runtime_output=$attempt_display_output
local runtime_error=$attempt_runtime_error

# parse_result accumulates _BASHUNIT_TEST_EXIT_CODE; reset it so each test's
Expand Down
14 changes: 14 additions & 0 deletions tests/acceptance/bashunit_assert_arity_test.sh
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
#!/usr/bin/env bash
set -euo pipefail

function test_missing_assertion_argument_is_a_usage_error() {
local fixture=tests/acceptance/fixtures/assert_arity/missing.sh
local output exit_code=0

output=$(NO_COLOR=1 ./bashunit --no-parallel --skip-env-file "$fixture" 2>&1) || exit_code=$?

assert_same 1 "$exit_code"
assert_contains "✗ Error: Wrong arg count" "$output"
assert_contains "assert_same expects 2 arguments (expected, actual), got 1" "$output"
assert_not_contains "but got" "$output"
}
7 changes: 4 additions & 3 deletions tests/acceptance/bashunit_strict_mode_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -44,14 +44,15 @@ function test_strict_mode_fails_on_unset_variable_in_set_up() {
assert_contains "failed" "$output"
}

function test_strict_mode_reports_an_omitted_variadic_actual_as_a_plain_failure() {
function test_strict_mode_reports_an_omitted_variadic_actual_as_a_usage_error() {
local output
output=$(BASHUNIT_STRICT_MODE=true ./bashunit --no-parallel --simple --skip-env-file --env "$TEST_ENV_FILE" \
tests/acceptance/fixtures/strict_mode_variadic_actual_omitted.sh 2>&1) || true

assert_not_contains "unbound variable" "$output"
assert_contains "to contain" "$output"
assert_contains "to start with" "$output"
assert_contains "✗ Error" "$output"
assert_contains "assert_contains expects 2 arguments (expected, actual), got 1" "$output"
assert_contains "assert_string_starts_with expects 2 arguments (expected, actual), got 1" "$output"
}

function test_cli_flag_overrides_env_var() {
Expand Down
6 changes: 6 additions & 0 deletions tests/acceptance/fixtures/assert_arity/missing.sh
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
#!/usr/bin/env bash

function test_wrong_arg_count() {
assert_same "only-one"
assert_same "later assertion" "later assertion"
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,8 +2,8 @@

# The "actual" operand of these assertions is variadic ("${@:2}"), so omitting
# it leaves an empty array. Expanding an empty array under `set -u` (--strict)
# is an unbound-variable error on Bash < 4.4, which used to abort the test with
# an internal error instead of reporting a normal assertion failure.
# is an unbound-variable error on Bash < 4.4, which used to abort the test. The
# assertions now reject the missing operand as a usage error first.
function test_contains_without_actual() {
assert_contains "needle"
}
Expand Down
Loading
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
96 changes: 96 additions & 0 deletions src/assert/core.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,6 +20,18 @@ function bashunit::assert::should_skip() {
bashunit::env::is_stop_on_assertion_failure_enabled && ((_BASHUNIT_ASSERTION_FAILED_IN_TEST))
}

# Emits a machine-detectable assertion usage error. The runner strips the
# prefix and reports the message through the existing Error channel.
function bashunit::assert::usage_error() {
local assertion=$1
local required=$2
local signature=$3
local supplied=$4

printf 'bashunit: assertion usage error: %s expects %s arguments (%s), got %s\n' \
"$assertion" "$required" "$signature" "$supplied" >&2
}

_BASHUNIT_ASSERT_LABEL_OUT=""

# Resolve assertion label into the slot _BASHUNIT_ASSERT_LABEL_OUT with no fork:
Expand DownExpand Up@@ -207,6 +219,10 @@ function bashunit::handle_bool_assertion_failure() {

function assert_same() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi

local expected="$1"
local actual="$2"
Expand All@@ -222,6 +238,10 @@ function assert_same() {

function assert_equals() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi

local expected="$1"
local actual="$2"
Expand All@@ -242,6 +262,10 @@ function assert_equals() {

function assert_not_equals() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi

local expected="$1"
local actual="$2"
Expand DownExpand Up@@ -290,6 +314,10 @@ function assert_not_empty() {

function assert_not_same() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi

local expected="$1"
local actual="$2"
Expand All@@ -305,6 +333,10 @@ function assert_not_same() {

function assert_contains() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi
local IFS=$' \t\n'

local expected="$1"
Expand DownExpand Up@@ -339,6 +371,10 @@ function bashunit::assert::_supports_nocasematch() {

function assert_contains_ignore_case() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi

local expected="$1"
local actual="$2"
Expand DownExpand Up@@ -396,6 +432,10 @@ function assert_contains_ignore_case() {
function assert_not_contains() {
local label_override=""
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi
local IFS=$' \t\n'

local expected="$1"
Expand All@@ -416,6 +456,10 @@ function assert_not_contains() {

function assert_matches() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "pattern, actual" "$#"
return 2
fi
local IFS=$' \t\n'

local expected="$1"
Expand All@@ -442,6 +486,10 @@ function assert_matches() {
function assert_not_matches() {
local label_override=""
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "pattern, actual" "$#"
return 2
fi
local IFS=$' \t\n'

local expected="$1"
Expand DownExpand Up@@ -706,6 +754,10 @@ function assert_command_not_found() {
function assert_string_starts_with() {
local label_override=""
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi
local IFS=$' \t\n'

local expected="$1"
Expand All@@ -727,6 +779,10 @@ function assert_string_starts_with() {

function assert_string_not_starts_with() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi

local expected="$1"
local actual="$2"
Expand All@@ -745,6 +801,10 @@ function assert_string_not_starts_with() {
function assert_string_ends_with() {
local label_override=""
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi
local IFS=$' \t\n'

local expected="$1"
Expand All@@ -767,6 +827,10 @@ function assert_string_ends_with() {
function assert_string_not_ends_with() {
local label_override=""
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi
local IFS=$' \t\n'

local expected="$1"
Expand All@@ -787,6 +851,10 @@ function assert_string_not_ends_with() {

function assert_less_than() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi

local expected="$1"
local actual="$2"
Expand All@@ -802,6 +870,10 @@ function assert_less_than() {

function assert_less_or_equal_than() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi

local expected="$1"
local actual="$2"
Expand All@@ -817,6 +889,10 @@ function assert_less_or_equal_than() {

function assert_greater_than() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi

local expected="$1"
local actual="$2"
Expand All@@ -832,6 +908,10 @@ function assert_greater_than() {

function assert_greater_or_equal_than() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi

local expected="$1"
local actual="$2"
Expand DownExpand Up@@ -868,6 +948,10 @@ function bashunit::assert::_is_numeric() {
##
function assert_within_delta() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 3 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 3 "expected, actual, delta" "$#"
return 2
fi

local expected="$1"
local actual="$2"
Expand DownExpand Up@@ -948,6 +1032,10 @@ function assert_within_delta() {

function assert_line_count() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi
local IFS=$' \t\n'

local expected="$1"
Expand DownExpand Up@@ -1029,6 +1117,10 @@ function bashunit::format_to_regex() {

function assert_string_matches_format() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "format, actual" "$#"
return 2
fi

local format="$1"
local actual="$2"
Expand All@@ -1047,6 +1139,10 @@ function assert_string_matches_format() {

function assert_string_not_matches_format() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "format, actual" "$#"
return 2
fi

local format="$1"
local actual="$2"
Expand Down
40 changes: 39 additions & 1 deletion src/runner/diagnostics.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -31,12 +31,50 @@ function bashunit::runner::halt_if_stop_on_failure() {
}

# Writes the detected runtime-error message (empty when none) into
# _BASHUNIT_RUNNER_RUNTIME_ERROR_OUT. Return-slot form avoids a per-test fork
# _BASHUNIT_RUNNER_RUNTIME_ERROR_OUT and display-safe output into
# _BASHUNIT_RUNNER_RUNTIME_OUTPUT_OUT. Return-slot form avoids a per-test fork
# on the hot path (#764).
# Arguments: $1 runtime_output
function bashunit::runner::detect_runtime_error() {
local runtime_output=$1
_BASHUNIT_RUNNER_RUNTIME_ERROR_OUT=""
_BASHUNIT_RUNNER_RUNTIME_OUTPUT_OUT=$runtime_output

local usage_prefix="bashunit: assertion usage error: "
local usage_marker=$'\n'"$usage_prefix"
local usage_before=""
local usage_rest=""
local usage_found=false
case "$runtime_output" in
"$usage_prefix"*)
usage_rest=${runtime_output#"$usage_prefix"}
usage_found=true
;;
*"$usage_marker"*)
usage_before=${runtime_output%%"$usage_marker"*}
usage_rest=${runtime_output#*"$usage_marker"}
usage_found=true
;;
esac

if [ "$usage_found" = true ]; then
local usage_error=${usage_rest%%$'\n'*}
local usage_after=""
if [ "$usage_rest" != "$usage_error" ]; then
usage_after=${usage_rest#*$'\n'}
fi
_BASHUNIT_RUNNER_RUNTIME_ERROR_OUT=$usage_error
if [ -n "$usage_before" ] && [ -n "$usage_after" ]; then
_BASHUNIT_RUNNER_RUNTIME_OUTPUT_OUT="$usage_before
$usage_after"
elif [ -n "$usage_before" ]; then
_BASHUNIT_RUNNER_RUNTIME_OUTPUT_OUT=$usage_before
else
_BASHUNIT_RUNNER_RUNTIME_OUTPUT_OUT=$usage_after
fi
return
fi

case "$runtime_output" in
*"command not found"* | *"unbound variable"* | *"permission denied"* | \
*"no such file or directory"* | *"syntax error"* | *"bad substitution"* | \
Expand Down
3 changes: 2 additions & 1 deletion src/runner/exec.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -326,6 +326,7 @@ function bashunit::runner::run_test() {
local attempt_runtime_output="${test_execution_result%%##ASSERTIONS_*}"
bashunit::runner::detect_runtime_error "$attempt_runtime_output"
local attempt_runtime_error=$_BASHUNIT_RUNNER_RUNTIME_ERROR_OUT
local attempt_display_output=$_BASHUNIT_RUNNER_RUNTIME_OUTPUT_OUT
bashunit::runner::extract_result_counts "$test_execution_result"
# Mirror the commit-phase failure test exactly (runtime error, non-zero exit,
# or a failed assertion); snapshot/incomplete/skipped/risky are not failures.
Expand DownExpand Up@@ -373,7 +374,7 @@ function bashunit::runner::run_test() {
# Reuse the final attempt's values (the loop always runs at least once and
# its locals persist in this function scope), instead of recomputing and
# forking detect_runtime_error a second time (#764).
local runtime_output=$attempt_runtime_output
local runtime_output=$attempt_display_output
local runtime_error=$attempt_runtime_error

# parse_result accumulates _BASHUNIT_TEST_EXIT_CODE; reset it so each test's
Expand Down
14 changes: 14 additions & 0 deletions tests/acceptance/bashunit_assert_arity_test.sh
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
#!/usr/bin/env bash
set -euo pipefail

function test_missing_assertion_argument_is_a_usage_error() {
local fixture=tests/acceptance/fixtures/assert_arity/missing.sh
local output exit_code=0

output=$(NO_COLOR=1 ./bashunit --no-parallel --skip-env-file "$fixture" 2>&1) || exit_code=$?

assert_same 1 "$exit_code"
assert_contains "✗ Error: Wrong arg count" "$output"
assert_contains "assert_same expects 2 arguments (expected, actual), got 1" "$output"
assert_not_contains "but got" "$output"
}
7 changes: 4 additions & 3 deletions tests/acceptance/bashunit_strict_mode_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -44,14 +44,15 @@ function test_strict_mode_fails_on_unset_variable_in_set_up() {
assert_contains "failed" "$output"
}

function test_strict_mode_reports_an_omitted_variadic_actual_as_a_plain_failure() {
function test_strict_mode_reports_an_omitted_variadic_actual_as_a_usage_error() {
local output
output=$(BASHUNIT_STRICT_MODE=true ./bashunit --no-parallel --simple --skip-env-file --env "$TEST_ENV_FILE" \
tests/acceptance/fixtures/strict_mode_variadic_actual_omitted.sh 2>&1) || true

assert_not_contains "unbound variable" "$output"
assert_contains "to contain" "$output"
assert_contains "to start with" "$output"
assert_contains "✗ Error" "$output"
assert_contains "assert_contains expects 2 arguments (expected, actual), got 1" "$output"
assert_contains "assert_string_starts_with expects 2 arguments (expected, actual), got 1" "$output"
}

function test_cli_flag_overrides_env_var() {
Expand Down
6 changes: 6 additions & 0 deletions tests/acceptance/fixtures/assert_arity/missing.sh
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
#!/usr/bin/env bash

function test_wrong_arg_count() {
assert_same "only-one"
assert_same "later assertion" "later assertion"
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,8 +2,8 @@

# The "actual" operand of these assertions is variadic ("${@:2}"), so omitting
# it leaves an empty array. Expanding an empty array under `set -u` (--strict)
# is an unbound-variable error on Bash < 4.4, which used to abort the test with
# an internal error instead of reporting a normal assertion failure.
# is an unbound-variable error on Bash < 4.4, which used to abort the test. The
# assertions now reject the missing operand as a usage error first.
function test_contains_without_actual() {
assert_contains "needle"
}
Expand Down
Loading
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
96 changes: 96 additions & 0 deletions src/assert/core.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,6 +20,18 @@ function bashunit::assert::should_skip() {
bashunit::env::is_stop_on_assertion_failure_enabled && ((_BASHUNIT_ASSERTION_FAILED_IN_TEST))
}

# Emits a machine-detectable assertion usage error. The runner strips the
# prefix and reports the message through the existing Error channel.
function bashunit::assert::usage_error() {
local assertion=$1
local required=$2
local signature=$3
local supplied=$4

printf 'bashunit: assertion usage error: %s expects %s arguments (%s), got %s\n' \
"$assertion" "$required" "$signature" "$supplied" >&2
}

_BASHUNIT_ASSERT_LABEL_OUT=""

# Resolve assertion label into the slot _BASHUNIT_ASSERT_LABEL_OUT with no fork:
Expand DownExpand Up@@ -207,6 +219,10 @@ function bashunit::handle_bool_assertion_failure() {

function assert_same() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi

local expected="$1"
local actual="$2"
Expand All@@ -222,6 +238,10 @@ function assert_same() {

function assert_equals() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi

local expected="$1"
local actual="$2"
Expand All@@ -242,6 +262,10 @@ function assert_equals() {

function assert_not_equals() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi

local expected="$1"
local actual="$2"
Expand DownExpand Up@@ -290,6 +314,10 @@ function assert_not_empty() {

function assert_not_same() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi

local expected="$1"
local actual="$2"
Expand All@@ -305,6 +333,10 @@ function assert_not_same() {

function assert_contains() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi
local IFS=$' \t\n'

local expected="$1"
Expand DownExpand Up@@ -339,6 +371,10 @@ function bashunit::assert::_supports_nocasematch() {

function assert_contains_ignore_case() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi

local expected="$1"
local actual="$2"
Expand DownExpand Up@@ -396,6 +432,10 @@ function assert_contains_ignore_case() {
function assert_not_contains() {
local label_override=""
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi
local IFS=$' \t\n'

local expected="$1"
Expand All@@ -416,6 +456,10 @@ function assert_not_contains() {

function assert_matches() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "pattern, actual" "$#"
return 2
fi
local IFS=$' \t\n'

local expected="$1"
Expand All@@ -442,6 +486,10 @@ function assert_matches() {
function assert_not_matches() {
local label_override=""
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "pattern, actual" "$#"
return 2
fi
local IFS=$' \t\n'

local expected="$1"
Expand DownExpand Up@@ -706,6 +754,10 @@ function assert_command_not_found() {
function assert_string_starts_with() {
local label_override=""
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi
local IFS=$' \t\n'

local expected="$1"
Expand All@@ -727,6 +779,10 @@ function assert_string_starts_with() {

function assert_string_not_starts_with() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi

local expected="$1"
local actual="$2"
Expand All@@ -745,6 +801,10 @@ function assert_string_not_starts_with() {
function assert_string_ends_with() {
local label_override=""
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi
local IFS=$' \t\n'

local expected="$1"
Expand All@@ -767,6 +827,10 @@ function assert_string_ends_with() {
function assert_string_not_ends_with() {
local label_override=""
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi
local IFS=$' \t\n'

local expected="$1"
Expand All@@ -787,6 +851,10 @@ function assert_string_not_ends_with() {

function assert_less_than() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi

local expected="$1"
local actual="$2"
Expand All@@ -802,6 +870,10 @@ function assert_less_than() {

function assert_less_or_equal_than() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi

local expected="$1"
local actual="$2"
Expand All@@ -817,6 +889,10 @@ function assert_less_or_equal_than() {

function assert_greater_than() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi

local expected="$1"
local actual="$2"
Expand All@@ -832,6 +908,10 @@ function assert_greater_than() {

function assert_greater_or_equal_than() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi

local expected="$1"
local actual="$2"
Expand DownExpand Up@@ -868,6 +948,10 @@ function bashunit::assert::_is_numeric() {
##
function assert_within_delta() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 3 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 3 "expected, actual, delta" "$#"
return 2
fi

local expected="$1"
local actual="$2"
Expand DownExpand Up@@ -948,6 +1032,10 @@ function assert_within_delta() {

function assert_line_count() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi
local IFS=$' \t\n'

local expected="$1"
Expand DownExpand Up@@ -1029,6 +1117,10 @@ function bashunit::format_to_regex() {

function assert_string_matches_format() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "format, actual" "$#"
return 2
fi

local format="$1"
local actual="$2"
Expand All@@ -1047,6 +1139,10 @@ function assert_string_matches_format() {

function assert_string_not_matches_format() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "format, actual" "$#"
return 2
fi

local format="$1"
local actual="$2"
Expand Down
40 changes: 39 additions & 1 deletion src/runner/diagnostics.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -31,12 +31,50 @@ function bashunit::runner::halt_if_stop_on_failure() {
}

# Writes the detected runtime-error message (empty when none) into
# _BASHUNIT_RUNNER_RUNTIME_ERROR_OUT. Return-slot form avoids a per-test fork
# _BASHUNIT_RUNNER_RUNTIME_ERROR_OUT and display-safe output into
# _BASHUNIT_RUNNER_RUNTIME_OUTPUT_OUT. Return-slot form avoids a per-test fork
# on the hot path (#764).
# Arguments: $1 runtime_output
function bashunit::runner::detect_runtime_error() {
local runtime_output=$1
_BASHUNIT_RUNNER_RUNTIME_ERROR_OUT=""
_BASHUNIT_RUNNER_RUNTIME_OUTPUT_OUT=$runtime_output

local usage_prefix="bashunit: assertion usage error: "
local usage_marker=$'\n'"$usage_prefix"
local usage_before=""
local usage_rest=""
local usage_found=false
case "$runtime_output" in
"$usage_prefix"*)
usage_rest=${runtime_output#"$usage_prefix"}
usage_found=true
;;
*"$usage_marker"*)
usage_before=${runtime_output%%"$usage_marker"*}
usage_rest=${runtime_output#*"$usage_marker"}
usage_found=true
;;
esac

if [ "$usage_found" = true ]; then
local usage_error=${usage_rest%%$'\n'*}
local usage_after=""
if [ "$usage_rest" != "$usage_error" ]; then
usage_after=${usage_rest#*$'\n'}
fi
_BASHUNIT_RUNNER_RUNTIME_ERROR_OUT=$usage_error
if [ -n "$usage_before" ] && [ -n "$usage_after" ]; then
_BASHUNIT_RUNNER_RUNTIME_OUTPUT_OUT="$usage_before
$usage_after"
elif [ -n "$usage_before" ]; then
_BASHUNIT_RUNNER_RUNTIME_OUTPUT_OUT=$usage_before
else
_BASHUNIT_RUNNER_RUNTIME_OUTPUT_OUT=$usage_after
fi
return
fi

case "$runtime_output" in
*"command not found"* | *"unbound variable"* | *"permission denied"* | \
*"no such file or directory"* | *"syntax error"* | *"bad substitution"* | \
Expand Down
3 changes: 2 additions & 1 deletion src/runner/exec.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -326,6 +326,7 @@ function bashunit::runner::run_test() {
local attempt_runtime_output="${test_execution_result%%##ASSERTIONS_*}"
bashunit::runner::detect_runtime_error "$attempt_runtime_output"
local attempt_runtime_error=$_BASHUNIT_RUNNER_RUNTIME_ERROR_OUT
local attempt_display_output=$_BASHUNIT_RUNNER_RUNTIME_OUTPUT_OUT
bashunit::runner::extract_result_counts "$test_execution_result"
# Mirror the commit-phase failure test exactly (runtime error, non-zero exit,
# or a failed assertion); snapshot/incomplete/skipped/risky are not failures.
Expand DownExpand Up@@ -373,7 +374,7 @@ function bashunit::runner::run_test() {
# Reuse the final attempt's values (the loop always runs at least once and
# its locals persist in this function scope), instead of recomputing and
# forking detect_runtime_error a second time (#764).
local runtime_output=$attempt_runtime_output
local runtime_output=$attempt_display_output
local runtime_error=$attempt_runtime_error

# parse_result accumulates _BASHUNIT_TEST_EXIT_CODE; reset it so each test's
Expand Down
14 changes: 14 additions & 0 deletions tests/acceptance/bashunit_assert_arity_test.sh
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
#!/usr/bin/env bash
set -euo pipefail

function test_missing_assertion_argument_is_a_usage_error() {
local fixture=tests/acceptance/fixtures/assert_arity/missing.sh
local output exit_code=0

output=$(NO_COLOR=1 ./bashunit --no-parallel --skip-env-file "$fixture" 2>&1) || exit_code=$?

assert_same 1 "$exit_code"
assert_contains "✗ Error: Wrong arg count" "$output"
assert_contains "assert_same expects 2 arguments (expected, actual), got 1" "$output"
assert_not_contains "but got" "$output"
}
7 changes: 4 additions & 3 deletions tests/acceptance/bashunit_strict_mode_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -44,14 +44,15 @@ function test_strict_mode_fails_on_unset_variable_in_set_up() {
assert_contains "failed" "$output"
}

function test_strict_mode_reports_an_omitted_variadic_actual_as_a_plain_failure() {
function test_strict_mode_reports_an_omitted_variadic_actual_as_a_usage_error() {
local output
output=$(BASHUNIT_STRICT_MODE=true ./bashunit --no-parallel --simple --skip-env-file --env "$TEST_ENV_FILE" \
tests/acceptance/fixtures/strict_mode_variadic_actual_omitted.sh 2>&1) || true

assert_not_contains "unbound variable" "$output"
assert_contains "to contain" "$output"
assert_contains "to start with" "$output"
assert_contains "✗ Error" "$output"
assert_contains "assert_contains expects 2 arguments (expected, actual), got 1" "$output"
assert_contains "assert_string_starts_with expects 2 arguments (expected, actual), got 1" "$output"
}

function test_cli_flag_overrides_env_var() {
Expand Down
6 changes: 6 additions & 0 deletions tests/acceptance/fixtures/assert_arity/missing.sh
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
#!/usr/bin/env bash

function test_wrong_arg_count() {
assert_same "only-one"
assert_same "later assertion" "later assertion"
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,8 +2,8 @@

# The "actual" operand of these assertions is variadic ("${@:2}"), so omitting
# it leaves an empty array. Expanding an empty array under `set -u` (--strict)
# is an unbound-variable error on Bash < 4.4, which used to abort the test with
# an internal error instead of reporting a normal assertion failure.
# is an unbound-variable error on Bash < 4.4, which used to abort the test. The
# assertions now reject the missing operand as a usage error first.
function test_contains_without_actual() {
assert_contains "needle"
}
Expand Down
Loading
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
96 changes: 96 additions & 0 deletions src/assert/core.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,6 +20,18 @@ function bashunit::assert::should_skip() {
bashunit::env::is_stop_on_assertion_failure_enabled && ((_BASHUNIT_ASSERTION_FAILED_IN_TEST))
}

# Emits a machine-detectable assertion usage error. The runner strips the
# prefix and reports the message through the existing Error channel.
function bashunit::assert::usage_error() {
local assertion=$1
local required=$2
local signature=$3
local supplied=$4

printf 'bashunit: assertion usage error: %s expects %s arguments (%s), got %s\n' \
"$assertion" "$required" "$signature" "$supplied" >&2
}

_BASHUNIT_ASSERT_LABEL_OUT=""

# Resolve assertion label into the slot _BASHUNIT_ASSERT_LABEL_OUT with no fork:
Expand DownExpand Up@@ -207,6 +219,10 @@ function bashunit::handle_bool_assertion_failure() {

function assert_same() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi

local expected="$1"
local actual="$2"
Expand All@@ -222,6 +238,10 @@ function assert_same() {

function assert_equals() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi

local expected="$1"
local actual="$2"
Expand All@@ -242,6 +262,10 @@ function assert_equals() {

function assert_not_equals() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi

local expected="$1"
local actual="$2"
Expand DownExpand Up@@ -290,6 +314,10 @@ function assert_not_empty() {

function assert_not_same() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi

local expected="$1"
local actual="$2"
Expand All@@ -305,6 +333,10 @@ function assert_not_same() {

function assert_contains() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi
local IFS=$' \t\n'

local expected="$1"
Expand DownExpand Up@@ -339,6 +371,10 @@ function bashunit::assert::_supports_nocasematch() {

function assert_contains_ignore_case() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi

local expected="$1"
local actual="$2"
Expand DownExpand Up@@ -396,6 +432,10 @@ function assert_contains_ignore_case() {
function assert_not_contains() {
local label_override=""
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi
local IFS=$' \t\n'

local expected="$1"
Expand All@@ -416,6 +456,10 @@ function assert_not_contains() {

function assert_matches() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "pattern, actual" "$#"
return 2
fi
local IFS=$' \t\n'

local expected="$1"
Expand All@@ -442,6 +486,10 @@ function assert_matches() {
function assert_not_matches() {
local label_override=""
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "pattern, actual" "$#"
return 2
fi
local IFS=$' \t\n'

local expected="$1"
Expand DownExpand Up@@ -706,6 +754,10 @@ function assert_command_not_found() {
function assert_string_starts_with() {
local label_override=""
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi
local IFS=$' \t\n'

local expected="$1"
Expand All@@ -727,6 +779,10 @@ function assert_string_starts_with() {

function assert_string_not_starts_with() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi

local expected="$1"
local actual="$2"
Expand All@@ -745,6 +801,10 @@ function assert_string_not_starts_with() {
function assert_string_ends_with() {
local label_override=""
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi
local IFS=$' \t\n'

local expected="$1"
Expand All@@ -767,6 +827,10 @@ function assert_string_ends_with() {
function assert_string_not_ends_with() {
local label_override=""
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi
local IFS=$' \t\n'

local expected="$1"
Expand All@@ -787,6 +851,10 @@ function assert_string_not_ends_with() {

function assert_less_than() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi

local expected="$1"
local actual="$2"
Expand All@@ -802,6 +870,10 @@ function assert_less_than() {

function assert_less_or_equal_than() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi

local expected="$1"
local actual="$2"
Expand All@@ -817,6 +889,10 @@ function assert_less_or_equal_than() {

function assert_greater_than() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi

local expected="$1"
local actual="$2"
Expand All@@ -832,6 +908,10 @@ function assert_greater_than() {

function assert_greater_or_equal_than() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi

local expected="$1"
local actual="$2"
Expand DownExpand Up@@ -868,6 +948,10 @@ function bashunit::assert::_is_numeric() {
##
function assert_within_delta() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 3 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 3 "expected, actual, delta" "$#"
return 2
fi

local expected="$1"
local actual="$2"
Expand DownExpand Up@@ -948,6 +1032,10 @@ function assert_within_delta() {

function assert_line_count() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "expected, actual" "$#"
return 2
fi
local IFS=$' \t\n'

local expected="$1"
Expand DownExpand Up@@ -1029,6 +1117,10 @@ function bashunit::format_to_regex() {

function assert_string_matches_format() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "format, actual" "$#"
return 2
fi

local format="$1"
local actual="$2"
Expand All@@ -1047,6 +1139,10 @@ function assert_string_matches_format() {

function assert_string_not_matches_format() {
bashunit::assert::should_skip && return 0
if [ "$#" -lt 2 ]; then
bashunit::assert::usage_error "${FUNCNAME[0]}" 2 "format, actual" "$#"
return 2
fi

local format="$1"
local actual="$2"
Expand Down
40 changes: 39 additions & 1 deletion src/runner/diagnostics.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -31,12 +31,50 @@ function bashunit::runner::halt_if_stop_on_failure() {
}

# Writes the detected runtime-error message (empty when none) into
# _BASHUNIT_RUNNER_RUNTIME_ERROR_OUT. Return-slot form avoids a per-test fork
# _BASHUNIT_RUNNER_RUNTIME_ERROR_OUT and display-safe output into
# _BASHUNIT_RUNNER_RUNTIME_OUTPUT_OUT. Return-slot form avoids a per-test fork
# on the hot path (#764).
# Arguments: $1 runtime_output
function bashunit::runner::detect_runtime_error() {
local runtime_output=$1
_BASHUNIT_RUNNER_RUNTIME_ERROR_OUT=""
_BASHUNIT_RUNNER_RUNTIME_OUTPUT_OUT=$runtime_output

local usage_prefix="bashunit: assertion usage error: "
local usage_marker=$'\n'"$usage_prefix"
local usage_before=""
local usage_rest=""
local usage_found=false
case "$runtime_output" in
"$usage_prefix"*)
usage_rest=${runtime_output#"$usage_prefix"}
usage_found=true
;;
*"$usage_marker"*)
usage_before=${runtime_output%%"$usage_marker"*}
usage_rest=${runtime_output#*"$usage_marker"}
usage_found=true
;;
esac

if [ "$usage_found" = true ]; then
local usage_error=${usage_rest%%$'\n'*}
local usage_after=""
if [ "$usage_rest" != "$usage_error" ]; then
usage_after=${usage_rest#*$'\n'}
fi
_BASHUNIT_RUNNER_RUNTIME_ERROR_OUT=$usage_error
if [ -n "$usage_before" ] && [ -n "$usage_after" ]; then
_BASHUNIT_RUNNER_RUNTIME_OUTPUT_OUT="$usage_before
$usage_after"
elif [ -n "$usage_before" ]; then
_BASHUNIT_RUNNER_RUNTIME_OUTPUT_OUT=$usage_before
else
_BASHUNIT_RUNNER_RUNTIME_OUTPUT_OUT=$usage_after
fi
return
fi

case "$runtime_output" in
*"command not found"* | *"unbound variable"* | *"permission denied"* | \
*"no such file or directory"* | *"syntax error"* | *"bad substitution"* | \
Expand Down
3 changes: 2 additions & 1 deletion src/runner/exec.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -326,6 +326,7 @@ function bashunit::runner::run_test() {
local attempt_runtime_output="${test_execution_result%%##ASSERTIONS_*}"
bashunit::runner::detect_runtime_error "$attempt_runtime_output"
local attempt_runtime_error=$_BASHUNIT_RUNNER_RUNTIME_ERROR_OUT
local attempt_display_output=$_BASHUNIT_RUNNER_RUNTIME_OUTPUT_OUT
bashunit::runner::extract_result_counts "$test_execution_result"
# Mirror the commit-phase failure test exactly (runtime error, non-zero exit,
# or a failed assertion); snapshot/incomplete/skipped/risky are not failures.
Expand DownExpand Up@@ -373,7 +374,7 @@ function bashunit::runner::run_test() {
# Reuse the final attempt's values (the loop always runs at least once and
# its locals persist in this function scope), instead of recomputing and
# forking detect_runtime_error a second time (#764).
local runtime_output=$attempt_runtime_output
local runtime_output=$attempt_display_output
local runtime_error=$attempt_runtime_error

# parse_result accumulates _BASHUNIT_TEST_EXIT_CODE; reset it so each test's
Expand Down
14 changes: 14 additions & 0 deletions tests/acceptance/bashunit_assert_arity_test.sh
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
#!/usr/bin/env bash
set -euo pipefail

function test_missing_assertion_argument_is_a_usage_error() {
local fixture=tests/acceptance/fixtures/assert_arity/missing.sh
local output exit_code=0

output=$(NO_COLOR=1 ./bashunit --no-parallel --skip-env-file "$fixture" 2>&1) || exit_code=$?

assert_same 1 "$exit_code"
assert_contains "✗ Error: Wrong arg count" "$output"
assert_contains "assert_same expects 2 arguments (expected, actual), got 1" "$output"
assert_not_contains "but got" "$output"
}
7 changes: 4 additions & 3 deletions tests/acceptance/bashunit_strict_mode_test.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -44,14 +44,15 @@ function test_strict_mode_fails_on_unset_variable_in_set_up() {
assert_contains "failed" "$output"
}

function test_strict_mode_reports_an_omitted_variadic_actual_as_a_plain_failure() {
function test_strict_mode_reports_an_omitted_variadic_actual_as_a_usage_error() {
local output
output=$(BASHUNIT_STRICT_MODE=true ./bashunit --no-parallel --simple --skip-env-file --env "$TEST_ENV_FILE" \
tests/acceptance/fixtures/strict_mode_variadic_actual_omitted.sh 2>&1) || true

assert_not_contains "unbound variable" "$output"
assert_contains "to contain" "$output"
assert_contains "to start with" "$output"
assert_contains "✗ Error" "$output"
assert_contains "assert_contains expects 2 arguments (expected, actual), got 1" "$output"
assert_contains "assert_string_starts_with expects 2 arguments (expected, actual), got 1" "$output"
}

function test_cli_flag_overrides_env_var() {
Expand Down
6 changes: 6 additions & 0 deletions tests/acceptance/fixtures/assert_arity/missing.sh
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
#!/usr/bin/env bash

function test_wrong_arg_count() {
assert_same "only-one"
assert_same "later assertion" "later assertion"
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,8 +2,8 @@

# The "actual" operand of these assertions is variadic ("${@:2}"), so omitting
# it leaves an empty array. Expanding an empty array under `set -u` (--strict)
# is an unbound-variable error on Bash < 4.4, which used to abort the test with
# an internal error instead of reporting a normal assertion failure.
# is an unbound-variable error on Bash < 4.4, which used to abort the test. The
# assertions now reject the missing operand as a usage error first.
function test_contains_without_actual() {
assert_contains "needle"
}
Expand Down
Loading
Loading