Files
MeshCore/test/test_rtttl_rollover.py
T

233 lines
12 KiB
Python

"""Exercise reviewed real RTTTL dependencies with actual 32-bit clock widths.
The freestanding ELF32 binaries do not require 32-bit libc/libstdc++ packages.
Only the clock, pins, IRQ controller and PWM registers are mocked. The queue
test compiles the real buzzer, decoder, clip catalog and voice-player sources.
"""
from pathlib import Path
import importlib.util
import json
import platform
import shutil
import subprocess
import tempfile
import unittest
ROOT = Path(__file__).resolve().parents[1]
FIXTURES = Path(__file__).resolve().parent / "fixtures/rtttl_rollover"
SPEC = importlib.util.spec_from_file_location("rtttl_rollover_fix", ROOT / "scripts/rtttl_rollover_fix.py")
FIX = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(FIX)
CXX = shutil.which("g++")
VERSIONS = ("1.3.0", "1.4.0")
PINS = {
"1.3.0": (
"b50c159c8806d177398972bf8f94ca762532a855b60cb1d3aa9c3ebf021423ca",
"abea0a4870e58222fdefd1e88d291136518912e83880af30d1c85d39d7bd5cc1",
"29204341d99b8adeef501c0d4cb47a4137534ebe6a30f2e6b3032d558aeef802",
),
"1.4.0": (
"d4fddfa663c8adeacea3b1daf56ec5fbe63ecaaf68e5477ac96dc4fb2e28d363",
"5a33eec77e2ba53ed9b83a8242633b8ae11873d3b4f8cfd257abbb2eb61b6220",
"e7710c4e3309b25517f5bace697c0c2574a5ce219f3cb156d352f739c178d75f",
),
}
def source_for(version):
path = FIXTURES / version / "src/NonBlockingRtttl.cpp.json"
return json.loads(path.read_text(encoding="ascii"))["source"]
def header_for(version):
return (FIXTURES / version / "src/NonBlockingRtttl.h").read_text(encoding="ascii")
class Node:
def __init__(self, path):
self.path = Path(path)
def srcnode(self):
return self
def get_abspath(self):
return str(self.path)
class BuildEnv:
def __init__(self, build):
self.build = Path(build)
self.registrations = []
def subst(self, value):
assert value == "$BUILD_DIR"
return str(self.build)
def File(self, value):
return Path(value)
def AddBuildMiddleware(self, callback, pattern):
self.registrations.append((callback, pattern))
class TransformTests(unittest.TestCase):
def test_fixtures_are_ascii_licensed_and_exact_reviewed_bytes_after_json_decode(self):
for path in FIXTURES.rglob("*"):
if path.is_file():
path.read_text(encoding="ascii")
for version in VERSIONS:
with self.subTest(version=version):
source, header = source_for(version), header_for(version)
expected_source, expected_header, _ = PINS[version]
self.assertEqual(FIX.digest(source), expected_source)
self.assertEqual(FIX.digest(header), expected_header)
self.assertIn("#define NONBLOCKINGRTTTL_VERSION " + version, header)
license_text = (FIXTURES / version / "LICENSE").read_text(encoding="ascii")
self.assertIn("MIT License", license_text)
self.assertIn("Copyright (c) 2018 Antoine Beauchamp", license_text)
self.assertIn("Permission is hereby granted, free of charge", license_text)
self.assertIn("THE SOFTWARE IS PROVIDED", license_text)
self.assertIn("Antoine Beauchamp", (FIXTURES / version / "AUTHORS").read_text())
def test_both_reviewed_versions_raw_crlf_idempotence_and_expected_patch_digest(self):
for version in VERSIONS:
with self.subTest(version=version):
original = source_for(version)
patched = FIX.patched_source(original)
self.assertEqual(FIX.digest(patched), PINS[version][2])
patched.encode("ascii")
self.assertEqual(FIX.patched_source(original.replace("\n", "\r\n")), patched)
self.assertEqual(FIX.patched_source(patched), patched)
self.assertEqual(FIX.patched_source(patched.replace("\n", "\r\n")), patched)
self.assertEqual(patched.count(FIX.PATCHED_BEGIN), 1)
self.assertEqual(patched.count(FIX.PATCHED_COMPARE), 1)
def test_unknown_changed_or_half_patched_sources_fail_closed(self):
for version in VERSIONS:
original = source_for(version)
for changed in (
original + "\n", original.replace("bpm=63", "bpm=64", 1),
original.replace(FIX.ORIGINAL_BEGIN, FIX.PATCHED_BEGIN, 1),
original.replace(FIX.ORIGINAL_COMPARE, FIX.PATCHED_COMPARE, 1),
):
with self.subTest(version=version), self.assertRaisesRegex(RuntimeError, "changed reviewed source"):
FIX.patched_source(changed)
def test_middleware_registers_only_expected_source_filter(self):
env = BuildEnv("unused")
FIX.install(env)
self.assertEqual(env.registrations, [(FIX.replace_rtttl_source, "*NonBlockingRtttl.cpp")])
def test_unrelated_source_is_unchanged_and_not_read(self):
with tempfile.TemporaryDirectory() as temporary:
env = BuildEnv(Path(temporary) / "build")
for path in ("src/not_rtttl.cpp", "other/NonBlockingRtttl.cpp"):
node = Node(Path(temporary) / path)
self.assertIs(FIX.replace_rtttl_source(env, node), node)
self.assertFalse(env.build.exists())
def test_private_source_and_header_are_paired_idempotent_and_leave_shared_files_unchanged(self):
for version in VERSIONS:
for crlf in (False, True):
with self.subTest(version=version, crlf=crlf), tempfile.TemporaryDirectory() as temporary:
directory = Path(temporary)
source = directory / "libdeps/NonBlockingRTTTL/src/NonBlockingRtttl.cpp"
source.parent.mkdir(parents=True)
original, header = source_for(version), header_for(version)
if crlf:
original, header = original.replace("\n", "\r\n"), header.replace("\n", "\r\n")
source.write_bytes(original.encode("utf-8"))
source.with_suffix(".h").write_bytes(header.encode("ascii"))
before = (source.read_bytes(), source.with_suffix(".h").read_bytes())
env = BuildEnv(directory / "build")
destination = FIX.replace_rtttl_source(env, Node(source))
self.assertEqual(destination, env.build / "patched-rtttl/NonBlockingRtttl.cpp")
self.assertEqual(FIX.digest(destination.read_text(encoding="ascii")), PINS[version][2])
self.assertEqual(destination.with_suffix(".h").read_text(encoding="ascii"), header_for(version))
times = (destination.stat().st_mtime_ns, destination.with_suffix(".h").stat().st_mtime_ns)
self.assertEqual(FIX.replace_rtttl_source(env, Node(source)), destination)
self.assertEqual(times, (destination.stat().st_mtime_ns, destination.with_suffix(".h").stat().st_mtime_ns))
self.assertEqual(before, (source.read_bytes(), source.with_suffix(".h").read_bytes()))
def test_missing_unknown_or_wrong_version_headers_fail_before_creating_private_output(self):
for version in VERSIONS:
other = "1.4.0" if version == "1.3.0" else "1.3.0"
for header in (None, header_for(version) + "\n", header_for(other), "// unknown\n", "\u00b0"):
with self.subTest(version=version, header=header is None), tempfile.TemporaryDirectory() as temporary:
directory = Path(temporary)
source = directory / "libdeps/NonBlockingRTTTL/src/NonBlockingRtttl.cpp"
source.parent.mkdir(parents=True)
source.write_text(source_for(version), encoding="utf-8")
if header is not None:
source.with_suffix(".h").write_text(header, encoding="utf-8")
env = BuildEnv(directory / "build")
with self.assertRaisesRegex(RuntimeError, "reviewed header"):
FIX.replace_rtttl_source(env, Node(source))
self.assertFalse(env.build.exists())
@unittest.skipUnless(CXX and platform.system() == "Linux" and platform.machine() in ("x86_64", "i386", "i686"),
"Freestanding ELF32 tests require Linux x86 and g++")
class RuntimeTests(unittest.TestCase):
def build_run(self, dependency, version, fixture):
with tempfile.TemporaryDirectory() as temporary:
directory = Path(temporary)
source = directory / "NonBlockingRtttl.cpp"
source.write_text(dependency, encoding="utf-8")
source.with_suffix(".h").write_text(header_for(version), encoding="ascii")
binary = directory / "case"
flags = ["-DRTTTL_NO_LOOPS=1"] if version == "1.3.0" else []
subprocess.run([
CXX, "-m32", "-std=c++17", "-Wall", "-Wextra", "-Werror", *flags,
"-fno-exceptions", "-fno-rtti", "-fno-stack-protector", "-nostdlib", "-no-pie",
"-I" + str(FIXTURES), "-I" + str(directory), "-I" + str(ROOT / "src"),
str(FIXTURES / fixture), str(source), "-o", str(binary),
], check=True, capture_output=True, timeout=30)
self.assertEqual(binary.read_bytes()[:5], b"\x7fELF\x01")
return subprocess.run([str(binary)], capture_output=True, timeout=10).returncode
def test_actual_unpatched_versions_reproduce_both_semantic_rollover_failures(self):
for version in VERSIONS:
with self.subTest(version=version):
self.assertEqual(self.build_run(source_for(version), version, "regression.cpp"), 30)
self.assertEqual(self.build_run(source_for(version), version, "queue.cpp"), 33)
def test_patched_versions_keep_tempo_empty_stop_replacement_and_real_voice_queue(self):
for version in VERSIONS:
with self.subTest(version=version):
patched = FIX.patched_source(source_for(version))
self.assertEqual(self.build_run(patched, version, "regression.cpp"), 0)
self.assertEqual(self.build_run(patched, version, "queue.cpp"), 0)
def test_each_timing_fix_is_required_not_merely_accepted_by_source_checks(self):
for version in VERSIONS:
with self.subTest(version=version):
original = source_for(version)
initial_only = original.replace(FIX.ORIGINAL_BEGIN, FIX.PATCHED_BEGIN, 1)
signed_only = original.replace(FIX.ORIGINAL_COMPARE, FIX.PATCHED_COMPARE, 1)
self.assertEqual(self.build_run(initial_only, version, "regression.cpp"), 30)
self.assertEqual(self.build_run(signed_only, version, "regression.cpp"), 28)
def test_private_cpp_quotes_its_paired_header_even_with_poison_include_path_first(self):
for version in VERSIONS:
with self.subTest(version=version), tempfile.TemporaryDirectory() as temporary:
directory = Path(temporary)
original = directory / "libdeps/NonBlockingRTTTL/src/NonBlockingRtttl.cpp"
original.parent.mkdir(parents=True)
original.write_text(source_for(version), encoding="utf-8")
original.with_suffix(".h").write_text(header_for(version), encoding="ascii")
destination = FIX.replace_rtttl_source(BuildEnv(directory / "build"), Node(original))
poison = directory / "poison"
poison.mkdir()
(poison / "NonBlockingRtttl.h").write_text('#error "wrong header selected"\n', encoding="ascii")
subprocess.run([
CXX, "-m32", "-std=c++17", "-Wall", "-Wextra", "-Werror",
"-fno-exceptions", "-fno-rtti", "-fno-stack-protector",
"-I" + str(poison), "-I" + str(FIXTURES),
"-c", str(destination), "-o", str(directory / "dependency.o"),
], check=True, capture_output=True, timeout=30)
if __name__ == "__main__":
unittest.main(verbosity=2)