Skip to content

Drop bash 5 assumptions from the test harness#70

Merged
jserv merged 1 commit into
mainfrom
bash-compat
Jun 5, 2026
Merged

Drop bash 5 assumptions from the test harness#70
jserv merged 1 commit into
mainfrom
bash-compat

Conversation

@jserv
Copy link
Copy Markdown
Contributor

@jserv jserv commented Jun 5, 2026

Makefile invokes scripts via plain "bash tests/foo.sh", so stock macOS picks up /bin/bash 3.2.57 regardless of the env-bash shebang. 2 failures surfaced on a clean Apple Silicon host without Homebrew bash, both under set -u:

  • tests/lib/test-runner.sh:55 read $EPOCHREALTIME (bash 5.0+ builtin), which expanded to the empty string and tripped the unbound-variable check. make test-busybox aborted at the first sample.
  • tests/driver.sh did "unset 'argv[0]'" then expanded "${argv[@]}". bash 3.2 reports the emptied array as unbound; the fix in 4.4+ silently returns zero elements. make check aborted partway through.

Land a shared tests/lib/bash-compat.sh that consolidates the cross-version shims so each script does not have to re-derive them:
bash_compat_require aborts with rc=127 and a brew-install hint if
BASH_VERSINFO is missing or below 3.2. The minimum is parameterised
through BASH_COMPAT_MIN_MAJOR / BASH_COMPAT_MIN_MINOR so a future bump
is a single-line change.

epoch_us prints integer microseconds. It picks the lowest-cost source
at sourcing time: $EPOCHREALTIME on bash 5.0+ (no fork), "date +%s %N"
when the probe shows the system date emits a 9-digit nanosecond field
(GNU coreutils and macOS 14+/Sonoma date both qualify), python3, perl,
or a whole-second fallback. The chosen implementation is bound to one
function name so callers stay version-agnostic.

The file header documents the supported subset: no EPOCHREALTIME, no
declare -A, no mapfile / readarray, no ${var^^} / ${var,,}, and guard
any potentially empty array expansion with ${arr[@]+"${arr[@]}"} so
set -u does not trip on it.

Wire the shim through the four hot scripts:
tests/lib/test-runner.sh sources bash-compat.sh and uses epoch_us in
the run() watchdog. The four "${TEST_RUNNER[@]}" expansions gain the
${TEST_RUNNER[@]+...} guard so the library is self-defending if a
caller ever sources it without setting the array first.

tests/driver.sh drops the "unset argv[0]" dance for an offset slice
"for arg in "${argv[@]:1}"", which produces zero iterations cleanly
even when argv has one element. The downstream "${args[@]}" expansion
picks up the same +guard so tests with no extra args still launch.

tests/test-perf.sh drops its local epoch_us in favor of the shared
helper and sources bash-compat.sh.

tests/test-rosetta-alpine.sh guards args_a / args_b which could be
empty when a pipeline test omits one side of the "--" separator.

Refactor the two "declare -A" uses to indexed-array + lookup pattern:
tests/fetch-fixtures.sh encodes PKGS as "repo:name:version" tuples and
exposes a pkg_version helper. The case "$entry" in "$target") form
with quoted target is glob-safe ("
" and "?" in target match
literally) and the trailing ":" prevents prefix collisions
(main:bash does not match main:bash-completion).

tests/test-matrix.sh encodes EXPECTED_BASELINES as "key |min|max" and
exposes expected_baseline_get with printf -v output parameters.
Parsing is right-anchored (${entry%|} then ${head##|}) so a key
containing a literal "|" would still split correctly; current keys
have none but the form removes the trap. The verify_expected_counts
call site flips to "if ! expected_baseline_get" so a miss stays
silent the way the prior "${arr[$key]:-}" empty-string check did.

Close #66


Summary by cubic

Make the test harness run on stock macOS /bin/bash 3.2 by removing bash 5-only features and adding a shared compatibility shim. Fixes set -u errors, hardens array handling, and keeps performance timing precise without requiring Homebrew bash.

  • Bug Fixes

    • Address set -u failures on bash 3.2: replace $EPOCHREALTIME, guard empty array expansions, and switch tests/driver.sh to "${argv[@]:1}" instead of unsetting argv[0].
    • Guard ${TEST_RUNNER[@]}/${args[@]} expansions across runners; Rosetta pipeline now handles empty sides safely.
  • Refactors

    • Add tests/lib/bash-compat.sh with bash_compat_require and epoch_us (prefers $EPOCHREALTIME, falls back to date +%s %N, python3, or perl) and source it from tests/lib/test-runner.sh and tests/test-perf.sh.
    • Replace associative arrays with portable indexed arrays: PKGS + pkg_version in tests/fetch-fixtures.sh, EXPECTED_BASELINES + expected_baseline_get in tests/test-matrix.sh.
    • Update docs/testing.md to state bash 3.2+ is sufficient and document the portable bash rules (no associative arrays, no ${var^^}/${var,,}, guard empty array expansions, etc.).

Written for commit 018857e. Summary will update on new commits.

Review in cubic

Makefile invokes scripts via plain "bash tests/foo.sh", so stock macOS
picks up /bin/bash 3.2.57 regardless of the env-bash shebang. 2 failures
surfaced on a clean Apple Silicon host without Homebrew bash, both under
set -u:
- tests/lib/test-runner.sh:55 read $EPOCHREALTIME (bash 5.0+ builtin),
  which expanded to the empty string and tripped the unbound-variable
  check. make test-busybox aborted at the first sample.
- tests/driver.sh did "unset 'argv[0]'" then expanded "${argv[@]}".
  bash 3.2 reports the emptied array as unbound; the fix in 4.4+ silently
  returns zero elements. make check aborted partway through.

Land a shared tests/lib/bash-compat.sh that consolidates the cross-version
shims so each script does not have to re-derive them:
  bash_compat_require aborts with rc=127 and a brew-install hint if
  BASH_VERSINFO is missing or below 3.2. The minimum is parameterised
  through BASH_COMPAT_MIN_MAJOR / BASH_COMPAT_MIN_MINOR so a future bump
  is a single-line change.

  epoch_us prints integer microseconds. It picks the lowest-cost source
  at sourcing time: $EPOCHREALTIME on bash 5.0+ (no fork), "date +%s %N"
  when the probe shows the system date emits a 9-digit nanosecond field
  (GNU coreutils and macOS 14+/Sonoma date both qualify), python3, perl,
  or a whole-second fallback. The chosen implementation is bound to one
  function name so callers stay version-agnostic.

  The file header documents the supported subset: no EPOCHREALTIME, no
  declare -A, no mapfile / readarray, no ${var^^} / ${var,,}, and guard
  any potentially empty array expansion with ${arr[@]+"${arr[@]}"} so
  set -u does not trip on it.

Wire the shim through the four hot scripts:
  tests/lib/test-runner.sh sources bash-compat.sh and uses epoch_us in
  the run() watchdog. The four "${TEST_RUNNER[@]}" expansions gain the
  ${TEST_RUNNER[@]+...} guard so the library is self-defending if a
  caller ever sources it without setting the array first.

  tests/driver.sh drops the "unset argv[0]" dance for an offset slice
  "for arg in "${argv[@]:1}"", which produces zero iterations cleanly
  even when argv has one element. The downstream "${args[@]}" expansion
  picks up the same +guard so tests with no extra args still launch.

  tests/test-perf.sh drops its local epoch_us in favor of the shared
  helper and sources bash-compat.sh.

  tests/test-rosetta-alpine.sh guards args_a / args_b which could be
  empty when a pipeline test omits one side of the "--" separator.

Refactor the two "declare -A" uses to indexed-array + lookup pattern:
  tests/fetch-fixtures.sh encodes PKGS as "repo:name:version" tuples and
  exposes a pkg_version helper. The case "$entry" in "$target"*) form
  with quoted target is glob-safe ("*" and "?" in target match
  literally) and the trailing ":" prevents prefix collisions
  (main:bash does not match main:bash-completion).

  tests/test-matrix.sh encodes EXPECTED_BASELINES as "key |min|max" and
  exposes expected_baseline_get with printf -v output parameters.
  Parsing is right-anchored (${entry%|*} then ${head##*|}) so a key
  containing a literal "|" would still split correctly; current keys
  have none but the form removes the trap. The verify_expected_counts
  call site flips to "if ! expected_baseline_get" so a miss stays
  silent the way the prior "${arr[$key]:-}" empty-string check did.

Close #66
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

No issues found across 8 files

Re-trigger cubic

@jserv jserv merged commit bde0b37 into main Jun 5, 2026
5 checks passed
@jserv jserv deleted the bash-compat branch June 5, 2026 04:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

tests: bash harness fails on stock macOS (bash 3.2)

1 participant