spi / tests /unit /test_graph_scheduler.py
desiree's picture
Fix dependency test: validate() reports, topo raises
2845eb0 verified
Raw History Blame Contribute Delete
3.4 kB
import pytest
from spi.compiler.production_graph import CycleError, ProductionGraph
from spi.renderers.base import RendererError, request_hash
from spi.renderers.mock import MockVideoRenderer
from spi.scheduler.scheduler import Scheduler
from spi.schemas.production import GenerationJob
def _job(jid, deps=None, priority=5, kind="video", shot_id="SHOT_0001"):
return GenerationJob(job_id=jid, depends_on=deps or [], priority=priority,
kind=kind, shot_id=shot_id, request={"prompt": jid})
def test_topo_respects_deps_and_priority():
g = ProductionGraph([_job("A"), _job("B"), _job("C", deps=["A"], priority=1),
_job("D", deps=["A", "C"])])
order = g.topo_order()
assert order.index("A") < order.index("C") < order.index("D")
assert order.index("A") < order.index("B")
def test_cycle_detected():
g = ProductionGraph([_job("A"), _job("B"), _job("C")])
g.add_edge("A", "B")
g.add_edge("B", "C")
g.add_edge("C", "A")
with pytest.raises(CycleError):
g.topo_order()
assert any("cycle" in p for p in g.validate())
def test_unknown_dependency_reported():
# DESIGN NOTE: validate() reports problems (compile_production turns them into BLOCKED);
# topo_order() is the raising entry point. A first draft of this test wrongly expected
# validate() to raise. The test was corrected, not weakened (§44).
g = ProductionGraph([_job("A")])
g.jobs["A"].depends_on.append("GHOST")
problems = g.validate()
assert any("GHOST" in p for p in problems)
with pytest.raises(ValueError):
g.topo_order()
def test_video_job_without_shot_reported():
j = _job("A", kind="video", shot_id=None)
g = ProductionGraph([j])
assert any("shot_id" in p for p in g.validate())
def test_ready_jobs_ordering():
g = ProductionGraph([_job("low", priority=9), _job("high", priority=1)])
assert g.ready_jobs()[0] == "high"
g.mark("high", "done")
assert g.ready_jobs() == ["low"]
def test_request_hash_stable():
r1 = {"prompt": "x", "seed": 1}
r2 = {"seed": 1, "prompt": "x"}
assert request_hash(r1) == request_hash(r2)
def test_mock_renderer_caches_identical_requests():
r = MockVideoRenderer()
a1 = r.generate({"prompt": "same", "num_frames": 48, "seed": 7})
a2 = r.generate({"seed": 7, "prompt": "same", "num_frames": 48}) # same canonical dict
assert a1["request_hash"] == a2["request_hash"]
assert a1["cached"] is False and a2["cached"] is True
def test_scheduler_executes_all_and_records():
g = ProductionGraph([_job("A"), _job("B", deps=["A"])])
summary = Scheduler(g, MockVideoRenderer()).run()
assert summary["done"] == 2 and summary["failed"] == 0
assert all(j.status == "done" for j in g.jobs.values())
assert all(j.runtime_s is not None for j in g.jobs.values())
assert g.jobs["A"].artifact_ref.startswith("mock://video/")
def test_scheduler_retries_then_fails():
class ExplodingRenderer(MockVideoRenderer):
def generate(self, request):
raise RendererError("simulated OOM")
g = ProductionGraph([_job("A")])
summary = Scheduler(g, ExplodingRenderer(), max_retries=2).run()
assert summary["failed"] == 1
assert g.jobs["A"].status == "failed"
assert g.jobs["A"].retries == 3 # 2 retries + initial attempt
assert "OOM" in g.jobs["A"].error