#!/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)" == true ]]; } image_of() { podman container inspect -f '{{.ImageName}}' "$name"; } from_home() { (cd ~ && "$CLAUDIO" shell -c true /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 ))