locust 2.26.1.dev26__py3-none-any.whl → 2.26.1.dev61__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- locust/_version.py +2 -2
- locust/argument_parser.py +61 -150
- locust/dispatch.py +35 -69
- locust/runners.py +3 -7
- locust/test/test_dispatch.py +35 -1
- locust/test/test_main.py +70 -29
- locust/test/test_parser.py +35 -57
- locust/user/users.py +21 -23
- {locust-2.26.1.dev26.dist-info → locust-2.26.1.dev61.dist-info}/METADATA +1 -2
- {locust-2.26.1.dev26.dist-info → locust-2.26.1.dev61.dist-info}/RECORD +14 -14
- {locust-2.26.1.dev26.dist-info → locust-2.26.1.dev61.dist-info}/LICENSE +0 -0
- {locust-2.26.1.dev26.dist-info → locust-2.26.1.dev61.dist-info}/WHEEL +0 -0
- {locust-2.26.1.dev26.dist-info → locust-2.26.1.dev61.dist-info}/entry_points.txt +0 -0
- {locust-2.26.1.dev26.dist-info → locust-2.26.1.dev61.dist-info}/top_level.txt +0 -0
locust/_version.py
CHANGED
@@ -12,5 +12,5 @@ __version__: str
|
|
12
12
|
__version_tuple__: VERSION_TUPLE
|
13
13
|
version_tuple: VERSION_TUPLE
|
14
14
|
|
15
|
-
__version__ = version = '2.26.1.
|
16
|
-
__version_tuple__ = version_tuple = (2, 26, 1, '
|
15
|
+
__version__ = version = '2.26.1.dev61'
|
16
|
+
__version_tuple__ = version_tuple = (2, 26, 1, 'dev61')
|
locust/argument_parser.py
CHANGED
@@ -93,90 +93,54 @@ class LocustTomlConfigParser(configargparse.TomlConfigParser):
|
|
93
93
|
return result
|
94
94
|
|
95
95
|
|
96
|
-
def
|
96
|
+
def parse_locustfile_paths(paths: list[str]) -> list[str]:
|
97
97
|
"""
|
98
|
-
|
99
|
-
"""
|
100
|
-
return os.path.isdir(path) and os.path.exists(os.path.join(path, "__init__.py"))
|
101
|
-
|
102
|
-
|
103
|
-
def find_locustfile(locustfile: str) -> str | None:
|
104
|
-
"""
|
105
|
-
Attempt to locate a locustfile, either explicitly or by searching parent dirs.
|
106
|
-
"""
|
107
|
-
# Obtain env value
|
108
|
-
names = [locustfile]
|
109
|
-
# Create .py version if necessary
|
110
|
-
if not names[0].endswith(".py"):
|
111
|
-
names.append(names[0] + ".py")
|
112
|
-
# Does the name contain path elements?
|
113
|
-
if os.path.dirname(names[0]):
|
114
|
-
# If so, expand home-directory markers and test for existence
|
115
|
-
for name in names:
|
116
|
-
expanded = os.path.expanduser(name)
|
117
|
-
if os.path.exists(expanded):
|
118
|
-
if name.endswith(".py") or _is_package(expanded):
|
119
|
-
return os.path.abspath(expanded)
|
120
|
-
else:
|
121
|
-
# Otherwise, start in cwd and work downwards towards filesystem root
|
122
|
-
path = os.path.abspath(".")
|
123
|
-
while True:
|
124
|
-
for name in names:
|
125
|
-
joined = os.path.join(path, name)
|
126
|
-
if os.path.exists(joined):
|
127
|
-
if name.endswith(".py") or _is_package(joined):
|
128
|
-
return os.path.abspath(joined)
|
129
|
-
parent_path = os.path.dirname(path)
|
130
|
-
if parent_path == path:
|
131
|
-
# we've reached the root path which has been checked this iteration
|
132
|
-
break
|
133
|
-
path = parent_path
|
134
|
-
|
135
|
-
return None
|
136
|
-
|
98
|
+
Returns a list of relative file paths.
|
137
99
|
|
138
|
-
|
139
|
-
|
140
|
-
Returns a list of relative file paths for the Locustfile Picker. If is_directory is True,
|
141
|
-
locustfiles is expected to have a single index which is a directory that will be searched for
|
142
|
-
locustfiles.
|
100
|
+
Args:
|
101
|
+
paths (list[str]): paths taken from the -f command
|
143
102
|
|
144
|
-
|
103
|
+
Returns:
|
104
|
+
list[str]: Parsed locust file paths
|
145
105
|
"""
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
106
|
+
# Parse each path and unpack the returned lists as a single list
|
107
|
+
return [parsed for path in paths for parsed in _parse_locustfile_path(path)]
|
108
|
+
|
109
|
+
|
110
|
+
def _parse_locustfile_path(path: str) -> list[str]:
|
111
|
+
parsed_paths = []
|
112
|
+
if is_url(path):
|
113
|
+
# Download the file and use the new path as locustfile
|
114
|
+
parsed_paths.append(download_locustfile_from_url(path))
|
115
|
+
elif os.path.isdir(path):
|
116
|
+
# Find all .py files in directory tree
|
117
|
+
for root, _dirs, fs in os.walk(path):
|
118
|
+
parsed_paths.extend(
|
119
|
+
[
|
120
|
+
os.path.abspath(os.path.join(root, f))
|
121
|
+
for f in fs
|
122
|
+
if os.path.isfile(os.path.join(root, f)) and f.endswith(".py") and not f.startswith("_")
|
123
|
+
]
|
124
|
+
)
|
125
|
+
if not parsed_paths:
|
126
|
+
sys.stderr.write(f"Could not find any locustfiles in directory '{path}'")
|
161
127
|
sys.exit(1)
|
162
|
-
|
163
|
-
for root, dirs, files in os.walk(locustdir):
|
164
|
-
for file in files:
|
165
|
-
if not file.startswith("_") and file.endswith(".py"):
|
166
|
-
file_path = os.path.join(root, file)
|
167
|
-
file_paths.append(file_path)
|
168
128
|
else:
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
129
|
+
# If file exists add the abspath
|
130
|
+
if os.path.exists(path) and path.endswith(".py"):
|
131
|
+
parsed_paths.append(os.path.abspath(path))
|
132
|
+
else:
|
133
|
+
note_about_file_endings = "Ensure your locustfile ends with '.py' or is a directory with locustfiles. "
|
134
|
+
sys.stderr.write(f"Could not find '{path}'. {note_about_file_endings}See --help for available options.\n")
|
135
|
+
sys.exit(1)
|
175
136
|
|
176
|
-
return
|
137
|
+
return parsed_paths
|
177
138
|
|
178
139
|
|
179
140
|
def is_url(url: str) -> bool:
|
141
|
+
"""
|
142
|
+
Check if path is an url
|
143
|
+
"""
|
180
144
|
try:
|
181
145
|
result = urlparse(url)
|
182
146
|
if result.scheme == "https" or result.scheme == "http":
|
@@ -188,6 +152,10 @@ def is_url(url: str) -> bool:
|
|
188
152
|
|
189
153
|
|
190
154
|
def download_locustfile_from_url(url: str) -> str:
|
155
|
+
"""
|
156
|
+
Attempt to download and save locustfile from url.
|
157
|
+
Returns path to downloaded file.
|
158
|
+
"""
|
191
159
|
try:
|
192
160
|
response = requests.get(url)
|
193
161
|
# Check if response is valid python code
|
@@ -244,7 +212,7 @@ See documentation for more details, including how to set options using a file or
|
|
244
212
|
"-f",
|
245
213
|
"--locustfile",
|
246
214
|
metavar="<filename>",
|
247
|
-
default="locustfile",
|
215
|
+
default="locustfile.py",
|
248
216
|
help="The Python file or module that contains your test, e.g. 'my_test.py'. Accepts multiple comma-separated .py files, a package name/directory or a url to a remote locustfile. Defaults to 'locustfile'.",
|
249
217
|
env_var="LOCUST_LOCUSTFILE",
|
250
218
|
)
|
@@ -314,7 +282,7 @@ def parse_locustfile_option(args=None) -> list[str]:
|
|
314
282
|
parser
|
315
283
|
|
316
284
|
Returns:
|
317
|
-
|
285
|
+
parsed_paths (List): List of locustfile paths
|
318
286
|
"""
|
319
287
|
parser = get_empty_argument_parser(add_help=False)
|
320
288
|
parser.add_argument(
|
@@ -354,6 +322,10 @@ def parse_locustfile_option(args=None) -> list[str]:
|
|
354
322
|
|
355
323
|
options, _ = parser.parse_known_args(args=args)
|
356
324
|
|
325
|
+
if options.help or options.version:
|
326
|
+
# if --help or --version is specified we'll call parse_options which will print the help/version message
|
327
|
+
parse_options(args=args)
|
328
|
+
|
357
329
|
if options.locustfile == "-":
|
358
330
|
if not options.worker:
|
359
331
|
sys.stderr.write(
|
@@ -364,51 +336,21 @@ def parse_locustfile_option(args=None) -> list[str]:
|
|
364
336
|
filename = download_locustfile_from_master(options.master_host, options.master_port)
|
365
337
|
return [filename]
|
366
338
|
|
367
|
-
|
368
|
-
|
369
|
-
download_locustfile_from_url(f) if is_url(f.strip()) else f.strip() for f in options.locustfile.split(",")
|
370
|
-
]
|
339
|
+
locustfile_list = [f.strip() for f in options.locustfile.split(",")]
|
340
|
+
parsed_paths = parse_locustfile_paths(locustfile_list)
|
371
341
|
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
locustfile = None
|
342
|
+
if not parsed_paths:
|
343
|
+
note_about_file_endings = ""
|
344
|
+
user_friendly_locustfile_name = options.locustfile
|
376
345
|
|
377
|
-
if not
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
locustfiles = find_locustfiles(locustfile_as_list, is_directory=False)
|
386
|
-
locustfile = None
|
387
|
-
else:
|
388
|
-
# Is a single file
|
389
|
-
locustfile = find_locustfile(locustfile_as_list[0])
|
390
|
-
locustfiles = []
|
391
|
-
|
392
|
-
if not locustfile:
|
393
|
-
if options.help or options.version:
|
394
|
-
# if --help or --version is specified we'll call parse_options which will print the help/version message
|
395
|
-
parse_options(args=args)
|
396
|
-
note_about_file_endings = ""
|
397
|
-
user_friendly_locustfile_name = options.locustfile
|
398
|
-
if options.locustfile == "locustfile":
|
399
|
-
user_friendly_locustfile_name = "locustfile.py"
|
400
|
-
elif not options.locustfile.endswith(".py"):
|
401
|
-
note_about_file_endings = (
|
402
|
-
"Ensure your locustfile ends with '.py' or is a directory with locustfiles. "
|
403
|
-
)
|
404
|
-
sys.stderr.write(
|
405
|
-
f"Could not find '{user_friendly_locustfile_name}'. {note_about_file_endings}See --help for available options.\n"
|
406
|
-
)
|
407
|
-
sys.exit(1)
|
408
|
-
else:
|
409
|
-
locustfiles.append(locustfile)
|
410
|
-
|
411
|
-
return locustfiles
|
346
|
+
if not options.locustfile.endswith(".py"):
|
347
|
+
note_about_file_endings = "Ensure your locustfile ends with '.py' or is a directory with parsed_paths. "
|
348
|
+
sys.stderr.write(
|
349
|
+
f"Could not find '{user_friendly_locustfile_name}'. {note_about_file_endings}See --help for available options.\n"
|
350
|
+
)
|
351
|
+
sys.exit(1)
|
352
|
+
|
353
|
+
return parsed_paths
|
412
354
|
|
413
355
|
|
414
356
|
def setup_parser_arguments(parser):
|
@@ -857,34 +799,3 @@ def ui_extra_args_dict(args=None) -> dict[str, dict[str, Any]]:
|
|
857
799
|
}
|
858
800
|
|
859
801
|
return extra_args
|
860
|
-
|
861
|
-
|
862
|
-
def locustfile_is_directory(locustfiles: list[str]) -> bool:
|
863
|
-
"""
|
864
|
-
If a user passes in a locustfile without a file extension and there is a directory with the same name,
|
865
|
-
this function defaults to using the file and will raise a warning.
|
866
|
-
In this example, foobar.py will be used:
|
867
|
-
├── src/
|
868
|
-
│ ├── foobar.py
|
869
|
-
├── foobar/
|
870
|
-
│ ├── locustfile.py
|
871
|
-
|
872
|
-
locust -f foobar
|
873
|
-
"""
|
874
|
-
if len(locustfiles) > 1:
|
875
|
-
return False
|
876
|
-
|
877
|
-
locustfile = locustfiles[0]
|
878
|
-
|
879
|
-
# Checking if the locustfile could be both a file and a directory
|
880
|
-
if not locustfile.endswith(".py"):
|
881
|
-
if os.path.isfile(locustfile) and os.path.isdir(locustfile):
|
882
|
-
msg = f"WARNING: Using {locustfile}.py instead of directory {os.path.abspath(locustfile)}\n"
|
883
|
-
sys.stderr.write(msg)
|
884
|
-
|
885
|
-
return False
|
886
|
-
|
887
|
-
if os.path.isdir(locustfile):
|
888
|
-
return True
|
889
|
-
|
890
|
-
return False
|
locust/dispatch.py
CHANGED
@@ -6,11 +6,12 @@ import math
|
|
6
6
|
import time
|
7
7
|
from collections import defaultdict
|
8
8
|
from collections.abc import Generator, Iterator
|
9
|
+
from heapq import heapify, heapreplace
|
10
|
+
from math import log2
|
9
11
|
from operator import attrgetter
|
10
12
|
from typing import TYPE_CHECKING
|
11
13
|
|
12
14
|
import gevent
|
13
|
-
from roundrobin import smooth
|
14
15
|
|
15
16
|
if TYPE_CHECKING:
|
16
17
|
from locust import User
|
@@ -26,6 +27,33 @@ if TYPE_CHECKING:
|
|
26
27
|
# profile = line_profiler.LineProfiler()
|
27
28
|
|
28
29
|
|
30
|
+
def _kl_generator(users: list[tuple[type[User], float]]) -> Iterator[str | None]:
|
31
|
+
"""Generator based on Kullback-Leibler divergence
|
32
|
+
|
33
|
+
For example, given users A, B with weights 5 and 1 respectively,
|
34
|
+
this algorithm will yield AAABAAAAABAA.
|
35
|
+
"""
|
36
|
+
if not users:
|
37
|
+
while True:
|
38
|
+
yield None
|
39
|
+
|
40
|
+
names = [u[0].__name__ for u in users]
|
41
|
+
weights = [u[1] for u in users]
|
42
|
+
generated = weights.copy()
|
43
|
+
|
44
|
+
heap = [(x * log2(x / (x + 1.0)), i) for i, x in enumerate(generated)]
|
45
|
+
heapify(heap)
|
46
|
+
|
47
|
+
while True:
|
48
|
+
i = heap[0][1] # choose element which choosing minimizes divergence the most
|
49
|
+
yield names[i]
|
50
|
+
generated[i] += 1.0
|
51
|
+
x = generated[i]
|
52
|
+
kl_diff = weights[i] * log2(x / (x + 1.0))
|
53
|
+
# calculate how much choosing element i for (x + 1)th time decreases divergence
|
54
|
+
heapreplace(heap, (kl_diff, i))
|
55
|
+
|
56
|
+
|
29
57
|
class UsersDispatcher(Iterator):
|
30
58
|
"""
|
31
59
|
Iterator that dispatches the users to the workers.
|
@@ -319,9 +347,7 @@ class UsersDispatcher(Iterator):
|
|
319
347
|
|
320
348
|
def _distribute_users(
|
321
349
|
self, target_user_count: int
|
322
|
-
) -> tuple[
|
323
|
-
dict[str, dict[str, int]], Generator[str | None, None, None], itertools.cycle, list[tuple[WorkerNode, str]]
|
324
|
-
]:
|
350
|
+
) -> tuple[dict[str, dict[str, int]], Iterator[str | None], itertools.cycle, list[tuple[WorkerNode, str]]]:
|
325
351
|
"""
|
326
352
|
This function might take some time to complete if the `target_user_count` is a big number. A big number
|
327
353
|
is typically > 50 000. However, this function is only called if a worker is added or removed while a test
|
@@ -350,71 +376,11 @@ class UsersDispatcher(Iterator):
|
|
350
376
|
|
351
377
|
return users_on_workers, user_gen, worker_gen, active_users
|
352
378
|
|
353
|
-
def _user_gen(self) ->
|
354
|
-
"""
|
355
|
-
This method generates users according to their weights using
|
356
|
-
a smooth weighted round-robin algorithm implemented by https://github.com/linnik/roundrobin.
|
357
|
-
|
358
|
-
For example, given users A, B with weights 5 and 1 respectively, this algorithm
|
359
|
-
will yield AAABAAAAABAA. The smooth aspect of this algorithm is what makes it possible
|
360
|
-
to keep the distribution during ramp-up and ramp-down. If we were to use a normal
|
361
|
-
weighted round-robin algorithm, we'd get AAAAABAAAAAB which would make the distribution
|
362
|
-
less accurate during ramp-up/down.
|
363
|
-
"""
|
364
|
-
|
365
|
-
def infinite_cycle_gen(users: list[tuple[type[User], int]]) -> itertools.cycle:
|
366
|
-
if not users:
|
367
|
-
return itertools.cycle([None])
|
368
|
-
|
369
|
-
def _get_order_of_magnitude(n: float) -> int:
|
370
|
-
"""Get how many times we need to multiply `n` to get an integer-like number.
|
371
|
-
For example:
|
372
|
-
0.1 would return 10,
|
373
|
-
0.04 would return 100,
|
374
|
-
0.0007 would return 10000.
|
375
|
-
"""
|
376
|
-
if n <= 0:
|
377
|
-
raise ValueError("To get the order of magnitude, the number must be greater than 0.")
|
378
|
-
|
379
|
-
counter = 0
|
380
|
-
while n < 1:
|
381
|
-
n *= 10
|
382
|
-
counter += 1
|
383
|
-
return 10**counter
|
384
|
-
|
385
|
-
# Get maximum order of magnitude to "normalize the weights".
|
386
|
-
# "Normalizing the weights" is to multiply all weights by the same number so that
|
387
|
-
# they become integers. Then we can find the largest common divisor of all the
|
388
|
-
# weights, divide them by it and get the smallest possible numbers with the same
|
389
|
-
# ratio as the numbers originally had.
|
390
|
-
max_order_of_magnitude = _get_order_of_magnitude(min(abs(u[1]) for u in users))
|
391
|
-
weights = tuple(int(u[1] * max_order_of_magnitude) for u in users)
|
392
|
-
|
393
|
-
greatest_common_divisor = math.gcd(*weights)
|
394
|
-
normalized_values = [
|
395
|
-
(
|
396
|
-
user[0].__name__,
|
397
|
-
normalized_weight // greatest_common_divisor,
|
398
|
-
)
|
399
|
-
for user, normalized_weight in zip(users, weights)
|
400
|
-
]
|
401
|
-
generation_length_to_get_proper_distribution = sum(
|
402
|
-
normalized_val[1] for normalized_val in normalized_values
|
403
|
-
)
|
404
|
-
gen = smooth(normalized_values)
|
405
|
-
|
406
|
-
# Instead of calling `gen()` for each user, we cycle through a generator of fixed-length
|
407
|
-
# `generation_length_to_get_proper_distribution`. Doing so greatly improves performance because
|
408
|
-
# we only ever need to call `gen()` a relatively small number of times. The length of this generator
|
409
|
-
# is chosen as the sum of the normalized weights. So, for users A, B, C of weights 2, 5, 6, the length is
|
410
|
-
# 2 + 5 + 6 = 13 which would yield the distribution `CBACBCBCBCABC` that gets repeated over and over
|
411
|
-
# until the target user count is reached.
|
412
|
-
return itertools.cycle(gen() for _ in range(generation_length_to_get_proper_distribution))
|
413
|
-
|
379
|
+
def _user_gen(self) -> Iterator[str | None]:
|
414
380
|
fixed_users = {u.__name__: u for u in self._user_classes if u.fixed_count}
|
415
381
|
|
416
|
-
|
417
|
-
|
382
|
+
fixed_users_gen = _kl_generator([(u, u.fixed_count) for u in fixed_users.values()])
|
383
|
+
weighted_users_gen = _kl_generator([(u, u.weight) for u in self._user_classes if not u.fixed_count])
|
418
384
|
|
419
385
|
# Spawn users
|
420
386
|
while True:
|
@@ -423,7 +389,7 @@ class UsersDispatcher(Iterator):
|
|
423
389
|
current_fixed_users_count = {u: self._get_user_current_count(u) for u in fixed_users}
|
424
390
|
spawned_classes: set[str] = set()
|
425
391
|
while len(spawned_classes) != len(fixed_users):
|
426
|
-
user_name: str | None = next(
|
392
|
+
user_name: str | None = next(fixed_users_gen)
|
427
393
|
if not user_name:
|
428
394
|
break
|
429
395
|
|
@@ -439,7 +405,7 @@ class UsersDispatcher(Iterator):
|
|
439
405
|
else:
|
440
406
|
spawned_classes.add(user_name)
|
441
407
|
|
442
|
-
yield next(
|
408
|
+
yield next(weighted_users_gen)
|
443
409
|
|
444
410
|
@staticmethod
|
445
411
|
def _fast_users_on_workers_copy(users_on_workers: dict[str, dict[str, int]]) -> dict[str, dict[str, int]]:
|
locust/runners.py
CHANGED
@@ -1032,23 +1032,19 @@ class MasterRunner(DistributedRunner):
|
|
1032
1032
|
elif msg.type == "locustfile":
|
1033
1033
|
logging.debug("Worker requested locust file")
|
1034
1034
|
assert self.environment.parsed_options
|
1035
|
-
filename =
|
1036
|
-
"locustfile.py"
|
1037
|
-
if self.environment.parsed_options.locustfile == "locustfile"
|
1038
|
-
else self.environment.parsed_options.locustfile
|
1039
|
-
)
|
1035
|
+
filename = self.environment.parsed_options.locustfile
|
1040
1036
|
try:
|
1041
1037
|
with open(filename) as f:
|
1042
1038
|
file_contents = f.read()
|
1043
1039
|
except Exception as e:
|
1044
1040
|
logger.error(
|
1045
|
-
f"--locustfile must be a
|
1041
|
+
f"--locustfile must be a full path to a single locustfile for file distribution to work {e}"
|
1046
1042
|
)
|
1047
1043
|
self.send_message(
|
1048
1044
|
"locustfile",
|
1049
1045
|
client_id=client_id,
|
1050
1046
|
data={
|
1051
|
-
"error": f"locustfile
|
1047
|
+
"error": f"locustfile must be a full path to a single locustfile for file distribution to work (was '{filename}')"
|
1052
1048
|
},
|
1053
1049
|
)
|
1054
1050
|
else:
|
locust/test/test_dispatch.py
CHANGED
@@ -5,6 +5,7 @@ from locust.dispatch import UsersDispatcher
|
|
5
5
|
from locust.runners import WorkerNode
|
6
6
|
from locust.test.util import clear_all_functools_lru_cache
|
7
7
|
|
8
|
+
import math
|
8
9
|
import time
|
9
10
|
import unittest
|
10
11
|
from operator import attrgetter
|
@@ -3924,7 +3925,6 @@ class TestRampUpDifferentUsers(unittest.TestCase):
|
|
3924
3925
|
|
3925
3926
|
user_dispatcher.new_dispatch(target_user_count=21, spawn_rate=21, user_classes=[User1, User2, User3])
|
3926
3927
|
dispatched_users = next(user_dispatcher)
|
3927
|
-
print(dispatched_users)
|
3928
3928
|
self.assertDictEqual(
|
3929
3929
|
dispatched_users,
|
3930
3930
|
{
|
@@ -4123,6 +4123,40 @@ class TestRampUpDifferentUsers(unittest.TestCase):
|
|
4123
4123
|
self.assertEqual(_user_count_on_worker(dispatched_users, worker_nodes[2].id), 6)
|
4124
4124
|
|
4125
4125
|
|
4126
|
+
class TestFloatWeithts(unittest.TestCase):
|
4127
|
+
def test_float_weights(self):
|
4128
|
+
"""Final distribution should be {"User1": 3, "User2": 3, "User3": 3}"""
|
4129
|
+
|
4130
|
+
for ratio in (1, 1.0, 10, 2.5, 0.3, 1 / 23, math.e, math.pi):
|
4131
|
+
|
4132
|
+
class User1(User):
|
4133
|
+
weight = 1 * ratio
|
4134
|
+
|
4135
|
+
class User2(User):
|
4136
|
+
weight = 2 * ratio
|
4137
|
+
|
4138
|
+
class User3(User):
|
4139
|
+
weight = 3 * ratio
|
4140
|
+
|
4141
|
+
worker_node1 = WorkerNode("1")
|
4142
|
+
worker_node2 = WorkerNode("2")
|
4143
|
+
worker_node3 = WorkerNode("3")
|
4144
|
+
|
4145
|
+
sleep_time = 0 # Speed-up test
|
4146
|
+
|
4147
|
+
users_dispatcher = UsersDispatcher(
|
4148
|
+
worker_nodes=[worker_node1, worker_node2, worker_node3], user_classes=[User1, User2, User3]
|
4149
|
+
)
|
4150
|
+
users_dispatcher.new_dispatch(target_user_count=9, spawn_rate=0.5)
|
4151
|
+
users_dispatcher._wait_between_dispatch = sleep_time
|
4152
|
+
|
4153
|
+
if ratio == 1:
|
4154
|
+
reference = list(users_dispatcher)
|
4155
|
+
else:
|
4156
|
+
for x in reference:
|
4157
|
+
self.assertDictEqual(x, next(users_dispatcher))
|
4158
|
+
|
4159
|
+
|
4126
4160
|
def _aggregate_dispatched_users(d: dict[str, dict[str, int]]) -> dict[str, int]:
|
4127
4161
|
user_classes = list(next(iter(d.values())).keys())
|
4128
4162
|
return {u: sum(d[u] for d in d.values()) for u in user_classes}
|
locust/test/test_main.py
CHANGED
@@ -1026,6 +1026,32 @@ class StandaloneIntegrationTests(ProcessIntegrationTest):
|
|
1026
1026
|
self.assertIn("Shutting down (exit code 0)", output)
|
1027
1027
|
self.assertEqual(0, proc.returncode)
|
1028
1028
|
|
1029
|
+
def test_with_package_as_locustfile(self):
|
1030
|
+
with TemporaryDirectory() as temp_dir:
|
1031
|
+
with open(f"{temp_dir}/__init__.py", mode="w"):
|
1032
|
+
with mock_locustfile(dir=temp_dir):
|
1033
|
+
proc = subprocess.Popen(
|
1034
|
+
[
|
1035
|
+
"locust",
|
1036
|
+
"-f",
|
1037
|
+
temp_dir,
|
1038
|
+
"--headless",
|
1039
|
+
"--exit-code-on-error",
|
1040
|
+
"0",
|
1041
|
+
"--run-time",
|
1042
|
+
"2",
|
1043
|
+
],
|
1044
|
+
stdout=PIPE,
|
1045
|
+
stderr=PIPE,
|
1046
|
+
text=True,
|
1047
|
+
)
|
1048
|
+
stdout, stderr = proc.communicate()
|
1049
|
+
self.assertIn("Starting Locust", stderr)
|
1050
|
+
self.assertIn("All users spawned:", stderr)
|
1051
|
+
self.assertIn('"UserSubclass": 1', stderr)
|
1052
|
+
self.assertIn("Shutting down (exit code 0)", stderr)
|
1053
|
+
self.assertEqual(0, proc.returncode)
|
1054
|
+
|
1029
1055
|
def test_command_line_user_selection(self):
|
1030
1056
|
LOCUSTFILE_CONTENT = textwrap.dedent(
|
1031
1057
|
"""
|
@@ -1132,7 +1158,7 @@ class StandaloneIntegrationTests(ProcessIntegrationTest):
|
|
1132
1158
|
stderr=PIPE,
|
1133
1159
|
text=True,
|
1134
1160
|
)
|
1135
|
-
gevent.sleep(
|
1161
|
+
gevent.sleep(2)
|
1136
1162
|
proc.send_signal(signal.SIGTERM)
|
1137
1163
|
stdout, stderr = proc.communicate()
|
1138
1164
|
|
@@ -1801,34 +1827,49 @@ class SecondUser(HttpUser):
|
|
1801
1827
|
"""
|
1802
1828
|
)
|
1803
1829
|
with mock_locustfile(content=LOCUSTFILE_CONTENT) as mocked:
|
1804
|
-
|
1805
|
-
|
1806
|
-
|
1807
|
-
|
1808
|
-
|
1809
|
-
|
1810
|
-
|
1811
|
-
|
1812
|
-
|
1813
|
-
|
1814
|
-
|
1815
|
-
|
1816
|
-
|
1817
|
-
|
1818
|
-
|
1819
|
-
|
1820
|
-
|
1821
|
-
|
1822
|
-
|
1823
|
-
|
1824
|
-
|
1825
|
-
|
1826
|
-
|
1827
|
-
|
1828
|
-
|
1829
|
-
|
1830
|
-
|
1831
|
-
|
1830
|
+
with mock_locustfile() as mocked2:
|
1831
|
+
proc = subprocess.Popen(
|
1832
|
+
[
|
1833
|
+
"locust",
|
1834
|
+
"-f",
|
1835
|
+
f"{mocked.file_path}, {mocked2.file_path}",
|
1836
|
+
"--headless",
|
1837
|
+
"--master",
|
1838
|
+
"-L",
|
1839
|
+
"debug",
|
1840
|
+
],
|
1841
|
+
stderr=STDOUT,
|
1842
|
+
stdout=PIPE,
|
1843
|
+
text=True,
|
1844
|
+
)
|
1845
|
+
proc_worker = subprocess.Popen(
|
1846
|
+
[
|
1847
|
+
"locust",
|
1848
|
+
"-f",
|
1849
|
+
"-",
|
1850
|
+
"--worker",
|
1851
|
+
],
|
1852
|
+
stderr=STDOUT,
|
1853
|
+
stdout=PIPE,
|
1854
|
+
text=True,
|
1855
|
+
)
|
1856
|
+
|
1857
|
+
try:
|
1858
|
+
stdout = proc_worker.communicate(timeout=5)[0]
|
1859
|
+
self.assertIn(
|
1860
|
+
"Got error from master: locustfile must be a full path to a single locustfile for file distribution to work",
|
1861
|
+
stdout,
|
1862
|
+
)
|
1863
|
+
proc.kill()
|
1864
|
+
master_stdout = proc.communicate()[0]
|
1865
|
+
self.assertIn(
|
1866
|
+
"--locustfile must be a full path to a single locustfile for file distribution", master_stdout
|
1867
|
+
)
|
1868
|
+
except Exception:
|
1869
|
+
proc.kill()
|
1870
|
+
proc_worker.kill()
|
1871
|
+
stdout, worker_stderr = proc_worker.communicate()
|
1872
|
+
assert False, f"worker never finished: {stdout}"
|
1832
1873
|
|
1833
1874
|
def test_json_schema(self):
|
1834
1875
|
LOCUSTFILE_CONTENT = textwrap.dedent(
|
locust/test/test_parser.py
CHANGED
@@ -1,9 +1,8 @@
|
|
1
1
|
import locust
|
2
2
|
from locust.argument_parser import (
|
3
|
-
find_locustfiles,
|
4
3
|
get_parser,
|
5
|
-
locustfile_is_directory,
|
6
4
|
parse_locustfile_option,
|
5
|
+
parse_locustfile_paths,
|
7
6
|
parse_options,
|
8
7
|
ui_extra_args_dict,
|
9
8
|
)
|
@@ -91,6 +90,7 @@ class TestArgumentParser(LocustTestCase):
|
|
91
90
|
super().setUp()
|
92
91
|
self.parent_dir = TemporaryDirectory()
|
93
92
|
self.child_dir = TemporaryDirectory(dir=self.parent_dir.name)
|
93
|
+
self.child_dir2 = TemporaryDirectory(dir=self.parent_dir.name)
|
94
94
|
|
95
95
|
def tearDown(self):
|
96
96
|
super().tearDown()
|
@@ -257,6 +257,33 @@ class TestArgumentParser(LocustTestCase):
|
|
257
257
|
]
|
258
258
|
)
|
259
259
|
|
260
|
+
def test_parse_locustfile_and_directory(self):
|
261
|
+
with mock_locustfile(filename_prefix="mock_locustfile1", dir=self.parent_dir.name) as mock_locustfile1:
|
262
|
+
with mock_locustfile(filename_prefix="mock_locustfile2", dir=self.parent_dir.name) as mock_locustfile2:
|
263
|
+
with mock_locustfile(filename_prefix="mock_locustfile3", dir=self.child_dir.name) as mock_locustfile3:
|
264
|
+
locustfiles = parse_locustfile_option(
|
265
|
+
args=[
|
266
|
+
"-f",
|
267
|
+
f"{mock_locustfile1.file_path},{self.child_dir.name}",
|
268
|
+
]
|
269
|
+
)
|
270
|
+
self.assertIn(mock_locustfile1.file_path, locustfiles)
|
271
|
+
self.assertNotIn(mock_locustfile2.file_path, locustfiles)
|
272
|
+
self.assertIn(mock_locustfile3.file_path, locustfiles)
|
273
|
+
|
274
|
+
def test_parse_multiple_directories(self):
|
275
|
+
with mock_locustfile(filename_prefix="mock_locustfile1", dir=self.child_dir.name) as mock_locustfile1:
|
276
|
+
with mock_locustfile(filename_prefix="mock_locustfile2", dir=self.child_dir2.name) as mock_locustfile2:
|
277
|
+
locustfiles = parse_locustfile_option(
|
278
|
+
args=[
|
279
|
+
"-f",
|
280
|
+
f"{self.child_dir.name},{self.child_dir2.name}",
|
281
|
+
]
|
282
|
+
)
|
283
|
+
|
284
|
+
self.assertIn(mock_locustfile1.file_path, locustfiles)
|
285
|
+
self.assertIn(mock_locustfile2.file_path, locustfiles)
|
286
|
+
|
260
287
|
def test_parse_locustfile_invalid_directory_error(self):
|
261
288
|
with mock.patch("sys.stderr", new=StringIO()):
|
262
289
|
with self.assertRaises(SystemExit):
|
@@ -377,7 +404,7 @@ class TestFindLocustfiles(LocustTestCase):
|
|
377
404
|
with mock_locustfile(dir=self.parent_dir1.name) as mocked1:
|
378
405
|
with mock_locustfile(dir=self.child_dir.name) as mocked2:
|
379
406
|
with mock_locustfile(dir=self.child_dir.name) as mocked3:
|
380
|
-
locustfiles =
|
407
|
+
locustfiles = parse_locustfile_paths([self.parent_dir1.name])
|
381
408
|
|
382
409
|
self.assertIn(mocked1.file_path, locustfiles)
|
383
410
|
self.assertIn(mocked2.file_path, locustfiles)
|
@@ -387,13 +414,13 @@ class TestFindLocustfiles(LocustTestCase):
|
|
387
414
|
def test_find_locustfiles_error_if_directory_doesnt_exist(self):
|
388
415
|
with mock.patch("sys.stderr", new=StringIO()):
|
389
416
|
with self.assertRaises(SystemExit):
|
390
|
-
|
417
|
+
parse_locustfile_paths(["some_directory"])
|
391
418
|
|
392
419
|
def test_find_locustfiles_ignores_invalid_files_in_directory(self):
|
393
420
|
with NamedTemporaryFile(suffix=".py", prefix="_", dir=self.parent_dir1.name) as invalid_file1:
|
394
421
|
with NamedTemporaryFile(suffix=".txt", prefix="", dir=self.parent_dir1.name) as invalid_file2:
|
395
422
|
with mock_locustfile(filename_prefix="mock_locustfile1", dir=self.parent_dir1.name) as mock_locustfile1:
|
396
|
-
locustfiles =
|
423
|
+
locustfiles = parse_locustfile_paths([self.parent_dir1.name])
|
397
424
|
|
398
425
|
self.assertIn(mock_locustfile1.file_path, locustfiles)
|
399
426
|
self.assertNotIn(invalid_file1.name, locustfiles)
|
@@ -403,7 +430,7 @@ class TestFindLocustfiles(LocustTestCase):
|
|
403
430
|
def test_find_locustfiles_with_multiple_locustfiles(self):
|
404
431
|
with mock_locustfile() as mocked1:
|
405
432
|
with mock_locustfile() as mocked2:
|
406
|
-
locustfiles =
|
433
|
+
locustfiles = parse_locustfile_paths([mocked1.file_path, mocked2.file_path])
|
407
434
|
|
408
435
|
self.assertIn(mocked1.file_path, locustfiles)
|
409
436
|
self.assertIn(mocked2.file_path, locustfiles)
|
@@ -415,58 +442,9 @@ class TestFindLocustfiles(LocustTestCase):
|
|
415
442
|
with mock_locustfile() as valid_file:
|
416
443
|
with self.assertRaises(SystemExit):
|
417
444
|
invalid_file = NamedTemporaryFile(suffix=".txt")
|
418
|
-
|
419
|
-
|
420
|
-
def test_find_locustfiles_error_if_invalid_directory(self):
|
421
|
-
with mock.patch("sys.stderr", new=StringIO()):
|
422
|
-
with mock_locustfile() as valid_file:
|
423
|
-
with self.assertRaises(SystemExit):
|
424
|
-
find_locustfiles([valid_file.file_path], True)
|
445
|
+
parse_locustfile_paths([valid_file.file_path, invalid_file.name])
|
425
446
|
|
426
447
|
def test_find_locustfiles_error_if_multiple_values_for_directory(self):
|
427
448
|
with mock.patch("sys.stderr", new=StringIO()):
|
428
449
|
with self.assertRaises(SystemExit):
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
class TestLocustfileIsDirectory(LocustTestCase):
|
433
|
-
def setUp(self):
|
434
|
-
super().setUp()
|
435
|
-
self.random_prefix = "locust/test/foobar_" + str(randint(1000, 9999))
|
436
|
-
self.mock_filename = self.random_prefix + ".py"
|
437
|
-
|
438
|
-
self.mock_locustfile = open(self.mock_filename, "w")
|
439
|
-
self.mock_locustfile.close()
|
440
|
-
self.mock_dir = os.mkdir(self.random_prefix)
|
441
|
-
|
442
|
-
def tearDown(self):
|
443
|
-
super().tearDown()
|
444
|
-
os.remove(self.mock_filename)
|
445
|
-
os.rmdir(self.random_prefix)
|
446
|
-
|
447
|
-
def test_locustfile_is_directory_single_locustfile(self):
|
448
|
-
with mock_locustfile() as mocked:
|
449
|
-
is_dir = locustfile_is_directory([mocked.file_path])
|
450
|
-
assert not is_dir
|
451
|
-
|
452
|
-
def test_locustfile_is_directory_single_locustfile_without_file_extension(self):
|
453
|
-
prefix_name = "foobar"
|
454
|
-
with NamedTemporaryFile(prefix=prefix_name, suffix=".py"):
|
455
|
-
is_dir = locustfile_is_directory([prefix_name])
|
456
|
-
assert not is_dir
|
457
|
-
|
458
|
-
def test_locustfile_is_directory_multiple_locustfiles(self):
|
459
|
-
with mock_locustfile() as mocked1:
|
460
|
-
with mock_locustfile() as mocked2:
|
461
|
-
is_dir = locustfile_is_directory([mocked1.file_path, mocked2.file_path])
|
462
|
-
assert not is_dir
|
463
|
-
|
464
|
-
def test_locustfile_is_directory_true_if_directory(self):
|
465
|
-
with TemporaryDirectory() as mocked_dir:
|
466
|
-
is_dir = locustfile_is_directory([mocked_dir])
|
467
|
-
assert is_dir
|
468
|
-
|
469
|
-
def test_locustfile_is_directory_false_if_file_and_directory_share_the_same_name(self):
|
470
|
-
"""See locustfile_is_directory docstring of an example of this usecase"""
|
471
|
-
is_dir = locustfile_is_directory([self.random_prefix, self.mock_filename])
|
472
|
-
assert not is_dir
|
450
|
+
parse_locustfile_paths([self.parent_dir1.name, self.parent_dir2.name])
|
locust/user/users.py
CHANGED
@@ -1,21 +1,8 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
-
import logging
|
4
|
-
import time
|
5
|
-
import traceback
|
6
|
-
from typing import Callable, final
|
7
|
-
|
8
|
-
from gevent import GreenletExit, greenlet
|
9
|
-
from gevent.pool import Group
|
10
|
-
from urllib3 import PoolManager
|
11
|
-
|
12
|
-
logger = logging.getLogger(__name__)
|
13
3
|
from locust.clients import HttpSession
|
14
4
|
from locust.exception import LocustError, StopUser
|
15
|
-
from locust.user.
|
16
|
-
from locust.util import deprecation
|
17
|
-
|
18
|
-
from .task import (
|
5
|
+
from locust.user.task import (
|
19
6
|
LOCUST_STATE_RUNNING,
|
20
7
|
LOCUST_STATE_STOPPING,
|
21
8
|
LOCUST_STATE_WAITING,
|
@@ -23,6 +10,17 @@ from .task import (
|
|
23
10
|
TaskSet,
|
24
11
|
get_tasks_from_base_classes,
|
25
12
|
)
|
13
|
+
from locust.user.wait_time import constant
|
14
|
+
from locust.util import deprecation
|
15
|
+
|
16
|
+
import logging
|
17
|
+
import time
|
18
|
+
import traceback
|
19
|
+
from typing import Callable, final
|
20
|
+
|
21
|
+
from gevent import GreenletExit, greenlet
|
22
|
+
from gevent.pool import Group
|
23
|
+
from urllib3 import PoolManager
|
26
24
|
|
27
25
|
logger = logging.getLogger(__name__)
|
28
26
|
|
@@ -105,27 +103,27 @@ class User(metaclass=UserMeta):
|
|
105
103
|
tasks = {ThreadPage:15, write_post:1}
|
106
104
|
"""
|
107
105
|
|
108
|
-
weight = 1
|
106
|
+
weight: float = 1
|
109
107
|
"""Probability of user class being chosen. The higher the weight, the greater the chance of it being chosen."""
|
110
108
|
|
111
|
-
fixed_count = 0
|
109
|
+
fixed_count: int = 0
|
112
110
|
"""
|
113
111
|
If the value > 0, the weight property will be ignored and the 'fixed_count'-instances will be spawned.
|
114
112
|
These Users are spawned first. If the total target count (specified by the --users arg) is not enough
|
115
113
|
to spawn all instances of each User class with the defined property, the final count of each User is undefined.
|
116
114
|
"""
|
117
115
|
|
118
|
-
abstract = True
|
116
|
+
abstract: bool = True
|
119
117
|
"""If abstract is True, the class is meant to be subclassed, and locust will not spawn users of this class during a test."""
|
120
118
|
|
121
|
-
def __init__(self, environment):
|
119
|
+
def __init__(self, environment) -> None:
|
122
120
|
super().__init__()
|
123
121
|
self.environment = environment
|
124
122
|
"""A reference to the :py:class:`Environment <locust.env.Environment>` in which this user is running"""
|
125
|
-
self._state = None
|
126
|
-
self._greenlet: greenlet.Greenlet = None
|
123
|
+
self._state: str | None = None
|
124
|
+
self._greenlet: greenlet.Greenlet | None = None
|
127
125
|
self._group: Group
|
128
|
-
self._taskset_instance: TaskSet = None
|
126
|
+
self._taskset_instance: TaskSet | None = None
|
129
127
|
self._cp_last_run = time.time() # used by constant_pacing wait_time
|
130
128
|
|
131
129
|
def on_start(self) -> None:
|
@@ -191,7 +189,7 @@ class User(metaclass=UserMeta):
|
|
191
189
|
self._group = group
|
192
190
|
return self._greenlet
|
193
191
|
|
194
|
-
def stop(self, force=False):
|
192
|
+
def stop(self, force: bool = False):
|
195
193
|
"""
|
196
194
|
Stop the user greenlet.
|
197
195
|
|
@@ -251,7 +249,7 @@ class HttpUser(User):
|
|
251
249
|
for keeping a user session between requests.
|
252
250
|
"""
|
253
251
|
|
254
|
-
abstract = True
|
252
|
+
abstract: bool = True
|
255
253
|
"""If abstract is True, the class is meant to be subclassed, and users will not choose this locust during a test"""
|
256
254
|
|
257
255
|
pool_manager: PoolManager | None = None
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: locust
|
3
|
-
Version: 2.26.1.
|
3
|
+
Version: 2.26.1.dev61
|
4
4
|
Summary: Developer friendly load testing framework
|
5
5
|
License: MIT
|
6
6
|
Project-URL: Homepage, https://github.com/locustio/locust
|
@@ -37,7 +37,6 @@ Requires-Dist: ConfigArgParse >=1.5.5
|
|
37
37
|
Requires-Dist: psutil >=5.9.1
|
38
38
|
Requires-Dist: Flask-Login >=0.6.3
|
39
39
|
Requires-Dist: Flask-Cors >=3.0.10
|
40
|
-
Requires-Dist: roundrobin >=0.0.2
|
41
40
|
Requires-Dist: pywin32 ; platform_system == "Windows"
|
42
41
|
Requires-Dist: tomli >=1.1.0 ; python_version < "3.11"
|
43
42
|
|
@@ -1,10 +1,10 @@
|
|
1
1
|
locust/__init__.py,sha256=g6oA-Ba_hs3gLWVf5MKJ1mvfltI8MFnDWG8qslqm8yg,1402
|
2
2
|
locust/__main__.py,sha256=vBQ82334kX06ImDbFlPFgiBRiLIinwNk3z8Khs6hd74,31
|
3
|
-
locust/_version.py,sha256=
|
4
|
-
locust/argument_parser.py,sha256=
|
3
|
+
locust/_version.py,sha256=LEYNsmYxTmpPGy-8xPWBC-TOuPW8IR_1QcZb3AZjrPE,428
|
4
|
+
locust/argument_parser.py,sha256=izMXLuMZWUpS6m8SrGRmOjLfPPuYWXCvQFicRmn-a90,28774
|
5
5
|
locust/clients.py,sha256=YKuAyMAbxs8_-w7XJw0hc67KFBNNLxibsw6FwiS01Q8,14781
|
6
6
|
locust/debug.py,sha256=We6Z9W0btkKSc7PxWmrZx-xMynvOOsKhG6jmDgQin0g,5134
|
7
|
-
locust/dispatch.py,sha256=
|
7
|
+
locust/dispatch.py,sha256=vYh0QEDFgJ3hY0HgSk-EiNO7IP9ffzXF_Et8wB9JvsI,16995
|
8
8
|
locust/env.py,sha256=nd6ui1bv6n-kkLkP3r61ZkskDY627dsKOAkYHhtOW7o,12472
|
9
9
|
locust/event.py,sha256=xgNKbcejxy1TNUfIdgV75KgD2_BOwQmvjrJ4hWuydRw,7740
|
10
10
|
locust/exception.py,sha256=jGgJ32ubuf4pWdlaVOkbh2Y0LlG0_DHi-lv3ib8ppOE,1791
|
@@ -13,7 +13,7 @@ locust/input_events.py,sha256=VQIrgXaoph3JgLo6REKtPBThEPUXYXG5Kcedly5aRYc,3272
|
|
13
13
|
locust/log.py,sha256=2IVp9YL4ZPfWdj3sBFuOHfgneg3g7m7tUGR-sy2s3E8,3155
|
14
14
|
locust/main.py,sha256=Un9THvATWEOewIeeoLkecxBluxQcoC7BkFhmk_UgTxM,28150
|
15
15
|
locust/py.typed,sha256=gkWLl8yD4mIZnNYYAIRM8g9VarLvWmTAFeUfEbxJLBw,65
|
16
|
-
locust/runners.py,sha256=
|
16
|
+
locust/runners.py,sha256=MGgzDU6_hhZ58Myn6bAWY92NQJKuwV9NyP4B9SdN9ek,67797
|
17
17
|
locust/shape.py,sha256=t-lwBS8LOjWcKXNL7j2U3zroIXJ1b0fazUwpRYQOKXw,1973
|
18
18
|
locust/stats.py,sha256=l2cxxVre8dvA4MIOD_ZKNj_fYySz5gTGC2f9Rc4-CL0,46134
|
19
19
|
locust/web.py,sha256=zj0Lm3tQq0MhbeJ1oKROnvauNibwUSBXsTehw2q2nAA,28251
|
@@ -53,7 +53,7 @@ locust/test/fake_module2_for_env_test.py,sha256=dzGYWCr1SSkd8Yyo68paUNrCNW7YY_Qg
|
|
53
53
|
locust/test/mock_locustfile.py,sha256=4xgoAYlhvdIBjGsLFFN0abpTNM7k12iSkrfTPUQhAMQ,1271
|
54
54
|
locust/test/mock_logging.py,sha256=qapKrKhTdlVc8foJB2Hxjn7SB6soaLeAj3VF4A6kZtw,806
|
55
55
|
locust/test/test_debugging.py,sha256=omQ0w5_Xh1xuTBzkd3VavEIircwtlmoOEHcMInY67vU,1053
|
56
|
-
locust/test/test_dispatch.py,sha256=
|
56
|
+
locust/test/test_dispatch.py,sha256=RjoncanN4FFt-aiTl4G8XRoc81n6fwfO8CacbjzpvP8,168856
|
57
57
|
locust/test/test_env.py,sha256=l0fLl9nubdgzxwFNajmBkJvQc5cO5rOTE4p12lbCbs0,8919
|
58
58
|
locust/test/test_fasthttp.py,sha256=jVA5wWjZxXYW6emzy-lfPC0AOabzT6rDCX0N7DPP9mc,30727
|
59
59
|
locust/test/test_http.py,sha256=VQCVY0inLC0RS-V3E9WHL3vBLGokZjQt0zKSrTNlQmM,12536
|
@@ -61,9 +61,9 @@ locust/test/test_interruptable_task.py,sha256=LZKSV-aJNnwfvAxguz6SckBEuGEnfGimoI
|
|
61
61
|
locust/test/test_load_locustfile.py,sha256=v-muHoM-CYu8t7DXm4AQtFP2q8RYfnTTUBqj7uVqhig,8494
|
62
62
|
locust/test/test_locust_class.py,sha256=oGhhOX848jHRQnIfFlhLlW-kHGYLyYsfDX8hM07Ro7g,25506
|
63
63
|
locust/test/test_log.py,sha256=YPY6vgTAy1KaNU2qoVvQrTH5x_mzRrljEHrkSBy3yxs,7553
|
64
|
-
locust/test/test_main.py,sha256=
|
64
|
+
locust/test/test_main.py,sha256=Ae3F8KTj65YakX_S8eckhJNbqKrRsivoPkFoZaHeyWI,85344
|
65
65
|
locust/test/test_old_wait_api.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
66
|
-
locust/test/test_parser.py,sha256
|
66
|
+
locust/test/test_parser.py,sha256=-2VO5Dopg-VoWvIgXrmr7GN40cqrnjUoctBHmVlyewg,17826
|
67
67
|
locust/test/test_runners.py,sha256=6FPd-3Glp5_xtVIE8yCHDonM0aYJ0A6He5KSxuWOk34,159381
|
68
68
|
locust/test/test_sequential_taskset.py,sha256=QjVMWWfGHn9hU5AvPxRDU7Vo5DcVW1VkMVfDA0k9OPE,3398
|
69
69
|
locust/test/test_stats.py,sha256=F51VkL3k3y4OhYBlRyV6vWzisenSAOmSWKy2IPVrnWM,33929
|
@@ -80,7 +80,7 @@ locust/user/__init__.py,sha256=S2yvmI_AU9kXirtTIVqiV_Hs7yXzqXvaSgkNo9ig-fk,71
|
|
80
80
|
locust/user/inspectuser.py,sha256=KgrWHyE5jhK6or58R7soLRf-_st42AaQrR72qbiXw9E,2641
|
81
81
|
locust/user/sequential_taskset.py,sha256=E8yykSZBO-QMcza1frr-7l8Cv_5bbSpjRO6sbkmGpZE,2544
|
82
82
|
locust/user/task.py,sha256=JvVVCQ1_UQSsahqaEZoFCD-cBXlOJLJ51ewXHNesSAI,16700
|
83
|
-
locust/user/users.py,sha256=
|
83
|
+
locust/user/users.py,sha256=BsKyxzLq1lmdYBXnwHNyMH_Nxfy7QRlyLnfiE8539HY,9989
|
84
84
|
locust/user/wait_time.py,sha256=bGRKMVx4lom75sX3POYJUa1CPeME2bEAXG6CEgxSO5U,2675
|
85
85
|
locust/util/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
86
86
|
locust/util/cache.py,sha256=IxbpGawl0-hoWKvCrtksxjSLf2GbBBTVns06F7mFBkM,1062
|
@@ -95,9 +95,9 @@ locust/webui/dist/report.html,sha256=sOdZZVgZbqgu86BBCSQf3uQUYXgmgSnXF32JpnyAII8
|
|
95
95
|
locust/webui/dist/assets/favicon.ico,sha256=IUl-rYqfpHdV38e-s0bkmFIeLS-n3Ug0DQxk-h202hI,8348
|
96
96
|
locust/webui/dist/assets/index-941b6e82.js,sha256=G3n5R81Svt0HzbWaV3AV20jLWGLr4X50UZ-Adu2KcxU,1645614
|
97
97
|
locust/webui/dist/assets/logo.png,sha256=EIVPqr6wE_yqguHaqFHIsH0ZACLSrvNWyYO7PbyIj4w,19299
|
98
|
-
locust-2.26.1.
|
99
|
-
locust-2.26.1.
|
100
|
-
locust-2.26.1.
|
101
|
-
locust-2.26.1.
|
102
|
-
locust-2.26.1.
|
103
|
-
locust-2.26.1.
|
98
|
+
locust-2.26.1.dev61.dist-info/LICENSE,sha256=78XGpIn3fHVBfaxlPNUfjVufSN7QsdhpJMRJHv2AFpo,1095
|
99
|
+
locust-2.26.1.dev61.dist-info/METADATA,sha256=iKcIMdLVlX5wXgc8qsYQMxWjMh5C0tLwzuSRnPSXsxA,7267
|
100
|
+
locust-2.26.1.dev61.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
101
|
+
locust-2.26.1.dev61.dist-info/entry_points.txt,sha256=RAdt8Ku-56m7bFjmdj-MBhbF6h4NX7tVODR9QNnOg0E,44
|
102
|
+
locust-2.26.1.dev61.dist-info/top_level.txt,sha256=XSsjgPA8Ggf9TqKVbkwSqZFuPlZ085X13M9orDycE20,7
|
103
|
+
locust-2.26.1.dev61.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|