Fix USB logging ownership, persistence, and reconnect safety
This commit is contained in:
1 parent
c1a533572a
commit
91d533bfff
32 files changed
+1543
-140
No files matched your search
@@ -2,6 +2,7 @@
|
||||
#include <helpers/ClientACLCLI.h>
|
||||
#include <helpers/CLICommandUtils.h>
|
||||
#include <helpers/LazyPersistence.h>
|
||||
#include <algorithm>
|
||||
#include <cstdarg>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
@@ -220,6 +221,11 @@ static void actual_role_dispatch_handles_radio_and_local() {
|
||||
CHECK(reply[0] == 0);
|
||||
CHECK(mesh::console.output.find("ACL:") == 0);
|
||||
CHECK(mesh::console.output.find(row(admin)) != std::string::npos);
|
||||
std::string local_output = mesh::console.output;
|
||||
local_output.erase(std::remove(local_output.begin(), local_output.end(), '\r'),
|
||||
local_output.end());
|
||||
CHECK(local_output == "ACL:\n" + row(admin) + "\n"
|
||||
+ row(acl.getClientByIdx(1)) + "\n");
|
||||
strcpy(command, "get acl");
|
||||
char paged[] = "get acl 1";
|
||||
mesh::console.output.clear();
|
||||
@@ -227,6 +233,27 @@ static void actual_role_dispatch_handles_radio_and_local() {
|
||||
CHECK(mesh::console.output.empty());
|
||||
CHECK(reply == expected);
|
||||
}
|
||||
|
||||
// Also exercise every role's local full-table listing, not just remote pages.
|
||||
// The real extracted helper must emit every active key, skip deleted entries,
|
||||
// and never include the secret fields stored alongside ACL public identities.
|
||||
ClientACL full;
|
||||
full.load(&fs, self);
|
||||
std::string expected_local = "ACL:\n";
|
||||
for (unsigned i = 0; i < MAX_CLIENTS; ++i) {
|
||||
auto* client = add(full, i, i % 7 == 0 ? 0 : PERM_ACL_ADMIN);
|
||||
if (client->permissions) expected_local += row(client) + "\n";
|
||||
}
|
||||
for (auto handler : {repeaterCommand, roomCommand, sensorCommand}) {
|
||||
char command[] = "get acl", reply[160] = {};
|
||||
mesh::console.output.clear();
|
||||
handler(full, nullptr, 0, command, reply);
|
||||
CHECK(reply[0] == 0);
|
||||
std::string local_output = mesh::console.output;
|
||||
local_output.erase(std::remove(local_output.begin(), local_output.end(), '\r'),
|
||||
local_output.end());
|
||||
CHECK(local_output == expected_local);
|
||||
}
|
||||
}
|
||||
|
||||
static void repeater_delegation_stays_denied() {
|
||||
|
||||
+11
@@ -25,6 +25,17 @@
|
||||
*/
|
||||
|
||||
// Pinned framework d541301: real power handler before the build-local fix.
|
||||
// The actual disconnect path is retained too: pull-up disable alone does NOT
|
||||
// produce a POWER removal edge, so local session recovery needs this event.
|
||||
void dcd_disconnect(uint8_t rhport) {
|
||||
(void) rhport;
|
||||
NRF_USBD->USBPULLUP = 0;
|
||||
|
||||
// Disable Pull-up does not trigger Power USB Removed, in fact it have no
|
||||
// impact on the USB Power status at all -> need to submit unplugged event to the stack.
|
||||
dcd_event_bus_signal(0, DCD_EVENT_UNPLUGGED, false);
|
||||
}
|
||||
|
||||
void tusb_hal_nrf_power_event(uint32_t event);
|
||||
void tusb_hal_nrf_power_event(uint32_t event) {
|
||||
// Value is chosen to be as same as NRFX_POWER_USB_EVT_* in nrfx_power.h
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
#pragma once
|
||||
|
||||
#include <cassert>
|
||||
#include <cstdarg>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
extern uint32_t mock_millis;
|
||||
inline uint32_t millis() { return mock_millis; }
|
||||
inline void delay(uint32_t value) { mock_millis += value; }
|
||||
|
||||
class Stream {
|
||||
public:
|
||||
virtual ~Stream() = default;
|
||||
virtual int available() = 0;
|
||||
virtual int read() = 0;
|
||||
virtual int peek() = 0;
|
||||
virtual void flush() = 0;
|
||||
virtual int availableForWrite() { return 0; }
|
||||
virtual size_t write(uint8_t value) = 0;
|
||||
virtual size_t write(const uint8_t* data, size_t size) = 0;
|
||||
size_t printf(const char* format, ...) {
|
||||
char buffer[256];
|
||||
va_list args;
|
||||
va_start(args, format);
|
||||
const int count = vsnprintf(buffer, sizeof(buffer), format, args);
|
||||
va_end(args);
|
||||
assert(count >= 0 && static_cast<size_t>(count) < sizeof(buffer));
|
||||
return write(reinterpret_cast<const uint8_t*>(buffer), static_cast<size_t>(count));
|
||||
}
|
||||
};
|
||||
|
||||
class RawSerial {
|
||||
public:
|
||||
int availableForWrite() { return 256; }
|
||||
template <typename... Args> void printf(Args...) { assert(false && "raw USB write"); }
|
||||
template <typename... Args> void println(Args...) { assert(false && "raw USB write"); }
|
||||
};
|
||||
extern RawSerial Serial;
|
||||
|
||||
struct MockFicr { uint32_t DEVICEID[2] = {0x123456, 0}; };
|
||||
extern MockFicr mock_ficr;
|
||||
#define NRF_FICR (&mock_ficr)
|
||||
#define NRF_SPIM1 1
|
||||
#define WB_IO2 2
|
||||
#define OUTPUT 1
|
||||
#define HIGH 1
|
||||
inline void pinMode(int, int) {}
|
||||
inline void digitalWrite(int, int) {}
|
||||
inline void vTaskDelete(void*) {}
|
||||
inline void vTaskDelay(uint32_t value) { delay(value); }
|
||||
inline uint32_t pdMS_TO_TICKS(uint32_t value) { return value; }
|
||||
inline void xTaskCreate(void (*)(void*), const char*, int, void*, int, void*) {}
|
||||
@@ -0,0 +1,63 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <SPI.h>
|
||||
|
||||
class IPAddress {
|
||||
public:
|
||||
uint8_t operator[](size_t index) const {
|
||||
static const uint8_t address[4] = {192, 168, 1, 42};
|
||||
return address[index];
|
||||
}
|
||||
};
|
||||
|
||||
extern std::string ethernet_output;
|
||||
extern std::string ethernet_input;
|
||||
extern bool accept_client;
|
||||
class EthernetClient {
|
||||
bool _connected = false;
|
||||
public:
|
||||
explicit EthernetClient(bool connected = false) : _connected(connected) {}
|
||||
explicit operator bool() const { return _connected; }
|
||||
bool connected() const { return _connected; }
|
||||
void stop() { _connected = false; }
|
||||
IPAddress remoteIP() const { return {}; }
|
||||
int available() const { return static_cast<int>(ethernet_input.size()); }
|
||||
int read() {
|
||||
assert(!ethernet_input.empty());
|
||||
const char value = ethernet_input.front();
|
||||
ethernet_input.erase(0, 1);
|
||||
return value;
|
||||
}
|
||||
void print(const char* text) { ethernet_output += text; }
|
||||
void println(const char* text = "") { ethernet_output += std::string(text) + "\r\n"; }
|
||||
};
|
||||
|
||||
class EthernetServer {
|
||||
public:
|
||||
explicit EthernetServer(int) {}
|
||||
void begin() {}
|
||||
EthernetClient accept() {
|
||||
const bool accepted = accept_client;
|
||||
accept_client = false;
|
||||
return EthernetClient(accepted);
|
||||
}
|
||||
};
|
||||
|
||||
enum { EthernetNoHardware = 0, EthernetPresent = 1, LinkOFF = 0, LinkON = 1 };
|
||||
class MockEthernet {
|
||||
public:
|
||||
int failures = 0;
|
||||
int hardware = EthernetPresent;
|
||||
int link = LinkON;
|
||||
void init(SPIClass&, int) {}
|
||||
int begin(uint8_t*, int, int) {
|
||||
if (failures > 0) { --failures; return 0; }
|
||||
return 1;
|
||||
}
|
||||
int hardwareStatus() const { return hardware; }
|
||||
int linkStatus() const { return link; }
|
||||
IPAddress localIP() const { return {}; }
|
||||
void maintain() {}
|
||||
};
|
||||
extern MockEthernet Ethernet;
|
||||
@@ -0,0 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
class SPIClass {
|
||||
public:
|
||||
SPIClass(int, int, int, int) {}
|
||||
void begin() {}
|
||||
};
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
#include <helpers/NonBlockingWriteStream.h>
|
||||
#include <helpers/nrf52/EthernetCLI.h>
|
||||
#include "NetworkLoggingMacro.h"
|
||||
|
||||
#include <iostream>
|
||||
|
||||
uint32_t mock_millis = 0;
|
||||
RawSerial Serial;
|
||||
MockFicr mock_ficr;
|
||||
MockEthernet Ethernet;
|
||||
std::string ethernet_output;
|
||||
std::string ethernet_input;
|
||||
bool accept_client = false;
|
||||
|
||||
class MockUsb : public Stream {
|
||||
public:
|
||||
int capacity = 256;
|
||||
bool refuse_write = false;
|
||||
unsigned int tries = 0;
|
||||
std::string output;
|
||||
int available() override { return 0; }
|
||||
int read() override { return -1; }
|
||||
int peek() override { return -1; }
|
||||
void flush() override { assert(false && "blocking flush"); }
|
||||
int availableForWrite() override { return capacity; }
|
||||
size_t write(uint8_t) override { assert(false && "blocking write"); return 0; }
|
||||
size_t write(const uint8_t*, size_t) override { assert(false && "blocking write"); return 0; }
|
||||
};
|
||||
|
||||
static MockUsb usb;
|
||||
static bool logging_enabled = true;
|
||||
static size_t tryUsbWrite(void*, const uint8_t* data, size_t size) {
|
||||
++usb.tries;
|
||||
if (usb.refuse_write || usb.capacity <= 0) return 0;
|
||||
const size_t written = size < static_cast<size_t>(usb.capacity)
|
||||
? size : static_cast<size_t>(usb.capacity);
|
||||
usb.output.append(reinterpret_cast<const char*>(data), written);
|
||||
return written;
|
||||
}
|
||||
static mesh::SingleAttemptNonBlockingStream logging_port(usb, tryUsbWrite);
|
||||
namespace mesh {
|
||||
bool isUsbLoggingEnabled() { return logging_enabled; }
|
||||
Stream& usbLoggingPort() { return logging_port; }
|
||||
}
|
||||
|
||||
static void reset() {
|
||||
usb.output.clear();
|
||||
usb.capacity = 256;
|
||||
usb.refuse_write = false;
|
||||
usb.tries = 0;
|
||||
logging_enabled = true;
|
||||
ethernet_running = false;
|
||||
ethernet_client = EthernetClient();
|
||||
ethernet_output.clear();
|
||||
ethernet_input.clear();
|
||||
accept_client = false;
|
||||
Ethernet = MockEthernet();
|
||||
mock_millis = 0;
|
||||
}
|
||||
|
||||
static void acceptClientAndReply() {
|
||||
accept_client = true;
|
||||
ethernet_loop_maintain();
|
||||
assert(ethernet_take_session_reset());
|
||||
assert(!ethernet_take_session_reset());
|
||||
ethernet_input = "eth.status\r";
|
||||
char command[80] = {};
|
||||
assert(ethernet_read_line(command, sizeof(command)));
|
||||
assert(strcmp(command, "eth.status") == 0);
|
||||
char reply[80] = {};
|
||||
assert(ethernet_handle_command(command, reply));
|
||||
ethernet_send_reply(reply);
|
||||
assert(ethernet_output == "MeshCore CLI\r\n\r\n -> ETH: 192.168.1.42:23\r\n");
|
||||
}
|
||||
|
||||
int main() {
|
||||
(void)ðernet_start_task;
|
||||
// Runtime-off diagnostics must not attempt USB, while functional Ethernet
|
||||
// commands and the client banner still work.
|
||||
reset();
|
||||
logging_enabled = false;
|
||||
ethernet_task(nullptr);
|
||||
acceptClientAndReply();
|
||||
assert(usb.output.empty() && usb.tries == 0 && mock_millis == 0);
|
||||
|
||||
reset();
|
||||
ethernet_task(nullptr);
|
||||
acceptClientAndReply();
|
||||
assert(usb.output.find("ETH: MAC: 02:92:1F:12:34:56\n") != std::string::npos);
|
||||
assert(usb.output.find("ETH: Listening on TCP port 23\n") != std::string::npos);
|
||||
assert(usb.output.find("ETH: Client connected from 192.168.1.42\n") != std::string::npos);
|
||||
assert(usb.tries == 6 && mock_millis == 0);
|
||||
|
||||
// An open-but-stalled host has no TX space. No raw write or wait is allowed.
|
||||
reset();
|
||||
usb.capacity = 0;
|
||||
ethernet_task(nullptr);
|
||||
acceptClientAndReply();
|
||||
assert(usb.output.empty() && usb.tries == 0 && mock_millis == 0);
|
||||
|
||||
// The host can also vanish after the capacity preflight. The real shared
|
||||
// single-attempt facade drops each diagnostic instead of retrying or waiting.
|
||||
reset();
|
||||
usb.refuse_write = true;
|
||||
ethernet_task(nullptr);
|
||||
acceptClientAndReply();
|
||||
assert(usb.output.empty() && usb.tries == 6 && mock_millis == 0);
|
||||
|
||||
for (int failure : {0, 1, 2}) {
|
||||
reset();
|
||||
Ethernet.failures = 1;
|
||||
if (failure == 0) Ethernet.hardware = EthernetNoHardware;
|
||||
if (failure == 1) Ethernet.link = LinkOFF;
|
||||
ethernet_task(nullptr);
|
||||
const char* expected = failure == 0 ? "Hardware not found, giving up"
|
||||
: failure == 1 ? "Cable not connected, will retry" : "DHCP failed, will retry";
|
||||
assert(usb.output.find(expected) != std::string::npos);
|
||||
assert(mock_millis == (failure == 0 ? 0u : 30000u));
|
||||
}
|
||||
|
||||
reset();
|
||||
int evaluated = 0;
|
||||
logging_enabled = false;
|
||||
NETWORK_DEBUG_PRINTLN("suppressed %d", ++evaluated);
|
||||
assert(evaluated == 0 && usb.tries == 0);
|
||||
logging_enabled = true;
|
||||
usb.capacity = 0;
|
||||
NETWORK_DEBUG_PRINTLN("stalled %d", ++evaluated);
|
||||
assert(evaluated == 0 && usb.tries == 0);
|
||||
usb.capacity = 256;
|
||||
NETWORK_DEBUG_PRINTLN("visible %d", ++evaluated);
|
||||
#if defined(MQTT_DEBUG) && MQTT_DEBUG
|
||||
assert(evaluated == 1 && usb.output == "MQTT: visible 1\n" && usb.tries == 1);
|
||||
usb.refuse_write = true;
|
||||
NETWORK_DEBUG_PRINTLN("disconnected %d", ++evaluated);
|
||||
assert(usb.tries == 2 && usb.output == "MQTT: visible 1\n");
|
||||
#else
|
||||
assert(evaluated == 0 && usb.output.empty() && usb.tries == 0);
|
||||
#endif
|
||||
assert(mock_millis == 0);
|
||||
std::cout << "PASS: gated USB diagnostics\n";
|
||||
}
|
||||
@@ -32,12 +32,23 @@ def role_handler(role, source):
|
||||
# The room source opens its next conditional branch's preprocessor guard
|
||||
# immediately before the closing brace of the local ACL branch.
|
||||
local = re.sub(r"\n#if defined\(WITH_MQTT_NEIGHBORS\)\n\s*}$", "\n}", local)
|
||||
usb_acl_helper = ""
|
||||
if role != "sensor":
|
||||
# Keep local dispatch attached to the actual USB listing helper. These
|
||||
# fixtures exercise legacy/nonnative USB; native-USB short writes are
|
||||
# covered separately by test_esp32_tinyusb_cooperative_output.py.
|
||||
usb_acl_helper = extract_braced(source, "void MyMesh::printAclSerial()")
|
||||
usb_acl_helper = usb_acl_helper.replace(
|
||||
"void MyMesh::printAclSerial()",
|
||||
f"static void {role}PrintAclSerial(ClientACL& acl)", 1)
|
||||
local = local.replace("printAclSerial();", f"{role}PrintAclSerial(acl);")
|
||||
guard = ""
|
||||
if role == "repeater":
|
||||
guard_start = body.rindex("if (sender && !sender->isAdmin())", 0,
|
||||
body.index("mesh::cli::handleACLGet("))
|
||||
guard = extract_braced(body[guard_start:], "if (sender && !sender->isAdmin())")
|
||||
return f"""
|
||||
{usb_acl_helper}
|
||||
static void {role}Command(ClientACL& acl, ClientInfo* sender,
|
||||
uint32_t sender_timestamp, char* command, char* reply) {{
|
||||
const int gpio_client_index = sender == nullptr ? -1 : 0;
|
||||
@@ -95,6 +106,7 @@ namespace mesh { class Utils { public:
|
||||
result = subprocess.run([
|
||||
compiler, "-std=c++17", "-Wall", "-Wextra", "-DESP32=1",
|
||||
"-DESP32_PLATFORM=1", "-DMESH_ENABLE_FLOOD_RULE_ENGINE=1",
|
||||
"-DMESH_ESP32_USB_CONSOLE_COOPERATIVE=0",
|
||||
f"-DMAX_CLIENTS={clients}", f"-I{work}", f"-I{ACL_MOCKS}",
|
||||
f"-I{ROOT / 'src'}", str(FIXTURE / "test_client_acl_cli.cpp"),
|
||||
"-o", str(binary),
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
"""Run the real logging setters with failed saves and failed MQTT transitions."""
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
from test_replay_reset_integration import extract_braced
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
HARNESS = r'''
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <initializer_list>
|
||||
#include "helpers/CLICommandUtils.h"
|
||||
struct Prefs { uint8_t usb_logging_enabled = 0, bridge_enabled = 0; };
|
||||
static bool live_usb = false;
|
||||
static unsigned usb_calls = 0;
|
||||
namespace mesh {
|
||||
void setUsbLoggingEnabled(bool value) { live_usb = value; ++usb_calls; }
|
||||
}
|
||||
struct Callbacks {
|
||||
bool live_wifi = false, apply_ok = true;
|
||||
unsigned calls = 0;
|
||||
bool setMqttBridgeState(bool value) {
|
||||
++calls;
|
||||
if (apply_ok) live_wifi = value;
|
||||
return apply_ok;
|
||||
}
|
||||
};
|
||||
struct CLI {
|
||||
Prefs prefs, persisted;
|
||||
Prefs* _prefs = &prefs;
|
||||
Callbacks callbacks;
|
||||
Callbacks* _callbacks = &callbacks;
|
||||
bool save_ok = true;
|
||||
unsigned saves = 0;
|
||||
bool trySavePrefs() { ++saves; if (save_ok) persisted = prefs; return save_ok; }
|
||||
void set(const char* config, char* reply) { @BRANCHES@ }
|
||||
};
|
||||
static void check(const char* command, bool usb, bool wifi, bool failed) {
|
||||
CLI cli;
|
||||
cli.prefs = {uint8_t(!usb), uint8_t(!wifi)};
|
||||
cli.persisted = cli.prefs;
|
||||
live_usb = !usb;
|
||||
cli.callbacks.live_wifi = !wifi;
|
||||
usb_calls = 0;
|
||||
cli.save_ok = !failed;
|
||||
char reply[160]{};
|
||||
cli.set(command, reply);
|
||||
assert(cli.saves == 1);
|
||||
assert(cli.prefs.usb_logging_enabled == (failed ? !usb : usb));
|
||||
assert(cli.persisted.usb_logging_enabled == (failed ? !usb : usb));
|
||||
assert(live_usb == (failed ? !usb : usb));
|
||||
assert(usb_calls == (failed ? 0u : 1u));
|
||||
assert(strstr(reply, failed ? "unchanged" : "(saved)"));
|
||||
if (strncmp(command, "logging.output", 14) == 0) {
|
||||
assert(cli.prefs.bridge_enabled == (failed ? !wifi : wifi));
|
||||
assert(cli.persisted.bridge_enabled == (failed ? !wifi : wifi));
|
||||
assert(cli.callbacks.live_wifi == (failed ? !wifi : wifi));
|
||||
assert(cli.callbacks.calls == (failed ? 0u : 1u));
|
||||
} else {
|
||||
assert(cli.callbacks.calls == 0);
|
||||
}
|
||||
}
|
||||
int main() {
|
||||
for (bool failed : {false, true}) {
|
||||
check("usb.logging on", true, false, failed);
|
||||
check("usb.logging off", false, false, failed);
|
||||
#if WITH_MQTT_BRIDGE
|
||||
check("logging.output off", false, false, failed);
|
||||
check("logging.output usb", true, false, failed);
|
||||
check("logging.output wifi", false, true, failed);
|
||||
check("logging.output both", true, true, failed);
|
||||
#endif
|
||||
}
|
||||
CLI cli;
|
||||
char reply[160]{};
|
||||
cli.set("usb.logging invalid", reply);
|
||||
assert(cli.saves == 0 && strstr(reply, "Error:"));
|
||||
#if WITH_MQTT_BRIDGE
|
||||
cli.set("logging.output invalid", reply);
|
||||
assert(cli.saves == 0 && strstr(reply, "Error:"));
|
||||
cli.callbacks.apply_ok = false;
|
||||
cli.set("logging.output both", reply);
|
||||
assert(cli.persisted.usb_logging_enabled && cli.persisted.bridge_enabled);
|
||||
assert(live_usb && !cli.callbacks.live_wifi);
|
||||
assert(strstr(reply, "saved, but MQTT runtime change failed"));
|
||||
#endif
|
||||
}
|
||||
'''
|
||||
|
||||
|
||||
class LoggingTransactionTest(unittest.TestCase):
|
||||
def test_companion_reply_survives_input_buffer_reset_during_mode_change(self):
|
||||
source = (ROOT / 'examples/companion_radio/MyMesh.cpp').read_text()
|
||||
branch = extract_braced(source, 'if (strncmp(command, "set logging.output ", 19) == 0)')
|
||||
harness = r'''
|
||||
#include <cassert>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <initializer_list>
|
||||
#include "helpers/CLICommandUtils.h"
|
||||
static char input[64];
|
||||
namespace CompanionMqttSetupPortal { static bool saveEnabled(bool) { return true; } }
|
||||
namespace mesh { static bool saveUsbLoggingBootPreference(bool) { return true; } }
|
||||
struct Mesh {
|
||||
bool _mqtt_enabled = true;
|
||||
struct { unsigned usb_logging_enabled = 1; } _prefs;
|
||||
void stopMQTT() {}
|
||||
bool savePrefs() { return true; }
|
||||
void applyUsbLoggingState(bool) { memset(input, 0, sizeof(input)); }
|
||||
bool handle(const char* command, char* reply, size_t reply_size) { @BRANCH@ return false; }
|
||||
};
|
||||
int main() {
|
||||
for (const char* mode : {"off", "usb", "wifi", "both"}) {
|
||||
Mesh node;
|
||||
char reply[160], expected[160];
|
||||
snprintf(input, sizeof(input), "set logging.output %s", mode);
|
||||
snprintf(expected, sizeof(expected), "OK - logging.output %s (saved)", mode);
|
||||
assert(node.handle(input, reply, sizeof(reply)));
|
||||
assert(input[0] == 0 && strcmp(reply, expected) == 0);
|
||||
}
|
||||
}
|
||||
'''.replace('@BRANCH@', branch)
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
cpp, exe = Path(directory) / 'test.cpp', Path(directory) / 'test.exe'
|
||||
cpp.write_text(harness)
|
||||
subprocess.run(['g++', '-std=c++17', '-Wall', '-Wextra', '-Werror',
|
||||
'-I' + str(ROOT / 'src'), str(cpp), '-o', str(exe)], check=True)
|
||||
subprocess.run([str(exe)], check=True)
|
||||
|
||||
def test_real_setters_preserve_live_and_saved_state_on_failure(self):
|
||||
source = (ROOT / 'src/helpers/CommonCLI.cpp').read_text()
|
||||
usb = extract_braced(source, 'if (strncmp(config, "usb.logging", 11) == 0')
|
||||
mqtt = extract_braced(source, 'if (strncmp(config, "logging.output", 14) == 0')
|
||||
for enabled in (0, 1):
|
||||
with self.subTest(mqtt=enabled), tempfile.TemporaryDirectory() as directory:
|
||||
cpp, exe = Path(directory) / 'test.cpp', Path(directory) / 'test.exe'
|
||||
cpp.write_text(HARNESS.replace('@BRANCHES@', usb + ('\n' + mqtt if enabled else '')))
|
||||
subprocess.run(['g++', '-std=c++17', '-Wall', '-Wextra', '-Werror',
|
||||
'-DWITH_MQTT_BRIDGE=' + str(enabled),
|
||||
'-I' + str(ROOT / 'src'), str(cpp), '-o', str(exe)], check=True)
|
||||
subprocess.run([str(exe)], check=True)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -38,16 +38,20 @@ struct Mesh {
|
||||
bool terminal = false, stale_input = true;
|
||||
bool isTerminalMode() const { return terminal; }
|
||||
void resetUsbHostSessionInput() { stale_input = false; }
|
||||
void applyUsbLoggingState(bool);
|
||||
} the_mesh;
|
||||
struct SerialInterface {
|
||||
bool passthrough = false, old_frame = true;
|
||||
uint32_t completed = 7;
|
||||
void setPassthroughMode(bool value) { passthrough = value; }
|
||||
bool isPassthroughMode() const { return passthrough; }
|
||||
void resetSessionState() { old_frame = false; }
|
||||
uint32_t getCompletedFrameCount() const { return completed; }
|
||||
} usb_serial_interface;
|
||||
struct Board { bool isUsbHostConnected() const { return data_connected; } } board;
|
||||
static bool usb_mota_mode = false, usb_logging_terminal_mode = false;
|
||||
static bool usb_logging_network_parked = false;
|
||||
static bool usb_protocol_initialized = true;
|
||||
static bool usb_host_session_connected = false, usb_terminal_discard_line = true;
|
||||
static bool usb_terminal_host_reset_completion_pending = false;
|
||||
static uint32_t usb_terminal_host_reset_retry_at = 0;
|
||||
@@ -57,13 +61,27 @@ static mesh::UsbAsciiSessionDefault usb_ascii_session_default;
|
||||
static mesh::UsbHostPresenceDebouncer usb_hwcdc_host_presence;
|
||||
static constexpr unsigned USB_TRANSPORT_RESET_RETRY_MS = 1000;
|
||||
static constexpr unsigned USB_HOST_LOSS_EDGE_MS = 100, USB_HOST_LOSS_GRACE_MS = 2000;
|
||||
struct Stream {
|
||||
int pending = 0;
|
||||
int available() const { return pending; }
|
||||
int read() { return pending-- > 0 ? 'x' : -1; }
|
||||
void print(const char*) {}
|
||||
} usb_stream;
|
||||
static Stream& usbTerminalOutput() { return usb_stream; }
|
||||
namespace mesh {
|
||||
Stream& usbCompanionPort() { return usb_stream; }
|
||||
void discardUsbTerminalOutput() {}
|
||||
bool takeUsbTerminalSessionReset() { bool value = reset_event; reset_event = false; return value; }
|
||||
bool resetUsbCompanionTransport() { return transport_ready; }
|
||||
bool tryCompleteUsbTerminalSessionReset() { return transport_ready; }
|
||||
bool hasDedicatedUsbLoggingPort() { return dedicated_logging; }
|
||||
bool isUsbLoggingEnabled() { return logging_enabled; }
|
||||
void setUsbLoggingEnabled(bool enabled) {
|
||||
if (enabled && usb_protocol_initialized && !dedicated_logging) {
|
||||
assert(usb_serial_interface.passthrough); // Must park before gate opens.
|
||||
}
|
||||
logging_enabled = enabled;
|
||||
}
|
||||
}
|
||||
static bool isUsbTerminalDataConnected() { return data_connected; }
|
||||
static bool isNetworkTerminalActive() { return network_active; }
|
||||
@@ -89,6 +107,7 @@ static void enterUsbLoggingTerminalMode() {
|
||||
static void service() {
|
||||
serviceUsbTerminalHostSessionReset();
|
||||
serviceUsbAsciiSessionDefault();
|
||||
serviceUsbLoggingOwnership(logging_enabled);
|
||||
}
|
||||
static void boundary(bool event = true) {
|
||||
reset_event = event;
|
||||
@@ -156,6 +175,41 @@ int main() {
|
||||
boundary(false);
|
||||
assert(the_mesh.terminal);
|
||||
#endif
|
||||
// A network CLI must retain ownership while USB diagnostics exclude frames.
|
||||
the_mesh.terminal = false;
|
||||
usb_serial_interface.passthrough = false;
|
||||
network_active = true;
|
||||
logging_enabled = false;
|
||||
the_mesh.applyUsbLoggingState(true);
|
||||
assert(network_active && !the_mesh.terminal);
|
||||
assert(usb_serial_interface.passthrough && usb_logging_network_parked);
|
||||
usb_stream.pending = 5;
|
||||
serviceUsbLoggingOwnership(logging_enabled);
|
||||
assert(usb_stream.pending == 5); // Still log-only; no USB CLI takes ownership.
|
||||
the_mesh.applyUsbLoggingState(false);
|
||||
assert(network_active && !the_mesh.terminal);
|
||||
assert(!usb_serial_interface.passthrough && !usb_logging_network_parked);
|
||||
assert(usb_stream.pending == 0);
|
||||
logging_enabled = true;
|
||||
serviceUsbLoggingOwnership(logging_enabled);
|
||||
network_active = false;
|
||||
usb_stream.pending = 5;
|
||||
serviceUsbLoggingOwnership(logging_enabled);
|
||||
assert(the_mesh.terminal && usb_logging_terminal_mode);
|
||||
assert(usb_serial_interface.passthrough && !usb_logging_network_parked);
|
||||
assert(usb_stream.pending == 0);
|
||||
// An ASCII-restoring network handoff must not accidentally reenable frames.
|
||||
logging_enabled = false;
|
||||
usb_logging_network_parked = true;
|
||||
usb_logging_terminal_mode = false;
|
||||
serviceUsbLoggingOwnership(logging_enabled);
|
||||
assert(the_mesh.terminal && usb_serial_interface.passthrough);
|
||||
// Remote logging cancels exclusive mOTA before the next framed dispatch.
|
||||
usb_mota_mode = true;
|
||||
the_mesh.terminal = false;
|
||||
logging_enabled = true;
|
||||
serviceUsbLoggingOwnership(logging_enabled);
|
||||
assert(!usb_mota_mode && the_mesh.terminal && usb_serial_interface.passthrough);
|
||||
logging_enabled = true;
|
||||
beginUsbDefaultSession();
|
||||
assert(the_mesh.terminal && usb_logging_terminal_mode);
|
||||
@@ -208,9 +262,11 @@ class CompanionUsbDefaultTest(unittest.TestCase):
|
||||
self.assertIn("companion_transport_boot_mode =\n CompanionTransportMode::Bluetooth;", MAIN)
|
||||
|
||||
def test_startup_and_reconnection_for_each_usb_transport(self):
|
||||
functions = "\n".join(function("static void " + name + "(") for name in (
|
||||
functions = "\n".join(function("static void " + name + "() {") for name in (
|
||||
"resetUsbTerminalHostSession", "serviceUsbTerminalHostSessionReset",
|
||||
"serviceUsbAsciiSessionDefault", "beginUsbDefaultSession"))
|
||||
functions += "\n" + function("static void serviceUsbLoggingOwnership(bool logging_enabled) {")
|
||||
functions += "\n" + function("void MyMesh::applyUsbLoggingState(bool enabled) {").replace("MyMesh::", "Mesh::")
|
||||
profiles = {
|
||||
"nrf52": ["NRF52_PLATFORM=1"],
|
||||
"esp32-tinyusb": ["ESP32=1", "ARDUINO_USB_MODE=0", "ARDUINO_USB_CDC_ON_BOOT=1", "MESH_ESP32_TINYUSB_NONBLOCKING=1"],
|
||||
@@ -237,8 +293,22 @@ class CompanionUsbDefaultTest(unittest.TestCase):
|
||||
loop = function("\nvoid loop() {")
|
||||
self.assertLess(loop.index("serviceUsbTerminalHostSessionReset();"), loop.index("serviceUsbAsciiSessionDefault();"))
|
||||
self.assertLess(loop.index("serviceUsbAsciiSessionDefault();"), loop.index("the_mesh.loop();"))
|
||||
self.assertLess(loop.index("serviceUsbLoggingOwnership(mesh::isUsbLoggingEnabled());"), loop.index("the_mesh.loop();"))
|
||||
self.assertLess(loop.index("expireUsbBinaryStartupProbeBeforeDispatch();"), loop.index("the_mesh.loop();"))
|
||||
self.assertNotIn("COMPANION_RADIO_FULL", MAIN)
|
||||
source = (ROOT / "examples/companion_radio/MyMesh.cpp").read_text()
|
||||
self.assertNotIn("mesh::setUsbLoggingEnabled(", source)
|
||||
logging_reply = source[source.index('if (strncmp(command, "set logging.output ", 19)'):
|
||||
source.index('if ((strncmp(command, "get ", 4)', source.index('if (strncmp(command, "set logging.output ", 19)'))]
|
||||
self.assertIn("mesh::cli::loggingOutputName(usb, wifi)", logging_reply)
|
||||
terminal = function("static void serviceUsbTerminal() {")
|
||||
self.assertLess(terminal.index("serviceUsbLoggingOwnership("), terminal.index("if (usb_mota_mode)"))
|
||||
leave = function("static void leaveUsbTerminalMode(bool acknowledge) {")
|
||||
self.assertIn("!mesh::hasDedicatedUsbLoggingPort() && mesh::isUsbLoggingEnabled()", leave)
|
||||
browser = function("bool MyMesh::beginStreamTerminal(Stream& output) {")
|
||||
self.assertIn("serviceUsbLoggingOwnership(mesh::isUsbLoggingEnabled());", browser)
|
||||
tcp = function("static void ota_console_loop() {")
|
||||
self.assertIn("serviceUsbLoggingOwnership(mesh::isUsbLoggingEnabled());", tcp)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Execute the real native-TinyUSB role output pumps with small host stubs."""
|
||||
"""Execute the real native-USB role output pumps with small host stubs."""
|
||||
|
||||
from pathlib import Path
|
||||
import os
|
||||
@@ -23,8 +23,9 @@ HARNESS = r'''
|
||||
#include <vector>
|
||||
#include <cstdint>
|
||||
#include <helpers/FileRead.h>
|
||||
#define MESH_ESP32_TINYUSB_NONBLOCKING 1
|
||||
@USB_CONFIG@
|
||||
#define MESH_ESP32_USB_CONSOLE_COOPERATIVE 1
|
||||
#define PUB_KEY_SIZE 32
|
||||
#define MAX_ROUTE_HASH_BYTES 3
|
||||
#define PACKET_LOG_FILE "/packet_log"
|
||||
static void require(bool ok, const char* what) {
|
||||
@@ -104,6 +105,15 @@ struct SimpleMeshTables {
|
||||
return result < static_cast<int>(rows.size()) ? &rows[result] : nullptr;
|
||||
}
|
||||
};
|
||||
struct FakeAcl {
|
||||
struct Client {
|
||||
struct { uint8_t pub_key[PUB_KEY_SIZE]; } id{};
|
||||
uint8_t permissions = 3;
|
||||
};
|
||||
std::vector<Client> clients;
|
||||
int getNumClients() const { return static_cast<int>(clients.size()); }
|
||||
Client* getClientByIdx(int index) { return &clients.at(index); }
|
||||
};
|
||||
class MyMesh {
|
||||
public:
|
||||
FakeFS* _fs = &fs;
|
||||
@@ -114,6 +124,9 @@ class MyMesh {
|
||||
bool serial_log_active = false;
|
||||
bool serial_log_eof_pending = false;
|
||||
bool serial_log_skip_line = false;
|
||||
int serial_acl_next = -1;
|
||||
int serial_acl_count = 0;
|
||||
bool serial_acl_header = false;
|
||||
int serial_recent_next = -1;
|
||||
int serial_recent_count = 0;
|
||||
bool serial_recent_header = false;
|
||||
@@ -121,8 +134,10 @@ class MyMesh {
|
||||
SimpleMeshTables::RecentRepeaterInfo serial_recent_cursor{};
|
||||
int serial_recent_cursor_index = -1;
|
||||
SimpleMeshTables tables;
|
||||
FakeAcl acl;
|
||||
const SimpleMeshTables* getTables() const { return &tables; }
|
||||
void dumpLogFile();
|
||||
void printAclSerial();
|
||||
bool hasPendingSerialOutput() const;
|
||||
void servicePendingSerialOutput();
|
||||
void cancelPendingSerialOutput();
|
||||
@@ -207,6 +222,78 @@ int main() {
|
||||
drain(radio);
|
||||
require(console.output.empty(), "canceled old-session output leaked");
|
||||
}
|
||||
setupFile("");
|
||||
{
|
||||
MyMesh radio;
|
||||
std::string expected = "ACL:\r\n";
|
||||
for (int i = 0; i < 32; ++i) {
|
||||
FakeAcl::Client client;
|
||||
std::fill(std::begin(client.id.pub_key), std::end(client.id.pub_key), i);
|
||||
radio.acl.clients.push_back(client);
|
||||
char key[PUB_KEY_SIZE * 2 + 1], line[80];
|
||||
mesh::Utils::toHex(key, client.id.pub_key, PUB_KEY_SIZE);
|
||||
snprintf(line, sizeof(line), "%02X %s\n", client.permissions, key);
|
||||
expected += line;
|
||||
}
|
||||
require(expected.size() > 2048, "ACL test does not exceed admission reserve");
|
||||
console.capacity = 256; // Accepted with a nearly-full HWCDC/logging FIFO.
|
||||
radio.printAclSerial();
|
||||
require(console.output.empty(), "ACL is still emitted synchronously");
|
||||
radio.acl.clients.emplace_back(); // New arrivals must not extend the snapshot.
|
||||
console.capacity = 0;
|
||||
for (int i = 0; i < 1000; ++i) radio.servicePendingSerialOutput();
|
||||
require(console.output.empty(), "ACL wrote into a stalled host");
|
||||
require(radio.serial_acl_next == 0, "stalled header skipped an ACL row");
|
||||
console.fail_once = true;
|
||||
console.short_limit = 17;
|
||||
drain(radio, 73);
|
||||
require(console.output == expected, "full ACL lost or duplicated short-write bytes");
|
||||
require(!radio.hasPendingSerialOutput(), "ACL blocks the next command after completion");
|
||||
}
|
||||
setupFile("");
|
||||
{
|
||||
MyMesh radio;
|
||||
radio.acl.clients.resize(32);
|
||||
for (auto& client : radio.acl.clients) client.permissions = 0;
|
||||
radio.printAclSerial();
|
||||
drain(radio, 73);
|
||||
require(console.output == "ACL:\r\n", "deleted ACL clients were printed");
|
||||
}
|
||||
setupFile("");
|
||||
{
|
||||
MyMesh radio;
|
||||
radio.acl.clients.resize(1);
|
||||
std::fill(std::begin(radio.acl.clients[0].id.pub_key),
|
||||
std::end(radio.acl.clients[0].id.pub_key), 0xAA);
|
||||
radio.printAclSerial();
|
||||
radio.servicePendingSerialOutput();
|
||||
console.short_limit = 7;
|
||||
radio.servicePendingSerialOutput();
|
||||
require(radio.serial_log_pending_size > 0, "ACL short-write suffix was not retained");
|
||||
radio.cancelPendingSerialOutput();
|
||||
require(!radio.hasPendingSerialOutput(), "ACL cancellation leaves a pending job");
|
||||
console.output.clear();
|
||||
console.records.clear();
|
||||
console.short_limit = 0;
|
||||
radio.acl.clients.clear();
|
||||
radio.printAclSerial();
|
||||
drain(radio, 73);
|
||||
require(console.output == "ACL:\r\n", "old ACL row leaked after reconnect cancellation");
|
||||
}
|
||||
setupFile("existing file dump\n");
|
||||
{
|
||||
MyMesh radio;
|
||||
radio.dumpLogFile();
|
||||
console.capacity = 0;
|
||||
radio.servicePendingSerialOutput();
|
||||
const size_t pending = radio.serial_log_pending_size;
|
||||
radio.printAclSerial();
|
||||
require(radio.serial_log_pending_size == pending && radio.serial_acl_next == -1,
|
||||
"overlapping ACL command overwrote a pending file record");
|
||||
drain(radio);
|
||||
require(console.output == "existing file dump\n" + eof,
|
||||
"overlapping ACL command corrupted an existing output job");
|
||||
}
|
||||
@RECENT_TEST@
|
||||
std::cout << "cooperative output checks passed\n";
|
||||
return 0;
|
||||
@@ -241,12 +328,23 @@ RECENT_TEST = r'''
|
||||
|
||||
|
||||
class CooperativeOutputTest(unittest.TestCase):
|
||||
def test_usb_acl_commands_route_to_the_cooperative_pump(self):
|
||||
for role in ("simple_repeater", "simple_room_server"):
|
||||
with self.subTest(role=role):
|
||||
text = (ROOT / f"examples/{role}/MyMesh.cpp").read_text()
|
||||
start = text.index('strcmp(command, "get acl") == 0) {')
|
||||
end = text.index("reply[0] = 0;", text.index("#endif", start))
|
||||
self.assertIn("printAclSerial();", text[start:end])
|
||||
self.assertNotIn("mesh::usbConsolePort().printf", text[start:end])
|
||||
|
||||
def test_real_role_pumps(self):
|
||||
compiler = shutil.which("g++") or shutil.which("clang++")
|
||||
if not compiler:
|
||||
self.skipTest("A host C++ compiler is required for pump execution")
|
||||
for role in ("simple_repeater", "simple_room_server"):
|
||||
with self.subTest(role=role), tempfile.TemporaryDirectory() as temporary:
|
||||
for role, config in ((role, config)
|
||||
for role in ("simple_repeater", "simple_room_server")
|
||||
for config in ("MESH_ESP32_TINYUSB_NONBLOCKING", "MESH_ESP32_HWCDC_SESSION_GUARD")):
|
||||
with self.subTest(role=role, transport=config), tempfile.TemporaryDirectory() as temporary:
|
||||
text = (ROOT / f"examples/{role}/MyMesh.cpp").read_text()
|
||||
dump_start = text.index("void MyMesh::dumpLogFile()")
|
||||
dump_end = text.index("\n#if MESH_ESP32_USB_CONSOLE_COOPERATIVE\nbool MyMesh::hasPendingSerialOutput", dump_start)
|
||||
@@ -261,7 +359,7 @@ class CooperativeOutputTest(unittest.TestCase):
|
||||
methods = text[format_start:format_end] + "\n" + text[recent_start:recent_end] + "\n" + methods
|
||||
program = HARNESS.replace("@METHODS@", methods).replace(
|
||||
"@RECENT_TEST@", RECENT_TEST if role == "simple_repeater" else ""
|
||||
)
|
||||
).replace("@USB_CONFIG@", f"#define {config} 1")
|
||||
executable = Path(temporary) / ("pump.exe" if os.name == "nt" else "pump")
|
||||
build = subprocess.run(
|
||||
[compiler, "-std=c++17", "-O0", "-I", str(ROOT / "src"),
|
||||
|
||||
@@ -140,15 +140,19 @@ class Esp32TinyUsbRoleHygieneTest(unittest.TestCase):
|
||||
|
||||
def test_sleep_keeps_raw_flush_only_for_other_esp32_transports(self):
|
||||
text = source("src/helpers/ESP32Board.cpp")
|
||||
shutdown = text[text.index("void ESP32Board::shutdownPeripherals()")
|
||||
: text.index("void ESP32Board::powerOff()")]
|
||||
sleep = text[text.index("void ESP32Board::enterDeepSleep(") :]
|
||||
guard = re.search(
|
||||
r"#if MESH_ESP32_USB_CONSOLE_COOPERATIVE\s+"
|
||||
r"mesh::serviceUsbTerminalPort\(\);\s+"
|
||||
r"#else\s+Serial\.flush\(\);\s+#endif",
|
||||
sleep,
|
||||
shutdown,
|
||||
)
|
||||
self.assertIsNotNone(guard)
|
||||
self.assertEqual(sleep.count("Serial.flush();"), 1)
|
||||
self.assertEqual(shutdown.count("Serial.flush();"), 1)
|
||||
self.assertNotIn("Serial.flush();", sleep)
|
||||
self.assertIn("shutdownPeripherals();", sleep)
|
||||
|
||||
def test_large_file_dumps_are_bounded_and_cancelable(self):
|
||||
for role in ROLES:
|
||||
@@ -180,6 +184,7 @@ class Esp32TinyUsbRoleHygieneTest(unittest.TestCase):
|
||||
def test_recent_list_advances_only_after_whole_row_admission(self):
|
||||
text = source("examples/simple_repeater/MyMesh.cpp")
|
||||
start = text.index("void MyMesh::servicePendingSerialOutput()")
|
||||
start = text.index("if (serial_recent_next >= 0)", start)
|
||||
service = text[start : text.index("if (!serial_log_active)", start)]
|
||||
self.assertIn("char record[64];", service)
|
||||
self.assertIn("serial_recent_next < serial_recent_count", service)
|
||||
|
||||
@@ -31,6 +31,10 @@ HARNESS = r'''
|
||||
static bool mounted = true, dtr[2] = {false, false}, dfu = false;
|
||||
static uint32_t now_ms = 100, rx_count = 0, tx_count = 0;
|
||||
static unsigned rx_flushes = 0, tx_clears = 0, log_edges = 0;
|
||||
// Model the endpoint separately from the software FIFO. A FIFO clear must
|
||||
// leave an armed packet intact; only a real detach + enumeration retires it.
|
||||
static bool armed_in[2] = {false, false}, attached = true;
|
||||
static unsigned detaches = 0, attaches = 0;
|
||||
static bool close_during_rx_sample = false;
|
||||
static uint32_t millis() { return now_ms; }
|
||||
static bool tud_mounted() { return mounted; }
|
||||
@@ -46,6 +50,11 @@ static uint32_t tud_cdc_n_available(uint8_t) {
|
||||
}
|
||||
static void tud_cdc_n_read_flush(uint8_t) { ++rx_flushes; rx_count = 0; }
|
||||
static uint32_t tud_cdc_n_write_clear(uint8_t) { ++tx_clears; tx_count = 0; return 0; }
|
||||
static bool mesh_tud_cdc_n_tx_pending(uint8_t n) { return armed_in[n]; }
|
||||
struct UsbDevice {
|
||||
void detach() { ++detaches; attached = false; }
|
||||
void attach() { ++attaches; attached = true; }
|
||||
} TinyUSBDevice;
|
||||
static uint32_t tud_cdc_n_write_available(uint8_t) { return 64 - tx_count; }
|
||||
static uint32_t tud_cdc_n_write(uint8_t, const void*, uint32_t size) {
|
||||
const auto count = std::min(size, 64 - tx_count); tx_count += count; return count;
|
||||
@@ -144,6 +153,32 @@ int main() {
|
||||
// No write waits when the FIFO is full, including with DTR low.
|
||||
tx_count = 64;
|
||||
assert(mesh::writeTinyUsbCdcOnce(nullptr, bytes, 4) == 0);
|
||||
// A previously submitted IN packet survives FIFO clearing. Quarantine the
|
||||
// entire device immediately, but never wait on the unread endpoint.
|
||||
tx_count = 4; armed_in[0] = true;
|
||||
rx_count = 4;
|
||||
assert(mesh::isUsbCompanionClientConnected());
|
||||
line(false);
|
||||
assert(tx_count == 0 && armed_in[0] && !attached && detaches == 1);
|
||||
assert(!mesh::isUsbCompanionClientConnected());
|
||||
assert(mesh::writeTinyUsbCdcOnce(nullptr, bytes, 4) == 0);
|
||||
// Repeated control callbacks cannot cause a detach/attach storm.
|
||||
meshTinyUsbCdcLineCodingChanged(0);
|
||||
line(false);
|
||||
assert(detaches == 1);
|
||||
now_ms += 100;
|
||||
mesh::serviceNrf52UsbSessionReenumeration();
|
||||
assert(attaches == 0); // owner has not consumed unplug yet
|
||||
mounted = false; armed_in[0] = false; dtr[0] = false;
|
||||
meshTinyUsbDeviceSessionBoundary();
|
||||
mesh::serviceNrf52UsbSessionReenumeration();
|
||||
assert(attaches == 1 && attached);
|
||||
mounted = true;
|
||||
meshTinyUsbDeviceSessionBoundary();
|
||||
settle();
|
||||
rx_count = 4;
|
||||
assert(mesh::isUsbCompanionClientConnected());
|
||||
rx_count = 0;
|
||||
baud = 1200;
|
||||
line(false);
|
||||
assert(dfu);
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run actual nRF52 USB lifecycle code with independent FIFO/endpoint state."""
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
USB = (ROOT / "src/helpers/UsbLogging.cpp").read_text()
|
||||
|
||||
|
||||
def function(signature):
|
||||
start = USB.index(signature)
|
||||
end = USB.index("{", start) + 1
|
||||
depth = 1
|
||||
while depth:
|
||||
depth += (USB[end] == "{") - (USB[end] == "}")
|
||||
end += 1
|
||||
return USB[start:end]
|
||||
|
||||
|
||||
class Nrf52UsbLifecycleTest(unittest.TestCase):
|
||||
def run_native(self, source):
|
||||
compiler = shutil.which("g++")
|
||||
if compiler is None:
|
||||
self.skipTest("C++ compiler unavailable")
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
cpp = Path(directory) / "lifecycle.cpp"
|
||||
binary = Path(directory) / "lifecycle"
|
||||
cpp.write_text(source)
|
||||
subprocess.run([compiler, "-std=c++17", "-Wall", "-Wextra",
|
||||
str(cpp), "-o", str(binary)], check=True,
|
||||
capture_output=True)
|
||||
subprocess.run([str(binary)], check=True, capture_output=True)
|
||||
|
||||
def test_framework_detach_queues_local_unmount_without_vbus_edge(self):
|
||||
pinned_driver = (ROOT / "test/fixtures/nrf52_usb_power_original.c").read_text()
|
||||
start = pinned_driver.index("void dcd_disconnect(")
|
||||
disconnect = pinned_driver[start:pinned_driver.index("\n}\n", start) + 3]
|
||||
self.run_native(r'''
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
static bool mounted = true, vbus = true;
|
||||
static unsigned owner_events = 0, reset_count = 0;
|
||||
static constexpr unsigned DCD_EVENT_UNPLUGGED = 0;
|
||||
struct Registers { unsigned USBPULLUP = 1; } registers;
|
||||
#define NRF_USBD (®isters)
|
||||
static void dcd_event_bus_signal(unsigned port, unsigned event, bool isr) {
|
||||
assert(port == 0 && event == DCD_EVENT_UNPLUGGED && !isr);
|
||||
++owner_events; // enqueue, do not automatically fake a VBUS/unmount edge
|
||||
}
|
||||
@DISCONNECT@
|
||||
// Actual usbd.c wrapper and Adafruit_USBD_Device.h inline entry point.
|
||||
static bool tud_disconnect() { dcd_disconnect(0); return true; }
|
||||
struct Device { bool detach() { return tud_disconnect(); } } TinyUSBDevice;
|
||||
static void usbd_reset(unsigned port) {
|
||||
assert(port == 0); mounted = false; ++reset_count;
|
||||
}
|
||||
static void tud_umount_cb() {}
|
||||
static void owner_task() {
|
||||
assert(owner_events == 1); --owner_events;
|
||||
// Exact DCD_EVENT_UNPLUGGED owner branch from the pinned usbd.c.
|
||||
usbd_reset(0);
|
||||
tud_umount_cb();
|
||||
}
|
||||
int main() {
|
||||
assert(TinyUSBDevice.detach());
|
||||
assert(vbus && registers.USBPULLUP == 0 && mounted);
|
||||
assert(owner_events == 1); // without this, recovery would wait forever
|
||||
owner_task();
|
||||
assert(vbus && !mounted && reset_count == 1);
|
||||
}
|
||||
'''.replace("@DISCONNECT@", disconnect))
|
||||
|
||||
def test_descriptor_mutation_is_detached_even_before_mount(self):
|
||||
source = r'''
|
||||
#include <atomic>
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#define MESH_DUAL_CDC_LOGGING 1
|
||||
static bool attached = true, mounted = false, descriptor_fetched = false;
|
||||
static unsigned detach_count = 0, attach_count = 0, descriptor_count = 1;
|
||||
static unsigned delay_ms = 0;
|
||||
struct Device {
|
||||
bool mounted() { return ::mounted; }
|
||||
void detach() { attached = false; ++detach_count; }
|
||||
void attach() {
|
||||
assert(descriptor_count == 2 && delay_ms >= 10);
|
||||
attached = true; ++attach_count;
|
||||
// Fresh enumeration replaces a host's old cached one-port descriptor.
|
||||
descriptor_fetched = false;
|
||||
}
|
||||
} TinyUSBDevice;
|
||||
static void delay(unsigned value) { assert(!attached); delay_ms += value; }
|
||||
namespace mesh {
|
||||
static std::atomic<bool> usb_logging_preference_known{false};
|
||||
static bool enabled = false, dedicated_usb_logging_port_started = false;
|
||||
static bool dedicated_usb_logging_port_configured = false;
|
||||
static bool isUsbLoggingEnabled() { return enabled; }
|
||||
static void setPlatformDebugOutputEnabled(bool) {}
|
||||
struct Port {
|
||||
void begin(unsigned baud) {
|
||||
assert(baud == 115200 && !attached && delay_ms >= 10);
|
||||
++descriptor_count;
|
||||
}
|
||||
} dedicated_usb_logging_port;
|
||||
@BEGIN@
|
||||
}
|
||||
int main() {
|
||||
// No descriptor or reconnect when the preference is unknown/disabled.
|
||||
mesh::beginUsbLoggingPort();
|
||||
mesh::usb_logging_preference_known = true;
|
||||
mesh::beginUsbLoggingPort();
|
||||
assert(detach_count == 0);
|
||||
// Host fetched the old descriptor but has not SET_CONFIGURATION yet.
|
||||
descriptor_fetched = true;
|
||||
assert(!mounted);
|
||||
mesh::enabled = true;
|
||||
mesh::beginUsbLoggingPort();
|
||||
assert(detach_count == 1 && attach_count == 1 && attached);
|
||||
assert(!descriptor_fetched && descriptor_count == 2);
|
||||
assert(mesh::dedicated_usb_logging_port_configured);
|
||||
mesh::beginUsbLoggingPort();
|
||||
assert(detach_count == 1 && attach_count == 1); // idempotent
|
||||
}
|
||||
'''.replace("@BEGIN@", function("void beginUsbLoggingPort()"))
|
||||
self.run_native(source)
|
||||
|
||||
def test_armed_logging_packet_and_racing_primary_packet_force_clean_bus(self):
|
||||
start = USB.index("static std::atomic<uint32_t> primary_usb_reset_generation")
|
||||
gates = USB[start:USB.index("\n#endif", start)]
|
||||
source = r'''
|
||||
#include <atomic>
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#define ENABLE_USB_INTERFACE 1
|
||||
static bool mounted = true, attached = true, armed[2] = {false, true};
|
||||
static uint32_t fifo[2] = {0, 32}, now = UINT32_MAX - 10;
|
||||
static unsigned detach_count = 0, attach_count = 0, reset_count = 0;
|
||||
static uint32_t millis() { return now; }
|
||||
static bool tud_mounted() { return mounted; }
|
||||
static uint32_t tud_cdc_n_available(uint8_t) { return 0; }
|
||||
static void tud_cdc_n_read_flush(uint8_t) {}
|
||||
static bool tud_cdc_n_write_clear(uint8_t n) { fifo[n] = 0; return true; }
|
||||
static bool mesh_tud_cdc_n_tx_pending(uint8_t n) { return armed[n]; }
|
||||
struct Device {
|
||||
void detach() { ++detach_count; attached = false; }
|
||||
void attach() { ++attach_count; attached = true; }
|
||||
} TinyUSBDevice;
|
||||
namespace mesh {
|
||||
@GATES@
|
||||
static std::atomic<bool> dedicated_usb_logging_port_connected{true};
|
||||
static std::atomic<uint32_t> dedicated_usb_logging_reset_generation{0};
|
||||
static uint8_t dedicated_usb_logging_quiet_sofs = 0;
|
||||
static constexpr uint8_t dedicated_usb_logging_host_settle_sofs = 50;
|
||||
static uint32_t primary_usb_terminal_taken_reset_generation = 0;
|
||||
static void resetDedicatedUsbLoggingUsbTaskState() { ++reset_count; }
|
||||
@RESTART@
|
||||
@COMPLETE@
|
||||
}
|
||||
int main() {
|
||||
// CDC1 old data is in endpoint RAM, not the software FIFO. No 50-SOF
|
||||
// waiting/clearing policy can retract it without re-enumeration.
|
||||
mesh::restartDedicatedUsbLoggingHostSession();
|
||||
assert(fifo[1] == 0 && armed[1] && !attached && detach_count == 1);
|
||||
assert(!mesh::dedicated_usb_logging_port_connected && reset_count == 1);
|
||||
assert(!mesh::canAccessPrimaryUsbSession(nullptr)); // both ports gated
|
||||
mesh::restartDedicatedUsbLoggingHostSession();
|
||||
assert(detach_count == 1); // repeated control requests do not reset timer
|
||||
now += 19; mounted = false; armed[1] = false;
|
||||
mesh::serviceNrf52UsbSessionReenumeration();
|
||||
assert(attach_count == 0); // wrap-safe bounded detach interval
|
||||
++now;
|
||||
mesh::serviceNrf52UsbSessionReenumeration();
|
||||
assert(attach_count == 1 && attached);
|
||||
mesh::serviceNrf52UsbSessionReenumeration();
|
||||
assert(attach_count == 1);
|
||||
// The final exclusive purge also catches an old producer that submitted
|
||||
// a primary IN packet after the owner callback's first FIFO clear.
|
||||
mounted = true; armed[0] = true; fifo[0] = 16;
|
||||
mesh::primary_usb_terminal_taken_reset_generation =
|
||||
mesh::primaryUsbSessionGeneration();
|
||||
mesh::completePrimaryUsbSessionReset(nullptr);
|
||||
assert(fifo[0] == 0 && armed[0] && !attached && detach_count == 2);
|
||||
assert(!mesh::canAccessPrimaryUsbSession(nullptr));
|
||||
}
|
||||
'''
|
||||
source = source.replace("@GATES@", gates)
|
||||
source = source.replace("@RESTART@", function("static void restartDedicatedUsbLoggingHostSession()"))
|
||||
source = source.replace("@COMPLETE@", function("static void completePrimaryUsbSessionReset("))
|
||||
self.run_native(source)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -255,7 +255,7 @@ class Nrf52UsbLoggingContractTest(unittest.TestCase):
|
||||
):]
|
||||
self.assertIn("if (instance == 1)", bridge)
|
||||
self.assertIn("handleDedicatedUsbLoggingLineState(dtr);", bridge)
|
||||
self.assertIn("tud_cdc_n_write_clear(1);", source)
|
||||
self.assertIn("clearNrf52UsbTxForSession(1);", source)
|
||||
self.assertIn("endPrimaryUsbHostSession(true);", bridge)
|
||||
self.assertIn("if (instance == 0 && !dtr)", bridge)
|
||||
self.assertIn("tud_cdc_get_line_coding(&coding);", bridge)
|
||||
@@ -306,7 +306,7 @@ class Nrf52UsbLoggingContractTest(unittest.TestCase):
|
||||
)
|
||||
self.assertLess(
|
||||
session_end.index("tud_cdc_n_read_flush(0)"),
|
||||
session_end.index("tud_cdc_n_write_clear(0)"),
|
||||
session_end.index("clearNrf52UsbTxForSession(0)"),
|
||||
)
|
||||
self.assertIn(
|
||||
"dedicated_usb_logging_line_state_dtr.exchange(", source
|
||||
@@ -332,7 +332,7 @@ class Nrf52UsbLoggingContractTest(unittest.TestCase):
|
||||
restart_gate = restart.index(
|
||||
"dedicated_usb_logging_port_connected.store(false"
|
||||
)
|
||||
restart_fifo = restart.index("tud_cdc_n_write_clear(1)")
|
||||
restart_fifo = restart.index("clearNrf52UsbTxForSession(1)")
|
||||
restart_state = restart.index("resetDedicatedUsbLoggingUsbTaskState()")
|
||||
restart_quiet = restart.index("dedicated_usb_logging_quiet_sofs =")
|
||||
restart_generation = restart.index(
|
||||
@@ -368,7 +368,7 @@ class Nrf52UsbLoggingContractTest(unittest.TestCase):
|
||||
self.assertLess(marker_write, queued_drain)
|
||||
|
||||
quiet_block = owner_service[quiet_start:positive_gate]
|
||||
quiet_fifo = quiet_block.index("tud_cdc_n_write_clear(1)")
|
||||
quiet_fifo = quiet_block.index("clearNrf52UsbTxForSession(1)")
|
||||
quiet_state = quiet_block.index(
|
||||
"resetDedicatedUsbLoggingUsbTaskState()"
|
||||
)
|
||||
@@ -595,7 +595,7 @@ class Nrf52UsbLoggingContractTest(unittest.TestCase):
|
||||
# A second purge after the settle gate would erase a new host's
|
||||
# immediately sent APP_START (meshcli does not delay after open).
|
||||
self.assertNotIn("tud_cdc_n_read_flush(0);", completion)
|
||||
self.assertIn("tud_cdc_n_write_clear(0);", completion)
|
||||
self.assertIn("clearNrf52UsbTxForSession(0);", completion)
|
||||
self.assertIn("primary_usb_allowed_generation.store(", completion)
|
||||
self.assertIn(".tryRunExclusive(", source)
|
||||
self.assertIn("primary_usb_reset_settle_until.store(", source)
|
||||
|
||||
@@ -46,6 +46,116 @@ int main() {
|
||||
|
||||
|
||||
class UsbPowerTests(unittest.TestCase):
|
||||
def test_cdc_query_tracks_armed_endpoint_not_fifo_bytes(self):
|
||||
original = r'''
|
||||
#include <assert.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include "cdc_device.h"
|
||||
#define CFG_TUD_CDC 2
|
||||
struct { uint8_t ep_in; unsigned tx_ff; } _cdcd_itf[2];
|
||||
static bool armed[256];
|
||||
static bool usbd_edpt_busy(uint8_t port, uint8_t endpoint) {
|
||||
assert(port == 0); return armed[endpoint];
|
||||
}
|
||||
static bool tu_fifo_clear(unsigned* fifo) { *fifo = 0; return true; }
|
||||
'''+ FIX.CDC_WRITE_CLEAR + r'''
|
||||
int main(void) {
|
||||
_cdcd_itf[0].ep_in = 0x81;
|
||||
_cdcd_itf[1].ep_in = 0x83;
|
||||
_cdcd_itf[0].tx_ff = 16;
|
||||
armed[0x81] = true;
|
||||
assert(mesh_tud_cdc_n_tx_pending(0));
|
||||
tud_cdc_n_write_clear(0);
|
||||
assert(_cdcd_itf[0].tx_ff == 0 && mesh_tud_cdc_n_tx_pending(0));
|
||||
assert(!mesh_tud_cdc_n_tx_pending(1));
|
||||
assert(!mesh_tud_cdc_n_tx_pending(2));
|
||||
armed[0x81] = false;
|
||||
assert(!mesh_tud_cdc_n_tx_pending(0));
|
||||
_cdcd_itf[1].ep_in = 0;
|
||||
armed[0] = true; // unopened CDC must not query control endpoint
|
||||
assert(!mesh_tud_cdc_n_tx_pending(1));
|
||||
}
|
||||
'''
|
||||
patched = FIX.patched_cdc_source(original)
|
||||
self.assertEqual(patched, FIX.patched_cdc_source(patched))
|
||||
self.assertEqual(patched, FIX.patched_cdc_source(original.replace("\n", "\r\n")))
|
||||
compiler = shutil.which("gcc")
|
||||
if compiler is None:
|
||||
self.skipTest("C compiler unavailable")
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
source = Path(directory) / "cdc.c"
|
||||
binary = Path(directory) / "cdc"
|
||||
header = Path(directory) / "class/cdc/cdc_device.h"
|
||||
header.parent.mkdir(parents=True)
|
||||
header.write_text("// Only the isolated session-query API is tested here.\n")
|
||||
source.write_text(patched)
|
||||
subprocess.run([compiler, "-std=c11", "-Wall", "-Wextra", "-Werror",
|
||||
"-I", directory, str(source), "-o", str(binary)],
|
||||
check=True, capture_output=True)
|
||||
subprocess.run([str(binary)], check=True, capture_output=True)
|
||||
|
||||
def test_relocated_cdc_driver_compiles_against_real_tinyusb_headers(self):
|
||||
# Exercise the middleware's actual relocated file, not just an API
|
||||
# extraction: cdc_device.h is originally a sibling of cdc_device.c.
|
||||
# CI without this hardware framework can still run the isolated API
|
||||
# and relocation guards; a local hardware build runs this full check.
|
||||
sdk = (Path.home() / ".platformio/packages/framework-arduinoadafruitnrf52/"
|
||||
"libraries/Adafruit_TinyUSB_Arduino/src")
|
||||
original = sdk / "class/cdc/cdc_device.c"
|
||||
compiler = shutil.which("gcc")
|
||||
if compiler is None or not original.is_file():
|
||||
self.skipTest("native compiler/nRF52 TinyUSB framework unavailable")
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
config = root / "tusb_config.h"
|
||||
config.write_text("""
|
||||
#define CFG_TUSB_MCU OPT_MCU_NONE
|
||||
#define CFG_TUSB_OS OPT_OS_NONE
|
||||
#define CFG_TUSB_RHPORT0_MODE OPT_MODE_DEVICE
|
||||
#define CFG_TUD_ENABLED 1
|
||||
#define CFG_TUD_CDC 2
|
||||
#define CFG_TUD_CDC_RX_BUFSIZE 256
|
||||
#define CFG_TUD_CDC_TX_BUFSIZE 256
|
||||
#define CFG_TUSB_DEBUG 0
|
||||
""")
|
||||
class Node:
|
||||
def srcnode(self): return self
|
||||
def get_abspath(self): return str(original)
|
||||
class Env:
|
||||
def subst(self, value):
|
||||
assert value == "$BUILD_DIR"
|
||||
return str(root / "build")
|
||||
def File(self, value): return Path(value)
|
||||
patched = FIX.replace_driver(Env(), Node())
|
||||
self.assertNotEqual(patched.parent, original.parent)
|
||||
self.assertIn(FIX.CDC_ROOT_INCLUDE, patched.read_text())
|
||||
result = subprocess.run(
|
||||
[compiler, "-std=c11", '-DCFG_TUSB_CONFIG_FILE="' + config.as_posix() + '"',
|
||||
"-I", str(root), "-I", str(sdk),
|
||||
"-c", str(patched), "-o", str(root / "cdc.o")],
|
||||
capture_output=True, text=True)
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
|
||||
def test_changed_cdc_driver_fails_closed(self):
|
||||
prefix = FIX.CDC_LOCAL_INCLUDE + "\n"
|
||||
for source in ("", prefix + FIX.CDC_WRITE_CLEAR.replace("tx_ff", "tx_fifo"),
|
||||
prefix + FIX.CDC_WRITE_CLEAR * 2,
|
||||
prefix + FIX.CDC_WRITE_CLEAR + FIX.CDC_SESSION_QUERY,
|
||||
FIX.CDC_ROOT_INCLUDE + "\n" + FIX.CDC_WRITE_CLEAR):
|
||||
with self.subTest(source=source), self.assertRaises(RuntimeError):
|
||||
FIX.patched_cdc_source(source)
|
||||
|
||||
def test_cdc_query_is_built_from_local_copy(self):
|
||||
hooks = []
|
||||
class Env:
|
||||
def AddBuildMiddleware(self, handler, pattern):
|
||||
hooks.append((handler, pattern))
|
||||
FIX.install(Env())
|
||||
self.assertEqual(len(hooks), 3)
|
||||
self.assertTrue(any("class*cdc*cdc_device.c" in pattern
|
||||
for _, pattern in hooks))
|
||||
|
||||
def test_actual_handler_and_inherited_hang(self):
|
||||
exercise(FIX.patched_source(ORIGINAL))
|
||||
|
||||
@@ -76,7 +186,9 @@ class UsbPowerTests(unittest.TestCase):
|
||||
|
||||
def test_unrecognized_or_partly_fixed_driver_fails_closed(self):
|
||||
for source in ("", ORIGINAL.replace("hfclk_running()", "clock_running()"),
|
||||
ORIGINAL + ORIGINAL, ORIGINAL.replace(FIX.OLD_READY, FIX.READY)):
|
||||
ORIGINAL + ORIGINAL, ORIGINAL.replace(FIX.OLD_READY, FIX.READY),
|
||||
ORIGINAL.replace("dcd_event_bus_signal(0, DCD_EVENT_UNPLUGGED, false);",
|
||||
"// removed local disconnect event")):
|
||||
with self.subTest(source=source[:40]), self.assertRaises(RuntimeError):
|
||||
FIX.patched_source(source)
|
||||
|
||||
|
||||
@@ -858,9 +858,15 @@ TEST(SerialModeSwitch, SingleTtyLoggingCannotStealNetworkTerminal) {
|
||||
using Action = mesh::UsbLoggingTerminalAction;
|
||||
EXPECT_EQ(mesh::selectUsbLoggingTerminalAction(
|
||||
false, true, false, false, true, true),
|
||||
Action::NO_ACTION);
|
||||
Action::PARK_USB_FOR_NETWORK);
|
||||
EXPECT_EQ(mesh::selectUsbLoggingTerminalAction(
|
||||
false, true, false, false, false, true),
|
||||
Action::PARK_USB_FOR_NETWORK);
|
||||
EXPECT_EQ(mesh::selectUsbLoggingTerminalAction(
|
||||
true, true, false, false, true, true),
|
||||
Action::NO_ACTION);
|
||||
EXPECT_EQ(mesh::selectUsbLoggingTerminalAction(
|
||||
false, false, false, false, true, true),
|
||||
Action::NO_ACTION);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Exercise Ethernet diagnostics and the production NetworkLink logging macro."""
|
||||
|
||||
from pathlib import Path
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
FIXTURE = ROOT / "test/fixtures/usb_diagnostic_logging"
|
||||
|
||||
|
||||
class UsbDiagnosticLoggingTests(unittest.TestCase):
|
||||
def test_runtime_gate_backpressure_and_functional_ethernet_output(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")
|
||||
network = (ROOT / "src/helpers/NetworkLink.cpp").read_text(encoding="utf-8")
|
||||
start = network.index("// Same \"MQTT: \" prefix")
|
||||
macro = network[start:network.index("namespace {", start)]
|
||||
with tempfile.TemporaryDirectory(prefix="meshcore-usb-diagnostics-") as directory:
|
||||
temporary = Path(directory)
|
||||
(temporary / "NetworkLoggingMacro.h").write_text(macro, encoding="utf-8")
|
||||
for debug in (None, 0, 1):
|
||||
with self.subTest(mqtt_debug=debug):
|
||||
binary = temporary / "usb-diagnostics.exe"
|
||||
defines = [] if debug is None else [f"-DMQTT_DEBUG={debug}"]
|
||||
compiled = subprocess.run(
|
||||
[compiler, "-std=c++17", "-Wall", "-Wextra", "-Werror",
|
||||
"-DARDUINO=1", "-DETHERNET_ENABLED=1", *defines,
|
||||
f"-I{FIXTURE / 'mocks'}", f"-I{ROOT / 'src'}",
|
||||
f"-I{temporary}", str(FIXTURE / "test.cpp"),
|
||||
"-o", str(binary)],
|
||||
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=10,
|
||||
)
|
||||
self.assertEqual(checked.returncode, 0, checked.stdout + checked.stderr)
|
||||
self.assertIn("PASS: gated USB diagnostics", checked.stdout)
|
||||
|
||||
def test_no_raw_serial_diagnostic_writes(self):
|
||||
for relative in ("src/helpers/nrf52/EthernetCLI.h", "src/helpers/NetworkLink.cpp"):
|
||||
with self.subTest(source=relative):
|
||||
source = (ROOT / relative).read_text(encoding="utf-8")
|
||||
self.assertIn("UsbLogging.h", source)
|
||||
self.assertIn("mesh::isUsbLoggingEnabled()", source)
|
||||
self.assertIn("mesh::usbLoggingPort()", source)
|
||||
self.assertIsNone(re.search(r"\bSerial\.(?:print|println|printf|write)\s*\(", source))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user