diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b8aba65..a47b02f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +### Performance +- Faster runtime-error detection: single `case` glob instead of 23-iteration loop in `detect_runtime_error` (#668) + ## [0.36.0](https://github.com/TypedDevs/bashunit/compare/0.35.0...0.36.0) - 2026-05-07 ### Fixed diff --git a/src/runner.sh b/src/runner.sh index 34abaf24..be5cdcbf 100755 --- a/src/runner.sh +++ b/src/runner.sh @@ -72,7 +72,7 @@ function bashunit::runner::compute_total_assertions() { incomplete="${incomplete%%##*}" snapshot="${test_execution_result##*##ASSERTIONS_SNAPSHOT=}" snapshot="${snapshot%%##*}" - printf '%d' "$(( ${failed:-0} + ${passed:-0} + ${skipped:-0} + ${incomplete:-0} + ${snapshot:-0} ))" + printf '%d' "$((${failed:-0} + ${passed:-0} + ${skipped:-0} + ${incomplete:-0} + ${snapshot:-0}))" } function bashunit::runner::extract_subshell_type() { @@ -92,24 +92,21 @@ function bashunit::runner::format_subshell_output() { function bashunit::runner::detect_runtime_error() { local runtime_output=$1 - local error - for error in "command not found" "unbound variable" "permission denied" \ - "no such file or directory" "syntax error" "bad substitution" \ - "division by 0" "cannot allocate memory" "bad file descriptor" \ - "segmentation fault" "illegal option" "argument list too long" \ - "readonly variable" "missing keyword" "killed" \ - "cannot execute binary file" "invalid arithmetic operator" \ - "ambiguous redirect" "integer expression expected" \ - "too many arguments" "value too great" \ - "not a valid identifier" "unexpected EOF"; do - case "$runtime_output" in - *"$error"*) - local runtime_error="${runtime_output#*: }" - printf '%s' "${runtime_error//$'\n'/}" - return 0 - ;; - esac - done + case "$runtime_output" in + *"command not found"* | *"unbound variable"* | *"permission denied"* | \ + *"no such file or directory"* | *"syntax error"* | *"bad substitution"* | \ + *"division by 0"* | *"cannot allocate memory"* | *"bad file descriptor"* | \ + *"segmentation fault"* | *"illegal option"* | *"argument list too long"* | \ + *"readonly variable"* | *"missing keyword"* | *"killed"* | \ + *"cannot execute binary file"* | *"invalid arithmetic operator"* | \ + *"ambiguous redirect"* | *"integer expression expected"* | \ + *"too many arguments"* | *"value too great"* | \ + *"not a valid identifier"* | *"unexpected EOF"*) + local runtime_error="${runtime_output#*: }" + printf '%s' "${runtime_error//$'\n'/}" + return 0 + ;; + esac printf '' } @@ -203,8 +200,8 @@ function bashunit::runner::load_test_files() { source_err="$(cat "$source_err_file")" fi rm -f "$source_err_file" - if [ "$source_status" -ne 0 ] || [ "$(printf '%s' "$source_err" \ - | "$GREP" -cE 'syntax error|unexpected EOF' || true)" -gt 0 ]; then + if [ "$source_status" -ne 0 ] || [ "$(printf '%s' "$source_err" | + "$GREP" -cE 'syntax error|unexpected EOF' || true)" -gt 0 ]; then local message="$source_err" [ -z "$message" ] && message="Failed to source '$test_file' (exit $source_status)" bashunit::runner::record_file_hook_failure \ diff --git a/tests/unit/runner_test.sh b/tests/unit/runner_test.sh index 66df4e42..eff156b0 100644 --- a/tests/unit/runner_test.sh +++ b/tests/unit/runner_test.sh @@ -47,3 +47,56 @@ function test_extract_assertion_runtime_output_keeps_user_output_that_looks_like assert_same "✗ Failed: emitted by the code under test" "$actual" } + +function test_detect_runtime_error_returns_empty_when_input_is_empty() { + local actual + actual="$(bashunit::runner::detect_runtime_error "")" + + assert_empty "$actual" +} + +function test_detect_runtime_error_returns_empty_when_no_known_error() { + local actual + actual="$(bashunit::runner::detect_runtime_error "all good here")" + + assert_empty "$actual" +} + +function test_detect_runtime_error_matches_command_not_found() { + local actual + actual="$(bashunit::runner::detect_runtime_error \ + "script.sh: line 3: foo: command not found")" + + assert_same "line 3: foo: command not found" "$actual" +} + +function test_detect_runtime_error_matches_syntax_error() { + local actual + actual="$(bashunit::runner::detect_runtime_error \ + "bash: -c: line 1: syntax error near unexpected token")" + + assert_same "-c: line 1: syntax error near unexpected token" "$actual" +} + +function test_detect_runtime_error_matches_killed() { + local actual + actual="$(bashunit::runner::detect_runtime_error "process: killed")" + + assert_same "killed" "$actual" +} + +function test_detect_runtime_error_strips_newlines_from_extracted_message() { + local input=$'bash: line 1: foo: command not found\nextra' + local actual + actual="$(bashunit::runner::detect_runtime_error "$input")" + + assert_same "line 1: foo: command not foundextra" "$actual" +} + +function test_detect_runtime_error_matches_unexpected_eof() { + local actual + actual="$(bashunit::runner::detect_runtime_error \ + "bash: line 5: unexpected EOF while looking for matching")" + + assert_same "line 5: unexpected EOF while looking for matching" "$actual" +}