[test] Test utils for verifying interrupt handling
New sh_wrappers:
'send_sigint' and 'expect_interrupt' work together by sending
an interrupt to a wrapped command, and verifying that the interrupt
status (exit code 130) propagates where expected.
Some inner command will signal when is waiting for interrupt via a
wait-file, at which point send_sigint will deliver the interrupt.
'sleepy_cat.sh' is an example that signals with a wait-file.
In the follow-up change, fakebuild will use this mechanism so that
we can verify end-to-end signal propagation through layers of wrapper
scripts.
Bug: 390427892
Change-Id: I53dcc162754f8d8dbad7c602d346bb00ed022ea8
Reviewed-on: https://fuchsia-review.googlesource.com/c/rsclient/+/1391953
Commit-Queue: David Fang <fangism@google.com>
Reviewed-by: Jay Zhuang <jayzhuang@google.com>
diff --git a/tools/bazel/sh_wrapper/test_lib/BUILD.bazel b/tools/bazel/sh_wrapper/test_lib/BUILD.bazel
index d4b41be..65f2d0e 100644
--- a/tools/bazel/sh_wrapper/test_lib/BUILD.bazel
+++ b/tools/bazel/sh_wrapper/test_lib/BUILD.bazel
@@ -84,6 +84,47 @@
position = "nonterminal",
)
+# This wrapper should be placed at the beginning of a test chain to verify
+# that the wrapped command exits with code 130 (interrupt).
+# This is typically used in conjunction with the `send_sigint_wrapper`.
+sh_wrapper(
+ name = "expect_interrupt",
+ args = [
+ "--expected_exit_code",
+ "130",
+ "--",
+ ],
+ executable = ":check_exit_code",
+ position = "nonterminal",
+)
+
+sh_binary(
+ name = "sleepy_cat",
+ srcs = ["sleepy_cat.sh"],
+)
+
+sh_wrapper(
+ name = "sleepy_cat_cmd",
+ executable = ":sleepy_cat",
+ position = "terminal",
+)
+
+sh_binary(
+ name = "send_sigint",
+ srcs = ["send_sigint.sh"],
+)
+
+# This wrapper sends a SIGINT to its child process, and is useful for testing
+# signal handling and graceful shutdown. It should be placed before the command
+# that is intended to receive the signal.
+# This is intended to be used with the `expect_interrupt` wrapper to verify
+# the exit code.
+sh_wrapper(
+ name = "send_sigint_wrapper",
+ executable = ":send_sigint",
+ position = "nonterminal",
+)
+
sh_binary(
name = "set_glog_v",
srcs = ["set_glog_v.sh"],
diff --git a/tools/bazel/sh_wrapper/test_lib/check_exit_code.sh b/tools/bazel/sh_wrapper/test_lib/check_exit_code.sh
index 9059f27..6c0c5bd 100755
--- a/tools/bazel/sh_wrapper/test_lib/check_exit_code.sh
+++ b/tools/bazel/sh_wrapper/test_lib/check_exit_code.sh
@@ -15,7 +15,8 @@
# This script wraps a command and verifies that it exits with an expected
-# code.
+# code. This is useful for expect-fail test cases, and verifying exit status
+# propagation through layers of wrappers.
set -euo pipefail
@@ -26,7 +27,8 @@
cat <<EOF
Usage: $0 [options] -- <command>
-Wraps a command and checks its exit code.
+Wraps a command and checks its exit code against an expected value.
+This converts expect-fail commands into successes for testing.
Options:
-h, --help Show this help message.
diff --git a/tools/bazel/sh_wrapper/test_lib/send_sigint.sh b/tools/bazel/sh_wrapper/test_lib/send_sigint.sh
new file mode 100755
index 0000000..f8c2b4a
--- /dev/null
+++ b/tools/bazel/sh_wrapper/test_lib/send_sigint.sh
@@ -0,0 +1,82 @@
+#!/bin/bash
+# Copyright 2025 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# This script sends a SIGINT to a wrapped command for testing signal
+# propagation and graceful shutdown.
+#
+# It is intended to be used as a non-terminal wrapper in a test chain,
+# and should be paired with the `expect_interrupt` wrapper to verify that
+# the command exits with the expected interrupt code (130).
+
+set -euo pipefail
+
+readonly SCRIPT_NAME=$(basename "$0")
+
+log() {
+ echo "[${SCRIPT_NAME}] $@" >&2
+}
+
+# Trap exit to see what code the script is exiting with.
+trap 'log "Exiting with status $?"' EXIT
+
+if [[ $# -eq 0 ]]; then
+ log "Usage: $0 <command> [args...]"
+ exit 1
+fi
+
+# Define a standard path for the wait file and export it for the child process.
+readonly WAIT_FILE="${TEST_TMPDIR}/sh_wrapper.wait"
+export SH_WRAPPER_TEST_WAIT_FILE="${WAIT_FILE}"
+log "Wait file is at: ${WAIT_FILE}"
+
+# Run the wrapped command in the background, in its own process group.
+log "Starting command in background: $@"
+set -m
+"$@" &
+readonly CMD_PID=$!
+set +m
+log "Command running with PID: ${CMD_PID}"
+
+# Poll for the wait file to appear.
+log "Polling for wait file..."
+for ((i=0; i<20; i++)); do
+ if [[ -f "${WAIT_FILE}" ]]; then
+ break
+ fi
+ sleep 0.1
+done
+
+if ! [[ -f "${WAIT_FILE}" ]]; then
+ log "Error: Timed out waiting for wrapped command to be ready."
+ log "Waiting for file: ${WAIT_FILE}"
+ # Send a SIGKILL to the entire process group.
+ kill -9 -"${CMD_PID}" || true
+ exit 1
+fi
+log "Wait file found."
+
+# Send the interrupt signal to the entire process group.
+log "Sending SIGINT to process group ${CMD_PID}"
+kill -INT -"${CMD_PID}"
+
+# Wait for the command to terminate and capture its exit code.
+log "Waiting for command to terminate..."
+exit_code=0
+wait "${CMD_PID}" || exit_code=$?
+log "Command terminated with exit code: ${exit_code}"
+
+# Propagate the exit code of the wrapped command.
+log "Propagating exit code ${exit_code}"
+exit ${exit_code}
diff --git a/tools/bazel/sh_wrapper/test_lib/sleepy_cat.sh b/tools/bazel/sh_wrapper/test_lib/sleepy_cat.sh
new file mode 100755
index 0000000..6180387
--- /dev/null
+++ b/tools/bazel/sh_wrapper/test_lib/sleepy_cat.sh
@@ -0,0 +1,54 @@
+#!/bin/bash
+# Copyright 2025 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# This script touches a wait file, sleeps for a specified duration, and then
+# executes 'cat' to wait indefinitely for a signal.
+
+set -euo pipefail
+
+# --- Argument and Environment Variable Parsing ---
+WAIT_FILE="${SH_WRAPPER_TEST_WAIT_FILE:-}"
+DELAY="${SLEEPY_CAT_DELAY:-0.1}"
+
+while [[ $# -gt 0 ]]; do
+ case "$1" in
+ --wait_file)
+ WAIT_FILE="$2"
+ shift 2
+ ;;
+ --delay)
+ DELAY="$2"
+ shift 2
+ ;;
+ *)
+ echo "Unknown option: $1" >&2
+ exit 1
+ ;;
+ esac
+done
+
+if [[ -z "${WAIT_FILE}" ]]; then
+ echo "Error: --wait_file or SH_WRAPPER_TEST_WAIT_FILE must be set." >&2
+ exit 1
+fi
+
+# Signal readiness by touching the wait file.
+touch "${WAIT_FILE}"
+
+# Sleep for the specified duration.
+sleep "${DELAY}"
+
+# Replace this script with 'cat' to wait for a signal.
+exec cat
diff --git a/tools/bazel/sh_wrapper/tests/BUILD.bazel b/tools/bazel/sh_wrapper/tests/BUILD.bazel
index b8adf31..9093232 100644
--- a/tools/bazel/sh_wrapper/tests/BUILD.bazel
+++ b/tools/bazel/sh_wrapper/tests/BUILD.bazel
@@ -68,6 +68,16 @@
],
)
+# A test to verify that the expect_interrupt wrapper correctly handles a SIGINT.
+sh_wrapper_test(
+ name = "expect_interrupt_test",
+ chain = [
+ "//tools/bazel/sh_wrapper/test_lib:expect_interrupt",
+ "//tools/bazel/sh_wrapper/test_lib:send_sigint_wrapper",
+ "//tools/bazel/sh_wrapper/test_lib:sleepy_cat_cmd",
+ ],
+)
+
# The following commented-out tests should work, in principle,
# but trigger an error complaining about duplicates.
# TODO: resolve this later