Skip to content

Commit

Permalink
Merge pull request #1266 from locustio/v1.0
Browse files Browse the repository at this point in the history
Work towards 1.0. Refactoring of runners/events/web ui. Getting rid of global state.
  • Loading branch information
heyman authored Mar 10, 2020
2 parents ed3b263 + 4115010 commit 8046eab
Show file tree
Hide file tree
Showing 39 changed files with 1,853 additions and 1,473 deletions.
28 changes: 19 additions & 9 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -77,25 +77,35 @@ InterruptTaskSet Exception
.. autoexception:: locust.exception.InterruptTaskSet


Environment class
=================
.. autoclass:: locust.env.Environment
:members:


.. _events:

Event hooks
===========

The event hooks are instances of the **locust.events.EventHook** class:
Locust provide event hooks that can be used to extend Locus in various ways.

.. autoclass:: locust.events.EventHook
The following event hooks are available under :py:attr:`Environment.events <locust.env.Environment.events>`,
and there's also a reference to these events under ``locust.events`` that can be used at the module level
of locust scripts (since the Environment instance hasn't been created when the locustfile is imported).

.. note::
.. autoclass:: locust.event.Events
:members:

It's highly recommended that you add a wildcard keyword argument in your event listeners
to prevent your code from breaking if new arguments are added in a future version.

Available hooks
EventHook class
---------------

The following event hooks are available under the **locust.events** module:
The event hooks are instances of the **locust.events.EventHook** class:

.. autoclass:: locust.event.EventHook

.. automodule:: locust.events
:members: request_success, request_failure, locust_error, report_to_master, slave_report, hatch_complete, quitting
.. note::

It's highly recommended that you add a wildcard keyword argument in your event listeners
to prevent your code from breaking if new arguments are added in a future version.
31 changes: 20 additions & 11 deletions docs/extending-locust.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,21 @@
Extending Locust
=================

Locust comes with a number of events that provides hooks for extending locust in different ways.
Locust comes with a number of events hooks that can be used to extend Locust in different ways.

Event listeners can be registered at the module level in a locust file. Here's an example::
Event hooks live on the Environment instance under the :py:attr:`events <locust.env.Environment.events>`
attribute. However, since the Environment instance hasn't been created when locustfiles are imported,
the events object can also be accessed at the module level of the locustfile through the
:py:attr:`locust.events` variable.

from locust import events
Here's an example on how to set up an event listener::

from locust import events
@events.request_success.add_listener
def my_success_handler(request_type, name, response_time, response_length, **kw):
print "Successfully fetched: %s" % (name)
print("Successfully made a request to: %s" % name)

events.request_success += my_success_handler

.. note::

Expand All @@ -29,12 +34,16 @@ Adding Web Routes
==================

Locust uses Flask to serve the web UI and therefore it is easy to add web end-points to the web UI.
Just import the Flask app in your locustfile and set up a new route::

from locust import web
By listening to the :py:attr:`init <locust.event.Events.init>` event, we can retrieve a reference
to the Flask app instance and use that to set up a new route::

@web.app.route("/added_page")
def my_added_page():
return "Another page"
from locust import events
@events.init.add_listener
def on_locust_init(web_ui, **kw):
@web_ui.app.route("/added_page")
def my_added_page():
return "Another page"

You should now be able to start locust and browse to http://127.0.0.1:8089/added_page

1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ Other functionalities
testing-other-systems
extending-locust
logging
use-as-lib
API
Expand Down
12 changes: 3 additions & 9 deletions docs/installation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -59,16 +59,10 @@ Once you've done that you should be able to just ``pip install locustio``.
Installing Locust on macOS
--------------------------

The following is currently the shortest path to installing gevent on OS X using Homebrew.
Make sure you have a working installation of Python 3.6 or higher and follow the above
instructions. `Homebrew <http://mxcl.github.com/homebrew/>`_ can be used to install Python
on macOS.

#. Install `Homebrew <http://mxcl.github.com/homebrew/>`_.
#. Install libev (dependency for gevent):

.. code-block:: console
brew install libev
#. Then follow the above instructions.

Increasing Maximum Number of Open Files Limit
---------------------------------------------
Expand Down
2 changes: 1 addition & 1 deletion docs/quickstart.rst
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ For example, for an exponentially distributed wait time with average of 1 second
class WebsiteUser(HttpLocust):
task_set = UserBehaviour
wait_time = lambda self: random.expovariate(1)*1000
wait_time = lambda self: random.expovariate(1)
Start Locust
Expand Down
6 changes: 3 additions & 3 deletions docs/running-locust-in-step-load-mode.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@ Options
=======

``--step-load``
------------
----------------

Enable Step Load mode to monitor how performance metrics varies when user load increases.


``--step-clients``
-----------
-------------------

Client count to increase by step in Step Load mode. Only used together with ``--step-load``.

Expand All @@ -33,7 +33,7 @@ Step duration in Step Load mode, e.g. (300s, 20m, 3h, 1h30m, etc.). Only used to


Running Locust in step load mode without the web UI
---------------------------------
----------------------------------------------------

If you want to run Locust in step load mode without the web UI, you can do that with ``--step-clients`` and ``--step-time``:

Expand Down
11 changes: 6 additions & 5 deletions docs/testing-other-systems.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ Testing other systems using custom clients

Locust was built with HTTP as its main target. However, it can easily be extended to load test
any request/response based system, by writing a custom client that triggers
:py:attr:`request_success <locust.events.request_success>` and
:py:attr:`request_failure <locust.events.request_failure>` events.
:py:attr:`request_success <locust.event.Events.request_success>` and
:py:attr:`request_failure <locust.event.Events.request_failure>` events.

Sample XML-RPC Locust client
============================
Expand All @@ -19,10 +19,11 @@ If you've written Locust tests before, you'll recognize the class called *ApiUse
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
library's :py:class:`xmlrpclib.ServerProxy`. It basically just proxies the function calls, but with the
important addition of firing :py:attr:`locust.events.request_success` and :py:attr:`locust.events.request_failure`
events, which will make all calls reported in Locust's statistics.
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.

Here's an implementation of an XML-RPC server that would work as a server for the code above:

.. literalinclude:: ../examples/custom_xmlrpc_client/server.py

46 changes: 46 additions & 0 deletions docs/use-as-lib.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
==========================
Using Locust as a library
==========================

It's possible to use Locust as a library instead of running Locust by invoking the ``locust`` command.

Here's an example::

import gevent
from locust import HttpLocust, TaskSet, task, between
from locust.runners import LocalLocustRunner
from locust.env import Environment
from locust.stats import stats_printer
from locust.log import setup_logging
from locust.web import WebUI
setup_logging("INFO", None)
class User(HttpLocust):
wait_time = between(1, 3)
host = "https://docs.locust.io"
class task_set(TaskSet):
@task
def my_task(self):
self.client.get("/")
@task
def task_404(self):
self.client.get("/non-existing-path")
# setup Environment and Runner
env = Environment(locust_classes=[User])
runner = LocalLocustRunner(environment=env)
# start a WebUI instance
web_ui = WebUI(runner=runner, environment=env)
gevent.spawn(lambda: web_ui.start("127.0.0.1", 8089))
# start a greenlet that periodically outputs the current stats
gevent.spawn(stats_printer(env.stats))
# start the test
runner.start(1, hatch_rate=10)
# wait for the greenlets (indefinitely)
runner.greenlet.join()
65 changes: 27 additions & 38 deletions docs/writing-a-locustfile.rst
Original file line number Diff line number Diff line change
Expand Up @@ -473,61 +473,50 @@ Example:
for i in range(10):
self.client.get("/blog?id=%i" % i, name="/blog?id=[id]")
Common libraries
=================
Often, people wish to group multiple locustfiles that share common libraries. In that case, it is important
to define the *project root* to be the directory where you invoke locust, and it is suggested that all
locustfiles live somewhere beneath the project root.
How to structure your test code
================================

A flat file structure works out of the box:
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.

* project root
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.

* ``commonlib_config.py``
How you structure the test source code is ofcourse entirely up to you, but we recommend that you
follow Python best practices. Here's an example file structure of an imaginary Locust project:

* ``commonlib_auth.py``

* ``locustfile_web_app.py``

* ``locustfile_api.py``

* ``locustfile_ecommerce.py``

The locustfiles may import common libraries using, e.g. ``import commonlib_auth``. This approach does not
cleanly separate common libraries from locust files, however.

Subdirectories can be a cleaner approach (see example below), but locust will only import modules relative to
the directory in which the running locustfile is placed. If you wish to import from your project root (i.e. the
location where you are running the locust command), make sure to write ``sys.path.append(os.getcwd())`` in your
locust file(s) before importing any common libraries---this will make the project root (i.e. the current
working directory) importable.

* project root

* ``__init__.py``
* Project root

* ``common/``

* ``__init__.py``

* ``auth.py``
* ``config.py``
* ``locustfile.py``
* ``requirements.txt`` (External Python dependencies is often kept in a requirements.txt)

* ``auth.py``
A project with multiple different locustfiles could also keep them in a separate subdirectory:

* ``locustfiles/``
* Project root

* ``common/``

* ``__init__.py``

* ``web_app.py``

* ``auth.py``
* ``config.py``
* ``locustfiles/``

* ``api.py``
* ``website.py``
* ``requirements.txt``

* ``ecommerce.py``

With the above project structure, your locust files can import common libraries using:
With any ofthe above project structure, your locustfile can import common libraries using:

.. code-block:: python
sys.path.append(os.getcwd())
import common.auth
26 changes: 26 additions & 0 deletions examples/add_command_line_argument.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from locust import HttpLocust, TaskSet, task, between
from locust import events


@events.init_command_line_parser.add_listener
def _(parser):
parser.add_argument(
'--custom-argument',
help="It's working"
)

@events.init.add_listener
def _(environment, **kw):
print("Custom argument supplied: %s" % environment.options.custom_argument)


class WebsiteUser(HttpLocust):
"""
Locust user class that does requests to the locust web server running on localhost
"""
host = "http://127.0.0.1:8089"
wait_time = between(2, 5)
class task_set(TaskSet):
@task
def my_task(self):
pass
2 changes: 1 addition & 1 deletion examples/custom_xmlrpc_client/server.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import random
import time
from SimpleXMLRPCServer import SimpleXMLRPCServer
from xmlrpc.server import SimpleXMLRPCServer


def get_time():
Expand Down
16 changes: 10 additions & 6 deletions examples/custom_xmlrpc_client/xmlrpc_locustfile.py
Original file line number Diff line number Diff line change
@@ -1,27 +1,30 @@
import time
import xmlrpclib
from xmlrpc.client import ServerProxy, Fault

from locust import Locust, TaskSet, events, task, between


class XmlRpcClient(xmlrpclib.ServerProxy):
class XmlRpcClient(ServerProxy):
"""
Simple, sample XML RPC client implementation that wraps xmlrpclib.ServerProxy and
fires locust events on request_success and request_failure, so that all requests
gets tracked in locust's statistics.
"""

_locust_environment = None

def __getattr__(self, name):
func = xmlrpclib.ServerProxy.__getattr__(self, name)
func = ServerProxy.__getattr__(self, name)
def wrapper(*args, **kwargs):
start_time = time.time()
try:
result = func(*args, **kwargs)
except xmlrpclib.Fault as e:
except Fault as e:
total_time = int((time.time() - start_time) * 1000)
events.request_failure.fire(request_type="xmlrpc", name=name, response_time=total_time, exception=e)
self._locust_environment.events.request_failure.fire(request_type="xmlrpc", name=name, response_time=total_time, exception=e)
else:
total_time = int((time.time() - start_time) * 1000)
events.request_success.fire(request_type="xmlrpc", name=name, response_time=total_time, response_length=0)
self._locust_environment.events.request_success.fire(request_type="xmlrpc", name=name, response_time=total_time, response_length=0)
# In this example, I've hardcoded response_length=0. If we would want the response length to be
# reported correctly in the statistics, we would probably need to hook in at a lower level

Expand All @@ -36,6 +39,7 @@ class XmlRpcLocust(Locust):
def __init__(self, *args, **kwargs):
super(XmlRpcLocust, self).__init__(*args, **kwargs)
self.client = XmlRpcClient(self.host)
self.client._locust_environment = self.environment


class ApiUser(XmlRpcLocust):
Expand Down
Loading

0 comments on commit 8046eab

Please sign in to comment.