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:
parent
75e074fb66
commit
b764034e46
6 changed files with 241 additions and 20 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
57
test/microwaveprop/weather/map_layers_test.exs
Normal file
57
test/microwaveprop/weather/map_layers_test.exs
Normal 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
|
||||
Loading…
Add table
Reference in a new issue