mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-10-21 15:31:59 +00:00
Keep Companion and Terminal Chat profiles unchanged; retain full-sensor and reduced-sensor OTA builds. Add compact crypto and OTA names, qualified display asset compression, font license packaging, and scope regression tests.
248 lines
13 KiB
Python
248 lines
13 KiB
Python
"""nRF52 font storage choices preserve renderer behavior and stay in scope."""
|
|
import hashlib
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
FIXTURES = ROOT / "test" / "fixtures" / "oled_font_experiments"
|
|
|
|
|
|
class OledFontExperimentsTest(unittest.TestCase):
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
cls.temp = tempfile.TemporaryDirectory()
|
|
cls.directory = Path(cls.temp.name)
|
|
cls.decoder = cls.directory / "tinf.o"
|
|
subprocess.run([
|
|
"cc", "-Os", "-DMESHCORE_TINF_IMPLEMENTATION=1", "-c",
|
|
str(ROOT / "src/helpers/ota/tinf/tinflate.c"), "-o", str(cls.decoder),
|
|
], check=True)
|
|
cls.results = {}
|
|
|
|
@classmethod
|
|
def tearDownClass(cls):
|
|
cls.temp.cleanup()
|
|
|
|
def run_renderer(self, mode, flags=(), signed_char=False, sanitized=False, explicit=True):
|
|
key = (mode, tuple(flags), signed_char, sanitized, explicit)
|
|
if key in self.results:
|
|
return self.results[key]
|
|
executable = self.directory / f"font_{len(self.results)}"
|
|
subprocess.run([
|
|
"c++", "-std=c++11", "-O2", "-Wall", "-Wextra",
|
|
"-Wno-implicit-fallthrough", "-DARDUINO=1",
|
|
"-fsigned-char" if signed_char else "-funsigned-char",
|
|
*( ["-fsanitize=address,undefined", "-fno-omit-frame-pointer"] if sanitized else [] ),
|
|
"-DOLEDDISPLAY_REDUCE_MEMORY=1",
|
|
*([f"-DMESH_ARIAL_EXPERIMENT={mode}"] if explicit else []),
|
|
f"-DEXPECT_MODE={mode if not flags else 0}",
|
|
*(flags or ("-DNRF52_PLATFORM=1", "-DMESH_NRF52_FLASH_TRIM=1", "-DST7789=1")),
|
|
f"-I{FIXTURES}", f"-I{ROOT / 'src'}",
|
|
str(FIXTURES / "test.cpp"),
|
|
str(ROOT / "src/helpers/ui/OLEDDisplay.cpp"),
|
|
str(ROOT / "src/helpers/ui/OLEDDisplayFonts.cpp"),
|
|
str(self.decoder), "-o", str(executable),
|
|
], check=True)
|
|
symbols = subprocess.run(["nm", "-C", "--defined-only", str(executable)],
|
|
text=True, capture_output=True, check=True).stdout
|
|
if not flags and mode != 0:
|
|
self.assertNotIn("ArialMT_Plain_10", symbols, "Unused initial Arial10 is still linked")
|
|
result = subprocess.run([str(executable)], text=True, capture_output=True, check=True)
|
|
self.results[key] = result.stdout.splitlines()
|
|
return self.results[key]
|
|
|
|
def test_all_glyphs_metrics_and_framebuffers_match(self):
|
|
baseline = self.run_renderer(0)
|
|
self.assertEqual(baseline[0], "initial 13")
|
|
self.assertEqual([line.split()[0] for line in baseline[1:]], [
|
|
"glyphs", "clipping", "utf8", "wrapping", "controls", "bitmaps", "logs", "custom"])
|
|
self.assertTrue(baseline[1].startswith("glyphs 448 "))
|
|
self.assertTrue(baseline[2].startswith("clipping 960 "))
|
|
for mode in (1, 2, 3):
|
|
with self.subTest(mode=mode):
|
|
actual = self.run_renderer(mode)
|
|
self.assertEqual(actual[0], "initial 19")
|
|
self.assertEqual(actual[1:], baseline[1:])
|
|
|
|
def test_experiment_guards_signed_char_latin1_indices(self):
|
|
baseline = self.run_renderer(0)
|
|
for mode in (1, 2, 3):
|
|
with self.subTest(mode=mode):
|
|
self.assertEqual(self.run_renderer(mode, signed_char=True)[1:], baseline[1:])
|
|
|
|
def test_experimental_renderer_passes_memory_and_undefined_behavior_checks(self):
|
|
baseline = self.run_renderer(0)
|
|
for mode in (1, 2, 3):
|
|
with self.subTest(mode=mode):
|
|
actual = self.run_renderer(mode, signed_char=True, sanitized=True)
|
|
self.assertEqual(actual[1:], baseline[1:])
|
|
|
|
def test_tinf_is_enabled_only_for_effective_zopfli_font_or_existing_features(self):
|
|
qualified = ("-DNRF52_PLATFORM=1", "-DMESH_NRF52_FLASH_TRIM=1", "-DST7789=1")
|
|
cases = [(*qualified, f"-DMESH_ARIAL_EXPERIMENT={mode}") for mode in range(10)]
|
|
expected = [mode in (3, 9) for mode in range(10)]
|
|
cases.append(qualified) # No override: production compressed Noto font.
|
|
expected.append(True)
|
|
cases.extend([
|
|
(*qualified, "-DMESH_NRF52_FONT_MODE=0"),
|
|
(*qualified, "-DMESH_NRF52_FONT_MODE=9"),
|
|
(*qualified, "-DMESH_NRF52_FONT_MODE=9", "-DMESH_ARIAL_EXPERIMENT=0"),
|
|
])
|
|
expected.extend((False, True, False))
|
|
excluded = (
|
|
("-DNRF52_PLATFORM=1", "-DST7789=1"),
|
|
("-DNRF52_PLATFORM=1", "-DMESH_NRF52_FLASH_TRIM=0", "-DST7789=1"),
|
|
("-DMESH_NRF52_FLASH_TRIM=1", "-DST7789=1"),
|
|
("-DNRF52_PLATFORM=1", "-DMESH_NRF52_FLASH_TRIM=1"),
|
|
(*qualified, "-DCOMPANION_RADIO_FULL=1"),
|
|
)
|
|
cases.extend((*flags, "-DMESH_ARIAL_EXPERIMENT=3") for flags in excluded)
|
|
expected.extend(False for _ in excluded)
|
|
cases.extend((*flags, "-DMESH_ARIAL_EXPERIMENT=9") for flags in excluded)
|
|
expected.extend(False for _ in excluded)
|
|
cases.extend(excluded) # Production default remains off outside scope.
|
|
expected.extend(False for _ in excluded)
|
|
cases.extend((*flags, "-DMESH_NRF52_FONT_MODE=9") for flags in excluded)
|
|
expected.extend(False for _ in excluded)
|
|
cases.extend([
|
|
("-DENABLE_OTA=1",), ("-DOTA_TRANSPORT_DEFLATE_TEST=1",),
|
|
("-DNRF52_PLATFORM=1", "-DCOMPANION_RADIO_FULL=1", "-DENABLE_USB_INTERFACE=1"),
|
|
])
|
|
expected.extend((True, True, True))
|
|
for flags, decoder_enabled in zip(cases, expected):
|
|
with self.subTest(flags=flags):
|
|
output = self.directory / "guard.o"
|
|
subprocess.run([
|
|
"cc", "-Os", *flags, "-c", str(ROOT / "src/helpers/ota/OtaTinf.c"),
|
|
"-o", str(output),
|
|
], check=True)
|
|
symbols = subprocess.run(
|
|
["nm", "-g", "--defined-only", str(output)],
|
|
text=True, capture_output=True, check=True).stdout
|
|
self.assertEqual("tinf_uncompress_exact" in symbols, decoder_enabled)
|
|
|
|
def test_similar_size_family_trials_match_raw_pixels_and_keep_full_coverage(self):
|
|
families = []
|
|
for mode in (4, 5, 6, 7, 8):
|
|
with self.subTest(mode=mode):
|
|
unsigned = self.run_renderer(mode)
|
|
self.assertEqual(unsigned[0], "initial 19")
|
|
self.assertTrue(unsigned[1].startswith("glyphs 448 "))
|
|
self.assertTrue(unsigned[2].startswith("clipping 960 "))
|
|
# The fixture compares every glyph against a separate
|
|
# pixel-at-a-time reference and checks 189 drawable slots,
|
|
# exact 19/28 line boxes and 12/17-pixel H cap heights.
|
|
self.assertEqual(self.run_renderer(mode, signed_char=True), unsigned)
|
|
self.assertEqual(self.run_renderer(mode, signed_char=True, sanitized=True), unsigned)
|
|
families.append(unsigned[1])
|
|
self.assertEqual(len(set(families)), 5, "family trials unexpectedly contain the same pixels")
|
|
|
|
def test_production_compressed_noto_default_matches_raw_family_exactly(self):
|
|
raw_noto = self.run_renderer(7)
|
|
compressed = self.run_renderer(9)
|
|
self.assertEqual(compressed, raw_noto)
|
|
self.assertEqual(self.run_renderer(9, explicit=False), raw_noto)
|
|
self.assertEqual(self.run_renderer(9, signed_char=True), raw_noto)
|
|
self.assertEqual(self.run_renderer(9, signed_char=True, sanitized=True), raw_noto)
|
|
self.assertEqual(self.run_renderer(9, signed_char=True, sanitized=True, explicit=False), raw_noto)
|
|
|
|
def test_production_font_optout_restores_original_renderer(self):
|
|
qualified = ("-DNRF52_PLATFORM=1", "-DMESH_NRF52_FLASH_TRIM=1", "-DST7789=1")
|
|
baseline = self.run_renderer(0)
|
|
self.assertEqual(self.run_renderer(
|
|
9, (*qualified, "-DMESH_NRF52_FONT_MODE=0"), explicit=False), baseline)
|
|
self.assertEqual(self.run_renderer(
|
|
0, (*qualified, "-DMESH_NRF52_FONT_MODE=9")), baseline)
|
|
|
|
def test_experiment_is_disabled_outside_nrf52_infrastructure_st7789(self):
|
|
baseline = self.run_renderer(0)
|
|
excluded = (
|
|
("-DNRF52_PLATFORM=1", "-DST7789=1"), # Companion: no trim macro
|
|
("-DNRF52_PLATFORM=1", "-DMESH_NRF52_FLASH_TRIM=0", "-DST7789=1"),
|
|
("-DNRF52_PLATFORM=1", "-DMESH_NRF52_FLASH_TRIM=1", "-DST7789=1",
|
|
"-DCOMPANION_RADIO_FULL=1"),
|
|
("-DMESH_NRF52_FLASH_TRIM=1", "-DST7789=1"), # not nRF52
|
|
("-DNRF52_PLATFORM=1", "-DMESH_NRF52_FLASH_TRIM=1"), # another display
|
|
)
|
|
for mode in (3, 8, 9):
|
|
for flags in excluded:
|
|
with self.subTest(mode=mode, flags=flags):
|
|
self.assertEqual(self.run_renderer(mode, flags), baseline)
|
|
for flags in excluded:
|
|
with self.subTest(default=True, flags=flags):
|
|
self.assertEqual(self.run_renderer(9, flags, explicit=False), baseline)
|
|
with self.subTest(override=True, flags=flags):
|
|
self.assertEqual(self.run_renderer(
|
|
9, (*flags, "-DMESH_NRF52_FONT_MODE=9"), explicit=False), baseline)
|
|
|
|
|
|
class FontTrialSourceAuditTest(unittest.TestCase):
|
|
def test_pinned_source_glyphs_keep_all_ink_pixels_when_sources_are_available(self):
|
|
# Normal firmware builds/CI need no TrueType files or rasterizer. The
|
|
# optional regeneration audit runs on the host that owns pinned fonts.
|
|
try:
|
|
from PIL import Image, ImageDraw, ImageFont, features
|
|
except ImportError:
|
|
self.skipTest("Pinned rasterizer is not installed")
|
|
sys.path.insert(0, str(ROOT / "scripts"))
|
|
import generate_font_family_trials as generator
|
|
if Image.__version__ != generator.PILLOW_VERSION or features.version("freetype2") != generator.FREETYPE_VERSION:
|
|
self.skipTest("Pinned Pillow/FreeType versions are not installed")
|
|
font_directory = Path("C:/Windows/Fonts")
|
|
if not font_directory.exists():
|
|
font_directory = Path("/mnt/c/Windows/Fonts")
|
|
if not all((font_directory / family.filename).exists() for family in generator.FAMILIES):
|
|
self.skipTest("Pinned source font files are not available")
|
|
header = (ROOT / "src/helpers/ui/FontFamilyTrials.h").read_text(encoding="utf-8")
|
|
for family in generator.FAMILIES:
|
|
source = font_directory / family.filename
|
|
self.assertEqual(hashlib.sha256(source.read_bytes()).hexdigest(), family.sha256)
|
|
section_match = re.search(
|
|
rf"#if MESH_ARIAL_EXPERIMENT == {family.mode}\b(.*?)#endif", header, re.S)
|
|
self.assertIsNotNone(section_match, "Generated header lacks a configured font family")
|
|
section = section_match[1]
|
|
for font_id, size in enumerate(family.sizes, 1):
|
|
body = re.search(rf"static const uint8_t TRIAL_{font_id}\[\] = \{{(.*?)\}};", section, re.S)[1]
|
|
data = bytes(int(value, 16) for value in re.findall(r"0x([0-9a-fA-F]+)", body))
|
|
first, count, height = data[2], data[3], data[1]
|
|
self.assertEqual((first, count, height), (32, 224, 19 if font_id == 1 else 28))
|
|
source_font = ImageFont.truetype(str(source), size, layout_engine=ImageFont.Layout.BASIC)
|
|
drawable = 0
|
|
for slot in range(count):
|
|
entry = data[4 + 4 * slot:8 + 4 * slot]
|
|
offset = int.from_bytes(entry[:2], "big")
|
|
length = entry[2]
|
|
if offset == 0xffff:
|
|
self.assertEqual(length, 0)
|
|
continue
|
|
drawable += 1
|
|
bitmap = data[4 + 4 * count + offset:4 + 4 * count + offset + length]
|
|
stride = (height + 7) // 8
|
|
encoded = {(index // stride, 8 * (index % stride) + bit)
|
|
for index, value in enumerate(bitmap)
|
|
for bit in range(8) if value & (1 << bit)}
|
|
image = Image.new("L", (256, 256), 0)
|
|
ImageDraw.Draw(image).text((96, 96), chr(first + slot),
|
|
font=source_font, fill=255, anchor="ls")
|
|
image = image.point(lambda value: 255 if value >= 128 else 0)
|
|
box = image.getbbox()
|
|
self.assertIsNotNone(box)
|
|
original = {(x, y) for y in range(box[1], box[3])
|
|
for x in range(box[0], box[2]) if image.getpixel((x, y))}
|
|
def normalize(pixels):
|
|
left = min(x for x, _ in pixels)
|
|
top = min(y for _, y in pixels)
|
|
return {(x - left, y - top) for x, y in pixels}
|
|
with self.subTest(mode=family.mode, size=size, code=first + slot):
|
|
self.assertEqual(normalize(encoded), normalize(original),
|
|
"Source ink was cropped or altered during bitmap placement")
|
|
self.assertEqual(drawable, 189)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|