Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
- Performance: a test in a file that defines `set_up` or `tear_down` is about 2.6x faster, which brings it level with a hookless test (14.1ms to 5.4ms per test on macOS arm64, bash 3.2). Each hook minted its output file with `mktemp` and removed it with `rm`, and the ownership marker that left behind made the runner `rm -rf` the test's temp files at exit — five forks per test, even for a test that created no temp file of its own (#1345)

### Fixed
- `assert_contains_ignore_case` documents what it actually guarantees: ASCII case folding. Whether `ñ` matches `Ñ` depends on the shell version, the locale and which `tr` the system ships — BSD `tr` folds it, GNU and busybox `tr` do not, and `nocasematch` only folds it in a UTF-8 locale. Behaviour is unchanged (#1351)
- A test function whose name another selected name starts with is no longer rejected as a duplicate. `test_a` alongside `test_ab` read as one name twice, which made the whole file select nothing. Only definition order reached it, so nothing hit it before (#1347)
- Coverage counts every line of a multiline statement: array literals, quoted strings, heredocs, backslash continuations. An array written one element per line cost one uncovered line per element. Bash 3.x records the assignment on its closing `)`, so there the whole array read as uncovered. A parent statement's hits no longer mark commands inside command or process substitutions as covered, even within quotes or arrays (#1338)
- `--exclude-filter` keeps a comma as part of the test function name. The value was split at the comma, so `--exclude-filter 'test_a,{b}'` also excluded `test_a`. Repeat the flag to exclude several names. `BASHUNIT_EXCLUDE_FILTER` stays comma-separated, so one of its filters cannot hold a literal comma (#1340)
Expand Down
2 changes: 2 additions & 0 deletions docs/assertions.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,8 @@ function test_failure() {

Reports an error if `needle` is not a substring of `haystack`.
Differences in casing are ignored when needle is searched for in haystack.
Case folding is guaranteed for ASCII only: whether `ñ` matches `Ñ` depends on
the shell version, the locale and which `tr` the system ships.

::: code-group
```bash [Example]
Expand Down
23 changes: 18 additions & 5 deletions src/assert/core.sh
Original file line number Diff line number Diff line change
Expand Up @@ -462,10 +462,22 @@ function assert_contains_ignore_case() {

# nocasematch (Bash 3.1+) folds case inside the `case` itself, which costs no
# fork at all; the two `tr` pipelines below cost two. Measured on Bash 3.2:
# 0.087ms per call versus 12.8ms. Both fold non-ASCII identically in a UTF-8
# locale -- `ñü` matches `ÑÜ` either way -- which rules out the tempting
# pure-bash A-Z loop, since that is ASCII-only and would silently stop
# matching accented text that matches today.
# 0.087ms per call versus 12.8ms.
#
# Only ASCII folding is promised, because only ASCII folding is portable.
# Measured on `ÑÜ`:
#
# BSD tr (macOS) folds -> ñü
# GNU tr (Debian) does not -> ÑÜ
# busybox tr does not -> ÑÜ
# nocasematch, UTF-8 folds -> matches
# nocasematch, LC_ALL=C does not -> no match
#
# So the two branches disagree on non-ASCII wherever `tr` is not BSD, and the
# fast path alone disagrees with itself across locales. The Bash 3.0 job runs
# on Debian, so that is CI, not a hypothetical. This comment used to claim the
# two agreed on non-ASCII in a UTF-8 locale, and used that to rule out a
# pure-bash A-Z fold for the fallback; the claim was wrong (#1351).
#
# Prior state is saved and restored rather than blindly unset: nocasematch is
# a global shell option and a user's test file may already have set it. `shopt
Expand All @@ -492,7 +504,8 @@ function assert_contains_ignore_case() {
return
fi

# Bash 3.0 only: nocasematch does not exist, so fold with tr.
# Bash 3.0 only: nocasematch does not exist, so fold with tr. ASCII-only in
# practice on GNU and busybox tr; see the note above.
local expected_lower
local actual_lower
expected_lower=$(printf '%s' "$expected" | tr '[:upper:]' '[:lower:]')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ Reports an error if `needle` is not a substring of `haystack`.

Reports an error if `needle` is not a substring of `haystack`.
Differences in casing are ignored when needle is searched for in haystack.
Case folding is guaranteed for ASCII only: whether `ñ` matches `Ñ` depends on
the shell version, the locale and which `tr` the system ships.


## assert_empty
Expand Down
13 changes: 13 additions & 0 deletions tests/unit/assert/string_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,19 @@ function test_successful_assert_contains_ignore_case() {
assert_empty "$(assert_contains_ignore_case "Linux" "GNU/LINUX")"
}

# ASCII case folding is the whole of what this assertion promises, and it has
# two implementations -- nocasematch from Bash 3.1, two `tr` pipelines below
# that. This runs on both: the Bash 3.0 job takes the `tr` branch, every other
# job the fast one. Non-ASCII is deliberately not asserted, because BSD `tr`
# folds it, GNU and busybox `tr` do not, and nocasematch itself only folds it
# in a UTF-8 locale (#1351).
function test_assert_contains_ignore_case_folds_ascii_on_either_implementation() {
assert_empty "$(assert_contains_ignore_case "linux" "GNU/LINUX")"
assert_empty "$(assert_contains_ignore_case "LINUX" "gnu/linux")"
assert_empty "$(assert_contains_ignore_case "LiNuX" "GnU/lInUx")"
assert_empty "$(assert_contains_ignore_case "0-9_." "0-9_.")"
}

function test_unsuccessful_assert_contains_ignore_case() {
local expected
expected="$(bashunit::console_results::print_failed_test \
Expand Down
Loading