Initial commit
This commit is contained in:
commit
41d230d524
163 files changed
+11500
No files matched your search
@@ -0,0 +1,67 @@
|
||||
#import "/.template/lib/domain/document.typ": render-options, document-profile, validate-profile-contract
|
||||
#import "/.template/lib/domain/attachments.typ": attachment, attachment-set
|
||||
#import "/.template/lib/domain/references.typ": bibliography-section, validate-bibliographies
|
||||
#import "/.template/lib/infrastructure/company-assets.typ": load-company, available-companies
|
||||
#import "/.template/lib/infrastructure/employees.typ": employee-directory, empty-private-settings, private-company-media, report-executor
|
||||
|
||||
#let passthrough(body, ctx) = body
|
||||
|
||||
#let run() = {
|
||||
let options = render-options()
|
||||
assert.eq(options.mode, "final")
|
||||
assert.eq(options.at("media-policy"), "placeholder")
|
||||
|
||||
let profile = document-profile("unit", passthrough)
|
||||
assert.eq(validate-profile-contract(profile).id, "unit")
|
||||
|
||||
assert.eq(available-companies().len(), 4)
|
||||
for id in available-companies() {
|
||||
let loaded = load-company(id)
|
||||
assert.eq(loaded.kind, "company-profile")
|
||||
assert(type(loaded.legal.name) == str)
|
||||
}
|
||||
let company = load-company("test-company")
|
||||
assert.eq(company.legal.name, "ООО «Тестовая компания»")
|
||||
assert.eq(type(company.resources.logo), path)
|
||||
|
||||
assert.eq(employee-directory.len(), 19)
|
||||
let unsigned = report-executor("fedorov")
|
||||
assert.eq(unsigned.at(1), "Федоров Д.А.")
|
||||
assert.eq(unsigned.at(2), none)
|
||||
|
||||
let private-settings = (
|
||||
companies: (scientia: (signature: true, stamp: false)),
|
||||
signatures: (
|
||||
musikhin: (enabled: true, offset: 1.25cm),
|
||||
fedorov: (enabled: false, offset: 0.7cm),
|
||||
),
|
||||
)
|
||||
let signed = report-executor(
|
||||
"musikhin",
|
||||
role: "Управляющий директор",
|
||||
private-settings: private-settings,
|
||||
)
|
||||
assert.eq(signed.at(0), "Управляющий директор")
|
||||
assert.eq(type(signed.at(2)), path)
|
||||
assert.eq(signed.at(3), 1.25cm)
|
||||
assert.eq(report-executor("fedorov", private-settings: private-settings).at(2), none)
|
||||
let media = private-company-media(private-settings, "scientia")
|
||||
assert.eq(type(media.signature), path)
|
||||
assert.eq(media.stamp, none)
|
||||
assert.eq(private-company-media(empty-private-settings, "scientia").signature, none)
|
||||
|
||||
let item = attachment("a", "Приложение", [Текст])
|
||||
let items = attachment-set(items: (item,), numbering: "cyrillic")
|
||||
assert.eq(items.items.len(), 1)
|
||||
|
||||
let bibliography = bibliography-section("main", path("/assets/references.bib"))
|
||||
assert.eq(validate-bibliographies((bibliography,)).len(), 1)
|
||||
assert.eq(bibliography.at("page-break"), true)
|
||||
let compact-bibliography = bibliography-section(
|
||||
"compact",
|
||||
path("/assets/references.bib"),
|
||||
page_break: false,
|
||||
)
|
||||
assert.eq(compact-bibliography.at("page-break"), false)
|
||||
"ok"
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
#import "/.template/lib/numbering.typ": cyrillic_numbering, num_11, num_1a, num_1_a
|
||||
|
||||
#let run() = {
|
||||
assert.eq(cyrillic_numbering(1), "А")
|
||||
assert.eq(cyrillic_numbering(8), "И")
|
||||
assert.eq(cyrillic_numbering(25), "Я")
|
||||
assert.eq(cyrillic_numbering(26), "26")
|
||||
assert.eq(num_11(1, 2, 3), "1.2.3.")
|
||||
assert.eq(num_1a(1, 2, 3), "1.б.3.")
|
||||
assert.eq(num_1_a(1, 2), "б.")
|
||||
"ok"
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
#import "/.template/lib/domain/parties.typ": recipient, party, signer
|
||||
#import "/.template/lib/presentation/profiles/index.typ" as profiles
|
||||
#import "/.template/lib/shared/numbering.typ": attachment_numbering
|
||||
|
||||
#let run() = {
|
||||
let addressee = recipient(company: "ООО «Адресат»")
|
||||
|
||||
let report = profiles.report(
|
||||
title: "Проверка отчёта",
|
||||
show_title_page: false,
|
||||
show_executors: false,
|
||||
show_outline: true,
|
||||
)
|
||||
let report-metadata = (report.validate)(report.metadata)
|
||||
assert.eq(report.id, "report")
|
||||
assert.eq(report-metadata.at("show-title-page"), false)
|
||||
assert.eq(report-metadata.at("show-executors"), false)
|
||||
assert.eq(report-metadata.at("show-outline"), true)
|
||||
|
||||
let letter = profiles.letter(recipient: addressee)
|
||||
assert.eq(letter.id, "letter")
|
||||
assert.eq(((letter.validate)(letter.metadata)).recipient.kind, "recipient")
|
||||
|
||||
let offer = profiles.commercial_offer(
|
||||
recipient: addressee,
|
||||
subject: "Предмет",
|
||||
amount: "100",
|
||||
)
|
||||
assert.eq(offer.id, "commercial-offer")
|
||||
assert.eq(((offer.validate)(offer.metadata)).terms.amount, "100")
|
||||
|
||||
let left = party(
|
||||
"left",
|
||||
"Исполнитель",
|
||||
"ООО «Исполнитель»",
|
||||
representative: signer("И.И. Исполнитель", "Директор"),
|
||||
)
|
||||
let right = party(
|
||||
"right",
|
||||
"Заказчик",
|
||||
"ООО «Заказчик»",
|
||||
representative: signer("З.З. Заказчик", "Директор"),
|
||||
)
|
||||
let contract = profiles.contract(
|
||||
number: "1",
|
||||
parties: (left, right),
|
||||
sections: (
|
||||
profiles.contract_section("subject", "Предмет", [Текст раздела]),
|
||||
),
|
||||
)
|
||||
assert.eq(contract.id, "contract")
|
||||
assert.eq(((contract.validate)(contract.metadata)).sections.len(), 1)
|
||||
|
||||
assert.eq(attachment_numbering("arabic", 2), "2")
|
||||
assert.eq(attachment_numbering("cyrillic", 2), "Б")
|
||||
assert.eq(attachment_numbering("none", 2), "")
|
||||
"ok"
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
#import "/.template/lib/api.typ": document, recipient, attachment, attachment-set
|
||||
#import "/.template/lib/presentation/profiles/commercial-offer.typ": commercial-offer-profile
|
||||
|
||||
#let addressee = recipient(
|
||||
company: "АО «Заказчик»",
|
||||
title: "Руководителю службы автоматизации",
|
||||
name: "С.С. Заказчикову",
|
||||
)
|
||||
|
||||
#let appendices = attachment-set(items: (
|
||||
attachment(
|
||||
"scope",
|
||||
"Техническое задание",
|
||||
[
|
||||
Поставка, настройка и ввод в эксплуатацию тестового комплекса.
|
||||
],
|
||||
),
|
||||
))
|
||||
|
||||
#show: document.with(
|
||||
company: "test-company",
|
||||
profile: commercial-offer-profile(
|
||||
recipient: addressee,
|
||||
subject: "Поставка и внедрение тестового комплекса",
|
||||
amount: "1 250 000",
|
||||
currency: "руб.",
|
||||
tax_note: "включая НДС 20 %",
|
||||
delivery_term: "45 рабочих дней",
|
||||
validity: "30 календарных дней",
|
||||
payment_terms: "30 % аванс, 70 % после приёмки",
|
||||
date: "26.08.2026",
|
||||
reference: "ТКП-001",
|
||||
note: "Контактное лицо: А.А. Автор",
|
||||
attachments: appendices,
|
||||
),
|
||||
)
|
||||
|
||||
Уважаемый Сергей Сергеевич!
|
||||
|
||||
Предлагаем выполнить комплекс работ по поставке и внедрению оборудования.
|
||||
|
||||
== Состав работ
|
||||
|
||||
- обследование объекта;
|
||||
- поставка оборудования;
|
||||
- монтаж и пусконаладка;
|
||||
- обучение персонала заказчика.
|
||||
|
||||
Гарантийный срок составляет 12 месяцев с даты подписания акта приёмки.
|
||||
@@ -0,0 +1,85 @@
|
||||
#import "/.template/lib/api.typ": document, party, signer, attachment, attachment-set, load-company
|
||||
#import "/.template/lib/presentation/profiles/contract.typ": contract-profile, contract-section
|
||||
|
||||
#let company = load-company("test-company")
|
||||
|
||||
#let contractor = party(
|
||||
"contractor",
|
||||
"Исполнитель",
|
||||
company.legal.name,
|
||||
legal: company.legal,
|
||||
contacts: company.contacts,
|
||||
banking: company.banking,
|
||||
representative: signer(
|
||||
company.director.name,
|
||||
company.director.title,
|
||||
basis: "Устава",
|
||||
signature: company.resources.signature,
|
||||
stamp: company.resources.stamp,
|
||||
),
|
||||
)
|
||||
|
||||
#let customer = party(
|
||||
"customer",
|
||||
"Заказчик",
|
||||
"ООО «Заказчик»",
|
||||
legal: (
|
||||
inn: "1111111111",
|
||||
kpp: "111111111",
|
||||
address: "г. Пример, ул. Договорная, 2",
|
||||
),
|
||||
contacts: (email: "customer@example.invalid"),
|
||||
banking: (
|
||||
bank: "Банк заказчика",
|
||||
account: "11111111111111111111",
|
||||
bik: "111111111",
|
||||
),
|
||||
representative: signer(
|
||||
"П.П. Заказчиков",
|
||||
"Генеральный директор",
|
||||
basis: "Устава",
|
||||
),
|
||||
)
|
||||
|
||||
#let sections = (
|
||||
contract-section(
|
||||
"subject",
|
||||
"Предмет договора",
|
||||
[Исполнитель обязуется выполнить работы, а Заказчик — принять и оплатить их на условиях настоящего договора.],
|
||||
),
|
||||
contract-section(
|
||||
"price",
|
||||
"Цена и порядок расчётов",
|
||||
[Цена договора составляет 1 250 000 рублей, включая НДС 20 %.],
|
||||
),
|
||||
contract-section(
|
||||
"liability",
|
||||
"Ответственность сторон",
|
||||
[Стороны несут ответственность в соответствии с законодательством Российской Федерации.],
|
||||
),
|
||||
)
|
||||
|
||||
#let appendices = attachment-set(items: (
|
||||
attachment(
|
||||
"specification",
|
||||
"Спецификация",
|
||||
[
|
||||
Перечень работ и оборудования согласуется сторонами в настоящем приложении.
|
||||
],
|
||||
),
|
||||
))
|
||||
|
||||
#show: document.with(
|
||||
company: "test-company",
|
||||
profile: contract-profile(
|
||||
number: "Д-001/2026",
|
||||
date: "26 августа 2026 г.",
|
||||
place: "г. Пример",
|
||||
title: "Договор оказания услуг",
|
||||
parties: (contractor, customer),
|
||||
sections: sections,
|
||||
attachments: appendices,
|
||||
),
|
||||
)
|
||||
|
||||
Настоящий договор вступает в силу с момента подписания обеими сторонами.
|
||||
@@ -0,0 +1,26 @@
|
||||
#import "/.template/lib/api.typ": document, document-profile
|
||||
|
||||
#let smoke-renderer(body, ctx) = [
|
||||
#set text(font: "Arial", size: 11pt, lang: "ru")
|
||||
#set page(paper: "a4")
|
||||
#text(weight: "bold")[DDD CORE SMOKE]
|
||||
|
||||
Компания: #ctx.company.legal.name
|
||||
|
||||
Профиль: #ctx.at("profile-id")
|
||||
|
||||
#body
|
||||
]
|
||||
|
||||
#let smoke-profile = document-profile(
|
||||
"smoke",
|
||||
smoke-renderer,
|
||||
metadata: (:),
|
||||
)
|
||||
|
||||
#show: document.with(
|
||||
company: "test-company",
|
||||
profile: smoke-profile,
|
||||
)
|
||||
|
||||
Изолированное ядро успешно собрано.
|
||||
@@ -0,0 +1,7 @@
|
||||
#import "/.template/lib/index.typ": document
|
||||
|
||||
#show: document.with(company: "test-company")
|
||||
|
||||
#heading(numbering: none)[ПРОВЕРКА ПРОФИЛЯ ПО УМОЛЧАНИЮ]
|
||||
|
||||
Корневой фасад формирует отчёт без явной передачи profile.
|
||||
@@ -0,0 +1,15 @@
|
||||
#import "/.template/lib/index.typ": document, profiles, recipient, info-block
|
||||
|
||||
#show: document.with(
|
||||
company: "test-company",
|
||||
profile: profiles.letter(
|
||||
recipient: recipient(company: "ООО «Проверка фасада»"),
|
||||
title: "Публичный фасад",
|
||||
show_stamp: false,
|
||||
),
|
||||
options: (mode: "clean-copy", media-policy: "hide"),
|
||||
)
|
||||
|
||||
#info-block(title: [ПРОВЕРКА КОМПОНЕНТА])[
|
||||
Корневой файл экспортирует единый фасад и namespace профилей.
|
||||
]
|
||||
@@ -0,0 +1,38 @@
|
||||
#import "/.template/lib/api.typ": document, recipient, attachment, attachment-set
|
||||
#import "/.template/lib/presentation/profiles/letter.typ": letter-profile
|
||||
|
||||
#let addressee = recipient(
|
||||
company: "ООО «Получатель»",
|
||||
title: "Генеральному директору",
|
||||
name: "П.П. Получателю",
|
||||
address: "г. Пример, ул. Адресная, 10",
|
||||
)
|
||||
|
||||
#let appendices = attachment-set(items: (
|
||||
attachment(
|
||||
"specification",
|
||||
"Краткая спецификация",
|
||||
[
|
||||
Состав приложения задаётся пользователем независимо от профиля письма.
|
||||
],
|
||||
),
|
||||
))
|
||||
|
||||
#show: document.with(
|
||||
company: "test-company",
|
||||
profile: letter-profile(
|
||||
recipient: addressee,
|
||||
date: "26.08.2026",
|
||||
reference: "Л-001",
|
||||
title: "О направлении материалов",
|
||||
note: "Исполнитель: А.А. Автор, +7 (000) 000-00-01",
|
||||
attachments: appendices,
|
||||
render_attachments: true,
|
||||
),
|
||||
)
|
||||
|
||||
Уважаемый Пётр Петрович!
|
||||
|
||||
Направляем материалы для рассмотрения. Профиль письма использует единый источник данных компании и самостоятельно формирует шапку, подвал, список приложений и блок подписи.
|
||||
|
||||
Просим подтвердить получение документов.
|
||||
@@ -0,0 +1,90 @@
|
||||
#import "/.template/lib/index.typ": numbered-list
|
||||
|
||||
#set page("a4", margin: 1cm)
|
||||
#set text(size: 9pt)
|
||||
#set par(first-line-indent: 0pt, leading: 0.4em)
|
||||
|
||||
= SCIENTIA LIST COUNTERS
|
||||
|
||||
#let compact-list(style, body) = numbered-list(
|
||||
levels: (style,),
|
||||
suffixes: ".",
|
||||
line-leading: 0.4em,
|
||||
item-spacing: 0pt,
|
||||
body,
|
||||
)
|
||||
|
||||
== Арабские числа
|
||||
|
||||
#compact-list("1")[
|
||||
+ Арабские: первый
|
||||
+ Арабские: второй
|
||||
+ Арабские: третий
|
||||
]
|
||||
|
||||
== Кириллица
|
||||
|
||||
#grid(
|
||||
columns: (1fr, 1fr),
|
||||
gutter: 1cm,
|
||||
[
|
||||
#compact-list("А")[
|
||||
+ Кириллица верхняя: первый
|
||||
+ Кириллица верхняя: второй
|
||||
+ Кириллица верхняя: третий
|
||||
]
|
||||
],
|
||||
[
|
||||
#compact-list("а")[
|
||||
+ Кириллица нижняя: первый
|
||||
+ Кириллица нижняя: второй
|
||||
+ Кириллица нижняя: третий
|
||||
]
|
||||
],
|
||||
)
|
||||
|
||||
== Латиница
|
||||
|
||||
#grid(
|
||||
columns: (1fr, 1fr),
|
||||
gutter: 1cm,
|
||||
[
|
||||
#compact-list("A")[
|
||||
+ Латиница верхняя: первый
|
||||
+ Латиница верхняя: второй
|
||||
+ Латиница верхняя: третий
|
||||
]
|
||||
],
|
||||
[
|
||||
#compact-list("a")[
|
||||
+ Латиница нижняя: первый
|
||||
+ Латиница нижняя: второй
|
||||
+ Латиница нижняя: третий
|
||||
]
|
||||
],
|
||||
)
|
||||
|
||||
== Ведущий ноль
|
||||
|
||||
#compact-list("01")[
|
||||
+ Ведущий ноль: первый
|
||||
+ Ведущий ноль: второй
|
||||
+ Ведущий ноль: третий
|
||||
]
|
||||
|
||||
== Вложенные счётчики
|
||||
|
||||
#numbered-list(
|
||||
levels: ("1", "а", "A"),
|
||||
full: true,
|
||||
separators: ".",
|
||||
suffixes: ".",
|
||||
line-leading: 0.4em,
|
||||
item-spacing: 0pt,
|
||||
)[
|
||||
+ Корневой уровень
|
||||
+ Второй уровень: первый
|
||||
+ Второй уровень: второй
|
||||
+ Третий уровень: первый
|
||||
+ Третий уровень: второй
|
||||
]
|
||||
@@ -0,0 +1,60 @@
|
||||
#import "/.template/lib/index.typ": document, profiles, recipient, party, signer, load-company
|
||||
|
||||
#let profile-id = sys.inputs.at("profile", default: "letter")
|
||||
#let mode = sys.inputs.at("mode", default: "draft")
|
||||
#let company-id = sys.inputs.at("company", default: "test-company")
|
||||
#let company = load-company(company-id)
|
||||
#let addressee = recipient(company: "ООО «Проверка режима»")
|
||||
|
||||
#let left = party(
|
||||
"left",
|
||||
"Исполнитель",
|
||||
company.legal.name,
|
||||
legal: company.legal,
|
||||
contacts: company.contacts,
|
||||
banking: company.banking,
|
||||
representative: signer(
|
||||
company.director.name,
|
||||
company.director.title,
|
||||
signature: company.resources.signature,
|
||||
stamp: company.resources.stamp,
|
||||
),
|
||||
)
|
||||
#let right = party(
|
||||
"right",
|
||||
"Заказчик",
|
||||
"ООО «Проверка режима»",
|
||||
representative: signer("П.П. Проверяющий", "Директор"),
|
||||
)
|
||||
|
||||
#let selected-profile = if profile-id == "report" {
|
||||
profiles.report(title: "Проверка режима выпуска", year: 2026)
|
||||
} else if profile-id == "letter" {
|
||||
profiles.letter(recipient: addressee, title: "Проверка режима выпуска")
|
||||
} else if profile-id == "commercial-offer" {
|
||||
profiles.commercial_offer(
|
||||
recipient: addressee,
|
||||
subject: "Проверка режима выпуска",
|
||||
amount: "100 000",
|
||||
currency: "руб.",
|
||||
)
|
||||
} else if profile-id == "contract" {
|
||||
profiles.contract(
|
||||
number: "MODE-1",
|
||||
date: "26.08.2026",
|
||||
parties: (left, right),
|
||||
sections: (
|
||||
profiles.contract_section("subject", "Предмет договора", [Проверка режима выпуска.]),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
panic("Неизвестный тестовый profile: " + profile-id)
|
||||
}
|
||||
|
||||
#show: document.with(
|
||||
company: company,
|
||||
profile: selected-profile,
|
||||
options: (mode: mode, watermark: if mode == "draft" { "ЧЕРНОВИК" } else { none }),
|
||||
)
|
||||
|
||||
Документ собран в режиме #mode для профиля #profile-id.
|
||||
@@ -0,0 +1,5 @@
|
||||
#import "/.template/lib/index.typ": document
|
||||
|
||||
#show: document.with(company: "unknown-company")
|
||||
|
||||
Этот текст не должен попасть в PDF.
|
||||
@@ -0,0 +1,13 @@
|
||||
#import "/.template/lib/index.typ": document, profiles, party
|
||||
|
||||
#let duplicate = (
|
||||
party("same", "Исполнитель", "ООО «Первый»"),
|
||||
party("same", "Заказчик", "ООО «Второй»"),
|
||||
)
|
||||
|
||||
#show: document.with(
|
||||
company: "test-company",
|
||||
profile: profiles.contract(number: "1", parties: duplicate),
|
||||
)
|
||||
|
||||
Этот текст не должен попасть в PDF.
|
||||
@@ -0,0 +1,8 @@
|
||||
#import "/.template/lib/index.typ": document, profiles
|
||||
|
||||
#show: document.with(
|
||||
company: "test-company",
|
||||
profile: profiles.letter(),
|
||||
)
|
||||
|
||||
Этот текст не должен попасть в PDF.
|
||||
@@ -0,0 +1,5 @@
|
||||
#import "/.template/lib/index.typ": numbered-list
|
||||
|
||||
#numbered-list(scheme: "неизвестная-схема")[
|
||||
+ Тестовый пункт
|
||||
]
|
||||
@@ -0,0 +1,11 @@
|
||||
#import "/.template/lib/index.typ": document, profiles, recipient
|
||||
|
||||
#show: document.with(
|
||||
company: "test-company",
|
||||
profile: profiles.commercial_offer(
|
||||
recipient: recipient(company: "ООО «Адресат»"),
|
||||
subject: "Предложение без стоимости",
|
||||
),
|
||||
)
|
||||
|
||||
Этот текст не должен попасть в PDF.
|
||||
@@ -0,0 +1,3 @@
|
||||
#import "/.template/lib/index.typ": vref
|
||||
|
||||
#vref(<missing-reference>, grammatical-case: "мест")
|
||||
@@ -0,0 +1,5 @@
|
||||
#import "/.template/lib/index.typ": vrefs
|
||||
|
||||
#vrefs((<valid-label>, "not-a-label"))
|
||||
|
||||
#figure(rect(width: 1cm, height: 1cm), caption: [Элемент]) <valid-label>
|
||||
@@ -0,0 +1,11 @@
|
||||
#import "/.template/lib/index.typ": document, profiles
|
||||
|
||||
#show: document.with(
|
||||
company: "test-company",
|
||||
profile: profiles.report(
|
||||
title: "Ошибка пути приложения",
|
||||
appendices: ("appendix.typ",),
|
||||
),
|
||||
)
|
||||
|
||||
Проверка понятной ошибки для строкового пути.
|
||||
@@ -0,0 +1,39 @@
|
||||
#import "/.template/lib/index.typ": document, profiles, recipient, company-profile
|
||||
|
||||
#let policy = sys.inputs.at("policy", default: "placeholder")
|
||||
#let company = company-profile(
|
||||
"no-media",
|
||||
(
|
||||
name: "ООО «Компания без встроенных графических ресурсов»",
|
||||
short-name: "Без ресурсов",
|
||||
jurisdiction: "RU",
|
||||
inn: "0000000000",
|
||||
),
|
||||
contacts: (
|
||||
email: "no-media@example.invalid",
|
||||
phone: "+7 (000) 000-00-00",
|
||||
address: "г. Пример, очень длинный адрес для проверки переноса строк в деловом документе, дом 100, офис 200",
|
||||
),
|
||||
banking: (:),
|
||||
brand: (color: rgb("e39f49")),
|
||||
director: (title: "Генеральный директор", name: "И.И. Подписант"),
|
||||
resources: (logo: none, signature: none, stamp: none),
|
||||
)
|
||||
|
||||
#show: document.with(
|
||||
company: company,
|
||||
profile: profiles.letter(
|
||||
recipient: recipient(
|
||||
company: "Акционерное общество «Организация с длинным наименованием для проверки устойчивости вёрстки»",
|
||||
title: "Заместителю генерального директора по техническим и коммерческим вопросам",
|
||||
name: "П.П. Получателю",
|
||||
address: "г. Пример, проспект Испытательный, дом 123, строение 45",
|
||||
),
|
||||
date: "26.08.2026",
|
||||
reference: "NO-MEDIA-001",
|
||||
title: "Проверка формирования документа без логотипа, подписи и печати",
|
||||
),
|
||||
options: (mode: "final", media-policy: policy),
|
||||
)
|
||||
|
||||
Документ без графических ресурсов собран с политикой #policy.
|
||||
@@ -0,0 +1,49 @@
|
||||
#import "/.template/lib/api.typ": load-company, recipient, signer
|
||||
#import "/.template/lib/presentation/foundation.typ": apply-foundation, render-media-slot
|
||||
#import "/.template/lib/presentation/components.typ": corp_table, company-footer, letter-header, signature-block
|
||||
|
||||
#let company = load-company("test-company")
|
||||
#let addressee = recipient(
|
||||
company: "ООО «Получатель»",
|
||||
title: "Руководителю проекта",
|
||||
name: "П.П. Примерову",
|
||||
)
|
||||
#let director = signer(
|
||||
company.director.name,
|
||||
company.director.title,
|
||||
signature: company.resources.signature,
|
||||
stamp: company.resources.stamp,
|
||||
)
|
||||
#let ctx = (
|
||||
company: company,
|
||||
options: (watermark: none),
|
||||
)
|
||||
|
||||
#show: apply-foundation.with(
|
||||
ctx: ctx,
|
||||
page-options: (margin: (x: 2cm, y: 1.5cm)),
|
||||
)
|
||||
|
||||
#letter-header(
|
||||
company,
|
||||
addressee,
|
||||
date: "26.08.2026",
|
||||
reference: "TEST-1",
|
||||
title: "Проверка компонентов",
|
||||
)
|
||||
|
||||
#v(0.8cm)
|
||||
Общая presentation-основа не зависит от конкретного профиля документа.
|
||||
|
||||
#v(0.8cm)
|
||||
#corp_table(
|
||||
columns: 2,
|
||||
[Компонент], [Состояние],
|
||||
[Таблица], [Работает],
|
||||
[Media slot], [#render-media-slot(none, width: 3cm, height: 1cm)],
|
||||
)
|
||||
|
||||
#v(1cm)
|
||||
#signature-block(director, company: company)
|
||||
|
||||
#place(bottom + left, dy: 0.5cm, company-footer(company))
|
||||
@@ -0,0 +1,49 @@
|
||||
#import "/.template/lib/index.typ": vref, vrefs
|
||||
|
||||
#set page(paper: "a4", margin: 15mm)
|
||||
#set text(lang: "ru", size: 9pt)
|
||||
#set heading(numbering: "1.")
|
||||
#set figure(numbering: "1")
|
||||
|
||||
#figure(
|
||||
rect(width: 12mm, height: 6mm),
|
||||
caption: [Первый рисунок],
|
||||
) <case-figure-a>
|
||||
|
||||
#figure(
|
||||
rect(width: 12mm, height: 6mm),
|
||||
caption: [Второй рисунок],
|
||||
) <case-figure-b>
|
||||
|
||||
#figure(
|
||||
table(columns: 1, [Значение]),
|
||||
caption: [Тестовая таблица],
|
||||
) <case-table>
|
||||
|
||||
#math.equation(block: true, numbering: "(1)")[$x = 1$] <case-equation>
|
||||
|
||||
= Тестовый раздел <case-section>
|
||||
|
||||
Предложный по умолчанию: на #vref(<case-figure-a>).
|
||||
|
||||
Именительный коротко: #vref(<case-figure-a>, "и").
|
||||
|
||||
Родительный коротко: без #vref(<case-figure-a>, "р").
|
||||
|
||||
Дательный коротко: к #vref(<case-figure-a>, "д").
|
||||
|
||||
Винительный коротко: вижу #vref(<case-figure-a>, "в").
|
||||
|
||||
Творительный коротко: перед #vref(<case-figure-a>, "т").
|
||||
|
||||
Предложный коротко: на #vref(<case-figure-a>, "п").
|
||||
|
||||
Явная строчная форма: на #vref(<case-figure-a>, capitalized: false).
|
||||
|
||||
Полное название: на #vref(<case-table>, "предложный").
|
||||
|
||||
Формула: согласно #vref(<case-equation>, "дательный").
|
||||
|
||||
Раздел: в #vref(<case-section>).
|
||||
|
||||
Группа по умолчанию: на #vrefs((<case-figure-a>, <case-figure-b>)).
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
= Исходные данные <test-appendix-source>
|
||||
|
||||
Первое приложение подключено отдельным файлом.
|
||||
|
||||
#figure(
|
||||
rect(width: 4cm, height: 1.5cm, fill: luma(235), stroke: 0.5pt),
|
||||
caption: [Схема первого приложения],
|
||||
) <test-appendix-figure>
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
= Проверочные расчёты <test-appendix-calculations>
|
||||
|
||||
Второе приложение автоматически продолжает кириллическую нумерацию.
|
||||
|
||||
#figure(
|
||||
table(
|
||||
columns: (1fr, 1fr),
|
||||
[Параметр], [Значение],
|
||||
[Коэффициент], [1,3],
|
||||
),
|
||||
caption: [Таблица второго приложения],
|
||||
) <test-appendix-table>
|
||||
@@ -0,0 +1,38 @@
|
||||
#import "/.template/lib/index.typ": document, profiles, vref, vrefs
|
||||
|
||||
#show: document.with(
|
||||
company: "test-company",
|
||||
profile: profiles.report(
|
||||
title: "Проверка файловых приложений",
|
||||
year: 2026,
|
||||
executors: (),
|
||||
appendices: (
|
||||
path("appendices/01-source-data.typ"),
|
||||
path("appendices/02-calculations.typ"),
|
||||
),
|
||||
appendix_numbering: "cyrillic",
|
||||
appendix_start: 1,
|
||||
show_title_page: false,
|
||||
show_executors: false,
|
||||
show_outline: true,
|
||||
),
|
||||
options: (mode: "clean-copy", media-policy: "hide"),
|
||||
)
|
||||
|
||||
= ОСНОВНОЙ ТЕКСТ
|
||||
|
||||
Стандартная ссылка: @test-appendix-source.
|
||||
|
||||
Именительный: #vref(<test-appendix-source>, "и").
|
||||
|
||||
Родительный: без #vref(<test-appendix-source>, "р").
|
||||
|
||||
Дательный: к #vref(<test-appendix-source>, "д").
|
||||
|
||||
Винительный: вижу #vref(<test-appendix-source>, "в").
|
||||
|
||||
Творительный: перед #vref(<test-appendix-source>, "т").
|
||||
|
||||
Предложный по умолчанию: в #vref(<test-appendix-source>).
|
||||
|
||||
Группа приложений: в #vrefs((<test-appendix-source>, <test-appendix-calculations>)).
|
||||
@@ -0,0 +1,71 @@
|
||||
#import "/.template/lib/report.typ": report
|
||||
#import "/.template/lib/company.typ": company-defaults
|
||||
#import "/.template/lib/components.typ": corp_table, formula
|
||||
#import "/.template/lib/appendices.typ": make_appendices
|
||||
#import "/.template/lib/references.typ": vref, eqref
|
||||
|
||||
#show: report.with(
|
||||
..company-defaults(company-id: "test-company"),
|
||||
udk: "000.000",
|
||||
director_date: "«26» августа 2026 г.",
|
||||
is_research: true,
|
||||
title: "Тестовый отчёт",
|
||||
theme: "Регрессионная проверка шаблона",
|
||||
is_intermediate: true,
|
||||
stage_num: 1,
|
||||
vol_num: 1,
|
||||
contract_num: "TEST-001",
|
||||
contract_date: "«26» августа 2026",
|
||||
year: 2026,
|
||||
executors: (
|
||||
("Ответственный исполнитель", "Тестов И.И.", path("/.template/companies/test-company/sign.svg"), 0.5cm),
|
||||
("Инженер", "Примеров П.П.", path("/.template/companies/test-company/sign.svg"), 0.5cm),
|
||||
),
|
||||
)
|
||||
|
||||
#heading(numbering: none)[ВВЕДЕНИЕ]
|
||||
|
||||
Этот документ фиксирует исходное поведение шаблона перед архитектурным рефакторингом.
|
||||
|
||||
= ОСНОВНОЙ РАЗДЕЛ
|
||||
|
||||
Сложные элементы должны сохранять нумерацию, подписи и ссылки.
|
||||
|
||||
#figure(
|
||||
image("/.template/companies/test-company/logo.svg", width: 35%),
|
||||
caption: [Синтетический логотип для визуального теста],
|
||||
) <baseline-figure>
|
||||
|
||||
Ссылка на рисунок: #vref(<baseline-figure>, grammatical-case: "вин").
|
||||
|
||||
#figure(
|
||||
corp_table(
|
||||
columns: (1fr, 1fr, 1fr),
|
||||
table.cell(rowspan: 2, align: center + horizon)[Параметр],
|
||||
table.cell(colspan: 2, align: center + horizon)[Значения],
|
||||
[Минимум], [Максимум],
|
||||
[Тестовая строка], [10], [20],
|
||||
[Вторая строка], [30], [40],
|
||||
),
|
||||
caption: [Таблица с многострочной шапкой],
|
||||
) <baseline-table>
|
||||
|
||||
Ссылка на таблицу: #vref(<baseline-table>, grammatical-case: "предл").
|
||||
|
||||
#formula($a^2 + b^2 = c^2$) <baseline-equation>
|
||||
|
||||
Ссылка на формулу: #eqref(<baseline-equation>).
|
||||
|
||||
#pagebreak()
|
||||
#heading(numbering: none)[ЗАКЛЮЧЕНИЕ]
|
||||
|
||||
Компиляция и визуальное сравнение подтверждают стабильность ключевых элементов.
|
||||
|
||||
#pagebreak()
|
||||
#heading(numbering: none)[СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ]
|
||||
#bibliography("/assets/references.bib", title: none, style: "gost-r-705-2008-numeric")
|
||||
|
||||
#show: make_appendices
|
||||
= ТЕСТОВОЕ ПРИЛОЖЕНИЕ
|
||||
|
||||
Содержимое приложения используется для проверки кириллической нумерации.
|
||||
@@ -0,0 +1,3 @@
|
||||
= Тестовое приложение <new-test-appendix>
|
||||
|
||||
Приложение создано как самостоятельный Typst-файл.
|
||||
@@ -0,0 +1,87 @@
|
||||
#import "/.template/lib/api.typ": document, bibliography-section
|
||||
#import "/.template/lib/presentation/profiles/report.typ": report-profile
|
||||
#import "/.template/lib/presentation/components.typ": corp_table, formula
|
||||
#import "/.template/lib/presentation/references.typ": vref, vrefs, eqref
|
||||
|
||||
#let appendices = (
|
||||
path("appendix.typ"),
|
||||
)
|
||||
|
||||
#let bibliographies = (
|
||||
bibliography-section(
|
||||
"normative",
|
||||
path("normative.bib"),
|
||||
title: [НОРМАТИВНЫЕ ИСТОЧНИКИ],
|
||||
group: "report-sources",
|
||||
),
|
||||
bibliography-section(
|
||||
"science",
|
||||
path("science.bib"),
|
||||
title: [НАУЧНЫЕ ИСТОЧНИКИ],
|
||||
group: "report-sources",
|
||||
),
|
||||
)
|
||||
|
||||
#show: document.with(
|
||||
company: "test-company",
|
||||
profile: report-profile(
|
||||
title: "Новый модульный отчёт",
|
||||
theme: "Проверка DDD-профиля",
|
||||
udk: "000.000",
|
||||
director_date: "«26» августа 2026 г.",
|
||||
is_research: true,
|
||||
is_intermediate: true,
|
||||
stage_number: 1,
|
||||
volume_number: 1,
|
||||
contract_number: "DDD-001",
|
||||
contract_date: "«26» августа 2026",
|
||||
city: "Пример",
|
||||
year: 2026,
|
||||
executors: (
|
||||
("Ответственный исполнитель", "Тестов И.И.", path("/.template/companies/test-company/sign.svg"), 0.5cm),
|
||||
),
|
||||
appendices: appendices,
|
||||
appendix_numbering: "cyrillic",
|
||||
appendix_start: 1,
|
||||
bibliographies: bibliographies,
|
||||
),
|
||||
)
|
||||
|
||||
#heading(numbering: none)[ВВЕДЕНИЕ]
|
||||
|
||||
Профиль отчёта использует нормативный источник @normative-test и научную публикацию @science-test.
|
||||
|
||||
Тестовое содержимое вынесено в #vref(<new-test-appendix>).
|
||||
|
||||
= ПРОВЕРКА ССЫЛОК
|
||||
|
||||
#figure(
|
||||
rect(width: 3cm, height: 1.5cm, fill: rgb("fbb20d")),
|
||||
caption: [Первый тестовый рисунок],
|
||||
) <new-figure-a>
|
||||
|
||||
#figure(
|
||||
circle(radius: 0.7cm, fill: rgb("e39f49")),
|
||||
caption: [Второй тестовый рисунок],
|
||||
) <new-figure-b>
|
||||
|
||||
Одна ссылка: #vref(<new-figure-a>, grammatical-case: "предл").
|
||||
|
||||
Несколько ссылок: #vrefs((<new-figure-a>, <new-figure-b>), grammatical-case: "вин").
|
||||
|
||||
#figure(
|
||||
corp_table(
|
||||
columns: 2,
|
||||
[Параметр], [Значение],
|
||||
[Архитектура], [Изолированная],
|
||||
),
|
||||
caption: [Проверочная таблица],
|
||||
)
|
||||
|
||||
#formula($x^2 + y^2 = z^2$) <new-equation>
|
||||
|
||||
Формула: #eqref(<new-equation>).
|
||||
|
||||
#heading(numbering: none)[ЗАКЛЮЧЕНИЕ]
|
||||
|
||||
Новый отчёт собирается через `document` и `report-profile`.
|
||||
@@ -0,0 +1,6 @@
|
||||
@book{normative-test,
|
||||
title={Синтетический нормативный источник},
|
||||
author={{Тестовый регулятор}},
|
||||
year={2026},
|
||||
publisher={Тестовое издательство}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
@article{science-test,
|
||||
title={Синтетическое исследование устойчивости шаблонов},
|
||||
author={Тестов, И. И.},
|
||||
year={2026},
|
||||
journal={Журнал тестовых данных},
|
||||
volume={1},
|
||||
pages={1--10}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
#import "/.template/lib/index.typ": document, profiles, load-company
|
||||
|
||||
#let enabled(name) = sys.inputs.at(name, default: "true") == "true"
|
||||
#let company = load-company("test-company", signature: none, stamp: none)
|
||||
#let executors = (
|
||||
("Ответственный исполнитель", "Тестов И.И.", none, 0cm),
|
||||
)
|
||||
|
||||
#show: document.with(
|
||||
company: company,
|
||||
profile: profiles.report(
|
||||
title: "Контрольный отчёт",
|
||||
theme: "Независимые служебные страницы",
|
||||
city: "Екатеринбург",
|
||||
year: 2026,
|
||||
executors: executors,
|
||||
show_title_page: enabled("title"),
|
||||
show_executors: enabled("executors"),
|
||||
show_outline: enabled("outline"),
|
||||
),
|
||||
options: (
|
||||
mode: "clean-copy",
|
||||
watermark: none,
|
||||
media-policy: "hide",
|
||||
diagnostics: true,
|
||||
),
|
||||
)
|
||||
|
||||
= ОСНОВНОЙ ТЕКСТ
|
||||
|
||||
Основной текст контрольного отчёта начинается без пустой промежуточной страницы.
|
||||
@@ -0,0 +1,136 @@
|
||||
#import "/.template/lib/index.typ": document, profiles, corp-table, formula, vref, vrefs, eqref
|
||||
|
||||
#let stress-rows = range(1, 73).map(index => (
|
||||
[#index],
|
||||
[
|
||||
#if index == 3 {
|
||||
[Составной параметр:
|
||||
- первый вложенный пункт;
|
||||
- второй вложенный пункт с длинным пояснением внутри ячейки.]
|
||||
} else {
|
||||
[Параметр #index с достаточно длинным наименованием для проверки переноса строк]
|
||||
}
|
||||
],
|
||||
[
|
||||
#if index == 4 {
|
||||
[Значение #index. #link("https://example.invalid/table-cell")[Ссылка внутри ячейки] сохраняет кликабельность и не меняет отступы.]
|
||||
} else {
|
||||
[Значение #index. Текст ячейки переносится, но сохраняет одинаковые внутренние поля и межстрочный интервал.]
|
||||
}
|
||||
],
|
||||
)).flatten()
|
||||
|
||||
#show: document.with(
|
||||
company: "test-company",
|
||||
profile: profiles.report(
|
||||
title: "Стресс-тест типографики",
|
||||
theme: "Таблицы, списки, формулы и грамматические ссылки",
|
||||
year: 2026,
|
||||
executors: (),
|
||||
),
|
||||
)
|
||||
|
||||
= КОНТЕКСТНЫЕ ИНТЕРВАЛЫ
|
||||
|
||||
== Заголовок перед абзацем
|
||||
|
||||
Обычный абзац после заголовка должен иметь устойчивую красную строку, нормативный межстрочный интервал и не зависеть от элемента, который находился перед заголовком.
|
||||
|
||||
== Заголовок перед маркированным списком
|
||||
|
||||
- Первый уровень маркированного списка;
|
||||
- второй уровень с длинным текстом, который переносится на новую строку без смещения маркера;
|
||||
- третий уровень.
|
||||
- Возврат на первый уровень.
|
||||
|
||||
== Заголовок перед нумерованным списком
|
||||
|
||||
+ Основной пункт
|
||||
+ Второй уровень
|
||||
+ Третий уровень
|
||||
+ Следующий основной пункт
|
||||
|
||||
== Соседние заголовки
|
||||
=== Заголовок третьего уровня
|
||||
==== Заголовок четвёртого уровня
|
||||
|
||||
Текст после цепочки заголовков.
|
||||
|
||||
= ССЫЛКИ С ПАДЕЖАМИ <stress-section>
|
||||
|
||||
#figure(
|
||||
rect(width: 3cm, height: 1.3cm, fill: rgb("fbb20d")),
|
||||
caption: [Первый контрольный рисунок],
|
||||
) <stress-figure-a>
|
||||
|
||||
#figure(
|
||||
circle(radius: 0.65cm, fill: rgb("e39f49")),
|
||||
caption: [Второй контрольный рисунок],
|
||||
) <stress-figure-b>
|
||||
|
||||
#formula($a^2 + b^2 = c^2$) <stress-equation>
|
||||
|
||||
Именительный: #vref(<stress-figure-a>, grammatical-case: "имен").
|
||||
Родительный: без #vref(<stress-figure-a>, grammatical-case: "род").
|
||||
Дательный: к #vref(<stress-figure-a>, grammatical-case: "дат").
|
||||
Винительный: см. #vref(<stress-figure-a>, grammatical-case: "вин").
|
||||
Творительный: перед #vref(<stress-figure-a>, grammatical-case: "тв").
|
||||
Предложный: на #vref(<stress-figure-a>, grammatical-case: "предл").
|
||||
|
||||
Групповая ссылка: на #vrefs(
|
||||
(<stress-figure-a>, <stress-figure-b>),
|
||||
grammatical-case: "предл",
|
||||
).
|
||||
|
||||
Формула в дательном падеже: к #vref(<stress-equation>, grammatical-case: "дат"); короткая ссылка #eqref(<stress-equation>).
|
||||
|
||||
Ссылка на раздел: в #vref(<stress-section>, grammatical-case: "предл").
|
||||
|
||||
Внешняя ссылка: #link("https://example.invalid/specification")[контрольная спецификация].
|
||||
|
||||
= МНОГОСТРАНИЧНАЯ ТАБЛИЦА
|
||||
|
||||
#figure(
|
||||
corp-table(
|
||||
columns: (1.2cm, 1.7fr, 2.3fr),
|
||||
header: (
|
||||
table.cell(rowspan: 2, align: center + horizon)[№],
|
||||
table.cell(colspan: 2, align: center + horizon)[Контрольные данные],
|
||||
[Параметр],
|
||||
[Описание и значение],
|
||||
),
|
||||
body: stress-rows,
|
||||
repeat_header: true,
|
||||
continuation: true,
|
||||
continuation_text: "Продолжение таблицы",
|
||||
body-leading: 0.65em,
|
||||
body-inset: (x: 4pt, y: 3pt),
|
||||
justify: false,
|
||||
hyphenate: false,
|
||||
),
|
||||
caption: [Таблица, переходящая через несколько страниц],
|
||||
) <stress-table>
|
||||
|
||||
= ПРОВЕРКА ПОСЛЕ ТАБЛИЦЫ
|
||||
|
||||
Текст после таблицы не должен прилипать к последней строке. Ссылка на #vref(<stress-table>, grammatical-case: "вин") остаётся рабочей.
|
||||
|
||||
#pagebreak()
|
||||
= ВЫСОКАЯ СТРОКА ТАБЛИЦЫ
|
||||
|
||||
#let tall-cell = range(1, 66).map(index => [Строка высокой ячейки #index. Содержимое одной строки таблицы продолжается без потери границ.]).join(linebreak())
|
||||
|
||||
#figure(
|
||||
corp-table(
|
||||
columns: (3cm, 1fr),
|
||||
header: ([Идентификатор], [Высокая ячейка]),
|
||||
body: ([ROW-ONE], tall-cell),
|
||||
row_breakable: true,
|
||||
page_break: "fit",
|
||||
repeat_header: true,
|
||||
continuation: true,
|
||||
continuation_text: "Продолжение высокой таблицы",
|
||||
justify: false,
|
||||
),
|
||||
caption: [Одна высокая строка, переходящая через страницы],
|
||||
) <tall-row-table>
|
||||
@@ -0,0 +1,542 @@
|
||||
{
|
||||
"version": 1,
|
||||
"typst_min": "0.15.1",
|
||||
"creation_timestamp": 1787702400,
|
||||
"dpi": 110,
|
||||
"visual_threshold": 0.001,
|
||||
"unit_tests": [
|
||||
{
|
||||
"id": "legacy-numbering",
|
||||
"module": ".template/development/tests/domain/legacy-numbering.typ"
|
||||
},
|
||||
{
|
||||
"id": "core-domain",
|
||||
"module": ".template/development/tests/domain/core.typ"
|
||||
},
|
||||
{
|
||||
"id": "profile-domain",
|
||||
"module": ".template/development/tests/domain/profiles.typ"
|
||||
}
|
||||
],
|
||||
"fixtures": [
|
||||
{
|
||||
"id": "core-smoke",
|
||||
"entry": ".template/development/tests/fixtures/core-smoke/main.typ",
|
||||
"expected_pages": 1,
|
||||
"expected_page_size": "A4",
|
||||
"required_text": [
|
||||
"DDD CORE SMOKE",
|
||||
"ООО «Тестовая компания»",
|
||||
"Изолированное ядро успешно собрано"
|
||||
],
|
||||
"snapshot_pages": []
|
||||
},
|
||||
{
|
||||
"id": "facade-smoke",
|
||||
"entry": ".template/development/tests/fixtures/facade-smoke/main.typ",
|
||||
"expected_pages": 1,
|
||||
"expected_page_size": "A4",
|
||||
"required_text": [
|
||||
"ПУБЛИЧНЫЙ ФАСАД",
|
||||
"Корневой файл экспортирует единый фасад"
|
||||
],
|
||||
"snapshot_pages": []
|
||||
},
|
||||
{
|
||||
"id": "default-report",
|
||||
"entry": ".template/development/tests/fixtures/default-report/main.typ",
|
||||
"expected_pages": 3,
|
||||
"expected_page_size": "A4",
|
||||
"required_text": [
|
||||
"ПРОВЕРКА ПРОФИЛЯ ПО УМОЛЧАНИЮ",
|
||||
"Корневой фасад формирует отчёт"
|
||||
],
|
||||
"snapshot_pages": []
|
||||
},
|
||||
{
|
||||
"id": "report-service-pages-full",
|
||||
"entry": ".template/development/tests/fixtures/report-service-pages/main.typ",
|
||||
"inputs": {"title": "true", "executors": "true", "outline": "true"},
|
||||
"expected_pages": 4,
|
||||
"expected_page_size": "A4",
|
||||
"required_text": [
|
||||
"КОНТРОЛЬНЫЙ ОТЧЁТ",
|
||||
"СПИСОК ИСПОЛНИТЕЛЕЙ",
|
||||
"СОДЕРЖАНИЕ",
|
||||
"ОСНОВНОЙ ТЕКСТ"
|
||||
],
|
||||
"snapshot_pages": []
|
||||
},
|
||||
{
|
||||
"id": "report-service-pages-outline-only",
|
||||
"entry": ".template/development/tests/fixtures/report-service-pages/main.typ",
|
||||
"inputs": {"title": "false", "executors": "false", "outline": "true"},
|
||||
"expected_pages": 2,
|
||||
"expected_page_size": "A4",
|
||||
"required_text": ["СОДЕРЖАНИЕ", "ОСНОВНОЙ ТЕКСТ"],
|
||||
"forbidden_text": ["КОНТРОЛЬНЫЙ ОТЧЁТ", "СПИСОК ИСПОЛНИТЕЛЕЙ"],
|
||||
"snapshot_pages": []
|
||||
},
|
||||
{
|
||||
"id": "report-service-pages-title-only",
|
||||
"entry": ".template/development/tests/fixtures/report-service-pages/main.typ",
|
||||
"inputs": {"title": "true", "executors": "false", "outline": "false"},
|
||||
"expected_pages": 2,
|
||||
"expected_page_size": "A4",
|
||||
"required_text": ["КОНТРОЛЬНЫЙ ОТЧЁТ", "ОСНОВНОЙ ТЕКСТ"],
|
||||
"forbidden_text": ["СПИСОК ИСПОЛНИТЕЛЕЙ", "СОДЕРЖАНИЕ"],
|
||||
"snapshot_pages": []
|
||||
},
|
||||
{
|
||||
"id": "report-service-pages-executors-only",
|
||||
"entry": ".template/development/tests/fixtures/report-service-pages/main.typ",
|
||||
"inputs": {"title": "false", "executors": "true", "outline": "false"},
|
||||
"expected_pages": 2,
|
||||
"expected_page_size": "A4",
|
||||
"required_text": ["СПИСОК ИСПОЛНИТЕЛЕЙ", "ОСНОВНОЙ ТЕКСТ"],
|
||||
"forbidden_text": ["КОНТРОЛЬНЫЙ ОТЧЁТ", "СОДЕРЖАНИЕ"],
|
||||
"snapshot_pages": []
|
||||
},
|
||||
{
|
||||
"id": "report-service-pages-body-only",
|
||||
"entry": ".template/development/tests/fixtures/report-service-pages/main.typ",
|
||||
"inputs": {"title": "false", "executors": "false", "outline": "false"},
|
||||
"expected_pages": 1,
|
||||
"expected_page_size": "A4",
|
||||
"required_text": ["ОСНОВНОЙ ТЕКСТ", "Основной текст контрольного отчёта"],
|
||||
"forbidden_text": ["КОНТРОЛЬНЫЙ ОТЧЁТ", "СПИСОК ИСПОЛНИТЕЛЕЙ", "СОДЕРЖАНИЕ"],
|
||||
"snapshot_pages": []
|
||||
},
|
||||
{
|
||||
"id": "presentation-smoke",
|
||||
"entry": ".template/development/tests/fixtures/presentation-smoke/main.typ",
|
||||
"expected_pages": 1,
|
||||
"expected_page_size": "A4",
|
||||
"required_text": [
|
||||
"ПРОВЕРКА КОМПОНЕНТОВ",
|
||||
"Общая presentation-основа",
|
||||
"Media slot"
|
||||
],
|
||||
"snapshot_pages": [1]
|
||||
},
|
||||
{
|
||||
"id": "report-new",
|
||||
"entry": ".template/development/tests/fixtures/report-new/main.typ",
|
||||
"expected_pages": 7,
|
||||
"expected_page_size": "A4",
|
||||
"required_text": [
|
||||
"Проверка DDD-профиля",
|
||||
"НОРМАТИВНЫЕ ИСТОЧНИКИ",
|
||||
"НАУЧНЫЕ ИСТОЧНИКИ",
|
||||
"Приложение А"
|
||||
],
|
||||
"snapshot_pages": [1, 2, 5, 6]
|
||||
},
|
||||
{
|
||||
"id": "report-stress",
|
||||
"entry": ".template/development/tests/fixtures/report-stress/main.typ",
|
||||
"expected_pages": 12,
|
||||
"expected_page_size": "A4",
|
||||
"required_text": [
|
||||
"Таблицы, списки, формулы и грамматические ссылки",
|
||||
"1. Основной пункт",
|
||||
"а) Второй уровень",
|
||||
"1) Третий уровень",
|
||||
"на Рисунках 2.1 и 2.2",
|
||||
"контрольная спецификация",
|
||||
"Продолжение таблицы 3.1",
|
||||
"Ссылка на Таблицу 3.1",
|
||||
"Продолжение высокой таблицы 5.1",
|
||||
"Строка высокой ячейки 65."
|
||||
],
|
||||
"required_text_counts": {
|
||||
"Контрольные данные": 6,
|
||||
"Продолжение таблицы 3.1": 5,
|
||||
"Высокая ячейка": 3,
|
||||
"Продолжение высокой таблицы 5.1": 2
|
||||
},
|
||||
"minimum_external_links": 2,
|
||||
"minimum_internal_links": 8,
|
||||
"snapshot_pages": [3, 4, 5, 9, 10, 11, 12]
|
||||
},
|
||||
{
|
||||
"id": "reference-cases",
|
||||
"entry": ".template/development/tests/fixtures/reference-cases/main.typ",
|
||||
"expected_pages": 1,
|
||||
"expected_page_size": "A4",
|
||||
"required_text": [
|
||||
"Предложный по умолчанию: на Рисунке",
|
||||
"Именительный коротко: Рисунок",
|
||||
"Родительный коротко: без Рисунка",
|
||||
"Дательный коротко: к Рисунку",
|
||||
"Винительный коротко: вижу Рисунок",
|
||||
"Творительный коротко: перед Рисунком",
|
||||
"Предложный коротко: на Рисунке",
|
||||
"Явная строчная форма: на рисунке",
|
||||
"Полное название: на Таблице",
|
||||
"Формула: согласно Формуле",
|
||||
"Раздел: в Разделе",
|
||||
"Группа по умолчанию: на Рисунках"
|
||||
],
|
||||
"snapshot_pages": []
|
||||
},
|
||||
{
|
||||
"id": "report-appendices",
|
||||
"entry": ".template/development/tests/fixtures/report-appendices/main.typ",
|
||||
"expected_pages": 4,
|
||||
"expected_page_size": "A4",
|
||||
"required_text": [
|
||||
"Приложение А. Исходные данные",
|
||||
"Приложение Б. Проверочные расчёты",
|
||||
"Стандартная ссылка: Приложение А",
|
||||
"Именительный: Приложение А",
|
||||
"Родительный: без Приложения А",
|
||||
"Дательный: к Приложению А",
|
||||
"Винительный: вижу Приложение А",
|
||||
"Творительный: перед Приложением А",
|
||||
"Предложный по умолчанию: в Приложении А",
|
||||
"Группа приложений: в Приложениях А и Б",
|
||||
"Рисунок А.1",
|
||||
"Таблица Б.1"
|
||||
],
|
||||
"minimum_internal_links": 8,
|
||||
"snapshot_pages": []
|
||||
},
|
||||
{
|
||||
"id": "letter",
|
||||
"entry": ".template/development/tests/fixtures/letter/main.typ",
|
||||
"expected_pages": 2,
|
||||
"expected_page_size": "A4",
|
||||
"required_text": [
|
||||
"О НАПРАВЛЕНИИ МАТЕРИАЛОВ",
|
||||
"Приложение 1: Краткая спецификация",
|
||||
"КРАТКАЯ СПЕЦИФИКАЦИЯ"
|
||||
],
|
||||
"snapshot_pages": [1, 2]
|
||||
},
|
||||
{
|
||||
"id": "commercial-offer",
|
||||
"entry": ".template/development/tests/fixtures/commercial-offer/main.typ",
|
||||
"expected_pages": 2,
|
||||
"expected_page_size": "A4",
|
||||
"required_text": [
|
||||
"ТЕХНИКО-КОММЕРЧЕСКОЕ ПРЕДЛОЖЕНИЕ",
|
||||
"1 250 000 руб.",
|
||||
"ТЕХНИЧЕСКОЕ ЗАДАНИЕ"
|
||||
],
|
||||
"snapshot_pages": [1, 2]
|
||||
},
|
||||
{
|
||||
"id": "contract",
|
||||
"entry": ".template/development/tests/fixtures/contract/main.typ",
|
||||
"expected_pages": 3,
|
||||
"expected_page_size": "A4",
|
||||
"required_text": [
|
||||
"ДОГОВОР ОКАЗАНИЯ УСЛУГ",
|
||||
"РЕКВИЗИТЫ И ПОДПИСИ СТОРОН",
|
||||
"СПЕЦИФИКАЦИЯ"
|
||||
],
|
||||
"snapshot_pages": [1, 2, 3]
|
||||
},
|
||||
{
|
||||
"id": "root-report",
|
||||
"entry": "main.typ",
|
||||
"expected_pages": 0,
|
||||
"expected_page_size": "A4",
|
||||
"required_text": [
|
||||
"ГЕОМЕХАНИЧЕСКОЕ ОБОСНОВАНИЕ УСТОЙЧИВОСТИ БОРТОВ",
|
||||
"СПИСОК ИСПОЛНИТЕЛЕЙ",
|
||||
"Результаты проверочных расчётов",
|
||||
"СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ",
|
||||
"Приложение А. Исходные данные",
|
||||
"Приложение Б. Дополнительные расчёты",
|
||||
"Рисунок Б.1"
|
||||
],
|
||||
"snapshot_pages": []
|
||||
},
|
||||
{
|
||||
"id": "public-report",
|
||||
"entry": "docs/examples/documents/report/main.typ",
|
||||
"expected_pages": 0,
|
||||
"expected_page_size": "A4",
|
||||
"required_text": [
|
||||
"Результаты проверочных расчётов",
|
||||
"СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ",
|
||||
"Приложение А. Исходные данные",
|
||||
"Приложение Б. Дополнительные расчёты",
|
||||
"Рисунок Б.1"
|
||||
],
|
||||
"snapshot_pages": []
|
||||
},
|
||||
{
|
||||
"id": "public-letter",
|
||||
"entry": "docs/examples/documents/letter/main.typ",
|
||||
"expected_pages": 0,
|
||||
"expected_page_size": "A4",
|
||||
"required_text": ["О НАПРАВЛЕНИИ МАТЕРИАЛОВ ЭТАПА 1", "Уважаемый Иван Иванович"],
|
||||
"snapshot_pages": []
|
||||
},
|
||||
{
|
||||
"id": "public-commercial-offer",
|
||||
"entry": "docs/examples/documents/commercial-offer/main.typ",
|
||||
"expected_pages": 0,
|
||||
"expected_page_size": "A4",
|
||||
"required_text": ["Геомеханическое сопровождение горных работ", "Состав и результат работ", "ТЕХНИЧЕСКОЕ ЗАДАНИЕ"],
|
||||
"snapshot_pages": []
|
||||
},
|
||||
{
|
||||
"id": "public-contract",
|
||||
"entry": "docs/examples/documents/contract/main.typ",
|
||||
"expected_pages": 0,
|
||||
"expected_page_size": "A4",
|
||||
"required_text": ["ДОГОВОР ОКАЗАНИЯ УСЛУГ", "ОТВЕТСТВЕННОСТЬ СТОРОН", "ТЕХНИЧЕСКОЕ ЗАДАНИЕ"],
|
||||
"snapshot_pages": []
|
||||
},
|
||||
{
|
||||
"id": "report-draft",
|
||||
"entry": ".template/development/tests/fixtures/modes/main.typ",
|
||||
"inputs": {"profile": "report", "mode": "draft"},
|
||||
"expected_pages": 3,
|
||||
"expected_page_size": "A4",
|
||||
"required_text": ["Документ собран в режиме draft"],
|
||||
"snapshot_pages": []
|
||||
},
|
||||
{
|
||||
"id": "report-clean-copy",
|
||||
"entry": ".template/development/tests/fixtures/modes/main.typ",
|
||||
"inputs": {"profile": "report", "mode": "clean-copy"},
|
||||
"expected_pages": 3,
|
||||
"expected_page_size": "A4",
|
||||
"required_text": ["Документ собран в режиме clean-copy"],
|
||||
"snapshot_pages": []
|
||||
},
|
||||
{
|
||||
"id": "letter-draft",
|
||||
"entry": ".template/development/tests/fixtures/modes/main.typ",
|
||||
"inputs": {"profile": "letter", "mode": "draft"},
|
||||
"expected_pages": 1,
|
||||
"expected_page_size": "A4",
|
||||
"required_text": ["Документ собран в режиме draft"],
|
||||
"snapshot_pages": []
|
||||
},
|
||||
{
|
||||
"id": "letter-clean-copy",
|
||||
"entry": ".template/development/tests/fixtures/modes/main.typ",
|
||||
"inputs": {"profile": "letter", "mode": "clean-copy"},
|
||||
"expected_pages": 1,
|
||||
"expected_page_size": "A4",
|
||||
"required_text": ["Документ собран в режиме clean-copy"],
|
||||
"snapshot_pages": []
|
||||
},
|
||||
{
|
||||
"id": "commercial-offer-draft",
|
||||
"entry": ".template/development/tests/fixtures/modes/main.typ",
|
||||
"inputs": {"profile": "commercial-offer", "mode": "draft"},
|
||||
"expected_pages": 1,
|
||||
"expected_page_size": "A4",
|
||||
"required_text": ["Документ собран в режиме draft"],
|
||||
"snapshot_pages": []
|
||||
},
|
||||
{
|
||||
"id": "commercial-offer-clean-copy",
|
||||
"entry": ".template/development/tests/fixtures/modes/main.typ",
|
||||
"inputs": {"profile": "commercial-offer", "mode": "clean-copy"},
|
||||
"expected_pages": 1,
|
||||
"expected_page_size": "A4",
|
||||
"required_text": ["Документ собран в режиме clean-copy"],
|
||||
"snapshot_pages": []
|
||||
},
|
||||
{
|
||||
"id": "contract-draft",
|
||||
"entry": ".template/development/tests/fixtures/modes/main.typ",
|
||||
"inputs": {"profile": "contract", "mode": "draft"},
|
||||
"expected_pages": 2,
|
||||
"expected_page_size": "A4",
|
||||
"required_text": ["Документ собран в режиме draft"],
|
||||
"snapshot_pages": []
|
||||
},
|
||||
{
|
||||
"id": "contract-clean-copy",
|
||||
"entry": ".template/development/tests/fixtures/modes/main.typ",
|
||||
"inputs": {"profile": "contract", "mode": "clean-copy"},
|
||||
"expected_pages": 2,
|
||||
"expected_page_size": "A4",
|
||||
"required_text": ["Документ собран в режиме clean-copy"],
|
||||
"snapshot_pages": []
|
||||
},
|
||||
{
|
||||
"id": "no-media-placeholder",
|
||||
"entry": ".template/development/tests/fixtures/no-media/main.typ",
|
||||
"inputs": {"policy": "placeholder"},
|
||||
"expected_pages": 1,
|
||||
"expected_page_size": "A4",
|
||||
"required_text": ["политикой placeholder"],
|
||||
"snapshot_pages": [1]
|
||||
},
|
||||
{
|
||||
"id": "no-media-hide",
|
||||
"entry": ".template/development/tests/fixtures/no-media/main.typ",
|
||||
"inputs": {"policy": "hide"},
|
||||
"expected_pages": 1,
|
||||
"expected_page_size": "A4",
|
||||
"required_text": ["политикой hide"],
|
||||
"snapshot_pages": []
|
||||
},
|
||||
{
|
||||
"id": "no-media-reserve-space",
|
||||
"entry": ".template/development/tests/fixtures/no-media/main.typ",
|
||||
"inputs": {"policy": "reserve-space"},
|
||||
"expected_pages": 1,
|
||||
"expected_page_size": "A4",
|
||||
"required_text": ["политикой reserve-space"],
|
||||
"snapshot_pages": []
|
||||
},
|
||||
{
|
||||
"id": "report-baseline",
|
||||
"entry": ".template/development/tests/fixtures/report-baseline/main.typ",
|
||||
"expected_pages": 7,
|
||||
"expected_page_size": "A4",
|
||||
"required_text": [
|
||||
"ТЕСТОВЫЙ ОТЧЁТ",
|
||||
"СПИСОК ИСПОЛНИТЕЛЕЙ",
|
||||
"СОДЕРЖАНИЕ",
|
||||
"Сложные элементы должны сохранять",
|
||||
"ТЕСТОВОЕ ПРИЛОЖЕНИЕ"
|
||||
],
|
||||
"snapshot_pages": [1, 2, 3, 4]
|
||||
},
|
||||
{
|
||||
"id": "list-counters",
|
||||
"entry": ".template/development/tests/fixtures/list-counters/main.typ",
|
||||
"expected_pages": 1,
|
||||
"expected_page_size": "A4",
|
||||
"required_text": [
|
||||
"1. Арабские: первый",
|
||||
"2. Арабские: второй",
|
||||
"3. Арабские: третий",
|
||||
"А. Кириллица верхняя: первый",
|
||||
"Б. Кириллица верхняя: второй",
|
||||
"В. Кириллица верхняя: третий",
|
||||
"а. Кириллица нижняя: первый",
|
||||
"б. Кириллица нижняя: второй",
|
||||
"в. Кириллица нижняя: третий",
|
||||
"A. Латиница верхняя: первый",
|
||||
"B. Латиница верхняя: второй",
|
||||
"C. Латиница верхняя: третий",
|
||||
"a. Латиница нижняя: первый",
|
||||
"b. Латиница нижняя: второй",
|
||||
"c. Латиница нижняя: третий",
|
||||
"01. Ведущий ноль: первый",
|
||||
"02. Ведущий ноль: второй",
|
||||
"03. Ведущий ноль: третий",
|
||||
"1.а. Второй уровень: первый",
|
||||
"1.б. Второй уровень: второй",
|
||||
"1.б.A. Третий уровень: первый",
|
||||
"1.б.B. Третий уровень: второй"
|
||||
],
|
||||
"snapshot_pages": []
|
||||
},
|
||||
{
|
||||
"id": "public-formatting-guide",
|
||||
"entry": "docs/examples/formatting/main.typ",
|
||||
"expected_pages": 0,
|
||||
"expected_page_size": "A4",
|
||||
"required_text": [
|
||||
"РИСУНКИ",
|
||||
"ТАБЛИЦЫ",
|
||||
"Обычная встроенная таблица",
|
||||
"Многострочная шапка и объединение ячеек",
|
||||
"ФОРМУЛЫ",
|
||||
"СПИСКИ И ТЕКСТ",
|
||||
"Быстрая многоуровневая схема ГОСТ",
|
||||
"Полная десятичная нумерация",
|
||||
"Доступные типы счётчиков",
|
||||
"Произвольные разделители",
|
||||
"Маркеры вместо чисел",
|
||||
"Геометрия и интервалы",
|
||||
"Продолжение с нужного номера",
|
||||
"1. Первый уровень.",
|
||||
"а) Второй уровень.",
|
||||
"1.1.1. Вложенный пункт.",
|
||||
"II. Второй.",
|
||||
"III. Третий.",
|
||||
"ii. Второй.",
|
||||
"iii. Третий.",
|
||||
"B. Второй.",
|
||||
"C. Третий.",
|
||||
"b. Второй.",
|
||||
"c. Третий.",
|
||||
"02. Второй.",
|
||||
"03. Третий.",
|
||||
"Б. Второй.",
|
||||
"В. Третий.",
|
||||
"б. Второй.",
|
||||
"в. Третий.",
|
||||
"1.а)A. Третий уровень",
|
||||
"A)1. Цифровой уровень.",
|
||||
"§ 001: Пользовательский уровень",
|
||||
"• Маркер •.",
|
||||
"∙ Маркер ∙.",
|
||||
"‣ Маркер ‣.",
|
||||
"⁃ Маркер ⁃.",
|
||||
"◦ Маркер ◦.",
|
||||
"08. Восьмой пункт",
|
||||
"09. Следующий пункт"
|
||||
],
|
||||
"snapshot_pages": []
|
||||
}
|
||||
],
|
||||
"negative_fixtures": [
|
||||
{
|
||||
"id": "negative-letter",
|
||||
"entry": ".template/development/tests/fixtures/negative-letter/main.typ",
|
||||
"required_errors": ["Scientia / letter.recipient"]
|
||||
},
|
||||
{
|
||||
"id": "negative-offer",
|
||||
"entry": ".template/development/tests/fixtures/negative-offer/main.typ",
|
||||
"required_errors": ["Scientia / commercial-offer.terms.amount"]
|
||||
},
|
||||
{
|
||||
"id": "negative-contract",
|
||||
"entry": ".template/development/tests/fixtures/negative-contract/main.typ",
|
||||
"required_errors": ["Scientia / parties", "повторяющийся id"]
|
||||
},
|
||||
{
|
||||
"id": "negative-company",
|
||||
"entry": ".template/development/tests/fixtures/negative-company/main.typ",
|
||||
"required_errors": ["Scientia / load-company.id", "неизвестная компания"]
|
||||
},
|
||||
{
|
||||
"id": "negative-references",
|
||||
"entry": ".template/development/tests/fixtures/negative-references/main.typ",
|
||||
"required_errors": ["Scientia / vrefs", "ожидался label"]
|
||||
},
|
||||
{
|
||||
"id": "negative-reference-case",
|
||||
"entry": ".template/development/tests/fixtures/negative-reference-case/main.typ",
|
||||
"required_errors": ["Scientia / reference", "неизвестный падеж"]
|
||||
},
|
||||
{
|
||||
"id": "negative-list-scheme",
|
||||
"entry": ".template/development/tests/fixtures/negative-list-scheme/main.typ",
|
||||
"required_errors": ["Scientia / numbered-list.scheme", "неизвестная схема"]
|
||||
},
|
||||
{
|
||||
"id": "negative-report-appendices",
|
||||
"entry": ".template/development/tests/fixtures/negative-report-appendices/main.typ",
|
||||
"required_errors": ["Scientia / report.appendices", "каждый элемент должен быть path"]
|
||||
}
|
||||
],
|
||||
"compile_matrices": [
|
||||
{
|
||||
"id": "company-profile-matrix",
|
||||
"entry": ".template/development/tests/fixtures/modes/main.typ",
|
||||
"parameters": {
|
||||
"company": ["scientia", "technology", "too", "test-company"],
|
||||
"profile": ["report", "letter", "commercial-offer", "contract"],
|
||||
"mode": ["final"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,645 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import itertools
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[3]
|
||||
TEST_ROOT = ROOT / ".template" / "development" / "tests"
|
||||
MANIFEST_PATH = TEST_ROOT / "manifest.json"
|
||||
SNAPSHOT_ROOT = TEST_ROOT / "snapshots"
|
||||
|
||||
|
||||
class TestFailure(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
def check_architecture() -> None:
|
||||
"""Проверяет направление зависимостей без запуска Typst renderer."""
|
||||
template_root = ROOT / ".template" / "lib"
|
||||
import_pattern = re.compile(r'#import\s+"([^"]+)"')
|
||||
graph: dict[Path, set[Path]] = {}
|
||||
|
||||
for source in template_root.rglob("*.typ"):
|
||||
graph[source.resolve()] = set()
|
||||
text = source.read_text(encoding="utf-8")
|
||||
source_area = source.relative_to(template_root).parts[0]
|
||||
code_without_line_comments = re.sub(r"//.*", "", text)
|
||||
if source_area == "domain":
|
||||
forbidden_runtime = re.search(
|
||||
r"(?:#(?:set|show)\b|\b(?:page|place|image|query|state)\s*\()",
|
||||
code_without_line_comments,
|
||||
)
|
||||
if forbidden_runtime:
|
||||
raise TestFailure(
|
||||
f"Domain-модуль {source.relative_to(ROOT)} содержит presentation/runtime вызов "
|
||||
f"{forbidden_runtime.group(0)!r}."
|
||||
)
|
||||
for raw_target in import_pattern.findall(text):
|
||||
if raw_target.startswith("@"):
|
||||
continue
|
||||
target = (source.parent / raw_target).resolve()
|
||||
try:
|
||||
relative = target.relative_to(template_root.resolve())
|
||||
except ValueError:
|
||||
continue
|
||||
if target.suffix == ".typ":
|
||||
graph[source.resolve()].add(target)
|
||||
|
||||
target_area = relative.parts[0]
|
||||
forbidden = {
|
||||
"domain": {"application", "infrastructure", "presentation"},
|
||||
"infrastructure": {"application", "presentation"},
|
||||
"application": {"presentation"},
|
||||
}.get(source_area, set())
|
||||
if target_area in forbidden:
|
||||
raise TestFailure(
|
||||
"Нарушена DDD-граница: "
|
||||
f"{source.relative_to(ROOT)} -> {target.relative_to(ROOT)}"
|
||||
)
|
||||
|
||||
if re.search(r'#import\s+"[^\"]*(?:main\.typ|chapters/|assets/|\.private/)', text):
|
||||
raise TestFailure(
|
||||
f"Library-модуль {source.relative_to(ROOT)} импортирует пользовательский слой."
|
||||
)
|
||||
|
||||
visiting: set[Path] = set()
|
||||
visited: set[Path] = set()
|
||||
|
||||
def visit(node: Path, stack: list[Path]) -> None:
|
||||
if node in visiting:
|
||||
cycle = stack[stack.index(node):] + [node]
|
||||
rendered = " -> ".join(str(item.relative_to(ROOT)) for item in cycle)
|
||||
raise TestFailure(f"Циклические Typst-импорты: {rendered}")
|
||||
if node in visited:
|
||||
return
|
||||
visiting.add(node)
|
||||
stack.append(node)
|
||||
for target in graph.get(node, set()):
|
||||
if target in graph:
|
||||
visit(target, stack)
|
||||
stack.pop()
|
||||
visiting.remove(node)
|
||||
visited.add(node)
|
||||
|
||||
for node in graph:
|
||||
visit(node, [])
|
||||
|
||||
|
||||
def check_workspace() -> None:
|
||||
"""Проверяет минимальный корень и синхронизируемую конфигурацию VS Code."""
|
||||
allowed_directories = {
|
||||
".git",
|
||||
".private",
|
||||
".template",
|
||||
".vscode",
|
||||
"assets",
|
||||
"chapters",
|
||||
"docs",
|
||||
}
|
||||
actual_directories = {item.name for item in ROOT.iterdir() if item.is_dir()}
|
||||
unexpected = actual_directories - allowed_directories
|
||||
if unexpected:
|
||||
raise TestFailure(
|
||||
"В корне обнаружены лишние каталоги: " + ", ".join(sorted(unexpected))
|
||||
)
|
||||
|
||||
required_files = {
|
||||
".gitignore",
|
||||
"README.md",
|
||||
"main.typ",
|
||||
}
|
||||
missing_files = {name for name in required_files if not (ROOT / name).is_file()}
|
||||
if missing_files:
|
||||
raise TestFailure(
|
||||
"В корне отсутствуют обязательные файлы: " + ", ".join(sorted(missing_files))
|
||||
)
|
||||
|
||||
extensions_path = ROOT / ".vscode" / "extensions.json"
|
||||
settings_path = ROOT / ".vscode" / "settings.json"
|
||||
extensions = json.loads(extensions_path.read_text(encoding="utf-8"))
|
||||
recommendations = set(extensions.get("recommendations", []))
|
||||
required_extensions = {
|
||||
"alefragnani.bookmarks",
|
||||
"streetsidesoftware.code-spell-checker",
|
||||
"streetsidesoftware.code-spell-checker-russian",
|
||||
"ms-ceintl.vscode-language-pack-ru",
|
||||
"myriad-dreamin.tinymist",
|
||||
"wayou.vscode-todo-highlight",
|
||||
"gruntfuggly.todo-tree",
|
||||
"mhutchie.git-graph",
|
||||
"local.typst-typewriter",
|
||||
"zotst.zotst",
|
||||
}
|
||||
missing_extensions = required_extensions - recommendations
|
||||
if missing_extensions:
|
||||
raise TestFailure(
|
||||
"В extensions.json отсутствуют рекомендации: "
|
||||
+ ", ".join(sorted(missing_extensions))
|
||||
)
|
||||
|
||||
settings = json.loads(settings_path.read_text(encoding="utf-8"))
|
||||
if settings.get("files.autoSave") != "afterDelay":
|
||||
raise TestFailure("Workspace должен включать files.autoSave=afterDelay.")
|
||||
if settings.get("files.exclude", {}).get(".template") is not True:
|
||||
raise TestFailure(".template должен быть скрыт в пользовательском Explorer VS Code.")
|
||||
if settings.get("files.exclude", {}).get(".vscode") is not True:
|
||||
raise TestFailure(".vscode должен быть скрыт в пользовательском Explorer VS Code.")
|
||||
if settings.get("files.exclude", {}).get("docs") is True:
|
||||
raise TestFailure("Публичная документация docs/ не должна быть скрыта.")
|
||||
if settings.get("typstTypewriter.navigator.mainFiles") != ["main.typ"]:
|
||||
raise TestFailure("Typewriter должен показывать единственный main.typ.")
|
||||
|
||||
gitignore = (ROOT / ".gitignore").read_text(encoding="utf-8")
|
||||
if ".private/" not in gitignore:
|
||||
raise TestFailure("В .gitignore отсутствует защита каталога .private/.")
|
||||
if ".vscode/" in gitignore:
|
||||
raise TestFailure(".vscode должен синхронизироваться через Git.")
|
||||
|
||||
private_example = ROOT / "docs" / "examples" / "private" / "settings.typ"
|
||||
if not private_example.is_file():
|
||||
raise TestFailure("Отсутствует безопасный пример .private/settings.typ.")
|
||||
private_example_text = private_example.read_text(encoding="utf-8")
|
||||
for employee_id in (
|
||||
"musikhin", "guzeev", "fedorov", "ilyasov", "khimichev",
|
||||
"brusnicin", "ozornin", "buhartdinov", "tkachenko",
|
||||
):
|
||||
if f"{employee_id}:" not in private_example_text:
|
||||
raise TestFailure(
|
||||
f"В примере private settings отсутствует сотрудник {employee_id}."
|
||||
)
|
||||
|
||||
tasks = json.loads((ROOT / ".vscode" / "tasks.json").read_text(encoding="utf-8"))
|
||||
task_labels = {task.get("label") for task in tasks.get("tasks", [])}
|
||||
if "Scientia: собрать PDF с приватными данными" in task_labels:
|
||||
raise TestFailure("Отдельная private build task больше не должна использоваться.")
|
||||
if (ROOT / ".template" / "development" / "tools" / "build-with-private-assets.ps1").exists():
|
||||
raise TestFailure("Устаревший ZIP installer должен быть удалён.")
|
||||
|
||||
for company_id in ("scientia", "technology", "too"):
|
||||
company_path = ROOT / ".template" / "companies" / company_id / "data.json"
|
||||
company = json.loads(company_path.read_text(encoding="utf-8"))
|
||||
if company.get("sign_image") is not None or company.get("stamp_image") is not None:
|
||||
raise TestFailure(
|
||||
f"Публичный профиль {company_id} не должен содержать подпись или печать."
|
||||
)
|
||||
|
||||
public_docs = (
|
||||
"README.md",
|
||||
"documents.md",
|
||||
"formatting.md",
|
||||
"vscode.md",
|
||||
"git.md",
|
||||
"private-assets.md",
|
||||
"writing-style.md",
|
||||
"troubleshooting.md",
|
||||
)
|
||||
for name in public_docs:
|
||||
if not (ROOT / "docs" / name).is_file():
|
||||
raise TestFailure(f"Публичная документация неполна: docs/{name}.")
|
||||
|
||||
for starter in ("report", "letter", "commercial-offer", "contract"):
|
||||
starter_root = ROOT / "docs" / "examples" / "documents" / starter
|
||||
if not (starter_root / "main.typ").is_file() or not (starter_root / "chapters").is_dir():
|
||||
raise TestFailure(f"Публичный пример {starter} неполон.")
|
||||
|
||||
for obsolete in ("document.typ", "draft.typ", "clean-copy.typ"):
|
||||
if (ROOT / obsolete).exists():
|
||||
raise TestFailure(f"Лишняя точка входа в корне: {obsolete}.")
|
||||
|
||||
|
||||
def run(command: list[str], *, cwd: Path = ROOT) -> str:
|
||||
result = subprocess.run(
|
||||
command,
|
||||
cwd=cwd,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
rendered = subprocess.list2cmdline(command)
|
||||
raise TestFailure(f"Команда завершилась с кодом {result.returncode}:\n{rendered}\n{result.stdout}")
|
||||
return result.stdout
|
||||
|
||||
|
||||
def find_executable(
|
||||
name: str,
|
||||
*,
|
||||
env_var: str | None = None,
|
||||
required: bool = True,
|
||||
) -> Path | None:
|
||||
if env_var and os.environ.get(env_var):
|
||||
candidate = Path(os.environ[env_var])
|
||||
if candidate.is_file():
|
||||
return candidate
|
||||
|
||||
located = shutil.which(name)
|
||||
if located:
|
||||
return Path(located)
|
||||
|
||||
executable = name + (".exe" if os.name == "nt" else "")
|
||||
candidates: list[Path] = []
|
||||
|
||||
poppler_bin = os.environ.get("POPPLER_BIN")
|
||||
if poppler_bin:
|
||||
candidates.append(Path(poppler_bin) / executable)
|
||||
|
||||
if os.name == "nt":
|
||||
candidates.append(
|
||||
Path.home()
|
||||
/ ".cache"
|
||||
/ "codex-runtimes"
|
||||
/ "codex-primary-runtime"
|
||||
/ "dependencies"
|
||||
/ "native"
|
||||
/ "poppler"
|
||||
/ "Library"
|
||||
/ "bin"
|
||||
/ executable
|
||||
)
|
||||
|
||||
for candidate in candidates:
|
||||
if candidate.is_file():
|
||||
return candidate
|
||||
|
||||
if required:
|
||||
raise TestFailure(
|
||||
f"Не найден исполняемый файл {name}. Добавьте его в PATH"
|
||||
+ (f" или задайте {env_var}" if env_var else "")
|
||||
+ ("/POPPLER_BIN." if name.startswith("pdf") else ".")
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def parse_version(value: str) -> tuple[int, ...]:
|
||||
match = re.search(r"(\d+)\.(\d+)\.(\d+)", value)
|
||||
if not match:
|
||||
raise TestFailure(f"Не удалось определить версию из строки: {value!r}")
|
||||
return tuple(int(part) for part in match.groups())
|
||||
|
||||
|
||||
def load_manifest() -> dict:
|
||||
return json.loads(MANIFEST_PATH.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def compile_fixture(
|
||||
typst: Path,
|
||||
entry: Path,
|
||||
output: Path,
|
||||
creation_timestamp: int,
|
||||
inputs: dict[str, str] | None = None,
|
||||
) -> str:
|
||||
command = [
|
||||
str(typst),
|
||||
"compile",
|
||||
"--root",
|
||||
str(ROOT),
|
||||
"--creation-timestamp",
|
||||
str(creation_timestamp),
|
||||
]
|
||||
for key, value in sorted((inputs or {}).items()):
|
||||
command.extend(("--input", f"{key}={value}"))
|
||||
command.extend((str(entry), str(output)))
|
||||
return run(command)
|
||||
|
||||
|
||||
def compile_expected_failure(
|
||||
typst: Path,
|
||||
entry: Path,
|
||||
output: Path,
|
||||
creation_timestamp: int,
|
||||
required_errors: list[str],
|
||||
inputs: dict[str, str] | None = None,
|
||||
) -> None:
|
||||
command = [
|
||||
str(typst),
|
||||
"compile",
|
||||
"--root",
|
||||
str(ROOT),
|
||||
"--creation-timestamp",
|
||||
str(creation_timestamp),
|
||||
]
|
||||
for key, value in sorted((inputs or {}).items()):
|
||||
command.extend(("--input", f"{key}={value}"))
|
||||
command.extend((str(entry), str(output)))
|
||||
result = subprocess.run(
|
||||
command,
|
||||
cwd=ROOT,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
raise TestFailure(f"{entry.relative_to(ROOT)}: ожидалась ошибка компиляции.")
|
||||
for marker in required_errors:
|
||||
if marker not in result.stdout:
|
||||
raise TestFailure(
|
||||
f"{entry.relative_to(ROOT)}: в diagnostic не найден маркер {marker!r}.\n"
|
||||
f"Фактический вывод:\n{result.stdout}"
|
||||
)
|
||||
|
||||
|
||||
def run_unit_test(typst: Path, module: str, creation_timestamp: int) -> None:
|
||||
expression = f'import "/{module.replace(os.sep, "/")}": run; run()'
|
||||
output = run(
|
||||
[
|
||||
str(typst),
|
||||
"eval",
|
||||
"--root",
|
||||
str(ROOT),
|
||||
"--creation-timestamp",
|
||||
str(creation_timestamp),
|
||||
expression,
|
||||
]
|
||||
).strip()
|
||||
if json.loads(output) != "ok":
|
||||
raise TestFailure(f"Unit-модуль {module} вернул неожиданное значение: {output}")
|
||||
|
||||
|
||||
def inspect_pdf(pdfinfo: Path, pdf: Path) -> tuple[int, str]:
|
||||
info = run([str(pdfinfo), str(pdf)])
|
||||
pages_match = re.search(r"^Pages:\s+(\d+)$", info, flags=re.MULTILINE)
|
||||
size_match = re.search(r"^Page size:.*\(([^)]+)\)$", info, flags=re.MULTILINE)
|
||||
if not pages_match or not size_match:
|
||||
raise TestFailure(f"Не удалось разобрать pdfinfo для {pdf}:\n{info}")
|
||||
return int(pages_match.group(1)), size_match.group(1).strip()
|
||||
|
||||
|
||||
def extract_text(pdftotext: Path | None, pdf: Path, target: Path) -> str:
|
||||
if pdftotext:
|
||||
run([str(pdftotext), "-layout", str(pdf), str(target)])
|
||||
return target.read_text(encoding="utf-8", errors="replace")
|
||||
|
||||
try:
|
||||
from pypdf import PdfReader
|
||||
except ImportError as exc:
|
||||
raise TestFailure(
|
||||
"Для проверки текста требуется pdftotext или Python-пакет pypdf."
|
||||
) from exc
|
||||
|
||||
reader = PdfReader(str(pdf))
|
||||
return "\n".join((page.extract_text() or "") for page in reader.pages)
|
||||
|
||||
|
||||
def count_pdf_links(pdf: Path) -> tuple[int, int]:
|
||||
try:
|
||||
from pypdf import PdfReader
|
||||
except ImportError as exc:
|
||||
raise TestFailure("Для проверки PDF-ссылок требуется Python-пакет pypdf.") from exc
|
||||
|
||||
external = internal = 0
|
||||
for page in PdfReader(str(pdf)).pages:
|
||||
for raw_annotation in page.get("/Annots", ()):
|
||||
annotation = raw_annotation.get_object()
|
||||
if annotation.get("/Subtype") != "/Link":
|
||||
continue
|
||||
action = annotation.get("/A")
|
||||
if action is not None and action.get_object().get("/URI") is not None:
|
||||
external += 1
|
||||
else:
|
||||
internal += 1
|
||||
return external, internal
|
||||
|
||||
|
||||
def render_page(pdftoppm: Path, pdf: Path, page: int, dpi: int, target: Path) -> Path:
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
run(
|
||||
[
|
||||
str(pdftoppm),
|
||||
"-png",
|
||||
"-r",
|
||||
str(dpi),
|
||||
"-f",
|
||||
str(page),
|
||||
"-l",
|
||||
str(page),
|
||||
"-singlefile",
|
||||
str(pdf),
|
||||
str(target.with_suffix("")),
|
||||
]
|
||||
)
|
||||
return target
|
||||
|
||||
|
||||
def image_difference(expected: Path, actual: Path) -> float:
|
||||
try:
|
||||
from PIL import Image, ImageChops
|
||||
except ImportError as exc:
|
||||
raise TestFailure("Для visual regression требуется пакет Pillow.") from exc
|
||||
|
||||
with Image.open(expected).convert("RGB") as left, Image.open(actual).convert("RGB") as right:
|
||||
if left.size != right.size:
|
||||
return 1.0
|
||||
diff = ImageChops.difference(left, right)
|
||||
histogram = diff.histogram()
|
||||
total = sum(value * (index % 256) for index, value in enumerate(histogram))
|
||||
maximum = left.width * left.height * 3 * 255
|
||||
return total / maximum
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Регрессионные тесты шаблона документов Scientia")
|
||||
parser.add_argument("--fixture", action="append", help="Запустить только fixture с указанным id")
|
||||
parser.add_argument("--compile-only", action="store_true", help="Не запускать PDF и visual проверки")
|
||||
parser.add_argument(
|
||||
"--update-snapshots",
|
||||
action="store_true",
|
||||
help="Явно обновить visual snapshots после ручного подтверждения",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
manifest = load_manifest()
|
||||
typst = find_executable("typst", env_var="TYPST_BIN")
|
||||
version_output = run([str(typst), "--version"]).strip()
|
||||
if parse_version(version_output) < parse_version(manifest["typst_min"]):
|
||||
raise TestFailure(
|
||||
f"Требуется Typst {manifest['typst_min']}+, найден {version_output}."
|
||||
)
|
||||
|
||||
pdfinfo = pdftotext = pdftoppm = None
|
||||
if not args.compile_only:
|
||||
pdfinfo = find_executable("pdfinfo")
|
||||
pdftotext = find_executable("pdftotext", required=False)
|
||||
pdftoppm = find_executable("pdftoppm")
|
||||
|
||||
creation_timestamp = int(manifest["creation_timestamp"])
|
||||
selected = set(args.fixture or [])
|
||||
known = {
|
||||
item["id"]
|
||||
for item in (
|
||||
*manifest["fixtures"],
|
||||
*manifest.get("negative_fixtures", []),
|
||||
*manifest.get("compile_matrices", []),
|
||||
)
|
||||
}
|
||||
unknown = selected - known
|
||||
if unknown:
|
||||
raise TestFailure(f"Неизвестные fixtures: {', '.join(sorted(unknown))}")
|
||||
|
||||
if not selected:
|
||||
print("[static] architecture")
|
||||
check_architecture()
|
||||
print("[static] workspace")
|
||||
check_workspace()
|
||||
|
||||
for unit in manifest.get("unit_tests", []):
|
||||
if selected:
|
||||
continue
|
||||
print(f"[unit] {unit['id']}")
|
||||
run_unit_test(typst, unit["module"], creation_timestamp)
|
||||
|
||||
temp_dir = Path(tempfile.mkdtemp(prefix="scientia-tests-"))
|
||||
failed = False
|
||||
try:
|
||||
for matrix in manifest.get("compile_matrices", []):
|
||||
if selected and matrix["id"] not in selected:
|
||||
continue
|
||||
keys = tuple(matrix["parameters"])
|
||||
values = tuple(matrix["parameters"][key] for key in keys)
|
||||
for combination in itertools.product(*values):
|
||||
inputs = dict(zip(keys, combination))
|
||||
suffix = ",".join(f"{key}={value}" for key, value in inputs.items())
|
||||
print(f"[matrix] {matrix['id']}[{suffix}]")
|
||||
safe_suffix = re.sub(r"[^A-Za-z0-9_.-]+", "-", suffix)
|
||||
compile_fixture(
|
||||
typst,
|
||||
ROOT / matrix["entry"],
|
||||
temp_dir / f"{matrix['id']}-{safe_suffix}.pdf",
|
||||
creation_timestamp,
|
||||
inputs=inputs,
|
||||
)
|
||||
|
||||
for fixture in manifest.get("negative_fixtures", []):
|
||||
if selected and fixture["id"] not in selected:
|
||||
continue
|
||||
fixture_id = fixture["id"]
|
||||
print(f"[negative] {fixture_id}")
|
||||
compile_expected_failure(
|
||||
typst,
|
||||
ROOT / fixture["entry"],
|
||||
temp_dir / f"{fixture_id}.pdf",
|
||||
creation_timestamp,
|
||||
fixture.get("required_errors", []),
|
||||
inputs=fixture.get("inputs"),
|
||||
)
|
||||
|
||||
for fixture in manifest["fixtures"]:
|
||||
if selected and fixture["id"] not in selected:
|
||||
continue
|
||||
|
||||
fixture_id = fixture["id"]
|
||||
print(f"[compile] {fixture_id}")
|
||||
entry = ROOT / fixture["entry"]
|
||||
output = temp_dir / f"{fixture_id}.pdf"
|
||||
compile_fixture(
|
||||
typst,
|
||||
entry,
|
||||
output,
|
||||
creation_timestamp,
|
||||
inputs=fixture.get("inputs"),
|
||||
)
|
||||
|
||||
if args.compile_only:
|
||||
continue
|
||||
|
||||
assert pdfinfo and pdftoppm
|
||||
pages, page_size = inspect_pdf(pdfinfo, output)
|
||||
expected_pages = int(fixture["expected_pages"])
|
||||
if expected_pages and pages != expected_pages:
|
||||
raise TestFailure(
|
||||
f"{fixture_id}: ожидалось {expected_pages} страниц, получено {pages}."
|
||||
)
|
||||
if page_size != fixture["expected_page_size"]:
|
||||
raise TestFailure(
|
||||
f"{fixture_id}: ожидался формат {fixture['expected_page_size']}, получен {page_size}."
|
||||
)
|
||||
|
||||
text = extract_text(pdftotext, output, temp_dir / f"{fixture_id}.txt")
|
||||
for marker in fixture.get("required_text", []):
|
||||
if marker not in text:
|
||||
raise TestFailure(f"{fixture_id}: в PDF не найден текстовый маркер {marker!r}.")
|
||||
for marker in fixture.get("forbidden_text", []):
|
||||
if marker in text:
|
||||
raise TestFailure(f"{fixture_id}: в PDF найден запрещённый текстовый маркер {marker!r}.")
|
||||
for marker, minimum in fixture.get("required_text_counts", {}).items():
|
||||
actual = text.count(marker)
|
||||
if actual < int(minimum):
|
||||
raise TestFailure(
|
||||
f"{fixture_id}: маркер {marker!r} найден {actual} раз; "
|
||||
f"требуется не менее {minimum}."
|
||||
)
|
||||
if "minimum_external_links" in fixture or "minimum_internal_links" in fixture:
|
||||
external_links, internal_links = count_pdf_links(output)
|
||||
minimum_external = int(fixture.get("minimum_external_links", 0))
|
||||
minimum_internal = int(fixture.get("minimum_internal_links", 0))
|
||||
if external_links < minimum_external:
|
||||
raise TestFailure(
|
||||
f"{fixture_id}: внешних PDF-ссылок {external_links}; "
|
||||
f"требуется не менее {minimum_external}."
|
||||
)
|
||||
if internal_links < minimum_internal:
|
||||
raise TestFailure(
|
||||
f"{fixture_id}: внутренних PDF-ссылок {internal_links}; "
|
||||
f"требуется не менее {minimum_internal}."
|
||||
)
|
||||
|
||||
for page in fixture.get("snapshot_pages", []):
|
||||
if page > pages:
|
||||
raise TestFailure(f"{fixture_id}: snapshot page {page} больше количества страниц {pages}.")
|
||||
actual = render_page(
|
||||
pdftoppm,
|
||||
output,
|
||||
page,
|
||||
int(manifest["dpi"]),
|
||||
temp_dir / fixture_id / f"page-{page}.png",
|
||||
)
|
||||
expected = SNAPSHOT_ROOT / fixture_id / f"page-{page}.png"
|
||||
if args.update_snapshots:
|
||||
expected.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(actual, expected)
|
||||
print(f"[snapshot:update] {expected.relative_to(ROOT)}")
|
||||
continue
|
||||
if not expected.is_file():
|
||||
raise TestFailure(
|
||||
f"Нет snapshot {expected.relative_to(ROOT)}. "
|
||||
"После ручной проверки запустите --update-snapshots."
|
||||
)
|
||||
difference = image_difference(expected, actual)
|
||||
threshold = float(fixture.get("visual_threshold", manifest["visual_threshold"]))
|
||||
if difference > threshold:
|
||||
raise TestFailure(
|
||||
f"{fixture_id}, страница {page}: visual diff {difference:.6f} "
|
||||
f"превышает порог {threshold:.6f}. Фактический файл: {actual}"
|
||||
)
|
||||
print(f"[snapshot:ok] {fixture_id}/page-{page} diff={difference:.6f}")
|
||||
|
||||
print(f"OK: {version_output}")
|
||||
return 0
|
||||
except Exception:
|
||||
failed = True
|
||||
raise
|
||||
finally:
|
||||
if failed:
|
||||
print(f"Временные результаты сохранены: {temp_dir}", file=sys.stderr)
|
||||
else:
|
||||
shutil.rmtree(temp_dir, ignore_errors=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
raise SystemExit(main())
|
||||
except TestFailure as exc:
|
||||
print(f"FAIL: {exc}", file=sys.stderr)
|
||||
raise SystemExit(1)
|
||||
Reference in new issue
Block a user