Files
MeshCore/test/test_companion_wifi_session.py

233 lines
11 KiB
Python

"""Exercise WiFi session cancellation against production route ownership code."""
from pathlib import Path
import os
import re
import subprocess
import sys
import tempfile
import unittest
from test_replay_reset_integration import extract_braced
ROOT = Path(__file__).resolve().parents[1]
MAIN = ROOT / "examples/companion_radio/main.cpp"
MESH = ROOT / "examples/companion_radio/MyMesh.cpp"
HARNESS = r'''
#include <cassert>
#include <cstdint>
#include <cstring>
#include <deque>
#include <vector>
#include <helpers/MultiSerialInterface.h>
#include <helpers/CompanionDelayedReplies.h>
struct FakeInterface : BaseSerialInterface {
bool enabled = false;
bool connected = true;
std::deque<std::vector<uint8_t>> input;
void enable() override { enabled = true; }
void disable() override { enabled = false; }
bool isEnabled() const override { return enabled; }
bool isConnected() const override { return connected; }
bool isReadBusy() const override { return false; }
bool isWriteBusy() const override { return false; }
size_t writeFrame(const uint8_t[], size_t len) override { return len; }
size_t checkRecvFrame(uint8_t dest[]) override {
if (input.empty()) return 0;
auto frame = input.front(); input.pop_front();
memcpy(dest, frame.data(), frame.size());
return frame.size();
}
} wifi_interface, usb_interface, bluetooth_interface;
MultiSerialInterface interface_manager;
constexpr int EXPECTED_ACK_TABLE_SIZE = 6;
struct MyMesh {
bool streaming = false;
int stream_cancels = 0, pending_cancels = 0, radio_cancels = 0;
int trace_cancels = 0, signing_cancels = 0, expirations = 0;
mesh::CompanionDelayedReplies _delayed_replies;
BaseSerialInterface* command_radio_reply_route = nullptr;
BaseSerialInterface* sign_data_reply_route = nullptr;
BaseSerialInterface* private_key_backup_route = nullptr;
unsigned long private_key_backup_deadline = 0;
char private_key_backup_nonce[17] = {};
uint8_t private_key_backup_sender[6] = {};
struct Ack { BaseSerialInterface* reply_route; bool radio_retry; };
Ack expected_ack_table[EXPECTED_ACK_TABLE_SIZE] = {};
void cancelSerialResponseStream(BaseSerialInterface* route) {
if (!interface_manager.isReplyRouteFor(route)) return;
++stream_cancels;
if (streaming) { streaming = false; interface_manager.unlockReplyRoute(); }
}
void clearPendingReqs() { ++pending_cancels; _delayed_replies.retireRequest(millis()); }
void cancelPendingRadioParamApply() { ++radio_cancels; command_radio_reply_route = nullptr; }
void clearBinaryTraceReply() { ++trace_cancels; _delayed_replies.retireBinaryTrace(millis()); }
void cancelSigningSession() { ++signing_cancels; sign_data_reply_route = nullptr; }
void expireExpectedAcks() { ++expirations; }
void cancelSerialOperationsForRoute(BaseSerialInterface*);
} the_mesh;
@CANCEL_OPERATIONS@
@CANCEL_SESSION@
int main() {
assert(interface_manager.addInterface(InterfaceType::WiFi, &wifi_interface));
assert(interface_manager.addInterface(InterfaceType::USB, &usb_interface));
assert(interface_manager.addInterface(InterfaceType::Bluetooth, &bluetooth_interface));
FakeInterface* routes[] = {&wifi_interface, &usb_interface, &bluetooth_interface};
for (auto owner : routes) {
interface_manager.enable();
the_mesh = MyMesh();
owner->input.push_back({0x04});
uint8_t command[MAX_FRAME_SIZE] = {};
assert(interface_manager.checkRecvFrame(command) == 1);
interface_manager.lockReplyRoute();
the_mesh.streaming = true;
const uint8_t peer[32] = {1};
assert(the_mesh._delayed_replies.reserveRequest(
mesh::CompanionDelayedReplies::Binary, peer, owner, false, millis()));
the_mesh._delayed_replies.armRequest(17, 1000, false, millis());
the_mesh.command_radio_reply_route = owner;
assert(the_mesh._delayed_replies.reserveBinaryTrace(17, 23, owner, millis()));
the_mesh._delayed_replies.armBinaryTrace(1000, millis());
the_mesh.sign_data_reply_route = owner;
the_mesh.private_key_backup_route = owner;
the_mesh.private_key_backup_deadline = 1234;
memcpy(the_mesh.private_key_backup_nonce, "0123456789abcdef", 17);
memset(the_mesh.private_key_backup_sender, 0x5A, 6);
for (unsigned i = 0; i < EXPECTED_ACK_TABLE_SIZE; ++i) {
the_mesh.expected_ack_table[i] = {routes[i % 3], true};
}
// The transport marks itself disconnected during the synchronous callback.
wifi_interface.connected = false;
cancelCompanionWiFiSession(nullptr);
wifi_interface.connected = true;
const bool cancelled = owner == &wifi_interface;
assert(the_mesh.stream_cancels == int(cancelled));
assert(the_mesh.streaming == !cancelled);
assert(the_mesh.pending_cancels == int(cancelled));
assert(the_mesh.radio_cancels == int(cancelled));
assert(the_mesh.trace_cancels == int(cancelled));
assert(the_mesh.signing_cancels == int(cancelled));
assert(the_mesh._delayed_replies.request.route == (cancelled ? nullptr : owner));
assert(the_mesh._delayed_replies.hasRequest() == !cancelled);
assert(the_mesh.command_radio_reply_route == (cancelled ? nullptr : owner));
assert(the_mesh._delayed_replies.trace.route == (cancelled ? nullptr : owner));
assert(the_mesh._delayed_replies.hasBinaryTrace() == !cancelled);
assert(the_mesh.sign_data_reply_route == (cancelled ? nullptr : owner));
assert(the_mesh.private_key_backup_route == (cancelled ? nullptr : owner));
assert(the_mesh.private_key_backup_deadline == (cancelled ? 0UL : 1234UL));
const char empty_nonce[17] = {};
const uint8_t empty_sender[6] = {};
assert(memcmp(the_mesh.private_key_backup_nonce,
cancelled ? empty_nonce : "0123456789abcdef", 17) == 0);
const uint8_t original_sender[6] = {0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A};
assert(memcmp(the_mesh.private_key_backup_sender,
cancelled ? empty_sender : original_sender, 6) == 0);
assert(!interface_manager.isReplyRouteFor(&wifi_interface));
if (!cancelled) assert(interface_manager.isReplyRouteFor(owner));
for (unsigned i = 0; i < EXPECTED_ACK_TABLE_SIZE; ++i) {
// Detach WiFi's notification, not the radio's accepted message/retry.
assert(the_mesh.expected_ack_table[i].reply_route ==
(i % 3 == 0 ? nullptr : routes[i % 3]));
assert(the_mesh.expected_ack_table[i].radio_retry);
}
assert(the_mesh.expirations == 1);
}
}
'''
class CompanionWiFiSessionTest(unittest.TestCase):
def test_callback_is_registered_before_wifi_interface_can_start(self):
source = MAIN.read_text(encoding="utf-8")
registration = source.index(
"wifi_interface.setSessionChangedCallback(cancelCompanionWiFiSession, nullptr)"
)
setup = source.index("void setup()")
add = source.index("interface_manager.addInterface(InterfaceType::WiFi", setup)
start = source.index("startCompanionWiFi();", add)
self.assertLess(registration, add)
self.assertLess(add, start)
def test_session_reset_cancels_only_wifi_owned_work(self):
self.check_session_reset("WiFi")
def test_session_reset_cancels_only_ethernet_owned_work(self):
self.check_session_reset("Ethernet")
def test_callback_is_registered_before_ethernet_interface_can_start(self):
source = MAIN.read_text(encoding="utf-8")
self.assertLess(
source.index("ethernet_interface.setSessionChangedCallback(cancelCompanionEthernetSession, nullptr)"),
source.index("ethernet_interface.begin();"),
)
def test_ethernet_callback_guards_cover_profiles_without_usb_or_wifi(self):
# Preserve the real main.cpp conditional nesting, not just an extracted
# callback body: Ethernet-only builds must see its definition as well
# as its setup registration. Headers and unrelated code are unnecessary
# for this preprocessor test and would require the firmware toolchain.
markers = []
continuation = False
for line in MAIN.read_text(encoding="utf-8").splitlines(keepends=True):
conditional = re.match(r"^\s*#\s*(?:if|ifdef|ifndef|elif|else|endif)\b", line)
if continuation or conditional:
markers.append(line)
continuation = line.rstrip().endswith("\\")
elif "static void cancelCompanionEthernetSession(" in line:
markers.append("ETHERNET_SESSION_CALLBACK_DECLARED\n")
elif "ethernet_interface.setSessionChangedCallback(" in line:
markers.append("ETHERNET_SESSION_CALLBACK_REGISTERED\n")
source = "".join(markers)
for platform in (("ESP32_PLATFORM", "ESP32"), ("NRF52_PLATFORM",)):
for ethernet in (False, True):
for usb in (False, True):
with self.subTest(platform=platform, ethernet=ethernet, usb=usb):
flags = [f"-D{name}=1" for name in platform]
if ethernet:
flags.append("-DETHERNET_ENABLED=1")
if usb:
flags.append("-DENABLE_USB_INTERFACE=1")
result = subprocess.run(
[os.environ.get("CXX", "g++"), "-E", "-P", "-x", "c++",
*flags, "-"], input=source, text=True, capture_output=True,
)
self.assertEqual(result.returncode, 0, result.stderr)
expected = ("ETHERNET_SESSION_CALLBACK_DECLARED\n"
"ETHERNET_SESSION_CALLBACK_REGISTERED") if ethernet else ""
self.assertEqual(result.stdout.strip(), expected)
def check_session_reset(self, transport):
main = MAIN.read_text(encoding="utf-8")
mesh = MESH.read_text(encoding="utf-8")
callback = extract_braced(main, f"static void cancelCompanion{transport}Session(")
operations = extract_braced(mesh, "void MyMesh::cancelSerialOperationsForRoute(")
harness = HARNESS.replace("WiFi", transport).replace("wifi", transport.lower())
source = harness.replace("@CANCEL_OPERATIONS@", operations).replace(
"@CANCEL_SESSION@", callback
)
with tempfile.TemporaryDirectory() as directory:
executable = Path(directory) / ("wifi_session.exe" if os.name == "nt" else "wifi_session")
result = subprocess.run(
[os.environ.get("CXX", "g++"), "-std=c++17", "-Werror",
*(["-fsanitize=address,undefined", "-fno-sanitize-recover=all",
"-fno-pie", "-no-pie"] if sys.platform.startswith("linux") else []),
f"-I{ROOT / 'test/mocks'}", f"-I{ROOT / 'src'}",
"-x", "c++", "-", str(ROOT / "src/helpers/CompanionDelayedReplies.cpp"),
"-o", str(executable)],
input=source, text=True, capture_output=True,
)
self.assertEqual(result.returncode, 0, result.stderr)
result = subprocess.run([str(executable)], text=True, capture_output=True)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
if __name__ == "__main__":
unittest.main()