locust 2.29.1.dev34__py3-none-any.whl → 2.29.1.dev39__py3-none-any.whl
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/_version.py +6 -2
- locust/contrib/fasthttp.py +55 -18
- locust/dispatch.py +7 -6
- locust/event.py +32 -0
- locust/runners.py +13 -22
- locust/stats.py +4 -17
- locust/user/sequential_taskset.py +8 -6
- locust/webui/dist/assets/{index-84c63e70.js → index-e9ad42b4.js} +2 -2
- locust/webui/dist/auth.html +1 -1
- locust/webui/dist/index.html +1 -1
- {locust-2.29.1.dev34.dist-info → locust-2.29.1.dev39.dist-info}/METADATA +31 -26
- locust-2.29.1.dev39.dist-info/RECORD +49 -0
- locust-2.29.1.dev39.dist-info/WHEEL +4 -0
- locust-2.29.1.dev39.dist-info/entry_points.txt +3 -0
- locust/test/__init__.py +0 -15
- locust/test/fake_module1_for_env_test.py +0 -7
- locust/test/fake_module2_for_env_test.py +0 -7
- locust/test/mock_locustfile.py +0 -56
- locust/test/mock_logging.py +0 -28
- locust/test/test_debugging.py +0 -39
- locust/test/test_dispatch.py +0 -4170
- locust/test/test_env.py +0 -283
- locust/test/test_fasthttp.py +0 -785
- locust/test/test_http.py +0 -325
- locust/test/test_interruptable_task.py +0 -48
- locust/test/test_load_locustfile.py +0 -228
- locust/test/test_locust_class.py +0 -831
- locust/test/test_log.py +0 -237
- locust/test/test_main.py +0 -2264
- locust/test/test_old_wait_api.py +0 -0
- locust/test/test_parser.py +0 -450
- locust/test/test_runners.py +0 -4342
- locust/test/test_sequential_taskset.py +0 -137
- locust/test/test_stats.py +0 -866
- locust/test/test_tags.py +0 -440
- locust/test/test_taskratio.py +0 -94
- locust/test/test_users.py +0 -69
- locust/test/test_util.py +0 -33
- locust/test/test_wait_time.py +0 -79
- locust/test/test_web.py +0 -1257
- locust/test/test_zmqrpc.py +0 -58
- locust/test/testcases.py +0 -248
- locust/test/util.py +0 -88
- locust-2.29.1.dev34.dist-info/RECORD +0 -79
- locust-2.29.1.dev34.dist-info/WHEEL +0 -5
- locust-2.29.1.dev34.dist-info/entry_points.txt +0 -2
- locust-2.29.1.dev34.dist-info/top_level.txt +0 -1
- {locust-2.29.1.dev34.dist-info → locust-2.29.1.dev39.dist-info}/LICENSE +0 -0
locust/_version.py
CHANGED
@@ -3,6 +3,7 @@
|
|
3
3
|
TYPE_CHECKING = False
|
4
4
|
if TYPE_CHECKING:
|
5
5
|
from typing import Tuple, Union
|
6
|
+
|
6
7
|
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
7
8
|
else:
|
8
9
|
VERSION_TUPLE = object
|
@@ -12,5 +13,8 @@ __version__: str
|
|
12
13
|
__version_tuple__: VERSION_TUPLE
|
13
14
|
version_tuple: VERSION_TUPLE
|
14
15
|
|
15
|
-
|
16
|
-
|
16
|
+
|
17
|
+
__version__ = "2.29.1.dev39"
|
18
|
+
version = __version__
|
19
|
+
__version_tuple__ = (2, 29, 1, "dev39")
|
20
|
+
version_tuple = __version_tuple__
|
locust/contrib/fasthttp.py
CHANGED
@@ -12,12 +12,11 @@ import socket
|
|
12
12
|
import time
|
13
13
|
import traceback
|
14
14
|
from base64 import b64encode
|
15
|
-
from collections.abc import Generator
|
16
15
|
from contextlib import contextmanager
|
17
16
|
from http.cookiejar import CookieJar
|
18
17
|
from json.decoder import JSONDecodeError
|
19
18
|
from ssl import SSLError
|
20
|
-
from typing import
|
19
|
+
from typing import TYPE_CHECKING, cast
|
21
20
|
from urllib.parse import urlparse, urlunparse
|
22
21
|
|
23
22
|
import gevent
|
@@ -32,6 +31,36 @@ from geventhttpclient.useragent import CompatRequest, CompatResponse, Connection
|
|
32
31
|
# borrow requests's content-type header parsing
|
33
32
|
from requests.utils import get_encoding_from_headers
|
34
33
|
|
34
|
+
if TYPE_CHECKING:
|
35
|
+
import sys
|
36
|
+
from collections.abc import Callable, Generator
|
37
|
+
from typing import TypedDict
|
38
|
+
|
39
|
+
if sys.version_info >= (3, 11):
|
40
|
+
from typing import Unpack
|
41
|
+
else:
|
42
|
+
from typing_extensions import Unpack
|
43
|
+
|
44
|
+
class PostKwargs(TypedDict, total=False):
|
45
|
+
name: str | None
|
46
|
+
catch_response: bool
|
47
|
+
stream: bool
|
48
|
+
headers: dict | None
|
49
|
+
auth: tuple[str | bytes, str | bytes] | None
|
50
|
+
allow_redirects: bool
|
51
|
+
context: dict
|
52
|
+
|
53
|
+
class PutKwargs(PostKwargs, total=False):
|
54
|
+
json: dict | None
|
55
|
+
|
56
|
+
class PatchKwargs(PostKwargs, total=False):
|
57
|
+
json: dict | None
|
58
|
+
|
59
|
+
class RESTKwargs(PostKwargs, total=False):
|
60
|
+
data: str | dict | None
|
61
|
+
json: dict | None
|
62
|
+
|
63
|
+
|
35
64
|
# Monkey patch geventhttpclient.useragent.CompatRequest so that Cookiejar works with Python >= 3.3
|
36
65
|
# More info: https://github.com/requests/requests/pull/871
|
37
66
|
CompatRequest.unverifiable = False
|
@@ -85,7 +114,7 @@ class FastHttpSession:
|
|
85
114
|
client_pool: HTTPClientPool | None = None,
|
86
115
|
ssl_context_factory: Callable | None = None,
|
87
116
|
**kwargs,
|
88
|
-
):
|
117
|
+
) -> None:
|
89
118
|
self.environment = environment
|
90
119
|
self.base_url = base_url
|
91
120
|
self.cookiejar = CookieJar()
|
@@ -117,14 +146,14 @@ class FastHttpSession:
|
|
117
146
|
# store authentication header (we construct this by using _basic_auth_str() function from requests.auth)
|
118
147
|
self.auth_header = _construct_basic_auth_str(parsed_url.username, parsed_url.password)
|
119
148
|
|
120
|
-
def _build_url(self, path):
|
149
|
+
def _build_url(self, path: str) -> str:
|
121
150
|
"""prepend url with hostname unless it's already an absolute URL"""
|
122
151
|
if absolute_http_url_regexp.match(path):
|
123
152
|
return path
|
124
153
|
else:
|
125
154
|
return f"{self.base_url}{path}"
|
126
155
|
|
127
|
-
def _send_request_safe_mode(self, method, url, **kwargs):
|
156
|
+
def _send_request_safe_mode(self, method: str, url: str, **kwargs):
|
128
157
|
"""
|
129
158
|
Send an HTTP request, and catch any exception that might occur due to either
|
130
159
|
connection problems, or invalid HTTP status codes
|
@@ -155,9 +184,9 @@ class FastHttpSession:
|
|
155
184
|
catch_response: bool = False,
|
156
185
|
stream: bool = False,
|
157
186
|
headers: dict | None = None,
|
158
|
-
auth=None,
|
187
|
+
auth: tuple[str | bytes, str | bytes] | None = None,
|
159
188
|
json: dict | None = None,
|
160
|
-
allow_redirects=True,
|
189
|
+
allow_redirects: bool = True,
|
161
190
|
context: dict = {},
|
162
191
|
**kwargs,
|
163
192
|
) -> ResponseContextManager | FastResponse:
|
@@ -187,6 +216,7 @@ class FastHttpSession:
|
|
187
216
|
and can instead be consumed by accessing the stream attribute on the Response object.
|
188
217
|
Another side effect of setting stream to True is that the time for downloading the response
|
189
218
|
content will not be accounted for in the request time that is reported by Locust.
|
219
|
+
:param allow_redirects: (optional) Set to True by default.
|
190
220
|
"""
|
191
221
|
# prepend url with hostname unless it's already an absolute URL
|
192
222
|
built_url = self._build_url(url)
|
@@ -250,7 +280,7 @@ class FastHttpSession:
|
|
250
280
|
# Record the consumed time
|
251
281
|
# Note: This is intentionally placed after we record the content_size above, since
|
252
282
|
# we'll then trigger fetching of the body (unless stream=True)
|
253
|
-
request_meta["response_time"] =
|
283
|
+
request_meta["response_time"] = (time.perf_counter() - start_perf_counter) * 1000
|
254
284
|
|
255
285
|
if catch_response:
|
256
286
|
return ResponseContextManager(response, environment=self.environment, request_meta=request_meta)
|
@@ -263,30 +293,37 @@ class FastHttpSession:
|
|
263
293
|
self.environment.events.request.fire(**request_meta)
|
264
294
|
return response
|
265
295
|
|
266
|
-
def delete(self, url, **kwargs):
|
296
|
+
def delete(self, url: str, **kwargs: Unpack[RESTKwargs]) -> ResponseContextManager | FastResponse:
|
297
|
+
"""Sends a DELETE request"""
|
267
298
|
return self.request("DELETE", url, **kwargs)
|
268
299
|
|
269
|
-
def get(self, url, **kwargs):
|
300
|
+
def get(self, url: str, **kwargs: Unpack[RESTKwargs]) -> ResponseContextManager | FastResponse:
|
270
301
|
"""Sends a GET request"""
|
271
302
|
return self.request("GET", url, **kwargs)
|
272
303
|
|
273
|
-
def head(self, url, **kwargs):
|
304
|
+
def head(self, url: str, **kwargs: Unpack[RESTKwargs]) -> ResponseContextManager | FastResponse:
|
274
305
|
"""Sends a HEAD request"""
|
275
306
|
return self.request("HEAD", url, **kwargs)
|
276
307
|
|
277
|
-
def options(self, url, **kwargs):
|
308
|
+
def options(self, url: str, **kwargs: Unpack[RESTKwargs]) -> ResponseContextManager | FastResponse:
|
278
309
|
"""Sends a OPTIONS request"""
|
279
310
|
return self.request("OPTIONS", url, **kwargs)
|
280
311
|
|
281
|
-
def patch(
|
282
|
-
|
312
|
+
def patch(
|
313
|
+
self, url: str, data: str | dict | None = None, **kwargs: Unpack[PatchKwargs]
|
314
|
+
) -> ResponseContextManager | FastResponse:
|
315
|
+
"""Sends a PATCH request"""
|
283
316
|
return self.request("PATCH", url, data=data, **kwargs)
|
284
317
|
|
285
|
-
def post(
|
318
|
+
def post(
|
319
|
+
self, url: str, data: str | dict | None = None, json: dict | None = None, **kwargs: Unpack[PostKwargs]
|
320
|
+
) -> ResponseContextManager | FastResponse:
|
286
321
|
"""Sends a POST request"""
|
287
|
-
return self.request("POST", url, data=data, **kwargs)
|
322
|
+
return self.request("POST", url, data=data, json=json, **kwargs)
|
288
323
|
|
289
|
-
def put(
|
324
|
+
def put(
|
325
|
+
self, url: str, data: str | dict | None = None, **kwargs: Unpack[PutKwargs]
|
326
|
+
) -> ResponseContextManager | FastResponse:
|
290
327
|
"""Sends a PUT request"""
|
291
328
|
return self.request("PUT", url, data=data, **kwargs)
|
292
329
|
|
@@ -446,7 +483,7 @@ class FastResponse(CompatResponse):
|
|
446
483
|
|
447
484
|
_response: HTTPSocketPoolResponse | None = None
|
448
485
|
|
449
|
-
encoding: str | None = None
|
486
|
+
encoding: str | float | None = None
|
450
487
|
"""In some cases setting the encoding explicitly is needed. If so, do it before calling .text"""
|
451
488
|
|
452
489
|
request: FastRequest | None = None
|
locust/dispatch.py
CHANGED
@@ -90,13 +90,13 @@ class UsersDispatcher(Iterator):
|
|
90
90
|
assert len(user_classes) > 0
|
91
91
|
assert len(set(self._user_classes)) == len(self._user_classes)
|
92
92
|
|
93
|
-
self._target_user_count: int =
|
93
|
+
self._target_user_count: int = 0
|
94
94
|
|
95
|
-
self._spawn_rate: float =
|
95
|
+
self._spawn_rate: float = 0.0
|
96
96
|
|
97
|
-
self._user_count_per_dispatch_iteration: int =
|
97
|
+
self._user_count_per_dispatch_iteration: int = 0
|
98
98
|
|
99
|
-
self._wait_between_dispatch: float =
|
99
|
+
self._wait_between_dispatch: float = 0.0
|
100
100
|
|
101
101
|
self._initial_users_on_workers = {
|
102
102
|
worker_node.id: {user_class.__name__: 0 for user_class in self._user_classes}
|
@@ -107,7 +107,8 @@ class UsersDispatcher(Iterator):
|
|
107
107
|
|
108
108
|
self._current_user_count = self.get_current_user_count()
|
109
109
|
|
110
|
-
self._dispatcher_generator: Generator[dict[str, dict[str, int]], None, None] = None
|
110
|
+
self._dispatcher_generator: Generator[dict[str, dict[str, int]], None, None] = None # type: ignore
|
111
|
+
# a generator is assigned (in new_dispatch()) to _dispatcher_generator before it's used
|
111
112
|
|
112
113
|
self._user_generator = self._user_gen()
|
113
114
|
|
@@ -131,7 +132,7 @@ class UsersDispatcher(Iterator):
|
|
131
132
|
return sum(map(sum, map(dict.values, self._users_on_workers.values())))
|
132
133
|
|
133
134
|
@property
|
134
|
-
def dispatch_in_progress(self):
|
135
|
+
def dispatch_in_progress(self) -> bool:
|
135
136
|
return self._dispatch_in_progress
|
136
137
|
|
137
138
|
@property
|
locust/event.py
CHANGED
@@ -235,6 +235,38 @@ class Events:
|
|
235
235
|
Fired when the CPU usage exceeds runners.CPU_WARNING_THRESHOLD (90% by default)
|
236
236
|
"""
|
237
237
|
|
238
|
+
heartbeat_sent: EventHook
|
239
|
+
"""
|
240
|
+
Fired when a heartbeat is sent by master to a worker.
|
241
|
+
|
242
|
+
Event arguments:
|
243
|
+
|
244
|
+
:param client_id: worker client id
|
245
|
+
:param timestamp: time in seconds since the epoch (float) when the event occured
|
246
|
+
"""
|
247
|
+
|
248
|
+
heartbeat_received: EventHook
|
249
|
+
"""
|
250
|
+
Fired when a heartbeat is received by a worker from master.
|
251
|
+
|
252
|
+
Event arguments:
|
253
|
+
|
254
|
+
:param client_id: worker client id
|
255
|
+
:param timestamp: time in seconds since the epoch (float) when the event occured
|
256
|
+
"""
|
257
|
+
|
258
|
+
usage_monitor: EventHook
|
259
|
+
"""
|
260
|
+
Fired every runners.CPU_MONITOR_INTERVAL (5.0 seconds by default) with information about
|
261
|
+
current CPU and memory usage.
|
262
|
+
|
263
|
+
Event arguments:
|
264
|
+
|
265
|
+
:param environment: locust environment
|
266
|
+
:param cpu_usage: current CPU usage in percent
|
267
|
+
:param memory_usage: current memory usage (RSS) in bytes
|
268
|
+
"""
|
269
|
+
|
238
270
|
def __init__(self):
|
239
271
|
# For backward compatibility use also values of class attributes
|
240
272
|
for name, value in vars(type(self)).items():
|
locust/runners.py
CHANGED
@@ -15,19 +15,9 @@ import traceback
|
|
15
15
|
from abc import abstractmethod
|
16
16
|
from collections import defaultdict
|
17
17
|
from collections.abc import Iterator, MutableMapping, ValuesView
|
18
|
-
from operator import
|
19
|
-
itemgetter,
|
20
|
-
methodcaller,
|
21
|
-
)
|
18
|
+
from operator import itemgetter, methodcaller
|
22
19
|
from types import TracebackType
|
23
|
-
from typing import
|
24
|
-
TYPE_CHECKING,
|
25
|
-
Any,
|
26
|
-
Callable,
|
27
|
-
NoReturn,
|
28
|
-
TypedDict,
|
29
|
-
cast,
|
30
|
-
)
|
20
|
+
from typing import TYPE_CHECKING, Any, Callable, NoReturn, TypedDict, cast
|
31
21
|
from uuid import uuid4
|
32
22
|
|
33
23
|
import gevent
|
@@ -40,15 +30,8 @@ from . import argument_parser
|
|
40
30
|
from .dispatch import UsersDispatcher
|
41
31
|
from .exception import RPCError, RPCReceiveError, RPCSendError
|
42
32
|
from .log import get_logs, greenlet_exception_logger
|
43
|
-
from .rpc import
|
44
|
-
|
45
|
-
rpc,
|
46
|
-
)
|
47
|
-
from .stats import (
|
48
|
-
RequestStats,
|
49
|
-
StatsError,
|
50
|
-
setup_distributed_stats_event_listeners,
|
51
|
-
)
|
33
|
+
from .rpc import Message, rpc
|
34
|
+
from .stats import RequestStats, StatsError, setup_distributed_stats_event_listeners
|
52
35
|
|
53
36
|
if TYPE_CHECKING:
|
54
37
|
from . import User
|
@@ -106,7 +89,7 @@ class Runner:
|
|
106
89
|
self.spawning_greenlet: gevent.Greenlet | None = None
|
107
90
|
self.shape_greenlet: gevent.Greenlet | None = None
|
108
91
|
self.shape_last_tick: tuple[int, float] | tuple[int, float, list[type[User]] | None] | None = None
|
109
|
-
self.current_cpu_usage:
|
92
|
+
self.current_cpu_usage: float = 0.0
|
110
93
|
self.cpu_warning_emitted: bool = False
|
111
94
|
self.worker_cpu_warning_emitted: bool = False
|
112
95
|
self.current_memory_usage: int = 0
|
@@ -308,6 +291,10 @@ class Runner:
|
|
308
291
|
f"CPU usage above {CPU_WARNING_THRESHOLD}%! This may constrain your throughput and may even give inconsistent response time measurements! See https://docs.locust.io/en/stable/running-distributed.html for how to distribute the load over multiple CPU cores or machines"
|
309
292
|
)
|
310
293
|
self.cpu_warning_emitted = True
|
294
|
+
|
295
|
+
self.environment.events.usage_monitor.fire(
|
296
|
+
environment=self.environment, cpu_usage=self.current_cpu_usage, memory_usage=self.current_memory_usage
|
297
|
+
)
|
311
298
|
gevent.sleep(CPU_MONITOR_INTERVAL)
|
312
299
|
|
313
300
|
@abstractmethod
|
@@ -1102,6 +1089,7 @@ class MasterRunner(DistributedRunner):
|
|
1102
1089
|
)
|
1103
1090
|
if "current_memory_usage" in msg.data:
|
1104
1091
|
c.memory_usage = msg.data["current_memory_usage"]
|
1092
|
+
self.environment.events.heartbeat_sent.fire(client_id=msg.node_id, timestamp=time.time())
|
1105
1093
|
self.server.send_to_client(Message("heartbeat", None, msg.node_id))
|
1106
1094
|
else:
|
1107
1095
|
logging.debug(f"Got heartbeat message from unknown worker {msg.node_id}")
|
@@ -1399,6 +1387,9 @@ class WorkerRunner(DistributedRunner):
|
|
1399
1387
|
self.reset_connection()
|
1400
1388
|
elif msg.type == "heartbeat":
|
1401
1389
|
self.last_heartbeat_timestamp = time.time()
|
1390
|
+
self.environment.events.heartbeat_received.fire(
|
1391
|
+
client_id=msg.node_id, timestamp=self.last_heartbeat_timestamp
|
1392
|
+
)
|
1402
1393
|
elif msg.type == "update_user_class":
|
1403
1394
|
self.environment.update_user_class(msg.data)
|
1404
1395
|
elif msg.type == "spawning_complete":
|
locust/stats.py
CHANGED
@@ -9,25 +9,12 @@ import signal
|
|
9
9
|
import time
|
10
10
|
from abc import abstractmethod
|
11
11
|
from collections import OrderedDict, defaultdict, namedtuple
|
12
|
-
from collections import (
|
13
|
-
OrderedDict as OrderedDictType,
|
14
|
-
)
|
15
12
|
from collections.abc import Iterable
|
16
13
|
from copy import copy
|
17
14
|
from html import escape
|
18
15
|
from itertools import chain
|
19
|
-
from tempfile import NamedTemporaryFile
|
20
16
|
from types import FrameType
|
21
|
-
from typing import
|
22
|
-
TYPE_CHECKING,
|
23
|
-
Any,
|
24
|
-
Callable,
|
25
|
-
NoReturn,
|
26
|
-
Protocol,
|
27
|
-
TypedDict,
|
28
|
-
TypeVar,
|
29
|
-
cast,
|
30
|
-
)
|
17
|
+
from typing import TYPE_CHECKING, Any, Callable, NoReturn, Protocol, TypedDict, TypeVar, cast
|
31
18
|
|
32
19
|
import gevent
|
33
20
|
|
@@ -319,12 +306,12 @@ class StatsEntry:
|
|
319
306
|
A {response_time => count} dict that holds the response time distribution of all
|
320
307
|
the requests.
|
321
308
|
|
322
|
-
The keys (the response time in ms) are rounded to store 1, 2, ...
|
323
|
-
|
309
|
+
The keys (the response time in ms) are rounded to store 1, 2, ... 98, 99, 100, 110, 120, ... 980, 990, 1000,
|
310
|
+
1100, 1200, ... 9800, 9900, 10_000, 11_000, 12_000 ... in order to save memory.
|
324
311
|
|
325
312
|
This dict is used to calculate the median and percentile response times.
|
326
313
|
"""
|
327
|
-
self.response_times_cache:
|
314
|
+
self.response_times_cache: OrderedDict[int, CachedResponseTimes] | None = None
|
328
315
|
"""
|
329
316
|
If use_response_times_cache is set to True, this will be a {timestamp => CachedResponseTimes()}
|
330
317
|
OrderedDict that holds a copy of the response_times dict for each of the last 20 seconds.
|
@@ -1,6 +1,6 @@
|
|
1
1
|
from locust.exception import LocustError
|
2
2
|
|
3
|
-
import
|
3
|
+
from itertools import cycle
|
4
4
|
|
5
5
|
from .task import TaskSet, TaskSetMeta
|
6
6
|
|
@@ -26,8 +26,12 @@ class SequentialTaskSetMeta(TaskSetMeta):
|
|
26
26
|
# compared to methods declared with @task
|
27
27
|
if isinstance(value, list):
|
28
28
|
new_tasks.extend(value)
|
29
|
+
elif isinstance(value, dict):
|
30
|
+
for task, weight in value.items():
|
31
|
+
for _ in range(weight):
|
32
|
+
new_tasks.append(task)
|
29
33
|
else:
|
30
|
-
raise ValueError("
|
34
|
+
raise ValueError("The 'tasks' attribute can only be set to list or dict")
|
31
35
|
|
32
36
|
if "locust_task_weight" in dir(value):
|
33
37
|
# method decorated with @task
|
@@ -52,13 +56,11 @@ class SequentialTaskSet(TaskSet, metaclass=SequentialTaskSetMeta):
|
|
52
56
|
|
53
57
|
def __init__(self, *args, **kwargs):
|
54
58
|
super().__init__(*args, **kwargs)
|
55
|
-
self.
|
59
|
+
self._task_cycle = cycle(self.tasks)
|
56
60
|
|
57
61
|
def get_next_task(self):
|
58
62
|
if not self.tasks:
|
59
63
|
raise LocustError(
|
60
64
|
"No tasks defined. Use the @task decorator or set the 'tasks' attribute of the SequentialTaskSet"
|
61
65
|
)
|
62
|
-
|
63
|
-
self._task_index += 1
|
64
|
-
return task
|
66
|
+
return next(self._task_cycle)
|