mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-10-20 16:12:38 +00:00
196 lines
10 KiB
Python
196 lines
10 KiB
Python
#!/usr/bin/env python3
|
|
"""Run actual Sensor response handlers, history aggregation and packet builders."""
|
|
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_client_acl_infrastructure import room_mail_crypto_arguments
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
FIXTURE = ROOT / "test/fixtures/sensor_reply_bounds/test_sensor_reply_bounds.cpp"
|
|
|
|
|
|
def production_methods(transform=None, room_transform=None):
|
|
source = (ROOT / "examples/simple_sensor/SensorMesh.cpp").read_text()
|
|
if transform:
|
|
source = transform(source)
|
|
handler = extract_braced(source, "uint8_t SensorMesh::handleRequest(")
|
|
begin = handler.index(" if (req_type == REQ_TYPE_GET_TELEMETRY_DATA")
|
|
generated = extract_braced(source, "static uint8_t getTelemetryPermissions(") + "\n"
|
|
if "static size_t boundedTelemPrefix(" in source:
|
|
generated += extract_braced(source, "static size_t boundedTelemPrefix(") + "\n"
|
|
generated += handler[:begin]
|
|
for signature in ("if (req_type == REQ_TYPE_GET_TELEMETRY_DATA", "if (req_type == REQ_TYPE_GET_AVG_MIN_MAX"):
|
|
generated += extract_braced(handler, signature) + "\n"
|
|
generated += "return 0;\n}\n"
|
|
room = (ROOT / "examples/simple_room_server/MyMesh.cpp").read_text()
|
|
if room_transform:
|
|
room = room_transform(room)
|
|
room_handler = extract_braced(room, "int MyMesh::handleRequest(")
|
|
room_begin = room_handler.index(" if (payload[0] == REQ_TYPE_GET_STATUS)")
|
|
generated += "#undef REQ_TYPE_GET_TELEMETRY_DATA\n#define REQ_TYPE_GET_TELEMETRY_DATA ROOM_REQ_TYPE_GET_TELEMETRY_DATA\n"
|
|
generated += room_handler[:room_begin]
|
|
generated += extract_braced(room_handler, "if (payload[0] == REQ_TYPE_GET_TELEMETRY_DATA)") + "\nreturn 0;\n}\n"
|
|
generated += "#undef REQ_TYPE_GET_TELEMETRY_DATA\n#define REQ_TYPE_GET_TELEMETRY_DATA SENSOR_REQ_TYPE_GET_TELEMETRY_DATA\n"
|
|
history = (ROOT / "examples/simple_sensor/TimeSeriesData.cpp").read_text()
|
|
generated += extract_braced(history, "void TimeSeriesData::calcMinMaxAvg(") + "\n"
|
|
mesh = (ROOT / "src/Mesh.cpp").read_text()
|
|
generated += "namespace mesh {\n#define MAX_COMBINED_PATH (MAX_PACKET_PAYLOAD - 2 - CIPHER_BLOCK_SIZE)\n"
|
|
for signature in ("Packet* Mesh::createPathReturn(const Identity& dest,",
|
|
"Packet* Mesh::createPathReturn(const uint8_t* dest_hash,", "Packet* Mesh::createDatagram("):
|
|
generated += extract_braced(mesh, signature) + "\n"
|
|
packet = (ROOT / "src/Packet.cpp").read_text()
|
|
for signature in ("Packet::Packet()", "bool Packet::isValidPathLen("):
|
|
generated += extract_braced(packet, signature) + "\n"
|
|
return generated + "}\n"
|
|
|
|
|
|
def production_capacities(sensor_transform=None, room_transform=None):
|
|
definitions = []
|
|
for role, path, transform in (
|
|
("SENSOR", "examples/simple_sensor/SensorMesh.cpp", sensor_transform),
|
|
("ROOM", "examples/simple_room_server/MyMesh.cpp", room_transform),
|
|
):
|
|
source = (ROOT / path).read_text()
|
|
if transform:
|
|
source = transform(source)
|
|
matches = re.findall(r"\btelemetry\(([^)\n]+)\)", source)
|
|
if len(matches) != 1:
|
|
raise AssertionError("one actual telemetry constructor required: " + path)
|
|
definitions.append("#define " + role + "_TELEMETRY_CAPACITY (" + matches[0] + ")")
|
|
opcode = re.search(r"#define REQ_TYPE_GET_TELEMETRY_DATA\s+(0x[0-9a-fA-F]+)", source).group(1)
|
|
definitions.append("#define " + role + "_REQ_TYPE_GET_TELEMETRY_DATA " + opcode)
|
|
return "\n".join(definitions) + "\n"
|
|
|
|
|
|
def production_telemetry_access():
|
|
source = (ROOT / "src/helpers/CommonCLI.h").read_text()
|
|
definitions = []
|
|
for mode in ("ALL", "ACL"):
|
|
matches = re.findall(r"^#define TELEMETRY_ACCESS_" + mode + r"\s+[0-9]+\s*$", source, re.M)
|
|
if len(matches) != 1:
|
|
raise AssertionError("one production telemetry access mode required: " + mode)
|
|
definitions.append(matches[0].strip())
|
|
return "\n".join(definitions) + "\n"
|
|
|
|
|
|
class SensorReplyBoundsTest(unittest.TestCase):
|
|
def execute(self, case, transform=None, room_transform=None):
|
|
compiler = shutil.which("g++") or shutil.which("clang++")
|
|
self.assertIsNotNone(compiler, "host C++17 compiler required")
|
|
with tempfile.TemporaryDirectory(prefix="sensor-reply-bounds-") as directory:
|
|
work = Path(directory)
|
|
(work / "telemetry_capacity.inc").write_text(production_capacities(transform, room_transform), encoding="ascii")
|
|
(work / "telemetry_access.inc").write_text(production_telemetry_access(), encoding="ascii")
|
|
(work / "production.inc").write_text(production_methods(transform, room_transform), encoding="ascii")
|
|
history_types = (ROOT / "examples/simple_sensor/TimeSeriesData.h").read_text()
|
|
history_types = "\n".join(line for line in history_types.splitlines() if not line.startswith("#"))
|
|
(work / "history_types.inc").write_text(history_types, encoding="ascii")
|
|
binary = work / "sensor.exe"
|
|
command = [compiler, "-std=c++17", "-Wall", "-Wextra", "-Werror", "-Wno-unused-parameter", "-Wno-reorder",
|
|
"-I" + str(work), "-I" + str(ROOT / "src"), str(FIXTURE),
|
|
*room_mail_crypto_arguments("room"), "-o", str(binary)]
|
|
if sys.platform.startswith("linux"):
|
|
command[1:1] = ["-fsanitize=address,undefined,float-cast-overflow", "-fno-sanitize-recover=all",
|
|
"-fno-pie", "-no-pie"]
|
|
built = subprocess.run(command, capture_output=True, text=True, timeout=60)
|
|
self.assertEqual(built.returncode, 0, built.stdout + built.stderr)
|
|
return subprocess.run([str(binary), case], capture_output=True, text=True, timeout=30)
|
|
|
|
def check_case(self, case):
|
|
run = self.execute(case)
|
|
self.assertEqual(run.returncode, 0, run.stdout + run.stderr)
|
|
self.assertIn("Sensor response bounds and packet admission passed", run.stdout)
|
|
|
|
def test_room_telemetry_entries_fit_all_routes_and_preserve_composites(self):
|
|
self.check_case("room")
|
|
|
|
def test_room_board_short_response_never_returns_a_tag_only_reply(self):
|
|
self.check_case("room.board")
|
|
|
|
def test_old_room_board_tag_only_response_is_rejected(self):
|
|
def old(source):
|
|
branch = extract_braced(source, "if (payload[0] == mesh::ROOM_BOARD_REQUEST_SUBTYPE)")
|
|
changed = branch.replace("return length ? 4 + length : 0;", "return 4 + length;", 1)
|
|
self.assertNotEqual(changed, branch)
|
|
return source.replace(branch, changed, 1)
|
|
self.assertNotEqual(self.execute("room.board", room_transform=old).returncode, 0)
|
|
|
|
def test_room_mailbox_admission_and_complete_reply_boundaries(self):
|
|
self.check_case("room.mail")
|
|
|
|
def test_old_room_mail_tag_only_response_is_rejected(self):
|
|
def old(source):
|
|
branch = extract_braced(source, "if (payload[0] == mesh::ROOM_MAIL_REQUEST_SUBTYPE)")
|
|
changed = branch.replace("return length ? 4 + length : 0;", "return 4 + length;")
|
|
self.assertNotEqual(changed, branch)
|
|
return source.replace(branch, changed, 1)
|
|
self.assertNotEqual(self.execute("room.mail", room_transform=old).returncode, 0)
|
|
|
|
def test_old_room_unbounded_copy_is_rejected(self):
|
|
def old(source):
|
|
begin = source.index(" const uint8_t* tbuf = telemetry.getBuffer();")
|
|
end = source.index(" return 4 + tlen; // reply_len", begin) + len(" return 4 + tlen; // reply_len")
|
|
return source[:begin] + """ uint8_t tlen = telemetry.getSize();
|
|
memcpy(&reply_data[4], telemetry.getBuffer(), tlen);
|
|
return 4 + tlen; // reply_len""" + source[end:]
|
|
self.assertNotEqual(self.execute("room", room_transform=old).returncode, 0)
|
|
|
|
def test_telemetry_entries_fit_every_valid_direct_and_flood_route(self):
|
|
self.check_case("telemetry")
|
|
|
|
def test_history_entries_types_counts_and_empty_query_are_safe(self):
|
|
self.check_case("history")
|
|
|
|
def test_packet_builder_accepts_maximum_telemetry_collection(self):
|
|
self.check_case("packet")
|
|
|
|
def test_empty_history_query_has_no_synthetic_zero_values(self):
|
|
self.check_case("empty")
|
|
|
|
def test_telemetry_permission_modes_match_current_prefs(self):
|
|
self.check_case("permissions")
|
|
|
|
def test_old_telemetry_copy_exceeds_route_admission(self):
|
|
def old(source):
|
|
begin = source.index(" const uint8_t* tbuf = telemetry.getBuffer();")
|
|
end = source.index(" return 4 + tlen; // reply_len", begin) + len(" return 4 + tlen; // reply_len")
|
|
return source[:begin] + """ uint8_t tlen = telemetry.getSize();
|
|
memcpy(&reply_data[4], telemetry.getBuffer(), tlen);
|
|
return 4 + tlen; // reply_len""" + source[end:]
|
|
self.assertNotEqual(self.execute("telemetry", old).returncode, 0)
|
|
|
|
def test_old_history_route_bounds_are_rejected(self):
|
|
def old(source):
|
|
return source.replace(" if (item_len > reply_capacity - ofs) break;", " (void)item_len;", 1)
|
|
self.assertNotEqual(self.execute("history", old).returncode, 0)
|
|
|
|
def test_old_history_composite_types_are_rejected(self):
|
|
def old(source):
|
|
return source.replace("!LPPData::isScalarType(d->_lpp_type)\n || ", "", 1)
|
|
self.assertNotEqual(self.execute("history", old).returncode, 0)
|
|
|
|
def test_old_history_provider_count_is_rejected(self):
|
|
def old(source):
|
|
return source.replace(" if (n < 0 || n > int(sizeof(data) / sizeof(data[0]))) return 0;", "", 1)
|
|
self.assertNotEqual(self.execute("history", old).returncode, 0)
|
|
|
|
def test_old_empty_history_nonfinite_conversion_is_rejected(self):
|
|
def old(source):
|
|
return source.replace("\n || !isfinite(d->_min) || !isfinite(d->_max) || !isfinite(d->_avg)", "", 1)
|
|
self.assertNotEqual(self.execute("history", old).returncode, 0)
|
|
|
|
def test_old_history_short_response_header_is_rejected(self):
|
|
def old(source):
|
|
return source.replace(" if (reply_capacity < 8) return 0; // tag and current timestamp", "", 1)
|
|
self.assertNotEqual(self.execute("history", old).returncode, 0)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|