#!/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())