Files
MeshCore/test/test_mqtt_diagnostics.py

166 lines
5.8 KiB
Python

#!/usr/bin/env python3
"""Exercise production MQTT diagnostics with distinct internal and DMA heaps."""
from pathlib import Path
import shutil
import subprocess
import tempfile
import unittest
ROOT = Path(__file__).resolve().parents[1]
def method(signature):
source = (ROOT / "src/helpers/bridges/MQTTBridge.cpp").read_text(encoding="utf-8")
start = source.index(signature)
opening = source.index("{", start)
depth = 1
pos = opening + 1
while depth:
depth += (source[pos] == "{") - (source[pos] == "}")
pos += 1
return source[start:pos] + "\n"
PREAMBLE = r'''
#include <cassert>
#include <cstdint>
#include <cstdarg>
#include <cstdio>
#include <cstring>
#include "helpers/MQTTReplyFormat.h"
#include "helpers/MQTTConnectionHealth.h"
#include "helpers/bridges/MQTTErrorLabels.h"
constexpr int RUNTIME_MQTT_SLOTS = 5;
constexpr unsigned MALLOC_CAP_DMA = 1U << 3;
constexpr unsigned MALLOC_CAP_INTERNAL = 1U << 11;
static unsigned dma_queries = 0;
static size_t dma_free = 8128, dma_largest = 1536;
size_t heap_caps_get_free_size(unsigned caps) {
assert(caps == MALLOC_CAP_DMA);
++dma_queries;
return dma_free;
}
size_t heap_caps_get_largest_free_block(unsigned caps) {
assert(caps == MALLOC_CAP_DMA);
++dma_queries;
return dma_largest;
}
struct {
int getFreeHeap() const { return 64000; }
int getMaxAllocHeap() const { return 30000; }
} ESP;
unsigned uxQueueMessagesWaiting(void*) { return 3; }
static char debug_line[512];
void capture_debug(const char* format, ...) {
va_list args;
va_start(args, format);
vsnprintf(debug_line, sizeof(debug_line), format, args);
va_end(args);
}
#define MQTT_DEBUG_PRINTLN(...) capture_debug(__VA_ARGS__)
struct MQTTPrefs {};
struct Client {
unsigned long getOutboxSize() const { return 0; }
unsigned long getOutboxDrops() const { return 0; }
unsigned long getPublishOk() const { return 123; }
unsigned long getPublishErr() const { return 4; }
};
struct MQTTBridge {
struct Slot {
Client* client = nullptr;
bool enabled = true, connected = true;
};
Slot _slots[RUNTIME_MQTT_SLOTS];
bool _initialized = true;
void* _packet_queue_handle = reinterpret_cast<void*>(1);
int _queue_count = 3, _skipped_publishes = 0;
unsigned long _filtered_packets = 0;
static constexpr int MAX_QUEUE_SIZE = 50;
bool isSlotReady(int) const { return true; }
static void formatMqttStatsReply(char*, size_t);
static const char* tlsErrorStr(int32_t);
void logMemoryStatus();
};
static MQTTBridge* s_mqtt_bridge_instance = nullptr;
'''
MAIN = r'''
int main() {
// Use the public production method, not the pure table alone. Unknown SDK
// codes retain raw-number formatting, and write failures stay distinct.
assert(strcmp(MQTTBridge::tlsErrorStr(0x8006), "connection timeout") == 0);
assert(strcmp(MQTTBridge::tlsErrorStr(0x8008), "server closed connection") == 0);
assert(strcmp(MQTTBridge::tlsErrorStr(0x8018), "TLS write failed") == 0);
assert(MQTTBridge::tlsErrorStr(0x800B) == nullptr);
char reply[160];
MQTTBridge::formatMqttStatsReply(reply, sizeof(reply));
assert(strcmp(reply, "> (bridge not running)") == 0);
assert(dma_queries == 0);
MQTTBridge bridge;
Client clients[RUNTIME_MQTT_SLOTS];
for (int i = 0; i < RUNTIME_MQTT_SLOTS; ++i) bridge._slots[i].client = &clients[i];
s_mqtt_bridge_instance = &bridge;
MQTTBridge::formatMqttStatsReply(reply, sizeof(reply));
assert(strstr(reply, "Free=64000 Max=30000") != nullptr);
#ifdef ESP_PLATFORM
// A large general internal block must not mask the smaller AES DMA heap.
assert(strstr(reply, "DMA=8128/1536") != nullptr);
assert(dma_queries == 2);
#else
assert(strstr(reply, "DMA=") == nullptr);
assert(dma_queries == 0);
#endif
// Normal five-slot stats still fit the fixed LoRa reply budget.
assert(strstr(reply, "s5=123/4") != nullptr);
bridge.logMemoryStatus();
#ifdef ESP_PLATFORM
assert(strstr(debug_line, "DMA=8128/1536") != nullptr);
#else
assert(strstr(debug_line, "DMA=") == nullptr);
#endif
// Large counters and new heap fields never write beyond a short reply.
struct { char reply[32]; unsigned char guard[8]; } bounded;
memset(&bounded, 0xA5, sizeof(bounded));
bridge._filtered_packets = 4294967295UL;
dma_free = 4294967295UL;
dma_largest = 4294967295UL;
MQTTBridge::formatMqttStatsReply(bounded.reply, sizeof(bounded.reply));
assert(bounded.reply[sizeof(bounded.reply) - 1] == '\0');
for (unsigned char byte : bounded.guard) assert(byte == 0xA5);
MQTTBridge::formatMqttStatsReply(nullptr, 32);
MQTTBridge::formatMqttStatsReply(bounded.reply, 0);
}
'''
class MqttDiagnosticsTests(unittest.TestCase):
def test_production_stats_and_labels(self):
compiler = shutil.which("g++")
if not compiler:
self.skipTest("g++ is unavailable")
source = PREAMBLE + "\n".join(method(signature) for signature in (
"void MQTTBridge::formatMqttStatsReply(",
"const char* MQTTBridge::tlsErrorStr(",
"void MQTTBridge::logMemoryStatus(",
)) + MAIN
with tempfile.TemporaryDirectory(prefix="meshcore-mqtt-diagnostics-") as directory:
test_source = Path(directory) / "diagnostics.cpp"
test_source.write_text(source, encoding="ascii")
for esp in (False, True):
with self.subTest(esp=esp):
executable = Path(directory) / ("esp" if esp else "other")
command = [compiler, "-std=c++17", "-Wall", "-Wextra", "-Werror",
"-I", str(ROOT / "src"), str(test_source), "-o", str(executable)]
if esp:
command.insert(1, "-DESP_PLATFORM=1")
subprocess.run(command, check=True, capture_output=True, text=True)
subprocess.run([str(executable)], check=True, capture_output=True, text=True)
if __name__ == "__main__":
unittest.main()