Fiw-Gentoo-Dots/tests/test_restoration.py

119 lines
7.4 KiB
Python

import importlib.util
import json
import subprocess
import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch
spec = importlib.util.spec_from_file_location('rice', Path(__file__).resolve().parents[1] / 'lib/rice.py')
rice = importlib.util.module_from_spec(spec)
spec.loader.exec_module(rice)
class RestorationTests(unittest.TestCase):
def test_app_capture_uses_explicit_preferences_and_omits_accounts_and_paths(self):
import capture
with tempfile.TemporaryDirectory() as temp:
root = Path(temp)
home, repo = root / 'home', root / 'repo'
source = home / '.local/share/PrismLauncher/prismlauncher.cfg'
source.parent.mkdir(parents=True)
source.write_text('[General]\nApplicationTheme=Breeze\nIconTheme=breeze_dark\nSelectedInstance=local-instance\nJavaPath=/device/java\nModrinthToken=local-only\n[Accounts]\nuser=local-user\n')
with patch.object(capture, 'HOME', home), patch.object(capture, 'REPO', repo):
capture.capture_apps()
result = (repo / 'configs/prism/kconfig/.local/share/PrismLauncher/prismlauncher.cfg').read_text()
self.assertEqual(result, '[General]\nApplicationTheme=Breeze\nIconTheme=breeze_dark\n')
def test_config_requirements_do_not_select_packages_and_report_actual_missing_apps(self):
selection = rice.load_selection()
selection.update(groups=[], configs=['kitty'], extras=[], bootloader='keep')
with patch.object(rice.shutil, 'which', return_value='portageq'), patch.object(rice.subprocess, 'run', return_value=subprocess.CompletedProcess([], 1)):
requirements = rice.config_requirements(selection)
self.assertEqual(requirements['kitty'], [{'package': 'x11-terms/kitty', 'status': 'missing'}])
self.assertEqual(rice.package_map(selection), {})
selection['groups'] = ['fiw-apps']
self.assertEqual(rice.config_requirements(selection)['kitty'][0]['status'], 'selected')
self.assertEqual(rice.config_requirements(selection, planned=False)['kitty'][0]['status'], 'missing')
def test_backup_restore_preserves_current_file_and_future_updates_preserve_restored_edits(self):
selection = rice.load_selection()
selection.update(groups=[], configs=['fastfetch'])
with tempfile.TemporaryDirectory() as temp, patch.object(rice.backups.os, 'geteuid', return_value=1000):
home = Path(temp)
target = home / '.config/fastfetch/config.jsonc'
target.parent.mkdir(parents=True)
target.write_text('original personal config\n')
rice.apply_configs(selection, home, conflict='apply')
current = target.read_text()
choices = rice.backups.list_backups(home, rice.backup_paths())
self.assertEqual(len(choices), 1)
report = rice.backups.restore(home, choices[0]['id'], rice.backup_paths(), lambda *args: 'restore', lambda *args: False, conflict='apply')
self.assertEqual(target.read_text(), 'original personal config\n')
saved = rice.backups.user_state(home) / 'backups' / report['current_backup'] / '.config/fastfetch/config.jsonc'
self.assertEqual(saved.read_text(), current)
rice.apply_configs(selection, home, update=True, conflict='apply')
self.assertEqual(target.read_text(), 'original personal config\n')
self.assertTrue(Path(str(target) + '.new').is_file())
def test_backup_preview_rejects_unknown_paths_and_destinations_outside_home(self):
with tempfile.TemporaryDirectory() as temp:
root = Path(temp)
home, outside = root / 'home', root / 'outside'
home.mkdir(); outside.mkdir()
backup_id = '20260101T000000.000000Z'
backup = rice.backups.user_state(home) / 'backups' / backup_id
source = backup / '.config/fastfetch/config.jsonc'
source.parent.mkdir(parents=True); source.write_text('previous')
(home / '.config').symlink_to(outside, target_is_directory=True)
with self.assertRaises(RuntimeError):
rice.backups.plan(home, backup_id, rice.backup_paths())
(home / '.config').unlink()
(backup / 'unrecognized').write_text('unrecognized')
with self.assertRaises(RuntimeError):
rice.backups.plan(home, backup_id, rice.backup_paths())
self.assertEqual(list(outside.iterdir()), [])
def test_shared_config_backup_keeps_original_before_all_selected_patches(self):
selection = rice.load_selection()
selection['configs'] = ['kde-style', 'kde-shortcuts']
with tempfile.TemporaryDirectory() as temp:
root = Path(temp); home = root / 'home'; home.mkdir()
relative = Path('.config/kwinrc')
target = home / relative; target.parent.mkdir()
original = '[Appearance]\ncolour=orange\n[Desktops]\nNumber=4\n'
target.write_text(original)
style, shortcuts = root / 'style', root / 'shortcuts'
style.write_text('[Appearance]\ncolour=purple\n')
shortcuts.write_text('[Desktops]\nNumber=1\n')
entries = [('kde-style', 'kconfig', style, relative), ('kde-shortcuts', 'kconfig', shortcuts, relative)]
with patch.object(rice, 'config_entries', return_value=entries):
rice.apply_configs(selection, home, conflict='apply')
saved = list((rice.backups.user_state(home) / 'backups').rglob('kwinrc'))
self.assertEqual(len(saved), 1)
self.assertEqual(saved[0].read_text(), original)
self.assertIn('colour=purple', target.read_text())
self.assertIn('Number=1', target.read_text())
def test_combined_report_correlates_scopes_and_excludes_unrelated_runs(self):
selection = rice.load_selection()
selection.update(groups=['gaming'], configs=['fastfetch'], flatpaks=['org.vinegarhq.Sober'], services=['audio'])
with tempfile.TemporaryDirectory() as temp:
root = Path(temp); home = root / 'home'; home.mkdir()
with patch.object(rice.reporting, 'SYSTEM_STATE', root / 'system'):
with patch.object(rice.reporting.os, 'geteuid', return_value=0):
rice.reporting.record(selection, 'current', 'packages', home, {'present': ['app-misc/fastfetch'], 'skipped': ['games-util/example'], 'missing': ['games-util/example']})
with patch.object(rice.reporting.os, 'geteuid', return_value=1000):
rice.reporting.record(selection, 'current', 'flatpaks', home, {'failed': ['org.vinegarhq.Sober']}, RuntimeError('download failed'))
rice.reporting.record(selection, 'different', 'configs', home, {'applied': ['unrelated-file']})
result = rice.reporting.summarize(selection, home, {}, ['manual reminder'], 'current')
self.assertEqual(result['steps']['packages']['status'], 'partial')
self.assertEqual(result['steps']['flatpaks']['status'], 'failed')
self.assertEqual(result['steps']['configs']['status'], 'not attempted')
self.assertEqual(result['steps']['services-user']['status'], 'not attempted')
self.assertNotIn('unrelated-file', json.dumps(result))
self.assertEqual(result['manual_steps'], ['manual reminder'])
if __name__ == '__main__':
unittest.main()