diff --git a/locust/clients.py b/locust/clients.py index 460ff6ad2b..20c50d829d 100644 --- a/locust/clients.py +++ b/locust/clients.py @@ -129,6 +129,9 @@ 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) + parsed_url = urlparse(url) + # absolute URLs may use a different host + actual_host = f"{parsed_url.scheme}://{parsed_url.netloc}" start_time = time.time() start_perf_counter = time.perf_counter() @@ -154,6 +157,7 @@ def request(self, method, url, name=None, catch_response=False, context={}, **kw "exception": None, "start_time": start_time, "url": url, + "host": actual_host, } # get the length of the content, but if the argument stream is set to True, we take diff --git a/locust/contrib/fasthttp.py b/locust/contrib/fasthttp.py index 4f0720e2a4..2606ec6184 100644 --- a/locust/contrib/fasthttp.py +++ b/locust/contrib/fasthttp.py @@ -188,6 +188,9 @@ def request( """ # prepend url with hostname unless it's already an absolute URL built_url = self._build_url(url) + parsed_url = urlparse(built_url) + # absolute URLs may use a different host + actual_host = f"{parsed_url.scheme}://{parsed_url.netloc}" start_time = time.time() # seconds since epoch @@ -224,6 +227,7 @@ def request( "exception": None, "start_time": start_time, "url": built_url, # this is a small deviation from HttpSession, which gets the final (possibly redirected) URL + "host": actual_host, } if not allow_redirects: diff --git a/locust/html.py b/locust/html.py index d2559124c9..50a68c1a75 100644 --- a/locust/html.py +++ b/locust/html.py @@ -21,12 +21,7 @@ def render_template(file, template_path, **kwargs): return template.render(**kwargs) -def get_html_report( - environment, - show_download_link=True, - use_modern_ui=False, - theme="", -): +def get_html_report(environment, show_download_link=True, use_modern_ui=False, theme="", **kwargs): root_path = os.path.dirname(os.path.abspath(__file__)) if use_modern_ui: static_path = os.path.join(root_path, "webui", "dist", "assets") @@ -53,6 +48,8 @@ def get_html_report( all_hosts = {l.host for l in environment.runner.user_classes} if len(all_hosts) == 1: host = list(all_hosts)[0] + elif use_modern_ui: + host = list(all_hosts) requests_statistics = list(chain(sort_stats(stats.entries), [stats.total])) failures_statistics = sort_stats(stats.errors) @@ -111,6 +108,7 @@ def get_html_report( { "name": escape(stat.name), "method": escape(stat.method or ""), + "host": escape(stat.host or ""), **{ str(percentile): stat.get_response_time_percentile(percentile) for percentile in PERCENTILES_FOR_HTML_REPORT @@ -120,13 +118,17 @@ def get_html_report( ], "start_time": start_time, "end_time": end_time, - "host": escape(str(host)), + "has_multiple_hosts": isinstance(host, list) + and len(host) > 1 + and any(stat.host for stat in requests_statistics), + "host": escape(host) if isinstance(host, str) else [escape(h) for h in host], "history": history, "show_download_link": show_download_link, "locustfile": escape(str(environment.locustfile)), "tasks": task_data, "percentile1": stats_module.PERCENTILES_TO_CHART[0], "percentile2": stats_module.PERCENTILES_TO_CHART[1], + **kwargs, }, theme=theme, static_js="\n".join(static_js), diff --git a/locust/runners.py b/locust/runners.py index efd63e1f00..9df458ddef 100644 --- a/locust/runners.py +++ b/locust/runners.py @@ -124,10 +124,10 @@ def __init__(self, environment: "Environment") -> None: self._users_dispatcher: Optional[UsersDispatcher] = None # set up event listeners for recording requests - def on_request(request_type, name, response_time, response_length, exception=None, **_kwargs): - self.stats.log_request(request_type, name, response_time, response_length) + def on_request(request_type, name, host, response_time, response_length, exception=None, **_kwargs): + self.stats.log_request(request_type, name, host, response_time, response_length) if exception: - self.stats.log_error(request_type, name, exception) + self.stats.log_error(request_type, name, host, exception) self.environment.events.request.add_listener(on_request) diff --git a/locust/stats.py b/locust/stats.py index 4cbfed338c..33a058797e 100644 --- a/locust/stats.py +++ b/locust/stats.py @@ -63,6 +63,7 @@ def writerow(self, columns: Iterable[str | int | float]) -> None: class StatsBaseDict(TypedDict): name: str method: str + host: str class StatsEntryDict(StatsBaseDict): @@ -88,6 +89,7 @@ class StatsErrorDict(StatsBaseDict): class StatsHolder(Protocol): name: str method: str + host: str S = TypeVar("S", bound=StatsHolder) @@ -193,7 +195,11 @@ def __init__(self, request_stats): def __missing__(self, key): self[key] = StatsEntry( - self.request_stats, key[0], key[1], use_response_times_cache=self.request_stats.use_response_times_cache + self.request_stats, + key[0], + key[1], + key[2], + use_response_times_cache=self.request_stats.use_response_times_cache, ) return self[key] @@ -211,7 +217,7 @@ def __init__(self, use_response_times_cache=True): is not needed. """ self.use_response_times_cache = use_response_times_cache - self.entries: Dict[Tuple[str, str], StatsEntry] = EntriesDict(self) + self.entries: Dict[Tuple[str, str, str], StatsEntry] = EntriesDict(self) self.errors: Dict[str, StatsError] = {} self.total = StatsEntry(self, "Aggregated", None, use_response_times_cache=self.use_response_times_cache) self.history = [] @@ -236,27 +242,27 @@ def last_request_timestamp(self): def start_time(self): return self.total.start_time - def log_request(self, method: str, name: str, response_time: int, content_length: int) -> None: + def log_request(self, method: str, name: str, host: str, response_time: int, content_length: int) -> None: self.total.log(response_time, content_length) - self.entries[(name, method)].log(response_time, content_length) + self.entries[(name, method, host)].log(response_time, content_length) - def log_error(self, method: str, name: str, error: Exception | str | None) -> None: + def log_error(self, method: str, name: str, host: str, error: Exception | str | None) -> None: self.total.log_error(error) - self.entries[(name, method)].log_error(error) + self.entries[(name, method, host)].log_error(error) # store error in errors dict - key = StatsError.create_key(method, name, error) + key = StatsError.create_key(method, name, host, error) entry = self.errors.get(key) if not entry: - entry = StatsError(method, name, error) + entry = StatsError(method, name, host, error) self.errors[key] = entry entry.occurred() - def get(self, name: str, method: str) -> "StatsEntry": + def get(self, name: str, method: str, host: str) -> "StatsEntry": """ - Retrieve a StatsEntry instance by name and method + Retrieve a StatsEntry instance by name, method, and host """ - return self.entries[(name, method)] + return self.entries[(name, method, host)] def reset_all(self) -> None: """ @@ -291,12 +297,21 @@ class StatsEntry: Represents a single stats entry (name and method) """ - def __init__(self, stats: Optional[RequestStats], name: str, method: str, use_response_times_cache: bool = False): + def __init__( + self, + stats: Optional[RequestStats], + name: str, + method: str, + host: str = "", + use_response_times_cache: bool = False, + ): self.stats = stats self.name = name """ Name (URL) of this stats entry """ self.method = method """ Method (GET, POST, PUT, etc.) """ + self.host = host + """ Host (BASE_URL) of the request """ self.use_response_times_cache = use_response_times_cache """ If set to True, the copy of the response_time dict will be stored in response_times_cache @@ -546,11 +561,11 @@ def serialize(self) -> StatsEntryDict: @classmethod def unserialize(cls, data: StatsEntryDict) -> "StatsEntry": """Return the unserialzed version of the specified dict""" - obj = cls(None, data["name"], data["method"]) + obj = cls(None, data["name"], data["method"], data.get("host", "")) valid_keys = StatsEntryDict.__annotations__.keys() for key, value in data.items(): - if key in ["name", "method"] or key not in valid_keys: + if key in ["name", "method", "host"] or key not in valid_keys: continue setattr(obj, key, value) @@ -699,13 +714,15 @@ def to_dict(self, escape_string_values=False): "ninetieth_response_time": self.get_response_time_percentile(0.9), "ninety_ninth_response_time": self.get_response_time_percentile(0.99), "avg_content_length": self.avg_content_length, + "host": self.host, } class StatsError: - def __init__(self, method: str, name: str, error: Exception | str | None, occurrences: int = 0): + def __init__(self, method: str, name: str, host: str, error: Exception | str | None, occurrences: int = 0): self.method = method self.name = name + self.host = host self.error = error self.occurrences = occurrences @@ -724,8 +741,8 @@ def parse_error(cls, error: Exception | str | None) -> str: return string_error.replace(hex_address, "0x....") @classmethod - def create_key(cls, method: str, name: str, error: Exception | str | None) -> str: - key = f"{method}.{name}.{StatsError.parse_error(error)!r}" + def create_key(cls, method: str, name: str, host: str, error: Exception | str | None) -> str: + key = f"{method}.{name}.{host}.{StatsError.parse_error(error)!r}" return hashlib.sha256(key.encode("utf-8")).hexdigest() def occurred(self) -> None: @@ -759,7 +776,7 @@ def _getattr(obj: "StatsError", key: str, default: Optional[Any]) -> Optional[An @classmethod def unserialize(cls, data: StatsErrorDict) -> "StatsError": - return cls(data["method"], data["name"], data["error"], data["occurrences"]) + return cls(data["method"], data["name"], data.get("host", ""), data["error"], data["occurrences"]) def to_dict(self, escape_string_values=False): return { @@ -767,6 +784,7 @@ def to_dict(self, escape_string_values=False): "name": escape(self.name), "error": escape(self.parse_error(self.error)), "occurrences": self.occurrences, + "host": self.host, } @@ -798,9 +816,11 @@ def on_report_to_master(client_id: str, data: Dict[str, Any]) -> None: def on_worker_report(client_id: str, data: Dict[str, Any]) -> None: for stats_data in data["stats"]: entry = StatsEntry.unserialize(stats_data) - request_key = (entry.name, entry.method) + request_key = (entry.name, entry.method, entry.host) if request_key not in stats.entries: - stats.entries[request_key] = StatsEntry(stats, entry.name, entry.method, use_response_times_cache=True) + stats.entries[request_key] = StatsEntry( + stats, entry.name, entry.method, entry.host, use_response_times_cache=True + ) stats.entries[request_key].extend(entry) for error_key, error in data["errors"].items(): @@ -910,6 +930,13 @@ def sort_stats(stats: Dict[Any, S]) -> List[S]: return [stats[key] for key in sorted(stats.keys())] +def group_stats_by(group_by: str, stats: List[S]) -> Dict[str, List[S]]: + return { + group_name: [stat for stat in stats if getattr(stat, group_by) == group_name] + for group_name in set(getattr(stat, group_by) for stat in stats) + } + + def stats_history(runner: "Runner") -> None: """Save current stats info to history for charts of report.""" while True: @@ -976,40 +1003,64 @@ def _percentile_fields(self, stats_entry: StatsEntry, use_current: bool = False) else: return [int(stats_entry.get_response_time_percentile(x) or 0) for x in self.percentiles_to_report] - def requests_csv(self, csv_writer: CSVWriter) -> None: + def requests_csv(self, csv_writer: CSVWriter, group_by: Optional[str] = "") -> None: """Write requests csv with header and data rows.""" - csv_writer.writerow(self.requests_csv_columns) - self._requests_data_rows(csv_writer) + sorted_stats = sort_stats(self.environment.stats.entries) + if group_by and any(getattr(stat, group_by) for stat in sorted_stats): + grouped_stats = group_stats_by(group_by, sorted_stats) + + for group_name, stats in grouped_stats.items(): + csv_writer.writerow([group_name]) + csv_writer.writerow(self.requests_csv_columns) + self._requests_data_rows(csv_writer, stats) + csv_writer.writerow([]) + else: + csv_writer.writerow(self.requests_csv_columns) + self._requests_data_rows(csv_writer, chain(sorted_stats, [self.environment.stats.total])) - def _requests_data_rows(self, csv_writer: CSVWriter) -> None: + def _requests_data_rows(self, csv_writer: CSVWriter, stats_entries=None) -> None: """Write requests csv data row, excluding header.""" - stats = self.environment.stats - for stats_entry in chain(sort_stats(stats.entries), [stats.total]): - csv_writer.writerow( - chain( - [ - stats_entry.method, - stats_entry.name, - stats_entry.num_requests, - stats_entry.num_failures, - stats_entry.median_response_time, - stats_entry.avg_response_time, - stats_entry.min_response_time or 0, - stats_entry.max_response_time, - stats_entry.avg_content_length, - stats_entry.total_rps, - stats_entry.total_fail_per_sec, - ], - self._percentile_fields(stats_entry), - ) - ) + stats_entries = ( + stats_entries + if stats_entries + else chain(sort_stats(self.environment.stats.entries), [self.environment.stats.total]) + ) + + for stats_entry in stats_entries: + rows = [ + stats_entry.method, + stats_entry.name, + stats_entry.num_requests, + stats_entry.num_failures, + stats_entry.median_response_time, + stats_entry.avg_response_time, + stats_entry.min_response_time or 0, + stats_entry.max_response_time, + stats_entry.avg_content_length, + stats_entry.total_rps, + stats_entry.total_fail_per_sec, + ] + + csv_writer.writerow(chain(rows, self._percentile_fields(stats_entry))) + + def failures_csv(self, csv_writer: CSVWriter, group_by: Optional[str] = "") -> None: + sorted_errors = sort_stats(self.environment.stats.errors) + if group_by and any(getattr(error, group_by) for error in sorted_errors): + grouped_errors = group_stats_by(group_by, sorted_errors) + + for group_name, failures in grouped_errors.items(): + csv_writer.writerow([group_name]) + csv_writer.writerow(self.failures_columns) + self._failures_data_rows(csv_writer, failures) + csv_writer.writerow([]) + else: + csv_writer.writerow(self.failures_columns) + self._failures_data_rows(csv_writer, sorted_errors) - def failures_csv(self, csv_writer: CSVWriter) -> None: - csv_writer.writerow(self.failures_columns) - self._failures_data_rows(csv_writer) + def _failures_data_rows(self, csv_writer: CSVWriter, failures_entries=None) -> None: + failures_entries = failures_entries if failures_entries else sort_stats(self.environment.stats.errors) - def _failures_data_rows(self, csv_writer: CSVWriter) -> None: - for stats_error in sort_stats(self.environment.stats.errors): + for stats_error in failures_entries: csv_writer.writerow( [ stats_error.method, diff --git a/locust/test/test_fasthttp.py b/locust/test/test_fasthttp.py index ed5ab9138e..fe8911cf9f 100644 --- a/locust/test/test_fasthttp.py +++ b/locust/test/test_fasthttp.py @@ -40,14 +40,14 @@ def test_404(self): 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) + self.assertEqual(1, self.runner.stats.get("/does_not_exist", "GET", s.base_url).num_failures) def test_204(self): 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) - self.assertEqual(0, self.runner.stats.get("/status/204", "GET").num_failures) + self.assertEqual(1, self.runner.stats.get("/status/204", "GET", s.base_url).num_requests) + self.assertEqual(0, self.runner.stats.get("/status/204", "GET", s.base_url).num_failures) self.assertEqual(r.url, "http://127.0.0.1:%i/status/204" % self.port) self.assertEqual(r.request.url, r.url) @@ -59,13 +59,15 @@ def test_streaming_response(self): r = s.get("/streaming/30") # verify that the time reported includes the download time of the whole streamed response - self.assertGreater(self.runner.stats.get("/streaming/30", method="GET").avg_response_time, 250) + self.assertGreater(self.runner.stats.get("/streaming/30", method="GET", host=s.base_url).avg_response_time, 250) self.runner.stats.clear_all() # verify that response time does NOT include whole download time, when using stream=True r = s.get("/streaming/30", stream=True) - self.assertGreaterEqual(self.runner.stats.get("/streaming/30", method="GET").avg_response_time, 0) - self.assertLess(self.runner.stats.get("/streaming/30", method="GET").avg_response_time, 250) + self.assertGreaterEqual( + self.runner.stats.get("/streaming/30", method="GET", host=s.base_url).avg_response_time, 0 + ) + self.assertLess(self.runner.stats.get("/streaming/30", method="GET", host=s.base_url).avg_response_time, 250) # download the content of the streaming response (so we don't get an ugly exception in the log) _ = r.content @@ -74,7 +76,7 @@ def test_slow_redirect(self): s = self.get_client() url = "/redirect?url=/redirect&delay=0.5" r = s.get(url) - stats = self.runner.stats.get(url, method="GET") + stats = self.runner.stats.get(url, method="GET", host=s.base_url) self.assertEqual(1, stats.num_requests) self.assertGreater(stats.avg_response_time, 500) @@ -83,8 +85,8 @@ def test_post_redirect(self): url = "/redirect" r = s.post(url) self.assertEqual(200, r.status_code) - post_stats = self.runner.stats.get(url, method="POST") - get_stats = self.runner.stats.get(url, method="GET") + post_stats = self.runner.stats.get(url, method="POST", host=s.base_url) + get_stats = self.runner.stats.get(url, method="GET", host=s.base_url) self.assertEqual(1, post_stats.num_requests) self.assertEqual(0, get_stats.num_requests) @@ -135,8 +137,8 @@ def test_catch_response_fail_successful_request(self): s = self.get_client() with s.get("/ultra_fast", catch_response=True) as r: r.failure("nope") - self.assertEqual(1, self.environment.stats.get("/ultra_fast", "GET").num_requests) - self.assertEqual(1, self.environment.stats.get("/ultra_fast", "GET").num_failures) + self.assertEqual(1, self.environment.stats.get("/ultra_fast", "GET", s.base_url).num_requests) + self.assertEqual(1, self.environment.stats.get("/ultra_fast", "GET", s.base_url).num_failures) def test_catch_response_pass_failed_request(self): s = self.get_client() @@ -176,8 +178,8 @@ def test_catch_response_default_success(self): s = self.get_client() with s.get("/ultra_fast", catch_response=True) as r: pass - self.assertEqual(1, self.environment.stats.get("/ultra_fast", "GET").num_requests) - self.assertEqual(0, self.environment.stats.get("/ultra_fast", "GET").num_failures) + self.assertEqual(1, self.environment.stats.get("/ultra_fast", "GET", s.base_url).num_requests) + self.assertEqual(0, self.environment.stats.get("/ultra_fast", "GET", s.base_url).num_failures) def test_catch_response_default_fail(self): s = self.get_client() @@ -265,11 +267,13 @@ class MyUser(FastHttpUser): locust = MyUser(self.environment) locust.client.get("/ultra_fast") self.assertEqual( - self.runner.stats.get("/ultra_fast", "GET").avg_content_length, len("This is an ultra fast response") + self.runner.stats.get("/ultra_fast", "GET", locust.host).avg_content_length, + len("This is an ultra fast response"), ) locust.client.get("/ultra_fast") self.assertEqual( - self.runner.stats.get("/ultra_fast", "GET").avg_content_length, len("This is an ultra fast response") + self.runner.stats.get("/ultra_fast", "GET", locust.host).avg_content_length, + len("This is an ultra fast response"), ) def test_request_stats_no_content_length(self): @@ -280,7 +284,7 @@ class MyUser(FastHttpUser): path = "/no_content_length" r = l.client.get(path) self.assertEqual( - self.runner.stats.get(path, "GET").avg_content_length, + self.runner.stats.get(path, "GET", l.host).avg_content_length, len("This response does not have content-length in the header"), ) @@ -291,7 +295,7 @@ class MyUser(FastHttpUser): l = MyUser(self.environment) path = "/no_content_length" r = l.client.get(path, stream=True) - self.assertEqual(0, self.runner.stats.get(path, "GET").avg_content_length) + self.assertEqual(0, self.runner.stats.get(path, "GET", l.host).avg_content_length) def test_request_stats_named_endpoint(self): class MyUser(FastHttpUser): @@ -299,7 +303,7 @@ class MyUser(FastHttpUser): locust = MyUser(self.environment) locust.client.get("/ultra_fast", name="my_custom_name") - self.assertEqual(1, self.runner.stats.get("my_custom_name", "GET").num_requests) + self.assertEqual(1, self.runner.stats.get("my_custom_name", "GET", locust.host).num_requests) def test_request_stats_query_variables(self): class MyUser(FastHttpUser): @@ -307,7 +311,7 @@ class MyUser(FastHttpUser): locust = MyUser(self.environment) locust.client.get("/ultra_fast?query=1") - self.assertEqual(1, self.runner.stats.get("/ultra_fast?query=1", "GET").num_requests) + self.assertEqual(1, self.runner.stats.get("/ultra_fast?query=1", "GET", locust.host).num_requests) def test_request_stats_put(self): class MyUser(FastHttpUser): @@ -315,7 +319,7 @@ class MyUser(FastHttpUser): locust = MyUser(self.environment) locust.client.put("/put") - self.assertEqual(1, self.runner.stats.get("/put", "PUT").num_requests) + self.assertEqual(1, self.runner.stats.get("/put", "PUT", locust.host).num_requests) def test_request_connection_error(self): class MyUser(FastHttpUser): @@ -324,8 +328,8 @@ class MyUser(FastHttpUser): locust = MyUser(self.environment) response = locust.client.get("/") self.assertEqual(response.status_code, 0) - self.assertEqual(1, self.runner.stats.get("/", "GET").num_failures) - self.assertEqual(1, self.runner.stats.get("/", "GET").num_requests) + self.assertEqual(1, self.runner.stats.get("/", "GET", locust.host).num_failures) + self.assertEqual(1, self.runner.stats.get("/", "GET", locust.host).num_requests) class TestFastHttpUserClass(WebserverTestCase): @@ -447,8 +451,8 @@ def t1(l): my_locust = MyUser(self.environment) my_locust.t1() - self.assertEqual(1, self.runner.stats.get("new name!", "GET").num_requests) - self.assertEqual(0, self.runner.stats.get("/ultra_fast", "GET").num_requests) + self.assertEqual(1, self.runner.stats.get("new name!", "GET", my_locust.host).num_requests) + self.assertEqual(0, self.runner.stats.get("/ultra_fast", "GET", my_locust.host).num_requests) def test_redirect_url_original_path_as_name(self): class MyUser(FastHttpUser): @@ -458,8 +462,8 @@ class MyUser(FastHttpUser): l.client.get("/redirect") self.assertEqual(1, len(self.runner.stats.entries)) - self.assertEqual(1, self.runner.stats.get("/redirect", "GET").num_requests) - self.assertEqual(0, self.runner.stats.get("/ultra_fast", "GET").num_requests) + self.assertEqual(1, self.runner.stats.get("/redirect", "GET", l.host).num_requests) + self.assertEqual(0, self.runner.stats.get("/ultra_fast", "GET", l.host).num_requests) def test_network_timeout_setting(self): class MyUser(FastHttpUser): @@ -479,7 +483,7 @@ class MyUser(FastHttpUser): timeout.cancel() self.assertTrue(isinstance(r.error.original, socket.timeout)) - self.assertEqual(1, self.runner.stats.get("/redirect?url=/redirect&delay=5.0", "GET").num_failures) + self.assertEqual(1, self.runner.stats.get("/redirect?url=/redirect&delay=5.0", "GET", l.host).num_failures) def test_max_redirect_setting(self): class MyUser(FastHttpUser): @@ -488,7 +492,7 @@ class MyUser(FastHttpUser): l = MyUser(self.environment) l.client.get("/redirect") - self.assertEqual(1, self.runner.stats.get("/redirect", "GET").num_failures) + self.assertEqual(1, self.runner.stats.get("/redirect", "GET", l.host).num_failures) def test_allow_redirects_override(self): class MyLocust(FastHttpUser): @@ -504,7 +508,7 @@ def test_slow_redirect(self): 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") + stats = self.runner.stats.get(url, method="GET", host=s.base_url) self.assertEqual(1, stats.num_requests) self.assertGreater(stats.avg_response_time, 500) diff --git a/locust/test/test_http.py b/locust/test/test_http.py index 0cf7d71728..70f612e2ba 100644 --- a/locust/test/test_http.py +++ b/locust/test/test_http.py @@ -50,13 +50,13 @@ def test_streaming_response(self): r = s.get("/streaming/30") # verify that the time reported includes the download time of the whole streamed response - self.assertGreater(self.runner.stats.get("/streaming/30", method="GET").avg_response_time, 250) + self.assertGreater(self.runner.stats.get("/streaming/30", method="GET", host=s.base_url).avg_response_time, 250) self.runner.stats.clear_all() # verify that response time does NOT include whole download time, when using stream=True r = s.get("/streaming/30", stream=True) - self.assertGreater(self.runner.stats.get("/streaming/30", method="GET").avg_response_time, 0) - self.assertLess(self.runner.stats.get("/streaming/30", method="GET").avg_response_time, 250) + self.assertGreater(self.runner.stats.get("/streaming/30", method="GET", host=s.base_url).avg_response_time, 0) + self.assertLess(self.runner.stats.get("/streaming/30", method="GET", host=s.base_url).avg_response_time, 250) # download the content of the streaming response (so we don't get an ugly exception in the log) _ = r.content @@ -65,7 +65,7 @@ def test_slow_redirect(self): s = self.get_client() url = "/redirect?url=/redirect&delay=0.5" r = s.get(url) - stats = self.runner.stats.get(url, method="GET") + stats = self.runner.stats.get(url, method="GET", host=s.base_url) self.assertEqual(1, stats.num_requests) self.assertGreater(stats.avg_response_time, 500) @@ -74,8 +74,8 @@ def test_post_redirect(self): url = "/redirect" r = s.post(url) self.assertEqual(200, r.status_code) - post_stats = self.runner.stats.get(url, method="POST") - get_stats = self.runner.stats.get(url, method="GET") + post_stats = self.runner.stats.get(url, method="POST", host=s.base_url) + get_stats = self.runner.stats.get(url, method="GET", host=s.base_url) self.assertEqual(1, post_stats.num_requests) self.assertEqual(0, get_stats.num_requests) @@ -173,15 +173,15 @@ def test_catch_response_fail_successful_request(self): s = self.get_client() with s.get("/ultra_fast", catch_response=True) as r: r.failure("nope") - self.assertEqual(1, self.environment.stats.get("/ultra_fast", "GET").num_requests) - self.assertEqual(1, self.environment.stats.get("/ultra_fast", "GET").num_failures) + self.assertEqual(1, self.environment.stats.get("/ultra_fast", "GET", s.base_url).num_requests) + self.assertEqual(1, self.environment.stats.get("/ultra_fast", "GET", s.base_url).num_failures) def test_catch_response_fail_successful_request_with_non_string_error_message(self): s = self.get_client() with s.get("/ultra_fast", catch_response=True) as r: r.failure({"other types are also wrapped as exceptions": True}) - self.assertEqual(1, self.environment.stats.get("/ultra_fast", "GET").num_requests) - self.assertEqual(1, self.environment.stats.get("/ultra_fast", "GET").num_failures) + self.assertEqual(1, self.environment.stats.get("/ultra_fast", "GET", s.base_url).num_requests) + self.assertEqual(1, self.environment.stats.get("/ultra_fast", "GET", s.base_url).num_failures) def test_catch_response_pass_failed_request(self): s = self.get_client() @@ -239,8 +239,8 @@ def test_catch_response_default_success(self): s = self.get_client() with s.get("/ultra_fast", catch_response=True) as r: pass - self.assertEqual(1, self.environment.stats.get("/ultra_fast", "GET").num_requests) - self.assertEqual(0, self.environment.stats.get("/ultra_fast", "GET").num_failures) + self.assertEqual(1, self.environment.stats.get("/ultra_fast", "GET", s.base_url).num_requests) + self.assertEqual(0, self.environment.stats.get("/ultra_fast", "GET", s.base_url).num_failures) def test_catch_response_default_fail(self): s = self.get_client() diff --git a/locust/test/test_locust_class.py b/locust/test/test_locust_class.py index 78a42b1306..f4939fba83 100644 --- a/locust/test/test_locust_class.py +++ b/locust/test/test_locust_class.py @@ -701,8 +701,8 @@ def t1(l): my_locust = MyUser(self.environment) my_locust.t1() - self.assertEqual(1, self.runner.stats.get("new name!", "GET").num_requests) - self.assertEqual(0, self.runner.stats.get("/ultra_fast", "GET").num_requests) + self.assertEqual(1, self.runner.stats.get("new name!", "GET", my_locust.host).num_requests) + self.assertEqual(0, self.runner.stats.get("/ultra_fast", "GET", my_locust.host).num_requests) def test_redirect_url_original_path_as_name(self): class MyUser(HttpUser): @@ -712,8 +712,8 @@ class MyUser(HttpUser): l.client.get("/redirect") self.assertEqual(1, len(self.runner.stats.entries)) - self.assertEqual(1, self.runner.stats.get("/redirect", "GET").num_requests) - self.assertEqual(0, self.runner.stats.get("/ultra_fast", "GET").num_requests) + self.assertEqual(1, self.runner.stats.get("/redirect", "GET", l.host).num_requests) + self.assertEqual(0, self.runner.stats.get("/ultra_fast", "GET", l.host).num_requests) class TestCatchResponse(WebserverTestCase): diff --git a/locust/test/test_runners.py b/locust/test/test_runners.py index f687f214ec..83055ff3e8 100644 --- a/locust/test/test_runners.py +++ b/locust/test/test_runners.py @@ -353,6 +353,7 @@ def my_task(self): self.user.environment.events.request.fire( request_type="GET", name="/test", + host="https://localhost", response_time=666, response_length=1337, exception=None, @@ -365,9 +366,9 @@ def my_task(self): runner = LocalRunner(environment) runner.start(user_count=6, spawn_rate=1, wait=False) sleep(3) - self.assertGreaterEqual(runner.stats.get("/test", "GET").num_requests, 3) + self.assertGreaterEqual(runner.stats.get("/test", "GET", "https://localhost").num_requests, 3) sleep(3.25) - self.assertLessEqual(runner.stats.get("/test", "GET").num_requests, 1) + self.assertLessEqual(runner.stats.get("/test", "GET", "https://localhost").num_requests, 1) runner.quit() def test_no_reset_stats(self): @@ -379,6 +380,7 @@ def my_task(self): self.user.environment.events.request.fire( request_type="GET", name="/test", + host="https://localhost", response_time=666, response_length=1337, exception=None, @@ -390,9 +392,9 @@ def my_task(self): runner = LocalRunner(environment) runner.start(user_count=6, spawn_rate=12, wait=False) sleep(0.25) - self.assertGreaterEqual(runner.stats.get("/test", "GET").num_requests, 3) + self.assertGreaterEqual(runner.stats.get("/test", "GET", "https://localhost").num_requests, 3) sleep(0.3) - self.assertEqual(6, runner.stats.get("/test", "GET").num_requests) + self.assertEqual(6, runner.stats.get("/test", "GET", "https://localhost").num_requests) runner.quit() def test_runner_reference_on_environment(self): @@ -844,6 +846,7 @@ def incr_stats(self): self.environment.events.request.fire( request_type="GET", name="/", + host="https://localhost", response_time=1337, response_length=666, exception=None, @@ -907,6 +910,7 @@ def incr_stats(self): self.environment.events.request.fire( request_type="GET", name="/", + host="https://localhost", response_time=1337, response_length=666, exception=None, @@ -1010,6 +1014,7 @@ def incr_stats(self): self.environment.events.request.fire( request_type="GET", name=self.environment.parsed_options.my_str_argument, + host="https://localhost", response_time=self.environment.parsed_options.my_int_argument, response_length=666, exception=None, @@ -1057,7 +1062,7 @@ def _(parser, **kw): self.assertEqual(0, worker.user_count) self.assertEqual(master_env.runner.stats.total.max_response_time, 42) - self.assertEqual(master_env.runner.stats.get("cool-string", "GET").avg_response_time, 42) + self.assertEqual(master_env.runner.stats.get("cool-string", "GET", "https://localhost").avg_response_time, 42) def test_test_stop_event(self): class TestUser(User): @@ -2060,16 +2065,16 @@ def test_worker_stats_report_median(self): master = self.get_runner() server.mocked_send(Message("client_ready", __version__, "fake_client")) - master.stats.get("/", "GET").log(100, 23455) - master.stats.get("/", "GET").log(800, 23455) - master.stats.get("/", "GET").log(700, 23455) + master.stats.get("/", "GET", "https://localhost").log(100, 23455) + master.stats.get("/", "GET", "https://localhost").log(800, 23455) + master.stats.get("/", "GET", "https://localhost").log(700, 23455) data = {"user_count": 1} self.environment.events.report_to_master.fire(client_id="fake_client", data=data) master.stats.clear_all() server.mocked_send(Message("stats", data, "fake_client")) - s = master.stats.get("/", "GET") + s = master.stats.get("/", "GET", "https://localhost") self.assertEqual(700, s.median_response_time) def test_worker_stats_report_with_none_response_times(self): @@ -2077,24 +2082,24 @@ def test_worker_stats_report_with_none_response_times(self): master = self.get_runner() server.mocked_send(Message("client_ready", __version__, "fake_client")) - master.stats.get("/mixed", "GET").log(0, 23455) - master.stats.get("/mixed", "GET").log(800, 23455) - master.stats.get("/mixed", "GET").log(700, 23455) - master.stats.get("/mixed", "GET").log(None, 23455) - master.stats.get("/mixed", "GET").log(None, 23455) - master.stats.get("/mixed", "GET").log(None, 23455) - master.stats.get("/mixed", "GET").log(None, 23455) - master.stats.get("/onlyNone", "GET").log(None, 23455) + master.stats.get("/mixed", "GET", "https://localhost").log(0, 23455) + master.stats.get("/mixed", "GET", "https://localhost").log(800, 23455) + master.stats.get("/mixed", "GET", "https://localhost").log(700, 23455) + master.stats.get("/mixed", "GET", "https://localhost").log(None, 23455) + master.stats.get("/mixed", "GET", "https://localhost").log(None, 23455) + master.stats.get("/mixed", "GET", "https://localhost").log(None, 23455) + master.stats.get("/mixed", "GET", "https://localhost").log(None, 23455) + master.stats.get("/onlyNone", "GET", "https://localhost").log(None, 23455) data = {"user_count": 1} self.environment.events.report_to_master.fire(client_id="fake_client", data=data) master.stats.clear_all() server.mocked_send(Message("stats", data, "fake_client")) - s1 = master.stats.get("/mixed", "GET") + s1 = master.stats.get("/mixed", "GET", "https://localhost") self.assertEqual(700, s1.median_response_time) self.assertEqual(500, s1.avg_response_time) - s2 = master.stats.get("/onlyNone", "GET") + s2 = master.stats.get("/onlyNone", "GET", "https://localhost") self.assertEqual(0, s2.median_response_time) self.assertEqual(0, s2.avg_response_time) @@ -2298,10 +2303,10 @@ def test_master_total_stats(self): master = self.get_runner() server.mocked_send(Message("client_ready", __version__, "fake_client")) stats = RequestStats() - stats.log_request("GET", "/1", 100, 3546) - stats.log_request("GET", "/1", 800, 56743) + stats.log_request("GET", "/1", "", 100, 3546) + stats.log_request("GET", "/1", "", 800, 56743) stats2 = RequestStats() - stats2.log_request("GET", "/2", 700, 2201) + stats2.log_request("GET", "/2", "", 700, 2201) server.mocked_send( Message( "stats", @@ -2333,14 +2338,14 @@ def test_master_total_stats_with_none_response_times(self): master = self.get_runner() server.mocked_send(Message("client_ready", __version__, "fake_client")) stats = RequestStats() - stats.log_request("GET", "/1", 100, 3546) - stats.log_request("GET", "/1", 800, 56743) - stats.log_request("GET", "/1", None, 56743) + stats.log_request("GET", "/1", "https://localhost", 100, 3546) + stats.log_request("GET", "/1", "https://localhost", 800, 56743) + stats.log_request("GET", "/1", "https://localhost", None, 56743) stats2 = RequestStats() - stats2.log_request("GET", "/2", 700, 2201) - stats2.log_request("GET", "/2", None, 2201) + stats2.log_request("GET", "/2", "https://localhost", 700, 2201) + stats2.log_request("GET", "/2", "https://localhost", None, 2201) stats3 = RequestStats() - stats3.log_request("GET", "/3", None, 2201) + stats3.log_request("GET", "/3", "https://localhost", None, 2201) server.mocked_send( Message( "stats", @@ -2389,8 +2394,8 @@ def test_master_current_response_times(self): mocked_time.return_value += 1.0234 server.mocked_send(Message("client_ready", __version__, "fake_client")) stats = RequestStats() - stats.log_request("GET", "/1", 100, 3546) - stats.log_request("GET", "/1", 800, 56743) + stats.log_request("GET", "/1", "https://localhost", 100, 3546) + stats.log_request("GET", "/1", "https://localhost", 800, 56743) server.mocked_send( Message( "stats", @@ -2405,7 +2410,7 @@ def test_master_current_response_times(self): ) mocked_time.return_value += 1 stats2 = RequestStats() - stats2.log_request("GET", "/2", 400, 2201) + stats2.log_request("GET", "/2", "https://localhost", 400, 2201) server.mocked_send( Message( "stats", @@ -2425,9 +2430,9 @@ def test_master_current_response_times(self): # let 10 second pass, do some more requests, send it to the master and make # sure the current response time percentiles only accounts for these new requests mocked_time.return_value += 10.10023 - stats.log_request("GET", "/1", 20, 1) - stats.log_request("GET", "/1", 30, 1) - stats.log_request("GET", "/1", 3000, 1) + stats.log_request("GET", "/1", "https://localhost", 20, 1) + stats.log_request("GET", "/1", "https://localhost", 30, 1) + stats.log_request("GET", "/1", "https://localhost", 3000, 1) server.mocked_send( Message( "stats", diff --git a/locust/test/test_stats.py b/locust/test/test_stats.py index 082ff7e2b7..c50925320d 100644 --- a/locust/test/test_stats.py +++ b/locust/test/test_stats.py @@ -48,10 +48,10 @@ def setUp(self): self.stats = RequestStats() def log(response_time, size): - self.stats.log_request("GET", "test_entry", response_time, size) + self.stats.log_request("GET", "test_entry", "", response_time, size) def log_error(exc): - self.stats.log_error("GET", "test_entry", exc) + self.stats.log_error("GET", "test_entry", "", exc) log(45, 1) log(135, 1) @@ -65,10 +65,10 @@ def log_error(exc): log(79, 1) log(None, 1) log_error(Exception("dummy fail")) - self.s = self.stats.entries[("test_entry", "GET")] + self.s = self.stats.entries[("test_entry", "GET", "")] def test_percentile(self): - s = StatsEntry(self.stats, "percentile_test", "GET") + s = StatsEntry(self.stats, "percentile_test", "GET", "") for x in range(100): s.log(x, 0) @@ -88,8 +88,8 @@ def test_median_out_of_min_max_bounds(self): self.assertEqual(s.median_response_time, 6099) def test_total_rps(self): - self.stats.log_request("GET", "other_endpoint", 1337, 1337) - s2 = self.stats.entries[("other_endpoint", "GET")] + self.stats.log_request("GET", "other_endpoint", "", 1337, 1337) + s2 = self.stats.entries[("other_endpoint", "GET", "")] s2.start_time = 2.0 s2.last_request_timestamp = 6.0 self.s.start_time = 1.0 @@ -272,17 +272,32 @@ def test_error_grouping(self): # reset stats self.stats = RequestStats() - self.stats.log_error("GET", "/some-path", Exception("Exception!")) - self.stats.log_error("GET", "/some-path", Exception("Exception!")) + self.stats.log_error("GET", "/some-path", "", Exception("Exception!")) + self.stats.log_error("GET", "/some-path", "", Exception("Exception!")) self.assertEqual(1, len(self.stats.errors)) self.assertEqual(2, list(self.stats.errors.values())[0].occurrences) - self.stats.log_error("GET", "/some-path", Exception("Another exception!")) - self.stats.log_error("GET", "/some-path", Exception("Another exception!")) - self.stats.log_error("GET", "/some-path", Exception("Third exception!")) + self.stats.log_error("GET", "/some-path", "", Exception("Another exception!")) + self.stats.log_error("GET", "/some-path", "", Exception("Another exception!")) + self.stats.log_error("GET", "/some-path", "", Exception("Third exception!")) self.assertEqual(3, len(self.stats.errors)) + def test_error_grouping_with_multiple_hosts(self): + # reset stats + self.stats = RequestStats() + + self.stats.log_error("GET", "/some-path", "http://127.0.0.1:1", Exception("Exception!")) + self.stats.log_error("GET", "/some-path", "http://127.0.0.1:2", Exception("Exception!")) + self.assertEqual(2, len(self.stats.errors)) + self.assertEqual(1, list(self.stats.errors.values())[0].occurrences) + self.assertEqual(1, list(self.stats.errors.values())[1].occurrences) + + self.stats.log_error("GET", "/some-path", "http://127.0.0.1:1", Exception("Another exception!")) + self.stats.log_error("GET", "/some-path", "http://127.0.0.1:1", Exception("Another exception!")) + self.stats.log_error("GET", "/some-path", "http://127.0.0.1:2", Exception("Third exception!")) + self.assertEqual(4, len(self.stats.errors)) + def test_error_grouping_errors_with_memory_addresses(self): # reset stats self.stats = RequestStats() @@ -290,7 +305,7 @@ def test_error_grouping_errors_with_memory_addresses(self): class Dummy: pass - self.stats.log_error("GET", "/", Exception(f"Error caused by {Dummy()!r}")) + self.stats.log_error("GET", "/", "", Exception(f"Error caused by {Dummy()!r}")) self.assertEqual(1, len(self.stats.errors)) def test_serialize_through_message(self): @@ -329,9 +344,9 @@ def setUp(self): 3, ), ]: - self.stats.log_request(method, name, i, 2000 + i) + self.stats.log_request(method, name, "", i, 2000 + i) if i % freq == 0: - self.stats.log_error(method, name, RuntimeError(f"{method} error")) + self.stats.log_error(method, name, "", RuntimeError(f"{method} error")) def test_print_percentile_stats(self): locust.stats.print_percentile_stats(self.stats) @@ -435,13 +450,13 @@ def test_csv_stats_writer_full_history(self): ) for i in range(10): - self.runner.stats.log_request("GET", "/", 100, content_length=666) + self.runner.stats.log_request("GET", "/", "", 100, content_length=666) greenlet = gevent.spawn(stats_writer) gevent.sleep(10) for i in range(10): - self.runner.stats.log_request("GET", "/", 10, content_length=666) + self.runner.stats.log_request("GET", "/", "", 10, content_length=666) gevent.sleep(5) @@ -492,16 +507,16 @@ def test_csv_stats_on_master_from_aggregated_stats(self): server.mocked_send(Message("client_ready", __version__, "fake_client")) - master.stats.entries[("/", "GET")].log(100, 23455) - master.stats.entries[("/", "GET")].log(800, 23455) - master.stats.entries[("/", "GET")].log(700, 23455) + master.stats.entries[("/", "GET", "")].log(100, 23455) + master.stats.entries[("/", "GET", "")].log(800, 23455) + master.stats.entries[("/", "GET", "")].log(700, 23455) data = {"user_count": 1} environment.events.report_to_master.fire(client_id="fake_client", data=data) master.stats.clear_all() server.mocked_send(Message("stats", data, "fake_client")) - s = master.stats.entries[("/", "GET")] + s = master.stats.entries[("/", "GET", "")] self.assertEqual(700, s.median_response_time) gevent.kill(greenlet) @@ -521,7 +536,7 @@ class TestUser(User): @task def t(self): - self.environment.runner.stats.log_request("GET", "/", 10, 10) + self.environment.runner.stats.log_request("GET", "/", "", 10, 10) environment = Environment(user_classes=[TestUser]) stats_writer = StatsCSVFileWriter(environment, PERCENTILES_TO_REPORT, self.STATS_BASE_NAME, full_history=True) @@ -571,7 +586,7 @@ def test_requests_csv_quote_escaping(self): } request_name_str = json.dumps(request_name_dict) - master.stats.entries[(request_name_str, "GET")].log(100, 23455) + master.stats.entries[(request_name_str, "GET", "")].log(100, 23455) data = {"user_count": 1} environment.events.report_to_master.fire(client_id="fake_client", data=data) master.stats.clear_all() @@ -779,49 +794,51 @@ class MyUser(HttpUser): def test_request_stats_content_length(self): self.locust.client.get("/ultra_fast") self.assertEqual( - self.runner.stats.entries[("/ultra_fast", "GET")].avg_content_length, len("This is an ultra fast response") + self.runner.stats.entries[("/ultra_fast", "GET", self.locust.host)].avg_content_length, + len("This is an ultra fast response"), ) self.locust.client.get("/ultra_fast") # test legacy stats.get() function sometimes too self.assertEqual( - self.runner.stats.get("/ultra_fast", "GET").avg_content_length, len("This is an ultra fast response") + self.runner.stats.get("/ultra_fast", "GET", self.locust.host).avg_content_length, + len("This is an ultra fast response"), ) def test_request_stats_no_content_length(self): path = "/no_content_length" self.locust.client.get(path) self.assertEqual( - self.runner.stats.entries[(path, "GET")].avg_content_length, + self.runner.stats.entries[(path, "GET", self.locust.host)].avg_content_length, len("This response does not have content-length in the header"), ) def test_request_stats_no_content_length_streaming(self): path = "/no_content_length" self.locust.client.get(path, stream=True) - self.assertEqual(0, self.runner.stats.entries[(path, "GET")].avg_content_length) + self.assertEqual(0, self.runner.stats.entries[(path, "GET", self.locust.host)].avg_content_length) def test_request_stats_named_endpoint(self): self.locust.client.get("/ultra_fast", name="my_custom_name") - self.assertEqual(1, self.runner.stats.entries[("my_custom_name", "GET")].num_requests) + self.assertEqual(1, self.runner.stats.entries[("my_custom_name", "GET", self.locust.host)].num_requests) def test_request_stats_named_endpoint_request_name(self): self.locust.client.request_name = "my_custom_name_1" self.locust.client.get("/ultra_fast") - self.assertEqual(1, self.runner.stats.entries[("my_custom_name_1", "GET")].num_requests) + self.assertEqual(1, self.runner.stats.entries[("my_custom_name_1", "GET", self.locust.host)].num_requests) self.locust.client.request_name = None def test_request_stats_named_endpoint_rename_request(self): with self.locust.client.rename_request("my_custom_name_3"): self.locust.client.get("/ultra_fast") - self.assertEqual(1, self.runner.stats.entries[("my_custom_name_3", "GET")].num_requests) + self.assertEqual(1, self.runner.stats.entries[("my_custom_name_3", "GET", self.locust.host)].num_requests) def test_request_stats_query_variables(self): self.locust.client.get("/ultra_fast?query=1") - self.assertEqual(1, self.runner.stats.entries[("/ultra_fast?query=1", "GET")].num_requests) + self.assertEqual(1, self.runner.stats.entries[("/ultra_fast?query=1", "GET", self.locust.host)].num_requests) def test_request_stats_put(self): self.locust.client.put("/put") - self.assertEqual(1, self.runner.stats.entries[("/put", "PUT")].num_requests) + self.assertEqual(1, self.runner.stats.entries[("/put", "PUT", self.locust.host)].num_requests) def test_request_connection_error(self): class MyUser(HttpUser): @@ -830,8 +847,8 @@ class MyUser(HttpUser): locust = MyUser(self.environment) response = locust.client.get("/", timeout=0.1) self.assertEqual(response.status_code, 0) - self.assertEqual(1, self.runner.stats.entries[("/", "GET")].num_failures) - self.assertEqual(1, self.runner.stats.entries[("/", "GET")].num_requests) + self.assertEqual(1, self.runner.stats.entries[("/", "GET", locust.host)].num_failures) + self.assertEqual(1, self.runner.stats.entries[("/", "GET", locust.host)].num_requests) class MyTaskSet(TaskSet): diff --git a/locust/test/test_web.py b/locust/test/test_web.py index 3616a8140b..2ed4b5594c 100644 --- a/locust/test/test_web.py +++ b/locust/test/test_web.py @@ -106,7 +106,7 @@ def test_stats_no_data(self): self.assertEqual(200, requests.get("http://127.0.0.1:%i/stats/requests" % self.web_port).status_code) def test_stats(self): - self.stats.log_request("GET", "/", 120, 5612) + self.stats.log_request("GET", "/", "", 120, 5612) response = requests.get("http://127.0.0.1:%i/stats/requests" % self.web_port) self.assertEqual(200, response.status_code) @@ -122,14 +122,14 @@ def test_stats(self): self.assertEqual(120, data["stats"][1]["avg_response_time"]) def test_stats_cache(self): - self.stats.log_request("GET", "/test", 120, 5612) + self.stats.log_request("GET", "/test", "", 120, 5612) response = requests.get("http://127.0.0.1:%i/stats/requests" % self.web_port) self.assertEqual(200, response.status_code) data = json.loads(response.text) self.assertEqual(2, len(data["stats"])) # one entry plus Aggregated # add another entry - self.stats.log_request("GET", "/test2", 120, 5612) + self.stats.log_request("GET", "/test2", "", 120, 5612) data = json.loads(requests.get("http://127.0.0.1:%i/stats/requests" % self.web_port).text) self.assertEqual(2, len(data["stats"])) # old value should be cached now @@ -139,8 +139,8 @@ def test_stats_cache(self): self.assertEqual(3, len(data["stats"])) # this should no longer be cached def test_stats_rounding(self): - self.stats.log_request("GET", "/test", 1.39764125, 2) - self.stats.log_request("GET", "/test", 999.9764125, 1000) + self.stats.log_request("GET", "/test", "", 1.39764125, 2) + self.stats.log_request("GET", "/test", "", 999.9764125, 1000) response = requests.get("http://127.0.0.1:%i/stats/requests" % self.web_port) self.assertEqual(200, response.status_code) @@ -148,28 +148,46 @@ def test_stats_rounding(self): self.assertEqual(1, data["stats"][0]["min_response_time"]) self.assertEqual(1000, data["stats"][0]["max_response_time"]) + def test_stats_multiple_hosts(self): + class UserOne(User): + host = "http://127.0.0.1:1" + + class UserTwo(User): + host = "http://127.0.0.1:2" + + self.environment.user_classes = [UserOne, UserTwo] + self.stats.clear_all() + self.stats.log_request("GET", "/test", "http://127.0.0.1:1", 1.39764125, 2) + self.stats.log_request("GET", "/test", "http://127.0.0.1:2", 1.39764125, 2) + response = requests.get("http://127.0.0.1:%i/stats/requests" % self.web_port) + self.assertEqual( + ["http://127.0.0.1:1", "http://127.0.0.1:2"], + [stat["host"] for stat in response.json()["stats"] if stat["host"]], + ) + def test_request_stats_csv(self): - self.stats.log_request("GET", "/test2", 120, 5612) + self.stats.log_request("GET", "/test2", "", 120, 5612) response = requests.get("http://127.0.0.1:%i/stats/requests/csv" % self.web_port) self.assertEqual(200, response.status_code) self._check_csv_headers(response.headers, "requests") def test_request_stats_full_history_csv_not_present(self): - self.stats.log_request("GET", "/test2", 120, 5612) + self.stats.log_request("GET", "/test2", "", 120, 5612) response = requests.get("http://127.0.0.1:%i/stats/requests_full_history/csv" % self.web_port) self.assertEqual(404, response.status_code) def test_failure_stats_csv(self): - self.stats.log_error("GET", "/", Exception("Error1337")) + self.stats.log_error("GET", "/", "", Exception("Error1337")) response = requests.get("http://127.0.0.1:%i/stats/failures/csv" % self.web_port) self.assertEqual(200, response.status_code) self._check_csv_headers(response.headers, "failures") def test_request_stats_with_errors(self): - self.stats.log_error("GET", "/", Exception("Error1337")) + self.stats.log_error("GET", "/", "http://127.0.0.1", Exception("Error1337")) response = requests.get("http://127.0.0.1:%i/stats/requests" % self.web_port) self.assertEqual(200, response.status_code) self.assertIn("Error1337", response.text) + self.assertIn("http://127.0.0.1", [error["host"] for error in response.json()["errors"]]) def test_reset_stats(self): try: @@ -179,20 +197,21 @@ def test_reset_stats(self): self.runner.log_exception("local", str(e), "".join(traceback.format_tb(tb))) self.runner.log_exception("local", str(e), "".join(traceback.format_tb(tb))) - self.stats.log_request("GET", "/test", 120, 5612) - self.stats.log_error("GET", "/", Exception("Error1337")) + self.stats.log_request("GET", "/test", "", 120, 5612) + self.stats.log_error("GET", "/", "", Exception("Error1337")) - response = requests.get("http://127.0.0.1:%i/stats/reset" % self.web_port) + host = "http://127.0.0.1:%i" % self.web_port + response = requests.get("%s/stats/reset" % host) self.assertEqual(200, response.status_code) self.assertEqual({}, self.stats.errors) self.assertEqual({}, self.runner.exceptions) - self.assertEqual(0, self.stats.get("/", "GET").num_requests) - self.assertEqual(0, self.stats.get("/", "GET").num_failures) - self.assertEqual(0, self.stats.get("/test", "GET").num_requests) - self.assertEqual(0, self.stats.get("/test", "GET").num_failures) + self.assertEqual(0, self.stats.get("/", "GET", host).num_requests) + self.assertEqual(0, self.stats.get("/", "GET", host).num_failures) + self.assertEqual(0, self.stats.get("/test", "GET", host).num_requests) + self.assertEqual(0, self.stats.get("/test", "GET", host).num_failures) def test_exceptions(self): try: @@ -883,11 +902,10 @@ class MyUser2(User): self.environment.user_classes = [MyUser, MyUser2] response = requests.get("http://127.0.0.1:%i/" % self.web_port) self.assertEqual(200, response.status_code) - self.assertNotIn("http://example.com", response.content.decode("utf-8")) self.assertIn("setting this will override the host on all User classes", response.content.decode("utf-8")) def test_report_page(self): - self.stats.log_request("GET", "/test", 120, 5612) + self.stats.log_request("GET", "/test", "", 120, 5612) r = requests.get("http://127.0.0.1:%i/stats/report" % self.web_port) self.assertEqual(200, r.status_code) self.assertIn("