Skip to content

Commit 06e8536

Browse files
authored
Merge pull request #194 from jmchilton/mermaid_declarative_tests
Declarative YAML test coverage for gxformat2.mermaid
2 parents 5b03c19 + 7d63601 commit 06e8536

5 files changed

Lines changed: 217 additions & 5 deletions

File tree

HISTORY.rst

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@ History
99
0.27.0.dev0
1010
---------------------
1111

12-
12+
* Declarative YAML-driven test coverage for ``gxformat2.mermaid``; fix
13+
``_input_type_str`` to accept string input types (previous ``.value``
14+
attribute access crashed on real workflows).
15+
1316

1417
---------------------
1518
0.26.0 (2026-04-17)
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
# Mermaid flowchart generation from Galaxy workflows.
2+
# workflow_to_mermaid returns a single string; the _lines variants split on "\n"
3+
# so per-line assertions can use list-mode helpers ($length, value_any_contains).
4+
5+
test_simple_graph_header:
6+
fixture: synthetic-graph-simple.gxwf.yml
7+
operation: workflow_to_mermaid_lines
8+
assertions:
9+
- path: [0]
10+
value: "graph LR"
11+
12+
test_simple_graph_data_input_shape:
13+
fixture: synthetic-graph-simple.gxwf.yml
14+
operation: workflow_to_mermaid_lines
15+
assertions:
16+
- path: []
17+
value_any_contains: 'input_0>"the_input<br/><i>data</i>"]'
18+
19+
test_simple_graph_tool_step_shape:
20+
fixture: synthetic-graph-simple.gxwf.yml
21+
operation: workflow_to_mermaid_lines
22+
assertions:
23+
- path: []
24+
value_any_contains: 'step_0["cat"]'
25+
26+
test_simple_graph_edge:
27+
fixture: synthetic-graph-simple.gxwf.yml
28+
operation: workflow_to_mermaid_lines
29+
assertions:
30+
- path: []
31+
value_any_contains: "input_0 --> step_0"
32+
33+
test_simple_graph_exact:
34+
fixture: synthetic-graph-simple.gxwf.yml
35+
operation: workflow_to_mermaid
36+
assertions:
37+
- path: []
38+
value: |-
39+
graph LR
40+
input_0>"the_input<br/><i>data</i>"]
41+
step_0["cat"]
42+
input_0 --> step_0
43+
44+
test_multi_data_inputs_distinct_ids:
45+
fixture: synthetic-multi-data-input.gxwf.yml
46+
operation: workflow_to_mermaid_lines
47+
assertions:
48+
- path: []
49+
value_any_contains: 'input_0>"optional<br/><i>data</i>"]'
50+
- path: []
51+
value_any_contains: 'input_1>"required<br/><i>data</i>"]'
52+
- path: []
53+
value_any_contains: "input_1 --> step_0"
54+
- path: []
55+
value_any_contains: "input_0 --> step_0"
56+
57+
test_int_input_renders:
58+
fixture: synthetic-int-input.gxwf.yml
59+
operation: workflow_to_mermaid_lines
60+
assertions:
61+
- path: []
62+
value_any_contains: '"num_lines<br/><i>int</i>"'
63+
- path: []
64+
value_any_contains: '"input_d<br/><i>data</i>"'
65+
66+
test_subworkflow_step_uses_double_bracket_shape:
67+
fixture: synthetic-graph-with-subworkflow.gxwf.yml
68+
operation: workflow_to_mermaid_lines
69+
assertions:
70+
- path: []
71+
value_any_contains: 'step_1[["nested_workflow"]]'
72+
- path: []
73+
value_any_contains: 'step_0["first_cat"]'
74+
- path: []
75+
value_any_contains: "step_0 --> step_1"
76+
77+
test_frame_comments_ignored_by_default:
78+
fixture: synthetic-comments-dict.gxwf.yml
79+
operation: workflow_to_mermaid
80+
assertions:
81+
- path: []
82+
value_contains: 'step_0["cat"]'
83+
84+
test_frame_comments_not_emitted_without_flag:
85+
fixture: synthetic-comments-dict.gxwf.yml
86+
operation: workflow_to_mermaid
87+
assertions:
88+
- path: []
89+
# Use [\s\S] instead of (?s) + . so both Python re and JS RegExp accept it.
90+
value_matches: '^(?![\s\S]*subgraph)'
91+
92+
test_frame_comments_rendered_as_subgraph:
93+
fixture: synthetic-comments-dict.gxwf.yml
94+
operation: workflow_to_mermaid_with_comments_lines
95+
assertions:
96+
- path: []
97+
value_any_contains: 'subgraph sub_0 ["Preprocessing"]'
98+
- path: []
99+
value_any_contains: 'step_0["cat"]'
100+
- path: []
101+
value_any_contains: "end"
102+
- path: []
103+
value_any_contains: "input_0 --> step_0"
104+
105+
test_basic_workflow_single_edge:
106+
fixture: synthetic-basic.gxwf.yml
107+
operation: workflow_to_mermaid_lines
108+
assertions:
109+
- path: [0]
110+
value: "graph LR"
111+
- path: []
112+
value_any_contains: 'input_0>"the_input<br/><i>data</i>"]'
113+
- path: []
114+
value_any_contains: 'step_0["cat"]'
115+
- path: []
116+
value_any_contains: "input_0 --> step_0"
117+
118+
test_real_sars_cov2_shape:
119+
fixture: real-sars-cov2-variant-calling.ga
120+
operation: workflow_to_mermaid_lines
121+
assertions:
122+
- path: [0]
123+
value: "graph LR"
124+
- path: []
125+
value_any_contains: 'input_0>"Accessions file<br/><i>data</i>"]'
126+
- path: []
127+
value_any_contains: 'step_8["SnpSift summary table"]'
128+
- path: []
129+
value_any_contains: "input_0 --> step_0"
130+
- path: []
131+
value_any_contains: "step_6 --> step_8"

gxformat2/mermaid/_builder.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,10 @@ def _input_type_str(inp: BaseInputParameter) -> str:
4646
if type_ is None:
4747
return "input"
4848
if isinstance(type_, list):
49-
if type_:
50-
return type_[0].value
51-
return "input"
52-
return type_.value
49+
if not type_:
50+
return "input"
51+
type_ = type_[0]
52+
return getattr(type_, "value", type_)
5353

5454

5555
def _node_line(node_id: str, label: str, shape: tuple[str, str]) -> str:

tests/test_interop_tests.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from gxformat2.lint import lint_format2 as _lint_format2_impl
1515
from gxformat2.lint import lint_ga as _lint_ga_impl
1616
from gxformat2.linting import LintContext
17+
from gxformat2.mermaid import workflow_to_mermaid as _mermaid_impl
1718
from gxformat2.normalized import (
1819
ensure_format2,
1920
ensure_native,
@@ -99,6 +100,18 @@ def _lint_best_practices_native(wf_dict):
99100
}
100101

101102

103+
def _workflow_to_mermaid(wf_dict):
104+
return _mermaid_impl(wf_dict)
105+
106+
107+
def _workflow_to_mermaid_lines(wf_dict):
108+
return _mermaid_impl(wf_dict).split("\n")
109+
110+
111+
def _workflow_to_mermaid_with_comments_lines(wf_dict):
112+
return _mermaid_impl(wf_dict, comments=True).split("\n")
113+
114+
102115
EXPECTATIONS_DIR = os.path.join(EXAMPLES_DIR, "expectations")
103116
OPERATIONS: Dict[str, Callable[..., Any]] = {
104117
"normalized_format2": normalized_format2,
@@ -117,6 +130,9 @@ def _lint_best_practices_native(wf_dict):
117130
"lint_native": _lint_native,
118131
"lint_best_practices_format2": _lint_best_practices_format2,
119132
"lint_best_practices_native": _lint_best_practices_native,
133+
"workflow_to_mermaid": _workflow_to_mermaid,
134+
"workflow_to_mermaid_lines": _workflow_to_mermaid_lines,
135+
"workflow_to_mermaid_with_comments_lines": _workflow_to_mermaid_with_comments_lines,
120136
}
121137

122138
suite = DeclarativeTestSuite(

tests/test_mermaid.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
"""CLI smoke tests for gxformat2.mermaid.
2+
3+
Behavioral coverage of workflow_to_mermaid lives in declarative YAML
4+
(gxformat2/examples/expectations/mermaid.yml) and runs from
5+
test_interop_tests.py — keep this file for things that aren't a pure
6+
function of (workflow dict) -> result.
7+
"""
8+
9+
import os
10+
import tempfile
11+
12+
from gxformat2.mermaid import main, to_mermaid
13+
14+
from ._helpers import example_path
15+
16+
EXAMPLE_PATH = example_path("real-hacked-unicycler-assembly-extra-annotations.ga")
17+
18+
19+
def test_cli_writes_mmd_raw():
20+
with tempfile.NamedTemporaryFile(suffix=".mmd", delete=False) as out:
21+
out_path = out.name
22+
try:
23+
main([EXAMPLE_PATH, out_path])
24+
with open(out_path) as f:
25+
content = f.read()
26+
assert content.startswith("graph LR\n")
27+
assert "```" not in content
28+
finally:
29+
os.unlink(out_path)
30+
31+
32+
def test_cli_wraps_md_in_fence():
33+
with tempfile.NamedTemporaryFile(suffix=".md", delete=False) as out:
34+
out_path = out.name
35+
try:
36+
main([EXAMPLE_PATH, out_path])
37+
with open(out_path) as f:
38+
content = f.read()
39+
assert content.startswith("```mermaid\n")
40+
assert content.rstrip().endswith("```")
41+
assert "graph LR" in content
42+
finally:
43+
os.unlink(out_path)
44+
45+
46+
def test_cli_stdout(capsys):
47+
main([EXAMPLE_PATH])
48+
captured = capsys.readouterr()
49+
assert captured.out.startswith("graph LR\n")
50+
51+
52+
def test_cli_comments_flag(capsys):
53+
# Workflow without frame comments → flag is a no-op (no subgraph).
54+
main([EXAMPLE_PATH, "--comments"])
55+
captured = capsys.readouterr()
56+
assert captured.out.startswith("graph LR\n")
57+
58+
59+
def test_to_mermaid_stdout_passthrough(capsys):
60+
to_mermaid(EXAMPLE_PATH)
61+
captured = capsys.readouterr()
62+
assert "graph LR" in captured.out

0 commit comments

Comments
 (0)