defmodule SnmpKit.SnmpLib.CacheTest do use ExUnit.Case, async: false alias SnmpKit.SnmpLib.Cache doctest Cache @moduletag :cache_test setup do # Ensure cache process is stopped before each test if Process.whereis(Cache) do try do GenServer.stop(Cache, :normal, 1000) catch :exit, _ -> :ok end end :timer.sleep(10) :ok end describe "Cache.start_link/1" do test "starts with default configuration" do assert {:ok, pid} = Cache.start_link() assert Process.alive?(pid) try do GenServer.stop(Cache) catch :exit, _ -> :ok end end test "starts with custom configuration" do opts = [ max_size: 50_000, compression_enabled: true, adaptive_ttl_enabled: true ] assert {:ok, pid} = Cache.start_link(opts) assert Process.alive?(pid) try do GenServer.stop(Cache) catch :exit, _ -> :ok end end end describe "Cache.put/3 and Cache.get/1" do setup do {:ok, _pid} = Cache.start_link() on_exit(fn -> if Process.whereis(Cache) do try do GenServer.stop(Cache, :normal, 1000) catch :exit, _ -> :ok end end end) :ok end test "stores and retrieves simple values" do key = "test_key" value = "test_value" :ok = Cache.put(key, value) assert {:ok, ^value} = Cache.get(key) end test "returns miss for non-existent keys" do assert :miss = Cache.get("non_existent_key") end test "handles complex data structures" do key = "complex_data" value = %{ device: "192.168.1.1", oids: [1, 3, 6, 1, 2, 1, 1, 1, 0], response: {:string, "Cisco Router"}, timestamp: System.system_time(:millisecond) } :ok = Cache.put(key, value) assert {:ok, ^value} = Cache.get(key) end test "respects TTL expiration" do key = "ttl_test" value = "expires_soon" # Set very short TTL :ok = Cache.put(key, value, ttl: 50) assert {:ok, ^value} = Cache.get(key) # Wait for expiration :timer.sleep(100) assert :miss = Cache.get(key) end test "handles compression for large values" do key = "large_data" # Create a large value that should trigger compression large_value = String.duplicate("A", 2000) :ok = Cache.put(key, large_value, compress: true) assert {:ok, ^large_value} = Cache.get(key) end end describe "Cache.put_adaptive/4" do setup do {:ok, _pid} = Cache.start_link() on_exit(fn -> if Process.whereis(Cache) do try do GenServer.stop(Cache, :normal, 1000) catch :exit, _ -> :ok end end end) :ok end test "stores values with adaptive TTL" do key = "adaptive_test" value = "adaptive_value" :ok = Cache.put_adaptive(key, value, 1000, :medium) assert {:ok, ^value} = Cache.get(key) end test "adjusts TTL based on volatility" do # Low volatility should have longer TTL :ok = Cache.put_adaptive("low_volatility", "stable_data", 1000, :low) # High volatility should have shorter TTL :ok = Cache.put_adaptive("high_volatility", "changing_data", 1000, :high) # Both should be retrievable immediately assert {:ok, "stable_data"} = Cache.get("low_volatility") assert {:ok, "changing_data"} = Cache.get("high_volatility") end end describe "Cache.delete/1" do setup do {:ok, _pid} = Cache.start_link() on_exit(fn -> if Process.whereis(Cache) do try do GenServer.stop(Cache, :normal, 1000) catch :exit, _ -> :ok end end end) :ok end test "deletes specific cache entries" do key = "delete_test" value = "to_be_deleted" :ok = Cache.put(key, value) assert {:ok, ^value} = Cache.get(key) :ok = Cache.delete(key) assert :miss = Cache.get(key) end end describe "Cache.invalidate_pattern/1" do setup do {:ok, _pid} = Cache.start_link() on_exit(fn -> if Process.whereis(Cache) do try do GenServer.stop(Cache, :normal, 1000) catch :exit, _ -> :ok end end end) :ok end test "invalidates entries matching pattern" do # Store entries with common prefix Cache.put("device_1:sysDescr", "Router 1") Cache.put("device_1:sysName", "R1") Cache.put("device_2:sysDescr", "Router 2") # Verify they exist assert {:ok, "Router 1"} = Cache.get("device_1:sysDescr") assert {:ok, "R1"} = Cache.get("device_1:sysName") assert {:ok, "Router 2"} = Cache.get("device_2:sysDescr") # Invalidate device_1 entries :ok = Cache.invalidate_pattern("device_1:*") # device_1 entries should be gone assert :miss = Cache.get("device_1:sysDescr") assert :miss = Cache.get("device_1:sysName") # device_2 entries should remain assert {:ok, "Router 2"} = Cache.get("device_2:sysDescr") end end describe "Cache.invalidate_by_tag/1" do setup do {:ok, _pid} = Cache.start_link() on_exit(fn -> if Process.whereis(Cache) do try do GenServer.stop(Cache, :normal, 1000) catch :exit, _ -> :ok end end end) :ok end test "invalidates entries by tag" do # Store entries with tags Cache.put("entry1", "data1", tags: [:interface_data]) Cache.put("entry2", "data2", tags: [:interface_data, :statistics]) Cache.put("entry3", "data3", tags: [:system_data]) # Verify they exist assert {:ok, "data1"} = Cache.get("entry1") assert {:ok, "data2"} = Cache.get("entry2") assert {:ok, "data3"} = Cache.get("entry3") # Invalidate by tag :ok = Cache.invalidate_by_tag(:interface_data) # Interface data entries should be gone assert :miss = Cache.get("entry1") assert :miss = Cache.get("entry2") # System data should remain assert {:ok, "data3"} = Cache.get("entry3") end end describe "Cache.warm_cache/3" do setup do {:ok, _pid} = Cache.start_link() on_exit(fn -> if Process.whereis(Cache) do try do GenServer.stop(Cache, :normal, 1000) catch :exit, _ -> :ok end end end) :ok end test "performs immediate cache warming" do device_id = "192.168.1.1" oids = ["1.3.6.1.2.1.1.1.0", "1.3.6.1.2.1.1.3.0"] assert :ok = Cache.warm_cache(device_id, oids, strategy: :immediate) # Allow time for async processing :timer.sleep(10) end test "performs predictive cache warming" do device_id = "192.168.1.1" assert :ok = Cache.warm_cache(device_id, :auto, strategy: :predictive) # Allow time for async processing :timer.sleep(10) end test "schedules cache warming" do device_id = "192.168.1.1" oids = ["1.3.6.1.2.1.1.1.0"] assert :ok = Cache.warm_cache(device_id, oids, strategy: :scheduled) # Allow time for async processing :timer.sleep(10) end end describe "Cache.get_stats/0" do setup do {:ok, _pid} = Cache.start_link() on_exit(fn -> if Process.whereis(Cache) do try do GenServer.stop(Cache, :normal, 1000) catch :exit, _ -> :ok end end end) :ok end test "returns cache statistics" do # Perform some cache operations to generate stats Cache.put("test1", "value1") Cache.put("test2", "value2") # Hit Cache.get("test1") # Miss Cache.get("test3") stats = Cache.get_stats() assert is_map(stats) assert Map.has_key?(stats, :total_entries) assert Map.has_key?(stats, :hit_rate) assert Map.has_key?(stats, :miss_rate) assert Map.has_key?(stats, :eviction_count) assert Map.has_key?(stats, :memory_usage_mb) assert Map.has_key?(stats, :compression_ratio) assert is_integer(stats.total_entries) assert is_float(stats.hit_rate) assert is_float(stats.miss_rate) assert is_integer(stats.eviction_count) assert is_float(stats.memory_usage_mb) assert is_float(stats.compression_ratio) # Should have some entries assert stats.total_entries > 0 end end describe "Cache.clear/0" do setup do {:ok, _pid} = Cache.start_link() on_exit(fn -> if Process.whereis(Cache) do try do GenServer.stop(Cache, :normal, 1000) catch :exit, _ -> :ok end end end) :ok end test "clears all cached data" do # Add some entries Cache.put("test1", "value1") Cache.put("test2", "value2") # Verify they exist assert {:ok, "value1"} = Cache.get("test1") assert {:ok, "value2"} = Cache.get("test2") # Clear cache :ok = Cache.clear() # All entries should be gone assert :miss = Cache.get("test1") assert :miss = Cache.get("test2") # Stats should be reset stats = Cache.get_stats() assert stats.total_entries == 0 end end describe "SNMP-specific caching patterns" do setup do {:ok, _pid} = Cache.start_link() on_exit(fn -> if Process.whereis(Cache) do try do GenServer.stop(Cache, :normal, 1000) catch :exit, _ -> :ok end end end) :ok end test "caches SNMP responses by device and OID" do device = "192.168.1.1" oid = [1, 3, 6, 1, 2, 1, 1, 1, 0] response = {:string, "Cisco 2960 Switch"} key = "#{device}:#{Enum.join(oid, ".")}" :ok = Cache.put(key, response, ttl: 300_000) assert {:ok, ^response} = Cache.get(key) end test "caches table walk results" do device = "192.168.1.1" table_oid = [1, 3, 6, 1, 2, 1, 2, 2] table_data = [ {[1, 3, 6, 1, 2, 1, 2, 2, 1, 1, 1], {:integer, 1}}, {[1, 3, 6, 1, 2, 1, 2, 2, 1, 1, 2], {:integer, 2}}, {[1, 3, 6, 1, 2, 1, 2, 2, 1, 1, 3], {:integer, 3}} ] key = "#{device}:walk:#{Enum.join(table_oid, ".")}" :ok = Cache.put_adaptive(key, table_data, 60_000, :medium) assert {:ok, ^table_data} = Cache.get(key) end test "uses device-specific cache invalidation" do device1 = "192.168.1.1" device2 = "192.168.1.2" # Cache data for both devices Cache.put("#{device1}:sysDescr", "Router 1") Cache.put("#{device1}:sysName", "R1") Cache.put("#{device2}:sysDescr", "Router 2") # Invalidate all data for device1 :ok = Cache.invalidate_pattern("#{device1}:*") # Device1 data should be gone assert :miss = Cache.get("#{device1}:sysDescr") assert :miss = Cache.get("#{device1}:sysName") # Device2 data should remain assert {:ok, "Router 2"} = Cache.get("#{device2}:sysDescr") end end end