Files
light-heart-labs--dreamserver/ods/tests/test-docker-image-pull-retry.sh
T
wehub-resource-sync 9e8f1bbeed
Dashboard / frontend (push) Failing after 0s
Dashboard / api (push) Failing after 0s
Lint PowerShell / powershell-lint (ubuntu-latest) (push) Failing after 1s
Python Lint / Lint Python with Ruff (push) Failing after 1s
ShellCheck / Lint shell scripts (push) Failing after 1s
Matrix Smoke / linux-smoke (push) Failing after 1s
Matrix Smoke / distro: cachyos (push) Failing after 15s
Matrix Smoke / distro: linux-mint-21.3 (push) Failing after 15s
Matrix Smoke / distro: debian-12 (push) Failing after 5m21s
Matrix Smoke / distro: fedora-41 (push) Failing after 4m56s
Matrix Smoke / distro: ubuntu-24.04 (push) Failing after 2m13s
Matrix Smoke / distro: rocky-9 (push) Failing after 10m39s
Matrix Smoke / distro: manjaro (push) Failing after 12m11s
Matrix Smoke / distro: opensuse-tw (push) Failing after 11m53s
Matrix Smoke / distro: archlinux (push) Failing after 20m3s
Matrix Smoke / distro: ubuntu-22.04 (push) Failing after 13m49s
Validate .env Schema / tier-1-env-validation (push) Successful in 52s
Validate .env Schema / tier-2-env-validation (push) Successful in 44s
Validate .env Schema / tier-3-env-validation (push) Successful in 52s
Validate .env Schema / tier-4-env-validation (push) Successful in 51s
Validate Extensions Catalog / Check catalog is up-to-date (push) Failing after 9m47s
Secret Scan / Scan for secrets (push) Failing after 21m4s
Validate Docker Compose / Validate Docker Compose files (push) Has been cancelled
Python Type Check / Type check with mypy (push) Has been cancelled
Validate .env Schema / tier-0-env-validation (push) Has been cancelled
Test Linux / integration-smoke (push) Has been cancelled
Lint PowerShell / powershell-lint (windows-latest) (push) Has been cancelled
Matrix Smoke / macos-smoke (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:31:33 +08:00

312 lines
9.7 KiB
Bash
Executable File

#!/bin/bash
# ==========================================================================
# ODS Docker Image Pull Retry Test Suite
# ==========================================================================
# Behavioral tests for pull_with_progress() using a mocked docker command.
# This avoids grep-based “string presence” tests and validates retry behavior.
#
# Usage: ./ods/tests/test-docker-image-pull-retry.sh
# ==========================================================================
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
# Colors
GREEN='\033[0;32m'
RED='\033[0;31m'
NC='\033[0m'
PASSED=0
FAILED=0
TMP_DIR=$(mktemp -d)
cleanup() { rm -rf "$TMP_DIR"; }
trap cleanup EXIT
export LOG_FILE="$TMP_DIR/install.log"
touch "$LOG_FILE"
print_pass() { echo -e "${GREEN}✓ PASS${NC}"; PASSED=$((PASSED + 1)); }
print_fail() { echo -e "${RED}✗ FAIL${NC} ${1:-}"; FAILED=$((FAILED + 1)); }
run_pull_with_progress() {
local docker_cmd=$1
local img=$2
local label=$3
local count=${4:-1}
local total=${5:-1}
# Run in a clean bash so we can stub sleep/spin_task without affecting this test harness.
bash -c '
set -euo pipefail
DOCKER_CMD="$1"; export DOCKER_CMD
IMG="$2"; LABEL="$3"; COUNT="$4"; TOTAL="$5"; ROOT_DIR="$6"
export LOG_FILE
# Minimal vars expected by ui.sh under -u
GRN=""; BGRN=""; DGRN=""; AMB=""; WHT=""; RED=""; NC=""; CURSOR=""
VERSION="test"; INTERACTIVE="false"; DRY_RUN=false
# Keep CI fast while preserving the requested retry delays for assertions.
sleep() {
if [[ -n "${SLEEP_LOG:-}" ]]; then
printf "%s " "$1" >> "$SLEEP_LOG"
fi
}
source "$ROOT_DIR/installers/lib/ui.sh"
spin_task() { local pid="$1"; wait "$pid"; }
pull_with_progress "$IMG" "$LABEL" "$COUNT" "$TOTAL"
' _ "$docker_cmd" "$img" "$label" "$count" "$total" "$ROOT_DIR" \
>>"$TMP_DIR/stdout.log" 2>>"$TMP_DIR/stderr.log"
}
echo ""
echo "╔═══════════════════════════════════════════╗"
echo "║ Docker Image Pull Retry Test Suite ║"
echo "╚═══════════════════════════════════════════╝"
echo ""
echo "1. Behavioral Tests (mocked docker)"
echo "──────────────────────────────────"
# --------------------------------------------------------------------------
# Mock docker scripts
# --------------------------------------------------------------------------
cat >"$TMP_DIR/docker-success" <<'EOF'
#!/usr/bin/env bash
if [[ "$1" == "pull" ]]; then
echo "Pulled"
exit 0
fi
exit 0
EOF
chmod +x "$TMP_DIR/docker-success"
cat >"$TMP_DIR/docker-fail-then-succeed" <<'EOF'
#!/usr/bin/env bash
state_file="${DOCKER_MOCK_STATE_FILE}"
: "${state_file:?DOCKER_MOCK_STATE_FILE required}"
if [[ "$1" == "pull" ]]; then
count=$(cat "$state_file" 2>/dev/null || echo 0)
count=$((count + 1))
echo "$count" > "$state_file"
if [[ $count -lt 3 ]]; then
echo "network timeout" >&2
exit 1
fi
echo "Pulled"
exit 0
fi
exit 0
EOF
chmod +x "$TMP_DIR/docker-fail-then-succeed"
cat >"$TMP_DIR/docker-always-timeout" <<'EOF'
#!/usr/bin/env bash
state_file="${DOCKER_MOCK_STATE_FILE}"
: "${state_file:?DOCKER_MOCK_STATE_FILE required}"
if [[ "$1" == "pull" ]]; then
count=$(cat "$state_file" 2>/dev/null || echo 0)
count=$((count + 1))
echo "$count" > "$state_file"
echo "network timeout" >&2
exit 1
fi
exit 0
EOF
chmod +x "$TMP_DIR/docker-always-timeout"
cat >"$TMP_DIR/docker-unauthorized" <<'EOF'
#!/usr/bin/env bash
state_file="${DOCKER_MOCK_STATE_FILE}"
: "${state_file:?DOCKER_MOCK_STATE_FILE required}"
if [[ "$1" == "pull" ]]; then
count=$(cat "$state_file" 2>/dev/null || echo 0)
count=$((count + 1))
echo "$count" > "$state_file"
echo "Error response from daemon: unauthorized: authentication required" >&2
exit 1
fi
exit 0
EOF
chmod +x "$TMP_DIR/docker-unauthorized"
# --------------------------------------------------------------------------
# Tests
# --------------------------------------------------------------------------
printf " %-60s " "succeeds on first attempt..."
rm -f "$TMP_DIR/stdout.log" "$TMP_DIR/stderr.log"
if run_pull_with_progress "$TMP_DIR/docker-success" "img" "label"; then
print_pass
else
print_fail
fi
printf " %-60s " "retries transient failures and succeeds..."
rm -f "$TMP_DIR/stdout.log" "$TMP_DIR/stderr.log"
export DOCKER_MOCK_STATE_FILE="$TMP_DIR/state-transient"
export SLEEP_LOG="$TMP_DIR/sleep-transient.log"
rm -f "$DOCKER_MOCK_STATE_FILE"
rm -f "$SLEEP_LOG"
if run_pull_with_progress "$TMP_DIR/docker-fail-then-succeed" "img" "label"; then
attempts=$(cat "$DOCKER_MOCK_STATE_FILE" 2>/dev/null || echo 0)
sleeps=$(sed 's/[[:space:]]*$//' "$SLEEP_LOG" 2>/dev/null || true)
if [[ "$attempts" == "3" && "$sleeps" == "5 15" ]]; then
print_pass
else
print_fail "(attempts=$attempts, expected 3; sleeps='$sleeps', expected '5 15')"
fi
else
print_fail
fi
unset SLEEP_LOG
printf " %-60s " "fails fast on unauthorized (no retries)..."
rm -f "$TMP_DIR/stdout.log" "$TMP_DIR/stderr.log"
export DOCKER_MOCK_STATE_FILE="$TMP_DIR/state-unauth"
rm -f "$DOCKER_MOCK_STATE_FILE"
if run_pull_with_progress "$TMP_DIR/docker-unauthorized" "img" "label"; then
print_fail "(unexpected success)"
else
attempts=$(cat "$DOCKER_MOCK_STATE_FILE" 2>/dev/null || echo 0)
if [[ "$attempts" == "1" ]]; then
print_pass
else
print_fail "(attempts=$attempts, expected 1)"
fi
fi
printf " %-60s " "uses full default retry schedule before giving up..."
rm -f "$TMP_DIR/stdout.log" "$TMP_DIR/stderr.log"
export DOCKER_MOCK_STATE_FILE="$TMP_DIR/state-default-timeout"
export SLEEP_LOG="$TMP_DIR/sleep-default-timeout.log"
rm -f "$DOCKER_MOCK_STATE_FILE" "$SLEEP_LOG"
if run_pull_with_progress "$TMP_DIR/docker-always-timeout" "img" "label"; then
print_fail "(unexpected success)"
else
attempts=$(cat "$DOCKER_MOCK_STATE_FILE" 2>/dev/null || echo 0)
sleeps=$(sed 's/[[:space:]]*$//' "$SLEEP_LOG" 2>/dev/null || true)
if [[ "$attempts" == "4" && "$sleeps" == "5 15 30" ]]; then
print_pass
else
print_fail "(attempts=$attempts, expected 4; sleeps='$sleeps', expected '5 15 30')"
fi
fi
unset SLEEP_LOG
printf " %-60s " "supports configurable retry delays and attempts..."
rm -f "$TMP_DIR/stdout.log" "$TMP_DIR/stderr.log"
export DOCKER_MOCK_STATE_FILE="$TMP_DIR/state-configurable"
export SLEEP_LOG="$TMP_DIR/sleep-configurable.log"
export ODS_DOCKER_PULL_MAX_ATTEMPTS=4
export ODS_DOCKER_PULL_RETRY_DELAYS="1 2"
rm -f "$DOCKER_MOCK_STATE_FILE" "$SLEEP_LOG"
if run_pull_with_progress "$TMP_DIR/docker-always-timeout" "img" "label"; then
print_fail "(unexpected success)"
else
attempts=$(cat "$DOCKER_MOCK_STATE_FILE" 2>/dev/null || echo 0)
sleeps=$(sed 's/[[:space:]]*$//' "$SLEEP_LOG" 2>/dev/null || true)
if [[ "$attempts" == "4" && "$sleeps" == "1 2 4" ]]; then
print_pass
else
print_fail "(attempts=$attempts, expected 4; sleeps='$sleeps', expected '1 2 4')"
fi
fi
unset ODS_DOCKER_PULL_MAX_ATTEMPTS ODS_DOCKER_PULL_RETRY_DELAYS SLEEP_LOG
echo ""
echo "2. Integration Tests"
echo "────────────────────"
printf " %-60s " "Phase 08 calls pull_with_progress..."
if grep -q "pull_with_progress" "$ROOT_DIR/installers/phases/08-images.sh"; then
print_pass
else
print_fail
fi
printf " %-60s " "Phase 08 audits resolved Compose images..."
if grep -q "ods_compose_external_images" "$ROOT_DIR/installers/phases/08-images.sh"; then
print_pass
else
print_fail
fi
printf " %-60s " "Phase 08 tracks pull_failed count..."
if grep -q "pull_failed" "$ROOT_DIR/installers/phases/08-images.sh"; then
print_pass
else
print_fail
fi
printf " %-60s " "Phase 08 fails before service startup on pull failures..."
if grep -q "Phase 5 will not perform unprotected Docker pulls" "$ROOT_DIR/installers/phases/08-images.sh"; then
print_pass
else
print_fail
fi
printf " %-60s " "Phase 11 preflights Compose images with retry..."
if grep -q "_phase11_pre_pull_compose_images" "$ROOT_DIR/installers/phases/11-services.sh" \
&& grep -q "pull_with_progress" "$ROOT_DIR/installers/phases/11-services.sh"; then
print_pass
else
print_fail
fi
printf " %-60s " "Phase 11 disables implicit Compose pulls..."
if grep -q -- "--pull never" "$ROOT_DIR/installers/phases/11-services.sh"; then
print_pass
else
print_fail
fi
echo ""
echo "3. Retry Strategy Tests (source-level sanity checks)"
echo "────────────────────────────────────────────────────"
printf " %-60s " "max_attempts is 4..."
retry_count=$(grep -A 15 "^pull_with_progress()" "$ROOT_DIR/installers/lib/ui.sh" | grep -m1 "max_attempts=" | grep -oE '[0-9]+' || true)
retry_count=${retry_count:-0}
if [[ "$retry_count" == "4" ]]; then
print_pass
else
print_fail "(found $retry_count, expected 4)"
fi
printf " %-60s " "default retry delays are 5s, 15s, 30s..."
default_delays=$(
bash -c '
set -euo pipefail
source "$1"
_docker_pull_retry_delay 1
_docker_pull_retry_delay 2
_docker_pull_retry_delay 3
' _ "$ROOT_DIR/installers/lib/ui.sh" | paste -sd ' '
)
if [[ "$default_delays" == "5 15 30" ]]; then
print_pass
else
print_fail "(found '$default_delays')"
fi
echo ""
echo "═══════════════════════════════════════════"
if [[ $FAILED -eq 0 ]]; then
echo -e "${GREEN}✓ All tests passed${NC} ($PASSED/$((PASSED + FAILED)))"
echo ""
exit 0
else
echo -e "${RED}✗ Some tests failed${NC} ($PASSED passed, $FAILED failed)"
echo ""
exit 1
fi