From 7a9b4e398cfc76f4bfe86d35f51ca16bc3cc6a64 Mon Sep 17 00:00:00 2001 From: Alexander Todorov Date: Thu, 23 Jan 2025 23:13:32 +0200 Subject: [PATCH] Add more web related locust tasks - tasks simulate most commonly performed actions - task frequency comes from curated plausible.io stats - reuse the existing JSON-RPC login and add the sessionid cookie to the browser context instead of logging in via the browser which greatly simplifies the code and uses only publicly available API! --- tests/performance/base.py | 56 +++--- tests/performance/web_simulation_test.py | 209 ++++++++++++++++++++++- tests/test_http.sh | 2 +- 3 files changed, 229 insertions(+), 38 deletions(-) diff --git a/tests/performance/base.py b/tests/performance/base.py index 705866aa1f..57de1015cd 100644 --- a/tests/performance/base.py +++ b/tests/performance/base.py @@ -3,6 +3,8 @@ # Licensed under GNU Affero General Public License v3 or later (AGPLv3+) # https://www.gnu.org/licenses/agpl-3.0.html +import re + from locust import FastHttpUser, between, task from locust_plugins.users.playwright import PlaywrightUser from requests.exceptions import HTTPError @@ -40,9 +42,18 @@ def log_response(user, response): if response.headers and "content-length" in response.headers: response_length = int(response.headers["content-length"]) + # compress the actual URLs for more concise reports target_url = request.url.replace(user.host, "") if target_url.startswith("/static/"): - target_url = "/static/..." + target_url = "/static/.../" + elif target_url.startswith("/cases/clone/?"): + target_url = "/cases/clone/" + elif re.match(r"/plan/\d+/", target_url): + target_url = "/plan/.../" + elif re.match(r"/runs/\d+/", target_url): + target_url = "/runs/.../" + elif target_url.startswith("/runs/from-plan/"): + target_url = "/runs/from-plan/.../" request_meta = { "request_type": request.method, @@ -114,41 +125,18 @@ class BrowserTestCase(PlaywrightUser, LoggedInTestCase): log_external = True - def do_login(self): - """ - Making this a no-op b/c we login via the browser in - self._pwprep() below and keep track of the session cookie! - """ - async def setup_page(self, page): - await page.context.add_cookies([self.session_cookie]) - page.on("response", lambda response: log_response(self, response)) - - async def _pwprep(self): - await super()._pwprep() - - # login via the browser - browser_context = await self.browser.new_context( - ignore_https_errors=True, base_url=self.host + cookies = dict_from_cookiejar(self.client.cookiejar) + await page.context.add_cookies( + [ + { + "name": "sessionid", + "value": cookies["sessionid"], + "url": self.host, + } + ] ) - page = await browser_context.new_page() - page.set_default_timeout(60000) - - await page.goto(self.login_url) - await page.wait_for_load_state() - - await page.locator("#inputUsername").fill(self.username) - await page.locator("#inputPassword").fill(self.password) - await page.get_by_role("button").click() - - # store this for later use b/c @pw creates - # a new context & page for every task! - for cookie in await page.context.cookies(): - if cookie["name"] == "sessionid": - self.session_cookie = cookie - - await page.close() - await browser_context.close() + page.on("response", lambda response: log_response(self, response)) class ExampleTestCase(LoggedInTestCase): diff --git a/tests/performance/web_simulation_test.py b/tests/performance/web_simulation_test.py index b6ea014f40..d154761d45 100644 --- a/tests/performance/web_simulation_test.py +++ b/tests/performance/web_simulation_test.py @@ -3,16 +3,213 @@ # Licensed under GNU Affero General Public License v3 or later (AGPLv3+) # https://www.gnu.org/licenses/agpl-3.0.html +import random +from datetime import datetime + +import gevent from base import BrowserTestCase -from locust import task +from locust import between, task from locust_plugins.users.playwright import pw +from playwright.async_api import expect class UserActionsTestCase(BrowserTestCase): + """ + Visit the most commonly visited pages in Kiwi TCMS according to + curated plausible.io stats. Try to behave like a human with generous + waiting time between the different pages b/c people don't click at + lightning fast speed like robots. + """ + log_external = False log_tasks = False + wait_time = between(60, 300) + + @task(10) + @pw + async def clone_test_case(self, page): + await self.setup_page(page) + + test_cases = self.json_rpc( + "TestCase.filter", + { + "case_status__is_confirmed": True, + }, + ) + # max 5 test cases at a time b/c it is rare that someone + # will clone more on a regular basis - plus we add the clones + # to all possible TestPlans in which the source cases are already included + how_many = random.randint(1, min(5, len(test_cases))) + chosen_cases = random.sample(test_cases, how_many) + target_url = "/cases/clone/?" + for test_case in chosen_cases: + target_url += f"c={test_case['id']}&" + + await page.goto(target_url) + await page.wait_for_load_state() + + # simulate reviewing the selection + gevent.sleep(random.random() * 60) + + # save + await page.get_by_text("Close Ad").click() + await page.get_by_role("button", name="Clone").click() + await page.wait_for_load_state() + + if len(chosen_cases) > 1: + success_message = page.locator(".alert-success") + await expect(success_message).to_contain_text( + "TestCase cloning was successfull" + ) + + @task(15) + @pw + async def create_new_test_case(self, page): + await self.setup_page(page) + + await page.goto("/cases/new/") + await page.wait_for_load_state() + + now = datetime.now().isoformat() + await page.locator("#id_summary").fill(f"Test Case by PW at {now}") + + # Simulate typing a long text - up to 2 mins + gevent.sleep(random.random() * 120) + + await page.locator("div.CodeMirror > div > textarea").fill("Given-When-Then") + await page.get_by_label("Status").select_option("CONFIRMED") + + # save + await page.get_by_text("Close Ad").click() + await page.get_by_role("button", name="Save").click() + await page.wait_for_url("/case/**/") + + @task(15) + @pw + async def create_new_test_run(self, page): + await self.setup_page(page) + + test_plans = self.json_rpc( + "TestPlan.filter", + {}, + ) + which_one = random.randint(0, len(test_plans) - 1) + chosen_plan = test_plans[which_one] + + test_cases = self.json_rpc( + "TestCase.filter", + { + "case_status__is_confirmed": True, + "plan": chosen_plan["id"], + }, + ) + + target_url = f"/runs/from-plan/{chosen_plan['id']}/?" + + if len(test_cases) > 0: + # will create a TR with max 100 TCs from the chosen TP + how_many = random.randint(1, min(100, len(test_cases))) + chosen_cases = random.sample(test_cases, how_many) + for test_case in chosen_cases: + target_url += f"c={test_case['id']}&" + + await page.goto(target_url) + await page.wait_for_load_state() + + await page.get_by_label("Build").select_option("unspecified") + await page.get_by_role("button", name="Save").click() + await page.wait_for_url("/runs/**/") + run_id = await page.locator("#test_run_pk").get_attribute("data-pk") + + h1 = page.locator("h1") + await expect(h1).to_contain_text("Test run for") - @task + # chosen TP does not contain any TCs => add TCs to TR! + if len(test_cases) == 0: + test_cases = self.json_rpc( + "TestCase.filter", + { + "case_status__is_confirmed": True, + }, + ) + how_many = random.randint(1, min(100, len(test_cases))) + chosen_cases = random.sample(test_cases, how_many) + for test_case in chosen_cases: + self.json_rpc("TestRun.add_case", [run_id, test_case["id"]]) + gevent.sleep(random.random() * 5) + + @task(18) + @pw + async def create_new_test_plan(self, page): + await self.setup_page(page) + + await page.goto("/plan/new/") + await page.wait_for_load_state() + + now = datetime.now().isoformat() + await page.locator("#id_name").fill(f"Created by PW at {now}") + await page.locator("div.CodeMirror > div > textarea").fill( + "This is the body of this TP document" + ) + + # save + await page.get_by_text("Close Ad").click() + await page.get_by_role("button", name="Save").click() + await page.wait_for_url("/plan/**/") + plan_id = await page.locator("#test_plan_pk").get_attribute("data-testplan-pk") + + # Simulate adding test cases one by one + gevent.sleep(random.random() * 2.5) + + test_cases = self.json_rpc( + "TestCase.filter", + { + "case_status__is_confirmed": True, + }, + ) + # max 100 TCs inside a TP + how_many = random.randint(1, min(100, len(test_cases))) + chosen_cases = random.sample(test_cases, how_many) + for test_case in chosen_cases: + self.json_rpc("TestPlan.add_case", [plan_id, test_case["id"]]) + gevent.sleep(random.random() * 5) + + await page.reload() + await page.wait_for_load_state() + + @task(20) + @pw + async def visit_test_plan_page(self, page): + await self.setup_page(page) + + test_plans = self.json_rpc( + "TestPlan.filter", + {}, + ) + which_one = random.randint(0, len(test_plans) - 1) + chosen_plan = test_plans[which_one] + + await page.goto(f"/plan/{chosen_plan['id']}/") + await page.wait_for_url("/plan/**/**") + await page.wait_for_load_state() + + @task(21) + @pw + async def visit_plans_search_page(self, page): + await self.setup_page(page) + + await page.goto("/plan/search/") + await page.wait_for_load_state() + + @task(33) + @pw + async def visit_runs_search_page(self, page): + await self.setup_page(page) + + await page.goto("/runs/search/") + await page.wait_for_load_state() + + @task(38) @pw async def visit_cases_search_page(self, page): await self.setup_page(page) @@ -20,4 +217,10 @@ async def visit_cases_search_page(self, page): await page.goto("/cases/search/") await page.wait_for_load_state() - # note: raise StopUser() doesn't work here + @task(100) + @pw + async def visit_dashboard_page(self, page): + await self.setup_page(page) + + await page.goto("/") + await page.wait_for_load_state() diff --git a/tests/test_http.sh b/tests/test_http.sh index 43957118e6..02ad0ae9b8 100755 --- a/tests/test_http.sh +++ b/tests/test_http.sh @@ -191,7 +191,7 @@ _EOF_ # this is designed to check that these files don't crash, rlRun -t -c "locust --headless --users 1 --spawn-rate 1 --run-time 5s -H https://localhost/ --locustfile tests/performance/base.py" rlRun -t -c "locust --headless --users 1 --spawn-rate 1 --run-time 5s -H https://localhost/ --locustfile tests/performance/api_write_test.py" - rlRun -t -c "locust --headless --users 1 --spawn-rate 1 --run-time 5s -H https://localhost/ --locustfile tests/performance/web_simulation_test.py" + rlRun -t -c "locust --headless --users 1 --spawn-rate 0.01 --run-time 5s -H https://localhost/ --locustfile tests/performance/web_simulation_test.py" rlPhaseEnd rlPhaseStartCleanup