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.
Files changed (64) hide show
  1. locust/__init__.py +7 -7
  2. locust/_version.py +2 -2
  3. locust/argument_parser.py +35 -18
  4. locust/clients.py +7 -6
  5. locust/contrib/fasthttp.py +42 -37
  6. locust/debug.py +14 -8
  7. locust/dispatch.py +25 -25
  8. locust/env.py +45 -37
  9. locust/event.py +13 -10
  10. locust/exception.py +0 -9
  11. locust/html.py +9 -8
  12. locust/input_events.py +7 -5
  13. locust/main.py +95 -47
  14. locust/rpc/protocol.py +4 -2
  15. locust/rpc/zmqrpc.py +6 -4
  16. locust/runners.py +69 -76
  17. locust/shape.py +7 -4
  18. locust/stats.py +73 -71
  19. locust/templates/index.html +8 -23
  20. locust/test/mock_logging.py +7 -5
  21. locust/test/test_debugging.py +4 -4
  22. locust/test/test_dispatch.py +10 -9
  23. locust/test/test_env.py +30 -1
  24. locust/test/test_fasthttp.py +9 -8
  25. locust/test/test_http.py +4 -3
  26. locust/test/test_interruptable_task.py +4 -4
  27. locust/test/test_load_locustfile.py +6 -5
  28. locust/test/test_locust_class.py +11 -5
  29. locust/test/test_log.py +5 -4
  30. locust/test/test_main.py +37 -7
  31. locust/test/test_parser.py +11 -15
  32. locust/test/test_runners.py +22 -21
  33. locust/test/test_sequential_taskset.py +3 -2
  34. locust/test/test_stats.py +16 -18
  35. locust/test/test_tags.py +3 -2
  36. locust/test/test_taskratio.py +3 -3
  37. locust/test/test_users.py +3 -3
  38. locust/test/test_util.py +3 -2
  39. locust/test/test_wait_time.py +3 -3
  40. locust/test/test_web.py +142 -27
  41. locust/test/test_zmqrpc.py +5 -3
  42. locust/test/testcases.py +7 -7
  43. locust/test/util.py +6 -7
  44. locust/user/__init__.py +1 -1
  45. locust/user/inspectuser.py +6 -5
  46. locust/user/sequential_taskset.py +3 -1
  47. locust/user/task.py +22 -27
  48. locust/user/users.py +17 -7
  49. locust/util/deprecation.py +0 -1
  50. locust/util/exception_handler.py +1 -1
  51. locust/util/load_locustfile.py +4 -2
  52. locust/web.py +102 -53
  53. locust/webui/dist/assets/auth-5e21717c.js.map +1 -0
  54. locust/webui/dist/assets/{index-01afe4fa.js → index-a83a5dd9.js} +85 -84
  55. locust/webui/dist/assets/index-a83a5dd9.js.map +1 -0
  56. locust/webui/dist/auth.html +20 -0
  57. locust/webui/dist/index.html +1 -1
  58. {locust-2.20.1.dev28.dist-info → locust-2.20.2.dist-info}/METADATA +2 -2
  59. locust-2.20.2.dist-info/RECORD +105 -0
  60. locust-2.20.1.dev28.dist-info/RECORD +0 -102
  61. {locust-2.20.1.dev28.dist-info → locust-2.20.2.dist-info}/LICENSE +0 -0
  62. {locust-2.20.1.dev28.dist-info → locust-2.20.2.dist-info}/WHEEL +0 -0
  63. {locust-2.20.1.dev28.dist-info → locust-2.20.2.dist-info}/entry_points.txt +0 -0
  64. {locust-2.20.1.dev28.dist-info → locust-2.20.2.dist-info}/top_level.txt +0 -0
locust/stats.py CHANGED
@@ -1,47 +1,44 @@
1
1
  from __future__ import annotations
2
- from abc import abstractmethod
2
+
3
+ import csv
3
4
  import datetime
4
5
  import hashlib
5
6
  import json
6
- from tempfile import NamedTemporaryFile
7
- import time
8
- from collections import namedtuple, OrderedDict
9
- from copy import copy
10
- from itertools import chain
7
+ import logging
11
8
  import os
12
- import csv
13
9
  import signal
14
- import gevent
10
+ import time
11
+ from abc import abstractmethod
12
+ from collections import OrderedDict, namedtuple
13
+ from copy import copy
15
14
  from html import escape
16
- from .util.rounding import proper_round
17
-
15
+ from itertools import chain
16
+ from tempfile import NamedTemporaryFile
17
+ from types import FrameType
18
18
  from typing import (
19
19
  TYPE_CHECKING,
20
20
  Any,
21
- Dict,
21
+ Callable,
22
22
  Iterable,
23
23
  NoReturn,
24
- Tuple,
25
- List,
26
- Optional,
27
- OrderedDict as OrderedDictType,
28
- Callable,
29
- TypeVar,
30
- cast,
31
24
  Protocol,
32
25
  TypedDict,
26
+ TypeVar,
27
+ cast,
28
+ )
29
+ from typing import (
30
+ OrderedDict as OrderedDictType,
33
31
  )
34
32
 
35
- from types import FrameType
33
+ import gevent
36
34
 
37
- from .exception import CatchResponseError
38
35
  from .event import Events
39
-
40
- import logging
36
+ from .exception import CatchResponseError
37
+ from .util.rounding import proper_round
41
38
 
42
39
  if TYPE_CHECKING:
43
- from .runners import Runner
44
40
  from .env import Environment
41
+ from .runners import Runner
45
42
 
46
43
  console_logger = logging.getLogger("locust.stats_logger")
47
44
 
@@ -66,18 +63,18 @@ class StatsBaseDict(TypedDict):
66
63
 
67
64
 
68
65
  class StatsEntryDict(StatsBaseDict):
69
- last_request_timestamp: Optional[float]
66
+ last_request_timestamp: float | None
70
67
  start_time: float
71
68
  num_requests: int
72
69
  num_none_requests: int
73
70
  num_failures: int
74
71
  total_response_time: int
75
72
  max_response_time: int
76
- min_response_time: Optional[int]
73
+ min_response_time: int | None
77
74
  total_content_length: int
78
- response_times: Dict[int, int]
79
- num_reqs_per_sec: Dict[int, int]
80
- num_fail_per_sec: Dict[int, int]
75
+ response_times: dict[int, int]
76
+ num_reqs_per_sec: dict[int, int]
77
+ num_fail_per_sec: dict[int, int]
81
78
 
82
79
 
83
80
  class StatsErrorDict(StatsBaseDict):
@@ -93,7 +90,7 @@ class StatsHolder(Protocol):
93
90
  S = TypeVar("S", bound=StatsHolder)
94
91
 
95
92
 
96
- def resize_handler(signum: int, frame: Optional[FrameType]):
93
+ def resize_handler(signum: int, frame: FrameType | None):
97
94
  global STATS_NAME_WIDTH
98
95
  if STATS_AUTORESIZE:
99
96
  try:
@@ -119,18 +116,17 @@ HISTORY_STATS_INTERVAL_SEC = 5
119
116
  CSV_STATS_INTERVAL_SEC = 1
120
117
  CSV_STATS_FLUSH_INTERVAL_SEC = 10
121
118
 
122
-
123
119
  """
124
120
  Default window size/resolution - in seconds - when calculating the current
125
121
  response time percentile
126
122
  """
127
123
  CURRENT_RESPONSE_TIME_PERCENTILE_WINDOW = 10
128
124
 
129
-
130
125
  CachedResponseTimes = namedtuple("CachedResponseTimes", ["response_times", "num_requests"])
131
126
 
132
127
  PERCENTILES_TO_REPORT = [0.50, 0.66, 0.75, 0.80, 0.90, 0.95, 0.98, 0.99, 0.999, 0.9999, 1.0]
133
128
 
129
+ PERCENTILES_TO_STATISTICS = [0.95, 0.99]
134
130
  PERCENTILES_TO_CHART = [0.50, 0.95]
135
131
  MODERN_UI_PERCENTILES_TO_CHART = [0.95]
136
132
 
@@ -139,7 +135,7 @@ class RequestStatsAdditionError(Exception):
139
135
  pass
140
136
 
141
137
 
142
- def get_readable_percentiles(percentile_list: List[float]) -> List[str]:
138
+ def get_readable_percentiles(percentile_list: list[float]) -> list[str]:
143
139
  """
144
140
  Converts a list of percentiles from 0-1 fraction to 0%-100% view for using in console & csv reporting
145
141
  :param percentile_list: The list of percentiles in range 0-1
@@ -151,7 +147,7 @@ def get_readable_percentiles(percentile_list: List[float]) -> List[str]:
151
147
  ]
152
148
 
153
149
 
154
- def calculate_response_time_percentile(response_times: Dict[int, int], num_requests: int, percent: float) -> int:
150
+ def calculate_response_time_percentile(response_times: dict[int, int], num_requests: int, percent: float) -> int:
155
151
  """
156
152
  Get the response time that a certain number of percent of the requests
157
153
  finished within. Arguments:
@@ -172,7 +168,7 @@ def calculate_response_time_percentile(response_times: Dict[int, int], num_reque
172
168
  return 0
173
169
 
174
170
 
175
- def diff_response_time_dicts(latest: Dict[int, int], old: Dict[int, int]) -> Dict[int, int]:
171
+ def diff_response_time_dicts(latest: dict[int, int], old: dict[int, int]) -> dict[int, int]:
176
172
  """
177
173
  Returns the delta between two {response_times:request_count} dicts.
178
174
 
@@ -212,8 +208,8 @@ class RequestStats:
212
208
  is not needed.
213
209
  """
214
210
  self.use_response_times_cache = use_response_times_cache
215
- self.entries: Dict[Tuple[str, str], StatsEntry] = EntriesDict(self)
216
- self.errors: Dict[str, StatsError] = {}
211
+ self.entries: dict[tuple[str, str], StatsEntry] = EntriesDict(self)
212
+ self.errors: dict[str, StatsError] = {}
217
213
  self.total = StatsEntry(self, "Aggregated", None, use_response_times_cache=self.use_response_times_cache)
218
214
  self.history = []
219
215
 
@@ -253,7 +249,7 @@ class RequestStats:
253
249
  self.errors[key] = entry
254
250
  entry.occurred()
255
251
 
256
- def get(self, name: str, method: str) -> "StatsEntry":
252
+ def get(self, name: str, method: str) -> StatsEntry:
257
253
  """
258
254
  Retrieve a StatsEntry instance by name and method
259
255
  """
@@ -278,12 +274,12 @@ class RequestStats:
278
274
  self.errors = {}
279
275
  self.history = []
280
276
 
281
- def serialize_stats(self) -> List["StatsEntryDict"]:
277
+ def serialize_stats(self) -> list[StatsEntryDict]:
282
278
  return [
283
279
  e.get_stripped_report() for e in self.entries.values() if not (e.num_requests == 0 and e.num_failures == 0)
284
280
  ]
285
281
 
286
- def serialize_errors(self) -> Dict[str, "StatsErrorDict"]:
282
+ def serialize_errors(self) -> dict[str, StatsErrorDict]:
287
283
  return {k: e.serialize() for k, e in self.errors.items()}
288
284
 
289
285
 
@@ -292,7 +288,7 @@ class StatsEntry:
292
288
  Represents a single stats entry (name and method)
293
289
  """
294
290
 
295
- def __init__(self, stats: Optional[RequestStats], name: str, method: str, use_response_times_cache: bool = False):
291
+ def __init__(self, stats: RequestStats | None, name: str, method: str, use_response_times_cache: bool = False):
296
292
  self.stats = stats
297
293
  self.name = name
298
294
  """ Name (URL) of this stats entry """
@@ -313,15 +309,15 @@ class StatsEntry:
313
309
  """ Number of failed request """
314
310
  self.total_response_time: int = 0
315
311
  """ Total sum of the response times """
316
- self.min_response_time: Optional[int] = None
312
+ self.min_response_time: int | None = None
317
313
  """ Minimum response time """
318
314
  self.max_response_time: int = 0
319
315
  """ Maximum response time """
320
- self.num_reqs_per_sec: Dict[int, int] = {}
316
+ self.num_reqs_per_sec: dict[int, int] = {}
321
317
  """ A {second => request_count} dict that holds the number of requests made per second """
322
- self.num_fail_per_sec: Dict[int, int] = {}
318
+ self.num_fail_per_sec: dict[int, int] = {}
323
319
  """ A (second => failure_count) dict that hold the number of failures per second """
324
- self.response_times: Dict[int, int] = {}
320
+ self.response_times: dict[int, int] = {}
325
321
  """
326
322
  A {response_time => count} dict that holds the response time distribution of all
327
323
  the requests.
@@ -331,7 +327,7 @@ class StatsEntry:
331
327
 
332
328
  This dict is used to calculate the median and percentile response times.
333
329
  """
334
- self.response_times_cache: Optional[OrderedDictType[int, CachedResponseTimes]] = None
330
+ self.response_times_cache: OrderedDictType[int, CachedResponseTimes] | None = None
335
331
  """
336
332
  If use_response_times_cache is set to True, this will be a {timestamp => CachedResponseTimes()}
337
333
  OrderedDict that holds a copy of the response_times dict for each of the last 20 seconds.
@@ -340,7 +336,7 @@ class StatsEntry:
340
336
  """ The sum of the content length of all the responses for this entry """
341
337
  self.start_time: float = 0.0
342
338
  """ Time of the first request for this entry """
343
- self.last_request_timestamp: Optional[float] = None
339
+ self.last_request_timestamp: float | None = None
344
340
  """ Time of the last request for this entry """
345
341
  self.reset()
346
342
 
@@ -456,7 +452,7 @@ class StatsEntry:
456
452
  return 0
457
453
  slice_start_time = max(int(self.stats.last_request_timestamp) - 12, int(self.stats.start_time or 0))
458
454
 
459
- reqs: List[int | float] = [
455
+ reqs: list[int | float] = [
460
456
  self.num_reqs_per_sec.get(t, 0) for t in range(slice_start_time, int(self.stats.last_request_timestamp) - 2)
461
457
  ]
462
458
  return avg(reqs)
@@ -497,7 +493,7 @@ class StatsEntry:
497
493
  except ZeroDivisionError:
498
494
  return 0
499
495
 
500
- def extend(self, other: "StatsEntry") -> None:
496
+ def extend(self, other: StatsEntry) -> None:
501
497
  """
502
498
  Extend the data from the current StatsEntry with the stats from another
503
499
  StatsEntry instance.
@@ -545,7 +541,7 @@ class StatsEntry:
545
541
  return cast(StatsEntryDict, {key: getattr(self, key, None) for key in StatsEntryDict.__annotations__.keys()})
546
542
 
547
543
  @classmethod
548
- def unserialize(cls, data: StatsEntryDict) -> "StatsEntry":
544
+ def unserialize(cls, data: StatsEntryDict) -> StatsEntry:
549
545
  """Return the unserialzed version of the specified dict"""
550
546
  obj = cls(None, data["name"], data["method"])
551
547
  valid_keys = StatsEntryDict.__annotations__.keys()
@@ -608,7 +604,7 @@ class StatsEntry:
608
604
  """
609
605
  return calculate_response_time_percentile(self.response_times, self.num_requests, percent)
610
606
 
611
- def get_current_response_time_percentile(self, percent: float) -> Optional[int]:
607
+ def get_current_response_time_percentile(self, percent: float) -> int | None:
612
608
  """
613
609
  Calculate the *current* response time for a certain percentile. We use a sliding
614
610
  window of (approximately) the last 10 seconds (specified by CURRENT_RESPONSE_TIME_PERCENTILE_WINDOW)
@@ -626,13 +622,13 @@ class StatsEntry:
626
622
  # when trying to fetch the cached response_times. We construct this list in such a way
627
623
  # that it's ordered by preference by starting to add t-10, then t-11, t-9, t-12, t-8,
628
624
  # and so on
629
- acceptable_timestamps: List[int] = []
625
+ acceptable_timestamps: list[int] = []
630
626
  acceptable_timestamps.append(t - CURRENT_RESPONSE_TIME_PERCENTILE_WINDOW)
631
627
  for i in range(1, 9):
632
628
  acceptable_timestamps.append(t - CURRENT_RESPONSE_TIME_PERCENTILE_WINDOW - i)
633
629
  acceptable_timestamps.append(t - CURRENT_RESPONSE_TIME_PERCENTILE_WINDOW + i)
634
630
 
635
- cached: Optional[CachedResponseTimes] = None
631
+ cached: CachedResponseTimes | None = None
636
632
  if self.response_times_cache is not None:
637
633
  for ts in acceptable_timestamps:
638
634
  if ts in self.response_times_cache:
@@ -685,6 +681,11 @@ class StatsEntry:
685
681
  self.response_times_cache.popitem(last=False)
686
682
 
687
683
  def to_dict(self, escape_string_values=False):
684
+ response_time_percentiles = {
685
+ f"response_time_percentile_{percentile}": self.get_response_time_percentile(percentile)
686
+ for percentile in PERCENTILES_TO_STATISTICS
687
+ }
688
+
688
689
  return {
689
690
  "method": escape(self.method or "") if escape_string_values else self.method,
690
691
  "name": escape(self.name) if escape_string_values else self.name,
@@ -697,8 +698,9 @@ class StatsEntry:
697
698
  "current_rps": self.current_rps,
698
699
  "current_fail_per_sec": self.current_fail_per_sec,
699
700
  "median_response_time": self.median_response_time,
700
- "ninetieth_response_time": self.get_response_time_percentile(0.9),
701
- "ninety_ninth_response_time": self.get_response_time_percentile(0.99),
701
+ "ninetieth_response_time": self.get_response_time_percentile(0.9), # for legacy ui
702
+ "ninety_ninth_response_time": self.get_response_time_percentile(0.99), # for legacy ui
703
+ **response_time_percentiles, # for modern ui
702
704
  "avg_content_length": self.avg_content_length,
703
705
  }
704
706
 
@@ -748,7 +750,7 @@ class StatsError:
748
750
  return f"{self.method} {self.name}: {unwrapped_error}"
749
751
 
750
752
  def serialize(self) -> StatsErrorDict:
751
- def _getattr(obj: "StatsError", key: str, default: Optional[Any]) -> Optional[Any]:
753
+ def _getattr(obj: StatsError, key: str, default: Any | None) -> Any | None:
752
754
  value = getattr(obj, key, default)
753
755
 
754
756
  if key in ["error"]:
@@ -759,7 +761,7 @@ class StatsError:
759
761
  return cast(StatsErrorDict, {key: _getattr(self, key, None) for key in StatsErrorDict.__annotations__.keys()})
760
762
 
761
763
  @classmethod
762
- def unserialize(cls, data: StatsErrorDict) -> "StatsError":
764
+ def unserialize(cls, data: StatsErrorDict) -> StatsError:
763
765
  return cls(data["method"], data["name"], data["error"], data["occurrences"])
764
766
 
765
767
  def to_dict(self, escape_string_values=False):
@@ -771,11 +773,11 @@ class StatsError:
771
773
  }
772
774
 
773
775
 
774
- def avg(values: List[float | int]) -> float:
776
+ def avg(values: list[float | int]) -> float:
775
777
  return sum(values, 0.0) / max(len(values), 1)
776
778
 
777
779
 
778
- def median_from_dict(total: int, count: Dict[int, int]) -> int:
780
+ def median_from_dict(total: int, count: dict[int, int]) -> int:
779
781
  """
780
782
  total is the number of requests made
781
783
  count is a dict {response_time: count}
@@ -790,13 +792,13 @@ def median_from_dict(total: int, count: Dict[int, int]) -> int:
790
792
 
791
793
 
792
794
  def setup_distributed_stats_event_listeners(events: Events, stats: RequestStats) -> None:
793
- def on_report_to_master(client_id: str, data: Dict[str, Any]) -> None:
795
+ def on_report_to_master(client_id: str, data: dict[str, Any]) -> None:
794
796
  data["stats"] = stats.serialize_stats()
795
797
  data["stats_total"] = stats.total.get_stripped_report()
796
798
  data["errors"] = stats.serialize_errors()
797
799
  stats.errors = {}
798
800
 
799
- def on_worker_report(client_id: str, data: Dict[str, Any]) -> None:
801
+ def on_worker_report(client_id: str, data: dict[str, Any]) -> None:
800
802
  for stats_data in data["stats"]:
801
803
  entry = StatsEntry.unserialize(stats_data)
802
804
  request_key = (entry.name, entry.method)
@@ -826,7 +828,7 @@ def print_stats_json(stats: RequestStats) -> None:
826
828
  print(json.dumps(stats.serialize_stats(), indent=4))
827
829
 
828
830
 
829
- def get_stats_summary(stats: RequestStats, current=True) -> List[str]:
831
+ def get_stats_summary(stats: RequestStats, current=True) -> list[str]:
830
832
  """
831
833
  stats summary will be returned as list of string
832
834
  """
@@ -852,7 +854,7 @@ def print_percentile_stats(stats: RequestStats) -> None:
852
854
  console_logger.info("")
853
855
 
854
856
 
855
- def get_percentile_stats_summary(stats: RequestStats) -> List[str]:
857
+ def get_percentile_stats_summary(stats: RequestStats) -> list[str]:
856
858
  """
857
859
  Percentile stats summary will be returned as list of string
858
860
  """
@@ -886,7 +888,7 @@ def print_error_report(stats: RequestStats) -> None:
886
888
  console_logger.info(line)
887
889
 
888
890
 
889
- def get_error_report_summary(stats) -> List[str]:
891
+ def get_error_report_summary(stats) -> list[str]:
890
892
  summary = ["Error report"]
891
893
  summary.append("%-18s %-100s" % ("# occurrences", "Error"))
892
894
  separator = f'{"-" * 18}|{"-" * ((80 + STATS_NAME_WIDTH) - 19)}'
@@ -907,11 +909,11 @@ def stats_printer(stats: RequestStats) -> Callable[[], None]:
907
909
  return stats_printer_func
908
910
 
909
911
 
910
- def sort_stats(stats: Dict[Any, S]) -> List[S]:
912
+ def sort_stats(stats: dict[Any, S]) -> list[S]:
911
913
  return [stats[key] for key in sorted(stats.keys())]
912
914
 
913
915
 
914
- def stats_history(runner: "Runner") -> None:
916
+ def stats_history(runner: Runner) -> None:
915
917
  """Save current stats info to history for charts of report."""
916
918
  while True:
917
919
  stats = runner.stats
@@ -925,6 +927,7 @@ def stats_history(runner: "Runner") -> None:
925
927
  }
926
928
 
927
929
  r = {
930
+ **current_response_time_percentiles,
928
931
  "time": datetime.datetime.now(tz=datetime.timezone.utc).strftime("%H:%M:%S"),
929
932
  "current_rps": stats.total.current_rps or 0,
930
933
  "current_fail_per_sec": stats.total.current_fail_per_sec or 0,
@@ -933,7 +936,6 @@ def stats_history(runner: "Runner") -> None:
933
936
  "response_time_percentile_2": stats.total.get_current_response_time_percentile(PERCENTILES_TO_CHART[1])
934
937
  or 0,
935
938
  "total_avg_response_time": stats.total.avg_response_time,
936
- "current_response_time_percentiles": current_response_time_percentiles,
937
939
  "user_count": runner.user_count or 0,
938
940
  }
939
941
  stats.history.append(r)
@@ -943,7 +945,7 @@ def stats_history(runner: "Runner") -> None:
943
945
  class StatsCSV:
944
946
  """Write statistics to csv_writer stream."""
945
947
 
946
- def __init__(self, environment: "Environment", percentiles_to_report: List[float]) -> None:
948
+ def __init__(self, environment: Environment, percentiles_to_report: list[float]) -> None:
947
949
  self.environment = environment
948
950
  self.percentiles_to_report = percentiles_to_report
949
951
 
@@ -977,7 +979,7 @@ class StatsCSV:
977
979
  "Nodes",
978
980
  ]
979
981
 
980
- def _percentile_fields(self, stats_entry: StatsEntry, use_current: bool = False) -> List[str] | List[int]:
982
+ def _percentile_fields(self, stats_entry: StatsEntry, use_current: bool = False) -> list[str] | list[int]:
981
983
  if not stats_entry.num_requests:
982
984
  return self.percentiles_na
983
985
  elif use_current:
@@ -1045,8 +1047,8 @@ class StatsCSVFileWriter(StatsCSV):
1045
1047
 
1046
1048
  def __init__(
1047
1049
  self,
1048
- environment: "Environment",
1049
- percentiles_to_report: List[float],
1050
+ environment: Environment,
1051
+ percentiles_to_report: list[float],
1050
1052
  base_filepath: str,
1051
1053
  full_history: bool = False,
1052
1054
  ):
@@ -1142,7 +1144,7 @@ class StatsCSVFileWriter(StatsCSV):
1142
1144
 
1143
1145
  stats = self.environment.stats
1144
1146
  timestamp = int(now)
1145
- stats_entries: List[StatsEntry] = []
1147
+ stats_entries: list[StatsEntry] = []
1146
1148
  if self.full_history:
1147
1149
  stats_entries = sort_stats(stats.entries)
1148
1150
 
@@ -273,30 +273,15 @@
273
273
  </div>
274
274
  <div class="padder">
275
275
  <h1>About</h1>
276
- <p>
277
- The original idea for Locust was Carl Byström's who made a first proof of concept in June 2010.
278
- Jonatan Heyman picked up Locust in January 2011, implemented the current concept of Locust classes
279
- and made it work distributed across multiple machines.
280
- </p>
281
- <p>
282
- Jonatan, Carl and Joakim Hamrén has continued the development of Locust at their job,
283
- ESN Social Software, who have adopted Locust as an inhouse Open Source project.
284
- </p>
285
-
286
- <h1>Authors and Copyright</h1>
287
- <a href="http://cgbystrom.com/">Carl Byström</a> (<a href="https://twitter.com/cgbystrom/">@cgbystrom</a>)<br>
288
- <a href="http://heyman.info/">Jonatan Heyman</a> (<a href="https://twitter.com/jonatanheyman/">@jonatanheyman</a>)<br>
289
- Joakim Hamrén (<a href="https://twitter.com/Jahaaja/">@jahaaja</a>)<br>
290
- <a href="http://esn.me/">ESN Social Software</a> (<a href="https://twitter.com/uprise_ea/">@uprise_ea</a>)<br>
291
- Hugo Heyman (<a href="https://twitter.com/hugoheyman/">@hugoheyman</a>)
292
-
293
-
294
- <h1>License</h1>
295
- Open source licensed under the MIT license.
296
-
276
+ <h2>Authors and Copyright</h2>
277
+ <a href="https://github.com/heyman">Jonatan Heyman</a><br>
278
+ <a href="https://github.com/cyberw">Lars Holmberg</a><br>
279
+ <a href="https://github.com/andrewbaldwin44">Andrew Baldwin</a><br>
280
+ Carl Byström, Joakim Hamrén & Hugo Heyman<br>
281
+ Many thanks to our other great <a href="https://github.com/locustio/locust/graphs/contributors">contributors</a>!
297
282
  <h2>Version <a href="https://github.com/locustio/locust/releases/tag/{{version}}">{{version}}</a></h2>
298
- <br>
299
- <a href="https://locust.io/">https://locust.io</a>
283
+ <h1>License</h1>
284
+ <a href="https://github.com/locustio/locust/blob/master/LICENSE">MIT license</a>
300
285
  </div>
301
286
  </div>
302
287
  <nav class="footer">
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  import logging
2
4
 
3
5
  from typing import List, Union, Dict
@@ -7,11 +9,11 @@ LogMessage = List[Union[str, Dict[str, TracebackType]]]
7
9
 
8
10
 
9
11
  class MockedLoggingHandler(logging.Handler):
10
- debug: List[LogMessage] = []
11
- warning: List[LogMessage] = []
12
- info: List[LogMessage] = []
13
- error: List[LogMessage] = []
14
- critical: List[LogMessage] = []
12
+ debug: list[LogMessage] = []
13
+ warning: list[LogMessage] = []
14
+ info: list[LogMessage] = []
15
+ error: list[LogMessage] = []
16
+ critical: list[LogMessage] = []
15
17
 
16
18
  def emit(self, record):
17
19
  if record.exc_info:
@@ -1,12 +1,12 @@
1
- import os
2
- from threading import Timer
3
- from unittest import mock
4
-
5
1
  from locust import debug, task
6
2
  from locust.test.testcases import LocustTestCase
7
3
  from locust.user.task import LOCUST_STATE_STOPPING
8
4
  from locust.user.users import HttpUser
9
5
 
6
+ import os
7
+ from threading import Timer
8
+ from unittest import mock
9
+
10
10
 
11
11
  class DebugTestCase(LocustTestCase):
12
12
  def setUp(self):
@@ -1,13 +1,14 @@
1
- import time
2
- import unittest
3
- from operator import attrgetter
4
- from typing import Dict, List, Tuple, Type
1
+ from __future__ import annotations
5
2
 
6
3
  from locust import User
7
4
  from locust.dispatch import UsersDispatcher
8
5
  from locust.runners import WorkerNode
9
6
  from locust.test.util import clear_all_functools_lru_cache
10
7
 
8
+ import time
9
+ import unittest
10
+ from operator import attrgetter
11
+
11
12
  _TOLERANCE = 0.025
12
13
 
13
14
 
@@ -3372,7 +3373,7 @@ class TestAddWorker(unittest.TestCase):
3372
3373
 
3373
3374
  class TestRampUpUsersFromZeroWithFixed(unittest.TestCase):
3374
3375
  class RampUpCase:
3375
- def __init__(self, fixed_counts: Tuple[int], weights: Tuple[int], target_user_count: int):
3376
+ def __init__(self, fixed_counts: tuple[int], weights: tuple[int], target_user_count: int):
3376
3377
  self.fixed_counts = fixed_counts
3377
3378
  self.weights = weights
3378
3379
  self.target_user_count = target_user_count
@@ -3382,7 +3383,7 @@ class TestRampUpUsersFromZeroWithFixed(unittest.TestCase):
3382
3383
  self.fixed_counts, self.weights, self.target_user_count
3383
3384
  )
3384
3385
 
3385
- def case_handler(self, cases: List[RampUpCase], expected: List[Dict[str, int]], user_classes: List[Type[User]]):
3386
+ def case_handler(self, cases: list[RampUpCase], expected: list[dict[str, int]], user_classes: list[type[User]]):
3386
3387
  self.assertEqual(len(cases), len(expected))
3387
3388
 
3388
3389
  for case_num in range(len(cases)):
@@ -4092,14 +4093,14 @@ class TestRampUpDifferentUsers(unittest.TestCase):
4092
4093
  self.assertEqual(_user_count_on_worker(dispatched_users, worker_nodes[2].id), 6)
4093
4094
 
4094
4095
 
4095
- def _aggregate_dispatched_users(d: Dict[str, Dict[str, int]]) -> Dict[str, int]:
4096
+ def _aggregate_dispatched_users(d: dict[str, dict[str, int]]) -> dict[str, int]:
4096
4097
  user_classes = list(next(iter(d.values())).keys())
4097
4098
  return {u: sum(d[u] for d in d.values()) for u in user_classes}
4098
4099
 
4099
4100
 
4100
- def _user_count(d: Dict[str, Dict[str, int]]) -> int:
4101
+ def _user_count(d: dict[str, dict[str, int]]) -> int:
4101
4102
  return sum(map(sum, map(dict.values, d.values()))) # type: ignore
4102
4103
 
4103
4104
 
4104
- def _user_count_on_worker(d: Dict[str, Dict[str, int]], worker_node_id: str) -> int:
4105
+ def _user_count_on_worker(d: dict[str, dict[str, int]], worker_node_id: str) -> int:
4105
4106
  return sum(d[worker_node_id].values())
locust/test/test_env.py CHANGED
@@ -7,9 +7,10 @@ from locust.user import (
7
7
  task,
8
8
  )
9
9
  from locust.user.task import TaskSet
10
- from .testcases import LocustTestCase
10
+
11
11
  from .fake_module1_for_env_test import MyUserWithSameName as MyUserWithSameName1
12
12
  from .fake_module2_for_env_test import MyUserWithSameName as MyUserWithSameName2
13
+ from .testcases import LocustTestCase
13
14
 
14
15
 
15
16
  class TestEnvironment(LocustTestCase):
@@ -199,3 +200,31 @@ class TestEnvironment(LocustTestCase):
199
200
  ValueError, r"instance of LoadTestShape or subclass LoadTestShape", msg="exception message is mismatching"
200
201
  ):
201
202
  Environment(user_classes=[MyUserWithSameName1], shape_class=SubLoadTestShape)
203
+
204
+ def test_update_user_class(self):
205
+ class MyUser1(User):
206
+ @task
207
+ def my_task(self):
208
+ pass
209
+
210
+ @task
211
+ def my_task_2(self):
212
+ pass
213
+
214
+ class MyUser2(User):
215
+ @task
216
+ def my_task(self):
217
+ pass
218
+
219
+ environment = Environment(
220
+ user_classes=[MyUser1, MyUser2],
221
+ available_user_classes={"User1": MyUser1, "User2": MyUser2},
222
+ available_user_tasks={"User1": MyUser1.tasks, "User2": MyUser2.tasks},
223
+ )
224
+
225
+ environment.update_user_class({"user_class_name": "User1", "host": "http://localhost", "tasks": ["my_task_2"]})
226
+
227
+ self.assertEqual(
228
+ environment.available_user_classes["User1"].json(),
229
+ {"host": "http://localhost", "tasks": ["my_task_2"], "fixed_count": 0, "weight": 1},
230
+ )
@@ -1,17 +1,18 @@
1
+ from locust import FastHttpUser
2
+ from locust.argument_parser import parse_options
3
+ from locust.contrib.fasthttp import FastHttpSession
4
+ from locust.exception import CatchResponseError, InterruptTaskSet, LocustError, ResponseError
5
+ from locust.user import TaskSet, task
6
+ from locust.util.load_locustfile import is_user_class
7
+
1
8
  import socket
2
- import gevent
3
9
  import time
4
10
  from tempfile import NamedTemporaryFile
5
11
 
12
+ import gevent
6
13
  from geventhttpclient.client import HTTPClientPool
7
14
 
8
- from locust.argument_parser import parse_options
9
- from locust.user import task, TaskSet
10
- from locust.contrib.fasthttp import FastHttpSession
11
- from locust import FastHttpUser
12
- from locust.exception import CatchResponseError, InterruptTaskSet, LocustError, ResponseError
13
- from locust.util.load_locustfile import is_user_class
14
- from .testcases import WebserverTestCase, LocustTestCase
15
+ from .testcases import LocustTestCase, WebserverTestCase
15
16
  from .util import create_tls_cert
16
17
 
17
18
 
locust/test/test_http.py CHANGED
@@ -1,10 +1,11 @@
1
+ from locust.clients import HttpSession
2
+ from locust.exception import LocustError, ResponseError
3
+ from locust.user.users import HttpUser
4
+
1
5
  import time
2
6
 
3
- from locust.user.users import HttpUser
4
7
  from requests.exceptions import InvalidSchema, InvalidURL, MissingSchema, RequestException
5
8
 
6
- from locust.clients import HttpSession
7
- from locust.exception import LocustError, ResponseError
8
9
  from .testcases import WebserverTestCase
9
10
 
10
11