towerops/test/test_helper.exs
Graham McIntire 6e4b07026f feat(coverage): tree-canopy clutter via LANDFIRE EVH
Closes the last clutter source. NLOS heatmaps now see trees as
obstacles in addition to buildings:

- New Towerops.Coverages.TreeCanopy module fetches an AAIGrid of
  canopy heights for the coverage bbox via the same /vsicurl/
  + Lidar.Reader pipeline used for terrain. Source defaults to
  the public LANDFIRE 2022 EVH COG (~30 m, all CONUS) and is
  override-able via :tree_canopy_url.
- EVH cells are integer codes; decode_evh maps the canopy range
  (101..199 → height-100 m), the herbaceous range (11..29 →
  (code-10) * 0.1 m), and zeroes everything else.
- Profile.sample/5 takes a :canopy grid alongside :clutter and
  uses max(building_height, canopy_height) so a tree poking above
  a one-storey roof still shadows.
- CoverageWorker fetches the canopy grid once per job (best
  effort — failures log + continue with nil) and threads it
  through the env map. LOS keeps clutter empty + canopy nil
  (height-above-clutter view); NLOS gets both.
- Tests: tree_canopy_test for the disabled / override paths;
  test_helper disables the LANDFIRE fetch by default so the
  rest of the suite doesn't stream remote bytes.
2026-05-06 16:54:59 -05:00

106 lines
3.6 KiB
Elixir

# Configure MIB directories BEFORE any NIF loading
# This must happen before ExUnit.start() to ensure ToweropsNative NIF
# can find vendor MIBs when it initializes
mib_dir = Application.app_dir(:towerops, "priv/mibs")
if File.dir?(mib_dir) do
# Get main directory plus all subdirectories for vendor MIBs
subdirs =
mib_dir
|> File.ls!()
|> Enum.map(&Path.join(mib_dir, &1))
|> Enum.filter(&File.dir?/1)
mib_dirs = [mib_dir | subdirs]
mibdirs_env = Enum.join(mib_dirs, ":")
# Set environment variables BEFORE initializing the NIF
System.put_env("MIBDIRS", mibdirs_env)
System.put_env("MIBS", "ALL")
# Add each directory to net-snmp's search path
Enum.each(mib_dirs, fn dir ->
case ToweropsNative.load_mib_directory(dir) do
:ok -> :ok
# Ignore errors in test environment
{:error, _reason} -> :ok
end
end)
# Explicitly initialize the MIB library with correct environment variables
case ToweropsNative.init_mib_library() do
result when result in ["initialized", "already_initialized"] ->
IO.puts("Test MIB library initialized: #{result}")
other ->
IO.puts("Warning: Unexpected MIB library init result: #{inspect(other)}")
end
end
# Load antenna pattern registry for tests that exercise the Coverages
# context (the schema validates antenna_slug against this in-memory registry).
Towerops.Coverages.Antenna.load_registry()
ExUnit.start()
# Skip the coverage-compute integration test when the GDAL CLI tools
# aren't installed (e.g. CI test gate runs in a lean image without
# gdal-bin). Locally we install GDAL via `brew install gdal` per
# CLAUDE.md, so the test runs there.
extra_excludes =
if System.find_executable("gdal_translate") && System.find_executable("gdaldem") do
[]
else
[:requires_gdal]
end
# Exclude tests by default
# Run specific tests with: mix test --only <tag>
ExUnit.configure(
exclude:
[
:integration,
# Tests making real network calls (ping, DNS, SSL)
:network,
# SnmpKit-specific exclusions
:performance,
:snmpv3,
:memory,
:shell_integration,
:optional,
:needs_simulator,
:manual,
:real_device,
:yecc_required
] ++ extra_excludes
)
# Define mocks for testing
Mox.defmock(Towerops.Monitoring.PingMock, for: Towerops.Monitoring.PingBehaviour)
Mox.defmock(Towerops.Snmp.PollerMock, for: Towerops.Snmp.PollerBehaviour)
Mox.defmock(Towerops.Snmp.SnmpMock, for: Towerops.Snmp.SnmpBehaviour)
Mox.defmock(Towerops.RateLimit.RedisClientMock, for: Towerops.RateLimit.RedisClient)
# Tests assert on the bare ":no_tile" return path of the LIDAR catalog
# without needing to mock the synthetic USGS NED runner. Real prod use
# enables this fallback for nationwide CONUS coverage.
Application.put_env(:towerops, :lidar_ned_fallback, false)
# Same idea for the LANDFIRE tree-canopy COG: tests disable it so the
# worker doesn't try to stream LANDFIRE bytes during compute. Tests
# that exercise the canopy code path opt back in explicitly.
Application.put_env(:towerops, :tree_canopy_enabled, false)
# Define stub module for SNMP mock (returns errors for all calls)
defmodule Towerops.Snmp.SnmpMockStub do
@moduledoc false
@behaviour Towerops.Snmp.SnmpBehaviour
def get(_target, _oid, _opts), do: {:error, :timeout}
def get_next(_target, _oid, _opts), do: {:error, :timeout}
def walk(_target, _oid, _opts), do: {:error, :timeout}
def get_bulk(_target, _oid, _opts), do: {:error, :timeout}
def get_multiple(_target, _oids, _opts), do: {:error, :timeout}
end
Ecto.Adapters.SQL.Sandbox.mode(Towerops.Repo, :manual)