Files
libretro/tests/test_site_exports.py
Abdessamad Derraz 87e19191b6 fix: pair each source repository with its own pin
A profile whose builds live in separate repositories keys source,
upstream and source_commit by build mode. The site flattened the URLs
but read a single scalar revision, so a libretro fork could be pinned
to the standalone commit, and binding the object form into SQLite
failed outright once ymir adopted it. URL and revision are now read as
pairs.

The site also published a sitemap nothing pointed at, so robots.txt is
generated alongside it.
2026-08-11 00:56:00 +02:00

262 lines
9.7 KiB
Python

"""Regression tests for the public website data and citation contracts."""
from __future__ import annotations
import hashlib
import json
import sqlite3
import sys
import tempfile
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT / "scripts"))
from generate_site import ( # noqa: E402
_admonition_body,
_browser_title,
_forge_sources,
_source_ref_markdown,
decorate_markdown_pages,
generate_data_exports,
)
class SiteReferenceContracts(unittest.TestCase):
def setUp(self) -> None:
scratch = ROOT / "tmp" / "tests"
scratch.mkdir(parents=True, exist_ok=True)
self.temp = tempfile.TemporaryDirectory(dir=scratch)
self.docs = Path(self.temp.name) / "docs"
self.docs.mkdir()
def tearDown(self) -> None:
self.temp.cleanup()
@staticmethod
def _fixtures() -> tuple[dict, dict, dict, dict]:
payload = b"firmware"
sha1 = hashlib.sha1(payload).hexdigest()
md5 = hashlib.md5(payload).hexdigest()
sha256 = hashlib.sha256(payload).hexdigest()
db = {
"schema_version": 1,
"generated_at": "2026-08-09T00:00:00Z",
"total_files": 1,
"total_size": len(payload),
"files": {
sha1: {
"path": "bios/Test/fw.bin",
"name": "fw.bin",
"size": len(payload),
"sha1": sha1,
"md5": md5,
"sha256": sha256,
"crc32": "0f00ba11",
"adler32": "0f00ba11",
}
},
"indexes": {
"by_md5": {md5: sha1},
"by_name": {"fw.bin": [sha1]},
"by_crc32": {"0f00ba11": sha1},
"by_sha256": {sha256: sha1},
"by_path_suffix": {"fw.bin": [sha1]},
},
}
coverages = {
"test": {
"platform": "Test Platform",
"config": {
"platform": "Test Platform",
"cores": ["testemu"],
"systems": {
"test-system": {
"files": [
{
"name": "fw.bin",
"destination": "fw.bin",
"required": True,
"sha1": sha1,
"size": len(payload),
}
]
}
},
},
"total": 1,
"present": 1,
"verified": 1,
"untested": 0,
"missing": 0,
"core_present": 1,
"core_missing": 0,
"core_unsourceable": 0,
"details": [],
}
}
profiles = {
"testemu": {
"emulator": "Test Emulator",
"type": "libretro",
"source": "https://github.com/example/emulator",
"source_commit": "a" * 40,
"cores": ["testemu"],
"systems": ["test-system"],
"files": [
{
"name": "fw.bin",
"system": "test-system",
"required": True,
"source_ref": "src/firmware.cpp:10-12",
}
],
}
}
stats = {
"schema_version": 1,
"generated_at": "2026-08-09T00:00:00Z",
"files": 1,
"size_bytes": len(payload),
"platforms": 1,
"emulators": 1,
"systems": 1,
"catalog_matched": 0,
"source": "https://github.com/Abdess/retrobios",
"downloads": "https://github.com/Abdess/retrobios/releases/tag/v2026.08.06",
"composition": {
"systems": {"files": 1, "size_bytes": len(payload)},
"arcade": {"files": 0, "size_bytes": 0},
"game_data": {"files": 0, "size_bytes": 0},
},
}
return db, coverages, profiles, stats
def test_source_references_are_revision_pinned_links(self):
_, _, profiles, _ = self._fixtures()
rendered = _source_ref_markdown(
profiles["testemu"], "src/firmware.cpp:10-12"
)
self.assertIn("/blob/" + "a" * 40 + "/src/firmware.cpp#L10-L12", rendered)
def test_ambiguous_two_repository_path_is_never_linked(self):
"""A path that could live in either declared repository stays plain.
The libretro port and the original emulator are two different trees.
Linking an unattributable path to whichever came first publishes a
citation that resolves to nothing.
"""
profile = {
"source": "https://github.com/libretro/example-libretro",
"source_commit": "a" * 40,
"upstream": "https://gitlab.com/vendor/example",
"upstream_commit": "b" * 40,
}
rendered = _source_ref_markdown(profile, "jg.c:213-214")
self.assertEqual(rendered, "`jg.c:213-214`")
self.assertNotIn("http", rendered)
# A repository-name prefix removes the ambiguity, so it links again.
prefixed = _source_ref_markdown(profile, "example/jg.c:213-214")
self.assertIn("gitlab.com/vendor/example/-/blob/" + "b" * 40 + "/jg.c", prefixed)
def test_preprocessor_tokens_cannot_become_page_headings(self):
rendered = _admonition_body("First paragraph.\n\n#if FEATURE\nEnabled")
self.assertIn("\\#if FEATURE", rendered)
self.assertNotIn("\n #if FEATURE", rendered)
def test_browser_titles_distinguish_emulators_from_system_pages(self):
self.assertEqual(
_browser_title(Path("emulators/qemu.md"), "QEMU"),
"QEMU emulator firmware",
)
self.assertEqual(
_browser_title(Path("systems/qemu.md"), "QEMU"),
"QEMU systems",
)
def test_exports_have_catalog_hashes_and_queryable_sqlite(self):
db, coverages, profiles, stats = self._fixtures()
exports = generate_data_exports(
self.docs, db, coverages, profiles, stats
)
catalog = json.loads((self.docs / "api/v1/catalog.json").read_text())
self.assertEqual(catalog["count"], len(exports))
for item in catalog["items"]:
path = self.docs / item["url"]
self.assertTrue(path.is_file())
self.assertEqual(hashlib.sha256(path.read_bytes()).hexdigest(), item["sha256"])
with sqlite3.connect(self.docs / "downloads/retrobios.sqlite") as connection:
self.assertEqual(connection.execute("SELECT count(*) FROM files").fetchone()[0], 1)
self.assertEqual(connection.execute("SELECT count(*) FROM platforms").fetchone()[0], 1)
self.assertEqual(connection.execute("SELECT count(*) FROM emulators").fetchone()[0], 1)
def test_page_metadata_is_specific_and_idempotent(self):
page = self.docs / "gaps.md"
page.write_text("# Gap Analysis - RetroBIOS\n\nCurrent unresolved entries.\n")
decorate_markdown_pages(self.docs)
decorate_markdown_pages(self.docs)
content = page.read_text()
self.assertEqual(content.count('type="application/ld+json"'), 1)
self.assertIn("Current RetroBIOS verification gaps", content)
self.assertIn('"@type":"TechArticle"', content)
class PerModeSourcePins(unittest.TestCase):
"""A profile whose builds live in separate repositories.
`source`, `upstream` and `source_commit` may each be keyed by build mode.
Reading the URL from one mode and the revision from another produces a
permalink into a tree that never held that line, and binding the object
form straight into SQLite fails outright.
"""
PROFILE = {
"emulator": "twobuilds",
"source": {
"standalone": "https://github.com/vendor/emu",
"libretro": "https://github.com/porter/emu-libretro",
},
"upstream": "https://github.com/vendor/emu",
"source_commit": {"standalone": "a" * 40, "libretro": "b" * 40},
}
def test_each_repository_keeps_its_own_revision(self):
pairs = {
(repo.slug, pin) for repo, pin in _forge_sources(self.PROFILE)
}
self.assertIn(("vendor/emu", "a" * 40), pairs)
self.assertIn(("porter/emu-libretro", "b" * 40), pairs)
# The libretro fork must never be pinned to the standalone revision.
self.assertNotIn(("porter/emu-libretro", "a" * 40), pairs)
self.assertNotIn(("vendor/emu", "b" * 40), pairs)
def test_a_build_mode_label_is_resolved_first(self):
repo, pin = _forge_sources(self.PROFILE, "libretro")[0]
self.assertEqual((repo.slug, pin), ("porter/emu-libretro", "b" * 40))
def test_the_plain_string_form_is_unchanged(self):
profile = {
"source": "https://github.com/vendor/emu",
"source_commit": "c" * 40,
}
self.assertEqual(
[(repo.slug, pin) for repo, pin in _forge_sources(profile)],
[("vendor/emu", "c" * 40)],
)
def test_sqlite_export_accepts_a_per_mode_commit(self):
from generate_site import _json_text
self.assertEqual(
_json_text(self.PROFILE["source_commit"]),
'{"libretro": "' + "b" * 40 + '", "standalone": "' + "a" * 40 + '"}',
)
self.assertEqual(_json_text("c" * 40), "c" * 40)
if __name__ == "__main__":
unittest.main()