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
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:
parent
3e51e62a8b
commit
079346a1b9
106 changed files with 436 additions and 429 deletions
259
.credo.exs
259
.credo.exs
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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, _} =
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
1
mix.exs
1
mix.exs
|
|
@ -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.
|
||||
|
|
|
|||
1
mix.lock
1
mix.lock
|
|
@ -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"},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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 ->
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Add table
Reference in a new issue