From 4253056d35fc0dadbc6e3b46f7563ee671f4c9ba Mon Sep 17 00:00:00 2001 From: serhiy Date: Thu, 19 Aug 2021 13:33:40 +0100 Subject: [PATCH 1/8] Add alternative way of grouping requests --- locust/clients.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/locust/clients.py b/locust/clients.py index 33c2a8730d..c67233c73e 100644 --- a/locust/clients.py +++ b/locust/clients.py @@ -5,6 +5,7 @@ from requests import Request, Response from requests.auth import HTTPBasicAuth from requests.exceptions import InvalidSchema, InvalidURL, MissingSchema, RequestException +from contextlib import contextmanager from urllib.parse import urlparse, urlunparse @@ -51,6 +52,10 @@ def __init__(self, base_url, request_event, user, *args, **kwargs): self.request_event = request_event self.user = user + # User can group name, or use the group context manager to gather performance statistics under a specific name + # This is an alternative to passing in the "name" parameter to the requests function + self.group_name = None + # Check for basic authentication parsed_url = urlparse(self.base_url) if parsed_url.username and parsed_url.password: @@ -72,6 +77,16 @@ def _build_url(self, path): else: return "%s%s" % (self.base_url, path) + @contextmanager + def group(self, *, name: str): + """Group requests using the "with" keyword""" + + self.group_name = name + try: + yield + finally: + self.group_name = None + def request(self, method, url, name=None, catch_response=False, context={}, **kwargs): """ Constructs and sends a :py:class:`requests.Request`. @@ -102,6 +117,10 @@ def request(self, method, url, name=None, catch_response=False, context={}, **kw :param cert: (optional) if String, path to ssl client cert file (.pem). If Tuple, ('cert', 'key') pair. """ + # if group name has been set and no name parameter has been passed in; set the name parameter to group_name + if self.group_name and not name: + name = self.group_name + # prepend url with hostname unless it's already an absolute URL url = self._build_url(url) start_time = time.perf_counter() From 156e896052c7f5a66889d127667cfb239da7a805 Mon Sep 17 00:00:00 2001 From: serhiy Date: Thu, 19 Aug 2021 14:02:44 +0100 Subject: [PATCH 2/8] Add docs on the new grouping methods --- docs/writing-a-locustfile.rst | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/docs/writing-a-locustfile.rst b/docs/writing-a-locustfile.rst index 34dbfb3d79..8acfbab574 100644 --- a/docs/writing-a-locustfile.rst +++ b/docs/writing-a-locustfile.rst @@ -546,6 +546,35 @@ Example: for i in range(10): self.client.get("/blog?id=%i" % i, name="/blog?id=[id]") +There may be situations where passing in a parameter into request function is not possible, such as when interacting with libraries/SDK's that +wrap a Requests session. An alternative say of grouping requests is provided By setting the ``client.group_name`` attribute + +.. code-block:: python + + # Statistics for these requests will be grouped under: /blog/?id=[id] + self.client.group_name="/blog?id=[id]" + for i in range(10): + self.client.get("/blog?id=%i" % i) + self.client.group_name=None + +If You want to chain multiple groupings with minimal boilerplate, you can use the ``client.group()`` context manager. + +.. code-block:: python + + @task + def multiple_groupings_example(self): + + # Statistics for these requests will be grouped under: /blog/?id=[id] + with self.client.group(name="/blog?id=[id]"): + for i in range(10): + self.client.get("/blog?id=%i" % i) + + # Statistics for these requests will be grouped under: /article/?id=[id] + with self.client.group(name="/article?id=[id]"): + for i in range(10): + self.client.get("/article?id=%i" % i) + + HTTP Proxy settings ------------------- From 5410e688ef616e4b76cae1b972d65c9087e4cd73 Mon Sep 17 00:00:00 2001 From: serhiy Date: Thu, 19 Aug 2021 14:32:44 +0100 Subject: [PATCH 3/8] Add example of session patching --- docs/testing-other-systems.rst | 11 ++++++++- .../session_patch_locustfile.py | 24 +++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 examples/sdk_session_patching/session_patch_locustfile.py diff --git a/docs/testing-other-systems.rst b/docs/testing-other-systems.rst index e8ddca34d2..8a7b3a9937 100644 --- a/docs/testing-other-systems.rst +++ b/docs/testing-other-systems.rst @@ -1,7 +1,7 @@ .. _testing-other-systems: ======================== -Testing non-HTTP systems +Testing other systems ======================== Locust only comes with built-in support for HTTP/HTTPS but it can be extended to load test almost any system. You do this by writing a custom client that triggers :py:attr:`request ` @@ -44,4 +44,13 @@ gRPC client, base User and example usage: .. literalinclude:: ../examples/grpc/locustfile.py + +Example: Patching over SDK's that wrap around Session objects +============================================================= + +If you have a prebuilt SDK for a target system that is a essentially a wrapper for Session object. You can use the a pattern of patching over the internal session object with the locust provided one: + +.. literalinclude:: ../examples/sdk_session_patching/session_patch_locustfile.py + + For more examples of user types, see `locust-plugins `_ (it has users for WebSocket/SocketIO, Kafka, Selenium/WebDriver and more) \ No newline at end of file diff --git a/examples/sdk_session_patching/session_patch_locustfile.py b/examples/sdk_session_patching/session_patch_locustfile.py new file mode 100644 index 0000000000..345e03d979 --- /dev/null +++ b/examples/sdk_session_patching/session_patch_locustfile.py @@ -0,0 +1,24 @@ +import locust +from locust.user import task +from archivist.archivist import Archivist # Example SDK under test + + +class ArchivistUser(locust.HttpUser): + + def on_start(self): + AUTH_TOKEN = None + + with open("auth.text", "r") as f: + AUTH_TOKEN = f.read() + + # Start an instance of of the SDK + self.arch: Archivist = Archivist(url=self.host, auth=AUTH_TOKEN) + # overwrite the internal _session attribute with the locust session + self.arch._session = self.client + + @task + def Create_assets(self): + """ User creates assets as fast as possible""" + + while True: + self.arch.assets.create(behaviours=[], attrs={}) From cfbab865ce9c8b6416b6d002a1436b6c20d5036d Mon Sep 17 00:00:00 2001 From: serhiy Date: Fri, 20 Aug 2021 12:24:43 +0100 Subject: [PATCH 4/8] Update field and function name from feedback --- docs/writing-a-locustfile.rst | 200 +++++++++++++++++----------------- locust/clients.py | 12 +- 2 files changed, 106 insertions(+), 106 deletions(-) diff --git a/docs/writing-a-locustfile.rst b/docs/writing-a-locustfile.rst index 8acfbab574..172c4c0cf1 100644 --- a/docs/writing-a-locustfile.rst +++ b/docs/writing-a-locustfile.rst @@ -18,13 +18,13 @@ Now, lets look at a more complete/realistic example of what your tests might loo def hello_world(self): self.client.get("/hello") self.client.get("/world") - + @task(3) def view_items(self): for item_id in range(10): self.client.get(f"/item?id={item_id}", name="/item") time.sleep(1) - + def on_start(self): self.client.post("/login", json={"username":"foo", "password":"bar"}) @@ -42,14 +42,14 @@ A locust file is just a normal Python module, it can import code from other file class QuickstartUser(HttpUser): -Here we define a class for the users that we will be simulating. It inherits from +Here we define a class for the users that we will be simulating. It inherits from :py:class:`HttpUser ` 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 +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. -For a file to be a valid locustfile it must contain at least one class inheriting from :py:class:`User `. +For a file to be a valid locustfile it must contain at least one class inheriting from :py:class:`User `. .. code-block:: python @@ -64,7 +64,7 @@ is executed. For more info see :ref:`wait-time`. def hello_world(self): ... -Methods decorated with ``@task`` are the core of your locust file. For every running user, +Methods decorated with ``@task`` are the core of your locust file. For every running user, Locust creates a greenlet (micro-thread), that will call those methods. .. code-block:: python @@ -73,16 +73,16 @@ Locust creates a greenlet (micro-thread), that will call those methods. def hello_world(self): self.client.get("/hello") self.client.get("/world") - + @task(3) def view_items(self): ... -We've declared two tasks by decorating two methods with ``@task``, one of which has been given a higher weight (3). -When our ``QuickstartUser`` runs it'll pick one of the declared tasks - in this case either ``hello_world`` or -``view_items`` - and execute it. Tasks are picked at random, but you can give them different weighting. The above -configuration will make Locust three times more likely to pick ``view_items`` than ``hello_world``. When a task has -finished executing, the User will then sleep during it's wait time (in this case between 1 and 5 seconds). +We've declared two tasks by decorating two methods with ``@task``, one of which has been given a higher weight (3). +When our ``QuickstartUser`` runs it'll pick one of the declared tasks - in this case either ``hello_world`` or +``view_items`` - and execute it. Tasks are picked at random, but you can give them different weighting. The above +configuration will make Locust three times more likely to pick ``view_items`` than ``hello_world``. When a task has +finished executing, the User will then sleep during it's wait time (in this case between 1 and 5 seconds). After it's wait time it'll pick a new task and keep repeating that. Note that only methods decorated with ``@task`` will be picked, so you can define your own internal helper methods any way you like. @@ -91,12 +91,12 @@ Note that only methods decorated with ``@task`` will be picked, so you can defin self.client.get("/hello") -The ``self.client`` attribute makes it possible to make HTTP calls that will be logged by Locust. For information on how -to make other kinds of requests, validate the response, etc, see -`Using the HTTP Client `_. +The ``self.client`` attribute makes it possible to make HTTP calls that will be logged by Locust. For information on how +to make other kinds of requests, validate the response, etc, see +`Using the HTTP Client `_. .. note:: - + HttpUser is not a real browser, and thus will not parse an HTML response to load resources or render the page. It will keep track of cookies though. .. code-block:: python @@ -107,8 +107,8 @@ to make other kinds of requests, validate the response, etc, see self.client.get(f"/item?id={item_id}", name="/item") time.sleep(1) -In the ``view_items`` task we load 10 different URLs by using a variable query parameter. -In order to not get 10 separate entries in Locust's statistics - since the stats is grouped on the URL - we use +In the ``view_items`` task we load 10 different URLs by using a variable query parameter. +In order to not get 10 separate 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 ``"/item"`` instead. .. code-block:: python @@ -116,15 +116,15 @@ the :ref:`name parameter ` to group all those requests under an def on_start(self): self.client.post("/login", json={"username":"foo", "password":"bar"}) -Additionally we've declared an `on_start` method. A method with this name will be called for each simulated +Additionally we've declared an `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`. User class ========== -A user class represents one user (or a swarming locust if you will). Locust will spawn one -instance of the User class for each user that is being simulated. There are some common attributes that -a User class may define. +A user class represents one user (or a swarming locust if you will). Locust will spawn one +instance of the User class for each user that is being simulated. There are some common attributes that +a User class may define. .. _wait-time: @@ -132,10 +132,10 @@ wait_time attribute ------------------- A User's :py:attr:`wait_time ` method is an optional attribute used to determine -how long a simulated user should wait between executing tasks. If no :py:attr:`wait_time ` +how long a simulated user should wait between executing tasks. If no :py:attr:`wait_time ` is specified, the next task will be executed as soon as one finishes. -There are three built in wait time functions: +There are three built in wait time functions: * :py:attr:`constant ` for a fixed amount of time @@ -146,7 +146,7 @@ For example, to make each user wait between 0.5 and 10 seconds between every tas .. code-block:: python from locust import User, task, between - + class MyUser(User): @task def my_task(self): @@ -158,26 +158,26 @@ For example, to make each user wait between 0.5 and 10 seconds between every tas This is very useful if you want to target a specific throughput. For example, if you want Locust to run 500 task iterations per second at peak load, you could use `wait_time = constant_pacing(10)` and a user count of 5000. If the time for each iteration exceeds 10 seconds then you'll get lower throughput. Note that as wait time is applied *after* task execution, if you have a high spawn rate/ramp up you may slightly exceed your target during rampup. -It's also possible to declare your own wait_time method directly on your class. +It's also possible to declare your own wait_time method directly on your class. For example, the following User class would sleep for one second, then two, then three, etc. .. code-block:: python class MyUser(User): last_wait_time = 0 - + def wait_time(self): self.last_wait_time += 1 return self.last_wait_time ... - + weight attribute ---------------- If more than one user class exists in the file, and no user classes are specified on the command line, -Locust will spawn an equal number of each of the user classes. You can also specify which of the +Locust will spawn an equal number of each of the user classes. You can also specify which of the user classes to use from the same locustfile by passing them as command line arguments: .. code-block:: console @@ -201,11 +201,11 @@ classes. Say for example, web users are three times more likely than mobile user host attribute -------------- -The host attribute is a URL prefix (i.e. "http://google.com") to the host that is to be loaded. -Usually, this is specified in Locust's web UI or on the command line, using the -:code:`--host` option, when locust is started. +The host attribute is a URL prefix (i.e. "http://google.com") to the host that is to be loaded. +Usually, this is specified in Locust's web UI or on the command line, using the +:code:`--host` option, when locust is started. -If one declares a host attribute in the user class, it will be used in the case when no :code:`--host` +If one declares a host attribute in the user class, it will be used in the case when no :code:`--host` is specified on the command line or in the web request. tasks attribute @@ -217,11 +217,11 @@ specify tasks using the *tasks* attribute which is described in more details :re environment attribute --------------------- -A reference to the :py:attr:`environment ` in which the user is running. Use this to interact with +A reference to the :py:attr:`environment ` in which the user is running. Use this to interact with the environment, or the :py:attr:`runner ` which it contains. E.g. to stop the runner from a task method: .. code-block:: python - + self.environment.runner.quit() If run on a standalone locust instance, this will stop the entire run. If run on worker node, it will stop that particular node. @@ -235,8 +235,8 @@ Users (and :ref:`TaskSets `) can declare an :py:meth:`on_start ` method. A User will call its :py:meth:`on_start ` method when it starts running, and its :py:meth:`on_stop ` method when it stops running. For a TaskSet, the -:py:meth:`on_start ` method is called when a simulated user starts executing -that TaskSet, and :py:meth:`on_stop ` is called when the simulated user stops +:py:meth:`on_start ` method is called when a simulated user starts executing +that TaskSet, and :py:meth:`on_stop ` is called when the simulated user stops executing that TaskSet (when :py:meth:`interrupt() ` is called, or the user is killed). @@ -244,11 +244,11 @@ Tasks ===== When a load test is started, an instance of a User 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 +and they will start running within their own green thread. When these users run they pick tasks that they execute, sleep for awhile, and then pick a new task 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. +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. @task decorator --------------- @@ -261,25 +261,25 @@ The easiest way to add a task for a User is by using the :py:meth:`task ` attribute. -The *tasks* attribute is either a list of Tasks, or a ** dict, where Task is either a -python callable or a :ref:`TaskSet ` class. If the task is a normal python function they +The *tasks* attribute is either a list of Tasks, or a ** dict, where Task is either a +python callable or a :ref:`TaskSet ` class. If the task is a normal python function they receive a single argument which is the User instance that is executing the task. Here is an example of a User task declared as a normal python function: @@ -301,25 +301,25 @@ Here is an example of a User task declared as a normal python function: .. code-block:: python from locust import User, constant - + def my_task(user): pass - + class MyUser(User): 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 +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 with a task 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*. -Internally the above dict will actually be expanded into a list (and the ``tasks`` attribute is updated) +Internally the above dict will actually be expanded into a list (and the ``tasks`` attribute is updated) that looks like this:: [my_task, my_task, my_task, another_task] @@ -375,24 +375,24 @@ Events ====== If you want to run some setup code as part of your test, it is often enough to put it at the module -level of your locustfile, but sometimes you need to do things at particular times in the run. For +level of your locustfile, but sometimes you need to do things at particular times in the run. For this need, Locust provides event hooks. test_start and test_stop ------------------------ -If you need to run some code at the start or stop of a load test, you should use the -:py:attr:`test_start ` and :py:attr:`test_stop ` +If you need to run some code at the start or stop of a load test, you should use the +:py:attr:`test_start ` and :py:attr:`test_stop ` events. You can set up listeners for these events at the module level of your locustfile: .. code-block:: python from locust import events - + @events.test_start.add_listener def on_test_start(environment, **kwargs): print("A new test is starting") - + @events.test_stop.add_listener def on_test_stop(environment, **kwargs): print("A new test is ending") @@ -429,14 +429,14 @@ HttpUser class .. code-block:: python from locust import HttpUser, task, between - + class MyUser(HttpUser): wait_time = between(5, 15) - + @task(4) def index(self): self.client.get("/") - + @task(1) def about(self): self.client.get("/about/") @@ -445,13 +445,13 @@ HttpUser class client attribute / HttpSession ------------------------------ -:py:attr:`client ` is an instance of :py:class:`HttpSession `. HttpSession is a subclass/wrapper for -:py:class:`requests.Session`, so its features are well documented and should be familiar to many. What HttpSession adds is mainly reporting of the request results into Locust (success/fail, response time, response length, name). +:py:attr:`client ` is an instance of :py:class:`HttpSession `. HttpSession is a subclass/wrapper for +:py:class:`requests.Session`, so its features are well documented and should be familiar to many. What HttpSession adds is mainly reporting of the request results into Locust (success/fail, response time, response length, name). -It contains methods for all HTTP methods: :py:meth:`get `, -:py:meth:`post `, :py:meth:`put `, -... +It contains methods for all HTTP methods: :py:meth:`get `, +:py:meth:`post `, :py:meth:`put `, +... Just like :py:class:`requests.Session`, it preserves cookies between requests so it can easily be used to log in to websites. @@ -464,7 +464,7 @@ Just like :py:class:`requests.Session`, it preserves cookies between requests so print("Response text:", response.text) response = self.client.get("/my-profile") -HttpSession catches any :py:class:`requests.RequestException` thrown by Session (caused by connection errors, timeouts or similar), instead returning a dummy +HttpSession catches any :py:class:`requests.RequestException` thrown by Session (caused by connection errors, timeouts or similar), instead returning a dummy Response object with *status_code* set to 0 and *content* set to None. @@ -473,14 +473,14 @@ Response object with *status_code* set to 0 and *content* set to None. Validating responses -------------------- -Requests are considered successful if the HTTP response code is OK (<400), but it is often useful to +Requests are considered successful if the HTTP response code is OK (<400), but it is often useful to do some additional validation of the response. -You can mark a request as failed by using the *catch_response* argument, a *with*-statement and +You can mark a request as failed by using the *catch_response* argument, a *with*-statement and a call to *response.failure()* .. code-block:: python - + with self.client.get("/", catch_response=True) as response: if response.text != "Success": response.failure("Got wrong response") @@ -514,7 +514,7 @@ REST/JSON APIs Here's an example of how to call a REST API and validate the response: .. code-block:: python - + from json import JSONDecodeError ... with self.client.post("/", json={"foo": 42, "bar": None}, catch_response=True) as response: @@ -533,10 +533,10 @@ locust-plugins has a ready-made class for testing REST API:s called `RestUser ` -different request methods. +by passing a *name* argument to the :py:class:`HttpSession's ` +different request methods. Example: @@ -547,66 +547,66 @@ Example: self.client.get("/blog?id=%i" % i, name="/blog?id=[id]") There may be situations where passing in a parameter into request function is not possible, such as when interacting with libraries/SDK's that -wrap a Requests session. An alternative say of grouping requests is provided By setting the ``client.group_name`` attribute +wrap a Requests session. An alternative say of grouping requests is provided By setting the ``client.request_name`` attribute .. code-block:: python # Statistics for these requests will be grouped under: /blog/?id=[id] - self.client.group_name="/blog?id=[id]" + self.client.request_name="/blog?id=[id]" for i in range(10): self.client.get("/blog?id=%i" % i) - self.client.group_name=None + self.client.request_name=None -If You want to chain multiple groupings with minimal boilerplate, you can use the ``client.group()`` context manager. +If You want to chain multiple groupings with minimal boilerplate, you can use the ``client.name_request()`` context manager. .. code-block:: python @task - def multiple_groupings_example(self): + def multiple_groupings_example(self): # Statistics for these requests will be grouped under: /blog/?id=[id] - with self.client.group(name="/blog?id=[id]"): + with self.client.name_request("/blog?id=[id]"): for i in range(10): self.client.get("/blog?id=%i" % i) # Statistics for these requests will be grouped under: /article/?id=[id] - with self.client.group(name="/article?id=[id]"): + with self.client.name_request("/article?id=[id]"): for i in range(10): self.client.get("/article?id=%i" % i) - + HTTP Proxy settings ------------------- -To improve performance, we configure requests to not look for HTTP proxy settings in the environment by setting -requests.Session's trust_env attribute to ``False``. If you don't want this you can manually set -``locust_instance.client.trust_env`` to ``True``. For further details, refer to the +To improve performance, we configure requests to not look for HTTP proxy settings in the environment by setting +requests.Session's trust_env attribute to ``False``. If you don't want this you can manually set +``locust_instance.client.trust_env`` to ``True``. For further details, refer to the `documentation of requests `_. TaskSets ================================ -TaskSets is a way to structure tests of hierarchial web sites/systems. You can :ref:`read more about it here ` +TaskSets is a way to structure tests of hierarchical web sites/systems. You can :ref:`read more about it here ` How to structure your test code ================================ -It's important to remember that the locustfile.py is just an ordinary Python module that is imported -by Locust. From this module you're free to import other python code just as you normally would -in any Python program. The current working directory is automatically added to python's ``sys.path``, -so any python file/module/packages that resides in the working directory can be imported using the +It's important to remember that the locustfile.py is just an ordinary Python module that is imported +by Locust. From this module you're free to import other python code just as you normally would +in any Python program. The current working directory is automatically added to python's ``sys.path``, +so any python file/module/packages that resides in the working directory can be imported using the python ``import`` statement. -For small tests, keeping all of the test code in a single ``locustfile.py`` should work fine, but for -larger test suites, you'll probably want to split the code into multiple files and directories. +For small tests, keeping all of the test code in a single ``locustfile.py`` should work fine, but for +larger test suites, you'll probably want to split the code into multiple files and directories. -How you structure the test source code is of course entirely up to you, but we recommend that you +How you structure the test source code is of course entirely up to you, but we recommend that you follow Python best practices. Here's an example file structure of an imaginary Locust project: * Project root * ``common/`` - + * ``__init__.py`` * ``auth.py`` * ``config.py`` @@ -618,12 +618,12 @@ A project with multiple different locustfiles could also keep them in a separate * Project root * ``common/`` - + * ``__init__.py`` * ``auth.py`` * ``config.py`` * ``locustfiles/`` - + * ``api.py`` * ``website.py`` * ``requirements.txt`` diff --git a/locust/clients.py b/locust/clients.py index c67233c73e..e0b4c18d88 100644 --- a/locust/clients.py +++ b/locust/clients.py @@ -54,7 +54,7 @@ def __init__(self, base_url, request_event, user, *args, **kwargs): # User can group name, or use the group context manager to gather performance statistics under a specific name # This is an alternative to passing in the "name" parameter to the requests function - self.group_name = None + self.request_name = None # Check for basic authentication parsed_url = urlparse(self.base_url) @@ -78,14 +78,14 @@ def _build_url(self, path): return "%s%s" % (self.base_url, path) @contextmanager - def group(self, *, name: str): + def name_request(self, name: str): """Group requests using the "with" keyword""" - self.group_name = name + self.request_name = name try: yield finally: - self.group_name = None + self.request_name = None def request(self, method, url, name=None, catch_response=False, context={}, **kwargs): """ @@ -118,8 +118,8 @@ def request(self, method, url, name=None, catch_response=False, context={}, **kw """ # if group name has been set and no name parameter has been passed in; set the name parameter to group_name - if self.group_name and not name: - name = self.group_name + if self.request_name and not name: + name = self.request_name # prepend url with hostname unless it's already an absolute URL url = self._build_url(url) From ca136464392899ea1d5023ecfd2e7700200d8404 Mon Sep 17 00:00:00 2001 From: serhiy Date: Fri, 20 Aug 2021 17:17:17 +0100 Subject: [PATCH 5/8] Black formatting --- examples/sdk_session_patching/session_patch_locustfile.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/examples/sdk_session_patching/session_patch_locustfile.py b/examples/sdk_session_patching/session_patch_locustfile.py index 345e03d979..2931c3ec8b 100644 --- a/examples/sdk_session_patching/session_patch_locustfile.py +++ b/examples/sdk_session_patching/session_patch_locustfile.py @@ -1,10 +1,9 @@ import locust from locust.user import task -from archivist.archivist import Archivist # Example SDK under test +from archivist.archivist import Archivist # Example SDK under test class ArchivistUser(locust.HttpUser): - def on_start(self): AUTH_TOKEN = None @@ -18,7 +17,7 @@ def on_start(self): @task def Create_assets(self): - """ User creates assets as fast as possible""" + """User creates assets as fast as possible""" while True: self.arch.assets.create(behaviours=[], attrs={}) From f5edd094c87a8f877f89e2029044e1d4a5bdb6f1 Mon Sep 17 00:00:00 2001 From: serhiy Date: Sat, 21 Aug 2021 13:20:16 +0100 Subject: [PATCH 6/8] Add unit tests for the new naming approaches --- locust/test/test_stats.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/locust/test/test_stats.py b/locust/test/test_stats.py index b183b0be82..d5885daddb 100644 --- a/locust/test/test_stats.py +++ b/locust/test/test_stats.py @@ -716,6 +716,17 @@ def test_request_stats_named_endpoint(self): self.locust.client.get("/ultra_fast", name="my_custom_name") self.assertEqual(1, self.runner.stats.get("my_custom_name", "GET").num_requests) + def test_request_stats_named_endpoint_request_name(self): + self.locust.client.request_name = "my_custom_name_1" + self.locust.client.get("/ultra_fast") + self.assertEqual(1, self.runner.stats.get("my_custom_name_1", "GET").num_requests) + self.locust.client.request_name = None + + def test_request_stats_named_endpoint_name_request(self): + with self.locust.client.name_request("my_custom_name_3"): + self.locust.client.get("/ultra_fast") + self.assertEqual(1, self.runner.stats.get("my_custom_name_3", "GET").num_requests) + def test_request_stats_query_variables(self): self.locust.client.get("/ultra_fast?query=1") self.assertEqual(1, self.runner.stats.get("/ultra_fast?query=1", "GET").num_requests) From a1840d58b5cc4be8017709b967f05481dcb4936a Mon Sep 17 00:00:00 2001 From: serhiy Date: Sat, 21 Aug 2021 14:01:40 +0100 Subject: [PATCH 7/8] Split out example of session patching --- docs/index.rst | 5 +++-- docs/testing-other-systems.rst | 10 +++++----- docs/testing-requests-based SDK's.rst | 16 ++++++++++++++++ .../session_patch_locustfile.py | 2 +- 4 files changed, 25 insertions(+), 8 deletions(-) create mode 100644 docs/testing-requests-based SDK's.rst diff --git a/docs/index.rst b/docs/index.rst index 0125c36e2d..9ba51b037f 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -52,6 +52,7 @@ Other functionalities custom-load-shape retrieving-stats testing-other-systems + testing-requests-based SDK's increase-performance extending-locust logging @@ -63,7 +64,7 @@ Further reading / knowledgebase .. toctree :: :maxdepth: 1 - + developing-locust further-reading @@ -74,7 +75,7 @@ API :maxdepth: 4 api - + Changelog diff --git a/docs/testing-other-systems.rst b/docs/testing-other-systems.rst index 8a7b3a9937..bc39665e4b 100644 --- a/docs/testing-other-systems.rst +++ b/docs/testing-other-systems.rst @@ -1,7 +1,7 @@ .. _testing-other-systems: ======================== -Testing other systems +Testing non-HTTP systems ======================== Locust only comes with built-in support for HTTP/HTTPS but it can be extended to load test almost any system. You do this by writing a custom client that triggers :py:attr:`request ` @@ -9,8 +9,8 @@ Locust only comes with built-in support for HTTP/HTTPS but it can be extended to .. note:: It is important that any protocol libraries you use can be `monkey-patched `_ by gevent (if they use the Python ``socket`` module or some other standard library function like ``subprocess`` you will be fine). Otherwise your calls will block the whole Locust/Python process (in practice limiting you to running a single User per worker process) - - Some C libraries cannot be monkey patched by gevent, but allow for other workarounds. For example, if you want to use psycopg2 to performance test PostgreSQL, you can use `psycogreen `_. + + Some C libraries cannot be monkey patched by gevent, but allow for other workarounds. For example, if you want to use psycopg2 to performance test PostgreSQL, you can use `psycogreen `_. Example: writing an XML-RPC User/client ======================================= @@ -33,7 +33,7 @@ The only significant difference is that you need to make gRPC gevent-compatible, .. code-block:: python import grpc.experimental.gevent as grpc_gevent - + grpc_gevent.init_gevent() Dummy server to test: @@ -53,4 +53,4 @@ If you have a prebuilt SDK for a target system that is a essentially a wrapper f .. literalinclude:: ../examples/sdk_session_patching/session_patch_locustfile.py -For more examples of user types, see `locust-plugins `_ (it has users for WebSocket/SocketIO, Kafka, Selenium/WebDriver and more) \ No newline at end of file +For more examples of user types, see `locust-plugins `_ (it has users for WebSocket/SocketIO, Kafka, Selenium/WebDriver and more) \ No newline at end of file diff --git a/docs/testing-requests-based SDK's.rst b/docs/testing-requests-based SDK's.rst new file mode 100644 index 0000000000..24dc21a599 --- /dev/null +++ b/docs/testing-requests-based SDK's.rst @@ -0,0 +1,16 @@ +.. _testing-request-sdks: + +============================= +testing-requests-based SDK's +============================= + + +Example: Patching over SDK's that wrap around Session objects +============================================================= + +If you have a prebuilt SDK for a target system that is a essentially a wrapper for Session object. +You can use the a pattern of patching over the internal session object with the locust provided one: + +.. literalinclude:: ../examples/sdk_session_patching/session_patch_locustfile.py + + diff --git a/examples/sdk_session_patching/session_patch_locustfile.py b/examples/sdk_session_patching/session_patch_locustfile.py index 2931c3ec8b..936e0c3235 100644 --- a/examples/sdk_session_patching/session_patch_locustfile.py +++ b/examples/sdk_session_patching/session_patch_locustfile.py @@ -20,4 +20,4 @@ def Create_assets(self): """User creates assets as fast as possible""" while True: - self.arch.assets.create(behaviours=[], attrs={}) + self.arch.assets.create(behaviours=["Builtin", "RecordEvidence", "Attachments"], attrs={"foo": "bar"}) From d4dd43866af2cda9d816e89c06668c40a3ee32e0 Mon Sep 17 00:00:00 2001 From: serhiy Date: Sat, 21 Aug 2021 14:52:39 +0100 Subject: [PATCH 8/8] Change context manager name to rename_request Remove duplicated documentation --- docs/testing-other-systems.rst | 8 -------- docs/writing-a-locustfile.rst | 6 +++--- locust/clients.py | 2 +- locust/test/test_stats.py | 4 ++-- 4 files changed, 6 insertions(+), 14 deletions(-) diff --git a/docs/testing-other-systems.rst b/docs/testing-other-systems.rst index bc39665e4b..0926cedf71 100644 --- a/docs/testing-other-systems.rst +++ b/docs/testing-other-systems.rst @@ -45,12 +45,4 @@ gRPC client, base User and example usage: .. literalinclude:: ../examples/grpc/locustfile.py -Example: Patching over SDK's that wrap around Session objects -============================================================= - -If you have a prebuilt SDK for a target system that is a essentially a wrapper for Session object. You can use the a pattern of patching over the internal session object with the locust provided one: - -.. literalinclude:: ../examples/sdk_session_patching/session_patch_locustfile.py - - For more examples of user types, see `locust-plugins `_ (it has users for WebSocket/SocketIO, Kafka, Selenium/WebDriver and more) \ No newline at end of file diff --git a/docs/writing-a-locustfile.rst b/docs/writing-a-locustfile.rst index 172c4c0cf1..f929e31f60 100644 --- a/docs/writing-a-locustfile.rst +++ b/docs/writing-a-locustfile.rst @@ -557,7 +557,7 @@ wrap a Requests session. An alternative say of grouping requests is provided By self.client.get("/blog?id=%i" % i) self.client.request_name=None -If You want to chain multiple groupings with minimal boilerplate, you can use the ``client.name_request()`` context manager. +If You want to chain multiple groupings with minimal boilerplate, you can use the ``client.rename_request()`` context manager. .. code-block:: python @@ -565,12 +565,12 @@ If You want to chain multiple groupings with minimal boilerplate, you can use th def multiple_groupings_example(self): # Statistics for these requests will be grouped under: /blog/?id=[id] - with self.client.name_request("/blog?id=[id]"): + with self.client.rename_request("/blog?id=[id]"): for i in range(10): self.client.get("/blog?id=%i" % i) # Statistics for these requests will be grouped under: /article/?id=[id] - with self.client.name_request("/article?id=[id]"): + with self.client.rename_request("/article?id=[id]"): for i in range(10): self.client.get("/article?id=%i" % i) diff --git a/locust/clients.py b/locust/clients.py index e0b4c18d88..17c189e7f6 100644 --- a/locust/clients.py +++ b/locust/clients.py @@ -78,7 +78,7 @@ def _build_url(self, path): return "%s%s" % (self.base_url, path) @contextmanager - def name_request(self, name: str): + def rename_request(self, name: str): """Group requests using the "with" keyword""" self.request_name = name diff --git a/locust/test/test_stats.py b/locust/test/test_stats.py index d5885daddb..161c43e07b 100644 --- a/locust/test/test_stats.py +++ b/locust/test/test_stats.py @@ -722,8 +722,8 @@ def test_request_stats_named_endpoint_request_name(self): self.assertEqual(1, self.runner.stats.get("my_custom_name_1", "GET").num_requests) self.locust.client.request_name = None - def test_request_stats_named_endpoint_name_request(self): - with self.locust.client.name_request("my_custom_name_3"): + def test_request_stats_named_endpoint_rename_request(self): + with self.locust.client.rename_request("my_custom_name_3"): self.locust.client.get("/ultra_fast") self.assertEqual(1, self.runner.stats.get("my_custom_name_3", "GET").num_requests)