diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7f1dafdcf..2fa3a8ece 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -68,6 +68,7 @@ python scripts/validate_pr.py --changed # the BIOS files the branch add python scripts/validate_schemas.py --source-only # profiles and platform files python scripts/profile_sync.py --changed --strict # each profile the branch adds or edits python scripts/refresh_data_dirs.py # data directories the pack-building tests read +python scripts/generate_site.py # the pages the site tests read python -m unittest discover -s tests -t . ``` diff --git a/scripts/generate_readme.py b/scripts/generate_readme.py index cf3a1fea1..aba4e9c9f 100644 --- a/scripts/generate_readme.py +++ b/scripts/generate_readme.py @@ -975,6 +975,7 @@ python scripts/validate_pr.py --changed # the BIOS files the branch add python scripts/validate_schemas.py --source-only # profiles and platform files python scripts/profile_sync.py --changed --strict # each profile the branch adds or edits python scripts/refresh_data_dirs.py # data directories the pack-building tests read +python scripts/generate_site.py # the pages the site tests read python -m unittest discover -s tests -t . ``` diff --git a/scripts/generate_site.py b/scripts/generate_site.py index 410a02537..2a763163b 100644 --- a/scripts/generate_site.py +++ b/scripts/generate_site.py @@ -5295,12 +5295,19 @@ def generate_provenance_page( kept.append(entry) data["missing"] = kept total_missing = distinct_missing(report) + # The three counts below added up to more than the total above it. + listed = sum(len(data["missing"]) for data in report.values()) + overlap = ( + f" A dump two catalogs describe is listed under each, so the lists " + f"hold {listed:,} rows." + if listed != total_missing else "" + ) lines.extend( [ "## Wanted: catalogued dumps this project does not have", "", f"The tables below list **{total_missing:,}** dumps a catalog " - "describes and the collection lacks. Nothing here ships in any " + f"describes and the collection lacks.{overlap} Nothing here ships in any " "pack: it is an acquisition list, published with hashes so anyone " "can check a personal collection against it. A contribution " "matching one of these hashes is welcome: see " @@ -10060,7 +10067,8 @@ def _copy_cell(path: str, algos: list[str], db: dict | None) -> str: def _render_problem_files( - platform_problems: list, settled: list | None = None, db: dict | None = None + platform_problems: list, settled: list | None = None, db: dict | None = None, + missing_total: int | None = None, ) -> list[str]: """Files a platform declares that its own check then rejects. @@ -10106,12 +10114,17 @@ def _render_problem_files( entry["platforms"].append(p["platform"]) total_problems = len(missing_files) + len(untested_files) + len(mismatch_files) + # One row per name: the page opened on "48 are missing", counted by + # destination, above a heading that said "10 files". Both are said. + names = _count(len(missing_files), "name") + missing_said = (f"{missing_total:,} missing under {names}" + if missing_total is not None else f"{len(missing_files)} missing") if total_problems > 0: lines.extend([ "## Problem files", "", - f"{len(missing_files)} missing, {len(untested_files)} untested, " + f"{missing_said}, {len(untested_files)} untested, " f"{len(mismatch_files)} discrepancy.", "", ]) @@ -10119,13 +10132,14 @@ def _render_problem_files( if missing_files: lines.extend([ f'### Missing ' - f"{_count(len(missing_files), 'file')}", + f"{_count(len(missing_files), 'name')}", "", "Required is the platform's own flag. Category says what the " "platform's emulators do with the file: `bios` is firmware they " "load, `game_data` comes from a copy of the game, `unread` is " "opened by none of them. Only a missing `bios` entry stops a " - "console from starting.", + "console from starting. A name a list declares in several " + "folders is one row.", "", "| File | Category | Required | Platforms |", "|------|----------|----------|-----------|", @@ -10440,9 +10454,9 @@ def generate_gap_analysis( prof_cell = gt_cell = "-" elif gt["total"]: prof_pct = f"{gt.get('with_profile', 0) / gt['total'] * 100:.0f}%" - prof_cell = f"{gt.get('with_profile', 0)} ({prof_pct})" + prof_cell = f"{gt.get('with_profile', 0):,} ({prof_pct})" gt_pct = f"{gt['with_validation'] / gt['total'] * 100:.0f}%" - gt_cell = f"{gt['with_validation']} ({gt_pct})" + gt_cell = f"{gt['with_validation']:,} ({gt_pct})" else: prof_cell = gt_cell = "0" lines.append( @@ -10513,14 +10527,14 @@ def generate_gap_analysis( "so there is nothing to compare: the platform's own source is the " "only authority for its files.", "", - "The same comparison drives `scripts/exporter/`: each platform's " - "own file can be regenerated in its native format with these " - "differences applied (System.dat, es_bios.xml, " - "batocera-systems.json, ...).", + "The same comparison feeds the [updated platform files](exports.md): " + "each platform's own file, patched with these differences and kept " + "in its native format (System.dat, es_bios.xml, batocera-systems, ...).", "", ]) - lines.extend(_render_problem_files(platform_problems, settled_problems, db)) + lines.extend(_render_problem_files(platform_problems, settled_problems, db, + total_missing_verify)) # ---- Section 3: Core complement (cross-reference provenance) ---- @@ -10572,8 +10586,8 @@ def generate_gap_analysis( f"{src_totals['missing']:,} to source.", "", "This counts every profiled emulator, including those no platform " - "ships yet, so it reaches past what the pack tables above measure: " - "those cover only the emulators each platform actually ships. " + "ships yet, so it reaches past what the first table above counts: " + "it covers only the emulators each platform actually ships. " "Whatever is not in the collection is an acquisition target, named " "per emulator below.", "", @@ -11027,6 +11041,12 @@ def generate_cross_reference( total_upstreams = len({ upstream_project(_xref_upstream(p))[0] for p in unique.values() if _xref_upstream(p) }) + # "623 cores across 18 platforms" counted every profile: 91 of them + # no platform runs. + run_cores = len({ + key for cov in coverages.values() + for key in resolve_platform_cores(cov["config"], unique) + }) lines = [ "# Cores by platform", @@ -11035,8 +11055,9 @@ def generate_cross_reference( "it comes from. A core built for RetroArch is often a port of another " "emulator, and the two can differ in the files they read and the checks " "they make, so each core names its upstream project. " - f"{total_cores} cores across {len(coverages)} platforms come from " - f"{total_upstreams} upstream projects.", + f"{total_cores} cores are profiled, {run_cores} of them run by the " + f"{len(coverages)} platforms, and they come from {total_upstreams} " + "upstream projects.", "", "The files a platform's emulators load and its own list does not name are " "counted on [What the packs do not hold](gaps.md).", diff --git a/tests/test_audit_regressions.py b/tests/test_audit_regressions.py index fe6ff453a..4f18a8ca8 100644 --- a/tests/test_audit_regressions.py +++ b/tests/test_audit_regressions.py @@ -5002,6 +5002,19 @@ class GapsPageCountsOneWay(unittest.TestCase): self.assertIn("Other revisions of a held destination", text) self.assertIn("`cdibios.zip`", text) + def test_missing_says_the_destination_count_of_the_opening_sentence(self): + # The page opened on "48 are missing" (by destination) above a + # heading that said "10 files": one row per name. + from scripts.generate_site import _render_problem_files + + problems = [{"platform": "AmberELEC", "name": "hardwareconfig.xml", + "status": "missing", "required": True, "reason": "", + "discrepancy": "", "category": "unread"}] * 3 + text = "\n".join(_render_problem_files(problems, missing_total=3)) + self.assertIn("3 missing under 1 name,", text) + self.assertIn('rb-badge-danger">1 name', text) + self.assertNotIn("1 missing", text) + def test_verify_marks_the_settled_entry(self): sys.path.insert(0, str(ROOT / "scripts")) import verify diff --git a/tests/test_doc_surfaces.py b/tests/test_doc_surfaces.py index 4acf3b158..fc5140625 100644 --- a/tests/test_doc_surfaces.py +++ b/tests/test_doc_surfaces.py @@ -9,10 +9,12 @@ tool prints, and a size is the one the release record holds. from __future__ import annotations +import ast import contextlib import re import sys import unittest +from functools import cache from pathlib import Path REPO_ROOT = Path(__file__).resolve().parent.parent @@ -380,6 +382,24 @@ class ContributorChecksMatchCi(unittest.TestCase): workflow.index("python -m unittest discover"), ) + def test_the_pr_checklist_generates_the_site_before_the_tests(self): + # validate.yml generates the pages before the suite. Without them + # the 228 tests that read the gitignored pages fail on a clone. + workflow = _read(".github/workflows/validate.yml") + self.assertLess( + workflow.index("run: python scripts/generate_site.py"), + workflow.index("python -m unittest discover"), + ) + for text in ( + contributing_text(lambda page: page, "# C", "here"), + _read("CONTRIBUTING.md"), + ): + block = _section(text, "## Before opening a PR") + self.assertIn("python scripts/generate_site.py", block) + self.assertLess( + block.index("generate_site.py"), block.index("unittest discover") + ) + class PagesOpenOnTheirContent(unittest.TestCase): """The download page said "This page picks the right pack for a setup" @@ -396,5 +416,180 @@ class PagesOpenOnTheirContent(unittest.TestCase): self.assertEqual(announcing, []) + + +class CoreCountsReadFromTheData(unittest.TestCase): + """The cores page said "623 cores across 18 platforms": that was every + profile, and 91 of them no platform runs.""" + + def test_the_run_count_is_what_the_platforms_resolve(self): + import yaml + from common import load_emulator_profiles, load_platform_config, resolve_platform_cores + + from tests import sitepages + + sitepages.require() + text = sitepages.page("cross-reference.md").read_text(encoding="utf-8") + profiles = load_emulator_profiles(str(REPO_ROOT / "emulators")) + unique = {k: v for k, v in profiles.items() if v.get("type") not in ("alias", "test")} + registry = yaml.safe_load((REPO_ROOT / "platforms" / "_registry.yml").read_text( + encoding="utf-8"))["platforms"] + run = set() + for name in registry: + config = load_platform_config(name, str(REPO_ROOT / "platforms")) + run |= set(resolve_platform_cores(config, unique)) + said = re.search(r"([\d,]+) cores are profiled, ([\d,]+) of them run by the (\d+) platforms", + text) + self.assertIsNotNone(said, "the page says how many profiled cores the platforms run") + self.assertEqual(int(said[1].replace(",", "")), len(unique)) + self.assertEqual(int(said[2].replace(",", "")), len(run)) + self.assertEqual(int(said[3]), len(registry)) + + +# What the generators write must not bring back what the project retired. +# The generated pages, README.md, CONTRIBUTING.md and the site's components +# are read for a retired command, address or feature term, each listed with +# the reason it was retired, and for every `--option` a generated command +# passes: the script it names must declare it in its parser. The pages copied +# from wiki/ are left to their own tests: the installer page keeps the raw +# one-liner as the documented fallback. + + +def _sitepages(): + from tests import sitepages + + return sitepages + +# (string, why it is retired). Compared case-sensitively against the text. +RETIRED = ( + ("raw.githubusercontent.com/Abdess/retrobios/main/install.sh", + "the site serves the one-liner at https://abdess.github.io/retrobios/i " + "since 2026-10-28, pinned to the commit the site was built from"), + ("raw.githubusercontent.com/Abdess/retrobios/main/install.ps1", + "served at https://abdess.github.io/retrobios/i.ps1 since 2026-10-28"), + ("Abdess/retroarch_system", + "the repository was renamed retrobios in March 2026"), + ("mkdocs serve", "the MkDocs site was replaced by Astro Starlight on 2026-10-22"), + ("mkdocs build", "the MkDocs site was replaced by Astro Starlight on 2026-10-22"), + ("pip install mkdocs", "the site is built by Node from website/package-lock.json"), + ('class="rb-tested', "the Tested blocks were removed on 2026-10-28: " + "only Proof lines and the Measurements page remain"), + ("rb-region-runs", "region outcomes left the system pages on 2026-10-28"), + ("## Campaign", "the campaign table left the Measurements page on 2026-10-28"), + ("| Tested by |", "the coverage table left the Measurements page on 2026-10-28"), + ("ms per frame", "benchmark figures stay in the records and SQLite files, " + "never on a page, since 2026-10-28"), + ("with this BIOS folder as", "the Tested block of platform pages, removed on 2026-10-28"), + ("regenerated in its native format", "the export patches each platform's own " + "file and never regenerates it (scripts/exporter/base_exporter.py needs_original)"), +) + +COMMAND = re.compile( + r"(?:python3? (?P