test: cover propagation/freshness_monitor
10 characterization tests: fresh vs stale thresholds (>120 min), boundary behavior, empty scores dir, supervised start, non-dedup of repeated stale ticks, :ignore when disabled via config.
This commit is contained in:
parent
a6a670aee7
commit
58e6fcff0d
1 changed files with 164 additions and 0 deletions
164
test/microwaveprop/propagation/freshness_monitor_test.exs
Normal file
164
test/microwaveprop/propagation/freshness_monitor_test.exs
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
defmodule Microwaveprop.Propagation.FreshnessMonitorTest do
|
||||
@moduledoc """
|
||||
Characterization tests for `FreshnessMonitor`.
|
||||
|
||||
The monitor is a simple GenServer that polls propagation score
|
||||
freshness on a 5-minute timer. On each tick it reads
|
||||
`Propagation.latest_valid_time/0` (which reads `ScoresFile`
|
||||
on disk) and enqueues a `PropagationGridWorker` job when either
|
||||
no scores exist yet or the newest score file is older than
|
||||
120 minutes.
|
||||
|
||||
These tests drive the tick path synchronously by invoking
|
||||
`handle_info(:check, state)` from the test process inside
|
||||
`Oban.Testing.with_testing_mode(:manual, ...)` — that keeps
|
||||
`Oban.insert` from executing the job inline, letting us assert
|
||||
on enqueued jobs via `all_enqueued/1`. One smoke test exercises
|
||||
the supervised startup path with a fresh ScoresFile so no enqueue
|
||||
is attempted.
|
||||
|
||||
The interval (`@check_interval = 5 min`) and staleness threshold
|
||||
(`@stale_threshold_minutes = 120`) are compile-time module
|
||||
attributes with no runtime override, so we can't shrink the
|
||||
interval — we exercise the tick directly instead.
|
||||
"""
|
||||
|
||||
use Microwaveprop.DataCase, async: false
|
||||
use Oban.Testing, repo: Microwaveprop.Repo
|
||||
|
||||
alias Microwaveprop.Propagation.FreshnessMonitor
|
||||
alias Microwaveprop.Propagation.ScoresFile
|
||||
alias Microwaveprop.Workers.PropagationGridWorker
|
||||
|
||||
@band_mhz 10_000
|
||||
@sample_scores [%{lat: 25.0, lon: -125.0, score: 50}]
|
||||
|
||||
defp minutes_ago(n) do
|
||||
DateTime.utc_now()
|
||||
|> DateTime.add(-n, :minute)
|
||||
|> DateTime.truncate(:second)
|
||||
end
|
||||
|
||||
describe "handle_info(:check, state) — characterization of tick behavior" do
|
||||
test "fresh grid: does not enqueue a PropagationGridWorker job" do
|
||||
ScoresFile.write!(@band_mhz, minutes_ago(15), @sample_scores)
|
||||
|
||||
Oban.Testing.with_testing_mode(:manual, fn ->
|
||||
assert {:noreply, %{}} = FreshnessMonitor.handle_info(:check, %{})
|
||||
assert [] = all_enqueued(worker: PropagationGridWorker)
|
||||
end)
|
||||
end
|
||||
|
||||
test "stale grid (>120m old): enqueues a PropagationGridWorker job" do
|
||||
ScoresFile.write!(@band_mhz, minutes_ago(180), @sample_scores)
|
||||
|
||||
Oban.Testing.with_testing_mode(:manual, fn ->
|
||||
assert {:noreply, %{}} = FreshnessMonitor.handle_info(:check, %{})
|
||||
|
||||
[job] = all_enqueued(worker: PropagationGridWorker)
|
||||
assert job.args == %{}
|
||||
assert job.queue == "propagation"
|
||||
end)
|
||||
end
|
||||
|
||||
test "just past threshold (121m old): enqueues" do
|
||||
ScoresFile.write!(@band_mhz, minutes_ago(121), @sample_scores)
|
||||
|
||||
Oban.Testing.with_testing_mode(:manual, fn ->
|
||||
assert {:noreply, %{}} = FreshnessMonitor.handle_info(:check, %{})
|
||||
assert [_job] = all_enqueued(worker: PropagationGridWorker)
|
||||
end)
|
||||
end
|
||||
|
||||
test "exactly at threshold (120m old): does NOT enqueue (strict `>` comparison)" do
|
||||
ScoresFile.write!(@band_mhz, minutes_ago(120), @sample_scores)
|
||||
|
||||
Oban.Testing.with_testing_mode(:manual, fn ->
|
||||
assert {:noreply, %{}} = FreshnessMonitor.handle_info(:check, %{})
|
||||
assert [] = all_enqueued(worker: PropagationGridWorker)
|
||||
end)
|
||||
end
|
||||
|
||||
test "empty grid (no score files): enqueues a PropagationGridWorker job" do
|
||||
# DataCase.reset_score_files/0 wipes the scores dir, so nothing exists.
|
||||
Oban.Testing.with_testing_mode(:manual, fn ->
|
||||
assert {:noreply, %{}} = FreshnessMonitor.handle_info(:check, %{})
|
||||
|
||||
[job] = all_enqueued(worker: PropagationGridWorker)
|
||||
assert job.args == %{}
|
||||
end)
|
||||
end
|
||||
|
||||
test "returns unchanged state map from the tick" do
|
||||
Oban.Testing.with_testing_mode(:manual, fn ->
|
||||
assert {:noreply, %{foo: :bar}} = FreshnessMonitor.handle_info(:check, %{foo: :bar})
|
||||
end)
|
||||
end
|
||||
|
||||
test "repeated ticks on a persistently stale grid each enqueue another job" do
|
||||
# Characterization note: the module comment claims an Oban unique
|
||||
# constraint on PropagationGridWorker suppresses duplicates, but
|
||||
# the worker currently declares no `unique:` option. Each tick
|
||||
# therefore inserts a fresh job.
|
||||
ScoresFile.write!(@band_mhz, minutes_ago(180), @sample_scores)
|
||||
|
||||
Oban.Testing.with_testing_mode(:manual, fn ->
|
||||
FreshnessMonitor.handle_info(:check, %{})
|
||||
FreshnessMonitor.handle_info(:check, %{})
|
||||
FreshnessMonitor.handle_info(:check, %{})
|
||||
|
||||
assert 3 == length(all_enqueued(worker: PropagationGridWorker))
|
||||
end)
|
||||
end
|
||||
|
||||
test "tick does not synchronously deliver the next :check message (it is scheduled with a 5-minute delay)" do
|
||||
# Characterizes the `Process.send_after(self(), :check, @check_interval)`
|
||||
# call at the end of handle_info/2: the follow-up :check is
|
||||
# scheduled, not sent immediately, so the test mailbox stays
|
||||
# empty.
|
||||
ScoresFile.write!(@band_mhz, minutes_ago(15), @sample_scores)
|
||||
|
||||
Oban.Testing.with_testing_mode(:manual, fn ->
|
||||
FreshnessMonitor.handle_info(:check, %{})
|
||||
refute_received :check
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
describe "start_link/1 — supervision lifecycle" do
|
||||
test "does not start when start_freshness_monitor config is false (test default)" do
|
||||
# In test.exs the flag is false, so start_link returns :ignore
|
||||
# and no process is registered.
|
||||
assert :ignore = FreshnessMonitor.start_link([])
|
||||
assert Process.whereis(FreshnessMonitor) == nil
|
||||
end
|
||||
|
||||
test "supervised start: boots cleanly with a fresh ScoresFile and no enqueue occurs" do
|
||||
ScoresFile.write!(@band_mhz, minutes_ago(15), @sample_scores)
|
||||
|
||||
# Enable the start flag for this test only.
|
||||
original = Application.get_env(:microwaveprop, :start_freshness_monitor)
|
||||
Application.put_env(:microwaveprop, :start_freshness_monitor, true)
|
||||
|
||||
on_exit(fn ->
|
||||
if is_nil(original) do
|
||||
Application.delete_env(:microwaveprop, :start_freshness_monitor)
|
||||
else
|
||||
Application.put_env(:microwaveprop, :start_freshness_monitor, original)
|
||||
end
|
||||
end)
|
||||
|
||||
pid = start_supervised!(FreshnessMonitor)
|
||||
assert is_pid(pid)
|
||||
assert Process.alive?(pid)
|
||||
assert Process.whereis(FreshnessMonitor) == pid
|
||||
|
||||
# Force the init-time :check to be processed by issuing a
|
||||
# synchronous call; GenServer.call flushes the mailbox up to
|
||||
# this point. FreshnessMonitor doesn't implement handle_call,
|
||||
# so use :sys.get_state instead — it waits for all pending
|
||||
# messages to be handled.
|
||||
assert %{} == :sys.get_state(pid)
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Add table
Reference in a new issue