Files
MeshCore/test/test_rs232_mqtt_runtime.py
T

230 lines
16 KiB
Python

#!/usr/bin/env python3
"""Execute real UART/MQTT/ESP-NOW repeater lifecycle and CLI independently."""
from pathlib import Path
import re
import shutil
import subprocess
import tempfile
import unittest
from test_replay_reset_integration import extract_braced
ROOT = Path(__file__).resolve().parents[1]
class RS232MQTTRuntimeTests(unittest.TestCase):
def test_independent_transports_route_retry_restart_and_survive_browser_ota(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")
observer = (ROOT / "src/helpers/CommonCLI_Observer.cpp").read_text(encoding="ascii")
header = (ROOT / "examples/simple_repeater/MyMesh.h").read_text(encoding="ascii")
implementation = (ROOT / "examples/simple_repeater/MyMesh.cpp").read_text(encoding="ascii")
flags = ["-DWITH_RS232_BRIDGE=Serial2", "-DWITH_RS232_BRIDGE_UART=2",
"-DWITH_RS232_BRIDGE_RX=5", "-DWITH_RS232_BRIDGE_TX=6",
"-DRS232_BRIDGE_MERGED=1", "-DWITH_MQTT_BRIDGE=1", "-DWITH_ESPNOW_BRIDGE=1", "-DWITH_BRIDGE=1",
"-DENV_INCLUDE_GPS=1", "-DLIGHTWEIGHT_WIFI_OTA=1", "-DMESH_ENABLE_FLOOD_RULE_ENGINE=1"]
def preprocess(source, extra=()):
return subprocess.run([compiler, "-E", "-P", "-x", "c++", *flags, *extra, "-"],
input=source, capture_output=True, text=True, check=True).stdout
processed_header = preprocess(re.sub(r'^\s*#include[^\n]*', '', header, flags=re.M))
lifecycle = "\n".join(extract_braced(processed_header, signature) for signature in (
"AbstractBridge* activeBridge() {", "const AbstractBridge* activeBridge() const {",
"RS232Bridge* createRS232Bridge()", "bool beginRS232Bridge()", "bool endRS232Bridge()",
"bool isBridgeRunning() const override", "bool rs232BridgeEnabled() const", "bool isRs232BridgeRunning()",
"bool setRs232BridgeState(bool enable)", "bool restartRs232Bridge()",
"bool startSharedEspNowBridgeIfReady()", "bool isEspNowBridgeRunning()",
"bool setEspNowBridgeState(bool enable)", "bool restartEspNowBridge()",
"bool isMqttBridgeRunning()", "bool requestMqttBridgeStop()", "bool isMqttBridgeStopping()",
"bool setMqttBridgeState(bool enable)",
"bool restartMqttBridge()", "bool setBridgeState(bool enable)", "bool restartBridge()",
))
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)
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_MQTT_BRIDGE)\n // MQTT owns TLS",
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 = preprocess("\n".join(extract_braced(cli, signature) for signature in (
"void CommonCLI::handleSetCmd(", "void CommonCLI::handleGetCmd(")))
sets = "\nelse ".join(extract_braced(commands, signature) for signature in (
'if (strncmp(config, "mqtt.enabled ", 13) == 0)',
'if (strncmp(config, "rs232.enabled ", 14) == 0)',
'if (memcmp(config, "espnow.enabled ", 15) == 0)',
'if (memcmp(config, "bridge.enabled ", 15) == 0)',
'if (memcmp(config, "bridge.baud ", 12) == 0)',
'if (memcmp(config, "bridge.uart ", 12) == 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(commands, signature) for signature in (
'if (configKeyEquals(config, "bridge.type"))',
'if (configKeyEquals(config, "rs232.enabled"))',
'if (configKeyEquals(config, "rs232.running"))',
'if (configKeyEquals(config, "bridge.enabled"))',
'if (configKeyEquals(config, "bridge.running"))',
'if (configKeyEquals(config, "espnow.enabled"))',
'if (configKeyEquals(config, "espnow.running"))',
'if (configKeyEquals(config, "bridge.baud"))',
'if (configKeyEquals(config, "bridge.uart"))',
))
gets += "\nelse " + "\nelse ".join(extract_braced(observer, signature) for signature in (
'if (strcmp(config, "mqtt.enabled") == 0)', 'if (strcmp(config, "mqtt.running") == 0)',
'if (strcmp(config, "mqtt.stopping") == 0)'))
ota_start = cli.index(' } else if (memcmp(command, "start ota", 9)')
ota_end = cli.index(' } else if (memcmp(command, "clock", 5)', ota_start)
code = (ROOT / "test/fixtures/rs232_mqtt_runtime.cpp").read_text(encoding="ascii")
for key, value in {
"LIFECYCLE": lifecycle, "DEFAULTS": implementation[defaults_start:defaults_end],
"FILTER": extract_braced(header, "void configureBridgeFilter(AbstractBridge* active_bridge)"),
"BOOT": implementation[boot_start:boot_end], "ROUTE_RX": routing["logRx"],
"ROUTE_TX": routing["logTx"], "SERVICE": implementation[service_start:service_end],
"PENDING": implementation[pending_start:pending_end], "SET_BRANCHES": sets,
"GET_BRANCHES": gets, "OTA": cli[ota_start:ota_end],
"PARSERS": "\n".join(extract_braced(cli, signature) for signature in (
"static bool parseOnOffStrict(", "static bool configKeyEquals(")),
}.items():
code = code.replace("@" + key + "@", value)
with tempfile.TemporaryDirectory(prefix="rs232-mqtt-runtime-") as temporary:
work = Path(temporary)
source, binary = work / "main.cpp", work / "test"
source.write_text(code, 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("Three transport runtime checks passed", result.stdout)
def test_uart_and_mqtt_without_espnow_preserve_primary_mqtt_alias(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")
callback_header = (ROOT / "src/helpers/CommonCLI.h").read_text(encoding="ascii")
header = (ROOT / "examples/simple_repeater/MyMesh.h").read_text(encoding="ascii")
implementation = (ROOT / "examples/simple_repeater/MyMesh.cpp").read_text(encoding="ascii")
flags = ["-DWITH_RS232_BRIDGE=Serial2", "-DWITH_RS232_BRIDGE_UART=2",
"-DWITH_RS232_BRIDGE_RX=5", "-DWITH_RS232_BRIDGE_TX=6", "-DRS232_BRIDGE_MERGED=1",
"-DWITH_MQTT_BRIDGE=1", "-DWITH_BRIDGE=1", "-DENV_INCLUDE_GPS=1",
"-DLIGHTWEIGHT_WIFI_OTA=1", "-DMESH_ENABLE_FLOOD_RULE_ENGINE=1"]
def preprocess(source):
return subprocess.run([compiler, "-E", "-P", "-x", "c++", *flags, "-"],
input=source, capture_output=True, text=True, check=True).stdout
processed_header = preprocess(re.sub(r'^\s*#include[^\n]*', '', header, flags=re.M))
lifecycle = "\n".join(extract_braced(processed_header, signature) for signature in (
"AbstractBridge* activeBridge() {", "const AbstractBridge* activeBridge() const {",
"RS232Bridge* createRS232Bridge()", "bool beginRS232Bridge()", "bool endRS232Bridge()",
"bool rs232BridgeEnabled() const", "bool isBridgeRunning() const override",
"bool isRs232BridgeRunning()", "bool setRs232BridgeState(bool enable)",
"bool restartRs232Bridge()", "bool isMqttBridgeRunning()", "bool requestMqttBridgeStop()",
"bool isMqttBridgeStopping()", "bool setBridgeState(bool enable)",
"bool restartBridge()"))
code = (ROOT / "test/fixtures/rs232_mqtt_runtime.cpp").read_text(encoding="ascii")
code = code[:code.index("int main() {")]
for signature in ("virtual bool isEspNowBridgeRunning() = 0;",
"virtual bool setEspNowBridgeState(bool) = 0;",
"virtual bool restartEspNowBridge() = 0;"):
code = code.replace(signature, "")
for short, full in (("virtual bool setMqttBridgeState(bool) = 0;", "virtual bool setMqttBridgeState(bool enable)"),
("virtual bool restartMqttBridge() = 0;", "virtual bool restartMqttBridge()")):
code = code.replace(short, extract_braced(callback_header, full))
commands = preprocess("\n".join(extract_braced(cli, signature) for signature in (
"void CommonCLI::handleSetCmd(", "void CommonCLI::handleGetCmd(")))
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)
service_start = implementation.index("#if defined(WITH_MQTT_BRIDGE)\n // MQTT owns TLS", 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)
route = {}
for name in ("logRx", "logTx"):
start = implementation.index("#ifdef WITH_MQTT_BRIDGE\n", implementation.index("void MyMesh::" + name + "("))
route[name] = implementation[start:implementation.index(" if (_logging)", start)]
ota_start = cli.index(' } else if (memcmp(command, "start ota", 9)')
defaults_start = implementation.index("// bridge defaults")
substitutions = {
"LIFECYCLE": lifecycle, "BOOT": implementation[boot_start:boot_end],
"DEFAULTS": implementation[defaults_start:implementation.index("// GPS defaults", defaults_start)],
"FILTER": extract_braced(header, "void configureBridgeFilter(AbstractBridge* active_bridge)"),
"ROUTE_RX": route["logRx"], "ROUTE_TX": route["logTx"],
"SERVICE": implementation[service_start:service_end], "PENDING": implementation[pending_start:pending_end],
"SET_BRANCHES": "\nelse ".join(extract_braced(commands, signature) for signature in (
'if (strncmp(config, "mqtt.enabled ", 13) == 0)', 'if (strncmp(config, "rs232.enabled ", 14) == 0)',
'if (memcmp(config, "bridge.enabled ", 15) == 0)', 'if (memcmp(config, "bridge.baud ", 12) == 0)',
'if (memcmp(config, "bridge.uart ", 12) == 0)')),
"GET_BRANCHES": extract_braced(commands, 'if (configKeyEquals(config, "bridge.type"))'),
"PARSERS": "\n".join(extract_braced(cli, signature) for signature in (
"static bool parseOnOffStrict(", "static bool configKeyEquals(")),
"OTA": cli[ota_start:cli.index(' } else if (memcmp(command, "clock", 5)', ota_start)],
}
for key, value in substitutions.items():
code = code.replace("@" + key + "@", value)
code += r'''int main() {
assert(millisHasNowPassed(millis()));
MyMesh mesh; mesh.defaults(); mesh.boot(); CommonCLI cli(mesh);
expect_get(cli, "bridge.type", "> mqtt+rs232");
assert(mesh.isMqttBridgeRunning() && !mesh.isRs232BridgeRunning());
char reply[256] = {};
cli.set("rs232.enabled on", reply); assert(strcmp(reply, "OK") == 0);
assert(mesh.isRs232BridgeRunning() && mesh.isMqttBridgeRunning());
const unsigned starts = mesh.mqtt_bridge->begins;
cli.set("bridge.baud 57600", reply); assert(strcmp(reply, "OK") == 0);
assert(mesh.isRs232BridgeRunning() && mesh.mqtt_bridge->begins == starts);
cli.set("bridge.enabled off", reply); assert(strcmp(reply, "OK") == 0);
assert(!mesh._prefs.bridge_enabled && mesh._prefs.rs232_bridge_enabled);
assert(!mesh.isMqttBridgeRunning() && mesh.isRs232BridgeRunning());
assert(mesh.isBridgeRunning() && mesh.bridgesPreventSleep());
cli.set("bridge.enabled on", reply); assert(strcmp(reply, "OK") == 0);
assert(mesh.isMqttBridgeRunning() && mesh.isRs232BridgeRunning());
mesh::Packet packet;
mesh.routeRx(&packet); mesh.routeTx(&packet); mesh.serviceBridges();
assert(mesh.bridge->sent == 1 && mesh.bridge->loops == 1);
assert(mesh.mqtt_bridge->received == 1 && mesh.mqtt_bridge->sent == 1);
assert(mesh.filter_checks == 3);
const unsigned uart_starts = Serial2.begins;
cli.run("start ota ap", reply);
assert(mesh._cli.board.ota && !mesh.isMqttBridgeRunning() && mesh.isRs232BridgeRunning());
cli.run("stop ota", reply);
assert(!mesh._cli.board.ota && mesh.isMqttBridgeRunning() && Serial2.begins == uart_starts);
cli.set("rs232.enabled off", reply); cli.set("mqtt.enabled off", reply);
assert(!mesh.bridge && mesh.isMqttBridgeStopping() && mesh.bridgesPreventSleep());
mesh.serviceBridges();
assert(!mesh.bridge && !mesh.isMqttBridgeRunning() && !mesh.bridgesPreventSleep());
cli.set("rs232.enabled on", reply); mesh.mqtt_bridge->fail_starts = 1;
cli.set("mqtt.enabled on", reply);
assert(strstr(reply, "setting saved") && !mesh.isMqttBridgeRunning());
assert(mesh.isRs232BridgeRunning() && mesh.bridgesPreventSleep());
cli.set("mqtt.enabled on", reply);
assert(strcmp(reply, "OK") == 0 && mesh.isMqttBridgeRunning());
}'''
with tempfile.TemporaryDirectory(prefix="rs232-mqtt-only-") as directory:
work = Path(directory)
source, binary = work / "main.cpp", work / "test"
source.write_text(code, encoding="ascii")
built = 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(built.returncode, 0, built.stdout + built.stderr)
checked = subprocess.run([str(binary)], capture_output=True, text=True, timeout=10)
self.assertEqual(checked.returncode, 0, checked.stdout + checked.stderr)
if __name__ == "__main__":
unittest.main()