"""DFD Гейна–Сарсона: локальный Pillow, без Graphviz.""" from __future__ import annotations from pathlib import Path from unittest.mock import patch import pytest from md2gost.dfd_gs import ( SAMPLE, DfdGsParseError, _arrange, parse_dfd_gs, render_dfd_gs, ) from md2gost.diagram_renderer import ( DIAGRAM_LANGS, configure_diagrams, is_diagram_lang, prepare_source, render_diagram, ) def test_fence_aliases_do_not_steal_yourdon(): assert is_diagram_lang("dfd-gs") assert is_diagram_lang("uml-dfd-gs") assert is_diagram_lang("gane-sarson") assert is_diagram_lang("uml-gane-sarson") assert "dfd-gs" in DIAGRAM_LANGS assert prepare_source("dfd-gs", SAMPLE)[1] == "dfd-gs" assert prepare_source("gane-sarson", SAMPLE)[1] == "dfd-gs" assert prepare_source("uml-dfd-gs", SAMPLE)[1] == "dfd-gs" assert prepare_source("dfd", "entity A Клиент\n")[1] == "dfd" assert prepare_source("uml-dfd", "entity A Клиент\n")[1] == "dfd" assert prepare_source("yourdon", "entity A Клиент\n")[1] == "dfd" def test_badges_and_colon_label(): diagram = parse_dfd_gs( """ entity A Художник entity B Заказчик process Accept Принять заявку process 3.1 Проверить срок store Orders Заказы store D4 Акты A --> Accept : файлы работы """ ) by_id = {node.id: node for node in diagram.nodes} assert by_id["A"].badge == "1" assert by_id["B"].badge == "2" assert by_id["Accept"].badge == "1" assert by_id["3.1"].badge == "3.1" assert by_id["Orders"].badge == "D1" assert by_id["D4"].badge == "D4" assert diagram.flows[0].label == "файлы работы" assert diagram.form == "kit" assert diagram.title == "" def test_store_code_skips_taken_numbers(): diagram = parse_dfd_gs( """ store D1 Заказы store Orders Журнал """ ) badges = {node.id: node.badge for node in diagram.nodes} assert badges == {"D1": "D1", "Orders": "D2"} def test_parse_errors(): with pytest.raises(DfdGsParseError, match="пустой"): parse_dfd_gs(" \n\n") with pytest.raises(DfdGsParseError, match="без узлов"): parse_dfd_gs("# только комментарий\n") with pytest.raises(DfdGsParseError, match="без узлов"): parse_dfd_gs("title Только шапка\n") with pytest.raises(DfdGsParseError, match="повтор"): parse_dfd_gs("entity A Один\nprocess A Два\n") with pytest.raises(DfdGsParseError, match="неизвестный узел"): parse_dfd_gs("entity A Один\nA --> B поток\n") with pytest.raises(DfdGsParseError, match="в себя"): parse_dfd_gs("entity A Один\nA --> A петля\n") with pytest.raises(DfdGsParseError, match="form"): parse_dfd_gs("form fancy\nentity A Один\n") with pytest.raises(DfdGsParseError, match="нет подписи"): parse_dfd_gs("process 1\n") def test_columns_and_straight_arrow(): diagram = parse_dfd_gs( """ entity A Художник process P Принять store S Файлы A --> P заявка P --> S файл """ ) layout = _arrange(diagram, 1) assert layout.placed["A"].x < layout.placed["P"].x < layout.placed["S"].x straight = _arrange( parse_dfd_gs("entity A Художник\nprocess P Принять\nA --> P заявка\n"), 1, ) pts = straight.routes[0].pts assert len(pts) == 2 assert abs(pts[0][1] - pts[1][1]) < 1 def test_opposite_flows_do_not_share_a_port(): diagram = parse_dfd_gs( """ process P Принять store S Заказы P --> S статус S --> P обновление """ ) layout = _arrange(diagram, 1) y1 = layout.routes[0].pts[0][1] y2 = layout.routes[1].pts[0][1] assert abs(y1 - y2) > 10 def test_stacked_processes_link_vertically(): diagram = parse_dfd_gs( """ process A Один process B Два A --> B дальше """ ) pts = _arrange(diagram, 1).routes[0].pts assert len(pts) == 2 assert abs(pts[0][0] - pts[1][0]) < 1 assert pts[0][1] < pts[1][1] def test_skipped_process_is_an_elbow(): diagram = parse_dfd_gs( """ process A Один process B Два process C Три A --> C дальше """ ) pts = _arrange(diagram, 1).routes[0].pts assert len(pts) >= 3 def test_render_png_and_svg_without_graphviz(): png, svg = render_dfd_gs(parse_dfd_gs(SAMPLE), scale=1) assert png.startswith(b"\x89PNG") assert svg is not None assert "USED AT" in svg assert "Тарасов" in svg assert "A44" in svg assert "rx=" in svg assert "Художник" in svg assert "rotate(-90" in svg assert "D2" in svg plain = parse_dfd_gs("form plain\nentity A Художник\nprocess P Принять\nA --> P заявка\n") _png, svg_plain = render_dfd_gs(plain, scale=1) assert "USED AT" not in (svg_plain or "") assert "rx=" in (svg_plain or "") def test_render_diagram_skips_jar_and_kroki(tmp_path): calls = {"jar": 0, "kroki": 0} def fake_jar(*_args, **_kwargs): calls["jar"] += 1 return False def fake_kroki(*_args, **_kwargs): calls["kroki"] += 1 return False configure_diagrams(fallback="off", cache_dir=str(tmp_path), diagram_scale=1, diagram_format="svg") with patch("md2gost.diagram_renderer._render_plantuml_jar", side_effect=fake_jar), \ patch("md2gost.diagram_renderer._render_kroki", side_effect=fake_kroki): result = render_diagram( "dfd-gs", SAMPLE, cache_dir=str(tmp_path), diagram_scale=1, diagram_format="svg", ) assert calls["jar"] == 0 assert calls["kroki"] == 0 assert Path(result.png_path).read_bytes().startswith(b"\x89PNG") assert result.svg_path and Path(result.svg_path).read_text(encoding="utf-8").startswith("