From 602d23a4fe325d2af74a1f05e6fa15590f3cfb86 Mon Sep 17 00:00:00 2001 From: alfredeen Date: Wed, 9 Oct 2024 09:15:03 +0200 Subject: [PATCH] Created user type Student as a subtype of PowerUser. This user created JupyterLab notebooks and is used in the classroom test plan. --- README.md | 2 +- source/tests/base_user_types.py | 118 +++++++++++++++++++++++++--- source/tests/test_plan_classroom.py | 27 ++----- source/tests/test_plan_normal.py | 2 +- 4 files changed, 118 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index f16c013..9b3534b 100644 --- a/README.md +++ b/README.md @@ -127,7 +127,7 @@ Or using the Web UI ### To run the Classroom test plan/scenario - locust --headless -f ./tests/test_plan_classroom.py --html ./reports/locust-report-classroom.html --users 1 --run-time 30s + locust --headless -f ./tests/test_plan_classroom.py --html ./reports/locust-report-classroom.html --users 10 --run-time 30s ## Tests under development diff --git a/source/tests/base_user_types.py b/source/tests/base_user_types.py index 2c4a9a6..decc50b 100644 --- a/source/tests/base_user_types.py +++ b/source/tests/base_user_types.py @@ -3,6 +3,7 @@ import warnings from locust import HttpUser, between, task +from lxml import etree logger = logging.getLogger(__name__) @@ -123,7 +124,7 @@ def get_token(self): class PowerBaseUser(HttpUser): """Base class for the power user type that logs into Serve using an existing user account, - then creates resources such as a project and app and finally deleted the app. + then creates resources such as a project and finally deletes the project. """ abstract = True @@ -132,6 +133,9 @@ class PowerBaseUser(HttpUser): user_individual_id = 0 local_individual_id = 0 + # Student type of Power users also create JupyterLab notebooks + is_student_user = False + username = "NOT_FOUND" password = SERVE_LOCUST_TEST_USER_PASS @@ -152,10 +156,12 @@ def on_start(self): """Called when a User starts running.""" self.client.verify = False # Don't check if certificate is valid self.local_individual_id = PowerBaseUser.get_user_id() - logger.info("ONSTART new user type %s, individual %s", self.user_type, self.local_individual_id) + logger.info( + f"ONSTART new user type {self.user_type}, individual {self.local_individual_id}, \ + IsStudent? {self.is_student_user}" + ) # Use the pre-created test users for this: f"locust_test_user_{self.local_individual_id}@test.uu.net" self.username = f"locust_test_user_{self.local_individual_id}@test.uu.net" - # self.username = "locust_test_persisted_user@test.uu.net" # Tasks @@ -192,7 +198,9 @@ def power_user_task(self): ) return else: - logger.info("Creating and deleting projects and apps as user %s", self.username) + logger.info( + f"Creating and deleting projects and apps as user {self.username}, IsStudent? {self.is_student_user}" + ) # Create project: locust_test_project_new_ project_name = f"locust_test_project_new_{self.local_individual_id}" @@ -202,14 +210,20 @@ def power_user_task(self): logger.info("Opening project at URL %s", self.project_url) self.client.get(self.project_url) - # TODO: create JupyterLab app - - # TODO: open the app + # Student type of users also create and use JupyterLab notebooks + if self.is_student_user: + # Create JupyterLab app + app_name = "locust-jupyterlab-app" + logger.info( + f"Creating a JupyterLab notebook {app_name}. This test user is a Student type of PowerUser." + ) + self._create_app(project_name, app_name) - # TODO: delete the app + # TODO: Consider also opening the app (and deleting after some time) - # Delete the project - self.delete_project() + else: + # Delete the project if the user is not a Student + self.delete_project() # Logout the user self.logout() @@ -279,6 +293,90 @@ def delete_project(self): # logger.debug(response.content) response.failure("Delete project failed. Response URL does not contain /projects.") + def _create_app(self, project_name: str, app_name: str): + # Update the csrf token + app_create_url = self.project_url + "apps/create/jupyter-lab?from=overview" + logger.info(f"Using this URL to create a JL notebook app: {app_create_url}") + self.get_token(app_create_url) + + # First make a dummy POST to the form to get the html and parse out the select option values + app_data = dict(csrfmiddlewaretoken=self.csrftoken) + + html_content = "" + with self.client.post( + url=app_create_url, + data=app_data, + headers={"Referer": "foo"}, + name="---CREATE-NEW-APP-JUPYTERLAB", + catch_response=True, + ) as response: + logger.debug("create JupyterLab app response.status_code = %s, %s", response.status_code, response.reason) + html_content = response.content + + # Parse the HTML content + parser = etree.HTMLParser() + tree = etree.fromstring(html_content, parser) + + # Must first get the volume, flavor, and environment values from the form + volume = None + flavor = None + environment = None + + # Extract the form values of the option elements using XPath + # Flavor: + el_volume = tree.xpath('//select[@name="volume"]/option') + el_flavor = tree.xpath('//select[@name="flavor"]/option') + el_environment = tree.xpath('//select[@name="environment"]/option') + + if el_volume: + volume = el_volume[0].get("value") + else: + print("Option element VOLUME not found") + + if el_flavor: + flavor = el_flavor[0].get("value") + else: + print("Option element FLAVOR not found") + + if el_environment: + environment = el_environment[0].get("value") + else: + print("Option element ENVIRONMENT not found") + + print(f"The parsed form values to use are: volume={volume}, flavor={flavor}, environment={environment}") + + # To create the app, perform a POST submit to a URL with pattern: + # https://serve-dev.scilifelab.se/projects/locust-appcreator-project-20241007-145428-sib/apps/create/jupyter-lab?from=overview + + app_data = dict( + name=app_name, + volume=volume, + access="project", + flavor=flavor, + environment=environment, + description="Project desc", + csrfmiddlewaretoken=self.csrftoken, + ) + + with self.client.post( + url=app_create_url, + data=app_data, + headers={"Referer": "foo"}, + name="---CREATE-NEW-APP-JUPYTERLAB", + catch_response=True, + ) as response: + logger.debug("create JupyterLab app response.status_code = %s, %s", response.status_code, response.reason) + # If succeeds then url = /projects// + logger.debug("create JupyterLab app response.url = %s", response.url) + if project_name in response.url and "create/jupyter-lab" not in response.url: + # The returned URL should NOT be back at the create app page + logger.info("Successfully created JupyterLab app %s", app_name) + else: + logger.warning(f"Create JupyterLab app failed. Response URL {response.url} does not indicate success.") + logger.debug(response.content) + response.failure("Create JupyterLab app failed. Response URL does not indicate success.") + def login(self): logger.info("Login as user %s", self.username) diff --git a/source/tests/test_plan_classroom.py b/source/tests/test_plan_classroom.py index 17b2aa0..0b3ea43 100644 --- a/source/tests/test_plan_classroom.py +++ b/source/tests/test_plan_classroom.py @@ -1,11 +1,6 @@ """Locust test file defining the test plan scenario for the classroom load.""" -from base_user_types import ( - AppViewerUser, - OpenAPIClientBaseUser, - PowerBaseUser, - VisitingBaseUser, -) +from base_user_types import AppViewerUser, PowerBaseUser, VisitingBaseUser from locust import between @@ -17,12 +12,14 @@ class VisitingClassroomUser(VisitingBaseUser): wait_time = between(2, 3) -class PowerClassroomUser(PowerBaseUser): - """Implements the PowerBaseUser user type.""" +class StudentClassroomUser(PowerBaseUser): + """Implements the PowerBaseUser user type as a Student type user.""" - user_type = "PowerClassroomUser" - weight = 6 - wait_time = between(1, 2) + is_student_user = True + + user_type = "StudentClassroomUser" + weight = 7 + wait_time = between(2, 3) class AppViewerClassroomUser(AppViewerUser): @@ -31,11 +28,3 @@ class AppViewerClassroomUser(AppViewerUser): user_type = "AppViewerClassroomUser" weight = 1 wait_time = between(4, 8) - - -class OpenAPIClientClassroomUser(OpenAPIClientBaseUser): - """Implements the ApiBaseUser user type.""" - - user_type = "OpenAPIClientClassroomUser" - weight = 1 - wait_time = between(0.5, 2) diff --git a/source/tests/test_plan_normal.py b/source/tests/test_plan_normal.py index de2b699..917636f 100644 --- a/source/tests/test_plan_normal.py +++ b/source/tests/test_plan_normal.py @@ -26,7 +26,7 @@ class PowerNormalUser(PowerBaseUser): class AppViewerNormalUser(AppViewerUser): - """Implements the VisitingBaseUser user type.""" + """Implements the AppViewerUser user type.""" user_type = "AppViewerNormalUser" weight = 2