mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-08-07 20:20:20 +03:00
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:
co-authored by
Claude Opus 5
parent
11e4e4301f
commit
d7fd6d79d3
+36
-13
@@ -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 `<Tag></Tag>`, trailing whitespace —
|
||||
// were REMOVED with the fix for issue #57. They masked exactly the divergence that issue
|
||||
// was about (PS wrote `<a />` plus a trailing newline, python wrote `<a/>` 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+</g, '><');
|
||||
// 6. Normalize empty elements: <Tag></Tag> → <Tag/>
|
||||
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): пустой элемент — <a/>, не <a /> и не <a></a>.
|
||||
// Проверено на 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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user