- Add timeout: 100 to SNMP tests to avoid 5s default timeout on unreachable IPs - Reduce retry delays in error handler tests from 50-100ms to 1ms - Fix Splynx rate limit test to stub auth immediately without extra calls - Use localhost instead of device.local to avoid DNS timeout - Use temp empty dir for ImportProfiles test instead of scanning 788 real files Results: - Top 10 slowest: 45.6s → 14.6s (68% reduction) - Overall suite: ~80s → 39.9s (50% faster) - Specific improvements: * Splynx sync: 7010ms → 134ms (98% faster) * ImportProfiles: 5502ms → 33ms (99% faster) * get_bulk!: 5043ms → 125ms (97% faster) * Walk hostname: 5005ms → 119ms (97% faster) * Error retries: 3103ms → 6ms (99% faster)
323 lines
9.8 KiB
Elixir
323 lines
9.8 KiB
Elixir
defmodule SnmpKit.SnmpLib.ErrorHandlerTest do
|
|
use ExUnit.Case, async: true
|
|
|
|
alias SnmpKit.SnmpLib.ErrorHandler
|
|
|
|
describe "with_retry/2" do
|
|
test "succeeds on first attempt" do
|
|
fun = fn -> {:ok, :success} end
|
|
|
|
assert {:ok, :success} = ErrorHandler.with_retry(fun)
|
|
end
|
|
|
|
test "retries transient errors" do
|
|
# Simulate transient error then success
|
|
agent = Agent.start_link(fn -> 0 end)
|
|
{:ok, pid} = agent
|
|
|
|
fun = fn ->
|
|
count = Agent.get_and_update(pid, fn c -> {c, c + 1} end)
|
|
|
|
if count < 2 do
|
|
{:error, :timeout}
|
|
else
|
|
{:ok, :success_after_retry}
|
|
end
|
|
end
|
|
|
|
assert {:ok, :success_after_retry} = ErrorHandler.with_retry(fun, max_attempts: 3, base_delay: 1)
|
|
|
|
Agent.stop(pid)
|
|
end
|
|
|
|
test "fails after max retries" do
|
|
fun = fn -> {:error, :timeout} end
|
|
|
|
assert {:error, {:max_retries_exceeded, :timeout}} =
|
|
ErrorHandler.with_retry(fun, max_attempts: 3, base_delay: 1)
|
|
end
|
|
|
|
test "does not retry permanent errors" do
|
|
fun = fn -> {:error, :authentication_failed} end
|
|
|
|
# Custom retry condition that returns false for permanent errors
|
|
retry_condition = fn
|
|
:authentication_failed -> false
|
|
_ -> true
|
|
end
|
|
|
|
assert {:error, :authentication_failed} =
|
|
ErrorHandler.with_retry(fun,
|
|
max_attempts: 3,
|
|
retry_condition: retry_condition
|
|
)
|
|
end
|
|
|
|
test "handles exceptions with retry" do
|
|
agent = Agent.start_link(fn -> 0 end)
|
|
{:ok, pid} = agent
|
|
|
|
fun = fn ->
|
|
count = Agent.get_and_update(pid, fn c -> {c, c + 1} end)
|
|
|
|
if count < 2 do
|
|
raise "temporary failure"
|
|
else
|
|
{:ok, :recovered}
|
|
end
|
|
end
|
|
|
|
assert {:ok, :recovered} = ErrorHandler.with_retry(fun, max_attempts: 3, base_delay: 10)
|
|
|
|
Agent.stop(pid)
|
|
end
|
|
|
|
test "respects custom base delay" do
|
|
fun = fn -> {:error, :timeout} end
|
|
|
|
start_time = System.monotonic_time(:millisecond)
|
|
|
|
ErrorHandler.with_retry(fun,
|
|
max_attempts: 2,
|
|
base_delay: 100,
|
|
strategy: :fixed,
|
|
jitter_factor: 0
|
|
)
|
|
|
|
end_time = System.monotonic_time(:millisecond)
|
|
|
|
# Should have at least one 100ms delay (allow for some timing variance)
|
|
assert end_time - start_time >= 90
|
|
end
|
|
|
|
test "uses exponential backoff strategy" do
|
|
attempts = Agent.start_link(fn -> [] end)
|
|
{:ok, pid} = attempts
|
|
|
|
fun = fn ->
|
|
Agent.update(pid, fn list -> [System.monotonic_time(:millisecond) | list] end)
|
|
{:error, :timeout}
|
|
end
|
|
|
|
ErrorHandler.with_retry(fun,
|
|
max_attempts: 3,
|
|
base_delay: 50,
|
|
strategy: :exponential,
|
|
jitter_factor: 0
|
|
)
|
|
|
|
timestamps = Agent.get(pid, fn list -> Enum.reverse(list) end)
|
|
|
|
# Verify exponential backoff pattern (roughly)
|
|
assert length(timestamps) == 3
|
|
Agent.stop(pid)
|
|
end
|
|
|
|
test "uses linear backoff strategy" do
|
|
fun = fn -> {:error, :timeout} end
|
|
|
|
result =
|
|
ErrorHandler.with_retry(fun,
|
|
max_attempts: 3,
|
|
base_delay: 10,
|
|
strategy: :linear
|
|
)
|
|
|
|
assert {:error, {:max_retries_exceeded, :timeout}} = result
|
|
end
|
|
end
|
|
|
|
describe "classify_error/1" do
|
|
test "classifies transient network errors" do
|
|
assert :transient = ErrorHandler.classify_error(:timeout)
|
|
assert :transient = ErrorHandler.classify_error(:nxdomain)
|
|
assert :transient = ErrorHandler.classify_error(:network_unreachable)
|
|
assert :transient = ErrorHandler.classify_error(:connection_refused)
|
|
end
|
|
|
|
test "classifies transient device errors" do
|
|
assert :transient = ErrorHandler.classify_error(:device_busy)
|
|
assert :transient = ErrorHandler.classify_error(:too_big)
|
|
assert :transient = ErrorHandler.classify_error(:resource_unavailable)
|
|
end
|
|
|
|
test "classifies permanent configuration errors" do
|
|
assert :permanent = ErrorHandler.classify_error(:authentication_failed)
|
|
assert :permanent = ErrorHandler.classify_error(:community_mismatch)
|
|
assert :permanent = ErrorHandler.classify_error(:unsupported_version)
|
|
assert :permanent = ErrorHandler.classify_error(:no_such_name)
|
|
assert :permanent = ErrorHandler.classify_error(:bad_value)
|
|
assert :permanent = ErrorHandler.classify_error(:read_only)
|
|
end
|
|
|
|
test "classifies degraded performance errors" do
|
|
assert :degraded = ErrorHandler.classify_error(:slow_response)
|
|
assert :degraded = ErrorHandler.classify_error(:partial_failure)
|
|
assert :degraded = ErrorHandler.classify_error(:high_error_rate)
|
|
end
|
|
|
|
test "classifies unknown errors" do
|
|
assert :unknown = ErrorHandler.classify_error(:unknown_error)
|
|
assert :unknown = ErrorHandler.classify_error(:something_weird)
|
|
end
|
|
|
|
test "classifies network errors" do
|
|
assert :transient = ErrorHandler.classify_error({:network_error, "connection reset"})
|
|
end
|
|
end
|
|
|
|
describe "circuit breaker" do
|
|
test "starts circuit breaker successfully" do
|
|
assert {:ok, pid} = ErrorHandler.start_circuit_breaker("test-device-1")
|
|
assert Process.alive?(pid)
|
|
GenServer.stop(pid)
|
|
end
|
|
|
|
test "executes function when circuit is closed" do
|
|
{:ok, breaker} = ErrorHandler.start_circuit_breaker("test-device-2")
|
|
|
|
fun = fn -> {:ok, :result} end
|
|
|
|
assert {:ok, :result} = ErrorHandler.call_through_breaker(breaker, fun)
|
|
|
|
GenServer.stop(breaker)
|
|
end
|
|
|
|
test "opens circuit after failure threshold" do
|
|
{:ok, breaker} =
|
|
ErrorHandler.start_circuit_breaker("test-device-3", failure_threshold: 2)
|
|
|
|
failing_fun = fn -> {:error, :device_down} end
|
|
|
|
# First failure
|
|
assert {:error, :device_down} = ErrorHandler.call_through_breaker(breaker, failing_fun)
|
|
|
|
# Second failure - should open circuit
|
|
assert {:error, :device_down} = ErrorHandler.call_through_breaker(breaker, failing_fun)
|
|
|
|
# Circuit should be open now
|
|
assert {:error, :circuit_open} = ErrorHandler.call_through_breaker(breaker, failing_fun)
|
|
|
|
GenServer.stop(breaker)
|
|
end
|
|
|
|
test "transitions from closed to half-open after recovery timeout" do
|
|
{:ok, breaker} =
|
|
ErrorHandler.start_circuit_breaker("test-device-4",
|
|
failure_threshold: 1,
|
|
recovery_timeout: 100
|
|
)
|
|
|
|
failing_fun = fn -> {:error, :timeout} end
|
|
|
|
# Trigger failure to open circuit
|
|
assert {:error, :timeout} = ErrorHandler.call_through_breaker(breaker, failing_fun)
|
|
|
|
# Circuit should be open
|
|
assert {:error, :circuit_open} = ErrorHandler.call_through_breaker(breaker, failing_fun)
|
|
|
|
# Wait for recovery timeout
|
|
Process.sleep(150)
|
|
|
|
# Should allow limited calls in half-open state
|
|
success_fun = fn -> {:ok, :recovered} end
|
|
assert {:ok, :recovered} = ErrorHandler.call_through_breaker(breaker, success_fun)
|
|
|
|
GenServer.stop(breaker)
|
|
end
|
|
|
|
test "resets circuit breaker" do
|
|
{:ok, breaker} = ErrorHandler.start_circuit_breaker("test-device-5")
|
|
|
|
# Get initial state
|
|
state = GenServer.call(breaker, :get_state)
|
|
assert state == :closed
|
|
|
|
# Reset
|
|
assert :ok = GenServer.call(breaker, :reset)
|
|
|
|
# Verify still closed
|
|
state = GenServer.call(breaker, :get_state)
|
|
assert state == :closed
|
|
|
|
GenServer.stop(breaker)
|
|
end
|
|
end
|
|
|
|
describe "adaptive_timeout/2" do
|
|
test "returns base timeout for unknown device" do
|
|
timeout = ErrorHandler.adaptive_timeout("invalid.device", base_timeout: 5000)
|
|
assert timeout == 5000
|
|
end
|
|
|
|
test "returns base timeout for nil device" do
|
|
timeout = ErrorHandler.adaptive_timeout(nil, base_timeout: 5000)
|
|
assert timeout == 5000
|
|
end
|
|
|
|
test "returns base timeout for empty device" do
|
|
timeout = ErrorHandler.adaptive_timeout("", base_timeout: 5000)
|
|
assert timeout == 5000
|
|
end
|
|
|
|
test "calculates adaptive timeout for known device" do
|
|
timeout = ErrorHandler.adaptive_timeout("192.168.1.1", base_timeout: 1000, max_timeout: 60_000)
|
|
|
|
# Should be at least base timeout
|
|
assert timeout >= 1000
|
|
# Should not exceed max timeout
|
|
assert timeout <= 60_000
|
|
end
|
|
|
|
test "respects custom safety factor" do
|
|
timeout1 = ErrorHandler.adaptive_timeout("192.168.1.1", base_timeout: 1000, safety_factor: 1.0)
|
|
timeout2 = ErrorHandler.adaptive_timeout("192.168.1.1", base_timeout: 1000, safety_factor: 3.0)
|
|
|
|
# Higher safety factor should give higher timeout (or at minimum be >= base)
|
|
assert timeout2 >= timeout1
|
|
end
|
|
end
|
|
|
|
describe "get_device_stats/1" do
|
|
test "returns placeholder stats for valid device" do
|
|
assert {:ok, stats} = ErrorHandler.get_device_stats("192.168.1.1")
|
|
assert stats.device_id == "192.168.1.1"
|
|
assert stats.success_count == 100
|
|
assert stats.failure_count == 5
|
|
assert stats.circuit_state == :closed
|
|
assert stats.quarantine_until == nil
|
|
end
|
|
|
|
test "returns error for nil device" do
|
|
assert {:error, :not_found} = ErrorHandler.get_device_stats(nil)
|
|
end
|
|
|
|
test "returns error for empty device" do
|
|
assert {:error, :not_found} = ErrorHandler.get_device_stats("")
|
|
end
|
|
|
|
test "returns error for invalid device" do
|
|
assert {:error, :not_found} = ErrorHandler.get_device_stats("invalid.device")
|
|
end
|
|
end
|
|
|
|
describe "quarantine_device/2" do
|
|
test "quarantines device successfully" do
|
|
assert :ok = ErrorHandler.quarantine_device("192.168.1.1", 300_000)
|
|
end
|
|
end
|
|
|
|
describe "quarantined?/1" do
|
|
test "returns false for valid device" do
|
|
refute ErrorHandler.quarantined?("192.168.1.1")
|
|
end
|
|
|
|
test "returns false for unknown device" do
|
|
refute ErrorHandler.quarantined?("invalid.device")
|
|
end
|
|
|
|
test "returns false for nil device" do
|
|
refute ErrorHandler.quarantined?(nil)
|
|
end
|
|
end
|
|
end
|