Skip to content

Commit 25353e4

Browse files
author
linux_name2
committed
tests: cover set_subvol, update_fstab_home, HOME_COPY_FILES, key trimming, home skeleton and orphan home GC
1 parent 9d85765 commit 25353e4

2 files changed

Lines changed: 275 additions & 4 deletions

File tree

‎tests/test_common.sh‎

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ section "Function definitions"
172172
EXPECTED_FUNCTIONS=(
173173
load_config validate_config check_dependencies acquire_lock
174174
is_child_of_aur_helper sign_uki verify_uki update_fstab
175+
update_fstab_home populate_home_skeleton
175176
get_current_subvol get_current_subvol_raw get_root_device
176177
ensure_btrfs_mounted validate_subvolume check_btrfs_space
177178
check_esp_space list_generations build_uki garbage_collect
@@ -200,6 +201,7 @@ assert_eq "KERNEL_PKG default" "linux" "$KERNEL_PKG"
200201
assert_eq "LOCK_FILE default" "/var/lock/atomic-upgrade.lock" "$LOCK_FILE"
201202
assert_eq "SBCTL_SIGN default" "0" "$SBCTL_SIGN"
202203
assert_eq "UPGRADE_GUARD default" "1" "$UPGRADE_GUARD"
204+
assert_eq "HOME_COPY_FILES default" "" "$HOME_COPY_FILES"
203205
assert_match "KERNEL_PARAMS contains rw" "rw" "$KERNEL_PARAMS"
204206
assert_match "KERNEL_PARAMS contains pti=on" "pti=on" "$KERNEL_PARAMS"
205207

@@ -967,6 +969,120 @@ section "is_child_of_aur_helper"
967969
run_cmd is_child_of_aur_helper
968970
assert_eq "not child of aur helper" "1" "$_rc"
969971

972+
# ── HOME_COPY_FILES config ─────────────────────
973+
974+
section "HOME_COPY_FILES config"
975+
976+
# Test: HOME_COPY_FILES loaded from config
977+
CONFIG_FILE="${TESTDIR}/home_copy.conf"
978+
cat > "$CONFIG_FILE" <<'EOF'
979+
HOME_COPY_FILES=.bashrc .ssh .gitconfig
980+
EOF
981+
HOME_COPY_FILES=""
982+
load_config
983+
assert_eq "HOME_COPY_FILES loaded" ".bashrc .ssh .gitconfig" "$HOME_COPY_FILES"
984+
985+
# Test: HOME_COPY_FILES with quotes
986+
CONFIG_FILE="${TESTDIR}/home_copy_quoted.conf"
987+
cat > "$CONFIG_FILE" <<'EOF'
988+
HOME_COPY_FILES=".bashrc .bash_profile .ssh"
989+
EOF
990+
HOME_COPY_FILES=""
991+
load_config
992+
assert_eq "HOME_COPY_FILES quoted" ".bashrc .bash_profile .ssh" "$HOME_COPY_FILES"
993+
994+
# Test: HOME_COPY_FILES absent → default preserved
995+
CONFIG_FILE="${TESTDIR}/home_copy_absent.conf"
996+
cat > "$CONFIG_FILE" <<'EOF'
997+
KEEP_GENERATIONS=3
998+
EOF
999+
HOME_COPY_FILES=""
1000+
load_config
1001+
assert_eq "HOME_COPY_FILES absent → stays empty" "" "$HOME_COPY_FILES"
1002+
1003+
# Restore
1004+
HOME_COPY_FILES=""
1005+
1006+
1007+
# ── config key whitespace trimming ─────────────
1008+
1009+
section "Config key whitespace trimming"
1010+
1011+
CONFIG_FILE="${TESTDIR}/whitespace_key.conf"
1012+
printf ' KEEP_GENERATIONS=7\n' > "$CONFIG_FILE"
1013+
printf '\tESP=/boot/efi\n' >> "$CONFIG_FILE"
1014+
KEEP_GENERATIONS=3
1015+
ESP="/efi"
1016+
load_config
1017+
assert_eq "leading spaces in key trimmed" "7" "$KEEP_GENERATIONS"
1018+
assert_eq "leading tab in key trimmed" "/boot/efi" "$ESP"
1019+
KEEP_GENERATIONS=3
1020+
ESP="/efi"
1021+
1022+
1023+
# ── populate_home_skeleton ─────────────────────
1024+
1025+
section "populate_home_skeleton"
1026+
1027+
# Test: unsafe path rejection (absolute path)
1028+
# Function iterates /home/* which may be empty in CI —
1029+
# Safety checks are inside the per-user loop, so if /home is empty
1030+
# in CI the paths won't be exercised — but the function must not crash
1031+
# the function doesn't crash and doesn't emit "Failed"
1032+
_PHS_TARGET="${TESTDIR}/phs_target"
1033+
mkdir -p "$_PHS_TARGET"
1034+
1035+
run_cmd populate_home_skeleton "$_PHS_TARGET" "/etc/passwd"
1036+
assert_eq "absolute path does not crash" "0" "$_rc"
1037+
assert_not_contains "absolute path no failure" "Failed" "$_out"
1038+
1039+
# Test: unsafe path rejection (path traversal)
1040+
run_cmd populate_home_skeleton "$_PHS_TARGET" "../../../etc/shadow"
1041+
assert_eq "traversal does not crash" "0" "$_rc"
1042+
assert_not_contains "traversal no failure" "Failed" "$_out"
1043+
1044+
# Test: empty copy_files produces skeleton message
1045+
run_cmd populate_home_skeleton "$_PHS_TARGET" ""
1046+
assert_eq "empty copy_files does not crash" "0" "$_rc"
1047+
1048+
# ── orphan home subvolumes in GC ──────────────
1049+
1050+
section "garbage_collect: orphan home subvolumes"
1051+
1052+
ESP="${TESTDIR}/esp_orphan_home"
1053+
BTRFS_MOUNT="${TESTDIR}/btrfs_orphan_home"
1054+
mkdir -p "${ESP}/EFI/Linux" "$BTRFS_MOUNT"
1055+
1056+
make_mock findmnt 'echo "rw,subvol=/root-20250605-120000"'
1057+
make_mock btrfs 'exit 0'
1058+
make_mock mountpoint 'exit 0'
1059+
_ROOT_DEVICE="${TESTDIR}/fake_dev/root_crypt"
1060+
1061+
# Current generation
1062+
touch "${ESP}/EFI/Linux/arch-20250605-120000.efi"
1063+
mkdir -p "${BTRFS_MOUNT}/root-20250605-120000"
1064+
1065+
# Generation with tag "kde"
1066+
touch "${ESP}/EFI/Linux/arch-20250604-100000-kde.efi"
1067+
mkdir -p "${BTRFS_MOUNT}/root-20250604-100000-kde"
1068+
1069+
# Home subvolume matching the tag — NOT orphan
1070+
mkdir -p "${BTRFS_MOUNT}/home-kde"
1071+
1072+
# Orphan home subvolume — no generation with tag "old-test"
1073+
mkdir -p "${BTRFS_MOUNT}/home-old-test"
1074+
1075+
run_cmd garbage_collect 3 0
1076+
assert_contains "orphan home detected" "Orphan home" "$_out"
1077+
assert_contains "orphan mentions tag" "old-test" "$_out"
1078+
assert_not_contains "non-orphan home not flagged" "Orphan home: home-kde" "$_out"
1079+
1080+
# Verify home subvolumes are never deleted (even orphans)
1081+
[[ -d "${BTRFS_MOUNT}/home-old-test" ]] \
1082+
&& ok "orphan home NOT auto-deleted" || fail "orphan home was deleted"
1083+
[[ -d "${BTRFS_MOUNT}/home-kde" ]] \
1084+
&& ok "active home preserved" || fail "active home was deleted"
1085+
9701086

9711087
# ═══════════════════════════════════════════════════════════════
9721088
# RESULTS

‎tests/test_fstab.py‎

Lines changed: 159 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import sys
77
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'lib', 'atomic'))
8-
from fstab import FstabEntry, update_fstab
8+
from fstab import FstabEntry, update_fstab, update_fstab_home
99

1010

1111
class TestFstabEntryParse:
@@ -137,13 +137,14 @@ def test_basic_replacement(self, tmp_path):
137137
assert "root-new" in text
138138
assert "subvol=home" in text
139139

140-
def test_backup_created(self, tmp_path):
140+
def test_backup_cleaned_after_success(self, tmp_path):
141+
"""Backup is created during write but removed on success."""
141142
path = tmp_path / "fstab"
142143
path.write_text("UUID=x / btrfs rw,subvol=root-old 0 0\n")
143144
update_fstab(str(path), "root-old", "root-new")
144145
bak = tmp_path / "fstab.bak"
145-
assert bak.exists()
146-
assert "root-old" in bak.read_text()
146+
assert not bak.exists()
147+
assert "root-new" in path.read_text()
147148

148149
def test_no_root_entry(self, tmp_path):
149150
path = tmp_path / "fstab"
@@ -267,3 +268,157 @@ def test_subvol_mismatch_without_subvolid_generic_error(self, tmp_path, capsys):
267268
assert "subvol=root-old not found" in err
268269
# Must NOT mention subvolid since there is none
269270
assert "subvolid=" not in err
271+
272+
273+
class TestSetSubvol:
274+
"""Tests for FstabEntry.set_subvol() — unconditional subvol replacement."""
275+
276+
def test_basic_replacement(self):
277+
e = FstabEntry.parse("UUID=x /home btrfs rw,subvol=/home 0 0\n")
278+
assert e.set_subvol("home-kde")
279+
assert "subvol=/home-kde" in e.options
280+
281+
def test_preserves_leading_slash(self):
282+
e = FstabEntry.parse("UUID=x /home btrfs rw,subvol=/home 0 0\n")
283+
e.set_subvol("home-kde")
284+
assert "subvol=/home-kde" in e.options
285+
286+
def test_no_leading_slash_preserved(self):
287+
e = FstabEntry.parse("UUID=x /home btrfs rw,subvol=home 0 0\n")
288+
e.set_subvol("home-kde")
289+
assert "subvol=home-kde" in e.options
290+
assert "/home-kde" not in e.options
291+
292+
def test_already_set_no_change(self):
293+
e = FstabEntry.parse("UUID=x /home btrfs rw,subvol=/home-kde 0 0\n")
294+
assert not e.set_subvol("home-kde")
295+
296+
def test_preserves_other_options(self):
297+
e = FstabEntry.parse(
298+
"UUID=x /home btrfs rw,noatime,subvol=/home,compress=zstd 0 0\n"
299+
)
300+
e.set_subvol("home-dev")
301+
opts = e.options.split(",")
302+
assert "rw" in opts
303+
assert "noatime" in opts
304+
assert "compress=zstd" in opts
305+
assert "subvol=/home-dev" in opts
306+
307+
def test_comment_line(self):
308+
e = FstabEntry.parse("# subvol=home\n")
309+
assert not e.set_subvol("home-new")
310+
311+
def test_input_slash_stripped(self):
312+
"""Input with leading slash is normalized."""
313+
e = FstabEntry.parse("UUID=x /home btrfs rw,subvol=/home 0 0\n")
314+
e.set_subvol("/home-kde")
315+
assert "subvol=/home-kde" in e.options
316+
317+
def test_no_subvol_option(self):
318+
"""Entry without subvol= → no change."""
319+
e = FstabEntry.parse("UUID=x /home btrfs rw,noatime 0 0\n")
320+
assert not e.set_subvol("home-kde")
321+
322+
323+
class TestUpdateFstabHome:
324+
"""Tests for update_fstab_home() function."""
325+
326+
def test_basic_replacement(self, tmp_path):
327+
path = tmp_path / "fstab"
328+
path.write_text(
329+
"UUID=x / btrfs rw,subvol=/root-old 0 0\n"
330+
"UUID=x /home btrfs rw,subvol=/home 0 0\n"
331+
)
332+
assert update_fstab_home(str(path), "home-kde")
333+
text = path.read_text()
334+
assert "subvol=/home-kde" in text
335+
# Root entry untouched
336+
assert "subvol=/root-old" in text
337+
338+
def test_no_home_entry(self, tmp_path):
339+
path = tmp_path / "fstab"
340+
path.write_text("UUID=x / btrfs rw,subvol=/root-old 0 0\n")
341+
assert not update_fstab_home(str(path), "home-kde")
342+
343+
def test_no_subvol_in_home(self, tmp_path):
344+
path = tmp_path / "fstab"
345+
path.write_text(
346+
"UUID=x / btrfs rw,subvol=/root-old 0 0\n"
347+
"UUID=y /home ext4 rw,noatime 0 0\n"
348+
)
349+
assert not update_fstab_home(str(path), "home-kde")
350+
351+
def test_already_set(self, tmp_path):
352+
path = tmp_path / "fstab"
353+
path.write_text(
354+
"UUID=x /home btrfs rw,subvol=/home-kde 0 0\n"
355+
)
356+
assert update_fstab_home(str(path), "home-kde")
357+
358+
def test_nonexistent_file(self):
359+
assert not update_fstab_home("/nonexistent/fstab", "home-kde")
360+
361+
def test_empty_subvol_name(self, tmp_path):
362+
path = tmp_path / "fstab"
363+
path.write_text("UUID=x /home btrfs rw,subvol=/home 0 0\n")
364+
assert not update_fstab_home(str(path), "")
365+
366+
def test_invalid_subvol_name(self, tmp_path):
367+
path = tmp_path / "fstab"
368+
path.write_text("UUID=x /home btrfs rw,subvol=/home 0 0\n")
369+
assert not update_fstab_home(str(path), "../etc/passwd")
370+
371+
def test_permissions_preserved(self, tmp_path):
372+
path = tmp_path / "fstab"
373+
path.write_text("UUID=x /home btrfs rw,subvol=/home 0 0\n")
374+
path.chmod(0o644)
375+
update_fstab_home(str(path), "home-kde")
376+
assert path.stat().st_mode & 0o7777 == 0o644
377+
378+
def test_backup_cleaned_up_on_success(self, tmp_path):
379+
path = tmp_path / "fstab"
380+
path.write_text("UUID=x /home btrfs rw,subvol=/home 0 0\n")
381+
update_fstab_home(str(path), "home-kde")
382+
assert not (tmp_path / "fstab.home-bak").exists()
383+
384+
def test_root_untouched(self, tmp_path):
385+
path = tmp_path / "fstab"
386+
path.write_text(
387+
"UUID=x / btrfs rw,subvol=/root-20250601 0 0\n"
388+
"UUID=x /home btrfs rw,subvol=/home 0 0\n"
389+
"UUID=x /var/log btrfs rw,subvol=/log 0 0\n"
390+
)
391+
update_fstab_home(str(path), "home-dev")
392+
text = path.read_text()
393+
assert "subvol=/root-20250601" in text
394+
assert "subvol=/home-dev" in text
395+
assert "subvol=/log" in text
396+
397+
def test_preserves_comments(self, tmp_path):
398+
path = tmp_path / "fstab"
399+
path.write_text(
400+
"# home filesystem\n"
401+
"UUID=x /home btrfs rw,subvol=/home 0 0\n"
402+
)
403+
update_fstab_home(str(path), "home-kde")
404+
text = path.read_text()
405+
assert "# home filesystem" in text
406+
assert "subvol=/home-kde" in text
407+
408+
def test_real_fstab(self, tmp_path):
409+
path = tmp_path / "fstab"
410+
path.write_text(
411+
"# /etc/fstab\n"
412+
"\n"
413+
"UUID=ABCD-1234 /efi vfat rw,relatime 0 2\n"
414+
"UUID=aaaa-bbbb / btrfs rw,noatime,compress=zstd:3,subvol=/root-20250220-141710 0 0\n"
415+
"UUID=aaaa-bbbb /home btrfs rw,noatime,compress=zstd:3,subvol=/home 0 0\n"
416+
"UUID=aaaa-bbbb /var/log btrfs rw,noatime,compress=zstd:3,subvol=/log 0 0\n"
417+
"tmpfs /tmp tmpfs defaults,noatime,mode=1777 0 0\n"
418+
)
419+
assert update_fstab_home(str(path), "home-kde")
420+
text = path.read_text()
421+
assert "subvol=/home-kde" in text
422+
assert "subvol=/root-20250220-141710" in text
423+
assert "subvol=/log" in text
424+
assert "ABCD-1234" in text

0 commit comments

Comments
 (0)