test: add /qsos→/contacts redirect + scorer conversion properties
- PageController: cover the legacy `/qsos` → `/contacts` redirect pair
(both the index and the id-preserving per-resource redirect), which
had no coverage since the route rename.
- Scorer property tests:
- f_to_c / c_to_f are inverses up to float epsilon, both propagate
nil, and the C→F slope is exactly 5/9.
- wind_speed_kts is non-negative, propagates nil on either axis,
and matches sqrt(u²+v²)·1.94384 (the 5-digit kts factor the
scorer uses).
- precip_to_rate_mmhr is non-negative, 0 for nil/zero/negative.
- dbz_to_rain_rate_mmhr is 0 below 5 dBZ (and for nil), and
monotonically non-decreasing above — guards against accidental
inversion of the Marshall-Palmer lookup.
Suite: 2,382 tests + 155 properties (was 2,380 + 148); coverage
71.08% → 71.12%; credo strict clean.
This commit is contained in:
parent
b3c035793a
commit
e7fb34eb6f
2 changed files with 101 additions and 0 deletions
|
|
@ -12,6 +12,92 @@ defmodule Microwaveprop.Propagation.ScorerPropertyTest do
|
|||
alias Microwaveprop.Propagation.BandConfig
|
||||
alias Microwaveprop.Propagation.Scorer
|
||||
|
||||
describe "temperature conversions" do
|
||||
property "f_to_c and c_to_f are inverses up to float epsilon" do
|
||||
check all(c <- float(min: -100.0, max: 150.0)) do
|
||||
round_tripped = c |> Scorer.c_to_f() |> Scorer.f_to_c()
|
||||
assert_in_delta round_tripped, c, 1.0e-9
|
||||
end
|
||||
end
|
||||
|
||||
property "f_to_c and c_to_f propagate nil (scorer nil-pass contract)" do
|
||||
assert Scorer.f_to_c(nil) == nil
|
||||
assert Scorer.c_to_f(nil) == nil
|
||||
end
|
||||
|
||||
property "f_to_c is linear (a F increase of ΔF maps to (5/9)·ΔF C)" do
|
||||
check all(
|
||||
base <- float(min: -50.0, max: 120.0),
|
||||
delta <- float(min: -30.0, max: 30.0)
|
||||
) do
|
||||
d_c = Scorer.f_to_c(base + delta) - Scorer.f_to_c(base)
|
||||
assert_in_delta d_c, delta * 5.0 / 9.0, 1.0e-9
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "wind_speed_kts/2" do
|
||||
property "is non-negative and propagates nil" do
|
||||
assert Scorer.wind_speed_kts(nil, 1.0) == nil
|
||||
assert Scorer.wind_speed_kts(1.0, nil) == nil
|
||||
|
||||
check all(
|
||||
u <- float(min: -30.0, max: 30.0),
|
||||
v <- float(min: -30.0, max: 30.0)
|
||||
) do
|
||||
kts = Scorer.wind_speed_kts(u, v)
|
||||
assert is_number(kts)
|
||||
assert kts >= 0.0
|
||||
end
|
||||
end
|
||||
|
||||
property "magnitude equals sqrt(u²+v²) × m/s→kts conversion" do
|
||||
# Scorer uses the 5-digit 1.94384 factor (not the full-precision
|
||||
# 1.9438444924...). That's ~0.001% off, which tops out at a few
|
||||
# millimetres per hour on a 60 kt wind — too small to matter for
|
||||
# propagation scoring.
|
||||
check all(
|
||||
u <- float(min: -20.0, max: 20.0),
|
||||
v <- float(min: -20.0, max: 20.0)
|
||||
) do
|
||||
speed_ms = :math.sqrt(u * u + v * v)
|
||||
expected = speed_ms * 1.94384
|
||||
assert_in_delta Scorer.wind_speed_kts(u, v), expected, 1.0e-6
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "precip_to_rate_mmhr/1 and dbz_to_rain_rate_mmhr/1" do
|
||||
property "precip_to_rate_mmhr is non-negative and 0-valued for nil/0" do
|
||||
assert Scorer.precip_to_rate_mmhr(nil) == 0.0
|
||||
assert Scorer.precip_to_rate_mmhr(0.0) == 0.0
|
||||
assert Scorer.precip_to_rate_mmhr(-1.0) == 0.0
|
||||
|
||||
check all(mm <- float(min: 0.01, max: 200.0)) do
|
||||
rate = Scorer.precip_to_rate_mmhr(mm)
|
||||
assert rate >= 0.0
|
||||
assert rate == mm
|
||||
end
|
||||
end
|
||||
|
||||
property "dbz_to_rain_rate_mmhr is 0 below 5 dBZ and monotonic non-decreasing above" do
|
||||
check all(dbz <- float(min: -30.0, max: 4.99)) do
|
||||
assert Scorer.dbz_to_rain_rate_mmhr(dbz) == 0.0
|
||||
end
|
||||
|
||||
assert Scorer.dbz_to_rain_rate_mmhr(nil) == 0.0
|
||||
|
||||
check all(
|
||||
a <- float(min: 5.0, max: 60.0),
|
||||
delta <- float(min: 0.0, max: 30.0)
|
||||
) do
|
||||
b = a + delta
|
||||
|
||||
assert Scorer.dbz_to_rain_rate_mmhr(a) <= Scorer.dbz_to_rain_rate_mmhr(b) + 1.0e-9
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# ── Generators ──────────────────────────────────────────────────
|
||||
|
||||
# All configured bands — harmful (24+ GHz) and beneficial (10 GHz and
|
||||
|
|
|
|||
|
|
@ -6,6 +6,21 @@ defmodule MicrowavepropWeb.PageControllerTest do
|
|||
assert redirected_to(conn) == "/map"
|
||||
end
|
||||
|
||||
describe "legacy /qsos redirects" do
|
||||
# The app was renamed `qsos` → `contacts` in the 2025 refactor; the
|
||||
# old routes stay around so bookmarked links don't 404.
|
||||
test "GET /qsos permanent-redirects to /contacts", %{conn: conn} do
|
||||
conn = get(conn, "/qsos")
|
||||
assert redirected_to(conn) == "/contacts"
|
||||
end
|
||||
|
||||
test "GET /qsos/:id permanent-redirects to /contacts/:id preserving the id", %{conn: conn} do
|
||||
id = "7d3cf5c8-6fd4-44b5-9b8e-84d9ef2c1b10"
|
||||
conn = get(conn, "/qsos/#{id}")
|
||||
assert redirected_to(conn) == "/contacts/#{id}"
|
||||
end
|
||||
end
|
||||
|
||||
# RFC 8288 Link headers for agent discovery. These advertise resources
|
||||
# that actually exist in this app to crawlers/agents that look at
|
||||
# response headers before parsing HTML.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue