"""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