mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-06-12 08:54:57 +03:00
Auto-build: opencode (python) from 6d119eb
This commit is contained in:
@@ -0,0 +1,352 @@
|
||||
# subsystem-validate v1.2 — Validate 1C subsystem XML structure
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[Parameter(Mandatory)][Alias('Path')][string]$SubsystemPath,
|
||||
[switch]$Detailed,
|
||||
[int]$MaxErrors = 30,
|
||||
[string]$OutFile
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
# --- Resolve path ---
|
||||
if (-not [System.IO.Path]::IsPathRooted($SubsystemPath)) {
|
||||
$SubsystemPath = Join-Path (Get-Location).Path $SubsystemPath
|
||||
}
|
||||
if (Test-Path $SubsystemPath -PathType Container) {
|
||||
$dirName = Split-Path $SubsystemPath -Leaf
|
||||
$candidate = Join-Path $SubsystemPath "$dirName.xml"
|
||||
$sibling = Join-Path (Split-Path $SubsystemPath) "$dirName.xml"
|
||||
if (Test-Path $candidate) { $SubsystemPath = $candidate }
|
||||
elseif (Test-Path $sibling) { $SubsystemPath = $sibling }
|
||||
else {
|
||||
Write-Host "[ERROR] No $dirName.xml found in directory: $SubsystemPath"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
# File not found — check Dir/Name/Name.xml → Dir/Name.xml
|
||||
if (-not (Test-Path $SubsystemPath)) {
|
||||
$fn = [System.IO.Path]::GetFileNameWithoutExtension($SubsystemPath)
|
||||
$pd = Split-Path $SubsystemPath
|
||||
if ($fn -eq (Split-Path $pd -Leaf)) {
|
||||
$c = Join-Path (Split-Path $pd) "$fn.xml"
|
||||
if (Test-Path $c) { $SubsystemPath = $c }
|
||||
}
|
||||
}
|
||||
if (-not (Test-Path $SubsystemPath)) {
|
||||
Write-Host "[ERROR] File not found: $SubsystemPath"
|
||||
exit 1
|
||||
}
|
||||
$resolvedPath = (Resolve-Path $SubsystemPath).Path
|
||||
|
||||
# --- Output infrastructure ---
|
||||
$script:errors = 0
|
||||
$script:warnings = 0
|
||||
$script:stopped = $false
|
||||
$script:okCount = 0
|
||||
$script:output = New-Object System.Text.StringBuilder 8192
|
||||
|
||||
function Out-Line([string]$msg) { $script:output.AppendLine($msg) | Out-Null }
|
||||
function Report-OK([string]$msg) {
|
||||
$script:okCount++
|
||||
if ($Detailed) { Out-Line "[OK] $msg" }
|
||||
}
|
||||
function Report-Error([string]$msg) {
|
||||
$script:errors++
|
||||
Out-Line "[ERROR] $msg"
|
||||
if ($script:errors -ge $MaxErrors) { $script:stopped = $true }
|
||||
}
|
||||
function Report-Warn([string]$msg) {
|
||||
$script:warnings++
|
||||
Out-Line "[WARN] $msg"
|
||||
}
|
||||
|
||||
$guidPattern = '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$'
|
||||
$identPattern = '^[A-Za-z\u0410-\u042F\u0401\u0430-\u044F\u0451_][A-Za-z0-9\u0410-\u042F\u0401\u0430-\u044F\u0451_]*$'
|
||||
|
||||
# Known plural forms that are NOT valid in subsystem Content (platform expects singular)
|
||||
$knownPluralTypes = @(
|
||||
"Catalogs","Documents","Enums","Constants","Reports","DataProcessors"
|
||||
"InformationRegisters","AccumulationRegisters","AccountingRegisters","CalculationRegisters"
|
||||
"ChartsOfAccounts","ChartsOfCharacteristicTypes","ChartsOfCalculationTypes"
|
||||
"BusinessProcesses","Tasks","ExchangePlans","DocumentJournals"
|
||||
"CommonModules","CommonCommands","CommonForms","CommonPictures","CommonTemplates"
|
||||
"CommonAttributes","CommandGroups","Roles","SessionParameters","FilterCriteria"
|
||||
"XDTOPackages","WebServices","HTTPServices","WSReferences","EventSubscriptions"
|
||||
"ScheduledJobs","SettingsStorages","FunctionalOptions","FunctionalOptionsParameters"
|
||||
"DefinedTypes","DocumentNumerators","Sequences","Subsystems","StyleItems","IntegrationServices"
|
||||
)
|
||||
|
||||
# --- 1. XML well-formedness + root structure ---
|
||||
$xmlDoc = $null
|
||||
try {
|
||||
[xml]$xmlDoc = Get-Content -Path $resolvedPath -Encoding UTF8
|
||||
} catch {
|
||||
Report-Error "1. XML parse error: $($_.Exception.Message)"
|
||||
$script:stopped = $true
|
||||
}
|
||||
|
||||
if (-not $script:stopped) {
|
||||
$ns = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable)
|
||||
$ns.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
|
||||
$ns.AddNamespace("v8", "http://v8.1c.ru/8.1/data/core")
|
||||
$ns.AddNamespace("xr", "http://v8.1c.ru/8.3/xcf/readable")
|
||||
$ns.AddNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance")
|
||||
|
||||
$root = $xmlDoc.DocumentElement
|
||||
$sub = $xmlDoc.SelectSingleNode("/md:MetaDataObject/md:Subsystem", $ns)
|
||||
$version = $root.GetAttribute("version")
|
||||
|
||||
if (-not $sub) {
|
||||
Report-Error "1. Root structure: expected MetaDataObject/Subsystem, not found"
|
||||
$script:stopped = $true
|
||||
} else {
|
||||
$uuid = $sub.GetAttribute("uuid")
|
||||
if ($uuid -and $uuid -match $guidPattern) {
|
||||
Report-OK "1. Root structure: MetaDataObject/Subsystem, uuid=$uuid, version $version"
|
||||
} else {
|
||||
Report-Error "1. Root structure: invalid or missing uuid"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# --- Properties checks ---
|
||||
if (-not $script:stopped) {
|
||||
$props = $sub.SelectSingleNode("md:Properties", $ns)
|
||||
if (-not $props) {
|
||||
Report-Error "2. Properties: <Properties> element not found"
|
||||
$script:stopped = $true
|
||||
}
|
||||
}
|
||||
|
||||
$subName = ""
|
||||
if (-not $script:stopped) {
|
||||
# --- 2. Required properties ---
|
||||
$requiredProps = @("Name","Synonym","Comment","IncludeHelpInContents","IncludeInCommandInterface","UseOneCommand","Explanation","Picture","Content")
|
||||
$missing = @()
|
||||
foreach ($p in $requiredProps) {
|
||||
$el = $props.SelectSingleNode("md:$p", $ns)
|
||||
if (-not $el) { $missing += $p }
|
||||
}
|
||||
if ($missing.Count -eq 0) {
|
||||
Report-OK "2. Properties: all 9 required properties present"
|
||||
} else {
|
||||
Report-Error "2. Properties: missing: $($missing -join ', ')"
|
||||
}
|
||||
|
||||
# --- 3. Name ---
|
||||
$nameEl = $props.SelectSingleNode("md:Name", $ns)
|
||||
$subName = if ($nameEl) { $nameEl.InnerText.Trim() } else { "" }
|
||||
# Re-insert header at position 0
|
||||
$headerLine = "=== Validation: Subsystem.$subName ==="
|
||||
$script:output.Insert(0, "$headerLine`r`n`r`n") | Out-Null
|
||||
|
||||
if ($subName -and $subName -match $identPattern) {
|
||||
Report-OK "3. Name: `"$subName`" - valid identifier"
|
||||
} elseif (-not $subName) {
|
||||
Report-Error "3. Name: empty"
|
||||
} else {
|
||||
Report-Error "3. Name: `"$subName`" - invalid identifier"
|
||||
}
|
||||
|
||||
# --- 4. Synonym ---
|
||||
$synEl = $props.SelectSingleNode("md:Synonym", $ns)
|
||||
if ($synEl -and $synEl.HasChildNodes) {
|
||||
$items = $synEl.SelectNodes("v8:item", $ns)
|
||||
if ($items.Count -gt 0) {
|
||||
$firstContent = ""
|
||||
foreach ($item in $items) {
|
||||
$c = $item.SelectSingleNode("v8:content", $ns)
|
||||
if ($c -and $c.InnerText) { $firstContent = $c.InnerText; break }
|
||||
}
|
||||
Report-OK "4. Synonym: `"$firstContent`" ($($items.Count) lang(s))"
|
||||
} else {
|
||||
Report-Warn "4. Synonym: element exists but no v8:item children"
|
||||
}
|
||||
} else {
|
||||
Report-Warn "4. Synonym: empty or missing"
|
||||
}
|
||||
|
||||
# --- 5. Boolean properties ---
|
||||
$boolProps = @("IncludeHelpInContents","IncludeInCommandInterface","UseOneCommand")
|
||||
$boolOk = $true
|
||||
$boolVals = @{}
|
||||
foreach ($bp in $boolProps) {
|
||||
$el = $props.SelectSingleNode("md:$bp", $ns)
|
||||
if ($el) {
|
||||
$val = $el.InnerText.Trim()
|
||||
$boolVals[$bp] = $val
|
||||
if ($val -ne "true" -and $val -ne "false") {
|
||||
Report-Error "5. Boolean property $bp = `"$val`" (expected true/false)"
|
||||
$boolOk = $false
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($boolOk) { Report-OK "5. Boolean properties: valid" }
|
||||
|
||||
# --- 6. Content items format ---
|
||||
$contentEl = $props.SelectSingleNode("md:Content", $ns)
|
||||
$contentItems = @()
|
||||
if ($contentEl -and $contentEl.HasChildNodes) {
|
||||
$xrItems = $contentEl.SelectNodes("xr:Item", $ns)
|
||||
$contentOk = $true
|
||||
foreach ($item in $xrItems) {
|
||||
$typeAttr = $item.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance")
|
||||
$text = $item.InnerText.Trim()
|
||||
$contentItems += $text
|
||||
if ($typeAttr -ne "xr:MDObjectRef") {
|
||||
Report-Error "6. Content item `"$text`": xsi:type=`"$typeAttr`" (expected xr:MDObjectRef)"
|
||||
$contentOk = $false
|
||||
}
|
||||
if ($text -notmatch '^[A-Za-z]+\..+$' -and $text -notmatch $guidPattern) {
|
||||
Report-Error "6. Content item `"$text`": invalid format (expected Type.Name or UUID)"
|
||||
$contentOk = $false
|
||||
}
|
||||
if ($text -match '^([A-Za-z]+)\.') {
|
||||
$typePart = $Matches[1]
|
||||
if ($typePart -in $knownPluralTypes) {
|
||||
Report-Error "6. Content item `"$text`": uses plural form `"$typePart`" (platform requires singular, e.g. Catalog not Catalogs)"
|
||||
$contentOk = $false
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($contentOk) { Report-OK "6. Content: $($xrItems.Count) items, all valid MDObjectRef format" }
|
||||
} else {
|
||||
Report-OK "6. Content: empty (no items)"
|
||||
}
|
||||
|
||||
# --- 7. Content duplicates ---
|
||||
if ($contentItems.Count -gt 0) {
|
||||
$dupes = $contentItems | Group-Object | Where-Object { $_.Count -gt 1 }
|
||||
if ($dupes) {
|
||||
$dupeNames = ($dupes | ForEach-Object { $_.Name }) -join ", "
|
||||
Report-Warn "7. Content: duplicates found: $dupeNames"
|
||||
} else {
|
||||
Report-OK "7. Content: no duplicates"
|
||||
}
|
||||
} else {
|
||||
Report-OK "7. Content: no duplicates (empty)"
|
||||
}
|
||||
|
||||
# --- 8. ChildObjects entries non-empty ---
|
||||
$childObjs = $sub.SelectSingleNode("md:ChildObjects", $ns)
|
||||
$childNames = @()
|
||||
if ($childObjs -and $childObjs.HasChildNodes) {
|
||||
$childOk = $true
|
||||
foreach ($child in $childObjs.ChildNodes) {
|
||||
if ($child.NodeType -ne 'Element') { continue }
|
||||
if ($child.LocalName -ne "Subsystem") {
|
||||
Report-Error "8. ChildObjects: unexpected element <$($child.LocalName)>"
|
||||
$childOk = $false
|
||||
} elseif (-not $child.InnerText.Trim()) {
|
||||
Report-Error "8. ChildObjects: empty <Subsystem> element"
|
||||
$childOk = $false
|
||||
} else {
|
||||
$childNames += $child.InnerText.Trim()
|
||||
}
|
||||
}
|
||||
if ($childOk) { Report-OK "8. ChildObjects: $($childNames.Count) entries, all non-empty" }
|
||||
} else {
|
||||
Report-OK "8. ChildObjects: empty (leaf subsystem)"
|
||||
}
|
||||
|
||||
# --- 9. ChildObjects duplicates ---
|
||||
if ($childNames.Count -gt 0) {
|
||||
$dupes = $childNames | Group-Object | Where-Object { $_.Count -gt 1 }
|
||||
if ($dupes) {
|
||||
$dupeNames = ($dupes | ForEach-Object { $_.Name }) -join ", "
|
||||
Report-Error "9. ChildObjects: duplicates: $dupeNames"
|
||||
} else {
|
||||
Report-OK "9. ChildObjects: no duplicates"
|
||||
}
|
||||
} else {
|
||||
Report-OK "9. ChildObjects: no duplicates (empty)"
|
||||
}
|
||||
|
||||
# --- 10. ChildObjects files exist ---
|
||||
if ($childNames.Count -gt 0) {
|
||||
$parentDir = [System.IO.Path]::GetDirectoryName($resolvedPath)
|
||||
$baseName = [System.IO.Path]::GetFileNameWithoutExtension($resolvedPath)
|
||||
$subsDir = Join-Path (Join-Path $parentDir $baseName) "Subsystems"
|
||||
$missingFiles = @()
|
||||
foreach ($cn in $childNames) {
|
||||
$childXml = Join-Path $subsDir "$cn.xml"
|
||||
if (-not (Test-Path $childXml)) { $missingFiles += $cn }
|
||||
}
|
||||
if ($missingFiles.Count -eq 0) {
|
||||
Report-OK "10. ChildObjects files: all $($childNames.Count) files exist"
|
||||
} else {
|
||||
Report-Warn "10. ChildObjects files: missing: $($missingFiles -join ', ')"
|
||||
}
|
||||
}
|
||||
|
||||
# --- 11. CommandInterface.xml ---
|
||||
$parentDir2 = [System.IO.Path]::GetDirectoryName($resolvedPath)
|
||||
$baseName2 = [System.IO.Path]::GetFileNameWithoutExtension($resolvedPath)
|
||||
$ciPath = Join-Path (Join-Path (Join-Path $parentDir2 $baseName2) "Ext") "CommandInterface.xml"
|
||||
if (Test-Path $ciPath) {
|
||||
try {
|
||||
[xml]$ciDoc = Get-Content -Path $ciPath -Encoding UTF8
|
||||
Report-OK "11. CommandInterface: exists, well-formed"
|
||||
} catch {
|
||||
Report-Warn "11. CommandInterface: exists but NOT well-formed: $($_.Exception.Message)"
|
||||
}
|
||||
} else {
|
||||
Report-OK "11. CommandInterface: not present"
|
||||
}
|
||||
|
||||
# --- 12. Picture format ---
|
||||
$picEl = $props.SelectSingleNode("md:Picture", $ns)
|
||||
if ($picEl -and $picEl.HasChildNodes) {
|
||||
$picRef = $picEl.SelectSingleNode("xr:Ref", $ns)
|
||||
if ($picRef -and $picRef.InnerText) {
|
||||
$refText = $picRef.InnerText
|
||||
if ($refText -match '^CommonPicture\.') {
|
||||
Report-OK "12. Picture: $refText"
|
||||
} else {
|
||||
Report-Warn "12. Picture: `"$refText`" (expected CommonPicture.XXX)"
|
||||
}
|
||||
} else {
|
||||
Report-Warn "12. Picture: has children but no xr:Ref content"
|
||||
}
|
||||
} else {
|
||||
Report-OK "12. Picture: empty (not set)"
|
||||
}
|
||||
|
||||
# --- 13. UseOneCommand constraint ---
|
||||
$useOne = $boolVals["UseOneCommand"]
|
||||
if ($useOne -eq "true") {
|
||||
if ($contentItems.Count -eq 1) {
|
||||
Report-OK "13. UseOneCommand: true, Content has exactly 1 item"
|
||||
} else {
|
||||
Report-Warn "13. UseOneCommand: true but Content has $($contentItems.Count) items (expected 1)"
|
||||
}
|
||||
} else {
|
||||
Report-OK "13. UseOneCommand: false (no constraint)"
|
||||
}
|
||||
}
|
||||
|
||||
# --- Finalize ---
|
||||
$checks = $script:okCount + $script:errors + $script:warnings
|
||||
|
||||
if ($script:errors -eq 0 -and $script:warnings -eq 0 -and -not $Detailed) {
|
||||
$result = "=== Validation OK: Subsystem.$subName ($checks checks) ==="
|
||||
} else {
|
||||
Out-Line ""
|
||||
Out-Line "=== Result: $($script:errors) errors, $($script:warnings) warnings ($checks checks) ==="
|
||||
$result = $script:output.ToString()
|
||||
}
|
||||
|
||||
Write-Host $result
|
||||
|
||||
if ($OutFile) {
|
||||
if (-not [System.IO.Path]::IsPathRooted($OutFile)) {
|
||||
$OutFile = Join-Path (Get-Location).Path $OutFile
|
||||
}
|
||||
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
|
||||
[System.IO.File]::WriteAllText($OutFile, $result, $utf8Bom)
|
||||
Write-Host "Written to: $OutFile"
|
||||
}
|
||||
|
||||
if ($script:errors -gt 0) { exit 1 } else { exit 0 }
|
||||
@@ -0,0 +1,371 @@
|
||||
#!/usr/bin/env python3
|
||||
# subsystem-validate v1.2 — Validate 1C subsystem XML structure
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
"""Validates subsystem XML file structure, properties, content items, child objects."""
|
||||
import sys, os, argparse, re
|
||||
from lxml import etree
|
||||
|
||||
NS = {
|
||||
'md': 'http://v8.1c.ru/8.3/MDClasses',
|
||||
'v8': 'http://v8.1c.ru/8.1/data/core',
|
||||
'xr': 'http://v8.1c.ru/8.3/xcf/readable',
|
||||
'xsi': 'http://www.w3.org/2001/XMLSchema-instance',
|
||||
}
|
||||
|
||||
GUID_PATTERN = re.compile(
|
||||
r'^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$'
|
||||
)
|
||||
IDENT_PATTERN = re.compile(
|
||||
r'^[A-Za-z\u0410-\u042F\u0401\u0430-\u044F\u0451_]'
|
||||
r'[A-Za-z0-9\u0410-\u042F\u0401\u0430-\u044F\u0451_]*$'
|
||||
)
|
||||
|
||||
KNOWN_PLURAL_TYPES = {
|
||||
'Catalogs', 'Documents', 'Enums', 'Constants', 'Reports', 'DataProcessors',
|
||||
'InformationRegisters', 'AccumulationRegisters', 'AccountingRegisters', 'CalculationRegisters',
|
||||
'ChartsOfAccounts', 'ChartsOfCharacteristicTypes', 'ChartsOfCalculationTypes',
|
||||
'BusinessProcesses', 'Tasks', 'ExchangePlans', 'DocumentJournals',
|
||||
'CommonModules', 'CommonCommands', 'CommonForms', 'CommonPictures', 'CommonTemplates',
|
||||
'CommonAttributes', 'CommandGroups', 'Roles', 'SessionParameters', 'FilterCriteria',
|
||||
'XDTOPackages', 'WebServices', 'HTTPServices', 'WSReferences', 'EventSubscriptions',
|
||||
'ScheduledJobs', 'SettingsStorages', 'FunctionalOptions', 'FunctionalOptionsParameters',
|
||||
'DefinedTypes', 'DocumentNumerators', 'Sequences', 'Subsystems', 'StyleItems', 'IntegrationServices',
|
||||
}
|
||||
|
||||
|
||||
class Reporter:
|
||||
def __init__(self, max_errors, detailed=False):
|
||||
self.errors = 0
|
||||
self.warnings = 0
|
||||
self.ok_count = 0
|
||||
self.stopped = False
|
||||
self.max_errors = max_errors
|
||||
self.detailed = detailed
|
||||
self.lines = []
|
||||
|
||||
def out(self, msg=''):
|
||||
self.lines.append(msg)
|
||||
|
||||
def ok(self, msg):
|
||||
self.ok_count += 1
|
||||
if self.detailed:
|
||||
self.lines.append(f'[OK] {msg}')
|
||||
|
||||
def error(self, msg):
|
||||
self.errors += 1
|
||||
self.lines.append(f'[ERROR] {msg}')
|
||||
if self.errors >= self.max_errors:
|
||||
self.stopped = True
|
||||
|
||||
def warn(self, msg):
|
||||
self.warnings += 1
|
||||
self.lines.append(f'[WARN] {msg}')
|
||||
|
||||
def text(self):
|
||||
return '\r\n'.join(self.lines) + '\r\n'
|
||||
|
||||
|
||||
def find_duplicates(items):
|
||||
seen = {}
|
||||
dupes = []
|
||||
for item in items:
|
||||
seen[item] = seen.get(item, 0) + 1
|
||||
for item, count in seen.items():
|
||||
if count > 1 and item not in dupes:
|
||||
dupes.append(item)
|
||||
return dupes
|
||||
|
||||
|
||||
def main():
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
sys.stderr.reconfigure(encoding="utf-8")
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Validate 1C subsystem XML structure', allow_abbrev=False
|
||||
)
|
||||
parser.add_argument('-SubsystemPath', '-Path', dest='SubsystemPath', required=True)
|
||||
parser.add_argument('-Detailed', action='store_true')
|
||||
parser.add_argument('-MaxErrors', dest='MaxErrors', type=int, default=30)
|
||||
parser.add_argument('-OutFile', dest='OutFile', default='')
|
||||
args = parser.parse_args()
|
||||
|
||||
subsystem_path = args.SubsystemPath
|
||||
detailed = args.Detailed
|
||||
max_errors = args.MaxErrors
|
||||
out_file = args.OutFile
|
||||
|
||||
# --- Resolve path ---
|
||||
if not os.path.isabs(subsystem_path):
|
||||
subsystem_path = os.path.join(os.getcwd(), subsystem_path)
|
||||
|
||||
if os.path.isdir(subsystem_path):
|
||||
dir_name = os.path.basename(subsystem_path)
|
||||
candidate = os.path.join(subsystem_path, dir_name + '.xml')
|
||||
sibling = os.path.join(os.path.dirname(subsystem_path), dir_name + '.xml')
|
||||
if os.path.exists(candidate):
|
||||
subsystem_path = candidate
|
||||
elif os.path.exists(sibling):
|
||||
subsystem_path = sibling
|
||||
else:
|
||||
print(f'[ERROR] No {dir_name}.xml found in directory: {subsystem_path}')
|
||||
sys.exit(1)
|
||||
|
||||
# File not found -- check Dir/Name/Name.xml -> Dir/Name.xml
|
||||
if not os.path.exists(subsystem_path):
|
||||
fn = os.path.splitext(os.path.basename(subsystem_path))[0]
|
||||
pd = os.path.dirname(subsystem_path)
|
||||
if fn == os.path.basename(pd):
|
||||
c = os.path.join(os.path.dirname(pd), fn + '.xml')
|
||||
if os.path.exists(c):
|
||||
subsystem_path = c
|
||||
|
||||
if not os.path.exists(subsystem_path):
|
||||
print(f'[ERROR] File not found: {subsystem_path}')
|
||||
sys.exit(1)
|
||||
|
||||
resolved_path = os.path.abspath(subsystem_path)
|
||||
r = Reporter(max_errors, detailed)
|
||||
|
||||
# --- 1. XML well-formedness + root structure ---
|
||||
xml_doc = None
|
||||
try:
|
||||
xml_parser = etree.XMLParser(remove_blank_text=False)
|
||||
xml_doc = etree.parse(resolved_path, xml_parser)
|
||||
except etree.XMLSyntaxError as e:
|
||||
r.error(f'1. XML parse error: {e}')
|
||||
r.stopped = True
|
||||
|
||||
sub = None
|
||||
version = ''
|
||||
if not r.stopped:
|
||||
root = xml_doc.getroot()
|
||||
version = root.get('version', '')
|
||||
sub_list = root.findall('md:Subsystem', NS)
|
||||
sub = sub_list[0] if sub_list else None
|
||||
|
||||
if sub is None:
|
||||
r.error('1. Root structure: expected MetaDataObject/Subsystem, not found')
|
||||
r.stopped = True
|
||||
else:
|
||||
uuid_val = sub.get('uuid', '')
|
||||
if uuid_val and GUID_PATTERN.match(uuid_val):
|
||||
r.ok(f'1. Root structure: MetaDataObject/Subsystem, uuid={uuid_val}, version {version}')
|
||||
else:
|
||||
r.error('1. Root structure: invalid or missing uuid')
|
||||
|
||||
# --- Properties checks ---
|
||||
props = None
|
||||
if not r.stopped:
|
||||
props_list = sub.findall('md:Properties', NS)
|
||||
props = props_list[0] if props_list else None
|
||||
if props is None:
|
||||
r.error('2. Properties: <Properties> element not found')
|
||||
r.stopped = True
|
||||
|
||||
sub_name = ''
|
||||
if not r.stopped:
|
||||
# --- 2. Required properties ---
|
||||
required_props = [
|
||||
'Name', 'Synonym', 'Comment', 'IncludeHelpInContents',
|
||||
'IncludeInCommandInterface', 'UseOneCommand', 'Explanation',
|
||||
'Picture', 'Content'
|
||||
]
|
||||
missing = []
|
||||
for p in required_props:
|
||||
el = props.find(f'md:{p}', NS)
|
||||
if el is None:
|
||||
missing.append(p)
|
||||
|
||||
if len(missing) == 0:
|
||||
r.ok('2. Properties: all 9 required properties present')
|
||||
else:
|
||||
r.error(f'2. Properties: missing: {", ".join(missing)}')
|
||||
|
||||
# --- 3. Name ---
|
||||
name_el = props.find('md:Name', NS)
|
||||
sub_name = (name_el.text or '').strip() if name_el is not None else ''
|
||||
|
||||
r.out('')
|
||||
r.out(f'=== Validation: Subsystem.{sub_name} ===')
|
||||
# Re-insert header at position 0
|
||||
header_line = f'=== Validation: Subsystem.{sub_name} ==='
|
||||
r.lines.insert(0, '')
|
||||
r.lines.insert(0, header_line)
|
||||
|
||||
if sub_name and IDENT_PATTERN.match(sub_name):
|
||||
r.ok(f'3. Name: "{sub_name}" - valid identifier')
|
||||
elif not sub_name:
|
||||
r.error('3. Name: empty')
|
||||
else:
|
||||
r.error(f'3. Name: "{sub_name}" - invalid identifier')
|
||||
|
||||
# --- 4. Synonym ---
|
||||
syn_el = props.find('md:Synonym', NS)
|
||||
if syn_el is not None and len(syn_el) > 0:
|
||||
items = syn_el.findall('v8:item', NS)
|
||||
if len(items) > 0:
|
||||
first_content = ''
|
||||
for item in items:
|
||||
c = item.find('v8:content', NS)
|
||||
if c is not None and c.text:
|
||||
first_content = c.text
|
||||
break
|
||||
r.ok(f'4. Synonym: "{first_content}" ({len(items)} lang(s))')
|
||||
else:
|
||||
r.warn('4. Synonym: element exists but no v8:item children')
|
||||
else:
|
||||
r.warn('4. Synonym: empty or missing')
|
||||
|
||||
# --- 5. Boolean properties ---
|
||||
bool_props = ['IncludeHelpInContents', 'IncludeInCommandInterface', 'UseOneCommand']
|
||||
bool_ok = True
|
||||
bool_vals = {}
|
||||
for bp in bool_props:
|
||||
el = props.find(f'md:{bp}', NS)
|
||||
if el is not None:
|
||||
val = (el.text or '').strip()
|
||||
bool_vals[bp] = val
|
||||
if val not in ('true', 'false'):
|
||||
r.error(f'5. Boolean property {bp} = "{val}" (expected true/false)')
|
||||
bool_ok = False
|
||||
if bool_ok:
|
||||
r.ok('5. Boolean properties: valid')
|
||||
|
||||
# --- 6. Content items format ---
|
||||
content_el = props.find('md:Content', NS)
|
||||
content_items = []
|
||||
if content_el is not None and len(content_el) > 0:
|
||||
xr_items = content_el.findall('xr:Item', NS)
|
||||
content_ok = True
|
||||
for item in xr_items:
|
||||
type_attr = item.get(f'{{{NS["xsi"]}}}type', '')
|
||||
text = (item.text or '').strip()
|
||||
content_items.append(text)
|
||||
if type_attr != 'xr:MDObjectRef':
|
||||
r.error(f'6. Content item "{text}": xsi:type="{type_attr}" (expected xr:MDObjectRef)')
|
||||
content_ok = False
|
||||
if not re.match(r'^[A-Za-z]+\..+$', text) and not GUID_PATTERN.match(text):
|
||||
r.error(f'6. Content item "{text}": invalid format (expected Type.Name or UUID)')
|
||||
content_ok = False
|
||||
m = re.match(r'^([A-Za-z]+)\.', text)
|
||||
if m and m.group(1) in KNOWN_PLURAL_TYPES:
|
||||
r.error(f'6. Content item "{text}": uses plural form "{m.group(1)}" (platform requires singular, e.g. Catalog not Catalogs)')
|
||||
content_ok = False
|
||||
if content_ok:
|
||||
r.ok(f'6. Content: {len(xr_items)} items, all valid MDObjectRef format')
|
||||
else:
|
||||
r.ok('6. Content: empty (no items)')
|
||||
|
||||
# --- 7. Content duplicates ---
|
||||
if len(content_items) > 0:
|
||||
dupes = find_duplicates(content_items)
|
||||
if dupes:
|
||||
r.warn(f'7. Content: duplicates found: {", ".join(dupes)}')
|
||||
else:
|
||||
r.ok('7. Content: no duplicates')
|
||||
|
||||
# --- 8. ChildObjects entries non-empty ---
|
||||
child_objs = sub.find('md:ChildObjects', NS)
|
||||
child_names = []
|
||||
if child_objs is not None and len(child_objs) > 0:
|
||||
child_ok = True
|
||||
for child in child_objs:
|
||||
if not isinstance(child.tag, str):
|
||||
continue
|
||||
local_name = etree.QName(child.tag).localname
|
||||
if local_name != 'Subsystem':
|
||||
r.error(f'8. ChildObjects: unexpected element <{local_name}>')
|
||||
child_ok = False
|
||||
elif not (child.text or '').strip():
|
||||
r.error('8. ChildObjects: empty <Subsystem> element')
|
||||
child_ok = False
|
||||
else:
|
||||
child_names.append((child.text or '').strip())
|
||||
if child_ok:
|
||||
r.ok(f'8. ChildObjects: {len(child_names)} entries, all non-empty')
|
||||
else:
|
||||
r.ok('8. ChildObjects: empty (leaf subsystem)')
|
||||
|
||||
# --- 9. ChildObjects duplicates ---
|
||||
if len(child_names) > 0:
|
||||
dupes = find_duplicates(child_names)
|
||||
if dupes:
|
||||
r.error(f'9. ChildObjects: duplicates: {", ".join(dupes)}')
|
||||
else:
|
||||
r.ok('9. ChildObjects: no duplicates')
|
||||
|
||||
# --- 10. ChildObjects files exist ---
|
||||
if len(child_names) > 0:
|
||||
parent_dir = os.path.dirname(resolved_path)
|
||||
base_name = os.path.splitext(os.path.basename(resolved_path))[0]
|
||||
subs_dir = os.path.join(parent_dir, base_name, 'Subsystems')
|
||||
missing_files = []
|
||||
for cn in child_names:
|
||||
child_xml = os.path.join(subs_dir, cn + '.xml')
|
||||
if not os.path.exists(child_xml):
|
||||
missing_files.append(cn)
|
||||
if len(missing_files) == 0:
|
||||
r.ok(f'10. ChildObjects files: all {len(child_names)} files exist')
|
||||
else:
|
||||
r.warn(f'10. ChildObjects files: missing: {", ".join(missing_files)}')
|
||||
|
||||
# --- 11. CommandInterface.xml ---
|
||||
parent_dir2 = os.path.dirname(resolved_path)
|
||||
base_name2 = os.path.splitext(os.path.basename(resolved_path))[0]
|
||||
ci_path = os.path.join(parent_dir2, base_name2, 'Ext', 'CommandInterface.xml')
|
||||
if os.path.exists(ci_path):
|
||||
try:
|
||||
etree.parse(ci_path, etree.XMLParser(remove_blank_text=False))
|
||||
r.ok('11. CommandInterface: exists, well-formed')
|
||||
except etree.XMLSyntaxError as e:
|
||||
r.warn(f'11. CommandInterface: exists but NOT well-formed: {e}')
|
||||
else:
|
||||
r.ok('11. CommandInterface: not present')
|
||||
|
||||
# --- 12. Picture format ---
|
||||
pic_el = props.find('md:Picture', NS)
|
||||
if pic_el is not None and len(pic_el) > 0:
|
||||
pic_ref = pic_el.find('xr:Ref', NS)
|
||||
if pic_ref is not None and pic_ref.text:
|
||||
ref_text = pic_ref.text
|
||||
if ref_text.startswith('CommonPicture.'):
|
||||
r.ok(f'12. Picture: {ref_text}')
|
||||
else:
|
||||
r.warn(f'12. Picture: "{ref_text}" (expected CommonPicture.XXX)')
|
||||
else:
|
||||
r.warn('12. Picture: has children but no xr:Ref content')
|
||||
else:
|
||||
r.ok('12. Picture: empty (not set)')
|
||||
|
||||
# --- 13. UseOneCommand constraint ---
|
||||
use_one = bool_vals.get('UseOneCommand', '')
|
||||
if use_one == 'true':
|
||||
if len(content_items) == 1:
|
||||
r.ok('13. UseOneCommand: true, Content has exactly 1 item')
|
||||
else:
|
||||
r.warn(f'13. UseOneCommand: true but Content has {len(content_items)} items (expected 1)')
|
||||
else:
|
||||
r.ok('13. UseOneCommand: false (no constraint)')
|
||||
|
||||
# --- Finalize ---
|
||||
checks = r.ok_count + r.errors + r.warnings
|
||||
if r.errors == 0 and r.warnings == 0 and not detailed:
|
||||
result = f'=== Validation OK: Subsystem.{sub_name} ({checks} checks) ==='
|
||||
else:
|
||||
r.out('')
|
||||
r.out(f'=== Result: {r.errors} errors, {r.warnings} warnings ({checks} checks) ===')
|
||||
result = '\r\n'.join(r.lines) + '\r\n'
|
||||
|
||||
print(result, end='')
|
||||
|
||||
if out_file:
|
||||
if not os.path.isabs(out_file):
|
||||
out_file = os.path.join(os.getcwd(), out_file)
|
||||
with open(out_file, 'w', encoding='utf-8-sig', newline='') as f:
|
||||
f.write(result)
|
||||
print(f'Written to: {out_file}')
|
||||
|
||||
sys.exit(1 if r.errors > 0 else 0)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user