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
253 lines
9.1 KiB
Bash
Executable File
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 ✔"
|