jkorstad commited on
Commit
a200321
·
verified ·
1 Parent(s): cfd33fa

deploy: tests/test_codegen.py

Browse files
Files changed (1) hide show
  1. tests/test_codegen.py +154 -0
tests/test_codegen.py ADDED
@@ -0,0 +1,154 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Codegen must be deterministic and produce code that is (a) valid Python and (b) a
2
+ working daggr graph. Both are asserted here - offline, with no network calls."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import ast
7
+ import json
8
+
9
+ from daggrstudio.codegen import (
10
+ build_graph,
11
+ infer_postprocess_hint,
12
+ node_name_for,
13
+ render_app_py,
14
+ resolve_postprocess,
15
+ )
16
+ from daggrstudio.registry.bricks import get_registry
17
+ from daggrstudio.spec import Binding, Step, WorkflowSpec
18
+ from tests.conftest import concept_spec
19
+
20
+
21
+ def test_build_graph_creates_nodes_and_edges():
22
+ graph = build_graph(concept_spec())
23
+ names = set(graph.nodes)
24
+ assert {"concept", "cutout"}.issubset(names) # space steps keep their id
25
+ assert any(n.startswith("organize_outputs") for n in names) # fn step named after fn
26
+
27
+ connections = graph.get_connections()
28
+ endpoints = {(c[0], c[1], c[2], c[3]) for c in connections} if connections else set()
29
+ assert connections, "expected at least one edge"
30
+ assert any("concept" in str(c) and "cutout" in str(c) for c in connections), endpoints
31
+
32
+
33
+ def test_graph_has_a_stable_name_and_persist_key():
34
+ graph = build_graph(concept_spec())
35
+ assert graph.name == "Concept to sprite"
36
+
37
+
38
+ def test_build_graph_rejects_unknown_brick():
39
+ spec = concept_spec()
40
+ spec.steps[0].brick_id = "not-a-real-brick"
41
+ spec.steps[0].source = "owner/does-not-exist"
42
+ import pytest
43
+
44
+ with pytest.raises(KeyError, match="unknown brick"):
45
+ build_graph(spec)
46
+
47
+
48
+ def test_seed_registry_entries_carry_provenance():
49
+ reg = get_registry()
50
+ seed = reg.by_id("flux1-schnell")
51
+ assert seed is not None and seed.verified_at # introspected in-session
52
+ unverified = reg.by_id("wan21")
53
+ assert unverified is not None
54
+ assert not unverified.is_live_verified or unverified.verified_at
55
+
56
+
57
+ def test_node_names_are_unique_and_url_safe():
58
+ used: set[str] = set()
59
+ a = node_name_for(Step(id="Concept Art", kind="space"), used)
60
+ b = node_name_for(Step(id="concept art", kind="space"), used)
61
+ assert a == "concept_art" and b == "concept_art_2"
62
+ assert node_name_for(Step(id="x", kind="fn"), used) is None
63
+
64
+
65
+ def test_resolve_postprocess_hints():
66
+ first = resolve_postprocess("first")
67
+ assert first("img", 42) == "img"
68
+ second = resolve_postprocess("tuple_index:1")
69
+ assert second("original", "processed") == "processed"
70
+ assert second("only") == "only" # tolerant of single-value returns
71
+ dp = resolve_postprocess("dict_path:path")
72
+ assert dp({"path": "/tmp/a.png"}) == "/tmp/a.png"
73
+ assert dp("/tmp/a.png") == "/tmp/a.png"
74
+ assert resolve_postprocess(None) is None
75
+ assert resolve_postprocess("nonsense") is None
76
+
77
+
78
+ def test_postprocess_hint_comes_from_the_registry():
79
+ spec = concept_spec()
80
+ registry = get_registry()
81
+ cutout = spec.steps[1]
82
+ brick = registry.by_id(cutout.brick_id)
83
+ # whatever the registry verified for this brick is what codegen must use
84
+ assert infer_postprocess_hint(brick, cutout) == (brick.postprocess_hint
85
+ if brick.postprocess_hint else None)
86
+ # an explicit step-level hint always overrides the registry
87
+ cutout.postprocess = "tuple_index:1"
88
+ assert infer_postprocess_hint(brick, cutout) == "tuple_index:1"
89
+
90
+
91
+ def test_multi_value_bricks_get_a_postprocess_hint():
92
+ """A brick that returns (value, extra) must never be used without unpacking."""
93
+ registry = get_registry()
94
+ multi = [b for b in registry.all() if b.postprocess_hint and b.status == "running"]
95
+ assert multi, "registry should expose at least one multi-return brick"
96
+ for brick in multi:
97
+ hint = brick.postprocess_hint
98
+ assert hint.startswith("tuple_index:") or hint.startswith("dict_path:")
99
+
100
+
101
+ def test_render_app_py_is_valid_python_and_self_contained():
102
+ spec = concept_spec()
103
+ registry = get_registry()
104
+ source = render_app_py(spec, registry)
105
+ ast.parse(source) # raises SyntaxError if codegen emits broken code
106
+ assert "from daggr import FnNode, GradioNode, InferenceNode, InputNode, Graph" in source
107
+ gen = registry.by_id(spec.steps[0].brick_id)
108
+ cut = registry.by_id(spec.steps[1].brick_id)
109
+ assert gen.source in source
110
+ assert f'api_name="{gen.api_name}"' in source
111
+ assert cut.source in source
112
+ assert "def organize_outputs(" in source # helper inlined
113
+ assert "daggrstudio" not in source # no dependency on this package
114
+ assert "graph.launch()" in source
115
+
116
+
117
+ def test_render_is_deterministic_and_tracks_the_spec():
118
+ spec = concept_spec()
119
+ one = render_app_py(spec)
120
+ two = render_app_py(spec)
121
+ # identical except for the generated timestamp line
122
+ assert "\n".join(l for l in one.splitlines() if "Generated:" not in l) == \
123
+ "\n".join(l for l in two.splitlines() if "Generated:" not in l)
124
+
125
+ spec.name = "Renamed"
126
+ assert "Renamed" in render_app_py(spec)
127
+
128
+
129
+ def test_rendered_app_reparses_into_an_equivalent_graph():
130
+ """The downloaded code and the in-Space graph share one spec - prove the spec survives
131
+ a JSON round trip through the render path."""
132
+ spec = concept_spec()
133
+ again = WorkflowSpec.from_dict(json.loads(json.dumps(spec.to_dict())))
134
+ assert render_app_py(again).replace("Generated: ", "").split("\n")[0:3] == \
135
+ render_app_py(spec).replace("Generated: ", "").split("\n")[0:3]
136
+
137
+
138
+ def test_fn_step_uses_the_real_function_signature():
139
+ graph = build_graph(concept_spec())
140
+ fn_nodes = [n for name, n in graph.nodes.items() if name.startswith("organize_outputs")]
141
+ assert fn_nodes, "fn step should produce a node"
142
+ node = fn_nodes[0]
143
+ assert "source" in node._input_ports
144
+ assert "output_dir" in node._input_ports
145
+
146
+
147
+ def test_build_graph_handles_a_spec_with_no_inputs():
148
+ spec = WorkflowSpec(name="No inputs", steps=[
149
+ Step(id="bg", kind="space", brick_id="background-removal",
150
+ inputs={"image": Binding(value="/tmp/x.png")},
151
+ outputs={"image": {"component": "image"}}),
152
+ ])
153
+ graph = build_graph(spec)
154
+ assert graph is not None and len(graph.nodes) == 1