Dotfiles_ArchAP/tests/test_appearance_app_sync.py

164 lines
7.8 KiB
Python

"""Contrats de validation et publication du synchroniseur Appearance."""
import importlib.util
import json
from pathlib import Path
import tempfile
import unittest
from unittest import mock
SOURCE = Path(__file__).resolve().parents[1] / 'bin/.local/bin/appearance-app-sync.py'
spec = importlib.util.spec_from_file_location('appearance_app_sync', SOURCE)
sync = importlib.util.module_from_spec(spec)
spec.loader.exec_module(sync)
class SyncTest(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.addCleanup(self.tmp.cleanup)
self.root = Path(self.tmp.name)
self.state = self.root / 'effective.json'
self.result = self.root / 'last-applied.json'
self.palette = sync.PALETTE
self.value = {'effectiveFlavor': 'mocha', 'effectiveAccent': 'lavender',
'effectiveDark': True, 'effectiveHighContrast': False,
'effectiveMode': 'normal', 'revision': 4,
'latitude': 48.0, 'savedLocations': ['secret']}
self.write()
def write(self):
self.state.write_text(json.dumps(self.value), encoding='utf-8')
def test_projection_excludes_location(self):
state = sync.read_effective(self.state)
self.assertEqual(set(state), {'effectiveFlavor', 'effectiveAccent',
'effectiveDark', 'effectiveHighContrast',
'effectiveMode', 'revision'})
def test_bad_state_never_runs_adapter_or_publishes(self):
for mutation in ({'revision': None}, {'effectiveFlavor': 'invalid'},
{'effectiveDark': False}, {'effectiveMode': 'sun-light'}):
with self.subTest(mutation=mutation):
self.value.update(mutation)
self.write()
with mock.patch.object(sync, 'ADAPTERS', {'gtk': mock.Mock()}) as adapters:
with self.assertRaises(ValueError):
sync.reconcile(self.state, self.result, self.palette)
adapters['gtk'].assert_not_called()
self.assertFalse(self.result.exists())
self.value = {'effectiveFlavor': 'mocha', 'effectiveAccent': 'lavender',
'effectiveDark': True, 'effectiveHighContrast': False,
'effectiveMode': 'normal', 'revision': 4}
del self.value['revision']
self.write()
with self.assertRaises(KeyError):
sync.reconcile(self.state, self.result, self.palette)
self.assertFalse(self.result.exists())
def test_partial_failure_and_privacy(self):
def failed(*args):
raise OSError('indisponible')
with mock.patch.object(sync, 'ADAPTERS', {'gtk': failed,
'qt': lambda *_: {'status': 'ok'}}):
result = sync.reconcile(self.state, self.result, self.palette)
self.assertEqual(result['adapters']['gtk']['status'], 'error')
self.assertEqual(result['adapters']['qt']['status'], 'ok')
content = self.result.read_text()
self.assertNotIn('latitude', content)
self.assertNotIn('secret', content)
def test_new_revision_wins_during_adapter_execution(self):
seen = []
def mutate_once(state, colors):
seen.append(state['revision'])
if len(seen) == 1:
self.value['revision'] = 5
self.write()
return {'status': 'ok'}
with mock.patch.object(sync, 'ADAPTERS', {'gtk': mutate_once}):
result = sync.reconcile(self.state, self.result, self.palette)
self.assertEqual(seen, [4, 5])
self.assertEqual(result['revision'], 5)
def test_missing_palette_does_not_replace_config(self):
with self.assertRaises(FileNotFoundError):
sync.reconcile(self.state, self.result, self.root / 'missing.json')
self.assertFalse(self.result.exists())
def test_qt_kitty_palette_is_shared(self):
p = sync.read_palette()['latte']
self.assertIn(p['base'].encode(), sync.kitty_theme(p, 'lavender', True))
self.assertIn(('#ff' + p['base'][1:]).encode(), sync.qt_palette(p, 'lavender'))
self.assertEqual(len(sync.qt_palette(p, 'lavender').splitlines()), 4)
def test_gtk_adapter_uses_effective_dark(self):
self.value.update(effectiveFlavor='latte', effectiveDark=False)
with mock.patch.object(sync, 'run') as run:
result = sync.gtk_adapter(self.value, sync.read_palette())
self.assertEqual(result['colorScheme'], 'prefer-light')
self.assertEqual(result['theme'], 'Adwaita')
self.assertEqual(run.call_count, 2)
def test_qt_adapter_writes_both_versions(self):
for version in ('qt5ct', 'qt6ct'):
config = self.root / version / (version + '.conf')
config.parent.mkdir(parents=True)
config.write_text('[Appearance]\ncolor_scheme_path=/old\nstyle=Fusion\n')
with mock.patch.object(sync, 'CONFIG', self.root):
sync.qt_adapter(self.value, sync.read_palette())
for version in ('qt5ct', 'qt6ct'):
self.assertTrue((self.root / version / 'colors/labfy-appearance.conf').is_file())
content = (self.root / version / (version + '.conf')).read_text()
self.assertIn('style=Fusion', content)
self.assertIn('labfy-appearance.conf', content)
def test_kitty_adapter_signals_only_verified_process(self):
kitty = self.root / 'kitty'
kitty.mkdir()
(kitty / 'kitty.conf').write_text('include generated-appearance.conf\n')
with mock.patch.object(sync, 'CONFIG', self.root), \
mock.patch.object(sync.os, 'scandir', return_value=[]):
result = sync.kitty_adapter(self.value, sync.read_palette())
self.assertEqual(result['instances'], 0)
self.assertIn('foreground', (kitty / 'generated-appearance.conf').read_text())
def test_nvim_contract_reports_revision(self):
self.assertEqual(sync.nvim_adapter(self.value, {})['revision'], 4)
def test_firefox_profile_is_read_only_and_reports_migration(self):
firefox = self.root / '.mozilla/firefox'
firefox.mkdir(parents=True)
(firefox / 'profiles.ini').write_text('[InstallABCD]\nDefault=profile\n')
profile = firefox / 'profile'
profile.mkdir()
prefs = profile / 'prefs.js'
prefs.write_text('user_pref("extensions.activeThemeID", "firefox-compact-dark@mozilla.org");\n'
'user_pref("layout.css.prefers-color-scheme.content-override", 1);\n')
before = prefs.read_bytes()
with mock.patch.object(sync.Path, 'home', return_value=self.root):
result = sync.firefox_adapter(self.value, {})
self.assertEqual(result['status'], 'unsupported')
self.assertEqual(result['contentOverride'], 1)
self.assertEqual(prefs.read_bytes(), before)
def test_firefox_proton_dual_palette_follows_system_without_profile_write(self):
firefox = self.root / '.mozilla/firefox'
firefox.mkdir(parents=True)
(firefox / 'profiles.ini').write_text('[InstallABCD]\nDefault=profile\n')
profile = firefox / 'profile'
profile.mkdir()
prefs = profile / 'prefs.js'
prefs.write_text('user_pref("extensions.activeThemeID", "proton-theme@mozilla.org");\n')
before = prefs.read_bytes()
with mock.patch.object(sync.Path, 'home', return_value=self.root):
result = sync.firefox_adapter(self.value, {})
self.assertEqual(result['status'], 'ok')
self.assertEqual(result['integration'], 'system')
self.assertEqual(result['lightDark'], 'supported')
self.assertIs(result['profileWrite'], False)
self.assertEqual(prefs.read_bytes(), before)
if __name__ == '__main__':
unittest.main()