chore: merge origin/main

This commit is contained in:
Abdessamad Derraz committed 2026-11-05 23:25:48 +01:00
commit 49ff682eda
40 files changed
+1304 -222

No files matched your search

+2 -1
View File
@@ -6781,7 +6781,8 @@ class SkipPacksHelpSaysWhatRuns(unittest.TestCase):
)
text = " ".join(proc.stdout.split())
self.assertNotIn("Only regenerate DB and verify", text)
self.assertIn("Steps 7 and 8 still rewrite README.md and the site pages in website/", text)
self.assertIn("step 4e still rewrites install/games/, and steps 7 to 8b rewrite README.md "
"and the site unless --skip-docs is given", text)
self.assertNotIn("docs/", text)
def test_every_step_it_names_is_one_it_skips(self):
+30
View File
@@ -78,6 +78,36 @@ class BeforeOpeningAPullRequest(unittest.TestCase):
self.assertIn("profile_sync.py --added --base upstream/main --strict", block)
self.assertNotIn("--changed --strict", block)
def test_it_names_the_site_build_and_the_packages_ci_runs(self):
"""A wiki-only change passed every listed check and failed CI on
validate_site.py, which the list did not name."""
text = generate_readme.generate_contributing()
section = text.split("## Before opening a PR", 1)[1].split("\n## ", 1)[0]
workflow = (REPO_ROOT / ".github" / "workflows" / "validate.yml").read_text(encoding="utf-8")
for command in ("npm ci --prefix website", "npm run build --prefix website",
"validate_site.py --site-dir website/dist",
"vermin --no-tips -t=3.8- --violations install.py"):
with self.subTest(command=command):
self.assertIn(command, workflow)
self.assertIn(command, section)
installed = re.search(r'^\s*run: pip install (pyyaml "jsonschema.*markdown.*)$',
workflow, re.M).group(1)
pip_line = next(line for line in section.splitlines() if line.startswith("pip install"))
for package in (re.split(r"[\[=]", token.strip('"'))[0] for token in installed.split()):
with self.subTest(package=package):
self.assertRegex(pip_line, rf"\b{package}\b")
def test_a_file_too_large_for_git_has_a_route(self):
"""The guide sent any file over 25 MB through a clone, which GitHub
refuses past 100 MB, and only a maintainer can write to the release."""
import validate_pr
text = generate_readme.generate_contributing()
section = text.split("## What happens next", 1)[1].split("\n## ", 1)[0]
self.assertEqual(validate_pr.MAX_FILE_SIZE, 100 * 1024 * 1024)
self.assertIn("100 MB", section)
self.assertIn(f"]({validate_pr.LARGE_FILE_ISSUE_URL})", section)
def test_it_installs_what_the_checks_need_and_no_site_package(self):
"""The site is built by Node from website/package-lock.json: a pip
line naming the old theme installed packages nothing reads."""
+15
View File
@@ -252,6 +252,21 @@ class TestPackDeterminism(unittest.TestCase):
names = {i.filename.rsplit("/", 1)[-1] for i in zf.infolist()}
self.assertIn("romset.zip", names)
def test_a_romset_is_the_collection_bytes_and_the_wiki_says_so(self):
"""The pack copies each ZIP byte for byte since fed03c3db, and the
wiki still told a third party to rebuild romsets to compare."""
pack = self._build("out1")
with zipfile.ZipFile(pack) as zf:
member = next(n for n in zf.namelist() if n.endswith("romset.zip"))
self.assertEqual(zf.read(member), (self.bios / "romset.zip").read_bytes())
root = Path(__file__).resolve().parent.parent / "wiki"
for page in ("architecture.md", "tools.md"):
with self.subTest(page=page):
text = " ".join((root / page).read_text(encoding="utf-8").split())
self.assertNotIn("rebuilt deterministically", text)
self.assertNotIn("rebuilds MAME", text)
self.assertNotIn("Rebuild MAME BIOS ZIPs", text)
def test_every_member_carries_the_fixed_epoch(self):
"""A member's date must come from the build's inputs, not its clock.
+86
View File
@@ -1799,5 +1799,91 @@ class ReportExcerptIsTheRun(unittest.TestCase):
self.assertIn(line, printed)
class ClaimRuleNamesTheSizeProof(unittest.TestCase):
"""architecture.md said a file found by its name claims nothing, while
slots.py has counted a name-found copy at the declared size against a
platform file of another size since 2b535525e."""
def test_the_page_states_what_slots_counts(self):
import slots
self.assertIn(slots.SIZE_EXACT, slots.PROVEN_STATUSES)
page = " ".join((ROOT / "wiki" / "architecture.md").read_text(encoding="utf-8").split())
section = page.split("### When both layers claim one destination", 1)[1].split("###", 1)[0]
self.assertIn(f"(`{slots.SIZE_EXACT}`)", section)
self.assertIn("Two files of equal size are revisions", section)
class CrossReferenceSettlesBySlotAndSize(unittest.TestCase):
"""reading-reports.md said the cross-reference checks whether a profile's
files "appear in the platform config", a by-name rule, while verify
settles a file only at its slot or at a size the core accepts."""
def test_the_page_states_the_rule_verify_reads(self):
from validation import declaration_settles
rom1 = {"name": "ROM1.BIN", "size": 32768, "validation": ["size"]}
self.assertFalse(declaration_settles(rom1, "ROM1.BIN", {"ROM1.BIN": [(4096, "rom1.bin")]},
dest="dosbox/ROM1.BIN"))
self.assertTrue(declaration_settles(rom1, "ROM1.BIN", {"ROM1.BIN": [(32768, "rom1.bin")]},
dest="dosbox/ROM1.BIN"))
page = " ".join((ROOT / "wiki" / "reading-reports.md").read_text(encoding="utf-8").split())
section = page.split("### Cross-reference section", 1)[1].split("###", 1)[0]
self.assertIn("declaration_settles()", section)
self.assertIn("A name alone is not enough", section)
self.assertNotIn("appear in the platform config", section)
class CommittedLargeFilesAreTheOnesNamed(unittest.TestCase):
"""release-process.md named tbblue.mmc among the committed files over
50 MB two weeks after it left bios/, and not uqm-remix-disc1.uqm."""
def test_the_list_is_the_tree(self):
import subprocess
listed = subprocess.run(["git", "ls-files", "-z", "bios"], cwd=ROOT, capture_output=True,
check=True).stdout.decode("utf-8").split("\0")
large = sorted(Path(path).name for path in listed
if path and (ROOT / path).is_file()
and (ROOT / path).stat().st_size > 50_000_000)
page = " ".join((ROOT / "wiki" / "release-process.md").read_text(encoding="utf-8").split())
said = re.search(r"Size does not decide it: (\w+) committed files are over 50 MB \(([^)]*)\)",
page)
self.assertIsNotNone(said)
self.assertEqual(sorted(re.findall(r"`([^`]+)`", said.group(2))), large)
self.assertEqual(said.group(1), {3: "three", 4: "four", 5: "five"}.get(len(large)))
class SkipFlagsNameTheSteps(unittest.TestCase):
"""--skip-packs said "add --skip-docs to stop after verify" while 4d and
4e still ran, and the flag table left 8b out of --skip-docs."""
STEP = r"\b\d[a-z]?\d?\b"
def test_the_flag_row_the_help_and_the_usage_agree_with_the_step_table(self):
page = (ROOT / "wiki" / "tools.md").read_text(encoding="utf-8")
table = page.split("Pipeline steps:", 1)[1].split("Pipeline flags:", 1)[0]
source = (ROOT / "scripts" / "pipeline.py").read_text(encoding="utf-8")
helps = {}
for node in ast.walk(ast.parse(source)):
if (isinstance(node, ast.Call) and getattr(node.func, "attr", "") == "add_argument"
and node.args and isinstance(node.args[0], ast.Constant)):
helps.update({node.args[0].value: ast.literal_eval(kw.value)
for kw in node.keywords if kw.arg == "help"})
for flag in ("--skip-packs", "--skip-docs"):
steps = set()
for row in table.splitlines():
cells = [c.strip() for c in row.split("|")]
if len(cells) > 3 and cells[3].startswith(f"`{flag}`"):
steps.add(cells[1])
self.assertTrue(steps)
row = re.search(rf"^\| `{flag}` \| skip steps ([^.|]*)", page, re.M).group(1)
usage = re.search(rf"pipeline\.py {flag} +# skip steps (.*)$", source, re.M).group(1)
said = helps[flag].split(":", 1)[0]
for text in (row, usage, said):
with self.subTest(flag=flag, text=text):
self.assertEqual(set(re.findall(self.STEP, text)), steps)
if __name__ == "__main__":
unittest.main()
+6 -4
View File
@@ -17,7 +17,7 @@ from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO_ROOT / "scripts"))
from cross_reference import FileConsumers # noqa: E402
from cross_reference import FileConsumers, entry_target # noqa: E402
from generate_site import generate_system_page # noqa: E402
_TMP = tempfile.TemporaryDirectory()
@@ -246,7 +246,6 @@ class EveryReaderIsTheEmulatorPackFile(unittest.TestCase):
build_zip_contents_index, load_data_dir_registry,
load_emulator_profiles, resolve_local_file,
)
from validation import with_archive_members
db_path = REPO_ROOT / "database.json"
if not db_path.exists():
@@ -275,9 +274,12 @@ class EveryReaderIsTheEmulatorPackFile(unittest.TestCase):
if members:
from_trees |= {(sha1, name) for sha1 in members.values()}
continue
# A ROM read inside an archive is the archive, as the
# emulator pack resolves it (archive_entry).
target, dest = entry_target(entry, files)
path, status = resolve_local_file(
{**with_archive_members(entry, files), "source_profile": name},
db, zips, dest_hint=entry.get("path") or "",
{**target, "source_profile": name},
db, zips, dest_hint=dest,
data_dir_registry=registry,
)
if path and status not in ("not_found", "hash_mismatch"):
+7
View File
@@ -5974,6 +5974,13 @@ class TestBranchProfiles(unittest.TestCase):
["draft", "new"],
)
def test_an_unfetched_base_says_to_fetch(self):
"""`--base upstream/main` with upstream added and never fetched
stopped on git's "Not a valid object name" and no next step."""
with self.assertRaises(SystemExit) as raised:
profile_sync.branch_profiles("emulators", "upstream/main", added_only=True)
self.assertIn("git fetch upstream", str(raised.exception))
def test_the_flag_selects_those_profiles(self):
args = profile_sync.build_parser().parse_args(["--changed", "--base", "main"])
profiles = {"kept": {}, "edited": {}, "new": {}, "draft": {}}
+16
View File
@@ -308,6 +308,22 @@ class ProfilesStateFactsAboutOtherProjects(unittest.TestCase):
]
self.assertEqual(offenders, [], "\n".join(offenders[:40]))
# A project's state is a dated fact or its own words, never a verdict:
# "Upstream project is dead" (yabause), "abandoned fork" (gearboy).
PROJECT_VERDICTS = re.compile(
r"\b(?:(?:project|upstream)(?:\s+project)?\s+(?:is\s+)?dead|"
r"abandoned\s+(?:fork|since)|project\s+abandoned|never-completed)\b",
re.IGNORECASE,
)
def test_no_verdict_on_a_project(self):
offenders = [
f"{path.relative_to(ROOT)}: {found.group(0)}"
for path in sorted((ROOT / "emulators").glob("*.yml"))
for found in self.PROJECT_VERDICTS.finditer(path.read_text(encoding="utf-8"))
]
self.assertEqual(offenders, [], "\n".join(offenders[:40]))
def test_no_grade_on_another_projects_file(self):
offenders = [
f"{path.relative_to(ROOT)}: {line.strip()[:100]}"
+1 -1
View File
@@ -130,7 +130,7 @@ class AForkPushedToItsOwnMain(unittest.TestCase):
# A project that cannot be reached is an error, never "nothing".
missing = self._changed(clone, str(Path(tmp, "absent")))
self.assertNotEqual(missing.returncode, 0)
self.assertIn("fetching its main failed", missing.stderr)
self.assertIn("fetching its main from", missing.stderr)
class RefreshFailuresAreFailures(unittest.TestCase):
+29 -4
View File
@@ -334,6 +334,22 @@ class NoLevelBadgeOnATabRoot(unittest.TestCase):
self.assertNotIn('class="rb-level ', _title_through_heading(_built(page)))
class OneTabIsNoTabBar(unittest.TestCase):
"""142 tab bars held one tab, most of them "On the device" right under
the heading "On the device"."""
def test_a_single_item_stands_alone(self):
import sitemarkup
import siteguides
self.assertEqual(sitemarkup.tabs([("Linux", ["run it"])]), ["**Linux**", "", "run it", ""])
self.assertEqual(sitemarkup.tabs([("", ["run it"])]), ["run it", ""])
self.assertIn('<Tabs syncKey="os">', sitemarkup.tabs([("A", ["a"]), ("B", ["b"])]))
device = "\n".join(siteguides.terminal_tabs(("device",), {}))
self.assertNotIn("<Tab", device)
self.assertNotIn("On the device", device)
class FooterLicenceMatchesTheLicenceFile(unittest.TestCase):
"""The footer said MIT covers the tooling. LICENSE names the metadata too."""
@@ -353,15 +369,24 @@ class WideTablesStackAndFiltersAreBuilt(unittest.TestCase):
def test_every_wide_table_stacks(self):
for page in ("emulators/duckstation", "gaps", "gaps-files-to-source", "emulators", "data",
"wiki/verification-modes"):
html = (DIST / page / "index.html")
if not html.is_file():
continue
for table in _tables(_read(html)):
for table in _tables(_built(page)):
if len(_headers(table)) < 4:
continue
with self.subTest(page=page, header=_headers(table)):
self.assertIn('data-rb-stack="true"', table.split(">", 1)[0])
def test_no_build_is_a_skip_not_a_pass(self):
"""Every missing page was a `continue`: with no build the test ran no
assertion and still reported ok."""
from unittest import mock
module = sys.modules[__name__]
case = WideTablesStackAndFiltersAreBuilt("test_every_wide_table_stacks")
result = unittest.TestResult()
with mock.patch.object(module, "DIST", REPO_ROOT / "tmp" / "no-such-dist"):
case.run(result)
self.assertEqual(len(result.skipped), 1)
def test_long_tables_get_their_labels_at_runtime(self):
"""Labels on every cell of a long stacked table were 122 KB of
emulators/mame and took it over the 600,000-byte page budget."""
+32
View File
@@ -68,6 +68,38 @@ class WhoShipsTheBuildAndWhy(unittest.TestCase):
"(https://example.org/news) (version 13.0 announced there, "
"found only as rebuilt copies) |", page)
def test_the_version_links_its_proof(self):
"""`0.97, stated in libretro/handy.h` was bare text beside a pinned
Revision link: the #define it reads was one search away."""
import generate_site as gs
import sitesources
import yaml
handy = yaml.safe_load((REPO_ROOT / "emulators" / "handy.yml").read_text(encoding="utf-8"))
path = handy["version_basis"]["path"]
self.assertIn(path, sitesources.cited_paths(handy))
self.assertIn(f"stated in [`{path}`](https://github.com/libretro/libretro-handy/blob/"
f"{handy['source_commit']}/{path})", gs.version_markdown(handy))
tagged = {"source": "https://github.com/owner/emu", "source_commit": "a" * 40,
"core_version": "1.2-3-gaaaaaaa",
"version_basis": {"kind": "tag", "tag": "1.2", "distance": 3}}
self.assertEqual(gs.version_markdown(tagged), "1.2-3-gaaaaaaa, 3 commits after tag "
"[`1.2`](https://github.com/owner/emu/tree/1.2)")
# A file no recorded tree confirms stays plain: no link beats a 404.
unread = {**tagged, "version_basis": {"kind": "file", "path": "src/none.h"}}
self.assertEqual(gs.version_markdown(unread), "1.2-3-gaaaaaaa, stated in `src/none.h`")
def test_the_upstream_pin_says_why_it_is_held(self):
"""mupen64plus showed its core revision and not that AmberELEC
builds that one: read as a profile behind HEAD."""
page = _page("mupen64plus", {
"type": "libretro",
"upstream_frozen": "AmberELEC builds the core at this revision.",
"files": [],
})
self.assertIn("| Upstream revision held | AmberELEC builds the core at this "
"revision. |", page)
class ACardKeepsItsAddress(unittest.TestCase):
"""emulators/raze/#file-duke3d-grp stopped pointing at its card once a
+10 -3
View File
@@ -254,11 +254,16 @@ class WhatTheAppShipsIsNoFileToGet(unittest.TestCase):
self.assertIn(">comes with the app</span>", badges)
self.assertNotIn(">required</span>", badges)
self.assertNotIn(">missing</span>", badges)
# Installed by one build only: the other still reads it from the folder.
# Narrowed to one build, the entry is that build's alone, as verify
# and the pack read it: EightyOne installs its ROMs, and the libretro
# core, which embeds them, reads no entry. Nothing to fetch.
dual = " ".join(gs._file_badges({"name": "a", "bundled": True, "mode": "standalone",
"required": True}, False))
self.assertIn(">required</span>", dual)
self.assertNotIn(">required</span>", dual)
self.assertNotIn(">missing</span>", dual)
self.assertIn("installed with the standalone emulator", dual)
rom = next(f for f in _profile("81")["files"] if f["name"] == "jupiterace.rom")
self.assertTrue(gs.comes_with_app(rom))
def test_counts_leave_them_out(self):
shipped = sum(1 for f in self.profile["files"] if gs.comes_with_app(f))
@@ -399,7 +404,9 @@ class AnAppThatReadsAnyBios(unittest.TestCase):
install = self.page[self.page.index("Install this emulator's files"):]
install = install[:install.index("</details>")]
self.assertRegex(install, r"With [^:]+: the installer finds")
self.assertEqual(set(re.findall(r'<TabItem label="([^"]+)">', install)), {"On the device"})
# One place to run it, so no tab bar (one tab offers no choice).
self.assertNotIn("<Tabs", install)
self.assertIn("curl -fsSL", install)
nether = _pages("nethersx2")["nethersx2"]
if "--emulator nethersx2" in nether:
self.assertIn("With ", nether)
+13 -3
View File
@@ -443,14 +443,22 @@ class PackNamesItsVersion(unittest.TestCase):
block = generate_site._pack_buttons("batocera")
version = generate_site._pack_version(release_record.pack_name_for("batocera", record))
self.assertIn(f"Made for Batocera {version}. "
+ generate_site.OTHER_VERSION, block)
+ generate_site.other_version("batocera"), block)
def test_a_version_gap_is_said_once(self):
record = {"tag": "v2026.10.10", "packs": {"RetroBat_1.0_BIOS_Pack.zip": {
"assets": ["RetroBat_1.0_BIOS_Pack.zip"], "platforms": ["retrobat"]}}}
released = generate_site.release_line("retrobat", record)
line = generate_site.version_line("retrobat", record, named=len(released) > 1)
self.assertEqual(line, generate_site.OTHER_VERSION)
self.assertEqual(line, generate_site.other_version("retrobat"))
def test_another_version_is_sent_to_the_check_commands(self):
"""A ZIP reader read "Run `--check` after copying." with no command."""
line = generate_site.other_version("emudeck")
self.assertNotIn("Run `--check`", line)
self.assertIn("](../platforms/emudeck.md#how-to-check)", line)
page = _page("platforms/emudeck.md")
self.assertIn("{ #how-to-check }", page)
def test_the_v_prefix_is_dropped(self):
self.assertEqual(generate_site.setup_version("retroarch", {}),
@@ -925,7 +933,9 @@ class ARouteWithoutANetwork(unittest.TestCase):
self.assertEqual(siteguides.GUIDES["retropie"].card_run, ("linux",))
text = _plain_page("platforms/retropie.md")
card = text.split("No network?", 1)[1].split("## On the device", 1)[0]
self.assertEqual(_tab_labels(card), {"Linux"})
# One system, so no tab bar: its label leads the command.
self.assertEqual(_tab_labels(card), set())
self.assertIn("**Linux**", card)
def test_no_zip_is_promised_for_an_unmarked_setup(self):
for name in SETUPS:
+22 -1
View File
@@ -422,7 +422,11 @@ class BuiltBatoceraPage(unittest.TestCase):
# PlayStation's list is long enough to sit on a page of its own.
pages = [main] + [p.read_text(encoding="utf-8") for p in
(DIST / "platforms").glob("batocera-sony-playstation*/index.html")]
html = "".join(pages)
# Read as a browser shows it: a page with no tab bar is plain
# Markdown, which keeps the entity a name's underscore is written as.
import html as _html
html = _html.unescape("".join(pages))
for text in ("Served by:", "scph1001_v20.bin", "https://github.com/stenzek/duckstation/blob/"):
with self.subTest(text=text):
self.assertTrue(text in html)
@@ -696,6 +700,23 @@ class ManifestsArePublished(unittest.TestCase):
self.assertLess(page.index("## Stable JSON endpoints"), page.index("The DAT is Logiqx XML"))
class TheLookupRecipeSaysWhatASha1Misses(unittest.TestCase):
"""The page said a SHA1 lookup was enough, "a setup that lists a file
by MD5 or CRC32 is filed under the SHA1 of the collection file it names
too". A record whose value no collection file answers has no SHA1: 702
platform MD5s were out of the recipe's reach."""
def test_the_page_and_the_records_agree(self):
record = {"layer": "platform", "algorithm": "md5", "value": "0" * 32,
"name": "dmg_boot.bin", "owner": "recalbox"}
self.assertEqual(generate_site._held_under_every_hash([record], {}, {}), [])
self.assertNotIn("sha1", record)
page = " ".join(generate_site.generate_data_page([]).split())
self.assertNotIn("none is needed", page)
self.assertIn("A list naming a file the collection lacks is filed under its MD5 "
"or CRC32 alone", page)
class AnIdLeadsToItsData(unittest.TestCase):
"""8.6: a system by its id, a core by the name the buildbot gives it."""
+76
View File
@@ -427,5 +427,81 @@ class TheSearchSkipsLinkLists(unittest.TestCase):
self.assertIn(short, text)
class OneDefinitionPerName(unittest.TestCase):
"""generate_site defined _REGION_WORDS twice: the second table replaced
the first at import, and _region_word, written beside the first, never
read it."""
def test_no_module_name_is_bound_twice(self):
import ast
for path in sorted((REPO_ROOT / "scripts").glob("*site*.py")):
seen: dict[str, int] = {}
for node in ast.parse(path.read_text(encoding="utf-8")).body:
if isinstance(node, (ast.FunctionDef, ast.ClassDef)):
names = [node.name]
elif isinstance(node, ast.Assign):
names = [t.id for t in node.targets if isinstance(t, ast.Name)]
elif isinstance(node, ast.AnnAssign) and isinstance(node.target, ast.Name):
names = [node.target.id]
else:
continue
for name in names:
self.assertNotIn(name, seen, f"{path.name}:{node.lineno} binds {name} "
f"again, first at line {seen.get(name)}")
seen[name] = node.lineno
@unittest.skipUnless((REPO_ROOT / "database.json").is_file(), "database.json is not built")
class AMemberIsReadInItsArchive(unittest.TestCase):
"""FBNeo reads sm1.sm1 inside neogeo.zip. Resolved by its crc32, the
entry credited a loose sm1.sm1 FBNeo never opens, and no neogeo.zip card
named FBNeo, while the pack ships it one."""
def test_the_archive_is_credited_and_named_on_the_card(self):
import json
import generate_site as gs
from common import archive_entry, resolve_local_file
from cross_reference import FileConsumers
db = json.loads((REPO_ROOT / "database.json").read_text(encoding="utf-8"))
profile = yaml.safe_load((REPO_ROOT / "emulators" / "fbneo.yml").read_text(encoding="utf-8"))
files = profile["files"]
consumers = FileConsumers(db)
consumers.add_emulators({"fbneo": profile})
credited = {consumers.path_of(sha1) for sha1 in consumers.emulators}
shipped, _status = resolve_local_file(
{**archive_entry("neogeo.zip", files), "source_profile": "fbneo"}, db)
self.assertIn(shipped, credited)
self.assertFalse([p for p in credited if p.endswith("/sm1.sm1")])
sm1 = next(f for f in files if f["name"] == "sm1.sm1")
rows = "\n".join(gs._render_emulator_file(sm1, profile, consumers, files,
lambda _f: True, owner="fbneo"))
self.assertIn("Collection copy: member `sm1.sm1` of", rows)
self.assertIn(shipped, rows)
class NoTooltipInsideAName(unittest.TestCase):
"""The glossary's capitalised terms matched almost only product names:
Free Unix Spectrum Emulator, Windows Terminal, Documents/Dolphin Emulator."""
def test_names_are_left_and_terms_are_marked(self):
import sitestarlight
terms = {"Emulator": "An app that acts like a console.",
"Terminal": "A window where you type commands.",
"BIOS": "The console's built-in software."}
for line in ("Fuse (Free Unix Spectrum Emulator) reads it.",
"Open Windows Terminal (WSL).",
"Copy it to Documents/Dolphin Emulator/Sys."):
with self.subTest(line=line):
self.assertNotIn("rb-term", sitestarlight.add_tooltips(line, terms))
marked = sitestarlight.add_tooltips(
"Emulator settings come first. The PlayStation BIOS goes there.", terms)
self.assertIn('title="An app that acts like a console.">Emulator</abbr>', marked)
self.assertIn('>BIOS</abbr>', marked)
if __name__ == "__main__":
unittest.main()
+34
View File
@@ -231,6 +231,40 @@ class Descriptions(unittest.TestCase):
self.assertTrue(text.startswith("DuckStation emulates Sony PlayStation."))
self.assertNotIn("source-pinned", text)
def test_a_page_whose_files_come_with_the_app_promises_none(self) -> None:
"""DaedalusX64's three files come with the app. The result read
"DaedalusX64 BIOS files" over "DaedalusX64 emulates Nintendo 64."."""
saved, shipped = dict(gs.PAGE_DATA), set(gs.NOTHING_TO_FETCH)
self.addCleanup(lambda: (gs.PAGE_DATA.clear(), gs.PAGE_DATA.update(saved),
gs.NOTHING_TO_FETCH.clear(), gs.NOTHING_TO_FETCH.update(shipped)))
with open(REPO_ROOT / "emulators" / "daedalusx64.yml", encoding="utf-8") as handle:
gs.record_emulator_page("daedalusx64", yaml.safe_load(handle))
key = Path("emulators/daedalusx64.md")
self.assertEqual(gs._browser_title(key, "DaedalusX64"), "DaedalusX64 emulator")
body = "# DaedalusX64\n\nDaedalusX64 emulates Nintendo 64.\n"
self.assertEqual(gs._page_description(body, key, "DaedalusX64"),
"DaedalusX64 emulates Nintendo 64. Every file it reads comes with the app.")
def test_an_initial_does_not_end_the_sentence(self) -> None:
"""hcl's description was "Hydra Castle Labyrinth: open-source port of E."."""
body = ("# Hydra Castle Labyrinth\n\n<details>\n<summary>Technical notes</summary>\n\n"
"Open-source port of E. Hashimoto's 2008 freeware Metroidvania. It reads "
"its data folder.\n\n</details>\n")
text = gs._emulator_description(body, "emulators/hcl.md", "Hydra Castle Labyrinth", "")
self.assertTrue(text.startswith("Hydra Castle Labyrinth: open-source port of E. "
"Hashimoto's 2008 freeware Metroidvania."), text)
self.assertNotIn("It reads", text)
def test_a_platform_page_says_what_it_gives(self) -> None:
"""All 18 ended on the label of a link a snippet cannot follow,
"ROCKNIX website.", and never named the BIOS files or their folder."""
body = ("# ROCKNIX\n\nA system for handhelds. [ROCKNIX website](https://rocknix.org).\n\n"
"## On the device { #install }\n")
self.assertEqual(
gs._page_description(body, Path("platforms/rocknix.md"), "ROCKNIX"),
"The BIOS files for ROCKNIX, the folder they go in and the command that installs "
"them. A system for handhelds.")
def test_a_console_page_is_described_by_its_start_line(self) -> None:
body = "# Sega: Dreamcast\n\nPart of Sega.\n\n**Start with**: `dc_boot.bin`, `dc_flash.bin`.\n"
text = gs._page_description(body, Path("systems/sega-dreamcast.md"), "Sega: Dreamcast")
+19
View File
@@ -116,5 +116,24 @@ class SystemNames(unittest.TestCase):
self.assertFalse(names & siterender.program_names(self.profiles))
class SectionsComeBeforeThePlan(unittest.TestCase):
"""plan_emulator_pages names systems. Run before set_console_sections,
it made siterender read ./database.json and ./emulators whatever --db
and --emulators-dir gave, so the parts were cut on another collection."""
def test_generate_installs_the_sections_before_planning(self):
import ast
import inspect
import generate_site
tree = ast.parse(inspect.getsource(generate_site._generate))
lines = {}
for node in ast.walk(tree):
if isinstance(node, ast.Call) and isinstance(node.func, ast.Name):
lines.setdefault(node.func.id, node.lineno)
self.assertLess(lines["set_console_sections"], lines["plan_emulator_pages"])
if __name__ == "__main__":
unittest.main()
+66 -10
View File
@@ -139,9 +139,25 @@ class AFileOnlyACoreReadsIsKnown(_Fixture):
)
self.assertIn(
("WARN", "File is not under bios/ yet. Its system's files are in "
"`bios/Maker/Console/`"),
"`bios/Maker/Console/`. --place copies it to `bios/Maker/Console/b.bin`"),
result.checks,
)
self.assertEqual(result.destination, "bios/Maker/Console/b.bin")
# A file of that name already there: the copy is a variant.
self._bios_file("bios/Maker/Console/b.bin", b"another")
self.assertEqual(
validate_pr.validate_file("Downloads/b.bin", db, EMPTY_HASHES, known).destination,
f"bios/Maker/Console/.variants/b.bin.{result.sha1[:8]}",
)
# Copied under the declared name, never over a file, and only once.
card = self._bios_file("card/B.BIN", missing)
named = validate_pr.validate_file(card, db, EMPTY_HASHES, known)
self.assertEqual(validate_pr.place([named]), [named.destination])
self.assertEqual((self.tmp / named.destination).read_bytes(), missing)
self.assertTrue((self.tmp / card).is_file())
self.assertEqual(validate_pr.place([named]), [])
unknown = validate_pr.validate_file(self._bios_file("card/x.bin"), db, EMPTY_HASHES, known)
self.assertEqual((unknown.outcome, unknown.destination), ("unknown", ""))
placed = validate_pr.validate_file(
self._bios_file("bios/Other/Place/b.bin", missing), db, EMPTY_HASHES, known,
)
@@ -243,20 +259,38 @@ class HashRecognition(_Fixture):
self.assertIn(".variants/known.bin.", markdown)
def test_an_unreferenced_file_asks_for_review(self):
"""Random bytes read "PASS" and "all checks passed", and the warning
named the platform configs alone though the profiles were searched."""
rel = self._bios_file("bios/Sony/PS/mystery.bin")
markdown = validate_pr.validate_file(rel, None, EMPTY_HASHES).to_markdown()
self.assertIn("needs manual review", markdown)
result = validate_pr.validate_file(rel, None, EMPTY_HASHES)
markdown = result.to_markdown()
self.assertEqual(result.outcome, "unknown")
self.assertIn("### REVIEW", markdown)
self.assertIn("no platform list or emulator profile declares this file", markdown)
self.assertIn("which emulator reads it", markdown)
def test_a_duplicate_of_a_known_file_is_reported(self):
rel = self._bios_file("bios/Sony/PS/dupe.bin")
"""A copy of a held file passed, and was sent to the folder that
already holds it."""
rel = self._bios_file("elsewhere/dupe.bin")
probe = validate_pr.validate_file(rel, None, EMPTY_HASHES)
db = {"files": {probe.sha1: {"path": "bios/Sony/PS/original.bin"}}}
markdown = validate_pr.validate_file(rel, db, EMPTY_HASHES).to_markdown()
self.assertIn("Duplicate", markdown)
self.assertIn("original.bin", markdown)
known = {**EMPTY_HASHES, "sha1": {probe.sha1},
"folders": {probe.sha1: {"bios/Sony/PS/"}}}
result = validate_pr.validate_file(rel, db, known)
markdown = result.to_markdown()
self.assertEqual(result.outcome, "held")
self.assertIn("### REVIEW", markdown)
self.assertIn("Already held at `bios/Sony/PS/original.bin`: nothing to add", markdown)
self.assertNotIn("not under bios/", markdown)
# A duplicate is worth saying out loud but must not block the PR.
self.assertTrue(validate_pr.validate_file(rel, db, EMPTY_HASHES).passed)
self.assertTrue(result.passed)
def test_a_copy_in_a_tree_read_by_path_is_kept(self):
rel = self._bios_file("bios/ScummVM/a/dupe.bin")
probe = validate_pr.validate_file(rel, None, EMPTY_HASHES)
db = {"files": {probe.sha1: {"path": "bios/ScummVM/b/dupe.bin"}}}
self.assertNotEqual(validate_pr.validate_file(rel, db, EMPTY_HASHES).outcome, "held")
def test_the_file_the_database_holds_is_not_its_own_duplicate(self):
"""The documented example on a tracked file warned "Duplicate:
@@ -334,7 +368,8 @@ class Placement(_Fixture):
class Reporting(_Fixture):
def test_markdown_carries_every_digest_and_the_verdict(self):
rel = self._bios_file("bios/Sega/Saturn/sega.bin")
result = validate_pr.validate_file(rel, None, EMPTY_HASHES)
probe = validate_pr.validate_file(rel, None, EMPTY_HASHES)
result = validate_pr.validate_file(rel, None, {**EMPTY_HASHES, "sha1": {probe.sha1}})
markdown = result.to_markdown()
for value in (result.sha1, result.md5, result.crc32):
self.assertIn(value, markdown)
@@ -344,12 +379,33 @@ class Reporting(_Fixture):
rel = self._bios_file("bios/Sega/Saturn/bad.exe")
self.assertIn("### FAIL", validate_pr.validate_file(rel, None, EMPTY_HASHES).to_markdown())
@unittest.skipUnless((REPO_ROOT / "database.json").is_file(), "database.json is not built")
def test_the_status_line_names_what_is_left_to_review(self):
import shutil
import subprocess
held = REPO_ROOT / "bios" / "Sony" / "PlayStation Portable" / "ppge_atlas.zim"
if not held.is_file():
self.skipTest(f"{held} is not here")
shutil.copy(held, self.tmp / "ppge_atlas.zim")
(self.tmp / "mystery.bin").write_bytes(os.urandom(4096))
proc = subprocess.run(
[sys.executable, str(REPO_ROOT / "scripts" / "validate_pr.py"), "--markdown",
str(self.tmp / "mystery.bin"), str(self.tmp / "ppge_atlas.zim")],
capture_output=True, text=True, timeout=300, check=False,
)
self.assertEqual(proc.returncode, 0, proc.stderr)
self.assertIn("**Status**: no check failed, to review: 1 already held, "
"1 not recognised", proc.stdout)
self.assertNotIn("all checks passed", proc.stdout)
class PlatformHashLoading(_Fixture):
def test_missing_directory_yields_empty_sets(self):
known = validate_pr.load_platform_hashes(str(self.tmp / "absent"))
self.assertEqual(known, {"sha1": set(), "md5": set(), "sha256": set(), "crc32": set(),
"names": set(), "folders": {}, "sizes": {}, "entries": {}})
"names": set(), "folders": {}, "sizes": {}, "entries": {},
"named": {}})
def test_declared_hashes_and_names_are_collected(self):
platforms = self.tmp / "platforms"
+41
View File
@@ -13,6 +13,7 @@ import subprocess
import sys
import tempfile
import unittest
from unittest import mock
from pathlib import Path
import yaml
@@ -146,5 +147,45 @@ class RunFromAnyDirectory(unittest.TestCase):
self.assertIn("Already in the database at this path", messages)
class AnUnfetchedUpstreamIsFetched(unittest.TestCase):
"""CONTRIBUTING has the project added as `upstream` with no fetch. The
remote named the project, so nothing was fetched, upstream/main did not
exist, origin/main was the pushed branch, and --changed found nothing."""
def _git(self, cwd: Path, *args: str) -> None:
subprocess.run(["git", *args], cwd=cwd, check=True, capture_output=True, text=True)
def test_the_branch_file_is_found(self):
with tempfile.TemporaryDirectory(dir=REPO_ROOT / "tmp") as tmp:
root = Path(tmp)
project = root / "Abdess" / "retrobios.git"
fork = root / "fork.git"
clone = root / "clone"
for bare in (project, fork):
bare.mkdir(parents=True)
self._git(bare, "init", "-q", "--bare", "-b", "main")
clone.mkdir()
self._git(clone, "init", "-q", "-b", "main")
self._git(clone, "-c", "user.name=t", "-c", "user.email=t@t", "commit", "-q",
"--allow-empty", "-m", "base")
self._git(clone, "push", "-q", str(project), "main")
(clone / "bios").mkdir()
(clone / "bios" / "new.bin").write_bytes(b"x")
self._git(clone, "add", "bios/new.bin")
self._git(clone, "-c", "user.name=t", "-c", "user.email=t@t", "commit", "-q",
"-m", "add")
self._git(clone, "remote", "add", "origin", str(fork))
self._git(clone, "push", "-q", "origin", "main")
self._git(clone, "fetch", "-q", "origin")
self._git(clone, "remote", "add", "upstream", str(project))
cwd = os.getcwd()
os.chdir(clone)
try:
with mock.patch.object(validate_pr, "PROJECT_URLS", (str(project),)):
self.assertEqual(validate_pr.get_changed_files(), ["bios/new.bin"])
finally:
os.chdir(cwd)
if __name__ == "__main__":
unittest.main()