locust 2.40.6.dev8__tar.gz → 2.40.6.dev17__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {locust-2.40.6.dev8 → locust-2.40.6.dev17}/PKG-INFO +4 -1
- {locust-2.40.6.dev8 → locust-2.40.6.dev17}/locust/_version.py +2 -2
- locust-2.40.6.dev17/locust/contrib/mqtt.py +462 -0
- {locust-2.40.6.dev8 → locust-2.40.6.dev17}/pyproject.toml +6 -7
- {locust-2.40.6.dev8 → locust-2.40.6.dev17}/.gitignore +0 -0
- {locust-2.40.6.dev8 → locust-2.40.6.dev17}/LICENSE +0 -0
- {locust-2.40.6.dev8 → locust-2.40.6.dev17}/README.md +0 -0
- {locust-2.40.6.dev8 → locust-2.40.6.dev17}/hatch_build.py +0 -0
- {locust-2.40.6.dev8 → locust-2.40.6.dev17}/locust/__init__.py +0 -0
- {locust-2.40.6.dev8 → locust-2.40.6.dev17}/locust/__main__.py +0 -0
- {locust-2.40.6.dev8 → locust-2.40.6.dev17}/locust/argument_parser.py +0 -0
- {locust-2.40.6.dev8 → locust-2.40.6.dev17}/locust/clients.py +0 -0
- {locust-2.40.6.dev8 → locust-2.40.6.dev17}/locust/contrib/__init__.py +0 -0
- {locust-2.40.6.dev8 → locust-2.40.6.dev17}/locust/contrib/fasthttp.py +0 -0
- {locust-2.40.6.dev8 → locust-2.40.6.dev17}/locust/contrib/milvus.py +0 -0
- {locust-2.40.6.dev8 → locust-2.40.6.dev17}/locust/contrib/mongodb.py +0 -0
- {locust-2.40.6.dev8 → locust-2.40.6.dev17}/locust/contrib/oai.py +0 -0
- {locust-2.40.6.dev8 → locust-2.40.6.dev17}/locust/contrib/postgres.py +0 -0
- {locust-2.40.6.dev8 → locust-2.40.6.dev17}/locust/contrib/socketio.py +0 -0
- {locust-2.40.6.dev8 → locust-2.40.6.dev17}/locust/debug.py +0 -0
- {locust-2.40.6.dev8 → locust-2.40.6.dev17}/locust/dispatch.py +0 -0
- {locust-2.40.6.dev8 → locust-2.40.6.dev17}/locust/env.py +0 -0
- {locust-2.40.6.dev8 → locust-2.40.6.dev17}/locust/event.py +0 -0
- {locust-2.40.6.dev8 → locust-2.40.6.dev17}/locust/exception.py +0 -0
- {locust-2.40.6.dev8 → locust-2.40.6.dev17}/locust/html.py +0 -0
- {locust-2.40.6.dev8 → locust-2.40.6.dev17}/locust/input_events.py +0 -0
- {locust-2.40.6.dev8 → locust-2.40.6.dev17}/locust/log.py +0 -0
- {locust-2.40.6.dev8 → locust-2.40.6.dev17}/locust/main.py +0 -0
- {locust-2.40.6.dev8 → locust-2.40.6.dev17}/locust/py.typed +0 -0
- {locust-2.40.6.dev8 → locust-2.40.6.dev17}/locust/rpc/__init__.py +0 -0
- {locust-2.40.6.dev8 → locust-2.40.6.dev17}/locust/rpc/protocol.py +0 -0
- {locust-2.40.6.dev8 → locust-2.40.6.dev17}/locust/rpc/zmqrpc.py +0 -0
- {locust-2.40.6.dev8 → locust-2.40.6.dev17}/locust/runners.py +0 -0
- {locust-2.40.6.dev8 → locust-2.40.6.dev17}/locust/shape.py +0 -0
- {locust-2.40.6.dev8 → locust-2.40.6.dev17}/locust/stats.py +0 -0
- {locust-2.40.6.dev8 → locust-2.40.6.dev17}/locust/user/__init__.py +0 -0
- {locust-2.40.6.dev8 → locust-2.40.6.dev17}/locust/user/inspectuser.py +0 -0
- {locust-2.40.6.dev8 → locust-2.40.6.dev17}/locust/user/markov_taskset.py +0 -0
- {locust-2.40.6.dev8 → locust-2.40.6.dev17}/locust/user/sequential_taskset.py +0 -0
- {locust-2.40.6.dev8 → locust-2.40.6.dev17}/locust/user/task.py +0 -0
- {locust-2.40.6.dev8 → locust-2.40.6.dev17}/locust/user/users.py +0 -0
- {locust-2.40.6.dev8 → locust-2.40.6.dev17}/locust/user/wait_time.py +0 -0
- {locust-2.40.6.dev8 → locust-2.40.6.dev17}/locust/util/__init__.py +0 -0
- {locust-2.40.6.dev8 → locust-2.40.6.dev17}/locust/util/cache.py +0 -0
- {locust-2.40.6.dev8 → locust-2.40.6.dev17}/locust/util/date.py +0 -0
- {locust-2.40.6.dev8 → locust-2.40.6.dev17}/locust/util/deprecation.py +0 -0
- {locust-2.40.6.dev8 → locust-2.40.6.dev17}/locust/util/directory.py +0 -0
- {locust-2.40.6.dev8 → locust-2.40.6.dev17}/locust/util/exception_handler.py +0 -0
- {locust-2.40.6.dev8 → locust-2.40.6.dev17}/locust/util/load_locustfile.py +0 -0
- {locust-2.40.6.dev8 → locust-2.40.6.dev17}/locust/util/rounding.py +0 -0
- {locust-2.40.6.dev8 → locust-2.40.6.dev17}/locust/util/timespan.py +0 -0
- {locust-2.40.6.dev8 → locust-2.40.6.dev17}/locust/util/url.py +0 -0
- {locust-2.40.6.dev8 → locust-2.40.6.dev17}/locust/web.py +0 -0
- {locust-2.40.6.dev8 → locust-2.40.6.dev17}/locust/webui/dist/assets/favicon-dark.png +0 -0
- {locust-2.40.6.dev8 → locust-2.40.6.dev17}/locust/webui/dist/assets/favicon-light.png +0 -0
- {locust-2.40.6.dev8 → locust-2.40.6.dev17}/locust/webui/dist/assets/graphs-dark.png +0 -0
- {locust-2.40.6.dev8 → locust-2.40.6.dev17}/locust/webui/dist/assets/graphs-light.png +0 -0
- {locust-2.40.6.dev8 → locust-2.40.6.dev17}/locust/webui/dist/assets/index-aozIzkOV.js +0 -0
- {locust-2.40.6.dev8 → locust-2.40.6.dev17}/locust/webui/dist/assets/terminal.gif +0 -0
- {locust-2.40.6.dev8 → locust-2.40.6.dev17}/locust/webui/dist/assets/testruns-dark.png +0 -0
- {locust-2.40.6.dev8 → locust-2.40.6.dev17}/locust/webui/dist/assets/testruns-light.png +0 -0
- {locust-2.40.6.dev8 → locust-2.40.6.dev17}/locust/webui/dist/auth.html +0 -0
- {locust-2.40.6.dev8 → locust-2.40.6.dev17}/locust/webui/dist/index.html +0 -0
- {locust-2.40.6.dev8 → locust-2.40.6.dev17}/locust/webui/dist/report.html +0 -0
- {locust-2.40.6.dev8 → locust-2.40.6.dev17}/pytest_locust/plugin.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: locust
|
3
|
-
Version: 2.40.6.
|
3
|
+
Version: 2.40.6.dev17
|
4
4
|
Summary: Developer-friendly load testing framework
|
5
5
|
Project-URL: homepage, https://locust.io/
|
6
6
|
Project-URL: repository, https://github.com/locustio/locust
|
@@ -32,6 +32,7 @@ Requires-Dist: gevent<25.8.1,>=24.10.1
|
|
32
32
|
Requires-Dist: geventhttpclient>=2.3.1
|
33
33
|
Requires-Dist: locust-cloud>=1.27.0
|
34
34
|
Requires-Dist: msgpack>=1.0.0
|
35
|
+
Requires-Dist: paho-mqtt>=2.1.0
|
35
36
|
Requires-Dist: psutil>=5.9.1
|
36
37
|
Requires-Dist: pytest<9.0.0,>=8.3.3
|
37
38
|
Requires-Dist: python-engineio>=4.12.2
|
@@ -45,6 +46,8 @@ Requires-Dist: typing-extensions>=4.6.0; python_version < '3.12'
|
|
45
46
|
Requires-Dist: werkzeug>=2.0.0
|
46
47
|
Provides-Extra: milvus
|
47
48
|
Requires-Dist: pymilvus>=2.5.0; extra == 'milvus'
|
49
|
+
Provides-Extra: mqtt
|
50
|
+
Requires-Dist: paho-mqtt>=2.1.0; extra == 'mqtt'
|
48
51
|
Description-Content-Type: text/markdown
|
49
52
|
|
50
53
|
# Locust
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
28
28
|
commit_id: COMMIT_ID
|
29
29
|
__commit_id__: COMMIT_ID
|
30
30
|
|
31
|
-
__version__ = version = '2.40.6.
|
32
|
-
__version_tuple__ = version_tuple = (2, 40, 6, '
|
31
|
+
__version__ = version = '2.40.6.dev17'
|
32
|
+
__version_tuple__ = version_tuple = (2, 40, 6, 'dev17')
|
33
33
|
|
34
34
|
__commit_id__ = commit_id = None
|
@@ -0,0 +1,462 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from locust import User
|
4
|
+
from locust.env import Environment
|
5
|
+
|
6
|
+
import random
|
7
|
+
import time
|
8
|
+
import typing
|
9
|
+
|
10
|
+
import paho.mqtt.client as mqtt
|
11
|
+
|
12
|
+
if typing.TYPE_CHECKING:
|
13
|
+
from paho.mqtt.client import MQTTMessageInfo
|
14
|
+
from paho.mqtt.enums import MQTTProtocolVersion
|
15
|
+
from paho.mqtt.properties import Properties
|
16
|
+
from paho.mqtt.reasoncodes import ReasonCode
|
17
|
+
from paho.mqtt.subscribeoptions import SubscribeOptions
|
18
|
+
|
19
|
+
|
20
|
+
# A SUBACK response for MQTT can only contain 0x00, 0x01, 0x02, or 0x80. 0x80
|
21
|
+
# indicates a failure to subscribe.
|
22
|
+
#
|
23
|
+
# http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Figure_3.26_-
|
24
|
+
SUBACK_FAILURE = 0x80
|
25
|
+
REQUEST_TYPE = "MQTT"
|
26
|
+
|
27
|
+
|
28
|
+
def _generate_random_id(
|
29
|
+
length: int,
|
30
|
+
alphabet: str = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
31
|
+
):
|
32
|
+
"""Generate a random ID from the given alphabet.
|
33
|
+
|
34
|
+
Args:
|
35
|
+
length: the number of random characters to generate.
|
36
|
+
alphabet: the pool of random characters to choose from.
|
37
|
+
"""
|
38
|
+
return "".join(random.choice(alphabet) for _ in range(length))
|
39
|
+
|
40
|
+
|
41
|
+
def _generate_mqtt_event_name(event_type: str, qos: int, topic: str):
|
42
|
+
"""Generate a name to identify publish/subscribe tasks.
|
43
|
+
|
44
|
+
This will be used to ultimately identify tasks in the Locust web console.
|
45
|
+
This will identify publish/subscribe tasks with their QoS & associated
|
46
|
+
topic.
|
47
|
+
|
48
|
+
Examples:
|
49
|
+
publish:0:my/topic
|
50
|
+
subscribe:1:my/other/topic
|
51
|
+
|
52
|
+
Args:
|
53
|
+
event_type: The type of MQTT event (subscribe or publish)
|
54
|
+
qos: The quality-of-service associated with this event
|
55
|
+
topic: The MQTT topic associated with this event
|
56
|
+
"""
|
57
|
+
return f"{event_type}:{qos}:{topic}"
|
58
|
+
|
59
|
+
|
60
|
+
class PublishedMessageContext(typing.NamedTuple):
|
61
|
+
"""Stores metadata about outgoing published messages."""
|
62
|
+
|
63
|
+
qos: int
|
64
|
+
topic: str
|
65
|
+
start_time: float
|
66
|
+
payload_size: int
|
67
|
+
|
68
|
+
|
69
|
+
class MqttClient(mqtt.Client):
|
70
|
+
def __init__(
|
71
|
+
self,
|
72
|
+
*args,
|
73
|
+
environment: Environment,
|
74
|
+
client_id: str | None = None,
|
75
|
+
protocol: MQTTProtocolVersion = mqtt.MQTTv311,
|
76
|
+
**kwargs,
|
77
|
+
):
|
78
|
+
"""Initializes a paho.mqtt.Client for use in Locust swarms.
|
79
|
+
|
80
|
+
This class passes most args & kwargs through to the underlying
|
81
|
+
paho.mqtt constructor.
|
82
|
+
|
83
|
+
Args:
|
84
|
+
environment: the Locust environment with which to associate events.
|
85
|
+
client_id: the MQTT Client ID to use in connecting to the broker.
|
86
|
+
If not set, one will be randomly generated.
|
87
|
+
protocol: the MQTT protocol version.
|
88
|
+
defaults to MQTT v3.11.
|
89
|
+
"""
|
90
|
+
# If a client ID is not provided, this class will randomly generate an ID
|
91
|
+
# of the form: `locust-[0-9a-zA-Z]{16}` (i.e., `locust-` followed by 16
|
92
|
+
# random characters, so that the resulting client ID does not exceed the
|
93
|
+
# specification limit of 23 characters).
|
94
|
+
|
95
|
+
# This is done in this wrapper class so that this locust client can
|
96
|
+
# self-identify when firing requests, since some versions of MQTT will
|
97
|
+
# have the broker assign IDs to clients that do not provide one: in this
|
98
|
+
# case, there is no way to retrieve the client ID.
|
99
|
+
|
100
|
+
# See https://github.com/eclipse/paho.mqtt.python/issues/237
|
101
|
+
if not client_id:
|
102
|
+
client_id = f"locust-{_generate_random_id(16)}"
|
103
|
+
|
104
|
+
super().__init__(*args, **kwargs)
|
105
|
+
self.environment = environment
|
106
|
+
# we need to set client_id in case the broker assigns one to us
|
107
|
+
self.client_id = client_id
|
108
|
+
|
109
|
+
self.on_publish = self._on_publish_cb # type: ignore[assignment]
|
110
|
+
|
111
|
+
if protocol == mqtt.MQTTv5:
|
112
|
+
self.on_disconnect = self._on_disconnect_cb_v5
|
113
|
+
self.on_connect = self._on_connect_cb_v5
|
114
|
+
self.on_subscribe = self._on_subscribe_cb_v5
|
115
|
+
else:
|
116
|
+
self.on_disconnect = self._on_disconnect_cb_v3x # type: ignore[assignment]
|
117
|
+
self.on_connect = self._on_connect_cb_v3x # type: ignore[assignment]
|
118
|
+
self.on_subscribe = self._on_subscribe_cb_v3x # type: ignore[assignment]
|
119
|
+
|
120
|
+
self._publish_requests: dict[int, PublishedMessageContext] = {}
|
121
|
+
self._subscribe_requests: dict[int, tuple[int, str, float]] = {}
|
122
|
+
|
123
|
+
def _generate_event_name(self, event_type: str, qos: int, topic: str):
|
124
|
+
return _generate_mqtt_event_name(event_type, qos, topic)
|
125
|
+
|
126
|
+
def _on_publish_cb(
|
127
|
+
self,
|
128
|
+
client: mqtt.Client,
|
129
|
+
userdata: typing.Any,
|
130
|
+
mid: int,
|
131
|
+
):
|
132
|
+
cb_time = time.time()
|
133
|
+
try:
|
134
|
+
request_context = self._publish_requests.pop(mid)
|
135
|
+
except KeyError:
|
136
|
+
# we shouldn't hit this block of code
|
137
|
+
self.environment.events.request.fire(
|
138
|
+
request_type=REQUEST_TYPE,
|
139
|
+
name="publish",
|
140
|
+
response_time=0,
|
141
|
+
response_length=0,
|
142
|
+
exception=AssertionError(f"Could not find message data for mid '{mid}' in _on_publish_cb."),
|
143
|
+
context={
|
144
|
+
"client_id": self.client_id,
|
145
|
+
"mid": mid,
|
146
|
+
},
|
147
|
+
)
|
148
|
+
else:
|
149
|
+
# fire successful publish event
|
150
|
+
self.environment.events.request.fire(
|
151
|
+
request_type=REQUEST_TYPE,
|
152
|
+
name=self._generate_event_name("publish", request_context.qos, request_context.topic),
|
153
|
+
response_time=(cb_time - request_context.start_time) * 1000,
|
154
|
+
response_length=request_context.payload_size,
|
155
|
+
exception=None,
|
156
|
+
context={
|
157
|
+
"client_id": self.client_id,
|
158
|
+
**request_context._asdict(),
|
159
|
+
},
|
160
|
+
)
|
161
|
+
|
162
|
+
def _on_subscribe_cb_v3x(
|
163
|
+
self,
|
164
|
+
client: mqtt.Client,
|
165
|
+
userdata: typing.Any,
|
166
|
+
mid: int,
|
167
|
+
granted_qos: list[int],
|
168
|
+
):
|
169
|
+
cb_time = time.time()
|
170
|
+
try:
|
171
|
+
qos, topic, start_time = self._subscribe_requests.pop(mid)
|
172
|
+
except KeyError:
|
173
|
+
# we shouldn't hit this block of code
|
174
|
+
self.environment.events.request.fire(
|
175
|
+
request_type=REQUEST_TYPE,
|
176
|
+
name="subscribe",
|
177
|
+
response_time=0,
|
178
|
+
response_length=0,
|
179
|
+
exception=AssertionError(f"Could not find message data for mid '{mid}' in _on_subscribe_cb."),
|
180
|
+
context={
|
181
|
+
"client_id": self.client_id,
|
182
|
+
"mid": mid,
|
183
|
+
},
|
184
|
+
)
|
185
|
+
else:
|
186
|
+
if SUBACK_FAILURE in granted_qos:
|
187
|
+
self.environment.events.request.fire(
|
188
|
+
request_type=REQUEST_TYPE,
|
189
|
+
name=self._generate_event_name("subscribe", qos, topic),
|
190
|
+
response_time=(cb_time - start_time) * 1000,
|
191
|
+
response_length=0,
|
192
|
+
exception=AssertionError(f"Broker returned an error response during subscription: {granted_qos}"),
|
193
|
+
context={
|
194
|
+
"client_id": self.client_id,
|
195
|
+
"qos": qos,
|
196
|
+
"topic": topic,
|
197
|
+
"start_time": start_time,
|
198
|
+
},
|
199
|
+
)
|
200
|
+
else:
|
201
|
+
# fire successful subscribe event
|
202
|
+
self.environment.events.request.fire(
|
203
|
+
request_type=REQUEST_TYPE,
|
204
|
+
name=self._generate_event_name("subscribe", qos, topic),
|
205
|
+
response_time=(cb_time - start_time) * 1000,
|
206
|
+
response_length=0,
|
207
|
+
exception=None,
|
208
|
+
context={
|
209
|
+
"client_id": self.client_id,
|
210
|
+
"qos": qos,
|
211
|
+
"topic": topic,
|
212
|
+
"start_time": start_time,
|
213
|
+
},
|
214
|
+
)
|
215
|
+
|
216
|
+
# pylint: disable=unused-argument
|
217
|
+
def _on_subscribe_cb_v5(
|
218
|
+
self,
|
219
|
+
client: mqtt.Client,
|
220
|
+
userdata: typing.Any,
|
221
|
+
mid: int,
|
222
|
+
reason_codes: list[ReasonCode],
|
223
|
+
properties: Properties,
|
224
|
+
) -> None:
|
225
|
+
granted_qos = [rc.value for rc in reason_codes]
|
226
|
+
return self._on_subscribe_cb_v3x(client, userdata, mid, granted_qos)
|
227
|
+
|
228
|
+
def _on_disconnect_cb(
|
229
|
+
self,
|
230
|
+
client: mqtt.Client,
|
231
|
+
userdata: typing.Any,
|
232
|
+
rc: int | ReasonCode,
|
233
|
+
):
|
234
|
+
if rc != 0:
|
235
|
+
self.environment.events.request.fire(
|
236
|
+
request_type=REQUEST_TYPE,
|
237
|
+
name="disconnect",
|
238
|
+
response_time=0,
|
239
|
+
response_length=0,
|
240
|
+
exception=rc,
|
241
|
+
context={
|
242
|
+
"client_id": self.client_id,
|
243
|
+
},
|
244
|
+
)
|
245
|
+
else:
|
246
|
+
self.environment.events.request.fire(
|
247
|
+
request_type=REQUEST_TYPE,
|
248
|
+
name="disconnect",
|
249
|
+
response_time=0,
|
250
|
+
response_length=0,
|
251
|
+
exception=None,
|
252
|
+
context={
|
253
|
+
"client_id": self.client_id,
|
254
|
+
},
|
255
|
+
)
|
256
|
+
|
257
|
+
def _on_disconnect_cb_v3x(
|
258
|
+
self,
|
259
|
+
client: mqtt.Client,
|
260
|
+
userdata: typing.Any,
|
261
|
+
rc: int,
|
262
|
+
):
|
263
|
+
return self._on_disconnect_cb(client, userdata, rc)
|
264
|
+
|
265
|
+
# pylint: disable=unused-argument
|
266
|
+
def _on_disconnect_cb_v5(
|
267
|
+
self,
|
268
|
+
client: mqtt.Client,
|
269
|
+
userdata: typing.Any,
|
270
|
+
disconnect_flags: mqtt.DisconnectFlags,
|
271
|
+
reasoncode: ReasonCode,
|
272
|
+
properties: Properties | None,
|
273
|
+
) -> None:
|
274
|
+
return self._on_disconnect_cb(client, userdata, reasoncode)
|
275
|
+
|
276
|
+
def _on_connect_cb(
|
277
|
+
self,
|
278
|
+
client: mqtt.Client,
|
279
|
+
userdata: typing.Any,
|
280
|
+
flags: dict[str, int],
|
281
|
+
rc: int | ReasonCode,
|
282
|
+
):
|
283
|
+
if rc != 0:
|
284
|
+
self.environment.events.request.fire(
|
285
|
+
request_type=REQUEST_TYPE,
|
286
|
+
name="connect",
|
287
|
+
response_time=0,
|
288
|
+
response_length=0,
|
289
|
+
exception=Exception(str(rc)),
|
290
|
+
context={
|
291
|
+
"client_id": self.client_id,
|
292
|
+
},
|
293
|
+
)
|
294
|
+
else:
|
295
|
+
self.environment.events.request.fire(
|
296
|
+
request_type=REQUEST_TYPE,
|
297
|
+
name="connect",
|
298
|
+
response_time=0,
|
299
|
+
response_length=0,
|
300
|
+
exception=None,
|
301
|
+
context={
|
302
|
+
"client_id": self.client_id,
|
303
|
+
},
|
304
|
+
)
|
305
|
+
|
306
|
+
def _on_connect_cb_v3x(
|
307
|
+
self,
|
308
|
+
client: mqtt.Client,
|
309
|
+
userdata: typing.Any,
|
310
|
+
flags: dict[str, int],
|
311
|
+
rc: int,
|
312
|
+
):
|
313
|
+
return self._on_connect_cb(client, userdata, flags, rc)
|
314
|
+
|
315
|
+
# pylint: disable=unused-argument
|
316
|
+
def _on_connect_cb_v5(
|
317
|
+
self,
|
318
|
+
client: mqtt.Client,
|
319
|
+
userdata: typing.Any,
|
320
|
+
connect_flags: mqtt.ConnectFlags,
|
321
|
+
reasoncode: ReasonCode,
|
322
|
+
properties: Properties | None,
|
323
|
+
) -> None:
|
324
|
+
self._on_connect_cb(client, userdata, {}, reasoncode)
|
325
|
+
|
326
|
+
def publish(
|
327
|
+
self,
|
328
|
+
topic: str,
|
329
|
+
payload: mqtt.PayloadType | None = None,
|
330
|
+
qos: int = 0,
|
331
|
+
retain: bool = False,
|
332
|
+
properties: Properties | None = None,
|
333
|
+
) -> MQTTMessageInfo:
|
334
|
+
"""Publish a message to the MQTT broker.
|
335
|
+
|
336
|
+
This method wraps the underlying paho-mqtt client's method in order to
|
337
|
+
set up & fire Locust events.
|
338
|
+
"""
|
339
|
+
request_context = PublishedMessageContext(
|
340
|
+
qos=qos,
|
341
|
+
topic=topic,
|
342
|
+
start_time=time.time(),
|
343
|
+
payload_size=len(payload) if payload and not isinstance(payload, (int, float)) else 0,
|
344
|
+
)
|
345
|
+
|
346
|
+
publish_info = super().publish(topic, payload=payload, qos=qos, retain=retain, properties=properties)
|
347
|
+
|
348
|
+
if publish_info.rc != mqtt.MQTT_ERR_SUCCESS:
|
349
|
+
self.environment.events.request.fire(
|
350
|
+
request_type=REQUEST_TYPE,
|
351
|
+
name=self._generate_event_name("publish", request_context.qos, request_context.topic),
|
352
|
+
response_time=0,
|
353
|
+
response_length=0,
|
354
|
+
exception=publish_info.rc,
|
355
|
+
context={
|
356
|
+
"client_id": self.client_id,
|
357
|
+
**request_context._asdict(),
|
358
|
+
},
|
359
|
+
)
|
360
|
+
else:
|
361
|
+
# store this for use in the on_publish callback
|
362
|
+
self._publish_requests[publish_info.mid] = request_context
|
363
|
+
|
364
|
+
return publish_info
|
365
|
+
|
366
|
+
def subscribe(
|
367
|
+
self,
|
368
|
+
topic: str
|
369
|
+
| tuple[str, int]
|
370
|
+
| tuple[str, SubscribeOptions]
|
371
|
+
| list[tuple[str, int]]
|
372
|
+
| list[tuple[str, SubscribeOptions]],
|
373
|
+
qos: int = 0,
|
374
|
+
options: SubscribeOptions | None = None,
|
375
|
+
properties: Properties | None = None,
|
376
|
+
) -> tuple[mqtt.MQTTErrorCode, int | None]:
|
377
|
+
"""Subscribe to a given topic.
|
378
|
+
|
379
|
+
This method wraps the underlying paho-mqtt client's method in order to
|
380
|
+
set up & fire Locust events.
|
381
|
+
"""
|
382
|
+
start_time = time.time()
|
383
|
+
subscribe_topic = topic if isinstance(topic, str) else topic[0][0]
|
384
|
+
|
385
|
+
result, mid = super().subscribe(topic, qos, options, properties) # type: ignore[arg-type]
|
386
|
+
|
387
|
+
if result != mqtt.MQTT_ERR_SUCCESS:
|
388
|
+
self.environment.events.request.fire(
|
389
|
+
request_type=REQUEST_TYPE,
|
390
|
+
name=self._generate_event_name("subscribe", qos, subscribe_topic),
|
391
|
+
response_time=0,
|
392
|
+
response_length=0,
|
393
|
+
exception=result,
|
394
|
+
context={
|
395
|
+
"client_id": self.client_id,
|
396
|
+
"qos": qos,
|
397
|
+
"topic": subscribe_topic,
|
398
|
+
"start_time": start_time,
|
399
|
+
},
|
400
|
+
)
|
401
|
+
else:
|
402
|
+
if mid is None:
|
403
|
+
# QoS 0 subscriptions do not have a mid, so we'll just fire a success event immediately
|
404
|
+
self.environment.events.request.fire(
|
405
|
+
request_type=REQUEST_TYPE,
|
406
|
+
name=self._generate_event_name("subscribe", qos, subscribe_topic),
|
407
|
+
response_time=(time.time() - start_time) * 1000,
|
408
|
+
response_length=0,
|
409
|
+
exception=None,
|
410
|
+
context={
|
411
|
+
"client_id": self.client_id,
|
412
|
+
"qos": qos,
|
413
|
+
"topic": subscribe_topic,
|
414
|
+
"start_time": start_time,
|
415
|
+
},
|
416
|
+
)
|
417
|
+
else:
|
418
|
+
self._subscribe_requests[mid] = (qos, subscribe_topic, start_time)
|
419
|
+
|
420
|
+
return result, mid
|
421
|
+
|
422
|
+
|
423
|
+
class MqttUser(User):
|
424
|
+
abstract = True
|
425
|
+
|
426
|
+
host = "localhost"
|
427
|
+
port = 1883
|
428
|
+
transport = "tcp"
|
429
|
+
ws_path = "/mqtt"
|
430
|
+
tls_context = None
|
431
|
+
client_cls: type[MqttClient] = MqttClient
|
432
|
+
client_id = None
|
433
|
+
username = None
|
434
|
+
password = None
|
435
|
+
protocol = mqtt.MQTTv311
|
436
|
+
|
437
|
+
def __init__(self, environment: Environment):
|
438
|
+
super().__init__(environment)
|
439
|
+
self.client: MqttClient = self.client_cls(
|
440
|
+
environment=self.environment,
|
441
|
+
transport=self.transport,
|
442
|
+
client_id=self.client_id,
|
443
|
+
protocol=self.protocol,
|
444
|
+
)
|
445
|
+
|
446
|
+
if self.tls_context:
|
447
|
+
self.client.tls_set_context(self.tls_context)
|
448
|
+
|
449
|
+
if self.transport == "websockets" and self.ws_path:
|
450
|
+
self.client.ws_set_options(path=self.ws_path)
|
451
|
+
|
452
|
+
if self.username and self.password:
|
453
|
+
self.client.username_pw_set(
|
454
|
+
username=self.username,
|
455
|
+
password=self.password,
|
456
|
+
)
|
457
|
+
|
458
|
+
self.client.connect_async(
|
459
|
+
host=self.host, # type: ignore
|
460
|
+
port=self.port,
|
461
|
+
)
|
462
|
+
self.client.loop_start()
|
@@ -51,12 +51,12 @@ dependencies = [
|
|
51
51
|
"python-socketio[client]>=5.13.0",
|
52
52
|
"python-engineio>=4.12.2",
|
53
53
|
"pytest>=8.3.3,<9.0.0",
|
54
|
+
"paho-mqtt>=2.1.0",
|
54
55
|
]
|
55
56
|
|
56
57
|
[project.optional-dependencies]
|
57
|
-
milvus = [
|
58
|
-
|
59
|
-
]
|
58
|
+
milvus = ["pymilvus>=2.5.0"]
|
59
|
+
mqtt = ["paho-mqtt>=2.1.0"]
|
60
60
|
|
61
61
|
[project.urls]
|
62
62
|
homepage = "https://locust.io/"
|
@@ -103,11 +103,10 @@ docs = [
|
|
103
103
|
# these optional dependencies are needed to build the some contrib modules
|
104
104
|
"pymilvus>=2.5.0",
|
105
105
|
"psycopg[binary]>=3.2.1",
|
106
|
-
"pymongo>=4.8.0"
|
107
|
-
]
|
108
|
-
milvus = [
|
109
|
-
"pymilvus>=2.5.0",
|
106
|
+
"pymongo>=4.8.0",
|
110
107
|
]
|
108
|
+
milvus = ["pymilvus>=2.5.0"]
|
109
|
+
mqtt = ["paho-mqtt>=2.1.0"]
|
111
110
|
|
112
111
|
[project.scripts]
|
113
112
|
locust = "locust.main:main"
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|