locust 2.20.1.dev28__py3-none-any.whl → 2.20.2__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/__init__.py +7 -7
- locust/_version.py +2 -2
- locust/argument_parser.py +35 -18
- locust/clients.py +7 -6
- locust/contrib/fasthttp.py +42 -37
- locust/debug.py +14 -8
- locust/dispatch.py +25 -25
- locust/env.py +45 -37
- locust/event.py +13 -10
- locust/exception.py +0 -9
- locust/html.py +9 -8
- locust/input_events.py +7 -5
- locust/main.py +95 -47
- locust/rpc/protocol.py +4 -2
- locust/rpc/zmqrpc.py +6 -4
- locust/runners.py +69 -76
- locust/shape.py +7 -4
- locust/stats.py +73 -71
- locust/templates/index.html +8 -23
- locust/test/mock_logging.py +7 -5
- locust/test/test_debugging.py +4 -4
- locust/test/test_dispatch.py +10 -9
- locust/test/test_env.py +30 -1
- locust/test/test_fasthttp.py +9 -8
- locust/test/test_http.py +4 -3
- locust/test/test_interruptable_task.py +4 -4
- locust/test/test_load_locustfile.py +6 -5
- locust/test/test_locust_class.py +11 -5
- locust/test/test_log.py +5 -4
- locust/test/test_main.py +37 -7
- locust/test/test_parser.py +11 -15
- locust/test/test_runners.py +22 -21
- locust/test/test_sequential_taskset.py +3 -2
- locust/test/test_stats.py +16 -18
- locust/test/test_tags.py +3 -2
- locust/test/test_taskratio.py +3 -3
- locust/test/test_users.py +3 -3
- locust/test/test_util.py +3 -2
- locust/test/test_wait_time.py +3 -3
- locust/test/test_web.py +142 -27
- locust/test/test_zmqrpc.py +5 -3
- locust/test/testcases.py +7 -7
- locust/test/util.py +6 -7
- locust/user/__init__.py +1 -1
- locust/user/inspectuser.py +6 -5
- locust/user/sequential_taskset.py +3 -1
- locust/user/task.py +22 -27
- locust/user/users.py +17 -7
- locust/util/deprecation.py +0 -1
- locust/util/exception_handler.py +1 -1
- locust/util/load_locustfile.py +4 -2
- locust/web.py +102 -53
- locust/webui/dist/assets/auth-5e21717c.js.map +1 -0
- locust/webui/dist/assets/{index-01afe4fa.js → index-a83a5dd9.js} +85 -84
- locust/webui/dist/assets/index-a83a5dd9.js.map +1 -0
- locust/webui/dist/auth.html +20 -0
- locust/webui/dist/index.html +1 -1
- {locust-2.20.1.dev28.dist-info → locust-2.20.2.dist-info}/METADATA +2 -2
- locust-2.20.2.dist-info/RECORD +105 -0
- locust-2.20.1.dev28.dist-info/RECORD +0 -102
- {locust-2.20.1.dev28.dist-info → locust-2.20.2.dist-info}/LICENSE +0 -0
- {locust-2.20.1.dev28.dist-info → locust-2.20.2.dist-info}/WHEEL +0 -0
- {locust-2.20.1.dev28.dist-info → locust-2.20.2.dist-info}/entry_points.txt +0 -0
- {locust-2.20.1.dev28.dist-info → locust-2.20.2.dist-info}/top_level.txt +0 -0
locust/user/inspectuser.py
CHANGED
@@ -1,7 +1,8 @@
|
|
1
|
-
from
|
1
|
+
from __future__ import annotations
|
2
|
+
|
2
3
|
import inspect
|
4
|
+
from collections import defaultdict
|
3
5
|
from json import dumps
|
4
|
-
from typing import List, Type, Dict
|
5
6
|
|
6
7
|
from .task import TaskSet
|
7
8
|
from .users import User
|
@@ -49,11 +50,11 @@ def _print_task_ratio(x, level=0):
|
|
49
50
|
_print_task_ratio(v["tasks"], level + 1)
|
50
51
|
|
51
52
|
|
52
|
-
def get_ratio(user_classes:
|
53
|
+
def get_ratio(user_classes: list[type[User]], user_spawned: dict[str, int], total: bool) -> dict[str, dict[str, float]]:
|
53
54
|
user_count = sum(user_spawned.values()) or 1
|
54
|
-
ratio_percent:
|
55
|
+
ratio_percent: dict[type[User], float] = {u: user_spawned.get(u.__name__, 0) / user_count for u in user_classes}
|
55
56
|
|
56
|
-
task_dict:
|
57
|
+
task_dict: dict[str, dict[str, float]] = {}
|
57
58
|
for u, r in ratio_percent.items():
|
58
59
|
d = {"ratio": r}
|
59
60
|
d["tasks"] = _get_task_ratio(u.tasks, total, r)
|
locust/user/task.py
CHANGED
@@ -1,4 +1,7 @@
|
|
1
1
|
from __future__ import annotations
|
2
|
+
|
3
|
+
from locust.exception import InterruptTaskSet, MissingWaitTimeError, RescheduleTask, RescheduleTaskImmediately, StopUser
|
4
|
+
|
2
5
|
import logging
|
3
6
|
import random
|
4
7
|
import traceback
|
@@ -6,22 +9,17 @@ from time import time
|
|
6
9
|
from typing import (
|
7
10
|
TYPE_CHECKING,
|
8
11
|
Callable,
|
9
|
-
List,
|
10
|
-
TypeVar,
|
11
|
-
Optional,
|
12
|
-
Type,
|
13
|
-
overload,
|
14
|
-
Dict,
|
15
|
-
Set,
|
16
12
|
Protocol,
|
13
|
+
Type,
|
14
|
+
TypeVar,
|
17
15
|
final,
|
16
|
+
overload,
|
18
17
|
runtime_checkable,
|
19
18
|
)
|
19
|
+
|
20
20
|
import gevent
|
21
21
|
from gevent import GreenletExit
|
22
22
|
|
23
|
-
from locust.exception import InterruptTaskSet, RescheduleTask, RescheduleTaskImmediately, StopUser, MissingWaitTimeError
|
24
|
-
|
25
23
|
if TYPE_CHECKING:
|
26
24
|
from locust import User
|
27
25
|
|
@@ -34,7 +32,7 @@ LOCUST_STATE_RUNNING, LOCUST_STATE_WAITING, LOCUST_STATE_STOPPING = ["running",
|
|
34
32
|
|
35
33
|
@runtime_checkable
|
36
34
|
class TaskHolder(Protocol[TaskT]):
|
37
|
-
tasks:
|
35
|
+
tasks: list[TaskT]
|
38
36
|
|
39
37
|
|
40
38
|
@overload
|
@@ -170,10 +168,10 @@ def get_tasks_from_base_classes(bases, class_dict):
|
|
170
168
|
|
171
169
|
|
172
170
|
def filter_tasks_by_tags(
|
173
|
-
task_holder:
|
174
|
-
tags:
|
175
|
-
exclude_tags:
|
176
|
-
checked:
|
171
|
+
task_holder: type[TaskHolder],
|
172
|
+
tags: set[str] | None = None,
|
173
|
+
exclude_tags: set[str] | None = None,
|
174
|
+
checked: dict[TaskT, bool] | None = None,
|
177
175
|
):
|
178
176
|
"""
|
179
177
|
Function used by Environment to recursively remove any tasks/TaskSets from a TaskSet/User that
|
@@ -238,7 +236,7 @@ class TaskSet(metaclass=TaskSetMeta):
|
|
238
236
|
will then continue in the first TaskSet).
|
239
237
|
"""
|
240
238
|
|
241
|
-
tasks:
|
239
|
+
tasks: list[TaskSet | Callable] = []
|
242
240
|
"""
|
243
241
|
Collection of python callables and/or TaskSet classes that the User(s) will run.
|
244
242
|
|
@@ -253,7 +251,7 @@ class TaskSet(metaclass=TaskSetMeta):
|
|
253
251
|
tasks = {ThreadPage:15, write_post:1}
|
254
252
|
"""
|
255
253
|
|
256
|
-
min_wait:
|
254
|
+
min_wait: float | None = None
|
257
255
|
"""
|
258
256
|
Deprecated: Use wait_time instead.
|
259
257
|
Minimum waiting time between the execution of user tasks. Can be used to override
|
@@ -261,7 +259,7 @@ class TaskSet(metaclass=TaskSetMeta):
|
|
261
259
|
TaskSet.
|
262
260
|
"""
|
263
261
|
|
264
|
-
max_wait:
|
262
|
+
max_wait: float | None = None
|
265
263
|
"""
|
266
264
|
Deprecated: Use wait_time instead.
|
267
265
|
Maximum waiting time between the execution of user tasks. Can be used to override
|
@@ -277,11 +275,11 @@ class TaskSet(metaclass=TaskSetMeta):
|
|
277
275
|
if not set on the TaskSet.
|
278
276
|
"""
|
279
277
|
|
280
|
-
_user:
|
281
|
-
_parent:
|
278
|
+
_user: User
|
279
|
+
_parent: User
|
282
280
|
|
283
|
-
def __init__(self, parent:
|
284
|
-
self._task_queue:
|
281
|
+
def __init__(self, parent: User) -> None:
|
282
|
+
self._task_queue: list[Callable] = []
|
285
283
|
self._time_start = time()
|
286
284
|
|
287
285
|
if isinstance(parent, TaskSet):
|
@@ -298,9 +296,10 @@ class TaskSet(metaclass=TaskSetMeta):
|
|
298
296
|
self.max_wait = self.user.max_wait
|
299
297
|
if not self.wait_function:
|
300
298
|
self.wait_function = self.user.wait_function
|
299
|
+
self._cp_last_run = time() # used by constant_pacing wait_time
|
301
300
|
|
302
301
|
@property
|
303
|
-
def user(self) ->
|
302
|
+
def user(self) -> User:
|
304
303
|
""":py:class:`User <locust.User>` instance that this TaskSet was created by"""
|
305
304
|
return self._user
|
306
305
|
|
@@ -426,11 +425,7 @@ class TaskSet(metaclass=TaskSetMeta):
|
|
426
425
|
return random.randint(self.min_wait, self.max_wait) / 1000.0
|
427
426
|
else:
|
428
427
|
raise MissingWaitTimeError(
|
429
|
-
"You must define a wait_time method on either the
|
430
|
-
% (
|
431
|
-
type(self.user).__name__,
|
432
|
-
type(self).__name__,
|
433
|
-
)
|
428
|
+
"You must define a wait_time method on either the {type(self.user).__name__} or {type(self).__name__} class"
|
434
429
|
)
|
435
430
|
|
436
431
|
def wait(self):
|
locust/user/users.py
CHANGED
@@ -1,12 +1,13 @@
|
|
1
1
|
from __future__ import annotations
|
2
|
-
from typing import Callable, Dict, List, Optional, final
|
3
2
|
|
3
|
+
import logging
|
4
4
|
import time
|
5
|
+
import traceback
|
6
|
+
from typing import Callable, final
|
7
|
+
|
5
8
|
from gevent import GreenletExit, greenlet
|
6
9
|
from gevent.pool import Group
|
7
10
|
from urllib3 import PoolManager
|
8
|
-
import logging
|
9
|
-
import traceback
|
10
11
|
|
11
12
|
logger = logging.getLogger(__name__)
|
12
13
|
from locust.clients import HttpSession
|
@@ -59,7 +60,7 @@ class User(metaclass=UserMeta):
|
|
59
60
|
:py:class:`HttpUser <locust.HttpUser>` class.
|
60
61
|
"""
|
61
62
|
|
62
|
-
host:
|
63
|
+
host: str | None = None
|
63
64
|
"""Base hostname to swarm. i.e: http://127.0.0.1:1234"""
|
64
65
|
|
65
66
|
min_wait = None
|
@@ -89,7 +90,7 @@ class User(metaclass=UserMeta):
|
|
89
90
|
Method that returns the time between the execution of locust tasks in milliseconds
|
90
91
|
"""
|
91
92
|
|
92
|
-
tasks:
|
93
|
+
tasks: list[TaskSet | Callable] = []
|
93
94
|
"""
|
94
95
|
Collection of python callables and/or TaskSet classes that the Locust user(s) will run.
|
95
96
|
|
@@ -216,13 +217,22 @@ class User(metaclass=UserMeta):
|
|
216
217
|
def greenlet(self):
|
217
218
|
return self._greenlet
|
218
219
|
|
219
|
-
def context(self) ->
|
220
|
+
def context(self) -> dict:
|
220
221
|
"""
|
221
222
|
Adds the returned value (a dict) to the context for :ref:`request event <request_context>`.
|
222
223
|
Override this in your User class to customize the context.
|
223
224
|
"""
|
224
225
|
return {}
|
225
226
|
|
227
|
+
@classmethod
|
228
|
+
def json(cls):
|
229
|
+
return {
|
230
|
+
"host": cls.host,
|
231
|
+
"weight": cls.weight,
|
232
|
+
"fixed_count": cls.fixed_count,
|
233
|
+
"tasks": [task.__name__ for task in cls.tasks],
|
234
|
+
}
|
235
|
+
|
226
236
|
@classmethod
|
227
237
|
def fullname(cls) -> str:
|
228
238
|
"""Fully qualified name of the user class, e.g. my_package.my_module.MyUserClass"""
|
@@ -244,7 +254,7 @@ class HttpUser(User):
|
|
244
254
|
abstract = True
|
245
255
|
"""If abstract is True, the class is meant to be subclassed, and users will not choose this locust during a test"""
|
246
256
|
|
247
|
-
pool_manager:
|
257
|
+
pool_manager: PoolManager | None = None
|
248
258
|
"""Connection pool manager to use. If not given, a new manager is created per single user."""
|
249
259
|
|
250
260
|
def __init__(self, *args, **kwargs):
|
locust/util/deprecation.py
CHANGED
locust/util/exception_handler.py
CHANGED
locust/util/load_locustfile.py
CHANGED
@@ -1,8 +1,10 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
1
3
|
import importlib
|
2
4
|
import inspect
|
3
5
|
import os
|
4
6
|
import sys
|
5
|
-
|
7
|
+
|
6
8
|
from ..shape import LoadTestShape
|
7
9
|
from ..user import User
|
8
10
|
|
@@ -21,7 +23,7 @@ def is_shape_class(item):
|
|
21
23
|
return bool(inspect.isclass(item) and issubclass(item, LoadTestShape) and not getattr(item, "abstract", True))
|
22
24
|
|
23
25
|
|
24
|
-
def load_locustfile(path) ->
|
26
|
+
def load_locustfile(path) -> tuple[str | None, dict[str, User], list[LoadTestShape]]:
|
25
27
|
"""
|
26
28
|
Import given locustfile path and return (docstring, callables).
|
27
29
|
|
locust/web.py
CHANGED
@@ -1,32 +1,44 @@
|
|
1
1
|
from __future__ import annotations
|
2
|
+
|
2
3
|
import csv
|
4
|
+
import json
|
3
5
|
import logging
|
4
6
|
import os.path
|
5
7
|
from functools import wraps
|
6
|
-
|
7
8
|
from html import escape
|
8
9
|
from io import StringIO
|
9
|
-
from json import dumps
|
10
10
|
from itertools import chain
|
11
|
+
from json import dumps
|
11
12
|
from time import time
|
12
|
-
from typing import TYPE_CHECKING,
|
13
|
+
from typing import TYPE_CHECKING, Any
|
13
14
|
|
14
15
|
import gevent
|
15
|
-
from flask import
|
16
|
-
|
16
|
+
from flask import (
|
17
|
+
Flask,
|
18
|
+
Response,
|
19
|
+
jsonify,
|
20
|
+
make_response,
|
21
|
+
redirect,
|
22
|
+
render_template,
|
23
|
+
request,
|
24
|
+
send_file,
|
25
|
+
send_from_directory,
|
26
|
+
url_for,
|
27
|
+
)
|
28
|
+
from flask_cors import CORS
|
29
|
+
from flask_login import LoginManager, login_required
|
17
30
|
from gevent import pywsgi
|
18
31
|
|
19
|
-
from .
|
20
|
-
from .
|
32
|
+
from . import __version__ as version
|
33
|
+
from . import argument_parser
|
34
|
+
from . import stats as stats_module
|
35
|
+
from .html import get_html_report
|
21
36
|
from .log import greenlet_exception_logger
|
22
|
-
from .
|
23
|
-
from . import
|
24
|
-
from .stats import StatsCSV
|
37
|
+
from .runners import STATE_MISSING, STATE_RUNNING, MasterRunner
|
38
|
+
from .stats import StatsCSV, StatsCSVFileWriter, StatsErrorDict, sort_stats
|
25
39
|
from .user.inspectuser import get_ratio
|
26
40
|
from .util.cache import memoize
|
27
41
|
from .util.timespan import parse_timespan
|
28
|
-
from .html import get_html_report
|
29
|
-
from flask_cors import CORS
|
30
42
|
|
31
43
|
if TYPE_CHECKING:
|
32
44
|
from .env import Environment
|
@@ -45,7 +57,7 @@ class WebUI:
|
|
45
57
|
in :attr:`environment.stats <locust.env.Environment.stats>`
|
46
58
|
"""
|
47
59
|
|
48
|
-
app:
|
60
|
+
app: Flask | None = None
|
49
61
|
"""
|
50
62
|
Reference to the :class:`flask.Flask` app. Can be used to add additional web routes and customize
|
51
63
|
the Flask app in other various ways. Example::
|
@@ -57,27 +69,30 @@ class WebUI:
|
|
57
69
|
return "your IP is: %s" % request.remote_addr
|
58
70
|
"""
|
59
71
|
|
60
|
-
greenlet:
|
72
|
+
greenlet: gevent.Greenlet | None = None
|
61
73
|
"""
|
62
74
|
Greenlet of the running web server
|
63
75
|
"""
|
64
76
|
|
65
|
-
server:
|
77
|
+
server: pywsgi.WSGIServer | None = None
|
66
78
|
"""Reference to the :class:`pyqsgi.WSGIServer` instance"""
|
67
79
|
|
68
|
-
template_args:
|
80
|
+
template_args: dict[str, Any]
|
69
81
|
"""Arguments used to render index.html for the web UI. Must be used with custom templates
|
70
82
|
extending index.html."""
|
71
83
|
|
84
|
+
auth_args: dict[str, Any]
|
85
|
+
"""Arguments used to render auth.html for the web UI auth page. Must be used when configuring auth"""
|
86
|
+
|
72
87
|
def __init__(
|
73
88
|
self,
|
74
|
-
environment:
|
89
|
+
environment: Environment,
|
75
90
|
host: str,
|
76
91
|
port: int,
|
77
|
-
|
78
|
-
tls_cert:
|
79
|
-
tls_key:
|
80
|
-
stats_csv_writer:
|
92
|
+
web_login: bool = False,
|
93
|
+
tls_cert: str | None = None,
|
94
|
+
tls_key: str | None = None,
|
95
|
+
stats_csv_writer: StatsCSV | None = None,
|
81
96
|
delayed_start=False,
|
82
97
|
userclass_picker_is_active=False,
|
83
98
|
modern_ui=False,
|
@@ -89,8 +104,7 @@ class WebUI:
|
|
89
104
|
environment: Reference to the current Locust Environment
|
90
105
|
host: Host/interface that the web server should accept connections to
|
91
106
|
port: Port that the web server should listen to
|
92
|
-
|
93
|
-
Should be supplied in the format: "user:pass".
|
107
|
+
web_login: Enables a login page for the modern UI
|
94
108
|
tls_cert: A path to a TLS certificate
|
95
109
|
tls_key: A path to a TLS private key
|
96
110
|
delayed_start: Whether or not to delay starting web UI until `start()` is called. Delaying web UI start
|
@@ -105,6 +119,7 @@ class WebUI:
|
|
105
119
|
self.tls_key = tls_key
|
106
120
|
self.userclass_picker_is_active = userclass_picker_is_active
|
107
121
|
self.modern_ui = modern_ui
|
122
|
+
self.web_login = web_login
|
108
123
|
app = Flask(__name__)
|
109
124
|
CORS(app)
|
110
125
|
self.app = app
|
@@ -113,24 +128,16 @@ class WebUI:
|
|
113
128
|
root_path = os.path.dirname(os.path.abspath(__file__))
|
114
129
|
app.root_path = root_path
|
115
130
|
self.webui_build_path = os.path.join(root_path, "webui", "dist")
|
116
|
-
self.
|
117
|
-
self.
|
118
|
-
self.greenlet: Optional[gevent.Greenlet] = None
|
119
|
-
self._swarm_greenlet: Optional[gevent.Greenlet] = None
|
131
|
+
self.greenlet: gevent.Greenlet | None = None
|
132
|
+
self._swarm_greenlet: gevent.Greenlet | None = None
|
120
133
|
self.template_args = {}
|
134
|
+
self.auth_args = {}
|
135
|
+
|
136
|
+
if self.web_login:
|
137
|
+
self.login_manager = LoginManager()
|
138
|
+
self.login_manager.init_app(app)
|
139
|
+
self.login_manager.login_view = "login"
|
121
140
|
|
122
|
-
if auth_credentials is not None:
|
123
|
-
credentials = auth_credentials.split(":")
|
124
|
-
if len(credentials) == 2:
|
125
|
-
self.app.config["BASIC_AUTH_USERNAME"] = credentials[0]
|
126
|
-
self.app.config["BASIC_AUTH_PASSWORD"] = credentials[1]
|
127
|
-
self.app.config["BASIC_AUTH_ENABLED"] = True
|
128
|
-
self.auth = BasicAuth()
|
129
|
-
self.auth.init_app(self.app)
|
130
|
-
else:
|
131
|
-
raise AuthCredentialsError(
|
132
|
-
"Invalid auth_credentials. It should be a string in the following format: 'user:pass'"
|
133
|
-
)
|
134
141
|
if environment.runner:
|
135
142
|
self.update_template_args()
|
136
143
|
if not delayed_start:
|
@@ -365,8 +372,8 @@ class WebUI:
|
|
365
372
|
@self.auth_required_if_enabled
|
366
373
|
@memoize(timeout=DEFAULT_CACHE_TIME, dynamic_timeout=True)
|
367
374
|
def request_stats() -> Response:
|
368
|
-
stats:
|
369
|
-
errors:
|
375
|
+
stats: list[dict[str, Any]] = []
|
376
|
+
errors: list[StatsErrorDict] = []
|
370
377
|
|
371
378
|
if environment.runner is None:
|
372
379
|
report = {
|
@@ -476,9 +483,9 @@ class WebUI:
|
|
476
483
|
|
477
484
|
@app.route("/tasks")
|
478
485
|
@self.auth_required_if_enabled
|
479
|
-
def tasks() ->
|
486
|
+
def tasks() -> dict[str, dict[str, dict[str, float]]]:
|
480
487
|
runner = self.environment.runner
|
481
|
-
user_spawned:
|
488
|
+
user_spawned: dict[str, int]
|
482
489
|
if runner is None:
|
483
490
|
user_spawned = {}
|
484
491
|
else:
|
@@ -508,6 +515,30 @@ class WebUI:
|
|
508
515
|
|
509
516
|
return jsonify({"logs": logs})
|
510
517
|
|
518
|
+
@app.route("/login")
|
519
|
+
def login():
|
520
|
+
if not self.web_login:
|
521
|
+
return redirect(url_for("index"))
|
522
|
+
|
523
|
+
if self.modern_ui:
|
524
|
+
self.set_static_modern_ui()
|
525
|
+
|
526
|
+
return render_template(
|
527
|
+
"auth.html",
|
528
|
+
auth_args=self.auth_args,
|
529
|
+
)
|
530
|
+
else:
|
531
|
+
return "Web Auth is only available on the modern web ui. Enable it with the --modern-ui flag"
|
532
|
+
|
533
|
+
@app.route("/user", methods=["POST"])
|
534
|
+
def update_user():
|
535
|
+
assert request.method == "POST"
|
536
|
+
|
537
|
+
user_settings = json.loads(request.data)
|
538
|
+
self.environment.update_user_class(user_settings)
|
539
|
+
|
540
|
+
return {}, 201
|
541
|
+
|
511
542
|
def start(self):
|
512
543
|
self.greenlet = gevent.spawn(self.start_server)
|
513
544
|
self.greenlet.link_exception(greenlet_exception_handler)
|
@@ -529,8 +560,8 @@ class WebUI:
|
|
529
560
|
|
530
561
|
def auth_required_if_enabled(self, view_func):
|
531
562
|
"""
|
532
|
-
Decorator that can be used on custom route methods that will turn on
|
533
|
-
authentication if the ``--web-
|
563
|
+
Decorator that can be used on custom route methods that will turn on Flask Login
|
564
|
+
authentication if the ``--web-login`` flag is used. Example::
|
534
565
|
|
535
566
|
@web_ui.app.route("/my_custom_route")
|
536
567
|
@web_ui.auth_required_if_enabled
|
@@ -540,11 +571,11 @@ class WebUI:
|
|
540
571
|
|
541
572
|
@wraps(view_func)
|
542
573
|
def wrapper(*args, **kwargs):
|
543
|
-
if self.
|
544
|
-
|
545
|
-
return view_func(*args, **kwargs)
|
546
|
-
|
547
|
-
return
|
574
|
+
if self.web_login:
|
575
|
+
try:
|
576
|
+
return login_required(view_func)(*args, **kwargs)
|
577
|
+
except Exception as e:
|
578
|
+
return f"Locust auth exception: {e} See https://docs.locust.io/en/stable/extending-locust.html#authentication for configuring authentication."
|
548
579
|
else:
|
549
580
|
return view_func(*args, **kwargs)
|
550
581
|
|
@@ -582,17 +613,32 @@ class WebUI:
|
|
582
613
|
stats = self.environment.runner.stats
|
583
614
|
extra_options = argument_parser.ui_extra_args_dict()
|
584
615
|
|
585
|
-
available_user_classes =
|
586
|
-
|
587
|
-
|
616
|
+
available_user_classes = None
|
617
|
+
users = None
|
618
|
+
if self.environment.available_user_classes:
|
619
|
+
available_user_classes = sorted(self.environment.available_user_classes)
|
620
|
+
users = {
|
621
|
+
user_class_name: user_class.json()
|
622
|
+
for (user_class_name, user_class) in self.environment.available_user_classes.items()
|
623
|
+
}
|
588
624
|
|
589
625
|
available_shape_classes = ["Default"]
|
590
626
|
if self.environment.available_shape_classes:
|
591
627
|
available_shape_classes += sorted(self.environment.available_shape_classes.keys())
|
592
628
|
|
629
|
+
available_user_tasks = (
|
630
|
+
{
|
631
|
+
user_class_name: [task.__name__ for task in user_class]
|
632
|
+
for (user_class_name, user_class) in self.environment.available_user_tasks.items()
|
633
|
+
}
|
634
|
+
if self.environment.available_user_tasks
|
635
|
+
else None
|
636
|
+
)
|
637
|
+
|
593
638
|
if self.modern_ui:
|
594
639
|
percentiles = {
|
595
640
|
"percentiles_to_chart": stats_module.MODERN_UI_PERCENTILES_TO_CHART,
|
641
|
+
"percentiles_to_statistics": stats_module.PERCENTILES_TO_STATISTICS,
|
596
642
|
}
|
597
643
|
else:
|
598
644
|
percentiles = {
|
@@ -623,12 +669,15 @@ class WebUI:
|
|
623
669
|
"show_userclass_picker": self.userclass_picker_is_active,
|
624
670
|
"available_user_classes": available_user_classes,
|
625
671
|
"available_shape_classes": available_shape_classes,
|
672
|
+
"available_user_tasks": available_user_tasks,
|
673
|
+
"users": users,
|
626
674
|
**percentiles,
|
627
675
|
}
|
628
676
|
|
629
677
|
def _update_shape_class(self, shape_class_name):
|
630
678
|
if shape_class_name:
|
631
679
|
shape_class = self.environment.available_shape_classes[shape_class_name]
|
680
|
+
shape_class.runner = self.environment.runner
|
632
681
|
else:
|
633
682
|
shape_class = None
|
634
683
|
|
@@ -0,0 +1 @@
|
|
1
|
+
{"version":3,"file":"auth-5e21717c.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}
|