- 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
151 lines
5.9 KiB
Elixir
151 lines
5.9 KiB
Elixir
defmodule Microwaveprop.RoverPlanningTest do
|
|
use Microwaveprop.DataCase, async: true
|
|
|
|
alias Microwaveprop.AccountsFixtures
|
|
alias Microwaveprop.Repo
|
|
alias Microwaveprop.Rover
|
|
alias Microwaveprop.RoverPlanning
|
|
alias Microwaveprop.RoverPlanning.Mission
|
|
alias Microwaveprop.Terrain.ElevationClient
|
|
|
|
setup do
|
|
# Oban runs inline in test, so RoverPathProfileWorker fires immediately
|
|
# when the context enqueues jobs. Stub elevation lookups so the worker
|
|
# can complete without hitting the real API.
|
|
Req.Test.stub(ElevationClient, fn conn ->
|
|
params = Plug.Conn.fetch_query_params(conn).query_params
|
|
lat_count = params["latitude"] |> String.split(",") |> length()
|
|
Req.Test.json(conn, %{"elevation" => List.duplicate(200.0, lat_count)})
|
|
end)
|
|
|
|
:ok
|
|
end
|
|
|
|
defp create_rover_location(attrs) do
|
|
user = AccountsFixtures.user_fixture()
|
|
|
|
{:ok, loc} =
|
|
Rover.create_location(
|
|
user,
|
|
Map.merge(%{lat: 32.5, lon: -97.5, status: :good}, attrs)
|
|
)
|
|
|
|
loc
|
|
end
|
|
|
|
defp valid_attrs(extra \\ %{}) do
|
|
Map.merge(
|
|
%{
|
|
"name" => "Test mission",
|
|
"band_mhz" => 10_000,
|
|
"only_known_good" => true,
|
|
"rover_height_ft" => 8.0,
|
|
"station_height_ft" => 30.0,
|
|
"stations" => %{
|
|
"0" => %{"input" => "EM12kp", "position" => 0}
|
|
}
|
|
},
|
|
extra
|
|
)
|
|
end
|
|
|
|
describe "create_mission/2" do
|
|
test "inserts a mission with stations and enqueues a path per (rover x station) pair" do
|
|
user = AccountsFixtures.user_fixture()
|
|
_ideal_loc = create_rover_location(%{lat: 32.0, lon: -97.0, status: :good})
|
|
_off_limits = create_rover_location(%{lat: 33.0, lon: -98.0, status: :bad})
|
|
|
|
assert {:ok, mission} = RoverPlanning.create_mission(user, valid_attrs())
|
|
assert mission.user_id == user.id
|
|
assert [station] = mission.stations
|
|
assert station.grid == "EM12KP"
|
|
assert is_float(station.lat)
|
|
assert is_float(station.lon)
|
|
|
|
paths = RoverPlanning.list_paths(mission)
|
|
# only_known_good=true -> only the :good location pairs with the station.
|
|
# Oban runs inline in test so the worker has already completed.
|
|
assert length(paths) == 1
|
|
assert hd(paths).status in [:complete, :failed]
|
|
end
|
|
|
|
test "completed path caches the link-budget loss components" do
|
|
user = AccountsFixtures.user_fixture()
|
|
_loc = create_rover_location(%{lat: 32.0, lon: -97.0, status: :good})
|
|
|
|
assert {:ok, mission} = RoverPlanning.create_mission(user, valid_attrs())
|
|
[path] = RoverPlanning.list_paths(mission)
|
|
assert path.status == :complete
|
|
|
|
# The result map MUST carry the cached loss-budget so the show
|
|
# page (and any future caller) can render link math without
|
|
# re-running the computation. Values come from the band's
|
|
# absorption coefficients x distance + free-space path loss + the
|
|
# already-cached terrain diffraction. Stored as floats.
|
|
assert is_float(path.result["free_space_loss_db"])
|
|
assert is_float(path.result["oxygen_loss_db"])
|
|
assert is_float(path.result["humidity_loss_db_baseline"])
|
|
assert is_float(path.result["total_baseline_loss_db"])
|
|
# Free-space loss for a ~140 km, 10 GHz path is ~155 dB; sanity
|
|
# check it's in the right order of magnitude (not zero, not 1000).
|
|
assert path.result["free_space_loss_db"] > 100
|
|
assert path.result["free_space_loss_db"] < 200
|
|
end
|
|
|
|
test "completed path stores a propagation_score key (may be nil without scores file)" do
|
|
user = AccountsFixtures.user_fixture()
|
|
_loc = create_rover_location(%{lat: 32.0, lon: -97.0, status: :good})
|
|
|
|
assert {:ok, mission} = RoverPlanning.create_mission(user, valid_attrs())
|
|
[path] = RoverPlanning.list_paths(mission)
|
|
# The worker is the gate: status only flips to :complete after the
|
|
# path-calculator pipeline (terrain + propagation lookup) has run.
|
|
# The score itself is nil in the test env (no .prop file on disk),
|
|
# but the key MUST be present so the show-page can render "---"
|
|
# without a key-existence check at every cell.
|
|
assert path.status == :complete
|
|
assert Map.has_key?(path.result, "propagation_score")
|
|
end
|
|
|
|
test "backfill_paths/0 re-runs failed paths across all missions" do
|
|
user = AccountsFixtures.user_fixture()
|
|
_loc = create_rover_location(%{lat: 32.0, lon: -97.0, status: :good})
|
|
|
|
assert {:ok, mission} = RoverPlanning.create_mission(user, valid_attrs())
|
|
[path] = RoverPlanning.list_paths(mission)
|
|
assert path.status == :complete
|
|
|
|
# Simulate a path that was stuck in :failed (e.g. transient
|
|
# elevation API error during the original computation).
|
|
{:ok, _} =
|
|
path
|
|
|> Ecto.Changeset.change(%{status: :failed, error: "transient", result: nil})
|
|
|> Repo.update()
|
|
|
|
assert :ok = RoverPlanning.backfill_paths()
|
|
|
|
# Oban inline -> backfill enqueues a job, the worker reruns
|
|
# immediately, and the path flips back to :complete.
|
|
[reloaded] = RoverPlanning.list_paths(mission)
|
|
assert reloaded.status == :complete
|
|
assert map_size(reloaded.result) > 0
|
|
end
|
|
|
|
test "backfill_paths/0 inserts missing rows for under-populated missions" do
|
|
user = AccountsFixtures.user_fixture()
|
|
_loc = create_rover_location(%{lat: 32.0, lon: -97.0, status: :good})
|
|
|
|
assert {:ok, mission} = RoverPlanning.create_mission(user, valid_attrs())
|
|
# Simulate a half-broken mission: paths were never persisted (e.g.
|
|
# the original create_mission Multi succeeded but the Oban insert
|
|
# was lost due to a crash). Backfill must repopulate the matrix.
|
|
Repo.delete_all(Ecto.Query.from(p in RoverPlanning.Path, where: p.mission_id == ^mission.id))
|
|
assert RoverPlanning.list_paths(mission) == []
|
|
|
|
assert :ok = RoverPlanning.backfill_paths()
|
|
|
|
assert [path] = RoverPlanning.list_paths(mission)
|
|
assert path.status == :complete
|
|
end
|
|
end
|
|
end
|