import datetime as dt import importlib.util from pathlib import Path import subprocess import unittest from unittest.mock import patch spec = importlib.util.spec_from_file_location('power_cycle', Path(__file__).resolve().parents[1] / 'scripts/night-power-cycle.py') power = importlib.util.module_from_spec(spec) spec.loader.exec_module(power) class PowerCycleTests(unittest.TestCase): def setUp(self): self.now = dt.datetime(2026, 11, 10, 0, 0, 1, tzinfo=power.TZ) def test_six_am_uses_shanghai_time_even_when_input_is_utc(self): target = power.wake_time(self.now.astimezone(dt.timezone.utc)) self.assertEqual(target.isoformat(), '2026-11-10T06:00:00+08:00') tomorrow = power.wake_time(self.now.replace(hour=18)) self.assertEqual(tomorrow.isoformat(), '2026-11-11T06:00:00+08:00') def test_dry_check_and_daytime_run_never_shutdown(self): with patch.object(power.shutil, 'which', side_effect=lambda name: '/usr/bin/' + name), \ patch.object(Path, 'exists', return_value=True), \ patch.object(power.subprocess, 'run') as run: power.power_cycle(self.now, check=True) run.assert_not_called() with self.assertRaises(RuntimeError): power.power_cycle(self.now.replace(hour=18)) run.assert_not_called() def test_shutdown_only_after_alarm_is_armed_and_verified(self): target = power.wake_time(self.now) with patch.object(power.shutil, 'which', side_effect=lambda name: '/usr/bin/' + name), \ patch.object(Path, 'exists', return_value=True), \ patch.object(Path, 'read_text', return_value=str(int(target.timestamp()))), \ patch.object(power.subprocess, 'run') as run: power.power_cycle(self.now) self.assertEqual(run.call_count, 2) self.assertIn('no', run.call_args_list[0].args[0]) self.assertIn(str(int(target.timestamp())), run.call_args_list[0].args[0]) self.assertEqual(run.call_args_list[1].args[0], ['/usr/bin/systemctl', '--no-block', 'poweroff']) def test_failed_or_wrong_alarm_prevents_shutdown(self): with patch.object(power.shutil, 'which', side_effect=lambda name: '/usr/bin/' + name), \ patch.object(Path, 'exists', return_value=True), \ patch.object(Path, 'read_text', return_value='0'), \ patch.object(power.subprocess, 'run') as run: with self.assertRaises(RuntimeError): power.power_cycle(self.now) self.assertEqual(run.call_count, 1) run.reset_mock() run.side_effect = subprocess.CalledProcessError(1, 'rtcwake') with self.assertRaises(subprocess.CalledProcessError): power.power_cycle(self.now) self.assertEqual(run.call_count, 1)