""" Tests for the product gallery's matching logic. The embedding model is injected, so these run without torch or a GPU: what is being tested is the retrieval contract - best view wins, several views per SKU count as one answer, and a stranger is rejected rather than named. """ from __future__ import annotations import os import sys import unittest import numpy as np current_dir = os.path.dirname(os.path.abspath(__file__)) project_root = os.path.dirname(current_dir) if os.path.exists(os.path.join(project_root, "stock")): os.chdir(project_root) if current_dir not in sys.path: sys.path.insert(0, current_dir) from gallery import ( # noqa: E402 DINOV2_MODEL, DINOV2_SHA256, GalleryEntry, ProductGallery, catalog_digest, dinov2_weights_path, head_class_set, load_dinov2, ) import gallery as gallery_module # noqa: E402 def a_gallery(views, threshold=0.55): """A gallery with hand-made embeddings: no model, no files.""" skus = sorted({sku for sku, _ in views}) gallery = ProductGallery("does-not-exist", skus, threshold=threshold) embeddings = [] entries = [] for index, (sku, vector) in enumerate(views): vector = np.asarray(vector, dtype=np.float32) vector /= np.linalg.norm(vector) embeddings.append(vector) entries.append(GalleryEntry(sku_id=sku, view=f"{sku}_{index}", embedding=vector)) gallery._entries = entries gallery._matrix = np.stack(embeddings) gallery._available = True return gallery def unit(vector): vector = np.asarray(vector, dtype=np.float32) return (vector / np.linalg.norm(vector)).tolist() class Backbone: """A deterministic stand-in for DINOv2: the prepared crop's channel means, tiled to the width the real backbone returns.""" def __init__(self): self.calls = 0 def __call__(self, tensor): self.calls += 1 return tensor.mean(dim=(2, 3)).repeat(1, gallery_module.DINOV2_EMBED_DIM // 3) def a_query(): """A unit vector in the backbone's space, for match() without a model.""" query = np.zeros(gallery_module.DINOV2_EMBED_DIM, dtype=np.float32) query[0] = 1.0 query[1] = 0.05 return query / np.linalg.norm(query) class UnreachableBackbone: def __call__(self, tensor): raise AssertionError("the gallery loaded a backbone it should have cached") def write_view(path, seed): import cv2 image = np.random.RandomState(seed).randint(0, 256, (24, 32, 3), dtype=np.uint8) cv2.imwrite(path, image) def a_store(directory, names=("sku_a", "sku_a_1", "sku_b")): """A store dir: one JPEG per view plus the stock.csv the catalog is read from.""" with open(os.path.join(directory, "stock.csv"), "w", encoding="utf-8") as handle: handle.write("SKU_ID,product_name\n") for index, name in enumerate(names): write_view(os.path.join(directory, f"{name}.jpeg"), index) def a_stored_gallery(directory, names, backbone, image_size=32): gallery = ProductGallery(directory, list(names), device="cpu", image_size=image_size) gallery._load_model = lambda: backbone return gallery class MatchingTests(unittest.TestCase): def test_the_nearest_view_names_the_product(self): gallery = a_gallery([("sku_a", unit([1, 0, 0])), ("sku_b", unit([0, 1, 0]))]) gallery.embed = lambda crop: np.array([0.9, 0.1, 0.0], dtype=np.float32) match = gallery.match(object()) self.assertFalse(match.rejected) self.assertEqual(match.sku_id, "sku_a") self.assertAlmostEqual(match.similarity, 0.9, places=3) def test_several_views_of_one_sku_are_one_answer(self): # The second view of sku_b is closer than any view of sku_a, but sku_a must # still win on its best view: a SKU is not outvoted by having more photos. gallery = a_gallery([ ("sku_a", unit([1, 0, 0])), ("sku_b", unit([0, 1, 0])), ("sku_b", unit([0.2, 0.98, 0])), ]) gallery.embed = lambda crop: np.array([1.0, 0.05, 0.0], dtype=np.float32) match = gallery.match(object()) self.assertEqual(match.sku_id, "sku_a") self.assertEqual(match.runner_up, "sku_b") def test_a_stranger_is_rejected_not_named(self): gallery = a_gallery([("sku_a", unit([1, 0, 0])), ("sku_b", unit([0, 1, 0]))]) gallery.embed = lambda crop: np.array([0.0, 0.0, 1.0], dtype=np.float32) match = gallery.match(object()) self.assertTrue(match.rejected) self.assertIsNone(match.sku_id) def test_the_threshold_moves_the_rejection_line(self): gallery = a_gallery([("sku_a", unit([1, 0, 0]))], threshold=0.99) gallery.embed = lambda crop: np.array([0.9, 0.4, 0.0], dtype=np.float32) self.assertTrue(gallery.match(object()).rejected) self.assertFalse(gallery.match(object(), threshold=0.5).rejected) def test_margin_separates_a_confident_match_from_a_close_one(self): gallery = a_gallery([("sku_a", unit([1, 0, 0])), ("sku_b", unit([0.9, 0.44, 0]))]) gallery.embed = lambda crop: np.array([1.0, 0.1, 0.0], dtype=np.float32) confident = gallery.match(object()) gallery.embed = lambda crop: np.array([0.95, 0.3, 0.0], dtype=np.float32) close = gallery.match(object()) self.assertGreater(confident.margin, close.margin) def test_the_per_sku_max_does_not_depend_on_entry_order(self): # Three SKUs, several views each, enrolled out of order: a SKU is scored # by its best view, and the answer cannot move because a row moved. gallery = a_gallery([ ("sku_b", unit([0, 1, 0])), ("sku_a", unit([1, 0, 0])), ("sku_c", unit([0, 0, 1])), ("sku_b", unit([0.3, 0.95, 0])), ("sku_a", unit([0.98, 0.2, 0])), ]) gallery.embed = lambda crop: np.array([1.0, 0.05, 0.0], dtype=np.float32) straight = gallery.match(object()) self.assertEqual(straight.sku_id, "sku_a") self.assertEqual(straight.runner_up, "sku_b") for order in ([4, 0, 3, 1, 2], [2, 3, 1, 4, 0]): gallery._entries = [gallery._entries[index] for index in order] gallery._matrix = gallery._matrix[order] shuffled = gallery.match(object()) self.assertEqual(shuffled.sku_id, straight.sku_id) self.assertEqual(shuffled.runner_up, straight.runner_up) self.assertAlmostEqual(shuffled.similarity, straight.similarity, places=6) self.assertAlmostEqual(shuffled.runner_up_similarity, straight.runner_up_similarity, places=6) def test_a_tie_keeps_the_first_enrolled_sku(self): # Exactly equidistant: first appearance decides, as it did before the # per-SKU maximum became a segmented reduction. gallery = a_gallery([("sku_a", unit([1, 0, 0])), ("sku_b", unit([0, 1, 0]))]) gallery.embed = lambda crop: np.array([1.0, 1.0, 0.0], dtype=np.float32) match = gallery.match(object()) self.assertFalse(match.rejected) self.assertEqual(match.sku_id, "sku_a") self.assertEqual(match.runner_up, "sku_b") self.assertAlmostEqual(match.similarity, match.runner_up_similarity, places=6) self.assertAlmostEqual(match.margin, 0.0, places=6) def test_an_unloaded_gallery_rejects_everything(self): gallery = ProductGallery("does-not-exist", ["sku_a"]) match = gallery.match(object()) self.assertTrue(match.rejected) class EnrollmentTests(unittest.TestCase): def test_views_are_found_by_sku_prefix(self): import tempfile with tempfile.TemporaryDirectory() as directory: for name in ("sku_a.jpeg", "sku_a_1.jpeg", "sku_a-2.jpeg", "sku_ab.jpeg", "notes.txt"): open(os.path.join(directory, name), "wb").close() gallery = ProductGallery(directory, ["sku_a", "sku_ab"]) self.assertEqual(len(gallery._views_for("sku_a")), 3, "one view, two extra views, and not sku_ab") self.assertEqual(len(gallery._views_for("sku_ab")), 1) class FeatureCacheTests(unittest.TestCase): """The cached matrix must be this store's, or it must not be used.""" def setUp(self): import hashlib import tempfile self.directory = tempfile.TemporaryDirectory() self.addCleanup(self.directory.cleanup) a_store(self.directory.name) # A cache is only served when the pinned backbone is on disk, so point the # pin at a stand-in file the module's expected digest is told about. The # check itself - hash this file, compare - is what is under test. self.weights = os.path.join(self.directory.name, "dinov2.safetensors") self.write(self.weights, b"not really a checkpoint") os.environ["DORAYAKI_DINOV2_WEIGHTS"] = self.weights self.addCleanup(os.environ.pop, "DORAYAKI_DINOV2_WEIGHTS", None) self.addCleanup(setattr, gallery_module, "DINOV2_SHA256", gallery_module.DINOV2_SHA256) gallery_module.DINOV2_SHA256 = hashlib.sha256(b"not really a checkpoint").hexdigest() def write(self, path, content): with open(path, "wb") as handle: handle.write(content) return path def real_gallery(self): """A gallery with the real _load_model: the server's own readiness path.""" return ProductGallery(self.directory.name, ["sku_a", "sku_b"], device="cpu", image_size=32) def loaded(self, names=("sku_a", "sku_b"), image_size=32): backbone = Backbone() gallery = a_stored_gallery(self.directory.name, names, backbone, image_size=image_size) self.assertTrue(gallery.load()) return gallery, backbone def cache_payload(self, gallery): """The rows and matrix a loaded gallery left on disk.""" with np.load(gallery._cache_path(), allow_pickle=False) as payload: return ( [str(value) for value in payload["skus"].tolist()], [str(value) for value in payload["views"].tolist()], payload["matrix"], ) def rewrite_cache(self, gallery, skus, views, matrix): """The same valid key with different contents: only the rows can catch it.""" path = gallery._cache_path() with np.load(path, allow_pickle=False) as payload: key = str(payload["key"].item()) np.savez(path, key=key, skus=np.array(skus), views=np.array(views), matrix=matrix) def re_embeds(self, first, why): """The one right answer to a rewritten cache: build it from the pixels.""" backbone = Backbone() gallery = a_stored_gallery(self.directory.name, ("sku_a", "sku_b"), backbone) self.assertTrue(gallery.load(), why) self.assertEqual(backbone.calls, 1, f"{why} must not be served from cache") np.testing.assert_array_equal(gallery._matrix, first._matrix) self.assertEqual([entry.sku_id for entry in gallery._entries], ["sku_a", "sku_a", "sku_b"]) def test_a_second_start_answers_the_same_from_the_cache(self): first, backbone = self.loaded() self.assertEqual(backbone.calls, 1) self.assertEqual(first.view_count(), 3) self.assertTrue(os.path.isfile(first._cache_path())) cached = a_stored_gallery(self.directory.name, ("sku_a", "sku_b"), UnreachableBackbone()) self.assertTrue(cached.load()) self.assertEqual(cached.view_count(), first.view_count()) np.testing.assert_array_equal(cached._matrix, first._matrix) self.assertEqual([entry.view for entry in cached._entries], [entry.view for entry in first._entries]) query = a_query() first.embed = cached.embed = lambda crop: query self.assertEqual(vars(cached.match(object())), vars(first.match(object()))) def test_changed_view_bytes_re_embed(self): first, _ = self.loaded() before = first._matrix.copy() write_view(os.path.join(self.directory.name, "sku_b.jpeg"), seed=99) second, backbone = self.loaded() self.assertEqual(backbone.calls, 1, "a replaced view must not be served from cache") self.assertFalse(np.array_equal(second._matrix, before)) def test_a_reordered_catalog_does_not_borrow_another_mapping(self): self.loaded() swapped, backbone = self.loaded(names=("sku_b", "sku_a")) self.assertEqual(backbone.calls, 1) self.assertEqual([entry.sku_id for entry in swapped._entries], ["sku_b", "sku_a", "sku_a"]) def test_another_image_size_is_never_served(self): small, _ = self.loaded(image_size=32) large, backbone = self.loaded(image_size=64) self.assertEqual(backbone.calls, 1) self.assertNotEqual(small._cache_path(), large._cache_path()) def test_a_corrupt_cache_re_embeds_the_same_matrix(self): first, _ = self.loaded() with open(first._cache_path(), "wb") as handle: handle.write(b"not an npz at all") second, backbone = self.loaded() self.assertEqual(backbone.calls, 1) np.testing.assert_array_equal(second._matrix, first._matrix) def test_swapped_cache_rows_are_not_served(self): # A current key says nothing about the rows inside the file. The two # sku_a views are adjacent, so a swap has to cross SKUs to mean # anything - and the matrix that moves with them is the dangerous part: # a vector that would then be filed under the wrong product. first, _ = self.loaded() skus, views, matrix = self.cache_payload(first) self.assertEqual(list(zip(skus, views)), [("sku_a", "sku_a.jpeg"), ("sku_a", "sku_a_1.jpeg"), ("sku_b", "sku_b.jpeg")]) self.rewrite_cache(first, skus[::-1], views[::-1], matrix[::-1]) self.re_embeds(first, "rows swapped with their vectors") def test_mislabelled_cache_rows_are_not_served(self): # The views and the matrix stay where they are, so every row names a # product it was not embedded for. first, _ = self.loaded() skus, views, matrix = self.cache_payload(first) self.rewrite_cache(first, [skus[2], skus[1], skus[0]], views, matrix) self.re_embeds(first, "labels swapped around the views") def test_a_duplicated_cache_row_is_not_served(self): # The first view written twice: one image would answer for two products. first, _ = self.loaded() skus, views, matrix = self.cache_payload(first) self.rewrite_cache(first, skus[:1] * 2 + skus[2:], views[:1] * 2 + views[2:], matrix) self.re_embeds(first, "a row repeated") def test_a_foreign_sku_row_is_not_served(self): # A row from another store's catalog: a SKU this gallery never enrolled. first, _ = self.loaded() skus, views, matrix = self.cache_payload(first) self.rewrite_cache(first, skus[1:] + ["other_store"], views[1:] + ["other_store.jpeg"], matrix) self.re_embeds(first, "another store's row") def test_a_cache_may_omit_a_view_it_could_not_decode(self): # The writer drops a view it could not read and keeps everyone else's # order, so such a cache is still this store's - and still worth serving # rather than re-embedding the whole catalog on every start. first, _ = self.loaded() skus, views, matrix = self.cache_payload(first) self.rewrite_cache(first, skus[:1] + skus[2:], views[:1] + views[2:], matrix[[0, 2]]) backbone = Backbone() gallery = a_stored_gallery(self.directory.name, ("sku_a", "sku_b"), backbone) self.assertTrue(gallery.load()) self.assertEqual(backbone.calls, 0, "a readable shorter cache is still a hit") self.assertEqual([entry.sku_id for entry in gallery._entries], ["sku_a", "sku_b"]) np.testing.assert_array_equal(gallery._matrix, first._matrix[[0, 2]]) def test_a_nonnumeric_cache_matrix_is_not_served(self): # Only the float matrix _cache_write produces is a cache. A complex # matrix is finite and casts without complaint, so it answers with a # different matrix than the one on disk; integer, boolean and text # matrices are not embeddings at all. first, _ = self.loaded() skus, views, matrix = self.cache_payload(first) others = { "complex": matrix.astype(np.complex128), "timedelta": np.zeros_like(matrix, dtype="timedelta64[s]"), "integer": (matrix * 8).astype(np.int32), "boolean": np.ones_like(matrix, dtype=bool), "text": np.zeros(matrix.shape, dtype="