Files
MeshCore/test/test_ethernet_kiss_probe.py
T

127 lines
5.2 KiB
Python

"""Host-only regression tests; no radio, network connection or TAP is opened."""
import contextlib
import importlib.util
import io
from pathlib import Path
import queue
import struct
import sys
import time
import unittest
from unittest.mock import patch
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "tools/hil"))
SPEC = importlib.util.spec_from_file_location("ethernet_kiss_probe", ROOT / "tools/hil/ethernet_kiss_probe.py")
probe = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(probe)
class FakeEndpoint:
def __init__(self):
self.incoming = queue.Queue()
self.peer = None
self.drop = False
self.frames = []
def send(self, frame, fragment=0):
self.frames.append(frame)
if not self.drop:
self.peer.incoming.put((frame, time.monotonic()))
def receive(self, timeout):
try:
return self.incoming.get(timeout=timeout)
except queue.Empty:
return None
class EthernetKissProbeTests(unittest.TestCase):
def test_tcp_endpoint_defaults_to_dedicated_kiss_port(self):
with patch.object(probe.socket, "create_connection") as connect, \
patch.object(probe.threading, "Thread"):
endpoint = probe.KissEndpoint("tcp://bench-radio")
connect.assert_called_once_with(("bench-radio", 5003), timeout=5)
endpoint.close()
def test_streaming_kiss_preserves_all_bytes(self):
frame = bytes(range(256))[:255]
encoded = probe.kiss_encode(frame)
decoder = probe.KissDecoder()
frames = []
for byte in encoded:
frames.extend(decoder.feed(bytes((byte,))))
self.assertEqual(frames, [frame])
self.assertEqual(decoder.errors, 0)
def test_decoder_ignores_controls_and_rejects_invalid_escapes(self):
decoder = probe.KissDecoder()
encoded = b"noise\xc0\x01control\xc0\x00\xdbbad\xc0"
self.assertEqual(decoder.feed(encoded), [])
self.assertEqual(decoder.errors, 1)
self.assertEqual(decoder.feed(probe.kiss_encode(b"valid")), [b"valid"])
self.assertEqual(decoder.feed(b"\xc0\x00" + b"X" * 300 + b"\xc0"), [])
self.assertEqual(decoder.errors, 2)
def test_ipv4_and_transport_checksums_are_valid_both_directions(self):
for kind in ("udp", "tcp", "icmp"):
for reverse in (False, True):
frame = probe.make_frame(kind, 255, 0x12345678, reverse)
self.assertEqual(len(frame), 255)
self.assertEqual(probe.checksum(frame[14:34]), 0)
self.assertEqual(struct.unpack("!H", frame[16:18])[0], 241)
transport = frame[34:]
protocol = frame[23]
pseudo = frame[26:34] + struct.pack("!BBH", 0, protocol, len(transport))
self.assertEqual(probe.checksum(transport if kind == "icmp" else pseudo + transport), 0)
self.assertIn(b"\xc0\xdb", transport)
def test_arp_and_boundaries_are_distinct(self):
arp = probe.make_frame("arp", 60, 1)
self.assertEqual(len(arp), 60)
self.assertEqual(arp[:6], b"\xff" * 6)
self.assertEqual(arp[12:14], b"\x08\x06")
self.assertNotEqual(arp, probe.make_frame("arp", 60, 2))
self.assertNotEqual(probe.make_frame("raw", 14, 1), probe.make_frame("raw", 14, 2))
for kind, size in probe.CASES:
self.assertEqual(len(probe.make_frame(kind, size, 1)), size)
for kind, size in (("raw", 13), ("udp", 41), ("tcp", 53), ("icmp", 256)):
with self.assertRaises(ValueError):
probe.make_frame(kind, size, 1)
def test_probe_exact_integrity_and_both_directions(self):
for duplex in (False, True):
a, b = FakeEndpoint(), FakeEndpoint()
a.peer, b.peer = b, a
result = probe.run_probe(a, b, count=2, timeout=0.1, interval=0, duplex=duplex)
self.assertTrue(result["passed"])
self.assertEqual(result["counts"], {"a_to_b_attempted": 16, "a_to_b_received": 16,
"b_to_a_attempted": 16, "b_to_a_received": 16})
for transfer in result["transfers"]:
self.assertEqual(transfer["sent_hex"], transfer["received_hex"])
self.assertGreaterEqual(transfer["latency_ms"], 0)
def test_lost_frames_are_not_retried_or_hidden(self):
a, b = FakeEndpoint(), FakeEndpoint()
a.peer, b.peer = b, a
a.drop = True
b.incoming.put((b"foreign traffic", time.monotonic()))
result = probe.run_probe(a, b, count=1, timeout=0.001, interval=0)
self.assertFalse(result["passed"])
self.assertEqual(result["counts"]["a_to_b_received"], 0)
self.assertEqual(result["counts"]["b_to_a_received"], 8)
self.assertEqual(len(a.frames), 8)
self.assertEqual(len(result["unexpected"]), 1)
self.assertEqual(result["retries"], 0)
def test_plan_does_not_open_endpoints(self):
with patch.object(probe, "KissEndpoint") as endpoint, contextlib.redirect_stdout(io.StringIO()):
self.assertEqual(probe.main(["--plan"]), 0)
endpoint.assert_not_called()
if __name__ == "__main__":
unittest.main()