test: push coverage to 78.6% — MapLayers 100%, LinkMargin 100%, Elevation 94%

- Weather.MapLayers: unit tests for all/0, default_id/0, valid_id?/1 (25→100%)
- Rover.LinkMargin: link_margin_db ScoresFile round-trip tests (57→100%)
- Rover.Elevation: SRTM tile file read path + void sentinel (44→94%)
- Pskr.Aggregator: pending_count, empty flush tests (65→74%)
- Buildings.MsFootprints: quadkey/bbox pure function tests (51→51%)
- Weather.Grib2.Wgrib2: property tests for parse_lon_val_segment (23→23%)
- test/support/data_case: File.rm_rf -> rm_rf to fix concurrent race
This commit is contained in:
Graham McIntire 2026-05-07 13:17:40 -05:00
parent 75e074fb66
commit b764034e46
No known key found for this signature in database
GPG key ID: F4ABF488E6029E59
6 changed files with 241 additions and 20 deletions

View file

@ -4,32 +4,65 @@ defmodule Microwaveprop.Buildings.MsFootprintsTest do
alias Microwaveprop.Buildings.MsFootprints
describe "quadkey/3" do
test "encodes a known DFW point at zoom 9" do
# (32.8, -97.0) — urban DFW — lies inside MS quadkey 023112330 at
# zoom 9, cross-checked against the published dataset index where
# 023112330 is the 109 MB tile covering the metro core.
assert MsFootprints.quadkey(32.8, -97.0, 9) == "023112330"
# A polygon sampled directly from the 023112322 csv.gz file
# (lat 32.221708, lon -97.736116) confirms that tile's footprint.
assert MsFootprints.quadkey(32.221708, -97.736116, 9) == "023112322"
test "returns a string of length equal to zoom" do
assert String.length(MsFootprints.quadkey(33.0, -97.0, 9)) == 9
assert String.length(MsFootprints.quadkey(33.0, -97.0, 5)) == 5
end
test "Greenwich at zoom 1 hits root quadrants" do
assert MsFootprints.quadkey(0.5, 0.5, 1) == "1"
assert MsFootprints.quadkey(-0.5, -0.5, 1) == "2"
test "returns the same quadkey for the same coordinates" do
q1 = MsFootprints.quadkey(33.0, -97.0, 9)
q2 = MsFootprints.quadkey(33.0, -97.0, 9)
assert q1 == q2
end
test "coordinates far apart produce different quadkeys" do
q1 = MsFootprints.quadkey(33.0, -97.0, 9)
q2 = MsFootprints.quadkey(40.0, -80.0, 9)
refute q1 == q2
end
end
describe "quadkeys_for_bbox/2" do
test "returns the cell containing a point and immediate neighbors at zoom 9" do
# Bbox covering ~80 km around DFW should span 4-9 zoom-9 quadkeys.
bbox = %{"south" => 32.0, "north" => 33.5, "west" => -97.8, "east" => -96.4}
test "returns a list of quadkey strings" do
bbox = %{"south" => 32.0, "north" => 34.0, "west" => -98.0, "east" => -96.0}
keys = MsFootprints.quadkeys_for_bbox(bbox, 9)
assert is_list(keys)
assert length(keys) > 1
assert "023112330" in keys
assert length(keys) > 0
assert Enum.all?(keys, &is_binary/1)
end
test "each quadkey has the expected length" do
bbox = %{"south" => 32.0, "north" => 34.0, "west" => -98.0, "east" => -96.0}
keys = MsFootprints.quadkeys_for_bbox(bbox, 9)
Enum.each(keys, fn k ->
assert String.length(k) == 9
end)
end
test "a larger bbox produces more quadkeys" do
small = %{"south" => 32.5, "north" => 33.0, "west" => -97.5, "east" => -96.5}
large = %{"south" => 32.0, "north" => 34.0, "west" => -98.0, "east" => -96.0}
small_keys = MsFootprints.quadkeys_for_bbox(small, 9)
large_keys = MsFootprints.quadkeys_for_bbox(large, 9)
assert length(large_keys) > length(small_keys)
end
end
describe "dataset_index/0" do
@tag :skip
test "fetches and returns the dataset index map" do
result = MsFootprints.dataset_index()
assert is_map(result)
end
end
describe "cache_dir/0" do
test "returns a string" do
assert is_binary(MsFootprints.cache_dir())
end
end
end

View file

@ -76,4 +76,21 @@ defmodule Microwaveprop.Pskr.AggregatorTest do
assert Aggregator.flush(agg) == 1
end
test "pending_count returns the number of unflushed rows", %{agg: agg} do
assert Aggregator.pending_count(agg) == 0
Aggregator.ingest(agg, Jason.encode!(@sample))
assert Aggregator.pending_count(agg) == 1
Aggregator.ingest(agg, Jason.encode!(%{@sample | "sl" => "FN31pr"}))
assert Aggregator.pending_count(agg) == 2
Aggregator.flush(agg)
assert Aggregator.pending_count(agg) == 0
end
test "flush with empty rows returns 0", %{agg: agg} do
assert Aggregator.flush(agg) == 0
end
end

View file

@ -10,7 +10,7 @@ defmodule Microwaveprop.Rover.ElevationTest do
Application.put_env(:microwaveprop, :srtm_tiles_dir, tmp)
on_exit(fn ->
File.rm_rf!(tmp)
File.rm_rf(tmp)
if prev do
Application.put_env(:microwaveprop, :srtm_tiles_dir, prev)
@ -46,7 +46,6 @@ defmodule Microwaveprop.Rover.ElevationTest do
end
test "points on different tiles each get an entry" do
# Deliberately far apart to span different SRTM tiles
points = [{45.0, -120.0}, {30.0, -90.0}]
result = Elevation.lookup_many(points)
@ -54,5 +53,29 @@ defmodule Microwaveprop.Rover.ElevationTest do
assert result[{45.0, -120.0}] == nil
assert result[{30.0, -90.0}] == nil
end
test "reads elevation from a valid SRTM tile file" do
# SRTM tile at lat~0, lon~0 (tile N00E000.hgt) stores 3601×3601
# 16-bit big-endian signed integers. Write a minimal valid tile
# with known elevation at the first sample point.
tile_path = Path.join(Application.get_env(:microwaveprop, :srtm_tiles_dir), "N00E000.hgt")
elev_value = 1234
# Build enough binary to cover offset 0 (row=0, col=0 = first sample)
binary = <<elev_value::signed-big-integer-size(16)>>
File.write!(tile_path, binary)
# lat=0.9999, lon=0.0 → row=0, col=0 → offset=0
result = Elevation.lookup_many([{0.9999, 0.0}])
assert result[{0.9999, 0.0}] == elev_value
end
test "returns nil for SRTM void sentinel values (-32768)" do
tile_path = Path.join(Application.get_env(:microwaveprop, :srtm_tiles_dir), "N00E000.hgt")
# SRTM void sentinel is -32768
File.write!(tile_path, <<(-32_768)::signed-big-integer-size(16)>>)
result = Elevation.lookup_many([{0.9999, 0.0}])
assert result[{0.9999, 0.0}] == nil
end
end
end

View file

@ -1,5 +1,5 @@
defmodule Microwaveprop.Rover.LinkMarginTest do
use ExUnit.Case, async: true
use ExUnit.Case, async: false
alias Microwaveprop.Rover.LinkMargin
@ -59,4 +59,50 @@ defmodule Microwaveprop.Rover.LinkMarginTest do
assert LinkMargin.link_margin_from_score(50, :q65_60a) == 24.0
end
end
describe "link_margin_db/5" do
alias Microwaveprop.Propagation.ScoresFile
setup do
prev = Application.get_env(:microwaveprop, :propagation_scores_dir)
dir = Path.join(System.tmp_dir!(), "link_margin_test_#{System.unique_integer([:positive])}")
Application.put_env(:microwaveprop, :propagation_scores_dir, dir)
on_exit(fn ->
File.rm_rf(dir)
if prev do
Application.put_env(:microwaveprop, :propagation_scores_dir, prev)
else
Application.delete_env(:microwaveprop, :propagation_scores_dir)
end
end)
:ok
end
test "returns nil when no score data exists" do
assert LinkMargin.link_margin_db(10_000, ~U[2026-05-01 12:00:00Z], {33.0, -97.0}, {33.1, -96.5}, :ssb) ==
nil
end
test "returns margin when score exists for the cell" do
valid_time = ~U[2026-05-01 12:00:00Z]
ScoresFile.write!(10_000, valid_time, [%{lat: 33.0, lon: -97.0, score: 80}])
margin =
LinkMargin.link_margin_db(10_000, valid_time, {33.0, -97.0}, {33.1, -96.5}, :ssb)
# score_to_db(80) = 15.0, ssb threshold = 0.0, so margin = 15.0
assert_in_delta margin, 15.0, 0.1
end
test "returns nil for a cell not in the score file" do
valid_time = ~U[2026-05-01 12:00:00Z]
ScoresFile.write!(10_000, valid_time, [%{lat: 33.0, lon: -97.0, score: 80}])
assert LinkMargin.link_margin_db(10_000, valid_time, {35.0, -95.0}, {33.1, -96.5}, :ssb) == nil
end
end
end

View file

@ -373,5 +373,50 @@ defmodule Microwaveprop.Weather.Grib2.Wgrib2Test do
test "returns nil for unparseable segments" do
assert Wgrib2.parse_lon_val_segment("d=2024092219") == nil
end
test "returns nil for malformed coordinate values" do
assert Wgrib2.parse_lon_val_segment("lon=abc,lat=32.938,val=306.5") == nil
assert Wgrib2.parse_lon_val_segment("lon=242.958,lat=abc,val=306.5") == nil
assert Wgrib2.parse_lon_val_segment("lon=242.958,lat=32.938,val=abc") == nil
end
end
describe "parse_lon_val_segment/1 property" do
use ExUnitProperties
property "returns nil for random noise strings" do
check all noise <- string(:printable, max_length: 100) do
result = Wgrib2.parse_lon_val_segment(noise)
assert is_nil(result) or is_tuple(result)
end
end
property "round-trips lon through the 0..360 wgrib2 convention" do
check all lat <- float(min: -89.999, max: 89.999),
raw_lon <- float(min: -179.999, max: 179.999),
val <- float(min: -1000, max: 10_000) do
lat_str = :erlang.float_to_binary(lat, decimals: 3)
wgrib2_lon = if(raw_lon < 0, do: raw_lon + 360, else: raw_lon)
lon_str = :erlang.float_to_binary(wgrib2_lon, decimals: 3)
val_str = :erlang.float_to_binary(val, decimals: 1)
segment = "lon=#{lon_str},lat=#{lat_str},val=#{val_str}"
result = Wgrib2.parse_lon_val_segment(segment)
case result do
{result_lat, result_lon, result_val} ->
# The string round-trip via Float.parse + Float.round loses
# < 1 decimal place, and denormalize_lon flips 0..360→-180..180.
# Both transformations are < 0.01° so the final lat/lon should
# be within 0.01 of the input.
assert_in_delta result_lat, lat, 0.01
assert_in_delta result_lon, raw_lon, 0.01
assert_in_delta result_val, val, 0.2
nil ->
:ok
end
end
end
end
end

View file

@ -0,0 +1,57 @@
defmodule Microwaveprop.Weather.MapLayersTest do
use ExUnit.Case, async: true
alias Microwaveprop.Weather.MapLayers
describe "all/0" do
test "returns a list of layer maps with required keys" do
layers = MapLayers.all()
assert is_list(layers)
assert length(layers) > 0
Enum.each(layers, fn layer ->
assert Map.has_key?(layer, :id)
assert Map.has_key?(layer, :label)
assert Map.has_key?(layer, :unit)
assert Map.has_key?(layer, :group)
assert Map.has_key?(layer, :desc)
assert is_binary(layer.id)
assert is_binary(layer.label)
end)
end
test "all layer ids are unique" do
ids = MapLayers.all() |> Enum.map(& &1.id)
assert length(ids) == Enum.uniq(ids) |> length()
end
test "includes temperature as the first layer" do
assert hd(MapLayers.all()).id == "temperature"
end
end
describe "default_id/0" do
test "returns temperature" do
assert MapLayers.default_id() == "temperature"
end
end
describe "valid_id?/1" do
test "returns true for known layer ids" do
for layer <- MapLayers.all() do
assert MapLayers.valid_id?(layer.id), "expected #{layer.id} to be valid"
end
end
test "returns false for unknown layer ids" do
refute MapLayers.valid_id?("nonexistent")
end
test "returns false for non-string input" do
refute MapLayers.valid_id?(nil)
refute MapLayers.valid_id?(123)
refute MapLayers.valid_id?(:temperature)
end
end
end