mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-10-25 21:29:05 -05:00
fix: one pack answer and fuller reference data
The API reads release_pack from the registry entry, not the inherited config, and the picker gets release-advertised.json. The lookup counts emulators that read a file by name, every record names what was declared, the Data page lists all schemas, the target lists and nested manifest fields, and systems.json items carry their api address.
This commit is contained in:
1 parent
329b9e1bda
commit
801b8bbda0
5 files changed
+448
-16
No files matched your search
@@ -166,7 +166,7 @@
|
||||
},
|
||||
"registry": {
|
||||
"type": "object",
|
||||
"description": "The platform's registry entry beside its list: install detection, logo, scraper and schedule. Not part of the list."
|
||||
"description": "The platform's registry entry beside its list: install detection, logo, scraper and schedule. Not part of the list. release_pack is the platform's own entry, never its parent's: true when a release publishes its ZIP."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -411,9 +411,10 @@
|
||||
"sha1",
|
||||
"md5",
|
||||
"crc32",
|
||||
"sha256"
|
||||
"sha256",
|
||||
"name"
|
||||
],
|
||||
"description": "On a copy of a platform or emulator record filed under another hash of its file: the algorithm of the value its owner wrote."
|
||||
"description": "On a platform or emulator record: the algorithm of the value its owner wrote, filed under that value and under the file's other hashes. name: the emulator reads the file by its name and declares no hash that matches it."
|
||||
},
|
||||
"owner": {
|
||||
"type": "string"
|
||||
@@ -501,6 +502,13 @@
|
||||
],
|
||||
"description": "Site-absolute path of the console section holding the id's files, null when no section holds them."
|
||||
},
|
||||
"api": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
],
|
||||
"description": "Site-absolute path of the JSON document holding the id's files, api/v1/systems/<slug>.json, keyed by the manufacturer page. Null when no page holds them."
|
||||
},
|
||||
"sha1": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
|
||||
+166
-10
@@ -44,6 +44,7 @@ from common import (
|
||||
compute_composition,
|
||||
count_catalog_matched,
|
||||
list_registered_platforms,
|
||||
list_release_pack_platforms,
|
||||
load_platform_registry,
|
||||
load_database,
|
||||
load_emulator_profiles,
|
||||
@@ -964,9 +965,21 @@ def _split_contract(config: dict) -> tuple[dict, dict]:
|
||||
contract = {key: value for key, value in config.items() if key in allowed}
|
||||
registry = {key: value for key, value in config.items()
|
||||
if key not in allowed and key != "_key"}
|
||||
# The merged config gives a child its parent's release_pack: Lakka and
|
||||
# RetroPie said true in the API while every page said they had no ZIP.
|
||||
# The registry's own entry decides, as it does for the pages.
|
||||
released = config.get("_key") in _release_pack_platforms()
|
||||
if released or "release_pack" in registry:
|
||||
registry["release_pack"] = released
|
||||
return contract, registry
|
||||
|
||||
|
||||
@functools.lru_cache(maxsize=1)
|
||||
def _release_pack_platforms() -> frozenset[str]:
|
||||
return frozenset(list_release_pack_platforms(
|
||||
str(_REPO_ROOT / "platforms"), include_archived=True))
|
||||
|
||||
|
||||
# Per-entry fields a folder check needs beyond the hashes: an md5 is a ZIP
|
||||
# member's when zipped_file is set, and a CSV without it judged 106 good
|
||||
# Batocera files wrong.
|
||||
@@ -1490,6 +1503,7 @@ def generate_data_exports(
|
||||
profiles: dict,
|
||||
stats: dict,
|
||||
gap_report: dict | None = None,
|
||||
consumers: FileConsumers | None = None,
|
||||
) -> list[dict]:
|
||||
"""Create versioned static API, CSV and SQLite metadata snapshots."""
|
||||
api = docs / "api" / "v1"
|
||||
@@ -1517,8 +1531,8 @@ def generate_data_exports(
|
||||
"file-locations.json": ("file-locations", locations),
|
||||
}
|
||||
write_lookup_shards(
|
||||
api, file_lookup_records(db, coverages, profiles, locations, members), generated_at,
|
||||
commit,
|
||||
api, file_lookup_records(db, coverages, profiles, locations, members, consumers),
|
||||
generated_at, commit,
|
||||
)
|
||||
for filename, (kind, items) in envelopes.items():
|
||||
document = {
|
||||
@@ -1714,6 +1728,21 @@ def generate_data_exports(
|
||||
return catalog_items
|
||||
|
||||
|
||||
def write_release_advertised(generated: Path) -> None:
|
||||
"""The release record as the pages read it, for the file picker.
|
||||
|
||||
The picker imported release.json whole and offered the ZIP of six setups
|
||||
the registry no longer releases, while their pages said "No ZIP". It
|
||||
reads this file instead: the platforms marked release_pack only.
|
||||
"""
|
||||
generated.mkdir(parents=True, exist_ok=True)
|
||||
write_if_changed(
|
||||
str(generated / "release-advertised.json"),
|
||||
json.dumps(release_record.load_advertised(), ensure_ascii=False, indent=1,
|
||||
sort_keys=True) + "\n",
|
||||
)
|
||||
|
||||
|
||||
def _publish_manifests(api: Path) -> list[tuple]:
|
||||
"""The install manifests and the release record, beside the API.
|
||||
|
||||
@@ -1732,6 +1761,15 @@ def _publish_manifests(api: Path) -> list[tuple]:
|
||||
_record(target)
|
||||
assets.append((target, f"Install manifest: {source.stem}", "application/json",
|
||||
"schemas/install-manifest.schema.json"))
|
||||
# The target lists install.py --target reads: no catalog entry, and
|
||||
# api/v1/targets/ answered 404.
|
||||
targets = api / "targets"
|
||||
targets.mkdir(parents=True, exist_ok=True)
|
||||
for source in sorted((_REPO_ROOT / "install" / "targets").glob("*.json")):
|
||||
shutil.copy2(source, targets / source.name)
|
||||
_record(targets / source.name)
|
||||
assets.append((targets / source.name, f"Target list: {source.stem}",
|
||||
"application/json", "schemas/target-manifest.schema.json"))
|
||||
release = _REPO_ROOT / "release.json"
|
||||
if release.is_file():
|
||||
shutil.copy2(release, api / "release.json")
|
||||
@@ -1791,6 +1829,7 @@ def system_index_items(db: dict, coverages: dict, profiles: dict) -> list[dict]:
|
||||
for sys_id in ids:
|
||||
held.setdefault(sys_id, set()).update(found)
|
||||
manufacturers = {_slug(mfr): mfr for mfr in _group_by_manufacturer(db)}
|
||||
base = urllib.parse.urlsplit(SITE_URL).path
|
||||
items = []
|
||||
for sys_id in sorted(system_ids(profiles, [c["config"] for c in coverages.values()])):
|
||||
section = system_section(sys_id)
|
||||
@@ -1803,8 +1842,12 @@ def system_index_items(db: dict, coverages: dict, profiles: dict) -> list[dict]:
|
||||
url = _site_path(console_href(*section)) if section else None
|
||||
if url is None and sha1s:
|
||||
manufacturer, url = _first_file_place(sys_id, sha1s, files, profiles, card_page)
|
||||
# The id's JSON document: api/v1/systems/ is keyed by the manufacturer
|
||||
# page, and sony-playstation.json answered 404.
|
||||
holder = section[0] if section else _slug(manufacturer) if manufacturer else ""
|
||||
api = f"{base}api/v1/systems/{holder}.json" if holder else None
|
||||
items.append({"id": sys_id, "name": name, "manufacturer": manufacturer,
|
||||
"url": url, "sha1": sha1s})
|
||||
"url": url, "api": api, "sha1": sha1s})
|
||||
return items
|
||||
|
||||
|
||||
@@ -1855,6 +1898,19 @@ def _is_manifest(item: dict) -> bool:
|
||||
return item["url"].startswith("api/v1/manifests/")
|
||||
|
||||
|
||||
def _is_target_list(item: dict) -> bool:
|
||||
return item["url"].startswith("api/v1/targets/")
|
||||
|
||||
|
||||
def _served_schemas() -> list[tuple[str, str]]:
|
||||
"""(file name, title) of every schema api/v1/schemas/ serves, from the folder
|
||||
the export copies: the page linked 7 of 11."""
|
||||
return [
|
||||
(path.name, str(json.loads(path.read_text(encoding="utf-8")).get("title") or path.stem))
|
||||
for path in sorted((_REPO_ROOT / "schemas").glob("*.schema.json"))
|
||||
]
|
||||
|
||||
|
||||
# A URL template written in prose (https://.../<ref>/<repo_path>): escaped,
|
||||
# the autolinker made a link of it, <ref> on screen and a 404 behind it.
|
||||
_URL_TEMPLATE = re.compile(r"(?<!`)https?://[^\s`]*<[^\s`]*?(?=[.,;]?(?:\s|$))")
|
||||
@@ -1881,6 +1937,38 @@ def _other_fields(schema: dict, shown: tuple[str, ...]) -> list[str]:
|
||||
return ["| Field | Meaning |", "|-------|---------|", *_schema_field_rows(schema, rest)]
|
||||
|
||||
|
||||
def _nested_fields(schema: dict, prefix: str = "") -> list[str]:
|
||||
"""A table for every object below *schema*, at any depth, and one line
|
||||
per code where a field lists its codes' meanings (enumDescriptions).
|
||||
|
||||
The top-level and `files` tables left 50 nested fields undescribed, all
|
||||
of `omitted_files` among them: a tool could not say why a file was not
|
||||
installed.
|
||||
"""
|
||||
lines: list[str] = []
|
||||
for key, spec in (schema.get("properties") or {}).items():
|
||||
path = f"{prefix}{key}"
|
||||
codes = spec.get("enumDescriptions")
|
||||
if codes:
|
||||
lines.extend([f"Codes of `{path}`:", ""])
|
||||
lines.extend(f"- `{code}`: {text}" for code, text in zip(spec["enum"], codes))
|
||||
lines.append("")
|
||||
for label, child, child_path in (
|
||||
("Each entry of", spec.get("items"), f"{path}[]"),
|
||||
("Each value of", spec.get("additionalProperties"), f"{path}{{}}"),
|
||||
("Fields of", spec, path),
|
||||
):
|
||||
if not isinstance(child, dict) or not child.get("properties"):
|
||||
continue
|
||||
lines.extend([
|
||||
f"{label} `{path}`:", "",
|
||||
"| Field | Meaning |", "|-------|---------|",
|
||||
*_schema_field_rows(child, tuple(child["properties"])), "",
|
||||
])
|
||||
lines.extend(_nested_fields(child, f"{child_path}."))
|
||||
return lines
|
||||
|
||||
|
||||
# The manifest fields an integrator reads first, in the order they matter.
|
||||
MANIFEST_TOP_FIELDS = ("platform", "version", "base_destination", "files",
|
||||
"omitted_files", "pack_files", "core_names", "system_cores")
|
||||
@@ -1918,10 +2006,12 @@ def generate_data_page(exports: list[dict]) -> str:
|
||||
(_REPO_ROOT / "schemas" / "install-manifest.schema.json").read_text(encoding="utf-8"))
|
||||
snippet = (_REPO_ROOT / "scripts" / "check_platform_folder.py").read_text(encoding="utf-8")
|
||||
manifests = [item for item in exports if _is_manifest(item)]
|
||||
targets = [item for item in exports if _is_target_list(item)]
|
||||
no_list = _platforms_without_a_list()
|
||||
lines = [
|
||||
"---",
|
||||
"terms: manifest, schema, check a folder, rate limit, api",
|
||||
"terms: manifest, schema, json schema, license, check a folder, "
|
||||
"check bios folder script, rate limit, api",
|
||||
"---",
|
||||
"",
|
||||
"# Data and API",
|
||||
@@ -1949,7 +2039,8 @@ def generate_data_page(exports: list[dict]) -> str:
|
||||
"|---------|----------|--------|-----:|",
|
||||
]
|
||||
for item in exports:
|
||||
if item["media_type"] != "application/json" or _is_manifest(item):
|
||||
if (item["media_type"] != "application/json" or _is_manifest(item)
|
||||
or _is_target_list(item)):
|
||||
continue
|
||||
schema = (
|
||||
f"[JSON Schema](api/v1/{item['schema']})"
|
||||
@@ -1966,6 +2057,10 @@ def generate_data_page(exports: list[dict]) -> str:
|
||||
"URL prefix (`/api/v2/`), and fields may only be added compatibly within v1. "
|
||||
"Each schema's `$id` is the address it is served at.",
|
||||
"",
|
||||
"Schemas:",
|
||||
"",
|
||||
*(f"- [`{name}`](api/v1/schemas/{name}): {title}" for name, title in _served_schemas()),
|
||||
"",
|
||||
"`database.json`, `stats.json`, `release.json` and the manifests are not "
|
||||
"envelopes. Their revision is the `commit` of `catalog.json` that lists "
|
||||
"their SHA256.",
|
||||
@@ -1983,7 +2078,8 @@ def generate_data_page(exports: list[dict]) -> str:
|
||||
"",
|
||||
"By id: [`systems.json`](api/v1/systems.json) gives each system id a platform "
|
||||
"or a profile writes (`sony-playstation`) with its name, its manufacturer, the "
|
||||
"`url` of the section holding its files and their SHA1. "
|
||||
"`url` of the section holding its files, the `api` document of that section "
|
||||
"and the files' SHA1. "
|
||||
"[`cores.json`](api/v1/cores.json) gives each core name the buildbot or a "
|
||||
"platform uses (`mednafen_psx`) the profile it designates (`beetle_psx`), "
|
||||
"whose document is `api/v1/emulators/<profile>.json`. A name two profiles "
|
||||
@@ -2041,6 +2137,16 @@ def generate_data_page(exports: list[dict]) -> str:
|
||||
f"[{item['url'].rsplit('/', 1)[-1].removesuffix('.json')}]({item['url']})"
|
||||
for item in manifests) + "." if manifests else "",
|
||||
"",
|
||||
"`api/v1/targets/<platform>.json` gives each hardware target of a platform "
|
||||
"the cores it runs, valid against "
|
||||
"[`target-manifest.schema.json`](api/v1/schemas/target-manifest.schema.json). "
|
||||
"`install.py --target` reads the same file. Like the manifests, they are "
|
||||
"copied byte for byte and are not envelopes.",
|
||||
"",
|
||||
"Target lists: " + ", ".join(
|
||||
f"[{item['url'].rsplit('/', 1)[-1].removesuffix('.json')}]({item['url']})"
|
||||
for item in targets) + "." if targets else "",
|
||||
"",
|
||||
"| Field | Meaning |",
|
||||
"|-------|---------|",
|
||||
*_schema_field_rows(manifest_schema, MANIFEST_TOP_FIELDS),
|
||||
@@ -2059,6 +2165,9 @@ def generate_data_page(exports: list[dict]) -> str:
|
||||
"Each entry of `files`:",
|
||||
"",
|
||||
*_other_fields(manifest_schema["$defs"]["file"], MANIFEST_FILE_FIELDS),
|
||||
"",
|
||||
*_nested_fields(manifest_schema),
|
||||
*_nested_fields(manifest_schema["$defs"]["file"], "files[]."),
|
||||
]),
|
||||
"## Check a folder for one platform",
|
||||
"",
|
||||
@@ -2088,8 +2197,12 @@ def generate_data_page(exports: list[dict]) -> str:
|
||||
"The script is [`scripts/check_platform_folder.py`]"
|
||||
f"({REPO_URL}/blob/main/scripts/check_platform_folder.py). A test runs it "
|
||||
"against the verifier on a sample folder, so the two give the same answer. "
|
||||
"`install.py --check` answers another question: whether the folder holds "
|
||||
"what the pack installs, the manifest above.",
|
||||
"It exits 1 when a file is wrong or missing. For an unknown platform it "
|
||||
"prints the known ones.",
|
||||
"",
|
||||
"`install.py --check` answers another question by default: whether the folder "
|
||||
"holds what the pack installs, the manifest above. With `--declared-only`, or "
|
||||
"from the `declared` event, `install.py --check` answers it too.",
|
||||
"",
|
||||
"## Bulk downloads",
|
||||
"",
|
||||
@@ -4020,7 +4133,8 @@ def generate_files_page() -> str:
|
||||
"""
|
||||
return "\n".join([
|
||||
"---",
|
||||
"terms: check my dump, md5 check, verify bios file, check hash",
|
||||
"terms: check my dump, md5 check, verify bios file, check hash, "
|
||||
"is my bios correct, is my bios right",
|
||||
"---",
|
||||
"",
|
||||
"# Find or check a file",
|
||||
@@ -4194,6 +4308,7 @@ def file_lookup_records(
|
||||
profiles: dict,
|
||||
locations: list[dict],
|
||||
members: dict | None = None,
|
||||
consumers: FileConsumers | None = None,
|
||||
) -> list[dict]:
|
||||
"""Every hash the site knows, one record per value, for files/?hash=.
|
||||
|
||||
@@ -4262,11 +4377,51 @@ def file_lookup_records(
|
||||
**({"system_name": entry_label} if entry_label else {}),
|
||||
})
|
||||
records += _held_under_every_hash(records, files, db.get("indexes") or {})
|
||||
for record in records:
|
||||
if record["layer"] in ("platform", "emulator"):
|
||||
record.setdefault("declared", record["algorithm"])
|
||||
if consumers is not None:
|
||||
records += _read_by_name(records, files, consumers)
|
||||
unique = {json.dumps(r, sort_keys=True): r for r in records}
|
||||
return sorted(unique.values(), key=lambda r: (
|
||||
r["value"], r["layer"], r.get("owner", ""), r["name"], r["url"]))
|
||||
|
||||
|
||||
def _read_by_name(records: list[dict], files: dict, consumers: FileConsumers) -> list[dict]:
|
||||
"""The emulators the file's card names that no declared hash reached.
|
||||
|
||||
The card counts every profile entry resolving to the file, by name
|
||||
included: scph5501.bin named 11 emulators on its card and 9 in the
|
||||
lookup, PCSX-ReARMed and PCSX1 reading it by name. Each is written under
|
||||
every hash of the file, `declared` saying no hash was.
|
||||
"""
|
||||
reached = {(r["sha1"], r["owner"]) for r in records
|
||||
if r["layer"] == "emulator" and r.get("sha1")}
|
||||
added: list[dict] = []
|
||||
for sha1, emulators in sorted(consumers.emulators.items()):
|
||||
entry = files.get(sha1)
|
||||
if entry is None:
|
||||
continue
|
||||
for emulator in sorted(emulators):
|
||||
if (sha1, emulator) in reached:
|
||||
continue
|
||||
index = consumers.emulator_entries.get(sha1, {}).get(emulator)
|
||||
entries = consumers.emulator_files.get(emulator) or []
|
||||
if index is None or index >= len(entries):
|
||||
continue
|
||||
anchor = _emulator_card_anchor(emulator, entries, index)
|
||||
url = _site_path(f"emulators/{emulator_card_page(emulator, anchor)}.md#{anchor}")
|
||||
name = str(entries[index].get("name", ""))
|
||||
for algorithm in HASH_ALGORITHMS:
|
||||
for value in _hash_values(entry.get(algorithm)):
|
||||
added.append({
|
||||
"value": value, "algorithm": algorithm, "layer": "emulator",
|
||||
"owner": emulator, "name": name, "url": url, "sha1": sha1,
|
||||
"declared": "name",
|
||||
})
|
||||
return added
|
||||
|
||||
|
||||
def _collection_sha1(files: dict, indexes: dict, algorithm: str, value: str) -> str | None:
|
||||
"""The collection file a declared value designates, None when none does."""
|
||||
if algorithm == "sha1":
|
||||
@@ -10117,8 +10272,9 @@ def _generate(args: argparse.Namespace) -> None:
|
||||
|
||||
print("Generating static API and bulk data exports...")
|
||||
exports = generate_data_exports(
|
||||
public, db, coverages, profiles, stats, gap_report
|
||||
public, db, coverages, profiles, stats, gap_report, consumers
|
||||
)
|
||||
write_release_advertised(Path(args.site_dir) / "src" / "generated")
|
||||
write_if_changed(str(docs / "data.md"), generate_data_page(exports) + reference_next())
|
||||
PAGE_DATA["data.md"] = dataset_structured_data(exports, db)
|
||||
|
||||
|
||||
@@ -267,7 +267,7 @@ class EveryEnvelopeCarriesTheCommit(unittest.TestCase):
|
||||
raise unittest.SkipTest("not a git checkout")
|
||||
lacking = [
|
||||
path.relative_to(API).as_posix() for path in sorted(API.rglob("*.json"))
|
||||
if path.parent.name not in ("schemas", "manifests")
|
||||
if path.parent.name not in ("schemas", "manifests", "targets")
|
||||
and path.name not in ("database.json", "stats.json", "release.json")
|
||||
and "commit" not in json.loads(path.read_text())
|
||||
]
|
||||
|
||||
@@ -263,14 +263,16 @@ class TheReferencePagesAnswerTheirWords(unittest.TestCase):
|
||||
def test_find_or_check_a_file(self):
|
||||
page = generate_site.generate_files_page()
|
||||
self.assertTrue(page.startswith(
|
||||
"---\nterms: check my dump, md5 check, verify bios file, check hash\n---\n"))
|
||||
"---\nterms: check my dump, md5 check, verify bios file, check hash, "
|
||||
"is my bios correct, is my bios right\n---\n"))
|
||||
self.assertIn("\n# Find or check a file\n\nCheck a BIOS dump: paste its hash "
|
||||
"or choose the file.\n", page)
|
||||
|
||||
def test_the_data_page(self):
|
||||
page = generate_site.generate_data_page([])
|
||||
self.assertTrue(page.startswith(
|
||||
"---\nterms: manifest, schema, check a folder, rate limit, api\n---\n"))
|
||||
"---\nterms: manifest, schema, json schema, license, check a folder, "
|
||||
"check bios folder script, rate limit, api\n---\n"))
|
||||
self.assertIn("## Hosting, rate limits and caching", page)
|
||||
self.assertIn("HTTP 429", page)
|
||||
self.assertIn("crypto.subtle.digest(\"SHA-1\"", page)
|
||||
|
||||
@@ -0,0 +1,266 @@
|
||||
"""Reference pages and data (round 5, package 8).
|
||||
|
||||
The API said Lakka and RetroPie had a release pack while their pages said
|
||||
none, 50 nested manifest fields and the nine omission codes had no
|
||||
description, the lookup named 9 emulators where the card named 11, four
|
||||
served schemas and the target lists appeared nowhere on the Data page, the
|
||||
folder check exited 0 on a folder missing every file, and a system id did
|
||||
not lead to its JSON document. Tests reading the generated site skip when
|
||||
it has not been generated.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import json
|
||||
import runpy
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
import urllib.error
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(REPO_ROOT / "scripts"))
|
||||
|
||||
import generate_site # noqa: E402
|
||||
import release_record # noqa: E402
|
||||
from common import list_release_pack_platforms # noqa: E402
|
||||
|
||||
PUBLIC = REPO_ROOT / "website" / "public"
|
||||
API = PUBLIC / "api" / "v1"
|
||||
GENERATED = REPO_ROOT / "website" / "src" / "generated"
|
||||
SCHEMA = json.loads((REPO_ROOT / "schemas" / "install-manifest.schema.json").read_text())
|
||||
|
||||
|
||||
def _built(path: Path) -> Path:
|
||||
if not path.exists():
|
||||
raise unittest.SkipTest(f"{path.relative_to(REPO_ROOT)} not generated")
|
||||
return path
|
||||
|
||||
|
||||
def _released() -> set[str]:
|
||||
return set(list_release_pack_platforms(str(REPO_ROOT / "platforms"), include_archived=True))
|
||||
|
||||
|
||||
class OneAnswerOnReleasePacks(unittest.TestCase):
|
||||
"""8.1: the API, the picker's data and the pages read the registry."""
|
||||
|
||||
def test_a_child_does_not_inherit_its_parents_pack(self):
|
||||
_contract, registry = generate_site._split_contract(
|
||||
{"_key": "lakka", "inherits": "retroarch", "release_pack": True})
|
||||
self.assertIs(registry["release_pack"], "lakka" in _released())
|
||||
_contract, registry = generate_site._split_contract({"_key": "minui"})
|
||||
self.assertNotIn("release_pack", registry)
|
||||
_contract, registry = generate_site._split_contract({"_key": "retroarch"})
|
||||
self.assertIs(registry["release_pack"], "retroarch" in _released())
|
||||
|
||||
def test_api_release_pack_set_equals_the_registry(self):
|
||||
found = set()
|
||||
for path in _built(API / "platforms").glob("*.json"):
|
||||
item = json.loads(path.read_text())["items"][0]
|
||||
if item["registry"].get("release_pack"):
|
||||
found.add(item["id"])
|
||||
self.assertEqual(found, _released())
|
||||
|
||||
def test_the_picker_file_is_the_advertised_record(self):
|
||||
with tempfile.TemporaryDirectory(dir=REPO_ROOT / "tmp" if (REPO_ROOT / "tmp").is_dir()
|
||||
else None) as folder:
|
||||
generate_site.write_release_advertised(Path(folder))
|
||||
written = json.loads((Path(folder) / "release-advertised.json").read_text())
|
||||
self.assertEqual(written, release_record.load_advertised())
|
||||
served = {p for pack in written.get("packs", {}).values() for p in pack["platforms"]}
|
||||
self.assertLessEqual(served, _released())
|
||||
|
||||
def test_the_built_picker_file(self):
|
||||
written = json.loads(_built(GENERATED / "release-advertised.json").read_text())
|
||||
self.assertEqual(written, release_record.load_advertised())
|
||||
|
||||
|
||||
def _properties(node: object, path: str = ""):
|
||||
"""(path, spec) of every property at any depth, $defs included."""
|
||||
if not isinstance(node, dict):
|
||||
return
|
||||
for key, spec in (node.get("properties") or {}).items():
|
||||
yield f"{path}{key}", spec
|
||||
yield from _properties(spec, f"{path}{key}.")
|
||||
for key in ("items", "additionalProperties"):
|
||||
yield from _properties(node.get(key), path)
|
||||
for name, spec in (node.get("$defs") or {}).items():
|
||||
yield from _properties(spec, f"#{name}.")
|
||||
for key in ("oneOf", "anyOf", "allOf"):
|
||||
for spec in node.get(key) or []:
|
||||
yield from _properties(spec, path)
|
||||
|
||||
|
||||
class EveryManifestFieldIsDescribed(unittest.TestCase):
|
||||
"""8.2: every property at any depth, and each omission code."""
|
||||
|
||||
def test_every_property_has_a_description(self):
|
||||
missing = [path for path, spec in _properties(SCHEMA)
|
||||
if isinstance(spec, dict) and not spec.get("description")]
|
||||
self.assertEqual(missing, [])
|
||||
|
||||
def test_each_reason_code_has_a_line(self):
|
||||
page = generate_site.generate_data_page([])
|
||||
for path, spec in _properties(SCHEMA):
|
||||
if path.endswith(".reason"):
|
||||
self.assertEqual(len(spec["enumDescriptions"]), len(spec["enum"]), path)
|
||||
for code in spec["enum"]:
|
||||
self.assertRegex(page, rf"\n- `{code}`: \S")
|
||||
|
||||
def test_nested_tables_are_on_the_page(self):
|
||||
page = generate_site.generate_data_page([])
|
||||
for label in ("Each entry of `omitted_files`:", "Each entry of `data_directories`:",
|
||||
"Each value of `data_directories[].files`:", "Fields of `save_directory`:",
|
||||
"Each entry of `files[].alternatives`:",
|
||||
"Each entry of `files[].slot_choices`:",
|
||||
"Each entry of `files[].declared_choices`:",
|
||||
"Each entry of `files[].absent_claims`:"):
|
||||
self.assertIn(label, page)
|
||||
for key in ("below_platform_folder", "strip_components", "by_core"):
|
||||
self.assertIn(f"`{key}`", page)
|
||||
|
||||
|
||||
class FakeConsumers:
|
||||
def __init__(self, sha1: str) -> None:
|
||||
self.emulators = {sha1: {"byname"}}
|
||||
self.emulator_entries = {sha1: {"byname": 0}}
|
||||
self.emulator_files = {"byname": [{"name": "bios.bin"}]}
|
||||
|
||||
|
||||
class LookupCountsEmulatorsLikeTheCard(unittest.TestCase):
|
||||
"""8.3: an emulator reading the file by name is in the verdict."""
|
||||
|
||||
SHA1 = "a" * 40
|
||||
|
||||
def records(self) -> list[dict]:
|
||||
db = {"files": {self.SHA1: {"name": "bios.bin", "path": "bios/Sony/PlayStation/bios.bin",
|
||||
"sha1": self.SHA1, "md5": "b" * 32}},
|
||||
"indexes": {"by_md5": {"b" * 32: self.SHA1}}}
|
||||
coverages = {"setup": {"config": {"systems": {"sony-playstation": {"files": [
|
||||
{"name": "bios.bin", "md5": "b" * 32}]}}}}}
|
||||
with mock.patch.object(generate_site, "_console_system_names", return_value={}), \
|
||||
mock.patch.object(generate_site, "_lookup_system_name", return_value=None), \
|
||||
mock.patch.object(generate_site, "emulator_card_page", side_effect=lambda k, a: k):
|
||||
return generate_site.file_lookup_records(
|
||||
db, coverages, {}, [{"sha1": self.SHA1, "url": "/x"}], None,
|
||||
FakeConsumers(self.SHA1))
|
||||
|
||||
def test_by_name_under_every_hash(self):
|
||||
records = self.records()
|
||||
for value in (self.SHA1, "b" * 32):
|
||||
named = [r for r in records if r["value"] == value and r.get("owner") == "byname"]
|
||||
self.assertEqual(len(named), 1, value)
|
||||
self.assertEqual(named[0]["declared"], "name")
|
||||
|
||||
def test_every_setup_and_emulator_record_says_what_was_declared(self):
|
||||
for record in self.records():
|
||||
if record["layer"] in ("platform", "emulator"):
|
||||
self.assertIn("declared", record)
|
||||
own = next(r for r in self.records()
|
||||
if r["layer"] == "platform" and r["value"] == "b" * 32)
|
||||
self.assertEqual(own["declared"], "md5")
|
||||
|
||||
def test_scph5501_names_the_cards_eleven(self):
|
||||
sha1 = "0555c6fae8906f3f09baf5988f00e55f88e9f30b"
|
||||
shard = json.loads(_built(API / "lookup" / f"{sha1[:2]}.json").read_text())
|
||||
owners = {r["owner"] for r in shard["items"]
|
||||
if r["value"] == sha1 and r["layer"] == "emulator"}
|
||||
self.assertGreaterEqual(len(owners), 11)
|
||||
self.assertLessEqual({"pcsx_rearmed", "pcsx1"}, owners)
|
||||
|
||||
|
||||
class TheDataPageListsWhatIsServed(unittest.TestCase):
|
||||
"""8.4 and 8.6."""
|
||||
|
||||
def test_every_schema_is_listed(self):
|
||||
page = generate_site.generate_data_page([])
|
||||
for path in (REPO_ROOT / "schemas").glob("*.schema.json"):
|
||||
self.assertIn(f"- [`{path.name}`](api/v1/schemas/{path.name}): ", page)
|
||||
|
||||
def test_target_lists_are_published_and_catalogued(self):
|
||||
catalog = json.loads(_built(API / "catalog.json").read_text())
|
||||
listed = {item["url"] for item in catalog["items"]}
|
||||
for source in (REPO_ROOT / "install" / "targets").glob("*.json"):
|
||||
self.assertIn(f"targets/{source.name}", listed)
|
||||
self.assertEqual((API / "targets" / source.name).read_bytes(), source.read_bytes())
|
||||
|
||||
def test_declared_only_and_search_words(self):
|
||||
page = generate_site.generate_data_page([])
|
||||
self.assertIn("With `--declared-only`, or from the `declared` event, "
|
||||
"`install.py --check` answers it too.", page)
|
||||
terms = page.split("\n", 2)[1]
|
||||
for word in ("json schema", "license", "check bios folder script"):
|
||||
self.assertIn(word, terms)
|
||||
files_terms = generate_site.generate_files_page().split("---\n", 2)[1]
|
||||
for word in ("is my bios correct", "is my bios right"):
|
||||
self.assertIn(word, files_terms)
|
||||
|
||||
|
||||
def _respond(routes: dict):
|
||||
def urlopen(url: str, *args, **kwargs):
|
||||
for suffix, body in routes.items():
|
||||
if url.endswith(suffix):
|
||||
if body is None:
|
||||
raise urllib.error.HTTPError(url, 404, "Not Found", {}, None)
|
||||
return io.BytesIO(json.dumps(body).encode())
|
||||
raise AssertionError(url)
|
||||
return urlopen
|
||||
|
||||
|
||||
class TheFolderCheckExitCode(unittest.TestCase):
|
||||
"""8.5: exit 1 on a file wrong or missing, a word for an unknown platform."""
|
||||
|
||||
SCRIPT = str(REPO_ROOT / "scripts" / "check_platform_folder.py")
|
||||
CONTRACT = {"items": [{"contract": {"verification_mode": "existence", "systems": {
|
||||
"x": {"files": [{"name": "a.bin", "destination": "a.bin"}]}}}}]}
|
||||
|
||||
def run_script(self, platform: str, folder: Path, routes: dict) -> tuple[object, str]:
|
||||
err = io.StringIO()
|
||||
with mock.patch("urllib.request.urlopen", _respond(routes)), \
|
||||
mock.patch.object(sys, "argv", ["check", platform, str(folder)]), \
|
||||
mock.patch("sys.stdout", io.StringIO()), mock.patch("sys.stderr", err), \
|
||||
self.assertRaises(SystemExit) as raised:
|
||||
runpy.run_path(self.SCRIPT, run_name="__main__")
|
||||
return raised.exception.code, err.getvalue()
|
||||
|
||||
def test_missing_exits_one_and_complete_exits_zero(self):
|
||||
with tempfile.TemporaryDirectory() as folder:
|
||||
code, _err = self.run_script("p", Path(folder), {"/p.json": self.CONTRACT})
|
||||
self.assertEqual(code, 1)
|
||||
(Path(folder) / "a.bin").write_bytes(b"x")
|
||||
code, _err = self.run_script("p", Path(folder), {"/p.json": self.CONTRACT})
|
||||
self.assertEqual(code, 0)
|
||||
|
||||
def test_an_unknown_platform_names_the_known_ones(self):
|
||||
catalog = {"items": [{"url": "manifests/batocera.json"}, {"url": "stats.json"}]}
|
||||
with tempfile.TemporaryDirectory() as folder:
|
||||
code, _err = self.run_script(
|
||||
"nosuch", Path(folder), {"/nosuch.json": None, "/catalog.json": catalog})
|
||||
self.assertEqual(code, "unknown platform: nosuch. Known: batocera")
|
||||
|
||||
def test_the_page_shows_the_script(self):
|
||||
script = Path(self.SCRIPT).read_text(encoding="utf-8")
|
||||
self.assertIn("```python\n" + script.rstrip("\n") + "\n```",
|
||||
generate_site.generate_data_page([]))
|
||||
|
||||
|
||||
class SystemItemsCarryTheirApi(unittest.TestCase):
|
||||
"""8.7."""
|
||||
|
||||
def test_each_api_address_is_served(self):
|
||||
items = json.loads(_built(API / "systems.json").read_text())["items"]
|
||||
playstation = next(i for i in items if i["id"] == "sony-playstation")
|
||||
self.assertEqual(playstation["api"], "/retrobios/api/v1/systems/sony.json")
|
||||
for item in items:
|
||||
if item["url"] is not None:
|
||||
self.assertIsNotNone(item["api"], item["id"])
|
||||
if item["api"]:
|
||||
self.assertTrue((PUBLIC / item["api"].removeprefix("/retrobios/")).is_file(),
|
||||
item["api"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user