From 02ce1665b4ebf23d7da726bf815639a0bfe47a95 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Wed, 1 Apr 2020 01:08:07 +0200 Subject: [PATCH 01/11] Change how tasks and TaskSets are declared on Locust classes. * Let users specify tasks directly under a Locust class, just as one would do it on a TaskSet class. These tasks will get the Locust instance as argument when executed. * Removed Locust.task_set from the public API, and instead let users use either the tasks attribute or the @task decorator. * Introduce a Locust.abstract boolean attribute. If it's set to True the Locust class is meant to be used as a base class, and users of that type can not be spawned directly from it. --- docs/api.rst | 4 +- examples/basic.py | 2 +- examples/browse_docs_sequence_test.py | 2 +- examples/browse_docs_test.py | 2 +- examples/custom_wait_function.py | 4 +- examples/dynamice_user_credentials.py | 2 +- examples/events.py | 2 +- examples/fast_http_locust.py | 2 +- examples/multiple_hosts.py | 2 +- examples/semaphore_wait.py | 2 +- locust/core.py | 378 +++++++++++++++----------- locust/inspectlocust.py | 4 +- locust/main.py | 4 +- locust/runners.py | 6 +- locust/test/mock_locustfile.py | 2 +- locust/test/test_fasthttp.py | 2 +- locust/test/test_locust_class.py | 113 +++++--- locust/test/test_main.py | 6 +- locust/test/test_runners.py | 70 ++--- locust/test/test_taskratio.py | 44 ++- 20 files changed, 376 insertions(+), 277 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index af262feaf8..8855461c60 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -7,13 +7,13 @@ Locust class ============ .. autoclass:: locust.core.Locust - :members: wait_time, task_set, weight + :members: wait_time, tasks, weight HttpLocust class ================ .. autoclass:: locust.core.HttpLocust - :members: wait_time, task_set, client + :members: wait_time, tasks, client TaskSet class diff --git a/examples/basic.py b/examples/basic.py index 86d94e1efa..b73d7b44eb 100644 --- a/examples/basic.py +++ b/examples/basic.py @@ -22,4 +22,4 @@ class WebsiteUser(HttpLocust): """ host = "http://127.0.0.1:8089" wait_time = between(2, 5) - task_set = UserTasks + tasks = [UserTasks] diff --git a/examples/browse_docs_sequence_test.py b/examples/browse_docs_sequence_test.py index f3a7b4651a..5c3326e631 100644 --- a/examples/browse_docs_sequence_test.py +++ b/examples/browse_docs_sequence_test.py @@ -39,7 +39,7 @@ def load_sub_page(self): class AwesomeUser(HttpLocust): - task_set = BrowseDocumentationSequence + tasks = [BrowseDocumentationSequence] host = "https://docs.locust.io/en/latest/" # we assume someone who is browsing the Locust docs, diff --git a/examples/browse_docs_test.py b/examples/browse_docs_test.py index 9f2401e4a6..1d93d2291a 100644 --- a/examples/browse_docs_test.py +++ b/examples/browse_docs_test.py @@ -38,7 +38,7 @@ def load_sub_page(self): class AwesomeUser(HttpLocust): - task_set = BrowseDocumentation + tasks = [BrowseDocumentation] host = "https://docs.locust.io/en/latest/" # we assume someone who is browsing the Locust docs, diff --git a/examples/custom_wait_function.py b/examples/custom_wait_function.py index 9bbc8105dc..2494c1ac4d 100644 --- a/examples/custom_wait_function.py +++ b/examples/custom_wait_function.py @@ -24,7 +24,7 @@ class WebsiteUser(HttpLocust): # Most task inter-arrival times approximate to exponential distributions # We will model this wait time as exponentially distributed with a mean of 1 second wait_time = lambda self: random.expovariate(1) - task_set = UserTasks + tasks = [UserTasks] def strictExp(min_wait,max_wait,mu=1): """ @@ -44,7 +44,7 @@ class StrictWebsiteUser(HttpLocust): """ host = "http://127.0.0.1:8089" wait_time = lambda self: strictExp(3, 7) - task_set = UserTasks + tasks = [UserTasks] diff --git a/examples/dynamice_user_credentials.py b/examples/dynamice_user_credentials.py index 21029e12e4..aed2052d14 100644 --- a/examples/dynamice_user_credentials.py +++ b/examples/dynamice_user_credentials.py @@ -20,5 +20,5 @@ def some_task(self): self.client.get("/protected/resource") class User(HttpLocust): - task_set = UserBehaviour + tasks = [UserBehaviour] wait_time = between(5, 60) diff --git a/examples/events.py b/examples/events.py index b87d71da59..1db81aaf90 100644 --- a/examples/events.py +++ b/examples/events.py @@ -21,7 +21,7 @@ def stats(l): class WebsiteUser(HttpLocust): host = "http://127.0.0.1:8089" wait_time = between(2, 5) - task_set = MyTaskSet + tasks = [MyTaskSet] stats = {"content-length":0} diff --git a/examples/fast_http_locust.py b/examples/fast_http_locust.py index 95eaf01f34..8b737edeba 100644 --- a/examples/fast_http_locust.py +++ b/examples/fast_http_locust.py @@ -19,5 +19,5 @@ class WebsiteUser(FastHttpLocust): """ host = "http://127.0.0.1:8089" wait_time = between(2, 5) - task_set = UserTasks + tasks = [UserTasks] diff --git a/examples/multiple_hosts.py b/examples/multiple_hosts.py index 4101611c75..6123983471 100644 --- a/examples/multiple_hosts.py +++ b/examples/multiple_hosts.py @@ -27,4 +27,4 @@ class WebsiteUser(MultipleHostsLocust): """ host = "http://127.0.0.1:8089" wait_time = between(2, 5) - task_set = UserTasks + tasks = [UserTasks] diff --git a/examples/semaphore_wait.py b/examples/semaphore_wait.py index 1f7275d7bb..0ac7f97dcd 100644 --- a/examples/semaphore_wait.py +++ b/examples/semaphore_wait.py @@ -23,4 +23,4 @@ def index(self): class WebsiteUser(HttpLocust): host = "http://127.0.0.1:8089" wait_time = between(2, 5) - task_set = UserTasks + tasks = [UserTasks] diff --git a/locust/core.py b/locust/core.py index c228af1970..a189ca7e9f 100644 --- a/locust/core.py +++ b/locust/core.py @@ -26,7 +26,7 @@ def task(weight=1): """ - Used as a convenience decorator to be able to declare tasks for a TaskSet + Used as a convenience decorator to be able to declare tasks for a Locust or a TaskSet inline in the class. Example:: class ForumPage(TaskSet): @@ -96,141 +96,35 @@ def __getattr__(self, _): raise LocustError("No client instantiated. Did you intend to inherit from HttpLocust?") -class Locust(object): +def get_tasks_from_base_classes(bases, class_dict): """ - Represents a "user" which is to be hatched and attack the system that is to be load tested. - - The behaviour of this user is defined by the task_set attribute, which should point to a - :py:class:`TaskSet ` class. - - This class should usually be subclassed by a class that defines some kind of client. For - example when load testing an HTTP system, you probably want to use the - :py:class:`HttpLocust ` class. + Function used by both TaskSetMeta and LocustMeta for collecting all declared tasks + on the TaskSet/Locust class and all it's base classes """ + new_tasks = [] + for base in bases: + if hasattr(base, "tasks") and base.tasks: + new_tasks += base.tasks - host = None - """Base hostname to swarm. i.e: http://127.0.0.1:1234""" - - min_wait = None - """Deprecated: Use wait_time instead. Minimum waiting time between the execution of locust tasks""" - - max_wait = None - """Deprecated: Use wait_time instead. Maximum waiting time between the execution of locust tasks""" - - wait_time = None - """ - Method that returns the time (in seconds) between the execution of locust tasks. - Can be overridden for individual TaskSets. - - Example:: - - from locust import Locust, between - class User(Locust): - wait_time = between(3, 25) - """ - - wait_function = None - """ - .. warning:: - - DEPRECATED: Use wait_time instead. Note that the new wait_time method should return seconds and not milliseconds. - - Method that returns the time between the execution of locust tasks in milliseconds - """ - - task_set = None - """TaskSet class that defines the execution behaviour of this locust""" - - weight = 10 - """Probability of locust being chosen. The higher the weight, the greater is the chance of it being chosen.""" - - client = NoClientWarningRaiser() - _catch_exceptions = True - _setup_has_run = False # Internal state to see if we have already run - _teardown_is_set = False # Internal state to see if we have already run - _lock = gevent.lock.Semaphore() # Lock to make sure setup is only run once - _state = False - - def __init__(self, environment): - super(Locust, self).__init__() - # check if deprecated wait API is used - deprecation.check_for_deprecated_wait_api(self) - - self.environment = environment + if "tasks" in class_dict and class_dict["tasks"] is not None: + tasks = class_dict["tasks"] + if isinstance(tasks, dict): + tasks = tasks.items() - with self._lock: - if hasattr(self, "setup") and self._setup_has_run is False: - self._set_setup_flag() - try: - self.setup() - except Exception as e: - self.environment.events.locust_error.fire(locust_instance=self, exception=e, tb=sys.exc_info()[2]) - logger.error("%s\n%s", e, traceback.format_exc()) - if hasattr(self, "teardown") and self._teardown_is_set is False: - self._set_teardown_flag() - self.environment.events.quitting.add_listener(self.teardown) - - @classmethod - def _set_setup_flag(cls): - cls._setup_has_run = True - - @classmethod - def _set_teardown_flag(cls): - cls._teardown_is_set = True - - def run(self, runner=None): - task_set_instance = self.task_set(self) - try: - task_set_instance.run() - except StopLocust: - pass - except (RescheduleTask, RescheduleTaskImmediately) as e: - raise LocustError("A task inside a Locust class' main TaskSet (`%s.task_set` of type `%s`) seems to have called interrupt() or raised an InterruptTaskSet exception. The interrupt() function is used to hand over execution to a parent TaskSet, and should never be called in the main TaskSet which a Locust class' task_set attribute points to." % (type(self).__name__, self.task_set.__name__)) from e - except GreenletExit as e: - if runner: - runner.state = STATE_CLEANUP - # Run the task_set on_stop method, if it has one - if hasattr(task_set_instance, "on_stop"): - task_set_instance.on_stop() - raise # Maybe something relies on this except being raised? - - -class HttpLocust(Locust): - """ - Represents an HTTP "user" which is to be hatched and attack the system that is to be load tested. - - The behaviour of this user is defined by the task_set attribute, which should point to a - :py:class:`TaskSet ` class. + for task in tasks: + if isinstance(task, tuple): + task, count = task + for i in range(count): + new_tasks.append(task) + else: + new_tasks.append(task) - This class creates a *client* attribute on instantiation which is an HTTP client with support - for keeping a user session between requests. - """ + for item in class_dict.values(): + if "locust_task_weight" in dir(item): + for i in range(0, item.locust_task_weight): + new_tasks.append(item) - client = None - """ - Instance of HttpSession that is created upon instantiation of Locust. - The client support cookies, and therefore keeps the session between HTTP requests. - """ - - trust_env = False - """ - Look for proxy settings will slow down the default http client. - It's the default behavior of the requests library. - We don't need this feature most of the time, so disable it by default. - """ - - def __init__(self, *args, **kwargs): - super(HttpLocust, self).__init__(*args, **kwargs) - if self.host is None: - raise LocustError("You must specify the base host. Either in the host attribute in the Locust class, or on the command line using the --host option.") - - session = HttpSession( - base_url=self.host, - request_success=self.environment.events.request_success, - request_failure=self.environment.events.request_failure, - ) - session.trust_env = self.trust_env - self.client = session + return new_tasks class TaskSetMeta(type): @@ -239,33 +133,10 @@ class TaskSetMeta(type): ratio using an {task:int} dict, or a [(task0,int), ..., (taskN,int)] list. """ - def __new__(mcs, classname, bases, classDict): - new_tasks = [] - for base in bases: - if hasattr(base, "tasks") and base.tasks: - new_tasks += base.tasks - - if "tasks" in classDict and classDict["tasks"] is not None: - tasks = classDict["tasks"] - if isinstance(tasks, dict): - tasks = tasks.items() - - for task in tasks: - if isinstance(task, tuple): - task, count = task - for i in range(count): - new_tasks.append(task) - else: - new_tasks.append(task) - - for item in classDict.values(): - if hasattr(item, "locust_task_weight"): - for i in range(0, item.locust_task_weight): - new_tasks.append(item) - - classDict["tasks"] = new_tasks - - return type.__new__(mcs, classname, bases, classDict) + def __new__(mcs, classname, bases, class_dict): + class_dict["tasks"] = get_tasks_from_base_classes(bases, class_dict) + return type.__new__(mcs, classname, bases, class_dict) + class TaskSet(object, metaclass=TaskSetMeta): """ @@ -288,7 +159,7 @@ class TaskSet(object, metaclass=TaskSetMeta): tasks = [] """ - List with python callables that represents a locust user task. + Collection of python callables and/or TaskSet classes that the Locust user(s) will run. If tasks is a list, the task to be performed will be picked randomly. @@ -548,3 +419,194 @@ def get_next_task(self): task = self.tasks[self._index] self._index = (self._index + 1) % len(self.tasks) return task + + +class DefaultTaskSet(TaskSet): + """ + Default root TaskSet that executes tasks in Locust.tasks. + It executes tasks declared directly on the Locust with the Locust user instance as the task argument. + """ + def get_next_task(self): + return random.choice(self.locust.tasks) + + def execute_task(self, task, *args, **kwargs): + if hasattr(task, "tasks") and issubclass(task, TaskSet): + # task is (nested) TaskSet class + task(self.locust).run(*args, **kwargs) + else: + # task is a function + task(self.locust, *args, **kwargs) + + +class LocustMeta(type): + """ + Meta class for the main Locust class. It's used to allow Locust classes to specify task execution + ratio using an {task:int} dict, or a [(task0,int), ..., (taskN,int)] list. + """ + def __new__(mcs, classname, bases, class_dict): + # gather any tasks that is declared on the class (or it's bases) + tasks = get_tasks_from_base_classes(bases, class_dict) + class_dict["tasks"] = tasks + + if not class_dict.get("abstract"): + # Not a base class + class_dict["abstract"] = False + + return type.__new__(mcs, classname, bases, class_dict) + + +class Locust(object, metaclass=LocustMeta): + """ + Represents a "user" which is to be hatched and attack the system that is to be load tested. + + The behaviour of this user is defined by it's tasks. Tasks can be declared either directly on the + class by using the :py:func:`@task decorator ` on the methods, or by setting + the :py:attr:`tasks attribute `. + + This class should usually be subclassed by a class that defines some kind of client. For + example when load testing an HTTP system, you probably want to use the + :py:class:`HttpLocust ` class. + """ + + host = None + """Base hostname to swarm. i.e: http://127.0.0.1:1234""" + + min_wait = None + """Deprecated: Use wait_time instead. Minimum waiting time between the execution of locust tasks""" + + max_wait = None + """Deprecated: Use wait_time instead. Maximum waiting time between the execution of locust tasks""" + + wait_time = None + """ + Method that returns the time (in seconds) between the execution of locust tasks. + Can be overridden for individual TaskSets. + + Example:: + + from locust import Locust, between + class User(Locust): + wait_time = between(3, 25) + """ + + wait_function = None + """ + .. warning:: + + DEPRECATED: Use wait_time instead. Note that the new wait_time method should return seconds and not milliseconds. + + Method that returns the time between the execution of locust tasks in milliseconds + """ + + tasks = [] + """ + Collection of python callables and/or TaskSet classes that the Locust user(s) will run. + + If tasks is a list, the task to be performed will be picked randomly. + + If tasks is a *(callable,int)* list of two-tuples, or a {callable:int} dict, + the task to be performed will be picked randomly, but each task will be weighted + according to it's corresponding int value. So in the following case *ThreadPage* will + be fifteen times more likely to be picked than *write_post*:: + + class ForumPage(TaskSet): + tasks = {ThreadPage:15, write_post:1} + """ + + weight = 10 + """Probability of locust being chosen. The higher the weight, the greater is the chance of it being chosen.""" + + abstract = True + """If abstract is True it the class is meant to be subclassed (users of this class itself will not be spawned during a test)""" + + _task_set = DefaultTaskSet + """TaskSet class that defines the execution behaviour of this locust""" + + client = NoClientWarningRaiser() + _catch_exceptions = True + _setup_has_run = False # Internal state to see if we have already run + _teardown_is_set = False # Internal state to see if we have already run + _lock = gevent.lock.Semaphore() # Lock to make sure setup is only run once + _state = False + + def __init__(self, environment): + super(Locust, self).__init__() + # check if deprecated wait API is used + deprecation.check_for_deprecated_wait_api(self) + + self.environment = environment + + with self._lock: + if hasattr(self, "setup") and self._setup_has_run is False: + self._set_setup_flag() + try: + self.setup() + except Exception as e: + self.environment.events.locust_error.fire(locust_instance=self, exception=e, tb=sys.exc_info()[2]) + logger.error("%s\n%s", e, traceback.format_exc()) + if hasattr(self, "teardown") and self._teardown_is_set is False: + self._set_teardown_flag() + self.environment.events.quitting.add_listener(self.teardown) + + @classmethod + def _set_setup_flag(cls): + cls._setup_has_run = True + + @classmethod + def _set_teardown_flag(cls): + cls._teardown_is_set = True + + def run(self, runner=None): + task_set_instance = self._task_set(self) + try: + task_set_instance.run() + except StopLocust: + pass + except GreenletExit as e: + if runner: + runner.state = STATE_CLEANUP + # Run the task_set on_stop method, if it has one + if hasattr(task_set_instance, "on_stop"): + task_set_instance.on_stop() + raise # Maybe something relies on this except being raised? + + +class HttpLocust(Locust): + """ + Represents an HTTP "user" which is to be hatched and attack the system that is to be load tested. + + The behaviour of this user is defined by it's tasks. Tasks can be declared either directly on the + class by using the :py:func:`@task decorator ` on the methods, or by setting + the :py:attr:`tasks attribute `. + + This class creates a *client* attribute on instantiation which is an HTTP client with support + for keeping a user session between requests. + """ + + abstract = True + + client = None + """ + Instance of HttpSession that is created upon instantiation of Locust. + The client support cookies, and therefore keeps the session between HTTP requests. + """ + + trust_env = False + """ + Look for proxy settings will slow down the default http client. + It's the default behavior of the requests library. + We don't need this feature most of the time, so disable it by default. + """ + + def __init__(self, *args, **kwargs): + super(HttpLocust, self).__init__(*args, **kwargs) + if self.host is None: + raise LocustError("You must specify the base host. Either in the host attribute in the Locust class, or on the command line using the --host option.") + + session = HttpSession( + base_url=self.host, + request_success=self.environment.events.request_success, + request_failure=self.environment.events.request_failure, + ) + session.trust_env = self.trust_env + self.client = session diff --git a/locust/inspectlocust.py b/locust/inspectlocust.py index d091d3d5e5..7bd6654da6 100644 --- a/locust/inspectlocust.py +++ b/locust/inspectlocust.py @@ -37,9 +37,7 @@ def get_task_ratio_dict(tasks, total=False, parent_ratio=1.0): for locust, ratio in ratio_percent.items(): d = {"ratio":ratio} if inspect.isclass(locust): - if issubclass(locust, Locust): - T = locust.task_set.tasks - elif issubclass(locust, TaskSet): + if issubclass(locust, (Locust, TaskSet)): T = locust.tasks if total: d["tasks"] = get_task_ratio_dict(T, total, ratio) diff --git a/locust/main.py b/locust/main.py index 7529171a2f..79a1f25344 100644 --- a/locust/main.py +++ b/locust/main.py @@ -35,9 +35,7 @@ def is_locust(tup): return bool( inspect.isclass(item) and issubclass(item, Locust) - and hasattr(item, "task_set") - and getattr(item, "task_set") - and not name.startswith('_') + and item.abstract == False ) diff --git a/locust/runners.py b/locust/runners.py index ddfea5152d..6d89d54192 100644 --- a/locust/runners.py +++ b/locust/runners.py @@ -87,13 +87,9 @@ def weight_locusts(self, amount): returns a list "bucket" with the weighted locusts """ bucket = [] - weight_sum = sum((locust.weight for locust in self.locust_classes if locust.task_set)) + weight_sum = sum((locust.weight for locust in self.locust_classes if locust._task_set)) residuals = {} for locust in self.locust_classes: - if not locust.task_set: - warnings.warn("Notice: Found Locust class (%s) got no task_set. Skipping..." % locust.__name__) - continue - if self.environment.host is not None: locust.host = self.environment.host diff --git a/locust/test/mock_locustfile.py b/locust/test/mock_locustfile.py index f912e5eee4..fb4dc3f1e0 100644 --- a/locust/test/mock_locustfile.py +++ b/locust/test/mock_locustfile.py @@ -26,7 +26,7 @@ class UserTasks(TaskSet): class LocustSubclass(HttpLocust): host = "http://127.0.0.1:8089" wait_time = between(2, 5) - task_set = UserTasks + tasks = [UserTasks] class NotLocustSubclass(): diff --git a/locust/test/test_fasthttp.py b/locust/test/test_fasthttp.py index e6e8084a6d..42d94e8389 100644 --- a/locust/test/test_fasthttp.py +++ b/locust/test/test_fasthttp.py @@ -382,7 +382,7 @@ def interrupted_task(self): raise InterruptTaskSet() class MyLocust(FastHttpLocust): host = "http://127.0.0.1:%i" % self.port - task_set = MyTaskSet + tasks = [MyTaskSet] l = MyLocust(self.environment) ts = MyTaskSet(l) diff --git a/locust/test/test_locust_class.py b/locust/test/test_locust_class.py index cfbdb28b4d..3f455b7afe 100644 --- a/locust/test/test_locust_class.py +++ b/locust/test/test_locust_class.py @@ -2,7 +2,7 @@ from locust.core import HttpLocust, Locust, TaskSet, task from locust.env import Environment from locust.exception import (CatchResponseError, LocustError, RescheduleTask, - RescheduleTaskImmediately) + RescheduleTaskImmediately, StopLocust) from locust.wait_time import between, constant from .testcases import LocustTestCase, WebserverTestCase @@ -37,12 +37,12 @@ class MyTasks(TaskSet): class User(Locust): wait_time = constant(0) - task_set = MyTasks + tasks = [MyTasks] _catch_exceptions = False l = MyTasks(User(self.environment)) self.assertRaisesRegex(Exception, "No tasks defined.*", l.run) - l.task_set = None + l.tasks = [] self.assertRaisesRegex(Exception, "No tasks defined.*", l.run) def test_task_decorator_ratio(self): @@ -72,6 +72,67 @@ def t4(self): self.assertEqual(t2_count, 2) self.assertEqual(t3_count, 3) self.assertEqual(t4_count, 13) + + def test_tasks_on_locust(self): + class MyLocust(Locust): + @task(2) + def t1(self): + pass + @task(3) + def t2(self): + pass + l = MyLocust(self.environment) + self.assertEqual(2, len([t for t in l.tasks if t.__name__ == MyLocust.t1.__name__])) + self.assertEqual(3, len([t for t in l.tasks if t.__name__ == MyLocust.t2.__name__])) + + def test_tasks_on_abstract_locust(self): + class AbstractLocust(Locust): + abstract = True + @task(2) + def t1(self): + pass + class MyLocust(AbstractLocust): + @task(3) + def t2(self): + pass + l = MyLocust(self.environment) + self.assertEqual(2, len([t for t in l.tasks if t.__name__ == MyLocust.t1.__name__])) + self.assertEqual(3, len([t for t in l.tasks if t.__name__ == MyLocust.t2.__name__])) + + def test_taskset_on_abstract_locust(self): + v = [0] + class AbstractLocust(Locust): + abstract = True + @task + class task_set(TaskSet): + @task + def t1(self): + v[0] = 1 + raise StopLocust() + class MyLocust(AbstractLocust): + pass + l = MyLocust(self.environment) + # check that the Locust can be run + l.run() + self.assertEqual(1, v[0]) + + def test_task_decorator_on_taskset(self): + state = [0] + class MyLocust(Locust): + wait_time = constant(0) + @task + def t1(self): + pass + @task + class MyTaskSet(TaskSet): + @task + def subtask(self): + state[0] = 1 + raise StopLocust() + + self.assertEqual([MyLocust.t1, MyLocust.MyTaskSet], MyLocust.tasks) + MyLocust(self.environment).run() + self.assertEqual(1, state[0]) def test_on_start(self): class MyTasks(TaskSet): @@ -256,44 +317,6 @@ class MyTaskSet(TaskSet): self.assertEqual((1,2,3), self.locust.sub_taskset_args) self.assertEqual({"hello":"world"}, self.locust.sub_taskset_kwargs) - def test_interrupt_taskset_in_main_taskset(self): - class MyTaskSet(TaskSet): - @task - def interrupted_task(self): - raise InterruptTaskSet(reschedule=False) - class MyLocust(Locust): - host = "http://127.0.0.1" - task_set = MyTaskSet - - class MyTaskSet2(TaskSet): - @task - def interrupted_task(self): - self.interrupt() - class MyLocust2(Locust): - host = "http://127.0.0.1" - task_set = MyTaskSet2 - - l = MyLocust(Environment()) - l2 = MyLocust2(Environment()) - self.assertRaises(LocustError, lambda: l.run()) - self.assertRaises(LocustError, lambda: l2.run()) - - try: - l.run() - except LocustError as e: - self.assertTrue("MyLocust" in e.args[0], "MyLocust should have been referred to in the exception message") - self.assertTrue("MyTaskSet" in e.args[0], "MyTaskSet should have been referred to in the exception message") - except: - raise - - try: - l2.run() - except LocustError as e: - self.assertTrue("MyLocust2" in e.args[0], "MyLocust2 should have been referred to in the exception message") - self.assertTrue("MyTaskSet2" in e.args[0], "MyTaskSet2 should have been referred to in the exception message") - except: - raise - def test_on_start_interrupt(self): class SubTaskSet(TaskSet): def on_start(self): @@ -304,7 +327,7 @@ def on_start(self): class MyLocust(Locust): host = "" - task_set = SubTaskSet + tasks = [SubTaskSet] l = MyLocust(Environment()) task_set = SubTaskSet(l) @@ -332,7 +355,7 @@ class RootTaskSet(TaskSet): class MyLocust(Locust): host = "" - task_set = RootTaskSet + tasks = [RootTaskSet] l = MyLocust(Environment()) l.run() @@ -459,7 +482,7 @@ def t1(self): class MyLocust(Locust): host = "http://127.0.0.1:%i" % self.port - task_set = MyTaskSet + tasks = [MyTaskSet] my_locust = MyLocust(self.environment) self.assertRaises(LocustError, lambda: my_locust.client.get("/")) @@ -549,7 +572,7 @@ def interrupted_task(self): raise InterruptTaskSet() class MyLocust(HttpLocust): host = "http://127.0.0.1:%i" % self.port - task_set = MyTaskSet + tasks = [MyTaskSet] l = MyLocust(self.environment) ts = MyTaskSet(l) diff --git a/locust/test/test_main.py b/locust/test/test_main.py index 234fd59af7..afe8bf3110 100644 --- a/locust/test/test_main.py +++ b/locust/test/test_main.py @@ -19,16 +19,16 @@ class MyTaskSet(TaskSet): pass class MyHttpLocust(HttpLocust): - task_set = MyTaskSet + tasks = [MyTaskSet] class MyLocust(Locust): - task_set = MyTaskSet + tasks = [MyTaskSet] self.assertTrue(main.is_locust(("MyHttpLocust", MyHttpLocust))) self.assertTrue(main.is_locust(("MyLocust", MyLocust))) class ThriftLocust(Locust): - pass + abstract = True self.assertFalse(main.is_locust(("ThriftLocust", ThriftLocust))) diff --git a/locust/test/test_runners.py b/locust/test/test_runners.py index ae6c16d09b..f4dff6f862 100644 --- a/locust/test/test_runners.py +++ b/locust/test/test_runners.py @@ -9,7 +9,7 @@ from locust.main import create_environment from locust.core import Locust, TaskSet, task from locust.env import Environment -from locust.exception import LocustError +from locust.exception import LocustError, StopLocust from locust.rpc import Message from locust.runners import LocustRunner, LocalLocustRunner, MasterLocustRunner, SlaveNode, \ SlaveLocustRunner, STATE_INIT, STATE_HATCHING, STATE_RUNNING, STATE_MISSING @@ -143,6 +143,7 @@ def test_kill_locusts(self): triggered = [False] class BaseLocust(Locust): wait_time = constant(1) + @task class task_set(TaskSet): @task def trigger(self): @@ -167,6 +168,7 @@ class User(Locust): def setup(self): User.setup_run_count += 1 raise Exception("some exception") + @task class task_set(TaskSet): @task def my_task(self): @@ -192,6 +194,7 @@ class User(Locust): task_run_count = 0 locust_error_count = 0 wait_time = constant(1) + @task class task_set(TaskSet): def setup(self): User.setup_run_count += 1 @@ -217,10 +220,9 @@ def on_locust_error(*args, **kwargs): def test_change_user_count_during_hatching(self): class User(Locust): wait_time = constant(1) - class task_set(TaskSet): - @task - def my_task(self): - pass + @task + def my_task(self): + pass environment = Environment(options=mocked_options()) runner = LocalLocustRunner(environment, [User]) @@ -234,6 +236,7 @@ def my_task(self): def test_reset_stats(self): class User(Locust): wait_time = constant(0) + @task class task_set(TaskSet): @task def my_task(self): @@ -257,6 +260,7 @@ def my_task(self): def test_no_reset_stats(self): class User(Locust): wait_time = constant(0) + @task class task_set(TaskSet): @task def my_task(self): @@ -510,7 +514,7 @@ def my_task(self): pass class MyTestLocust(Locust): - task_set = MyTaskSet + tasks = [MyTaskSet] wait_time = constant(0.1) environment = Environment(options=mocked_options()) @@ -596,10 +600,9 @@ class HeyAnException(Exception): pass class MyLocust(Locust): - class task_set(TaskSet): - @task - def will_error(self): - raise HeyAnException(":(") + @task + def will_error(self): + raise HeyAnException(":(") runner = LocalLocustRunner(self.environment, [MyLocust]) @@ -634,19 +637,18 @@ def will_error(self): @task(1) def will_stop(self): - self.interrupt() + raise StopLocust() class MyLocust(Locust): wait_time = constant(0.01) - task_set = MyTaskSet + tasks = [MyTaskSet] runner = LocalLocustRunner(self.environment, [MyLocust]) l = MyLocust(self.environment) - l.task_set._task_queue = [l.task_set.will_error, l.task_set.will_stop] - self.assertRaises(LocustError, l.run) # make sure HeyAnException isn't raised - l.task_set._task_queue = [l.task_set.will_error, l.task_set.will_stop] - self.assertRaises(LocustError, l.run) # make sure HeyAnException isn't raised + # make sure HeyAnException isn't raised + l.run() + l.run() # make sure we got two entries in the error log self.assertEqual(2, len(self.mocked_log.error)) @@ -675,13 +677,12 @@ def get_runner(self, environment=None, locust_classes=[]): def test_slave_stop_timeout(self): class MyTestLocust(Locust): _test_state = 0 - class task_set(TaskSet): - wait_time = constant(0) - @task - def the_task(self): - MyTestLocust._test_state = 1 - gevent.sleep(0.2) - MyTestLocust._test_state = 2 + wait_time = constant(0) + @task + def the_task(self): + MyTestLocust._test_state = 1 + gevent.sleep(0.2) + MyTestLocust._test_state = 2 with mock.patch("locust.rpc.rpc.Client", mocked_rpc()) as client: environment = Environment(options=mocked_options()) @@ -711,13 +712,12 @@ def the_task(self): def test_slave_without_stop_timeout(self): class MyTestLocust(Locust): _test_state = 0 - class task_set(TaskSet): - wait_time = constant(0) - @task - def the_task(self): - MyTestLocust._test_state = 1 - gevent.sleep(0.2) - MyTestLocust._test_state = 2 + wait_time = constant(0) + @task + def the_task(self): + MyTestLocust._test_state = 1 + gevent.sleep(0.2) + MyTestLocust._test_state = 2 with mock.patch("locust.rpc.rpc.Client", mocked_rpc()) as client: options = mocked_options() @@ -802,7 +802,7 @@ def my_task(self): MyTaskSet.state = "third" # should only run when run time + stop_timeout is > short_time * 2 class MyTestLocust(Locust): - task_set = MyTaskSet + tasks = [MyTaskSet] wait_time = constant(0) options = mocked_options() @@ -849,7 +849,7 @@ def my_task(self): MyTaskSet.my_task_run = True class MyTestLocust(Locust): - task_set = MyTaskSet + tasks = [MyTaskSet] wait_time = constant(0) environment = create_environment(mocked_options()) @@ -870,7 +870,7 @@ def my_task(self): pass class MyTestLocust(Locust): - task_set = MyTaskSet + tasks = [MyTaskSet] wait_time = between(1, 1) options = mocked_options() @@ -901,7 +901,7 @@ class MyTaskSet(TaskSet): tasks = [MySubTaskSet] class MyTestLocust(Locust): - task_set = MyTaskSet + tasks = [MyTaskSet] environment = create_environment(mocked_options()) environment.stop_timeout = short_time @@ -930,7 +930,7 @@ def my_task(self): MyTaskSet.state = "third" # should only run when run time + stop_timeout is > short_time * 2 class MyTestLocust(Locust): - task_set = MyTaskSet + tasks = [MyTaskSet] wait_time = constant(0) environment = create_environment(mocked_options()) diff --git a/locust/test/test_taskratio.py b/locust/test/test_taskratio.py index f5fbecdb37..db63f1ea62 100644 --- a/locust/test/test_taskratio.py +++ b/locust/test/test_taskratio.py @@ -22,9 +22,9 @@ def task2(self): pass class User(Locust): - task_set = Tasks + tasks = [Tasks] - ratio_dict = get_task_ratio_dict(User.task_set.tasks, total=True) + ratio_dict = get_task_ratio_dict(Tasks.tasks, total=True) self.assertEqual({ 'SubTasks': { @@ -49,18 +49,40 @@ def task3(self): class UnlikelyLocust(Locust): weight = 1 - task_set = Tasks + tasks = [Tasks] class MoreLikelyLocust(Locust): weight = 3 - task_set = Tasks + tasks = [Tasks] ratio_dict = get_task_ratio_dict([UnlikelyLocust, MoreLikelyLocust], total=True) - - self.assertEqual({ - 'UnlikelyLocust': {'tasks': {'task1': {'ratio': 0.25*0.25}, 'task3': {'ratio': 0.25*0.75}}, 'ratio': 0.25}, - 'MoreLikelyLocust': {'tasks': {'task1': {'ratio': 0.75*0.25}, 'task3': {'ratio': 0.75*0.75}}, 'ratio': 0.75} - }, ratio_dict) - unlikely = ratio_dict['UnlikelyLocust']['tasks'] - likely = ratio_dict['MoreLikelyLocust']['tasks'] + + self.assertDictEqual({ + 'UnlikelyLocust': { + 'ratio': 0.25, + 'tasks': { + 'Tasks': { + 'tasks': { + 'task1': {'ratio': 0.25*0.25}, + 'task3': {'ratio': 0.25*0.75}, + }, + 'ratio': 0.25 + } + }, + }, + 'MoreLikelyLocust': { + 'ratio': 0.75, + 'tasks': { + 'Tasks': { + 'tasks': { + 'task1': {'ratio': 0.75*0.25}, + 'task3': {'ratio': 0.75*0.75}, + }, + 'ratio': 0.75, + }, + }, + } + }, ratio_dict) + unlikely = ratio_dict['UnlikelyLocust']['tasks']['Tasks']['tasks'] + likely = ratio_dict['MoreLikelyLocust']['tasks']['Tasks']['tasks'] assert unlikely['task1']['ratio'] + unlikely['task3']['ratio'] + likely['task1']['ratio'] + likely['task3']['ratio'] == 1 From b68ba4314ef239fc6c46f6fc3f9fc514932c4298 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Wed, 1 Apr 2020 01:58:30 +0200 Subject: [PATCH 02/11] Fix test --- locust/test/test_runners.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/locust/test/test_runners.py b/locust/test/test_runners.py index f1e31858ee..cb1f0fa87b 100644 --- a/locust/test/test_runners.py +++ b/locust/test/test_runners.py @@ -91,12 +91,11 @@ def test_cpu_warning(self): runners.CPU_MONITOR_INTERVAL = 2.0 try: class CpuLocust(Locust): - wait_time = constant(0) - class task_set(TaskSet): - @task - def cpu_task(self): - for i in range(1000000): - _ = 3 / 2 + wait_time = constant(0.001) + @task + def cpu_task(self): + for i in range(1000000): + _ = 3 / 2 environment = Environment( options=mocked_options(), ) From af1e5e8dcf5b33c6beb715ffdae943bf2fc56c60 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Wed, 1 Apr 2020 12:58:44 +0200 Subject: [PATCH 03/11] Fix headline underline length (to avoid RST syntax warnings) --- docs/running-locust-distributed.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/running-locust-distributed.rst b/docs/running-locust-distributed.rst index bb9f351130..85b7dc6fd1 100644 --- a/docs/running-locust-distributed.rst +++ b/docs/running-locust-distributed.rst @@ -53,7 +53,7 @@ Sets locust in master mode. The web interface will run on this node. ``--worker`` ------------ +------------ Sets locust in worker mode. @@ -82,7 +82,7 @@ Optionally used together with ``--master``. Determines what network ports that t listen to. Defaults to 5557. ``--expect-workers=X`` ---------------------- +---------------------- Used when starting the master node with ``--no-web``. The master node will then wait until X worker nodes has connected before the test is started. From 60e1afd91999f7ff5de95894b6a3225d443da156 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Thu, 2 Apr 2020 10:38:22 +0200 Subject: [PATCH 04/11] Add on_start hook to Locust class --- locust/core.py | 2 ++ locust/test/test_locust_class.py | 21 +++++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/locust/core.py b/locust/core.py index a189ca7e9f..843c53dd8c 100644 --- a/locust/core.py +++ b/locust/core.py @@ -559,6 +559,8 @@ def _set_teardown_flag(cls): def run(self, runner=None): task_set_instance = self._task_set(self) try: + if hasattr(self, "on_start"): + self.on_start() task_set_instance.run() except StopLocust: pass diff --git a/locust/test/test_locust_class.py b/locust/test/test_locust_class.py index 3f455b7afe..385deaafee 100644 --- a/locust/test/test_locust_class.py +++ b/locust/test/test_locust_class.py @@ -134,6 +134,27 @@ def subtask(self): MyLocust(self.environment).run() self.assertEqual(1, state[0]) + def test_locust_on_start(self): + class MyLocust(Locust): + t1_executed = False + t2_executed = False + + def on_start(self): + self.t1() + + def t1(self): + self.t1_executed = True + + @task + def t2(self): + self.t2_executed = True + raise StopLocust() + + l = MyLocust(self.environment) + l.run() + self.assertTrue(l.t1_executed) + self.assertTrue(l.t2_executed) + def test_on_start(self): class MyTasks(TaskSet): t1_executed = False From a3f7ebd681839b44c47aad7e8889a37d17de0d2c Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Thu, 2 Apr 2020 13:35:03 +0200 Subject: [PATCH 05/11] Move test to proper test class --- locust/test/test_locust_class.py | 42 ++++++++++++++++---------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/locust/test/test_locust_class.py b/locust/test/test_locust_class.py index 385deaafee..7b38f48ce6 100644 --- a/locust/test/test_locust_class.py +++ b/locust/test/test_locust_class.py @@ -134,27 +134,6 @@ def subtask(self): MyLocust(self.environment).run() self.assertEqual(1, state[0]) - def test_locust_on_start(self): - class MyLocust(Locust): - t1_executed = False - t2_executed = False - - def on_start(self): - self.t1() - - def t1(self): - self.t1_executed = True - - @task - def t2(self): - self.t2_executed = True - raise StopLocust() - - l = MyLocust(self.environment) - l.run() - self.assertTrue(l.t1_executed) - self.assertTrue(l.t2_executed) - def test_on_start(self): class MyTasks(TaskSet): t1_executed = False @@ -393,6 +372,27 @@ def setup(self): User(self.environment) User(self.environment) self.assertEqual(1, User.setup_run_count) + + def test_locust_on_start(self): + class MyLocust(Locust): + t1_executed = False + t2_executed = False + + def on_start(self): + self.t1() + + def t1(self): + self.t1_executed = True + + @task + def t2(self): + self.t2_executed = True + raise StopLocust() + + l = MyLocust(self.environment) + l.run() + self.assertTrue(l.t1_executed) + self.assertTrue(l.t2_executed) class TestWebLocustClass(WebserverTestCase): From bbd73cd47b4c88e1cb3a9bd46ca3e72869478c2a Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Fri, 3 Apr 2020 10:04:10 +0200 Subject: [PATCH 06/11] Add abstract attribute to API docs for Locust and HttpLocust --- docs/api.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index 8855461c60..e852f9904b 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -7,13 +7,13 @@ Locust class ============ .. autoclass:: locust.core.Locust - :members: wait_time, tasks, weight + :members: wait_time, tasks, weight, abstract HttpLocust class ================ .. autoclass:: locust.core.HttpLocust - :members: wait_time, tasks, client + :members: wait_time, tasks, client, abstract TaskSet class From 6765d27d6e7542ce2db33837f109900745b7361b Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Fri, 3 Apr 2020 10:37:34 +0200 Subject: [PATCH 07/11] Add docstring to HttpLocust.abstract --- locust/core.py | 1 + 1 file changed, 1 insertion(+) diff --git a/locust/core.py b/locust/core.py index 30ba5035b2..6f92e5e78a 100644 --- a/locust/core.py +++ b/locust/core.py @@ -586,6 +586,7 @@ class by using the :py:func:`@task decorator ` on the methods, """ abstract = True + """If abstract is True it the class is meant to be subclassed (users of this class itself will not be spawned during a test)""" client = None """ From 5aacb9853a852f7476ecd79e00bc11acc41f4eaf Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Fri, 3 Apr 2020 15:37:05 +0200 Subject: [PATCH 08/11] Improve fasthttp documentation --- docs/increase-performance.rst | 19 +++++++++---------- locust/contrib/fasthttp.py | 20 ++++++++++++++++---- 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/docs/increase-performance.rst b/docs/increase-performance.rst index 78f8b05973..63de112f5f 100644 --- a/docs/increase-performance.rst +++ b/docs/increase-performance.rst @@ -43,21 +43,20 @@ Subclass FastHttpLocust instead of HttpLocust:: the default HttpLocust that uses python-requests. Therefore FastHttpLocust might not work as a drop-in replacement for HttpLocust, depending on how the HttpClient is used. -.. note:: - - You can set the following properties on your FastHttpLocust subclass to alter its behaviour: - - | network_timeout (default 60.0) - | connection_timeout (default 60.0) - | max_redirects (default 5, meaning 4 redirects) - | max_retries (default 1, meaning zero retries) - | insecure (default True, meaning ignore ssl failures) API === + +FastHttpLocust class +-------------------- + +.. autoclass:: locust.contrib.fasthttp.FastHttpLocust + :members: network_timeout, connection_timeout, max_redirects, max_retries, insecure + + FastHttpSession class -===================== +--------------------- .. autoclass:: locust.contrib.fasthttp.FastHttpSession :members: request, get, post, delete, put, head, options, patch diff --git a/locust/contrib/fasthttp.py b/locust/contrib/fasthttp.py index 928212cf88..379bf4fb32 100644 --- a/locust/contrib/fasthttp.py +++ b/locust/contrib/fasthttp.py @@ -49,10 +49,12 @@ def _construct_basic_auth_str(username, password): class FastHttpLocust(Locust): """ - Represents an HTTP "user" which is to be hatched and attack the system that is to be load tested. + FastHttpLocust uses a different HTTP client (geventhttpclient) compared to HttpLocust (python-requests). + It's significantly faster, but not as capable. - The behaviour of this user is defined by the task_set attribute, which should point to a - :py:class:`TaskSet ` class. + The behaviour of this user is defined by it's tasks. Tasks can be declared either directly on the + class by using the :py:func:`@task decorator ` on the methods, or by setting + the :py:attr:`tasks attribute `. This class creates a *client* attribute on instantiation which is an HTTP client with support for keeping a user session between requests. @@ -64,13 +66,23 @@ class FastHttpLocust(Locust): The client support cookies, and therefore keeps the session between HTTP requests. """ - # various UserAgent settings. Change these in your subclass to alter FastHttpLocust's behaviour. + # Below are various UserAgent settings. Change these in your subclass to alter FastHttpLocust's behaviour. # It needs to be done before FastHttpLocust is instantiated, changing them later will have no effect + network_timeout: float = 60.0 + """Parameter passed to FastHttpSession""" + connection_timeout: float = 60.0 + """Parameter passed to FastHttpSession""" + max_redirects: int = 5 + """Parameter passed to FastHttpSession. Default 5, meaning 4 redirects.""" + max_retries: int = 1 + """Parameter passed to FastHttpSession. Default 1, meaning zero retries.""" + insecure: bool = True + """Parameter passed to FastHttpSession. Default True, meaning no SSL verification.""" def __init__(self, environment): super().__init__(environment) From a69a4519f63e82a468431596cf79201dd1803309 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Fri, 3 Apr 2020 15:50:58 +0200 Subject: [PATCH 09/11] Rewrote all documentation to the new API that allow tasks directly under Locust classes. Some general documentation improvements here and there. --- docs/increase-performance.rst | 10 +- docs/quickstart.rst | 128 +++++---- docs/testing-other-systems.rst | 12 +- docs/writing-a-locustfile.rst | 256 ++++++++++-------- .../custom_xmlrpc_client/xmlrpc_locustfile.py | 19 +- 5 files changed, 237 insertions(+), 188 deletions(-) diff --git a/docs/increase-performance.rst b/docs/increase-performance.rst index 63de112f5f..9a47391c05 100644 --- a/docs/increase-performance.rst +++ b/docs/increase-performance.rst @@ -24,17 +24,15 @@ How to use FastHttpLocust Subclass FastHttpLocust instead of HttpLocust:: - from locust import TaskSet, task, between + from locust import task, between from locust.contrib.fasthttp import FastHttpLocust - class MyTaskSet(TaskSet): + class MyLocust(FastHttpLocust): + wait_time = between(1, 60) + @task def index(self): response = self.client.get("/") - - class MyLocust(FastHttpLocust): - task_set = MyTaskSet - wait_time = between(1, 60) .. note:: diff --git a/docs/quickstart.rst b/docs/quickstart.rst index da845d19cd..55cbd5a84f 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -5,86 +5,96 @@ Quick start Example locustfile.py ===================== -Below is a quick little example of a simple **locustfile.py**: - +When using Locust you define the behaviour of users in Python code, and then you have the ability to +simulate any number of those users while gathering request statistic. The entrypoint for defining the +user behaviour is the `locustfile.py`. -.. code-block:: python +.. note:: - from locust import HttpLocust, TaskSet, between + The ``locustfile.py`` is a normal Python file that will get imported by Locust. Within it you + can import modules just as you would in any python code. + + The file can be named something else and specified with the `-f` flag to the ``locust`` command. - def login(l): - l.client.post("/login", {"username":"ellen_key", "password":"education"}) +Below is a quick little example of a simple **locustfile.py**: - def index(l): - l.client.get("/") +.. code-block:: python + + import random + from locust import HttpLocust, task, between + + class WebsiteUser(HttpLocust): + wait_time = between(5, 9) + + @task(2) + def index(self): + self.client.get("/") + + @task(1) + def view_post(self): + post_id = random.randint(1, 10000) + self.client.get("/post?id=%i" % post_id, name="/post?id=[post-id]") + + def on_start(self): + """ on_start is called when a Locust start before any task is scheduled """ + self.login() + + def login(self): + self.client.post("/login", {"username":"ellen_key", "password":"education"}) - def profile(l): - l.client.get("/profile") - class UserBehavior(TaskSet): - tasks = {index: 2, profile: 1} +Let's break it down: +-------------------- - def on_start(self): - login(self) +.. code-block:: python class WebsiteUser(HttpLocust): - task_set = UserBehavior - wait_time = between(5.0, 9.0) +Here we define a class for the users that we will be simulating. It inherits from +:py:class:`HttpLocust ` which gives each user a ``client`` attribute, +which is an instance of :py:class:`HttpSession `, that +can be used to make HTTP requests to the target system that we want to load test. When a test starts +locust will create an instance of this class for every user that it simulates, and each of these +users will start running within their own green gevent thread. -Here we define a number of Locust tasks, which are normal Python callables that take one argument -(a :py:class:`Locust ` class instance). These tasks are gathered under a -:py:class:`TaskSet ` class in the *tasks* attribute. Then we have a -:py:class:`HttpLocust ` class which represents a user, where we define how -long a simulated user should wait between executing tasks, as well as what -:py:class:`TaskSet ` class should define the user's \"behaviour\". -:py:class:`TaskSet ` classes can be nested. +.. code-block:: python -The :py:class:`HttpLocust ` class inherits from the -:py:class:`Locust ` class, and it adds a client attribute which is an instance of -:py:class:`HttpSession ` that can be used to make HTTP requests. + wait_time = between(5, 9) -Another way we could declare tasks, which is usually more convenient, is to use the -``@task`` decorator. The following code is equivalent to the above: +Our class defines a ``wait_time`` function that will make the simulated users wait between 5 and 9 seconds after each task +is executed. .. code-block:: python - from locust import HttpLocust, TaskSet, task, between - - class UserBehaviour(TaskSet): - def on_start(self): - """ on_start is called when a Locust start before any task is scheduled """ - self.login() - - def login(self): - self.client.post("/login", {"username":"ellen_key", "password":"education"}) - - @task(2) - def index(self): - self.client.get("/") - - @task(1) - def profile(self): - self.client.get("/profile") + @task(2) + def index(self): + self.client.get("/") - class WebsiteUser(HttpLocust): - task_set = UserBehaviour - wait_time = between(5, 9) + @task(1) + def view_post(self): + ... -The :py:class:`Locust ` class (as well as :py:class:`HttpLocust ` -since it's a subclass) also allows one to specify the wait time between the execution of tasks -(:code:`wait_time = between(5, 9)`) as well as other user behaviours. -With the between function the time is randomly chosen uniformly between the specified min and max values, -but any user-defined time distributions can be used by setting *wait_time* to any arbitrary function. -For example, for an exponentially distributed wait time with average of 1 second: +We've also declared two tasks by decorating two methods with ``@task`` and given them +different weights (2 and 1). When a simulated user of this type runs it'll pick one of either ``index`` +or ``view_post`` - with twice the chance of picking ``index`` - call that method and then pick a duration +uniformly between 5 and 9 and just sleep for that duration. After it's wait time it'll pick a new task +and keep repeating that. .. code-block:: python - import random - - class WebsiteUser(HttpLocust): - task_set = UserBehaviour - wait_time = lambda self: random.expovariate(1) + post_id = random.randint(1, 10000) + self.client.get("/post?id=%i" % post_id, name="/post?id=[post-id]") + +In the ``view_post`` task we load a dynamic URL by using a query parameter that is a number picked at random between +1 and 10000. In order to not get 10k entries in Locust's statistics - since the stats is grouped on the URL - we use +the :ref:`name parameter ` to group all those requests under an entry named ``"/post?id=[post-id]"`` instead. + +.. code-block:: python + + def on_start(self): + +Additionally we've declared a `on_start` method. A method with this name will be called for each simulated +user when they start. For more info see :ref:`on-start-on-stop`. Start Locust diff --git a/docs/testing-other-systems.rst b/docs/testing-other-systems.rst index 51f882d392..ea6d0eac0e 100644 --- a/docs/testing-other-systems.rst +++ b/docs/testing-other-systems.rst @@ -15,10 +15,14 @@ Here is an example of a Locust class, **XmlRpcLocust**, which provides an XML-RP .. literalinclude:: ../examples/custom_xmlrpc_client/xmlrpc_locustfile.py -If you've written Locust tests before, you'll recognize the class called *ApiUser* which is a normal -Locust class that has a *TaskSet* class with *tasks* in its *task_set* attribute. However, the *ApiUser* -inherits from *XmlRpcLocust* that you can see right above ApiUser. The *XmlRpcLocust* class provides an -instance of XmlRpcClient under the *client* attribute. The *XmlRpcClient* is a wrapper around the standard +If you've written Locust tests before, you'll recognize the class called ``ApiUser`` which is a normal +Locust class that has a couple of tasks declared. However, the ``ApiUser`` inherits from +``XmlRpcLocust`` that you can see right above ``ApiUser``. The ``XmlRpcLocust`` is marked as abstract +using ``abstract = True`` which means that Locust till not try to create simulated users from that class +(only of classes that extends it). ``XmlRpcLocust`` provides an instance of XmlRpcClient under the +``client`` attribute. + +The ``XmlRpcClient`` is a wrapper around the standard library's :py:class:`xmlrpc.client.ServerProxy`. It basically just proxies the function calls, but with the important addition of firing :py:attr:`locust.event.Events.request_success` and :py:attr:`locust.event.Events.request_failure` events, which will record all calls in Locust's statistics. diff --git a/docs/writing-a-locustfile.rst b/docs/writing-a-locustfile.rst index 86af397b4e..0333f06ce6 100644 --- a/docs/writing-a-locustfile.rst +++ b/docs/writing-a-locustfile.rst @@ -12,13 +12,6 @@ A locust class represents one user (or a swarming locust if you will). Locust wi instance of the locust class for each user that is being simulated. There are a few attributes that a locust class should typically define. -The *task_set* attribute ------------------------- - -The :py:attr:`task_set ` attribute should point to a -:py:class:`TaskSet ` class which defines the behaviour of the user and -is described in more detail below. - The *wait_time* attribute ------------------------- @@ -100,52 +93,52 @@ Usually, this is specified in Locust's web UI or on the command line, using the If one declares a host attribute in the locust class, it will be used in the case when no :code:`--host` is specified on the command line or in the web request. +The *tasks* attribute +--------------------- + +A Locust class can have tasks declared as methods under it using the :py:func:`@task ` decorator, but one can also +specify tasks using the *tasks* attribute which is descibed in more details :ref:`below `. -TaskSet class -============= -If the Locust class represents a swarming locust, you could say that the TaskSet class represents -the brain of the locust. Each Locust class must have a *task_set* attribute set, that points to -a TaskSet. +Tasks +===== -A TaskSet is, like its name suggests, a collection of tasks. These tasks are normal python callables -and—if we were load-testing an auction website—could do stuff like "loading the start page", -"searching for some product" and "making a bid". +When a load test is started, an instance of a Locust class will be created for each simulated user +and they will start running within their own green thread. When these users run they pick tasks that +they execute, sleeps for awhile, and then picks a new task and so on. -When a load test is started, each instance of the spawned Locust classes will start executing their -TaskSet. What happens then is that each TaskSet will pick one of its tasks and call it. It will then -wait a number of seconds, specified by the Locust class' *wait_time* method (unless a *wait_time* -method has been declared directly on the TaskSet, in which case it will use its own method instead). -Then it will again pick a new task to be called, wait again, and so on. +The tasks are normal python callables and — if we were load-testing an auction website — they could do +stuff like "loading the start page", "searching for some product", "making a bid", etc. Declaring tasks --------------- -The typical way of declaring tasks for a TaskSet it to use the :py:meth:`task ` decorator. +The typical way of declaring tasks for a Locust class (or a TaskSet) it to use the +:py:meth:`task ` decorator. Here is an example: .. code-block:: python - from locust import Locust, TaskSet, task - - class MyTaskSet(TaskSet): + from locust import Locust, task + from locust.wait_time import constant + + class MyLocust(Locust): + wait_time = constant(1) + @task def my_task(self): - print("Locust instance (%r) executing my_task" % (self.locust)) - - class MyLocust(Locust): - task_set = MyTaskSet + print("Locust instance (%r) executing my_task" % self) **@task** takes an optional weight argument that can be used to specify the task's execution ratio. In -the following example *task2* will be executed twice as much as *task1*: +the following example *task2* will have twice the chance of being picked as *task1*: .. code-block:: python - from locust import Locust, TaskSet, task + from locust import Locust, task from locust.wait_time import between - class MyTaskSet(TaskSet): + class MyLocust(Locust): wait_time = between(5, 15) @task(3) @@ -155,128 +148,176 @@ the following example *task2* will be executed twice as much as *task1*: @task(6) def task2(self): pass - - class MyLocust(Locust): - task_set = MyTaskSet +.. _tasks-attribute: + tasks attribute --------------- Using the @task decorator to declare tasks is a convenience, and usually the best way to do -it. However, it's also possible to define the tasks of a TaskSet by setting the -:py:attr:`tasks ` attribute (using the @task decorator will actually +it. However, it's also possible to define the tasks of a Locust or TaskSet by setting the +:py:attr:`tasks ` attribute (using the @task decorator will actually just populate the *tasks* attribute). -The *tasks* attribute is either a list of python callables, or a ** dict. -The tasks are python callables that receive one argument—the TaskSet class instance that is executing -the task. Here is an extremely simple example of a locustfile (this locustfile won't actually load test anything): +The *tasks* attribute is either a list of Task, or a ** dict, where Task is either a +python callable or a TaskSet class (more on that below). If the task is a normal python function they +receive a single argument which is the Locust instance that is executing the task. + +Here is an example of a locust task declared as a normal python function: .. code-block:: python - from locust import Locust, TaskSet + from locust import Locust, constant def my_task(l): pass - class MyTaskSet(TaskSet): - tasks = [my_task] - class MyLocust(Locust): - task_set = MyTaskSet + tasks = [my_task] + wait_time = constant(1) If the tasks attribute is specified as a list, each time a task is to be performed, it will be randomly -chosen from the *tasks* attribute. If however, *tasks* is a dict—with callables as keys and ints -as values—the task that is to be executed will be chosen at random but with the int as ratio. So +chosen from the *tasks* attribute. If however, *tasks* is a dict — with callables as keys and ints +as values — the task that is to be executed will be chosen at random but with the int as ratio. So with a tasks that looks like this:: {my_task: 3, another_task: 1} -*my_task* would be 3 times more likely to be executed than *another_task*. +*my_task* would be 3 times more likely to be executed than *another_task*. -TaskSets can be nested ----------------------- +Internally the above dict will actually be expanded into a list (and the ``tasks`` attribute is updated) +that looks like this:: -A very important property of TaskSets is that they can be nested, because real websites are usually -built up in an hierarchical way, with multiple sub-sections. Nesting TaskSets will therefore allow -us to define a behaviour that simulates users in a more realistic way. For example -we could define TaskSets with the following structure: + [my_task, my_task, my_task, another_task] -* Main user behaviour +and then Python's ``random.choice()`` is used pick tasks from the list. - * Index page - * Forum page - - * Read thread - - * Reply - - * New thread - * View next page - - * Browse categories - - * Watch movie - * Filter movies - - * About page -The way you nest TaskSets is just like when you specify a task using the **tasks** attribute, but -instead of referring to a python function, you refer to another TaskSet: + +TaskSet class +============= + +Since real websites are usually built up in an hierarchical way, with multiple sub-sections, +locust has the TaskSet class. A locust task can not only be a Python callable, but also a +TaskSet class. A TaskSet is a collection of locust tasks that will be executed much like the +tasks declared directly on a Locust class, with the user sleeping in between task executions. +Here's a short example of a locustfile that has a TaskSet: .. code-block:: python - class ForumPage(TaskSet): - @task(20) - def read_thread(self): + from locust import Locust, TaskSet, between + + class ForumSection(TaskSet): + @task(10) + def view_thread(self): pass @task(1) - def new_thread(self): + def create_thread(self): pass - @task(5) + @task(1) def stop(self): self.interrupt() - class UserBehaviour(TaskSet): - tasks = {ForumPage:10} + class LoggedInUser(Locust): + wait_time = between(5, 120) + tasks = {ForumSection:2} @task - def index(self): + def index_page(self): pass -So in the above example, if the ForumPage would get selected for execution when the UserBehaviour -TaskSet is executing, then the ForumPage TaskSet would start executing. The ForumPage TaskSet -would then pick one of its own tasks, execute it, wait, and so on. +A TaskSet can also be inlined directly under a Locust/TaskSet class using the @task decorator: + +.. code-block:: python + + class MyUser(Locust): + @task(1) + class MyTaskSet(TaskSet): + ... -There is one important thing to note about the above example, and that is the call to -self.interrupt() in the ForumPage's stop method. What this does is essentially to -stop executing the ForumPage task set and the execution will continue in the UserBehaviour instance. -If we didn't have a call to the :py:meth:`interrupt() ` method -somewhere in ForumPage, the Locust would never stop running the ForumPage task once it has started. -But by having the interrupt function, we can—together with task weighting—define how likely it -is that a simulated user leaves the forum. -It's also possible to declare a nested TaskSet, inline in a class, using the -:py:meth:`@task ` decorator, just like when declaring normal tasks: +The tasks of a TaskSet class can be other TaskSet classes, allowing them to be nested any number +of levels. This allows us to define a behaviour that simulates users in a more realistic way. +For example we could define TaskSets with the following structure:: + + - Main user behaviour + - Index page + - Forum page + - Read thread + - Reply + - New thread + - View next page + - Browse categories + - Watch movie + - Filter movies + - About page + +When a running Locust thread picks a TaskSet class for execution an instance of this class will +be created and execution will then go into this TaskSet. What happens then is that one of the +TaskSet's tasks will be picked and executed, and then the thread will sleep for a duration specified +by the Locust's wait_time function (unless a ``wait_time`` function has been declared directly on +the TaskSet class, in which case it'll use that function instead), then pick a new task from the +TaskSet's tasks, wait again, and so on. + + + +Interrupting a TaskSet +---------------------- + +One important thing to know about TaskSets is that they will never stop executing their tasks, and +hand over execution back to their parent Locust/TaskSet, by themselves. This has to be done by the +developer by calling the :py:meth:`TaskSet.interrupt() ` method. + +.. autofunction:: locust.core.TaskSet.interrupt + :noindex: + +In the following example, if we didn't have the stop task that calls ``self.interrupt()``, the +simulated user would never stop running tasks from the Forum taskset once it has went into it: .. code-block:: python - class MyTaskSet(TaskSet): + class RegisteredUser(Locust): @task - class SubTaskSet(TaskSet): - @task - def my_task(self): + class Forum(TaskSet): + @task(5) + def view_thread(self): pass + + @task(1) + def stop(self): + self.interrupt() + + @task + def frontpage(self): + pass + +Using the interrupt function, we can — together with task weighting — define how likely it +is that a simulated user leaves the forum. + + +Differences between tasks in TaskSet and Locust classes +------------------------------------------------------- + +One difference for tasks residing under a TaskSet, compared to tasks residing directly under a Locust, +is that the argument that they are passed when executed (``self`` for tasks declared as methods with +the :py:func:`@task ` decorator) is a reference to the TaskSet instance, instead of +the Locust user instance. The Locust instance can be accessed from within a TaskSet instance through the +:py:attr:`TaskSet.locust `. TaskSets also contains a convenience +:py:attr:`client ` attribute that refers to the client attribute on the +Locust instance. + Referencing the Locust instance, or the parent TaskSet instance --------------------------------------------------------------- A TaskSet instance will have the attribute :py:attr:`locust ` point to its Locust instance, and the attribute :py:attr:`parent ` point to its -parent TaskSet (it will point to the Locust instance, in the base TaskSet). +parent TaskSet instance. + TaskSequence class @@ -304,6 +345,8 @@ To define this order you should do the following: In the above example, the order is defined to execute first_task, then second_task and lastly the third_task for 10 times. As you can see, you can compose :py:meth:`@seq_task ` with :py:meth:`@task ` decorator, and of course you can also nest TaskSets within TaskSequences and vice versa. +.. _on-start-on-stop: + Setups, Teardowns, on_start, and on_stop ======================================== @@ -363,9 +406,11 @@ with two URLs; **/** and **/about/**: .. code-block:: python - from locust import HttpLocust, TaskSet, task, between + from locust import HttpLocust, task, between - class MyTaskSet(TaskSet): + class MyLocust(HttpLocust): + wait_time = between(5, 15) + @task(2) def index(self): self.client.get("/") @@ -373,19 +418,10 @@ with two URLs; **/** and **/about/**: @task(1) def about(self): self.client.get("/about/") - - class MyLocust(HttpLocust): - task_set = MyTaskSet - wait_time = between(5, 15) Using the above Locust class, each simulated user will wait between 5 and 15 seconds between the requests, and **/** will be requested twice as much as **/about/**. -The attentive reader will find it odd that we can reference the HttpSession instance -using *self.client* inside the TaskSet, and not *self.locust.client*. We can do this -because the :py:class:`TaskSet ` class has a convenience property -called client that simply returns self.locust.client. - Using the HTTP client ---------------------- @@ -457,6 +493,8 @@ be reported as a success in the statistics: response.success() +.. _name-parameter: + Grouping requests to URLs with dynamic parameters ------------------------------------------------- @@ -523,7 +561,7 @@ A project with multiple different locustfiles could also keep them in a separate * ``requirements.txt`` -With any ofthe above project structure, your locustfile can import common libraries using: +With any of the above project structure, your locustfile can import common libraries using: .. code-block:: python diff --git a/examples/custom_xmlrpc_client/xmlrpc_locustfile.py b/examples/custom_xmlrpc_client/xmlrpc_locustfile.py index 0b3987183c..f55e4a3812 100644 --- a/examples/custom_xmlrpc_client/xmlrpc_locustfile.py +++ b/examples/custom_xmlrpc_client/xmlrpc_locustfile.py @@ -1,7 +1,7 @@ import time from xmlrpc.client import ServerProxy, Fault -from locust import Locust, TaskSet, events, task, between +from locust import Locust, task, between class XmlRpcClient(ServerProxy): @@ -36,6 +36,7 @@ class XmlRpcLocust(Locust): This is the abstract Locust class which should be subclassed. It provides an XML-RPC client that can be used to make XML-RPC requests that will be tracked in Locust's statistics. """ + abstract = True def __init__(self, *args, **kwargs): super(XmlRpcLocust, self).__init__(*args, **kwargs) self.client = XmlRpcClient(self.host) @@ -43,15 +44,13 @@ def __init__(self, *args, **kwargs): class ApiUser(XmlRpcLocust): - host = "http://127.0.0.1:8877/" wait_time = between(0.1, 1) - class task_set(TaskSet): - @task(10) - def get_time(self): - self.client.get_time() - - @task(5) - def get_random_number(self): - self.client.get_random_number(0, 100) + @task(10) + def get_time(self): + self.client.get_time() + + @task(5) + def get_random_number(self): + self.client.get_random_number(0, 100) From f1f94fb4d76c7d17f961d636df55684849870b51 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Fri, 3 Apr 2020 17:01:40 +0200 Subject: [PATCH 10/11] Remove Locust._task_set since there shouldn't be any problem with hard coding DefaultTaskSet as far as I can tell --- locust/core.py | 5 +---- locust/runners.py | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/locust/core.py b/locust/core.py index 6f92e5e78a..0940498cc4 100644 --- a/locust/core.py +++ b/locust/core.py @@ -519,9 +519,6 @@ class ForumPage(TaskSet): abstract = True """If abstract is True it the class is meant to be subclassed (users of this class itself will not be spawned during a test)""" - _task_set = DefaultTaskSet - """TaskSet class that defines the execution behaviour of this locust""" - client = NoClientWarningRaiser() _catch_exceptions = True _setup_has_run = False # Internal state to see if we have already run @@ -557,7 +554,7 @@ def _set_teardown_flag(cls): cls._teardown_is_set = True def run(self, runner=None): - task_set_instance = self._task_set(self) + task_set_instance = DefaultTaskSet(self) try: if hasattr(self, "on_start"): self.on_start() diff --git a/locust/runners.py b/locust/runners.py index 05185858a7..be27fe26d1 100644 --- a/locust/runners.py +++ b/locust/runners.py @@ -87,7 +87,7 @@ def weight_locusts(self, amount): returns a list "bucket" with the weighted locusts """ bucket = [] - weight_sum = sum((locust.weight for locust in self.locust_classes if locust._task_set)) + weight_sum = sum([locust.weight for locust in self.locust_classes]) residuals = {} for locust in self.locust_classes: if self.environment.host is not None: From 014d9a51b7a571d49b1f40f6d6363aeee91ee178 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Fri, 3 Apr 2020 17:19:03 +0200 Subject: [PATCH 11/11] Emit DepricationWarning if a Locust class has a task_set attribute of type TaskSet (that hasn't been decorated by @task) --- locust/core.py | 3 +++ locust/test/test_runners.py | 11 +++++------ locust/test/test_stats.py | 9 ++++----- locust/test/test_web.py | 21 +++++++++------------ locust/util/deprecation.py | 9 +++++++++ 5 files changed, 30 insertions(+), 23 deletions(-) diff --git a/locust/core.py b/locust/core.py index 0940498cc4..07932b9be6 100644 --- a/locust/core.py +++ b/locust/core.py @@ -452,6 +452,9 @@ def __new__(mcs, classname, bases, class_dict): # Not a base class class_dict["abstract"] = False + # check if class uses deprecated task_set attribute + deprecation.check_for_deprecated_task_set_attribute(class_dict) + return type.__new__(mcs, classname, bases, class_dict) diff --git a/locust/test/test_runners.py b/locust/test/test_runners.py index cb1f0fa87b..0dc00594eb 100644 --- a/locust/test/test_runners.py +++ b/locust/test/test_runners.py @@ -111,7 +111,7 @@ def cpu_task(self): def test_weight_locusts(self): maxDiff = 2048 class BaseLocust(Locust): - class task_set(TaskSet): pass + pass class L1(BaseLocust): weight = 101 class L2(BaseLocust): @@ -126,7 +126,7 @@ class L3(BaseLocust): def test_weight_locusts_fewer_amount_than_locust_classes(self): class BaseLocust(Locust): - class task_set(TaskSet): pass + pass class L1(BaseLocust): weight = 101 class L2(BaseLocust): @@ -748,10 +748,9 @@ def the_task(self): def test_change_user_count_during_hatching(self): class User(Locust): wait_time = constant(1) - class task_set(TaskSet): - @task - def my_task(self): - pass + @task + def my_task(self): + pass with mock.patch("locust.rpc.rpc.Client", mocked_rpc()) as client: options = mocked_options() diff --git a/locust/test/test_stats.py b/locust/test/test_stats.py index a23425a281..4b8a6a45b7 100644 --- a/locust/test/test_stats.py +++ b/locust/test/test_stats.py @@ -296,11 +296,10 @@ class User(Locust): task_run_count = 0 locust_error_count = 0 wait_time = locust.wait_time.constant(1) - - class task_set(TaskSet): - @task - def my_task(self): - User.task_run_count += 1 + @task + def my_task(self): + User.task_run_count += 1 + self.environment = Environment(options=mocked_options()) locust.runners.locust_runner = locust.runners.LocalLocustRunner(self.environment, [User]) self.remove_file_if_exists(self.STATS_FILENAME) diff --git a/locust/test/test_web.py b/locust/test/test_web.py index 0f4b4e457e..20c91b06b5 100644 --- a/locust/test/test_web.py +++ b/locust/test/test_web.py @@ -185,10 +185,9 @@ def test_exceptions_csv(self): def test_swarm_host_value_specified(self): class MyLocust(Locust): wait_time = constant(1) - class task_set(TaskSet): - @task(1) - def my_task(self): - pass + @task(1) + def my_task(self): + pass self.environment.locust_classes = [MyLocust] response = requests.post( "http://127.0.0.1:%i/swarm" % self.web_port, @@ -201,10 +200,9 @@ def my_task(self): def test_swarm_host_value_not_specified(self): class MyLocust(Locust): wait_time = constant(1) - class task_set(TaskSet): - @task(1) - def my_task(self): - pass + @task(1) + def my_task(self): + pass self.runner.locust_classes = [MyLocust] response = requests.post( "http://127.0.0.1:%i/swarm" % self.web_port, @@ -248,10 +246,9 @@ class MyLocust2(Locust): def test_swarm_in_step_load_mode(self): class MyLocust(Locust): wait_time = constant(1) - class task_set(TaskSet): - @task(1) - def my_task(self): - pass + @task(1) + def my_task(self): + pass self.environment.locust_classes = [MyLocust] self.environment.step_load = True response = requests.post( diff --git a/locust/util/deprecation.py b/locust/util/deprecation.py index e4608ea14c..9526ecb7d5 100644 --- a/locust/util/deprecation.py +++ b/locust/util/deprecation.py @@ -27,3 +27,12 @@ def format_min_max_wait(i): format_min_max_wait(locust_or_taskset.min_wait), format_min_max_wait(locust_or_taskset.max_wait), ), DeprecationWarning) + + +def check_for_deprecated_task_set_attribute(class_dict): + from locust.core import TaskSet + if "task_set" in class_dict: + task_set = class_dict["task_set"] + if issubclass(task_set, TaskSet) and not hasattr(task_set, "locust_task_weight"): + warnings.warn("Usage of Locust.task_set is deprecated since version 1.0. Set the tasks attribute instead " + "(tasks = [%s])" % task_set.__name__, DeprecationWarning)