diff --git a/scripts/pipeline.py b/scripts/pipeline.py index c8fccc55..aca807ae 100644 --- a/scripts/pipeline.py +++ b/scripts/pipeline.py @@ -40,39 +40,28 @@ def run(cmd: list[str], label: str) -> tuple[bool, str]: return ok, output -def parse_verify_counts(output: str) -> dict[str, tuple[int, int, int, int]]: - """Extract per-group OK/total/wrong/missing from verify output. +def parse_verify_counts(output: str) -> dict[str, tuple[int, int]]: + """Extract per-group OK/total from verify output. - Returns {group_label: (ok, total, wrong, missing)}. - Group label = "Lakka / RetroArch" for grouped platforms. + Matches: "Label: X/Y OK ..." or "Label: X/Y present ..." + Returns {group_label: (ok, total)}. """ + import re counts = {} for line in output.splitlines(): - if " files OK" not in line: - continue - label, rest = line.split(":", 1) - rest = rest.strip() - frac = rest.split(" files OK")[0].strip() - if "/" not in frac: - continue - ok, total = int(frac.split("/")[0]), int(frac.split("/")[1]) - wrong = 0 - missing = 0 - if "wrong hash" in rest: - for part in rest.split(","): - part = part.strip() - if "wrong hash" in part: - wrong = int(part.split()[0]) - elif "missing" in part: - missing = int(part.split()[0]) - counts[label.strip()] = (ok, total, wrong, missing) + m = re.match(r"^(.+?):\s+(\d+)/(\d+)\s+(OK|present)", line) + if m: + label = m.group(1).strip() + ok, total = int(m.group(2)), int(m.group(3)) + for name in label.split(" / "): + counts[name.strip()] = (ok, total) return counts -def parse_pack_counts(output: str) -> dict[str, tuple[int, int, int, int, int]]: - """Extract per-pack files_packed/ok/total/wrong/missing. +def parse_pack_counts(output: str) -> dict[str, tuple[int, int]]: + """Extract per-pack OK/total from generate_pack output. - Returns {pack_label: (packed, ok, total, wrong, missing)}. + Returns {pack_label: (ok, total)}. """ import re counts = {} @@ -82,16 +71,10 @@ def parse_pack_counts(output: str) -> dict[str, tuple[int, int, int, int, int]]: if m: current_label = m.group(1) continue - if "files packed" not in line or "files OK" not in line: - continue - packed = int(re.search(r"(\d+) files packed", line).group(1)) frac_m = re.search(r"(\d+)/(\d+) files OK", line) - ok, total = int(frac_m.group(1)), int(frac_m.group(2)) - wrong_m = re.search(r"(\d+) wrong hash", line) - wrong = int(wrong_m.group(1)) if wrong_m else 0 - miss_m = re.search(r"(\d+) missing", line) - missing = int(miss_m.group(1)) if miss_m else 0 - counts[current_label] = (packed, ok, total, wrong, missing) + if frac_m and "files packed" in line: + ok, total = int(frac_m.group(1)), int(frac_m.group(2)) + counts[current_label] = (ok, total) return counts @@ -102,13 +85,11 @@ def check_consistency(verify_output: str, pack_output: str) -> bool: print("\n--- 5/5 consistency check ---") all_ok = True - matched_verify = set() - for v_label, (v_ok, v_total, v_wrong, v_miss) in sorted(v.items()): - # Match by label overlap (handles "Lakka + RetroArch" vs "Lakka / RetroArch") + for v_label, (v_ok, v_total) in sorted(v.items()): + # Match by name overlap (handles "Lakka + RetroArch" vs "Lakka / RetroArch") p_match = None for p_label in p: - # Check if any platform name in the verify group matches the pack label v_names = {n.strip().lower() for n in v_label.split("/")} p_names = {n.strip().lower() for n in p_label.replace("+", "/").split("/")} if v_names & p_names: @@ -116,25 +97,14 @@ def check_consistency(verify_output: str, pack_output: str) -> bool: break if p_match: - matched_verify.add(v_label) - _, p_ok, p_total, p_wrong, p_miss = p[p_match] - checks_match = v_ok == p_ok and v_total == p_total - detail_match = v_wrong == p_wrong and v_miss == p_miss - if checks_match and detail_match: - print(f" {v_label}: {v_ok}/{v_total} OK") + p_ok, p_total = p[p_match] + if v_ok == p_ok and v_total == p_total: + print(f" {v_label}: verify {v_ok}/{v_total} == pack {p_ok}/{p_total} OK") else: - print(f" {v_label}: MISMATCH") - print(f" verify: {v_ok}/{v_total} OK, {v_wrong} wrong, {v_miss} missing") - print(f" pack: {p_ok}/{p_total} OK, {p_wrong} wrong, {p_miss} missing") + print(f" {v_label}: MISMATCH verify {v_ok}/{v_total} != pack {p_ok}/{p_total}") all_ok = False else: - # Grouped platform — check if another label in the same verify group matched - v_names = [n.strip() for n in v_label.split("/")] - other_matched = any( - name in lbl for lbl in matched_verify for name in v_names - ) - if not other_matched: - print(f" {v_label}: {v_ok}/{v_total} OK (no separate pack — grouped or archived)") + print(f" {v_label}: {v_ok}/{v_total} (no separate pack)") status = "OK" if all_ok else "FAILED" print(f"--- consistency check: {status} ---") diff --git a/scripts/verify.py b/scripts/verify.py index ce9d9a7b..f0306e19 100644 --- a/scripts/verify.py +++ b/scripts/verify.py @@ -368,37 +368,74 @@ def print_platform_result(result: dict, group: list[str]) -> None: total = result["total_files"] c = result["severity_counts"] label = " / ".join(group) + ok_count = c[Severity.OK] + problems = total - ok_count - parts = [f"{c[Severity.OK]}/{total} OK"] - if c[Severity.CRITICAL]: - parts.append(f"{c[Severity.CRITICAL]} CRITICAL") - if c[Severity.WARNING]: - parts.append(f"{c[Severity.WARNING]} warning") - if c[Severity.INFO]: - parts.append(f"{c[Severity.INFO]} info") + # Summary line — platform-native terminology + if mode == "existence": + if problems: + missing = c.get(Severity.WARNING, 0) + c.get(Severity.CRITICAL, 0) + optional_missing = c.get(Severity.INFO, 0) + parts = [f"{ok_count}/{total} present"] + if missing: + parts.append(f"{missing} missing") + if optional_missing: + parts.append(f"{optional_missing} optional missing") + else: + parts = [f"{ok_count}/{total} present"] + else: + untested = c.get(Severity.WARNING, 0) + missing = c.get(Severity.CRITICAL, 0) + parts = [f"{ok_count}/{total} OK"] + if untested: + parts.append(f"{untested} untested") + if missing: + parts.append(f"{missing} missing") print(f"{label}: {', '.join(parts)} [{mode}]") - # Detail non-OK entries + # Detail non-OK entries with required/optional + seen_details = set() for d in result["details"]: if d["status"] == Status.UNTESTED: + key = f"{d['system']}/{d['name']}" + if key in seen_details: + continue + seen_details.add(key) req = "required" if d.get("required", True) else "optional" reason = d.get("reason", "") - print(f" UNTESTED ({req}): {d['system']}/{d['name']} — {reason}") + print(f" UNTESTED ({req}): {key} — {reason}") for d in result["details"]: if d["status"] == Status.MISSING: + key = f"{d['system']}/{d['name']}" + if key in seen_details: + continue + seen_details.add(key) req = "required" if d.get("required", True) else "optional" - print(f" MISSING ({req}): {d['system']}/{d['name']}") + print(f" MISSING ({req}): {key}") - # Cross-reference gaps + # Cross-reference: undeclared files used by cores undeclared = result.get("undeclared_files", []) if undeclared: - print(f" Undeclared files used by cores ({len(undeclared)}):") - for u in undeclared[:20]: - req = "required" if u["required"] else "optional" - loc = "in repo" if u["in_repo"] else "NOT in repo" - print(f" {u['emulator']} → {u['name']} ({req}, {loc})") - if len(undeclared) > 20: - print(f" ... and {len(undeclared) - 20} more") + req_not_in_repo = [u for u in undeclared if u["required"] and not u["in_repo"]] + req_in_repo = [u for u in undeclared if u["required"] and u["in_repo"]] + opt_count = len(undeclared) - len(req_not_in_repo) - len(req_in_repo) + + summary_parts = [] + if req_not_in_repo: + summary_parts.append(f"{len(req_not_in_repo)} required NOT in repo") + if req_in_repo: + summary_parts.append(f"{len(req_in_repo)} required in repo") + summary_parts.append(f"{opt_count} optional") + print(f" Core gaps: {len(undeclared)} undeclared ({', '.join(summary_parts)})") + + # Show only critical gaps (required + not in repo) + for u in req_not_in_repo: + print(f" {u['emulator']} → {u['name']} (required, NOT in repo)") + # Show required in repo (actionable) + for u in req_in_repo[:10]: + print(f" {u['emulator']} → {u['name']} (required, in repo)") + if len(req_in_repo) > 10: + print(f" ... and {len(req_in_repo) - 10} more required in repo") def main():