prop/test/microwaveprop/radio/band_resolver_property_test.exs

97 lines
3.2 KiB
Elixir

defmodule Microwaveprop.Radio.BandResolverPropertyTest do
use ExUnit.Case, async: true
use ExUnitProperties
alias Microwaveprop.Radio.BandResolver
describe "resolve/1 output contract" do
property "result is always nil or a member of allowed_bands/0 (strings)" do
check all(s <- string(:printable)) do
case BandResolver.resolve(s) do
nil -> :ok
mhz -> assert mhz in BandResolver.allowed_bands()
end
end
end
property "result is always nil or a member of allowed_bands/0 (integers)" do
check all(i <- integer(-1_000..500_000)) do
case BandResolver.resolve(i) do
nil -> :ok
mhz -> assert mhz in BandResolver.allowed_bands()
end
end
end
property "result is always nil or a member of allowed_bands/0 (floats)" do
check all(f <- float(min: -10.0, max: 500_000.0)) do
case BandResolver.resolve(f) do
nil -> :ok
mhz -> assert mhz in BandResolver.allowed_bands()
end
end
end
property "never crashes on arbitrary term input" do
check all(
term <-
one_of([
constant(nil),
constant(""),
constant(:atom),
constant([]),
constant(%{}),
string(:printable),
integer(),
float()
])
) do
result = BandResolver.resolve(term)
assert is_nil(result) or is_integer(result)
end
end
end
describe "resolve/1 is idempotent for canonical bands" do
property "every allowed band resolves to itself" do
check all(mhz <- member_of(BandResolver.allowed_bands())) do
assert BandResolver.resolve(mhz) == mhz
assert BandResolver.resolve(Integer.to_string(mhz)) == mhz
end
end
property "resolve_as_string/1 round-trips through Integer.to_string" do
check all(mhz <- member_of(BandResolver.allowed_bands())) do
assert BandResolver.resolve_as_string(mhz) == Integer.to_string(mhz)
end
end
end
describe "resolve/1 snap-to-nearest" do
property "frequencies within a small neighborhood of a microwave band resolve to that band" do
# The >= 900 MHz branch uses nearest-neighbor snapping. A small
# perturbation of a canonical band value must land on the same
# band — but we clamp the perturbed input to stay ≥ 900 so we
# don't cross the resolver's lower cutoff for 902 (the smallest
# microwave band).
bands_900_plus = Enum.filter(BandResolver.allowed_bands(), &(&1 >= 900))
check all(
mhz <- member_of(bands_900_plus),
delta_pct <- float(min: -0.001, max: 0.001)
) do
perturbed = max(900.01, mhz * (1.0 + delta_pct))
# Only assert when the perturbed value is still unambiguously
# closer to `mhz` than to any other allowed band.
nearest_other =
bands_900_plus
|> Enum.reject(&(&1 == mhz))
|> Enum.min_by(&abs(&1 - perturbed))
if abs(perturbed - mhz) < abs(perturbed - nearest_other) do
assert BandResolver.resolve(perturbed) == mhz
end
end
end
end
end