Files
claudio/test.sh

133 lines
5.7 KiB
Bash
Executable File

#!/usr/bin/env bash
# test.sh — end-to-end tests for claudio, run on the host with the real podman.
#
# Usage: ./test.sh build the image (cached if unchanged) and test
# ./test.sh --update also test `claudio update` (slow: full rebuild)
#
# It works in throwaway directories under /tmp and removes the containers and
# PACKAGES images it creates. The shared claudio-home volume is not touched.
set -uo pipefail
CLAUDIO="$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/claudio.sh"
proj="$(mktemp -d /tmp/claudio-test.XXXXXX)"
extra="$(mktemp -d /tmp/claudio-test-mount.XXXXXX)"
# Same formula as claudio.sh, to find this project's PERSIST container.
name="claudio-$(printf %s "${proj##*/}" | tr -c 'a-zA-Z0-9_.-' -)-$(printf %s "$proj" | sha256sum | cut -c1-8)"
pkg_images() { podman images -q --filter label=claudio.packages | sort -u; }
pkg_before="$(pkg_images)"
cleanup() {
(cd "$proj" && "$CLAUDIO" reset)
comm -13 <(echo "$pkg_before") <(pkg_images) | xargs -r podman rmi -f >/dev/null
rm -rf "$proj" "$extra"
}
trap cleanup EXIT
pass=0 fail=0
check() { # check "description" command...
if "${@:2}"; then echo " ok $1"; ((pass++))
else echo " FAIL $1"; ((fail++)); fi
}
not() { ! "$@"; }
cfg() { printf '%s\n' "$@" > "$proj/.claudio"; } # write the project's .claudio
claudio() { (cd "$proj" && "$CLAUDIO" "$@" </dev/null); }
cin() { claudio shell -c "$1"; } # run a command in the container
exits_with() { local want="$1"; shift; "$@"; [[ $? == "$want" ]]; }
wait_for() { for _ in {1..30}; do "$@" && return; sleep 1; done; return 1; }
running() { [[ "$(podman container inspect -f '{{.State.Running}}' "$name" 2>/dev/null)" == true ]]; }
image_of() { podman container inspect -f '{{.ImageName}}' "$name"; }
from_home() { (cd ~ && "$CLAUDIO" shell -c true </dev/null 2>/dev/null); }
owned_by_you() { cin 'touch owned' && [[ "$(stat -c %u "$proj/owned")" == "$(id -u)" ]]; }
pkg_image_labeled() { [[ -n "$(podman images -q --filter label=claudio.packages \
--filter reference='localhost/claudio:pkgs-*')" ]]; }
claude_runs() { claudio --version >/dev/null; }
bg_session_up() { podman exec "$name" pgrep -fx 'sleep 8' >/dev/null 2>&1; }
echo "== build"
"$CLAUDIO" build || { echo "build failed"; exit 1; }
echo "== basics"
cfg ''
check "refuses to run from \$HOME" not from_home
check "runs as uid 1000 in the project dir" cin "[[ \$(id -u) == 1000 && \$PWD == '$proj' ]]"
check "claude is installed" claude_runs
check "files in the project are yours" owned_by_you
check "exit status is passed through" exits_with 3 cin 'exit 3'
check "home is the claudio-home volume" cin 'mountpoint -q /home/claude'
echo "== MOUNTS"
cfg "MOUNTS=('$extra')"
check "bare path is mounted read-write" cin "touch '$extra/rw'"
cfg "MOUNTS=('$extra:/mnt/x:ro')"
check "':ro' mount is read-only" cin 'test -f /mnt/x/rw && ! touch /mnt/x/no 2>/dev/null'
echo "== ROOT"
cfg ''
check "no sudo without ROOT=1" cin '! sudo -n true 2>/dev/null'
cfg 'ROOT=1'
check "passwordless sudo with ROOT=1" cin '[[ $(sudo -n id -u) == 0 ]]'
check "root can change the system" cin 'sudo -n touch /opt/marker'
check "...but it is gone next run" cin '! test -e /opt/marker'
echo "== PACKAGES"
cfg 'PACKAGES=(hello)'
check "package is installed" cin 'hello >/dev/null'
check "image is labeled claudio.packages" pkg_image_labeled
cfg ''
check "not installed without PACKAGES" cin '! command -v hello >/dev/null'
echo "== PERSIST"
persist=('ROOT=1' 'PERSIST=1' 'PACKAGES=(hello)')
cfg "${persist[@]}"
check "system changes are kept" cin 'sudo -n touch /opt/marker'
check "...and seen by the next run" cin 'test -e /opt/marker'
check "PACKAGES and sudo work too" cin 'hello >/dev/null && sudo -n true'
check "stopped after the run" not running
check "exit status is passed through" exits_with 3 cin 'exit 3'
check "claudio (not shell) runs claude" claude_runs
cin 'sleep 8' & bg=$!
check "background session started" wait_for bg_session_up
cin true
check "still running while a session is" running
cfg "${persist[@]}" "MOUNTS=('$extra')" # must change the podman options, not just the file
check "refuses to recreate while in use" not cin true 2>/dev/null
cfg "${persist[@]}"
wait "$bg"
check "stopped after the last session" not running
cfg "${persist[@]}" "MOUNTS=('$extra')"
check "settings change recreates it" cin '! test -e /opt/marker'
claudio reset
check "reset removes the container" not podman container exists "$name"
cfg "${persist[@]}"
cin 'sudo -n touch /opt/marker'
cfg "${persist[@]/#PERSIST=1/PERSIST=0}"
check "PERSIST=0 ignores the container" cin '! test -e /opt/marker'
echo "== GUI / AUDIO (skipped if your session has no such socket)"
host_run="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}"
wl="${WAYLAND_DISPLAY:-wayland-0}"
if [[ -S "$host_run/$wl" ]]; then
cfg 'GUI=1'
check "Wayland socket is shared" cin "test -S /run/user/1000/$wl && [[ \$WAYLAND_DISPLAY == '$wl' ]]"
else echo " skip GUI (no $host_run/$wl)"; fi
if [[ -S "$host_run/pulse/native" ]]; then
cfg 'AUDIO=1'
check "PulseAudio socket is shared" cin 'test -S /run/user/1000/pulse/native'
else echo " skip AUDIO (no $host_run/pulse/native)"; fi
if [[ "${1:-}" == --update ]]; then
echo "== update"
cfg "${persist[@]}"
cin 'sudo -n touch /opt/marker'
old="$(image_of)"
"$CLAUDIO" update
check "update recreates the container" cin '! test -e /opt/marker && hello >/dev/null'
check "...on a new PACKAGES image" [ "$(image_of)" != "$old" ]
fi
echo "== $pass passed, $fail failed"
(( fail == 0 ))