Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/BUG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ labels: bug
| Q | A |
|------------------|--------------------------|
| OS | macOS / Linux / Windows |
| Shell & version | sh 2.0 / bash 3.2 / ... |
| Shell & version | sh 2.0 / bash 3.0 / ... |
| bashunit version | x.y.z |

#### Summary
Expand Down
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

- Update docs mocks usage
- Add support for `.bash` test files
- Add runtime check for Bash >= 3.2
- Lower minimum supported Bash version to 3.0

## [0.22.3](https://github.com/TypedDevs/bashunit/compare/0.22.2...0.22.3) - 2025-07-27

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ You can find the complete documentation for **bashunit** online, including insta

## Requirements

bashunit requires **Bash 3.2** or newer.
bashunit requires **Bash 3.0** or newer.

## Contribute

Expand Down
11 changes: 7 additions & 4 deletions bashunit
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
set -euo pipefail

declare -r BASHUNIT_MIN_BASH_VERSION="3.2"
declare -r BASHUNIT_MIN_BASH_VERSION="3.0"

function _check_bash_version() {
local current_version
Expand All @@ -19,7 +19,10 @@ function _check_bash_version() {
local major minor
IFS=. read -r major minor _ <<< "$current_version"

if (( major < 3 )) || { (( major == 3 )) && (( minor < 2 )); }; then
local min_major min_minor
IFS=. read -r min_major min_minor _ <<< "$BASHUNIT_MIN_BASH_VERSION"

if (( major < min_major )) || { (( major == min_major )) && (( minor < min_minor )); }; then
printf 'Bashunit requires Bash >= %s. Current version: %s\n' "$BASHUNIT_MIN_BASH_VERSION" "$current_version" >&2
exit 1
fi
Expand Down Expand Up @@ -145,7 +148,7 @@ while [[ $# -gt 0 ]]; do
trap '' EXIT && exit 0
;;
*)
_RAW_ARGS+=("$1")
_RAW_ARGS[${#_RAW_ARGS[@]}]="$1"
;;
esac
shift
Expand All @@ -157,7 +160,7 @@ if [[ ${#_RAW_ARGS[@]} -gt 0 ]]; then
[[ "$_BENCH_MODE" == true ]] && pattern='*[bB]ench.sh'
for arg in "${_RAW_ARGS[@]}"; do
while IFS= read -r file; do
_ARGS+=("$file")
_ARGS[${#_ARGS[@]}]="$file"
done < <(helper::find_files_recursive "$arg" "$pattern")
done
fi
Expand Down
2 changes: 1 addition & 1 deletion docs/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Here, we provide different options that you can use to install **bashunit** in y

## Requirements

bashunit requires **Bash 3.2** or newer.
bashunit requires **Bash 3.0** or newer.

## install.sh

Expand Down
12 changes: 6 additions & 6 deletions src/benchmark.sh
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,11 @@ function benchmark::parse_annotations() {
}

function benchmark::add_result() {
_BENCH_NAMES+=("$1")
_BENCH_REVS+=("$2")
_BENCH_ITS+=("$3")
_BENCH_AVERAGES+=("$4")
_BENCH_MAX_MILLIS+=("$5")
_BENCH_NAMES[${#_BENCH_NAMES[@]}]="$1"
_BENCH_REVS[${#_BENCH_REVS[@]}]="$2"
_BENCH_ITS[${#_BENCH_ITS[@]}]="$3"
_BENCH_AVERAGES[${#_BENCH_AVERAGES[@]}]="$4"
_BENCH_MAX_MILLIS[${#_BENCH_MAX_MILLIS[@]}]="$5"
}

# shellcheck disable=SC2155
Expand All @@ -67,7 +67,7 @@ function benchmark::run_function() {
local end_time=$(clock::now)
local dur_ns=$(math::calculate "($end_time - $start_time)")
local dur_ms=$(math::calculate "$dur_ns / 1000000")
durations+=("$dur_ms")
durations[${#durations[@]}]="$dur_ms"

if env::is_bench_mode_enabled; then
local label="$(helper::normalize_test_function_name "$fn_name")"
Expand Down
12 changes: 6 additions & 6 deletions src/clock.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,34 +7,34 @@ function clock::_choose_impl() {
local attempts=()

# 1. Try Perl with Time::HiRes
attempts+=("Perl")
attempts[${#attempts[@]}]="Perl"
if dependencies::has_perl && perl -MTime::HiRes -e "" &>/dev/null; then
_CLOCK_NOW_IMPL="perl"
return 0
fi

# 2. Try Python 3 with time module
attempts+=("Python")
attempts[${#attempts[@]}]="Python"
if dependencies::has_python; then
_CLOCK_NOW_IMPL="python"
return 0
fi

# 3. Try Node.js
attempts+=("Node")
attempts[${#attempts[@]}]="Node"
if dependencies::has_node; then
_CLOCK_NOW_IMPL="node"
return 0
fi
# 4. Windows fallback with PowerShell
attempts+=("PowerShell")
attempts[${#attempts[@]}]="PowerShell"
if check_os::is_windows && dependencies::has_powershell; then
_CLOCK_NOW_IMPL="powershell"
return 0
fi

# 5. Unix fallback using `date +%s%N` (if not macOS or Alpine)
attempts+=("date")
attempts[${#attempts[@]}]="date"
if ! check_os::is_macos && ! check_os::is_alpine; then
local result
result=$(date +%s%N 2>/dev/null)
Expand All @@ -45,7 +45,7 @@ function clock::_choose_impl() {
fi

# 6. Try using native shell EPOCHREALTIME (if available)
attempts+=("EPOCHREALTIME")
attempts[${#attempts[@]}]="EPOCHREALTIME"
if shell_time="$(clock::shell_time)"; then
_CLOCK_NOW_IMPL="shell"
return 0
Expand Down
15 changes: 8 additions & 7 deletions src/console_results.sh
Original file line number Diff line number Diff line change
Expand Up @@ -172,10 +172,11 @@ ${_COLOR_FAILED}βœ— Failed${_COLOR_DEFAULT}: %s
"${function_name}" "${expected}" "${failure_condition_message}" "${actual}")"

if [ -n "$extra_key" ]; then
line+="$(printf "\

${_COLOR_FAINT}%s${_COLOR_DEFAULT} ${_COLOR_BOLD}'%s'${_COLOR_DEFAULT}\n" \
"${extra_key}" "${extra_value}")"
line="${line}$(
printf "%s%s %s'%s'%s\n" \
"${_COLOR_FAINT}" "${extra_key}" \
"${_COLOR_BOLD}" "${extra_value}" "${_COLOR_DEFAULT}"
)"
fi

state::print_line "failed" "$line"
Expand All @@ -200,7 +201,7 @@ function console_results::print_failed_snapshot_test() {
"$snapshot_file" "$actual_file" 2>/dev/null \
| tail -n +6 | sed "s/^/ /")"

line+="$git_diff_output"
line="${line}$git_diff_output"
rm "$actual_file"
fi

Expand All @@ -215,7 +216,7 @@ function console_results::print_skipped_test() {
line="$(printf "${_COLOR_SKIPPED}β†· Skipped${_COLOR_DEFAULT}: %s\n" "${function_name}")"

if [[ -n "$reason" ]]; then
line+="$(printf "${_COLOR_FAINT} %s${_COLOR_DEFAULT}\n" "${reason}")"
line="${line}$(printf "${_COLOR_FAINT} %s${_COLOR_DEFAULT}\n" "${reason}")"
fi

state::print_line "skipped" "$line"
Expand All @@ -229,7 +230,7 @@ function console_results::print_incomplete_test() {
line="$(printf "${_COLOR_INCOMPLETE}βœ’ Incomplete${_COLOR_DEFAULT}: %s\n" "${function_name}")"

if [[ -n "$pending" ]]; then
line+="$(printf "${_COLOR_FAINT} %s${_COLOR_DEFAULT}\n" "${pending}")"
line="${line}$(printf "${_COLOR_FAINT} %s${_COLOR_DEFAULT}\n" "${pending}")"
fi

state::print_line "incomplete" "$line"
Expand Down
2 changes: 1 addition & 1 deletion src/globals.sh
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ function random_str() {
local chars='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
local str=''
for (( i=0; i<length; i++ )); do
str+="${chars:RANDOM%${#chars}:1}"
str="${str}${chars:RANDOM%${#chars}:1}"
done
echo "$str"
}
Expand Down
8 changes: 4 additions & 4 deletions src/helpers.sh
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ function helper::get_functions_to_run() {
if [[ $filtered_functions == *" $fn"* ]]; then
return 1
fi
filtered_functions+=" $fn"
filtered_functions="${filtered_functions} $fn"
fi
done

Expand Down Expand Up @@ -239,7 +239,7 @@ function helpers::find_total_tests() {
for fn_name in "${functions_to_run[@]}"; do
local provider_data=()
Copy link

@akinomyoga akinomyoga Aug 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In Bash 3.0, this doesn't work. This is equivalent to local provider_data='()' and creates a scalar variable containing the string '()' in Bash 3.0.

I just noticed this by looking at the diff, but I haven't checked the entire codebase. There could still be other compatibility issues.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the feedback. I didn't tested with real bash 3.0 binary as I had to leave after I created the PR and wanted to know how far away from the final result this would be. I will continue with it and test it with bash 3.0 binary this weekend. Also, I should find a way to setup this bash 3.0 in the CI to run all tests using that version

while IFS=" " read -r line; do
provider_data+=("$line")
provider_data[${#provider_data[@]}]="$line"
done <<< "$(helper::get_provider_data "$fn_name" "$file")"

if [[ "${#provider_data[@]}" -eq 0 ]]; then
Expand Down Expand Up @@ -268,7 +268,7 @@ function helper::load_test_files() {
if [[ "${#files[@]}" -eq 0 ]]; then
if [[ -n "${BASHUNIT_DEFAULT_PATH}" ]]; then
while IFS='' read -r line; do
test_files+=("$line")
test_files[${#test_files[@]}]="$line"
done < <(helper::find_files_recursive "$BASHUNIT_DEFAULT_PATH")
fi
else
Expand All @@ -287,7 +287,7 @@ function helper::load_bench_files() {
if [[ "${#files[@]}" -eq 0 ]]; then
if [[ -n "${BASHUNIT_DEFAULT_PATH}" ]]; then
while IFS='' read -r line; do
bench_files+=("$line")
bench_files[${#bench_files[@]}]="$line"
done < <(helper::find_files_recursive "$BASHUNIT_DEFAULT_PATH" '*[bB]ench.sh')
fi
else
Expand Down
6 changes: 3 additions & 3 deletions src/main.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ function main::exec_tests() {

local test_files=()
while IFS= read -r line; do
test_files+=("$line")
test_files[${#test_files[@]}]="$line"
done < <(helper::load_test_files "$filter" "${files[@]}")

internal_log "exec_tests" "filter:$filter" "files:${test_files[*]}"
Expand Down Expand Up @@ -81,7 +81,7 @@ function main::exec_benchmarks() {

local bench_files=()
while IFS= read -r line; do
bench_files+=("$line")
bench_files[${#bench_files[@]}]="$line"
done < <(helper::load_bench_files "$filter" "${files[@]}")

internal_log "exec_benchmarks" "filter:$filter" "files:${bench_files[*]}"
Expand Down Expand Up @@ -147,7 +147,7 @@ function main::exec_assert() {
inner_exit_code=$?
# Remove the last argument and append the exit code
args=("${args[@]:0:last_index}")
args+=("$inner_exit_code")
args[${#args[@]}]="$inner_exit_code"
;;
*)
# Add more cases here for other assert_* handlers if needed
Expand Down
10 changes: 5 additions & 5 deletions src/reports.sh
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,11 @@ function reports::add_test() {
local assertions="$4"
local status="$5"

_REPORTS_TEST_FILES+=("$file")
_REPORTS_TEST_NAMES+=("$test_name")
_REPORTS_TEST_STATUSES+=("$status")
_REPORTS_TEST_ASSERTIONS+=("$assertions")
_REPORTS_TEST_DURATIONS+=("$duration")
_REPORTS_TEST_FILES[${#_REPORTS_TEST_FILES[@]}]="$file"
_REPORTS_TEST_NAMES[${#_REPORTS_TEST_NAMES[@]}]="$test_name"
_REPORTS_TEST_STATUSES[${#_REPORTS_TEST_STATUSES[@]}]="$status"
_REPORTS_TEST_ASSERTIONS[${#_REPORTS_TEST_ASSERTIONS[@]}]="$assertions"
_REPORTS_TEST_DURATIONS[${#_REPORTS_TEST_DURATIONS[@]}]="$duration"
}

function reports::generate_junit_xml() {
Expand Down
12 changes: 6 additions & 6 deletions src/runner.sh
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ function runner::call_test_functions() {

local provider_data=()
while IFS=" " read -r line; do
provider_data+=("$line")
provider_data[${#provider_data[@]}]="$line"
done <<< "$(helper::get_provider_data "$fn_name" "$script")"

# No data provider found
Expand Down Expand Up @@ -431,11 +431,11 @@ function runner::parse_result_sync() {

local regex
regex='ASSERTIONS_FAILED=([0-9]*)##'
regex+='ASSERTIONS_PASSED=([0-9]*)##'
regex+='ASSERTIONS_SKIPPED=([0-9]*)##'
regex+='ASSERTIONS_INCOMPLETE=([0-9]*)##'
regex+='ASSERTIONS_SNAPSHOT=([0-9]*)##'
regex+='TEST_EXIT_CODE=([0-9]*)'
regex="${regex}ASSERTIONS_PASSED=([0-9]*)##"
regex="${regex}ASSERTIONS_SKIPPED=([0-9]*)##"
regex="${regex}ASSERTIONS_INCOMPLETE=([0-9]*)##"
regex="${regex}ASSERTIONS_SNAPSHOT=([0-9]*)##"
regex="${regex}TEST_EXIT_CODE=([0-9]*)"

if [[ $result_line =~ $regex ]]; then
assertions_failed="${BASH_REMATCH[1]}"
Expand Down
2 changes: 1 addition & 1 deletion src/state.sh
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ function state::set_file_with_duplicated_function_names() {
}

function state::add_test_output() {
_TEST_OUTPUT+="$1"
_TEST_OUTPUT="${_TEST_OUTPUT}$1"
}

function state::get_test_exit_code() {
Expand Down
10 changes: 5 additions & 5 deletions src/str.sh
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,14 @@ function str::rpad() {
# If the current character is part of an ANSI sequence, skip it and copy it
if [[ "$original_char" == $'\x1b' ]]; then
while [[ "${left_text:$j:1}" != "m" && $j -lt ${#left_text} ]]; do
result_left_text+="${left_text:$j:1}"
result_left_text="${result_left_text}${left_text:$j:1}"
((j++))
done
result_left_text+="${left_text:$j:1}" # Append the final 'm'
result_left_text="${result_left_text}${left_text:$j:1}" # Append the final 'm'
((j++))
elif [[ "$char" == "$original_char" ]]; then
# Match the actual character
result_left_text+="$char"
result_left_text="${result_left_text}$char"
((i++))
((j++))
else
Expand All @@ -50,13 +50,13 @@ function str::rpad() {

local remaining_space
if $is_truncated ; then
result_left_text+="..."
result_left_text="${result_left_text}..."
# 1: due to a blank space
# 3: due to the appended ...
remaining_space=$((width_padding - ${#clean_left_text} - ${#right_word} - 1 - 3))
else
# Copy any remaining characters after the truncation point
result_left_text+="${left_text:$j}"
result_left_text="${result_left_text}${left_text:$j}"
remaining_space=$((width_padding - ${#clean_left_text} - ${#right_word} - 1))
fi

Expand Down
4 changes: 2 additions & 2 deletions src/test_doubles.sh
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ function mock() {

export -f "${command?}"

MOCKED_FUNCTIONS+=("$command")
MOCKED_FUNCTIONS[${#MOCKED_FUNCTIONS[@]}]="$command"
}

function spy() {
Expand All @@ -60,7 +60,7 @@ function spy() {

export -f "${command?}"

MOCKED_FUNCTIONS+=("$command")
MOCKED_FUNCTIONS[${#MOCKED_FUNCTIONS[@]}]="$command"
}

function assert_have_been_called() {
Expand Down
4 changes: 2 additions & 2 deletions tests/unit/bash_version_test.sh
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
#!/usr/bin/env bash

function test_fail_with_old_bash_version() {
output=$(BASHUNIT_TEST_BASH_VERSION=3.1 ./bashunit --version 2>&1)
output=$(BASHUNIT_TEST_BASH_VERSION=2.9 ./bashunit --version 2>&1)
exit_code=$?
assert_contains "Bashunit requires Bash >= 3.2. Current version: 3.1" "$output"
assert_contains "Bashunit requires Bash >= 3.0. Current version: 2.9" "$output"
assert_general_error "$output" "" "$exit_code"
}
Loading