b38661f588
Python application / build (push) Has been cancelled
- Add\Rework UI - Add Split Table and Listing - Add Support Customazeble schems
542 lines
19 KiB
Python
542 lines
19 KiB
Python
"""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.
|
||
|
||

|
||
|
||
%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.
|
||
|
||

|
||
|
||
# Практическая работа №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_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_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
|
||
|