From bc2233e725519204fd21bea20f4b3686888aa843 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Wed, 21 Oct 2026 06:57:49 +0200 Subject: [PATCH] fix: write truth systems in a stable order Systems a core covers without a file were added while walking a set, so the hash seed placed them and two runs wrote dist/truth/.yml in two orders. The normalized system map was built from a set too. --- scripts/truth.py | 13 +++++---- tests/test_truth_layout.py | 54 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 5 deletions(-) diff --git a/scripts/truth.py b/scripts/truth.py index 13116bb5..794e7575 100644 --- a/scripts/truth.py +++ b/scripts/truth.py @@ -550,10 +550,12 @@ def generate_platform_truth( if fname: declared_by_name.setdefault(fname, []).append((psid, fe)) - # Normalized ID -> platform system ID + # Normalized ID -> platform system ID. Walked in the platform's order, + # not the set's: two IDs normalizing alike went to whichever the hash + # seed put last. norm_to_platform: dict[str, str] = {} - for psid in platform_sys_ids: - norm_to_platform[_norm_system_id(psid)] = psid + for psid in config.get("systems", {}): + norm_to_platform.setdefault(_norm_system_id(psid), psid) def _map_sys_id(profile_sid: str) -> str: """Map a profile system ID to the platform's system ID.""" @@ -669,8 +671,9 @@ def generate_platform_truth( # Ensure all systems of resolved cores have entries (even with 0 files). # This documents that the system is covered: the core was analyzed and - # needs no external files for this system. - for emu_name in cores_profiled: + # needs no external files for this system. Sorted: the set's order is + # the hash seed's, and it decided where each empty system was written. + for emu_name in sorted(cores_profiled): profile = profiles[emu_name] for prof_sid in profile.get("systems", []): sys_id = _map_sys_id(prof_sid) diff --git a/tests/test_truth_layout.py b/tests/test_truth_layout.py index fff48cea..73f514d2 100644 --- a/tests/test_truth_layout.py +++ b/tests/test_truth_layout.py @@ -8,6 +8,10 @@ out on RetroBat, EmuDeck and RetroDECK. from __future__ import annotations +import json +import os +import random +import subprocess import sys import unittest from pathlib import Path @@ -400,5 +404,55 @@ class TruthKnowsWhatTheEmulatorInstalls(unittest.TestCase): self.assertEqual(names, []) +class TheModelIsAFunctionOfItsInputs(unittest.TestCase): + """Two runs on the same inputs wrote dist/truth/.yml with its + systems in two orders. The systems a core covers without a file were + added by walking a set, so the hash seed decided where each was written. + """ + + NAMES = [f"core{i:02d}" for i in range(16)] + SCRIPT = ( + "import json, sys\n" + "sys.path.insert(0, sys.argv[1])\n" + "from truth import generate_platform_truth\n" + "names = [f'core{i:02d}' for i in range(16)]\n" + "profiles = {n: {'emulator': n, 'type': 'libretro',\n" + " 'systems': [f'maker-system-{n}'], 'files': []} for n in names}\n" + "config = {'cores': names,\n" + " 'systems': {f'maker-system-{n}': {'files': []} for n in names}}\n" + "print(json.dumps(generate_platform_truth('p', config, {}, profiles)))\n" + ) + + def _model(self, seed: str) -> str: + proc = subprocess.run( + [sys.executable, "-c", self.SCRIPT, str(REPO_ROOT / "scripts")], + env={**os.environ, "PYTHONHASHSEED": seed}, + capture_output=True, text=True, timeout=120, check=True, + ) + return proc.stdout + + def test_the_hash_seed_changes_nothing(self): + first = self._model("0") + for seed in ("1", "2", "3", "4"): + with self.subTest(seed=seed): + self.assertEqual(self._model(seed), first) + + def test_the_profile_order_changes_nothing(self): + profiles = { + n: {"emulator": n, "type": "libretro", "systems": [f"maker-system-{n}"], + "files": [{"name": f"{n}.bin", "system": f"maker-system-{n}", + "md5": f"{i:032x}"}]} + for i, n in enumerate(self.NAMES) + } + config = {"cores": self.NAMES, + "systems": {f"maker-system-{n}": {"files": []} for n in self.NAMES}} + expected = json.dumps(generate_platform_truth("p", config, {}, profiles)) + shuffled = list(profiles.items()) + random.Random(7).shuffle(shuffled) + self.assertEqual( + json.dumps(generate_platform_truth("p", config, {}, dict(shuffled))), expected + ) + + if __name__ == "__main__": unittest.main()