Skip to content

Latest commit

 

History

History
1048 lines (875 loc) · 76.8 KB

File metadata and controls

1048 lines (875 loc) · 76.8 KB

Changelog

Unreleased

Fixed

  • Coverage counts every line of a multiline statement: array literals, quoted strings, heredocs, backslash continuations. An array written one element per line cost one uncovered line per element. Bash 3.x records the assignment on its closing ), so there the whole array read as uncovered. A parent statement's hits no longer mark commands inside command or process substitutions as covered, even within quotes or arrays (#1338)
  • --exclude-filter keeps a comma as part of the test function name. The value was split at the comma, so --exclude-filter 'test_a,{b}' also excluded test_a. Repeat the flag to exclude several names. BASHUNIT_EXCLUDE_FILTER stays comma-separated, so one of its filters cannot hold a literal comma (#1340)
  • A --parallel run no longer replays bash's child setpgid warning as the stderr of a file that wrote nothing. Job control puts each test in its own process group. The parent-side setpgid then loses a harmless race with a child that already exec'd. The child sets the group first, so only the message was ever wrong (#1344)
  • A timed-out test finishes its tear_down. The watchdog allowed a flat 0.3s between SIGTERM and SIGKILL. A loaded machine missed that window in about one parallel full-suite run in three. It now polls for the body to exit, capped at 2s (#1344)

0.50.1 - 2026-08-22

Fixed

  • tear_down_after_script runs when set_up_before_script fails, so it can release file-scoped resources acquired before the failure (#1318)
  • Under --parallel, tear_down_after_script runs after the file's own tests instead of alongside them, so a fixture it releases stays alive for the tests that read it. The same file no longer passed sequentially and failed in parallel (#1320)
  • --stop-on-failure runs tear_down_after_script for the file it halts in, so a sequential run releases what set_up_before_script acquired before the halt (#1321)
  • bashunit bench runs tear_down_after_script before it aborts on a malformed annotation, so the file releases what set_up_before_script acquired (#1322)
  • Ctrl-C runs tear_down_after_script for the file it interrupts in a sequential run, so a file-scoped resource is released. A second Ctrl-C now ends the run even if that hook never returns (#1323)
  • A test killed by --test-timeout runs its tear_down, so a per-test resource is released. Best effort within the watchdog's grace before it sends SIGKILL, so a hook cannot outlive the timeout it cleans up after (#1324)
  • A test file that fails to source sweeps its script temp files, so a bashunit::temp_file it created at top level no longer survives the run. bashunit bench already did this (#1325)
  • A malformed @timeout or @retry runs tear_down_after_script before it aborts the run, so the file releases what set_up_before_script acquired. Sequential and --parallel both leaked it (#1329)
  • Ctrl-C releases what an interrupted --parallel run acquired: the file's tear_down_after_script and the tear_down of a test in flight. The worker that owns the file's hook now handles the signal and reaches its test bodies, which a kill from the parent could not (#1331)
  • A malformed @timeout or @retry fails a --parallel run alongside a passing file. The abort happened inside the file's worker and never reached the parent, so the run printed the error and still exited 0, which kept it out of CI (#1335)

0.50.0 - 2026-08-18

Added

  • -u as the short form of --snapshot-update, the spelling jest and vitest both use (#1293)

Changed

  • --seed <n> now implies --random-order. Replaying a failure with the seed the header printed used to select nothing, so the run came back in defined order and green. An order named explicitly still wins, as in RSpec (#1287)
  • Performance: cold start is about 9ms faster, roughly 15%. The colour palette cost one subshell fork per colour, and every invocation paid it — --version included (#1285)
  • Performance: a --parallel run writing a report is about 1.9x faster and uses 1.8x less CPU. Each result row cost fourteen base64 forks per test, so --log-junit cost several times more than running the tests (#1289)

Fixed

  • --output json and --output junit are no longer corrupted by console text on stdout, which left the document unparseable. Four sources: an empty --parallel selection (#1295), the blank line after a tear_down_after_script (#1297), the duplicate-function abort, and a --parallel worker's stderr replay (#1299)
  • --parallel reports no longer count a file-level hook failure twice. JSON, JUnit, HTML and Markdown listed it as two failed tests while the console summary of the same run said one (#1301)
  • bashunit bench exits non-zero when a benchmark file fails to source or its set_up_before_script fails. It printed the error and exited 0, so the failure never reached CI, and a syntax error silently dropped every bench_ function after it (#1303)
  • --output tap escapes # in a test description, as --report-tap already did. A failing test titled ... # SKIP ... reached the consumer as a skip and left CI silently (#1309)
  • bashunit watch no longer takes an option's value as the path it polls: watch --tag slow tests/ polled a directory named slow. Options may now appear in any position (#1291)
  • The JUnit report escapes the file path; a name holding ", & or < closed the attribute early and the document stopped parsing (#1313)
  • The Cobertura report escapes XML metacharacters in a path, so GitLab, Azure and Jenkins render coverage instead of silently showing nothing (#1311)
  • GitHub Actions annotations encode : and , in their property values; a comma in a test title ended the title there and invented a property (#1307)
  • A ``` fence in a failure message no longer truncates the --report-md summary, which is appended to `$GITHUB_STEP_SUMMARY` (#1305)

Security

  • A test file's path is no longer evaluated as shell. The per-test EXIT trap interpolated the path into the trap string, and a trap body is re-evaluated when it fires, so a filename holding a command substitution executed it — reachable by any run over a tree whose filenames someone else controls, which is what CI does with a checked-out branch. The same mangling meant such a file's assertions never ran and it was reported risky instead of failing (#1315)

Removed

  • bashunit learn, the interactive tutorial. Nobody used it, and it was broken for most of the nine months it shipped without anyone reporting it. Learning bashunit belongs in the docs at https://bashunit.com, not in a subsystem inside the runner — which is also 6% of the distributable. Calling it now says it was removed and points there (#1256, #1258)

0.49.0 - 2026-08-16

Added

  • --pass-with-no-tests exits 0 when a run selects no tests, for the case where that is deliberate — a CI matrix whose shards are not all populated, or a changed-files run that touched no tests. The run still reports No tests found; only the verdict changes. It does not excuse a path that is not on disk. Same flag, same spelling, as jest, vitest, Playwright and Cypress (#1263)
  • --list-tags prints the tags of the selected files, one per line, sorted and deduplicated, and runs nothing. Tags live only in # @tag comments, so a mistyped --tag had no list to check against (#1265)

Changed

  • A test or bench path that does not exist is now named and refused before the run starts, instead of the No tests found / No benchmarks found an empty selection gives. Every genuinely-empty case keeps that message: a directory holding no tests, a --filter or --tag matching nothing, an empty --shard, --changed with no changes, and a glob the shell left unexpanded (#1263)
  • A path argument that selects nothing no longer falls back to BASHUNIT_DEFAULT_PATH. bashunit empty_dir/ ran the default suite and exited 0, reporting a pass for tests the caller never named (#1263)
  • --snapshot-report-unused and --snapshot-prune now report a snapshot whose test file was deleted or renamed — the most common way one is orphaned, and the only kind neither flag could ever see. One whose file exists but was not part of this run is still left alone. This widens what --snapshot-prune deletes (#1194)
  • A --tag matching nothing now names the tags the run saw, or says no test carries one (#1265)
  • Performance: a sequential run is about 3.9ms faster per test file, and the total runtime costs one fork and one subshell less (#1271)
  • Performance: per-test cleanup no longer reads the whole of BASHUNIT_TEMP_DIR, which is shared and survives between runs — a 100-test file went from 978ms to 542ms against 5000 leftovers, and runtime no longer grows with that directory. A file a test writes there by hand, rather than via temp_file/temp_dir, is no longer removed for it (#1269)

Fixed

  • A sequential run no longer leaks a file descriptor per test file. At the limit it stopped executing tests and reported risky while still exiting 0: 120 files under a 120-descriptor limit ran 2 assertions instead of 120 (#1271)
  • Time taken no longer reports 0ms, or a negative duration, for a runtime it could not measure — both read as real measurements. It now says unknown (#1271)
  • A run using --test-timeout no longer leaves its watchdog holding the caller's captured output for the rest of the timeout budget; a nested run under --suite did this to the run that spawned it (#1137)
  • A test that both fails an assertion and hits a shell error no longer reports the failure text with the diagnostic glued onto the end; the error message is the diagnostic alone (#1267)
  • --list under --parallel no longer prints No tests found in the middle of the ids: a listing dispatches no worker, so there is nothing to aggregate (#1007)

0.48.0 - 2026-08-14

Added

  • --verbose warns on Bash 3.x that coverage does not count lines run inside a subshell, so a lower percentage there explains itself (#1112)

Changed

  • Performance: --coverage is roughly 5x faster and --coverage-report-html roughly 19x — this repo went from 16.2s to 2.9s, and a 128-file HTML report from 58.7s to 3.1s (#1092, #1096, #1098, #1099, #1102, #1104, #1110, #1117)
  • Performance: ./build.sh is about 1.9x faster (7.6s to 4.0s), producing a byte-identical artifact (#1233)
  • Performance: cold start makes two fewer forks, about 4ms of a 65ms startup (#1124)
  • The HTML report gained a Failures section with each failure's name, file:line and message, and its summary now counts risky and flaky tests (#1251, #1252)
  • A --filter that matches nothing names the test it most likely meant: filters match the test function name, case-sensitively, not the humanized title in the report (#1237)
  • --env with a space in the path now says the value was split on the first space to pass bootstrap arguments, and that BASHUNIT_BOOTSTRAP takes the path whole (#1247)
  • The bootstrap error names the actual cause — missing, a directory, or not a regular file — instead of cannot read for all of them (#1262)
  • bashunit doc <filter> says No assertion matches '<filter>' instead of printing nothing (#1201)
  • install.sh destination errors no longer advise a -d flag that does not exist; the script takes positional arguments (#1221)
  • bashunit learn generates starter files that are valid bash, and verifies lessons against the learner's code rather than the hint comments in its own template (#1256, #1258)
  • The coverage HTML report handles filenames containing |, < or & (#1254)
  • A test file that fails to source without writing to stderr reports its size, so a truncated file can be told from one whose last command failed (#1137)
  • The example/ demo is covered by the suite; nothing ran it before (#1219)
  • Docs: benchmarks are the bashunit bench [path] subcommand — -s -b and --simple --bench never existed (#1227)
  • Docs: test functions need a literal, lowercase test_ prefix; the guide's camelCase example and its case-insensitive claim were both wrong (#1215)
  • Docs: assert_equals strips ANSI codes, tabs and newlines — not spaces (#1225)
  • Docs: an empty entry in the -e/--env/--boot file assigns an empty value; it does not restore the default (#1217)
  • Docs: assert_matches costs ~2.5ms per call against ~0.065ms for assert_same, so prefer assert_contains for a fixed substring (#1187)
  • Docs: a @data_provider test shares one snapshot across all its values; use assert_match_named_snapshot "$1" for one each (#1185)

Fixed

  • bashunit --output junit produces valid XML, and --parallel --stop-on-failure no longer corrupts a machine --output stream (#1239, #1243)
  • The HTML report escapes test titles instead of writing them into the markup (#1249)
  • --coverage-paths accepts a path containing a space, an apostrophe or a glob character; it used to break the DEBUG trap and fail passing tests (#1245)
  • A piped --parallel run no longer emits a stray \r \r, and an empty one renders its notice on its own line (#1239)
  • Duplicate test functions are detected under --parallel again; the run reported "All tests passed" over a file where one of two same-named tests never ran (#1147)
  • A @data_provider that is undefined or yields no data is reported as an error naming the provider, instead of the test vanishing behind "No tests found" (#1145)
  • A bootstrap file that fails to load reports it and exits non-zero through every path that loads one, instead of leaving the run with no tests and exit 0 (#1179, #1181)
  • bashunit bench reports No benchmarks found and exits non-zero when the path does not exist or holds no bench_ function (#1199)
  • A report path that is a directory fails fast with is a directory, not a file instead of exiting 0 with no report written (#1177)
  • A coverage run that tracked no executable line says so and names --coverage-paths, instead of reporting Coverage 0% is below minimum N% (#1171)
  • assert_exec "cmd" --exit 1 works under --strict; set -e aborted the test before the assertion could read the code (#1207)
  • assert_equals no longer expands backslash escapes while normalizing, so C:\ and C:\\ differ and a literal \t is not a real tab (#1108)
  • assert_file_contains accepts a needle starting with a dash, and assert_file_not_contains matches literally like its counterpart (#1108)
  • The mock/spy misuse message names a helper that exists (bashunit::mock, not mock), and reports a usable-name error instead of a raw bash syntax error (#1136, #1229)
  • A JSON test skipped for a missing jq is reported under its own name, not bashunit::assert_json::require_jq (#1223)
  • A data-provider value ending in a backslash reaches the test instead of arriving unset (#1134)
  • Coverage no longer loses hits recorded inside a command substitution; on Bash 5 a run reported 196 of 236 real hits (#1101)
  • install.sh names the real problem when the destination is unusable, and validates it before any network call (#1197)
  • bashunit init no longer adds a dead BASHUNIT_BOOTSTRAP line to .env on every run, and reports what it wrote (#1175)
  • A run survives its scratch directory going missing and says so once on stderr (#1163, #1167)
  • A run's scratch-directory cleanup can no longer widen to every concurrent run's (#1165)
  • TAP escapes a # in a test name, so check # SKIP me is no longer read as a directive (#1119)
  • GitHub Actions annotations are percent-encoded on Bash 3.0 too (#1121)

0.47.0 - 2026-08-13

Added

  • --output <text|tap|json|junit> prints the JSON and JUnit reports on stdout, so a pipeline needs no temp file; --report-json still writes its file alongside (#1018)
  • bashunit::skip_if, bashunit::skip_unless, bashunit::skip_unless_command <cmd> and bashunit::skip_on <windows|macos|linux> mark a test skipped and end it, replacing bashunit::skip && return (#1019)
  • Per-test # @timeout <seconds>, # @retry <n> and # @skip [reason] annotations override the run-wide flags in both directions; a malformed value aborts the run (#1020)
  • [suite:<name>] sections in .bashunitrc name a set of paths and options; --suite <name> runs one (repeatable) and --list-suites lists them (#1021)
  • --sandbox fails a test that runs an external command it did not mock, and --sandbox-allow <cmd,...> widens the baseline allowlist (ADR-012) (#1022)
  • bashunit::mock_sequence <cmd> <answer>… answers each call with the next entry, so retry loops need no hand-rolled counter file; the last entry repeats once exhausted (#1023)
  • assert_have_never_been_called <cmd> asserts a spied command never ran, printing the recorded calls when it did (#1023)
  • assert_is_file_readable, assert_is_file_writable, assert_is_file_executable, their negatives and assert_is_file_not_empty give files the parity directories already had (#1024)
  • assert_json_key_not_exists checks that a JSON path is absent, and assert_json_length the size of an array, object or string (#1025)
  • bashunit bench --report-json <file> and --report-junit <file> write the benchmark run to disk, so a CI run leaves an artifact to store, chart and compare (#1028)
  • bashunit bench --baseline <file> fails a run when a benchmark is more than --baseline-tolerance percent (default 10) slower than the recorded one, comparing medians; --baseline-update <file> records the new reference (#1029)
  • --snapshot-prune deletes the snapshot files no test resolved, printing every path; full runs only, and never on a run with failures (#1030)

Changed

  • LCOV BRDA carries the arm's execution count instead of a 0/1 taken flag, taken from the arm's first executable line; BRF and BRH are unchanged (#1061)
  • Performance: --coverage is about 10x faster — a run over this repo's src went from 9.23s to 0.96s. The report phase classifies lines, scans declarations and branches and emits the whole LCOV report in one awk invocation per run instead of Bash loops and forks per file, hit data is grouped once, the DEBUG trap rejects untracked lines before recording, and the caches are read through the variable table (#1056, #1057, #1059, #1060, #1084, #1088, #1090)

Fixed

  • Coverage reports every file under --coverage-paths, not only the ones a test executed: an untouched file shows as 0/N (0%) and --coverage-min gates on that denominator. This repo reported 11 of its own 121 files. Percentages drop, because the old ones were measured over the files that ran (#1053)
  • --coverage-diff counts a changed file that no test executed, instead of skipping it and letting a brand new untested file pass a --coverage-min 90 gate. A docs-only commit still reports 100% (#1054)
  • Coverage read a statement ending in ) as a case arm, so x=$(foo) left the denominator while x=$(printf '%s\n') stayed. A ) now closes an arm only when no ( opened earlier on the line, recovering 456 executable lines of this repo's src/. Percentages move in both directions per file (#1055)
  • A brace inside a comment, a string or a heredoc no longer counts towards a function's span, so a single stray { stops swallowing every later function in the file — 11 functions in this repo's src/coverage/lines.sh were reported as 1, and END { inside an embedded awk program was reported as a function. FN, FNDA, FNF and FNH change; lines and branches do not (#1086)

0.46.0 - 2026-08-11

Added

  • --changed [<ref>] runs only the test files git reports as touched since <ref> (default origin/HEAD, then HEAD), covering committed, staged, unstaged and untracked changes (#1010)
  • --order-by <defined|defects|random> picks the execution order; defects runs the last run's failures first and still runs the whole suite (#1011)
  • --list (alias --dry-run) prints the tests a run would execute without running them; --list-format json emits file, function, name, line and tags, honouring every selection flag including --shard (#1007)
  • --exclude-filter <name> skips tests by name, the counterpart of --exclude-tag: repeatable, OR'd, and wins over --filter (#1009)
  • --tag accepts expressions: 'a&&b' (AND), '!a' (NOT) and 'a&&!b'; repeated --tag flags keep OR semantics and --exclude-tag still wins (#1008)
  • # @tags a b above any top-level line tags every test in the file, unioned with per-function # @tag (#1008)
  • --repeat <n> runs each selected test n times to hunt flakiness before CI does: one report line with the aggregate outcome, a failure names its iteration, and repeat wraps --retry (#1013)
  • Flaky is a first-class outcome: a test that only passed after a retry is counted separately, stays inside the pass total so the exit code is unchanged, and is carried into JUnit (<flakyFailure>), TAP, JSON, HTML and GitHub Actions with the first attempt's failure message; --fail-on-flaky turns such a run red (#1012)
  • --coverage-report-cobertura [file] writes Cobertura XML (default coverage/cobertura.xml), the format GitLab merge-request visualisation, Azure DevOps and Jenkins consume, with repo-relative filenames, per-line hits and condition-coverage on branch lines, alongside the LCOV and HTML reports (#1017)
  • --coverage-diff <ref> limits the coverage console report to lines changed since a base ref; --coverage-min then gates on that diff percentage (#1032)
  • --report-md <file> writes a Markdown run summary — verdict, counts table, failures with their message, plus coverage and slowest tests when those ran — and inside GitHub Actions appends it to $GITHUB_STEP_SUMMARY (#1015)
  • GitHub Actions annotations print to stdout automatically inside Actions, carrying the failing test's file and line so they land on the right line of the diff; --gha-annotations <auto|always|never> overrides the detection and action.yml gains an annotations input (#1014)
  • assert_between <min> <max> <actual> and assert_not_between add inclusive numeric-range assertions for integers and decimals (#1026)
  • assert_command_available <command> asserts a command, shell builtin or function resolves through command -v (#1027)
  • --verbose reports the coverage engine in use, and an explicit BASHUNIT_COVERAGE_ENGINE=xtrace the running Bash cannot honour now warns instead of being silently ignored (#1005)

Changed

  • JUnit XML: one <testsuite> per test file with its own counts, time and timestamp instead of a single flat suite, classname on every <testcase>, <failure message="..."> carrying the first informative line of the real message with type="AssertionFailed", <system-out> with the test's captured output, and aggregate totals on <testsuites>, so consumers that group by suite or classname (Jenkins, GitLab, dorny/test-reporter) get real groupings (#1016)
  • Performance: --coverage is about 1.6x to 2.3x faster; executable-line classification no longer forks grep per source line, roughly half of a coverage run's wall time on both engines (#1005)
  • bashunit test --help lists --show-skipped and --show-incomplete, both accepted by the parser but never advertised; BASHUNIT_COVERAGE_SHOW_FUNCTIONS and BASHUNIT_COVERAGE_SHOW_UNCOVERED are registered in src/config/env.sh like every other setting, and .env.example now lists all 66 settings, 19 of which were missing (#1063)
  • Docs: full audit of the reference pages against the code — docs/configuration.md gained the 17 settings it never documented, docs/command-line.md gained the assert subcommand section and real example output, and the coverage settings and diff-coverage narrative now live in one place instead of two that had drifted (#1063)

Fixed

  • Report formats are no longer empty under --parallel; --report-junit, --report-tap, --report-json, --report-html and --log-junit all recorded zero tests, because the rows were collected inside the per-test worker and nothing rebuilt them in the parent (#1004)
  • A failed assertion is no longer reported twice: bashunit::assert_that returns 1 on failure by design, so a custom assertion ending with it made the test body exit non-zero and the runner printed a spurious ✗ Error on top of the ✗ Failed. Custom assertions no longer need a trailing return 0, and a real runtime error is still reported as an error (#1063)
  • --coverage-report with no value uses coverage/lcov.info instead of aborting the run with $2: unbound variable, and no longer consumes a following flag as its filename; write the test path before it, since an optional value cannot be told apart from a path (#1063)
  • --list --list-format json reports each tag as its own array element; the emitter split the tag list on whitespace while every other consumer splits it on commas, so two tags rendered as ["slow,fileTag"] (#1063)
  • assert_within_delta rejects malformed numbers such as 1.2.3 or 5-3 as non-numeric instead of leaking a raw bc parse error or evaluating them as an expression (#1026)
  • Build: the standalone binary size budget is 544 KiB, raised from 500 KiB after ordinary feature growth crossed it; the artifact keeps its indentation rather than being minified (#1045)

0.45.0 - 2026-08-09

Added

  • assert_is_symlink, assert_is_not_symlink and assert_symlink_to assert on a symbolic link itself, which every other filesystem assertion follows through to the target (#981)
  • assert_true and assert_false accept a command with its arguments — assert_true test -d /tmp. A single argument keeps its previous meaning (#994)
  • Named snapshot assertions support multiple snapshots per test; mismatches show the resolved path and --snapshot-update hint (#986)

Changed

  • assert_true / assert_false report unknown command / not executable instead of a bare exit code 127 or 126 (#982)
  • Core comparison assertions report missing required arguments as usage errors instead of comparing against empty values (#983)
  • Performance: Literal snapshots bypass placeholder regex processing unless they contain a placeholder (about 13x faster) (#985)
  • Performance: assert_within_delta uses fixed-point arithmetic for common values, with a bc/awk fallback for unsupported inputs (about 6.6x faster) (#979)
  • Performance: Spy assertions and call counters use builtins instead of cat and command substitutions (about 6.5x faster) (#978)
  • Performance: assert_contains_ignore_case uses Bash's nocasematch where available, falling back to tr on Bash 3.0 (about 10x faster) (#977)
  • Build: standalone binaries omit source comments while preserving heredoc content and source markers, reducing the current artifact by about 22% (#990)
  • Internal: Split src/runner.sh and src/coverage.sh into focused modules with no behavior change; see ADR-010 (#924, #925)

Fixed

  • Snapshot placeholders no longer match anything at all without perl; the grep fallback treated a multi-line pattern as separate alternatives, and now uses awk over the whole value (#1002)
  • Snapshots resolve correctly for an absolute test path; the ./ prefix made them cwd-relative, so a stray snapshot was recorded and every snapshot assertion passed (#1002)
  • assert_false no longer passes when the command does not exist; exit codes 127 and 126 fail both boolean assertions, because the command never ran (#982)
  • assert_have_been_called_times and assert_have_been_called_nth_with report a usage error for a non-numeric count instead of leaking a raw integer expression expected (#984)
  • A failing test whose output quotes a shell-error phrase is no longer also reported as a runtime error (#992)
  • Runtime errors are recognised from the exit code when the diagnostic text is translated or redirected away (#998)
  • assert_within_delta accepts a leading + on any operand (#979)
  • Invalid BASHUNIT_SHARD_INDEX / BASHUNIT_SHARD_TOTAL values now fail with a clear error instead of reaching raw arithmetic or reporting no tests (#969)
  • Date assertions reject unparseable values instead of crashing or treating them as epoch 0 (#968)
  • assert_json_equals rejects invalid JSON instead of considering two unparseable values equal (#967)
  • Parallel runs preserve results from same-named test files in different directories (#959)
  • Coverage no longer counts variable assignments as functions or emits malformed LCOV records for assignments containing | (#936)
  • The nightly coverage workflow discovers nested unit tests while excluding coverage meta-tests and fixtures (#980)
  • build.sh deduplicates embedded files by repository-relative path, preventing duplicate or missing modules with the same filename (#923)
  • bashunit doc no longer errors when the default bootstrap file is missing (#929)

0.44.0 - 2026-07-29

Added

  • bashunit doc --custom lists the assertions your project defines, rendering the comment block above each one; plain bashunit doc appends them as a "Custom assertions" section. Needs --boot / BASHUNIT_BOOTSTRAP, which bashunit doc now accepts (#918)
  • bashunit::assert_once <label> <actual> makes a composed assertion count and report once instead of once per inner step, under its own label. Opt-in, so existing totals are unchanged (#917)
  • assert_assertion_passes, assert_assertion_fails and assert_assertion_fails_with <message> assert the verdict a custom assertion reports. The inner assertion runs isolated (counters, output and stop-on-failure guard restored), so testing a failing assertion no longer means rebuilding its expected output by hand (#916)
  • bashunit::assert_that <expected> <actual> <cmd> [args...] writes a custom assertion in one call: it runs the command and marks the assertion passed or failed, so a forgotten return or bashunit::assertion_passed can no longer drift the two counters apart (#915)
  • bashunit::assertion_failed takes an optional 4th argument labelling the failure block, so a custom assertion can name itself instead of showing the test name (#915)
  • --snapshot-report-unused lists snapshot files no test resolved, the leftovers of a rename or deletion. Reports only, never deletes; refused on partial runs (#902)
  • --no-snapshot-create / BASHUNIT_SNAPSHOT_CREATE=false fails on a missing snapshot instead of recording it. Recommended in CI, where a never-committed snapshot used to pass while asserting nothing (#901)
  • --snapshot-update / BASHUNIT_SNAPSHOT_UPDATE=true re-records existing snapshots (combine with --filter); snapshots holding a placeholder are left alone (#900)
  • bashunit::mock <cmd> <code>: a lone all-digits argument is an exit code, matching bashunit::spy; no throwaway return 1 helper needed (#898)
  • assert_have_been_called_with_any <spy> <expected>: passes when any recorded call matches, not only the last one (#897)
  • A failed call assertion prints the calls recorded for that spy, capped at 10 with an explicit … and N more (#896)
  • assert_have_been_called_with_args <spy> <arg>...: compares the recorded arguments one by one, so cmd "a b" no longer matches cmd a b (#894)
  • BASHUNIT_COVERAGE_ENGINE=auto|xtrace|trap: a new xtrace engine, ~4x cheaper per captured line. Needs Bash 4.1+, so auto (the default) falls back to trap below that (#860)
  • BASHUNIT_COVERAGE_SHOW_LINE_HITS=true prints per-line execution counts in the text coverage report (#856)
  • Docs: an Agentic coding page, linked llms.txt / llms-full.txt, and a drop-in agent skill at bashunit.com/bashunit-skill.md
  • Docs: Nix as an install option, since bashunit is in nixpkgs (nix-shell -p bashunit, nix run nixpkgs#bashunit)

Changed

  • A failed assert_have_been_called_with / _with_args states which call it compared (compared 'the last of 2 calls') (#897)
  • Deprecated forms warn at runtime on stderr; silence with BASHUNIT_NO_DEPRECATION_WARNINGS=true (#866)
  • bashunit::state::print_line / print_tap_line moved to bashunit::console_results::*; no alias kept (#868)
  • bashunit doc and the Assertions page now cover all 71 assertions, with a quick-reference table
  • The --parallel unsupported-OS warning no longer claims Alpine is excluded
  • Docs: bashunit::unmock and the lifecycle of a double: automatic per-test cleanup, set_up_before_script doubles, and what unmock reaches (#899)
  • Docs: invalid-input handling, BASHUNIT_REPORT_TAP/BASHUNIT_REPORT_JSON, the standalone exit code, and the optional ["snapshot_file"] argument
  • Docs: the sidebar outline now lists h3 headings

Fixed

  • Failure and snapshot diffs render with --no-ext-diff, so a configured diff.external (difftastic) no longer blanks them (#912)
  • Time reads and the JUnit report pass the locale via env; the LC_ALL=C cmd prefix segfaults inside $() on Bash 5.3 macOS (#912)
  • Call assertions (assert_not_called, assert_have_been_called*) fail with was never registered as a spy instead of reporting zero calls when the name was never spied, so a typo no longer passes silently (#895)
  • The per-argument form a spy records held a literal $'\x1f' string instead of the separator byte, so it could not be compared against (#894)
  • --parallel no longer discards worker stderr written outside a test body; it renders as a Stderr from <file> block (#864)
  • The minimum-bash gate compares the minor version and parses suffixed versions; the floor is unchanged at Bash 3.0+
  • An empty entry in .env no longer blanks a value the caller exported or passed on the command line (#865)
  • Malformed benchmark annotations (@revs, @its, @max_ms) error instead of silently falling back, and @max_ms accepts a decimal value (#884)
  • An unknown option is rejected instead of being treated as a test path (#871)
  • --jobs, --retry, --test-timeout, --coverage-min and --output reject invalid values; --jobs abc used to hang (#873)
  • A missing --env/--boot file errors instead of a green run that tested nothing; report paths and --seed are validated too (#875)
  • BASHUNIT_COVERAGE_THRESHOLD_LOW/BASHUNIT_COVERAGE_THRESHOLD_HIGH validate as non-negative integers
  • bashunit assert <name> with no arguments errors instead of exiting 0 (#877)
  • The exit-code assertions fail closed on a non-integer exit code, which used to count as passed
  • Variadic assertions with the actual value omitted fail cleanly on Bash 3.2 under --strict
  • assert_arrays_equal failing outside a test function shows its real label
  • Scratch directories that cannot be created under TMPDIR abort with an actionable error
  • A set_up_before_script that changes directory no longer drops the remaining test files
  • A test path combining a glob and a space (./bashunit "my tests/*") is no longer word-split
  • An unreadable or truncated parallel .result file counts as a failed test instead of aborting aggregation
  • release.sh reports a failed rollback as failed
  • Docs: the quickstart's duration format, and the real BASHUNIT_SHOW_EXECUTION_TIME default (auto)

Removed

  • bin/create-pr, an unreferenced vendored copy of Chemaclass/create-pr; use the upstream tool (#867)
  • Dead internal helpers with no remaining callers

0.43.0 - 2026-07-24

Added

  • Branch coverage now reports loop constructs (while/until/for/select): the loop body is a single-arm branch, marked covered only when the loop ran at least once, so a never-entered zero-iteration loop surfaces as an uncovered branch (#855)

Changed

  • Core string assertions (assert_contains/assert_not_contains, assert_matches/assert_not_matches, assert_string_starts_with/assert_string_ends_with and their negations) no longer fork a subshell per call to join their arguments; a fork-free join with identical behaviour replaces it (#844)
  • The array, date, duration, json, files and folders assertions now resolve their failure label through the fork-free slot helper instead of a per-call command substitution — same labels, fewer forks
  • Parallel test workers name their per-test result file by a per-suite ordinal instead of mktemp + mv, removing two forks per test (plus the echo \| tr \| sed arg sanitizing for data-provider tests) with identical result aggregation (#851)

0.42.0 - 2026-07-20

Fixed

  • A failing set_up_before_script/set_up now fails every test in the file with the hook's error, keeps the totals consistent, and lets the suite continue — a strict test file can no longer abort the whole run mid-suite (#836)
  • Run-mode flags (--parallel, --simple, --strict, --retry, --seed, report paths, …) no longer leak through the environment into nested bashunit runs, so a script under test that calls bashunit gets default behavior; BASHUNIT_*=… configuration is unchanged (#834, #837)
  • install.sh fails fast: a failed download, clone, build or copy aborts with a clear error instead of reporting success, and a failed beta clone no longer runs build.sh in the caller's directory (#840)
  • ./build.sh --verify exits non-zero when the built binary fails the suite, and the verification run no longer crashes mid-suite from tests resolving repo paths against the build folder (#834)
  • ./bashunit bench works again from a repository checkout (the dev entrypoint never sourced src/benchmark.sh) (#834)
  • Snapshot placeholders (::ignore::) now work on systems without perl; multi-line placeholders still need perl (#823)
  • Runs no longer leak a scratch directory under $TMPDIR/bashunit/run/ — it is removed on exit, including --version/--help, subcommands and Ctrl-C (#811)
  • bashunit::helper::get_function_line_number no longer disables extdebug for its caller (#808)
  • --test-timeout no longer intermittently reports a fast test as timed out; the watchdog signals by pid and skips a test that already completed

Added

  • watch subcommand no longer fails when neither inotifywait nor fswatch is installed — it falls back to pure-shell polling (interval via BASHUNIT_WATCH_INTERVAL, default 2s) instead of exiting (#779)
  • Shell tab-completion scripts for bash and zsh under completions/ (subcommands, test flags with value hints, assertion names), kept in sync by an anti-drift CI test (#778)
  • --rerun-failed (BASHUNIT_RERUN_FAILED) replays only the previously failing tests, recorded in .bashunit/last-failed; composes with --filter/--tag/--parallel and falls back to the full suite when empty (add .bashunit/ to .gitignore) (#776)
  • Optional nightly coverage.yml workflow publishes a shields.io coverage badge from --coverage over the unit suite; schedule/manual only, never gates merges (#754)
  • --jobs auto / -j auto caps parallel concurrency at the CPU core count (portable across Linux/macOS/BSD); the default stays unlimited (#766)

Changed

  • build.sh hardened: runs under set -euo pipefail, derives the embed list from the entrypoint's source order (single source of truth), guards against duplicate embeds and missing doc markers, drops eval, and gates every build behind bash -n (#834)
  • bashunit doc no longer forks an echo | sed pipe per line of the assertion docs: a single awk pass prints the same bytes in ~50ms instead of ~5s (#832)
  • Multi-file runs are no longer quadratic in file count; bashunit's own 63-file unit suite: ~64s -> ~22s sequential, ~26s -> ~7s parallel (#829)
  • Major performance work with no behaviour change (near fork-free hot paths, cached snapshot/--tag scans, single-pass failure rendering). On bash 3.2: 100x10 assert_equals ~1.50s -> ~0.76s, 500 snapshot assertions ~7.5s -> ~3.0s, 100 tagged tests ~2.92s -> ~0.68s, acceptance suite ~61s -> ~17s (#761-#764, #772-#775, #798, #801-#807, #809, #810, #813, #817)
  • Per-test timing now defaults to auto (BASHUNIT_SHOW_EXECUTION_TIME=true|false|auto): shown only when the clock is fork-free, avoiding perl forks on bash 3.2; --profile/--verbose/reports still measure (see adrs/adr-008-auto-skip-per-test-timing.md) (#765)
  • assert_equals/assert_same failures with multiline values now render a git word-diff below the header (requires git, opt out with BASHUNIT_NO_DIFF=true, respects --no-color); machine reports keep the raw values (#777)

0.41.0 - 2026-07-11

Added

  • --retry <n> / BASHUNIT_RETRY re-runs a failed test up to N times; passes if any attempt passes, annotates retried tests, and works with --parallel and --stop-on-failure (#737)
  • --random-order with --seed <n> / BASHUNIT_SEED randomizes test file and function order to surface inter-test coupling; prints the seed for reproducible replay and works with --parallel (#738)
  • --shard <index>/<total> runs a deterministic, non-overlapping subset of the test files to split a suite across parallel CI runners; composes with --parallel (#739)
  • --report-tap <file> writes a TAP v13 report to a file (complements the streaming --output tap) (#740)
  • --report-json <file> writes results as JSON (summary counts + per-test records); no jq dependency (#741)
  • assert_file_permissions <mode> <file> asserts a file's octal permission mode; portable across GNU/BSD stat (#742)
  • assert_array_length <n> <array> asserts an array has exactly n elements (#743)
  • assert_within_delta <expected> <actual> <delta> asserts a number is within a tolerance; supports floats (#744)

Fixed

  • watch subcommand failed with bashunit::watch::run: command not found in the released binary because src/watch.sh was missing from the build; it is now bundled (#735)

Changed

  • Faster test execution by removing subprocess forks from hot paths (no behaviour change)

0.40.0 - 2026-06-16

Added

  • --test-timeout <seconds> flag and BASHUNIT_TEST_TIMEOUT env var to abort a test that runs longer than N seconds, mark it failed and continue. Disabled by default; no external timeout needed, works on Bash 3.2+ (#721)

Fixed

  • A non-zero test exit no longer leaks into later tests in the same file (per-test exit code is now reset)
  • Coverage now counts backslash line-continuation lines as covered (#722)
  • Spying/mocking the printf builtin no longer breaks coverage collection: the buffer is flushed with builtin printf (#724)

Changed

  • URLs now point to the new primary domain bashunit.com (old bashunit.typeddevs.com redirects)
  • Docs site deploys to GitHub Pages on the bashunit.com custom domain (deploy-gh-pages.yml)

Removed

  • Weekly-downloads chart on the docs homepage (non-portable data source)

0.39.1 - 2026-06-09

Fixed

  • bashunit upgrade resolved the floating v0 Action tag as the latest version, breaking upgrades; it now only considers exact-version tags

0.39.0 - 2026-06-09

Added

  • GitHub Action args input: when set, runs bashunit <args> after installing, so a workflow can install and run the suite in a single step
  • Floating major tag for the GitHub Action: the release process now force-moves v0 to each release, so workflows can pin TypedDevs/bashunit@v0 to track the latest release within a major (#700)
  • bashunit init now scaffolds a .github/workflows/tests.yml CI workflow using the official action (existing files are left untouched) (#702)
  • --report-junit <file> flag as an alias of --log-junit, for naming parity with --report-html (#705)

Changed

  • The GitHub Action's version input now defaults to the version pinned at the action ref instead of latest, so pinning the action by SHA pins a visible bashunit version
  • install.sh now verifies the release checksum by default (set BASHUNIT_VERIFY_CHECKSUM=false to opt out); it soft-skips with a warning when a checksum asset is unavailable unless verification was explicitly requested (#703)
  • --log-gha annotations now include the failing test's line (::error file=…,line=…), so they pin to the exact line in a pull request's "Files changed" tab (#704)

0.38.0 - 2026-06-07

Added

  • Official TypedDevs/bashunit GitHub Action: composite install action, pinnable by commit SHA for immutable installs (version, directory, add-to-path, verify-checksum inputs; path, version outputs) (#695)
  • install.sh sha256 checksum verification via BASHUNIT_VERIFY_CHECKSUM=true, validating the download against the release checksum asset (#695)

Fixed

  • install.sh fails loudly (non-zero exit, no stub binary) on a failed download and retries transient failures, instead of silently reporting success (#695)
  • install.sh creates nested target directories (mkdir -p) (#695)

0.37.0 - 2026-06-03

Added

  • --profile reports the slowest tests (BASHUNIT_PROFILE_COUNT, default 10) (#678)
  • Readable snapshot diff when git is unavailable (#679)
  • Failure output shows the test file:line (#680)
  • .bashunitrc project config file (#681)
  • Killed tests report the cause: timeout, SIGINT, SIGKILL/OOM, SIGTERM (#683)

Fixed

  • --strict skips Bash 3.0's broken pipefail (pipefail only on Bash >= 3.1)
  • bashunit watch forwards --filter and other flags correctly (#682)
  • learn and coverage use mktemp -d for temp directories
  • parallel::cleanup refuses to rm -rf outside */bashunit/parallel/*

Internal

  • Global-slot return pattern for hot-path helpers; _BASHUNIT_SPY_* namespace (#674)
  • Windows acceptance CI: matrix shards now cover every test file (bashunit_test.sh, coverage_*, install, mock were silently skipped) and job labels show only the shard name

Performance

  • Faster runtime-error detection: single case glob (#668)
  • Cache coverage-enabled flag in hot path (#664)
  • Parallel runner uses wait -n on Bash 4.3+ instead of polling (#667)
  • Hot-path result helpers return via global slots, dropping a fork per call (#662)
  • Drop grep/random_str forks in generate_id and normalize_variable_name (#663)

0.36.0 - 2026-05-07

Fixed

  • bashunit upgrade exits non-zero with a clear error when the download fails (no more false success message)

Added

  • --show-output displays captured test output on assertion failures (#637)
  • npm registry distribution: npm install -g bashunit (#244)
  • bashunit::env::supports_color and bashunit::io::clear_screen helpers (#247)
  • LCOV reports now include FN, FNDA, FNF and FNH function records, consumed by genhtml, Codecov and Coveralls
  • LCOV reports now include BRDA, BRF and BRH branch records for if/elif/else chains and case patterns (see adrs/adr-007-branch-coverage-mvp.md)
  • BASHUNIT_COVERAGE_SHOW_FUNCTIONS=true adds a per-function coverage block to the text report
  • BASHUNIT_COVERAGE_SHOW_UNCOVERED=true adds an "Uncovered Lines" block to the text report, with consecutive line numbers compressed into ranges

Changed

  • Docs moved into their own npm workspace under docs/ (use cd docs && npm ci or make docs/install)
  • Pre-commit hook skips the test suite when no shell files are staged
  • ANSI escapes route through _BASHUNIT_COLOR_* constants (#247)

Performance

  • Faster coverage report generation: single-pass file scan, native bash regex, cached file stats (#636)

Internal

  • bashunit::runner::run_test split into nine named helpers (no behavior change)
  • release.sh handles docs/package.json as a first-class release file
  • Structural test guards root package.json against regaining dependencies or scripts

0.35.0 - 2026-04-26

Added

  • bashunit::spy accepts an optional exit code or custom implementation function (#600)
  • Assert functions accept an optional trailing label to override the failure title (#77)
  • --fail-on-risky flag and BASHUNIT_FAIL_ON_RISKY env var treat no-assertion tests as failures (#115)
  • --log-gha <file> flag and BASHUNIT_LOG_GHA env var emit GitHub Actions workflow commands so failed, risky and incomplete tests show up as inline PR annotations (#280)
  • assert_exec accepts --stdin, --stdout-contains, --stdout-not-contains, --stderr-contains and --stderr-not-contains flags to test interactive prompt commands and substring output (#301)

Changed

  • Parallel test execution is now enabled on Alpine Linux (#370)

Fixed

  • Dim/faint labels now render as gray (SGR 90) so keywords like Expected and Tests: stay colored in GitHub Actions logs (#323)
  • Syntax error in a test file now fails the suite instead of passing silently (#220)
  • --stop-on-failure now stops on runtime errors such as command not found or illegal option (#383)
  • Spying on echo or printf no longer hangs via infinite recursion (#607)
  • LCOV and HTML coverage reports no longer produce empty output under set -e (#618)
  • clock::now handles EPOCHREALTIME values that use a comma decimal separator
  • Invalid .env.example coverage threshold entry; CI now copies .env.example to .env so config parse errors are caught
  • Coverage no longer counts case patterns with trailing comments (e.g. *thing) # note) or loop terminators with redirections/pipes (e.g. done < file, done <<<"$var", done | sort) as executable lines (#634)
  • assert_true and assert_false now report empty strings as assertion failures instead of trying to execute them

0.34.1 - 2026-03-20

Added

  • Add risky test detection for tests with zero assertions (shown as warning, does not fail)

Fixed

  • Fix source of non-existent file in set_up() silently passing all tests (#611)
  • Fix set_up running before strict mode — unbound variables in hooks now detected with --strict
  • Fix source failure in tear_down(), set_up_before_script(), and tear_down_after_script() silently passing
  • Add missing runtime error patterns: ambiguous redirect, integer expression expected, too many arguments, value too great, not a valid identifier, unexpected EOF

0.34.0 - 2026-03-17

Added

  • Add --watch mode and watch [path] subcommand to re-run tests on file changes
    • Uses inotifywait on Linux or fswatch on macOS; clear install hint if unavailable
  • Add --jobs N flag to limit parallel test concurrency
  • Add --tag and --exclude-tag CLI flags for filtering tests by # @tag annotations
  • Add TAP version 13 output format via --output tap for CI/CD integration
  • Add source context display in failure summaries showing relevant assertion lines
  • Add date comparison assertions: assert_date_equals, assert_date_before, assert_date_after, assert_date_within_range, assert_date_within_delta
    • Auto-detects epoch seconds, ISO 8601, space-separated datetime, and timezone offsets
  • Add assert_have_been_called_nth_with for verifying spy arguments on the Nth invocation
  • Add assert_string_matches_format and assert_string_not_matches_format with format placeholders (%d, %s, %f, %i, %x, %e, %%)
  • Add JSON assertions: assert_json_key_exists, assert_json_contains, assert_json_equals (requires jq)
  • Add duration assertions: assert_duration, assert_duration_less_than, assert_duration_greater_than

Changed

  • Split Windows CI test jobs into parallel chunks to avoid timeouts
  • Optimize clock: prioritize EPOCHREALTIME over subprocess-based fallbacks
  • Cache function discovery to avoid duplicate pipeline per test file
  • Reduce subshells in test execution hot path
  • Batch coverage recording with in-memory buffering
  • Cache uname result at source time to eliminate repeated subprocess forks
  • Replace bc and awk subprocesses with native bash arithmetic in clock and duration formatting
  • Cache base64 -w flag support at load time instead of detecting per test
  • Use direct variable access for assertion state instead of getter subshells in runner hot path

Fixed

  • Fix misleading error message for assert_not_same and assert_not_equals on failure (#604)
  • Mocking mktemp no longer breaks spy creation (#602)
  • JUnit XML report now conforms to the standard schema
    • Remove non-standard attributes from <testsuite> and <testcase>
    • Add errors="0" attribute and <failure>/<skipped> child elements per spec
    • skipped count now includes both skipped and incomplete tests
    • Convert time values from milliseconds to seconds (float)
    • Strip ANSI escape sequences and invalid XML control characters from failure messages
    • Include actual failure messages in <failure> body

Tests

  • Add unit tests for env.sh, math.sh, colors.sh, test_title.sh, console_header.sh, and doc.sh

0.33.0 - 2026-02-15

Changed

  • Lower minimum Bash version requirement from 3.2 to 3.0
  • Improve parallel test execution performance (30-40% faster on large test suites)

Added

  • Display test output (stdout/stderr) on failure for runtime errors
    • Enabled by default; disable with --no-output-on-failure or BASHUNIT_SHOW_OUTPUT_ON_FAILURE=false
  • Add --no-progress flag to suppress real-time progress display (Issue #503)
    • Also available via BASHUNIT_NO_PROGRESS=true environment variable
  • Support # bashunit: no-parallel-tests directive to opt out of test-level parallelism

Fixed

  • Data providers now work without the function keyword on test functions (Issue #586)
  • Install tests now pass in sandboxed/no-network environments (Issue #582)
  • Parallel test execution now works correctly in strict mode (bash -e -o pipefail)

0.32.0 - 2026-01-12

Changed

  • Hook output now displays with right-aligned timing and bullet prefix (● set_up_before_script 2.03s)

Added

  • Better code coverage HTML report
  • Auto-discover coverage paths from test file names when BASHUNIT_COVERAGE_PATHS is not set
    • tests/unit/assert_test.sh automatically tracks src/assert.sh
    • Removes the need for manual --coverage-paths configuration in most cases
  • --coverage-report-html now defaults to coverage/html when no directory is specified

Fixed

  • Coverage now excludes control flow keywords (then, else, fi, do, done, esac, ;;, case patterns) from line tracking
  • Coverage HTML report now correctly handles namespaced functions with colons (e.g., bashunit::assert::equals)
  • Coverage data no longer lost when running coverage_test.sh alongside other tests

0.31.0 - 2025-12-19

Added

  • Display set_up_before_script and tear_down_after_script execution with duration
    • Shows "Running hook_name... done (Xs)" during test runs
    • Helps identify slow setup/teardown operations
    • Suppressed in failures-only and parallel modes
  • Add code coverage tracking with --coverage flag (Issue #190)
    • Tracks line coverage using Bash DEBUG trap mechanism
    • Configurable source paths via --coverage-paths (default: src/)
    • Configurable exclusions via --coverage-exclude (default: tests/*,vendor/*,*_test.sh,*Test.sh)
    • LCOV format output via --coverage-report (default: coverage/lcov.info)
    • HTML coverage report via --coverage-report-html <dir> with line-by-line highlighting
    • Minimum coverage threshold via --coverage-min (fails if below)
    • Console-only mode with --no-coverage-report
    • Color-coded console output with configurable thresholds (50%/80%)

Fixed

  • Fix bench command not working in standalone/installed bashunit (missing benchmark.sh in build)
  • Fix helper::get_latest_tag returning version with ^{} suffix from annotated git tags

0.30.0 - 2025-12-14

Added

  • Display execution time in minutes format when tests run over 60 seconds (e.g., "2m 1s")
  • Display individual test duration in human-readable format (ms, s, or m s) instead of always milliseconds
  • Add --failures-only flag to suppress passed/skipped/incomplete tests and show only failures
  • Add --no-color flag to disable ANSI color output (also supports NO_COLOR env var per no-color.org standard)
  • Add multiple assertions support in standalone mode: bashunit assert "cmd" exit_code "1" contains "error"
  • Add release.sh script to automate the release process with dry-run support

Changed

  • BREAKING: Rename --preserve-env flag to --skip-env-file for clearer semantics
  • BREAKING: Rename BASHUNIT_PRESERVE_ENV environment variable to BASHUNIT_SKIP_ENV_FILE
  • Improve documentation for --skip-env-file clarifying that shell functions are not inherited
  • Add tip to Bootstrap documentation promoting it as the solution for function availability in tests
  • Clarify that exit code assertions check $? instead of executing commands

Fixed

  • Internal flaky tests when running --strict
  • Visible stdout/stderr during normal execution set_up_before_script and tear_down_after_script

0.29.0 - 2025-12-08

Added

  • Add bootstrap argument passing support via --env "file.sh arg1 arg2" or BASHUNIT_BOOTSTRAP_ARGS
  • Add --preserve-env flag to skip .env loading and use shell environment only
  • Add -l, --login flag to run tests in login shell context
  • Add --strict flag to enable strict shell mode (set -euo pipefail) for tests
  • Add BASHUNIT_STRICT_MODE configuration option (default: false)
  • Add -R, --run-all flag to run all assertions even when one fails
  • Add BASHUNIT_STOP_ON_ASSERTION_FAILURE configuration option (default: true)

Changed

  • BREAKING: Namespace all internal functions and variables to prevent collisions with user code
    • All helper functions now use bashunit:: prefix (e.g., skipbashunit::skip)
    • All internal functions now use bashunit:: prefix (e.g., helper::trimbashunit::helper::trim)
    • All internal variables now use _BASHUNIT_ prefix (e.g., _TESTS_PASSED_BASHUNIT_TESTS_PASSED)
    • All assert_* functions remain unchanged (public API)

Fixed

  • Improve assert command output: show assert <fn> instead of internal function name in failure messages
  • Custom assertions now display the correct test function name in failure messages
  • Data providers now work when set_up_before_script changes directory
  • Subsequent test files now run when set_up_before_script changes directory
  • Catch intermediate failing commands in set_up_before_script and tear_down_after_script

0.28.0 - 2025-12-01

Added

  • Add inline filter syntax to run specific tests from a file
    • path::function_name - filter tests by function name
    • path:line_number - run the test containing the specified line
  • Add --show-skipped and --show-incomplete options to display skipped/incomplete tests at the end

Changed

  • BREAKING: Introduce subcommand-based CLI architecture
    • bashunit test [path] - run tests (default, backwards compatible with bashunit [path])
    • bashunit bench [path] - run benchmarks (replaces --bench)
    • bashunit doc [filter] - show assertion docs (replaces --doc)
    • bashunit init [dir] - initialize project (replaces --init)
    • bashunit learn - interactive tutorial (replaces --learn)
    • bashunit upgrade - upgrade to latest (replaces --upgrade)
  • BREAKING: Tests now stop at first assertion failure within a test function
    • Matches PHPUnit and Jest default behavior
    • Subsequent assertions in the same test are skipped after a failure
    • Other test functions continue to run normally

Fixed

  • Stop executing remaining commands in set_up/tear_down after first failure
  • Count all tests as failed when set_up_before_script fails

Performance

  • Optimize assertion guard: use integer comparison instead of string comparison

0.27.0 - 2025-11-26

Added

  • Add --learn interactive tutorial with 10 progressive lessons for hands-on learning
  • Add comprehensive "Common Patterns" documentation with real-world testing examples

Fixed

  • Pass arguments to mocked functions
  • Fix lifecycle hooks not catching intermediate failing commands in set_up and tear_down

Changed

  • Simplify CI: use only -latest runners for Ubuntu and macOS, remove deprecated macos-13

Performance

  • Optimize temp directory creation: initialize once at startup instead of per temp file
  • Optimize parallel result aggregation: use bash redirection instead of spawning tail subprocess
  • Optimize state access: cache state values to avoid repeated subshell invocations

0.26.0 - 2025-11-02

  • Add assert_unsuccessful_code assertion to check for non-zero exit codes
  • Fix bench tests missing test_file var
  • Fix compatibility with older python versions for clock::now
  • Fix data_set with arguments containing spaces
  • Eliminated redundant declare -F | awk calls that were happening for every test/bench file
  • Replaced tail and process with Bash parameter expansion

0.25.0 - 2025-10-05

  • Add AI developer tools integration and guidelines
    • Add Project-wide copilot-instructions.md
    • Add AGENTS.md for external developer tools integration
    • Add tasks storage policy clarifying .tasks/ (versioned) vs .task/ (git-ignored)
  • Include set_test_title helper in the single-file library
  • Fix lifecycle hooks capture-and-report flow errors
    • set_up, tear_down, set_up_before_script, tear_down_after_script
  • Fix false negative from assert_have_been_called_with with pipes
  • Fix preservation of trailing whitespace in final argument to data_set
  • Fix unbound variable error in parse_data_provider_args with set -u
  • Fix wrong assertion_failed name of test on failure
  • Fix test name interpolation on failure

0.24.0 - 2025-09-14

  • Improve assert_have_been_called_with with strict argument matching
  • Make Windows install clearer in the docs by adding an option for Linux/Mac and another one for Windows.
  • Add data_set function for empty values and values with spaces/tabs/newlines
  • Document workaround for function name collisions when sourcing scripts
  • Fix temp_dir and temp_file data not being cleaned up when created in set_up_before_script
  • Fix /tmp/bashunit/parallel not being cleaned after test run

0.23.0 - 2025-08-03

  • Update docs mocks usage
  • Skip report tracking unless a report output is requested
  • Add support for .bash test files
  • Add runtime check for Bash >= 3.2
  • Add fallback for clock::now with seconds resolution only
  • Add set_test_title to allow custom test titles
  • Add assert_exec to validate exit code, stdout and stderr at once

0.22.3 - 2025-07-27

  • Fix NixOS support
    • Fix parallel and compgen
    • Use command -v instead of which

0.22.2 - 2025-07-26

  • Fix broken core snapshot tests
  • Improve NixOS support
  • Add line number to failing tests

0.22.1 - 2025-07-23

  • Fix prevents writing in src dir during tests
  • Fix negative widths with rpad
  • Fix internal assert_line_count and call_test_functions
  • Improve suffix assertion checks to use shell pattern matching
  • Include calling function name in dev log output for easier debugging
  • Add more internal dev log messages and prefix them with [INTERNAL]
    • Toggle internal log messages with BASHUNIT_INTERNAL_LOG=true|false

0.22.0 - 2025-07-20

  • Fix process time always shows as 0 ms
  • Fixed terminal width detection first tput and fall back stty
  • Refactor clock optimizing the implementation used to get the time
  • Add --init [dir] option to get you started quickly
  • Optimize --help message
  • Add --no-output option

0.21.0 - 2025-06-18

  • Fix typo "to has been called"
  • Add weekly downloads to the docs
  • Fix parallel runner
  • Count data providers when counting total tests
  • Add benchmark feature
  • Support placeholder ::ignore:: in snapshots
  • Add project overview docs
  • Improve clock performance
  • Make install.sh args more flexible
  • Improve Windows detection allowing parallel tests on Git Bash, MSYS and Cygwin
  • Add more CI jobs for different ubuntu and macos versions

0.20.0 - 2025-06-01

  • Fix asserts on test doubles in subshell
  • Allow interpolating arguments in data providers output
  • Deprecate # data_provider in favor of # @data_provider
  • Allow assert_have_been_called_with to check arguments of specific calls
  • Enable parallel tests on Windows
  • Add assert_not_called
  • Improve helper::find_total_tests performance
  • Added assert_match_snapshot_ignore_colors
  • Optimize runner::parse_result_sync
  • Fix parse_result_parallel template

0.19.1 - 2025-05-23

  • Replace #!/bin/bash with #!/usr/bin/env bash
  • Usage printf with awk, which correctly handles float rounding and improves portability

0.19.0 - 2025-02-19

  • Fixed false negative with set -e
  • Fixed name rendered when having test_test_*
  • Fixed duplicate function detection
  • Fixed display test with multiple outputs in multiline
  • Improved output: adding a space between each test file
  • Removed BASHUNIT_DEV_MODE in favor of BASHUNIT_DEV_LOG
  • Added source file and line on global dev function log

0.18.0 - 2024-10-16

  • Added -p|--parallel to enable running tests in parallel
    • Enabled only in macOS and Ubuntu
  • Added assert_file_contains and assert_file_not_contains
  • Added assert_true and assert_false
  • Added BASHUNIT_DEV_LOG
  • Added global util functions
    • current_dir
    • current_filename
    • caller_filename
    • caller_line
    • current_timestamp
    • is_command_available
    • random_str
    • temp_file
    • temp_dir
    • cleanup_temp_files
    • log
  • Add default env values:
    • BASHUNIT_DEFAULT_PATH="tests"
    • BASHUNIT_BOOTSTRAP="tests/bootstrap.sh"
  • Add check that git is installed to install.sh
  • Add -vvv|--verbose to display internal details of each test
  • Fixed -S|--stop-on-failure behaviour
  • Improved time taken display
  • Improved clean up temporal files and directories
  • Improved CI test speed by running them in parallel
  • Removed git dependency for stable installations
  • Rename option --verbose to --detailed
    • which is the default display behaviour, the opposite as --simple
  • Added assert_not_same

0.17.0 - 2024-10-01

  • Fixed simple output for non-successful states
  • Added support for Alpine (Linux Distro)
  • Added optional file-path as 2nd arg to --debug option
  • Added runtime duration per test
  • Added defer expressions with when using standalone assertions
  • Added failing tests after running the entire suite
  • Improved runtime errors handling
  • Simplified total tests display on the header
  • Renamed BASHUNIT_TESTS_ENV to BASHUNIT_BOOTSTRAP
  • Better handler runtime errors
  • Display failing tests after running the entire suite
  • Added defer expressions with eval when using standalone assertions
  • Fixed simple output for non-successful states
  • Remove deprecated assertions
  • Some required dependencies now optional: perl, coreutils
  • Upgrade and install script can now use wget if curl is not installed
  • Tests can be also be timed by making use of EPOCHREALTIME on supported system
  • Switch to testing the environment of capabilities
    • rather than assuming various operating systems and Linux distributions have programs installed

0.16.0 - 2024-09-15

  • Fixed clock::now can't locate Time when is not available
  • Fixed failing tests when command not found and unbound variable
  • Fixed total tests wrong number
  • Update GitHub actions installation steps documentation
  • Added assert_files_equals, assert_files_not_equals
  • Added BASHUNIT_TESTS_ENV

0.15.0 - 2024-09-01

  • Fixed --filter|-f to work with test_* matching function name input.
  • Added assertions to log file
  • Rename the current assert_equals to assert_same
  • Rename assert_equals_ignore_colors to assert_equals and ignore all special chars
  • Data providers support multiple arguments
  • Remove multi-invokers in favor of data providers
  • Removing trailing slashes / from the test directories naming output.
  • Align "Expected" and "but got" on assert_* fails message.
  • Change -v as shortcut for --version
  • Add -vvv as shortcut for --verbose
  • Fix wrong commit id when installing beta
  • Add display total tests upfront when running bashunit
  • Add BASHUNIT_ suffix to all .env config keys
    • BASHUNIT_SHOW_HEADER
    • BASHUNIT_HEADER_ASCII_ART
    • BASHUNIT_SIMPLE_OUTPUT
    • BASHUNIT_STOP_ON_FAILURE
    • BASHUNIT_SHOW_EXECUTION_TIME
    • BASHUNIT_DEFAULT_PATH
    • BASHUNIT_LOG_JUNIT
    • BASHUNIT_REPORT_HTML

0.14.0 - 2024-07-14

  • Fix echo does not break test execution results
  • Add bashunit facade to enable custom assertions
  • Document how to verify the sha256sum of the final executable
  • Generate checksum on build
  • Enable display execution time on macOS with SHOW_EXECUTION_TIME
  • Support for displaying the clock without perl (for non-macOS)
  • Enable strict mode
  • Add --log-junit <log.xml> option
  • Add -r|--report-html <report.html> option
  • Add --debug option
  • Add dump and dd functions for local debugging

0.13.0 - 2024-06-23

  • Allow calling assertions standalone outside tests
  • Add the latest version when installing beta
  • Add assert_line_count
  • Add hash to the installation script when installing a beta version
  • Add GitHub Actions to installation doc

0.12.0 - 2024-06-11

  • Add missing assertion in non-stable versions
  • Fix test with rm command in macOS
  • Add multi-invokers; consolidate parameterized-testing documentation
  • Add fail() function
  • Remove all test mocks after each test case

0.11.0 - 2024-03-02

  • Add --upgrade option to ./bashunit
  • Remove support to deprecated setUp, tearDown, setUpBeforeScript and tearDownAfterScript functions
  • Optimize test execution time
  • Test functions are now run in the order they're defined in a test file
  • Increase contrast of test results

0.10.1 - 2023-11-13

  • Fix find tests inside folder
  • Add current date on beta installation version

0.10.0 - 2023-11-09

  • Installer no longer needs git
  • Add assert_contains_ignore_case
  • Add assert_equals_ignore_colors
  • Add assert_match_snapshot
  • Add SHOW_EXECUTION_TIME to environment config
  • Add docs for environment variables
  • Improve data provider output
  • Add .env variable DEFAULT_PATH
  • Improve duplicated function names output
  • Allow installing (non-stable) beta using the installer

0.9.0 - 2023-10-15

  • Optimised docs Fonts (Serving directly from origin instead of Google Fonts proxy)
  • Add Brew installation to docs
  • Add --help option
  • Add -e|--env option
  • Add -S|--stop-on-failure option
  • Add data_provider
  • Add blog posts to the website
  • Add assert_string_not_starts_with
  • Add assert_string_starts_with
  • Add assert_string_ends_with
  • Add assert_string_not_ends_with
  • Add assert_less_than
  • Add assert_less_or_equal_than
  • Add assert_greater_than
  • Add assert_greater_or_equal_than

0.8.0 - 2023-10-08

  • Rename these functions from camelCase to snake_case:
    • setUp -> set_up
    • tearDown -> tear_down
    • setUpBeforeScript -> set_up_before_script
    • tearDownAfterScript -> tear_down_after_script
  • Add --version option
  • Add -v|--verbose option
  • Add ASCII art logo
  • Find all test on a directory
  • Add skip and todo functions
  • Add SIMPLE_OUTPUT to .env
  • Allow using main or latest when using install.sh

0.7.0 - 2023-10-02

  • Added --simple argument for a simpler output
  • Manage error when test execution fails
  • Split install and build scripts
  • Added these functions
    • mock
    • spy
    • assert_have_been_called
    • assert_have_been_called_with
    • assert_have_been_called_times
    • assert_file_exists
    • assert_file_not_exists
    • assert_is_file_empty
    • assert_is_file
    • assert_directory_exists
    • assert_directory_not_exists
    • assert_is_directory
    • assert_is_directory_empty
    • assert_is_directory_not_empty
    • assert_is_directory_readable
    • assert_is_directory_not_readable
    • assert_is_directory_writable
    • assert_is_directory_not_writable
  • Rename assertions from camelCase to snake_case:
    • assertEquals -> assert_equals
    • assertNotEquals -> assert_not_equals
    • assertEmpty -> assert_empty
    • assertNotEmpty -> assert_not_empty
    • assertContains -> assert_contains
    • assertNotContains -> assert_not_contains
    • assertMatches -> assert_matches
    • assertNotMatches -> assert_not_matches
    • assertExitCode -> assert_exit_code
    • assertSuccessfulCode -> assert_successful_code
    • assertGeneralError -> assert_general_error
    • assertCommandNotFound -> assert_command_not_found
    • assertArrayContains -> assert_array_contains
    • assertArrayNotContains -> assert_array_not_contains

0.6.0 - 2023-09-19

  • Added assertExitCode
  • Added assertSuccessfulCode
  • Added assertGeneralError
  • Added assertCommandNotFound
  • Added assertArrayContains
  • Added assertArrayNotContains
  • Added assertEmpty
  • Added assertNotEmpty
  • Added setUp, setUpBeforeScript, tearDown and tearDownAfterScript function execution before and/or after test and/or script execution
  • Improved the readability of the assert by using guard clause
  • Update documentation
  • Add support for the static analysis on macOS
  • Fix bug with watcher for the development of bashunit
  • Fix error on count assertions
  • Added pipeline to add contributors to the readme
  • Added documentation with VitePress
  • Stop runner when found duplicate test functions

0.5.0 - 2023-09-10

  • Added logo
  • Added assertNotEquals
  • Added assertMatches
  • Added assertNotMatches
  • Added make test/watch to run your test every second
  • Added time taken to run the test in ms (only to Linux)
  • Simplified assertions over test results
  • Added acceptance test to the library
  • Added pre-commit to the project
  • Allow parallel tests to run base on a .env configuration enabled by default
  • Added static analysis tools to the deployment pipelines
  • New summary output

0.4.0 - 2023-09-08

  • Better output colors and symbols
  • Add option --filter to ./bashunit script
    • Trigger tests filtered by name
  • Change the output styles
    • Emojis
    • Colors
    • Bolds
  • Added count to all test

0.3.0 - 2023-09-07

  • Added assertContains
  • Added assertNotContains
  • Display Passed tests in green, and Failed tests in red
  • Avoid stop running tests after a failing one test

0.2.0 - 2023-09-05

  • Fix keeping in memory test func after running them
  • Create a ./bashunit entry point
  • Change ROOT_DIR to BASHUNIT_ROOT_DIR
  • Allow writing test with camelCase as well
  • Allow running example log_test from anywhere

0.1.0 - 2023-09-04

  • Added assertEquals function