diff --git a/.github/ISSUE_TEMPLATE/BUG.md b/.github/ISSUE_TEMPLATE/BUG.md index 0e632865..2f81cb7e 100644 --- a/.github/ISSUE_TEMPLATE/BUG.md +++ b/.github/ISSUE_TEMPLATE/BUG.md @@ -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 diff --git a/.github/docker/Dockerfile b/.github/docker/Dockerfile new file mode 100644 index 00000000..15ac655e --- /dev/null +++ b/.github/docker/Dockerfile @@ -0,0 +1,32 @@ +# Stage 1: Build Bash 3.0 on Ubuntu +FROM ubuntu:20.04 AS builder + +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + build-essential \ + wget \ + ca-certificates && \ + wget https://ftp.gnu.org/gnu/bash/bash-3.0.tar.gz && \ + tar -xzf bash-3.0.tar.gz && \ + cd bash-3.0 && \ + ./configure --prefix=/bash3 --build=x86_64-unknown-linux-gnu && \ + make && make install && \ + cd .. && \ + rm -rf bash-3.0* + +# Stage 2: Minimal Alpine with Bash 3.0 +FROM alpine:3.18 + +# Copy compiled Bash 3.0 from the builder +COPY --from=builder /bash3 /bash3 + +# Create the canonical /usr/bin/bash symlink +RUN ln -sf /bash3/bin/bash /usr/bin/bash + +# Optional: Test version +RUN /usr/bin/bash --version + +WORKDIR /project +CMD ["/usr/bin/bash"] diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 33648025..10def780 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -80,6 +80,22 @@ jobs: chown -R builder /project && \ su - builder -c 'cd /project; make test';" + bash30: + name: "Bash 3.0" + runs-on: ubuntu-20.04 + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Build test image + run: docker build -t bash3 -f .github/docker/Dockerfile . + + - name: Run Tests + run: | + docker run --rm -v "$(pwd)":/project -w /project bash3 make test + simple-output: name: "Simple output" runs-on: ubuntu-latest diff --git a/CHANGELOG.md b/CHANGELOG.md index d4f5526c..bfb7e448 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Add support for `.bash` test files - Add runtime check for Bash >= 3.2 - Add fallback for clock with seconds resolution only +- 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 diff --git a/README.md b/README.md index 225950ec..50306e98 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/bashunit b/bashunit index 38e22b94..185e316b 100755 --- a/bashunit +++ b/bashunit @@ -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 @@ -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 @@ -145,7 +148,7 @@ while [[ $# -gt 0 ]]; do trap '' EXIT && exit 0 ;; *) - _RAW_ARGS+=("$1") + _RAW_ARGS[${#_RAW_ARGS[@]}]="$1" ;; esac shift @@ -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 diff --git a/docs/installation.md b/docs/installation.md index c7544fa7..0b956cb0 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -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 diff --git a/src/benchmark.sh b/src/benchmark.sh index 32b9eb91..75eae808 100644 --- a/src/benchmark.sh +++ b/src/benchmark.sh @@ -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 @@ -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")" diff --git a/src/clock.sh b/src/clock.sh index 27aed6cf..6ed430f3 100644 --- a/src/clock.sh +++ b/src/clock.sh @@ -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) @@ -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 diff --git a/src/console_results.sh b/src/console_results.sh index 92d18723..bc2b2e54 100644 --- a/src/console_results.sh +++ b/src/console_results.sh @@ -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" @@ -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 @@ -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" @@ -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" diff --git a/src/globals.sh b/src/globals.sh index e11705fc..5eb62998 100644 --- a/src/globals.sh +++ b/src/globals.sh @@ -32,7 +32,7 @@ function random_str() { local chars='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' local str='' for (( i=0; i&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" }