Files
MeshCore/test/test_espnow_diagnostic_cli.py
T

241 lines
10 KiB
Python

"""Compile actual ESP-NOW diagnostic dispatch/forwarders with native fakes."""
from pathlib import Path
import re
import shutil
import subprocess
import tempfile
import unittest
from test_replay_reset_integration import extract_braced
ROOT = Path(__file__).resolve().parents[1]
class EspNowDiagnosticCliTests(unittest.TestCase):
def compile_run(self, source, flags=()):
compiler = shutil.which("g++") or shutil.which("clang++")
self.assertIsNotNone(compiler)
with tempfile.TemporaryDirectory(prefix="espnow-cli-") as directory:
source_path = Path(directory) / "main.cpp"
binary = Path(directory) / "check"
source_path.write_text('#include <helpers/CLICommandUtils.h>\n' + source, encoding="ascii")
subprocess.run([compiler, "-std=c++17", "-Wall", "-Wextra", "-Werror",
"-fsanitize=address,undefined", "-fno-omit-frame-pointer",
"-I", str(ROOT / "src"), *flags, str(source_path),
"-o", str(binary)], check=True, capture_output=True, text=True)
subprocess.run([str(binary)], check=True, capture_output=True, text=True)
def test_actual_dispatch_strict_local_actions_and_bounded_replies(self):
source = (ROOT / "src/helpers/CommonCLI.cpp").read_text(encoding="ascii")
header = (ROOT / "src/helpers/CommonCLI.h").read_text(encoding="ascii")
body = extract_braced(source, "bool CommonCLI::handleEspNowDiagnosticCommand(")
defaults = "\n".join(extract_braced(header, signature) for signature in (
"virtual void formatEspNowStatus(", "virtual void formatEspNowStats(",
"virtual bool sendEspNowTest(", "virtual bool resetEspNowStats("))
self.assertIn("if (handleEspNowDiagnosticCommand(sender_timestamp, command, reply)) return;", source)
self.compile_run(r'''
#include <cassert>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <string>
#include <helpers/bridges/ESPNowBridgeDiagnostics.h>
static uint32_t now_ms = 0x1234ABCD;
uint32_t millis() { return now_ms; }
struct Callbacks {
virtual ~Callbacks() = default;
''' + defaults + r'''
};
struct Fake : Callbacks {
int statuses = 0, stats = 0, sends = 0, resets = 0;
uint32_t nonce = 0;
size_t capacity = 0;
std::string page;
bool send_result = true, reset_result = true, long_reply = false;
void fill(char* reply, size_t cap, const char* value) {
capacity = cap;
const std::string text = long_reply ? std::string(300, 'x') : value;
snprintf(reply, cap, "%s", text.c_str());
}
void formatEspNowStatus(char* r, size_t cap) override {
++statuses; fill(r, cap, "status");
}
void formatEspNowStats(const char* p, char* r, size_t cap) override {
++stats; page = p; fill(r, cap, "stats");
}
bool sendEspNowTest(uint32_t n) override {
++sends; nonce = n; return send_result;
}
bool resetEspNowStats() override { ++resets; return reset_result; }
int count() const { return statuses + stats + sends + resets; }
};
struct CommonCLI {
Callbacks* _callbacks;
bool handleEspNowDiagnosticCommand(uint32_t, const char*, char*);
};
''' + body + r'''
int main() {
Fake callbacks;
CommonCLI cli{&callbacks};
char reply[160];
auto command = [&](const char* text, uint32_t sender = 0) {
memset(reply, 'Z', sizeof(reply));
const bool handled = cli.handleEspNowDiagnosticCommand(sender, text, reply);
assert(reply[157] == 'Z' && reply[158] == 'Z' && reply[159] == 'Z');
if (handled) assert(memchr(reply, 0, 157) != nullptr);
return handled;
};
assert(command("get espnow.status", 77));
assert(callbacks.statuses == 1 && callbacks.capacity == 157);
for (const char* page : {"", "rx", "tx", "drops", "test"}) {
const std::string text = std::string("get espnow.stats") + (*page ? " " : "") + page;
assert(command(text.c_str(), 77));
assert(callbacks.page == page && callbacks.capacity == 157);
}
callbacks.long_reply = true;
assert(command("get espnow.status"));
assert(strlen(reply) == 156);
assert(command("get espnow.stats rx"));
assert(strlen(reply) == 156);
callbacks.long_reply = false;
for (const char* text : {
"get espnow.status extra", "get espnow.statusx", "get espnow.status ",
"get espnow.statsrx", "get espnow.stats x", "get espnow.stats rx junk",
"get espnow.stats ", "get espnow.stats\trx", "get espnow.stats rx",
"espnow", "espnow testx", "espnow test ", "espnow test 1234567",
"espnow test 123456789", "espnow test 0x123456", "espnow test +1234567",
"espnow test 1234567Z", "espnow test 12345678 ", "espnow test 12345678 x",
"espnow resetstatsx", "espnow resetstats ", "espnow resetstats now"}) {
const int before = callbacks.count();
assert(command(text));
assert(strncmp(reply, "Error:", 6) == 0 && callbacks.count() == before);
}
for (const char* text : {"espnowtest", "espnowx test", "get espnow.enabled", "get repeat"}) {
const int before = callbacks.count();
assert(!command(text));
assert(callbacks.count() == before);
}
for (uint32_t remote : {1U, UINT32_MAX}) {
for (const char* text : {"espnow test", "espnow test 12345678", "espnow resetstats"}) {
const int before = callbacks.count();
assert(command(text, remote));
assert(strstr(reply, "local connection") && callbacks.count() == before);
}
}
assert(command("espnow test"));
assert(callbacks.sends == 1 && callbacks.nonce == now_ms);
assert(strcmp(reply, "OK - test queued id=1234ABCD; confirm on peer") == 0);
assert(command("espnow test 89abcDEF"));
assert(callbacks.sends == 2 && callbacks.nonce == 0x89ABCDEF);
assert(command("espnow test 00000000"));
assert(callbacks.sends == 3 && callbacks.nonce == 0);
callbacks.send_result = false;
assert(command("espnow test DEADBEEF"));
assert(callbacks.sends == 4 && strstr(reply, "not queued"));
assert(command("espnow resetstats"));
assert(callbacks.resets == 1 && strcmp(reply, "OK - ESP-NOW statistics reset") == 0);
callbacks.reset_result = false;
assert(command("espnow resetstats"));
assert(callbacks.resets == 2 && strstr(reply, "unavailable"));
Callbacks unsupported;
cli._callbacks = &unsupported;
assert(command("get espnow.status") && strstr(reply, "unavailable"));
assert(command("get espnow.stats") && strstr(reply, "unavailable"));
assert(command("espnow test") && strstr(reply, "not queued"));
assert(command("espnow resetstats") && strstr(reply, "unavailable"));
}
''')
def test_actual_role_forwarders_select_only_the_espnow_transport(self):
repeater = (ROOT / "examples/simple_repeater/MyMesh.h").read_text(encoding="ascii")
start = repeater.index("#if defined(WITH_ESPNOW_BRIDGE)\n ESPNowBridge& diagnosticEspNowBridge()")
end = repeater.index(" // Retry ESP-NOW", start)
repeater = repeater[start:end] + "\n#endif\n"
room = (ROOT / "examples/simple_room_server/MyMesh.h").read_text(encoding="ascii")
start = room.index("#if defined(WITH_BRIDGE)\n#if defined(WITH_MQTT_BRIDGE) && defined(WITH_ESPNOW_BRIDGE)\n void formatEspNowStatus(")
end = room.index("\n#endif", start) + len("\n#endif")
room = room[start:end] + "\n#endif\n"
prefix = r'''
#include <cassert>
#include <cstdint>
#include <cstdio>
#include <cstring>
struct Base {
virtual void formatEspNowStatus(char*, size_t) {}
virtual void formatEspNowStats(const char*, char*, size_t) {}
virtual bool sendEspNowTest(uint32_t) { return false; }
virtual bool resetEspNowStats() { return false; }
};
struct ESPNowBridge {
int calls = 0; uint32_t nonce = 0;
void formatStatus(char* r, size_t cap) { ++calls; snprintf(r, cap, "status"); }
void formatStats(const char* page, char* r, size_t cap) { ++calls; assert(strcmp(page,"rx")==0); snprintf(r, cap, "rx"); }
bool sendTest(uint32_t n) { ++calls; nonce = n; return true; }
void resetStats() { ++calls; }
};
struct Role : Base {
ESPNowBridge bridge, espnow_bridge;
'''
suffix = r'''
};
int main() {
Role role; char reply[157];
role.formatEspNowStatus(reply, sizeof(reply));
role.formatEspNowStats("rx", reply, sizeof(reply));
assert(role.sendEspNowTest(0xABCD));
assert(role.resetEspNowStats());
assert(role.EXPECTED.calls == 4 && role.EXPECTED.nonce == 0xABCD);
assert(role.OTHER.calls == 0);
}
'''
for macros in ((), ("WITH_RS232_BRIDGE",), ("WITH_MQTT_BRIDGE",),
("WITH_MQTT_BRIDGE", "WITH_RS232_BRIDGE")):
with self.subTest(role="repeater", macros=macros):
selected = "espnow_bridge" if macros else "bridge"
code = prefix + repeater + suffix.replace("EXPECTED", selected).replace(
"OTHER", "bridge" if macros else "espnow_bridge")
self.compile_run(code, ["-DWITH_ESPNOW_BRIDGE=1", *["-D" + m for m in macros]])
with self.subTest(role="room"):
code = prefix + room + suffix.replace("EXPECTED", "espnow_bridge").replace("OTHER", "bridge")
self.compile_run(code, ["-DWITH_BRIDGE=1", "-DWITH_MQTT_BRIDGE=1", "-DWITH_ESPNOW_BRIDGE=1"])
def test_diagnostic_virtuals_are_absent_without_espnow(self):
header = (ROOT / "src/helpers/CommonCLI.h").read_text(encoding="ascii")
start = header.index("#if defined(WITH_ESPNOW_BRIDGE)\n virtual void formatEspNowStatus(")
end = header.index("\n#endif", start) + len("\n#endif")
block = header[start:end]
source = r'''
#include <cassert>
#include <cstdint>
#include <cstdio>
#include <type_traits>
#include <utility>
struct Callbacks {
''' + block + r'''
};
template <typename T, typename = void> struct HasDiagnostics : std::false_type {};
template <typename T> struct HasDiagnostics<T, std::void_t<
decltype(std::declval<T>().formatEspNowStatus(nullptr, 0)),
decltype(std::declval<T>().formatEspNowStats(nullptr, nullptr, 0)),
decltype(std::declval<T>().sendEspNowTest(0)),
decltype(std::declval<T>().resetEspNowStats())>> : std::true_type {};
int main() {
#if defined(WITH_ESPNOW_BRIDGE)
static_assert(HasDiagnostics<Callbacks>::value);
Callbacks callbacks; char reply[157];
callbacks.formatEspNowStatus(reply, sizeof(reply));
assert(reply[0] == 'E');
assert(!callbacks.sendEspNowTest(0) && !callbacks.resetEspNowStats());
#else
static_assert(!HasDiagnostics<Callbacks>::value);
static_assert(sizeof(Callbacks) == 1);
#endif
}
'''
self.compile_run(source)
self.compile_run(source, ["-DWITH_ESPNOW_BRIDGE=1"])
if __name__ == "__main__":
unittest.main()