Files
MeshCore/test/test_ota_target_names.py

143 lines
6.8 KiB
Python

"""All OTA names survive front coding; original Companion API stays unchanged."""
import re
import os
import shutil
import subprocess
import sys
import tempfile
import unittest
import zlib
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path[:0] = [str(ROOT / "tools" / "mota"), str(ROOT / "scripts")]
import gen_targets
HEADER = ROOT / "src" / "helpers" / "ota" / "OtaTargets.h"
HARNESS = ROOT / "test" / "fixtures" / "ota_target_names" / "test.cpp"
class OtaTargetNameTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.text = HEADER.read_text(encoding="utf-8")
cls.rows = [(int(target, 16), name) for target, name in re.findall(
r'\{ 0x([0-9a-fA-F]{8}), "([^"]+)" \},', cls.text)]
def compile_and_lookup(self, flags, ids, header=None):
with tempfile.TemporaryDirectory() as temp_dir:
executable = Path(temp_dir) / "target_names"
if header is not None:
alternate = Path(temp_dir) / "helpers/ota"
alternate.mkdir(parents=True)
(alternate / "OtaTargets.h").write_text(header, encoding="utf-8")
(alternate / "tinf").mkdir()
(alternate / "tinf/tinf.h").write_text(
(ROOT / "src/helpers/ota/tinf/tinf.h").read_text(), encoding="utf-8")
decoder = Path(temp_dir) / "tinf.o"
subprocess.run([
os.environ.get("CC") or shutil.which("cc") or "gcc",
"-Os", "-DMESHCORE_TINF_IMPLEMENTATION=1", "-c",
str(ROOT / "src/helpers/ota/tinf/tinflate.c"), "-o", str(decoder),
], check=True)
subprocess.run([
"c++", "-std=c++11", "-O2", "-Wall", "-Wextra", "-Werror",
*flags, f"-I{temp_dir}", f"-I{ROOT / 'src'}", str(HARNESS), str(decoder), "-o", str(executable),
], check=True)
result = subprocess.run(
[str(executable)], input="\n".join(f"{target:08x}" for target in ids),
text=True, capture_output=True, check=True)
return result.stdout.splitlines()
def test_generator_reproduces_checked_in_header_and_all_ids(self):
self.assertGreater(len(self.rows), 600)
self.assertEqual(self.rows, gen_targets.target_rows(name for _, name in self.rows))
self.assertEqual(self.text, gen_targets.generate_header(self.rows))
def test_zopfli_chunks_retain_every_name_and_have_bounded_output(self):
data, offsets, raw_lengths, firsts = gen_targets.compressed_name_chunks(self.rows)
actual = []
for group, raw_length in enumerate(raw_lengths):
raw = zlib.decompress(data[offsets[group]:offsets[group + 1]], -15)
self.assertEqual(len(raw), raw_length)
self.assertLessEqual(raw_length, 512)
position = 0
previous = b""
for row in range(firsts[group], firsts[group + 1]):
prefix = raw[position]
self.assertLessEqual(prefix, len(previous))
end = raw.index(0, position + 1)
previous = previous[:prefix] + raw[position + 1:end]
position = end + 1
actual.append(previous.decode("utf-8"))
self.assertEqual(position, len(raw))
self.assertEqual(actual, [name for _, name in self.rows])
original_size = sum(8 + len(name.encode("utf-8")) + 1 for _, name in self.rows)
compressed_size = 4 * len(self.rows) + 2 * (len(offsets) + len(raw_lengths) + len(firsts)) + len(data)
self.assertGreater(original_size - compressed_size, 19000)
def test_generator_requires_google_zopfli_1000(self):
from unittest.mock import patch
sys.path.insert(0, str(ROOT / "scripts"))
import zopfli_compress
gen_targets._compressed_name_chunks.cache_clear()
original = zopfli_compress.raw_deflate_compress
with patch.object(zopfli_compress, "raw_deflate_compress", wraps=original) as compress:
gen_targets.compressed_name_chunks([(1, "unique_test_target")])
self.assertEqual(compress.call_count, 1)
self.assertEqual(compress.call_args.kwargs, {"numiterations": 1000})
def test_cpp_standard_and_compact_match_all_names(self):
ids = [target for target, _ in self.rows] + [0, 0xffffffff]
expected = [name for _, name in self.rows] + ["?", "?"]
for flags in ([], ["-DOTA_TARGET_NAME_FRONT_CODED=1"]):
with self.subTest(flags=flags):
self.assertEqual(self.compile_and_lookup(flags, ids), expected)
def test_table_disabled_preserves_optional_local_name(self):
local_id, local_name = self.rows[0]
ids = [local_id, self.rows[1][0], 0]
for compressed in (0, 1):
flags = ["-DOTA_TARGET_NAME_TABLE=0", f"-DOTA_TARGET_NAME_FRONT_CODED={compressed}"]
with self.subTest(compressed=compressed):
self.assertEqual(self.compile_and_lookup(flags, ids), ["?", "?", "?"])
self.assertEqual(self.compile_and_lookup(flags + [
f"-DOTA_LOCAL_TARGET_ID=0x{local_id:08x}",
f'-DOTA_LOCAL_TARGET_NAME="{local_name}"',
], ids), [local_name, "?", "?"])
def test_cpp_decoder_fails_closed_on_invalid_records_and_trailing_bytes(self):
from zopfli_compress import raw_deflate_compress
rows = [(1, "hello")]
header = gen_targets.generate_header(rows)
original, _, _, _ = gen_targets.compressed_name_chunks(rows)
invalid = [
raw_deflate_compress(b"\xffhello\0", numiterations=1000), # invalid restart prefix
raw_deflate_compress(b"\0hello!", numiterations=1000), # missing terminator
original + b"\0", # trailing whole byte
original[:-1], # truncated input
]
for stream in invalid:
altered = re.sub(
r"(static const uint8_t NAMES\[\] = \{).*?(\n \};)",
lambda match: match[1] + "\n " + ", ".join(f"0x{byte:02x}" for byte in stream) + "," + match[2],
header, flags=re.S)
altered = re.sub(
r"(static const uint16_t OFFSETS\[\] = \{).*?(\n \};)",
lambda match: match[1] + f"\n 0, {len(stream)}," + match[2],
altered, flags=re.S)
with self.subTest(stream=stream.hex()):
self.assertEqual(self.compile_and_lookup(
["-DOTA_TARGET_NAME_FRONT_CODED=1"], [1], altered), ["?"])
def test_generator_rejects_unencodable_names(self):
for name in ("x\0y", "x" * 256):
with self.subTest(name=name):
with self.assertRaises(ValueError):
gen_targets.compressed_name_chunks([(1, name)])
if __name__ == "__main__":
unittest.main()