diff --git a/CHANGELOG.md b/CHANGELOG.md index 2df67a5a..8d7e5332 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Hot-path coverage flag now cached in `_BASHUNIT_COVERAGE_ON`, removing a function dispatch per call (#664) - Parallel runner blocks on `wait -n` on Bash 4.3+ instead of polling `jobs -r`, removing sleep-induced slot-release latency (#667) - Hot-path result helpers (`extract_encoded_field`, `extract_subshell_type`, `format_subshell_output`, `compute_total_assertions`) use outvar pattern, dropping a fork per call per test (#662) +- `generate_id` and `normalize_variable_name` drop `grep` and `random_str` forks via pure-bash globbing/inlining (#663) ## [0.36.0](https://github.com/TypedDevs/bashunit/compare/0.35.0...0.36.0) - 2026-05-07 diff --git a/src/helpers.sh b/src/helpers.sh index c8d18311..33a5a0a5 100755 --- a/src/helpers.sh +++ b/src/helpers.sh @@ -268,14 +268,15 @@ function bashunit::helper::find_files_recursive() { function bashunit::helper::normalize_variable_name() { local input_string="$1" - local normalized_string - - normalized_string="${input_string//[^a-zA-Z0-9_]/_}" - - local _re='^[a-zA-Z_]' - if [ "$(builtin echo "$normalized_string" | "$GREP" -cE "$_re" || true)" -eq 0 ]; then - normalized_string="_$normalized_string" - fi + local normalized_string="${input_string//[^a-zA-Z0-9_]/_}" + + # First character must be alpha or underscore. Empty string also gets a `_` + # prefix to satisfy the same identifier rule. Uses pure-bash globbing to + # avoid a per-call grep fork (called once per test via generate_id). + case "${normalized_string:0:1}" in + [a-zA-Z_]) ;; + *) normalized_string="_$normalized_string" ;; + esac builtin echo "$normalized_string" } @@ -439,12 +440,23 @@ function bashunit::helper::get_function_line_number() { function bashunit::helper::generate_id() { local basename="$1" - local sanitized_basename - sanitized_basename="$(bashunit::helper::normalize_variable_name "$basename")" + # Inline normalize_variable_name + random_str to avoid two forks per call. + # generate_id is called once per test and per file load. + local sanitized="${basename//[^a-zA-Z0-9_]/_}" + case "${sanitized:0:1}" in + [a-zA-Z_]) ;; + *) sanitized="_$sanitized" ;; + esac if bashunit::env::is_parallel_run_enabled; then - echo "${sanitized_basename}_$$_$(bashunit::random_str 6)" + local _chars='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' + local _suffix='' + local _i + for ((_i = 0; _i < 6; _i++)); do + _suffix="$_suffix${_chars:RANDOM%${#_chars}:1}" + done + echo "${sanitized}_$$_${_suffix}" else - echo "${sanitized_basename}_$$" + echo "${sanitized}_$$" fi } diff --git a/tests/unit/helpers_test.sh b/tests/unit/helpers_test.sh index 049cbc13..95fbd527 100644 --- a/tests/unit/helpers_test.sh +++ b/tests/unit/helpers_test.sh @@ -127,6 +127,45 @@ function test_check_duplicate_functions_without_function_keyword() { assert_general_error "$(bashunit::helper::check_duplicate_functions "$file")" } +function test_generate_id_uses_pid_suffix_when_not_parallel() { + local _orig="${BASHUNIT_PARALLEL_RUN-}" + export BASHUNIT_PARALLEL_RUN=false + + local id + id="$(bashunit::helper::generate_id "test_foo")" + assert_same "test_foo_$$" "$id" + + export BASHUNIT_PARALLEL_RUN="$_orig" +} + +function test_generate_id_appends_random_suffix_when_parallel() { + local _orig="${BASHUNIT_PARALLEL_RUN-}" + export BASHUNIT_PARALLEL_RUN=true + + local id1 id2 + id1="$(bashunit::helper::generate_id "test_foo")" + id2="$(bashunit::helper::generate_id "test_foo")" + + assert_matches "^test_foo_${$}_[a-zA-Z0-9]{6}$" "$id1" + assert_matches "^test_foo_${$}_[a-zA-Z0-9]{6}$" "$id2" + + export BASHUNIT_PARALLEL_RUN="$_orig" +} + +function test_generate_id_sanitizes_basename() { + local _orig="${BASHUNIT_PARALLEL_RUN-}" + export BASHUNIT_PARALLEL_RUN=false + + local id + id="$(bashunit::helper::generate_id "my-file.sh")" + assert_same "my_file_sh_$$" "$id" + + id="$(bashunit::helper::generate_id "123start")" + assert_same "_123start_$$" "$id" + + export BASHUNIT_PARALLEL_RUN="$_orig" +} + function test_normalize_variable_name() { assert_same "valid_name123" "$(bashunit::helper::normalize_variable_name "valid_name123")" assert_same "non_valid_symbols__________" "$(bashunit::helper::normalize_variable_name "non_valid_symbols!@#$%^&*()")"