Files
MeshCore/test/test_room_board_store.py
mikecarper d031609f26 Improve room retention, moderation and local browser access
Add opt-in bounded chat history, persist explicit room ACL roles, enforce full-key bans and per-user quotas, and provide versioned information-board reads plus an ESP32 local room page. Preserve stored evidence on faults and withhold room delivery acknowledgments when a Companion cannot retain the message.

Exercise production handlers and filesystem fault recovery, pinned official-app JavaScript, real Chromium interactions, and representative ESP32, nRF52, RP2040 and STM32 builds. Add regression suites and bounded browser-workspace RAM reservations to CI.
2026-10-09 23:44:00 -07:00

323 lines
19 KiB
Python

#!/usr/bin/env python3
"""Exercise the real bounded room board with faults, paging, and power cuts."""
from pathlib import Path
import os
import shutil
import subprocess
import sys
import tempfile
import unittest
from test_room_topic_store import HARNESS as FILESYSTEM_HARNESS, STAT_MOCK
ROOT = Path(__file__).resolve().parents[1]
LFS_MOCK = r'''
struct lfs_info {};
static constexpr int LFS_ERR_NOENT = -2;
static int lfs_stat(MemoryFS* fs, const char* path, struct lfs_info*) {
assert(fs && fs->lock_depth == 1);
const std::string op = std::string("stat:") + path;
const bool okay = fs->begin(op);
const bool present = fs->files.count(path) || fs->directories.count(path);
fs->end(op);
return !okay ? -5 : present ? 0 : LFS_ERR_NOENT;
}
'''
TESTS = r'''
#define FILE_O_WRITE 1
#include <helpers/RoomBoardStore.h>
using Result = mesh::RoomBoardResult;
using Index = mesh::RoomBoardIndex;
static const char* P = mesh::ROOM_BOARD_PRIMARY_PATH;
static const char* T = mesh::ROOM_BOARD_TEMP_PATH;
static const char* B = mesh::ROOM_BOARD_BACKUP_PATH;
static unsigned scenarios = 0;
static const std::string OLD_BODY = "Original room directions";
static const std::string NEW_BODY = std::string(2048, 'n');
static bool load(MemoryFS& fs, Index& index) {
metadata_filesystem = &fs; return mesh::loadRoomBoard(&fs, index);
}
static Result save(MemoryFS& fs, uint8_t id, const char* title,
const std::string& body, uint32_t version = 0, bool short_reader = false) {
metadata_filesystem = &fs;
return mesh::saveRoomBoardArticle(&fs, id, title, body.size(),
[&](size_t offset, uint8_t* out, size_t length) -> size_t {
assert(length <= 64 && offset + length <= body.size());
memcpy(out, body.data() + offset, length);
return short_reader && length ? length - 1 : length;
}, version);
}
static Result erase(MemoryFS& fs, uint8_t id, uint32_t version = 0) {
metadata_filesystem = &fs; return mesh::deleteRoomBoardArticle(&fs, id, version);
}
static Result read(MemoryFS& fs, uint8_t id, uint32_t version, size_t offset,
uint8_t* out, size_t capacity, size_t& count) {
metadata_filesystem = &fs;
return mesh::readRoomBoardArticle(&fs, id, version, offset, out, capacity, count);
}
static MemoryFS clone(const MemoryFS& source) {
MemoryFS fs; fs.directories = source.directories;
for (const auto& file : source.files) fs.put(file.first.c_str(), *file.second);
return fs;
}
static std::map<std::string, std::vector<uint8_t>> snapshot(const MemoryFS& fs) {
std::map<std::string, std::vector<uint8_t>> result;
for (const auto& file : fs.files) result[file.first] = *file.second;
return result;
}
static Index loaded(MemoryFS& fs, uint32_t revision, uint8_t count) {
Index index; assert(load(fs, index));
assert(index.revision == revision && index.count == count); return index;
}
static std::string bodyPath(const mesh::RoomBoardArticle& article) {
char path[24]; mesh::room_board_detail::articlePath(article, path); return path;
}
static void bodyIs(MemoryFS& fs, const mesh::RoomBoardArticle& article, const std::string& expected) {
assert(article.body_length == expected.size());
std::string assembled;
for (size_t offset = 0; offset <= expected.size();) {
uint8_t out[39]; memset(out, 0x7f, sizeof(out)); size_t length = 999;
assert(read(fs, article.id, article.version, offset, out + 1, 37, length) == Result::Success);
assert(out[0] == 0x7f && out[38] == 0x7f && length <= 37);
assembled.append(reinterpret_cast<const char*>(out + 1), length);
offset += length;
if (length == 0) break;
}
assert(assembled == expected);
}
static void checksum(std::vector<uint8_t>& image) {
const uint32_t value = mesh::room_board_detail::crc(0xffffffffu, image.data(), image.size() - 4) ^ 0xffffffffu;
mesh::room_board_detail::put32(image.data() + image.size() - 4, value);
}
static void preserveUnavailable(MemoryFS& fs) {
const auto before = snapshot(fs); const auto directories = fs.directories;
Index index; index.count = 7; index.revision = 99;
assert(!load(fs, index) && index.count == 0 && index.revision == 0);
assert(save(fs, 1, "Changed", NEW_BODY) == Result::Unavailable);
assert(erase(fs, 1) == Result::Unavailable);
uint8_t out[128]; size_t count = 99;
assert(read(fs, 1, 1, 0, out, sizeof(out), count) == Result::Unavailable && count == 0);
assert(snapshot(fs) == before && fs.directories == directories && fs.write_opens == 0);
++scenarios;
}
static void failedUpdate(MemoryFS& fs) {
assert(save(fs, 1, "New title", NEW_BODY) != Result::Success);
fs.reset(); Index index = loaded(fs, 1, 1); bodyIs(fs, index.articles[0], OLD_BODY); ++scenarios;
}
int main() {
MemoryFS initial; Index empty = loaded(initial, 0, 0); (void)empty;
assert(save(initial, 1, "Old title", OLD_BODY) == Result::Success);
Index old = loaded(initial, 1, 1); bodyIs(initial, old.articles[0], OLD_BODY);
initial.reset();
const auto old_index = initial.get(P);
const std::string old_path = bodyPath(old.articles[0]);
const std::string new_path = "/room_board_1.b";
{ MemoryFS fs = clone(initial);
assert(save(fs, 1, "New title", NEW_BODY, 1) == Result::Success);
Index next = loaded(fs, 2, 1); assert(next.articles[0].version == 2 && next.articles[0].bank == 1);
bodyIs(fs, next.articles[0], NEW_BODY);
assert(save(fs, 1, "Again", "third", 2) == Result::Success);
next = loaded(fs, 3, 1); assert(next.articles[0].bank == 0); bodyIs(fs, next.articles[0], "third");
assert(erase(fs, 1, 3) == Result::Success); loaded(fs, 4, 0);
assert(save(fs, 1, "Replacement", "", 0) == Result::Success);
next = loaded(fs, 5, 1); bodyIs(fs, next.articles[0], ""); ++scenarios;
}
{ MemoryFS fs;
for (uint8_t id = 8; id != 0; --id) assert(save(fs, id, "Directions", std::string(2048, char('a' + id))) == Result::Success);
Index index = loaded(fs, 8, 8);
for (uint8_t i = 0; i < 8; ++i) { assert(index.articles[i].id == i + 1); bodyIs(fs, index.articles[i], std::string(2048, char('a' + i + 1))); }
assert(fs.files.size() == 9); // One metadata image and one streamed body per article.
assert(erase(fs, 4, index.articles[3].version) == Result::Success);
index = loaded(fs, 9, 7); assert(index.articles[3].id == 5);
assert(save(fs, 4, "Returned", "body") == Result::Success);
index = loaded(fs, 10, 8); assert(index.articles[3].id == 4 && index.articles[3].version == 10); ++scenarios;
}
{ MemoryFS fs = clone(initial);
const std::string maximum_title(63, 't'); assert(save(fs, 2, maximum_title.c_str(), "body") == Result::Success);
assert(save(fs, 3, u8"Café 地図 📡", "body") == Result::Success);
fs.reset(); const auto before = snapshot(fs);
for (const std::string& title : {std::string(""), std::string(64, 't'), std::string("\xc0\xaf"), std::string("\xed\xa0\x80"), std::string("\xf4\x90\x80\x80"), std::string("\xe2\x82"), std::string("line\nfeed")}) {
assert(save(fs, 1, title.c_str(), "body") == Result::Invalid);
}
assert(save(fs, 0, "Title", "body") == Result::Invalid);
assert(save(fs, 9, "Title", "body") == Result::Invalid);
assert(save(fs, 1, nullptr, "body") == Result::Invalid);
assert(save(fs, 1, "Title", std::string(2049, 'x')) == Result::Invalid);
assert(fs.trace.empty() && snapshot(fs) == before); ++scenarios;
}
{ MemoryFS fs = clone(initial); uint8_t out[130]; size_t count;
assert(read(fs, 1, 2, 0, out, 128, count) == Result::StaleVersion && count == 0);
assert(read(fs, 2, 1, 0, out, 128, count) == Result::NotFound && count == 0);
assert(read(fs, 1, 0, 0, out, 128, count) == Result::Invalid && count == 0);
assert(read(fs, 1, 1, OLD_BODY.size() + 1, out, 128, count) == Result::Invalid && count == 0);
assert(read(fs, 1, 1, 0, out, 129, count) == Result::Invalid && count == 0);
assert(read(fs, 1, 1, 0, out, 0, count) == Result::Invalid && count == 0);
assert(read(fs, 1, 1, 0, nullptr, 128, count) == Result::Invalid && count == 0);
const auto before = snapshot(fs);
assert(save(fs, 1, "Changed", NEW_BODY, 2) == Result::StaleVersion);
assert(save(fs, 2, "Changed", NEW_BODY, 2) == Result::StaleVersion);
assert(erase(fs, 1, 2) == Result::StaleVersion && erase(fs, 2) == Result::NotFound);
assert(snapshot(fs) == before);
assert(save(fs, 1, "Changed", NEW_BODY, 1) == Result::Success);
assert(read(fs, 1, 1, 0, out, 128, count) == Result::StaleVersion && count == 0);
assert(erase(fs, 1, 2) == Result::Success);
assert(read(fs, 1, 1, 0, out, 128, count) == Result::NotFound && count == 0);
assert(save(fs, 1, "Reused", "new") == Result::Success);
assert(read(fs, 1, 1, 0, out, 128, count) == Result::StaleVersion && count == 0); ++scenarios;
}
{ MemoryFS fs = clone(initial); auto image = fs.get(P);
mesh::room_board_detail::put32(image.data() + 4, UINT32_MAX); checksum(image); fs.put(P, image);
loaded(fs, UINT32_MAX, 1); const auto before = snapshot(fs); fs.reset();
assert(save(fs, 1, "Changed", NEW_BODY) == Result::Unavailable);
assert(erase(fs, 1) == Result::Unavailable && fs.write_opens == 0 && snapshot(fs) == before); ++scenarios;
}
{ Index index; assert(!mesh::loadRoomBoard(static_cast<MemoryFS*>(nullptr), index));
uint8_t out[8]; size_t count;
assert(mesh::readRoomBoardArticle(static_cast<MemoryFS*>(nullptr), 1, 1, 0, out, 8, count) == Result::Unavailable);
assert(mesh::deleteRoomBoardArticle(static_cast<MemoryFS*>(nullptr), 1) == Result::Unavailable); ++scenarios;
}
// Every single-byte corruption in the real index/body must fail closed and
// preserve the primary plus its backup/temp evidence without any writes.
for (const std::string& path : {std::string(P), old_path}) {
const auto original = initial.get(path.c_str());
for (size_t byte = 0; byte < original.size(); ++byte) {
MemoryFS fs = clone(initial); auto corrupted = original; corrupted[byte] ^= 1;
fs.put(path.c_str(), corrupted); fs.put(B, old_index); fs.put(T, old_index); preserveUnavailable(fs);
}
for (const auto& invalid : {std::vector<uint8_t>{}, std::vector<uint8_t>{1}, std::vector<uint8_t>(original.begin(), original.end() - 1)}) {
MemoryFS fs = clone(initial); fs.put(path.c_str(), invalid); fs.put(B, old_index); fs.put(T, old_index); preserveUnavailable(fs);
}
auto trailing = original; trailing.push_back(0);
MemoryFS fs = clone(initial); fs.put(path.c_str(), trailing); fs.put(B, old_index); preserveUnavailable(fs);
auto future = original; future[3] = 2; checksum(future);
fs = clone(initial); fs.put(path.c_str(), future); fs.put(B, old_index); preserveUnavailable(fs);
fs = clone(initial); fs.directories.insert(path); preserveUnavailable(fs);
for (const std::string operation : {"open:r:", "read:", "size:"}) {
fs = clone(initial); fs.faults.insert(operation + path); preserveUnavailable(fs);
}
}
for (const auto& mutation : {std::make_pair(size_t(9), uint8_t(1)), std::make_pair(size_t(8), uint8_t(9)),
std::make_pair(size_t(16), uint8_t(0)), std::make_pair(size_t(17), uint8_t(2)),
std::make_pair(size_t(19), uint8_t(9)), std::make_pair(size_t(20), uint8_t(0)),
std::make_pair(size_t(24), uint8_t(0)), std::make_pair(size_t(70), uint8_t('x'))}) {
MemoryFS fs = clone(initial); auto image = old_index; image[mutation.first] = mutation.second; checksum(image);
fs.put(P, image); preserveUnavailable(fs);
}
{ MemoryFS fs = clone(initial); auto image = fs.get(old_path.c_str()); image[12] = 1; checksum(image);
fs.put(old_path.c_str(), image); preserveUnavailable(fs); }
{ MemoryFS fs = clone(initial); fs.put(B, old_index); fs.files.erase(P);
Index index = loaded(fs, 1, 1); bodyIs(fs, index.articles[0], OLD_BODY); assert(!fs.files.count(B)); ++scenarios; }
{ MemoryFS fs; fs.put(T, old_index); fs.put(old_path.c_str(), initial.get(old_path.c_str()));
loaded(fs, 0, 0); assert(!fs.files.count(P) && !fs.files.count(T)); ++scenarios; }
{ MemoryFS fs = clone(initial); fs.files.erase(P); auto future = old_index; future[3] = 2; checksum(future);
fs.put(B, future); fs.put(T, old_index); preserveUnavailable(fs); }
for (const std::string& fault : {std::string("open:w:") + new_path, std::string("write:") + new_path,
std::string("flush:") + new_path, std::string("close:w:") + new_path,
std::string("open:r:") + new_path, std::string("read:") + new_path, std::string("size:") + new_path,
std::string("directory:") + new_path, std::string("open:w:") + T, std::string("write:") + T,
std::string("flush:") + T, std::string("close:w:") + T, std::string("open:r:") + T,
std::string("read:") + T, std::string("size:") + T,
std::string("rename:") + P + ":" + B, std::string("rename:") + T + ":" + P}) {
MemoryFS fs = clone(initial); fs.faults.insert(fault); failedUpdate(fs);
}
{ MemoryFS fs = clone(initial); fs.short_write = true; failedUpdate(fs); }
{ MemoryFS fs = clone(initial); fs.short_read = true; failedUpdate(fs); }
{ MemoryFS fs = clone(initial); assert(save(fs, 1, "Changed", NEW_BODY, 0, true) == Result::WriteFailure);
fs.reset(); loaded(fs, 1, 1); ++scenarios; }
{ MemoryFS fs = clone(initial); fs.replacement_on_close = initial.get(old_path.c_str()); failedUpdate(fs); }
// A full filesystem can truncate any streamed body or metadata boundary.
for (size_t budget : {size_t(0), size_t(15), size_t(16), size_t(100), size_t(2067), size_t(2068), size_t(2100), size_t(2159)}) {
MemoryFS fs = clone(initial); fs.write_budget = budget; failedUpdate(fs);
}
{ MemoryFS fs = clone(initial); fs.put(new_path.c_str(), std::vector<uint8_t>{1});
fs.faults.insert("remove:" + new_path); failedUpdate(fs); }
for (const char* artifact : {T, B}) {
MemoryFS fs = clone(initial); fs.put(artifact, old_index); fs.faults.insert(std::string("remove:") + artifact);
loaded(fs, 1, 1); fs.write_opens = 0; assert(save(fs, 1, "Changed", NEW_BODY) == Result::Unavailable && fs.write_opens == 0);
fs.reset(); loaded(fs, 1, 1); ++scenarios;
}
{ MemoryFS fs = clone(initial); fs.faults.insert(std::string("remove:") + B);
assert(save(fs, 1, "Changed", NEW_BODY) == Result::Success);
loaded(fs, 2, 1); assert(fs.files.count(B));
assert(save(fs, 1, "Changed again", "x") == Result::Unavailable);
fs.reset(); loaded(fs, 2, 1); ++scenarios; }
{ MemoryFS fs = clone(initial); fs.faults.insert(std::string("rename:") + T + ":" + P);
fs.faults.insert(std::string("rename:") + B + ":" + P); fs.faults.insert(std::string("remove:") + T);
assert(save(fs, 1, "Changed", NEW_BODY) == Result::WriteFailure && !fs.files.count(P) && fs.files.count(B));
fs.reset(); Index index = loaded(fs, 1, 1); bodyIs(fs, index.articles[0], OLD_BODY); ++scenarios; }
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) || defined(ESP32_PLATFORM)
{ MemoryFS fs = clone(initial); fs.faults.insert(std::string("stat:") + P); preserveUnavailable(fs); }
{ MemoryFS fs = clone(initial); fs.files.erase(P); fs.put(B, old_index);
fs.faults.insert(std::string("stat:") + B); preserveUnavailable(fs); }
#endif
// Cut before and after every filesystem operation in publish, update, and
// delete. Reboot may expose only the preceding image or the committed image.
for (unsigned mode = 0; mode < 3; ++mode) {
MemoryFS baseline = mode == 0 ? MemoryFS{} : clone(initial);
assert((mode == 2 ? erase(baseline, 1) : save(baseline, 1, "New title", NEW_BODY)) == Result::Success);
const auto committed_image = baseline.get(P); const size_t boundaries = baseline.boundary;
for (size_t cut = 1; cut <= boundaries; ++cut) {
MemoryFS fs = mode == 0 ? MemoryFS{} : clone(initial); fs.cut_at = cut;
try { if (mode == 2) erase(fs, 1); else save(fs, 1, "New title", NEW_BODY); } catch (const PowerCut&) {}
const bool committed = fs.files.count(P) && fs.get(P) == committed_image;
fs.reset();
Index index = loaded(fs, committed ? (mode == 0 ? 1 : 2) : (mode == 0 ? 0 : 1),
committed && mode == 2 ? 0 : (mode == 0 && !committed ? 0 : 1));
if (index.count) bodyIs(fs, index.articles[0], committed ? NEW_BODY : OLD_BODY);
assert(!fs.files.count(T) && !fs.files.count(B)); ++scenarios;
}
}
printf("PASS: %u room board persistence, version/paging, fault and power-cut scenarios\n", scenarios);
}
'''
class RoomBoardStoreTest(unittest.TestCase):
def test_real_store_with_all_backend_modes_and_faults(self):
compiler = os.environ.get("CXX") or shutil.which("g++") or shutil.which("clang++")
if compiler is None:
self.skipTest("a host C++ compiler is required")
filesystem = FILESYSTEM_HARNESS.split("#define FILE_O_WRITE 1")[0]
filesystem = filesystem.replace(" void _lockFS() {}\n void _unlockFS() {}\n MemoryFS* _getFS() { return this; }\n", "", 1)
filesystem = filesystem.replace(" void point(const std::string& event) {", """ int lock_depth = 0;
void _lockFS() { assert(lock_depth++ == 0); }
void _unlockFS() { assert(--lock_depth == 0); }
MemoryFS* _getFS() { return this; }
size_t write_budget = std::numeric_limits<size_t>::max();
void point(const std::string& event) {""")
filesystem = filesystem.replace(" bytes->insert(bytes->end(), data, data + length);", """ length = std::min(length, fs->write_budget);
fs->write_budget -= length;
bytes->insert(bytes->end(), data, data + length);""")
filesystem = filesystem.replace("void reset() { trace.clear();", "void reset() { lock_depth = 0; write_budget = std::numeric_limits<size_t>::max(); trace.clear();")
sanitizers = (["-fsanitize=address,undefined", "-fno-sanitize-recover=all", "-fno-pie", "-no-pie"]
if sys.platform.startswith("linux") else [])
with tempfile.TemporaryDirectory(prefix="meshcore-room-board-") as temporary:
work = Path(temporary)
(work / "sys").mkdir()
(work / "sys/stat.h").write_text(STAT_MOCK)
source = work / "board.cpp"
for platform in (None, "NRF52_PLATFORM", "STM32_PLATFORM", "RP2040_PLATFORM", "ESP32_PLATFORM"):
with self.subTest(platform=platform):
source.write_text(filesystem + (LFS_MOCK if platform in ("NRF52_PLATFORM", "STM32_PLATFORM") else "") + TESTS)
binary = work / (platform or "generic")
defines = ["-D" + platform + "=1"] if platform else []
built = subprocess.run([compiler, "-std=c++11", "-Wall", "-Wextra", "-Werror", *sanitizers,
*defines, "-I" + str(work), "-I" + str(ROOT / "src"), str(source), "-o", str(binary)],
capture_output=True, text=True, timeout=60)
self.assertEqual(built.returncode, 0, built.stdout + built.stderr)
checked = subprocess.run([str(binary)], capture_output=True, text=True, timeout=60)
self.assertEqual(checked.returncode, 0, checked.stdout + checked.stderr)
self.assertIn("room board persistence, version/paging, fault and power-cut scenarios", checked.stdout)
print(checked.stdout.strip())
if __name__ == "__main__":
unittest.main()