From d7fd6d79d3e7a72c3634f2f7ffe0c9a9b3c43f32 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Wed, 5 Aug 2026 16:23:13 +0300 Subject: [PATCH] =?UTF-8?q?test(runner):=20=D0=B1=D0=B0=D0=B9=D1=82=D0=BE?= =?UTF-8?q?=D0=B2=D1=8B=D0=B5=20=D0=BF=D1=80=D0=BE=D0=B2=D0=B5=D1=80=D0=BA?= =?UTF-8?q?=D0=B8=20=D0=BA=D0=B0=D0=BD=D0=BE=D0=BD=D0=B0=20=D0=B2=D0=BC?= =?UTF-8?q?=D0=B5=D1=81=D1=82=D0=BE=20=D0=BC=D0=B0=D1=81=D0=BE=D0=BA=20(#5?= =?UTF-8?q?7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Раннер сам прятал дефекты, которые чинит #57. normalizeXmlContent при runtime=python снимал ровно четыре измерения: пробел перед `/>`, whitespace между тегами, пустую пару и хвостовой пробельный мусор. Паритет PS<->PY по этим измерениям проверялся ЧЕРЕЗ маску — расхождение физически не могло упасть. Снято три из четырёх (пробел, пустая пара, хвост). Схлопывание whitespace между тегами оставлено: порты кое-где расставляют отступы иначе, это форматирование, а не байтовый канон. Ужесточён checkPreserves: eol теперь считает ОДИНОЧНЫЕ LF, а не «есть ли хоть один CR». Прежняя проверка пропускала смешанный выход — cfe-init давал 10 CR на 70 строк и проходил её, то есть головной дефект тикета был ей невидим. Добавлены ключи selfClose:"tight" и noEmptyPairs; preserves проставлен в 13 кейсах навыков-эмиттеров (по одному на навык, на его СОБСТВЕННЫЙ артефакт). Снятие масок сразу вскрыло четыре реальных расхождения портов: - form-edit собирает выход из OuterXml и писал ``; в списке 17 навыков его не было, потому что искал по вызовам Save — здесь другой путь. Тот же случай, что с Form.xml в cfe-borrow; - meta-edit py дописывал хвостовой перевод в создаваемый Ext/Predefined.xml и читал существующий без newline='' (терял CRLF); - skd-info py писал отчёт -OutFile без хвостового перевода, PS через WriteAllLines — с ним. Это текстовый отчёт, канон Конфигуратора к нему не относится, поэтому выровнял py по существующему эталону; - фикстуры кейсов содержали — снимок нашего же старого вывода. Конфигуратор пустых пар не пишет, .NET их сохраняет, lxml схлопывает. Поправлены 10 фикстур: пары → самозакрывающиеся, EOL и BOM не тронуты. Результат: PS 641/641, python 638/641 (+3 runtimeOnly-скипа) — со снятыми масками. Дрейф снэпшотов: 10 файлов, только пробельные теги и пустые пары. Co-Authored-By: Claude Opus 5 --- .../skills/form-edit/scripts/form-edit.ps1 | 5 +- .claude/skills/form-edit/scripts/form-edit.py | 2 +- .../skills/meta-edit/scripts/meta-edit.ps1 | 2 +- .claude/skills/meta-edit/scripts/meta-edit.py | 8 +-- .claude/skills/skd-info/scripts/skd-info.ps1 | 2 +- .claude/skills/skd-info/scripts/skd-info.py | 7 ++- tests/skills/README.md | 10 +++- .../fixtures/add-bot/Configuration.xml | 4 +- .../fixtures/crlf-config/Configuration.xml | 4 +- .../snapshots/add-bot/Configuration.xml | 4 +- .../roundtrip-crlf-preserve/Configuration.xml | 2 +- .../fixtures/with-bot/Configuration.xml | 4 +- .../snapshots/with-bot/Configuration.xml | 4 +- tests/skills/cases/cf-init/basic.json | 14 +++++- .../missing-language/Configuration.xml | 6 +-- .../fixtures/with-bot/Configuration.xml | 4 +- .../snapshots/with-bot/Configuration.xml | 4 +- tests/skills/cases/cfe-init/basic.json | 19 ++++++- .../broken-extension/Configuration.xml | 2 +- tests/skills/cases/epf-init/basic.json | 18 ++++++- tests/skills/cases/erf-init/basic.json | 18 ++++++- tests/skills/cases/form-add/basic.json | 29 +++++++++-- tests/skills/cases/form-compile/minimal.json | 31 ++++++++++-- .../Реквизиты/Forms/Форма/Ext/Form.xml | 2 +- .../КомандыТест/Forms/Форма/Ext/Form.xml | 4 +- .../Тест/Forms/Форма/Ext/Form.xml | 10 ++-- .../Группа/Forms/Форма/Ext/Form.xml | 16 +++--- tests/skills/cases/help-add/basic.json | 21 +++++++- .../cases/meta-compile/catalog-basic.json | 20 +++++++- .../fixtures/crlf-catalog/Configuration.xml | 4 +- .../roundtrip-crlf-preserve/Configuration.xml | 4 +- .../Catalogs/ТестКоманд.xml | 2 +- tests/skills/cases/mxl-compile/minimal.json | 27 ++++++++-- .../skills/cases/role-compile/basic-role.json | 12 ++++- tests/skills/cases/skd-compile/minimal.json | 30 +++++++++--- .../fixtures/empty-field/Template.xml | 4 +- .../skills/cases/subsystem-compile/basic.json | 28 +++++++++-- .../fixtures/crlf-subsystem/Configuration.xml | 4 +- .../roundtrip-crlf-preserve/Configuration.xml | 4 +- tests/skills/cases/xdto-compile/minimal.json | 23 +++++++-- tests/skills/runner.mjs | 49 ++++++++++++++----- 41 files changed, 361 insertions(+), 106 deletions(-) diff --git a/.claude/skills/form-edit/scripts/form-edit.ps1 b/.claude/skills/form-edit/scripts/form-edit.ps1 index ae684300..09e71fda 100644 --- a/.claude/skills/form-edit/scripts/form-edit.ps1 +++ b/.claude/skills/form-edit/scripts/form-edit.ps1 @@ -1,4 +1,4 @@ -# form-edit v1.6 — Edit 1C managed form elements +# form-edit v1.7 — Edit 1C managed form elements # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [Parameter(Mandatory)] @@ -1387,6 +1387,9 @@ if ($def.elementEvents -and $def.elementEvents.Count -gt 0) { $content = $xmlDoc.OuterXml # Ensure encoding declaration is uppercase UTF-8 $content = $content -replace '^<\?xml version="1.0" encoding="utf-8"\?>', '' +# Пустой элемент: OuterXml (как и XmlWriter) пишет ``, Конфигуратор — ``. +# Гард на CDATA/комментарии: только там ` />` может быть содержимым, а не концом тега. +if ($content -notmatch '', '/>') } $enc = New-Object System.Text.UTF8Encoding($true) [System.IO.File]::WriteAllText($resolvedFormPath, $content, $enc) diff --git a/.claude/skills/form-edit/scripts/form-edit.py b/.claude/skills/form-edit/scripts/form-edit.py index 5e684077..1d0c6daa 100644 --- a/.claude/skills/form-edit/scripts/form-edit.py +++ b/.claude/skills/form-edit/scripts/form-edit.py @@ -1,4 +1,4 @@ -# form-edit v1.6 — Edit 1C managed form elements (Python port) +# form-edit v1.7 — Edit 1C managed form elements (Python port) # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse import json diff --git a/.claude/skills/meta-edit/scripts/meta-edit.ps1 b/.claude/skills/meta-edit/scripts/meta-edit.ps1 index 377e05b2..5cf448b4 100644 --- a/.claude/skills/meta-edit/scripts/meta-edit.ps1 +++ b/.claude/skills/meta-edit/scripts/meta-edit.ps1 @@ -1,4 +1,4 @@ -# meta-edit v1.26 — Edit existing 1C metadata object XML +# meta-edit v1.27 — Edit existing 1C metadata object XML # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [string]$DefinitionFile, diff --git a/.claude/skills/meta-edit/scripts/meta-edit.py b/.claude/skills/meta-edit/scripts/meta-edit.py index 1c693702..de20f404 100644 --- a/.claude/skills/meta-edit/scripts/meta-edit.py +++ b/.claude/skills/meta-edit/scripts/meta-edit.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# meta-edit v1.26 — Edit existing 1C metadata object XML +# meta-edit v1.27 — Edit existing 1C metadata object XML # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse @@ -3094,7 +3094,9 @@ def add_predefined_items(items): item_list = items if isinstance(items, list) else [items] items_xml = ''.join(build_predef_item_xml('\t', it, code_type) for it in item_list) if os.path.exists(path): - with open(path, 'r', encoding='utf-8-sig') as f: + # newline='' => без трансляции переводов строк: иначе CRLF молча схлопнется + # в LF при чтении и файл будет переписан в LF (#44/#46/#47). + with open(path, 'r', encoding='utf-8-sig', newline='') as f: text = f.read() text = text.replace('', items_xml + '') else: @@ -3103,7 +3105,7 @@ def add_predefined_items(items): 'xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" ' 'xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" ' f'xsi:type="{xsi_type}" version="{version}">\r\n') - text = hdr + items_xml + '\r\n' + text = hdr + items_xml + '' with open(path, 'wb') as f: f.write(b'\xef\xbb\xbf') f.write(text.encode('utf-8')) diff --git a/.claude/skills/skd-info/scripts/skd-info.ps1 b/.claude/skills/skd-info/scripts/skd-info.ps1 index 10be42c1..dc3fe7e5 100644 --- a/.claude/skills/skd-info/scripts/skd-info.ps1 +++ b/.claude/skills/skd-info/scripts/skd-info.ps1 @@ -1,4 +1,4 @@ -# skd-info v1.8 — Analyze 1C DCS structure +# skd-info v1.9 — Analyze 1C DCS structure # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [Parameter(Mandatory=$true)] diff --git a/.claude/skills/skd-info/scripts/skd-info.py b/.claude/skills/skd-info/scripts/skd-info.py index bec0476e..a537618a 100644 --- a/.claude/skills/skd-info/scripts/skd-info.py +++ b/.claude/skills/skd-info/scripts/skd-info.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# skd-info v1.8 — Analyze 1C DCS structure +# skd-info v1.9 — Analyze 1C DCS structure # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse @@ -1802,7 +1802,10 @@ def main(): if not os.path.isabs(out_path): out_path = os.path.join(os.getcwd(), out_path) with open(out_path, "w", encoding="utf-8-sig") as fh: - fh.write("\n".join(result)) + # Хвостовой перевод строки — как у PS-порта (WriteAllLines его добавляет). + # Это текстовый отчёт, а не XML метаданных: канон Конфигуратора сюда не + # относится, важен лишь паритет портов. + fh.write("\n".join(result) + "\n") print(f"Written {total_lines} lines to {args.OutFile}") sys.exit(0) diff --git a/tests/skills/README.md b/tests/skills/README.md index 92ca6db6..a1cf5f94 100644 --- a/tests/skills/README.md +++ b/tests/skills/README.md @@ -252,10 +252,18 @@ ibcmd-проход автоматически `○ skipped`, если рядом |---|---| | `file` | Путь к файлу относительно `workDir` (обязателен) | | `bom` | `true`/`false` — наличие UTF-8 BOM | -| `eol` | `"crlf"` / `"lf"` | +| `eol` | `"crlf"` / `"lf"` — проверяются ОДИНОЧНЫЕ переводы строк, поэтому смешанный выход падает | | `encoding` | Ожидаемое значение в XML-декларации, напр. `"UTF-8"` | | `finalNewline` | `true`/`false` — перевод строки в конце файла | | `noCR13` | `true` — в выходе не должно быть литерала ` ` | +| `selfClose` | `"tight"` — пустой элемент только как ``, без пробела перед `/>` | +| `noEmptyPairs` | `true` — пустого элемента в форме `` быть не должно | + +Канон выгрузки Конфигуратора (issue #57), измеренный на чистой выгрузке пустой ИБ на Windows +и macOS и на 8 выгрузках в `cfsrc/`: **CRLF, BOM, последний байт `>` (без перевода строки), +`` без пробела, ноль пустых пар, `encoding="UTF-8"`.** Для файла, который навык СОЗДАЁТ, +ожидается канон; для файла, который он ПРАВИТ, — стиль входного файла (контракт #44/#46/#47), +поэтому в кейсах `roundtrip-crlf-preserve` ожидания могут отличаться от канона. `preserves` и эталон **дополняют** друг друга: первый следит за байтовым стилем файла, второй — за структурой содержимого. Наличие одного не отменяет необходимости другого. diff --git a/tests/skills/cases/cf-edit/fixtures/add-bot/Configuration.xml b/tests/skills/cases/cf-edit/fixtures/add-bot/Configuration.xml index 3a873eda..5eef6df1 100644 --- a/tests/skills/cases/cf-edit/fixtures/add-bot/Configuration.xml +++ b/tests/skills/cases/cf-edit/fixtures/add-bot/Configuration.xml @@ -48,8 +48,8 @@ Russian - - + + Version8_3_24 Language.Русский Managed diff --git a/tests/skills/cases/cf-edit/fixtures/crlf-config/Configuration.xml b/tests/skills/cases/cf-edit/fixtures/crlf-config/Configuration.xml index a45d0db9..79a9e13b 100644 --- a/tests/skills/cases/cf-edit/fixtures/crlf-config/Configuration.xml +++ b/tests/skills/cases/cf-edit/fixtures/crlf-config/Configuration.xml @@ -48,8 +48,8 @@ Russian - - + + false false diff --git a/tests/skills/cases/cf-edit/snapshots/add-bot/Configuration.xml b/tests/skills/cases/cf-edit/snapshots/add-bot/Configuration.xml index 2e9a7fce..0f4ddd5e 100644 --- a/tests/skills/cases/cf-edit/snapshots/add-bot/Configuration.xml +++ b/tests/skills/cases/cf-edit/snapshots/add-bot/Configuration.xml @@ -48,8 +48,8 @@ Russian - - + + Version8_3_24 Language.Русский Managed diff --git a/tests/skills/cases/cf-edit/snapshots/roundtrip-crlf-preserve/Configuration.xml b/tests/skills/cases/cf-edit/snapshots/roundtrip-crlf-preserve/Configuration.xml index 319f0d90..18132008 100644 --- a/tests/skills/cases/cf-edit/snapshots/roundtrip-crlf-preserve/Configuration.xml +++ b/tests/skills/cases/cf-edit/snapshots/roundtrip-crlf-preserve/Configuration.xml @@ -48,7 +48,7 @@ Russian - + 1.0.0.2 false diff --git a/tests/skills/cases/cf-info/fixtures/with-bot/Configuration.xml b/tests/skills/cases/cf-info/fixtures/with-bot/Configuration.xml index f4ad7c07..81fb29d8 100644 --- a/tests/skills/cases/cf-info/fixtures/with-bot/Configuration.xml +++ b/tests/skills/cases/cf-info/fixtures/with-bot/Configuration.xml @@ -48,8 +48,8 @@ Russian - - + + Version8_3_24 Language.Русский Managed diff --git a/tests/skills/cases/cf-info/snapshots/with-bot/Configuration.xml b/tests/skills/cases/cf-info/snapshots/with-bot/Configuration.xml index 2e9a7fce..0f4ddd5e 100644 --- a/tests/skills/cases/cf-info/snapshots/with-bot/Configuration.xml +++ b/tests/skills/cases/cf-info/snapshots/with-bot/Configuration.xml @@ -48,8 +48,8 @@ Russian - - + + Version8_3_24 Language.Русский Managed diff --git a/tests/skills/cases/cf-init/basic.json b/tests/skills/cases/cf-init/basic.json index c2a18c01..330825fb 100644 --- a/tests/skills/cases/cf-init/basic.json +++ b/tests/skills/cases/cf-init/basic.json @@ -8,6 +8,16 @@ "Configuration.xml", "Languages/Русский.xml", "Ext/ClientApplicationInterface.xml" - ] + ], + "preserves": { + "file": "Configuration.xml", + "bom": true, + "eol": "crlf", + "encoding": "UTF-8", + "finalNewline": false, + "noCR13": true, + "selfClose": "tight", + "noEmptyPairs": true + } } -} \ No newline at end of file +} diff --git a/tests/skills/cases/cf-validate/fixtures/missing-language/Configuration.xml b/tests/skills/cases/cf-validate/fixtures/missing-language/Configuration.xml index 718fc3b1..293d140f 100644 --- a/tests/skills/cases/cf-validate/fixtures/missing-language/Configuration.xml +++ b/tests/skills/cases/cf-validate/fixtures/missing-language/Configuration.xml @@ -32,7 +32,7 @@ - + ru @@ -48,8 +48,8 @@ Russian - - + + Version8_3_24 Language.Русский Managed diff --git a/tests/skills/cases/cf-validate/fixtures/with-bot/Configuration.xml b/tests/skills/cases/cf-validate/fixtures/with-bot/Configuration.xml index f4ad7c07..81fb29d8 100644 --- a/tests/skills/cases/cf-validate/fixtures/with-bot/Configuration.xml +++ b/tests/skills/cases/cf-validate/fixtures/with-bot/Configuration.xml @@ -48,8 +48,8 @@ Russian - - + + Version8_3_24 Language.Русский Managed diff --git a/tests/skills/cases/cf-validate/snapshots/with-bot/Configuration.xml b/tests/skills/cases/cf-validate/snapshots/with-bot/Configuration.xml index 2e9a7fce..0f4ddd5e 100644 --- a/tests/skills/cases/cf-validate/snapshots/with-bot/Configuration.xml +++ b/tests/skills/cases/cf-validate/snapshots/with-bot/Configuration.xml @@ -48,8 +48,8 @@ Russian - - + + Version8_3_24 Language.Русский Managed diff --git a/tests/skills/cases/cfe-init/basic.json b/tests/skills/cases/cfe-init/basic.json index db6f680c..d0d07159 100644 --- a/tests/skills/cases/cfe-init/basic.json +++ b/tests/skills/cases/cfe-init/basic.json @@ -1,8 +1,23 @@ { "name": "Пустое расширение", - "params": { "name": "МоёРасширение", "outputDir": "ext" }, + "params": { + "name": "МоёРасширение", + "outputDir": "ext" + }, "validatePath": "ext", "expect": { - "files": ["ext/Configuration.xml"] + "files": [ + "ext/Configuration.xml" + ], + "preserves": { + "file": "Configuration.xml", + "bom": true, + "eol": "crlf", + "encoding": "UTF-8", + "finalNewline": false, + "noCR13": true, + "selfClose": "tight", + "noEmptyPairs": true + } } } diff --git a/tests/skills/cases/cfe-validate/fixtures/broken-extension/Configuration.xml b/tests/skills/cases/cfe-validate/fixtures/broken-extension/Configuration.xml index e54fcbb9..017eb8c7 100644 --- a/tests/skills/cases/cfe-validate/fixtures/broken-extension/Configuration.xml +++ b/tests/skills/cases/cfe-validate/fixtures/broken-extension/Configuration.xml @@ -8,7 +8,7 @@ - + Version8_3_24 diff --git a/tests/skills/cases/epf-init/basic.json b/tests/skills/cases/epf-init/basic.json index 5682aa79..2cbf64aa 100644 --- a/tests/skills/cases/epf-init/basic.json +++ b/tests/skills/cases/epf-init/basic.json @@ -1,8 +1,22 @@ { "name": "Пустая внешняя обработка", - "params": { "name": "ТестоваяОбработка" }, + "params": { + "name": "ТестоваяОбработка" + }, "validatePath": "ТестоваяОбработка", "expect": { - "files": ["ТестоваяОбработка.xml"] + "files": [ + "ТестоваяОбработка.xml" + ], + "preserves": { + "file": "ТестоваяОбработка.xml", + "bom": true, + "eol": "crlf", + "encoding": "UTF-8", + "finalNewline": false, + "noCR13": true, + "selfClose": "tight", + "noEmptyPairs": true + } } } diff --git a/tests/skills/cases/erf-init/basic.json b/tests/skills/cases/erf-init/basic.json index 5ddcd366..41fc86a4 100644 --- a/tests/skills/cases/erf-init/basic.json +++ b/tests/skills/cases/erf-init/basic.json @@ -1,8 +1,22 @@ { "name": "Пустой внешний отчёт", - "params": { "name": "ТестовыйОтчёт" }, + "params": { + "name": "ТестовыйОтчёт" + }, "validatePath": "ТестовыйОтчёт", "expect": { - "files": ["ТестовыйОтчёт.xml"] + "files": [ + "ТестовыйОтчёт.xml" + ], + "preserves": { + "file": "ТестовыйОтчёт.xml", + "bom": true, + "eol": "crlf", + "encoding": "UTF-8", + "finalNewline": false, + "noCR13": true, + "selfClose": "tight", + "noEmptyPairs": true + } } } diff --git a/tests/skills/cases/form-add/basic.json b/tests/skills/cases/form-add/basic.json index f4fa1ad5..ca4d9bda 100644 --- a/tests/skills/cases/form-add/basic.json +++ b/tests/skills/cases/form-add/basic.json @@ -3,10 +3,31 @@ "preRun": [ { "script": "meta-compile/scripts/meta-compile", - "input": { "type": "Catalog", "name": "Товары" }, - "args": { "-JsonPath": "{inputFile}", "-OutputDir": "{workDir}" } + "input": { + "type": "Catalog", + "name": "Товары" + }, + "args": { + "-JsonPath": "{inputFile}", + "-OutputDir": "{workDir}" + } } ], - "params": { "objectPath": "Catalogs/Товары.xml", "formName": "ФормаЭлемента" }, - "validatePath": "Catalogs/Товары/Forms/ФормаЭлемента" + "params": { + "objectPath": "Catalogs/Товары.xml", + "formName": "ФормаЭлемента" + }, + "validatePath": "Catalogs/Товары/Forms/ФормаЭлемента", + "expect": { + "preserves": { + "file": "Catalogs/Товары/Forms/ФормаЭлемента/Ext/Form.xml", + "bom": true, + "eol": "crlf", + "encoding": "UTF-8", + "finalNewline": false, + "noCR13": true, + "selfClose": "tight", + "noEmptyPairs": true + } + } } diff --git a/tests/skills/cases/form-compile/minimal.json b/tests/skills/cases/form-compile/minimal.json index de66ff6a..5d7d8318 100644 --- a/tests/skills/cases/form-compile/minimal.json +++ b/tests/skills/cases/form-compile/minimal.json @@ -3,17 +3,40 @@ "preRun": [ { "script": "meta-compile/scripts/meta-compile", - "input": { "type": "DataProcessor", "name": "Минимальная" }, - "args": { "-JsonPath": "{inputFile}", "-OutputDir": "{workDir}" } + "input": { + "type": "DataProcessor", + "name": "Минимальная" + }, + "args": { + "-JsonPath": "{inputFile}", + "-OutputDir": "{workDir}" + } }, { "script": "form-add/scripts/form-add", - "args": { "-ObjectPath": "{workDir}/DataProcessors/Минимальная.xml", "-FormName": "Форма" } + "args": { + "-ObjectPath": "{workDir}/DataProcessors/Минимальная.xml", + "-FormName": "Форма" + } } ], - "params": { "outputPath": "DataProcessors/Минимальная/Forms/Форма/Ext/Form.xml" }, + "params": { + "outputPath": "DataProcessors/Минимальная/Forms/Форма/Ext/Form.xml" + }, "validatePath": "DataProcessors/Минимальная/Forms/Форма/Ext/Form.xml", "input": { "title": "Минимальная форма" + }, + "expect": { + "preserves": { + "file": "DataProcessors/Минимальная/Forms/Форма/Ext/Form.xml", + "bom": true, + "eol": "crlf", + "encoding": "UTF-8", + "finalNewline": false, + "noCR13": true, + "selfClose": "tight", + "noEmptyPairs": true + } } } diff --git a/tests/skills/cases/form-edit/snapshots/add-attribute/DataProcessors/Реквизиты/Forms/Форма/Ext/Form.xml b/tests/skills/cases/form-edit/snapshots/add-attribute/DataProcessors/Реквизиты/Forms/Форма/Ext/Form.xml index e6ea540c..0fa499b0 100644 --- a/tests/skills/cases/form-edit/snapshots/add-attribute/DataProcessors/Реквизиты/Forms/Форма/Ext/Form.xml +++ b/tests/skills/cases/form-edit/snapshots/add-attribute/DataProcessors/Реквизиты/Forms/Форма/Ext/Form.xml @@ -7,7 +7,7 @@ false - + diff --git a/tests/skills/cases/form-edit/snapshots/add-command/DataProcessors/КомандыТест/Forms/Форма/Ext/Form.xml b/tests/skills/cases/form-edit/snapshots/add-command/DataProcessors/КомандыТест/Forms/Форма/Ext/Form.xml index faf7a246..21140f06 100644 --- a/tests/skills/cases/form-edit/snapshots/add-command/DataProcessors/КомандыТест/Forms/Форма/Ext/Form.xml +++ b/tests/skills/cases/form-edit/snapshots/add-command/DataProcessors/КомандыТест/Forms/Форма/Ext/Form.xml @@ -7,12 +7,12 @@ false - + diff --git a/tests/skills/cases/form-edit/snapshots/add-element/DataProcessors/Тест/Forms/Форма/Ext/Form.xml b/tests/skills/cases/form-edit/snapshots/add-element/DataProcessors/Тест/Forms/Форма/Ext/Form.xml index e1548bad..35b20101 100644 --- a/tests/skills/cases/form-edit/snapshots/add-element/DataProcessors/Тест/Forms/Форма/Ext/Form.xml +++ b/tests/skills/cases/form-edit/snapshots/add-element/DataProcessors/Тест/Forms/Форма/Ext/Form.xml @@ -7,7 +7,7 @@ false - + Поле1 @@ -17,8 +17,8 @@ Поле 1 - - + + Поле2 @@ -28,8 +28,8 @@ Поле 2 - - + + diff --git a/tests/skills/cases/form-edit/snapshots/add-group-with-fields/DataProcessors/Группа/Forms/Форма/Ext/Form.xml b/tests/skills/cases/form-edit/snapshots/add-group-with-fields/DataProcessors/Группа/Forms/Форма/Ext/Form.xml index cf0b1995..1861f7f9 100644 --- a/tests/skills/cases/form-edit/snapshots/add-group-with-fields/DataProcessors/Группа/Forms/Форма/Ext/Form.xml +++ b/tests/skills/cases/form-edit/snapshots/add-group-with-fields/DataProcessors/Группа/Forms/Форма/Ext/Form.xml @@ -7,7 +7,7 @@ false - + Поле1 @@ -17,8 +17,8 @@ Существующее поле - - + + @@ -28,7 +28,7 @@ </v8:item> Horizontal - + Поле2 @@ -38,8 +38,8 @@ Поле 2 - - + + Поле3 @@ -49,8 +49,8 @@ Поле 3 - - + + diff --git a/tests/skills/cases/help-add/basic.json b/tests/skills/cases/help-add/basic.json index d3fae3d2..4e8ea472 100644 --- a/tests/skills/cases/help-add/basic.json +++ b/tests/skills/cases/help-add/basic.json @@ -3,8 +3,25 @@ "preRun": [ { "script": "epf-init/scripts/init", - "args": { "-Name": "МояОбработка", "-SrcDir": "{workDir}" } + "args": { + "-Name": "МояОбработка", + "-SrcDir": "{workDir}" + } } ], - "params": { "objectName": "МояОбработка" } + "params": { + "objectName": "МояОбработка" + }, + "expect": { + "preserves": { + "file": "МояОбработка.xml", + "bom": true, + "eol": "crlf", + "encoding": "UTF-8", + "finalNewline": false, + "noCR13": true, + "selfClose": "tight", + "noEmptyPairs": true + } + } } diff --git a/tests/skills/cases/meta-compile/catalog-basic.json b/tests/skills/cases/meta-compile/catalog-basic.json index abb73e93..1a3d7b5f 100644 --- a/tests/skills/cases/meta-compile/catalog-basic.json +++ b/tests/skills/cases/meta-compile/catalog-basic.json @@ -1,8 +1,24 @@ { "name": "Простой справочник без реквизитов", - "input": { "type": "Catalog", "name": "Валюты" }, + "input": { + "type": "Catalog", + "name": "Валюты" + }, "validatePath": "Catalogs/Валюты", "expect": { - "files": ["Catalogs/Валюты.xml", "Catalogs/Валюты/Ext/ObjectModule.bsl"] + "files": [ + "Catalogs/Валюты.xml", + "Catalogs/Валюты/Ext/ObjectModule.bsl" + ], + "preserves": { + "file": "Catalogs/Валюты.xml", + "bom": true, + "eol": "crlf", + "encoding": "UTF-8", + "finalNewline": false, + "noCR13": true, + "selfClose": "tight", + "noEmptyPairs": true + } } } diff --git a/tests/skills/cases/meta-edit/fixtures/crlf-catalog/Configuration.xml b/tests/skills/cases/meta-edit/fixtures/crlf-catalog/Configuration.xml index e088855a..63ea4930 100644 --- a/tests/skills/cases/meta-edit/fixtures/crlf-catalog/Configuration.xml +++ b/tests/skills/cases/meta-edit/fixtures/crlf-catalog/Configuration.xml @@ -48,8 +48,8 @@ Russian - - + + false false diff --git a/tests/skills/cases/meta-edit/snapshots/roundtrip-crlf-preserve/Configuration.xml b/tests/skills/cases/meta-edit/snapshots/roundtrip-crlf-preserve/Configuration.xml index 431235cf..ee88a27c 100644 --- a/tests/skills/cases/meta-edit/snapshots/roundtrip-crlf-preserve/Configuration.xml +++ b/tests/skills/cases/meta-edit/snapshots/roundtrip-crlf-preserve/Configuration.xml @@ -48,8 +48,8 @@ Russian - - + + false false diff --git a/tests/skills/cases/meta-validate/fixtures/catalog-command-no-group/Catalogs/ТестКоманд.xml b/tests/skills/cases/meta-validate/fixtures/catalog-command-no-group/Catalogs/ТестКоманд.xml index 6837f4dc..f313f3d0 100644 --- a/tests/skills/cases/meta-validate/fixtures/catalog-command-no-group/Catalogs/ТестКоманд.xml +++ b/tests/skills/cases/meta-validate/fixtures/catalog-command-no-group/Catalogs/ТестКоманд.xml @@ -97,7 +97,7 @@ - + d5p1:CatalogRef.ТестКоманд diff --git a/tests/skills/cases/mxl-compile/minimal.json b/tests/skills/cases/mxl-compile/minimal.json index 44ea94f1..1dde26c4 100644 --- a/tests/skills/cases/mxl-compile/minimal.json +++ b/tests/skills/cases/mxl-compile/minimal.json @@ -6,14 +6,35 @@ { "name": "Ячейка", "rows": [ - { "cells": [{ "col": 1, "text": "Значение" }] } + { + "cells": [ + { + "col": 1, + "text": "Значение" + } + ] + } ] } ] }, - "params": { "outputPath": "Template.xml" }, + "params": { + "outputPath": "Template.xml" + }, "validatePath": "Template.xml", "expect": { - "files": ["Template.xml"] + "files": [ + "Template.xml" + ], + "preserves": { + "file": "Template.xml", + "bom": true, + "eol": "crlf", + "encoding": "UTF-8", + "finalNewline": false, + "noCR13": true, + "selfClose": "tight", + "noEmptyPairs": true + } } } diff --git a/tests/skills/cases/role-compile/basic-role.json b/tests/skills/cases/role-compile/basic-role.json index 8ed4937c..88463da6 100644 --- a/tests/skills/cases/role-compile/basic-role.json +++ b/tests/skills/cases/role-compile/basic-role.json @@ -24,6 +24,16 @@ "files": [ "Roles/Кладовщик.xml", "Roles/Кладовщик/Ext/Rights.xml" - ] + ], + "preserves": { + "file": "Roles/Кладовщик/Ext/Rights.xml", + "bom": true, + "eol": "crlf", + "encoding": "UTF-8", + "finalNewline": false, + "noCR13": true, + "selfClose": "tight", + "noEmptyPairs": true + } } } diff --git a/tests/skills/cases/skd-compile/minimal.json b/tests/skills/cases/skd-compile/minimal.json index 8d260ea3..3e8d4499 100644 --- a/tests/skills/cases/skd-compile/minimal.json +++ b/tests/skills/cases/skd-compile/minimal.json @@ -1,14 +1,32 @@ { "name": "Минимальная СКД — один набор, один запрос", - "params": { "outputPath": "Template.xml" }, + "params": { + "outputPath": "Template.xml" + }, "input": { - "dataSets": [{ - "query": "ВЫБРАТЬ Номенклатура.Наименование КАК Наименование ИЗ Справочник.Номенклатура КАК Номенклатура", - "fields": ["Наименование"] - }] + "dataSets": [ + { + "query": "ВЫБРАТЬ Номенклатура.Наименование КАК Наименование ИЗ Справочник.Номенклатура КАК Номенклатура", + "fields": [ + "Наименование" + ] + } + ] }, "validatePath": "Template.xml", "expect": { - "files": ["Template.xml"] + "files": [ + "Template.xml" + ], + "preserves": { + "file": "Template.xml", + "bom": true, + "eol": "crlf", + "encoding": "UTF-8", + "finalNewline": false, + "noCR13": true, + "selfClose": "tight", + "noEmptyPairs": true + } } } diff --git a/tests/skills/cases/skd-validate/fixtures/empty-field/Template.xml b/tests/skills/cases/skd-validate/fixtures/empty-field/Template.xml index 9b431c82..20cab85f 100644 --- a/tests/skills/cases/skd-validate/fixtures/empty-field/Template.xml +++ b/tests/skills/cases/skd-validate/fixtures/empty-field/Template.xml @@ -16,8 +16,8 @@ ИсточникДанных1 SELECT 1 - - + + diff --git a/tests/skills/cases/subsystem-compile/basic.json b/tests/skills/cases/subsystem-compile/basic.json index 0931586d..4d8fad68 100644 --- a/tests/skills/cases/subsystem-compile/basic.json +++ b/tests/skills/cases/subsystem-compile/basic.json @@ -3,17 +3,37 @@ "preRun": [ { "script": "meta-compile/scripts/meta-compile", - "input": { "type": "Catalog", "name": "Товары" }, - "args": { "-JsonPath": "{inputFile}", "-OutputDir": "{workDir}" } + "input": { + "type": "Catalog", + "name": "Товары" + }, + "args": { + "-JsonPath": "{inputFile}", + "-OutputDir": "{workDir}" + } } ], "input": { "name": "Склад", "synonym": "Склад", - "content": ["Catalogs.Товары"] + "content": [ + "Catalogs.Товары" + ] }, "validatePath": "Subsystems/Склад", "expect": { - "files": ["Subsystems/Склад.xml"] + "files": [ + "Subsystems/Склад.xml" + ], + "preserves": { + "file": "Subsystems/Склад.xml", + "bom": true, + "eol": "crlf", + "encoding": "UTF-8", + "finalNewline": false, + "noCR13": true, + "selfClose": "tight", + "noEmptyPairs": true + } } } diff --git a/tests/skills/cases/subsystem-edit/fixtures/crlf-subsystem/Configuration.xml b/tests/skills/cases/subsystem-edit/fixtures/crlf-subsystem/Configuration.xml index 19aa5597..da643e02 100644 --- a/tests/skills/cases/subsystem-edit/fixtures/crlf-subsystem/Configuration.xml +++ b/tests/skills/cases/subsystem-edit/fixtures/crlf-subsystem/Configuration.xml @@ -48,8 +48,8 @@ Russian - - + + false false diff --git a/tests/skills/cases/subsystem-edit/snapshots/roundtrip-crlf-preserve/Configuration.xml b/tests/skills/cases/subsystem-edit/snapshots/roundtrip-crlf-preserve/Configuration.xml index 8d77fef8..7d6b7b98 100644 --- a/tests/skills/cases/subsystem-edit/snapshots/roundtrip-crlf-preserve/Configuration.xml +++ b/tests/skills/cases/subsystem-edit/snapshots/roundtrip-crlf-preserve/Configuration.xml @@ -48,8 +48,8 @@ Russian - - + + false false diff --git a/tests/skills/cases/xdto-compile/minimal.json b/tests/skills/cases/xdto-compile/minimal.json index 81a07ea2..143cfd71 100644 --- a/tests/skills/cases/xdto-compile/minimal.json +++ b/tests/skills/cases/xdto-compile/minimal.json @@ -1,9 +1,26 @@ { "name": "минимальный пакет: один objectType, два свойства", - "caseFiles": ["minimal.xsd"], - "params": { "xsdFile": "minimal.xsd" }, + "caseFiles": [ + "minimal.xsd" + ], + "params": { + "xsdFile": "minimal.xsd" + }, "validatePath": "XDTOPackages/minimal", "expect": { - "files": ["XDTOPackages/minimal.xml", "XDTOPackages/minimal/Ext/Package.bin"] + "files": [ + "XDTOPackages/minimal.xml", + "XDTOPackages/minimal/Ext/Package.bin" + ], + "preserves": { + "file": "XDTOPackages/minimal.xml", + "bom": true, + "eol": "crlf", + "encoding": "UTF-8", + "finalNewline": false, + "noCR13": true, + "selfClose": "tight", + "noEmptyPairs": true + } } } diff --git a/tests/skills/runner.mjs b/tests/skills/runner.mjs index 0e2b614e..f1e716bb 100644 --- a/tests/skills/runner.mjs +++ b/tests/skills/runner.mjs @@ -303,6 +303,14 @@ function buildArgs(skillConfig, caseData, workDir, inputFilePath, runtime) { const UUID_RE = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi; +// Applied to the python runtime only: irons out ElementTree/lxml serialization quirks +// so a snapshot recorded from PowerShell can still be compared against the python port. +// +// Three former steps — space before `/>`, empty pair ``, trailing whitespace — +// were REMOVED with the fix for issue #57. They masked exactly the divergence that issue +// was about (PS wrote `` plus a trailing newline, python wrote `` without one), +// so port parity was being checked through the mask. Do not bring them back: the byte +// canon itself is now asserted per case via `preserves`. function normalizeXmlContent(text, opts = {}) { let s = text; // 1. XML declaration: normalize quotes and encoding case @@ -317,14 +325,9 @@ function normalizeXmlContent(text, opts = {}) { if (!opts.keepXmlns) { s = s.replace(/\s+xmlns(?::[\w]+)?="[^"]*"/g, ''); } - // 4. Normalize self-closing tags: remove space before /> - s = s.replace(/\s*\/>/g, '/>'); - // 5. Collapse whitespace between tags: "> \n\t <" → "><" + // 3. Collapse whitespace between tags: "> \n\t <" → "><". Kept: the ports indent a + // few blocks differently, which is formatting rather than the byte canon. s = s.replace(/>\s+<'); - // 6. Normalize empty elements: - s = s.replace(/<([\w:.]+)([^>]*)><\/\1>/g, '<$1$2/>'); - // 7. Strip trailing whitespace - s = s.trimEnd(); return s; } @@ -356,10 +359,11 @@ function normalizeContent(text, config, relFile) { return s; } -// ─── Byte-style preservation check (round-trip #44/#46/#47) ───────────────── +// ─── Byte-style preservation check (round-trip #44/#46/#47, канон #57) ────── // Проверяет СЫРЫЕ байты файла (в обход normalizeContent): BOM / EOL / регистр -// encoding / финальный перенос / отсутствие . spec: { file, bom, eol:"crlf"|"lf", -// encoding, finalNewline, noCR13 }. Возвращает массив ошибок. +// encoding / финальный перенос / отсутствие / форма пустого элемента. +// spec: { file, bom, eol:"crlf"|"lf", encoding, finalNewline, noCR13, +// selfClose:"tight", noEmptyPairs }. Возвращает массив ошибок. function checkPreserves(workDir, spec) { const errs = []; const target = join(workDir, spec.file); @@ -371,9 +375,18 @@ function checkPreserves(workDir, spec) { if (spec.bom !== undefined && hasBom !== spec.bom) errs.push(`preserves: BOM expected ${spec.bom}, got ${hasBom}`); if (spec.eol) { - const hasCR = body.includes(0x0d); - const wantCR = spec.eol === 'crlf'; - if (hasCR !== wantCR) errs.push(`preserves: EOL expected ${spec.eol} (CR=${wantCR}), got CR=${hasCR}`); + // Считаем ОДИНОЧНЫЕ LF, а не «есть ли хоть один CR»: прежняя проверка пропускала + // смешанный выход (cfe-init давал 10 CR на 70 строк и проходил её) — то есть + // главный дефект #57 был ей невидим. + let lf = 0, crlf = 0; + for (let i = 0; i < body.length; i++) { + if (body[i] === 0x0a) { lf++; if (i > 0 && body[i - 1] === 0x0d) crlf++; } + } + const loneLF = lf - crlf; + if (spec.eol === 'crlf' && loneLF > 0) + errs.push(`preserves: EOL expected crlf, got mixed (${crlf} CRLF + ${loneLF} lone LF)`); + if (spec.eol === 'lf' && crlf > 0) + errs.push(`preserves: EOL expected lf, got mixed (${crlf} CRLF + ${loneLF} lone LF)`); } if (spec.encoding) { const m = /encoding="([^"]+)"/.exec(text); @@ -384,6 +397,16 @@ function checkPreserves(workDir, spec) { if (endsNL !== spec.finalNewline) errs.push(`preserves: finalNewline expected ${spec.finalNewline}, got ${endsNL}`); } if (spec.noCR13 && text.includes(' ')) errs.push(`preserves: unexpected literal in output`); + // Канон Конфигуратора (#57): пустой элемент — , не и не . + // Проверено на 8 выгрузках в cfsrc: 21 294 119 самозакрывающихся тегов, пробельных 0. + if (spec.selfClose === 'tight') { + const spaced = text.match(/<[\w:.]+[^<>]*?\s\/>/g); + if (spaced) errs.push(`preserves: expected tight self-closing, got ${spaced.length}× spaced (e.g. ${spaced[0].slice(0, 60)})`); + } + if (spec.noEmptyPairs) { + const pairs = text.match(/<([\w:.]+)([^<>]*)><\/\1>/g); + if (pairs) errs.push(`preserves: expected self-closing, got ${pairs.length}× empty pair (e.g. ${pairs[0].slice(0, 60)})`); + } return errs; }