From 377e53f4c00510eac577ab230438cc4d8d65f724 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Wed, 21 Oct 2026 10:05:18 +0200 Subject: [PATCH] feat: show load_from, built_by and pin fields load_from, found_by, on_mismatch, built_by, pin_frozen and version_source were in the profiles and on no page: Dolphin's gba_bios.bin showed a path under system/ although the libretro core reads it from its save directory, and mame0278 did not say only Recalbox ships it. --- scripts/generate_site.py | 85 ++++++++++++++++++++++++++++++++ tests/test_site_emulator_page.py | 73 +++++++++++++++++++++++++++ wiki/testing-guide.md | 2 +- 3 files changed, 159 insertions(+), 1 deletion(-) create mode 100644 tests/test_site_emulator_page.py diff --git a/scripts/generate_site.py b/scripts/generate_site.py index b6c99522..21325f7e 100644 --- a/scripts/generate_site.py +++ b/scripts/generate_site.py @@ -2978,6 +2978,83 @@ def _first_hash_value(entry: dict) -> str: return "" +_LOAD_FROM = { + "system_dir": "the system (BIOS) directory", + "save_dir": "the save directory, not the BIOS directory", + "content_dir": "the folder of the loaded content, not the BIOS directory", + "working_directory": "the working directory, not the BIOS directory", + "data_dir": "the emulator's data directory, not the BIOS directory", + "hdd_image": "inside the emulated hard disk image, not the BIOS directory", + "internal_storage": "the app's internal storage, not the BIOS directory", +} + + +def _by_build(value: object, words: Callable[[str], str]) -> str: + """A field keyed by build, said per build, or a plain value said once.""" + if isinstance(value, dict): + return ", ".join(f"{words(str(v))} ({mode} build)" for mode, v in value.items() if v) + return words(str(value)) if value else "" + + +def _load_from_text(value: object) -> str: + """Where the code reads a file, in words. + + The card showed the Path alone: Dolphin's libretro core reads + gba_bios.bin from its save directory, and a reader put it under system/. + """ + if value in (None, "", "system_dir"): + return "" + return _by_build(value, lambda v: _LOAD_FROM.get(v, _code(v))) + + +def _found_by_text(value: object) -> str: + """How the code finds a file: by the name it opens, or by content.""" + words = { + "name": "file name (the code opens this name)", + "content": "content (the code scans the folder and identifies the file " + "by its hash, any file name works)", + } + return _by_build(value, lambda v: words.get(v, _code(v))) + + +def _on_mismatch_lines(value: object) -> list[str]: + """What the code does when a declared check fails, where it is not reject.""" + said = {"warn": "warns and runs with the file", "reject": "rejects the file"} + if value == "warn": + return [f"On a failed check: {said['warn']}"] + if isinstance(value, dict): + return [f"On a failed {check} check: {said.get(str(how), _code(str(how)))}" + for check, how in value.items()] + return [] + + +def _build_rows(profile: dict) -> list[str]: + """Meta card rows for who ships the build, how its files are found, why + its revision is held and where its releases are announced.""" + rows = [] + built_by = profile.get("built_by") or [] + if built_by: + names = ", ".join(f"[{p}](../platforms/{p}.md)" for p in map(str, built_by)) + rows.append(f"| Built by | {names}, under its core name. The libretro " + "buildbot does not publish this build. |") + found = _found_by_text(profile.get("found_by")) + if found: + rows.append(f"| Files found by | {found} |") + frozen = profile.get("pin_frozen") + if frozen: + held = _escape_tags(' '.join(str(frozen).split())).replace('|', r'\|') + rows.append(f"| Revision held | {held} |") + announced = profile.get("version_source") + if isinstance(announced, dict) and announced.get("url"): + url = str(announced["url"]) + row = f"| Releases announced at | [{url}]({url})" + if announced.get("no_genuine_copy"): + row += (f" (version {_escape_tags(str(announced['no_genuine_copy']))} " + "announced there, found only as rebuilt copies)") + rows.append(row + " |") + return rows + + def _render_emulator_file( f: dict, profile: dict, @@ -3042,6 +3119,12 @@ def _render_emulator_file( # under Firmware/, and a page naming only the core's path sent its # users to the wrong folder. details.append(f"Standalone path: `{standalone_path}`") + read_from = _load_from_text(f.get("load_from")) + if read_from: + details.append(f"Read from: {read_from}") + found = _found_by_text(f.get("found_by")) + if found: + details.append(f"Found by: {found}") if fsystem: details.append(f"System: {_system_link(fsystem, '../')}") if size: @@ -3099,6 +3182,7 @@ def _render_emulator_file( elif isinstance(validation, dict): for scope, checks in validation.items(): details.append(f"Validation ({scope}): {', '.join(checks)}") + details.extend(_on_mismatch_lines(f.get("on_mismatch"))) if f.get("installed_on"): details.append("Installed by the platform's own package on: " + ", ".join(sorted(f["installed_on"]))) @@ -3622,6 +3706,7 @@ def generate_emulator_page( revision = revision_markdown(profile) if revision: lines.append(f"| Revision | {revision} |") + lines.extend(_build_rows(profile)) lines.append(f"| Profiled | {profiled} |") # The YAML is the reusable document this site renders, and the JSON is the # same profile for a program. diff --git a/tests/test_site_emulator_page.py b/tests/test_site_emulator_page.py new file mode 100644 index 00000000..8f0e60af --- /dev/null +++ b/tests/test_site_emulator_page.py @@ -0,0 +1,73 @@ +"""What an emulator page says about a profile and its files. + +No network and no database: pages are rendered from small profiles. +""" + +from __future__ import annotations + +import sys +import unittest +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(REPO_ROOT / "scripts")) + +import generate_site as gs # noqa: E402 + +EMPTY_DB = {"files": {}, "indexes": {"by_name": {}}} + + +def _page(name: str, profile: dict) -> str: + return gs.generate_emulator_page(name, profile, EMPTY_DB, None, set()) + + +class WhereAndHowAFileIsRead(unittest.TestCase): + """load_from, found_by and on_mismatch were in 99, 4 and 25 entries and + on no page: Dolphin's gba_bios.bin showed a Path a RetroArch user put + under system/, while the libretro core reads it from its save directory.""" + + def test_load_from(self): + page = _page("dolphin", {"type": "standalone + libretro", "files": [ + {"name": "gba_bios.bin", "path": "GBA/gba_bios.bin", + "load_from": {"libretro": "save_dir"}}, + {"name": "hdd.rom", "load_from": "hdd_image"}, + {"name": "bios.bin", "load_from": "system_dir"}, + ]}) + self.assertIn("- Read from: the save directory, not the BIOS directory " + "(libretro build)", page) + self.assertIn("- Read from: inside the emulated hard disk image", page) + self.assertEqual(page.count("Read from:"), 2) + + def test_found_by_and_on_mismatch(self): + page = _page("x", {"type": "standalone", "found_by": "content", "files": [ + {"name": "scph5501.bin", "found_by": "content", "on_mismatch": "warn"}, + {"name": "boot.bin", "on_mismatch": {"signature": "warn"}}, + ]}) + self.assertIn("| Files found by | content (the code scans the folder", page) + self.assertIn("- Found by: content (", page) + self.assertIn("- On a failed check: warns and runs with the file", page) + self.assertIn("- On a failed signature check: warns and runs with the file", page) + + +class WhoShipsTheBuildAndWhy(unittest.TestCase): + """built_by, pin_frozen and version_source were in 9, 13 and 40 profiles + and on no page: mame0278 did not say only Recalbox ships it.""" + + def test_meta_rows(self): + page = _page("mame0278", { + "type": "libretro", "built_by": ["recalbox"], + "pin_frozen": "ROCKNIX 20261001 builds this revision.", + "version_source": {"url": "https://example.org/news", "pattern": "v(\\S+)", + "no_genuine_copy": "13.0"}, + "files": [], + }) + self.assertIn("| Built by | [recalbox](../platforms/recalbox.md), under its " + "core name.", page) + self.assertIn("| Revision held | ROCKNIX 20261001 builds this revision. |", page) + self.assertIn("| Releases announced at | [https://example.org/news]" + "(https://example.org/news) (version 13.0 announced there, " + "found only as rebuilt copies) |", page) + + +if __name__ == "__main__": + unittest.main() diff --git a/wiki/testing-guide.md b/wiki/testing-guide.md index da47320f..794720e7 100644 --- a/wiki/testing-guide.md +++ b/wiki/testing-guide.md @@ -44,7 +44,7 @@ Every module in `tests/`, by the area it guards. | Installer and downloads | loopback server | `test_check_release`, `test_download`, `test_install`, `test_install_cores`, `test_install_experience`, `test_install_selection` | | Truth and native export | synthetic + cached originals | `test_export_counts`, `test_export_native_layer`, `test_export_report`, `test_exporters`, `test_truth_diff`, `test_truth_layout` | | Scrapers, upstream and provenance | inline sources, synthetic | `test_check_freshness`, `test_dedup`, `test_fbneo_parser`, `test_find_pinned_revision`, `test_hash_merge`, `test_launchers`, `test_launchers_emudeck`, `test_launchers_recalbox`, `test_launchers_retrobat`, `test_launchers_retrodeck`, `test_launchers_retropie`, `test_launchers_rocknix`, `test_mame_devices`, `test_mame_parser`, `test_mame_ref_audit`, `test_profile_sync`, `test_provenance`, `test_refresh_data_dirs`, `test_refresh_failures`, `test_refresh_stale`, `test_retrodeck_scraper_errors`, `test_romset_recipes`, `test_scraper_contract`, `test_upstream`, `test_upstream_index` | -| Site, docs and repository contracts | rendered pages, sources | `test_contributing_text`, `test_data_dir_refs`, `test_dataclass_defaults`, `test_doc_pages`, `test_doc_surfaces`, `test_optional_dependencies`, `test_prose_style`, `test_site_availability`, `test_site_cores`, `test_site_exports`, `test_site_platform_hashes`, `test_site_render`, `test_site_system_links`, `test_site_validation`, `test_site_write`, `test_validate_pr`, `test_validate_pr_flow`, `test_validate_pr_hashes`, `test_workflow_paths` | +| Site, docs and repository contracts | rendered pages, sources | `test_contributing_text`, `test_data_dir_refs`, `test_dataclass_defaults`, `test_doc_pages`, `test_doc_surfaces`, `test_optional_dependencies`, `test_prose_style`, `test_site_availability`, `test_site_cores`, `test_site_emulator_page`, `test_site_exports`, `test_site_platform_hashes`, `test_site_render`, `test_site_system_links`, `test_site_validation`, `test_site_write`, `test_validate_pr`, `test_validate_pr_flow`, `test_validate_pr_hashes`, `test_workflow_paths` | ## Test architecture