Problem
bashunit::mock (src/doubles/mock.sh:45) replaces a command with a fixed body or a fixed exit code, and that is all it can do. Two everyday testing needs have no expression:
1. Different results per call. Retry logic, polling loops and backoff are exactly the code that most needs testing, and they all require "fail the first time, succeed the second":
# there is no way to write this today
bashunit::mock curl --returns-in-sequence 1 1 0
The workaround is a hand-rolled counter file in each test, which every user reinvents.
2. Asserting a command was never called. The spy side can assert "called N times", but expressing "this destructive command must never run" means writing assert_have_been_called_times 0 rm, which reads backwards and — before #895 — silently passed for a command that was never spied at all.
Proposal
bashunit::mock_sequence <cmd><body-or-code> [<body-or-code>…]
- Each call consumes the next entry; the last entry repeats once exhausted (document this — the alternative, failing after exhaustion, is also defensible; pick one and test it).
- Entries follow the existing convention that an all-digits value is an exit code and anything else is a body (
bashunit::doubles::is_exit_code, src/doubles/mock.sh:37), so the two doubles keep reading the same way. - Registered in the same registry the runner unwinds after each test, so no leakage between tests.
assert_have_never_been_called <cmd>
Acceptance criteria
Repo checklist (agent)
- TDD: RED → GREEN → REFACTOR. Write the failing test first.
- Bash 3.0+ only: no
printf -v, no += append, no declare -A, no [[ ]], no ${var,,}, no &>>, no ${arr[-1]}. Expanding a possibly-empty array under set -u needs ${arr[@]+"${arr[@]}"}. - A new CLI flag must be wired in all of these or a parity test fails:
- Gates:
make sa, make lint, ./bashunit tests/, ./bashunit --parallel tests/. Never run shfmt -w. - Docs: update
docs/command-line.md. Editing docs/assertions.md invalidates the bashunit doc acceptance snapshot — regenerate it. - CHANGELOG.md: add one line under
## Unreleased. - Fixtures under
tests/acceptance/fixtures/ must not end in *test.sh. - One issue = one PR.
Problem
bashunit::mock(src/doubles/mock.sh:45) replaces a command with a fixed body or a fixed exit code, and that is all it can do. Two everyday testing needs have no expression:1. Different results per call. Retry logic, polling loops and backoff are exactly the code that most needs testing, and they all require "fail the first time, succeed the second":
# there is no way to write this today bashunit::mock curl --returns-in-sequence 1 1 0The workaround is a hand-rolled counter file in each test, which every user reinvents.
2. Asserting a command was never called. The spy side can assert "called N times", but expressing "this destructive command must never run" means writing
assert_have_been_called_times 0 rm, which reads backwards and — before #895 — silently passed for a command that was never spied at all.Proposal
bashunit::doubles::is_exit_code,src/doubles/mock.sh:37), so the two doubles keep reading the same way.bashunit::spy::call_log_to_slot,src/doubles/spy.sh:92).<cmd>was never spied, matching the fix(doubles): call assertions silently pass when the command was never spied #895 rule that an unregistered double is a broken test, not a vacuous pass.Acceptance criteria
bashunit::mock_sequence cmd 1 0returns 1 on the first call and 0 on the second--parallelbashunit::unmockclears a sequenced mock completelyassert_have_never_been_called cmdpasses when the command was spied and never calledcmdwas never spied (fix(doubles): call assertions silently pass when the command was never spied #895 rule)is_exit_codeexists — see the comment atsrc/doubles/mock.sh:33)docs/test-doubles.md; tests intests/unit/doubles/andtests/functional/doubles_test.shRepo checklist (agent)
printf -v, no+=append, nodeclare -A, no[[ ]], no${var,,}, no&>>, no${arr[-1]}. Expanding a possibly-empty array underset -uneeds${arr[@]+"${arr[@]}"}.src/main/test.sh(report-style flags needexport -n, seesrc/main/test.sh:188-196for why)bashunit::main::validate_config_or_exit(src/main/validate.sh:60) — unvalidated input used to run the wrong thing and exit 0 (Unknown options are silently ignored: a typo'd flag runs a different suite and exits 0 #871, --jobs with a non-integer value hangs on Bash 3.x and is silently ignored on Bash 4.3+ #873)src/config/env.shand a documented line in.env.example--helptext in the same block it belongs tocompletions/bashunit.bashandcompletions/_bashunit(anti-drift test feat(cli): bash and zsh completion scripts with an anti-drift test #778 fails otherwise)make sa,make lint,./bashunit tests/,./bashunit --parallel tests/. Never runshfmt -w.docs/command-line.md. Editingdocs/assertions.mdinvalidates thebashunit docacceptance snapshot — regenerate it.## Unreleased.tests/acceptance/fixtures/must not end in*test.sh.