@@ -973,6 +973,64 @@ def test_locate_site_class_cache_is_bounded(ag3_sim_api: AnophelesSnpData):
973973 assert len (ag3_sim_api ._cache_locate_site_class ) <= _LOCATE_SITE_CLASS_CACHE_MAXSIZE
974974
975975
976+ def test_locate_site_class_cache_lru_eviction (ag3_sim_api : AnophelesSnpData ):
977+ """Verify true LRU semantics: recently *accessed* entries survive eviction,
978+ while least-recently-used entries are evicted first."""
979+ from collections import OrderedDict
980+
981+ from malariagen_data .anoph .snp_data import _LOCATE_SITE_CLASS_CACHE_MAXSIZE
982+
983+ cache = ag3_sim_api ._cache_locate_site_class
984+
985+ # Start from a clean cache.
986+ cache .clear ()
987+ assert isinstance (cache , OrderedDict )
988+
989+ maxsize = _LOCATE_SITE_CLASS_CACHE_MAXSIZE # 64
990+
991+ # --- Phase 1: fill the cache to exactly maxsize ---
992+ dummy = np .array ([True , False ])
993+ for i in range (maxsize ):
994+ key = (f"contig_{ i } " , f"mask_{ i } " , f"class_{ i } " )
995+ cache [key ] = dummy
996+ assert len (cache ) == maxsize
997+
998+ # Remember the first key inserted (the oldest / least-recently-used).
999+ first_key = ("contig_0" , "mask_0" , "class_0" )
1000+ assert first_key in cache
1001+
1002+ # --- Phase 2: simulate an access (LRU promotion) on the first key ---
1003+ # move_to_end makes it the most-recently-used entry.
1004+ cache .move_to_end (first_key )
1005+
1006+ # Insert one more entry, exceeding maxsize.
1007+ overflow_key = ("overflow" , "mask" , "class" )
1008+ cache [overflow_key ] = dummy
1009+
1010+ # Evict to maintain the bound (same logic as _locate_site_class).
1011+ while len (cache ) > maxsize :
1012+ oldest = next (iter (cache ))
1013+ del cache [oldest ]
1014+
1015+ # The first key should STILL be present because it was promoted.
1016+ assert (
1017+ first_key in cache
1018+ ), "LRU promotion via move_to_end must keep recently accessed entries alive"
1019+
1020+ # The second key ("contig_1", ...) — which was never re-accessed —
1021+ # should have been evicted as the new least-recently-used entry.
1022+ second_key = ("contig_1" , "mask_1" , "class_1" )
1023+ assert (
1024+ second_key not in cache
1025+ ), "The least-recently-used entry should be evicted when cache exceeds maxsize"
1026+
1027+ # The overflow key should be present (it was just inserted).
1028+ assert overflow_key in cache
1029+
1030+ # Cache size must remain bounded.
1031+ assert len (cache ) == maxsize
1032+
1033+
9761034def test_snp_calls_cache_is_per_instance (ag3_sim_api : AnophelesSnpData ):
9771035 """_cached_snp_calls must be a per-instance lru_cache, not a class-level one.
9781036
0 commit comments