| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174 |
- """当天失败自动重试判定与循环逻辑测试。"""
- from __future__ import annotations
- import unittest
- from datetime import date
- from unittest.mock import patch
- from app.scheduler import (
- _festival_demand_needs_retry,
- _gap_script_demand_needs_retry,
- _is_write_success,
- _run_with_same_day_retry,
- _should_run_immediately_on_startup,
- )
- class SameDayRetryDecisionTest(unittest.TestCase):
- def test_write_success_when_mysql_saved(self) -> None:
- self.assertTrue(
- _is_write_success({"mysql_save": {"skipped": False, "saved_count": 3}})
- )
- def test_write_success_when_partition_exists(self) -> None:
- self.assertTrue(_is_write_success({"skip_reason": "partition_data_exists"}))
- def test_festival_retry_on_missing_source(self) -> None:
- self.assertTrue(_festival_demand_needs_retry({"skip_reason": "no_demand_names"}))
- def test_festival_no_retry_when_no_active_festivals(self) -> None:
- self.assertFalse(
- _festival_demand_needs_retry({"skip_reason": "no_active_festivals"})
- )
- def test_festival_no_retry_when_nothing_to_write(self) -> None:
- self.assertFalse(_festival_demand_needs_retry({"mysql_save": {}}))
- def test_gap_retry_on_missing_source(self) -> None:
- self.assertTrue(_gap_script_demand_needs_retry({"skip_reason": "no_demand_names"}))
- def test_gap_no_retry_when_no_matched(self) -> None:
- self.assertFalse(
- _gap_script_demand_needs_retry({"skip_reason": "no_matched_demands"})
- )
- class SameDayRetryLoopTest(unittest.TestCase):
- def test_retries_until_write_success(self) -> None:
- calls = {"n": 0}
- summaries = [
- {"skip_reason": "no_demand_names", "mysql_save": {}},
- {"mysql_save": {"skipped": False, "saved_count": 1}},
- ]
- def run_once_fn() -> dict:
- idx = calls["n"]
- calls["n"] += 1
- return summaries[idx]
- printed: list[dict] = []
- with patch("app.scheduler.time.sleep") as sleep_mock, patch(
- "app.scheduler._today_shanghai",
- return_value=date(2026, 8, 12),
- ):
- _run_with_same_day_retry(
- job_name="festival_demand",
- query_date=date(2026, 8, 12),
- run_once_fn=run_once_fn,
- needs_retry=_festival_demand_needs_retry,
- print_summary=printed.append,
- interval_seconds=600,
- )
- self.assertEqual(calls["n"], 2)
- sleep_mock.assert_called_once_with(600)
- def test_retries_on_exception_then_succeeds(self) -> None:
- calls = {"n": 0}
- def run_once_fn() -> dict:
- calls["n"] += 1
- if calls["n"] == 1:
- raise RuntimeError("boom")
- return {"mysql_save": {"skipped": False, "saved_count": 2}}
- with patch("app.scheduler.time.sleep") as sleep_mock, patch(
- "app.scheduler._today_shanghai",
- return_value=date(2026, 8, 12),
- ):
- _run_with_same_day_retry(
- job_name="gap_script_demand",
- query_date=date(2026, 8, 12),
- run_once_fn=run_once_fn,
- needs_retry=_gap_script_demand_needs_retry,
- print_summary=lambda _summary: None,
- interval_seconds=600,
- )
- self.assertEqual(calls["n"], 2)
- sleep_mock.assert_called_once_with(600)
- def test_stops_when_day_changes(self) -> None:
- calls = {"n": 0}
- # 第一次判定仍是当天 → sleep;醒来后跨日 → 停止
- today_values = [date(2026, 8, 12), date(2026, 8, 13)]
- def run_once_fn() -> dict:
- calls["n"] += 1
- return {"skip_reason": "no_demand_names", "mysql_save": {}}
- with patch("app.scheduler.time.sleep") as sleep_mock, patch(
- "app.scheduler._today_shanghai",
- side_effect=today_values,
- ):
- _run_with_same_day_retry(
- job_name="festival_demand",
- query_date=date(2026, 8, 12),
- run_once_fn=run_once_fn,
- needs_retry=_festival_demand_needs_retry,
- print_summary=lambda _summary: None,
- interval_seconds=600,
- )
- self.assertEqual(calls["n"], 1)
- sleep_mock.assert_called_once_with(600)
- class StartupImmediateRunTest(unittest.TestCase):
- def test_skip_startup_when_today_already_succeeded(self) -> None:
- with patch(
- "app.scheduler._has_today_output_data",
- return_value=True,
- ), patch(
- "app.scheduler._today_shanghai",
- return_value=date(2026, 8, 12),
- ):
- should_run = _should_run_immediately_on_startup(
- strategy="去年同期阳历-节点事件",
- repository_cls=object,
- job_name="festival_demand",
- )
- self.assertFalse(should_run)
- def test_startup_run_when_today_not_succeeded(self) -> None:
- with patch(
- "app.scheduler._has_today_output_data",
- return_value=False,
- ), patch(
- "app.scheduler._today_shanghai",
- return_value=date(2026, 8, 12),
- ):
- should_run = _should_run_immediately_on_startup(
- strategy="当下供需gap-脚本主驱",
- repository_cls=object,
- job_name="gap_script_demand",
- )
- self.assertTrue(should_run)
- def test_startup_run_when_check_fails(self) -> None:
- with patch(
- "app.scheduler._has_today_output_data",
- side_effect=RuntimeError("db down"),
- ):
- should_run = _should_run_immediately_on_startup(
- strategy="当下供需gap-脚本主驱",
- repository_cls=object,
- job_name="gap_script_demand",
- )
- self.assertTrue(should_run)
- if __name__ == "__main__":
- unittest.main()
|