chore: merge round 2 fixes (ops)

This commit is contained in:
Abdessamad Derraz committed 2026-10-21 10:08:27 +02:00
commit d890ad40e6
26 files changed
+1131 -110

No files matched your search

+29 -1
View File
@@ -18,7 +18,12 @@ from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO_ROOT / "scripts"))
from common import ArtifactLockBusy, artifact_lock, hold_artifact_lock # noqa: E402
from common import ( # noqa: E402
ArtifactLockBusy,
artifact_lock,
hold_artifact_lock,
hold_repository_outputs,
)
def _tempdir(test: unittest.TestCase) -> str:
@@ -200,6 +205,29 @@ class PackLockCliTest(unittest.TestCase):
self.assertIn("is in use by another run", proc.stderr)
class PipelineHoldsItsFixedOutputs(unittest.TestCase):
"""A pipeline on a scratch --output-dir still rewrites database.json,
install/, README.md and docs/: only the output directory was locked."""
def test_a_second_pipeline_is_refused_whatever_its_output_dir(self):
scratch = _tempdir(self)
with hold_repository_outputs(str(REPO_ROOT)):
proc = subprocess.run(
[sys.executable, "scripts/pipeline.py", "--offline", "--skip-docs",
"--output-dir", scratch],
capture_output=True, text=True, cwd=str(REPO_ROOT), timeout=120,
)
self.assertEqual(proc.returncode, 1, proc.stdout + proc.stderr)
self.assertIn("another pipeline run is writing database.json", proc.stderr)
self.assertNotIn("generate database", proc.stdout.lower())
def test_two_holds_of_the_repository_exclude_each_other(self):
with hold_repository_outputs(str(REPO_ROOT)), \
self.assertRaises(ArtifactLockBusy):
with hold_repository_outputs(str(REPO_ROOT)):
pass
class HeldLockTest(unittest.TestCase):
"""pipeline.py took the lock step by step: between the pack build and the
integrity check dist/ was free, another run's purge emptied it, and the
+94 -1
View File
@@ -3761,6 +3761,21 @@ class ADataDirectoryIsHeldWhileRead(unittest.TestCase):
self.assertFalse(exclusive_free())
self.assertTrue(exclusive_free())
def test_a_cache_not_yet_on_disk_is_held_too(self):
"""Skipped while absent, a tree a refresh installed mid-run was
missing for verify and present for the packs of the same run."""
import fcntl
from scripts import refresh_data_dirs
cache = self.tmp / "absent"
lock = cache.with_name(f".{cache.name}.lock")
with contextlib.ExitStack() as held:
refresh_data_dirs.hold_data_dirs(held, {"absent": {"local_cache": str(cache)}})
self.assertFalse(cache.exists())
with open(lock, "a") as handle, self.assertRaises(BlockingIOError):
fcntl.flock(handle, fcntl.LOCK_EX | fcntl.LOCK_NB)
def test_both_entry_points_hold_the_trees(self):
pack = (ROOT / "scripts" / "generate_pack.py").read_text()
verify_src = (ROOT / "scripts" / "verify.py").read_text()
@@ -5446,7 +5461,8 @@ class ReportsHoldTheDataTrees(unittest.TestCase):
def test_each_entry_point_holds_them(self):
import ast
for script in ("generate_readme.py", "generate_site.py", "slots.py"):
for script in ("generate_readme.py", "generate_site.py", "slots.py",
"export_native.py", "cross_reference.py"):
tree = ast.parse((ROOT / "scripts" / script).read_text(encoding="utf-8"))
main = next(
node for node in tree.body
@@ -5582,6 +5598,83 @@ class AnUnansweredSearchIsNotANotFound(unittest.TestCase):
self.assertEqual(result["stats"]["errors"], 0)
class AutoFetchProvesEveryHashItIsGiven(unittest.TestCase):
"""auto_fetch checked a download by sha1 or md5 only: RetroDECK's
dsifirmware.bin, pinned by sha256 alone, was announced as searched and
could never be accepted."""
DATA = b"firmware bytes"
def test_a_sha256_proves_a_download(self):
import hashlib
from scripts import auto_fetch
sha256 = {"sha256": hashlib.sha256(self.DATA).hexdigest()}
self.assertTrue(auto_fetch.verify_content(self.DATA, sha256))
self.assertFalse(auto_fetch.verify_content(b"other", sha256))
def test_every_declared_cryptographic_hash_must_match(self):
import hashlib
from scripts import auto_fetch
both = {"sha256": hashlib.sha256(self.DATA).hexdigest(), "md5": "0" * 32}
self.assertFalse(auto_fetch.verify_content(self.DATA, both))
both["md5"] = hashlib.md5(self.DATA).hexdigest()
self.assertTrue(auto_fetch.verify_content(self.DATA, both))
def test_crc32_and_size_never_accept_a_download(self):
"""A crc32 can be forged: bytes a source crafts to it would land in
bios/."""
import urllib.request
import zlib
from unittest import mock
from scripts import auto_fetch
crc = {"name": "forged.bin", "system": "test-system",
"crc32": f"{zlib.crc32(self.DATA):08X}", "size": len(self.DATA)}
self.assertFalse(auto_fetch.verify_content(self.DATA, crc))
self.assertFalse(auto_fetch.verifiable(crc))
served = io.BytesIO(self.DATA)
with tempfile.TemporaryDirectory() as bios, \
mock.patch.object(urllib.request, "urlopen", lambda *a, **k: served), \
mock.patch.object(auto_fetch, "step2_scan_branches",
lambda e: self.DATA), \
contextlib.redirect_stdout(io.StringIO()) as out:
result = auto_fetch.fetch_missing([crc], {"files": {}}, bios)
self.assertEqual(list(Path(bios).rglob("*")), [])
self.assertEqual(result["stats"]["found"], 0)
self.assertEqual(result["stats"]["unverifiable"], 1)
self.assertIn("declares no cryptographic hash", out.getvalue())
def test_a_held_sha256_is_found_in_the_database(self):
from scripts import auto_fetch
db = {"files": {"s": {"path": "bios/x/dsi.bin", "size": 3}},
"indexes": {"by_sha256": {"f" * 64: "s"}}}
self.assertEqual(
auto_fetch.step1_crossref_db({"sha256": "F" * 64}, db), "bios/x/dsi.bin"
)
def test_an_entry_without_a_hash_is_not_searched(self):
from unittest import mock
from scripts import auto_fetch
entry = {"name": "DICROM.ROM", "system": "fujitsu-fm7"}
out = io.StringIO()
with mock.patch.object(auto_fetch, "step2_scan_branches") as branches, \
contextlib.redirect_stdout(out):
result = auto_fetch.fetch_missing([entry], {"files": {}}, "bios", dry_run=True)
branches.assert_not_called()
self.assertEqual(result["stats"]["unverifiable"], 1)
self.assertNotIn("Would search", out.getvalue())
self.assertIn("Not searched", out.getvalue())
class AForcedRehashIsCheckedAgainstTheLastOne(unittest.TestCase):
"""This host has computed a wrong SHA-1 under load. The pipeline's forced
rebuild threw the previous digests away and wrote whatever it read."""
+29 -1
View File
@@ -377,7 +377,8 @@ class ProfileSummaryTests(unittest.TestCase):
{"name": "a", "pin": "1" * 40, "head": "1" * 40, "needs_review": 0},
{"name": "b", "pin": "1" * 40, "head": "2" * 40, "needs_review": 0},
{"name": "c", "pin": "1" * 40, "head": "2" * 40, "needs_review": 3},
{"name": "d", "skipped": "host does not resolve"},
{"name": "d", "skipped": "no declared repository answers"},
{"name": "e", "skipped": "upstream gone (declared): archived 2019"},
]
findings = cf.summarize_profile_sync(report)
by_subject = {f.subject: f for f in findings}
@@ -385,6 +386,8 @@ class ProfileSummaryTests(unittest.TestCase):
self.assertEqual(by_subject["profile_sync"].local, "1 at tip, 1 anchored past the tip")
self.assertEqual(by_subject["c"].status, cf.STALE)
self.assertEqual(by_subject["d"].status, cf.UNKNOWN)
# Never checkable by nature: it does not hold the exit code.
self.assertEqual(by_subject["e"].status, cf.SKIPPED)
self.assertNotIn("a", by_subject)
self.assertNotIn("b", by_subject)
@@ -442,6 +445,31 @@ class RepositoryWiringTests(unittest.TestCase):
self.assertIn("[ci]", text)
self.assertIn("[data]", text)
def test_an_unanswered_upstream_is_not_fresh(self):
"""With no network every data row was unknown and the run exited 0,
the answer of a collection with nothing to refresh."""
self.assertEqual(cf.exit_code([cf.Finding("data", "a", cf.UNKNOWN)]), 1)
self.assertEqual(cf.exit_code([cf.Finding("data", "a", cf.SKIPPED),
cf.Finding("data", "b", cf.OK)]), 0)
def test_stale_rows_name_the_command_that_refreshes_them(self):
text = cf.render([cf.Finding("native", "batocera", cf.STALE)])
self.assertIn("refresh_stale.py", text)
self.assertNotIn("refresh_stale.py", cf.render([cf.Finding("ci", "a", cf.OK)]))
def test_a_stale_original_names_its_platform(self):
"""The hint refetched the originals of all twelve platforms to fix
one, and wrote no export."""
source = Path(cf.__file__).read_text(encoding="utf-8")
hint = source.index('" fetches it, then the export is written again"')
self.assertIn("--platform {name} --refresh-cache", source[hint - 120:hint])
def test_an_unknown_area_names_the_valid_ones(self):
message = cf.unknown_areas_message(["platform"])
self.assertIn("did you mean platforms?", message)
for area in cf.AREAS:
self.assertIn(area, message)
def test_a_target_of_every_declared_core_does_not_age(self):
"""RetroBat, RetroDECK and RomM name one target that runs every core
they declare. With nothing hand-kept, there is no date to watch."""
+3 -1
View File
@@ -40,12 +40,14 @@ SPLIT_RECORD = {
"Batocera_1_BIOS_Pack.part1of2.zip",
"Batocera_1_BIOS_Pack.part2of2.zip",
],
"platforms": ["batocera"],
},
"RomM_1_BIOS_Pack.zip": {
"files": 1,
"extracted_size": 1024**2,
"download_size": 1024**2,
"assets": ["RomM_1_BIOS_Pack.zip"],
"platforms": ["romm"],
},
},
}
@@ -244,7 +246,7 @@ class SizesComeFromTheRecord(unittest.TestCase):
tag, platform, download, extracted = quoted.groups()
if tag != RECORD.get("tag"):
self.skipTest(f"release.json records {RECORD.get('tag')}, not {tag}")
pack = release_record.pack_for(platform, RECORD)
pack = release_record.pack_for(platform.lower(), RECORD)
self.assertEqual(download, format_size(pack["download_size"]))
self.assertEqual(extracted, format_size(pack["extracted_size"]))
+43
View File
@@ -373,5 +373,48 @@ class Cli(unittest.TestCase):
self.assertEqual(raised.exception.code, 2)
class TheDevicesFollowTheRelease(unittest.TestCase):
"""Pipeline step 2a moved mame.yml to a new release and left the device
sets on the lines of the previous one: the profile named two releases."""
def _run(self, moves: bool, dry_run: bool = False) -> list:
import argparse
from unittest import mock
from scripts.scraper import mame_hash_scraper as scraper
with tempfile.TemporaryDirectory() as directory:
profile = Path(directory) / "mame.yml"
profile.write_text('core_version: "0.289"\nfiles: []\n')
def merge(path, *_args, **_kwargs):
if moves:
profile.write_text('core_version: "0.290"\nfiles: []\n')
merged = []
with mock.patch.multiple(
scraper,
_EMULATORS_DIR=Path(directory),
_fetch_hashes=lambda force: {"bios_sets": {}, "version": "0.290"},
_find_mame_profiles=lambda: [profile],
compute_diff=lambda *a, **k: {"updated": ["x"], "added": []},
_format_diff=lambda *a, **k: [],
merge_mame_profile=merge,
fetch_release_listxml=lambda version: Path(f"mame{version}lx.zip"),
_merge_devices=merged.append,
), mock.patch("sys.stdout", new=io.StringIO()):
scraper._run(argparse.Namespace(force=False, json=False, dry_run=dry_run))
return merged
def test_a_moved_pin_merges_that_release_devices(self):
(args,) = self._run(moves=True)
self.assertEqual(args.listxml, Path("mame0.290lx.zip"))
self.assertFalse(args.dry_run)
def test_an_unmoved_pin_leaves_the_devices(self):
self.assertEqual(self._run(moves=False), [])
self.assertEqual(self._run(moves=True, dry_run=True), [])
if __name__ == "__main__":
unittest.main()
+40 -6
View File
@@ -283,12 +283,14 @@ RECORD = {
"RetroArch_Lakka_v1.22.2_BIOS_Pack.zip.001",
"RetroArch_Lakka_v1.22.2_BIOS_Pack.zip.002",
],
"platforms": ["lakka", "retroarch"],
},
"MiSTer_FPGA_2026-08-29_BIOS_Pack.zip": {
"files": 74,
"extracted_size": 17000000,
"download_size": 16769418,
"assets": ["MiSTer_FPGA_2026-08-29_BIOS_Pack.zip"],
"platforms": ["misterfpga"],
},
},
}
@@ -340,6 +342,11 @@ class TheTableDescribesTheRelease(unittest.TestCase):
class ReleaseRecord(unittest.TestCase):
OWNERS = {
"Whole_1.0_BIOS_Pack.zip": ["whole"],
"Demo_2.0_BIOS_Pack.zip": ["demo"],
}
def setUp(self):
self._tmp = tempfile.TemporaryDirectory()
self.dist = Path(self._tmp.name) / "dist"
@@ -364,7 +371,7 @@ class ReleaseRecord(unittest.TestCase):
return path
def test_a_pack_in_parts_is_counted_once_across_them(self):
record = release_record.build_record(self.dist, "v1")
record = release_record.build_record(self.dist, "v1", owners=self.OWNERS)
self.assertEqual(record["tag"], "v1")
demo = record["packs"]["Demo_2.0_BIOS_Pack.zip"]
self.assertEqual(demo["files"], 6)
@@ -378,7 +385,7 @@ class ReleaseRecord(unittest.TestCase):
def test_a_pack_that_disagrees_with_its_manifest_is_named(self):
"""The count main predicts and the count the archive holds are two
computations of one thing. Apart, one of them is wrong."""
record = release_record.build_record(self.dist, "v1")
record = release_record.build_record(self.dist, "v1", owners=self.OWNERS)
(self.install / "demo.json").write_text(json.dumps({"pack_files": 6}))
(self.install / "whole.json").write_text(json.dumps({"pack_files": 5}))
self.assertEqual(
@@ -395,12 +402,38 @@ class ReleaseRecord(unittest.TestCase):
{f"dir/file{n}.bin": bytes([n]) * 700 + bytes(range(256)) for n in range(6)},
)
with self.assertRaises(release_record.UnfinishedSplitError):
release_record.build_record(self.dist, "v1")
release_record.build_record(self.dist, "v1", owners=self.OWNERS)
def test_a_narrowed_pack_is_refused_not_recorded(self):
"""A pack that sorts before the full one (Android targets, Asia) was
taken for the platform's pack by a substring match."""
self._pack("Whole_1.0_Asia_BIOS_Pack.zip", {"a.bin": b"a"})
with self.assertRaises(release_record.NotAReleasePackError) as raised:
release_record.build_record(self.dist, "v1", owners=self.OWNERS)
self.assertIn("Whole_1.0_Asia_BIOS_Pack.zip", str(raised.exception))
def test_a_platform_finds_the_pack_that_serves_it_by_name(self):
record = release_record.build_record(self.dist, "v1", owners=self.OWNERS)
record["packs"]["Whole_1.0_AAA_BIOS_Pack.zip"] = dict(
record["packs"]["Whole_1.0_BIOS_Pack.zip"], files=99, platforms=["other"]
)
self.assertEqual(
release_record.pack_name_for("whole", record), "Whole_1.0_BIOS_Pack.zip"
)
self.assertEqual(release_record.pack_for("whole", record)["files"], 2)
def test_the_owners_are_the_full_pack_names_the_builder_writes(self):
owners = release_record.release_owners(str(REPO_ROOT / "platforms"))
retroarch = [name for name, serves in owners.items() if "retroarch" in serves]
self.assertEqual(len(retroarch), 1, retroarch)
for tags in ("_AndroidArm64V8A", "_Asia", "_Required"):
narrowed = retroarch[0].replace("_BIOS_Pack.zip", f"{tags}_BIOS_Pack.zip")
self.assertNotIn(narrowed, owners)
def test_a_directory_being_written_is_not_read(self):
with common.artifact_lock(str(self.dist)), \
self.assertRaises(common.ArtifactLockBusy):
release_record.build_record(self.dist, "v1")
release_record.build_record(self.dist, "v1", owners=self.OWNERS)
class TheCollectionTotalNamesTheCollection(unittest.TestCase):
@@ -483,10 +516,11 @@ class TheReleaseNotesReadTheRecord(unittest.TestCase):
"RetroArch_Lakka_v1.22.2_BIOS_Pack.zip": {
"files": 8504, "extracted_size": 6_400_000_000, "download_size": 4_000_000_000,
"assets": ["RetroArch_Lakka_v1.22.2_BIOS_Pack.part1of2.zip",
"RetroArch_Lakka_v1.22.2_BIOS_Pack.part2of2.zip"]},
"RetroArch_Lakka_v1.22.2_BIOS_Pack.part2of2.zip"],
"platforms": ["lakka", "retroarch"]},
"EmuDeck_2.7EA_BIOS_Pack.zip": {
"files": 620, "extracted_size": 3_500_000_000, "download_size": 3_000_000_000,
"assets": ["EmuDeck_2.7EA_BIOS_Pack.zip"]},
"assets": ["EmuDeck_2.7EA_BIOS_Pack.zip"], "platforms": ["emudeck"]},
}}
def render(self, record=None):
+49
View File
@@ -554,6 +554,55 @@ class OneDumpIsCountedOnce(unittest.TestCase):
self.assertEqual(provenance_report.distinct_missing(twice), 1)
class AMissingDumpNamesTheProfileItFills(unittest.TestCase):
"""XM7 requires DICROM.ROM and the collection lacks it, yet the report
listed No-Intro's dump at the rank of a Kickstart beta no core reads."""
PROFILES = {
"xm7": {"files": [
{"name": "DICROM.ROM", "required": True, "min_size": 262144},
{"name": "held.rom", "required": True, "size": 16},
]},
"hashed": {"files": [{"name": "x.bin", "sha1": "c" * 40}]},
}
def _report(self, db):
from scripts import provenance_report # noqa: PLC0415
snapshots = {"no-intro": {"entries": [
{"dat": "FM-7", "name": "DICROM.rom", "sha1": "a" * 40, "size": 262144},
{"dat": "FM-7", "name": "held.rom", "sha1": "b" * 40, "size": 16},
{"dat": "FM-7", "name": "Other Name.rom", "sha1": "c" * 40, "size": 8},
{"dat": "FM-7", "name": "unread.rom", "sha1": "d" * 40, "size": 8},
]}}
needs = provenance_report.profile_needs(self.PROFILES, db)
return provenance_report.build_report(db, snapshots, {}, {}, needs)
def test_a_dump_a_profile_reads_names_it(self):
db = {"files": {"b" * 40: {"name": "held.rom", "md5": "", "size": 16}}}
missing = {e["name"]: e.get("needed_by") for e in self._report(db)["no-intro"]["missing"]}
self.assertEqual(missing["DICROM.rom"], [
{"emulator": "xm7", "name": "DICROM.ROM", "required": True,
"by": "name and size"},
])
self.assertEqual(missing["Other Name.rom"][0]["by"], "sha1")
self.assertIsNone(missing["unread.rom"])
def test_a_size_outside_the_profile_range_fills_nothing(self):
from scripts import provenance_report # noqa: PLC0415
needs = provenance_report.profile_needs(self.PROFILES, {"files": {}})
small = {"name": "DICROM.rom", "size": 1024}
self.assertEqual(provenance_report.needs_filled(small, needs), [])
def test_a_need_the_collection_answers_is_not_one(self):
from scripts import provenance_report # noqa: PLC0415
db = {"files": {"e" * 40: {"name": "dicrom.rom", "size": 262144}}}
needs = provenance_report.profile_needs(self.PROFILES, db)
self.assertNotIn("dicrom.rom", needs["by_name"])
class TheWantedPageAsksOnlyForWhatCannotBeMade(unittest.TestCase):
"""The site's Wanted page asked the public for 79 dumps the collection
already produces (a Kickstart's odd bytes, a character ROM's halves)."""
+49 -3
View File
@@ -30,8 +30,8 @@ def finding(area: str, subject: str, status: str = "STALE") -> dict:
class Planning(unittest.TestCase):
def _plan(self, *findings: dict):
return rs.plan_jobs(list(findings), REGISTRY)
def _plan(self, *findings: dict, parents: dict | None = None):
return rs.plan_jobs(list(findings), REGISTRY, parents or {})
def test_a_stale_platform_is_rescraped_then_its_original_is_refreshed(self):
jobs, surfaced, _ = self._plan(finding("platforms", "retrobat"))
@@ -47,9 +47,28 @@ class Planning(unittest.TestCase):
[list(c[1:]) for c in jobs[0].then],
[["scripts/generate_truth.py", "--platform", "retrobat"],
["scripts/export_native.py", "--platform", "retrobat", "--refresh-cache"],
["scripts/export_native.py", "--platform", "retrobat"]],
["scripts/export_native.py", "--platform", "retrobat"],
["scripts/verify.py", "--platform", "retrobat"]],
)
def test_a_child_waits_for_its_rescraped_parent(self):
"""retropie inherits retroarch: run beside the parent's rescrape, its
truth read the old parent and its export was refused as stale."""
registry = dict(REGISTRY, retroarch={"scraper": "libretro"},
retropie={"scraper": "retropie"})
parents = {"retropie": "retroarch", "lakka": "retroarch"}
jobs, _, _ = rs.plan_jobs(
[finding("platforms", "retroarch"), finding("platforms", "retropie"),
finding("platforms", "retrobat")],
registry, parents,
)
after = {(j.area, j.subject): j.after for j in jobs}
self.assertEqual(after[("platforms", "retroarch")], frozenset())
self.assertEqual(after[("platforms", "retrobat")], frozenset())
self.assertEqual(after[("platforms", "retropie")], frozenset({"retroarch"}))
# lakka has no row and no scraper: its export follows the parent's.
self.assertEqual(after[("native", "lakka")], frozenset({"retroarch"}))
def test_a_native_row_of_a_rescraped_platform_is_not_a_second_job(self):
"""Two jobs would race: the refresh must follow the scrape."""
jobs, _, ignored = self._plan(
@@ -132,6 +151,33 @@ class Planning(unittest.TestCase):
class Running(unittest.TestCase):
def test_a_child_starts_after_its_parent_ends(self):
with tempfile.TemporaryDirectory() as directory:
order = Path(directory) / "order"
def job(area, subject, after=()):
code = f"open({str(order)!r}, 'a').write({subject!r} + chr(10))"
if subject == "parent":
code = "import time; time.sleep(0.5); " + code
return rs.Job(area, subject, [sys.executable, "-c", code],
Path(directory) / f"{subject}.log", (), frozenset(after))
rs.run_jobs([job("native", "child", ["parent"]),
job("platforms", "parent")], {}, 4)
self.assertEqual(order.read_text().split(), ["parent", "child"])
def test_the_report_prints_verify_after_a_rescrape(self):
(REPO_ROOT / "tmp").mkdir(exist_ok=True)
with tempfile.TemporaryDirectory(dir=REPO_ROOT / "tmp") as directory:
log = Path(directory) / "x.log"
log.write_text(
"$ python -m scripts.scraper.x_scraper\nscraped\n"
"$ python scripts/verify.py --platform x\n"
"X: 10/12 OK, 2 missing [md5]\n"
)
job = rs.Job("platforms", "x", ["true"], log)
text = rs.render([rs.JobResult(job, 0, 1.0, "")], [], [])
self.assertIn("X: 10/12 OK, 2 missing [md5]", text)
self.assertIn("python scripts/verify.py --platform x", text)
def _job(self, directory: str, command: list[str], then=()) -> rs.Job:
return rs.Job("platforms", "x", command, Path(directory) / "x.log", then)
+82
View File
@@ -694,6 +694,88 @@ class AnOversizedDatIsNamed(unittest.TestCase):
FBNEO_DAT = (
b'<?xml version="1.0"?>\n<datafile><header><name>FinalBurn Neo - Demo</name>'
b"<version>1.0.0.03</version></header>"
b'<game name="demobios"><description>Demo BIOS</description>'
b'<rom name="a.bin" size="17" crc="' + _crc(ROM_A).encode() + b'"/></game>'
b"</datafile>\n"
)
class FbneoFetchReadsOneRevision(unittest.TestCase):
"""The FBNeo import listed blobs at the tip but downloaded branch URLs a
CDN caches, and --fetch <revision> read the tip whatever was named."""
def setUp(self):
from scripts.scraper import romset_dat_importer as importer
self.importer = importer
self._tmp = tempfile.TemporaryDirectory(dir=TMP_ROOT)
self.dir = Path(self._tmp.name)
self.calls: list[str] = []
self.served = FBNEO_DAT
def tearDown(self):
self._tmp.cleanup()
def _api(self, url: str) -> object:
self.calls.append(url)
if "/commits/" in url:
return {"sha": "c0ffee"}
return [{
"type": "file", "name": "Demo.dat",
"sha": self.importer.git_blob_sha(FBNEO_DAT),
"download_url": "https://raw.githubusercontent.com/libretro/FBNeo/c0ffee/dats/Demo.dat",
}]
def _download(self, url: str, destination: Path) -> Path:
destination.write_bytes(self.served)
return destination
def _patched(self):
from unittest import mock
return mock.patch.multiple(
self.importer, _api_json=self._api, _download=self._download
)
def test_bytes_that_are_not_the_listed_blob_are_refused(self):
self.served = FBNEO_DAT.replace(b"1.0.0.03", b"1.0.0.02")
with self._patched(), self.assertRaises(self.importer.BlobMismatchError):
self.importer.fetch_pack("fbneo", "latest", self.dir)
self.assertFalse((self.dir / "fbneo-dats" / "Demo.dat").exists())
def test_the_named_revision_is_the_one_listed(self):
with self._patched():
target, blobs = self.importer.fetch_pack("fbneo", "v1.0.0.02", self.dir)
self.assertEqual(
self.calls,
[f"{self.importer.FBNEO_API}/commits/v1.0.0.02",
f"{self.importer.FBNEO_API}/contents/dats?ref=c0ffee"],
)
self.assertEqual(list(blobs), ["Demo.dat"])
def test_a_revision_import_is_labelled_and_leaves_the_tip_blobs(self):
import contextlib
import io
from unittest import mock
output = self.dir / "fbneo.json"
output.write_text(json.dumps({"upstream": {"blobs": {"Demo.dat": "tip"}}}))
argv = ["romset_dat_importer", "--source", "fbneo", "--fetch", "v1.0.0.02",
"--all", "--cache-dir", str(self.dir), "--output", str(output)]
with self._patched(), mock.patch.object(sys, "argv", argv), \
contextlib.redirect_stdout(io.StringIO()):
self.assertEqual(self.importer.main(), 0)
snapshot = json.loads(output.read_text())
self.assertEqual(snapshot["upstream"]["blobs"], {"Demo.dat": "tip"})
self.assertEqual(
[entry["dat"] for entry in snapshot["entries"]],
["FinalBurn Neo - Demo @ v1.0.0.02"],
)
class LooseRomsAreAtoms(unittest.TestCase):
"""AtomPool read only ZIP members: nmk004.zip stayed unidentified while
its one ROM sat loose in bios/Arcade/FBNeo/nmk004.bin."""