test_same_day_retry.py 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. """当天失败自动重试判定与循环逻辑测试。"""
  2. from __future__ import annotations
  3. import unittest
  4. from datetime import date
  5. from unittest.mock import patch
  6. from app.scheduler import (
  7. _festival_demand_needs_retry,
  8. _gap_script_demand_needs_retry,
  9. _is_write_success,
  10. _run_with_same_day_retry,
  11. _should_run_immediately_on_startup,
  12. )
  13. class SameDayRetryDecisionTest(unittest.TestCase):
  14. def test_write_success_when_mysql_saved(self) -> None:
  15. self.assertTrue(
  16. _is_write_success({"mysql_save": {"skipped": False, "saved_count": 3}})
  17. )
  18. def test_write_success_when_partition_exists(self) -> None:
  19. self.assertTrue(_is_write_success({"skip_reason": "partition_data_exists"}))
  20. def test_festival_retry_on_missing_source(self) -> None:
  21. self.assertTrue(_festival_demand_needs_retry({"skip_reason": "no_demand_names"}))
  22. def test_festival_no_retry_when_no_active_festivals(self) -> None:
  23. self.assertFalse(
  24. _festival_demand_needs_retry({"skip_reason": "no_active_festivals"})
  25. )
  26. def test_festival_no_retry_when_nothing_to_write(self) -> None:
  27. self.assertFalse(_festival_demand_needs_retry({"mysql_save": {}}))
  28. def test_gap_retry_on_missing_source(self) -> None:
  29. self.assertTrue(_gap_script_demand_needs_retry({"skip_reason": "no_demand_names"}))
  30. def test_gap_no_retry_when_no_matched(self) -> None:
  31. self.assertFalse(
  32. _gap_script_demand_needs_retry({"skip_reason": "no_matched_demands"})
  33. )
  34. class SameDayRetryLoopTest(unittest.TestCase):
  35. def test_retries_until_write_success(self) -> None:
  36. calls = {"n": 0}
  37. summaries = [
  38. {"skip_reason": "no_demand_names", "mysql_save": {}},
  39. {"mysql_save": {"skipped": False, "saved_count": 1}},
  40. ]
  41. def run_once_fn() -> dict:
  42. idx = calls["n"]
  43. calls["n"] += 1
  44. return summaries[idx]
  45. printed: list[dict] = []
  46. with patch("app.scheduler.time.sleep") as sleep_mock, patch(
  47. "app.scheduler._today_shanghai",
  48. return_value=date(2026, 8, 12),
  49. ):
  50. _run_with_same_day_retry(
  51. job_name="festival_demand",
  52. query_date=date(2026, 8, 12),
  53. run_once_fn=run_once_fn,
  54. needs_retry=_festival_demand_needs_retry,
  55. print_summary=printed.append,
  56. interval_seconds=600,
  57. )
  58. self.assertEqual(calls["n"], 2)
  59. sleep_mock.assert_called_once_with(600)
  60. def test_retries_on_exception_then_succeeds(self) -> None:
  61. calls = {"n": 0}
  62. def run_once_fn() -> dict:
  63. calls["n"] += 1
  64. if calls["n"] == 1:
  65. raise RuntimeError("boom")
  66. return {"mysql_save": {"skipped": False, "saved_count": 2}}
  67. with patch("app.scheduler.time.sleep") as sleep_mock, patch(
  68. "app.scheduler._today_shanghai",
  69. return_value=date(2026, 8, 12),
  70. ):
  71. _run_with_same_day_retry(
  72. job_name="gap_script_demand",
  73. query_date=date(2026, 8, 12),
  74. run_once_fn=run_once_fn,
  75. needs_retry=_gap_script_demand_needs_retry,
  76. print_summary=lambda _summary: None,
  77. interval_seconds=600,
  78. )
  79. self.assertEqual(calls["n"], 2)
  80. sleep_mock.assert_called_once_with(600)
  81. def test_stops_when_day_changes(self) -> None:
  82. calls = {"n": 0}
  83. # 第一次判定仍是当天 → sleep;醒来后跨日 → 停止
  84. today_values = [date(2026, 8, 12), date(2026, 8, 13)]
  85. def run_once_fn() -> dict:
  86. calls["n"] += 1
  87. return {"skip_reason": "no_demand_names", "mysql_save": {}}
  88. with patch("app.scheduler.time.sleep") as sleep_mock, patch(
  89. "app.scheduler._today_shanghai",
  90. side_effect=today_values,
  91. ):
  92. _run_with_same_day_retry(
  93. job_name="festival_demand",
  94. query_date=date(2026, 8, 12),
  95. run_once_fn=run_once_fn,
  96. needs_retry=_festival_demand_needs_retry,
  97. print_summary=lambda _summary: None,
  98. interval_seconds=600,
  99. )
  100. self.assertEqual(calls["n"], 1)
  101. sleep_mock.assert_called_once_with(600)
  102. class StartupImmediateRunTest(unittest.TestCase):
  103. def test_skip_startup_when_today_already_succeeded(self) -> None:
  104. with patch(
  105. "app.scheduler._has_today_output_data",
  106. return_value=True,
  107. ), patch(
  108. "app.scheduler._today_shanghai",
  109. return_value=date(2026, 8, 12),
  110. ):
  111. should_run = _should_run_immediately_on_startup(
  112. strategy="去年同期阳历-节点事件",
  113. repository_cls=object,
  114. job_name="festival_demand",
  115. )
  116. self.assertFalse(should_run)
  117. def test_startup_run_when_today_not_succeeded(self) -> None:
  118. with patch(
  119. "app.scheduler._has_today_output_data",
  120. return_value=False,
  121. ), patch(
  122. "app.scheduler._today_shanghai",
  123. return_value=date(2026, 8, 12),
  124. ):
  125. should_run = _should_run_immediately_on_startup(
  126. strategy="当下供需gap-脚本主驱",
  127. repository_cls=object,
  128. job_name="gap_script_demand",
  129. )
  130. self.assertTrue(should_run)
  131. def test_startup_run_when_check_fails(self) -> None:
  132. with patch(
  133. "app.scheduler._has_today_output_data",
  134. side_effect=RuntimeError("db down"),
  135. ):
  136. should_run = _should_run_immediately_on_startup(
  137. strategy="当下供需gap-脚本主驱",
  138. repository_cls=object,
  139. job_name="gap_script_demand",
  140. )
  141. self.assertTrue(should_run)
  142. if __name__ == "__main__":
  143. unittest.main()