190 lines
6.8 KiB
Python
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() == []
|