From cedf6ccbdd469d47e56691e8766f260527dd33a6 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Fri, 16 Oct 2026 15:48:33 +0200 Subject: [PATCH] fix: count size and cleared hash changes --- scripts/export_native.py | 100 +++++++++++++++++++++++++++--------- tests/test_export_counts.py | 31 +++++++++++ 2 files changed, 108 insertions(+), 23 deletions(-) diff --git a/scripts/export_native.py b/scripts/export_native.py index df106445..8997647c 100644 --- a/scripts/export_native.py +++ b/scripts/export_native.py @@ -20,6 +20,7 @@ import json import sys import urllib.error import urllib.request +from dataclasses import dataclass, field from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parent)) @@ -27,7 +28,7 @@ sys.path.insert(0, str(Path(__file__).resolve().parent)) from artifacts import file_lock, write_bytes_atomic, write_text_atomic from common import list_registered_platforms, load_platform_config, yaml_load from exporter import discover_exporters -from exporter.baseline import build_native_model +from exporter.baseline import HASH_FIELDS, _hash_values, _single_size, build_native_model DEFAULT_CACHE = ".cache/upstream-native" SOURCES_INDEX = ".sources.json" @@ -255,6 +256,65 @@ def refresh_cache( return True, [f"{len(wanted)} file(s) fetched"] +@dataclass +class LandedChanges: + """What an export changed in the platform's file, field by field.""" + + applied: list[str] = field(default_factory=list) + filled: list[str] = field(default_factory=list) + resized: list[str] = field(default_factory=list) + cleared: list[str] = field(default_factory=list) + requirements: int = 0 + + +def landed_changes(exporter: object, systems: dict) -> LandedChanges: + """The changes the written file carries. + + Only what the format states: a correction to a field the file has no + place for, or one its render keeps out, is not a change the maintainer + will find in the diff, and counting it would announce work the export + did not do. A hash written where the platform left none changes the + check from existence to content, so it is counted too. The truth taking + over an entry also rewrites its size and drops the platform's hashes it + does not declare: both are in the diff. + """ + changes = LandedChanges() + for system in systems.values(): + for entry in system.files: + label = f"{entry.native_system}/{entry.name}" + for field_name in entry.corrections: + if not exporter.states(entry, field_name): + continue + if field_name == "required": + changes.requirements += 1 + else: + changes.applied.append(f"{label} {field_name}") + changes.filled.extend( + f"{label} {field_name}" + for field_name in entry.filled + if exporter.states(entry, field_name) + ) + if entry.platform is None: + continue + theirs = entry.platform.get("size") + ours = entry.size() + if ( + theirs + and ours + and _single_size(theirs) not in (None, ours) + and exporter.states(entry, "size") + ): + changes.resized.append(label) + changes.cleared.extend( + f"{label} {field_name}" + for field_name in HASH_FIELDS + if _hash_values(entry.platform, field_name) + and not entry.hashes(field_name) + and exporter.states(entry, field_name) + ) + return changes + + def export_platform( platform: str, exporter_class: type, @@ -302,28 +362,10 @@ def export_platform( path.parent.mkdir(parents=True, exist_ok=True) path.write_text(content, encoding="utf-8") - # Only what the format states: a correction to a field the file has no - # place for, or one its render keeps out, is not a change the maintainer - # will find in the diff, and counting it would announce work the export - # did not do. A hash written where the platform left none changes the - # check from existence to content, so it is counted too. - applied: list[str] = [] - filled: list[str] = [] - requirements = 0 - for system in systems.values(): - for entry in system.files: - for field_name in entry.corrections: - if not exporter.states(entry, field_name): - continue - if field_name == "required": - requirements += 1 - else: - applied.append(f"{entry.native_system}/{entry.name} {field_name}") - filled.extend( - f"{entry.native_system}/{entry.name} {field_name}" - for field_name in entry.filled - if exporter.states(entry, field_name) - ) + changes = landed_changes(exporter, systems) + applied, filled = changes.applied, changes.filled + resized, cleared = changes.resized, changes.cleared + requirements = changes.requirements landed = 0 refused = 0 @@ -349,6 +391,10 @@ def export_platform( ) if filled: summary += f", {len(filled)} hashes filled" + if resized: + summary += f", {len(resized)} sizes corrected" + if cleared: + summary += f", {len(cleared)} hashes cleared" if refused: summary += f", {refused} the format cannot state" if lost: @@ -362,6 +408,14 @@ def export_platform( messages.append(f"hash filled: {fill}") if len(filled) > 5: messages.append(f"and {len(filled) - 5} more hash fills") + for label in resized[:5]: + messages.append(f"size corrected: {label}") + if len(resized) > 5: + messages.append(f"and {len(resized) - 5} more size corrections") + for clear in cleared[:5]: + messages.append(f"hash cleared: {clear}") + if len(cleared) > 5: + messages.append(f"and {len(cleared) - 5} more hashes cleared") messages.extend(f"INVALID: {issue}" for issue in issues[:10]) if len(issues) > 10: diff --git a/tests/test_export_counts.py b/tests/test_export_counts.py index a32ccd08..25ce79c6 100644 --- a/tests/test_export_counts.py +++ b/tests/test_export_counts.py @@ -592,3 +592,34 @@ class BatoceraKeepsTheRunnerKeys(unittest.TestCase): if __name__ == "__main__": unittest.main() + + +class SummaryCountsWhatTheTruthRewrites(unittest.TestCase): + """A size and a dropped hash are in the diff, so they are counted.""" + + def _entry(self): + # The sha1s contradict, so the truth describes the file alone: its + # size replaces the platform's and the platform's crc32 is dropped. + return NativeFile( + "neocdz.zip", "neocdz.zip", "neocd", + platform={"size": 953078, "crc32": "a034e470", "sha1": "2" * 40}, + truth={"size": 955352, "sha1": "7" * 40}, + corrections=["sha1"], + ) + + def test_a_format_with_the_fields_counts_them(self): + from export_native import landed_changes # noqa: PLC0415 + + entry = self._entry() + changes = landed_changes(SystemDat(), {"neocd": NativeSystem("neocd", files=[entry])}) + self.assertEqual(changes.resized, ["neocd/neocdz.zip"]) + self.assertEqual(changes.cleared, ["neocd/neocdz.zip crc32"]) + self.assertEqual(changes.applied, ["neocd/neocdz.zip sha1"]) + + def test_a_format_without_them_does_not(self): + from export_native import landed_changes # noqa: PLC0415 + from exporter.batocera_exporter import Exporter as Batocera # noqa: PLC0415 + + entry = self._entry() + changes = landed_changes(Batocera(), {"neocd": NativeSystem("neocd", files=[entry])}) + self.assertEqual((changes.resized, changes.cleared), ([], []))