Files
MeshCore/test/test_companion_delayed_reply_delivery.py
T

150 lines
8.6 KiB
Python

#!/usr/bin/env python3
"""Run actual delayed reply producers and bounded UART/TCP admission."""
from pathlib import Path
import os
import re
import shutil
import subprocess
import sys
import tempfile
import unittest
from test_replay_reset_integration import extract_braced
ROOT = Path(__file__).resolve().parents[1]
FIXTURE = ROOT / "test/fixtures/companion_delayed_reply_delivery/test.cpp"
SANITIZERS = (["-fsanitize=address,undefined", "-fno-sanitize-recover=all",
"-fno-pie", "-no-pie"] if sys.platform.startswith("linux") else [])
def production_methods(source, base, packet, dispatcher):
methods = "\n".join(extract_braced(source, signature) for signature in (
"void MyMesh::writeErrFrame(", "size_t MyMesh::writePendingSerialFrame(",
"void MyMesh::onContactResponse(", "bool MyMesh::onContactPathRecv(",
"void MyMesh::onTraceRecv(", "void MyMesh::clearPendingReqs(",
"bool MyMesh::hasPendingReqs(", "bool MyMesh::beginPendingRequest(",
"bool MyMesh::allowRequestTag(", "bool MyMesh::allocateRequestTag(",
"void MyMesh::armPendingRequest(",
"void MyMesh::finishPendingRequest(",
"void MyMesh::abandonPendingRequest(", "void MyMesh::servicePendingSerialReply(",
"void MyMesh::servicePendingSerialReply(uint32_t now)",
"void MyMesh::clearBinaryTraceReply(", "void MyMesh::serviceBinaryTraceReply(",
"void MyMesh::serviceBinaryTraceReply(uint32_t now)",
"void MyMesh::cancelSerialOperationsForRoute(",
"bool MyMesh::hasFiniteDelayedReplyForRoute(",
))
terminal = "\n".join(extract_braced(source, signature) for signature in (
"void MyMesh::clearTerminalLogin(", "void MyMesh::serviceTerminalLogin(",
"void MyMesh::clearTerminalLogin(uint32_t now)",
"void MyMesh::sendTerminalLogin(", "void MyMesh::printTerminalSendStatus(",
"void MyMesh::serviceTerminalLogin(uint32_t now)",
"void MyMesh::clearTerminalTrace(", "void MyMesh::serviceTerminalTrace(",
"void MyMesh::clearTerminalTrace(uint32_t now)",
"void MyMesh::serviceTerminalTrace(uint32_t now)",
"void MyMesh::sendTerminalTraceRoute(",
))
methods += "\n#if COMPANION_FEATURE_TEXT_TERMINAL\n" + terminal + "\n#endif\n"
methods += "\n" + "\n".join(extract_braced(base, signature) for signature in (
"bool BaseChatMesh::onContactPathRecv(", "int BaseChatMesh::sendLogin(",
"bool BaseChatMesh::allocateRequestTag(",
"int BaseChatMesh::sendAnonReq(",
"int BaseChatMesh::sendRequest(const ContactInfo& recipient, const uint8_t*",
"int BaseChatMesh::sendRequest(const ContactInfo& recipient, uint8_t",
))
methods += "\n" + "\n".join(
extract_braced(dispatcher, signature).replace("Dispatcher::", "MyMesh::")
for signature in ("bool Dispatcher::millisHasNowPassed(",
"unsigned long Dispatcher::futureMillis(")
)
methods += "\nnamespace mesh {\n" + "\n".join(extract_braced(packet, signature)
for signature in ("bool Packet::isValidPathLen(", "size_t Packet::writePath(")) + "\n}\n"
# Execute real request guards and command branches, excluding unrelated
# firmware features. Packet allocation/transmission are counted boundaries.
handler = extract_braced(source, "void MyMesh::handleCmdFrame(")
prefix = handler[handler.index("{") + 1:handler.index(" if (cmd_frame[0] == CMD_DEVICE_QUERY")]
branches = []
for command in ("LOGIN", "ANON_REQ", "STATUS_REQ", "PATH_DISCOVERY_REQ",
"TELEMETRY_REQ", "BINARY_REQ", "TRACE_PATH"):
block = extract_braced(source, "} else if (cmd_frame[0] == CMD_SEND_" + command)
branches.append(block.replace("} else if", "if", 1) + "\n")
methods += "\nvoid MyMesh::handleRequestFrame(size_t len) {\n" + prefix + "\n".join(branches) + "\n}\n"
return methods
def production_delayed_reply_inputs(work):
source = (ROOT / "examples/companion_radio/MyMesh.cpp").read_text()
base = (ROOT / "src/helpers/BaseChatMesh.cpp").read_text()
packet = (ROOT / "src/Packet.cpp").read_text()
dispatcher = (ROOT / "src/Dispatcher.cpp").read_text()
native = (ROOT / "test/test_serial_mode_switch/test_serial_mode_switch.cpp").read_text()
core = (ROOT / "src/MeshCore.h").read_text()
constants = "\n".join(re.findall(
r"^#define (?:PUSH_CODE_\w+|RESP_CODE_\w+|CMD_SEND_\w+|ERR_CODE_\w+|REQ_TYPE_GET_TELEMETRY_DATA)\s+.+$",
source, re.MULTILINE))
wanted = ("PUB_KEY_SIZE", "MAX_PACKET_PAYLOAD", "MAX_PATH_SIZE", "OUT_PATH_UNKNOWN",
"MSG_SEND_FAILED", "MSG_SEND_SENT_DIRECT", "MSG_SEND_SENT_FLOOD",
"PAYLOAD_TYPE_RESPONSE", "PAYLOAD_TYPE_REQ", "PAYLOAD_TYPE_ANON_REQ",
"PAYLOAD_TYPE_ACK", "REQ_TYPE_GET_STATUS", "REQ_TYPE_GET_TELEMETRY_DATA",
"TELEM_PERM_BASE", "ADV_TYPE_NONE", "ADV_TYPE_ROOM", "RESP_SERVER_LOGIN_OK")
for relative in ("examples/companion_radio/MyMesh.h", "src/MeshCore.h", "src/Packet.h", "src/helpers/BaseChatMesh.h",
"src/helpers/ContactInfo.h", "src/helpers/AdvertDataHelpers.h",
"src/helpers/SensorManager.h"):
text = (ROOT / relative).read_text()
constants += "\n" + "\n".join(re.findall(
r"^#define (?:" + "|".join(wanted) + r")\s+.+$", text, re.MULTILINE))
constants += "\n#define EXPECTED_ACK_TABLE_SIZE 2\n"
clock = "\n".join(extract_braced(core, signature) for signature in (
"uint32_t getCurrentTimeUnique()", "void resetUniqueTime(uint32_t time)"))
# printf is an Arduino Print operation absent from the lean shared
# host mock. Add only this I/O boundary, leaving transport logic real.
stream = (ROOT / "test/mocks/Stream.h").read_text()
stream = "#include <cstdarg>\n#include <cstdio>\n" + stream.replace(
"public:", "public:\n size_t printf(const char* format, ...) {\n"
" char text[256]; va_list args; va_start(args, format);\n"
" int n=vsnprintf(text,sizeof(text),format,args); va_end(args);\n"
" return n>0 ? write((const uint8_t*)text, (size_t)n<sizeof(text) ? (size_t)n : sizeof(text)-1) : 0;\n"
" }", 1)
(work / "production_constants.inc").write_text(constants, encoding="ascii")
(work / "production_clock.inc").write_text(clock, encoding="ascii")
(work / "production_stream.inc").write_text(
extract_braced(native, "class BufferStream") + ";\n", encoding="ascii")
(work / "production_replies.inc").write_text(
production_methods(source, base, packet, dispatcher), encoding="ascii")
(work / "Stream.h").write_text(stream, encoding="ascii")
(work / "Arduino.h").write_text(
(ROOT / "test/mocks/Arduino.h").read_text(), encoding="ascii")
class CompanionDelayedReplyDeliveryTests(unittest.TestCase):
def test_actual_producers_routes_deadlines_and_required_admission(self):
compiler = os.environ.get("CXX") or shutil.which("g++") or shutil.which("clang++")
if compiler is None:
self.skipTest("a host C++17 compiler is required")
with tempfile.TemporaryDirectory(prefix="meshcore-delayed-replies-") as directory:
work = Path(directory)
production_delayed_reply_inputs(work)
for terminal in (0, 1):
with self.subTest(terminal=terminal):
binary = work / f"delayed-{terminal}.exe"
compiled = subprocess.run([
compiler, "-std=c++17", "-Wall", "-Wextra", "-Werror",
"-Wno-unused-parameter", "-Wno-sign-compare", "-Wno-class-memaccess", *SANITIZERS,
f"-DCOMPANION_FEATURE_TEXT_TERMINAL={terminal}", "-DMESH_ENABLE_ONE_KEY_DM=0",
f"-I{work}", f"-I{ROOT / 'test/mocks'}",
f"-I{ROOT / 'test/fixtures/serial_wifi_sessions/mocks'}", f"-I{ROOT / 'src'}",
str(FIXTURE), str(ROOT / "src/helpers/CompanionDelayedReplies.cpp"),
str(ROOT / "src/helpers/ArduinoSerialInterface.cpp"),
str(ROOT / "src/helpers/wifi/SerialWifiInterface.cpp"), "-o", str(binary),
str(ROOT / "src/helpers/TxtDataHelpers.cpp"),
], capture_output=True, text=True, timeout=60)
self.assertEqual(compiled.returncode, 0, compiled.stdout + compiled.stderr)
checked = subprocess.run([str(binary)], capture_output=True, text=True, timeout=20)
self.assertEqual(checked.returncode, 0, checked.stdout + checked.stderr)
self.assertIn("PASS: delayed production reply delivery", checked.stdout)
if __name__ == "__main__":
unittest.main()