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:
Siggi (OpenClaw Agent)
2026-03-01 20:58:18 +00:00
commit 16accb6b24
15086 changed files with 1292356 additions and 0 deletions

View File

@@ -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

View File

@@ -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")

View File

@@ -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')

View File

@@ -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
)

View File

@@ -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)

View File

@@ -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)

View 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