1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
|
"""Tests for the backup_system_file helper in the archsetup installer.
backup_system_file snapshots a pre-existing system file to
`<path>.archsetup.bak` before archsetup edits it in place, so a botched
in-place edit (fstab, mkinitcpio.conf, sudoers, ...) is recoverable. It is
idempotent: it never overwrites an existing backup, so the pristine original
survives repeated edits within a run and across re-runs of the installer. It
no-ops (success) when the target does not exist.
These tests exercise the REAL function body, extracted from the `archsetup`
script at run time (not a copy), so the production code path is what runs.
Edits run against real temp files the test creates and tears down.
Run from repo root:
python3 -m unittest tests.backup-system-file.test_backup_system_file
"""
import os
import shutil
import stat
import subprocess
import tempfile
import unittest
REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
ARCHSETUP = os.path.join(REPO_ROOT, "archsetup")
class BackupHarness(unittest.TestCase):
"""Source backup_system_file out of the real archsetup script and invoke it."""
def setUp(self):
self.tmp = tempfile.mkdtemp(prefix="backup-system-file-test-")
# A bash wrapper that extracts just the backup_system_file function from
# the real installer and invokes it with the test's arg. Sourcing the
# sed-extracted function means we test the production code path, not a
# reimplementation. The helper is self-contained (prints its own
# warnings), so no logger stub is needed.
self.wrapper = os.path.join(self.tmp, "run.sh")
with open(self.wrapper, "w") as f:
f.write(
"#!/bin/bash\n"
'ARCHSETUP="$1"; shift\n'
"source <(sed -n '/^backup_system_file() {/,/^}/p' \"$ARCHSETUP\")\n"
'backup_system_file "$@"\n'
)
os.chmod(self.wrapper, 0o755)
def tearDown(self):
# Restore writability in case a test made a dir read-only.
for root, dirs, _ in os.walk(self.tmp):
for d in dirs:
os.chmod(os.path.join(root, d), 0o755)
shutil.rmtree(self.tmp, ignore_errors=True)
def run_backup(self, target):
return subprocess.run(
["bash", self.wrapper, ARCHSETUP, target],
capture_output=True, text=True, timeout=10,
)
def write(self, name, content, mode=None):
path = os.path.join(self.tmp, name)
with open(path, "w") as f:
f.write(content)
if mode is not None:
os.chmod(path, mode)
return path
# -----------------------------------------------------------------------------
# Normal cases
# -----------------------------------------------------------------------------
class TestBackupNormal(BackupHarness):
def test_existing_file_is_backed_up_with_same_content(self):
target = self.write("fstab", "UUID=abc / ext4 defaults 0 1\n")
result = self.run_backup(target)
self.assertEqual(result.returncode, 0, msg=result.stderr)
backup = target + ".archsetup.bak"
self.assertTrue(os.path.isfile(backup), "backup should be created")
with open(backup) as f:
self.assertEqual(f.read(), "UUID=abc / ext4 defaults 0 1\n")
def test_backup_preserves_mode(self):
# sudoers ships 0440; a restored backup must keep restrictive perms.
target = self.write("sudoers", "root ALL=(ALL) ALL\n", mode=0o440)
result = self.run_backup(target)
self.assertEqual(result.returncode, 0, msg=result.stderr)
backup = target + ".archsetup.bak"
self.assertEqual(stat.S_IMODE(os.stat(backup).st_mode), 0o440)
# -----------------------------------------------------------------------------
# Boundary cases
# -----------------------------------------------------------------------------
class TestBackupBoundary(BackupHarness):
def test_existing_backup_is_not_overwritten(self):
# The pristine original must survive a later edit + second backup call.
target = self.write("pacman.conf", "PRISTINE\n")
self.assertEqual(self.run_backup(target).returncode, 0)
# Simulate archsetup editing the file in place, then backing up again.
with open(target, "w") as f:
f.write("EDITED\n")
result = self.run_backup(target)
self.assertEqual(result.returncode, 0, msg=result.stderr)
with open(target + ".archsetup.bak") as f:
self.assertEqual(f.read(), "PRISTINE\n", "backup must stay pristine")
def test_missing_target_is_a_quiet_noop(self):
target = os.path.join(self.tmp, "never-existed.conf")
result = self.run_backup(target)
self.assertEqual(result.returncode, 0, msg=result.stderr)
self.assertFalse(os.path.exists(target + ".archsetup.bak"))
def test_second_call_same_run_is_a_noop(self):
# A file edited twice in one run (e.g. mkinitcpio MODULES then HOOKS)
# gets backed up once; the second call must not error or re-copy.
target = self.write("mkinitcpio.conf", "HOOKS=(base udev)\n")
self.assertEqual(self.run_backup(target).returncode, 0)
backup = target + ".archsetup.bak"
first_mtime = os.stat(backup).st_mtime_ns
result = self.run_backup(target)
self.assertEqual(result.returncode, 0, msg=result.stderr)
self.assertEqual(os.stat(backup).st_mtime_ns, first_mtime,
"backup must not be rewritten on the second call")
# -----------------------------------------------------------------------------
# Error cases
# -----------------------------------------------------------------------------
class TestBackupErrors(BackupHarness):
def test_empty_target_is_refused(self):
result = self.run_backup("")
self.assertNotEqual(result.returncode, 0)
def test_copy_failure_returns_nonzero(self):
# Target exists but its directory is read-only, so the .bak can't be
# written. The helper must report failure rather than silently skip.
subdir = os.path.join(self.tmp, "ro")
os.makedirs(subdir)
target = os.path.join(subdir, "fstab")
with open(target, "w") as f:
f.write("data\n")
os.chmod(subdir, 0o500) # r-x: owner cannot create the .bak here
try:
result = self.run_backup(target)
finally:
os.chmod(subdir, 0o755)
self.assertNotEqual(result.returncode, 0)
self.assertFalse(os.path.exists(target + ".archsetup.bak"))
if __name__ == "__main__":
unittest.main()
|