"""Tests for the backup_system_file helper in the archsetup installer. backup_system_file snapshots a pre-existing system file to `.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()