120 lines
6.1 KiB
Python
120 lines
6.1 KiB
Python
#!/usr/bin/env python3
|
|
"""Qualify the real bounded world-map decoder, projection and generated pack."""
|
|
import importlib.util
|
|
from pathlib import Path
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
import tempfile
|
|
import unittest
|
|
import zlib
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
SPEC = importlib.util.spec_from_file_location("pack_world_map", ROOT / "tools/meshpocket_map/pack_world_map.py")
|
|
packer = importlib.util.module_from_spec(SPEC)
|
|
SPEC.loader.exec_module(packer)
|
|
|
|
|
|
def run(command):
|
|
result = subprocess.run(command, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
|
if result.returncode:
|
|
raise AssertionError("command failed: " + " ".join(map(str, command)) + "\n" + result.stdout)
|
|
return result.stdout
|
|
|
|
|
|
class PocketWorldMapTests(unittest.TestCase):
|
|
def test_generated_pack_bounds_and_roundtrip(self):
|
|
header = packer.OUTPUT.read_text(encoding="utf-8")
|
|
chunks = [tuple(map(int, row)) for row in re.findall(
|
|
r"\{(\d+), (\d+), (\d+), (-?\d+), (-?\d+), (-?\d+), (-?\d+)\}", header)]
|
|
data_section = header.split("static const uint8_t kDeflate[] = {", 1)[1].split("};", 1)[0]
|
|
packed = bytes(int(byte, 16) for byte in re.findall(r"0x([0-9a-f]{2})", data_section))
|
|
self.assertEqual(len(chunks), 32)
|
|
self.assertEqual(len(packed) + 24 * len(chunks), 80658)
|
|
self.assertIn("kRingCount = 1422;", header)
|
|
offset = 0
|
|
for start, size, raw_size, west, south, east, north in chunks:
|
|
self.assertEqual(start, offset)
|
|
self.assertLessEqual(raw_size, 3072)
|
|
self.assertLessEqual(west, east)
|
|
self.assertLessEqual(south, north)
|
|
stream = zlib.decompressobj(wbits=-15)
|
|
raw = stream.decompress(packed[start:start + size])
|
|
self.assertTrue(stream.eof)
|
|
self.assertFalse(stream.unused_data)
|
|
self.assertEqual(len(raw), raw_size)
|
|
offset += size
|
|
self.assertEqual(offset, len(packed))
|
|
|
|
def test_simplification_retains_tiny_islands_and_dateline(self):
|
|
island = [(170.0, -20.0), (170.001, -20.0), (170.001, -20.001),
|
|
(170.0, -20.001), (170.0, -20.0)]
|
|
simplified = packer.simplify(island)
|
|
self.assertGreaterEqual(len(set(simplified)), 3)
|
|
self.assertEqual(simplified[0], simplified[-1])
|
|
crossing = [(179.8, -20.0), (-179.8, -20.0), (-179.8, -20.4),
|
|
(179.8, -20.4), (179.8, -20.0)]
|
|
simplified = packer.simplify(crossing)
|
|
self.assertLess(max(p[0] for p in simplified) - min(p[0] for p in simplified), 1000)
|
|
self.assertEqual(simplified[0], simplified[-1])
|
|
with self.assertRaisesRegex(ValueError, "SHA-256"):
|
|
list(packer.source_rings(b"incorrect source archive"))
|
|
|
|
def test_real_renderer_and_parser_under_sanitizers(self):
|
|
cc, cxx = shutil.which("gcc"), shutil.which("g++")
|
|
if not cc or not cxx:
|
|
self.skipTest("GCC/G++ unavailable")
|
|
with tempfile.TemporaryDirectory(prefix="mesh-pocket-map-") as temporary:
|
|
directory = Path(temporary)
|
|
# Windows MinGW commonly has no sanitizer runtime. Linux CI runs
|
|
# the same real renderer with both bounds/undefined checks active.
|
|
sanitizer = [] if __import__("os").name == "nt" else ["-fsanitize=address,undefined"]
|
|
run([cc, "-std=c11", "-DMESHCORE_TINF_IMPLEMENTATION", "-g", "-O1", *sanitizer,
|
|
"-c", str(ROOT / "src/helpers/ota/tinf/tinflate.c"), "-o", str(directory / "tinf.o")])
|
|
executable = directory / "world_map"
|
|
run([cxx, "-std=c++17", "-DPOCKET_WORLD_MAP_TEST", "-I" + str(ROOT / "src"),
|
|
"-g", "-O1", *sanitizer, str(ROOT / "test/fixtures/pocket_world_map/test_world_map.cpp"),
|
|
str(ROOT / "src/helpers/ui/PocketWorldMap.cpp"), str(directory / "tinf.o"), "-o", str(executable)])
|
|
self.assertIn("checks passed", run([str(executable)]))
|
|
|
|
def test_arm_decode_stack_fits_companion_loop(self):
|
|
tool = shutil.which("arm-none-eabi-g++")
|
|
if not tool:
|
|
candidate = Path.home() / ".platformio/packages/toolchain-gccarmnoneeabi/bin/arm-none-eabi-g++"
|
|
if candidate.is_file():
|
|
tool = str(candidate)
|
|
if not tool:
|
|
self.skipTest("ARM compiler unavailable")
|
|
cc = str(Path(tool).with_name("arm-none-eabi-gcc"))
|
|
with tempfile.TemporaryDirectory(prefix="mesh-pocket-map-stack-") as temporary:
|
|
directory = Path(temporary)
|
|
flags = ["-mcpu=cortex-m4", "-mthumb", "-Oz", "-fno-inline-small-functions", "-fstack-usage"]
|
|
run([tool, "-std=gnu++17", *flags, "-fno-exceptions", "-fno-rtti", "-DPOCKET_WORLD_MAP_TEST",
|
|
"-I" + str(ROOT / "src"), "-c", str(ROOT / "src/helpers/ui/PocketWorldMap.cpp"),
|
|
"-o", str(directory / "map.o")])
|
|
run([cc, "-std=gnu11", *flags, "-DMESHCORE_TINF_IMPLEMENTATION", "-c",
|
|
str(ROOT / "src/helpers/ota/tinf/tinflate.c"), "-o", str(directory / "tinf.o")])
|
|
|
|
def frames(name):
|
|
rows = {}
|
|
for line in (directory / (name + ".su")).read_text().splitlines():
|
|
function, size, kind = line.rsplit("\t", 2)
|
|
self.assertEqual(kind, "static", line)
|
|
rows[function] = int(size)
|
|
return rows
|
|
|
|
renderer, decoder = frames("map"), frames("tinf")
|
|
entry = next(size for name, size in renderer.items() if "::renderWorldMap(" in name)
|
|
# Decoder helper sum overbounds mutually exclusive branches.
|
|
decode = sum(size for name, size in decoder.items()
|
|
if not name.endswith(":tinf_init") and not name.endswith(":tinf_uncompress"))
|
|
draw = sum(size for name, size in renderer.items()
|
|
if any(signature in name for signature in ("::drawChunk(", "::project(", "::readDelta(")))
|
|
peak = entry + max(decode, draw)
|
|
self.assertLessEqual(peak + 1536, 8192)
|
|
print(f"ARM Pocket map local stack: {peak} bytes + 1536 caller reserve <= 8192")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|