The flight-stack test suite runs anywhere — no drone, no flight controller, no SITL. Hardware modules are stubbed, the AI source is bypassed, and the database uses a throwaway SQLite file per test.
python -m venv .venv && source .venv/bin/activate
pip install -r requirements-dev.txt
python -m pytest # the flight-stack suite (tests/)pytest.ini restricts collection to tests/ and excludes archive/,
hailo-rpi5-examples/, and tools/ (the last has its own suite). The log viewer
tests run separately and need Flask:
pip install flask
python -m pytest tools/log_server/tests/The flight stack binds its collaborators by name at import time:
# states/scan.py
from telemetry import telemetry_singleton
from DB_abstraction import db_abstraction
from mission_logging import log_eventImporting telemetry for real would try to open a serial port; importing
DB_abstraction instantiates a real DBAbstraction() (and thus a SQLite engine)
at module load. So tests either:
- Stub the module in
sys.modulesbefore importing the code under test, or - Use the real module but patch the consuming module's attribute (never the source) — because the consumer holds its own binding.
# Patch where the name is USED, not where it is defined:
monkeypatch.setattr(states.scan, "db_abstraction", fake_db) # correct
monkeypatch.setattr(DB_abstraction, "db_abstraction", fake_db) # has no effect on scan- Import-time DB guard — points
DBat a temp path during collection, so no test run can ever create a straydroneDB.dbin the repo. - Factories:
make_drone_state,make_detection,make_frame. fake_telemetry,fake_db— recording stand-ins (tests/support.py).fresh_db— a realDBAbstractionbacked by a temp SQLite file, with theDatabaseSessionsingleton reset around the test.reset_state_globals— clears module-level FSM/AI state (homing timers, the scan "processed" latch,shared_data, the AI singleton).constants_guard— snapshot/restoreconstantsfor tests that mutate tunables.
ensure_stub_module/ensure_real_module— install a stub, or evict a stub a sibling test left insys.modulesand import the real module. This is what makes the suite order-independent despite the sharedsys.modulesnamespace.FakeTelemetry,FakeDB— record commands / serve canned data.heartbeat_msg,global_position_msg,attitude_msg, … — fake MAVLink messages (duck-typedSimpleNamespace) in raw wire units._StopLoop— raise from a mock to break an otherwise infinite loop under test.
Because sys.modules is shared across the session:
- A test that stubs
mission_logging/telemetrymust do so conditionally (only if not already imported) or it will clobber the real module for later tests. Theif name not in sys.modulespattern intest_geometry_math.pyandensure_real_moduleintest_db_layer.py/test_safe_fixes.pycooperate to keep this safe. test_sim_ai.pysetsconstants.SIM_AI_ENABLE_IMPERFECTIONS = Falsefor the session; new tests must not assume its default.
telemetry's actual MAVLink I/O (serial/UDP) — only the parsing and command shaping are unit-tested via fake messages.- The
sim_aibackground generation loop (a closure insiderun_sim_ai); its pure projection helpers are covered intest_sim_ai.py. - The Hailo camera callback (
ai_callback.py, a symlink into vendored code).
- Create
tests/test_state_<name>.py. sys.path.insert(0, repo_root), then import the state module.- In each test, build a state with
make_drone_state(...)and a frame withmake_frame(make_detection(...)). monkeypatch.setattr(states.<name>, "telemetry_singleton", FakeTelemetry())and likewise fordb_abstraction/log_event.- Call the state function and assert on the returned
DroneStateEnumand on the commands recorded by the fakes.
See tests/test_state_homing.py for the richest example (timers, velocity law,
altitude bands).