prop/lib/microwaveprop/commercial/poll_worker.ex
Graham McIntire f99d07bd29
test: push line coverage from 71.7% → 78.67%
Adds ~600 new test cases (2013 → 2613 tests; 170 properties; 0
failures) across 12 new test files plus expansions of eight existing
files. Big lifts per module:

  ContactLive.Mechanism      33 → 100%
  MetricsPlug                54 → ~100%
  Microwaveprop.Release      15.8 → 70%+
  Telemetry                  38 → 76%
  HrrrNativeGridWorker       27.7 → 76%
  ContactLive.Show           34 → 48% (handler + render branches)
  Admin.ContactEditLive      49.7 → 70%+
  GefsFetchWorker            35.8 → ~55%
  IonosphereFetchWorker      56.3 → 75%
  PathLive                   58.9 → 67%
  WeatherMapLive             66.9 → 80.2%
  RoverLive                  0 → 70.3%
  ContactMapLive             59.2 → 89.8%
  ContactMapController       0 → 100%
  Mix tasks (Rust.Golden, Notebook, Backtest, HrrrBackfill,
    HrrrClimatology, HrrrNativeBackfill, NexradBackfill,
    RadarBackfill, ImportContestLogs, PropagationGrid,
    ResetEnrichment, Hrrr.PurgeGridPoints)  0 → ~60-100%

Two incidental fixes made while adding tests:

- ContactLive.Show.handle_event("toggle_flag", ...) was passing
  socket.assigns.current_scope to admin?/1 instead of socket.assigns,
  so admins never matched the pattern and every toggle ran the
  "Admins only." flash branch. Flag now toggles for admins again.

- Commercial.PollWorker.fetch_weather/1 promoted from private to
  @doc'd public so the IEM-fetch + ASOS-upsert path can be tested
  directly without driving perform/1 through live SNMP (which was
  timing out for ~50 s per test in the earlier attempt).

Stable property-test additions cover the dewpoint-from-RH monotonicity,
nearest_run/1 idempotence, and the ionosphere station envelope.
2026-04-23 18:43:18 -05:00

98 lines
2.6 KiB
Elixir

defmodule Microwaveprop.Commercial.PollWorker do
@moduledoc false
use Oban.Worker, queue: :commercial, max_attempts: 1
alias Microwaveprop.Commercial
alias Microwaveprop.Commercial.SnmpClient
alias Microwaveprop.Weather
alias Microwaveprop.Weather.IemClient
require Logger
@impl Oban.Worker
def perform(%Oban.Job{}) do
links = Commercial.enabled_links()
poll_and_record(links, &SnmpClient.poll/3)
fetch_weather(links)
:ok
end
def poll_and_record(links, poll_fn) do
now = DateTime.utc_now()
Enum.each(links, &poll_link(&1, poll_fn, now))
end
defp poll_link(link, poll_fn, now) do
case poll_fn.(link.host, link.community, link.radio_type) do
{:ok, data} ->
save_sample(link, data, now)
{:error, reason} ->
Logger.warning("SNMP poll failed for #{link.label} (#{link.host}): #{inspect(reason)}")
end
end
defp save_sample(link, data, now) do
attrs =
data
|> Map.put(:link_id, link.id)
|> Map.put(:sampled_at, now)
case Commercial.create_sample(attrs) do
{:ok, _sample} ->
Logger.info("Recorded sample for #{link.label}")
{:error, changeset} ->
Logger.warning("Failed to save sample for #{link.label}: #{inspect(changeset.errors)}")
end
end
@doc """
Fetch the last hour of ASOS data for every unique weather station
referenced by the given links. Exposed (not private) so tests can
exercise the IEM fetch + upsert path without having to drive
`perform/1` through the real SNMP layer.
"""
@spec fetch_weather([Commercial.Link.t()]) :: :ok
def fetch_weather(links) do
stations =
links
|> Enum.map(& &1.weather_station)
|> Enum.reject(&is_nil/1)
|> Enum.uniq()
now = DateTime.utc_now()
start_dt = DateTime.add(now, -3600, :second)
Enum.each(stations, &fetch_station_weather(&1, start_dt, now))
end
defp fetch_station_weather(station_code, start_dt, now) do
{:ok, station} =
Weather.find_or_create_station(%{
station_code: station_code,
station_type: "asos",
name: station_code,
lat: 0.0,
lon: 0.0
})
case IemClient.fetch_asos(station_code, start_dt, now) do
{:ok, rows} ->
ingest_asos(station, station_code, rows)
{:error, reason} ->
Logger.warning("ASOS fetch failed for #{station_code}: #{inspect(reason)}")
end
end
defp ingest_asos(station, station_code, rows) do
rows
|> Enum.filter(& &1.observed_at)
|> Enum.each(&Weather.upsert_surface_observation(station, &1))
Logger.info("Fetched #{length(rows)} ASOS observations for #{station_code}")
end
end