Skip to content

Commit

Permalink
Add more web related locust tasks
Browse files Browse the repository at this point in the history
- 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!
  • Loading branch information
atodorov committed Jan 29, 2025
1 parent 7ce5a22 commit 7a9b4e3
Show file tree
Hide file tree
Showing 3 changed files with 229 additions and 38 deletions.
56 changes: 22 additions & 34 deletions tests/performance/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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):
Expand Down
209 changes: 206 additions & 3 deletions tests/performance/web_simulation_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,224 @@
# 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)

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()
2 changes: 1 addition & 1 deletion tests/test_http.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 7a9b4e3

Please sign in to comment.