1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
|
#!/usr/bin/env python3
# Author: cirakg & claude ai
# Summary: Validate the resource XML against the SDK's resources.xsd and cross-check settings, properties and strings.
"""Validate the app's resource XML and cross-check the settings form (M7).
Checks:
1. Every XML file under resources/ and resources-*/ validates against the
Connect IQ SDK's bin/resources.xsd (the schema monkeyc ships).
2. settings.xml refers only to properties defined in properties.xml and to
strings defined in strings.xml (titles, prompts, descriptions, list
entries, error messages).
3. Every property appears in the form exactly once: a property the phone
can't show can't be cleared or read back, and one shown twice is a typo.
4. A list setting's default value is one of its entries, and read-only
(status) settings are string properties.
The watch side of the contract (every key the code reads exists, the form
defaults match) is checked by the Monkey C test settingsDefaultsMatchProperties.
Usage: check_resources.py ROOT [--xsd PATH]
(default PATH: $CIQ_SDK/bin/resources.xsd)
Exit: 0 all good, 1 problems found, 2 the schema can't be read.
"""
from __future__ import annotations
import argparse
import os
import re
import sys
import xml.etree.ElementTree as ET
from collections import Counter
from pathlib import Path
import xmlschema
EXIT_OK, EXIT_PROBLEMS, EXIT_NO_SCHEMA = 0, 1, 2
REFERENCE_RE = re.compile(r"@(Strings|Properties)\.(\w+)")
def resource_files(root: Path) -> list[Path]:
"""Every resource XML file, in a stable order."""
files: list[Path] = []
for folder in sorted(root.glob("resources*")):
if folder.is_dir():
files.extend(sorted(folder.rglob("*.xml")))
return files
def validate(schema: xmlschema.XMLSchema, files: list[Path], root: Path) -> list[str]:
"""Schema errors, one line per problem."""
problems = []
for path in files:
for error in schema.iter_errors(str(path)):
problems.append(f"{path.relative_to(root)}: {error.reason} (at {error.path})")
return problems
def ids(path: Path, tag: str) -> list[str]:
"""The id attributes of the given elements in one file."""
return [element.get("id", "") for element in ET.parse(path).getroot().iter(tag)]
def cross_check(settings: Path, properties: Path, strings: list[Path]) -> list[str]:
"""Problems between settings.xml, properties.xml and the strings."""
problems: list[str] = []
prop_types = {p.get("id", ""): p.get("type", "") for p in ET.parse(properties).getroot().iter("property")}
prop_defaults = {p.get("id", ""): (p.text or "").strip() for p in ET.parse(properties).getroot().iter("property")}
string_ids = {sid for path in strings for sid in ids(path, "string")}
text = settings.read_text(encoding="utf-8")
for kind, name in REFERENCE_RE.findall(text):
if kind == "Strings" and name not in string_ids:
problems.append(f"settings.xml: @Strings.{name} is not defined")
if kind == "Properties" and name not in prop_types:
problems.append(f"settings.xml: @Properties.{name} is not defined in properties.xml")
used: Counter[str] = Counter()
for setting in ET.parse(settings).getroot().iter("setting"):
key = REFERENCE_RE.sub(r"\2", setting.get("propertyKey", ""))
used[key] += 1
config = setting.find("settingConfig")
if config is None or key not in prop_types:
continue
if config.get("type") == "list":
values = [entry.get("value", "") for entry in config.iter("listEntry")]
if prop_defaults[key] not in values:
problems.append(f"{key}: default {prop_defaults[key]!r} is not one of the list values {values}")
if config.get("readonly") == "true" and prop_types[key] != "string":
problems.append(f"{key}: read-only settings must be string properties, not {prop_types[key]}")
for key in prop_types:
if used[key] == 0:
problems.append(f"properties.xml: {key} is not in the settings form")
elif used[key] > 1:
problems.append(f"settings.xml: {key} appears {used[key]} times")
return problems
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description="Validate resource XML and cross-check the settings form.")
parser.add_argument("root", type=Path, help="the project folder")
parser.add_argument("--xsd", type=Path, help="the SDK's resources.xsd")
args = parser.parse_args(argv)
root: Path = args.root
xsd: Path | None = args.xsd
if xsd is None and os.environ.get("CIQ_SDK"):
xsd = Path(os.environ["CIQ_SDK"]) / "bin" / "resources.xsd"
if xsd is None or not xsd.is_file():
print(f"error: SDK schema not found ({xsd}); set CIQ_SDK or pass --xsd", file=sys.stderr)
return EXIT_NO_SCHEMA
schema = xmlschema.XMLSchema(str(xsd))
files = resource_files(root)
problems = validate(schema, files, root)
problems += cross_check(
root / "resources" / "settings" / "settings.xml",
root / "resources" / "settings" / "properties.xml",
[path for path in files if path.parent.name == "strings"],
)
for problem in problems:
print(problem)
print(f"check_resources: {len(files)} files, {len(problems)} problems")
return EXIT_PROBLEMS if problems else EXIT_OK
if __name__ == "__main__":
sys.exit(main())
|