diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 474f404..d5f9c1b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -119,8 +119,8 @@ jobs: image: alpine:latest steps: - - name: Install bash, zsh and dash - run: apk add --no-cache bash zsh dash + - name: Install bash, zsh, dash and shellcheck + run: apk add --no-cache bash zsh dash shellcheck - name: Checkout code uses: actions/checkout@v7 @@ -137,7 +137,8 @@ jobs: adduser -D tester chown -R tester "$GITHUB_WORKSPACE" su tester -s /bin/sh -c 'cd tests && umask 022 && - TEST_PARALLEL_JOBS=4 TEST_SHELLS="bash,bash --posix,zsh,dash,busybox sh" \ + TEST_MAX_SKIPPED=10 TEST_PARALLEL_JOBS=4 \ + TEST_SHELLS="bash,bash --posix,zsh,dash,busybox sh" \ TEST_REQUIRE_SHELLS="zsh dash busybox" bash ./run_tests.sh' # Runs consumer-style scripts against the last release of the previous major version and diff --git a/tests/run_tests.sh b/tests/run_tests.sh index 9e216bd..58a94db 100755 --- a/tests/run_tests.sh +++ b/tests/run_tests.sh @@ -594,6 +594,19 @@ main() { print_summary local exit_status=$? + # Optional skip-count guard (TEST_MAX_SKIPPED): when set, a run that skips more + # tests than the threshold fails, so a future environment change cannot silently + # reduce coverage again (Issue #152). + if [[ -n "${TEST_MAX_SKIPPED:-}" ]]; then + if ! [[ "$TEST_MAX_SKIPPED" =~ ^[0-9]+$ ]]; then + echo -e "${COLOR_RED}Error: TEST_MAX_SKIPPED must be a non-negative integer (got '$TEST_MAX_SKIPPED')${COLOR_RESET}" + exit_status=1 + elif [[ "$SKIPPED_TESTS" -gt "$TEST_MAX_SKIPPED" ]]; then + echo -e "${COLOR_RED}Error: $SKIPPED_TESTS tests skipped, exceeding TEST_MAX_SKIPPED=$TEST_MAX_SKIPPED${COLOR_RESET}" + exit_status=1 + fi + fi + # Generate JUnit XML if requested if [[ "$JUNIT_OUTPUT" == "true" ]]; then generate_junit_xml "$OUTPUT_DIR/junit.xml" diff --git a/tests/test_helpers.sh b/tests/test_helpers.sh index 3f33249..cbd4f1a 100755 --- a/tests/test_helpers.sh +++ b/tests/test_helpers.sh @@ -203,6 +203,36 @@ skip_test() { teardown_test 0 } +# Return 0 when the system runs musl, where C.UTF-8 works without any installed locale +# files (musl's default since 1.1.11, 2015). musl ships no `locale` utility, so +# `locale -a` lists nothing even though C.UTF-8 (and every other locale name) is +# available. Everything else, including glibc, returns 1 and keeps the `locale -a` +# based detection. +musl_c_utf8() { + command -v ldd >/dev/null 2>&1 || return 1 + local ldd_banner + # musl's ldd prints the version banner (and exits nonzero) on stderr; glibc's prints on + # stdout. musl's ldd also exits 1, and run_tests.sh runs with pipefail, so the banner is + # captured first and the probe's status comes from grep alone. + ldd_banner="$(ldd --version 2>&1 || true)" + printf '%s\n' "$ldd_banner" | grep -qi musl +} + +# Print an installed UTF-8 locale name, or nothing. On musl there is no `locale` +# utility and no locale files, but C.UTF-8 still works (musl_c_utf8 above). +utf8_locale_name() { + local candidate + for candidate in C.UTF-8 C.utf8 en_US.UTF-8 en_US.utf8; do + if locale -a 2>/dev/null | grep -qx "$candidate"; then + printf '%s\n' "$candidate" + return 0 + fi + done + if musl_c_utf8; then + printf '%s\n' "C.UTF-8" + fi +} + # Assertion functions # shellcheck disable=SC2016 diff --git a/tests/test_posix_compat.sh b/tests/test_posix_compat.sh index 080b5b5..83c9e99 100755 --- a/tests/test_posix_compat.sh +++ b/tests/test_posix_compat.sh @@ -55,17 +55,6 @@ run_snippet_in_shell() { return "$status" } -# Print an installed UTF-8 locale name, or nothing -utf8_locale_name() { - local candidate - for candidate in C.UTF-8 C.utf8 en_US.UTF-8 en_US.utf8; do - if locale -a 2>/dev/null | grep -qx "$candidate"; then - printf '%s\n' "$candidate" - return 0 - fi - done -} - test_posix_caller_detection() { local shell_command="$1" start_test "[$shell_command] script name is the calling script when sourced with ." @@ -840,7 +829,11 @@ test_posix_levels_in_turkish_locale() { require_shell "${shell_command%% *}" || return 0 if ! locale -a 2>/dev/null | grep -qiE '^tr_TR\.utf-?8$'; then - skip_test "tr_TR.UTF-8 locale not installed" + if musl_c_utf8; then + skip_test "musl has no locale-specific case mapping: the LC_ALL=C guard cannot be tested" + else + skip_test "tr_TR.UTF-8 locale not installed" + fi return 0 fi diff --git a/tests/test_shell_compat.sh b/tests/test_shell_compat.sh index 6b02c23..33cca7e 100755 --- a/tests/test_shell_compat.sh +++ b/tests/test_shell_compat.sh @@ -54,6 +54,13 @@ compat_locale_available() { if [[ "$normalized_wanted" == "c" || "$normalized_wanted" == "posix" ]]; then return 0 fi + # The C.UTF-8 case goes through the shared helper (test_helpers.sh), which reports + # it on musl even though `locale -a` prints nothing there. Other names stay + # unavailable: musl only speaks UTF-8. + if [[ "$normalized_wanted" == "c.utf8" ]] \ + && [[ "$(utf8_locale_name)" == "C.UTF-8" ]]; then + return 0 + fi locale -a 2>/dev/null | tr '[:upper:]' '[:lower:]' | tr -d '-' \ | grep -qx "$normalized_wanted" } diff --git a/tests/test_validator_equivalence.sh b/tests/test_validator_equivalence.sh index 3daca16..697e4c5 100644 --- a/tests/test_validator_equivalence.sh +++ b/tests/test_validator_equivalence.sh @@ -20,16 +20,11 @@ EQUIVALENCE_GOLDEN_DIR="$EQUIVALENCE_DIR/golden" EQUIVALENCE_DRIVER="$EQUIVALENCE_DIR/validator_driver.sh" # Print the name of an installed UTF-8 locale, or nothing. -# The UTF-8 golden file records glibc's character classes, so it is only checked on Linux. +# The UTF-8 golden file records glibc's character classes, so it is only checked on +# Linux; the shared utf8_locale_name (test_helpers.sh) adds the musl C.UTF-8 fallback. equivalence_utf8_locale() { [[ "$(uname -s)" == "Linux" ]] || return 0 - local candidate - for candidate in C.UTF-8 C.utf8 en_US.UTF-8 en_US.utf8; do - if locale -a 2>/dev/null | grep -qx "$candidate"; then - printf '%s\n' "$candidate" - return 0 - fi - done + utf8_locale_name } # Print the check names used in a golden file, space separated