From cfc77545a78070263ef393920427cf90df52ebdf Mon Sep 17 00:00:00 2001 From: Chemaclass Date: Tue, 12 May 2026 10:28:34 +0200 Subject: [PATCH 1/2] perf(helpers): inline normalize+random in generate_id; drop grep fork bashunit::helper::normalize_variable_name now uses a pure-bash case glob to test the first-character identifier rule, eliminating a grep fork per call. All callers benefit (generate_id, snapshot, test_doubles). bashunit::helper::generate_id inlines the normalize and random_str logic, saving two subshell captures per call. generate_id is called once per test and per file load, so on a 800-test suite this drops ~1600 forks. Adds direct tests for generate_id covering sync/parallel suffixes and basename sanitization. Closes #663 --- src/helpers.sh | 36 +++++++++++++++++++++++------------ tests/unit/helpers_test.sh | 39 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 12 deletions(-) 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!@#$%^&*()")" From acd60ba2a435ca85a46bd75583f17775e3837374 Mon Sep 17 00:00:00 2001 From: Chemaclass Date: Tue, 12 May 2026 10:35:17 +0200 Subject: [PATCH 2/2] docs(changelog): note generate_id fork removal perf improvement --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) 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