Complete Yocto mirror with license table for TQMa6UL (2038-compliance)
- 264 license table entries with exact download URLs (224/264 resolved) - Complete sources/ directory with all BitBake recipes - Build configuration: tqma6ul-multi-mba6ulx, spaetzle (musl) - Full traceability for Softwarefreigabeantrag - GCC 13.4.0, Linux 6.6.102, U-Boot 2023.04, musl 1.2.4 - License distribution: GPL-2.0 (24), MIT (23), GPL-2.0+ (18), BSD-3 (16)
This commit is contained in:
0
sources/poky/bitbake/lib/toaster/tests/__init__.py
Normal file
0
sources/poky/bitbake/lib/toaster/tests/__init__.py
Normal file
74
sources/poky/bitbake/lib/toaster/tests/browser/README
Normal file
74
sources/poky/bitbake/lib/toaster/tests/browser/README
Normal 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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')
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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")
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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))
|
||||
@@ -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')
|
||||
@@ -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)
|
||||
@@ -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")
|
||||
@@ -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')
|
||||
@@ -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')
|
||||
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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')
|
||||
@@ -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')
|
||||
14
sources/poky/bitbake/lib/toaster/tests/builds/README
Normal file
14
sources/poky/bitbake/lib/toaster/tests/builds/README
Normal 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.
|
||||
166
sources/poky/bitbake/lib/toaster/tests/builds/buildtest.py
Normal file
166
sources/poky/bitbake/lib/toaster/tests/builds/buildtest.py
Normal 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
|
||||
@@ -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")
|
||||
@@ -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)")
|
||||
@@ -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!")
|
||||
@@ -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)
|
||||
58
sources/poky/bitbake/lib/toaster/tests/db/test_db.py
Normal file
58
sources/poky/bitbake/lib/toaster/tests/db/test_db.py
Normal 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")
|
||||
22
sources/poky/bitbake/lib/toaster/tests/eventreplay/README
Normal file
22
sources/poky/bitbake/lib/toaster/tests/eventreplay/README
Normal 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.
|
||||
@@ -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))
|
||||
@@ -0,0 +1,138 @@
|
||||
#! /usr/bin/env python3
|
||||
#
|
||||
# BitBake Toaster functional tests implementation
|
||||
#
|
||||
# Copyright (C) 2017 Intel Corporation
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
|
||||
import os
|
||||
import logging
|
||||
import subprocess
|
||||
import signal
|
||||
import re
|
||||
|
||||
from tests.browser.selenium_helpers_base import SeleniumTestCaseBase
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.common.exceptions import NoSuchElementException
|
||||
|
||||
logger = logging.getLogger("toaster")
|
||||
toaster_processes = []
|
||||
|
||||
class SeleniumFunctionalTestCase(SeleniumTestCaseBase):
|
||||
wait_toaster_time = 10
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
# So that the buildinfo helper uses the test database'
|
||||
if os.environ.get('DJANGO_SETTINGS_MODULE', '') != \
|
||||
'toastermain.settings_test':
|
||||
raise RuntimeError("Please initialise django with the tests settings: "
|
||||
"DJANGO_SETTINGS_MODULE='toastermain.settings_test'")
|
||||
|
||||
# Wait for any known toaster processes to exit
|
||||
global toaster_processes
|
||||
for toaster_process in toaster_processes:
|
||||
try:
|
||||
os.waitpid(toaster_process, os.WNOHANG)
|
||||
except ChildProcessError:
|
||||
pass
|
||||
|
||||
# start toaster
|
||||
cmd = "bash -c 'source toaster start'"
|
||||
start_process = subprocess.Popen(
|
||||
cmd,
|
||||
cwd=os.environ.get("BUILDDIR"),
|
||||
shell=True)
|
||||
toaster_processes = [start_process.pid]
|
||||
if start_process.wait() != 0:
|
||||
port_use = os.popen("lsof -i -P -n | grep '8000 (LISTEN)'").read().strip()
|
||||
message = ''
|
||||
if port_use:
|
||||
process_id = port_use.split()[1]
|
||||
process = os.popen(f"ps -o cmd= -p {process_id}").read().strip()
|
||||
message = f"Port 8000 occupied by {process}"
|
||||
raise RuntimeError(f"Can't initialize toaster. {message}")
|
||||
|
||||
builddir = os.environ.get("BUILDDIR")
|
||||
with open(os.path.join(builddir, '.toastermain.pid'), 'r') as f:
|
||||
toaster_processes.append(int(f.read()))
|
||||
with open(os.path.join(builddir, '.runbuilds.pid'), 'r') as f:
|
||||
toaster_processes.append(int(f.read()))
|
||||
|
||||
super(SeleniumFunctionalTestCase, cls).setUpClass()
|
||||
cls.live_server_url = 'http://localhost:8000/'
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
super(SeleniumFunctionalTestCase, cls).tearDownClass()
|
||||
|
||||
global toaster_processes
|
||||
|
||||
cmd = "bash -c 'source toaster stop'"
|
||||
stop_process = subprocess.Popen(
|
||||
cmd,
|
||||
cwd=os.environ.get("BUILDDIR"),
|
||||
shell=True)
|
||||
# Toaster stop has been known to hang in these tests so force kill if it stalls
|
||||
try:
|
||||
if stop_process.wait(cls.wait_toaster_time) != 0:
|
||||
raise Exception('Toaster stop process failed')
|
||||
except Exception as e:
|
||||
if e is subprocess.TimeoutExpired:
|
||||
print('Toaster stop process took too long. Force killing toaster...')
|
||||
else:
|
||||
print('Toaster stop process failed. Force killing toaster...')
|
||||
stop_process.kill()
|
||||
for toaster_process in toaster_processes:
|
||||
os.kill(toaster_process, signal.SIGTERM)
|
||||
|
||||
|
||||
def get_URL(self):
|
||||
rc=self.get_page_source()
|
||||
project_url=re.search(r"(projectPageUrl\s:\s\")(.*)(\",)",rc)
|
||||
return project_url.group(2)
|
||||
|
||||
|
||||
def find_element_by_link_text_in_table(self, table_id, link_text):
|
||||
"""
|
||||
Assume there're multiple suitable "find_element_by_link_text".
|
||||
In this circumstance we need to specify "table".
|
||||
"""
|
||||
try:
|
||||
table_element = self.get_table_element(table_id)
|
||||
element = table_element.find_element(By.LINK_TEXT, link_text)
|
||||
except NoSuchElementException:
|
||||
print('no element found')
|
||||
raise
|
||||
return element
|
||||
|
||||
def get_table_element(self, table_id, *coordinate):
|
||||
if len(coordinate) == 0:
|
||||
#return whole-table element
|
||||
element_xpath = "//*[@id='" + table_id + "']"
|
||||
try:
|
||||
element = self.driver.find_element(By.XPATH, element_xpath)
|
||||
except NoSuchElementException:
|
||||
raise
|
||||
return element
|
||||
row = coordinate[0]
|
||||
|
||||
if len(coordinate) == 1:
|
||||
#return whole-row element
|
||||
element_xpath = "//*[@id='" + table_id + "']/tbody/tr[" + str(row) + "]"
|
||||
try:
|
||||
element = self.driver.find_element(By.XPATH, element_xpath)
|
||||
except NoSuchElementException:
|
||||
return False
|
||||
return element
|
||||
#now we are looking for an element with specified X and Y
|
||||
column = coordinate[1]
|
||||
|
||||
element_xpath = "//*[@id='" + table_id + "']/tbody/tr[" + str(row) + "]/td[" + str(column) + "]"
|
||||
try:
|
||||
element = self.driver.find_element(By.XPATH, element_xpath)
|
||||
except NoSuchElementException:
|
||||
return False
|
||||
return element
|
||||
@@ -0,0 +1,179 @@
|
||||
#! /usr/bin/env python3
|
||||
# BitBake Toaster UI tests implementation
|
||||
#
|
||||
# Copyright (C) 2023 Savoir-faire Linux
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
|
||||
import re
|
||||
import pytest
|
||||
from django.urls import reverse
|
||||
from selenium.webdriver.support.select import Select
|
||||
from tests.functional.functional_helpers import SeleniumFunctionalTestCase
|
||||
from orm.models import Project
|
||||
from selenium.webdriver.common.by import By
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.order("last")
|
||||
class TestCreateNewProject(SeleniumFunctionalTestCase):
|
||||
|
||||
def _create_test_new_project(
|
||||
self,
|
||||
project_name,
|
||||
release,
|
||||
release_title,
|
||||
merge_toaster_settings,
|
||||
):
|
||||
""" Create/Test new project using:
|
||||
- Project Name: Any string
|
||||
- Release: Any string
|
||||
- Merge Toaster settings: True or False
|
||||
"""
|
||||
self.get(reverse('newproject'))
|
||||
self.wait_until_visible('#new-project-name', poll=3)
|
||||
self.driver.find_element(By.ID,
|
||||
"new-project-name").send_keys(project_name)
|
||||
|
||||
select = Select(self.find('#projectversion'))
|
||||
select.select_by_value(release)
|
||||
|
||||
# check merge toaster settings
|
||||
checkbox = self.find('.checkbox-mergeattr')
|
||||
if merge_toaster_settings:
|
||||
if not checkbox.is_selected():
|
||||
checkbox.click()
|
||||
else:
|
||||
if checkbox.is_selected():
|
||||
checkbox.click()
|
||||
|
||||
self.driver.find_element(By.ID, "create-project-button").click()
|
||||
|
||||
element = self.wait_until_visible('#project-created-notification', poll=3)
|
||||
self.assertTrue(
|
||||
self.element_exists('#project-created-notification'),
|
||||
f"Project:{project_name} creation notification not shown"
|
||||
)
|
||||
self.assertTrue(
|
||||
project_name in element.text,
|
||||
f"New project name:{project_name} not in new project notification"
|
||||
)
|
||||
self.assertTrue(
|
||||
Project.objects.filter(name=project_name).count(),
|
||||
f"New project:{project_name} not found in database"
|
||||
)
|
||||
|
||||
# check release
|
||||
self.assertTrue(re.search(
|
||||
release_title,
|
||||
self.driver.find_element(By.XPATH,
|
||||
"//span[@id='project-release-title']"
|
||||
).text),
|
||||
'The project release is not defined')
|
||||
|
||||
def test_create_new_project_master(self):
|
||||
""" Test create new project using:
|
||||
- Project Name: Any string
|
||||
- Release: Yocto Project master (option value: 3)
|
||||
- Merge Toaster settings: False
|
||||
"""
|
||||
release = '3'
|
||||
release_title = 'Yocto Project master'
|
||||
project_name = 'projectmaster'
|
||||
self._create_test_new_project(
|
||||
project_name,
|
||||
release,
|
||||
release_title,
|
||||
False,
|
||||
)
|
||||
|
||||
def test_create_new_project_kirkstone(self):
|
||||
""" Test create new project using:
|
||||
- Project Name: Any string
|
||||
- Release: Yocto Project 4.0 "Kirkstone" (option value: 1)
|
||||
- Merge Toaster settings: True
|
||||
"""
|
||||
release = '1'
|
||||
release_title = 'Yocto Project 4.0 "Kirkstone"'
|
||||
project_name = 'projectkirkstone'
|
||||
self._create_test_new_project(
|
||||
project_name,
|
||||
release,
|
||||
release_title,
|
||||
True,
|
||||
)
|
||||
|
||||
def test_create_new_project_dunfell(self):
|
||||
""" Test create new project using:
|
||||
- Project Name: Any string
|
||||
- Release: Yocto Project 3.1 "Dunfell" (option value: 5)
|
||||
- Merge Toaster settings: False
|
||||
"""
|
||||
release = '5'
|
||||
release_title = 'Yocto Project 3.1 "Dunfell"'
|
||||
project_name = 'projectdunfell'
|
||||
self._create_test_new_project(
|
||||
project_name,
|
||||
release,
|
||||
release_title,
|
||||
False,
|
||||
)
|
||||
|
||||
def test_create_new_project_local(self):
|
||||
""" Test create new project using:
|
||||
- Project Name: Any string
|
||||
- Release: Local Yocto Project (option value: 2)
|
||||
- Merge Toaster settings: True
|
||||
"""
|
||||
release = '2'
|
||||
release_title = 'Local Yocto Project'
|
||||
project_name = 'projectlocal'
|
||||
self._create_test_new_project(
|
||||
project_name,
|
||||
release,
|
||||
release_title,
|
||||
True,
|
||||
)
|
||||
|
||||
def test_create_new_project_without_name(self):
|
||||
""" Test create new project without project name """
|
||||
self.get(reverse('newproject'))
|
||||
|
||||
select = Select(self.find('#projectversion'))
|
||||
select.select_by_value(str(3))
|
||||
|
||||
# Check input name has required attribute
|
||||
input_name = self.driver.find_element(By.ID, "new-project-name")
|
||||
self.assertIsNotNone(input_name.get_attribute('required'),
|
||||
'Input name has not required attribute')
|
||||
|
||||
# Check create button is disabled
|
||||
create_btn = self.driver.find_element(By.ID, "create-project-button")
|
||||
self.assertIsNotNone(create_btn.get_attribute('disabled'),
|
||||
'Create button is not disabled')
|
||||
|
||||
def test_import_new_project(self):
|
||||
""" Test import new project using:
|
||||
- Project Name: Any string
|
||||
- Project type: select (Import command line project)
|
||||
- Import existing project directory: Wrong Path
|
||||
"""
|
||||
project_name = 'projectimport'
|
||||
self.get(reverse('newproject'))
|
||||
self.driver.find_element(By.ID,
|
||||
"new-project-name").send_keys(project_name)
|
||||
# select import project
|
||||
self.find('#type-import').click()
|
||||
|
||||
# set wrong path
|
||||
wrong_path = '/wrongpath'
|
||||
self.driver.find_element(By.ID,
|
||||
"import-project-dir").send_keys(wrong_path)
|
||||
self.driver.find_element(By.ID, "create-project-button").click()
|
||||
|
||||
# check error message
|
||||
self.assertTrue(self.element_exists('.alert-danger'),
|
||||
'Allert message not shown')
|
||||
self.assertTrue(wrong_path in self.find('.alert-danger').text,
|
||||
"Wrong path not in alert message")
|
||||
@@ -0,0 +1,257 @@
|
||||
#! /usr/bin/env python3
|
||||
#
|
||||
# BitBake Toaster functional tests implementation
|
||||
#
|
||||
# Copyright (C) 2017 Intel Corporation
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
|
||||
import re
|
||||
from django.urls import reverse
|
||||
import pytest
|
||||
from tests.functional.functional_helpers import SeleniumFunctionalTestCase
|
||||
from orm.models import Project
|
||||
from selenium.webdriver.common.by import By
|
||||
|
||||
from tests.functional.utils import get_projectId_from_url
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.order("second_to_last")
|
||||
class FuntionalTestBasic(SeleniumFunctionalTestCase):
|
||||
"""Basic functional tests for Toaster"""
|
||||
project_id = None
|
||||
|
||||
def setUp(self):
|
||||
super(FuntionalTestBasic, self).setUp()
|
||||
if not FuntionalTestBasic.project_id:
|
||||
self._create_slenium_project()
|
||||
current_url = self.driver.current_url
|
||||
FuntionalTestBasic.project_id = get_projectId_from_url(current_url)
|
||||
|
||||
# testcase (1514)
|
||||
def _create_slenium_project(self):
|
||||
project_name = 'selenium-project'
|
||||
self.get(reverse('newproject'))
|
||||
self.wait_until_visible('#new-project-name', poll=3)
|
||||
self.driver.find_element(By.ID, "new-project-name").send_keys(project_name)
|
||||
self.driver.find_element(By.ID, 'projectversion').click()
|
||||
self.driver.find_element(By.ID, "create-project-button").click()
|
||||
element = self.wait_until_visible('#project-created-notification', poll=10)
|
||||
self.assertTrue(self.element_exists('#project-created-notification'),'Project creation notification not shown')
|
||||
self.assertTrue(project_name in element.text,
|
||||
"New project name not in new project notification")
|
||||
self.assertTrue(Project.objects.filter(name=project_name).count(),
|
||||
"New project not found in database")
|
||||
return Project.objects.last().id
|
||||
|
||||
# testcase (1515)
|
||||
def test_verify_left_bar_menu(self):
|
||||
self.get(reverse('all-projects'))
|
||||
self.wait_until_present('#projectstable', poll=10)
|
||||
self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click()
|
||||
self.wait_until_present('#config-nav', poll=10)
|
||||
self.assertTrue(self.element_exists('#config-nav'),'Configuration Tab does not exist')
|
||||
project_URL=self.get_URL()
|
||||
self.driver.find_element(By.XPATH, '//a[@href="'+project_URL+'"]').click()
|
||||
self.wait_until_present('#config-nav', poll=10)
|
||||
|
||||
try:
|
||||
self.driver.find_element(By.XPATH, "//*[@id='config-nav']/ul/li/a[@href="+'"'+project_URL+'customimages/"'+"]").click()
|
||||
self.wait_until_present('#config-nav', poll=10)
|
||||
self.assertTrue(re.search("Custom images",self.driver.find_element(By.XPATH, "//div[@class='col-md-10']").text),'Custom images information is not loading properly')
|
||||
except:
|
||||
self.fail(msg='No Custom images tab available')
|
||||
|
||||
try:
|
||||
self.driver.find_element(By.XPATH, "//*[@id='config-nav']/ul/li/a[@href="+'"'+project_URL+'images/"'+"]").click()
|
||||
self.wait_until_present('#config-nav', poll=10)
|
||||
self.assertTrue(re.search("Compatible image recipes",self.driver.find_element(By.XPATH, "//div[@class='col-md-10']").text),'The Compatible image recipes information is not loading properly')
|
||||
except:
|
||||
self.fail(msg='No Compatible image tab available')
|
||||
|
||||
try:
|
||||
self.driver.find_element(By.XPATH, "//*[@id='config-nav']/ul/li/a[@href="+'"'+project_URL+'softwarerecipes/"'+"]").click()
|
||||
self.wait_until_present('#config-nav', poll=10)
|
||||
self.assertTrue(re.search("Compatible software recipes",self.driver.find_element(By.XPATH, "//div[@class='col-md-10']").text),'The Compatible software recipe information is not loading properly')
|
||||
except:
|
||||
self.fail(msg='No Compatible software recipe tab available')
|
||||
|
||||
try:
|
||||
self.driver.find_element(By.XPATH, "//*[@id='config-nav']/ul/li/a[@href="+'"'+project_URL+'machines/"'+"]").click()
|
||||
self.wait_until_present('#config-nav', poll=10)
|
||||
self.assertTrue(re.search("Compatible machines",self.driver.find_element(By.XPATH, "//div[@class='col-md-10']").text),'The Compatible machine information is not loading properly')
|
||||
except:
|
||||
self.fail(msg='No Compatible machines tab available')
|
||||
|
||||
try:
|
||||
self.driver.find_element(By.XPATH, "//*[@id='config-nav']/ul/li/a[@href="+'"'+project_URL+'layers/"'+"]").click()
|
||||
self.wait_until_present('#config-nav', poll=10)
|
||||
self.assertTrue(re.search("Compatible layers",self.driver.find_element(By.XPATH, "//div[@class='col-md-10']").text),'The Compatible layer information is not loading properly')
|
||||
except:
|
||||
self.fail(msg='No Compatible layers tab available')
|
||||
|
||||
try:
|
||||
self.driver.find_element(By.XPATH, "//*[@id='config-nav']/ul/li/a[@href="+'"'+project_URL+'configuration"'+"]").click()
|
||||
self.wait_until_present('#config-nav', poll=10)
|
||||
self.assertTrue(re.search("Bitbake variables",self.driver.find_element(By.XPATH, "//div[@class='col-md-10']").text),'The Bitbake variables information is not loading properly')
|
||||
except:
|
||||
self.fail(msg='No Bitbake variables tab available')
|
||||
|
||||
# testcase (1516)
|
||||
def test_review_configuration_information(self):
|
||||
self.get(reverse('all-projects'))
|
||||
self.wait_until_present('#projectstable', poll=10)
|
||||
self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click()
|
||||
project_URL=self.get_URL()
|
||||
self.wait_until_present('#config-nav', poll=10)
|
||||
try:
|
||||
self.assertTrue(self.element_exists('#machine-section'),'Machine section for the project configuration page does not exist')
|
||||
self.assertTrue(re.search("qemux86-64",self.driver.find_element(By.XPATH, "//span[@id='project-machine-name']").text),'The machine type is not assigned')
|
||||
self.driver.find_element(By.XPATH, "//span[@id='change-machine-toggle']").click()
|
||||
self.wait_until_visible('#select-machine-form', poll=10)
|
||||
self.wait_until_visible('#cancel-machine-change', poll=10)
|
||||
self.driver.find_element(By.XPATH, "//form[@id='select-machine-form']/a[@id='cancel-machine-change']").click()
|
||||
except:
|
||||
self.fail(msg='The machine information is wrong in the configuration page')
|
||||
|
||||
try:
|
||||
self.driver.find_element(By.ID, 'no-most-built')
|
||||
except:
|
||||
self.fail(msg='No Most built information in project detail page')
|
||||
|
||||
try:
|
||||
self.assertTrue(re.search("Yocto Project master",self.driver.find_element(By.XPATH, "//span[@id='project-release-title']").text),'The project release is not defined')
|
||||
except:
|
||||
self.fail(msg='No project release title information in project detail page')
|
||||
|
||||
try:
|
||||
self.driver.find_element(By.XPATH, "//div[@id='layer-container']")
|
||||
self.assertTrue(re.search("3",self.driver.find_element(By.ID, "project-layers-count").text),'There should be 3 layers listed in the layer count')
|
||||
layer_list = self.driver.find_element(By.ID, "layers-in-project-list")
|
||||
layers = layer_list.find_elements(By.TAG_NAME, "li")
|
||||
for layer in layers:
|
||||
if re.match ("openembedded-core",layer.text):
|
||||
print ("openembedded-core layer is a default layer in the project configuration")
|
||||
elif re.match ("meta-poky",layer.text):
|
||||
print ("meta-poky layer is a default layer in the project configuration")
|
||||
elif re.match ("meta-yocto-bsp",layer.text):
|
||||
print ("meta-yocto-bsp is a default layer in the project configuratoin")
|
||||
else:
|
||||
self.fail(msg='default layers are missing from the project configuration')
|
||||
except:
|
||||
self.fail(msg='No Layer information in project detail page')
|
||||
|
||||
# testcase (1517)
|
||||
def test_verify_machine_information(self):
|
||||
self.get(reverse('all-projects'))
|
||||
self.wait_until_present('#projectstable', poll=10)
|
||||
self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click()
|
||||
self.wait_until_present('#config-nav', poll=10)
|
||||
|
||||
try:
|
||||
self.assertTrue(self.element_exists('#machine-section'),'Machine section for the project configuration page does not exist')
|
||||
self.assertTrue(re.search("qemux86-64",self.driver.find_element(By.ID, "project-machine-name").text),'The machine type is not assigned')
|
||||
self.driver.find_element(By.ID, "change-machine-toggle").click()
|
||||
self.wait_until_visible('#select-machine-form', poll=10)
|
||||
self.wait_until_visible('#cancel-machine-change', poll=10)
|
||||
self.driver.find_element(By.ID, "cancel-machine-change").click()
|
||||
except:
|
||||
self.fail(msg='The machine information is wrong in the configuration page')
|
||||
|
||||
# testcase (1518)
|
||||
def test_verify_most_built_recipes_information(self):
|
||||
self.get(reverse('all-projects'))
|
||||
self.wait_until_present('#projectstable', poll=10)
|
||||
self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click()
|
||||
self.wait_until_present('#config-nav', poll=10)
|
||||
project_URL=self.get_URL()
|
||||
try:
|
||||
self.assertTrue(re.search("You haven't built any recipes yet",self.driver.find_element(By.ID, "no-most-built").text),'Default message of no builds is not present')
|
||||
self.driver.find_element(By.XPATH, "//div[@id='no-most-built']/p/a[@href="+'"'+project_URL+'images/"'+"]").click()
|
||||
self.wait_until_present('#config-nav', poll=10)
|
||||
self.assertTrue(re.search("Compatible image recipes",self.driver.find_element(By.XPATH, "//div[@class='col-md-10']").text),'The Choose a recipe to build link is not working properly')
|
||||
except:
|
||||
self.fail(msg='No Most built information in project detail page')
|
||||
|
||||
# testcase (1519)
|
||||
def test_verify_project_release_information(self):
|
||||
self.get(reverse('all-projects'))
|
||||
self.wait_until_present('#projectstable', poll=10)
|
||||
self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click()
|
||||
self.wait_until_present('#config-nav', poll=10)
|
||||
|
||||
try:
|
||||
self.assertTrue(re.search("Yocto Project master",self.driver.find_element(By.ID, "project-release-title").text),'The project release is not defined')
|
||||
except:
|
||||
self.fail(msg='No project release title information in project detail page')
|
||||
|
||||
# testcase (1520)
|
||||
def test_verify_layer_information(self):
|
||||
self.get(reverse('all-projects'))
|
||||
self.wait_until_present('#projectstable', poll=10)
|
||||
self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click()
|
||||
self.wait_until_present('#config-nav', poll=10)
|
||||
project_URL=self.get_URL()
|
||||
try:
|
||||
self.driver.find_element(By.XPATH, "//div[@id='layer-container']")
|
||||
self.assertTrue(re.search("3",self.driver.find_element(By.ID, "project-layers-count").text),'There should be 3 layers listed in the layer count')
|
||||
layer_list = self.driver.find_element(By.ID, "layers-in-project-list")
|
||||
layers = layer_list.find_elements(By.TAG_NAME, "li")
|
||||
|
||||
for layer in layers:
|
||||
if re.match ("openembedded-core",layer.text):
|
||||
print ("openembedded-core layer is a default layer in the project configuration")
|
||||
elif re.match ("meta-poky",layer.text):
|
||||
print ("meta-poky layer is a default layer in the project configuration")
|
||||
elif re.match ("meta-yocto-bsp",layer.text):
|
||||
print ("meta-yocto-bsp is a default layer in the project configuratoin")
|
||||
else:
|
||||
self.fail(msg='default layers are missing from the project configuration')
|
||||
|
||||
self.driver.find_element(By.XPATH, "//input[@id='layer-add-input']")
|
||||
self.driver.find_element(By.XPATH, "//button[@id='add-layer-btn']")
|
||||
self.driver.find_element(By.XPATH, "//div[@id='layer-container']/form[@class='form-inline']/p/a[@id='view-compatible-layers']")
|
||||
self.driver.find_element(By.XPATH, "//div[@id='layer-container']/form[@class='form-inline']/p/a[@href="+'"'+project_URL+'importlayer"'+"]")
|
||||
except:
|
||||
self.fail(msg='No Layer information in project detail page')
|
||||
|
||||
# testcase (1521)
|
||||
def test_verify_project_detail_links(self):
|
||||
self.get(reverse('all-projects'))
|
||||
self.wait_until_present('#projectstable', poll=10)
|
||||
self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click()
|
||||
self.wait_until_present('#config-nav', poll=10)
|
||||
project_URL=self.get_URL()
|
||||
self.driver.find_element(By.XPATH, "//div[@id='project-topbar']/ul[@class='nav nav-tabs']/li[@id='topbar-configuration-tab']/a[@href="+'"'+project_URL+'"'+"]").click()
|
||||
self.wait_until_present('#config-nav', poll=10)
|
||||
self.assertTrue(re.search("Configuration",self.driver.find_element(By.XPATH, "//div[@id='project-topbar']/ul[@class='nav nav-tabs']/li[@id='topbar-configuration-tab']/a[@href="+'"'+project_URL+'"'+"]").text), 'Configuration tab in project topbar is misspelled')
|
||||
|
||||
try:
|
||||
self.driver.find_element(By.XPATH, "//div[@id='project-topbar']/ul[@class='nav nav-tabs']/li/a[@href="+'"'+project_URL+'builds/"'+"]").click()
|
||||
self.wait_until_visible('#project-topbar', poll=10)
|
||||
self.assertTrue(re.search("Builds",self.driver.find_element(By.XPATH, "//div[@id='project-topbar']/ul[@class='nav nav-tabs']/li/a[@href="+'"'+project_URL+'builds/"'+"]").text), 'Builds tab in project topbar is misspelled')
|
||||
self.driver.find_element(By.XPATH, "//div[@id='empty-state-projectbuildstable']")
|
||||
except:
|
||||
self.fail(msg='Builds tab information is not present')
|
||||
|
||||
try:
|
||||
self.driver.find_element(By.XPATH, "//div[@id='project-topbar']/ul[@class='nav nav-tabs']/li/a[@href="+'"'+project_URL+'importlayer"'+"]").click()
|
||||
self.wait_until_visible('#project-topbar', poll=10)
|
||||
self.assertTrue(re.search("Import layer",self.driver.find_element(By.XPATH, "//div[@id='project-topbar']/ul[@class='nav nav-tabs']/li/a[@href="+'"'+project_URL+'importlayer"'+"]").text), 'Import layer tab in project topbar is misspelled')
|
||||
self.driver.find_element(By.XPATH, "//fieldset[@id='repo-select']")
|
||||
self.driver.find_element(By.XPATH, "//fieldset[@id='git-repo']")
|
||||
except:
|
||||
self.fail(msg='Import layer tab not loading properly')
|
||||
|
||||
try:
|
||||
self.driver.find_element(By.XPATH, "//div[@id='project-topbar']/ul[@class='nav nav-tabs']/li/a[@href="+'"'+project_URL+'newcustomimage/"'+"]").click()
|
||||
self.wait_until_visible('#project-topbar', poll=10)
|
||||
self.assertTrue(re.search("New custom image",self.driver.find_element(By.XPATH, "//div[@id='project-topbar']/ul[@class='nav nav-tabs']/li/a[@href="+'"'+project_URL+'newcustomimage/"'+"]").text), 'New custom image tab in project topbar is misspelled')
|
||||
self.assertTrue(re.search("Select the image recipe you want to customise",self.driver.find_element(By.XPATH, "//div[@class='col-md-12']/h2").text),'The new custom image tab is not loading correctly')
|
||||
except:
|
||||
self.fail(msg='New custom image tab not loading properly')
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,341 @@
|
||||
#! /usr/bin/env python3 #
|
||||
# BitBake Toaster UI tests implementation
|
||||
#
|
||||
# Copyright (C) 2023 Savoir-faire Linux
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
|
||||
import string
|
||||
import random
|
||||
import pytest
|
||||
from django.urls import reverse
|
||||
from selenium.webdriver import Keys
|
||||
from selenium.webdriver.support.select import Select
|
||||
from selenium.common.exceptions import TimeoutException
|
||||
from tests.functional.functional_helpers import SeleniumFunctionalTestCase
|
||||
from selenium.webdriver.common.by import By
|
||||
|
||||
from .utils import get_projectId_from_url
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.order("last")
|
||||
class TestProjectConfig(SeleniumFunctionalTestCase):
|
||||
project_id = None
|
||||
PROJECT_NAME = 'TestProjectConfig'
|
||||
INVALID_PATH_START_TEXT = 'The directory path should either start with a /'
|
||||
INVALID_PATH_CHAR_TEXT = 'The directory path cannot include spaces or ' \
|
||||
'any of these characters'
|
||||
|
||||
def _create_project(self, project_name):
|
||||
""" Create/Test new project using:
|
||||
- Project Name: Any string
|
||||
- Release: Any string
|
||||
- Merge Toaster settings: True or False
|
||||
"""
|
||||
self.get(reverse('newproject'))
|
||||
self.wait_until_visible('#new-project-name', poll=2)
|
||||
self.find("#new-project-name").send_keys(project_name)
|
||||
select = Select(self.find("#projectversion"))
|
||||
select.select_by_value('3')
|
||||
|
||||
# check merge toaster settings
|
||||
checkbox = self.find('.checkbox-mergeattr')
|
||||
if not checkbox.is_selected():
|
||||
checkbox.click()
|
||||
|
||||
if self.PROJECT_NAME != 'TestProjectConfig':
|
||||
# Reset project name if it's not the default one
|
||||
self.PROJECT_NAME = 'TestProjectConfig'
|
||||
|
||||
self.find("#create-project-button").click()
|
||||
|
||||
try:
|
||||
self.wait_until_visible('#hint-error-project-name', poll=2)
|
||||
url = reverse('project', args=(TestProjectConfig.project_id, ))
|
||||
self.get(url)
|
||||
self.wait_until_visible('#config-nav', poll=3)
|
||||
except TimeoutException:
|
||||
self.wait_until_visible('#config-nav', poll=3)
|
||||
|
||||
def _random_string(self, length):
|
||||
return ''.join(
|
||||
random.choice(string.ascii_letters) for _ in range(length)
|
||||
)
|
||||
|
||||
def _get_config_nav_item(self, index):
|
||||
config_nav = self.find('#config-nav')
|
||||
return config_nav.find_elements(By.TAG_NAME, 'li')[index]
|
||||
|
||||
def _navigate_bbv_page(self):
|
||||
""" Navigate to project BitBake variables page """
|
||||
# check if the menu is displayed
|
||||
if TestProjectConfig.project_id is None:
|
||||
self._create_project(project_name=self._random_string(10))
|
||||
current_url = self.driver.current_url
|
||||
TestProjectConfig.project_id = get_projectId_from_url(current_url)
|
||||
else:
|
||||
url = reverse('projectconf', args=(TestProjectConfig.project_id,))
|
||||
self.get(url)
|
||||
self.wait_until_visible('#config-nav', poll=3)
|
||||
bbv_page_link = self._get_config_nav_item(9)
|
||||
bbv_page_link.click()
|
||||
self.wait_until_visible('#config-nav', poll=3)
|
||||
|
||||
def test_no_underscore_iamgefs_type(self):
|
||||
"""
|
||||
Should not accept IMAGEFS_TYPE with an underscore
|
||||
"""
|
||||
self._navigate_bbv_page()
|
||||
imagefs_type = "foo_bar"
|
||||
|
||||
self.wait_until_visible('#change-image_fstypes-icon', poll=2)
|
||||
|
||||
self.click('#change-image_fstypes-icon')
|
||||
|
||||
self.enter_text('#new-imagefs_types', imagefs_type)
|
||||
|
||||
element = self.wait_until_visible('#hintError-image-fs_type', poll=2)
|
||||
|
||||
self.assertTrue(("A valid image type cannot include underscores" in element.text),
|
||||
"Did not find underscore error message")
|
||||
|
||||
def test_checkbox_verification(self):
|
||||
"""
|
||||
Should automatically check the checkbox if user enters value
|
||||
text box, if value is there in the checkbox.
|
||||
"""
|
||||
self._navigate_bbv_page()
|
||||
|
||||
imagefs_type = "btrfs"
|
||||
|
||||
self.wait_until_visible('#change-image_fstypes-icon', poll=2)
|
||||
|
||||
self.click('#change-image_fstypes-icon')
|
||||
|
||||
self.enter_text('#new-imagefs_types', imagefs_type)
|
||||
|
||||
checkboxes = self.driver.find_elements(By.XPATH, "//input[@class='fs-checkbox-fstypes']")
|
||||
|
||||
for checkbox in checkboxes:
|
||||
if checkbox.get_attribute("value") == "btrfs":
|
||||
self.assertEqual(checkbox.is_selected(), True)
|
||||
|
||||
def test_textbox_with_checkbox_verification(self):
|
||||
"""
|
||||
Should automatically add or remove value in textbox, if user checks
|
||||
or unchecks checkboxes.
|
||||
"""
|
||||
self._navigate_bbv_page()
|
||||
|
||||
self.wait_until_visible('#change-image_fstypes-icon', poll=2)
|
||||
|
||||
self.click('#change-image_fstypes-icon')
|
||||
|
||||
checkboxes_selector = '.fs-checkbox-fstypes'
|
||||
|
||||
self.wait_until_visible(checkboxes_selector, poll=2)
|
||||
checkboxes = self.find_all(checkboxes_selector)
|
||||
|
||||
for checkbox in checkboxes:
|
||||
if checkbox.get_attribute("value") == "cpio":
|
||||
checkbox.click()
|
||||
element = self.driver.find_element(By.ID, 'new-imagefs_types')
|
||||
|
||||
self.wait_until_visible('#new-imagefs_types', poll=2)
|
||||
|
||||
self.assertTrue(("cpio" in element.get_attribute('value'),
|
||||
"Imagefs not added into the textbox"))
|
||||
checkbox.click()
|
||||
self.assertTrue(("cpio" not in element.text),
|
||||
"Image still present in the textbox")
|
||||
|
||||
def test_set_download_dir(self):
|
||||
"""
|
||||
Validate the allowed and disallowed types in the directory field for
|
||||
DL_DIR
|
||||
"""
|
||||
self._navigate_bbv_page()
|
||||
|
||||
# activate the input to edit download dir
|
||||
try:
|
||||
change_dl_dir_btn = self.wait_until_visible('#change-dl_dir-icon', poll=2)
|
||||
except TimeoutException:
|
||||
# If download dir is not displayed, test is skipped
|
||||
change_dl_dir_btn = None
|
||||
|
||||
if change_dl_dir_btn:
|
||||
change_dl_dir_btn = self.wait_until_visible('#change-dl_dir-icon', poll=2)
|
||||
change_dl_dir_btn.click()
|
||||
|
||||
# downloads dir path doesn't start with / or ${...}
|
||||
input_field = self.wait_until_visible('#new-dl_dir', poll=2)
|
||||
input_field.clear()
|
||||
self.enter_text('#new-dl_dir', 'home/foo')
|
||||
element = self.wait_until_visible('#hintError-initialChar-dl_dir', poll=2)
|
||||
|
||||
msg = 'downloads directory path starts with invalid character but ' \
|
||||
'treated as valid'
|
||||
self.assertTrue((self.INVALID_PATH_START_TEXT in element.text), msg)
|
||||
|
||||
# downloads dir path has a space
|
||||
self.driver.find_element(By.ID, 'new-dl_dir').clear()
|
||||
self.enter_text('#new-dl_dir', '/foo/bar a')
|
||||
|
||||
element = self.wait_until_visible('#hintError-dl_dir', poll=2)
|
||||
msg = 'downloads directory path characters invalid but treated as valid'
|
||||
self.assertTrue((self.INVALID_PATH_CHAR_TEXT in element.text), msg)
|
||||
|
||||
# downloads dir path starts with ${...} but has a space
|
||||
self.driver.find_element(By.ID,'new-dl_dir').clear()
|
||||
self.enter_text('#new-dl_dir', '${TOPDIR}/down foo')
|
||||
|
||||
element = self.wait_until_visible('#hintError-dl_dir', poll=2)
|
||||
msg = 'downloads directory path characters invalid but treated as valid'
|
||||
self.assertTrue((self.INVALID_PATH_CHAR_TEXT in element.text), msg)
|
||||
|
||||
# downloads dir path starts with /
|
||||
self.driver.find_element(By.ID,'new-dl_dir').clear()
|
||||
self.enter_text('#new-dl_dir', '/bar/foo')
|
||||
|
||||
hidden_element = self.driver.find_element(By.ID,'hintError-dl_dir')
|
||||
self.assertEqual(hidden_element.is_displayed(), False,
|
||||
'downloads directory path valid but treated as invalid')
|
||||
|
||||
# downloads dir path starts with ${...}
|
||||
self.driver.find_element(By.ID,'new-dl_dir').clear()
|
||||
self.enter_text('#new-dl_dir', '${TOPDIR}/down')
|
||||
|
||||
hidden_element = self.driver.find_element(By.ID,'hintError-dl_dir')
|
||||
self.assertEqual(hidden_element.is_displayed(), False,
|
||||
'downloads directory path valid but treated as invalid')
|
||||
|
||||
def test_set_sstate_dir(self):
|
||||
"""
|
||||
Validate the allowed and disallowed types in the directory field for
|
||||
SSTATE_DIR
|
||||
"""
|
||||
self._navigate_bbv_page()
|
||||
|
||||
try:
|
||||
btn_chg_sstate_dir = self.wait_until_visible(
|
||||
'#change-sstate_dir-icon',
|
||||
poll=2
|
||||
)
|
||||
self.click('#change-sstate_dir-icon')
|
||||
except TimeoutException:
|
||||
# If sstate_dir is not displayed, test is skipped
|
||||
btn_chg_sstate_dir = None
|
||||
|
||||
if btn_chg_sstate_dir: # Skip continuation if sstate_dir is not displayed
|
||||
# path doesn't start with / or ${...}
|
||||
input_field = self.wait_until_visible('#new-sstate_dir', poll=2)
|
||||
input_field.clear()
|
||||
self.enter_text('#new-sstate_dir', 'home/foo')
|
||||
element = self.wait_until_visible('#hintError-initialChar-sstate_dir', poll=2)
|
||||
|
||||
msg = 'sstate directory path starts with invalid character but ' \
|
||||
'treated as valid'
|
||||
self.assertTrue((self.INVALID_PATH_START_TEXT in element.text), msg)
|
||||
|
||||
# path has a space
|
||||
self.driver.find_element(By.ID, 'new-sstate_dir').clear()
|
||||
self.enter_text('#new-sstate_dir', '/foo/bar a')
|
||||
|
||||
element = self.wait_until_visible('#hintError-sstate_dir', poll=2)
|
||||
msg = 'sstate directory path characters invalid but treated as valid'
|
||||
self.assertTrue((self.INVALID_PATH_CHAR_TEXT in element.text), msg)
|
||||
|
||||
# path starts with ${...} but has a space
|
||||
self.driver.find_element(By.ID,'new-sstate_dir').clear()
|
||||
self.enter_text('#new-sstate_dir', '${TOPDIR}/down foo')
|
||||
|
||||
element = self.wait_until_visible('#hintError-sstate_dir', poll=2)
|
||||
msg = 'sstate directory path characters invalid but treated as valid'
|
||||
self.assertTrue((self.INVALID_PATH_CHAR_TEXT in element.text), msg)
|
||||
|
||||
# path starts with /
|
||||
self.driver.find_element(By.ID,'new-sstate_dir').clear()
|
||||
self.enter_text('#new-sstate_dir', '/bar/foo')
|
||||
|
||||
hidden_element = self.driver.find_element(By.ID, 'hintError-sstate_dir')
|
||||
self.assertEqual(hidden_element.is_displayed(), False,
|
||||
'sstate directory path valid but treated as invalid')
|
||||
|
||||
# paths starts with ${...}
|
||||
self.driver.find_element(By.ID, 'new-sstate_dir').clear()
|
||||
self.enter_text('#new-sstate_dir', '${TOPDIR}/down')
|
||||
|
||||
hidden_element = self.driver.find_element(By.ID, 'hintError-sstate_dir')
|
||||
self.assertEqual(hidden_element.is_displayed(), False,
|
||||
'sstate directory path valid but treated as invalid')
|
||||
|
||||
def _change_bbv_value(self, **kwargs):
|
||||
var_name, field, btn_id, input_id, value, save_btn, *_ = kwargs.values()
|
||||
""" Change bitbake variable value """
|
||||
self._navigate_bbv_page()
|
||||
self.wait_until_visible(f'#{btn_id}', poll=2)
|
||||
if kwargs.get('new_variable'):
|
||||
self.find(f"#{btn_id}").clear()
|
||||
self.enter_text(f"#{btn_id}", f"{var_name}")
|
||||
else:
|
||||
self.click(f'#{btn_id}')
|
||||
self.wait_until_visible(f'#{input_id}', poll=2)
|
||||
|
||||
if kwargs.get('is_select'):
|
||||
select = Select(self.find(f'#{input_id}'))
|
||||
select.select_by_visible_text(value)
|
||||
else:
|
||||
self.find(f"#{input_id}").clear()
|
||||
self.enter_text(f'#{input_id}', f'{value}')
|
||||
self.click(f'#{save_btn}')
|
||||
value_displayed = str(self.wait_until_visible(f'#{field}').text).lower()
|
||||
msg = f'{var_name} variable not changed'
|
||||
self.assertTrue(str(value).lower() in value_displayed, msg)
|
||||
|
||||
def test_change_distro_var(self):
|
||||
""" Test changing distro variable """
|
||||
self._change_bbv_value(
|
||||
var_name='DISTRO',
|
||||
field='distro',
|
||||
btn_id='change-distro-icon',
|
||||
input_id='new-distro',
|
||||
value='poky-changed',
|
||||
save_btn="apply-change-distro",
|
||||
)
|
||||
|
||||
def test_set_image_install_append_var(self):
|
||||
""" Test setting IMAGE_INSTALL:append variable """
|
||||
self._change_bbv_value(
|
||||
var_name='IMAGE_INSTALL:append',
|
||||
field='image_install',
|
||||
btn_id='change-image_install-icon',
|
||||
input_id='new-image_install',
|
||||
value='bash, apt, busybox',
|
||||
save_btn="apply-change-image_install",
|
||||
)
|
||||
|
||||
def test_set_package_classes_var(self):
|
||||
""" Test setting PACKAGE_CLASSES variable """
|
||||
self._change_bbv_value(
|
||||
var_name='PACKAGE_CLASSES',
|
||||
field='package_classes',
|
||||
btn_id='change-package_classes-icon',
|
||||
input_id='package_classes-select',
|
||||
value='package_deb',
|
||||
save_btn="apply-change-package_classes",
|
||||
is_select=True,
|
||||
)
|
||||
|
||||
def test_create_new_bbv(self):
|
||||
""" Test creating new bitbake variable """
|
||||
self._change_bbv_value(
|
||||
var_name='New_Custom_Variable',
|
||||
field='configvar-list',
|
||||
btn_id='variable',
|
||||
input_id='value',
|
||||
value='new variable value',
|
||||
save_btn="add-configvar-button",
|
||||
new_variable=True
|
||||
)
|
||||
@@ -0,0 +1,792 @@
|
||||
#! /usr/bin/env python3 #
|
||||
# BitBake Toaster UI tests implementation
|
||||
#
|
||||
# Copyright (C) 2023 Savoir-faire Linux
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
|
||||
import os
|
||||
import random
|
||||
import string
|
||||
from unittest import skip
|
||||
import pytest
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
from selenium.webdriver.support.select import Select
|
||||
from selenium.common.exceptions import TimeoutException
|
||||
from tests.functional.functional_helpers import SeleniumFunctionalTestCase
|
||||
from orm.models import Build, Project, Target
|
||||
from selenium.webdriver.common.by import By
|
||||
|
||||
from .utils import get_projectId_from_url, wait_until_build, wait_until_build_cancelled
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.order("last")
|
||||
class TestProjectPage(SeleniumFunctionalTestCase):
|
||||
project_id = None
|
||||
PROJECT_NAME = 'TestProjectPage'
|
||||
|
||||
def _create_project(self, project_name):
|
||||
""" Create/Test new project using:
|
||||
- Project Name: Any string
|
||||
- Release: Any string
|
||||
- Merge Toaster settings: True or False
|
||||
"""
|
||||
self.get(reverse('newproject'))
|
||||
self.wait_until_visible('#new-project-name')
|
||||
self.find("#new-project-name").send_keys(project_name)
|
||||
select = Select(self.find("#projectversion"))
|
||||
select.select_by_value('3')
|
||||
|
||||
# check merge toaster settings
|
||||
checkbox = self.find('.checkbox-mergeattr')
|
||||
if not checkbox.is_selected():
|
||||
checkbox.click()
|
||||
|
||||
if self.PROJECT_NAME != 'TestProjectPage':
|
||||
# Reset project name if it's not the default one
|
||||
self.PROJECT_NAME = 'TestProjectPage'
|
||||
|
||||
self.find("#create-project-button").click()
|
||||
|
||||
try:
|
||||
self.wait_until_visible('#hint-error-project-name')
|
||||
url = reverse('project', args=(TestProjectPage.project_id, ))
|
||||
self.get(url)
|
||||
self.wait_until_visible('#config-nav', poll=3)
|
||||
except TimeoutException:
|
||||
self.wait_until_visible('#config-nav', poll=3)
|
||||
|
||||
def _random_string(self, length):
|
||||
return ''.join(
|
||||
random.choice(string.ascii_letters) for _ in range(length)
|
||||
)
|
||||
|
||||
def _navigate_to_project_page(self):
|
||||
# Navigate to project page
|
||||
if TestProjectPage.project_id is None:
|
||||
self._create_project(project_name=self._random_string(10))
|
||||
current_url = self.driver.current_url
|
||||
TestProjectPage.project_id = get_projectId_from_url(current_url)
|
||||
else:
|
||||
url = reverse('project', args=(TestProjectPage.project_id,))
|
||||
self.get(url)
|
||||
self.wait_until_visible('#config-nav')
|
||||
|
||||
def _get_create_builds(self, **kwargs):
|
||||
""" Create a build and return the build object """
|
||||
# parameters for builds to associate with the projects
|
||||
now = timezone.now()
|
||||
self.project1_build_success = {
|
||||
'project': Project.objects.get(id=TestProjectPage.project_id),
|
||||
'started_on': now,
|
||||
'completed_on': now,
|
||||
'outcome': Build.SUCCEEDED
|
||||
}
|
||||
|
||||
self.project1_build_failure = {
|
||||
'project': Project.objects.get(id=TestProjectPage.project_id),
|
||||
'started_on': now,
|
||||
'completed_on': now,
|
||||
'outcome': Build.FAILED
|
||||
}
|
||||
build1 = Build.objects.create(**self.project1_build_success)
|
||||
build2 = Build.objects.create(**self.project1_build_failure)
|
||||
|
||||
# add some targets to these builds so they have recipe links
|
||||
# (and so we can find the row in the ToasterTable corresponding to
|
||||
# a particular build)
|
||||
Target.objects.create(build=build1, target='foo')
|
||||
Target.objects.create(build=build2, target='bar')
|
||||
|
||||
if kwargs:
|
||||
# Create kwargs.get('success') builds with success status with target
|
||||
# and kwargs.get('failure') builds with failure status with target
|
||||
for i in range(kwargs.get('success', 0)):
|
||||
now = timezone.now()
|
||||
self.project1_build_success['started_on'] = now
|
||||
self.project1_build_success[
|
||||
'completed_on'] = now - timezone.timedelta(days=i)
|
||||
build = Build.objects.create(**self.project1_build_success)
|
||||
Target.objects.create(build=build,
|
||||
target=f'{i}_success_recipe',
|
||||
task=f'{i}_success_task')
|
||||
|
||||
for i in range(kwargs.get('failure', 0)):
|
||||
now = timezone.now()
|
||||
self.project1_build_failure['started_on'] = now
|
||||
self.project1_build_failure[
|
||||
'completed_on'] = now - timezone.timedelta(days=i)
|
||||
build = Build.objects.create(**self.project1_build_failure)
|
||||
Target.objects.create(build=build,
|
||||
target=f'{i}_fail_recipe',
|
||||
task=f'{i}_fail_task')
|
||||
return build1, build2
|
||||
|
||||
def _mixin_test_table_edit_column(
|
||||
self,
|
||||
table_id,
|
||||
edit_btn_id,
|
||||
list_check_box_id: list
|
||||
):
|
||||
# Check edit column
|
||||
edit_column = self.find(f'#{edit_btn_id}')
|
||||
self.assertTrue(edit_column.is_displayed())
|
||||
edit_column.click()
|
||||
# Check dropdown is visible
|
||||
self.wait_until_visible('ul.dropdown-menu.editcol')
|
||||
for check_box_id in list_check_box_id:
|
||||
# Check that we can hide/show table column
|
||||
check_box = self.find(f'#{check_box_id}')
|
||||
th_class = str(check_box_id).replace('checkbox-', '')
|
||||
if check_box.is_selected():
|
||||
# check if column is visible in table
|
||||
self.assertTrue(
|
||||
self.find(
|
||||
f'#{table_id} thead th.{th_class}'
|
||||
).is_displayed(),
|
||||
f"The {th_class} column is checked in EditColumn dropdown, but it's not visible in table"
|
||||
)
|
||||
check_box.click()
|
||||
# check if column is hidden in table
|
||||
self.assertFalse(
|
||||
self.find(
|
||||
f'#{table_id} thead th.{th_class}'
|
||||
).is_displayed(),
|
||||
f"The {th_class} column is unchecked in EditColumn dropdown, but it's visible in table"
|
||||
)
|
||||
else:
|
||||
# check if column is hidden in table
|
||||
self.assertFalse(
|
||||
self.find(
|
||||
f'#{table_id} thead th.{th_class}'
|
||||
).is_displayed(),
|
||||
f"The {th_class} column is unchecked in EditColumn dropdown, but it's visible in table"
|
||||
)
|
||||
check_box.click()
|
||||
# check if column is visible in table
|
||||
self.assertTrue(
|
||||
self.find(
|
||||
f'#{table_id} thead th.{th_class}'
|
||||
).is_displayed(),
|
||||
f"The {th_class} column is checked in EditColumn dropdown, but it's not visible in table"
|
||||
)
|
||||
|
||||
def _get_config_nav_item(self, index):
|
||||
config_nav = self.find('#config-nav')
|
||||
return config_nav.find_elements(By.TAG_NAME, 'li')[index]
|
||||
|
||||
def _navigate_to_config_nav(self, nav_id, nav_index):
|
||||
# navigate to the project page
|
||||
self._navigate_to_project_page()
|
||||
# click on "Software recipe" tab
|
||||
soft_recipe = self._get_config_nav_item(nav_index)
|
||||
soft_recipe.click()
|
||||
self.wait_until_visible(f'#{nav_id}')
|
||||
|
||||
def _mixin_test_table_show_rows(self, table_selector, **kwargs):
|
||||
""" Test the show rows feature in the builds table on the all builds page """
|
||||
def test_show_rows(row_to_show, show_row_link):
|
||||
# Check that we can show rows == row_to_show
|
||||
show_row_link.select_by_value(str(row_to_show))
|
||||
self.wait_until_visible(f'#{table_selector} tbody tr', poll=3)
|
||||
# check at least some rows are visible
|
||||
self.assertTrue(
|
||||
len(self.find_all(f'#{table_selector} tbody tr')) > 0
|
||||
)
|
||||
self.wait_until_present(f'#{table_selector} tbody tr')
|
||||
show_rows = self.driver.find_elements(
|
||||
By.XPATH,
|
||||
f'//select[@class="form-control pagesize-{table_selector}"]'
|
||||
)
|
||||
rows_to_show = [10, 25, 50, 100, 150]
|
||||
to_skip = kwargs.get('to_skip', [])
|
||||
# Check show rows
|
||||
for show_row_link in show_rows:
|
||||
show_row_link = Select(show_row_link)
|
||||
for row_to_show in rows_to_show:
|
||||
if row_to_show not in to_skip:
|
||||
test_show_rows(row_to_show, show_row_link)
|
||||
|
||||
def _mixin_test_table_search_input(self, **kwargs):
|
||||
input_selector, input_text, searchBtn_selector, table_selector, *_ = kwargs.values()
|
||||
# Test search input
|
||||
self.wait_until_visible(f'#{input_selector}')
|
||||
recipe_input = self.find(f'#{input_selector}')
|
||||
recipe_input.send_keys(input_text)
|
||||
self.find(f'#{searchBtn_selector}').click()
|
||||
self.wait_until_visible(f'#{table_selector} tbody tr')
|
||||
rows = self.find_all(f'#{table_selector} tbody tr')
|
||||
self.assertTrue(len(rows) > 0)
|
||||
|
||||
def test_create_project(self):
|
||||
""" Create/Test new project using:
|
||||
- Project Name: Any string
|
||||
- Release: Any string
|
||||
- Merge Toaster settings: True or False
|
||||
"""
|
||||
self._create_project(project_name=self.PROJECT_NAME)
|
||||
|
||||
def test_image_recipe_editColumn(self):
|
||||
""" Test the edit column feature in image recipe table on project page """
|
||||
self._get_create_builds(success=10, failure=10)
|
||||
|
||||
url = reverse('projectimagerecipes', args=(TestProjectPage.project_id,))
|
||||
self.get(url)
|
||||
self.wait_until_present('#imagerecipestable tbody tr')
|
||||
|
||||
column_list = [
|
||||
'get_description_or_summary', 'layer_version__get_vcs_reference',
|
||||
'layer_version__layer__name', 'license', 'recipe-file', 'section',
|
||||
'version'
|
||||
]
|
||||
|
||||
# Check that we can hide the edit column
|
||||
self._mixin_test_table_edit_column(
|
||||
'imagerecipestable',
|
||||
'edit-columns-button',
|
||||
[f'checkbox-{column}' for column in column_list]
|
||||
)
|
||||
|
||||
def test_page_header_on_project_page(self):
|
||||
""" Check page header in project page:
|
||||
- AT LEFT -> Logo of Yocto project, displayed, clickable
|
||||
- "Toaster"+" Information icon", displayed, clickable
|
||||
- "Server Icon" + "All builds", displayed, clickable
|
||||
- "Directory Icon" + "All projects", displayed, clickable
|
||||
- "Book Icon" + "Documentation", displayed, clickable
|
||||
- AT RIGHT -> button "New project", displayed, clickable
|
||||
"""
|
||||
# navigate to the project page
|
||||
self._navigate_to_project_page()
|
||||
|
||||
# check page header
|
||||
# AT LEFT -> Logo of Yocto project
|
||||
logo = self.driver.find_element(
|
||||
By.XPATH,
|
||||
"//div[@class='toaster-navbar-brand']",
|
||||
)
|
||||
logo_img = logo.find_element(By.TAG_NAME, 'img')
|
||||
self.assertTrue(logo_img.is_displayed(),
|
||||
'Logo of Yocto project not found')
|
||||
self.assertTrue(
|
||||
'/static/img/logo.png' in str(logo_img.get_attribute('src')),
|
||||
'Logo of Yocto project not found'
|
||||
)
|
||||
# "Toaster"+" Information icon", clickable
|
||||
toaster = self.driver.find_element(
|
||||
By.XPATH,
|
||||
"//div[@class='toaster-navbar-brand']//a[@class='brand']",
|
||||
)
|
||||
self.assertTrue(toaster.is_displayed(), 'Toaster not found')
|
||||
self.assertTrue(toaster.text == 'Toaster')
|
||||
info_sign = self.find('.glyphicon-info-sign')
|
||||
self.assertTrue(info_sign.is_displayed())
|
||||
|
||||
# "Server Icon" + "All builds"
|
||||
all_builds = self.find('#navbar-all-builds')
|
||||
all_builds_link = all_builds.find_element(By.TAG_NAME, 'a')
|
||||
self.assertTrue("All builds" in all_builds_link.text)
|
||||
self.assertTrue(
|
||||
'/toastergui/builds/' in str(all_builds_link.get_attribute('href'))
|
||||
)
|
||||
server_icon = all_builds.find_element(By.TAG_NAME, 'i')
|
||||
self.assertTrue(
|
||||
server_icon.get_attribute('class') == 'glyphicon glyphicon-tasks'
|
||||
)
|
||||
self.assertTrue(server_icon.is_displayed())
|
||||
|
||||
# "Directory Icon" + "All projects"
|
||||
all_projects = self.find('#navbar-all-projects')
|
||||
all_projects_link = all_projects.find_element(By.TAG_NAME, 'a')
|
||||
self.assertTrue("All projects" in all_projects_link.text)
|
||||
self.assertTrue(
|
||||
'/toastergui/projects/' in str(all_projects_link.get_attribute(
|
||||
'href'))
|
||||
)
|
||||
dir_icon = all_projects.find_element(By.TAG_NAME, 'i')
|
||||
self.assertTrue(
|
||||
dir_icon.get_attribute('class') == 'icon-folder-open'
|
||||
)
|
||||
self.assertTrue(dir_icon.is_displayed())
|
||||
|
||||
# "Book Icon" + "Documentation"
|
||||
toaster_docs_link = self.find('#navbar-docs')
|
||||
toaster_docs_link_link = toaster_docs_link.find_element(By.TAG_NAME,
|
||||
'a')
|
||||
self.assertTrue("Documentation" in toaster_docs_link_link.text)
|
||||
self.assertTrue(
|
||||
toaster_docs_link_link.get_attribute('href') == 'http://docs.yoctoproject.org/toaster-manual/index.html#toaster-user-manual'
|
||||
)
|
||||
book_icon = toaster_docs_link.find_element(By.TAG_NAME, 'i')
|
||||
self.assertTrue(
|
||||
book_icon.get_attribute('class') == 'glyphicon glyphicon-book'
|
||||
)
|
||||
self.assertTrue(book_icon.is_displayed())
|
||||
|
||||
# AT RIGHT -> button "New project"
|
||||
new_project_button = self.find('#new-project-button')
|
||||
self.assertTrue(new_project_button.is_displayed())
|
||||
self.assertTrue(new_project_button.text == 'New project')
|
||||
new_project_button.click()
|
||||
self.assertTrue(
|
||||
'/toastergui/newproject/' in str(self.driver.current_url)
|
||||
)
|
||||
|
||||
def test_edit_project_name(self):
|
||||
""" Test edit project name:
|
||||
- Click on "Edit" icon button
|
||||
- Change project name
|
||||
- Click on "Save" button
|
||||
- Check project name is changed
|
||||
"""
|
||||
# navigate to the project page
|
||||
self._navigate_to_project_page()
|
||||
|
||||
# click on "Edit" icon button
|
||||
self.wait_until_visible('#project-name-container')
|
||||
edit_button = self.find('#project-change-form-toggle')
|
||||
edit_button.click()
|
||||
project_name_input = self.find('#project-name-change-input')
|
||||
self.assertTrue(project_name_input.is_displayed())
|
||||
project_name_input.clear()
|
||||
project_name_input.send_keys('New Name')
|
||||
self.find('#project-name-change-btn').click()
|
||||
|
||||
# check project name is changed
|
||||
self.wait_until_visible('#project-name-container')
|
||||
self.assertTrue(
|
||||
'New Name' in str(self.find('#project-name-container').text)
|
||||
)
|
||||
|
||||
def test_project_page_tabs(self):
|
||||
""" Test project tabs:
|
||||
- "configuration" tab
|
||||
- "Builds" tab
|
||||
- "Import layers" tab
|
||||
- "New custom image" tab
|
||||
Check search box used to build recipes
|
||||
"""
|
||||
# navigate to the project page
|
||||
self._navigate_to_project_page()
|
||||
|
||||
# check "configuration" tab
|
||||
self.wait_until_visible('#topbar-configuration-tab')
|
||||
config_tab = self.find('#topbar-configuration-tab')
|
||||
self.assertTrue(config_tab.get_attribute('class') == 'active')
|
||||
self.assertTrue('Configuration' in str(config_tab.text))
|
||||
self.assertTrue(
|
||||
f"/toastergui/project/{TestProjectPage.project_id}" in str(self.driver.current_url)
|
||||
)
|
||||
|
||||
def get_tabs():
|
||||
# tabs links list
|
||||
return self.driver.find_elements(
|
||||
By.XPATH,
|
||||
'//div[@id="project-topbar"]//li'
|
||||
)
|
||||
|
||||
def check_tab_link(tab_index, tab_name, url):
|
||||
tab = get_tabs()[tab_index]
|
||||
tab_link = tab.find_element(By.TAG_NAME, 'a')
|
||||
self.assertTrue(url in tab_link.get_attribute('href'))
|
||||
self.assertTrue(tab_name in tab_link.text)
|
||||
self.assertTrue(tab.get_attribute('class') == 'active')
|
||||
|
||||
# check "Builds" tab
|
||||
builds_tab = get_tabs()[1]
|
||||
builds_tab.find_element(By.TAG_NAME, 'a').click()
|
||||
check_tab_link(
|
||||
1,
|
||||
'Builds',
|
||||
f"/toastergui/project/{TestProjectPage.project_id}/builds"
|
||||
)
|
||||
|
||||
# check "Import layers" tab
|
||||
import_layers_tab = get_tabs()[2]
|
||||
import_layers_tab.find_element(By.TAG_NAME, 'a').click()
|
||||
check_tab_link(
|
||||
2,
|
||||
'Import layer',
|
||||
f"/toastergui/project/{TestProjectPage.project_id}/importlayer"
|
||||
)
|
||||
|
||||
# check "New custom image" tab
|
||||
new_custom_image_tab = get_tabs()[3]
|
||||
new_custom_image_tab.find_element(By.TAG_NAME, 'a').click()
|
||||
check_tab_link(
|
||||
3,
|
||||
'New custom image',
|
||||
f"/toastergui/project/{TestProjectPage.project_id}/newcustomimage"
|
||||
)
|
||||
|
||||
# check search box can be use to build recipes
|
||||
search_box = self.find('#build-input')
|
||||
search_box.send_keys('core-image-minimal')
|
||||
self.find('#build-button').click()
|
||||
self.wait_until_visible('#latest-builds')
|
||||
lastest_builds = self.driver.find_elements(
|
||||
By.XPATH,
|
||||
'//div[@id="latest-builds"]',
|
||||
)
|
||||
last_build = lastest_builds[0]
|
||||
self.assertTrue(
|
||||
'core-image-minimal' in str(last_build.text)
|
||||
)
|
||||
|
||||
def test_softwareRecipe_page(self):
|
||||
""" Test software recipe page
|
||||
- Check title "Compatible software recipes" is displayed
|
||||
- Check search input
|
||||
- Check "build recipe" button works
|
||||
- Check software recipe table feature(show/hide column, pagination)
|
||||
"""
|
||||
self._navigate_to_config_nav('softwarerecipestable', 4)
|
||||
# check title "Compatible software recipes" is displayed
|
||||
self.assertTrue("Compatible software recipes" in self.get_page_source())
|
||||
# Test search input
|
||||
self._mixin_test_table_search_input(
|
||||
input_selector='search-input-softwarerecipestable',
|
||||
input_text='busybox',
|
||||
searchBtn_selector='search-submit-softwarerecipestable',
|
||||
table_selector='softwarerecipestable'
|
||||
)
|
||||
# check "build recipe" button works
|
||||
rows = self.find_all('#softwarerecipestable tbody tr')
|
||||
image_to_build = rows[0]
|
||||
build_btn = image_to_build.find_element(
|
||||
By.XPATH,
|
||||
'//td[@class="add-del-layers"]//a[1]'
|
||||
)
|
||||
build_btn.click()
|
||||
build_state = wait_until_build(self, 'queued cloning starting parsing failed')
|
||||
lastest_builds = self.driver.find_elements(
|
||||
By.XPATH,
|
||||
'//div[@id="latest-builds"]/div'
|
||||
)
|
||||
self.assertTrue(len(lastest_builds) > 0)
|
||||
last_build = lastest_builds[0]
|
||||
cancel_button = last_build.find_element(
|
||||
By.XPATH,
|
||||
'//span[@class="cancel-build-btn pull-right alert-link"]',
|
||||
)
|
||||
cancel_button.click()
|
||||
if 'starting' not in build_state: # change build state when cancelled in starting state
|
||||
wait_until_build_cancelled(self)
|
||||
|
||||
# check software recipe table feature(show/hide column, pagination)
|
||||
self._navigate_to_config_nav('softwarerecipestable', 4)
|
||||
column_list = [
|
||||
'get_description_or_summary',
|
||||
'layer_version__get_vcs_reference',
|
||||
'layer_version__layer__name',
|
||||
'license',
|
||||
'recipe-file',
|
||||
'section',
|
||||
'version',
|
||||
]
|
||||
self._mixin_test_table_edit_column(
|
||||
'softwarerecipestable',
|
||||
'edit-columns-button',
|
||||
[f'checkbox-{column}' for column in column_list]
|
||||
)
|
||||
self._navigate_to_config_nav('softwarerecipestable', 4)
|
||||
# check show rows(pagination)
|
||||
self._mixin_test_table_show_rows(
|
||||
table_selector='softwarerecipestable',
|
||||
to_skip=[150],
|
||||
)
|
||||
|
||||
def test_machines_page(self):
|
||||
""" Test Machine page
|
||||
- Check if title "Compatible machines" is displayed
|
||||
- Check search input
|
||||
- Check "Select machine" button works
|
||||
- Check "Add layer" button works
|
||||
- Check Machine table feature(show/hide column, pagination)
|
||||
"""
|
||||
self._navigate_to_config_nav('machinestable', 5)
|
||||
# check title "Compatible software recipes" is displayed
|
||||
self.assertTrue("Compatible machines" in self.get_page_source())
|
||||
# Test search input
|
||||
self._mixin_test_table_search_input(
|
||||
input_selector='search-input-machinestable',
|
||||
input_text='qemux86-64',
|
||||
searchBtn_selector='search-submit-machinestable',
|
||||
table_selector='machinestable'
|
||||
)
|
||||
# check "Select machine" button works
|
||||
rows = self.find_all('#machinestable tbody tr')
|
||||
machine_to_select = rows[0]
|
||||
select_btn = machine_to_select.find_element(
|
||||
By.XPATH,
|
||||
'//td[@class="add-del-layers"]//a[1]'
|
||||
)
|
||||
select_btn.send_keys(Keys.RETURN)
|
||||
self.wait_until_visible('#config-nav')
|
||||
project_machine_name = self.find('#project-machine-name')
|
||||
self.assertTrue(
|
||||
'qemux86-64' in project_machine_name.text
|
||||
)
|
||||
# check "Add layer" button works
|
||||
self._navigate_to_config_nav('machinestable', 5)
|
||||
# Search for a machine whit layer not in project
|
||||
self._mixin_test_table_search_input(
|
||||
input_selector='search-input-machinestable',
|
||||
input_text='qemux86-64-tpm2',
|
||||
searchBtn_selector='search-submit-machinestable',
|
||||
table_selector='machinestable'
|
||||
)
|
||||
self.wait_until_visible('#machinestable tbody tr', poll=3)
|
||||
rows = self.find_all('#machinestable tbody tr')
|
||||
machine_to_add = rows[0]
|
||||
add_btn = machine_to_add.find_element(By.XPATH, '//td[@class="add-del-layers"]')
|
||||
add_btn.click()
|
||||
self.wait_until_visible('#change-notification')
|
||||
change_notification = self.find('#change-notification')
|
||||
self.assertTrue(
|
||||
f'You have added 1 layer to your project' in str(change_notification.text)
|
||||
)
|
||||
# check Machine table feature(show/hide column, pagination)
|
||||
self._navigate_to_config_nav('machinestable', 5)
|
||||
column_list = [
|
||||
'description',
|
||||
'layer_version__get_vcs_reference',
|
||||
'layer_version__layer__name',
|
||||
'machinefile',
|
||||
]
|
||||
self._mixin_test_table_edit_column(
|
||||
'machinestable',
|
||||
'edit-columns-button',
|
||||
[f'checkbox-{column}' for column in column_list]
|
||||
)
|
||||
self._navigate_to_config_nav('machinestable', 5)
|
||||
# check show rows(pagination)
|
||||
self._mixin_test_table_show_rows(
|
||||
table_selector='machinestable',
|
||||
to_skip=[150],
|
||||
)
|
||||
|
||||
def test_layers_page(self):
|
||||
""" Test layers page
|
||||
- Check if title "Compatible layerss" is displayed
|
||||
- Check search input
|
||||
- Check "Add layer" button works
|
||||
- Check "Remove layer" button works
|
||||
- Check layers table feature(show/hide column, pagination)
|
||||
"""
|
||||
self._navigate_to_config_nav('layerstable', 6)
|
||||
# check title "Compatible layers" is displayed
|
||||
self.assertTrue("Compatible layers" in self.get_page_source())
|
||||
# Test search input
|
||||
input_text='meta-tanowrt'
|
||||
self._mixin_test_table_search_input(
|
||||
input_selector='search-input-layerstable',
|
||||
input_text=input_text,
|
||||
searchBtn_selector='search-submit-layerstable',
|
||||
table_selector='layerstable'
|
||||
)
|
||||
# check "Add layer" button works
|
||||
self.wait_until_visible('#layerstable tbody tr', poll=3)
|
||||
rows = self.find_all('#layerstable tbody tr')
|
||||
layer_to_add = rows[0]
|
||||
add_btn = layer_to_add.find_element(
|
||||
By.XPATH,
|
||||
'//td[@class="add-del-layers"]'
|
||||
)
|
||||
add_btn.click()
|
||||
# check modal is displayed
|
||||
self.wait_until_visible('#dependencies-modal', poll=3)
|
||||
list_dependencies = self.find_all('#dependencies-list li')
|
||||
# click on add-layers button
|
||||
add_layers_btn = self.driver.find_element(
|
||||
By.XPATH,
|
||||
'//form[@id="dependencies-modal-form"]//button[@class="btn btn-primary"]'
|
||||
)
|
||||
add_layers_btn.click()
|
||||
self.wait_until_visible('#change-notification')
|
||||
change_notification = self.find('#change-notification')
|
||||
self.assertTrue(
|
||||
f'You have added {len(list_dependencies)+1} layers to your project: {input_text} and its dependencies' in str(change_notification.text)
|
||||
)
|
||||
# check "Remove layer" button works
|
||||
self.wait_until_visible('#layerstable tbody tr', poll=3)
|
||||
rows = self.find_all('#layerstable tbody tr')
|
||||
layer_to_remove = rows[0]
|
||||
remove_btn = layer_to_remove.find_element(
|
||||
By.XPATH,
|
||||
'//td[@class="add-del-layers"]'
|
||||
)
|
||||
remove_btn.click()
|
||||
self.wait_until_visible('#change-notification', poll=2)
|
||||
change_notification = self.find('#change-notification')
|
||||
self.assertTrue(
|
||||
f'You have removed 1 layer from your project: {input_text}' in str(change_notification.text)
|
||||
)
|
||||
# check layers table feature(show/hide column, pagination)
|
||||
self._navigate_to_config_nav('layerstable', 6)
|
||||
column_list = [
|
||||
'dependencies',
|
||||
'revision',
|
||||
'layer__vcs_url',
|
||||
'git_subdir',
|
||||
'layer__summary',
|
||||
]
|
||||
self._mixin_test_table_edit_column(
|
||||
'layerstable',
|
||||
'edit-columns-button',
|
||||
[f'checkbox-{column}' for column in column_list]
|
||||
)
|
||||
self._navigate_to_config_nav('layerstable', 6)
|
||||
# check show rows(pagination)
|
||||
self._mixin_test_table_show_rows(
|
||||
table_selector='layerstable',
|
||||
to_skip=[150],
|
||||
)
|
||||
|
||||
def test_distro_page(self):
|
||||
""" Test distros page
|
||||
- Check if title "Compatible distros" is displayed
|
||||
- Check search input
|
||||
- Check "Add layer" button works
|
||||
- Check distro table feature(show/hide column, pagination)
|
||||
"""
|
||||
self._navigate_to_config_nav('distrostable', 7)
|
||||
# check title "Compatible distros" is displayed
|
||||
self.assertTrue("Compatible Distros" in self.get_page_source())
|
||||
# Test search input
|
||||
input_text='poky-altcfg'
|
||||
self._mixin_test_table_search_input(
|
||||
input_selector='search-input-distrostable',
|
||||
input_text=input_text,
|
||||
searchBtn_selector='search-submit-distrostable',
|
||||
table_selector='distrostable'
|
||||
)
|
||||
# check "Add distro" button works
|
||||
rows = self.find_all('#distrostable tbody tr')
|
||||
distro_to_add = rows[0]
|
||||
add_btn = distro_to_add.find_element(
|
||||
By.XPATH,
|
||||
'//td[@class="add-del-layers"]//a[1]'
|
||||
)
|
||||
add_btn.click()
|
||||
self.wait_until_visible('#change-notification', poll=2)
|
||||
change_notification = self.find('#change-notification')
|
||||
self.assertTrue(
|
||||
f'You have changed the distro to: {input_text}' in str(change_notification.text)
|
||||
)
|
||||
# check distro table feature(show/hide column, pagination)
|
||||
self._navigate_to_config_nav('distrostable', 7)
|
||||
column_list = [
|
||||
'description',
|
||||
'templatefile',
|
||||
'layer_version__get_vcs_reference',
|
||||
'layer_version__layer__name',
|
||||
]
|
||||
self._mixin_test_table_edit_column(
|
||||
'distrostable',
|
||||
'edit-columns-button',
|
||||
[f'checkbox-{column}' for column in column_list]
|
||||
)
|
||||
self._navigate_to_config_nav('distrostable', 7)
|
||||
# check show rows(pagination)
|
||||
self._mixin_test_table_show_rows(
|
||||
table_selector='distrostable',
|
||||
to_skip=[150],
|
||||
)
|
||||
|
||||
def test_single_layer_page(self):
|
||||
""" Test layer page
|
||||
- Check if title is displayed
|
||||
- Check add/remove layer button works
|
||||
- Check tabs(layers, recipes, machines) are displayed
|
||||
- Check left section is displayed
|
||||
- Check layer name
|
||||
- Check layer summary
|
||||
- Check layer description
|
||||
"""
|
||||
url = reverse("layerdetails", args=(TestProjectPage.project_id, 8))
|
||||
self.get(url)
|
||||
self.wait_until_visible('.page-header')
|
||||
# check title is displayed
|
||||
self.assertTrue(self.find('.page-header h1').is_displayed())
|
||||
|
||||
# check add layer button works
|
||||
remove_layer_btn = self.find('#add-remove-layer-btn')
|
||||
remove_layer_btn.click()
|
||||
self.wait_until_visible('#change-notification', poll=2)
|
||||
change_notification = self.find('#change-notification')
|
||||
self.assertTrue(
|
||||
f'You have removed 1 layer from your project' in str(change_notification.text)
|
||||
)
|
||||
# check add layer button works, 18 is the random layer id
|
||||
add_layer_btn = self.find('#add-remove-layer-btn')
|
||||
add_layer_btn.click()
|
||||
self.wait_until_visible('#change-notification')
|
||||
change_notification = self.find('#change-notification')
|
||||
self.assertTrue(
|
||||
f'You have added 1 layer to your project' in str(change_notification.text)
|
||||
)
|
||||
# check tabs(layers, recipes, machines) are displayed
|
||||
tabs = self.find_all('.nav-tabs li')
|
||||
self.assertEqual(len(tabs), 3)
|
||||
# Check first tab
|
||||
tabs[0].click()
|
||||
self.assertTrue(
|
||||
'active' in str(self.find('#information').get_attribute('class'))
|
||||
)
|
||||
# Check second tab
|
||||
tabs[1].click()
|
||||
self.assertTrue(
|
||||
'active' in str(self.find('#recipes').get_attribute('class'))
|
||||
)
|
||||
# Check third tab
|
||||
tabs[2].click()
|
||||
self.assertTrue(
|
||||
'active' in str(self.find('#machines').get_attribute('class'))
|
||||
)
|
||||
# Check left section is displayed
|
||||
section = self.find('.well')
|
||||
# Check layer name
|
||||
self.assertTrue(
|
||||
section.find_element(By.XPATH, '//h2[1]').is_displayed()
|
||||
)
|
||||
# Check layer summary
|
||||
self.assertTrue("Summary" in section.text)
|
||||
# Check layer description
|
||||
self.assertTrue("Description" in section.text)
|
||||
|
||||
def test_single_recipe_page(self):
|
||||
""" Test recipe page
|
||||
- Check if title is displayed
|
||||
- Check add recipe layer displayed
|
||||
- Check left section is displayed
|
||||
- Check recipe: name, summary, description, Version, Section,
|
||||
License, Approx. packages included, Approx. size, Recipe file
|
||||
"""
|
||||
url = reverse("recipedetails", args=(TestProjectPage.project_id, 53428))
|
||||
self.get(url)
|
||||
self.wait_until_visible('.page-header')
|
||||
# check title is displayed
|
||||
self.assertTrue(self.find('.page-header h1').is_displayed())
|
||||
# check add recipe layer displayed
|
||||
add_recipe_layer_btn = self.find('#add-layer-btn')
|
||||
self.assertTrue(add_recipe_layer_btn.is_displayed())
|
||||
# check left section is displayed
|
||||
section = self.find('.well')
|
||||
# Check recipe name
|
||||
self.assertTrue(
|
||||
section.find_element(By.XPATH, '//h2[1]').is_displayed()
|
||||
)
|
||||
# Check recipe sections details info are displayed
|
||||
self.assertTrue("Summary" in section.text)
|
||||
self.assertTrue("Description" in section.text)
|
||||
self.assertTrue("Version" in section.text)
|
||||
self.assertTrue("Section" in section.text)
|
||||
self.assertTrue("License" in section.text)
|
||||
self.assertTrue("Approx. packages included" in section.text)
|
||||
self.assertTrue("Approx. package size" in section.text)
|
||||
self.assertTrue("Recipe file" in section.text)
|
||||
@@ -0,0 +1,528 @@
|
||||
#! /usr/bin/env python3 #
|
||||
# BitBake Toaster UI tests implementation
|
||||
#
|
||||
# Copyright (C) 2023 Savoir-faire Linux
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
|
||||
import string
|
||||
import random
|
||||
import pytest
|
||||
from django.urls import reverse
|
||||
from selenium.webdriver import Keys
|
||||
from selenium.webdriver.support.select import Select
|
||||
from selenium.common.exceptions import ElementClickInterceptedException, NoSuchElementException, TimeoutException
|
||||
from orm.models import Project
|
||||
from tests.functional.functional_helpers import SeleniumFunctionalTestCase
|
||||
from selenium.webdriver.common.by import By
|
||||
|
||||
from .utils import get_projectId_from_url, wait_until_build, wait_until_build_cancelled
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.order("last")
|
||||
class TestProjectConfigTab(SeleniumFunctionalTestCase):
|
||||
PROJECT_NAME = 'TestProjectConfigTab'
|
||||
project_id = None
|
||||
|
||||
def _create_project(self, project_name, **kwargs):
|
||||
""" Create/Test new project using:
|
||||
- Project Name: Any string
|
||||
- Release: Any string
|
||||
- Merge Toaster settings: True or False
|
||||
"""
|
||||
release = kwargs.get('release', '3')
|
||||
self.get(reverse('newproject'))
|
||||
self.wait_until_visible('#new-project-name')
|
||||
self.find("#new-project-name").send_keys(project_name)
|
||||
select = Select(self.find("#projectversion"))
|
||||
select.select_by_value(release)
|
||||
|
||||
# check merge toaster settings
|
||||
checkbox = self.find('.checkbox-mergeattr')
|
||||
if not checkbox.is_selected():
|
||||
checkbox.click()
|
||||
|
||||
if self.PROJECT_NAME != 'TestProjectConfigTab':
|
||||
# Reset project name if it's not the default one
|
||||
self.PROJECT_NAME = 'TestProjectConfigTab'
|
||||
|
||||
self.find("#create-project-button").click()
|
||||
|
||||
try:
|
||||
self.wait_until_visible('#hint-error-project-name', poll=3)
|
||||
url = reverse('project', args=(TestProjectConfigTab.project_id, ))
|
||||
self.get(url)
|
||||
self.wait_until_visible('#config-nav', poll=3)
|
||||
except TimeoutException:
|
||||
self.wait_until_visible('#config-nav', poll=3)
|
||||
|
||||
def _random_string(self, length):
|
||||
return ''.join(
|
||||
random.choice(string.ascii_letters) for _ in range(length)
|
||||
)
|
||||
|
||||
def _navigate_to_project_page(self):
|
||||
# Navigate to project page
|
||||
if TestProjectConfigTab.project_id is None:
|
||||
self._create_project(project_name=self._random_string(10))
|
||||
current_url = self.driver.current_url
|
||||
TestProjectConfigTab.project_id = get_projectId_from_url(
|
||||
current_url)
|
||||
else:
|
||||
url = reverse('project', args=(TestProjectConfigTab.project_id,))
|
||||
self.get(url)
|
||||
self.wait_until_visible('#config-nav')
|
||||
|
||||
def _create_builds(self):
|
||||
# check search box can be use to build recipes
|
||||
search_box = self.find('#build-input')
|
||||
search_box.send_keys('foo')
|
||||
self.find('#build-button').click()
|
||||
self.wait_until_present('#latest-builds')
|
||||
# loop until reach the parsing state
|
||||
wait_until_build(self, 'queued cloning starting parsing failed')
|
||||
lastest_builds = self.driver.find_elements(
|
||||
By.XPATH,
|
||||
'//div[@id="latest-builds"]/div',
|
||||
)
|
||||
last_build = lastest_builds[0]
|
||||
self.assertTrue(
|
||||
'foo' in str(last_build.text)
|
||||
)
|
||||
last_build = lastest_builds[0]
|
||||
try:
|
||||
cancel_button = last_build.find_element(
|
||||
By.XPATH,
|
||||
'//span[@class="cancel-build-btn pull-right alert-link"]',
|
||||
)
|
||||
cancel_button.click()
|
||||
except NoSuchElementException:
|
||||
# Skip if the build is already cancelled
|
||||
pass
|
||||
wait_until_build_cancelled(self)
|
||||
|
||||
def _get_tabs(self):
|
||||
# tabs links list
|
||||
return self.driver.find_elements(
|
||||
By.XPATH,
|
||||
'//div[@id="project-topbar"]//li'
|
||||
)
|
||||
|
||||
def _get_config_nav_item(self, index):
|
||||
config_nav = self.find('#config-nav')
|
||||
return config_nav.find_elements(By.TAG_NAME, 'li')[index]
|
||||
|
||||
def test_project_config_nav(self):
|
||||
""" Test project config tab navigation:
|
||||
- Check if the menu is displayed and contains the right elements:
|
||||
- Configuration
|
||||
- COMPATIBLE METADATA
|
||||
- Custom images
|
||||
- Image recipes
|
||||
- Software recipes
|
||||
- Machines
|
||||
- Layers
|
||||
- Distro
|
||||
- EXTRA CONFIGURATION
|
||||
- Bitbake variables
|
||||
- Actions
|
||||
- Delete project
|
||||
"""
|
||||
self._navigate_to_project_page()
|
||||
|
||||
def _get_config_nav_item(index):
|
||||
config_nav = self.find('#config-nav')
|
||||
return config_nav.find_elements(By.TAG_NAME, 'li')[index]
|
||||
|
||||
def check_config_nav_item(index, item_name, url):
|
||||
item = _get_config_nav_item(index)
|
||||
self.assertTrue(item_name in item.text)
|
||||
self.assertTrue(item.get_attribute('class') == 'active')
|
||||
self.assertTrue(url in self.driver.current_url)
|
||||
|
||||
# check if the menu contains the right elements
|
||||
# COMPATIBLE METADATA
|
||||
compatible_metadata = _get_config_nav_item(1)
|
||||
self.assertTrue(
|
||||
"compatible metadata" in compatible_metadata.text.lower()
|
||||
)
|
||||
# EXTRA CONFIGURATION
|
||||
extra_configuration = _get_config_nav_item(8)
|
||||
self.assertTrue(
|
||||
"extra configuration" in extra_configuration.text.lower()
|
||||
)
|
||||
# Actions
|
||||
actions = _get_config_nav_item(10)
|
||||
self.assertTrue("actions" in str(actions.text).lower())
|
||||
|
||||
conf_nav_list = [
|
||||
# config
|
||||
[0, 'Configuration',
|
||||
f"/toastergui/project/{TestProjectConfigTab.project_id}"],
|
||||
# custom images
|
||||
[2, 'Custom images',
|
||||
f"/toastergui/project/{TestProjectConfigTab.project_id}/customimages"],
|
||||
# image recipes
|
||||
[3, 'Image recipes',
|
||||
f"/toastergui/project/{TestProjectConfigTab.project_id}/images"],
|
||||
# software recipes
|
||||
[4, 'Software recipes',
|
||||
f"/toastergui/project/{TestProjectConfigTab.project_id}/softwarerecipes"],
|
||||
# machines
|
||||
[5, 'Machines',
|
||||
f"/toastergui/project/{TestProjectConfigTab.project_id}/machines"],
|
||||
# layers
|
||||
[6, 'Layers',
|
||||
f"/toastergui/project/{TestProjectConfigTab.project_id}/layers"],
|
||||
# distro
|
||||
[7, 'Distros',
|
||||
f"/toastergui/project/{TestProjectConfigTab.project_id}/distros"],
|
||||
# [9, 'BitBake variables', f"/toastergui/project/{TestProjectConfigTab.project_id}/configuration"], # bitbake variables
|
||||
]
|
||||
for index, item_name, url in conf_nav_list:
|
||||
item = _get_config_nav_item(index)
|
||||
if item.get_attribute('class') != 'active':
|
||||
item.click()
|
||||
check_config_nav_item(index, item_name, url)
|
||||
|
||||
def test_image_recipe_editColumn(self):
|
||||
""" Test the edit column feature in image recipe table on project page """
|
||||
def test_edit_column(check_box_id):
|
||||
# Check that we can hide/show table column
|
||||
check_box = self.find(f'#{check_box_id}')
|
||||
th_class = str(check_box_id).replace('checkbox-', '')
|
||||
if check_box.is_selected():
|
||||
# check if column is visible in table
|
||||
self.assertTrue(
|
||||
self.find(
|
||||
f'#imagerecipestable thead th.{th_class}'
|
||||
).is_displayed(),
|
||||
f"The {th_class} column is checked in EditColumn dropdown, but it's not visible in table"
|
||||
)
|
||||
check_box.click()
|
||||
# check if column is hidden in table
|
||||
self.assertFalse(
|
||||
self.find(
|
||||
f'#imagerecipestable thead th.{th_class}'
|
||||
).is_displayed(),
|
||||
f"The {th_class} column is unchecked in EditColumn dropdown, but it's visible in table"
|
||||
)
|
||||
else:
|
||||
# check if column is hidden in table
|
||||
self.assertFalse(
|
||||
self.find(
|
||||
f'#imagerecipestable thead th.{th_class}'
|
||||
).is_displayed(),
|
||||
f"The {th_class} column is unchecked in EditColumn dropdown, but it's visible in table"
|
||||
)
|
||||
check_box.click()
|
||||
# check if column is visible in table
|
||||
self.assertTrue(
|
||||
self.find(
|
||||
f'#imagerecipestable thead th.{th_class}'
|
||||
).is_displayed(),
|
||||
f"The {th_class} column is checked in EditColumn dropdown, but it's not visible in table"
|
||||
)
|
||||
|
||||
self._navigate_to_project_page()
|
||||
# navigate to project image recipe page
|
||||
recipe_image_page_link = self._get_config_nav_item(3)
|
||||
recipe_image_page_link.click()
|
||||
self.wait_until_present('#imagerecipestable tbody tr')
|
||||
|
||||
# Check edit column
|
||||
edit_column = self.find('#edit-columns-button')
|
||||
self.assertTrue(edit_column.is_displayed())
|
||||
edit_column.click()
|
||||
# Check dropdown is visible
|
||||
self.wait_until_visible('ul.dropdown-menu.editcol')
|
||||
|
||||
# Check that we can hide the edit column
|
||||
test_edit_column('checkbox-get_description_or_summary')
|
||||
test_edit_column('checkbox-layer_version__get_vcs_reference')
|
||||
test_edit_column('checkbox-layer_version__layer__name')
|
||||
test_edit_column('checkbox-license')
|
||||
test_edit_column('checkbox-recipe-file')
|
||||
test_edit_column('checkbox-section')
|
||||
test_edit_column('checkbox-version')
|
||||
|
||||
def test_image_recipe_show_rows(self):
|
||||
""" Test the show rows feature in image recipe table on project page """
|
||||
def test_show_rows(row_to_show, show_row_link):
|
||||
# Check that we can show rows == row_to_show
|
||||
show_row_link.select_by_value(str(row_to_show))
|
||||
self.wait_until_visible('#imagerecipestable tbody tr', poll=3)
|
||||
# check at least some rows are visible
|
||||
self.assertTrue(
|
||||
len(self.find_all('#imagerecipestable tbody tr')) > 0
|
||||
)
|
||||
|
||||
self._navigate_to_project_page()
|
||||
# navigate to project image recipe page
|
||||
recipe_image_page_link = self._get_config_nav_item(3)
|
||||
recipe_image_page_link.click()
|
||||
self.wait_until_present('#imagerecipestable tbody tr')
|
||||
|
||||
show_rows = self.driver.find_elements(
|
||||
By.XPATH,
|
||||
'//select[@class="form-control pagesize-imagerecipestable"]'
|
||||
)
|
||||
# Check show rows
|
||||
for show_row_link in show_rows:
|
||||
show_row_link = Select(show_row_link)
|
||||
test_show_rows(10, show_row_link)
|
||||
test_show_rows(25, show_row_link)
|
||||
test_show_rows(50, show_row_link)
|
||||
test_show_rows(100, show_row_link)
|
||||
test_show_rows(150, show_row_link)
|
||||
|
||||
def test_project_config_tab_right_section(self):
|
||||
""" Test project config tab right section contains five blocks:
|
||||
- Machine:
|
||||
- check 'Machine' is displayed
|
||||
- check can change Machine
|
||||
- Distro:
|
||||
- check 'Distro' is displayed
|
||||
- check can change Distro
|
||||
- Most built recipes:
|
||||
- check 'Most built recipes' is displayed
|
||||
- check can select a recipe and build it
|
||||
- Project release:
|
||||
- check 'Project release' is displayed
|
||||
- check project has right release displayed
|
||||
- Layers:
|
||||
- check can add a layer if exists
|
||||
- check at least three layers are displayed
|
||||
- openembedded-core
|
||||
- meta-poky
|
||||
- meta-yocto-bsp
|
||||
"""
|
||||
# Create a new project for this test
|
||||
project_name = self._random_string(10)
|
||||
self._create_project(project_name=project_name)
|
||||
# check if the menu is displayed
|
||||
self.wait_until_visible('#project-page')
|
||||
block_l = self.driver.find_element(
|
||||
By.XPATH, '//*[@id="project-page"]/div[2]')
|
||||
project_release = self.driver.find_element(
|
||||
By.XPATH, '//*[@id="project-page"]/div[1]/div[4]')
|
||||
layers = block_l.find_element(By.ID, 'layer-container')
|
||||
|
||||
def check_machine_distro(self, item_name, new_item_name, block_id):
|
||||
block = self.find(f'#{block_id}')
|
||||
title = block.find_element(By.TAG_NAME, 'h3')
|
||||
self.assertTrue(item_name.capitalize() in title.text)
|
||||
edit_btn = self.find(f'#change-{item_name}-toggle')
|
||||
edit_btn.click()
|
||||
self.wait_until_visible(f'#{item_name}-change-input')
|
||||
name_input = self.find(f'#{item_name}-change-input')
|
||||
name_input.clear()
|
||||
name_input.send_keys(new_item_name)
|
||||
change_btn = self.find(f'#{item_name}-change-btn')
|
||||
change_btn.click()
|
||||
self.wait_until_visible(f'#project-{item_name}-name')
|
||||
project_name = self.find(f'#project-{item_name}-name')
|
||||
self.assertTrue(new_item_name in project_name.text)
|
||||
# check change notificaiton is displayed
|
||||
change_notification = self.find('#change-notification')
|
||||
self.assertTrue(
|
||||
f'You have changed the {item_name} to: {new_item_name}' in change_notification.text
|
||||
)
|
||||
|
||||
# Machine
|
||||
check_machine_distro(self, 'machine', 'qemux86-64', 'machine-section')
|
||||
# Distro
|
||||
check_machine_distro(self, 'distro', 'poky-altcfg', 'distro-section')
|
||||
|
||||
# Project release
|
||||
title = project_release.find_element(By.TAG_NAME, 'h3')
|
||||
self.assertTrue("Project release" in title.text)
|
||||
self.assertTrue(
|
||||
"Yocto Project master" in self.find('#project-release-title').text
|
||||
)
|
||||
# Layers
|
||||
title = layers.find_element(By.TAG_NAME, 'h3')
|
||||
self.assertTrue("Layers" in title.text)
|
||||
# check at least three layers are displayed
|
||||
# openembedded-core
|
||||
# meta-poky
|
||||
# meta-yocto-bsp
|
||||
layers_list = layers.find_element(By.ID, 'layers-in-project-list')
|
||||
layers_list_items = layers_list.find_elements(By.TAG_NAME, 'li')
|
||||
# remove all layers except the first three layers
|
||||
for i in range(3, len(layers_list_items)):
|
||||
layers_list_items[i].find_element(By.TAG_NAME, 'span').click()
|
||||
# check can add a layer if exists
|
||||
add_layer_input = layers.find_element(By.ID, 'layer-add-input')
|
||||
add_layer_input.send_keys('meta-oe')
|
||||
self.wait_until_visible('#layer-container > form > div > span > div')
|
||||
dropdown_item = self.driver.find_element(
|
||||
By.XPATH,
|
||||
'//*[@id="layer-container"]/form/div/span/div'
|
||||
)
|
||||
try:
|
||||
dropdown_item.click()
|
||||
except ElementClickInterceptedException:
|
||||
self.skipTest(
|
||||
"layer-container dropdown item click intercepted. Element not properly visible.")
|
||||
add_layer_btn = layers.find_element(By.ID, 'add-layer-btn')
|
||||
add_layer_btn.click()
|
||||
self.wait_until_visible('#layers-in-project-list')
|
||||
# check layer is added
|
||||
layers_list_items = layers_list.find_elements(By.TAG_NAME, 'li')
|
||||
self.assertTrue(len(layers_list_items) == 4)
|
||||
|
||||
def test_most_build_recipes(self):
|
||||
""" Test most build recipes block contains"""
|
||||
def rebuild_from_most_build_recipes(recipe_list_items):
|
||||
checkbox = recipe_list_items[0].find_element(By.TAG_NAME, 'input')
|
||||
checkbox.click()
|
||||
build_btn = self.find('#freq-build-btn')
|
||||
build_btn.click()
|
||||
self.wait_until_visible('#latest-builds')
|
||||
wait_until_build(self, 'queued cloning starting parsing failed')
|
||||
lastest_builds = self.driver.find_elements(
|
||||
By.XPATH,
|
||||
'//div[@id="latest-builds"]/div'
|
||||
)
|
||||
self.assertTrue(len(lastest_builds) >= 2)
|
||||
last_build = lastest_builds[0]
|
||||
try:
|
||||
cancel_button = last_build.find_element(
|
||||
By.XPATH,
|
||||
'//span[@class="cancel-build-btn pull-right alert-link"]',
|
||||
)
|
||||
cancel_button.click()
|
||||
except NoSuchElementException:
|
||||
# Skip if the build is already cancelled
|
||||
pass
|
||||
wait_until_build_cancelled(self)
|
||||
# Create a new project for remaining asserts
|
||||
project_name = self._random_string(10)
|
||||
self._create_project(project_name=project_name, release='2')
|
||||
current_url = self.driver.current_url
|
||||
TestProjectConfigTab.project_id = get_projectId_from_url(current_url)
|
||||
url = current_url.split('?')[0]
|
||||
|
||||
# Create a new builds
|
||||
self._create_builds()
|
||||
|
||||
# back to project page
|
||||
self.driver.get(url)
|
||||
|
||||
self.wait_until_visible('#project-page', poll=3)
|
||||
|
||||
# Most built recipes
|
||||
most_built_recipes = self.driver.find_element(
|
||||
By.XPATH, '//*[@id="project-page"]/div[1]/div[3]')
|
||||
title = most_built_recipes.find_element(By.TAG_NAME, 'h3')
|
||||
self.assertTrue("Most built recipes" in title.text)
|
||||
# check can select a recipe and build it
|
||||
self.wait_until_visible('#freq-build-list', poll=3)
|
||||
recipe_list = self.find('#freq-build-list')
|
||||
recipe_list_items = recipe_list.find_elements(By.TAG_NAME, 'li')
|
||||
self.assertTrue(
|
||||
len(recipe_list_items) > 0,
|
||||
msg="Any recipes found in the most built recipes list",
|
||||
)
|
||||
rebuild_from_most_build_recipes(recipe_list_items)
|
||||
TestProjectConfigTab.project_id = None # reset project id
|
||||
|
||||
def test_project_page_tab_importlayer(self):
|
||||
""" Test project page tab import layer """
|
||||
self._navigate_to_project_page()
|
||||
# navigate to "Import layers" tab
|
||||
import_layers_tab = self._get_tabs()[2]
|
||||
import_layers_tab.find_element(By.TAG_NAME, 'a').click()
|
||||
self.wait_until_visible('#layer-git-repo-url')
|
||||
|
||||
# Check git repo radio button
|
||||
git_repo_radio = self.find('#git-repo-radio')
|
||||
git_repo_radio.click()
|
||||
|
||||
# Set git repo url
|
||||
input_repo_url = self.find('#layer-git-repo-url')
|
||||
input_repo_url.send_keys('git://git.yoctoproject.org/meta-fake')
|
||||
# Blur the input to trigger the validation
|
||||
input_repo_url.send_keys(Keys.TAB)
|
||||
|
||||
# Check name is set
|
||||
input_layer_name = self.find('#import-layer-name')
|
||||
self.assertTrue(input_layer_name.get_attribute('value') == 'meta-fake')
|
||||
|
||||
# Set branch
|
||||
input_branch = self.find('#layer-git-ref')
|
||||
input_branch.send_keys('master')
|
||||
|
||||
# Import layer
|
||||
self.find('#import-and-add-btn').click()
|
||||
|
||||
# Check layer is added
|
||||
self.wait_until_visible('#layer-container')
|
||||
block_l = self.driver.find_element(
|
||||
By.XPATH, '//*[@id="project-page"]/div[2]')
|
||||
layers = block_l.find_element(By.ID, 'layer-container')
|
||||
layers_list = layers.find_element(By.ID, 'layers-in-project-list')
|
||||
layers_list_items = layers_list.find_elements(By.TAG_NAME, 'li')
|
||||
self.assertTrue(
|
||||
'meta-fake' in str(layers_list_items[-1].text)
|
||||
)
|
||||
|
||||
def test_project_page_custom_image_no_image(self):
|
||||
""" Test project page tab "New custom image" when no custom image """
|
||||
project_name = self._random_string(10)
|
||||
self._create_project(project_name=project_name)
|
||||
current_url = self.driver.current_url
|
||||
TestProjectConfigTab.project_id = get_projectId_from_url(current_url)
|
||||
# navigate to "Custom image" tab
|
||||
custom_image_section = self._get_config_nav_item(2)
|
||||
custom_image_section.click()
|
||||
self.wait_until_visible('#empty-state-customimagestable')
|
||||
|
||||
# Check message when no custom image
|
||||
self.assertTrue(
|
||||
"You have not created any custom images yet." in str(
|
||||
self.find('#empty-state-customimagestable').text
|
||||
)
|
||||
)
|
||||
div_empty_msg = self.find('#empty-state-customimagestable')
|
||||
link_create_custom_image = div_empty_msg.find_element(
|
||||
By.TAG_NAME, 'a')
|
||||
self.assertTrue(TestProjectConfigTab.project_id is not None)
|
||||
self.assertTrue(
|
||||
f"/toastergui/project/{TestProjectConfigTab.project_id}/newcustomimage" in str(
|
||||
link_create_custom_image.get_attribute('href')
|
||||
)
|
||||
)
|
||||
self.assertTrue(
|
||||
"Create your first custom image" in str(
|
||||
link_create_custom_image.text
|
||||
)
|
||||
)
|
||||
TestProjectConfigTab.project_id = None # reset project id
|
||||
|
||||
def test_project_page_image_recipe(self):
|
||||
""" Test project page section images
|
||||
- Check image recipes are displayed
|
||||
- Check search input
|
||||
- Check image recipe build button works
|
||||
- Check image recipe table features(show/hide column, pagination)
|
||||
"""
|
||||
self._navigate_to_project_page()
|
||||
# navigate to "Images section"
|
||||
images_section = self._get_config_nav_item(3)
|
||||
images_section.click()
|
||||
self.wait_until_visible('#imagerecipestable')
|
||||
rows = self.find_all('#imagerecipestable tbody tr')
|
||||
self.assertTrue(len(rows) > 0)
|
||||
|
||||
# Test search input
|
||||
self.wait_until_visible('#search-input-imagerecipestable')
|
||||
recipe_input = self.find('#search-input-imagerecipestable')
|
||||
recipe_input.send_keys('core-image-minimal')
|
||||
self.find('#search-submit-imagerecipestable').click()
|
||||
self.wait_until_visible('#imagerecipestable tbody tr')
|
||||
rows = self.find_all('#imagerecipestable tbody tr')
|
||||
self.assertTrue(len(rows) > 0)
|
||||
89
sources/poky/bitbake/lib/toaster/tests/functional/utils.py
Normal file
89
sources/poky/bitbake/lib/toaster/tests/functional/utils.py
Normal file
@@ -0,0 +1,89 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# BitBake Toaster UI tests implementation
|
||||
#
|
||||
# Copyright (C) 2023 Savoir-faire Linux
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
|
||||
|
||||
from time import sleep
|
||||
from selenium.common.exceptions import NoSuchElementException, StaleElementReferenceException, TimeoutException
|
||||
from selenium.webdriver.common.by import By
|
||||
|
||||
from orm.models import Build
|
||||
|
||||
|
||||
def wait_until_build(test_instance, state):
|
||||
timeout = 60
|
||||
start_time = 0
|
||||
build_state = ''
|
||||
while True:
|
||||
try:
|
||||
if start_time > timeout:
|
||||
raise TimeoutException(
|
||||
f'Build did not reach {state} state within {timeout} seconds'
|
||||
)
|
||||
last_build_state = test_instance.driver.find_element(
|
||||
By.XPATH,
|
||||
'//*[@id="latest-builds"]/div[1]//div[@class="build-state"]',
|
||||
)
|
||||
build_state = last_build_state.get_attribute(
|
||||
'data-build-state')
|
||||
state_text = state.lower().split()
|
||||
if any(x in str(build_state).lower() for x in state_text):
|
||||
return str(build_state).lower()
|
||||
if 'failed' in str(build_state).lower():
|
||||
break
|
||||
except NoSuchElementException:
|
||||
continue
|
||||
except TimeoutException:
|
||||
break
|
||||
start_time += 1
|
||||
sleep(1) # take a breath and try again
|
||||
|
||||
def wait_until_build_cancelled(test_instance):
|
||||
""" Cancel build take a while sometime, the method is to wait driver action
|
||||
until build being cancelled
|
||||
"""
|
||||
timeout = 30
|
||||
start_time = 0
|
||||
build = None
|
||||
while True:
|
||||
try:
|
||||
if start_time > timeout:
|
||||
raise TimeoutException(
|
||||
f'Build did not reach cancelled state within {timeout} seconds'
|
||||
)
|
||||
last_build_state = test_instance.driver.find_element(
|
||||
By.XPATH,
|
||||
'//*[@id="latest-builds"]/div[1]//div[@class="build-state"]',
|
||||
)
|
||||
build_state = last_build_state.get_attribute(
|
||||
'data-build-state')
|
||||
if 'failed' in str(build_state).lower():
|
||||
break
|
||||
if 'cancelling' in str(build_state).lower():
|
||||
# Change build state to cancelled
|
||||
if not build: # get build object only once
|
||||
build = Build.objects.last()
|
||||
build.outcome = Build.CANCELLED
|
||||
build.save()
|
||||
if 'cancelled' in str(build_state).lower():
|
||||
break
|
||||
except NoSuchElementException:
|
||||
continue
|
||||
except StaleElementReferenceException:
|
||||
continue
|
||||
except TimeoutException:
|
||||
break
|
||||
start_time += 1
|
||||
sleep(1) # take a breath and try again
|
||||
|
||||
def get_projectId_from_url(url):
|
||||
# url = 'http://domainename.com/toastergui/project/1656/whatever
|
||||
# or url = 'http://domainename.com/toastergui/project/1/
|
||||
# or url = 'http://domainename.com/toastergui/project/186
|
||||
assert '/toastergui/project/' in url, "URL is not valid"
|
||||
url_to_list = url.split('/toastergui/project/')
|
||||
return int(url_to_list[1].split('/')[0]) # project_id
|
||||
@@ -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
|
||||
4
sources/poky/bitbake/lib/toaster/tests/views/README
Normal file
4
sources/poky/bitbake/lib/toaster/tests/views/README
Normal 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
|
||||
544
sources/poky/bitbake/lib/toaster/tests/views/test_views.py
Normal file
544
sources/poky/bitbake/lib/toaster/tests/views/test_views.py
Normal 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)
|
||||
Reference in New Issue
Block a user