mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-11-01 00:27:20 -05:00
fix: replace the system icon cache whole
The cache was truncated and dumped in place. A concurrent build read
half a file, took it for empty, and an offline run then wrote {} over
every recorded icon. The file is now replaced through a scratch file,
merged with what another run recorded meanwhile, and left alone when
nothing new was learned.
This commit is contained in:
1 parent
348c56e94c
commit
c87c73d0a8
3 files changed
+129
-17
No files matched your search
@@ -0,0 +1,84 @@
|
||||
"""Renderings shared by the site pages, and the icon cache they read.
|
||||
|
||||
No network: icon checks are answered by a stub.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
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 siterender # noqa: E402
|
||||
|
||||
|
||||
class _Served:
|
||||
status = 200
|
||||
|
||||
def __enter__(self) -> "_Served":
|
||||
return self
|
||||
|
||||
def __exit__(self, *exc: object) -> None:
|
||||
return None
|
||||
|
||||
|
||||
class IconCacheIsReplacedWhole(unittest.TestCase):
|
||||
"""The cache was truncated then dumped in place: a concurrent reader saw
|
||||
half a file, took it for empty, and an offline run then wrote ``{}``
|
||||
over every recorded icon."""
|
||||
|
||||
def setUp(self) -> None:
|
||||
tmp = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(tmp.cleanup)
|
||||
self.path = Path(tmp.name) / "system_icons.json"
|
||||
patcher = mock.patch.object(siterender, "ICON_CACHE_PATH", self.path)
|
||||
patcher.start()
|
||||
self.addCleanup(patcher.stop)
|
||||
saved = dict(siterender._icon_available)
|
||||
self.addCleanup(lambda: (siterender._icon_available.clear(),
|
||||
siterender._icon_available.update(saved)))
|
||||
|
||||
def test_an_offline_run_leaves_an_unreadable_cache_untouched(self):
|
||||
self.path.write_text('{"Sega - Mega Drive": tr')
|
||||
offline = mock.patch.object(
|
||||
siterender.urllib.request, "urlopen",
|
||||
side_effect=urllib.error.URLError("offline"))
|
||||
with offline, mock.patch("sys.stderr"), mock.patch("builtins.print"):
|
||||
siterender.prime_system_icons({"Sega - Mega Drive"})
|
||||
self.assertEqual(self.path.read_text(), '{"Sega - Mega Drive": tr')
|
||||
|
||||
def test_names_another_run_recorded_meanwhile_are_kept(self):
|
||||
self.path.write_text(json.dumps({"A - One": True}))
|
||||
|
||||
def answer(*_args: object, **_kwargs: object) -> _Served:
|
||||
# A second build records its own check while this one runs.
|
||||
self.path.write_text(json.dumps({"A - One": True, "C - Three": False}))
|
||||
return _Served()
|
||||
|
||||
with mock.patch.object(siterender.urllib.request, "urlopen", side_effect=answer), \
|
||||
mock.patch("builtins.print"):
|
||||
siterender.prime_system_icons({"A - One", "B - Two"})
|
||||
self.assertEqual(json.loads(self.path.read_text()),
|
||||
{"A - One": True, "B - Two": True, "C - Three": False})
|
||||
self.assertEqual(sorted(p.name for p in self.path.parent.iterdir()),
|
||||
["system_icons.json"])
|
||||
|
||||
def test_the_write_goes_through_a_scratch_file(self):
|
||||
with mock.patch.object(siterender.urllib.request, "urlopen",
|
||||
return_value=_Served()), \
|
||||
mock.patch.object(siterender, "write_text_atomic",
|
||||
wraps=siterender.write_text_atomic) as atomic, \
|
||||
mock.patch("builtins.print"):
|
||||
siterender.prime_system_icons({"B - Two"})
|
||||
atomic.assert_called_once()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user