Files
cozystack/hack/check-host-runtime.bats
Aleksei Sviridkin ceccac27b0 test(hack): clean up bats temp dirs without EXIT traps
A `trap ... EXIT` inside an `@test` replaces the one the bats binary
installs for its own bookkeeping, so a test that then fails prints no
TAP line at all. Not `not ok`, nothing: the run ends with a warning
that N of M tests executed, and anyone judging it by grepping for
`not ok` reads a green suite. Cleanup moves to the last line of each
body instead. Both runners set -e, so a failed test leaves its scratch
directory behind, which is what you want to look at anyway.

hack/promote-retag_test.bats was failing this way already, reporting
five passes and no failures while six of its eleven tests printed
nothing. Those six invoke the script for real, and it requires
sha256sum; they pin PATH to the stub directory plus /usr/bin and /bin,
which is where coreutils installs that binary and where macOS does not,
so the failure was of the platform rather than of the code and CI never
saw it. The stub directory already supplied yq and skopeo for the same
reason and now supplies sha256sum too, resolved from absolute
candidates because the stub itself is first on PATH.

Two file headers described the trap arrangement they no longer use and
are corrected rather than left to outlive it.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-08-05 16:03:57 +03:00

417 lines
14 KiB
Bash

#!/usr/bin/env bats
# -----------------------------------------------------------------------------
# Unit tests for hack/check-host-runtime.sh
#
# The script warns when a standalone containerd.service or docker.service is
# active alongside the embedded k3s runtime on Ubuntu hosts running the
# cozystack "generic" variant. Warnings go to stderr; exit code is always 0.
#
# Test strategy: each test builds its own temporary stub directory and prepends
# it to PATH to inject a fake `systemctl` (and optionally `du`) binary. The
# script itself honors a small set of COZYSTACK_PREFLIGHT_* environment
# variables to redirect socket/dir probes into the stub tree, so tests do not
# need root privileges or a real systemd host.
#
# Each test removes its stub dir on the last line of its body rather than from
# an EXIT trap: a trap inside an @test replaces the one the bats binary installs
# for its own bookkeeping, and a test that then fails prints no TAP line at all.
# Both runners set -e, so a failed test leaves its stub dir behind, which is what
# you want to look at anyway.
#
# Tests are otherwise self-contained — no shared setup/teardown helpers,
# because cozytest.sh's awk parser only recognizes @test blocks and treats a
# bare `}` on its own line as the end of a test function.
#
# Title syntax constraints (inherited from cozytest.sh's awk parser):
# - Titles must be delimited by ASCII double quotes; embedded literal
# double quotes are NOT escaped and will silently truncate the title.
# - Only alphanumeric characters from the title survive into the shell
# function name (everything else becomes '_'), so titles that differ
# only in punctuation collapse to the same function name. Keep titles
# distinctive in their alphanumeric run.
#
# Run with: hack/cozytest.sh hack/check-host-runtime.bats
# (or `bats hack/check-host-runtime.bats` if the bats binary is
# installed; cozytest.sh is the CI path.)
# -----------------------------------------------------------------------------
@test "clean host with no runtime services exits silently" {
STUB_DIR=$(mktemp -d)
cat >"$STUB_DIR/systemctl" <<'STUBEOF'
#!/bin/sh
if [ "$1" = "--version" ]; then
echo "systemd stub"
exit 0
fi
exit 1
STUBEOF
chmod +x "$STUB_DIR/systemctl"
STDERR_FILE="$STUB_DIR/stderr"
COZYSTACK_CONTAINERD_SOCKET="$STUB_DIR/missing-containerd.sock" \
COZYSTACK_DOCKER_SOCKET_PATHS="$STUB_DIR/missing-docker1.sock $STUB_DIR/missing-docker2.sock" \
COZYSTACK_CONTAINERD_DIR="$STUB_DIR/missing-containerd-dir" \
COZYSTACK_DOCKER_DIR="$STUB_DIR/missing-docker-dir" \
PATH="$STUB_DIR:$PATH" \
bash hack/check-host-runtime.sh >"$STUB_DIR/stdout" 2>"$STDERR_FILE"
[ ! -s "$STDERR_FILE" ]
[ ! -s "$STUB_DIR/stdout" ]
rm -rf "$STUB_DIR"
}
@test "standalone containerd service active prints warning" {
STUB_DIR=$(mktemp -d)
cat >"$STUB_DIR/systemctl" <<'STUBEOF'
#!/bin/sh
if [ "$1" = "--version" ]; then
echo "systemd stub"
exit 0
fi
if [ "$1" = "is-active" ] && [ "$2" = "containerd.service" ]; then
echo active
exit 0
fi
exit 1
STUBEOF
chmod +x "$STUB_DIR/systemctl"
mkdir -p "$STUB_DIR/var-lib-containerd"
echo dummy >"$STUB_DIR/var-lib-containerd/dummy"
STDERR_FILE="$STUB_DIR/stderr"
COZYSTACK_CONTAINERD_SOCKET="$STUB_DIR/missing-containerd.sock" \
COZYSTACK_DOCKER_SOCKET_PATHS="$STUB_DIR/missing-docker.sock" \
COZYSTACK_CONTAINERD_DIR="$STUB_DIR/var-lib-containerd" \
COZYSTACK_DOCKER_DIR="$STUB_DIR/missing-docker-dir" \
PATH="$STUB_DIR:$PATH" \
bash hack/check-host-runtime.sh 2>"$STDERR_FILE"
grep -q 'standalone containerd.service' "$STDERR_FILE"
if grep -q 'standalone docker.service' "$STDERR_FILE"; then
echo "unexpected docker warning found:" >&2
cat "$STDERR_FILE" >&2
exit 1
fi
# HINT line must name only the detected service, not advise disabling
# docker.service when only containerd.service is running. The sudo
# prefix is also required — without it the command silently no-ops
# for a non-root operator, so the prefix is part of the contract.
grep -q 'sudo systemctl disable --now containerd.service' "$STDERR_FILE"
if grep -q 'systemctl disable --now.*docker' "$STDERR_FILE"; then
echo "HINT unexpectedly mentions docker:" >&2
cat "$STDERR_FILE" >&2
exit 1
fi
rm -rf "$STUB_DIR"
}
@test "standalone docker service active prints warning" {
STUB_DIR=$(mktemp -d)
cat >"$STUB_DIR/systemctl" <<'STUBEOF'
#!/bin/sh
if [ "$1" = "--version" ]; then
echo "systemd stub"
exit 0
fi
if [ "$1" = "is-active" ] && [ "$2" = "docker.service" ]; then
echo active
exit 0
fi
exit 1
STUBEOF
chmod +x "$STUB_DIR/systemctl"
mkdir -p "$STUB_DIR/var-lib-docker"
echo dummy >"$STUB_DIR/var-lib-docker/dummy"
STDERR_FILE="$STUB_DIR/stderr"
COZYSTACK_CONTAINERD_SOCKET="$STUB_DIR/missing-containerd.sock" \
COZYSTACK_DOCKER_SOCKET_PATHS="$STUB_DIR/missing-docker.sock" \
COZYSTACK_CONTAINERD_DIR="$STUB_DIR/missing-containerd-dir" \
COZYSTACK_DOCKER_DIR="$STUB_DIR/var-lib-docker" \
PATH="$STUB_DIR:$PATH" \
bash hack/check-host-runtime.sh 2>"$STDERR_FILE"
grep -q 'standalone docker.service' "$STDERR_FILE"
if grep -q 'standalone containerd.service' "$STDERR_FILE"; then
echo "unexpected containerd warning found:" >&2
cat "$STDERR_FILE" >&2
exit 1
fi
# HINT line must name only the detected service. As in the
# containerd test, the sudo prefix is part of the contract.
grep -q 'sudo systemctl disable --now docker.service' "$STDERR_FILE"
if grep -q 'systemctl disable --now.*containerd' "$STDERR_FILE"; then
echo "HINT unexpectedly mentions containerd:" >&2
cat "$STDERR_FILE" >&2
exit 1
fi
rm -rf "$STUB_DIR"
}
@test "both services active prints two warnings and the HINT block" {
STUB_DIR=$(mktemp -d)
cat >"$STUB_DIR/systemctl" <<'STUBEOF'
#!/bin/sh
if [ "$1" = "--version" ]; then
echo "systemd stub"
exit 0
fi
if [ "$1" = "is-active" ]; then
case "$2" in
containerd.service|docker.service) echo active; exit 0 ;;
esac
fi
exit 1
STUBEOF
chmod +x "$STUB_DIR/systemctl"
mkdir -p "$STUB_DIR/var-lib-containerd" "$STUB_DIR/var-lib-docker"
STDERR_FILE="$STUB_DIR/stderr"
# Capture exit code explicitly: the script contract says exit 0
# unconditionally (warning, not blocker). `set -e` in the test
# function body would already fail on a nonzero exit, but an
# explicit status check locks in the contract and makes a
# regression show up as "expected 0, got N" rather than as a
# generic test failure.
status=0
COZYSTACK_CONTAINERD_SOCKET="$STUB_DIR/missing-containerd.sock" \
COZYSTACK_DOCKER_SOCKET_PATHS="$STUB_DIR/missing-docker.sock" \
COZYSTACK_CONTAINERD_DIR="$STUB_DIR/var-lib-containerd" \
COZYSTACK_DOCKER_DIR="$STUB_DIR/var-lib-docker" \
PATH="$STUB_DIR:$PATH" \
bash hack/check-host-runtime.sh 2>"$STDERR_FILE" || status=$?
[ "$status" -eq 0 ]
grep -q 'standalone containerd.service' "$STDERR_FILE"
grep -q 'standalone docker.service' "$STDERR_FILE"
# HINT block must fire whenever warnings exist; otherwise a future silent
# removal of the HINT would go unnoticed. When both services fire the HINT
# must list both in a single sudo systemctl disable invocation — the sudo
# prefix is as important as the systemctl verb, otherwise the operator
# would be told to run it as a non-root user and quietly fail.
grep -q 'HINT:' "$STDERR_FILE"
grep -q 'sudo systemctl disable --now containerd.service docker.service' "$STDERR_FILE"
rm -rf "$STUB_DIR"
}
@test "failing du does not suppress the containerd warning" {
STUB_DIR=$(mktemp -d)
cat >"$STUB_DIR/systemctl" <<'STUBEOF'
#!/bin/sh
if [ "$1" = "--version" ]; then
echo "systemd stub"
exit 0
fi
if [ "$1" = "is-active" ] && [ "$2" = "containerd.service" ]; then
echo active
exit 0
fi
exit 1
STUBEOF
chmod +x "$STUB_DIR/systemctl"
cat >"$STUB_DIR/du" <<'DUEOF'
#!/bin/sh
exit 1
DUEOF
chmod +x "$STUB_DIR/du"
mkdir -p "$STUB_DIR/var-lib-containerd"
STDERR_FILE="$STUB_DIR/stderr"
COZYSTACK_CONTAINERD_SOCKET="$STUB_DIR/missing-containerd.sock" \
COZYSTACK_DOCKER_SOCKET_PATHS="$STUB_DIR/missing-docker.sock" \
COZYSTACK_CONTAINERD_DIR="$STUB_DIR/var-lib-containerd" \
COZYSTACK_DOCKER_DIR="$STUB_DIR/missing-docker-dir" \
PATH="$STUB_DIR:$PATH" \
bash hack/check-host-runtime.sh 2>"$STDERR_FILE"
grep -q 'standalone containerd.service' "$STDERR_FILE"
rm -rf "$STUB_DIR"
}
@test "containerd socket fallback fires when systemctl is unavailable" {
STUB_DIR=$(mktemp -d)
# The script uses `[ -e "$sock" ]`, not `[ -S ... ]`, so a regular
# file is a valid stand-in for a unix socket in tests. This also
# removes any optional runtime dependency on python3 and makes the
# test unconditional on every CI runner.
SOCK="$STUB_DIR/containerd.sock"
touch "$SOCK"
STDERR_FILE="$STUB_DIR/stderr"
COZYSTACK_PREFLIGHT_FORCE_NO_SYSTEMCTL=1 \
COZYSTACK_CONTAINERD_SOCKET="$SOCK" \
COZYSTACK_DOCKER_SOCKET_PATHS="$STUB_DIR/missing-docker.sock" \
COZYSTACK_CONTAINERD_DIR="$STUB_DIR/missing-containerd-dir" \
COZYSTACK_DOCKER_DIR="$STUB_DIR/missing-docker-dir" \
bash hack/check-host-runtime.sh 2>"$STDERR_FILE"
grep -q 'standalone containerd.service' "$STDERR_FILE"
rm -rf "$STUB_DIR"
}
@test "docker socket fallback fires when systemctl is unavailable" {
STUB_DIR=$(mktemp -d)
SOCK="$STUB_DIR/docker.sock"
touch "$SOCK"
STDERR_FILE="$STUB_DIR/stderr"
COZYSTACK_PREFLIGHT_FORCE_NO_SYSTEMCTL=1 \
COZYSTACK_CONTAINERD_SOCKET="$STUB_DIR/missing-containerd.sock" \
COZYSTACK_DOCKER_SOCKET_PATHS="$SOCK" \
COZYSTACK_CONTAINERD_DIR="$STUB_DIR/missing-containerd-dir" \
COZYSTACK_DOCKER_DIR="$STUB_DIR/missing-docker-dir" \
bash hack/check-host-runtime.sh 2>"$STDERR_FILE"
grep -q 'standalone docker.service' "$STDERR_FILE"
if grep -q 'standalone containerd.service' "$STDERR_FILE"; then
echo "unexpected containerd warning found:" >&2
cat "$STDERR_FILE" >&2
exit 1
fi
rm -rf "$STUB_DIR"
}
@test "clean host without systemctl exits silently" {
STUB_DIR=$(mktemp -d)
STDERR_FILE="$STUB_DIR/stderr"
COZYSTACK_PREFLIGHT_FORCE_NO_SYSTEMCTL=1 \
COZYSTACK_CONTAINERD_SOCKET="$STUB_DIR/missing-containerd.sock" \
COZYSTACK_DOCKER_SOCKET_PATHS="$STUB_DIR/missing-docker1.sock $STUB_DIR/missing-docker2.sock" \
COZYSTACK_CONTAINERD_DIR="$STUB_DIR/missing-containerd-dir" \
COZYSTACK_DOCKER_DIR="$STUB_DIR/missing-docker-dir" \
bash hack/check-host-runtime.sh >"$STUB_DIR/stdout" 2>"$STDERR_FILE"
[ ! -s "$STDERR_FILE" ]
[ ! -s "$STUB_DIR/stdout" ]
rm -rf "$STUB_DIR"
}
@test "docker service plus socket still emits exactly one warning" {
STUB_DIR=$(mktemp -d)
cat >"$STUB_DIR/systemctl" <<'STUBEOF'
#!/bin/sh
if [ "$1" = "--version" ]; then
echo "systemd stub"
exit 0
fi
if [ "$1" = "is-active" ] && [ "$2" = "docker.service" ]; then
echo active
exit 0
fi
exit 1
STUBEOF
chmod +x "$STUB_DIR/systemctl"
SOCK="$STUB_DIR/docker.sock"
touch "$SOCK"
mkdir -p "$STUB_DIR/var-lib-docker"
STDERR_FILE="$STUB_DIR/stderr"
COZYSTACK_CONTAINERD_SOCKET="$STUB_DIR/missing-containerd.sock" \
COZYSTACK_DOCKER_SOCKET_PATHS="$SOCK" \
COZYSTACK_CONTAINERD_DIR="$STUB_DIR/missing-containerd-dir" \
COZYSTACK_DOCKER_DIR="$STUB_DIR/var-lib-docker" \
PATH="$STUB_DIR:$PATH" \
bash hack/check-host-runtime.sh 2>"$STDERR_FILE"
count=$(grep -c 'standalone docker.service' "$STDERR_FILE")
if [ "$count" != "1" ]; then
echo "expected exactly one docker warning, got $count" >&2
cat "$STDERR_FILE" >&2
exit 1
fi
rm -rf "$STUB_DIR"
}
@test "docker socket paths with glob chars do not expand" {
STUB_DIR=$(mktemp -d)
# Create two directories that a naive `for sock in $PATHS` loop
# would glob-expand and treat as existing "sockets". With the
# array-based parsing the literal path "$STUB_DIR/var-lib-*" does
# not exist and no warning must fire.
mkdir -p "$STUB_DIR/var-lib-docker" "$STUB_DIR/var-lib-containerd"
cat >"$STUB_DIR/systemctl" <<'STUBEOF'
#!/bin/sh
if [ "$1" = "--version" ]; then
echo "systemd stub"
exit 0
fi
exit 1
STUBEOF
chmod +x "$STUB_DIR/systemctl"
STDERR_FILE="$STUB_DIR/stderr"
COZYSTACK_CONTAINERD_SOCKET="$STUB_DIR/missing-containerd.sock" \
COZYSTACK_DOCKER_SOCKET_PATHS="$STUB_DIR/var-lib-*" \
COZYSTACK_CONTAINERD_DIR="$STUB_DIR/missing-containerd-dir" \
COZYSTACK_DOCKER_DIR="$STUB_DIR/missing-docker-dir" \
PATH="$STUB_DIR:$PATH" \
bash hack/check-host-runtime.sh 2>"$STDERR_FILE"
if grep -q 'standalone docker.service' "$STDERR_FILE"; then
echo "glob pattern expanded — docker warning should not fire:" >&2
cat "$STDERR_FILE" >&2
exit 1
fi
rm -rf "$STUB_DIR"
}
@test "containerd service plus socket still emits exactly one warning" {
STUB_DIR=$(mktemp -d)
cat >"$STUB_DIR/systemctl" <<'STUBEOF'
#!/bin/sh
if [ "$1" = "--version" ]; then
echo "systemd stub"
exit 0
fi
if [ "$1" = "is-active" ] && [ "$2" = "containerd.service" ]; then
echo active
exit 0
fi
exit 1
STUBEOF
chmod +x "$STUB_DIR/systemctl"
# The script uses `[ -e "$sock" ]`, not `[ -S ... ]`, so a regular
# file is a valid stand-in for a unix socket in tests. This also
# removes any optional runtime dependency on python3 and makes the
# test unconditional on every CI runner.
SOCK="$STUB_DIR/containerd.sock"
touch "$SOCK"
mkdir -p "$STUB_DIR/var-lib-containerd"
STDERR_FILE="$STUB_DIR/stderr"
COZYSTACK_CONTAINERD_SOCKET="$SOCK" \
COZYSTACK_DOCKER_SOCKET_PATHS="$STUB_DIR/missing-docker.sock" \
COZYSTACK_CONTAINERD_DIR="$STUB_DIR/var-lib-containerd" \
COZYSTACK_DOCKER_DIR="$STUB_DIR/missing-docker-dir" \
PATH="$STUB_DIR:$PATH" \
bash hack/check-host-runtime.sh 2>"$STDERR_FILE"
count=$(grep -c 'standalone containerd.service' "$STDERR_FILE")
if [ "$count" != "1" ]; then
echo "expected exactly one containerd warning, got $count" >&2
cat "$STDERR_FILE" >&2
exit 1
fi
rm -rf "$STUB_DIR"
}