110 lines
5.3 KiB
Python
110 lines
5.3 KiB
Python
"""Contrats Night Light sur préférences et service simulés, sans localisation réelle."""
|
|
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/night-light-manager.py'
|
|
SPEC = importlib.util.spec_from_file_location('night_light_manager', SOURCE)
|
|
night = importlib.util.module_from_spec(SPEC)
|
|
SPEC.loader.exec_module(night)
|
|
|
|
|
|
class NightLightTest(unittest.TestCase):
|
|
def setUp(self):
|
|
self.temp = tempfile.TemporaryDirectory()
|
|
self.root = Path(self.temp.name)
|
|
self.patchers = [mock.patch.object(night, 'CONFIG', self.root / 'preferences.json'),
|
|
mock.patch.object(night, 'STATE', self.root / 'effective.json'),
|
|
mock.patch.object(night, 'LOCK', self.root / 'wallpaper.lock')]
|
|
for patcher in self.patchers:
|
|
patcher.start()
|
|
self.pref = {'version': 2, 'themeMode': 'manual', 'manualFlavor': 'mocha',
|
|
'wallpaperDirectory': '/fixture', 'nightLightMode': 'auto',
|
|
'nightTemperature': 3000, 'dayTemperature': 6500,
|
|
'scheduleType': 'solar', 'latitude': '12.5', 'longitude': '-8.25'}
|
|
night.atomic_json(night.CONFIG, self.pref)
|
|
night.atomic_json(night.STATE, {'effectiveFlavor': 'mocha', 'revision': 5})
|
|
|
|
def tearDown(self):
|
|
for patcher in reversed(self.patchers):
|
|
patcher.stop()
|
|
self.temp.cleanup()
|
|
|
|
def test_migration_preserves_other_preferences(self):
|
|
result = night.normalized(self.pref)
|
|
self.assertEqual(result['version'], 3)
|
|
self.assertEqual(result['themeMode'], 'manual')
|
|
self.assertEqual(result['wallpaperDirectory'], '/fixture')
|
|
|
|
def test_auto_argv_uses_solar_and_baseline(self):
|
|
self.assertEqual(night.argv(self.pref), ['/usr/bin/wlsunset', '-l', '12.5', '-L', '-8.25',
|
|
'-t', '3000', '-T', '6500'])
|
|
|
|
def test_on_argv_uses_manual_schedule_for_force_low(self):
|
|
pref = {**self.pref, 'nightLightMode': 'on'}
|
|
self.assertEqual(night.argv(pref), ['/usr/bin/wlsunset', '-S', '06:00', '-s', '18:00',
|
|
'-t', '3000', '-T', '6500'])
|
|
|
|
def test_off_has_no_process_argv(self):
|
|
self.assertIsNone(night.argv({**self.pref, 'nightLightMode': 'off'}))
|
|
|
|
def test_invalid_mode_and_temperature(self):
|
|
for mode in ('invalid', '', None):
|
|
with self.subTest(mode=mode), self.assertRaises(ValueError):
|
|
night.normalized({**self.pref, 'nightLightMode': mode})
|
|
for value in ('3500', '', None, float('nan'), 2400, 5100, 3550, True):
|
|
with self.subTest(value=value), self.assertRaises(ValueError):
|
|
night.temperature(value)
|
|
|
|
def test_fresh_install_has_no_invented_coordinates(self):
|
|
self.assertEqual(night.normalized({})['nightLightMode'], 'off')
|
|
with self.assertRaisesRegex(ValueError, 'Planification à configurer'):
|
|
night.normalized({'nightLightMode': 'auto'})
|
|
|
|
def test_atomic_write_keeps_previous_on_replace_failure(self):
|
|
before = night.CONFIG.read_bytes()
|
|
with mock.patch.object(night.os, 'replace', side_effect=OSError('échec')):
|
|
with self.assertRaises(OSError):
|
|
night.atomic_json(night.CONFIG, {'bad': True})
|
|
self.assertEqual(night.CONFIG.read_bytes(), before)
|
|
|
|
def test_change_rolls_back_preferences_and_state(self):
|
|
before_pref = night.CONFIG.read_bytes()
|
|
before_state = night.STATE.read_bytes()
|
|
with mock.patch.object(night, 'apply_service', side_effect=[RuntimeError('service'), None]) as apply:
|
|
with self.assertRaisesRegex(RuntimeError, 'service'):
|
|
night.change(mode='off')
|
|
self.assertEqual(apply.call_count, 2)
|
|
self.assertEqual(night.CONFIG.read_bytes(), before_pref)
|
|
self.assertEqual(night.STATE.read_bytes(), before_state)
|
|
|
|
def test_change_keeps_appearance_fields(self):
|
|
with mock.patch.object(night, 'apply_service'), mock.patch.object(night, 'publish'):
|
|
result = night.change(mode='off')
|
|
self.assertEqual(result['nightLightMode'], 'off')
|
|
self.assertEqual(json.loads(night.STATE.read_text())['effectiveFlavor'], 'mocha')
|
|
self.assertEqual(json.loads(night.CONFIG.read_text())['manualFlavor'], 'mocha')
|
|
|
|
def test_reconcile_repairs_missing_service(self):
|
|
with mock.patch.object(night, 'verify', side_effect=RuntimeError('absent')), \
|
|
mock.patch.object(night, 'unit_state', return_value=(False, 0)), \
|
|
mock.patch.object(night, 'wlsunset_pids', return_value=[]), \
|
|
mock.patch.object(night, 'apply_service') as apply:
|
|
result = night.reconcile()
|
|
apply.assert_called_once()
|
|
self.assertEqual(result['effectiveNightLightMode'], 'auto')
|
|
|
|
def test_future_suspension_preserves_preference(self):
|
|
pref = night.normalized(self.pref)
|
|
state = {'nightLightSuspended': True}
|
|
result = night.snapshot(pref, state)
|
|
self.assertEqual(result['nightLightMode'], 'auto')
|
|
self.assertEqual(result['effectiveNightLightMode'], 'off')
|
|
self.assertEqual(night.effective('on', True), 'off')
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|