diff --git a/docs/api.rst b/docs/api.rst index 7df962fc4c..1eb8a470a5 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -8,13 +8,17 @@ Locust class .. autoclass:: locust.core.Locust :members: tasks, min_wait, max_wait, schedule_task, client + + .. autoattribute:: locust.core.LocustBase.min_wait + .. autoattribute:: locust.core.LocustBase.max_wait + .. autoattribute:: locust.core.LocustBase.tasks -HttpBrowser class +HttpSession class ================= -.. autoclass:: locust.clients.HttpBrowser - :members: __init__, get, post +.. autoclass:: locust.clients.HttpSession + :members: __init__, request, get, post, delete, put, head, options, patch HttpResponse class ================== diff --git a/docs/changelog.rst b/docs/changelog.rst index ab69087796..150cc5dd2e 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,21 +1,105 @@ -========== +########## Changelog -========== +########## 0.6 === -Improvements and bug fixes --------------------------- - -* Scheduled task callabled can now take keyword arguments. +.. warning:: + + This version comes with non backward compatible changes to the API. + Anyone who is currently using existing locust scripts and want to upgrade to 0.6 + is adviced to read through these changes. It's nothing major, and the upgrade + should be possible without too much pain. + + +Locust now uses Requests +------------------------ + +Locust's own HttpBrowser class (which was typically accessed through *self.client* from within a locust class) +has been replaced by a thin wrapper around the requests library (http://python-requests.org). This comes with +a number of advantages. Users can now take advantage of a well documented, well written, fully fledged +library for making HTTP requests. However, it also comes with some small API changes wich will require users +to update their existing load testing scripts. + +Gzip encoding turned on by default +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The HTTP client now sends headers for accepting gzip encoding by default. The **--gzip** command line argument +has been removed and if someone want to disable the *Accept-Encoding* that the HTTP client uses, or any +other HTTP headers you can do:: + + class MyWebUser(Locust): + def on_start(self): + self.client.headers = {"Accept-Encoding":""} + + +Improved HTTP client +^^^^^^^^^^^^^^^^^^^^ + +Because of the switch to using python-requests in the HTTP client, the API for the client has also +gotten a few changes. + +* Additionally to the :py:meth:`get `, :py:meth:`post `, + :py:meth:`put `, :py:meth:`delete ` and + :py:meth:`head ` methods, the :py:class:`HttpSession ` class + now also has :py:meth:`patch ` and :py:meth:`options ` methods. + +* All arguments to the HTTP request methods, except for **url** and **data** should now be specified as keyword arguments. + For example, previously one could specify headers using:: + + client.get("/path", {"User-Agent":"locust"}) # this will no longer work + + And should now be specified like:: + + client.get("/path", headers={"User-Agent":"locust"}) + +* In general the whole HTTP client is now more powerful since it leverages on python-requests. Features that we're + now able to use in Locust includes file upload, SSL, connection keep-alive, and more. + See the `python-requests documentation `_ for more details. + +* The new :py:class:`HttpSession ` class' methods now return python-request + :py:class:`Response ` objects. This means that accessing the content of the response + is no longer made using the **data** attribute, but instead the **content** attribute. The HTTP response + code is now accessed through the **status_code** attribute, instead of the **code** attribute. + + +HttpSession methods' catch_response argument improved and allow_http_error argument removed +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +* When doing HTTP requests using the **catch_response** argument, the method now returns a CatchedResponse + object which is a context manager that can be used to manually deciding if the request should be reported + as successful or as a failure, in the Locust statistics. + + .. autoclass:: locust.clients.CatchedResponse + :members: response, request, success, failure + :noindex: + +* The **allow_http_error** argument of the HTTP client's methods has been removed. Instead one can use the + **catch_response** argument to get a context manager, which can be used together with a with statement. + + The following code in the previous Locust version:: + + client.get("/does/not/exist", allow_http_error=True) + + Can instead now be written like: + + with client.get("does/not/exist", catch_response=True) as catched: + catched.success() + + +Other improvements and bug fixes +-------------------------------- + +* Scheduled task callables can now take keyword arguments. * SubLocust classes that are scheduled using :func:`locust.core.Locust.schedule_task` can now take arguments and keyword arguments (available in *self.args* and *self.kwargs*). + API Changes ----------- +* HttpBrowser has been removed in favor of python requests library. * Changed signature of :func:`locust.core.Locust.schedule_task`. Previously all extra arguments that was given to the method was passed on to the the task when it was called. It no longer accepts extra arguments. Instead, it takes an *args* argument (list) and a *kwargs* argument (dict) which are be passed to the task when diff --git a/docs/conf.py b/docs/conf.py index dab5786141..e69daf0b2a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -14,7 +14,7 @@ # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ["sphinx.ext.autodoc"] +extensions = ["sphinx.ext.autodoc", "sphinx.ext.intersphinx"] # autoclass options #autoclass_content = "both" @@ -32,6 +32,11 @@ project = 'Locust' #copyright = '' +# Intersphinx config +intersphinx_mapping = { + 'requests': ('http://requests.readthedocs.org/en/latest/', None), +} + # The default replacements for |version| and |release|, also used in various # other places throughout the built documents. # diff --git a/docs/writing-a-locustfile.rst b/docs/writing-a-locustfile.rst index 0f67a4d635..3361dcca3d 100644 --- a/docs/writing-a-locustfile.rst +++ b/docs/writing-a-locustfile.rst @@ -132,32 +132,27 @@ and **/** will be requested twice the amount of times than **/about/**. Using the HTTP client ====================== -Each instance of Locust has an instance of HttpBrowser in the *client* attribute. +Each instance of Locust has an instance of HttpSession in the *client* attribute. -.. autoclass:: locust.clients.HttpBrowser - :members: __init__, get, post +.. autoclass:: locust.clients.HttpSession + :members: __init__, get, post, request :noindex: By default, requests are marked as failed requests unless the HTTP response code is ok (2xx). However, one can mark requests as failed, even when the response code is okay, by using the *catch_response* argument with a with statement:: - from locust import ResponseError - - client = HttpBrowser("http://example.com") - with client.get("/", catch_response=True) as response: - if response.data != "Success": - raise ResponseError("Got wrong response") - -Just as one can mark requests with OK response codes as failures, one can also make requests that -results in an HTTP error code still result in a success in the statistics:: - - client = HttpBrowser("http://example.com") - response = client.get("/does_not_exist/", allow_http_error=True) - if response.exception: - print "We got an HTTPError exception, but the request will still be marked as OK" - -Also, *catch_response* and *allow_http_error* can be used together. + with client.get("/", catch_response=True) as catched: + if catched.response.content != "Success": + catched.failure("Got wrong response") + +Just as one can mark requests with OK response codes as failures, one can also use **catch_response** +argument together with a *with* statement to make requests that resulted in an HTTP error code still +be reported as a success in the statistics:: + + with client.get("/does_not_exist/", catch_response=True) as catched: + if catched.response.status_code == 404: + catched.success() The on_start function diff --git a/examples/basic.py b/examples/basic.py index 8adad2cc76..489753a1f0 100644 --- a/examples/basic.py +++ b/examples/basic.py @@ -1,4 +1,4 @@ -from locust import Locust, require_once +from locust import Locust, require_once, task import random def login(l): @@ -20,3 +20,7 @@ class WebsiteUser(Locust): tasks = [index, stats] min_wait=2000 max_wait=5000 + + @task + def page404(self): + self.client.get("/does_not_exist") diff --git a/locust/clients.py b/locust/clients.py index 93a4a38f83..1247f0fd1f 100644 --- a/locust/clients.py +++ b/locust/clients.py @@ -1,292 +1,199 @@ -import urllib2 -import urllib +import re import time -import base64 +from collections import namedtuple from urlparse import urlparse, urlunparse -from exception import ResponseError -from urllib2 import HTTPError, URLError -from httplib import BadStatusLine -import socket -from StringIO import StringIO -import gzip +import requests +from requests.auth import HTTPBasicAuth import events -from locust.exception import LocustError +from exception import CatchResponseError, ResponseError -class NoneContext(object): - def __enter__(self): - return None +absolute_http_url_regexp = re.compile(r"^https?://", re.I) - def __exit__(self, exc, value, traceback): - return True -def log_request(f): - def _wrapper(*args, **kwargs): - request_method = args[1] - name = kwargs.get('name', args[2]) or args[2] - if "catch_response" in kwargs: - catch_response = kwargs["catch_response"] - del kwargs["catch_response"] - else: - catch_response = False - if "allow_http_error" in kwargs: - allow_http_error = kwargs["allow_http_error"] - del kwargs["allow_http_error"] +class HttpSession(requests.Session): + """ + Class for performing web requests and holding (session-) cookies between requests (in order + to be able to log in and out of websites). Each request is logged so that locust can display + statistics. + + This is a slightly extended version of `python-request `_'s + :py:class:`requests.Session` class and mostly this class works exactly the same. However + the methods for making requests (get, post, delete, put, head, options, patch, request) + can now take a *url* argument that's only the path part of the URL, in which case the host + part of the URL will be prepended with the HttpSession.base_url which is normally inherited + from a Locust class' host property. + + Each of the methods for making requests also takes two additional optional arguments which + are Locust specific and doesn't exist in python-requests. These are: + + :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). + """ + def __init__(self, *args, **kwargs): + self.base_url = kwargs.pop("base_url") + + # 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)) + # configure requests to use basic auth + kwargs["auth"] = HTTPBasicAuth(parsed_url.username, parsed_url.password) + + super(HttpSession, self).__init__(self, *args, **kwargs) + + 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: - allow_http_error = False - - try: - start = time.time() - try: - retval = f(*args, **kwargs) - except HTTPError, ex: - if allow_http_error: - retval = ex.locust_http_response - retval.exception = ex + return "%s%s" % (self.base_url, path) + + def request(self, method, url, name=None, catch_response=False, **kwargs): + """ + Constructs and sends a :py:class:`requests.Request`. + Returns :py:class:`requests.Response` object. + + :param method: method for the new :class:`Request` object. + :param url: URL for the new :class:`Request` object. + :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 params: (optional) Dictionary or bytes to be sent in the query string for the :class:`Request`. + :param data: (optional) Dictionary or bytes to send in the body of the :class:`Request`. + :param headers: (optional) Dictionary of HTTP Headers to send with the :class:`Request`. + :param cookies: (optional) Dict or CookieJar object to send with the :class:`Request`. + :param files: (optional) Dictionary of 'filename': file-like-objects for multipart encoding upload. + :param auth: (optional) Auth tuple or callable to enable Basic/Digest/Custom HTTP Auth. + :param timeout: (optional) Float describing the timeout of the request. + :param allow_redirects: (optional) Boolean. Set to True by default. + :param proxies: (optional) Dictionary mapping protocol to the URL of the proxy. + :param return_response: (optional) If False, an un-sent Request object will returned. + :param config: (optional) A configuration dictionary. See ``request.defaults`` for allowed keys and their default values. + :param prefetch: (optional) whether to immediately download the response content. Defaults to ``True``. + :param verify: (optional) if ``True``, the SSL cert will be verified. A CA_BUNDLE path can also be provided. + :param cert: (optional) if String, path to ssl client cert file (.pem). If Tuple, ('cert', 'key') pair. + """ + + """ + Send the actual request using requests.Session's request() method but first + we install event hooks that will report the request as a success or a failure, + to locust's statistics + """ + # prepend url with hostname unless it's already an absolute URL + url = self._build_url(url) + + # set up pre and post request hooks for recording the requests in the statistics + def on_pre_request(request): + request.locust_start_time = time.time() + + def on_response(response): + request = response.request + request.locust_response_time = int((time.time() - request.locust_start_time) * 1000) + + if not catch_response: + try: + response.raise_for_status() + except requests.exceptions.RequestException, e: + events.request_failure.fire(request.method, name or request.path_url, request.locust_response_time, e, response) else: - raise ex - retval.catch_response = catch_response - retval.allow_http_error = allow_http_error - response_time = int((time.time() - start) * 1000) - if catch_response: - retval._trigger_success = lambda : events.request_success.fire(request_method, name, response_time, retval) - retval._trigger_failure = lambda e : events.request_failure.fire(request_method, name, response_time, e, None) - else: - events.request_success.fire(request_method, name, response_time, retval) - return retval - except Exception, e: - response_time = int((time.time() - start) * 1000) - response = None - - if isinstance(e, HTTPError): - e.msg += " (" + request_method + " " + name + ")" - response = e.locust_http_response - elif isinstance(e, URLError) or isinstance(e, BadStatusLine): - e.args = tuple(list(e.args) + [request_method, name]) - elif isinstance(e, socket.error): - pass - else: - raise - - events.request_failure.fire(request_method, name, response_time, e, response) - + events.request_success.fire(request.method, name or request.path_url, request.locust_response_time, response) + + kwargs["hooks"] = {"pre_request":on_pre_request, "response":on_response} + response = super(HttpSession, self).request(method, url, **kwargs) if catch_response: - return NoneContext() - return None - - return _wrapper - -class HttpBasicAuthHandler(urllib2.BaseHandler): - def __init__(self, username, password): - self.username = username - self.password = password - - def http_request(self, request): - base64string = base64.encodestring('%s:%s' % (self.username, self.password)).replace('\n', '') - request.add_header("Authorization", "Basic %s" % base64string) - return request + return CatchedResponse(response, name=name) + else: + return response - #Do the same thing for https requests - https_request = http_request -class HttpResponse(object): +class CatchedResponse(object): """ - An instance of HttpResponse is returned by HttpBrowser's get and post functions. - It contains response data for the request that was made. + Context manager that allows for manually controlling if an HTTP request should be marked + as successful or a failure. """ - - url = None - """URL that was requested""" - - code = None - """HTTP response code""" - - data = None - """Response data""" - - catch_response = False - allow_http_error = False - _trigger_success = None - _trigger_failure = None - - def __init__(self, method, url, name, code, data, info, gzip): - self.method = method - self.url = url - self._name = name - self.code = code - self.data = data - self._info = info - self._gzip = gzip - self._decoded = False - - @property - def info(self): - """ - urllib2 info object containing info about the response - """ - return self._info() - - def _get_data(self): - if self._gzip and not self._decoded and self._info().get("Content-Encoding") == "gzip": - self._data = gzip.GzipFile(fileobj=StringIO(self._data)).read() - self._decoded = True - return self._data - - def _set_data(self, data): - self._data = data - + + response = None + """ The :py:class:`Response ` object that was returned""" + + request = None + """ The original :py:class:`Request ` object that was constructed for making the HTTP request""" + + def __init__(self, response, name=None): + self.response = response + self.request = response.request + self.name = name + self._is_reported = False + def __enter__(self): - if not self.catch_response: - raise LocustError("If using response in a with() statement you must use catch_response=True") 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._trigger_failure(value) + self.failure(value) else: - return + return False else: - self._trigger_success() + try: + self.response.raise_for_status() + except requests.exceptions.RequestException, e: + self.failure(e) + else: + self.success() return True - - data = property(_get_data, _set_data) - -class HttpBrowser(object): - """ - Class for performing web requests and holding session cookie between requests (in order - to be able to log in to websites). - - Logs each request so that locust can display statistics. - """ - - def __init__(self, base_url, gzip=False): - self.base_url = base_url - self.gzip = gzip - self.new_session() - def new_session(self): + def success(self): """ - Get a new HTTP session for this HttpBrowser instance + Report the current response as successful """ - handlers = [urllib2.HTTPCookieProcessor()] - - # 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)) - - auth_handler = HttpBasicAuthHandler(parsed_url.username, parsed_url.password) - handlers.append(auth_handler) - - self.opener = urllib2.build_opener(*handlers) - urllib2.install_opener(self.opener) - - def get(self, path, headers={}, name=None, **kwargs): + events.request_success.fire( + self.request.method, + self.name or self.request.path_url, + self.request.locust_response_time, + self.response, + ) + self._is_reported = True + + def failure(self, exc): """ - Make an HTTP GET request. - - Arguments: - - * *path* is the relative path to request. - * *headers* is an optional dict with HTTP request headers - * *name* is an optional argument that can be specified to use as label in the statistics instead of the path - * *catch_response* is an optional boolean argument that, if set, can be used to make a request with a with statement. - This will allows the request to be marked as a fail based on the content of the response, even if the - response code is ok (2xx). - * *allow_http_error* os an optional boolean argument, that, if set, can be used to not mark responses with - HTTP errors as failures. If an HTTPError occurs, it will be available in the *exception* attribute of the - response. + Report the current response as a failure. - Returns an HttpResponse instance, or None if the request failed. + exc can be either a python exception, or a string in which case it will + be wrapped inside a CatchResponseError. Example:: - client = HttpBrowser("http://example.com") - response = client.get("/") - - Example using the with statement:: - - from locust import ResponseError - - with self.client.get("/inbox", catch_response=True) as response: - if response.data == "fail": - raise ResponseError("Request failed") + with self.client.get("/", catch_response=True) as catched: + if catched.response.content == "": + catched.failure("No data") """ - return self._request('GET', path, None, headers=headers, name=name, **kwargs) - - def post(self, path, data, headers={}, name=None, **kwargs): - """ - Make an HTTP POST request. - - Arguments: - - * *path* is the relative path to request. - * *data* dict with the data that will be sent in the body of the POST request - * *headers* is an optional dict with HTTP request headers - * *name* is an optional argument that can be specified to use as label in the statistics instead of the path - * *catch_response* is an optional boolean argument that, if set, can be used to make a request with a with statement. - This will allows the request to be marked as a fail based on the content of the response, even if the - response code is ok (2xx). - * *allow_http_error* os an optional boolean argument, that, if set, can be used to not mark responses with - HTTP errors as failures. If an HTTPError occurs, it will be available in the *exception* attribute of the - response. - - Returns an HttpResponse instance, or None if the request failed. - - Example:: - - client = HttpBrowser("http://example.com") - response = client.post("/post", {"user":"joe_hill"}) + if isinstance(exc, basestring): + exc = CatchResponseError(exc) - Example using the with statement:: - - from locust import ResponseError - - with self.client.post("/inbox", {"user":"ada", content="Hello!"}, catch_response=True) as response: - if response.data == "fail": - raise ResponseError("Posting of inbox message failed") - """ - return self._request('POST', path, data, headers=headers, name=name, **kwargs) - - def put(self, path, data, headers={}, name=None, **kwargs): - return self._request('PUT', path, data, headers=headers, name=name, **kwargs) - - def delete(self, path, headers={}, name=None, **kwargs): - return self._request('DELETE', path, None, headers=headers, name=name, **kwargs) - - def head(self, path, headers={}, name=None, **kwargs): - return self._request('HEAD', path, None, headers=headers, name=name, **kwargs) - - @log_request - def _request(self, method, path, data=None, headers={}, name=None): - if self.gzip: - headers["Accept-Encoding"] = "gzip" - - if data is not None: - try: - data = urllib.urlencode(data) - except TypeError: - pass # ignore if someone sends in an already prepared string - - url = self.base_url + path - request = urllib2.Request(url, data, headers) - request.get_method = lambda: method - try: - f = self.opener.open(request) - data = f.read() - f.close() - except HTTPError, e: - data = e.read() - e.locust_http_response = HttpResponse(method, url, name, e.code, data, e.info, self.gzip) - e.close() - raise e - - return HttpResponse(method, url, name, f.code, data, f.info, self.gzip) + events.request_failure.fire( + self.request.method, + self.name or self.request.path_url, + self.request.locust_response_time, + exc, + self.response, + ) + self._is_reported = True diff --git a/locust/core.py b/locust/core.py index 8100ce0f00..cfcd28d096 100644 --- a/locust/core.py +++ b/locust/core.py @@ -10,7 +10,7 @@ import traceback import logging -from clients import HttpBrowser +from clients import HttpSession import events from exception import LocustError, InterruptLocust, RescheduleTaskImmediately @@ -117,9 +117,10 @@ class LocustBase(object): If tasks is a list, the task to be performed will be picked randomly. - If tasks is a *(callable,int)* list of two-tuples, 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 *task1* will be three times more - likely to be picked than *task2*:: + 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 *task1* will + be three times more likely to be picked than *task2*:: class User(Locust): tasks = [(task1, 3), (task2, 1)] @@ -129,7 +130,7 @@ class User(Locust): """Base hostname to swarm. i.e: http://127.0.0.1:1234""" min_wait = 1000 - """Minimum waiting time between two execution of locust tasks""" + """Minimum waiting time between the execution of locust tasks""" max_wait = 1000 """Maximum waiting time between the execution of locust tasks""" @@ -246,22 +247,16 @@ class Locust(LocustBase): client = None """ - Instance of HttpBrowser that is created upon instantiation of Locust. + Instance of HttpSession that is created upon instantiation of Locust. The client support cookies, and therefore keeps the session between HTTP requests. """ - gzip = False - """ - If set to True the HTTP client will set headers for accepting gzip, and decode gzip data - that is sent back from the server. This attribute is set from the command line. - """ - def __init__(self): super(Locust, 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.") - - self.client = HttpBrowser(self.host, self.gzip) + + self.client = HttpSession(base_url=self.host) class WebLocust(Locust): def __init__(self, *args, **kwargs): diff --git a/locust/exception.py b/locust/exception.py index 409709d667..853542b35b 100644 --- a/locust/exception.py +++ b/locust/exception.py @@ -4,6 +4,9 @@ class LocustError(Exception): class ResponseError(LocustError): pass +class CatchResponseError(LocustError): + pass + class InterruptLocust(Exception): """ Exception that will interrupt a Locust when thrown inside a task diff --git a/locust/main.py b/locust/main.py index 22d412f5c7..c0a36d9122 100644 --- a/locust/main.py +++ b/locust/main.py @@ -140,15 +140,6 @@ def parse_options(): help="Enables the auto tuning ramping feature for finding highest stable client count. NOTE having ramp enabled will add some more overhead for additional stats gathering" ) - # if the HTTP client should set gzip headers and try to use gzip decoding - parser.add_option( - '--gzip', - action='store_true', - dest='gzip', - default=False, - help="If present, the HTTP client will set request header Accept-Encoding: gzip, and try to gzip decode the response data." - ) - # if we shgould print stats in the console parser.add_option( '--print-stats', @@ -362,9 +353,6 @@ def main(): logger.info("Starting web monitor on port 8089") main_greenlet = gevent.spawn(web.start, locust_classes, options.hatch_rate, options.num_clients, options.num_requests, options.ramp) - # enable/disable gzip in WebLocust's HTTP client - WebLocust.gzip = options.gzip - if not options.master and not options.slave: runners.locust_runner = LocalLocustRunner(locust_classes, options.hatch_rate, options.num_clients, options.num_requests, options.host) # spawn client spawning/hatching greenlet diff --git a/locust/stats.py b/locust/stats.py index 171df56108..4ce9bf7565 100644 --- a/locust/stats.py +++ b/locust/stats.py @@ -269,7 +269,7 @@ def on_request_success(method, name, response_time, response): if RequestStats.global_max_requests is not None and RequestStats.total_num_requests >= RequestStats.global_max_requests: raise InterruptLocust("Maximum number of requests reached") - content_length = int(response.info.getheader("Content-Length") or 0) + content_length = int(response.headers.get("content-length") or 0) RequestStats.get(method, name).log(response_time, content_length) def on_request_failure(method, name, response_time, error, response=None): diff --git a/locust/test/locust_class.py b/locust/test/locust_class.py index 5178c33231..49ad8dfa03 100644 --- a/locust/test/locust_class.py +++ b/locust/test/locust_class.py @@ -1,7 +1,9 @@ +import unittest + from locust.core import Locust, SubLocust, require_once, task, events, RescheduleTaskImmediately -from locust.clients import HttpBrowser from locust import ResponseError, InterruptLocust -import unittest +from locust.exception import CatchResponseError + from testcases import WebserverTestCase class TestLocustClass(unittest.TestCase): @@ -222,53 +224,59 @@ class MyLocust(Locust): my_locust = MyLocust() t1(my_locust) - self.assertEqual(self.response.data, "This is an ultra fast response") + self.assertEqual(self.response.content, "This is an ultra fast response") def test_client_request_headers(self): class MyLocust(Locust): host = "http://127.0.0.1:%i" % self.port locust = MyLocust() - self.assertEqual("hello", locust.client.get("/request_header_test", {"X-Header-Test":"hello"}).data) + self.assertEqual("hello", locust.client.get("/request_header_test", headers={"X-Header-Test":"hello"}).content) def test_client_get(self): class MyLocust(Locust): host = "http://127.0.0.1:%i" % self.port locust = MyLocust() - self.assertEqual("GET", locust.client.get("/request_method").data) + self.assertEqual("GET", locust.client.get("/request_method").content) + + def test_client_get_absolute_url(self): + class MyLocust(Locust): + 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).content) def test_client_post(self): class MyLocust(Locust): host = "http://127.0.0.1:%i" % self.port locust = MyLocust() - self.assertEqual("POST", locust.client.post("/request_method", {"arg":"hello world"}).data) - self.assertEqual("hello world", locust.client.post("/post", {"arg":"hello world"}).data) + self.assertEqual("POST", locust.client.post("/request_method", {"arg":"hello world"}).content) + self.assertEqual("hello world", locust.client.post("/post", {"arg":"hello world"}).content) def test_client_put(self): class MyLocust(Locust): host = "http://127.0.0.1:%i" % self.port locust = MyLocust() - self.assertEqual("PUT", locust.client.put("/request_method", {"arg":"hello world"}).data) - self.assertEqual("hello world", locust.client.put("/put", {"arg":"hello world"}).data) + self.assertEqual("PUT", locust.client.put("/request_method", {"arg":"hello world"}).content) + self.assertEqual("hello world", locust.client.put("/put", {"arg":"hello world"}).content) def test_client_delete(self): class MyLocust(Locust): host = "http://127.0.0.1:%i" % self.port locust = MyLocust() - self.assertEqual("DELETE", locust.client.delete("/request_method").method) - self.assertEqual(200, locust.client.delete("/request_method").code) + self.assertEqual("DELETE", locust.client.delete("/request_method").content) + self.assertEqual(200, locust.client.delete("/request_method").status_code) def test_client_head(self): class MyLocust(Locust): host = "http://127.0.0.1:%i" % self.port locust = MyLocust() - self.assertEqual("HEAD", locust.client.head("/request_method").method) - self.assertEqual(200, locust.client.head("/request_method").code) + self.assertEqual(200, locust.client.head("/request_method").status_code) def test_client_basic_auth(self): class MyLocust(Locust): @@ -283,7 +291,7 @@ class MyUnauthorizedLocust(Locust): locust = MyLocust() unauthorized = MyUnauthorizedLocust() authorized = MyAuthorizedLocust() - self.assertEqual("Authorized", authorized.client.get("/basic_auth").data) + self.assertEqual("Authorized", authorized.client.get("/basic_auth").content) self.assertFalse(locust.client.get("/basic_auth")) self.assertFalse(unauthorized.client.get("/basic_auth")) @@ -304,51 +312,59 @@ def t1(l): self.assertEqual(1, RequestStats.get("GET", "new name!").num_reqs) self.assertEqual(0, RequestStats.get("GET", "/ultra_fast").num_reqs) - - def test_catch_response(self): - class MyLocust(Locust): - host = "http://127.0.0.1:%i" % self.port - locust = MyLocust() - - num = {'failures': 0, 'success': 0} - def on_failure(method, path, response_time, exception, response): num['failures'] += 1 - def on_success(a, b, c, d): num['success'] += 1 - - events.request_failure += on_failure - events.request_success += on_success - self.assertEqual(None, locust.client.get("/fail")) - self.assertEqual(1, num['failures']) - self.assertEqual(0, num['success']) - - with locust.client.get("/ultra_fast", catch_response=True) as response: pass - self.assertEqual(1, num['failures']) - self.assertEqual(1, num['success']) - - with locust.client.get("/ultra_fast", catch_response=True) as response: - raise ResponseError("Not working") - - self.assertEqual(2, num['failures']) - self.assertEqual(1, num['success']) - - def test_allow_http_error(self): +class TestCatchResponse(WebserverTestCase): + def setUp(self): + super(TestCatchResponse, self).setUp() + class MyLocust(Locust): host = "http://127.0.0.1:%i" % self.port - l = MyLocust() + + self.locust = MyLocust() - num = {'failures': 0, 'success': 0} - def on_failure(method, path, response_time, exception, response): num['failures'] += 1 - def on_success(a, b, c, d): num['success'] += 1 + self.num_failures = 0 + self.num_success = 0 + def on_failure(method, path, response_time, exception, response): + self.num_failures += 1 + self.last_failure_exception = exception + def on_success(a, b, c, d): + self.num_success += 1 events.request_failure += on_failure events.request_success += on_success - l.client.get("/fail", allow_http_error=True) - self.assertEqual(num["failures"], 0) + 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 l.client.get("/fail", allow_http_error=True, catch_response=True) as r: + 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(num["failures"], 1) + + 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 catched: 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 catched: + catched.failure("Haha!") + self.assertEqual(1, self.num_failures) + self.assertEqual(0, self.num_success) + self.assertIsInstance(self.last_failure_exception, CatchResponseError) + + def test_catch_response_http_manual_success(self): + with self.locust.client.get("/fail", catch_response=True) as catched: + catched.success() + self.assertEqual(0, self.num_failures) + self.assertEqual(1, self.num_success) def test_interrupt_locust_with_catch_response(self): class MyLocust(Locust): @@ -358,12 +374,7 @@ def interrupted_task(self): with self.client.get("/ultra_fast", catch_response=True) as r: raise InterruptLocust() - num = {'failures': 0, 'success': 0} - def on_failure(method, path, response_time, exception, response): num['failures'] += 1 - def on_success(a, b, c, d): num['success'] += 1 - events.request_failure += on_failure - events.request_success += on_success - l = MyLocust() self.assertRaises(InterruptLocust, lambda: l.interrupted_task()) - self.assertEqual(num["failures"], 0) + self.assertEqual(0, self.num_failures) + self.assertEqual(0, self.num_success) diff --git a/locust/test/runtests.py b/locust/test/runtests.py index 8cfe0a9929..bf2792e0d6 100644 --- a/locust/test/runtests.py +++ b/locust/test/runtests.py @@ -1,5 +1,5 @@ import unittest -from locust_class import TestLocustClass, TestSubLocust, TestWebLocustClass +from locust_class import TestLocustClass, TestSubLocust, TestWebLocustClass, TestCatchResponse from test_stats import TestRequestStats, TestRequestStatsWithWebserver, TestInspectLocust if __name__ == '__main__': diff --git a/requirements.txt b/requirements.txt index 63d4f50657..22cffdd4e5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ greenlet gevent>=0.13 Flask>=0.8 +requests==0.14