Repository navigation
Releases
0.53.0
✨ Improvements
- Homepage example covering a Bash function, test, run command and passing output
- Release posts for 0.43.0 through 0.52.0, including the 0.50.1 cleanup patch
🛠️ Changes
- Faster indexed lookups for large provider and annotation maps on Bash 3.0 and newer (#1392)
- Common patterns moved under Writing Tests; Quickstart links to function and script testing recipes
- Shorter guides, reference pages and release posts
- Faster project test suite: fewer repeated bashunit runs, and contributor docs use
make test/parallel
🐛 Bug Fixes
- Release preflight checks the repository API through the authenticated GitHub CLI instead of requiring access to the GitHub homepage
Assertions and doubles
- Date assertions reject trailing text on macOS and handle colon-separated timezone offsets consistently across platforms (#1404)
assert_string_not_starts_withjoins all haystack arguments and labels failures with the test name, matching its siblings (#1403)assert_matchesandassert_not_matchesaccept patterns starting with-and fail when grep rejects a pattern (#1398)assert_files_equalsandassert_files_not_equalsname missing paths and fail when either file is absent (#1397)- Snapshots record and update
-n,-e,-Eand combined echo flags literally (#1402) - Spies keep multiline arguments in one call, including comparisons and failure logs (#1399)
- Call argument assertions reject uncalled spies and missing call indices (#1400)
bashunit::unmockrestores functions replaced by spies, mocks or mock sequences (#1401)
Setup and selection
install.shandupgradepreserve installed binaries after failed or empty downloads; installation also preserves them after checksum failures (#1406, #1407)install.shresolves relative destinations from the caller's directory and reports the resolved path (#1408)install.shrejects prefixed or incomplete versions instead of treating them as directories (#1409)initquotes bootstrap and test paths containing spaces in.env. Generated workflows preserve whitespace in test paths without a committed.env(#1411, #1425).bashunitrcsuite options are not exported; global settings before and after suite sections still apply (#1410)- Include, inline and exclusion filters preserve
test_within names such aslatest_version(#1414) --debugpreserves existing test files and directories instead of treating them as trace files (#1405)
Test discovery and execution
- Annotations, tags, header counts and duplicate checks support functions without parentheses (#1422)
- Duplicate checks catch definitions with next-line opening braces instead of losing the first definition (#1421)
- Echo providers preserve
#and parentheses, report malformed rows and keep later rows. Quoted values preserve carriage returns on Windows (#1419) - SIGTERM and SIGHUP run teardown hooks, stop parallel workers, remove scratch files and exit with status 1 (#1417)
- Parallel results and hook output stay separate for paths differing by slashes and underscores (#1416)
- Relative hidden paths are counted and keep separate results from matching visible paths (#1393)
- Passing many test files by name no longer leaks two open files per path, which crashed macOS Bash 3.2 at about 126 files (#1461)
Reports and reruns
- Sequential
--stop-on-failurewrites requested reports and updates the rerun cache for executed tests (#1415) - Passing file or filtered runs preserve failure-cache entries for unexecuted tests (#1418)
- Coverage paths are checked before tests run; failed LCOV or HTML writes fail the run (#1412)
- TAP keeps titles containing line breaks on one result line (#1413)
- Failure source context stays within a one-line test instead of listing the next function's assertions (#1420)
Documentation
- Homepage logo respects reduced motion and stops tracking the pointer after navigation
- Primary buttons use dark text in both themes, including hover and active states
- Quickstart commands match
install.shand project or global npm installs
👥 Contributors
Checksum
SHA256: 21661108a5d7a396c16ffb74af14c43affcc18d7f68cd0fb3d8768559cc2d690
Full Changelog: 0.52.0...0.53.0
0.52.0
🛠️ Changes
- Sequential multi-file runs reuse provider and annotation metadata for unchanged test files up to 512 bytes (#1372)
- Parallel reports avoid per-row base64 processes while preserving values and completion order (#1371)
- Data providers avoid per-argument base64 processes while preserving quoting, empty values and parser isolation (#1370)
- JSON reports avoid per-field processes for ordinary filenames, test names and empty messages while preserving escaping (#1369)
- Contributor instructions share task-specific context and model tiers for Claude Code and Codex through a pinned agnostic-ai npm development dependency; shorter workflows align review and release checks with the current code (#1387)
🐛 Bug Fixes
- Releases update and commit project versions in both npm lockfiles and restore them on rollback
- Concurrent provider rows own separate temporary files, even with identical arguments (#1379)
- Synchronous provider rows retain separate results in parallel runs, so later passing rows cannot hide failures (#1377)
- Late timeout signals no longer interrupt
tear_down; the watchdog still bounds hanging cleanup (#1376) - Piped parallel output no longer waits for an inactive spinner's sleep process (#1368)
- Cancellation regression checks distinguish exited test bodies awaiting reaping from running cleanup (#1385)
- Bash compatibility checks scan only shell source files, excluding generated contributor instructions (#1387)
- Skipped macOS and Windows acceptance CI jobs have readable names, with shard labels in test steps
- Linux CI installs the standalone build optimizer required by beta installer acceptance tests (#1383)
👥 Contributors
Checksum
SHA256: 6193370a2164abcc1e64601af461ea811505c6cefdc8fa33e579a07e2d05ffe3
Full Changelog: 0.51.0...0.52.0
0.51.0
🛠️ Changes
- Performance: a
--parallelrun on a shell withoutEPOCHREALTIMEno longer forksperlonce per test. The clock probe resolved inside a command substitution, so the resolved value died with it and every worker re-probed: 502perlexecs for a 500-test file, now 2. It happened even with per-test timing off, since deciding that timing is off is what asks whether the clock is expensive. Fixed by #1358; this adds the guards that keep it fixed (#1353) - A helper can ship a faster body for a newer Bash and keep the Bash 3.0 one as a fallback, chosen once at load time. The floor does not move: 3.0 keeps working and keeps being tested. The compatibility rules now record each construct's minimum version instead of only banning it, and allow one inside a matching version gate, never for a construct that is a parse error on the floor. See
adrs/adr-013-bash-version-gated-fast-paths.md(#1352) - Performance: a file defining
set_up_before_scriptortear_down_after_scriptcosts about 5.6ms less on Bash 3.2, and a test rendered with--show-execution-timeabout 0.74ms less. Four clock reads per file and the per-test padding no longer go through a capture subshell (#1348) - Performance: startup is faster on suites of plain test files. Printing "Running N tests" sourced every file a second time and re-ran every data provider before the run began. A file whose functions the provider scan can already see is counted from that scan instead: over this repo's 241 files the counting pass went from 1.72s to 1.51s, and 124 of them no longer source or run a provider twice. Files with a data provider, a heredoc, a multi-line string, an
eval, a nestedsourceor a conditional definition keep the old path, so the count can never disagree with the run (#1347) - Performance: a sequential run is about 0.7ms faster per test, taking a 500-test file from 2.75s to 2.40s on macOS arm64, bash 3.2. A test's
<file>:<line>is resolved on demand instead of forking a subshell per test to read it (#1346) - A standalone
bashunit assert <fn> ...no longer reports a source location belonging to another process. Launched from inside a test, it inherited that test's exported location and printed it as its own (#1346) - Performance: a test in a file that defines
set_uportear_downis about 2.6x faster, 14.1ms to 5.4ms per test on macOS arm64, bash 3.2, which brings it level with a hookless test. Each hook minted its output file withmktempand removed it withrm, and the ownership marker that left behind made the runner sweep the test's temp files at exit: five forks per test, even for a test that created no temp file (#1345)
🐛 Bug Fixes
- The deprecation warning for the old
--assertoption names the invocation you typed:bashunit -a,bashunit --assertorbashunit test --assert. It always saidbashunit test --assert, becausebashunit -areaches that path through the inferredtestsubcommand, so it read like a warning about a command you never ran (#1366) assert_contains_ignore_casedocuments what it actually guarantees: ASCII case folding. WhetherñmatchesÑdepends on the shell version, the locale and whichtrthe system ships. Behaviour is unchanged (#1351)- A test function whose name another selected name starts with is no longer rejected as a duplicate.
test_aalongsidetest_abread as one name twice, which made the whole file select nothing (#1347) - 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, and on Bash 3.x the whole array read as uncovered. A parent statement's hits no longer mark commands inside command or process substitutions as covered (#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 (#1340)- A
--parallelrun no longer replays bash'schild setpgidwarning as the stderr of a file that wrote nothing. The child sets its process 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, which a loaded machine missed in about one parallel full-suite run in three. It now polls for the body to exit, capped at 2s (#1344)
👥 Contributors
Checksum
SHA256: 307e3a84d5561d1a1d8b5d378184bb1dfda3e7a7856c4a9435d63a53853cf72c
Full Changelog: 0.50.1...0.51.0
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