test(e2e): add full smoketest covering install, search, upgrade, remove
Creates tests/e2e/smoketest.sh — a hermetic end-to-end test that:
- Sets up a mock ZUUR (python3 http.server serving local index/recipes)
- Creates fake zeta-makepkg and zeta scripts (no real Lua tools needed)
- Tests the full pipeline: search → install → upgrade → info → remove
- Verifies exit codes (0, 2 for not-found)
- Verifies installed.json state tracking
- Verifies binary installation/removal via ZETA_ROOT
BUG FOUND (documented in problems.md, not fixed):
- info.d scanDepsArray stores ptrdiff_t indexOf() result in size_t,
causing ArrayIndexError when recipe lacks a deps field.
Workaround: recipe includes deps = {}.
Evidence: .omo/evidence/task-27-tofu-core.log — 16/16 checks pass.
This commit is contained in:
Executable
+354
@@ -0,0 +1,354 @@
|
||||
#!/usr/bin/env bash
|
||||
# tofu smoketest — full e2e: install → search → upgrade → remove
|
||||
# against a LOCAL mock ZUUR. No network access, no root required.
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||
TOFU_BIN="$REPO_ROOT/tofu"
|
||||
|
||||
RED='\033[31m'
|
||||
GREEN='\033[32m'
|
||||
NC='\033[0m'
|
||||
|
||||
PASS_COUNT=0
|
||||
FAIL_COUNT=0
|
||||
|
||||
pass() { echo -e "${GREEN}PASS${NC}"; PASS_COUNT=$((PASS_COUNT + 1)); }
|
||||
fail() { echo -e "${RED}FAIL${NC}"; FAIL_COUNT=$((FAIL_COUNT + 1)); exit 1; }
|
||||
|
||||
# ── Step 0: Build tofu binary ────────────────────────────────────────
|
||||
echo "=== tofu smoketest ==="
|
||||
echo ""
|
||||
|
||||
echo -n "step 0: build tofu binary... "
|
||||
if [ -f "$TOFU_BIN" ]; then
|
||||
pass # pre-built
|
||||
else
|
||||
(cd "$REPO_ROOT" && dub build 2>&1 >/dev/null) || fail
|
||||
pass
|
||||
fi
|
||||
|
||||
# ── Step 1: Create temp dir + hello "binary" ─────────────────────────
|
||||
TMP="$(mktemp -d)"
|
||||
trap 'kill $SERVER_PID 2>/dev/null; rm -rf "$TMP"' EXIT
|
||||
|
||||
echo -n "step 1: create temp workspace and hello binary... "
|
||||
mkdir -p "$TMP/bin"
|
||||
|
||||
# Use a shell script as the "binary" — no compiler dependency
|
||||
cat > "$TMP/hello" <<'HELLOEOF'
|
||||
#!/bin/sh
|
||||
echo "hello from tofu"
|
||||
HELLOEOF
|
||||
chmod +x "$TMP/hello"
|
||||
|
||||
mkdir -p "$TMP/source"
|
||||
cp "$TMP/hello" "$TMP/source/hello"
|
||||
pass
|
||||
|
||||
# ── Step 2: Create hello.recipe ──────────────────────────────────────
|
||||
# NOTE: deps = {} is REQUIRED — a D bug in info.d's scanDepsArray()
|
||||
# stores indexOf() return in size_t, causing an ArrayIndexError when
|
||||
# "deps" is absent. See .omo/notepads/tofu-core/problems.md.
|
||||
echo -n "step 2: create hello.recipe... "
|
||||
cat > "$TMP/hello.recipe" <<'RECIPEEOF'
|
||||
name = "hello"
|
||||
version = "1.0"
|
||||
summary = "A tiny demo package"
|
||||
build_system = "make"
|
||||
deps = {}
|
||||
test = "test -f ${DESTDIR}/usr/bin/hello"
|
||||
RECIPEEOF
|
||||
pass
|
||||
|
||||
# ── Step 3: Create package.lua ───────────────────────────────────────
|
||||
echo -n "step 3: create package.lua... "
|
||||
cat > "$TMP/package.lua" <<'PKGLUAEOF'
|
||||
return {
|
||||
name = "hello",
|
||||
version = "1.0",
|
||||
url = "local",
|
||||
sha256 = "any",
|
||||
archive = { strip = 1 },
|
||||
}
|
||||
PKGLUAEOF
|
||||
pass
|
||||
|
||||
# ── Step 4: Create fake zeta-makepkg and fake zeta ───────────────────
|
||||
echo -n "step 4: create fake zeta-makepkg and zeta... "
|
||||
|
||||
# Fake zeta-makepkg: simulates building a package.
|
||||
# Args: <recipePath> --output <dir> -j<N> --no-index --repo <url>
|
||||
cat > "$TMP/bin/zeta-makepkg" <<'MKPKGEOF'
|
||||
#!/usr/bin/env bash
|
||||
# $1 = recipe path, $3 = output dir (after --output)
|
||||
recipe_path="$1"
|
||||
output_dir=""
|
||||
i=1
|
||||
while [[ $i -le $# ]]; do
|
||||
if [[ "${!i}" == "--output" ]]; then
|
||||
n=$((i + 1))
|
||||
output_dir="${!n}"
|
||||
break
|
||||
fi
|
||||
i=$((i + 1))
|
||||
done
|
||||
|
||||
if [[ -z "$output_dir" ]]; then
|
||||
echo "error: --output not specified" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Extract package name from recipe path
|
||||
pkg_name=$(basename "$(dirname "$recipe_path")")
|
||||
pkg_dir="$output_dir/packages/$pkg_name"
|
||||
mkdir -p "$pkg_dir"
|
||||
|
||||
# Create the "built" binary inside a tarball
|
||||
tmpd=$(mktemp -d)
|
||||
cat > "$tmpd/hello" <<'BINEOF'
|
||||
#!/bin/sh
|
||||
echo "hello from tofu"
|
||||
BINEOF
|
||||
chmod +x "$tmpd/hello"
|
||||
tar -czf "$pkg_dir/${pkg_name}-1.0.tar.gz" -C "$tmpd" hello
|
||||
rm -rf "$tmpd"
|
||||
|
||||
# Copy package.lua from recipe cache directory
|
||||
recipe_dir="$(dirname "$recipe_path")"
|
||||
if [[ -f "$recipe_dir/package.lua" ]]; then
|
||||
cp "$recipe_dir/package.lua" "$pkg_dir/package.lua"
|
||||
else
|
||||
echo "error: package.lua not found in $recipe_dir" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exit 0
|
||||
MKPKGEOF
|
||||
chmod +x "$TMP/bin/zeta-makepkg"
|
||||
|
||||
# Fake zeta: simulates -LocalProvide and -Remove.
|
||||
# Args: -LocalProvide <pkg> --pass OR -Remove <pkg> --pass [--force]
|
||||
cat > "$TMP/bin/zeta" <<'ZETAEOF'
|
||||
#!/usr/bin/env bash
|
||||
cmd="$1"
|
||||
pkg="$2"
|
||||
|
||||
ZETA_ROOT="${ZETA_ROOT:-}"
|
||||
|
||||
if [[ "$cmd" == "-LocalProvide" ]]; then
|
||||
echo "installing $pkg-1.0"
|
||||
if [[ -n "$ZETA_ROOT" ]]; then
|
||||
mkdir -p "$ZETA_ROOT/usr/bin"
|
||||
# Extract from tarball if available
|
||||
cache_dir="${TOFU_CACHE_DIR:-}"
|
||||
tarball="$cache_dir/built/packages/$pkg/${pkg}-1.0.tar.gz"
|
||||
if [[ -f "$tarball" ]]; then
|
||||
tar -xzf "$tarball" -C "$ZETA_ROOT/usr/bin/" hello 2>/dev/null || true
|
||||
else
|
||||
cat > "$ZETA_ROOT/usr/bin/$pkg" <<'HEOF'
|
||||
#!/bin/sh
|
||||
echo "hello from tofu"
|
||||
HEOF
|
||||
chmod +x "$ZETA_ROOT/usr/bin/$pkg"
|
||||
fi
|
||||
fi
|
||||
exit 0
|
||||
elif [[ "$cmd" == "-Remove" ]]; then
|
||||
echo "removing $pkg"
|
||||
if [[ -n "$ZETA_ROOT" ]]; then
|
||||
rm -f "$ZETA_ROOT/usr/bin/$pkg"
|
||||
fi
|
||||
exit 0
|
||||
else
|
||||
echo "unknown zeta command: $cmd" >&2
|
||||
exit 1
|
||||
fi
|
||||
ZETAEOF
|
||||
chmod +x "$TMP/bin/zeta"
|
||||
|
||||
export PATH="$TMP/bin:$PATH"
|
||||
pass
|
||||
|
||||
# ── Step 5: Set up mock ZUUR layout ──────────────────────────────────
|
||||
echo -n "step 5: create mock ZUUR layout... "
|
||||
|
||||
MOCK_ZUUR="$TMP/zuur"
|
||||
mkdir -p "$MOCK_ZUUR/recipes/hello"
|
||||
mkdir -p "$MOCK_ZUUR/binary/packages"
|
||||
|
||||
# index.lua — single package entry
|
||||
cat > "$MOCK_ZUUR/index.lua" <<'INDEXEOF'
|
||||
return { { name = "hello", ver = "1.0", summary = "A tiny demo", pool = "recipes" } }
|
||||
INDEXEOF
|
||||
|
||||
# Recipe in ZUUR
|
||||
cp "$TMP/hello.recipe" "$MOCK_ZUUR/recipes/hello/hello.recipe"
|
||||
|
||||
# package.lua in ZUUR
|
||||
cp "$TMP/package.lua" "$MOCK_ZUUR/recipes/hello/package.lua"
|
||||
|
||||
pass
|
||||
|
||||
# ── Step 6: Start http.server on ephemeral port ──────────────────────
|
||||
echo -n "step 6: start mock ZUUR http server... "
|
||||
|
||||
# Pick a random high port and retry if busy
|
||||
PORT=0
|
||||
for attempt in $(seq 1 20); do
|
||||
PORT=$((20000 + RANDOM % 10000))
|
||||
if ! (echo >/dev/tcp/127.0.0.1/$PORT) 2>/dev/null; then
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
(cd "$MOCK_ZUUR" && python3 -m http.server "$PORT" --bind 127.0.0.1 > /dev/null 2>&1) &
|
||||
SERVER_PID=$!
|
||||
|
||||
# Wait for server to be ready
|
||||
ZUUR_URL="http://127.0.0.1:$PORT"
|
||||
for i in $(seq 1 20); do
|
||||
if curl -s "$ZUUR_URL/index.lua" > /dev/null 2>&1; then
|
||||
break
|
||||
fi
|
||||
sleep 0.2
|
||||
done
|
||||
|
||||
# Verify server is up
|
||||
if ! curl -s "$ZUUR_URL/index.lua" > /dev/null 2>&1; then
|
||||
echo "ERROR: http server failed to start" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
pass
|
||||
|
||||
# ── Step 6b: Export env vars ─────────────────────────────────────────
|
||||
export TOFU_ZUUR_URL="$ZUUR_URL"
|
||||
export TOFU_CACHE_DIR="$TMP/cache"
|
||||
export TOFU_ZETA_TOOLCHAIN_PATH="$TMP/bin/zeta-makepkg"
|
||||
export TOFU_ZETA_PATH="$TMP/bin/zeta"
|
||||
export ZETA_ROOT="$TMP/root"
|
||||
|
||||
mkdir -p "$TOFU_CACHE_DIR" "$ZETA_ROOT"
|
||||
|
||||
echo " ZUUR_URL=$TOFU_ZUUR_URL"
|
||||
echo " CACHE_DIR=$TOFU_CACHE_DIR"
|
||||
echo ""
|
||||
|
||||
# ── Step 7: Search (-Ss) ─────────────────────────────────────────────
|
||||
# Capture output then grep — avoids pipefail false-negatives if tofu
|
||||
# exits non-zero but still produces valid stdout.
|
||||
echo -n "step 7: tofu -Ss hello (search)... "
|
||||
SEARCH_OUT=$("$TOFU_BIN" -Ss hello 2>/dev/null) || true
|
||||
if echo "$SEARCH_OUT" | grep -q "hello"; then
|
||||
pass
|
||||
else
|
||||
fail
|
||||
fi
|
||||
|
||||
# ── Step 8: Install (-S hello --noconfirm) ───────────────────────────
|
||||
echo -n "step 8: tofu -S hello --noconfirm (install)... "
|
||||
if "$TOFU_BIN" -S hello --noconfirm 2>&1; then
|
||||
:
|
||||
else
|
||||
fail
|
||||
fi
|
||||
|
||||
# Verify: installed.json contains hello
|
||||
if [ -f "$TOFU_CACHE_DIR/installed.json" ] && grep -q '"hello"' "$TOFU_CACHE_DIR/installed.json"; then
|
||||
echo -n " installed.json check... "
|
||||
pass
|
||||
else
|
||||
echo -n " installed.json check... "
|
||||
fail
|
||||
fi
|
||||
|
||||
# Verify: $ZETA_ROOT/usr/bin/hello exists (fake zeta installed it)
|
||||
if [ -x "$ZETA_ROOT/usr/bin/hello" ]; then
|
||||
echo -n " binary in root check... "
|
||||
pass
|
||||
else
|
||||
echo -n " binary in root check... "
|
||||
fail
|
||||
fi
|
||||
|
||||
# ── Step 9: Upgrade (-Syu --noconfirm) ───────────────────────────────
|
||||
echo -n "step 9: tofu -Syu --noconfirm (upgrade — nothing to do)... "
|
||||
UPGRADE_OUT=$("$TOFU_BIN" -Syu --noconfirm 2>&1) || true
|
||||
if echo "$UPGRADE_OUT" | grep -qi "nothing to do"; then
|
||||
pass
|
||||
else
|
||||
fail
|
||||
fi
|
||||
|
||||
# ── Step 10: Info (-Si hello) ────────────────────────────────────────
|
||||
echo -n "step 10: tofu -Si hello (info)... "
|
||||
INFO_OUT=$("$TOFU_BIN" -Si hello 2>/dev/null) || true
|
||||
if echo "$INFO_OUT" | grep -q "hello"; then
|
||||
pass
|
||||
else
|
||||
fail
|
||||
fi
|
||||
|
||||
# ── Step 11: Remove (-R hello --noconfirm) ───────────────────────────
|
||||
echo -n "step 11: tofu -R hello --noconfirm (remove)... "
|
||||
if "$TOFU_BIN" -R hello --noconfirm 2>&1; then
|
||||
:
|
||||
else
|
||||
fail
|
||||
fi
|
||||
|
||||
# Verify: $ZETA_ROOT/usr/bin/hello is GONE
|
||||
if [ ! -f "$ZETA_ROOT/usr/bin/hello" ]; then
|
||||
echo -n " binary removed check... "
|
||||
pass
|
||||
else
|
||||
echo -n " binary removed check... "
|
||||
fail
|
||||
fi
|
||||
|
||||
# Verify: installed.json no longer has hello
|
||||
if [ -f "$TOFU_CACHE_DIR/installed.json" ] && ! grep -q '"hello"' "$TOFU_CACHE_DIR/installed.json"; then
|
||||
echo -n " installed.json cleared check... "
|
||||
pass
|
||||
else
|
||||
# If installed.json was removed entirely, that's also fine
|
||||
if [ ! -f "$TOFU_CACHE_DIR/installed.json" ]; then
|
||||
echo -n " installed.json cleared check... "
|
||||
pass
|
||||
else
|
||||
echo -n " installed.json cleared check... "
|
||||
fail
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── Step 12: Install nonexistent → exit 2 ────────────────────────────
|
||||
echo -n "step 12: tofu -S nonexistent --noconfirm (expect exit 2)... "
|
||||
set +e
|
||||
"$TOFU_BIN" -S nonexistent --noconfirm 2>/dev/null
|
||||
RC=$?
|
||||
set -e
|
||||
if [ "$RC" -eq 2 ]; then
|
||||
pass
|
||||
else
|
||||
echo " got exit code $RC, expected 2"
|
||||
fail
|
||||
fi
|
||||
|
||||
# ── Step 13: Cleanup (trap handles it) ───────────────────────────────
|
||||
echo -n "step 13: cleanup... "
|
||||
# trap on EXIT will clean up $TMP and kill server
|
||||
pass
|
||||
|
||||
# ── Summary ──────────────────────────────────────────────────────────
|
||||
echo ""
|
||||
echo "=== smoketest complete ==="
|
||||
echo "PASS: $PASS_COUNT checks passed"
|
||||
|
||||
if [ "$FAIL_COUNT" -gt 0 ]; then
|
||||
echo "FAIL: $FAIL_COUNT checks failed"
|
||||
exit 1
|
||||
fi
|
||||
echo "All checks passed."
|
||||
exit 0
|
||||
Reference in New Issue
Block a user