prop/test/microwaveprop/radio/band_resolver_property_test.exs
Graham McIntire fd976b0cd5
fix: resolve 391 Credo issues across codebase
- Add jump_credo_checks ~> 0.4 with all 20 checks enabled
- Fix all standard Credo issues: 139 @spec (113 done, 26 remain),
  4 refactoring, 3 alias usage, 9 System.cmd env, 5 unsafe_to_atom,
  2 max line length, 9 assert_receive timeout
- Fix 170+ jump_credo_checks warnings:
  - 117 TopLevelAliasImportRequire: move nested alias/import to module top
  - 32 UseObanProWorker: switch to Oban.Pro.Worker
  - 4 DoctestIExExamples: add doctests / create test file
  - ~20 WeakAssertion: strengthen type-check assertions
  - Various ConditionalAssertion, AssertReceiveTimeout fixes
- Exclude vendor/ from Credo analysis
- Remaining: 175 warnings (mostly opinionated WeakAssertion,
  AvoidSocketAssignsInTest), 26 @spec annotations
2026-06-12 13:51:32 -05:00

99 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
case BandResolver.resolve(term) do
nil -> :ok
mhz -> assert is_integer(mhz)
end
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