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
@@ -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 # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param( param(
[Parameter(Mandatory)] [Parameter(Mandatory)]
@@ -1387,6 +1387,9 @@ if ($def.elementEvents -and $def.elementEvents.Count -gt 0) {
$content = $xmlDoc.OuterXml $content = $xmlDoc.OuterXml
# Ensure encoding declaration is uppercase UTF-8 # Ensure encoding declaration is uppercase UTF-8
$content = $content -replace '^<\?xml version="1.0" encoding="utf-8"\?>', '<?xml version="1.0" encoding="UTF-8"?>' $content = $content -replace '^<\?xml version="1.0" encoding="utf-8"\?>', '<?xml version="1.0" encoding="UTF-8"?>'
# Пустой элемент: OuterXml (как и XmlWriter) пишет `<a />`, Конфигуратор — `<a/>`.
# Гард на CDATA/комментарии: только там ` />` может быть содержимым, а не концом тега.
if ($content -notmatch '<!\[CDATA\[|<!--') { $content = [regex]::Replace($content, '(?<=\S) />', '/>') }
$enc = New-Object System.Text.UTF8Encoding($true) $enc = New-Object System.Text.UTF8Encoding($true)
[System.IO.File]::WriteAllText($resolvedFormPath, $content, $enc) [System.IO.File]::WriteAllText($resolvedFormPath, $content, $enc)
@@ -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 # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
import json import json
@@ -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 # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param( param(
[string]$DefinitionFile, [string]$DefinitionFile,
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/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 # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
@@ -3094,7 +3094,9 @@ def add_predefined_items(items):
item_list = items if isinstance(items, list) else [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) items_xml = ''.join(build_predef_item_xml('\t', it, code_type) for it in item_list)
if os.path.exists(path): 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 = f.read()
text = text.replace('</PredefinedData>', items_xml + '</PredefinedData>') text = text.replace('</PredefinedData>', items_xml + '</PredefinedData>')
else: 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: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" ' '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') f'xsi:type="{xsi_type}" version="{version}">\r\n')
text = hdr + items_xml + '</PredefinedData>\r\n' text = hdr + items_xml + '</PredefinedData>'
with open(path, 'wb') as f: with open(path, 'wb') as f:
f.write(b'\xef\xbb\xbf') f.write(b'\xef\xbb\xbf')
f.write(text.encode('utf-8')) f.write(text.encode('utf-8'))
+1 -1
View File
@@ -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 # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param( param(
[Parameter(Mandatory=$true)] [Parameter(Mandatory=$true)]
+5 -2
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/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 # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
@@ -1802,7 +1802,10 @@ def main():
if not os.path.isabs(out_path): if not os.path.isabs(out_path):
out_path = os.path.join(os.getcwd(), out_path) out_path = os.path.join(os.getcwd(), out_path)
with open(out_path, "w", encoding="utf-8-sig") as fh: 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}") print(f"Written {total_lines} lines to {args.OutFile}")
sys.exit(0) sys.exit(0)
+9 -1
View File
@@ -252,10 +252,18 @@ ibcmd-проход автоматически `○ skipped`, если рядом
|---|---| |---|---|
| `file` | Путь к файлу относительно `workDir` (обязателен) | | `file` | Путь к файлу относительно `workDir` (обязателен) |
| `bom` | `true`/`false` — наличие UTF-8 BOM | | `bom` | `true`/`false` — наличие UTF-8 BOM |
| `eol` | `"crlf"` / `"lf"` | | `eol` | `"crlf"` / `"lf"` — проверяются ОДИНОЧНЫЕ переводы строк, поэтому смешанный выход падает |
| `encoding` | Ожидаемое значение в XML-декларации, напр. `"UTF-8"` | | `encoding` | Ожидаемое значение в XML-декларации, напр. `"UTF-8"` |
| `finalNewline` | `true`/`false` — перевод строки в конце файла | | `finalNewline` | `true`/`false` — перевод строки в конце файла |
| `noCR13` | `true` — в выходе не должно быть литерала `&#13;` | | `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` и эталон **дополняют** друг друга: первый следит за байтовым стилем файла, второй — за `preserves` и эталон **дополняют** друг друга: первый следит за байтовым стилем файла, второй — за
структурой содержимого. Наличие одного не отменяет необходимости другого. структурой содержимого. Наличие одного не отменяет необходимости другого.
@@ -48,8 +48,8 @@
</UsePurposes> </UsePurposes>
<ScriptVariant>Russian</ScriptVariant> <ScriptVariant>Russian</ScriptVariant>
<DefaultRoles/> <DefaultRoles/>
<Vendor></Vendor> <Vendor/>
<Version></Version> <Version/>
<CompatibilityMode>Version8_3_24</CompatibilityMode> <CompatibilityMode>Version8_3_24</CompatibilityMode>
<DefaultLanguage>Language.Русский</DefaultLanguage> <DefaultLanguage>Language.Русский</DefaultLanguage>
<DataLockControlMode>Managed</DataLockControlMode> <DataLockControlMode>Managed</DataLockControlMode>
@@ -48,8 +48,8 @@
</UsePurposes> </UsePurposes>
<ScriptVariant>Russian</ScriptVariant> <ScriptVariant>Russian</ScriptVariant>
<DefaultRoles/> <DefaultRoles/>
<Vendor></Vendor> <Vendor/>
<Version></Version> <Version/>
<UpdateCatalogAddress/> <UpdateCatalogAddress/>
<IncludeHelpInContents>false</IncludeHelpInContents> <IncludeHelpInContents>false</IncludeHelpInContents>
<UseManagedFormInOrdinaryApplication>false</UseManagedFormInOrdinaryApplication> <UseManagedFormInOrdinaryApplication>false</UseManagedFormInOrdinaryApplication>
@@ -48,8 +48,8 @@
</UsePurposes> </UsePurposes>
<ScriptVariant>Russian</ScriptVariant> <ScriptVariant>Russian</ScriptVariant>
<DefaultRoles/> <DefaultRoles/>
<Vendor></Vendor> <Vendor/>
<Version></Version> <Version/>
<CompatibilityMode>Version8_3_24</CompatibilityMode> <CompatibilityMode>Version8_3_24</CompatibilityMode>
<DefaultLanguage>Language.Русский</DefaultLanguage> <DefaultLanguage>Language.Русский</DefaultLanguage>
<DataLockControlMode>Managed</DataLockControlMode> <DataLockControlMode>Managed</DataLockControlMode>
@@ -48,7 +48,7 @@
</UsePurposes> </UsePurposes>
<ScriptVariant>Russian</ScriptVariant> <ScriptVariant>Russian</ScriptVariant>
<DefaultRoles/> <DefaultRoles/>
<Vendor></Vendor> <Vendor/>
<Version>1.0.0.2</Version> <Version>1.0.0.2</Version>
<UpdateCatalogAddress/> <UpdateCatalogAddress/>
<IncludeHelpInContents>false</IncludeHelpInContents> <IncludeHelpInContents>false</IncludeHelpInContents>
@@ -48,8 +48,8 @@
</UsePurposes> </UsePurposes>
<ScriptVariant>Russian</ScriptVariant> <ScriptVariant>Russian</ScriptVariant>
<DefaultRoles/> <DefaultRoles/>
<Vendor></Vendor> <Vendor/>
<Version></Version> <Version/>
<CompatibilityMode>Version8_3_24</CompatibilityMode> <CompatibilityMode>Version8_3_24</CompatibilityMode>
<DefaultLanguage>Language.Русский</DefaultLanguage> <DefaultLanguage>Language.Русский</DefaultLanguage>
<DataLockControlMode>Managed</DataLockControlMode> <DataLockControlMode>Managed</DataLockControlMode>
@@ -48,8 +48,8 @@
</UsePurposes> </UsePurposes>
<ScriptVariant>Russian</ScriptVariant> <ScriptVariant>Russian</ScriptVariant>
<DefaultRoles/> <DefaultRoles/>
<Vendor></Vendor> <Vendor/>
<Version></Version> <Version/>
<CompatibilityMode>Version8_3_24</CompatibilityMode> <CompatibilityMode>Version8_3_24</CompatibilityMode>
<DefaultLanguage>Language.Русский</DefaultLanguage> <DefaultLanguage>Language.Русский</DefaultLanguage>
<DataLockControlMode>Managed</DataLockControlMode> <DataLockControlMode>Managed</DataLockControlMode>
+11 -1
View File
@@ -8,6 +8,16 @@
"Configuration.xml", "Configuration.xml",
"Languages/Русский.xml", "Languages/Русский.xml",
"Ext/ClientApplicationInterface.xml" "Ext/ClientApplicationInterface.xml"
] ],
"preserves": {
"file": "Configuration.xml",
"bom": true,
"eol": "crlf",
"encoding": "UTF-8",
"finalNewline": false,
"noCR13": true,
"selfClose": "tight",
"noEmptyPairs": true
}
} }
} }
@@ -32,7 +32,7 @@
</xr:ContainedObject> </xr:ContainedObject>
</InternalInfo> </InternalInfo>
<Properties> <Properties>
<Name></Name> <Name/>
<Synonym> <Synonym>
<v8:item> <v8:item>
<v8:lang>ru</v8:lang> <v8:lang>ru</v8:lang>
@@ -48,8 +48,8 @@
</UsePurposes> </UsePurposes>
<ScriptVariant>Russian</ScriptVariant> <ScriptVariant>Russian</ScriptVariant>
<DefaultRoles/> <DefaultRoles/>
<Vendor></Vendor> <Vendor/>
<Version></Version> <Version/>
<CompatibilityMode>Version8_3_24</CompatibilityMode> <CompatibilityMode>Version8_3_24</CompatibilityMode>
<DefaultLanguage>Language.Русский</DefaultLanguage> <DefaultLanguage>Language.Русский</DefaultLanguage>
<DataLockControlMode>Managed</DataLockControlMode> <DataLockControlMode>Managed</DataLockControlMode>
@@ -48,8 +48,8 @@
</UsePurposes> </UsePurposes>
<ScriptVariant>Russian</ScriptVariant> <ScriptVariant>Russian</ScriptVariant>
<DefaultRoles/> <DefaultRoles/>
<Vendor></Vendor> <Vendor/>
<Version></Version> <Version/>
<CompatibilityMode>Version8_3_24</CompatibilityMode> <CompatibilityMode>Version8_3_24</CompatibilityMode>
<DefaultLanguage>Language.Русский</DefaultLanguage> <DefaultLanguage>Language.Русский</DefaultLanguage>
<DataLockControlMode>Managed</DataLockControlMode> <DataLockControlMode>Managed</DataLockControlMode>
@@ -48,8 +48,8 @@
</UsePurposes> </UsePurposes>
<ScriptVariant>Russian</ScriptVariant> <ScriptVariant>Russian</ScriptVariant>
<DefaultRoles/> <DefaultRoles/>
<Vendor></Vendor> <Vendor/>
<Version></Version> <Version/>
<CompatibilityMode>Version8_3_24</CompatibilityMode> <CompatibilityMode>Version8_3_24</CompatibilityMode>
<DefaultLanguage>Language.Русский</DefaultLanguage> <DefaultLanguage>Language.Русский</DefaultLanguage>
<DataLockControlMode>Managed</DataLockControlMode> <DataLockControlMode>Managed</DataLockControlMode>
+17 -2
View File
@@ -1,8 +1,23 @@
{ {
"name": "Пустое расширение", "name": "Пустое расширение",
"params": { "name": "МоёРасширение", "outputDir": "ext" }, "params": {
"name": "МоёРасширение",
"outputDir": "ext"
},
"validatePath": "ext", "validatePath": "ext",
"expect": { "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
}
} }
} }
@@ -8,7 +8,7 @@
</xr:ContainedObject> </xr:ContainedObject>
</InternalInfo> </InternalInfo>
<Properties> <Properties>
<Name></Name> <Name/>
<CompatibilityMode>Version8_3_24</CompatibilityMode> <CompatibilityMode>Version8_3_24</CompatibilityMode>
</Properties> </Properties>
<ChildObjects/> <ChildObjects/>
+16 -2
View File
@@ -1,8 +1,22 @@
{ {
"name": "Пустая внешняя обработка", "name": "Пустая внешняя обработка",
"params": { "name": "ТестоваяОбработка" }, "params": {
"name": "ТестоваяОбработка"
},
"validatePath": "ТестоваяОбработка", "validatePath": "ТестоваяОбработка",
"expect": { "expect": {
"files": ["ТестоваяОбработка.xml"] "files": [
"ТестоваяОбработка.xml"
],
"preserves": {
"file": "ТестоваяОбработка.xml",
"bom": true,
"eol": "crlf",
"encoding": "UTF-8",
"finalNewline": false,
"noCR13": true,
"selfClose": "tight",
"noEmptyPairs": true
}
} }
} }
+16 -2
View File
@@ -1,8 +1,22 @@
{ {
"name": "Пустой внешний отчёт", "name": "Пустой внешний отчёт",
"params": { "name": "ТестовыйОтчёт" }, "params": {
"name": "ТестовыйОтчёт"
},
"validatePath": "ТестовыйОтчёт", "validatePath": "ТестовыйОтчёт",
"expect": { "expect": {
"files": ["ТестовыйОтчёт.xml"] "files": [
"ТестовыйОтчёт.xml"
],
"preserves": {
"file": "ТестовыйОтчёт.xml",
"bom": true,
"eol": "crlf",
"encoding": "UTF-8",
"finalNewline": false,
"noCR13": true,
"selfClose": "tight",
"noEmptyPairs": true
}
} }
} }
+25 -4
View File
@@ -3,10 +3,31 @@
"preRun": [ "preRun": [
{ {
"script": "meta-compile/scripts/meta-compile", "script": "meta-compile/scripts/meta-compile",
"input": { "type": "Catalog", "name": "Товары" }, "input": {
"args": { "-JsonPath": "{inputFile}", "-OutputDir": "{workDir}" } "type": "Catalog",
"name": "Товары"
},
"args": {
"-JsonPath": "{inputFile}",
"-OutputDir": "{workDir}"
}
} }
], ],
"params": { "objectPath": "Catalogs/Товары.xml", "formName": "ФормаЭлемента" }, "params": {
"validatePath": "Catalogs/Товары/Forms/ФормаЭлемента" "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
}
}
} }
+27 -4
View File
@@ -3,17 +3,40 @@
"preRun": [ "preRun": [
{ {
"script": "meta-compile/scripts/meta-compile", "script": "meta-compile/scripts/meta-compile",
"input": { "type": "DataProcessor", "name": "Минимальная" }, "input": {
"args": { "-JsonPath": "{inputFile}", "-OutputDir": "{workDir}" } "type": "DataProcessor",
"name": "Минимальная"
},
"args": {
"-JsonPath": "{inputFile}",
"-OutputDir": "{workDir}"
}
}, },
{ {
"script": "form-add/scripts/form-add", "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", "validatePath": "DataProcessors/Минимальная/Forms/Форма/Ext/Form.xml",
"input": { "input": {
"title": "Минимальная форма" "title": "Минимальная форма"
},
"expect": {
"preserves": {
"file": "DataProcessors/Минимальная/Forms/Форма/Ext/Form.xml",
"bom": true,
"eol": "crlf",
"encoding": "UTF-8",
"finalNewline": false,
"noCR13": true,
"selfClose": "tight",
"noEmptyPairs": true
}
} }
} }
@@ -7,7 +7,7 @@
</v8:item> </v8:item>
</Title> </Title>
<AutoTitle>false</AutoTitle> <AutoTitle>false</AutoTitle>
<AutoCommandBar name="ФормаКоманднаяПанель" id="-1" /> <AutoCommandBar name="ФормаКоманднаяПанель" id="-1"/>
<Attributes> <Attributes>
<Attribute name="Объект" id="1"> <Attribute name="Объект" id="1">
<Type> <Type>
@@ -7,12 +7,12 @@
</v8:item> </v8:item>
</Title> </Title>
<AutoTitle>false</AutoTitle> <AutoTitle>false</AutoTitle>
<AutoCommandBar name="ФормаКоманднаяПанель" id="-1" /> <AutoCommandBar name="ФормаКоманднаяПанель" id="-1"/>
<ChildItems> <ChildItems>
<Button name="Выполнить" id="1"> <Button name="Выполнить" id="1">
<CommandName>Form.Command.Выполнить</CommandName> <CommandName>Form.Command.Выполнить</CommandName>
<DefaultButton>true</DefaultButton> <DefaultButton>true</DefaultButton>
<ExtendedTooltip name="ВыполнитьРасширеннаяПодсказка" id="2" /> <ExtendedTooltip name="ВыполнитьРасширеннаяПодсказка" id="2"/>
</Button> </Button>
</ChildItems> </ChildItems>
<Attributes> <Attributes>
@@ -7,7 +7,7 @@
</v8:item> </v8:item>
</Title> </Title>
<AutoTitle>false</AutoTitle> <AutoTitle>false</AutoTitle>
<AutoCommandBar name="ФормаКоманднаяПанель" id="-1" /> <AutoCommandBar name="ФормаКоманднаяПанель" id="-1"/>
<ChildItems> <ChildItems>
<InputField name="Поле1" id="1"> <InputField name="Поле1" id="1">
<DataPath>Поле1</DataPath> <DataPath>Поле1</DataPath>
@@ -17,8 +17,8 @@
<v8:content>Поле 1</v8:content> <v8:content>Поле 1</v8:content>
</v8:item> </v8:item>
</Title> </Title>
<ContextMenu name="Поле1КонтекстноеМеню" id="2" /> <ContextMenu name="Поле1КонтекстноеМеню" id="2"/>
<ExtendedTooltip name="Поле1РасширеннаяПодсказка" id="3" /> <ExtendedTooltip name="Поле1РасширеннаяПодсказка" id="3"/>
</InputField> </InputField>
<InputField name="Поле2" id="4"> <InputField name="Поле2" id="4">
<DataPath>Поле2</DataPath> <DataPath>Поле2</DataPath>
@@ -28,8 +28,8 @@
<v8:content>Поле 2</v8:content> <v8:content>Поле 2</v8:content>
</v8:item> </v8:item>
</Title> </Title>
<ContextMenu name="Поле2КонтекстноеМеню" id="5" /> <ContextMenu name="Поле2КонтекстноеМеню" id="5"/>
<ExtendedTooltip name="Поле2РасширеннаяПодсказка" id="6" /> <ExtendedTooltip name="Поле2РасширеннаяПодсказка" id="6"/>
</InputField> </InputField>
</ChildItems> </ChildItems>
<Attributes> <Attributes>
@@ -7,7 +7,7 @@
</v8:item> </v8:item>
</Title> </Title>
<AutoTitle>false</AutoTitle> <AutoTitle>false</AutoTitle>
<AutoCommandBar name="ФормаКоманднаяПанель" id="-1" /> <AutoCommandBar name="ФормаКоманднаяПанель" id="-1"/>
<ChildItems> <ChildItems>
<InputField name="Поле1" id="1"> <InputField name="Поле1" id="1">
<DataPath>Поле1</DataPath> <DataPath>Поле1</DataPath>
@@ -17,8 +17,8 @@
<v8:content>Существующее поле</v8:content> <v8:content>Существующее поле</v8:content>
</v8:item> </v8:item>
</Title> </Title>
<ContextMenu name="Поле1КонтекстноеМеню" id="2" /> <ContextMenu name="Поле1КонтекстноеМеню" id="2"/>
<ExtendedTooltip name="Поле1РасширеннаяПодсказка" id="3" /> <ExtendedTooltip name="Поле1РасширеннаяПодсказка" id="3"/>
</InputField> </InputField>
<UsualGroup name="ГруппаНовая" id="4"> <UsualGroup name="ГруппаНовая" id="4">
<Title> <Title>
@@ -28,7 +28,7 @@
</v8:item> </v8:item>
</Title> </Title>
<Group>Horizontal</Group> <Group>Horizontal</Group>
<ExtendedTooltip name="ГруппаНоваяРасширеннаяПодсказка" id="5" /> <ExtendedTooltip name="ГруппаНоваяРасширеннаяПодсказка" id="5"/>
<ChildItems> <ChildItems>
<InputField name="Поле2" id="6"> <InputField name="Поле2" id="6">
<DataPath>Поле2</DataPath> <DataPath>Поле2</DataPath>
@@ -38,8 +38,8 @@
<v8:content>Поле 2</v8:content> <v8:content>Поле 2</v8:content>
</v8:item> </v8:item>
</Title> </Title>
<ContextMenu name="Поле2КонтекстноеМеню" id="7" /> <ContextMenu name="Поле2КонтекстноеМеню" id="7"/>
<ExtendedTooltip name="Поле2РасширеннаяПодсказка" id="8" /> <ExtendedTooltip name="Поле2РасширеннаяПодсказка" id="8"/>
</InputField> </InputField>
<InputField name="Поле3" id="9"> <InputField name="Поле3" id="9">
<DataPath>Поле3</DataPath> <DataPath>Поле3</DataPath>
@@ -49,8 +49,8 @@
<v8:content>Поле 3</v8:content> <v8:content>Поле 3</v8:content>
</v8:item> </v8:item>
</Title> </Title>
<ContextMenu name="Поле3КонтекстноеМеню" id="10" /> <ContextMenu name="Поле3КонтекстноеМеню" id="10"/>
<ExtendedTooltip name="Поле3РасширеннаяПодсказка" id="11" /> <ExtendedTooltip name="Поле3РасширеннаяПодсказка" id="11"/>
</InputField> </InputField>
</ChildItems> </ChildItems>
</UsualGroup> </UsualGroup>
+19 -2
View File
@@ -3,8 +3,25 @@
"preRun": [ "preRun": [
{ {
"script": "epf-init/scripts/init", "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
}
}
} }
@@ -1,8 +1,24 @@
{ {
"name": "Простой справочник без реквизитов", "name": "Простой справочник без реквизитов",
"input": { "type": "Catalog", "name": "Валюты" }, "input": {
"type": "Catalog",
"name": "Валюты"
},
"validatePath": "Catalogs/Валюты", "validatePath": "Catalogs/Валюты",
"expect": { "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
}
} }
} }
@@ -48,8 +48,8 @@
</UsePurposes> </UsePurposes>
<ScriptVariant>Russian</ScriptVariant> <ScriptVariant>Russian</ScriptVariant>
<DefaultRoles /> <DefaultRoles />
<Vendor></Vendor> <Vendor/>
<Version></Version> <Version/>
<UpdateCatalogAddress /> <UpdateCatalogAddress />
<IncludeHelpInContents>false</IncludeHelpInContents> <IncludeHelpInContents>false</IncludeHelpInContents>
<UseManagedFormInOrdinaryApplication>false</UseManagedFormInOrdinaryApplication> <UseManagedFormInOrdinaryApplication>false</UseManagedFormInOrdinaryApplication>
@@ -48,8 +48,8 @@
</UsePurposes> </UsePurposes>
<ScriptVariant>Russian</ScriptVariant> <ScriptVariant>Russian</ScriptVariant>
<DefaultRoles /> <DefaultRoles />
<Vendor></Vendor> <Vendor/>
<Version></Version> <Version/>
<UpdateCatalogAddress /> <UpdateCatalogAddress />
<IncludeHelpInContents>false</IncludeHelpInContents> <IncludeHelpInContents>false</IncludeHelpInContents>
<UseManagedFormInOrdinaryApplication>false</UseManagedFormInOrdinaryApplication> <UseManagedFormInOrdinaryApplication>false</UseManagedFormInOrdinaryApplication>
@@ -97,7 +97,7 @@
</v8:item> </v8:item>
</Synonym> </Synonym>
<Comment/> <Comment/>
<Group></Group> <Group/>
<CommandParameterType> <CommandParameterType>
<v8:Type xmlns:d5p1="http://v8.1c.ru/8.1/data/enterprise/current-config">d5p1:CatalogRef.ТестКоманд</v8:Type> <v8:Type xmlns:d5p1="http://v8.1c.ru/8.1/data/enterprise/current-config">d5p1:CatalogRef.ТестКоманд</v8:Type>
</CommandParameterType> </CommandParameterType>
+24 -3
View File
@@ -6,14 +6,35 @@
{ {
"name": "Ячейка", "name": "Ячейка",
"rows": [ "rows": [
{ "cells": [{ "col": 1, "text": "Значение" }] } {
"cells": [
{
"col": 1,
"text": "Значение"
}
]
}
] ]
} }
] ]
}, },
"params": { "outputPath": "Template.xml" }, "params": {
"outputPath": "Template.xml"
},
"validatePath": "Template.xml", "validatePath": "Template.xml",
"expect": { "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
}
} }
} }
@@ -24,6 +24,16 @@
"files": [ "files": [
"Roles/Кладовщик.xml", "Roles/Кладовщик.xml",
"Roles/Кладовщик/Ext/Rights.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
}
} }
} }
+24 -6
View File
@@ -1,14 +1,32 @@
{ {
"name": "Минимальная СКД — один набор, один запрос", "name": "Минимальная СКД — один набор, один запрос",
"params": { "outputPath": "Template.xml" }, "params": {
"outputPath": "Template.xml"
},
"input": { "input": {
"dataSets": [{ "dataSets": [
"query": "ВЫБРАТЬ Номенклатура.Наименование КАК Наименование ИЗ Справочник.Номенклатура КАК Номенклатура", {
"fields": ["Наименование"] "query": "ВЫБРАТЬ Номенклатура.Наименование КАК Наименование ИЗ Справочник.Номенклатура КАК Номенклатура",
}] "fields": [
"Наименование"
]
}
]
}, },
"validatePath": "Template.xml", "validatePath": "Template.xml",
"expect": { "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
}
} }
} }
@@ -16,8 +16,8 @@
<dataSource>ИсточникДанных1</dataSource> <dataSource>ИсточникДанных1</dataSource>
<query>SELECT 1</query> <query>SELECT 1</query>
<field xsi:type="DataSetFieldField"> <field xsi:type="DataSetFieldField">
<dataPath></dataPath> <dataPath/>
<field></field> <field/>
</field> </field>
</dataSet> </dataSet>
<settingsVariant> <settingsVariant>
@@ -3,17 +3,37 @@
"preRun": [ "preRun": [
{ {
"script": "meta-compile/scripts/meta-compile", "script": "meta-compile/scripts/meta-compile",
"input": { "type": "Catalog", "name": "Товары" }, "input": {
"args": { "-JsonPath": "{inputFile}", "-OutputDir": "{workDir}" } "type": "Catalog",
"name": "Товары"
},
"args": {
"-JsonPath": "{inputFile}",
"-OutputDir": "{workDir}"
}
} }
], ],
"input": { "input": {
"name": "Склад", "name": "Склад",
"synonym": "Склад", "synonym": "Склад",
"content": ["Catalogs.Товары"] "content": [
"Catalogs.Товары"
]
}, },
"validatePath": "Subsystems/Склад", "validatePath": "Subsystems/Склад",
"expect": { "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
}
} }
} }
@@ -48,8 +48,8 @@
</UsePurposes> </UsePurposes>
<ScriptVariant>Russian</ScriptVariant> <ScriptVariant>Russian</ScriptVariant>
<DefaultRoles /> <DefaultRoles />
<Vendor></Vendor> <Vendor/>
<Version></Version> <Version/>
<UpdateCatalogAddress /> <UpdateCatalogAddress />
<IncludeHelpInContents>false</IncludeHelpInContents> <IncludeHelpInContents>false</IncludeHelpInContents>
<UseManagedFormInOrdinaryApplication>false</UseManagedFormInOrdinaryApplication> <UseManagedFormInOrdinaryApplication>false</UseManagedFormInOrdinaryApplication>
@@ -48,8 +48,8 @@
</UsePurposes> </UsePurposes>
<ScriptVariant>Russian</ScriptVariant> <ScriptVariant>Russian</ScriptVariant>
<DefaultRoles /> <DefaultRoles />
<Vendor></Vendor> <Vendor/>
<Version></Version> <Version/>
<UpdateCatalogAddress /> <UpdateCatalogAddress />
<IncludeHelpInContents>false</IncludeHelpInContents> <IncludeHelpInContents>false</IncludeHelpInContents>
<UseManagedFormInOrdinaryApplication>false</UseManagedFormInOrdinaryApplication> <UseManagedFormInOrdinaryApplication>false</UseManagedFormInOrdinaryApplication>
+20 -3
View File
@@ -1,9 +1,26 @@
{ {
"name": "минимальный пакет: один objectType, два свойства", "name": "минимальный пакет: один objectType, два свойства",
"caseFiles": ["minimal.xsd"], "caseFiles": [
"params": { "xsdFile": "minimal.xsd" }, "minimal.xsd"
],
"params": {
"xsdFile": "minimal.xsd"
},
"validatePath": "XDTOPackages/minimal", "validatePath": "XDTOPackages/minimal",
"expect": { "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
}
} }
} }
+36 -13
View File
@@ -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; 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 = {}) { function normalizeXmlContent(text, opts = {}) {
let s = text; let s = text;
// 1. XML declaration: normalize quotes and encoding case // 1. XML declaration: normalize quotes and encoding case
@@ -317,14 +325,9 @@ function normalizeXmlContent(text, opts = {}) {
if (!opts.keepXmlns) { if (!opts.keepXmlns) {
s = s.replace(/\s+xmlns(?::[\w]+)?="[^"]*"/g, ''); s = s.replace(/\s+xmlns(?::[\w]+)?="[^"]*"/g, '');
} }
// 4. Normalize self-closing tags: remove space before /> // 3. Collapse whitespace between tags: "> \n\t <" → "><". Kept: the ports indent a
s = s.replace(/\s*\/>/g, '/>'); // few blocks differently, which is formatting rather than the byte canon.
// 5. Collapse whitespace between tags: "> \n\t <" → "><"
s = s.replace(/>\s+</g, '><'); 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; return s;
} }
@@ -356,10 +359,11 @@ function normalizeContent(text, config, relFile) {
return s; return s;
} }
// ─── Byte-style preservation check (round-trip #44/#46/#47) ───────────────── // ─── Byte-style preservation check (round-trip #44/#46/#47, канон #57) ──────
// Проверяет СЫРЫЕ байты файла (в обход normalizeContent): BOM / EOL / регистр // Проверяет СЫРЫЕ байты файла (в обход normalizeContent): BOM / EOL / регистр
// encoding / финальный перенос / отсутствие &#13;. spec: { file, bom, eol:"crlf"|"lf", // encoding / финальный перенос / отсутствие &#13; / форма пустого элемента.
// encoding, finalNewline, noCR13 }. Возвращает массив ошибок. // spec: { file, bom, eol:"crlf"|"lf", encoding, finalNewline, noCR13,
// selfClose:"tight", noEmptyPairs }. Возвращает массив ошибок.
function checkPreserves(workDir, spec) { function checkPreserves(workDir, spec) {
const errs = []; const errs = [];
const target = join(workDir, spec.file); const target = join(workDir, spec.file);
@@ -371,9 +375,18 @@ function checkPreserves(workDir, spec) {
if (spec.bom !== undefined && hasBom !== spec.bom) if (spec.bom !== undefined && hasBom !== spec.bom)
errs.push(`preserves: BOM expected ${spec.bom}, got ${hasBom}`); errs.push(`preserves: BOM expected ${spec.bom}, got ${hasBom}`);
if (spec.eol) { if (spec.eol) {
const hasCR = body.includes(0x0d); // Считаем ОДИНОЧНЫЕ LF, а не «есть ли хоть один CR»: прежняя проверка пропускала
const wantCR = spec.eol === 'crlf'; // смешанный выход (cfe-init давал 10 CR на 70 строк и проходил её) — то есть
if (hasCR !== wantCR) errs.push(`preserves: EOL expected ${spec.eol} (CR=${wantCR}), got CR=${hasCR}`); // главный дефект #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) { if (spec.encoding) {
const m = /encoding="([^"]+)"/.exec(text); 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 (endsNL !== spec.finalNewline) errs.push(`preserves: finalNewline expected ${spec.finalNewline}, got ${endsNL}`);
} }
if (spec.noCR13 && text.includes('&#13;')) errs.push(`preserves: unexpected &#13; literal in output`); if (spec.noCR13 && text.includes('&#13;')) errs.push(`preserves: unexpected &#13; 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; return errs;
} }