"""Unit tests for MIREA TZ checker and numbering helpers.""" from md2gost.checker import check_markdown from md2gost.numberer import Numberer from md2gost.profiles import preprocess_markdown, find_formula_refs from md2gost.bibliography import find_citations, extract_year SAMPLE_OK = """ # *СОДЕРЖАНИЕ [TOC] # *ВВЕДЕНИЕ Текст введения без ссылок на источники. # Анализ предметной области Согласно исследованиям [1] применяется подход, показанный на @Рисунок:arch. ![схема](img.png "%arch Архитектура системы") %tbl1 Сравнение подходов | Подход | Оценка | |--------|--------| | A | 1 | | B | 2 | Далее см. Таблицу — @Таблица:tbl1. # *ЗАКЛЮЧЕНИЕ Выводы по работе. # *СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ [1]: Иванов И. И. Информационные системы. — М.: Наука, 2023. — 120 с. [2]: Петров П. П. Базы данных. — СПб.: Питер, 2022. — 200 с. [3]: ГОСТ 7.32-2017. Отчет о НИР. — М., 2022. [4]: Сидоров С. С. Python. — М.: ДМК, 2024. — 300 с. [5]: Козлов К. К. Анализ данных. — М.: Бином, 2022. — 150 с. # *ПРИЛОЖЕНИЯ ## Приложение А Листинг модуля Код. """ def test_numberer_section_scoped(): n = Numberer() n.enter_section(1) assert n.next_number("Рисунок", "a") == "1.1" assert n.next_number("Рисунок", "b") == "1.2" n.enter_section(2) assert n.next_number("Рисунок") == "2.1" n.enter_appendix("Б") assert n.next_number("Рисунок", "x") == "Б.1" def test_numberer_continuous_pis(): n = Numberer(mode="continuous") n.enter_section(1) assert n.next_number("Рисунок", "a") == "1" assert n.next_number("Таблица", "t") == "1" n.enter_section(2) assert n.next_number("Рисунок", "b") == "2" n.enter_appendix("А") assert n.next_number("Рисунок", "c") == "3" SAMPLE_PIS = """ # *СОДЕРЖАНИЕ [TOC] # Практическая работа №1. Формирование требований к системе Цель работы — сформировать требования. ## Описание предметной области Текст с рисунком @Рисунок:arch. ![схема](img.png "%arch Архитектура") # Практическая работа №2. Моделирование процессов ## Построение модели %tbl1 Сравнение подходов | A | B | |---|---| | 1 | 2 | См. @Таблица:tbl1. """ def test_checker_ok_pis_custom(): issues = check_markdown(SAMPLE_PIS, "PIS_custom") errors = [i for i in issues if i.severity == "error"] assert not errors, errors def test_checker_pis_requires_practical(): text = SAMPLE_PIS.replace("Практическая работа №1.", "Раздел 1.") text = text.replace("Практическая работа №2.", "Раздел 2.") issues = check_markdown(text, "PIS_custom") assert any(i.id == "structure.practical" for i in issues) def test_pis_styles_h1_centered(): import docx from md2gost.styles import apply_pis_custom_styles from docx.enum.text import WD_ALIGN_PARAGRAPH doc = docx.Document(r"md2gost/Template.docx") apply_pis_custom_styles(doc) h1 = doc.styles["Heading 1"] assert h1.paragraph_format.alignment == WD_ALIGN_PARAGRAPH.CENTER assert h1.font.all_caps is True h2 = doc.styles["Heading 2"] assert h2.paragraph_format.first_line_indent is not None assert abs(h2.paragraph_format.first_line_indent.cm - 1.25) < 0.01 assert h2.paragraph_format.left_indent is None or h2.paragraph_format.left_indent.cm == 0 def test_soderzhanie_excluded_from_toc_outline(): import docx from md2gost.renderable.heading import Heading from md2gost.styles import apply_mirea_styles doc = docx.Document(r"md2gost/Template.docx") doc._body.clear_content() apply_mirea_styles(doc) h = Heading(doc._body, 1, False) h.add_run("СОДЕРЖАНИЕ") h.exclude_from_toc() levels = h._docx_paragraph._p.pPr.xpath("./w:outlineLvl") assert levels and levels[0].get( "{http://schemas.openxmlformats.org/wordprocessingml/2006/main}val" ) == "9" def test_heading_font_is_tnr_black(): import docx from docx.oxml.ns import qn from md2gost.styles import apply_mirea_styles doc = docx.Document(r"md2gost/Template.docx") apply_mirea_styles(doc) rPr = doc.styles["Heading 1"].element.find(qn("w:rPr")) rFonts = rPr.find(qn("w:rFonts")) assert rFonts.get(qn("w:ascii")) == "Times New Roman" assert rFonts.get(qn("w:hAnsi")) == "Times New Roman" assert rFonts.get(qn("w:eastAsia")) == "Times New Roman" color = rPr.find(qn("w:color")) assert color is not None assert color.get(qn("w:val")) == "000000" assert color.get(qn("w:themeColor")) is None def test_checker_ok_coursework(): issues = check_markdown(SAMPLE_OK, "coursework") errors = [i for i in issues if i.severity == "error"] assert not errors, errors def test_checker_ris_forbidden(): text = SAMPLE_OK.replace("@Рисунок:arch", "рис. 1.1") issues = check_markdown(text, "coursework") assert any(i.id == "ref.ris" for i in issues) def test_checker_cite_in_intro(): text = SAMPLE_OK.replace("Текст введения без ссылок на источники.", "См. [1].") issues = check_markdown(text, "coursework") assert any(i.id == "cite.intro" for i in issues) def test_checker_vkr_graphic(): issues = check_markdown(SAMPLE_OK, "vkr") assert any(i.id == "structure.graphic" for i in issues) def test_preprocess_quotes(): assert "«тест»" in preprocess_markdown('Он сказал "тест" вслух.') def test_preprocess_skips_uml_and_mermaid_fences(): md = ( 'В тексте "кавычки".\n\n' "```uml\n" 'rectangle "Акционеры" as own\n' "```\n\n" "```mermaid\n" 'title "Выручка"\n' "```\n\n" "```uml-c4context\n" 'Person(user, "Заявитель")\n' "```\n" ) out = preprocess_markdown(md) assert "«кавычки»" in out assert 'rectangle "Акционеры" as own' in out assert 'title "Выручка"' in out assert 'Person(user, "Заявитель")' in out assert "«Акционеры»" not in out def test_formula_refs(): assert find_formula_refs("см. @Формула:eq1 и далее") == {"eq1"} def test_citations_order(): assert find_citations("a [2] b [1] c [2, 3]") == ["2", "1", "3"] def test_extract_year(): assert extract_year("М.: Наука, 2023. — 120 с.") == 2023 def test_biblio_entries_from_concatenated_paragraph(): from md2gost.biblio_processor import entries_from_text, extract_bibliography_from_markdown entries = entries_from_text( "[1]: Первый источник. — М., 2024.[2]: Второй. URL: https://a.com/x-y/[3]: Третий." ) assert [e.key for e in entries] == ["1", "2", "3"] assert "https://a.com/x-y/" in entries[1].text md = """# *ВВЕДЕНИЕ Текст. # *СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ [1]: Ааа. — М., 2024. [2]: Ббб. — СПб., 2023. # *ПРИЛОЖЕНИЯ """ flat = extract_bibliography_from_markdown(md) assert len(flat) == 2 assert flat[0].key == "1" def test_emdash_to_hyphen_preprocess(): from md2gost.profiles import preprocess_markdown, set_emdash_to_hyphen, dash_separator out = preprocess_markdown("Цель — тест. Слово - слово.", emdash_to_hyphen=True) assert "—" not in out assert "Цель - тест" in out assert dash_separator() == " - " out2 = preprocess_markdown("Цель — тест.", emdash_to_hyphen=False) assert "—" in out2 assert dash_separator() == " — " set_emdash_to_hyphen(False) def test_native_toc_inserts_word_field(): import docx from md2gost.renderable.toc import ToC doc = docx.Document(r"md2gost/Template.docx") doc._body.clear_content() toc = ToC(doc._body, toc_mode="native") toc.fill() xml = toc._paragraph._docx_paragraph._p.xml assert "TOC" in xml assert "fldChar" in xml def test_reference_ignores_trailing_punctuation(): from md2gost.extended_markdown import markdown def refs(text): doc = markdown.parse(text) out = [] def walk(node): if node.__class__.__name__ == "Reference": out.append((node.type, node.name)) kids = getattr(node, "children", None) if isinstance(kids, list): for c in kids: walk(c) walk(doc) return out assert ("Рисунок", "submitted") in refs("@Рисунок:submitted, @Рисунок:lmsafter") assert ("Рисунок", "deploy") in refs("см. (@Рисунок:deploy). Далее") def test_heading_manual_kills_word_numbering(): import docx from md2gost.renderable.heading import Heading doc = docx.Document(r'md2gost/Template.docx') doc._body.clear_content() h = Heading(doc._body, 1, True, numbering_mode='manual') h.add_run('1 Разработка') h.apply_numbering_mode('manual') assert h.text.startswith('1 ') num_pr = h._docx_paragraph._p.pPr.xpath('.//w:numId') assert num_pr and num_pr[0].get('{http://schemas.openxmlformats.org/wordprocessingml/2006/main}val') == '0' def test_heading_auto_strips_leading_number(): import docx from md2gost.renderable.heading import Heading doc = docx.Document(r'md2gost/Template.docx') doc._body.clear_content() h = Heading(doc._body, 2, True, numbering_mode='auto') h.add_run('1.1 Описание') h.apply_numbering_mode('auto') assert h.text == 'Описание' def test_toc_tab_stops_match_text_area(): import docx from md2gost.styles import apply_mirea_styles doc = docx.Document(r'md2gost/Template.docx') apply_mirea_styles(doc) section = doc.sections[0] usable = section.page_width.mm - section.left_margin.mm - section.right_margin.mm for name in ('toc 1', 'toc 2'): tabs = list(doc.styles[name].paragraph_format.tab_stops) right_tabs = [t for t in tabs if 'RIGHT' in str(t.alignment)] assert len(right_tabs) == 1, (name, tabs) assert abs(right_tabs[0].position.mm - usable) < 0.05, (name, right_tabs[0].position.mm, usable) edge = section.left_margin.mm + right_tabs[0].position.mm assert abs(edge - (section.page_width.mm - section.right_margin.mm)) < 0.05 SAMPLE_APID = """ # *СОДЕРЖАНИЕ [TOC] # *ВВЕДЕНИЕ Текст введения без ссылок. # 1 Теоретические аспекты разработки архитектуры приложений и данных: описание микросервисной архитектуры Согласно [1] архитектура задаёт структуру системы. Далее [2]. # 2 Прикладные аспекты разработки архитектуры приложений и данных web-сервиса «SMEZHNO» ## 2.1 Описание проекта команды и разрабатываемого программного приложения «SMEZHNO» Текст проекта [3]. ## 2.2 Описание роли «Неавторизованный пользователь» web-сервиса «SMEZHNO» Роль. ## 2.3 Описание архитектуры программного приложения и данных web-сервиса «SMEZHNO» для роли «Неавторизованный пользователь» Архитектура [4]. ## 2.4 Варианты развития архитектуры программного приложения «SMEZHNO» Альтернативы [5]. # *ЗАКЛЮЧЕНИЕ Выводы. # *СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ [1]: ISO/IEC/IEEE 42010:2022. Architecture description. — Geneva: ISO, 2022. [2]: The Open Group. ArchiMate 3.2 Specification. — 2023. [3]: Аншина М. Л. Материалы лекционного курса. — М.: РТУ МИРЭА, 2024. [4]: Ньюмен С. Создание микросервисов. — СПб.: Питер, 2023. — 624 с. [5]: The Open Group. The TOGAF Standard, 10th Edition. — 2022. [6]: PostgreSQL 16 Documentation. URL: https://www.postgresql.org/docs/16/ (дата обращения: 01.05.2025). [7]: Kubernetes Documentation. URL: https://kubernetes.io/docs/home/ (дата обращения: 01.05.2025). """ def test_checker_apid_ok(): issues = check_markdown(SAMPLE_APID, "APID_coursework") errors = [i for i in issues if i.severity == "error"] assert not errors, errors def test_checker_apid_requires_sections(): text = SAMPLE_APID.replace("## 2.4 Варианты развития", "## 2.4 Прочее") issues = check_markdown(text, "APID_coursework") assert any(i.id == "structure.apid" for i in issues) def test_apid_biblio_min_seven(): text = SAMPLE_APID # drop last three sources -> 4 left text = text.split("[5]:")[0] + "\n" issues = check_markdown(text, "APID_coursework") assert any(i.id == "biblio.count" for i in issues) def test_special_heading_alignment_soderzhanie_vs_vvedenie(): import docx from docx.enum.text import WD_ALIGN_PARAGRAPH from docx.shared import Cm from md2gost.renderable.heading import Heading from md2gost.renderer import Renderer from md2gost.styles import apply_mirea_styles doc = docx.Document(r"md2gost/Template.docx") doc._body.clear_content() apply_mirea_styles(doc) renderer = Renderer(doc, skip_numbering=True) h_toc = Heading(doc._body, 1, False) h_toc.add_run("СОДЕРЖАНИЕ") renderer._handle_heading(h_toc) assert h_toc._docx_paragraph.alignment == WD_ALIGN_PARAGRAPH.CENTER assert abs(h_toc._docx_paragraph.paragraph_format.left_indent.cm - 0) < 0.01 h_intro = Heading(doc._body, 1, False) h_intro.add_run("ВВЕДЕНИЕ") renderer._handle_heading(h_intro) # Left like Heading 1 (style indent 1.25), not forced center assert h_intro._docx_paragraph.alignment != WD_ALIGN_PARAGRAPH.CENTER def test_appendix_item_becomes_h3_plus_title(): import docx from docx.enum.text import WD_ALIGN_PARAGRAPH from md2gost.renderable.heading import Heading from md2gost.renderer import Renderer from md2gost.styles import apply_mirea_styles doc = docx.Document(r"md2gost/Template.docx") doc._body.clear_content() apply_mirea_styles(doc) renderer = Renderer(doc, skip_numbering=True) h = Heading(doc._body, 2, True) h.add_run("Приложение А Листинг модуля") renderer._handle_heading(h) assert h.style.name == "Heading 3" assert h.text == "Приложение А" assert h._docx_paragraph.alignment == WD_ALIGN_PARAGRAPH.CENTER assert len(renderer._after_current) == 1 title_p = renderer._after_current[0] assert "Листинг модуля" in title_p._docx_paragraph.text assert title_p._docx_paragraph.alignment == WD_ALIGN_PARAGRAPH.CENTER def test_appendix_index_auto_inserted(): import docx from md2gost.renderable.heading import Heading from md2gost.renderer import Renderer from md2gost.styles import apply_mirea_styles doc = docx.Document(r"md2gost/Template.docx") doc._body.clear_content() apply_mirea_styles(doc) renderer = Renderer(doc, skip_numbering=True) sec = Heading(doc._body, 1, False) sec.add_run("ПРИЛОЖЕНИЯ") a = Heading(doc._body, 2, True) a.add_run("Приложение А Графический материал") b = Heading(doc._body, 2, True) b.add_run("Приложение Б Листинг") out = renderer._ensure_appendix_index([sec, a, b]) assert len(out) == 5 # sec + 2 index lines + a + b assert out[0] is sec t0 = out[1]._docx_paragraph.text t1 = out[2]._docx_paragraph.text assert t0.startswith("Приложение А") assert "Графический материал" in t0 assert t1.startswith("Приложение Б") assert "Листинг" in t1 assert out[3] is a assert out[4] is b # If list already present — do not duplicate from md2gost.renderable.paragraph import Paragraph manual = Paragraph(doc._body) manual.add_run("Приложение А — вручную") out2 = renderer._ensure_appendix_index([sec, manual, a, b]) assert out2 == [sec, manual, a, b] def test_table_caption_does_not_set_page_break_before(): import os import docx from docx.shared import Mm from md2gost import package_dir from md2gost.styles import apply_mirea_styles from md2gost.layout_tracker import LayoutState from md2gost.renderable.caption import Caption, CaptionInfo doc = docx.Document(os.path.join(package_dir(), "Template.docx")) apply_mirea_styles(doc) cap = Caption(doc._body, "Таблица", CaptionInfo("t1", "Реквизиты"), "1", True) state = LayoutState(Mm(250), Mm(160)) state.add_height(Mm(248)) leftover = state.remaining_page_height infos = list(cap.render(None, state)) assert len(infos) == 1 assert cap._docx_paragraph.style.name == "Название таблицы" assert cap._docx_paragraph.paragraph_format.page_break_before in (None, False) assert infos[0].height > leftover def test_caption_table_keep_with_next_and_toc_styles(): import docx from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_LINE_SPACING from md2gost.styles import apply_mirea_styles doc = docx.Document(r"md2gost/Template.docx") apply_mirea_styles(doc) assert doc.styles["Caption Table"].paragraph_format.keep_with_next is True assert doc.styles["Table Text"].paragraph_format.alignment == WD_ALIGN_PARAGRAPH.LEFT bh = doc.styles["Bibliography Heading"] assert abs(bh.paragraph_format.left_indent.cm - 1.25) < 0.01 toc1 = doc.styles["toc 1"] assert toc1.font.all_caps is True assert toc1.font.bold is False assert toc1.paragraph_format.line_spacing_rule == WD_LINE_SPACING.ONE_POINT_FIVE def test_emdash_default_false_in_pipeline(): from md2gost.pipeline import ConvertRequest assert ConvertRequest().emdash_to_hyphen is False def test_object_ref_unused_warning(): text = SAMPLE_OK.replace("@Рисунок:arch", "схема") issues = check_markdown(text, "coursework") assert any(i.id == "ref.unused" for i in issues) def test_table_continuation_warning(): issues = check_markdown(SAMPLE_OK, "coursework", table_continuation="off") assert any(i.id == "table.continuation" for i in issues) issues2 = check_markdown(SAMPLE_OK, "coursework", table_continuation="caption") assert not any(i.id == "table.continuation" for i in issues2) issues3 = check_markdown(SAMPLE_OK, "coursework", table_continuation="word") assert not any(i.id == "table.continuation" for i in issues3) def test_appendix_list_warning(): text = SAMPLE_OK + "\n## Приложение Б Ещё\n\nТекст.\n" # No list between ПРИЛОЖЕНИЯ and first appendix issues = check_markdown(text, "coursework") assert any(i.id == "appendix.toc" for i in issues) def test_vkr_biblio_per_section(): text = """ # *СОДЕРЖАНИЕ [TOC] # *ВВЕДЕНИЕ Текст. # 1 Раздел См. [1.1]. # *ЗАКЛЮЧЕНИЕ Выводы. # *СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ ## Нормативные [1.1]: ГОСТ 7.32-2017. — М., 2022. ## Научные [2.1]: Иванов И. И. Книга. — М., 2023. # *ПРИЛОЖЕНИЯ ## Приложение А Графический материал Слайды. """ issues = check_markdown(text, "vkr") assert any(i.id == "biblio.count" for i in issues) def test_page_fill_evaluate_heuristic(): from md2gost.page_fill_check import PageMetric, evaluate_page_fill pages = [ PageMetric(1, 0.4, "1 Анализ", "1 Анализ"), PageMetric(2, 0.9, "1 Анализ", "2 Проект", is_last_doc_page=False), PageMetric(3, 0.5, "2 Проект", None, is_last_doc_page=True), ] issues = evaluate_page_fill(pages) assert len(issues) == 1 assert issues[0].page_index == 1 assert "heuristic" in issues[0].message # Last page of section — no issue pages2 = [ PageMetric(1, 0.3, "1 Анализ", "2 Проект"), PageMetric(2, 0.9, "2 Проект", None, is_last_doc_page=True), ] assert evaluate_page_fill(pages2) == [] # Landscape skipped pages3 = [ PageMetric(1, 0.2, "1 Анализ", "1 Анализ", is_landscape=True), PageMetric(2, 0.9, "1 Анализ", None, is_last_doc_page=True), ] assert evaluate_page_fill(pages3) == [] def test_landscape_valign_center(): from docx import Document from docx.oxml.ns import qn from md2gost import package_dir import os from md2gost.page_geometry import apply_section_geometry doc = Document(os.path.join(package_dir(), "Template.docx")) section = doc.sections[0] apply_section_geometry(section, landscape=True) valign = section._sectPr.find(qn("w:vAlign")) assert valign is not None assert valign.get(qn("w:val")) == "center" apply_section_geometry(section, landscape=False) assert section._sectPr.find(qn("w:vAlign")) is None