diff --git a/scripts/export_native.py b/scripts/export_native.py index c5a2b09e..c920899e 100644 --- a/scripts/export_native.py +++ b/scripts/export_native.py @@ -685,36 +685,45 @@ def export_platform( own_summary = exporter.outcome(systems, produced) summary = own_summary if summary is None: - summary = ( - f"{report.files_kept} kept, {landed} added, " - f"{_count(len(applied), 'hash')} corrected, " - f"{_count(requirements, 'requirement')} corrected" + # What the written file carries, in the terms the exports page uses. + # What it does not carry is counted on a line of its own below. + parts = [f"{report.files_kept} kept"] + landed_counts = ( + (landed, "file", "", "added"), + (len(applied), "hash", "", "updated to the emulator source"), + (requirements, "required flag", "", "aligned with the code"), + (len(filled), "hash", "", "added to an existing entry"), + (len(resized), "size", "", "updated to the emulator source"), + (len(unsized), "size", "", "removed"), + (len(cleared), "hash", "", "removed"), + (lost, "entry", "entries", "removed, the format cannot state them"), ) - if filled: - summary += f", {_count(len(filled), 'hash')} filled" - if resized: - summary += f", {_count(len(resized), 'size')} corrected" - if unsized: - summary += f", {_count(len(unsized), 'size')} removed" - if cleared: - summary += f", {_count(len(cleared), 'hash')} cleared" - if refused: - summary += f", {_count(refused, 'addition')} left out" - if lost: - summary += f", {_count(lost, 'platform entry', 'platform entries')} the format cannot keep" + parts += [ + f"{_count(number, noun, plural)} {verb}" + for number, noun, plural, verb in landed_counts + if number + ] + summary = ", ".join(parts) withheld = exporter.withheld() - if withheld: - summary += f", {_count(len(withheld), 'correction')} withheld" - if report.labels: - # A content-scanning core's names are labels: a list checked by path - # cannot hold them. - summary += f", {_count(report.labels, 'entry', 'entries')} found by content not listed" + unwritten = [ + f"{_count(number, noun, plural)} {what}" + for number, noun, plural, what in ( + (refused, "file", "", "the format or its checker cannot carry"), + (len(withheld), "update", "", "withheld"), + # A content-scanning core's names are labels: a list checked + # by path cannot hold them. + (report.labels, "entry", "entries", "found by content"), + ) + if number + ] # Reported, never removed: the platform's file keeps every entry. candidates = removal_candidates( systems, scraped, load_emulator_profiles("emulators"), load_data_dir_registry(platforms_dir), ) messages.append(summary) + if unwritten: + messages.append("not in the file: " + ", ".join(unwritten)) if candidates: open_question = sum(c["verdict"] == "undetermined" for c in candidates) messages.append( @@ -723,26 +732,16 @@ def export_platform( + ", listed in the report" ) messages.extend(f"withheld: {reason}" for reason in withheld) - for correction in applied[:5]: - messages.append(f"hash corrected: {correction}") - if len(applied) > 5: - messages.append(f"and {_count(len(applied) - 5, 'more hash correction')}") - for fill in filled[:5]: - messages.append(f"hash filled: {fill}") - if len(filled) > 5: - messages.append(f"and {_count(len(filled) - 5, 'more hash fill')}") - for label in resized[:5]: - messages.append(f"size corrected: {label}") - if len(resized) > 5: - messages.append(f"and {_count(len(resized) - 5, 'more size correction')}") - for label in unsized[:5]: - messages.append(f"size removed: {label}") - if len(unsized) > 5: - messages.append(f"and {len(unsized) - 5} more removed") - for clear in cleared[:5]: - messages.append(f"hash cleared: {clear}") - if len(cleared) > 5: - messages.append(f"and {len(cleared) - 5} more cleared") + for heading, labels in ( + ("hash updated", applied), + ("hash added", filled), + ("size updated", resized), + ("size removed", unsized), + ("hash removed", cleared), + ): + messages.extend(f"{heading}: {label}" for label in labels[:5]) + if len(labels) > 5: + messages.append(f"and {len(labels) - 5} more") write_report(output_dir / f"{platform}.json", { "platform": platform, diff --git a/tests/test_export_report.py b/tests/test_export_report.py index 135e80d5..7008c4c8 100644 --- a/tests/test_export_report.py +++ b/tests/test_export_report.py @@ -26,7 +26,7 @@ def _report(platform: str, files: list[str], **changes: object) -> dict: "platform": platform, "originals": {f: f"https://example.org/{platform}/{f}" for f in files}, "files": files, - "summary": "3 kept, 1 added, 7 hashes corrected, 0 requirements corrected", + "summary": "3 kept, 1 file added, 7 hashes updated to the emulator source", "hashes_corrected": [], "hashes_filled": [], "sizes_corrected": [], "sizes_removed": [], "hashes_cleared": [], "requirements_corrected": 0, "withheld": [], "invalid": [], @@ -157,6 +157,75 @@ class ThePageAddressesMaintainersAsPeers(unittest.TestCase): self.assertNotIn(word, page) +class TheRunReportStatesWhatTheFileCarries(unittest.TestCase): + """exports/.json, the report beside the file offered to a + maintainer, said "82 hashes corrected" and "854 additions left out": + the words the exports page avoids, and counts of things not in the file + mixed into the summary of what is.""" + + JUDGING = ("their", "corrected", "wrong", "drift", "left out", "never seen") + + def test_summary_and_messages(self): + import json # noqa: PLC0415 + from unittest import mock # noqa: PLC0415 + + from exporter.base_exporter import BaseExporter # noqa: PLC0415 + + class Plain(BaseExporter): + @staticmethod + def platform_name() -> str: + return "plat" + + @staticmethod + def native_filename() -> str: + return "list.json" + + @staticmethod + def requires() -> str: + return "md5" + + def render(self, systems, report, originals, scraped=None): + return {"list.json": json.dumps(sorted( + fe.name for system in systems.values() + for fe in system.files if self.writable(fe) + ))} + + def validate(self, systems, produced): + return [] + + truth = {"systems": {"psx": {"files": [ + {"name": "a.bin", "md5": "b" * 32, "validation": ["md5"]}, + {"name": "new.bin", "md5": "c" * 32}, + {"name": "nohash.bin", "size": 16}, + ]}}} + scraped = {"systems": {"psx": {"native_id": "psx", "files": [ + {"name": "a.bin", "destination": "a.bin", "native_system": "psx", + "md5": "a" * 32}, + ]}}} + with tempfile.TemporaryDirectory(dir=ROOT / "tmp") as scratch, \ + mock.patch.object(export_native, "load_inputs", + return_value=(truth, scraped)), \ + mock.patch.object(export_native, "unpinned", return_value=[]), \ + mock.patch.object(export_native, "removal_candidates", return_value=[]): + ok, messages = export_native.export_platform( + "plat", Plain, Path(scratch), Path(scratch) / "out", + str(ROOT / "platforms"), Path(scratch) / "cache", False, + ) + self.assertTrue(ok, messages) + report = json.loads((Path(scratch) / "out" / "plat.json").read_text()) + self.assertEqual( + report["summary"], + "1 kept, 1 file added, 1 hash updated to the emulator source", + ) + self.assertIn( + "not in the file: 1 file the format or its checker cannot carry", messages + ) + self.assertIn("hash updated: psx/a.bin md5", messages) + for text in [report["summary"], *messages]: + for word in self.JUDGING: + self.assertNotIn(word, text.lower()) + + UPSTREAM_CACHE = ROOT / ".cache" / "upstream-native" _DAT_ROM = re.compile(r"^\trom \( name (\"[^\"]+\"|\S+) (.*) \)$") _DAT_FIELDS = {"size": "size", "crc": "crc32", "md5": "md5", "sha1": "sha1"}