Skip to content

Commit c91fd30

Browse files
committed
minor change to frag ack callback handling, added test case for fragmentation
1 parent 04ae89b commit c91fd30

File tree

2 files changed

+196
-1
lines changed

2 files changed

+196
-1
lines changed

bellows/ezsp/protocol.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,10 +212,13 @@ def __call__(self, data: bytes) -> None:
212212
fragment_index=fragment_index,
213213
payload=result[7],
214214
)
215+
if not hasattr(self, "_ack_tasks"):
216+
self._ack_tasks = set()
215217
ack_task = asyncio.create_task(
216218
self._send_fragment_ack(sender, aps_frame, frag_count, frag_index)
217219
) # APS Ack
218-
ack_task.add_done_callback(self._ack_tasks.remove)
220+
self._ack_tasks.add(ack_task)
221+
ack_task.add_done_callback(lambda t: self._ack_tasks.discard(t))
219222

220223
if not complete:
221224
# Do not pass partial data up the stack

tests/test_fragmentation.py

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
import pytest
2+
3+
from bellows.ezsp.fragmentation import FragmentManager
4+
5+
6+
@pytest.fixture
7+
def frag_manager():
8+
return FragmentManager()
9+
10+
11+
@pytest.mark.asyncio
12+
async def test_single_fragment_complete(frag_manager):
13+
# If we receive a single-fragment message, the fragemnt manager should immediately report completion.
14+
15+
key = (0x1234, 0xAB, 0x1234, 0x5678)
16+
fragment_count = 1
17+
fragment_index = 0
18+
payload = b"Single fragment"
19+
20+
(
21+
complete,
22+
reassembled,
23+
returned_frag_count,
24+
returned_frag_index,
25+
) = frag_manager.handle_incoming_fragment(
26+
sender_nwk=key[0],
27+
aps_sequence=key[1],
28+
profile_id=key[2],
29+
cluster_id=key[3],
30+
fragment_count=fragment_count,
31+
fragment_index=fragment_index,
32+
payload=payload,
33+
)
34+
35+
assert complete is True
36+
assert reassembled == payload
37+
assert returned_frag_count == fragment_count
38+
assert returned_frag_index == fragment_index
39+
assert key not in frag_manager._partial
40+
assert key not in frag_manager._cleanup_timers
41+
42+
43+
@pytest.mark.asyncio
44+
async def test_two_fragments_in_order(frag_manager):
45+
# A two-fragment message should remain partial until we've received both pieces.
46+
key = (0x1111, 0x01, 0x9999, 0x2222)
47+
fragment_count = 2
48+
49+
# First fragment
50+
(
51+
complete,
52+
reassembled,
53+
returned_frag_count,
54+
returned_frag_index,
55+
) = frag_manager.handle_incoming_fragment(
56+
sender_nwk=key[0],
57+
aps_sequence=key[1],
58+
profile_id=key[2],
59+
cluster_id=key[3],
60+
fragment_count=fragment_count,
61+
fragment_index=0,
62+
payload=b"Frag0-",
63+
)
64+
assert complete is False
65+
assert reassembled is None
66+
assert key in frag_manager._partial
67+
assert frag_manager._partial[key].fragments_received == 1
68+
69+
# Second fragment
70+
(
71+
complete,
72+
reassembled,
73+
returned_frag_count,
74+
returned_frag_index,
75+
) = frag_manager.handle_incoming_fragment(
76+
sender_nwk=key[0],
77+
aps_sequence=key[1],
78+
profile_id=key[2],
79+
cluster_id=key[3],
80+
fragment_count=fragment_count,
81+
fragment_index=1,
82+
payload=b"Frag1",
83+
)
84+
assert complete is True
85+
assert reassembled == b"Frag0-Frag1"
86+
assert key not in frag_manager._partial
87+
assert key not in frag_manager._cleanup_timers
88+
89+
90+
@pytest.mark.asyncio
91+
async def test_out_of_order_fragments(frag_manager):
92+
# Receiving fragments in reverse order
93+
key = (0x9999, 0xCD, 0x1234, 0xABCD)
94+
fragment_count = 2
95+
96+
# Second fragment arrives first
97+
(
98+
complete,
99+
reassembled,
100+
returned_frag_count,
101+
returned_frag_index,
102+
) = frag_manager.handle_incoming_fragment(
103+
sender_nwk=key[0],
104+
aps_sequence=key[1],
105+
profile_id=key[2],
106+
cluster_id=key[3],
107+
fragment_count=fragment_count,
108+
fragment_index=1,
109+
payload=b"World",
110+
)
111+
assert not complete
112+
assert reassembled is None
113+
114+
# Then the first fragment
115+
(
116+
complete,
117+
reassembled,
118+
returned_frag_count,
119+
returned_frag_index,
120+
) = frag_manager.handle_incoming_fragment(
121+
sender_nwk=key[0],
122+
aps_sequence=key[1],
123+
profile_id=key[2],
124+
cluster_id=key[3],
125+
fragment_count=fragment_count,
126+
fragment_index=0,
127+
payload=b"Hello ",
128+
)
129+
assert complete
130+
assert reassembled == b"Hello World"
131+
132+
133+
@pytest.mark.asyncio
134+
async def test_repeated_fragments_ignored(frag_manager):
135+
# Ensure repeated arrivals of the same fragment index do not double-count or break the logic.
136+
137+
key = (0xAAA, 0xBB, 0xCCC, 0xDDD)
138+
fragment_count = 2
139+
140+
# First fragment
141+
(
142+
complete,
143+
reassembled,
144+
returned_frag_count,
145+
returned_frag_index,
146+
) = frag_manager.handle_incoming_fragment(
147+
sender_nwk=key[0],
148+
aps_sequence=key[1],
149+
profile_id=key[2],
150+
cluster_id=key[3],
151+
fragment_count=fragment_count,
152+
fragment_index=0,
153+
payload=b"first",
154+
)
155+
assert not complete
156+
assert frag_manager._partial[key].fragments_received == 1
157+
158+
# Repeat the same fragment index
159+
(
160+
complete,
161+
reassembled,
162+
returned_frag_count,
163+
returned_frag_index,
164+
) = frag_manager.handle_incoming_fragment(
165+
sender_nwk=key[0],
166+
aps_sequence=key[1],
167+
profile_id=key[2],
168+
cluster_id=key[3],
169+
fragment_count=fragment_count,
170+
fragment_index=0,
171+
payload=b"first",
172+
)
173+
assert not complete
174+
assert frag_manager._partial[key].fragments_received == 1, "Should not increment"
175+
176+
# Second fragment completes
177+
(
178+
complete,
179+
reassembled,
180+
returned_frag_count,
181+
returned_frag_index,
182+
) = frag_manager.handle_incoming_fragment(
183+
sender_nwk=key[0],
184+
aps_sequence=key[1],
185+
profile_id=key[2],
186+
cluster_id=key[3],
187+
fragment_count=fragment_count,
188+
fragment_index=1,
189+
payload=b"second",
190+
)
191+
assert complete
192+
assert reassembled == b"firstsecond"

0 commit comments

Comments
 (0)