daggr-studio / tests /test_codegen.py
jkorstad's picture
deploy: tests/test_codegen.py
a200321 verified
Raw History Blame Contribute Delete
6.13 kB
"""Codegen must be deterministic and produce code that is (a) valid Python and (b) a
working daggr graph. Both are asserted here - offline, with no network calls."""
from __future__ import annotations
import ast
import json
from daggrstudio.codegen import (
build_graph,
infer_postprocess_hint,
node_name_for,
render_app_py,
resolve_postprocess,
)
from daggrstudio.registry.bricks import get_registry
from daggrstudio.spec import Binding, Step, WorkflowSpec
from tests.conftest import concept_spec
def test_build_graph_creates_nodes_and_edges():
graph = build_graph(concept_spec())
names = set(graph.nodes)
assert {"concept", "cutout"}.issubset(names) # space steps keep their id
assert any(n.startswith("organize_outputs") for n in names) # fn step named after fn
connections = graph.get_connections()
endpoints = {(c[0], c[1], c[2], c[3]) for c in connections} if connections else set()
assert connections, "expected at least one edge"
assert any("concept" in str(c) and "cutout" in str(c) for c in connections), endpoints
def test_graph_has_a_stable_name_and_persist_key():
graph = build_graph(concept_spec())
assert graph.name == "Concept to sprite"
def test_build_graph_rejects_unknown_brick():
spec = concept_spec()
spec.steps[0].brick_id = "not-a-real-brick"
spec.steps[0].source = "owner/does-not-exist"
import pytest
with pytest.raises(KeyError, match="unknown brick"):
build_graph(spec)
def test_seed_registry_entries_carry_provenance():
reg = get_registry()
seed = reg.by_id("flux1-schnell")
assert seed is not None and seed.verified_at # introspected in-session
unverified = reg.by_id("wan21")
assert unverified is not None
assert not unverified.is_live_verified or unverified.verified_at
def test_node_names_are_unique_and_url_safe():
used: set[str] = set()
a = node_name_for(Step(id="Concept Art", kind="space"), used)
b = node_name_for(Step(id="concept art", kind="space"), used)
assert a == "concept_art" and b == "concept_art_2"
assert node_name_for(Step(id="x", kind="fn"), used) is None
def test_resolve_postprocess_hints():
first = resolve_postprocess("first")
assert first("img", 42) == "img"
second = resolve_postprocess("tuple_index:1")
assert second("original", "processed") == "processed"
assert second("only") == "only" # tolerant of single-value returns
dp = resolve_postprocess("dict_path:path")
assert dp({"path": "/tmp/a.png"}) == "/tmp/a.png"
assert dp("/tmp/a.png") == "/tmp/a.png"
assert resolve_postprocess(None) is None
assert resolve_postprocess("nonsense") is None
def test_postprocess_hint_comes_from_the_registry():
spec = concept_spec()
registry = get_registry()
cutout = spec.steps[1]
brick = registry.by_id(cutout.brick_id)
# whatever the registry verified for this brick is what codegen must use
assert infer_postprocess_hint(brick, cutout) == (brick.postprocess_hint
if brick.postprocess_hint else None)
# an explicit step-level hint always overrides the registry
cutout.postprocess = "tuple_index:1"
assert infer_postprocess_hint(brick, cutout) == "tuple_index:1"
def test_multi_value_bricks_get_a_postprocess_hint():
"""A brick that returns (value, extra) must never be used without unpacking."""
registry = get_registry()
multi = [b for b in registry.all() if b.postprocess_hint and b.status == "running"]
assert multi, "registry should expose at least one multi-return brick"
for brick in multi:
hint = brick.postprocess_hint
assert hint.startswith("tuple_index:") or hint.startswith("dict_path:")
def test_render_app_py_is_valid_python_and_self_contained():
spec = concept_spec()
registry = get_registry()
source = render_app_py(spec, registry)
ast.parse(source) # raises SyntaxError if codegen emits broken code
assert "from daggr import FnNode, GradioNode, InferenceNode, InputNode, Graph" in source
gen = registry.by_id(spec.steps[0].brick_id)
cut = registry.by_id(spec.steps[1].brick_id)
assert gen.source in source
assert f'api_name="{gen.api_name}"' in source
assert cut.source in source
assert "def organize_outputs(" in source # helper inlined
assert "daggrstudio" not in source # no dependency on this package
assert "graph.launch()" in source
def test_render_is_deterministic_and_tracks_the_spec():
spec = concept_spec()
one = render_app_py(spec)
two = render_app_py(spec)
# identical except for the generated timestamp line
assert "\n".join(l for l in one.splitlines() if "Generated:" not in l) == \
"\n".join(l for l in two.splitlines() if "Generated:" not in l)
spec.name = "Renamed"
assert "Renamed" in render_app_py(spec)
def test_rendered_app_reparses_into_an_equivalent_graph():
"""The downloaded code and the in-Space graph share one spec - prove the spec survives
a JSON round trip through the render path."""
spec = concept_spec()
again = WorkflowSpec.from_dict(json.loads(json.dumps(spec.to_dict())))
assert render_app_py(again).replace("Generated: ", "").split("\n")[0:3] == \
render_app_py(spec).replace("Generated: ", "").split("\n")[0:3]
def test_fn_step_uses_the_real_function_signature():
graph = build_graph(concept_spec())
fn_nodes = [n for name, n in graph.nodes.items() if name.startswith("organize_outputs")]
assert fn_nodes, "fn step should produce a node"
node = fn_nodes[0]
assert "source" in node._input_ports
assert "output_dir" in node._input_ports
def test_build_graph_handles_a_spec_with_no_inputs():
spec = WorkflowSpec(name="No inputs", steps=[
Step(id="bg", kind="space", brick_id="background-removal",
inputs={"image": Binding(value="/tmp/x.png")},
outputs={"image": {"component": "image"}}),
])
graph = build_graph(spec)
assert graph is not None and len(graph.nodes) == 1