#!/usr/bin/env bash # Copies and strict mocks only: no host block device or privileged command is touched. set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" command -v bwrap >/dev/null || { echo 'bwrap requis' >&2; exit 1; } BASE="$(mktemp -d)" trap 'rm -rf -- "$BASE"' EXIT passes=0 fail() { printf 'ÉCHEC : %s\n' "$*" >&2; exit 1; } new_case() { case_dir="$(mktemp -d "$BASE/case.XXXXXXXX")" mkdir -p "$case_dir"/{repo/scripts,bin,mnt,tmp/fixture,sys/dev/block,sys/firmware/efi,run/archiso,run/lock} mkdir -p "$case_dir/run/archiso"/{bootmnt/airootfs,cowspace/upper,cowspace/work} for mm in 8:16 8:17 8:18 8:20; do mkdir -p "$case_dir/sys/dev/block/$mm/holders"; done cp "$ROOT/scripts/prepare-disk.sh" "$ROOT/scripts/prepare-disk.py" "$case_dir/repo/scripts/" cp "$ROOT/tests/prepare-disk/mock-command.py" "$case_dir/mock-command.py" for cmd in readlink lsblk findmnt swapon wipefs parted partprobe udevadm mkfs.fat mkfs.btrfs btrfs mount umount; do ln -s /opt/mock-command.py "$case_dir/bin/$cmd" done for cmd in bash python3 dirname mkdir; do ln -s "/usr/bin/$cmd" "$case_dir/bin/$cmd"; done printf '{"partitioned":false,"mounts":[]}' > "$case_dir/tmp/fixture/state.json" : > "$case_dir/tmp/fixture/calls" } run_case() { local label="$1"; shift local mnt_args=(--bind "$case_dir/mnt" /mnt) [[ "${CASE_MNT_LINK:-0}" != 1 ]] || mnt_args=(--symlink /tmp/fixture /mnt) set +e printf '%s\n' "${ANSWER:-}" | bwrap --unshare-user --unshare-pid --unshare-net --uid "${TEST_UID:-0}" --gid "${TEST_UID:-0}" \ --tmpfs / --ro-bind /usr /usr --symlink usr/lib /lib --symlink usr/lib /lib64 \ --symlink usr/bin /bin "${mnt_args[@]}" --bind "$case_dir/tmp" /tmp \ --bind "$case_dir/sys" /sys --bind "$case_dir/run" /run --tmpfs /opt \ --ro-bind "$case_dir/repo" /opt/repo --ro-bind "$case_dir/mock-command.py" /opt/mock-command.py \ --ro-bind "$case_dir/bin" /opt/bin --dev /dev --proc /proc \ --setenv PATH /opt/bin --setenv CASE_FAIL "${CASE_FAIL:-}" --setenv CASE_STYLE "${CASE_STYLE:-}" \ -- /opt/repo/scripts/prepare-disk.sh "$@" > "$case_dir/out" 2>&1 rc=$? set -e printf '[test] %s : rc=%s\n' "$label" "$rc" } no_mutation() { ! grep -Eq '^(wipefs|parted|partprobe|udevadm|mkfs\.fat|mkfs\.btrfs|btrfs|mount|umount) ' "$case_dir/tmp/fixture/calls" || fail 'mutation imprévue'; } expect() { [[ "$rc" == "$1" ]] || fail "code $rc attendu $1: $(cat "$case_dir/out")"; } new_case; run_case help --help; expect 0; no_mutation; ((passes+=1)) new_case; TEST_UID=1000 run_case dry --dry-run /dev/target; expect 0; no_mutation; ((passes+=1)) new_case; CASE_FAIL=overlay_live TEST_UID=1000 run_case dry_overlay --dry-run /dev/target expect 0; no_mutation; ((passes+=1)) for scenario in wrong_type mounted_child mounted_disk source_target swap_partition swapfile mnt_occupied overlay_unmapped lsblk:inspection findmnt:inspection swapon:inspection; do new_case; CASE_FAIL="$scenario" run_case "$scenario" /dev/target; expect 1; no_mutation; ((passes+=1)) done new_case; touch "$case_dir/sys/dev/block/8:16/holders/dm-test" run_case holder /dev/target; expect 1; grep -q 'holder actif' "$case_dir/out" || fail 'holder non diagnostiqué'; no_mutation; ((passes+=1)) new_case; rmdir "$case_dir/sys/dev/block/8:16/holders" run_case holders_unreadable /dev/target; expect 1; grep -q 'holders illisibles' "$case_dir/out" || fail 'lecture holders ignorée'; no_mutation; ((passes+=1)) new_case; touch "$case_dir/mnt/old-data" run_case mnt_data /dev/target; expect 1; grep -q '/mnt lié, absent ou non vide' "$case_dir/out" || fail 'données /mnt ignorées'; no_mutation; ((passes+=1)) new_case; CASE_MNT_LINK=1 run_case mnt_link /dev/target; expect 1 grep -q '/mnt lié, absent ou non vide' "$case_dir/out" || fail 'lien /mnt ignoré'; no_mutation; ((passes+=1)) new_case; rm "$case_dir/bin/parted"; ANSWER='ERASE /dev/target 8:16' run_case missing_dependency /dev/target expect 1; grep -q 'dépendance absente : parted' "$case_dir/out" || fail 'dépendance non diagnostiquée'; no_mutation; ((passes+=1)) new_case; rm -r "$case_dir/run/archiso"; run_case no_live /dev/target expect 1; grep -q 'live Arch UEFI' "$case_dir/out" || fail 'contexte live non vérifié'; no_mutation; ((passes+=1)) new_case; run_case partition /dev/target1; expect 1; no_mutation; ((passes+=1)) new_case; CASE_FAIL=source_target run_case alias_source /dev/alias; expect 1 grep -q 'disque ou descendant monté' "$case_dir/out" || fail 'alias source non protégé'; no_mutation; ((passes+=1)) new_case; CASE_FAIL=readlink run_case alias_missing /dev/alias; expect 1; no_mutation; ((passes+=1)) new_case; ANSWER=no run_case refusal /dev/target; expect 1; no_mutation; ((passes+=1)) new_case; run_case eof /dev/target; expect 1; no_mutation; ((passes+=1)) new_case; ANSWER='ERASE /dev/target 8:16' CASE_FAIL=identity_change run_case identity_change /dev/target expect 1; grep -q 'identité changée' "$case_dir/out" || fail 'changement identité non bloqué'; no_mutation; ((passes+=1)) new_case; ANSWER='ERASE /dev/target 8:16' run_case nominal /dev/alias; expect 0 [[ "$(grep -c '^mount ' "$case_dir/tmp/fixture/calls")" == 7 ]] || fail 'montages incomplets' [[ -d "$case_dir/mnt/@/.snapshots" ]] || fail 'sous-volume imbriqué absent' ((passes+=1)) new_case; CASE_STYLE=nvme ANSWER='ERASE /dev/nvme9n9 8:16' run_case nominal_nvme /dev/nvme9n9; expect 0 grep -q '^mkfs.fat -F 32 -n EFI /dev/nvme9n9p1$' "$case_dir/tmp/fixture/calls" || fail 'partition NVMe non vérifiée' ((passes+=1)) new_case; CASE_FAIL=overlay_live ANSWER='ERASE /dev/target 8:16' run_case nominal_overlay /dev/target expect 0; ((passes+=1)) new_case; CASE_FAIL=canonical_subvol ANSWER='ERASE /dev/target 8:16' run_case canonical_subvol /dev/target expect 0; ((passes+=1)) new_case exec 9> "$case_dir/run/lock/arch-system-prepare-8_16.lock" flock -n 9 || fail 'verrou fixture indisponible' ANSWER='ERASE /dev/target 8:16' run_case concurrent /dev/target; expect 1 grep -q 'autre invocation coopérante' "$case_dir/out" || fail 'concurrence non bloquée' no_mutation exec 9>&- ((passes+=1)) new_case; ANSWER='ERASE /dev/target 8:16' CASE_FAIL=bad_partitions run_case bad_partitions /dev/target; expect 1 ! grep -q '^mkfs' "$case_dir/tmp/fixture/calls" || fail 'formatage malgré partitions invalides' ((passes+=1)) new_case; ANSWER='ERASE /dev/target 8:16' CASE_FAIL=mkfs.btrfs run_case fail_mkfs /dev/target; expect 1 ! grep -q '^mount ' "$case_dir/tmp/fixture/calls" || fail 'montage après formatage échoué' ((passes+=1)) for scenario in wipefs parted partprobe udevadm mkfs.fat btrfs; do new_case; ANSWER='ERASE /dev/target 8:16' CASE_FAIL="$scenario" run_case "fail_$scenario" /dev/target; expect 1 grep -q "^$scenario " "$case_dir/tmp/fixture/calls" || fail "étape $scenario non atteinte" grep -q "étape à vérifier" "$case_dir/out" || fail 'bilan d’échec absent' case "$scenario" in wipefs) ! grep -q '^parted ' "$case_dir/tmp/fixture/calls" || fail 'parted après wipefs échoué' ;; parted) [[ "$(grep -c '^parted ' "$case_dir/tmp/fixture/calls")" == 1 ]] || fail 'parted poursuivi après échec' ;; partprobe) ! grep -q '^udevadm ' "$case_dir/tmp/fixture/calls" || fail 'udev après partprobe échoué' ;; udevadm) ! grep -q '^mkfs' "$case_dir/tmp/fixture/calls" || fail 'formatage après udev échoué' ;; mkfs.fat) ! grep -q '^mkfs.btrfs ' "$case_dir/tmp/fixture/calls" || fail 'Btrfs après FAT32 échoué' ;; btrfs) [[ "$(grep -c '^btrfs ' "$case_dir/tmp/fixture/calls")" == 1 ]] || fail 'sous-volume poursuivi après échec' grep -q '^umount -- /mnt$' "$case_dir/tmp/fixture/calls" || fail 'montage top-level non nettoyé' ;; esac ((passes+=1)) done new_case; ANSWER='ERASE /dev/target 8:16' CASE_FAIL=snapshot_conflict run_case snapshot_conflict /dev/target; expect 1 grep -q 'subvolume destination already exists' "$case_dir/out" || fail 'précondition .snapshots non testée' ! grep -q '^mount -o' "$case_dir/tmp/fixture/calls" || fail 'montage final malgré conflit .snapshots' ((passes+=1)) for scenario in mount:third mount_cleanup_fail mount_replaced; do new_case; ANSWER='ERASE /dev/target 8:16' CASE_FAIL="$scenario" run_case "$scenario" /dev/target; expect 1 if [[ "$scenario" == mount:third ]]; then grep -q '^umount -- /mnt$' "$case_dir/tmp/fixture/calls" || fail 'nettoyage ciblé absent' elif [[ "$scenario" == mount_cleanup_fail ]]; then grep -q 'Montages restants à examiner : /mnt' "$case_dir/out" || fail 'échec nettoyage non signalé' else grep -q 'montage à examiner, non démonté : /mnt' "$case_dir/out" || fail 'montage remplacé démonté' [[ "$(grep -c '^umount -- /mnt$' "$case_dir/tmp/fixture/calls")" == 1 ]] || fail 'montage remplacé démonté' fi ((passes+=1)) done for scenario in signal_INT signal_TERM; do new_case; ANSWER='ERASE /dev/target 8:16' CASE_FAIL="$scenario" run_case "$scenario" /dev/target [[ "$rc" == 130 || "$rc" == 143 ]] || fail "code interruption inattendu : $rc" ! grep -q '^mkfs' "$case_dir/tmp/fixture/calls" || fail 'formatage après interruption' ((passes+=1)) done printf 'Résultat : %s scénarios confinés de préparation validés.\n' "$passes"