From 709f189a6297d8ac75a5242bbd9b130484c107e1 Mon Sep 17 00:00:00 2001 From: Lars Holmberg Date: Fri, 21 Aug 2020 10:35:26 +0200 Subject: [PATCH 1/5] clarify logging message ("immediately" might sound like it disregards stop timeout, which it doesnt). 99% of users wont be using ramp down so making it just a little bit clearer for them is not worth confusing it for everyone else. --- locust/runners.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locust/runners.py b/locust/runners.py index 5a4ccdff70..0cb107ed38 100644 --- a/locust/runners.py +++ b/locust/runners.py @@ -199,7 +199,7 @@ def stop_users(self, user_count, stop_rate=None): if stop_rate == None or stop_rate >= user_count: sleep_time = 0 - logger.info("Stopping %i users immediately" % (user_count)) + logger.info("Stopping %i users" % (user_count)) else: sleep_time = 1.0 / stop_rate logger.info("Stopping %i users at rate of %g users/s" % (user_count, stop_rate)) From 36d874fce4338539007eebc25c2505231d633963 Mon Sep 17 00:00:00 2001 From: Lars Holmberg Date: Fri, 21 Aug 2020 14:12:31 +0200 Subject: [PATCH 2/5] Fix shape worker in headless. --- locust/main.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/locust/main.py b/locust/main.py index e1028941d2..5ba52112dd 100644 --- a/locust/main.py +++ b/locust/main.py @@ -321,6 +321,8 @@ def timelimit_stop(): # start the test if options.step_time: runner.start_stepload(options.num_users, options.spawn_rate, options.step_users, options.step_time) + if environment.shape_class: + environment.runner.start_shape() else: runner.start(options.num_users, options.spawn_rate) From 7b5589a23a2af148a187bc971edd6bd57f9655fe Mon Sep 17 00:00:00 2001 From: Lars Holmberg Date: Fri, 21 Aug 2020 15:10:20 +0200 Subject: [PATCH 3/5] Fix test for non-shape runs and add a dedicated one for shaped runs --- locust/test/mock_locustfile.py | 2 -- locust/test/test_main.py | 40 ++++++++++++++++++++++++++++++++-- 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/locust/test/mock_locustfile.py b/locust/test/mock_locustfile.py index 601f2a15d8..cc620d9c6e 100644 --- a/locust/test/mock_locustfile.py +++ b/locust/test/mock_locustfile.py @@ -32,8 +32,6 @@ class UserSubclass(HttpUser): class NotUserSubclass(): host = "http://localhost:8000" -class LoadTestShape(LoadTestShape): - pass ''' class MockedLocustfile: diff --git a/locust/test/test_main.py b/locust/test/test_main.py index b206320840..3fff9087b4 100644 --- a/locust/test/test_main.py +++ b/locust/test/test_main.py @@ -13,7 +13,7 @@ from locust.argument_parser import parse_options from locust.main import create_environment from locust.user import HttpUser, User, TaskSet -from .mock_locustfile import mock_locustfile +from .mock_locustfile import mock_locustfile, MOCK_LOUCSTFILE_CONTENT from .testcases import LocustTestCase from .util import temporary_file, get_free_tcp_port @@ -48,6 +48,7 @@ def test_load_locust_file_from_absolute_path(self): self.assertIn('UserSubclass', user_classes) self.assertNotIn('NotUserSubclass', user_classes) self.assertNotIn('LoadTestShape', user_classes) + self.assertIsNone(shape_class) def test_load_locust_file_from_relative_path(self): with mock_locustfile() as mocked: @@ -64,7 +65,18 @@ def test_return_docstring_and_user_classes(self): self.assertIn('UserSubclass', user_classes) self.assertNotIn('NotUserSubclass', user_classes) self.assertNotIn('LoadTestShape', user_classes) - + + def test_with_shape_class(self): + content = MOCK_LOUCSTFILE_CONTENT + '''class LoadTestShape(LoadTestShape): + pass + ''' + with mock_locustfile(content=content) as mocked: + docstring, user_classes, shape_class = main.load_locustfile(mocked.file_path) + self.assertEqual("This is a mock locust file for unit testing", docstring) + self.assertIn('UserSubclass', user_classes) + self.assertNotIn('NotUserSubclass', user_classes) + self.assertEqual(shape_class.__class__.__name__, 'LoadTestShape') + def test_create_environment(self): options = parse_options(args=[ "--host", "https://custom-host", @@ -190,6 +202,30 @@ def test_default_headless_spawn_options(self): ).decode("utf-8").strip() self.assertIn("Spawning 1 users at the rate 1 users/s", output) + def test_default_headless_spawn_options_with_shape(self): + content = MOCK_LOUCSTFILE_CONTENT + ''' +class LoadTestShape(LoadTestShape): + def tick(self): + run_time = self.get_run_time() + if run_time < 2: + return (10, 1) + + return None + ''' + with mock_locustfile(content=content) as mocked: + output = subprocess.check_output( + ["locust", + "-f", mocked.file_path, + "--host", "https://test.com/", + "--run-time", "1s", + "--headless"], + stderr=subprocess.STDOUT, + timeout=3, + ).decode("utf-8").strip() + self.assertIn("Shape test updating to 10 users at 1.00 spawn rate", output) + self.assertIn("Cleaning up runner...", output) + + def test_web_options(self): port = get_free_tcp_port() if platform.system() == "Darwin": From 1b9738c363a12996e379f434ff44e782ef4f7ffc Mon Sep 17 00:00:00 2001 From: Lars Holmberg Date: Sat, 22 Aug 2020 10:19:07 +0200 Subject: [PATCH 4/5] Release 1.2.2 --- CHANGELOG.md | 14 ++++++++++++-- docs/changelog.rst | 5 +++++ locust/__init__.py | 2 +- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 756d7547ff..f3d60f7858 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## [1.2.2](https://github.com/locustio/locust/tree/1.2.2) (2020-08-22) + +[Full Changelog](https://github.com/locustio/locust/compare/1.2.1...1.2.2) + +**Merged pull requests:** + +- Fix load shape worker in headless. [\#1539](https://github.com/locustio/locust/pull/1539) ([cyberw](https://github.com/cyberw)) +- Add test case for stats\_history [\#1538](https://github.com/locustio/locust/pull/1538) ([taojy123](https://github.com/taojy123)) +- Update README.md to have full links to images [\#1536](https://github.com/locustio/locust/pull/1536) ([max-rocket-internet](https://github.com/max-rocket-internet)) + ## [1.2.1](https://github.com/locustio/locust/tree/1.2.1) (2020-08-20) [Full Changelog](https://github.com/locustio/locust/compare/1.2...1.2.1) @@ -1075,6 +1085,7 @@ - Suggest Python version [\#231](https://github.com/locustio/locust/issues/231) - Changing locustfile.py on master via UI and having master / slave replication [\#209](https://github.com/locustio/locust/issues/209) - Option to prevent stats from being reset when all locusts are hatched [\#205](https://github.com/locustio/locust/issues/205) +- PUT requests are shown as GET [\#204](https://github.com/locustio/locust/issues/204) - Cannot simulate one single user [\#178](https://github.com/locustio/locust/issues/178) - Feature request: Stepped hatch rate [\#168](https://github.com/locustio/locust/issues/168) - Having a locust "die" or stop after one task [\#161](https://github.com/locustio/locust/issues/161) @@ -1302,7 +1313,6 @@ - multiple slaves of different server specs ? [\#210](https://github.com/locustio/locust/issues/210) - multiple url tests ? [\#208](https://github.com/locustio/locust/issues/208) - how to get the recorded data [\#206](https://github.com/locustio/locust/issues/206) -- PUT requests are shown as GET [\#204](https://github.com/locustio/locust/issues/204) - http proxy support [\#203](https://github.com/locustio/locust/issues/203) - error report is not included in `--logfile` nor is it available for download as a csv [\#202](https://github.com/locustio/locust/issues/202) - Stats get corrupted when the number of swarm users reaches the objective [\#201](https://github.com/locustio/locust/issues/201) @@ -1387,6 +1397,7 @@ - Fixed Docs Homebrew Link [\#143](https://github.com/locustio/locust/pull/143) ([saulshanabrook](https://github.com/saulshanabrook)) - Fix typo [\#132](https://github.com/locustio/locust/pull/132) ([rafax](https://github.com/rafax)) - Fix task ratio [\#125](https://github.com/locustio/locust/pull/125) ([sanga](https://github.com/sanga)) +- fix typo in downloadable CSV [\#100](https://github.com/locustio/locust/pull/100) ([sghill](https://github.com/sghill)) ## [v0.7](https://github.com/locustio/locust/tree/v0.7) (2014-01-20) @@ -1439,7 +1450,6 @@ - fix module and variable name clash \(traceback refers to a mod so it's a ... [\#115](https://github.com/locustio/locust/pull/115) ([sanga](https://github.com/sanga)) - Removes duplicate attribute documentation [\#106](https://github.com/locustio/locust/pull/106) ([djoume](https://github.com/djoume)) - Fixes typo in example code [\#105](https://github.com/locustio/locust/pull/105) ([djoume](https://github.com/djoume)) -- fix typo in downloadable CSV [\#100](https://github.com/locustio/locust/pull/100) ([sghill](https://github.com/sghill)) - Documented more 0.7 changes [\#90](https://github.com/locustio/locust/pull/90) ([EnTeQuAk](https://github.com/EnTeQuAk)) - include hostname in log messages [\#89](https://github.com/locustio/locust/pull/89) ([sanga](https://github.com/sanga)) - Cleanups \(deprecated code, unused imports\) [\#88](https://github.com/locustio/locust/pull/88) ([EnTeQuAk](https://github.com/EnTeQuAk)) diff --git a/docs/changelog.rst b/docs/changelog.rst index a46854b375..59b97a0046 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,11 @@ Changelog Highlights For full details of the Locust changelog, please see https://github.com/locustio/locust/blob/master/CHANGELOG.md +1.2.2 +===== + +* Bug fix (LoadTestShape in headless mode https://github.com/locustio/locust/pull/1539) + 1.2.1 ===== diff --git a/locust/__init__.py b/locust/__init__.py index a116b381cd..5d00094cfe 100644 --- a/locust/__init__.py +++ b/locust/__init__.py @@ -13,7 +13,7 @@ from .event import Events events = Events() -__version__ = "1.2.1" +__version__ = "1.2.2" __all__ = ( "SequentialTaskSet", "wait_time", From bdea5444403f9b67007bf427d28e8e640fcffb87 Mon Sep 17 00:00:00 2001 From: Max Williams Date: Mon, 24 Aug 2020 12:08:19 +0200 Subject: [PATCH 5/5] Fix stopping of LoadTestShape test --- locust/runners.py | 4 +++ locust/test/test_runners.py | 51 +++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/locust/runners.py b/locust/runners.py index 0cb107ed38..50ae71a56b 100644 --- a/locust/runners.py +++ b/locust/runners.py @@ -525,6 +525,10 @@ def start(self, user_count, spawn_rate): def stop(self): if self.state not in [STATE_INIT, STATE_STOPPED, STATE_STOPPING]: self.state = STATE_STOPPING + + if self.environment.shape_class: + self.shape_last_state = None + for client in self.clients.all: self.server.send_to_client(Message("stop", None, client.id)) self.environment.events.test_stop.fire(environment=self.environment) diff --git a/locust/test/test_runners.py b/locust/test/test_runners.py index 0666f4153d..368283229b 100644 --- a/locust/test/test_runners.py +++ b/locust/test/test_runners.py @@ -473,6 +473,57 @@ def tick(self): for worker in workers: self.assertEqual(0, worker.user_count, "Shape test has not stopped") + def test_distributed_shape_stop_and_restart(self): + """ + Test stopping and then restarting a LoadTestShape + """ + class TestUser(User): + wait_time = constant(0) + @task + def my_task(self): + pass + + class TestShape(LoadTestShape): + def tick(self): + run_time = self.get_run_time() + if run_time < 10: + return (4, 4) + else: + return None + + with mock.patch("locust.runners.WORKER_REPORT_INTERVAL", new=0.3): + master_env = Environment(user_classes=[TestUser], shape_class=TestShape()) + master_env.shape_class.reset_time() + master = master_env.create_master_runner("*", 0) + + workers = [] + for i in range(2): + worker_env = Environment(user_classes=[TestUser]) + worker = worker_env.create_worker_runner("127.0.0.1", master.server.port) + workers.append(worker) + + # Give workers time to connect + sleep(0.1) + + # Start a shape test and ensure workers have connected and started the correct amounf of users + master.start_shape() + sleep(1) + for worker in workers: + self.assertEqual(2, worker.user_count, "Shape test has not started correctly") + + # Stop the test and ensure all user count is 0 + master.stop() + sleep(1) + for worker in workers: + self.assertEqual(0, worker.user_count, "Shape test has not stopped") + + # Then restart the test again and ensure workers have connected and started the correct amounf of users + master.start_shape() + sleep(1) + for worker in workers: + self.assertEqual(2, worker.user_count, "Shape test has not started again correctly") + master.stop() + class TestMasterRunner(LocustTestCase): def setUp(self):