Files
MeshCore/test/test_nrf52_ble_tx_session.py
T

124 lines
6.0 KiB
Python

#!/usr/bin/env python3
"""Run actual BLE TX methods across callback resets and connection changes."""
from pathlib import Path
import os
import shutil
import subprocess
import tempfile
import unittest
from cpp_source import body
ROOT = Path(__file__).resolve().parents[1]
START_GATE = " if (!_companionDataSeen.load(std::memory_order_acquire)) return 0;\n"
LEGACY_CLEAR = r'''
void SerialBLEInterface::clearBuffers() {
send_queue_len = 0;
_last_retry_attempt = 0;
_tx_stall_watchdog.reset();
_tx_disconnect_recovery.complete();
recv_queue.reset([this]() { bleuart.flush(); });
}
'''
LEGACY_ENQUEUE = r'''
size_t SerialBLEInterface::writeFrame(const uint8_t src[], size_t len) {
if (len > MAX_FRAME_SIZE) return 0;
if (isConnected() && len > 0) {
if (!mesh::enqueueCompanionFrame(send_queue, send_queue_len, FRAME_QUEUE_SIZE, src, len)) return 0;
return len;
}
return 0;
}
'''
LEGACY_WRITE = r'''
size_t SerialBLEInterface::writeBleUartFrame(const Frame& frame) {
BLEConnection* conn = Bluefruit.Connection(_conn_handle);
if (!conn || !conn->connected() || !bleuart.notifyEnabled(_conn_handle)) return 0;
const uint16_t mtu = conn->getMtu();
if (mtu <= 3) return 0;
return mesh::writeBleFrameInChunks(frame.buf, frame.len, mtu - 3,
[this](const uint8_t* data, size_t len) { return bleuart.write(_conn_handle, data, len); });
}
'''
class Nrf52BleTxSessionTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
compiler = os.environ.get("CXX") or shutil.which("g++") or shutil.which("clang++")
if not compiler:
raise RuntimeError("BLE TX session regression requires a host C++17 compiler")
source = (ROOT / "src/helpers/nrf52/SerialBLEInterface.cpp").read_text()
signatures = (
"void SerialBLEInterface::clearBuffers()", "void SerialBLEInterface::resetTxState()",
"bool SerialBLEInterface::servicePendingTxReset()", "void SerialBLEInterface::shiftSendQueueLeft()",
"size_t SerialBLEInterface::writeBleUartFrame(", "bool SerialBLEInterface::hasRequiredReplyCapacity() const",
"size_t SerialBLEInterface::writeFrame(", "size_t SerialBLEInterface::checkRecvFrame(",
"void SerialBLEInterface::onBleUartRX(",
"void SerialBLEInterface::recoverStalledTx(", "void SerialBLEInterface::serviceTxRecovery(",
"bool SerialBLEInterface::isConnected() const",
"bool SerialBLEInterface::isWriteBusy() const", "bool SerialBLEInterface::hasPendingIO() const")
methods = "\n".join(body(source, signature) for signature in signatures)
header = (ROOT / "src/helpers/nrf52/SerialBLEInterface.h").read_text()
methods += "\n" + body(header, "void setMotaStreamActive(bool active)").replace(
"void setMotaStreamActive(", "void SerialBLEInterface::setMotaStreamActive(", 1)
if methods.count(START_GATE) != 1:
raise AssertionError("Expected exactly one production application-start gate")
variants = {"production": methods,
"legacy_enqueue": methods.replace(body(source, signatures[0]), LEGACY_CLEAR).replace(
body(source, "size_t SerialBLEInterface::writeFrame("), LEGACY_ENQUEUE),
"legacy_write": methods.replace(body(source, "size_t SerialBLEInterface::writeBleUartFrame("), LEGACY_WRITE),
"legacy_start": methods.replace(START_GATE, "", 1)}
fixture = (ROOT / "test/fixtures/nrf52_ble_tx_session.cpp").read_text(encoding="ascii")
cls.work = tempfile.TemporaryDirectory(prefix="meshcore-ble-tx-session-")
cls.binaries = {}
for name, methods in variants.items():
directory = Path(cls.work.name)
path, binary = directory / (name + ".cpp"), directory / name
path.write_text(fixture.replace("@METHODS@", methods), encoding="ascii")
result = subprocess.run([
compiler, "-std=c++17", "-O2", "-g", "-Wall", "-Wextra", "-Werror", "-Wno-unused-parameter",
"-fsanitize=address,undefined", "-fno-sanitize-recover=all", "-fno-pie", "-no-pie",
"-I" + str(ROOT / "src"), str(path), "-o", str(binary)],
capture_output=True, text=True, timeout=30)
if result.returncode:
cls.work.cleanup()
raise AssertionError(result.stdout + result.stderr)
cls.binaries[name] = binary
@classmethod
def tearDownClass(cls):
cls.work.cleanup()
def run_case(self, variant, scenario):
return subprocess.run([str(self.binaries[variant]), scenario], capture_output=True,
text=True, timeout=10)
def test_production_tx_state_is_main_owned_and_fragments_stay_in_session(self):
result = self.run_case("production", "all")
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
def test_previous_reset_and_chunk_paths_reproduce_the_failures(self):
for variant, scenario, assertion in (
("legacy_enqueue", "enqueue", "writeFrame(interrupted"),
("legacy_write", "connection", "chunks.size() == 1")):
with self.subTest(variant=variant):
result = self.run_case(variant, scenario)
self.assertNotEqual(result.returncode, 0)
self.assertIn(assertion, result.stderr)
def test_early_bonded_notifications_wait_for_uart_or_mota_application_start(self):
result = self.run_case("production", "application-start")
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
def test_previous_start_path_reproduces_partial_sixty_byte_notification(self):
result = self.run_case("legacy_start", "prestart-partial")
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
result = self.run_case("legacy_start", "application-start")
self.assertNotEqual(result.returncode, 0)
self.assertIn("bleuart.chunks.empty()", result.stderr)
if __name__ == "__main__":
unittest.main()