Files
MeshCore/test/test_esp32_native_ble_stop.py
T

206 lines
8.7 KiB
Python

#!/usr/bin/env python3
"""Execute the native Arduino NimBLE stop path, including failed host init.
The production teardown methods are compiled unchanged. The doubles model the
native wrapper's host-task lifecycle and ignored controller teardown failures.
"""
from pathlib import Path
import os
import subprocess
import tempfile
import unittest
ROOT = Path(__file__).resolve().parents[1]
def function(source, signature):
start = source.index(signature)
opening = source.index("{", start)
depth = 0
for end in range(opening, len(source)):
depth += (source[end] == "{") - (source[end] == "}")
if depth == 0:
return source[start:end + 1]
raise AssertionError(signature)
HARNESS = r'''
#include <atomic>
#include <cassert>
#include <cstdio>
#include <initializer_list>
#include <vector>
using esp_err_t = int;
constexpr int ESP_OK = 0;
enum esp_bt_controller_status_t { ESP_BT_CONTROLLER_STATUS_IDLE,
ESP_BT_CONTROLLER_STATUS_INITED, ESP_BT_CONTROLLER_STATUS_ENABLED };
static auto controller = ESP_BT_CONTROLLER_STATUS_IDLE;
static bool initialized = false, host_task_created = false, host_running = false;
static bool fail_host_stop = false, fail_controller_stop = false;
static bool fail_controller_deinit = false;
static unsigned native_stops = 0, wrapper_deinits = 0, objects = 0;
static unsigned controller_stops = 0, controller_deinits = 0;
int nimble_port_stop() {
// A failed BLEDevice::init can precede host task/lock creation. Calling the
// real SDK stop in that state is unsafe; the production gate must skip it.
assert(host_task_created && initialized);
++native_stops;
if (fail_host_stop) return 1;
if (!host_running) return 8; // BLE_HS_EALREADY on the wrapper's second call.
host_running = false;
return 0;
}
esp_bt_controller_status_t esp_bt_controller_get_status() { return controller; }
int esp_bt_controller_disable() {
++controller_stops;
if (fail_controller_stop) return 1;
controller = ESP_BT_CONTROLLER_STATUS_INITED;
return ESP_OK;
}
int esp_bt_controller_deinit() {
++controller_deinits;
if (fail_controller_deinit) return 1;
controller = ESP_BT_CONTROLLER_STATUS_IDLE;
return ESP_OK;
}
struct GattObject {
GattObject() { ++objects; }
~GattObject() { assert(!host_running); --objects; }
};
using BLEServer = GattObject;
using BLEService = GattObject;
using BLECharacteristic = GattObject;
using BLE2902 = GattObject;
static BLEServer* library_server = nullptr;
static std::vector<GattObject*> library_children;
struct BLEDevice {
static bool getInitialized() { return initialized; }
static BLEServer* getServer() { return library_server; }
static void deinit(bool release) {
assert(!release); // No irreversible controller-memory release.
if (!initialized) return;
++wrapper_deinits;
// The current native wrapper deletes the server before stopping its port.
// The adapter must have stopped callback dispatch before entering here.
for (auto* child : library_children) delete child;
library_children.clear();
delete library_server; library_server = nullptr;
(void)nimble_port_stop();
initialized = host_task_created = false;
// Model its ignored controller teardown error. The adapter must verify
// the physical state independently after this wrapper reports stopped.
if (esp_bt_controller_disable() == ESP_OK) (void)esp_bt_controller_deinit();
}
};
@OWNERSHIP@
@CONTROLLER_STOP@
class SerialBLEInterface {
public:
std::atomic<bool> _callbacksEnabled{true};
bool _nativeHostStopped = false;
bool _advertisingConfigured = true;
BLEServer* pServer = nullptr;
BLEService* pService = nullptr;
BLECharacteristic* pTxCharacteristic = nullptr;
BLECharacteristic* pRxCharacteristic = nullptr;
BLE2902* pTxDescriptor = nullptr;
bool stopBluetoothStack();
void startHost() {
assert(objects == 0);
initialized = host_task_created = host_running = true;
controller = ESP_BT_CONTROLLER_STATUS_ENABLED;
pServer = library_server = new BLEServer;
pService = new BLEService;
pTxCharacteristic = new BLECharacteristic;
pRxCharacteristic = new BLECharacteristic;
pTxDescriptor = new BLE2902;
library_children = {pService};
}
};
@STACK_STOP@
static void reset() {
assert(objects == 0 && library_server == nullptr);
initialized = host_task_created = host_running = false;
fail_host_stop = fail_controller_stop = fail_controller_deinit = false;
native_stops = wrapper_deinits = controller_stops = controller_deinits = 0;
controller = ESP_BT_CONTROLLER_STATUS_IDLE;
}
int main() {
// Initialization failed before creating a native host task. The controller
// may nevertheless need cleanup, without touching uninitialized host locks.
for (auto partial : {ESP_BT_CONTROLLER_STATUS_IDLE,
ESP_BT_CONTROLLER_STATUS_INITED,
ESP_BT_CONTROLLER_STATUS_ENABLED}) {
reset(); SerialBLEInterface adapter; controller = partial;
assert(adapter.stopBluetoothStack());
assert(native_stops == 0 && wrapper_deinits == 0);
assert(controller == ESP_BT_CONTROLLER_STATUS_IDLE);
assert(!adapter._callbacksEnabled.load() && objects == 0);
}
reset(); SerialBLEInterface normal; normal.startHost();
assert(normal.stopBluetoothStack());
assert(native_stops == 2 && wrapper_deinits == 1 && objects == 0);
assert(!initialized && !host_task_created && !host_running);
assert(controller == ESP_BT_CONTROLLER_STATUS_IDLE && normal.pServer == nullptr);
reset(); SerialBLEInterface failed_host; failed_host.startHost();
fail_host_stop = true;
assert(!failed_host.stopBluetoothStack());
assert(objects == 5 && initialized && host_running && wrapper_deinits == 0);
fail_host_stop = false;
assert(failed_host.stopBluetoothStack()); assert(objects == 0);
for (bool deinit_failure : {false, true}) {
reset(); SerialBLEInterface partial; partial.startHost();
fail_controller_stop = !deinit_failure;
fail_controller_deinit = deinit_failure;
assert(!partial.stopBluetoothStack());
assert(!initialized && !host_task_created && partial._nativeHostStopped);
assert(objects == 0 && controller != ESP_BT_CONTROLLER_STATUS_IDLE);
const unsigned stopped = native_stops;
fail_controller_stop = fail_controller_deinit = false;
assert(partial.stopBluetoothStack());
assert(native_stops == stopped); // No native host task remains to stop.
assert(controller == ESP_BT_CONTROLLER_STATUS_IDLE && objects == 0);
}
puts("native NimBLE failed-init and teardown cases passed");
}
'''
class NativeNimbleStopTests(unittest.TestCase):
def test_production_stop_handles_absent_and_started_host_tasks(self):
source = (ROOT / "src/helpers/esp32/SerialBLEInterface.cpp").read_text()
ownership_start = source.index("template <typename Device,")
ownership = source[ownership_start:source.index("\n#endif", ownership_start)]
harness = HARNESS.replace("@OWNERSHIP@", ownership).replace(
"@CONTROLLER_STOP@", function(source, "static bool stopBluetoothController()"))
harness = harness.replace("@STACK_STOP@", function(
source, "bool SerialBLEInterface::stopBluetoothStack()"))
with tempfile.TemporaryDirectory(prefix="mesh-native-nimble-stop-") as directory:
cpp, exe = Path(directory) / "stop.cpp", Path(directory) / "stop"
cpp.write_text(harness)
built = subprocess.run([
os.environ.get("CXX", "c++"), "-std=c++17", "-Wall", "-Wextra", "-Werror",
"-fsanitize=address,undefined", "-DCONFIG_NIMBLE_ENABLED=1", "-DMESH_BLE_USES_NIMBLE=1",
str(cpp), "-o", str(exe)], text=True, capture_output=True, timeout=30)
self.assertEqual(built.returncode, 0, built.stderr)
result = subprocess.run([str(exe)], text=True, capture_output=True, timeout=10)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertIn("failed-init and teardown cases passed", result.stdout)
# Prove the failed-init fixture detects the original unsafe stop.
broken = harness.replace("!_nativeHostStopped && BLEDevice::getInitialized()",
"!_nativeHostStopped")
cpp.write_text(broken)
built = subprocess.run([
os.environ.get("CXX", "c++"), "-std=c++17", "-DCONFIG_NIMBLE_ENABLED=1", "-DMESH_BLE_USES_NIMBLE=1",
str(cpp), "-o", str(exe)], text=True, capture_output=True, timeout=30)
self.assertEqual(built.returncode, 0, built.stderr)
result = subprocess.run([str(exe)], text=True, capture_output=True, timeout=10)
self.assertNotEqual(result.returncode, 0)
self.assertIn("host_task_created && initialized", result.stderr)
if __name__ == "__main__":
unittest.main()