import fakeredis import pytest from tasks import store as task_store from tasks.store import Status, TaskStore pytestmark = pytest.mark.anyio @pytest.fixture def store(): return TaskStore(fakeredis.aioredis.FakeRedis(decode_responses=True)) async def test_claim_once(store): await store.create("t", "s", "https://example.com") await store.heartbeat("w1", {}) assert (await store.get("t"))["status"] == Status.QUEUED assert await store.claim("t", "w1") assert not await store.claim("t", "w2") assert not await store.cancel("t") async def test_cancelled_task_is_not_claimed(store): await store.create("t", "s", None) assert await store.cancel("t") assert not await store.claim("t", "w1") assert (await store.get("t"))["status"] == Status.CANCELLED async def test_missing_task_is_not_claimed(store): assert not await store.claim("nope", "w1") async def test_redelivery_while_running_here_is_skipped(store): await store.create("t", "s", None) await store.heartbeat("w1", {}) assert await store.claim("t", "w1") assert not await store.claim("t", "w1", running_here=True) async def test_task_of_dead_worker_is_reclaimed_limited_times(store): await store.create("t", "s", None) await store.heartbeat("w2", {}) assert await store.claim("t", "w1") # w1 умер — heartbeat нет assert await store.claim("t", "w2") # attempts = 2 await store.remove_worker("w2") assert not await store.claim("t", "w3") task = await store.get("t") assert task["status"] == Status.FAILED and "worker died" in task["error"] async def test_task_of_live_worker_is_not_reclaimed(store): await store.create("t", "s", None) await store.heartbeat("w1", {}) assert await store.claim("t", "w1") assert not await store.claim("t", "w2") async def test_finish_sets_result_and_ttl(store): await store.create("t", "s", None) assert await store.redis.ttl("robotex:task:t") == task_store.PENDING_TTL await store.finish("t", {"status": "ok", "actions": []}) task = await store.get("t") assert task["status"] == Status.DONE and task["result"] == {"status": "ok", "actions": []} assert await store.redis.ttl("robotex:task:t") == task_store.RESULT_TTL async def test_live_window_requires_live_worker(store): await store.set_session("s", "w1", 5, 300, client_id=7) assert await store.live_window("s") is None await store.heartbeat("w1", {"browser_slots": 1, "free_slots": 0}) assert await store.live_window("s") == {"worker_id": "w1", "idle_timeout": "5", "client_id": "7"} assert await store.workers() == [{"worker_id": "w1", "browser_slots": 1, "free_slots": 0}] async def test_worker_names_are_unique_and_reused(store): """Имена worker-N: занятое не выдаётся, освободившееся — переиспользуется (#110).""" assert await store.acquire_worker_name({"instance": "a"}) == "worker-1" assert await store.acquire_worker_name({"instance": "b"}) == "worker-2" await store.remove_worker("worker-1", "a") assert await store.acquire_worker_name({"instance": "c"}) == "worker-1" assert await store.redis.ttl("robotex:worker:worker-1") == task_store.HEARTBEAT_TTL async def test_heartbeat_of_taken_name_is_refused(store): """Имя истекло и его занял другой процесс: старый владелец его не продлит и не удалит.""" assert await store.acquire_worker_name({"instance": "old"}) == "worker-1" assert await store.heartbeat("worker-1", {"instance": "old", "windows": 1}) await store.redis.delete("robotex:worker:worker-1") # TTL истёк, пока Redis был недоступен assert await store.acquire_worker_name({"instance": "new"}) == "worker-1" assert not await store.heartbeat("worker-1", {"instance": "old"}) await store.remove_worker("worker-1", "old") assert (await store.workers())[0]["instance"] == "new" async def test_expired_own_name_is_renewed(store): """Имя истекло, но никто его не занял — heartbeat просто продлевает его.""" assert await store.acquire_worker_name({"instance": "a"}) == "worker-1" await store.redis.delete("robotex:worker:worker-1") assert await store.heartbeat("worker-1", {"instance": "a"}) assert await store.worker_alive("worker-1")