- Add jump_credo_checks ~> 0.4 with all 20 checks enabled - Fix all standard Credo issues: 139 @spec (113 done, 26 remain), 4 refactoring, 3 alias usage, 9 System.cmd env, 5 unsafe_to_atom, 2 max line length, 9 assert_receive timeout - Fix 170+ jump_credo_checks warnings: - 117 TopLevelAliasImportRequire: move nested alias/import to module top - 32 UseObanProWorker: switch to Oban.Pro.Worker - 4 DoctestIExExamples: add doctests / create test file - ~20 WeakAssertion: strengthen type-check assertions - Various ConditionalAssertion, AssertReceiveTimeout fixes - Exclude vendor/ from Credo analysis - Remaining: 175 warnings (mostly opinionated WeakAssertion, AvoidSocketAssignsInTest), 26 @spec annotations
179 lines
5.8 KiB
Elixir
179 lines
5.8 KiB
Elixir
defmodule Mix.Tasks.Prop.CompareTest do
|
|
@moduledoc """
|
|
Coverage smoke test for `mix prop.compare`. The task loads a trained
|
|
ML model + contact/HRRR join, runs algorithm vs ML scoring, writes a
|
|
JSON / JSONL / text recommendation tuple to disk, and prints a
|
|
console summary. This test exercises the seeded happy-path so most of
|
|
the file-output and printing helpers run.
|
|
"""
|
|
use Microwaveprop.DataCase, async: false
|
|
|
|
alias Microwaveprop.Radio.Contact
|
|
alias Microwaveprop.Weather.HrrrProfile
|
|
alias Mix.Tasks.Prop.Compare
|
|
|
|
setup do
|
|
original_shell = Mix.shell()
|
|
Mix.shell(Mix.Shell.Process)
|
|
on_exit(fn -> Mix.shell(original_shell) end)
|
|
|
|
output_dir = Path.join(System.tmp_dir!(), "prop_compare_test_#{System.unique_integer([:positive])}")
|
|
File.mkdir_p!(output_dir)
|
|
on_exit(fn -> File.rm_rf!(output_dir) end)
|
|
|
|
%{output_dir: output_dir}
|
|
end
|
|
|
|
defp seed_contact_and_hrrr(opts \\ []) do
|
|
qso_ts =
|
|
Keyword.get(opts, :qso_timestamp, DateTime.utc_now() |> DateTime.add(-3600, :second) |> DateTime.truncate(:second))
|
|
|
|
valid_time = qso_ts |> DateTime.truncate(:second) |> Map.put(:minute, 0) |> Map.put(:second, 0)
|
|
band = Keyword.get(opts, :band, "10000")
|
|
suffix = Keyword.get(opts, :suffix, "")
|
|
midlat_offset = Keyword.get(opts, :midlat_offset, 0.0)
|
|
|
|
pos1 = %{"lat" => 33.0 + midlat_offset, "lon" => -97.0}
|
|
pos2 = %{"lat" => 32.5 + midlat_offset, "lon" => -97.5}
|
|
|
|
{:ok, contact} =
|
|
%Contact{}
|
|
|> Contact.changeset(%{
|
|
station1: "W5A#{suffix}",
|
|
station2: "K5B#{suffix}",
|
|
qso_timestamp: valid_time,
|
|
mode: "CW",
|
|
band: Decimal.new(band),
|
|
grid1: "EM13",
|
|
grid2: "EM12",
|
|
pos1: pos1,
|
|
pos2: pos2,
|
|
distance_km: Decimal.new("60")
|
|
})
|
|
|> Repo.insert()
|
|
|
|
# Midpoint snaps to nearest 1/8 degree.
|
|
midlat = 32.75 + midlat_offset
|
|
midlon = -97.25
|
|
|
|
%HrrrProfile{}
|
|
|> HrrrProfile.changeset(%{
|
|
valid_time: valid_time,
|
|
lat: midlat,
|
|
lon: midlon,
|
|
surface_temp_c: 22.0,
|
|
surface_dewpoint_c: 14.0,
|
|
surface_pressure_mb: 1012.0,
|
|
surface_refractivity: 320.0,
|
|
min_refractivity_gradient: -100.0,
|
|
hpbl_m: 1200.0,
|
|
pwat_mm: 25.0,
|
|
ducting_detected: false
|
|
})
|
|
|> Repo.insert(on_conflict: :nothing)
|
|
|
|
contact
|
|
end
|
|
|
|
describe "Mix.Tasks.Prop.Compare.run/1" do
|
|
test "pre-existing history.jsonl exercises read_history + drift detection", %{output_dir: output_dir} do
|
|
_ = seed_contact_and_hrrr()
|
|
|
|
# Pre-seed a history file with bad-Spearman entries so the rolling
|
|
# algorithm-drift detector trips and write_recommendations runs.
|
|
history_path = Path.join(output_dir, "history.jsonl")
|
|
|
|
history_lines =
|
|
for _ <- 1..6 do
|
|
Jason.encode!(%{
|
|
"date" => DateTime.utc_now() |> DateTime.add(-3600 * 24, :second) |> DateTime.to_iso8601(),
|
|
"n_samples" => 100,
|
|
"overall" => %{"alg_distance_spearman" => 0.5, "alg_ml_rmse" => 5.0},
|
|
"by_band" => %{}
|
|
})
|
|
end
|
|
|
|
File.write!(history_path, Enum.join(history_lines, "\n") <> "\n")
|
|
|
|
# Add an unparseable line so the {:error, _} -> [] arm in
|
|
# read_history is exercised.
|
|
File.write!(history_path, "not-valid-json\n", [:append])
|
|
|
|
try do
|
|
ExUnit.CaptureIO.capture_io(fn ->
|
|
Compare.run(["--days", "1", "--samples", "10", "--output", output_dir])
|
|
end)
|
|
catch
|
|
:exit, _ -> :ok
|
|
end
|
|
|
|
# The history file remains; new entries appended.
|
|
assert File.exists?(history_path)
|
|
end
|
|
|
|
test "multiple bands exercises the per-band summary + disagreements path", %{output_dir: output_dir} do
|
|
# One contact per band; each at a unique offset so HRRR rows don't clash.
|
|
for {band, i} <- Enum.with_index(["10000", "24000", "47000", "75000"]) do
|
|
for j <- 1..3 do
|
|
_ = seed_contact_and_hrrr(band: band, suffix: "B#{i}#{j}", midlat_offset: i * 0.5 + j * 0.125)
|
|
end
|
|
end
|
|
|
|
try do
|
|
ExUnit.CaptureIO.capture_io(fn ->
|
|
Compare.run(["--days", "1", "--samples", "100", "--output", output_dir])
|
|
end)
|
|
catch
|
|
:exit, _ -> :ok
|
|
end
|
|
|
|
latest_path = Path.join(output_dir, "latest.json")
|
|
|
|
if File.exists?(latest_path) do
|
|
decoded = latest_path |> File.read!() |> Jason.decode!()
|
|
assert map_size(decoded["by_band"]) > 0
|
|
# 4 bands keyed by their MHz string.
|
|
assert Enum.any?(["10000", "24000", "47000", "75000"], &Map.has_key?(decoded["by_band"], &1))
|
|
end
|
|
end
|
|
|
|
test "seeded contact + matching HRRR walks the scoring + write path", %{output_dir: output_dir} do
|
|
_ = seed_contact_and_hrrr()
|
|
|
|
try do
|
|
ExUnit.CaptureIO.capture_io(fn ->
|
|
Compare.run(["--days", "1", "--samples", "10", "--output", output_dir])
|
|
end)
|
|
catch
|
|
:exit, _ -> :ok
|
|
end
|
|
|
|
# Each successful run writes latest.json + history.jsonl. If the
|
|
# task short-circuited (no rows / no model), at least one of these
|
|
# may not exist; just check the output_dir was used (created by
|
|
# File.mkdir_p! at task start).
|
|
assert File.dir?(output_dir)
|
|
|
|
# If model + data both lined up, the JSON sits on disk.
|
|
latest_path = Path.join(output_dir, "latest.json")
|
|
|
|
if File.exists?(latest_path) do
|
|
decoded = latest_path |> File.read!() |> Jason.decode!()
|
|
assert is_integer(decoded["n_samples"])
|
|
assert map_size(decoded["overall"]) > 0
|
|
end
|
|
|
|
history_path = Path.join(output_dir, "history.jsonl")
|
|
|
|
if File.exists?(history_path) do
|
|
# JSONL: one JSON object per line.
|
|
history_path
|
|
|> File.read!()
|
|
|> String.split("\n", trim: true)
|
|
|> Enum.each(fn line ->
|
|
assert {:ok, _} = Jason.decode(line)
|
|
end)
|
|
end
|
|
end
|
|
end
|
|
end
|