perf(assert): fold case with nocasematch instead of two tr forks - #977
Merged
Conversation
assert_contains_ignore_case lowercased both operands through a `tr` pipeline,
paying two subprocess forks on every call. `shopt -s nocasematch` folds case
inside the `case` statement itself and costs none. Measured on Bash 3.2: 0.087ms
per call against 12.8ms, and 131ms against 1419ms for a 300-assertion run.
nocasematch arrived in Bash 3.1 and this project's floor is 3.0, so the tr path
stays as the fallback for that one version -- gated by a predicate in the same
shape as runner/context.sh's existing pipefail check. Verified by forcing the
predicate to fail: the whole assert suite passes on the 3.0 path, with identical
results.
Two things worth recording, because both rule out approaches that look better.
A pure-bash A-Z loop would be fork-free on every version including 3.0, and it
benchmarks well: 0.22ms against 2.50ms at 8 characters, crossing over around
128. It is still wrong. In a UTF-8 locale `tr '[:upper:]' '[:lower:]'` folds
accented text, so `ñü` matches `ÑÜ` today; an ASCII-only loop would silently
stop matching it, and the repo's Spanish, Brazilian and Japanese locale jobs
exist for exactly this. nocasematch folds non-ASCII identically -- checked in
both directions.
`${var,,}` is equally correct on Bash 4+ and was rejected for a different
reason: it is banned syntax here, so using it would mean punching an exception
into the Bash 3.0 compatibility gate, which is the strongest contract in the
repo. nocasematch needs no exception and starts helping a whole minor version
earlier.
The option is saved and restored rather than blindly unset -- it is global, and
a user's test file may have set it deliberately. `shopt -q` is a builtin, so the
save is free. Three tests pin that: unset stays unset, set stays set, and
non-ASCII still folds.
1657 sequential / 1616 parallel; baseline + 3. Fork budgets unchanged.
The new non-ASCII test failed on Windows. Not a regression: Git Bash runs in a C-ish locale where neither implementation folds accented case. Verified both agree there -- under LC_ALL=C, tr leaves 'ÑÜ' unchanged and nocasematch does not match either, which is correct behaviour and identical between the two paths. The test was asserting a UTF-8-only outcome as if it were universal. It now probes whether the platform folds non-ASCII at all and skips when it does not, so it still guards the equivalence that matters wherever that equivalence is observable.
The Bash 3.0 jobs went red on the new leak tests, and correctly so: they call `shopt -u nocasematch` directly, and on 3.0 that is an "invalid shell option name" error, so the tests could not even set themselves up. The production code was already gated -- 3.0 takes the tr path. Only the tests reached for the option unconditionally. They now skip on the same predicate, which is also the honest outcome: on 3.0 there is no nocasematch, so there is no option to leak.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
🤔 Background
assert_contains_ignore_caselowercased both operands through atrpipeline, paying two subprocess forks on every call.shopt -s nocasematchfolds case inside thecasestatement itself and costs none.trforksnocasematch💡 Changes
nocasematch arrived in Bash 3.1 and this project's floor is 3.0, so the
trpath stays as the fallback for that one version — gated by a predicate in the same shape asrunner/context.sh's existing pipefail check. Verified by forcing the predicate to fail: the whole assert suite passes on the 3.0 path with identical results.The option is saved and restored, not blindly unset — it's global, and a user's test file may have set it deliberately.
shopt -qis a builtin, so the save is free.🚫 Two better-looking approaches, both rejected
A pure-bash A-Z loop would be fork-free on every version including 3.0, and benchmarks well (0.22ms vs 2.50ms at 8 chars, crossing over around 128). It's still wrong. In a UTF-8 locale
tr '[:upper:]' '[:lower:]'folds accented text, soñümatchesÑÜtoday; an ASCII-only loop would silently stop matching it — and this repo's Spanish, Brazilian and Japanese locale jobs exist for exactly that. nocasematch folds non-ASCII identically, checked in both directions.${var,,}is equally correct on Bash 4+, and was rejected for a different reason: it's banned syntax here, so using it would mean punching an exception into the Bash 3.0 compatibility gate — the strongest contract in the repo. nocasematch needs no exception and starts helping a whole minor version earlier.✅ Verification
Three new tests pin the behaviour: unset stays unset, set stays set, non-ASCII still folds.
make sa·make lint·bash build.sh bin -v→✅ Build verified ✅· 1657 sequential / 1616 parallel-simple-strict · fork budgets unchanged (11/11).