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,74 @@
# Running Toaster's browser-based test suite
These tests require Selenium to be installed in your Python environment.
The simplest way to install this is via pip3:
pip3 install selenium==2.53.2
Note that if you use other versions of Selenium, some of the tests (such as
tests.browser.test_js_unit_tests.TestJsUnitTests) may fail, as these rely on
a Selenium test report with a version-specific format.
To run tests against Chrome:
* Download chromedriver for your host OS from
https://sites.google.com/a/chromium.org/chromedriver/downloads
* On *nix systems, put chromedriver on PATH
* On Windows, put chromedriver.exe in the same directory as chrome.exe
To run tests against PhantomJS (headless):
--NOTE - Selenium seems to be deprecating support for this mode ---
* Download and install PhantomJS:
http://phantomjs.org/download.html
* On *nix systems, put phantomjs on PATH
* Not tested on Windows
To run tests against Firefox, you may need to install the Marionette driver,
depending on how new your version of Firefox is. One clue that you need to do
this is if you see an exception like:
selenium.common.exceptions.WebDriverException: Message: The browser
appears to have exited before we could connect. If you specified
a log_file in the FirefoxBinary constructor, check it for details.
See https://developer.mozilla.org/en-US/docs/Mozilla/QA/Marionette/WebDriver
for installation instructions. Ensure that the Marionette executable (renamed
as wires on Linux or wires.exe on Windows) is on your PATH; and use "marionette"
as the browser string passed via TOASTER_TESTS_BROWSER (see below).
(Note: The Toaster tests have been checked against Firefox 47 with the
Marionette driver.)
The test cases will instantiate a Selenium driver set by the
TOASTER_TESTS_BROWSER environment variable, or Chrome if this is not specified.
To run tests against the Selenium Firefox Docker container:
More explanation is located at https://wiki.yoctoproject.org/wiki/TipsAndTricks/TestingToasterWithContainers
* Run the Selenium container:
** docker run -it --rm=true -p 5900:5900 -p 4444:4444 --name=selenium selenium/standalone-firefox-debug:2.53.0
*** 5900 is the default vnc port. If you are runing a vnc server on your machine map a different port e.g. -p 6900:5900 and connect vnc client to 127.0.0.1:6900
*** 4444 is the default selenium sever port.
* Run the tests
** TOASTER_TESTS_BROWSER=http://127.0.0.1:4444/wd/hub TOASTER_TESTS_URL=http://172.17.0.1:8000 ./bitbake/lib/toaster/manage.py test --liveserver=172.17.0.1:8000 tests.browser
** TOASTER_TESTS_BROWSER=remote TOASTER_REMOTE_HUB=http://127.0.0.1:4444/wd/hub ./bitbake/lib/toaster/manage.py test --liveserver=172.17.0.1:8000 tests.browser
*** TOASTER_REMOTE_HUB - This is the address for the Selenium Remote Web Driver hub. Assuming you ran the contianer with -p 4444:4444 it will be http://127.0.0.1:4444/wd/hub.
*** --liveserver=xxx tells Django to run the test server on an interface and port reachable by both host and container.
**** 172.17.0.1 is the default docker bridge on linux, viewable from inside and outside the contianers. Find it with "ip -4 addr show dev docker0"
* connect to the vnc server to see the tests if you would like
** xtightvncviewer 127.0.0.1:5900
** note, you need to wait for the test container to come up before this can connect.
Available drivers:
* chrome (default)
* firefox
* marionette (for newer Firefoxes)
* ie
* phantomjs (deprecated)
* remote
e.g. to run the test suite with phantomjs where you have phantomjs installed
in /home/me/apps/phantomjs:
PATH=/home/me/apps/phantomjs/bin:$PATH TOASTER_TESTS_BROWSER=phantomjs manage.py test tests.browser

View File

@@ -0,0 +1,21 @@
#! /usr/bin/env python3
#
# BitBake Toaster Implementation
#
# Copyright (C) 2013-2016 Intel Corporation
#
# SPDX-License-Identifier: GPL-2.0-only
#
# The Wait class and some of SeleniumDriverHelper and SeleniumTestCase are
# modified from Patchwork, released under the same licence terms as Toaster:
# https://github.com/dlespiau/patchwork/blob/master/patchwork/tests.browser.py
"""
Helper methods for creating Toaster Selenium tests which run within
the context of Django unit tests.
"""
from django.contrib.staticfiles.testing import StaticLiveServerTestCase
from tests.browser.selenium_helpers_base import SeleniumTestCaseBase
class SeleniumTestCase(SeleniumTestCaseBase, StaticLiveServerTestCase):
pass

View File

@@ -0,0 +1,267 @@
#! /usr/bin/env python3
#
# BitBake Toaster Implementation
#
# Copyright (C) 2013-2016 Intel Corporation
#
# SPDX-License-Identifier: GPL-2.0-only
#
# The Wait class and some of SeleniumDriverHelper and SeleniumTestCase are
# modified from Patchwork, released under the same licence terms as Toaster:
# https://github.com/dlespiau/patchwork/blob/master/patchwork/tests.browser.py
"""
Helper methods for creating Toaster Selenium tests which run within
the context of Django unit tests.
"""
import os
import time
import unittest
import pytest
from selenium import webdriver
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.common.by import By
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
from selenium.common.exceptions import NoSuchElementException, \
StaleElementReferenceException, TimeoutException, \
SessionNotCreatedException
def create_selenium_driver(cls,browser='chrome'):
# set default browser string based on env (if available)
env_browser = os.environ.get('TOASTER_TESTS_BROWSER')
if env_browser:
browser = env_browser
if browser == 'chrome':
options = webdriver.ChromeOptions()
options.add_argument('--headless')
options.add_argument('--disable-infobars')
options.add_argument('--disable-dev-shm-usage')
options.add_argument('--no-sandbox')
options.add_argument('--remote-debugging-port=9222')
try:
return webdriver.Chrome(options=options)
except SessionNotCreatedException as e:
exit_message = "Halting tests prematurely to avoid cascading errors."
# check if chrome / chromedriver exists
chrome_path = os.popen("find ~/.cache/selenium/chrome/ -name 'chrome' -type f -print -quit").read().strip()
if not chrome_path:
pytest.exit(f"Failed to install/find chrome.\n{exit_message}")
chromedriver_path = os.popen("find ~/.cache/selenium/chromedriver/ -name 'chromedriver' -type f -print -quit").read().strip()
if not chromedriver_path:
pytest.exit(f"Failed to install/find chromedriver.\n{exit_message}")
# check if depends on each are fulfilled
depends_chrome = os.popen(f"ldd {chrome_path} | grep 'not found'").read().strip()
if depends_chrome:
pytest.exit(f"Missing chrome dependencies.\n{depends_chrome}\n{exit_message}")
depends_chromedriver = os.popen(f"ldd {chromedriver_path} | grep 'not found'").read().strip()
if depends_chromedriver:
pytest.exit(f"Missing chromedriver dependencies.\n{depends_chromedriver}\n{exit_message}")
# print original error otherwise
pytest.exit(f"Failed to start chromedriver.\n{e}\n{exit_message}")
elif browser == 'firefox':
return webdriver.Firefox()
elif browser == 'marionette':
capabilities = DesiredCapabilities.FIREFOX
capabilities['marionette'] = True
return webdriver.Firefox(capabilities=capabilities)
elif browser == 'ie':
return webdriver.Ie()
elif browser == 'phantomjs':
return webdriver.PhantomJS()
elif browser == 'remote':
# if we were to add yet another env variable like TOASTER_REMOTE_BROWSER
# we could let people pick firefox or chrome, left for later
remote_hub= os.environ.get('TOASTER_REMOTE_HUB')
driver = webdriver.Remote(remote_hub,
webdriver.DesiredCapabilities.FIREFOX.copy())
driver.get("http://%s:%s"%(cls.server_thread.host,cls.server_thread.port))
return driver
else:
msg = 'Selenium driver for browser %s is not available' % browser
raise RuntimeError(msg)
class Wait(WebDriverWait):
"""
Subclass of WebDriverWait with predetermined timeout and poll
frequency. Also deals with a wider variety of exceptions.
"""
_TIMEOUT = 10
_POLL_FREQUENCY = 0.5
def __init__(self, driver, timeout=_TIMEOUT, poll=_POLL_FREQUENCY):
self._TIMEOUT = timeout
self._POLL_FREQUENCY = poll
super(Wait, self).__init__(driver, self._TIMEOUT, self._POLL_FREQUENCY)
def until(self, method, message=''):
"""
Calls the method provided with the driver as an argument until the
return value is not False.
"""
end_time = time.time() + self._timeout
while True:
try:
value = method(self._driver)
if value:
return value
except NoSuchElementException:
pass
except StaleElementReferenceException:
pass
time.sleep(self._poll)
if time.time() > end_time:
break
raise TimeoutException(message)
def until_not(self, method, message=''):
"""
Calls the method provided with the driver as an argument until the
return value is False.
"""
end_time = time.time() + self._timeout
while True:
try:
value = method(self._driver)
if not value:
return value
except NoSuchElementException:
return True
except StaleElementReferenceException:
pass
time.sleep(self._poll)
if time.time() > end_time:
break
raise TimeoutException(message)
class SeleniumTestCaseBase(unittest.TestCase):
"""
NB StaticLiveServerTestCase is used as the base test case so that
static files are served correctly in a Selenium test run context; see
https://docs.djangoproject.com/en/1.9/ref/contrib/staticfiles/#specialized-test-case-to-support-live-testing
"""
@classmethod
def setUpClass(cls):
""" Create a webdriver driver at the class level """
super(SeleniumTestCaseBase, cls).setUpClass()
# instantiate the Selenium webdriver once for all the test methods
# in this test case
cls.driver = create_selenium_driver(cls)
cls.driver.maximize_window()
@classmethod
def tearDownClass(cls):
""" Clean up webdriver driver """
cls.driver.quit()
# Allow driver resources to be properly freed before proceeding with further tests
time.sleep(5)
super(SeleniumTestCaseBase, cls).tearDownClass()
def get(self, url):
"""
Selenium requires absolute URLs, so convert Django URLs returned
by resolve() or similar to absolute ones and get using the
webdriver instance.
url: a relative URL
"""
abs_url = '%s%s' % (self.live_server_url, url)
self.driver.get(abs_url)
try: # Ensure page is loaded before proceeding
self.wait_until_visible("#global-nav", poll=3)
except NoSuchElementException:
self.driver.implicitly_wait(3)
except TimeoutException:
self.driver.implicitly_wait(3)
def find(self, selector):
""" Find single element by CSS selector """
return self.driver.find_element(By.CSS_SELECTOR, selector)
def find_all(self, selector):
""" Find all elements matching CSS selector """
return self.driver.find_elements(By.CSS_SELECTOR, selector)
def element_exists(self, selector):
"""
Return True if one element matching selector exists,
False otherwise
"""
return len(self.find_all(selector)) == 1
def focused_element(self):
""" Return the element which currently has focus on the page """
return self.driver.switch_to.active_element
def wait_until_present(self, selector, poll=0.5):
""" Wait until element matching CSS selector is on the page """
is_present = lambda driver: self.find(selector)
msg = 'An element matching "%s" should be on the page' % selector
element = Wait(self.driver, poll=poll).until(is_present, msg)
if poll > 2:
time.sleep(poll) # element need more delay to be present
return element
def wait_until_visible(self, selector, poll=1):
""" Wait until element matching CSS selector is visible on the page """
is_visible = lambda driver: self.find(selector).is_displayed()
msg = 'An element matching "%s" should be visible' % selector
Wait(self.driver, poll=poll).until(is_visible, msg)
time.sleep(poll) # wait for visibility to settle
return self.find(selector)
def wait_until_clickable(self, selector, poll=1):
""" Wait until element matching CSS selector is visible on the page """
WebDriverWait(
self.driver,
Wait._TIMEOUT,
poll_frequency=poll
).until(
EC.element_to_be_clickable((By.ID, selector.removeprefix('#')
)
)
)
return self.find(selector)
def wait_until_focused(self, selector):
""" Wait until element matching CSS selector has focus """
is_focused = \
lambda driver: self.find(selector) == self.focused_element()
msg = 'An element matching "%s" should be focused' % selector
Wait(self.driver).until(is_focused, msg)
return self.find(selector)
def enter_text(self, selector, value):
""" Insert text into element matching selector """
# note that keyup events don't occur until the element is clicked
# (in the case of <input type="text"...>, for example), so simulate
# user clicking the element before inserting text into it
field = self.click(selector)
field.send_keys(value)
return field
def click(self, selector):
""" Click on element which matches CSS selector """
element = self.wait_until_visible(selector)
element.click()
return element
def get_page_source(self):
""" Get raw HTML for the current page """
return self.driver.page_source

View File

@@ -0,0 +1,476 @@
#! /usr/bin/env python3
#
# BitBake Toaster Implementation
#
# Copyright (C) 2013-2016 Intel Corporation
#
# SPDX-License-Identifier: GPL-2.0-only
#
import os
import re
from django.urls import reverse
from selenium.webdriver.support.select import Select
from django.utils import timezone
from bldcontrol.models import BuildRequest
from tests.browser.selenium_helpers import SeleniumTestCase
from orm.models import BitbakeVersion, Layer, Layer_Version, Recipe, Release, Project, Build, Target, Task
from selenium.webdriver.common.by import By
class TestAllBuildsPage(SeleniumTestCase):
""" Tests for all builds page /builds/ """
PROJECT_NAME = 'test project'
CLI_BUILDS_PROJECT_NAME = 'command line builds'
def setUp(self):
builldir = os.environ.get('BUILDDIR', './')
bbv = BitbakeVersion.objects.create(name='bbv1', giturl=f'{builldir}/',
branch='master', dirpath='')
release = Release.objects.create(name='release1',
bitbake_version=bbv)
self.project1 = Project.objects.create_project(name=self.PROJECT_NAME,
release=release)
self.default_project = Project.objects.create_project(
name=self.CLI_BUILDS_PROJECT_NAME,
release=release
)
self.default_project.is_default = True
self.default_project.save()
# parameters for builds to associate with the projects
now = timezone.now()
self.project1_build_success = {
'project': self.project1,
'started_on': now,
'completed_on': now,
'outcome': Build.SUCCEEDED
}
self.project1_build_failure = {
'project': self.project1,
'started_on': now,
'completed_on': now,
'outcome': Build.FAILED
}
self.default_project_build_success = {
'project': self.default_project,
'started_on': now,
'completed_on': now,
'outcome': Build.SUCCEEDED
}
def _get_build_time_element(self, build):
"""
Return the HTML element containing the build time for a build
in the recent builds area
"""
selector = 'div[data-latest-build-result="%s"] ' \
'[data-role="data-recent-build-buildtime-field"]' % build.id
# because this loads via Ajax, wait for it to be visible
self.wait_until_visible(selector)
build_time_spans = self.find_all(selector)
self.assertEqual(len(build_time_spans), 1)
return build_time_spans[0]
def _get_row_for_build(self, build):
""" Get the table row for the build from the all builds table """
self.wait_until_visible('#allbuildstable')
rows = self.find_all('#allbuildstable tr')
# look for the row with a download link on the recipe which matches the
# build ID
url = reverse('builddashboard', args=(build.id,))
selector = 'td.target a[href="%s"]' % url
found_row = None
for row in rows:
outcome_links = row.find_elements(By.CSS_SELECTOR, selector)
if len(outcome_links) == 1:
found_row = row
break
self.assertNotEqual(found_row, None)
return found_row
def _get_create_builds(self, **kwargs):
""" Create a build and return the build object """
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')
self._set_buildRequest_and_task_on_build(build)
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')
self._set_buildRequest_and_task_on_build(build)
return build1, build2
def _create_recipe(self):
""" Add a recipe to the database and return it """
layer = Layer.objects.create()
layer_version = Layer_Version.objects.create(layer=layer)
return Recipe.objects.create(name='recipe_foo', layer_version=layer_version)
def _set_buildRequest_and_task_on_build(self, build):
""" Set buildRequest and task on build """
build.recipes_parsed = 1
build.save()
buildRequest = BuildRequest.objects.create(
build=build,
project=self.project1,
state=BuildRequest.REQ_COMPLETED)
build.build_request = buildRequest
recipe = self._create_recipe()
task = Task.objects.create(build=build,
recipe=recipe,
task_name='task',
outcome=Task.OUTCOME_SUCCESS)
task.save()
build.save()
def test_show_tasks_with_suffix(self):
""" Task should be shown as suffix on build name """
build = Build.objects.create(**self.project1_build_success)
target = 'bash'
task = 'clean'
Target.objects.create(build=build, target=target, task=task)
url = reverse('all-builds')
self.get(url)
self.wait_until_visible('td[class="target"]')
cell = self.find('td[class="target"]')
content = cell.get_attribute('innerHTML')
expected_text = '%s:%s' % (target, task)
self.assertTrue(re.search(expected_text, content),
'"target" cell should contain text %s' % expected_text)
def test_rebuild_buttons(self):
"""
Test 'Rebuild' buttons in recent builds section
'Rebuild' button should not be shown for command-line builds,
but should be shown for other builds
"""
build1 = Build.objects.create(**self.project1_build_success)
default_build = Build.objects.create(
**self.default_project_build_success)
url = reverse('all-builds')
self.get(url)
# should see a rebuild button for non-command-line builds
self.wait_until_visible('#allbuildstable tbody tr')
selector = 'div[data-latest-build-result="%s"] .rebuild-btn' % build1.id
run_again_button = self.find_all(selector)
self.assertEqual(len(run_again_button), 1,
'should see a rebuild button for non-cli builds')
# shouldn't see a rebuild button for command-line builds
selector = 'div[data-latest-build-result="%s"] .rebuild-btn' % default_build.id
run_again_button = self.find_all(selector)
self.assertEqual(len(run_again_button), 0,
'should not see a rebuild button for cli builds')
def test_tooltips_on_project_name(self):
"""
Test tooltips shown next to project name in the main table
A tooltip should be present next to the command line
builds project name in the all builds page, but not for
other projects
"""
Build.objects.create(**self.project1_build_success)
Build.objects.create(**self.default_project_build_success)
url = reverse('all-builds')
self.get(url)
self.wait_until_visible('#allbuildstable', poll=3)
# get the project name cells from the table
cells = self.find_all('#allbuildstable td[class="project"]')
selector = 'span.get-help'
for cell in cells:
content = cell.get_attribute('innerHTML')
help_icons = cell.find_elements(By.CSS_SELECTOR, selector)
if re.search(self.PROJECT_NAME, content):
# no help icon next to non-cli project name
msg = 'should not be a help icon for non-cli builds name'
self.assertEqual(len(help_icons), 0, msg)
elif re.search(self.CLI_BUILDS_PROJECT_NAME, content):
# help icon next to cli project name
msg = 'should be a help icon for cli builds name'
self.assertEqual(len(help_icons), 1, msg)
else:
msg = 'found unexpected project name cell in all builds table'
self.fail(msg)
def test_builds_time_links(self):
"""
Successful builds should have links on the time column and in the
recent builds area; failed builds should not have links on the time column,
or in the recent builds area
"""
build1, build2 = self._get_create_builds()
url = reverse('all-builds')
self.get(url)
self.wait_until_visible('#allbuildstable', poll=3)
# test recent builds area for successful build
element = self._get_build_time_element(build1)
links = element.find_elements(By.CSS_SELECTOR, 'a')
msg = 'should be a link on the build time for a successful recent build'
self.assertEqual(len(links), 1, msg)
# test recent builds area for failed build
element = self._get_build_time_element(build2)
links = element.find_elements(By.CSS_SELECTOR, 'a')
msg = 'should not be a link on the build time for a failed recent build'
self.assertEqual(len(links), 0, msg)
# test the time column for successful build
build1_row = self._get_row_for_build(build1)
links = build1_row.find_elements(By.CSS_SELECTOR, 'td.time a')
msg = 'should be a link on the build time for a successful build'
self.assertEqual(len(links), 1, msg)
# test the time column for failed build
build2_row = self._get_row_for_build(build2)
links = build2_row.find_elements(By.CSS_SELECTOR, 'td.time a')
msg = 'should not be a link on the build time for a failed build'
self.assertEqual(len(links), 0, msg)
def test_builds_table_search_box(self):
""" Test the search box in the builds table on the all builds page """
self._get_create_builds()
url = reverse('all-builds')
self.get(url)
# Check search box is present and works
self.wait_until_visible('#allbuildstable tbody tr')
search_box = self.find('#search-input-allbuildstable')
self.assertTrue(search_box.is_displayed())
# Check that we can search for a build by recipe name
search_box.send_keys('foo')
search_btn = self.find('#search-submit-allbuildstable')
search_btn.click()
self.wait_until_visible('#allbuildstable tbody tr')
rows = self.find_all('#allbuildstable tbody tr')
self.assertTrue(len(rows) >= 1)
def test_filtering_on_failure_tasks_column(self):
""" Test the filtering on failure tasks column in the builds table on the all builds page """
def _check_if_filter_failed_tasks_column_is_visible():
# check if failed tasks filter column is visible, if not click on it
# 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')
filter_fails_task_checkbox = self.find('#checkbox-failed_tasks')
if not filter_fails_task_checkbox.is_selected():
filter_fails_task_checkbox.click()
edit_column.click()
self._get_create_builds(success=10, failure=10)
url = reverse('all-builds')
self.get(url)
# Check filtering on failure tasks column
self.wait_until_visible('#allbuildstable tbody tr')
_check_if_filter_failed_tasks_column_is_visible()
failed_tasks_filter = self.find('#failed_tasks_filter')
failed_tasks_filter.click()
# Check popup is visible
self.wait_until_visible('#filter-modal-allbuildstable')
self.assertTrue(
self.find('#filter-modal-allbuildstable').is_displayed())
# Check that we can filter by failure tasks
build_without_failure_tasks = self.find(
'#failed_tasks_filter\\:without_failed_tasks')
build_without_failure_tasks.click()
# click on apply button
self.find('#filter-modal-allbuildstable .btn-primary').click()
self.wait_until_visible('#allbuildstable tbody tr')
# Check if filter is applied, by checking if failed_tasks_filter has btn-primary class
self.assertTrue(self.find('#failed_tasks_filter').get_attribute(
'class').find('btn-primary') != -1)
def test_filtering_on_completedOn_column(self):
""" Test the filtering on completed_on column in the builds table on the all builds page """
self._get_create_builds(success=10, failure=10)
url = reverse('all-builds')
self.get(url)
# Check filtering on failure tasks column
self.wait_until_visible('#allbuildstable tbody tr')
completed_on_filter = self.find('#completed_on_filter')
completed_on_filter.click()
# Check popup is visible
self.wait_until_visible('#filter-modal-allbuildstable')
self.assertTrue(
self.find('#filter-modal-allbuildstable').is_displayed())
# Check that we can filter by failure tasks
build_without_failure_tasks = self.find(
'#completed_on_filter\\:date_range')
build_without_failure_tasks.click()
# click on apply button
self.find('#filter-modal-allbuildstable .btn-primary').click()
self.wait_until_visible('#allbuildstable tbody tr')
# Check if filter is applied, by checking if completed_on_filter has btn-primary class
self.assertTrue(self.find('#completed_on_filter').get_attribute(
'class').find('btn-primary') != -1)
# Filter by date range
self.find('#completed_on_filter').click()
self.wait_until_visible('#filter-modal-allbuildstable')
date_ranges = self.driver.find_elements(
By.XPATH, '//input[@class="form-control hasDatepicker"]')
today = timezone.now()
yestersday = today - timezone.timedelta(days=1)
date_ranges[0].send_keys(yestersday.strftime('%Y-%m-%d'))
date_ranges[1].send_keys(today.strftime('%Y-%m-%d'))
self.find('#filter-modal-allbuildstable .btn-primary').click()
self.wait_until_visible('#allbuildstable tbody tr')
self.assertTrue(self.find('#completed_on_filter').get_attribute(
'class').find('btn-primary') != -1)
# Check if filter is applied, number of builds displayed should be 6
self.assertTrue(len(self.find_all('#allbuildstable tbody tr')) >= 4)
def test_builds_table_editColumn(self):
""" Test the edit column feature in the builds table on the all builds page """
self._get_create_builds(success=10, failure=10)
def test_edit_column(check_box_id):
# Check that we can hide/show table column
check_box = self.find(f'#{check_box_id}')
th_class = str(check_box_id).replace('checkbox-', '')
if check_box.is_selected():
# check if column is visible in table
self.assertTrue(
self.find(
f'#allbuildstable 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'#allbuildstable 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'#allbuildstable 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'#allbuildstable thead th.{th_class}'
).is_displayed(),
f"The {th_class} column is checked in EditColumn dropdown, but it's not visible in table"
)
url = reverse('all-builds')
self.get(url)
self.wait_until_visible('#allbuildstable 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-errors_no')
test_edit_column('checkbox-failed_tasks')
test_edit_column('checkbox-image_files')
test_edit_column('checkbox-project')
test_edit_column('checkbox-started_on')
test_edit_column('checkbox-time')
test_edit_column('checkbox-warnings_no')
def test_builds_table_show_rows(self):
""" Test the show rows feature in the builds table on the all builds page """
self._get_create_builds(success=100, failure=100)
def test_show_rows(row_to_show, show_row_link):
# Check that we can show rows == row_to_show
show_row_link.select_by_value(str(row_to_show))
self.wait_until_visible('#allbuildstable tbody tr', poll=3)
# check at least some rows are visible
self.assertTrue(
len(self.find_all('#allbuildstable tbody tr')) > 0
)
url = reverse('all-builds')
self.get(url)
self.wait_until_visible('#allbuildstable tbody tr')
show_rows = self.driver.find_elements(
By.XPATH,
'//select[@class="form-control pagesize-allbuildstable"]'
)
# 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)

View File

@@ -0,0 +1,337 @@
#! /usr/bin/env python3
#
# BitBake Toaster Implementation
#
# Copyright (C) 2013-2016 Intel Corporation
#
# SPDX-License-Identifier: GPL-2.0-only
#
import os
import re
from django.urls import reverse
from django.utils import timezone
from selenium.webdriver.support.select import Select
from tests.browser.selenium_helpers import SeleniumTestCase
from orm.models import BitbakeVersion, Release, Project, Build
from orm.models import ProjectVariable
from selenium.webdriver.common.by import By
class TestAllProjectsPage(SeleniumTestCase):
""" Browser tests for projects page /projects/ """
PROJECT_NAME = 'test project'
CLI_BUILDS_PROJECT_NAME = 'command line builds'
MACHINE_NAME = 'delorean'
def setUp(self):
""" Add default project manually """
project = Project.objects.create_project(
self.CLI_BUILDS_PROJECT_NAME, None)
self.default_project = project
self.default_project.is_default = True
self.default_project.save()
# this project is only set for some of the tests
self.project = None
self.release = None
def _create_projects(self, nb_project=10):
projects = []
for i in range(1, nb_project + 1):
projects.append(
Project(
name='test project {}'.format(i),
release=self.release,
)
)
Project.objects.bulk_create(projects)
def _add_build_to_default_project(self):
""" Add a build to the default project (not used in all tests) """
now = timezone.now()
build = Build.objects.create(project=self.default_project,
started_on=now,
completed_on=now)
build.save()
def _add_non_default_project(self):
""" Add another project """
builldir = os.environ.get('BUILDDIR', './')
bbv = BitbakeVersion.objects.create(name='test bbv', giturl=f'{builldir}/',
branch='master', dirpath='')
self.release = Release.objects.create(name='test release',
branch_name='master',
bitbake_version=bbv)
self.project = Project.objects.create_project(
self.PROJECT_NAME, self.release)
self.project.is_default = False
self.project.save()
# fake the MACHINE variable
project_var = ProjectVariable.objects.create(project=self.project,
name='MACHINE',
value=self.MACHINE_NAME)
project_var.save()
def _get_row_for_project(self, project_name):
""" Get the HTML row for a project, or None if not found """
self.wait_until_visible('#projectstable tbody tr', poll=3)
rows = self.find_all('#projectstable tbody tr')
# find the row with a project name matching the one supplied
found_row = None
for row in rows:
if re.search(project_name, row.get_attribute('innerHTML')):
found_row = row
break
return found_row
def test_default_project_hidden(self):
"""
The default project should be hidden if it has no builds
and we should see the "no results" area
"""
url = reverse('all-projects')
self.get(url)
self.wait_until_visible('#empty-state-projectstable')
rows = self.find_all('#projectstable tbody tr')
self.assertEqual(len(rows), 0, 'should be no projects displayed')
def test_default_project_has_build(self):
""" The default project should be shown if it has builds """
self._add_build_to_default_project()
url = reverse('all-projects')
self.get(url)
default_project_row = self._get_row_for_project(
self.default_project.name)
self.assertNotEqual(default_project_row, None,
'default project "cli builds" should be in page')
def test_default_project_release(self):
"""
The release for the default project should display as
'Not applicable'
"""
# need a build, otherwise project doesn't display at all
self._add_build_to_default_project()
# another project to test, which should show release
self._add_non_default_project()
self.get(reverse('all-projects'))
self.wait_until_visible("#projectstable tr")
# find the row for the default project
default_project_row = self._get_row_for_project(
self.default_project.name)
# check the release text for the default project
selector = 'span[data-project-field="release"] span.text-muted'
element = default_project_row.find_element(By.CSS_SELECTOR, selector)
text = element.text.strip()
self.assertEqual(text, 'Not applicable',
'release should be "not applicable" for default project')
# find the row for the default project
other_project_row = self._get_row_for_project(self.project.name)
# check the link in the release cell for the other project
selector = 'span[data-project-field="release"]'
element = other_project_row.find_element(By.CSS_SELECTOR, selector)
text = element.text.strip()
self.assertEqual(text, self.release.name,
'release name should be shown for non-default project')
def test_default_project_machine(self):
"""
The machine for the default project should display as
'Not applicable'
"""
# need a build, otherwise project doesn't display at all
self._add_build_to_default_project()
# another project to test, which should show machine
self._add_non_default_project()
self.get(reverse('all-projects'))
self.wait_until_visible("#projectstable tr")
# find the row for the default project
default_project_row = self._get_row_for_project(
self.default_project.name)
# check the machine cell for the default project
selector = 'span[data-project-field="machine"] span.text-muted'
element = default_project_row.find_element(By.CSS_SELECTOR, selector)
text = element.text.strip()
self.assertEqual(text, 'Not applicable',
'machine should be not applicable for default project')
# find the row for the default project
other_project_row = self._get_row_for_project(self.project.name)
# check the link in the machine cell for the other project
selector = 'span[data-project-field="machine"]'
element = other_project_row.find_element(By.CSS_SELECTOR, selector)
text = element.text.strip()
self.assertEqual(text, self.MACHINE_NAME,
'machine name should be shown for non-default project')
def test_project_page_links(self):
"""
Test that links for the default project point to the builds
page /projects/X/builds for that project, and that links for
other projects point to their configuration pages /projects/X/
"""
# need a build, otherwise project doesn't display at all
self._add_build_to_default_project()
# another project to test
self._add_non_default_project()
self.get(reverse('all-projects'))
# find the row for the default project
default_project_row = self._get_row_for_project(
self.default_project.name)
# check the link on the name field
selector = 'span[data-project-field="name"] a'
element = default_project_row.find_element(By.CSS_SELECTOR, selector)
link_url = element.get_attribute('href').strip()
expected_url = reverse(
'projectbuilds', args=(self.default_project.id,))
msg = 'link on default project name should point to builds but was %s' % link_url
self.assertTrue(link_url.endswith(expected_url), msg)
# find the row for the other project
other_project_row = self._get_row_for_project(self.project.name)
# check the link for the other project
selector = 'span[data-project-field="name"] a'
element = other_project_row.find_element(By.CSS_SELECTOR, selector)
link_url = element.get_attribute('href').strip()
expected_url = reverse('project', args=(self.project.id,))
msg = 'link on project name should point to configuration but was %s' % link_url
self.assertTrue(link_url.endswith(expected_url), msg)
def test_allProject_table_search_box(self):
""" Test the search box in the all project table on the all projects page """
self._create_projects()
url = reverse('all-projects')
self.get(url)
# Chseck search box is present and works
self.wait_until_visible('#projectstable tbody tr', poll=3)
search_box = self.find('#search-input-projectstable')
self.assertTrue(search_box.is_displayed())
# Check that we can search for a project by project name
search_box.send_keys('test project 10')
search_btn = self.find('#search-submit-projectstable')
search_btn.click()
self.wait_until_visible('#projectstable tbody tr', poll=3)
rows = self.find_all('#projectstable tbody tr')
self.assertTrue(len(rows) == 1)
def test_allProject_table_editColumn(self):
""" Test the edit column feature in the projects table on the all projects page """
self._create_projects()
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'#projectstable 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'#projectstable 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'#projectstable 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'#projectstable thead th.{th_class}'
).is_displayed(),
f"The {th_class} column is checked in EditColumn dropdown, but it's not visible in table"
)
url = reverse('all-projects')
self.get(url)
self.wait_until_visible('#projectstable tbody tr', poll=3)
# 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-errors')
test_edit_column('checkbox-image_files')
test_edit_column('checkbox-last_build_outcome')
test_edit_column('checkbox-recipe_name')
test_edit_column('checkbox-warnings')
def test_allProject_table_show_rows(self):
""" Test the show rows feature in the projects table on the all projects page """
self._create_projects(nb_project=200)
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('#projectstable tbody tr', poll=3)
# check at least some rows are visible
self.assertTrue(
len(self.find_all('#projectstable tbody tr')) > 0
)
url = reverse('all-projects')
self.get(url)
self.wait_until_visible('#projectstable tbody tr', poll=3)
show_rows = self.driver.find_elements(
By.XPATH,
'//select[@class="form-control pagesize-projectstable"]'
)
# 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)

View File

@@ -0,0 +1,340 @@
#! /usr/bin/env python3
#
# BitBake Toaster Implementation
#
# Copyright (C) 2013-2016 Intel Corporation
#
# SPDX-License-Identifier: GPL-2.0-only
#
import os
from django.urls import reverse
from django.utils import timezone
from tests.browser.selenium_helpers import SeleniumTestCase
from orm.models import Project, Release, BitbakeVersion, Build, LogMessage
from orm.models import Layer, Layer_Version, Recipe, CustomImageRecipe, Variable
from selenium.webdriver.common.by import By
class TestBuildDashboardPage(SeleniumTestCase):
""" Tests for the build dashboard /build/X """
def setUp(self):
builldir = os.environ.get('BUILDDIR', './')
bbv = BitbakeVersion.objects.create(name='bbv1', giturl=f'{builldir}/',
branch='master', dirpath="")
release = Release.objects.create(name='release1',
bitbake_version=bbv)
project = Project.objects.create_project(name='test project',
release=release)
now = timezone.now()
self.build1 = Build.objects.create(project=project,
started_on=now,
completed_on=now,
outcome=Build.SUCCEEDED)
self.build2 = Build.objects.create(project=project,
started_on=now,
completed_on=now,
outcome=Build.SUCCEEDED)
self.build3 = Build.objects.create(project=project,
started_on=now,
completed_on=now,
outcome=Build.FAILED)
# add Variable objects to the successful builds, as this is the criterion
# used to determine whether the left-hand panel should be displayed
Variable.objects.create(build=self.build1,
variable_name='Foo',
variable_value='Bar')
Variable.objects.create(build=self.build2,
variable_name='Foo',
variable_value='Bar')
# exception
msg1 = 'an exception was thrown'
self.exception_message = LogMessage.objects.create(
build=self.build1,
level=LogMessage.EXCEPTION,
message=msg1
)
# critical
msg2 = 'a critical error occurred'
self.critical_message = LogMessage.objects.create(
build=self.build1,
level=LogMessage.CRITICAL,
message=msg2
)
# error on the failed build
msg3 = 'an error occurred'
self.error_message = LogMessage.objects.create(
build=self.build3,
level=LogMessage.ERROR,
message=msg3
)
# warning on the failed build
msg4 = 'DANGER WILL ROBINSON'
self.warning_message = LogMessage.objects.create(
build=self.build3,
level=LogMessage.WARNING,
message=msg4
)
# recipes related to the build, for testing the edit custom image/new
# custom image buttons
layer = Layer.objects.create(name='alayer')
layer_version = Layer_Version.objects.create(
layer=layer, build=self.build1
)
# non-image recipes related to a build, for testing the new custom
# image button
layer_version2 = Layer_Version.objects.create(layer=layer,
build=self.build3)
# image recipes
self.image_recipe1 = Recipe.objects.create(
name='recipeA',
layer_version=layer_version,
file_path='/foo/recipeA.bb',
is_image=True
)
self.image_recipe2 = Recipe.objects.create(
name='recipeB',
layer_version=layer_version,
file_path='/foo/recipeB.bb',
is_image=True
)
# custom image recipes for this project
self.custom_image_recipe1 = CustomImageRecipe.objects.create(
name='customRecipeY',
project=project,
layer_version=layer_version,
file_path='/foo/customRecipeY.bb',
base_recipe=self.image_recipe1,
is_image=True
)
self.custom_image_recipe2 = CustomImageRecipe.objects.create(
name='customRecipeZ',
project=project,
layer_version=layer_version,
file_path='/foo/customRecipeZ.bb',
base_recipe=self.image_recipe2,
is_image=True
)
# custom image recipe for a different project (to test filtering
# of image recipes and custom image recipes is correct: this shouldn't
# show up in either query against self.build1)
self.custom_image_recipe3 = CustomImageRecipe.objects.create(
name='customRecipeOmega',
project=Project.objects.create(name='baz', release=release),
layer_version=Layer_Version.objects.create(
layer=layer, build=self.build2
),
file_path='/foo/customRecipeOmega.bb',
base_recipe=self.image_recipe2,
is_image=True
)
# another non-image recipe (to test filtering of image recipes and
# custom image recipes is correct: this shouldn't show up in either
# for any build)
self.non_image_recipe = Recipe.objects.create(
name='nonImageRecipe',
layer_version=layer_version,
file_path='/foo/nonImageRecipe.bb',
is_image=False
)
def _get_build_dashboard(self, build):
"""
Navigate to the build dashboard for build
"""
url = reverse('builddashboard', args=(build.id,))
self.get(url)
self.wait_until_visible('#global-nav', poll=3)
def _get_build_dashboard_errors(self, build):
"""
Get a list of HTML fragments representing the errors on the
dashboard for the Build object build
"""
self._get_build_dashboard(build)
return self.find_all('#errors div.alert-danger')
def _check_for_log_message(self, message_elements, log_message):
"""
Check that the LogMessage <log_message> has a representation in
the HTML elements <message_elements>.
message_elements: WebElements representing the log messages shown
in the build dashboard; each should have a <pre> element inside
it with a data-log-message-id attribute
log_message: orm.models.LogMessage instance
"""
expected_text = log_message.message
expected_pk = str(log_message.pk)
found = False
for element in message_elements:
log_message_text = element.find_element(By.TAG_NAME, 'pre').text.strip()
text_matches = (log_message_text == expected_text)
log_message_pk = element.get_attribute('data-log-message-id')
id_matches = (log_message_pk == expected_pk)
if text_matches and id_matches:
found = True
break
template_vars = (expected_text, expected_pk)
assertion_failed_msg = 'message not found: ' \
'expected text "%s" and ID %s' % template_vars
self.assertTrue(found, assertion_failed_msg)
def _check_for_error_message(self, build, log_message):
"""
Check whether the LogMessage instance <log_message> is
represented as an HTML error in the dashboard page for the Build object
build
"""
errors = self._get_build_dashboard_errors(build)
self._check_for_log_message(errors, log_message)
def _check_labels_in_modal(self, modal, expected):
"""
Check that the text values of the <label> elements inside
the WebElement modal match the list of text values in expected
"""
# labels containing the radio buttons we're testing for
labels = modal.find_elements(By.CSS_SELECTOR,".radio")
labels_text = [lab.text for lab in labels]
self.assertEqual(len(labels_text), len(expected))
for expected_text in expected:
self.assertTrue(expected_text in labels_text,
"Could not find %s in %s" % (expected_text,
labels_text))
def test_exceptions_show_as_errors(self):
"""
LogMessages with level EXCEPTION should display in the errors
section of the page
"""
self._check_for_error_message(self.build1, self.exception_message)
def test_criticals_show_as_errors(self):
"""
LogMessages with level CRITICAL should display in the errors
section of the page
"""
self._check_for_error_message(self.build1, self.critical_message)
def test_edit_custom_image_button(self):
"""
A build which built two custom images should present a modal which lets
the user choose one of them to edit
"""
self._get_build_dashboard(self.build1)
# click the "edit custom image" button, which populates the modal
selector = '[data-role="edit-custom-image-trigger"]'
self.click(selector)
modal = self.driver.find_element(By.ID, 'edit-custom-image-modal')
self.wait_until_visible("#edit-custom-image-modal")
# recipes we expect to see in the edit custom image modal
expected_recipes = [
self.custom_image_recipe1.name,
self.custom_image_recipe2.name
]
self._check_labels_in_modal(modal, expected_recipes)
def test_new_custom_image_button(self):
"""
Check that a build with multiple images and custom images presents
all of them as options for creating a new custom image from
"""
self._get_build_dashboard(self.build1)
# click the "new custom image" button, which populates the modal
selector = '[data-role="new-custom-image-trigger"]'
self.click(selector)
modal = self.driver.find_element(By.ID,'new-custom-image-modal')
self.wait_until_visible("#new-custom-image-modal")
# recipes we expect to see in the new custom image modal
expected_recipes = [
self.image_recipe1.name,
self.image_recipe2.name,
self.custom_image_recipe1.name,
self.custom_image_recipe2.name
]
self._check_labels_in_modal(modal, expected_recipes)
def test_new_custom_image_button_no_image(self):
"""
Check that a build which builds non-image recipes doesn't show
the new custom image button on the dashboard.
"""
self._get_build_dashboard(self.build3)
selector = '[data-role="new-custom-image-trigger"]'
self.assertFalse(self.element_exists(selector),
'new custom image button should not show for builds which ' \
'don\'t have any image recipes')
def test_left_panel(self):
""""
Builds which succeed should have a left panel and a build summary
"""
self._get_build_dashboard(self.build1)
left_panel = self.find_all('#nav')
self.assertEqual(len(left_panel), 1)
build_summary = self.find_all('[data-role="build-summary-heading"]')
self.assertEqual(len(build_summary), 1)
def test_failed_no_left_panel(self):
"""
Builds which fail should have no left panel and no build summary
"""
self._get_build_dashboard(self.build3)
left_panel = self.find_all('#nav')
self.assertEqual(len(left_panel), 0)
build_summary = self.find_all('[data-role="build-summary-heading"]')
self.assertEqual(len(build_summary), 0)
def test_failed_shows_errors_and_warnings(self):
"""
Failed builds should still show error and warning messages
"""
self._get_build_dashboard(self.build3)
errors = self.find_all('#errors div.alert-danger')
self._check_for_log_message(errors, self.error_message)
# expand the warnings area
self.click('#warning-toggle')
self.wait_until_visible('#warnings div.alert-warning')
warnings = self.find_all('#warnings div.alert-warning')
self._check_for_log_message(warnings, self.warning_message)

View File

@@ -0,0 +1,212 @@
#! /usr/bin/env python3
#
# BitBake Toaster Implementation
#
# Copyright (C) 2013-2016 Intel Corporation
#
# SPDX-License-Identifier: GPL-2.0-only
#
import os
from django.urls import reverse
from django.utils import timezone
from tests.browser.selenium_helpers import SeleniumTestCase
from orm.models import Project, Release, BitbakeVersion, Build, Target, Package
from orm.models import Target_Image_File, TargetSDKFile, TargetKernelFile
from orm.models import Target_Installed_Package, Variable
class TestBuildDashboardPageArtifacts(SeleniumTestCase):
""" Tests for artifacts on the build dashboard /build/X """
def setUp(self):
builldir = os.environ.get('BUILDDIR', './')
bbv = BitbakeVersion.objects.create(name='bbv1', giturl=f'{builldir}/',
branch='master', dirpath="")
release = Release.objects.create(name='release1',
bitbake_version=bbv)
self.project = Project.objects.create_project(name='test project',
release=release)
def _get_build_dashboard(self, build):
"""
Navigate to the build dashboard for build
"""
url = reverse('builddashboard', args=(build.id,))
self.get(url)
def _has_build_artifacts_heading(self):
"""
Check whether the "Build artifacts" heading is visible (True if it
is, False otherwise).
"""
return self.element_exists('[data-heading="build-artifacts"]')
def _has_images_menu_option(self):
"""
Try to get the "Images" list element from the left-hand menu in the
build dashboard, and return True if it is present, False otherwise.
"""
return self.element_exists('li.nav-header[data-menu-heading="images"]')
def test_no_artifacts(self):
"""
If a build produced no artifacts, the artifacts heading and images
menu option shouldn't show.
"""
now = timezone.now()
build = Build.objects.create(project=self.project,
started_on=now, completed_on=now, outcome=Build.SUCCEEDED)
Target.objects.create(is_image=False, build=build, task='',
target='mpfr-native')
self._get_build_dashboard(build)
# check build artifacts heading
msg = 'Build artifacts heading should not be displayed for non-image' \
'builds'
self.assertFalse(self._has_build_artifacts_heading(), msg)
# check "Images" option in left-hand menu (should not be there)
msg = 'Images option should not be shown in left-hand menu'
self.assertFalse(self._has_images_menu_option(), msg)
def test_sdk_artifacts(self):
"""
If a build produced SDK artifacts, they should be shown, but the section
for image files and the images menu option should be hidden.
The packages count and size should also be hidden.
"""
now = timezone.now()
build = Build.objects.create(project=self.project,
started_on=now, completed_on=timezone.now(),
outcome=Build.SUCCEEDED)
target = Target.objects.create(is_image=True, build=build,
task='populate_sdk', target='core-image-minimal')
sdk_file1 = TargetSDKFile.objects.create(target=target,
file_size=100000,
file_name='/home/foo/core-image-minimal.toolchain.sh')
sdk_file2 = TargetSDKFile.objects.create(target=target,
file_size=120000,
file_name='/home/foo/x86_64.toolchain.sh')
self._get_build_dashboard(build)
# check build artifacts heading
msg = 'Build artifacts heading should be displayed for SDK ' \
'builds which generate artifacts'
self.assertTrue(self._has_build_artifacts_heading(), msg)
# check "Images" option in left-hand menu (should not be there)
msg = 'Images option should not be shown in left-hand menu for ' \
'builds which didn\'t generate an image file'
self.assertFalse(self._has_images_menu_option(), msg)
# check links to SDK artifacts
sdk_artifact_links = self.find_all('[data-links="sdk-artifacts"] li')
self.assertEqual(len(sdk_artifact_links), 2,
'should be links to 2 SDK artifacts')
# package count and size should not be visible, no link on
# target name
selector = '[data-value="target-package-count"]'
self.assertFalse(self.element_exists(selector),
'package count should not be shown for non-image builds')
selector = '[data-value="target-package-size"]'
self.assertFalse(self.element_exists(selector),
'package size should not be shown for non-image builds')
selector = '[data-link="target-packages"]'
self.assertFalse(self.element_exists(selector),
'link to target packages should not be on target heading')
def test_image_artifacts(self):
"""
If a build produced image files, kernel artifacts, and manifests,
they should all be shown, as well as the image link in the left-hand
menu.
The packages count and size should be shown, with a link to the
package display page.
"""
now = timezone.now()
build = Build.objects.create(project=self.project,
started_on=now, completed_on=timezone.now(),
outcome=Build.SUCCEEDED)
# add a variable to the build so that it counts as "started"
Variable.objects.create(build=build,
variable_name='Christopher',
variable_value='Lee')
target = Target.objects.create(is_image=True, build=build,
task='', target='core-image-minimal',
license_manifest_path='/home/foo/license.manifest',
package_manifest_path='/home/foo/package.manifest')
image_file = Target_Image_File.objects.create(target=target,
file_name='/home/foo/core-image-minimal.ext4', file_size=9000)
kernel_file1 = TargetKernelFile.objects.create(target=target,
file_name='/home/foo/bzImage', file_size=2000)
kernel_file2 = TargetKernelFile.objects.create(target=target,
file_name='/home/foo/bzImage', file_size=2000)
package = Package.objects.create(build=build, name='foo', size=1024,
installed_name='foo1')
installed_package = Target_Installed_Package.objects.create(
target=target, package=package)
self._get_build_dashboard(build)
# check build artifacts heading
msg = 'Build artifacts heading should be displayed for image ' \
'builds'
self.assertTrue(self._has_build_artifacts_heading(), msg)
# check "Images" option in left-hand menu (should be there)
msg = 'Images option should be shown in left-hand menu for image builds'
self.assertTrue(self._has_images_menu_option(), msg)
# check link to image file
selector = '[data-links="image-artifacts"] li'
self.assertTrue(self.element_exists(selector),
'should be a link to the image file (selector %s)' % selector)
# check links to kernel artifacts
kernel_artifact_links = \
self.find_all('[data-links="kernel-artifacts"] li')
self.assertEqual(len(kernel_artifact_links), 2,
'should be links to 2 kernel artifacts')
# check manifest links
selector = 'a[data-link="license-manifest"]'
self.assertTrue(self.element_exists(selector),
'should be a link to the license manifest (selector %s)' % selector)
selector = 'a[data-link="package-manifest"]'
self.assertTrue(self.element_exists(selector),
'should be a link to the package manifest (selector %s)' % selector)
# check package count and size, link on target name
selector = '[data-value="target-package-count"]'
element = self.find(selector)
self.assertEqual(element.text, '1',
'package count should be shown for image builds')
selector = '[data-value="target-package-size"]'
element = self.find(selector)
self.assertEqual(element.text, '1.0 KB',
'package size should be shown for image builds')
selector = '[data-link="target-packages"]'
self.assertTrue(self.element_exists(selector),
'link to target packages should be on target heading')

View File

@@ -0,0 +1,54 @@
#! /usr/bin/env python3
#
# BitBake Toaster Implementation
#
# Copyright (C) 2013-2016 Intel Corporation
#
# SPDX-License-Identifier: GPL-2.0-only
#
from django.urls import reverse
from django.utils import timezone
from tests.browser.selenium_helpers import SeleniumTestCase
from orm.models import Project, Build, Recipe, Task, Layer, Layer_Version
from orm.models import Target
class TestBuilddashboardPageRecipes(SeleniumTestCase):
""" Test build dashboard recipes sub-page """
def setUp(self):
project = Project.objects.get_or_create_default_project()
now = timezone.now()
self.build = Build.objects.create(project=project,
started_on=now,
completed_on=now)
layer = Layer.objects.create()
layer_version = Layer_Version.objects.create(layer=layer,
build=self.build)
recipe = Recipe.objects.create(layer_version=layer_version)
task = Task.objects.create(build=self.build, recipe=recipe, order=1)
Target.objects.create(build=self.build, task=task, target='do_build')
def test_build_recipes_columns(self):
"""
Check that non-hideable columns of the table on the recipes sub-page
are disabled on the edit columns dropdown.
"""
url = reverse('recipes', args=(self.build.id,))
self.get(url)
self.wait_until_visible('#edit-columns-button')
# check that options for the non-hideable columns are disabled
non_hideable = ['name', 'version']
for column in non_hideable:
selector = 'input#checkbox-%s[disabled="disabled"]' % column
self.wait_until_present(selector)

View File

@@ -0,0 +1,53 @@
#! /usr/bin/env python3
#
# BitBake Toaster Implementation
#
# Copyright (C) 2013-2016 Intel Corporation
#
# SPDX-License-Identifier: GPL-2.0-only
#
from django.urls import reverse
from django.utils import timezone
from tests.browser.selenium_helpers import SeleniumTestCase
from orm.models import Project, Build, Recipe, Task, Layer, Layer_Version
from orm.models import Target
class TestBuilddashboardPageTasks(SeleniumTestCase):
""" Test build dashboard tasks sub-page """
def setUp(self):
project = Project.objects.get_or_create_default_project()
now = timezone.now()
self.build = Build.objects.create(project=project,
started_on=now,
completed_on=now)
layer = Layer.objects.create()
layer_version = Layer_Version.objects.create(layer=layer)
recipe = Recipe.objects.create(layer_version=layer_version)
task = Task.objects.create(build=self.build, recipe=recipe, order=1)
Target.objects.create(build=self.build, task=task, target='do_build')
def test_build_tasks_columns(self):
"""
Check that non-hideable columns of the table on the tasks sub-page
are disabled on the edit columns dropdown.
"""
url = reverse('tasks', args=(self.build.id,))
self.get(url)
self.wait_until_visible('#edit-columns-button')
# check that options for the non-hideable columns are disabled
non_hideable = ['order', 'task_name', 'recipe__name']
for column in non_hideable:
selector = 'input#checkbox-%s[disabled="disabled"]' % column
self.wait_until_present(selector)

View File

@@ -0,0 +1,103 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# BitBake Toaster UI tests implementation
#
# Copyright (C) 2023 Savoir-faire Linux Inc
#
# SPDX-License-Identifier: GPL-2.0-only
import pytest
from django.urls import reverse
from selenium.webdriver.support.ui import Select
from tests.browser.selenium_helpers import SeleniumTestCase
from orm.models import BitbakeVersion, Project, Release
from selenium.webdriver.common.by import By
class TestDeleteProject(SeleniumTestCase):
def setUp(self):
bitbake, _ = BitbakeVersion.objects.get_or_create(
name="master",
giturl="git://master",
branch="master",
dirpath="master")
self.release, _ = Release.objects.get_or_create(
name="master",
description="Yocto Project master",
branch_name="master",
helptext="latest",
bitbake_version=bitbake)
Release.objects.get_or_create(
name="foo",
description="Yocto Project foo",
branch_name="foo",
helptext="latest",
bitbake_version=bitbake)
@pytest.mark.django_db
def test_delete_project(self):
""" Test delete a project
- Check delete modal is visible
- Check delete modal has right text
- Confirm delete
- Check project is deleted
"""
project_name = "project_to_delete"
url = reverse('newproject')
self.get(url)
self.enter_text('#new-project-name', project_name)
select = Select(self.find('#projectversion'))
select.select_by_value(str(self.release.pk))
self.click("#create-project-button")
# We should get redirected to the new project's page with the
# notification at the top
element = self.wait_until_visible('#project-created-notification')
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")
# Delete project
delete_project_link = self.driver.find_element(
By.XPATH, '//a[@href="#delete-project-modal"]')
delete_project_link.click()
# Check delete modal is visible
self.wait_until_visible('#delete-project-modal')
# Check delete modal has right text
modal_header_text = self.find('#delete-project-modal .modal-header').text
self.assertTrue(
"Are you sure you want to delete this project?" in modal_header_text,
"Delete project modal header text is wrong")
modal_body_text = self.find('#delete-project-modal .modal-body').text
self.assertTrue(
"Cancel its builds currently in progress" in modal_body_text,
"Modal body doesn't contain: Cancel its builds currently in progress")
self.assertTrue(
"Remove its configuration information" in modal_body_text,
"Modal body doesn't contain: Remove its configuration information")
self.assertTrue(
"Remove its imported layers" in modal_body_text,
"Modal body doesn't contain: Remove its imported layers")
self.assertTrue(
"Remove its custom images" in modal_body_text,
"Modal body doesn't contain: Remove its custom images")
self.assertTrue(
"Remove all its build information" in modal_body_text,
"Modal body doesn't contain: Remove all its build information")
# Confirm delete
delete_btn = self.find('#delete-project-confirmed')
delete_btn.click()
# Check project is deleted
self.wait_until_visible('#change-notification')
delete_notification = self.find('#change-notification-msg')
self.assertTrue("You have deleted 1 project:" in delete_notification.text)
self.assertTrue(project_name in delete_notification.text)
self.assertFalse(Project.objects.filter(name=project_name).exists(),
"Project not deleted from database")

View File

@@ -0,0 +1,45 @@
#! /usr/bin/env python3
#
# BitBake Toaster Implementation
#
# Copyright (C) 2013-2016 Intel Corporation
#
# SPDX-License-Identifier: GPL-2.0-only
#
"""
Run the js unit tests
"""
from django.urls import reverse
from tests.browser.selenium_helpers import SeleniumTestCase
import logging
logger = logging.getLogger("toaster")
class TestJsUnitTests(SeleniumTestCase):
""" Test landing page shows the Toaster brand """
fixtures = ['toastergui-unittest-data']
def test_that_js_unit_tests_pass(self):
url = reverse('js-unit-tests')
self.get(url)
self.wait_until_present('#qunit-testresult .failed')
failed = self.find("#qunit-testresult .failed").text
passed = self.find("#qunit-testresult .passed").text
total = self.find("#qunit-testresult .total").text
logger.info("Js unit tests completed %s out of %s passed, %s failed",
passed,
total,
failed)
failed_tests = self.find_all("li .fail .test-message")
for fail in failed_tests:
logger.error("JS unit test failed: %s" % fail.text)
self.assertEqual(failed, '0',
"%s JS unit tests failed" % failed)

View File

@@ -0,0 +1,221 @@
#! /usr/bin/env python3
#
# BitBake Toaster Implementation
#
# SPDX-License-Identifier: GPL-2.0-only
#
# Copyright (C) 2013-2016 Intel Corporation
#
from django.urls import reverse
from django.utils import timezone
from tests.browser.selenium_helpers import SeleniumTestCase
from selenium.webdriver.common.by import By
from orm.models import Layer, Layer_Version, Project, Build
class TestLandingPage(SeleniumTestCase):
""" Tests for redirects on the landing page """
PROJECT_NAME = 'test project'
LANDING_PAGE_TITLE = 'This is Toaster'
CLI_BUILDS_PROJECT_NAME = 'command line builds'
def setUp(self):
""" Add default project manually """
self.project = Project.objects.create_project(
self.CLI_BUILDS_PROJECT_NAME,
None
)
self.project.is_default = True
self.project.save()
def test_icon_info_visible_and_clickable(self):
""" Test that the information icon is visible and clickable """
self.get(reverse('landing'))
info_sign = self.find('#toaster-version-info-sign')
# check that the info sign is visible
self.assertTrue(info_sign.is_displayed())
# check that the info sign is clickable
# and info modal is appearing when clicking on the info sign
info_sign.click() # click on the info sign make attribute 'aria-describedby' visible
info_model_id = info_sign.get_attribute('aria-describedby')
info_modal = self.find(f'#{info_model_id}')
self.assertTrue(info_modal.is_displayed())
self.assertTrue("Toaster version information" in info_modal.text)
def test_documentation_link_displayed(self):
""" Test that the documentation link is displayed """
self.get(reverse('landing'))
documentation_link = self.find('#navbar-docs > a')
# check that the documentation link is visible
self.assertTrue(documentation_link.is_displayed())
# check browser open new tab toaster manual when clicking on the documentation link
self.assertEqual(documentation_link.get_attribute('target'), '_blank')
self.assertEqual(
documentation_link.get_attribute('href'),
'http://docs.yoctoproject.org/toaster-manual/index.html#toaster-user-manual')
self.assertTrue("Documentation" in documentation_link.text)
def test_openembedded_jumbotron_link_visible_and_clickable(self):
""" Test OpenEmbedded link jumbotron is visible and clickable: """
self.get(reverse('landing'))
jumbotron = self.find('.jumbotron')
# check OpenEmbedded
openembedded = jumbotron.find_element(By.LINK_TEXT, 'OpenEmbedded')
self.assertTrue(openembedded.is_displayed())
openembedded.click()
self.assertTrue("openembedded.org" in self.driver.current_url)
def test_bitbake_jumbotron_link_visible_and_clickable(self):
""" Test BitBake link jumbotron is visible and clickable: """
self.get(reverse('landing'))
jumbotron = self.find('.jumbotron')
# check BitBake
bitbake = jumbotron.find_element(By.LINK_TEXT, 'BitBake')
self.assertTrue(bitbake.is_displayed())
bitbake.click()
self.assertTrue(
"docs.yoctoproject.org/bitbake.html" in self.driver.current_url)
def test_yoctoproject_jumbotron_link_visible_and_clickable(self):
""" Test Yocto Project link jumbotron is visible and clickable: """
self.get(reverse('landing'))
jumbotron = self.find('.jumbotron')
# check Yocto Project
yoctoproject = jumbotron.find_element(By.LINK_TEXT, 'Yocto Project')
self.assertTrue(yoctoproject.is_displayed())
yoctoproject.click()
self.assertTrue("yoctoproject.org" in self.driver.current_url)
def test_link_setup_using_toaster_visible_and_clickable(self):
""" Test big magenta button setting up and using toaster link in jumbotron
if visible and clickable
"""
self.get(reverse('landing'))
jumbotron = self.find('.jumbotron')
# check Big magenta button
big_magenta_button = jumbotron.find_element(By.LINK_TEXT,
'Toaster is ready to capture your command line builds'
)
self.assertTrue(big_magenta_button.is_displayed())
big_magenta_button.click()
self.assertTrue(
"docs.yoctoproject.org/toaster-manual/setup-and-use.html#setting-up-and-using-toaster" in self.driver.current_url)
def test_link_create_new_project_in_jumbotron_visible_and_clickable(self):
""" Test big blue button create new project jumbotron if visible and clickable """
# Create a layer and a layer version to make visible the big blue button
layer = Layer.objects.create(name='bar')
Layer_Version.objects.create(layer=layer)
self.get(reverse('landing'))
jumbotron = self.find('.jumbotron')
# check Big Blue button
big_blue_button = jumbotron.find_element(By.LINK_TEXT,
'Create your first Toaster project to run manage builds'
)
self.assertTrue(big_blue_button.is_displayed())
big_blue_button.click()
self.assertTrue("toastergui/newproject/" in self.driver.current_url)
def test_toaster_manual_link_visible_and_clickable(self):
""" Test Read the Toaster manual link jumbotron is visible and clickable: """
self.get(reverse('landing'))
jumbotron = self.find('.jumbotron')
# check Read the Toaster manual
toaster_manual = jumbotron.find_element(
By.LINK_TEXT, 'Read the Toaster manual')
self.assertTrue(toaster_manual.is_displayed())
toaster_manual.click()
self.assertTrue(
"https://docs.yoctoproject.org/toaster-manual/index.html#toaster-user-manual" in self.driver.current_url)
def test_contrib_to_toaster_link_visible_and_clickable(self):
""" Test Contribute to Toaster link jumbotron is visible and clickable: """
self.get(reverse('landing'))
jumbotron = self.find('.jumbotron')
# check Contribute to Toaster
contribute_to_toaster = jumbotron.find_element(
By.LINK_TEXT, 'Contribute to Toaster')
self.assertTrue(contribute_to_toaster.is_displayed())
contribute_to_toaster.click()
self.assertTrue(
"wiki.yoctoproject.org/wiki/contribute_to_toaster" in str(self.driver.current_url).lower())
def test_only_default_project(self):
"""
No projects except default
=> should see the landing page
"""
self.get(reverse('landing'))
self.assertTrue(self.LANDING_PAGE_TITLE in self.get_page_source())
def test_default_project_has_build(self):
"""
Default project has a build, no other projects
=> should see the builds page
"""
now = timezone.now()
build = Build.objects.create(project=self.project,
started_on=now,
completed_on=now)
build.save()
self.get(reverse('landing'))
elements = self.find_all('#allbuildstable')
self.assertEqual(len(elements), 1, 'should redirect to builds')
content = self.get_page_source()
self.assertFalse(self.PROJECT_NAME in content,
'should not show builds for project %s' % self.PROJECT_NAME)
self.assertTrue(self.CLI_BUILDS_PROJECT_NAME in content,
'should show builds for cli project')
def test_user_project_exists(self):
"""
User has added a project (without builds)
=> should see the projects page
"""
user_project = Project.objects.create_project('foo', None)
user_project.save()
self.get(reverse('landing'))
elements = self.find_all('#projectstable')
self.assertEqual(len(elements), 1, 'should redirect to projects')
def test_user_project_has_build(self):
"""
User has added a project (with builds), command line builds doesn't
=> should see the builds page
"""
user_project = Project.objects.create_project(self.PROJECT_NAME, None)
user_project.save()
now = timezone.now()
build = Build.objects.create(project=user_project,
started_on=now,
completed_on=now)
build.save()
self.get(reverse('landing'))
self.wait_until_visible("#latest-builds", poll=3)
elements = self.find_all('#allbuildstable')
self.assertEqual(len(elements), 1, 'should redirect to builds')
content = self.get_page_source()
self.assertTrue(self.PROJECT_NAME in content,
'should show builds for project %s' % self.PROJECT_NAME)

View File

@@ -0,0 +1,237 @@
#! /usr/bin/env python3
#
# BitBake Toaster Implementation
#
# SPDX-License-Identifier: GPL-2.0-only
#
# Copyright (C) 2013-2016 Intel Corporation
#
from django.urls import reverse
from selenium.common.exceptions import ElementClickInterceptedException, TimeoutException
from tests.browser.selenium_helpers import SeleniumTestCase
from orm.models import Layer, Layer_Version, Project, LayerSource, Release
from orm.models import BitbakeVersion
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.common.by import By
class TestLayerDetailsPage(SeleniumTestCase):
""" Test layerdetails page works correctly """
def __init__(self, *args, **kwargs):
super(TestLayerDetailsPage, self).__init__(*args, **kwargs)
self.initial_values = None
self.url = None
self.imported_layer_version = None
def setUp(self):
release = Release.objects.create(
name='baz',
bitbake_version=BitbakeVersion.objects.create(name='v1')
)
# project to add new custom images to
self.project = Project.objects.create(name='foo', release=release)
name = "meta-imported"
vcs_url = "git://example.com/meta-imported"
subdir = "/layer"
gitrev = "d33d"
summary = "A imported layer"
description = "This was imported"
imported_layer = Layer.objects.create(name=name,
vcs_url=vcs_url,
summary=summary,
description=description)
self.imported_layer_version = Layer_Version.objects.create(
layer=imported_layer,
layer_source=LayerSource.TYPE_IMPORTED,
branch=gitrev,
commit=gitrev,
dirpath=subdir,
project=self.project)
self.initial_values = [name, vcs_url, subdir, gitrev, summary,
description]
self.url = reverse('layerdetails',
args=(self.project.pk,
self.imported_layer_version.pk))
def _edit_layerdetails(self):
""" Edit all the editable fields for the layer refresh the page and
check that the new values exist"""
self.get(self.url)
self.wait_until_visible("#add-remove-layer-btn")
self.click("#add-remove-layer-btn")
self.click("#edit-layer-source")
self.click("#repo")
self.wait_until_visible("#layer-git-repo-url")
# Open every edit box
for btn in self.find_all("dd .glyphicon-edit"):
btn.click()
# Wait for the inputs to become visible after animation
self.wait_until_visible("#layer-git input[type=text]")
self.wait_until_visible("dd textarea")
self.wait_until_visible("dd .change-btn")
# Edit each value
for inputs in self.find_all("#layer-git input[type=text]") + \
self.find_all("dd textarea"):
# ignore the tt inputs (twitter typeahead input)
if "tt-" in inputs.get_attribute("class"):
continue
value = inputs.get_attribute("value")
self.assertTrue(value in self.initial_values,
"Expecting any of \"%s\"but got \"%s\"" %
(self.initial_values, value))
# Make sure the input visible beofre sending keys
self.wait_until_visible("#layer-git input[type=text]")
inputs.send_keys("-edited")
# Save the new values
for save_btn in self.find_all(".change-btn"):
save_btn.click()
try:
self.wait_until_visible("#save-changes-for-switch", poll=3)
btn_save_chg_for_switch = self.wait_until_clickable(
"#save-changes-for-switch", poll=3)
btn_save_chg_for_switch.click()
except ElementClickInterceptedException:
self.skipTest(
"save-changes-for-switch click intercepted. Element not visible or maybe covered by another element.")
except TimeoutException:
self.skipTest(
"save-changes-for-switch is not clickable within the specified timeout.")
self.wait_until_visible("#edit-layer-source")
# Refresh the page to see if the new values are returned
self.get(self.url)
new_values = ["%s-edited" % old_val
for old_val in self.initial_values]
for inputs in self.find_all('#layer-git input[type="text"]') + \
self.find_all('dd textarea'):
# ignore the tt inputs (twitter typeahead input)
if "tt-" in inputs.get_attribute("class"):
continue
value = inputs.get_attribute("value")
self.assertTrue(value in new_values,
"Expecting any of \"%s\" but got \"%s\"" %
(new_values, value))
# Now convert it to a local layer
self.click("#edit-layer-source")
self.click("#dir")
dir_input = self.wait_until_visible("#layer-dir-path-in-details")
new_dir = "/home/test/my-meta-dir"
dir_input.send_keys(new_dir)
try:
self.wait_until_visible("#save-changes-for-switch", poll=3)
btn_save_chg_for_switch = self.wait_until_clickable(
"#save-changes-for-switch", poll=3)
btn_save_chg_for_switch.click()
except ElementClickInterceptedException:
self.skipTest(
"save-changes-for-switch click intercepted. Element not properly visible or maybe behind another element.")
except TimeoutException:
self.skipTest(
"save-changes-for-switch is not clickable within the specified timeout.")
self.wait_until_visible("#edit-layer-source")
# Refresh the page to see if the new values are returned
self.get(self.url)
dir_input = self.find("#layer-dir-path-in-details")
self.assertTrue(new_dir in dir_input.get_attribute("value"),
"Expected %s in the dir value for layer directory" %
new_dir)
def test_edit_layerdetails_page(self):
try:
self._edit_layerdetails()
except ElementClickInterceptedException:
self.skipTest(
"ElementClickInterceptedException occured. Element not visible or maybe covered by another element.")
def test_delete_layer(self):
""" Delete the layer """
self.get(self.url)
# Wait for the tables to load to avoid a race condition where the
# toaster tables have made an async request. If the layer is deleted
# before the request finishes it will cause an exception and fail this
# test.
wait = WebDriverWait(self.driver, 30)
wait.until(EC.text_to_be_present_in_element(
(By.CLASS_NAME,
"table-count-recipestable"), "0"))
wait.until(EC.text_to_be_present_in_element(
(By.CLASS_NAME,
"table-count-machinestable"), "0"))
self.click('a[data-target="#delete-layer-modal"]')
self.wait_until_visible("#delete-layer-modal")
self.click("#layer-delete-confirmed")
notification = self.wait_until_visible("#change-notification-msg")
expected_text = "You have deleted 1 layer from your project: %s" % \
self.imported_layer_version.layer.name
self.assertTrue(expected_text in notification.text,
"Expected notification text \"%s\" not found instead"
"it was \"%s\"" %
(expected_text, notification.text))
def test_addrm_to_project(self):
self.get(self.url)
# Add the layer
self.click("#add-remove-layer-btn")
notification = self.wait_until_visible("#change-notification-msg")
expected_text = "You have added 1 layer to your project: %s" % \
self.imported_layer_version.layer.name
self.assertTrue(expected_text in notification.text,
"Expected notification text %s not found was "
" \"%s\" instead" %
(expected_text, notification.text))
# Remove the layer
self.click("#add-remove-layer-btn")
notification = self.wait_until_visible("#change-notification-msg")
expected_text = "You have removed 1 layer from your project: %s" % \
self.imported_layer_version.layer.name
self.assertTrue(expected_text in notification.text,
"Expected notification text %s not found was "
" \"%s\" instead" %
(expected_text, notification.text))

View File

@@ -0,0 +1,201 @@
#! /usr/bin/env python3
#
# BitBake Toaster Implementation
#
# SPDX-License-Identifier: GPL-2.0-only
#
# Copyright (C) 2013-2016 Intel Corporation
#
from django.urls import reverse
from django.utils import timezone
from tests.browser.selenium_helpers import SeleniumTestCase
from tests.browser.selenium_helpers_base import Wait
from orm.models import Project, Build, Task, Recipe, Layer, Layer_Version
from bldcontrol.models import BuildRequest
from selenium.webdriver.common.by import By
class TestMostRecentBuildsStates(SeleniumTestCase):
""" Test states update correctly in most recent builds area """
def _create_build_request(self):
project = Project.objects.get_or_create_default_project()
now = timezone.now()
build = Build.objects.create(project=project, build_name='fakebuild',
started_on=now, completed_on=now)
return BuildRequest.objects.create(build=build, project=project,
state=BuildRequest.REQ_QUEUED)
def _create_recipe(self):
""" Add a recipe to the database and return it """
layer = Layer.objects.create()
layer_version = Layer_Version.objects.create(layer=layer)
return Recipe.objects.create(name='foo', layer_version=layer_version)
def _check_build_states(self, build_request):
recipes_to_parse = 10
url = reverse('all-builds')
self.get(url)
build = build_request.build
base_selector = '[data-latest-build-result="%s"] ' % build.id
# build queued; check shown as queued
selector = base_selector + '[data-build-state="Queued"]'
element = self.wait_until_visible(selector)
self.assertRegex(element.get_attribute('innerHTML'),
'Build queued', 'build should show queued status')
# waiting for recipes to be parsed
build.outcome = Build.IN_PROGRESS
build.recipes_to_parse = recipes_to_parse
build.recipes_parsed = 0
build.save()
build_request.state = BuildRequest.REQ_INPROGRESS
build_request.save()
self.get(url)
selector = base_selector + '[data-build-state="Parsing"]'
element = self.wait_until_visible(selector)
bar_selector = '#recipes-parsed-percentage-bar-%s' % build.id
bar_element = element.find_element(By.CSS_SELECTOR, bar_selector)
self.assertEqual(bar_element.value_of_css_property('width'), '0px',
'recipe parse progress should be at 0')
# recipes being parsed; check parse progress
build.recipes_parsed = 5
build.save()
self.get(url)
element = self.wait_until_visible(selector)
bar_element = element.find_element(By.CSS_SELECTOR, bar_selector)
recipe_bar_updated = lambda driver: \
bar_element.get_attribute('style') == 'width: 50%;'
msg = 'recipe parse progress bar should update to 50%'
element = Wait(self.driver).until(recipe_bar_updated, msg)
# all recipes parsed, task started, waiting for first task to finish;
# check status is shown as "Tasks starting..."
build.recipes_parsed = recipes_to_parse
build.save()
recipe = self._create_recipe()
task1 = Task.objects.create(build=build, recipe=recipe,
task_name='Lionel')
task2 = Task.objects.create(build=build, recipe=recipe,
task_name='Jeffries')
self.get(url)
selector = base_selector + '[data-build-state="Starting"]'
element = self.wait_until_visible(selector)
self.assertRegex(element.get_attribute('innerHTML'),
'Tasks starting', 'build should show "tasks starting" status')
# first task finished; check tasks progress bar
task1.outcome = Task.OUTCOME_SUCCESS
task1.save()
self.get(url)
selector = base_selector + '[data-build-state="In Progress"]'
element = self.wait_until_visible(selector)
bar_selector = '#build-pc-done-bar-%s' % build.id
bar_element = element.find_element(By.CSS_SELECTOR, bar_selector)
task_bar_updated = lambda driver: \
bar_element.get_attribute('style') == 'width: 50%;'
msg = 'tasks progress bar should update to 50%'
element = Wait(self.driver).until(task_bar_updated, msg)
# last task finished; check tasks progress bar updates
task2.outcome = Task.OUTCOME_SUCCESS
task2.save()
self.get(url)
element = self.wait_until_visible(selector)
bar_element = element.find_element(By.CSS_SELECTOR, bar_selector)
task_bar_updated = lambda driver: \
bar_element.get_attribute('style') == 'width: 100%;'
msg = 'tasks progress bar should update to 100%'
element = Wait(self.driver).until(task_bar_updated, msg)
def test_states_to_success(self):
"""
Test state transitions in the recent builds area for a build which
completes successfully.
"""
build_request = self._create_build_request()
self._check_build_states(build_request)
# all tasks complete and build succeeded; check success state shown
build = build_request.build
build.outcome = Build.SUCCEEDED
build.save()
selector = '[data-latest-build-result="%s"] ' \
'[data-build-state="Succeeded"]' % build.id
element = self.wait_until_visible(selector)
def test_states_to_failure(self):
"""
Test state transitions in the recent builds area for a build which
completes in a failure.
"""
build_request = self._create_build_request()
self._check_build_states(build_request)
# all tasks complete and build succeeded; check fail state shown
build = build_request.build
build.outcome = Build.FAILED
build.save()
selector = '[data-latest-build-result="%s"] ' \
'[data-build-state="Failed"]' % build.id
element = self.wait_until_visible(selector)
def test_states_cancelling(self):
"""
Test that most recent build area updates correctly for a build
which is cancelled.
"""
url = reverse('all-builds')
build_request = self._create_build_request()
build = build_request.build
# cancel the build
build_request.state = BuildRequest.REQ_CANCELLING
build_request.save()
self.get(url)
# check cancelling state
selector = '[data-latest-build-result="%s"] ' \
'[data-build-state="Cancelling"]' % build.id
element = self.wait_until_visible(selector)
self.assertRegex(element.get_attribute('innerHTML'),
'Cancelling the build', 'build should show "cancelling" status')
# check cancelled state
build.outcome = Build.CANCELLED
build.save()
self.get(url)
selector = '[data-latest-build-result="%s"] ' \
'[data-build-state="Cancelled"]' % build.id
element = self.wait_until_visible(selector)
self.assertRegex(element.get_attribute('innerHTML'),
'Build cancelled', 'build should show "cancelled" status')

View File

@@ -0,0 +1,159 @@
#! /usr/bin/env python3
#
# BitBake Toaster Implementation
#
# Copyright (C) 2013-2016 Intel Corporation
#
# SPDX-License-Identifier: GPL-2.0-only
#
from bldcontrol.models import BuildEnvironment
from django.urls import reverse
from tests.browser.selenium_helpers import SeleniumTestCase
from orm.models import BitbakeVersion, Release, Project, ProjectLayer, Layer
from orm.models import Layer_Version, Recipe, CustomImageRecipe
class TestNewCustomImagePage(SeleniumTestCase):
CUSTOM_IMAGE_NAME = 'roopa-doopa'
def setUp(self):
BuildEnvironment.objects.get_or_create(
betype=BuildEnvironment.TYPE_LOCAL,
)
release = Release.objects.create(
name='baz',
bitbake_version=BitbakeVersion.objects.create(name='v1')
)
# project to add new custom images to
self.project = Project.objects.create(name='foo', release=release)
# layer associated with the project
layer = Layer.objects.create(name='bar')
layer_version = Layer_Version.objects.create(
layer=layer,
project=self.project
)
# properly add the layer to the project
ProjectLayer.objects.create(
project=self.project,
layercommit=layer_version,
optional=False
)
# add a fake image recipe to the layer that can be customised
builldir = os.environ.get('BUILDDIR', './')
self.recipe = Recipe.objects.create(
name='core-image-minimal',
layer_version=layer_version,
file_path=f'{builldir}/core-image-minimal.bb',
is_image=True
)
# create a tmp file for the recipe
with open(self.recipe.file_path, 'w') as f:
f.write('foo')
# another project with a custom image already in it
project2 = Project.objects.create(name='whoop', release=release)
layer_version2 = Layer_Version.objects.create(
layer=layer,
project=project2
)
ProjectLayer.objects.create(
project=project2,
layercommit=layer_version2,
optional=False
)
recipe2 = Recipe.objects.create(
name='core-image-minimal',
layer_version=layer_version2,
is_image=True
)
CustomImageRecipe.objects.create(
name=self.CUSTOM_IMAGE_NAME,
base_recipe=recipe2,
layer_version=layer_version2,
file_path='/1/2',
project=project2
)
def _create_custom_image(self, new_custom_image_name):
"""
1. Go to the 'new custom image' page
2. Click the button for the fake core-image-minimal
3. Wait for the dialog box for setting the name of the new custom
image
4. Insert new_custom_image_name into that dialog's text box
"""
url = reverse('newcustomimage', args=(self.project.id,))
self.get(url)
self.wait_until_visible('#global-nav', poll=3)
self.click('button[data-recipe="%s"]' % self.recipe.id)
selector = '#new-custom-image-modal input[type="text"]'
self.enter_text(selector, new_custom_image_name)
self.click('#create-new-custom-image-btn')
def _check_for_custom_image(self, image_name):
"""
Fetch the list of custom images for the project and check the
image with name image_name is listed there
"""
url = reverse('projectcustomimages', args=(self.project.id,))
self.get(url)
self.wait_until_visible('#customimagestable')
element = self.find('#customimagestable td[class="name"] a')
msg = 'should be a custom image link with text %s' % image_name
self.assertEqual(element.text.strip(), image_name, msg)
def test_new_image(self):
"""
Should be able to create a new custom image
"""
custom_image_name = 'boo-image'
self._create_custom_image(custom_image_name)
self.wait_until_visible('#image-created-notification')
self._check_for_custom_image(custom_image_name)
def test_new_duplicates_other_project_image(self):
"""
Should be able to create a new custom image if its name is the same
as a custom image in another project
"""
self._create_custom_image(self.CUSTOM_IMAGE_NAME)
self.wait_until_visible('#image-created-notification')
self._check_for_custom_image(self.CUSTOM_IMAGE_NAME)
def test_new_duplicates_non_image_recipe(self):
"""
Should not be able to create a new custom image whose name is the
same as an existing non-image recipe
"""
self._create_custom_image(self.recipe.name)
element = self.wait_until_visible('#invalid-name-help')
self.assertRegex(element.text.strip(),
'image with this name already exists')
def test_new_duplicates_project_image(self):
"""
Should not be able to create a new custom image whose name is the same
as a custom image in this project
"""
# create the image
custom_image_name = 'doh-image'
self._create_custom_image(custom_image_name)
self.wait_until_visible('#image-created-notification')
self._check_for_custom_image(custom_image_name)
# try to create an image with the same name
self._create_custom_image(custom_image_name)
element = self.wait_until_visible('#invalid-name-help')
expected = 'An image with this name already exists in this project'
self.assertRegex(element.text.strip(), expected)

View File

@@ -0,0 +1,109 @@
#! /usr/bin/env python3
#
# BitBake Toaster Implementation
#
# Copyright (C) 2013-2016 Intel Corporation
#
# SPDX-License-Identifier: GPL-2.0-only
#
from django.urls import reverse
from tests.browser.selenium_helpers import SeleniumTestCase
from selenium.webdriver.support.ui import Select
from selenium.common.exceptions import InvalidElementStateException
from selenium.webdriver.common.by import By
from orm.models import Project, Release, BitbakeVersion
class TestNewProjectPage(SeleniumTestCase):
""" Test project data at /project/X/ is displayed correctly """
def setUp(self):
bitbake, c = BitbakeVersion.objects.get_or_create(
name="master",
giturl="git://master",
branch="master",
dirpath="master")
release, c = Release.objects.get_or_create(name="msater",
description="master"
"release",
branch_name="master",
helptext="latest",
bitbake_version=bitbake)
self.release, c = Release.objects.get_or_create(
name="msater2",
description="master2"
"release2",
branch_name="master2",
helptext="latest2",
bitbake_version=bitbake)
def test_create_new_project(self):
""" Test creating a project """
project_name = "masterproject"
url = reverse('newproject')
self.get(url)
self.wait_until_visible('#new-project-name', poll=3)
self.enter_text('#new-project-name', project_name)
select = Select(self.find('#projectversion'))
select.select_by_value(str(self.release.pk))
self.click("#create-project-button")
# We should get redirected to the new project's page with the
# notification at the top
element = self.wait_until_visible(
'#project-created-notification', poll=3)
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")
def test_new_duplicates_project_name(self):
"""
Should not be able to create a new project whose name is the same
as an existing project
"""
project_name = "dupproject"
Project.objects.create_project(name=project_name,
release=self.release)
url = reverse('newproject')
self.get(url)
self.wait_until_visible('#new-project-name', poll=3)
self.enter_text('#new-project-name', project_name)
select = Select(self.find('#projectversion'))
select.select_by_value(str(self.release.pk))
radio = self.driver.find_element(By.ID, 'type-new')
radio.click()
self.click("#create-project-button")
self.wait_until_present('#hint-error-project-name', poll=3)
element = self.find('#hint-error-project-name')
self.assertTrue(("Project names must be unique" in element.text),
"Did not find unique project name error message")
# Try and click it anyway, if it submits we'll have a new project in
# the db and assert then
try:
self.click("#create-project-button")
except InvalidElementStateException:
pass
self.assertTrue(
(Project.objects.filter(name=project_name).count() == 1),
"New project not found in database")

View File

@@ -0,0 +1,158 @@
#! /usr/bin/env python3
#
# BitBake Toaster Implementation
#
# Copyright (C) 2013-2016 Intel Corporation
#
# SPDX-License-Identifier: GPL-2.0-only
#
import os
import re
from django.urls import reverse
from django.utils import timezone
from tests.browser.selenium_helpers import SeleniumTestCase
from orm.models import BitbakeVersion, Release, Project, Build, Target
class TestProjectBuildsPage(SeleniumTestCase):
""" Test data at /project/X/builds is displayed correctly """
PROJECT_NAME = 'test project'
CLI_BUILDS_PROJECT_NAME = 'command line builds'
def setUp(self):
builldir = os.environ.get('BUILDDIR', './')
bbv = BitbakeVersion.objects.create(name='bbv1', giturl=f'{builldir}/',
branch='master', dirpath='')
release = Release.objects.create(name='release1',
bitbake_version=bbv)
self.project1 = Project.objects.create_project(name=self.PROJECT_NAME,
release=release)
self.project1.save()
self.project2 = Project.objects.create_project(name=self.PROJECT_NAME,
release=release)
self.project2.save()
self.default_project = Project.objects.create_project(
name=self.CLI_BUILDS_PROJECT_NAME,
release=release
)
self.default_project.is_default = True
self.default_project.save()
# parameters for builds to associate with the projects
now = timezone.now()
self.project1_build_success = {
'project': self.project1,
'started_on': now,
'completed_on': now,
'outcome': Build.SUCCEEDED
}
self.project1_build_in_progress = {
'project': self.project1,
'started_on': now,
'completed_on': now,
'outcome': Build.IN_PROGRESS
}
self.project2_build_success = {
'project': self.project2,
'started_on': now,
'completed_on': now,
'outcome': Build.SUCCEEDED
}
self.project2_build_in_progress = {
'project': self.project2,
'started_on': now,
'completed_on': now,
'outcome': Build.IN_PROGRESS
}
def _get_rows_for_project(self, project_id):
"""
Helper to retrieve HTML rows for a project's builds,
as shown in the main table of the page
"""
url = reverse('projectbuilds', args=(project_id,))
self.get(url)
self.wait_until_present('#projectbuildstable tbody tr')
return self.find_all('#projectbuildstable tbody tr')
def test_show_builds_for_project(self):
""" Builds for a project should be displayed in the main table """
Build.objects.create(**self.project1_build_success)
Build.objects.create(**self.project1_build_success)
build_rows = self._get_rows_for_project(self.project1.id)
self.assertEqual(len(build_rows), 2)
def test_show_builds_project_only(self):
""" Builds for other projects should be excluded """
Build.objects.create(**self.project1_build_success)
Build.objects.create(**self.project1_build_success)
Build.objects.create(**self.project1_build_success)
# shouldn't see these two
Build.objects.create(**self.project2_build_success)
Build.objects.create(**self.project2_build_in_progress)
build_rows = self._get_rows_for_project(self.project1.id)
self.assertEqual(len(build_rows), 3)
def test_builds_exclude_in_progress(self):
""" "in progress" builds should not be shown in main table """
Build.objects.create(**self.project1_build_success)
Build.objects.create(**self.project1_build_success)
# shouldn't see this one
Build.objects.create(**self.project1_build_in_progress)
# shouldn't see these two either, as they belong to a different project
Build.objects.create(**self.project2_build_success)
Build.objects.create(**self.project2_build_in_progress)
build_rows = self._get_rows_for_project(self.project1.id)
self.assertEqual(len(build_rows), 2)
def test_show_tasks_with_suffix(self):
""" Task should be shown as suffixes on build names """
build = Build.objects.create(**self.project1_build_success)
target = 'bash'
task = 'clean'
Target.objects.create(build=build, target=target, task=task)
url = reverse('projectbuilds', args=(self.project1.id,))
self.get(url)
self.wait_until_present('td[class="target"]')
cell = self.find('td[class="target"]')
content = cell.get_attribute('innerHTML')
expected_text = '%s:%s' % (target, task)
self.assertTrue(re.search(expected_text, content),
'"target" cell should contain text %s' % expected_text)
def test_cli_builds_hides_tabs(self):
"""
Display for command line builds should hide tabs
"""
url = reverse('projectbuilds', args=(self.default_project.id,))
self.get(url)
tabs = self.find_all('#project-topbar')
self.assertEqual(len(tabs), 0,
'should be no top bar shown for command line builds')
def test_non_cli_builds_has_tabs(self):
"""
Non-command-line builds projects should show the tabs
"""
url = reverse('projectbuilds', args=(self.project1.id,))
self.get(url)
tabs = self.find_all('#project-topbar')
self.assertEqual(len(tabs), 1,
'should be a top bar shown for non-command-line builds')

View File

@@ -0,0 +1,220 @@
#! /usr/bin/env python3
#
# BitBake Toaster Implementation
#
# Copyright (C) 2013-2016 Intel Corporation
#
# SPDX-License-Identifier: GPL-2.0-only
#
import os
from django.urls import reverse
from tests.browser.selenium_helpers import SeleniumTestCase
from orm.models import BitbakeVersion, Release, Project, ProjectVariable
from selenium.webdriver.common.by import By
class TestProjectConfigsPage(SeleniumTestCase):
""" Test data at /project/X/builds is displayed correctly """
PROJECT_NAME = 'test project'
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 setUp(self):
builldir = os.environ.get('BUILDDIR', './')
bbv = BitbakeVersion.objects.create(name='bbv1', giturl=f'{builldir}/',
branch='master', dirpath='')
release = Release.objects.create(name='release1',
bitbake_version=bbv)
self.project1 = Project.objects.create_project(name=self.PROJECT_NAME,
release=release)
self.project1.save()
def test_no_underscore_iamgefs_type(self):
"""
Should not accept IMAGEFS_TYPE with an underscore
"""
imagefs_type = "foo_bar"
ProjectVariable.objects.get_or_create(project = self.project1, name = "IMAGE_FSTYPES", value = "abcd ")
url = reverse('projectconf', args=(self.project1.id,));
self.get(url);
self.click('#change-image_fstypes-icon')
self.enter_text('#new-imagefs_types', imagefs_type)
element = self.wait_until_visible('#hintError-image-fs_type')
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.
"""
imagefs_type = "btrfs"
ProjectVariable.objects.get_or_create(project = self.project1, name = "IMAGE_FSTYPES", value = "abcd ")
url = reverse('projectconf', args=(self.project1.id,));
self.get(url);
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.
"""
ProjectVariable.objects.get_or_create(project = self.project1, name = "IMAGE_FSTYPES", value = "abcd ")
url = reverse('projectconf', args=(self.project1.id,));
self.get(url);
self.click('#change-image_fstypes-icon')
self.wait_until_visible('#new-imagefs_types')
checkboxes_selector = '.fs-checkbox-fstypes'
self.wait_until_visible(checkboxes_selector)
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')
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
"""
ProjectVariable.objects.get_or_create(project=self.project1,
name='DL_DIR')
url = reverse('projectconf', args=(self.project1.id,))
self.get(url)
# activate the input to edit download dir
self.click('#change-dl_dir-icon')
self.wait_until_visible('#new-dl_dir')
# downloads dir path doesn't start with / or ${...}
self.enter_text('#new-dl_dir', 'home/foo')
element = self.wait_until_visible('#hintError-initialChar-dl_dir')
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')
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')
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
"""
ProjectVariable.objects.get_or_create(project=self.project1,
name='SSTATE_DIR')
url = reverse('projectconf', args=(self.project1.id,))
self.get(url)
self.click('#change-sstate_dir-icon')
self.wait_until_visible('#new-sstate_dir')
# path doesn't start with / or ${...}
self.enter_text('#new-sstate_dir', 'home/foo')
element = self.wait_until_visible('#hintError-initialChar-sstate_dir')
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')
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')
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')

View File

@@ -0,0 +1,47 @@
#! /usr/bin/env python3
#
# BitBake Toaster Implementation
#
# Copyright (C) 2013-2016 Intel Corporation
#
# SPDX-License-Identifier: GPL-2.0-only
#
from django.urls import reverse
from django.utils import timezone
from tests.browser.selenium_helpers import SeleniumTestCase
from orm.models import Build, Project
class TestProjectPage(SeleniumTestCase):
""" Test project data at /project/X/ is displayed correctly """
CLI_BUILDS_PROJECT_NAME = 'Command line builds'
def test_cli_builds_in_progress(self):
"""
In progress builds should not cause an error to be thrown
when navigating to "command line builds" project page;
see https://bugzilla.yoctoproject.org/show_bug.cgi?id=8277
"""
# add the "command line builds" default project; this mirrors what
# we do with get_or_create_default_project()
default_project = Project.objects.create_project(self.CLI_BUILDS_PROJECT_NAME, None)
default_project.is_default = True
default_project.save()
# add an "in progress" build for the default project
now = timezone.now()
Build.objects.create(project=default_project,
started_on=now,
completed_on=now,
outcome=Build.IN_PROGRESS)
# navigate to the project page for the default project
url = reverse("project", args=(default_project.id,))
self.get(url)
# check that we get a project page with the correct heading
project_name = self.find('.project-name').text.strip()
self.assertEqual(project_name, self.CLI_BUILDS_PROJECT_NAME)

View File

@@ -0,0 +1,39 @@
#! /usr/bin/env python3
#
# BitBake Toaster Implementation
#
# Copyright (C) 2013-2016 Intel Corporation
#
# SPDX-License-Identifier: GPL-2.0-only
#
"""
A small example test demonstrating the basics of writing a test with
Toaster's SeleniumTestCase; this just fetches the Toaster home page
and checks it has the word "Toaster" in the brand link
New test files should follow this structure, should be named "test_*.py",
and should be in the same directory as this sample.
"""
from django.urls import reverse
from tests.browser.selenium_helpers import SeleniumTestCase
class TestSample(SeleniumTestCase):
""" Test landing page shows the Toaster brand """
def test_landing_page_has_brand(self):
url = reverse('landing')
self.get(url)
brand_link = self.find('.toaster-navbar-brand a.brand')
self.assertEqual(brand_link.text.strip(), 'Toaster')
def test_no_builds_message(self):
""" Test that a message is shown when there are no builds """
url = reverse('all-builds')
self.get(url)
self.wait_until_visible('#empty-state-allbuildstable') # wait for the empty state div to appear
div_msg = self.find('#empty-state-allbuildstable .alert-info')
msg = 'Sorry - no data found'
self.assertEqual(div_msg.text, msg)

View File

@@ -0,0 +1,64 @@
#! /usr/bin/env python3
#
# BitBake Toaster Implementation
#
# Copyright (C) 2013-2016 Intel Corporation
#
# SPDX-License-Identifier: GPL-2.0-only
#
from django.urls import reverse
from django.utils import timezone
from tests.browser.selenium_helpers import SeleniumTestCase
from orm.models import Project, Build, Layer, Layer_Version, Recipe, Target
from orm.models import Task, Task_Dependency
class TestTaskPage(SeleniumTestCase):
""" Test page which shows an individual task """
RECIPE_NAME = 'bar'
RECIPE_VERSION = '0.1'
TASK_NAME = 'do_da_doo_ron_ron'
def setUp(self):
now = timezone.now()
project = Project.objects.get_or_create_default_project()
self.build = Build.objects.create(project=project, started_on=now,
completed_on=now)
Target.objects.create(target='foo', build=self.build)
layer = Layer.objects.create()
layer_version = Layer_Version.objects.create(layer=layer)
recipe = Recipe.objects.create(name=TestTaskPage.RECIPE_NAME,
layer_version=layer_version, version=TestTaskPage.RECIPE_VERSION)
self.task = Task.objects.create(build=self.build, recipe=recipe,
order=1, outcome=Task.OUTCOME_COVERED, task_executed=False,
task_name=TestTaskPage.TASK_NAME)
def test_covered_task(self):
"""
Check that covered tasks are displayed for tasks which have
dependencies on themselves
"""
# the infinite loop which of bug 9952 was down to tasks which
# depend on themselves, so add self-dependent tasks to replicate the
# situation which caused the infinite loop (now fixed)
Task_Dependency.objects.create(task=self.task, depends_on=self.task)
url = reverse('task', args=(self.build.id, self.task.id,))
self.get(url)
# check that we see the task name
self.wait_until_visible('.page-header h1')
heading = self.find('.page-header h1')
expected_heading = '%s_%s %s' % (TestTaskPage.RECIPE_NAME,
TestTaskPage.RECIPE_VERSION, TestTaskPage.TASK_NAME)
self.assertEqual(heading.text, expected_heading,
'Heading should show recipe name, version and task')

View File

@@ -0,0 +1,151 @@
#! /usr/bin/env python3
#
# BitBake Toaster Implementation
#
# Copyright (C) 2013-2016 Intel Corporation
#
# SPDX-License-Identifier: GPL-2.0-only
#
from datetime import datetime
import os
from django.urls import reverse
from django.utils import timezone
from tests.browser.selenium_helpers import SeleniumTestCase
from orm.models import BitbakeVersion, Release, Project, Build
from selenium.webdriver.common.by import By
class TestToasterTableUI(SeleniumTestCase):
"""
Tests for the UI elements of ToasterTable (sorting etc.);
note that the tests cover generic functionality of ToasterTable which
manifests as UI elements in the browser, and can only be tested via
Selenium.
"""
def setUp(self):
pass
def _get_orderby_heading(self, table):
"""
Get the current order by finding the column heading in <table> with
the sorted class on it.
table: WebElement for a ToasterTable
"""
selector = 'thead a.sorted'
heading = table.find_element(By.CSS_SELECTOR, selector)
return heading.get_attribute('innerHTML').strip()
def _get_datetime_from_cell(self, row, selector):
"""
Return the value in the cell selected by <selector> on <row> as a
datetime.
row: <tr> WebElement for a row in the ToasterTable
selector: CSS selector to use to find the cell containing the date time
string
"""
cell = row.find_element(By.CSS_SELECTOR, selector)
cell_text = cell.get_attribute('innerHTML').strip()
return datetime.strptime(cell_text, '%d/%m/%y %H:%M')
def test_revert_orderby(self):
"""
Test that sort order for a table reverts to the default sort order
if the current sort column is hidden.
"""
now = timezone.now()
later = now + timezone.timedelta(hours=1)
even_later = later + timezone.timedelta(hours=1)
builldir = os.environ.get('BUILDDIR', './')
bbv = BitbakeVersion.objects.create(name='test bbv', giturl=f'{builldir}/',
branch='master', dirpath='')
release = Release.objects.create(name='test release',
branch_name='master',
bitbake_version=bbv)
project = Project.objects.create_project('project', release)
# set up two builds which will order differently when sorted by
# started_on or completed_on
# started first, finished last
build1 = Build.objects.create(project=project,
started_on=now,
completed_on=even_later,
outcome=Build.SUCCEEDED)
# started second, finished first
build2 = Build.objects.create(project=project,
started_on=later,
completed_on=later,
outcome=Build.SUCCEEDED)
url = reverse('all-builds')
self.get(url)
table = self.wait_until_visible('#allbuildstable')
# check ordering (default is by -completed_on); so build1 should be
# first as it finished last
active_heading = self._get_orderby_heading(table)
self.assertEqual(active_heading, 'Completed on',
'table should be sorted by "Completed on" by default')
row_selector = '#allbuildstable tbody tr'
cell_selector = 'td.completed_on'
rows = self.find_all(row_selector)
row1_completed_on = self._get_datetime_from_cell(rows[0], cell_selector)
row2_completed_on = self._get_datetime_from_cell(rows[1], cell_selector)
self.assertTrue(row1_completed_on > row2_completed_on,
'table should be sorted by -completed_on')
# turn on started_on column
self.click('#edit-columns-button')
self.click('#checkbox-started_on')
# sort by started_on column
links = table.find_elements(By.CSS_SELECTOR, 'th.started_on a')
for link in links:
if link.get_attribute('innerHTML').strip() == 'Started on':
link.click()
break
# wait for table data to reload in response to new sort
self.wait_until_visible('#allbuildstable')
# check ordering; build1 should be first
active_heading = self._get_orderby_heading(table)
self.assertEqual(active_heading, 'Started on',
'table should be sorted by "Started on"')
cell_selector = 'td.started_on'
rows = self.find_all(row_selector)
row1_started_on = self._get_datetime_from_cell(rows[0], cell_selector)
row2_started_on = self._get_datetime_from_cell(rows[1], cell_selector)
self.assertTrue(row1_started_on < row2_started_on,
'table should be sorted by started_on')
# turn off started_on column
self.click('#edit-columns-button')
self.click('#checkbox-started_on')
# wait for table data to reload in response to new sort
self.wait_until_visible('#allbuildstable')
# check ordering (should revert to completed_on); build2 should be first
active_heading = self._get_orderby_heading(table)
self.assertEqual(active_heading, 'Completed on',
'table should be sorted by "Completed on" after hiding sort column')
cell_selector = 'td.completed_on'
rows = self.find_all(row_selector)
row1_completed_on = self._get_datetime_from_cell(rows[0], cell_selector)
row2_completed_on = self._get_datetime_from_cell(rows[1], cell_selector)
self.assertTrue(row1_completed_on > row2_completed_on,
'table should be sorted by -completed_on')

View File

@@ -0,0 +1,14 @@
# Running build tests
These tests are to test the running of builds and the data produced by the builds.
Your oe build environment must be sourced/initialised for these tests to run.
The simplest way to run the tests are the following commands:
$ . oe-init-build-env
$ cd bitbake/lib/toaster/ # path my vary but this is into toaster's directory
$ DJANGO_SETTINGS_MODULE='toastermain.settings_test' ./manage.py test tests.builds
Optional environment variables:
- TOASTER_DIR (where toaster keeps it's artifacts)
- TOASTER_CONF a path to the toasterconf.json file. This will need to be set if you don't execute the tests from toaster's own directory.

View File

@@ -0,0 +1,166 @@
#! /usr/bin/env python3
#
# BitBake Toaster Implementation
#
# Copyright (C) 2016 Intel Corporation
#
# SPDX-License-Identifier: GPL-2.0-only
#
import os
import sys
import time
import unittest
from orm.models import Project, Release, ProjectTarget, Build, ProjectVariable
from bldcontrol.models import BuildEnvironment
from bldcontrol.management.commands.runbuilds import Command\
as RunBuildsCommand
from django.core.management import call_command
import subprocess
import logging
logger = logging.getLogger("toaster")
# We use unittest.TestCase instead of django.test.TestCase because we don't
# want to wrap everything in a database transaction as an external process
# (bitbake needs access to the database)
def load_build_environment():
call_command('loaddata', 'settings.xml', app_label="orm")
call_command('loaddata', 'poky.xml', app_label="orm")
current_builddir = os.environ.get("BUILDDIR")
if current_builddir:
BuildTest.BUILDDIR = current_builddir
else:
# Setup a builddir based on default layout
# bitbake inside openebedded-core
oe_init_build_env_path = os.path.join(
os.path.dirname(os.path.abspath(__file__)),
os.pardir,
os.pardir,
os.pardir,
os.pardir,
os.pardir,
'oe-init-build-env'
)
if not os.path.exists(oe_init_build_env_path):
raise Exception("We had no BUILDDIR set and couldn't "
"find oe-init-build-env to set this up "
"ourselves please run oe-init-build-env "
"before running these tests")
oe_init_build_env_path = os.path.realpath(oe_init_build_env_path)
cmd = "bash -c 'source oe-init-build-env %s'" % BuildTest.BUILDDIR
p = subprocess.Popen(
cmd,
cwd=os.path.dirname(oe_init_build_env_path),
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
output, err = p.communicate()
p.wait()
logger.info("oe-init-build-env %s %s" % (output, err))
os.environ['BUILDDIR'] = BuildTest.BUILDDIR
# Setup the path to bitbake we know where to find this
bitbake_path = os.path.join(
os.path.dirname(os.path.abspath(__file__)),
os.pardir,
os.pardir,
os.pardir,
os.pardir,
'bin',
'bitbake')
if not os.path.exists(bitbake_path):
raise Exception("Could not find bitbake at the expected path %s"
% bitbake_path)
os.environ['BBBASEDIR'] = bitbake_path
class BuildTest(unittest.TestCase):
PROJECT_NAME = "Testbuild"
BUILDDIR = os.environ.get("BUILDDIR")
def build(self, target):
# So that the buildinfo helper uses the test database'
self.assertEqual(
os.environ.get('DJANGO_SETTINGS_MODULE', ''),
'toastermain.settings_test',
"Please initialise django with the tests settings: "
"DJANGO_SETTINGS_MODULE='toastermain.settings_test'")
built = self.target_already_built(target)
if built:
return built
load_build_environment()
BuildEnvironment.objects.get_or_create(
betype=BuildEnvironment.TYPE_LOCAL,
sourcedir=BuildTest.BUILDDIR,
builddir=BuildTest.BUILDDIR
)
release = Release.objects.get(name='local')
# Create a project for this build to run in
project = Project.objects.create_project(name=BuildTest.PROJECT_NAME,
release=release)
passthrough_variable_names = ["SSTATE_DIR", "DL_DIR", "SSTATE_MIRRORS", "BB_HASHSERVE", "BB_HASHSERVE_UPSTREAM"]
for variable_name in passthrough_variable_names:
current_variable = os.environ.get(variable_name)
if current_variable:
ProjectVariable.objects.get_or_create(
name=variable_name,
value=current_variable,
project=project)
if os.environ.get("TOASTER_TEST_USE_SSTATE_MIRROR"):
ProjectVariable.objects.get_or_create(
name="SSTATE_MIRRORS",
value="file://.* http://sstate.yoctoproject.org/all/PATH;downloadfilename=PATH",
project=project)
ProjectTarget.objects.create(project=project,
target=target,
task="")
build_request = project.schedule_build()
# run runbuilds command to dispatch the build
# e.g. manage.py runubilds
RunBuildsCommand().runbuild()
build_pk = build_request.build.pk
while Build.objects.get(pk=build_pk).outcome == Build.IN_PROGRESS:
sys.stdout.write("\rBuilding %s %d%%" %
(target,
build_request.build.completeper()))
sys.stdout.flush()
time.sleep(1)
self.assertEqual(Build.objects.get(pk=build_pk).outcome,
Build.SUCCEEDED,
"Build did not SUCCEEDED")
logger.info("\nBuild finished %s" % build_request.build.outcome)
return build_request.build
def target_already_built(self, target):
""" If the target is already built no need to build it again"""
for build in Build.objects.filter(
project__name=BuildTest.PROJECT_NAME):
targets = build.target_set.values_list('target', flat=True)
if target in targets:
return build
return None

View File

@@ -0,0 +1,363 @@
#! /usr/bin/env python3
#
# BitBake Toaster Implementation
#
# Copyright (C) 2016 Intel Corporation
#
# SPDX-License-Identifier: GPL-2.0-only
#
# Tests were part of openembedded-core oe selftest Authored by: Lucian Musat
# Ionut Chisanovici, Paul Eggleton and Cristian Iorga
import os
import pytest
from django.db.models import Q
from orm.models import Target_Image_File, Target_Installed_Package, Task
from orm.models import Package_Dependency, Recipe_Dependency, Build
from orm.models import Task_Dependency, Package, Target, Recipe
from orm.models import CustomImagePackage
from tests.builds.buildtest import BuildTest
@pytest.mark.order(4)
@pytest.mark.django_db(True)
class BuildCoreImageMinimal(BuildTest):
"""Build core-image-minimal and test the results"""
def setUp(self):
self.completed_build = self.target_already_built("core-image-minimal")
# Check if build name is unique - tc_id=795
def test_Build_Unique_Name(self):
all_builds = Build.objects.all().count()
distinct_builds = Build.objects.values('id').distinct().count()
self.assertEqual(distinct_builds,
all_builds,
msg='Build name is not unique')
# Check if build cooker log path is unique - tc_id=819
def test_Build_Unique_Cooker_Log_Path(self):
distinct_path = Build.objects.values(
'cooker_log_path').distinct().count()
total_builds = Build.objects.values('id').count()
self.assertEqual(distinct_path,
total_builds,
msg='Build cooker log path is not unique')
# Check task order sequence for one build - tc=825
def test_Task_Order_Sequence(self):
cnt_err = []
tasks = Task.objects.filter(
Q(build=self.completed_build),
~Q(order=None),
~Q(task_name__contains='_setscene')
).values('id', 'order').order_by("order")
cnt_tasks = 0
for task in tasks:
cnt_tasks += 1
if (task['order'] != cnt_tasks):
cnt_err.append(task['id'])
self.assertEqual(
len(cnt_err), 0, msg='Errors for task id: %s' % cnt_err)
# Check if disk_io matches the difference between EndTimeIO and
# StartTimeIO in build stats - tc=828
# def test_Task_Disk_IO_TC828(self):
# Check if outcome = 2 (SSTATE) then sstate_result must be 3 (RESTORED) -
# tc=832
def test_Task_If_Outcome_2_Sstate_Result_Must_Be_3(self):
tasks = Task.objects.filter(outcome=2).values('id', 'sstate_result')
cnt_err = []
for task in tasks:
if (task['sstate_result'] != 3):
cnt_err.append(task['id'])
self.assertEqual(len(cnt_err),
0,
msg='Errors for task id: %s' % cnt_err)
# Check if outcome = 1 (COVERED) or 3 (EXISTING) then sstate_result must
# be 0 (SSTATE_NA) - tc=833
def test_Task_If_Outcome_1_3_Sstate_Result_Must_Be_0(self):
tasks = Task.objects.filter(
outcome__in=(Task.OUTCOME_COVERED,
Task.OUTCOME_PREBUILT)).values('id',
'task_name',
'sstate_result')
cnt_err = []
for task in tasks:
if (task['sstate_result'] != Task.SSTATE_NA and
task['sstate_result'] != Task.SSTATE_MISS):
cnt_err.append({'id': task['id'],
'name': task['task_name'],
'sstate_result': task['sstate_result']})
self.assertEqual(len(cnt_err),
0,
msg='Errors for task id: %s' % cnt_err)
# Check if outcome is 0 (SUCCESS) or 4 (FAILED) then sstate_result must be
# 0 (NA), 1 (MISS) or 2 (FAILED) - tc=834
def test_Task_If_Outcome_0_4_Sstate_Result_Must_Be_0_1_2(self):
tasks = Task.objects.filter(
outcome__in=(0, 4)).values('id', 'sstate_result')
cnt_err = []
for task in tasks:
if (task['sstate_result'] not in [0, 1, 2]):
cnt_err.append(task['id'])
self.assertEqual(len(cnt_err),
0,
msg='Errors for task id: %s' % cnt_err)
# Check if task_executed = TRUE (1), script_type must be 0 (CODING_NA), 2
# (CODING_PYTHON), 3 (CODING_SHELL) - tc=891
def test_Task_If_Task_Executed_True_Script_Type_0_2_3(self):
tasks = Task.objects.filter(
task_executed=1).values('id', 'script_type')
cnt_err = []
for task in tasks:
if (task['script_type'] not in [0, 2, 3]):
cnt_err.append(task['id'])
self.assertEqual(len(cnt_err),
0,
msg='Errors for task id: %s' % cnt_err)
# Check if task_executed = TRUE (1), outcome must be 0 (SUCCESS) or 4
# (FAILED) - tc=836
def test_Task_If_Task_Executed_True_Outcome_0_4(self):
tasks = Task.objects.filter(task_executed=1).values('id', 'outcome')
cnt_err = []
for task in tasks:
if (task['outcome'] not in [0, 4]):
cnt_err.append(task['id'])
self.assertEqual(len(cnt_err),
0,
msg='Errors for task id: %s' % cnt_err)
# Check if task_executed = FALSE (0), script_type must be 0 - tc=890
def test_Task_If_Task_Executed_False_Script_Type_0(self):
tasks = Task.objects.filter(
task_executed=0).values('id', 'script_type')
cnt_err = []
for task in tasks:
if (task['script_type'] != 0):
cnt_err.append(task['id'])
self.assertEqual(len(cnt_err),
0,
msg='Errors for task id: %s' % cnt_err)
# Check if task_executed = FALSE (0) and build outcome = SUCCEEDED (0),
# task outcome must be 1 (COVERED), 2 (CACHED), 3 (PREBUILT), 5 (EMPTY) -
# tc=837
def test_Task_If_Task_Executed_False_Outcome_1_2_3_5(self):
builds = Build.objects.filter(outcome=0).values('id')
cnt_err = []
for build in builds:
tasks = Task.objects.filter(
build=build['id'], task_executed=0).values('id', 'outcome')
for task in tasks:
if (task['outcome'] not in [1, 2, 3, 5]):
cnt_err.append(task['id'])
self.assertEqual(len(cnt_err),
0,
msg='Errors for task id: %s' % cnt_err)
# Key verification - tc=888
def test_Target_Installed_Package(self):
rows = Target_Installed_Package.objects.values('id',
'target_id',
'package_id')
cnt_err = []
for row in rows:
target = Target.objects.filter(id=row['target_id']).values('id')
package = Package.objects.filter(id=row['package_id']).values('id')
if (not target or not package):
cnt_err.append(row['id'])
self.assertEqual(len(cnt_err),
0,
msg='Errors for target installed package id: %s' %
cnt_err)
# Key verification - tc=889
def test_Task_Dependency(self):
rows = Task_Dependency.objects.values('id',
'task_id',
'depends_on_id')
cnt_err = []
for row in rows:
task_id = Task.objects.filter(id=row['task_id']).values('id')
depends_on_id = Task.objects.filter(
id=row['depends_on_id']).values('id')
if (not task_id or not depends_on_id):
cnt_err.append(row['id'])
self.assertEqual(len(cnt_err),
0,
msg='Errors for task dependency id: %s' % cnt_err)
# Check if build target file_name is populated only if is_image=true AND
# orm_build.outcome=0 then if the file exists and its size matches
# the file_size value. Need to add the tc in the test run
def test_Target_File_Name_Populated(self):
cnt_err = []
builds = Build.objects.filter(outcome=0).values('id')
for build in builds:
targets = Target.objects.filter(
build_id=build['id'], is_image=1).values('id')
for target in targets:
target_files = Target_Image_File.objects.filter(
target_id=target['id']).values('id',
'file_name',
'file_size')
for file_info in target_files:
target_id = file_info['id']
target_file_name = file_info['file_name']
target_file_size = file_info['file_size']
if (not target_file_name or not target_file_size):
cnt_err.append(target_id)
else:
if (not os.path.exists(target_file_name)):
cnt_err.append(target_id)
else:
if (os.path.getsize(target_file_name) !=
target_file_size):
cnt_err.append(target_id)
self.assertEqual(len(cnt_err), 0,
msg='Errors for target image file id: %s' %
cnt_err)
# Key verification - tc=884
def test_Package_Dependency(self):
cnt_err = []
deps = Package_Dependency.objects.values(
'id', 'package_id', 'depends_on_id')
for dep in deps:
if (dep['package_id'] == dep['depends_on_id']):
cnt_err.append(dep['id'])
self.assertEqual(len(cnt_err), 0,
msg='Errors for package dependency id: %s' % cnt_err)
# Recipe key verification, recipe name does not depends on a recipe having
# the same name - tc=883
def test_Recipe_Dependency(self):
deps = Recipe_Dependency.objects.values(
'id', 'recipe_id', 'depends_on_id')
cnt_err = []
for dep in deps:
if (not dep['recipe_id'] or not dep['depends_on_id']):
cnt_err.append(dep['id'])
else:
name = Recipe.objects.filter(
id=dep['recipe_id']).values('name')
dep_name = Recipe.objects.filter(
id=dep['depends_on_id']).values('name')
if (name == dep_name):
cnt_err.append(dep['id'])
self.assertEqual(len(cnt_err), 0,
msg='Errors for recipe dependency id: %s' % cnt_err)
# Check if package name does not start with a number (0-9) - tc=846
def test_Package_Name_For_Number(self):
packages = Package.objects.filter(~Q(size=-1)).values('id', 'name')
cnt_err = []
for package in packages:
if (package['name'][0].isdigit() is True):
cnt_err.append(package['id'])
self.assertEqual(
len(cnt_err), 0, msg='Errors for package id: %s' % cnt_err)
# Check if package version starts with a number (0-9) - tc=847
def test_Package_Version_Starts_With_Number(self):
packages = Package.objects.filter(
~Q(size=-1)).values('id', 'version')
cnt_err = []
for package in packages:
if (package['version'][0].isdigit() is False):
cnt_err.append(package['id'])
self.assertEqual(
len(cnt_err), 0, msg='Errors for package id: %s' % cnt_err)
# Check if package revision starts with 'r' - tc=848
def test_Package_Revision_Starts_With_r(self):
packages = Package.objects.filter(
~Q(size=-1)).values('id', 'revision')
cnt_err = []
for package in packages:
if (package['revision'][0].startswith("r") is False):
cnt_err.append(package['id'])
self.assertEqual(
len(cnt_err), 0, msg='Errors for package id: %s' % cnt_err)
# Check the validity of the package build_id
# TC must be added in test run
def test_Package_Build_Id(self):
packages = Package.objects.filter(
~Q(size=-1)).values('id', 'build_id')
cnt_err = []
for package in packages:
build_id = Build.objects.filter(
id=package['build_id']).values('id')
if (not build_id):
# They have no build_id but if they are
# CustomImagePackage that's expected
try:
CustomImagePackage.objects.get(pk=package['id'])
except CustomImagePackage.DoesNotExist:
cnt_err.append(package['id'])
self.assertEqual(len(cnt_err),
0,
msg="Errors for package id: %s they have no build"
"associated with them" % cnt_err)
# Check the validity of package recipe_id
# TC must be added in test run
def test_Package_Recipe_Id(self):
packages = Package.objects.filter(
~Q(size=-1)).values('id', 'recipe_id')
cnt_err = []
for package in packages:
recipe_id = Recipe.objects.filter(
id=package['recipe_id']).values('id')
if (not recipe_id):
cnt_err.append(package['id'])
self.assertEqual(
len(cnt_err), 0, msg='Errors for package id: %s' % cnt_err)
# Check if package installed_size field is not null
# TC must be aded in test run
def test_Package_Installed_Size_Not_NULL(self):
packages = Package.objects.filter(
installed_size__isnull=True).values('id')
cnt_err = []
for package in packages:
cnt_err.append(package['id'])
self.assertEqual(
len(cnt_err), 0, msg='Errors for package id: %s' % cnt_err)
def test_custom_packages_generated(self):
"""Test if there is a corresponding generated CustomImagePackage"""
""" for each of the packages generated"""
missing_packages = []
for package in Package.objects.all():
try:
CustomImagePackage.objects.get(name=package.name)
except CustomImagePackage.DoesNotExist:
missing_packages.append(package.name)
self.assertEqual(len(missing_packages), 0,
"Some package were created from the build but their"
" corresponding CustomImagePackage was not found")

View File

@@ -0,0 +1,49 @@
#! /usr/bin/env python3
#
# BitBake Toaster Implementation
#
# Copyright (C) 2016 Intel Corporation
#
# SPDX-License-Identifier: GPL-2.0-only
#
import pytest
from django.test import TestCase
from django.core import management
from orm.models import Layer_Version, Layer, Release, ToasterSetting
@pytest.mark.order(2)
class TestLoadDataFixtures(TestCase):
""" Test loading our 3 provided fixtures """
def test_run_loaddata_poky_command(self):
management.call_command('loaddata', 'poky')
num_releases = Release.objects.count()
self.assertTrue(
Layer_Version.objects.filter(
layer__name="meta-poky").count() == num_releases,
"Loaded poky fixture but don't have a meta-poky for all releases"
" defined")
def test_run_loaddata_oecore_command(self):
management.call_command('loaddata', 'oe-core')
# We only have the one layer for oe-core setup
self.assertTrue(
Layer.objects.filter(name="openembedded-core").count() > 0,
"Loaded oe-core fixture but still have no openemebedded-core"
" layer")
def test_run_loaddata_settings_command(self):
management.call_command('loaddata', 'settings')
self.assertTrue(
ToasterSetting.objects.filter(name="DEFAULT_RELEASE").count() > 0,
"Loaded settings but have no DEFAULT_RELEASE")
self.assertTrue(
ToasterSetting.objects.filter(
name__startswith="DEFCONF").count() > 0,
"Loaded settings but have no DEFCONF (default project "
"configuration values)")

View File

@@ -0,0 +1,34 @@
#! /usr/bin/env python3
#
# BitBake Toaster Implementation
#
# Copyright (C) 2016 Intel Corporation
#
# SPDX-License-Identifier: GPL-2.0-only
#
import pytest
from django.test import TestCase
from django.core import management
from orm.models import Layer_Version, Machine, Recipe
@pytest.mark.order(3)
class TestLayerIndexUpdater(TestCase):
def test_run_lsupdates_command(self):
# Load some release information for us to fetch from the layer index
management.call_command('loaddata', 'poky')
old_layers_count = Layer_Version.objects.count()
old_recipes_count = Recipe.objects.count()
old_machines_count = Machine.objects.count()
# Now fetch the metadata from the layer index
management.call_command('lsupdates')
self.assertTrue(Layer_Version.objects.count() > old_layers_count,
"lsupdates ran but we still have no more layers!")
self.assertTrue(Recipe.objects.count() > old_recipes_count,
"lsupdates ran but we still have no more Recipes!")
self.assertTrue(Machine.objects.count() > old_machines_count,
"lsupdates ran but we still have no more Machines!")

View File

@@ -0,0 +1,81 @@
#! /usr/bin/env python3
#
# BitBake Toaster Implementation
#
# Copyright (C) 2016 Intel Corporation
#
# SPDX-License-Identifier: GPL-2.0-only
#
import os
from django.test import TestCase
from django.core import management
from orm.models import signal_runbuilds
import threading
import time
import subprocess
import signal
import logging
class KillRunbuilds(threading.Thread):
""" Kill the runbuilds process after an amount of time """
def __init__(self, *args, **kwargs):
super(KillRunbuilds, self).__init__(*args, **kwargs)
self.daemon = True
def run(self):
time.sleep(5)
signal_runbuilds()
time.sleep(1)
pidfile_path = os.path.join(os.environ.get("BUILDDIR", "."),
".runbuilds.pid")
try:
with open(pidfile_path) as pidfile:
pid = pidfile.read()
os.kill(int(pid), signal.SIGTERM)
except ProcessLookupError:
logging.warning("Runbuilds not running or already killed")
class TestCommands(TestCase):
""" Sanity test that runbuilds executes OK """
def setUp(self):
os.environ.setdefault("DJANGO_SETTINGS_MODULE",
"toastermain.settings_test")
os.environ.setdefault("BUILDDIR",
"/tmp/")
# Setup a real database if needed for runbuilds process
# to connect to
management.call_command('migrate')
def test_runbuilds_command(self):
kill_runbuilds = KillRunbuilds()
kill_runbuilds.start()
manage_py = os.path.join(
os.path.dirname(os.path.abspath(__file__)),
os.pardir,
os.pardir,
"manage.py")
command = "%s runbuilds" % manage_py
process = subprocess.Popen(command,
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
(out, err) = process.communicate()
process.wait()
self.assertNotEqual(process.returncode, 1,
"Runbuilds returned an error %s" % err)

View File

@@ -0,0 +1,58 @@
# The MIT License (MIT)
#
# Copyright (c) 2016 Damien Lespiau
#
# SPDX-License-Identifier: MIT
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import sys
import pytest
try:
from StringIO import StringIO
except ImportError:
from io import StringIO
from contextlib import contextmanager
from django.core import management
from django.test import TestCase
@contextmanager
def capture(command, *args, **kwargs):
out, sys.stdout = sys.stdout, StringIO()
command(*args, **kwargs)
sys.stdout.seek(0)
yield sys.stdout.read()
sys.stdout = out
def makemigrations():
management.call_command('makemigrations')
@pytest.mark.order(1)
class MigrationTest(TestCase):
def testPendingMigration(self):
"""Make sure there's no pending migration."""
with capture(makemigrations) as output:
self.assertEqual(output, "No changes detected\n")

View File

@@ -0,0 +1,22 @@
# Running eventreplay tests
These tests use event log files produced by bitbake <target> -w <event log file>
You need to have event log files produced before running this tests.
At the moment of writing this document tests use 2 event log files: zlib.events
and core-image-minimal.events. They're not provided with the tests due to their
significant size.
Here is how to produce them:
$ . oe-init-build-env
$ rm -r tmp sstate-cache
$ bitbake core-image-minimal -w core-image-minimal.events
$ rm -rf tmp sstate-cache
$ bitbake zlib -w zlib.events
After that it should be possible to run eventreplay tests this way:
$ EVENTREPLAY_DIR=./ DJANGO_SETTINGS_MODULE=toastermain.settings_test ../bitbake/lib/toaster/manage.py test -v2 tests.eventreplay
Note that environment variable EVENTREPLAY_DIR should point to the directory with event log files.

View File

@@ -0,0 +1,85 @@
#! /usr/bin/env python3
#
# BitBake Toaster Implementation
#
# Copyright (C) 2016 Intel Corporation
#
# SPDX-License-Identifier: GPL-2.0-only
#
# Tests were part of openembedded-core oe selftest Authored by: Lucian Musat
# Ionut Chisanovici, Paul Eggleton and Cristian Iorga
"""
Test toaster backend by playing build event log files
using toaster-eventreplay script
"""
import os
from subprocess import getstatusoutput
from pathlib import Path
from django.test import TestCase
from orm.models import Target_Installed_Package, Package, Build
class EventReplay(TestCase):
"""Base class for eventreplay test cases"""
def setUp(self):
"""
Setup build environment:
- set self.script to toaster-eventreplay path
- set self.eventplay_dir to the value of EVENTPLAY_DIR env variable
"""
bitbake_dir = Path(__file__.split('lib/toaster')[0])
self.script = bitbake_dir / 'bin' / 'toaster-eventreplay'
self.assertTrue(self.script.exists(), "%s doesn't exist")
self.eventplay_dir = os.getenv("EVENTREPLAY_DIR")
self.assertTrue(self.eventplay_dir,
"Environment variable EVENTREPLAY_DIR is not set")
def _replay(self, eventfile):
"""Run toaster-eventplay <eventfile>"""
eventpath = Path(self.eventplay_dir) / eventfile
status, output = getstatusoutput('%s %s' % (self.script, eventpath))
if status:
print(output)
self.assertEqual(status, 0)
class CoreImageMinimalEventReplay(EventReplay):
"""Replay core-image-minimal events"""
def test_installed_packages(self):
"""Test if all required packages have been installed"""
self._replay('core-image-minimal.events')
# test installed packages
packages = sorted(Target_Installed_Package.objects.\
values_list('package__name', flat=True))
self.assertEqual(packages, ['base-files', 'base-passwd', 'busybox',
'busybox-hwclock', 'busybox-syslog',
'busybox-udhcpc', 'eudev', 'glibc',
'init-ifupdown', 'initscripts',
'initscripts-functions', 'kernel-base',
'kernel-module-uvesafb', 'libkmod',
'modutils-initscripts', 'netbase',
'packagegroup-core-boot', 'run-postinsts',
'sysvinit', 'sysvinit-inittab',
'sysvinit-pidof', 'udev-cache',
'update-alternatives-opkg',
'update-rc.d', 'util-linux-libblkid',
'util-linux-libuuid', 'v86d', 'zlib'])
class ZlibEventReplay(EventReplay):
"""Replay zlib events"""
def test_replay_zlib(self):
"""Test if zlib build and package are in the database"""
self._replay("zlib.events")
self.assertEqual(Build.objects.last().target_set.last().target, "zlib")
self.assertTrue('zlib' in Package.objects.values_list('name', flat=True))

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

View File

@@ -0,0 +1,7 @@
selenium>=4.13.0
pytest==7.4.2
pytest-django==4.5.2
pytest-env==1.1.0
pytest-html==4.0.2
pytest-metadata==3.0.0
pytest-order==1.1.0

View File

@@ -0,0 +1,4 @@
Django unit tests to verify classes and functions based on django Views
To run just these tests use ./manage.py test tests.views

View File

@@ -0,0 +1,544 @@
#! /usr/bin/env python3
#
# BitBake Toaster Implementation
#
# Copyright (C) 2013-2015 Intel Corporation
#
# SPDX-License-Identifier: GPL-2.0-only
#
"""Test cases for Toaster GUI and ReST."""
import os
import pytest
from django.test import TestCase
from django.test.client import RequestFactory
from django.urls import reverse
from django.db.models import Q
from orm.models import Project, Package
from orm.models import Layer_Version, Recipe
from orm.models import CustomImageRecipe
from orm.models import CustomImagePackage
from bldcontrol.models import BuildEnvironment
import inspect
import toastergui
from toastergui.tables import SoftwareRecipesTable
import json
from bs4 import BeautifulSoup
import string
PROJECT_NAME = "test project"
PROJECT_NAME2 = "test project 2"
CLI_BUILDS_PROJECT_NAME = 'Command line builds'
class ViewTests(TestCase):
"""Tests to verify view APIs."""
fixtures = ['toastergui-unittest-data']
builldir = os.environ.get('BUILDDIR')
def setUp(self):
self.project = Project.objects.first()
self.recipe1 = Recipe.objects.get(pk=2)
# create a file and to recipe1 file_path
file_path = f"{self.builldir}/{self.recipe1.name.strip().replace(' ', '-')}.bb"
with open(file_path, 'w') as f:
f.write('foo')
self.recipe1.file_path = file_path
self.recipe1.save()
self.customr = CustomImageRecipe.objects.first()
self.cust_package = CustomImagePackage.objects.first()
self.package = Package.objects.first()
self.lver = Layer_Version.objects.first()
if BuildEnvironment.objects.count() == 0:
BuildEnvironment.objects.create(betype=BuildEnvironment.TYPE_LOCAL)
def test_get_base_call_returns_html(self):
"""Basic test for all-projects view"""
response = self.client.get(reverse('all-projects'), follow=True)
self.assertEqual(response.status_code, 200)
self.assertTrue(response['Content-Type'].startswith('text/html'))
self.assertTemplateUsed(response, "projects-toastertable.html")
def test_get_json_call_returns_json(self):
"""Test for all projects output in json format"""
url = reverse('all-projects')
response = self.client.get(url, {"format": "json"}, follow=True)
self.assertEqual(response.status_code, 200)
self.assertTrue(response['Content-Type'].startswith(
'application/json'))
data = json.loads(response.content.decode('utf-8'))
self.assertTrue("error" in data)
self.assertEqual(data["error"], "ok")
self.assertTrue("rows" in data)
name_found = False
for row in data["rows"]:
name_found = row['name'].find(self.project.name)
self.assertTrue(name_found,
"project name not found in projects table")
def test_typeaheads(self):
"""Test typeahead ReST API"""
layers_url = reverse('xhr_layerstypeahead', args=(self.project.id,))
prj_url = reverse('xhr_projectstypeahead')
urls = [layers_url,
prj_url,
reverse('xhr_recipestypeahead', args=(self.project.id,)),
reverse('xhr_machinestypeahead', args=(self.project.id,))]
def basic_reponse_check(response, url):
"""Check data structure of http response."""
self.assertEqual(response.status_code, 200)
self.assertTrue(response['Content-Type'].startswith(
'application/json'))
data = json.loads(response.content.decode('utf-8'))
self.assertTrue("error" in data)
self.assertEqual(data["error"], "ok")
self.assertTrue("results" in data)
# We got a result so now check the fields
if len(data['results']) > 0:
result = data['results'][0]
self.assertTrue(len(result['name']) > 0)
self.assertTrue("detail" in result)
self.assertTrue(result['id'] > 0)
# Special check for the layers typeahead's extra fields
if url == layers_url:
self.assertTrue(len(result['layerdetailurl']) > 0)
self.assertTrue(len(result['vcs_url']) > 0)
self.assertTrue(len(result['vcs_reference']) > 0)
# Special check for project typeahead extra fields
elif url == prj_url:
self.assertTrue(len(result['projectPageUrl']) > 0)
return True
return False
for url in urls:
results = False
for typeing in list(string.ascii_letters):
response = self.client.get(url, {'search': typeing})
results = basic_reponse_check(response, url)
if results:
break
# After "typeing" the alpabet we should have result true
# from each of the urls
self.assertTrue(results)
def test_xhr_add_layer(self):
"""Test xhr_add API"""
# Test for importing an already existing layer
api_url = reverse('xhr_layer', args=(self.project.id,))
layer_data = {'vcs_url': "git://git.example.com/test",
'name': "base-layer",
'git_ref': "c12b9596afd236116b25ce26dbe0d793de9dc7ce",
'project_id': self.project.id,
'local_source_dir': "",
'add_to_project': True,
'dir_path': "/path/in/repository"}
layer_data_json = json.dumps(layer_data)
response = self.client.put(api_url, layer_data_json)
data = json.loads(response.content.decode('utf-8'))
self.assertEqual(response.status_code, 200)
self.assertEqual(data["error"], "ok")
self.assertTrue(
layer_data['name'] in
self.project.get_all_compatible_layer_versions().values_list(
'layer__name',
flat=True),
"Could not find imported layer in project's all layers list"
)
# Empty data passed
response = self.client.put(api_url, "{}")
data = json.loads(response.content.decode('utf-8'))
self.assertNotEqual(data["error"], "ok")
def test_custom_ok(self):
"""Test successful return from ReST API xhr_customrecipe"""
url = reverse('xhr_customrecipe')
params = {'name': 'custom', 'project': self.project.id,
'base': self.recipe1.id}
response = self.client.post(url, params)
self.assertEqual(response.status_code, 200)
data = json.loads(response.content.decode('utf-8'))
self.assertEqual(data['error'], 'ok')
self.assertTrue('url' in data)
# get recipe from the database
recipe = CustomImageRecipe.objects.get(project=self.project,
name=params['name'])
args = (self.project.id, recipe.id,)
self.assertEqual(reverse('customrecipe', args=args), data['url'])
def test_custom_incomplete_params(self):
"""Test not passing all required parameters to xhr_customrecipe"""
url = reverse('xhr_customrecipe')
for params in [{}, {'name': 'custom'},
{'name': 'custom', 'project': self.project.id}]:
response = self.client.post(url, params)
self.assertEqual(response.status_code, 200)
data = json.loads(response.content.decode('utf-8'))
self.assertNotEqual(data["error"], "ok")
def test_xhr_custom_wrong_project(self):
"""Test passing wrong project id to xhr_customrecipe"""
url = reverse('xhr_customrecipe')
params = {'name': 'custom', 'project': 0, "base": self.recipe1.id}
response = self.client.post(url, params)
self.assertEqual(response.status_code, 200)
data = json.loads(response.content.decode('utf-8'))
self.assertNotEqual(data["error"], "ok")
def test_xhr_custom_wrong_base(self):
"""Test passing wrong base recipe id to xhr_customrecipe"""
url = reverse('xhr_customrecipe')
params = {'name': 'custom', 'project': self.project.id, "base": 0}
response = self.client.post(url, params)
self.assertEqual(response.status_code, 200)
data = json.loads(response.content.decode('utf-8'))
self.assertNotEqual(data["error"], "ok")
def test_xhr_custom_details(self):
"""Test getting custom recipe details"""
url = reverse('xhr_customrecipe_id', args=(self.customr.id,))
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
expected = {"error": "ok",
"info": {'id': self.customr.id,
'name': self.customr.name,
'base_recipe_id': self.recipe1.id,
'project_id': self.project.id}}
self.assertEqual(json.loads(response.content.decode('utf-8')),
expected)
def test_xhr_custom_del(self):
"""Test deleting custom recipe"""
name = "to be deleted"
recipe = CustomImageRecipe.objects.create(
name=name, project=self.project,
base_recipe=self.recipe1,
file_path=f"{self.builldir}/testing",
layer_version=self.customr.layer_version)
url = reverse('xhr_customrecipe_id', args=(recipe.id,))
response = self.client.delete(url)
self.assertEqual(response.status_code, 200)
gotoUrl = reverse('projectcustomimages', args=(self.project.pk,))
self.assertEqual(json.loads(response.content.decode('utf-8')),
{"error": "ok",
"gotoUrl": gotoUrl})
# try to delete not-existent recipe
url = reverse('xhr_customrecipe_id', args=(recipe.id,))
response = self.client.delete(url)
self.assertEqual(response.status_code, 200)
self.assertNotEqual(json.loads(
response.content.decode('utf-8'))["error"], "ok")
def test_xhr_custom_packages(self):
"""Test adding and deleting package to a custom recipe"""
# add self.package to recipe
response = self.client.put(reverse('xhr_customrecipe_packages',
args=(self.customr.id,
self.cust_package.id)))
self.assertEqual(response.status_code, 200)
self.assertEqual(json.loads(response.content.decode('utf-8')),
{"error": "ok"})
self.assertEqual(self.customr.appends_set.first().name,
self.cust_package.name)
# delete it
to_delete = self.customr.appends_set.first().pk
del_url = reverse('xhr_customrecipe_packages',
args=(self.customr.id, to_delete))
response = self.client.delete(del_url)
self.assertEqual(response.status_code, 200)
self.assertEqual(json.loads(response.content.decode('utf-8')),
{"error": "ok"})
all_packages = self.customr.get_all_packages().values_list('pk',
flat=True)
self.assertFalse(to_delete in all_packages)
# delete invalid package to test error condition
del_url = reverse('xhr_customrecipe_packages',
args=(self.customr.id,
99999))
response = self.client.delete(del_url)
self.assertEqual(response.status_code, 200)
self.assertNotEqual(json.loads(
response.content.decode('utf-8'))["error"], "ok")
def test_xhr_custom_packages_err(self):
"""Test error conditions of xhr_customrecipe_packages"""
# test calls with wrong recipe id and wrong package id
for args in [(0, self.package.id), (self.customr.id, 0)]:
url = reverse('xhr_customrecipe_packages', args=args)
# test put and delete methods
for method in (self.client.put, self.client.delete):
response = method(url)
self.assertEqual(response.status_code, 200)
self.assertNotEqual(json.loads(
response.content.decode('utf-8')),
{"error": "ok"})
def test_download_custom_recipe(self):
"""Download the recipe file generated for the custom image"""
# Create a dummy recipe file for the custom image generation to read
open(f"{self.builldir}/a_recipe.bb", 'a').close()
response = self.client.get(reverse('customrecipedownload',
args=(self.project.id,
self.customr.id)))
self.assertEqual(response.status_code, 200)
def test_software_recipes_table(self):
"""Test structure returned for Software RecipesTable"""
table = SoftwareRecipesTable()
request = RequestFactory().get('/foo/', {'format': 'json'})
response = table.get(request, pid=self.project.id)
data = json.loads(response.content.decode('utf-8'))
recipes = Recipe.objects.filter(Q(is_image=False))
self.assertTrue(len(recipes) > 1,
"Need more than one software recipe to test "
"SoftwareRecipesTable")
recipe1 = recipes[0]
recipe2 = recipes[1]
rows = data['rows']
row1 = next(x for x in rows if x['name'] == recipe1.name)
row2 = next(x for x in rows if x['name'] == recipe2.name)
self.assertEqual(response.status_code, 200, 'should be 200 OK status')
# check other columns have been populated correctly
self.assertTrue(recipe1.name in row1['name'])
self.assertTrue(recipe1.version in row1['version'])
self.assertTrue(recipe1.description in
row1['get_description_or_summary'])
self.assertTrue(recipe1.layer_version.layer.name in
row1['layer_version__layer__name'])
self.assertTrue(recipe2.name in row2['name'])
self.assertTrue(recipe2.version in row2['version'])
self.assertTrue(recipe2.description in
row2['get_description_or_summary'])
self.assertTrue(recipe2.layer_version.layer.name in
row2['layer_version__layer__name'])
def test_toaster_tables(self):
"""Test all ToasterTables instances"""
def get_data(table, options={}):
"""Send a request and parse the json response"""
options['format'] = "json"
options['nocache'] = "true"
request = RequestFactory().get('/', options)
# This is the image recipe needed for a package list for
# PackagesTable do this here to throw a non exist exception
image_recipe = Recipe.objects.get(pk=4)
# Add any kwargs that are needed by any of the possible tables
args = {'pid': self.project.id,
'layerid': self.lver.pk,
'recipeid': self.recipe1.pk,
'recipe_id': image_recipe.pk,
'custrecipeid': self.customr.pk,
'build_id': 1,
'target_id': 1}
response = table.get(request, **args)
return json.loads(response.content.decode('utf-8'))
def get_text_from_td(td):
"""If we have html in the td then extract the text portion"""
# just so we don't waste time parsing non html
if "<" not in td:
ret = td
else:
ret = BeautifulSoup(td, "html.parser").text
if len(ret):
return "0"
else:
return ret
# Get a list of classes in tables module
tables = inspect.getmembers(toastergui.tables, inspect.isclass)
tables.extend(inspect.getmembers(toastergui.buildtables,
inspect.isclass))
for name, table_cls in tables:
# Filter out the non ToasterTables from the tables module
if not issubclass(table_cls, toastergui.widgets.ToasterTable) or \
table_cls == toastergui.widgets.ToasterTable or \
'Mixin' in name:
continue
# Get the table data without any options, this also does the
# initialisation of the table i.e. setup_columns,
# setup_filters and setup_queryset that we can use later
table = table_cls()
all_data = get_data(table)
self.assertTrue(len(all_data['rows']) > 1,
"Cannot test on a %s table with < 1 row" % name)
if table.default_orderby:
row_one = get_text_from_td(
all_data['rows'][0][table.default_orderby.strip("-")])
row_two = get_text_from_td(
all_data['rows'][1][table.default_orderby.strip("-")])
if '-' in table.default_orderby:
self.assertTrue(row_one >= row_two,
"Default ordering not working on %s"
" '%s' should be >= '%s'" %
(name, row_one, row_two))
else:
self.assertTrue(row_one <= row_two,
"Default ordering not working on %s"
" '%s' should be <= '%s'" %
(name, row_one, row_two))
# Test the column ordering and filtering functionality
for column in table.columns:
if column['orderable']:
# If a column is orderable test it in both order
# directions ordering on the columns field_name
ascending = get_data(table_cls(),
{"orderby": column['field_name']})
row_one = get_text_from_td(
ascending['rows'][0][column['field_name']])
row_two = get_text_from_td(
ascending['rows'][1][column['field_name']])
self.assertTrue(row_one <= row_two,
"Ascending sort applied but row 0: \"%s\""
" is less than row 1: \"%s\" "
"%s %s " %
(row_one, row_two,
column['field_name'], name))
descending = get_data(table_cls(),
{"orderby":
'-'+column['field_name']})
row_one = get_text_from_td(
descending['rows'][0][column['field_name']])
row_two = get_text_from_td(
descending['rows'][1][column['field_name']])
self.assertTrue(row_one >= row_two,
"Descending sort applied but row 0: %s"
"is greater than row 1: %s"
"field %s table %s" %
(row_one,
row_two,
column['field_name'], name))
# If the two start rows are the same we haven't actually
# changed the order
self.assertNotEqual(ascending['rows'][0],
descending['rows'][0],
"An orderby %s has not changed the "
"order of the data in table %s" %
(column['field_name'], name))
if column['filter_name']:
# If a filter is available for the column get the filter
# info. This contains what filter actions are defined.
filter_info = get_data(table_cls(),
{"cmd": "filterinfo",
"name": column['filter_name']})
self.assertTrue(len(filter_info['filter_actions']) > 0,
"Filter %s was defined but no actions "
"added to it" % column['filter_name'])
for filter_action in filter_info['filter_actions']:
# filter string to pass as the option
# This is the name of the filter:action
# e.g. project_filter:not_in_project
filter_string = "%s:%s" % (
column['filter_name'],
filter_action['action_name'])
# Now get the data with the filter applied
filtered_data = get_data(table_cls(),
{"filter": filter_string})
# date range filter actions can't specify the
# number of results they return, so their count is 0
if filter_action['count'] is not None:
self.assertEqual(
len(filtered_data['rows']),
int(filter_action['count']),
"We added a table filter for %s but "
"the number of rows returned was not "
"what the filter info said there "
"would be" % name)
# Test search functionality on the table
something_found = False
for search in list(string.ascii_letters):
search_data = get_data(table_cls(), {'search': search})
if len(search_data['rows']) > 0:
something_found = True
break
self.assertTrue(something_found,
"We went through the whole alphabet and nothing"
" was found for the search of table %s" % name)
# Test the limit functionality on the table
limited_data = get_data(table_cls(), {'limit': "1"})
self.assertEqual(len(limited_data['rows']),
1,
"Limit 1 set on table %s but not 1 row returned"
% name)
# Test the pagination functionality on the table
page_one_data = get_data(table_cls(), {'limit': "1",
"page": "1"})['rows'][0]
page_two_data = get_data(table_cls(), {'limit': "1",
"page": "2"})['rows'][0]
self.assertNotEqual(page_one_data,
page_two_data,
"Changed page on table %s but first row is"
" the same as the previous page" % name)