Files
IQ.Pilot/iqpilot/iq_maps/tests/test_tile_bundle_downloader.py
2026-07-28 20:43:30 -05:00

236 lines
8.7 KiB
Python

import hashlib
import json
import sqlite3
import threading
from functools import partial
from http.server import HTTPServer, SimpleHTTPRequestHandler
import pytest
from openpilot.iqpilot.iq_maps import tile_bundle_downloader as tbd
from openpilot.iqpilot.ui.onroad import offline_tiles
PNG_1X1 = (
b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01" +
b"\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\rIDATx\x9cc\xf8\xcf" +
b"\xc0\xf0\x1f\x00\x05\x00\x01\xff\x89\x99=\x1d\x00\x00\x00\x00IEND\xaeB`\x82"
)
class FakeParams:
def __init__(self):
self.store: dict[str, object] = {}
def get(self, key, return_default=False):
return self.store.get(key)
def get_bool(self, key):
return bool(self.store.get(key))
def put(self, key, value):
self.store[key] = value
def put_bool(self, key, value):
self.store[key] = bool(value)
def remove(self, key):
self.store.pop(key, None)
def _make_mbtiles(path):
conn = sqlite3.connect(path)
conn.execute("CREATE TABLE metadata (name text, value text)")
conn.execute("CREATE TABLE tiles (zoom_level integer, tile_column integer, tile_row integer, tile_data blob)")
conn.executemany("INSERT INTO metadata (name, value) VALUES (?, ?)",
[("format", "png"), ("minzoom", "10"), ("maxzoom", "16"),
("bounds", "-124.5,32.4,-114.1,42.1")])
conn.execute("INSERT INTO tiles VALUES (?, ?, ?, ?)", (10, 163, 396, PNG_1X1))
conn.commit()
conn.close()
@pytest.fixture
def hosting(tmp_path, monkeypatch):
"""Local static host serving index.json + a us_state.CA bundle; offline root redirected."""
serve_root = tmp_path / "serve"
serve_root.mkdir()
bundle = serve_root / "us_state.CA.mbtiles"
_make_mbtiles(bundle)
payload = bundle.read_bytes()
index = {
"version": 1,
"regions": {
"us_state.CA": {
"path": "us_state.CA.mbtiles",
"bytes": len(payload),
"sha256": hashlib.sha256(payload).hexdigest(),
"bounds": "-124.5,32.4,-114.1,42.1",
"minzoom": 10,
"maxzoom": 16,
"version": "20260709",
}
},
}
(serve_root / "index.json").write_text(json.dumps(index))
handler = partial(SimpleHTTPRequestHandler, directory=str(serve_root))
server = HTTPServer(("127.0.0.1", 0), handler)
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
offline_root = tmp_path / "offline_maps"
monkeypatch.setenv(offline_tiles.OFFLINE_TILE_ROOT_ENV, str(offline_root / "tiles"))
offline_tiles._region_roots_cache = None
offline_tiles._region_bounds_cache.clear()
params = FakeParams()
params.put(tbd.BASE_URL_PARAM, f"http://127.0.0.1:{server.server_address[1]}")
sessions: list = []
real_ctor = tbd.TileBundleDownloader.__init__
def tracking_ctor(self, *args, **kwargs):
real_ctor(self, *args, **kwargs)
sessions.append(self.session)
monkeypatch.setattr(tbd.TileBundleDownloader, "__init__", tracking_ctor)
try:
yield params, index, offline_root
finally:
for session in sessions:
session.close()
server.shutdown()
server.server_close()
def test_download_installs_bundle_and_manifest(hosting):
params, index, offline_root = hosting
dl = tbd.TileBundleDownloader(params=params, mem_params=params)
assert dl.download_regions(["us_state.CA"]) is True
installed = offline_root / "regions" / "us_state.CA" / "tiles" / "offline.mbtiles"
assert installed.exists()
manifest = json.loads((installed.parent.parent / "manifest.json").read_text())
assert manifest["mbtiles"]["bounds"] == "-124.5,32.4,-114.1,42.1"
assert manifest["mbtiles"]["sha256"] == index["regions"]["us_state.CA"]["sha256"]
# request/progress params cleaned up
assert params.get(tbd.REQUEST_PARAM) is None
assert params.get(tbd.PROGRESS_PARAM)["active"] is False
# and the on-screen map provider can find + read it
assert offline_tiles.find_offline_mbtiles_path(37.0, -120.0) == installed
conn = offline_tiles.open_mbtiles(installed)
try:
assert offline_tiles.mbtiles_is_raster(conn)
assert offline_tiles.load_raster_tile_blob(conn, 10, 163, 2 ** 10 - 1 - 396) == PNG_1X1
finally:
conn.close()
def test_skips_already_installed_matching_sha(hosting):
params, _, offline_root = hosting
dl = tbd.TileBundleDownloader(params=params, mem_params=params)
assert dl.download_regions(["us_state.CA"]) is True
installed = offline_root / "regions" / "us_state.CA" / "tiles" / "offline.mbtiles"
first_mtime = installed.stat().st_mtime_ns
assert dl.download_regions(["us_state.CA"]) is True
assert installed.stat().st_mtime_ns == first_mtime
def test_unknown_region_fails_cleanly(hosting):
params, _, offline_root = hosting
dl = tbd.TileBundleDownloader(params=params, mem_params=params)
assert dl.download_regions(["us_state.ZZ"]) is False
assert not (offline_root / "regions" / "us_state.ZZ").exists()
def test_resume_from_partial(hosting):
params, _, offline_root = hosting
part = offline_root / "regions" / "us_state.CA" / "tiles" / "offline.mbtiles.part"
part.parent.mkdir(parents=True)
# pre-seed the first half as an interrupted download
full = (offline_root / ".." / "serve" / "us_state.CA.mbtiles").resolve().read_bytes()
part.write_bytes(full[: len(full) // 2])
dl = tbd.TileBundleDownloader(params=params, mem_params=params)
assert dl.download_regions(["us_state.CA"]) is True
installed = part.parent / "offline.mbtiles"
assert installed.read_bytes() == full
assert not part.exists()
def test_cancel_aborts_before_install(hosting):
params, _, offline_root = hosting
dl = tbd.TileBundleDownloader(params=params, mem_params=params, abort_check=lambda: True)
assert dl.download_regions(["us_state.CA"]) is False
assert not (offline_root / "regions" / "us_state.CA" / "tiles" / "offline.mbtiles").exists()
# request param cleaned up so the UI doesn't show a stuck download
assert params.get(tbd.REQUEST_PARAM) is None
def test_sha_mismatch_rejected(hosting):
params, index, offline_root = hosting
index["regions"]["us_state.CA"]["sha256"] = "0" * 64
serve_root = (offline_root / ".." / "serve").resolve()
(serve_root / "index.json").write_text(json.dumps(index))
dl = tbd.TileBundleDownloader(params=params, mem_params=params)
assert dl.download_regions(["us_state.CA"]) is False
assert not (offline_root / "regions" / "us_state.CA" / "tiles" / "offline.mbtiles").exists()
def test_new_region_visible_without_process_restart(hosting):
"""Regression: lru_cache on _candidate_region_roots hid freshly downloaded regions."""
params, _, offline_root = hosting
# UI already scanned (and found nothing)
offline_tiles._region_roots_cache = None
assert offline_tiles.find_offline_mbtiles_path(37.0, -120.0) is None
dl = tbd.TileBundleDownloader(params=params, mem_params=params)
assert dl.download_regions(["us_state.CA"]) is True
# TTL cache: expire it and the new region shows up in the same process
offline_tiles._region_roots_cache = None
found = offline_tiles.find_offline_mbtiles_path(37.0, -120.0)
assert found is not None and found.exists()
def test_candidate_base_urls_param_override_wins():
params = FakeParams()
params.put(tbd.BASE_URL_PARAM, "https://my-r2.example.com/v1/")
assert tbd.candidate_base_urls(params) == ["https://my-r2.example.com/v1"]
def test_candidate_base_urls_private_endpoints_first():
params = FakeParams()
urls = tbd.candidate_base_urls(params)
# embedded private endpoints (gitea) come before the public default
assert urls[-1] == tbd.DEFAULT_TILE_BUNDLE_BASE_URL
if tbd._private_base_urls is not None:
assert any("git.konn3kt.com" in url for url in urls[:-1])
assert tbd.request_auth() is not None
def test_day_variant_downloaded_and_manifested(hosting, tmp_path):
params, index, offline_root = hosting
serve_root = (offline_root / ".." / "serve").resolve()
day_bundle = serve_root / "us_state.CA_day.mbtiles"
_make_mbtiles(day_bundle)
day_payload = day_bundle.read_bytes()
entry = index["regions"]["us_state.CA"]
entry["day_path"] = "us_state.CA_day.mbtiles"
entry["day_bytes"] = len(day_payload)
entry["day_sha256"] = hashlib.sha256(day_payload).hexdigest()
(serve_root / "index.json").write_text(json.dumps(index))
dl = tbd.TileBundleDownloader(params=params, mem_params=params)
assert dl.download_regions(["us_state.CA"]) is True
tiles = offline_root / "regions" / "us_state.CA" / "tiles"
assert (tiles / "offline.mbtiles").exists()
assert (tiles / "offline_day.mbtiles").exists()
manifest = json.loads((tiles.parent / "manifest.json").read_text())
assert manifest["mbtiles_day"]["sha256"] == entry["day_sha256"]
# installed-and-current check must account for the day file
assert dl._installed_matches("us_state.CA", entry) is True
(tiles / "offline_day.mbtiles").unlink()
assert dl._installed_matches("us_state.CA", entry) is False