Files
cellar-cli-c/tests/parity_test.sh
T
Matteo Benedetto 090005f6b9 cellar-cli-c: client CLI di Cellar reimplementato in C (statico, retrocompatibile)
Reimplementazione fedele di cellar-cli.py (enne2/cellar @ f5216b1) in C99
POSIX.1-2008, mono-thread, senza dipendenze esterne: miniz e' vendored per
deflate/inflate raw, mentre contenitore gzip, multipart/form-data in streaming,
tar PAX/ustar, JSON e client HTTP/1.1 sono scritti a mano.

- 7 comandi (list, upload, download, install, scan-local, wizard-upload,
  wizard-install) con stdout/stderr/exit code identici al client Python
- 30/30 test di parita' (tests/parity_test.sh) contro due server mock
  indipendenti: output a confronto, alberi installati, interop tar con
  tarfile di Python e con GNU tar, archivio C equivalente a quello Python
- build statiche x86-64 / i686 / aarch64: nessun simbolo GLIBC richiesto,
  nessuna syscall moderna (statx/openat2/memfd_create/getrandom), LFS 64 bit,
  resolver DNS di riserva (hosts + query UDP) per glibc statica senza NSS
- miglioramento rispetto all'originale: upload multipart in streaming invece
  di leggere l'intero archivio in RAM
- divergenze volute documentate nel README (help piu' sintetico, mtime dei
  symlink non ripristinato come tarfile, EOF nei prompt, niente TLS)

Build: make | make 32 | make arm64 | make test | make verify
2026-09-20 17:29:14 +02:00

253 lines
9.1 KiB
Bash
Executable File

#!/usr/bin/env bash
# Test di parita' fra il client C e il client Python, su due server mock
# indipendenti (stesso stato iniziale) cosi' che gli ID coincidano.
#
# C_BIN=dist/cellar-cli-asan tests/parity_test.sh
# C_BIN=dist/cellar-cli tests/parity_test.sh
set -uo pipefail
ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
C_BIN="${C_BIN:-$ROOT/dist/cellar-cli-asan}"
PY="${PYTHON:-python3}"
PY_CLIENT="${PY_CLIENT:-$ROOT/tests/ref/cellar-cli.py}"
WORK="$ROOT/tests/tmp"
PORT_C="${PORT_C:-18091}"
PORT_P="${PORT_P:-18092}"
PORT_DEAD="${PORT_DEAD:-18999}"
PASS=0
FAIL=0
FAILED=()
SRV_C_PID=""
SRV_P_PID=""
ok() { PASS=$((PASS + 1)); printf ' PASS %s\n' "$1"; }
ko() { FAIL=$((FAIL + 1)); FAILED+=("$1"); printf ' FAIL %s\n' "$1"; }
cleanup() {
[ -n "$SRV_C_PID" ] && kill "$SRV_C_PID" 2>/dev/null
[ -n "$SRV_P_PID" ] && kill "$SRV_P_PID" 2>/dev/null
wait 2>/dev/null
return 0
}
trap cleanup EXIT
norm() {
sed -e "s|$WORK/home-c|@HOME@|g" -e "s|$WORK/home-p|@HOME@|g" \
-e "s|bottle-install-[A-Za-z0-9_]\{6,\}|bottle-install-XXXXXX|g" \
-e "s|$WORK/installs/c|@INSTALL@|g" -e "s|$WORK/installs/p|@INSTALL@|g" \
-e "s|$WORK/bottles|@BOTTLES@|g" \
-e "s|-[0-9]\{8\}-[0-9]\{6\}\.tar\.gz|-TIMESTAMP.tar.gz|g" \
-e "s|$PORT_C|@PORT@|g" -e "s|$PORT_P|@PORT@|g"
}
check() {
local name="$1"
shift
local stdin_file="-"
if [ "${1:-}" = "--stdin" ]; then
stdin_file="$2"
shift 2
fi
local -a cargs=() pargs=()
local a
for a in "$@"; do
cargs+=("${a//@SERVER@/http://127.0.0.1:$PORT_C}")
pargs+=("${a//@SERVER@/http://127.0.0.1:$PORT_P}")
done
local rc_c rc_p
if [ "$stdin_file" = "-" ]; then
HOME="$WORK/home-c" "$C_BIN" "${cargs[@]}" >"$WORK/c.out" 2>"$WORK/c.err"
rc_c=$?
HOME="$WORK/home-p" "$PY" "$PY_CLIENT" "${pargs[@]}" >"$WORK/p.out" 2>"$WORK/p.err"
rc_p=$?
else
HOME="$WORK/home-c" "$C_BIN" "${cargs[@]}" <"$stdin_file" >"$WORK/c.out" 2>"$WORK/c.err"
rc_c=$?
HOME="$WORK/home-p" "$PY" "$PY_CLIENT" "${pargs[@]}" <"$stdin_file" >"$WORK/p.out" 2>"$WORK/p.err"
rc_p=$?
fi
norm <"$WORK/c.out" >"$WORK/c.out.n"
norm <"$WORK/p.out" >"$WORK/p.out.n"
norm <"$WORK/c.err" >"$WORK/c.err.n"
norm <"$WORK/p.err" >"$WORK/p.err.n"
local problems=""
[ "$rc_c" != "$rc_p" ] && problems+="exit($rc_c vs $rc_p) "
diff -q "$WORK/p.out.n" "$WORK/c.out.n" >/dev/null || problems+="stdout "
diff -q "$WORK/p.err.n" "$WORK/c.err.n" >/dev/null || problems+="stderr "
if [ -z "$problems" ]; then
ok "$name"
else
ko "$name [$problems]"
echo " --- stdout (PY < | C >) ---"
diff "$WORK/p.out.n" "$WORK/c.out.n" | head -20 | sed 's/^/ /'
echo " --- stderr (PY < | C >) ---"
diff "$WORK/p.err.n" "$WORK/c.err.n" | head -10 | sed 's/^/ /'
fi
}
# ------------------------------------------------------------------ setup
[ -x "$C_BIN" ] || { echo "binario C non trovato: $C_BIN (esegui make asan)"; exit 1; }
[ -f "$PY_CLIENT" ] || { echo "client Python di riferimento mancante: $PY_CLIENT"; exit 1; }
rm -rf "$WORK"
mkdir -p "$WORK"/{home-c,home-p,state-c,state-p,installs/c,installs/p,dl-dir}
printf '[cellar]\nserver = http://127.0.0.1:%s\nbottles_dir = %s/installs/c\n\n' "$PORT_C" "$WORK" >"$WORK/home-c/.cellar.conf"
printf '[cellar]\nserver = http://127.0.0.1:%s\nbottles_dir = %s/installs/p\n\n' "$PORT_P" "$WORK" >"$WORK/home-p/.cellar.conf"
BOTTLES="$WORK/bottles"
bash "$ROOT/tests/make_fake_bottle.sh" "$BOTTLES" >/dev/null
"$PY" "$ROOT/tests/mock_server.py" --port "$PORT_C" --state "$WORK/state-c" &
SRV_C_PID=$!
"$PY" "$ROOT/tests/mock_server.py" --port "$PORT_P" --state "$WORK/state-p" &
SRV_P_PID=$!
for _ in $(seq 50); do
curl -sf "http://127.0.0.1:$PORT_C/health" >/dev/null 2>&1 && break
sleep 0.1
done
for _ in $(seq 50); do
curl -sf "http://127.0.0.1:$PORT_P/health" >/dev/null 2>&1 && break
sleep 0.1
done
# archivio di riferimento creato da tarfile di Python (per testare upload,
# download, install e l'estrattore C su un archivio "python-made")
"$PY" - "$BOTTLES/Fake Bottle" "$WORK/ref-archive.tar.gz" <<'PYEOF'
import sys, tarfile
src, dst = sys.argv[1], sys.argv[2]
with tarfile.open(dst, "w:gz") as tf:
tf.add(src, arcname="Fake Bottle")
PYEOF
printf '1\nParity Wizard\nFake Bottle\nBackup via wizard\nwizard,tags\n' >"$WORK/wiz-upload.in"
printf '1\ny\n' >"$WORK/wiz-install.in"
echo "== parita' C ($C_BIN) vs Python ($PY_CLIENT) =="
# ------------------------------------------------------------------ casi
check "list vuota (usa ~/.cellar.conf)" "list"
check "scan-local tabella" "scan-local" "--bottles-dir" "$BOTTLES"
check "scan-local --json" "scan-local" "--bottles-dir" "$BOTTLES" "--json"
check "scan-local dir inesistente" "scan-local" "--bottles-dir" "$WORK/nope"
check "upload con tutti i campi" "upload" "$WORK/ref-archive.tar.gz" "--name" "Parity Test" \
"--bottle-name" "Fake Bottle" "--description" "descrizione" "--tags" "a,b" \
"--arch" "win32" "--runner" "soda-9.0-1" "--windows-version" "win10"
check "list con 1 record" "list"
check "download su file" "download" "1" "$WORK/dl.bin"
check "download su directory (nome da Content-Disposition)" "download" "1" "$WORK/dl-dir"
check "install (prima volta)" "install" "Fake Bottle"
check "install (esiste, senza --replace)" "install" "Fake Bottle"
check "install --replace" "install" "Fake Bottle" "--replace"
check "install con ref inesistente" "install" "NoSuchBottle"
check "download id inesistente (404)" "download" "99" "$WORK/dl-404.bin"
check "server irraggiungibile" "--server" "http://127.0.0.1:$PORT_DEAD" "list"
check "wizard-upload (input da pipe)" --stdin "$WORK/wiz-upload.in" "wizard-upload" "--bottles-dir" "$BOTTLES"
check "list con 2 record" "list"
check "wizard-install --replace" --stdin "$WORK/wiz-install.in" "wizard-install" "--replace"
check "errore: nessun argomento"
check "errore: comando sconosciuto" "frobnicate"
check "errore: upload senza --name" "upload" "$WORK/ref-archive.tar.gz"
check "errore: archive_id non numerico" "download" "abc" "$WORK/out.bin"
check "errore: argomento extra" "list" "extra"
check "errore: opzione sconosciuta" "scan-local" "--nope"
# ------------------------------------------------- verifiche sugli artefatti
echo "== artefatti =="
# 1. download: stesso contenuto del file caricato
if cmp -s "$WORK/dl.bin" "$WORK/ref-archive.tar.gz"; then
ok "download produce byte identici all'upload"
else
ko "download produce byte identici all'upload"
fi
if [ -f "$WORK/dl-dir/ref-archive.tar.gz" ]; then
ok "download in directory usa il filename del server"
else
ko "download in directory usa il filename del server"
fi
# 2. albero installato: Python (archivio python-made) vs C (archivio python-made)
if "$PY" "$ROOT/tests/compare_trees.py" "$WORK/installs/p/Fake Bottle" "$WORK/installs/c/Fake Bottle" --mtimes \
>"$WORK/trees1.txt" 2>&1; then
ok "albero installato identico (extract python-made)"
else
ko "albero installato identico (extract python-made)"
sed 's/^/ /' "$WORK/trees1.txt" | head -15
fi
# 3. archivio creato dal writer C vs writer Python (dall'upload dei wizard)
# l'archivio piu' recente e' quello creato dal writer del client (wizard-upload)
c_arch=$(ls -t "$WORK"/state-c/storage/*.tar.gz 2>/dev/null | head -1)
p_arch=$(ls -t "$WORK"/state-p/storage/*.tar.gz 2>/dev/null | head -1)
if [ -n "$c_arch" ] && [ -n "$p_arch" ]; then
if "$PY" "$ROOT/tests/tar_compare.py" "$p_arch" "$c_arch" >"$WORK/tar.txt" 2>&1; then
ok "archivio C equivalente a quello Python (tarfile)"
else
ko "archivio C equivalente a quello Python (tarfile)"
sed 's/^/ /' "$WORK/tar.txt" | head -15
fi
# l'archivio C deve essere leggibile anche da GNU tar
if tar tzf "$c_arch" >/dev/null 2>&1; then
ok "archivio C leggibile da GNU tar"
else
ko "archivio C leggibile da GNU tar"
fi
else
ko "archivi dei wizard non trovati negli storage dei mock server"
fi
# 4. estrazione incrociata: Python estrae l'archivio creato dal C
if [ -n "$c_arch" ]; then
rm -rf "$WORK/xpy"
mkdir -p "$WORK/xpy"
if "$PY" - "$c_arch" "$WORK/xpy" <<'PYEOF'
import sys, tarfile
with tarfile.open(sys.argv[1], "r:gz") as tf:
tf.extractall(sys.argv[2], filter="fully_trusted")
PYEOF
then
if "$PY" "$ROOT/tests/compare_trees.py" "$WORK/xpy/Fake Bottle" "$WORK/installs/p/Fake Bottle" --mtimes \
>"$WORK/trees2.txt" 2>&1; then
ok "python estrae l'archivio C con metadata identici"
else
ko "python estrae l'archivio C con metadata identici"
sed 's/^/ /' "$WORK/trees2.txt" | head -15
fi
else
ko "python estrae l'archivio C"
fi
fi
# 5. --help esce 0 in entrambi (il testo e' volutamente diverso)
HOME="$WORK/home-c" "$C_BIN" --help >/dev/null 2>&1
rc_c=$?
HOME="$WORK/home-p" "$PY" "$PY_CLIENT" --help >/dev/null 2>&1
rc_p=$?
if [ "$rc_c" = "0" ] && [ "$rc_p" = "0" ]; then
ok "--help esce 0 in entrambi"
else
ko "--help esce 0 in entrambi ($rc_c vs $rc_p)"
fi
# ------------------------------------------------------------------ riepilogo
echo
echo "=================================================="
printf 'PASS: %d FAIL: %d\n' "$PASS" "$FAIL"
if [ "$FAIL" -gt 0 ]; then
printf 'casi falliti:\n'
for n in "${FAILED[@]}"; do printf ' - %s\n' "$n"; done
exit 1
fi
echo "parita' completa ✔"