Testing
Running the suite
No ROS2 context, no network access, and no external services are required — all external dependencies are mocked.
Run a single file:
Run tests matching a keyword:
Test files
| File | Scope | Key dependencies mocked |
|---|---|---|
test_core.py |
Way classification, serialization, combine_ways |
— |
test_astar.py |
Grid A* planner | — |
test_rrt.py |
RRT* planner — path found, collision avoidance, edge cases | — |
test_graph_planner.py |
Graph-based planner | — |
test_overpass.py |
OverpassClient — retry logic, rate limiting, status polling |
requests.Session, time.sleep |
test_parsing.py |
parse_osm_ways, separate_ways, parse_osm_nodes — OSM element classification and buffering |
overpy.Overpass (fixture JSON) |
test_fill_grid.py |
ReplanPath.fill_grid — footway cost assignment, barrier cell marking |
— |
test_viewer_helpers.py |
helpers.py — GeoJSON conversion, way splitting, node overrides, annotation I/O, change log migration |
— |
test_viewer_routes.py |
Flask REST API — annotation CRUD, file listing, mapdata fetch, way operations, path-traversal security | Filesystem (temp dir) |
test_integration.py |
Full pipeline — GPX parse, save/reload roundtrip, OSM cache, mocked Overpass query, parse_intersections |
OverpassClient |
test_errors.py |
Error paths — malformed GPX, corrupt files, Overpass timeouts, planning failures | requests.Session, time.sleep |
Test design principles
No network. Every test that would otherwise reach the Overpass API patches requests.Session.post / requests.Session.get via unittest.mock.patch. time.sleep is patched alongside so retries complete instantly.
No ROS2. The Flask app is created via create_app(data_dir=...) which bypasses the ROS2 node initialisation (guarded by ROS_AVAILABLE).
Real filesystem, isolated. Route tests use pytest's tmp_path fixture, giving each test function its own directory. The load_mapdata_cached cache keys on (path, mtime) so cross-test contamination cannot occur.
Minimal fixtures. Shared helper functions (e.g. _make_mapdata) are plain functions defined in the test module, not conftest fixtures, to keep test files self-contained.
Module-by-module notes
test_rrt.py
test_rrt_star_with_obstacle checks obstacle avoidance at two levels:
- Node check — each waypoint in the returned path must have grid cost
< 0.95. - Segment check — 20 linearly-interpolated samples along each segment between consecutive waypoints must also have grid cost
< 0.95.
The segment check is critical: without it a two-point path [start, goal] passes the node check even when the straight line between them cuts directly through the obstacle region.
The test sets traversability_threshold=0.5 so that cells with the obstacle value 1.0 are treated as hard collisions by the planner.
The segment check excludes the single outer ring of boundary cells (rows/columns 40 and 59) to account for a known Bresenham corner artefact where a diagonal segment can graze a single boundary cell that the rasteriser does not visit.
test_parsing.py
Tests the three OSM parsing functions directly using overpy.Overpass().parse_json(fixture_json) as input — no real network call:
parse_osm_ways— footway produces aLineString, road is classified viais_road(), closed way produces aPolygon.separate_ways— footway goes to the footways list (buffered to Polygon); road goes to roads; barrier way (withbarrier: *tag dict) goes to barriers.BUFFER_WIDTHSfor road > footway.parse_osm_nodes— node withbarrier: blockproduces a buffered-PointWay; nodes inway_node_idsare skipped; non-obstacle nodes are excluded.
test_fill_grid.py
Tests ReplanPath.fill_grid with a hand-built MapData containing a 16 m footway in a 20 × 20 m grid (UTM zone 33U). Key assertions:
- Cells exactly on the footway centreline have cost
< DEFAULT_OFF_PATH_COST(0.9). - Cells at the far corner (>
max_path_distfrom any path) have cost ≥ 0.85. - Cells inside a 2 × 2 m barrier polygon passed as an obstacle are
np.infin the 2-D grid cache. - Cells clearly outside the barrier are finite.
test_overpass.py
OverpassClient is instantiated directly and its session attribute is patched in place, which avoids replacing the class globally and keeps each test independent. The nine tests cover:
- Successful query returning an
overpy.Result - 429/406 response rotating the active endpoint
- 500 response triggering a retry on the same endpoint
- All retries exhausted →
None requests.Timeout→None_wait_for_slotshort-circuiting for non-overpass-api.deendpoints_wait_for_slotreturning immediately when slots are available_wait_for_slotsleeping the correct number of seconds when no slots are available
test_viewer_helpers.py
Pure functions in helpers.py are tested in isolation using hand-constructed Way objects and fixed UTM coordinates in zone 33U (Prague area, easting ≈ 458 000, northing ≈ 5 550 500). Notable cases:
- GeoJSON roundtrip —
geom_to_geojsonfollowed bygeojson_geom_to_utmmust recover the original geometry within 2 m (equals_exact(tolerance=2.0)). - Way splitting — a five-node
LineStringway split at its middle node must yield two segments with virtual IDs"{id}:0"and"{id}:1"and the correct node subsets. - Node deletion — removing a node from a four-node way must return a three-node way; removing a node from a two-node way must return
None. - Change log migration —
migrate_change_logpopulates the log for untracked deletions, is idempotent once the migration version matches, drops legacy entries without atskey on re-migration, and preserves entries that carry a timestamp.
test_viewer_routes.py
A _make_mapdata helper creates a minimal .mapdata file (one footway Way, two entries in nodes_cache) and writes it to a tmp_path. Two fixtures build on this:
app_client— empty data directory, used for 400/404 edge cases.app_client_with_file— data directory pre-populated withtest.mapdata.
The annotation lifecycle (create → update → delete) is tested end-to-end: each step checks both the HTTP status code and the persisted state. Additional coverage includes:
- Path-traversal security —
GET /api/mapdata?file=../../../etc/passwdmust return 400. - Way tag overrides —
PUT /api/ways/{id}/tagsstores tag overrides;DELETEremoves them. - Hide / show / restore — the hide, show, and restore endpoints return 204 and update the annotations file correctly.
- Node operations — node deletion and position-override move operations.
- Way splitting — a 3-node footway can be split at its middle node, returning two segments; undoing the split restores one segment.
test_integration.py
test_run_parse_with_mocked_overpass patches map_data.map_data.OverpassClient at the class level, replacing query_raw with a return value of a minimal Overpass JSON string containing one footway way. client.api is set to a real overpy.Overpass() instance so that parse_json works correctly. After run_queries(use_cache=False) and run_parse(), the test asserts that at least one footway was classified.
Additional coverage:
- OSM cache roundtrip —
_save_osm_cachethen_load_osm_cachereturns the same raw JSON strings. - OSM cache bbox mismatch — shifting
min_latby 1° makes_load_osm_cachereturnNone. parse_intersections— two footways sharing a middle node produce exactly one crossroadWayat that node; no shared nodes → no crossroads; three footways sharing one endpoint → crossroad.
test_errors.py
test_rrt_star_goal_in_isolated_obstacle creates a grid that is entirely blocked except for a small free patch around the start. Since the goal lies within the blocked region and no path can reach it, find_path must return None.
Adding new tests
- Place the test file in
tests/. - Import only from
map_data.*— do not import from neighbouring test modules. - Mock any external I/O at the lowest sensible level (
requests.Session, not the wholerequestsmodule). - Use
tmp_pathfor any test that reads or writes files. - Run
pytest tests/ -vbefore opening a pull request.