143 lines
6.8 KiB
Python
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()
|