summaryrefslogtreecommitdiff
path: root/tools/ciq_test_report.py
diff options
context:
space:
mode:
authorcirakg <cirakg@cirakg.xyz>2026-10-18 21:10:37 +0200
committercirakg <cirakg@cirakg.xyz>2026-10-18 21:10:37 +0200
commit2292bf700d28ade39cf2d64b45509e6db21034bc (patch)
treeef455320d39ebe51db236219dbbda2b148dcbe18 /tools/ciq_test_report.py
parent85f1ecabd7f58f306b2a042376209ac8cf49ab9b (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/ciq_test_report.py')
-rw-r--r--tools/ciq_test_report.py39
1 files changed, 27 insertions, 12 deletions
diff --git a/tools/ciq_test_report.py b/tools/ciq_test_report.py
index b35095f..e69b93f 100644
--- a/tools/ciq_test_report.py
+++ b/tools/ciq_test_report.py
@@ -1,23 +1,30 @@
#!/usr/bin/env python3
+# Author: cirakg & claude ai
+# Summary: Decide pass/fail of a Connect IQ unit-test run from its console log, and write JUnit XML.
"""Turn the console output of a Connect IQ unit-test run into a verdict.
-`monkeydo app.prg <device> -t` prints Garmin's "Run No Evil" report:
+`monkeydo app.prg <device> -t` prints Garmin's "Run No Evil" report. Captured
+from SDK 9.2.0 (tools/tests/data/runnoevil_pass.log):
Executing test smokeVersionIsSemver...
- DEBUG (14:16): version 0.1.0
+ DEBUG (18:41): version 0.1.0
PASS
==============================================================================
RESULTS
- Test: Status:
- smokeVersionIsSemver PASS
+ Test: Status:
+ smokeVersionIsSemver PASS
Ran 1 test
- PASSED (failures=0, errors=0)
+ PASSED (passed=1, failed=0, errors=0)
-monkeydo's exit code is reported to be non-zero even when every test passes,
+Garmin's documentation shows the older summary form `PASSED (failures=0,
+errors=0)`; both are accepted.
+
+monkeydo exits with 1 even when every test passes (verified with SDK 9.2.0),
so this script decides instead. A run passes only when all of these hold:
* the RESULTS section and the final PASSED/FAILED line are present (a crash
or a watchdog kill leaves the log incomplete);
- * the final line says PASSED with zero failures and zero errors;
+ * the final line says PASSED with zero failed and zero errors, and its
+ passed count (when present) equals the number of passing rows;
* "Ran N tests" matches the number of per-test rows, and N >= --min-tests;
* every per-test row says PASS.
@@ -36,7 +43,8 @@ from xml.sax.saxutils import escape, quoteattr
EXIT_PASS, EXIT_FAIL, EXIT_INCOMPLETE = 0, 1, 2
-SUMMARY_RE = re.compile(r"^(PASSED|FAILED)\s*\(\s*failures\s*=\s*(\d+)\s*,\s*errors\s*=\s*(\d+)\s*\)", re.I)
+SUMMARY_RE = re.compile(r"^(PASSED|FAILED)\s*\(([^)]*)\)\s*$", re.I)
+COUNT_RE = re.compile(r"(\w+)\s*=\s*(\d+)")
RAN_RE = re.compile(r"^Ran\s+(\d+)\s+tests?\b", re.I)
ROW_RE = re.compile(r"^(\S+)\s+(PASS|FAIL|ERROR)\s*$", re.I)
EXECUTING_RE = re.compile(r"^Executing test\s+(\S+?)(?:\.\.\.|…)?\s*$")
@@ -50,7 +58,8 @@ class Report:
output: dict[str, list[str]] = field(default_factory=dict) # test name -> its console lines
ran: int | None = None
summary: str | None = None # PASSED or FAILED
- failures: int = 0
+ passed: int | None = None # absent in the older summary format
+ failed: int = 0
errors: int = 0
def problems(self, min_tests: int) -> tuple[int, list[str]]:
@@ -58,8 +67,11 @@ class Report:
if self.summary is None or self.ran is None:
return EXIT_INCOMPLETE, ["log has no complete RESULTS section (crash, timeout or watchdog?)"]
reasons = []
- if self.summary != "PASSED" or self.failures or self.errors:
- reasons.append(f"summary says {self.summary} (failures={self.failures}, errors={self.errors})")
+ if self.summary != "PASSED" or self.failed or self.errors:
+ reasons.append(f"summary says {self.summary} (failed={self.failed}, errors={self.errors})")
+ passing_rows = sum(1 for status in self.results.values() if status == "PASS")
+ if self.passed is not None and self.passed != passing_rows:
+ reasons.append(f"summary says passed={self.passed} but {passing_rows} rows say PASS")
if self.ran != len(self.results):
reasons.append(f"'Ran {self.ran}' but {len(self.results)} result rows")
if self.ran < min_tests:
@@ -91,7 +103,10 @@ def parse(text: str) -> Report:
report.ran = int(match.group(1))
elif match := SUMMARY_RE.match(line):
report.summary = match.group(1).upper()
- report.failures, report.errors = int(match.group(2)), int(match.group(3))
+ counts = {key.lower(): int(value) for key, value in COUNT_RE.findall(match.group(2))}
+ report.passed = counts.get("passed")
+ report.failed = counts.get("failed", counts.get("failures", 0))
+ report.errors = counts.get("errors", 0)
return report