8
8
import socket
9
9
import ssl
10
10
import time
11
+ import errno
12
+
11
13
from unittest import TestCase , main
12
14
from unittest .mock import patch
15
+ from unittest import mock
13
16
14
17
import adafruit_minimqtt .adafruit_minimqtt as MQTT
15
18
16
19
20
+ class Nulltet :
21
+ """
22
+ Mock Socket that does nothing.
23
+
24
+ Inspired by the Mocket class from Adafruit_CircuitPython_Requests
25
+ """
26
+
27
+ def __init__ (self ):
28
+ self .sent = bytearray ()
29
+
30
+ self .timeout = mock .Mock ()
31
+ self .connect = mock .Mock ()
32
+ self .close = mock .Mock ()
33
+
34
+ def send (self , bytes_to_send ):
35
+ """
36
+ Record the bytes. return the length of this bytearray.
37
+ """
38
+ self .sent .extend (bytes_to_send )
39
+ return len (bytes_to_send )
40
+
41
+ # MiniMQTT checks for the presence of "recv_into" and switches behavior based on that.
42
+ # pylint: disable=unused-argument,no-self-use
43
+ def recv_into (self , retbuf , bufsize ):
44
+ """Always raise timeout exception."""
45
+ exc = OSError ()
46
+ exc .errno = errno .ETIMEDOUT
47
+ raise exc
48
+
49
+
50
+ class Pingtet :
51
+ """
52
+ Mock Socket tailored for PINGREQ testing.
53
+ Records sent data, hands out PINGRESP for each PINGREQ received.
54
+
55
+ Inspired by the Mocket class from Adafruit_CircuitPython_Requests
56
+ """
57
+
58
+ PINGRESP = bytearray ([0xD0 , 0x00 ])
59
+
60
+ def __init__ (self ):
61
+ self ._to_send = self .PINGRESP
62
+
63
+ self .sent = bytearray ()
64
+
65
+ self .timeout = mock .Mock ()
66
+ self .connect = mock .Mock ()
67
+ self .close = mock .Mock ()
68
+
69
+ self ._got_pingreq = False
70
+
71
+ def send (self , bytes_to_send ):
72
+ """
73
+ Recognize PINGREQ and record the indication that it was received.
74
+ Assumes it was sent in one chunk (of 2 bytes).
75
+ Also record the bytes. return the length of this bytearray.
76
+ """
77
+ self .sent .extend (bytes_to_send )
78
+ if bytes_to_send == b"\xc0 \0 " :
79
+ self ._got_pingreq = True
80
+ return len (bytes_to_send )
81
+
82
+ # MiniMQTT checks for the presence of "recv_into" and switches behavior based on that.
83
+ def recv_into (self , retbuf , bufsize ):
84
+ """
85
+ If the PINGREQ indication is on, return PINGRESP, otherwise raise timeout exception.
86
+ """
87
+ if self ._got_pingreq :
88
+ size = min (bufsize , len (self ._to_send ))
89
+ if size == 0 :
90
+ return size
91
+ chop = self ._to_send [0 :size ]
92
+ retbuf [0 :] = chop
93
+ self ._to_send = self ._to_send [size :]
94
+ if len (self ._to_send ) == 0 :
95
+ self ._got_pingreq = False
96
+ self ._to_send = self .PINGRESP
97
+ return size
98
+
99
+ exc = OSError ()
100
+ exc .errno = errno .ETIMEDOUT
101
+ raise exc
102
+
103
+
17
104
class Loop (TestCase ):
18
105
"""basic loop() test"""
19
106
@@ -54,6 +141,8 @@ def test_loop_basic(self) -> None:
54
141
55
142
time_before = time .monotonic ()
56
143
timeout = random .randint (3 , 8 )
144
+ # pylint: disable=protected-access
145
+ mqtt_client ._last_msg_sent_timestamp = mqtt_client .get_monotonic_time ()
57
146
rcs = mqtt_client .loop (timeout = timeout )
58
147
time_after = time .monotonic ()
59
148
@@ -64,6 +153,7 @@ def test_loop_basic(self) -> None:
64
153
assert rcs is not None
65
154
assert len (rcs ) >= 1
66
155
expected_rc = self .INITIAL_RCS_VAL
156
+ # pylint: disable=not-an-iterable
67
157
for ret_code in rcs :
68
158
assert ret_code == expected_rc
69
159
expected_rc += 1
@@ -104,6 +194,71 @@ def test_loop_is_connected(self):
104
194
105
195
assert "not connected" in str (context .exception )
106
196
197
+ # pylint: disable=no-self-use
198
+ def test_loop_ping_timeout (self ):
199
+ """Verify that ping will be sent even with loop timeout bigger than keep alive timeout
200
+ and no outgoing messages are sent."""
201
+
202
+ recv_timeout = 2
203
+ keep_alive_timeout = recv_timeout * 2
204
+ mqtt_client = MQTT .MQTT (
205
+ broker = "localhost" ,
206
+ port = 1883 ,
207
+ ssl_context = ssl .create_default_context (),
208
+ connect_retries = 1 ,
209
+ socket_timeout = 1 ,
210
+ recv_timeout = recv_timeout ,
211
+ keep_alive = keep_alive_timeout ,
212
+ )
213
+
214
+ # patch is_connected() to avoid CONNECT/CONNACK handling.
215
+ mqtt_client .is_connected = lambda : True
216
+ mocket = Pingtet ()
217
+ # pylint: disable=protected-access
218
+ mqtt_client ._sock = mocket
219
+
220
+ start = time .monotonic ()
221
+ res = mqtt_client .loop (timeout = 2 * keep_alive_timeout )
222
+ assert time .monotonic () - start >= 2 * keep_alive_timeout
223
+ assert len (mocket .sent ) > 0
224
+ assert len (res ) == 2
225
+ assert set (res ) == {int (0xD0 )}
226
+
227
+ # pylint: disable=no-self-use
228
+ def test_loop_ping_vs_msgs_sent (self ):
229
+ """Verify that ping will not be sent unnecessarily."""
230
+
231
+ recv_timeout = 2
232
+ keep_alive_timeout = recv_timeout * 2
233
+ mqtt_client = MQTT .MQTT (
234
+ broker = "localhost" ,
235
+ port = 1883 ,
236
+ ssl_context = ssl .create_default_context (),
237
+ connect_retries = 1 ,
238
+ socket_timeout = 1 ,
239
+ recv_timeout = recv_timeout ,
240
+ keep_alive = keep_alive_timeout ,
241
+ )
242
+
243
+ # patch is_connected() to avoid CONNECT/CONNACK handling.
244
+ mqtt_client .is_connected = lambda : True
245
+
246
+ # With QoS=0 no PUBACK message is sent, so Nulltet can be used.
247
+ mocket = Nulltet ()
248
+ # pylint: disable=protected-access
249
+ mqtt_client ._sock = mocket
250
+
251
+ i = 0
252
+ topic = "foo"
253
+ message = "bar"
254
+ for _ in range (3 * keep_alive_timeout ):
255
+ mqtt_client .publish (topic , message , qos = 0 )
256
+ mqtt_client .loop (1 )
257
+ i += 1
258
+
259
+ # This means no other messages than the PUBLISH messages generated by the code above.
260
+ assert len (mocket .sent ) == i * (2 + 2 + len (topic ) + len (message ))
261
+
107
262
108
263
if __name__ == "__main__" :
109
264
main ()
0 commit comments