mirror of
https://github.com/libretro/virtualjaguar-libretro.git
synced 2026-11-07 04:36:00 -10:00
* docs(cheats): community cheat store design and plan (#1071) Refs #1071 Co-Authored-By: Claude Opus 5.5 <[email protected]> * docs(cheats): map printf_notify to app toasts Refs #1071 Co-Authored-By: Claude Opus 5.5 <[email protected]> * feat(cheat-import): BigPEmu API map; int_full settings omitted; printf_notify ignored Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(cheat-import): refuse side effects in message args; scope Unsupported.call to API gaps Co-Authored-By: Claude Opus 5.5 <[email protected]> * feat(cheat-import): committed cartridge identity table (hashes only) scan_cartridges.py walks a ROM directory (read-only) and writes a sorted FNV1a64 -> CRC32 table with size, title and name; no ROM bytes or paths. load_cartridges rejects duplicate fnv/crc, bad hex and format != 1. The generated cartridges.json (152 rows) lets CI map community script hashes to the CRC32 the app binds packs to without any ROMs. Co-Authored-By: Claude Opus 5.5 <[email protected]> * feat(cheat-import): community cheat pack generator (#1071) build_packs.py converts every pinned community C cheat source with convert.py and writes the site/cheats tree: per-cartridge .cht packs (one per CRC32, sources merged, identical cheats deduped, same-name cheats suffixed), manifest.json, the unsupported.json worklist with per-call API usage, index.html and README. Deterministic, bounded fetch of one codeload tarball per source, --source-dir for local checkouts, --summary and --bump for the bump workflow. sources.json pins jguffer/BigPEmu_Scripts at 2421150. Co-Authored-By: Claude Opus 5.5 <[email protected]> * data(cheats): generate community packs from jguffer/BigPEmu_Scripts@2421150 23 cartridge packs (72 cheats, 9 partial) plus the unsupported.json worklist (55 entries), manifest and index, generated with build_packs.py from the pinned codeload tarball; byte-identical to a local checkout run. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(cheat-import): worklist unmatched literals beside a hit; bound fetch errors and member count - identity row for table-missing literals in a file that also has a hit - http.client.HTTPException during fetch is a FetchError (exit 2) - tar expansion cap counts a header block per member and caps member count - Collector uses the api_map given to build() Co-Authored-By: Claude Opus 5.5 <[email protected]> * data(cheats): regenerate for generator fix (unmatched-literal identity rows) Co-Authored-By: Claude Opus 5.5 <[email protected]> * feat(site): publish community cheat packs under /cheats/ build_site.py copies the committed site/cheats tree verbatim; check_site.py gains check_cheats (manifest format/sha256/bytes/CRC guard, unlisted .cht, unsupported.json, index.html links, 256 KiB manifest and 1 MiB tree limits) with unit tests. pages.yml watches site/cheats and tools/cheat-import. The converter README now documents the author's permission, the credit, the sources -> cartridges -> packs workflow and how to add a source; the community_test harness and its fixtures are removed (the committed packs and the regeneration diff are the conversion contract). Co-Authored-By: Claude Opus 5.5 <[email protected]> * ci(cheats): regen-diff job, concurrency, weekly pin-bump PR cheat-import.yml: add site/cheats/** to the path filters, a per-ref concurrency group, drop the community_test.py fetch step (the files are gone), and add a packs job that regenerates twice, diffs the runs and the committed tree, flags unmapped BigPEmu calls, then runs the site-check tests and build_site + check_site. cheat-packs-bump.yml: weekly (Mon 06:23 UTC) poll of each pinned github source; on movement, bump the pin, regenerate, validate, force-push the fixed branch cheats/pin-bump and open or refresh a develop PR (validation failures head the body instead of being dropped), then dispatch cheat-import.yml on the branch because GITHUB_TOKEN PRs do not fire pull_request workflows. test_committed_sources no longer pins the commit/date value: every bump PR would otherwise fail the unit tests; only the shape is asserted. Refs #1071 Co-Authored-By: Claude Opus 5.5 <[email protected]> * ci(cheats): gate bump workflow to the upstream repo, drop bot trailer Only libretro/virtualjaguar-libretro opens bump PRs (the Provenance-Emu fork runs crons too), with a 30 minute job timeout. The bot-authored bump commit no longer carries a Claude co-author trailer. Refs #1071 Co-Authored-By: Claude Opus 5.5 <[email protected]> * ci(cheats): pin pycparser, force LF, add job timeouts Pin pycparser==3.11 (newest release that installs on the Python 3.10 CI leg; 2.22, 2.23, 3.0, 3.11 and 3.12 all regenerate byte-identical packs). Add a root .gitattributes forcing LF for the generator and generated tree so pack bytes cannot drift by checkout. Give both jobs a 20 minute timeout. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(cheats): dedupe pack header title, refuse control-character paths The pack's first line printed the title twice when the cartridge name began with the title. Print the name alone in that case. Fetch and local-dir reads now fail closed on any path containing a control character, since names can reach ::warning:: annotations and PR bodies. Co-Authored-By: Claude Opus 5.5 <[email protected]> * data(cheats): regenerate packs with deduplicated header title Line 1 of each pack no longer repeats the title when the cartridge name already starts with it. Regenerated from the pinned source commit; two runs and a --source-dir run are byte-identical. Co-Authored-By: Claude Opus 5.5 <[email protected]> * ci(cheats): even indentation on a bump-workflow continuation line editorconfig-checker wants multiples of 2 in workflow files. Refs #1071 Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Claude Opus 5.5 <[email protected]>
122 lines
4.3 KiB
Python
122 lines
4.3 KiB
Python
#!/usr/bin/env python3
|
|
"""Unit tests for check_site.check_cheats (stdlib only)."""
|
|
import hashlib
|
|
import json
|
|
import shutil
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
import check_site # noqa: E402
|
|
|
|
REPO = Path(__file__).resolve().parent.parent
|
|
CHT = ('cheats = 1\n\nvj_rom_crc32 = "0509C85E"\n'
|
|
'cheat0_desc = "x"\ncheat0_enable = false\n').encode()
|
|
|
|
|
|
def build(root):
|
|
"""A minimal valid <root>/cheats tree."""
|
|
c = root / "cheats"
|
|
(c / "packs").mkdir(parents=True)
|
|
(c / "packs" / "0509C85E.cht").write_bytes(CHT)
|
|
manifest = {"format": 1, "packs": [{
|
|
"file": "packs/0509C85E.cht", "crc32": "0509C85E",
|
|
"sha256": hashlib.sha256(CHT).hexdigest(), "bytes": len(CHT),
|
|
"messages": ["hello"]}]}
|
|
(c / "manifest.json").write_text(json.dumps(manifest))
|
|
(c / "unsupported.json").write_text("{}")
|
|
(c / "index.html").write_text(
|
|
'<a href="packs/0509C85E.cht">p</a><a href="https://x.test/">x</a>')
|
|
return c
|
|
|
|
|
|
class CheckCheats(unittest.TestCase):
|
|
def setUp(self):
|
|
self.tmp = Path(tempfile.mkdtemp())
|
|
self.addCleanup(shutil.rmtree, self.tmp, True)
|
|
self.c = build(self.tmp)
|
|
|
|
def problems(self):
|
|
return check_site.check_cheats(self.tmp)
|
|
|
|
def rewrite_manifest(self, fn):
|
|
p = self.c / "manifest.json"
|
|
m = json.loads(p.read_text())
|
|
fn(m)
|
|
p.write_text(json.dumps(m))
|
|
|
|
def test_absent_is_skipped(self):
|
|
self.assertEqual(check_site.check_cheats(self.tmp / "nope"), [])
|
|
|
|
def test_valid(self):
|
|
self.assertEqual(self.problems(), [])
|
|
|
|
def test_corrupt_pack_names_file(self):
|
|
f = self.c / "packs" / "0509C85E.cht"
|
|
b = bytearray(f.read_bytes())
|
|
b[-3] ^= 1
|
|
f.write_bytes(bytes(b))
|
|
out = self.problems()
|
|
self.assertTrue(any("0509C85E.cht" in p and "sha256" in p for p in out), out)
|
|
|
|
def test_wrong_size(self):
|
|
self.rewrite_manifest(lambda m: m["packs"][0].update(bytes=1))
|
|
self.assertTrue(any("bytes" in p for p in self.problems()))
|
|
|
|
def test_crc_mismatch(self):
|
|
self.rewrite_manifest(lambda m: m["packs"][0].update(crc32="DEADBEEF"))
|
|
self.assertTrue(any("vj_rom_crc32" in p for p in self.problems()))
|
|
|
|
def test_missing_pack(self):
|
|
(self.c / "packs" / "0509C85E.cht").unlink()
|
|
self.assertTrue(any("missing" in p for p in self.problems()))
|
|
|
|
def test_unlisted_cht(self):
|
|
(self.c / "packs" / "AAAAAAAA.cht").write_bytes(CHT)
|
|
self.assertTrue(any("AAAAAAAA.cht" in p for p in self.problems()))
|
|
|
|
def test_bad_format(self):
|
|
self.rewrite_manifest(lambda m: m.update(format=2))
|
|
self.assertTrue(any("format" in p for p in self.problems()))
|
|
|
|
def test_manifest_not_json(self):
|
|
(self.c / "manifest.json").write_text("{")
|
|
self.assertTrue(any("does not parse" in p for p in self.problems()))
|
|
|
|
def test_unsupported_not_json(self):
|
|
(self.c / "unsupported.json").write_text("{")
|
|
self.assertTrue(any("unsupported.json" in p for p in self.problems()))
|
|
|
|
def test_broken_index_link(self):
|
|
(self.c / "index.html").write_text('<a href="packs/NOPE.cht">p</a>')
|
|
self.assertTrue(any("NOPE.cht" in p for p in self.problems()))
|
|
|
|
def test_messages_must_be_strings(self):
|
|
self.rewrite_manifest(lambda m: m["packs"][0].update(messages=[1]))
|
|
self.assertTrue(any("messages" in p for p in self.problems()))
|
|
|
|
def test_messages_optional(self):
|
|
self.rewrite_manifest(lambda m: m["packs"][0].pop("messages"))
|
|
self.assertEqual(self.problems(), [])
|
|
|
|
def test_manifest_too_big(self):
|
|
self.rewrite_manifest(lambda m: m.update(pad="x" * (256 * 1024)))
|
|
self.assertTrue(any("manifest.json is" in p for p in self.problems()))
|
|
|
|
def test_tree_too_big(self):
|
|
(self.c / "packs" / "pad.bin").write_bytes(b"\0" * (1024 * 1024))
|
|
self.assertTrue(any("tree is" in p for p in self.problems()))
|
|
|
|
def test_committed_tree_is_valid(self):
|
|
stage = self.tmp / "real"
|
|
stage.mkdir()
|
|
shutil.copytree(REPO / "site" / "cheats", stage / "cheats")
|
|
(stage / "app.html").write_text("") # index.html links ../app.html
|
|
self.assertEqual(check_site.check_cheats(stage), [])
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|