Skip to content

Commit e4dda32

Browse files
talsperreclaude
andcommitted
test: split card-dag tests out of test_graph_inference
Bundle the three card-related tests into test_card_dag.py: - test_transform_flow_graph_supports_explicit_endpoints - test_transform_flow_graph_keeps_legacy_start_end_detection - test_default_card_includes_custom_graph_endpoints All three target the cards' graph data layer (transform_flow_graph and the DAG component render path). Keeping them next to the graph-execution tests in test_graph_inference.py blurred file boundaries and meant test_graph_inference.py had to import DefaultCardJSON, transform_flow_graph, and a four-line importlib helper just for the one render test. To support the render test cleanly, hoist the FlowSpec class loader into conftest.py as _load_flow_class plus a custom_named_card_flow fixture. DefaultCardJSON.render() does getattr(self.flow, step_name) and inspect.getsource(...) to build the Task Code panel and crashes if flow is None, so the test still needs the actual class object; moving the loader into conftest gets the importlib dance out of the test files. No behavior change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 7fe41a8 commit e4dda32

3 files changed

Lines changed: 121 additions & 94 deletions

File tree

test/unit/graph_inference/conftest.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,28 @@
11
import os
2+
from importlib.util import module_from_spec, spec_from_file_location
3+
24
import pytest
35
from metaflow import Runner, Flow
46

57
FLOWS_DIR = os.path.join(os.path.dirname(__file__), "flows")
68

79

10+
def _load_flow_class(flow_file, flow_class_name):
11+
"""Import a FlowSpec subclass from a file in FLOWS_DIR.
12+
13+
Loads the module by file path so FLOWS_DIR is not added to sys.path.
14+
Used by fixtures that need to pass `flow=` to a Card constructor;
15+
`DefaultCardJSON` does `getattr(self.flow, step_name)` and
16+
`inspect.getsource(...)` to build the Task Code panel and crashes if
17+
`flow` is None.
18+
"""
19+
spec = spec_from_file_location(flow_class_name, os.path.join(FLOWS_DIR, flow_file))
20+
module = module_from_spec(spec)
21+
assert spec.loader is not None
22+
spec.loader.exec_module(module)
23+
return getattr(module, flow_class_name)
24+
25+
826
def create_flow_fixture(flow_name, flow_file):
927
"""Factory function to create flow fixtures."""
1028

@@ -60,3 +78,11 @@ def flow_fixture(request):
6078
"single_step_bare_flow.py",
6179
)
6280
)
81+
82+
83+
@pytest.fixture(scope="session")
84+
def custom_named_card_flow():
85+
"""Instance of CustomNamedCardFlow for tests that pass flow= to a Card."""
86+
return _load_flow_class("custom_named_card_flow.py", "CustomNamedCardFlow")(
87+
use_cli=False
88+
)
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
"""
2+
Tests for the cards' DAG-related graph data layer.
3+
4+
Covers:
5+
- `transform_flow_graph` - the function that normalizes the legacy
6+
flat-step-dict and the new `{steps, start_step, end_step}` shapes
7+
into the structure the DAG card component renders against.
8+
- The end-to-end render path: a Run with custom-named entry/terminal
9+
steps produces a DAG card whose `start_step` / `end_step` /
10+
`steps` reflect the user's actual step names rather than the
11+
legacy hardcoded "start" / "end".
12+
"""
13+
14+
import json
15+
16+
from metaflow.plugins.cards.card_modules.basic import (
17+
DefaultCardJSON,
18+
transform_flow_graph,
19+
)
20+
21+
22+
def _find_components_by_type(node, component_type):
23+
if isinstance(node, dict):
24+
if node.get("type") == component_type:
25+
yield node
26+
for value in node.values():
27+
yield from _find_components_by_type(value, component_type)
28+
elif isinstance(node, list):
29+
for item in node:
30+
yield from _find_components_by_type(item, component_type)
31+
32+
33+
# ---------------------------------------------------------------------------
34+
# transform_flow_graph: shape-detection unit tests
35+
# ---------------------------------------------------------------------------
36+
37+
38+
def test_transform_flow_graph_supports_explicit_endpoints():
39+
graph = {
40+
"start_step": "begin",
41+
"end_step": "finish",
42+
"steps": {
43+
"begin": {"type": "start", "next": ["middle"], "doc": "begin"},
44+
"middle": {"type": "linear", "next": ["finish"], "doc": "middle"},
45+
"finish": {"type": "end", "next": [], "doc": "finish"},
46+
},
47+
}
48+
49+
transformed = transform_flow_graph(graph)
50+
51+
assert transformed["start_step"] == "begin"
52+
assert transformed["end_step"] == "finish"
53+
assert set(transformed["steps"]) == {"begin", "middle", "finish"}
54+
assert transformed["steps"]["begin"]["type"] == "start"
55+
assert transformed["steps"]["middle"]["box_next"] is False
56+
assert transformed["steps"]["finish"]["type"] == "end"
57+
58+
59+
def test_transform_flow_graph_keeps_legacy_start_end_detection():
60+
graph = {
61+
"start": {"type": "start", "next": ["end"], "doc": ""},
62+
"end": {"type": "end", "next": [], "doc": ""},
63+
}
64+
65+
transformed = transform_flow_graph(graph)
66+
67+
assert transformed["start_step"] == "start"
68+
assert transformed["end_step"] == "end"
69+
assert set(transformed["steps"]) == {"start", "end"}
70+
71+
72+
# ---------------------------------------------------------------------------
73+
# DefaultCardJSON: end-to-end render with custom-named endpoints
74+
# ---------------------------------------------------------------------------
75+
76+
77+
def test_default_card_includes_custom_graph_endpoints(
78+
custom_named_card_run, custom_named_card_flow
79+
):
80+
graph = custom_named_card_run["_parameters"].task["_graph_info"].data
81+
card_data = json.loads(
82+
DefaultCardJSON(graph=graph, flow=custom_named_card_flow).render(
83+
custom_named_card_run["begin"].task
84+
)
85+
)
86+
87+
dag_components = list(_find_components_by_type(card_data["components"], "dag"))
88+
assert len(dag_components) == 1
89+
90+
dag_data = dag_components[0]["data"]
91+
assert dag_data["start_step"] == "begin"
92+
assert dag_data["end_step"] == "finish"
93+
assert set(dag_data["steps"]) == {"begin", "middle", "finish"}
94+
assert "start" not in dag_data["steps"]
95+
assert "end" not in dag_data["steps"]

test/unit/graph_inference/test_graph_inference.py

Lines changed: 0 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -9,37 +9,7 @@
99
- Single-step flows execute end-to-end
1010
"""
1111

12-
import json
13-
from importlib.util import module_from_spec, spec_from_file_location
14-
from pathlib import Path
15-
16-
import pytest
1712
from metaflow.events import Trigger
18-
from metaflow.plugins.cards.card_modules.basic import (
19-
DefaultCardJSON,
20-
transform_flow_graph,
21-
)
22-
23-
FLOWS_DIR = Path(__file__).resolve().parent / "flows"
24-
25-
26-
def _find_components_by_type(node, component_type):
27-
if isinstance(node, dict):
28-
if node.get("type") == component_type:
29-
yield node
30-
for value in node.values():
31-
yield from _find_components_by_type(value, component_type)
32-
elif isinstance(node, list):
33-
for item in node:
34-
yield from _find_components_by_type(item, component_type)
35-
36-
37-
def _load_flow(flow_file, flow_class_name):
38-
spec = spec_from_file_location(flow_class_name, FLOWS_DIR / flow_file)
39-
module = module_from_spec(spec)
40-
assert spec.loader is not None
41-
spec.loader.exec_module(module)
42-
return getattr(module, flow_class_name)(use_cli=False)
4313

4414

4515
# ---------------------------------------------------------------------------
@@ -227,70 +197,6 @@ def test_trigger_from_runs_uses_custom_terminal_step(custom_named_run):
227197
assert trigger.run.pathspec == custom_named_run.pathspec
228198

229199

230-
# ---------------------------------------------------------------------------
231-
# Card graph transform
232-
# ---------------------------------------------------------------------------
233-
234-
235-
def test_transform_flow_graph_supports_explicit_endpoints():
236-
graph = {
237-
"start_step": "begin",
238-
"end_step": "finish",
239-
"steps": {
240-
"begin": {"type": "start", "next": ["middle"], "doc": "begin"},
241-
"middle": {"type": "linear", "next": ["finish"], "doc": "middle"},
242-
"finish": {"type": "end", "next": [], "doc": "finish"},
243-
},
244-
}
245-
246-
transformed = transform_flow_graph(graph)
247-
248-
assert transformed["start_step"] == "begin"
249-
assert transformed["end_step"] == "finish"
250-
assert set(transformed["steps"]) == {"begin", "middle", "finish"}
251-
assert transformed["steps"]["begin"]["type"] == "start"
252-
assert transformed["steps"]["middle"]["box_next"] is False
253-
assert transformed["steps"]["finish"]["type"] == "end"
254-
255-
256-
def test_transform_flow_graph_keeps_legacy_start_end_detection():
257-
graph = {
258-
"start": {"type": "start", "next": ["end"], "doc": ""},
259-
"end": {"type": "end", "next": [], "doc": ""},
260-
}
261-
262-
transformed = transform_flow_graph(graph)
263-
264-
assert transformed["start_step"] == "start"
265-
assert transformed["end_step"] == "end"
266-
assert set(transformed["steps"]) == {"start", "end"}
267-
268-
269-
# ---------------------------------------------------------------------------
270-
# Cards integration
271-
# ---------------------------------------------------------------------------
272-
273-
274-
def test_default_card_includes_custom_graph_endpoints(custom_named_card_run):
275-
flow = _load_flow("custom_named_card_flow.py", "CustomNamedCardFlow")
276-
graph = custom_named_card_run["_parameters"].task["_graph_info"].data
277-
card_data = json.loads(
278-
DefaultCardJSON(graph=graph, flow=flow).render(
279-
custom_named_card_run["begin"].task
280-
)
281-
)
282-
283-
dag_components = list(_find_components_by_type(card_data["components"], "dag"))
284-
assert len(dag_components) == 1
285-
286-
dag_data = dag_components[0]["data"]
287-
assert dag_data["start_step"] == "begin"
288-
assert dag_data["end_step"] == "finish"
289-
assert set(dag_data["steps"]) == {"begin", "middle", "finish"}
290-
assert "start" not in dag_data["steps"]
291-
assert "end" not in dag_data["steps"]
292-
293-
294200
# ---------------------------------------------------------------------------
295201
# Composition: single-step flows with Config, stacked decorators, FlowMutator
296202
# ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)