Files
MeshCore/test/test_nrf52_framework_logging.py
T

251 lines
9.8 KiB
Python

"""Reproduce framework flash-event text corrupting the Companion USB stream."""
import importlib.util
from pathlib import Path
import shutil
import subprocess
import tempfile
import unittest
from test_replay_reset_integration import extract_braced
ROOT = Path(__file__).resolve().parents[1]
SPEC = importlib.util.spec_from_file_location(
"nrf52_core_fix", ROOT / "scripts/nrf52_loop_stack_fix.py")
FIX = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(FIX)
# The pinned Adafruit core's printf retarget, including its explicit non-USB
# logger branches. The runtime test executes the production patch against it.
CORE_LOGGER = """// nanolib printf() retarget
// Logger 0: Serial (CDC), 1 Serial1 (UART), 2 Segger RTT
int _write (int fd, const void *buf, size_t count)
{
(void) fd;
size_t ret = 0;
#if CFG_LOGGER == 2 || CFG_SYSVIEW
SEGGER_RTT_Write(0, buf, count);
ret = count;
#elif CFG_LOGGER == 1
if ( Serial1 )
{
ret = Serial1.write((const uint8_t *) buf, count);
}
#else
if ( Serial )
{
ret = Serial.write((const uint8_t *) buf, count);
}
#endif
return (int) ret;
}
"""
ARDUINO = """#pragma once
#include <cstddef>
#include <cstdint>
class Stream {
public:
virtual ~Stream() = default;
virtual int available() { return 0; }
virtual int read() { return -1; }
virtual int peek() { return -1; }
virtual void flush() {}
virtual int availableForWrite() { return 0; }
virtual size_t write(uint8_t byte) { return write(&byte, 1); }
virtual size_t write(const uint8_t*, size_t) = 0;
};
"""
RUNTIME = r'''
#include <atomic>
#include <cassert>
#include <cstring>
#include <helpers/NonBlockingWriteStream.h>
struct FakeSerial : Stream {
bool connected = true;
int capacity = 4096;
unsigned bytes = 0, calls = 0;
explicit operator bool() const { return connected; }
int availableForWrite() override { return connected ? capacity : 0; }
size_t write(const uint8_t*, size_t size) override {
++calls; bytes += size; return size;
}
};
static FakeSerial Serial, Serial1, dedicated;
static unsigned rtt_bytes = 0;
static void SEGGER_RTT_Write(unsigned, const void*, size_t size) { rtt_bytes += size; }
static bool in_isr = false;
static bool isInISR() { return in_isr; }
namespace mesh {
static bool logging = true, debug = true, packet_stream = false;
static bool dedicated_usb_logging_port_started = true;
static std::atomic<bool> dedicated_usb_logging_port_connected{true};
static bool isUsbLoggingEnabled() { return logging; }
static bool isUsbDebugEnabled() { return debug; }
static bool isUsbLoggingPacketStream() { return packet_stream; }
@DEBUG_GATE@
class NullStream : public Stream {
public:
size_t write(const uint8_t*, size_t size) override { return size; }
};
static NullStream null_usb_logging_stream;
static WholeRecordNonBlockingStream<640> nonblocking_primary_usb_logging_port(Serial);
static WholeRecordNonBlockingStream<640> nonblocking_dedicated_usb_logging_port(dedicated);
@LOGGING_PORT@
@DEBUG_PORT@
}
@HOOK@
extern "C" {
@CORE@
}
static void clear_output() {
Serial.bytes = Serial.calls = Serial1.bytes = Serial1.calls = 0;
dedicated.bytes = dedicated.calls = rtt_bytes = 0;
}
static const char flash_event[] = "[SOC ] NRF_EVT_FLASH_OPERATION_SUCCESS\r\n";
static void emit_contact_save_and_delete_events() {
static_assert(sizeof(flash_event) - 1 == 42, "Real captured event length");
for (unsigned i = 0; i < 48; ++i) {
assert(_write(1, flash_event, sizeof(flash_event) - 1) == 42);
}
}
int main() {
emit_contact_save_and_delete_events();
#if CFG_LOGGER == 2 || CFG_SYSVIEW
assert(rtt_bytes == 2016 && Serial.bytes == 0 && dedicated.bytes == 0);
#elif CFG_LOGGER == 1
assert(Serial1.bytes == 2016 && Serial.bytes == 0 && dedicated.bytes == 0);
#elif !PATCHED || !WITH_HOOK
// Reproduce the complete 2016-byte corruption seen on the RAK4631.
assert(Serial.bytes == 2016 && dedicated.bytes == 0);
#else
#if defined(MESH_DUAL_CDC_LOGGING)
assert(Serial.bytes == 0 && dedicated.bytes == 2016);
#else
// A logging-only/ASCII single-CDC role retains its selected primary route.
assert(Serial.bytes == 2016 && dedicated.bytes == 0);
#endif
clear_output();
mesh::debug = false;
emit_contact_save_and_delete_events();
assert(Serial.bytes == 0 && dedicated.bytes == 0);
mesh::debug = true;
mesh::logging = false;
emit_contact_save_and_delete_events();
assert(Serial.bytes == 0 && dedicated.bytes == 0);
mesh::logging = true;
in_isr = true;
emit_contact_save_and_delete_events();
assert(Serial.bytes == 0 && dedicated.bytes == 0);
in_isr = false;
Serial.capacity = dedicated.capacity = 0;
emit_contact_save_and_delete_events();
assert(Serial.calls == 0 && dedicated.calls == 0); // No retry/wait on a full FIFO.
Serial.capacity = dedicated.capacity = 4096;
#if defined(MESH_DUAL_CDC_LOGGING)
mesh::dedicated_usb_logging_port_connected.store(false);
emit_contact_save_and_delete_events();
assert(Serial.bytes == 0 && dedicated.bytes == 0); // Never fall back to CDC0.
mesh::dedicated_usb_logging_port_connected.store(true);
mesh::packet_stream = true;
emit_contact_save_and_delete_events();
assert(Serial.bytes == 2016 && dedicated.bytes == 0); // Explicit primary logging.
clear_output();
mesh::packet_stream = false;
#endif
// Dropped records report consumed; later enabling debug remains usable.
emit_contact_save_and_delete_events();
#if defined(MESH_DUAL_CDC_LOGGING)
assert(Serial.bytes == 0 && dedicated.bytes == 2016);
#else
assert(Serial.bytes == 2016 && dedicated.bytes == 0);
#endif
assert(_write(1, nullptr, 42) == 0);
assert(_write(1, flash_event, 0) == 0);
#endif
}
'''
class Nrf52FrameworkLoggingTest(unittest.TestCase):
def test_retarget_is_idempotent_and_rejects_changed_or_partial_core(self):
patched = FIX.patched_logging_source(CORE_LOGGER.replace("\n", "\r\n"))
self.assertEqual(FIX.patched_logging_source(patched), patched)
for source in (
CORE_LOGGER.replace("ret = Serial.write", "ret = another_writer"),
CORE_LOGGER.replace(FIX.OLD_LOGGER, FIX.LOGGER),
FIX.LOGGER_DECLARATION + CORE_LOGGER,
CORE_LOGGER + CORE_LOGGER,
):
with self.subTest(source=source), self.assertRaises(RuntimeError):
FIX.patched_logging_source(source)
def test_existing_private_core_copy_keeps_both_policies(self):
source = (FIX.LOOP_STACK + "\n" + FIX.OLD_LOOP_CREATE + "\n" + CORE_LOGGER)
patched = FIX.patched_logging_source(FIX.patched_source(source))
self.assertIn(FIX.LOOP_CREATE, patched)
self.assertIn(FIX.LOGGER, patched)
self.assertEqual(FIX.patched_source(patched), patched)
self.assertEqual(FIX.patched_logging_source(patched), patched)
script = (ROOT / "scripts/nrf52_loop_stack_fix.py").read_text()
self.assertIn("patched_logging_source(patched_source(", script)
@unittest.skipUnless(shutil.which("c++"), "native C++ compiler unavailable")
def test_flash_event_corruption_then_routing_gates_and_bounded_output(self):
source = (ROOT / "src/helpers/UsbLogging.cpp").read_text()
header = (ROOT / "src/helpers/UsbLogging.h").read_text()
parts = {
"DEBUG_GATE": extract_braced(header, "inline bool isUsbDebugLoggingEnabled("),
"LOGGING_PORT": extract_braced(source, "Stream& usbLoggingPort("),
"DEBUG_PORT": extract_braced(source, "Stream& usbDebugPort("),
}
hook = extract_braced(source, 'extern "C" size_t meshNrf52FrameworkDebugWrite(')
# Keep the hook architecture-scoped; unrelated ESP output is unchanged.
self.assertIn('#if defined(NRF52_PLATFORM)\n// The framework', source)
with tempfile.TemporaryDirectory(prefix="meshcore-framework-log-") as temp:
work = Path(temp)
(work / "Arduino.h").write_text(ARDUINO)
for patched, with_hook, dual, logger, sysview in (
(False, True, True, 0, 0),
(True, True, True, 0, 0),
(True, True, False, 0, 0),
(True, False, True, 0, 0),
(True, True, True, 1, 0),
(True, True, True, 2, 0),
(True, True, True, 0, 1),
):
with self.subTest(patched=patched, hook=with_hook, dual=dual,
logger=logger, sysview=sysview):
generated = RUNTIME
for key, value in parts.items():
generated = generated.replace("@" + key + "@", value)
generated = generated.replace("@HOOK@", hook if with_hook else "")
generated = generated.replace("@CORE@", FIX.patched_logging_source(CORE_LOGGER)
if patched else CORE_LOGGER)
(work / "main.cpp").write_text(generated)
flags = [f"-DPATCHED={int(patched)}", f"-DWITH_HOOK={int(with_hook)}",
f"-DCFG_LOGGER={logger}", f"-DCFG_SYSVIEW={sysview}",
"-DNRF52_PLATFORM=1"]
if dual:
flags.append("-DMESH_DUAL_CDC_LOGGING=1")
built = subprocess.run([
"c++", "-std=c++17", "-O2", "-flto", "-flto-partition=one",
"-fno-semantic-interposition", "-I" + str(work),
"-I" + str(ROOT / "src"), *flags, str(work / "main.cpp"),
"-o", str(work / "test"),
], text=True, capture_output=True)
self.assertEqual(built.returncode, 0, built.stderr)
run = subprocess.run([str(work / "test")], text=True, capture_output=True, timeout=5)
self.assertEqual(run.returncode, 0, run.stderr)
if __name__ == "__main__":
unittest.main()