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.BandConfig
|
||||||
alias Microwaveprop.Propagation.Scorer
|
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 ──────────────────────────────────────────────────
|
# ── Generators ──────────────────────────────────────────────────
|
||||||
|
|
||||||
# All configured bands — harmful (24+ GHz) and beneficial (10 GHz and
|
# All configured bands — harmful (24+ GHz) and beneficial (10 GHz and
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,21 @@ defmodule MicrowavepropWeb.PageControllerTest do
|
||||||
assert redirected_to(conn) == "/map"
|
assert redirected_to(conn) == "/map"
|
||||||
end
|
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
|
# RFC 8288 Link headers for agent discovery. These advertise resources
|
||||||
# that actually exist in this app to crawlers/agents that look at
|
# that actually exist in this app to crawlers/agents that look at
|
||||||
# response headers before parsing HTML.
|
# response headers before parsing HTML.
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue