@@ -133,3 +133,160 @@ async def test_parsing_schema_response(prot_hndl_v9):
133
133
134
134
rsp = await coro
135
135
assert rsp == GetTokenDataRsp (status = t .EmberStatus .LIBRARY_NOT_PRESENT )
136
+
137
+
138
+ async def test_send_fragment_ack (prot_hndl , caplog ):
139
+ """Test the _send_fragment_ack method."""
140
+ sender = 0x1D6F
141
+ incoming_aps = t .EmberApsFrame (
142
+ profileId = 260 ,
143
+ clusterId = 65281 ,
144
+ sourceEndpoint = 2 ,
145
+ destinationEndpoint = 2 ,
146
+ options = 33088 ,
147
+ groupId = 512 ,
148
+ sequence = 238 ,
149
+ )
150
+ fragment_count = 2
151
+ fragment_index = 0
152
+
153
+ expected_ack_frame = t .EmberApsFrame (
154
+ profileId = 260 ,
155
+ clusterId = 65281 ,
156
+ sourceEndpoint = 2 ,
157
+ destinationEndpoint = 2 ,
158
+ options = 33088 ,
159
+ groupId = ((0xFF00 ) | (fragment_index & 0xFF )),
160
+ sequence = 238 ,
161
+ )
162
+
163
+ with patch .object (prot_hndl , "sendReply" , new = AsyncMock ()) as mock_send_reply :
164
+ mock_send_reply .return_value = (t .EmberStatus .SUCCESS ,)
165
+
166
+ caplog .set_level (logging .DEBUG )
167
+ status = await prot_hndl ._send_fragment_ack (
168
+ sender , incoming_aps , fragment_count , fragment_index
169
+ )
170
+
171
+ # Assertions
172
+ assert status == t .EmberStatus .SUCCESS
173
+ assert (
174
+ "Sending fragment ack to 0x1d6f for fragment index=1/2" .lower ()
175
+ in caplog .text .lower ()
176
+ )
177
+ mock_send_reply .assert_called_once_with (sender , expected_ack_frame , b"" )
178
+
179
+
180
+ async def test_incoming_fragmented_message_incomplete (prot_hndl , caplog ):
181
+ """Test handling of an incomplete fragmented message."""
182
+ packet = b"\x90 \x01 \x45 \x00 \x05 \x01 \x01 \xff \x02 \x02 \x40 \x81 \x00 \x02 \xee \xff \xf8 \x6f \x1d \xff \xff \x01 \xdd "
183
+
184
+ # Parse packet manually to extract parameters for assertions
185
+ sender = 0x1D6F
186
+ aps_frame = t .EmberApsFrame (
187
+ profileId = 261 , # 0x0105
188
+ clusterId = 65281 , # 0xFF01
189
+ sourceEndpoint = 2 , # 0x02
190
+ destinationEndpoint = 2 , # 0x02
191
+ options = 33088 , # 0x8140 (APS_OPTION_FRAGMENT + others)
192
+ groupId = 512 , # 0x0002 (fragment_count=2, fragment_index=0)
193
+ sequence = 238 , # 0xEE
194
+ )
195
+
196
+ with patch .object (prot_hndl , "_send_fragment_ack" , new = AsyncMock ()) as mock_ack :
197
+ mock_ack .return_value = None
198
+
199
+ caplog .set_level (logging .DEBUG )
200
+ prot_hndl (packet )
201
+
202
+ assert len (prot_hndl ._fragment_ack_tasks ) == 1
203
+ ack_task = next (iter (prot_hndl ._fragment_ack_tasks ))
204
+ await asyncio .gather (ack_task ) # Ensure task completes and triggers callback
205
+ assert (
206
+ len (prot_hndl ._fragment_ack_tasks ) == 0
207
+ ), "Done callback should have removed task"
208
+
209
+ prot_hndl ._handle_callback .assert_not_called ()
210
+ assert "Fragment reassembly not complete. waiting for more data." in caplog .text
211
+ mock_ack .assert_called_once_with (sender , aps_frame , 2 , 0 )
212
+
213
+
214
+ async def test_incoming_fragmented_message_complete (prot_hndl , caplog ):
215
+ """Test handling of a complete fragmented message."""
216
+ packet1 = (
217
+ b"\x90 \x01 \x45 \x00 \x04 \x01 \x01 \xff \x02 \x02 \x40 \x81 \x00 \x02 \xee \xff \xf8 \x6f \x1d \xff \xff \x09 "
218
+ + b"complete "
219
+ ) # fragment index 0
220
+ packet2 = (
221
+ b"\x90 \x01 \x45 \x00 \x04 \x01 \x01 \xff \x02 \x02 \x40 \x81 \x01 \x02 \xee \xff \xf8 \x6f \x1d \xff \xff \x07 "
222
+ + b"message"
223
+ ) # fragment index 1
224
+ sender = 0x1D6F
225
+
226
+ aps_frame_1 = t .EmberApsFrame (
227
+ profileId = 260 ,
228
+ clusterId = 65281 ,
229
+ sourceEndpoint = 2 ,
230
+ destinationEndpoint = 2 ,
231
+ options = 33088 , # Includes APS_OPTION_FRAGMENT
232
+ groupId = 512 , # fragment_count=2, fragment_index=0
233
+ sequence = 238 ,
234
+ )
235
+ aps_frame_2 = t .EmberApsFrame (
236
+ profileId = 260 ,
237
+ clusterId = 65281 ,
238
+ sourceEndpoint = 2 ,
239
+ destinationEndpoint = 2 ,
240
+ options = 33088 ,
241
+ groupId = 513 , # fragment_count=2, fragment_index=1
242
+ sequence = 238 ,
243
+ )
244
+ reassembled = b"complete message"
245
+
246
+ with patch .object (prot_hndl , "_send_fragment_ack" , new = AsyncMock ()) as mock_ack :
247
+ mock_ack .return_value = None
248
+ caplog .set_level (logging .DEBUG )
249
+
250
+ # Packet 1
251
+ prot_hndl (packet1 )
252
+ assert len (prot_hndl ._fragment_ack_tasks ) == 1
253
+ ack_task = next (iter (prot_hndl ._fragment_ack_tasks ))
254
+ await asyncio .gather (ack_task ) # Ensure task completes and triggers callback
255
+ assert (
256
+ len (prot_hndl ._fragment_ack_tasks ) == 0
257
+ ), "Done callback should have removed task"
258
+
259
+ prot_hndl ._handle_callback .assert_not_called ()
260
+ assert (
261
+ "Reassembled fragmented message. Proceeding with normal handling."
262
+ not in caplog .text
263
+ )
264
+ mock_ack .assert_called_with (sender , aps_frame_1 , 2 , 0 )
265
+
266
+ # Packet 2
267
+ prot_hndl (packet2 )
268
+ assert len (prot_hndl ._fragment_ack_tasks ) == 1
269
+ ack_task = next (iter (prot_hndl ._fragment_ack_tasks ))
270
+ await asyncio .gather (ack_task ) # Ensure task completes and triggers callback
271
+ assert (
272
+ len (prot_hndl ._fragment_ack_tasks ) == 0
273
+ ), "Done callback should have removed task"
274
+
275
+ prot_hndl ._handle_callback .assert_called_once_with (
276
+ "incomingMessageHandler" ,
277
+ [
278
+ t .EmberIncomingMessageType .INCOMING_UNICAST , # 0x00
279
+ aps_frame_2 , # Parsed APS frame
280
+ 255 , # lastHopLqi: 0xFF
281
+ - 8 , # lastHopRssi: 0xF8
282
+ sender , # 0x1D6F
283
+ 255 , # bindingIndex: 0xFF
284
+ 255 , # addressIndex: 0xFF
285
+ reassembled , # Reassembled payload
286
+ ],
287
+ )
288
+ assert (
289
+ "Reassembled fragmented message. Proceeding with normal handling."
290
+ in caplog .text
291
+ )
292
+ mock_ack .assert_called_with (sender , aps_frame_2 , 2 , 1 )
0 commit comments