Files
MeshCore/test/fixtures/management/runtime_test.cpp

275 lines
14 KiB
C++

#include "ManagementReporter.h"
#include "CommonCLI.h"
#include "PersistentStoreFormat.h"
#include <cassert>
#include <cstdio>
#include <memory>
#include <string>
using namespace mesh;
using namespace mesh::management;
namespace mesh {
UsbLoggingStatus test_usb_status;
unsigned usb_status_calls = 0;
UsbLoggingStatus usbLoggingStatus() { ++usb_status_calls; return test_usb_status; }
}
struct Fixture {
MemoryFS fs;
Mesh mesh;
MainBoard board;
SensorManager sensors;
ClientACL acl;
NodePrefs prefs;
CommonCLI cli;
CommonCLICallbacks callbacks;
std::unique_ptr<ManagementReporter> reporter;
Fixture() { test_millis = 0; test_usb_status = UsbLoggingStatus(); usb_status_calls = 0; reboot(); }
void reboot() { reporter.reset(); test_millis = 0; reporter.reset(new ManagementReporter(mesh, board, sensors, acl, prefs, callbacks, cli, &fs)); }
std::string cmd(const char* text, bool ok = true) {
char b[200], reply[160] = {}; strcpy(b, text);
assert(reporter->command(b, reply, sizeof(reply)));
if (!strncmp(text, "set ", 4) && ((!strncmp(reply, "OK", 2)) != ok)) {
fprintf(stderr, "%s: %s\n", text, reply); assert(false);
}
if (!strncmp(text, "set mgmt.password ", 18)) assert(b[18] == 0);
return reply;
}
void advance(uint32_t seconds) {
while (seconds) { const uint32_t step = seconds < 60 ? seconds : 60; test_millis += step * 1000;
reporter->loop(); seconds -= step;
}
}
void configure(bool direct = true) {
cmd("set mgmt.enabled on", false);
cmd("set mgmt.password short", false);
cmd("set mgmt.password management test password");
if (direct) cmd("set mgmt.interval 5");
cmd("set mgmt.enabled on");
// Simulate a route becoming unavailable after enable so fallback-only
// tests exercise the scoped 21-day safety report.
if (!direct) cli.path_len = 0xff;
const auto& file = fs.files["/management"];
assert(std::string(file.begin(), file.end()).find("management test password") == std::string::npos);
}
std::vector<uint8_t> installLegacyRoute() {
auto legacy = fs.files["/management"];
memcpy(legacy.data(), "MGC1", 4); legacy[7] = 1; legacy[8] = 0x12;
write32(legacy.data() + 116, storage::updateCRC32(0xffffffff, legacy.data(), 116));
fs.files["/management"] = legacy;
return legacy;
}
};
int main() {
{
Fixture f;
assert(f.cmd("get mgmt").find("direct=5d") != std::string::npos);
assert(f.cmd("get mgmt").find("flood=21d") != std::string::npos);
f.advance(100 * DAY); assert(f.mesh.packets.empty());
f.configure(); f.cmd("set mgmt.interval 91", false);
f.advance(5 * DAY - 60); assert(f.mesh.packets.empty());
// Changes to RTC in either direction have no effect on flood eligibility.
f.mesh.rtc.time = 0; f.advance(60); assert(f.mesh.packets.size() == 1 && !f.mesh.packets[0].flood);
f.mesh.rtc.time = 0xffffffff; f.reboot();
f.advance(5 * DAY + 3600); assert(f.mesh.packets.size() == 2);
for (const auto& p : f.mesh.packets) assert(!p.flood);
}
{
Fixture f; f.configure(); f.advance(21 * DAY + 601);
assert(f.mesh.packets.size() == 5 && !f.mesh.packets[0].flood && f.mesh.packets.back().flood);
const uint32_t seq = read32(f.mesh.packets.back().payload + 20); f.reboot(); f.advance(600);
assert(f.mesh.packets.size() == 5);
f.cmd("set mgmt.enabled off"); f.cmd("set mgmt.enabled on"); f.advance(600);
assert(f.mesh.packets.size() == 5 && read32(f.mesh.packets.back().payload + 20) == seq);
}
{
Fixture f; f.configure(); f.cmd("set mgmt.flood off");
f.advance(21 * DAY + 3600); assert(f.mesh.packets.size() == 4);
for (const auto& p : f.mesh.packets) assert(!p.flood);
}
{
Fixture f; f.configure(); f.cmd("set mgmt.direct off");
f.advance(21 * DAY + 60); assert(f.mesh.packets.size() == 1);
assert(f.mesh.packets[0].flood);
}
{
Fixture f; f.configure(); f.cmd("set mgmt.direct 10");
f.cmd("set mgmt.flood 30"); f.advance(30 * DAY + 60);
assert(f.mesh.packets.size() == 3 && f.mesh.packets.back().flood);
assert(!f.mesh.packets[0].flood && !f.mesh.packets[1].flood);
}
{
Fixture f; f.configure(false); f.advance(21 * DAY - 60); f.fs.fail_write = true; f.advance(120);
assert(f.mesh.packets.empty()); assert(f.cmd("get mgmt").find("FAULT") != std::string::npos);
// Last hourly checkpoint was one hour before the failed reservation.
f.fs.fail_write = false; f.reboot(); f.advance(3600); assert(f.mesh.packets.size() == 1);
}
{
Fixture f; f.configure(false); f.fs.files["/management"][72] ^= 1; f.reboot(); f.advance(100 * DAY);
assert(f.mesh.packets.empty()); assert(f.cmd("get mgmt").find("FAULT") != std::string::npos);
}
{
Fixture f; f.configure(false); f.fs.fail_read_open = true; f.reboot(); f.advance(100 * DAY);
assert(f.mesh.packets.empty());
}
{
Fixture f;
for (unsigned i = 0; i < 36; ++i) { ClientInfo c; c.id.pub_key[0] = i; f.acl.clients.push_back(c); }
test_usb_status.supported = true; test_usb_status.watchdog_enabled = true;
test_usb_status.host_connected = test_usb_status.reader_connected = true;
test_usb_status.stalled = true; test_usb_status.retry_seconds = 7200;
test_usb_status.inactive_seconds = 3600; test_usb_status.backoff_step = 1;
test_usb_status.last_event.reasons = UsbLoggingWatchdogEvent::TX_STALLED;
test_usb_status.last_event.action = UsbLoggingWatchdogEvent::REENUMERATE;
test_usb_status.last_event.sequence = 17; test_usb_status.last_event.uptime_seconds = 300;
test_usb_status.last_event.persisted = true;
f.configure(false); f.advance(21 * DAY + 600); assert(f.mesh.packets.size() == 9);
assert(usb_status_calls == 1);
for (unsigned i = 0; i < 9; ++i) {
const auto& p = f.mesh.packets[i]; assert(validPage(p.payload, p.payload_len));
assert(p.payload[78] == i && p.payload[80] == 36 && p.flood);
assert(!memcmp(p.payload, "MGR2", 4) && read16(p.payload + HEADER) == 61);
assert(!(read16(p.payload + HEADER) & 1024)); // an open reader is not an active logger
assert(read32(p.payload + HEADER + 3) == 7200);
assert(read32(p.payload + HEADER + 7) == 3600);
UsbLoggingWatchdogEvent event;
assert(decodeUsbWatchdogEvent(p.payload + WATCHDOG_EVENT_OFFSET, event));
assert(event.reasons == UsbLoggingWatchdogEvent::TX_STALLED && event.action == UsbLoggingWatchdogEvent::REENUMERATE);
assert(event.sequence == 17 && event.uptime_seconds == 300 && event.persisted && event.epoch == 0);
assert(floodSize(p.payload_len) <= 179);
}
}
{
Fixture f;
for (unsigned i = 0; i < 7; ++i) { ClientInfo c; c.id.pub_key[0] = i; f.acl.clients.push_back(c); }
f.configure(false); f.advance(21 * DAY); assert(f.mesh.packets.size() == 1);
assert(!memcmp(f.mesh.packets[0].payload, "MGR2", 4));
assert(read16(f.mesh.packets[0].payload + HEADER) == 0); // unsupported is explicit
test_usb_status.supported = true; test_usb_status.retry_seconds = 999;
test_usb_status.watchdog_auto = true; test_usb_status.auto_connected_seconds = 100;
test_usb_status.host_connected = test_usb_status.reader_connected = test_usb_status.logger_active = true;
test_usb_status.last_event.reasons = UsbLoggingWatchdogEvent::CLIENT_INACTIVE;
test_usb_status.last_event.action = UsbLoggingWatchdogEvent::SOFT_RECOVERY;
test_usb_status.last_event.sequence = 1; test_usb_status.last_event.epoch = 1700000001;
f.board.voltage = 2900; f.board.temperature = 60; f.advance(60);
assert(f.mesh.packets.size() == 2);
assert(read16(f.mesh.packets[1].payload + HEADER) == 0); // frozen report snapshot
assert(read32(f.mesh.packets[1].payload + HEADER + 11) == 0);
assert(read32(f.mesh.packets[1].payload + WATCHDOG_EVENT_OFFSET + 9) == 0);
assert(usb_status_calls == 1);
// The low reading was not in the first snapshot and must survive into the next one.
f.board.voltage = 3740; f.board.temperature = 24; f.advance(21 * DAY + 3600);
assert(f.mesh.packets.size() == 4);
assert(read16(f.mesh.packets.back().payload + 66) == 2900);
assert(f.mesh.packets.back().payload[69] == temperature(60));
assert(read16(f.mesh.packets.back().payload + HEADER) == 1561);
assert(read32(f.mesh.packets.back().payload + HEADER + 11) == 100);
assert(read32(f.mesh.packets.back().payload + WATCHDOG_EVENT_OFFSET + 9) == 1);
assert(usb_status_calls == 2);
}
{
Fixture f; f.configure(false); f.mesh.temp = true; f.advance(22 * DAY); assert(f.mesh.packets.empty());
f.mesh.temp = false; f.mesh.budget = 0; f.advance(60); assert(f.mesh.packets.empty());
f.mesh.budget = 1000; f.advance(60); assert(f.mesh.packets.size() == 1);
}
{
Fixture f; f.configure(false); f.cmd("set mgmt.flood 90"); f.advance(89 * DAY); assert(f.mesh.packets.empty());
f.advance(DAY); assert(f.mesh.packets.size() == 1);
}
{
Fixture f; f.configure();
const auto saved = f.fs.files;
const std::string state = f.cmd("get mgmt");
for (const char* field : {"direct", "flood", "interval"}) {
for (const char* value : {"", "-", "-5", "-4294967291", "-4294967275", "+21",
" 21", "21 ", "21x", "4294967301", "4294967317",
"18446744073709551637", "999999999999999999999999"}) {
const std::string command = std::string("set mgmt.") + field + " " + value;
f.cmd(command.c_str(), false);
assert(f.cmd("get mgmt") == state && f.fs.files == saved);
}
}
f.cmd("set mgmt.direct 0005"); f.cmd("set mgmt.flood 0021"); f.cmd("set mgmt.interval 0005");
assert(f.cmd("get mgmt").find("direct=5d") != std::string::npos);
assert(f.cmd("get mgmt").find("flood=21d") != std::string::npos);
f.cmd("set mgmt.direct 90"); f.cmd("set mgmt.flood 90");
f.cmd("set mgmt.direct 4", false); f.cmd("set mgmt.flood 20", false);
f.cmd("set mgmt.interval 91", false);
}
{
Fixture f;
for (unsigned i = 0; i < 36; ++i) { ClientInfo c; c.id.pub_key[0] = i; f.acl.clients.push_back(c); }
f.configure(false); f.advance(21 * DAY); assert(f.mesh.packets.size() == 1);
const auto saved = f.fs.files;
const std::string state = f.cmd("get mgmt");
for (const char* command : {"set mgmt.unknown value", "set mgmt.enabled maybe",
"set mgmt.direct 4", "set mgmt.flood 20", "set mgmt.interval 91",
"set mgmt.password short", "set mgmt.direct 5", "set mgmt.enabled on"}) {
f.cmd(command, false);
assert(f.cmd("get mgmt") == state && f.fs.files == saved);
}
f.cli.scope_available = false;
const std::string unscoped = f.cmd("get mgmt");
f.cmd("set mgmt.flood 21", false);
assert(f.cmd("get mgmt") == unscoped && f.fs.files == saved);
f.cli.scope_available = true;
f.advance(60); assert(f.mesh.packets.size() == 2);
f.advance(7 * 60); assert(f.mesh.packets.size() == 9 && usb_status_calls == 1);
for (unsigned page = 0; page < 9; ++page) assert(f.mesh.packets[page].payload[78] == page);
}
for (bool fail_save : {false, true}) {
Fixture f;
for (unsigned i = 0; i < 8; ++i) { ClientInfo c; c.id.pub_key[0] = i; f.acl.clients.push_back(c); }
f.configure(false); f.advance(21 * DAY); assert(f.mesh.packets.size() == 1);
const auto saved = f.fs.files;
f.fs.fail_write = fail_save;
f.cmd("set mgmt.flood 30", !fail_save);
f.advance(3600); assert(f.mesh.packets.size() == 1);
assert((f.cmd("get mgmt").find("FAULT") != std::string::npos) == fail_save);
if (fail_save) assert(f.fs.files == saved);
else assert(f.cmd("get mgmt").find("flood=30d") != std::string::npos);
}
{
Fixture f; f.configure(false);
f.fs.files["/management"][72] ^= 1; f.reboot();
assert(f.cmd("get mgmt").find("FAULT") != std::string::npos);
const auto saved = f.fs.files;
f.cmd("set mgmt.password another valid secret", false);
f.cmd("set mgmt.password short", false);
assert(f.fs.files == saved);
}
for (bool migration_succeeds : {false, true}) {
Fixture f; f.configure();
const auto legacy = f.installLegacyRoute();
f.cli.adoption_succeeds = migration_succeeds;
f.reboot(); f.advance(3600);
assert(f.cli.adoption_calls == 1 && f.mesh.packets.empty());
if (!migration_succeeds) {
// The route write alone failed, while management writes still work.
// Do not let an hourly MGC2 rewrite destroy the only saved legacy path.
assert(f.fs.files["/management"] == legacy);
const std::string fault = f.cmd("get mgmt");
assert(fault.find("FAULT") != std::string::npos);
assert(fault.find("> off key=unset") != std::string::npos);
f.cli.adoption_succeeds = true;
f.reboot(); f.advance(3600);
assert(f.cli.adoption_calls == 2);
}
const uint8_t* path = nullptr; uint8_t path_len = OUT_PATH_UNKNOWN;
assert(f.cli.getDataTxPath(path, path_len) && path_len == 1 && path[0] == 0x12);
assert(!memcmp(f.fs.files["/management"].data(), "MGC2", 4));
assert(f.cmd("get mgmt").find("FAULT") == std::string::npos);
const auto calls = f.cli.adoption_calls;
f.reboot(); assert(f.cli.adoption_calls == calls); // no repeated migration
}
{
Fixture f; f.configure(); f.installLegacyRoute();
const uint8_t explicit_path[] = {0x34};
assert(f.cli.adoptLegacyDataTxPath(explicit_path, 1)); // already saved /data_tx
f.cli.adoption_succeeds = false; // no additional route write should be needed
f.reboot(); f.advance(3600);
const uint8_t* path = nullptr; uint8_t path_len = OUT_PATH_UNKNOWN;
assert(f.cli.getDataTxPath(path, path_len) && path_len == 1 && path[0] == 0x34);
assert(f.cmd("get mgmt").find("FAULT") == std::string::npos);
assert(!memcmp(f.fs.files["/management"].data(), "MGC2", 4));
}
}