mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-10-20 17:22:01 +00:00
Acquire fresh GPS and validated UTC, check an authenticated owner Companion, and apply saved lost-status cadence with bounded awake windows. Persist request tags, routes, and six-hour flood reservations; preserve the four-day cutoff across sleep and restart. Keep Dispatcher recovery active after a failed radio wake, honor denied flood retries after lost TxDone, and qualify GPS ownership and clock corrections. Add executable regression tests and nRF52/ESP32 Sensor build jobs. Validation: six firmware builds, 1829 native test cases, sanitized tracker/storage/GPS/sleep/recovery tests, and strict documentation build. Bluetooth Find My advertising and automatic Room mailbox polling remain separate work.
182 lines
8.7 KiB
Python
182 lines
8.7 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):
|
|
methods = ""
|
|
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_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()
|