From patchwork Fri Dec 8 01:53:49 2023 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Alassane Yattara X-Patchwork-Id: 35893 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from aws-us-west-2-korg-lkml-1.web.codeaurora.org (localhost.localdomain [127.0.0.1]) by smtp.lore.kernel.org (Postfix) with ESMTP id 708B7C10F04 for ; Fri, 8 Dec 2023 01:54:09 +0000 (UTC) Received: from mail.savoirfairelinux.com (mail.savoirfairelinux.com [208.88.110.44]) by mx.groups.io with SMTP id smtpd.web10.10271.1702000439596979620 for ; Thu, 07 Dec 2023 17:53:59 -0800 Authentication-Results: mx.groups.io; dkim=pass header.i=@savoirfairelinux.com header.s=DFC430D2-D198-11EC-948E-34200CB392D2 header.b=jYK+WvD4; spf=pass (domain: savoirfairelinux.com, ip: 208.88.110.44, mailfrom: alassane.yattara@savoirfairelinux.com) Received: from localhost (localhost [127.0.0.1]) by mail.savoirfairelinux.com (Postfix) with ESMTP id F139A9C366F for ; Thu, 7 Dec 2023 20:53:58 -0500 (EST) Received: from mail.savoirfairelinux.com ([127.0.0.1]) by localhost (mail.savoirfairelinux.com [127.0.0.1]) (amavis, port 10032) with ESMTP id zbWlKpWjuw-7; Thu, 7 Dec 2023 20:53:58 -0500 (EST) Received: from localhost (localhost [127.0.0.1]) by mail.savoirfairelinux.com (Postfix) with ESMTP id 03DC69C3678; Thu, 7 Dec 2023 20:53:58 -0500 (EST) DKIM-Filter: OpenDKIM Filter v2.10.3 mail.savoirfairelinux.com 03DC69C3678 DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=savoirfairelinux.com; s=DFC430D2-D198-11EC-948E-34200CB392D2; t=1702000438; bh=pJyjScw3mXtyLRKG9WPHEr3Itcgknnb5d0cPWyR7adI=; h=From:To:Date:Message-Id:MIME-Version; b=jYK+WvD4qDN5aRDfz/IcUEdOLyS0UYTxlm45EXmzfC6/j/TCGKJ7aF61yif9YCuNb styN7JsPcMjsNy9nS6iAXMKIXPO0KdMVly4psESMC66+h+cXCcE+VkiiR8n2K3XOeX Qp/4zhaM0SJDDiZF2WRcLMi+k9sZN0Mu+2CY3npParHunQXIpiNFq9Lpxy7eVO3dLk SMQytdm5T94SYQGezMUco3Dvw31QdWXdYqqbbBU2aoQN+nHL35f+WL+YIcD4xdjFV4 9S/LXJWgzdvMqp6H6f68t3TGAnokOaneXMHsrWSg3Pb+lwQdauupFqk6JEyaztXMqK COvpbIhf9Kokw== X-Virus-Scanned: amavis at mail.savoirfairelinux.com Received: from mail.savoirfairelinux.com ([127.0.0.1]) by localhost (mail.savoirfairelinux.com [127.0.0.1]) (amavis, port 10026) with ESMTP id PS11K9n33oqg; Thu, 7 Dec 2023 20:53:57 -0500 (EST) Received: from jedi.. (unknown [196.127.183.75]) by mail.savoirfairelinux.com (Postfix) with ESMTPSA id 407A39C33E2; Thu, 7 Dec 2023 20:53:57 -0500 (EST) From: Alassane Yattara To: toaster@lists.yoctoproject.org Cc: Alassane Yattara Subject: [PATCH 4/4] toaster/test: Bug fixes, functional tests dependent on each other Date: Fri, 8 Dec 2023 02:53:49 +0100 Message-Id: <20231208015349.678997-4-alassane.yattara@savoirfairelinux.com> X-Mailer: git-send-email 2.34.1 In-Reply-To: <20231208015349.678997-1-alassane.yattara@savoirfairelinux.com> References: <20231208015349.678997-1-alassane.yattara@savoirfairelinux.com> MIME-Version: 1.0 List-Id: X-Webhook-Received: from li982-79.members.linode.com [45.33.32.79] by aws-us-west-2-korg-lkml-1.web.codeaurora.org with HTTPS for ; Fri, 08 Dec 2023 01:54:09 -0000 X-Groupsio-URL: https://lists.yoctoproject.org/g/toaster/message/6069 refactor test_create_project and test_project_page to remove their dependencies Signed-off-by: Alassane Yattara --- .../functional/test_create_new_project.py | 1 + .../tests/functional/test_project_page.py | 164 +++++++++--------- 2 files changed, 79 insertions(+), 86 deletions(-) diff --git a/lib/toaster/tests/functional/test_create_new_project.py b/lib/toaster/tests/functional/test_create_new_project.py index dc7d1fc2..bbda0cf4 100644 --- a/lib/toaster/tests/functional/test_create_new_project.py +++ b/lib/toaster/tests/functional/test_create_new_project.py @@ -16,6 +16,7 @@ from selenium.webdriver.common.by import By @pytest.mark.django_db +@pytest.mark.order("last") class TestCreateNewProject(SeleniumFunctionalTestCase): def _create_test_new_project( diff --git a/lib/toaster/tests/functional/test_project_page.py b/lib/toaster/tests/functional/test_project_page.py index 03f64f8f..077badb0 100644 --- a/lib/toaster/tests/functional/test_project_page.py +++ b/lib/toaster/tests/functional/test_project_page.py @@ -6,88 +6,89 @@ # SPDX-License-Identifier: GPL-2.0-only # +import os import random import string +from unittest import skip import pytest -from time import sleep from django.urls import reverse from django.utils import timezone from selenium.webdriver.common.keys import Keys from selenium.webdriver.support.select import Select -from selenium.common.exceptions import NoSuchElementException, TimeoutException +from selenium.common.exceptions import TimeoutException from tests.functional.functional_helpers import SeleniumFunctionalTestCase from orm.models import Build, Project, Target from selenium.webdriver.common.by import By +from .utils import get_projectId_from_url, wait_until_build, wait_until_build_cancelled + @pytest.mark.django_db +@pytest.mark.order("last") class TestProjectPage(SeleniumFunctionalTestCase): + project_id = None + PROJECT_NAME = 'TestProjectPage' - def setUp(self): - super().setUp() - release = '3' - project_name = 'project_' + self.generate_random_string() - self._create_test_new_project( - project_name, - release, - False, - ) - - def generate_random_string(self, length=10): - characters = string.ascii_letters + string.digits # alphabetic and numerical characters - random_string = ''.join(random.choice(characters) for _ in range(length)) - return random_string - - def _create_test_new_project( - self, - project_name, - release, - merge_toaster_settings, - ): + def _create_project(self, project_name): """ Create/Test new project using: - Project Name: Any string - Release: Any string - Merge Toaster settings: True or False """ self.get(reverse('newproject')) - self.driver.find_element(By.ID, - "new-project-name").send_keys(project_name) - - select = Select(self.find('#projectversion')) - select.select_by_value(release) + self.wait_until_visible('#new-project-name') + self.find("#new-project-name").send_keys(project_name) + select = Select(self.find("#projectversion")) + select.select_by_value('3') # check merge toaster settings checkbox = self.find('.checkbox-mergeattr') - if merge_toaster_settings: - if not checkbox.is_selected(): - checkbox.click() + if not checkbox.is_selected(): + checkbox.click() + + if self.PROJECT_NAME != 'TestProjectPage': + # Reset project name if it's not the default one + self.PROJECT_NAME = 'TestProjectPage' + + self.find("#create-project-button").click() + + try: + self.wait_until_visible('#hint-error-project-name') + url = reverse('project', args=(TestProjectPage.project_id, )) + self.get(url) + self.wait_until_visible('#config-nav', poll=3) + except TimeoutException: + self.wait_until_visible('#config-nav', poll=3) + + def _random_string(self, length): + return ''.join( + random.choice(string.ascii_letters) for _ in range(length) + ) + + def _navigate_to_project_page(self): + # Navigate to project page + if TestProjectPage.project_id is None: + self._create_project(project_name=self._random_string(10)) + current_url = self.driver.current_url + TestProjectPage.project_id = get_projectId_from_url(current_url) else: - if checkbox.is_selected(): - checkbox.click() - - self.driver.find_element(By.ID, "create-project-button").click() + url = reverse('project', args=(TestProjectPage.project_id,)) + self.get(url) + self.wait_until_visible('#config-nav') def _get_create_builds(self, **kwargs): """ Create a build and return the build object """ # parameters for builds to associate with the projects now = timezone.now() - release = '3' - project_name = 'projectmaster' - self._create_test_new_project( - project_name+"2", - release, - False, - ) - self.project1_build_success = { - 'project': Project.objects.get(id=1), + 'project': Project.objects.get(id=TestProjectPage.project_id), 'started_on': now, 'completed_on': now, 'outcome': Build.SUCCEEDED } self.project1_build_failure = { - 'project': Project.objects.get(id=1), + 'project': Project.objects.get(id=TestProjectPage.project_id), 'started_on': now, 'completed_on': now, 'outcome': Build.FAILED @@ -180,9 +181,7 @@ class TestProjectPage(SeleniumFunctionalTestCase): def _navigate_to_config_nav(self, nav_id, nav_index): # navigate to the project page - url = reverse("project", args=(1,)) - self.get(url) - self.wait_until_visible('#config-nav') + self._navigate_to_project_page() # click on "Software recipe" tab soft_recipe = self._get_config_nav_item(nav_index) soft_recipe.click() @@ -211,29 +210,6 @@ class TestProjectPage(SeleniumFunctionalTestCase): if row_to_show not in to_skip: test_show_rows(row_to_show, show_row_link) - def _wait_until_build(self, state): - timeout = 10 - start_time = 0 - while True: - if start_time > timeout: - raise TimeoutException( - f'Build did not reach {state} state within {timeout} seconds' - ) - try: - last_build_state = self.driver.find_element( - By.XPATH, - '//*[@id="latest-builds"]/div[1]//div[@class="build-state"]', - ) - build_state = last_build_state.get_attribute( - 'data-build-state') - state_text = state.lower().split() - if any(x in str(build_state).lower() for x in state_text): - break - except NoSuchElementException: - continue - start_time += 1 - sleep(1) # take a breath and try again - def _mixin_test_table_search_input(self, **kwargs): input_selector, input_text, searchBtn_selector, table_selector, *_ = kwargs.values() # Test search input @@ -245,11 +221,19 @@ class TestProjectPage(SeleniumFunctionalTestCase): rows = self.find_all(f'#{table_selector} tbody tr') self.assertTrue(len(rows) > 0) + def test_create_project(self): + """ Create/Test new project using: + - Project Name: Any string + - Release: Any string + - Merge Toaster settings: True or False + """ + self._create_project(project_name=self.PROJECT_NAME) + def test_image_recipe_editColumn(self): """ Test the edit column feature in image recipe table on project page """ self._get_create_builds(success=10, failure=10) - url = reverse('projectimagerecipes', args=(1,)) + url = reverse('projectimagerecipes', args=(TestProjectPage.project_id,)) self.get(url) self.wait_until_present('#imagerecipestable tbody tr') @@ -276,8 +260,7 @@ class TestProjectPage(SeleniumFunctionalTestCase): - AT RIGHT -> button "New project", displayed, clickable """ # navigate to the project page - url = reverse("project", args=(1,)) - self.get(url) + self._navigate_to_project_page() # check page header # AT LEFT -> Logo of Yocto project @@ -360,8 +343,7 @@ class TestProjectPage(SeleniumFunctionalTestCase): - Check project name is changed """ # navigate to the project page - url = reverse("project", args=(1,)) - self.get(url) + self._navigate_to_project_page() # click on "Edit" icon button self.wait_until_visible('#project-name-container') @@ -388,8 +370,7 @@ class TestProjectPage(SeleniumFunctionalTestCase): Check search box used to build recipes """ # navigate to the project page - url = reverse("project", args=(1,)) - self.get(url) + self._navigate_to_project_page() # check "configuration" tab self.wait_until_visible('#topbar-configuration-tab') @@ -397,7 +378,7 @@ class TestProjectPage(SeleniumFunctionalTestCase): self.assertTrue(config_tab.get_attribute('class') == 'active') self.assertTrue('Configuration' in str(config_tab.text)) self.assertTrue( - f"/toastergui/project/1" in str(self.driver.current_url) + f"/toastergui/project/{TestProjectPage.project_id}" in str(self.driver.current_url) ) def get_tabs(): @@ -420,7 +401,7 @@ class TestProjectPage(SeleniumFunctionalTestCase): check_tab_link( 1, 'Builds', - f"/toastergui/project/1/builds" + f"/toastergui/project/{TestProjectPage.project_id}/builds" ) # check "Import layers" tab @@ -429,7 +410,7 @@ class TestProjectPage(SeleniumFunctionalTestCase): check_tab_link( 2, 'Import layer', - f"/toastergui/project/1/importlayer" + f"/toastergui/project/{TestProjectPage.project_id}/importlayer" ) # check "New custom image" tab @@ -438,7 +419,7 @@ class TestProjectPage(SeleniumFunctionalTestCase): check_tab_link( 3, 'New custom image', - f"/toastergui/project/1/newcustomimage" + f"/toastergui/project/{TestProjectPage.project_id}/newcustomimage" ) # check search box can be use to build recipes @@ -480,12 +461,20 @@ class TestProjectPage(SeleniumFunctionalTestCase): '//td[@class="add-del-layers"]//a[1]' ) build_btn.click() - self._wait_until_build('parsing starting cloning queued') + build_state = wait_until_build(self, 'parsing starting cloning queued') lastest_builds = self.driver.find_elements( By.XPATH, '//div[@id="latest-builds"]/div' ) self.assertTrue(len(lastest_builds) > 0) + last_build = lastest_builds[0] + cancel_button = last_build.find_element( + By.XPATH, + '//span[@class="cancel-build-btn pull-right alert-link"]', + ) + cancel_button.click() + if 'starting' not in build_state: # change build state when cancelled in starting state + wait_until_build_cancelled(self) # check software recipe table feature(show/hide column, pagination) self._navigate_to_config_nav('softwarerecipestable', 4) @@ -547,6 +536,7 @@ class TestProjectPage(SeleniumFunctionalTestCase): searchBtn_selector='search-submit-machinestable', table_selector='machinestable' ) + self.wait_until_visible('#machinestable tbody tr', poll=3) rows = self.find_all('#machinestable tbody tr') machine_to_add = rows[0] add_btn = machine_to_add.find_element(By.XPATH, '//td[@class="add-del-layers"]') @@ -593,6 +583,7 @@ class TestProjectPage(SeleniumFunctionalTestCase): table_selector='layerstable' ) # check "Add layer" button works + self.wait_until_visible('#layerstable tbody tr', poll=3) rows = self.find_all('#layerstable tbody tr') layer_to_add = rows[0] add_btn = layer_to_add.find_element( @@ -601,7 +592,7 @@ class TestProjectPage(SeleniumFunctionalTestCase): ) add_btn.click() # check modal is displayed - self.wait_until_visible('#dependencies-modal', poll=2) + self.wait_until_visible('#dependencies-modal', poll=3) list_dependencies = self.find_all('#dependencies-list li') # click on add-layers button add_layers_btn = self.driver.find_element( @@ -615,6 +606,7 @@ class TestProjectPage(SeleniumFunctionalTestCase): f'You have added {len(list_dependencies)+1} layers to your project: {input_text} and its dependencies' in str(change_notification.text) ) # check "Remove layer" button works + self.wait_until_visible('#layerstable tbody tr', poll=3) rows = self.find_all('#layerstable tbody tr') layer_to_remove = rows[0] remove_btn = layer_to_remove.find_element( @@ -706,7 +698,7 @@ class TestProjectPage(SeleniumFunctionalTestCase): - Check layer summary - Check layer description """ - url = reverse("layerdetails", args=(1, 8)) + url = reverse("layerdetails", args=(TestProjectPage.project_id, 8)) self.get(url) self.wait_until_visible('.page-header') # check title is displayed @@ -765,7 +757,7 @@ class TestProjectPage(SeleniumFunctionalTestCase): - Check recipe: name, summary, description, Version, Section, License, Approx. packages included, Approx. size, Recipe file """ - url = reverse("recipedetails", args=(1, 53428)) + url = reverse("recipedetails", args=(TestProjectPage.project_id, 53428)) self.get(url) self.wait_until_visible('.page-header') # check title is displayed