Skip to content

Commit d32aa5d

Browse files
authored
Merge pull request #516 from TypedDevs/perf/optimize-test-runner
Optimize test runner
2 parents 0867947 + 4041c99 commit d32aa5d

File tree

9 files changed

+224
-153
lines changed

9 files changed

+224
-153
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@
1313
### Changed
1414
- Simplify CI: use only `-latest` runners for Ubuntu and macOS, remove deprecated `macos-13`
1515

16+
### Performance
17+
- Optimize temp directory creation: initialize once at startup instead of per temp file
18+
- Optimize parallel result aggregation: use bash redirection instead of spawning `tail` subprocess
19+
- Optimize state access: cache state values to avoid repeated subshell invocations
20+
1621
## [0.26.0](https://github.com/TypedDevs/bashunit/compare/0.25.0...0.26.0) - 2025-11-02
1722

1823
- Add `assert_unsuccessful_code` assertion to check for non-zero exit codes

src/console_header.sh

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ function console_header::print_version() {
2525
local total_tests
2626
if [[ ${#files[@]} -eq 0 ]]; then
2727
total_tests=0
28+
elif parallel::is_enabled && env::is_simple_output_enabled; then
29+
# Skip counting in parallel+simple mode for faster startup
30+
total_tests=0
2831
else
2932
total_tests=$(helper::find_total_tests "$filter" "${files[@]}")
3033
fi

src/console_results.sh

Lines changed: 46 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -16,75 +16,87 @@ function console_results::render_result() {
1616
printf "\n\n"
1717
fi
1818

19+
# Cache state values to avoid repeated subshell invocations
20+
local tests_passed=$_TESTS_PASSED
21+
local tests_skipped=$_TESTS_SKIPPED
22+
local tests_incomplete=$_TESTS_INCOMPLETE
23+
local tests_snapshot=$_TESTS_SNAPSHOT
24+
local tests_failed=$_TESTS_FAILED
25+
local assertions_passed=$_ASSERTIONS_PASSED
26+
local assertions_skipped=$_ASSERTIONS_SKIPPED
27+
local assertions_incomplete=$_ASSERTIONS_INCOMPLETE
28+
local assertions_snapshot=$_ASSERTIONS_SNAPSHOT
29+
local assertions_failed=$_ASSERTIONS_FAILED
30+
1931
local total_tests=0
20-
((total_tests += $(state::get_tests_passed))) || true
21-
((total_tests += $(state::get_tests_skipped))) || true
22-
((total_tests += $(state::get_tests_incomplete))) || true
23-
((total_tests += $(state::get_tests_snapshot))) || true
24-
((total_tests += $(state::get_tests_failed))) || true
32+
((total_tests += tests_passed)) || true
33+
((total_tests += tests_skipped)) || true
34+
((total_tests += tests_incomplete)) || true
35+
((total_tests += tests_snapshot)) || true
36+
((total_tests += tests_failed)) || true
2537

2638
local total_assertions=0
27-
((total_assertions += $(state::get_assertions_passed))) || true
28-
((total_assertions += $(state::get_assertions_skipped))) || true
29-
((total_assertions += $(state::get_assertions_incomplete))) || true
30-
((total_assertions += $(state::get_assertions_snapshot))) || true
31-
((total_assertions += $(state::get_assertions_failed))) || true
39+
((total_assertions += assertions_passed)) || true
40+
((total_assertions += assertions_skipped)) || true
41+
((total_assertions += assertions_incomplete)) || true
42+
((total_assertions += assertions_snapshot)) || true
43+
((total_assertions += assertions_failed)) || true
3244

3345
printf "%sTests: %s" "$_COLOR_FAINT" "$_COLOR_DEFAULT"
34-
if [[ "$(state::get_tests_passed)" -gt 0 ]] || [[ "$(state::get_assertions_passed)" -gt 0 ]]; then
35-
printf " %s%s passed%s," "$_COLOR_PASSED" "$(state::get_tests_passed)" "$_COLOR_DEFAULT"
46+
if [[ "$tests_passed" -gt 0 ]] || [[ "$assertions_passed" -gt 0 ]]; then
47+
printf " %s%s passed%s," "$_COLOR_PASSED" "$tests_passed" "$_COLOR_DEFAULT"
3648
fi
37-
if [[ "$(state::get_tests_skipped)" -gt 0 ]] || [[ "$(state::get_assertions_skipped)" -gt 0 ]]; then
38-
printf " %s%s skipped%s," "$_COLOR_SKIPPED" "$(state::get_tests_skipped)" "$_COLOR_DEFAULT"
49+
if [[ "$tests_skipped" -gt 0 ]] || [[ "$assertions_skipped" -gt 0 ]]; then
50+
printf " %s%s skipped%s," "$_COLOR_SKIPPED" "$tests_skipped" "$_COLOR_DEFAULT"
3951
fi
40-
if [[ "$(state::get_tests_incomplete)" -gt 0 ]] || [[ "$(state::get_assertions_incomplete)" -gt 0 ]]; then
41-
printf " %s%s incomplete%s," "$_COLOR_INCOMPLETE" "$(state::get_tests_incomplete)" "$_COLOR_DEFAULT"
52+
if [[ "$tests_incomplete" -gt 0 ]] || [[ "$assertions_incomplete" -gt 0 ]]; then
53+
printf " %s%s incomplete%s," "$_COLOR_INCOMPLETE" "$tests_incomplete" "$_COLOR_DEFAULT"
4254
fi
43-
if [[ "$(state::get_tests_snapshot)" -gt 0 ]] || [[ "$(state::get_assertions_snapshot)" -gt 0 ]]; then
44-
printf " %s%s snapshot%s," "$_COLOR_SNAPSHOT" "$(state::get_tests_snapshot)" "$_COLOR_DEFAULT"
55+
if [[ "$tests_snapshot" -gt 0 ]] || [[ "$assertions_snapshot" -gt 0 ]]; then
56+
printf " %s%s snapshot%s," "$_COLOR_SNAPSHOT" "$tests_snapshot" "$_COLOR_DEFAULT"
4557
fi
46-
if [[ "$(state::get_tests_failed)" -gt 0 ]] || [[ "$(state::get_assertions_failed)" -gt 0 ]]; then
47-
printf " %s%s failed%s," "$_COLOR_FAILED" "$(state::get_tests_failed)" "$_COLOR_DEFAULT"
58+
if [[ "$tests_failed" -gt 0 ]] || [[ "$assertions_failed" -gt 0 ]]; then
59+
printf " %s%s failed%s," "$_COLOR_FAILED" "$tests_failed" "$_COLOR_DEFAULT"
4860
fi
4961
printf " %s total\n" "$total_tests"
5062

5163
printf "%sAssertions:%s" "$_COLOR_FAINT" "$_COLOR_DEFAULT"
52-
if [[ "$(state::get_tests_passed)" -gt 0 ]] || [[ "$(state::get_assertions_passed)" -gt 0 ]]; then
53-
printf " %s%s passed%s," "$_COLOR_PASSED" "$(state::get_assertions_passed)" "$_COLOR_DEFAULT"
64+
if [[ "$tests_passed" -gt 0 ]] || [[ "$assertions_passed" -gt 0 ]]; then
65+
printf " %s%s passed%s," "$_COLOR_PASSED" "$assertions_passed" "$_COLOR_DEFAULT"
5466
fi
55-
if [[ "$(state::get_tests_skipped)" -gt 0 ]] || [[ "$(state::get_assertions_skipped)" -gt 0 ]]; then
56-
printf " %s%s skipped%s," "$_COLOR_SKIPPED" "$(state::get_assertions_skipped)" "$_COLOR_DEFAULT"
67+
if [[ "$tests_skipped" -gt 0 ]] || [[ "$assertions_skipped" -gt 0 ]]; then
68+
printf " %s%s skipped%s," "$_COLOR_SKIPPED" "$assertions_skipped" "$_COLOR_DEFAULT"
5769
fi
58-
if [[ "$(state::get_tests_incomplete)" -gt 0 ]] || [[ "$(state::get_assertions_incomplete)" -gt 0 ]]; then
59-
printf " %s%s incomplete%s," "$_COLOR_INCOMPLETE" "$(state::get_assertions_incomplete)" "$_COLOR_DEFAULT"
70+
if [[ "$tests_incomplete" -gt 0 ]] || [[ "$assertions_incomplete" -gt 0 ]]; then
71+
printf " %s%s incomplete%s," "$_COLOR_INCOMPLETE" "$assertions_incomplete" "$_COLOR_DEFAULT"
6072
fi
61-
if [[ "$(state::get_tests_snapshot)" -gt 0 ]] || [[ "$(state::get_assertions_snapshot)" -gt 0 ]]; then
62-
printf " %s%s snapshot%s," "$_COLOR_SNAPSHOT" "$(state::get_assertions_snapshot)" "$_COLOR_DEFAULT"
73+
if [[ "$tests_snapshot" -gt 0 ]] || [[ "$assertions_snapshot" -gt 0 ]]; then
74+
printf " %s%s snapshot%s," "$_COLOR_SNAPSHOT" "$assertions_snapshot" "$_COLOR_DEFAULT"
6375
fi
64-
if [[ "$(state::get_tests_failed)" -gt 0 ]] || [[ "$(state::get_assertions_failed)" -gt 0 ]]; then
65-
printf " %s%s failed%s," "$_COLOR_FAILED" "$(state::get_assertions_failed)" "$_COLOR_DEFAULT"
76+
if [[ "$tests_failed" -gt 0 ]] || [[ "$assertions_failed" -gt 0 ]]; then
77+
printf " %s%s failed%s," "$_COLOR_FAILED" "$assertions_failed" "$_COLOR_DEFAULT"
6678
fi
6779
printf " %s total\n" "$total_assertions"
6880

69-
if [[ "$(state::get_tests_failed)" -gt 0 ]]; then
81+
if [[ "$tests_failed" -gt 0 ]]; then
7082
printf "\n%s%s%s\n" "$_COLOR_RETURN_ERROR" " Some tests failed " "$_COLOR_DEFAULT"
7183
console_results::print_execution_time
7284
return 1
7385
fi
7486

75-
if [[ "$(state::get_tests_incomplete)" -gt 0 ]]; then
87+
if [[ "$tests_incomplete" -gt 0 ]]; then
7688
printf "\n%s%s%s\n" "$_COLOR_RETURN_INCOMPLETE" " Some tests incomplete " "$_COLOR_DEFAULT"
7789
console_results::print_execution_time
7890
return 0
7991
fi
8092

81-
if [[ "$(state::get_tests_skipped)" -gt 0 ]]; then
93+
if [[ "$tests_skipped" -gt 0 ]]; then
8294
printf "\n%s%s%s\n" "$_COLOR_RETURN_SKIPPED" " Some tests skipped " "$_COLOR_DEFAULT"
8395
console_results::print_execution_time
8496
return 0
8597
fi
8698

87-
if [[ "$(state::get_tests_snapshot)" -gt 0 ]]; then
99+
if [[ "$tests_snapshot" -gt 0 ]]; then
88100
printf "\n%s%s%s\n" "$_COLOR_RETURN_SNAPSHOT" " Some snapshots created " "$_COLOR_DEFAULT"
89101
console_results::print_execution_time
90102
return 0

src/env.sh

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,10 @@ TERMINAL_WIDTH="$(env::find_terminal_width)"
159159
FAILURES_OUTPUT_PATH=$(mktemp)
160160
CAT="$(command -v cat)"
161161

162+
# Initialize temp directory once at startup for performance
163+
BASHUNIT_TEMP_DIR="${TMPDIR:-/tmp}/bashunit/tmp"
164+
mkdir -p "$BASHUNIT_TEMP_DIR" && chmod -R 777 "$BASHUNIT_TEMP_DIR"
165+
162166
if env::is_dev_mode_enabled; then
163167
internal_log "info" "Dev log enabled" "file:$BASHUNIT_DEV_LOG"
164168
fi

src/globals.sh

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,6 @@ function random_str() {
3939

4040
function temp_file() {
4141
local prefix=${1:-bashunit}
42-
local base_dir="${TMPDIR:-/tmp}/bashunit/tmp"
43-
mkdir -p "$base_dir" && chmod -R 777 "$base_dir"
4442
local test_prefix=""
4543
if [[ -n "${BASHUNIT_CURRENT_TEST_ID:-}" ]]; then
4644
# We're inside a test function - use test ID
@@ -49,13 +47,11 @@ function temp_file() {
4947
# We're at script level (e.g., in set_up_before_script) - use script ID
5048
test_prefix="${BASHUNIT_CURRENT_SCRIPT_ID}_"
5149
fi
52-
mktemp "$base_dir/${test_prefix}${prefix}.XXXXXXX"
50+
mktemp "$BASHUNIT_TEMP_DIR/${test_prefix}${prefix}.XXXXXXX"
5351
}
5452

5553
function temp_dir() {
5654
local prefix=${1:-bashunit}
57-
local base_dir="${TMPDIR:-/tmp}/bashunit/tmp"
58-
mkdir -p "$base_dir" && chmod -R 777 "$base_dir"
5955
local test_prefix=""
6056
if [[ -n "${BASHUNIT_CURRENT_TEST_ID:-}" ]]; then
6157
# We're inside a test function - use test ID
@@ -64,20 +60,20 @@ function temp_dir() {
6460
# We're at script level (e.g., in set_up_before_script) - use script ID
6561
test_prefix="${BASHUNIT_CURRENT_SCRIPT_ID}_"
6662
fi
67-
mktemp -d "$base_dir/${test_prefix}${prefix}.XXXXXXX"
63+
mktemp -d "$BASHUNIT_TEMP_DIR/${test_prefix}${prefix}.XXXXXXX"
6864
}
6965

7066
function cleanup_testcase_temp_files() {
7167
internal_log "cleanup_testcase_temp_files"
7268
if [[ -n "${BASHUNIT_CURRENT_TEST_ID:-}" ]]; then
73-
rm -rf "${TMPDIR:-/tmp}/bashunit/tmp/${BASHUNIT_CURRENT_TEST_ID}"_*
69+
rm -rf "$BASHUNIT_TEMP_DIR/${BASHUNIT_CURRENT_TEST_ID}"_*
7470
fi
7571
}
7672

7773
function cleanup_script_temp_files() {
7874
internal_log "cleanup_script_temp_files"
7975
if [[ -n "${BASHUNIT_CURRENT_SCRIPT_ID:-}" ]]; then
80-
rm -rf "${TMPDIR:-/tmp}/bashunit/tmp/${BASHUNIT_CURRENT_SCRIPT_ID}"_*
76+
rm -rf "$BASHUNIT_TEMP_DIR/${BASHUNIT_CURRENT_SCRIPT_ID}"_*
8177
fi
8278
}
8379

src/helpers.sh

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,18 @@ function helper::normalize_test_function_name() {
4141
fi
4242
# Replace underscores with spaces
4343
result="${result//_/ }"
44-
# Capitalize the first letter
45-
result="$(tr '[:lower:]' '[:upper:]' <<< "${result:0:1}")${result:1}"
44+
# Capitalize the first letter (bash 3.2 compatible, no subprocess)
45+
local first_char="${result:0:1}"
46+
case "$first_char" in
47+
a) first_char='A' ;; b) first_char='B' ;; c) first_char='C' ;; d) first_char='D' ;;
48+
e) first_char='E' ;; f) first_char='F' ;; g) first_char='G' ;; h) first_char='H' ;;
49+
i) first_char='I' ;; j) first_char='J' ;; k) first_char='K' ;; l) first_char='L' ;;
50+
m) first_char='M' ;; n) first_char='N' ;; o) first_char='O' ;; p) first_char='P' ;;
51+
q) first_char='Q' ;; r) first_char='R' ;; s) first_char='S' ;; t) first_char='T' ;;
52+
u) first_char='U' ;; v) first_char='V' ;; w) first_char='W' ;; x) first_char='X' ;;
53+
y) first_char='Y' ;; z) first_char='Z' ;;
54+
esac
55+
result="${first_char}${result:1}"
4656

4757
echo "$result"
4858
}
@@ -74,19 +84,19 @@ function helper::encode_base64() {
7484
local value="$1"
7585

7686
if command -v base64 >/dev/null; then
77-
echo "$value" | base64 | tr -d '\n'
87+
printf '%s' "$value" | base64 -w 0 2>/dev/null || printf '%s' "$value" | base64 | tr -d '\n'
7888
else
79-
echo "$value" | openssl enc -base64 -A
89+
printf '%s' "$value" | openssl enc -base64 -A
8090
fi
8191
}
8292

8393
function helper::decode_base64() {
8494
local value="$1"
8595

8696
if command -v base64 >/dev/null; then
87-
echo "$value" | base64 -d
97+
printf '%s' "$value" | base64 -d
8898
else
89-
echo "$value" | openssl enc -d -base64
99+
printf '%s' "$value" | openssl enc -d -base64
90100
fi
91101
}
92102

@@ -148,7 +158,7 @@ function helper::get_functions_to_run() {
148158
function helper::execute_function_if_exists() {
149159
local fn_name="$1"
150160

151-
if [[ "$(type -t "$fn_name")" == "function" ]]; then
161+
if declare -F "$fn_name" >/dev/null 2>&1; then
152162
"$fn_name"
153163
return $?
154164
fi
@@ -215,8 +225,7 @@ function helper::get_provider_data() {
215225
data_provider_function=$(
216226
# shellcheck disable=SC1087
217227
grep -B 2 -E "function[[:space:]]+$function_name[[:space:]]*\(\)" "$script" 2>/dev/null | \
218-
grep -E "^[[:space:]]*# *@?data_provider[[:space:]]+" | \
219-
sed -E 's/^[[:space:]]*# *@?data_provider[[:space:]]+//' || true
228+
sed -nE 's/^[[:space:]]*# *@?data_provider[[:space:]]+//p'
220229
)
221230

222231
if [[ -n "$data_provider_function" ]]; then

src/parallel.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ function parallel::aggregate_test_results() {
2323

2424
for result_file in "${result_files[@]}"; do
2525
local result_line
26-
result_line=$(tail -n 1 "$result_file")
26+
result_line=$(tail -n 1 < "$result_file")
2727

2828
local failed="${result_line##*##ASSERTIONS_FAILED=}"
2929
failed="${failed%%##*}"; failed=${failed:-0}

src/runner.sh

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -684,9 +684,7 @@ function runner::execute_file_hook() {
684684
local test_file="$2"
685685
local render_header="${3:-false}"
686686

687-
if [[ "$(type -t "$hook_name")" != "function" ]]; then
688-
return 0
689-
fi
687+
declare -F "$hook_name" >/dev/null 2>&1 || return 0
690688

691689
local hook_output=""
692690
local status=0
@@ -738,9 +736,7 @@ function runner::run_tear_down() {
738736
function runner::execute_test_hook() {
739737
local hook_name="$1"
740738

741-
if [[ "$(type -t "$hook_name")" != "function" ]]; then
742-
return 0
743-
fi
739+
declare -F "$hook_name" >/dev/null 2>&1 || return 0
744740

745741
local hook_output=""
746742
local status=0

0 commit comments

Comments
 (0)