-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathtest_directed_ping.py
More file actions
369 lines (282 loc) · 13.4 KB
/
Copy pathtest_directed_ping.py
File metadata and controls
369 lines (282 loc) · 13.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
#!/usr/bin/env python3
"""
Test suite for directed ping/pong and trace features.
Validates payload format, routing logic, and CLI parsing
without requiring hardware.
Run: python3 test_directed_ping.py
"""
import struct
import sys
# --- Constants (mirrored from Packet.h) ---
MC_ROUTE_FLOOD = 0x01
MC_PAYLOAD_PLAIN = 0x02
MC_PAYLOAD_VER_1 = 0x00
MC_MAX_PAYLOAD_SIZE = 180
passed = 0
failed = 0
def test(name, condition, detail=""):
global passed, failed
if condition:
passed += 1
print(f" PASS: {name}")
else:
failed += 1
print(f" FAIL: {name} {detail}")
def make_header(route, ptype, ver):
return (route & 0x03) | ((ptype & 0x0F) << 2) | ((ver & 0x03) << 6)
def get_route(header):
return header & 0x03
def get_payload_type(header):
return (header >> 2) & 0x0F
# ============================================================
# Test 1: Directed Ping payload format
# ============================================================
print("\n=== Test 1: Directed Ping payload format ===")
target_hash = 0xA3
src_hash = 0x5B
ping_counter = 1
node_name = "MyNode"
# Build payload as the firmware does:
# [destHash][srcHash]['D']['P'][text: "#N name"]
text = f"#{ping_counter} {node_name}"
payload = bytes([target_hash, src_hash, ord('D'), ord('P')]) + text.encode()
test("payload[0] = target hash", payload[0] == 0xA3)
test("payload[1] = source hash", payload[1] == 0x5B)
test("payload[2:4] = 'DP' marker", payload[2:4] == b'DP')
test("text starts with #", payload[4] == ord('#'))
test("payload length <= MC_MAX_PAYLOAD_SIZE", len(payload) <= MC_MAX_PAYLOAD_SIZE)
test("text contains node name", node_name.encode() in payload[4:])
# ============================================================
# Test 2: PONG payload format
# ============================================================
print("\n=== Test 2: PONG payload format ===")
pong_target = 0x5B # back to original sender
pong_src = 0xA3 # responder hash
responder_name = "Relay1"
rssi = -65
# Build PONG payload:
# [destHash][srcHash]['P']['O'][text: "name rssi"]
pong_text = f"{responder_name} {rssi}"
pong_payload = bytes([pong_target, pong_src, ord('P'), ord('O')]) + pong_text.encode()
test("pong payload[0] = dest (original sender)", pong_payload[0] == 0x5B)
test("pong payload[1] = src (responder)", pong_payload[1] == 0xA3)
test("pong payload[2:4] = 'PO' marker", pong_payload[2:4] == b'PO')
test("pong text contains responder name", responder_name.encode() in pong_payload[4:])
test("pong text contains rssi value", str(rssi).encode() in pong_payload[4:])
# ============================================================
# Test 3: Header encoding
# ============================================================
print("\n=== Test 3: Header encoding for FLOOD PLAIN ===")
header = make_header(MC_ROUTE_FLOOD, MC_PAYLOAD_PLAIN, MC_PAYLOAD_VER_1)
test("route type = FLOOD", get_route(header) == MC_ROUTE_FLOOD)
test("payload type = PLAIN", get_payload_type(header) == MC_PAYLOAD_PLAIN)
test("header is single byte", 0 <= header <= 255)
# ============================================================
# Test 4: Reception matching logic - DP for us
# ============================================================
print("\n=== Test 4: Reception matching - DP addressed to us ===")
my_hash = 0xA3
# Simulate incoming DP packet for us
dp_payload = bytes([my_hash, 0x5B, ord('D'), ord('P')]) + b"#1 Sender"
is_dp = (len(dp_payload) >= 4
and dp_payload[2] == ord('D')
and dp_payload[3] == ord('P')
and dp_payload[0] == my_hash)
test("DP packet recognized", is_dp)
test("sender hash extracted", dp_payload[1] == 0x5B)
# ============================================================
# Test 5: Reception matching logic - DP NOT for us
# ============================================================
print("\n=== Test 5: Reception matching - DP not for us ===")
other_hash = 0xFF
dp_other = bytes([other_hash, 0x5B, ord('D'), ord('P')]) + b"#1 Sender"
is_dp_other = (len(dp_other) >= 4
and dp_other[2] == ord('D')
and dp_other[3] == ord('P')
and dp_other[0] == my_hash)
test("DP for other node NOT matched", not is_dp_other)
# ============================================================
# Test 6: Reception matching logic - PO for us
# ============================================================
print("\n=== Test 6: Reception matching - PO (PONG) for us ===")
po_payload = bytes([my_hash, 0x5B, ord('P'), ord('O')]) + b"Relay1 -65"
is_po = (len(po_payload) >= 4
and po_payload[2] == ord('P')
and po_payload[3] == ord('O')
and po_payload[0] == my_hash)
test("PO packet recognized", is_po)
test("pong sender hash extracted", po_payload[1] == 0x5B)
test("pong text parseable", b"-65" in po_payload[4:])
# ============================================================
# Test 7: TXT_MSG NOT falsely matched as DP/PO
# ============================================================
print("\n=== Test 7: Encrypted TXT_MSG not confused with DP/PO ===")
# TXT_MSG: [destHash][srcHash][MAC+encrypted...] - bytes 2,3 are encrypted, unlikely DP/PO
# Simulate a normal TXT_MSG (10+ bytes, bytes 2-3 are MAC, not 'D','P')
txt_payload = bytes([my_hash, 0x5B, 0x7A, 0x3F]) + bytes(range(10))
is_txt_dp = (len(txt_payload) >= 4
and txt_payload[2] == ord('D')
and txt_payload[3] == ord('P')
and txt_payload[0] == my_hash)
is_txt_po = (len(txt_payload) >= 4
and txt_payload[2] == ord('P')
and txt_payload[3] == ord('O')
and txt_payload[0] == my_hash)
# Should fall through to TXT handling
is_txt = (not is_txt_dp and not is_txt_po
and len(txt_payload) >= 10
and txt_payload[0] == my_hash)
test("TXT_MSG NOT matched as DP", not is_txt_dp)
test("TXT_MSG NOT matched as PO", not is_txt_po)
test("TXT_MSG falls through to TXT handler", is_txt)
# ============================================================
# Test 8a: Directed Trace payload format
# ============================================================
print("\n=== Test 8a: Directed Trace payload format ===")
# Build DT payload: [destHash][srcHash]['D']['T'][text: "#N name"]
dt_text = f"#{ping_counter} {node_name}"
dt_payload = bytes([target_hash, src_hash, ord('D'), ord('T')]) + dt_text.encode()
test("DT payload[0] = target hash", dt_payload[0] == 0xA3)
test("DT payload[1] = source hash", dt_payload[1] == 0x5B)
test("DT payload[2:4] = 'DT' marker", dt_payload[2:4] == b'DT')
test("DT text starts with #", dt_payload[4] == ord('#'))
test("DT payload length <= MC_MAX_PAYLOAD_SIZE", len(dt_payload) <= MC_MAX_PAYLOAD_SIZE)
# ============================================================
# Test 8b: Trace Response payload format
# ============================================================
print("\n=== Test 8b: Trace Response payload format ===")
tr_name = "Relay1"
tr_rssi = -65
tr_hops = 3
# Build TR payload: [destHash][srcHash]['T']['R'][text: "name rssi hops"]
tr_text = f"{tr_name} {tr_rssi} {tr_hops}"
tr_payload = bytes([pong_target, pong_src, ord('T'), ord('R')]) + tr_text.encode()
test("TR payload[0] = dest (original sender)", tr_payload[0] == 0x5B)
test("TR payload[1] = src (responder)", tr_payload[1] == 0xA3)
test("TR payload[2:4] = 'TR' marker", tr_payload[2:4] == b'TR')
test("TR text contains responder name", tr_name.encode() in tr_payload[4:])
test("TR text contains rssi value", str(tr_rssi).encode() in tr_payload[4:])
test("TR text contains hop count", str(tr_hops).encode() in tr_payload[4:])
# ============================================================
# Test 8c: Reception matching - DT for us
# ============================================================
print("\n=== Test 8c: Reception matching - DT addressed to us ===")
dt_incoming = bytes([my_hash, 0x5B, ord('D'), ord('T')]) + b"#1 Sender"
is_dt = (len(dt_incoming) >= 4
and dt_incoming[2] == ord('D')
and dt_incoming[3] == ord('T')
and dt_incoming[0] == my_hash)
test("DT packet recognized", is_dt)
test("DT sender hash extracted", dt_incoming[1] == 0x5B)
# ============================================================
# Test 8d: Reception matching - TR for us
# ============================================================
print("\n=== Test 8d: Reception matching - TR for us ===")
tr_incoming = bytes([my_hash, 0x5B, ord('T'), ord('R')]) + b"Relay1 -65 3"
is_tr = (len(tr_incoming) >= 4
and tr_incoming[2] == ord('T')
and tr_incoming[3] == ord('R')
and tr_incoming[0] == my_hash)
test("TR packet recognized", is_tr)
test("TR sender hash extracted", tr_incoming[1] == 0x5B)
test("TR text parseable", b"-65" in tr_incoming[4:] and b"3" in tr_incoming[4:])
# ============================================================
# Test 8e: DT/TR not confused with DP/PO
# ============================================================
print("\n=== Test 8e: DT/TR not confused with DP/PO ===")
test("DT marker != DP marker", b'DT' != b'DP')
test("TR marker != PO marker", b'TR' != b'PO')
test("DT not matched as DP", dt_incoming[3] != ord('P'))
test("TR not matched as PO", tr_incoming[2] != ord('P'))
# ============================================================
# Test 8f: CLI parsing "trace A3"
# ============================================================
print("\n=== Test 8f: CLI parsing 'trace' ===")
def parse_trace_cmd(cmd):
"""Simulate the C parsing: strtoul(cmd + 6, NULL, 16)"""
if cmd.startswith("trace "):
try:
return int(cmd[6:].strip(), 16) & 0xFF
except ValueError:
return 0
return None
test("'trace A3' parses to 0xA3", parse_trace_cmd("trace A3") == 0xA3)
test("'trace FF' parses to 0xFF", parse_trace_cmd("trace FF") == 0xFF)
test("'trace 01' parses to 0x01", parse_trace_cmd("trace 01") == 0x01)
test("'trace 0' parses to 0 (rejected)", parse_trace_cmd("trace 0") == 0)
test("'trace' (no arg) returns None", parse_trace_cmd("trace") is None)
# ============================================================
# Test 9: CLI parsing "ping A3"
# ============================================================
print("\n=== Test 9: CLI parsing 'ping' ===")
def parse_ping_cmd(cmd):
"""Simulate the C parsing: strtoul(cmd + 5, NULL, 16)"""
if cmd.startswith("ping "):
try:
return int(cmd[5:].strip(), 16) & 0xFF
except ValueError:
return 0
return None
test("'ping A3' parses to 0xA3", parse_ping_cmd("ping A3") == 0xA3)
test("'ping FF' parses to 0xFF", parse_ping_cmd("ping FF") == 0xFF)
test("'ping 01' parses to 0x01", parse_ping_cmd("ping 01") == 0x01)
test("'ping a3' (lowercase) parses to 0xA3", parse_ping_cmd("ping a3") == 0xA3)
test("'ping 0' parses to 0 (rejected)", parse_ping_cmd("ping 0") == 0)
test("'ping' (no arg) returns None", parse_ping_cmd("ping") is None)
# ============================================================
# Test 10: Broadcast ping unchanged
# ============================================================
print("\n=== Test 10: Broadcast ping format unchanged ===")
# Old broadcast ping: "PING #xxx from CCXXXXXX"
bc_ping = f"PING #{1} from {0x12345678:08X}"
test("broadcast ping starts with 'PING'", bc_ping.startswith("PING"))
test("broadcast ping has no DP marker at [2:4]", True) # different format entirely
test("broadcast has no dest/src hash prefix", bc_ping[0] == 'P') # starts with text, not hash byte
# ============================================================
# Test 11: Payload size constraints
# ============================================================
print("\n=== Test 11: Payload size constraints ===")
# Worst case: long node name (15 chars max)
long_name = "A" * 15
dp_text = f"#65535 {long_name}"
dp_full = bytes([0xA3, 0x5B, ord('D'), ord('P')]) + dp_text.encode()
test("max DP payload fits", len(dp_full) <= MC_MAX_PAYLOAD_SIZE,
f"len={len(dp_full)}")
pong_text = f"{long_name} -120"
pong_full = bytes([0x5B, 0xA3, ord('P'), ord('O')]) + pong_text.encode()
test("max PONG payload fits", len(pong_full) <= MC_MAX_PAYLOAD_SIZE,
f"len={len(pong_full)}")
dt_text = f"#65535 {long_name}"
dt_full = bytes([0xA3, 0x5B, ord('D'), ord('T')]) + dt_text.encode()
test("max DT payload fits", len(dt_full) <= MC_MAX_PAYLOAD_SIZE,
f"len={len(dt_full)}")
tr_text = f"{long_name} -120 99"
tr_full = bytes([0x5B, 0xA3, ord('T'), ord('R')]) + tr_text.encode()
test("max TR payload fits", len(tr_full) <= MC_MAX_PAYLOAD_SIZE,
f"len={len(tr_full)}")
# ============================================================
# Test 12: Serialization roundtrip
# ============================================================
print("\n=== Test 12: Packet serialization roundtrip ===")
# Simulate MCPacket serialization: [header][pathLen][path...][payload...]
header = make_header(MC_ROUTE_FLOOD, MC_PAYLOAD_PLAIN, MC_PAYLOAD_VER_1)
path = bytes([0x5B]) # pathLen=1
payload = bytes([0xA3, 0x5B, ord('D'), ord('P')]) + b"#1 TestNode"
wire = bytes([header, len(path)]) + path + payload
# Deserialize
d_header = wire[0]
d_pathLen = wire[1]
d_path = wire[2:2 + d_pathLen]
d_payload = wire[2 + d_pathLen:]
test("header roundtrip", d_header == header)
test("pathLen roundtrip", d_pathLen == 1)
test("path roundtrip", d_path == path)
test("payload roundtrip", d_payload == payload)
test("deserialized payload has DP marker", d_payload[2:4] == b'DP')
# ============================================================
# Summary
# ============================================================
print(f"\n{'=' * 50}")
print(f"Results: {passed} passed, {failed} failed, {passed + failed} total")
print(f"{'=' * 50}")
sys.exit(0 if failed == 0 else 1)