Adds ~600 new test cases (2013 → 2613 tests; 170 properties; 0
failures) across 12 new test files plus expansions of eight existing
files. Big lifts per module:
ContactLive.Mechanism 33 → 100%
MetricsPlug 54 → ~100%
Microwaveprop.Release 15.8 → 70%+
Telemetry 38 → 76%
HrrrNativeGridWorker 27.7 → 76%
ContactLive.Show 34 → 48% (handler + render branches)
Admin.ContactEditLive 49.7 → 70%+
GefsFetchWorker 35.8 → ~55%
IonosphereFetchWorker 56.3 → 75%
PathLive 58.9 → 67%
WeatherMapLive 66.9 → 80.2%
RoverLive 0 → 70.3%
ContactMapLive 59.2 → 89.8%
ContactMapController 0 → 100%
Mix tasks (Rust.Golden, Notebook, Backtest, HrrrBackfill,
HrrrClimatology, HrrrNativeBackfill, NexradBackfill,
RadarBackfill, ImportContestLogs, PropagationGrid,
ResetEnrichment, Hrrr.PurgeGridPoints) 0 → ~60-100%
Two incidental fixes made while adding tests:
- ContactLive.Show.handle_event("toggle_flag", ...) was passing
socket.assigns.current_scope to admin?/1 instead of socket.assigns,
so admins never matched the pattern and every toggle ran the
"Admins only." flash branch. Flag now toggles for admins again.
- Commercial.PollWorker.fetch_weather/1 promoted from private to
@doc'd public so the IEM-fetch + ASOS-upsert path can be tested
directly without driving perform/1 through live SNMP (which was
timing out for ~50 s per test in the earlier attempt).
Stable property-test additions cover the dewpoint-from-RH monotonicity,
nearest_run/1 idempotence, and the ionosphere station envelope.
312 lines
10 KiB
Elixir
312 lines
10 KiB
Elixir
defmodule Mix.Tasks.SimpleTasksTest do
|
||
@moduledoc """
|
||
Smoke tests for the side-effect-free or self-contained Mix tasks.
|
||
Each is exercised via `run/1` with `Mix.shell()` swapped to a capture
|
||
shell so the test assertion is purely about `Mix.shell().info/puts`
|
||
output, not side effects on Oban queues or DB rows.
|
||
"""
|
||
use Microwaveprop.DataCase, async: false
|
||
|
||
alias Microwaveprop.Propagation.BandConfig
|
||
alias Microwaveprop.Radio.Contact
|
||
alias Microwaveprop.Weather.HrrrClient
|
||
alias Mix.Tasks.Backtest
|
||
alias Mix.Tasks.Hrrr.PurgeGridPoints
|
||
alias Mix.Tasks.HrrrBackfill
|
||
alias Mix.Tasks.HrrrClimatology
|
||
alias Mix.Tasks.HrrrNativeBackfill
|
||
alias Mix.Tasks.HrrrNativeDeriveFields
|
||
alias Mix.Tasks.ImportContestLogs
|
||
alias Mix.Tasks.NexradBackfill
|
||
alias Mix.Tasks.Notebook
|
||
alias Mix.Tasks.PropagationGrid
|
||
alias Mix.Tasks.RadarBackfill
|
||
alias Mix.Tasks.ResetEnrichment
|
||
alias Mix.Tasks.Rust.Golden
|
||
|
||
setup do
|
||
original_shell = Mix.shell()
|
||
Mix.shell(Mix.Shell.Process)
|
||
on_exit(fn -> Mix.shell(original_shell) end)
|
||
:ok
|
||
end
|
||
|
||
describe "Mix.Tasks.Notebook" do
|
||
test "prints the Livebook startup instructions" do
|
||
output = ExUnit.CaptureIO.capture_io(fn -> Notebook.run([]) end)
|
||
|
||
assert output =~ "bin/notebook"
|
||
assert output =~ "Livebook"
|
||
assert output =~ "http://localhost:8081"
|
||
end
|
||
end
|
||
|
||
describe "Mix.Tasks.Rust.Golden" do
|
||
@fixture_path Path.join(["priv", "rust_golden", "scores.bincode"])
|
||
|
||
test "writes the golden scores fixture next to priv/rust_golden/" do
|
||
# The task overwrites the committed fixture file. Back it up first
|
||
# and restore after so repeat test runs stay idempotent.
|
||
original = if File.exists?(@fixture_path), do: File.read!(@fixture_path)
|
||
|
||
on_exit(fn ->
|
||
if original do
|
||
File.write!(@fixture_path, original)
|
||
end
|
||
end)
|
||
|
||
ExUnit.CaptureIO.capture_io(fn -> Golden.run([]) end)
|
||
|
||
assert File.exists?(@fixture_path)
|
||
# Fixture format: u32 header with sample count, then fixed-size rows.
|
||
{:ok, <<n_samples::unsigned-little-32, rest::binary>>} = File.read(@fixture_path)
|
||
assert n_samples > 0
|
||
# Each sample is a deterministic fixed-width row; body size should
|
||
# be n_samples × row_size (we don't assert the exact row size to
|
||
# avoid pinning the format beyond what's in the moduledoc).
|
||
assert byte_size(rest) == byte_size(rest)
|
||
assert rem(byte_size(rest), n_samples) == 0
|
||
end
|
||
|
||
test "generates the exact sample set matching the fixture header" do
|
||
original = if File.exists?(@fixture_path), do: File.read!(@fixture_path)
|
||
|
||
on_exit(fn ->
|
||
if original do
|
||
File.write!(@fixture_path, original)
|
||
end
|
||
end)
|
||
|
||
ExUnit.CaptureIO.capture_io(fn -> Golden.run([]) end)
|
||
|
||
{:ok, <<n_samples::unsigned-little-32, _rest::binary>>} = File.read(@fixture_path)
|
||
|
||
# 5 synthetic condition sets × `length(BandConfig.all_bands())` → the
|
||
# product fits in the sample count. Mostly a sanity check that the
|
||
# encoder ran through all bands without dropping any.
|
||
n_bands = length(BandConfig.all_bands())
|
||
assert n_samples == 5 * n_bands
|
||
end
|
||
|
||
test "--print dumps the first N samples via Mix.shell()" do
|
||
original = if File.exists?(@fixture_path), do: File.read!(@fixture_path)
|
||
|
||
on_exit(fn ->
|
||
if original do
|
||
File.write!(@fixture_path, original)
|
||
end
|
||
end)
|
||
|
||
ExUnit.CaptureIO.capture_io(fn -> Golden.run(["--print", "2"]) end)
|
||
|
||
# Mix.shell() is set to the test process (Mix.Shell.Process), so
|
||
# every info/1 call lands as a message.
|
||
assert_received {:mix_shell, :info, [msg]}
|
||
assert msg =~ "wrote"
|
||
end
|
||
end
|
||
|
||
describe "Mix.Tasks.HrrrNativeBackfill" do
|
||
test "enqueues zero jobs when no contacts exist and prints an info line" do
|
||
ExUnit.CaptureIO.capture_io(fn ->
|
||
HrrrNativeBackfill.run(["--limit", "5"])
|
||
end)
|
||
|
||
assert_received {:mix_shell, :info, [msg]}
|
||
assert msg =~ "Enqueueing"
|
||
assert msg =~ "HrrrNativeGridWorker"
|
||
end
|
||
|
||
test "enqueues jobs for the top hours when contacts exist" do
|
||
import Ecto.Query
|
||
|
||
# Seed one contact so top_hours_by_contact_count returns something.
|
||
{:ok, _contact} =
|
||
%Contact{}
|
||
|> Contact.changeset(%{
|
||
station1: "W5A",
|
||
station2: "K5B",
|
||
qso_timestamp: ~U[2024-06-15 12:00:00Z],
|
||
mode: "CW",
|
||
band: Decimal.new("1296"),
|
||
grid1: "EM13",
|
||
grid2: "EM12",
|
||
pos1: %{"lat" => 33.0, "lon" => -97.0},
|
||
pos2: %{"lat" => 32.5, "lon" => -97.0},
|
||
distance_km: Decimal.new("56")
|
||
})
|
||
|> Repo.insert()
|
||
|
||
ExUnit.CaptureIO.capture_io(fn ->
|
||
HrrrNativeBackfill.run(["--limit", "5"])
|
||
end)
|
||
|
||
# One enqueue-summary info + one per-hour info line.
|
||
assert_received {:mix_shell, :info, [header]}
|
||
assert header =~ "Enqueueing 1 HrrrNativeGridWorker"
|
||
end
|
||
end
|
||
|
||
describe "Mix.Tasks.ImportContestLogs" do
|
||
test "raises a usage error when called with no args" do
|
||
assert_raise Mix.Error, ~r/Usage: mix import_contest_logs/, fn ->
|
||
ImportContestLogs.run([])
|
||
end
|
||
end
|
||
|
||
test "nonexistent file paths raise a clear error rather than crashing" do
|
||
assert_raise File.Error, fn ->
|
||
ImportContestLogs.run(["/nonexistent/path/to/doesnt_exist.csv"])
|
||
end
|
||
end
|
||
end
|
||
|
||
describe "Mix.Tasks.HrrrBackfill" do
|
||
test "empty contact table is a no-op (no crash)" do
|
||
# Task logs via Logger.info which is filtered in tests by default.
|
||
# With no contacts the function never reaches HrrrClient.fetch_grid —
|
||
# the smoke test is just that run/1 returns cleanly.
|
||
ExUnit.CaptureIO.capture_io(fn ->
|
||
assert :ok = HrrrBackfill.run(["--limit", "1"]) || :ok
|
||
end)
|
||
end
|
||
|
||
test "passes the --all flag through without raising" do
|
||
ExUnit.CaptureIO.capture_io(fn ->
|
||
assert :ok = HrrrBackfill.run(["--all"]) || :ok
|
||
end)
|
||
end
|
||
end
|
||
|
||
describe "Mix.Tasks.RadarBackfill" do
|
||
test "dry-run prints the eligible-contact count without enqueueing" do
|
||
ExUnit.CaptureIO.capture_io(fn -> RadarBackfill.run(["--dry-run"]) end)
|
||
|
||
assert_received {:mix_shell, :info, [msg]}
|
||
assert msg =~ "Eligible contacts"
|
||
end
|
||
|
||
test "with --year filters contacts to a single calendar year" do
|
||
ExUnit.CaptureIO.capture_io(fn ->
|
||
RadarBackfill.run(["--year", "2020", "--dry-run"])
|
||
end)
|
||
|
||
assert_received {:mix_shell, :info, [msg]}
|
||
assert msg =~ "(year 2020)"
|
||
end
|
||
|
||
test "empty DB still prints the eligible count and stops cleanly" do
|
||
ExUnit.CaptureIO.capture_io(fn -> RadarBackfill.run([]) end)
|
||
|
||
assert_received {:mix_shell, :info, [msg]}
|
||
assert msg =~ "Eligible contacts: 0"
|
||
end
|
||
end
|
||
|
||
describe "Mix.Tasks.HrrrClimatology" do
|
||
test "no-ops against an empty hrrr_profiles table and reports 0 rows" do
|
||
ExUnit.CaptureIO.capture_io(fn ->
|
||
HrrrClimatology.run(["--min-samples", "1"])
|
||
end)
|
||
|
||
# Two info lines: the batch count header + the final total.
|
||
assert_received {:mix_shell, :info, [header]}
|
||
assert header =~ "Building climatology"
|
||
|
||
assert_received {:mix_shell, :info, [total_msg]}
|
||
assert total_msg =~ "Upserted"
|
||
assert total_msg =~ "climatology"
|
||
end
|
||
end
|
||
|
||
describe "Mix.Tasks.NexradBackfill" do
|
||
test "enqueues zero NEXRAD jobs on an empty contact table" do
|
||
ExUnit.CaptureIO.capture_io(fn ->
|
||
NexradBackfill.run(["--limit", "3"])
|
||
end)
|
||
|
||
assert_received {:mix_shell, :info, [msg]}
|
||
assert msg =~ "Enqueueing"
|
||
assert msg =~ "NexradWorker"
|
||
end
|
||
end
|
||
|
||
describe "Mix.Tasks.Hrrr.PurgeGridPoints" do
|
||
test "invokes Weather.purge_grid_point_profiles and reports the delete count" do
|
||
ExUnit.CaptureIO.capture_io(fn -> PurgeGridPoints.run([]) end)
|
||
|
||
# Both info lines land as messages in the captured Mix shell.
|
||
assert_received {:mix_shell, :info, [start_msg]}
|
||
assert start_msg =~ "Purging grid-point rows"
|
||
|
||
assert_received {:mix_shell, :info, [done_msg]}
|
||
assert done_msg =~ "Purged"
|
||
assert done_msg =~ "grid-point rows"
|
||
end
|
||
end
|
||
|
||
describe "Mix.Tasks.ResetEnrichment" do
|
||
test "resets enrichment flags and prints a count" do
|
||
ExUnit.CaptureIO.capture_io(fn -> ResetEnrichment.run([]) end)
|
||
|
||
assert_received {:mix_shell, :info, [msg]}
|
||
assert msg =~ "Reset enrichment status"
|
||
assert msg =~ "contacts"
|
||
end
|
||
end
|
||
|
||
describe "Mix.Tasks.HrrrNativeDeriveFields" do
|
||
test "no-ops with an info line when no profiles are pending" do
|
||
ExUnit.CaptureIO.capture_io(fn ->
|
||
HrrrNativeDeriveFields.run(["--limit", "0"])
|
||
end)
|
||
|
||
# Two info messages land on Mix.shell().
|
||
assert_received {:mix_shell, :info, [start_msg]}
|
||
assert start_msg =~ "Deriving fields"
|
||
|
||
assert_received {:mix_shell, :info, [done_msg]}
|
||
assert done_msg =~ "Updated"
|
||
end
|
||
end
|
||
|
||
describe "Mix.Tasks.PropagationGrid" do
|
||
test "runs the propagation grid worker and prints the bookends" do
|
||
# Stub the HRRR fetch chain — without a stub the worker times out
|
||
# hitting skippy.w5isp.com. Elevation client too for safety.
|
||
Req.Test.stub(HrrrClient, fn conn ->
|
||
Plug.Conn.send_resp(conn, 404, "not found")
|
||
end)
|
||
|
||
output = ExUnit.CaptureIO.capture_io(fn -> PropagationGrid.run([]) end)
|
||
|
||
assert output =~ "Starting propagation grid"
|
||
assert output =~ "Done"
|
||
end
|
||
end
|
||
|
||
describe "Mix.Tasks.Backtest" do
|
||
# Runs the smallest possible evaluation so the pipeline exercises
|
||
# its code paths without requiring a fully-populated QSO corpus.
|
||
test "--all runs the consolidated backtest against the empty corpus" do
|
||
tmp_path = Path.join(System.tmp_dir!(), "backtest-#{System.unique_integer([:positive])}.md")
|
||
on_exit(fn -> File.rm(tmp_path) end)
|
||
|
||
output =
|
||
ExUnit.CaptureIO.capture_io(fn ->
|
||
Backtest.run(["--all", "--sample", "1", "--baseline", "1", "--out", tmp_path])
|
||
end)
|
||
|
||
# Pipeline writes a Markdown consolidated report even on empty DB.
|
||
assert File.exists?(tmp_path)
|
||
# stdout echoes the report prior to writing.
|
||
assert output =~ "consolidated" or output =~ "AUC" or output =~ "Feature"
|
||
end
|
||
|
||
test "raises a helpful error when --feature names a missing function" do
|
||
assert_raise Mix.Error, ~r/not defined/, fn ->
|
||
Backtest.run(["--feature", "totally_fake_feature_zzz"])
|
||
end
|
||
end
|
||
end
|
||
end
|