locust 2.35.1.dev5__tar.gz → 2.35.1.dev20__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.
Files changed (59) hide show
  1. {locust-2.35.1.dev5 → locust-2.35.1.dev20}/PKG-INFO +1 -1
  2. {locust-2.35.1.dev5 → locust-2.35.1.dev20}/locust/_version.py +2 -2
  3. {locust-2.35.1.dev5 → locust-2.35.1.dev20}/locust/html.py +12 -13
  4. {locust-2.35.1.dev5 → locust-2.35.1.dev20}/locust/main.py +73 -81
  5. {locust-2.35.1.dev5 → locust-2.35.1.dev20}/locust/stats.py +43 -4
  6. {locust-2.35.1.dev5 → locust-2.35.1.dev20}/locust/util/load_locustfile.py +4 -4
  7. {locust-2.35.1.dev5 → locust-2.35.1.dev20}/locust/web.py +23 -21
  8. {locust-2.35.1.dev5 → locust-2.35.1.dev20}/.gitignore +0 -0
  9. {locust-2.35.1.dev5 → locust-2.35.1.dev20}/LICENSE +0 -0
  10. {locust-2.35.1.dev5 → locust-2.35.1.dev20}/README.md +0 -0
  11. {locust-2.35.1.dev5 → locust-2.35.1.dev20}/hatch_build.py +0 -0
  12. {locust-2.35.1.dev5 → locust-2.35.1.dev20}/locust/__init__.py +0 -0
  13. {locust-2.35.1.dev5 → locust-2.35.1.dev20}/locust/__main__.py +0 -0
  14. {locust-2.35.1.dev5 → locust-2.35.1.dev20}/locust/argument_parser.py +0 -0
  15. {locust-2.35.1.dev5 → locust-2.35.1.dev20}/locust/clients.py +0 -0
  16. {locust-2.35.1.dev5 → locust-2.35.1.dev20}/locust/contrib/__init__.py +0 -0
  17. {locust-2.35.1.dev5 → locust-2.35.1.dev20}/locust/contrib/fasthttp.py +0 -0
  18. {locust-2.35.1.dev5 → locust-2.35.1.dev20}/locust/contrib/mongodb.py +0 -0
  19. {locust-2.35.1.dev5 → locust-2.35.1.dev20}/locust/contrib/oai.py +0 -0
  20. {locust-2.35.1.dev5 → locust-2.35.1.dev20}/locust/contrib/postgres.py +0 -0
  21. {locust-2.35.1.dev5 → locust-2.35.1.dev20}/locust/debug.py +0 -0
  22. {locust-2.35.1.dev5 → locust-2.35.1.dev20}/locust/dispatch.py +0 -0
  23. {locust-2.35.1.dev5 → locust-2.35.1.dev20}/locust/env.py +0 -0
  24. {locust-2.35.1.dev5 → locust-2.35.1.dev20}/locust/event.py +0 -0
  25. {locust-2.35.1.dev5 → locust-2.35.1.dev20}/locust/exception.py +0 -0
  26. {locust-2.35.1.dev5 → locust-2.35.1.dev20}/locust/input_events.py +0 -0
  27. {locust-2.35.1.dev5 → locust-2.35.1.dev20}/locust/log.py +0 -0
  28. {locust-2.35.1.dev5 → locust-2.35.1.dev20}/locust/py.typed +0 -0
  29. {locust-2.35.1.dev5 → locust-2.35.1.dev20}/locust/rpc/__init__.py +0 -0
  30. {locust-2.35.1.dev5 → locust-2.35.1.dev20}/locust/rpc/protocol.py +0 -0
  31. {locust-2.35.1.dev5 → locust-2.35.1.dev20}/locust/rpc/zmqrpc.py +0 -0
  32. {locust-2.35.1.dev5 → locust-2.35.1.dev20}/locust/runners.py +0 -0
  33. {locust-2.35.1.dev5 → locust-2.35.1.dev20}/locust/shape.py +0 -0
  34. {locust-2.35.1.dev5 → locust-2.35.1.dev20}/locust/user/__init__.py +0 -0
  35. {locust-2.35.1.dev5 → locust-2.35.1.dev20}/locust/user/inspectuser.py +0 -0
  36. {locust-2.35.1.dev5 → locust-2.35.1.dev20}/locust/user/sequential_taskset.py +0 -0
  37. {locust-2.35.1.dev5 → locust-2.35.1.dev20}/locust/user/task.py +0 -0
  38. {locust-2.35.1.dev5 → locust-2.35.1.dev20}/locust/user/users.py +0 -0
  39. {locust-2.35.1.dev5 → locust-2.35.1.dev20}/locust/user/wait_time.py +0 -0
  40. {locust-2.35.1.dev5 → locust-2.35.1.dev20}/locust/util/__init__.py +0 -0
  41. {locust-2.35.1.dev5 → locust-2.35.1.dev20}/locust/util/cache.py +0 -0
  42. {locust-2.35.1.dev5 → locust-2.35.1.dev20}/locust/util/date.py +0 -0
  43. {locust-2.35.1.dev5 → locust-2.35.1.dev20}/locust/util/deprecation.py +0 -0
  44. {locust-2.35.1.dev5 → locust-2.35.1.dev20}/locust/util/directory.py +0 -0
  45. {locust-2.35.1.dev5 → locust-2.35.1.dev20}/locust/util/exception_handler.py +0 -0
  46. {locust-2.35.1.dev5 → locust-2.35.1.dev20}/locust/util/rounding.py +0 -0
  47. {locust-2.35.1.dev5 → locust-2.35.1.dev20}/locust/util/timespan.py +0 -0
  48. {locust-2.35.1.dev5 → locust-2.35.1.dev20}/locust/util/url.py +0 -0
  49. {locust-2.35.1.dev5 → locust-2.35.1.dev20}/locust/webui/dist/assets/favicon-dark.png +0 -0
  50. {locust-2.35.1.dev5 → locust-2.35.1.dev20}/locust/webui/dist/assets/favicon-light.png +0 -0
  51. {locust-2.35.1.dev5 → locust-2.35.1.dev20}/locust/webui/dist/assets/graphs-dark.png +0 -0
  52. {locust-2.35.1.dev5 → locust-2.35.1.dev20}/locust/webui/dist/assets/graphs-light.png +0 -0
  53. {locust-2.35.1.dev5 → locust-2.35.1.dev20}/locust/webui/dist/assets/index-DQLt1q6M.js +0 -0
  54. {locust-2.35.1.dev5 → locust-2.35.1.dev20}/locust/webui/dist/assets/testruns-dark.png +0 -0
  55. {locust-2.35.1.dev5 → locust-2.35.1.dev20}/locust/webui/dist/assets/testruns-light.png +0 -0
  56. {locust-2.35.1.dev5 → locust-2.35.1.dev20}/locust/webui/dist/auth.html +0 -0
  57. {locust-2.35.1.dev5 → locust-2.35.1.dev20}/locust/webui/dist/index.html +0 -0
  58. {locust-2.35.1.dev5 → locust-2.35.1.dev20}/locust/webui/dist/report.html +0 -0
  59. {locust-2.35.1.dev5 → locust-2.35.1.dev20}/pyproject.toml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: locust
3
- Version: 2.35.1.dev5
3
+ Version: 2.35.1.dev20
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
@@ -17,5 +17,5 @@ __version__: str
17
17
  __version_tuple__: VERSION_TUPLE
18
18
  version_tuple: VERSION_TUPLE
19
19
 
20
- __version__ = version = '2.35.1.dev5'
21
- __version_tuple__ = version_tuple = (2, 35, 1, 'dev5')
20
+ __version__ = version = '2.35.1.dev20'
21
+ __version_tuple__ = version_tuple = (2, 35, 1, 'dev20')
@@ -5,9 +5,8 @@ from itertools import chain
5
5
  from jinja2 import Environment as JinjaEnvironment
6
6
  from jinja2 import FileSystemLoader
7
7
 
8
- from . import stats as stats_module
8
+ from . import stats
9
9
  from .runners import STATE_STOPPED, STATE_STOPPING, MasterRunner
10
- from .stats import sort_stats, update_stats_history
11
10
  from .user.inspectuser import get_ratio
12
11
  from .util.date import format_duration, format_utc_timestamp
13
12
 
@@ -37,14 +36,14 @@ def get_html_report(
37
36
  show_download_link=True,
38
37
  theme="",
39
38
  ):
40
- stats = environment.runner.stats
39
+ request_stats = environment.runner.stats
41
40
 
42
- start_time = format_utc_timestamp(stats.start_time)
41
+ start_time = format_utc_timestamp(request_stats.start_time)
43
42
 
44
- if end_ts := stats.last_request_timestamp:
43
+ if end_ts := request_stats.last_request_timestamp:
45
44
  end_time = format_utc_timestamp(end_ts)
46
45
  else:
47
- end_ts = stats.start_time
46
+ end_ts = request_stats.start_time
48
47
  end_time = start_time
49
48
 
50
49
  host = None
@@ -55,15 +54,15 @@ def get_html_report(
55
54
  if len(all_hosts) == 1:
56
55
  host = list(all_hosts)[0]
57
56
 
58
- requests_statistics = list(chain(sort_stats(stats.entries), [stats.total]))
59
- failures_statistics = sort_stats(stats.errors)
57
+ requests_statistics = list(chain(stats.sort_stats(request_stats.entries), [request_stats.total]))
58
+ failures_statistics = stats.sort_stats(request_stats.errors)
60
59
  exceptions_statistics = [
61
60
  {**exc, "nodes": ", ".join(exc["nodes"])} for exc in environment.runner.exceptions.values()
62
61
  ]
63
62
 
64
- if stats.history and stats.history[-1]["time"] < end_time:
65
- update_stats_history(environment.runner, end_time)
66
- history = stats.history
63
+ if request_stats.history and request_stats.history[-1]["time"] < end_time:
64
+ stats.update_stats_history(environment.runner, end_time)
65
+ history = request_stats.history
67
66
 
68
67
  is_distributed = isinstance(environment.runner, MasterRunner)
69
68
  user_spawned = (
@@ -98,13 +97,13 @@ def get_html_report(
98
97
  ],
99
98
  "start_time": start_time,
100
99
  "end_time": end_time,
101
- "duration": format_duration(stats.start_time, end_ts),
100
+ "duration": format_duration(request_stats.start_time, end_ts),
102
101
  "host": escape(str(host)),
103
102
  "history": history,
104
103
  "show_download_link": show_download_link,
105
104
  "locustfile": escape(str(environment.locustfile)),
106
105
  "tasks": task_data,
107
- "percentiles_to_chart": stats_module.PERCENTILES_TO_CHART,
106
+ "percentiles_to_chart": stats.PERCENTILES_TO_CHART,
108
107
  "profile": escape(str(environment.profile)) if environment.profile else None,
109
108
  },
110
109
  theme=theme,
@@ -15,6 +15,7 @@ import sys
15
15
  import time
16
16
  import traceback
17
17
  import webbrowser
18
+ from typing import TYPE_CHECKING
18
19
 
19
20
  import gevent
20
21
 
@@ -24,16 +25,6 @@ from .env import Environment
24
25
  from .html import get_html_report, process_html_filename
25
26
  from .input_events import input_listener
26
27
  from .log import greenlet_exception_logger, setup_logging
27
- from .stats import (
28
- StatsCSV,
29
- StatsCSVFileWriter,
30
- print_error_report,
31
- print_percentile_stats,
32
- print_stats,
33
- print_stats_json,
34
- stats_history,
35
- stats_printer,
36
- )
37
28
  from .user.inspectuser import print_task_ratio, print_task_ratio_json
38
29
  from .util.load_locustfile import load_locustfile
39
30
 
@@ -52,6 +43,9 @@ except ModuleNotFoundError as e:
52
43
  if e.msg != "No module named 'locust_cloud'":
53
44
  raise
54
45
 
46
+ if TYPE_CHECKING:
47
+ from collections.abc import Callable
48
+
55
49
  version = locust.__version__
56
50
 
57
51
  # Options to ignore when using a custom shape class without `use_common_options=True`
@@ -93,79 +87,75 @@ def create_environment(
93
87
  )
94
88
 
95
89
 
96
- def main():
97
- # find specified locustfile(s) and make sure it exists, using a very simplified
98
- # command line parser that is only used to parse the -f option.
99
- locustfiles = parse_locustfile_option()
100
- locustfiles_length = len(locustfiles)
90
+ def merge_locustfiles_content(
91
+ locustfiles: list[str],
92
+ ) -> tuple[
93
+ dict[str, type[locust.User]],
94
+ dict[str, locust.LoadTestShape],
95
+ dict[str, list[locust.TaskSet | Callable]],
96
+ locust.LoadTestShape | None,
97
+ ]:
98
+ """
99
+ Validate content of each locustfile in locustfiles and merge data to single objects output.
101
100
 
102
- # Grabbing the Locustfile if only one was provided. Otherwise, allowing users to select the locustfile in the UI
103
- # If --headless or --autostart and multiple locustfiles, all provided UserClasses will be ran
104
- locustfile = locustfiles[0] if locustfiles_length == 1 else None
101
+ Can stop locust execution on errors.
102
+ """
103
+ available_user_classes: dict[str, type[locust.User]] = {}
104
+ available_shape_classes: dict[str, locust.LoadTestShape] = {}
105
+ # TODO: list[locust.TaskSet | Callable] should be replaced with correct type,
106
+ # supported by User class task attribute. This require additional rewrite,
107
+ # out of main refactoring.
108
+ # Check docs for real supported task attribute signature for User\TaskSet class.
109
+ available_user_tasks: dict[str, list[locust.TaskSet | Callable]] = {}
105
110
 
106
- # Importing Locustfile(s) - setting available UserClasses and ShapeClasses to choose from in UI
107
- user_classes: dict[str, locust.User] = {}
108
- available_user_classes = {}
109
- available_shape_classes = {}
110
- available_user_tasks = {}
111
- shape_class = None
112
111
  for _locustfile in locustfiles:
113
- docstring, _user_classes, shape_classes = load_locustfile(_locustfile)
112
+ user_classes, shape_classes = load_locustfile(_locustfile)
114
113
 
115
114
  # Setting Available Shape Classes
116
- if shape_classes:
117
- shape_class = shape_classes[0]
118
- for shape_class in shape_classes:
119
- shape_class_name = type(shape_class).__name__
120
- if shape_class_name in available_shape_classes.keys():
121
- sys.stderr.write(f"Duplicate shape classes: {shape_class_name}\n")
122
- sys.exit(1)
115
+ for _shape_class in shape_classes:
116
+ shape_class_name = type(_shape_class).__name__
117
+ if shape_class_name in available_shape_classes.keys():
118
+ sys.stderr.write(f"Duplicate shape classes: {shape_class_name}\n")
119
+ sys.exit(1)
123
120
 
124
- available_shape_classes[shape_class_name] = shape_class
121
+ available_shape_classes[shape_class_name] = _shape_class
125
122
 
126
123
  # Setting Available User Classes
127
- for key, value in _user_classes.items():
128
- if key in available_user_classes.keys():
129
- previous_path = inspect.getfile(user_classes[key])
130
- new_path = inspect.getfile(value)
124
+ for class_name, class_definition in user_classes.items():
125
+ if class_name in available_user_classes.keys():
126
+ previous_path = inspect.getfile(available_user_classes[class_name])
127
+ new_path = inspect.getfile(class_definition)
131
128
  if previous_path == new_path:
132
129
  # The same User class was defined in two locustfiles but one probably imported the other, so we just ignore it
133
130
  continue
134
131
  else:
135
132
  sys.stderr.write(
136
- f"Duplicate user class names: {key} is defined in both {previous_path} and {new_path}\n"
133
+ f"Duplicate user class names: {class_name} is defined in both {previous_path} and {new_path}\n"
137
134
  )
138
135
  sys.exit(1)
139
136
 
140
- user_classes[key] = value
141
- available_user_classes[key] = value
142
- available_user_tasks[key] = value.tasks or {}
137
+ available_user_classes[class_name] = class_definition
138
+ available_user_tasks[class_name] = class_definition.tasks
143
139
 
144
- if len(stats.PERCENTILES_TO_CHART) > 6:
145
- logging.error("stats.PERCENTILES_TO_CHART parameter should be a maximum of 6 parameters \n")
146
- sys.exit(1)
140
+ shape_class = list(available_shape_classes.values())[0] if available_shape_classes else None
147
141
 
148
- def is_valid_percentile(parameter):
149
- try:
150
- if 0 < float(parameter) < 1:
151
- return True
152
- return False
153
- except ValueError:
154
- return False
155
-
156
- for percentile in stats.PERCENTILES_TO_CHART:
157
- if not is_valid_percentile(percentile):
158
- logging.error(
159
- "stats.PERCENTILES_TO_CHART parameter need to be float and value between. 0 < percentile < 1 Eg 0.95\n"
160
- )
161
- sys.exit(1)
142
+ return available_user_classes, available_shape_classes, available_user_tasks, shape_class
162
143
 
163
- for percentile in stats.PERCENTILES_TO_STATISTICS:
164
- if not is_valid_percentile(percentile):
165
- logging.error(
166
- "stats.PERCENTILES_TO_STATISTICS parameter need to be float and value between. 0 < percentile < 1 Eg 0.95\n"
167
- )
168
- sys.exit(1)
144
+
145
+ def main():
146
+ # find specified locustfile(s) and make sure it exists, using a very simplified
147
+ # command line parser that is only used to parse the -f option.
148
+ locustfiles = parse_locustfile_option()
149
+
150
+ # Importing Locustfile(s) - setting available UserClasses and ShapeClasses to choose from in UI
151
+ (
152
+ available_user_classes,
153
+ available_shape_classes,
154
+ available_user_tasks,
155
+ shape_class,
156
+ ) = merge_locustfiles_content(locustfiles)
157
+
158
+ stats.validate_stats_configuration()
169
159
 
170
160
  # parse all command line options
171
161
  options = parse_options()
@@ -290,25 +280,25 @@ def main():
290
280
 
291
281
  if options.list_commands:
292
282
  print("Available Users:")
293
- for name in user_classes:
283
+ for name in available_user_classes:
294
284
  print(" " + name)
295
285
  sys.exit(0)
296
286
 
297
- if not user_classes:
287
+ if not available_user_classes:
298
288
  logger.error("No User class found!")
299
289
  sys.exit(1)
300
290
 
301
291
  # make sure specified User exists
302
292
  if options.user_classes:
303
- if missing := set(options.user_classes) - set(user_classes.keys()):
293
+ if missing := set(options.user_classes) - set(available_user_classes.keys()):
304
294
  logger.error(f"Unknown User(s): {', '.join(missing)}\n")
305
295
  sys.exit(1)
306
296
  else:
307
- names = set(options.user_classes) & set(user_classes.keys())
308
- user_classes = [user_classes[n] for n in names]
297
+ names = set(options.user_classes) & set(available_user_classes.keys())
298
+ user_classes = [available_user_classes[n] for n in names]
309
299
  else:
310
300
  # list() call is needed to consume the dict_view object in Python 3
311
- user_classes = list(user_classes.values())
301
+ user_classes = list(available_user_classes.values())
312
302
 
313
303
  if not shape_class and options.num_users:
314
304
  fixed_count_total = sum([user_class.fixed_count for user_class in user_classes])
@@ -327,7 +317,8 @@ def main():
327
317
  if soft_limit < minimum_open_file_limit:
328
318
  # Increasing the limit to 10000 within a running process should work on at least MacOS.
329
319
  # It does not work on all OS:es, but we should be no worse off for trying.
330
- resource.setrlimit(resource.RLIMIT_NOFILE, [minimum_open_file_limit, hard_limit])
320
+ limits = minimum_open_file_limit, hard_limit
321
+ resource.setrlimit(resource.RLIMIT_NOFILE, limits)
331
322
  except BaseException:
332
323
  logger.warning(
333
324
  f"""System open file limit '{soft_limit} is below minimum setting '{minimum_open_file_limit}'.
@@ -335,9 +326,10 @@ It's not high enough for load testing, and the OS didn't allow locust to increas
335
326
  See https://github.com/locustio/locust/wiki/Installation#increasing-maximum-number-of-open-files-limit for more info."""
336
327
  )
337
328
 
338
- # create locust Environment
339
- locustfile_path = None if not locustfile else os.path.basename(locustfile)
329
+ # At least one locust file exists, or system will exit earlier
330
+ locustfile_path = os.path.basename(locustfiles[0])
340
331
 
332
+ # create locust Environment
341
333
  environment = create_environment(
342
334
  user_classes,
343
335
  options,
@@ -422,11 +414,11 @@ See https://github.com/locustio/locust/wiki/Installation#increasing-maximum-numb
422
414
  base_csv_dir = options.csv_prefix[: -len(base_csv_file)]
423
415
  if not os.path.exists(base_csv_dir) and len(base_csv_dir) != 0:
424
416
  os.makedirs(base_csv_dir)
425
- stats_csv_writer = StatsCSVFileWriter(
417
+ stats_csv_writer = stats.StatsCSVFileWriter(
426
418
  environment, stats.PERCENTILES_TO_REPORT, options.csv_prefix, options.stats_history_enabled
427
419
  )
428
420
  else:
429
- stats_csv_writer = StatsCSV(environment, stats.PERCENTILES_TO_REPORT)
421
+ stats_csv_writer = stats.StatsCSV(environment, stats.PERCENTILES_TO_REPORT)
430
422
 
431
423
  # start Web UI
432
424
  if not options.headless and not options.worker:
@@ -512,10 +504,10 @@ See https://github.com/locustio/locust/wiki/Installation#increasing-maximum-numb
512
504
  stats_printer_greenlet = None
513
505
  if not options.only_summary and (options.print_stats or (options.headless and not options.worker)):
514
506
  # spawn stats printing greenlet
515
- stats_printer_greenlet = gevent.spawn(stats_printer(runner.stats))
507
+ stats_printer_greenlet = gevent.spawn(stats.stats_printer(runner.stats))
516
508
  stats_printer_greenlet.link_exception(greenlet_exception_handler)
517
509
 
518
- gevent.spawn(stats_history, runner)
510
+ gevent.spawn(stats.stats_history, runner)
519
511
 
520
512
  def start_automatic_run():
521
513
  if options.master:
@@ -633,11 +625,11 @@ See https://github.com/locustio/locust/wiki/Installation#increasing-maximum-numb
633
625
  if runner is not None:
634
626
  runner.quit()
635
627
  if options.json:
636
- print_stats_json(runner.stats)
628
+ stats.print_stats_json(runner.stats)
637
629
  elif not isinstance(runner, locust.runners.WorkerRunner):
638
- print_stats(runner.stats, current=False)
639
- print_percentile_stats(runner.stats)
640
- print_error_report(runner.stats)
630
+ stats.print_stats(runner.stats, current=False)
631
+ stats.print_percentile_stats(runner.stats)
632
+ stats.print_error_report(runner.stats)
641
633
  environment.events.quit.fire(exit_code=code)
642
634
  sys.exit(code)
643
635
 
@@ -6,25 +6,28 @@ import json
6
6
  import logging
7
7
  import os
8
8
  import signal
9
+ import sys
9
10
  import time
10
11
  from abc import abstractmethod
11
12
  from collections import OrderedDict, defaultdict, namedtuple
12
- from collections.abc import Callable, Iterable
13
13
  from copy import copy
14
14
  from html import escape
15
15
  from itertools import chain
16
- from types import FrameType
17
- from typing import TYPE_CHECKING, Any, NoReturn, Protocol, TypedDict, TypeVar, cast
16
+ from typing import TYPE_CHECKING, Protocol, TypedDict, TypeVar, cast
18
17
 
19
18
  import gevent
20
19
 
21
- from .event import Events
22
20
  from .exception import CatchResponseError
23
21
  from .util.date import format_utc_timestamp
24
22
  from .util.rounding import proper_round
25
23
 
26
24
  if TYPE_CHECKING:
25
+ from collections.abc import Callable, Iterable
26
+ from types import FrameType
27
+ from typing import Any, NoReturn
28
+
27
29
  from .env import Environment
30
+ from .event import Events
28
31
  from .runners import Runner
29
32
 
30
33
  console_logger = logging.getLogger("locust.stats_logger")
@@ -1183,3 +1186,39 @@ class StatsCSVFileWriter(StatsCSV):
1183
1186
 
1184
1187
  def stats_history_file_name(self) -> str:
1185
1188
  return self.base_filepath + "_stats_history.csv"
1189
+
1190
+
1191
+ def _is_valid_percentile(parameter) -> bool:
1192
+ """Validate single percentile value from .stats constants."""
1193
+ try:
1194
+ if 0 < float(parameter) < 1:
1195
+ return True
1196
+ return False
1197
+ except ValueError:
1198
+ return False
1199
+
1200
+
1201
+ def validate_stats_configuration() -> None:
1202
+ """
1203
+ This function validates .stats file's constants, that may be patched in user
1204
+ environments.
1205
+
1206
+ No return in normal conditional. Stops locust execution on error.
1207
+ """
1208
+ if len(PERCENTILES_TO_CHART) > 6:
1209
+ sys.stderr.write("stats.PERCENTILES_TO_CHART parameter should be a maximum of 6 parameters \n")
1210
+ sys.exit(1)
1211
+
1212
+ for percentile in PERCENTILES_TO_CHART:
1213
+ if not _is_valid_percentile(percentile):
1214
+ sys.stderr.write(
1215
+ "stats.PERCENTILES_TO_CHART parameter need to be float and value between. 0 < percentile < 1 Eg 0.95\n"
1216
+ )
1217
+ sys.exit(1)
1218
+
1219
+ for percentile in PERCENTILES_TO_STATISTICS:
1220
+ if not _is_valid_percentile(percentile):
1221
+ sys.stderr.write(
1222
+ "stats.PERCENTILES_TO_STATISTICS parameter need to be float and value between. 0 < percentile < 1 Eg 0.95\n"
1223
+ )
1224
+ sys.exit(1)
@@ -10,21 +10,21 @@ from ..shape import LoadTestShape
10
10
  from ..user import User
11
11
 
12
12
 
13
- def is_user_class(item):
13
+ def is_user_class(item) -> bool:
14
14
  """
15
15
  Check if a variable is a runnable (non-abstract) User class
16
16
  """
17
17
  return bool(inspect.isclass(item) and issubclass(item, User) and item.abstract is False)
18
18
 
19
19
 
20
- def is_shape_class(item):
20
+ def is_shape_class(item) -> bool:
21
21
  """
22
22
  Check if a class is a LoadTestShape
23
23
  """
24
24
  return bool(inspect.isclass(item) and issubclass(item, LoadTestShape) and not getattr(item, "abstract", True))
25
25
 
26
26
 
27
- def load_locustfile(path) -> tuple[str | None, dict[str, User], list[LoadTestShape]]:
27
+ def load_locustfile(path) -> tuple[dict[str, type[User]], list[LoadTestShape]]:
28
28
  """
29
29
  Import given locustfile path and return (docstring, callables).
30
30
 
@@ -82,4 +82,4 @@ def load_locustfile(path) -> tuple[str | None, dict[str, User], list[LoadTestSha
82
82
  # Find shape class, if any, return it
83
83
  shape_classes = [value() for value in vars(imported).values() if is_shape_class(value)]
84
84
 
85
- return imported.__doc__, user_classes, shape_classes
85
+ return user_classes, shape_classes
@@ -32,12 +32,10 @@ from flask_login import LoginManager, login_required
32
32
  from gevent import pywsgi
33
33
 
34
34
  from . import __version__ as version
35
- from . import argument_parser
36
- from . import stats as stats_module
35
+ from . import argument_parser, stats
37
36
  from .html import DEFAULT_BUILD_PATH, get_html_report, render_template_from
38
37
  from .log import get_logs, greenlet_exception_logger
39
38
  from .runners import STATE_MISSING, STATE_RUNNING, MasterRunner
40
- from .stats import StatsCSV, StatsCSVFileWriter, StatsErrorDict, sort_stats
41
39
  from .user.inspectuser import get_ratio
42
40
  from .util.cache import memoize
43
41
  from .util.date import format_safe_timestamp
@@ -126,7 +124,7 @@ class WebUI:
126
124
  web_login: bool = False,
127
125
  tls_cert: str | None = None,
128
126
  tls_key: str | None = None,
129
- stats_csv_writer: StatsCSV | None = None,
127
+ stats_csv_writer: stats.StatsCSV | None = None,
130
128
  delayed_start=False,
131
129
  userclass_picker_is_active=False,
132
130
  build_path: str | None = None,
@@ -145,7 +143,7 @@ class WebUI:
145
143
  allows for adding Flask routes or Blueprints before accepting requests, avoiding errors.
146
144
  """
147
145
  environment.web_ui = self
148
- self.stats_csv_writer = stats_csv_writer or StatsCSV(environment, stats_module.PERCENTILES_TO_REPORT)
146
+ self.stats_csv_writer = stats_csv_writer or stats.StatsCSV(environment, stats.PERCENTILES_TO_REPORT)
149
147
  self.environment = environment
150
148
  self.host = host
151
149
  self.port = port
@@ -404,7 +402,11 @@ class WebUI:
404
402
  @self.auth_required_if_enabled
405
403
  def request_stats_full_history_csv() -> Response:
406
404
  options = self.environment.parsed_options
407
- if options and options.stats_history_enabled and isinstance(self.stats_csv_writer, StatsCSVFileWriter):
405
+ if (
406
+ options
407
+ and options.stats_history_enabled
408
+ and isinstance(self.stats_csv_writer, stats.StatsCSVFileWriter)
409
+ ):
408
410
  return send_file(
409
411
  os.path.abspath(self.stats_csv_writer.stats_history_file_name()),
410
412
  mimetype="text/csv",
@@ -430,12 +432,12 @@ class WebUI:
430
432
  @self.auth_required_if_enabled
431
433
  @memoize(timeout=DEFAULT_CACHE_TIME, dynamic_timeout=True)
432
434
  def request_stats() -> Response:
433
- stats: list[dict[str, Any]] = []
434
- errors: list[StatsErrorDict] = []
435
+ _stats: list[dict[str, Any]] = []
436
+ errors: list[stats.StatsErrorDict] = []
435
437
 
436
438
  if environment.runner is None:
437
439
  report = {
438
- "stats": stats,
440
+ "stats": _stats,
439
441
  "errors": errors,
440
442
  "total_rps": 0.0,
441
443
  "total_fail_per_sec": 0.0,
@@ -451,21 +453,21 @@ class WebUI:
451
453
 
452
454
  return jsonify(report)
453
455
 
454
- for s in chain(sort_stats(environment.runner.stats.entries), [environment.runner.stats.total]):
455
- stats.append(s.to_dict())
456
+ for s in chain(stats.sort_stats(environment.runner.stats.entries), [environment.runner.stats.total]):
457
+ _stats.append(s.to_dict())
456
458
 
457
459
  errors = [e.serialize() for e in environment.runner.errors.values()]
458
460
 
459
461
  # Truncate the total number of stats and errors displayed since a large number of rows will cause the app
460
462
  # to render extremely slowly. Aggregate stats should be preserved.
461
- truncated_stats = stats[:500]
462
- if len(stats) > 500:
463
- truncated_stats += [stats[-1]]
463
+ truncated_stats = _stats[:500]
464
+ if len(_stats) > 500:
465
+ truncated_stats += [_stats[-1]]
464
466
 
465
467
  report = {"stats": truncated_stats, "errors": errors[:500]}
466
- total_stats = stats[-1]
468
+ total_stats = _stats[-1]
467
469
 
468
- if stats:
470
+ if _stats:
469
471
  report["total_rps"] = total_stats["current_rps"]
470
472
  report["total_fail_per_sec"] = total_stats["current_fail_per_sec"]
471
473
  report["fail_ratio"] = environment.runner.stats.total.fail_ratio
@@ -473,7 +475,7 @@ class WebUI:
473
475
  f"response_time_percentile_{percentile}": environment.runner.stats.total.get_current_response_time_percentile(
474
476
  percentile
475
477
  )
476
- for percentile in stats_module.PERCENTILES_TO_CHART
478
+ for percentile in stats.PERCENTILES_TO_CHART
477
479
  }
478
480
 
479
481
  if isinstance(environment.runner, MasterRunner):
@@ -658,7 +660,7 @@ class WebUI:
658
660
  else:
659
661
  worker_count = 0
660
662
 
661
- stats = self.environment.runner.stats
663
+ request_stats = self.environment.runner.stats
662
664
  extra_options = argument_parser.ui_extra_args_dict()
663
665
 
664
666
  available_user_classes = None
@@ -690,7 +692,7 @@ class WebUI:
690
692
  "user_count": self.environment.runner.user_count,
691
693
  "version": version,
692
694
  "host": host if host else "",
693
- "history": stats.history if stats.num_requests > 0 else [],
695
+ "history": request_stats.history if request_stats.num_requests > 0 else [],
694
696
  "override_host_warning": override_host_warning,
695
697
  "num_users": options and options.num_users,
696
698
  "spawn_rate": options and options.spawn_rate,
@@ -710,8 +712,8 @@ class WebUI:
710
712
  "available_shape_classes": available_shape_classes,
711
713
  "available_user_tasks": available_user_tasks,
712
714
  "users": users,
713
- "percentiles_to_chart": stats_module.PERCENTILES_TO_CHART,
714
- "percentiles_to_statistics": stats_module.PERCENTILES_TO_STATISTICS,
715
+ "percentiles_to_chart": stats.PERCENTILES_TO_CHART,
716
+ "percentiles_to_statistics": stats.PERCENTILES_TO_STATISTICS,
715
717
  }
716
718
 
717
719
  self.template_args = {**self.template_args, **new_template_args}
File without changes
File without changes
File without changes