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:
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')
|
||||
Reference in New Issue
Block a user