Spaces:
Running
Running
| """Tests for core.job_queue — serial execution, cancel, position, failure isolation.""" | |
| import asyncio | |
| import pytest | |
| import pytest_asyncio | |
| from core.job_queue import Job, JobQueue, JobState | |
| async def queue(): | |
| q = JobQueue(name="test", concurrency=1) | |
| await q.start() | |
| try: | |
| yield q | |
| finally: | |
| await q.stop() | |
| async def test_single_job_runs_and_completes(queue): | |
| async def work(job: Job): | |
| return 42 | |
| j = await queue.submit("one", work) | |
| done = await queue.wait(j.id, timeout=2.0) | |
| assert done.state == JobState.DONE | |
| assert done.result == 42 | |
| async def test_jobs_run_serially_not_in_parallel(queue): | |
| """Two submits must not overlap — concurrency=1.""" | |
| events = [] | |
| start_a = asyncio.Event() | |
| release_a = asyncio.Event() | |
| async def slow_a(job): | |
| events.append("a-start") | |
| start_a.set() | |
| await release_a.wait() | |
| events.append("a-end") | |
| async def quick_b(job): | |
| events.append("b-start") | |
| events.append("b-end") | |
| ja = await queue.submit("a", slow_a) | |
| jb = await queue.submit("b", quick_b) | |
| await start_a.wait() | |
| # While A runs, B must still be queued. | |
| assert queue.get(jb.id).state == JobState.QUEUED | |
| release_a.set() | |
| await queue.wait(jb.id, timeout=2.0) | |
| assert events == ["a-start", "a-end", "b-start", "b-end"] | |
| async def test_cancel_while_queued_skips_execution(queue): | |
| ran = asyncio.Event() | |
| block = asyncio.Event() | |
| async def blocker(job): | |
| await block.wait() | |
| async def never(job): | |
| ran.set() | |
| ja = await queue.submit("blocker", blocker) | |
| jb = await queue.submit("skipped", never) | |
| cancelled = await queue.cancel(jb.id) | |
| assert cancelled is True | |
| assert queue.get(jb.id).state == JobState.CANCELLED | |
| block.set() | |
| await queue.wait(ja.id, timeout=2.0) | |
| # Give worker a tick to process the cancelled entry. | |
| await asyncio.sleep(0.05) | |
| assert not ran.is_set() | |
| async def test_cancel_while_running_sets_flag_for_cooperative_exit(queue): | |
| async def cooperative(job): | |
| for _ in range(20): | |
| if job.is_cancelled: | |
| return "stopped-early" | |
| await asyncio.sleep(0.02) | |
| return "finished-normally" | |
| j = await queue.submit("coop", cooperative) | |
| await asyncio.sleep(0.05) # let it start | |
| await queue.cancel(j.id) | |
| done = await queue.wait(j.id, timeout=2.0) | |
| # Worker marks cancelled when is_cancelled was true at return. | |
| assert done.state == JobState.CANCELLED | |
| assert done.result == "stopped-early" or done.result is None | |
| async def test_failure_isolated_next_job_still_runs(queue): | |
| async def boom(job): | |
| raise RuntimeError("kaboom") | |
| async def ok(job): | |
| return "ok" | |
| ja = await queue.submit("fail", boom) | |
| jb = await queue.submit("ok", ok) | |
| await queue.wait(ja.id, timeout=2.0) | |
| await queue.wait(jb.id, timeout=2.0) | |
| assert queue.get(ja.id).state == JobState.FAILED | |
| assert queue.get(ja.id).error == "kaboom" | |
| assert queue.get(jb.id).state == JobState.DONE | |
| async def test_position_reports_zero_for_next_up(queue): | |
| block = asyncio.Event() | |
| async def wait_a(job): | |
| await block.wait() | |
| async def wait_b(job): | |
| await block.wait() | |
| ja = await queue.submit("a", wait_a) | |
| jb = await queue.submit("b", wait_b) | |
| jc = await queue.submit("c", wait_b) | |
| await asyncio.sleep(0.02) # let worker pick a | |
| assert queue.get(ja.id).state == JobState.RUNNING | |
| # positions among QUEUED jobs | |
| assert queue.position(jb.id) == 0 | |
| assert queue.position(jc.id) == 1 | |
| assert queue.position(ja.id) == -1 | |
| block.set() | |
| await queue.wait(jc.id, timeout=2.0) | |
| async def test_submit_idempotent_on_job_id(queue): | |
| async def work(job): | |
| return 1 | |
| j1 = await queue.submit("x", work, job_id="fixed") | |
| j2 = await queue.submit("x", work, job_id="fixed") | |
| assert j1 is j2 | |
| await queue.wait("fixed", timeout=2.0) | |
| async def test_cancel_unknown_returns_false(queue): | |
| assert await queue.cancel("does-not-exist") is False | |
| async def test_purge_finished_drops_old_rows(queue): | |
| async def work(job): | |
| return 1 | |
| j = await queue.submit("x", work) | |
| await queue.wait(j.id, timeout=2.0) | |
| # Force finished_at into the past. | |
| queue.get(j.id).finished_at = 0.0 | |
| dropped = queue.purge_finished(older_than_seconds=1.0) | |
| assert dropped == 1 | |
| assert queue.get(j.id) is None | |
| async def test_list_jobs_live_only_by_default(queue): | |
| finished_evt = asyncio.Event() | |
| block = asyncio.Event() | |
| async def finish_fast(job): | |
| finished_evt.set() | |
| async def wait_forever(job): | |
| await block.wait() | |
| j1 = await queue.submit("done", finish_fast) | |
| await finished_evt.wait() | |
| await queue.wait(j1.id, timeout=1.0) | |
| j2 = await queue.submit("live", wait_forever) | |
| await asyncio.sleep(0.02) | |
| live = queue.list_jobs() | |
| all_jobs = queue.list_jobs(include_finished=True) | |
| assert j2.id in [j.id for j in live] | |
| assert j1.id not in [j.id for j in live] | |
| assert j1.id in [j.id for j in all_jobs] | |
| block.set() | |
| await queue.wait(j2.id, timeout=1.0) | |