from __future__ import annotations import unittest from unittest.mock import MagicMock, patch from mobilede_scraper.worker import self_heal class TestSelfHeal(unittest.TestCase): def test_read_last_progress_ts_uses_global_key(self) -> None: redis_client = MagicMock() redis_client.get.return_value = "1776800000" ts = self_heal._read_last_progress_ts(redis_client) self.assertEqual(ts, 1776800000) redis_client.scan_iter.assert_not_called() def test_read_last_progress_ts_fallbacks_to_task_progress_keys(self) -> None: redis_client = MagicMock() redis_client.get.side_effect = lambda key: { self_heal.GLOBAL_PROGRESS_TS_KEY: None, "mobilede:state:task_progress:a": '{"ts": 100}', "mobilede:state:task_progress:b": '{"ts": 250}', "mobilede:state:task_progress:c": '{"ts": 150}', }.get(key) redis_client.scan_iter.return_value = [ "mobilede:state:task_progress:a", "mobilede:state:task_progress:b", "mobilede:state:task_progress:c", ] ts = self_heal._read_last_progress_ts(redis_client) self.assertEqual(ts, 250) def test_read_last_progress_ts_ignores_broken_payloads(self) -> None: redis_client = MagicMock() redis_client.get.side_effect = lambda key: { self_heal.GLOBAL_PROGRESS_TS_KEY: None, "mobilede:state:task_progress:a": "{bad-json}", "mobilede:state:task_progress:b": '{"foo": "bar"}', }.get(key) redis_client.scan_iter.return_value = [ "mobilede:state:task_progress:a", "mobilede:state:task_progress:b", ] ts = self_heal._read_last_progress_ts(redis_client) self.assertIsNone(ts) def test_has_inflight_work_uses_canonical_segment_locks(self) -> None: redis_client = MagicMock() redis_client.llen.return_value = 0 redis_client.scan_iter.side_effect = [iter(["mobilede:locks:segment:abc"]), iter([])] has_inflight, flags = self_heal._has_inflight_work(redis_client, "mobilede_sync") self.assertTrue(has_inflight) self.assertEqual(flags["has_segment_lock"], 1) @patch("mobilede_scraper.worker.self_heal.time.sleep", return_value=None) @patch("mobilede_scraper.worker.self_heal.os.kill") @patch("builtins.open") def test_kill_worker_process_sends_term_and_kill(self, open_mock, kill_mock, _sleep_mock) -> None: open_mock.return_value.__enter__.return_value.read.return_value = "123" # SIGTERM -> process alive check (pid,0) -> SIGKILL kill_mock.side_effect = [None, None, None] self_heal._kill_worker_process() self.assertEqual(kill_mock.call_args_list[0].args[0], 123) self.assertEqual(kill_mock.call_args_list[1].args, (123, 0)) self.assertEqual(kill_mock.call_args_list[2].args[0], 123) @patch("mobilede_scraper.worker.self_heal.time.sleep", return_value=None) @patch("mobilede_scraper.worker.self_heal.os.kill") @patch("builtins.open") def test_kill_worker_process_skips_sigkill_when_already_exited(self, open_mock, kill_mock, _sleep_mock) -> None: open_mock.return_value.__enter__.return_value.read.return_value = "123" kill_mock.side_effect = [None, ProcessLookupError()] self_heal._kill_worker_process() # Только SIGTERM и проверка существования процесса. self.assertEqual(len(kill_mock.call_args_list), 2) if __name__ == "__main__": unittest.main()