feat(clients): LRU mesh cache for Open3DModel recipes (etag-validated)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
16ffe6ac20
commit
684bd2de5e
2 files changed
+146
No files matched your search
@@ -0,0 +1,101 @@
|
||||
"""Unit tests for RecipeMeshCache."""
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from presa_mano.clients.open3dmodel_cache import RecipeMesh, RecipeMeshCache
|
||||
|
||||
|
||||
def _verts_tris():
|
||||
verts = np.array([[0.,0.,0.],[1.,0.,0.],[0.,1.,0.]])
|
||||
tris = np.array([[0,1,2]], dtype=np.int32)
|
||||
return verts, tris
|
||||
|
||||
|
||||
def _mock_client(stl_bytes_by_id: dict[str, tuple[bytes, str]]):
|
||||
"""Mock client whose get_stl_bytes returns the registered (bytes, etag)
|
||||
for each recipe_id, and counts calls."""
|
||||
m = MagicMock()
|
||||
m._call_count = {}
|
||||
def _get(recipe_id: str):
|
||||
m._call_count[recipe_id] = m._call_count.get(recipe_id, 0) + 1
|
||||
return stl_bytes_by_id[recipe_id]
|
||||
m.get_stl_bytes.side_effect = _get
|
||||
return m
|
||||
|
||||
|
||||
def test_cache_miss_then_hit(monkeypatch):
|
||||
verts, tris = _verts_tris()
|
||||
monkeypatch.setattr(
|
||||
"presa_mano.clients.open3dmodel_cache.parse_stl_bytes",
|
||||
lambda b: (verts, tris),
|
||||
)
|
||||
client = _mock_client({"r1": (b"STL1", "etag-A")})
|
||||
cache = RecipeMeshCache(client=client, capacity=4)
|
||||
|
||||
m1 = cache.get("r1")
|
||||
assert isinstance(m1, RecipeMesh)
|
||||
np.testing.assert_array_equal(m1.vertices, verts)
|
||||
np.testing.assert_array_equal(m1.triangles, tris)
|
||||
assert m1.etag == "etag-A"
|
||||
assert client._call_count["r1"] == 1
|
||||
|
||||
# secondo accesso: la cache valida l'etag (refetch dei bytes, sempre)
|
||||
# ma evita il parse -> identity preserved sull'oggetto RecipeMesh.
|
||||
m2 = cache.get("r1")
|
||||
assert m2 is m1
|
||||
assert client._call_count["r1"] == 2
|
||||
|
||||
|
||||
def test_cache_etag_change_refetches(monkeypatch):
|
||||
verts, tris = _verts_tris()
|
||||
monkeypatch.setattr(
|
||||
"presa_mano.clients.open3dmodel_cache.parse_stl_bytes",
|
||||
lambda b: (verts, tris),
|
||||
)
|
||||
client = MagicMock()
|
||||
seq = iter([(b"STL_OLD", "etag-A"), (b"STL_NEW", "etag-B")])
|
||||
client.get_stl_bytes.side_effect = lambda rid: next(seq)
|
||||
cache = RecipeMeshCache(client=client, capacity=4)
|
||||
cache.get("r1")
|
||||
m2 = cache.get("r1")
|
||||
assert m2.etag == "etag-B"
|
||||
assert client.get_stl_bytes.call_count == 2
|
||||
|
||||
|
||||
def test_cache_lru_evicts_oldest(monkeypatch):
|
||||
verts, tris = _verts_tris()
|
||||
monkeypatch.setattr(
|
||||
"presa_mano.clients.open3dmodel_cache.parse_stl_bytes",
|
||||
lambda b: (verts, tris),
|
||||
)
|
||||
client = _mock_client({
|
||||
f"r{i}": (f"S{i}".encode(), f"e{i}") for i in range(10)
|
||||
})
|
||||
cache = RecipeMeshCache(client=client, capacity=3)
|
||||
cache.get("r1"); cache.get("r2"); cache.get("r3")
|
||||
# r1 e' il piu' vecchio
|
||||
cache.get("r4") # evict r1
|
||||
cache.get("r1") # miss -> re-fetch (call_count va a 2)
|
||||
assert client._call_count["r1"] == 2
|
||||
|
||||
|
||||
def test_cache_get_uses_overrideable_client(monkeypatch):
|
||||
"""`get(recipe_id, client=other)` permette di passare un client fresco
|
||||
(utile quando PUT /api/config cambia base_url)."""
|
||||
verts, tris = _verts_tris()
|
||||
monkeypatch.setattr(
|
||||
"presa_mano.clients.open3dmodel_cache.parse_stl_bytes",
|
||||
lambda b: (verts, tris),
|
||||
)
|
||||
default_client = _mock_client({"r1": (b"S1", "e1")})
|
||||
override_client = _mock_client({"r1": (b"S2", "e2")})
|
||||
cache = RecipeMeshCache(client=default_client, capacity=2)
|
||||
|
||||
m = cache.get("r1", client=override_client)
|
||||
assert m.etag == "e2"
|
||||
assert default_client._call_count.get("r1", 0) == 0
|
||||
assert override_client._call_count["r1"] == 1
|
||||
Reference in new issue
Block a user