locust 2.22.1.dev45__py3-none-any.whl → 2.22.1.dev67__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 +79 -1
- locust/runners.py +34 -0
- locust/test/test_main.py +177 -8
- locust/webui/dist/assets/{index-e80c0bba.js → index-5730ea01.js} +48 -49
- locust/webui/dist/auth.html +1 -1
- locust/webui/dist/index.html +1 -1
- {locust-2.22.1.dev45.dist-info → locust-2.22.1.dev67.dist-info}/METADATA +1 -1
- {locust-2.22.1.dev45.dist-info → locust-2.22.1.dev67.dist-info}/RECORD +13 -15
- locust/webui/dist/assets/auth-0ef39448.js.map +0 -1
- locust/webui/dist/assets/index-e80c0bba.js.map +0 -1
- {locust-2.22.1.dev45.dist-info → locust-2.22.1.dev67.dist-info}/LICENSE +0 -0
- {locust-2.22.1.dev45.dist-info → locust-2.22.1.dev67.dist-info}/WHEEL +0 -0
- {locust-2.22.1.dev45.dist-info → locust-2.22.1.dev67.dist-info}/entry_points.txt +0 -0
- {locust-2.22.1.dev45.dist-info → locust-2.22.1.dev67.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.22.1.
|
16
|
-
__version_tuple__ = version_tuple = (2, 22, 1, '
|
15
|
+
__version__ = version = '2.22.1.dev67'
|
16
|
+
__version_tuple__ = version_tuple = (2, 22, 1, 'dev67')
|
locust/argument_parser.py
CHANGED
@@ -1,14 +1,20 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
3
|
import locust
|
4
|
+
from locust import runners
|
5
|
+
from locust.rpc import Message, zmqrpc
|
4
6
|
|
5
7
|
import os
|
6
8
|
import platform
|
9
|
+
import socket
|
7
10
|
import sys
|
8
11
|
import textwrap
|
9
12
|
from typing import Any, NamedTuple
|
13
|
+
from uuid import uuid4
|
10
14
|
|
11
15
|
import configargparse
|
16
|
+
import gevent
|
17
|
+
from gevent.event import Event
|
12
18
|
|
13
19
|
version = locust.__version__
|
14
20
|
|
@@ -175,6 +181,46 @@ See documentation for more details, including how to set options using a file or
|
|
175
181
|
return parser
|
176
182
|
|
177
183
|
|
184
|
+
def download_locustfile_from_master(master_host: str, master_port: int) -> str:
|
185
|
+
client_id = socket.gethostname() + "_download_locustfile_" + uuid4().hex
|
186
|
+
tempclient = zmqrpc.Client(master_host, master_port, client_id)
|
187
|
+
got_reply = False
|
188
|
+
|
189
|
+
def ask_for_locustfile():
|
190
|
+
while not got_reply:
|
191
|
+
tempclient.send(Message("locustfile", None, client_id))
|
192
|
+
gevent.sleep(1)
|
193
|
+
|
194
|
+
def wait_for_reply():
|
195
|
+
return tempclient.recv()
|
196
|
+
|
197
|
+
gevent.spawn(ask_for_locustfile)
|
198
|
+
try:
|
199
|
+
# wait same time as for client_ready ack. not that it is really relevant...
|
200
|
+
msg = gevent.spawn(wait_for_reply).get(timeout=runners.CONNECT_TIMEOUT * runners.CONNECT_RETRY_COUNT)
|
201
|
+
got_reply = True
|
202
|
+
except gevent.Timeout:
|
203
|
+
sys.stderr.write(
|
204
|
+
f"Got no locustfile response from master, gave up after {runners.CONNECT_TIMEOUT * runners.CONNECT_RETRY_COUNT}s\n"
|
205
|
+
)
|
206
|
+
sys.exit(1)
|
207
|
+
|
208
|
+
if msg.type != "locustfile":
|
209
|
+
sys.stderr.write(f"Got wrong message type from master {msg.type}\n")
|
210
|
+
sys.exit(1)
|
211
|
+
|
212
|
+
if "error" in msg.data:
|
213
|
+
sys.stderr.write(f"Got error from master: {msg.data['error']}\n")
|
214
|
+
sys.exit(1)
|
215
|
+
|
216
|
+
filename = msg.data["filename"]
|
217
|
+
with open(filename, "w") as local_file:
|
218
|
+
local_file.write(msg.data["contents"])
|
219
|
+
|
220
|
+
tempclient.close()
|
221
|
+
return filename
|
222
|
+
|
223
|
+
|
178
224
|
def parse_locustfile_option(args=None) -> list[str]:
|
179
225
|
"""
|
180
226
|
Construct a command line parser that is only used to parse the -f argument so that we can
|
@@ -197,9 +243,41 @@ def parse_locustfile_option(args=None) -> list[str]:
|
|
197
243
|
action="store_true",
|
198
244
|
default=False,
|
199
245
|
)
|
246
|
+
# the following arguments are only used for downloading the locustfile from master
|
247
|
+
parser.add_argument(
|
248
|
+
"--worker",
|
249
|
+
action="store_true",
|
250
|
+
env_var="LOCUST_MODE_WORKER",
|
251
|
+
)
|
252
|
+
parser.add_argument(
|
253
|
+
"--master", # this is just here to prevent argparse from giving the dreaded "ambiguous option: --master could match --master-host, --master-port"
|
254
|
+
action="store_true",
|
255
|
+
env_var="LOCUST_MODE_MASTER",
|
256
|
+
)
|
257
|
+
parser.add_argument(
|
258
|
+
"--master-host",
|
259
|
+
default="127.0.0.1",
|
260
|
+
env_var="LOCUST_MASTER_NODE_HOST",
|
261
|
+
)
|
262
|
+
parser.add_argument(
|
263
|
+
"--master-port",
|
264
|
+
type=int,
|
265
|
+
default=5557,
|
266
|
+
env_var="LOCUST_MASTER_NODE_PORT",
|
267
|
+
)
|
200
268
|
|
201
269
|
options, _ = parser.parse_known_args(args=args)
|
202
270
|
|
271
|
+
if options.locustfile == "-":
|
272
|
+
if not options.worker:
|
273
|
+
sys.stderr.write(
|
274
|
+
"locustfile was set to '-' (meaning to download from master) but --worker was not specified.\n"
|
275
|
+
)
|
276
|
+
sys.exit(1)
|
277
|
+
# having this in argument_parser module is a bit weird, but it needs to be done early
|
278
|
+
filename = download_locustfile_from_master(options.master_host, options.master_port)
|
279
|
+
return [filename]
|
280
|
+
|
203
281
|
# Comma separated string to list
|
204
282
|
locustfile_as_list = [locustfile.strip() for locustfile in options.locustfile.split(",")]
|
205
283
|
|
@@ -457,7 +535,7 @@ Typically ONLY these options (and --locustfile) need to be specified on workers,
|
|
457
535
|
worker_group.add_argument(
|
458
536
|
"--worker",
|
459
537
|
action="store_true",
|
460
|
-
help="Set locust to run in distributed mode with this process as worker",
|
538
|
+
help="Set locust to run in distributed mode with this process as worker. Can be combined with setting --locustfile to '-' to download it from master.",
|
461
539
|
env_var="LOCUST_MODE_WORKER",
|
462
540
|
)
|
463
541
|
worker_group.add_argument(
|
locust/runners.py
CHANGED
@@ -1033,6 +1033,40 @@ class MasterRunner(DistributedRunner):
|
|
1033
1033
|
# emit a warning if the worker's clock seem to be out of sync with our clock
|
1034
1034
|
# if abs(time() - msg.data["time"]) > 5.0:
|
1035
1035
|
# warnings.warn("The worker node's clock seem to be out of sync. For the statistics to be correct the different locust servers need to have synchronized clocks.")
|
1036
|
+
elif msg.type == "locustfile":
|
1037
|
+
logging.debug("Worker requested locust file")
|
1038
|
+
assert self.environment.parsed_options
|
1039
|
+
filename = (
|
1040
|
+
"locustfile.py"
|
1041
|
+
if self.environment.parsed_options.locustfile == "locustfile"
|
1042
|
+
else self.environment.parsed_options.locustfile
|
1043
|
+
)
|
1044
|
+
try:
|
1045
|
+
with open(filename) as f:
|
1046
|
+
file_contents = f.read()
|
1047
|
+
except Exception as e:
|
1048
|
+
logger.error(
|
1049
|
+
f"--locustfile must be a plain filename (not a module name) for file distribution to work {e}"
|
1050
|
+
)
|
1051
|
+
self.send_message(
|
1052
|
+
"locustfile",
|
1053
|
+
client_id=client_id,
|
1054
|
+
data={
|
1055
|
+
"error": f"locustfile parameter on master must be a plain filename (not a module name) (was '{filename}')"
|
1056
|
+
},
|
1057
|
+
)
|
1058
|
+
else:
|
1059
|
+
if getattr(self, "_old_file_contents", file_contents) != file_contents:
|
1060
|
+
logger.warning(
|
1061
|
+
"Locustfile contents changed on disk after first worker requested locustfile, sending new content. If you make any major changes (like changing User class names) you need to restart master."
|
1062
|
+
)
|
1063
|
+
self._old_file_contents = file_contents
|
1064
|
+
self.send_message(
|
1065
|
+
"locustfile",
|
1066
|
+
client_id=client_id,
|
1067
|
+
data={"filename": filename, "contents": file_contents},
|
1068
|
+
)
|
1069
|
+
continue
|
1036
1070
|
elif msg.type == "client_stopped":
|
1037
1071
|
if msg.node_id not in self.clients:
|
1038
1072
|
logger.warning(f"Received {msg.type} message from an unknown worker: {msg.node_id}.")
|
locust/test/test_main.py
CHANGED
@@ -21,6 +21,8 @@ from pyquery import PyQuery as pq
|
|
21
21
|
from .mock_locustfile import MOCK_LOCUSTFILE_CONTENT, mock_locustfile
|
22
22
|
from .util import get_free_tcp_port, patch_env, temporary_file
|
23
23
|
|
24
|
+
SHORT_SLEEP = 2 if sys.platform == "darwin" else 1 # macOS is slow on GH, give it some extra time
|
25
|
+
|
24
26
|
|
25
27
|
def is_port_in_use(port: int) -> bool:
|
26
28
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
@@ -202,7 +204,7 @@ class StandaloneIntegrationTests(ProcessIntegrationTest):
|
|
202
204
|
)
|
203
205
|
) as file_path:
|
204
206
|
proc = subprocess.Popen(["locust", "-f", file_path], stdout=PIPE, stderr=PIPE, text=True)
|
205
|
-
gevent.sleep(
|
207
|
+
gevent.sleep(SHORT_SLEEP)
|
206
208
|
proc.send_signal(signal.SIGTERM)
|
207
209
|
stdout, stderr = proc.communicate()
|
208
210
|
self.assertIn("Starting web interface at", stderr)
|
@@ -295,7 +297,7 @@ class StandaloneIntegrationTests(ProcessIntegrationTest):
|
|
295
297
|
proc = subprocess.Popen(
|
296
298
|
["locust", "-f", f"{mocked1.file_path},{mocked2.file_path}"], stdout=PIPE, stderr=PIPE, text=True
|
297
299
|
)
|
298
|
-
gevent.sleep(
|
300
|
+
gevent.sleep(SHORT_SLEEP)
|
299
301
|
proc.send_signal(signal.SIGTERM)
|
300
302
|
stdout, stderr = proc.communicate()
|
301
303
|
self.assertIn("Starting web interface at", stderr)
|
@@ -310,7 +312,7 @@ class StandaloneIntegrationTests(ProcessIntegrationTest):
|
|
310
312
|
with mock_locustfile(content=MOCK_LOCUSTFILE_CONTENT_A, dir=temp_dir):
|
311
313
|
with mock_locustfile(content=MOCK_LOCUSTFILE_CONTENT_B, dir=temp_dir):
|
312
314
|
proc = subprocess.Popen(["locust", "-f", temp_dir], stdout=PIPE, stderr=PIPE, text=True)
|
313
|
-
gevent.sleep(
|
315
|
+
gevent.sleep(SHORT_SLEEP)
|
314
316
|
proc.send_signal(signal.SIGTERM)
|
315
317
|
stdout, stderr = proc.communicate()
|
316
318
|
self.assertIn("Starting web interface at", stderr)
|
@@ -355,7 +357,7 @@ class StandaloneIntegrationTests(ProcessIntegrationTest):
|
|
355
357
|
proc = subprocess.Popen(
|
356
358
|
["locust", "-f", f"{mocked1.file_path},{mocked2}"], stdout=PIPE, stderr=PIPE, text=True
|
357
359
|
)
|
358
|
-
gevent.sleep(
|
360
|
+
gevent.sleep(SHORT_SLEEP)
|
359
361
|
proc.send_signal(signal.SIGTERM)
|
360
362
|
stdout, stderr = proc.communicate()
|
361
363
|
self.assertIn("Starting web interface at", stderr)
|
@@ -1660,6 +1662,171 @@ class SecondUser(HttpUser):
|
|
1660
1662
|
self.assertEqual(0, proc.returncode)
|
1661
1663
|
self.assertEqual(0, proc_worker.returncode)
|
1662
1664
|
|
1665
|
+
def test_locustfile_distribution(self):
|
1666
|
+
LOCUSTFILE_CONTENT = textwrap.dedent(
|
1667
|
+
"""
|
1668
|
+
from locust import User, task, constant
|
1669
|
+
|
1670
|
+
class User1(User):
|
1671
|
+
wait_time = constant(1)
|
1672
|
+
|
1673
|
+
@task
|
1674
|
+
def t(self):
|
1675
|
+
pass
|
1676
|
+
"""
|
1677
|
+
)
|
1678
|
+
with mock_locustfile(content=LOCUSTFILE_CONTENT) as mocked:
|
1679
|
+
proc = subprocess.Popen(
|
1680
|
+
[
|
1681
|
+
"locust",
|
1682
|
+
"-f",
|
1683
|
+
mocked.file_path,
|
1684
|
+
"--headless",
|
1685
|
+
"--master",
|
1686
|
+
"--expect-workers",
|
1687
|
+
"2",
|
1688
|
+
"-t",
|
1689
|
+
"1s",
|
1690
|
+
],
|
1691
|
+
stderr=STDOUT,
|
1692
|
+
stdout=PIPE,
|
1693
|
+
text=True,
|
1694
|
+
)
|
1695
|
+
proc_worker = subprocess.Popen(
|
1696
|
+
[
|
1697
|
+
"locust",
|
1698
|
+
"-f",
|
1699
|
+
"-",
|
1700
|
+
"--worker",
|
1701
|
+
],
|
1702
|
+
stderr=STDOUT,
|
1703
|
+
stdout=PIPE,
|
1704
|
+
text=True,
|
1705
|
+
)
|
1706
|
+
gevent.sleep(2)
|
1707
|
+
# modify the locustfile to trigger warning about file change when the second worker connects
|
1708
|
+
with open(mocked.file_path, "w") as locustfile:
|
1709
|
+
locustfile.write(LOCUSTFILE_CONTENT)
|
1710
|
+
locustfile.write("\n# New comment\n")
|
1711
|
+
gevent.sleep(2)
|
1712
|
+
proc_worker2 = subprocess.Popen(
|
1713
|
+
[
|
1714
|
+
"locust",
|
1715
|
+
"-f",
|
1716
|
+
"-",
|
1717
|
+
"--worker",
|
1718
|
+
],
|
1719
|
+
stderr=STDOUT,
|
1720
|
+
stdout=PIPE,
|
1721
|
+
text=True,
|
1722
|
+
)
|
1723
|
+
stdout = proc.communicate()[0]
|
1724
|
+
proc_worker2.communicate()
|
1725
|
+
proc_worker.communicate()
|
1726
|
+
|
1727
|
+
self.assertIn('All users spawned: {"User1": 1} (1 total users)', stdout)
|
1728
|
+
self.assertIn("Locustfile contents changed on disk after first worker requested locustfile", stdout)
|
1729
|
+
self.assertIn("Shutting down (exit code 0)", stdout)
|
1730
|
+
|
1731
|
+
self.assertEqual(0, proc.returncode)
|
1732
|
+
self.assertEqual(0, proc_worker.returncode)
|
1733
|
+
|
1734
|
+
def test_locustfile_distribution_with_workers_started_first(self):
|
1735
|
+
LOCUSTFILE_CONTENT = textwrap.dedent(
|
1736
|
+
"""
|
1737
|
+
from locust import User, task, constant
|
1738
|
+
|
1739
|
+
class User1(User):
|
1740
|
+
wait_time = constant(1)
|
1741
|
+
|
1742
|
+
@task
|
1743
|
+
def t(self):
|
1744
|
+
print("hello")
|
1745
|
+
"""
|
1746
|
+
)
|
1747
|
+
with mock_locustfile(content=LOCUSTFILE_CONTENT) as mocked:
|
1748
|
+
proc_worker = subprocess.Popen(
|
1749
|
+
[
|
1750
|
+
"locust",
|
1751
|
+
"-f",
|
1752
|
+
"-",
|
1753
|
+
"--worker",
|
1754
|
+
],
|
1755
|
+
stderr=STDOUT,
|
1756
|
+
stdout=PIPE,
|
1757
|
+
text=True,
|
1758
|
+
)
|
1759
|
+
gevent.sleep(2)
|
1760
|
+
proc = subprocess.Popen(
|
1761
|
+
[
|
1762
|
+
"locust",
|
1763
|
+
"-f",
|
1764
|
+
mocked.file_path,
|
1765
|
+
"--headless",
|
1766
|
+
"--master",
|
1767
|
+
"--expect-workers",
|
1768
|
+
"1",
|
1769
|
+
"-t",
|
1770
|
+
"1",
|
1771
|
+
],
|
1772
|
+
stderr=STDOUT,
|
1773
|
+
stdout=PIPE,
|
1774
|
+
text=True,
|
1775
|
+
)
|
1776
|
+
|
1777
|
+
stdout = proc.communicate()[0]
|
1778
|
+
worker_stdout = proc_worker.communicate()[0]
|
1779
|
+
|
1780
|
+
self.assertIn('All users spawned: {"User1": ', stdout)
|
1781
|
+
self.assertIn("Shutting down (exit code 0)", stdout)
|
1782
|
+
|
1783
|
+
self.assertEqual(0, proc.returncode)
|
1784
|
+
self.assertEqual(0, proc_worker.returncode)
|
1785
|
+
self.assertIn("hello", worker_stdout)
|
1786
|
+
|
1787
|
+
def test_distributed_with_locustfile_distribution_not_plain_filename(self):
|
1788
|
+
LOCUSTFILE_CONTENT = textwrap.dedent(
|
1789
|
+
"""
|
1790
|
+
from locust import User, task, constant
|
1791
|
+
|
1792
|
+
class User1(User):
|
1793
|
+
wait_time = constant(1)
|
1794
|
+
|
1795
|
+
@task
|
1796
|
+
def t(self):
|
1797
|
+
pass
|
1798
|
+
"""
|
1799
|
+
)
|
1800
|
+
with mock_locustfile(content=LOCUSTFILE_CONTENT) as mocked:
|
1801
|
+
proc = subprocess.Popen(
|
1802
|
+
[
|
1803
|
+
"locust",
|
1804
|
+
"-f",
|
1805
|
+
mocked.file_path[:-3], # remove ".py"
|
1806
|
+
"--headless",
|
1807
|
+
"--master",
|
1808
|
+
],
|
1809
|
+
stderr=STDOUT,
|
1810
|
+
stdout=PIPE,
|
1811
|
+
text=True,
|
1812
|
+
)
|
1813
|
+
proc_worker = subprocess.Popen(
|
1814
|
+
[
|
1815
|
+
"locust",
|
1816
|
+
"-f",
|
1817
|
+
"-",
|
1818
|
+
"--worker",
|
1819
|
+
],
|
1820
|
+
stderr=STDOUT,
|
1821
|
+
stdout=PIPE,
|
1822
|
+
text=True,
|
1823
|
+
)
|
1824
|
+
stdout = proc_worker.communicate()[0]
|
1825
|
+
self.assertIn("Got error from master: locustfile parameter on master must be a plain filename", stdout)
|
1826
|
+
proc.kill()
|
1827
|
+
master_stdout = proc.communicate()[0]
|
1828
|
+
self.assertIn("--locustfile must be a plain filename (not a module name) for file distribut", master_stdout)
|
1829
|
+
|
1663
1830
|
def test_json_schema(self):
|
1664
1831
|
LOCUSTFILE_CONTENT = textwrap.dedent(
|
1665
1832
|
"""
|
@@ -1972,18 +2139,19 @@ class AnyUser(HttpUser):
|
|
1972
2139
|
text=True,
|
1973
2140
|
start_new_session=True,
|
1974
2141
|
)
|
1975
|
-
gevent.sleep(
|
2142
|
+
gevent.sleep(2)
|
1976
2143
|
master_proc.kill()
|
1977
2144
|
master_proc.wait()
|
1978
2145
|
try:
|
1979
|
-
|
2146
|
+
worker_stdout, worker_stderr = worker_parent_proc.communicate(timeout=7)
|
1980
2147
|
except Exception:
|
1981
2148
|
os.killpg(worker_parent_proc.pid, signal.SIGTERM)
|
1982
|
-
|
1983
|
-
assert False, f"worker never finished: {worker_stderr}"
|
2149
|
+
worker_stdout, worker_stderr = worker_parent_proc.communicate()
|
2150
|
+
assert False, f"worker never finished: {worker_stdout} / {worker_stderr}"
|
1984
2151
|
|
1985
2152
|
self.assertNotIn("Traceback", worker_stderr)
|
1986
2153
|
self.assertIn("Didn't get heartbeat from master in over ", worker_stderr)
|
2154
|
+
self.assertIn("worker index:", worker_stdout)
|
1987
2155
|
|
1988
2156
|
@unittest.skipIf(os.name == "nt", reason="--processes doesnt work on windows")
|
1989
2157
|
def test_processes_error_doesnt_blow_up_completely(self):
|
@@ -2010,6 +2178,7 @@ class AnyUser(HttpUser):
|
|
2010
2178
|
self.assertNotIn("Traceback", stderr)
|
2011
2179
|
|
2012
2180
|
@unittest.skipIf(os.name == "nt", reason="--processes doesnt work on windows")
|
2181
|
+
@unittest.skipIf(sys.platform == "darwin", reason="Flaky on macOS :-/")
|
2013
2182
|
def test_processes_workers_quit_unexpected(self):
|
2014
2183
|
content = """
|
2015
2184
|
from locust import runners, events, User, task
|