Skip to content

Releases: TypedDevs/bashunit

0.50.1

Choose a tag to compare

@ChemaclassChemaclass released this 22 Aug 08:35
0.50.1
28ea63c

🐛 Bug Fixes

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

👥 Contributors

Checksum

SHA256: 18d83d590c5304f1853dd4fe4fec4ec6effbd9fe5a21831fe9f66f70afe17d93

Full Changelog:0.50.0...0.50.1

0.50.0

Choose a tag to compare

@ChemaclassChemaclass released this 18 Aug 00:48
0.50.0
94d00d2

✨ Improvements

  • -u as 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 — --version included (#1285)
  • Performance: a --parallel run writing a report is about 1.9x faster and uses 1.8x less CPU. Each result row cost fourteen base64 forks per test, so --log-junit cost several times more than running the tests (#1289)

🐛 Bug Fixes

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

🔒 Security

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

🗑️ Removed

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

👥 Contributors

Checksum

SHA256: 1df4d6358292fa972e3870cc6ad5946c06b3fdf162aa796fa108dc1641465b14

Full Changelog:0.49.0...0.50.0

0.49.0

Choose a tag to compare

@ChemaclassChemaclass released this 16 Aug 17:48
0.49.0
81fb3e1

⚠️ 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-tests exits 0 when a run selects no tests. The run still reports No tests found; only the verdict changes. It does not excuse a path that is not on disk. Same flag, same spelling, as jest, vitest, Playwright and Cypress (#1263)
  • --list-tags prints the tags of the selected files, one per line, sorted and deduplicated, and runs nothing (#1265)

Changed

  • A test or bench path that does not exist is named and refused before the run starts, instead of the No tests found / No benchmarks found an empty selection gives. Every genuinely-empty case keeps that message (#1263)
  • A path argument that selects nothing no longer falls back to BASHUNIT_DEFAULT_PATH (#1263)
  • --snapshot-report-unused and --snapshot-prune now report a snapshot whose test file was deleted or renamed — the most common way one is orphaned. This widens what --snapshot-prune deletes (#1194)
  • A --tag matching 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 risky while still exiting 0 (#1271)
  • Time taken no longer reports 0ms, or a negative duration, for a runtime it could not measure (#1271)
  • A run using --test-timeout no 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)
  • --list under --parallel no longer prints No tests found in the middle of the ids (#1007)

Full changelog: 0.48.0...0.49.0

0.48.0

Choose a tag to compare

@ChemaclassChemaclass released this 14 Aug 18:45
0.48.0
71078c5

✨ Improvements

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

🛠️ Changes

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

🐛 Bug Fixes

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

👥 Contributors

Checksum

SHA256: 9e27d930a505fcdc46e0c3275ca943d412e5df4b51dc1f5b5219d794d3b1893d

Full Changelog:0.47.0...0.48.0

0.47.0

Choose a tag to compare

@ChemaclassChemaclass released this 13 Aug 06:25
0.47.0
3800e2a

✨ Improvements

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

🛠️ Changes

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

🐛 Bug Fixes

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

👥 Contributors

Checksum

SHA256: defa50ff54c902acf33c17a2813a879defb349452b51f667736800e63c0156ae

Full Changelog:0.46.0...0.47.0

0.46.0

Choose a tag to compare

@ChemaclassChemaclass released this 11 Aug 19:31
0.46.0
1026187

✨ Improvements

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

🛠️ Changes

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

🐛 Bug Fixes

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

👥 Contributors

Checksum

SHA256: c49fd3874c7df68170f6a22d76599031113f2bb8a5a3bca664fa056dc214e85f

Full Changelog:0.45.0...0.46.0

0.45.0

Choose a tag to compare

@ChemaclassChemaclass released this 09 Aug 08:45
0.45.0
2b87bad

✨ Improvements

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

🛠️ Changes

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

🐛 Bug Fixes

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

👥 Contributors

Checksum

SHA256: 19983f26299825ff26cfbb90e6b3b6e86fc8044168191d3e8b86f615313a80a9

Full Changelog:0.44.0...0.45.0

0.44.0

Choose a tag to compare

@ChemaclassChemaclass released this 29 Jul 08:27
0.44.0
94933c0

✨ Improvements

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

🛠️ Changes

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

🐛 Bug Fixes

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

Removed

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

👥 Contributors

Checksum

SHA256: d3d8ed473f414bdbcce89e215d489646461f2dc90796d98dba42d9768036900c

Full Changelog:0.43.0...0.44.0

0.43.0

Choose a tag to compare

@ChemaclassChemaclass released this 24 Jul 07:15
0.43.0
3914514

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

👥 Contributors

Checksum

SHA256: 151f3647964d53d3f5a7065c141790fc1b66ea3039024c80ed09b3a9602064a2

Full Changelog:0.42.0...0.43.0

0.42.0

Choose a tag to compare

@ChemaclassChemaclass released this 20 Jul 05:36
0.42.0
f54da91

🐛 Bug Fixes

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

✨ Improvements

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

🛠️ Changes

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

👥 Contributors

Checksum

SHA256: a0e39761363d8b6876059cd5927cd4bed1b578be616c5490a8bf4102284a308c

Full Changelog:0.41.0...0.42.0

, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Releases · TypedDevs/bashunit · GitHub
Skip to content

Releases: TypedDevs/bashunit

0.50.1

Choose a tag to compare

@ChemaclassChemaclass released this 22 Aug 08:35
0.50.1
28ea63c

🐛 Bug Fixes

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

👥 Contributors

Checksum

SHA256: 18d83d590c5304f1853dd4fe4fec4ec6effbd9fe5a21831fe9f66f70afe17d93

Full Changelog:0.50.0...0.50.1

0.50.0

Choose a tag to compare

@ChemaclassChemaclass released this 18 Aug 00:48
0.50.0
94d00d2

✨ Improvements

  • -u as 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 — --version included (#1285)
  • Performance: a --parallel run writing a report is about 1.9x faster and uses 1.8x less CPU. Each result row cost fourteen base64 forks per test, so --log-junit cost several times more than running the tests (#1289)

🐛 Bug Fixes

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

🔒 Security

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

🗑️ Removed

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

👥 Contributors

Checksum

SHA256: 1df4d6358292fa972e3870cc6ad5946c06b3fdf162aa796fa108dc1641465b14

Full Changelog:0.49.0...0.50.0

0.49.0

Choose a tag to compare

@ChemaclassChemaclass released this 16 Aug 17:48
0.49.0
81fb3e1

⚠️ 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-tests exits 0 when a run selects no tests. The run still reports No tests found; only the verdict changes. It does not excuse a path that is not on disk. Same flag, same spelling, as jest, vitest, Playwright and Cypress (#1263)
  • --list-tags prints the tags of the selected files, one per line, sorted and deduplicated, and runs nothing (#1265)

Changed

  • A test or bench path that does not exist is named and refused before the run starts, instead of the No tests found / No benchmarks found an empty selection gives. Every genuinely-empty case keeps that message (#1263)
  • A path argument that selects nothing no longer falls back to BASHUNIT_DEFAULT_PATH (#1263)
  • --snapshot-report-unused and --snapshot-prune now report a snapshot whose test file was deleted or renamed — the most common way one is orphaned. This widens what --snapshot-prune deletes (#1194)
  • A --tag matching 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 risky while still exiting 0 (#1271)
  • Time taken no longer reports 0ms, or a negative duration, for a runtime it could not measure (#1271)
  • A run using --test-timeout no 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)
  • --list under --parallel no longer prints No tests found in the middle of the ids (#1007)

Full changelog: 0.48.0...0.49.0

0.48.0

Choose a tag to compare

@ChemaclassChemaclass released this 14 Aug 18:45
0.48.0
71078c5

✨ Improvements

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

🛠️ Changes

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

🐛 Bug Fixes

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

👥 Contributors

Checksum

SHA256: 9e27d930a505fcdc46e0c3275ca943d412e5df4b51dc1f5b5219d794d3b1893d

Full Changelog:0.47.0...0.48.0

0.47.0

Choose a tag to compare

@ChemaclassChemaclass released this 13 Aug 06:25
0.47.0
3800e2a

✨ Improvements

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

🛠️ Changes

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

🐛 Bug Fixes

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

👥 Contributors

Checksum

SHA256: defa50ff54c902acf33c17a2813a879defb349452b51f667736800e63c0156ae

Full Changelog:0.46.0...0.47.0

0.46.0

Choose a tag to compare

@ChemaclassChemaclass released this 11 Aug 19:31
0.46.0
1026187

✨ Improvements

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

🛠️ Changes

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

🐛 Bug Fixes

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

👥 Contributors

Checksum

SHA256: c49fd3874c7df68170f6a22d76599031113f2bb8a5a3bca664fa056dc214e85f

Full Changelog:0.45.0...0.46.0

0.45.0

Choose a tag to compare

@ChemaclassChemaclass released this 09 Aug 08:45
0.45.0
2b87bad

✨ Improvements

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

🛠️ Changes

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

🐛 Bug Fixes

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

👥 Contributors

Checksum

SHA256: 19983f26299825ff26cfbb90e6b3b6e86fc8044168191d3e8b86f615313a80a9

Full Changelog:0.44.0...0.45.0

0.44.0

Choose a tag to compare

@ChemaclassChemaclass released this 29 Jul 08:27
0.44.0
94933c0

✨ Improvements

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

🛠️ Changes

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

🐛 Bug Fixes

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

Removed

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

👥 Contributors

Checksum

SHA256: d3d8ed473f414bdbcce89e215d489646461f2dc90796d98dba42d9768036900c

Full Changelog:0.43.0...0.44.0

0.43.0

Choose a tag to compare

@ChemaclassChemaclass released this 24 Jul 07:15
0.43.0
3914514

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

👥 Contributors

Checksum

SHA256: 151f3647964d53d3f5a7065c141790fc1b66ea3039024c80ed09b3a9602064a2

Full Changelog:0.42.0...0.43.0

0.42.0

Choose a tag to compare

@ChemaclassChemaclass released this 20 Jul 05:36
0.42.0
f54da91

🐛 Bug Fixes

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

✨ Improvements

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

🛠️ Changes

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

👥 Contributors

Checksum

SHA256: a0e39761363d8b6876059cd5927cd4bed1b578be616c5490a8bf4102284a308c

Full Changelog:0.41.0...0.42.0

, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Releases · TypedDevs/bashunit · GitHub
Skip to content

Releases: TypedDevs/bashunit

0.50.1

Choose a tag to compare

@ChemaclassChemaclass released this 22 Aug 08:35
0.50.1
28ea63c

🐛 Bug Fixes

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

👥 Contributors

Checksum

SHA256: 18d83d590c5304f1853dd4fe4fec4ec6effbd9fe5a21831fe9f66f70afe17d93

Full Changelog:0.50.0...0.50.1

0.50.0

Choose a tag to compare

@ChemaclassChemaclass released this 18 Aug 00:48
0.50.0
94d00d2

✨ Improvements

  • -u as 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 — --version included (#1285)
  • Performance: a --parallel run writing a report is about 1.9x faster and uses 1.8x less CPU. Each result row cost fourteen base64 forks per test, so --log-junit cost several times more than running the tests (#1289)

🐛 Bug Fixes

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

🔒 Security

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

🗑️ Removed

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

👥 Contributors

Checksum

SHA256: 1df4d6358292fa972e3870cc6ad5946c06b3fdf162aa796fa108dc1641465b14

Full Changelog:0.49.0...0.50.0

0.49.0

Choose a tag to compare

@ChemaclassChemaclass released this 16 Aug 17:48
0.49.0
81fb3e1

⚠️ 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-tests exits 0 when a run selects no tests. The run still reports No tests found; only the verdict changes. It does not excuse a path that is not on disk. Same flag, same spelling, as jest, vitest, Playwright and Cypress (#1263)
  • --list-tags prints the tags of the selected files, one per line, sorted and deduplicated, and runs nothing (#1265)

Changed

  • A test or bench path that does not exist is named and refused before the run starts, instead of the No tests found / No benchmarks found an empty selection gives. Every genuinely-empty case keeps that message (#1263)
  • A path argument that selects nothing no longer falls back to BASHUNIT_DEFAULT_PATH (#1263)
  • --snapshot-report-unused and --snapshot-prune now report a snapshot whose test file was deleted or renamed — the most common way one is orphaned. This widens what --snapshot-prune deletes (#1194)
  • A --tag matching 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 risky while still exiting 0 (#1271)
  • Time taken no longer reports 0ms, or a negative duration, for a runtime it could not measure (#1271)
  • A run using --test-timeout no 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)
  • --list under --parallel no longer prints No tests found in the middle of the ids (#1007)

Full changelog: 0.48.0...0.49.0

0.48.0

Choose a tag to compare

@ChemaclassChemaclass released this 14 Aug 18:45
0.48.0
71078c5

✨ Improvements

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

🛠️ Changes

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

🐛 Bug Fixes

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

👥 Contributors

Checksum

SHA256: 9e27d930a505fcdc46e0c3275ca943d412e5df4b51dc1f5b5219d794d3b1893d

Full Changelog:0.47.0...0.48.0

0.47.0

Choose a tag to compare

@ChemaclassChemaclass released this 13 Aug 06:25
0.47.0
3800e2a

✨ Improvements

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

🛠️ Changes

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

🐛 Bug Fixes

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

👥 Contributors

Checksum

SHA256: defa50ff54c902acf33c17a2813a879defb349452b51f667736800e63c0156ae

Full Changelog:0.46.0...0.47.0

0.46.0

Choose a tag to compare

@ChemaclassChemaclass released this 11 Aug 19:31
0.46.0
1026187

✨ Improvements

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

🛠️ Changes

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

🐛 Bug Fixes

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

👥 Contributors

Checksum

SHA256: c49fd3874c7df68170f6a22d76599031113f2bb8a5a3bca664fa056dc214e85f

Full Changelog:0.45.0...0.46.0

0.45.0

Choose a tag to compare

@ChemaclassChemaclass released this 09 Aug 08:45
0.45.0
2b87bad

✨ Improvements

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

🛠️ Changes

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

🐛 Bug Fixes

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

👥 Contributors

Checksum

SHA256: 19983f26299825ff26cfbb90e6b3b6e86fc8044168191d3e8b86f615313a80a9

Full Changelog:0.44.0...0.45.0

0.44.0

Choose a tag to compare

@ChemaclassChemaclass released this 29 Jul 08:27
0.44.0
94933c0

✨ Improvements

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

🛠️ Changes

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

🐛 Bug Fixes

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

Removed

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

👥 Contributors

Checksum

SHA256: d3d8ed473f414bdbcce89e215d489646461f2dc90796d98dba42d9768036900c

Full Changelog:0.43.0...0.44.0

0.43.0

Choose a tag to compare

@ChemaclassChemaclass released this 24 Jul 07:15
0.43.0
3914514

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

👥 Contributors

Checksum

SHA256: 151f3647964d53d3f5a7065c141790fc1b66ea3039024c80ed09b3a9602064a2

Full Changelog:0.42.0...0.43.0

0.42.0

Choose a tag to compare

@ChemaclassChemaclass released this 20 Jul 05:36
0.42.0
f54da91

🐛 Bug Fixes

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

✨ Improvements

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

🛠️ Changes

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

👥 Contributors

Checksum

SHA256: a0e39761363d8b6876059cd5927cd4bed1b578be616c5490a8bf4102284a308c

Full Changelog:0.41.0...0.42.0

, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Releases · TypedDevs/bashunit · GitHub
Skip to content

Releases: TypedDevs/bashunit

0.50.1

Choose a tag to compare

@ChemaclassChemaclass released this 22 Aug 08:35
0.50.1
28ea63c

🐛 Bug Fixes

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

👥 Contributors

Checksum

SHA256: 18d83d590c5304f1853dd4fe4fec4ec6effbd9fe5a21831fe9f66f70afe17d93

Full Changelog:0.50.0...0.50.1

0.50.0

Choose a tag to compare

@ChemaclassChemaclass released this 18 Aug 00:48
0.50.0
94d00d2

✨ Improvements

  • -u as 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 — --version included (#1285)
  • Performance: a --parallel run writing a report is about 1.9x faster and uses 1.8x less CPU. Each result row cost fourteen base64 forks per test, so --log-junit cost several times more than running the tests (#1289)

🐛 Bug Fixes

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

🔒 Security

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

🗑️ Removed

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

👥 Contributors

Checksum

SHA256: 1df4d6358292fa972e3870cc6ad5946c06b3fdf162aa796fa108dc1641465b14

Full Changelog:0.49.0...0.50.0

0.49.0

Choose a tag to compare

@ChemaclassChemaclass released this 16 Aug 17:48
0.49.0
81fb3e1

⚠️ 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-tests exits 0 when a run selects no tests. The run still reports No tests found; only the verdict changes. It does not excuse a path that is not on disk. Same flag, same spelling, as jest, vitest, Playwright and Cypress (#1263)
  • --list-tags prints the tags of the selected files, one per line, sorted and deduplicated, and runs nothing (#1265)

Changed

  • A test or bench path that does not exist is named and refused before the run starts, instead of the No tests found / No benchmarks found an empty selection gives. Every genuinely-empty case keeps that message (#1263)
  • A path argument that selects nothing no longer falls back to BASHUNIT_DEFAULT_PATH (#1263)
  • --snapshot-report-unused and --snapshot-prune now report a snapshot whose test file was deleted or renamed — the most common way one is orphaned. This widens what --snapshot-prune deletes (#1194)
  • A --tag matching 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 risky while still exiting 0 (#1271)
  • Time taken no longer reports 0ms, or a negative duration, for a runtime it could not measure (#1271)
  • A run using --test-timeout no 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)
  • --list under --parallel no longer prints No tests found in the middle of the ids (#1007)

Full changelog: 0.48.0...0.49.0

0.48.0

Choose a tag to compare

@ChemaclassChemaclass released this 14 Aug 18:45
0.48.0
71078c5

✨ Improvements

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

🛠️ Changes

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

🐛 Bug Fixes

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

👥 Contributors

Checksum

SHA256: 9e27d930a505fcdc46e0c3275ca943d412e5df4b51dc1f5b5219d794d3b1893d

Full Changelog:0.47.0...0.48.0

0.47.0

Choose a tag to compare

@ChemaclassChemaclass released this 13 Aug 06:25
0.47.0
3800e2a

✨ Improvements

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

🛠️ Changes

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

🐛 Bug Fixes

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

👥 Contributors

Checksum

SHA256: defa50ff54c902acf33c17a2813a879defb349452b51f667736800e63c0156ae

Full Changelog:0.46.0...0.47.0

0.46.0

Choose a tag to compare

@ChemaclassChemaclass released this 11 Aug 19:31
0.46.0
1026187

✨ Improvements

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

🛠️ Changes

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

🐛 Bug Fixes

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

👥 Contributors

Checksum

SHA256: c49fd3874c7df68170f6a22d76599031113f2bb8a5a3bca664fa056dc214e85f

Full Changelog:0.45.0...0.46.0

0.45.0

Choose a tag to compare

@ChemaclassChemaclass released this 09 Aug 08:45
0.45.0
2b87bad

✨ Improvements

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

🛠️ Changes

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

🐛 Bug Fixes

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

👥 Contributors

Checksum

SHA256: 19983f26299825ff26cfbb90e6b3b6e86fc8044168191d3e8b86f615313a80a9

Full Changelog:0.44.0...0.45.0

0.44.0

Choose a tag to compare

@ChemaclassChemaclass released this 29 Jul 08:27
0.44.0
94933c0

✨ Improvements

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

🛠️ Changes

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

🐛 Bug Fixes

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

Removed

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

👥 Contributors

Checksum

SHA256: d3d8ed473f414bdbcce89e215d489646461f2dc90796d98dba42d9768036900c

Full Changelog:0.43.0...0.44.0

0.43.0

Choose a tag to compare

@ChemaclassChemaclass released this 24 Jul 07:15
0.43.0
3914514

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

👥 Contributors

Checksum

SHA256: 151f3647964d53d3f5a7065c141790fc1b66ea3039024c80ed09b3a9602064a2

Full Changelog:0.42.0...0.43.0

0.42.0

Choose a tag to compare

@ChemaclassChemaclass released this 20 Jul 05:36
0.42.0
f54da91

🐛 Bug Fixes

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

✨ Improvements

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

🛠️ Changes

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

👥 Contributors

Checksum

SHA256: a0e39761363d8b6876059cd5927cd4bed1b578be616c5490a8bf4102284a308c

Full Changelog:0.41.0...0.42.0

, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' Releases · TypedDevs/bashunit · GitHub
Skip to content

Releases: TypedDevs/bashunit

0.50.1

Choose a tag to compare

@ChemaclassChemaclass released this 22 Aug 08:35
0.50.1
28ea63c

🐛 Bug Fixes

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

👥 Contributors

Checksum

SHA256: 18d83d590c5304f1853dd4fe4fec4ec6effbd9fe5a21831fe9f66f70afe17d93

Full Changelog:0.50.0...0.50.1

0.50.0

Choose a tag to compare

@ChemaclassChemaclass released this 18 Aug 00:48
0.50.0
94d00d2

✨ Improvements

  • -u as 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 — --version included (#1285)
  • Performance: a --parallel run writing a report is about 1.9x faster and uses 1.8x less CPU. Each result row cost fourteen base64 forks per test, so --log-junit cost several times more than running the tests (#1289)

🐛 Bug Fixes

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

🔒 Security

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

🗑️ Removed

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

👥 Contributors

Checksum

SHA256: 1df4d6358292fa972e3870cc6ad5946c06b3fdf162aa796fa108dc1641465b14

Full Changelog:0.49.0...0.50.0

0.49.0

Choose a tag to compare

@ChemaclassChemaclass released this 16 Aug 17:48
0.49.0
81fb3e1

⚠️ 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-tests exits 0 when a run selects no tests. The run still reports No tests found; only the verdict changes. It does not excuse a path that is not on disk. Same flag, same spelling, as jest, vitest, Playwright and Cypress (#1263)
  • --list-tags prints the tags of the selected files, one per line, sorted and deduplicated, and runs nothing (#1265)

Changed

  • A test or bench path that does not exist is named and refused before the run starts, instead of the No tests found / No benchmarks found an empty selection gives. Every genuinely-empty case keeps that message (#1263)
  • A path argument that selects nothing no longer falls back to BASHUNIT_DEFAULT_PATH (#1263)
  • --snapshot-report-unused and --snapshot-prune now report a snapshot whose test file was deleted or renamed — the most common way one is orphaned. This widens what --snapshot-prune deletes (#1194)
  • A --tag matching 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 risky while still exiting 0 (#1271)
  • Time taken no longer reports 0ms, or a negative duration, for a runtime it could not measure (#1271)
  • A run using --test-timeout no 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)
  • --list under --parallel no longer prints No tests found in the middle of the ids (#1007)

Full changelog: 0.48.0...0.49.0

0.48.0

Choose a tag to compare

@ChemaclassChemaclass released this 14 Aug 18:45
0.48.0
71078c5

✨ Improvements

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

🛠️ Changes

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

🐛 Bug Fixes

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

👥 Contributors

Checksum

SHA256: 9e27d930a505fcdc46e0c3275ca943d412e5df4b51dc1f5b5219d794d3b1893d

Full Changelog:0.47.0...0.48.0

0.47.0

Choose a tag to compare

@ChemaclassChemaclass released this 13 Aug 06:25
0.47.0
3800e2a

✨ Improvements

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

🛠️ Changes

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

🐛 Bug Fixes

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

👥 Contributors

Checksum

SHA256: defa50ff54c902acf33c17a2813a879defb349452b51f667736800e63c0156ae

Full Changelog:0.46.0...0.47.0

0.46.0

Choose a tag to compare

@ChemaclassChemaclass released this 11 Aug 19:31
0.46.0
1026187

✨ Improvements

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

🛠️ Changes

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

🐛 Bug Fixes

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

👥 Contributors

Checksum

SHA256: c49fd3874c7df68170f6a22d76599031113f2bb8a5a3bca664fa056dc214e85f

Full Changelog:0.45.0...0.46.0

0.45.0

Choose a tag to compare

@ChemaclassChemaclass released this 09 Aug 08:45
0.45.0
2b87bad

✨ Improvements

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

🛠️ Changes

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

🐛 Bug Fixes

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

👥 Contributors

Checksum

SHA256: 19983f26299825ff26cfbb90e6b3b6e86fc8044168191d3e8b86f615313a80a9

Full Changelog:0.44.0...0.45.0

0.44.0

Choose a tag to compare

@ChemaclassChemaclass released this 29 Jul 08:27
0.44.0
94933c0

✨ Improvements

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

🛠️ Changes

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

🐛 Bug Fixes

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

Removed

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

👥 Contributors

Checksum

SHA256: d3d8ed473f414bdbcce89e215d489646461f2dc90796d98dba42d9768036900c

Full Changelog:0.43.0...0.44.0

0.43.0

Choose a tag to compare

@ChemaclassChemaclass released this 24 Jul 07:15
0.43.0
3914514

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

👥 Contributors

Checksum

SHA256: 151f3647964d53d3f5a7065c141790fc1b66ea3039024c80ed09b3a9602064a2

Full Changelog:0.42.0...0.43.0

0.42.0

Choose a tag to compare

@ChemaclassChemaclass released this 20 Jul 05:36
0.42.0
f54da91

🐛 Bug Fixes

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

✨ Improvements

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

🛠️ Changes

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

👥 Contributors

Checksum

SHA256: a0e39761363d8b6876059cd5927cd4bed1b578be616c5490a8bf4102284a308c

Full Changelog:0.41.0...0.42.0

, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Releases · TypedDevs/bashunit · GitHub
Skip to content

Releases: TypedDevs/bashunit

0.50.1

Choose a tag to compare

@ChemaclassChemaclass released this 22 Aug 08:35
0.50.1
28ea63c

🐛 Bug Fixes

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

👥 Contributors

Checksum

SHA256: 18d83d590c5304f1853dd4fe4fec4ec6effbd9fe5a21831fe9f66f70afe17d93

Full Changelog:0.50.0...0.50.1

0.50.0

Choose a tag to compare

@ChemaclassChemaclass released this 18 Aug 00:48
0.50.0
94d00d2

✨ Improvements

  • -u as 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 — --version included (#1285)
  • Performance: a --parallel run writing a report is about 1.9x faster and uses 1.8x less CPU. Each result row cost fourteen base64 forks per test, so --log-junit cost several times more than running the tests (#1289)

🐛 Bug Fixes

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

🔒 Security

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

🗑️ Removed

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

👥 Contributors

Checksum

SHA256: 1df4d6358292fa972e3870cc6ad5946c06b3fdf162aa796fa108dc1641465b14

Full Changelog:0.49.0...0.50.0

0.49.0

Choose a tag to compare

@ChemaclassChemaclass released this 16 Aug 17:48
0.49.0
81fb3e1

⚠️ 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-tests exits 0 when a run selects no tests. The run still reports No tests found; only the verdict changes. It does not excuse a path that is not on disk. Same flag, same spelling, as jest, vitest, Playwright and Cypress (#1263)
  • --list-tags prints the tags of the selected files, one per line, sorted and deduplicated, and runs nothing (#1265)

Changed

  • A test or bench path that does not exist is named and refused before the run starts, instead of the No tests found / No benchmarks found an empty selection gives. Every genuinely-empty case keeps that message (#1263)
  • A path argument that selects nothing no longer falls back to BASHUNIT_DEFAULT_PATH (#1263)
  • --snapshot-report-unused and --snapshot-prune now report a snapshot whose test file was deleted or renamed — the most common way one is orphaned. This widens what --snapshot-prune deletes (#1194)
  • A --tag matching 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 risky while still exiting 0 (#1271)
  • Time taken no longer reports 0ms, or a negative duration, for a runtime it could not measure (#1271)
  • A run using --test-timeout no 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)
  • --list under --parallel no longer prints No tests found in the middle of the ids (#1007)

Full changelog: 0.48.0...0.49.0

0.48.0

Choose a tag to compare

@ChemaclassChemaclass released this 14 Aug 18:45
0.48.0
71078c5

✨ Improvements

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

🛠️ Changes

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

🐛 Bug Fixes

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

👥 Contributors

Checksum

SHA256: 9e27d930a505fcdc46e0c3275ca943d412e5df4b51dc1f5b5219d794d3b1893d

Full Changelog:0.47.0...0.48.0

0.47.0

Choose a tag to compare

@ChemaclassChemaclass released this 13 Aug 06:25
0.47.0
3800e2a

✨ Improvements

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

🛠️ Changes

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

🐛 Bug Fixes

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

👥 Contributors

Checksum

SHA256: defa50ff54c902acf33c17a2813a879defb349452b51f667736800e63c0156ae

Full Changelog:0.46.0...0.47.0

0.46.0

Choose a tag to compare

@ChemaclassChemaclass released this 11 Aug 19:31
0.46.0
1026187

✨ Improvements

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

🛠️ Changes

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

🐛 Bug Fixes

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

👥 Contributors

Checksum

SHA256: c49fd3874c7df68170f6a22d76599031113f2bb8a5a3bca664fa056dc214e85f

Full Changelog:0.45.0...0.46.0

0.45.0

Choose a tag to compare

@ChemaclassChemaclass released this 09 Aug 08:45
0.45.0
2b87bad

✨ Improvements

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

🛠️ Changes

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

🐛 Bug Fixes

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

👥 Contributors

Checksum

SHA256: 19983f26299825ff26cfbb90e6b3b6e86fc8044168191d3e8b86f615313a80a9

Full Changelog:0.44.0...0.45.0

0.44.0

Choose a tag to compare

@ChemaclassChemaclass released this 29 Jul 08:27
0.44.0
94933c0

✨ Improvements

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

🛠️ Changes

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

🐛 Bug Fixes

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

Removed

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

👥 Contributors

Checksum

SHA256: d3d8ed473f414bdbcce89e215d489646461f2dc90796d98dba42d9768036900c

Full Changelog:0.43.0...0.44.0

0.43.0

Choose a tag to compare

@ChemaclassChemaclass released this 24 Jul 07:15
0.43.0
3914514

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

👥 Contributors

Checksum

SHA256: 151f3647964d53d3f5a7065c141790fc1b66ea3039024c80ed09b3a9602064a2

Full Changelog:0.42.0...0.43.0

0.42.0

Choose a tag to compare

@ChemaclassChemaclass released this 20 Jul 05:36
0.42.0
f54da91

🐛 Bug Fixes

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

✨ Improvements

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

🛠️ Changes

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

👥 Contributors

Checksum

SHA256: a0e39761363d8b6876059cd5927cd4bed1b578be616c5490a8bf4102284a308c

Full Changelog:0.41.0...0.42.0

, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); Releases · TypedDevs/bashunit · GitHub
Skip to content

Releases: TypedDevs/bashunit

0.50.1

Choose a tag to compare

@ChemaclassChemaclass released this 22 Aug 08:35
0.50.1
28ea63c

🐛 Bug Fixes

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

👥 Contributors

Checksum

SHA256: 18d83d590c5304f1853dd4fe4fec4ec6effbd9fe5a21831fe9f66f70afe17d93

Full Changelog:0.50.0...0.50.1

0.50.0

Choose a tag to compare

@ChemaclassChemaclass released this 18 Aug 00:48
0.50.0
94d00d2

✨ Improvements

  • -u as 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 — --version included (#1285)
  • Performance: a --parallel run writing a report is about 1.9x faster and uses 1.8x less CPU. Each result row cost fourteen base64 forks per test, so --log-junit cost several times more than running the tests (#1289)

🐛 Bug Fixes

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

🔒 Security

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

🗑️ Removed

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

👥 Contributors

Checksum

SHA256: 1df4d6358292fa972e3870cc6ad5946c06b3fdf162aa796fa108dc1641465b14

Full Changelog:0.49.0...0.50.0

0.49.0

Choose a tag to compare

@ChemaclassChemaclass released this 16 Aug 17:48
0.49.0
81fb3e1

⚠️ 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-tests exits 0 when a run selects no tests. The run still reports No tests found; only the verdict changes. It does not excuse a path that is not on disk. Same flag, same spelling, as jest, vitest, Playwright and Cypress (#1263)
  • --list-tags prints the tags of the selected files, one per line, sorted and deduplicated, and runs nothing (#1265)

Changed

  • A test or bench path that does not exist is named and refused before the run starts, instead of the No tests found / No benchmarks found an empty selection gives. Every genuinely-empty case keeps that message (#1263)
  • A path argument that selects nothing no longer falls back to BASHUNIT_DEFAULT_PATH (#1263)
  • --snapshot-report-unused and --snapshot-prune now report a snapshot whose test file was deleted or renamed — the most common way one is orphaned. This widens what --snapshot-prune deletes (#1194)
  • A --tag matching 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 risky while still exiting 0 (#1271)
  • Time taken no longer reports 0ms, or a negative duration, for a runtime it could not measure (#1271)
  • A run using --test-timeout no 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)
  • --list under --parallel no longer prints No tests found in the middle of the ids (#1007)

Full changelog: 0.48.0...0.49.0

0.48.0

Choose a tag to compare

@ChemaclassChemaclass released this 14 Aug 18:45
0.48.0
71078c5

✨ Improvements

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

🛠️ Changes

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

🐛 Bug Fixes

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

👥 Contributors

Checksum

SHA256: 9e27d930a505fcdc46e0c3275ca943d412e5df4b51dc1f5b5219d794d3b1893d

Full Changelog:0.47.0...0.48.0

0.47.0

Choose a tag to compare

@ChemaclassChemaclass released this 13 Aug 06:25
0.47.0
3800e2a

✨ Improvements

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

🛠️ Changes

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

🐛 Bug Fixes

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

👥 Contributors

Checksum

SHA256: defa50ff54c902acf33c17a2813a879defb349452b51f667736800e63c0156ae

Full Changelog:0.46.0...0.47.0

0.46.0

Choose a tag to compare

@ChemaclassChemaclass released this 11 Aug 19:31
0.46.0
1026187

✨ Improvements

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

🛠️ Changes

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

🐛 Bug Fixes

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

👥 Contributors

Checksum

SHA256: c49fd3874c7df68170f6a22d76599031113f2bb8a5a3bca664fa056dc214e85f

Full Changelog:0.45.0...0.46.0

0.45.0

Choose a tag to compare

@ChemaclassChemaclass released this 09 Aug 08:45
0.45.0
2b87bad

✨ Improvements

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

🛠️ Changes

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

🐛 Bug Fixes

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

👥 Contributors

Checksum

SHA256: 19983f26299825ff26cfbb90e6b3b6e86fc8044168191d3e8b86f615313a80a9

Full Changelog:0.44.0...0.45.0

0.44.0

Choose a tag to compare

@ChemaclassChemaclass released this 29 Jul 08:27
0.44.0
94933c0

✨ Improvements

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

🛠️ Changes

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

🐛 Bug Fixes

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

Removed

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

👥 Contributors

Checksum

SHA256: d3d8ed473f414bdbcce89e215d489646461f2dc90796d98dba42d9768036900c

Full Changelog:0.43.0...0.44.0

0.43.0

Choose a tag to compare

@ChemaclassChemaclass released this 24 Jul 07:15
0.43.0
3914514

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

👥 Contributors

Checksum

SHA256: 151f3647964d53d3f5a7065c141790fc1b66ea3039024c80ed09b3a9602064a2

Full Changelog:0.42.0...0.43.0

0.42.0

Choose a tag to compare

@ChemaclassChemaclass released this 20 Jul 05:36
0.42.0
f54da91

🐛 Bug Fixes

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

✨ Improvements

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

🛠️ Changes

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

👥 Contributors

Checksum

SHA256: a0e39761363d8b6876059cd5927cd4bed1b578be616c5490a8bf4102284a308c

Full Changelog:0.41.0...0.42.0