aprs.me/test/aprsme/performance/insert_optimizer_test.exs
Graham McIntire c5a82b77c1
refactor: pattern-match over conditionals in Navigation and InsertOptimizer
Swaps `if`/`cond` branches for multi-clause function heads or head-pattern
matching, making the control flow visible in the function signatures:

- Navigation.determine_map_location: resolve_location/4 now dispatches on
  the shape of the geolocation input and whether URL params are explicit.
- Navigation.handle_callsign_tracking: per-case clauses for empty callsign
  and explicit-URL-params short-circuits, plus a final clause that does the
  DB lookup and rescues once.
- Navigation.handle_callsign_search: dispatch_callsign_search/3 separates
  the valid/invalid callsign branches.
- InsertOptimizer.optimize_based_on_performance: pattern-match on
  [_, _, _ | _] to require ≥3 samples without calling length/1.
- InsertOptimizer.calculate_insert_options and throughput helpers split
  into guarded clauses instead of inline `if`.
- maybe_emit_optimization uses same-var pattern matching to skip the
  telemetry hit when nothing changed.

Adds unit tests covering every branch of both modules plus SignalHandler,
LocaleHook, and the InsertOptimizer's :noproc fallback path. Coverage
64.69% → 65.72%.
2026-04-23 13:38:24 -05:00

153 lines
5.5 KiB
Elixir

defmodule Aprsme.Performance.InsertOptimizerTest do
use ExUnit.Case, async: true
alias Aprsme.Performance.InsertOptimizer
defp fresh_state do
%{
current_batch_size: 200,
insert_options: [returning: false, on_conflict: :nothing, timeout: 15_000],
performance_history: [],
last_optimization: System.monotonic_time(:millisecond)
}
end
defp metric(throughput, duration_ms) do
%{
batch_size: 200,
duration_ms: duration_ms,
success_count: 100,
throughput: throughput,
timestamp: System.monotonic_time(:millisecond)
}
end
describe "GenServer callbacks" do
test "handle_call :get_batch_size returns current batch size" do
state = fresh_state()
assert {:reply, 200, ^state} =
InsertOptimizer.handle_call(:get_batch_size, self(), state)
end
test "handle_call :get_insert_options returns insert_options" do
state = fresh_state()
opts = state.insert_options
assert {:reply, ^opts, ^state} = InsertOptimizer.handle_call(:get_insert_options, self(), state)
end
test "handle_cast records a new metric in performance_history" do
state = fresh_state()
{:noreply, new_state} =
InsertOptimizer.handle_cast({:record_metrics, 200, 1_000, 100}, state)
assert [entry | _] = new_state.performance_history
assert entry.batch_size == 200
assert entry.duration_ms == 1_000
assert entry.success_count == 100
# Throughput = 100 / (1000/1000) = 100 pps
assert entry.throughput == 100.0
end
test "handle_cast with zero duration avoids div-by-0, throughput is 0" do
state = fresh_state()
{:noreply, new_state} =
InsertOptimizer.handle_cast({:record_metrics, 100, 0, 50}, state)
assert [entry | _] = new_state.performance_history
assert entry.throughput == 0
end
test "performance history is capped at 20 entries" do
state = fresh_state()
final =
Enum.reduce(1..25, state, fn _, s ->
{:noreply, s2} = InsertOptimizer.handle_cast({:record_metrics, 200, 1000, 100}, s)
s2
end)
assert length(final.performance_history) == 20
end
end
describe "handle_info :optimize_settings" do
test "with fewer than 3 metrics, state is unchanged except for re-scheduled timer" do
state = %{fresh_state() | performance_history: [metric(100, 1000), metric(90, 1100)]}
{:noreply, new_state} = InsertOptimizer.handle_info(:optimize_settings, state)
# Batch size shouldn't have moved.
assert new_state.current_batch_size == state.current_batch_size
assert new_state.performance_history == state.performance_history
end
test "grows batch size when throughput is high and duration low" do
# With avg_throughput > 200 and avg_duration < 2000, batch size should scale up
# from 200 → min(@max, round(200 * 1.2)) = 240.
history =
Enum.map(1..5, fn _ -> metric(300, 1000) end)
state = %{fresh_state() | performance_history: history}
{:noreply, new_state} = InsertOptimizer.handle_info(:optimize_settings, state)
assert new_state.current_batch_size == 240
# Fast INSERTs keep default options.
assert Keyword.get(new_state.insert_options, :timeout) == 15_000
end
test "shrinks batch size when throughput is low and duration high" do
# avg_throughput < 50 and avg_duration > 5000 → batch_size drops by 20%.
history = Enum.map(1..5, fn _ -> metric(10, 6000) end)
state = %{fresh_state() | performance_history: history}
{:noreply, new_state} = InsertOptimizer.handle_info(:optimize_settings, state)
assert new_state.current_batch_size == 160
# Slow INSERTs bump timeout.
assert Keyword.get(new_state.insert_options, :timeout) == 30_000
end
test "keeps batch size when performance is middling" do
# 100 pps at 3000ms falls in neither bucket → no change to batch size.
history = Enum.map(1..5, fn _ -> metric(100, 3000) end)
state = %{fresh_state() | performance_history: history}
{:noreply, new_state} = InsertOptimizer.handle_info(:optimize_settings, state)
assert new_state.current_batch_size == 200
end
test "never drops below the minimum batch size" do
state = %{
fresh_state()
| current_batch_size: 110,
performance_history: Enum.map(1..5, fn _ -> metric(5, 10_000) end)
}
{:noreply, new_state} = InsertOptimizer.handle_info(:optimize_settings, state)
assert new_state.current_batch_size == 100
end
test "never exceeds the maximum batch size" do
state = %{
fresh_state()
| current_batch_size: 750,
performance_history: Enum.map(1..5, fn _ -> metric(500, 500) end)
}
{:noreply, new_state} = InsertOptimizer.handle_info(:optimize_settings, state)
assert new_state.current_batch_size == 800
end
end
describe "client API when GenServer isn't running" do
test "get_optimal_batch_size falls back to base size on :noproc" do
# The supervised instance isn't started in the app tree, so this should
# hit the catch clause and return the base batch size (200).
assert InsertOptimizer.get_optimal_batch_size() == 200
end
test "get_insert_options falls back to default options on :noproc" do
opts = InsertOptimizer.get_insert_options()
assert Keyword.get(opts, :returning) == false
assert Keyword.get(opts, :on_conflict) == :nothing
assert Keyword.get(opts, :timeout) == 15_000
end
end
end