from __future__ import annotations import base64 import hashlib import json from pathlib import Path import pytest from app.fonts.store import FontCatalog, FontValidationError from app.templates.store import TemplateStore, prepare_scene from app.animations.store import AnimationStore ID = "builtin:walnut-bus-8x16" def scene(font=ID): return prepare_scene(dict(version=1, width=64, height=64, pixelRgb=base64.b64encode(bytes(64*64*3)).decode("ascii"), elements=[dict(id="text",type="text",text="高铁路",font=font,size=16,x=0,y=8,align="left",color="#FFFFFF")])) class ChangingFont: def __init__(self, catalog): self.catalog, self.version = catalog, 1 def render_version(self, identifier): return str(self.version)*64 if identifier == ID else None def resolve(self, identifier): return self.catalog.resolve(ID if self.version == 1 else "builtin:walnut-bus-16x32") def test_template_cache_rebuilt_without_revision_or_record_change(tmp_path): owner=ChangingFont(FontCatalog(tmp_path)) store=TemplateStore(tmp_path, font_resolver=owner.resolve) item=store.create("bus",scene()) path=store.thumbnail_path(item["id"]) record=store._record_path(item["id"]) before_bytes, before_record = path.read_bytes(), record.read_bytes() assert "font_v=" in item["thumbnail_url"] owner.version=2 restarted=TemplateStore(tmp_path,font_resolver=owner.resolve) updated=restarted.list()["templates"][0] assert updated["revision"] == item["revision"] assert record.read_bytes() == before_record assert path.read_bytes() != before_bytes assert updated["thumbnail_url"] != item["thumbnail_url"] stable=path.stat().st_mtime_ns TemplateStore(tmp_path,font_resolver=owner.resolve) assert path.stat().st_mtime_ns == stable def test_animation_cache_rebuilt_without_scene_or_revision_change(tmp_path): owner=ChangingFont(FontCatalog(tmp_path)) store=AnimationStore(tmp_path,font_resolver=owner.resolve) item=store.create_from_frames("bus",[dict(scene=scene(),duration_ms=100)]) frame=store.get(item["id"])["frames"][0] stored_frame=store._read(item["id"])["frames"][0] path=store._thumb_dir(item["id"])/stored_frame["thumbnail"] record=store._path(item["id"]) before_bytes, before_record = path.read_bytes(), record.read_bytes() owner.version=2 restarted=AnimationStore(tmp_path,font_resolver=owner.resolve) updated=restarted.get(item["id"]) assert updated["revision"] == item["revision"] assert record.read_bytes() == before_record assert path.read_bytes() != before_bytes assert updated["frames"][0]["thumbnail_url"] != frame["thumbnail_url"] stable=path.stat().st_mtime_ns AnimationStore(tmp_path,font_resolver=owner.resolve) assert path.stat().st_mtime_ns == stable def test_unrelated_thumbnails_untouched(tmp_path): owner=ChangingFont(FontCatalog(tmp_path)) store=TemplateStore(tmp_path,font_resolver=owner.resolve) item=store.create("normal",scene("default")) path=store.thumbnail_path(item["id"]) stable=path.read_bytes(),path.stat().st_mtime_ns assert "font_v=" not in item["thumbnail_url"] owner.version=2 TemplateStore(tmp_path,font_resolver=owner.resolve) assert (path.read_bytes(),path.stat().st_mtime_ns) == stable def test_exact_obsolete_retirement_and_corruption_preservation(tmp_path, monkeypatch): import app.fonts.store as module bundle=Path(module.__file__).parent/'bundled' registration=json.loads((bundle/'manifest.json').read_text(encoding='utf-8')) old_payload=b'old-font-fixture' old_digest=hashlib.sha256(old_payload).hexdigest() registration['obsolete']=[old_digest] registration['aliases']={f'user:{old_digest}:0':ID} # Substitute only the registration reader, retaining real bundled font # files and checksum validation; fixture files never need old private state. real_read=Path.read_text def read(path,*args,**kwargs): if path == bundle/'manifest.json':return json.dumps(registration) return real_read(path,*args,**kwargs) monkeypatch.setattr(Path,'read_text',read) root=tmp_path/'fonts';root.mkdir() old=root/f'{old_digest}.font';old.write_bytes(old_payload) unrelated=root/'unregistered.font';unrelated.write_bytes(b'keep') catalog=FontCatalog(tmp_path) assert not old.exists() assert unrelated.read_bytes() == b'keep' assert catalog.resolve(f'user:{old_digest}:0') == catalog.resolve(ID) old.write_bytes(b'corrupt') with pytest.raises(FontValidationError,match='preserved'): FontCatalog(tmp_path) assert old.read_bytes() == b'corrupt'