prop/test/microwaveprop/weather/ncei_metar_client_test.exs
Graham McIntire 56e6a3513c
test: cover METAR field extractors and Scorer.path_integrated_conditions
- NceiMetarClient: wind-group parsing (VVVKKKT), missing-wind-group
  fallback to nil, multi-layer sky condition concat, missing-sky token
  returns nil, missing altimeter returns nil, T-group-less fallback to
  the coarse "TT/DD" field.
- Scorer.path_integrated_conditions/2: averages surface temp/dew,
  takes MIN of pressure + refractivity gradient, takes AVG of HPBL +
  PWAT, threads contact time+longitude, and propagates -97.0 longitude
  when pos1 lacks a lon key. Returns nil when every profile is missing
  surface temp OR dewpoint; returns nil on empty-list input;
  tolerates nil pressure / gradient / HPBL / PWAT when temp + dew
  are present.

Suite: 2,409 tests + 159 properties (was 2,397 + 159); credo strict
clean.
2026-04-23 14:13:42 -05:00

112 lines
4.4 KiB
Elixir

defmodule Microwaveprop.Weather.NceiMetarClientTest do
use ExUnit.Case, async: true
alias Microwaveprop.Weather.NceiMetarClient
@sample_line "03927KDFW DFW20260301000010303/01/26 00:00:31 5-MIN KDFW 010600Z 17012KT 10SM CLR 21/09 A2997 560 47 1400 160/12 RMK AO2 T02110094"
describe "parse/1" do
test "parses a single KDFW observation" do
[obs] = NceiMetarClient.parse(@sample_line)
assert obs.icao == "KDFW"
assert obs.observed_at == ~U[2026-03-01 00:00:00Z]
# T02110094 → 21.1°C = 70.0°F, 9.4°C = 48.9°F
assert_in_delta obs.temp_f, 70.0, 0.2
assert_in_delta obs.dewpoint_f, 48.9, 0.2
assert obs.wind_speed_kts == 12.0
assert obs.wind_direction_deg == 170
assert_in_delta obs.altimeter_setting, 29.97, 0.01
assert obs.sky_condition == "CLR"
end
test "parses multiple lines" do
text = """
03927KDFW DFW20260301000010303/01/26 00:00:31 5-MIN KDFW 010600Z 17012KT 10SM CLR 21/09 A2997 560 47 1400 160/12 RMK AO2 T02110094
03927KDFW DFW20260301000510303/01/26 00:05:31 5-MIN KDFW 010605Z 17012KT 10SM CLR 21/09 A2997 560 47 1400 160/12 RMK AO2 T02110094
"""
obs = NceiMetarClient.parse(text)
assert length(obs) == 2
assert Enum.at(obs, 0).observed_at == ~U[2026-03-01 00:00:00Z]
assert Enum.at(obs, 1).observed_at == ~U[2026-03-01 00:05:00Z]
end
test "skips blank or too-short lines" do
assert NceiMetarClient.parse("") == []
assert NceiMetarClient.parse("short\n\n") == []
end
test "handles negative temperatures via M prefix" do
line =
"03927KDFW DFW20260115120010103/15/26 12:00:31 5-MIN KDFW 151200Z 36005KT 10SM CLR M02/M08 A3032 560 47 1400 360/05 RMK AO2"
[obs] = NceiMetarClient.parse(line)
# M02 = -2°C = 28.4°F, M08 = -8°C = 17.6°F
assert_in_delta obs.temp_f, 28.4, 0.2
assert_in_delta obs.dewpoint_f, 17.6, 0.2
end
test "extracts wind speed/direction from the VVVKKT group" do
# 17015KT = 170° at 15 kt.
line =
"03927KDFW DFW20260301000010303/01/26 00:00:31 5-MIN KDFW 010600Z 17015KT 10SM CLR 21/09 A2997 560 47 1400 160/12 RMK AO2 T02110094"
[obs] = NceiMetarClient.parse(line)
assert obs.wind_direction_deg == 170
assert obs.wind_speed_kts == 15.0
end
test "returns nil wind values when the wind group is missing" do
# Valid METAR without a wind group (CALM-ish). extract_wind_* should
# fall through to nil rather than crashing.
line =
"03927KDFW DFW20260301000010303/01/26 00:00:31 5-MIN KDFW 010600Z 10SM CLR 21/09 A2997 560 47 1400 160/12 RMK AO2 T02110094"
[obs] = NceiMetarClient.parse(line)
assert obs.wind_speed_kts == nil
assert obs.wind_direction_deg == nil
end
test "concatenates multiple sky tokens" do
# Layered ceiling: SCT030 + BKN080.
line =
"03927KDFW DFW20260301000010303/01/26 00:00:31 5-MIN KDFW 010600Z 17012KT 10SM SCT030 BKN080 21/09 A2997 560 47 1400 160/12 RMK AO2 T02110094"
[obs] = NceiMetarClient.parse(line)
assert obs.sky_condition =~ "SCT030"
assert obs.sky_condition =~ "BKN080"
end
test "sky_condition is nil when no cloud-layer token is present" do
# No CLR/FEW/SCT/BKN/OVC/VV token at all.
line =
"03927KDFW DFW20260301000010303/01/26 00:00:31 5-MIN KDFW 010600Z 17012KT 10SM 21/09 A2997 560 47 1400 160/12 RMK AO2 T02110094"
[obs] = NceiMetarClient.parse(line)
assert obs.sky_condition == nil
end
test "altimeter_setting is nil when the A-group is absent" do
line =
"03927KDFW DFW20260301000010303/01/26 00:00:31 5-MIN KDFW 010600Z 17012KT 10SM CLR 21/09 560 47 1400 160/12 RMK AO2 T02110094"
[obs] = NceiMetarClient.parse(line)
assert obs.altimeter_setting == nil
end
test "falls back to the coarse temp/dew pair when no T-group is in the remark" do
# No T-group → extract_temp_f falls through to the "21/09" field,
# which gives integer °C temperatures.
line =
"03927KDFW DFW20260301000010303/01/26 00:00:31 5-MIN KDFW 010600Z 17012KT 10SM CLR 21/09 A2997 560 47 1400 160/12 RMK AO2"
[obs] = NceiMetarClient.parse(line)
# 21°C = 69.8°F, 9°C = 48.2°F — coarser than the T-group but
# within ±0.3°F.
assert_in_delta obs.temp_f, 69.8, 0.3
assert_in_delta obs.dewpoint_f, 48.2, 0.3
end
end
end