119 lines
7.4 KiB
Python
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()
|