From f420dacd5b9e868fd8fa8f9f69143ef99c2aaaaf Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Tue, 14 Apr 2020 12:12:33 +0200 Subject: [PATCH 01/10] Remove unused attributes from Environment --- locust/env.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/locust/env.py b/locust/env.py index 004576528b..26963b6d6b 100644 --- a/locust/env.py +++ b/locust/env.py @@ -38,18 +38,6 @@ class Environment: If False, exeptions will be raised. """ - master_host = "127.0.0.1" - """Hostname of master node that the worker should connect to""" - - master_port = 5557 - """Port of master node that the worker should connect to. Defaults to 5557.""" - - master_bind_host = "*" - """Hostname/interfaces that the master node should expect workers to connect to. Defaults to '*' which means all interfaces.""" - - master_bind_port = 5557 - """Port that the master node should listen to and expect workers to connect to. Defaults to 5557.""" - def __init__( self, events=None, From cd3c3cff1e8ed348ce410f5953686c5d249f819e Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Tue, 14 Apr 2020 12:31:09 +0200 Subject: [PATCH 02/10] Small refactoring of Runner/Environment * Change how Runners and WebUI classes are created by introducing methods for this on Environment. This allows us to get rid of setting `environment.runner = self` in the LocustRunner constructor. It should also simplify the API for users who want to run Locust as a library. * Move RequestStats instance to Environment.stats. * Add Environment.locust_classes. * Remove Environment.options since it's no longer needed. * Use keyword only arguments for the Environment constructor. --- locust/env.py | 75 +++++++++++++++++++++++++++--- locust/exception.py | 5 +- locust/main.py | 25 ++++------ locust/runners.py | 16 +++---- locust/stats.py | 6 +-- locust/test/test_main.py | 4 +- locust/test/test_runners.py | 92 +++++++++++++++++-------------------- locust/test/test_stats.py | 12 +++-- locust/test/test_web.py | 3 +- locust/test/testcases.py | 2 +- 10 files changed, 142 insertions(+), 98 deletions(-) diff --git a/locust/env.py b/locust/env.py index 26963b6d6b..f307b90591 100644 --- a/locust/env.py +++ b/locust/env.py @@ -1,4 +1,8 @@ from .event import Events +from .exception import RunnerAlreadyExistsError +from .stats import RequestStats +from .runners import LocalLocustRunner, MasterLocustRunner, WorkerLocustRunner +from .web import WebUI class Environment: @@ -8,15 +12,18 @@ class Environment: See :ref:`events` for available events. """ + locust_classes = [] + """Locust User classes that the runner will run""" + + stats = None + """Reference to RequestStats instance""" + runner = None """Reference to the LocustRunner instance""" web_ui = None """Reference to the WebUI instance""" - options = None - """Parsed command line options""" - host = None """Base URL of the target system""" @@ -39,9 +46,9 @@ class Environment: """ def __init__( - self, + self, *, + locust_classes=[], events=None, - options=None, host=None, reset_stats=False, step_load=False, @@ -53,10 +60,66 @@ def __init__( else: self.events = Events() - self.options = options + self.locust_classes = locust_classes + self.stats = RequestStats() self.host = host self.reset_stats = reset_stats self.step_load = step_load self.stop_timeout = stop_timeout self.catch_exceptions = catch_exceptions + + def _create_runner(self, runner_class, *args, **kwargs): + if self.runner is not None: + raise RunnerAlreadyExistsError("Environment.runner already exists (%s)" % self.runner) + self.runner = runner_class(self, *args, **kwargs) + return self.runner + + def create_local_runner(self): + """ + Create a LocalLocustRunner instance for this Environment + """ + return self._create_runner(LocalLocustRunner, locust_classes=self.locust_classes) + + def create_master_runner(self, master_bind_host="*", master_bind_port=5557): + """ + Create a MasterLocustRunner instance for this Environment + + Arguments: + master_bind_host: Interface/host that the master should use for incoming worker connections. + Defaults to "*" which means all interfaces. + master_bind_port: Port that the master should listen for incoming worker connections on + """ + return self._create_runner( + MasterLocustRunner, + locust_classes=self.locust_classes, + master_bind_host=master_bind_host, + master_bind_port=master_bind_port, + ) + + def create_worker_runner(self, master_host, master_port): + """ + Create a WorkerLocustRunner instance for this Environment + + Arguments: + master_host: Host/IP of a running master node + master_port: Port on master node to connect to + """ + # Create a new RequestStats with use_response_times_cache set to False to save some memory + # and CPU cycles, since the response_times_cache is not needed for Worker nodes + self.stats = RequestStats(use_response_times_cache=False) + return self._create_runner( + WorkerLocustRunner, + locust_classes=self.locust_classes, + master_host=master_host, + master_port=master_port, + ) + + def create_web_ui(self, auth_credentials=None): + """ + Creates a WebUI instance for this Environment + Arguments: + auth_credentials: If provided (in format "username:password") basic auth will be enabled + """ + self.web_ui = WebUI(self, auth_credentials=auth_credentials) + return self.web_ui diff --git a/locust/exception.py b/locust/exception.py index 4004eac70f..79c3704f7f 100644 --- a/locust/exception.py +++ b/locust/exception.py @@ -50,4 +50,7 @@ class AuthCredentialsError(ValueError): Exception when the auth credentials provided are not in the correct format """ - pass \ No newline at end of file + pass + +class RunnerAlreadyExistsError(Exception): + pass diff --git a/locust/main.py b/locust/main.py index 7245eb1b11..0adfa47124 100644 --- a/locust/main.py +++ b/locust/main.py @@ -16,11 +16,9 @@ from .env import Environment from .inspectlocust import get_task_ratio_dict, print_task_ratio from .log import console_logger, setup_logging -from .runners import LocalLocustRunner, MasterLocustRunner, WorkerLocustRunner from .stats import (print_error_report, print_percentile_stats, print_stats, stats_printer, stats_writer, write_csv_files) from .util.timespan import parse_timespan -from .web import WebUI from .exception import AuthCredentialsError _internals = [Locust, HttpLocust] @@ -89,14 +87,14 @@ def __import_locustfile__(filename, path): return imported.__doc__, locusts -def create_environment(options, events=None): +def create_environment(locust_classes, options, events=None): """ Create an Environment instance from options """ return Environment( + locust_classes=locust_classes, events=events, host=options.host, - options=options, reset_stats=options.reset_stats, step_load=options.step_load, stop_timeout=options.stop_timeout, @@ -148,7 +146,7 @@ def main(): locust_classes = list(locusts.values()) # create locust Environment - environment = create_environment(options, events=locust.events) + environment = create_environment(locust_classes, options, events=locust.events) if options.show_task_ratio: console_logger.info("\n Task ratio per locust class") @@ -181,25 +179,18 @@ def main(): sys.exit(1) if options.master: - runner = MasterLocustRunner( - environment, - locust_classes, - master_bind_host=options.master_bind_host, + runner = environment.create_master_runner( + master_bind_host=options.master_bind_host, master_bind_port=options.master_bind_port, ) elif options.worker: try: - runner = WorkerLocustRunner( - environment, - locust_classes, - master_host=options.master_host, - master_port=options.master_port, - ) + runner = environment.create_worker_runner(options.master_host, options.master_port) except socket.error as e: logger.error("Failed to connect to the Locust master: %s", e) sys.exit(-1) else: - runner = LocalLocustRunner(environment, locust_classes) + runner = environment.create_local_runner() # main_greenlet is pointing to runners.greenlet by default, it will point the web greenlet later if in web mode main_greenlet = runner.greenlet @@ -228,7 +219,7 @@ def timelimit_stop(): # spawn web greenlet logger.info("Starting web monitor at http://%s:%s" % (options.web_host or "*", options.web_port)) try: - web_ui = WebUI(environment=environment, auth_credentials=options.web_auth) + web_ui = environment.create_web_ui(auth_credentials=options.web_auth) except AuthCredentialsError: logger.error("Credentials supplied with --web-auth should have the format: username:password") sys.exit(1) diff --git a/locust/runners.py b/locust/runners.py index 7c4fff94db..4bb1a77a12 100644 --- a/locust/runners.py +++ b/locust/runners.py @@ -29,7 +29,6 @@ class LocustRunner(object): def __init__(self, environment, locust_classes): - environment.runner = self self.environment = environment self.locust_classes = locust_classes self.locusts = Group() @@ -41,7 +40,6 @@ def __init__(self, environment, locust_classes): self.cpu_warning_emitted = False self.greenlet.spawn(self.monitor_cpu) self.exceptions = {} - self.stats = RequestStats() # set up event listeners for recording requests def on_request_success(request_type, name, response_time, response_length, **kwargs): @@ -68,6 +66,10 @@ def __del__(self): if self.greenlet and len(self.greenlet) > 0: self.greenlet.kill(block=False) + @property + def stats(self): + return self.environment.stats + @property def errors(self): return self.stats.errors @@ -501,14 +503,8 @@ def worker_count(self): return len(self.clients.ready) + len(self.clients.hatching) + len(self.clients.running) class WorkerLocustRunner(DistributedLocustRunner): - def __init__(self, *args, master_host, master_port, **kwargs): - # Create a new RequestStats with use_response_times_cache set to False to save some memory - # and CPU cycles. We need to create the new RequestStats before we call super() (since int's - # used in the constructor of DistributedLocustRunner) - self.stats = RequestStats(use_response_times_cache=False) - - super().__init__(*args, **kwargs) - + def __init__(self, environment, *args, master_host, master_port, **kwargs): + super().__init__(environment, *args, **kwargs) self.client_id = socket.gethostname() + "_" + uuid4().hex self.master_host = master_host self.master_port = master_port diff --git a/locust/stats.py b/locust/stats.py index 301f355ae5..968d4267fd 100644 --- a/locust/stats.py +++ b/locust/stats.py @@ -763,13 +763,13 @@ def stats_writer(environment, base_filepath, full_history=False): def write_csv_files(environment, base_filepath, full_history=False): """Writes the requests, distribution, and failures csvs.""" with open(base_filepath + '_stats.csv', 'w') as f: - f.write(requests_csv(environment.runner.stats)) + f.write(requests_csv(environment.stats)) with open(base_filepath + '_stats_history.csv', 'a') as f: f.write(stats_history_csv(environment, full_history) + "\n") with open(base_filepath + '_failures.csv', 'w') as f: - f.write(failures_csv(environment.runner.stats)) + f.write(failures_csv(environment.stats)) def sort_stats(stats): @@ -869,7 +869,7 @@ def stats_history_csv(environment, all_entries=False): Aggregated stats entry, but if all_entries is set to True, a row for each entry will will be included. """ - stats = environment.runner.stats + stats = environment.stats timestamp = int(time.time()) stats_entries = [] if all_entries: diff --git a/locust/test/test_main.py b/locust/test/test_main.py index aa296e9b15..e7f094feed 100644 --- a/locust/test/test_main.py +++ b/locust/test/test_main.py @@ -58,12 +58,12 @@ def test_create_environment(self): "--host", "https://custom-host", "--reset-stats", ]) - env = create_environment(options) + env = create_environment([], options) self.assertEqual("https://custom-host", env.host) self.assertTrue(env.reset_stats) options = parse_options(args=[]) - env = create_environment(options) + env = create_environment([], options) self.assertEqual(None, env.host) self.assertFalse(env.reset_stats) diff --git a/locust/test/test_runners.py b/locust/test/test_runners.py index 0baeb50b4f..70c7725300 100644 --- a/locust/test/test_runners.py +++ b/locust/test/test_runners.py @@ -1,10 +1,11 @@ +import mock import unittest import gevent from gevent import sleep from gevent.queue import Queue -import mock +import locust from locust import runners from locust.main import create_environment from locust.core import Locust, TaskSet, task @@ -108,9 +109,7 @@ class CpuLocust(Locust): def cpu_task(self): for i in range(1000000): _ = 3 / 2 - environment = Environment( - options=mocked_options(), - ) + environment = Environment() runner = LocalLocustRunner(environment, [CpuLocust]) self.assertFalse(runner.cpu_warning_emitted) runner.spawn_locusts(1, 1, wait=False) @@ -131,7 +130,7 @@ class L2(BaseLocust): class L3(BaseLocust): weight = 100 - runner = LocustRunner(Environment(options=mocked_options()), locust_classes=[L1, L2, L3]) + runner = Environment(locust_classes=[L1, L2, L3]).create_local_runner() self.assert_locust_class_distribution({L1:10, L2:9, L3:10}, runner.weight_locusts(29)) self.assert_locust_class_distribution({L1:10, L2:10, L3:10}, runner.weight_locusts(30)) self.assert_locust_class_distribution({L1:11, L2:10, L3:10}, runner.weight_locusts(31)) @@ -146,7 +145,7 @@ class L2(BaseLocust): class L3(BaseLocust): weight = 100 - runner = LocustRunner(Environment(options=mocked_options()), locust_classes=[L1, L2, L3]) + runner = Environment(locust_classes=[L1, L2, L3]).create_local_runner() self.assertEqual(1, len(runner.weight_locusts(1))) self.assert_locust_class_distribution({L1:1}, runner.weight_locusts(1)) @@ -159,7 +158,7 @@ class task_set(TaskSet): @task def trigger(self): triggered[0] = True - runner = LocustRunner(Environment(options=mocked_options()), locust_classes=[BaseLocust]) + runner = Environment(locust_classes=[BaseLocust]).create_local_runner() runner.spawn_locusts(2, hatch_rate=2, wait=False) self.assertEqual(2, len(runner.locusts)) g1 = list(runner.locusts)[0] @@ -180,7 +179,7 @@ def my_task(self): test_start_run = [0] - environment = Environment(options=mocked_options()) + environment = Environment() def on_test_start(*args, **kwargs): test_start_run[0] += 1 environment.events.test_start.add_listener(on_test_start) @@ -200,7 +199,7 @@ def my_task(self): pass test_stop_run = [0] - environment = Environment(options=mocked_options()) + environment = Environment() def on_test_stop(*args, **kwargs): test_stop_run[0] += 1 environment.events.test_stop.add_listener(on_test_stop) @@ -219,7 +218,7 @@ def my_task(self): pass test_stop_run = [0] - environment = Environment(options=mocked_options()) + environment = Environment() def on_test_stop(*args, **kwargs): test_stop_run[0] += 1 environment.events.test_stop.add_listener(on_test_stop) @@ -238,7 +237,7 @@ def my_task(self): pass test_stop_run = [0] - environment = Environment(options=mocked_options()) + environment = Environment() def on_test_stop(*args, **kwargs): test_stop_run[0] += 1 environment.events.test_stop.add_listener(on_test_stop) @@ -257,7 +256,7 @@ class User(Locust): def my_task(self): pass - environment = Environment(options=mocked_options()) + environment = Environment() runner = LocalLocustRunner(environment, [User]) runner.start(locust_count=10, hatch_rate=5, wait=False) sleep(0.6) @@ -281,7 +280,7 @@ def my_task(self): ) sleep(2) - environment = Environment(reset_stats=True, options=mocked_options()) + environment = Environment(reset_stats=True) runner = LocalLocustRunner(environment, locust_classes=[User]) runner.start(locust_count=6, hatch_rate=12, wait=False) sleep(0.25) @@ -305,7 +304,7 @@ def my_task(self): ) sleep(2) - environment = Environment(reset_stats=False, options=mocked_options()) + environment = Environment(reset_stats=False) runner = LocalLocustRunner(environment, locust_classes=[User]) runner.start(locust_count=6, hatch_rate=12, wait=False) sleep(0.25) @@ -338,14 +337,14 @@ def incr_stats(l): ) with mock.patch("locust.runners.WORKER_REPORT_INTERVAL", new=0.3): # start a Master runner - master_env = Environment() - master = MasterLocustRunner(master_env, [TestUser], master_bind_host="*", master_bind_port=0) + master_env = Environment(locust_classes=[TestUser]) + master = master_env.create_master_runner("*", 0) sleep(0) # start 3 Worker runners workers = [] for i in range(3): - worker_env = Environment() - worker = WorkerLocustRunner(worker_env, [TestUser], master_host="127.0.0.1", master_port=master.server.port) + worker_env = Environment(locust_classes=[TestUser]) + worker = worker_env.create_worker_runner("127.0.0.1", master.server.port) workers.append(worker) # give workers time to connect @@ -374,18 +373,13 @@ def incr_stats(l): class TestMasterRunner(LocustTestCase): def setUp(self): super(TestMasterRunner, self).setUp() - #self._worker_report_event_handlers = [h for h in events.worker_report._handlers] - self.environment.options = mocked_options() - - class MyTestLocust(Locust): - pass + self.environment = Environment(events=locust.events, catch_exceptions=False) def tearDown(self): - #events.worker_report._handlers = self._worker_report_event_handlers super(TestMasterRunner, self).tearDown() def get_runner(self): - return MasterLocustRunner(self.environment, [], master_bind_host="*", master_bind_port=5557) + return self.environment.create_master_runner("*", 5557) def test_worker_connect(self): with mock.patch("locust.rpc.rpc.Server", mocked_rpc()) as server: @@ -512,9 +506,9 @@ def test_master_current_response_times(self): start_time = 1 with mock.patch("time.time") as mocked_time: mocked_time.return_value = start_time - self.runner.stats.reset_all() with mock.patch("locust.rpc.rpc.Server", mocked_rpc()) as server: master = self.get_runner() + self.environment.stats.reset_all() mocked_time.return_value += 1.0234 server.mocked_send(Message("client_ready", None, "fake_client")) stats = RequestStats() @@ -680,7 +674,7 @@ class MyTestLocust(Locust): tasks = [MyTaskSet] wait_time = constant(0.1) - environment = Environment(options=mocked_options()) + environment = Environment() runner = LocalLocustRunner(environment, [MyTestLocust]) timeout = gevent.Timeout(2.0) @@ -764,7 +758,8 @@ class MyLocust(Locust): def will_error(self): raise HeyAnException(":(") - runner = LocalLocustRunner(self.environment, [MyLocust]) + self.environment.locust_classes = [MyLocust] + runner = self.environment.create_local_runner() l = MyLocust(self.environment) @@ -857,7 +852,7 @@ def the_task(self): MyTestLocust._test_state = 2 with mock.patch("locust.rpc.rpc.Client", mocked_rpc()) as client: - environment = Environment(options=mocked_options()) + environment = Environment() test_start_run = [False] @environment.events.test_start.add_listener def on_test_start(**kw): @@ -898,9 +893,7 @@ def the_task(self): MyTestLocust._test_state = 2 with mock.patch("locust.rpc.rpc.Client", mocked_rpc()) as client: - options = mocked_options() - options.stop_timeout = None - environment = Environment(options=options) + environment = Environment(stop_timeout=None) worker = self.get_runner(environment=environment, locust_classes=[MyTestLocust]) self.assertEqual(1, len(client.outbox)) self.assertEqual("client_ready", client.outbox[0].type) @@ -932,9 +925,7 @@ def my_task(self): pass with mock.patch("locust.rpc.rpc.Client", mocked_rpc()) as client: - options = mocked_options() - options.stop_timeout = None - environment = Environment(options=options) + environment = Environment() worker = self.get_runner(environment=environment, locust_classes=[User]) client.mocked_send(Message("hatch", { @@ -981,8 +972,7 @@ class MyTestLocust(Locust): tasks = [MyTaskSet] wait_time = constant(0) - options = mocked_options() - environment = Environment(options=options) + environment = Environment() runner = LocalLocustRunner(environment, [MyTestLocust]) runner.start(1, 1) gevent.sleep(short_time / 2) @@ -1028,9 +1018,9 @@ class MyTestLocust(Locust): tasks = [MyTaskSet] wait_time = constant(0) - environment = create_environment(mocked_options()) + environment = create_environment([MyTestLocust], mocked_options()) environment.stop_timeout = short_time - runner = LocalLocustRunner(environment, [MyTestLocust]) + runner = environment.create_local_runner() runner.start(1, 1) gevent.sleep(short_time / 2) runner.quit() @@ -1049,10 +1039,8 @@ class MyTestLocust(Locust): tasks = [MyTaskSet] wait_time = between(1, 1) - options = mocked_options() - options.stop_timeout = short_time - environment = Environment(options=options) - runner = LocalLocustRunner(environment, [MyTestLocust]) + environment = Environment(locust_classes=[MyTestLocust], stop_timeout=short_time) + runner = environment.create_local_runner() runner.start(1, 1) gevent.sleep(short_time) # sleep to make sure locust has had time to start waiting timeout = gevent.Timeout(short_time) @@ -1080,9 +1068,9 @@ class MyTestLocust(Locust): tasks = [MyTaskSet] wait_time = constant(0) - environment = create_environment(mocked_options()) + environment = create_environment([MyTestLocust], mocked_options()) environment.stop_timeout = short_time - runner = LocalLocustRunner(environment, [MyTestLocust]) + runner = environment.create_local_runner() runner.start(1, 1, wait=True) gevent.sleep(0) timeout = gevent.Timeout(short_time) @@ -1108,9 +1096,9 @@ class MyTestLocust(Locust): tasks = [MySubTaskSet] wait_time = constant(3) - environment = create_environment(mocked_options()) + environment = create_environment([MyTestLocust], mocked_options()) environment.stop_timeout = 0.3 - runner = LocalLocustRunner(environment, [MyTestLocust]) + runner = environment.create_local_runner() runner.start(1, 1, wait=True) gevent.sleep(0) timeout = gevent.Timeout(0.11) @@ -1139,24 +1127,26 @@ class MyTestLocust(Locust): tasks = [MyTaskSet] wait_time = constant(0) - environment = create_environment(mocked_options()) - runner = LocalLocustRunner(environment, [MyTestLocust]) + environment = create_environment([MyTestLocust], mocked_options()) + runner = environment.create_local_runner() runner.start(1, 1) gevent.sleep(short_time / 2) runner.kill_locusts(1) self.assertEqual("first", MyTaskSet.state) runner.quit() + environment.runner = None environment.stop_timeout = short_time / 2 # exit with timeout - runner = LocalLocustRunner(environment, [MyTestLocust]) + runner = environment.create_local_runner() runner.start(1, 1) gevent.sleep(short_time) runner.kill_locusts(1) self.assertEqual("second", MyTaskSet.state) runner.quit() + environment.runner = None environment.stop_timeout = short_time * 3 # allow task iteration to complete, with some margin - runner = LocalLocustRunner(environment, [MyTestLocust]) + runner = environment.create_local_runner() runner.start(1, 1) gevent.sleep(short_time) timeout = gevent.Timeout(short_time * 2) diff --git a/locust/test/test_stats.py b/locust/test/test_stats.py index a523d9b80c..f43fa60eff 100644 --- a/locust/test/test_stats.py +++ b/locust/test/test_stats.py @@ -360,7 +360,8 @@ def test_csv_stats_writer_full_history(self): def test_csv_stats_on_master_from_aggregated_stats(self): # Failing test for: https://github.com/locustio/locust/issues/1315 with mock.patch("locust.rpc.rpc.Server", mocked_rpc()) as server: - master = MasterLocustRunner(self.environment, [], master_bind_host="*", master_bind_port=0) + environment = Environment() + master = environment.create_master_runner(master_bind_host="*", master_bind_port=0) server.mocked_send(Message("client_ready", None, "fake_client")) master.stats.get("/", "GET").log(100, 23455) @@ -368,14 +369,14 @@ def test_csv_stats_on_master_from_aggregated_stats(self): master.stats.get("/", "GET").log(700, 23455) data = {"user_count":1} - self.environment.events.report_to_master.fire(client_id="fake_client", data=data) + environment.events.report_to_master.fire(client_id="fake_client", data=data) master.stats.clear_all() server.mocked_send(Message("stats", data, "fake_client")) s = master.stats.get("/", "GET") self.assertEqual(700, s.median_response_time) - locust.stats.write_csv_files(self.environment, self.STATS_BASE_NAME, full_history=True) + locust.stats.write_csv_files(environment, self.STATS_BASE_NAME, full_history=True) self.assertTrue(os.path.exists(self.STATS_FILENAME)) self.assertTrue(os.path.exists(self.STATS_HISTORY_FILENAME)) self.assertTrue(os.path.exists(self.STATS_FAILURES_FILENAME)) @@ -388,11 +389,12 @@ class TestUser(Locust): @task def t(self): self.environment.runner.stats.log_request("GET", "/", 10, 10) - runner = LocalLocustRunner(self.environment, [TestUser]) + environment = Environment(locust_classes=[TestUser]) + runner = environment.create_local_runner() runner.start(3, 5) # spawn a user every 0.2 second gevent.sleep(0.1) - greenlet = gevent.spawn(stats_writer, self.environment, self.STATS_BASE_NAME, full_history=True) + greenlet = gevent.spawn(stats_writer, environment, self.STATS_BASE_NAME, full_history=True) gevent.sleep(0.6) gevent.kill(greenlet) diff --git a/locust/test/test_web.py b/locust/test/test_web.py index b64f834c7a..5452041c37 100644 --- a/locust/test/test_web.py +++ b/locust/test/test_web.py @@ -25,8 +25,7 @@ def setUp(self): parser = get_parser(default_config_files=[]) self.environment.options = parser.parse_args([]) - self.runner = LocustRunner(self.environment, []) - self.stats = self.runner.stats + self.stats = self.environment.stats self.web_ui = WebUI(self.environment) self.web_ui.app.view_functions["request_stats"].clear_cache() diff --git a/locust/test/testcases.py b/locust/test/testcases.py index bf03229c12..7322550dd8 100644 --- a/locust/test/testcases.py +++ b/locust/test/testcases.py @@ -132,7 +132,7 @@ def setUp(self): locust.events = Events() self.environment = Environment(events=locust.events, catch_exceptions=False) - self.runner = LocustRunner(self.environment, []) + self.runner = self.environment.create_local_runner() # When running the tests in Python 3 we get warnings about unclosed sockets. # This causes tests that depends on calls to sys.stderr to fail, so we'll From 439f0b792ec2265605c92c061de52c114811c57e Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Tue, 14 Apr 2020 14:16:24 +0200 Subject: [PATCH 03/10] Automatically start the web server greenlet when WebUI is instatiated --- locust/env.py | 9 ++++++--- locust/test/test_web.py | 14 +++++--------- locust/web.py | 23 ++++++++++++++++++----- 3 files changed, 29 insertions(+), 17 deletions(-) diff --git a/locust/env.py b/locust/env.py index f307b90591..4ec01e5e0f 100644 --- a/locust/env.py +++ b/locust/env.py @@ -114,12 +114,15 @@ def create_worker_runner(self, master_host, master_port): master_port=master_port, ) - def create_web_ui(self, auth_credentials=None): + def create_web_ui(self, host="*", port=8089, auth_credentials=None): """ - Creates a WebUI instance for this Environment + Creates a WebUI instance for this Environment and start running the web server Arguments: + host: Host/interface that the web server should accept connections to. Defaults to "*" + which means all interfaces + port: Port that the web server should listen to auth_credentials: If provided (in format "username:password") basic auth will be enabled """ - self.web_ui = WebUI(self, auth_credentials=auth_credentials) + self.web_ui = WebUI(self, host, port, auth_credentials=auth_credentials) return self.web_ui diff --git a/locust/test/test_web.py b/locust/test/test_web.py index 5452041c37..bad6e9dde2 100644 --- a/locust/test/test_web.py +++ b/locust/test/test_web.py @@ -27,10 +27,8 @@ def setUp(self): self.environment.options = parser.parse_args([]) self.stats = self.environment.stats - self.web_ui = WebUI(self.environment) + self.web_ui = self.environment.create_web_ui("127.0.0.1", 0) self.web_ui.app.view_functions["request_stats"].clear_cache() - self.web_ui.app.config["BASIC_AUTH_ENABLED"] = False - gevent.spawn(lambda: self.web_ui.start("127.0.0.1", 0)) gevent.sleep(0.01) self.web_port = self.web_ui.server.server_port @@ -44,10 +42,9 @@ def test_web_ui_reference_on_environment(self): def test_web_ui_no_runner(self): env = Environment() - web_ui = WebUI(env) + web_ui = WebUI(env, "127.0.0.1", 0) + gevent.sleep(0.01) try: - gevent.spawn(lambda: web_ui.start("127.0.0.1", 0)) - gevent.sleep(0.01) response = requests.get("http://127.0.0.1:%i/" % web_ui.server.server_port) self.assertEqual(500, response.status_code) self.assertEqual("Error: Locust Environment does not have any runner", response.text) @@ -259,12 +256,11 @@ def setUp(self): super(TestWebUIAuth, self).setUp() parser = get_parser(default_config_files=[]) - self.environment.options = parser.parse_args(["--web-auth", "john:doe"]) + options = parser.parse_args(["--web-auth", "john:doe"]) self.runner = LocustRunner(self.environment, []) self.stats = self.runner.stats - self.web_ui = WebUI(self.environment, self.environment.options.web_auth) + self.web_ui = self.environment.create_web_ui("127.0.0.1", 0, auth_credentials=options.web_auth) self.web_ui.app.view_functions["request_stats"].clear_cache() - gevent.spawn(lambda: self.web_ui.start("127.0.0.1", 0)) gevent.sleep(0.01) self.web_port = self.web_ui.server.server_port diff --git a/locust/web.py b/locust/web.py index d455fcaa0d..89568935a3 100644 --- a/locust/web.py +++ b/locust/web.py @@ -9,6 +9,7 @@ from itertools import chain from time import time +import gevent from flask import Flask, make_response, jsonify, render_template, request from flask_basicauth import BasicAuth from gevent import pywsgi @@ -31,19 +32,28 @@ class WebUI: server = None """Reference to pyqsgi.WSGIServer once it's started""" - def __init__(self, environment, auth_credentials=None): + def __init__(self, environment, host, port, auth_credentials=None): """ - If auth_credentials is provided, it will enable basic auth with all the routes protected by default. - Should be supplied in the format: "user:pass". + Create WebUI instance and start running the web server in a separate greenlet (self.greenlet) + + Arguments: + environment: Reference to the curren Locust Environment + host: Host/interface that the web server should accept connections to + port: Port that the web server should listen to + auth_credentials: If provided, it will enable basic auth with all the routes protected by default. + Should be supplied in the format: "user:pass". """ environment.web_ui = self self.environment = environment + self.host = host + self.port = port app = Flask(__name__) self.app = app app.debug = True app.root_path = os.path.dirname(os.path.abspath(__file__)) self.app.config["BASIC_AUTH_ENABLED"] = False self.auth = None + self.greenlet = None if auth_credentials is not None: credentials = auth_credentials.split(':') @@ -231,9 +241,12 @@ def exceptions_csv(): response.headers["Content-type"] = "text/csv" response.headers["Content-disposition"] = disposition return response + + # start the web server + self.greenlet = gevent.spawn(self.start) - def start(self, host, port): - self.server = pywsgi.WSGIServer((host, port), self.app, log=None) + def start(self): + self.server = pywsgi.WSGIServer((self.host, self.port), self.app, log=None) self.server.serve_forever() def stop(self): From 7b173cc2ca947df8f44f28242799353b609f1adc Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Tue, 14 Apr 2020 14:28:59 +0200 Subject: [PATCH 04/10] ReStructured Text formatting of docstrings --- locust/env.py | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/locust/env.py b/locust/env.py index 4ec01e5e0f..9b2f224ab2 100644 --- a/locust/env.py +++ b/locust/env.py @@ -84,10 +84,9 @@ def create_master_runner(self, master_bind_host="*", master_bind_port=5557): """ Create a MasterLocustRunner instance for this Environment - Arguments: - master_bind_host: Interface/host that the master should use for incoming worker connections. - Defaults to "*" which means all interfaces. - master_bind_port: Port that the master should listen for incoming worker connections on + :param master_bind_host: Interface/host that the master should use for incoming worker connections. + Defaults to "*" which means all interfaces. + :param master_bind_port: Port that the master should listen for incoming worker connections on """ return self._create_runner( MasterLocustRunner, @@ -100,9 +99,8 @@ def create_worker_runner(self, master_host, master_port): """ Create a WorkerLocustRunner instance for this Environment - Arguments: - master_host: Host/IP of a running master node - master_port: Port on master node to connect to + :param master_host: Host/IP of a running master node + :param master_port: Port on master node to connect to """ # Create a new RequestStats with use_response_times_cache set to False to save some memory # and CPU cycles, since the response_times_cache is not needed for Worker nodes @@ -118,11 +116,10 @@ def create_web_ui(self, host="*", port=8089, auth_credentials=None): """ Creates a WebUI instance for this Environment and start running the web server - Arguments: - host: Host/interface that the web server should accept connections to. Defaults to "*" - which means all interfaces - port: Port that the web server should listen to - auth_credentials: If provided (in format "username:password") basic auth will be enabled + :param host: Host/interface that the web server should accept connections to. Defaults to "*" + which means all interfaces + :param port: Port that the web server should listen to + :param auth_credentials: If provided (in format "username:password") basic auth will be enabled """ self.web_ui = WebUI(self, host, port, auth_credentials=auth_credentials) return self.web_ui From c5f84c2982d5317106247dafca1083bf49b60dc7 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Tue, 14 Apr 2020 14:37:14 +0200 Subject: [PATCH 05/10] Update examples/use_as_lib.py, and include that file on "Use as lib" documentation page --- docs/use-as-lib.rst | 45 +++++------------------------------------- examples/use_as_lib.py | 42 +++++++++++++++++++-------------------- 2 files changed, 26 insertions(+), 61 deletions(-) diff --git a/docs/use-as-lib.rst b/docs/use-as-lib.rst index 076d2691b8..1c27080a21 100644 --- a/docs/use-as-lib.rst +++ b/docs/use-as-lib.rst @@ -2,45 +2,10 @@ Using Locust as a library ========================== -It's possible to use Locust as a library instead of running Locust by invoking the ``locust`` command. +It's possible to use Locust as a library, instead of running Locust using the ``locust`` command. -Here's an example:: +Here's an example: + +.. literalinclude:: ../examples/use_as_lib.py + :language: python - import gevent - from locust import HttpLocust, TaskSet, task, between - from locust.runners import LocalLocustRunner - from locust.env import Environment - from locust.stats import stats_printer - from locust.log import setup_logging - from locust.web import WebUI - - setup_logging("INFO", None) - - - class User(HttpLocust): - wait_time = between(1, 3) - host = "https://docs.locust.io" - - class task_set(TaskSet): - @task - def my_task(self): - self.client.get("/") - - @task - def task_404(self): - self.client.get("/non-existing-path") - - # setup Environment and Runner - env = Environment() - runner = LocalLocustRunner(environment=env, locust_classes=[User]) - # start a WebUI instance - web_ui = WebUI(environment=env) - gevent.spawn(lambda: web_ui.start("127.0.0.1", 8089)) - - # start a greenlet that periodically outputs the current stats - gevent.spawn(stats_printer(env.stats)) - - # start the test - runner.start(1, hatch_rate=10) - # wait for the greenlets (indefinitely) - runner.greenlet.join() diff --git a/examples/use_as_lib.py b/examples/use_as_lib.py index f2999879c0..dcf9051e01 100644 --- a/examples/use_as_lib.py +++ b/examples/use_as_lib.py @@ -12,33 +12,33 @@ class User(HttpLocust): wait_time = between(1, 3) host = "https://docs.locust.io" - - class task_set(TaskSet): - @task - def my_task(self): - self.client.get("/") - - @task - def task_404(self): - self.client.get("/non-existing-path") -# setup Environment and Runner -env = Environment() -runner = LocalLocustRunner(environment=env, locust_classes=[User]) -# start a WebUI instance -web_ui = WebUI(environment=env) -gevent.spawn(lambda: web_ui.start("127.0.0.1", 8089)) + @task + def my_task(self): + self.client.get("/") + @task + def task_404(self): + self.client.get("/non-existing-path") + +# setup Environment and Runner +env = Environment(locust_classes=[User]) +runner = env.create_local_runner() -# TODO: fix -#def on_request_success(request_type, name, response_time, response_length, **kwargs): -# report_to_grafana("%_%s" % (request_type, name), response_time) -#env.events.request_succes.add_listener(on_request_success) +# start a WebUI instance +web_ui = env.create_web_ui("127.0.0.1", 8089) # start a greenlet that periodically outputs the current stats -gevent.spawn(stats_printer(runner.stats)) +gevent.spawn(stats_printer(env.stats)) # start the test runner.start(1, hatch_rate=10) -# wait for the greenlets (indefinitely) + +# in 60 seconds stop the runner +gevent.spawn_later(10, lambda: runner.quit()) + +# wait for the greenlets runner.greenlet.join() + +# stop the web server for good measures +web_ui.stop() \ No newline at end of file From 542ef71a8dc30dfbfaf84670062836fad5174c2a Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Tue, 14 Apr 2020 16:20:51 +0200 Subject: [PATCH 06/10] Fix typo --- examples/use_as_lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/use_as_lib.py b/examples/use_as_lib.py index dcf9051e01..5e1f9c9437 100644 --- a/examples/use_as_lib.py +++ b/examples/use_as_lib.py @@ -35,7 +35,7 @@ def task_404(self): runner.start(1, hatch_rate=10) # in 60 seconds stop the runner -gevent.spawn_later(10, lambda: runner.quit()) +gevent.spawn_later(60, lambda: runner.quit()) # wait for the greenlets runner.greenlet.join() From ad82e84c6743f6c0084d8340aeb120dc5e837060 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Tue, 14 Apr 2020 16:32:47 +0200 Subject: [PATCH 07/10] Add documentation for different Runner classes. Add documentation for WebUI class. Add documentation on how to use Locust as a library. --- docs/api.rst | 20 +++++++++++++++ docs/use-as-lib.rst | 35 +++++++++++++++++++++++++-- locust/env.py | 10 ++++---- locust/runners.py | 59 +++++++++++++++++++++++++++++++++++++++++++++ locust/web.py | 37 +++++++++++++++++++++++++++- 5 files changed, 153 insertions(+), 8 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index fb77b8c1b7..3f92fe5b03 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -105,3 +105,23 @@ The event hooks are instances of the **locust.events.EventHook** class: It's highly recommended that you add a wildcard keyword argument in your event listeners to prevent your code from breaking if new arguments are added in a future version. + + +Locust Runner classes +===================== + +.. autoclass:: locust.runners.LocustRunner + :members: start, stop, quit, user_count + +.. autoclass:: locust.runners.LocalLocustRunner + +.. autoclass:: locust.runners.MasterLocustRunner + +.. autoclass:: locust.runners.WorkerLocustRunner + + +Web UI class +============ + +.. autoclass:: locust.web.WebUI + :members: diff --git a/docs/use-as-lib.rst b/docs/use-as-lib.rst index 1c27080a21..a6ccbd04d4 100644 --- a/docs/use-as-lib.rst +++ b/docs/use-as-lib.rst @@ -4,8 +4,39 @@ Using Locust as a library It's possible to use Locust as a library, instead of running Locust using the ``locust`` command. -Here's an example: +To run Locust as a library you need to create an :py:class:`Environment ` instance: + +.. code-block:: python + + from locust.env import Environment + + env = Environment(locust_classes=[MyTestUser]) + +The :py:class:`Environment ` instance's +:py:meth:`create_local_runner `, +:py:meth:`create_master_runner ` or +:py:meth:`create_worker_runner can then be used to start a +:py:class:`LocustRunner ` instance, which can be used to start a load test: + +.. code-block:: python + + runner = env.create_local_runner() + runner.start(5000, hatch_rate=20) + runner.greenlet.join() + +We could also use the :py:class:`Environment ` instance's +:py:meth:`create_web_ui ` to start a Web UI that can be used view +the stats, and to control the runner (e.g. start and stop load tests): + +.. code-block:: python + + runner = env.create_local_runner() + web_ui = Environment.create_web_ui() + web_ui.greenlet.join() + + +Full example +============ .. literalinclude:: ../examples/use_as_lib.py :language: python - diff --git a/locust/env.py b/locust/env.py index 9b2f224ab2..ff4d0c9f3a 100644 --- a/locust/env.py +++ b/locust/env.py @@ -19,7 +19,7 @@ class Environment: """Reference to RequestStats instance""" runner = None - """Reference to the LocustRunner instance""" + """Reference to the :class:`LocustRunner ` instance""" web_ui = None """Reference to the WebUI instance""" @@ -76,13 +76,13 @@ def _create_runner(self, runner_class, *args, **kwargs): def create_local_runner(self): """ - Create a LocalLocustRunner instance for this Environment + Create a :class:`LocalLocustRunner ` instance for this Environment """ return self._create_runner(LocalLocustRunner, locust_classes=self.locust_classes) def create_master_runner(self, master_bind_host="*", master_bind_port=5557): """ - Create a MasterLocustRunner instance for this Environment + Create a :class:`MasterLocustRunner ` instance for this Environment :param master_bind_host: Interface/host that the master should use for incoming worker connections. Defaults to "*" which means all interfaces. @@ -97,7 +97,7 @@ def create_master_runner(self, master_bind_host="*", master_bind_port=5557): def create_worker_runner(self, master_host, master_port): """ - Create a WorkerLocustRunner instance for this Environment + Create a :class:`WorkerLocustRunner ` instance for this Environment :param master_host: Host/IP of a running master node :param master_port: Port on master node to connect to @@ -114,7 +114,7 @@ def create_worker_runner(self, master_host, master_port): def create_web_ui(self, host="*", port=8089, auth_credentials=None): """ - Creates a WebUI instance for this Environment and start running the web server + Creates a :class:`WebUI ` instance for this Environment and start running the web server :param host: Host/interface that the web server should accept connections to. Defaults to "*" which means all interfaces diff --git a/locust/runners.py b/locust/runners.py index 7128b35f86..63706faa02 100644 --- a/locust/runners.py +++ b/locust/runners.py @@ -32,6 +32,15 @@ class LocustRunner(object): + """ + Orchestrates the load test by starting and stopping the locust users. + + Use one of the :meth:`create_local_runner `, + :meth:`create_master_runner ` or + :meth:`create_worker_runner ` methods on + the :class:`Environment ` instance to create a runner of the + desired type. + """ def __init__(self, environment, locust_classes): self.environment = environment self.locust_classes = locust_classes @@ -80,6 +89,9 @@ def errors(self): @property def user_count(self): + """ + :returns: Number of currently running locust users + """ return len(self.locusts) def cpu_log_warning(self): @@ -201,6 +213,15 @@ def monitor_cpu(self): gevent.sleep(CPU_MONITOR_INTERVAL) def start(self, locust_count, hatch_rate, wait=False): + """ + Start running a load test + + :param locust_count: Number of locust users to start + :param hatch_rate: Number of locust users to spawn per second + :param wait: If True calls to this method will block until all users are spawned. + If False (the default), a greenlet that spawns the users will be + started and the call to this method will return immediately. + """ if self.state != STATE_RUNNING and self.state != STATE_HATCHING: self.stats.clear_all() self.exceptions = {} @@ -250,6 +271,9 @@ def stepload_worker(self, hatch_rate, step_clients_growth, step_duration): gevent.sleep(step_duration) def stop(self): + """ + Stop a running load test by killing all running locusts + """ self.state = STATE_CLEANUP # if we are currently hatching locusts we need to kill the hatching greenlet first if self.hatching_greenlet and not self.hatching_greenlet.ready(): @@ -259,6 +283,9 @@ def stop(self): self.cpu_log_warning() def quit(self): + """ + Stop any running load test and kill all greenlets for the runner + """ self.stop() self.greenlet.kill(block=True) @@ -271,7 +298,13 @@ def log_exception(self, node_id, msg, formatted_tb): class LocalLocustRunner(LocustRunner): + """ + Runner for running single process load test + """ def __init__(self, environment, locust_classes): + """ + :param environment: Environment instance + """ super(LocalLocustRunner, self).__init__(environment, locust_classes) # register listener thats logs the exception for the local runner @@ -316,7 +349,20 @@ def __init__(self, id, state=STATE_INIT, heartbeat_liveness=HEARTBEAT_LIVENESS): self.cpu_warning_emitted = False class MasterLocustRunner(DistributedLocustRunner): + """ + Runner used to run distributed load tests across multiple processes and/or machines. + + MasterLocustRunner doesn't spawn any locust user greenlets itself. Instead it expects + :class:`WorkerLocustRunners ` to connect to it, which it will then direct + to start and stop locust user greenlets. Stats sent back from the + :class:`WorkerLocustRunners ` will aggregated. + """ def __init__(self, *args, master_bind_host, master_bind_port, **kwargs): + """ + :param environment: Environment instance + :param master_bind_host: Host/interface to use for incoming worker connections + :param master_bind_port: Port to use for incoming worker connections + """ super().__init__(*args, **kwargs) self.worker_cpu_warning_emitted = False self.target_user_count = None @@ -509,7 +555,20 @@ def worker_count(self): return len(self.clients.ready) + len(self.clients.hatching) + len(self.clients.running) class WorkerLocustRunner(DistributedLocustRunner): + """ + Runner used to run distributed load tests across multiple processes and/or machines. + + WorkerLocustRunner connects to a :class:`MasterLocustRunner` from which it'll receive + instructions to start and stop locust user greenlets. The WorkerLocustRunner will preiodically + take the stats generated by the running users and send back to the :class:`MasterLocustRunner`. + """ + def __init__(self, environment, *args, master_host, master_port, **kwargs): + """ + :param environment: Environment instance + :param master_host: Host/IP to use for connection to the master + :param master_port: Port to use for connecting to the master + """ super().__init__(environment, *args, **kwargs) self.client_id = socket.gethostname() + "_" + uuid4().hex self.master_host = master_host diff --git a/locust/web.py b/locust/web.py index 89568935a3..f1aa2aeb26 100644 --- a/locust/web.py +++ b/locust/web.py @@ -29,8 +29,31 @@ class WebUI: + """ + Sets up and runs a Flask web app that can start and stop load tests using the + :attr:`environment.runner ` as well as show the load test statistics + in :attr:`environment.stats ` + """ + + app = None + """ + Reference to the :class:`flask.Flask` app. Can be used to add additional web routes and customize + the Flask app in other various ways. Example:: + + from flask import request + + @web_ui.app.route("/my_custom_route") + def my_custom_route(): + return "your IP is: %s" % request.remote_addr + """ + + greenlet = None + """ + Greenlet of the running web server + """ + server = None - """Reference to pyqsgi.WSGIServer once it's started""" + """Reference to the :class:`pyqsgi.WSGIServer` instance""" def __init__(self, environment, host, port, auth_credentials=None): """ @@ -250,9 +273,21 @@ def start(self): self.server.serve_forever() def stop(self): + """ + Stop the running web server + """ self.server.stop() def auth_required_if_enabled(self, view_func): + """ + Decorator that can be used on custom route methods that will turn on Basic Auth + authentication if the ``--web-auth`` flag is used. Example:: + + @web_ui.app.route("/my_custom_route") + @web_ui.auth_required_if_enabled + def my_custom_route(): + return "custom response" + """ @wraps(view_func) def wrapper(*args, **kwargs): if self.app.config["BASIC_AUTH_ENABLED"]: From 3a2ba2e5d045bc7292bbf308bebea84e9c3cdf3a Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Tue, 14 Apr 2020 16:54:35 +0200 Subject: [PATCH 08/10] Remove unused imports --- examples/use_as_lib.py | 4 +--- locust/test/test_stats.py | 3 +-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/examples/use_as_lib.py b/examples/use_as_lib.py index 5e1f9c9437..d1a037fa0d 100644 --- a/examples/use_as_lib.py +++ b/examples/use_as_lib.py @@ -1,10 +1,8 @@ import gevent -from locust import HttpLocust, TaskSet, task, between -from locust.runners import LocalLocustRunner +from locust import HttpLocust, task, between from locust.env import Environment from locust.stats import stats_printer from locust.log import setup_logging -from locust.web import WebUI setup_logging("INFO", None) diff --git a/locust/test/test_stats.py b/locust/test/test_stats.py index f43fa60eff..8d3e4133e6 100644 --- a/locust/test/test_stats.py +++ b/locust/test/test_stats.py @@ -10,14 +10,13 @@ from locust.core import HttpLocust, TaskSet, task, Locust from locust.env import Environment from locust.inspectlocust import get_task_ratio_dict -from locust.runners import LocalLocustRunner, MasterLocustRunner from locust.rpc.protocol import Message from locust.stats import CachedResponseTimes, RequestStats, StatsEntry, diff_response_time_dicts, stats_writer from locust.test.testcases import LocustTestCase from locust.wait_time import constant from .testcases import WebserverTestCase -from .test_runners import mocked_options, mocked_rpc +from .test_runners import mocked_rpc class TestRequestStats(unittest.TestCase): From 0e84f4393e638e4ca1acd1547b6d9fc00aa7fcb2 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Tue, 14 Apr 2020 17:08:09 +0200 Subject: [PATCH 09/10] Remove locust_classes constructor argument from runner classes, and instead use Environment.locust_clases --- locust/env.py | 4 +-- locust/runners.py | 19 +++++----- locust/test/test_runners.py | 69 ++++++++++++++++++++----------------- locust/test/test_web.py | 13 ++++--- locust/test/testcases.py | 1 - 5 files changed, 55 insertions(+), 51 deletions(-) diff --git a/locust/env.py b/locust/env.py index ff4d0c9f3a..c1c4a96bd4 100644 --- a/locust/env.py +++ b/locust/env.py @@ -78,7 +78,7 @@ def create_local_runner(self): """ Create a :class:`LocalLocustRunner ` instance for this Environment """ - return self._create_runner(LocalLocustRunner, locust_classes=self.locust_classes) + return self._create_runner(LocalLocustRunner) def create_master_runner(self, master_bind_host="*", master_bind_port=5557): """ @@ -90,7 +90,6 @@ def create_master_runner(self, master_bind_host="*", master_bind_port=5557): """ return self._create_runner( MasterLocustRunner, - locust_classes=self.locust_classes, master_bind_host=master_bind_host, master_bind_port=master_bind_port, ) @@ -107,7 +106,6 @@ def create_worker_runner(self, master_host, master_port): self.stats = RequestStats(use_response_times_cache=False) return self._create_runner( WorkerLocustRunner, - locust_classes=self.locust_classes, master_host=master_host, master_port=master_port, ) diff --git a/locust/runners.py b/locust/runners.py index 63706faa02..9d3b2ba115 100644 --- a/locust/runners.py +++ b/locust/runners.py @@ -41,9 +41,8 @@ class LocustRunner(object): the :class:`Environment ` instance to create a runner of the desired type. """ - def __init__(self, environment, locust_classes): + def __init__(self, environment): self.environment = environment - self.locust_classes = locust_classes self.locusts = Group() self.greenlet = Group() self.state = STATE_INIT @@ -79,6 +78,10 @@ def __del__(self): if self.greenlet and len(self.greenlet) > 0: self.greenlet.kill(block=False) + @property + def locust_classes(self): + return self.environment.locust_classes + @property def stats(self): return self.environment.stats @@ -301,11 +304,11 @@ class LocalLocustRunner(LocustRunner): """ Runner for running single process load test """ - def __init__(self, environment, locust_classes): + def __init__(self, environment): """ :param environment: Environment instance """ - super(LocalLocustRunner, self).__init__(environment, locust_classes) + super(LocalLocustRunner, self).__init__(environment) # register listener thats logs the exception for the local runner def on_locust_error(locust_instance, exception, tb): @@ -357,13 +360,13 @@ class MasterLocustRunner(DistributedLocustRunner): to start and stop locust user greenlets. Stats sent back from the :class:`WorkerLocustRunners ` will aggregated. """ - def __init__(self, *args, master_bind_host, master_bind_port, **kwargs): + def __init__(self, environment, master_bind_host, master_bind_port): """ :param environment: Environment instance :param master_bind_host: Host/interface to use for incoming worker connections :param master_bind_port: Port to use for incoming worker connections """ - super().__init__(*args, **kwargs) + super().__init__(environment) self.worker_cpu_warning_emitted = False self.target_user_count = None self.master_bind_host = master_bind_host @@ -563,13 +566,13 @@ class WorkerLocustRunner(DistributedLocustRunner): take the stats generated by the running users and send back to the :class:`MasterLocustRunner`. """ - def __init__(self, environment, *args, master_host, master_port, **kwargs): + def __init__(self, environment, master_host, master_port): """ :param environment: Environment instance :param master_host: Host/IP to use for connection to the master :param master_port: Port to use for connecting to the master """ - super().__init__(environment, *args, **kwargs) + super().__init__(environment) self.client_id = socket.gethostname() + "_" + uuid4().hex self.master_host = master_host self.master_port = master_port diff --git a/locust/test/test_runners.py b/locust/test/test_runners.py index 70c7725300..d1db61c3ae 100644 --- a/locust/test/test_runners.py +++ b/locust/test/test_runners.py @@ -10,9 +10,9 @@ from locust.main import create_environment from locust.core import Locust, TaskSet, task from locust.env import Environment -from locust.exception import LocustError, RPCError, StopLocust +from locust.exception import RPCError, StopLocust from locust.rpc import Message -from locust.runners import LocustRunner, LocalLocustRunner, MasterLocustRunner, WorkerNode, \ +from locust.runners import LocalLocustRunner, WorkerNode, \ WorkerLocustRunner, STATE_INIT, STATE_HATCHING, STATE_RUNNING, STATE_MISSING from locust.stats import RequestStats from locust.test.testcases import LocustTestCase @@ -109,8 +109,8 @@ class CpuLocust(Locust): def cpu_task(self): for i in range(1000000): _ = 3 / 2 - environment = Environment() - runner = LocalLocustRunner(environment, [CpuLocust]) + environment = Environment(locust_classes=[CpuLocust]) + runner = LocalLocustRunner(environment) self.assertFalse(runner.cpu_warning_emitted) runner.spawn_locusts(1, 1, wait=False) sleep(2.5) @@ -179,12 +179,12 @@ def my_task(self): test_start_run = [0] - environment = Environment() + environment = Environment(locust_classes=[User]) def on_test_start(*args, **kwargs): test_start_run[0] += 1 environment.events.test_start.add_listener(on_test_start) - runner = LocalLocustRunner(environment, locust_classes=[User]) + runner = LocalLocustRunner(environment) runner.start(locust_count=3, hatch_rate=3, wait=False) runner.hatching_greenlet.get(timeout=3) @@ -199,12 +199,12 @@ def my_task(self): pass test_stop_run = [0] - environment = Environment() + environment = Environment(locust_classes=[User]) def on_test_stop(*args, **kwargs): test_stop_run[0] += 1 environment.events.test_stop.add_listener(on_test_stop) - runner = LocalLocustRunner(environment, locust_classes=[User]) + runner = LocalLocustRunner(environment) runner.start(locust_count=3, hatch_rate=3, wait=False) self.assertEqual(0, test_stop_run[0]) runner.stop() @@ -218,12 +218,12 @@ def my_task(self): pass test_stop_run = [0] - environment = Environment() + environment = Environment(locust_classes=[User]) def on_test_stop(*args, **kwargs): test_stop_run[0] += 1 environment.events.test_stop.add_listener(on_test_stop) - runner = LocalLocustRunner(environment, locust_classes=[User]) + runner = LocalLocustRunner(environment) runner.start(locust_count=3, hatch_rate=3, wait=False) self.assertEqual(0, test_stop_run[0]) runner.quit() @@ -237,12 +237,12 @@ def my_task(self): pass test_stop_run = [0] - environment = Environment() + environment = Environment(locust_classes=[User]) def on_test_stop(*args, **kwargs): test_stop_run[0] += 1 environment.events.test_stop.add_listener(on_test_stop) - runner = LocalLocustRunner(environment, locust_classes=[User]) + runner = LocalLocustRunner(environment) runner.start(locust_count=3, hatch_rate=3, wait=False) self.assertEqual(0, test_stop_run[0]) runner.stop() @@ -256,8 +256,8 @@ class User(Locust): def my_task(self): pass - environment = Environment() - runner = LocalLocustRunner(environment, [User]) + environment = Environment(locust_classes=[User]) + runner = LocalLocustRunner(environment) runner.start(locust_count=10, hatch_rate=5, wait=False) sleep(0.6) runner.start(locust_count=5, hatch_rate=5, wait=False) @@ -280,8 +280,8 @@ def my_task(self): ) sleep(2) - environment = Environment(reset_stats=True) - runner = LocalLocustRunner(environment, locust_classes=[User]) + environment = Environment(locust_classes=[User], reset_stats=True) + runner = LocalLocustRunner(environment) runner.start(locust_count=6, hatch_rate=12, wait=False) sleep(0.25) self.assertGreaterEqual(runner.stats.get("/test", "GET").num_requests, 3) @@ -304,8 +304,8 @@ def my_task(self): ) sleep(2) - environment = Environment(reset_stats=False) - runner = LocalLocustRunner(environment, locust_classes=[User]) + environment = Environment(reset_stats=False, locust_classes=[User]) + runner = LocalLocustRunner(environment) runner.start(locust_count=6, hatch_rate=12, wait=False) sleep(0.25) self.assertGreaterEqual(runner.stats.get("/test", "GET").num_requests, 3) @@ -315,8 +315,9 @@ def my_task(self): def test_runner_reference_on_environment(self): env = Environment() - runner = LocalLocustRunner(environment=env, locust_classes=[]) + runner = env.create_local_runner() self.assertEqual(env, runner.environment) + self.assertEqual(runner, env.runner) class TestMasterWorkerRunners(LocustTestCase): @@ -674,8 +675,8 @@ class MyTestLocust(Locust): tasks = [MyTaskSet] wait_time = constant(0.1) - environment = Environment() - runner = LocalLocustRunner(environment, [MyTestLocust]) + environment = Environment(locust_classes=[MyTestLocust]) + runner = LocalLocustRunner(environment) timeout = gevent.Timeout(2.0) timeout.start() @@ -797,7 +798,8 @@ class MyLocust(Locust): # set config to catch exceptions in locust users self.environment.catch_exceptions = True - runner = LocalLocustRunner(self.environment, [MyLocust]) + self.environment.locust_classes = [MyLocust] + runner = LocalLocustRunner(self.environment) l = MyLocust(self.environment) # make sure HeyAnException isn't raised @@ -839,7 +841,8 @@ def tearDown(self): def get_runner(self, environment=None, locust_classes=[]): if environment is None: environment = self.environment - return WorkerLocustRunner(environment, locust_classes, master_host="localhost", master_port=5557) + environment.locust_classes = locust_classes + return WorkerLocustRunner(environment, master_host="localhost", master_port=5557) def test_worker_stop_timeout(self): class MyTestLocust(Locust): @@ -972,23 +975,25 @@ class MyTestLocust(Locust): tasks = [MyTaskSet] wait_time = constant(0) - environment = Environment() - runner = LocalLocustRunner(environment, [MyTestLocust]) - runner.start(1, 1) + environment = Environment(locust_classes=[MyTestLocust]) + runner = environment.create_local_runner() + runner.start(1, 1, wait=False) gevent.sleep(short_time / 2) runner.quit() self.assertEqual("first", MyTaskSet.state) - environment.stop_timeout = short_time / 2 # exit with timeout - runner = LocalLocustRunner(environment, [MyTestLocust]) - runner.start(1, 1) + # exit with timeout + environment = Environment(locust_classes=[MyTestLocust], stop_timeout=short_time/2) + runner = environment.create_local_runner() + runner.start(1, 1, wait=False) gevent.sleep(short_time) runner.quit() self.assertEqual("second", MyTaskSet.state) - environment.stop_timeout = short_time * 3 # allow task iteration to complete, with some margin - runner = LocalLocustRunner(environment, [MyTestLocust]) - runner.start(1, 1) + # allow task iteration to complete, with some margin + environment = Environment(locust_classes=[MyTestLocust], stop_timeout=short_time*3) + runner = environment.create_local_runner() + runner.start(1, 1, wait=False) gevent.sleep(short_time) timeout = gevent.Timeout(short_time * 2) timeout.start() diff --git a/locust/test/test_web.py b/locust/test/test_web.py index bad6e9dde2..047d8f1715 100644 --- a/locust/test/test_web.py +++ b/locust/test/test_web.py @@ -7,11 +7,10 @@ import gevent import requests -from flask_basicauth import BasicAuth from locust import constant from locust.argument_parser import get_parser -from locust.core import Locust, TaskSet, task +from locust.core import Locust, task from locust.env import Environment from locust.runners import LocustRunner from locust.web import WebUI @@ -195,7 +194,7 @@ class MyLocust(Locust): @task(1) def my_task(self): pass - self.runner.locust_classes = [MyLocust] + self.environment.locust_classes = [MyLocust] response = requests.post( "http://127.0.0.1:%i/swarm" % self.web_port, data={'locust_count': 5, 'hatch_rate': 5}, @@ -207,7 +206,7 @@ def my_task(self): def test_host_value_from_locust_class(self): class MyLocust(Locust): host = "http://example.com" - self.environment.runner.locust_classes = [MyLocust] + self.environment.locust_classes = [MyLocust] response = requests.get("http://127.0.0.1:%i/" % self.web_port) self.assertEqual(200, response.status_code) self.assertIn("http://example.com", response.content.decode("utf-8")) @@ -218,7 +217,7 @@ class MyLocust(Locust): host = "http://example.com" class MyLocust2(Locust): host = "http://example.com" - self.environment.runner.locust_classes = [MyLocust, MyLocust2] + self.environment.locust_classes = [MyLocust, MyLocust2] response = requests.get("http://127.0.0.1:%i/" % self.web_port) self.assertEqual(200, response.status_code) self.assertIn("http://example.com", response.content.decode("utf-8")) @@ -229,7 +228,7 @@ class MyLocust(Locust): host = None class MyLocust2(Locust): host = "http://example.com" - self.environment.runner.locust_classes = [MyLocust, MyLocust2] + self.environment.locust_classes = [MyLocust, MyLocust2] response = requests.get("http://127.0.0.1:%i/" % self.web_port) self.assertEqual(200, response.status_code) self.assertNotIn("http://example.com", response.content.decode("utf-8")) @@ -257,7 +256,7 @@ def setUp(self): parser = get_parser(default_config_files=[]) options = parser.parse_args(["--web-auth", "john:doe"]) - self.runner = LocustRunner(self.environment, []) + self.runner = LocustRunner(self.environment) self.stats = self.runner.stats self.web_ui = self.environment.create_web_ui("127.0.0.1", 0, auth_credentials=options.web_auth) self.web_ui.app.view_functions["request_stats"].clear_cache() diff --git a/locust/test/testcases.py b/locust/test/testcases.py index 298cca762e..b4a7a772cc 100644 --- a/locust/test/testcases.py +++ b/locust/test/testcases.py @@ -15,7 +15,6 @@ import locust from locust.event import Events from locust.env import Environment -from locust.runners import LocustRunner from locust.test.mock_logging import MockedLoggingHandler From e32332de3de5f95414cd1c7d09cf4ba4e5fa3df8 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Thu, 16 Apr 2020 20:39:52 +0200 Subject: [PATCH 10/10] Access runner and web_ui through Environment instance instead of using return value --- docs/use-as-lib.rst | 16 ++++++++-------- examples/use_as_lib.py | 12 ++++++------ 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/docs/use-as-lib.rst b/docs/use-as-lib.rst index a6ccbd04d4..c028b75ac4 100644 --- a/docs/use-as-lib.rst +++ b/docs/use-as-lib.rst @@ -20,19 +20,19 @@ The :py:class:`Environment ` instance's .. code-block:: python - runner = env.create_local_runner() - runner.start(5000, hatch_rate=20) - runner.greenlet.join() + env.create_local_runner() + env.runner.start(5000, hatch_rate=20) + env.runner.greenlet.join() We could also use the :py:class:`Environment ` instance's -:py:meth:`create_web_ui ` to start a Web UI that can be used view -the stats, and to control the runner (e.g. start and stop load tests): +:py:meth:`create_web_ui ` method to start a Web UI that can be used +to view the stats, and to control the runner (e.g. start and stop load tests): .. code-block:: python - runner = env.create_local_runner() - web_ui = Environment.create_web_ui() - web_ui.greenlet.join() + env.create_local_runner() + env.create_web_ui() + env.web_ui.greenlet.join() Full example diff --git a/examples/use_as_lib.py b/examples/use_as_lib.py index d1a037fa0d..7ab5a5ea60 100644 --- a/examples/use_as_lib.py +++ b/examples/use_as_lib.py @@ -21,22 +21,22 @@ def task_404(self): # setup Environment and Runner env = Environment(locust_classes=[User]) -runner = env.create_local_runner() +env.create_local_runner() # start a WebUI instance -web_ui = env.create_web_ui("127.0.0.1", 8089) +env.create_web_ui("127.0.0.1", 8089) # start a greenlet that periodically outputs the current stats gevent.spawn(stats_printer(env.stats)) # start the test -runner.start(1, hatch_rate=10) +env.runner.start(1, hatch_rate=10) # in 60 seconds stop the runner -gevent.spawn_later(60, lambda: runner.quit()) +gevent.spawn_later(60, lambda: env.runner.quit()) # wait for the greenlets -runner.greenlet.join() +env.runner.greenlet.join() # stop the web server for good measures -web_ui.stop() \ No newline at end of file +env.web_ui.stop() \ No newline at end of file