"""DFD via pbauermeister/data-flow-diagram (Graphviz / Kroki).""" from __future__ import annotations from pathlib import Path from unittest.mock import patch import pytest from md2gost.diagram_renderer import ( DIAGRAM_LANGS, configure_diagrams, is_diagram_lang, prepare_source, render_diagram, ) from md2gost.dfd import ( SAMPLE_CONTEXT, SAMPLE_LEVEL0, DfdParseError, _compile_dot, write_dfd_outputs, ) from md2gost.extended_markdown import markdown from md2gost.extended_markdown.caption import Caption as CapEl from md2gost.renderable.caption import CaptionInfo from md2gost.renderable.diagram import DiagramFigure from md2gost.renderable_factory import RenderableFactory def test_is_diagram_lang_dfd(): assert is_diagram_lang("dfd") assert is_diagram_lang("uml-dfd") assert is_diagram_lang("data-flow-diagram") assert is_diagram_lang("yourdon") assert is_diagram_lang("uml-yourdon") assert "dfd" in DIAGRAM_LANGS def test_prepare_dfd_stays_native(): src, dtype = prepare_source("dfd", SAMPLE_CONTEXT) assert dtype == "dfd" assert "@startuml" not in src.lower() assert "process" in src assert "System" in src for lang in ("uml-dfd", "data-flow-diagram", "yourdon"): _, dtype2 = prepare_source(lang, SAMPLE_LEVEL0) assert dtype2 == "dfd" def test_compile_dot_empty_raises(): with pytest.raises(DfdParseError, match="пустой"): _compile_dot(" \n\n ") def test_compile_dot_sample(monkeypatch): """When the package is importable, real compile; else accept mock.""" try: dot, is_ctx = _compile_dot(SAMPLE_CONTEXT) except DfdParseError as exc: if "data-flow-diagram не найден" in str(exc) or "не найден" in str(exc): pytest.skip("data-flow-diagram not installed for this Python") raise assert "digraph" in dot.lower() or "graph" in dot.lower() assert is_ctx is True def test_strip_graphviz_tcl_libs(tmp_path): from md2gost.dfd import GRAPHVIZ_TCL_DLL_NAMES, strip_graphviz_tcl_libs bin_dir = tmp_path / "bin" bin_dir.mkdir() (bin_dir / "dot.exe").write_bytes(b"mz") (bin_dir / "tcl86t.dll").write_bytes(b"tcl") (bin_dir / "gdtclft.dll").write_bytes(b"gd") (bin_dir / "cgraph.dll").write_bytes(b"keep") tcl_mod = tmp_path / "lib" / "graphviz" / "tcl" tcl_mod.mkdir(parents=True) (tcl_mod / "pkgIndex.tcl").write_text("x", encoding="utf-8") removed = strip_graphviz_tcl_libs(tmp_path) assert removed >= 3 assert not (bin_dir / "tcl86t.dll").exists() assert not (bin_dir / "gdtclft.dll").exists() assert (bin_dir / "dot.exe").exists() assert (bin_dir / "cgraph.dll").exists() assert not tcl_mod.exists() for name in GRAPHVIZ_TCL_DLL_NAMES: assert not (bin_dir / name).exists() def test_write_dfd_outputs_uses_local_or_kroki(tmp_path): png = tmp_path / "a.png" svg = tmp_path / "a.svg" fake_dot = "digraph G { a -> b; }\n" def fake_local(dot, out_path, fmt, *, context): Path(out_path).write_bytes( b"\x89PNG\r\n\x1a\n" if fmt == "png" else b"" ) if fmt == "png": # minimal valid-enough PNG header for startswith checks Path(out_path).write_bytes( b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR" + b"\x00" * 20 ) else: Path(out_path).write_text( "Клиент", encoding="utf-8", ) return True with patch("md2gost.dfd._compile_dot", return_value=(fake_dot, False)), \ patch("md2gost.dfd._render_dot_local", side_effect=fake_local), \ patch("md2gost.dfd._render_dot_kroki", return_value=False): assert write_dfd_outputs(SAMPLE_LEVEL0, png, svg, scale=1) assert png.read_bytes().startswith(b"\x89PNG") assert "Клиент" in svg.read_text(encoding="utf-8") def test_write_dfd_outputs_falls_back_to_kroki(tmp_path): png = tmp_path / "a.png" def fake_kroki(dot, out_path, fmt): Path(out_path).write_bytes(b"\x89PNG\r\n\x1a\n" + b"\x00" * 32) return True with patch("md2gost.dfd._compile_dot", return_value=("digraph G {}", False)), \ patch("md2gost.dfd._render_dot_local", return_value=False), \ patch("md2gost.dfd._render_dot_kroki", side_effect=fake_kroki): assert write_dfd_outputs(SAMPLE_CONTEXT, png, None, scale=1) assert png.read_bytes().startswith(b"\x89PNG") def test_write_dfd_outputs_raises_when_no_renderer(tmp_path): with patch("md2gost.dfd._compile_dot", return_value=("digraph G {}", False)), \ patch("md2gost.dfd._render_dot_local", return_value=False), \ patch("md2gost.dfd._render_dot_kroki", return_value=False): with pytest.raises(DfdParseError, match="Graphviz|Kroki"): write_dfd_outputs(SAMPLE_CONTEXT, tmp_path / "x.png", None) def test_render_diagram_dfd_skips_jar(tmp_path): calls = {"jar": 0} def fake_jar(*args, **kwargs): calls["jar"] += 1 return False def fake_write(source, out_png, out_svg, *, scale=2.0): Path(out_png).write_bytes(b"\x89PNG\r\n\x1a\n" + b"\x00" * 32) return True configure_diagrams(fallback="off", cache_dir=str(tmp_path), diagram_scale=1) with patch("md2gost.diagram_renderer._render_plantuml_jar", side_effect=fake_jar), \ patch("md2gost.dfd.write_dfd_outputs", side_effect=fake_write): result = render_diagram( "dfd", SAMPLE_CONTEXT, cache_dir=str(tmp_path), diagram_scale=1, ) assert calls["jar"] == 0 assert Path(result.png_path).read_bytes().startswith(b"\x89PNG") def test_render_diagram_dfd_bad_source(tmp_path): configure_diagrams(fallback="off", cache_dir=str(tmp_path)) with patch("md2gost.dfd._compile_dot", side_effect=DfdParseError("пустой DFD")): with pytest.raises(RuntimeError, match="пустой|DFD"): render_diagram("dfd", " ", cache_dir=str(tmp_path)) def test_factory_routes_dfd_to_diagram(): from docx import Document from md2gost import package_dir import os md = "%ctx Контекст DFD\n\n```dfd\n" + SAMPLE_CONTEXT + "```\n" parsed = markdown.parse(md) doc = Document(os.path.join(package_dir(), "Template.docx")) factory = RenderableFactory(doc._body) caption = None renderables = [] for el in parsed.children: from marko.block import BlankLine if isinstance(el, BlankLine): continue if isinstance(el, CapEl): caption = CaptionInfo(el.unique_name, el.text, el.with_listing) continue renderables.append(factory.create(el, caption)) caption = None assert len(renderables) == 1 assert isinstance(renderables[0], DiagramFigure) assert renderables[0]._lang == "dfd"