* [PATCH 1/4] toaster/test: Ensure to kill toaster process create for tests functional
@ 2023-12-08 1:53 Alassane Yattara
2023-12-08 1:53 ` [PATCH 2/4] toaster/test: Added functional/utils, contains useful methods using by functional tests Alassane Yattara
` (2 more replies)
0 siblings, 3 replies; 4+ messages in thread
From: Alassane Yattara @ 2023-12-08 1:53 UTC (permalink / raw)
To: toaster; +Cc: Alassane Yattara
Toaster background task runbuilds continu running when even if tests is
done
Signed-off-by: Alassane Yattara <alassane.yattara@savoirfairelinux.com>
---
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):
--
2.34.1
^ permalink raw reply related [flat|nested] 4+ messages in thread
* [PATCH 2/4] toaster/test: Added functional/utils, contains useful methods using by functional tests
2023-12-08 1:53 [PATCH 1/4] toaster/test: Ensure to kill toaster process create for tests functional Alassane Yattara
@ 2023-12-08 1:53 ` Alassane Yattara
2023-12-08 1:53 ` [PATCH 3/4] toaster/test: Refactorize tests/functional Alassane Yattara
2023-12-08 1:53 ` [PATCH 4/4] toaster/test: Bug fixes, functional tests dependent on each other Alassane Yattara
2 siblings, 0 replies; 4+ messages in thread
From: Alassane Yattara @ 2023-12-08 1:53 UTC (permalink / raw)
To: toaster; +Cc: Alassane Yattara
Signed-off-by: Alassane Yattara <alassane.yattara@savoirfairelinux.com>
---
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
--
2.34.1
^ permalink raw reply related [flat|nested] 4+ messages in thread
* [PATCH 3/4] toaster/test: Refactorize tests/functional
2023-12-08 1:53 [PATCH 1/4] toaster/test: Ensure to kill toaster process create for tests functional Alassane Yattara
2023-12-08 1:53 ` [PATCH 2/4] toaster/test: Added functional/utils, contains useful methods using by functional tests Alassane Yattara
@ 2023-12-08 1:53 ` Alassane Yattara
2023-12-08 1:53 ` [PATCH 4/4] toaster/test: Bug fixes, functional tests dependent on each other Alassane Yattara
2 siblings, 0 replies; 4+ messages in thread
From: Alassane Yattara @ 2023-12-08 1:53 UTC (permalink / raw)
To: toaster; +Cc: Alassane Yattara
- 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 <alassane.yattara@savoirfairelinux.com>
---
.../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)
--
2.34.1
^ permalink raw reply related [flat|nested] 4+ messages in thread
* [PATCH 4/4] toaster/test: Bug fixes, functional tests dependent on each other
2023-12-08 1:53 [PATCH 1/4] toaster/test: Ensure to kill toaster process create for tests functional Alassane Yattara
2023-12-08 1:53 ` [PATCH 2/4] toaster/test: Added functional/utils, contains useful methods using by functional tests Alassane Yattara
2023-12-08 1:53 ` [PATCH 3/4] toaster/test: Refactorize tests/functional Alassane Yattara
@ 2023-12-08 1:53 ` Alassane Yattara
2 siblings, 0 replies; 4+ messages in thread
From: Alassane Yattara @ 2023-12-08 1:53 UTC (permalink / raw)
To: toaster; +Cc: Alassane Yattara
refactor test_create_project and test_project_page to remove their dependencies
Signed-off-by: Alassane Yattara <alassane.yattara@savoirfairelinux.com>
---
.../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
--
2.34.1
^ permalink raw reply related [flat|nested] 4+ messages in thread
end of thread, other threads:[~2023-12-08 1:54 UTC | newest]
Thread overview: 4+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2023-12-08 1:53 [PATCH 1/4] toaster/test: Ensure to kill toaster process create for tests functional Alassane Yattara
2023-12-08 1:53 ` [PATCH 2/4] toaster/test: Added functional/utils, contains useful methods using by functional tests Alassane Yattara
2023-12-08 1:53 ` [PATCH 3/4] toaster/test: Refactorize tests/functional Alassane Yattara
2023-12-08 1:53 ` [PATCH 4/4] toaster/test: Bug fixes, functional tests dependent on each other Alassane Yattara
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).