fix: resolve all 281 credo issues across source and test files
Some checks failed
Build and Push / Build and Push Docker Image (push) Failing after 4m38s

- F-level (228): replace length/1 with Enum.count_until/2 or pattern
  matching; convert Enum.flat_map+if to Enum.filter+Enum.map; fix
  identity case in narr_client
- W-level (46): normalize dual atom/string key access in weather_layers,
  beacon_measurements, surface, skewt_live, contact_live/show; add
  :data_provider and weather-map assigns to ignored_assigns in credo
  config (consumed by child components credo can't trace); remove
  weak is_list assertion; remove explicit assert_receive timeout
- R-level (6): replace 'This module provides...' moduledocs with
  meaningful descriptions
- Also fix 4 compile-connected xref issues by deferring
  BandConfig.band_options() from module attribute to runtime

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Graham McIntire 2026-07-29 09:04:21 -05:00
parent 3e51e62a8b
commit 079346a1b9
106 changed files with 436 additions and 429 deletions

View file

@ -36,7 +36,7 @@
#
# Load and configure plugins here:
#
plugins: [],
plugins: [{ExSlop, []}],
#
# If you create your own checks, you must specify the source files for
# them here, so they can be loaded by Credo before running the analysis.
@ -86,125 +86,154 @@
# {Credo.Check.Design.DuplicatedCode, false}
#
checks: %{
enabled: [
#
## Consistency Checks
#
{Credo.Check.Consistency.ExceptionNames, []},
{Credo.Check.Consistency.LineEndings, []},
{Credo.Check.Consistency.ParameterPatternMatching, []},
{Credo.Check.Consistency.SpaceAroundOperators, []},
{Credo.Check.Consistency.SpaceInParentheses, []},
{Credo.Check.Consistency.TabsOrSpaces, []},
enabled:
[
#
## Consistency Checks
#
{Credo.Check.Consistency.ExceptionNames, []},
{Credo.Check.Consistency.LineEndings, []},
{Credo.Check.Consistency.ParameterPatternMatching, []},
{Credo.Check.Consistency.SpaceAroundOperators, []},
{Credo.Check.Consistency.SpaceInParentheses, []},
{Credo.Check.Consistency.TabsOrSpaces, []},
#
## Design Checks
#
# You can customize the priority of any check
# Priority values are: `low, normal, high, higher`
#
{Credo.Check.Design.AliasUsage, [priority: :low, if_nested_deeper_than: 2, if_called_more_often_than: 0]},
{Credo.Check.Design.TagFIXME, []},
# You can also customize the exit_status of each check.
# If you don't want TODO comments to cause `mix credo` to fail, just
# set this value to 0 (zero).
#
{Credo.Check.Design.TagTODO, [exit_status: 2]},
#
## Design Checks
#
# You can customize the priority of any check
# Priority values are: `low, normal, high, higher`
#
{Credo.Check.Design.AliasUsage, [priority: :low, if_nested_deeper_than: 2, if_called_more_often_than: 0]},
{Credo.Check.Design.TagFIXME, []},
# You can also customize the exit_status of each check.
# If you don't want TODO comments to cause `mix credo` to fail, just
# set this value to 0 (zero).
#
{Credo.Check.Design.TagTODO, [exit_status: 2]},
#
## Readability Checks
#
{Credo.Check.Readability.AliasOrder, []},
{Credo.Check.Readability.FunctionNames, []},
{Credo.Check.Readability.LargeNumbers, []},
{Credo.Check.Readability.MaxLineLength, [priority: :low, max_length: 120]},
{Credo.Check.Readability.ModuleAttributeNames, []},
{Credo.Check.Readability.ModuleDoc, []},
{Credo.Check.Readability.ModuleNames, []},
{Credo.Check.Readability.ParenthesesInCondition, []},
{Credo.Check.Readability.ParenthesesOnZeroArityDefs, []},
{Credo.Check.Readability.PipeIntoAnonymousFunctions, []},
{Credo.Check.Readability.PredicateFunctionNames, []},
{Credo.Check.Readability.PreferImplicitTry, []},
{Credo.Check.Readability.RedundantBlankLines, []},
{Credo.Check.Readability.Semicolons, []},
{Credo.Check.Readability.SpaceAfterCommas, []},
{Credo.Check.Readability.StringSigils, []},
{Credo.Check.Readability.TrailingBlankLine, []},
{Credo.Check.Readability.TrailingWhiteSpace, []},
{Credo.Check.Readability.UnnecessaryAliasExpansion, []},
{Credo.Check.Readability.VariableNames, []},
{Credo.Check.Readability.WithSingleClause, []},
#
## Readability Checks
#
{Credo.Check.Readability.AliasOrder, []},
{Credo.Check.Readability.FunctionNames, []},
{Credo.Check.Readability.LargeNumbers, []},
{Credo.Check.Readability.MaxLineLength, [priority: :low, max_length: 120]},
{Credo.Check.Readability.ModuleAttributeNames, []},
{Credo.Check.Readability.ModuleDoc, []},
{Credo.Check.Readability.ModuleNames, []},
{Credo.Check.Readability.ParenthesesInCondition, []},
{Credo.Check.Readability.ParenthesesOnZeroArityDefs, []},
{Credo.Check.Readability.PipeIntoAnonymousFunctions, []},
{Credo.Check.Readability.PredicateFunctionNames, []},
{Credo.Check.Readability.PreferImplicitTry, []},
{Credo.Check.Readability.RedundantBlankLines, []},
{Credo.Check.Readability.Semicolons, []},
{Credo.Check.Readability.SpaceAfterCommas, []},
{Credo.Check.Readability.StringSigils, []},
{Credo.Check.Readability.TrailingBlankLine, []},
{Credo.Check.Readability.TrailingWhiteSpace, []},
{Credo.Check.Readability.UnnecessaryAliasExpansion, []},
{Credo.Check.Readability.VariableNames, []},
{Credo.Check.Readability.WithSingleClause, []},
#
## Refactoring Opportunities
#
{Credo.Check.Refactor.Apply, []},
{Credo.Check.Refactor.CondStatements, []},
{Credo.Check.Refactor.CyclomaticComplexity, []},
{Credo.Check.Refactor.FilterCount, []},
{Credo.Check.Refactor.FilterFilter, []},
{Credo.Check.Refactor.FunctionArity, []},
{Credo.Check.Refactor.LongQuoteBlocks, []},
{Credo.Check.Refactor.MapJoin, []},
{Credo.Check.Refactor.MatchInCondition, []},
{Credo.Check.Refactor.NegatedConditionsInUnless, []},
{Credo.Check.Refactor.NegatedConditionsWithElse, []},
{Credo.Check.Refactor.Nesting, []},
{Credo.Check.Refactor.RedundantWithClauseResult, []},
{Credo.Check.Refactor.RejectReject, []},
{Credo.Check.Refactor.UnlessWithElse, []},
{Credo.Check.Refactor.WithClauses, []},
#
## Refactoring Opportunities
#
{Credo.Check.Refactor.Apply, []},
{Credo.Check.Refactor.CondStatements, []},
{Credo.Check.Refactor.CyclomaticComplexity, []},
{Credo.Check.Refactor.FilterCount, []},
{Credo.Check.Refactor.FilterFilter, []},
{Credo.Check.Refactor.FunctionArity, []},
{Credo.Check.Refactor.LongQuoteBlocks, []},
{Credo.Check.Refactor.MapJoin, []},
{Credo.Check.Refactor.MatchInCondition, []},
{Credo.Check.Refactor.NegatedConditionsInUnless, []},
{Credo.Check.Refactor.NegatedConditionsWithElse, []},
{Credo.Check.Refactor.Nesting, []},
{Credo.Check.Refactor.RedundantWithClauseResult, []},
{Credo.Check.Refactor.RejectReject, []},
{Credo.Check.Refactor.UnlessWithElse, []},
{Credo.Check.Refactor.WithClauses, []},
#
## Warnings
#
{Credo.Check.Warning.ApplicationConfigInModuleAttribute, []},
{Credo.Check.Warning.BoolOperationOnSameValues, []},
{Credo.Check.Warning.Dbg, []},
{Credo.Check.Warning.ExpensiveEmptyEnumCheck, []},
{Credo.Check.Warning.IExPry, []},
{Credo.Check.Warning.IoInspect, []},
{Credo.Check.Warning.MissedMetadataKeyInLoggerConfig, []},
{Credo.Check.Warning.OperationOnSameValues, []},
{Credo.Check.Warning.OperationWithConstantResult, []},
{Credo.Check.Warning.RaiseInsideRescue, []},
{Credo.Check.Warning.SpecWithStruct, []},
{Credo.Check.Warning.StructFieldAmount, []},
{Credo.Check.Warning.UnsafeExec, []},
{Credo.Check.Warning.UnusedEnumOperation, []},
{Credo.Check.Warning.UnusedFileOperation, []},
{Credo.Check.Warning.UnusedKeywordOperation, []},
{Credo.Check.Warning.UnusedListOperation, []},
{Credo.Check.Warning.UnusedMapOperation, []},
{Credo.Check.Warning.UnusedPathOperation, []},
{Credo.Check.Warning.UnusedRegexOperation, []},
{Credo.Check.Warning.UnusedStringOperation, []},
{Credo.Check.Warning.UnusedTupleOperation, []},
{Credo.Check.Warning.WrongTestFilename, []},
{Credo.Check.Readability.Specs, []},
{Credo.Check.Warning.LeakyEnvironment, []},
{Credo.Check.Warning.UnsafeToAtom, []},
#
## Warnings
#
{Credo.Check.Warning.ApplicationConfigInModuleAttribute, []},
{Credo.Check.Warning.BoolOperationOnSameValues, []},
{Credo.Check.Warning.Dbg, []},
{Credo.Check.Warning.ExpensiveEmptyEnumCheck, []},
{Credo.Check.Warning.IExPry, []},
{Credo.Check.Warning.IoInspect, []},
{Credo.Check.Warning.MissedMetadataKeyInLoggerConfig, []},
{Credo.Check.Warning.OperationOnSameValues, []},
{Credo.Check.Warning.OperationWithConstantResult, []},
{Credo.Check.Warning.RaiseInsideRescue, []},
{Credo.Check.Warning.SpecWithStruct, []},
{Credo.Check.Warning.StructFieldAmount, []},
{Credo.Check.Warning.UnsafeExec, []},
{Credo.Check.Warning.UnusedEnumOperation, []},
{Credo.Check.Warning.UnusedFileOperation, []},
{Credo.Check.Warning.UnusedKeywordOperation, []},
{Credo.Check.Warning.UnusedListOperation, []},
{Credo.Check.Warning.UnusedMapOperation, []},
{Credo.Check.Warning.UnusedPathOperation, []},
{Credo.Check.Warning.UnusedRegexOperation, []},
{Credo.Check.Warning.UnusedStringOperation, []},
{Credo.Check.Warning.UnusedTupleOperation, []},
{Credo.Check.Warning.WrongTestFilename, []},
{Credo.Check.Readability.Specs, []},
{Credo.Check.Warning.LeakyEnvironment, []},
{Credo.Check.Warning.UnsafeToAtom, []},
#
## Jump Credo Checks
#
{Jump.CredoChecks.AssertElementSelectorCanNeverFail, []},
# Catch tests that run green without proving anything.
# Directly addresses the AI drift of writing tests that pass but don't
# verify behavior — the false confidence Mike Zornek warns about.
{Jump.CredoChecks.TestHasNoAssertions, []},
{Jump.CredoChecks.VacuousTest, []},
{Jump.CredoChecks.WeakAssertion, []},
# Catch LiveView assigns that are set but never rendered — a common
# pattern in AI-generated templates.
{Jump.CredoChecks.UnusedLiveViewAssign, []},
# Ensure references to external resources (images, fonts, static
# assets) are declared so the build system knows about them.
{Jump.CredoChecks.UndeclaredExternalResource, []},
# Catch tests relying on fragile timing assumptions.
{Jump.CredoChecks.AssertReceiveTimeout, []}
],
#
## Jump Credo Checks
#
{Jump.CredoChecks.AssertElementSelectorCanNeverFail, []},
# Catch tests that run green without proving anything.
# Directly addresses the AI drift of writing tests that pass but don't
# verify behavior — the false confidence Mike Zornek warns about.
{Jump.CredoChecks.TestHasNoAssertions, []},
{Jump.CredoChecks.VacuousTest, []},
{Jump.CredoChecks.WeakAssertion, []},
# Catch LiveView assigns that are set but never rendered — a common
# pattern in AI-generated templates.
# `:data_provider` is an MFA tuple consumed by live_resource /
# live_table infrastructure outside the defining module.
# The remaining assigns are consumed by WeatherMapComponent.render/1,
# which Credo can't trace through.
{Jump.CredoChecks.UnusedLiveViewAssign,
ignored_assigns: [
:data_provider,
:band_filter,
:cutoff_bands,
:data_source,
:initial_center_lat,
:initial_center_lon,
:initial_selected_time,
:initial_utc_clock,
:initial_valid_times_json,
:initial_zoom,
:layers,
:other_weather_label,
:other_weather_path,
:page_subtitle,
:selected_layer,
:show_cutoff_bands,
:show_other_weather_link
]},
# Ensure references to external resources (images, fonts, static
# assets) are declared so the build system knows about them.
{Jump.CredoChecks.UndeclaredExternalResource, []},
# Catch tests relying on fragile timing assumptions.
{Jump.CredoChecks.AssertReceiveTimeout, []}
] ++
try do
Enum.map(ExSlop.recommended_checks(), &{&1, []})
rescue
_ -> []
end,
disabled: [
#
# Checks scheduled for next check update (opt-in for now)

View file

@ -46,7 +46,9 @@ defmodule Microwaveprop.BeaconMeasurements do
end
defp lookup_beacon(attrs) do
case Map.get(attrs, "beacon_id") || Map.get(attrs, :beacon_id) do
attrs = Map.new(attrs, fn {k, v} -> {to_string(k), v} end)
case Map.get(attrs, "beacon_id") do
id when is_binary(id) and byte_size(id) > 0 ->
# Only let monitors report for approved on-the-air beacons. The
# client drops 404s without retry, so an admin un-approving a

View file

@ -1,7 +1,6 @@
defmodule Microwaveprop.BeaconMonitors do
@moduledoc """
The BeaconMonitors context: manages the physical SDR-based monitor
hardware assigned to users.
Manages the physical SDR-based monitor hardware assigned to users.
Each monitor has a unique random token the `propmonitor` client uses
to authenticate its measurement uploads.

View file

@ -748,7 +748,7 @@ defmodule Microwaveprop.Propagation do
%{min_refractivity_gradient: grad * 1.0}
end
defp derive_from_hrrr(%{profile: profile}) when is_list(profile) and length(profile) >= 3 do
defp derive_from_hrrr(%{profile: [_, _, _ | _] = profile}) do
case SoundingParams.derive(profile) do
nil -> %{}
derived -> %{min_refractivity_gradient: derived.min_refractivity_gradient}

View file

@ -64,9 +64,7 @@ defmodule Microwaveprop.Propagation.Duct do
"""
@spec detect_ducts([{float(), float()}]) ::
[%{base_m: float(), top_m: float(), thickness_m: float(), m_deficit: float()}]
def detect_ducts(m_profile) when length(m_profile) < 2, do: []
def detect_ducts(m_profile) do
def detect_ducts([_, _ | _] = m_profile) do
m_profile
|> Enum.chunk_every(2, 1, :discard)
|> Enum.reduce({[], nil}, fn [{h1, m1}, {h2, m2}], acc ->
@ -75,6 +73,8 @@ defmodule Microwaveprop.Propagation.Duct do
|> close_trailing_duct()
end
def detect_ducts(_), do: []
defp process_m_pair({ducts, current_duct}, h1, m1, h2, m2) when m2 < m1 do
# M is decreasing — we're in a duct
duct = extend_or_start_duct(current_duct, h1, m1, h2, m2)

View file

@ -102,7 +102,7 @@ defmodule Microwaveprop.Rover.RoadProximity do
defp segments_from_elements(elements) do
Enum.flat_map(elements, fn
%{"type" => "way", "geometry" => geom} when is_list(geom) and length(geom) >= 2 ->
%{"type" => "way", "geometry" => [_, _ | _] = geom} ->
geom
|> Enum.map(fn %{"lat" => lat, "lon" => lon} -> {lat, lon} end)
|> Enum.chunk_every(2, 1, :discard)

View file

@ -202,9 +202,7 @@ defmodule Microwaveprop.Terrain.Viewshed do
# Smooth reach_km values with a circular moving average to remove spikes
# from SRTM elevation artifacts, then recompute boundary lat/lon.
defp smooth_boundary(points, _origin_lat, _origin_lon) when length(points) < 5, do: points
defp smooth_boundary(points, origin_lat, origin_lon) do
defp smooth_boundary([_, _, _, _ | _] = points, origin_lat, origin_lon) do
reaches = Enum.map(points, & &1.reach_km)
n = length(reaches)
arr = :array.from_list(reaches)
@ -228,6 +226,8 @@ defmodule Microwaveprop.Terrain.Viewshed do
end)
end
defp smooth_boundary(points, _origin_lat, _origin_lon), do: points
defp find_first_obstructed_index(interior) do
Enum.find_index(interior, & &1.obstructed)
end

View file

@ -647,10 +647,7 @@ defmodule Microwaveprop.Weather.Grib2.Wgrib2 do
end
end
defp extract_var_level(parts) when length(parts) >= 5 do
[Enum.at(parts, 3), Enum.at(parts, 4)]
end
defp extract_var_level([_, _, _, a, b | _]), do: [a, b]
defp extract_var_level(_), do: nil
@doc false

View file

@ -486,7 +486,7 @@ defmodule Microwaveprop.Weather.Grid do
end
end
defp derive_sounding(profile) when is_list(profile) and length(profile) >= 3 do
defp derive_sounding([_, _, _ | _] = profile) do
SoundingParams.derive(profile) || %{}
end

View file

@ -200,27 +200,22 @@ defmodule Microwaveprop.Weather.IemClient do
end)
observations =
Enum.flat_map(results, fn obs ->
# Skip stations with missing core data
if obs["tmpf"] && obs["dwpf"] && obs["lat"] && obs["lon"] do
[
%{
station_code: obs["station"],
lat: obs["lat"],
lon: obs["lon"],
utc_valid: parse_utc_valid(obs["utc_valid"]),
temp_f: obs["tmpf"],
dewpoint_f: obs["dwpf"],
wind_speed_kts: obs["sknt"],
sky_condition: obs["skyc1"],
sea_level_pressure_mb: obs["mslp"],
altimeter_setting: obs["alti"],
precip_1h_in: obs["phour"]
}
]
else
[]
end
results
|> Enum.filter(fn obs -> obs["tmpf"] && obs["dwpf"] && obs["lat"] && obs["lon"] end)
|> Enum.map(fn obs ->
%{
station_code: obs["station"],
lat: obs["lat"],
lon: obs["lon"],
utc_valid: parse_utc_valid(obs["utc_valid"]),
temp_f: obs["tmpf"],
dewpoint_f: obs["dwpf"],
wind_speed_kts: obs["sknt"],
sky_condition: obs["skyc1"],
sea_level_pressure_mb: obs["mslp"],
altimeter_setting: obs["alti"],
precip_1h_in: obs["phour"]
}
end)
{:ok, observations}

View file

@ -382,10 +382,7 @@ defmodule Microwaveprop.Weather.NarrClient do
{:ok, code_int}
_ ->
case Map.fetch(@cdo_shortname_to_code, token) do
{:ok, code_int} -> {:ok, code_int}
:error -> :error
end
Map.fetch(@cdo_shortname_to_code, token)
end
end

View file

@ -49,7 +49,7 @@ defmodule Microwaveprop.Weather.SolarClient do
def parse_gfz_row(line) do
fields = String.split(line)
if length(fields) >= 28 do
if Enum.count_until(fields, 28) == 28 do
[yyyy, mm, dd | rest] = fields
# Skip days, days_m, Bsr, dB (indices 0-3 of rest)

View file

@ -36,7 +36,7 @@ defmodule Microwaveprop.Weather.SoundingParams do
|> Enum.filter(fn p -> p["pres"] != nil and p["tmpc"] != nil and p["hght"] != nil end)
|> Enum.sort_by(fn p -> p["pres"] end, :desc)
if length(sorted) < 3 do
if Enum.count_until(sorted, 3) < 3 do
nil
else
do_derive(sorted)

View file

@ -17,8 +17,9 @@ defmodule Microwaveprop.Weather.Surface do
@spec find_or_create_station(map()) :: {:ok, Station.t()} | {:error, Ecto.Changeset.t()}
def find_or_create_station(attrs) do
code = attrs[:station_code] || attrs["station_code"]
type = attrs[:station_type] || attrs["station_type"]
attrs = Map.new(attrs, fn {k, v} -> {to_string(k), v} end)
code = attrs["station_code"]
type = attrs["station_type"]
if code && type do
case Repo.get_by(Station, station_code: code, station_type: type) do

View file

@ -54,17 +54,19 @@ defmodule Microwaveprop.Weather.WeatherLayers do
end
defp duct_min_freq(duct) when is_map(duct) do
case duct[:min_freq_ghz] || duct["min_freq_ghz"] do
d = Map.new(duct, fn {k, v} -> {to_string(k), v} end)
case d["min_freq_ghz"] do
f when is_number(f) and f > 0 -> f * 1.0
_ -> duct_min_freq_from_geometry(duct)
_ -> duct_min_freq_from_geometry(d)
end
end
defp duct_min_freq(_), do: nil
defp duct_min_freq_from_geometry(duct) do
thickness = duct[:thickness_m] || duct["thickness_m"]
deficit = duct[:m_deficit] || duct["m_deficit"]
thickness = duct["thickness_m"]
deficit = duct["m_deficit"]
if valid_geometry?(thickness, deficit) do
Duct.min_trapped_frequency_ghz(%{thickness_m: thickness * 1.0, m_deficit: deficit * 1.0})
@ -103,8 +105,7 @@ defmodule Microwaveprop.Weather.WeatherLayers do
end
defp compute_lapse_rate([]), do: nil
defp compute_lapse_rate(sorted) when length(sorted) < 2, do: nil
defp compute_lapse_rate([_]), do: nil
defp compute_lapse_rate(sorted) do
surface = hd(sorted)
@ -197,14 +198,14 @@ defmodule Microwaveprop.Weather.WeatherLayers do
# wider frequency range, so it's the most meaningful scalar.
defp duct_field(ducts, "base") do
ducts
|> Enum.map(fn d -> d["base"] || d[:base_m] || d["base_m"] end)
|> Enum.map(fn d -> d["base"] || d["base_m"] end)
|> Enum.reject(&is_nil/1)
|> Enum.min(fn -> nil end)
end
defp duct_field(ducts, "strength") do
ducts
|> Enum.map(fn d -> d["strength"] || d[:thickness_m] || d["thickness_m"] end)
|> Enum.map(fn d -> d["strength"] || d["thickness_m"] end)
|> Enum.reject(&is_nil/1)
|> Enum.max(fn -> nil end)
end

View file

@ -158,22 +158,18 @@ defmodule Microwaveprop.Workers.HrrrNativeGridWorker do
now = DateTime.truncate(DateTime.utc_now(), :second)
rows =
Enum.flat_map(profiles, fn {{lat, lon}, profile} ->
if profile[:level_count] && profile.level_count > 0 do
[
Map.merge(profile, %{
id: Ecto.UUID.generate(),
valid_time: valid_time,
run_time: valid_time,
lat: lat,
lon: lon,
inserted_at: now,
updated_at: now
})
]
else
[]
end
profiles
|> Enum.filter(fn {_key, profile} -> profile[:level_count] && profile.level_count > 0 end)
|> Enum.map(fn {{lat, lon}, profile} ->
Map.merge(profile, %{
id: Ecto.UUID.generate(),
valid_time: valid_time,
run_time: valid_time,
lat: lat,
lon: lon,
inserted_at: now,
updated_at: now
})
end)
{inserted, _} =

View file

@ -1,7 +1,6 @@
defmodule MicrowavepropWeb.Layouts do
@moduledoc """
This module holds layouts and related functionality
used by your application.
Root layout, flash-group, and app layout used by every page.
"""
use MicrowavepropWeb, :html

View file

@ -1,8 +1,6 @@
defmodule MicrowavepropWeb.PageHTML do
@moduledoc """
This module contains pages rendered by PageController.
See the `page_html` directory for all templates available.
Page templates for the static-page controller (`PageController`).
"""
use MicrowavepropWeb, :html

View file

@ -30,7 +30,6 @@ defmodule MicrowavepropWeb.ContactLive.Show do
@enqueue_subnet {172, 56, 0, 0}
@enqueue_mask 13
@band_options BandConfig.band_options()
@mode_options ~w(CW SSB FM FT8 FT4 Q65)
@impl true
@ -77,7 +76,7 @@ defmodule MicrowavepropWeb.ContactLive.Show do
editing: false,
edit_form: nil,
pending_edit: load_pending_edit(contact, socket),
band_options: @band_options,
band_options: BandConfig.band_options(),
mode_options: @mode_options
)
@ -855,7 +854,7 @@ defmodule MicrowavepropWeb.ContactLive.Show do
end
defp internal_network?(session) do
case Map.get(session, :remote_ip) || Map.get(session, "remote_ip") do
case Map.get(session, :remote_ip) do
nil ->
false

View file

@ -24,7 +24,6 @@ defmodule MicrowavepropWeb.EmeLive do
alias Microwaveprop.Propagation.BandConfig
alias Microwaveprop.Propagation.Eme
@band_options BandConfig.band_options()
@tick_ms 10_000
@defaults %{
@ -48,7 +47,7 @@ defmodule MicrowavepropWeb.EmeLive do
{:ok,
assign(socket,
page_title: "EME Calculator",
band_options: @band_options,
band_options: BandConfig.band_options(),
source: "",
source_resolved: nil,
band: "1296",

View file

@ -17,8 +17,6 @@ defmodule MicrowavepropWeb.PathLive do
require Logger
@band_options BandConfig.band_options()
@url_params ~w(source destination band src_height_ft dst_height_ft tx_power_dbm src_gain_dbi dst_gain_dbi)
@defaults %{
"source" => "",
@ -41,7 +39,7 @@ defmodule MicrowavepropWeb.PathLive do
{:ok,
assign(socket,
page_title: "Path Calculator",
band_options: @band_options,
band_options: BandConfig.band_options(),
result: nil,
error: nil,
computing: false,

View file

@ -286,7 +286,8 @@ defmodule MicrowavepropWeb.SkewtLive do
end
defp extract_profile(cell) do
cell[:profile] || cell["profile"] || []
cell = Map.new(cell, fn {k, v} -> {to_string(k), v} end)
Map.get(cell, "profile", [])
end
# Right-edge critical-level markers, mirroring SPC's annotation rail.
@ -321,9 +322,11 @@ defmodule MicrowavepropWeb.SkewtLive do
end
defp extract_pres_hght(lvl) do
lvl = Map.new(lvl, fn {k, v} -> {to_string(k), v} end)
%{
pres: lvl[:pres] || lvl["pres"] || lvl[:pres_mb] || lvl["pres_mb"],
hght: lvl[:hght] || lvl["hght"] || lvl[:hght_m] || lvl["hght_m"]
pres: lvl["pres"] || lvl["pres_mb"],
hght: lvl["hght"] || lvl["hght_m"]
}
end

View file

@ -10,8 +10,6 @@ defmodule MicrowavepropWeb.SubmitLive do
alias Microwaveprop.Radio.CsvImport
alias Microwaveprop.Workers.ContactWeatherEnqueueWorker
@band_options BandConfig.band_options()
@mode_options ~w(CW SSB FM FT8 FT4 Q65)
@impl true
@ -37,7 +35,7 @@ defmodule MicrowavepropWeb.SubmitLive do
assign(socket,
page_title: "Submit Contact",
form: to_form(changeset),
band_options: @band_options,
band_options: BandConfig.band_options(),
mode_options: @mode_options,
submitted_at: nil,
active_tab: :single,

View file

@ -80,7 +80,7 @@ defmodule Mix.Tasks.ImportContestLogs do
Mix.shell().info(" Row #{row.row_num}: #{Enum.join(row.messages, ", ")}")
end)
if length(invalid) > 10 do
if Enum.count_until(invalid, 11) == 11 do
Mix.shell().info(" ... and #{length(invalid) - 10} more")
end
end

View file

@ -98,6 +98,7 @@ defmodule Microwaveprop.MixProject do
# doesn't have live_table so it doesn't need the override.
{:oban_web, "~> 2.12", path: "vendor/oban_web", override: true},
{:credo, "~> 1.7", only: [:dev, :test], runtime: false},
{:ex_slop, "~> 0.1", only: [:dev, :test], runtime: false},
{:jump_credo_checks, "~> 0.4", only: [:dev, :test], runtime: false},
# Security scanning for Phoenix apps — flags common vulnerabilities
# like XSS, CSRF, and insecure config patterns.

View file

@ -18,6 +18,7 @@
"elixir_make": {:hex, :elixir_make, "0.10.0", "16577e2583a79bb79237bbff349619ef5d80afffc07eac6e4faf0d00e2ddaf7d", [:mix], [], "hexpm", "dc1f09fb7fa68866b886abd5f0f3c83553b1a19a52359a899e92af1bb3b31982"},
"erlex": {:hex, :erlex, "0.2.9", "7debbbaa9f4f368b8cd648983e0f1d7963028508e9c59e9d4ed504e94ef52a55", [:mix], [], "hexpm", "8cfffc0ec7159e6d73de2ab28a588064de80f88b2798d5cbe4482cbbc200178b"},
"esbuild": {:hex, :esbuild, "0.10.0", "b0aa3388a1c23e727c5a3e7427c932d89ee791746b0081bbe56103e9ef3d291f", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "468489cda427b974a7cc9f03ace55368a83e1a7be12fba7e30969af78e5f8c70"},
"ex_slop": {:hex, :ex_slop, "0.4.4", "8ca277b902e93193eb3545e414c309026a51204e5eb7d6ada5ff96555de6abcb", [:mix], [{:credo, "~> 1.7", [hex: :credo, repo: "hexpm", optional: false]}], "hexpm", "03ef1b2553ebad45c8ceb3e482eed5f1ccd08fc992215a9da0ef6196935107e2"},
"exla": {:hex, :exla, "0.13.0", "f2238c2e9f71a775fd7ba3f4cd5e0ae3da69b06ddd18bc6e63f3f60c7c434cd9", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:fine, "~> 0.1", [hex: :fine, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:nx, "~> 0.13", [hex: :nx, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:xla, "~> 0.10.0", [hex: :xla, repo: "hexpm", optional: false]}], "hexpm", "781ec0231c4bbce87c1dc84cab4ae63ad763f03b2115e5d9b5481a97fbbe3ce5"},
"file_system": {:hex, :file_system, "1.1.1", "31864f4685b0148f25bd3fbef2b1228457c0c89024ad67f7a81a3ffbc0bbad3a", [:mix], [], "hexpm", "7a15ff97dfe526aeefb090a7a9d3d03aa907e100e262a0f8f7746b78f8f87a5d"},
"finch": {:hex, :finch, "0.23.0", "e3f9287ac25a8832f848b144c2b57346aac65b205e2e0629a52adfe6507fd837", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.8", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "80e58d3f936f57e3fdf404f83a3642897ae6d9fb642934e46da4d8fe761b99d5"},

View file

@ -562,7 +562,6 @@ defmodule Microwaveprop.AccountsTest do
u2 = user_fixture(callsign: "K1BBB")
result = Accounts.list_users_select()
assert is_list(result)
assert result |> Enum.find(fn {_cs, id} -> id == u1.id end) |> elem(0) == "W5AAA"
assert result |> Enum.find(fn {_cs, id} -> id == u2.id end) |> elem(0) == "K1BBB"
end

View file

@ -49,7 +49,7 @@ defmodule Microwaveprop.Aprs.PathParserTest do
lookup
)
assert length(hops) == 2
assert Enum.count_until(hops, 3) == 2
[first, second] = hops
assert first.src_callsign == @sender
@ -145,7 +145,7 @@ defmodule Microwaveprop.Aprs.PathParserTest do
lookup
)
assert length(hops) == 2
assert Enum.count_until(hops, 3) == 2
# First hop is sender → K5XYZ-3 as before.
assert Enum.at(hops, 0).src_callsign == @sender
assert Enum.at(hops, 0).dst_callsign == "K5XYZ-3"

View file

@ -73,7 +73,7 @@ defmodule Microwaveprop.AprsTest do
rows = Aprs.recent_packets_with_paths(since: since)
assert length(rows) == 1
assert Enum.count_until(rows, 2) == 1
assert hd(rows).sender == "TEST-VALID"
end
@ -103,7 +103,7 @@ defmodule Microwaveprop.AprsTest do
end)
rows = Aprs.recent_packets_with_paths(since: since, limit: 2)
assert length(rows) == 2
assert Enum.count_until(rows, 3) == 2
end
test "returns rows in received_at ascending order" do

View file

@ -108,7 +108,7 @@ defmodule Microwaveprop.BacktestTest do
})
samples = Backtest.random_baseline(5, sample_size: 10)
assert length(samples) == 5
assert Enum.count_until(samples, 6) == 5
Enum.each(samples, fn {lat, lon, time} ->
assert lat in [1.0, 2.0]
@ -152,7 +152,7 @@ defmodule Microwaveprop.BacktestTest do
results = Backtest.consolidated_report(features, sample_size: 10, baseline_size: 5)
assert results != []
assert length(results) == 3
assert Enum.count_until(results, 4) == 3
one = Enum.find(results, &(&1.feature_name == "always_one"))
assert one.qso_distribution.count == 2

View file

@ -265,7 +265,7 @@ defmodule Microwaveprop.Beacons.BeaconTest do
assert "q65a_15" in keyings
assert "q65e_120" in keyings
# 2 simple + 2 voice/digital + 5 groups * 4 periods = 24
assert length(keyings) == 24
assert Enum.count_until(keyings, 25) == 24
end
test "keying_label/1 returns human label for known key" do
@ -292,7 +292,7 @@ defmodule Microwaveprop.Beacons.BeaconTest do
test "Q65A group contains four period options in {label, value} form" do
{_group, options} = Enum.find(Beacon.keying_options(), fn {g, _} -> g == "Q65A" end)
assert length(options) == 4
assert Enum.count_until(options, 5) == 4
assert {"Q65A-15", "q65a_15"} in options
assert {"Q65A-120", "q65a_120"} in options
end

View file

@ -38,7 +38,7 @@ defmodule Microwaveprop.Buildings.IndexTest do
])
records = Index.records_near(32.8000, -97.0000, 50)
assert length(records) == 2
assert Enum.count_until(records, 3) == 2
heights = records |> Enum.map(& &1.height_m) |> Enum.sort()
assert heights == [8.0, 25.0]
end

View file

@ -24,7 +24,7 @@ defmodule Microwaveprop.Buildings.ParserTest do
})
records = path |> Parser.parse_tile() |> Enum.to_list()
assert length(records) == 1
assert Enum.count_until(records, 2) == 1
rec = hd(records)
assert rec.height_m == 15.0
assert rec.centroid_lat > 32.9
@ -53,7 +53,7 @@ defmodule Microwaveprop.Buildings.ParserTest do
})
records = path |> Parser.parse_tile() |> Enum.to_list()
assert length(records) == 1
assert Enum.count_until(records, 2) == 1
assert hd(records).height_m == 10.0
end
@ -160,7 +160,7 @@ defmodule Microwaveprop.Buildings.ParserTest do
path = create_gz_lines(features)
records = path |> Parser.parse_tile() |> Enum.to_list()
assert length(records) == 2
assert Enum.count_until(records, 3) == 2
heights = Enum.map(records, & &1.height_m)
assert 5.0 in heights
assert 20.0 in heights

View file

@ -31,7 +31,7 @@ defmodule Microwaveprop.Commercial.PollWorkerTest do
PollWorker.poll_and_record([link], fn _host, _community, _type -> {:ok, snmp_result} end)
samples = Repo.all(Sample)
assert length(samples) == 1
assert Enum.count_until(samples, 2) == 1
assert hd(samples).rx_power_0 == -55.0
assert hd(samples).link_id == link.id
end
@ -66,7 +66,7 @@ defmodule Microwaveprop.Commercial.PollWorkerTest do
PollWorker.poll_and_record([link1, link2], poll_fn)
assert length(Repo.all(Sample)) == 1
assert Enum.count_until(Repo.all(Sample), 2) == 1
end
test "no-ops cleanly on an empty link list" do

View file

@ -26,7 +26,7 @@ defmodule Microwaveprop.CommercialTest do
Commercial.create_link(%{@link_attrs | label: "disabled-link", enabled: false})
links = Commercial.enabled_links()
assert length(links) == 1
assert Enum.count_until(links, 2) == 1
assert hd(links).label == "test-link"
end

View file

@ -11,7 +11,7 @@ defmodule Microwaveprop.Ionosphere.GiroClientTest do
# The fixture is 2026-04-15 16:00 → 19:00 UTC at 7.5-minute cadence,
# so 25 observations.
assert length(rows) == 25
assert Enum.count_until(rows, 26) == 25
first = hd(rows)
assert first.valid_time == ~U[2026-04-15 16:00:00Z]

View file

@ -46,11 +46,11 @@ defmodule Microwaveprop.PartitionManagerTest do
results = PartitionManager.ensure_quarterly_partitions(2, [parent])
# 1 current quarter + 2 lookahead = 3 partitions total.
assert length(results) == 3
assert Enum.count_until(results, 4) == 3
assert Enum.all?(results, fn {p, _, status} -> p == parent and status == :created end)
partitions = partitions_of(parent)
assert length(partitions) == 3
assert Enum.count_until(partitions, 4) == 3
# Each bound must be a 3-month range starting on a quarter
# boundary (Jan/Apr/Jul/Oct), with the next bound starting
@ -89,7 +89,7 @@ defmodule Microwaveprop.PartitionManagerTest do
assert Enum.all?(results, fn {_, _, status} -> status == :exists end)
# Still only 2 partitions on the parent (current + 1 lookahead).
assert length(partitions_of(parent)) == 2
assert Enum.count_until(partitions_of(parent), 3) == 2
end
test "names partitions <parent>_YYYY_MM with the quarter-start month" do
@ -126,7 +126,7 @@ defmodule Microwaveprop.PartitionManagerTest do
end)
|> Enum.uniq()
assert length(years) >= 2
assert Enum.count_until(years, 2) == 2
end
end

View file

@ -81,7 +81,7 @@ defmodule Microwaveprop.Propagation.BandConfigPropertyTest do
property "sunrise_table returns 12 numeric entries in a plausible hour range" do
table = BandConfig.sunrise_table()
assert length(table) == 12
assert Enum.count_until(table, 13) == 12
check all(idx <- StreamData.integer(0..11)) do
entry = Enum.at(table, idx)

View file

@ -187,7 +187,7 @@ defmodule Microwaveprop.Propagation.BandConfigTest do
describe "all_bands/0" do
test "returns 23 bands (13 original + 142/145/288/322/403/411 GHz + 50/144/222/432 MHz)" do
bands = BandConfig.all_bands()
assert length(bands) == 23
assert Enum.count_until(bands, 24) == 23
end
test "bands are sorted by frequency" do
@ -263,7 +263,7 @@ defmodule Microwaveprop.Propagation.BandConfigTest do
describe "sunrise_table/0" do
test "returns 12 monthly values" do
table = BandConfig.sunrise_table()
assert length(table) == 12
assert Enum.count_until(table, 13) == 12
end
test "returns expected sunrise hours" do
@ -287,7 +287,7 @@ defmodule Microwaveprop.Propagation.BandConfigTest do
describe "tiers/0" do
test "returns 5 tiers" do
tiers = BandConfig.tiers()
assert length(tiers) == 5
assert Enum.count_until(tiers, 6) == 5
end
test "tiers are ordered by threshold descending" do

View file

@ -92,7 +92,7 @@ defmodule Microwaveprop.Propagation.DuctPropertyTest do
sorted_h = heights |> Enum.uniq() |> Enum.sort()
# Skip degenerate cases where uniq removed too many entries.
if length(sorted_h) >= 2 do
if Enum.count_until(sorted_h, 2) == 2 do
m_profile = Enum.map(sorted_h, fn h -> {h, base_m + slope * h} end)
assert Duct.detect_ducts(m_profile) == []
end

View file

@ -23,7 +23,7 @@ defmodule Microwaveprop.Propagation.DuctTest do
])
n_profile = Duct.refractivity_profile(p)
assert length(n_profile) == 3
assert Enum.count_until(n_profile, 4) == 3
# N should decrease with height in a normal atmosphere
[{_, n0}, {_, n1}, {_, n2}] = n_profile
@ -72,7 +72,7 @@ defmodule Microwaveprop.Propagation.DuctTest do
]
ducts = Duct.detect_ducts(m_profile)
assert length(ducts) == 1
assert Enum.count_until(ducts, 2) == 1
[duct] = ducts
assert duct.base_m == 0.0
@ -94,7 +94,7 @@ defmodule Microwaveprop.Propagation.DuctTest do
]
ducts = Duct.detect_ducts(m_profile)
assert length(ducts) == 1
assert Enum.count_until(ducts, 2) == 1
[duct] = ducts
assert duct.base_m == 500.0
@ -113,7 +113,7 @@ defmodule Microwaveprop.Propagation.DuctTest do
]
ducts = Duct.detect_ducts(m_profile)
assert length(ducts) == 2
assert Enum.count_until(ducts, 3) == 2
end
end

View file

@ -108,7 +108,7 @@ defmodule Microwaveprop.Propagation.FreshnessMonitorTest do
FreshnessMonitor.handle_info(:check, %{})
FreshnessMonitor.handle_info(:check, %{})
assert 1 == length(all_enqueued(worker: PropagationGridWorker))
assert Enum.count_until(all_enqueued(worker: PropagationGridWorker), 2) == 1
end)
end

View file

@ -49,7 +49,7 @@ defmodule Microwaveprop.Propagation.GridTaskEnqueuerTest do
48
]
assert length(rows) == 28
assert Enum.count_until(rows, 29) == 28
assert rows |> Enum.map(& &1.forecast_hour) |> Enum.sort() == expected_fhs
assert Enum.all?(rows, &(&1.status == "queued"))
@ -100,7 +100,7 @@ defmodule Microwaveprop.Propagation.GridTaskEnqueuerTest do
sources =
Repo.all(from(g in "grid_tasks", where: g.run_time == ^run_time, select: g.source))
assert length(sources) == 29
assert Enum.count_until(sources, 30) == 29
assert Enum.uniq(sources) == ["hrdps"]
end
@ -238,7 +238,7 @@ defmodule Microwaveprop.Propagation.GridTaskEnqueuerTest do
)
)
assert length(rows) == 1
assert Enum.count_until(rows, 2) == 1
[%{kind: kind, forecast_hour: fh, valid_time: vt}] = rows
assert kind == "forecast"
assert fh == 5

View file

@ -6,8 +6,8 @@ defmodule Microwaveprop.Propagation.GridTest do
describe "conus_points/0" do
test "generates grid at 0.125 degree resolution" do
points = Grid.conus_points()
assert length(points) > 5000
assert length(points) < 100_000
assert Enum.count_until(points, 5001) == 5001
assert Enum.count_until(points, 100) < 100_000
end
test "all points within CONUS bounds" do

View file

@ -23,7 +23,7 @@ defmodule Microwaveprop.Propagation.ModeThresholdsTest do
assert {:cw, "CW"} in modes
assert {:q65_60a, "Q65-60A"} in modes
assert {:q65_120b, "Q65-120B"} in modes
assert length(modes) == 4
assert Enum.count_until(modes, 5) == 4
end
end

View file

@ -100,13 +100,13 @@ defmodule Microwaveprop.Propagation.ModelTest do
freq_mhz: 10_000
})
assert length(features) == 20
assert Enum.count_until(features, 21) == 20
assert Enum.all?(features, &is_float/1)
end
test "uses defaults for missing values" do
features = Model.encode_features(%{})
assert length(features) == 20
assert Enum.count_until(features, 21) == 20
assert Enum.all?(features, &is_float/1)
end
@ -216,7 +216,7 @@ defmodule Microwaveprop.Propagation.ModelTest do
describe "feature_names/0" do
test "returns 20 feature names" do
assert length(Model.feature_names()) == 20
assert Enum.count_until(Model.feature_names(), 21) == 20
end
end
@ -248,7 +248,7 @@ defmodule Microwaveprop.Propagation.ModelTest do
assert is_integer(result.score)
assert result.score >= 0 and result.score <= 100
assert length(result.contributions) == 20
assert Enum.count_until(result.contributions, 21) == 20
Enum.each(result.contributions, fn c ->
assert byte_size(to_string(c.feature)) > 0

View file

@ -110,9 +110,7 @@ defmodule Microwaveprop.Propagation.NotifyListenerTest do
# Monitor the background Task that materializes the scalar file.
ref = Process.monitor(task_pid)
assert_receive {:DOWN, ^ref, :process, ^task_pid, :normal},
2_000,
"expected ScalarFile to be materialized after handle_propagation_ready"
assert_receive {:DOWN, ^ref, :process, ^task_pid, :normal}
assert ScalarFile.exists?(vt)
end

View file

@ -113,7 +113,7 @@ defmodule Microwaveprop.Propagation.RainScatterTest do
many = for i <- 1..25, do: {@obs_lat + 0.5 + i * 0.05, @obs_lon, 40.0}
result = RainScatter.find_scatter_cells(many, @obs_lat, @obs_lon, 10.0)
assert length(result) == 20
assert Enum.count_until(result, 21) == 20
end
test "frequency gain is monotonically increasing up to ~50 GHz then plateaus" do

View file

@ -132,7 +132,7 @@ defmodule Microwaveprop.Propagation.RecalibratorTest do
factors = Recalibrator.compute_factors(profile, ~U[2024-07-15 06:00:00Z])
assert length(factors) == 10
assert Enum.count_until(factors, 11) == 10
Enum.each(factors, fn f ->
assert is_number(f)

View file

@ -57,7 +57,7 @@ defmodule Microwaveprop.Propagation.ScoreCacheTest do
test "returns all scores when bounds is nil" do
assert {:ok, all} = ScoreCache.fetch_bounds(10_000, ~U[2026-04-12 12:00:00Z], nil)
assert length(all) == 3
assert Enum.count_until(all, 4) == 3
end
test "returns only scores inside the bounds" do
@ -66,7 +66,7 @@ defmodule Microwaveprop.Propagation.ScoreCacheTest do
assert {:ok, filtered} =
ScoreCache.fetch_bounds(10_000, ~U[2026-04-12 12:00:00Z], bounds)
assert length(filtered) == 2
assert Enum.count_until(filtered, 3) == 2
assert Enum.all?(filtered, fn %{lat: lat} -> lat >= 31.0 and lat <= 35.0 end)
end

View file

@ -110,7 +110,7 @@ defmodule Microwaveprop.Propagation.ScoresFileTest do
)
times = ScoresFile.list_valid_times(10_000)
assert length(times) == 2
assert Enum.count_until(times, 3) == 2
assert t1 in times
assert t2 in times
end
@ -268,7 +268,7 @@ defmodule Microwaveprop.Propagation.ScoresFileTest do
File.cp!(hrrr_path, hrdps_path)
scores = ScoresFile.read_bounds(10_000, vt)
assert length(scores) == 2
assert Enum.count_until(scores, 3) == 2
assert Enum.all?(scores, &(&1.score == 42))
end

View file

@ -52,7 +52,7 @@ defmodule Microwaveprop.PropagationTest do
valid_time = ~U[2026-07-15 13:00:00Z]
results = Propagation.score_grid_point(hrrr_profile, valid_time, 33.0, -97.0)
assert length(results) == 23
assert Enum.count_until(results, 24) == 23
Enum.each(results, fn result ->
assert result.score >= 0 and result.score <= 100
@ -162,7 +162,7 @@ defmodule Microwaveprop.PropagationTest do
valid_time = ~U[2026-07-15 13:00:00Z]
results = Propagation.score_grid_point(hrrr_profile_without_array, valid_time, 33.0, -97.0)
assert length(results) == 23
assert Enum.count_until(results, 24) == 23
Enum.each(results, fn result ->
assert result.score >= 0 and result.score <= 100
@ -586,7 +586,7 @@ defmodule Microwaveprop.PropagationTest do
Propagation.replace_scores(scores, valid_time)
results = Propagation.latest_scores(10_000)
assert length(results) == 2
assert Enum.count_until(results, 3) == 2
end
test "returns empty list when no data" do
@ -793,7 +793,7 @@ defmodule Microwaveprop.PropagationTest do
times = Enum.map(result, & &1.valid_time)
assert t3 in times
assert length(result) == 3
assert Enum.count_until(result, 4) == 3
end
end
@ -961,7 +961,7 @@ defmodule Microwaveprop.PropagationTest do
bounds = %{"south" => 20.0, "north" => 30.0, "west" => -130.0, "east" => -120.0}
fresh = Propagation.scores_at_fresh(10_000, valid_time, bounds)
assert length(fresh) == 1
assert Enum.count_until(fresh, 2) == 1
assert hd(fresh).lat == 25.0
end
end
@ -984,7 +984,7 @@ defmodule Microwaveprop.PropagationTest do
ScoreCache.sync()
assert {:ok, scores} = ScoreCache.fetch(10_000, valid_time)
assert length(scores) == 2
assert Enum.count_until(scores, 3) == 2
end
test "only warms the requested band" do
@ -1117,7 +1117,7 @@ defmodule Microwaveprop.PropagationTest do
end
rows = Propagation.list_recent_run_timings(limit: 10)
assert length(rows) == 3
assert Enum.count_until(rows, 4) == 3
assert Enum.map(rows, & &1.forecast_hour) == [2, 1, 0]
end
end

View file

@ -36,7 +36,7 @@ defmodule Microwaveprop.Radio.AdifImportTest do
describe "preview/2" do
test "parses a valid ADIF record" do
assert {:ok, preview} = AdifImport.preview(@valid_adif, @submitter)
assert length(preview.valid) == 1
assert Enum.count_until(preview.valid, 2) == 1
assert preview.invalid == []
assert preview.duplicates == []
assert preview.refinements == []
@ -179,7 +179,7 @@ defmodule Microwaveprop.Radio.AdifImportTest do
"""
assert {:ok, preview} = AdifImport.preview(adif, @submitter)
assert length(preview.valid) == 2
assert Enum.count_until(preview.valid, 3) == 2
end
test "skips ADIF header before <EOH>" do
@ -192,7 +192,7 @@ defmodule Microwaveprop.Radio.AdifImportTest do
"""
assert {:ok, preview} = AdifImport.preview(adif, @submitter)
assert length(preview.valid) == 1
assert Enum.count_until(preview.valid, 2) == 1
end
test "returns error for empty ADIF" do
@ -216,7 +216,7 @@ defmodule Microwaveprop.Radio.AdifImportTest do
assert {:ok, preview} = AdifImport.preview(adif, @submitter)
assert preview.valid == []
assert length(preview.invalid) == 1
assert Enum.count_until(preview.invalid, 2) == 1
end
test "skips HF contacts" do
@ -226,7 +226,7 @@ defmodule Microwaveprop.Radio.AdifImportTest do
assert {:ok, preview} = AdifImport.preview(adif, @submitter)
assert preview.valid == []
assert length(preview.invalid) == 1
assert Enum.count_until(preview.invalid, 2) == 1
end
test "accepts 2m contacts" do
@ -255,7 +255,7 @@ defmodule Microwaveprop.Radio.AdifImportTest do
assert {:ok, preview} = AdifImport.preview(@valid_adif, @submitter)
assert preview.valid == []
assert preview.refinements == []
assert length(preview.duplicates) == 1
assert Enum.count_until(preview.duplicates, 2) == 1
end
test "upload with longer grid than existing routes to refinements" do
@ -288,7 +288,7 @@ defmodule Microwaveprop.Radio.AdifImportTest do
"""
assert {:ok, preview} = AdifImport.preview(adif, @submitter)
assert length(preview.valid) == 1
assert Enum.count_until(preview.valid, 2) == 1
end
test "handles TIME_ON with 4 digits (HHMM)" do
@ -330,7 +330,7 @@ defmodule Microwaveprop.Radio.AdifImportTest do
"""
assert {:ok, preview} = AdifImport.preview(adif, @submitter)
assert length(preview.valid) == 1
assert Enum.count_until(preview.valid, 2) == 1
end
test "strips APP_ prefix from app-defined fields" do
@ -418,7 +418,7 @@ defmodule Microwaveprop.Radio.AdifImportTest do
"""
assert {:ok, preview} = AdifImport.preview(adif, @submitter)
assert length(preview.valid) == 1
assert Enum.count_until(preview.valid, 2) == 1
end
test "parses records containing UTF-8 characters in earlier fields" do

View file

@ -254,7 +254,7 @@ defmodule Microwaveprop.Radio.ContactEditTest do
Radio.create_contact_edit(contact, user, %{"grid2" => "EL29ab"})
edits = Radio.list_pending_edits()
assert length(edits) == 2
assert Enum.count_until(edits, 3) == 2
fields = Enum.flat_map(edits, &Map.keys(&1.proposed_changes))
assert "grid1" in fields

View file

@ -89,7 +89,7 @@ defmodule Microwaveprop.Radio.CsvImportTest do
)
assert {:ok, %{imported: imported, errors: []}} = CsvImport.import(csv, @submitter)
assert length(imported) == 2
assert Enum.count_until(imported, 3) == 2
end
test "trims whitespace from fields" do
@ -133,7 +133,7 @@ defmodule Microwaveprop.Radio.CsvImportTest do
)
assert {:ok, %{imported: imported, errors: []}} = CsvImport.import(csv, @submitter)
assert length(imported) == 2
assert Enum.count_until(imported, 3) == 2
end
test "contacts are persisted to the database" do
@ -249,7 +249,7 @@ defmodule Microwaveprop.Radio.CsvImportTest do
)
assert {:ok, %{imported: imported, errors: errors}} = CsvImport.import(csv, @submitter)
assert length(imported) == 2
assert Enum.count_until(imported, 3) == 2
assert [{3, _error_msgs}] = errors
end
@ -281,7 +281,7 @@ defmodule Microwaveprop.Radio.CsvImportTest do
)
assert {:ok, %{imported: imported, errors: errors}} = CsvImport.import(csv, @submitter)
assert length(imported) == 2
assert Enum.count_until(imported, 3) == 2
# Row 4 is the bad row (header=1, data=2, blank=3, bad=4, good=5)
assert [{4, _error_msgs}] = errors
end
@ -592,7 +592,7 @@ defmodule Microwaveprop.Radio.CsvImportTest do
assert {:ok, preview} = CsvImport.preview(csv, @submitter)
assert preview.valid == []
assert length(preview.invalid) == 2
assert Enum.count_until(preview.invalid, 3) == 2
assert Enum.all?(preview.invalid, &(&1.messages != []))
end
@ -647,7 +647,7 @@ defmodule Microwaveprop.Radio.CsvImportTest do
)
assert {:ok, preview} = CsvImport.preview(csv, @submitter)
assert length(preview.valid) == 2
assert Enum.count_until(preview.valid, 3) == 2
assert preview.duplicates == []
end
@ -663,7 +663,7 @@ defmodule Microwaveprop.Radio.CsvImportTest do
)
assert {:ok, preview} = CsvImport.preview(csv, @submitter)
assert length(preview.valid) == 2
assert Enum.count_until(preview.valid, 3) == 2
end
test "different grids are not duplicates (rover moved)" do
@ -678,7 +678,7 @@ defmodule Microwaveprop.Radio.CsvImportTest do
)
assert {:ok, preview} = CsvImport.preview(csv, @submitter)
assert length(preview.valid) == 2
assert Enum.count_until(preview.valid, 3) == 2
assert preview.refinements == []
end
@ -777,7 +777,7 @@ defmodule Microwaveprop.Radio.CsvImportTest do
@valid_header <> "\n" <> "W5XD,K5TR,EM13,EM00,10000,CW,2026-03-28T18:15:00Z"
assert {:ok, p2} = CsvImport.preview(rover_csv, @submitter)
assert length(p2.valid) == 1
assert Enum.count_until(p2.valid, 2) == 1
assert p2.refinements == []
assert p2.duplicates == []
end
@ -973,7 +973,7 @@ defmodule Microwaveprop.Radio.CsvImportTest do
assert_enqueued(worker: ContactImportWorker, args: %{"offset" => 0, "limit" => 100})
assert_enqueued(worker: ContactImportWorker, args: %{"offset" => 100, "limit" => 100})
assert_enqueued(worker: ContactImportWorker, args: %{"offset" => 200, "limit" => 50})
assert length(all_enqueued(worker: ContactImportWorker)) == 3
assert Enum.count_until(all_enqueued(worker: ContactImportWorker), 4) == 3
end
test "returns pre-run counters and status on the persisted run" do

View file

@ -50,7 +50,7 @@ defmodule Microwaveprop.RadioTest do
result = Radio.list_contacts()
assert length(result.entries) == 20
assert Enum.count_until(result.entries, 21) == 20
assert result.page == 1
assert result.total_pages == 2
assert result.total_entries == 25
@ -68,7 +68,7 @@ defmodule Microwaveprop.RadioTest do
result = Radio.list_contacts(page: 2)
assert length(result.entries) == 5
assert Enum.count_until(result.entries, 6) == 5
assert result.page == 2
assert result.total_pages == 2
end
@ -81,7 +81,7 @@ defmodule Microwaveprop.RadioTest do
result = Radio.list_contacts(per_page: 50)
assert length(result.entries) == 25
assert Enum.count_until(result.entries, 26) == 25
assert result.total_pages == 1
end
@ -93,7 +93,7 @@ defmodule Microwaveprop.RadioTest do
# 5000 is well above the 200 cap; should still return all 3 rows.
result = Radio.list_contacts(per_page: 5000)
assert length(result.entries) == 3
assert Enum.count_until(result.entries, 4) == 3
end
test "falls back to default when per_page is invalid" do
@ -103,7 +103,7 @@ defmodule Microwaveprop.RadioTest do
end
result = Radio.list_contacts(per_page: 0)
assert length(result.entries) == 20
assert Enum.count_until(result.entries, 21) == 20
end
end
@ -171,7 +171,7 @@ defmodule Microwaveprop.RadioTest do
create_contact(%{qso_timestamp: ts})
end
assert length(Radio.unprocessed_contacts(3)) == 3
assert Enum.count_until(Radio.unprocessed_contacts(3), 4) == 3
end
end
@ -300,7 +300,7 @@ defmodule Microwaveprop.RadioTest do
create_contact(%{qso_timestamp: ts})
end
assert length(Radio.unprocessed_hrrr_contacts(3)) == 3
assert Enum.count_until(Radio.unprocessed_hrrr_contacts(3), 4) == 3
end
end
@ -339,7 +339,7 @@ defmodule Microwaveprop.RadioTest do
results = Radio.unprocessed_terrain_contacts()
ids = Enum.map(results, & &1.id)
assert q1.id in ids
assert length(ids) == 1
assert Enum.count_until(ids, 2) == 1
end
test "excludes QSOs already marked as terrain_status" do
@ -364,7 +364,7 @@ defmodule Microwaveprop.RadioTest do
create_contact(%{qso_timestamp: ts})
end
assert length(Radio.unprocessed_terrain_contacts(3)) == 3
assert Enum.count_until(Radio.unprocessed_terrain_contacts(3), 4) == 3
end
end
@ -404,7 +404,7 @@ defmodule Microwaveprop.RadioTest do
points = Radio.contact_path_points(contact)
assert length(points) == 3
assert Enum.count_until(points, 4) == 3
[{lat1, lon1}, {mid_lat, mid_lon}, {lat2, lon2}] = points
assert lat1 == 32.9
assert lon1 == -97.0
@ -436,7 +436,7 @@ defmodule Microwaveprop.RadioTest do
})
points = Radio.contact_path_points(contact)
assert length(points) == 3
assert Enum.count_until(points, 4) == 3
end
test "computes a great-circle midpoint across the anti-meridian" do
@ -769,7 +769,7 @@ defmodule Microwaveprop.RadioTest do
result = Radio.list_contacts_involving_callsign("W5ISP")
assert length(result) == 100
assert Enum.count_until(result, 101) == 100
# The 100 newest by qso_timestamp correspond to i = 21..120; i=120 is newest.
assert hd(result).station2 == "K5X120"
end

View file

@ -26,7 +26,7 @@ defmodule Microwaveprop.Rover.CandidateDetailTest do
result =
CandidateDetail.summarize(candidate, stations, 10_000, ~U[2026-04-29 12:00:00Z], :ssb)
assert length(result.links) == 2
assert Enum.count_until(result.links, 3) == 2
Enum.each(result.links, fn link ->
assert byte_size(link.callsign) > 0

View file

@ -49,7 +49,7 @@ defmodule Microwaveprop.Rover.ComputeTest do
assert Map.has_key?(result, :top_candidates)
assert result.cells != []
assert length(result.top_candidates) <= 5
assert Enum.count_until(result.top_candidates, 6) <= 5
refute result.top_candidates == []
# Sorted by score desc.

View file

@ -17,7 +17,7 @@ defmodule Microwaveprop.RoverLocationsTest do
{:ok, _l2} = Rover.create_location(b, %{lat: 33.0, lon: -96.5, status: :bad})
result = Rover.list_locations()
assert length(result) == 2
assert Enum.count_until(result, 3) == 2
assert Enum.all?(result, &match?(%Location{}, &1))
end
end

View file

@ -113,7 +113,7 @@ defmodule Microwaveprop.RoverPlanningTest do
assert is_float(station.lon)
paths = RoverPlanning.list_paths(mission)
assert length(paths) == 1
assert Enum.count_until(paths, 2) == 1
assert hd(paths).status == :complete
end

View file

@ -83,7 +83,7 @@ defmodule Microwaveprop.RoverTest do
describe "default_stations/0" do
test "returns 3 hardcoded NTMS stations" do
stations = Rover.default_stations()
assert length(stations) == 3
assert Enum.count_until(stations, 4) == 3
callsigns = Enum.map(stations, & &1.callsign)
assert "W5LUA" in callsigns
assert "W5HN" in callsigns

View file

@ -9,7 +9,7 @@ defmodule Microwaveprop.SpaceWeather.SwpcClientTest do
body = File.read!("test/fixtures/swpc/kp_sample.json")
assert {:ok, rows} = SwpcClient.parse_kp(body)
assert length(rows) == 10
assert Enum.count_until(rows, 11) == 10
first = hd(rows)
assert first.valid_time == ~U[2026-04-15 13:49:00Z]
@ -53,7 +53,7 @@ defmodule Microwaveprop.SpaceWeather.SwpcClientTest do
body = File.read!("test/fixtures/swpc/f107_sample.json")
assert {:ok, rows} = SwpcClient.parse_f107(body)
assert length(rows) == 10
assert Enum.count_until(rows, 11) == 10
first = hd(rows)
assert first.valid_time == ~U[2026-04-15 17:00:00Z]
@ -77,7 +77,7 @@ defmodule Microwaveprop.SpaceWeather.SwpcClientTest do
# Fixture has 12 records (6 timestamps x 2 bands). We keep the
# long-wavelength band (0.1-0.8nm) which is the standard SWF /
# D-layer absorption proxy.
assert length(rows) == 6
assert Enum.count_until(rows, 7) == 6
assert Enum.all?(rows, fn r -> r.energy_band == "0.1-0.8nm" end)
first = hd(rows)

View file

@ -7,7 +7,7 @@ defmodule Microwaveprop.Terrain.ElevationClientTest do
test "generates N+1 evenly spaced points" do
points = ElevationClient.sample_path(32.9, -97.0, 30.3, -97.7, 4)
assert length(points) == 5
assert Enum.count_until(points, 6) == 5
first = hd(points)
last = List.last(points)
@ -40,7 +40,7 @@ defmodule Microwaveprop.Terrain.ElevationClientTest do
assert {:ok, profile} =
ElevationClient.fetch_elevation_profile(32.9, -97.0, 30.3, -97.7, 2)
assert length(profile) == 3
assert Enum.count_until(profile, 4) == 3
first = hd(profile)
assert Map.has_key?(first, :lat)
@ -67,7 +67,7 @@ defmodule Microwaveprop.Terrain.ElevationClientTest do
ElevationClient.fetch_elevation_profile(32.9, -97.0, 30.3, -97.7, 150)
# 151 points → 2 batches (100 + 51)
assert length(profile) == 151
assert Enum.count_until(profile, 152) == 151
assert :counters.get(call_count, 1) == 2
end
@ -97,7 +97,7 @@ defmodule Microwaveprop.Terrain.ElevationClientTest do
assert {:ok, profile} =
ElevationClient.fetch_elevation_profile(32.9, -97.0, 30.3, -97.7, 2)
assert length(profile) == 3
assert Enum.count_until(profile, 4) == 3
assert hd(profile).elev == 100.0
end
@ -141,7 +141,7 @@ defmodule Microwaveprop.Terrain.ElevationClientSrtmTest do
assert {:ok, profile} =
ElevationClient.fetch_elevation_profile(32.78, -96.8, 32.5, -96.5, 4)
assert length(profile) == 5
assert Enum.count_until(profile, 6) == 5
assert hd(profile).elev > 0
end
@ -152,7 +152,7 @@ defmodule Microwaveprop.Terrain.ElevationClientSrtmTest do
assert {:ok, profile} =
ElevationClient.fetch_elevation_profile(32.9, -97.0, 30.3, -97.7, 2)
assert length(profile) == 3
assert Enum.count_until(profile, 4) == 3
assert Enum.all?(profile, fn p -> p.elev == 0 end)
end
end

View file

@ -158,7 +158,7 @@ defmodule Microwaveprop.Terrain.SrtmTest do
assert {:ok, profile} =
Srtm.fetch_elevation_profile(32.78, -96.8, 32.5, -96.5, @tiles_dir, 10)
assert length(profile) == 11
assert Enum.count_until(profile, 12) == 11
first = hd(profile)
assert Map.has_key?(first, :lat)

View file

@ -139,7 +139,7 @@ defmodule Microwaveprop.ValkeyTest do
test "returns :ok when all SETs succeed" do
expect(MockAdapter, :pipeline, fn _conn, cmds, _opts ->
assert length(cmds) == 2
assert Enum.count_until(cmds, 3) == 2
assert cmds |> hd() |> List.first() == "SET"
{:ok, ["OK", "OK"]}
end)

View file

@ -140,7 +140,7 @@ defmodule Microwaveprop.Weather.GefsClientTest do
test "builds a pressure-level profile with Td derived from RH at each level" do
p = GefsClient.build_profile(@raw)
assert length(p.profile) == 2
assert Enum.count_until(p.profile, 3) == 2
l1000 = Enum.find(p.profile, &(&1["pres"] == 1000.0))
assert_in_delta l1000["tmpc"], 20.0, 0.01

View file

@ -85,7 +85,7 @@ defmodule Microwaveprop.Weather.GefsProfileTest do
assert count == 3
rows = Microwaveprop.Repo.all(GefsProfile)
assert length(rows) == 3
assert Enum.count_until(rows, 4) == 3
end
test "skips conflicting rows quietly" do

View file

@ -14,7 +14,7 @@ defmodule Microwaveprop.Weather.Grib2.ExtractorTest do
blob = msg1 <> msg2
messages = Extractor.split_messages(blob)
assert length(messages) == 2
assert Enum.count_until(messages, 3) == 2
assert Enum.at(messages, 0) == msg1
assert Enum.at(messages, 1) == msg2
end
@ -22,7 +22,7 @@ defmodule Microwaveprop.Weather.Grib2.ExtractorTest do
test "handles single message" do
msg = build_synthetic_grib2_message(0, 0, 0, 103, 2)
messages = Extractor.split_messages(msg)
assert length(messages) == 1
assert Enum.count_until(messages, 2) == 1
end
test "handles empty binary" do

View file

@ -192,7 +192,7 @@ defmodule Microwaveprop.Weather.Grib2.Wgrib2Test do
{:ok, grid} = Wgrib2.extract_grid(fixture, ":(TMP|DPT):", @dallas_grid)
# hrrr_multi.grib2 contains exactly TMP:2m and DPT:2m.
assert length(messages) == 2
assert Enum.count_until(messages, 3) == 2
vars = messages |> Enum.map(& &1.var) |> Enum.sort()
assert vars == ["DPT", "TMP"]

View file

@ -34,13 +34,13 @@ defmodule Microwaveprop.Weather.GridCacheTest do
test "returns all rows when bounds are nil" do
assert {:ok, list} = GridCache.fetch_bounds(~U[2026-04-12 12:00:00Z], nil)
assert length(list) == 3
assert Enum.count_until(list, 4) == 3
end
test "returns only rows inside the bounds" do
bounds = %{"south" => 31.0, "north" => 35.0, "west" => -100.0, "east" => -95.0}
assert {:ok, list} = GridCache.fetch_bounds(~U[2026-04-12 12:00:00Z], bounds)
assert length(list) == 2
assert Enum.count_until(list, 3) == 2
end
test "returns :miss when the valid_time is not cached" do

View file

@ -173,7 +173,7 @@ defmodule Microwaveprop.Weather.GridCacheValkey.DataCacheTest do
test "fetch returns decoded rows" do
result = GridCache.fetch(~U[2026-05-01 12:00:00Z])
assert {:ok, rows} = result
assert length(rows) == 2
assert Enum.count_until(rows, 3) == 2
assert hd(rows).lat == 32.0
assert hd(rows).temperature == 25.0
end
@ -181,7 +181,7 @@ defmodule Microwaveprop.Weather.GridCacheValkey.DataCacheTest do
test "fetch_bounds filters rows within bounds" do
bounds = %{"south" => 30.0, "north" => 35.0, "west" => -100.0, "east" => -95.0}
assert {:ok, rows} = GridCache.fetch_bounds(~U[2026-05-01 12:00:00Z], bounds)
assert length(rows) == 1
assert Enum.count_until(rows, 2) == 1
assert hd(rows).lat == 32.0
end

View file

@ -146,7 +146,7 @@ defmodule Microwaveprop.Weather.HrdpsClientTest do
}
profile = HrdpsClient.build_profile_from_extracted(extracted)
assert length(profile.profile) == 1
assert Enum.count_until(profile.profile, 2) == 1
assert hd(profile.profile)["pres"] == 700.0
end
end

View file

@ -94,7 +94,7 @@ defmodule Microwaveprop.Weather.HrrrClientTest do
test "parses idx into structured list" do
entries = HrrrClient.parse_idx(@sample_idx)
assert length(entries) == 6
assert Enum.count_until(entries, 7) == 6
first = hd(entries)
assert first.msg == 1
@ -129,7 +129,7 @@ defmodule Microwaveprop.Weather.HrrrClientTest do
entries = HrrrClient.parse_idx(input)
assert length(entries) == 2
assert Enum.count_until(entries, 3) == 2
assert Enum.map(entries, & &1.msg) == [1, 2]
assert Enum.map(entries, & &1.var) == ["TMP", "DPT"]
end
@ -151,7 +151,7 @@ defmodule Microwaveprop.Weather.HrrrClientTest do
ranges = HrrrClient.byte_ranges_for_messages(entries, wanted)
assert length(ranges) == 2
assert Enum.count_until(ranges, 3) == 2
assert {0, 999} in ranges
assert {2000, 2999} in ranges
end
@ -167,7 +167,7 @@ defmodule Microwaveprop.Weather.HrrrClientTest do
ranges = HrrrClient.byte_ranges_for_messages(entries, wanted)
# Last message - uses a large end offset
assert length(ranges) == 1
assert Enum.count_until(ranges, 2) == 1
[{start, _end}] = ranges
assert start == 1000
end
@ -329,7 +329,7 @@ defmodule Microwaveprop.Weather.HrrrClientTest do
assert_in_delta result.surface_pressure_mb, 1013.5, 0.1
assert result.hpbl_m == 1500.0
assert result.pwat_mm == 25.0
assert length(result.profile) == 3
assert Enum.count_until(result.profile, 4) == 3
first_level = hd(result.profile)
assert first_level["pres"] == 1000.0
@ -392,7 +392,7 @@ defmodule Microwaveprop.Weather.HrrrClientTest do
}
result = HrrrClient.build_profile(parsed)
assert length(result.profile) == 1
assert Enum.count_until(result.profile, 2) == 1
end
test "includes upper-air levels above 700 mb for the skew-T plot" do
@ -435,7 +435,7 @@ defmodule Microwaveprop.Weather.HrrrClientTest do
messages = HrrrClient.pressure_messages(:grid)
# 13 levels × 3 vars (TMP/DPT/HGT)
assert length(messages) == 39
assert Enum.count_until(messages, 40) == 39
levels =
messages
@ -463,7 +463,7 @@ defmodule Microwaveprop.Weather.HrrrClientTest do
messages = HrrrClient.pressure_messages(:profile)
# 25 levels × 3 vars
assert length(messages) == 75
assert Enum.count_until(messages, 76) == 75
levels =
messages
@ -494,7 +494,7 @@ defmodule Microwaveprop.Weather.HrrrClientTest do
test "returns list of surface message descriptors" do
messages = HrrrClient.surface_messages()
assert length(messages) == 9
assert Enum.count_until(messages, 10) == 9
vars = Enum.map(messages, & &1.var)
assert "TMP" in vars

View file

@ -29,7 +29,7 @@ defmodule Microwaveprop.Weather.HrrrNativeClientTest do
describe "native_messages/0" do
test "lists 350 messages (7 vars x 50 levels)" do
messages = HrrrNativeClient.native_messages()
assert length(messages) == 350
assert Enum.count_until(messages, 351) == 350
end
test "every level appears with every essential variable" do
@ -147,7 +147,7 @@ defmodule Microwaveprop.Weather.HrrrNativeClientTest do
ranges = HrrrNativeClient.essential_byte_ranges(idx_entries)
# Expect 3 ranges (the three essentials we actually have)
assert length(ranges) == 3
assert Enum.count_until(ranges, 4) == 3
# Each range is {start, end} tuples, all integers
Enum.each(ranges, fn {s, e} ->
assert is_integer(s) and is_integer(e)
@ -175,7 +175,7 @@ defmodule Microwaveprop.Weather.HrrrNativeClientTest do
test "native_variables lists exactly the 7 native-grid fields" do
vars = HrrrNativeClient.native_variables()
assert length(vars) == 7
assert Enum.count_until(vars, 8) == 7
assert "TMP" in vars
assert "SPFH" in vars
assert "HGT" in vars
@ -248,7 +248,7 @@ defmodule Microwaveprop.Weather.HrrrNativeClientTest do
ranges = HrrrNativeClient.duct_byte_ranges(idx_entries)
# UGRD is not in the duct set, so only 3 ranges should come back.
assert length(ranges) == 3
assert Enum.count_until(ranges, 4) == 3
Enum.each(ranges, fn {s, e} ->
assert is_integer(s) and is_integer(e)

View file

@ -63,7 +63,7 @@ defmodule Microwaveprop.Weather.HrrrNativeProfileTest do
levels = HrrrNativeProfile.to_skew_t_profile(profile)
assert length(levels) == 3
assert Enum.count_until(levels, 4) == 3
# Ordered surface-first (descending pressure).
pressures = Enum.map(levels, & &1["pres"])
@ -104,7 +104,7 @@ defmodule Microwaveprop.Weather.HrrrNativeProfileTest do
profile = struct(HrrrNativeProfile, attrs)
levels = HrrrNativeProfile.to_skew_t_profile(profile)
assert length(levels) == 1
assert Enum.count_until(levels, 2) == 1
assert hd(levels)["hght"] == 10.0
end
end

View file

@ -42,7 +42,7 @@ defmodule Microwaveprop.Weather.HrrrProfileLookupTest do
end
times = HrrrProfileLookup.list_valid_times_near(32.65, -97.13, limit: 3)
assert length(times) == 3
assert Enum.count_until(times, 4) == 3
# Capped to the 3 most recent — but still sorted ascending in the
# returned list so SkewtLive's selector renders left-to-right.
assert Enum.map(times, &DateTime.to_iso8601/1) == [
@ -59,8 +59,8 @@ defmodule Microwaveprop.Weather.HrrrProfileLookupTest do
tight = HrrrProfileLookup.list_valid_times_near(32.625, -97.125, tolerance_deg: 0.07)
wide = HrrrProfileLookup.list_valid_times_near(32.625, -97.125, tolerance_deg: 0.25)
assert length(tight) == 1
assert length(wide) == 2
assert Enum.count_until(tight, 2) == 1
assert Enum.count_until(wide, 3) == 2
end
end
@ -80,7 +80,7 @@ defmodule Microwaveprop.Weather.HrrrProfileLookupTest do
# Default fixture is the 13-level @full_profile (HRRR's standard
# canonical pressure-level set).
assert length(profile) == 13
assert Enum.count_until(profile, 14) == 13
[%{"pres" => pres} | _] = profile
assert pres in [1000.0, 1000]
assert cell[:surface_pressure_mb] == 1013.0
@ -140,7 +140,7 @@ defmodule Microwaveprop.Weather.HrrrProfileLookupTest do
# Threshold of 0 → both rows.
both = HrrrProfileLookup.list_valid_times_near(32.65, -97.13, min_profile_levels: 0)
assert length(both) == 2
assert Enum.count_until(both, 3) == 2
end
test "read_point_near returns nil for partial-profile valid_times under the default threshold" do

View file

@ -51,7 +51,7 @@ defmodule Microwaveprop.Weather.HrrrProfileTest do
test "persists to the database with JSONB profile" do
changeset = HrrrProfile.changeset(%HrrrProfile{}, @valid_attrs)
assert {:ok, profile} = Repo.insert(changeset)
assert length(profile.profile) == 3
assert Enum.count_until(profile.profile, 4) == 3
assert hd(profile.profile)["pres"] == 1000.0
assert profile.ducting_detected == false
assert profile.hpbl_m == 1500.0

View file

@ -13,7 +13,7 @@ defmodule Microwaveprop.Weather.IemClientTest do
"""
rows = IemClient.parse_asos_csv(csv)
assert length(rows) == 2
assert Enum.count_until(rows, 3) == 2
first = hd(rows)
assert first.temp_f == 75.0
@ -40,7 +40,7 @@ defmodule Microwaveprop.Weather.IemClientTest do
"""
rows = IemClient.parse_asos_csv(csv)
assert length(rows) == 1
assert Enum.count_until(rows, 2) == 1
first = hd(rows)
assert first.temp_f == nil
@ -59,7 +59,7 @@ defmodule Microwaveprop.Weather.IemClientTest do
"""
rows = IemClient.parse_asos_csv(csv)
assert length(rows) == 1
assert Enum.count_until(rows, 2) == 1
end
end
@ -79,11 +79,11 @@ defmodule Microwaveprop.Weather.IemClientTest do
}
result = IemClient.parse_raob_json(json)
assert length(result) == 1
assert Enum.count_until(result, 2) == 1
sounding = hd(result)
assert sounding.observed_at == ~U[2026-03-28 12:00:00Z]
assert length(sounding.profile) == 2
assert Enum.count_until(sounding.profile, 3) == 2
assert hd(sounding.profile)["pres"] == 1013.0
end
@ -142,7 +142,7 @@ defmodule Microwaveprop.Weather.IemClientTest do
"""
rows = IemClient.parse_asos_csv(csv)
assert length(rows) == 2
assert Enum.count_until(rows, 3) == 2
codes = Enum.map(rows, & &1.station_code)
assert "KDFW" in codes
@ -173,8 +173,8 @@ defmodule Microwaveprop.Weather.IemClientTest do
)
assert map_size(grouped) > 0
assert length(Map.fetch!(grouped, "KDFW")) == 2
assert length(Map.fetch!(grouped, "KFTW")) == 1
assert Enum.count_until(Map.fetch!(grouped, "KDFW"), 3) == 2
assert Enum.count_until(Map.fetch!(grouped, "KFTW"), 2) == 1
# Stations in the request but absent from the response get an empty list
# so callers can distinguish "no data" from "not asked about this code".
assert Map.fetch!(grouped, "KSPS") == []
@ -273,7 +273,7 @@ defmodule Microwaveprop.Weather.IemClientTest do
assert {:ok, [sounding]} = IemClient.fetch_raob("FWD", ~U[2026-03-28 12:00:00Z])
assert sounding.observed_at == ~U[2026-03-28 12:00:00Z]
assert length(sounding.profile) == 1
assert Enum.count_until(sounding.profile, 2) == 1
end
test "returns error on non-200 status" do
@ -308,7 +308,7 @@ defmodule Microwaveprop.Weather.IemClientTest do
end)
assert {:ok, observations} = IemClient.fetch_current_asos(["TX_ASOS"])
assert length(observations) == 1
assert Enum.count_until(observations, 2) == 1
assert hd(observations).station_code == "KDFW"
assert hd(observations).temp_f == 75.0
end
@ -346,7 +346,7 @@ defmodule Microwaveprop.Weather.IemClientTest do
}
stations = IemClient.parse_network_json(json)
assert length(stations) == 2
assert Enum.count_until(stations, 3) == 2
first = hd(stations)
assert first.station_code == "KJFK"
@ -380,7 +380,7 @@ defmodule Microwaveprop.Weather.IemClientTest do
}
stations = IemClient.parse_network_json(json)
assert length(stations) == 1
assert Enum.count_until(stations, 2) == 1
station = hd(stations)
assert station.station_code == "KDFW"
@ -406,7 +406,7 @@ defmodule Microwaveprop.Weather.IemClientTest do
}
result = IemClient.parse_iemre_json(json)
assert length(result) == 2
assert Enum.count_until(result, 3) == 2
assert hd(result)["utc_hour"] == 0
end
@ -444,7 +444,7 @@ defmodule Microwaveprop.Weather.IemClientTest do
end)
assert {:ok, data} = IemClient.fetch_iemre(32.875, -97.0, ~D[2026-03-28])
assert length(data) == 2
assert Enum.count_until(data, 3) == 2
assert Enum.at(data, 1)["p01m_mm"] == 1.2
end
@ -520,7 +520,7 @@ defmodule Microwaveprop.Weather.IemClientTest do
"""
rows = IemClient.parse_asos_csv(csv)
assert length(rows) == 1
assert Enum.count_until(rows, 2) == 1
end
test "returns [] for an empty string body" do

View file

@ -41,7 +41,7 @@ defmodule Microwaveprop.Weather.IemreObservationTest do
assert obs.lat == 32.875
assert obs.lon == -97.0
assert obs.date == ~D[2026-03-28]
assert length(obs.hourly) == 2
assert Enum.count_until(obs.hourly, 3) == 2
end
test "enforces unique constraint on lat, lon, date" do

View file

@ -222,7 +222,7 @@ defmodule Microwaveprop.Weather.NarrClientTest do
assert_in_delta attrs.pwat_mm, 45.1, 1.0
# Profile array — fixture only contains 850 mb, so length is 1
assert length(attrs.profile) == 1
assert Enum.count_until(attrs.profile, 2) == 1
[level_850] = attrs.profile
assert_in_delta level_850["pres"], 850.0, 0.1
# 291.52 K
@ -287,7 +287,7 @@ defmodule Microwaveprop.Weather.NarrClientTest do
# Profile — 900 hPa has HGT+TMP+SPFH? No, only TMP is there at 900. Only
# 850 hPa has all three (TMP, HGT, SPFH) → one profile entry.
assert length(attrs.profile) == 1
assert Enum.count_until(attrs.profile, 2) == 1
[level_850] = attrs.profile
assert_in_delta level_850["pres"], 850.0, 0.01
assert_in_delta level_850["tmpc"], 18.37, 0.5

View file

@ -27,7 +27,7 @@ defmodule Microwaveprop.Weather.NceiMetarClientTest do
"""
obs = NceiMetarClient.parse(text)
assert length(obs) == 2
assert Enum.count_until(obs, 3) == 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

View file

@ -302,7 +302,7 @@ defmodule Microwaveprop.Weather.NexradClientTest do
# DFW area within the 30x30 image
{:ok, observations} = NexradClient.fetch_frame(~U[2022-08-20 14:05:00Z], [{32.9, -97.0}])
assert length(observations) == 1
assert Enum.count_until(observations, 2) == 1
obs = hd(observations)
assert_in_delta obs.lat, 32.9, 0.01
assert_in_delta obs.lon, -97.0, 0.01

View file

@ -98,7 +98,7 @@ defmodule Microwaveprop.Weather.ScalarFileTest do
ScalarFile.write!(valid_time, rows)
assert length(ScalarFile.read_bounds(valid_time, nil)) == 2
assert Enum.count_until(ScalarFile.read_bounds(valid_time, nil), 3) == 2
end
test "preserves the full set of derived layer fields on round-trip" do
@ -165,7 +165,7 @@ defmodule Microwaveprop.Weather.ScalarFileTest do
rows = ScalarFile.read_bounds(vt, nil)
sorted = Enum.sort_by(rows, & &1.lat)
assert length(sorted) == 2
assert Enum.count_until(sorted, 3) == 2
assert Enum.at(sorted, 0).lat == 33.0
assert Enum.at(sorted, 1).lat == 53.0
end

View file

@ -66,7 +66,7 @@ defmodule Microwaveprop.Weather.SkewtParamsTest do
assert is_number(params[:lapse_rate_700_500_c_per_km])
assert is_number(params[:lapse_rate_sfc_3km_c_per_km])
# Parcel trajectory used by SkewtSvg to draw the dashed grey line.
assert length(params[:parcel_trace]) >= 2
assert Enum.count_until(params[:parcel_trace], 2) == 2
end
test "summer profile is unstable: SBCAPE > 0, SBCIN <= 0, LFC < surface" do

View file

@ -52,7 +52,7 @@ defmodule Microwaveprop.Weather.SolarClientTest do
describe "parse_gfz_file/1" do
test "skips comment lines and parses data rows" do
result = SolarClient.parse_gfz_file(@sample_gfz_file)
assert length(result) == 2
assert Enum.count_until(result, 3) == 2
[first, second] = result
assert first.date == ~D[2024-06-15]
@ -75,14 +75,14 @@ defmodule Microwaveprop.Weather.SolarClientTest do
records = SolarClient.parse_gfz_file(@sample_gfz_file)
# Records are 2024-06-15 and 2024-06-16
filtered = SolarClient.filter_since(records, ~D[2024-06-16])
assert length(filtered) == 1
assert Enum.count_until(filtered, 2) == 1
assert hd(filtered).date == ~D[2024-06-16]
end
test "returns all records when since_date is before all records" do
records = SolarClient.parse_gfz_file(@sample_gfz_file)
filtered = SolarClient.filter_since(records, ~D[2020-01-01])
assert length(filtered) == 2
assert Enum.count_until(filtered, 3) == 2
end
test "returns empty list when since_date is after all records" do
@ -150,7 +150,7 @@ defmodule Microwaveprop.Weather.SolarClientTest do
test "includes the boundary day itself (>= semantics)" do
records = SolarClient.parse_gfz_file(@sample_gfz_file)
filtered = SolarClient.filter_since(records, ~D[2024-06-15])
assert length(filtered) == 2
assert Enum.count_until(filtered, 3) == 2
end
test "accepts an already-empty record list" do

View file

@ -78,7 +78,7 @@ defmodule Microwaveprop.Weather.SoundingTest do
attrs = Map.put(@valid_sounding_attrs, :station_id, station.id)
changeset = Sounding.changeset(%Sounding{}, attrs)
assert {:ok, sounding} = Repo.insert(changeset)
assert length(sounding.profile) == 3
assert Enum.count_until(sounding.profile, 4) == 3
assert hd(sounding.profile)["pres"] == 1013.0
assert sounding.ducting_detected == false
assert sounding.k_index == 28.0

View file

@ -29,7 +29,7 @@ defmodule Microwaveprop.Weather.UwyoSoundingClientTest do
test "returns one sounding entry for a valid response" do
assert [sounding] = UwyoSoundingClient.parse_sounding_html(@fixture)
assert sounding.observed_at == ~U[2026-04-13 00:00:00Z]
assert length(sounding.profile) > 20
assert Enum.count_until(sounding.profile, 21) == 21
end
test "parses the surface level with all fields populated" do
@ -73,7 +73,7 @@ defmodule Microwaveprop.Weather.UwyoSoundingClientTest do
"""
[sounding] = UwyoSoundingClient.parse_sounding_html(html)
assert length(sounding.profile) == 1
assert Enum.count_until(sounding.profile, 2) == 1
assert hd(sounding.profile)["pres"] == 920.0
end
@ -175,7 +175,7 @@ defmodule Microwaveprop.Weather.UwyoSoundingClientTest do
assert [sounding] = UwyoSoundingClient.parse_sounding_html(html)
# Only the well-formed row survived.
assert length(sounding.profile) == 1
assert Enum.count_until(sounding.profile, 2) == 1
assert hd(sounding.profile)["pres"] == 1000.0
end
end

View file

@ -166,7 +166,7 @@ defmodule Microwaveprop.WeatherExtraTest do
test "early-morning times bracket prev-day 12Z and same-day 00Z" do
times = Weather.sounding_times_around(~U[2026-04-20 06:00:00Z])
assert length(times) >= 2
assert Enum.count_until(times, 2) == 2
assert ~U[2026-04-19 12:00:00Z] in times
assert ~U[2026-04-20 00:00:00Z] in times
end

View file

@ -74,7 +74,7 @@ defmodule Microwaveprop.WeatherGridTest do
bounds = %{"south" => 32.0, "north" => 33.0, "west" => -98.0, "east" => -97.0}
result = Weather.load_weather_grid(bounds)
assert length(result) == 2
assert Enum.count_until(result, 3) == 2
point = Enum.find(result, &(&1.lat == 32.125))
assert point.temperature == 25.0
assert point.dewpoint_depression == 7.0
@ -118,7 +118,7 @@ defmodule Microwaveprop.WeatherGridTest do
bounds = %{"south" => 32.0, "north" => 33.0, "west" => -98.0, "east" => -97.0}
result = Weather.load_weather_grid(bounds)
assert length(result) == 1
assert Enum.count_until(result, 2) == 1
assert hd(result).temperature == 28.0
end
@ -212,7 +212,7 @@ defmodule Microwaveprop.WeatherGridTest do
result = Weather.build_grid_cache_rows(grid_data, now)
assert length(result) == 2
assert Enum.count_until(result, 3) == 2
point = Enum.find(result, &(&1.lat == 32.125))
assert point.lon == -97.375
@ -293,7 +293,7 @@ defmodule Microwaveprop.WeatherGridTest do
result = Weather.build_grid_cache_rows(grid_data, now)
assert length(result) == 1
assert Enum.count_until(result, 2) == 1
assert hd(result).lat == 32.375
end
@ -329,7 +329,7 @@ defmodule Microwaveprop.WeatherGridTest do
bounds = %{"south" => 32.0, "north" => 33.0, "west" => -98.0, "east" => -97.0}
rows = Weather.build_grid_cache_rows(grid_data, now, bounds)
assert length(rows) == 1
assert Enum.count_until(rows, 2) == 1
assert hd(rows).lat == 32.125
assert hd(rows).lon == -97.375
end
@ -361,7 +361,7 @@ defmodule Microwaveprop.WeatherGridTest do
}
}
assert length(Weather.build_grid_cache_rows(grid_data, now, nil)) == 2
assert Enum.count_until(Weather.build_grid_cache_rows(grid_data, now, nil), 3) == 2
end
test "explicit ducting_detected: false on the profile wins over derived true from sounding params" do
@ -569,7 +569,7 @@ defmodule Microwaveprop.WeatherGridTest do
bounds = %{"south" => 32.0, "north" => 33.0, "west" => -98.0, "east" => -97.0}
rows = Weather.load_weather_grid(bounds)
assert length(rows) == 2
assert Enum.count_until(rows, 3) == 2
assert Enum.any?(rows, &(&1.temperature == 25.0))
assert Enum.any?(rows, &(&1.temperature == 26.0))
end
@ -683,11 +683,11 @@ defmodule Microwaveprop.WeatherGridTest do
# First call: miss → reads ScalarFile → populates GridCache.
bounds = %{"south" => 32.0, "north" => 33.0, "west" => -98.0, "east" => -97.0}
rows = Weather.weather_grid_at(vt, bounds)
assert length(rows) == 2
assert Enum.count_until(rows, 3) == 2
# GridCache is now warm — confirms cache populate happened.
assert {:ok, cached_rows} = GridCache.fetch(vt)
assert length(cached_rows) == 2
assert Enum.count_until(cached_rows, 3) == 2
# Wipe the on-disk ScalarFile. The next call should still return data
# because GridCache is the hot path.
@ -695,7 +695,7 @@ defmodule Microwaveprop.WeatherGridTest do
refute ScalarFile.exists?(vt)
rows_again = Weather.weather_grid_at(vt, bounds)
assert length(rows_again) == 2
assert Enum.count_until(rows_again, 3) == 2
end
end

View file

@ -476,7 +476,7 @@ defmodule Microwaveprop.WeatherTest do
time_window_hours: 3
)
assert length(result.surface_observations) == 1
assert Enum.count_until(result.surface_observations, 2) == 1
obs = hd(result.surface_observations)
assert obs.temp_f == 75.0
assert obs.station.station_code == "KDFW"
@ -500,7 +500,7 @@ defmodule Microwaveprop.WeatherTest do
time_window_hours: 6
)
assert length(result.soundings) == 1
assert Enum.count_until(result.soundings, 2) == 1
assert hd(result.soundings).station.station_code == "FWD"
end
@ -834,7 +834,7 @@ defmodule Microwaveprop.WeatherTest do
assert record.sfi == 150.2
assert record.sunspot_number == 120
assert record.ap_index == 8
assert length(record.kp_values) == 8
assert Enum.count_until(record.kp_values, 9) == 8
end
test "updates existing record on date conflict" do
@ -941,7 +941,7 @@ defmodule Microwaveprop.WeatherTest do
assert Weather.purge_grid_point_profiles() == 2
remaining = Repo.all(HrrrProfile)
assert length(remaining) == 1
assert Enum.count_until(remaining, 2) == 1
assert hd(remaining).id == qso_linked.id
end
@ -1160,7 +1160,7 @@ defmodule Microwaveprop.WeatherTest do
}
profiles = Weather.hrrr_profiles_for_path(contact)
assert length(profiles) >= 2
assert Enum.count_until(profiles, 2) == 2
end
test "returns empty list when no HRRR profiles exist nearby" do
@ -1183,7 +1183,7 @@ defmodule Microwaveprop.WeatherTest do
}
profiles = Weather.hrrr_profiles_for_path(contact)
assert length(profiles) == 1
assert Enum.count_until(profiles, 2) == 1
end
end
@ -1206,7 +1206,7 @@ defmodule Microwaveprop.WeatherTest do
}
observations = Weather.iemre_for_path(contact)
assert length(observations) >= 2
assert Enum.count_until(observations, 2) == 2
end
test "returns empty list when no observations exist" do
@ -1229,7 +1229,7 @@ defmodule Microwaveprop.WeatherTest do
}
observations = Weather.iemre_for_path(contact)
assert length(observations) == 1
assert Enum.count_until(observations, 2) == 1
end
end
@ -1379,7 +1379,7 @@ defmodule Microwaveprop.WeatherTest do
rows = Weather.build_grid_cache_rows(grid_data, ~U[2026-04-29 12:00:00Z], bounds)
# Only the (32, -97) point is inside bounds.
assert length(rows) == 1
assert Enum.count_until(rows, 2) == 1
end
end
end

View file

@ -248,7 +248,7 @@ defmodule Microwaveprop.Workers.ContactImportWorkerTest do
{:ok, preview} = CsvImport.preview(csv, @submitter)
assert preview.valid == []
assert length(preview.duplicates) == 1
assert Enum.count_until(preview.duplicates, 2) == 1
{:ok, run_id} = CsvImport.enqueue(preview, @submitter)
@ -269,14 +269,14 @@ defmodule Microwaveprop.Workers.ContactImportWorkerTest do
])
{:ok, preview} = CsvImport.preview(csv, @submitter)
assert length(preview.valid) == 1
assert length(preview.duplicates) == 1
assert Enum.count_until(preview.valid, 2) == 1
assert Enum.count_until(preview.duplicates, 2) == 1
{:ok, run_id} = CsvImport.enqueue(preview, @submitter)
run = Repo.get!(ImportRun, run_id)
assert run.total_rows == 1
assert length(run.rows["rows"]) == 1
assert Enum.count_until(run.rows["rows"], 2) == 1
assert Repo.aggregate(Contact, :count) == 1
end

View file

@ -100,7 +100,7 @@ defmodule Microwaveprop.Workers.ContactWeatherEnqueueWorkerTest do
kdfw_jobs =
Enum.filter(day_jobs, fn j -> j.changes.args["station_code"] == "KDFW" end)
assert length(kdfw_jobs) == 1
assert Enum.count_until(kdfw_jobs, 2) == 1
end
test "emits both UTC days when the window crosses midnight" do
@ -299,7 +299,7 @@ defmodule Microwaveprop.Workers.ContactWeatherEnqueueWorkerTest do
assert [row] = rows
# pos1 + midpoint + pos2 → 3 distinct HRRR grid cells in the batch.
assert length(row.points) == 3
assert Enum.count_until(row.points, 4) == 3
assert row.status == "queued"
end
@ -373,7 +373,7 @@ defmodule Microwaveprop.Workers.ContactWeatherEnqueueWorkerTest do
rows = Repo.all(from t in "hrrr_fetch_tasks", select: %{vt: t.valid_time, points: t.points})
assert [row] = rows
assert length(row.points) == 2
assert Enum.count_until(row.points, 3) == 2
end
test "re-enqueuing a 'done' row flips status back to queued" do
@ -397,7 +397,7 @@ defmodule Microwaveprop.Workers.ContactWeatherEnqueueWorkerTest do
jobs = ContactWeatherEnqueueWorker.build_terrain_jobs([contact])
assert length(jobs) == 1
assert Enum.count_until(jobs, 2) == 1
job = hd(jobs)
assert job.changes.args["contact_id"] == contact.id
end
@ -406,7 +406,7 @@ defmodule Microwaveprop.Workers.ContactWeatherEnqueueWorkerTest do
contact = create_contact()
jobs = ContactWeatherEnqueueWorker.build_terrain_jobs([contact, contact])
assert length(jobs) == 1
assert Enum.count_until(jobs, 2) == 1
end
end
@ -465,7 +465,7 @@ defmodule Microwaveprop.Workers.ContactWeatherEnqueueWorkerTest do
jobs = ContactWeatherEnqueueWorker.build_iemre_jobs([contact])
# pos1, midpoint, pos2 may have distinct IEMRE grid cells
assert length(jobs) >= 2
assert Enum.count_until(jobs, 2) == 2
lats = Enum.map(jobs, & &1.changes.args["lat"])
assert 32.875 in lats
end
@ -475,7 +475,7 @@ defmodule Microwaveprop.Workers.ContactWeatherEnqueueWorkerTest do
jobs = ContactWeatherEnqueueWorker.build_iemre_jobs([contact])
assert length(jobs) == 1
assert Enum.count_until(jobs, 2) == 1
job = hd(jobs)
assert job.changes.args["lat"] == 32.875
assert job.changes.args["lon"] == -97.0
@ -502,7 +502,7 @@ defmodule Microwaveprop.Workers.ContactWeatherEnqueueWorkerTest do
jobs = ContactWeatherEnqueueWorker.build_iemre_jobs([q1, q2])
# Same location and same date → 1 job
assert length(jobs) == 1
assert Enum.count_until(jobs, 2) == 1
end
test "creates separate jobs for different dates" do
@ -523,7 +523,7 @@ defmodule Microwaveprop.Workers.ContactWeatherEnqueueWorkerTest do
})
jobs = ContactWeatherEnqueueWorker.build_iemre_jobs([q1, q2])
assert length(jobs) == 2
assert Enum.count_until(jobs, 3) == 2
end
test "skips QSOs without pos1" do
@ -606,7 +606,7 @@ defmodule Microwaveprop.Workers.ContactWeatherEnqueueWorkerTest do
jobs = ContactWeatherEnqueueWorker.build_narr_jobs([contact])
assert length(jobs) == 1
assert Enum.count_until(jobs, 2) == 1
job = hd(jobs)
assert job.changes.worker == "Microwaveprop.Workers.NarrFetchWorker"
# 32.907 → 33.0 and -97.038 → -97.0 after 0.25° snap
@ -640,7 +640,7 @@ defmodule Microwaveprop.Workers.ContactWeatherEnqueueWorkerTest do
jobs = ContactWeatherEnqueueWorker.build_narr_jobs([contact])
# pos1, midpoint, pos2 round to three distinct 0.25° grid cells.
assert length(jobs) == 3
assert Enum.count_until(jobs, 4) == 3
assert Enum.all?(jobs, &(&1.changes.worker == "Microwaveprop.Workers.NarrFetchWorker"))
end
@ -714,7 +714,7 @@ defmodule Microwaveprop.Workers.ContactWeatherEnqueueWorkerTest do
jobs = ContactWeatherEnqueueWorker.build_narr_jobs([q1, q2])
# Both snap to 12:00Z and round to the same 0.25° grid cell → 1 job.
assert length(jobs) == 1
assert Enum.count_until(jobs, 2) == 1
assert hd(jobs).changes.worker == "Microwaveprop.Workers.NarrFetchWorker"
end
end

View file

@ -79,7 +79,7 @@ defmodule Microwaveprop.Workers.GefsFetchWorkerTest do
hours = GefsFetchWorker.extended_horizon_hours()
assert List.first(hours) == 24
assert List.last(hours) == 168
assert length(hours) == 25
assert Enum.count_until(hours, 26) == 25
assert Enum.all?(hours, &(rem(&1, 6) == 0))
end
end
@ -225,7 +225,7 @@ defmodule Microwaveprop.Workers.GefsFetchWorkerTest do
assert count == 2
persisted = Microwaveprop.Repo.all(GefsProfile)
assert length(persisted) == 2
assert Enum.count_until(persisted, 3) == 2
end
end

View file

@ -55,7 +55,7 @@ defmodule Microwaveprop.Workers.HrdpsGridWorkerTest do
# One forecast row, no analysis. /weather only consumes current
# hour so we don't pay the wgrib2 cost for the rest of the chain.
assert length(rows) == 1
assert Enum.count_until(rows, 2) == 1
[%{kind: kind, forecast_hour: fh}] = rows
assert kind == "forecast"
assert fh >= 1 and fh <= 18

View file

@ -34,7 +34,7 @@ defmodule Microwaveprop.Workers.HrrrNativeGridWorkerTest do
points = HrrrNativeGridWorker.points_of_interest_for_hour(~U[2026-03-28 18:00:00Z])
assert length(points) == 2
assert Enum.count_until(points, 3) == 2
assert {32.9, -97.0} in points
assert {33.0, -97.1} in points
end

View file

@ -58,7 +58,7 @@ defmodule Microwaveprop.Workers.IemreFetchWorkerTest do
assert obs.lat == 32.875
assert obs.lon == -97.0
assert obs.date == ~D[2026-03-28]
assert length(obs.hourly) == 2
assert Enum.count_until(obs.hourly, 3) == 2
end
test "stores stub on empty data so backfill doesn't retry" do

View file

@ -80,7 +80,7 @@ defmodule Microwaveprop.Workers.NexradWorkerTest do
assert :ok = NexradWorker.perform(%Oban.Job{args: args})
rows = Repo.all(NexradObservation)
assert length(rows) == 2
assert Enum.count_until(rows, 3) == 2
coords = rows |> Enum.map(&{&1.lat, &1.lon}) |> Enum.sort()
assert coords == [{32.9, -97.0}, {33.5, -96.5}]
@ -165,7 +165,7 @@ defmodule Microwaveprop.Workers.NexradWorkerTest do
assert :ok = NexradWorker.perform(%Oban.Job{args: args})
rows = Repo.all(NexradObservation)
assert length(rows) == 1
assert Enum.count_until(rows, 2) == 1
assert hd(rows).lat == 32.9
assert hd(rows).lon == -97.0
end

View file

@ -92,7 +92,7 @@ defmodule Microwaveprop.Workers.RadarFrameWorkerTest do
rows =
Repo.all(from(r in ContactCommonVolumeRadar, where: r.contact_id == ^contact.id))
assert length(rows) == 1
assert Enum.count_until(rows, 2) == 1
[second] = rows
assert second.id == first.id
assert second.observed_at == ~U[2024-09-15 18:35:00Z]

View file

@ -1684,7 +1684,7 @@ defmodule MicrowavepropWeb.ContactLive.ShowCoverageTest do
|> Map.fetch!(:assigns)
|> Map.fetch!(:surface_observations)
assert length(obs) <= 5
assert Enum.count_until(obs, 6) <= 5
distances =
Enum.map(obs, fn o ->

View file

@ -233,7 +233,7 @@ defmodule MicrowavepropWeb.RoverLiveTest do
{:ok, _view, html} = live(conn, ~p"/rover")
stations = Rover.list_stations(user)
assert length(stations) == 3
assert Enum.count_until(stations, 4) == 3
assert stations |> Enum.map(& &1.callsign) |> Enum.sort() == ~w(N5XU W5HN W5LUA)
assert html =~ "W5LUA"
end
@ -264,7 +264,7 @@ defmodule MicrowavepropWeb.RoverLiveTest do
[first | _] = Rover.list_stations(user)
render_hook(view, "delete_station", %{"id" => to_string(first.id)})
assert length(Rover.list_stations(user)) == 2
assert Enum.count_until(Rover.list_stations(user), 3) == 2
end
test "toggle_station flips the persisted selected flag", %{conn: conn, user: user} do

Some files were not shown because too many files have changed in this diff Show more