189 lines
5.5 KiB
Elixir
189 lines
5.5 KiB
Elixir
defmodule Aprsme.CacheTest do
|
|
use ExUnit.Case, async: true
|
|
|
|
alias Aprsme.Cache
|
|
|
|
@table_name :cache_test_table
|
|
|
|
setup do
|
|
:ets.new(@table_name, [:set, :public, :named_table])
|
|
|
|
on_exit(fn ->
|
|
try do
|
|
:ets.delete(@table_name)
|
|
rescue
|
|
ArgumentError -> :ok
|
|
end
|
|
end)
|
|
|
|
:ok
|
|
end
|
|
|
|
describe "put/4 and get/2" do
|
|
test "stores and retrieves a value" do
|
|
assert {:ok, true} = Cache.put(@table_name, :key, "value")
|
|
assert {:ok, "value"} = Cache.get(@table_name, :key)
|
|
end
|
|
|
|
test "returns nil for missing keys" do
|
|
assert {:ok, nil} = Cache.get(@table_name, :missing)
|
|
end
|
|
|
|
test "overwrites existing values" do
|
|
Cache.put(@table_name, :key, "first")
|
|
Cache.put(@table_name, :key, "second")
|
|
assert {:ok, "second"} = Cache.get(@table_name, :key)
|
|
end
|
|
end
|
|
|
|
describe "TTL support" do
|
|
test "entry with TTL is returned before expiry" do
|
|
Cache.put(@table_name, :key, "value", ttl: 60_000)
|
|
assert {:ok, "value"} = Cache.get(@table_name, :key)
|
|
end
|
|
|
|
test "entry with expired TTL returns nil" do
|
|
# Use a TTL of 1ms and sleep briefly
|
|
Cache.put(@table_name, :key, "value", ttl: 1)
|
|
Process.sleep(5)
|
|
assert {:ok, nil} = Cache.get(@table_name, :key)
|
|
end
|
|
|
|
test "entry without TTL never expires" do
|
|
Cache.put(@table_name, :key, "value")
|
|
assert {:ok, "value"} = Cache.get(@table_name, :key)
|
|
end
|
|
|
|
test "expired entry is lazily deleted from ETS" do
|
|
Cache.put(@table_name, :key, "value", ttl: 1)
|
|
Process.sleep(5)
|
|
|
|
# First get triggers lazy deletion
|
|
Cache.get(@table_name, :key)
|
|
|
|
# Verify entry is gone from ETS
|
|
assert [] = :ets.lookup(@table_name, :key)
|
|
end
|
|
end
|
|
|
|
describe "exists?/2" do
|
|
test "returns true for existing key" do
|
|
Cache.put(@table_name, :key, "value")
|
|
assert Cache.exists?(@table_name, :key)
|
|
end
|
|
|
|
test "returns false for missing key" do
|
|
refute Cache.exists?(@table_name, :missing)
|
|
end
|
|
|
|
test "returns false for expired key" do
|
|
Cache.put(@table_name, :key, "value", ttl: 1)
|
|
Process.sleep(5)
|
|
refute Cache.exists?(@table_name, :key)
|
|
end
|
|
end
|
|
|
|
describe "ttl/2" do
|
|
test "returns nil for entries without TTL" do
|
|
Cache.put(@table_name, :key, "value")
|
|
assert {:ok, nil} = Cache.ttl(@table_name, :key)
|
|
end
|
|
|
|
test "returns remaining time for entries with TTL" do
|
|
Cache.put(@table_name, :key, "value", ttl: 60_000)
|
|
assert {:ok, remaining} = Cache.ttl(@table_name, :key)
|
|
assert is_integer(remaining)
|
|
assert remaining > 0
|
|
assert remaining <= 60_000
|
|
end
|
|
|
|
test "returns nil for missing keys" do
|
|
assert {:ok, nil} = Cache.ttl(@table_name, :missing)
|
|
end
|
|
end
|
|
|
|
describe "del/2" do
|
|
test "deletes an existing key" do
|
|
Cache.put(@table_name, :key, "value")
|
|
assert {:ok, true} = Cache.del(@table_name, :key)
|
|
assert {:ok, nil} = Cache.get(@table_name, :key)
|
|
end
|
|
end
|
|
|
|
describe "clear/1" do
|
|
test "removes all entries" do
|
|
Cache.put(@table_name, :a, 1)
|
|
Cache.put(@table_name, :b, 2)
|
|
assert {:ok, true} = Cache.clear(@table_name)
|
|
assert {:ok, nil} = Cache.get(@table_name, :a)
|
|
assert {:ok, nil} = Cache.get(@table_name, :b)
|
|
end
|
|
end
|
|
|
|
describe "to_timeout/1" do
|
|
test "converts time units to milliseconds" do
|
|
assert Cache.to_timeout(hour: 1) == 3_600_000
|
|
assert Cache.to_timeout(minute: 30) == 1_800_000
|
|
assert Cache.to_timeout(day: 1) == 86_400_000
|
|
assert Cache.to_timeout(second: 5) == 5_000
|
|
assert Cache.to_timeout(hours: 2, minutes: 30) == 9_000_000
|
|
end
|
|
end
|
|
|
|
describe "error handling" do
|
|
test "get returns error for non-existent table" do
|
|
assert {:error, :no_cache} = Cache.get(:nonexistent_table, :key)
|
|
end
|
|
|
|
test "put returns error for non-existent table" do
|
|
assert {:error, :no_cache} = Cache.put(:nonexistent_table, :key, "value")
|
|
end
|
|
|
|
test "del returns error for non-existent table" do
|
|
assert {:error, :no_cache} = Cache.del(:nonexistent_table, :key)
|
|
end
|
|
|
|
test "clear returns error for non-existent table" do
|
|
assert {:error, :no_cache} = Cache.clear(:nonexistent_table)
|
|
end
|
|
end
|
|
|
|
describe "stats/1" do
|
|
test "returns the current size of the cache" do
|
|
Cache.put(@table_name, :a, 1)
|
|
Cache.put(@table_name, :b, 2)
|
|
assert {:ok, %{size: size}} = Cache.stats(@table_name)
|
|
assert size == 2
|
|
end
|
|
|
|
test "returns error for non-existent tables" do
|
|
assert {:error, :no_cache} = Cache.stats(:does_not_exist_table)
|
|
end
|
|
end
|
|
|
|
describe "legacy 2-element tuple lookups" do
|
|
test "get handles tuples that lack an expires_at" do
|
|
:ets.insert(@table_name, {:legacy_key, "legacy_value"})
|
|
assert {:ok, "legacy_value"} = Cache.get(@table_name, :legacy_key)
|
|
end
|
|
|
|
test "ttl handles legacy tuples (returns nil)" do
|
|
:ets.insert(@table_name, {:legacy_key, "legacy_value"})
|
|
assert {:ok, nil} = Cache.ttl(@table_name, :legacy_key)
|
|
end
|
|
end
|
|
|
|
describe "to_timeout/1 with more units" do
|
|
test "supports :millisecond/:milliseconds keys" do
|
|
assert Cache.to_timeout(millisecond: 500) == 500
|
|
assert Cache.to_timeout(milliseconds: 250) == 250
|
|
end
|
|
|
|
test "ignores invalid TTL values in put/4" do
|
|
# Negative TTL → :infinity under the clause that matches `_ -> :infinity`.
|
|
assert {:ok, true} = Cache.put(@table_name, :inf_key, "val", ttl: -1)
|
|
assert {:ok, "val"} = Cache.get(@table_name, :inf_key)
|
|
assert {:ok, nil} = Cache.ttl(@table_name, :inf_key)
|
|
end
|
|
end
|
|
end
|