From d5b3d85483ff86138ce5748d57e165d064545cab Mon Sep 17 00:00:00 2001
From: Viraj Patel <144226112+VirajP1002@users.noreply.github.com>
Date: Fri, 4 Oct 2024 15:38:57 +0100
Subject: [PATCH] Add session/download-pdf endpoints and add graph to represent
values (#164)
---
.coveragerc | 2 +-
.../test_benchmark_business_happy_path.json | 10 +-
scripts/benchmark_stats.py | 115 +++++++----
scripts/visualise_results.py | 73 +++++--
tests/conftest.py | 61 +++++-
.../2024-02-09T03:09:41/mock_output_stats.csv | 96 +++++++++
.../2024-02-12T03:09:41/mock_output_stats.csv | 96 +++++++++
tests/test_benchmark_stats.py | 76 ++++++-
tests/test_get_summary.py | 21 +-
tests/test_visualise_results.py | 190 +++++++++++++++---
10 files changed, 633 insertions(+), 107 deletions(-)
create mode 100644 tests/mock_stats/2024-02-09T03:09:41/mock_output_stats.csv
create mode 100644 tests/mock_stats/2024-02-12T03:09:41/mock_output_stats.csv
diff --git a/.coveragerc b/.coveragerc
index c135d532..1177471d 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -2,4 +2,4 @@
include = scripts/*
[report]
-fail_under = 80
+fail_under = 85
diff --git a/requests/test_benchmark_business_happy_path.json b/requests/test_benchmark_business_happy_path.json
index 45837e9c..91edcbd8 100644
--- a/requests/test_benchmark_business_happy_path.json
+++ b/requests/test_benchmark_business_happy_path.json
@@ -73,7 +73,7 @@
"method": "POST",
"url": "/questionnaire/block-383/",
"data": {
- "answer-439": "Hi eQ team \ud83d\ude0d"
+ "answer-439": "Hi eQ Team"
}
},
{
@@ -119,8 +119,8 @@
"method": "POST",
"url": "/questionnaire/people/add-person/",
"data": {
- "first-name": "Erling",
- "last-name": "Haaland"
+ "first-name": "Kylian",
+ "last-name": "Mbappé"
}
},
{
@@ -540,6 +540,10 @@
"method": "GET",
"url": "/submitted/view-response/"
},
+ {
+ "method": "GET",
+ "url": "/submitted/download-pdf"
+ },
{
"method": "GET",
"url": "/submitted/thank-you/"
diff --git a/scripts/benchmark_stats.py b/scripts/benchmark_stats.py
index c60e44c1..0cd27524 100644
--- a/scripts/benchmark_stats.py
+++ b/scripts/benchmark_stats.py
@@ -20,25 +20,41 @@ def __init__(self, folder_paths: List[str]):
"POST": {"response_times": [], "total": 0},
}
+ self._session_percentile: List[int] = []
+ self._pdf_percentile: List[int] = []
self._total_failures: int = 0
self._percentiles: Mapping[int : List[float]] = defaultdict(list) # noqa: E203
self._process_file_data()
- def __str__(self):
- formatted_percentiles = "\n".join(
+ self._formatted_percentiles: str = "\n".join(
f"{percentile}th: {self.percentiles[percentile]}ms"
for percentile in self.PERCENTILES_TO_REPORT
)
+
+ self.formatted_average_get: str = self.formatted_percentile(self.average_get)
+ self.formatted_average_post: str = self.formatted_percentile(self.average_post)
+ self.formatted_average_pdf_percentile: str = self.formatted_percentile(
+ self.average_pdf_percentile
+ )
+ self.formatted_average_session_percentile: str = self.formatted_percentile(
+ self.average_session_percentile
+ )
+
+ def __str__(self):
if self.output_to_github:
- formatted_percentiles = formatted_percentiles.replace(os.linesep, "
")
+ formatted_percentiles = self._formatted_percentiles.replace(
+ os.linesep, "
"
+ )
return (
f'{{"body": "'
f"**Benchmark Results**
"
f"Percentile Averages:
"
f"{formatted_percentiles}
"
- f"GETs (99th): {self.average_get}ms
"
- f"POSTs (99th): {self.average_post}ms
"
+ f"GETs (99th): {self.formatted_average_get}
"
+ f"POSTs (99th): {self.formatted_average_post}
"
+ f"PDF: {self.formatted_average_pdf_percentile}
"
+ f"Session: {self.formatted_average_session_percentile}
"
f"Total Requests: {self.total_requests:,}
"
f"Total Failures: {self._total_failures:,}
"
f'Error Percentage: {(round(self.error_percentage, 2))}%
"}}'
@@ -46,10 +62,13 @@ def __str__(self):
return (
f"---\n"
f"Percentile Averages:\n"
- f"{formatted_percentiles}\n"
+ f"{self._formatted_percentiles}\n"
+ f"---\n"
+ f"GETs (99th): {self.formatted_average_get}\n"
+ f"POSTs (99th): {self.formatted_average_post}\n"
f"---\n"
- f"GETs (99th): {self.average_get}ms\n"
- f"POSTs (99th): {self.average_post}ms\n"
+ f"PDF: {self.formatted_average_pdf_percentile}\n"
+ f"Session: {self.formatted_average_session_percentile}\n"
f"---\n"
f"Total Requests: {self.total_requests:,}\n"
f"Total Failures: {self._total_failures:,}\n"
@@ -60,34 +79,48 @@ def _process_file_data(self):
for file in self.files:
with open(file) as fp:
for row in DictReader(fp, delimiter=","):
- request_count = int(
- row.get("Request Count") or row.get("# requests")
- )
- if row["Name"] == "Aggregated":
- failure_count = row.get("Failure Count") or row.get(
- "# failures"
- )
- self._total_failures += int(failure_count)
- else:
- weighted_request_count = self._get_weighted_request_count(
- request_count
- )
- for percentile in self.PERCENTILES_TO_REPORT:
- weighted_percentile = (
- float(row[f"{percentile}%"]) * weighted_request_count
- )
- self._percentiles[percentile].append(weighted_percentile)
-
- percentile_response_time = float(
- row[f"{self.PERCENTILE_TO_USE_FOR_AVERAGES}%"]
- )
- weighted_response_time = (
- percentile_response_time * weighted_request_count
- )
- self._requests[row["Type"]]["response_times"].append(
- weighted_response_time
- )
- self._requests[row["Type"]]["total"] += request_count
+ self._process_row(row)
+
+ def _process_row(self, row):
+ # Handle special 'Aggregated' row
+ if row["Name"] == "Aggregated":
+ failure_count = row["Failure Count"] or row["# failures"]
+ self._total_failures += int(failure_count)
+ return
+
+ if row["Name"] == "/submitted/download-pdf":
+ self._pdf_percentile.append(
+ int(row[f"{self.PERCENTILE_TO_USE_FOR_AVERAGES}%"])
+ )
+ elif row["Name"] == "/session":
+ self._session_percentile.append(
+ int(row[f"{self.PERCENTILE_TO_USE_FOR_AVERAGES}%"])
+ )
+
+ request_count = int(row.get("Request Count") or row.get("# requests"))
+ weighted_request_count = self._get_weighted_request_count(request_count)
+
+ for percentile in self.PERCENTILES_TO_REPORT:
+ weighted_percentile = float(row[f"{percentile}%"]) * weighted_request_count
+ self._percentiles[percentile].append(weighted_percentile)
+
+ percentile_response_time = float(row[f"{self.PERCENTILE_TO_USE_FOR_AVERAGES}%"])
+ weighted_response_time = percentile_response_time * weighted_request_count
+ self._requests[row["Type"]]["response_times"].append(weighted_response_time)
+ self._requests[row["Type"]]["total"] += request_count
+
+ @property
+ def average_pdf_percentile(self) -> int | None:
+ if self._pdf_percentile:
+ average_pdf_percentile = sum(self._pdf_percentile) / len(
+ self._pdf_percentile
+ )
+ return round(average_pdf_percentile)
+ return None
+
+ @property
+ def average_session_percentile(self) -> int:
+ return round(sum(self._session_percentile) / len(self._session_percentile))
@property
def files(self) -> List[str]:
@@ -96,7 +129,7 @@ def files(self) -> List[str]:
@property
def percentiles(self) -> Mapping:
return {
- percentile: int(
+ percentile: round(
sum(values) / self._get_weighted_request_count(self.total_requests)
)
for percentile, values in self._percentiles.items()
@@ -108,14 +141,14 @@ def total_requests(self) -> int:
@property
def average_get(self) -> int:
- return int(
+ return round(
sum(self._requests["GET"]["response_times"])
/ self._get_weighted_request_count(self._requests["GET"]["total"])
)
@property
def average_post(self) -> int:
- return int(
+ return round(
sum(self._requests["POST"]["response_times"])
/ self._get_weighted_request_count(self._requests["POST"]["total"])
)
@@ -124,5 +157,9 @@ def average_post(self) -> int:
def error_percentage(self) -> float:
return (self._total_failures * 100) / self.total_requests
+ @staticmethod
+ def formatted_percentile(percentile) -> str:
+ return f"{percentile}ms" if percentile else "N/A"
+
def _get_weighted_request_count(self, request_count: int) -> float:
return request_count * self.PERCENTILE_TO_USE_FOR_AVERAGES / 100
diff --git a/scripts/visualise_results.py b/scripts/visualise_results.py
index 6dc589bc..5d01bdf7 100644
--- a/scripts/visualise_results.py
+++ b/scripts/visualise_results.py
@@ -6,38 +6,69 @@
from scripts.get_summary import get_results, parse_environment_variables
PERCENTILES_TO_GRAPH = (50, 90, 95, 99)
+PERCENTILES_TO_PLOT = ("50th", "90th", "95th", "99th")
+
+ADDITIONAL_METRICS_TO_GRAPH = ("PDF", "Session")
class GraphGenerationFailed(Exception):
pass
-def plot_data(df, number_of_days_to_plot):
- try:
- plt.style.use("fast")
+def plot_data(dataframes, number_of_days_to_plot):
+ plt.style.use("fast")
+ fig, axes = plt.subplots(nrows=1, ncols=len(dataframes), figsize=(15, 6))
+ for i, dataframe in enumerate(dataframes):
+ """
+ We use the .subplot() method below to switch the indexes of the plots themselves,
+ if we do not switch plots or remove this method, the visuals (such as the graph background)
+ and, most importantly, the axes values will only be applied to the second subplot.
+ """
+ plt.subplot(1, len(dataframes), i + 1)
if (
number_of_days_to_plot and number_of_days_to_plot <= 45
): # To make the chart still easily digestible
- df.plot.line(marker="o", markersize=8)
+ dataframe.plot.line(
+ marker="o", markersize=8, ax=axes[i] if len(dataframes) > 1 else axes
+ )
plt.grid(True, axis="both", alpha=0.3)
else:
- df.plot.line()
+ dataframe.plot.line(ax=axes[i] if len(dataframes) > 1 else axes)
plt.margins(0.03, 0.07)
- plt.legend(frameon=True, prop={"size": 17})
- plt.xticks(df.index, df["DATE"], size="small", rotation=90)
+ plt.legend(frameon=True, prop={"size": 10})
+ plt.xticks(dataframe.index, dataframe["DATE"], size="small", rotation=90)
plt.yticks(size="small")
plt.ylabel("Average Response Time (ms)")
plt.xlabel("Run Date (YYYY-MM-DD)", labelpad=13)
- plt.savefig("performance_graph.png", bbox_inches="tight")
- print("Graph saved as performance_graph.png")
+
+def create_graph(dataframes, number_of_days_to_plot, filename):
+ try:
+ plot_data(dataframes, number_of_days_to_plot)
+ plt.savefig(filename, bbox_inches="tight")
+ print("Graph saved as", filename)
except Exception as e:
raise GraphGenerationFailed from e
-def get_data_frame(results):
+def get_dataframes(folders, number_of_days):
+ results = list(get_results(folders, number_of_days))
+ performance_dataframe = get_performance_data_frame(results)
+ additional_metrics_dataframe = get_additional_metrics_data_frame(results)
+
+ return [performance_dataframe, additional_metrics_dataframe]
+
+
+def create_dataframe(result_fields, values_to_plot):
+ return DataFrame(
+ result_fields,
+ columns=["DATE", *(f"{percentile}" for percentile in values_to_plot)],
+ )
+
+
+def get_performance_data_frame(results):
result_fields = [
[
result.date,
@@ -49,16 +80,26 @@ def get_data_frame(results):
for result in results
]
- percentile_columns = (f"{percentile}th" for percentile in PERCENTILES_TO_GRAPH)
- return DataFrame(result_fields, columns=["DATE", *percentile_columns])
+ return create_dataframe(result_fields, PERCENTILES_TO_PLOT)
+
+
+def get_additional_metrics_data_frame(results):
+ result_fields = [
+ [
+ result.date,
+ result.statistics.average_pdf_percentile,
+ result.statistics.average_session_percentile,
+ ]
+ for result in results
+ ]
+
+ return create_dataframe(result_fields, ADDITIONAL_METRICS_TO_GRAPH)
if __name__ == "__main__":
parsed_variables = parse_environment_variables()
number_of_days = parsed_variables["number_of_days"]
-
folders = sorted(glob(f"{parsed_variables['output_dir']}/*"))
- results = get_results(folders, number_of_days)
- dataframe = get_data_frame(results)
- plot_data(dataframe, number_of_days)
+ dataframes = get_dataframes(folders, number_of_days)
+ create_graph(dataframes, number_of_days, "performance_graph.png")
diff --git a/tests/conftest.py b/tests/conftest.py
index d5950d67..83f6f53d 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -6,42 +6,93 @@
"---\n"
"Percentile Averages:\n"
"50th: 58ms\n"
- "90th: 96ms\n"
- "95th: 173ms\n"
+ "90th: 97ms\n"
+ "95th: 174ms\n"
"99th: 301ms\n"
"99.9th: 477ms\n"
"---\n"
"GETs (99th): 380ms\n"
- "POSTs (99th): 211ms\n"
+ "POSTs (99th): 212ms\n"
+ "---\n"
+ "PDF: N/A\n"
+ "Session: 7600ms\n"
"---\n"
"Total Requests: 70,640\n"
"Total Failures: 1\n"
"Error Percentage: 0.0%\n"
)
+EXPECTED_OUTPUT_SINGLE_FOLDER_WITH_PDF = (
+ "---\n"
+ "Percentile Averages:\n"
+ "50th: 52ms\n"
+ "90th: 75ms\n"
+ "95th: 76ms\n"
+ "99th: 76ms\n"
+ "99.9th: 76ms\n"
+ "---\n"
+ "GETs (99th): 118ms\n"
+ "POSTs (99th): 28ms\n"
+ "---\n"
+ "PDF: 4700ms\n"
+ "Session: 180ms\n"
+ "---\n"
+ "Total Requests: 1,097\n"
+ "Total Failures: 0\n"
+ "Error Percentage: 0.0%\n"
+)
+
EXPECTED_OUTPUT_MULTIPLE_FOLDERS = (
"---\n"
"Percentile Averages:\n"
"50th: 58ms\n"
- "90th: 98ms\n"
+ "90th: 99ms\n"
"95th: 176ms\n"
- "99th: 313ms\n"
+ "99th: 314ms\n"
"99.9th: 595ms\n"
"---\n"
"GETs (99th): 383ms\n"
"POSTs (99th): 234ms\n"
"---\n"
+ "PDF: N/A\n"
+ "Session: 7300ms\n"
+ "---\n"
"Total Requests: 211,841\n"
"Total Failures: 2\n"
"Error Percentage: 0.0%\n"
)
+EXPECTED_OUTPUT_MULTIPLE_FOLDERS_WITH_PDF = (
+ "---\n"
+ "Percentile Averages:\n"
+ "50th: 58ms\n"
+ "90th: 98ms\n"
+ "95th: 175ms\n"
+ "99th: 311ms\n"
+ "99.9th: 590ms\n"
+ "---\n"
+ "GETs (99th): 380ms\n"
+ "POSTs (99th): 232ms\n"
+ "---\n"
+ "PDF: 4850ms\n"
+ "Session: 4504ms\n"
+ "---\n"
+ "Total Requests: 214,092\n"
+ "Total Failures: 2\n"
+ "Error Percentage: 0.0%\n"
+)
+
@pytest.fixture
def get_results_single_file():
return get_results(folders=["./tests/mock_stats/2024-02-07T03:09:41"])
+@pytest.fixture
+def get_results_single_file_with_pdf_endpoint():
+ return get_results(folders=["./tests/mock_stats/2024-02-09T03:09:41"])
+
+
@pytest.fixture
def get_results_single_file_github():
return get_results(folders=["./tests/mock_stats/2024-02-07T03:09:41"])
diff --git a/tests/mock_stats/2024-02-09T03:09:41/mock_output_stats.csv b/tests/mock_stats/2024-02-09T03:09:41/mock_output_stats.csv
new file mode 100644
index 00000000..8bd3b260
--- /dev/null
+++ b/tests/mock_stats/2024-02-09T03:09:41/mock_output_stats.csv
@@ -0,0 +1,96 @@
+Type,Name,Request Count,Failure Count,Median Response Time,Average Response Time,Min Response Time,Max Response Time,Average Content Size,Requests/s,Failures/s,50%,66%,75%,80%,90%,95%,98%,99%,99.9%,99.99%,100%
+GET,/questionnaire,10,0,12,12.521058297716081,10.754290968179703,15.16284397803247,255.0,0.16827041003026288,0.0,12,13,14,15,15,15,15,15,15,15,15
+GET,/questionnaire/,20,0,28,28.886581800179556,26.16768190637231,35.674995044246316,124766.5,0.33654082006052577,0.0,29,29,30,30,33,36,36,36,36,36,36
+POST,/questionnaire/,20,0,16,67.88471359177493,11.075941030867398,127.49155692290515,250.0,0.33654082006052577,0.0,120,120,120,120,130,130,130,130,130,130,130
+GET,/questionnaire/any-businesses-or-branches/,10,0,29,29.739139915909618,25.784903089515865,37.286181934177876,104244.0,0.16827041003026288,0.0,29,31,31,32,37,37,37,37,37,37,37
+POST,/questionnaire/any-businesses-or-branches/,10,0,18,19.040479708928615,16.26271998975426,24.36625398695469,273.0,0.16827041003026288,0.0,19,20,21,21,24,24,24,24,24,24,24
+GET,/questionnaire/any-companies-or-branches/,10,0,28,29.01079428847879,26.22722997330129,34.41495297010988,104222.0,0.16827041003026288,0.0,28,29,31,32,34,34,34,34,34,34,34
+POST,/questionnaire/any-companies-or-branches/,10,0,17,17.971195501741022,15.619804034940898,21.72014396637678,331.0,0.16827041003026288,0.0,17,18,18,21,22,22,22,22,22,22,22
+GET,/questionnaire/any-other-companies-or-branches/,10,0,28,29.08987768460065,26.61975403316319,36.18324000854045,106951.0,0.16827041003026288,0.0,28,29,30,30,36,36,36,36,36,36,36
+POST,/questionnaire/any-other-companies-or-branches/,10,0,17,17.7094150101766,15.995775000192225,20.150010008364916,269.0,0.16827041003026288,0.0,17,19,19,19,20,20,20,20,20,20,20
+GET,/questionnaire/any-other-trading-details/,10,0,29,28.194183227606118,25.597497005946934,31.18219505995512,103072.0,0.16827041003026288,0.0,29,29,29,30,31,31,31,31,31,31,31
+POST,/questionnaire/any-other-trading-details/,10,0,19,18.429460003972054,15.40442300029099,22.981974994763732,271.0,0.16827041003026288,0.0,19,19,20,21,23,23,23,23,23,23,23
+GET,/questionnaire/block-379/,10,0,28,30.026265396736562,26.597855030559003,34.05541297979653,103881.0,0.16827041003026288,0.0,31,31,31,34,34,34,34,34,34,34,34
+POST,/questionnaire/block-379/,10,0,17,18.071466695982963,15.901401988230646,24.334451067261398,237.0,0.16827041003026288,0.0,18,19,19,20,24,24,24,24,24,24,24
+GET,/questionnaire/block-381/,10,0,29,29.40658861771226,27.395667042583227,33.33715605549514,107705.0,0.16827041003026288,0.0,29,29,30,32,33,33,33,33,33,33,33
+POST,/questionnaire/block-381/,10,0,18,18.5218526981771,16.332408995367587,22.22067303955555,239.0,0.16827041003026288,0.0,18,18,19,21,22,22,22,22,22,22,22
+GET,/questionnaire/block-383/,10,0,27,27.59334621950984,24.56335793249309,34.02357897721231,103527.0,0.16827041003026288,0.0,27,27,29,32,34,34,34,34,34,34,34
+POST,/questionnaire/block-383/,10,0,16,16.820050589740276,14.71883396152407,22.712663048878312,277.0,0.16827041003026288,0.0,16,16,18,20,23,23,23,23,23,23,23
+GET,/questionnaire/block-4616/,10,0,27,28.000582312233746,25.313068996183574,34.9235039902851,103887.0,0.16827041003026288,0.0,27,27,30,30,35,35,35,35,35,35,35
+POST,/questionnaire/block-4616/,10,0,16,17.99908650573343,15.175107982940972,26.206664042547345,239.0,0.16827041003026288,0.0,18,19,19,20,26,26,26,26,26,26,26
+GET,/questionnaire/block-4952/,10,0,28,34.04230660526082,26.330921100452542,84.26787203643471,104489.0,0.16827041003026288,0.0,29,30,31,32,84,84,84,84,84,84,84
+POST,/questionnaire/block-4952/,10,0,16,16.96460577659309,15.445746947079897,19.629093003459275,239.0,0.16827041003026288,0.0,17,17,18,19,20,20,20,20,20,20,20
+GET,/questionnaire/block-4953/,10,0,29,31.004790775477886,27.259176946245134,47.45357600040734,106833.0,0.16827041003026288,0.0,29,29,30,35,47,47,47,47,47,47,47
+POST,/questionnaire/block-4953/,10,0,17,19.734478509053588,15.719956019893289,29.1489859810099,237.0,0.16827041003026288,0.0,17,18,23,27,29,29,29,29,29,29,29
+GET,/questionnaire/companies/add-company/?previous=any-companies-or-branches,10,0,27,27.450701699126512,25.360413012094796,33.92333001829684,103154.0,0.16827041003026288,0.0,27,27,27,29,34,34,34,34,34,34,34
+POST,/questionnaire/companies/add-company/?previous=any-companies-or-branches,10,0,16,16.604762407951057,15.842041000723839,18.9445250434801,307.0,0.16827041003026288,0.0,16,17,17,17,19,19,19,19,19,19,19
+GET,/questionnaire/companies/{id}/companies-repeating-block-1/,10,0,30,29.946386290248483,28.417859924957156,31.86855895910412,105200.0,0.16827041003026288,0.0,30,30,30,31,32,32,32,32,32,32,32
+POST,/questionnaire/companies/{id}/companies-repeating-block-1/,10,0,17,17.949695198331028,16.497505945153534,20.655443891882896,307.0,0.16827041003026288,0.0,18,18,19,19,21,21,21,21,21,21,21
+GET,/questionnaire/companies/{id}/companies-repeating-block-2/,10,0,28,29.13420789409429,26.86727698892355,33.08491606730968,106197.0,0.16827041003026288,0.0,29,29,30,31,33,33,33,33,33,33,33
+POST,/questionnaire/companies/{id}/companies-repeating-block-2/,10,0,17,17.478320689406246,15.56993497069925,21.750068990513682,281.0,0.16827041003026288,0.0,17,18,18,19,22,22,22,22,22,22,22
+GET,/questionnaire/currency-total-playback-1/,10,0,26,25.991786795202643,24.673041072674096,27.197445975616574,105447.0,0.16827041003026288,0.0,26,26,27,27,27,27,27,27,27,27,27
+POST,/questionnaire/currency-total-playback-1/,10,0,15,15.363235981203616,14.145056018605828,18.488502013497055,271.0,0.16827041003026288,0.0,15,15,16,16,18,18,18,18,18,18,18
+GET,/questionnaire/distance-calculated-summary-1/,10,0,26,26.958028390072286,25.212451932020485,33.91596698202193,107231.0,0.16827041003026288,0.0,26,26,27,29,34,34,34,34,34,34,34
+POST,/questionnaire/distance-calculated-summary-1/,10,0,15,17.041597503703088,14.112673001363873,34.61689094547182,273.0,0.16827041003026288,0.0,15,15,15,17,35,35,35,35,35,35,35
+GET,/questionnaire/distance-calculated-summary-2/,10,0,26,27.42138138273731,25.336914928629994,31.599247944541276,107111.0,0.16827041003026288,0.0,26,28,29,30,32,32,32,32,32,32,32
+POST,/questionnaire/distance-calculated-summary-2/,10,0,16,16.347184986807406,14.384760987013578,21.528469980694354,273.0,0.16827041003026288,0.0,16,17,17,17,22,22,22,22,22,22,22
+GET,/questionnaire/distance-grand-calculated-summary/,10,0,26,27.186598512344062,25.21088405046612,35.17036803532392,106649.0,0.16827041003026288,0.0,26,27,27,28,35,35,35,35,35,35,35
+POST,/questionnaire/distance-grand-calculated-summary/,10,0,15,15.215421572793275,14.440665021538734,16.188485897146165,281.0,0.16827041003026288,0.0,15,15,16,16,16,16,16,16,16,16,16
+GET,/questionnaire/fourth-number-block/,10,0,28,28.731524804607034,26.62210096605122,32.096863025799394,104031.0,0.16827041003026288,0.0,28,29,30,31,32,32,32,32,32,32,32
+POST,/questionnaire/fourth-number-block/,10,0,17,17.6956680836156,16.26275002490729,21.105890977196395,277.0,0.16827041003026288,0.0,17,17,19,20,21,21,21,21,21,21,21
+GET,/questionnaire/grand-calculated-summary-first-number-block/,10,0,27,28.092170681338757,25.621325941756368,32.35362295527011,103212.0,0.16827041003026288,0.0,28,28,29,29,32,32,32,32,32,32,32
+POST,/questionnaire/grand-calculated-summary-first-number-block/,10,0,17,18.501907016616315,16.26997604034841,22.67963602207601,307.0,0.16827041003026288,0.0,18,19,20,21,23,23,23,23,23,23,23
+GET,/questionnaire/grand-calculated-summary-second-number-block/,10,0,27,27.77743588667363,26.732837897725403,28.838922968134284,104109.0,0.16827041003026288,0.0,28,28,29,29,29,29,29,29,29,29,29
+POST,/questionnaire/grand-calculated-summary-second-number-block/,10,0,17,17.26736529963091,16.129455994814634,19.050556933507323,277.0,0.16827041003026288,0.0,17,17,19,19,19,19,19,19,19,19,19
+GET,/questionnaire/introduction-block/,10,0,27,28.128321992699057,24.617624934762716,36.184969008900225,102921.0,0.16827041003026288,0.0,28,28,30,30,36,36,36,36,36,36,36
+POST,/questionnaire/introduction-block/,10,0,16,16.819810320157558,14.043923001736403,19.904726999811828,237.0,0.16827041003026288,0.0,17,18,18,20,20,20,20,20,20,20,20
+GET,/questionnaire/list-collector/,20,0,29,28.637125651584938,26.174005935899913,32.87482797168195,106126.0,0.33654082006052577,0.0,29,29,31,31,33,33,33,33,33,33,33
+POST,/questionnaire/list-collector/,20,0,16,17.133900639601052,15.025158994831145,21.707257023081183,252.0,0.33654082006052577,0.0,16,17,18,20,21,22,22,22,22,22,22
+GET,/questionnaire/number-calculated-summary-1/,10,0,25.06849798373878,26.762340066488832,25.06849798373878,36.39646898955107,107174.0,0.16827041003026288,0.0,26,26,26,27,36,36,36,36,36,36,36
+POST,/questionnaire/number-calculated-summary-1/,10,0,16,15.766054100822657,14.917496009729803,16.795122995972633,305.0,0.16827041003026288,0.0,16,16,16,17,17,17,17,17,17,17,17
+GET,/questionnaire/number-calculated-summary-2/,10,0,28,29.673091298900545,24.701099027879536,43.17428101785481,107086.0,0.16827041003026288,0.0,29,31,32,32,43,43,43,43,43,43,43
+POST,/questionnaire/number-calculated-summary-2/,10,0,17,17.022436810657382,15.20423498004675,21.666259039193392,285.0,0.16827041003026288,0.0,17,17,17,19,22,22,22,22,22,22,22
+GET,/questionnaire/number-grand-calculated-summary/,10,0,26,26.58855898771435,25.433333939872682,28.5050020320341,107421.0,0.16827041003026288,0.0,27,27,27,27,29,29,29,29,29,29,29
+POST,/questionnaire/number-grand-calculated-summary/,10,0,15,15.796137298457325,14.5332160172984,17.235978972166777,253.0,0.16827041003026288,0.0,16,16,17,17,17,17,17,17,17,17,17
+GET,/questionnaire/people/add-person/,10,0,29,28.47801949828863,26.244669104926288,31.142375897616148,103536.0,0.16827041003026288,0.0,29,29,30,30,31,31,31,31,31,31,31
+POST,/questionnaire/people/add-person/,10,0,16,17.800712818279862,15.090629109181464,24.46487091947347,247.0,0.16827041003026288,0.0,18,18,20,20,24,24,24,24,24,24,24
+GET,/questionnaire/people/{id}/add-or-edit-primary-person/,10,0,28,28.526184894144535,25.846129981800914,31.753687071613967,103388.0,0.16827041003026288,0.0,29,29,30,31,32,32,32,32,32,32,32
+POST,/questionnaire/people/{id}/add-or-edit-primary-person/,10,0,15,16.360942495521158,14.966896967962384,20.22734296042472,247.0,0.16827041003026288,0.0,16,16,17,18,20,20,20,20,20,20,20
+GET,/questionnaire/people/{id}/currency-total-playback-2/,20,0,26,27.140947157749906,24.882465018890798,35.9009790699929,107510.0,0.33654082006052577,0.0,26,27,28,28,33,36,36,36,36,36,36
+POST,/questionnaire/people/{id}/currency-total-playback-2/,20,0,15,15.932991093723103,14.336524996906519,27.647967101074755,301.0,0.33654082006052577,0.0,15,16,16,16,20,28,28,28,28,28,28
+GET,/questionnaire/people/{id}/grand-calculated-summary-third-number-block/,20,0,26,28.15191480331123,25.683920015580952,35.238586948253214,104082.0,0.33654082006052577,0.0,27,29,30,30,34,35,35,35,35,35,35
+POST,/questionnaire/people/{id}/grand-calculated-summary-third-number-block/,20,0,17,18.63090189290233,15.944191021844745,27.298807981424034,297.0,0.33654082006052577,0.0,17,18,20,20,25,27,27,27,27,27,27
+GET,/questionnaire/people/{id}/mutually-exclusive-checkbox/,20,0,29,30.849684780696407,27.999941958114505,46.14933708216995,106482.0,0.33654082006052577,0.0,29,30,32,32,38,46,46,46,46,46,46
+POST,/questionnaire/people/{id}/mutually-exclusive-checkbox/,20,0,17,18.320259114261717,16.936696018092334,25.426201056689024,281.0,0.33654082006052577,0.0,17,18,18,18,22,25,25,25,25,25,25
+GET,/questionnaire/people/{id}/set-min-max-block/,20,0,28,30.380786635214463,27.523183962330222,40.01657501794398,105566.0,0.33654082006052577,0.0,28,31,34,34,37,40,40,40,40,40,40
+POST,/questionnaire/people/{id}/set-min-max-block/,20,0,18,19.674770248821005,16.968537936918437,25.762334000319242,303.0,0.33654082006052577,0.0,18,21,22,23,24,26,26,26,26,26,26
+GET,/questionnaire/primary-person-list-collector/,10,0,28,28.62796471454203,26.188035029917955,32.09985001012683,102963.0,0.16827041003026288,0.0,28,29,30,32,32,32,32,32,32,32,32
+POST,/questionnaire/primary-person-list-collector/,10,0,16,17.051099834498018,15.541013097390532,19.65773105621338,299.0,0.16827041003026288,0.0,16,17,19,19,20,20,20,20,20,20,20
+GET,/questionnaire/responsible-party-business/,10,0,27,28.190073277801275,25.525647099129856,35.90523498132825,104384.0,0.16827041003026288,0.0,27,28,29,31,36,36,36,36,36,36,36
+POST,/questionnaire/responsible-party-business/,10,0,19,19.311857991851866,16.27724792342633,24.056772934272885,271.0,0.16827041003026288,0.0,20,20,20,21,24,24,24,24,24,24,24
+GET,/questionnaire/responsible-party/,10,0,28,28.231770684942603,26.809547911398113,30.833784956485033,103237.0,0.16827041003026288,0.0,28,29,29,30,31,31,31,31,31,31,31
+POST,/questionnaire/responsible-party/,10,0,17,18.198419269174337,15.82070195581764,21.607434027828276,269.0,0.16827041003026288,0.0,18,19,20,21,22,22,22,22,22,22,22
+GET,/questionnaire/second-number-block/,10,0,26,27.04410991864279,25.441315956413746,33.1697630463168,103676.0,0.16827041003026288,0.0,27,27,27,28,33,33,33,33,33,33,33
+POST,/questionnaire/second-number-block/,10,0,16,16.45449810894206,15.262449043802917,18.231995054520667,269.0,0.16827041003026288,0.0,16,17,17,17,18,18,18,18,18,18,18
+GET,/questionnaire/sections/calculated-summary-section/{id}/,20,0,28,28.50581799284555,26.30755095742643,32.931374036706984,111885.5,0.33654082006052577,0.0,28,29,30,31,32,33,33,33,33,33,33
+POST,/questionnaire/sections/calculated-summary-section/{id}/,20,0,12,12.543371896026656,10.795803042128682,17.2119700582698,319.0,0.33654082006052577,0.0,12,13,13,13,16,17,17,17,17,17,17
+GET,/questionnaire/sections/grand-calculated-summary-section-1/,10,0,27,26.878911699168384,24.99825495760888,29.059723019599915,109526.0,0.16827041003026288,0.0,27,28,28,29,29,29,29,29,29,29,29
+POST,/questionnaire/sections/grand-calculated-summary-section-1/,10,0,12,11.894893401768059,10.908885975368321,12.971151969395578,255.0,0.16827041003026288,0.0,12,12,12,13,13,13,13,13,13,13,13
+GET,/questionnaire/sections/questions-section/,10,0,25,26.00394559558481,24.42492393311113,30.237193102948368,106134.0,0.16827041003026288,0.0,26,26,27,28,30,30,30,30,30,30,30
+POST,/questionnaire/sections/questions-section/,10,0,11,11.733577097766101,10.506162070669234,14.970474992878735,333.0,0.16827041003026288,0.0,11,12,12,14,15,15,15,15,15,15,15
+GET,/questionnaire/sections/section-businesses/,10,0,26,30.75341918738559,24.09891993738711,71.59794098697603,106389.0,0.16827041003026288,0.0,26,26,29,30,72,72,72,72,72,72,72
+POST,/questionnaire/sections/section-businesses/,10,0,12,11.757753603160381,10.716030024923384,14.510384993627667,217.0,0.16827041003026288,0.0,12,12,12,12,15,15,15,15,15,15,15
+GET,/questionnaire/sections/section-companies/,10,0,32,34.451518091373146,27.63120597228408,62.01387196779251,118468.0,0.16827041003026288,0.0,32,34,34,36,62,62,62,62,62,62,62
+POST,/questionnaire/sections/section-companies/,10,0,12,12.594723678193986,10.99176099523902,16.99272694531828,217.0,0.16827041003026288,0.0,12,13,13,13,17,17,17,17,17,17,17
+GET,/questionnaire/skip-first-block/,10,0,27,27.238223515450954,25.755911017768085,29.09757010638714,103122.0,0.16827041003026288,0.0,27,27,28,29,29,29,29,29,29,29,29
+POST,/questionnaire/skip-first-block/,10,0,16,16.659896483179182,15.223064925521612,21.316227968782187,257.0,0.16827041003026288,0.0,16,17,17,17,21,21,21,21,21,21,21
+GET,/questionnaire/third-number-block/,10,0,27,28.251892584376037,25.959860999137163,32.420119969174266,103176.0,0.16827041003026288,0.0,28,28,29,30,32,32,32,32,32,32,32
+POST,/questionnaire/third-number-block/,10,0,17,18.482219707220793,16.706367023289204,20.714276004582644,257.0,0.16827041003026288,0.0,19,19,20,20,21,21,21,21,21,21,21
+GET,/session,10,0,160.0,159.4516820157878,154.2557330103591,179.00221806485206,217.0,0.16827041003026288,0.0,160,160,160,160,180,180,180,180,180,180,180
+GET,/submitted/download-pdf,10,0,2200.0,2994.80036071036,2120.7086900249124,4710.857865982689,20775.1,0.16827041003026288,0.0,2800,3700,3900,4100,4700,4700,4700,4700,4700,4700,4700
+GET,/submitted/feedback/send,10,0,28,29.466230585239828,27.52751694060862,34.44048692472279,106342.0,0.16827041003026288,0.0,28,29,31,33,34,34,34,34,34,34,34
+POST,/submitted/feedback/send,9,0,120.0,121.27008144226339,118.64531796891242,124.89975499920547,235.0,0.1514433690272366,0.0,120,120,120,120,120,120,120,120,120,120,120
+GET,/submitted/feedback/sent,9,0,24,24.43193111361729,23.565704934298992,26.932036969810724,102849.0,0.1514433690272366,0.0,24,24,24,27,27,27,27,27,27,27,27
+GET,/submitted/thank-you/,29,0,27,27.370365782127042,24.496339028701186,33.864647964946926,103505.0,0.48798418908776237,0.0,27,29,29,29,32,33,34,34,34,34,34
+GET,/submitted/view-response/,10,0,39,39.50409650569782,37.48978895600885,44.821103918366134,142934.0,0.16827041003026288,0.0,39,40,40,41,45,45,45,45,45,45,45
+,Aggregated,1097,0,25,53.09622206862289,10.506162070669234,4710.857865982689,54728.734731084776,18.45926398031984,0.0,25,27,28,29,32,38,130,180,4100,4700,4700
diff --git a/tests/mock_stats/2024-02-12T03:09:41/mock_output_stats.csv b/tests/mock_stats/2024-02-12T03:09:41/mock_output_stats.csv
new file mode 100644
index 00000000..6aa2b23b
--- /dev/null
+++ b/tests/mock_stats/2024-02-12T03:09:41/mock_output_stats.csv
@@ -0,0 +1,96 @@
+Type,Name,Request Count,Failure Count,Median Response Time,Average Response Time,Min Response Time,Max Response Time,Average Content Size,Requests/s,Failures/s,50%,66%,75%,80%,90%,95%,98%,99%,99.9%,99.99%,100%
+GET,/questionnaire,11,0,11,12.023728647777302,10.659591993317008,16.86776103451848,255.0,0.18483579903286773,0.0,11,12,12,12,13,17,17,17,17,17,17
+GET,/questionnaire/,20,0,27,27.822096494492143,25.893958983942866,31.30856400821358,124766.5,0.33606508915066857,0.0,27,28,29,30,30,31,31,31,31,31,31
+POST,/questionnaire/,20,0,13,70.47625207924284,11.11529697664082,185.51862495951355,250.0,0.33606508915066857,0.0,120,120,120,120,130,190,190,190,190,190,190
+GET,/questionnaire/any-businesses-or-branches/,10,0,27,27.335992793086916,25.67505999468267,30.2064890274778,104244.0,0.16803254457533429,0.0,27,28,28,29,30,30,30,30,30,30,30
+POST,/questionnaire/any-businesses-or-branches/,10,0,17,17.933263699524105,16.102439956739545,21.13697910681367,273.0,0.16803254457533429,0.0,17,18,20,21,21,21,21,21,21,21,21
+GET,/questionnaire/any-companies-or-branches/,10,0,27,27.461823297198862,25.768079911358654,29.576422995887697,104222.0,0.16803254457533429,0.0,28,28,29,29,30,30,30,30,30,30,30
+POST,/questionnaire/any-companies-or-branches/,10,0,16,16.89466502284631,15.855325968004763,20.083628012798727,331.0,0.16803254457533429,0.0,17,17,17,18,20,20,20,20,20,20,20
+GET,/questionnaire/any-other-companies-or-branches/,10,0,29,33.64940480096266,27.694375021383166,75.47839696053416,106951.0,0.16803254457533429,0.0,29,29,30,33,75,75,75,75,75,75,75
+POST,/questionnaire/any-other-companies-or-branches/,10,0,17,17.985032498836517,16.167290974408388,21.366916014812887,269.0,0.16803254457533429,0.0,17,19,20,21,21,21,21,21,21,21,21
+GET,/questionnaire/any-other-trading-details/,10,0,27,27.282707183621824,25.096507975831628,30.944325029850006,103072.0,0.16803254457533429,0.0,27,28,28,29,31,31,31,31,31,31,31
+POST,/questionnaire/any-other-trading-details/,10,0,16,16.9910132070072,15.270124073140323,19.96381103526801,271.0,0.16803254457533429,0.0,17,17,18,19,20,20,20,20,20,20,20
+GET,/questionnaire/block-379/,11,0,28,29.262934398668055,26.443929062224925,40.021431050263345,103881.0,0.18483579903286773,0.0,28,29,30,30,30,40,40,40,40,40,40
+POST,/questionnaire/block-379/,11,0,16,16.81917956606908,15.54040506016463,19.282479071989655,237.0,0.18483579903286773,0.0,16,17,18,18,18,19,19,19,19,19,19
+GET,/questionnaire/block-381/,11,0,28,29.239173184826292,26.50415094103664,33.66381209343672,107705.0,0.18483579903286773,0.0,28,31,31,31,32,34,34,34,34,34,34
+POST,/questionnaire/block-381/,11,0,18,18.82509716828777,16.460436978377402,24.544372921809554,239.0,0.18483579903286773,0.0,18,19,20,20,21,25,25,25,25,25,25
+GET,/questionnaire/block-383/,11,0,27,27.14749553706497,24.845288950018585,32.74102695286274,103527.0,0.18483579903286773,0.0,27,28,28,28,29,33,33,33,33,33,33
+POST,/questionnaire/block-383/,11,0,16,16.259432188235223,14.399928972125053,19.27156699821353,277.0,0.18483579903286773,0.0,16,17,17,17,18,19,19,19,19,19,19
+GET,/questionnaire/block-4616/,11,0,26,28.267299618825994,25.36962495651096,39.74363300949335,103887.0,0.18483579903286773,0.0,26,29,30,30,30,40,40,40,40,40,40
+POST,/questionnaire/block-4616/,11,0,17,17.35131273215467,15.40341298095882,20.49127104692161,239.0,0.18483579903286773,0.0,17,18,19,19,20,20,20,20,20,20,20
+GET,/questionnaire/block-4952/,11,0,29,28.558125349015675,26.32978104520589,33.233374007977545,104489.0,0.18483579903286773,0.0,29,30,30,30,31,33,33,33,33,33,33
+POST,/questionnaire/block-4952/,11,0,16,16.94978934458711,15.46619099099189,20.28111508116126,239.0,0.18483579903286773,0.0,16,17,18,18,20,20,20,20,20,20,20
+GET,/questionnaire/block-4953/,11,0,28,30.14042670838535,26.912695029750466,43.50681998766959,106833.0,0.18483579903286773,0.0,28,29,31,31,35,44,44,44,44,44,44
+POST,/questionnaire/block-4953/,11,0,18,17.694243822585452,15.729359001852572,21.3465120177716,237.0,0.18483579903286773,0.0,18,19,19,19,19,21,21,21,21,21,21
+GET,/questionnaire/companies/add-company/?previous=any-companies-or-branches,10,0,27,28.427368181291968,25.55144694633782,36.88562905881554,103154.0,0.16803254457533429,0.0,28,29,30,30,37,37,37,37,37,37,37
+POST,/questionnaire/companies/add-company/?previous=any-companies-or-branches,10,0,17,16.946173075120896,15.525153954513371,21.091613918542862,307.0,0.16803254457533429,0.0,17,17,17,18,21,21,21,21,21,21,21
+GET,/questionnaire/companies/{id}/companies-repeating-block-1/,10,0,31,31.832728488370776,28.245499939657748,35.611258004792035,105200.0,0.16803254457533429,0.0,32,33,34,34,36,36,36,36,36,36,36
+POST,/questionnaire/companies/{id}/companies-repeating-block-1/,10,0,18,18.056010187137872,16.234264941886067,22.1514260629192,307.0,0.16803254457533429,0.0,18,18,18,19,22,22,22,22,22,22,22
+GET,/questionnaire/companies/{id}/companies-repeating-block-2/,10,0,28,28.515839506872,27.04095304943621,31.27285896334797,106197.0,0.16803254457533429,0.0,29,29,29,29,31,31,31,31,31,31,31
+POST,/questionnaire/companies/{id}/companies-repeating-block-2/,10,0,17,26.59225690877065,15.712260967120528,71.17931498214602,281.0,0.16803254457533429,0.0,17,17,17,60,71,71,71,71,71,71,71
+GET,/questionnaire/currency-total-playback-1/,11,0,25,25.79123854891143,24.590146029368043,27.532499050721526,105447.0,0.18483579903286773,0.0,25,27,27,27,27,28,28,28,28,28,28
+POST,/questionnaire/currency-total-playback-1/,11,0,15,15.217405172403563,14.311515958979726,16.584786935709417,271.0,0.18483579903286773,0.0,15,16,16,16,16,17,17,17,17,17,17
+GET,/questionnaire/distance-calculated-summary-1/,10,0,25,25.557694514282048,24.973108083941042,26.436373009346426,107231.0,0.16803254457533429,0.0,26,26,26,26,26,26,26,26,26,26,26
+POST,/questionnaire/distance-calculated-summary-1/,10,0,15,14.863740419968963,14.354609069414437,15.64662007149309,273.0,0.16803254457533429,0.0,15,15,15,16,16,16,16,16,16,16,16
+GET,/questionnaire/distance-calculated-summary-2/,10,0,26,26.89779318170622,25.148609071038663,29.60843697655946,107111.0,0.16803254457533429,0.0,27,27,28,29,30,30,30,30,30,30,30
+POST,/questionnaire/distance-calculated-summary-2/,10,0,15,15.710299997590482,14.467647997662425,18.956255982629955,273.0,0.16803254457533429,0.0,15,15,17,17,19,19,19,19,19,19,19
+GET,/questionnaire/distance-grand-calculated-summary/,10,0,27,27.285301266238093,24.897646042518318,31.329549965448678,106649.0,0.16803254457533429,0.0,27,28,29,29,31,31,31,31,31,31,31
+POST,/questionnaire/distance-grand-calculated-summary/,10,0,15,15.699037618469447,14.237706083804369,18.080833018757403,281.0,0.16803254457533429,0.0,16,16,17,17,18,18,18,18,18,18,18
+GET,/questionnaire/fourth-number-block/,10,0,28,28.12962430762127,26.47744701243937,30.249361065216362,104031.0,0.16803254457533429,0.0,28,29,29,30,30,30,30,30,30,30,30
+POST,/questionnaire/fourth-number-block/,10,0,17,17.136811593081802,15.666393912397325,19.444087985903025,277.0,0.16803254457533429,0.0,17,17,18,18,19,19,19,19,19,19,19
+GET,/questionnaire/grand-calculated-summary-first-number-block/,10,0,26,26.822160417214036,25.580993969924748,29.98897701036185,103212.0,0.16803254457533429,0.0,26,26,28,29,30,30,30,30,30,30,30
+POST,/questionnaire/grand-calculated-summary-first-number-block/,10,0,16,16.614362399559468,15.781100024469197,18.95773597061634,307.0,0.16803254457533429,0.0,16,17,17,17,19,19,19,19,19,19,19
+GET,/questionnaire/grand-calculated-summary-second-number-block/,10,0,27,27.427900524344295,26.261829072609544,30.83911002613604,104109.0,0.16803254457533429,0.0,27,27,28,29,31,31,31,31,31,31,31
+POST,/questionnaire/grand-calculated-summary-second-number-block/,10,0,16,17.074522899929434,15.845473972149193,20.658474997617304,277.0,0.16803254457533429,0.0,17,17,17,18,21,21,21,21,21,21,21
+GET,/questionnaire/introduction-block/,11,0,26,27.6235130996528,24.181190063245595,44.168516993522644,102921.0,0.18483579903286773,0.0,26,27,28,28,28,44,44,44,44,44,44
+POST,/questionnaire/introduction-block/,11,0,15,15.390935459766876,13.959693955257535,17.118699033744633,237.0,0.18483579903286773,0.0,15,15,17,17,17,17,17,17,17,17,17
+GET,/questionnaire/list-collector/,22,0,28,29.472088213713672,25.900017004460096,54.458655999042094,106126.0,0.36967159806573546,0.0,28,29,30,30,31,33,54,54,54,54,54
+POST,/questionnaire/list-collector/,22,0,17,17.382982737300072,14.940560911782086,22.597166011109948,252.0,0.36967159806573546,0.0,17,18,19,19,19,19,23,23,23,23,23
+GET,/questionnaire/number-calculated-summary-1/,10,0,25,25.545710476581007,24.545509018935263,27.720846934244037,107174.0,0.16803254457533429,0.0,25,26,26,26,28,28,28,28,28,28,28
+POST,/questionnaire/number-calculated-summary-1/,10,0,15,16.258963895961642,14.39303602091968,20.91054094489664,305.0,0.16803254457533429,0.0,15,16,19,19,21,21,21,21,21,21,21
+GET,/questionnaire/number-calculated-summary-2/,10,0,26,26.225437386892736,24.55532201565802,30.877685989253223,107086.0,0.16803254457533429,0.0,26,26,26,27,31,31,31,31,31,31,31
+POST,/questionnaire/number-calculated-summary-2/,10,0,16,16.186045680660754,14.696788974106312,18.164873938076198,285.0,0.16803254457533429,0.0,16,16,17,17,18,18,18,18,18,18,18
+GET,/questionnaire/number-grand-calculated-summary/,10,0,27,27.126433479133993,25.356811936944723,31.230952008627355,107421.0,0.16803254457533429,0.0,27,27,27,29,31,31,31,31,31,31,31
+POST,/questionnaire/number-grand-calculated-summary/,10,0,15,15.870061598252505,14.255363959819078,18.072025035507977,253.0,0.16803254457533429,0.0,16,16,17,18,18,18,18,18,18,18,18
+GET,/questionnaire/people/add-person/,11,0,27,28.25361707205461,25.949920993298292,33.061775960959494,103536.0,0.18483579903286773,0.0,27,29,31,31,31,33,33,33,33,33,33
+POST,/questionnaire/people/add-person/,11,0,16,15.967307083139366,14.423005981370807,18.14297295641154,247.0,0.18483579903286773,0.0,16,16,17,17,18,18,18,18,18,18,18
+GET,/questionnaire/people/{id}/add-or-edit-primary-person/,11,0,27,27.788131434301082,25.6284800125286,31.455648015253246,103388.0,0.18483579903286773,0.0,27,29,30,30,31,31,31,31,31,31,31
+POST,/questionnaire/people/{id}/add-or-edit-primary-person/,11,0,16,16.341146367432717,14.625110081396997,19.73554096184671,247.0,0.18483579903286773,0.0,16,17,17,17,18,20,20,20,20,20,20
+GET,/questionnaire/people/{id}/currency-total-playback-2/,22,0,26,26.033330221914433,24.840443045832217,29.136425931937993,107510.0,0.36967159806573546,0.0,26,26,26,27,27,28,29,29,29,29,29
+POST,/questionnaire/people/{id}/currency-total-playback-2/,22,0,15,14.866983824917538,14.102965011261404,16.571006970480084,301.0,0.36967159806573546,0.0,15,15,15,15,16,16,17,17,17,17,17
+GET,/questionnaire/people/{id}/grand-calculated-summary-third-number-block/,22,0,27,27.216435986867342,25.53047996480018,31.706031993962824,104082.0,0.36967159806573546,0.0,27,27,28,28,29,31,32,32,32,32,32
+POST,/questionnaire/people/{id}/grand-calculated-summary-third-number-block/,22,0,17,17.354623451617293,16.052633989602327,19.64696403592825,297.0,0.36967159806573546,0.0,17,18,18,18,19,20,20,20,20,20,20
+GET,/questionnaire/people/{id}/mutually-exclusive-checkbox/,22,0,29,29.089198961049657,28.18134892731905,32.69398398697376,106482.0,0.36967159806573546,0.0,29,29,30,30,30,30,33,33,33,33,33
+POST,/questionnaire/people/{id}/mutually-exclusive-checkbox/,22,0,17,17.72611483995041,16.758193029090762,19.687114981934428,281.0,0.36967159806573546,0.0,18,18,18,19,19,19,20,20,20,20,20
+GET,/questionnaire/people/{id}/set-min-max-block/,22,0,28,28.6779242715883,27.23730495199561,32.71366294939071,105566.0,0.36967159806573546,0.0,28,29,30,30,31,31,33,33,33,33,33
+POST,/questionnaire/people/{id}/set-min-max-block/,22,0,18,18.47250515658578,16.97343692649156,26.118434965610504,303.0,0.36967159806573546,0.0,18,18,18,19,20,21,26,26,26,26,26
+GET,/questionnaire/primary-person-list-collector/,11,0,28,27.347312446429648,25.326667935587466,30.411778017878532,102963.0,0.18483579903286773,0.0,28,28,28,28,29,30,30,30,30,30,30
+POST,/questionnaire/primary-person-list-collector/,11,0,16,16.617663627998397,14.992184937000275,19.398897071368992,299.0,0.18483579903286773,0.0,16,17,18,18,18,19,19,19,19,19,19
+GET,/questionnaire/responsible-party-business/,10,0,27,27.359139104373753,25.207280996255577,31.138586928136647,104384.0,0.16803254457533429,0.0,27,28,29,29,31,31,31,31,31,31,31
+POST,/questionnaire/responsible-party-business/,10,0,18,20.860162819735706,15.71378402877599,43.15320204477757,271.0,0.16803254457533429,0.0,19,19,21,22,43,43,43,43,43,43,43
+GET,/questionnaire/responsible-party/,10,0,27,27.0687646814622,25.491180014796555,30.16620094422251,103237.0,0.16803254457533429,0.0,27,28,28,28,30,30,30,30,30,30,30
+POST,/questionnaire/responsible-party/,10,0,16,16.51082979515195,15.771970967762172,17.900313017889857,269.0,0.16803254457533429,0.0,16,16,17,18,18,18,18,18,18,18,18
+GET,/questionnaire/second-number-block/,11,0,26,26.420731997032735,25.023204972967505,28.467142023146152,103676.0,0.18483579903286773,0.0,26,27,27,27,28,28,28,28,28,28,28
+POST,/questionnaire/second-number-block/,11,0,16,16.451824636367913,15.505062998272479,19.18758498504758,269.0,0.18483579903286773,0.0,16,17,17,17,18,19,19,19,19,19,19
+GET,/questionnaire/sections/calculated-summary-section/{id}/,22,0,27,27.969528079583224,26.076174923218787,41.80481797084212,111885.5,0.36967159806573546,0.0,27,27,27,28,30,32,42,42,42,42,42
+POST,/questionnaire/sections/calculated-summary-section/{id}/,22,0,11,11.792160538871856,10.615107952617109,14.324543997645378,319.0,0.36967159806573546,0.0,11,12,12,14,14,14,14,14,14,14,14
+GET,/questionnaire/sections/grand-calculated-summary-section-1/,10,0,26,30.542609677650034,24.657204980030656,62.56481597665697,109526.0,0.16803254457533429,0.0,27,28,30,32,63,63,63,63,63,63,63
+POST,/questionnaire/sections/grand-calculated-summary-section-1/,10,0,11,11.934103991370648,10.609688004478812,15.364074963144958,255.0,0.16803254457533429,0.0,12,12,13,13,15,15,15,15,15,15,15
+GET,/questionnaire/sections/questions-section/,11,0,25,25.554111174477097,24.403361952863634,29.54151900485158,106134.0,0.18483579903286773,0.0,25,26,26,26,27,30,30,30,30,30,30
+POST,/questionnaire/sections/questions-section/,11,0,12,11.84888310010799,10.717255063354969,13.678024988621473,333.0,0.18483579903286773,0.0,12,12,13,13,13,14,14,14,14,14,14
+GET,/questionnaire/sections/section-businesses/,10,0,26,25.659533601719886,23.98305502720177,28.549611917696893,106389.0,0.16803254457533429,0.0,26,26,26,27,29,29,29,29,29,29,29
+POST,/questionnaire/sections/section-businesses/,10,0,11,11.572326358873397,10.67960704676807,12.709217960946262,217.0,0.16803254457533429,0.0,12,12,12,12,13,13,13,13,13,13,13
+GET,/questionnaire/sections/section-companies/,10,0,28,28.882129304111004,27.452037087641656,32.60107210371643,118468.0,0.16803254457533429,0.0,29,30,30,30,33,33,33,33,33,33,33
+POST,/questionnaire/sections/section-companies/,10,0,12,11.53269688365981,10.884089046157897,12.649790034629405,217.0,0.16803254457533429,0.0,12,12,12,12,13,13,13,13,13,13,13
+GET,/questionnaire/skip-first-block/,11,0,27,27.013281201520428,25.26564395520836,29.000431997701526,103122.0,0.18483579903286773,0.0,27,28,28,28,28,29,29,29,29,29,29
+POST,/questionnaire/skip-first-block/,11,0,16,16.28173692998561,15.399846946820617,17.30755204334855,257.0,0.18483579903286773,0.0,16,17,17,17,17,17,17,17,17,17,17
+GET,/questionnaire/third-number-block/,10,0,28,35.814188595395535,25.51633003167808,110.29883194714785,103176.0,0.16803254457533429,0.0,28,29,29,29,110,110,110,110,110,110,110
+POST,/questionnaire/third-number-block/,10,0,17,19.61497610900551,15.962035045959055,40.94952798914164,257.0,0.16803254457533429,0.0,17,18,18,20,41,41,41,41,41,41,41
+GET,/session,11,0,160.0,190.8947893693535,152.82216493505985,441.3383030332625,217.0,0.18483579903286773,0.0,160,160,170,170,240,440,440,440,440,440,440
+GET,/submitted/download-pdf,10,0,2100.0,2846.635233727284,2059.2743470333517,4959.792048088275,20850.3,0.16803254457533429,0.0,2200,2300,3900,4800,5000,5000,5000,5000,5000,5000,5000
+GET,/submitted/feedback/send,10,0,28,28.79959971178323,27.299063047394156,31.47279191762209,106342.0,0.16803254457533429,0.0,29,29,30,31,31,31,31,31,31,31,31
+POST,/submitted/feedback/send,10,0,120.0,121.63299061357975,119.44213102106005,123.58560203574598,235.0,0.16803254457533429,0.0,120,120,120,120,120,120,120,120,120,120,120
+GET,/submitted/feedback/sent,10,0,25,29.2888872907497,24.152715923264623,65.0689989561215,102849.0,0.16803254457533429,0.0,26,26,26,27,65,65,65,65,65,65,65
+GET,/submitted/thank-you/,30,0,27,27.305805997457355,24.546172004193068,33.88853895012289,103505.0,0.5040976337260029,0.0,28,28,29,29,30,31,34,34,34,34,34
+GET,/submitted/view-response/,10,0,39,39.77046888321638,37.52195311244577,42.70393692422658,142934.0,0.16803254457533429,0.0,40,40,41,42,43,43,43,43,43,43,43
+,Aggregated,1154,0,25,50.44127832155956,10.609688004478812,4959.792048088275,54590.278162911614,19.39095564399358,0.0,25,27,28,28,30,40,120,240,4800,5000,5000
diff --git a/tests/test_benchmark_stats.py b/tests/test_benchmark_stats.py
index 43fc67c7..cfcd3023 100644
--- a/tests/test_benchmark_stats.py
+++ b/tests/test_benchmark_stats.py
@@ -3,7 +3,9 @@
from scripts.benchmark_stats import BenchmarkStats
from tests.conftest import (
EXPECTED_OUTPUT_MULTIPLE_FOLDERS,
+ EXPECTED_OUTPUT_MULTIPLE_FOLDERS_WITH_PDF,
EXPECTED_OUTPUT_SINGLE_FOLDER,
+ EXPECTED_OUTPUT_SINGLE_FOLDER_WITH_PDF,
)
@@ -12,6 +14,11 @@ def benchmark_stats():
return BenchmarkStats(folder_paths=["./tests/mock_stats/2024-02-07T03:09:41"])
+@pytest.fixture
+def benchmark_stats_pdf():
+ return BenchmarkStats(folder_paths=["./tests/mock_stats/2024-02-09T03:09:41"])
+
+
@pytest.fixture
def benchmark_stats_multiple():
return BenchmarkStats(
@@ -22,12 +29,32 @@ def benchmark_stats_multiple():
)
-def test_formatted_percentiles(benchmark_stats):
- assert EXPECTED_OUTPUT_SINGLE_FOLDER == str(benchmark_stats)
+@pytest.fixture
+def benchmark_stats_multiple_with_pdf():
+ return BenchmarkStats(
+ folder_paths=[
+ "./tests/mock_stats/2024-02-07T03:09:41",
+ "./tests/mock_stats/2024-02-06T03:09:41",
+ "./tests/mock_stats/2024-02-09T03:09:41",
+ "./tests/mock_stats/2024-02-12T03:09:41",
+ ]
+ )
-def test_formatted_percentiles_multiple_folders(benchmark_stats_multiple):
- assert EXPECTED_OUTPUT_MULTIPLE_FOLDERS == str(benchmark_stats_multiple)
+@pytest.mark.parametrize(
+ "benchmark_stats_fixture, expected_result",
+ (
+ ("benchmark_stats", EXPECTED_OUTPUT_SINGLE_FOLDER),
+ ("benchmark_stats_pdf", EXPECTED_OUTPUT_SINGLE_FOLDER_WITH_PDF),
+ ("benchmark_stats_multiple", EXPECTED_OUTPUT_MULTIPLE_FOLDERS),
+ (
+ "benchmark_stats_multiple_with_pdf",
+ EXPECTED_OUTPUT_MULTIPLE_FOLDERS_WITH_PDF,
+ ),
+ ),
+)
+def test_formatted_percentile(benchmark_stats_fixture, expected_result, request):
+ assert str(request.getfixturevalue(benchmark_stats_fixture)) == expected_result
def test_files(benchmark_stats):
@@ -37,7 +64,7 @@ def test_files(benchmark_stats):
def test_percentiles(benchmark_stats):
- assert benchmark_stats.percentiles == {50: 58, 90: 96, 95: 173, 99: 301, 99.9: 477}
+ assert benchmark_stats.percentiles == {50: 58, 90: 97, 95: 174, 99: 301, 99.9: 477}
def test_total_requests(benchmark_stats):
@@ -49,8 +76,45 @@ def test_average_get(benchmark_stats):
def test_average_post(benchmark_stats):
- assert benchmark_stats.average_post == 211
+ assert benchmark_stats.average_post == 212
def test_error_percentage(benchmark_stats):
assert benchmark_stats.error_percentage == 0.0014156285390713476
+
+
+@pytest.mark.parametrize(
+ "benchmark_stats_fixture, percentile_value",
+ (
+ ("benchmark_stats_pdf", 4700),
+ ("benchmark_stats_multiple_with_pdf", 4850),
+ ),
+)
+def test_pdf_percentile(benchmark_stats_fixture, percentile_value, request):
+ assert (
+ request.getfixturevalue(benchmark_stats_fixture)
+ ).average_pdf_percentile == percentile_value
+
+
+def test_non_applicable_pdf_percentile(benchmark_stats):
+ assert benchmark_stats.average_pdf_percentile is None
+
+
+def test_session_percentile(benchmark_stats_pdf):
+ assert benchmark_stats_pdf.average_session_percentile == 180
+
+
+def test_no_pdf_endpoint_formatted_percentile(benchmark_stats):
+ assert (
+ benchmark_stats.formatted_percentile(benchmark_stats.average_pdf_percentile)
+ == "N/A"
+ )
+
+
+def test_formatted_pdf_percentile(benchmark_stats_pdf):
+ assert (
+ benchmark_stats_pdf.formatted_percentile(
+ benchmark_stats_pdf.average_pdf_percentile
+ )
+ == "4700ms"
+ )
diff --git a/tests/test_get_summary.py b/tests/test_get_summary.py
index fdc4a38c..5e69e018 100644
--- a/tests/test_get_summary.py
+++ b/tests/test_get_summary.py
@@ -8,13 +8,16 @@
"---\n"
"Percentile Averages:\n"
"50th: 58ms\n"
- "90th: 99ms\n"
- "95th: 177ms\n"
- "99th: 319ms\n"
+ "90th: 100ms\n"
+ "95th: 178ms\n"
+ "99th: 320ms\n"
"99.9th: 654ms\n"
"---\n"
- "GETs (99th): 384ms\n"
- "POSTs (99th): 245ms\n"
+ "GETs (99th): 385ms\n"
+ "POSTs (99th): 246ms\n"
+ "---\n"
+ "PDF: N/A\n"
+ "Session: 7150ms\n"
"---\n"
"Total Requests: 141,201\n"
"Total Failures: 1\n"
@@ -26,12 +29,14 @@
"**Benchmark Results**
"
"Percentile Averages:
"
"50th: 58ms
"
- "90th: 96ms
"
- "95th: 173ms
"
+ "90th: 97ms
"
+ "95th: 174ms
"
"99th: 301ms
"
"99.9th: 477ms
"
"GETs (99th): 380ms
"
- "POSTs (99th): 211ms
"
+ "POSTs (99th): 212ms
"
+ "PDF: N/A
"
+ "Session: 7600ms
"
"Total Requests: 70,640
"
"Total Failures: 1
"
"Error Percentage: 0.0%
"
diff --git a/tests/test_visualise_results.py b/tests/test_visualise_results.py
index 503f3b51..665a55c0 100644
--- a/tests/test_visualise_results.py
+++ b/tests/test_visualise_results.py
@@ -1,62 +1,194 @@
+from glob import glob
+
import pytest
+from freezegun import freeze_time
from pandas import DataFrame
from pandas.testing import assert_frame_equal
from scripts.get_summary import get_results
-from scripts.visualise_results import GraphGenerationFailed, get_data_frame, plot_data
+from scripts.visualise_results import (
+ GraphGenerationFailed,
+ create_graph,
+ get_additional_metrics_data_frame,
+ get_dataframes,
+ get_performance_data_frame,
+)
-expected_data_frame = DataFrame.from_dict(
- {"DATE": ["2024-02-07"], "50th": [58], "90th": [96], "95th": [173], "99th": [301]}
+expected_performance_dataframe = DataFrame.from_dict(
+ {"DATE": ["2024-02-07"], "50th": [58], "90th": [97], "95th": [174], "99th": [301]}
)
-expected_data_frame_multiple_files = DataFrame.from_dict(
+expected_pdf_session_dataframe = DataFrame.from_dict(
+ {"DATE": ["2024-02-09"], "PDF": [4700], "Session": [180]}
+)
+
+expected_performance_dataframe_consecutive_days = DataFrame.from_dict(
{
"DATE": ["2024-02-07", "2024-02-06"],
"50th": [58, 58],
- "90th": [96, 99],
- "95th": [173, 177],
- "99th": [301, 319],
+ "90th": [97, 100],
+ "95th": [174, 178],
+ "99th": [301, 320],
}
)
+expected_pdf_session_dataframe_consecutive_days = DataFrame.from_dict(
+ {
+ "DATE": ["2024-02-09", "2024-02-12"],
+ "PDF": [4700, 5000],
+ "Session": [180, 440],
+ }
+)
-def test_get_data_frame_single_file(get_results_single_file):
- dataframe = get_data_frame(get_results_single_file)
- assert_frame_equal(dataframe, expected_data_frame)
+expected_performance_dataframe_multiple_days = DataFrame.from_dict(
+ {
+ "DATE": ["2024-02-06", "2024-02-07", "2024-02-09", "2024-02-12"],
+ "50th": [58, 58, 52, 45],
+ "90th": [100, 97, 75, 75],
+ "95th": [178, 174, 76, 79],
+ "99th": [320, 301, 76, 80],
+ }
+)
+
+expected_pdf_session_dataframe_multiple_days = DataFrame.from_dict(
+ {
+ "DATE": ["2024-02-06", "2024-02-07", "2024-02-09", "2024-02-12"],
+ "PDF": [None, None, 4700, 5000],
+ "Session": [7150, 7600, 180, 440],
+ }
+)
+
+dataframes_list = [
+ expected_performance_dataframe_multiple_days,
+ expected_pdf_session_dataframe_multiple_days,
+]
-def test_get_data_frame_multiple_files():
- results = get_results(
- folders=[
- "./tests/mock_stats/2024-02-07T03:09:41",
- "./tests/mock_stats/2024-02-06T03:09:41",
- ]
- )
- dataframe = get_data_frame(results)
- assert_frame_equal(dataframe, expected_data_frame_multiple_files)
+@pytest.mark.parametrize(
+ "results_file_fixture, data_frame_method, expected_result",
+ (
+ (
+ "get_results_single_file",
+ get_performance_data_frame,
+ expected_performance_dataframe,
+ ),
+ (
+ "get_results_single_file_with_pdf_endpoint",
+ get_additional_metrics_data_frame,
+ expected_pdf_session_dataframe,
+ ),
+ ),
+)
+def test_get_data_frame_single_file(
+ results_file_fixture, data_frame_method, expected_result, request
+):
+ dataframe = data_frame_method(request.getfixturevalue(results_file_fixture))
+ assert_frame_equal(dataframe, expected_result)
-def test_plot_data_df(mocker, get_results_single_file):
- dataframe = get_data_frame(get_results_single_file)
+@pytest.mark.parametrize(
+ "folders, data_frame_method, expected_result",
+ (
+ (
+ [
+ "./tests/mock_stats/2024-02-07T03:09:41",
+ "./tests/mock_stats/2024-02-06T03:09:41",
+ ],
+ get_performance_data_frame,
+ expected_performance_dataframe_consecutive_days,
+ ),
+ (
+ [
+ "./tests/mock_stats/2024-02-09T03:09:41",
+ "./tests/mock_stats/2024-02-12T03:09:41",
+ ],
+ get_additional_metrics_data_frame,
+ expected_pdf_session_dataframe_consecutive_days,
+ ),
+ ),
+)
+def test_get_data_frame_multiple_files(folders, data_frame_method, expected_result):
+ results = get_results(folders=folders)
+ dataframe = data_frame_method(results)
+ assert_frame_equal(dataframe, expected_result)
+
+
+@pytest.mark.parametrize(
+ "expected_dataframe, data_frame_method, results_file",
+ (
+ (
+ expected_performance_dataframe,
+ get_performance_data_frame,
+ "get_results_single_file",
+ ),
+ (
+ expected_pdf_session_dataframe,
+ get_additional_metrics_data_frame,
+ "get_results_single_file_with_pdf_endpoint",
+ ),
+ ),
+)
+def test_plot_data_df(
+ expected_dataframe, data_frame_method, results_file, mocker, request
+):
+ dataframe = data_frame_method(request.getfixturevalue(results_file))
mock_plot_data = mocker.patch("scripts.visualise_results.plot_data")
mock_plot_data(dataframe, 1)
assert mock_plot_data.call_count == 1
- assert_frame_equal(mock_plot_data.call_args[0][0], expected_data_frame)
+ assert_frame_equal(mock_plot_data.call_args[0][0], expected_dataframe)
-def test_plot_data(mocker, get_results_single_file):
- dataframe = get_data_frame(get_results_single_file)
+@pytest.mark.parametrize(
+ "results_file, data_frame_method, image_name",
+ (
+ (
+ "get_results_single_file",
+ get_performance_data_frame,
+ "performance_graph.png",
+ ),
+ (
+ "get_results_single_file_with_pdf_endpoint",
+ get_additional_metrics_data_frame,
+ "additional_metrics.png",
+ ),
+ ),
+)
+def test_individual_graph_creation(
+ results_file, data_frame_method, image_name, mocker, request
+):
+ dataframe = data_frame_method(request.getfixturevalue(results_file))
try:
- graph_output = mocker.patch("matplotlib.pyplot.savefig")
- plot_data(dataframe, 1)
- assert graph_output.call_count == 1
+ graph_creation = mocker.patch("matplotlib.pyplot.subplot")
+ create_graph([dataframe], 1, image_name)
+ assert graph_creation.call_count == 1
except GraphGenerationFailed:
pytest.fail("Graph generation failed")
-def test_plot_data_failed():
+@freeze_time("2024-03-1")
+def test_get_dataframes():
+ folders = sorted(glob("tests/mock_stats/*"))
+ dataframes = get_dataframes(folders, 30)
+
+ for i, item in enumerate(dataframes):
+ assert_frame_equal(item, dataframes_list[i])
+
+
+def test_create_graph_failed():
dataframe = DataFrame.from_dict({})
with pytest.raises(GraphGenerationFailed):
- plot_data(dataframe, 1)
+ create_graph([dataframe], 1, "test_graph.png")
+
+
+@freeze_time("2024-03-1")
+def test_create_graph(mocker):
+ folders = sorted(glob("tests/mock_stats/*"))
+ dataframes = get_dataframes(folders, 30)
+ try:
+ graph_output = mocker.patch("matplotlib.pyplot.savefig")
+ create_graph(dataframes, 1, "performance_graph.png")
+ assert graph_output.call_count == 1
+ except GraphGenerationFailed:
+ pytest.fail("Graph generation failed")