Uh oh!
There was an error while loading. Please reload this page.
Releases: TypedDevs/bashunit
Release list
0.50.1
🐛 Bug Fixes
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)
👥 Contributors
Checksum
SHA256: 18d83d590c5304f1853dd4fe4fec4ec6effbd9fe5a21831fe9f66f70afe17d93
Full Changelog:0.50.0...0.50.1
0.50.0
✨ Improvements
-uas the short form of--snapshot-update, the spelling jest and vitest both use (#1293)
🛠️ Changes
--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)
🐛 Bug Fixes
--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)
🔒 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
riskyinstead 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)
👥 Contributors
Checksum
SHA256: 1df4d6358292fa972e3870cc6ad5946c06b3fdf162aa796fa108dc1641465b14
Full Changelog:0.49.0...0.50.0
0.49.0
⚠️ Upgrade note
bashunit empty_dir/ — a path that exists but holds no test files — used to exit 0 while silently running BASHUNIT_DEFAULT_PATH instead. It now reports No tests found and exits 1.
If a pipeline passes a path that is sometimes empty, its build was green over a suite it never asked for, and it will go red on upgrade. Where the empty run is deliberate — a sharded CI matrix, a changed-files run — use the new flag:
bashunit --pass-with-no-tests --shard 4/4 tests/Added
--pass-with-no-testsexits 0 when a run selects 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 (#1265)
Changed
- A
testorbenchpath that does not exist is 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 (#1263) - A path argument that selects nothing no longer falls back to
BASHUNIT_DEFAULT_PATH(#1263) --snapshot-report-unusedand--snapshot-prunenow report a snapshot whose test file was deleted or renamed — the most common way one is orphaned. This widens what--snapshot-prunedeletes (#1194)- A
--tagmatching nothing now names the tags the run saw (#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— a 100-test file went from 978ms to 542ms against 5000 leftovers (#1269)
Fixed
- 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 (#1271) Time takenno longer reports0ms, or a negative duration, for a runtime it could not measure (#1271)- A run using
--test-timeoutno longer leaves its watchdog holding the caller's captured output for the rest of the timeout budget (#1137) - A test that both fails an assertion and hits a shell error reports the diagnostic alone (#1267)
--listunder--parallelno longer printsNo tests foundin the middle of the ids (#1007)
Full changelog: 0.48.0...0.49.0
0.48.0
✨ Improvements
--verbosewarns on Bash 3.x that coverage does not count lines run inside a subshell, so a lower percentage there explains itself (#1112)
🛠️ Changes
- 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)
🐛 Bug Fixes
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)
👥 Contributors
Checksum
SHA256: 9e27d930a505fcdc46e0c3275ca943d412e5df4b51dc1f5b5219d794d3b1893d
Full Changelog:0.47.0...0.48.0
0.47.0
✨ Improvements
--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)
🛠️ Changes
- 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)
🐛 Bug Fixes
- 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)
👥 Contributors
Checksum
SHA256: defa50ff54c902acf33c17a2813a879defb349452b51f667736800e63c0156ae
Full Changelog:0.46.0...0.47.0
0.46.0
✨ Improvements
--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)
🛠️ Changes
- 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)
🐛 Bug Fixes
- 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)
👥 Contributors
Checksum
SHA256: c49fd3874c7df68170f6a22d76599031113f2bb8a5a3bca664fa056dc214e85f
Full Changelog:0.45.0...0.46.0
0.45.0
✨ Improvements
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)
🛠️ Changes
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)
🐛 Bug Fixes
- 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)
👥 Contributors
Checksum
SHA256: 19983f26299825ff26cfbb90e6b3b6e86fc8044168191d3e8b86f615313a80a9
Full Changelog:0.44.0...0.45.0
0.44.0
✨ Improvements
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)
🛠️ Changes
- 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
🐛 Bug Fixes
- 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)
Removed
bin/create-pr, an unreferenced vendored copy of Chemaclass/create-pr; use the upstream tool (#867)- Dead internal helpers with no remaining callers
👥 Contributors
Checksum
SHA256: d3d8ed473f414bdbcce89e215d489646461f2dc90796d98dba42d9768036900c
Full Changelog:0.43.0...0.44.0
0.43.0
✨ Improvements
- 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)
🛠️ Changes
- 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)
👥 Contributors
Checksum
SHA256: 151f3647964d53d3f5a7065c141790fc1b66ea3039024c80ed09b3a9602064a2
Full Changelog:0.42.0...0.43.0
0.42.0
🐛 Bug Fixes
- 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
✨ Improvements
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)
🛠️ Changes
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)
👥 Contributors
Checksum
SHA256: a0e39761363d8b6876059cd5927cd4bed1b578be616c5490a8bf4102284a308c
Full Changelog:0.41.0...0.42.0