From patchwork Fri Dec 8 01:53:46 2023 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Alassane Yattara X-Patchwork-Id: 35890 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 5F8ADC4167B for ; Fri, 8 Dec 2023 01:53:59 +0000 (UTC) Received: from mail.savoirfairelinux.com (mail.savoirfairelinux.com [208.88.110.44]) by mx.groups.io with SMTP id smtpd.web11.10346.1702000435627225278 for ; Thu, 07 Dec 2023 17:53:55 -0800 Authentication-Results: mx.groups.io; dkim=pass header.i=@savoirfairelinux.com header.s=DFC430D2-D198-11EC-948E-34200CB392D2 header.b=Az+KvoPU; 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 800EA9C33BA for ; Thu, 7 Dec 2023 20:53:54 -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 wMWBgCgZv8dt; Thu, 7 Dec 2023 20:53:54 -0500 (EST) Received: from localhost (localhost [127.0.0.1]) by mail.savoirfairelinux.com (Postfix) with ESMTP id 3C9799C33E2; Thu, 7 Dec 2023 20:53:54 -0500 (EST) DKIM-Filter: OpenDKIM Filter v2.10.3 mail.savoirfairelinux.com 3C9799C33E2 DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=savoirfairelinux.com; s=DFC430D2-D198-11EC-948E-34200CB392D2; t=1702000434; bh=kXHiGTTvkSE94RAwVf732AXnJ0fLRLfhLbc5yfgtXf8=; h=From:To:Date:Message-Id:MIME-Version; b=Az+KvoPUTXlVjLAOWqOjto0XbVbQHP4UPSHYmkymoAwwG32wunxtWnS7Qvl85Jd24 8dhsJ2ZyePNzQM2eIbvlxzTYQpmZ1T2iTLAIV4djL4Huc8WtO39Eub6zTnnuJ+gRsK zokRzctYGpVg7ymIgi9WVMUedVY2yl8P5GvwWZ1nEJrVV/e3Q3XYdfj3LIvYFBS4Wt QbjeioKPPJ86gTn6s/Oz6mIwafDVq6bbRZMyKQTUm7PU9ZVrbqFw5X0dwLxdKW8KXH Q/B0zY/LU2r+Y8cuifBchyurXlDJw2tlHREjBRb3DkpTY6eJwqb6mqWeum9O4a0pfA bLGDIxg0cw4vg== 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 swHWnep3Y6V3; Thu, 7 Dec 2023 20:53:54 -0500 (EST) Received: from jedi.. (unknown [196.127.183.75]) by mail.savoirfairelinux.com (Postfix) with ESMTPSA id 5CEC19C33BA; Thu, 7 Dec 2023 20:53:53 -0500 (EST) From: Alassane Yattara To: toaster@lists.yoctoproject.org Cc: Alassane Yattara Subject: [PATCH 1/4] toaster/test: Ensure to kill toaster process create for tests functional Date: Fri, 8 Dec 2023 02:53:46 +0100 Message-Id: <20231208015349.678997-1-alassane.yattara@savoirfairelinux.com> X-Mailer: git-send-email 2.34.1 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:53:59 -0000 X-Groupsio-URL: https://lists.yoctoproject.org/g/toaster/message/6067 Toaster background task runbuilds continu running when even if tests is done Signed-off-by: Alassane Yattara --- lib/toaster/tests/functional/functional_helpers.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/toaster/tests/functional/functional_helpers.py b/lib/toaster/tests/functional/functional_helpers.py index b80d403b..c37c5f8d 100644 --- a/lib/toaster/tests/functional/functional_helpers.py +++ b/lib/toaster/tests/functional/functional_helpers.py @@ -33,11 +33,11 @@ class SeleniumFunctionalTestCase(SeleniumTestCaseBase): # start toaster cmd = "bash -c 'source toaster start'" - p = subprocess.Popen( + cls.p = subprocess.Popen( cmd, cwd=os.environ.get("BUILDDIR"), shell=True) - if p.wait() != 0: + if cls.p.wait() != 0: raise RuntimeError("Can't initialize toaster") super(SeleniumFunctionalTestCase, cls).setUpClass() @@ -58,6 +58,7 @@ class SeleniumFunctionalTestCase(SeleniumTestCaseBase): with open(os.path.join(builddir, '.runbuilds.pid'), 'r') as f: runbuilds_pid = int(f.read()) os.kill(runbuilds_pid, signal.SIGTERM) + cls.p.kill() def get_URL(self): From patchwork Fri Dec 8 01:53:47 2023 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Alassane Yattara X-Patchwork-Id: 35891 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 5F8D8C46CC5 for ; Fri, 8 Dec 2023 01:53:59 +0000 (UTC) Received: from mail.savoirfairelinux.com (mail.savoirfairelinux.com [208.88.110.44]) by mx.groups.io with SMTP id smtpd.web11.10347.1702000436547970455 for ; Thu, 07 Dec 2023 17:53:56 -0800 Authentication-Results: mx.groups.io; dkim=pass header.i=@savoirfairelinux.com header.s=DFC430D2-D198-11EC-948E-34200CB392D2 header.b=U1X6Cg5u; 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 E51A99C33BA for ; Thu, 7 Dec 2023 20:53:55 -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 5I6Yapsmd0O3; Thu, 7 Dec 2023 20:53:55 -0500 (EST) Received: from localhost (localhost [127.0.0.1]) by mail.savoirfairelinux.com (Postfix) with ESMTP id 72CDD9C3472; Thu, 7 Dec 2023 20:53:55 -0500 (EST) DKIM-Filter: OpenDKIM Filter v2.10.3 mail.savoirfairelinux.com 72CDD9C3472 DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=savoirfairelinux.com; s=DFC430D2-D198-11EC-948E-34200CB392D2; t=1702000435; bh=pp6zLWIo90Ps7WkBEqk9c38PxAJKi0ezvosNJG3A7dM=; h=From:To:Date:Message-Id:MIME-Version; b=U1X6Cg5u39wYllRssGZihZB8bnMpiINwRa9Bfr4H1s+uV5wX8atCWyZxrU0QYWM45 FGjhduw+XAM8XetQVBVPj/xM6DOpr3H7lQRbS2CSTWrICjUwtWAmowUUEyod9/yfrt MQpHwKl0DSyCEfEZSVue7M80PJ4B7vrbjoyHNZkgoXSXF0p7Q4qtIf+AKPpGgVdIQ5 nL+eY7UsIOzJTOE50EonXH7K2T2AuWfp02j4tGq28Z8FwO01QyCPeHK1i7gMd8iu03 avMwHrDtVrNdIaQ1utTecxb/95WVH5MARWGMkujguLxTQ9f7OWJSuapQB73dxztiWM xI89/kQB+Nsrw== 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 LbFdLBE7ZCJZ; Thu, 7 Dec 2023 20:53:55 -0500 (EST) Received: from jedi.. (unknown [196.127.183.75]) by mail.savoirfairelinux.com (Postfix) with ESMTPSA id 930359C33E2; Thu, 7 Dec 2023 20:53:54 -0500 (EST) From: Alassane Yattara To: toaster@lists.yoctoproject.org Cc: Alassane Yattara Subject: [PATCH 2/4] toaster/test: Added functional/utils, contains useful methods using by functional tests Date: Fri, 8 Dec 2023 02:53:47 +0100 Message-Id: <20231208015349.678997-2-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:53:59 -0000 X-Groupsio-URL: https://lists.yoctoproject.org/g/toaster/message/6068 Signed-off-by: Alassane Yattara --- lib/toaster/tests/functional/utils.py | 89 +++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 lib/toaster/tests/functional/utils.py diff --git a/lib/toaster/tests/functional/utils.py b/lib/toaster/tests/functional/utils.py new file mode 100644 index 00000000..bde1146e --- /dev/null +++ b/lib/toaster/tests/functional/utils.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# BitBake Toaster UI tests implementation +# +# Copyright (C) 2023 Savoir-faire Linux Inc +# +# SPDX-License-Identifier: GPL-2.0-only + + +from time import sleep +from selenium.common.exceptions import NoSuchElementException, StaleElementReferenceException, TimeoutException +from selenium.webdriver.common.by import By + +from orm.models import Build + + +def wait_until_build(test_instance, state): + timeout = 60 + start_time = 0 + build_state = '' + while True: + try: + if start_time > timeout: + raise TimeoutException( + f'Build did not reach {state} state within {timeout} seconds' + ) + last_build_state = test_instance.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): + return str(build_state).lower() + if 'failed' in str(build_state).lower(): + break + except NoSuchElementException: + continue + except TimeoutException: + break + start_time += 1 + sleep(1) # take a breath and try again + +def wait_until_build_cancelled(test_instance): + """ Cancel build take a while sometime, the method is to wait driver action + until build being cancelled + """ + timeout = 30 + start_time = 0 + build = None + while True: + try: + if start_time > timeout: + raise TimeoutException( + f'Build did not reach cancelled state within {timeout} seconds' + ) + last_build_state = test_instance.driver.find_element( + By.XPATH, + '//*[@id="latest-builds"]/div[1]//div[@class="build-state"]', + ) + build_state = last_build_state.get_attribute( + 'data-build-state') + if 'failed' in str(build_state).lower(): + break + if 'cancelling' in str(build_state).lower(): + # Change build state to cancelled + if not build: # get build object only once + build = Build.objects.last() + build.outcome = Build.CANCELLED + build.save() + if 'cancelled' in str(build_state).lower(): + break + except NoSuchElementException: + continue + except StaleElementReferenceException: + continue + except TimeoutException: + break + start_time += 1 + sleep(1) # take a breath and try again + +def get_projectId_from_url(url): + # url = 'http://domainename.com/toastergui/project/1656/whatever + # or url = 'http://domainename.com/toastergui/project/1/ + # or url = 'http://domainename.com/toastergui/project/186 + assert '/toastergui/project/' in url, "URL is not valid" + url_to_list = url.split('/toastergui/project/') + return int(url_to_list[1].split('/')[0]) # project_id From patchwork Fri Dec 8 01:53:48 2023 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Alassane Yattara X-Patchwork-Id: 35892 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 7090FC46CA3 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.web11.10348.1702000439975487666 for ; Thu, 07 Dec 2023 17:54:00 -0800 Authentication-Results: mx.groups.io; dkim=pass header.i=@savoirfairelinux.com header.s=DFC430D2-D198-11EC-948E-34200CB392D2 header.b=Qion+QFA; 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 58EFA9C3472 for ; Thu, 7 Dec 2023 20:53:59 -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 LSTVfyUjUo5l; Thu, 7 Dec 2023 20:53:57 -0500 (EST) Received: from localhost (localhost [127.0.0.1]) by mail.savoirfairelinux.com (Postfix) with ESMTP id E2F669C33BA; Thu, 7 Dec 2023 20:53:56 -0500 (EST) DKIM-Filter: OpenDKIM Filter v2.10.3 mail.savoirfairelinux.com E2F669C33BA DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=savoirfairelinux.com; s=DFC430D2-D198-11EC-948E-34200CB392D2; t=1702000436; bh=pD2TC+aByrxAp1AwQjbm4/ivgOg7xUgsyRvZN6Wb2Sw=; h=From:To:Date:Message-Id:MIME-Version; b=Qion+QFAcfqSnMyNp9Lcp4JEpiwtEvRmaeQV7kZFmUNlwUyseJlNxDtQAy6/sdGUQ Odxu7gzlA+txyODMpfWXrt0nlGWraeq2pDkYPdEECp0G4UDIoTcPgXtWabYMYYwCmp sscrMIKBspx6J62ZZvOsDFrqGHG/xJH/JFxZs9IUPjK1BFZhqVZsxVLp7xXACdINOx gLzxHKehD6B/xFmi3Ca/sz+8uko6OIyc7gXQTueIH2Qv72OqTAKNZDn+PRHKYen1dY rEJsTYZmfdwCYJVC5068wjuScYglZCNEIulnhnEd05yDBB1/TzZrerSW7E8vwo7t2I mHl7YglGa09aA== 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 8H6M96EAkhnq; Thu, 7 Dec 2023 20:53:56 -0500 (EST) Received: from jedi.. (unknown [196.127.183.75]) by mail.savoirfairelinux.com (Postfix) with ESMTPSA id CABE99C33E2; Thu, 7 Dec 2023 20:53:55 -0500 (EST) From: Alassane Yattara To: toaster@lists.yoctoproject.org Cc: Alassane Yattara Subject: [PATCH 3/4] toaster/test: Refactorize tests/functional Date: Fri, 8 Dec 2023 02:53:48 +0100 Message-Id: <20231208015349.678997-3-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/6070 - Split testcases from test_project_page_tab_config into tow files - Added new testcases in test_project_config - Test changing distro variable - Test setting IMAGE_INSTALL:append variable - Test setting PACKAGE_CLASSES variable - Test creating new bitbake variable Signed-off-by: Alassane Yattara --- .../tests/functional/test_project_config.py | 335 ++++++++++++ .../test_project_page_tab_config.py | 495 ++++++++---------- 2 files changed, 554 insertions(+), 276 deletions(-) create mode 100644 lib/toaster/tests/functional/test_project_config.py diff --git a/lib/toaster/tests/functional/test_project_config.py b/lib/toaster/tests/functional/test_project_config.py new file mode 100644 index 00000000..2d162d81 --- /dev/null +++ b/lib/toaster/tests/functional/test_project_config.py @@ -0,0 +1,335 @@ +#! /usr/bin/env python3 # +# BitBake Toaster UI tests implementation +# +# Copyright (C) 2023 Savoir-faire Linux +# +# SPDX-License-Identifier: GPL-2.0-only +# + +import string +import random +import pytest +from django.urls import reverse +from selenium.webdriver import Keys +from selenium.webdriver.support.select import Select +from selenium.common.exceptions import TimeoutException +from tests.functional.functional_helpers import SeleniumFunctionalTestCase +from selenium.webdriver.common.by import By + +from .utils import get_projectId_from_url + + +@pytest.mark.django_db +@pytest.mark.order("last") +class TestProjectConfig(SeleniumFunctionalTestCase): + project_id = None + PROJECT_NAME = 'TestProjectConfig' + INVALID_PATH_START_TEXT = 'The directory path should either start with a /' + INVALID_PATH_CHAR_TEXT = 'The directory path cannot include spaces or ' \ + 'any of these characters' + + 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.wait_until_visible('#new-project-name', poll=2) + 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 not checkbox.is_selected(): + checkbox.click() + + if self.PROJECT_NAME != 'TestProjectConfig': + # Reset project name if it's not the default one + self.PROJECT_NAME = 'TestProjectConfig' + + self.find("#create-project-button").click() + + try: + self.wait_until_visible('#hint-error-project-name', poll=2) + url = reverse('project', args=(TestProjectConfig.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 _get_config_nav_item(self, index): + config_nav = self.find('#config-nav') + return config_nav.find_elements(By.TAG_NAME, 'li')[index] + + def _navigate_bbv_page(self): + """ Navigate to project BitBake variables page """ + # check if the menu is displayed + if TestProjectConfig.project_id is None: + self._create_project(project_name=self._random_string(10)) + current_url = self.driver.current_url + TestProjectConfig.project_id = get_projectId_from_url(current_url) + else: + url = reverse('projectconf', args=(TestProjectConfig.project_id,)) + self.get(url) + self.wait_until_visible('#config-nav', poll=3) + bbv_page_link = self._get_config_nav_item(9) + bbv_page_link.click() + self.wait_until_visible('#config-nav', poll=3) + + def test_no_underscore_iamgefs_type(self): + """ + Should not accept IMAGEFS_TYPE with an underscore + """ + self._navigate_bbv_page() + imagefs_type = "foo_bar" + + self.wait_until_visible('#change-image_fstypes-icon', poll=2) + + self.click('#change-image_fstypes-icon') + + self.enter_text('#new-imagefs_types', imagefs_type) + + element = self.wait_until_visible('#hintError-image-fs_type', poll=2) + + self.assertTrue(("A valid image type cannot include underscores" in element.text), + "Did not find underscore error message") + + def test_checkbox_verification(self): + """ + Should automatically check the checkbox if user enters value + text box, if value is there in the checkbox. + """ + self._navigate_bbv_page() + + imagefs_type = "btrfs" + + self.wait_until_visible('#change-image_fstypes-icon', poll=2) + + self.click('#change-image_fstypes-icon') + + self.enter_text('#new-imagefs_types', imagefs_type) + + checkboxes = self.driver.find_elements(By.XPATH, "//input[@class='fs-checkbox-fstypes']") + + for checkbox in checkboxes: + if checkbox.get_attribute("value") == "btrfs": + self.assertEqual(checkbox.is_selected(), True) + + def test_textbox_with_checkbox_verification(self): + """ + Should automatically add or remove value in textbox, if user checks + or unchecks checkboxes. + """ + self._navigate_bbv_page() + + self.wait_until_visible('#change-image_fstypes-icon', poll=2) + + self.click('#change-image_fstypes-icon') + + checkboxes_selector = '.fs-checkbox-fstypes' + + self.wait_until_visible(checkboxes_selector, poll=2) + checkboxes = self.find_all(checkboxes_selector) + + for checkbox in checkboxes: + if checkbox.get_attribute("value") == "cpio": + checkbox.click() + element = self.driver.find_element(By.ID, 'new-imagefs_types') + + self.wait_until_visible('#new-imagefs_types', poll=2) + + self.assertTrue(("cpio" in element.get_attribute('value'), + "Imagefs not added into the textbox")) + checkbox.click() + self.assertTrue(("cpio" not in element.text), + "Image still present in the textbox") + + def test_set_download_dir(self): + """ + Validate the allowed and disallowed types in the directory field for + DL_DIR + """ + self._navigate_bbv_page() + + # activate the input to edit download dir + try: + change_dl_dir_btn = self.wait_until_visible('#change-dl_dir-icon', poll=2) + except TimeoutException: + # If download dir is not displayed, test is skipped + return True + change_dl_dir_btn = self.wait_until_visible('#change-dl_dir-icon', poll=2) + change_dl_dir_btn.click() + + # downloads dir path doesn't start with / or ${...} + input_field = self.wait_until_visible('#new-dl_dir', poll=2) + input_field.clear() + self.enter_text('#new-dl_dir', 'home/foo') + element = self.wait_until_visible('#hintError-initialChar-dl_dir', poll=2) + + msg = 'downloads directory path starts with invalid character but ' \ + 'treated as valid' + self.assertTrue((self.INVALID_PATH_START_TEXT in element.text), msg) + + # downloads dir path has a space + self.driver.find_element(By.ID, 'new-dl_dir').clear() + self.enter_text('#new-dl_dir', '/foo/bar a') + + element = self.wait_until_visible('#hintError-dl_dir', poll=2) + msg = 'downloads directory path characters invalid but treated as valid' + self.assertTrue((self.INVALID_PATH_CHAR_TEXT in element.text), msg) + + # downloads dir path starts with ${...} but has a space + self.driver.find_element(By.ID,'new-dl_dir').clear() + self.enter_text('#new-dl_dir', '${TOPDIR}/down foo') + + element = self.wait_until_visible('#hintError-dl_dir', poll=2) + msg = 'downloads directory path characters invalid but treated as valid' + self.assertTrue((self.INVALID_PATH_CHAR_TEXT in element.text), msg) + + # downloads dir path starts with / + self.driver.find_element(By.ID,'new-dl_dir').clear() + self.enter_text('#new-dl_dir', '/bar/foo') + + hidden_element = self.driver.find_element(By.ID,'hintError-dl_dir') + self.assertEqual(hidden_element.is_displayed(), False, + 'downloads directory path valid but treated as invalid') + + # downloads dir path starts with ${...} + self.driver.find_element(By.ID,'new-dl_dir').clear() + self.enter_text('#new-dl_dir', '${TOPDIR}/down') + + hidden_element = self.driver.find_element(By.ID,'hintError-dl_dir') + self.assertEqual(hidden_element.is_displayed(), False, + 'downloads directory path valid but treated as invalid') + + def test_set_sstate_dir(self): + """ + Validate the allowed and disallowed types in the directory field for + SSTATE_DIR + """ + self._navigate_bbv_page() + + try: + self.wait_until_visible('#change-sstate_dir-icon', poll=2) + self.click('#change-sstate_dir-icon') + except TimeoutException: + # If sstate_dir is not displayed, test is skipped + return True + + # path doesn't start with / or ${...} + input_field = self.wait_until_visible('#new-sstate_dir', poll=2) + input_field.clear() + self.enter_text('#new-sstate_dir', 'home/foo') + element = self.wait_until_visible('#hintError-initialChar-sstate_dir', poll=2) + + msg = 'sstate directory path starts with invalid character but ' \ + 'treated as valid' + self.assertTrue((self.INVALID_PATH_START_TEXT in element.text), msg) + + # path has a space + self.driver.find_element(By.ID, 'new-sstate_dir').clear() + self.enter_text('#new-sstate_dir', '/foo/bar a') + + element = self.wait_until_visible('#hintError-sstate_dir', poll=2) + msg = 'sstate directory path characters invalid but treated as valid' + self.assertTrue((self.INVALID_PATH_CHAR_TEXT in element.text), msg) + + # path starts with ${...} but has a space + self.driver.find_element(By.ID,'new-sstate_dir').clear() + self.enter_text('#new-sstate_dir', '${TOPDIR}/down foo') + + element = self.wait_until_visible('#hintError-sstate_dir', poll=2) + msg = 'sstate directory path characters invalid but treated as valid' + self.assertTrue((self.INVALID_PATH_CHAR_TEXT in element.text), msg) + + # path starts with / + self.driver.find_element(By.ID,'new-sstate_dir').clear() + self.enter_text('#new-sstate_dir', '/bar/foo') + + hidden_element = self.driver.find_element(By.ID, 'hintError-sstate_dir') + self.assertEqual(hidden_element.is_displayed(), False, + 'sstate directory path valid but treated as invalid') + + # paths starts with ${...} + self.driver.find_element(By.ID, 'new-sstate_dir').clear() + self.enter_text('#new-sstate_dir', '${TOPDIR}/down') + + hidden_element = self.driver.find_element(By.ID, 'hintError-sstate_dir') + self.assertEqual(hidden_element.is_displayed(), False, + 'sstate directory path valid but treated as invalid') + + def _change_bbv_value(self, **kwargs): + var_name, field, btn_id, input_id, value, save_btn, *_ = kwargs.values() + """ Change bitbake variable value """ + self._navigate_bbv_page() + self.wait_until_visible(f'#{btn_id}', poll=2) + if kwargs.get('new_variable'): + self.find(f"#{btn_id}").clear() + self.enter_text(f"#{btn_id}", f"{var_name}") + else: + self.click(f'#{btn_id}') + self.wait_until_visible(f'#{input_id}', poll=2) + + if kwargs.get('is_select'): + select = Select(self.find(f'#{input_id}')) + select.select_by_visible_text(value) + else: + self.find(f"#{input_id}").clear() + self.enter_text(f'#{input_id}', f'{value}') + self.click(f'#{save_btn}') + value_displayed = str(self.wait_until_visible(f'#{field}').text).lower() + msg = f'{var_name} variable not changed' + self.assertTrue(str(value).lower() in value_displayed, msg) + + def test_change_distro_var(self): + """ Test changing distro variable """ + self._change_bbv_value( + var_name='DISTRO', + field='distro', + btn_id='change-distro-icon', + input_id='new-distro', + value='poky-changed', + save_btn="apply-change-distro", + ) + + def test_set_image_install_append_var(self): + """ Test setting IMAGE_INSTALL:append variable """ + self._change_bbv_value( + var_name='IMAGE_INSTALL:append', + field='image_install', + btn_id='change-image_install-icon', + input_id='new-image_install', + value='bash, apt, busybox', + save_btn="apply-change-image_install", + ) + + def test_set_package_classes_var(self): + """ Test setting PACKAGE_CLASSES variable """ + self._change_bbv_value( + var_name='PACKAGE_CLASSES', + field='package_classes', + btn_id='change-package_classes-icon', + input_id='package_classes-select', + value='package_deb', + save_btn="apply-change-package_classes", + is_select=True, + ) + + def test_create_new_bbv(self): + """ Test creating new bitbake variable """ + self._change_bbv_value( + var_name='New_Custom_Variable', + field='configvar-list', + btn_id='variable', + input_id='value', + value='new variable value', + save_btn="add-configvar-button", + new_variable=True + ) diff --git a/lib/toaster/tests/functional/test_project_page_tab_config.py b/lib/toaster/tests/functional/test_project_page_tab_config.py index 23012d78..d911ff00 100644 --- a/lib/toaster/tests/functional/test_project_page_tab_config.py +++ b/lib/toaster/tests/functional/test_project_page_tab_config.py @@ -6,87 +6,81 @@ # SPDX-License-Identifier: GPL-2.0-only # -from time import sleep +import string +import random import pytest -from django.utils import timezone from django.urls import reverse from selenium.webdriver import Keys from selenium.webdriver.support.select import Select -from selenium.common.exceptions import NoSuchElementException -from orm.models import Build, Project, Target +from selenium.common.exceptions import TimeoutException +from orm.models import Project from tests.functional.functional_helpers import SeleniumFunctionalTestCase 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 TestProjectConfigTab(SeleniumFunctionalTestCase): + PROJECT_NAME = 'TestProjectConfigTab' + project_id = None - def setUp(self): - self.recipe = None - super().setUp() - release = '3' - project_name = 'projectmaster' - self._create_test_new_project( - project_name, - release, - False, - ) - - 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 != 'TestProjectConfigTab': + # Reset project name if it's not the default one + self.PROJECT_NAME = 'TestProjectConfigTab' + + self.find("#create-project-button").click() + + try: + self.wait_until_visible('#hint-error-project-name') + url = reverse('project', args=(TestProjectConfigTab.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 TestProjectConfigTab.project_id is None: + self._create_project(project_name=self._random_string(10)) + current_url = self.driver.current_url + TestProjectConfigTab.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() - - @classmethod - def _wait_until_build(cls, state): - while True: - try: - last_build_state = cls.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 - sleep(1) + url = reverse('project', args=(TestProjectConfigTab.project_id,)) + self.get(url) + self.wait_until_visible('#config-nav') def _create_builds(self): # check search box can be use to build recipes search_box = self.find('#build-input') search_box.send_keys('core-image-minimal') self.find('#build-button').click() - sleep(1) self.wait_until_visible('#latest-builds') # loop until reach the parsing state - self._wait_until_build('parsing starting cloning') + build_state = wait_until_build(self, 'parsing starting cloning') lastest_builds = self.driver.find_elements( By.XPATH, '//div[@id="latest-builds"]/div', @@ -100,8 +94,9 @@ class TestProjectConfigTab(SeleniumFunctionalTestCase): '//span[@class="cancel-build-btn pull-right alert-link"]', ) cancel_button.click() - sleep(1) - self._wait_until_build('cancelled') + if 'starting' not in build_state: # change build state when cancelled in starting state + wait_until_build_cancelled(self) + return build_state def _get_tabs(self): # tabs links list @@ -114,64 +109,6 @@ class TestProjectConfigTab(SeleniumFunctionalTestCase): config_nav = self.find('#config-nav') return config_nav.find_elements(By.TAG_NAME, 'li')[index] - 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), - 'started_on': now, - 'completed_on': now, - 'outcome': Build.SUCCEEDED - } - - self.project1_build_failure = { - 'project': Project.objects.get(id=1), - 'started_on': now, - 'completed_on': now, - 'outcome': Build.FAILED - } - build1 = Build.objects.create(**self.project1_build_success) - build2 = Build.objects.create(**self.project1_build_failure) - - # add some targets to these builds so they have recipe links - # (and so we can find the row in the ToasterTable corresponding to - # a particular build) - Target.objects.create(build=build1, target='foo') - Target.objects.create(build=build2, target='bar') - - if kwargs: - # Create kwargs.get('success') builds with success status with target - # and kwargs.get('failure') builds with failure status with target - for i in range(kwargs.get('success', 0)): - now = timezone.now() - self.project1_build_success['started_on'] = now - self.project1_build_success[ - 'completed_on'] = now - timezone.timedelta(days=i) - build = Build.objects.create(**self.project1_build_success) - Target.objects.create(build=build, - target=f'{i}_success_recipe', - task=f'{i}_success_task') - - for i in range(kwargs.get('failure', 0)): - now = timezone.now() - self.project1_build_failure['started_on'] = now - self.project1_build_failure[ - 'completed_on'] = now - timezone.timedelta(days=i) - build = Build.objects.create(**self.project1_build_failure) - Target.objects.create(build=build, - target=f'{i}_fail_recipe', - task=f'{i}_fail_task') - return build1, build2 - def test_project_config_nav(self): """ Test project config tab navigation: - Check if the menu is displayed and contains the right elements: @@ -188,13 +125,7 @@ class TestProjectConfigTab(SeleniumFunctionalTestCase): - Actions - Delete project """ - # navigate to the project page - url = reverse("project", args=(1,)) - self.get(url) - - # check if the menu is displayed - self.wait_until_visible('#config-nav') - + self._navigate_to_project_page() def _get_config_nav_item(index): config_nav = self.find('#config-nav') return config_nav.find_elements(By.TAG_NAME, 'li')[index] @@ -221,14 +152,14 @@ class TestProjectConfigTab(SeleniumFunctionalTestCase): self.assertTrue("actions" in str(actions.text).lower()) conf_nav_list = [ - [0, 'Configuration', f"/toastergui/project/1"], # config - [2, 'Custom images', f"/toastergui/project/1/customimages"], # custom images - [3, 'Image recipes', f"/toastergui/project/1/images"], # image recipes - [4, 'Software recipes', f"/toastergui/project/1/softwarerecipes"], # software recipes - [5, 'Machines', f"/toastergui/project/1/machines"], # machines - [6, 'Layers', f"/toastergui/project/1/layers"], # layers - [7, 'Distro', f"/toastergui/project/1/distro"], # distro - [9, 'BitBake variables', f"/toastergui/project/1/configuration"], # bitbake variables + [0, 'Configuration', f"/toastergui/project/{TestProjectConfigTab.project_id}"], # config + [2, 'Custom images', f"/toastergui/project/{TestProjectConfigTab.project_id}/customimages"], # custom images + [3, 'Image recipes', f"/toastergui/project/{TestProjectConfigTab.project_id}/images"], # image recipes + [4, 'Software recipes', f"/toastergui/project/{TestProjectConfigTab.project_id}/softwarerecipes"], # software recipes + [5, 'Machines', f"/toastergui/project/{TestProjectConfigTab.project_id}/machines"], # machines + [6, 'Layers', f"/toastergui/project/{TestProjectConfigTab.project_id}/layers"], # layers + [7, 'Distros', f"/toastergui/project/{TestProjectConfigTab.project_id}/distros"], # distro + # [9, 'BitBake variables', f"/toastergui/project/{TestProjectConfigTab.project_id}/configuration"], # bitbake variables ] for index, item_name, url in conf_nav_list: item = _get_config_nav_item(index) @@ -236,6 +167,96 @@ class TestProjectConfigTab(SeleniumFunctionalTestCase): item.click() check_config_nav_item(index, item_name, url) + def test_image_recipe_editColumn(self): + """ Test the edit column feature in image recipe table on project page """ + def test_edit_column(check_box_id): + # Check that we can hide/show table column + check_box = self.find(f'#{check_box_id}') + th_class = str(check_box_id).replace('checkbox-', '') + if check_box.is_selected(): + # check if column is visible in table + self.assertTrue( + self.find( + f'#imagerecipestable thead th.{th_class}' + ).is_displayed(), + f"The {th_class} column is checked in EditColumn dropdown, but it's not visible in table" + ) + check_box.click() + # check if column is hidden in table + self.assertFalse( + self.find( + f'#imagerecipestable thead th.{th_class}' + ).is_displayed(), + f"The {th_class} column is unchecked in EditColumn dropdown, but it's visible in table" + ) + else: + # check if column is hidden in table + self.assertFalse( + self.find( + f'#imagerecipestable thead th.{th_class}' + ).is_displayed(), + f"The {th_class} column is unchecked in EditColumn dropdown, but it's visible in table" + ) + check_box.click() + # check if column is visible in table + self.assertTrue( + self.find( + f'#imagerecipestable thead th.{th_class}' + ).is_displayed(), + f"The {th_class} column is checked in EditColumn dropdown, but it's not visible in table" + ) + + self._navigate_to_project_page() + # navigate to project image recipe page + recipe_image_page_link = self._get_config_nav_item(3) + recipe_image_page_link.click() + self.wait_until_present('#imagerecipestable tbody tr') + + # Check edit column + edit_column = self.find('#edit-columns-button') + self.assertTrue(edit_column.is_displayed()) + edit_column.click() + # Check dropdown is visible + self.wait_until_visible('ul.dropdown-menu.editcol') + + # Check that we can hide the edit column + test_edit_column('checkbox-get_description_or_summary') + test_edit_column('checkbox-layer_version__get_vcs_reference') + test_edit_column('checkbox-layer_version__layer__name') + test_edit_column('checkbox-license') + test_edit_column('checkbox-recipe-file') + test_edit_column('checkbox-section') + test_edit_column('checkbox-version') + + def test_image_recipe_show_rows(self): + """ Test the show rows feature in image recipe table on project page """ + def test_show_rows(row_to_show, show_row_link): + # Check that we can show rows == row_to_show + show_row_link.select_by_value(str(row_to_show)) + self.wait_until_visible('#imagerecipestable tbody tr') + self.assertTrue( + len(self.find_all('#imagerecipestable tbody tr')) == row_to_show + ) + + self._navigate_to_project_page() + # navigate to project image recipe page + recipe_image_page_link = self._get_config_nav_item(3) + recipe_image_page_link.click() + self.wait_until_present('#imagerecipestable tbody tr') + + show_rows = self.driver.find_elements( + By.XPATH, + '//select[@class="form-control pagesize-imagerecipestable"]' + ) + # Check show rows + for show_row_link in show_rows: + show_row_link = Select(show_row_link) + test_show_rows(10, show_row_link) + test_show_rows(25, show_row_link) + test_show_rows(50, show_row_link) + test_show_rows(100, show_row_link) + test_show_rows(150, show_row_link) + def test_project_config_tab_right_section(self): """ Test project config tab right section contains five blocks: - Machine: @@ -257,35 +278,36 @@ class TestProjectConfigTab(SeleniumFunctionalTestCase): - meta-poky - meta-yocto-bsp """ - # navigate to the project page - url = reverse("project", args=(1,)) - self.get(url) - + # Create a new project for this test + project_name = self._random_string(10) + self._create_project(project_name=project_name) + current_url = self.driver.current_url + TestProjectConfigTab.project_id = get_projectId_from_url(current_url) + url = current_url.split('?')[0] # check if the menu is displayed self.wait_until_visible('#project-page') block_l = self.driver.find_element( By.XPATH, '//*[@id="project-page"]/div[2]') - machine = self.find('#machine-section') - distro = self.find('#distro-section') most_built_recipes = self.driver.find_element( By.XPATH, '//*[@id="project-page"]/div[1]/div[3]') project_release = self.driver.find_element( By.XPATH, '//*[@id="project-page"]/div[1]/div[4]') layers = block_l.find_element(By.ID, 'layer-container') - def check_machine_distro(self, item_name, new_item_name, block): + def check_machine_distro(self, item_name, new_item_name, block_id): + block = self.find(f'#{block_id}') title = block.find_element(By.TAG_NAME, 'h3') self.assertTrue(item_name.capitalize() in title.text) - edit_btn = block.find_element(By.ID, f'change-{item_name}-toggle') + edit_btn = self.find(f'#change-{item_name}-toggle') edit_btn.click() - sleep(1) - name_input = block.find_element(By.ID, f'{item_name}-change-input') + self.wait_until_visible(f'#{item_name}-change-input') + name_input = self.find(f'#{item_name}-change-input') name_input.clear() name_input.send_keys(new_item_name) - change_btn = block.find_element(By.ID, f'{item_name}-change-btn') + change_btn = self.find(f'#{item_name}-change-btn') change_btn.click() - sleep(1) - project_name = block.find_element(By.ID, f'project-{item_name}-name') + self.wait_until_visible(f'#project-{item_name}-name') + project_name = self.find(f'#project-{item_name}-name') self.assertTrue(new_item_name in project_name.text) # check change notificaiton is displayed change_notification = self.find('#change-notification') @@ -293,10 +315,30 @@ class TestProjectConfigTab(SeleniumFunctionalTestCase): f'You have changed the {item_name} to: {new_item_name}' in change_notification.text ) + def rebuild_from_most_build_recipes(recipe_list_items): + checkbox = recipe_list_items[0].find_element(By.TAG_NAME, 'input') + checkbox.click() + build_btn = self.find('#freq-build-btn') + build_btn.click() + self.wait_until_visible('#latest-builds') + build_state = wait_until_build(self, 'parsing starting cloning queued') + lastest_builds = self.driver.find_elements( + By.XPATH, + '//div[@id="latest-builds"]/div' + ) + last_build = lastest_builds[0] + self.assertTrue(len(lastest_builds) >= 2) + 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) # Machine - check_machine_distro(self, 'machine', 'qemux86-64', machine) + check_machine_distro(self, 'machine', 'qemux86-64', 'machine-section') # Distro - check_machine_distro(self, 'distro', 'poky-altcfg', distro) + check_machine_distro(self, 'distro', 'poky-altcfg', 'distro-section') # Project release title = project_release.find_element(By.TAG_NAME, 'h3') @@ -304,7 +346,6 @@ class TestProjectConfigTab(SeleniumFunctionalTestCase): self.assertTrue( "Yocto Project master" in self.find('#project-release-title').text ) - # Layers title = layers.find_element(By.TAG_NAME, 'h3') self.assertTrue("Layers" in title.text) @@ -314,7 +355,9 @@ class TestProjectConfigTab(SeleniumFunctionalTestCase): # meta-yocto-bsp layers_list = layers.find_element(By.ID, 'layers-in-project-list') layers_list_items = layers_list.find_elements(By.TAG_NAME, 'li') - self.assertTrue(len(layers_list_items) == 3) + # remove all layers except the first three layers + for i in range(3, len(layers_list_items)): + layers_list_items[i].find_element(By.TAG_NAME, 'span').click() # check can add a layer if exists add_layer_input = layers.find_element(By.ID, 'layer-add-input') add_layer_input.send_keys('meta-oe') @@ -326,7 +369,7 @@ class TestProjectConfigTab(SeleniumFunctionalTestCase): dropdown_item.click() add_layer_btn = layers.find_element(By.ID, 'add-layer-btn') add_layer_btn.click() - sleep(1) + self.wait_until_visible('#layers-in-project-list') # check layer is added layers_list_items = layers_list.find_elements(By.TAG_NAME, 'li') self.assertTrue(len(layers_list_items) == 4) @@ -334,48 +377,33 @@ class TestProjectConfigTab(SeleniumFunctionalTestCase): # Most built recipes title = most_built_recipes.find_element(By.TAG_NAME, 'h3') self.assertTrue("Most built recipes" in title.text) - # Create a new builds 5 - self._create_builds() + # Create a new builds + build_state = self._create_builds() # Refresh the page - self.get(url) + self.driver.get(url) - sleep(1) # wait for page to load - self.wait_until_visible('#project-page') + self.wait_until_visible('#project-page', poll=3) # check can select a recipe and build it most_built_recipes = self.driver.find_element( By.XPATH, '//*[@id="project-page"]/div[1]/div[3]') recipe_list = most_built_recipes.find_element(By.ID, 'freq-build-list') recipe_list_items = recipe_list.find_elements(By.TAG_NAME, 'li') - self.assertTrue( - len(recipe_list_items) > 0, - msg="No recipes found in the most built recipes list", - ) - checkbox = recipe_list_items[0].find_element(By.TAG_NAME, 'input') - checkbox.click() - build_btn = self.find('#freq-build-btn') - build_btn.click() - sleep(1) # wait for page to load - self.wait_until_visible('#latest-builds') - self._wait_until_build('parsing starting cloning queueing') - lastest_builds = self.driver.find_elements( - By.XPATH, - '//div[@id="latest-builds"]/div' - ) - 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() - self.assertTrue(len(lastest_builds) == 2) + if 'starting' not in build_state: # Build will not appear in the list if canceled in starting state + self.assertTrue( + len(recipe_list_items) > 0, + msg="No recipes found in the most built recipes list", + ) + rebuild_from_most_build_recipes(recipe_list_items) + else: + self.assertTrue( + len(recipe_list_items) == 0, + msg="Recipes found in the most built recipes list", + ) def test_project_page_tab_importlayer(self): """ Test project page tab import layer """ - # navigate to the project page - url = reverse("project", args=(1,)) - self.get(url) - + self._navigate_to_project_page() # navigate to "Import layers" tab import_layers_tab = self._get_tabs()[2] import_layers_tab.find_element(By.TAG_NAME, 'a').click() @@ -415,10 +443,10 @@ class TestProjectConfigTab(SeleniumFunctionalTestCase): def test_project_page_custom_image_no_image(self): """ Test project page tab "New custom image" when no custom image """ - # navigate to the project page - url = reverse("project", args=(1,)) - self.get(url) - + project_name = self._random_string(10) + self._create_project(project_name=project_name) + current_url = self.driver.current_url + TestProjectConfigTab.project_id = get_projectId_from_url(current_url) # navigate to "Custom image" tab custom_image_section = self._get_config_nav_item(2) custom_image_section.click() @@ -433,8 +461,10 @@ class TestProjectConfigTab(SeleniumFunctionalTestCase): div_empty_msg = self.find('#empty-state-customimagestable') link_create_custom_image = div_empty_msg.find_element( By.TAG_NAME, 'a') + last_project_id = Project.objects.get(name=project_name).id + self.assertTrue(last_project_id is not None) self.assertTrue( - f"/toastergui/project/1/newcustomimage" in str( + f"/toastergui/project/{last_project_id}/newcustomimage" in str( link_create_custom_image.get_attribute('href') ) ) @@ -451,11 +481,7 @@ class TestProjectConfigTab(SeleniumFunctionalTestCase): - Check image recipe build button works - Check image recipe table features(show/hide column, pagination) """ - # navigate to the project page - url = reverse("project", args=(1,)) - self.get(url) - self.wait_until_visible('#config-nav') - + self._navigate_to_project_page() # navigate to "Images section" images_section = self._get_config_nav_item(3) images_section.click() @@ -479,100 +505,17 @@ class TestProjectConfigTab(SeleniumFunctionalTestCase): '//td[@class="add-del-layers"]' ) build_btn.click() - self._wait_until_build('parsing starting cloning') + 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) - - 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) - - def test_edit_column(check_box_id): - # Check that we can hide/show table column - check_box = self.find(f'#{check_box_id}') - th_class = str(check_box_id).replace('checkbox-', '') - if check_box.is_selected(): - # check if column is visible in table - self.assertTrue( - self.find( - f'#imagerecipestable thead th.{th_class}' - ).is_displayed(), - f"The {th_class} column is checked in EditColumn dropdown, but it's not visible in table" - ) - check_box.click() - # check if column is hidden in table - self.assertFalse( - self.find( - f'#imagerecipestable thead th.{th_class}' - ).is_displayed(), - f"The {th_class} column is unchecked in EditColumn dropdown, but it's visible in table" - ) - else: - # check if column is hidden in table - self.assertFalse( - self.find( - f'#imagerecipestable thead th.{th_class}' - ).is_displayed(), - f"The {th_class} column is unchecked in EditColumn dropdown, but it's visible in table" - ) - check_box.click() - # check if column is visible in table - self.assertTrue( - self.find( - f'#imagerecipestable thead th.{th_class}' - ).is_displayed(), - f"The {th_class} column is checked in EditColumn dropdown, but it's not visible in table" - ) - - url = reverse('projectimagerecipes', args=(1,)) - self.get(url) - self.wait_until_present('#imagerecipestable tbody tr') - - # Check edit column - edit_column = self.find('#edit-columns-button') - self.assertTrue(edit_column.is_displayed()) - edit_column.click() - # Check dropdown is visible - self.wait_until_visible('ul.dropdown-menu.editcol') - - # Check that we can hide the edit column - test_edit_column('checkbox-get_description_or_summary') - test_edit_column('checkbox-layer_version__get_vcs_reference') - test_edit_column('checkbox-layer_version__layer__name') - test_edit_column('checkbox-license') - test_edit_column('checkbox-recipe-file') - test_edit_column('checkbox-section') - test_edit_column('checkbox-version') - - def test_image_recipe_show_rows(self): - """ Test the show rows feature in image recipe table on project page """ - self._get_create_builds(success=100, failure=100) - - def test_show_rows(row_to_show, show_row_link): - # Check that we can show rows == row_to_show - show_row_link.select_by_value(str(row_to_show)) - self.wait_until_present('#imagerecipestable tbody tr') - sleep(1) - self.assertTrue( - len(self.find_all('#imagerecipestable tbody tr')) == row_to_show - ) - - url = reverse('projectimagerecipes', args=(2,)) - self.get(url) - self.wait_until_present('#imagerecipestable tbody tr') - - show_rows = self.driver.find_elements( + last_build = lastest_builds[0] + cancel_button = last_build.find_element( By.XPATH, - '//select[@class="form-control pagesize-imagerecipestable"]' + '//span[@class="cancel-build-btn pull-right alert-link"]', ) - # Check show rows - for show_row_link in show_rows: - show_row_link = Select(show_row_link) - test_show_rows(10, show_row_link) - test_show_rows(25, show_row_link) - test_show_rows(50, show_row_link) - test_show_rows(100, show_row_link) - test_show_rows(150, show_row_link) + cancel_button.click() + if 'starting' not in build_state: # change build state when cancelled in starting state + wait_until_build_cancelled(self) 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