diff --git a/docs/increase-performance.rst b/docs/increase-performance.rst
new file mode 100644
index 0000000000..f27da73095
--- /dev/null
+++ b/docs/increase-performance.rst
@@ -0,0 +1,62 @@
+.. _increase-performance:
+
+==============================================================
+Increase Locust's performance with a faster HTTP client
+==============================================================
+
+Locust's default HTTP client uses `python-requests `_.
+The reason for this is that requests is a very well-maintained python package, that
+provides a really nice API, that many python developers are familiar with. Therefore,
+in many cases, we recommend that you use the default :py:class:`HttpLocust `
+which uses requests. However, if you're planning to run really large scale scale tests,
+Locust comes with an alternative HTTP client,
+:py:class:`FastHttpLocust ` which
+uses `geventhttpclient `_ instead of requests.
+This client is significantly faster, and we've seen 5x-6x performance increases for making
+HTTP-requests. This does not necessarily mean that the number of users one can simulate
+per CPU core will automatically increase 5x-6x, since it also depends on what else
+the load testing script does. However, if your locust scripts are spending most of their
+CPU time in making HTTP-requests, you are likely to see signifant performance gains.
+
+
+How to use FastHttpLocust
+===========================
+
+First, you need to install the geventhttplocust python package::
+
+ pip install geventhttpclient
+
+Then you just subclass FastHttpLocust instead of HttpLocust::
+
+ from locust import TaskSet, task
+ from locust.contrib.fasthttp import FastHttpLocust
+
+ class MyTaskSet(TaskSet):
+ @task
+ def index(self):
+ response = self.client.get("/")
+
+ class MyLocust(FastHttpLocust):
+ task_set = MyTaskSet
+ min_wait = 1000
+ max_wait = 60000
+
+
+.. note::
+
+ FastHttpLocust uses a whole other HTTP client implementation, with a different API, compared to
+ the default HttpLocust that uses python-requests. Therefore FastHttpLocust might not work as a d
+ rop-in replacement for HttpLocust, depending on how the HttpClient is used.
+
+
+API
+===
+
+FastHttpSession class
+=====================
+
+.. autoclass:: locust.contrib.fasthttp.FastHttpSession
+ :members: __init__, request, get, post, delete, put, head, options, patch
+
+.. autoclass:: locust.contrib.fasthttp.FastResponse
+ :members: content, text, headers
diff --git a/docs/index.rst b/docs/index.rst
index 1f001a8e2b..f0219d97ab 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -31,6 +31,7 @@ Locust Documentation
running-locust-distributed
running-locust-without-web-ui
+ increase-performance
.. toctree ::
:maxdepth: 4
diff --git a/docs/running-locust-distributed.rst b/docs/running-locust-distributed.rst
index d0cb704871..6bd0ad1397 100644
--- a/docs/running-locust-distributed.rst
+++ b/docs/running-locust-distributed.rst
@@ -91,3 +91,10 @@ Running Locust distributed without the web UI
=============================================
See :ref:`running-locust-distributed-without-web-ui`
+
+
+Increase Locust's performance
+=============================
+
+If your planning to run large-scale load tests you might be interested to use the alternative
+HTTP client that's shipped with Locust. You can read more about it here: :ref:`increase-performance`
diff --git a/docs/writing-a-locustfile.rst b/docs/writing-a-locustfile.rst
index 104998541b..5c1460fffe 100644
--- a/docs/writing-a-locustfile.rst
+++ b/docs/writing-a-locustfile.rst
@@ -73,7 +73,7 @@ classes. Say for example, web users are three times more likely than mobile user
The *host* attribute
--------------------
-The host attribute is a URL prefix (i.e. "https://google.com") to the host that is to be loaded.
+The host attribute is a URL prefix (i.e. "https://google.com") to the host that is to be loaded.
Usually, this is specified on the command line, using the :code:`--host` option, when locust is started.
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.
@@ -322,7 +322,7 @@ Since many setup and cleanup operations are dependent on each other, here is the
In general, the setup and teardown methods should be complementary.
-Making HTTP requests
+Making HTTP requests
=====================
So far, we've only covered the task scheduling part of a Locust user. In order to actually load test
@@ -405,6 +405,8 @@ Response object. The request will be reported as a failure in Locust's statistic
Response's *content* attribute will be set to None, and its *status_code* will be 0.
+.. _catch-response:
+
Manually controlling if a request should be considered successful or a failure
------------------------------------------------------------------------------
diff --git a/locust/contrib/__init__.py b/locust/contrib/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/locust/contrib/fasthttp.py b/locust/contrib/fasthttp.py
new file mode 100644
index 0000000000..880b6e85b4
--- /dev/null
+++ b/locust/contrib/fasthttp.py
@@ -0,0 +1,383 @@
+from __future__ import absolute_import
+
+import chardet
+import re
+import six
+import socket
+from base64 import b64encode
+from six.moves.urllib.parse import urlparse, urlunparse
+from ssl import SSLError
+from timeit import default_timer
+
+if six.PY2:
+ from cookielib import CookieJar
+ class ConnectionRefusedError(Exception):
+ # ConnectionRefusedError doesn't exist in python 2, so we'll
+ # define a dummy class to avoid a NameError
+ pass
+ str = unicode
+else:
+ from http.cookiejar import CookieJar
+
+from gevent.timeout import Timeout
+from geventhttpclient.useragent import UserAgent, CompatRequest, CompatResponse, ConnectionError
+from geventhttpclient.response import HTTPConnectionClosed
+
+from locust import events
+from locust.core import Locust
+from locust.exception import LocustError, CatchResponseError, ResponseError
+
+
+# Monkey patch geventhttpclient.useragent.CompatRequest so that Cookiejar works with Python >= 3.3
+# More info: https://github.com/requests/requests/pull/871
+CompatRequest.unverifiable = False
+
+# Regexp for checking if an absolute URL was specified
+absolute_http_url_regexp = re.compile(r"^https?://", re.I)
+
+# List of exceptions that can be raised by geventhttpclient when sending an HTTP request,
+# and that should result in a Locust failure
+FAILURE_EXCEPTIONS = (ConnectionError, ConnectionRefusedError, socket.error, \
+ SSLError, Timeout, HTTPConnectionClosed)
+
+
+def _construct_basic_auth_str(username, password):
+ """Construct Authorization header value to be used in HTTP Basic Auth"""
+ if isinstance(username, str):
+ username = username.encode('latin1')
+ if isinstance(password, str):
+ password = password.encode('latin1')
+ return 'Basic ' + b64encode(b':'.join((username, password))).strip().decode("ascii")
+
+
+class FastHttpLocust(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.
+
+ This class creates a *client* attribute on instantiation which is an HTTP client with support
+ for keeping a user session between requests.
+ """
+
+ client = None
+ """
+ Instance of HttpSession that is created upon instantiation of Locust.
+ The client support cookies, and therefore keeps the session between HTTP requests.
+ """
+
+ def __init__(self):
+ super(FastHttpLocust, self).__init__()
+ 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.")
+ if not re.match(r"^https?://[^/]+$", self.host, re.I):
+ raise LocustError("Invalid host (`%s`). The specified host string must be a base URL without a trailing slash. E.g. http://example.org" % self.host)
+
+ self.client = FastHttpSession(base_url=self.host)
+
+
+class FastHttpSession(object):
+ auth_header = None
+
+ def __init__(self, base_url):
+ self.base_url = base_url
+ self.cookiejar = CookieJar()
+ self.client = LocustUserAgent(max_retries=1, cookiejar=self.cookiejar)
+
+ # Check for basic authentication
+ parsed_url = urlparse(self.base_url)
+ if parsed_url.username and parsed_url.password:
+ netloc = parsed_url.hostname
+ if parsed_url.port:
+ netloc += ":%d" % parsed_url.port
+
+ # remove username and password from the base_url
+ self.base_url = urlunparse((parsed_url.scheme, netloc, parsed_url.path, parsed_url.params, parsed_url.query, parsed_url.fragment))
+ # store authentication header (we construct this by using _basic_auth_str() function from requests.auth)
+ self.auth_header = _construct_basic_auth_str(parsed_url.username, parsed_url.password)
+
+ def _build_url(self, path):
+ """ prepend url with hostname unless it's already an absolute URL """
+ if absolute_http_url_regexp.match(path):
+ return path
+ else:
+ return "%s%s" % (self.base_url, path)
+
+ def _send_request_safe_mode(self, method, url, **kwargs):
+ """
+ Send an HTTP request, and catch any exception that might occur due to either
+ connection problems, or invalid HTTP status codes
+ """
+ try:
+ return self.client.urlopen(url, method=method, **kwargs)
+ except FAILURE_EXCEPTIONS as e:
+ if hasattr(e, "response"):
+ r = e.response
+ else:
+ r = ErrorResponse()
+ r.error = e
+ return r
+
+ def request(self, method, path, name=None, data=None, catch_response=False, stream=False, \
+ headers=None, auth=None, **kwargs):
+ """
+ Send and HTTP request
+ Returns :py:class:`locust.contrib.fasthttp.FastResponse` object.
+
+ :param method: method for the new :class:`Request` object.
+ :param path: Path that will be concatenated with the base host URL that has been specified.
+ Can also be a full URL, in which case the full URL will be requested, and the base host
+ is ignored.
+ :param name: (optional) An argument that can be specified to use as label in Locust's
+ statistics instead of the URL path. This can be used to group different URL's
+ that are requested into a single entry in Locust's statistics.
+ :param catch_response: (optional) Boolean argument that, if set, can be used to make a request
+ return a context manager to work as argument to a with statement. This will allow the
+ request to be marked as a fail based on the content of the response, even if the response
+ code is ok (2xx). The opposite also works, one can use catch_response to catch a request
+ and then mark it as successful even if the response code was not (i.e 500 or 404).
+ :param data: (optional) Dictionary or bytes to send in the body of the request.
+ :param headers: (optional) Dictionary of HTTP Headers to send with the request.
+ :param auth: (optional) Auth (username, password) tuple to enable Basic HTTP Auth.
+ :param stream: (optional) If set to true the response body will not be consumed immediately
+ and can instead be consumed by accessing the stream attribute on the Response object.
+ Another side effect of setting stream to True is that the time for downloading the response
+ content will not be accounted for in the request time that is reported by Locust.
+ """
+ # prepend url with hostname unless it's already an absolute URL
+ url = self._build_url(path)
+
+ # store meta data that is used when reporting the request to locust's statistics
+ request_meta = {}
+ # set up pre_request hook for attaching meta data to the request object
+ request_meta["method"] = method
+ request_meta["start_time"] = default_timer()
+ request_meta["name"] = name or path
+
+ if auth:
+ headers = headers or {}
+ headers['Authorization'] = _construct_basic_auth_str(auth[0], auth[1])
+ elif self.auth_header:
+ headers = headers or {}
+ headers['Authorization'] = self.auth_header
+
+ # send request, and catch any exceptions
+ response = self._send_request_safe_mode(method, url, payload=data, headers=headers, **kwargs)
+
+ # get the length of the content, but if the argument stream is set to True, we take
+ # the size from the content-length header, in order to not trigger fetching of the body
+ if stream:
+ request_meta["content_size"] = int(response.headers.get("content-length") or 0)
+ else:
+ request_meta["content_size"] = len(response.content or "")
+
+ # Record the consumed time
+ # Note: This is intentionally placed after we record the content_size above, since
+ # we'll then trigger fetching of the body (unless stream=True)
+ request_meta["response_time"] = int((default_timer() - request_meta["start_time"]) * 1000)
+
+ if catch_response:
+ response.locust_request_meta = request_meta
+ return ResponseContextManager(response)
+ else:
+ try:
+ response.raise_for_status()
+ except FAILURE_EXCEPTIONS as e:
+ events.request_failure.fire(
+ request_type=request_meta["method"],
+ name=request_meta["name"],
+ response_time=request_meta["response_time"],
+ exception=e,
+ )
+ else:
+ events.request_success.fire(
+ request_type=request_meta["method"],
+ name=request_meta["name"],
+ response_time=request_meta["response_time"],
+ response_length=request_meta["content_size"],
+ )
+ return response
+
+ def delete(self, path, **kwargs):
+ return self.request("DELETE", path, **kwargs)
+
+ def get(self, path, **kwargs):
+ """Sends a GET request"""
+ return self.request("GET", path, **kwargs)
+
+ def head(self, path, **kwargs):
+ """Sends a HEAD request"""
+ return self.request("HEAD", path, **kwargs)
+
+ def options(self, path, **kwargs):
+ """Sends a OPTIONS request"""
+ return self.request("OPTIONS", path, **kwargs)
+
+ def patch(self, path, data=None, **kwargs):
+ """Sends a POST request"""
+ return self.request("PATCH", path, data=data, **kwargs)
+
+ def post(self, path, data=None, **kwargs):
+ """Sends a POST request"""
+ return self.request("POST", path, data=data, **kwargs)
+
+ def put(self, path, data=None, **kwargs):
+ """Sends a PUT request"""
+ return self.request("PUT", path, data=data, **kwargs)
+
+
+class FastResponse(CompatResponse):
+ headers = None
+ """Dict like object containing the response headers"""
+
+ _response = None
+
+ @property
+ def text(self):
+ """
+ Returns the text content of the response as a decoded string
+ (unicode on python2)
+ """
+ # Decode unicode from detected encoding.
+ try:
+ content = str(self.content, self.apparent_encoding, errors='replace')
+ except (LookupError, TypeError):
+ # A LookupError is raised if the encoding was not found which could
+ # indicate a misspelling or similar mistake.
+ #
+ # A TypeError can be raised if encoding is None
+ #
+ # Fallback to decode without specifying encoding
+ content = str(self.content, errors='replace')
+ return content
+
+ @property
+ def apparent_encoding(self):
+ """The apparent encoding, provided by the chardet library."""
+ return chardet.detect(self.content)['encoding']
+
+ def raise_for_status(self):
+ """Raise any connection errors that occured during the request"""
+ if hasattr(self, 'error') and self.error:
+ raise self.error
+
+ @property
+ def status_code(self):
+ """
+ We override status_code in order to return None if no valid response was
+ returned. E.g. in the case of connection errors
+ """
+ return self._response is not None and self._response.get_code() or 0
+
+ def _content(self):
+ if self.headers is None:
+ return None
+ return super(FastResponse, self)._content()
+
+
+class ErrorResponse(object):
+ """
+ This is used as a dummy response object when geventhttpclient raises an error
+ that doesn't have a real Response object attached. E.g. a socket error or similar
+ """
+ headers = None
+ content = None
+ status_code = 0
+ error = None
+ text = None
+ def raise_for_status(self):
+ raise self.error
+
+
+class LocustUserAgent(UserAgent):
+ response_type = FastResponse
+
+ def _urlopen(self, request):
+ """Override _urlopen() in order to make it use the response_type attribute"""
+ client = self.clientpool.get_client(request.url_split)
+ resp = client.request(request.method, request.url_split.request_uri,
+ body=request.payload, headers=request.headers)
+ return self.response_type(resp, request=request, sent_request=resp._sent_request)
+
+
+class ResponseContextManager(FastResponse):
+ """
+ A Response class that also acts as a context manager that provides the ability to manually
+ control if an HTTP request should be marked as successful or a failure in Locust's statistics
+
+ This class is a subclass of :py:class:`FastResponse `
+ with two additional methods: :py:meth:`success `
+ and :py:meth:`failure `.
+ """
+
+ _is_reported = False
+
+ def __init__(self, response):
+ # copy data from response to this object
+ self.__dict__ = response.__dict__
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, exc, value, traceback):
+ if self._is_reported:
+ # if the user has already manually marked this response as failure or success
+ # we can ignore the default haviour of letting the response code determine the outcome
+ return exc is None
+
+ if exc:
+ if isinstance(value, ResponseError):
+ self.failure(value)
+ else:
+ return False
+ else:
+ try:
+ self.raise_for_status()
+ except FAILURE_EXCEPTIONS as e:
+ self.failure(e)
+ else:
+ self.success()
+ return True
+
+ def success(self):
+ """
+ Report the response as successful
+
+ Example::
+
+ with self.client.get("/does/not/exist", catch_response=True) as response:
+ if response.status_code == 404:
+ response.success()
+ """
+ events.request_success.fire(
+ request_type=self.locust_request_meta["method"],
+ name=self.locust_request_meta["name"],
+ response_time=self.locust_request_meta["response_time"],
+ response_length=self.locust_request_meta["content_size"],
+ )
+ self._is_reported = True
+
+ def failure(self, exc):
+ """
+ Report the response as a failure.
+
+ exc can be either a python exception, or a string in which case it will
+ be wrapped inside a CatchResponseError.
+
+ Example::
+
+ with self.client.get("/", catch_response=True) as response:
+ if response.content == "":
+ response.failure("No data")
+ """
+ if isinstance(exc, six.string_types):
+ exc = CatchResponseError(exc)
+
+ events.request_failure.fire(
+ request_type=self.locust_request_meta["method"],
+ name=self.locust_request_meta["name"],
+ response_time=self.locust_request_meta["response_time"],
+ exception=exc,
+ )
+ self._is_reported = True
diff --git a/locust/test/test_client.py b/locust/test/test_client.py
index e33019ebcf..82fe866a82 100644
--- a/locust/test/test_client.py
+++ b/locust/test/test_client.py
@@ -94,6 +94,6 @@ def test_options(self):
self.assertEqual(200, r.status_code)
self.assertEqual("", r.content.decode())
self.assertEqual(
- set(["OPTIONS", "DELETE", "PUT", "GET", "POST", "HEAD"]),
+ set(["OPTIONS", "DELETE", "PUT", "GET", "POST", "HEAD", "PATCH"]),
set(r.headers["allow"].split(", ")),
)
diff --git a/locust/test/test_fasthttp.py b/locust/test/test_fasthttp.py
new file mode 100644
index 0000000000..f0a45107ec
--- /dev/null
+++ b/locust/test/test_fasthttp.py
@@ -0,0 +1,389 @@
+import six
+import socket
+
+from locust import TaskSet, task, events
+from locust.core import LocustError
+from locust.contrib.fasthttp import FastHttpSession, FastHttpLocust
+from locust.exception import CatchResponseError, InterruptTaskSet, ResponseError
+from locust.stats import global_stats
+
+from .testcases import WebserverTestCase
+
+
+class TestFastHttpSession(WebserverTestCase):
+ def test_get(self):
+ s = FastHttpSession("http://127.0.0.1:%i" % self.port)
+ r = s.get("/ultra_fast")
+ self.assertEqual(200, r.status_code)
+
+ def test_connection_error(self):
+ global_stats.clear_all()
+ s = FastHttpSession("http://localhost:1")
+ r = s.get("/", timeout=0.1)
+ self.assertEqual(r.status_code, 0)
+ self.assertEqual(None, r.content)
+ self.assertEqual(1, len(global_stats.errors))
+ if six.PY2:
+ self.assertTrue(isinstance(r.error, socket.error))
+ self.assertTrue(isinstance(six.next(six.itervalues(global_stats.errors)).error, socket.error))
+ else:
+ self.assertTrue(isinstance(r.error, ConnectionRefusedError))
+ self.assertTrue(isinstance(six.next(six.itervalues(global_stats.errors)).error, ConnectionRefusedError))
+
+ def test_404(self):
+ global_stats.clear_all()
+ s = FastHttpSession("http://127.0.0.1:%i" % self.port)
+ r = s.get("/does_not_exist")
+ self.assertEqual(404, r.status_code)
+ self.assertEqual(1, global_stats.get("/does_not_exist", "GET").num_failures)
+
+ def test_streaming_response(self):
+ """
+ Test a request to an endpoint that returns a streaming response
+ """
+ s = FastHttpSession("http://127.0.0.1:%i" % self.port)
+ r = s.get("/streaming/30")
+
+ # verify that the time reported includes the download time of the whole streamed response
+ self.assertGreater(global_stats.get("/streaming/30", method="GET").avg_response_time, 250)
+ global_stats.clear_all()
+
+ # verify that response time does NOT include whole download time, when using stream=True
+ r = s.get("/streaming/30", stream=True)
+ self.assertGreaterEqual(global_stats.get("/streaming/30", method="GET").avg_response_time, 0)
+ self.assertLess(global_stats.get("/streaming/30", method="GET").avg_response_time, 250)
+
+ # download the content of the streaming response (so we don't get an ugly exception in the log)
+ _ = r.content
+
+ def test_slow_redirect(self):
+ s = FastHttpSession("http://127.0.0.1:%i" % self.port)
+ url = "/redirect?url=/redirect?delay=0.5"
+ r = s.get(url)
+ stats = global_stats.get(url, method="GET")
+ self.assertEqual(1, stats.num_requests)
+ self.assertGreater(stats.avg_response_time, 500)
+
+ def test_post_redirect(self):
+ s = FastHttpSession("http://127.0.0.1:%i" % self.port)
+ url = "/redirect"
+ r = s.post(url)
+ self.assertEqual(200, r.status_code)
+ post_stats = global_stats.get(url, method="POST")
+ get_stats = global_stats.get(url, method="GET")
+ self.assertEqual(1, post_stats.num_requests)
+ self.assertEqual(0, get_stats.num_requests)
+
+ def test_cookie(self):
+ s = FastHttpSession("http://127.0.0.1:%i" % self.port)
+ r = s.post("/set_cookie?name=testcookie&value=1337")
+ self.assertEqual(200, r.status_code)
+ r = s.get("/get_cookie?name=testcookie")
+ self.assertEqual('1337', r.content.decode())
+
+ def test_head(self):
+ s = FastHttpSession("http://127.0.0.1:%i" % self.port)
+ r = s.head("/request_method")
+ self.assertEqual(200, r.status_code)
+ self.assertEqual("", r.content.decode())
+
+ def test_delete(self):
+ s = FastHttpSession("http://127.0.0.1:%i" % self.port)
+ r = s.delete("/request_method")
+ self.assertEqual(200, r.status_code)
+ self.assertEqual("DELETE", r.content.decode())
+
+ def test_patch(self):
+ s = FastHttpSession("http://127.0.0.1:%i" % self.port)
+ r = s.patch("/request_method")
+ self.assertEqual(200, r.status_code)
+ self.assertEqual("PATCH", r.content.decode())
+
+ def test_options(self):
+ s = FastHttpSession("http://127.0.0.1:%i" % self.port)
+ r = s.options("/request_method")
+ self.assertEqual(200, r.status_code)
+ self.assertEqual("", r.content.decode())
+ self.assertEqual(
+ set(["OPTIONS", "DELETE", "PUT", "GET", "POST", "HEAD", "PATCH"]),
+ set(r.headers["allow"].split(", ")),
+ )
+
+
+class TestRequestStatsWithWebserver(WebserverTestCase):
+ def test_request_stats_content_length(self):
+ class MyLocust(FastHttpLocust):
+ host = "http://127.0.0.1:%i" % self.port
+
+ locust = MyLocust()
+ locust.client.get("/ultra_fast")
+ self.assertEqual(global_stats.get("/ultra_fast", "GET").avg_content_length, len("This is an ultra fast response"))
+ locust.client.get("/ultra_fast")
+ self.assertEqual(global_stats.get("/ultra_fast", "GET").avg_content_length, len("This is an ultra fast response"))
+
+ def test_request_stats_no_content_length(self):
+ class MyLocust(FastHttpLocust):
+ host = "http://127.0.0.1:%i" % self.port
+ l = MyLocust()
+ path = "/no_content_length"
+ r = l.client.get(path)
+ self.assertEqual(global_stats.get(path, "GET").avg_content_length, len("This response does not have content-length in the header"))
+
+ def test_request_stats_no_content_length_streaming(self):
+ class MyLocust(FastHttpLocust):
+ host = "http://127.0.0.1:%i" % self.port
+ l = MyLocust()
+ path = "/no_content_length"
+ r = l.client.get(path, stream=True)
+ self.assertEqual(0, global_stats.get(path, "GET").avg_content_length)
+
+ def test_request_stats_named_endpoint(self):
+ class MyLocust(FastHttpLocust):
+ host = "http://127.0.0.1:%i" % self.port
+
+ locust = MyLocust()
+ locust.client.get("/ultra_fast", name="my_custom_name")
+ self.assertEqual(1, global_stats.get("my_custom_name", "GET").num_requests)
+
+ def test_request_stats_query_variables(self):
+ class MyLocust(FastHttpLocust):
+ host = "http://127.0.0.1:%i" % self.port
+
+ locust = MyLocust()
+ locust.client.get("/ultra_fast?query=1")
+ self.assertEqual(1, global_stats.get("/ultra_fast?query=1", "GET").num_requests)
+
+ def test_request_stats_put(self):
+ class MyLocust(FastHttpLocust):
+ host = "http://127.0.0.1:%i" % self.port
+
+ locust = MyLocust()
+ locust.client.put("/put")
+ self.assertEqual(1, global_stats.get("/put", "PUT").num_requests)
+
+ def test_request_connection_error(self):
+ class MyLocust(FastHttpLocust):
+ host = "http://localhost:1"
+
+ locust = MyLocust()
+ response = locust.client.get("/", timeout=0.1)
+ self.assertEqual(response.status_code, 0)
+ self.assertEqual(1, global_stats.get("/", "GET").num_failures)
+ self.assertEqual(0, global_stats.get("/", "GET").num_requests)
+
+
+class TestFastHttpLocustClass(WebserverTestCase):
+ def test_get_request(self):
+ self.response = ""
+ def t1(l):
+ self.response = l.client.get("/ultra_fast")
+ class MyLocust(FastHttpLocust):
+ tasks = [t1]
+ host = "http://127.0.0.1:%i" % self.port
+
+ my_locust = MyLocust()
+ t1(my_locust)
+ self.assertEqual(self.response.text, "This is an ultra fast response")
+
+ def test_client_request_headers(self):
+ class MyLocust(FastHttpLocust):
+ host = "http://127.0.0.1:%i" % self.port
+
+ locust = MyLocust()
+ self.assertEqual("hello", locust.client.get("/request_header_test", headers={"X-Header-Test":"hello"}).text)
+
+ def test_client_get(self):
+ class MyLocust(FastHttpLocust):
+ host = "http://127.0.0.1:%i" % self.port
+
+ locust = MyLocust()
+ self.assertEqual("GET", locust.client.get("/request_method").text)
+
+ def test_client_get_absolute_url(self):
+ class MyLocust(FastHttpLocust):
+ host = "http://127.0.0.1:%i" % self.port
+
+ locust = MyLocust()
+ self.assertEqual("GET", locust.client.get("http://127.0.0.1:%i/request_method" % self.port).text)
+
+ def test_client_post(self):
+ class MyLocust(FastHttpLocust):
+ host = "http://127.0.0.1:%i" % self.port
+
+ locust = MyLocust()
+ self.assertEqual("POST", locust.client.post("/request_method", {"arg":"hello world"}).text)
+ self.assertEqual("hello world", locust.client.post("/post", {"arg":"hello world"}).text)
+
+ def test_client_put(self):
+ class MyLocust(FastHttpLocust):
+ host = "http://127.0.0.1:%i" % self.port
+
+ locust = MyLocust()
+ self.assertEqual("PUT", locust.client.put("/request_method", {"arg":"hello world"}).text)
+ self.assertEqual("hello world", locust.client.put("/put", {"arg":"hello world"}).text)
+
+ def test_client_delete(self):
+ class MyLocust(FastHttpLocust):
+ host = "http://127.0.0.1:%i" % self.port
+
+ locust = MyLocust()
+ self.assertEqual("DELETE", locust.client.delete("/request_method").text)
+ self.assertEqual(200, locust.client.delete("/request_method").status_code)
+
+ def test_client_head(self):
+ class MyLocust(FastHttpLocust):
+ host = "http://127.0.0.1:%i" % self.port
+
+ locust = MyLocust()
+ self.assertEqual(200, locust.client.head("/request_method").status_code)
+
+ def test_log_request_name_argument(self):
+ from locust.stats import global_stats
+ self.response = ""
+
+ class MyLocust(FastHttpLocust):
+ tasks = []
+ host = "http://127.0.0.1:%i" % self.port
+
+ @task()
+ def t1(l):
+ self.response = l.client.get("/ultra_fast", name="new name!")
+
+ my_locust = MyLocust()
+ my_locust.t1()
+
+ self.assertEqual(1, global_stats.get("new name!", "GET").num_requests)
+ self.assertEqual(0, global_stats.get("/ultra_fast", "GET").num_requests)
+
+ def test_redirect_url_original_path_as_name(self):
+ class MyLocust(FastHttpLocust):
+ host = "http://127.0.0.1:%i" % self.port
+
+ l = MyLocust()
+ l.client.get("/redirect")
+
+ from locust.stats import global_stats
+ self.assertEqual(1, len(global_stats.entries))
+ self.assertEqual(1, global_stats.get("/redirect", "GET").num_requests)
+ self.assertEqual(0, global_stats.get("/ultra_fast", "GET").num_requests)
+
+ def test_client_basic_auth(self):
+ class MyLocust(FastHttpLocust):
+ host = "http://127.0.0.1:%i" % self.port
+
+ class MyAuthorizedLocust(FastHttpLocust):
+ host = "http://locust:menace@127.0.0.1:%i" % self.port
+
+ class MyUnauthorizedLocust(FastHttpLocust):
+ host = "http://locust:wrong@127.0.0.1:%i" % self.port
+
+ locust = MyLocust()
+ unauthorized = MyUnauthorizedLocust()
+ authorized = MyAuthorizedLocust()
+ response = authorized.client.get("/basic_auth")
+ self.assertEqual(200, response.status_code)
+ self.assertEqual("Authorized", response.text)
+ self.assertEqual(401, locust.client.get("/basic_auth").status_code)
+ self.assertEqual(401, unauthorized.client.get("/basic_auth").status_code)
+
+
+class TestFastHttpCatchResponse(WebserverTestCase):
+ def setUp(self):
+ super(TestFastHttpCatchResponse, self).setUp()
+
+ class MyLocust(FastHttpLocust):
+ host = "http://127.0.0.1:%i" % self.port
+
+ self.locust = MyLocust()
+
+ self.num_failures = 0
+ self.num_success = 0
+ def on_failure(request_type, name, response_time, exception):
+ self.num_failures += 1
+ self.last_failure_exception = exception
+ def on_success(**kwargs):
+ self.num_success += 1
+ events.request_failure += on_failure
+ events.request_success += on_success
+
+ def test_catch_response(self):
+ self.assertEqual(500, self.locust.client.get("/fail").status_code)
+ self.assertEqual(1, self.num_failures)
+ self.assertEqual(0, self.num_success)
+
+ with self.locust.client.get("/ultra_fast", catch_response=True) as response: pass
+ self.assertEqual(1, self.num_failures)
+ self.assertEqual(1, self.num_success)
+
+ with self.locust.client.get("/ultra_fast", catch_response=True) as response:
+ raise ResponseError("Not working")
+
+ self.assertEqual(2, self.num_failures)
+ self.assertEqual(1, self.num_success)
+
+ def test_catch_response_http_fail(self):
+ with self.locust.client.get("/fail", catch_response=True) as response: pass
+ self.assertEqual(1, self.num_failures)
+ self.assertEqual(0, self.num_success)
+
+ def test_catch_response_http_manual_fail(self):
+ with self.locust.client.get("/ultra_fast", catch_response=True) as response:
+ response.failure("Haha!")
+ self.assertEqual(1, self.num_failures)
+ self.assertEqual(0, self.num_success)
+ self.assertTrue(
+ isinstance(self.last_failure_exception, CatchResponseError),
+ "Failure event handler should have been passed a CatchResponseError instance"
+ )
+
+ def test_catch_response_http_manual_success(self):
+ with self.locust.client.get("/fail", catch_response=True) as response:
+ response.success()
+ self.assertEqual(0, self.num_failures)
+ self.assertEqual(1, self.num_success)
+
+ def test_catch_response_allow_404(self):
+ with self.locust.client.get("/does/not/exist", catch_response=True) as response:
+ self.assertEqual(404, response.status_code)
+ if response.status_code == 404:
+ response.success()
+ self.assertEqual(0, self.num_failures)
+ self.assertEqual(1, self.num_success)
+
+ def test_interrupt_taskset_with_catch_response(self):
+ class MyTaskSet(TaskSet):
+ @task
+ def interrupted_task(self):
+ with self.client.get("/ultra_fast", catch_response=True) as r:
+ raise InterruptTaskSet()
+ class MyLocust(FastHttpLocust):
+ host = "http://127.0.0.1:%i" % self.port
+ task_set = MyTaskSet
+
+ l = MyLocust()
+ ts = MyTaskSet(l)
+ self.assertRaises(InterruptTaskSet, lambda: ts.interrupted_task())
+ self.assertEqual(0, self.num_failures)
+ self.assertEqual(0, self.num_success)
+
+ def test_catch_response_connection_error_success(self):
+ class MyLocust(FastHttpLocust):
+ host = "http://127.0.0.1:1"
+ l = MyLocust()
+ with l.client.get("/", catch_response=True) as r:
+ self.assertEqual(r.status_code, 0)
+ self.assertEqual(None, r.content)
+ r.success()
+ self.assertEqual(1, self.num_success)
+ self.assertEqual(0, self.num_failures)
+
+ def test_catch_response_connection_error_fail(self):
+ class MyLocust(FastHttpLocust):
+ host = "http://127.0.0.1:1"
+ l = MyLocust()
+ with l.client.get("/", catch_response=True) as r:
+ self.assertEqual(r.status_code, 0)
+ self.assertEqual(None, r.content)
+ r.failure("Manual fail")
+ self.assertEqual(0, self.num_success)
+ self.assertEqual(1, self.num_failures)
diff --git a/locust/test/test_locust_class.py b/locust/test/test_locust_class.py
index 9b2bc64eb4..6613685890 100644
--- a/locust/test/test_locust_class.py
+++ b/locust/test/test_locust_class.py
@@ -555,6 +555,6 @@ class MyLocust(HttpLocust):
with l.client.get("/", catch_response=True) as r:
self.assertEqual(r.status_code, 0)
self.assertEqual(None, r.content)
- r.success()
- self.assertEqual(1, self.num_success)
- self.assertEqual(0, self.num_failures)
+ r.failure("Manual fail")
+ self.assertEqual(0, self.num_success)
+ self.assertEqual(1, self.num_failures)
diff --git a/locust/test/testcases.py b/locust/test/testcases.py
index 5e997d7419..c2671e9160 100644
--- a/locust/test/testcases.py
+++ b/locust/test/testcases.py
@@ -41,7 +41,7 @@ def consistent():
gevent.sleep(0.2)
return "This is a consistent response"
-@app.route("/request_method", methods=["POST", "GET", "HEAD", "PUT", "DELETE"])
+@app.route("/request_method", methods=["POST", "GET", "HEAD", "PUT", "DELETE", "PATCH"])
def request_method():
return request.method
diff --git a/setup.py b/setup.py
index e286d07563..458aaba6c4 100644
--- a/setup.py
+++ b/setup.py
@@ -42,7 +42,15 @@
packages=find_packages(exclude=['examples', 'tests']),
include_package_data=True,
zip_safe=False,
- install_requires=["gevent>=1.2.2", "flask>=0.10.1", "requests>=2.9.1", "msgpack>=0.4.2", "six>=1.10.0", "pyzmq>=16.0.2"],
+ install_requires=[
+ "gevent>=1.2.2",
+ "flask>=0.10.1",
+ "requests>=2.9.1",
+ "msgpack-python>=0.4.2",
+ "six>=1.10.0",
+ "pyzmq>=16.0.2",
+ "geventhttpclient-wheels",
+ ],
test_suite="locust.test",
tests_require=['mock'],
entry_points={