diff --git a/.claude/rules/perf-fork-budget.md b/.claude/rules/perf-fork-budget.md index 260131a7..35ae3748 100644 --- a/.claude/rules/perf-fork-budget.md +++ b/.claude/rules/perf-fork-budget.md @@ -74,6 +74,10 @@ only** — toggling it in the caller's shell clobbers caller state (#808). because 3.2 changed whether a quoted right-hand side is a regex or a literal, so at the 3.0 floor the same pattern would match differently across supported versions. Removing the fork means picking one semantic and breaking the other. + Measured cost of keeping it, so the trade is at least quantified: 2000 + `assert_same` run in ~130 ms (0.065 ms each), 500 `assert_matches` in ~1.25 s + (**2.5 ms each, ~38x**). That is the fork, and it is the price of the 3.0 + floor -- not a regression to chase. - **Single-file build artifact**: sourcing `bin/bashunit` is *not* faster than sourcing `src/*.sh` (parse time dominates, file opens don't). - **`tput cols` at startup**: returns 80 on non-tty; snapshots depend on that diff --git a/CHANGELOG.md b/CHANGELOG.md index a3ca52ef..93c37c7c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - `--verbose` warns on Bash 3.x that coverage does not count lines run inside a subshell, so a percentage that reads lower there than on Bash 4+ explains itself (#1112) ### Changed +- Docs: `assert_matches` notes that it runs `grep -E` in a subprocess per call — measured at ~2.5ms against ~0.065ms for `assert_same`, about 38x — and points at `assert_contains` for hot loops where the pattern is a fixed substring. The subprocess is deliberate: Bash 3.2 changed whether a quoted right-hand side of `[[ =~ ]]` is a regex or a literal, so at the Bash 3.0 floor the same pattern would match differently across supported versions (#1187) - Docs: `assert_match_snapshot` warns that a `@data_provider` test shares one snapshot across all its values — the filename comes from the test function, so the first value creates it and the rest fail against its content. `assert_match_named_snapshot "$1"` gives each value its own (#1185) - A test file that fails to source without writing to stderr now reports its size and says there was no stderr, so a truncated file can be told apart from one whose last command returned non-zero (#1137) - Performance: cold start makes two fewer forks — `check_os::init` ran twice, once at source time and again from the entrypoint, and the root directory came from `$(dirname …)` — worth about 4ms of a 65ms startup, on every invocation (#1124) diff --git a/docs/assertions.md b/docs/assertions.md index 67860f04..b9ff3228 100644 --- a/docs/assertions.md +++ b/docs/assertions.md @@ -254,6 +254,20 @@ function test_failure() { ``` ::: +::: tip Cost in a hot loop +This assertion runs `grep -E` in a subprocess for every call, so it is far more +expensive than a string comparison: measured here, 500 `assert_matches` take +~1.25 s against ~130 ms for 2000 `assert_same` — about **38x per call**. + +That subprocess is deliberate. Bash 3.2 changed whether a quoted right-hand side +of `[[ =~ ]]` is a regex or a literal, so at bashunit's Bash 3.0 floor the same +pattern would match differently across supported versions. + +It is irrelevant for ordinary suites. If you are asserting inside a large loop +and the pattern is a fixed substring or a glob, prefer +[assert_contains](#assert-contains). +::: + ## assert_string_starts_with > `assert_string_starts_with "needle" "haystack"...`