diff --git a/tests/README.md b/tests/README.md index d691f10..7fd0a5f 100644 --- a/tests/README.md +++ b/tests/README.md @@ -45,6 +45,11 @@ the build does. The stage either passes or the build fails; nothing is tagged. fake `docker ps` rows, what the argument parser refuses, and that a start which never serves fails loudly and cleans up. `docker` and `curl` are stubbed on PATH and `DOCKER_HOST` points at nothing, so no container is ever started. +- `test-image.sh` — how `scripts/image.sh` picks an image: the commit parsed out + of a tag, the selectors that name a build, and what it refuses when a selector + matches nothing or matches two. A stub `docker` serves a fixture table of tags, + so there is no daemon here; the git side is a throwaway repository, because + choosing between builds made in the same second is a git question. ## Not run by default diff --git a/tests/shell/test-image.sh b/tests/shell/test-image.sh new file mode 100755 index 0000000..7f357a2 --- /dev/null +++ b/tests/shell/test-image.sh @@ -0,0 +1,229 @@ +#!/usr/bin/env bash +# scripts/image.sh decides which locally built image is in play, so what is +# worth testing is the deciding: which commit a tag names, which of several tags +# wins, and what it says when the answer is none of them. +# +# No daemon is needed and none is available - this runs in the image's test +# stage. A stub `docker` on PATH answers the two read-only queries the script +# makes from a fixture table of tags, which is also the only way to test it +# against a machine holding a known set of images. The git side is real: a +# throwaway repository, the same trick tests/shell/test-deploy.sh uses, because +# choosing between builds made in the same second is a git question. +# +# Not covered: `diff` and `--deep`. Those read layers and files out of real +# images, which needs a daemon; ./scripts/test.sh is where that would live. +set -uo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +# Not TMP: this file sources image.sh, which has a TMP of its own. +WORK="$(mktemp -d)" +trap 'rm -rf "$WORK"' EXIT + +pass=0; fail=0 +check() { if "${@:2}"; then echo "ok $1"; pass=$((pass+1)); else echo "FAIL $1"; fail=$((fail+1)); fi; } + +eq() { [ "$1" = "$2" ] || { echo " want '$2', got '$1'" >&2; return 1; }; } +has() { case "$2" in *"$1"*) return 0 ;; esac; echo " no '$1' in the output" >&2; return 1; } +nope() { ! "$@"; } + +# --- a fake repo with the script in it --------------------------------------- + +mkdir -p "$WORK/arena/scripts" "$WORK/bin" +cp "$ROOT/scripts/image.sh" "$WORK/arena/scripts/image.sh" +cp "$ROOT/versions.env" "$WORK/arena/versions.env" + +git -C "$WORK/arena" init --quiet -b main +git -C "$WORK/arena" config user.email t@t +git -C "$WORK/arena" config user.name t +git -C "$WORK/arena" add -A +git -C "$WORK/arena" commit --quiet -m "first" +C1="$(git -C "$WORK/arena" rev-parse --short=12 HEAD)" +git -C "$WORK/arena" commit --quiet --allow-empty -m "second" +C2="$(git -C "$WORK/arena" rev-parse --short=12 HEAD)" +git -C "$WORK/arena" commit --quiet --allow-empty -m "third and newest" +C3="$(git -C "$WORK/arena" rev-parse --short=12 HEAD)" + +# Stands in for the daemon. Reads tag|id|size|created|revision rows out of +# $FIXTURE and answers in the shapes image.sh asks for. Deliberately bash and +# coreutils only: the test stage is the build environment plus a few packages, +# and a stub needing something it does not have fails for the wrong reason. +cat > "$WORK/bin/docker" <<'STUB' +#!/usr/bin/env bash +row() { + local t id size created rev + while IFS='|' read -r t id size created rev; do + if [ "$t" = "$1" ]; then printf '%s|%s|%s|%s\n' "$id" "$size" "$created" "$rev"; return 0; fi + done < "$FIXTURE" + return 1 +} + +[ "$1" = image ] || exit 99 +shift + +case "$1" in + ls) + shift + # `image ls -f dangling=true`: this machine has none. + if [ "$1" = "-f" ]; then exit 0; fi + cut -d'|' -f1 "$FIXTURE" + ;; + inspect) + shift + fmt="" + if [ "$1" = "--format" ]; then fmt="$2"; shift 2; fi + rc=0 + for ref in "$@"; do + if ! out="$(row "${ref##*:}")"; then + echo "Error response from daemon: No such image: $ref" >&2 + rc=1 + continue + fi + case "$fmt" in + "") ;; # an existence check + *Id*) printf 'sha256:%s\n' "$out" ;; + *) echo "stub: unhandled --format $fmt" >&2; exit 98 ;; + esac + done + exit "$rc" + ;; + *) exit 99 ;; +esac +STUB +chmod +x "$WORK/bin/docker" + +img() { + ( cd "$WORK/arena" && PATH="$WORK/bin:$PATH" ./scripts/image.sh "$@" ) +} + +# Three builds one second apart on the clock but not in git, which is the case +# that made this script sort for itself: docker gives them one Created value and +# lists them in a different order every time. The rows are out of order here on +# purpose - nothing may depend on what the daemon happened to print first. +SAME="2026-08-07T17:44:54.895042045-04:00" +cat > "$WORK/fixture" < "$WORK/fixture-ambiguous" <&1)" +check "refuses a sha that names two builds" test $? -ne 0 +check " and names the first of them" has "claude-one-deadbeef1234" "$out" +check " and the second" has "claude-two-deadbeef1234" "$out" + +# --- list --------------------------------------------------------------------- + +out="$(img list 2>&1)" +check "six tags over five images are counted as five" has "5 image(s)" "$out" +check " with the moving tag named as an alias, not a row" has "also tagged: latest" "$out" +check "a commit this repo does not have is called out" \ + has "1 image(s) name a commit this repo does not have" "$out" +check "an old tag gets its commit from the revision label" has " ${C1:0:7} " "$out" +check "marks the row versions.env names" has "* mm0.51.0-sur26.4.7-main-$C3" "$out" + +out="$(IMAGE_TAG=mm0.51.0-sur26.4.7-main-000000000000 img list 2>&1)" +check "says so when versions.env names nothing that is built" has "nothing here is" "$out" + +# --- verify ------------------------------------------------------------------- + +out="$(img verify newest 2>&1)" +check "verify names the commit's subject" has "third and newest" "$out" +check " and lists the other tags on that image" has "also latest" "$out" + +out="$(img verify deadbeef 2>&1)" +check "verify says when nothing can rebuild an image" has "NOT in this repo" "$out" + +echo +echo "$pass passed, $fail failed" +[ "$fail" -eq 0 ]