aprs.me/test/aprsme/regex_cache_test.exs

88 lines
3.2 KiB
Elixir

defmodule Aprsme.RegexCacheTest do
# async: false — the cache is a singleton GenServer owning a named ETS table
# shared across the whole application. Parallel tests would see each other.
use ExUnit.Case, async: false
alias Aprsme.RegexCache
# Each test gets a unique pattern to avoid cross-test pollution of the
# shared cache (we can't safely clear it without racing other callers).
defp unique_pattern(tag) do
suffix = System.unique_integer([:positive, :monotonic])
"#{tag}_#{suffix}"
end
describe "get_or_compile/1" do
test "compiles and returns a Regex on first call" do
pattern = unique_pattern("hello")
assert {:ok, regex} = RegexCache.get_or_compile(pattern)
assert %Regex{} = regex
assert Regex.match?(regex, pattern)
end
test "returns the same compiled regex on subsequent calls" do
pattern = unique_pattern("same")
assert {:ok, r1} = RegexCache.get_or_compile(pattern)
assert {:ok, r2} = RegexCache.get_or_compile(pattern)
assert r1 == r2
end
test "returns {:error, _} for invalid patterns" do
assert {:error, _reason} = RegexCache.get_or_compile("[")
assert {:error, _reason} = RegexCache.get_or_compile("(unbalanced")
end
test "does not cache failed compilations" do
assert {:error, _} = RegexCache.get_or_compile("[bad")
# Still an error on retry — error path didn't poison the cache with nil.
assert {:error, _} = RegexCache.get_or_compile("[bad")
end
test "distinct patterns produce distinct regexes" do
p1 = unique_pattern("a")
p2 = unique_pattern("b")
assert {:ok, r1} = RegexCache.get_or_compile(p1)
assert {:ok, r2} = RegexCache.get_or_compile(p2)
refute r1 == r2
end
test "compiled regex actually matches as expected" do
pattern = unique_pattern("match") <> "\\d+"
assert {:ok, regex} = RegexCache.get_or_compile(pattern)
base = String.trim_trailing(pattern, "\\d+")
assert Regex.match?(regex, "#{base}123")
refute Regex.match?(regex, "nope")
end
test "caches many different patterns without losing recent hits" do
# Exercise the handle_call path for cache hits + misses across many patterns.
patterns = for i <- 1..50, do: "bulk_pattern_#{System.unique_integer()}_#{i}"
for p <- patterns do
assert {:ok, %Regex{}} = RegexCache.get_or_compile(p)
end
# All patterns should still be retrievable.
for p <- patterns do
assert {:ok, %Regex{}} = RegexCache.get_or_compile(p)
end
end
test "exercises LRU eviction when cache grows past the max-size threshold" do
# Generate enough patterns to push past @max_cache_size (1000).
# Existing cache entries count too, so overshoot by ~150 to be safe.
patterns = for i <- 1..1_200, do: "lru_pattern_#{System.unique_integer()}_#{i}"
for p <- patterns do
assert {:ok, %Regex{}} = RegexCache.get_or_compile(p)
end
# The first few patterns should likely be evicted, but still compile fine
# on re-request (no caching contract violation).
[first | _] = patterns
assert {:ok, %Regex{}} = RegexCache.get_or_compile(first)
end
end
end