Complete Yocto mirror with license table for TQMa6UL (2038-compliance)
- 264 license table entries with exact download URLs (224/264 resolved) - Complete sources/ directory with all BitBake recipes - Build configuration: tqma6ul-multi-mba6ulx, spaetzle (musl) - Full traceability for Softwarefreigabeantrag - GCC 13.4.0, Linux 6.6.102, U-Boot 2023.04, musl 1.2.4 - License distribution: GPL-2.0 (24), MIT (23), GPL-2.0+ (18), BSD-3 (16)
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
#! /usr/bin/env python3
|
||||
#
|
||||
# BitBake Toaster functional tests implementation
|
||||
#
|
||||
# Copyright (C) 2017 Intel Corporation
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
|
||||
import os
|
||||
import logging
|
||||
import subprocess
|
||||
import signal
|
||||
import re
|
||||
|
||||
from tests.browser.selenium_helpers_base import SeleniumTestCaseBase
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.common.exceptions import NoSuchElementException
|
||||
|
||||
logger = logging.getLogger("toaster")
|
||||
toaster_processes = []
|
||||
|
||||
class SeleniumFunctionalTestCase(SeleniumTestCaseBase):
|
||||
wait_toaster_time = 10
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
# So that the buildinfo helper uses the test database'
|
||||
if os.environ.get('DJANGO_SETTINGS_MODULE', '') != \
|
||||
'toastermain.settings_test':
|
||||
raise RuntimeError("Please initialise django with the tests settings: "
|
||||
"DJANGO_SETTINGS_MODULE='toastermain.settings_test'")
|
||||
|
||||
# Wait for any known toaster processes to exit
|
||||
global toaster_processes
|
||||
for toaster_process in toaster_processes:
|
||||
try:
|
||||
os.waitpid(toaster_process, os.WNOHANG)
|
||||
except ChildProcessError:
|
||||
pass
|
||||
|
||||
# start toaster
|
||||
cmd = "bash -c 'source toaster start'"
|
||||
start_process = subprocess.Popen(
|
||||
cmd,
|
||||
cwd=os.environ.get("BUILDDIR"),
|
||||
shell=True)
|
||||
toaster_processes = [start_process.pid]
|
||||
if start_process.wait() != 0:
|
||||
port_use = os.popen("lsof -i -P -n | grep '8000 (LISTEN)'").read().strip()
|
||||
message = ''
|
||||
if port_use:
|
||||
process_id = port_use.split()[1]
|
||||
process = os.popen(f"ps -o cmd= -p {process_id}").read().strip()
|
||||
message = f"Port 8000 occupied by {process}"
|
||||
raise RuntimeError(f"Can't initialize toaster. {message}")
|
||||
|
||||
builddir = os.environ.get("BUILDDIR")
|
||||
with open(os.path.join(builddir, '.toastermain.pid'), 'r') as f:
|
||||
toaster_processes.append(int(f.read()))
|
||||
with open(os.path.join(builddir, '.runbuilds.pid'), 'r') as f:
|
||||
toaster_processes.append(int(f.read()))
|
||||
|
||||
super(SeleniumFunctionalTestCase, cls).setUpClass()
|
||||
cls.live_server_url = 'http://localhost:8000/'
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
super(SeleniumFunctionalTestCase, cls).tearDownClass()
|
||||
|
||||
global toaster_processes
|
||||
|
||||
cmd = "bash -c 'source toaster stop'"
|
||||
stop_process = subprocess.Popen(
|
||||
cmd,
|
||||
cwd=os.environ.get("BUILDDIR"),
|
||||
shell=True)
|
||||
# Toaster stop has been known to hang in these tests so force kill if it stalls
|
||||
try:
|
||||
if stop_process.wait(cls.wait_toaster_time) != 0:
|
||||
raise Exception('Toaster stop process failed')
|
||||
except Exception as e:
|
||||
if e is subprocess.TimeoutExpired:
|
||||
print('Toaster stop process took too long. Force killing toaster...')
|
||||
else:
|
||||
print('Toaster stop process failed. Force killing toaster...')
|
||||
stop_process.kill()
|
||||
for toaster_process in toaster_processes:
|
||||
os.kill(toaster_process, signal.SIGTERM)
|
||||
|
||||
|
||||
def get_URL(self):
|
||||
rc=self.get_page_source()
|
||||
project_url=re.search(r"(projectPageUrl\s:\s\")(.*)(\",)",rc)
|
||||
return project_url.group(2)
|
||||
|
||||
|
||||
def find_element_by_link_text_in_table(self, table_id, link_text):
|
||||
"""
|
||||
Assume there're multiple suitable "find_element_by_link_text".
|
||||
In this circumstance we need to specify "table".
|
||||
"""
|
||||
try:
|
||||
table_element = self.get_table_element(table_id)
|
||||
element = table_element.find_element(By.LINK_TEXT, link_text)
|
||||
except NoSuchElementException:
|
||||
print('no element found')
|
||||
raise
|
||||
return element
|
||||
|
||||
def get_table_element(self, table_id, *coordinate):
|
||||
if len(coordinate) == 0:
|
||||
#return whole-table element
|
||||
element_xpath = "//*[@id='" + table_id + "']"
|
||||
try:
|
||||
element = self.driver.find_element(By.XPATH, element_xpath)
|
||||
except NoSuchElementException:
|
||||
raise
|
||||
return element
|
||||
row = coordinate[0]
|
||||
|
||||
if len(coordinate) == 1:
|
||||
#return whole-row element
|
||||
element_xpath = "//*[@id='" + table_id + "']/tbody/tr[" + str(row) + "]"
|
||||
try:
|
||||
element = self.driver.find_element(By.XPATH, element_xpath)
|
||||
except NoSuchElementException:
|
||||
return False
|
||||
return element
|
||||
#now we are looking for an element with specified X and Y
|
||||
column = coordinate[1]
|
||||
|
||||
element_xpath = "//*[@id='" + table_id + "']/tbody/tr[" + str(row) + "]/td[" + str(column) + "]"
|
||||
try:
|
||||
element = self.driver.find_element(By.XPATH, element_xpath)
|
||||
except NoSuchElementException:
|
||||
return False
|
||||
return element
|
||||
@@ -0,0 +1,179 @@
|
||||
#! /usr/bin/env python3
|
||||
# BitBake Toaster UI tests implementation
|
||||
#
|
||||
# Copyright (C) 2023 Savoir-faire Linux
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
|
||||
import re
|
||||
import pytest
|
||||
from django.urls import reverse
|
||||
from selenium.webdriver.support.select import Select
|
||||
from tests.functional.functional_helpers import SeleniumFunctionalTestCase
|
||||
from orm.models import Project
|
||||
from selenium.webdriver.common.by import By
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.order("last")
|
||||
class TestCreateNewProject(SeleniumFunctionalTestCase):
|
||||
|
||||
def _create_test_new_project(
|
||||
self,
|
||||
project_name,
|
||||
release,
|
||||
release_title,
|
||||
merge_toaster_settings,
|
||||
):
|
||||
""" 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=3)
|
||||
self.driver.find_element(By.ID,
|
||||
"new-project-name").send_keys(project_name)
|
||||
|
||||
select = Select(self.find('#projectversion'))
|
||||
select.select_by_value(release)
|
||||
|
||||
# check merge toaster settings
|
||||
checkbox = self.find('.checkbox-mergeattr')
|
||||
if merge_toaster_settings:
|
||||
if not checkbox.is_selected():
|
||||
checkbox.click()
|
||||
else:
|
||||
if checkbox.is_selected():
|
||||
checkbox.click()
|
||||
|
||||
self.driver.find_element(By.ID, "create-project-button").click()
|
||||
|
||||
element = self.wait_until_visible('#project-created-notification', poll=3)
|
||||
self.assertTrue(
|
||||
self.element_exists('#project-created-notification'),
|
||||
f"Project:{project_name} creation notification not shown"
|
||||
)
|
||||
self.assertTrue(
|
||||
project_name in element.text,
|
||||
f"New project name:{project_name} not in new project notification"
|
||||
)
|
||||
self.assertTrue(
|
||||
Project.objects.filter(name=project_name).count(),
|
||||
f"New project:{project_name} not found in database"
|
||||
)
|
||||
|
||||
# check release
|
||||
self.assertTrue(re.search(
|
||||
release_title,
|
||||
self.driver.find_element(By.XPATH,
|
||||
"//span[@id='project-release-title']"
|
||||
).text),
|
||||
'The project release is not defined')
|
||||
|
||||
def test_create_new_project_master(self):
|
||||
""" Test create new project using:
|
||||
- Project Name: Any string
|
||||
- Release: Yocto Project master (option value: 3)
|
||||
- Merge Toaster settings: False
|
||||
"""
|
||||
release = '3'
|
||||
release_title = 'Yocto Project master'
|
||||
project_name = 'projectmaster'
|
||||
self._create_test_new_project(
|
||||
project_name,
|
||||
release,
|
||||
release_title,
|
||||
False,
|
||||
)
|
||||
|
||||
def test_create_new_project_kirkstone(self):
|
||||
""" Test create new project using:
|
||||
- Project Name: Any string
|
||||
- Release: Yocto Project 4.0 "Kirkstone" (option value: 1)
|
||||
- Merge Toaster settings: True
|
||||
"""
|
||||
release = '1'
|
||||
release_title = 'Yocto Project 4.0 "Kirkstone"'
|
||||
project_name = 'projectkirkstone'
|
||||
self._create_test_new_project(
|
||||
project_name,
|
||||
release,
|
||||
release_title,
|
||||
True,
|
||||
)
|
||||
|
||||
def test_create_new_project_dunfell(self):
|
||||
""" Test create new project using:
|
||||
- Project Name: Any string
|
||||
- Release: Yocto Project 3.1 "Dunfell" (option value: 5)
|
||||
- Merge Toaster settings: False
|
||||
"""
|
||||
release = '5'
|
||||
release_title = 'Yocto Project 3.1 "Dunfell"'
|
||||
project_name = 'projectdunfell'
|
||||
self._create_test_new_project(
|
||||
project_name,
|
||||
release,
|
||||
release_title,
|
||||
False,
|
||||
)
|
||||
|
||||
def test_create_new_project_local(self):
|
||||
""" Test create new project using:
|
||||
- Project Name: Any string
|
||||
- Release: Local Yocto Project (option value: 2)
|
||||
- Merge Toaster settings: True
|
||||
"""
|
||||
release = '2'
|
||||
release_title = 'Local Yocto Project'
|
||||
project_name = 'projectlocal'
|
||||
self._create_test_new_project(
|
||||
project_name,
|
||||
release,
|
||||
release_title,
|
||||
True,
|
||||
)
|
||||
|
||||
def test_create_new_project_without_name(self):
|
||||
""" Test create new project without project name """
|
||||
self.get(reverse('newproject'))
|
||||
|
||||
select = Select(self.find('#projectversion'))
|
||||
select.select_by_value(str(3))
|
||||
|
||||
# Check input name has required attribute
|
||||
input_name = self.driver.find_element(By.ID, "new-project-name")
|
||||
self.assertIsNotNone(input_name.get_attribute('required'),
|
||||
'Input name has not required attribute')
|
||||
|
||||
# Check create button is disabled
|
||||
create_btn = self.driver.find_element(By.ID, "create-project-button")
|
||||
self.assertIsNotNone(create_btn.get_attribute('disabled'),
|
||||
'Create button is not disabled')
|
||||
|
||||
def test_import_new_project(self):
|
||||
""" Test import new project using:
|
||||
- Project Name: Any string
|
||||
- Project type: select (Import command line project)
|
||||
- Import existing project directory: Wrong Path
|
||||
"""
|
||||
project_name = 'projectimport'
|
||||
self.get(reverse('newproject'))
|
||||
self.driver.find_element(By.ID,
|
||||
"new-project-name").send_keys(project_name)
|
||||
# select import project
|
||||
self.find('#type-import').click()
|
||||
|
||||
# set wrong path
|
||||
wrong_path = '/wrongpath'
|
||||
self.driver.find_element(By.ID,
|
||||
"import-project-dir").send_keys(wrong_path)
|
||||
self.driver.find_element(By.ID, "create-project-button").click()
|
||||
|
||||
# check error message
|
||||
self.assertTrue(self.element_exists('.alert-danger'),
|
||||
'Allert message not shown')
|
||||
self.assertTrue(wrong_path in self.find('.alert-danger').text,
|
||||
"Wrong path not in alert message")
|
||||
@@ -0,0 +1,257 @@
|
||||
#! /usr/bin/env python3
|
||||
#
|
||||
# BitBake Toaster functional tests implementation
|
||||
#
|
||||
# Copyright (C) 2017 Intel Corporation
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
|
||||
import re
|
||||
from django.urls import reverse
|
||||
import pytest
|
||||
from tests.functional.functional_helpers import SeleniumFunctionalTestCase
|
||||
from orm.models import Project
|
||||
from selenium.webdriver.common.by import By
|
||||
|
||||
from tests.functional.utils import get_projectId_from_url
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.order("second_to_last")
|
||||
class FuntionalTestBasic(SeleniumFunctionalTestCase):
|
||||
"""Basic functional tests for Toaster"""
|
||||
project_id = None
|
||||
|
||||
def setUp(self):
|
||||
super(FuntionalTestBasic, self).setUp()
|
||||
if not FuntionalTestBasic.project_id:
|
||||
self._create_slenium_project()
|
||||
current_url = self.driver.current_url
|
||||
FuntionalTestBasic.project_id = get_projectId_from_url(current_url)
|
||||
|
||||
# testcase (1514)
|
||||
def _create_slenium_project(self):
|
||||
project_name = 'selenium-project'
|
||||
self.get(reverse('newproject'))
|
||||
self.wait_until_visible('#new-project-name', poll=3)
|
||||
self.driver.find_element(By.ID, "new-project-name").send_keys(project_name)
|
||||
self.driver.find_element(By.ID, 'projectversion').click()
|
||||
self.driver.find_element(By.ID, "create-project-button").click()
|
||||
element = self.wait_until_visible('#project-created-notification', poll=10)
|
||||
self.assertTrue(self.element_exists('#project-created-notification'),'Project creation notification not shown')
|
||||
self.assertTrue(project_name in element.text,
|
||||
"New project name not in new project notification")
|
||||
self.assertTrue(Project.objects.filter(name=project_name).count(),
|
||||
"New project not found in database")
|
||||
return Project.objects.last().id
|
||||
|
||||
# testcase (1515)
|
||||
def test_verify_left_bar_menu(self):
|
||||
self.get(reverse('all-projects'))
|
||||
self.wait_until_present('#projectstable', poll=10)
|
||||
self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click()
|
||||
self.wait_until_present('#config-nav', poll=10)
|
||||
self.assertTrue(self.element_exists('#config-nav'),'Configuration Tab does not exist')
|
||||
project_URL=self.get_URL()
|
||||
self.driver.find_element(By.XPATH, '//a[@href="'+project_URL+'"]').click()
|
||||
self.wait_until_present('#config-nav', poll=10)
|
||||
|
||||
try:
|
||||
self.driver.find_element(By.XPATH, "//*[@id='config-nav']/ul/li/a[@href="+'"'+project_URL+'customimages/"'+"]").click()
|
||||
self.wait_until_present('#config-nav', poll=10)
|
||||
self.assertTrue(re.search("Custom images",self.driver.find_element(By.XPATH, "//div[@class='col-md-10']").text),'Custom images information is not loading properly')
|
||||
except:
|
||||
self.fail(msg='No Custom images tab available')
|
||||
|
||||
try:
|
||||
self.driver.find_element(By.XPATH, "//*[@id='config-nav']/ul/li/a[@href="+'"'+project_URL+'images/"'+"]").click()
|
||||
self.wait_until_present('#config-nav', poll=10)
|
||||
self.assertTrue(re.search("Compatible image recipes",self.driver.find_element(By.XPATH, "//div[@class='col-md-10']").text),'The Compatible image recipes information is not loading properly')
|
||||
except:
|
||||
self.fail(msg='No Compatible image tab available')
|
||||
|
||||
try:
|
||||
self.driver.find_element(By.XPATH, "//*[@id='config-nav']/ul/li/a[@href="+'"'+project_URL+'softwarerecipes/"'+"]").click()
|
||||
self.wait_until_present('#config-nav', poll=10)
|
||||
self.assertTrue(re.search("Compatible software recipes",self.driver.find_element(By.XPATH, "//div[@class='col-md-10']").text),'The Compatible software recipe information is not loading properly')
|
||||
except:
|
||||
self.fail(msg='No Compatible software recipe tab available')
|
||||
|
||||
try:
|
||||
self.driver.find_element(By.XPATH, "//*[@id='config-nav']/ul/li/a[@href="+'"'+project_URL+'machines/"'+"]").click()
|
||||
self.wait_until_present('#config-nav', poll=10)
|
||||
self.assertTrue(re.search("Compatible machines",self.driver.find_element(By.XPATH, "//div[@class='col-md-10']").text),'The Compatible machine information is not loading properly')
|
||||
except:
|
||||
self.fail(msg='No Compatible machines tab available')
|
||||
|
||||
try:
|
||||
self.driver.find_element(By.XPATH, "//*[@id='config-nav']/ul/li/a[@href="+'"'+project_URL+'layers/"'+"]").click()
|
||||
self.wait_until_present('#config-nav', poll=10)
|
||||
self.assertTrue(re.search("Compatible layers",self.driver.find_element(By.XPATH, "//div[@class='col-md-10']").text),'The Compatible layer information is not loading properly')
|
||||
except:
|
||||
self.fail(msg='No Compatible layers tab available')
|
||||
|
||||
try:
|
||||
self.driver.find_element(By.XPATH, "//*[@id='config-nav']/ul/li/a[@href="+'"'+project_URL+'configuration"'+"]").click()
|
||||
self.wait_until_present('#config-nav', poll=10)
|
||||
self.assertTrue(re.search("Bitbake variables",self.driver.find_element(By.XPATH, "//div[@class='col-md-10']").text),'The Bitbake variables information is not loading properly')
|
||||
except:
|
||||
self.fail(msg='No Bitbake variables tab available')
|
||||
|
||||
# testcase (1516)
|
||||
def test_review_configuration_information(self):
|
||||
self.get(reverse('all-projects'))
|
||||
self.wait_until_present('#projectstable', poll=10)
|
||||
self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click()
|
||||
project_URL=self.get_URL()
|
||||
self.wait_until_present('#config-nav', poll=10)
|
||||
try:
|
||||
self.assertTrue(self.element_exists('#machine-section'),'Machine section for the project configuration page does not exist')
|
||||
self.assertTrue(re.search("qemux86-64",self.driver.find_element(By.XPATH, "//span[@id='project-machine-name']").text),'The machine type is not assigned')
|
||||
self.driver.find_element(By.XPATH, "//span[@id='change-machine-toggle']").click()
|
||||
self.wait_until_visible('#select-machine-form', poll=10)
|
||||
self.wait_until_visible('#cancel-machine-change', poll=10)
|
||||
self.driver.find_element(By.XPATH, "//form[@id='select-machine-form']/a[@id='cancel-machine-change']").click()
|
||||
except:
|
||||
self.fail(msg='The machine information is wrong in the configuration page')
|
||||
|
||||
try:
|
||||
self.driver.find_element(By.ID, 'no-most-built')
|
||||
except:
|
||||
self.fail(msg='No Most built information in project detail page')
|
||||
|
||||
try:
|
||||
self.assertTrue(re.search("Yocto Project master",self.driver.find_element(By.XPATH, "//span[@id='project-release-title']").text),'The project release is not defined')
|
||||
except:
|
||||
self.fail(msg='No project release title information in project detail page')
|
||||
|
||||
try:
|
||||
self.driver.find_element(By.XPATH, "//div[@id='layer-container']")
|
||||
self.assertTrue(re.search("3",self.driver.find_element(By.ID, "project-layers-count").text),'There should be 3 layers listed in the layer count')
|
||||
layer_list = self.driver.find_element(By.ID, "layers-in-project-list")
|
||||
layers = layer_list.find_elements(By.TAG_NAME, "li")
|
||||
for layer in layers:
|
||||
if re.match ("openembedded-core",layer.text):
|
||||
print ("openembedded-core layer is a default layer in the project configuration")
|
||||
elif re.match ("meta-poky",layer.text):
|
||||
print ("meta-poky layer is a default layer in the project configuration")
|
||||
elif re.match ("meta-yocto-bsp",layer.text):
|
||||
print ("meta-yocto-bsp is a default layer in the project configuratoin")
|
||||
else:
|
||||
self.fail(msg='default layers are missing from the project configuration')
|
||||
except:
|
||||
self.fail(msg='No Layer information in project detail page')
|
||||
|
||||
# testcase (1517)
|
||||
def test_verify_machine_information(self):
|
||||
self.get(reverse('all-projects'))
|
||||
self.wait_until_present('#projectstable', poll=10)
|
||||
self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click()
|
||||
self.wait_until_present('#config-nav', poll=10)
|
||||
|
||||
try:
|
||||
self.assertTrue(self.element_exists('#machine-section'),'Machine section for the project configuration page does not exist')
|
||||
self.assertTrue(re.search("qemux86-64",self.driver.find_element(By.ID, "project-machine-name").text),'The machine type is not assigned')
|
||||
self.driver.find_element(By.ID, "change-machine-toggle").click()
|
||||
self.wait_until_visible('#select-machine-form', poll=10)
|
||||
self.wait_until_visible('#cancel-machine-change', poll=10)
|
||||
self.driver.find_element(By.ID, "cancel-machine-change").click()
|
||||
except:
|
||||
self.fail(msg='The machine information is wrong in the configuration page')
|
||||
|
||||
# testcase (1518)
|
||||
def test_verify_most_built_recipes_information(self):
|
||||
self.get(reverse('all-projects'))
|
||||
self.wait_until_present('#projectstable', poll=10)
|
||||
self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click()
|
||||
self.wait_until_present('#config-nav', poll=10)
|
||||
project_URL=self.get_URL()
|
||||
try:
|
||||
self.assertTrue(re.search("You haven't built any recipes yet",self.driver.find_element(By.ID, "no-most-built").text),'Default message of no builds is not present')
|
||||
self.driver.find_element(By.XPATH, "//div[@id='no-most-built']/p/a[@href="+'"'+project_URL+'images/"'+"]").click()
|
||||
self.wait_until_present('#config-nav', poll=10)
|
||||
self.assertTrue(re.search("Compatible image recipes",self.driver.find_element(By.XPATH, "//div[@class='col-md-10']").text),'The Choose a recipe to build link is not working properly')
|
||||
except:
|
||||
self.fail(msg='No Most built information in project detail page')
|
||||
|
||||
# testcase (1519)
|
||||
def test_verify_project_release_information(self):
|
||||
self.get(reverse('all-projects'))
|
||||
self.wait_until_present('#projectstable', poll=10)
|
||||
self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click()
|
||||
self.wait_until_present('#config-nav', poll=10)
|
||||
|
||||
try:
|
||||
self.assertTrue(re.search("Yocto Project master",self.driver.find_element(By.ID, "project-release-title").text),'The project release is not defined')
|
||||
except:
|
||||
self.fail(msg='No project release title information in project detail page')
|
||||
|
||||
# testcase (1520)
|
||||
def test_verify_layer_information(self):
|
||||
self.get(reverse('all-projects'))
|
||||
self.wait_until_present('#projectstable', poll=10)
|
||||
self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click()
|
||||
self.wait_until_present('#config-nav', poll=10)
|
||||
project_URL=self.get_URL()
|
||||
try:
|
||||
self.driver.find_element(By.XPATH, "//div[@id='layer-container']")
|
||||
self.assertTrue(re.search("3",self.driver.find_element(By.ID, "project-layers-count").text),'There should be 3 layers listed in the layer count')
|
||||
layer_list = self.driver.find_element(By.ID, "layers-in-project-list")
|
||||
layers = layer_list.find_elements(By.TAG_NAME, "li")
|
||||
|
||||
for layer in layers:
|
||||
if re.match ("openembedded-core",layer.text):
|
||||
print ("openembedded-core layer is a default layer in the project configuration")
|
||||
elif re.match ("meta-poky",layer.text):
|
||||
print ("meta-poky layer is a default layer in the project configuration")
|
||||
elif re.match ("meta-yocto-bsp",layer.text):
|
||||
print ("meta-yocto-bsp is a default layer in the project configuratoin")
|
||||
else:
|
||||
self.fail(msg='default layers are missing from the project configuration')
|
||||
|
||||
self.driver.find_element(By.XPATH, "//input[@id='layer-add-input']")
|
||||
self.driver.find_element(By.XPATH, "//button[@id='add-layer-btn']")
|
||||
self.driver.find_element(By.XPATH, "//div[@id='layer-container']/form[@class='form-inline']/p/a[@id='view-compatible-layers']")
|
||||
self.driver.find_element(By.XPATH, "//div[@id='layer-container']/form[@class='form-inline']/p/a[@href="+'"'+project_URL+'importlayer"'+"]")
|
||||
except:
|
||||
self.fail(msg='No Layer information in project detail page')
|
||||
|
||||
# testcase (1521)
|
||||
def test_verify_project_detail_links(self):
|
||||
self.get(reverse('all-projects'))
|
||||
self.wait_until_present('#projectstable', poll=10)
|
||||
self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click()
|
||||
self.wait_until_present('#config-nav', poll=10)
|
||||
project_URL=self.get_URL()
|
||||
self.driver.find_element(By.XPATH, "//div[@id='project-topbar']/ul[@class='nav nav-tabs']/li[@id='topbar-configuration-tab']/a[@href="+'"'+project_URL+'"'+"]").click()
|
||||
self.wait_until_present('#config-nav', poll=10)
|
||||
self.assertTrue(re.search("Configuration",self.driver.find_element(By.XPATH, "//div[@id='project-topbar']/ul[@class='nav nav-tabs']/li[@id='topbar-configuration-tab']/a[@href="+'"'+project_URL+'"'+"]").text), 'Configuration tab in project topbar is misspelled')
|
||||
|
||||
try:
|
||||
self.driver.find_element(By.XPATH, "//div[@id='project-topbar']/ul[@class='nav nav-tabs']/li/a[@href="+'"'+project_URL+'builds/"'+"]").click()
|
||||
self.wait_until_visible('#project-topbar', poll=10)
|
||||
self.assertTrue(re.search("Builds",self.driver.find_element(By.XPATH, "//div[@id='project-topbar']/ul[@class='nav nav-tabs']/li/a[@href="+'"'+project_URL+'builds/"'+"]").text), 'Builds tab in project topbar is misspelled')
|
||||
self.driver.find_element(By.XPATH, "//div[@id='empty-state-projectbuildstable']")
|
||||
except:
|
||||
self.fail(msg='Builds tab information is not present')
|
||||
|
||||
try:
|
||||
self.driver.find_element(By.XPATH, "//div[@id='project-topbar']/ul[@class='nav nav-tabs']/li/a[@href="+'"'+project_URL+'importlayer"'+"]").click()
|
||||
self.wait_until_visible('#project-topbar', poll=10)
|
||||
self.assertTrue(re.search("Import layer",self.driver.find_element(By.XPATH, "//div[@id='project-topbar']/ul[@class='nav nav-tabs']/li/a[@href="+'"'+project_URL+'importlayer"'+"]").text), 'Import layer tab in project topbar is misspelled')
|
||||
self.driver.find_element(By.XPATH, "//fieldset[@id='repo-select']")
|
||||
self.driver.find_element(By.XPATH, "//fieldset[@id='git-repo']")
|
||||
except:
|
||||
self.fail(msg='Import layer tab not loading properly')
|
||||
|
||||
try:
|
||||
self.driver.find_element(By.XPATH, "//div[@id='project-topbar']/ul[@class='nav nav-tabs']/li/a[@href="+'"'+project_URL+'newcustomimage/"'+"]").click()
|
||||
self.wait_until_visible('#project-topbar', poll=10)
|
||||
self.assertTrue(re.search("New custom image",self.driver.find_element(By.XPATH, "//div[@id='project-topbar']/ul[@class='nav nav-tabs']/li/a[@href="+'"'+project_URL+'newcustomimage/"'+"]").text), 'New custom image tab in project topbar is misspelled')
|
||||
self.assertTrue(re.search("Select the image recipe you want to customise",self.driver.find_element(By.XPATH, "//div[@class='col-md-12']/h2").text),'The new custom image tab is not loading correctly')
|
||||
except:
|
||||
self.fail(msg='New custom image tab not loading properly')
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,341 @@
|
||||
#! /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
|
||||
change_dl_dir_btn = None
|
||||
|
||||
if change_dl_dir_btn:
|
||||
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:
|
||||
btn_chg_sstate_dir = 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
|
||||
btn_chg_sstate_dir = None
|
||||
|
||||
if btn_chg_sstate_dir: # Skip continuation if sstate_dir is not displayed
|
||||
# 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
|
||||
)
|
||||
@@ -0,0 +1,792 @@
|
||||
#! /usr/bin/env python3 #
|
||||
# BitBake Toaster UI tests implementation
|
||||
#
|
||||
# Copyright (C) 2023 Savoir-faire Linux
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
|
||||
import os
|
||||
import random
|
||||
import string
|
||||
from unittest import skip
|
||||
import pytest
|
||||
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 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 _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')
|
||||
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 != '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:
|
||||
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()
|
||||
self.project1_build_success = {
|
||||
'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=TestProjectPage.project_id),
|
||||
'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 _mixin_test_table_edit_column(
|
||||
self,
|
||||
table_id,
|
||||
edit_btn_id,
|
||||
list_check_box_id: list
|
||||
):
|
||||
# Check edit column
|
||||
edit_column = self.find(f'#{edit_btn_id}')
|
||||
self.assertTrue(edit_column.is_displayed())
|
||||
edit_column.click()
|
||||
# Check dropdown is visible
|
||||
self.wait_until_visible('ul.dropdown-menu.editcol')
|
||||
for check_box_id in list_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'#{table_id} 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'#{table_id} 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'#{table_id} 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'#{table_id} thead th.{th_class}'
|
||||
).is_displayed(),
|
||||
f"The {th_class} column is checked in EditColumn dropdown, but it's not visible in table"
|
||||
)
|
||||
|
||||
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_to_config_nav(self, nav_id, nav_index):
|
||||
# navigate to the project page
|
||||
self._navigate_to_project_page()
|
||||
# click on "Software recipe" tab
|
||||
soft_recipe = self._get_config_nav_item(nav_index)
|
||||
soft_recipe.click()
|
||||
self.wait_until_visible(f'#{nav_id}')
|
||||
|
||||
def _mixin_test_table_show_rows(self, table_selector, **kwargs):
|
||||
""" Test the show rows feature in the builds table on the all builds 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(f'#{table_selector} tbody tr', poll=3)
|
||||
# check at least some rows are visible
|
||||
self.assertTrue(
|
||||
len(self.find_all(f'#{table_selector} tbody tr')) > 0
|
||||
)
|
||||
self.wait_until_present(f'#{table_selector} tbody tr')
|
||||
show_rows = self.driver.find_elements(
|
||||
By.XPATH,
|
||||
f'//select[@class="form-control pagesize-{table_selector}"]'
|
||||
)
|
||||
rows_to_show = [10, 25, 50, 100, 150]
|
||||
to_skip = kwargs.get('to_skip', [])
|
||||
# Check show rows
|
||||
for show_row_link in show_rows:
|
||||
show_row_link = Select(show_row_link)
|
||||
for row_to_show in rows_to_show:
|
||||
if row_to_show not in to_skip:
|
||||
test_show_rows(row_to_show, show_row_link)
|
||||
|
||||
def _mixin_test_table_search_input(self, **kwargs):
|
||||
input_selector, input_text, searchBtn_selector, table_selector, *_ = kwargs.values()
|
||||
# Test search input
|
||||
self.wait_until_visible(f'#{input_selector}')
|
||||
recipe_input = self.find(f'#{input_selector}')
|
||||
recipe_input.send_keys(input_text)
|
||||
self.find(f'#{searchBtn_selector}').click()
|
||||
self.wait_until_visible(f'#{table_selector} tbody tr')
|
||||
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=(TestProjectPage.project_id,))
|
||||
self.get(url)
|
||||
self.wait_until_present('#imagerecipestable tbody tr')
|
||||
|
||||
column_list = [
|
||||
'get_description_or_summary', 'layer_version__get_vcs_reference',
|
||||
'layer_version__layer__name', 'license', 'recipe-file', 'section',
|
||||
'version'
|
||||
]
|
||||
|
||||
# Check that we can hide the edit column
|
||||
self._mixin_test_table_edit_column(
|
||||
'imagerecipestable',
|
||||
'edit-columns-button',
|
||||
[f'checkbox-{column}' for column in column_list]
|
||||
)
|
||||
|
||||
def test_page_header_on_project_page(self):
|
||||
""" Check page header in project page:
|
||||
- AT LEFT -> Logo of Yocto project, displayed, clickable
|
||||
- "Toaster"+" Information icon", displayed, clickable
|
||||
- "Server Icon" + "All builds", displayed, clickable
|
||||
- "Directory Icon" + "All projects", displayed, clickable
|
||||
- "Book Icon" + "Documentation", displayed, clickable
|
||||
- AT RIGHT -> button "New project", displayed, clickable
|
||||
"""
|
||||
# navigate to the project page
|
||||
self._navigate_to_project_page()
|
||||
|
||||
# check page header
|
||||
# AT LEFT -> Logo of Yocto project
|
||||
logo = self.driver.find_element(
|
||||
By.XPATH,
|
||||
"//div[@class='toaster-navbar-brand']",
|
||||
)
|
||||
logo_img = logo.find_element(By.TAG_NAME, 'img')
|
||||
self.assertTrue(logo_img.is_displayed(),
|
||||
'Logo of Yocto project not found')
|
||||
self.assertTrue(
|
||||
'/static/img/logo.png' in str(logo_img.get_attribute('src')),
|
||||
'Logo of Yocto project not found'
|
||||
)
|
||||
# "Toaster"+" Information icon", clickable
|
||||
toaster = self.driver.find_element(
|
||||
By.XPATH,
|
||||
"//div[@class='toaster-navbar-brand']//a[@class='brand']",
|
||||
)
|
||||
self.assertTrue(toaster.is_displayed(), 'Toaster not found')
|
||||
self.assertTrue(toaster.text == 'Toaster')
|
||||
info_sign = self.find('.glyphicon-info-sign')
|
||||
self.assertTrue(info_sign.is_displayed())
|
||||
|
||||
# "Server Icon" + "All builds"
|
||||
all_builds = self.find('#navbar-all-builds')
|
||||
all_builds_link = all_builds.find_element(By.TAG_NAME, 'a')
|
||||
self.assertTrue("All builds" in all_builds_link.text)
|
||||
self.assertTrue(
|
||||
'/toastergui/builds/' in str(all_builds_link.get_attribute('href'))
|
||||
)
|
||||
server_icon = all_builds.find_element(By.TAG_NAME, 'i')
|
||||
self.assertTrue(
|
||||
server_icon.get_attribute('class') == 'glyphicon glyphicon-tasks'
|
||||
)
|
||||
self.assertTrue(server_icon.is_displayed())
|
||||
|
||||
# "Directory Icon" + "All projects"
|
||||
all_projects = self.find('#navbar-all-projects')
|
||||
all_projects_link = all_projects.find_element(By.TAG_NAME, 'a')
|
||||
self.assertTrue("All projects" in all_projects_link.text)
|
||||
self.assertTrue(
|
||||
'/toastergui/projects/' in str(all_projects_link.get_attribute(
|
||||
'href'))
|
||||
)
|
||||
dir_icon = all_projects.find_element(By.TAG_NAME, 'i')
|
||||
self.assertTrue(
|
||||
dir_icon.get_attribute('class') == 'icon-folder-open'
|
||||
)
|
||||
self.assertTrue(dir_icon.is_displayed())
|
||||
|
||||
# "Book Icon" + "Documentation"
|
||||
toaster_docs_link = self.find('#navbar-docs')
|
||||
toaster_docs_link_link = toaster_docs_link.find_element(By.TAG_NAME,
|
||||
'a')
|
||||
self.assertTrue("Documentation" in toaster_docs_link_link.text)
|
||||
self.assertTrue(
|
||||
toaster_docs_link_link.get_attribute('href') == 'http://docs.yoctoproject.org/toaster-manual/index.html#toaster-user-manual'
|
||||
)
|
||||
book_icon = toaster_docs_link.find_element(By.TAG_NAME, 'i')
|
||||
self.assertTrue(
|
||||
book_icon.get_attribute('class') == 'glyphicon glyphicon-book'
|
||||
)
|
||||
self.assertTrue(book_icon.is_displayed())
|
||||
|
||||
# AT RIGHT -> button "New project"
|
||||
new_project_button = self.find('#new-project-button')
|
||||
self.assertTrue(new_project_button.is_displayed())
|
||||
self.assertTrue(new_project_button.text == 'New project')
|
||||
new_project_button.click()
|
||||
self.assertTrue(
|
||||
'/toastergui/newproject/' in str(self.driver.current_url)
|
||||
)
|
||||
|
||||
def test_edit_project_name(self):
|
||||
""" Test edit project name:
|
||||
- Click on "Edit" icon button
|
||||
- Change project name
|
||||
- Click on "Save" button
|
||||
- Check project name is changed
|
||||
"""
|
||||
# navigate to the project page
|
||||
self._navigate_to_project_page()
|
||||
|
||||
# click on "Edit" icon button
|
||||
self.wait_until_visible('#project-name-container')
|
||||
edit_button = self.find('#project-change-form-toggle')
|
||||
edit_button.click()
|
||||
project_name_input = self.find('#project-name-change-input')
|
||||
self.assertTrue(project_name_input.is_displayed())
|
||||
project_name_input.clear()
|
||||
project_name_input.send_keys('New Name')
|
||||
self.find('#project-name-change-btn').click()
|
||||
|
||||
# check project name is changed
|
||||
self.wait_until_visible('#project-name-container')
|
||||
self.assertTrue(
|
||||
'New Name' in str(self.find('#project-name-container').text)
|
||||
)
|
||||
|
||||
def test_project_page_tabs(self):
|
||||
""" Test project tabs:
|
||||
- "configuration" tab
|
||||
- "Builds" tab
|
||||
- "Import layers" tab
|
||||
- "New custom image" tab
|
||||
Check search box used to build recipes
|
||||
"""
|
||||
# navigate to the project page
|
||||
self._navigate_to_project_page()
|
||||
|
||||
# check "configuration" tab
|
||||
self.wait_until_visible('#topbar-configuration-tab')
|
||||
config_tab = self.find('#topbar-configuration-tab')
|
||||
self.assertTrue(config_tab.get_attribute('class') == 'active')
|
||||
self.assertTrue('Configuration' in str(config_tab.text))
|
||||
self.assertTrue(
|
||||
f"/toastergui/project/{TestProjectPage.project_id}" in str(self.driver.current_url)
|
||||
)
|
||||
|
||||
def get_tabs():
|
||||
# tabs links list
|
||||
return self.driver.find_elements(
|
||||
By.XPATH,
|
||||
'//div[@id="project-topbar"]//li'
|
||||
)
|
||||
|
||||
def check_tab_link(tab_index, tab_name, url):
|
||||
tab = get_tabs()[tab_index]
|
||||
tab_link = tab.find_element(By.TAG_NAME, 'a')
|
||||
self.assertTrue(url in tab_link.get_attribute('href'))
|
||||
self.assertTrue(tab_name in tab_link.text)
|
||||
self.assertTrue(tab.get_attribute('class') == 'active')
|
||||
|
||||
# check "Builds" tab
|
||||
builds_tab = get_tabs()[1]
|
||||
builds_tab.find_element(By.TAG_NAME, 'a').click()
|
||||
check_tab_link(
|
||||
1,
|
||||
'Builds',
|
||||
f"/toastergui/project/{TestProjectPage.project_id}/builds"
|
||||
)
|
||||
|
||||
# check "Import layers" tab
|
||||
import_layers_tab = get_tabs()[2]
|
||||
import_layers_tab.find_element(By.TAG_NAME, 'a').click()
|
||||
check_tab_link(
|
||||
2,
|
||||
'Import layer',
|
||||
f"/toastergui/project/{TestProjectPage.project_id}/importlayer"
|
||||
)
|
||||
|
||||
# check "New custom image" tab
|
||||
new_custom_image_tab = get_tabs()[3]
|
||||
new_custom_image_tab.find_element(By.TAG_NAME, 'a').click()
|
||||
check_tab_link(
|
||||
3,
|
||||
'New custom image',
|
||||
f"/toastergui/project/{TestProjectPage.project_id}/newcustomimage"
|
||||
)
|
||||
|
||||
# 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()
|
||||
self.wait_until_visible('#latest-builds')
|
||||
lastest_builds = self.driver.find_elements(
|
||||
By.XPATH,
|
||||
'//div[@id="latest-builds"]',
|
||||
)
|
||||
last_build = lastest_builds[0]
|
||||
self.assertTrue(
|
||||
'core-image-minimal' in str(last_build.text)
|
||||
)
|
||||
|
||||
def test_softwareRecipe_page(self):
|
||||
""" Test software recipe page
|
||||
- Check title "Compatible software recipes" is displayed
|
||||
- Check search input
|
||||
- Check "build recipe" button works
|
||||
- Check software recipe table feature(show/hide column, pagination)
|
||||
"""
|
||||
self._navigate_to_config_nav('softwarerecipestable', 4)
|
||||
# check title "Compatible software recipes" is displayed
|
||||
self.assertTrue("Compatible software recipes" in self.get_page_source())
|
||||
# Test search input
|
||||
self._mixin_test_table_search_input(
|
||||
input_selector='search-input-softwarerecipestable',
|
||||
input_text='busybox',
|
||||
searchBtn_selector='search-submit-softwarerecipestable',
|
||||
table_selector='softwarerecipestable'
|
||||
)
|
||||
# check "build recipe" button works
|
||||
rows = self.find_all('#softwarerecipestable tbody tr')
|
||||
image_to_build = rows[0]
|
||||
build_btn = image_to_build.find_element(
|
||||
By.XPATH,
|
||||
'//td[@class="add-del-layers"]//a[1]'
|
||||
)
|
||||
build_btn.click()
|
||||
build_state = wait_until_build(self, 'queued cloning starting parsing failed')
|
||||
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)
|
||||
column_list = [
|
||||
'get_description_or_summary',
|
||||
'layer_version__get_vcs_reference',
|
||||
'layer_version__layer__name',
|
||||
'license',
|
||||
'recipe-file',
|
||||
'section',
|
||||
'version',
|
||||
]
|
||||
self._mixin_test_table_edit_column(
|
||||
'softwarerecipestable',
|
||||
'edit-columns-button',
|
||||
[f'checkbox-{column}' for column in column_list]
|
||||
)
|
||||
self._navigate_to_config_nav('softwarerecipestable', 4)
|
||||
# check show rows(pagination)
|
||||
self._mixin_test_table_show_rows(
|
||||
table_selector='softwarerecipestable',
|
||||
to_skip=[150],
|
||||
)
|
||||
|
||||
def test_machines_page(self):
|
||||
""" Test Machine page
|
||||
- Check if title "Compatible machines" is displayed
|
||||
- Check search input
|
||||
- Check "Select machine" button works
|
||||
- Check "Add layer" button works
|
||||
- Check Machine table feature(show/hide column, pagination)
|
||||
"""
|
||||
self._navigate_to_config_nav('machinestable', 5)
|
||||
# check title "Compatible software recipes" is displayed
|
||||
self.assertTrue("Compatible machines" in self.get_page_source())
|
||||
# Test search input
|
||||
self._mixin_test_table_search_input(
|
||||
input_selector='search-input-machinestable',
|
||||
input_text='qemux86-64',
|
||||
searchBtn_selector='search-submit-machinestable',
|
||||
table_selector='machinestable'
|
||||
)
|
||||
# check "Select machine" button works
|
||||
rows = self.find_all('#machinestable tbody tr')
|
||||
machine_to_select = rows[0]
|
||||
select_btn = machine_to_select.find_element(
|
||||
By.XPATH,
|
||||
'//td[@class="add-del-layers"]//a[1]'
|
||||
)
|
||||
select_btn.send_keys(Keys.RETURN)
|
||||
self.wait_until_visible('#config-nav')
|
||||
project_machine_name = self.find('#project-machine-name')
|
||||
self.assertTrue(
|
||||
'qemux86-64' in project_machine_name.text
|
||||
)
|
||||
# check "Add layer" button works
|
||||
self._navigate_to_config_nav('machinestable', 5)
|
||||
# Search for a machine whit layer not in project
|
||||
self._mixin_test_table_search_input(
|
||||
input_selector='search-input-machinestable',
|
||||
input_text='qemux86-64-tpm2',
|
||||
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"]')
|
||||
add_btn.click()
|
||||
self.wait_until_visible('#change-notification')
|
||||
change_notification = self.find('#change-notification')
|
||||
self.assertTrue(
|
||||
f'You have added 1 layer to your project' in str(change_notification.text)
|
||||
)
|
||||
# check Machine table feature(show/hide column, pagination)
|
||||
self._navigate_to_config_nav('machinestable', 5)
|
||||
column_list = [
|
||||
'description',
|
||||
'layer_version__get_vcs_reference',
|
||||
'layer_version__layer__name',
|
||||
'machinefile',
|
||||
]
|
||||
self._mixin_test_table_edit_column(
|
||||
'machinestable',
|
||||
'edit-columns-button',
|
||||
[f'checkbox-{column}' for column in column_list]
|
||||
)
|
||||
self._navigate_to_config_nav('machinestable', 5)
|
||||
# check show rows(pagination)
|
||||
self._mixin_test_table_show_rows(
|
||||
table_selector='machinestable',
|
||||
to_skip=[150],
|
||||
)
|
||||
|
||||
def test_layers_page(self):
|
||||
""" Test layers page
|
||||
- Check if title "Compatible layerss" is displayed
|
||||
- Check search input
|
||||
- Check "Add layer" button works
|
||||
- Check "Remove layer" button works
|
||||
- Check layers table feature(show/hide column, pagination)
|
||||
"""
|
||||
self._navigate_to_config_nav('layerstable', 6)
|
||||
# check title "Compatible layers" is displayed
|
||||
self.assertTrue("Compatible layers" in self.get_page_source())
|
||||
# Test search input
|
||||
input_text='meta-tanowrt'
|
||||
self._mixin_test_table_search_input(
|
||||
input_selector='search-input-layerstable',
|
||||
input_text=input_text,
|
||||
searchBtn_selector='search-submit-layerstable',
|
||||
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(
|
||||
By.XPATH,
|
||||
'//td[@class="add-del-layers"]'
|
||||
)
|
||||
add_btn.click()
|
||||
# check modal is displayed
|
||||
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(
|
||||
By.XPATH,
|
||||
'//form[@id="dependencies-modal-form"]//button[@class="btn btn-primary"]'
|
||||
)
|
||||
add_layers_btn.click()
|
||||
self.wait_until_visible('#change-notification')
|
||||
change_notification = self.find('#change-notification')
|
||||
self.assertTrue(
|
||||
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(
|
||||
By.XPATH,
|
||||
'//td[@class="add-del-layers"]'
|
||||
)
|
||||
remove_btn.click()
|
||||
self.wait_until_visible('#change-notification', poll=2)
|
||||
change_notification = self.find('#change-notification')
|
||||
self.assertTrue(
|
||||
f'You have removed 1 layer from your project: {input_text}' in str(change_notification.text)
|
||||
)
|
||||
# check layers table feature(show/hide column, pagination)
|
||||
self._navigate_to_config_nav('layerstable', 6)
|
||||
column_list = [
|
||||
'dependencies',
|
||||
'revision',
|
||||
'layer__vcs_url',
|
||||
'git_subdir',
|
||||
'layer__summary',
|
||||
]
|
||||
self._mixin_test_table_edit_column(
|
||||
'layerstable',
|
||||
'edit-columns-button',
|
||||
[f'checkbox-{column}' for column in column_list]
|
||||
)
|
||||
self._navigate_to_config_nav('layerstable', 6)
|
||||
# check show rows(pagination)
|
||||
self._mixin_test_table_show_rows(
|
||||
table_selector='layerstable',
|
||||
to_skip=[150],
|
||||
)
|
||||
|
||||
def test_distro_page(self):
|
||||
""" Test distros page
|
||||
- Check if title "Compatible distros" is displayed
|
||||
- Check search input
|
||||
- Check "Add layer" button works
|
||||
- Check distro table feature(show/hide column, pagination)
|
||||
"""
|
||||
self._navigate_to_config_nav('distrostable', 7)
|
||||
# check title "Compatible distros" is displayed
|
||||
self.assertTrue("Compatible Distros" in self.get_page_source())
|
||||
# Test search input
|
||||
input_text='poky-altcfg'
|
||||
self._mixin_test_table_search_input(
|
||||
input_selector='search-input-distrostable',
|
||||
input_text=input_text,
|
||||
searchBtn_selector='search-submit-distrostable',
|
||||
table_selector='distrostable'
|
||||
)
|
||||
# check "Add distro" button works
|
||||
rows = self.find_all('#distrostable tbody tr')
|
||||
distro_to_add = rows[0]
|
||||
add_btn = distro_to_add.find_element(
|
||||
By.XPATH,
|
||||
'//td[@class="add-del-layers"]//a[1]'
|
||||
)
|
||||
add_btn.click()
|
||||
self.wait_until_visible('#change-notification', poll=2)
|
||||
change_notification = self.find('#change-notification')
|
||||
self.assertTrue(
|
||||
f'You have changed the distro to: {input_text}' in str(change_notification.text)
|
||||
)
|
||||
# check distro table feature(show/hide column, pagination)
|
||||
self._navigate_to_config_nav('distrostable', 7)
|
||||
column_list = [
|
||||
'description',
|
||||
'templatefile',
|
||||
'layer_version__get_vcs_reference',
|
||||
'layer_version__layer__name',
|
||||
]
|
||||
self._mixin_test_table_edit_column(
|
||||
'distrostable',
|
||||
'edit-columns-button',
|
||||
[f'checkbox-{column}' for column in column_list]
|
||||
)
|
||||
self._navigate_to_config_nav('distrostable', 7)
|
||||
# check show rows(pagination)
|
||||
self._mixin_test_table_show_rows(
|
||||
table_selector='distrostable',
|
||||
to_skip=[150],
|
||||
)
|
||||
|
||||
def test_single_layer_page(self):
|
||||
""" Test layer page
|
||||
- Check if title is displayed
|
||||
- Check add/remove layer button works
|
||||
- Check tabs(layers, recipes, machines) are displayed
|
||||
- Check left section is displayed
|
||||
- Check layer name
|
||||
- Check layer summary
|
||||
- Check layer description
|
||||
"""
|
||||
url = reverse("layerdetails", args=(TestProjectPage.project_id, 8))
|
||||
self.get(url)
|
||||
self.wait_until_visible('.page-header')
|
||||
# check title is displayed
|
||||
self.assertTrue(self.find('.page-header h1').is_displayed())
|
||||
|
||||
# check add layer button works
|
||||
remove_layer_btn = self.find('#add-remove-layer-btn')
|
||||
remove_layer_btn.click()
|
||||
self.wait_until_visible('#change-notification', poll=2)
|
||||
change_notification = self.find('#change-notification')
|
||||
self.assertTrue(
|
||||
f'You have removed 1 layer from your project' in str(change_notification.text)
|
||||
)
|
||||
# check add layer button works, 18 is the random layer id
|
||||
add_layer_btn = self.find('#add-remove-layer-btn')
|
||||
add_layer_btn.click()
|
||||
self.wait_until_visible('#change-notification')
|
||||
change_notification = self.find('#change-notification')
|
||||
self.assertTrue(
|
||||
f'You have added 1 layer to your project' in str(change_notification.text)
|
||||
)
|
||||
# check tabs(layers, recipes, machines) are displayed
|
||||
tabs = self.find_all('.nav-tabs li')
|
||||
self.assertEqual(len(tabs), 3)
|
||||
# Check first tab
|
||||
tabs[0].click()
|
||||
self.assertTrue(
|
||||
'active' in str(self.find('#information').get_attribute('class'))
|
||||
)
|
||||
# Check second tab
|
||||
tabs[1].click()
|
||||
self.assertTrue(
|
||||
'active' in str(self.find('#recipes').get_attribute('class'))
|
||||
)
|
||||
# Check third tab
|
||||
tabs[2].click()
|
||||
self.assertTrue(
|
||||
'active' in str(self.find('#machines').get_attribute('class'))
|
||||
)
|
||||
# Check left section is displayed
|
||||
section = self.find('.well')
|
||||
# Check layer name
|
||||
self.assertTrue(
|
||||
section.find_element(By.XPATH, '//h2[1]').is_displayed()
|
||||
)
|
||||
# Check layer summary
|
||||
self.assertTrue("Summary" in section.text)
|
||||
# Check layer description
|
||||
self.assertTrue("Description" in section.text)
|
||||
|
||||
def test_single_recipe_page(self):
|
||||
""" Test recipe page
|
||||
- Check if title is displayed
|
||||
- Check add recipe layer displayed
|
||||
- Check left section is displayed
|
||||
- Check recipe: name, summary, description, Version, Section,
|
||||
License, Approx. packages included, Approx. size, Recipe file
|
||||
"""
|
||||
url = reverse("recipedetails", args=(TestProjectPage.project_id, 53428))
|
||||
self.get(url)
|
||||
self.wait_until_visible('.page-header')
|
||||
# check title is displayed
|
||||
self.assertTrue(self.find('.page-header h1').is_displayed())
|
||||
# check add recipe layer displayed
|
||||
add_recipe_layer_btn = self.find('#add-layer-btn')
|
||||
self.assertTrue(add_recipe_layer_btn.is_displayed())
|
||||
# check left section is displayed
|
||||
section = self.find('.well')
|
||||
# Check recipe name
|
||||
self.assertTrue(
|
||||
section.find_element(By.XPATH, '//h2[1]').is_displayed()
|
||||
)
|
||||
# Check recipe sections details info are displayed
|
||||
self.assertTrue("Summary" in section.text)
|
||||
self.assertTrue("Description" in section.text)
|
||||
self.assertTrue("Version" in section.text)
|
||||
self.assertTrue("Section" in section.text)
|
||||
self.assertTrue("License" in section.text)
|
||||
self.assertTrue("Approx. packages included" in section.text)
|
||||
self.assertTrue("Approx. package size" in section.text)
|
||||
self.assertTrue("Recipe file" in section.text)
|
||||
@@ -0,0 +1,528 @@
|
||||
#! /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 ElementClickInterceptedException, NoSuchElementException, 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 _create_project(self, project_name, **kwargs):
|
||||
""" Create/Test new project using:
|
||||
- Project Name: Any string
|
||||
- Release: Any string
|
||||
- Merge Toaster settings: True or False
|
||||
"""
|
||||
release = kwargs.get('release', '3')
|
||||
self.get(reverse('newproject'))
|
||||
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(release)
|
||||
|
||||
# check merge toaster settings
|
||||
checkbox = self.find('.checkbox-mergeattr')
|
||||
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', poll=3)
|
||||
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:
|
||||
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('foo')
|
||||
self.find('#build-button').click()
|
||||
self.wait_until_present('#latest-builds')
|
||||
# loop until reach the parsing state
|
||||
wait_until_build(self, 'queued cloning starting parsing failed')
|
||||
lastest_builds = self.driver.find_elements(
|
||||
By.XPATH,
|
||||
'//div[@id="latest-builds"]/div',
|
||||
)
|
||||
last_build = lastest_builds[0]
|
||||
self.assertTrue(
|
||||
'foo' in str(last_build.text)
|
||||
)
|
||||
last_build = lastest_builds[0]
|
||||
try:
|
||||
cancel_button = last_build.find_element(
|
||||
By.XPATH,
|
||||
'//span[@class="cancel-build-btn pull-right alert-link"]',
|
||||
)
|
||||
cancel_button.click()
|
||||
except NoSuchElementException:
|
||||
# Skip if the build is already cancelled
|
||||
pass
|
||||
wait_until_build_cancelled(self)
|
||||
|
||||
def _get_tabs(self):
|
||||
# tabs links list
|
||||
return self.driver.find_elements(
|
||||
By.XPATH,
|
||||
'//div[@id="project-topbar"]//li'
|
||||
)
|
||||
|
||||
def _get_config_nav_item(self, index):
|
||||
config_nav = self.find('#config-nav')
|
||||
return config_nav.find_elements(By.TAG_NAME, 'li')[index]
|
||||
|
||||
def test_project_config_nav(self):
|
||||
""" Test project config tab navigation:
|
||||
- Check if the menu is displayed and contains the right elements:
|
||||
- Configuration
|
||||
- COMPATIBLE METADATA
|
||||
- Custom images
|
||||
- Image recipes
|
||||
- Software recipes
|
||||
- Machines
|
||||
- Layers
|
||||
- Distro
|
||||
- EXTRA CONFIGURATION
|
||||
- Bitbake variables
|
||||
- Actions
|
||||
- Delete project
|
||||
"""
|
||||
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]
|
||||
|
||||
def check_config_nav_item(index, item_name, url):
|
||||
item = _get_config_nav_item(index)
|
||||
self.assertTrue(item_name in item.text)
|
||||
self.assertTrue(item.get_attribute('class') == 'active')
|
||||
self.assertTrue(url in self.driver.current_url)
|
||||
|
||||
# check if the menu contains the right elements
|
||||
# COMPATIBLE METADATA
|
||||
compatible_metadata = _get_config_nav_item(1)
|
||||
self.assertTrue(
|
||||
"compatible metadata" in compatible_metadata.text.lower()
|
||||
)
|
||||
# EXTRA CONFIGURATION
|
||||
extra_configuration = _get_config_nav_item(8)
|
||||
self.assertTrue(
|
||||
"extra configuration" in extra_configuration.text.lower()
|
||||
)
|
||||
# Actions
|
||||
actions = _get_config_nav_item(10)
|
||||
self.assertTrue("actions" in str(actions.text).lower())
|
||||
|
||||
conf_nav_list = [
|
||||
# config
|
||||
[0, 'Configuration',
|
||||
f"/toastergui/project/{TestProjectConfigTab.project_id}"],
|
||||
# custom images
|
||||
[2, 'Custom images',
|
||||
f"/toastergui/project/{TestProjectConfigTab.project_id}/customimages"],
|
||||
# image recipes
|
||||
[3, 'Image recipes',
|
||||
f"/toastergui/project/{TestProjectConfigTab.project_id}/images"],
|
||||
# software recipes
|
||||
[4, 'Software recipes',
|
||||
f"/toastergui/project/{TestProjectConfigTab.project_id}/softwarerecipes"],
|
||||
# machines
|
||||
[5, 'Machines',
|
||||
f"/toastergui/project/{TestProjectConfigTab.project_id}/machines"],
|
||||
# layers
|
||||
[6, 'Layers',
|
||||
f"/toastergui/project/{TestProjectConfigTab.project_id}/layers"],
|
||||
# distro
|
||||
[7, 'Distros',
|
||||
f"/toastergui/project/{TestProjectConfigTab.project_id}/distros"],
|
||||
# [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)
|
||||
if item.get_attribute('class') != 'active':
|
||||
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', poll=3)
|
||||
# check at least some rows are visible
|
||||
self.assertTrue(
|
||||
len(self.find_all('#imagerecipestable tbody tr')) > 0
|
||||
)
|
||||
|
||||
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:
|
||||
- check 'Machine' is displayed
|
||||
- check can change Machine
|
||||
- Distro:
|
||||
- check 'Distro' is displayed
|
||||
- check can change Distro
|
||||
- Most built recipes:
|
||||
- check 'Most built recipes' is displayed
|
||||
- check can select a recipe and build it
|
||||
- Project release:
|
||||
- check 'Project release' is displayed
|
||||
- check project has right release displayed
|
||||
- Layers:
|
||||
- check can add a layer if exists
|
||||
- check at least three layers are displayed
|
||||
- openembedded-core
|
||||
- meta-poky
|
||||
- meta-yocto-bsp
|
||||
"""
|
||||
# Create a new project for this test
|
||||
project_name = self._random_string(10)
|
||||
self._create_project(project_name=project_name)
|
||||
# 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]')
|
||||
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_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 = self.find(f'#change-{item_name}-toggle')
|
||||
edit_btn.click()
|
||||
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 = self.find(f'#{item_name}-change-btn')
|
||||
change_btn.click()
|
||||
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')
|
||||
self.assertTrue(
|
||||
f'You have changed the {item_name} to: {new_item_name}' in change_notification.text
|
||||
)
|
||||
|
||||
# Machine
|
||||
check_machine_distro(self, 'machine', 'qemux86-64', 'machine-section')
|
||||
# Distro
|
||||
check_machine_distro(self, 'distro', 'poky-altcfg', 'distro-section')
|
||||
|
||||
# Project release
|
||||
title = project_release.find_element(By.TAG_NAME, 'h3')
|
||||
self.assertTrue("Project release" in title.text)
|
||||
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)
|
||||
# check at least three layers are displayed
|
||||
# openembedded-core
|
||||
# meta-poky
|
||||
# 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')
|
||||
# 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')
|
||||
self.wait_until_visible('#layer-container > form > div > span > div')
|
||||
dropdown_item = self.driver.find_element(
|
||||
By.XPATH,
|
||||
'//*[@id="layer-container"]/form/div/span/div'
|
||||
)
|
||||
try:
|
||||
dropdown_item.click()
|
||||
except ElementClickInterceptedException:
|
||||
self.skipTest(
|
||||
"layer-container dropdown item click intercepted. Element not properly visible.")
|
||||
add_layer_btn = layers.find_element(By.ID, 'add-layer-btn')
|
||||
add_layer_btn.click()
|
||||
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)
|
||||
|
||||
def test_most_build_recipes(self):
|
||||
""" Test most build recipes block contains"""
|
||||
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')
|
||||
wait_until_build(self, 'queued cloning starting parsing failed')
|
||||
lastest_builds = self.driver.find_elements(
|
||||
By.XPATH,
|
||||
'//div[@id="latest-builds"]/div'
|
||||
)
|
||||
self.assertTrue(len(lastest_builds) >= 2)
|
||||
last_build = lastest_builds[0]
|
||||
try:
|
||||
cancel_button = last_build.find_element(
|
||||
By.XPATH,
|
||||
'//span[@class="cancel-build-btn pull-right alert-link"]',
|
||||
)
|
||||
cancel_button.click()
|
||||
except NoSuchElementException:
|
||||
# Skip if the build is already cancelled
|
||||
pass
|
||||
wait_until_build_cancelled(self)
|
||||
# Create a new project for remaining asserts
|
||||
project_name = self._random_string(10)
|
||||
self._create_project(project_name=project_name, release='2')
|
||||
current_url = self.driver.current_url
|
||||
TestProjectConfigTab.project_id = get_projectId_from_url(current_url)
|
||||
url = current_url.split('?')[0]
|
||||
|
||||
# Create a new builds
|
||||
self._create_builds()
|
||||
|
||||
# back to project page
|
||||
self.driver.get(url)
|
||||
|
||||
self.wait_until_visible('#project-page', poll=3)
|
||||
|
||||
# Most built recipes
|
||||
most_built_recipes = self.driver.find_element(
|
||||
By.XPATH, '//*[@id="project-page"]/div[1]/div[3]')
|
||||
title = most_built_recipes.find_element(By.TAG_NAME, 'h3')
|
||||
self.assertTrue("Most built recipes" in title.text)
|
||||
# check can select a recipe and build it
|
||||
self.wait_until_visible('#freq-build-list', poll=3)
|
||||
recipe_list = self.find('#freq-build-list')
|
||||
recipe_list_items = recipe_list.find_elements(By.TAG_NAME, 'li')
|
||||
self.assertTrue(
|
||||
len(recipe_list_items) > 0,
|
||||
msg="Any recipes found in the most built recipes list",
|
||||
)
|
||||
rebuild_from_most_build_recipes(recipe_list_items)
|
||||
TestProjectConfigTab.project_id = None # reset project id
|
||||
|
||||
def test_project_page_tab_importlayer(self):
|
||||
""" Test project page tab import layer """
|
||||
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()
|
||||
self.wait_until_visible('#layer-git-repo-url')
|
||||
|
||||
# Check git repo radio button
|
||||
git_repo_radio = self.find('#git-repo-radio')
|
||||
git_repo_radio.click()
|
||||
|
||||
# Set git repo url
|
||||
input_repo_url = self.find('#layer-git-repo-url')
|
||||
input_repo_url.send_keys('git://git.yoctoproject.org/meta-fake')
|
||||
# Blur the input to trigger the validation
|
||||
input_repo_url.send_keys(Keys.TAB)
|
||||
|
||||
# Check name is set
|
||||
input_layer_name = self.find('#import-layer-name')
|
||||
self.assertTrue(input_layer_name.get_attribute('value') == 'meta-fake')
|
||||
|
||||
# Set branch
|
||||
input_branch = self.find('#layer-git-ref')
|
||||
input_branch.send_keys('master')
|
||||
|
||||
# Import layer
|
||||
self.find('#import-and-add-btn').click()
|
||||
|
||||
# Check layer is added
|
||||
self.wait_until_visible('#layer-container')
|
||||
block_l = self.driver.find_element(
|
||||
By.XPATH, '//*[@id="project-page"]/div[2]')
|
||||
layers = block_l.find_element(By.ID, 'layer-container')
|
||||
layers_list = layers.find_element(By.ID, 'layers-in-project-list')
|
||||
layers_list_items = layers_list.find_elements(By.TAG_NAME, 'li')
|
||||
self.assertTrue(
|
||||
'meta-fake' in str(layers_list_items[-1].text)
|
||||
)
|
||||
|
||||
def test_project_page_custom_image_no_image(self):
|
||||
""" Test project page tab "New custom image" when no custom image """
|
||||
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()
|
||||
self.wait_until_visible('#empty-state-customimagestable')
|
||||
|
||||
# Check message when no custom image
|
||||
self.assertTrue(
|
||||
"You have not created any custom images yet." in str(
|
||||
self.find('#empty-state-customimagestable').text
|
||||
)
|
||||
)
|
||||
div_empty_msg = self.find('#empty-state-customimagestable')
|
||||
link_create_custom_image = div_empty_msg.find_element(
|
||||
By.TAG_NAME, 'a')
|
||||
self.assertTrue(TestProjectConfigTab.project_id is not None)
|
||||
self.assertTrue(
|
||||
f"/toastergui/project/{TestProjectConfigTab.project_id}/newcustomimage" in str(
|
||||
link_create_custom_image.get_attribute('href')
|
||||
)
|
||||
)
|
||||
self.assertTrue(
|
||||
"Create your first custom image" in str(
|
||||
link_create_custom_image.text
|
||||
)
|
||||
)
|
||||
TestProjectConfigTab.project_id = None # reset project id
|
||||
|
||||
def test_project_page_image_recipe(self):
|
||||
""" Test project page section images
|
||||
- Check image recipes are displayed
|
||||
- Check search input
|
||||
- Check image recipe build button works
|
||||
- Check image recipe table features(show/hide column, pagination)
|
||||
"""
|
||||
self._navigate_to_project_page()
|
||||
# navigate to "Images section"
|
||||
images_section = self._get_config_nav_item(3)
|
||||
images_section.click()
|
||||
self.wait_until_visible('#imagerecipestable')
|
||||
rows = self.find_all('#imagerecipestable tbody tr')
|
||||
self.assertTrue(len(rows) > 0)
|
||||
|
||||
# Test search input
|
||||
self.wait_until_visible('#search-input-imagerecipestable')
|
||||
recipe_input = self.find('#search-input-imagerecipestable')
|
||||
recipe_input.send_keys('core-image-minimal')
|
||||
self.find('#search-submit-imagerecipestable').click()
|
||||
self.wait_until_visible('#imagerecipestable tbody tr')
|
||||
rows = self.find_all('#imagerecipestable tbody tr')
|
||||
self.assertTrue(len(rows) > 0)
|
||||
89
sources/poky/bitbake/lib/toaster/tests/functional/utils.py
Normal file
89
sources/poky/bitbake/lib/toaster/tests/functional/utils.py
Normal file
@@ -0,0 +1,89 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# BitBake Toaster UI tests implementation
|
||||
#
|
||||
# Copyright (C) 2023 Savoir-faire Linux
|
||||
#
|
||||
# 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
|
||||
Reference in New Issue
Block a user