diff --git a/scripts/siterender.py b/scripts/siterender.py index ebc47e8ab..9f19a9d02 100644 --- a/scripts/siterender.py +++ b/scripts/siterender.py @@ -372,19 +372,21 @@ def _in_arcade(sha1: str, db: dict) -> bool: return sha1 in _arcade_suffix_sha1s(db) -_suffix_cache: list[tuple[int, frozenset[str]] | None] = [None] +_suffix_cache: list[tuple[dict, frozenset[str]] | None] = [None] def _arcade_suffix_sha1s(db: dict) -> frozenset[str]: + # Holding the database itself, never id(db): a database freed and + # replaced at the same address answered with the first one's set. cached = _suffix_cache[0] - if cached is not None and cached[0] == id(db): + if cached is not None and cached[0] is db: return cached[1] found = frozenset( sha1s[0] for tail, sha1s in db.get("indexes", {}).get("by_path_suffix", {}).items() if tail.startswith("Arcade/") and len(sha1s) == 1 ) - _suffix_cache[0] = (id(db), found) + _suffix_cache[0] = (db, found) return found diff --git a/tests/test_site_render.py b/tests/test_site_render.py index 35b27dbc8..f4b11a4ac 100644 --- a/tests/test_site_render.py +++ b/tests/test_site_render.py @@ -503,5 +503,34 @@ class NoTooltipInsideAName(unittest.TestCase): self.assertIn('>BIOS', marked) +class TheArcadeCacheHoldsItsDatabase(unittest.TestCase): + """The set of sha1s filed under Arcade/ was cached by id(db): a database + freed and replaced at the same address answered with the first one's set, + and an arcade card of the second landed in another section.""" + + @staticmethod + def _db(sha1: str) -> dict: + return {"files": {}, "indexes": {"by_path_suffix": {"Arcade/x.zip": [sha1]}}} + + def test_a_new_database_at_a_reused_address_is_read_again(self): + saved = siterender._suffix_cache[0] + self.addCleanup(siterender._suffix_cache.__setitem__, 0, saved) + first = self._db("a" * 40) + self.assertTrue(siterender._in_arcade("a" * 40, first)) + address = id(first) + del first + # CPython hands a freed dict's address to a later one of its size, + # unless the cache still holds the first database. + for _ in range(1000): + second = self._db("b" * 40) + if id(second) == address: + break + del second + else: + second = self._db("b" * 40) + self.assertTrue(siterender._in_arcade("b" * 40, second)) + self.assertFalse(siterender._in_arcade("a" * 40, second)) + + if __name__ == "__main__": unittest.main()