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 @@ -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
- A `--filter` that selects nothing now explains why instead of ending on a bare `No tests found`: filters match the test **function name**, case-sensitively, while the report prints a humanized title, so feeding back the name you just read (`--filter "User login"` for `test_user_login`) silently matched nothing. The run names the test it most likely meant, resolving both the capitalisation and the spaces
- `bashunit doc <filter>` says `No assertion matches '<filter>'` instead of printing nothing, which was indistinguishable from a broken install. Mirrors the existing `--custom` wording; the exit code stays 0 because `doc` is informational (#1201)
- `install.sh` destination errors no longer advise a `-d` flag that does not exist — the script takes positional arguments, so following the advice produced `Invalid arguments`. The one message whose job is to tell you how to recover pointed at a form the parser rejects (#1221)
- The `example/` demo is covered by the suite. `docs/examples.md` and `example/README.md` send new users to `./bashunit example` as their first contact with the framework, and nothing ran it — `make test` collects from `tests/` only — so it could break while CI stayed green. The dead `EXAMPLE_TEST_SCRIPTS` variable, pointing at a file deleted several releases ago, is gone with it (#1219)
Expand Down
10 changes: 10 additions & 0 deletions docs/command-line.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,16 @@ bashunit test tests/ --filter "user_login"
```
:::

The match is against the **function name**, case-sensitively — not the humanized
title the report prints. So the title `✓ Passed: User login` belongs to
`test_user_login`, and `--filter "User login"` selects nothing. A filter that
matches no test says so and names the test it most likely meant:

```
No tests found
No test matches 'User login'. Filters match the function name, not the title: did you mean 'test_user_login'?
```

### Exclude filter

> `bashunit test --exclude-filter "name"`
Expand Down
47 changes: 47 additions & 0 deletions src/console/summary.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,52 @@

# Run totals, execution time and hook completion.

##
# Explains an empty selection that `--filter` caused.
#
# The report prints a humanized title -- `✓ Passed: Alpha` for `test_alpha` --
# while the flag matches the *function name*, case-sensitively. So the most
# natural guess, the name the user just read, selects nothing and the run ends
# on a bare "No tests found" with nowhere to go.
#
# Only the filter is lowercased, never the candidate list: test function names
# are lowercase by convention, so this catches the reported case without a fork
# per function on a path that has already decided to run nothing.
##
function bashunit::console_results::print_filter_hint() {
local filter="${_BASHUNIT_ACTIVE_FILTER:-}"
[ -n "$filter" ] || return 0

local needle="${filter#test_}"
local suggestion=""
if [ -n "$needle" ]; then
local lowered
lowered="$(printf '%s' "$needle" | tr '[:upper:]' '[:lower:]')"
# A humanized title is the function name with underscores shown as spaces,
# so putting them back is what resolves the whole-title copy -- the case
# the project's own agent rules warn about.
local underscored="${lowered// /_}"
local fn
for fn in ${_BASHUNIT_CACHED_ALL_FUNCTIONS:-}; do
case "$fn" in
test_*"$lowered"* | test_*"$underscored"*)
suggestion="$fn"
break
;;
esac
done
fi

if [ -n "$suggestion" ]; then
printf "%sNo test matches '%s'. Filters match the function name, not the title: did you mean '%s'?%s\n" \
"${_BASHUNIT_COLOR_FAINT:-}" "$filter" "$suggestion" "${_BASHUNIT_COLOR_DEFAULT:-}"
return 0
fi

printf "%sNo test matches '%s'. Filters match the function name (test_...), not the title in the report.%s\n" \
"${_BASHUNIT_COLOR_FAINT:-}" "$filter" "${_BASHUNIT_COLOR_DEFAULT:-}"
}

function bashunit::console_results::render_result() {
if [ "$(bashunit::state::is_duplicated_test_functions_found)" = true ]; then
bashunit::console_results::print_execution_time
Expand Down Expand Up @@ -164,6 +210,7 @@ function bashunit::console_results::render_result() {

if [ "$total_tests" -eq 0 ]; then
printf "\n%s%s%s\n" "$_BASHUNIT_COLOR_RETURN_ERROR" " No tests found " "$_BASHUNIT_COLOR_DEFAULT"
bashunit::console_results::print_filter_hint
bashunit::console_results::print_execution_time
return 1
fi
Expand Down
3 changes: 3 additions & 0 deletions src/runner/discovery.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ function bashunit::runner::load_test_files() {
local filter=$1
local tag_filter="${2:-}"
local exclude_tag_filter="${3:-}"
# Carried to the summary so an empty selection can explain itself: the filter
# is a local here, and the report renders long after this returns.
_BASHUNIT_ACTIVE_FILTER="$filter"
shift 3
local IFS=$' \t\n'
local -a files
Expand Down
89 changes: 89 additions & 0 deletions tests/acceptance/bashunit_filter_hint_test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
#!/usr/bin/env bash
set -euo pipefail

# The report prints a humanized title -- `✓ Passed: Alpha` for `test_alpha` --
# and `--filter` matches the function name, case-sensitively. So the most
# natural thing a user can do, copying the name they just read, finds nothing:
#
# $ bashunit --filter Alpha t_test.sh
# No tests found
#
# That is a dead end. The run knows the filter and knows every function it
# sourced, so it can say which rule was applied and name the test that would
# have matched.

function set_up_before_script() {
BASHUNIT_BIN="$(pwd)/bashunit"
}

function set_up() {
WORKDIR="$(bashunit::temp_dir)"
{
printf '%s\n' '#!/usr/bin/env bash'
printf '%s\n' 'function test_alpha() { assert_same 1 1; }'
printf '%s\n' 'function test_beta_case() { assert_same 1 1; }'
} >"$WORKDIR/t_test.sh"
}

function _run() { # $@ = flags
(cd "$WORKDIR" && "$BASHUNIT_BIN" --no-parallel "$@" t_test.sh 2>&1 | strip_ansi) || true
}

# The title as printed, fed straight back.
function test_a_filter_matching_only_by_case_names_the_test_it_meant() {
local output
output="$(_run --filter Alpha)"

assert_contains "No tests found" "$output"
assert_contains "test_alpha" "$output"
}

# The rule itself has to be stated, or the reader learns nothing from a filter
# that has no near match.
function test_a_filter_matching_nothing_states_the_rule() {
local output
output="$(_run --filter nothing_like_this)"

assert_contains "No tests found" "$output"
assert_contains "function name" "$output"
}

# A humanized multi-word title is the same mistake, and the space is why the
# project's own agent rules already warn about it. The title is the function
# name with underscores shown as spaces, so it resolves to a suggestion too.
function test_a_humanized_title_with_a_space_names_the_test_it_meant() {
local output
output="$(_run --filter "Beta case")"

assert_contains "No tests found" "$output"
assert_contains "test_beta_case" "$output"
}

# No filter, no hint: an empty run for any other reason must read exactly as it
# did before, which is also what the path snapshot pins.
function test_an_empty_run_without_a_filter_is_unchanged() {
local output
output="$( (cd "$WORKDIR" && "$BASHUNIT_BIN" --no-parallel --filter '' t_test.sh 2>&1 | strip_ansi) || true)"

assert_not_contains "function name" "$output"
}

# The hint must not appear when the filter did select something.
function test_a_matching_filter_prints_no_hint() {
local output
output="$(_run --filter alpha)"

assert_contains "1 passed" "$output"
assert_not_contains "function name" "$output"
}

# The summary renders in the parent in both modes, but that is exactly the
# boundary where this project has shipped silent divergences before (#1145,
# #1147), so assert it rather than reason about it.
function test_the_hint_reaches_a_parallel_run_too() {
local output
output="$( (cd "$WORKDIR" && "$BASHUNIT_BIN" --parallel --filter "Beta case" t_test.sh 2>&1 | strip_ansi) || true)"

assert_contains "No tests found" "$output"
assert_contains "test_beta_case" "$output"
}
Loading