From 96a3b72e96df0d40b10851a4649d5ad18b3fc64f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dennis=20Krone=CC=81?= Date: Mon, 19 Apr 2021 12:54:58 +0200 Subject: [PATCH 01/21] Rename test_client to test_http. Will also add kwargs handling to request events. --- locust/test/test_fasthttp.py | 2 +- locust/test/{test_client.py => test_http.py} | 26 +++++++++++++++++++- locust/test/test_locust_class.py | 2 +- 3 files changed, 27 insertions(+), 3 deletions(-) rename locust/test/{test_client.py => test_http.py} (90%) diff --git a/locust/test/test_fasthttp.py b/locust/test/test_fasthttp.py index b521dc04f5..9a8c4926fc 100644 --- a/locust/test/test_fasthttp.py +++ b/locust/test/test_fasthttp.py @@ -423,7 +423,7 @@ class MyUser(FastHttpUser): self.num_failures = 0 self.num_success = 0 - def on_failure(request_type, name, response_time, response_length, exception): + def on_failure(request_type, name, response_time, response_length, exception, **kwargs): self.num_failures += 1 self.last_failure_exception = exception diff --git a/locust/test/test_client.py b/locust/test/test_http.py similarity index 90% rename from locust/test/test_client.py rename to locust/test/test_http.py index 46d00d7a2e..1ad90b4fb6 100644 --- a/locust/test/test_client.py +++ b/locust/test/test_http.py @@ -107,6 +107,29 @@ def test_options(self): set(r.headers["allow"].split(", ")), ) + def test_error_message(self): + s = self.get_client() + kwargs = {} + + def on_error(**kw): + kwargs.update(kw) + + self.environment.events.request_failure.add_listener(on_error) + s.request("get", "/wrong_url", context={"foo": "bar"}) + self.assertIn("/wrong_url", str(kwargs["exception"])) + self.assertDictEqual({"foo": "bar"}, kwargs["context"]) + + def test_context_in_success(self): + s = self.get_client() + kwargs = {} + + def on_success(**kw): + kwargs.update(kw) + + self.environment.events.request_success.add_listener(on_success) + s.request("get", "/request_method", context={"foo": "bar"}) + self.assertDictEqual({"foo": "bar"}, kwargs["context"]) + def test_error_message_with_name_replacement(self): s = self.get_client() kwargs = {} @@ -115,8 +138,9 @@ def on_error(**kw): kwargs.update(kw) self.environment.events.request_failure.add_listener(on_error) - s.request("get", "/wrong_url/01", name="replaced_url_name") + s.request("get", "/wrong_url/01", name="replaced_url_name", context={"foo": "bar"}) self.assertIn("for url: replaced_url_name", str(kwargs["exception"])) + self.assertDictEqual({"foo": "bar"}, kwargs["context"]) def test_get_with_params(self): s = self.get_client() diff --git a/locust/test/test_locust_class.py b/locust/test/test_locust_class.py index 27e2e9dbd3..1dd699ce70 100644 --- a/locust/test/test_locust_class.py +++ b/locust/test/test_locust_class.py @@ -724,7 +724,7 @@ class MyUser(HttpUser): self.num_failures = 0 self.num_success = 0 - def on_failure(request_type, name, response_time, response_length, exception): + def on_failure(request_type, name, response_time, response_length, exception, **kwargs): self.num_failures += 1 self.last_failure_exception = exception From 15c2689dc8d4587fcc6c1c7e1ed0c2e727498280 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dennis=20Krone=CC=81?= Date: Wed, 21 Apr 2021 16:23:42 +0200 Subject: [PATCH 02/21] Add the 'request' event which is triggered on both successful and failed requests. The old request_success and request_failed still exists but should probably be removed at some point. --- locust/clients.py | 66 +++++++++++++++++--------------------- locust/contrib/fasthttp.py | 56 +++++++++++++++----------------- locust/event.py | 14 ++++++++ locust/runners.py | 18 +++++++---- locust/user/users.py | 3 +- 5 files changed, 80 insertions(+), 77 deletions(-) diff --git a/locust/clients.py b/locust/clients.py index d1e62a1430..e90f22687a 100644 --- a/locust/clients.py +++ b/locust/clients.py @@ -44,12 +44,11 @@ class HttpSession(requests.Session): and then mark it as successful even if the response code was not (i.e 500 or 404). """ - def __init__(self, base_url, request_success, request_failure, *args, **kwargs): + def __init__(self, base_url, request_event, *args, **kwargs): super().__init__(*args, **kwargs) self.base_url = base_url - self.request_success = request_success - self.request_failure = request_failure + self.request_event = request_event # Check for basic authentication parsed_url = urlparse(self.base_url) @@ -72,7 +71,7 @@ def _build_url(self, path): else: return "%s%s" % (self.base_url, path) - def request(self, method, url, name=None, catch_response=False, **kwargs): + def request(self, method, url, name=None, catch_response=False, context={}, **kwargs): """ Constructs and sends a :py:class:`requests.Request`. Returns :py:class:`requests.Response` object. @@ -119,6 +118,8 @@ def request(self, method, url, name=None, catch_response=False, **kwargs): request_meta["name"] = name or (response.history and response.history[0] or response).request.path_url + request_meta["context"] = context + # 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 kwargs.get("stream", False): @@ -129,7 +130,8 @@ def request(self, method, url, name=None, catch_response=False, **kwargs): if catch_response: response.locust_request_meta = request_meta return ResponseContextManager( - response, request_success=self.request_success, request_failure=self.request_failure + response, + request_event=self.request_event, ) else: if name: @@ -138,23 +140,21 @@ def request(self, method, url, name=None, catch_response=False, **kwargs): # to temporarily override the response.url attribute orig_url = response.url response.url = name + + exc = None try: response.raise_for_status() except RequestException as e: - self.request_failure.fire( - request_type=request_meta["method"], - name=request_meta["name"], - response_time=request_meta["response_time"], - response_length=request_meta["content_size"], - exception=e, - ) - else: - self.request_success.fire( - request_type=request_meta["method"], - name=request_meta["name"], - response_time=request_meta["response_time"], - response_length=request_meta["content_size"], - ) + exc = e + + self.request_event.fire( + request_type=request_meta["method"], + name=request_meta["name"], + response_time=request_meta["response_time"], + response_length=request_meta["content_size"], + exception=exc, + context=request_meta["context"], + ) if name: response.url = orig_url return response @@ -189,11 +189,10 @@ class ResponseContextManager(LocustResponse): _manual_result = None - def __init__(self, response, request_success, request_failure): + def __init__(self, response, request_event): # copy data from response to this object self.__dict__ = response.__dict__ - self._request_success = request_success - self._request_failure = request_failure + self._request_event = request_event def __enter__(self): return self @@ -201,9 +200,9 @@ def __enter__(self): def __exit__(self, exc, value, traceback): if self._manual_result is not None: if self._manual_result is True: - self._report_success() + self._report_request() elif isinstance(self._manual_result, Exception): - self._report_failure(self._manual_result) + self._report_request(self._manual_result) # if the user has already manually marked this response as failure or success # we can ignore the default behaviour of letting the response code determine the outcome @@ -211,7 +210,7 @@ def __exit__(self, exc, value, traceback): if exc: if isinstance(value, ResponseError): - self._report_failure(value) + self._report_request(value) else: # we want other unknown exceptions to be raised return False @@ -219,27 +218,20 @@ def __exit__(self, exc, value, traceback): try: self.raise_for_status() except requests.exceptions.RequestException as e: - self._report_failure(e) + self._report_request(e) else: - self._report_success() + self._report_request() return True - def _report_success(self): - self._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"], - ) - - def _report_failure(self, exc): - self._request_failure.fire( + def _report_request(self, exc=None): + self._request_event.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"], exception=exc, + context=self.locust_request_meta["context"], ) def success(self): diff --git a/locust/contrib/fasthttp.py b/locust/contrib/fasthttp.py index e692ffb82b..3ddad10350 100644 --- a/locust/contrib/fasthttp.py +++ b/locust/contrib/fasthttp.py @@ -127,6 +127,7 @@ def request( auth=None, json: dict = None, allow_redirects=True, + context: dict = {}, **kwargs, ): """ @@ -165,6 +166,7 @@ def request( request_meta["method"] = method request_meta["start_time"] = default_timer() request_meta["name"] = name or path + request_meta["context"] = context headers = headers or {} if auth: @@ -200,13 +202,15 @@ def request( request_meta["content_size"] = len(response.content or "") except HTTPParseError as e: request_meta["response_time"] = int((default_timer() - request_meta["start_time"]) * 1000) - self.environment.events.request_failure.fire( + self.environment.events.request.fire( request_type=request_meta["method"], name=request_meta["name"], response_time=request_meta["response_time"], response_length=0, exception=e, + context=request_meta["context"], ) + return response # Record the consumed time @@ -218,23 +222,20 @@ def request( response.locust_request_meta = request_meta return ResponseContextManager(response, environment=self.environment) else: + exception = None try: response.raise_for_status() except FAILURE_EXCEPTIONS as e: - self.environment.events.request_failure.fire( - request_type=request_meta["method"], - name=request_meta["name"], - response_time=request_meta["response_time"], - response_length=request_meta["content_size"], - exception=e, - ) - else: - self.environment.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"], - ) + exception = e + + self.environment.events.request.fire( + request_type=request_meta["method"], + name=request_meta["name"], + response_time=request_meta["response_time"], + response_length=request_meta["content_size"], + exception=exception, + context=request_meta["context"], + ) return response def delete(self, path, **kwargs): @@ -430,9 +431,9 @@ def __enter__(self): def __exit__(self, exc, value, traceback): if self._manual_result is not None: if self._manual_result is True: - self._report_success() + self._report_request() elif isinstance(self._manual_result, Exception): - self._report_failure(self._manual_result) + self._report_request(exc=self._manual_result) # if the user has already manually marked this response as failure or success # we can ignore the default behaviour of letting the response code determine the outcome @@ -440,33 +441,26 @@ def __exit__(self, exc, value, traceback): if exc: if isinstance(value, ResponseError): - self._report_failure(value) + self._report_request(value) else: return False else: try: self.raise_for_status() except FAILURE_EXCEPTIONS as e: - self._report_failure(e) - else: - self._report_success() - return True + exc = e + self._report_request(exc) - def _report_success(self): - self.environment.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"], - ) + return True - def _report_failure(self, exc): - self.environment.events.request_failure.fire( + def _report_request(self, exc=None): + self.environment.events.request.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"], exception=exc, + context=self.locust_request_meta["context"], ) def success(self): diff --git a/locust/event.py b/locust/event.py index f79942c764..d6e2a9976b 100644 --- a/locust/event.py +++ b/locust/event.py @@ -48,6 +48,20 @@ def fire(self, *, reverse=False, **kwargs): class Events: + request: EventHook + """ + Fired when a request in completed, successful or unsuccessful. This event is typically used to report requests when writing custom clients for locust. + + Event arguments: + + :param request_type: Request type method used + :param name: Path to the URL that was called (or override name if it was used in the call to the client) + :param response_time: Time in milliseconds until exception was thrown + :param response_length: Content-length of the response + :param exception: Exception instance that was thrown. None if no exception + :param context: Dict with context values specified when performing request + """ + request_success: EventHook """ Fired when a request is completed successfully. This event is typically used to report requests diff --git a/locust/runners.py b/locust/runners.py index 0dcaf29cfd..95466893b7 100644 --- a/locust/runners.py +++ b/locust/runners.py @@ -71,15 +71,19 @@ def __init__(self, environment): self.target_user_count = None # set up event listeners for recording requests - def on_request_success(request_type, name, response_time, response_length, **kwargs): + def on_request(request_type, name, response_time, response_length, exception, context, **kwargs): self.stats.log_request(request_type, name, response_time, response_length) + if exception: + self.stats.log_error(request_type, name, exception) + # Fire fail event. To be removed in future versions. + self.environment.events.request_failure.fire( + request_type, name, response_time, response_length, exception + ) + else: + # Fire success event.To be removed in future versions. + self.environment.events.request_success.fire(request_type, name, response_time, response_length) - def on_request_failure(request_type, name, response_time, response_length, exception, **kwargs): - self.stats.log_request(request_type, name, response_time, response_length) - self.stats.log_error(request_type, name, exception) - - self.environment.events.request_success.add_listener(on_request_success) - self.environment.events.request_failure.add_listener(on_request_failure) + self.environment.events.request.add_listener(on_request) self.connection_broken = False # register listener that resets stats when spawning is complete diff --git a/locust/user/users.py b/locust/user/users.py index 5add2c7099..c9b4b3a5d6 100644 --- a/locust/user/users.py +++ b/locust/user/users.py @@ -216,8 +216,7 @@ def __init__(self, *args, **kwargs): session = HttpSession( base_url=self.host, - request_success=self.environment.events.request_success, - request_failure=self.environment.events.request_failure, + request_event=self.environment.events.request, ) session.trust_env = False self.client = session From 81cbeb9c152a41f2a9743be2cb433b4e54e6882d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dennis=20Krone=CC=81?= Date: Wed, 21 Apr 2021 16:24:44 +0200 Subject: [PATCH 03/21] Update tests and examples for the new request event. --- examples/events.py | 4 ++-- examples/extend_web_ui/extend.py | 4 ++-- examples/manual_stats_reporting.py | 5 +++-- locust/test/test_fasthttp.py | 18 ++++++++++-------- locust/test/test_http.py | 17 +++++++++-------- locust/test/test_locust_class.py | 15 +++++++-------- 6 files changed, 33 insertions(+), 30 deletions(-) diff --git a/examples/events.py b/examples/events.py index 13d842e3b0..f3b3b9dd5d 100644 --- a/examples/events.py +++ b/examples/events.py @@ -47,8 +47,8 @@ def total_content_length(): return "Total content-length received: %i" % stats["content-length"] -@events.request_success.add_listener -def on_request_success(request_type, name, response_time, response_length): +@events.request.add_listener +def on_request(request_type, name, response_time, response_length, exception, context, **kwargs): """ Event handler that get triggered on every successful request """ diff --git a/examples/extend_web_ui/extend.py b/examples/extend_web_ui/extend.py index 0bd12f4b8c..f22aed5a71 100644 --- a/examples/extend_web_ui/extend.py +++ b/examples/extend_web_ui/extend.py @@ -123,8 +123,8 @@ def content_length_csv(): environment.web_ui.app.register_blueprint(extend) -@events.request_success.add_listener -def on_request_success(request_type, name, response_time, response_length): +@events.request.add_listener +def on_request(request_type, name, response_time, response_length, exception, context, **kwargs): """ Event handler that get triggered on every successful request """ diff --git a/examples/manual_stats_reporting.py b/examples/manual_stats_reporting.py index 69e09c15cb..23299a6541 100644 --- a/examples/manual_stats_reporting.py +++ b/examples/manual_stats_reporting.py @@ -30,7 +30,7 @@ def _manual_report(name): try: yield except Exception as e: - events.request_failure.fire( + events.request.fire( request_type="manual", name=name, response_time=(time() - start_time) * 1000, @@ -39,11 +39,12 @@ def _manual_report(name): ) raise else: - events.request_success.fire( + events.request.fire( request_type="manual", name=name, response_time=(time() - start_time) * 1000, response_length=0, + exception=None, ) diff --git a/locust/test/test_fasthttp.py b/locust/test/test_fasthttp.py index 9a8c4926fc..1e44f66466 100644 --- a/locust/test/test_fasthttp.py +++ b/locust/test/test_fasthttp.py @@ -423,15 +423,14 @@ class MyUser(FastHttpUser): self.num_failures = 0 self.num_success = 0 - def on_failure(request_type, name, response_time, response_length, exception, **kwargs): - self.num_failures += 1 - self.last_failure_exception = exception + def on_request(exception, **kwargs): + if exception: + self.num_failures += 1 + self.last_failure_exception = exception + else: + self.num_success += 1 - def on_success(**kwargs): - self.num_success += 1 - - self.environment.events.request_failure.add_listener(on_failure) - self.environment.events.request_success.add_listener(on_success) + self.environment.events.request.add_listener(on_request) def test_catch_response(self): self.assertEqual(500, self.locust.client.get("/fail").status_code) @@ -443,10 +442,13 @@ def test_catch_response(self): self.assertEqual(1, self.num_failures) self.assertEqual(1, self.num_success) self.assertIn("ultra fast", str(response.content)) + print(self.num_failures) with self.locust.client.get("/ultra_fast", catch_response=True) as response: raise ResponseError("Not working") + print(self.num_failures) + self.assertEqual(2, self.num_failures) self.assertEqual(1, self.num_success) diff --git a/locust/test/test_http.py b/locust/test/test_http.py index 1ad90b4fb6..7d8491e0c1 100644 --- a/locust/test/test_http.py +++ b/locust/test/test_http.py @@ -13,8 +13,7 @@ def get_client(self, base_url=None): base_url = "http://127.0.0.1:%i" % self.port return HttpSession( base_url=base_url, - request_success=self.environment.events.request_success, - request_failure=self.environment.events.request_failure, + request_event=self.environment.events.request, ) def test_get(self): @@ -111,10 +110,10 @@ def test_error_message(self): s = self.get_client() kwargs = {} - def on_error(**kw): + def on_request(**kw): kwargs.update(kw) - self.environment.events.request_failure.add_listener(on_error) + self.environment.events.request.add_listener(on_request) s.request("get", "/wrong_url", context={"foo": "bar"}) self.assertIn("/wrong_url", str(kwargs["exception"])) self.assertDictEqual({"foo": "bar"}, kwargs["context"]) @@ -123,10 +122,11 @@ def test_context_in_success(self): s = self.get_client() kwargs = {} - def on_success(**kw): + def on_request(exception, **kw): + self.assertIsNone(exception) kwargs.update(kw) - self.environment.events.request_success.add_listener(on_success) + self.environment.events.request.add_listener(on_request) s.request("get", "/request_method", context={"foo": "bar"}) self.assertDictEqual({"foo": "bar"}, kwargs["context"]) @@ -134,10 +134,11 @@ def test_error_message_with_name_replacement(self): s = self.get_client() kwargs = {} - def on_error(**kw): + def on_request(**kw): + self.assertIsNotNone(kw["exception"]) kwargs.update(kw) - self.environment.events.request_failure.add_listener(on_error) + self.environment.events.request.add_listener(on_request) s.request("get", "/wrong_url/01", name="replaced_url_name", context={"foo": "bar"}) self.assertIn("for url: replaced_url_name", str(kwargs["exception"])) self.assertDictEqual({"foo": "bar"}, kwargs["context"]) diff --git a/locust/test/test_locust_class.py b/locust/test/test_locust_class.py index 1dd699ce70..933af1a05f 100644 --- a/locust/test/test_locust_class.py +++ b/locust/test/test_locust_class.py @@ -724,15 +724,14 @@ class MyUser(HttpUser): self.num_failures = 0 self.num_success = 0 - def on_failure(request_type, name, response_time, response_length, exception, **kwargs): - self.num_failures += 1 - self.last_failure_exception = exception - - def on_success(**kwargs): - self.num_success += 1 + def on_request(request_type, name, response_time, response_length, exception, context, **kwargs): + if exception: + self.num_failures += 1 + self.last_failure_exception = exception + else: + self.num_success += 1 - self.environment.events.request_failure.add_listener(on_failure) - self.environment.events.request_success.add_listener(on_success) + self.environment.events.request.add_listener(on_request) def test_catch_response(self): self.assertEqual(500, self.locust.client.get("/fail").status_code) From 896ca9f722aad071a519dcaeff5ffbe29804dab1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dennis=20Krone=CC=81?= Date: Fri, 23 Apr 2021 14:54:25 +0200 Subject: [PATCH 04/21] Move request_success/failure event propagation to events.py --- locust/event.py | 40 ++++++++++++++++++++++++++++++++++++++-- locust/runners.py | 7 ------- 2 files changed, 38 insertions(+), 9 deletions(-) diff --git a/locust/event.py b/locust/event.py index d6e2a9976b..9dd5d2de8d 100644 --- a/locust/event.py +++ b/locust/event.py @@ -47,6 +47,16 @@ def fire(self, *, reverse=False, **kwargs): log.unhandled_greenlet_exception = True +class DeprecatedEventHook(EventHook): + def __init__(self, message): + self.message = message + super().__init__() + + def add_listener(self, handler): + logging.warning(self.message) + return super().add_listener(handler) + + class Events: request: EventHook """ @@ -62,7 +72,7 @@ class Events: :param context: Dict with context values specified when performing request """ - request_success: EventHook + request_success: DeprecatedEventHook """ Fired when a request is completed successfully. This event is typically used to report requests when writing custom clients for locust. @@ -75,7 +85,7 @@ class Events: :param response_length: Content-length of the response """ - request_failure: EventHook + request_failure: DeprecatedEventHook """ Fired when a request fails. This event is typically used to report failed requests when writing custom clients for locust. @@ -193,3 +203,29 @@ def __init__(self): for name, value in self.__annotations__.items(): if value == EventHook: setattr(self, name, value()) + + setattr( + self, "request_success", DeprecatedEventHook("request_success event deprecated. Use the request event.") + ) + setattr( + self, "request_failure", DeprecatedEventHook("request_failure event deprecated. Use the request event.") + ) + + def on_request(request_type, name, response_time, response_length, exception, context, **kwargs): + if exception: + self.request_failure.fire( + request_type=request_type, + name=name, + response_time=response_time, + response_length=response_length, + exception=exception, + ) + else: + self.request_success.fire( + request_type=request_type, + name=name, + response_time=response_time, + response_length=response_length, + ) + + self.request.add_listener(on_request) diff --git a/locust/runners.py b/locust/runners.py index 95466893b7..f3f4684c37 100644 --- a/locust/runners.py +++ b/locust/runners.py @@ -75,13 +75,6 @@ def on_request(request_type, name, response_time, response_length, exception, co self.stats.log_request(request_type, name, response_time, response_length) if exception: self.stats.log_error(request_type, name, exception) - # Fire fail event. To be removed in future versions. - self.environment.events.request_failure.fire( - request_type, name, response_time, response_length, exception - ) - else: - # Fire success event.To be removed in future versions. - self.environment.events.request_success.fire(request_type, name, response_time, response_length) self.environment.events.request.add_listener(on_request) self.connection_broken = False From 41e331c8dc2b395a960733b6517423d2115ca922 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dennis=20Krone=CC=81?= Date: Fri, 23 Apr 2021 14:56:11 +0200 Subject: [PATCH 05/21] Cleanup of comments --- locust/clients.py | 10 +++------- locust/contrib/fasthttp.py | 4 ++-- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/locust/clients.py b/locust/clients.py index e90f22687a..04e9ef2384 100644 --- a/locust/clients.py +++ b/locust/clients.py @@ -129,10 +129,7 @@ def request(self, method, url, name=None, catch_response=False, context={}, **kw if catch_response: response.locust_request_meta = request_meta - return ResponseContextManager( - response, - request_event=self.request_event, - ) + return ResponseContextManager(response, request_event=self.request_event) else: if name: # Since we use the Exception message when grouping failures, in order to not get @@ -198,14 +195,13 @@ def __enter__(self): return self def __exit__(self, exc, value, traceback): + # if the user has already manually marked this response as failure or success + # we can ignore the default behaviour of letting the response code determine the outcome if self._manual_result is not None: if self._manual_result is True: self._report_request() elif isinstance(self._manual_result, Exception): self._report_request(self._manual_result) - - # if the user has already manually marked this response as failure or success - # we can ignore the default behaviour of letting the response code determine the outcome return exc is None if exc: diff --git a/locust/contrib/fasthttp.py b/locust/contrib/fasthttp.py index 3ddad10350..13f62bcf5c 100644 --- a/locust/contrib/fasthttp.py +++ b/locust/contrib/fasthttp.py @@ -429,14 +429,14 @@ def __enter__(self): return self def __exit__(self, exc, value, traceback): + # if the user has already manually marked this response as failure or success + # we can ignore the default behaviour of letting the response code determine the outcome if self._manual_result is not None: if self._manual_result is True: self._report_request() elif isinstance(self._manual_result, Exception): self._report_request(exc=self._manual_result) - # if the user has already manually marked this response as failure or success - # we can ignore the default behaviour of letting the response code determine the outcome return exc is None if exc: From 6e4b2e24852208d828dfe9f453a9261224935199 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dennis=20Krone=CC=81?= Date: Fri, 23 Apr 2021 17:05:45 +0200 Subject: [PATCH 06/21] Update tests to use new request event --- locust/test/test_runners.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/locust/test/test_runners.py b/locust/test/test_runners.py index 60af245bba..ff0926407b 100644 --- a/locust/test/test_runners.py +++ b/locust/test/test_runners.py @@ -311,11 +311,13 @@ class MyUser(User): class task_set(TaskSet): @task def my_task(self): - self.user.environment.events.request_success.fire( + self.user.environment.events.request.fire( request_type="GET", name="/test", response_time=666, response_length=1337, + exception=None, + context={}, ) sleep(2) @@ -334,11 +336,13 @@ class MyUser(User): class task_set(TaskSet): @task def my_task(self): - self.user.environment.events.request_success.fire( + self.user.environment.events.request.fire( request_type="GET", name="/test", response_time=666, response_length=1337, + exception=None, + context={}, ) sleep(2) @@ -447,11 +451,13 @@ class TestUser(User): @task def incr_stats(l): - l.environment.events.request_success.fire( + l.environment.events.request.fire( request_type="GET", name="/", response_time=1337, response_length=666, + exception=None, + context={}, ) with mock.patch("locust.runners.WORKER_REPORT_INTERVAL", new=0.3): From e18c1751129107343bc2bb32ea38c31cb0edc16e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dennis=20Krone=CC=81?= Date: Mon, 26 Apr 2021 13:58:12 +0200 Subject: [PATCH 07/21] Cleanup code and fix faulty comments. --- docs/extending-locust.rst | 7 +++--- .../custom_xmlrpc_client/xmlrpc_locustfile.py | 25 +++++++++++-------- examples/events.py | 2 +- examples/extend_web_ui/extend.py | 2 +- locust/clients.py | 2 +- locust/contrib/fasthttp.py | 4 +-- locust/test/test_fasthttp.py | 3 --- 7 files changed, 23 insertions(+), 22 deletions(-) diff --git a/docs/extending-locust.rst b/docs/extending-locust.rst index 16fbb1505e..56437b5e78 100644 --- a/docs/extending-locust.rst +++ b/docs/extending-locust.rst @@ -15,9 +15,10 @@ 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 made a request to: %s" % name) + @events.request.add_listener + def my_request_handler(request_type, name, response_time, response_length, context, exception, **kw): + if not exception: + print("Successfully made a request to: %s" % name) .. note:: diff --git a/examples/custom_xmlrpc_client/xmlrpc_locustfile.py b/examples/custom_xmlrpc_client/xmlrpc_locustfile.py index fb31d522de..7385c12603 100644 --- a/examples/custom_xmlrpc_client/xmlrpc_locustfile.py +++ b/examples/custom_xmlrpc_client/xmlrpc_locustfile.py @@ -18,20 +18,23 @@ def __getattr__(self, name): def wrapper(*args, **kwargs): start_time = time.time() + exc = None try: result = func(*args, **kwargs) except Fault as e: - total_time = int((time.time() - start_time) * 1000) - 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) - 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 + exc = e + + total_time = int((time.time() - start_time) * 1000) + self._locust_environment.events.request.fire( + request_type="xmlrpc", + name=name, + response_time=total_time, + response_length=0, + context={}, + exception=exc, + ) + # 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 return wrapper diff --git a/examples/events.py b/examples/events.py index f3b3b9dd5d..dc312bf914 100644 --- a/examples/events.py +++ b/examples/events.py @@ -50,7 +50,7 @@ def total_content_length(): @events.request.add_listener def on_request(request_type, name, response_time, response_length, exception, context, **kwargs): """ - Event handler that get triggered on every successful request + Event handler that get triggered on every request. """ stats["content-length"] += response_length diff --git a/examples/extend_web_ui/extend.py b/examples/extend_web_ui/extend.py index f22aed5a71..a897289514 100644 --- a/examples/extend_web_ui/extend.py +++ b/examples/extend_web_ui/extend.py @@ -126,7 +126,7 @@ def content_length_csv(): @events.request.add_listener def on_request(request_type, name, response_time, response_length, exception, context, **kwargs): """ - Event handler that get triggered on every successful request + Event handler that get triggered on every request """ stats.setdefault(name, {"content-length": 0}) stats[name]["content-length"] += response_length diff --git a/locust/clients.py b/locust/clients.py index 04e9ef2384..b1fe038968 100644 --- a/locust/clients.py +++ b/locust/clients.py @@ -226,8 +226,8 @@ def _report_request(self, exc=None): name=self.locust_request_meta["name"], response_time=self.locust_request_meta["response_time"], response_length=self.locust_request_meta["content_size"], - exception=exc, context=self.locust_request_meta["context"], + exception=exc, ) def success(self): diff --git a/locust/contrib/fasthttp.py b/locust/contrib/fasthttp.py index 13f62bcf5c..33a5d7b2c8 100644 --- a/locust/contrib/fasthttp.py +++ b/locust/contrib/fasthttp.py @@ -435,7 +435,7 @@ def __exit__(self, exc, value, traceback): if self._manual_result is True: self._report_request() elif isinstance(self._manual_result, Exception): - self._report_request(exc=self._manual_result) + self._report_request(self._manual_result) return exc is None @@ -459,8 +459,8 @@ def _report_request(self, exc=None): name=self.locust_request_meta["name"], response_time=self.locust_request_meta["response_time"], response_length=self.locust_request_meta["content_size"], - exception=exc, context=self.locust_request_meta["context"], + exception=exc, ) def success(self): diff --git a/locust/test/test_fasthttp.py b/locust/test/test_fasthttp.py index 1e44f66466..3e138b6bd6 100644 --- a/locust/test/test_fasthttp.py +++ b/locust/test/test_fasthttp.py @@ -442,13 +442,10 @@ def test_catch_response(self): self.assertEqual(1, self.num_failures) self.assertEqual(1, self.num_success) self.assertIn("ultra fast", str(response.content)) - print(self.num_failures) with self.locust.client.get("/ultra_fast", catch_response=True) as response: raise ResponseError("Not working") - print(self.num_failures) - self.assertEqual(2, self.num_failures) self.assertEqual(1, self.num_success) From 63814a687a807a1ab30b70ded001720209a80574 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dennis=20Krone=CC=81?= Date: Mon, 26 Apr 2021 14:30:44 +0200 Subject: [PATCH 08/21] Update changelog --- docs/changelog.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 6d5ac183d6..8c63ae6aee 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,12 @@ Changelog Highlights For full details of the Locust changelog, please see https://github.com/locustio/locust/blob/master/CHANGELOG.md +1.5.0 +===== + +* Add new event called request. Is called on every request successful or not. request_success and request_failure are still available but can be considered deprecated +* Add parameter to the request event. Can be used to forward information when calling a request, things like user information, tags etc + 1.4.4 ===== From a88a84cadd7206b1c6b7f36cba856082c9b47b1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dennis=20Krone=CC=81?= Date: Mon, 26 Apr 2021 16:04:21 +0200 Subject: [PATCH 09/21] Pin a black version --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index d4ffd43955..a23f696702 100644 --- a/tox.ini +++ b/tox.ini @@ -15,7 +15,7 @@ deps = mock pyquery cryptography - black + black==21.4b0 commands = flake8 . --count --show-source --statistics coverage run -m unittest discover [] From 2e7a8b5697a98d1d314d6fc3ef0589f81f09d7fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dennis=20Krone=CC=81?= Date: Mon, 26 Apr 2021 16:05:54 +0200 Subject: [PATCH 10/21] Reformat files with new black version --- locust/clients.py | 2 +- locust/contrib/fasthttp.py | 2 +- locust/test/test_runners.py | 4 ++-- locust/test/util.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/locust/clients.py b/locust/clients.py index b1fe038968..77eb9447ca 100644 --- a/locust/clients.py +++ b/locust/clients.py @@ -65,7 +65,7 @@ def __init__(self, base_url, request_event, *args, **kwargs): self.auth = HTTPBasicAuth(parsed_url.username, parsed_url.password) def _build_url(self, path): - """ prepend url with hostname unless it's already an absolute URL """ + """prepend url with hostname unless it's already an absolute URL""" if absolute_http_url_regexp.match(path): return path else: diff --git a/locust/contrib/fasthttp.py b/locust/contrib/fasthttp.py index 33a5d7b2c8..48aa461f2a 100644 --- a/locust/contrib/fasthttp.py +++ b/locust/contrib/fasthttp.py @@ -94,7 +94,7 @@ def __init__(self, environment: Environment, base_url: str, insecure=True, **kwa 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 """ + """prepend url with hostname unless it's already an absolute URL""" if absolute_http_url_regexp.match(path): return path else: diff --git a/locust/test/test_runners.py b/locust/test/test_runners.py index ff0926407b..0786e41b67 100644 --- a/locust/test/test_runners.py +++ b/locust/test/test_runners.py @@ -1269,7 +1269,7 @@ def will_error(self): self.assertEqual(2, exception["count"]) def test_exception_is_caught(self): - """ Test that exceptions are stored, and execution continues """ + """Test that exceptions are stored, and execution continues""" class MyTaskSet(TaskSet): def __init__(self, *a, **kw): @@ -1308,7 +1308,7 @@ class MyUser(User): self.assertEqual(2, exception["count"]) def test_master_reset_connection(self): - """ Test that connection will be reset when network issues found """ + """Test that connection will be reset when network issues found""" with mock.patch("locust.runners.FALLBACK_INTERVAL", new=0.1): with mock.patch("locust.rpc.rpc.Server", mocked_rpc()) as server: master = self.get_runner() diff --git a/locust/test/util.py b/locust/test/util.py index 018bb17e89..b1ea5fd34d 100644 --- a/locust/test/util.py +++ b/locust/test/util.py @@ -37,7 +37,7 @@ def get_free_tcp_port(): def create_tls_cert(hostname): - """ Generate a TLS cert and private key to serve over https """ + """Generate a TLS cert and private key to serve over https""" key = rsa.generate_private_key(public_exponent=2 ** 16 + 1, key_size=2048, backend=default_backend()) name = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, hostname)]) now = datetime.utcnow() From 2c442dc91d48d9b1d0f61f20c3c4aaa73e2d7839 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dennis=20Krone=CC=81?= Date: Mon, 26 Apr 2021 16:08:54 +0200 Subject: [PATCH 11/21] Ignore git blame history for black formatting --- .git-blame-ignore-revs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index 98508993f6..c97121c523 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -1,2 +1,3 @@ # Migrate code style to Black -7c0fcc213d3988f6e7c6ffef63b24afe00e5fbd9 \ No newline at end of file +7c0fcc213d3988f6e7c6ffef63b24afe00e5fbd9 +2e7a8b5697a98d1d314d6fc3ef0589f81f09d7fe From 246089cf1312ab69c39d3518df38657209437d7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dennis=20Krone=CC=81?= Date: Tue, 27 Apr 2021 17:01:02 +0200 Subject: [PATCH 12/21] Rename request forwarding event --- locust/event.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/locust/event.py b/locust/event.py index 9dd5d2de8d..06c4b1ebb6 100644 --- a/locust/event.py +++ b/locust/event.py @@ -211,7 +211,9 @@ def __init__(self): self, "request_failure", DeprecatedEventHook("request_failure event deprecated. Use the request event.") ) - def on_request(request_type, name, response_time, response_length, exception, context, **kwargs): + def fire_deprecated_request_handlers( + request_type, name, response_time, response_length, exception, context, **kwargs + ): if exception: self.request_failure.fire( request_type=request_type, @@ -228,4 +230,4 @@ def on_request(request_type, name, response_time, response_length, exception, co response_length=response_length, ) - self.request.add_listener(on_request) + self.request.add_listener(fire_deprecated_request_handlers) From 46c0f725a61ae3c68d49559056ec22cee42e6e9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dennis=20Krone=CC=81?= Date: Tue, 27 Apr 2021 17:01:21 +0200 Subject: [PATCH 13/21] Add test for deprecated request events --- locust/test/test_fasthttp.py | 19 +++++++++++++++++++ locust/test/test_http.py | 17 +++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/locust/test/test_fasthttp.py b/locust/test/test_fasthttp.py index 3e138b6bd6..d8a0a477d7 100644 --- a/locust/test/test_fasthttp.py +++ b/locust/test/test_fasthttp.py @@ -520,6 +520,25 @@ class MyUser(FastHttpUser): self.assertEqual(0, self.num_success) self.assertEqual(1, self.num_failures) + def test_status_specific_request_event(self): + status = {"success_amount": 0, "failure_amount": 0} + + def on_success(**kw): + status["success_amount"] += 1 + + def on_failure(**kw): + status["failure_amount"] += 1 + + self.environment.events.request_success.add_listener(on_success) + self.environment.events.request_failure.add_listener(on_failure) + with self.locust.client.get("/ultra_fast", catch_response=True) as response: + pass + with self.locust.client.get("/wrong_url", catch_response=True) as response: + pass + + self.assertEqual(1, status["success_amount"]) + self.assertEqual(1, status["failure_amount"]) + class TestFastHttpSsl(LocustTestCase): def setUp(self): diff --git a/locust/test/test_http.py b/locust/test/test_http.py index 7d8491e0c1..706c905c34 100644 --- a/locust/test/test_http.py +++ b/locust/test/test_http.py @@ -130,6 +130,23 @@ def on_request(exception, **kw): s.request("get", "/request_method", context={"foo": "bar"}) self.assertDictEqual({"foo": "bar"}, kwargs["context"]) + def test_status_specific_request_event(self): + s = self.get_client() + status = {"success_amount": 0, "failure_amount": 0} + + def on_success(**kw): + status["success_amount"] += 1 + + def on_failure(**kw): + status["failure_amount"] += 1 + + self.environment.events.request_success.add_listener(on_success) + self.environment.events.request_failure.add_listener(on_failure) + s.request("get", "/request_method") + s.request("get", "/wrong_url") + self.assertEqual(1, status["success_amount"]) + self.assertEqual(1, status["failure_amount"]) + def test_error_message_with_name_replacement(self): s = self.get_client() kwargs = {} From 6c4da3a1f1f19aa919c0f2a875ca00d88c8905a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dennis=20Krone=CC=81?= Date: Wed, 28 Apr 2021 11:36:20 +0200 Subject: [PATCH 14/21] Rename request test --- locust/test/test_fasthttp.py | 2 +- locust/test/test_http.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/locust/test/test_fasthttp.py b/locust/test/test_fasthttp.py index d8a0a477d7..5c8b72e721 100644 --- a/locust/test/test_fasthttp.py +++ b/locust/test/test_fasthttp.py @@ -520,7 +520,7 @@ class MyUser(FastHttpUser): self.assertEqual(0, self.num_success) self.assertEqual(1, self.num_failures) - def test_status_specific_request_event(self): + def test_deprecated_request_events(self): status = {"success_amount": 0, "failure_amount": 0} def on_success(**kw): diff --git a/locust/test/test_http.py b/locust/test/test_http.py index 706c905c34..d7e4c6faea 100644 --- a/locust/test/test_http.py +++ b/locust/test/test_http.py @@ -130,7 +130,7 @@ def on_request(exception, **kw): s.request("get", "/request_method", context={"foo": "bar"}) self.assertDictEqual({"foo": "bar"}, kwargs["context"]) - def test_status_specific_request_event(self): + def test_deprecated_request_events(self): s = self.get_client() status = {"success_amount": 0, "failure_amount": 0} From 7076a8a7e552b7158b94ca61354940bb8a9852f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dennis=20Krone=CC=81?= Date: Thu, 29 Apr 2021 09:50:41 +0200 Subject: [PATCH 15/21] Include the locust user in request meta. Will allow getting the calling user in the request event. --- locust/clients.py | 74 +++++++++++++++-------------------- locust/contrib/fasthttp.py | 75 +++++++++++++++--------------------- locust/test/test_fasthttp.py | 28 +++++++------- locust/test/test_http.py | 1 + locust/user/users.py | 1 + 5 files changed, 76 insertions(+), 103 deletions(-) diff --git a/locust/clients.py b/locust/clients.py index 77eb9447ca..8c5273f438 100644 --- a/locust/clients.py +++ b/locust/clients.py @@ -44,11 +44,12 @@ class HttpSession(requests.Session): and then mark it as successful even if the response code was not (i.e 500 or 404). """ - def __init__(self, base_url, request_event, *args, **kwargs): + def __init__(self, base_url, request_event, user, *args, **kwargs): super().__init__(*args, **kwargs) self.base_url = base_url self.request_event = request_event + self.user = user # Check for basic authentication parsed_url = urlparse(self.base_url) @@ -103,33 +104,30 @@ def request(self, method, url, name=None, catch_response=False, context={}, **kw # prepend url with hostname unless it's already an absolute URL url = self._build_url(url) - - # 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"] = time.monotonic() + start_time = time.monotonic() response = self._send_request_safe_mode(method, url, **kwargs) - # record the consumed time - request_meta["response_time"] = (time.monotonic() - request_meta["start_time"]) * 1000 - - request_meta["name"] = name or (response.history and response.history[0] or response).request.path_url - - request_meta["context"] = context + # store meta data that is used when reporting the request to locust's statistics + request_meta = { + "request_type": method, + "start_time": start_time, + "response_time": (time.monotonic() - start_time) * 1000, + "name": name or (response.history and response.history[0] or response).request.path_url, + "context": context, + "user": self.user, + "exception": None, + } # 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 kwargs.get("stream", False): - request_meta["content_size"] = int(response.headers.get("content-length") or 0) + request_meta["response_length"] = int(response.headers.get("content-length") or 0) else: - request_meta["content_size"] = len(response.content or b"") + request_meta["response_length"] = len(response.content or b"") if catch_response: - response.locust_request_meta = request_meta - return ResponseContextManager(response, request_event=self.request_event) + return ResponseContextManager(response, request_event=self.request_event, request_meta=request_meta) else: if name: # Since we use the Exception message when grouping failures, in order to not get @@ -138,20 +136,12 @@ def request(self, method, url, name=None, catch_response=False, context={}, **kw orig_url = response.url response.url = name - exc = None try: response.raise_for_status() except RequestException as e: - exc = e - - self.request_event.fire( - request_type=request_meta["method"], - name=request_meta["name"], - response_time=request_meta["response_time"], - response_length=request_meta["content_size"], - exception=exc, - context=request_meta["context"], - ) + request_meta["exception"] = e + + self.request_event.fire(**request_meta) if name: response.url = orig_url return response @@ -186,10 +176,11 @@ class ResponseContextManager(LocustResponse): _manual_result = None - def __init__(self, response, request_event): + def __init__(self, response, request_event, request_meta): # copy data from response to this object self.__dict__ = response.__dict__ self._request_event = request_event + self.request_meta = request_meta def __enter__(self): return self @@ -199,14 +190,16 @@ def __exit__(self, exc, value, traceback): # we can ignore the default behaviour of letting the response code determine the outcome if self._manual_result is not None: if self._manual_result is True: - self._report_request() + self.request_meta["exception"] = None elif isinstance(self._manual_result, Exception): - self._report_request(self._manual_result) + self.request_meta["exception"] = self._manual_result + self._report_request() return exc is None if exc: if isinstance(value, ResponseError): - self._report_request(value) + self.request_meta["exception"] = value + self._report_request() else: # we want other unknown exceptions to be raised return False @@ -214,21 +207,14 @@ def __exit__(self, exc, value, traceback): try: self.raise_for_status() except requests.exceptions.RequestException as e: - self._report_request(e) - else: - self._report_request() + self.request_meta["exception"] = e + + self._report_request() return True def _report_request(self, exc=None): - self._request_event.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"], - context=self.locust_request_meta["context"], - exception=exc, - ) + self._request_event.fire(**self.request_meta) def success(self): """ diff --git a/locust/contrib/fasthttp.py b/locust/contrib/fasthttp.py index 48aa461f2a..279e683903 100644 --- a/locust/contrib/fasthttp.py +++ b/locust/contrib/fasthttp.py @@ -64,10 +64,11 @@ def insecure_ssl_context_factory(): class FastHttpSession: auth_header = None - def __init__(self, environment: Environment, base_url: str, insecure=True, **kwargs): + def __init__(self, environment: Environment, base_url: str, user: "FastHttpUser", insecure=True, **kwargs): self.environment = environment self.base_url = base_url self.cookiejar = CookieJar() + self.user = user if insecure: ssl_context_factory = insecure_ssl_context_factory else: @@ -160,13 +161,16 @@ def request( # prepend url with hostname unless it's already an absolute URL url = self._build_url(path) + start_time = default_timer() + # 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 - request_meta["context"] = context + request_meta = { + "request_type": method, + "start_time": start_time, + "name": name or path, + "context": context, + "exception": None, + } headers = headers or {} if auth: @@ -196,21 +200,15 @@ def request( # 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) + request_meta["response_length"] = int(response.headers.get("response_length") or 0) else: try: - request_meta["content_size"] = len(response.content or "") + request_meta["response_length"] = len(response.content or "") except HTTPParseError as e: request_meta["response_time"] = int((default_timer() - request_meta["start_time"]) * 1000) - self.environment.events.request.fire( - request_type=request_meta["method"], - name=request_meta["name"], - response_time=request_meta["response_time"], - response_length=0, - exception=e, - context=request_meta["context"], - ) - + request_meta["response_length"] = 0 + request_meta["exception"] = e + self.environment.events.request.fire(**request_meta) return response # Record the consumed time @@ -219,23 +217,14 @@ def request( request_meta["response_time"] = int((default_timer() - request_meta["start_time"]) * 1000) if catch_response: - response.locust_request_meta = request_meta - return ResponseContextManager(response, environment=self.environment) + return ResponseContextManager(response, environment=self.environment, request_meta=request_meta) else: - exception = None try: response.raise_for_status() except FAILURE_EXCEPTIONS as e: - exception = e - - self.environment.events.request.fire( - request_type=request_meta["method"], - name=request_meta["name"], - response_time=request_meta["response_time"], - response_length=request_meta["content_size"], - exception=exception, - context=request_meta["context"], - ) + request_meta["exception"] = e + + self.environment.events.request.fire(**request_meta) return response def delete(self, path, **kwargs): @@ -418,12 +407,13 @@ class ResponseContextManager(FastResponse): _manual_result = None - def __init__(self, response, environment): + def __init__(self, response, environment, request_meta): # copy data from response to this object self.__dict__ = response.__dict__ self._cached_content = response.content # store reference to locust Environment self.environment = environment + self._request_meta = request_meta def __enter__(self): return self @@ -435,33 +425,28 @@ def __exit__(self, exc, value, traceback): if self._manual_result is True: self._report_request() elif isinstance(self._manual_result, Exception): - self._report_request(self._manual_result) + self._request_meta["exception"] = self._manual_result + self._report_request() return exc is None if exc: if isinstance(value, ResponseError): - self._report_request(value) + self._request_meta["exception"] = value + self._report_request() else: return False else: try: self.raise_for_status() except FAILURE_EXCEPTIONS as e: - exc = e - self._report_request(exc) + self._request_meta["exception"] = e + self._report_request() return True - def _report_request(self, exc=None): - self.environment.events.request.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"], - context=self.locust_request_meta["context"], - exception=exc, - ) + def _report_request(self): + self.environment.events.request.fire(**self._request_meta) def success(self): """ diff --git a/locust/test/test_fasthttp.py b/locust/test/test_fasthttp.py index 5c8b72e721..3067c10603 100644 --- a/locust/test/test_fasthttp.py +++ b/locust/test/test_fasthttp.py @@ -12,15 +12,15 @@ class TestFastHttpSession(WebserverTestCase): def get_client(self): - return FastHttpSession(self.environment, base_url="http://127.0.0.1:%i" % self.port) + return FastHttpSession(self.environment, base_url="http://127.0.0.1:%i" % self.port, user=None) def test_get(self): - s = FastHttpSession(self.environment, "http://127.0.0.1:%i" % self.port) + s = self.get_client() r = s.get("/ultra_fast") self.assertEqual(200, r.status_code) def test_connection_error(self): - s = FastHttpSession(self.environment, "http://localhost:1") + s = FastHttpSession(self.environment, "http://localhost:1", user=None) r = s.get("/", timeout=0.1) self.assertEqual(r.status_code, 0) self.assertEqual(None, r.content) @@ -29,13 +29,13 @@ def test_connection_error(self): self.assertTrue(isinstance(next(iter(self.runner.stats.errors.values())).error, ConnectionRefusedError)) def test_404(self): - s = FastHttpSession(self.environment, "http://127.0.0.1:%i" % self.port) + s = self.get_client() r = s.get("/does_not_exist") self.assertEqual(404, r.status_code) self.assertEqual(1, self.runner.stats.get("/does_not_exist", "GET").num_failures) def test_204(self): - s = FastHttpSession(self.environment, "http://127.0.0.1:%i" % self.port) + s = self.get_client() r = s.get("/status/204") self.assertEqual(204, r.status_code) self.assertEqual(1, self.runner.stats.get("/status/204", "GET").num_requests) @@ -45,7 +45,7 @@ def test_streaming_response(self): """ Test a request to an endpoint that returns a streaming response """ - s = FastHttpSession(self.environment, "http://127.0.0.1:%i" % self.port) + s = self.get_client() r = s.get("/streaming/30") # verify that the time reported includes the download time of the whole streamed response @@ -61,7 +61,7 @@ def test_streaming_response(self): _ = r.content def test_slow_redirect(self): - s = FastHttpSession(self.environment, "http://127.0.0.1:%i" % self.port) + s = self.get_client() url = "/redirect?url=/redirect&delay=0.5" r = s.get(url) stats = self.runner.stats.get(url, method="GET") @@ -69,7 +69,7 @@ def test_slow_redirect(self): self.assertGreater(stats.avg_response_time, 500) def test_post_redirect(self): - s = FastHttpSession(self.environment, "http://127.0.0.1:%i" % self.port) + s = self.get_client() url = "/redirect" r = s.post(url) self.assertEqual(200, r.status_code) @@ -79,32 +79,32 @@ def test_post_redirect(self): self.assertEqual(0, get_stats.num_requests) def test_cookie(self): - s = FastHttpSession(self.environment, "http://127.0.0.1:%i" % self.port) + s = self.get_client() 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(self.environment, "http://127.0.0.1:%i" % self.port) + s = self.get_client() r = s.head("/request_method") self.assertEqual(200, r.status_code) self.assertEqual("", r.content.decode()) def test_delete(self): - s = FastHttpSession(self.environment, "http://127.0.0.1:%i" % self.port) + s = self.get_client() r = s.delete("/request_method") self.assertEqual(200, r.status_code) self.assertEqual("DELETE", r.content.decode()) def test_patch(self): - s = FastHttpSession(self.environment, "http://127.0.0.1:%i" % self.port) + s = self.get_client() r = s.patch("/request_method") self.assertEqual(200, r.status_code) self.assertEqual("PATCH", r.content.decode()) def test_options(self): - s = FastHttpSession(self.environment, "http://127.0.0.1:%i" % self.port) + s = self.get_client() r = s.options("/request_method") self.assertEqual(200, r.status_code) self.assertEqual("", r.content.decode()) @@ -114,7 +114,7 @@ def test_options(self): ) def test_json_payload(self): - s = FastHttpSession(self.environment, "http://127.0.0.1:%i" % self.port) + s = self.get_client() r = s.post("/request_method", json={"foo": "bar"}) self.assertEqual(200, r.status_code) diff --git a/locust/test/test_http.py b/locust/test/test_http.py index d7e4c6faea..84fa89ad90 100644 --- a/locust/test/test_http.py +++ b/locust/test/test_http.py @@ -14,6 +14,7 @@ def get_client(self, base_url=None): return HttpSession( base_url=base_url, request_event=self.environment.events.request, + user=None, ) def test_get(self): diff --git a/locust/user/users.py b/locust/user/users.py index c9b4b3a5d6..ac620816ea 100644 --- a/locust/user/users.py +++ b/locust/user/users.py @@ -217,6 +217,7 @@ def __init__(self, *args, **kwargs): session = HttpSession( base_url=self.host, request_event=self.environment.events.request, + user=self, ) session.trust_env = False self.client = session From 98e33779a549b5b7402ee8444ab360dfd0c20844 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dennis=20Krone=CC=81?= Date: Thu, 29 Apr 2021 10:09:13 +0200 Subject: [PATCH 16/21] Fix issues with tests caused by earlier commits. --- locust/contrib/fasthttp.py | 1 + locust/test/test_fasthttp.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/locust/contrib/fasthttp.py b/locust/contrib/fasthttp.py index 279e683903..4617c4d59e 100644 --- a/locust/contrib/fasthttp.py +++ b/locust/contrib/fasthttp.py @@ -312,6 +312,7 @@ def __init__(self, environment): max_redirects=self.max_redirects, max_retries=self.max_retries, insecure=self.insecure, + user=self, ) diff --git a/locust/test/test_fasthttp.py b/locust/test/test_fasthttp.py index 3067c10603..0aa8888a85 100644 --- a/locust/test/test_fasthttp.py +++ b/locust/test/test_fasthttp.py @@ -384,7 +384,7 @@ class MyLocust(FastHttpUser): self.assertFalse("location" in resp.headers) def test_slow_redirect(self): - s = FastHttpSession(self.environment, "http://127.0.0.1:%i" % self.port) + s = FastHttpSession(self.environment, "http://127.0.0.1:%i" % self.port, user=None) url = "/redirect?url=/redirect&delay=0.5" r = s.get(url) stats = self.runner.stats.get(url, method="GET") @@ -565,7 +565,7 @@ def tearDown(self): self.web_ui.stop() def test_ssl_request_insecure(self): - s = FastHttpSession(self.environment, "https://127.0.0.1:%i" % self.web_port, insecure=True) + s = FastHttpSession(self.environment, "https://127.0.0.1:%i" % self.web_port, insecure=True, user=None) r = s.get("/") self.assertEqual(200, r.status_code) self.assertIn("Locust", r.content.decode("utf-8")) From 2c3da19bd7367d1833377bfe9a05d6b84d095a5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dennis=20Krone=CC=81?= Date: Thu, 29 Apr 2021 17:07:15 +0200 Subject: [PATCH 17/21] Add a context method to the user class. Context returned from this method is forwarded in requests performed with httpSession and fastHttpSession. Allows context to be connected to a User. --- locust/clients.py | 4 +++- locust/contrib/fasthttp.py | 3 +++ locust/test/test_fasthttp.py | 17 +++++++++++++++++ locust/test/test_http.py | 25 ++++++++++++++++++++++--- locust/user/users.py | 5 ++++- 5 files changed, 49 insertions(+), 5 deletions(-) diff --git a/locust/clients.py b/locust/clients.py index 8c5273f438..a51098ada0 100644 --- a/locust/clients.py +++ b/locust/clients.py @@ -108,6 +108,9 @@ def request(self, method, url, name=None, catch_response=False, context={}, **kw response = self._send_request_safe_mode(method, url, **kwargs) + if self.user and self.user.context(): + context.update(self.user.context()) + # store meta data that is used when reporting the request to locust's statistics request_meta = { "request_type": method, @@ -115,7 +118,6 @@ def request(self, method, url, name=None, catch_response=False, context={}, **kw "response_time": (time.monotonic() - start_time) * 1000, "name": name or (response.history and response.history[0] or response).request.path_url, "context": context, - "user": self.user, "exception": None, } diff --git a/locust/contrib/fasthttp.py b/locust/contrib/fasthttp.py index 4617c4d59e..91adf051d1 100644 --- a/locust/contrib/fasthttp.py +++ b/locust/contrib/fasthttp.py @@ -163,6 +163,9 @@ def request( start_time = default_timer() + if self.user and self.user.context(): + context.update(self.user.context()) + # store meta data that is used when reporting the request to locust's statistics request_meta = { "request_type": method, diff --git a/locust/test/test_fasthttp.py b/locust/test/test_fasthttp.py index 0aa8888a85..9e4c62681e 100644 --- a/locust/test/test_fasthttp.py +++ b/locust/test/test_fasthttp.py @@ -250,6 +250,23 @@ def test_is_abstract(self): self.assertTrue(FastHttpUser.abstract) self.assertFalse(is_user_class(FastHttpUser)) + def test_class_context(self): + class MyUser(FastHttpUser): + host = "http://127.0.0.1:%i" % self.port + + def context(self): + return {"user": self} + + kwargs = {} + + def on_request(**kw): + kwargs.update(kw) + + self.environment.events.request.add_listener(on_request) + user = MyUser(self.environment) + user.client.request("get", "/request_method") + self.assertDictContainsSubset({"user": user}, kwargs["context"]) + def test_get_request(self): self.response = "" diff --git a/locust/test/test_http.py b/locust/test/test_http.py index 84fa89ad90..062d781843 100644 --- a/locust/test/test_http.py +++ b/locust/test/test_http.py @@ -1,3 +1,4 @@ +from locust.user.users import HttpUser from requests.exceptions import InvalidSchema, InvalidURL, MissingSchema, RequestException @@ -117,7 +118,7 @@ def on_request(**kw): self.environment.events.request.add_listener(on_request) s.request("get", "/wrong_url", context={"foo": "bar"}) self.assertIn("/wrong_url", str(kwargs["exception"])) - self.assertDictEqual({"foo": "bar"}, kwargs["context"]) + self.assertDictContainsSubset({"foo": "bar"}, kwargs["context"]) def test_context_in_success(self): s = self.get_client() @@ -129,7 +130,7 @@ def on_request(exception, **kw): self.environment.events.request.add_listener(on_request) s.request("get", "/request_method", context={"foo": "bar"}) - self.assertDictEqual({"foo": "bar"}, kwargs["context"]) + self.assertDictContainsSubset({"foo": "bar"}, kwargs["context"]) def test_deprecated_request_events(self): s = self.get_client() @@ -159,7 +160,7 @@ def on_request(**kw): self.environment.events.request.add_listener(on_request) s.request("get", "/wrong_url/01", name="replaced_url_name", context={"foo": "bar"}) self.assertIn("for url: replaced_url_name", str(kwargs["exception"])) - self.assertDictEqual({"foo": "bar"}, kwargs["context"]) + self.assertDictContainsSubset({"foo": "bar"}, kwargs["context"]) def test_get_with_params(self): s = self.get_client() @@ -239,3 +240,21 @@ def test_catch_response_default_fail(self): pass self.assertEqual(1, self.environment.stats.total.num_requests) self.assertEqual(1, self.environment.stats.total.num_failures) + + def test_user_context(self): + class TestUser(HttpUser): + host = "http://localhost" + + def context(self): + return {"user": self} + + kwargs = {} + + def on_request(**kw): + kwargs.update(kw) + + self.environment.events.request.add_listener(on_request) + + user = TestUser(self.environment) + user.client.request("get", "/request_method") + self.assertDictContainsSubset({"user": user}, kwargs["context"]) diff --git a/locust/user/users.py b/locust/user/users.py index ac620816ea..b05a56170c 100644 --- a/locust/user/users.py +++ b/locust/user/users.py @@ -1,5 +1,5 @@ from locust.user.wait_time import constant -from typing import Any, Callable, List, TypeVar, Union +from typing import Any, Callable, Dict, List, TypeVar, Union from gevent import GreenletExit, greenlet from gevent.pool import Group from locust.clients import HttpSession @@ -185,6 +185,9 @@ def stop(self, force=False): self._state = LOCUST_STATE_STOPPING return False + def context(self) -> Dict: + return {} + class HttpUser(User): """ From 619f30582ff212e3bab6898d253a4fd08eb7a9a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dennis=20Krone=CC=81?= Date: Fri, 30 Apr 2021 14:46:43 +0200 Subject: [PATCH 18/21] Cleanup changes and update documentation --- docs/changelog.rst | 4 +-- docs/extending-locust.rst | 6 +++-- docs/testing-other-systems.rst | 5 ++-- .../custom_xmlrpc_client/xmlrpc_locustfile.py | 27 ++++++++++--------- locust/clients.py | 2 +- locust/contrib/fasthttp.py | 6 ++--- locust/event.py | 11 +++----- locust/test/test_fasthttp.py | 2 +- locust/test/test_http.py | 8 +++--- 9 files changed, 35 insertions(+), 36 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 8c63ae6aee..c38ec58e80 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -7,8 +7,8 @@ For full details of the Locust changelog, please see https://github.com/locustio 1.5.0 ===== -* Add new event called request. Is called on every request successful or not. request_success and request_failure are still available but can be considered deprecated -* Add parameter to the request event. Can be used to forward information when calling a request, things like user information, tags etc +* Add new event called request. Is called on every request successful or not. request_success and request_failure are still available but are deprecated +* Add parameter context to the request event. Can be used to forward information when calling a request, things like user information, tags etc 1.4.4 ===== diff --git a/docs/extending-locust.rst b/docs/extending-locust.rst index 56437b5e78..9d152dffdc 100644 --- a/docs/extending-locust.rst +++ b/docs/extending-locust.rst @@ -17,8 +17,10 @@ Here's an example on how to set up an event listener:: @events.request.add_listener def my_request_handler(request_type, name, response_time, response_length, context, exception, **kw): - if not exception: - print("Successfully made a request to: %s" % name) + if exception: + print(f"Request to {name} failed with exception {exception}") + else: + print(f"Successfully made a request to: {name}) .. note:: diff --git a/docs/testing-other-systems.rst b/docs/testing-other-systems.rst index 1cae723b5c..a5a646decd 100644 --- a/docs/testing-other-systems.rst +++ b/docs/testing-other-systems.rst @@ -6,8 +6,7 @@ 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 ` and -:py:attr:`request_failure ` events. +:py:attr:`request ` .. note:: @@ -32,7 +31,7 @@ using ``abstract = True`` which means that Locust will not try to create simulat The ``XmlRpcClient`` is a wrapper around the standard library's :py:class:`xmlrpc.client.ServerProxy`. It basically just proxies the function calls, but with the -important addition of firing :py:attr:`locust.event.Events.request_success` and :py:attr:`locust.event.Events.request_failure` +important addition of firing :py:attr:`locust.event.Events.request` 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: diff --git a/examples/custom_xmlrpc_client/xmlrpc_locustfile.py b/examples/custom_xmlrpc_client/xmlrpc_locustfile.py index 7385c12603..76ecd5b514 100644 --- a/examples/custom_xmlrpc_client/xmlrpc_locustfile.py +++ b/examples/custom_xmlrpc_client/xmlrpc_locustfile.py @@ -7,7 +7,7 @@ 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 + fires locust events on request, so that all requests gets tracked in locust's statistics. """ @@ -18,21 +18,22 @@ def __getattr__(self, name): def wrapper(*args, **kwargs): start_time = time.time() - exc = None + request_meta = { + "request_type": "xmlrpc", + "name": name, + "response_time": 0, + "response_length": 0, + "context": {}, + "exception": None, + } + try: result = func(*args, **kwargs) except Fault as e: - exc = e - - total_time = int((time.time() - start_time) * 1000) - self._locust_environment.events.request.fire( - request_type="xmlrpc", - name=name, - response_time=total_time, - response_length=0, - context={}, - exception=exc, - ) + request_meta["exception"] = e + + request_meta["response_time"] = int((time.time() - start_time) * 1000) + self._locust_environment.events.request.fire(**request_meta) # 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 diff --git a/locust/clients.py b/locust/clients.py index a51098ada0..58c8548a46 100644 --- a/locust/clients.py +++ b/locust/clients.py @@ -108,7 +108,7 @@ def request(self, method, url, name=None, catch_response=False, context={}, **kw response = self._send_request_safe_mode(method, url, **kwargs) - if self.user and self.user.context(): + if self.user: context.update(self.user.context()) # store meta data that is used when reporting the request to locust's statistics diff --git a/locust/contrib/fasthttp.py b/locust/contrib/fasthttp.py index 91adf051d1..c02d34d968 100644 --- a/locust/contrib/fasthttp.py +++ b/locust/contrib/fasthttp.py @@ -163,7 +163,7 @@ def request( start_time = default_timer() - if self.user and self.user.context(): + if self.user: context.update(self.user.context()) # store meta data that is used when reporting the request to locust's statistics @@ -416,7 +416,7 @@ def __init__(self, response, environment, request_meta): self.__dict__ = response.__dict__ self._cached_content = response.content # store reference to locust Environment - self.environment = environment + self._environment = environment self._request_meta = request_meta def __enter__(self): @@ -450,7 +450,7 @@ def __exit__(self, exc, value, traceback): return True def _report_request(self): - self.environment.events.request.fire(**self._request_meta) + self._environment.events.request.fire(**self._request_meta) def success(self): """ diff --git a/locust/event.py b/locust/event.py index 06c4b1ebb6..2972cab3c5 100644 --- a/locust/event.py +++ b/locust/event.py @@ -39,7 +39,7 @@ def fire(self, *, reverse=False, **kwargs): try: handler(**kwargs) except (StopUser, RescheduleTask, RescheduleTaskImmediately, InterruptTaskSet): - # These exceptions could be thrown by, for example, a request_failure handler, + # These exceptions could be thrown by, for example, a request handler, # in which case they are entirely appropriate and should not be caught raise except Exception: @@ -204,12 +204,9 @@ def __init__(self): if value == EventHook: setattr(self, name, value()) - setattr( - self, "request_success", DeprecatedEventHook("request_success event deprecated. Use the request event.") - ) - setattr( - self, "request_failure", DeprecatedEventHook("request_failure event deprecated. Use the request event.") - ) + self.request_success = DeprecatedEventHook("request_success event deprecated. Use the request event.") + + self.request_failure = DeprecatedEventHook("request_failure event deprecated. Use the request event.") def fire_deprecated_request_handlers( request_type, name, response_time, response_length, exception, context, **kwargs diff --git a/locust/test/test_fasthttp.py b/locust/test/test_fasthttp.py index 9e4c62681e..c413af9b45 100644 --- a/locust/test/test_fasthttp.py +++ b/locust/test/test_fasthttp.py @@ -265,7 +265,7 @@ def on_request(**kw): self.environment.events.request.add_listener(on_request) user = MyUser(self.environment) user.client.request("get", "/request_method") - self.assertDictContainsSubset({"user": user}, kwargs["context"]) + self.assertDictEqual({"user": user}, kwargs["context"]) def test_get_request(self): self.response = "" diff --git a/locust/test/test_http.py b/locust/test/test_http.py index 062d781843..74652fd6e0 100644 --- a/locust/test/test_http.py +++ b/locust/test/test_http.py @@ -118,7 +118,7 @@ def on_request(**kw): self.environment.events.request.add_listener(on_request) s.request("get", "/wrong_url", context={"foo": "bar"}) self.assertIn("/wrong_url", str(kwargs["exception"])) - self.assertDictContainsSubset({"foo": "bar"}, kwargs["context"]) + self.assertDictEqual({"foo": "bar"}, kwargs["context"]) def test_context_in_success(self): s = self.get_client() @@ -130,7 +130,7 @@ def on_request(exception, **kw): self.environment.events.request.add_listener(on_request) s.request("get", "/request_method", context={"foo": "bar"}) - self.assertDictContainsSubset({"foo": "bar"}, kwargs["context"]) + self.assertDictEqual({"foo": "bar"}, kwargs["context"]) def test_deprecated_request_events(self): s = self.get_client() @@ -160,7 +160,7 @@ def on_request(**kw): self.environment.events.request.add_listener(on_request) s.request("get", "/wrong_url/01", name="replaced_url_name", context={"foo": "bar"}) self.assertIn("for url: replaced_url_name", str(kwargs["exception"])) - self.assertDictContainsSubset({"foo": "bar"}, kwargs["context"]) + self.assertDictEqual({"foo": "bar"}, kwargs["context"]) def test_get_with_params(self): s = self.get_client() @@ -257,4 +257,4 @@ def on_request(**kw): user = TestUser(self.environment) user.client.request("get", "/request_method") - self.assertDictContainsSubset({"user": user}, kwargs["context"]) + self.assertDictEqual({"user": user}, kwargs["context"]) From 26f71972a2df205b102b35ad18950295775aeb23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dennis=20Krone=CC=81?= Date: Mon, 3 May 2021 15:47:37 +0200 Subject: [PATCH 19/21] Avoid overwriting default parameter value. --- locust/clients.py | 2 +- locust/contrib/fasthttp.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/locust/clients.py b/locust/clients.py index 58c8548a46..184ff8cea6 100644 --- a/locust/clients.py +++ b/locust/clients.py @@ -109,7 +109,7 @@ def request(self, method, url, name=None, catch_response=False, context={}, **kw response = self._send_request_safe_mode(method, url, **kwargs) if self.user: - context.update(self.user.context()) + context = {**context, **self.user.context()} # store meta data that is used when reporting the request to locust's statistics request_meta = { diff --git a/locust/contrib/fasthttp.py b/locust/contrib/fasthttp.py index c02d34d968..201fb940c9 100644 --- a/locust/contrib/fasthttp.py +++ b/locust/contrib/fasthttp.py @@ -164,7 +164,7 @@ def request( start_time = default_timer() if self.user: - context.update(self.user.context()) + context = {**context, **self.user.context()} # store meta data that is used when reporting the request to locust's statistics request_meta = { From 74d89a3f52182de6f807edc3f5df40f0fa06ce66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dennis=20Krone=CC=81?= Date: Mon, 3 May 2021 15:48:04 +0200 Subject: [PATCH 20/21] Update documentation regarding request context --- docs/api.rst | 2 +- docs/extending-locust.rst | 36 ++++++++++++++++++- docs/testing-other-systems.rst | 2 +- .../custom_xmlrpc_client/xmlrpc_locustfile.py | 2 +- locust/event.py | 6 ++-- locust/user/users.py | 6 ++++ 6 files changed, 47 insertions(+), 7 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index 22f616fcc6..6dc0fce99c 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -7,7 +7,7 @@ User class ============ .. autoclass:: locust.User - :members: wait_time, tasks, weight, abstract, on_start, on_stop, wait + :members: wait_time, tasks, weight, abstract, on_start, on_stop, wait, context HttpUser class ================ diff --git a/docs/extending-locust.rst b/docs/extending-locust.rst index 9d152dffdc..5d879d59a4 100644 --- a/docs/extending-locust.rst +++ b/docs/extending-locust.rst @@ -29,12 +29,46 @@ Here's an example on how to set up an event listener:: (the \**kw in the code above), to prevent your code from breaking if new arguments are added in a future version. + +Request context +================== + +By using the context parameter in the request method information you can attach data that will be forwarded by the +request event. This should be a dictionary and can be set directly when calling request() or on a class level +by overwriting the User.context() method. + +Context from request method:: + + class MyUser(HttpUser): + @task + def t(self): + self.client.post("/login", json={"username": "foo"}, context={"username": "foo"}) + + @events.request.add_listener + def on_request(context, **kwargs): + print(context["username"]) + +Context from User class:: + + class MyUser(HttpUser): + def context(self): + return {"username": self.username} + + @task + def t(self): + self.username = "foo" + self.client.post("/login", json={"username": self.username}) + + @events.request.add_listener + def on_request(self, context, **kwargs): + print(context["username"]) + + .. seealso:: To see all available events, please see :ref:`events`. - Adding Web Routes ================== diff --git a/docs/testing-other-systems.rst b/docs/testing-other-systems.rst index a5a646decd..5634cb7608 100644 --- a/docs/testing-other-systems.rst +++ b/docs/testing-other-systems.rst @@ -32,7 +32,7 @@ using ``abstract = True`` which means that Locust will not try to create simulat The ``XmlRpcClient`` is a wrapper around the standard library's :py:class:`xmlrpc.client.ServerProxy`. It basically just proxies the function calls, but with the important addition of firing :py:attr:`locust.event.Events.request` -events, which will record all calls in Locust's statistics. +event, 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: diff --git a/examples/custom_xmlrpc_client/xmlrpc_locustfile.py b/examples/custom_xmlrpc_client/xmlrpc_locustfile.py index 76ecd5b514..347346a0c1 100644 --- a/examples/custom_xmlrpc_client/xmlrpc_locustfile.py +++ b/examples/custom_xmlrpc_client/xmlrpc_locustfile.py @@ -8,7 +8,7 @@ class XmlRpcClient(ServerProxy): """ Simple, sample XML RPC client implementation that wraps xmlrpclib.ServerProxy and fires locust events on request, so that all requests - gets tracked in locust's statistics. + get tracked in locust's statistics. """ _locust_environment = None diff --git a/locust/event.py b/locust/event.py index 2972cab3c5..5c8e139346 100644 --- a/locust/event.py +++ b/locust/event.py @@ -74,8 +74,8 @@ class Events: request_success: DeprecatedEventHook """ - Fired when a request is completed successfully. This event is typically used to report requests - when writing custom clients for locust. + DEPRECATED. Fired when a request is completed successfully. This event is typically used to report requests + when writing custom clients for locust. Event arguments: @@ -87,7 +87,7 @@ class Events: request_failure: DeprecatedEventHook """ - Fired when a request fails. This event is typically used to report failed requests when writing + DEPRECATED. Fired when a request fails. This event is typically used to report failed requests when writing custom clients for locust. Event arguments: diff --git a/locust/user/users.py b/locust/user/users.py index b05a56170c..7fe5f30e03 100644 --- a/locust/user/users.py +++ b/locust/user/users.py @@ -186,6 +186,12 @@ def stop(self, force=False): return False def context(self) -> Dict: + """ + Returns user specific context. Override this method to customize data to be forwarded in request event. + + :return: Context data + :rtype: Dict + """ return {} From 2e23289ab874971a5cbc9b948bee53448cd4d11a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dennis=20Krone=CC=81?= Date: Mon, 3 May 2021 16:11:52 +0200 Subject: [PATCH 21/21] Remove trailing whitespace --- locust/event.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locust/event.py b/locust/event.py index 5c8e139346..37a6b74e4f 100644 --- a/locust/event.py +++ b/locust/event.py @@ -75,7 +75,7 @@ class Events: request_success: DeprecatedEventHook """ DEPRECATED. Fired when a request is completed successfully. This event is typically used to report requests - when writing custom clients for locust. + when writing custom clients for locust. Event arguments: