- Dockerfile.agent: fix CMD (python -m src.agent.run), use backend/requirements.txt, minimal COPY (only src.agent + src.core.cot_logger), add GRACE contract - docker-compose.yml: SERVICE_TOKEN_SECRET -> SERVICE_JWT (match code) - docker-compose.enterprise-clean.yml: same env var fix - docker/.env.agent.example: same env var fix - build.sh: same env var fix - chore: semantics-testing SKILL.md, backend tests, pyproject.toml
911 lines
47 KiB
Python
911 lines
47 KiB
Python
# #region Test.Scheduler [C:3] [TYPE Module] [SEMANTICS test,scheduler,core]
|
|
# @BRIEF Unit tests for SchedulerModule — SchedulerService lifecycle, job management, and ThrottledSchedulerConfigurator.
|
|
# @RELATION BINDS_TO -> [SchedulerModule]
|
|
# @TEST_EDGE: empty_queue -> _trigger_backup with no active tasks creates a new task
|
|
# @TEST_EDGE: duplicate_backup -> _trigger_backup skips when backup already running
|
|
# @TEST_EDGE: invalid_cron -> add_backup_job handles invalid cron expression gracefully
|
|
# @TEST_EDGE: translation_load_failure -> load_schedules continues when translation DB query fails
|
|
# @TEST_EDGE: validation_load_failure -> load_schedules continues when validation DB query fails
|
|
# @TEST_EDGE: policy_not_found -> reload_validation_policy removes job when policy missing
|
|
# @TEST_EDGE: policy_inactive -> reload_validation_policy removes job when policy inactive
|
|
# @TEST_EDGE: policy_no_schedule -> reload_validation_policy skips when no schedule_days/window
|
|
# @TEST_EDGE: zero_window -> calculate_schedule returns start_dt repeated for zero-length window
|
|
# @TEST_EDGE: midnight_crossing -> calculate_schedule handles window crossing midnight
|
|
# @TEST_EDGE: single_dashboard -> calculate_schedule returns single start_dt for one dashboard
|
|
import asyncio
|
|
from datetime import date, datetime, time, timedelta
|
|
from types import SimpleNamespace
|
|
from unittest.mock import AsyncMock, MagicMock, patch, PropertyMock
|
|
|
|
import pytest
|
|
|
|
|
|
# ── Helpers (C1 — anchor pair only) ──
|
|
|
|
# #region _build_config_manager [C:1] [TYPE Function]
|
|
def _build_config_manager(environments=None, app_timezone="Europe/Moscow"):
|
|
"""Build a mock ConfigManager with configurable environments."""
|
|
cm = MagicMock()
|
|
config = MagicMock()
|
|
settings = MagicMock()
|
|
settings.app_timezone = app_timezone
|
|
config.settings = settings
|
|
config.environments = environments or []
|
|
cm.get_config.return_value = config
|
|
return cm
|
|
# #endregion _build_config_manager
|
|
|
|
# #region _build_env [C:1] [TYPE Function]
|
|
def _build_env(env_id="env-1", cron="0 2 * * *", enabled=True):
|
|
"""Build a mock Environment with backup_schedule."""
|
|
env = MagicMock()
|
|
env.id = env_id
|
|
sched = MagicMock()
|
|
sched.enabled = enabled
|
|
sched.cron_expression = cron
|
|
env.backup_schedule = sched
|
|
return env
|
|
# #endregion _build_env
|
|
|
|
# #region _build_task [C:1] [TYPE Function]
|
|
def _build_task(plugin_id="superset-backup", status="RUNNING", environment_id="env-1"):
|
|
"""Build a mock TaskRecord."""
|
|
task = MagicMock()
|
|
task.plugin_id = plugin_id
|
|
task.status = status
|
|
task.params = {"environment_id": environment_id}
|
|
return task
|
|
# #endregion _build_task
|
|
|
|
# #region _build_policy [C:1] [TYPE Function]
|
|
def _build_policy(policy_id="pol-1", is_active=True, schedule_days=None, window_start=None, window_end=None, dashboard_ids=None, environment_id="env-1", provider_id="prov-1"):
|
|
"""Build a mock ValidationPolicy."""
|
|
policy = MagicMock()
|
|
policy.id = policy_id
|
|
policy.is_active = is_active
|
|
policy.schedule_days = schedule_days
|
|
policy.window_start = window_start
|
|
policy.window_end = window_end
|
|
policy.dashboard_ids = dashboard_ids
|
|
policy.environment_id = environment_id
|
|
policy.provider_id = provider_id
|
|
return policy
|
|
# #endregion _build_policy
|
|
|
|
|
|
# ── Fixtures ──
|
|
|
|
@pytest.fixture
|
|
def mock_task_manager():
|
|
tm = MagicMock()
|
|
tm.get_tasks.return_value = []
|
|
tm.create_task = AsyncMock(return_value=MagicMock())
|
|
return tm
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_config_manager():
|
|
return _build_config_manager()
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_scheduler():
|
|
"""Patch BackgroundScheduler so no real threads are started."""
|
|
with patch("src.core.scheduler.BackgroundScheduler") as cls:
|
|
sched_instance = MagicMock()
|
|
sched_instance.running = False
|
|
cls.return_value = sched_instance
|
|
yield sched_instance
|
|
|
|
|
|
@pytest.fixture
|
|
def service(mock_task_manager, mock_config_manager, mock_scheduler):
|
|
"""Construct a SchedulerService with all external deps mocked."""
|
|
# Create a fresh event loop to avoid "no current event loop" errors
|
|
# when the scheduler test runs after async tests in the same session.
|
|
loop = asyncio.new_event_loop()
|
|
asyncio.set_event_loop(loop)
|
|
with patch("src.core.scheduler.belief_scope") as mock_bs:
|
|
mock_bs.return_value.__enter__ = MagicMock()
|
|
mock_bs.return_value.__exit__ = MagicMock(return_value=False)
|
|
with patch("src.core.scheduler.asyncio.get_event_loop", return_value=loop):
|
|
from src.core.scheduler import SchedulerService
|
|
svc = SchedulerService(mock_task_manager, mock_config_manager)
|
|
yield svc
|
|
loop.close()
|
|
|
|
|
|
# ══════════════════════════════════════════════════════════════
|
|
# SchedulerService.__init__ (lines 28-32)
|
|
# ══════════════════════════════════════════════════════════════
|
|
|
|
# #region test_init_stores_deps [C:2] [TYPE Function]
|
|
# @BRIEF __init__ stores task_manager, config_manager, creates scheduler and loop.
|
|
def test_init_stores_deps(service, mock_task_manager, mock_config_manager, mock_scheduler):
|
|
assert service.task_manager is mock_task_manager
|
|
assert service.config_manager is mock_config_manager
|
|
assert service.scheduler is mock_scheduler
|
|
assert service.loop is not None
|
|
# #endregion test_init_stores_deps
|
|
|
|
|
|
# ══════════════════════════════════════════════════════════════
|
|
# SchedulerService.start (lines 40-44)
|
|
# ══════════════════════════════════════════════════════════════
|
|
|
|
# #region test_start_when_not_running [C:2] [TYPE Function]
|
|
# @BRIEF start() starts scheduler and loads schedules when not already running.
|
|
def test_start_when_not_running(service, mock_scheduler):
|
|
mock_scheduler.running = False
|
|
with patch("src.core.scheduler.belief_scope") as mock_bs:
|
|
mock_bs.return_value.__enter__ = MagicMock()
|
|
mock_bs.return_value.__exit__ = MagicMock(return_value=False)
|
|
with patch.object(service, "load_schedules") as mock_load:
|
|
service.start()
|
|
mock_scheduler.start.assert_called_once()
|
|
mock_load.assert_called_once()
|
|
# #endregion test_start_when_not_running
|
|
|
|
# #region test_start_when_already_running [C:2] [TYPE Function]
|
|
# @BRIEF start() is a no-op when scheduler is already running.
|
|
def test_start_when_already_running(service, mock_scheduler):
|
|
mock_scheduler.running = True
|
|
with patch("src.core.scheduler.belief_scope") as mock_bs:
|
|
mock_bs.return_value.__enter__ = MagicMock()
|
|
mock_bs.return_value.__exit__ = MagicMock(return_value=False)
|
|
with patch.object(service, "load_schedules") as mock_load:
|
|
service.start()
|
|
mock_scheduler.start.assert_not_called()
|
|
mock_load.assert_not_called()
|
|
# #endregion test_start_when_already_running
|
|
|
|
|
|
# ══════════════════════════════════════════════════════════════
|
|
# SchedulerService.stop (lines 52-55)
|
|
# ══════════════════════════════════════════════════════════════
|
|
|
|
# #region test_stop_when_running [C:2] [TYPE Function]
|
|
# @BRIEF stop() shuts down scheduler when it is running.
|
|
def test_stop_when_running(service, mock_scheduler):
|
|
mock_scheduler.running = True
|
|
with patch("src.core.scheduler.belief_scope") as mock_bs:
|
|
mock_bs.return_value.__enter__ = MagicMock()
|
|
mock_bs.return_value.__exit__ = MagicMock(return_value=False)
|
|
service.stop()
|
|
mock_scheduler.shutdown.assert_called_once()
|
|
# #endregion test_stop_when_running
|
|
|
|
# #region test_stop_when_not_running [C:2] [TYPE Function]
|
|
# @BRIEF stop() is a no-op when scheduler is not running.
|
|
def test_stop_when_not_running(service, mock_scheduler):
|
|
mock_scheduler.running = False
|
|
with patch("src.core.scheduler.belief_scope") as mock_bs:
|
|
mock_bs.return_value.__enter__ = MagicMock()
|
|
mock_bs.return_value.__exit__ = MagicMock(return_value=False)
|
|
service.stop()
|
|
mock_scheduler.shutdown.assert_not_called()
|
|
# #endregion test_stop_when_not_running
|
|
|
|
|
|
# ══════════════════════════════════════════════════════════════
|
|
# SchedulerService.load_schedules (lines 63-130)
|
|
# ══════════════════════════════════════════════════════════════
|
|
|
|
# #region test_load_schedules_backup_jobs [C:2] [TYPE Function]
|
|
# @BRIEF load_schedules registers backup jobs for enabled environments.
|
|
def test_load_schedules_backup_jobs(service, mock_scheduler, mock_config_manager):
|
|
env1 = _build_env("env-1", "0 2 * * *", enabled=True)
|
|
env2 = _build_env("env-2", "0 3 * * *", enabled=False)
|
|
cm = _build_config_manager(environments=[env1, env2])
|
|
service.config_manager = cm
|
|
|
|
with patch("src.core.scheduler.belief_scope") as mock_bs:
|
|
mock_bs.return_value.__enter__ = MagicMock()
|
|
mock_bs.return_value.__exit__ = MagicMock(return_value=False)
|
|
with patch("src.core.scheduler.SessionLocal") as mock_session:
|
|
db_mock = MagicMock()
|
|
mock_session.return_value = db_mock
|
|
# No translation schedules, no validation policies
|
|
db_mock.query.return_value.filter.return_value.all.return_value = []
|
|
with patch.object(service, "add_backup_job") as mock_add:
|
|
service.load_schedules()
|
|
# Only env-1 has enabled schedule
|
|
mock_add.assert_called_once_with("env-1", "0 2 * * *")
|
|
# #endregion test_load_schedules_backup_jobs
|
|
|
|
# #region test_load_schedules_translation_exception [C:2] [TYPE Function]
|
|
# @BRIEF load_schedules catches exception when loading translation schedules (lines 93-94).
|
|
def test_load_schedules_translation_exception(service, mock_config_manager):
|
|
cm = _build_config_manager(environments=[])
|
|
service.config_manager = cm
|
|
|
|
with patch("src.core.scheduler.belief_scope") as mock_bs:
|
|
mock_bs.return_value.__enter__ = MagicMock()
|
|
mock_bs.return_value.__exit__ = MagicMock(return_value=False)
|
|
with patch("src.core.scheduler.SessionLocal", side_effect=RuntimeError("DB connection lost")):
|
|
# Should not raise — exception is caught at line 93
|
|
service.load_schedules()
|
|
# #endregion test_load_schedules_translation_exception
|
|
|
|
# #region test_load_schedules_validation_exception [C:2] [TYPE Function]
|
|
# @BRIEF load_schedules catches exception when loading validation schedules (lines 128-129).
|
|
def test_load_schedules_validation_exception(service, mock_config_manager):
|
|
cm = _build_config_manager(environments=[])
|
|
service.config_manager = cm
|
|
|
|
call_count = 0
|
|
original_session_local = None
|
|
|
|
def session_side_effect():
|
|
nonlocal call_count
|
|
call_count += 1
|
|
if call_count == 1:
|
|
# First call (translation) — return empty mock
|
|
db = MagicMock()
|
|
db.query.return_value.filter.return_value.all.return_value = []
|
|
return db
|
|
else:
|
|
# Second call (validation) — raise
|
|
raise RuntimeError("Validation DB error")
|
|
|
|
with patch("src.core.scheduler.belief_scope") as mock_bs:
|
|
mock_bs.return_value.__enter__ = MagicMock()
|
|
mock_bs.return_value.__exit__ = MagicMock(return_value=False)
|
|
with patch("src.core.scheduler.SessionLocal", side_effect=session_side_effect):
|
|
# Should not raise — exception caught at line 128
|
|
service.load_schedules()
|
|
# #endregion test_load_schedules_validation_exception
|
|
|
|
# #region test_load_schedules_translation_schedules_success [C:2] [TYPE Function]
|
|
# @BRIEF load_schedules registers active translation schedules from DB (lines 82, 90).
|
|
def test_load_schedules_translation_schedules_success(service, mock_config_manager):
|
|
cm = _build_config_manager(environments=[], app_timezone="UTC")
|
|
service.config_manager = cm
|
|
|
|
# Build a mock TranslationSchedule row
|
|
ts = MagicMock()
|
|
ts.id = "ts-1"
|
|
ts.job_id = "job-ts-1"
|
|
ts.cron_expression = "*/10 * * * *"
|
|
ts.timezone = "US/Pacific"
|
|
ts.execution_mode = "incremental"
|
|
|
|
call_count = 0
|
|
|
|
def session_factory():
|
|
nonlocal call_count
|
|
call_count += 1
|
|
db = MagicMock()
|
|
if call_count == 1:
|
|
# Translation query — return one active schedule
|
|
db.query.return_value.filter.return_value.all.return_value = [ts]
|
|
else:
|
|
# Validation query — empty
|
|
db.query.return_value.filter.return_value.all.return_value = []
|
|
return db
|
|
|
|
with patch("src.core.scheduler.belief_scope") as mock_bs:
|
|
mock_bs.return_value.__enter__ = MagicMock()
|
|
mock_bs.return_value.__exit__ = MagicMock(return_value=False)
|
|
with patch("src.core.scheduler.SessionLocal", side_effect=session_factory):
|
|
with patch.object(service, "add_translation_job") as mock_add_t:
|
|
with patch.object(service, "add_validation_job") as mock_add_v:
|
|
service.load_schedules()
|
|
mock_add_t.assert_called_once_with(
|
|
schedule_id="ts-1",
|
|
job_id="job-ts-1",
|
|
cron_expression="*/10 * * * *",
|
|
timezone="US/Pacific",
|
|
execution_mode="incremental",
|
|
)
|
|
mock_add_v.assert_not_called()
|
|
# #endregion test_load_schedules_translation_schedules_success
|
|
|
|
# #region test_load_schedules_validation_policy_skip_empty_days [C:2] [TYPE Function]
|
|
# @BRIEF load_schedules skips validation policies with empty schedule_days (line 112).
|
|
def test_load_schedules_validation_policy_skip_empty_days(service, mock_config_manager):
|
|
cm = _build_config_manager(environments=[], app_timezone="UTC")
|
|
service.config_manager = cm
|
|
|
|
policy_no_days = _build_policy("pol-1", is_active=True, schedule_days=[], window_start=time(10, 0))
|
|
policy_none_days = _build_policy("pol-2", is_active=True, schedule_days=None, window_start=time(10, 0))
|
|
|
|
call_count = 0
|
|
|
|
def session_factory():
|
|
nonlocal call_count
|
|
call_count += 1
|
|
db = MagicMock()
|
|
if call_count == 1:
|
|
# Translation query — empty
|
|
db.query.return_value.filter.return_value.all.return_value = []
|
|
else:
|
|
# Validation query — policies with empty/None schedule_days
|
|
db.query.return_value.filter.return_value.all.return_value = [policy_no_days, policy_none_days]
|
|
return db
|
|
|
|
with patch("src.core.scheduler.belief_scope") as mock_bs:
|
|
mock_bs.return_value.__enter__ = MagicMock()
|
|
mock_bs.return_value.__exit__ = MagicMock(return_value=False)
|
|
with patch("src.core.scheduler.SessionLocal", side_effect=session_factory):
|
|
with patch.object(service, "add_validation_job") as mock_add:
|
|
service.load_schedules()
|
|
# Neither policy should be registered (both have empty/None schedule_days)
|
|
mock_add.assert_not_called()
|
|
# #endregion test_load_schedules_validation_policy_skip_empty_days
|
|
|
|
# #region test_load_schedules_validation_policy_skip_no_window [C:2] [TYPE Function]
|
|
# @BRIEF load_schedules skips validation policies with None window_start (line 115).
|
|
def test_load_schedules_validation_policy_skip_no_window(service, mock_config_manager):
|
|
cm = _build_config_manager(environments=[], app_timezone="UTC")
|
|
service.config_manager = cm
|
|
|
|
policy_no_ws = _build_policy("pol-3", is_active=True, schedule_days=[1, 3], window_start=None)
|
|
|
|
call_count = 0
|
|
|
|
def session_factory():
|
|
nonlocal call_count
|
|
call_count += 1
|
|
db = MagicMock()
|
|
if call_count == 1:
|
|
db.query.return_value.filter.return_value.all.return_value = []
|
|
else:
|
|
db.query.return_value.filter.return_value.all.return_value = [policy_no_ws]
|
|
return db
|
|
|
|
with patch("src.core.scheduler.belief_scope") as mock_bs:
|
|
mock_bs.return_value.__enter__ = MagicMock()
|
|
mock_bs.return_value.__exit__ = MagicMock(return_value=False)
|
|
with patch("src.core.scheduler.SessionLocal", side_effect=session_factory):
|
|
with patch.object(service, "add_validation_job") as mock_add:
|
|
service.load_schedules()
|
|
mock_add.assert_not_called()
|
|
# #endregion test_load_schedules_validation_policy_skip_no_window
|
|
|
|
# #region test_load_schedules_validation_policy_success [C:2] [TYPE Function]
|
|
# @BRIEF load_schedules registers valid validation policies with cron expression.
|
|
def test_load_schedules_validation_policy_success(service, mock_config_manager):
|
|
cm = _build_config_manager(environments=[], app_timezone="UTC")
|
|
service.config_manager = cm
|
|
|
|
ws = time(10, 30)
|
|
policy = _build_policy("pol-ok", is_active=True, schedule_days=[1, 3, 5], window_start=ws)
|
|
|
|
call_count = 0
|
|
|
|
def session_factory():
|
|
nonlocal call_count
|
|
call_count += 1
|
|
db = MagicMock()
|
|
if call_count == 1:
|
|
db.query.return_value.filter.return_value.all.return_value = []
|
|
else:
|
|
db.query.return_value.filter.return_value.all.return_value = [policy]
|
|
return db
|
|
|
|
with patch("src.core.scheduler.belief_scope") as mock_bs:
|
|
mock_bs.return_value.__enter__ = MagicMock()
|
|
mock_bs.return_value.__exit__ = MagicMock(return_value=False)
|
|
with patch("src.core.scheduler.SessionLocal", side_effect=session_factory):
|
|
with patch.object(service, "add_validation_job") as mock_add:
|
|
service.load_schedules()
|
|
# cron: minute=30 hour=10 * * 1,3,5
|
|
mock_add.assert_called_once_with(policy_id="pol-ok", cron_expression="30 10 * * 1,3,5", app_timezone="UTC")
|
|
# #endregion test_load_schedules_validation_policy_success
|
|
|
|
|
|
# ══════════════════════════════════════════════════════════════
|
|
# SchedulerService.add_backup_job (lines 139-157)
|
|
# ══════════════════════════════════════════════════════════════
|
|
|
|
# #region test_add_backup_job_success [C:2] [TYPE Function]
|
|
# @BRIEF add_backup_job registers a job with APScheduler.
|
|
def test_add_backup_job_success(service, mock_scheduler):
|
|
with patch("src.core.scheduler.belief_scope") as mock_bs:
|
|
mock_bs.return_value.__enter__ = MagicMock()
|
|
mock_bs.return_value.__exit__ = MagicMock(return_value=False)
|
|
with patch("src.core.scheduler.CronTrigger") as mock_trigger:
|
|
mock_trigger.from_crontab.return_value = "trigger_obj"
|
|
service.add_backup_job("env-1", "0 2 * * *")
|
|
mock_scheduler.add_job.assert_called_once()
|
|
call_kwargs = mock_scheduler.add_job.call_args
|
|
assert call_kwargs.kwargs["id"] == "backup_env-1"
|
|
assert call_kwargs.kwargs["replace_existing"] is True
|
|
# #endregion test_add_backup_job_success
|
|
|
|
# #region test_add_backup_job_invalid_cron [C:2] [TYPE Function]
|
|
# @BRIEF add_backup_job handles invalid cron expression without raising (lines 156-157).
|
|
def test_add_backup_job_invalid_cron(service, mock_scheduler):
|
|
with patch("src.core.scheduler.belief_scope") as mock_bs:
|
|
mock_bs.return_value.__enter__ = MagicMock()
|
|
mock_bs.return_value.__exit__ = MagicMock(return_value=False)
|
|
with patch("src.core.scheduler.CronTrigger") as mock_trigger:
|
|
mock_trigger.from_crontab.side_effect = ValueError("Invalid cron")
|
|
# Should not raise
|
|
service.add_backup_job("env-bad", "invalid_cron")
|
|
mock_scheduler.add_job.assert_not_called()
|
|
# #endregion test_add_backup_job_invalid_cron
|
|
|
|
|
|
# ══════════════════════════════════════════════════════════════
|
|
# SchedulerService.add_translation_job (lines 167-193)
|
|
# ══════════════════════════════════════════════════════════════
|
|
|
|
# #region test_add_translation_job_success [C:2] [TYPE Function]
|
|
# @BRIEF add_translation_job registers a translation job with APScheduler.
|
|
def test_add_translation_job_success(service, mock_scheduler):
|
|
with patch("src.core.scheduler.belief_scope") as mock_bs:
|
|
mock_bs.return_value.__enter__ = MagicMock()
|
|
mock_bs.return_value.__exit__ = MagicMock(return_value=False)
|
|
with patch("src.core.scheduler.CronTrigger") as mock_trigger:
|
|
mock_trigger.from_crontab.return_value = "trigger_obj"
|
|
service.add_translation_job("sched-1", "job-1", "*/5 * * * *", "US/Eastern", "incremental")
|
|
mock_scheduler.add_job.assert_called_once()
|
|
call_kwargs = mock_scheduler.add_job.call_args
|
|
assert call_kwargs.kwargs["id"] == "translate_sched-1"
|
|
# #endregion test_add_translation_job_success
|
|
|
|
# #region test_add_translation_job_exception [C:2] [TYPE Function]
|
|
# @BRIEF add_translation_job handles registration failure gracefully (lines 189-190).
|
|
def test_add_translation_job_exception(service, mock_scheduler):
|
|
mock_scheduler.add_job.side_effect = RuntimeError("APScheduler failure")
|
|
with patch("src.core.scheduler.belief_scope") as mock_bs:
|
|
mock_bs.return_value.__enter__ = MagicMock()
|
|
mock_bs.return_value.__exit__ = MagicMock(return_value=False)
|
|
with patch("src.core.scheduler.CronTrigger") as mock_trigger:
|
|
mock_trigger.from_crontab.return_value = "trigger_obj"
|
|
# Should not raise
|
|
service.add_translation_job("sched-2", "job-2", "0 0 * * *")
|
|
# #endregion test_add_translation_job_exception
|
|
|
|
|
|
# ══════════════════════════════════════════════════════════════
|
|
# SchedulerService.remove_translation_job
|
|
# ══════════════════════════════════════════════════════════════
|
|
|
|
# #region test_remove_translation_job_exists [C:2] [TYPE Function]
|
|
# @BRIEF remove_translation_job removes an existing job.
|
|
def test_remove_translation_job_exists(service, mock_scheduler):
|
|
with patch("src.core.scheduler.belief_scope") as mock_bs:
|
|
mock_bs.return_value.__enter__ = MagicMock()
|
|
mock_bs.return_value.__exit__ = MagicMock(return_value=False)
|
|
service.remove_translation_job("sched-1")
|
|
mock_scheduler.remove_job.assert_called_once_with("translate_sched-1")
|
|
# #endregion test_remove_translation_job_exists
|
|
|
|
# #region test_remove_translation_job_not_found [C:2] [TYPE Function]
|
|
# @BRIEF remove_translation_job silently handles missing job.
|
|
def test_remove_translation_job_not_found(service, mock_scheduler):
|
|
mock_scheduler.remove_job.side_effect = Exception("Job not found")
|
|
with patch("src.core.scheduler.belief_scope") as mock_bs:
|
|
mock_bs.return_value.__enter__ = MagicMock()
|
|
mock_bs.return_value.__exit__ = MagicMock(return_value=False)
|
|
# Should not raise
|
|
service.remove_translation_job("sched-missing")
|
|
# #endregion test_remove_translation_job_not_found
|
|
|
|
|
|
# ══════════════════════════════════════════════════════════════
|
|
# SchedulerService._trigger_backup (lines 218-241)
|
|
# ══════════════════════════════════════════════════════════════
|
|
|
|
# #region test_trigger_backup_no_active_tasks [C:2] [TYPE Function]
|
|
# @BRIEF _trigger_backup creates a task when no backup is running (lines 219-236).
|
|
def test_trigger_backup_no_active_tasks(service, mock_task_manager):
|
|
mock_task_manager.get_tasks.return_value = []
|
|
with patch("src.core.scheduler.seed_trace_id"):
|
|
with patch("src.core.scheduler.belief_scope") as mock_bs:
|
|
mock_bs.return_value.__enter__ = MagicMock()
|
|
mock_bs.return_value.__exit__ = MagicMock(return_value=False)
|
|
with patch("src.core.scheduler.asyncio.run_coroutine_threadsafe") as mock_run:
|
|
service._trigger_backup("env-1")
|
|
# create_task is called and its coroutine passed to run_coroutine_threadsafe
|
|
mock_run.assert_called_once()
|
|
mock_task_manager.create_task.assert_called_once_with(
|
|
"superset-backup", {"environment_id": "env-1"}
|
|
)
|
|
# #endregion test_trigger_backup_no_active_tasks
|
|
|
|
# #region test_trigger_backup_already_running [C:2] [TYPE Function]
|
|
# @BRIEF _trigger_backup skips when a backup is already running for the environment.
|
|
def test_trigger_backup_already_running(service, mock_task_manager):
|
|
running_task = _build_task(plugin_id="superset-backup", status="RUNNING", environment_id="env-1")
|
|
mock_task_manager.get_tasks.return_value = [running_task]
|
|
with patch("src.core.scheduler.seed_trace_id"):
|
|
with patch("src.core.scheduler.belief_scope") as mock_bs:
|
|
mock_bs.return_value.__enter__ = MagicMock()
|
|
mock_bs.return_value.__exit__ = MagicMock(return_value=False)
|
|
with patch("src.core.scheduler.asyncio.run_coroutine_threadsafe") as mock_run:
|
|
service._trigger_backup("env-1")
|
|
mock_run.assert_not_called()
|
|
# #endregion test_trigger_backup_already_running
|
|
|
|
# #region test_trigger_backup_pending_status [C:2] [TYPE Function]
|
|
# @BRIEF _trigger_backup skips when a backup is in PENDING status for the environment.
|
|
def test_trigger_backup_pending_status(service, mock_task_manager):
|
|
pending_task = _build_task(plugin_id="superset-backup", status="PENDING", environment_id="env-1")
|
|
mock_task_manager.get_tasks.return_value = [pending_task]
|
|
with patch("src.core.scheduler.seed_trace_id"):
|
|
with patch("src.core.scheduler.belief_scope") as mock_bs:
|
|
mock_bs.return_value.__enter__ = MagicMock()
|
|
mock_bs.return_value.__exit__ = MagicMock(return_value=False)
|
|
with patch("src.core.scheduler.asyncio.run_coroutine_threadsafe") as mock_run:
|
|
service._trigger_backup("env-1")
|
|
mock_run.assert_not_called()
|
|
# #endregion test_trigger_backup_pending_status
|
|
|
|
# #region test_trigger_backup_different_env [C:2] [TYPE Function]
|
|
# @BRIEF _trigger_backup proceeds when running backup is for a different environment.
|
|
def test_trigger_backup_different_env(service, mock_task_manager):
|
|
other_task = _build_task(plugin_id="superset-backup", status="RUNNING", environment_id="env-2")
|
|
mock_task_manager.get_tasks.return_value = [other_task]
|
|
with patch("src.core.scheduler.seed_trace_id"):
|
|
with patch("src.core.scheduler.belief_scope") as mock_bs:
|
|
mock_bs.return_value.__enter__ = MagicMock()
|
|
mock_bs.return_value.__exit__ = MagicMock(return_value=False)
|
|
with patch("src.core.scheduler.asyncio.run_coroutine_threadsafe") as mock_run:
|
|
service._trigger_backup("env-1")
|
|
mock_run.assert_called_once()
|
|
# #endregion test_trigger_backup_different_env
|
|
|
|
# #region test_trigger_backup_other_plugin [C:2] [TYPE Function]
|
|
# @BRIEF _trigger_backup proceeds when running task is a different plugin.
|
|
def test_trigger_backup_other_plugin(service, mock_task_manager):
|
|
other_plugin_task = _build_task(plugin_id="other-plugin", status="RUNNING", environment_id="env-1")
|
|
mock_task_manager.get_tasks.return_value = [other_plugin_task]
|
|
with patch("src.core.scheduler.seed_trace_id"):
|
|
with patch("src.core.scheduler.belief_scope") as mock_bs:
|
|
mock_bs.return_value.__enter__ = MagicMock()
|
|
mock_bs.return_value.__exit__ = MagicMock(return_value=False)
|
|
with patch("src.core.scheduler.asyncio.run_coroutine_threadsafe") as mock_run:
|
|
service._trigger_backup("env-1")
|
|
mock_run.assert_called_once()
|
|
# #endregion test_trigger_backup_other_plugin
|
|
|
|
|
|
# ══════════════════════════════════════════════════════════════
|
|
# SchedulerService.add_validation_job
|
|
# ══════════════════════════════════════════════════════════════
|
|
|
|
# #region test_add_validation_job_success [C:2] [TYPE Function]
|
|
# @BRIEF add_validation_job registers a validation job with APScheduler.
|
|
def test_add_validation_job_success(service, mock_scheduler):
|
|
with patch("src.core.scheduler.belief_scope") as mock_bs:
|
|
mock_bs.return_value.__enter__ = MagicMock()
|
|
mock_bs.return_value.__exit__ = MagicMock(return_value=False)
|
|
with patch("src.core.scheduler.CronTrigger") as mock_trigger:
|
|
mock_trigger.from_crontab.return_value = "trigger_obj"
|
|
service.add_validation_job("pol-1", "30 10 * * 1,3", app_timezone="UTC")
|
|
mock_scheduler.add_job.assert_called_once()
|
|
call_kwargs = mock_scheduler.add_job.call_args
|
|
assert call_kwargs.kwargs["id"] == "validation_pol-1"
|
|
# #endregion test_add_validation_job_success
|
|
|
|
# #region test_add_validation_job_exception [C:2] [TYPE Function]
|
|
# @BRIEF add_validation_job handles registration failure gracefully.
|
|
def test_add_validation_job_exception(service, mock_scheduler):
|
|
mock_scheduler.add_job.side_effect = RuntimeError("Scheduler error")
|
|
with patch("src.core.scheduler.belief_scope") as mock_bs:
|
|
mock_bs.return_value.__enter__ = MagicMock()
|
|
mock_bs.return_value.__exit__ = MagicMock(return_value=False)
|
|
with patch("src.core.scheduler.CronTrigger") as mock_trigger:
|
|
mock_trigger.from_crontab.return_value = "trigger_obj"
|
|
# Should not raise
|
|
service.add_validation_job("pol-bad", "30 10 * * 1")
|
|
# #endregion test_add_validation_job_exception
|
|
|
|
|
|
# ══════════════════════════════════════════════════════════════
|
|
# SchedulerService.remove_validation_job
|
|
# ══════════════════════════════════════════════════════════════
|
|
|
|
# #region test_remove_validation_job_exists [C:2] [TYPE Function]
|
|
# @BRIEF remove_validation_job removes an existing job.
|
|
def test_remove_validation_job_exists(service, mock_scheduler):
|
|
with patch("src.core.scheduler.belief_scope") as mock_bs:
|
|
mock_bs.return_value.__enter__ = MagicMock()
|
|
mock_bs.return_value.__exit__ = MagicMock(return_value=False)
|
|
service.remove_validation_job("pol-1")
|
|
mock_scheduler.remove_job.assert_called_once_with("validation_pol-1")
|
|
# #endregion test_remove_validation_job_exists
|
|
|
|
# #region test_remove_validation_job_not_found [C:2] [TYPE Function]
|
|
# @BRIEF remove_validation_job silently handles missing job.
|
|
def test_remove_validation_job_not_found(service, mock_scheduler):
|
|
mock_scheduler.remove_job.side_effect = Exception("Not found")
|
|
with patch("src.core.scheduler.belief_scope") as mock_bs:
|
|
mock_bs.return_value.__enter__ = MagicMock()
|
|
mock_bs.return_value.__exit__ = MagicMock(return_value=False)
|
|
# Should not raise
|
|
service.remove_validation_job("pol-missing")
|
|
# #endregion test_remove_validation_job_not_found
|
|
|
|
|
|
# ══════════════════════════════════════════════════════════════
|
|
# SchedulerService.reload_validation_policy (lines 300-331)
|
|
# ══════════════════════════════════════════════════════════════
|
|
|
|
# #region test_reload_validation_policy_not_found [C:2] [TYPE Function]
|
|
# @BRIEF reload_validation_policy removes job when policy not found in DB (lines 315-316).
|
|
def test_reload_validation_policy_not_found(service, mock_scheduler):
|
|
with patch("src.core.scheduler.belief_scope") as mock_bs:
|
|
mock_bs.return_value.__enter__ = MagicMock()
|
|
mock_bs.return_value.__exit__ = MagicMock(return_value=False)
|
|
with patch("src.core.scheduler.SessionLocal") as mock_session:
|
|
db_mock = MagicMock()
|
|
mock_session.return_value = db_mock
|
|
db_mock.query.return_value.filter.return_value.first.return_value = None
|
|
with patch.object(service, "remove_validation_job") as mock_remove:
|
|
with patch.object(service, "add_validation_job") as mock_add:
|
|
service.reload_validation_policy("pol-missing")
|
|
mock_remove.assert_called_once_with("pol-missing")
|
|
mock_add.assert_not_called()
|
|
# #endregion test_reload_validation_policy_not_found
|
|
|
|
# #region test_reload_validation_policy_inactive [C:2] [TYPE Function]
|
|
# @BRIEF reload_validation_policy removes job when policy is inactive (lines 319-320).
|
|
def test_reload_validation_policy_inactive(service, mock_scheduler):
|
|
policy = _build_policy("pol-inactive", is_active=False)
|
|
with patch("src.core.scheduler.belief_scope") as mock_bs:
|
|
mock_bs.return_value.__enter__ = MagicMock()
|
|
mock_bs.return_value.__exit__ = MagicMock(return_value=False)
|
|
with patch("src.core.scheduler.SessionLocal") as mock_session:
|
|
db_mock = MagicMock()
|
|
mock_session.return_value = db_mock
|
|
db_mock.query.return_value.filter.return_value.first.return_value = policy
|
|
with patch.object(service, "remove_validation_job") as mock_remove:
|
|
with patch.object(service, "add_validation_job") as mock_add:
|
|
service.reload_validation_policy("pol-inactive")
|
|
mock_remove.assert_called_once_with("pol-inactive")
|
|
mock_add.assert_not_called()
|
|
# #endregion test_reload_validation_policy_inactive
|
|
|
|
# #region test_reload_validation_policy_no_schedule [C:2] [TYPE Function]
|
|
# @BRIEF reload_validation_policy skips registration when no schedule_days or window_start (lines 327-328 area).
|
|
def test_reload_validation_policy_no_schedule(service, mock_scheduler):
|
|
policy = _build_policy("pol-nosched", is_active=True, schedule_days=[], window_start=None)
|
|
with patch("src.core.scheduler.belief_scope") as mock_bs:
|
|
mock_bs.return_value.__enter__ = MagicMock()
|
|
mock_bs.return_value.__exit__ = MagicMock(return_value=False)
|
|
with patch("src.core.scheduler.SessionLocal") as mock_session:
|
|
db_mock = MagicMock()
|
|
mock_session.return_value = db_mock
|
|
db_mock.query.return_value.filter.return_value.first.return_value = policy
|
|
with patch.object(service, "remove_validation_job") as mock_remove:
|
|
with patch.object(service, "add_validation_job") as mock_add:
|
|
service.reload_validation_policy("pol-nosched")
|
|
mock_remove.assert_called_once_with("pol-nosched")
|
|
mock_add.assert_not_called()
|
|
# #endregion test_reload_validation_policy_no_schedule
|
|
|
|
# #region test_reload_validation_policy_success [C:2] [TYPE Function]
|
|
# @BRIEF reload_validation_policy removes old and registers new job for active policy with schedule.
|
|
def test_reload_validation_policy_success(service, mock_scheduler):
|
|
ws = time(14, 0)
|
|
policy = _build_policy("pol-ok", is_active=True, schedule_days=[0, 2, 4], window_start=ws)
|
|
with patch("src.core.scheduler.belief_scope") as mock_bs:
|
|
mock_bs.return_value.__enter__ = MagicMock()
|
|
mock_bs.return_value.__exit__ = MagicMock(return_value=False)
|
|
with patch("src.core.scheduler.SessionLocal") as mock_session:
|
|
db_mock = MagicMock()
|
|
mock_session.return_value = db_mock
|
|
db_mock.query.return_value.filter.return_value.first.return_value = policy
|
|
with patch.object(service, "remove_validation_job") as mock_remove:
|
|
with patch.object(service, "add_validation_job") as mock_add:
|
|
service.reload_validation_policy("pol-ok", app_timezone="UTC")
|
|
mock_remove.assert_called_once_with("pol-ok")
|
|
# cron: minute=0 hour=14 * * 0,2,4
|
|
mock_add.assert_called_once_with("pol-ok", "0 14 * * 0,2,4", app_timezone="UTC")
|
|
# #endregion test_reload_validation_policy_success
|
|
|
|
# #region test_reload_validation_policy_exception [C:2] [TYPE Function]
|
|
# @BRIEF reload_validation_policy handles DB exception gracefully (lines 327-328).
|
|
def test_reload_validation_policy_exception(service, mock_scheduler):
|
|
with patch("src.core.scheduler.belief_scope") as mock_bs:
|
|
mock_bs.return_value.__enter__ = MagicMock()
|
|
mock_bs.return_value.__exit__ = MagicMock(return_value=False)
|
|
with patch("src.core.scheduler.SessionLocal", side_effect=RuntimeError("DB down")):
|
|
with patch.object(service, "remove_validation_job"):
|
|
# Should not raise
|
|
service.reload_validation_policy("pol-err")
|
|
# #endregion test_reload_validation_policy_exception
|
|
|
|
|
|
# ══════════════════════════════════════════════════════════════
|
|
# SchedulerService._trigger_validation (lines 338-391)
|
|
# ══════════════════════════════════════════════════════════════
|
|
|
|
# #region test_trigger_validation_policy_not_found [C:2] [TYPE Function]
|
|
# @BRIEF _trigger_validation returns early when policy not found in DB.
|
|
def test_trigger_validation_policy_not_found(service, mock_task_manager):
|
|
with patch("src.core.scheduler.seed_trace_id"):
|
|
with patch("src.core.scheduler.belief_scope") as mock_bs:
|
|
mock_bs.return_value.__enter__ = MagicMock()
|
|
mock_bs.return_value.__exit__ = MagicMock(return_value=False)
|
|
with patch("src.core.scheduler.SessionLocal") as mock_session:
|
|
db_mock = MagicMock()
|
|
mock_session.return_value = db_mock
|
|
db_mock.query.return_value.filter.return_value.first.return_value = None
|
|
# Should not raise
|
|
service._trigger_validation("pol-missing")
|
|
db_mock.close.assert_called_once()
|
|
# #endregion test_trigger_validation_policy_not_found
|
|
|
|
# #region test_trigger_validation_policy_inactive [C:2] [TYPE Function]
|
|
# @BRIEF _trigger_validation returns early when policy is inactive.
|
|
def test_trigger_validation_policy_inactive(service, mock_task_manager):
|
|
policy = _build_policy("pol-inactive", is_active=False)
|
|
with patch("src.core.scheduler.seed_trace_id"):
|
|
with patch("src.core.scheduler.belief_scope") as mock_bs:
|
|
mock_bs.return_value.__enter__ = MagicMock()
|
|
mock_bs.return_value.__exit__ = MagicMock(return_value=False)
|
|
with patch("src.core.scheduler.SessionLocal") as mock_session:
|
|
db_mock = MagicMock()
|
|
mock_session.return_value = db_mock
|
|
db_mock.query.return_value.filter.return_value.first.return_value = policy
|
|
service._trigger_validation("pol-inactive")
|
|
mock_task_manager.create_task.assert_not_called()
|
|
# #endregion test_trigger_validation_policy_inactive
|
|
|
|
# #region test_trigger_validation_no_dashboards [C:2] [TYPE Function]
|
|
# @BRIEF _trigger_validation returns early when policy has no dashboard_ids.
|
|
def test_trigger_validation_no_dashboards(service, mock_task_manager):
|
|
policy = _build_policy("pol-nodash", is_active=True, dashboard_ids=[], window_start=time(10, 0), window_end=time(11, 0))
|
|
with patch("src.core.scheduler.seed_trace_id"):
|
|
with patch("src.core.scheduler.belief_scope") as mock_bs:
|
|
mock_bs.return_value.__enter__ = MagicMock()
|
|
mock_bs.return_value.__exit__ = MagicMock(return_value=False)
|
|
with patch("src.core.scheduler.SessionLocal") as mock_session:
|
|
db_mock = MagicMock()
|
|
mock_session.return_value = db_mock
|
|
db_mock.query.return_value.filter.return_value.first.return_value = policy
|
|
service._trigger_validation("pol-nodash")
|
|
mock_task_manager.create_task.assert_not_called()
|
|
# #endregion test_trigger_validation_no_dashboards
|
|
|
|
# #region test_trigger_validation_success [C:2] [TYPE Function]
|
|
# @BRIEF _trigger_validation spawns tasks for each dashboard in the policy.
|
|
def test_trigger_validation_success(service, mock_task_manager):
|
|
dash_ids = ["dash-1", "dash-2"]
|
|
policy = _build_policy(
|
|
"pol-ok", is_active=True, dashboard_ids=dash_ids,
|
|
window_start=time(10, 0), window_end=time(11, 0),
|
|
environment_id="env-1", provider_id="prov-1",
|
|
)
|
|
with patch("src.core.scheduler.seed_trace_id"):
|
|
with patch("src.core.scheduler.belief_scope") as mock_bs:
|
|
mock_bs.return_value.__enter__ = MagicMock()
|
|
mock_bs.return_value.__exit__ = MagicMock(return_value=False)
|
|
with patch("src.core.scheduler.SessionLocal") as mock_session:
|
|
db_mock = MagicMock()
|
|
mock_session.return_value = db_mock
|
|
db_mock.query.return_value.filter.return_value.first.return_value = policy
|
|
with patch("src.core.scheduler.ThrottledSchedulerConfigurator") as mock_cfg:
|
|
mock_cfg.calculate_schedule.return_value = [
|
|
datetime(2026, 6, 11, 10, 0),
|
|
datetime(2026, 6, 11, 11, 0),
|
|
]
|
|
with patch("src.core.scheduler.asyncio.run_coroutine_threadsafe") as mock_run:
|
|
service._trigger_validation("pol-ok")
|
|
# Two dashboards → two run_coroutine_threadsafe calls
|
|
assert mock_run.call_count == 2
|
|
# #endregion test_trigger_validation_success
|
|
|
|
# #region test_trigger_validation_exception [C:2] [TYPE Function]
|
|
# @BRIEF _trigger_validation handles exception gracefully (lines 385-386).
|
|
def test_trigger_validation_exception(service, mock_task_manager):
|
|
with patch("src.core.scheduler.seed_trace_id"):
|
|
with patch("src.core.scheduler.belief_scope") as mock_bs:
|
|
mock_bs.return_value.__enter__ = MagicMock()
|
|
mock_bs.return_value.__exit__ = MagicMock(return_value=False)
|
|
with patch("src.core.scheduler.SessionLocal") as mock_session:
|
|
db_mock = MagicMock()
|
|
mock_session.return_value = db_mock
|
|
# Raise inside the try block (query, not SessionLocal itself)
|
|
db_mock.query.side_effect = RuntimeError("DB crash")
|
|
# Should not raise
|
|
service._trigger_validation("pol-err")
|
|
db_mock.close.assert_called_once()
|
|
# #endregion test_trigger_validation_exception
|
|
|
|
|
|
# ══════════════════════════════════════════════════════════════
|
|
# ThrottledSchedulerConfigurator.calculate_schedule (lines 410-445)
|
|
# ══════════════════════════════════════════════════════════════
|
|
|
|
# #region test_calculate_schedule_empty [C:2] [TYPE Function]
|
|
# @BRIEF calculate_schedule returns empty list for empty dashboard_ids (line 417).
|
|
def test_calculate_schedule_empty():
|
|
from src.core.scheduler import ThrottledSchedulerConfigurator
|
|
result = ThrottledSchedulerConfigurator.calculate_schedule(
|
|
window_start=time(10, 0),
|
|
window_end=time(11, 0),
|
|
dashboard_ids=[],
|
|
current_date=date(2026, 6, 11),
|
|
)
|
|
assert result == []
|
|
# #endregion test_calculate_schedule_empty
|
|
|
|
# #region test_calculate_schedule_single_dashboard [C:2] [TYPE Function]
|
|
# @BRIEF calculate_schedule returns single start_dt for one dashboard (line 434).
|
|
def test_calculate_schedule_single_dashboard():
|
|
from src.core.scheduler import ThrottledSchedulerConfigurator
|
|
result = ThrottledSchedulerConfigurator.calculate_schedule(
|
|
window_start=time(10, 0),
|
|
window_end=time(11, 0),
|
|
dashboard_ids=["dash-1"],
|
|
current_date=date(2026, 6, 11),
|
|
)
|
|
assert len(result) == 1
|
|
assert result[0] == datetime(2026, 6, 11, 10, 0)
|
|
# #endregion test_calculate_schedule_single_dashboard
|
|
|
|
# #region test_calculate_schedule_multiple [C:2] [TYPE Function]
|
|
# @BRIEF calculate_schedule distributes tasks evenly across the window.
|
|
def test_calculate_schedule_multiple():
|
|
from src.core.scheduler import ThrottledSchedulerConfigurator
|
|
result = ThrottledSchedulerConfigurator.calculate_schedule(
|
|
window_start=time(10, 0),
|
|
window_end=time(11, 0),
|
|
dashboard_ids=["d1", "d2", "d3"],
|
|
current_date=date(2026, 6, 11),
|
|
)
|
|
assert len(result) == 3
|
|
# First at 10:00, last at 11:00, middle at 10:30
|
|
assert result[0] == datetime(2026, 6, 11, 10, 0)
|
|
assert result[1] == datetime(2026, 6, 11, 10, 30)
|
|
assert result[2] == datetime(2026, 6, 11, 11, 0)
|
|
# #endregion test_calculate_schedule_multiple
|
|
|
|
# #region test_calculate_schedule_midnight_crossing [C:2] [TYPE Function]
|
|
# @BRIEF calculate_schedule handles window crossing midnight (line 422).
|
|
def test_calculate_schedule_midnight_crossing():
|
|
from src.core.scheduler import ThrottledSchedulerConfigurator
|
|
result = ThrottledSchedulerConfigurator.calculate_schedule(
|
|
window_start=time(23, 0),
|
|
window_end=time(1, 0),
|
|
dashboard_ids=["d1", "d2", "d3"],
|
|
current_date=date(2026, 6, 11),
|
|
)
|
|
assert len(result) == 3
|
|
# Window: 23:00 → 01:00 next day = 2 hours = 7200s
|
|
# Interval: 7200 / 2 = 3600s = 1h
|
|
assert result[0] == datetime(2026, 6, 11, 23, 0)
|
|
assert result[1] == datetime(2026, 6, 12, 0, 0)
|
|
assert result[2] == datetime(2026, 6, 12, 1, 0)
|
|
# #endregion test_calculate_schedule_midnight_crossing
|
|
|
|
# #region test_calculate_schedule_zero_window [C:2] [TYPE Function]
|
|
# @BRIEF calculate_schedule returns start_dt repeated for zero-length window (lines 426-429).
|
|
def test_calculate_schedule_zero_window():
|
|
from src.core.scheduler import ThrottledSchedulerConfigurator
|
|
result = ThrottledSchedulerConfigurator.calculate_schedule(
|
|
window_start=time(10, 0),
|
|
window_end=time(10, 0),
|
|
dashboard_ids=["d1", "d2", "d3"],
|
|
current_date=date(2026, 6, 11),
|
|
)
|
|
assert len(result) == 3
|
|
expected = datetime(2026, 6, 11, 10, 0)
|
|
assert all(t == expected for t in result)
|
|
# #endregion test_calculate_schedule_zero_window
|
|
|
|
# #region test_calculate_schedule_small_window_warning [C:2] [TYPE Function]
|
|
# @BRIEF calculate_schedule warns when interval < 1s (lines 439+).
|
|
def test_calculate_schedule_small_window_warning():
|
|
from src.core.scheduler import ThrottledSchedulerConfigurator
|
|
# 10 dashboards in 5 seconds → interval = 5/9 ≈ 0.56s < 1s
|
|
result = ThrottledSchedulerConfigurator.calculate_schedule(
|
|
window_start=time(10, 0, 0),
|
|
window_end=time(10, 0, 5),
|
|
dashboard_ids=[f"d{i}" for i in range(10)],
|
|
current_date=date(2026, 6, 11),
|
|
)
|
|
assert len(result) == 10
|
|
# First should be at start, last should be within the window
|
|
assert result[0] == datetime(2026, 6, 11, 10, 0, 0)
|
|
# #endregion test_calculate_schedule_small_window_warning
|
|
|
|
# #endregion Test.Scheduler
|