Files
pyxis/tests/native/test_map_tile_store_path_capacity.py
T
Torlando b7ab97d624 test: add host regression for MapTileStoreSD mount-path overflow
A 31-character pack ID makes mounted tile paths
(/sd/pyxis-map/packs/<id>/tiles/<z>/<x>/<y>.png) exceed the 68-byte mount
buffer once either x or y is two digits, so beginRead returns
INVALID_ARGUMENT and the UI shows 'Tile I/O error' at z4+ even though the
file exists and is intact.

Compiles the unmodified MapTilePack/MapTileStoreSD/SDAccess/codec/manifest
sources against small Arduino/FreeRTOS host shims. The core section (no
card needed) drives the real store with a model of the mount-prefix
arithmetic and asserts the 31-char-ID boundary loads at z4/z5; an optional
end-to-end section runs the real MapTilePack over a bound card root when
/sd is writable.

Fails on the current 64-byte PATH_CAPACITY: red by design.
2026-08-28 22:44:27 +00:00

110 lines
4.6 KiB
Python

from __future__ import annotations
from pathlib import Path
import os
import shutil
import subprocess
import pytest
from native_test import find_cxx
ROOT = Path(__file__).resolve().parents[2]
TEST_SOURCE = ROOT / "tests/native/test_map_tile_store_path_capacity.cpp"
PACK_SOURCE = ROOT / "lib/tdeck_ui/Hardware/TDeck/MapTilePack.cpp"
STORE_SOURCE = ROOT / "lib/tdeck_ui/Hardware/TDeck/MapTileStoreSD.cpp"
SDACCESS_SOURCE = ROOT / "lib/tdeck_ui/Hardware/TDeck/SDAccess.cpp"
CODEC_SOURCE = ROOT / "lib/tdeck_ui/Hardware/TDeck/ActiveMapSetCodec.cpp"
MANIFEST_SOURCE = ROOT / "lib/tdeck_ui/UI/LXMF/MapPackManifest.cpp"
SHIM_DIR = ROOT / "tests/native/sdhostshim"
def sudo_ok() -> bool:
return shutil.which("sudo") is not None and subprocess.run(
["sudo", "-n", "true"], capture_output=True, timeout=10
).returncode == 0
def sd_writable() -> bool:
return os.access("/sd", os.W_OK)
def run_test(binary: Path, card_root: Path, env: dict, timeout: int) -> subprocess.CompletedProcess[str]:
"""Run the test binary, arranging a /sd bind mount for the end-to-end
section when possible.
The production store reads the literal "/sd" mount point, so the
end-to-end card-content section only runs when "/sd" is a writable
directory. Without a usable password-less sudo we still run the binary
normally: the core regression section (mount-independent) executes, and
the binary prints a skip note for the card section.
"""
command = [str(binary), str(card_root)]
mounted = False
run_as_root = False
if not sd_writable() and sudo_ok():
probe = subprocess.run(["mountpoint", "-q", "/sd"], capture_output=True, timeout=10)
if probe.returncode == 0:
# A live mount we did not create: leave it alone, run normally.
pass
else:
subprocess.run(
["sudo", "-n", "mkdir", "-p", "/sd"], capture_output=True, timeout=10)
mnt = subprocess.run(
["sudo", "-n", "mount", "--bind", str(card_root), "/sd"],
capture_output=True, text=True, timeout=30,
)
if mnt.returncode == 0:
mounted = True
run_as_root = True # bind mount content is root-owned
else:
raise pytest.skip(f"could not bind-mount card root at /sd: {mnt.stderr.strip()}")
if run_as_root:
command = ["sudo", "-n"] + command
try:
return subprocess.run(command, capture_output=True, text=True,
timeout=timeout, env=env)
finally:
if mounted:
subprocess.run(["sudo", "-n", "umount", "/sd"], capture_output=True, timeout=30)
@pytest.mark.parametrize("sanitize", [False, True], ids=["strict-cxx11", "asan-ubsan"])
def test_map_tile_store_path_capacity(tmp_path: Path, sanitize: bool) -> None:
# Note: -Wconversion/-Wsign-conversion are deliberately omitted here (and
# only here): SDAccess.cpp is ARDUINO-gated production code that only ever
# compiles under PlatformIO's ESP32 flag set, where the uint8_t/int
# Arduino API conversions are routine. The other flags above are kept so
# the unmodified store/pack sources stay strictly checked.
binary = tmp_path / "test_map_tile_store_path_capacity"
card_root = tmp_path / "card"
card_root.mkdir()
command = [
find_cxx(), "-std=c++11", "-Wall", "-Wextra", "-Werror", "-pedantic",
"-DARDUINO=100",
f"-I{SHIM_DIR}", f"-I{ROOT / 'lib/tdeck_ui'}",
str(TEST_SOURCE), str(PACK_SOURCE), str(STORE_SOURCE),
str(SDACCESS_SOURCE), str(CODEC_SOURCE), str(MANIFEST_SOURCE),
"-o", str(binary),
]
if sanitize:
command[1:1] = ["-fsanitize=address,undefined", "-fno-omit-frame-pointer"]
compiled = subprocess.run(command, capture_output=True, text=True, timeout=120,
cwd=ROOT)
assert compiled.returncode == 0, compiled.stdout + compiled.stderr
env = os.environ.copy()
if sanitize:
env["ASAN_OPTIONS"] = "detect_leaks=1:halt_on_error=1"
env["UBSAN_OPTIONS"] = "halt_on_error=1:print_stacktrace=1"
ran = run_test(binary, card_root, env, timeout=120)
assert ran.returncode == 0, ran.stdout + ran.stderr
# The end-to-end section (4 tests) is conditional on a writable /sd mount;
# the core regression section (6 tests) always runs.
for expected in (
"map tile store path capacity: 6 tests passed\n",
"map tile store path capacity: 10 tests passed\n",
):
if ran.stdout == expected:
return
raise AssertionError(f"unexpected output: {ran.stdout!r}")