locust 2.39.2.dev5__py3-none-any.whl → 2.39.2.dev9__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 CHANGED
@@ -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.39.2.dev5'
32
- __version_tuple__ = version_tuple = (2, 39, 2, 'dev5')
31
+ __version__ = version = '2.39.2.dev9'
32
+ __version_tuple__ = version_tuple = (2, 39, 2, 'dev9')
33
33
 
34
34
  __commit_id__ = commit_id = None
locust/clients.py CHANGED
@@ -185,18 +185,20 @@ class HttpSession(requests.Session):
185
185
  name = self.request_name
186
186
 
187
187
  # prepend url with hostname unless it's already an absolute URL
188
- url = self._build_url(url)
188
+ complete_url = self._build_url(url)
189
189
 
190
190
  start_time = time.time()
191
191
  start_perf_counter = time.perf_counter()
192
- response = self._send_request_safe_mode(method, url, data=data, json=json, **kwargs)
192
+ response = self._send_request_safe_mode(method, complete_url, data=data, json=json, **kwargs)
193
193
  response_time = (time.perf_counter() - start_perf_counter) * 1000
194
194
 
195
- request_before_redirect = (response.history and response.history[0] or response).request
196
- url = request_before_redirect.url # type: ignore
197
-
198
- if not name:
199
- name = request_before_redirect.path_url
195
+ if request_before_redirect := (response.history and response.history[0] or response).request:
196
+ complete_url = str(request_before_redirect.url)
197
+ if not name:
198
+ name = request_before_redirect.path_url
199
+ else: # in rare cases, such as ProtocolError('Response ended prematurely'), response.request may be None
200
+ if not name:
201
+ name = str(url)
200
202
 
201
203
  if self.user:
202
204
  context = {**self.user.context(), **context}
@@ -210,7 +212,7 @@ class HttpSession(requests.Session):
210
212
  "response": response,
211
213
  "exception": None,
212
214
  "start_time": start_time,
213
- "url": url,
215
+ "url": complete_url,
214
216
  }
215
217
 
216
218
  # get the length of the content, but if the argument stream is set to True, we take
locust/main.py CHANGED
@@ -30,7 +30,7 @@ from .html import get_html_report, process_html_filename
30
30
  from .input_events import input_listener
31
31
  from .log import greenlet_exception_logger, setup_logging
32
32
  from .user.inspectuser import print_task_ratio, print_task_ratio_json
33
- from .util.load_locustfile import load_locustfile
33
+ from .util.load_locustfile import load_locustfile, load_locustfile_pytest
34
34
 
35
35
  # import external plugins if installed to allow for registering custom arguments etc
36
36
  try:
@@ -123,9 +123,7 @@ def merge_locustfiles_content(
123
123
  # Check docs for real supported task attribute signature for User\TaskSet class.
124
124
  available_user_tasks: dict[str, list[locust.TaskSet | Callable]] = {}
125
125
 
126
- for _locustfile in locustfiles:
127
- user_classes, shape_classes = load_locustfile(_locustfile)
128
-
126
+ def set_available_things(user_classes, shape_classes):
129
127
  # Setting Available Shape Classes
130
128
  for _shape_class in shape_classes:
131
129
  shape_class_name = type(_shape_class).__name__
@@ -152,6 +150,15 @@ def merge_locustfiles_content(
152
150
  available_user_classes[class_name] = class_definition
153
151
  available_user_tasks[class_name] = class_definition.tasks
154
152
 
153
+ for _locustfile in locustfiles:
154
+ user_classes, shape_classes = load_locustfile(_locustfile)
155
+ set_available_things(user_classes, shape_classes)
156
+
157
+ if not available_user_classes: # only load pytest-based locustfiles if no regular ones were found
158
+ for _locustfile in locustfiles:
159
+ user_classes = load_locustfile_pytest(_locustfile)
160
+ set_available_things(user_classes, [])
161
+
155
162
  shape_class = list(available_shape_classes.values())[0] if available_shape_classes else None
156
163
 
157
164
  return available_user_classes, available_shape_classes, available_user_tasks, shape_class
locust/pytestplugin.py ADDED
@@ -0,0 +1,44 @@
1
+ from locust.clients import HttpSession
2
+ from locust.contrib.fasthttp import FastHttpSession
3
+ from locust.event import EventHook
4
+ from locust.user.users import User
5
+
6
+ import pytest
7
+
8
+
9
+ class NoOpEvent(EventHook):
10
+ pass
11
+
12
+
13
+ _config: pytest.Config
14
+
15
+
16
+ # capture Config object instead of having to pass it explicitly to fixtures, which would make them more complex to call from Locust
17
+ def pytest_configure(config):
18
+ global _config
19
+ _config = config
20
+
21
+
22
+ @pytest.fixture
23
+ def session(user: User | None = None):
24
+ s = HttpSession(
25
+ base_url=user.host if user else _config.getoption("--host"),
26
+ request_event=user.environment.events.request if user else NoOpEvent(),
27
+ user=user,
28
+ )
29
+ yield s
30
+ s.close()
31
+
32
+
33
+ @pytest.fixture
34
+ def fastsession(user: User | None = None):
35
+ s = FastHttpSession(
36
+ base_url=user.host if user else _config.getoption("--host"),
37
+ request_event=user.environment.events.request if user else NoOpEvent(),
38
+ user=user,
39
+ )
40
+ yield s
41
+
42
+
43
+ def pytest_addoption(parser: pytest.Parser) -> None:
44
+ parser.addoption("--host", "-H", action="store", default=None)
locust/user/task.py CHANGED
@@ -211,7 +211,11 @@ def filter_tasks_by_tags(
211
211
  checked[task] = passing
212
212
 
213
213
  task_holder.tasks = new_tasks
214
- if not new_tasks and not is_markov_taskset(task_holder):
214
+ if (
215
+ not new_tasks
216
+ and not is_markov_taskset(task_holder)
217
+ and not getattr(task_holder, "functions", False) # PytestUser
218
+ ):
215
219
  logging.warning(f"{task_holder.__name__} had no tasks left after filtering, instantiating it will fail!")
216
220
 
217
221
 
locust/user/users.py CHANGED
@@ -14,15 +14,24 @@ from locust.user.wait_time import constant
14
14
  from locust.util import deprecation
15
15
 
16
16
  import logging
17
+ import sys
17
18
  import time
18
19
  import traceback
19
20
  from collections.abc import Callable
20
21
  from typing import final
21
22
 
23
+ import pytest
22
24
  from gevent import GreenletExit, greenlet
23
25
  from gevent.pool import Group
26
+ from geventhttpclient.useragent import ConnectionError
27
+ from requests.exceptions import RequestException
24
28
  from urllib3 import PoolManager
25
29
 
30
+ if sys.version_info >= (3, 12):
31
+ from typing import override
32
+ else:
33
+ from typing_extensions import override
34
+
26
35
  logger = logging.getLogger(__name__)
27
36
 
28
37
 
@@ -144,7 +153,7 @@ class User(metaclass=UserMeta):
144
153
  self._state = LOCUST_STATE_RUNNING
145
154
  self._taskset_instance = DefaultTaskSet(self)
146
155
  try:
147
- # run the TaskSet on_start method, if it has one
156
+ # run the User on_start method, if it has one
148
157
  try:
149
158
  self.on_start()
150
159
  except Exception as e:
@@ -274,3 +283,24 @@ class HttpUser(User):
274
283
  The client supports cookies, and therefore keeps the session between HTTP requests.
275
284
  """
276
285
  self.client.trust_env = False
286
+
287
+
288
+ class PytestUser(User):
289
+ abstract = True
290
+ functions: list[pytest.Function]
291
+ fixtures: list
292
+
293
+ @override
294
+ def run(self): # type: ignore[override] # We actually DO want to change the default User behavior
295
+ self._state = LOCUST_STATE_RUNNING
296
+ self.fixtures = [next(f.fixturedef.func(self)) for f in self.functions] # type: ignore[attr-defined]
297
+ while True:
298
+ for i in range(len(self.fixtures)):
299
+ try: # try-except is for supporting .raise_for_status() in tests
300
+ self.functions[i].obj(self.fixtures[i])
301
+ except RequestException as e:
302
+ if isinstance(e, ValueError): # things like MissingSchema etc, lets not catch that
303
+ raise
304
+ logger.debug("%s\n%s", e, traceback.format_exc())
305
+ except ConnectionError as e:
306
+ logger.debug("%s\n%s", e, traceback.format_exc())
@@ -6,8 +6,12 @@ import inspect
6
6
  import os
7
7
  import sys
8
8
 
9
+ import pytest
10
+ from _pytest.config import Config
11
+
9
12
  from ..shape import LoadTestShape
10
13
  from ..user import User
14
+ from ..user.users import PytestUser
11
15
 
12
16
 
13
17
  def is_user_class(item) -> bool:
@@ -81,5 +85,45 @@ def load_locustfile(path) -> tuple[dict[str, type[User]], list[LoadTestShape]]:
81
85
 
82
86
  # Find shape class, if any, return it
83
87
  shape_classes = [value() for value in vars(imported).values() if is_shape_class(value)]
84
-
85
88
  return user_classes, shape_classes
89
+
90
+
91
+ def load_locustfile_pytest(path) -> dict[str, type[User]]:
92
+ """
93
+ Create User classes from pytest test functions.
94
+
95
+ It relies on some pytest internals to collect test functions and their fixtures,
96
+ but it should be reasonably stable for simple use cases.
97
+
98
+ Fixtures (like `session` and `fastsession`) are defined in `pytestplugin.py`
99
+
100
+ See `examples/test_pytest.py` and `locust/test/test_pytest_locustfile.py`
101
+ """
102
+ user_classes: dict[str, type[PytestUser]] = {}
103
+ # collect tests and set up fixture manager
104
+ config = Config.fromdictargs({}, [path])
105
+ config._do_configure()
106
+ session = pytest.Session.from_config(config)
107
+ config.hook.pytest_sessionstart(session=session)
108
+ session.perform_collect()
109
+ config.hook.pytest_collection_modifyitems(session=session, config=config, items=session.items)
110
+ fm = session._fixturemanager
111
+
112
+ for function in session.items:
113
+ if isinstance(function, pytest.Function):
114
+ sig = inspect.signature(function.obj)
115
+ function.kwargs = {} # type: ignore[attr-defined]
116
+ for name in sig.parameters:
117
+ defs = fm.getfixturedefs(name, function)
118
+ if not defs:
119
+ raise ValueError(f"Could not find fixture for parameter {name!r} in {function.name}")
120
+ if len(defs) > 1:
121
+ raise ValueError(f"Multiple fixtures found for parameter {name!r} in {function.name}: {defs}")
122
+ function.fixturedef = defs[0] # type: ignore[attr-defined]
123
+ if not function.name in user_classes:
124
+ user_classes[function.name] = type(function.name, (PytestUser,), {})
125
+ user_classes[function.name].functions = []
126
+ user_classes[function.name].functions.append(function)
127
+ else:
128
+ pass # Skipping non-function item
129
+ return user_classes # type: ignore
locust/web.py CHANGED
@@ -320,7 +320,7 @@ class WebUI:
320
320
  return jsonify({"success": False, "message": err_msg, "host": environment.host})
321
321
  self._swarm_greenlet = gevent.spawn(environment.runner.start, user_count, spawn_rate)
322
322
  self._swarm_greenlet.link_exception(greenlet_exception_handler)
323
- response_data = {
323
+ response_data: dict[str, Any] = {
324
324
  "success": True,
325
325
  "message": "Swarming started",
326
326
  "host": environment.host,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: locust
3
- Version: 2.39.2.dev5
3
+ Version: 2.39.2.dev9
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
@@ -33,6 +33,7 @@ Requires-Dist: geventhttpclient>=2.3.1
33
33
  Requires-Dist: locust-cloud>=1.26.3
34
34
  Requires-Dist: msgpack>=1.0.0
35
35
  Requires-Dist: psutil>=5.9.1
36
+ Requires-Dist: pytest<9.0.0,>=8.3.3
36
37
  Requires-Dist: python-engineio>=4.12.2
37
38
  Requires-Dist: python-socketio[client]>=5.13.0
38
39
  Requires-Dist: pywin32; sys_platform == 'win32'
@@ -41,7 +42,7 @@ Requires-Dist: requests>=2.26.0; python_version <= '3.11'
41
42
  Requires-Dist: requests>=2.32.2; python_version > '3.11'
42
43
  Requires-Dist: setuptools>=70.0.0
43
44
  Requires-Dist: tomli>=1.1.0; python_version < '3.11'
44
- Requires-Dist: typing-extensions>=4.6.0; python_version < '3.11'
45
+ 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'
@@ -1,8 +1,8 @@
1
1
  locust/__init__.py,sha256=HadpgGidiyCDPSKwkxrk1Qw6eB7dTmftNJVftuJzAiw,1876
2
2
  locust/__main__.py,sha256=vBQ82334kX06ImDbFlPFgiBRiLIinwNk3z8Khs6hd74,31
3
- locust/_version.py,sha256=fRtcXazmY5njv-S1j8yP2Xoo7Da8whdN6bT5b5ZLlvY,719
3
+ locust/_version.py,sha256=VX41U7Wggg2m1yGrPlakiKFDzYhAx7npbEREkp0qihE,719
4
4
  locust/argument_parser.py,sha256=t6mAoK9u13DxC9UH-alVqS6fFABFTyNWSJG89yQ4QQQ,33056
5
- locust/clients.py,sha256=o-277lWQdpmPnoRTdf3IQVNPQT8LMFDtPtuxbLHQIIs,19286
5
+ locust/clients.py,sha256=CeyTYgKuASZS6QKc71T4V3bqYPGlJr3Ef_i6qP07gTQ,19498
6
6
  locust/debug.py,sha256=7CCm8bIg44uGH2wqBlo1rXBzV2VzwPicLxLewz8r5CQ,5099
7
7
  locust/dispatch.py,sha256=prdwtb9EoN4A9klgiKgWuwQmvFB8hEuFHOK6ot62AJI,16202
8
8
  locust/env.py,sha256=PypNHmEFBhGBk9dU6pZ2cL5L0TThYejaGWW16RO3ZNQ,13203
@@ -11,12 +11,13 @@ locust/exception.py,sha256=jGgJ32ubuf4pWdlaVOkbh2Y0LlG0_DHi-lv3ib8ppOE,1791
11
11
  locust/html.py,sha256=18LlaL-NEQdthXVdS16gRAd4SOpW_bowOJKyuLvWrCY,4043
12
12
  locust/input_events.py,sha256=lqLDB2Ax-OQ7-vtYNkGjySjfaWVobBzqf0GxRwjcLcQ,3323
13
13
  locust/log.py,sha256=5E2ZUOa3V4sfCqa-470Gle1Ik9L5nxYitsjEB9tRwE0,3455
14
- locust/main.py,sha256=P9r-CxA3Hr1U3JKU9WsMkc-Mf5PTeW7JfclezXP3nqY,28497
14
+ locust/main.py,sha256=rcUD3MnBnFqeXC41fjGkh4p9POLGWglgPvRAfHKtEnc,28896
15
15
  locust/py.typed,sha256=gkWLl8yD4mIZnNYYAIRM8g9VarLvWmTAFeUfEbxJLBw,65
16
+ locust/pytestplugin.py,sha256=fc616tXWQ0FvZ6aa_sBzvaCwjqsBo-tg8uP3-0jxcdU,1116
16
17
  locust/runners.py,sha256=niYmGsfOpxMfVmTXGod4MYTefpaZ2wirFlhqxRw5mq4,70617
17
18
  locust/shape.py,sha256=t-lwBS8LOjWcKXNL7j2U3zroIXJ1b0fazUwpRYQOKXw,1973
18
19
  locust/stats.py,sha256=qyoSKT0i7RunLDj5pMGqizK1Sp8bcqUsXwh2m4_DpR8,47203
19
- locust/web.py,sha256=HLFN9jUtKG3sMIKu_Xw9wtvTAFxXvzDHdtLtfb_JxUQ,31849
20
+ locust/web.py,sha256=pLYuocmx9gifJ4vsVDgGDYIPkQhQxI7kKjxoXcUajqM,31865
20
21
  locust/contrib/__init__.py,sha256=LtZN7MczpIAbZkN7PT2h8W2wgb9nBl6cDXbFCVsV4fo,290
21
22
  locust/contrib/fasthttp.py,sha256=c8MznjqbtYTKZPc20OC4GCiaENDCJmmNSNuHqtMhh2Q,29883
22
23
  locust/contrib/milvus.py,sha256=YabgLd0lImzWupJFCm0OZAW-Nxeibwn91ldWpZ2irDo,12811
@@ -31,8 +32,8 @@ locust/user/__init__.py,sha256=RgdRCflP2dIDcvwVMdhPQHAMhWVwQarQ9wWjF9HKk0w,151
31
32
  locust/user/inspectuser.py,sha256=KgrWHyE5jhK6or58R7soLRf-_st42AaQrR72qbiXw9E,2641
32
33
  locust/user/markov_taskset.py,sha256=eESre6OacbP7nTzZFwxUe7TO4X4l7WqOAEETtDzsIfU,11784
33
34
  locust/user/sequential_taskset.py,sha256=SbrrGU9HV2nEWe6zQVtjymn8NgPISP7QSNoVdyoXjYg,2687
34
- locust/user/task.py,sha256=k7g86WYm1I-tuNm2nVI-3TZegGhmo-pIk7pPFILk3yc,17147
35
- locust/user/users.py,sha256=c3Dtldg5ypA0rAE4eBn1mCzFtJ-Nq9BPblqM67wEjSY,10016
35
+ locust/user/task.py,sha256=QnNDs8lEZGgMqBe8l_O4FKgn4WUL9r5WTRHWw88nO-w,17242
36
+ locust/user/users.py,sha256=n2IE7MwIKj2JUgYU4_7Vv-cNk-NWDRhj62dQbxrGhIk,11204
36
37
  locust/user/wait_time.py,sha256=bGRKMVx4lom75sX3POYJUa1CPeME2bEAXG6CEgxSO5U,2675
37
38
  locust/util/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
38
39
  locust/util/cache.py,sha256=IxbpGawl0-hoWKvCrtksxjSLf2GbBBTVns06F7mFBkM,1062
@@ -40,7 +41,7 @@ locust/util/date.py,sha256=2uZAY-fkJq7llUcVywTDTbe-_2IYumCv18n3Vrc75gw,833
40
41
  locust/util/deprecation.py,sha256=4my4IcFpbM6yEKr569GMUKMsS0ywp0N4JPhhwm3J1-w,2026
41
42
  locust/util/directory.py,sha256=2EeuVIIFEErm0OpNXdsEgQLx49jAXq-PvMj2uY0Mr8o,326
42
43
  locust/util/exception_handler.py,sha256=jTMyBq2a0O07fRjmqGkheyaPj58tUgnbbcjoesKGPws,797
43
- locust/util/load_locustfile.py,sha256=0ncZh2GQvkiItZB0ePy6JQ_2ayZNP9jXOTfUhdvlQB4,2956
44
+ locust/util/load_locustfile.py,sha256=KzrLXeLDhWo7ia9hxACkuES7xykgaBlIt8yJQkHYs8U,4929
44
45
  locust/util/rounding.py,sha256=5haxR8mKhATqag6WvPby-MSRRgIw5Ob6thbyvMYZM7o,92
45
46
  locust/util/timespan.py,sha256=Y0LtnhUq2Mq19p04u0XtBlYQ_-S2cRvwRdgru8W9WhA,986
46
47
  locust/util/url.py,sha256=s_W2PCxvxTWxWX0yUvp-8VBuQm881KwI5X9iifogZG4,321
@@ -55,8 +56,8 @@ locust/webui/dist/assets/index-BjqxSg7R.js,sha256=3JyrKWfAg8LlTy2bxAJh73c6njNPhN
55
56
  locust/webui/dist/assets/terminal.gif,sha256=iw80LO2u0dnf4wpGfFJZauBeKTcSpw9iUfISXT2nEF4,75302
56
57
  locust/webui/dist/assets/testruns-dark.png,sha256=G4p2VZSBuuqF4neqUaPSshIp5OKQJ_Bvb69Luj6XuVs,125231
57
58
  locust/webui/dist/assets/testruns-light.png,sha256=JinGDiiBPOkhpfF-XCbmQqhRInqItrjrBTLKt5MlqVI,130301
58
- locust-2.39.2.dev5.dist-info/METADATA,sha256=QPujpa-6pGk9u1kOGbIvJxfXBQXx5th08GUINKOASVM,9562
59
- locust-2.39.2.dev5.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
60
- locust-2.39.2.dev5.dist-info/entry_points.txt,sha256=RAdt8Ku-56m7bFjmdj-MBhbF6h4NX7tVODR9QNnOg0E,44
61
- locust-2.39.2.dev5.dist-info/licenses/LICENSE,sha256=5hnz-Vpj0Z3kSCQl0LzV2hT1TLc4LHcbpBp3Cy-EuyM,1110
62
- locust-2.39.2.dev5.dist-info/RECORD,,
59
+ locust-2.39.2.dev9.dist-info/METADATA,sha256=JW2c3PONjpR3wtpXUW16q2ltIY0St-LrHZkiNabsiQg,9598
60
+ locust-2.39.2.dev9.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
61
+ locust-2.39.2.dev9.dist-info/entry_points.txt,sha256=k0386Xi0vBR3OVIqeGNYz7cz8QUGJ3iLmQ0WQU91ogw,85
62
+ locust-2.39.2.dev9.dist-info/licenses/LICENSE,sha256=5hnz-Vpj0Z3kSCQl0LzV2hT1TLc4LHcbpBp3Cy-EuyM,1110
63
+ locust-2.39.2.dev9.dist-info/RECORD,,
@@ -1,2 +1,5 @@
1
1
  [console_scripts]
2
2
  locust = locust.main:main
3
+
4
+ [pytest11]
5
+ locust = locust.pytestplugin