v0.6.0 BigUpdate
Example document / build (push) Has been cancelled
Python application / build (push) Has been cancelled

This commit is contained in:
Igor20264
2026-10-20 11:38:06 +03:00
parent daa84f8bce
commit 8223e0ac14
77 changed files with 12158 additions and 117 deletions
+202
View File
@@ -0,0 +1,202 @@
"""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("<svg")
assert result.pixel_scale == 1
def test_render_diagram_bad_source(tmp_path):
configure_diagrams(fallback="off", cache_dir=str(tmp_path))
with pytest.raises(RuntimeError, match="пустой"):
render_diagram("gane-sarson", " ", cache_dir=str(tmp_path))