132 lines
9.1 KiB
Bash
Executable file
132 lines
9.1 KiB
Bash
Executable file
#!/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"
|