locust 2.18.1.dev4__py3-none-any.whl → 2.18.1.dev16__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.

Potentially problematic release.


This version of locust might be problematic. Click here for more details.

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.18.1.dev4'
16
- __version_tuple__ = version_tuple = (2, 18, 1, 'dev4')
15
+ __version__ = version = '2.18.1.dev16'
16
+ __version_tuple__ = version_tuple = (2, 18, 1, 'dev16')
locust/log.py CHANGED
@@ -9,6 +9,15 @@ HOSTNAME = socket.gethostname()
9
9
  unhandled_greenlet_exception = False
10
10
 
11
11
 
12
+ class LogReader(logging.Handler):
13
+ def __init__(self):
14
+ super().__init__()
15
+ self.logs = []
16
+
17
+ def emit(self, record):
18
+ self.logs.append(self.format(record))
19
+
20
+
12
21
  def setup_logging(loglevel, logfile=None):
13
22
  loglevel = loglevel.upper()
14
23
 
@@ -32,21 +41,22 @@ def setup_logging(loglevel, logfile=None):
32
41
  "class": "logging.StreamHandler",
33
42
  "formatter": "plain",
34
43
  },
44
+ "log_reader": {"class": "locust.log.LogReader", "formatter": "default"},
35
45
  },
36
46
  "loggers": {
37
47
  "locust": {
38
- "handlers": ["console"],
48
+ "handlers": ["console", "log_reader"],
39
49
  "level": loglevel,
40
50
  "propagate": False,
41
51
  },
42
52
  "locust.stats_logger": {
43
- "handlers": ["console_plain"],
53
+ "handlers": ["console_plain", "log_reader"],
44
54
  "level": "INFO",
45
55
  "propagate": False,
46
56
  },
47
57
  },
48
58
  "root": {
49
- "handlers": ["console"],
59
+ "handlers": ["console", "log_reader"],
50
60
  "level": loglevel,
51
61
  },
52
62
  }
@@ -58,8 +68,8 @@ def setup_logging(loglevel, logfile=None):
58
68
  "filename": logfile,
59
69
  "formatter": "default",
60
70
  }
61
- LOGGING_CONFIG["loggers"]["locust"]["handlers"] = ["file"]
62
- LOGGING_CONFIG["root"]["handlers"] = ["file"]
71
+ LOGGING_CONFIG["loggers"]["locust"]["handlers"] = ["file", "log_reader"]
72
+ LOGGING_CONFIG["root"]["handlers"] = ["file", "log_reader"]
63
73
 
64
74
  logging.config.dictConfig(LOGGING_CONFIG)
65
75
 
locust/main.py CHANGED
@@ -86,17 +86,20 @@ def main():
86
86
  user_classes: Dict[str, locust.User] = {}
87
87
  available_user_classes = {}
88
88
  available_shape_classes = {}
89
+ shape_class = None
89
90
  for _locustfile in locustfiles:
90
- docstring, _user_classes, shape_class = load_locustfile(_locustfile)
91
+ docstring, _user_classes, shape_classes = load_locustfile(_locustfile)
91
92
 
92
93
  # Setting Available Shape Classes
93
- if shape_class:
94
- shape_class_name = type(shape_class).__name__
95
- if shape_class_name in available_shape_classes.keys():
96
- sys.stderr.write(f"Duplicate shape classes: {shape_class_name}\n")
97
- sys.exit(1)
94
+ if shape_classes:
95
+ shape_class = shape_classes[0]
96
+ for shape_class in shape_classes:
97
+ shape_class_name = type(shape_class).__name__
98
+ if shape_class_name in available_shape_classes.keys():
99
+ sys.stderr.write(f"Duplicate shape classes: {shape_class_name}\n")
100
+ sys.exit(1)
98
101
 
99
- available_shape_classes[shape_class_name] = shape_class
102
+ available_shape_classes[shape_class_name] = shape_class
100
103
 
101
104
  # Setting Available User Classes
102
105
  for key, value in _user_classes.items():
@@ -470,5 +470,22 @@ body.ready .main {
470
470
  margin-right: 10px;
471
471
  color: #48a584;
472
472
  }
473
+ .userClass {
474
+ border: none;
475
+ background: #fff;
476
+ height: 100px;
477
+ width: 340px;
478
+ font-size: 18px;
479
+ padding-left: 10px;
480
+ }
481
+
482
+ .shapeClass {
483
+ border: none;
484
+ background: #fff;
485
+ /* height: 100px; */
486
+ width: 340px;
487
+ font-size: 18px;
488
+ padding-left: 10px;
489
+ }
473
490
 
474
491
  /*# sourceMappingURL=application.css.map */
@@ -65,7 +65,7 @@
65
65
  {% for user in available_user_classes %}
66
66
  <option value="{{user}}" selected>{{user}}</option>
67
67
  {% endfor %}
68
- </select><br>range
68
+ </select><br>
69
69
  <label for="available_shape_classes">ShapeClass </label>
70
70
  <select name="shape_class" id="shape-classes" class="shapeClass">
71
71
  {% for shape_class in available_shape_classes %}
@@ -37,25 +37,25 @@ class TestLoadLocustfile(LocustTestCase):
37
37
 
38
38
  def test_load_locust_file_from_absolute_path(self):
39
39
  with mock_locustfile() as mocked:
40
- docstring, user_classes, shape_class = main.load_locustfile(mocked.file_path)
40
+ docstring, user_classes, shape_classes = main.load_locustfile(mocked.file_path)
41
41
  self.assertIn("UserSubclass", user_classes)
42
42
  self.assertNotIn("NotUserSubclass", user_classes)
43
43
  self.assertNotIn("LoadTestShape", user_classes)
44
- self.assertIsNone(shape_class)
44
+ self.assertEqual(shape_classes, [])
45
45
 
46
46
  def test_load_locust_file_from_relative_path(self):
47
47
  with mock_locustfile() as mocked:
48
- docstring, user_classes, shape_class = main.load_locustfile(
48
+ docstring, user_classes, shape_classes = main.load_locustfile(
49
49
  os.path.join(os.path.relpath(mocked.directory, os.getcwd()), mocked.filename)
50
50
  )
51
51
 
52
52
  def test_load_locust_file_with_a_dot_in_filename(self):
53
53
  with mock_locustfile(filename_prefix="mocked.locust.file") as mocked:
54
- docstring, user_classes, shape_class = main.load_locustfile(mocked.file_path)
54
+ docstring, user_classes, shape_classes = main.load_locustfile(mocked.file_path)
55
55
 
56
56
  def test_return_docstring_and_user_classes(self):
57
57
  with mock_locustfile() as mocked:
58
- docstring, user_classes, shape_class = main.load_locustfile(mocked.file_path)
58
+ docstring, user_classes, shape_classes = main.load_locustfile(mocked.file_path)
59
59
  self.assertEqual("This is a mock locust file for unit testing", docstring)
60
60
  self.assertIn("UserSubclass", user_classes)
61
61
  self.assertNotIn("NotUserSubclass", user_classes)
@@ -70,11 +70,31 @@ class TestLoadLocustfile(LocustTestCase):
70
70
  """
71
71
  )
72
72
  with mock_locustfile(content=content) as mocked:
73
- docstring, user_classes, shape_class = main.load_locustfile(mocked.file_path)
73
+ docstring, user_classes, shape_classes = main.load_locustfile(mocked.file_path)
74
74
  self.assertEqual("This is a mock locust file for unit testing", docstring)
75
75
  self.assertIn("UserSubclass", user_classes)
76
76
  self.assertNotIn("NotUserSubclass", user_classes)
77
- self.assertEqual(shape_class.__class__.__name__, "LoadTestShape")
77
+ self.assertEqual(shape_classes[0].__class__.__name__, "LoadTestShape")
78
+
79
+ def test_with_multiple_shape_classes(self):
80
+ content = MOCK_LOCUSTFILE_CONTENT + textwrap.dedent(
81
+ """\
82
+ class LoadTestShape1(LoadTestShape):
83
+ def tick(self):
84
+ pass
85
+
86
+ class LoadTestShape2(LoadTestShape):
87
+ def tick(self):
88
+ pass
89
+ """
90
+ )
91
+ with mock_locustfile(content=content) as mocked:
92
+ docstring, user_classes, shape_classes = main.load_locustfile(mocked.file_path)
93
+ self.assertEqual("This is a mock locust file for unit testing", docstring)
94
+ self.assertIn("UserSubclass", user_classes)
95
+ self.assertNotIn("NotUserSubclass", user_classes)
96
+ self.assertEqual(shape_classes[0].__class__.__name__, "LoadTestShape1")
97
+ self.assertEqual(shape_classes[1].__class__.__name__, "LoadTestShape2")
78
98
 
79
99
  def test_with_abstract_shape_class(self):
80
100
  content = MOCK_LOCUSTFILE_CONTENT + textwrap.dedent(
@@ -92,10 +112,10 @@ class TestLoadLocustfile(LocustTestCase):
92
112
  )
93
113
 
94
114
  with mock_locustfile(content=content) as mocked:
95
- _, user_classes, shape_class = main.load_locustfile(mocked.file_path)
115
+ _, user_classes, shape_classes = main.load_locustfile(mocked.file_path)
96
116
  self.assertNotIn("UserBaseLoadTestShape", user_classes)
97
117
  self.assertNotIn("UserLoadTestShape", user_classes)
98
- self.assertEqual(shape_class.__class__.__name__, "UserLoadTestShape")
118
+ self.assertEqual(shape_classes[0].__class__.__name__, "UserLoadTestShape")
99
119
 
100
120
  def test_with_not_imported_shape_class(self):
101
121
  content = MOCK_LOCUSTFILE_CONTENT + textwrap.dedent(
@@ -107,9 +127,9 @@ class TestLoadLocustfile(LocustTestCase):
107
127
  )
108
128
 
109
129
  with mock_locustfile(content=content) as mocked:
110
- _, user_classes, shape_class = main.load_locustfile(mocked.file_path)
130
+ _, user_classes, shape_classes = main.load_locustfile(mocked.file_path)
111
131
  self.assertNotIn("UserLoadTestShape", user_classes)
112
- self.assertEqual(shape_class.__class__.__name__, "UserLoadTestShape")
132
+ self.assertEqual(shape_classes[0].__class__.__name__, "UserLoadTestShape")
113
133
 
114
134
  def test_create_environment(self):
115
135
  options = parse_options(
locust/test/test_web.py CHANGED
@@ -5,13 +5,13 @@ import os
5
5
  import re
6
6
  import textwrap
7
7
  import traceback
8
+ import logging
8
9
  from io import StringIO
9
10
  from tempfile import NamedTemporaryFile, TemporaryDirectory
10
11
 
11
12
  import gevent
12
13
  import requests
13
14
  from pyquery import PyQuery as pq
14
-
15
15
  import locust
16
16
  from locust import constant, LoadTestShape
17
17
  from locust.argument_parser import get_parser, parse_options
@@ -21,6 +21,7 @@ from locust.runners import Runner
21
21
  from locust import stats
22
22
  from locust.stats import StatsCSVFileWriter
23
23
  from locust.web import WebUI
24
+ from locust.log import LogReader
24
25
 
25
26
  from .mock_locustfile import mock_locustfile
26
27
  from .testcases import LocustTestCase
@@ -563,7 +564,15 @@ class TestWebUI(LocustTestCase, _HeaderCheckMixin):
563
564
  def t(self):
564
565
  pass
565
566
 
566
- class TestShape(LoadTestShape):
567
+ class TestShape1(LoadTestShape):
568
+ def tick(self):
569
+ run_time = self.get_run_time()
570
+ if run_time < 10:
571
+ return 4, 4
572
+ else:
573
+ return None
574
+
575
+ class TestShape2(LoadTestShape):
567
576
  def tick(self):
568
577
  run_time = self.get_run_time()
569
578
  if run_time < 10:
@@ -573,8 +582,8 @@ class TestWebUI(LocustTestCase, _HeaderCheckMixin):
573
582
 
574
583
  self.environment.web_ui.userclass_picker_is_active = True
575
584
  self.environment.available_user_classes = {"User1": User1, "User2": User2}
576
- self.environment.available_shape_classes = {"TestShape": TestShape()}
577
- self.environment.shape_class = None
585
+ self.environment.available_shape_classes = {"TestShape1": TestShape1(), "TestShape2": TestShape2()}
586
+ self.environment.shape_class = TestShape1()
578
587
 
579
588
  response = requests.post(
580
589
  "http://127.0.0.1:%i/swarm" % self.web_port,
@@ -583,14 +592,14 @@ class TestWebUI(LocustTestCase, _HeaderCheckMixin):
583
592
  "spawn_rate": 5,
584
593
  "host": "https://localhost",
585
594
  "user_classes": "User1",
586
- "shape_class": "TestShape",
595
+ "shape_class": "TestShape2",
587
596
  },
588
597
  )
589
598
 
590
599
  self.assertEqual(200, response.status_code)
591
600
  self.assertEqual("https://localhost", response.json()["host"])
592
601
  self.assertEqual(self.environment.host, "https://localhost")
593
- assert isinstance(self.environment.shape_class, TestShape)
602
+ assert isinstance(self.environment.shape_class, TestShape2)
594
603
 
595
604
  # stop
596
605
  gevent.sleep(1)
@@ -1005,6 +1014,19 @@ class TestWebUI(LocustTestCase, _HeaderCheckMixin):
1005
1014
  self.assertIn("Script: <span>locust.py</span>", str(d))
1006
1015
  self.assertIn("Target Host: <span>http://localhost</span>", str(d))
1007
1016
 
1017
+ def test_logs(self):
1018
+ log_handler = LogReader()
1019
+ log_handler.name = "log_reader"
1020
+ log_handler.setLevel(logging.INFO)
1021
+ logger = logging.getLogger("root")
1022
+ logger.addHandler(log_handler)
1023
+ log_line = "some log info"
1024
+ logger.info(log_line)
1025
+
1026
+ response = requests.get("http://127.0.0.1:%i/logs" % self.web_port)
1027
+
1028
+ self.assertIn(log_line, response.json().get("logs"))
1029
+
1008
1030
 
1009
1031
  class TestWebUIAuth(LocustTestCase):
1010
1032
  def setUp(self):
@@ -2,7 +2,7 @@ import importlib
2
2
  import inspect
3
3
  import os
4
4
  import sys
5
- from typing import Dict, Optional, Tuple
5
+ from typing import Dict, List, Optional, Tuple
6
6
  from ..shape import LoadTestShape
7
7
  from ..user import User
8
8
 
@@ -21,7 +21,7 @@ def is_shape_class(item):
21
21
  return bool(inspect.isclass(item) and issubclass(item, LoadTestShape) and not getattr(item, "abstract", True))
22
22
 
23
23
 
24
- def load_locustfile(path) -> Tuple[Optional[str], Dict[str, User], Optional[LoadTestShape]]:
24
+ def load_locustfile(path) -> Tuple[Optional[str], Dict[str, User], List[LoadTestShape]]:
25
25
  """
26
26
  Import given locustfile path and return (docstring, callables).
27
27
 
@@ -65,10 +65,6 @@ def load_locustfile(path) -> Tuple[Optional[str], Dict[str, User], Optional[Load
65
65
  user_classes = {name: value for name, value in vars(imported).items() if is_user_class(value)}
66
66
 
67
67
  # Find shape class, if any, return it
68
- shape_classes = [value for name, value in vars(imported).items() if is_shape_class(value)]
69
- if shape_classes:
70
- shape_class = shape_classes[0]()
71
- else:
72
- shape_class = None
68
+ shape_classes = [value() for name, value in vars(imported).items() if is_shape_class(value)]
73
69
 
74
- return imported.__doc__, user_classes, shape_class
70
+ return imported.__doc__, user_classes, shape_classes
locust/web.py CHANGED
@@ -136,6 +136,12 @@ class WebUI:
136
136
  if not delayed_start:
137
137
  self.start()
138
138
 
139
+ @app.errorhandler(Exception)
140
+ def handle_exception(error):
141
+ error_message = str(error)
142
+ logger.log(logging.CRITICAL, error_message)
143
+ return make_response(error_message, 500)
144
+
139
145
  @app.route("/assets/<path:path>")
140
146
  def send_assets(path):
141
147
  webui_build_path = self.webui_build_path
@@ -229,7 +235,11 @@ class WebUI:
229
235
  if environment.shape_class and environment.runner is not None:
230
236
  environment.runner.start_shape()
231
237
  return jsonify(
232
- {"success": True, "message": "Swarming started using shape class", "host": environment.host}
238
+ {
239
+ "success": True,
240
+ "message": f"Swarming started using shape class '{type(environment.shape_class).__name__}'",
241
+ "host": environment.host,
242
+ }
233
243
  )
234
244
 
235
245
  if self._swarm_greenlet is not None:
@@ -474,6 +484,20 @@ class WebUI:
474
484
  }
475
485
  return task_data
476
486
 
487
+ @app.route("/logs")
488
+ @self.auth_required_if_enabled
489
+ def logs():
490
+ log_reader_handler = [
491
+ handler for handler in logging.getLogger("root").handlers if handler.name == "log_reader"
492
+ ]
493
+
494
+ if log_reader_handler:
495
+ logs = log_reader_handler[0].logs
496
+ else:
497
+ logs = []
498
+
499
+ return jsonify({"logs": logs})
500
+
477
501
  def start(self):
478
502
  self.greenlet = gevent.spawn(self.start_server)
479
503
  self.greenlet.link_exception(greenlet_exception_handler)