464 lines
11 KiB
Elixir
464 lines
11 KiB
Elixir
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 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
|