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() == []