From 19cc16e72e87d4d7c8fd91573004cd1772989e63 Mon Sep 17 00:00:00 2001 From: Chemaclass Date: Sat, 25 Jul 2026 15:33:28 +0200 Subject: [PATCH] feat(env): warn at runtime on deprecated forms Deprecations were recorded only in help text and docs, with no path out: the unprefixed aliases have been superseded since 0.15.0 and 'bashunit test --assert' since the assert subcommand landed, and neither ever told a user at runtime. Adds bashunit::env::warn_deprecated and wires it to both. The warning goes to stderr so it cannot corrupt a report on stdout, and BASHUNIT_NO_DEPRECATION_WARNINGS=true silences it. Detection for the unprefixed aliases runs before the ':=' block, because afterwards the prefixed name is set either way and the two cases are indistinguishable. It is pure bash: a 'compgen -v' capture would cost a fork on the budgeted cold-start path. The hand-maintained alias list is kept honest by an anti-drift test against env.sh. Documents the policy: a deprecated form warns for at least one minor before removal in a major. Note '-a/--assert' is NOT a pure rename -- it goes through cmd_test's parser so it also accepts --env/--no-parallel, which 'bashunit assert' forwards to the assertion instead. That gap must close before the form can be removed; recorded in the code. --- CHANGELOG.md | 1 + docs/configuration.md | 32 ++++++++++ src/env.sh | 57 +++++++++++++++++ src/main.sh | 9 +++ .../acceptance/bashunit_assert_errors_test.sh | 19 +++++- .../bashunit_direct_fn_call_advanced_test.sh | 4 ++ .../bashunit_direct_fn_call_basic_test.sh | 4 ++ ...t_bashunit_direct_fn_call_failure.snapshot | 2 +- tests/unit/env_deprecated_aliases_test.sh | 61 +++++++++++++++++++ 9 files changed, 187 insertions(+), 2 deletions(-) create mode 100644 tests/unit/env_deprecated_aliases_test.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 8129ad44..7bb36578 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - Per-line execution hit counts in the text report: `BASHUNIT_COVERAGE_SHOW_LINE_HITS=true` prints a `Line Hits` block listing each covered line as `:` per file. The LCOV report already carried the same counts in its `DA:,` records; those are now pinned by tests (#856) ### Changed +- Deprecated forms now warn at runtime instead of only being marked deprecated in a help string. When an unprefixed setting (`VERBOSE`, `COVERAGE`, …) is what supplied a value, or `bashunit test --assert` is used, bashunit prints `Deprecated: … Use … instead.` on **stderr**, so it never corrupts a report on stdout. Silence with `BASHUNIT_NO_DEPRECATION_WARNINGS=true`. The policy is now documented: a deprecated form warns for at least one minor release before being removed in a major one (#866) - Docs: [Command line](https://bashunit.com/command-line#invalid-input) now documents how bashunit rejects invalid input — unknown options, out-of-range values and unusable bootstrap/report paths — which had been shipped across several releases without a page describing it. `BASHUNIT_REPORT_TAP` and `BASHUNIT_REPORT_JSON` are documented in [Configuration](https://bashunit.com/configuration) alongside the JUnit/HTML/GHA reports they sit next to, and [Standalone](https://bashunit.com/standalone) documents the malformed-invocation exit code - Docs: `snapshots.md` omitted the optional `["snapshot_file"]` argument that both snapshot assertions accept; the README claimed "hundreds of assertions" against an actual 71 - `bashunit doc` and the [Assertions](https://bashunit.com/assertions) reference now cover all 71 assertions instead of 64. The two snapshot assertions and all five spy assertions were documented only on their own pages, so `bashunit doc` — the command the docs tell agents to use so they do not invent names — listed no spy assertion at all diff --git a/docs/configuration.md b/docs/configuration.md index 9df2fa6c..ed0922af 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -875,6 +875,38 @@ BASHUNIT_COVERAGE_THRESHOLD_HIGH=90 ``` ::: +## Deprecations + +Every setting also answers to an **unprefixed** name — `VERBOSE` as well as +`BASHUNIT_VERBOSE`. Those unprefixed aliases predate the `BASHUNIT_` prefix +introduced in 0.15.0 and are deprecated: the names are generic enough that an +unrelated tool exporting `COVERAGE=true` or `VERBOSE=true` silently +reconfigures bashunit. Always use the prefixed name. + +When a deprecated form is what supplied a value, bashunit says so on stderr: + +``` +Deprecated: the unprefixed `VERBOSE`. Use `BASHUNIT_VERBOSE` instead. +``` + +The warning goes to stderr, so it never corrupts a report on stdout. Silence it +with: + +> `BASHUNIT_NO_DEPRECATION_WARNINGS=true` + +::: tip Deprecation policy +A form that still works but is on its way out warns for at least one minor +release before it is removed in a major one. If you see one of these warnings, +you have time to migrate — but the form will not be there forever. +::: + +Currently deprecated: + +| Deprecated | Use instead | +|------------|-------------| +| Unprefixed settings (`VERBOSE`, `COVERAGE`, …) | The `BASHUNIT_`-prefixed name | +| `bashunit test --assert ` | [`bashunit assert `](/standalone) | + ## Related - [Command line](/command-line) — flags that mirror these settings diff --git a/src/env.sh b/src/env.sh index f4ddf985..91a2ee9b 100644 --- a/src/env.sh +++ b/src/env.sh @@ -45,6 +45,24 @@ function bashunit::env::load_config_file() { done <"$file" } +## +# Reports a deprecated form once per run, on stderr so it cannot corrupt a +# report on stdout. Silence with BASHUNIT_NO_DEPRECATION_WARNINGS=true. +# +# The deprecation policy: a form that still works but is on its way out warns +# here for at least one minor release before it is removed in a major one. That +# gives the warning somewhere to live instead of the deprecation sitting +# indefinitely in a help string nobody reads. +# +# Arguments: $1 the deprecated form, $2 what to use instead +## +function bashunit::env::warn_deprecated() { + [ "${BASHUNIT_NO_DEPRECATION_WARNINGS:-false}" = "true" ] && return 0 + + printf "%sDeprecated: %s. Use %s instead.%s\n" \ + "${_BASHUNIT_COLOR_SKIPPED:-}" "$1" "$2" "${_BASHUNIT_COLOR_DEFAULT:-}" >&2 +} + ## # Echoes $1 when it is a positive integer, otherwise echoes the default $2. # Arguments: $1 candidate value, $2 fallback default @@ -99,6 +117,45 @@ if [ "${BASHUNIT_SKIP_ENV_FILE:-false}" != "true" ]; then fi fi +# Settings that still answer to an unprefixed name (`VERBOSE`, `COVERAGE`, …). +# The prefix landed in 0.15.0; these remain only for back-compat and are an +# active hazard, because the names are generic enough that an unrelated tool +# exporting COVERAGE=true silently reconfigures bashunit (#866). +# +# Kept in sync with the `: "${BASHUNIT_X:=${X:=…}}"` lines below by +# tests/unit/env_deprecated_aliases_test.sh, which fails if the two drift. +_BASHUNIT_DEPRECATED_ALIASES="DEFAULT_PATH DEV_LOG BOOTSTRAP BOOTSTRAP_ARGS +LOG_JUNIT LOG_GHA REPORT_HTML REPORT_TAP REPORT_JSON WATCH_INTERVAL COVERAGE +COVERAGE_PATHS COVERAGE_EXCLUDE COVERAGE_REPORT COVERAGE_REPORT_HTML +COVERAGE_MIN COVERAGE_THRESHOLD_LOW COVERAGE_THRESHOLD_HIGH PARALLEL_RUN +SHOW_HEADER HEADER_ASCII_ART SIMPLE_OUTPUT STOP_ON_FAILURE SHOW_EXECUTION_TIME +VERBOSE BENCH_MODE NO_OUTPUT INTERNAL_LOG SHOW_SKIPPED SHOW_INCOMPLETE +STRICT_MODE STOP_ON_ASSERTION_FAILURE SKIP_ENV_FILE LOGIN_SHELL FAILURES_ONLY +SHOW_OUTPUT_ON_FAILURE NO_DIFF NO_PROGRESS OUTPUT_FORMAT FAIL_ON_RISKY PROFILE +PROFILE_COUNT TEST_TIMEOUT" + +# Record which unprefixed names are about to supply a value. Runs BEFORE the +# `:=` block below, because after it the prefixed name is set either way and the +# two cases are indistinguishable. Pure bash: a `compgen -v` capture would cost +# a fork on the cold-start path, which is budgeted (#801). +_bashunit_deprecated_in_use="" +for _bashunit_alias in $_BASHUNIT_DEPRECATED_ALIASES; do + eval "_bashunit_alias_prefixed=\${BASHUNIT_$_bashunit_alias+set}" + [ -n "$_bashunit_alias_prefixed" ] && continue + eval "_bashunit_alias_value=\${$_bashunit_alias:-}" + [ -n "$_bashunit_alias_value" ] && + _bashunit_deprecated_in_use="$_bashunit_deprecated_in_use $_bashunit_alias" +done +unset _bashunit_alias _bashunit_alias_prefixed _bashunit_alias_value + +if [ -n "$_bashunit_deprecated_in_use" ]; then + for _bashunit_alias in $_bashunit_deprecated_in_use; do + bashunit::env::warn_deprecated \ + "the unprefixed \`$_bashunit_alias\`" "\`BASHUNIT_$_bashunit_alias\`" + done + unset _bashunit_alias +fi + _BASHUNIT_DEFAULT_DEFAULT_PATH="tests" _BASHUNIT_DEFAULT_BOOTSTRAP="tests/bootstrap.sh" _BASHUNIT_DEFAULT_DEV_LOG="" diff --git a/src/main.sh b/src/main.sh index 8985345a..ef516f43 100644 --- a/src/main.sh +++ b/src/main.sh @@ -166,6 +166,15 @@ function bashunit::main::cmd_test() { while [ $# -gt 0 ]; do case "$1" in -a | --assert) + # Superseded by the `bashunit assert` subcommand. The help text has said + # "deprecated" since that subcommand landed without ever warning at + # runtime, which is how a deprecation stays put forever. + # + # Not a pure rename: this path goes through cmd_test's parser, so it also + # accepts test-level flags (`--env`, `--no-parallel`). `bashunit assert` + # forwards everything to the assertion instead. Removing this form needs + # that gap closed first. + bashunit::env::warn_deprecated "\`bashunit test --assert\`" "\`bashunit assert\`" assert_fn="$2" shift ;; diff --git a/tests/acceptance/bashunit_assert_errors_test.sh b/tests/acceptance/bashunit_assert_errors_test.sh index fc12aa58..11791631 100644 --- a/tests/acceptance/bashunit_assert_errors_test.sh +++ b/tests/acceptance/bashunit_assert_errors_test.sh @@ -56,7 +56,9 @@ function test_bashunit_assert_subcommand_failure() { assert_general_error "" "" "$exit_code" } -# Test backward compatibility with --assert option +# Backward compatibility with the deprecated --assert option. These are the only +# tests that should still use it: everything else exercises `bashunit assert`. +# The form keeps working and warns; it is removed no earlier than the next major. function test_bashunit_old_assert_option_still_works() { local output output=$(./bashunit -a equals "foo" "foo" 2>&1) @@ -69,6 +71,21 @@ function test_bashunit_old_assert_option_long_form() { assert_successful_code "$output" } +function test_bashunit_old_assert_option_warns_that_it_is_deprecated() { + local warnings + warnings=$(./bashunit -a equals "foo" "foo" 2>&1 >/dev/null) + + assert_contains "Deprecated" "$warnings" + assert_contains "bashunit assert" "$warnings" +} + +function test_bashunit_new_assert_subcommand_does_not_warn() { + local warnings + warnings=$(./bashunit assert equals "foo" "foo" 2>&1 >/dev/null) + + assert_empty "$warnings" +} + # Test deprecation notice in help function test_bashunit_test_help_shows_deprecation() { local output diff --git a/tests/acceptance/bashunit_direct_fn_call_advanced_test.sh b/tests/acceptance/bashunit_direct_fn_call_advanced_test.sh index 93b9c497..3e545316 100644 --- a/tests/acceptance/bashunit_direct_fn_call_advanced_test.sh +++ b/tests/acceptance/bashunit_direct_fn_call_advanced_test.sh @@ -7,6 +7,10 @@ function set_up_before_script() { function set_up() { export BASHUNIT_SIMPLE_OUTPUT=false + # These exercise the deprecated `-a` form itself, so silence its warning to + # keep the captured output stable. The warning is covered by + # bashunit_assert_errors_test.sh instead. + export BASHUNIT_NO_DEPRECATION_WARNINGS=true } function test_bashunit_direct_fn_call_failure() { diff --git a/tests/acceptance/bashunit_direct_fn_call_basic_test.sh b/tests/acceptance/bashunit_direct_fn_call_basic_test.sh index 31382300..5c7e4542 100644 --- a/tests/acceptance/bashunit_direct_fn_call_basic_test.sh +++ b/tests/acceptance/bashunit_direct_fn_call_basic_test.sh @@ -11,6 +11,10 @@ find me with \n a regular expression" function set_up() { export BASHUNIT_SIMPLE_OUTPUT=false + # These exercise the deprecated `-a` form itself, so silence its warning to + # keep the captured output stable. The warning is covered by + # bashunit_assert_errors_test.sh instead. + export BASHUNIT_NO_DEPRECATION_WARNINGS=true } function test_bashunit_direct_fn_call_passes() { diff --git a/tests/acceptance/snapshots/bashunit_direct_fn_call_advanced_test_sh.test_bashunit_direct_fn_call_failure.snapshot b/tests/acceptance/snapshots/bashunit_direct_fn_call_advanced_test_sh.test_bashunit_direct_fn_call_failure.snapshot index e90a70f3..c38ae128 100644 --- a/tests/acceptance/snapshots/bashunit_direct_fn_call_advanced_test_sh.test_bashunit_direct_fn_call_failure.snapshot +++ b/tests/acceptance/snapshots/bashunit_direct_fn_call_advanced_test_sh.test_bashunit_direct_fn_call_failure.snapshot @@ -1,4 +1,4 @@ ✗ Failed: assert same Expected 'foo' but got  'bar' - at tests/acceptance/bashunit_direct_fn_call_advanced_test.sh:12 + at tests/acceptance/bashunit_direct_fn_call_advanced_test.sh:16 diff --git a/tests/unit/env_deprecated_aliases_test.sh b/tests/unit/env_deprecated_aliases_test.sh new file mode 100644 index 00000000..de25ff9c --- /dev/null +++ b/tests/unit/env_deprecated_aliases_test.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash + +# _BASHUNIT_DEPRECATED_ALIASES is hand-maintained: deriving it at runtime would +# need a `compgen -v` capture, and that costs a fork on the cold-start path, +# which is budgeted (#801). These tests are what keeps a hand-maintained list +# honest -- they fail if a setting gains or loses its unprefixed alias without +# the list being updated. + +# Every `: "${BASHUNIT_X:=${X:=...}}"` line in env.sh declares an unprefixed +# alias for X. That is the source of truth. +function bashunit::test::aliases_declared_in_env_sh() { + grep -oE '^: "\$\{BASHUNIT_[A-Z0-9_]+:=\$\{[A-Z0-9_]+:=' src/env.sh | + sed -E 's/^: "\$\{BASHUNIT_[A-Z0-9_]+:=\$\{([A-Z0-9_]+):=$/\1/' | + sort +} + +function bashunit::test::aliases_listed_in_the_constant() { + # Unquoted on purpose: the constant is a whitespace-separated list and the + # word splitting is what turns it into one name per line. + # shellcheck disable=SC2086 + printf '%s\n' $_BASHUNIT_DEPRECATED_ALIASES | sort +} + +function test_the_alias_list_matches_the_declarations_in_env_sh() { + local declared listed + declared=$(bashunit::test::aliases_declared_in_env_sh) + listed=$(bashunit::test::aliases_listed_in_the_constant) + + assert_same "$declared" "$listed" +} + +function test_the_alias_list_is_not_empty() { + assert_not_empty "$_BASHUNIT_DEPRECATED_ALIASES" +} + +# The warning must name the offending variable and point at the replacement, so +# the message is actionable without consulting the docs. +function test_warning_names_the_variable_and_its_replacement() { + local output + output=$(bashunit::env::warn_deprecated "the unprefixed \`VERBOSE\`" "\`BASHUNIT_VERBOSE\`" 2>&1) + + assert_contains "VERBOSE" "$output" + assert_contains "BASHUNIT_VERBOSE" "$output" + assert_contains "Deprecated" "$output" +} + +function test_warnings_can_be_silenced() { + local output + output=$(BASHUNIT_NO_DEPRECATION_WARNINGS=true \ + bashunit::env::warn_deprecated "anything" "anything else" 2>&1) + + assert_empty "$output" +} + +# stdout carries reports (TAP, JSON); a warning there would corrupt them. +function test_warning_goes_to_stderr_not_stdout() { + local on_stdout + on_stdout=$(bashunit::env::warn_deprecated "the unprefixed \`VERBOSE\`" "\`BASHUNIT_VERBOSE\`" 2>/dev/null) + + assert_empty "$on_stdout" +}