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 @@ -7,6 +7,7 @@

### Changed
- `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)
- Docs: an empty entry in the `-e/--env/--boot` file assigns an empty value rather than restoring the built-in default, so blanking a boolean disables it; the guide said it "wipes" the value, which reads as a reset. The rest of the precedence ladder is now covered by tests (#1217)
- Docs: the test-function name rule is stated correctly — the prefix is a literal, lowercase `test_`, so the guide's own `testRenderAllTestsPassedWhenNotFailedTests` example and its claim that names are case-insensitive were both wrong, and a function named either way is silently never run (#1215)
- Docs: `assert_matches` runs `grep -E` in a subprocess per call — ~2.5ms against ~0.065ms for `assert_same`, about 38x — so hot loops matching a fixed substring should prefer `assert_contains`. The subprocess is required by the Bash 3.0 floor (#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)
Expand Down
9 changes: 7 additions & 2 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,10 @@ to lowest:
The `--env` file is sourced during flag parsing, so it overrides `.env`, `.bashunitrc` and
the ambient environment, and it loses only to flags written **after** it on the command
line: `bashunit --simple --env custom.env` uses the file's value, `bashunit --env custom.env --simple`
uses the flag. Unlike `.env`, an empty entry in that file does wipe a value.
uses the flag. Unlike `.env`, an empty entry in that file is **not** treated as "not
configured here": `BASHUNIT_SHOW_HEADER=` assigns the empty string, which is not the
built-in default — for a boolean setting it simply is not `true`. Delete the entry to fall
back to the default; blanking it disables the setting.

An entry left **empty** in `.env` means "not configured here" and does not
override the environment, so `BASHUNIT_OUTPUT_FORMAT=tap ./bashunit` keeps
Expand All @@ -53,7 +56,9 @@ always wins. `--skip-env-file` skips both `.env` and `.bashunitrc`.
The two files are read differently, which is why they behave differently: `.env` is
**sourced** as a shell script under `allexport`, so it can hold arbitrary shell and an
empty entry is unconditional (hence the preservation rule above), while `.bashunitrc` is
parsed as literal `KEY=value` lines and only fills names that are not already set.
parsed as literal `KEY=value` lines and only fills names that are not already set. The
`--env` file is sourced the same way as `.env` but without that preservation pass, which
is why blanking an entry there assigns an empty value instead of being ignored.

## Benchmark reports

Expand Down
136 changes: 136 additions & 0 deletions tests/acceptance/bashunit_env_precedence_test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
#!/usr/bin/env bash
set -euo pipefail

# The configuration precedence ladder is six levels deep and every level is a
# claim in docs/configuration.md that nothing executed. The two files are read
# by different code -- `.env` is sourced with a preservation pass for values the
# file blanks, the `--env`/`--boot` file is a plain `source` under allexport --
# so the levels can drift apart without either side erroring.
#
# The blanking rule is the one that was documented wrong: an empty entry in the
# `--env` file assigns the empty string, which for a boolean is not the default
# but `false` (#1217).
#
# BASHUNIT_SHOW_HEADER (default true) is the probe for the file/environment
# levels; BASHUNIT_SIMPLE_OUTPUT for the two that need a real CLI flag.

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

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

# Says "shown" or "hidden" for the run header. Every call is a fresh process, so
# the ambient value is passed in rather than exported here.
#
# The "unset" case has to unset it *in the subshell* rather than just not
# setting it: CI runs `cp .env.example .env`, and `.env` is sourced under
# allexport, so the outer run exports BASHUNIT_SHOW_HEADER to every child. An
# inherited value outranks `.bashunitrc` -- correctly, per the ladder -- which
# made the `.bashunitrc` case fail on Linux and the two `--skip-env-file` and
# `--env` cases pass for the wrong reason.
function _header() { # $1 = ambient BASHUNIT_SHOW_HEADER ("" for unset), $@ = extra args
local ambient="$1"
shift
local output
if [ -n "$ambient" ]; then
output=$(cd "$WORKDIR" && BASHUNIT_SHOW_HEADER="$ambient" \
"$BASHUNIT_BIN" --no-parallel "$@" t_test.sh 2>&1 | strip_ansi) || true
else
output=$(cd "$WORKDIR" && unset BASHUNIT_SHOW_HEADER &&
"$BASHUNIT_BIN" --no-parallel "$@" t_test.sh 2>&1 | strip_ansi) || true
fi
# The header is colour-wrapped, so this must run on stripped output: an
# unstripped match silently never fires and every case reads as "hidden".
case "$output" in
*"bashunit - "*) echo "shown" ;;
*) echo "hidden" ;;
esac
}

function _simple() { # $@ = args; says "simple" or "verbose"
local output
output=$(cd "$WORKDIR" && unset BASHUNIT_SIMPLE_OUTPUT &&
"$BASHUNIT_BIN" --no-parallel "$@" t_test.sh 2>&1) || true
case "$output" in
*"Passed"*) echo "verbose" ;;
*) echo "simple" ;;
esac
}

function test_the_builtin_default_shows_the_header() {
assert_same "shown" "$(_header "")"
}

function test_bashunitrc_beats_the_builtin_default() {
printf '%s\n' 'BASHUNIT_SHOW_HEADER=false' >"$WORKDIR/.bashunitrc"

assert_same "hidden" "$(_header "")"
}

function test_the_environment_beats_bashunitrc() {
printf '%s\n' 'BASHUNIT_SHOW_HEADER=false' >"$WORKDIR/.bashunitrc"

assert_same "shown" "$(_header "true")"
}

function test_an_env_entry_with_a_value_beats_the_environment() {
printf '%s\n' 'BASHUNIT_SHOW_HEADER=false' >"$WORKDIR/.env"

assert_same "hidden" "$(_header "true")"
}

# "Not configured here": .env snapshots and restores values it blanks.
function test_an_empty_env_entry_preserves_the_environment() {
printf '%s\n' 'BASHUNIT_SHOW_HEADER=' >"$WORKDIR/.env"

assert_same "shown" "$(_header "true")"
}

function test_the_env_flag_file_beats_dot_env() {
printf '%s\n' 'BASHUNIT_SHOW_HEADER=true' >"$WORKDIR/.env"
printf '%s\n' 'BASHUNIT_SHOW_HEADER=false' >"$WORKDIR/custom.env"

assert_same "hidden" "$(_header "" --env custom.env)"
}

function test_the_env_flag_file_beats_the_environment() {
printf '%s\n' 'BASHUNIT_SHOW_HEADER=false' >"$WORKDIR/custom.env"

assert_same "hidden" "$(_header "true" --env custom.env)"
}

# The documented difference from .env, stated in terms of what is observable:
# the entry overrides, and an empty value is not the default. With the default
# being `true`, a wipe-to-default would show the header; an empty string does
# not equal "true", so it hides it.
function test_an_empty_entry_in_the_env_flag_file_is_not_the_default() {
printf '%s\n' 'BASHUNIT_SHOW_HEADER=' >"$WORKDIR/custom.env"

assert_same "hidden" "$(_header "true" --env custom.env)"
}

function test_skip_env_file_skips_both_files() {
printf '%s\n' 'BASHUNIT_SHOW_HEADER=false' >"$WORKDIR/.env"
printf '%s\n' 'BASHUNIT_SHOW_HEADER=false' >"$WORKDIR/.bashunitrc"

assert_same "shown" "$(_header "" --skip-env-file)"
}

# The --env file is sourced during flag parsing, so it beats a flag written
# before it and loses to one written after.
function test_a_flag_before_the_env_flag_file_loses_to_it() {
printf '%s\n' 'BASHUNIT_SIMPLE_OUTPUT=false' >"$WORKDIR/custom.env"

assert_same "verbose" "$(_simple --simple --env custom.env)"
}

function test_a_flag_after_the_env_flag_file_wins() {
printf '%s\n' 'BASHUNIT_SIMPLE_OUTPUT=false' >"$WORKDIR/custom.env"

assert_same "simple" "$(_simple --env custom.env --simple)"
}
Loading