183 lines
6.1 KiB
Bash
183 lines
6.1 KiB
Bash
#!/bin/sh
|
|
|
|
# In-container tests for tuxreaperd, the tux-dock micro init system.
|
|
#
|
|
# tuxreaperd is container-tailored: tux-dock mounts it into created containers
|
|
# where it runs as PID 1, acting as a subreaper (PR_SET_CHILD_SUBREAPER) with a
|
|
# SIGCHLD reaper loop so orphaned descendants never linger as zombies.
|
|
#
|
|
# These tests drive tuxreaperd the same way the app does (bind-mounted, PID 1)
|
|
# and verify zombie reaping, exit-status propagation, and signal forwarding.
|
|
|
|
set -eu
|
|
|
|
image=${TUX_DOCK_TEST_IMAGE:-debian:forky}
|
|
reaper_bin=${1:-build/tuxreaperd}
|
|
reaper_src=${2:-}
|
|
name="tux-reaper-test-$$-$(date +%s)"
|
|
cleanup_status=0
|
|
failures=0
|
|
|
|
run_raw() {
|
|
command_name=$1
|
|
shift
|
|
started=$(date +%s)
|
|
printf '%s\n' "[TEST OUTPUT BEGIN] $command_name"
|
|
set +e
|
|
"$@" 2>&1
|
|
command_status=$?
|
|
set -e
|
|
elapsed=$(( $(date +%s) - started ))
|
|
printf '%s\n' "[TEST OUTPUT END] $command_name"
|
|
printf '%s\n' "[TEST RESULT] command=$command_name status=$command_status elapsed=${elapsed}s"
|
|
return "$command_status"
|
|
}
|
|
|
|
cleanup() {
|
|
if docker container inspect "$name" >/dev/null 2>&1; then
|
|
printf '%s\n' "[TUI] Cleanup: Remove Container"
|
|
if ! docker rm -f "$name" >/dev/null 2>&1; then
|
|
cleanup_status=1
|
|
fi
|
|
printf '%s\n' "[TUI] Complete: Container removed"
|
|
fi
|
|
}
|
|
trap cleanup EXIT INT TERM
|
|
|
|
# Resolve an absolute path for the bind mount (docker requires absolute paths).
|
|
if [ ! -f "$reaper_bin" ]; then
|
|
if command -v gcc >/dev/null 2>&1 && [ -n "$reaper_src" ]; then
|
|
printf '%s\n' "[TUI] tuxreaperd binary not found, building $reaper_bin"
|
|
mkdir -p "$(dirname "$reaper_bin")"
|
|
gcc -O2 -static -Wall -Wextra "$reaper_src" -o "$reaper_bin"
|
|
else
|
|
printf '%s\n' "tuxreaperd binary not found at $reaper_bin (run ./compile.sh)" >&2
|
|
exit 1
|
|
fi
|
|
fi
|
|
reaper_dir=$(CDPATH= cd "$(dirname "$reaper_bin")" && pwd)
|
|
reaper_bin=$reaper_dir/$(basename "$reaper_bin")
|
|
|
|
if ! docker image inspect "$image" >/dev/null 2>&1; then
|
|
printf '%s\n' "[TUI] Pull Docker Image: $image"
|
|
run_raw "docker pull $image" docker pull "$image"
|
|
else
|
|
printf '%s\n' "[TUI] Image available: $image"
|
|
fi
|
|
|
|
printf '%s\n' "[TUI] Create container with tuxreaperd as PID 1"
|
|
run_raw "docker create tuxreaperd container" \
|
|
docker create --name "$name" -v "$reaper_bin:/usr/local/bin/tuxreaperd:ro" \
|
|
"$image" /usr/local/bin/tuxreaperd sleep infinity
|
|
run_raw "docker start $name" docker start "$name"
|
|
|
|
printf '%s\n' "[TUI] Verify tuxreaperd is PID 1"
|
|
if [ "$(docker exec "$name" cat /proc/1/comm 2>/dev/null)" = "tuxreaperd" ]; then
|
|
printf '%s\n' "[PASS] tuxreaperd is PID 1 in the container"
|
|
else
|
|
printf '%s\n' "[FAIL] PID 1 is not tuxreaperd"
|
|
failures=$((failures + 1))
|
|
fi
|
|
|
|
printf '%s\n' "[TUI] Spawn orphaned grandchildren inside the container"
|
|
# Each inner shell backgrounds a short-lived sleep and exits; the orphaned
|
|
# sleep reparents to PID 1 (tuxreaperd, the subreaper) and becomes a zombie on
|
|
# exit unless the SIGCHLD reaper loop reaps it.
|
|
docker exec "$name" sh -c '
|
|
i=0
|
|
while [ "$i" -lt 20 ]; do
|
|
sh -c "sleep 0.1 &"
|
|
i=$((i + 1))
|
|
done
|
|
sleep 2
|
|
'
|
|
printf '%s\n' "[TUI] Count zombie processes under PID 1"
|
|
zombie_count() {
|
|
docker exec "$name" sh -c '
|
|
zombies=0
|
|
for f in /proc/[0-9]*/stat; do
|
|
[ -r "$f" ] || continue
|
|
line=
|
|
IFS= read -r line < "$f" || continue
|
|
rest=${line##*) }
|
|
set -- $rest
|
|
[ "$1" = Z ] && zombies=$((zombies + 1))
|
|
done
|
|
printf "%s\n" "$zombies"
|
|
'
|
|
}
|
|
zombies=999
|
|
attempt=0
|
|
while [ "$attempt" -lt 10 ]; do
|
|
zombies=$(zombie_count)
|
|
[ "$zombies" -eq 0 ] && break
|
|
attempt=$((attempt + 1))
|
|
sleep 1
|
|
done
|
|
if [ "$zombies" -eq 0 ]; then
|
|
printf '%s\n' "[PASS] No zombie processes remain under tuxreaperd"
|
|
else
|
|
printf '%s\n' "[FAIL] $zombies zombie process(es) not reaped"
|
|
failures=$((failures + 1))
|
|
fi
|
|
|
|
printf '%s\n' "[TUI] Verify exit-status propagation from the reaped child"
|
|
status=0
|
|
run_raw "docker run tuxreaperd exit-status" \
|
|
docker run --rm -v "$reaper_bin:/usr/local/bin/tuxreaperd:ro" \
|
|
"$image" /usr/local/bin/tuxreaperd sh -c 'exit 42' || status=$?
|
|
if [ "$status" -eq 42 ]; then
|
|
printf '%s\n' "[PASS] Exit status 42 propagated from the child workload"
|
|
else
|
|
printf '%s\n' "[FAIL] Expected exit status 42, got $status"
|
|
failures=$((failures + 1))
|
|
fi
|
|
|
|
printf '%s\n' "[TUI] Verify SIGTERM forwarding through docker stop"
|
|
if docker stop "$name" >/dev/null 2>&1; then
|
|
code=$(docker wait "$name")
|
|
if [ "$code" = "143" ]; then
|
|
printf '%s\n' "[PASS] SIGTERM forwarded to workload, clean shutdown (exit 143)"
|
|
else
|
|
printf '%s\n' "[FAIL] Expected container exit 143, got $code"
|
|
failures=$((failures + 1))
|
|
fi
|
|
else
|
|
printf '%s\n' "[FAIL] docker stop failed (did tuxreaperd fail to forward SIGTERM?)"
|
|
failures=$((failures + 1))
|
|
fi
|
|
|
|
printf '%s\n' "[TUI] Verify tuxreaperd waits for descendants after main child exits"
|
|
descendant_start=$(date +%s)
|
|
run_raw "docker run tuxreaperd descendant-wait" \
|
|
docker run --rm -v "$reaper_bin:/usr/local/bin/tuxreaperd:ro" \
|
|
"$image" /usr/local/bin/tuxreaperd sh -c 'sleep 3 & exit 0' || true
|
|
descendant_elapsed=$(( $(date +%s) - descendant_start ))
|
|
if [ "$descendant_elapsed" -ge 3 ] && [ "$descendant_elapsed" -le 10 ]; then
|
|
printf '%s\n' "[PASS] tuxreaperd waited for descendant to finish (elapsed ${descendant_elapsed}s)"
|
|
else
|
|
printf '%s\n' "[FAIL] Expected ~3s wait, got ${descendant_elapsed}s"
|
|
failures=$((failures + 1))
|
|
fi
|
|
|
|
trap - EXIT INT TERM
|
|
if [ "$failures" -gt 0 ]; then
|
|
printf '%s\n' "tuxreaperd tests failed: $failures failure(s)" >&2
|
|
exit 1
|
|
fi
|
|
|
|
printf '%s\n' "[TUI] Remove Container: $name"
|
|
run_raw "docker rm -f $name" docker rm -f "$name"
|
|
printf '%s\n' "[TUI] Complete: Container removed"
|
|
|
|
printf '%s\n' "[TUI] Verify removal: docker container inspect $name"
|
|
if run_raw "docker container inspect $name" docker container inspect "$name"; then
|
|
printf '%s\n' "Container still exists after removal" >&2
|
|
exit 1
|
|
else
|
|
printf '%s\n' "[TUI] Complete: Removal verified"
|
|
fi
|
|
|
|
|
|
printf '%s\n' "tuxreaperd tests passed"
|
|
exit "$cleanup_status"
|