diff options
| author | cirakg <cirakg@cirakg.xyz> | 2026-10-18 21:10:37 +0200 |
|---|---|---|
| committer | cirakg <cirakg@cirakg.xyz> | 2026-10-18 21:10:37 +0200 |
| commit | 2292bf700d28ade39cf2d64b45509e6db21034bc (patch) | |
| tree | ef455320d39ebe51db236219dbbda2b148dcbe18 /tools/gen_products.py | |
| parent | 85f1ecabd7f58f306b2a042376209ac8cf49ab9b (diff) | |
test(tools): add screenshot, budget and product checks; enforce file headers
- sim_screenshot.py finds the device drawing in the simulator window,
crops the screen and compares it with approved golden images.
- check_budgets.py reads key=value DIAG/HARNESS lines (the simulator
console escapes quotes, so JSON was dropped).
- ciq_test_report.py accepts SDK 9.2.0's real summary line
`PASSED (passed=N, failed=N, errors=N)`; a captured log is a fixture.
- gen_products.py checks all manifest products are watches with
API 5.0+ and at least 512 KB for apps.
- lint_project.py rule HDR001: every file carries the Author/Summary
header (JSON keys, PNG text chunks; generated package-lock exempt).
- make_icon.py --all draws one icon per launcher size in use.
- Golden screenshots for fr255s, venusq2, fr70 and fr965.
- 44 tests, warnings as errors, ruff and mypy --strict clean.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'tools/gen_products.py')
| -rw-r--r-- | tools/gen_products.py | 104 |
1 files changed, 104 insertions, 0 deletions
diff --git a/tools/gen_products.py b/tools/gen_products.py new file mode 100644 index 0000000..7504287 --- /dev/null +++ b/tools/gen_products.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 +# Author: cirakg & claude ai +# Summary: Check that every product in manifest.xml is a watch with API 5.0+ and at least 512 KB for watch apps. +"""Check the manifest's product list against the downloaded device files. + +The target rule (README section 14, decision D4): a product belongs in +manifest.xml when it is in Garmin's "Watches/Wearables" group, every firmware +variant supports Connect IQ API 5.0.0 or newer, and watch apps get at least +512 KB of memory. The data comes from each device's compiler.json, which +`make sdk` downloads for exactly the products in the manifest. + +Usage: gen_products.py --check [--manifest manifest.xml] [--devices DIR] +Exit: 0 every product qualifies, 1 some don't, 2 device files missing. + +Finding devices Garmin releases later needs the device catalogue (a login); +re-run `make sdk` after adding a new product to the manifest. +""" + +from __future__ import annotations + +import argparse +import json +import sys +import xml.etree.ElementTree as ET +from dataclasses import dataclass +from pathlib import Path + +EXIT_OK, EXIT_UNQUALIFIED, EXIT_MISSING = 0, 1, 2 +IQ_NS = "{http://www.garmin.com/xml/connectiq}" +MIN_API = (5, 0, 0) +MIN_WATCH_APP_MEMORY = 512 * 1024 +WATCH_GROUP = "Watches/Wearables" +DEFAULT_DEVICES = Path.home() / ".Garmin" / "ConnectIQ" / "Devices" + + +@dataclass(frozen=True) +class Device: + """What the target rule needs from one compiler.json.""" + + product: str + name: str + group: str + min_api: tuple[int, ...] + watch_app_memory: int + + def problems(self) -> list[str]: + reasons = [] + if self.group != WATCH_GROUP: + reasons.append(f"group is '{self.group}', not '{WATCH_GROUP}'") + if self.min_api < MIN_API: + reasons.append(f"API {'.'.join(map(str, self.min_api))} is below 5.0.0") + if self.watch_app_memory < MIN_WATCH_APP_MEMORY: + reasons.append(f"watch apps get {self.watch_app_memory // 1024} KB, below 512 KB") + return reasons + + +def parse_version(text: str) -> tuple[int, ...]: + return tuple(int(part) for part in text.split(".")) + + +def load_device(devices: Path, product: str) -> Device: + compiler = json.loads((devices / product / "compiler.json").read_text(encoding="utf-8")) + memory = {app["type"]: int(app["memoryLimit"]) for app in compiler["appTypes"]} + return Device( + product=product, + name=str(compiler.get("displayName", product)), + group=str(compiler.get("webDocDeviceGroup", "")), + min_api=min(parse_version(p["connectIQVersion"]) for p in compiler["partNumbers"]), + watch_app_memory=memory.get("watchApp", 0), + ) + + +def manifest_products(manifest: Path) -> list[str]: + root = ET.parse(manifest).getroot() + return [p.attrib["id"] for p in root.iter(f"{IQ_NS}product")] + + +def check(manifest: Path, devices: Path) -> int: + products = manifest_products(manifest) + worst = EXIT_OK + for product in products: + if not (devices / product / "compiler.json").exists(): + print(f"{product}: no device files (run 'make sdk')") + worst = max(worst, EXIT_MISSING) + continue + device = load_device(devices, product) + for reason in device.problems(): + print(f"{product} ({device.name}): {reason}") + worst = max(worst, EXIT_UNQUALIFIED) + print(f"gen_products: {len(products)} products checked") + return worst + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description="Check the manifest's products against the target rule.") + parser.add_argument("--check", action="store_true", required=True) + parser.add_argument("--manifest", type=Path, default=Path("manifest.xml")) + parser.add_argument("--devices", type=Path, default=DEFAULT_DEVICES) + args = parser.parse_args(argv) + return check(args.manifest, args.devices) + + +if __name__ == "__main__": + sys.exit(main()) |
