- 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-filterkeeps a comma as part of the test function name. The value was split at the comma, so--exclude-filter 'test_a,{b}'also excludedtest_a. Repeat the flag to exclude several names.BASHUNIT_EXCLUDE_FILTERstays comma-separated, so one of its filters cannot hold a literal comma (#1340)- A
--parallelrun no longer replays bash'schild setpgidwarning as the stderr of a file that wrote nothing. Job control puts each test in its own process group. The parent-sidesetpgidthen 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
tear_down_after_scriptruns whenset_up_before_scriptfails, so it can release file-scoped resources acquired before the failure (#1318)- Under
--parallel,tear_down_after_scriptruns 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-failurerunstear_down_after_scriptfor the file it halts in, so a sequential run releases whatset_up_before_scriptacquired before the halt (#1321)bashunit benchrunstear_down_after_scriptbefore it aborts on a malformed annotation, so the file releases whatset_up_before_scriptacquired (#1322)- Ctrl-C runs
tear_down_after_scriptfor 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-timeoutruns itstear_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_fileit created at top level no longer survives the run.bashunit benchalready did this (#1325) - A malformed
@timeoutor@retryrunstear_down_after_scriptbefore it aborts the run, so the file releases whatset_up_before_scriptacquired. Sequential and--parallelboth leaked it (#1329) - Ctrl-C releases what an interrupted
--parallelrun acquired: the file'stear_down_after_scriptand thetear_downof 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
@timeoutor@retryfails a--parallelrun 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
-uas the short form of--snapshot-update, the spelling jest and vitest both use (#1293)
--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 —
--versionincluded (#1285) - Performance: a
--parallelrun writing a report is about 1.9x faster and uses 1.8x less CPU. Each result row cost fourteenbase64forks per test, so--log-junitcost several times more than running the tests (#1289)
--output jsonand--output junitare no longer corrupted by console text on stdout, which left the document unparseable. Four sources: an empty--parallelselection (#1295), the blank line after atear_down_after_script(#1297), the duplicate-function abort, and a--parallelworker's stderr replay (#1299)--parallelreports 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 benchexits non-zero when a benchmark file fails to source or itsset_up_before_scriptfails. It printed the error and exited 0, so the failure never reached CI, and a syntax error silently dropped everybench_function after it (#1303)--output tapescapes#in a test description, as--report-tapalready did. A failing test titled... # SKIP ...reached the consumer as a skip and left CI silently (#1309)bashunit watchno longer takes an option's value as the path it polls:watch --tag slow tests/polled a directory namedslow. 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-mdsummary, which is appended to `$GITHUB_STEP_SUMMARY` (#1305)
- 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
riskyinstead of failing (#1315)
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
--pass-with-no-testsexits 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 reportsNo 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-tagsprints the tags of the selected files, one per line, sorted and deduplicated, and runs nothing. Tags live only in# @tagcomments, so a mistyped--taghad no list to check against (#1265)
- A
testorbenchpath that does not exist is now named and refused before the run starts, instead of theNo tests found/No benchmarks foundan empty selection gives. Every genuinely-empty case keeps that message: a directory holding no tests, a--filteror--tagmatching nothing, an empty--shard,--changedwith 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-unusedand--snapshot-prunenow 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-prunedeletes (#1194)- A
--tagmatching 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 viatemp_file/temp_dir, is no longer removed for it (#1269)
- A sequential run no longer leaks a file descriptor per test file. At the limit it stopped executing tests and reported
riskywhile still exiting 0: 120 files under a 120-descriptor limit ran 2 assertions instead of 120 (#1271) Time takenno longer reports0ms, or a negative duration, for a runtime it could not measure — both read as real measurements. It now saysunknown(#1271)- A run using
--test-timeoutno longer leaves its watchdog holding the caller's captured output for the rest of the timeout budget; a nested run under--suitedid 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)
--listunder--parallelno longer printsNo tests foundin the middle of the ids: a listing dispatches no worker, so there is nothing to aggregate (#1007)
0.48.0 - 2026-08-14
--verbosewarns on Bash 3.x that coverage does not count lines run inside a subshell, so a lower percentage there explains itself (#1112)
- Performance:
--coverageis roughly 5x faster and--coverage-report-htmlroughly 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.shis 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
Failuressection with each failure's name,file:lineand message, and its summary now counts risky and flaky tests (#1251, #1252) - A
--filterthat 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) --envwith a space in the path now says the value was split on the first space to pass bootstrap arguments, and thatBASHUNIT_BOOTSTRAPtakes the path whole (#1247)- The bootstrap error names the actual cause — missing, a directory, or not a regular file — instead of
cannot readfor all of them (#1262) bashunit doc <filter>saysNo assertion matches '<filter>'instead of printing nothing (#1201)install.shdestination errors no longer advise a-dflag that does not exist; the script takes positional arguments (#1221)bashunit learngenerates 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 -band--simple --benchnever 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_equalsstrips ANSI codes, tabs and newlines — not spaces (#1225) - Docs: an empty entry in the
-e/--env/--bootfile assigns an empty value; it does not restore the default (#1217) - Docs:
assert_matchescosts ~2.5ms per call against ~0.065ms forassert_same, so preferassert_containsfor a fixed substring (#1187) - Docs: a
@data_providertest shares one snapshot across all its values; useassert_match_named_snapshot "$1"for one each (#1185)
bashunit --output junitproduces valid XML, and--parallel --stop-on-failureno longer corrupts a machine--outputstream (#1239, #1243)- The HTML report escapes test titles instead of writing them into the markup (#1249)
--coverage-pathsaccepts 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
--parallelrun 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
--parallelagain; the run reported "All tests passed" over a file where one of two same-named tests never ran (#1147) - A
@data_providerthat 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 benchreportsNo benchmarks foundand exits non-zero when the path does not exist or holds nobench_function (#1199)- A report path that is a directory fails fast with
is a directory, not a fileinstead of exiting 0 with no report written (#1177) - A coverage run that tracked no executable line says so and names
--coverage-paths, instead of reportingCoverage 0% is below minimum N%(#1171) assert_exec "cmd" --exit 1works under--strict;set -eaborted the test before the assertion could read the code (#1207)assert_equalsno longer expands backslash escapes while normalizing, soC:\andC:\\differ and a literal\tis not a real tab (#1108)assert_file_containsaccepts a needle starting with a dash, andassert_file_not_containsmatches literally like its counterpart (#1108)- The mock/spy misuse message names a helper that exists (
bashunit::mock, notmock), and reports a usable-name error instead of a raw bash syntax error (#1136, #1229) - A JSON test skipped for a missing
jqis reported under its own name, notbashunit::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.shnames the real problem when the destination is unusable, and validates it before any network call (#1197)bashunit initno longer adds a deadBASHUNIT_BOOTSTRAPline to.envon 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, socheck # SKIP meis 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
--output <text|tap|json|junit>prints the JSON and JUnit reports on stdout, so a pipeline needs no temp file;--report-jsonstill writes its file alongside (#1018)bashunit::skip_if,bashunit::skip_unless,bashunit::skip_unless_command <cmd>andbashunit::skip_on <windows|macos|linux>mark a test skipped and end it, replacingbashunit::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.bashunitrcname a set of paths and options;--suite <name>runs one (repeatable) and--list-suiteslists them (#1021)--sandboxfails 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 andassert_is_file_not_emptygive files the parity directories already had (#1024)assert_json_key_not_existschecks that a JSON path is absent, andassert_json_lengththe 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-tolerancepercent (default 10) slower than the recorded one, comparing medians;--baseline-update <file>records the new reference (#1029)--snapshot-prunedeletes the snapshot files no test resolved, printing every path; full runs only, and never on a run with failures (#1030)
- LCOV
BRDAcarries the arm's execution count instead of a 0/1 taken flag, taken from the arm's first executable line;BRFandBRHare unchanged (#1061) - Performance:
--coverageis about 10x faster — a run over this repo'ssrcwent 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)
- Coverage reports every file under
--coverage-paths, not only the ones a test executed: an untouched file shows as0/N (0%)and--coverage-mingates 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-diffcounts a changed file that no test executed, instead of skipping it and letting a brand new untested file pass a--coverage-min 90gate. A docs-only commit still reports 100% (#1054)- Coverage read a statement ending in
)as acasearm, sox=$(foo)left the denominator whilex=$(printf '%s\n')stayed. A)now closes an arm only when no(opened earlier on the line, recovering 456 executable lines of this repo'ssrc/. 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'ssrc/coverage/lines.shwere reported as 1, andEND {inside an embedded awk program was reported as a function.FN,FNDA,FNFandFNHchange; lines and branches do not (#1086)
0.46.0 - 2026-08-11
--changed [<ref>]runs only the test files git reports as touched since<ref>(defaultorigin/HEAD, thenHEAD), covering committed, staged, unstaged and untracked changes (#1010)--order-by <defined|defects|random>picks the execution order;defectsruns 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 jsonemits 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)--tagaccepts expressions:'a&&b'(AND),'!a'(NOT) and'a&&!b'; repeated--tagflags keep OR semantics and--exclude-tagstill wins (#1008)# @tags a babove 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-flakyturns such a run red (#1012) --coverage-report-cobertura [file]writes Cobertura XML (defaultcoverage/cobertura.xml), the format GitLab merge-request visualisation, Azure DevOps and Jenkins consume, with repo-relative filenames, per-line hits andcondition-coverageon 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-minthen 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 andaction.ymlgains anannotationsinput (#1014) assert_between <min> <max> <actual>andassert_not_betweenadd inclusive numeric-range assertions for integers and decimals (#1026)assert_command_available <command>asserts a command, shell builtin or function resolves throughcommand -v(#1027)--verbosereports the coverage engine in use, and an explicitBASHUNIT_COVERAGE_ENGINE=xtracethe running Bash cannot honour now warns instead of being silently ignored (#1005)
- JUnit XML: one
<testsuite>per test file with its own counts, time and timestamp instead of a single flat suite,classnameon every<testcase>,<failure message="...">carrying the first informative line of the real message withtype="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:
--coverageis about 1.6x to 2.3x faster; executable-line classification no longer forksgrepper source line, roughly half of a coverage run's wall time on both engines (#1005) bashunit test --helplists--show-skippedand--show-incomplete, both accepted by the parser but never advertised;BASHUNIT_COVERAGE_SHOW_FUNCTIONSandBASHUNIT_COVERAGE_SHOW_UNCOVEREDare registered insrc/config/env.shlike every other setting, and.env.examplenow lists all 66 settings, 19 of which were missing (#1063)- Docs: full audit of the reference pages against the code —
docs/configuration.mdgained the 17 settings it never documented,docs/command-line.mdgained theassertsubcommand 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)
- Report formats are no longer empty under
--parallel;--report-junit,--report-tap,--report-json,--report-htmland--log-junitall 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_thatreturns 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✗ Erroron top of the✗ Failed. Custom assertions no longer need a trailingreturn 0, and a real runtime error is still reported as an error (#1063) --coverage-reportwith no value usescoverage/lcov.infoinstead 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 jsonreports 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_deltarejects malformed numbers such as1.2.3or5-3as non-numeric instead of leaking a rawbcparse 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
assert_is_symlink,assert_is_not_symlinkandassert_symlink_toassert on a symbolic link itself, which every other filesystem assertion follows through to the target (#981)assert_trueandassert_falseaccept 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-updatehint (#986)
assert_true/assert_falsereportunknown command/not executableinstead 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_deltauses fixed-point arithmetic for common values, with abc/awkfallback for unsupported inputs (about 6.6x faster) (#979) - Performance: Spy assertions and call counters use builtins instead of
catand command substitutions (about 6.5x faster) (#978) - Performance:
assert_contains_ignore_caseuses Bash'snocasematchwhere available, falling back totron 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.shandsrc/coverage.shinto focused modules with no behavior change; see ADR-010 (#924, #925)
- Snapshot placeholders no longer match anything at all without
perl; thegrepfallback treated a multi-line pattern as separate alternatives, and now usesawkover 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_falseno 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_timesandassert_have_been_called_nth_withreport a usage error for a non-numeric count instead of leaking a rawinteger 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_deltaaccepts a leading+on any operand (#979)- Invalid
BASHUNIT_SHARD_INDEX/BASHUNIT_SHARD_TOTALvalues 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_equalsrejects 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.shdeduplicates embedded files by repository-relative path, preventing duplicate or missing modules with the same filename (#923)bashunit docno longer errors when the default bootstrap file is missing (#929)
0.44.0 - 2026-07-29
bashunit doc --customlists the assertions your project defines, rendering the comment block above each one; plainbashunit docappends them as a "Custom assertions" section. Needs--boot/BASHUNIT_BOOTSTRAP, whichbashunit docnow 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_failsandassert_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 forgottenreturnorbashunit::assertion_passedcan no longer drift the two counters apart (#915)bashunit::assertion_failedtakes an optional 4th argument labelling the failure block, so a custom assertion can name itself instead of showing the test name (#915)--snapshot-report-unusedlists 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=falsefails 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=truere-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, matchingbashunit::spy; no throwawayreturn 1helper 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, socmd "a b"no longer matchescmd a b(#894)BASHUNIT_COVERAGE_ENGINE=auto|xtrace|trap: a newxtraceengine, ~4x cheaper per captured line. Needs Bash 4.1+, soauto(the default) falls back totrapbelow that (#860)BASHUNIT_COVERAGE_SHOW_LINE_HITS=trueprints 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)
- A failed
assert_have_been_called_with/_with_argsstates 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_linemoved tobashunit::console_results::*; no alias kept (#868)bashunit docand the Assertions page now cover all 71 assertions, with a quick-reference table- The
--parallelunsupported-OS warning no longer claims Alpine is excluded - Docs:
bashunit::unmockand the lifecycle of a double: automatic per-test cleanup,set_up_before_scriptdoubles, and whatunmockreaches (#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
h3headings
- Failure and snapshot diffs render with
--no-ext-diff, so a configureddiff.external(difftastic) no longer blanks them (#912) - Time reads and the JUnit report pass the locale via
env; theLC_ALL=C cmdprefix segfaults inside$()on Bash 5.3 macOS (#912) - Call assertions (
assert_not_called,assert_have_been_called*) fail withwas never registered as a spyinstead 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) --parallelno longer discards worker stderr written outside a test body; it renders as aStderr 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
.envno 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_msaccepts a decimal value (#884) - An unknown option is rejected instead of being treated as a test path (#871)
--jobs,--retry,--test-timeout,--coverage-minand--outputreject invalid values;--jobs abcused to hang (#873)- A missing
--env/--bootfile errors instead of a green run that tested nothing; report paths and--seedare validated too (#875) BASHUNIT_COVERAGE_THRESHOLD_LOW/BASHUNIT_COVERAGE_THRESHOLD_HIGHvalidate as non-negative integersbashunit assert <name>with no arguments errors instead of exiting0(#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_equalfailing outside a test function shows its real label- Scratch directories that cannot be created under
TMPDIRabort with an actionable error - A
set_up_before_scriptthat 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
.resultfile counts as a failed test instead of aborting aggregation release.shreports a failed rollback as failed- Docs: the quickstart's duration format, and the real
BASHUNIT_SHOW_EXECUTION_TIMEdefault (auto)
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
- 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)
- Core string assertions (
assert_contains/assert_not_contains,assert_matches/assert_not_matches,assert_string_starts_with/assert_string_ends_withand 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 theecho \| tr \| sedarg sanitizing for data-provider tests) with identical result aggregation (#851)
0.42.0 - 2026-07-20
- A failing
set_up_before_script/set_upnow 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.shfails fast: a failed download, clone, build or copy aborts with a clear error instead of reporting success, and a failed beta clone no longer runsbuild.shin the caller's directory (#840)./build.sh --verifyexits 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 benchworks again from a repository checkout (the dev entrypoint never sourcedsrc/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_numberno longer disablesextdebugfor its caller (#808)--test-timeoutno longer intermittently reports a fast test as timed out; the watchdog signals by pid and skips a test that already completed
watchsubcommand no longer fails when neitherinotifywaitnorfswatchis installed — it falls back to pure-shell polling (interval viaBASHUNIT_WATCH_INTERVAL, default2s) instead of exiting (#779)- Shell tab-completion scripts for bash and zsh under
completions/(subcommands,testflags 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/--paralleland falls back to the full suite when empty (add.bashunit/to.gitignore) (#776)- Optional nightly
coverage.ymlworkflow publishes a shields.io coverage badge from--coverageover the unit suite; schedule/manual only, never gates merges (#754) --jobs auto/-j autocaps parallel concurrency at the CPU core count (portable across Linux/macOS/BSD); the default stays unlimited (#766)
build.shhardened: runs underset -euo pipefail, derives the embed list from the entrypoint'ssourceorder (single source of truth), guards against duplicate embeds and missing doc markers, dropseval, and gates every build behindbash -n(#834)bashunit docno longer forks anecho | sedpipe 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/
--tagscans, single-pass failure rendering). On bash 3.2: 100x10assert_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, avoidingperlforks on bash 3.2;--profile/--verbose/reports still measure (seeadrs/adr-008-auto-skip-per-test-timing.md) (#765) assert_equals/assert_samefailures with multiline values now render a git word-diff below the header (requires git, opt out withBASHUNIT_NO_DIFF=true, respects--no-color); machine reports keep the raw values (#777)
0.41.0 - 2026-07-11
--retry <n>/BASHUNIT_RETRYre-runs a failed test up to N times; passes if any attempt passes, annotates retried tests, and works with--paralleland--stop-on-failure(#737)--random-orderwith--seed <n>/BASHUNIT_SEEDrandomizes 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); nojqdependency (#741)assert_file_permissions <mode> <file>asserts a file's octal permission mode; portable across GNU/BSDstat(#742)assert_array_length <n> <array>asserts an array has exactlynelements (#743)assert_within_delta <expected> <actual> <delta>asserts a number is within a tolerance; supports floats (#744)
watchsubcommand failed withbashunit::watch::run: command not foundin the released binary becausesrc/watch.shwas missing from the build; it is now bundled (#735)
- Faster test execution by removing subprocess forks from hot paths (no behaviour change)
0.40.0 - 2026-06-16
--test-timeout <seconds>flag andBASHUNIT_TEST_TIMEOUTenv var to abort a test that runs longer than N seconds, mark it failed and continue. Disabled by default; no externaltimeoutneeded, works on Bash 3.2+ (#721)
- 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
printfbuiltin no longer breaks coverage collection: the buffer is flushed withbuiltin printf(#724)
- URLs now point to the new primary domain
bashunit.com(oldbashunit.typeddevs.comredirects) - Docs site deploys to GitHub Pages on the
bashunit.comcustom domain (deploy-gh-pages.yml)
- Weekly-downloads chart on the docs homepage (non-portable data source)
0.39.1 - 2026-06-09
bashunit upgraderesolved the floatingv0Action tag as the latest version, breaking upgrades; it now only considers exact-version tags
0.39.0 - 2026-06-09
- GitHub Action
argsinput: when set, runsbashunit <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
v0to each release, so workflows can pinTypedDevs/bashunit@v0to track the latest release within a major (#700) bashunit initnow scaffolds a.github/workflows/tests.ymlCI 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)
- The GitHub Action's
versioninput now defaults to the version pinned at the action ref instead oflatest, so pinning the action by SHA pins a visible bashunit version install.shnow verifies the release checksum by default (setBASHUNIT_VERIFY_CHECKSUM=falseto opt out); it soft-skips with a warning when a checksum asset is unavailable unless verification was explicitly requested (#703)--log-ghaannotations now include the failing test'sline(::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
- Official
TypedDevs/bashunitGitHub Action: composite install action, pinnable by commit SHA for immutable installs (version,directory,add-to-path,verify-checksuminputs;path,versionoutputs) (#695) install.shsha256 checksum verification viaBASHUNIT_VERIFY_CHECKSUM=true, validating the download against the releasechecksumasset (#695)
install.shfails loudly (non-zero exit, no stub binary) on a failed download and retries transient failures, instead of silently reporting success (#695)install.shcreates nested target directories (mkdir -p) (#695)
0.37.0 - 2026-06-03
--profilereports the slowest tests (BASHUNIT_PROFILE_COUNT, default 10) (#678)- Readable snapshot diff when
gitis unavailable (#679) - Failure output shows the test
file:line(#680) .bashunitrcproject config file (#681)- Killed tests report the cause: timeout, SIGINT, SIGKILL/OOM, SIGTERM (#683)
--strictskips Bash 3.0's brokenpipefail(pipefailonly on Bash >= 3.1)bashunit watchforwards--filterand other flags correctly (#682)learnand coverage usemktemp -dfor temp directoriesparallel::cleanuprefuses torm -rfoutside*/bashunit/parallel/*
- 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,mockwere silently skipped) and job labels show only the shard name
- Faster runtime-error detection: single
caseglob (#668) - Cache coverage-enabled flag in hot path (#664)
- Parallel runner uses
wait -non Bash 4.3+ instead of polling (#667) - Hot-path result helpers return via global slots, dropping a fork per call (#662)
- Drop
grep/random_strforks ingenerate_idandnormalize_variable_name(#663)
0.36.0 - 2026-05-07
bashunit upgradeexits non-zero with a clear error when the download fails (no more false success message)
--show-outputdisplays captured test output on assertion failures (#637)- npm registry distribution:
npm install -g bashunit(#244) bashunit::env::supports_colorandbashunit::io::clear_screenhelpers (#247)- LCOV reports now include
FN,FNDA,FNFandFNHfunction records, consumed bygenhtml, Codecov and Coveralls - LCOV reports now include
BRDA,BRFandBRHbranch records forif/elif/elsechains andcasepatterns (seeadrs/adr-007-branch-coverage-mvp.md) BASHUNIT_COVERAGE_SHOW_FUNCTIONS=trueadds a per-function coverage block to the text reportBASHUNIT_COVERAGE_SHOW_UNCOVERED=trueadds an "Uncovered Lines" block to the text report, with consecutive line numbers compressed into ranges
- Docs moved into their own npm workspace under
docs/(usecd docs && npm ciormake docs/install) - Pre-commit hook skips the test suite when no shell files are staged
- ANSI escapes route through
_BASHUNIT_COLOR_*constants (#247)
- Faster coverage report generation: single-pass file scan, native bash regex, cached file stats (#636)
bashunit::runner::run_testsplit into nine named helpers (no behavior change)release.shhandlesdocs/package.jsonas a first-class release file- Structural test guards root
package.jsonagainst regaining dependencies or scripts
0.35.0 - 2026-04-26
bashunit::spyaccepts an optional exit code or custom implementation function (#600)- Assert functions accept an optional trailing label to override the failure title (#77)
--fail-on-riskyflag andBASHUNIT_FAIL_ON_RISKYenv var treat no-assertion tests as failures (#115)--log-gha <file>flag andBASHUNIT_LOG_GHAenv var emit GitHub Actions workflow commands so failed, risky and incomplete tests show up as inline PR annotations (#280)assert_execaccepts--stdin,--stdout-contains,--stdout-not-contains,--stderr-containsand--stderr-not-containsflags to test interactive prompt commands and substring output (#301)
- Parallel test execution is now enabled on Alpine Linux (#370)
- Dim/faint labels now render as gray (SGR 90) so keywords like
ExpectedandTests:stay colored in GitHub Actions logs (#323) - Syntax error in a test file now fails the suite instead of passing silently (#220)
--stop-on-failurenow stops on runtime errors such ascommand not foundorillegal option(#383)- Spying on
echoorprintfno longer hangs via infinite recursion (#607) - LCOV and HTML coverage reports no longer produce empty output under
set -e(#618) clock::nowhandlesEPOCHREALTIMEvalues that use a comma decimal separator- Invalid
.env.examplecoverage threshold entry; CI now copies.env.exampleto.envso 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_trueandassert_falsenow report empty strings as assertion failures instead of trying to execute them
0.34.1 - 2026-03-20
- Add risky test detection for tests with zero assertions (shown as warning, does not fail)
- Fix
sourceof non-existent file inset_up()silently passing all tests (#611) - Fix
set_uprunning before strict mode — unbound variables in hooks now detected with--strict - Fix
sourcefailure intear_down(),set_up_before_script(), andtear_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
- Add
--watchmode andwatch [path]subcommand to re-run tests on file changes- Uses
inotifywaiton Linux orfswatchon macOS; clear install hint if unavailable
- Uses
- Add
--jobs Nflag to limit parallel test concurrency - Add
--tagand--exclude-tagCLI flags for filtering tests by# @tagannotations - Add TAP version 13 output format via
--output tapfor 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_withfor verifying spy arguments on the Nth invocation - Add
assert_string_matches_formatandassert_string_not_matches_formatwith format placeholders (%d,%s,%f,%i,%x,%e,%%) - Add JSON assertions:
assert_json_key_exists,assert_json_contains,assert_json_equals(requiresjq) - Add duration assertions:
assert_duration,assert_duration_less_than,assert_duration_greater_than
- 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
unameresult at source time to eliminate repeated subprocess forks - Replace
bcandawksubprocesses with native bash arithmetic in clock and duration formatting - Cache
base64 -wflag support at load time instead of detecting per test - Use direct variable access for assertion state instead of getter subshells in runner hot path
- Fix misleading error message for
assert_not_sameandassert_not_equalson failure (#604) - Mocking
mktempno 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 skippedcount now includes both skipped and incomplete tests- Convert
timevalues from milliseconds to seconds (float) - Strip ANSI escape sequences and invalid XML control characters from failure messages
- Include actual failure messages in
<failure>body
- Remove non-standard attributes from
- Add unit tests for
env.sh,math.sh,colors.sh,test_title.sh,console_header.sh, anddoc.sh
0.33.0 - 2026-02-15
- Lower minimum Bash version requirement from 3.2 to 3.0
- Improve parallel test execution performance (30-40% faster on large test suites)
- Display test output (stdout/stderr) on failure for runtime errors
- Enabled by default; disable with
--no-output-on-failureorBASHUNIT_SHOW_OUTPUT_ON_FAILURE=false
- Enabled by default; disable with
- Add
--no-progressflag to suppress real-time progress display (Issue #503)- Also available via
BASHUNIT_NO_PROGRESS=trueenvironment variable
- Also available via
- Support
# bashunit: no-parallel-testsdirective to opt out of test-level parallelism
- Data providers now work without the
functionkeyword 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
- Hook output now displays with right-aligned timing and bullet prefix (
● set_up_before_script 2.03s)
- Better code coverage HTML report
- Auto-discover coverage paths from test file names when
BASHUNIT_COVERAGE_PATHSis not settests/unit/assert_test.shautomatically trackssrc/assert.sh- Removes the need for manual
--coverage-pathsconfiguration in most cases
--coverage-report-htmlnow defaults tocoverage/htmlwhen no directory is specified
- 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.shalongside other tests
0.31.0 - 2025-12-19
- Display
set_up_before_scriptandtear_down_after_scriptexecution 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
--coverageflag (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%)
- Fix
benchcommand not working in standalone/installed bashunit (missingbenchmark.shin build) - Fix
helper::get_latest_tagreturning version with^{}suffix from annotated git tags
0.30.0 - 2025-12-14
- 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-onlyflag to suppress passed/skipped/incomplete tests and show only failures - Add
--no-colorflag to disable ANSI color output (also supportsNO_COLORenv var per no-color.org standard) - Add multiple assertions support in standalone mode:
bashunit assert "cmd" exit_code "1" contains "error" - Add
release.shscript to automate the release process with dry-run support
- BREAKING: Rename
--preserve-envflag to--skip-env-filefor clearer semantics - BREAKING: Rename
BASHUNIT_PRESERVE_ENVenvironment variable toBASHUNIT_SKIP_ENV_FILE - Improve documentation for
--skip-env-fileclarifying 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
- Internal flaky tests when running
--strict - Visible stdout/stderr during normal execution
set_up_before_scriptandtear_down_after_script
0.29.0 - 2025-12-08
- Add bootstrap argument passing support via
--env "file.sh arg1 arg2"orBASHUNIT_BOOTSTRAP_ARGS - Add
--preserve-envflag to skip.envloading and use shell environment only - Add
-l, --loginflag to run tests in login shell context - Add
--strictflag to enable strict shell mode (set -euo pipefail) for tests - Add
BASHUNIT_STRICT_MODEconfiguration option (default:false) - Add
-R, --run-allflag to run all assertions even when one fails - Add
BASHUNIT_STOP_ON_ASSERTION_FAILUREconfiguration option (default:true)
- BREAKING: Namespace all internal functions and variables to prevent collisions with user code
- All helper functions now use
bashunit::prefix (e.g.,skip→bashunit::skip) - All internal functions now use
bashunit::prefix (e.g.,helper::trim→bashunit::helper::trim) - All internal variables now use
_BASHUNIT_prefix (e.g.,_TESTS_PASSED→_BASHUNIT_TESTS_PASSED) - All
assert_*functions remain unchanged (public API)
- All helper functions now use
- Improve
assertcommand output: showassert <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_scriptchanges directory - Subsequent test files now run when
set_up_before_scriptchanges directory - Catch intermediate failing commands in
set_up_before_scriptandtear_down_after_script
0.28.0 - 2025-12-01
- Add inline filter syntax to run specific tests from a file
path::function_name- filter tests by function namepath:line_number- run the test containing the specified line
- Add
--show-skippedand--show-incompleteoptions to display skipped/incomplete tests at the end
- BREAKING: Introduce subcommand-based CLI architecture
bashunit test [path]- run tests (default, backwards compatible withbashunit [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
- Stop executing remaining commands in
set_up/tear_downafter first failure - Count all tests as failed when
set_up_before_scriptfails
- Optimize assertion guard: use integer comparison instead of string comparison
0.27.0 - 2025-11-26
- Add
--learninteractive tutorial with 10 progressive lessons for hands-on learning - Add comprehensive "Common Patterns" documentation with real-world testing examples
- Pass arguments to mocked functions
- Fix lifecycle hooks not catching intermediate failing commands in
set_upandtear_down
- Simplify CI: use only
-latestrunners for Ubuntu and macOS, remove deprecatedmacos-13
- Optimize temp directory creation: initialize once at startup instead of per temp file
- Optimize parallel result aggregation: use bash redirection instead of spawning
tailsubprocess - Optimize state access: cache state values to avoid repeated subshell invocations
0.26.0 - 2025-11-02
- Add
assert_unsuccessful_codeassertion 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_setwith arguments containing spaces - Eliminated redundant
declare -F | awkcalls 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.mdfor external developer tools integration - Add tasks storage policy clarifying
.tasks/(versioned) vs.task/(git-ignored)
- Add Project-wide
- Include
set_test_titlehelper 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_withwith pipes - Fix preservation of trailing whitespace in final argument to
data_set - Fix unbound variable error in
parse_data_provider_argswithset -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_withwith 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_dirandtemp_filedata not being cleaned up when created inset_up_before_script - Fix
/tmp/bashunit/parallelnot 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
.bashtest files - Add runtime check for Bash >= 3.2
- Add fallback for
clock::nowwith seconds resolution only - Add
set_test_titleto allow custom test titles - Add
assert_execto validate exit code, stdout and stderr at once
0.22.3 - 2025-07-27
- Fix NixOS support
- Fix parallel and
compgen - Use
command -vinstead ofwhich
- Fix parallel and
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
- Toggle internal log messages with
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
--helpmessage - Add
--no-outputoption
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_providerin favor of# @data_provider - Allow
assert_have_been_called_withto check arguments of specific calls - Enable parallel tests on Windows
- Add
assert_not_called - Improve
helper::find_total_testsperformance - Added
assert_match_snapshot_ignore_colors - Optimize
runner::parse_result_sync - Fix
parse_result_paralleltemplate
0.19.1 - 2025-05-23
- Replace
#!/bin/bashwith#!/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_MODEin favor ofBASHUNIT_DEV_LOG - Added source file and line on global dev function
log
0.18.0 - 2024-10-16
- Added
-p|--parallelto enable running tests in parallel- Enabled only in macOS and Ubuntu
- Added
assert_file_containsandassert_file_not_contains - Added
assert_trueandassert_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|--verboseto display internal details of each test - Fixed
-S|--stop-on-failurebehaviour - 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
--verboseto--detailed- which is the default display behaviour, the opposite as
--simple
- which is the default display behaviour, the opposite as
- 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
--debugoption - 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_ENVtoBASHUNIT_BOOTSTRAP - Better handler runtime errors
- Display failing tests after running the entire suite
- Added defer expressions with
evalwhen 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
wgetifcurlis not installed - Tests can be also be timed by making use of
EPOCHREALTIMEon 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::nowcan't locate Time when is not available - Fixed failing tests when
command not foundandunbound 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|-fto work withtest_*matching function name input. - Added assertions to log file
- Rename the current
assert_equalstoassert_same - Rename
assert_equals_ignore_colorstoassert_equalsand ignore all special chars - Data providers support multiple arguments
- Remove
multi-invokersin favor ofdata providers - Removing trailing slashes
/from the test directories naming output. - Align "Expected" and "but got" on
assert_*fails message. - Change
-vas shortcut for--version - Add
-vvvas 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
sha256sumof 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
--debugoption - Add
dumpandddfunctions 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
rmcommand 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
--upgradeoption to./bashunit - Remove support to deprecated
setUp,tearDown,setUpBeforeScriptandtearDownAfterScriptfunctions - 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_TIMEto 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
--helpoption - Add
-e|--envoption - Add
-S|--stop-on-failureoption - 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_uptearDown->tear_downsetUpBeforeScript->set_up_before_scripttearDownAfterScript->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
mainorlatestwhen using install.sh
0.7.0 - 2023-10-02
- Added
--simpleargument for a simpler output - Manage error when test execution fails
- Split install and build scripts
- Added these functions
mockspyassert_have_been_calledassert_have_been_called_withassert_have_been_called_timesassert_file_existsassert_file_not_existsassert_is_file_emptyassert_is_fileassert_directory_existsassert_directory_not_existsassert_is_directoryassert_is_directory_emptyassert_is_directory_not_emptyassert_is_directory_readableassert_is_directory_not_readableassert_is_directory_writableassert_is_directory_not_writable
- Rename assertions from camelCase to snake_case:
assertEquals->assert_equalsassertNotEquals->assert_not_equalsassertEmpty->assert_emptyassertNotEmpty->assert_not_emptyassertContains->assert_containsassertNotContains->assert_not_containsassertMatches->assert_matchesassertNotMatches->assert_not_matchesassertExitCode->assert_exit_codeassertSuccessfulCode->assert_successful_codeassertGeneralError->assert_general_errorassertCommandNotFound->assert_command_not_foundassertArrayContains->assert_array_containsassertArrayNotContains->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,tearDownandtearDownAfterScriptfunction 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/watchto 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
--filterto./bashunitscript- 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
./bashunitentry 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
assertEqualsfunction