|
4 | 4 | GATT) to lock down: a message split across multiple BLE notification |
5 | 5 | chunks, multiple complete messages delivered in a single chunk, and |
6 | 6 | resynchronization after a corrupted/oversized packet using the |
7 | | -``packet_starts`` boundary tracking in ``discard_packet``. |
| 7 | +``packet_starts`` boundary tracking in ``discard_packet``. It also verifies |
| 8 | +that a stale partial frame is dropped after the BLE inter-chunk timeout while |
| 9 | +normal fast multi-chunk messages still reassemble. |
8 | 10 | """ |
9 | 11 |
|
10 | 12 | from unittest import TestCase |
| 13 | +from unittest.mock import patch |
11 | 14 |
|
12 | 15 | from tesla_fleet_api.tesla.vehicle.bluetooth import ReassemblingBuffer, prependLength |
13 | 16 | from tesla_fleet_api.tesla.vehicle.proto.universal_message_pb2 import ( |
@@ -114,3 +117,57 @@ def test_oversized_length_header_discards_and_resyncs(self) -> None: |
114 | 117 | self.assertEqual( |
115 | 118 | self.received[0].from_destination.domain, Domain.DOMAIN_VEHICLE_SECURITY |
116 | 119 | ) |
| 120 | + |
| 121 | + def test_stale_partial_is_discarded_after_timeout(self) -> None: |
| 122 | + stale = RoutableMessage( |
| 123 | + from_destination=Destination(domain=Domain.DOMAIN_VEHICLE_SECURITY), |
| 124 | + request_uuid=b"0123456789abcdef", |
| 125 | + ) |
| 126 | + stale_payload = framed(stale) |
| 127 | + |
| 128 | + fresh = RoutableMessage( |
| 129 | + from_destination=Destination(domain=Domain.DOMAIN_INFOTAINMENT) |
| 130 | + ) |
| 131 | + |
| 132 | + with patch( |
| 133 | + "tesla_fleet_api.tesla.vehicle.bluetooth.time.monotonic" |
| 134 | + ) as mock_monotonic: |
| 135 | + # Deliver only the first half of `stale` - a dropped chunk mid-message. |
| 136 | + mock_monotonic.return_value = 0.0 |
| 137 | + self.buffer.receive_data(stale_payload[: len(stale_payload) // 2]) |
| 138 | + self.assertEqual(self.received, []) |
| 139 | + |
| 140 | + # The next chunk arrives well past the stale-chunk timeout: the |
| 141 | + # partial must be dropped, not prepended to the new message. |
| 142 | + mock_monotonic.return_value = 2.0 |
| 143 | + self.buffer.receive_data(framed(fresh)) |
| 144 | + |
| 145 | + self.assertEqual(len(self.received), 1) |
| 146 | + self.assertEqual( |
| 147 | + self.received[0].from_destination.domain, Domain.DOMAIN_INFOTAINMENT |
| 148 | + ) |
| 149 | + |
| 150 | + def test_fast_multi_chunk_message_under_timeout_still_reassembles(self) -> None: |
| 151 | + msg = RoutableMessage( |
| 152 | + from_destination=Destination(domain=Domain.DOMAIN_VEHICLE_SECURITY), |
| 153 | + request_uuid=b"0123456789abcdef", |
| 154 | + ) |
| 155 | + payload = framed(msg) |
| 156 | + chunk_size = 5 |
| 157 | + |
| 158 | + with patch( |
| 159 | + "tesla_fleet_api.tesla.vehicle.bluetooth.time.monotonic" |
| 160 | + ) as mock_monotonic: |
| 161 | + # Each chunk arrives 0.1s after the previous one - well under the |
| 162 | + # stale-chunk timeout - so the partial must survive intact. |
| 163 | + clock = 0.0 |
| 164 | + for i in range(0, len(payload), chunk_size): |
| 165 | + mock_monotonic.return_value = clock |
| 166 | + self.buffer.receive_data(payload[i : i + chunk_size]) |
| 167 | + clock += 0.1 |
| 168 | + |
| 169 | + self.assertEqual(len(self.received), 1) |
| 170 | + self.assertEqual( |
| 171 | + self.received[0].from_destination.domain, Domain.DOMAIN_VEHICLE_SECURITY |
| 172 | + ) |
| 173 | + self.assertEqual(self.received[0].request_uuid, b"0123456789abcdef") |
0 commit comments