Files
MeshCore/test/test_mini_repeater_logging.py
T

576 lines
27 KiB
Python

#!/usr/bin/env python3
"""Keep constrained Mini Repeater builds compatible with the MQTT packet log.
Compile the production repeater RX method, Dispatcher TX method, packet wire
codec and bounded serial writer under all eight feature-gate combinations.
Hardware transport/radio are stand-ins; this does not qualify physical USB/RF.
"""
import configparser
import itertools
import os
from pathlib import Path
import re
import shutil
import subprocess
import sys
import tempfile
import unittest
from cpp_source import body
ROOT = Path(__file__).resolve().parents[1]
HARNESS = r'''
#include <Arduino.h>
inline uint32_t micros() { return millis() * 1000U; }
#include <Packet.h>
#include <helpers/CarrierTools.h>
#include <helpers/ManagementFeatureConfig.h>
#include <helpers/PacketFileLoggingConfig.h>
#include <helpers/SerialPacketLog.h>
#include "@PRODUCTION_UTILS@"
#define MAX_ROUTE_HASH_BYTES @ROUTE_HASH_LIMIT@
static const uint8_t FLOOD_RETRY_BRIDGE_OTHER_BUCKET = @BRIDGE_BUCKET_COUNT@;
@BUCKET_MASK_METHOD@
#include <algorithm>
#include <cassert>
#include <cstdarg>
#include <cstdio>
#include <cstring>
#include <limits>
#include <string>
#include <vector>
static_assert(MESH_PACKET_LOGGING == 1, "packet logging must remain compiled");
static_assert(MESH_PACKET_LOGGING_COMPACT == 1, "retain compact serial protocol");
static_assert(MESH_ENABLE_CARRIER_TOOLS == EXPECT_CARRIER, "exercise actual flag");
static_assert(MESH_ENABLE_MANAGEMENT_REPORTING == EXPECT_MANAGEMENT, "exercise actual flag");
static_assert(MESH_ENABLE_PACKET_FILE_LOGGING == EXPECT_FILE_LOGGING, "exercise actual flag");
struct Port : Stream {
std::string output;
bool stalled = false;
size_t chunk = 7, wedge_after = std::numeric_limits<size_t>::max();
int availableForWrite() override {
return stalled || output.size() >= wedge_after ? 0 : int(chunk);
}
size_t write(const uint8_t* data, size_t size) override {
if (!availableForWrite()) return 0;
size = std::min(size, chunk);
output.append(reinterpret_cast<const char*>(data), size);
return size;
}
} log_port;
namespace mesh {
bool logging_enabled = true;
bool isUsbLoggingEnabled() { return logging_enabled; }
Stream& usbLoggingPort() { return log_port; }
void Utils::toHex(char* destination, const uint8_t* source, size_t size) {
const char* digits = "0123456789ABCDEF";
while (size--) {
const uint8_t value = *source++;
*destination++ = digits[value >> 4];
*destination++ = digits[value & 15];
}
*destination = 0;
}
@PACKET_METHODS@
enum class RadioParamApplyResult { APPLIED, BUSY };
struct Profile { uint8_t cr = 5; };
struct Profiles {
Profile value;
const Profile& params(uint8_t) const { return value; }
};
struct Radio {
bool send_ok = true;
unsigned sends = 0;
std::vector<uint8_t> raw;
Profiles settings;
RadioParamApplyResult prepareTransmitProfile(uint8_t, bool) {
return RadioParamApplyResult::APPLIED;
}
Profiles* profiles() { return &settings; }
uint32_t getEstAirtimeFor(int) { return 10; }
bool setCodingRate(uint8_t) { return true; }
bool startSendRaw(const uint8_t* bytes, int length) {
++sends;
if (send_ok) raw.assign(bytes, bytes + length);
return send_ok;
}
float getLastSNR() const { return 2.5f; }
float getLastRSSI() const { return -92; }
};
struct Clock { uint32_t getMillis() const { return millis(); } };
struct Dispatcher {
Packet* outbound = nullptr;
Radio radio;
Radio* _radio = &radio;
Clock clock;
Clock* _ms = &clock;
uint8_t outbound_restore_cr = 0;
uint32_t outbound_start = 0, outbound_expiry = 0;
bool restoreOutboundTxOverrides() { return true; }
bool isPacketRadioCurrent(const Packet*) const { return true; }
uint8_t getDefaultTxCodingRate() const { return 5; }
uint32_t futureMillis(uint32_t wait) const { return millis() + wait; }
const char* getLogDateTime() const { return "unused compact timestamp"; }
bool startOutboundTransmit();
};
@TX_METHOD@
} // namespace mesh
#define PACKET_LOG_FILE "/packet_log"
struct FakeFS {
unsigned opens = 0, writes = 0, removes = 0;
std::string output;
void remove(const char* name) {
assert(!std::strcmp(name, PACKET_LOG_FILE));
++removes;
}
};
struct File {
FakeFS* fs;
explicit operator bool() const { return true; }
void print(const char* text) { ++fs->writes; fs->output += text; }
void printf(const char* format, ...) {
char text[1024];
va_list args;
va_start(args, format);
const int length = std::vsnprintf(text, sizeof(text), format, args);
va_end(args);
assert(length >= 0 && size_t(length) < sizeof(text));
++fs->writes;
fs->output += text;
}
void close() {}
};
struct Callbacks {
virtual void setLoggingOn(bool enable) = 0;
virtual void eraseLogFile() = 0;
};
inline uint8_t getRetryLogCodingRate(const mesh::Packet*, uint8_t fallback) { return fallback; }
inline uint16_t rxPowerSavingPreambleForParams(uint8_t, float) { return 16; }
unsigned direct_format_calls = 0;
void formatDirectRetryTarget(char* output, size_t size, const uint8_t*, uint8_t) {
++direct_format_calls;
std::snprintf(output, size, "01");
}
struct MyMesh : Callbacks {
struct { bool flood_retry_bridge_enabled = false; } _prefs;
struct FloodRetryBridgeState {
uint8_t source_mask = 0, target_mask = 0, progress_marker = 0;
};
mutable FloodRetryBridgeState retry_state;
bool has_retry_state = false;
bool _logging = false;
FakeFS fs;
FakeFS* _fs = &fs;
mesh::Radio radio;
mesh::Radio* _radio = &radio;
unsigned refreshed = 0;
uint8_t active_sf = 7;
float active_bw = 125;
File openAppend(const char* name) {
assert(!std::strcmp(name, PACKET_LOG_FILE));
++fs.opens;
return File{&fs};
}
const char* getLogDateTime() const { return "timestamp"; }
uint8_t getDefaultTxCodingRate() const { return 5; }
void formatFloodRetryPath(char* output, size_t size, const mesh::Packet*) {
std::snprintf(output, size, "01");
}
bool formatFloodRetryHeard(char* output, size_t size, const mesh::Packet* packet) const;
FloodRetryBridgeState* floodRetryBridgeStateFor(const mesh::Packet*, bool) const {
return has_retry_state ? &retry_state : nullptr;
}
uint8_t floodRetryBucketMaskForPathHop(const uint8_t* prefix, uint8_t,
uint8_t, uint8_t) const { return *prefix; }
uint8_t floodRetryBridgeHeardMask(const mesh::Packet* packet,
uint8_t source_mask, uint8_t progress_marker) const;
bool hasFloodRetryHeard(const mesh::Packet* packet) const;
void refreshFloodRetryReachability(const mesh::Packet*) { ++refreshed; }
@INLINE_CALLBACKS@
void logRxRaw(float snr, float rssi, const uint8_t raw[], int len);
void logRx(mesh::Packet* pkt, int len, float score);
void logTx(mesh::Packet* pkt, int len);
void logTxFail(mesh::Packet* pkt, int len);
void onFloodRetryEvent(const char* event, const mesh::Packet* packet,
uint32_t delay_millis, uint8_t retry_attempt);
void onDirectRetryEvent(const char* event, const mesh::Packet* packet,
uint32_t delay_millis, uint8_t retry_attempt,
const uint8_t* target_hash, uint8_t target_hash_len,
int16_t payload_type);
};
@RX_METHOD@
@FILE_METHODS@
static void reset() {
log_port = Port{};
mesh::logging_enabled = true;
mesh::serialLogDroppedCount() = 0;
mesh::serialLogPortSeen() = false;
g_mock_millis = 100;
}
static std::string hex(const uint8_t* bytes, size_t size) {
const char* digits = "0123456789ABCDEF";
std::string output;
for (size_t i = 0; i < size; ++i) {
output += digits[bytes[i] >> 4];
output += digits[bytes[i] & 15];
}
return output;
}
static mesh::Packet makePacket(bool maximum = false) {
mesh::Packet packet;
packet.header = (PAYLOAD_TYPE_GRP_DATA << PH_TYPE_SHIFT) | ROUTE_TYPE_TRANSPORT_DIRECT;
packet.transport_codes[0] = 0x1234;
packet.transport_codes[1] = 0xABCD;
packet.setPathHashSizeAndCount(2, maximum ? 32 : 2);
for (unsigned i = 0; i < packet.getPathByteLen(); ++i) packet.path[i] = uint8_t(i);
packet.payload_len = maximum ? MAX_PACKET_PAYLOAD : 3;
for (unsigned i = 0; i < packet.payload_len; ++i) packet.payload[i] = uint8_t(i ^ 0xAB);
return packet;
}
int main() {
MyMesh direct_diagnostics;
direct_diagnostics.setLoggingOn(true);
direct_diagnostics.onDirectRetryEvent("good", nullptr, 12, 1, nullptr, 0, 3);
assert(direct_format_calls == unsigned(EXPECT_FILE_LOGGING));
assert(direct_diagnostics.fs.opens == unsigned(EXPECT_FILE_LOGGING));
if (EXPECT_FILE_LOGGING)
assert(direct_diagnostics.fs.output.find(": direct retry good") != std::string::npos);
MyMesh repeater;
// Actual inline callbacks and persistent packet emitters remain independent
// of live USB logging. The file-off control cannot be re-enabled at runtime.
assert(!repeater._logging);
repeater.setLoggingOn(true);
assert(repeater._logging == bool(EXPECT_FILE_LOGGING));
mesh::Packet stored = makePacket();
repeater.logRx(&stored, 42, 0.25f);
repeater.logTx(&stored, 42);
repeater.logTxFail(&stored, 42);
repeater.onFloodRetryEvent("good", &stored, 12, 1);
assert(repeater.refreshed == 1); // Reachability is not a logging side effect.
repeater.onFloodRetryEvent("good", nullptr, 12, 1);
repeater.onFloodRetryEvent("failure", &stored, 12, 1);
assert(repeater.refreshed == 1);
repeater.eraseLogFile();
if (EXPECT_FILE_LOGGING) {
assert(repeater.fs.opens == 5 && repeater.fs.writes > 0 && repeater.fs.removes == 1);
for (const char* marker : {": RX,", ": TX,", ": TX FAIL!", ": FLOOD RETRY good"})
assert(repeater.fs.output.find(marker) != std::string::npos);
} else {
assert(repeater.fs.opens == 0 && repeater.fs.writes == 0 && repeater.fs.removes == 0);
assert(repeater.fs.output.empty());
}
repeater.setLoggingOn(false);
assert(!repeater._logging);
const unsigned old_opens = repeater.fs.opens;
repeater.logRx(&stored, 42, 0.25f);
repeater.logTx(&stored, 42);
repeater.logTxFail(&stored, 42);
repeater.onFloodRetryEvent("good", &stored, 12, 1);
assert(repeater.refreshed == 2 && repeater.fs.opens == old_opens);
// The unlogged path must make the same routing decision as the textual
// formatter: a real target echo, excluding source and progress-marker hops.
MyMesh routing;
mesh::Packet heard = makePacket();
heard.header = (PAYLOAD_TYPE_GRP_DATA << PH_TYPE_SHIFT) | ROUTE_TYPE_FLOOD;
heard.setPathHashSizeAndCount(1, 3);
heard.path[0] = 1; heard.path[1] = 2; heard.path[2] = 4;
assert(routing.hasFloodRetryHeard(&heard));
assert(!routing.hasFloodRetryHeard(nullptr));
const uint8_t valid_path = heard.path_len;
heard.path_len = 0;
assert(!routing.hasFloodRetryHeard(&heard));
heard.path_len = 0xC1; // Reserved four-byte route-hash mode.
assert(!routing.hasFloodRetryHeard(&heard));
heard.path_len = valid_path;
routing._prefs.flood_retry_bridge_enabled = true;
assert(!routing.hasFloodRetryHeard(&heard)); // No pending bridge retry.
routing.has_retry_state = true;
routing.retry_state = {1, 4, 2};
assert(routing.hasFloodRetryHeard(&heard));
routing.onFloodRetryEvent("good", &heard, 12, 1);
assert(routing.refreshed == 1);
routing.retry_state.target_mask = 2; // Progress marker is not an echo.
assert(!routing.hasFloodRetryHeard(&heard));
routing.onFloodRetryEvent("good", &heard, 12, 1);
assert(routing.refreshed == 1);
routing.retry_state.target_mask = 1; // The source is not a target echo.
assert(!routing.hasFloodRetryHeard(&heard));
routing.retry_state = {0, 2, 0};
assert(routing.hasFloodRetryHeard(&heard));
routing.onFloodRetryEvent("queued", &heard, 12, 1);
routing.onFloodRetryEvent("failure", &heard, 12, 1);
assert(routing.refreshed == 1);
routing.onFloodRetryEvent("good", &heard, 12, 1);
assert(routing.refreshed == 2);
// Compare the production eligibility check with the production formatter's
// success result using its actual 96-byte caller buffer, including every
// source/target/progress combination for representative route-hash sizes.
for (unsigned hash_size = 1; hash_size <= MAX_ROUTE_HASH_BYTES; ++hash_size) {
heard.setPathHashSizeAndCount(hash_size, 3);
memset(heard.path, 0, sizeof(heard.path));
heard.path[0] = 1;
heard.path[hash_size] = 2;
heard.path[hash_size * 2] = uint8_t(1U << FLOOD_RETRY_BRIDGE_OTHER_BUCKET);
for (unsigned source = 0; source < 8; ++source)
for (unsigned target : {0U, 1U, 2U, 3U, 64U, 127U})
for (unsigned progress = 0; progress <= 3; ++progress) {
routing.retry_state = {uint8_t(source), uint8_t(target), uint8_t(progress)};
char formatted[96];
assert(routing.hasFloodRetryHeard(&heard)
== routing.formatFloodRetryHeard(formatted, sizeof(formatted), &heard));
}
}
const uint8_t raw[] = {0x00, 0xAB, 0xFF};
reset();
repeater.logRxRaw(2.5f, -92, raw, sizeof(raw));
assert(log_port.output == "R00ABFF\n");
assert(g_mock_millis == 100 && mesh::serialLogDroppedCount() == 0);
uint8_t maximum_rx[MAX_TRANS_UNIT];
for (unsigned i = 0; i < sizeof(maximum_rx); ++i) maximum_rx[i] = uint8_t(i);
reset();
repeater.logRxRaw(0, -100, maximum_rx, sizeof(maximum_rx));
assert(log_port.output == "R" + hex(maximum_rx, sizeof(maximum_rx)) + "\n");
assert(log_port.output.size() == 2 + MAX_TRANS_UNIT * 2);
for (const bool maximum : {false, true}) {
mesh::Packet packet = makePacket(maximum);
uint8_t wire[MAX_TRANS_UNIT];
const size_t length = packet.writeTo(wire);
assert(length == size_t(packet.getRawLength()));
if (maximum) assert(length == 254);
// Exercise real decoding/bounds too: feature gates must not alter packet
// framing, transport codes, path bytes, payload or accepted wire lengths.
mesh::Packet decoded;
assert(decoded.readFrom(wire, uint8_t(length)));
assert(decoded.header == packet.header && decoded.path_len == packet.path_len);
assert(decoded.payload_len == packet.payload_len);
assert(!std::memcmp(decoded.payload, packet.payload, packet.payload_len));
assert(!decoded.readFrom(wire, 1));
auto invalid = std::vector<uint8_t>(wire, wire + length);
invalid[5] = 0xC1; // reserved four-byte path mode
assert(!decoded.readFrom(invalid.data(), uint8_t(invalid.size())));
reset();
mesh::Dispatcher dispatcher;
dispatcher.outbound = &packet;
assert(dispatcher.startOutboundTransmit());
assert(dispatcher.radio.raw == std::vector<uint8_t>(wire, wire + length));
assert(log_port.output == "T" + hex(wire, length) + "\n");
assert(mesh::serialLogDroppedCount() == 0);
reset();
mesh::logging_enabled = false;
repeater.logRxRaw(0, -100, maximum_rx, sizeof(maximum_rx));
assert(dispatcher.startOutboundTransmit());
assert(log_port.output.empty());
assert(dispatcher.radio.raw == std::vector<uint8_t>(wire, wire + length));
reset();
dispatcher.radio.send_ok = false;
assert(!dispatcher.startOutboundTransmit());
assert(log_port.output.empty()); // Never claim a TX which did not start.
}
// An initially undrained host must cost neither time nor a false drop.
reset();
log_port.stalled = true;
repeater.logRxRaw(0, -100, raw, sizeof(raw));
assert(log_port.output.empty() && g_mock_millis == 100);
assert(mesh::serialLogDroppedCount() == 0);
// Once a host was observed, refused RX and TX records count as lost. RF
// transmission still proceeds; recovery exposes the existing DROP marker.
reset();
repeater.logRxRaw(0, -100, raw, sizeof(raw));
log_port.output.clear();
log_port.stalled = true;
repeater.logRxRaw(0, -100, raw, sizeof(raw));
mesh::Packet packet = makePacket();
mesh::Dispatcher dispatcher;
dispatcher.outbound = &packet;
assert(dispatcher.startOutboundTransmit() && dispatcher.radio.sends == 1);
assert(log_port.output.empty() && g_mock_millis == 100);
assert(mesh::serialLogDroppedCount() == 2);
log_port.stalled = false;
repeater.logRxRaw(0, -100, raw, sizeof(raw));
assert(log_port.output == "DROP:2\r\nR00ABFF\n");
// Keep the bounded writer's existing mid-record stall behavior explicit.
reset();
mesh::serialLogPortSeen() = true;
log_port.wedge_after = 14;
repeater.logRxRaw(0, -100, maximum_rx, sizeof(maximum_rx));
assert(log_port.output.size() == 14);
assert(g_mock_millis - 100 <= SERIAL_LOG_WRITE_BUDGET_MS + 1);
assert(mesh::serialLogDroppedCount() == 1);
std::puts("PASS: production compact repeater RX/TX logging survives all feature gates");
}
'''
class MiniRepeaterLoggingTest(unittest.TestCase):
def test_actual_raw_rx_tx_packet_codec_and_backpressure_with_all_gate_combinations(self):
compiler = os.environ.get("CXX") or shutil.which("g++") or shutil.which("clang++")
self.assertIsNotNone(compiler, "a host C++17 compiler is required")
packet = (ROOT / "src/Packet.cpp").read_text()
packet_methods = "\n".join(body(packet, signature) for signature in (
"Packet::Packet()", "bool Packet::isValidPathLen(",
"size_t Packet::writePath(", "int Packet::getRawLength() const",
"uint8_t Packet::writeTo(", "bool Packet::readFrom("))
dispatcher = (ROOT / "src/Dispatcher.cpp").read_text()
repeater = (ROOT / "examples/simple_repeater/MyMesh.cpp").read_text()
route_hash_limit = re.search(r"^#define MAX_ROUTE_HASH_BYTES\s+(\d+)\s*$",
(ROOT / "src/helpers/SimpleMeshTables.h").read_text(), re.MULTILINE)
self.assertIsNotNone(route_hash_limit)
bridge_bucket_count = re.search(r"^\s*#define FLOOD_RETRY_BRIDGE_BUCKETS\s+(\d+)\s*$",
(ROOT / "src/helpers/CommonCLI.h").read_text(), re.MULTILINE)
self.assertIsNotNone(bridge_bucket_count)
program = HARNESS.replace("@ROUTE_HASH_LIMIT@", route_hash_limit.group(1))
program = program.replace("@PRODUCTION_UTILS@", str(ROOT / "src/Utils.h"))
program = program.replace("@BRIDGE_BUCKET_COUNT@", bridge_bucket_count.group(1))
program = program.replace("@BUCKET_MASK_METHOD@",
body(repeater, "static uint8_t floodRetryBucketMask("))
program = program.replace("@PACKET_METHODS@", packet_methods).replace(
"@TX_METHOD@", body(dispatcher, "bool Dispatcher::startOutboundTransmit()"))
program = program.replace("@RX_METHOD@", body(repeater, "void MyMesh::logRxRaw("))
header = (ROOT / "examples/simple_repeater/MyMesh.h").read_text()
program = program.replace("@INLINE_CALLBACKS@", "\n".join(body(header, signature) for signature in (
"void setLoggingOn(", "void eraseLogFile()")))
program = program.replace("@FILE_METHODS@", "\n".join(body(repeater, signature) for signature in (
"void MyMesh::logRx(", "void MyMesh::logTx(", "void MyMesh::logTxFail(",
"void MyMesh::onFloodRetryEvent(", "bool MyMesh::hasFloodRetryHeard(",
"uint8_t MyMesh::floodRetryBridgeHeardMask(", "bool MyMesh::formatFloodRetryHeard(",
"void MyMesh::onDirectRetryEvent(")))
sanitizers = (["-fsanitize=address,undefined", "-fno-sanitize-recover=all",
"-fno-pie", "-no-pie"] if sys.platform.startswith("linux") else [])
with tempfile.TemporaryDirectory(prefix="mini-experiment-logging-") as directory:
source, executable = Path(directory) / "test.cpp", Path(directory) / "test"
source.write_text(program, encoding="ascii")
for carrier, management, file_logging in itertools.product((0, 1), repeat=3):
with self.subTest(carrier_tools=carrier, management_reporting=management,
packet_file_logging=file_logging):
command = [compiler, "-std=c++17", "-O1", "-Wall", "-Wextra", "-Werror",
"-Wno-unused-parameter", "-Wno-unused-variable",
"-Wno-unused-but-set-variable", "-Wno-sign-compare", *sanitizers,
"-DARDUINO=1", "-DSTM32_PLATFORM=1", "-DMESH_DEBUG=0",
"-DMESH_PACKET_LOGGING=1", "-DMESH_PACKET_LOGGING_COMPACT=1",
f"-DMESH_ENABLE_CARRIER_TOOLS={carrier}",
f"-DMESH_ENABLE_MANAGEMENT_REPORTING={management}",
f"-DMESH_ENABLE_PACKET_FILE_LOGGING={file_logging}",
f"-DEXPECT_CARRIER={carrier}", f"-DEXPECT_MANAGEMENT={management}",
f"-DEXPECT_FILE_LOGGING={file_logging}",
"-I", str(ROOT / "test/mocks"), "-I", str(ROOT / "src"),
str(source), "-o", str(executable)]
built = subprocess.run(command, capture_output=True, text=True, timeout=60)
self.assertEqual(built.returncode, 0, built.stdout + built.stderr)
tested = subprocess.run([str(executable)], capture_output=True, text=True, timeout=15)
self.assertEqual(tested.returncode, 0, tested.stdout + tested.stderr)
self.assertIn("PASS: production compact repeater RX/TX", tested.stdout)
def test_timestamp_override_is_retained_for_every_actual_consumer(self):
compiler = os.environ.get("CXX") or shutil.which("g++") or shutil.which("clang++")
self.assertIsNotNone(compiler, "a host C++17 compiler is required")
header = (ROOT / "examples/simple_repeater/MyMesh.h").read_text()
source = (ROOT / "examples/simple_repeater/MyMesh.cpp").read_text()
def guarded(text, signature):
start = text.index(signature)
guard = text.rfind("#if ", 0, start)
end = text.index("#endif", start) + len("#endif")
self.assertGreaterEqual(guard, 0)
return text[guard:end]
declaration = guarded(header, "const char* getLogDateTime() override;")
definition = guarded(source, "const char *MyMesh::getLogDateTime()")
blocks, checks = ["#include <cassert>\n#include <cstdint>\n#include <cstdio>\n#include <cstring>"], []
for index, (debug, files, packets, compact) in enumerate(itertools.product((0, 1), repeat=4)):
flags = dict(MESH_DEBUG=debug, MESH_ENABLE_PACKET_FILE_LOGGING=files,
MESH_PACKET_LOGGING=packets, MESH_PACKET_LOGGING_COMPACT=compact)
for name, value in flags.items():
blocks.append(f"#undef {name}\n#define {name} {value}")
blocks.append(f"namespace timestamp_variant_{index} {{")
blocks.append(r'''
struct Clock { uint32_t getCurrentTime() const { return 123; } };
struct DateTime {
explicit DateTime(uint32_t) {}
unsigned hour() const { return 3; } unsigned minute() const { return 4; }
unsigned second() const { return 5; } unsigned day() const { return 2; }
unsigned month() const { return 1; } unsigned year() const { return 2026; }
};
struct Base { virtual const char* getLogDateTime() { return "unused"; } };
struct MyMesh : Base {
Clock clock;
Clock* getRTCClock() { return &clock; }
''')
blocks.extend((declaration, "};", definition))
expected = ("03:04:05 - 2/1/2026 U" if debug or files or (packets and not compact)
else "unused")
blocks.append(f'void check() {{ MyMesh mesh; assert(!strcmp(mesh.getLogDateTime(), "{expected}")); }}\n}}')
checks.append(f"timestamp_variant_{index}::check();")
blocks.append("int main() {" + " ".join(checks) + "}")
with tempfile.TemporaryDirectory(prefix="mini-timestamp-consumers-") as directory:
path, executable = Path(directory) / "test.cpp", Path(directory) / "test"
path.write_text("\n".join(blocks), encoding="ascii")
built = subprocess.run([compiler, "-std=c++17", "-O1", "-Wall", "-Wextra",
"-Werror", str(path), "-o", str(executable)],
capture_output=True, text=True, timeout=60)
self.assertEqual(built.returncode, 0, built.stdout + built.stderr)
tested = subprocess.run([str(executable)], capture_output=True, text=True, timeout=15)
self.assertEqual(tested.returncode, 0, tested.stdout + tested.stderr)
def test_baseline_storage_logging_host_cli_and_authentication_contracts_remain(self):
config = configparser.ConfigParser(interpolation=None, inline_comment_prefixes=(";",))
config.read(ROOT / "variants/wio-e5-mini/platformio.ini")
variant = config["env:wio-e5-mini_repeater"]
self.assertEqual(variant.getint("board_upload.maximum_size"), 245760)
flags = variant["build_flags"]
self.assertRegex(flags, r"-D\s+LFS_FLASH_TOTAL_SIZE=16384\b")
self.assertRegex(flags, r"-D\s+MESH_PACKET_LOGGING_COMPACT=1\b")
self.assertNotRegex(flags, r"-D\s+MESH_ENABLE_HOST_CLI=0\b")
self.assertNotRegex(flags, r"-D\s+MESH_PACKET_LOGGING=0\b")
header = (ROOT / "examples/simple_repeater/MyMesh.h").read_text()
self.assertIn("#ifndef MESH_ENABLE_HOST_CLI\n#define MESH_ENABLE_HOST_CLI 1\n#endif", header)
file_config = (ROOT / "src/helpers/PacketFileLoggingConfig.h").read_text()
self.assertRegex(file_config, r"#ifndef MESH_ENABLE_PACKET_FILE_LOGGING\s+"
r"#define MESH_ENABLE_PACKET_FILE_LOGGING 1\s+#endif")
source = (ROOT / "examples/simple_repeater/MyMesh.cpp").read_text()
dispatch = body(source, "void MyMesh::handleCommand(uint32_t sender_timestamp, ClientInfo* sender,")
self.assertLess(dispatch.index("handleReplayResetCommand(sender, command, reply, usb_origin)"),
dispatch.index("_cli.handleCommand("))
self.assertIn("if (sender != nullptr && sender_timestamp == 0) sender_timestamp = 1;", dispatch)
receive = source[source.index("else if (type == PAYLOAD_TYPE_TXT_MSG && len > 5"):]
self.assertIn("client->isAdmin() || client->isRegionMgr() || client->isFilterMgr()", receive)
self.assertIn("sender_timestamp < client->last_timestamp && !cached_retry", receive)
host_complete = body(source, "bool MyMesh::completeHostCliRequest(")
self.assertIn("client_index < 0 || !acl.getClientByIdx(client_index)->isAdmin()", host_complete)
host_reply = body(source, "bool MyMesh::handleHostCliSerialReply(")
self.assertIn("parsed.request_id != deferred_cli_command.request_id", host_reply)
self.assertIn("parsed.request_nonce != host_cli_nonce", host_reply)
self.assertIn("else if (!host_cli_claimed)", host_reply)
deferred = body(source, "void __attribute__((noinline)) MyMesh::processDeferredCliCommand()")
self.assertIn("if (!client->isAdmin())", deferred)
self.assertIn("self_id.sign(signature, (const uint8_t*)signed_content,", deferred)
common = body((ROOT / "src/helpers/CommonCLI.cpp").read_text(), "void CommonCLI::handleCommand(")
self.assertIn("mesh::handleUsbLoggingWatchdogCommand(", common)
self.assertIn("_radio_profiles.handle(", common)
self.assertIn('#endif\n } else if (sender_timestamp == 0 && memcmp(command, "stats-packets",', common)
if __name__ == "__main__":
unittest.main()