Files
MeshCore/test/test_sensor_tracker_client.py
T

227 lines
11 KiB
Python

#!/usr/bin/env python3
"""Execute the actual Sensor tracker with real wire, route and durable storage."""
from pathlib import Path
import os
import shutil
import subprocess
import sys
import tempfile
import unittest
from test_client_acl_infrastructure import room_mail_crypto_arguments
from test_replay_reset_integration import extract_braced
ROOT = Path(__file__).resolve().parents[1]
FIXTURE = ROOT / "test/fixtures"
SANITIZERS = (["-fsanitize=address,undefined,float-cast-overflow", "-fno-sanitize-recover=all",
"-fno-pie", "-no-pie"] if sys.platform.startswith("linux") else [])
def peer_methods(source):
policy = (ROOT / "src/helpers/MorseLoopPolicy.h").read_text(encoding="ascii")
methods = "namespace mesh {\n" + extract_braced(
policy, "inline bool morseTimingActive(").replace(
"const Radio* radio", "const ::FakeRadio* radio") + "\n}\n"
# Execute the actual foreground ordering through tracker service, including
# the Morse early return. Later sensor/advert/persistence work is unrelated.
loop = extract_braced(source, "void SensorMesh::loop()")
boundary = "\n if (next_flood_advert &&"
if loop.count(boundary) != 1:
raise AssertionError("tracker foreground service boundary changed")
methods += loop[:loop.index(boundary)] + "}\n"
# Preserve the existing local-console lease renewal, including its sender
# check. Command parsing and its timestamp normalization remain separate.
methods += "void SensorMesh::localConsoleLease(uint32_t sender_timestamp) {\n"
methods += extract_braced(source, "if (sender_timestamp == 0 && tracker.enabled)") + "\n}\n"
for signature in ("int SensorMesh::searchPeersByHash(", "void SensorMesh::getPeerSharedSecret(",
"bool SensorMesh::allowPacketForward(", "void SensorMesh::sendSelfAdvertisement("):
methods += extract_braced(source, signature) + "\n"
# Compile the complete actual authenticated tracker dispatch, including its
# full-owner fallback and ACL bounds. Ordinary application handling beyond
# that boundary is represented by a counter, rather than a second tracker.
for signature, ending in (("void SensorMesh::onPeerDataRecv(", "++ordinary_data;\n}"),
("bool SensorMesh::onPeerPathRecv(", "++ordinary_paths; return false;\n}")):
function = extract_braced(source, signature)
boundary = " ClientInfo* from = acl.getClientByIdx(i);"
if boundary not in function:
raise AssertionError("ordinary ACL callback boundary changed")
methods += function[:function.index(boundary)] + ending + "\n"
return methods
class SensorTrackerClientTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.compiler = os.environ.get("CXX") or shutil.which("g++") or shutil.which("clang++")
if cls.compiler is None:
raise unittest.SkipTest("a host C++ compiler is required")
cls.temp = tempfile.TemporaryDirectory(prefix="meshcore-tracker-client-")
cls.work = Path(cls.temp.name)
cls.tracker = (ROOT / "examples/simple_sensor/Tracker.cpp").read_text(encoding="ascii")
cls.sensor = (ROOT / "examples/simple_sensor/SensorMesh.cpp").read_text(encoding="ascii")
cls.crypto = room_mail_crypto_arguments("room")
cls.binary = cls.build("production", cls.tracker, cls.sensor)
@classmethod
def tearDownClass(cls):
cls.temp.cleanup()
@classmethod
def build(cls, name, tracker, sensor):
work = cls.work / name
work.mkdir()
(work / "SensorMesh.h").write_text('#include "sensor_tracker_client.h"\n', encoding="ascii")
(work / "helpers").mkdir()
# The production watchdog header imports hardware identity/filesystem
# dependencies. Its sole Tracker API is supplied by this radio adapter.
(work / "helpers/UsbLoggingWatchdog.h").write_text(
"#pragma once\nnamespace mesh { bool isUsbLoggingWatchdogArmed(); }\n",
encoding="ascii")
(work / "Tracker.cpp").write_text(tracker, encoding="ascii")
fixture = (FIXTURE / "sensor_tracker_client.cpp").read_text(encoding="ascii")
cpp = work / "client.cpp"
cpp.write_text(fixture.replace("@PEER_METHODS@", peer_methods(sensor)), encoding="ascii")
binary = work / "client"
built = subprocess.run([
cls.compiler, "-std=c++17", "-Wall", "-Wextra", "-Werror", "-Wno-unused-parameter",
*SANITIZERS, "-I", str(work), "-I", str(FIXTURE), "-I", str(ROOT / "src"),
str(cpp), str(ROOT / "src/Packet.cpp"), *cls.crypto, "-o", str(binary),
], text=True, capture_output=True, timeout=60)
if built.returncode:
raise AssertionError(built.stdout + built.stderr)
return binary
def run_group(self, name, binary=None, success=True):
run = subprocess.run([str(binary or self.binary), name], text=True, capture_output=True, timeout=20)
if success:
self.assertEqual(run.returncode, 0, run.stdout + run.stderr)
self.assertIn("PASS: sensor tracker client " + name, run.stdout)
else:
self.assertNotEqual(run.returncode, 0, "regression mutation escaped the assertions")
self.assertIn("Assertion", run.stderr)
def test_local_commands_and_durable_settings(self):
self.run_group("commands")
def test_safe_lost_unknown_intervals_and_awake_lease(self):
self.run_group("cadence")
def test_awake_lease_strict_expiry_wrap_parked_and_morse_paths(self):
self.run_group("awake-expiry")
def test_expiry_preserves_other_owners_and_console_renewal(self):
self.run_group("awake-owners")
def test_expired_awake_lease_stays_retired_during_26_days_of_checks(self):
self.run_group("awake-long")
def test_negative_control_expired_awake_lease_resurrects(self):
guard = extract_braced(self.sensor,
"if (tracker_awake_until && millisHasNowPassed(tracker_awake_until))")
mutated = self.sensor.replace(guard, "", 1)
self.run_group("awake-long", self.build("retained-awake-lease", self.tracker, mutated), success=False)
def test_negative_control_parked_radio_must_retire_expired_lease(self):
guard = "if (tracker_awake_until && millisHasNowPassed(tracker_awake_until))"
self.assertEqual(self.sensor.count(guard), 1)
mutated = self.sensor.replace(guard,
"if (!tracker_radio_asleep && tracker_awake_until && millisHasNowPassed(tracker_awake_until))", 1)
self.run_group("awake-expiry", self.build("parked-awake-lease", self.tracker, mutated), success=False)
def test_negative_control_morse_cannot_defer_lease_retirement(self):
guard = extract_braced(self.sensor,
"if (tracker_awake_until && millisHasNowPassed(tracker_awake_until))")
mutated = self.sensor.replace(guard, "", 1)
after_morse = " _clock_sync.loop();"
self.assertEqual(mutated.count(after_morse), 1)
mutated = mutated.replace(after_morse, guard + "\n" + after_morse, 1)
self.run_group("awake-expiry", self.build("morse-awake-lease", self.tracker, mutated), success=False)
def test_reply_window_starts_after_transmission_and_wraps(self):
self.run_group("timing")
def test_full_owner_authentication_and_nonce_isolation(self):
self.run_group("identity")
def test_learn_paths_only_from_accepted_matched_responses(self):
self.run_group("paths")
def test_tracker_suppresses_normal_adverts_alerts_and_forwarding(self):
self.run_group("suppression")
def test_six_hour_flood_budget_repeater_postponement_and_four_day_lock(self):
self.run_group("floods")
def test_storage_faults_and_rebooted_reservation_never_flood(self):
self.run_group("durability")
def test_queue_and_inflight_cancellation_ownership(self):
self.run_group("ownership")
def test_gps_timeout_invalid_fixes_and_maintenance_awake_guards(self):
self.run_group("gps")
def test_reboot_preserves_nonce_floor_and_conservative_flood_wait(self):
self.run_group("reboot")
def test_idle_clock_rollback_and_response_crossing_four_day_cutoff(self):
self.run_group("clock-cutoff")
def test_failed_warm_and_hard_wakes_restore_dispatcher_liveness(self):
self.run_group("wake")
def test_zero_rtc_allows_gps_sync_without_radio_or_storage_fault(self):
self.run_group("zero-clock")
def test_forward_utc_correction_requires_real_six_hour_quiet_wait(self):
self.run_group("forward-clock")
def test_gps_manager_owns_acquisition_completion_and_final_fix(self):
self.run_group("gps-handoff")
def test_negative_control_original_normal_traffic_is_detected(self):
guard = " if (isTrackerModeEnabled()) return false;"
advert = " if (isTrackerModeEnabled()) return;"
forward_function = extract_braced(self.sensor, "bool SensorMesh::allowPacketForward(")
advert_function = extract_braced(self.sensor, "void SensorMesh::sendSelfAdvertisement(")
self.assertEqual(forward_function.count(guard), 1)
self.assertEqual(advert_function.count(advert), 1)
mutated = self.sensor.replace(forward_function, forward_function.replace(guard, "", 1), 1)
mutated = mutated.replace(advert_function, advert_function.replace(advert, "", 1), 1)
self.run_group("suppression", self.build("no-suppression", self.tracker, mutated), success=False)
def test_negative_control_missing_durability_gate_is_detected(self):
guard = "if (!saveTrackerRecord(candidate)) return; // Reserve before any RF admission."
self.assertEqual(self.tracker.count(guard), 1)
mutated = self.tracker.replace(guard, "saveTrackerRecord(candidate);", 1)
self.run_group("durability", self.build("no-durability", mutated, self.sensor), success=False)
def test_negative_control_failed_wake_leaves_dispatcher_disabled(self):
guard = "if (!_radio->setTrackerSleep(false)) _radio->recoverRadio(true);"
self.assertEqual(self.tracker.count(guard), 1)
mutated = self.tracker.replace(guard, "if (!_radio->setTrackerSleep(false)) return;", 1)
self.run_group("wake", self.build("failed-wake-stuck", mutated, self.sensor), success=False)
def test_negative_control_zero_clock_allocates_invalid_nonce(self):
guard = extract_braced(self.tracker, "if (now == 0) {")
mutated = self.tracker.replace(guard, "", 1)
self.run_group("zero-clock", self.build("zero-clock-nonce", mutated, self.sensor), success=False)
def test_negative_control_forward_clock_can_manufacture_flood_wait(self):
guard = " tracker_boot_anchor = now;"
self.assertEqual(self.tracker.count(guard), 1)
mutated = self.tracker.replace(guard, "", 1)
self.run_group("forward-clock", self.build("forward-clock-flood", mutated, self.sensor), success=False)
def test_negative_control_tracker_forces_early_manager_handoff(self):
guard = "if (sensors.isTrackerGpsAcquisitionPending()) return;"
self.assertEqual(self.tracker.count(guard), 1)
early = "if (sensors.isTrackerGpsAcquisitionPending() && !millisHasNowPassed(sensors.acquisition_until)) return;"
mutated = self.tracker.replace(guard, early, 1)
self.run_group("gps-handoff", self.build("early-gps-handoff", mutated, self.sensor), success=False)
if __name__ == "__main__":
unittest.main()