Files
HaloKeymind/test/test_espnow_runtime.py
T

112 lines
6.9 KiB
Python

#!/usr/bin/env python3
"""Run the sole repeater ESP-NOW production lifecycle and CLI with fault injection."""
from pathlib import Path
import shutil
import re
import subprocess
import tempfile
import unittest
from test_replay_reset_integration import extract_braced
ROOT = Path(__file__).resolve().parents[1]
class ESPNowRuntimeTests(unittest.TestCase):
def test_production_defaults_aliases_routing_retries_and_ota_exclusion(self):
compiler = shutil.which("g++") or shutil.which("clang++")
self.assertIsNotNone(compiler, "a host C++ compiler is required")
cli = (ROOT / "src/helpers/CommonCLI.cpp").read_text(encoding="ascii")
header = (ROOT / "examples/simple_repeater/MyMesh.h").read_text(encoding="ascii")
callback_header = (ROOT / "src/helpers/CommonCLI.h").read_text(encoding="ascii")
implementation = (ROOT / "examples/simple_repeater/MyMesh.cpp").read_text(encoding="ascii")
defaults_start = implementation.index("// bridge defaults")
defaults_end = implementation.index("// GPS defaults", defaults_start)
boot_start = implementation.index('mesh::hilStartupTrace("mesh_bridge_begin");')
boot_start = implementation.index("#if defined(WITH_BRIDGE)", boot_start)
boot_end = implementation.index('mesh::hilStartupTrace("mesh_bridge_ready");', boot_start)
lifecycle_signatures = (
"AbstractBridge* activeBridge() {", "const AbstractBridge* activeBridge() const {",
"bool startSharedEspNowBridgeIfReady()", "bool isBridgeRunning() const override",
"bool setBridgeState(bool enable) override",
"bool restartBridge() override",
)
routing = {}
for name in ("logRx", "logTx"):
start = implementation.index("void MyMesh::" + name + "(")
start = implementation.index("#ifdef WITH_MQTT_BRIDGE\n", start)
end = implementation.index(" if (_logging)", start)
routing[name] = implementation[start:end]
service_start = implementation.index("#if defined(WITH_ESPNOW_BRIDGE)",
implementation.index("MyMesh::servicePostMeshLoop()"))
service_end = implementation.index(" if (next_flood_advert", service_start)
pending_start = implementation.index("#if defined(WITH_BRIDGE)",
implementation.index("bool MyMesh::hasPendingWork()"))
pending_end = implementation.index(" if (radio_driver.isWatchdogObserving()", pending_start)
commands = "\n".join(extract_braced(cli, signature) for signature in (
"void CommonCLI::handleSetCmd(", "void CommonCLI::handleGetCmd("))
template = (ROOT / "test/fixtures/espnow_runtime.cpp").read_text(encoding="ascii")
for key, value in {
"DEFAULTS": implementation[defaults_start:defaults_end],
"BOOT": implementation[boot_start:boot_end],
"CALLBACK_FORWARDS": "\n".join(extract_braced(callback_header, signature) for signature in (
"virtual bool setEspNowBridgeState(bool enable)",
"virtual bool restartEspNowBridge()", "virtual bool isEspNowBridgeRunning()")),
"PARSERS": "\n".join(extract_braced(cli, signature) for signature in (
"static bool parseOnOffStrict(", "static bool configKeyEquals(")),
"ROUTE_RX": routing["logRx"], "ROUTE_TX": routing["logTx"],
"SERVICE": implementation[service_start:service_end],
"PENDING": implementation[pending_start:pending_end],
}.items():
template = template.replace("@" + key + "@", value)
for profile, macros in (
("merged", ["ESPNOW_BRIDGE_MERGED=1"]),
("legacy_dedicated", []),
("merged_default_on", ["ESPNOW_BRIDGE_MERGED=1", "ESPNOW_BRIDGE_DEFAULT_ON=1"]),
):
with self.subTest(profile=profile), tempfile.TemporaryDirectory(prefix="espnow-runtime-") as directory:
work = Path(directory)
flags = ["-DWITH_BRIDGE=1", "-DWITH_ESPNOW_BRIDGE=1",
*["-D" + macro for macro in macros]]
processed = subprocess.run([compiler, "-E", "-P", "-x", "c++", *flags, "-"],
input=commands, capture_output=True, text=True, check=True).stdout
processed_header = subprocess.run([compiler, "-E", "-P", "-x", "c++", *flags, "-"],
input=re.sub(r'^\s*#include[^\n]*', '', header, flags=re.M),
capture_output=True, text=True, check=True).stdout
lifecycle = "\n".join(extract_braced(processed_header, signature)
for signature in lifecycle_signatures)
sets = "\nelse ".join(extract_braced(processed, signature) for signature in (
'if (memcmp(config, "espnow.enabled ", 15) == 0)',
'if (memcmp(config, "bridge.enabled ", 15) == 0)',
'if (memcmp(config, "bridge.source ", 14) == 0)',
'if (memcmp(config, "bridge.channel ", 15) == 0)',
'if (memcmp(config, "bridge.secret ", 14) == 0)',
'if (memcmp(config, "bridge.format ", 14) == 0)',
))
gets = "\nelse ".join(extract_braced(processed, signature) for signature in (
'if (configKeyEquals(config, "bridge.type"))',
'if (configKeyEquals(config, "bridge.enabled"))',
'if (configKeyEquals(config, "bridge.running"))',
'if (configKeyEquals(config, "espnow.enabled"))',
'if (configKeyEquals(config, "espnow.running"))',
'if (configKeyEquals(config, "bridge.channel"))',
'if (configKeyEquals(config, "bridge.secret"))',
'if (configKeyEquals(config, "bridge.format"))',
))
source, binary = work / "main.cpp", work / "test"
source.write_text(template.replace("@SET_BRANCHES@", sets)
.replace("@GET_BRANCHES@", gets)
.replace("@LIFECYCLE@", lifecycle), encoding="ascii")
result = subprocess.run([compiler, "-std=c++17", "-Wall", "-Wextra", "-Werror",
"-fsanitize=address,undefined", "-fno-sanitize-recover=all", "-fno-pie", "-no-pie",
*flags, "-I" + str(ROOT / "src"), str(source), "-o", str(binary)],
capture_output=True, text=True, timeout=60)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
result = subprocess.run([str(binary)], capture_output=True, text=True, timeout=10)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertIn("ESP-NOW runtime checks passed", result.stdout)
if __name__ == "__main__":
unittest.main()