Files
MeshCore/test/test_room_catchup.py
T

131 lines
6.3 KiB
Python

#!/usr/bin/env python3
"""Run actual room catch-up planning against production client and post layouts."""
from datetime import datetime, timezone
from pathlib import Path
import re
import shutil
import subprocess
import sys
import tempfile
import unittest
from test_replay_reset_integration import extract_braced
ROOT = Path(__file__).resolve().parents[1]
FIXTURE = ROOT / "test/fixtures/room_catchup.cpp"
def date_reference():
rows = ['{"0", 0}', '{"4294967295", UINT32_MAX}',
'{"2106-02-07T06:28:15Z", UINT32_MAX}']
for year in range(1970, 2107):
for month, day in ((1, 1), (2, 28), (3, 1), (12, 31)):
date = datetime(year, month, day, tzinfo=timezone.utc)
timestamp = int(date.timestamp())
if timestamp <= 0xFFFFFFFF:
rows.append('{"' + date.strftime("%Y-%m-%d") + '", ' + str(timestamp) + '}')
later = date.replace(hour=12, minute=34, second=56)
if int(later.timestamp()) <= 0xFFFFFFFF:
rows.append('{"' + later.strftime("%Y-%m-%dT%H:%M:%SZ") + '", '
+ str(int(later.timestamp())) + '}')
if year % 4 == 0 and (year % 100 != 0 or year % 400 == 0):
date = datetime(year, 2, 29, tzinfo=timezone.utc)
rows.append('{"' + date.strftime("%Y-%m-%d") + '", ' + str(int(date.timestamp())) + '}')
return ",\n ".join(rows)
def production_source():
room = (ROOT / "examples/simple_room_server/MyMesh.cpp").read_text()
room_header = (ROOT / "examples/simple_room_server/MyMesh.h").read_text()
acl = (ROOT / "src/helpers/ClientACL.h").read_text()
constants = "\n".join(line for line in acl.splitlines()
if line.startswith(("#define PERM_ACL_", "#define OUT_PATH_")))
text_length = re.search(r"^#define\s+MAX_POST_TEXT_LEN\s+.*$", room_header, re.MULTILINE)
if not text_length:
raise AssertionError("production post text capacity not found")
constants += "\n" + text_length[0]
replacements = {
"@CONSTANTS@": constants,
"@CLIENT_INFO@": extract_braced(acl, "struct ClientInfo {"),
"@POST_INFO@": extract_braced(room_header, "struct PostInfo {"),
"@PROCESS_ACK@": extract_braced(room, "bool MyMesh::processAck("),
"@VALID_DATES@": date_reference(),
}
source = FIXTURE.read_text()
for marker, replacement in replacements.items():
source = source.replace(marker, replacement)
return source
class RoomCatchUpTests(unittest.TestCase):
@classmethod
def build(cls, name, helper=None):
work = Path(cls.directory.name)
include = work / name
include.mkdir()
if helper is not None:
(include / "helpers").mkdir()
(include / "helpers/RoomCatchUp.h").write_text(helper, encoding="ascii")
source = include / "test.cpp"
source.write_text(cls.generated, encoding="ascii")
binary = include / "catchup"
command = [cls.compiler, "-std=c++17", "-O1", "-Wall", "-Wextra", "-Werror", "-Wno-unused-parameter",
"-I" + str(include), "-I" + str(ROOT / "src"), str(source), "-o", str(binary)]
if sys.platform.startswith("linux"):
command[1:1] = ["-fsanitize=address,undefined", "-fno-sanitize-recover=all",
"-fno-omit-frame-pointer", "-fno-pie", "-no-pie"]
checked = subprocess.run(command, capture_output=True, text=True, timeout=60)
if checked.returncode:
raise AssertionError(checked.stdout + checked.stderr)
return binary
@classmethod
def setUpClass(cls):
cls.compiler = shutil.which("g++") or shutil.which("clang++")
if not cls.compiler:
raise AssertionError("a host C++17 compiler is required")
cls.directory = tempfile.TemporaryDirectory(prefix="room-catchup-")
cls.addClassCleanup(cls.directory.cleanup)
cls.generated = production_source()
cls.binary = cls.build("production")
def run_case(self, case, expected):
checked = subprocess.run([str(self.binary), case], capture_output=True, text=True, timeout=20)
self.assertEqual(checked.returncode, 0, checked.stdout + checked.stderr)
self.assertIn(expected, checked.stdout)
def test_before_skipnext_keepnewest_ring_order_and_author_exclusion(self):
self.run_case("counts", "all modes order cyclic retention, exclude own posts, and bound future selections passed")
def test_epoch_limit_empty_retention_and_duplicate_timestamps(self):
self.run_case("bounds", "uint32 limits, empty retention, bounded input, and equal-timestamp accounting passed")
def test_actual_client_and_ack_handler_preserve_topic_and_quotas(self):
self.run_case("pending", "actual client application clears skipped posts, preserves topics and quotas, and rejects stale ACKs passed")
def test_strict_dates_match_python_utc_reference_and_reject_invalid_input(self):
self.run_case("dates", "strict UTC Gregorian and uint32 parsing match independent epoch references passed")
def test_randomized_plans_match_independent_ordered_reference(self):
self.run_case("random", "15000 plans match independent ordered reference without mutating retention passed")
def test_negative_controls_detect_self_posts_date_boundary_and_uncleared_ack(self):
helper = (ROOT / "src/helpers/RoomCatchUp.h").read_text()
controls = (
("self-posts", "&& !post.author.matches(client.id)", "", "counts"),
("date-boundary", "post.post_timestamp < value", "post.post_timestamp <= value", "counts"),
("pending-ack", "client.extra.room.pending_ack = 0;", "(void)client.extra.room.pending_ack;", "pending"),
)
for name, before, after, case in controls:
with self.subTest(control=name):
self.assertIn(before, helper, "update the negative control to the production seam")
binary = self.build(name, helper.replace(before, after))
checked = subprocess.run([str(binary), case], capture_output=True, text=True, timeout=20)
self.assertNotEqual(checked.returncode, 0, "negative control missed " + name)
self.assertIn("Assertion", checked.stderr, checked.stdout + checked.stderr)
if __name__ == "__main__":
unittest.main()