From 2292bf700d28ade39cf2d64b45509e6db21034bc Mon Sep 17 00:00:00 2001 From: cirakg Date: Sun, 18 Oct 2026 21:10:37 +0200 Subject: 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 --- tools/lint_project.py | 74 ++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 71 insertions(+), 3 deletions(-) (limited to 'tools/lint_project.py') diff --git a/tools/lint_project.py b/tools/lint_project.py index f11d35e..e02215b 100644 --- a/tools/lint_project.py +++ b/tools/lint_project.py @@ -1,5 +1,7 @@ #!/usr/bin/env python3 -"""Project-specific lint rules for the Monkey C sources and the manifest. +# Author: cirakg & claude ai +# Summary: Project-specific lint rules for the Monkey C sources, the manifest and the file headers. +"""Project-specific lint rules. The Monkey C compiler (strict type checking, `make lint`) catches language errors. These rules catch project policy violations the compiler can't see: @@ -14,6 +16,11 @@ errors. These rules catch project policy violations the compiler can't see: can be allowed with the comment `// lint: allow-literal` on its line. MC005 no network: manifest.xml declares no permissions and no source file imports or uses Toybox.Communications + HDR001 every project file names its author and summarises itself: + "Author: cirakg & claude ai" and "Summary: ..." in its first lines; + JSON files use "author"/"_author" and "description"/"_summary" keys; + PNG images use Author and Description text chunks. Files that a tool + regenerates (GENERATED below) are exempt. Usage: lint_project.py [ROOT] Exit: 0 clean, 1 problems found (printed as file:line: CODE message). @@ -21,13 +28,24 @@ Exit: 0 clean, 1 problems found (printed as file:line: CODE message). from __future__ import annotations +import json +import os import re import sys from collections.abc import Iterable from dataclasses import dataclass from pathlib import Path +from PIL import Image + +AUTHOR = "cirakg & claude ai" +HEADER_LINES = 15 DIAG_FILE = Path("source/util/Diag.mc") +# Folders that hold local tool state or generated output, never project files. +SKIP_DIRS = {".git", ".cache", "build", "node_modules", "__pycache__", ".pytest_cache", ".mypy_cache", ".ruff_cache"} +# Written by a tool that would drop a hand-added header on the next run. +GENERATED = {Path("docker/docs/package-lock.json")} + BASE32_SECRET_RE = re.compile(r'"[^"\n]*[A-Z2-7]{16,}[^"\n]*"') HEX_SECRET_RE = re.compile(r'"[^"\n]*[0-9a-fA-F]{32,}[^"\n]*"') MARKER_RE = re.compile(r"\b(TODO|FIXME|XXX)\b") @@ -35,6 +53,8 @@ PRINTLN_RE = re.compile(r"\bSystem\.println\s*\(") COMMUNICATIONS_RE = re.compile(r"\bToybox\.Communications\b|\bCommunications\.") PERMISSION_RE = re.compile(r" Iterable[Problem]: yield Problem(rel, number, "MC005", "manifest must not request permissions") -def lint(root: Path) -> list[Problem]: +def lint_code(root: Path) -> list[Problem]: + """The MC rules: Monkey C sources, tests and the manifest.""" problems: list[Problem] = [] for folder in ("source", "test"): for path in sorted((root / folder).rglob("*.mc")): @@ -96,7 +117,54 @@ def lint(root: Path) -> list[Problem]: problems += check_manifest(manifest, manifest.relative_to(root)) else: problems.append(Problem(Path("manifest.xml"), 1, "MC005", "manifest.xml is missing")) - return sorted(problems) + return problems + + +def project_files(root: Path) -> list[Path]: + """Every file of the project, skipping tool state and build output.""" + files = [] + for folder, dirs, names in os.walk(root): + dirs[:] = sorted(d for d in dirs if d not in SKIP_DIRS) + files += [Path(folder) / name for name in sorted(names)] + return files + + +def check_header(path: Path, rel: Path) -> Iterable[Problem]: + """HDR001 for one file, by file type.""" + if rel in GENERATED: + return + suffix = path.suffix.lower() + if suffix == ".png": + with Image.open(path) as img: + info = img.info + if info.get("Author") != AUTHOR or not info.get("Description"): + yield Problem(rel, 1, "HDR001", f"PNG needs Author '{AUTHOR}' and Description text chunks") + return + if suffix == ".json": + data = json.loads(path.read_text(encoding="utf-8")) + meta = data if isinstance(data, dict) else {} + if meta.get("author", meta.get("_author")) != AUTHOR or not meta.get("description", meta.get("_summary")): + yield Problem(rel, 1, "HDR001", 'JSON needs "author"/"_author" and "description"/"_summary" keys') + return + try: + head = "\n".join(path.read_text(encoding="utf-8").splitlines()[:HEADER_LINES]) + except UnicodeDecodeError: + yield Problem(rel, 1, "HDR001", "binary file type without a header convention") + return + if not AUTHOR_RE.search(head) or not SUMMARY_RE.search(head): + message = f"needs 'Author: {AUTHOR}' and 'Summary: ...' in its first {HEADER_LINES} lines" + yield Problem(rel, 1, "HDR001", message) + + +def lint_headers(root: Path) -> list[Problem]: + problems: list[Problem] = [] + for path in project_files(root): + problems += check_header(path, path.relative_to(root)) + return problems + + +def lint(root: Path) -> list[Problem]: + return sorted(lint_code(root) + lint_headers(root)) def main(argv: list[str]) -> int: -- cgit v1.2.3