mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-08-07 20:20:20 +03:00
feat(cfe-borrow,cfe-init,form-add,form-compile,role-compile,subsystem-compile,subsystem-edit,template-add,xdto-compile): xmlns:pal в формате 2.21
Платформа 8.5 объявляет пространство палитры в шапках MetaDataObject и Form. Радиус снят по выгрузке УНФ 8.5, а не угадан: pal есть у MetaDataObject, Form, document (тело MXL), Style, AppearanceTemplate, GraphicalSchema; у Rights, Help, CommandInterface, DataCompositionSchema и корней extrnprops его нет. Поэтому role-compile правит шапку Role.xml и не трогает Rights.xml. Вставка идёт на место — после lf, перед style: объявления платформа держит по алфавиту, дописать в конец нельзя. Попутно в cfe-borrow тег <Form> больше не копируется из исходной формы целиком: из него берутся только объявления пространств, а version подставляется своя. Раньше версия источника молча побеждала — вопреки комментарию рядом. Тесты: снята нормализация xmlns в снэпшотах py-прогона (она прятала ровно этот класс расхождений и к моменту снятия была мёртвой — ни один кейс на неё не опирался); добавлены expect.fileContains/fileNotContains по сырым байтам и гейт на неизвестные ключи expect.* — он вскрыл два кейса-пустышки (skd-compile, skd-edit), они починены. 648/648 ps1, 645/648 py. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
d60dd47f0b
commit
53c1d59ec1
+65
-11
@@ -311,21 +311,18 @@ const UUID_RE = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi
|
||||
// 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 = {}) {
|
||||
//
|
||||
// Четвёртый шаг — вырезание объявлений xmlns — снят по той же причине: он прятал
|
||||
// целый класс расхождений (лишнее/недостающее объявление в шапке, как xmlns:pal
|
||||
// формата 2.21) и к моменту снятия был мёртвым — ни один кейс на него не опирался.
|
||||
function normalizeXmlContent(text) {
|
||||
let s = text;
|
||||
// 1. XML declaration: normalize quotes and encoding case
|
||||
s = s.replace(
|
||||
/<\?xml\s+version=['"]1\.0['"]\s+encoding=['"]([^'"]+)['"]\s*\?>/gi,
|
||||
(_, enc) => `<?xml version="1.0" encoding="${enc.toLowerCase()}"?>`
|
||||
);
|
||||
// 2. Strip xmlns declarations (Python etree strips unused ones).
|
||||
// Skipped for Configuration.xml: those declarations are load-bearing (they back
|
||||
// xsi:type values like app:ApplicationUsePurpose in UsePurposes) and dropping them
|
||||
// is exactly the corruption of issue #38 — keeping them lets the test guard against it.
|
||||
if (!opts.keepXmlns) {
|
||||
s = s.replace(/\s+xmlns(?::[\w]+)?="[^"]*"/g, '');
|
||||
}
|
||||
// 3. Collapse whitespace between tags: "> \n\t <" → "><". Kept: the ports indent a
|
||||
// 2. 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, '><');
|
||||
return s;
|
||||
@@ -338,8 +335,7 @@ function normalizeContent(text, config, relFile) {
|
||||
s = s.replace(/\r\n/g, '\n');
|
||||
// Normalize XML differences (Python etree serialization quirks)
|
||||
if (config?.runtime === 'python') {
|
||||
const base = relFile ? relFile.split(/[\\/]/).pop() : '';
|
||||
s = normalizeXmlContent(s, { keepXmlns: base === 'Configuration.xml' });
|
||||
s = normalizeXmlContent(s);
|
||||
}
|
||||
|
||||
// Normalize UUIDs
|
||||
@@ -359,6 +355,42 @@ function normalizeContent(text, config, relFile) {
|
||||
return s;
|
||||
}
|
||||
|
||||
// ─── Проверка содержимого файла по СЫРЫМ байтам ────────────────────────────
|
||||
// Снэпшотное сравнение в py-прогоне режет объявления xmlns (normalizeXmlContent),
|
||||
// поэтому наличие/отсутствие конкретного объявления через снэпшот не проверить —
|
||||
// он совпадёт при любом исходе. Эта проверка читает файл как есть.
|
||||
// spec: { file, text } | { file, text: [...] }. Возвращает массив ошибок.
|
||||
function checkFileContains(workDir, spec, expectPresent) {
|
||||
const errs = [];
|
||||
const target = join(workDir, spec.file);
|
||||
if (!existsSync(target)) {
|
||||
errs.push(`${expectPresent ? 'fileContains' : 'fileNotContains'}: file not found: ${spec.file}`);
|
||||
return errs;
|
||||
}
|
||||
const text = readFileSync(target).toString('utf8').replace(/^/, '');
|
||||
const needles = Array.isArray(spec.text) ? spec.text : [spec.text];
|
||||
for (const needle of needles) {
|
||||
const found = text.includes(needle);
|
||||
if (expectPresent && !found) errs.push(`${spec.file} does not contain "${needle}"`);
|
||||
if (!expectPresent && found) errs.push(`${spec.file} unexpectedly contains "${needle}"`);
|
||||
}
|
||||
return errs;
|
||||
}
|
||||
|
||||
// Ключи expect, которые раннер действительно умеет. Неизвестный ключ = кейс,
|
||||
// который молча ничего не проверяет (так уже было с 9 кейсами meta-edit) —
|
||||
// поэтому он ошибка, а не игнор.
|
||||
const KNOWN_EXPECT_KEYS = new Set([
|
||||
'files', 'stdoutContains', 'stdoutNotContains', 'preserves',
|
||||
'fileContains', 'fileNotContains',
|
||||
]);
|
||||
|
||||
function checkExpectKeys(caseData) {
|
||||
if (!caseData.expect) return [];
|
||||
const unknown = Object.keys(caseData.expect).filter(k => !KNOWN_EXPECT_KEYS.has(k));
|
||||
return unknown.map(k => `expect.${k}: раннер такого ключа не знает — кейс ничего не проверяет`);
|
||||
}
|
||||
|
||||
// ─── Byte-style preservation check (round-trip #44/#46/#47, канон #57) ──────
|
||||
// Проверяет СЫРЫЕ байты файла (в обход normalizeContent): BOM / EOL / регистр
|
||||
// encoding / финальный перенос / отсутствие / форма пустого элемента.
|
||||
@@ -709,6 +741,7 @@ async function runCaseAsync(testCase, opts) {
|
||||
|
||||
// Assertions
|
||||
const errors = [];
|
||||
errors.push(...checkExpectKeys(caseData));
|
||||
if (caseData.expectError) {
|
||||
if (exitCode === 0) errors.push('Expected error (non-zero exit) but got exitCode=0');
|
||||
if (typeof caseData.expectError === 'string' && !stderr.includes(caseData.expectError)) {
|
||||
@@ -748,6 +781,16 @@ async function runCaseAsync(testCase, opts) {
|
||||
? caseData.expect.preserves : [caseData.expect.preserves];
|
||||
for (const spec of specs) errors.push(...checkPreserves(workDir, spec));
|
||||
}
|
||||
if (caseData.expect?.fileContains) {
|
||||
const specs = Array.isArray(caseData.expect.fileContains)
|
||||
? caseData.expect.fileContains : [caseData.expect.fileContains];
|
||||
for (const spec of specs) errors.push(...checkFileContains(workDir, spec, true));
|
||||
}
|
||||
if (caseData.expect?.fileNotContains) {
|
||||
const specs = Array.isArray(caseData.expect.fileNotContains)
|
||||
? caseData.expect.fileNotContains : [caseData.expect.fileNotContains];
|
||||
for (const spec of specs) errors.push(...checkFileContains(workDir, spec, false));
|
||||
}
|
||||
if (errors.length === 0 && !caseData.expectError && !workspace.readOnly) {
|
||||
const snapshotConfig = { ...skillConfig.snapshot, runtime: opts.runtime };
|
||||
if (opts.updateSnapshots) {
|
||||
@@ -898,6 +941,7 @@ function runCase(testCase, opts) {
|
||||
|
||||
// 4. Assertions
|
||||
const errors = [];
|
||||
errors.push(...checkExpectKeys(caseData));
|
||||
|
||||
if (caseData.expectError) {
|
||||
// Negative case — expect failure
|
||||
@@ -949,6 +993,16 @@ function runCase(testCase, opts) {
|
||||
? caseData.expect.preserves : [caseData.expect.preserves];
|
||||
for (const spec of specs) errors.push(...checkPreserves(workDir, spec));
|
||||
}
|
||||
if (caseData.expect?.fileContains) {
|
||||
const specs = Array.isArray(caseData.expect.fileContains)
|
||||
? caseData.expect.fileContains : [caseData.expect.fileContains];
|
||||
for (const spec of specs) errors.push(...checkFileContains(workDir, spec, true));
|
||||
}
|
||||
if (caseData.expect?.fileNotContains) {
|
||||
const specs = Array.isArray(caseData.expect.fileNotContains)
|
||||
? caseData.expect.fileNotContains : [caseData.expect.fileNotContains];
|
||||
for (const spec of specs) errors.push(...checkFileContains(workDir, spec, false));
|
||||
}
|
||||
|
||||
// Snapshot comparison (skip for external/read-only workspaces)
|
||||
if (errors.length === 0 && !caseData.expectError && !workspace.readOnly) {
|
||||
|
||||
Reference in New Issue
Block a user