diff --git a/.claude/skills/skd-validate/SKILL.md b/.claude/skills/skd-validate/SKILL.md index f302a802a..902fb49fd 100644 --- a/.claude/skills/skd-validate/SKILL.md +++ b/.claude/skills/skd-validate/SKILL.md @@ -12,6 +12,8 @@ allowed-tools: Проверяет структурную корректность Template.xml схемы компоновки данных. Выявляет ошибки формата, битые ссылки, дубликаты имён. +Если в проекте есть XSD-схемы платформы (`/v8-xsd-fetch`), дополнительно проверяет порядок элементов, типы и значения по схеме версии формата выгрузки. + ## Параметры | Параметр | Обяз. | Умолч. | Описание | diff --git a/.claude/skills/skd-validate/scripts/skd-validate.ps1 b/.claude/skills/skd-validate/scripts/skd-validate.ps1 index d0390953c..4b6235a4f 100644 --- a/.claude/skills/skd-validate/scripts/skd-validate.ps1 +++ b/.claude/skills/skd-validate/scripts/skd-validate.ps1 @@ -1,4 +1,4 @@ -# skd-validate v1.4 — Validate 1C DCS structure +# skd-validate v1.5 — Validate 1C DCS structure # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills [CmdletBinding(PositionalBinding=$false)] param( @@ -926,6 +926,222 @@ if ($vChecked -gt 0 -and $vOk) { if ($script:stopped) { & $finalize; exit 1 } +# --- 18. XSD (schemas of the platform, /v8-xsd-fetch) --- +# Порядок элементов, типы и значения — по XSD платформы, если они есть в проекте. Нет схем — +# нет проверки. Схемы XDTO совпадают с выгрузкой конфигуратора только в СКД, и то кроме +# нескольких мест, которые платформа пишет иначе, — они отсекаются ниже (Test-XsdPlatformNoise). + +# Копия общего эталона (семья find_v8project, авторитет — cf-edit). +function Find-V8Project([string]$startDir) { + $d = $startDir + for ($i = 0; $i -lt 20 -and $d; $i++) { + $pj = Join-Path $d ".v8-project.json" + if (Test-Path $pj) { return $pj } + $parent = [System.IO.Path]::GetDirectoryName($d) + if ($parent -eq $d) { break } + $d = $parent + } + return $null +} + +# Штамп версии формата — атрибут version КОРНЕВОГО элемента файла. Именно корневого: в Form.xml +# расширения ниже стоит . Читается только заголовок, без разбора файла. +function Get-RootVersion([string]$xmlPath) { + if (-not (Test-Path -LiteralPath $xmlPath -PathType Leaf)) { return $null } + $buf = New-Object byte[] 4096 + $fs = [System.IO.File]::OpenRead($xmlPath) + try { $len = $fs.Read($buf, 0, $buf.Length) } finally { $fs.Dispose() } + $head = [System.Text.Encoding]::UTF8.GetString($buf, 0, $len) + $m = [regex]::Match($head, '<[A-Za-z_][\w.:-]*(\s[^>]*)?/?>') + if (-not $m.Success) { return $null } + $v = [regex]::Match($m.Groups[1].Value, '(?:^|\s)version="([^"]*)"') + if ($v.Success) { return $v.Groups[1].Value } + return $null +} + +# Корень автономной внешней обработки/отчёта. Копия общего эталона (семья is_external_root, +# авторитет — cf-edit). +function Test-ExternalObjectRoot([string]$xmlPath) { + if (-not (Test-Path $xmlPath)) { return $false } + try { + [xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8 + $el = $mx.DocumentElement.FirstChild + while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling } + if ($el) { return @('ExternalDataProcessor','ExternalReport') -contains $el.LocalName } + } catch {} + return $false +} + +# Якорь выгрузки, в чьём дереве лежит файл: корень автономной EPF/ERF либо Configuration.xml, +# ближайший вверх. Корень обработки проверяется первым — иначе обработка, лежащая в дереве +# конфигурации, сверялась бы с конфигурацией. Нет якоря — сверять не с чем. +function Find-DumpAnchor([string]$startDir) { + $d = $startDir + for ($i = 0; $i -lt 15 -and $d; $i++) { + if (Test-ExternalObjectRoot "$d.xml") { return "$d.xml" } + $cfg = Join-Path $d "Configuration.xml" + if (Test-Path $cfg) { return $cfg } + $parent = [System.IO.Path]::GetDirectoryName($d) + if (-not $parent -or $parent -eq $d) { break } + $d = $parent + } + return $null +} + +function Get-FormatRank([string]$ver) { + if ($ver -match '^(\d+)\.(\d+)$') { return [int]$Matches[1] * 100 + [int]$Matches[2] } + return 0 +} + +$xsdDcsNs = "http://v8.1c.ru/8.1/data-composition-system/schema" + +# targetNamespace схемы — из заголовка файла, без разбора +function Get-XsdTargetNs([string]$path) { + $buf = New-Object byte[] 4096 + $fs = [System.IO.File]::OpenRead($path) + try { $len = $fs.Read($buf, 0, $buf.Length) } finally { $fs.Dispose() } + $m = [regex]::Match([System.Text.Encoding]::UTF8.GetString($buf, 0, $len), 'targetNamespace="([^"]*)"') + if ($m.Success) { return $m.Groups[1].Value } + return $null +} + +# Каталог версии годится, если в нём есть схема СКД (в 2.10–2.11 её нет) +function Test-XsdVersionDir([string]$dir) { + foreach ($f in Get-ChildItem -LiteralPath $dir -Filter *.xsd -File -ErrorAction SilentlyContinue) { + if ((Get-XsdTargetNs $f.FullName) -ceq $xsdDcsNs) { return $true } + } + return $false +} + +# Каталог схем: .v8-project.json — сначала от шаблона (проект, которому он принадлежит), потом +# от текущего каталога; xsdPath из него, иначе .v8-xsd рядом с ним. Нет проекта — нет схем. +function Resolve-XsdRoot([string]$templateDir) { + $pj = Find-V8Project $templateDir + if (-not $pj) { $pj = Find-V8Project (Get-Location).Path } + if (-not $pj) { return $null } + $pjDir = [System.IO.Path]::GetDirectoryName($pj) + $rel = ".v8-xsd" + try { + $cfg = Get-Content -LiteralPath $pj -Raw -Encoding UTF8 | ConvertFrom-Json + if ($cfg.xsdPath) { $rel = "$($cfg.xsdPath)" } + } catch {} + $dir = if ([System.IO.Path]::IsPathRooted($rel)) { $rel } else { Join-Path $pjDir $rel } + if (Test-Path -LiteralPath $dir -PathType Container) { return $dir } + return $null +} + +# Места, которые платформа пишет не так, как описывает XSD, — сверено на всех схемах компоновки +# выгрузок БП и УНФ. Правило — по месту (элемент, родитель, xsi:type), не по тексту: текст +# зависит от движка и языка. +function Test-XsdPlatformNoise($info) { + $e = $info.elem; $p = $info.parent + if (($p -ceq 'groupTemplate' -or $p -ceq 'groupHeaderTemplate') -and ($e -ceq 'templateType' -or $e -ceq 'groupName')) { return $true } + if ($p -ceq 'right' -and $e -ceq 'lastId') { return $true } + if ($p -ceq 'nestedSchema' -and $e -ceq 'schema') { return $true } + if ($info.xsiNs -ceq 'http://v8.1c.ru/8.2/data/chart') { return $true } + # Пустое выражение параметра макета платформа не пишет, схема требует + if ($e -ceq 'parameter' -and $info.xsiLocal -ceq 'ExpressionAreaTemplateParameter') { return $true } + return $false +} + +function Invoke-XsdCheck { + $xsdRoot = Resolve-XsdRoot ([System.IO.Path]::GetDirectoryName($resolvedPath)) + if (-not $xsdRoot) { Report-OK "XSD: no schemas in project — not checked"; return } + $anchor = Find-DumpAnchor ([System.IO.Path]::GetDirectoryName($resolvedPath)) + $ver = if ($anchor) { Get-RootVersion $anchor } else { $null } + if (-not $ver -or (Get-FormatRank $ver) -eq 0) { Report-OK "XSD: format version unknown (template outside a dump) — not checked"; return } + + # Точная версия — ошибки; ближайшая более новая — предупреждения; только старше — пропуск + $cands = @(Get-ChildItem -LiteralPath $xsdRoot -Directory | + Where-Object { $_.Name -match '^\d+\.\d+$' -and (Get-FormatRank $_.Name) -ge (Get-FormatRank $ver) } | + Sort-Object { Get-FormatRank $_.Name } | + Where-Object { Test-XsdVersionDir $_.FullName }) + if ($cands.Count -eq 0) { Report-Warn "XSD: no schemas for format $ver in '$xsdRoot' — not checked"; return } + $useDir = $cands[0].FullName + $useVer = $cands[0].Name + $exact = ($useVer -eq $ver) + + # Набор схем. У import нет schemaLocation — связь по namespace, поэтому в набор кладём все. + # Корень DataCompositionSchema в схеме объявлен как dataCompositionSchema (строчная) — + # дописываем элемент в саму схему СКД: вторую схему того же namespace без SourceUri + # XmlSchemaSet молча отбрасывает. + $schemas = @() + foreach ($f in Get-ChildItem -LiteralPath $useDir -Filter *.xsd -File) { + $fs = [System.IO.File]::OpenRead($f.FullName) + try { $schemas += [System.Xml.Schema.XmlSchema]::Read($fs, $null) } finally { $fs.Dispose() } + } + $dcs = $schemas | Where-Object { $_.TargetNamespace -ceq $xsdDcsNs } | Select-Object -First 1 + $hasRoot = $false + foreach ($it in $dcs.Items) { + if ($it -is [System.Xml.Schema.XmlSchemaElement] -and $it.Name -ceq 'DataCompositionSchema') { $hasRoot = $true } + } + if (-not $hasRoot) { + $el = New-Object System.Xml.Schema.XmlSchemaElement + $el.Name = 'DataCompositionSchema' + $el.SchemaTypeName = New-Object System.Xml.XmlQualifiedName('DataCompositionSchema', $xsdDcsNs) + [void]$dcs.Items.Add($el) + } + $set = New-Object System.Xml.Schema.XmlSchemaSet + $set.XmlResolver = $null + foreach ($sc in $schemas) { [void]$set.Add($sc) } + try { $set.Compile() } catch { Report-Warn "XSD: schemas in '$useDir' do not compile — not checked: $($_.Exception.Message)"; return } + + # Своя цепочка открытых элементов: в обработчике нужен родитель нарушителя + $script:xsdStack = New-Object System.Collections.ArrayList + $script:xsdFound = 0 + $script:xsdTag = if ($exact) { "XSD $useVer" } else { "XSD $useVer (no schemas for $ver)" } + $script:xsdExact = $exact + $handler = { + param($sender, $e) + if ($script:stopped) { return } + $st = $script:xsdStack + if ($sender.NodeType -eq 'Element') { + $xt = $sender.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance") + $info = @{ elem = $sender.LocalName; parent = $(if ($st.Count -gt 0) { $st[$st.Count - 1].name } else { '' }); xsiNs = ''; xsiLocal = '' } + if ($xt) { + $pp = $xt.Split(':') + $info.xsiLocal = $pp[-1] + if ($pp.Count -gt 1) { $info.xsiNs = "$($sender.LookupNamespace($pp[0]))" } + } + } else { + $top = if ($st.Count -gt 0) { $st[$st.Count - 1] } else { @{ name = ''; xsiNs = ''; xsiLocal = '' } } + $info = @{ elem = $top.name; parent = $(if ($st.Count -gt 1) { $st[$st.Count - 2].name } else { '' }); xsiNs = $top.xsiNs; xsiLocal = $top.xsiLocal } + } + if (Test-XsdPlatformNoise $info) { return } + $script:xsdFound++ + $msg = "$($script:xsdTag), line $($e.Exception.LineNumber): $($info.parent)/$($info.elem): $($e.Message -replace '[\r\n]+', ' ')" + if ($script:xsdExact) { Report-Error $msg } else { Report-Warn $msg } + } + $rs = New-Object System.Xml.XmlReaderSettings + $rs.ValidationType = 'Schema' + $rs.Schemas = $set + $rs.DtdProcessing = 'Prohibit' + $rs.add_ValidationEventHandler($handler) + $r = [System.Xml.XmlReader]::Create($resolvedPath, $rs) + try { + while ($r.Read()) { + if ($r.NodeType -eq 'Element' -and -not $r.IsEmptyElement) { + $xt = $r.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance") + $entry = @{ name = $r.LocalName; xsiNs = ''; xsiLocal = '' } + if ($xt) { + $pp = $xt.Split(':') + $entry.xsiLocal = $pp[-1] + if ($pp.Count -gt 1) { $entry.xsiNs = "$($r.LookupNamespace($pp[0]))" } + } + [void]$script:xsdStack.Add($entry) + } elseif ($r.NodeType -eq 'EndElement' -and $script:xsdStack.Count -gt 0) { + $script:xsdStack.RemoveAt($script:xsdStack.Count - 1) + } + if ($script:stopped) { break } + } + } finally { $r.Dispose() } + if ($script:xsdFound -eq 0) { Report-OK "$($script:xsdTag): order, types and values OK" } +} + +Invoke-XsdCheck + +if ($script:stopped) { & $finalize; exit 1 } + # --- Final output --- & $finalize diff --git a/.claude/skills/skd-validate/scripts/skd-validate.py b/.claude/skills/skd-validate/scripts/skd-validate.py index 0c3270da0..ce0ee9f6b 100644 --- a/.claude/skills/skd-validate/scripts/skd-validate.py +++ b/.claude/skills/skd-validate/scripts/skd-validate.py @@ -1,4 +1,4 @@ -# skd-validate v1.4 — Validate 1C DCS structure (Python port) +# skd-validate v1.5 — Validate 1C DCS structure (Python port) # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse import os @@ -881,6 +881,290 @@ for vn in value_nodes: if v_checked > 0 and v_ok: report_ok(f"{v_checked} element(s) with xsi:type: content OK") +if stopped: + finalize() + sys.exit(1) + +# ── 18. XSD (schemas of the platform, /v8-xsd-fetch) ────────── +# Порядок элементов, типы и значения — по XSD платформы, если они есть в проекте. Нет схем — +# нет проверки. Схемы XDTO совпадают с выгрузкой конфигуратора только в СКД, и то кроме +# нескольких мест, которые платформа пишет иначе, — они отсекаются ниже (xsd_platform_noise). +import json +import re +import shutil +import tempfile +from pathlib import Path + + +def _sg_find_v8project(start_dir): + d = start_dir + for _ in range(20): + if not d: + break + pj = os.path.join(d, ".v8-project.json") + if os.path.isfile(pj): + return pj + parent = os.path.dirname(d) + if parent == d: + break + d = parent + return None + + +# Штамп версии формата — атрибут version КОРНЕВОГО элемента файла. Именно корневого: в Form.xml +# расширения ниже стоит . Читается только заголовок, без разбора файла. +def root_version(xml_path): + if not os.path.isfile(xml_path): + return None + with open(xml_path, "rb") as f: + head = f.read(4096).decode("utf-8", errors="ignore") + m = re.search(r'<[A-Za-z_][\w.:-]*(\s[^>]*)?/?>', head) + if not m: + return None + v = re.search(r'(?:^|\s)version="([^"]*)"', m.group(1) or "") + if v: + return v.group(1) + return None + + +# Корень автономной внешней обработки/отчёта. Копия общего эталона (семья is_external_root, +# авторитет — cf-edit). +def _sg_is_external_root(xml_path): + if not os.path.isfile(xml_path): + return False + try: + mx = etree.parse(xml_path).getroot() + for child in mx: + if isinstance(child.tag, str): + return child.tag.split("}")[-1] in ("ExternalDataProcessor", "ExternalReport") + except Exception: + return False + return False + + +# Якорь выгрузки, в чьём дереве лежит файл: корень автономной EPF/ERF либо Configuration.xml, +# ближайший вверх. Корень обработки проверяется первым — иначе обработка, лежащая в дереве +# конфигурации, сверялась бы с конфигурацией. Нет якоря — сверять не с чем. +def find_dump_anchor(start_dir): + d = start_dir + for _ in range(15): + if not d: + break + if _sg_is_external_root(d + ".xml"): + return d + ".xml" + cfg = os.path.join(d, "Configuration.xml") + if os.path.exists(cfg): + return cfg + parent = os.path.dirname(d) + if not parent or parent == d: + break + d = parent + return None + + +def format_rank(ver): + """"2.20" → 220, "2.9" → 209. Строковое сравнение неверно ("2.9" > "2.17").""" + m = re.match(r'^(\d+)\.(\d+)$', ver or '') + return int(m.group(1)) * 100 + int(m.group(2)) if m else 0 + + +XSD_DCS_NS = "http://v8.1c.ru/8.1/data-composition-system/schema" +XS_NS = "http://www.w3.org/2001/XMLSchema" + + +def xsd_target_ns(path): + """targetNamespace схемы — из заголовка файла, без разбора.""" + with open(path, "rb") as f: + head = f.read(4096).decode("utf-8", errors="ignore") + m = re.search(r'targetNamespace="([^"]*)"', head) + return m.group(1) if m else None + + +def xsd_index(vdir): + """namespace → файл схемы в каталоге версии.""" + idx = {} + for name in sorted(os.listdir(vdir)): + fp = os.path.join(vdir, name) + if name.lower().endswith(".xsd") and os.path.isfile(fp): + ns = xsd_target_ns(fp) + if ns and ns not in idx: + idx[ns] = fp + return idx + + +def resolve_xsd_root(template_dir): + """Каталог схем: .v8-project.json — сначала от шаблона (проект, которому он принадлежит), + потом от текущего каталога; xsdPath из него, иначе .v8-xsd рядом с ним. Нет проекта — нет схем.""" + pj = _sg_find_v8project(template_dir) or _sg_find_v8project(os.getcwd()) + if not pj: + return None + pj_dir = os.path.dirname(pj) + rel = ".v8-xsd" + try: + with open(pj, encoding="utf-8-sig") as f: + cfg = json.load(f) + if isinstance(cfg, dict) and cfg.get("xsdPath"): + rel = str(cfg["xsdPath"]) + except Exception: + pass + d = rel if os.path.isabs(rel) else os.path.join(pj_dir, rel) + return d if os.path.isdir(d) else None + + +def xsd_platform_noise(info): + """Места, которые платформа пишет не так, как описывает XSD, — сверено на всех схемах + компоновки выгрузок БП и УНФ. Правило — по месту (элемент, родитель, xsi:type), не по тексту: + текст зависит от движка и языка.""" + e, p = info["elem"], info["parent"] + if p in ("groupTemplate", "groupHeaderTemplate") and e in ("templateType", "groupName"): + return True + if p == "right" and e == "lastId": + return True + if p == "nestedSchema" and e == "schema": + return True + if info["xsiNs"] == "http://v8.1c.ru/8.2/data/chart": + return True + # Пустое выражение параметра макета платформа не пишет, схема требует + if e == "parameter" and info["xsiLocal"] == "ExpressionAreaTemplateParameter": + return True + return False + + +def _xsd_build_schema(vdir, tmp): + """Схема для lxml. У import в схемах платформы нет schemaLocation, а libxml2 сам по namespace + не ищет, — корневая схема импортирует замыкание зависимостей СКД с путями, листья первыми. + Корень DataCompositionSchema в схеме объявлен как dataCompositionSchema (строчная) — схему + СКД подключаем через обёртку: xs:include настоящего файла плюс элемент с нужным именем.""" + idx = xsd_index(vdir) + order, seen = [], set() + + def visit(ns): + if ns in seen or ns not in idx: + return + seen.add(ns) + doc = etree.parse(idx[ns]) + for imp in doc.getroot().iter("{%s}import" % XS_NS): + visit(imp.get("namespace")) + order.append(ns) + + # Сначала замыкание СКД, затем остальные схемы каталога: значения в выгрузке ссылаются через + # xsi:type на типы, которые схема СКД не импортирует (data/enterprise: LinkedValueChangeMode, + # FoldersAndItemsUse …) — как и в PS-мастере, где в набор кладутся все схемы. + visit(XSD_DCS_NS) + for ns in idx: + visit(ns) + dcs_doc = etree.parse(idx[XSD_DCS_NS]) + has_root = any(el.get("name") == "DataCompositionSchema" + for el in dcs_doc.getroot().findall("{%s}element" % XS_NS)) + locations = {ns: Path(idx[ns]).as_uri() for ns in order} + if not has_root: + wrapper = os.path.join(tmp, "dcs-root.xsd") + with open(wrapper, "w", encoding="utf-8") as f: + f.write('\n' + '\n' + ' \n' + ' \n' + '\n' % (XS_NS, XSD_DCS_NS, XSD_DCS_NS, locations[XSD_DCS_NS])) + locations[XSD_DCS_NS] = Path(wrapper).as_uri() + root = os.path.join(tmp, "root.xsd") + with open(root, "w", encoding="utf-8") as f: + f.write('\n\n' % XS_NS) + for ns in order: + f.write(' \n' % (ns, locations[ns])) + f.write('\n') + return etree.XMLSchema(etree.parse(root)) + + +def _xsd_elem_info(el): + if el is None or not isinstance(el.tag, str): + return {"elem": "", "parent": "", "xsiNs": "", "xsiLocal": ""} + par = el.getparent() + info = {"elem": etree.QName(el).localname, + "parent": etree.QName(par).localname if par is not None and isinstance(par.tag, str) else "", + "xsiNs": "", "xsiLocal": ""} + xt = el.get(XSI_TYPE) + if xt: + pp = xt.split(":") + info["xsiLocal"] = pp[-1] + if len(pp) > 1: + info["xsiNs"] = el.nsmap.get(pp[0]) or "" + return info + + +def _xsd_error_element(err): + """Элемент нарушения по error.path. Путь несёт префиксы документа (dcsset:settings) — xpath + получает их из корня; префикс, объявленный глубже, не разрешится — тогда ищем по строке и + имени последнего шага.""" + if not err.path: + return None + ns = {k: v for k, v in tree.getroot().nsmap.items() if k} + try: + hits = tree.xpath(err.path, namespaces=ns) + if hits and isinstance(hits[0], etree._Element): + return hits[0] + except Exception: + pass + last = re.sub(r'\[\d+\]$', '', err.path.rsplit('/', 1)[-1]).split(':')[-1] + for el in tree.iter(): + if isinstance(el.tag, str) and el.sourceline == err.line and etree.QName(el).localname == last: + return el + return None + + +def xsd_check(): + xsd_root = resolve_xsd_root(os.path.dirname(resolved_path)) + if not xsd_root: + report_ok("XSD: no schemas in project — not checked") + return + anchor = find_dump_anchor(os.path.dirname(resolved_path)) + ver = root_version(anchor) if anchor else None + if not ver or format_rank(ver) == 0: + report_ok("XSD: format version unknown (template outside a dump) — not checked") + return + + # Точная версия — ошибки; ближайшая более новая — предупреждения; только старше — пропуск + cands = sorted((d for d in os.listdir(xsd_root) + if re.match(r'^\d+\.\d+$', d) and os.path.isdir(os.path.join(xsd_root, d)) + and format_rank(d) >= format_rank(ver)), key=format_rank) + cands = [d for d in cands if XSD_DCS_NS in xsd_index(os.path.join(xsd_root, d))] + if not cands: + report_warn(f"XSD: no schemas for format {ver} in '{xsd_root}' — not checked") + return + use_ver = cands[0] + exact = use_ver == ver + + tmp = tempfile.mkdtemp(prefix="skd-xsd-") + try: + try: + schema = _xsd_build_schema(os.path.join(xsd_root, use_ver), tmp) + except Exception as ex: + report_warn(f"XSD: schemas in '{os.path.join(xsd_root, use_ver)}' do not compile — not checked: {ex}") + return + finally: + shutil.rmtree(tmp, ignore_errors=True) + + tag = f"XSD {use_ver}" if exact else f"XSD {use_ver} (no schemas for {ver})" + schema.validate(tree) + found = 0 + for err in schema.error_log: + if stopped: + break + el = _xsd_error_element(err) + info = _xsd_elem_info(el) + if xsd_platform_noise(info): + continue + found += 1 + msg = f"{tag}, line {err.line}: {info['parent']}/{info['elem']}: {' '.join(err.message.split())}" + if exact: + report_error(msg) + else: + report_warn(msg) + if found == 0: + report_ok(f"{tag}: order, types and values OK") + + +xsd_check() + if stopped: finalize() sys.exit(1) diff --git a/docs/v8-project-guide.md b/docs/v8-project-guide.md index d7d3dcb04..23b8bb7de 100644 --- a/docs/v8-project-guide.md +++ b/docs/v8-project-guide.md @@ -268,6 +268,10 @@ [yellow-hammer/namespace-forest](https://github.com/yellow-hammer/namespace-forest). Схемы — собственность ООО «1С-Софт», в Git их не добавляют. Нет схем — нет проверки. +Схемы читает `/skd-validate`: порядок элементов, типы и значения схемы компоновки — по схемам +версии формата выгрузки (атрибут `version` в `Configuration.xml`). Для объектов метаданных и форм +схемы XDTO не совпадают с форматом выгрузки конфигуратора, поэтому другие валидаторы их не используют. + ### `webUrl` — URL веб-клиента (per-database) URL для открытия базы в браузере через `/web-test`. Задаётся в записи конкретной базы. diff --git a/tests/skills/README.md b/tests/skills/README.md index 147b9c380..253c9a355 100644 --- a/tests/skills/README.md +++ b/tests/skills/README.md @@ -24,7 +24,8 @@ Exit code: 0 = все прошли, 1 = есть падения. ### Сколько кейсов должно быть пропущено `○ skipped` — норма, а не падение: кейс может гейтиться по ОС (`osOnly`), по порту -(`runtimeOnly`) или требовать внешней выгрузки (`setup: external:`), которой на машине нет. +(`runtimeOnly`), требовать внешней выгрузки (`setup: external:`), которой на машине нет, или +локальных данных вне репозитория (`requires`, например XSD-схемы 1С в `.v8-xsd`). Но само число ни о чём не говорит, пока не с чем сверить, а запоминать его нельзя — оно растёт с набором кейсов. @@ -402,6 +403,7 @@ ibcmd-проход автоматически `○ skipped`, если рядом | `noSnapshot` | нет | Непустая строка с причиной — кейс объявляет, что эталон не нужен (см. «Эталоны») | | `idempotent` | нет | `true` — повторный прогон с теми же аргументами должен дать байт-в-байт тот же `workDir` | | `runtimeOnly` | нет | `"powershell"` / `"python"` — кейс имеет смысл только на одном порте, на другом `○ skipped` | +| `requires` | нет | Путь или массив путей от корня репозитория, без которых кейс не имеет смысла (например `".v8-xsd/2.20"` — схемы 1С не входят в репозиторий). Нет пути — `○ skipped`. В `writeFile` путь к корню — плейсхолдер `{repoRoot}` | | `osOnly` | нет | `"win32"` / `"darwin"` / `"linux"` — кейс работает только на этой ОС, на других `○ skipped`. Принимает и массив: `["darwin","linux"]`. Для фейка платформы **не нужен**: см. `fakePlatform` | | `fakePlatform` | нет | `{ "log": "…", "exit": 0 }` — кейсу нужна фейковая платформа (см. ниже) | | `cwd` | нет | `"workDir"` — запустить навык из рабочего каталога (нужно, если кейс кладёт туда `.v8-project.json`) | diff --git a/tests/skills/cases/skd-validate/fixtures/xsd-bad-211/Configuration.xml b/tests/skills/cases/skd-validate/fixtures/xsd-bad-211/Configuration.xml new file mode 100644 index 000000000..b89e78d93 --- /dev/null +++ b/tests/skills/cases/skd-validate/fixtures/xsd-bad-211/Configuration.xml @@ -0,0 +1,4 @@ + + + + diff --git a/tests/skills/cases/skd-validate/fixtures/xsd-bad-211/Reports/Отчет/Templates/Схема/Ext/Template.xml b/tests/skills/cases/skd-validate/fixtures/xsd-bad-211/Reports/Отчет/Templates/Схема/Ext/Template.xml new file mode 100644 index 000000000..fc71280e1 --- /dev/null +++ b/tests/skills/cases/skd-validate/fixtures/xsd-bad-211/Reports/Отчет/Templates/Схема/Ext/Template.xml @@ -0,0 +1,104 @@ + + + + ИсточникДанных1 + Local + + + НаборДанных1 + + Число + Число + + xs:decimal + + 10 + 0 + Any + + + Формат(Число, "ЧГ=") + + + Флаг + Флаг + + xs:boolean + + + ИсточникДанных1 + ВЫБРАТЬ 1 КАК Число, ИСТИНА КАК Флаг + + + Удвоенное + Число * 2 + + xs:decimal + + 10 + 0 + Any + + + + true + + + + П + + xs:boolean + + False + false + + + Основной + + + ru + Основной + + + + + + Число + + + + + Флаг + Equal + true + + + + + Заголовок + + + ru + Пример + + + + + + + П + false + 0930043f-cf2a-42c1-ae8c-4accf2fd2604 + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/skills/cases/skd-validate/fixtures/xsd-bad-220/Configuration.xml b/tests/skills/cases/skd-validate/fixtures/xsd-bad-220/Configuration.xml new file mode 100644 index 000000000..398d6c6e9 --- /dev/null +++ b/tests/skills/cases/skd-validate/fixtures/xsd-bad-220/Configuration.xml @@ -0,0 +1,4 @@ + + + + diff --git a/tests/skills/cases/skd-validate/fixtures/xsd-bad-220/Reports/Отчет/Templates/Схема/Ext/Template.xml b/tests/skills/cases/skd-validate/fixtures/xsd-bad-220/Reports/Отчет/Templates/Схема/Ext/Template.xml new file mode 100644 index 000000000..fc71280e1 --- /dev/null +++ b/tests/skills/cases/skd-validate/fixtures/xsd-bad-220/Reports/Отчет/Templates/Схема/Ext/Template.xml @@ -0,0 +1,104 @@ + + + + ИсточникДанных1 + Local + + + НаборДанных1 + + Число + Число + + xs:decimal + + 10 + 0 + Any + + + Формат(Число, "ЧГ=") + + + Флаг + Флаг + + xs:boolean + + + ИсточникДанных1 + ВЫБРАТЬ 1 КАК Число, ИСТИНА КАК Флаг + + + Удвоенное + Число * 2 + + xs:decimal + + 10 + 0 + Any + + + + true + + + + П + + xs:boolean + + False + false + + + Основной + + + ru + Основной + + + + + + Число + + + + + Флаг + Equal + true + + + + + Заголовок + + + ru + Пример + + + + + + + П + false + 0930043f-cf2a-42c1-ae8c-4accf2fd2604 + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/skills/cases/skd-validate/fixtures/xsd-good-220/Configuration.xml b/tests/skills/cases/skd-validate/fixtures/xsd-good-220/Configuration.xml new file mode 100644 index 000000000..398d6c6e9 --- /dev/null +++ b/tests/skills/cases/skd-validate/fixtures/xsd-good-220/Configuration.xml @@ -0,0 +1,4 @@ + + + + diff --git a/tests/skills/cases/skd-validate/fixtures/xsd-good-220/Reports/Отчет/Templates/Схема/Ext/Template.xml b/tests/skills/cases/skd-validate/fixtures/xsd-good-220/Reports/Отчет/Templates/Схема/Ext/Template.xml new file mode 100644 index 000000000..7ae404c19 --- /dev/null +++ b/tests/skills/cases/skd-validate/fixtures/xsd-good-220/Reports/Отчет/Templates/Схема/Ext/Template.xml @@ -0,0 +1,104 @@ + + + + ИсточникДанных1 + Local + + + НаборДанных1 + + Число + Число + Формат(Число, "ЧГ=") + + xs:decimal + + 10 + 0 + Any + + + + + Флаг + Флаг + + xs:boolean + + + ИсточникДанных1 + ВЫБРАТЬ 1 КАК Число, ИСТИНА КАК Флаг + + + Удвоенное + Число * 2 + + true + + + xs:decimal + + 10 + 0 + Any + + + + + П + + xs:boolean + + false + false + + + Основной + + + ru + Основной + + + + + + Число + + + + + Флаг + Equal + true + + + + + П + false + 0eb70b37-3a01-40a0-81c1-98028ef06a05 + + + + + Заголовок + + + ru + Пример + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/skills/cases/skd-validate/fixtures/xsd-no-anchor/Template.xml b/tests/skills/cases/skd-validate/fixtures/xsd-no-anchor/Template.xml new file mode 100644 index 000000000..fc71280e1 --- /dev/null +++ b/tests/skills/cases/skd-validate/fixtures/xsd-no-anchor/Template.xml @@ -0,0 +1,104 @@ + + + + ИсточникДанных1 + Local + + + НаборДанных1 + + Число + Число + + xs:decimal + + 10 + 0 + Any + + + Формат(Число, "ЧГ=") + + + Флаг + Флаг + + xs:boolean + + + ИсточникДанных1 + ВЫБРАТЬ 1 КАК Число, ИСТИНА КАК Флаг + + + Удвоенное + Число * 2 + + xs:decimal + + 10 + 0 + Any + + + + true + + + + П + + xs:boolean + + False + false + + + Основной + + + ru + Основной + + + + + + Число + + + + + Флаг + Equal + true + + + + + Заголовок + + + ru + Пример + + + + + + + П + false + 0930043f-cf2a-42c1-ae8c-4accf2fd2604 + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/skills/cases/skd-validate/fixtures/xsd-noise-220/Configuration.xml b/tests/skills/cases/skd-validate/fixtures/xsd-noise-220/Configuration.xml new file mode 100644 index 000000000..398d6c6e9 --- /dev/null +++ b/tests/skills/cases/skd-validate/fixtures/xsd-noise-220/Configuration.xml @@ -0,0 +1,4 @@ + + + + diff --git a/tests/skills/cases/skd-validate/fixtures/xsd-noise-220/Reports/Отчет/Templates/Схема/Ext/Template.xml b/tests/skills/cases/skd-validate/fixtures/xsd-noise-220/Reports/Отчет/Templates/Схема/Ext/Template.xml new file mode 100644 index 000000000..67d1c9f7d --- /dev/null +++ b/tests/skills/cases/skd-validate/fixtures/xsd-noise-220/Reports/Отчет/Templates/Схема/Ext/Template.xml @@ -0,0 +1,407 @@ + + + + ИсточникДанных1 + Local + + + Тест + + Организация + Организация + + true + + + d5p1:CatalogRef.Организации + + + + Номенклатура + Номенклатура + + true + + + d5p1:CatalogRef.Номенклатура + + + + Сумма + Сумма + + xs:decimal + + 15 + 2 + Any + + + + + Количество + Количество + + xs:decimal + + 15 + 3 + Any + + + + + Статус + Статус + + xs:string + + 0 + Variable + + + + ИсточникДанных1 + ВЫБРАТЬ * ИЗ Справочник.Номенклатура + + + Период + + v8:StandardPeriod + + + LastMonth + + false + true + Always + + + НачалоПериода + + <v8:item> + <v8:lang>ru</v8:lang> + <v8:content>Начало периода</v8:content> + </v8:item> + + + xs:dateTime + + DateTime + + + 0001-01-01T00:00:00 + true + &Период.ДатаНачала + + + КонецПериода + + <v8:item> + <v8:lang>ru</v8:lang> + <v8:content>Конец периода</v8:content> + </v8:item> + + + xs:dateTime + + DateTime + + + 0001-01-01T00:00:00 + true + &Период.ДатаОкончания + + + Активные + + xs:boolean + + true + false + + + + ДанныеОтчета + Header + + + + Основной + + + ru + Основной вариант + + + + + + Организация + + + + + ru + Объёмы + + + + Сумма + + + Количество + + Auto + + + + + + false + Организация + Equal + UUID-001 + + + OrGroup + + Статус + Equal + Активен + + + Сумма + Greater + 1000 + + + + + + Период + + LastMonth + + UUID-002 + + + Активные + true + UUID-003 + + + + + Сумма + Desc + + + + + + + + + Сумма + Greater + 10000 + + + + + ЦветТекста + style:НегативныйТекстЦвет + + + Большие суммы + Normal + UUID-004 + + + + + Заголовок + + + ru + Сводка по организациям + + + + + + + + Организация + Items + None + 0001-01-01T00:00:00 + 0001-01-01T00:00:00 + + + + + + + + + + + + Номенклатура + Items + None + 0001-01-01T00:00:00 + 0001-01-01T00:00:00 + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/skills/cases/skd-validate/xsd-exact-errors.json b/tests/skills/cases/skd-validate/xsd-exact-errors.json new file mode 100644 index 000000000..509421c7f --- /dev/null +++ b/tests/skills/cases/skd-validate/xsd-exact-errors.json @@ -0,0 +1,28 @@ +{ + "name": "XSD точной версии: нарушенный порядок и False — ошибки с местом нарушения", + "setup": "fixture:xsd-bad-220", + "requires": ".v8-xsd/2.20", + "preRun": [ + { + "writeFile": { + "path": ".v8-project.json", + "content": { + "xsdPath": "{repoRoot}/.v8-xsd" + } + } + } + ], + "params": { + "templatePath": "Reports/Отчет/Templates/Схема/Ext/Template.xml" + }, + "expectError": true, + "expect": { + "stdoutContains": [ + "[ERROR] XSD 2.20, line 20: field/presentationExpression", + "[ERROR] XSD 2.20, line 43: calculatedField/useRestriction", + "[ERROR] XSD 2.20, line 52: parameter/value", + "[ERROR] XSD 2.20, line 87: settings/dataParameters" + ] + }, + "noSnapshot": "результат — только вывод; схемы 1С не должны попасть в эталон" +} diff --git a/tests/skills/cases/skd-validate/xsd-exact-ok.json b/tests/skills/cases/skd-validate/xsd-exact-ok.json new file mode 100644 index 000000000..552e48c73 --- /dev/null +++ b/tests/skills/cases/skd-validate/xsd-exact-ok.json @@ -0,0 +1,29 @@ +{ + "name": "XSD точной версии: правильная схема проходит без строк XSD", + "setup": "fixture:xsd-good-220", + "requires": ".v8-xsd/2.20", + "preRun": [ + { + "writeFile": { + "path": ".v8-project.json", + "content": { + "xsdPath": "{repoRoot}/.v8-xsd" + } + } + } + ], + "params": { + "templatePath": "Reports/Отчет/Templates/Схема/Ext/Template.xml" + }, + "args_extra": [ + "-Detailed" + ], + "expect": { + "stdoutContains": "[OK] XSD 2.20: order, types and values OK", + "stdoutNotContains": [ + "[ERROR]", + "[WARN]" + ] + }, + "noSnapshot": "результат — только вывод" +} diff --git a/tests/skills/cases/skd-validate/xsd-newer-warns.json b/tests/skills/cases/skd-validate/xsd-newer-warns.json new file mode 100644 index 000000000..9c96e75b3 --- /dev/null +++ b/tests/skills/cases/skd-validate/xsd-newer-warns.json @@ -0,0 +1,28 @@ +{ + "name": "Схем точной версии нет (2.11 без схемы СКД) — по ближайшей новой, нарушения предупреждениями", + "setup": "fixture:xsd-bad-211", + "requires": [ + ".v8-xsd/2.11", + ".v8-xsd/2.12" + ], + "preRun": [ + { + "writeFile": { + "path": ".v8-project.json", + "content": { + "xsdPath": "{repoRoot}/.v8-xsd" + } + } + } + ], + "params": { + "templatePath": "Reports/Отчет/Templates/Схема/Ext/Template.xml" + }, + "expect": { + "stdoutContains": [ + "[WARN] XSD 2.12 (no schemas for 2.11), line 20: field/presentationExpression" + ], + "stdoutNotContains": "[ERROR]" + }, + "noSnapshot": "результат — только вывод" +} diff --git a/tests/skills/cases/skd-validate/xsd-no-anchor.json b/tests/skills/cases/skd-validate/xsd-no-anchor.json new file mode 100644 index 000000000..a71f28e52 --- /dev/null +++ b/tests/skills/cases/skd-validate/xsd-no-anchor.json @@ -0,0 +1,34 @@ +{ + "name": "Шаблон вне выгрузки — версия формата неизвестна, проверка не выполняется", + "setup": "fixture:xsd-no-anchor", + "preRun": [ + { + "writeFile": { + "path": ".v8-project.json", + "content": { + "xsdPath": "xsd" + } + } + }, + { + "writeFile": { + "path": "xsd/2.20/README.txt", + "content": "каталог версии" + } + } + ], + "params": { + "templatePath": "Template.xml" + }, + "args_extra": [ + "-Detailed" + ], + "expect": { + "stdoutContains": "[OK] XSD: format version unknown (template outside a dump) — not checked", + "stdoutNotContains": [ + "[ERROR]", + "[WARN]" + ] + }, + "noSnapshot": "результат — только вывод" +} diff --git a/tests/skills/cases/skd-validate/xsd-no-schemas-detailed.json b/tests/skills/cases/skd-validate/xsd-no-schemas-detailed.json new file mode 100644 index 000000000..42a412ad9 --- /dev/null +++ b/tests/skills/cases/skd-validate/xsd-no-schemas-detailed.json @@ -0,0 +1,28 @@ +{ + "name": "Каталога схем нет, -Detailed — строка о пропуске", + "setup": "fixture:xsd-bad-220", + "preRun": [ + { + "writeFile": { + "path": ".v8-project.json", + "content": { + "xsdPath": "нет-такого-каталога" + } + } + } + ], + "params": { + "templatePath": "Reports/Отчет/Templates/Схема/Ext/Template.xml" + }, + "args_extra": [ + "-Detailed" + ], + "expect": { + "stdoutContains": "[OK] XSD: no schemas in project — not checked", + "stdoutNotContains": [ + "[ERROR]", + "[WARN]" + ] + }, + "noSnapshot": "результат — только вывод" +} diff --git a/tests/skills/cases/skd-validate/xsd-no-schemas.json b/tests/skills/cases/skd-validate/xsd-no-schemas.json new file mode 100644 index 000000000..f6eb537a9 --- /dev/null +++ b/tests/skills/cases/skd-validate/xsd-no-schemas.json @@ -0,0 +1,22 @@ +{ + "name": "Каталога схем нет — проверки нет, строк XSD в выводе нет", + "setup": "fixture:xsd-bad-220", + "preRun": [ + { + "writeFile": { + "path": ".v8-project.json", + "content": { + "xsdPath": "нет-такого-каталога" + } + } + } + ], + "params": { + "templatePath": "Reports/Отчет/Templates/Схема/Ext/Template.xml" + }, + "expect": { + "stdoutContains": "=== Validation OK", + "stdoutNotContains": "XSD" + }, + "noSnapshot": "результат — только вывод" +} diff --git a/tests/skills/cases/skd-validate/xsd-only-older.json b/tests/skills/cases/skd-validate/xsd-only-older.json new file mode 100644 index 000000000..c4be474bb --- /dev/null +++ b/tests/skills/cases/skd-validate/xsd-only-older.json @@ -0,0 +1,28 @@ +{ + "name": "В каталоге схем только старшие версии — предупреждение, проверка не выполняется", + "setup": "fixture:xsd-bad-220", + "preRun": [ + { + "writeFile": { + "path": ".v8-project.json", + "content": { + "xsdPath": "xsd" + } + } + }, + { + "writeFile": { + "path": "xsd/2.10/README.txt", + "content": "нет схемы СКД" + } + } + ], + "params": { + "templatePath": "Reports/Отчет/Templates/Схема/Ext/Template.xml" + }, + "expect": { + "stdoutContains": "[WARN] XSD: no schemas for format 2.20 in", + "stdoutNotContains": "[ERROR]" + }, + "noSnapshot": "результат — только вывод" +} diff --git a/tests/skills/cases/skd-validate/xsd-platform-noise.json b/tests/skills/cases/skd-validate/xsd-platform-noise.json new file mode 100644 index 000000000..874f155eb --- /dev/null +++ b/tests/skills/cases/skd-validate/xsd-platform-noise.json @@ -0,0 +1,29 @@ +{ + "name": "Места, которые платформа пишет иначе, чем XSD (templateType в groupHeaderTemplate), — не ошибка", + "setup": "fixture:xsd-noise-220", + "requires": ".v8-xsd/2.20", + "preRun": [ + { + "writeFile": { + "path": ".v8-project.json", + "content": { + "xsdPath": "{repoRoot}/.v8-xsd" + } + } + } + ], + "params": { + "templatePath": "Reports/Отчет/Templates/Схема/Ext/Template.xml" + }, + "args_extra": [ + "-Detailed" + ], + "expect": { + "stdoutContains": "[OK] XSD 2.20: order, types and values OK", + "stdoutNotContains": [ + "[ERROR]", + "[WARN]" + ] + }, + "noSnapshot": "результат — только вывод" +} diff --git a/tests/skills/check-inline-drift.mjs b/tests/skills/check-inline-drift.mjs index 3d45d2199..f0f557365 100644 --- a/tests/skills/check-inline-drift.mjs +++ b/tests/skills/check-inline-drift.mjs @@ -81,7 +81,7 @@ const FAMILIES = [ 'form-info', 'meta-info', 'mxl-info', 'role-info', 'skd-info', 'subsystem-info', // Валидаторы отличают автономную обработку от конфигурации: тот же корень — якорь выгрузки // для сверки штампа версии (find_dump_anchor) - 'form-validate', 'interface-validate', 'meta-validate', 'role-validate', 'subsystem-validate'] }, + 'form-validate', 'interface-validate', 'meta-validate', 'role-validate', 'skd-validate', 'subsystem-validate'] }, ], }, { @@ -93,7 +93,7 @@ const FAMILIES = [ { id: 'full', authority: 'cf-edit', consumers: ['cfe-borrow', 'db-cfe-admin', 'db-create', 'db-dump-cf', 'db-dump-dt', 'db-dump-xml', 'db-load-cf', 'db-load-dt', 'db-load-git', 'db-load-xml', 'db-repo', 'db-run', 'db-update', - 'epf-build', 'epf-dump', 'web-publish', 'v8-xsd-fetch', + 'epf-build', 'epf-dump', 'web-publish', 'v8-xsd-fetch', 'skd-validate', 'form-add', 'form-compile', 'form-edit', 'help-add', 'interface-edit', 'meta-compile', 'meta-edit', 'meta-remove', 'mxl-compile', 'role-compile', 'role-edit', 'skd-compile', 'skd-edit', 'subsystem-compile', 'subsystem-edit', 'template-add', 'xdto-compile', 'xdto-edit'] }, @@ -130,13 +130,13 @@ const FAMILIES = [ { name: 'root_version', py: 'root_version', ps1: 'Get-RootVersion', variants: [ - { id: 'base', authority: 'meta-validate', consumers: ['cf-validate', 'cfe-validate', 'epf-validate', 'form-validate', 'interface-validate', 'role-validate', 'subsystem-validate'] }, + { id: 'base', authority: 'meta-validate', consumers: ['cf-validate', 'cfe-validate', 'epf-validate', 'form-validate', 'interface-validate', 'role-validate', 'skd-validate', 'subsystem-validate'] }, ], }, { name: 'find_dump_anchor', py: 'find_dump_anchor', ps1: 'Find-DumpAnchor', variants: [ - { id: 'base', authority: 'meta-validate', consumers: ['form-validate', 'interface-validate', 'role-validate', 'subsystem-validate'] }, + { id: 'base', authority: 'meta-validate', consumers: ['form-validate', 'interface-validate', 'role-validate', 'skd-validate', 'subsystem-validate'] }, ], }, { @@ -151,7 +151,7 @@ const FAMILIES = [ { id: 'base', authority: 'meta-compile', consumers: ['cf-init', 'cf-validate', 'cfe-borrow', 'cfe-init', 'cfe-patch-method', 'cfe-validate', 'epf-build', 'epf-init', 'epf-validate', 'erf-init', 'form-add', 'form-compile', 'form-validate', - 'meta-validate', 'mxl-compile', 'role-compile', 'role-edit', 'subsystem-compile', 'subsystem-edit', + 'meta-validate', 'mxl-compile', 'role-compile', 'role-edit', 'skd-validate', 'subsystem-compile', 'subsystem-edit', 'template-add', 'xdto-compile'] }, ], }, diff --git a/tests/skills/expected-skips.mjs b/tests/skills/expected-skips.mjs index 61eb0d1be..d3058b1b3 100644 --- a/tests/skills/expected-skips.mjs +++ b/tests/skills/expected-skips.mjs @@ -22,7 +22,7 @@ const runtime = argv.includes('--runtime') ? argv[argv.indexOf('--runtime') + 1] const listMode = argv.includes('--list'); const os = process.platform; -const reasons = { external: [], runtimeOnly: [], osOnly: [] }; +const reasons = { external: [], runtimeOnly: [], osOnly: [], requires: [] }; for (const skill of readdirSync(CASES)) { const dir = join(CASES, skill); @@ -38,6 +38,7 @@ for (const skill of readdirSync(CASES)) { // Порядок совпадает с раннером: сначала ОС, потом порт, потом фикстура. if (c.osOnly && ![].concat(c.osOnly).includes(os)) { reasons.osOnly.push(id); continue; } if (c.runtimeOnly && c.runtimeOnly !== runtime) { reasons.runtimeOnly.push(id); continue; } + if (c.requires && ![].concat(c.requires).every(p => existsSync(join(HERE, '..', '..', p)))) { reasons.requires.push(id); continue; } const setup = String(c.setup || skillCfg.setup || ''); if (setup.startsWith('external:') && !existsSync(setup.slice('external:'.length))) { reasons.external.push(id); @@ -45,7 +46,7 @@ for (const skill of readdirSync(CASES)) { } } -const total = reasons.external.length + reasons.runtimeOnly.length + reasons.osOnly.length; +const total = reasons.external.length + reasons.runtimeOnly.length + reasons.osOnly.length + reasons.requires.length; if (listMode) { for (const [key, ids] of Object.entries(reasons)) { @@ -58,6 +59,7 @@ if (listMode) { console.log( `Ожидается пропущенных: ${total} [${os}, runtime: ${runtime}] ` + - `= external ${reasons.external.length} + runtimeOnly ${reasons.runtimeOnly.length} + osOnly ${reasons.osOnly.length}`, + `= external ${reasons.external.length} + runtimeOnly ${reasons.runtimeOnly.length} + osOnly ${reasons.osOnly.length}` + + ` + requires ${reasons.requires.length}`, ); console.log('Сверить с "Skipped: N" из прогона runner.mjs с теми же --runtime.'); diff --git a/tests/skills/runner.mjs b/tests/skills/runner.mjs index 57b40b36c..7effb4557 100644 --- a/tests/skills/runner.mjs +++ b/tests/skills/runner.mjs @@ -282,7 +282,7 @@ function cleanupWorkspace(ws) { // выбор платформы пришлось бы писать двумя копиями под osOnly. function substPlaceholders(s, workDir) { const fakePath = join(workDir, process.platform === 'win32' ? 'fake.cmd' : 'fake.sh'); - return s.replaceAll('{workDir}', workDir).replaceAll('{fakePlatform}', fakePath); + return s.replaceAll('{workDir}', workDir).replaceAll('{fakePlatform}', fakePath).replaceAll('{repoRoot}', REPO_ROOT); } function substDeep(value, workDir) { @@ -835,6 +835,13 @@ async function runCaseAsync(testCase, opts) { return { id: testCase.id, skill: testCase.skillDir, name: testCase.name, passed: true, skipped: true, errors: [], elapsed: '0.0s' }; } + // requires: кейс опирается на локальные данные, которые в репозиторий не входят (XSD-схемы 1С + // в .v8-xsd — их правообладатель 1С). Пути от корня репозитория; нет хотя бы одного — skipped. + // Не external: тот отдаёт каталог на месте только для чтения, а кейсу нужно разложить свои файлы. + if (caseData.requires && ![].concat(caseData.requires).every(p => existsSync(resolve(REPO_ROOT, p)))) { + return { id: testCase.id, skill: testCase.skillDir, name: testCase.name, passed: true, skipped: true, errors: [], elapsed: '0.0s' }; + } + try { const skillCasesDir = join(CASES, testCase.skillDir); const fixturePath = ensureSetup(setupName, opts.runtime, skillCasesDir);