Files
MeshCore/test/test_room_rtc_posts.py
T

279 lines
16 KiB
Python

#!/usr/bin/env python3
"""Exercise the real qualified RTC post bank, post/history methods and lifetime."""
from pathlib import Path
import re
import shutil
import subprocess
import sys
import tempfile
import unittest
from test_replay_reset_integration import extract_braced
from test_room_history_store import STAT_MOCK
ROOT = Path(__file__).resolve().parents[1]
class RoomRtcPostsTests(unittest.TestCase):
def sources(self):
source = (ROOT / "examples/simple_room_server/MyMesh.cpp").read_text()
header = (ROOT / "examples/simple_room_server/MyMesh.h").read_text()
start = source.index("#if ROOM_POSTS_RTC_SLOW_STORAGE\n#include <esp_attr.h>")
bank = source[start:source.index("MyMesh::MyMesh(", start)]
constructor = extract_braced(source, "MyMesh::MyMesh(")
initializer = re.search(r"posts\(acquireRoomRtcPosts\(this\)\)", constructor)[0]
clear = re.search(r"memset\(posts, 0, sizeof\(posts\)\);", constructor)[0]
destructor = extract_braced(source, "MyMesh::~MyMesh()")
post = extract_braced(header, "struct PostInfo") + ";\n"
return source, header, bank, initializer, clear, destructor, post
def compile(self, directory, source, *, ndebug=False, name="posts"):
directory = Path(directory)
(directory / "esp_attr.h").write_text('#define RTC_SLOW_ATTR __attribute__((section(".rtc.force_slow.0")))\n')
(directory / "sdkconfig.h").write_text("#define CONFIG_IDF_TARGET_ESP32 1\n")
path = directory / (name + ".cpp")
path.write_text(source)
binary = directory / name
flags = ["-DNDEBUG"] if ndebug else []
sanitized = (["-fsanitize=address,undefined", "-fno-sanitize-recover=all", "-fno-pie", "-no-pie"]
if sys.platform.startswith("linux") else [])
built = subprocess.run([shutil.which("g++") or "clang++", "-std=c++17", "-Wall", "-Wextra", "-Werror",
"-Wno-unused-function", "-Wno-unused-parameter", "-Wno-class-memaccess", *sanitized, *flags,
"-DROOM_POSTS_RTC_SLOW_STORAGE=1", "-DESP32_PLATFORM=1",
"-I" + str(directory), "-I" + str(ROOT / "src"),
"-I" + str(ROOT / "test/mocks"), str(path), "-o", str(binary)],
text=True, capture_output=True, timeout=60)
self.assertEqual(built.returncode, 0, built.stdout + built.stderr)
return binary
def minimal_fixture(self):
_, _, bank, initializer, clear, destructor, post = self.sources()
_, header, _, _, _, _, _ = self.sources()
deleted = "\n".join(line for line in header.splitlines() if "MyMesh" in line and "= delete;" in line)
return r'''
#include <type_traits>
#include <cstdint>
#include <cstring>
#include <cstdlib>
#include <cstdio>
#define MAX_UNSYNCED_POSTS 32
#define MAX_POST_TEXT_LEN 151
#define CHECK(value) do { if (!(value)) abort(); } while (0)
namespace mesh { struct Identity { uint8_t pub_key[32]; Identity() { memset(pub_key,0,32); } }; }
''' + post + bank + r'''
struct MyMesh {
PostInfo (&posts)[MAX_UNSYNCED_POSTS];
MyMesh() : ''' + initializer + " { " + clear + r''' }
~MyMesh();
''' + deleted + r'''
};
static_assert(!std::is_copy_constructible<MyMesh>::value && !std::is_move_constructible<MyMesh>::value, "RTC owner cannot be copied or moved");
''' + destructor + r'''
int main(int argc,char** argv) {
int mode=argc>1 ? atoi(argv[1]) : 0;
if(mode==1) { MyMesh first; MyMesh second; return 1; }
if(mode==2) { MyMesh first; releaseRoomRtcPosts(&mode); return 1; }
if(mode==3) { acquireRoomRtcPosts(nullptr); return 1; }
PostInfo* address=nullptr;
{
MyMesh first; address=first.posts;
CHECK(sizeof(first.posts)==6016);
for(size_t i=0;i<sizeof(first.posts);++i) CHECK(reinterpret_cast<uint8_t*>(first.posts)[i]==0);
for(unsigned i=0;i<32;++i) {
memset(first.posts[i].author.pub_key,int(i+1),32);
first.posts[i].post_timestamp=0xF0000000+i;
memset(first.posts[i].text,int('A'+i%26),151);first.posts[i].text[151]=0;
}
for(unsigned i=0;i<32;++i) {
CHECK(first.posts[i].author.pub_key[31]==i+1);
CHECK(first.posts[i].post_timestamp==0xF0000000+i);
CHECK(strlen(first.posts[i].text)==151);
}
}
{ MyMesh second; CHECK(second.posts==address);
for(size_t i=0;i<sizeof(second.posts);++i) CHECK(reinterpret_cast<uint8_t*>(second.posts)[i]==0); }
CHECK(room_rtc_posts_owner==nullptr);
puts("RTC post ownership, complete capacity and boot reset passed");
}
'''
def test_actual_bank_ownership_capacity_and_reset_with_ndebug(self):
with tempfile.TemporaryDirectory(prefix="room-rtc-owner-") as directory:
binary = self.compile(directory, self.minimal_fixture(), ndebug=True)
okay = subprocess.run([str(binary)], capture_output=True, text=True, timeout=20)
self.assertEqual(okay.returncode, 0, okay.stdout + okay.stderr)
for mode in (1, 2, 3):
with self.subTest(mode=mode):
failed = subprocess.run([str(binary), str(mode)], capture_output=True, text=True, timeout=20)
self.assertEqual(failed.returncode, -6, failed.stdout + failed.stderr)
def test_physical_bank_size_and_ordinary_disabled_source(self):
with tempfile.TemporaryDirectory(prefix="room-rtc-section-") as directory:
binary = self.compile(directory, self.minimal_fixture())
sections = subprocess.check_output(["readelf", "-SW", str(binary)], text=True)
section = next(line for line in sections.splitlines() if ".rtc.force_slow.0" in line)
self.assertRegex(section, r"\b001780\b") # All 32 complete 188-byte records.
_, _, bank, _, _, _, _ = self.sources()
# The default path must not include any SDK headers, owner or bank.
source = Path(directory) / "disabled.cpp"
source.write_text(bank + "\nint main() {}\n")
output = subprocess.run([shutil.which("g++") or "clang++", "-E", "-P", "-x", "c++", str(source)],
capture_output=True, text=True, timeout=20)
self.assertEqual(output.returncode, 0, output.stderr)
self.assertEqual(output.stdout.strip(), "int main() {}")
def test_storage_flag_rejects_unqualified_boards_and_invalid_values(self):
_, header, _, _, _, _, _ = self.sources()
start = header.index("#ifndef ROOM_POSTS_RTC_SLOW_STORAGE")
policy = header[start:header.index("struct PostInfo", start)]
cases = (([], True), (["-DROOM_POSTS_RTC_SLOW_STORAGE=0"], True),
(["-DROOM_POSTS_RTC_SLOW_STORAGE=2"], False),
(["-DROOM_POSTS_RTC_SLOW_STORAGE=1", "-DNRF52_PLATFORM=1"], False),
(["-DROOM_POSTS_RTC_SLOW_STORAGE=1", "-DESP32_PLATFORM=1"], False),
(["-DROOM_POSTS_RTC_SLOW_STORAGE=1", "-DESP32_PLATFORM=1", "-DLILYGO_TLORA=1",
"-DMESHCORE_EXPANDED_PARTITION_PROFILE=1"], True),
*((["-DROOM_POSTS_RTC_SLOW_STORAGE=1", "-DESP32_PLATFORM=1", "-D" + board + "=1",
"-DMESHCORE_EXPANDED_PARTITION_PROFILE=1"], True)
for board in ("TBEAM_SX1262", "TBEAM_SX1276")),
*((["-DROOM_POSTS_RTC_SLOW_STORAGE=1", "-DESP32_PLATFORM=1", "-D" + board + "=1"], False)
for board in ("LILYGO_TLORA", "TBEAM_SX1262", "TBEAM_SX1276")))
for flags, valid in cases:
with self.subTest(flags=flags):
result = subprocess.run([shutil.which("g++") or "clang++", "-std=c++17", "-x", "c++",
"-fsyntax-only", *flags, "-"], input=policy + "\nint main() {}\n",
capture_output=True, text=True, timeout=20)
self.assertEqual(result.returncode == 0, valid, result.stderr)
def test_only_qualified_full_room_recipes_select_complete_storage_policy(self):
text = (ROOT / "build_legacy.sh").read_text()
functions = []
for name in ("apply_esp32_full_size_profile", "apply_esp32_iram_advert_storage_profile"):
start = text.index(name + "() {")
functions.append(text[start:text.index("\n}\n", start) + 2])
targets = {
"Tbeam_SX1262_room_server_observer_mqtt": True,
"Tbeam_SX1276_room_server_observer_mqtt": True,
"LilyGo_TLora_V2_1_1_6_room_server_observer_mqtt_": True,
"Tbeam_SX1262_room_server": False,
"Tbeam_SX1276_room_server": False,
"Tbeam_SX1262_repeater_observer_mqtt": False,
"Tbeam_SX1276_companion_radio_ble": False,
"T_Beam_S3_Supreme_SX1262_room_server_observer_mqtt": False,
"RAK_4631_room_server": False,
}
for full in ("0", "1"):
for supported in ("0", "1"):
for target, selected in targets.items():
with self.subTest(full=full, supported=supported, target=target):
code = r'''
ESP32_FULL_BUILD="$1"
QUALIFIED="$2"
ESP32_FULL_MAX_NEIGHBOURS=254
DRAM_LIMITED_MAX_NEIGHBOURS=50
PLATFORMIO_BUILD_FLAGS="-DEXISTING=1"
declare -A PIO_ENV_FULL_WIFI_OTA_BY_NAME=()
supports_esp32_full_build() { [ "$QUALIFIED" = 1 ]; }
is_room_server_role_target() { [[ "$1" == *room_server* ]]; }
requires_dram_limited_neighbors() { [[ "$1" == Tbeam_SX12* ]]; }
requires_esp32_companion_full_ota_fallback() { return 1; }
append_platformio_build_unflags() { :; }
record_build_reduction() { :; }
''' + "\n".join(functions) + r'''
apply_esp32_full_size_profile "$3"
apply_esp32_iram_advert_storage_profile "$3"
printf "%s" "$PLATFORMIO_BUILD_FLAGS"
'''
result = subprocess.run(["bash", "-c", code, "bash", full, supported, target],
capture_output=True, text=True, check=True, timeout=10)
enabled = full == "1" and supported == "1" and selected
flags = result.stdout.split()
for name in ("ROOM_POSTS_RTC_SLOW_STORAGE", "OTA_BOUNDED_SOURCE_PROOFS"):
self.assertEqual(flags.count("-D" + name + "=1"), int(enabled), result.stdout)
if enabled:
self.assertIn("-DFLOOD_ADVERT_IRAM_WORD_STORAGE=1", flags)
self.assertNotIn("-DFLOOD_PACKET_FILTER_SLOTS=31", flags)
self.assertIn("-DMESH_ENABLE_ROOM_FLOOD_RULE_ENGINE=1", flags)
self.assertIn("-DMAX_NEIGHBOURS=" + ("50" if target.startswith("Tbeam_") else "254"), flags)
self.assertNotIn("-DMAX_UNSYNCED_POSTS", result.stdout)
self.assertNotIn("-DFLOOD_ADVERT_SOURCE_SLOTS", result.stdout)
def test_real_post_fifo_history_reload_maximum_text_and_catchup(self):
source, header, bank, initializer, clear, destructor, post = self.sources()
fixture = (ROOT / "test/fixtures/room_history_integration/test.cpp").read_text().split("int main()", 1)[0]
# Keep the actual persistence/admission methods and existing FS fault
# boundary, but simulate boots sequentially because this bank has one owner.
fixture = fixture[:fixture.index("static void compareBoot(")]
fixture = fixture.replace("struct Identity { uint8_t pub_key[32] = {}; };", "struct Identity { uint8_t pub_key[32] = {}; bool matches(const Identity& other) const { return !memcmp(pub_key,other.pub_key,32); } };")
fixture = fixture.replace("struct MyMesh {", bank + "\nstruct MyMesh {")
fixture = fixture.replace(" PostInfo posts[MAX_UNSYNCED_POSTS] = {};", " PostInfo (&posts)[MAX_UNSYNCED_POSTS];")
fixture = fixture.replace("explicit MyMesh(MemoryFS* fs) : _fs(fs) {", "explicit MyMesh(MemoryFS* fs) : _fs(fs), " + initializer + " { " + clear)
fixture = fixture.replace(" Clock* getRTCClock()", " ~MyMesh();\n Clock* getRTCClock()")
fixture = fixture.replace('#include "../room_history_store/filesystem.h"', '#include "' + str(ROOT / 'test/fixtures/room_history_store/filesystem.h') + '"')
fixture += destructor + r'''
#include <helpers/RoomCatchUp.h>
struct SyncClient {
mesh::Identity id;
struct { struct { uint32_t sync_since=0,pending_ack=0,pending_topic_revision=0,push_post_timestamp=0; } room; } extra;
};
int main() {
MemoryFS fs; std::vector<std::string> expected;
for(int utf8=0;utf8<2;++utf8) {
std::string full(151,'A');
if(utf8) { full="x";for(int i=0;i<50;++i)full+="\xe2\x98\x83"; }
{
MyMesh m(&fs);m.loadRoomHistory();
assert(m.command("room.history.clear").find("cleared")!=std::string::npos);
assert(m.command("set room.history on").find("enabled")!=std::string::npos);
assert(full.size()==151 && m.addSystemPost(full.c_str()));
assert(m.retained().back()==full && m.posts[0].text[151]==0);
assert(!m.addSystemPost((full+"!").c_str()));
for(int i=0;i<70;++i) assert(m.addSystemPost(("post-"+std::to_string(i)).c_str()));
assert(m.retained().size()==32 && m.retained().front()=="post-38");
assert(m.post_ready_mask==0 && m.snapshotRoomHistory());
expected=m.retained();
PostInfo ordinary[32];memcpy(ordinary,m.posts,sizeof(ordinary));
SyncClient client;
for(auto mode : {mesh::RoomCatchUpMode::Before,mesh::RoomCatchUpMode::SkipNext,mesh::RoomCatchUpMode::KeepNewest})
for(uint32_t value : {0u,1u,16u,31u,32u,100u,UINT32_MAX}) {
auto rtc=mesh::planRoomCatchUp(client,m.posts,32,mode,value);
auto old=mesh::planRoomCatchUp(client,ordinary,32,mode,value);
assert(rtc.candidate_cursor==old.candidate_cursor && rtc.skipped==old.skipped);
assert(rtc.backlog_remaining==old.backlog_remaining && rtc.pending_post_cancelled==old.pending_post_cancelled);
}
}
{
MyMesh reboot(&fs);
assert(reboot.retained().empty()); // Always clear retained RTC bytes first.
reboot.loadRoomHistory();assert(reboot.retained()==expected);
uint32_t floor=reboot.last_room_timestamp;
assert(reboot.addSystemPost(full.c_str()) && reboot.last_room_timestamp>floor);
assert(reboot.retained().back()==full && reboot.snapshotRoomHistory());
}
}
puts("RTC Room FIFO, maximum text, durable reload and catch-up passed");
}
'''
acl = (ROOT / "src/helpers/ClientACL.h").read_text()
state = "\n".join(line for line in acl.splitlines() if line.startswith("#define PERM_ACL_"))
state += "\n" + post
definitions = "\n".join(extract_braced(source, signature) for signature in (
"bool MyMesh::addPost(", "bool MyMesh::addSystemPost(", "bool MyMesh::storePost(",
"bool MyMesh::snapshotRoomHistory(", "void MyMesh::loadRoomHistory(",
"bool MyMesh::handleRoomHistoryCommand("))
definitions += "\n" + extract_braced((ROOT / "src/helpers/TxtDataHelpers.cpp").read_text(), "void StrHelper::strncpy(")
gate = extract_braced(extract_braced(source, "void MyMesh::onPeerDataRecv("), "if (flags == TXT_TYPE_PLAIN)")
with tempfile.TemporaryDirectory(prefix="room-rtc-history-") as directory:
work = Path(directory);(work / "sys").mkdir()
(work / "sys/stat.h").write_text(STAT_MOCK)
(work / "state.inc").write_text(state)
(work / "production.inc").write_text(definitions)
(work / "post_gate.inc").write_text(gate)
binary = self.compile(work, fixture)
ran = subprocess.run([str(binary)], capture_output=True, text=True, timeout=30)
self.assertEqual(ran.returncode, 0, ran.stdout + ran.stderr)
self.assertIn("durable reload and catch-up passed", ran.stdout)
if __name__ == "__main__":
unittest.main()