infrasynth-backend-kit/tests/test_scheduler/test_services.py
2026-08-28 14:38:47 -05:00

190 lines
6.8 KiB
Python

from unittest import mock
import pytest
from infrasynth.scheduler.models import ScheduledTask, TaskExecution
from infrasynth.scheduler.services import TaskService
pytestmark = pytest.mark.django_db
@pytest.fixture
def celery_task():
return ScheduledTask.objects.create(
name="deliver",
task_path="infrasynth.notifications.tasks.deliver_notification",
schedule_type=ScheduledTask.ScheduleType.MANUAL,
args=[999999],
)
@pytest.fixture
def plain_task():
return ScheduledTask.objects.create(
name="plain",
task_path="os.getpid",
schedule_type=ScheduledTask.ScheduleType.MANUAL,
)
class TestRunNow:
def test_creates_execution_for_celery_task(self, celery_task):
execution = TaskService().run_now(celery_task.id)
execution.refresh_from_db()
assert execution.task_id == celery_task.id
assert execution.status == TaskExecution.Status.RUNNING
assert execution.celery_task_id
assert execution.started_at is not None
def test_runs_plain_function(self, plain_task):
execution = TaskService().run_now(plain_task.id)
execution.refresh_from_db()
assert execution.status == TaskExecution.Status.SUCCESS
assert execution.completed_at is not None
def test_plain_function_with_args(self):
task = ScheduledTask.objects.create(
name="pow",
task_path="math.pow",
schedule_type=ScheduledTask.ScheduleType.MANUAL,
args=[2, 10],
)
execution = TaskService().run_now(task.id)
execution.refresh_from_db()
assert execution.status == TaskExecution.Status.SUCCESS
def test_unimportable_task_marks_failure(self):
task = ScheduledTask.objects.create(
name="broken",
task_path="does.not.exist",
schedule_type=ScheduledTask.ScheduleType.MANUAL,
)
execution = TaskService().run_now(task.id)
execution.refresh_from_db()
assert execution.status == TaskExecution.Status.FAILURE
assert "Could not import" in execution.error_traceback
assert execution.completed_at is not None
def test_task_exception_marks_failure(self):
task = ScheduledTask.objects.create(
name="boom",
task_path="math.sqrt",
schedule_type=ScheduledTask.ScheduleType.MANUAL,
args=["not-a-number"],
)
execution = TaskService().run_now(task.id)
execution.refresh_from_db()
assert execution.status == TaskExecution.Status.FAILURE
assert "must be real number" in execution.error_traceback
def test_emits_task_failed_signal_on_import_error(self):
from infrasynth.scheduler.signals import task_failed
task = ScheduledTask.objects.create(
name="broken2",
task_path="does.not.exist",
schedule_type=ScheduledTask.ScheduleType.MANUAL,
)
sent = []
receiver = lambda **kw: sent.append(kw) # noqa: E731
task_failed.connect(receiver, weak=False)
TaskService().run_now(task.id)
assert sent
assert sent[0]["task_name"] == "broken2"
task_failed.disconnect(receiver)
def test_emits_scheduled_and_started_signals(self, celery_task):
from infrasynth.scheduler.signals import task_scheduled, task_started
scheduled = []
started = []
rec_sched = lambda **kw: scheduled.append(kw) # noqa: E731
rec_start = lambda **kw: started.append(kw) # noqa: E731
task_scheduled.connect(rec_sched, weak=False)
task_started.connect(rec_start, weak=False)
TaskService().run_now(celery_task.id)
assert scheduled
assert started
assert started[0]["task_name"] == "deliver"
task_scheduled.disconnect(rec_sched)
task_started.disconnect(rec_start)
class TestToggle:
def test_toggle_enables_disables(self, celery_task):
service = TaskService()
assert celery_task.is_active is True
task = service.toggle(celery_task.id)
assert task.is_active is False
task = service.toggle(celery_task.id)
assert task.is_active is True
def test_toggle_persists(self, celery_task):
TaskService().toggle(celery_task.id)
celery_task.refresh_from_db()
assert celery_task.is_active is False
class TestQueueStatus:
def test_aggregates_worker_tasks(self):
with mock.patch("celery.current_app") as app:
inspect = app.control.inspect()
inspect.active.return_value = {
"worker1": [
{"delivery_info": {"routing_key": "default"}},
{"delivery_info": {"routing_key": "webhooks"}},
]
}
inspect.scheduled.return_value = {"worker1": [{"delivery_info": {"routing_key": "default"}}]}
inspect.reserved.return_value = {}
stats = TaskService().get_queue_status()
assert stats["total_active"] == 2
assert stats["total_scheduled"] == 1
assert stats["total_reserved"] == 0
assert stats["queues"]["default"]["active"] == 1
assert stats["queues"]["default"]["scheduled"] == 1
assert stats["queues"]["webhooks"]["active"] == 1
def test_empty_inspect(self):
with mock.patch("celery.current_app") as app:
app.control.inspect().active.return_value = None
app.control.inspect().scheduled.return_value = None
app.control.inspect().reserved.return_value = None
stats = TaskService().get_queue_status()
assert stats == {
"queues": {},
"total_active": 0,
"total_scheduled": 0,
"total_reserved": 0,
}
class TestGetWorkers:
def test_lists_workers_with_stats(self):
with mock.patch("celery.current_app") as app:
inspect = app.control.inspect()
inspect.stats.return_value = {
"worker1@host": {
"total": {"task": 42},
"uptime": 3600,
"queues": ["default"],
}
}
inspect.active.return_value = {"worker1@host": [{}, {}]}
workers = TaskService().get_workers()
assert len(workers) == 1
worker = workers[0]
assert worker["hostname"] == "worker1@host"
assert worker["active_tasks"] == 2
assert worker["processed"] == 42
assert worker["uptime_seconds"] == 3600
assert worker["queues"] == ["default"]
assert worker["status"] == "online"
def test_no_workers(self):
with mock.patch("celery.current_app") as app:
app.control.inspect().stats.return_value = None
app.control.inspect().active.return_value = None
assert TaskService().get_workers() == []