diff --git a/scripts/generate_site.py b/scripts/generate_site.py
index b177260f..1bcd12cf 100644
--- a/scripts/generate_site.py
+++ b/scripts/generate_site.py
@@ -1873,11 +1873,15 @@ def _file_badges(f: dict, in_repo: bool) -> list[str]:
badges.append(
'optional'
)
- if not in_repo:
+ if not in_repo and f.get("unsourceable"):
+ badges.append(
+ 'unsourceable'
+ )
+ elif not in_repo:
badges.append(
'missing'
)
- elif in_repo:
+ else:
badges.append(
'in repo'
)
@@ -2236,7 +2240,9 @@ def _availability_check(db: dict, data_names):
def _file_available(f: dict) -> bool:
"""Check if a file is available using the same resolution as cross_reference."""
- if not f.get("name"):
+ # cross_reference sets these aside before resolving: the file found
+ # under the name belongs to another game or release.
+ if not f.get("name") or f.get("unsourceable"):
return False
if f.get("storage", "") in ("release", "large_file"):
return True
@@ -2367,14 +2373,18 @@ def generate_emulator_page(
bios_zips = [f for f in files if f.get("category") == "bios_zip"]
in_repo_count = sum(1 for f in files if _file_available(f))
- missing_count = len(files) - in_repo_count
+ unsourceable_count = sum(1 for f in files if f.get("unsourceable"))
+ missing_count = len(files) - in_repo_count - unsourceable_count
req_count = sum(1 for f in files if f.get("required"))
opt_count = len(files) - req_count
hle_count = sum(1 for f in files if f.get("hle_fallback"))
parts = [f"**{len(files)} files**"]
parts.append(f"{req_count} required, {opt_count} optional")
- parts.append(f"{in_repo_count} in repo, {missing_count} missing")
+ held = f"{in_repo_count} in repo, {missing_count} missing"
+ if unsourceable_count:
+ held += f", {unsourceable_count} unsourceable"
+ parts.append(held)
if hle_count:
parts.append(f"{hle_count} with HLE fallback")
lines.append(" | ".join(parts))
diff --git a/tests/test_site_availability.py b/tests/test_site_availability.py
new file mode 100644
index 00000000..19012687
--- /dev/null
+++ b/tests/test_site_availability.py
@@ -0,0 +1,62 @@
+"""An emulator page never shows an unsourceable entry as held.
+
+The page resolved each entry by name without the `unsourceable:` check
+cross_reference runs first: ioquake3's pak0.pk3 wore a green "in repo"
+badge, served by the pak0.pk3 of another game, and 52 such entries read as
+held across the site while the gaps page listed them as known absences.
+"""
+
+from __future__ import annotations
+
+import sys
+import unittest
+from pathlib import Path
+
+import yaml
+
+REPO_ROOT = Path(__file__).resolve().parent.parent
+sys.path.insert(0, str(REPO_ROOT / "scripts"))
+
+from generate_site import _availability_check, _file_badges
+
+SHA1 = "a" * 40
+DB = {
+ "files": {SHA1: {"name": "pak0.pk3", "sha1": SHA1, "path": "bios/x/pak0.pk3"}},
+ "indexes": {"by_name": {"pak0.pk3": [SHA1]}},
+}
+
+
+class UnsourceableIsNotHeld(unittest.TestCase):
+ def test_a_homonym_does_not_stand_in(self):
+ available = _availability_check(DB, set())
+ self.assertTrue(available({"name": "pak0.pk3"}))
+ self.assertFalse(available({"name": "pak0.pk3", "unsourceable": "retail data"}))
+
+ def test_the_badge_says_why(self):
+ entry = {"name": "pak0.pk3", "unsourceable": "retail data"}
+ badges = " ".join(_file_badges(entry, False))
+ self.assertIn("unsourceable", badges)
+ self.assertNotIn(">missing<", badges)
+ self.assertNotIn("in repo", badges)
+
+ def test_no_profile_entry_is_shown_held(self):
+ from common import load_database
+
+ db_path = REPO_ROOT / "database.json"
+ if not db_path.exists():
+ self.skipTest("database.json is not built")
+ available = _availability_check(load_database(str(db_path)), set())
+ shown_held = []
+ for path in sorted((REPO_ROOT / "emulators").glob("*.yml")):
+ document = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
+ shown_held.extend(
+ f"{path.stem}: {entry['name']}"
+ for entry in document.get("files") or []
+ if isinstance(entry, dict) and entry.get("unsourceable")
+ and available(entry)
+ )
+ self.assertEqual(shown_held, [])
+
+
+if __name__ == "__main__":
+ unittest.main()