locust 2.20.1.dev26__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.dev26.dist-info → locust-2.20.2.dist-info}/METADATA +2 -2
- locust-2.20.2.dist-info/RECORD +105 -0
- locust-2.20.1.dev26.dist-info/RECORD +0 -102
- {locust-2.20.1.dev26.dist-info → locust-2.20.2.dist-info}/LICENSE +0 -0
- {locust-2.20.1.dev26.dist-info → locust-2.20.2.dist-info}/WHEEL +0 -0
- {locust-2.20.1.dev26.dist-info → locust-2.20.2.dist-info}/entry_points.txt +0 -0
- {locust-2.20.1.dev26.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
|
-
|
2
|
+
|
3
|
+
import csv
|
3
4
|
import datetime
|
4
5
|
import hashlib
|
5
6
|
import json
|
6
|
-
|
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
|
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
|
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
|
-
|
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
|
-
|
33
|
+
import gevent
|
36
34
|
|
37
|
-
from .exception import CatchResponseError
|
38
35
|
from .event import Events
|
39
|
-
|
40
|
-
import
|
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:
|
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:
|
73
|
+
min_response_time: int | None
|
77
74
|
total_content_length: int
|
78
|
-
response_times:
|
79
|
-
num_reqs_per_sec:
|
80
|
-
num_fail_per_sec:
|
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:
|
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:
|
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:
|
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:
|
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:
|
216
|
-
self.errors:
|
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) ->
|
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) ->
|
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) ->
|
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:
|
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:
|
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:
|
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:
|
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:
|
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:
|
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:
|
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:
|
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:
|
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) ->
|
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) ->
|
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:
|
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:
|
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:
|
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) ->
|
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:
|
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:
|
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:
|
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:
|
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) ->
|
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) ->
|
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) ->
|
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:
|
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:
|
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:
|
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) ->
|
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:
|
1049
|
-
percentiles_to_report:
|
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:
|
1147
|
+
stats_entries: list[StatsEntry] = []
|
1146
1148
|
if self.full_history:
|
1147
1149
|
stats_entries = sort_stats(stats.entries)
|
1148
1150
|
|
locust/templates/index.html
CHANGED
@@ -273,30 +273,15 @@
|
|
273
273
|
</div>
|
274
274
|
<div class="padder">
|
275
275
|
<h1>About</h1>
|
276
|
-
<
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
<
|
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
|
-
<
|
299
|
-
<a href="https://
|
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">
|
locust/test/mock_logging.py
CHANGED
@@ -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:
|
11
|
-
warning:
|
12
|
-
info:
|
13
|
-
error:
|
14
|
-
critical:
|
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:
|
locust/test/test_debugging.py
CHANGED
@@ -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):
|
locust/test/test_dispatch.py
CHANGED
@@ -1,13 +1,14 @@
|
|
1
|
-
import
|
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:
|
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:
|
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:
|
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:
|
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:
|
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
|
-
|
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
|
+
)
|
locust/test/test_fasthttp.py
CHANGED
@@ -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
|
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
|
|