From 69473de54a73e525433b2201bb9f082e2616b8a9 Mon Sep 17 00:00:00 2001 From: Suzu Date: Sat, 5 Sep 2026 03:57:04 +0800 Subject: [PATCH 1/3] Avoid SIGPIPE in output pattern checks run_check and run_pipe can reject matching output when grep exits before printf finishes writing. Under pipefail, the resulting SIGPIPE makes a successful command appear to have missed its expected pattern. Pass captured output through a here-string. Cover early and late matches, missing patterns, and nonzero command exits in make check. --- mk/tests.mk | 8 ++++-- tests/lib/test-runner.sh | 4 +-- tests/test-runner.sh | 57 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 65 insertions(+), 4 deletions(-) create mode 100755 tests/test-runner.sh diff --git a/mk/tests.mk b/mk/tests.mk index 029d7561..12c2251d 100644 --- a/mk/tests.mk +++ b/mk/tests.mk @@ -22,7 +22,7 @@ ELFUSE_HOST_NOFILE_MIN ?= $(shell bash "$(CURDIR)/tests/test-config.sh" --host-n test-sysroot-create-paths test-fork-ipc-protocol-host \ test-vcpu-run-hooks-host test-identity-override-host \ test-dynamic-array-host test-string-builder-host test-gdbstub-host \ - test-config \ + test-config test-runner \ test-mremap-tail-emfile \ test-proctitle-host test-proctitle-low-stack \ test-sysroot-procfs-exec test-sysroot-fd-magiclink \ @@ -66,6 +66,10 @@ test-hello: $(ELFUSE_BIN) $(TEST_HELLO_DEP) test-config: @bash tests/test-config-cli.sh +## Verify output matching and exit status checks in the shared shell runner +test-runner: + @bash tests/test-runner.sh + ## Run the libc-based file-backed region removal EMFILE regression probe test-mremap-tail-emfile: $(ELFUSE_BIN) $(BUILD_DIR)/test-mremap-tail-emfile @printf "$(BLUE)▸ Running$(RESET) test-mremap-tail-emfile\n" @@ -283,7 +287,7 @@ check-sanitizer: $(ELFUSE_BIN) $(TEST_DEPS) $(CHECK_HOST_UNIT_BINS) $(CHECK_SHARED_LANES) ## Run the unit test suite plus busybox applet validation -check: $(ELFUSE_BIN) $(TEST_DEPS) check-syscall-coverage check-eintr-contract check-lock-order check-atomics check-ascii check-svc-tails check-skill-refs test-config \ +check: $(ELFUSE_BIN) $(TEST_DEPS) check-syscall-coverage check-eintr-contract check-lock-order check-atomics check-ascii check-svc-tails check-skill-refs test-config test-runner \ $(CHECK_HOST_UNIT_BINS) @bash tests/driver.sh -e $(ELFUSE_BIN) -d $(TEST_DIR) -v $(CHECK_SHARED_LANES) diff --git a/tests/lib/test-runner.sh b/tests/lib/test-runner.sh index 502eb016..4568f1d2 100644 --- a/tests/lib/test-runner.sh +++ b/tests/lib/test-runner.sh @@ -264,7 +264,7 @@ run_check() test_report fail "$tool" " (exit rc=$rc)" test_excerpt "$output" fail=$((fail + 1)) - elif printf "%s\n" "$output" | grep -qE "$pattern"; then + elif grep -qE "$pattern" <<< "$output"; then test_report ok "$tool" pass=$((pass + 1)) else @@ -324,7 +324,7 @@ run_pipe() test_report fail "$tool" " (exit rc=$rc)" test_excerpt "$output" fail=$((fail + 1)) - elif printf "%s\n" "$output" | grep -qE "$pattern"; then + elif grep -qE "$pattern" <<< "$output"; then test_report ok "$tool" pass=$((pass + 1)) else diff --git a/tests/test-runner.sh b/tests/test-runner.sh new file mode 100755 index 00000000..91faffed --- /dev/null +++ b/tests/test-runner.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash + +# Copyright 2026 elfuse contributors +# SPDX-License-Identifier: Apache-2.0 + +set -euo pipefail + +if [ "${1:-}" = "--emit" ]; then + awk 'BEGIN { + print "match-begin" + for (i = 0; i < 30000; i++) + print "abcdefghijklmnopqrstuvwxyz" + print "match-end" + }' + exit "$2" +fi + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +export TEST_SAMPLE_TIMEOUTS=0 +TEST_RUNNER=("$BASH") +BIN="$SCRIPT_DIR" + +# shellcheck source=tests/lib/test-runner.sh +. "$SCRIPT_DIR/lib/test-runner.sh" + +checks=0 +failures=0 + +expect_result() +{ + local runner="$1" label="$2" pattern="$3" rc="$4" want="$5" + local output + checks=$((checks + 1)) + if output=$( + if [ "$runner" = run_pipe ]; then + run_pipe test-runner.sh "$pattern" input --emit "$rc" + else + run_check test-runner.sh "$pattern" --emit "$rc" + fi + [ "$pass" -eq "$want" ] && [ "$fail" -eq "$((1 - want))" ] + ); then + printf 'PASS: %s %s\n' "$runner" "$label" + else + printf 'FAIL: %s %s\n%s\n' "$runner" "$label" "$output" >&2 + failures=$((failures + 1)) + fi +} + +for runner in run_check run_pipe; do + expect_result "$runner" early-match '^match-begin$' 0 1 + expect_result "$runner" late-match '^match-end$' 0 1 + expect_result "$runner" missing-pattern '^absent$' 0 0 + expect_result "$runner" failed-command '^match-begin$' 7 0 +done + +printf 'test-runner: %s checks, %s failures\n' "$checks" "$failures" +[ "$failures" -eq 0 ] From d1275e0f7066176de7233fac6d33cc9753e737d6 Mon Sep 17 00:00:00 2001 From: Suzu Date: Sat, 5 Sep 2026 08:07:47 +0800 Subject: [PATCH 2/3] Document the runner test's required conditions The output must exceed pipe capacity for early grep exit to expose SIGPIPE. Each assertion also relies on a command-substitution subshell inheriting zero pass/fail counts without updating the parent shell. --- tests/test-runner.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test-runner.sh b/tests/test-runner.sh index 91faffed..ca608471 100755 --- a/tests/test-runner.sh +++ b/tests/test-runner.sh @@ -8,6 +8,7 @@ set -euo pipefail if [ "${1:-}" = "--emit" ]; then awk 'BEGIN { print "match-begin" + # Keep output larger than pipe capacity to expose early-match SIGPIPE. for (i = 0; i < 30000; i++) print "abcdefghijklmnopqrstuvwxyz" print "match-end" @@ -31,6 +32,7 @@ expect_result() local runner="$1" label="$2" pattern="$3" rc="$4" want="$5" local output checks=$((checks + 1)) + # Each subshell inherits zero pass and fail counts and discards updates. if output=$( if [ "$runner" = run_pipe ]; then run_pipe test-runner.sh "$pattern" input --emit "$rc" From 6a8518633e8fa52424469d03ecbdeb8b3ca59208 Mon Sep 17 00:00:00 2001 From: Suzu Date: Sat, 5 Sep 2026 08:07:53 +0800 Subject: [PATCH 3/3] Avoid SIGPIPE in matrix and static output checks Six captured-output checks still use echo piped to grep -q. An early match can leave echo writing to a closed pipe and report a false failure under pipefail. Use here-strings while retaining diff's expected exit status of 1. Extracted before/after paths reproduce all six early-match failures and confirm the fixes on Bash 3.2 and 5.2. Late matches, missing patterns, timeouts, and the matrix and diff exit-status checks retain their expected results. The shared runner tests and make check-format pass. --- tests/test-matrix.sh | 4 ++-- tests/test-static-bins.sh | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/test-matrix.sh b/tests/test-matrix.sh index 2bc99566..4e62777d 100755 --- a/tests/test-matrix.sh +++ b/tests/test-matrix.sh @@ -562,7 +562,7 @@ test_check() test_report fail "$label" " (exit $rc)" test_excerpt "$output" fail=$((fail + 1)) - elif echo "$output" | grep -qE "$pattern"; then + elif grep -qE "$pattern" <<< "$output"; then test_report ok "$label" pass=$((pass + 1)) else @@ -630,7 +630,7 @@ test_pipe() test_report fail "$label" " (exit $rc)" test_excerpt "$output" fail=$((fail + 1)) - elif echo "$output" | grep -qE "$pattern"; then + elif grep -qE "$pattern" <<< "$output"; then test_report ok "$label" pass=$((pass + 1)) else diff --git a/tests/test-static-bins.sh b/tests/test-static-bins.sh index eae786ff..443baaa1 100755 --- a/tests/test-static-bins.sh +++ b/tests/test-static-bins.sh @@ -90,7 +90,7 @@ srun_check() if [ "$rc" -eq 124 ]; then test_report fail "$label" " (timeout)" fail=$((fail + 1)) - elif echo "$output" | grep -qE "$pattern"; then + elif grep -qE "$pattern" <<< "$output"; then test_report ok "$label" pass=$((pass + 1)) else @@ -124,7 +124,7 @@ srun_pipe() if [ "$rc" -eq 124 ]; then test_report fail "$label" " (timeout)" fail=$((fail + 1)) - elif echo "$output" | grep -qE "$pattern"; then + elif grep -qE "$pattern" <<< "$output"; then test_report ok "$label" pass=$((pass + 1)) else @@ -158,7 +158,7 @@ srun_script() if [ "$rc" -eq 124 ]; then test_report fail "$label" " (timeout)" fail=$((fail + 1)) - elif echo "$output" | grep -qE "$pattern"; then + elif grep -qE "$pattern" <<< "$output"; then test_report ok "$label" pass=$((pass + 1)) else @@ -355,7 +355,7 @@ if [ -n "$DIFF_BIN" ]; then if [ "$rc" -eq 124 ]; then test_report fail "$label" " (timeout)" fail=$((fail + 1)) - elif [ "$rc" = "1" ] && echo "$output" | grep -qE "^[<>]"; then + elif [ "$rc" = "1" ] && grep -qE "^[<>]" <<< "$output"; then test_report ok "$label" pass=$((pass + 1)) else