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
@@ -84,6 +174,70 @@ def test_loop_is_connected(self):
84
174
85
175
assert "not connected" in str (context .exception )
86
176
177
+ # pylint: disable=no-self-use
178
+ def test_loop_ping_timeout (self ):
179
+ """Verify that ping will be sent even with loop timeout bigger than keep alive timeout
180
+ and no outgoing messages are sent."""
181
+
182
+ recv_timeout = 2
183
+ keep_alive_timeout = recv_timeout * 2
184
+ mqtt_client = MQTT .MQTT (
185
+ broker = "localhost" ,
186
+ port = 1883 ,
187
+ ssl_context = ssl .create_default_context (),
188
+ connect_retries = 1 ,
189
+ socket_timeout = 1 ,
190
+ recv_timeout = recv_timeout ,
191
+ keep_alive = keep_alive_timeout ,
192
+ )
193
+
194
+ # patch is_connected() to avoid CONNECT/CONNACK handling.
195
+ mqtt_client .is_connected = lambda : True
196
+ mocket = Pingtet ()
197
+ # pylint: disable=protected-access
198
+ mqtt_client ._sock = mocket
199
+
200
+ start = time .monotonic ()
201
+ res = mqtt_client .loop (timeout = 2 * keep_alive_timeout )
202
+ assert time .monotonic () - start >= 2 * keep_alive_timeout
203
+ assert len (mocket .sent ) > 0
204
+ assert len (res ) == 2
205
+ assert set (res ) == {int (0xD0 )}
206
+
207
+ # pylint: disable=no-self-use
208
+ def test_loop_ping_vs_msgs_sent (self ):
209
+ """Verify that ping will not be sent unnecessarily."""
210
+
211
+ recv_timeout = 2
212
+ keep_alive_timeout = recv_timeout * 2
213
+ mqtt_client = MQTT .MQTT (
214
+ broker = "localhost" ,
215
+ port = 1883 ,
216
+ ssl_context = ssl .create_default_context (),
217
+ connect_retries = 1 ,
218
+ socket_timeout = 1 ,
219
+ recv_timeout = recv_timeout ,
220
+ keep_alive = keep_alive_timeout ,
221
+ )
222
+
223
+ # patch is_connected() to avoid CONNECT/CONNACK handling.
224
+ mqtt_client .is_connected = lambda : True
225
+
226
+ mocket = Nulltet ()
227
+ # pylint: disable=protected-access
228
+ mqtt_client ._sock = mocket
229
+
230
+ i = 0
231
+ topic = "foo"
232
+ message = "bar"
233
+ for _ in range (3 * keep_alive_timeout ):
234
+ mqtt_client .publish (topic , message )
235
+ mqtt_client .loop (1 )
236
+ i += 1
237
+
238
+ # This means no other messages than the PUBLISH messages generated by the code above.
239
+ assert len (mocket .sent ) == i * (2 + 2 + len (topic ) + len (message ))
240
+
87
241
88
242
if __name__ == "__main__" :
89
243
main ()
0 commit comments