mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-04-19 07:12:36 -05:00
feat: complete platform-native verification with cross-reference
verify.py output now uses platform-native terminology: - md5 platforms: X/Y OK, N untested, M missing - existence platforms: X/Y present, M missing Each problem shows (required/optional) from platform YAML. Core gaps section summarizes undeclared files by severity: - required NOT in repo: critical gaps needing sourcing - required in repo: can be added to platform config - optional: informational Consistency check in pipeline.py updated to match new format. All 7 platforms verified, consistency OK across verify and pack.
This commit is contained in:
+24
-54
@@ -40,39 +40,28 @@ def run(cmd: list[str], label: str) -> tuple[bool, str]:
|
|||||||
return ok, output
|
return ok, output
|
||||||
|
|
||||||
|
|
||||||
def parse_verify_counts(output: str) -> dict[str, tuple[int, int, int, int]]:
|
def parse_verify_counts(output: str) -> dict[str, tuple[int, int]]:
|
||||||
"""Extract per-group OK/total/wrong/missing from verify output.
|
"""Extract per-group OK/total from verify output.
|
||||||
|
|
||||||
Returns {group_label: (ok, total, wrong, missing)}.
|
Matches: "Label: X/Y OK ..." or "Label: X/Y present ..."
|
||||||
Group label = "Lakka / RetroArch" for grouped platforms.
|
Returns {group_label: (ok, total)}.
|
||||||
"""
|
"""
|
||||||
|
import re
|
||||||
counts = {}
|
counts = {}
|
||||||
for line in output.splitlines():
|
for line in output.splitlines():
|
||||||
if " files OK" not in line:
|
m = re.match(r"^(.+?):\s+(\d+)/(\d+)\s+(OK|present)", line)
|
||||||
continue
|
if m:
|
||||||
label, rest = line.split(":", 1)
|
label = m.group(1).strip()
|
||||||
rest = rest.strip()
|
ok, total = int(m.group(2)), int(m.group(3))
|
||||||
frac = rest.split(" files OK")[0].strip()
|
for name in label.split(" / "):
|
||||||
if "/" not in frac:
|
counts[name.strip()] = (ok, total)
|
||||||
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)
|
|
||||||
return counts
|
return counts
|
||||||
|
|
||||||
|
|
||||||
def parse_pack_counts(output: str) -> dict[str, tuple[int, int, int, int, int]]:
|
def parse_pack_counts(output: str) -> dict[str, tuple[int, int]]:
|
||||||
"""Extract per-pack files_packed/ok/total/wrong/missing.
|
"""Extract per-pack OK/total from generate_pack output.
|
||||||
|
|
||||||
Returns {pack_label: (packed, ok, total, wrong, missing)}.
|
Returns {pack_label: (ok, total)}.
|
||||||
"""
|
"""
|
||||||
import re
|
import re
|
||||||
counts = {}
|
counts = {}
|
||||||
@@ -82,16 +71,10 @@ def parse_pack_counts(output: str) -> dict[str, tuple[int, int, int, int, int]]:
|
|||||||
if m:
|
if m:
|
||||||
current_label = m.group(1)
|
current_label = m.group(1)
|
||||||
continue
|
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)
|
frac_m = re.search(r"(\d+)/(\d+) files OK", line)
|
||||||
ok, total = int(frac_m.group(1)), int(frac_m.group(2))
|
if frac_m and "files packed" in line:
|
||||||
wrong_m = re.search(r"(\d+) wrong hash", line)
|
ok, total = int(frac_m.group(1)), int(frac_m.group(2))
|
||||||
wrong = int(wrong_m.group(1)) if wrong_m else 0
|
counts[current_label] = (ok, total)
|
||||||
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)
|
|
||||||
return counts
|
return counts
|
||||||
|
|
||||||
|
|
||||||
@@ -102,13 +85,11 @@ def check_consistency(verify_output: str, pack_output: str) -> bool:
|
|||||||
|
|
||||||
print("\n--- 5/5 consistency check ---")
|
print("\n--- 5/5 consistency check ---")
|
||||||
all_ok = True
|
all_ok = True
|
||||||
matched_verify = set()
|
|
||||||
|
|
||||||
for v_label, (v_ok, v_total, v_wrong, v_miss) in sorted(v.items()):
|
for v_label, (v_ok, v_total) in sorted(v.items()):
|
||||||
# Match by label overlap (handles "Lakka + RetroArch" vs "Lakka / RetroArch")
|
# Match by name overlap (handles "Lakka + RetroArch" vs "Lakka / RetroArch")
|
||||||
p_match = None
|
p_match = None
|
||||||
for p_label in p:
|
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("/")}
|
v_names = {n.strip().lower() for n in v_label.split("/")}
|
||||||
p_names = {n.strip().lower() for n in p_label.replace("+", "/").split("/")}
|
p_names = {n.strip().lower() for n in p_label.replace("+", "/").split("/")}
|
||||||
if v_names & p_names:
|
if v_names & p_names:
|
||||||
@@ -116,25 +97,14 @@ def check_consistency(verify_output: str, pack_output: str) -> bool:
|
|||||||
break
|
break
|
||||||
|
|
||||||
if p_match:
|
if p_match:
|
||||||
matched_verify.add(v_label)
|
p_ok, p_total = p[p_match]
|
||||||
_, p_ok, p_total, p_wrong, p_miss = p[p_match]
|
if v_ok == p_ok and v_total == p_total:
|
||||||
checks_match = v_ok == p_ok and v_total == p_total
|
print(f" {v_label}: verify {v_ok}/{v_total} == pack {p_ok}/{p_total} OK")
|
||||||
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")
|
|
||||||
else:
|
else:
|
||||||
print(f" {v_label}: MISMATCH")
|
print(f" {v_label}: MISMATCH verify {v_ok}/{v_total} != pack {p_ok}/{p_total}")
|
||||||
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")
|
|
||||||
all_ok = False
|
all_ok = False
|
||||||
else:
|
else:
|
||||||
# Grouped platform — check if another label in the same verify group matched
|
print(f" {v_label}: {v_ok}/{v_total} (no separate pack)")
|
||||||
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)")
|
|
||||||
|
|
||||||
status = "OK" if all_ok else "FAILED"
|
status = "OK" if all_ok else "FAILED"
|
||||||
print(f"--- consistency check: {status} ---")
|
print(f"--- consistency check: {status} ---")
|
||||||
|
|||||||
+55
-18
@@ -368,37 +368,74 @@ def print_platform_result(result: dict, group: list[str]) -> None:
|
|||||||
total = result["total_files"]
|
total = result["total_files"]
|
||||||
c = result["severity_counts"]
|
c = result["severity_counts"]
|
||||||
label = " / ".join(group)
|
label = " / ".join(group)
|
||||||
|
ok_count = c[Severity.OK]
|
||||||
|
problems = total - ok_count
|
||||||
|
|
||||||
parts = [f"{c[Severity.OK]}/{total} OK"]
|
# Summary line — platform-native terminology
|
||||||
if c[Severity.CRITICAL]:
|
if mode == "existence":
|
||||||
parts.append(f"{c[Severity.CRITICAL]} CRITICAL")
|
if problems:
|
||||||
if c[Severity.WARNING]:
|
missing = c.get(Severity.WARNING, 0) + c.get(Severity.CRITICAL, 0)
|
||||||
parts.append(f"{c[Severity.WARNING]} warning")
|
optional_missing = c.get(Severity.INFO, 0)
|
||||||
if c[Severity.INFO]:
|
parts = [f"{ok_count}/{total} present"]
|
||||||
parts.append(f"{c[Severity.INFO]} info")
|
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}]")
|
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"]:
|
for d in result["details"]:
|
||||||
if d["status"] == Status.UNTESTED:
|
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"
|
req = "required" if d.get("required", True) else "optional"
|
||||||
reason = d.get("reason", "")
|
reason = d.get("reason", "")
|
||||||
print(f" UNTESTED ({req}): {d['system']}/{d['name']} — {reason}")
|
print(f" UNTESTED ({req}): {key} — {reason}")
|
||||||
for d in result["details"]:
|
for d in result["details"]:
|
||||||
if d["status"] == Status.MISSING:
|
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"
|
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", [])
|
undeclared = result.get("undeclared_files", [])
|
||||||
if undeclared:
|
if undeclared:
|
||||||
print(f" Undeclared files used by cores ({len(undeclared)}):")
|
req_not_in_repo = [u for u in undeclared if u["required"] and not u["in_repo"]]
|
||||||
for u in undeclared[:20]:
|
req_in_repo = [u for u in undeclared if u["required"] and u["in_repo"]]
|
||||||
req = "required" if u["required"] else "optional"
|
opt_count = len(undeclared) - len(req_not_in_repo) - len(req_in_repo)
|
||||||
loc = "in repo" if u["in_repo"] else "NOT in repo"
|
|
||||||
print(f" {u['emulator']} → {u['name']} ({req}, {loc})")
|
summary_parts = []
|
||||||
if len(undeclared) > 20:
|
if req_not_in_repo:
|
||||||
print(f" ... and {len(undeclared) - 20} more")
|
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():
|
def main():
|
||||||
|
|||||||
Reference in New Issue
Block a user