Add test coverage for conversion, geometry, PHG, and encoding modules

Cover to_float, to_decimal, normalize_data_type, weather_fields,
sanitize_packet_strings, to_float_safe, has_weather_data, create_point,
extract_coordinates, lat/lon helpers, PHG extraction, and altitude
extraction with 79 new tests across 4 test files.
This commit is contained in:
Graham McIntire 2026-02-20 13:53:17 -06:00
parent 918af2069a
commit e917a6b718
No known key found for this signature in database
4 changed files with 591 additions and 1 deletions

View file

@ -83,4 +83,90 @@ defmodule Aprsme.EncodingTest do
assert result == "Café"
end
end
describe "to_float_safe/1" do
test "parses valid numeric string" do
assert Encoding.to_float_safe("3.14") == {:ok, 3.14}
assert Encoding.to_float_safe("42") == {:ok, 42.0}
end
test "trims whitespace" do
assert Encoding.to_float_safe(" 3.14 ") == {:ok, 3.14}
end
test "truncates strings longer than 30 characters" do
# "1.0" padded with trailing zeros — valid after truncation
long = "1." <> String.duplicate("0", 40)
assert Encoding.to_float_safe(long) == {:ok, 1.0}
end
test "returns nil for out-of-range float" do
assert is_nil(Encoding.to_float_safe("9.1e15"))
assert is_nil(Encoding.to_float_safe("-9.1e15"))
end
test "returns value for in-range float" do
assert {:ok, f} = Encoding.to_float_safe("1000000.0")
assert f == 1_000_000.0
end
test "returns nil for invalid string" do
assert is_nil(Encoding.to_float_safe("abc"))
assert is_nil(Encoding.to_float_safe(""))
end
test "returns nil for non-binary input" do
assert is_nil(Encoding.to_float_safe(nil))
assert is_nil(Encoding.to_float_safe(123))
assert is_nil(Encoding.to_float_safe(:atom))
end
test "handles scientific notation" do
assert {:ok, f} = Encoding.to_float_safe("1.5e3")
assert f == 1500.0
end
test "handles zero" do
assert Encoding.to_float_safe("0") == {:ok, 0.0}
assert Encoding.to_float_safe("0.0") == {:ok, 0.0}
end
end
describe "has_weather_data/4" do
test "returns true when single field present" do
assert Encoding.has_weather_data(72.0, nil, nil, nil)
assert Encoding.has_weather_data(nil, 50, nil, nil)
assert Encoding.has_weather_data(nil, nil, 5.0, nil)
assert Encoding.has_weather_data(nil, nil, nil, 1013.25)
end
test "returns true when multiple fields present" do
assert Encoding.has_weather_data(72.0, 50, 5.0, 1013.25)
end
test "returns false when all nil" do
refute Encoding.has_weather_data(nil, nil, nil, nil)
end
test "treats zero as valid weather data" do
assert Encoding.has_weather_data(0, nil, nil, nil)
assert Encoding.has_weather_data(nil, 0, nil, nil)
end
test "treats negative values as valid weather data" do
assert Encoding.has_weather_data(-10, nil, nil, nil)
end
end
describe "to_hex/1" do
test "returns empty string for non-binary" do
assert Encoding.to_hex(nil) == ""
assert Encoding.to_hex(123) == ""
end
test "handles boundary byte values" do
assert Encoding.to_hex(<<0>>) == "00"
assert Encoding.to_hex(<<255>>) == "FF"
end
end
end

View file

@ -1,6 +1,7 @@
defmodule Aprsme.EncodingUtilsTest do
use ExUnit.Case
alias Aprs.Types.MicE
alias Aprsme.EncodingUtils
alias Aprsme.Packet
@ -121,7 +122,7 @@ defmodule Aprsme.EncodingUtilsTest do
test "sanitizes message field in MicE struct" do
invalid_message = <<72, 101, 108, 108, 111, 211, 87, 111, 114, 108, 100>>
mic_e = %Aprs.Types.MicE{message: invalid_message, lat_degrees: 40}
mic_e = %MicE{message: invalid_message, lat_degrees: 40}
sanitized = EncodingUtils.sanitize_data_extended(mic_e)
@ -149,6 +150,194 @@ defmodule Aprsme.EncodingUtilsTest do
end
end
describe "to_float/1" do
test "converts integer to float" do
assert EncodingUtils.to_float(1) == 1.0
assert EncodingUtils.to_float(0) == 0.0
assert EncodingUtils.to_float(-42) == -42.0
end
test "returns float unchanged" do
assert EncodingUtils.to_float(1.5) == 1.5
assert EncodingUtils.to_float(-3.14) == -3.14
assert EncodingUtils.to_float(0.0) == 0.0
end
test "returns nil for out-of-range integer" do
assert is_nil(EncodingUtils.to_float(10_000_000_000_000_000))
assert is_nil(EncodingUtils.to_float(-10_000_000_000_000_000))
end
test "converts valid string to float" do
assert EncodingUtils.to_float("2.3") == 2.3
assert EncodingUtils.to_float("42") == 42.0
assert EncodingUtils.to_float("-7.5") == -7.5
assert EncodingUtils.to_float("0") == 0.0
end
test "handles string with trailing text" do
assert EncodingUtils.to_float("12.5abc") == 12.5
end
test "returns nil for invalid string" do
assert is_nil(EncodingUtils.to_float("bad"))
assert is_nil(EncodingUtils.to_float(""))
end
test "converts Decimal to float" do
assert EncodingUtils.to_float(Decimal.new("3.14")) == 3.14
end
test "returns nil for nil" do
assert is_nil(EncodingUtils.to_float(nil))
end
test "returns nil for unsupported types" do
assert is_nil(EncodingUtils.to_float(:atom))
assert is_nil(EncodingUtils.to_float([1, 2]))
assert is_nil(EncodingUtils.to_float(%{a: 1}))
end
end
describe "to_decimal/1" do
test "returns Decimal unchanged" do
d = Decimal.new("3.14")
assert EncodingUtils.to_decimal(d) == d
end
test "converts float to Decimal" do
result = EncodingUtils.to_decimal(1.5)
assert is_struct(result, Decimal)
assert Decimal.to_float(result) == 1.5
end
test "converts integer to Decimal" do
result = EncodingUtils.to_decimal(42)
assert is_struct(result, Decimal)
assert Decimal.equal?(result, Decimal.new(42))
end
test "converts valid string to Decimal" do
result = EncodingUtils.to_decimal("2.3")
assert is_struct(result, Decimal)
assert Decimal.equal?(result, Decimal.new("2.3"))
end
test "returns nil for invalid string" do
assert is_nil(EncodingUtils.to_decimal("bad"))
assert is_nil(EncodingUtils.to_decimal(""))
end
test "handles negative values" do
result = EncodingUtils.to_decimal("-7.5")
assert is_struct(result, Decimal)
assert Decimal.negative?(result)
end
test "handles zero" do
result = EncodingUtils.to_decimal(0)
assert Decimal.equal?(result, Decimal.new(0))
end
test "handles very large integer" do
result = EncodingUtils.to_decimal(999_999_999_999)
assert is_struct(result, Decimal)
assert Decimal.equal?(result, Decimal.new(999_999_999_999))
end
test "returns nil for unsupported types" do
assert is_nil(EncodingUtils.to_decimal(nil))
assert is_nil(EncodingUtils.to_decimal(:atom))
assert is_nil(EncodingUtils.to_decimal([1, 2]))
end
end
describe "normalize_data_type/1" do
test "converts atom value with atom key" do
assert EncodingUtils.normalize_data_type(%{data_type: :weather}) == %{data_type: "weather"}
end
test "converts atom value with string key" do
assert EncodingUtils.normalize_data_type(%{"data_type" => :position}) == %{"data_type" => "position"}
end
test "leaves string value unchanged" do
assert EncodingUtils.normalize_data_type(%{data_type: "bar"}) == %{data_type: "bar"}
assert EncodingUtils.normalize_data_type(%{"data_type" => "baz"}) == %{"data_type" => "baz"}
end
test "returns map without data_type unchanged" do
assert EncodingUtils.normalize_data_type(%{foo: 1}) == %{foo: 1}
end
test "returns non-map input unchanged" do
assert EncodingUtils.normalize_data_type("string") == "string"
assert EncodingUtils.normalize_data_type(nil) == nil
end
end
describe "weather_fields/0" do
test "returns all 10 weather fields" do
fields = EncodingUtils.weather_fields()
assert length(fields) == 10
end
test "includes expected fields" do
fields = EncodingUtils.weather_fields()
assert :temperature in fields
assert :humidity in fields
assert :wind_speed in fields
assert :wind_direction in fields
assert :wind_gust in fields
assert :pressure in fields
assert :rain_1h in fields
assert :rain_24h in fields
assert :rain_since_midnight in fields
assert :snow in fields
end
test "all fields are atoms" do
assert Enum.all?(EncodingUtils.weather_fields(), &is_atom/1)
end
end
describe "sanitize_packet_strings/1" do
test "passes DateTime through unchanged" do
dt = ~U[2024-01-01 00:00:00Z]
assert EncodingUtils.sanitize_packet_strings(dt) == dt
end
test "passes NaiveDateTime through unchanged" do
ndt = ~N[2024-01-01 00:00:00]
assert EncodingUtils.sanitize_packet_strings(ndt) == ndt
end
test "sanitizes nested map values" do
input = %{"outer" => %{"inner" => <<0, 65, 66>>}}
result = EncodingUtils.sanitize_packet_strings(input)
assert result["outer"]["inner"] == "AB"
end
test "sanitizes list elements" do
input = ["abc", <<0, 65, 66, 67>>]
result = EncodingUtils.sanitize_packet_strings(input)
assert result == ["abc", "ABC"]
end
test "converts struct to map and sanitizes" do
input = %MicE{message: <<0, 72, 73>>}
result = EncodingUtils.sanitize_packet_strings(input)
assert is_map(result)
assert result[:message] == "HI"
end
test "passes non-binary values through" do
assert EncodingUtils.sanitize_packet_strings(nil) == nil
assert EncodingUtils.sanitize_packet_strings(42) == 42
assert EncodingUtils.sanitize_packet_strings(true) == true
end
end
describe "encoding_info/1" do
test "returns info for valid UTF-8 string" do
info = EncodingUtils.encoding_info("Hello")

View file

@ -0,0 +1,109 @@
defmodule Aprsme.PacketGeometryTest do
use Aprsme.DataCase, async: true
alias Aprsme.Packet
describe "create_point/2" do
test "creates point from valid float coordinates" do
point = Packet.create_point(40.7128, -74.006)
assert %Geo.Point{coordinates: {-74.006, 40.7128}, srid: 4326} = point
end
test "creates point from Decimal coordinates" do
lat = Decimal.new("40.7128")
lon = Decimal.new("-74.006")
point = Packet.create_point(lat, lon)
assert %Geo.Point{srid: 4326} = point
end
test "creates point from integer coordinates" do
point = Packet.create_point(40, -74)
assert %Geo.Point{coordinates: {-74, 40}, srid: 4326} = point
end
test "returns nil for invalid latitude" do
assert is_nil(Packet.create_point(91.0, 0.0))
assert is_nil(Packet.create_point(-91.0, 0.0))
end
test "returns nil for invalid longitude" do
assert is_nil(Packet.create_point(0.0, 181.0))
assert is_nil(Packet.create_point(0.0, -181.0))
end
test "returns nil for nil coordinates" do
assert is_nil(Packet.create_point(nil, nil))
assert is_nil(Packet.create_point(40.0, nil))
assert is_nil(Packet.create_point(nil, -74.0))
end
test "handles boundary coordinates" do
# North pole
assert %Geo.Point{} = Packet.create_point(90.0, 0.0)
# South pole
assert %Geo.Point{} = Packet.create_point(-90.0, 0.0)
# Date line
assert %Geo.Point{} = Packet.create_point(0.0, 180.0)
assert %Geo.Point{} = Packet.create_point(0.0, -180.0)
# Equator/prime meridian
point = Packet.create_point(0.0, 0.0)
assert %Geo.Point{srid: 4326} = point
assert point.coordinates == {0.0, 0.0}
end
test "returns nil for non-numeric types" do
assert is_nil(Packet.create_point("40.0", "-74.0"))
assert is_nil(Packet.create_point(:lat, :lon))
end
end
describe "extract_coordinates/1" do
test "extracts lat/lon from Geo.Point" do
point = %Geo.Point{coordinates: {-74.006, 40.7128}, srid: 4326}
assert {40.7128, -74.006} = Packet.extract_coordinates(point)
end
test "returns nil tuple for nil" do
assert {nil, nil} = Packet.extract_coordinates(nil)
end
test "returns nil tuple for non-point input" do
assert {nil, nil} = Packet.extract_coordinates(%{lat: 40.0, lon: -74.0})
assert {nil, nil} = Packet.extract_coordinates("not a point")
end
end
describe "lat/1" do
test "returns latitude from packet with location" do
packet = %Packet{location: %Geo.Point{coordinates: {-74.006, 40.7128}, srid: 4326}}
assert Packet.lat(packet) == 40.7128
end
test "returns nil for packet without location" do
packet = %Packet{location: nil}
assert is_nil(Packet.lat(packet))
end
test "returns nil for non-packet" do
assert is_nil(Packet.lat(nil))
assert is_nil(Packet.lat(%{}))
end
end
describe "lon/1" do
test "returns longitude from packet with location" do
packet = %Packet{location: %Geo.Point{coordinates: {-74.006, 40.7128}, srid: 4326}}
assert Packet.lon(packet) == -74.006
end
test "returns nil for packet without location" do
packet = %Packet{location: nil}
assert is_nil(Packet.lon(packet))
end
test "returns nil for non-packet" do
assert is_nil(Packet.lon(nil))
assert is_nil(Packet.lon(%{}))
end
end
end

View file

@ -352,4 +352,210 @@ defmodule Aprsme.PacketTest do
assert is_nil(result[:comment])
end
end
describe "PHG extraction" do
test "extracts PHG from comment string" do
attrs = %{
sender: "TEST",
data_type: "position",
data_extended: %{
comment: "PHG5530 Some comment",
latitude: 33.0,
longitude: -96.0
}
}
result = Packet.extract_additional_data(attrs, "test_packet")
# PHG5530: power=5^2=25, height=10*2^5=320, gain=3, dir=0*45=0
assert result.data["phg_power"] == 25
assert result.data["phg_height"] == 320
assert result.data["phg_gain"] == 3
assert result.data["phg_directivity"] == 0
end
test "calculates omnidirectional antenna (digit 9 = 360 degrees)" do
attrs = %{
sender: "TEST",
data_type: "position",
data_extended: %{
comment: "PHG2369 My station",
latitude: 40.0,
longitude: -74.0
}
}
result = Packet.extract_additional_data(attrs, "test_packet")
# PHG2369: power=2^2=4, height=10*2^3=80, gain=6, dir=9→360
assert result.data["phg_power"] == 4
assert result.data["phg_height"] == 80
assert result.data["phg_gain"] == 6
assert result.data["phg_directivity"] == 360
end
test "calculates directional antenna" do
attrs = %{
sender: "TEST",
data_type: "position",
data_extended: %{
comment: "PHG1064 Directional",
latitude: 35.0,
longitude: -80.0
}
}
result = Packet.extract_additional_data(attrs, "test_packet")
# PHG1064: power=1^2=1, height=10*2^0=10, gain=6, dir=4*45=180
assert result.data["phg_power"] == 1
assert result.data["phg_height"] == 10
assert result.data["phg_gain"] == 6
assert result.data["phg_directivity"] == 180
end
test "extracts PHG from data_extended map" do
attrs = %{
sender: "TEST",
data_type: "position",
data_extended: %{
comment: "My station",
latitude: 33.0,
longitude: -96.0,
phg: %{power: 25, height: 320, gain: 3, directivity: 0}
}
}
result = Packet.extract_additional_data(attrs, "test_packet")
assert result.data["phg_power"] == 25
assert result.data["phg_height"] == 320
assert result.data["phg_gain"] == 3
assert result.data["phg_directivity"] == 0
end
test "extracts PHG from data_extended string format" do
attrs = %{
sender: "TEST",
data_type: "position",
data_extended: %{
comment: "My station",
latitude: 33.0,
longitude: -96.0,
phg: "5530"
}
}
result = Packet.extract_additional_data(attrs, "test_packet")
assert result.data["phg_power"] == 25
assert result.data["phg_height"] == 320
assert result.data["phg_gain"] == 3
assert result.data["phg_directivity"] == 0
end
test "handles zero PHG values" do
attrs = %{
sender: "TEST",
data_type: "position",
data_extended: %{
comment: "PHG0000 Minimal",
latitude: 33.0,
longitude: -96.0
}
}
result = Packet.extract_additional_data(attrs, "test_packet")
# PHG0000: power=0^2=0, height=10*2^0=10, gain=0, dir=0*45=0
assert result.data["phg_power"] == 0
assert result.data["phg_height"] == 10
assert result.data["phg_gain"] == 0
assert result.data["phg_directivity"] == 0
end
end
describe "altitude extraction" do
test "extracts standard altitude from comment" do
attrs = %{
sender: "TEST",
data_type: "position",
data_extended: %{
comment: "Some info /A=001234 more info",
latitude: 33.0,
longitude: -96.0
}
}
result = Packet.extract_additional_data(attrs, "test_packet")
assert result[:altitude] == 1234.0
end
test "extracts altitude from data_extended map" do
attrs = %{
sender: "TEST",
data_type: "position",
data_extended: %{
comment: "My station",
latitude: 33.0,
longitude: -96.0,
altitude: 5280.0
}
}
result = Packet.extract_additional_data(attrs, "test_packet")
assert result[:altitude] == 5280.0
end
test "extracts zero altitude" do
attrs = %{
sender: "TEST",
data_type: "position",
data_extended: %{
comment: "/A=000000 ground level",
latitude: 33.0,
longitude: -96.0
}
}
result = Packet.extract_additional_data(attrs, "test_packet")
assert result[:altitude] == 0.0
end
test "returns nil when no altitude present" do
attrs = %{
sender: "TEST",
data_type: "position",
data_extended: %{
comment: "Just a plain comment",
latitude: 33.0,
longitude: -96.0
}
}
result = Packet.extract_additional_data(attrs, "test_packet")
assert is_nil(result[:altitude])
end
test "prefers data_extended altitude over comment-extracted altitude" do
attrs = %{
sender: "TEST",
data_type: "position",
data_extended: %{
comment: "/A=001000 info",
latitude: 33.0,
longitude: -96.0,
altitude: 9999.0
}
}
result = Packet.extract_additional_data(attrs, "test_packet")
assert result[:altitude] == 9999.0
end
end
end