diff --git a/scripts/verify.py b/scripts/verify.py index d740e96f2..3b1d73b6e 100644 --- a/scripts/verify.py +++ b/scripts/verify.py @@ -29,6 +29,7 @@ import contextlib import json import os import sys +from collections import Counter from collections.abc import Mapping from pathlib import Path @@ -1800,6 +1801,19 @@ def _core_required_missing(bios_files: list[dict]) -> list[dict]: ] +def _omission_reasons(entries: list[dict]) -> str: + """Each reason a held file was left out, with its count when several. + + Printing the first entry's reason for the whole count named one cause + for 412 RomM files when 20 carried it. + """ + counts = Counter(u.get("omitted", "not placed") for u in entries) + if len(counts) == 1: + return next(iter(counts)) + ranked = sorted(counts.items(), key=lambda item: (-item[1], item[0])) + return ", ".join(f"{count} {reason}" for reason, count in ranked) + + def _print_undeclared_section(result: dict, verbose: bool) -> None: """Print cross-reference section for undeclared files used by cores.""" undeclared = result.get("undeclared_files", []) @@ -1843,7 +1857,8 @@ def _print_undeclared_section(result: dict, verbose: bool) -> None: names = ", ".join(sorted({u["name"] for u in bundled})) print(f" Bundled with their emulator, not collected: {names}") if unplaced: - print(f" Core files held but not packed: {len(unplaced)} ({unplaced[0]['omitted']})") + print(f" Core files held but not packed: {len(unplaced)} " + f"({_omission_reasons(unplaced)})") for u in req_not_in_repo: _print_undeclared_entry(u, "MISSING (required)", verbose) @@ -1869,7 +1884,7 @@ def _print_undeclared_section(result: dict, verbose: bool) -> None: if gd_unplaced: print( f" Game data held but not packed: {len(gd_unplaced)} " - f"({gd_unplaced[0].get('omitted', 'not placed')})" + f"({_omission_reasons(gd_unplaced)})" ) diff --git a/tests/test_tool_messages.py b/tests/test_tool_messages.py index 1d97f3b7c..a866120e2 100644 --- a/tests/test_tool_messages.py +++ b/tests/test_tool_messages.py @@ -157,6 +157,44 @@ class EveryPlatformInTheSummaryCarriesItsCount(unittest.TestCase): self.assertIn("3 warning (Lakka 2, ROCKNIX 1)", summary) +class HeldButNotPackedNamesEachReason(unittest.TestCase): + """'Core files held but not packed: 16 (a packed file occupies its + directory)' on Lakka, where 14 of the 16 are installed with their + emulator: the first entry's reason stood for the whole count.""" + + @staticmethod + def _entry(name: str, reason: str, category: str = "bios") -> dict: + return {"name": name, "emulator": "x", "required": True, "in_repo": True, + "in_pack": False, "omitted": reason, "category": category} + + def _printed(self, entries: list[dict]) -> str: + import contextlib # noqa: PLC0415 + import io # noqa: PLC0415 + + out = io.StringIO() + with contextlib.redirect_stdout(out): + verify._print_undeclared_section({"undeclared_files": entries}, False) + return out.getvalue() + + def test_each_reason_carries_its_count(self): + entries = [self._entry("a.rom", "a packed file occupies its directory")] + entries += [self._entry(f"{i}.rom", "installed with its emulator") for i in range(3)] + entries += [self._entry(f"g{i}", "installed with its emulator", "game_data") + for i in range(2)] + entries.append(self._entry("g9", "no platform slug for its system", "game_data")) + text = self._printed(entries) + self.assertIn( + "Core files held but not packed: 4 (3 installed with its emulator, " + "1 a packed file occupies its directory)", text) + self.assertIn( + "Game data held but not packed: 3 (2 installed with its emulator, " + "1 no platform slug for its system)", text) + + def test_a_single_reason_reads_as_before(self): + text = self._printed([self._entry("a.rom", "installed with its emulator")] * 2) + self.assertIn("Core files held but not packed: 2 (installed with its emulator)", text) + + class OneSpellingPerOption(unittest.TestCase): """verify.py -p batocera worked and slots.py -p batocera was refused.""" diff --git a/wiki/reading-reports.md b/wiki/reading-reports.md index 159fef7b7..c6ce6e212 100644 --- a/wiki/reading-reports.md +++ b/wiki/reading-reports.md @@ -117,7 +117,8 @@ RetroDECK: 2007/2008 OK, 1 untested [md5] The run prints more `DISCREPANCY` lines than this excerpt shows. The core and game data lines count what the emulator cores add beyond the platform's -own list. A file held but not packed is left out for the reason in parentheses. +own list. A file held but not packed is left out for the reason in parentheses, each +reason with its count when the files have several. `--verbose` also lists the passing entries that an emulator validates, with the check and the source lines behind it: