409 lines
21 KiB
Bash
Executable file
409 lines
21 KiB
Bash
Executable file
#!/usr/bin/env bash
|
||
# Seules des copies dans bwrap lisent les sources synthétiques ; jamais ArchASP.
|
||
set -euo pipefail
|
||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||
command -v bwrap >/dev/null || { printf 'bwrap requis\n' >&2; exit 1; }
|
||
TEST_HOME="$(mktemp -d)"
|
||
trap 'rm -rf -- "$TEST_HOME"' EXIT
|
||
passes=0 case_dir='' rc=0
|
||
fail() { printf 'ÉCHEC : %s\n' "$*" >&2; exit 1; }
|
||
pass() { passes=$((passes + 1)); }
|
||
expect_rc() { [[ "$rc" == "$1" ]] || fail "code $rc, attendu $1 : $(cat "$case_dir/output")"; }
|
||
contains() { grep -Fq -- "$1" "$case_dir/output" || fail "texte absent : $1"; }
|
||
not_contains() { ! grep -Fq -- "$1" "$case_dir/output" || fail "texte inattendu : $1"; }
|
||
no_repo_change() {
|
||
[[ "$(cat "$case_dir/repo/pkglist/pacman.txt")" == old-list ]] || fail 'liste du dépôt modifiée'
|
||
[[ "$(cat "$case_dir/repo/rootfs/etc/pacman.conf")" == old-config ]] || fail 'configuration du dépôt modifiée'
|
||
[[ "$(cat "$case_dir/repo/inventory/storage/lsblk.txt")" == old-inventory ]] || fail 'inventaire du dépôt modifié'
|
||
}
|
||
no_complete() { [[ -z "$(find "$case_dir/exports" -maxdepth 1 -type d -name 'export-*' -print -quit 2>/dev/null)" ]] || fail 'faux candidat complet'; }
|
||
incomplete_marked() {
|
||
local stage
|
||
stage="$(find "$case_dir/exports" -maxdepth 1 -type d -name '.incomplete.*' -print -quit)"
|
||
[[ -n "$stage" ]] || fail 'zone incomplète absente'
|
||
grep -Fq "$(printf 'global_status\tINCOMPLETE')" "$stage/manifest.tsv" || fail 'statut incomplet absent'
|
||
}
|
||
one_final() {
|
||
local n
|
||
n="$(find "$case_dir/exports" -maxdepth 1 -type d -name 'export-*' | wc -l)"
|
||
[[ "$n" == 1 ]] || fail "nombre de candidats complets : $n"
|
||
final_dir="$(find "$case_dir/exports" -maxdepth 1 -type d -name 'export-*' -print -quit)"
|
||
}
|
||
check_hashes() {
|
||
python3 - "$1" <<'PY'
|
||
from pathlib import Path
|
||
import hashlib,sys,stat
|
||
import re
|
||
p=Path(sys.argv[1]); lines=(p/'manifest.tsv').read_text().splitlines()
|
||
assert 'format\t1' in lines
|
||
assert any(x.startswith('source_revision\t074d5dd0') for x in lines)
|
||
assert any(x.startswith('global_status\tCOMPLETE') for x in lines)
|
||
assert stat.S_IMODE(p.stat().st_mode)==0o700
|
||
assert stat.S_IMODE((p/'manifest.tsv').stat().st_mode)==0o600
|
||
items=lines[lines.index('file\tsha256')+1:]
|
||
assert items
|
||
listed=set()
|
||
for row in items:
|
||
rel,digest=row.split('\t')
|
||
assert re.fullmatch(r'[0-9a-f]{64}',digest),rel
|
||
listed.add(rel)
|
||
f=p/rel
|
||
assert f.is_file(),rel
|
||
assert hashlib.sha256(f.read_bytes()).hexdigest()==digest,rel
|
||
assert stat.S_IMODE(f.stat().st_mode)==0o600,rel
|
||
assert 'manifest.tsv' not in [x.split('\t')[0] for x in items]
|
||
actual={str(f.relative_to(p)) for f in p.rglob('*') if f.is_file() and f.name!='manifest.tsv'}
|
||
assert listed==actual,(listed,actual)
|
||
PY
|
||
}
|
||
|
||
new_case() {
|
||
case_dir="$(mktemp -d "$TEST_HOME/case.XXXXXXXX")"
|
||
mkdir -p "$case_dir"/{repo/scripts,repo/pkglist,repo/rootfs/etc,repo/inventory/storage,etc/tlp.d,etc/systemd,etc/snapper/configs,etc/NetworkManager/system-connections,boot,usr-local-sbin,tmp,home,bin,dev,run}
|
||
cp "$ROOT/scripts/backup.sh" "$case_dir/repo/scripts/backup.sh"
|
||
chmod 755 "$case_dir/repo/scripts/backup.sh"
|
||
printf 'old-list\n' > "$case_dir/repo/pkglist/pacman.txt"
|
||
printf 'old-config\n' > "$case_dir/repo/rootfs/etc/pacman.conf"
|
||
printf 'old-inventory\n' > "$case_dir/repo/inventory/storage/lsblk.txt"
|
||
printf 'old-findmnt\n' > "$case_dir/repo/inventory/storage/findmnt.txt"
|
||
for f in fstab pacman.conf makepkg.conf mkinitcpio.conf nftables.conf limine-snapper-sync.conf; do
|
||
printf 'fixture %s\n' "$f" > "$case_dir/etc/$f"
|
||
done
|
||
printf 'fixture tlp\n' > "$case_dir/etc/tlp.d/10-archasp.conf"
|
||
printf 'fixture zram\n' > "$case_dir/etc/systemd/zram-generator.conf"
|
||
printf 'fixture snapper\n' > "$case_dir/etc/snapper/configs/root"
|
||
printf 'fixture helper\n' > "$case_dir/usr-local-sbin/batlimit-set"
|
||
printf 'fixture helper\n' > "$case_dir/usr-local-sbin/vmware-stack"
|
||
cat > "$case_dir/boot/limine.conf" <<'LIMINE'
|
||
/Arch Zen
|
||
protocol: linux
|
||
kernel_path: boot():/vmlinuz-linux-zen
|
||
cmdline: root=UUID=1111-AAAA rw
|
||
/Arch LTS
|
||
protocol: linux
|
||
cmdline: root=PARTUUID=2222-BBBB rw
|
||
### Auto-generated by limine-snapper-sync
|
||
/Snapshot private value
|
||
LIMINE
|
||
printf 'SECRET_NEVER_EXPORT\n' > "$case_dir/etc/NetworkManager/system-connections/secret"
|
||
printf 'SECRET_NEVER_EXPORT\n' > "$case_dir/home/.env"
|
||
for safe in bash cat dirname realpath stat mkdir mktemp date awk sed grep rm rmdir sort; do
|
||
[[ -x "/usr/bin/$safe" ]] || fail "outil réel absent : /usr/bin/$safe"
|
||
ln -s "/usr/bin/$safe" "$case_dir/bin/$safe"
|
||
done
|
||
for mocked in sudo pacman systemctl lsblk findmnt free swapon git mv sha256sum; do
|
||
ln -s /mnt/mock-command.sh "$case_dir/bin/$mocked"
|
||
done
|
||
cp "$ROOT/tests/backup/mock-command.sh" "$case_dir/mock-command.sh"
|
||
chmod 755 "$case_dir/mock-command.sh"
|
||
: > "$case_dir/calls"
|
||
}
|
||
|
||
# La racine hôte est en lecture seule ; /etc, /boot, /usr/local/sbin, /home,
|
||
# /root, /run et /tmp sont remplacés ou masqués ; aucune connexion réseau.
|
||
sandbox() {
|
||
local uid_args=()
|
||
[[ "${SIM_AS_ROOT:-0}" != 1 ]] || uid_args=(--uid 0 --gid 0)
|
||
bwrap --unshare-user --unshare-pid --unshare-net "${uid_args[@]}" \
|
||
--ro-bind / / --bind "$case_dir/etc" /etc --bind "$case_dir/boot" /boot \
|
||
--bind "$case_dir/usr-local-sbin" /usr/local/sbin \
|
||
--tmpfs /home --tmpfs /root --tmpfs /run \
|
||
--bind "$case_dir/tmp" /tmp --bind "$case_dir" /mnt --dev /dev --proc /proc \
|
||
--setenv PATH /mnt/bin --setenv HOME /mnt/home --setenv TMPDIR /tmp \
|
||
--setenv XDG_STATE_HOME "${SIM_XDG:-}" \
|
||
--setenv SIM_LEGACY "${SIM_LEGACY:-0}" \
|
||
--setenv SIM_FAIL_KEY "${SIM_FAIL_KEY:-}" \
|
||
--setenv SIM_EMPTY_KEY "${SIM_EMPTY_KEY:-}" \
|
||
--setenv SIM_ORPHANS_EMPTY_ERROR "${SIM_ORPHANS_EMPTY_ERROR:-0}" \
|
||
--setenv SIM_INSPECT_TARGET "${SIM_INSPECT_TARGET:-}" \
|
||
--setenv SIM_INSPECT_MODE "${SIM_INSPECT_MODE:-}" \
|
||
--setenv SIM_SIGNAL_KEY "${SIM_SIGNAL_KEY:-}" \
|
||
--setenv SIM_SIGNAL "${SIM_SIGNAL:-TERM}" \
|
||
--setenv SIM_FINAL_FAIL "${SIM_FINAL_FAIL:-0}" \
|
||
--setenv SIM_FINAL_COLLIDE "${SIM_FINAL_COLLIDE:-0}" \
|
||
--setenv SIM_FINAL_SIGNAL_PHASE "${SIM_FINAL_SIGNAL_PHASE:-}" \
|
||
--setenv SIM_FINAL_SIGNAL "${SIM_FINAL_SIGNAL:-TERM}" \
|
||
--setenv SIM_HASH_MODE "${SIM_HASH_MODE:-}" \
|
||
-- /mnt/repo/scripts/backup.sh "$@"
|
||
}
|
||
run_case() {
|
||
local name="$1"; shift
|
||
set +e
|
||
sandbox "$@" > "$case_dir/stdout" 2> "$case_dir/stderr"
|
||
rc=$?
|
||
set -e
|
||
cat "$case_dir/stdout" "$case_dir/stderr" > "$case_dir/output"
|
||
if [[ "$name" != legacy_partial && "$name" != invalid && "$name" != missing_arg && "$name" != missing_arg_option ]]; then
|
||
if (( rc == 0 )); then
|
||
if [[ "$name" == dry_relative_xdg ]]; then
|
||
[[ "$(cat "$case_dir/stderr")" == '[!] XDG_STATE_HOME relatif ignoré ; défaut sous HOME utilisé.' ]] || fail 'diagnostic XDG inattendu'
|
||
else
|
||
[[ ! -s "$case_dir/stderr" ]] || fail "stderr de succès inattendu : $(cat "$case_dir/stderr")"
|
||
fi
|
||
else
|
||
if grep -Ev '^\[!\] ' "$case_dir/stderr" > "$case_dir/unexpected-stderr"; then
|
||
fail "stderr d’erreur inattendu : $(cat "$case_dir/unexpected-stderr")"
|
||
fi
|
||
fi
|
||
fi
|
||
printf '[test] %s : rc=%s\n' "$name" "$rc"
|
||
}
|
||
|
||
# Reproduction du script initial sur sa copie seulement. Un échec tardif
|
||
# laisse déjà une liste remplacée alors que l'ancien inventaire demeure.
|
||
new_case
|
||
git -C "$ROOT" show 074d5dd0b2a1b362c8c0cd6413ab77f868461e25:scripts/backup.sh > "$case_dir/repo/scripts/backup.sh"
|
||
SIM_LEGACY=1 SIM_FAIL_KEY=lsblk run_case legacy_partial
|
||
expect_rc 47
|
||
[[ "$(cat "$case_dir/repo/pkglist/pacman.txt")" == alpha ]] || fail 'défaut initial non reproduit'
|
||
[[ "$(cat "$case_dir/repo/inventory/storage/lsblk.txt")" == partial ]] || fail 'sortie partielle initiale non reproduite'
|
||
[[ "$(cat "$case_dir/repo/inventory/storage/findmnt.txt")" == old-findmnt ]] || fail 'mélange ancien/nouveau non reproduit'
|
||
pass
|
||
|
||
new_case
|
||
run_case help --help; expect_rc 0; contains 'Export système privé'; [[ ! -s "$case_dir/calls" ]] || fail 'appel en aide'; pass
|
||
run_case invalid --bad; expect_rc 2; pass
|
||
run_case missing_arg --output-dir; expect_rc 2; pass
|
||
run_case missing_arg_option --output-dir --dry-run; expect_rc 2; pass
|
||
SIM_AS_ROOT=1 run_case root_refused --output-dir /mnt/exports
|
||
expect_rc 1; contains 'exécution root refusée'; [[ ! -s "$case_dir/calls" ]] || fail 'appel en root refusé'; pass
|
||
|
||
new_case
|
||
run_case dry_default --dry-run; expect_rc 0; contains '/mnt/home/.local/state/arch-system/exports'
|
||
[[ ! -e "$case_dir/home/.local" && ! -s "$case_dir/calls" ]] || fail 'simulation avec effet'
|
||
pass
|
||
new_case
|
||
SIM_XDG=/mnt/state run_case dry_xdg --dry-run; expect_rc 0; contains '/mnt/state/arch-system/exports'; pass
|
||
new_case
|
||
SIM_XDG=relative run_case dry_relative_xdg --dry-run; expect_rc 0
|
||
contains 'XDG_STATE_HOME relatif ignoré'; contains '/mnt/home/.local/state/arch-system/exports'; pass
|
||
new_case
|
||
run_case dry_spaces --dry-run --output-dir '/mnt/parent with spaces'
|
||
expect_rc 0; contains '/mnt/parent with spaces'; pass
|
||
new_case
|
||
run_case dry_normalized --dry-run --output-dir /mnt/home/../exports
|
||
expect_rc 0; contains 'Parent : /mnt/exports'; pass
|
||
new_case
|
||
run_case inside_repo --dry-run --output-dir /mnt/repo/exports
|
||
expect_rc 1; contains 'destination dans le dépôt interdite'; pass
|
||
new_case
|
||
ln -s /mnt/repo "$case_dir/alias"
|
||
run_case repo_alias --dry-run --output-dir /mnt/alias/exports
|
||
expect_rc 1; contains 'destination dans le dépôt interdite'; pass
|
||
new_case
|
||
ln -s /mnt/home "$case_dir/alias"
|
||
run_case linked_parent --dry-run --output-dir /mnt/alias/exports
|
||
expect_rc 1; contains 'lien dans le parent refusé'; pass
|
||
|
||
# Vérification séparée du fragment réel, sans privilèges dans le même bwrap.
|
||
new_case
|
||
inspect_code="$(python3 - "$ROOT/scripts/backup.sh" <<'PY'
|
||
from pathlib import Path
|
||
import sys
|
||
s=Path(sys.argv[1]).read_text()
|
||
print(s.split("readonly INSPECT_CODE='",1)[1].split("\n'",1)[0])
|
||
PY
|
||
)"
|
||
mkdir "$case_dir/etc/target-directory"
|
||
ln -s /mnt/missing "$case_dir/etc/target-link"
|
||
ln -s /mnt/missing "$case_dir/etc/parent-link"
|
||
inspect_fragment() {
|
||
local target="$1" expected_rc="$2" expected_state="$3" actual
|
||
set +e
|
||
bwrap --unshare-user --unshare-pid --unshare-net --ro-bind / / \
|
||
--bind "$case_dir/etc" /etc --tmpfs /home --tmpfs /root --tmpfs /run \
|
||
--bind "$case_dir/tmp" /tmp --bind "$case_dir" /mnt --dev /dev --proc /proc \
|
||
--setenv PATH /mnt/bin \
|
||
-- /usr/bin/bash -c "$inspect_code" -- "$target" > "$case_dir/inspect.out" 2> "$case_dir/inspect.err"
|
||
actual=$?
|
||
set -e
|
||
[[ "$actual" == "$expected_rc" && "$(cat "$case_dir/inspect.out")" == "$expected_state" ]] ||
|
||
fail "inspection réelle : $target = $actual/$(cat "$case_dir/inspect.out")"
|
||
pass
|
||
}
|
||
inspect_fragment /etc/pacman.conf 0 EXISTING
|
||
inspect_fragment /etc/missing 0 ABSENT
|
||
inspect_fragment /etc/missing/child 0 ABSENT
|
||
inspect_fragment /etc/target-directory 0 INVALID
|
||
inspect_fragment /etc/target-link 0 LINK
|
||
inspect_fragment /etc/parent-link/child 0 LINK
|
||
inspect_fragment relative 2 ''
|
||
inspect_fragment / 2 ''
|
||
|
||
new_case
|
||
run_case nominal --output-dir /mnt/exports; expect_rc 0
|
||
[[ ! -s "$case_dir/stderr" ]] || fail "stderr nominal : $(cat "$case_dir/stderr")"
|
||
one_final; check_hashes "$final_dir"; no_repo_change
|
||
grep -Fq 'global_status' "$final_dir/manifest.tsv" || fail 'statut global absent'
|
||
grep -Fq 'root=UUID=@ROOT_UUID@' "$final_dir/templates/limine.conf" || fail 'UUID non remplacé'
|
||
grep -Fq 'root=PARTUUID=@ROOT_PARTUUID@' "$final_dir/templates/limine.conf" || fail 'PARTUUID non remplacé'
|
||
! grep -Fq 'Auto-generated' "$final_dir/templates/limine.conf" || fail 'section générée conservée'
|
||
! grep -R -Fq 'SECRET_NEVER_EXPORT' "$final_dir" || fail 'fichier interdit collecté'
|
||
pass
|
||
run_case second_export --output-dir /mnt/exports; expect_rc 0
|
||
[[ "$(find "$case_dir/exports" -maxdepth 1 -type d -name 'export-*' | wc -l)" == 2 ]] || fail 'collision des invocations successives'
|
||
pass
|
||
new_case
|
||
run_case actual_default; expect_rc 0
|
||
default_final="$(find "$case_dir/home/.local/state/arch-system/exports" -maxdepth 1 -type d -name 'export-*' -print -quit)"
|
||
[[ -n "$default_final" ]] || fail 'export par défaut absent'
|
||
check_hashes "$default_final"; no_repo_change; pass
|
||
new_case
|
||
SIM_XDG=/mnt/custom-state run_case actual_xdg; expect_rc 0
|
||
xdg_final="$(find "$case_dir/custom-state/arch-system/exports" -maxdepth 1 -type d -name 'export-*' -print -quit)"
|
||
[[ -n "$xdg_final" ]] || fail 'export XDG absent'
|
||
check_hashes "$xdg_final"; pass
|
||
new_case
|
||
run_case actual_spaces --output-dir '/mnt/parent with spaces'; expect_rc 0
|
||
space_final="$(find "$case_dir/parent with spaces" -maxdepth 1 -type d -name 'export-*' -print -quit)"
|
||
[[ -n "$space_final" ]] || fail 'export avec espaces absent'
|
||
check_hashes "$space_final"; pass
|
||
new_case
|
||
run_case prior_for_backslash --output-dir /mnt/exports; expect_rc 0
|
||
one_final; prior_dir="$final_dir"
|
||
run_case actual_backslash --output-dir '/mnt/parent\backslash'; expect_rc 0
|
||
backslash_final="$(find "$case_dir/parent\backslash" -maxdepth 1 -type d -name 'export-*' -print -quit)"
|
||
[[ -n "$backslash_final" ]] || fail 'candidat avec antislash absent'
|
||
check_hashes "$backslash_final"; check_hashes "$prior_dir"; no_repo_change; pass
|
||
new_case
|
||
mkdir -m 0755 "$case_dir/exports"
|
||
run_case public_parent --output-dir /mnt/exports; expect_rc 1
|
||
contains 'parent final non privé'
|
||
[[ -z "$(find "$case_dir/exports" -mindepth 1 -print -quit)" ]] || fail 'zone créée dans parent public'
|
||
pass
|
||
|
||
new_case
|
||
set +e
|
||
sandbox --output-dir /mnt/exports > "$case_dir/concurrent-a.out" 2> "$case_dir/concurrent-a.err" & pid_a=$!
|
||
sandbox --output-dir /mnt/exports > "$case_dir/concurrent-b.out" 2> "$case_dir/concurrent-b.err" & pid_b=$!
|
||
wait "$pid_a"; rc_a=$?
|
||
wait "$pid_b"; rc_b=$?
|
||
set -e
|
||
[[ "$rc_a" == 0 && "$rc_b" == 0 ]] || fail 'concurrence : échec d’une invocation'
|
||
[[ "$(find "$case_dir/exports" -maxdepth 1 -type d -name 'export-*' | wc -l)" == 2 ]] || fail 'collision simultanée'
|
||
for candidate in "$case_dir"/exports/export-*; do check_hashes "$candidate"; done
|
||
no_repo_change; pass
|
||
|
||
new_case
|
||
SIM_FAIL_KEY=pacman_native run_case early_failure --output-dir /mnt/exports
|
||
expect_rc 1; contains 'collecte obligatoire échouée : pacman_native'; no_complete; no_repo_change
|
||
incomplete_marked
|
||
! find "$case_dir/exports" -type f -name pacman.txt | grep -q . || fail 'sortie partielle conservée comme fichier'
|
||
pass
|
||
new_case
|
||
SIM_FAIL_KEY=btrfs_filesystem_usage run_case late_failure --output-dir /mnt/exports
|
||
expect_rc 1; contains 'collecte obligatoire échouée : btrfs_usage'; no_complete; no_repo_change
|
||
incomplete_marked
|
||
pass
|
||
new_case
|
||
SIM_INSPECT_TARGET=/etc/fstab SIM_INSPECT_MODE=fail run_case sudo_refusal --output-dir /mnt/exports
|
||
expect_rc 1; contains 'contrôle obligatoire impossible : fstab'; no_complete; no_repo_change; pass
|
||
new_case
|
||
SIM_INSPECT_TARGET=/etc/nftables.conf SIM_INSPECT_MODE=fail run_case optional_sudo_refusal --output-dir /mnt/exports
|
||
expect_rc 0; one_final
|
||
grep -Fq "$(printf 'nftables_conf\toptional\tCHECK_ERROR\t')" "$final_dir/manifest.tsv" || fail 'refus facultatif assimilé à absence'
|
||
[[ ! -e "$final_dir/rootfs/etc/nftables.conf" ]] || fail 'lecture facultative malgré refus'
|
||
pass
|
||
for mode in parent_fail empty unknown valid_fail; do
|
||
new_case
|
||
SIM_INSPECT_TARGET=/etc/fstab SIM_INSPECT_MODE="$mode" run_case "inspect_$mode" --output-dir /mnt/exports
|
||
expect_rc 1; no_complete; no_repo_change; pass
|
||
done
|
||
new_case
|
||
rm "$case_dir/etc/fstab"
|
||
run_case missing_required --output-dir /mnt/exports
|
||
expect_rc 1; contains 'source obligatoire absente : fstab'; no_complete; pass
|
||
new_case
|
||
rm "$case_dir/etc/nftables.conf"
|
||
SIM_EMPTY_KEY=pacman_foreign run_case empty_and_absent --output-dir /mnt/exports
|
||
expect_rc 0; one_final; check_hashes "$final_dir"
|
||
grep -Fq "$(printf 'pacman_foreign\trequired\tEMPTY\t')" "$final_dir/manifest.tsv" || fail 'liste vide non distinguée'
|
||
grep -Fq "$(printf 'nftables_conf\toptional\tABSENT\t')" "$final_dir/manifest.tsv" || fail 'absence facultative non distinguée'
|
||
grep -Fq "$(printf 'nvme_health\toptional\tUNAVAILABLE\t')" "$final_dir/manifest.tsv" || fail 'NVMe indisponible non distingué'
|
||
pass
|
||
new_case
|
||
SIM_FAIL_KEY=pacman_orphans run_case optional_failure --output-dir /mnt/exports
|
||
expect_rc 0; one_final
|
||
grep -Fq "$(printf 'pacman_orphans\toptional\tCOLLECT_ERROR\t')" "$final_dir/manifest.tsv" || fail 'échec facultatif dissimulé'
|
||
pass
|
||
new_case
|
||
SIM_ORPHANS_EMPTY_ERROR=1 run_case orphan_ambiguous --output-dir /mnt/exports
|
||
expect_rc 0; one_final
|
||
grep -Fq "$(printf 'pacman_orphans\toptional\tCOLLECT_ERROR\t')" "$final_dir/manifest.tsv" || fail 'code 1 d’orphelins assimilé à vide'
|
||
[[ ! -e "$final_dir/pkglist/orphans.txt" ]] || fail 'fausse liste vide d’orphelins'
|
||
pass
|
||
new_case
|
||
SIM_FAIL_KEY='read:/etc/pacman.conf' run_case privileged_read_partial --output-dir /mnt/exports
|
||
expect_rc 1; contains 'lecture obligatoire échouée : pacman_conf'; no_complete
|
||
! find "$case_dir/exports" -type f -name pacman.conf | grep -q . || fail 'lecture partielle publiée'
|
||
pass
|
||
new_case
|
||
SIM_FAIL_KEY=user_state run_case optional_user_failure --output-dir /mnt/exports
|
||
expect_rc 0; one_final
|
||
grep -Fq "$(printf 'user_enabled\toptional\tCOLLECT_ERROR\t')" "$final_dir/manifest.tsv" || fail 'échec systemd utilisateur dissimulé'
|
||
pass
|
||
new_case
|
||
printf '/Arch\n cmdline: root=/dev/sda2 rw\n' > "$case_dir/boot/limine.conf"
|
||
run_case limine_unsupported --output-dir /mnt/exports; expect_rc 0; one_final
|
||
grep -Fq 'root_non_pris_en_charge' "$final_dir/manifest.tsv" || fail 'forme Limine non prise en charge masquée'
|
||
pass
|
||
new_case
|
||
SIM_FINAL_FAIL=1 run_case finalize_fail --output-dir /mnt/exports
|
||
expect_rc 1; contains 'renommage final impossible'; no_complete; no_repo_change; pass
|
||
new_case
|
||
SIM_FINAL_COLLIDE=1 run_case finalize_collision --output-dir /mnt/exports
|
||
expect_rc 1; contains 'renommage final non effectué'
|
||
[[ "$(find "$case_dir/exports" -maxdepth 1 -type d -name 'export-*' | wc -l)" == 1 ]] || fail 'collision mal traitée'
|
||
[[ "$(cat "$case_dir"/exports/export-*/keep)" == 'existing candidate' ]] || fail 'destination préexistante écrasée'
|
||
pass
|
||
for hash_mode in fail malformed; do
|
||
new_case
|
||
run_case prior_for_hash --output-dir /mnt/exports; expect_rc 0
|
||
one_final; prior_dir="$final_dir"
|
||
SIM_HASH_MODE="$hash_mode" run_case "hash_$hash_mode" --output-dir /mnt/exports
|
||
expect_rc 1
|
||
if [[ "$hash_mode" == fail ]]; then contains 'empreinte impossible'
|
||
else contains "réponse d'empreinte invalide"; fi
|
||
incomplete_marked
|
||
[[ "$(find "$case_dir/exports" -maxdepth 1 -type d -name 'export-*' | wc -l)" == 1 ]] || fail 'faux candidat après erreur de somme'
|
||
check_hashes "$prior_dir"; no_repo_change; pass
|
||
done
|
||
for phase in before after; do
|
||
for signal in INT TERM; do
|
||
new_case
|
||
run_case prior_for_final_signal --output-dir /mnt/exports; expect_rc 0
|
||
one_final; prior_dir="$final_dir"
|
||
SIM_FINAL_SIGNAL_PHASE="$phase" SIM_FINAL_SIGNAL="$signal" run_case "final_${phase}_${signal}" --output-dir /mnt/exports
|
||
if [[ "$signal" == INT ]]; then expect_rc 130; else expect_rc 143; fi
|
||
contains "Interruption $signal ("
|
||
check_hashes "$prior_dir"; no_repo_change
|
||
if [[ "$phase" == before ]]; then
|
||
incomplete_marked
|
||
contains 'Export incomplet conservé : /mnt/exports/.incomplete.'
|
||
not_contains 'Finalisation acquise'
|
||
[[ "$(find "$case_dir/exports" -maxdepth 1 -type d -name 'export-*' | wc -l)" == 1 ]] || fail 'finalisation revendiquée avant renommage'
|
||
else
|
||
[[ -z "$(find "$case_dir/exports" -maxdepth 1 -type d -name '.incomplete.*' -print -quit)" ]] || fail 'staging restant après renommage'
|
||
[[ "$(find "$case_dir/exports" -maxdepth 1 -type d -name 'export-*' | wc -l)" == 2 ]] || fail 'candidat finalisé non conservé'
|
||
for candidate in "$case_dir"/exports/export-*; do
|
||
[[ "$candidate" == "$prior_dir" ]] && continue
|
||
check_hashes "$candidate"
|
||
contains "Finalisation acquise, terminaison interrompue ; candidat conservé : /mnt/exports/${candidate##*/}"
|
||
done
|
||
fi
|
||
pass
|
||
done
|
||
done
|
||
for signal in INT TERM; do
|
||
new_case
|
||
run_case prior_export --output-dir /mnt/exports; expect_rc 0
|
||
one_final; prior_dir="$final_dir"
|
||
SIM_SIGNAL_KEY=btrfs_filesystem_usage SIM_SIGNAL="$signal" run_case "signal_$signal" --output-dir /mnt/exports
|
||
if [[ "$signal" == INT ]]; then expect_rc 130; else expect_rc 143; fi
|
||
contains "Interruption $signal"
|
||
incomplete_marked
|
||
[[ -d "$prior_dir" && "$(find "$case_dir/exports" -maxdepth 1 -type d -name 'export-*' | wc -l)" == 1 ]] || fail 'export antérieur affecté par le signal'
|
||
check_hashes "$prior_dir"; no_repo_change; pass
|
||
done
|
||
|
||
printf 'Résultat : %s scénarios synthétiques confinés validés.\n' "$passes"
|