test(runner): байтовые проверки канона вместо масок (#57)

Раннер сам прятал дефекты, которые чинит #57. normalizeXmlContent при
runtime=python снимал ровно четыре измерения: пробел перед `/>`, whitespace между
тегами, пустую пару <Tag></Tag> и хвостовой пробельный мусор. Паритет PS<->PY по
этим измерениям проверялся ЧЕРЕЗ маску — расхождение физически не могло упасть.

Снято три из четырёх (пробел, пустая пара, хвост). Схлопывание whitespace между
тегами оставлено: порты кое-где расставляют отступы иначе, это форматирование, а
не байтовый канон.

Ужесточён checkPreserves: eol теперь считает ОДИНОЧНЫЕ LF, а не «есть ли хоть один
CR». Прежняя проверка пропускала смешанный выход — cfe-init давал 10 CR на 70
строк и проходил её, то есть головной дефект тикета был ей невидим. Добавлены
ключи selfClose:"tight" и noEmptyPairs; preserves проставлен в 13 кейсах
навыков-эмиттеров (по одному на навык, на его СОБСТВЕННЫЙ артефакт).

Снятие масок сразу вскрыло четыре реальных расхождения портов:

- form-edit собирает выход из OuterXml и писал `<a />`; в списке 17 навыков его не
  было, потому что искал по вызовам Save — здесь другой путь. Тот же случай, что
  с Form.xml в cfe-borrow;
- meta-edit py дописывал хвостовой перевод в создаваемый Ext/Predefined.xml и
  читал существующий без newline='' (терял CRLF);
- skd-info py писал отчёт -OutFile без хвостового перевода, PS через WriteAllLines
  — с ним. Это текстовый отчёт, канон Конфигуратора к нему не относится, поэтому
  выровнял py по существующему эталону;
- фикстуры кейсов содержали <Vendor></Vendor> — снимок нашего же старого вывода.
  Конфигуратор пустых пар не пишет, .NET их сохраняет, lxml схлопывает. Поправлены
  10 фикстур: пары → самозакрывающиеся, EOL и BOM не тронуты.

Результат: PS 641/641, python 638/641 (+3 runtimeOnly-скипа) — со снятыми масками.
Дрейф снэпшотов: 10 файлов, только пробельные теги и пустые пары.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-08-05 16:23:13 +03:00
co-authored by Claude Opus 5
parent 11e4e4301f
commit d7fd6d79d3
41 changed files with 361 additions and 106 deletions
+9 -1
View File
@@ -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` — в выходе не должно быть литерала `&#13;` |
| `selfClose` | `"tight"` — пустой элемент только как `<a/>`, без пробела перед `/>` |
| `noEmptyPairs` | `true` — пустого элемента в форме `<a></a>` быть не должно |
Канон выгрузки Конфигуратора (issue #57), измеренный на чистой выгрузке пустой ИБ на Windows
и macOS и на 8 выгрузках в `cfsrc/`: **CRLF, BOM, последний байт `>` (без перевода строки),
`<a/>` без пробела, ноль пустых пар, `encoding="UTF-8"`.** Для файла, который навык СОЗДАЁТ,
ожидается канон; для файла, который он ПРАВИТ, — стиль входного файла (контракт #44/#46/#47),
поэтому в кейсах `roundtrip-crlf-preserve` ожидания могут отличаться от канона.
`preserves` и эталон **дополняют** друг друга: первый следит за байтовым стилем файла, второй — за
структурой содержимого. Наличие одного не отменяет необходимости другого.