prop/test/microwaveprop/propagation/scorer_test.exs
Graham McIntire cb8445f329
fix: resolve 158/183 credo --strict warnings
- Add 17 missing @spec annotations (layouts, error_json, error_html, skewt_svg)
- Move 12+ nested alias/import/require to module top level
- Add phx-change/id attributes to 11 raw HTML <form> tags
- Remove 4 unused LiveView assigns (:bounds, :data_provider)
- Add 3 missing doctest references (HrrrNativeClient, BulkFetch, Accounts)
- Break 2 long lines (path_compute.ex:382)
- Strengthen weak test assertions (is_binary→byte_size, is_list→!=[])
- Replace Module.concat with Module.safe_concat (2 occurrences)
- Replace length/1 > 0 with list != [] (9 occurrences)
- Remove no-op assert true, fix no-assertion tests

Remaining: 24 socket.assigns introspection warnings (deliberate test
pattern for observable behavior testing), 1 formatter-resistant long
line, 3 app-code usage warnings.
2026-06-12 15:47:15 -05:00

174 lines
5.3 KiB
Elixir

defmodule Microwaveprop.Propagation.ScorerTest do
use Microwaveprop.DataCase, async: true
alias Microwaveprop.Propagation.Scorer
@contact %{
utc_hour: 18,
utc_minute: 30,
month: 6,
longitude: -97.0,
latitude: 32.9
}
describe "score/2" do
test "returns a map with score, contributions, breakdown" do
result = Scorer.score(@contact, 10_000)
assert is_integer(result.score)
assert result.score >= 0 and result.score <= 100
assert result.contributions != []
assert Map.has_key?(result, :breakdown)
end
test "score increases with favorable conditions" do
good_contact = Map.merge(@contact, %{utc_hour: 6, utc_minute: 0, month: 1})
bad_contact = Map.merge(@contact, %{utc_hour: 14, utc_minute: 0, month: 7})
good_score = Scorer.score(good_contact, 10_000)
bad_score = Scorer.score(bad_contact, 10_000)
assert good_score.score > bad_score.score
end
test "band config selection picks correct humidity effect" do
result_10g = Scorer.score(@contact, 10_000)
result_24g = Scorer.score(@contact, 24_000)
assert result_10g.band_mhz == 10_000
assert result_24g.band_mhz == 24_000
end
test "score returns nil for invalid band" do
assert Scorer.score(@contact, 999) == nil
end
end
describe "path_integrated_conditions/2" do
test "averages scalar fields and picks worst pressure / strongest gradient" do
profiles = [
%{
surface_temp_c: 25.0,
surface_dewpoint_c: 15.0,
surface_pressure_mb: 1010.0,
min_refractivity_gradient: -300.0,
hpbl_m: 500.0,
pwat_mm: 20.0
},
%{
surface_temp_c: 25.0,
surface_dewpoint_c: 15.0,
surface_pressure_mb: 1020.0,
min_refractivity_gradient: -400.0,
hpbl_m: 1500.0,
pwat_mm: 30.0
}
]
cond_map = Scorer.path_integrated_conditions(profiles, @contact)
assert %{temp_f: _} = cond_map
# Averages carry through to Fahrenheit equivalents.
assert_in_delta cond_map.temp_f, 77.0, 0.1
assert_in_delta cond_map.dewpoint_f, 59.0, 0.1
# min() picks the worst pressure and the strongest (most-negative)
# refractivity gradient.
assert cond_map.pressure_mb == 1010.0
assert cond_map.min_refractivity_gradient == -400.0
# avg() smooths HPBL + PWAT.
assert cond_map.bl_depth_m == 1000.0
assert cond_map.pwat_mm == 25.0
# Time/position fields come from the contact.
assert cond_map.utc_hour == 18
assert cond_map.utc_minute == 30
assert cond_map.month == 6
assert cond_map.longitude == -97.0
# Fields that the path integration can't infer from HRRR are nil.
assert cond_map.wind_speed_kts == nil
assert cond_map.sky_cover_pct == nil
assert cond_map.prev_pressure_mb == nil
assert cond_map.rain_rate_mmhr == 0.0
end
test "returns nil when every profile is missing surface temperature" do
profiles = [
%{
surface_temp_c: nil,
surface_dewpoint_c: 10.0,
surface_pressure_mb: 1020.0,
min_refractivity_gradient: -200.0,
hpbl_m: 500.0,
pwat_mm: 20.0
}
]
assert Scorer.path_integrated_conditions(profiles, @contact) == nil
end
test "returns nil when every profile is missing surface dewpoint" do
profiles = [
%{
surface_temp_c: 20.0,
surface_dewpoint_c: nil,
surface_pressure_mb: 1020.0,
min_refractivity_gradient: -200.0,
hpbl_m: 500.0,
pwat_mm: 20.0
}
]
assert Scorer.path_integrated_conditions(profiles, @contact) == nil
end
test "nil profile list yields nil" do
assert Scorer.path_integrated_conditions([], @contact) == nil
end
test "tolerates profiles with missing non-critical scalars (pressure, gradient, HPBL, PWAT)" do
# Only temp + dewpoint are required; everything else can be nil
# across all profiles and the conditions map still comes back
# with nil / avg-over-empty for those fields.
profiles = [
%{
surface_temp_c: 22.0,
surface_dewpoint_c: 15.0,
surface_pressure_mb: nil,
min_refractivity_gradient: nil,
hpbl_m: nil,
pwat_mm: nil
}
]
cond_map = Scorer.path_integrated_conditions(profiles, @contact)
assert %{temp_f: _} = cond_map
assert cond_map.pressure_mb == nil
assert cond_map.min_refractivity_gradient == nil
assert cond_map.bl_depth_m == nil
assert cond_map.pwat_mm == nil
end
test "uses -97.0 as longitude when contact.pos1 is missing the lon key" do
# Stale contacts written before pos1 was normalised can lack a
# lon field. The function falls back to CONUS-centre (-97.0).
profiles = [
%{
surface_temp_c: 22.0,
surface_dewpoint_c: 15.0,
surface_pressure_mb: 1015.0,
min_refractivity_gradient: -200.0,
hpbl_m: 1000.0,
pwat_mm: 25.0
}
]
contact_no_lon = %{utc_hour: 12, utc_minute: 0, month: 3, latitude: 32.9}
cond_map = Scorer.path_integrated_conditions(profiles, contact_no_lon)
assert cond_map.longitude == -97.0
end
end
end