Initial commit

This commit is contained in:
Your Name
2026-04-23 17:07:55 +08:00
commit b7e39e063b
16725 changed files with 1625565 additions and 0 deletions
@@ -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,213 @@
#! /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
from selenium import webdriver
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
from selenium.common.exceptions import NoSuchElementException, \
StaleElementReferenceException, TimeoutException
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':
return webdriver.Chrome(
service_args=["--verbose", "--log-path=selenium.log"]
)
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):
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()
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)
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):
""" 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).until(is_present, msg)
return element
def wait_until_visible(self, selector):
""" 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).until(is_visible, msg)
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,221 @@
#! /usr/bin/env python3
#
# BitBake Toaster Implementation
#
# Copyright (C) 2013-2016 Intel Corporation
#
# SPDX-License-Identifier: GPL-2.0-only
#
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 TestAllBuildsPage(SeleniumTestCase):
""" Tests for all builds page /builds/ """
PROJECT_NAME = 'test project'
CLI_BUILDS_PROJECT_NAME = 'command line builds'
def setUp(self):
bbv = BitbakeVersion.objects.create(name='bbv1', giturl='/tmp/',
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_present(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_present('#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 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_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_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)
# 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')
# should see a rebuild button for non-command-line builds
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')
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)
# 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 = 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')
url = reverse('all-builds')
self.get(url)
# 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.assertEquals(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.assertEquals(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.assertEquals(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.assertEquals(len(links), 0, msg)
@@ -0,0 +1,205 @@
#! /usr/bin/env python3
#
# BitBake Toaster Implementation
#
# Copyright (C) 2013-2016 Intel Corporation
#
# SPDX-License-Identifier: GPL-2.0-only
#
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
from orm.models import ProjectVariable
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 _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 """
bbv = BitbakeVersion.objects.create(name='test bbv', giturl='/tmp/',
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_present('#projectstable tbody tr')
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)
@@ -0,0 +1,335 @@
#! /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, Release, BitbakeVersion, Build, LogMessage
from orm.models import Layer, Layer_Version, Recipe, CustomImageRecipe, Variable
class TestBuildDashboardPage(SeleniumTestCase):
""" Tests for the build dashboard /build/X """
def setUp(self):
bbv = BitbakeVersion.objects.create(name='bbv1', giturl='/tmp/',
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)
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,210 @@
#! /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, 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):
bbv = BitbakeVersion.objects.create(name='bbv1', giturl='/tmp/',
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.assertEquals(element.text, '1',
'package count should be shown for image builds')
selector = '[data-value="target-package-size"]'
element = self.find(selector)
self.assertEquals(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,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,96 @@
#! /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 orm.models import 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_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'))
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)
self.assertFalse(self.CLI_BUILDS_PROJECT_NAME in content,
'should not show builds for cli project')
@@ -0,0 +1,204 @@
#! /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 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 test_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.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))
inputs.send_keys("-edited")
# Save the new values
for save_btn in self.find_all(".change-btn"):
save_btn.click()
self.click("#save-changes-for-switch")
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)
self.click("#save-changes-for-switch")
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_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,199 @@
#! /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
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.assertRegexpMatches(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_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.assertRegexpMatches(element.get_attribute('innerHTML'),
'Tasks starting', 'build should show "tasks starting" status')
# first task finished; check tasks progress bar
task1.order = 1
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.order = 2
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.assertRegexpMatches(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.assertRegexpMatches(element.get_attribute('innerHTML'),
'Build cancelled', 'build should show "cancelled" status')
@@ -0,0 +1,149 @@
#! /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 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):
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
self.recipe = Recipe.objects.create(
name='core-image-minimal',
layer_version=layer_version,
is_image=True
)
# 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.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.assertRegexpMatches(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.assertRegexpMatches(element.text.strip(), expected)
@@ -0,0 +1,101 @@
#! /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 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.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")
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.enter_text('#new-project-name', project_name)
select = Select(self.find('#projectversion'))
select.select_by_value(str(self.release.pk))
element = self.wait_until_visible('#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,156 @@
#! /usr/bin/env python3
#
# BitBake Toaster Implementation
#
# Copyright (C) 2013-2016 Intel Corporation
#
# SPDX-License-Identifier: GPL-2.0-only
#
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):
bbv = BitbakeVersion.objects.create(name='bbv1', giturl='/tmp/',
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,217 @@
#! /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 orm.models import BitbakeVersion, Release, Project, ProjectVariable
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):
bbv = BitbakeVersion.objects.create(name='bbv1', giturl='/tmp/',
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,29 @@
#! /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')
@@ -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,148 @@
#! /usr/bin/env python3
#
# BitBake Toaster Implementation
#
# Copyright (C) 2013-2016 Intel Corporation
#
# SPDX-License-Identifier: GPL-2.0-only
#
from datetime import datetime
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
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)
bbv = BitbakeVersion.objects.create(name='test bbv', giturl='/tmp/',
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')
@@ -0,0 +1,14 @@
# Running build tests
These tests are to test the running of builds and the data produced by the builds.
Your oe build environment must be sourced/initialised for these tests to run.
The simplest way to run the tests are the following commands:
$ . oe-init-build-env
$ cd bitbake/lib/toaster/ # path my vary but this is into toaster's directory
$ DJANGO_SETTINGS_MODULE='toastermain.settings_test' ./manage.py test tests.builds
Optional environment variables:
- TOASTER_DIR (where toaster keeps it's artifacts)
- TOASTER_CONF a path to the toasterconf.json file. This will need to be set if you don't execute the tests from toaster's own directory.
@@ -0,0 +1,157 @@
#! /usr/bin/env python3
#
# BitBake Toaster Implementation
#
# Copyright (C) 2016 Intel Corporation
#
# SPDX-License-Identifier: GPL-2.0-only
#
import os
import sys
import time
import unittest
from orm.models import Project, Release, ProjectTarget, Build, ProjectVariable
from bldcontrol.models import BuildEnvironment
from bldcontrol.management.commands.runbuilds import Command\
as RunBuildsCommand
from django.core.management import call_command
import subprocess
import logging
logger = logging.getLogger("toaster")
# We use unittest.TestCase instead of django.test.TestCase because we don't
# want to wrap everything in a database transaction as an external process
# (bitbake needs access to the database)
def load_build_environment():
call_command('loaddata', 'settings.xml', app_label="orm")
call_command('loaddata', 'poky.xml', app_label="orm")
current_builddir = os.environ.get("BUILDDIR")
if current_builddir:
BuildTest.BUILDDIR = current_builddir
else:
# Setup a builddir based on default layout
# bitbake inside openebedded-core
oe_init_build_env_path = os.path.join(
os.path.dirname(os.path.abspath(__file__)),
os.pardir,
os.pardir,
os.pardir,
os.pardir,
os.pardir,
'oe-init-build-env'
)
if not os.path.exists(oe_init_build_env_path):
raise Exception("We had no BUILDDIR set and couldn't "
"find oe-init-build-env to set this up "
"ourselves please run oe-init-build-env "
"before running these tests")
oe_init_build_env_path = os.path.realpath(oe_init_build_env_path)
cmd = "bash -c 'source oe-init-build-env %s'" % BuildTest.BUILDDIR
p = subprocess.Popen(
cmd,
cwd=os.path.dirname(oe_init_build_env_path),
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
output, err = p.communicate()
p.wait()
logger.info("oe-init-build-env %s %s" % (output, err))
os.environ['BUILDDIR'] = BuildTest.BUILDDIR
# Setup the path to bitbake we know where to find this
bitbake_path = os.path.join(
os.path.dirname(os.path.abspath(__file__)),
os.pardir,
os.pardir,
os.pardir,
os.pardir,
'bin',
'bitbake')
if not os.path.exists(bitbake_path):
raise Exception("Could not find bitbake at the expected path %s"
% bitbake_path)
os.environ['BBBASEDIR'] = bitbake_path
class BuildTest(unittest.TestCase):
PROJECT_NAME = "Testbuild"
BUILDDIR = "/tmp/build/"
def build(self, target):
# So that the buildinfo helper uses the test database'
self.assertEqual(
os.environ.get('DJANGO_SETTINGS_MODULE', ''),
'toastermain.settings_test',
"Please initialise django with the tests settings: "
"DJANGO_SETTINGS_MODULE='toastermain.settings_test'")
built = self.target_already_built(target)
if built:
return built
load_build_environment()
BuildEnvironment.objects.get_or_create(
betype=BuildEnvironment.TYPE_LOCAL,
sourcedir=BuildTest.BUILDDIR,
builddir=BuildTest.BUILDDIR
)
release = Release.objects.get(name='local')
# Create a project for this build to run in
project = Project.objects.create_project(name=BuildTest.PROJECT_NAME,
release=release)
if os.environ.get("TOASTER_TEST_USE_SSTATE_MIRROR"):
ProjectVariable.objects.get_or_create(
name="SSTATE_MIRRORS",
value="file://.* http://sstate.yoctoproject.org/PATH;downloadfilename=PATH",
project=project)
ProjectTarget.objects.create(project=project,
target=target,
task="")
build_request = project.schedule_build()
# run runbuilds command to dispatch the build
# e.g. manage.py runubilds
RunBuildsCommand().runbuild()
build_pk = build_request.build.pk
while Build.objects.get(pk=build_pk).outcome == Build.IN_PROGRESS:
sys.stdout.write("\rBuilding %s %d%%" %
(target,
build_request.build.completeper()))
sys.stdout.flush()
time.sleep(1)
self.assertEqual(Build.objects.get(pk=build_pk).outcome,
Build.SUCCEEDED,
"Build did not SUCCEEDED")
logger.info("\nBuild finished %s" % build_request.build.outcome)
return build_request.build
def target_already_built(self, target):
""" If the target is already built no need to build it again"""
for build in Build.objects.filter(
project__name=BuildTest.PROJECT_NAME):
targets = build.target_set.values_list('target', flat=True)
if target in targets:
return build
return None
@@ -0,0 +1,373 @@
#! /usr/bin/env python3
#
# BitBake Toaster Implementation
#
# Copyright (C) 2016 Intel Corporation
#
# SPDX-License-Identifier: GPL-2.0-only
#
# Tests were part of openembedded-core oe selftest Authored by: Lucian Musat
# Ionut Chisanovici, Paul Eggleton and Cristian Iorga
import os
from django.db.models import Q
from orm.models import Target_Image_File, Target_Installed_Package, Task
from orm.models import Package_Dependency, Recipe_Dependency, Build
from orm.models import Task_Dependency, Package, Target, Recipe
from orm.models import CustomImagePackage
from tests.builds.buildtest import BuildTest
class BuildCoreImageMinimal(BuildTest):
"""Build core-image-minimal and test the results"""
def setUp(self):
self.completed_build = self.build("core-image-minimal")
# Check if build name is unique - tc_id=795
def test_Build_Unique_Name(self):
all_builds = Build.objects.all().count()
distinct_builds = Build.objects.values('id').distinct().count()
self.assertEqual(distinct_builds,
all_builds,
msg='Build name is not unique')
# Check if build cooker log path is unique - tc_id=819
def test_Build_Unique_Cooker_Log_Path(self):
distinct_path = Build.objects.values(
'cooker_log_path').distinct().count()
total_builds = Build.objects.values('id').count()
self.assertEqual(distinct_path,
total_builds,
msg='Build cooker log path is not unique')
# Check if task order is unique for one build - tc=824
def test_Task_Unique_Order(self):
total_task_order = Task.objects.filter(
build=self.built).values('order').count()
distinct_task_order = Task.objects.filter(
build=self.completed_build).values('order').distinct().count()
self.assertEqual(total_task_order,
distinct_task_order,
msg='Errors task order is not unique')
# Check task order sequence for one build - tc=825
def test_Task_Order_Sequence(self):
cnt_err = []
tasks = Task.objects.filter(
Q(build=self.completed_build),
~Q(order=None),
~Q(task_name__contains='_setscene')
).values('id', 'order').order_by("order")
cnt_tasks = 0
for task in tasks:
cnt_tasks += 1
if (task['order'] != cnt_tasks):
cnt_err.append(task['id'])
self.assertEqual(
len(cnt_err), 0, msg='Errors for task id: %s' % cnt_err)
# Check if disk_io matches the difference between EndTimeIO and
# StartTimeIO in build stats - tc=828
# def test_Task_Disk_IO_TC828(self):
# Check if outcome = 2 (SSTATE) then sstate_result must be 3 (RESTORED) -
# tc=832
def test_Task_If_Outcome_2_Sstate_Result_Must_Be_3(self):
tasks = Task.objects.filter(outcome=2).values('id', 'sstate_result')
cnt_err = []
for task in tasks:
if (task['sstate_result'] != 3):
cnt_err.append(task['id'])
self.assertEqual(len(cnt_err),
0,
msg='Errors for task id: %s' % cnt_err)
# Check if outcome = 1 (COVERED) or 3 (EXISTING) then sstate_result must
# be 0 (SSTATE_NA) - tc=833
def test_Task_If_Outcome_1_3_Sstate_Result_Must_Be_0(self):
tasks = Task.objects.filter(
outcome__in=(Task.OUTCOME_COVERED,
Task.OUTCOME_PREBUILT)).values('id',
'task_name',
'sstate_result')
cnt_err = []
for task in tasks:
if (task['sstate_result'] != Task.SSTATE_NA and
task['sstate_result'] != Task.SSTATE_MISS):
cnt_err.append({'id': task['id'],
'name': task['task_name'],
'sstate_result': task['sstate_result']})
self.assertEqual(len(cnt_err),
0,
msg='Errors for task id: %s' % cnt_err)
# Check if outcome is 0 (SUCCESS) or 4 (FAILED) then sstate_result must be
# 0 (NA), 1 (MISS) or 2 (FAILED) - tc=834
def test_Task_If_Outcome_0_4_Sstate_Result_Must_Be_0_1_2(self):
tasks = Task.objects.filter(
outcome__in=(0, 4)).values('id', 'sstate_result')
cnt_err = []
for task in tasks:
if (task['sstate_result'] not in [0, 1, 2]):
cnt_err.append(task['id'])
self.assertEqual(len(cnt_err),
0,
msg='Errors for task id: %s' % cnt_err)
# Check if task_executed = TRUE (1), script_type must be 0 (CODING_NA), 2
# (CODING_PYTHON), 3 (CODING_SHELL) - tc=891
def test_Task_If_Task_Executed_True_Script_Type_0_2_3(self):
tasks = Task.objects.filter(
task_executed=1).values('id', 'script_type')
cnt_err = []
for task in tasks:
if (task['script_type'] not in [0, 2, 3]):
cnt_err.append(task['id'])
self.assertEqual(len(cnt_err),
0,
msg='Errors for task id: %s' % cnt_err)
# Check if task_executed = TRUE (1), outcome must be 0 (SUCCESS) or 4
# (FAILED) - tc=836
def test_Task_If_Task_Executed_True_Outcome_0_4(self):
tasks = Task.objects.filter(task_executed=1).values('id', 'outcome')
cnt_err = []
for task in tasks:
if (task['outcome'] not in [0, 4]):
cnt_err.append(task['id'])
self.assertEqual(len(cnt_err),
0,
msg='Errors for task id: %s' % cnt_err)
# Check if task_executed = FALSE (0), script_type must be 0 - tc=890
def test_Task_If_Task_Executed_False_Script_Type_0(self):
tasks = Task.objects.filter(
task_executed=0).values('id', 'script_type')
cnt_err = []
for task in tasks:
if (task['script_type'] != 0):
cnt_err.append(task['id'])
self.assertEqual(len(cnt_err),
0,
msg='Errors for task id: %s' % cnt_err)
# Check if task_executed = FALSE (0) and build outcome = SUCCEEDED (0),
# task outcome must be 1 (COVERED), 2 (CACHED), 3 (PREBUILT), 5 (EMPTY) -
# tc=837
def test_Task_If_Task_Executed_False_Outcome_1_2_3_5(self):
builds = Build.objects.filter(outcome=0).values('id')
cnt_err = []
for build in builds:
tasks = Task.objects.filter(
build=build['id'], task_executed=0).values('id', 'outcome')
for task in tasks:
if (task['outcome'] not in [1, 2, 3, 5]):
cnt_err.append(task['id'])
self.assertEqual(len(cnt_err),
0,
msg='Errors for task id: %s' % cnt_err)
# Key verification - tc=888
def test_Target_Installed_Package(self):
rows = Target_Installed_Package.objects.values('id',
'target_id',
'package_id')
cnt_err = []
for row in rows:
target = Target.objects.filter(id=row['target_id']).values('id')
package = Package.objects.filter(id=row['package_id']).values('id')
if (not target or not package):
cnt_err.append(row['id'])
self.assertEqual(len(cnt_err),
0,
msg='Errors for target installed package id: %s' %
cnt_err)
# Key verification - tc=889
def test_Task_Dependency(self):
rows = Task_Dependency.objects.values('id',
'task_id',
'depends_on_id')
cnt_err = []
for row in rows:
task_id = Task.objects.filter(id=row['task_id']).values('id')
depends_on_id = Task.objects.filter(
id=row['depends_on_id']).values('id')
if (not task_id or not depends_on_id):
cnt_err.append(row['id'])
self.assertEqual(len(cnt_err),
0,
msg='Errors for task dependency id: %s' % cnt_err)
# Check if build target file_name is populated only if is_image=true AND
# orm_build.outcome=0 then if the file exists and its size matches
# the file_size value. Need to add the tc in the test run
def test_Target_File_Name_Populated(self):
builds = Build.objects.filter(outcome=0).values('id')
for build in builds:
targets = Target.objects.filter(
build_id=build['id'], is_image=1).values('id')
for target in targets:
target_files = Target_Image_File.objects.filter(
target_id=target['id']).values('id',
'file_name',
'file_size')
cnt_err = []
for file_info in target_files:
target_id = file_info['id']
target_file_name = file_info['file_name']
target_file_size = file_info['file_size']
if (not target_file_name or not target_file_size):
cnt_err.append(target_id)
else:
if (not os.path.exists(target_file_name)):
cnt_err.append(target_id)
else:
if (os.path.getsize(target_file_name) !=
target_file_size):
cnt_err.append(target_id)
self.assertEqual(len(cnt_err), 0,
msg='Errors for target image file id: %s' %
cnt_err)
# Key verification - tc=884
def test_Package_Dependency(self):
cnt_err = []
deps = Package_Dependency.objects.values(
'id', 'package_id', 'depends_on_id')
for dep in deps:
if (dep['package_id'] == dep['depends_on_id']):
cnt_err.append(dep['id'])
self.assertEqual(len(cnt_err), 0,
msg='Errors for package dependency id: %s' % cnt_err)
# Recipe key verification, recipe name does not depends on a recipe having
# the same name - tc=883
def test_Recipe_Dependency(self):
deps = Recipe_Dependency.objects.values(
'id', 'recipe_id', 'depends_on_id')
cnt_err = []
for dep in deps:
if (not dep['recipe_id'] or not dep['depends_on_id']):
cnt_err.append(dep['id'])
else:
name = Recipe.objects.filter(
id=dep['recipe_id']).values('name')
dep_name = Recipe.objects.filter(
id=dep['depends_on_id']).values('name')
if (name == dep_name):
cnt_err.append(dep['id'])
self.assertEqual(len(cnt_err), 0,
msg='Errors for recipe dependency id: %s' % cnt_err)
# Check if package name does not start with a number (0-9) - tc=846
def test_Package_Name_For_Number(self):
packages = Package.objects.filter(~Q(size=-1)).values('id', 'name')
cnt_err = []
for package in packages:
if (package['name'][0].isdigit() is True):
cnt_err.append(package['id'])
self.assertEqual(
len(cnt_err), 0, msg='Errors for package id: %s' % cnt_err)
# Check if package version starts with a number (0-9) - tc=847
def test_Package_Version_Starts_With_Number(self):
packages = Package.objects.filter(
~Q(size=-1)).values('id', 'version')
cnt_err = []
for package in packages:
if (package['version'][0].isdigit() is False):
cnt_err.append(package['id'])
self.assertEqual(
len(cnt_err), 0, msg='Errors for package id: %s' % cnt_err)
# Check if package revision starts with 'r' - tc=848
def test_Package_Revision_Starts_With_r(self):
packages = Package.objects.filter(
~Q(size=-1)).values('id', 'revision')
cnt_err = []
for package in packages:
if (package['revision'][0].startswith("r") is False):
cnt_err.append(package['id'])
self.assertEqual(
len(cnt_err), 0, msg='Errors for package id: %s' % cnt_err)
# Check the validity of the package build_id
# TC must be added in test run
def test_Package_Build_Id(self):
packages = Package.objects.filter(
~Q(size=-1)).values('id', 'build_id')
cnt_err = []
for package in packages:
build_id = Build.objects.filter(
id=package['build_id']).values('id')
if (not build_id):
# They have no build_id but if they are
# CustomImagePackage that's expected
try:
CustomImagePackage.objects.get(pk=package['id'])
except CustomImagePackage.DoesNotExist:
cnt_err.append(package['id'])
self.assertEqual(len(cnt_err),
0,
msg="Errors for package id: %s they have no build"
"associated with them" % cnt_err)
# Check the validity of package recipe_id
# TC must be added in test run
def test_Package_Recipe_Id(self):
packages = Package.objects.filter(
~Q(size=-1)).values('id', 'recipe_id')
cnt_err = []
for package in packages:
recipe_id = Recipe.objects.filter(
id=package['recipe_id']).values('id')
if (not recipe_id):
cnt_err.append(package['id'])
self.assertEqual(
len(cnt_err), 0, msg='Errors for package id: %s' % cnt_err)
# Check if package installed_size field is not null
# TC must be aded in test run
def test_Package_Installed_Size_Not_NULL(self):
packages = Package.objects.filter(
installed_size__isnull=True).values('id')
cnt_err = []
for package in packages:
cnt_err.append(package['id'])
self.assertEqual(
len(cnt_err), 0, msg='Errors for package id: %s' % cnt_err)
def test_custom_packages_generated(self):
"""Test if there is a corresponding generated CustomImagePackage"""
""" for each of the packages generated"""
missing_packages = []
for package in Package.objects.all():
try:
CustomImagePackage.objects.get(name=package.name)
except CustomImagePackage.DoesNotExist:
missing_packages.append(package.name)
self.assertEqual(len(missing_packages), 0,
"Some package were created from the build but their"
" corresponding CustomImagePackage was not found")
@@ -0,0 +1,49 @@
#! /usr/bin/env python3
#
# BitBake Toaster Implementation
#
# Copyright (C) 2016 Intel Corporation
#
# SPDX-License-Identifier: GPL-2.0-only
#
from django.test import TestCase
from django.core import management
from orm.models import Layer_Version, Layer, Release, ToasterSetting
class TestLoadDataFixtures(TestCase):
""" Test loading our 3 provided fixtures """
def test_run_loaddata_poky_command(self):
management.call_command('loaddata', 'poky')
num_releases = Release.objects.count()
self.assertTrue(
Layer_Version.objects.filter(
layer__name="meta-poky").count() == num_releases,
"Loaded poky fixture but don't have a meta-poky for all releases"
" defined")
def test_run_loaddata_oecore_command(self):
management.call_command('loaddata', 'oe-core')
# We only have the one layer for oe-core setup
self.assertTrue(
Layer.objects.filter(name="openembedded-core").count() > 0,
"Loaded oe-core fixture but still have no openemebedded-core"
" layer")
def test_run_loaddata_settings_command(self):
management.call_command('loaddata', 'settings')
self.assertTrue(
ToasterSetting.objects.filter(name="DEFAULT_RELEASE").count() > 0,
"Loaded settings but have no DEFAULT_RELEASE")
self.assertTrue(
ToasterSetting.objects.filter(
name__startswith="DEFCONF").count() > 0,
"Loaded settings but have no DEFCONF (default project "
"configuration values)")
@@ -0,0 +1,33 @@
#! /usr/bin/env python3
#
# BitBake Toaster Implementation
#
# Copyright (C) 2016 Intel Corporation
#
# SPDX-License-Identifier: GPL-2.0-only
#
from django.test import TestCase
from django.core import management
from orm.models import Layer_Version, Machine, Recipe
class TestLayerIndexUpdater(TestCase):
def test_run_lsupdates_command(self):
# Load some release information for us to fetch from the layer index
management.call_command('loaddata', 'poky')
old_layers_count = Layer_Version.objects.count()
old_recipes_count = Recipe.objects.count()
old_machines_count = Machine.objects.count()
# Now fetch the metadata from the layer index
management.call_command('lsupdates')
self.assertTrue(Layer_Version.objects.count() > old_layers_count,
"lsupdates ran but we still have no more layers!")
self.assertTrue(Recipe.objects.count() > old_recipes_count,
"lsupdates ran but we still have no more Recipes!")
self.assertTrue(Machine.objects.count() > old_machines_count,
"lsupdates ran but we still have no more Machines!")
@@ -0,0 +1,76 @@
#! /usr/bin/env python3
#
# BitBake Toaster Implementation
#
# Copyright (C) 2016 Intel Corporation
#
# SPDX-License-Identifier: GPL-2.0-only
#
import os
from django.test import TestCase
from django.core import management
from orm.models import signal_runbuilds
import threading
import time
import subprocess
import signal
class KillRunbuilds(threading.Thread):
""" Kill the runbuilds process after an amount of time """
def __init__(self, *args, **kwargs):
super(KillRunbuilds, self).__init__(*args, **kwargs)
self.daemon = True
def run(self):
time.sleep(5)
signal_runbuilds()
time.sleep(1)
pidfile_path = os.path.join(os.environ.get("BUILDDIR", "."),
".runbuilds.pid")
with open(pidfile_path) as pidfile:
pid = pidfile.read()
os.kill(int(pid), signal.SIGTERM)
class TestCommands(TestCase):
""" Sanity test that runbuilds executes OK """
def setUp(self):
os.environ.setdefault("DJANGO_SETTINGS_MODULE",
"toastermain.settings_test")
os.environ.setdefault("BUILDDIR",
"/tmp/")
# Setup a real database if needed for runbuilds process
# to connect to
management.call_command('migrate')
def test_runbuilds_command(self):
kill_runbuilds = KillRunbuilds()
kill_runbuilds.start()
manage_py = os.path.join(
os.path.dirname(os.path.abspath(__file__)),
os.pardir,
os.pardir,
"manage.py")
command = "%s runbuilds" % manage_py
process = subprocess.Popen(command,
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
(out, err) = process.communicate()
process.wait()
self.assertNotEqual(process.returncode, 1,
"Runbuilds returned an error %s" % err)
@@ -0,0 +1,57 @@
# The MIT License (MIT)
#
# Copyright (c) 2016 Damien Lespiau
#
# SPDX-License-Identifier: MIT
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import sys
try:
from StringIO import StringIO
except ImportError:
from io import StringIO
from contextlib import contextmanager
from django.core import management
from django.test import TestCase
@contextmanager
def capture(command, *args, **kwargs):
out, sys.stdout = sys.stdout, StringIO()
command(*args, **kwargs)
sys.stdout.seek(0)
yield sys.stdout.read()
sys.stdout = out
def makemigrations():
management.call_command('makemigrations')
class MigrationTest(TestCase):
def testPendingMigration(self):
"""Make sure there's no pending migration."""
with capture(makemigrations) as output:
self.assertEqual(output, "No changes detected\n")
@@ -0,0 +1,22 @@
# Running eventreplay tests
These tests use event log files produced by bitbake <target> -w <event log file>
You need to have event log files produced before running this tests.
At the moment of writing this document tests use 2 event log files: zlib.events
and core-image-minimal.events. They're not provided with the tests due to their
significant size.
Here is how to produce them:
$ . oe-init-build-env
$ rm -r tmp sstate-cache
$ bitbake core-image-minimal -w core-image-minimal.events
$ rm -rf tmp sstate-cache
$ bitbake zlib -w zlib.events
After that it should be possible to run eventreplay tests this way:
$ EVENTREPLAY_DIR=./ DJANGO_SETTINGS_MODULE=toastermain.settings_test ../bitbake/lib/toaster/manage.py test -v2 tests.eventreplay
Note that environment variable EVENTREPLAY_DIR should point to the directory with event log files.
@@ -0,0 +1,85 @@
#! /usr/bin/env python3
#
# BitBake Toaster Implementation
#
# Copyright (C) 2016 Intel Corporation
#
# SPDX-License-Identifier: GPL-2.0-only
#
# Tests were part of openembedded-core oe selftest Authored by: Lucian Musat
# Ionut Chisanovici, Paul Eggleton and Cristian Iorga
"""
Test toaster backend by playing build event log files
using toaster-eventreplay script
"""
import os
from subprocess import getstatusoutput
from pathlib import Path
from django.test import TestCase
from orm.models import Target_Installed_Package, Package, Build
class EventReplay(TestCase):
"""Base class for eventreplay test cases"""
def setUp(self):
"""
Setup build environment:
- set self.script to toaster-eventreplay path
- set self.eventplay_dir to the value of EVENTPLAY_DIR env variable
"""
bitbake_dir = Path(__file__.split('lib/toaster')[0])
self.script = bitbake_dir / 'bin' / 'toaster-eventreplay'
self.assertTrue(self.script.exists(), "%s doesn't exist")
self.eventplay_dir = os.getenv("EVENTREPLAY_DIR")
self.assertTrue(self.eventplay_dir,
"Environment variable EVENTREPLAY_DIR is not set")
def _replay(self, eventfile):
"""Run toaster-eventplay <eventfile>"""
eventpath = Path(self.eventplay_dir) / eventfile
status, output = getstatusoutput('%s %s' % (self.script, eventpath))
if status:
print(output)
self.assertEqual(status, 0)
class CoreImageMinimalEventReplay(EventReplay):
"""Replay core-image-minimal events"""
def test_installed_packages(self):
"""Test if all required packages have been installed"""
self._replay('core-image-minimal.events')
# test installed packages
packages = sorted(Target_Installed_Package.objects.\
values_list('package__name', flat=True))
self.assertEqual(packages, ['base-files', 'base-passwd', 'busybox',
'busybox-hwclock', 'busybox-syslog',
'busybox-udhcpc', 'eudev', 'glibc',
'init-ifupdown', 'initscripts',
'initscripts-functions', 'kernel-base',
'kernel-module-uvesafb', 'libkmod',
'modutils-initscripts', 'netbase',
'packagegroup-core-boot', 'run-postinsts',
'sysvinit', 'sysvinit-inittab',
'sysvinit-pidof', 'udev-cache',
'update-alternatives-opkg',
'update-rc.d', 'util-linux-libblkid',
'util-linux-libuuid', 'v86d', 'zlib'])
class ZlibEventReplay(EventReplay):
"""Replay zlib events"""
def test_replay_zlib(self):
"""Test if zlib build and package are in the database"""
self._replay("zlib.events")
self.assertEqual(Build.objects.last().target_set.last().target, "zlib")
self.assertTrue('zlib' in Package.objects.values_list('name', flat=True))
@@ -0,0 +1,110 @@
#! /usr/bin/env python3
#
# BitBake Toaster functional tests implementation
#
# Copyright (C) 2017 Intel Corporation
#
# SPDX-License-Identifier: GPL-2.0-only
#
import os
import logging
import subprocess
import signal
import time
import re
from tests.browser.selenium_helpers_base import SeleniumTestCaseBase
from tests.builds.buildtest import load_build_environment
logger = logging.getLogger("toaster")
class SeleniumFunctionalTestCase(SeleniumTestCaseBase):
wait_toaster_time = 5
@classmethod
def setUpClass(cls):
# So that the buildinfo helper uses the test database'
if os.environ.get('DJANGO_SETTINGS_MODULE', '') != \
'toastermain.settings_test':
raise RuntimeError("Please initialise django with the tests settings: " \
"DJANGO_SETTINGS_MODULE='toastermain.settings_test'")
load_build_environment()
# start toaster
cmd = "bash -c 'source toaster start'"
p = subprocess.Popen(
cmd,
cwd=os.environ.get("BUILDDIR"),
shell=True)
if p.wait() != 0:
raise RuntimeError("Can't initialize toaster")
super(SeleniumFunctionalTestCase, cls).setUpClass()
cls.live_server_url = 'http://localhost:8000/'
@classmethod
def tearDownClass(cls):
super(SeleniumFunctionalTestCase, cls).tearDownClass()
# XXX: source toaster stop gets blocked, to review why?
# from now send SIGTERM by hand
time.sleep(cls.wait_toaster_time)
builddir = os.environ.get("BUILDDIR")
with open(os.path.join(builddir, '.toastermain.pid'), 'r') as f:
toastermain_pid = int(f.read())
os.kill(toastermain_pid, signal.SIGTERM)
with open(os.path.join(builddir, '.runbuilds.pid'), 'r') as f:
runbuilds_pid = int(f.read())
os.kill(runbuilds_pid, signal.SIGTERM)
def get_URL(self):
rc=self.get_page_source()
project_url=re.search("(projectPageUrl\s:\s\")(.*)(\",)",rc)
return project_url.group(2)
def find_element_by_link_text_in_table(self, table_id, link_text):
"""
Assume there're multiple suitable "find_element_by_link_text".
In this circumstance we need to specify "table".
"""
try:
table_element = self.get_table_element(table_id)
element = table_element.find_element_by_link_text(link_text)
except self.NoSuchElementException:
print('no element found')
raise
return element
def get_table_element(self, table_id, *coordinate):
if len(coordinate) == 0:
#return whole-table element
element_xpath = "//*[@id='" + table_id + "']"
try:
element = self.driver.find_element_by_xpath(element_xpath)
except self.NoSuchElementException:
raise
return element
row = coordinate[0]
if len(coordinate) == 1:
#return whole-row element
element_xpath = "//*[@id='" + table_id + "']/tbody/tr[" + str(row) + "]"
try:
element = self.driver.find_element_by_xpath(element_xpath)
except self.NoSuchElementException:
return False
return element
#now we are looking for an element with specified X and Y
column = coordinate[1]
element_xpath = "//*[@id='" + table_id + "']/tbody/tr[" + str(row) + "]/td[" + str(column) + "]"
try:
element = self.driver.find_element_by_xpath(element_xpath)
except self.NoSuchElementException:
return False
return element
@@ -0,0 +1,230 @@
#! /usr/bin/env python3
#
# BitBake Toaster functional tests implementation
#
# Copyright (C) 2017 Intel Corporation
#
# SPDX-License-Identifier: GPL-2.0-only
#
import re
from tests.functional.functional_helpers import SeleniumFunctionalTestCase
from orm.models import Project
class FuntionalTestBasic(SeleniumFunctionalTestCase):
# testcase (1514)
def test_create_slenium_project(self):
project_name = 'selenium-project'
self.get('')
self.driver.find_element_by_link_text("To start building, create your first Toaster project").click()
self.driver.find_element_by_id("new-project-name").send_keys(project_name)
self.driver.find_element_by_id('projectversion').click()
self.driver.find_element_by_id("create-project-button").click()
element = self.wait_until_visible('#project-created-notification')
self.assertTrue(self.element_exists('#project-created-notification'),'Project creation notification not shown')
self.assertTrue(project_name in element.text,
"New project name not in new project notification")
self.assertTrue(Project.objects.filter(name=project_name).count(),
"New project not found in database")
# testcase (1515)
def test_verify_left_bar_menu(self):
self.get('')
self.wait_until_visible('#projectstable')
self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click()
self.assertTrue(self.element_exists('#config-nav'),'Configuration Tab does not exist')
project_URL=self.get_URL()
self.driver.find_element_by_xpath('//a[@href="'+project_URL+'"]').click()
try:
self.driver.find_element_by_xpath("//*[@id='config-nav']/ul/li/a[@href="+'"'+project_URL+'customimages/"'+"]").click()
self.assertTrue(re.search("Custom images",self.driver.find_element_by_xpath("//div[@class='col-md-10']").text),'Custom images information is not loading properly')
except:
self.fail(msg='No Custom images tab available')
try:
self.driver.find_element_by_xpath("//*[@id='config-nav']/ul/li/a[@href="+'"'+project_URL+'images/"'+"]").click()
self.assertTrue(re.search("Compatible image recipes",self.driver.find_element_by_xpath("//div[@class='col-md-10']").text),'The Compatible image recipes information is not loading properly')
except:
self.fail(msg='No Compatible image tab available')
try:
self.driver.find_element_by_xpath("//*[@id='config-nav']/ul/li/a[@href="+'"'+project_URL+'softwarerecipes/"'+"]").click()
self.assertTrue(re.search("Compatible software recipes",self.driver.find_element_by_xpath("//div[@class='col-md-10']").text),'The Compatible software recipe information is not loading properly')
except:
self.fail(msg='No Compatible software recipe tab available')
try:
self.driver.find_element_by_xpath("//*[@id='config-nav']/ul/li/a[@href="+'"'+project_URL+'machines/"'+"]").click()
self.assertTrue(re.search("Compatible machines",self.driver.find_element_by_xpath("//div[@class='col-md-10']").text),'The Compatible machine information is not loading properly')
except:
self.fail(msg='No Compatible machines tab available')
try:
self.driver.find_element_by_xpath("//*[@id='config-nav']/ul/li/a[@href="+'"'+project_URL+'layers/"'+"]").click()
self.assertTrue(re.search("Compatible layers",self.driver.find_element_by_xpath("//div[@class='col-md-10']").text),'The Compatible layer information is not loading properly')
except:
self.fail(msg='No Compatible layers tab available')
try:
self.driver.find_element_by_xpath("//*[@id='config-nav']/ul/li/a[@href="+'"'+project_URL+'configuration"'+"]").click()
self.assertTrue(re.search("Bitbake variables",self.driver.find_element_by_xpath("//div[@class='col-md-10']").text),'The Bitbake variables information is not loading properly')
except:
self.fail(msg='No Bitbake variables tab available')
# testcase (1516)
def test_review_configuration_information(self):
self.get('')
self.driver.find_element_by_xpath("//div[@id='global-nav']/ul/li/a[@href="+'"'+'/toastergui/projects/'+'"'+"]").click()
self.wait_until_visible('#projectstable')
self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click()
project_URL=self.get_URL()
try:
self.assertTrue(self.element_exists('#machine-section'),'Machine section for the project configuration page does not exist')
self.assertTrue(re.search("qemux86",self.driver.find_element_by_xpath("//span[@id='project-machine-name']").text),'The machine type is not assigned')
self.driver.find_element_by_xpath("//span[@id='change-machine-toggle']").click()
self.wait_until_visible('#select-machine-form')
self.wait_until_visible('#cancel-machine-change')
self.driver.find_element_by_xpath("//form[@id='select-machine-form']/a[@id='cancel-machine-change']").click()
except:
self.fail(msg='The machine information is wrong in the configuration page')
try:
self.driver.find_element_by_id('no-most-built')
except:
self.fail(msg='No Most built information in project detail page')
try:
self.assertTrue(re.search("Yocto Project master",self.driver.find_element_by_xpath("//span[@id='project-release-title']").text),'The project release is not defined')
except:
self.fail(msg='No project release title information in project detail page')
try:
self.driver.find_element_by_xpath("//div[@id='layer-container']")
self.assertTrue(re.search("3",self.driver.find_element_by_id("project-layers-count").text),'There should be 3 layers listed in the layer count')
layer_list = self.driver.find_element_by_id("layers-in-project-list")
layers = layer_list.find_elements_by_tag_name("li")
for layer in layers:
if re.match ("openembedded-core",layer.text):
print ("openembedded-core layer is a default layer in the project configuration")
elif re.match ("meta-poky",layer.text):
print ("meta-poky layer is a default layer in the project configuration")
elif re.match ("meta-yocto-bsp",layer.text):
print ("meta-yocto-bsp is a default layer in the project configuratoin")
else:
self.fail(msg='default layers are missing from the project configuration')
except:
self.fail(msg='No Layer information in project detail page')
# testcase (1517)
def test_verify_machine_information(self):
self.get('')
self.driver.find_element_by_xpath("//div[@id='global-nav']/ul/li/a[@href="+'"'+'/toastergui/projects/'+'"'+"]").click()
self.wait_until_visible('#projectstable')
self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click()
try:
self.assertTrue(self.element_exists('#machine-section'),'Machine section for the project configuration page does not exist')
self.assertTrue(re.search("qemux86",self.driver.find_element_by_id("project-machine-name").text),'The machine type is not assigned')
self.driver.find_element_by_id("change-machine-toggle").click()
self.wait_until_visible('#select-machine-form')
self.wait_until_visible('#cancel-machine-change')
self.driver.find_element_by_id("cancel-machine-change").click()
except:
self.fail(msg='The machine information is wrong in the configuration page')
# testcase (1518)
def test_verify_most_built_recipes_information(self):
self.get('')
self.driver.find_element_by_xpath("//div[@id='global-nav']/ul/li/a[@href="+'"'+'/toastergui/projects/'+'"'+"]").click()
self.wait_until_visible('#projectstable')
self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click()
project_URL=self.get_URL()
try:
self.assertTrue(re.search("You haven't built any recipes yet",self.driver.find_element_by_id("no-most-built").text),'Default message of no builds is not present')
self.driver.find_element_by_xpath("//div[@id='no-most-built']/p/a[@href="+'"'+project_URL+'images/"'+"]").click()
self.assertTrue(re.search("Compatible image recipes",self.driver.find_element_by_xpath("//div[@class='col-md-10']").text),'The Choose a recipe to build link is not working properly')
except:
self.fail(msg='No Most built information in project detail page')
# testcase (1519)
def test_verify_project_release_information(self):
self.get('')
self.driver.find_element_by_xpath("//div[@id='global-nav']/ul/li/a[@href="+'"'+'/toastergui/projects/'+'"'+"]").click()
self.wait_until_visible('#projectstable')
self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click()
try:
self.assertTrue(re.search("Yocto Project master",self.driver.find_element_by_id("project-release-title").text),'The project release is not defined')
except:
self.fail(msg='No project release title information in project detail page')
# testcase (1520)
def test_verify_layer_information(self):
self.get('')
self.driver.find_element_by_xpath("//div[@id='global-nav']/ul/li/a[@href="+'"'+'/toastergui/projects/'+'"'+"]").click()
self.wait_until_visible('#projectstable')
self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click()
project_URL=self.get_URL()
try:
self.driver.find_element_by_xpath("//div[@id='layer-container']")
self.assertTrue(re.search("3",self.driver.find_element_by_id("project-layers-count").text),'There should be 3 layers listed in the layer count')
layer_list = self.driver.find_element_by_id("layers-in-project-list")
layers = layer_list.find_elements_by_tag_name("li")
for layer in layers:
if re.match ("openembedded-core",layer.text):
print ("openembedded-core layer is a default layer in the project configuration")
elif re.match ("meta-poky",layer.text):
print ("meta-poky layer is a default layer in the project configuration")
elif re.match ("meta-yocto-bsp",layer.text):
print ("meta-yocto-bsp is a default layer in the project configuratoin")
else:
self.fail(msg='default layers are missing from the project configuration')
self.driver.find_element_by_xpath("//input[@id='layer-add-input']")
self.driver.find_element_by_xpath("//button[@id='add-layer-btn']")
self.driver.find_element_by_xpath("//div[@id='layer-container']/form[@class='form-inline']/p/a[@id='view-compatible-layers']")
self.driver.find_element_by_xpath("//div[@id='layer-container']/form[@class='form-inline']/p/a[@href="+'"'+project_URL+'importlayer"'+"]")
except:
self.fail(msg='No Layer information in project detail page')
# testcase (1521)
def test_verify_project_detail_links(self):
self.get('')
self.driver.find_element_by_xpath("//div[@id='global-nav']/ul/li/a[@href="+'"'+'/toastergui/projects/'+'"'+"]").click()
self.wait_until_visible('#projectstable')
self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click()
project_URL=self.get_URL()
self.driver.find_element_by_xpath("//div[@id='project-topbar']/ul[@class='nav nav-tabs']/li[@id='topbar-configuration-tab']/a[@href="+'"'+project_URL+'"'+"]").click()
self.assertTrue(re.search("Configuration",self.driver.find_element_by_xpath("//div[@id='project-topbar']/ul[@class='nav nav-tabs']/li[@id='topbar-configuration-tab']/a[@href="+'"'+project_URL+'"'+"]").text), 'Configuration tab in project topbar is misspelled')
try:
self.driver.find_element_by_xpath("//div[@id='project-topbar']/ul[@class='nav nav-tabs']/li/a[@href="+'"'+project_URL+'builds/"'+"]").click()
self.assertTrue(re.search("Builds",self.driver.find_element_by_xpath("//div[@id='project-topbar']/ul[@class='nav nav-tabs']/li/a[@href="+'"'+project_URL+'builds/"'+"]").text), 'Builds tab in project topbar is misspelled')
self.driver.find_element_by_xpath("//div[@id='empty-state-projectbuildstable']")
except:
self.fail(msg='Builds tab information is not present')
try:
self.driver.find_element_by_xpath("//div[@id='project-topbar']/ul[@class='nav nav-tabs']/li/a[@href="+'"'+project_URL+'importlayer"'+"]").click()
self.assertTrue(re.search("Import layer",self.driver.find_element_by_xpath("//div[@id='project-topbar']/ul[@class='nav nav-tabs']/li/a[@href="+'"'+project_URL+'importlayer"'+"]").text), 'Import layer tab in project topbar is misspelled')
self.driver.find_element_by_xpath("//fieldset[@id='repo-select']")
self.driver.find_element_by_xpath("//fieldset[@id='git-repo']")
except:
self.fail(msg='Import layer tab not loading properly')
try:
self.driver.find_element_by_xpath("//div[@id='project-topbar']/ul[@class='nav nav-tabs']/li/a[@href="+'"'+project_URL+'newcustomimage/"'+"]").click()
self.assertTrue(re.search("New custom image",self.driver.find_element_by_xpath("//div[@id='project-topbar']/ul[@class='nav nav-tabs']/li/a[@href="+'"'+project_URL+'newcustomimage/"'+"]").text), 'New custom image tab in project topbar is misspelled')
self.assertTrue(re.search("Select the image recipe you want to customise",self.driver.find_element_by_xpath("//div[@class='col-md-12']/h2").text),'The new custom image tab is not loading correctly')
except:
self.fail(msg='New custom image tab not loading properly')
@@ -0,0 +1 @@
selenium==2.49.2
@@ -0,0 +1,4 @@
Django unit tests to verify classes and functions based on django Views
To run just these tests use ./manage.py test tests.views
@@ -0,0 +1,528 @@
#! /usr/bin/env python3
#
# BitBake Toaster Implementation
#
# Copyright (C) 2013-2015 Intel Corporation
#
# SPDX-License-Identifier: GPL-2.0-only
#
"""Test cases for Toaster GUI and ReST."""
from django.test import TestCase
from django.test.client import RequestFactory
from django.urls import reverse
from django.db.models import Q
from orm.models import Project, Package
from orm.models import Layer_Version, Recipe
from orm.models import CustomImageRecipe
from orm.models import CustomImagePackage
import inspect
import toastergui
from toastergui.tables import SoftwareRecipesTable
import json
from bs4 import BeautifulSoup
import string
PROJECT_NAME = "test project"
PROJECT_NAME2 = "test project 2"
CLI_BUILDS_PROJECT_NAME = 'Command line builds'
class ViewTests(TestCase):
"""Tests to verify view APIs."""
fixtures = ['toastergui-unittest-data']
def setUp(self):
self.project = Project.objects.first()
self.recipe1 = Recipe.objects.get(pk=2)
self.customr = CustomImageRecipe.objects.first()
self.cust_package = CustomImagePackage.objects.first()
self.package = Package.objects.first()
self.lver = Layer_Version.objects.first()
def test_get_base_call_returns_html(self):
"""Basic test for all-projects view"""
response = self.client.get(reverse('all-projects'), follow=True)
self.assertEqual(response.status_code, 200)
self.assertTrue(response['Content-Type'].startswith('text/html'))
self.assertTemplateUsed(response, "projects-toastertable.html")
def test_get_json_call_returns_json(self):
"""Test for all projects output in json format"""
url = reverse('all-projects')
response = self.client.get(url, {"format": "json"}, follow=True)
self.assertEqual(response.status_code, 200)
self.assertTrue(response['Content-Type'].startswith(
'application/json'))
data = json.loads(response.content.decode('utf-8'))
self.assertTrue("error" in data)
self.assertEqual(data["error"], "ok")
self.assertTrue("rows" in data)
name_found = False
for row in data["rows"]:
name_found = row['name'].find(self.project.name)
self.assertTrue(name_found,
"project name not found in projects table")
def test_typeaheads(self):
"""Test typeahead ReST API"""
layers_url = reverse('xhr_layerstypeahead', args=(self.project.id,))
prj_url = reverse('xhr_projectstypeahead')
urls = [layers_url,
prj_url,
reverse('xhr_recipestypeahead', args=(self.project.id,)),
reverse('xhr_machinestypeahead', args=(self.project.id,))]
def basic_reponse_check(response, url):
"""Check data structure of http response."""
self.assertEqual(response.status_code, 200)
self.assertTrue(response['Content-Type'].startswith(
'application/json'))
data = json.loads(response.content.decode('utf-8'))
self.assertTrue("error" in data)
self.assertEqual(data["error"], "ok")
self.assertTrue("results" in data)
# We got a result so now check the fields
if len(data['results']) > 0:
result = data['results'][0]
self.assertTrue(len(result['name']) > 0)
self.assertTrue("detail" in result)
self.assertTrue(result['id'] > 0)
# Special check for the layers typeahead's extra fields
if url == layers_url:
self.assertTrue(len(result['layerdetailurl']) > 0)
self.assertTrue(len(result['vcs_url']) > 0)
self.assertTrue(len(result['vcs_reference']) > 0)
# Special check for project typeahead extra fields
elif url == prj_url:
self.assertTrue(len(result['projectPageUrl']) > 0)
return True
return False
for url in urls:
results = False
for typeing in list(string.ascii_letters):
response = self.client.get(url, {'search': typeing})
results = basic_reponse_check(response, url)
if results:
break
# After "typeing" the alpabet we should have result true
# from each of the urls
self.assertTrue(results)
def test_xhr_add_layer(self):
"""Test xhr_add API"""
# Test for importing an already existing layer
api_url = reverse('xhr_layer', args=(self.project.id,))
layer_data = {'vcs_url': "git://git.example.com/test",
'name': "base-layer",
'git_ref': "c12b9596afd236116b25ce26dbe0d793de9dc7ce",
'project_id': self.project.id,
'local_source_dir': "",
'add_to_project': True,
'dir_path': "/path/in/repository"}
layer_data_json = json.dumps(layer_data)
response = self.client.put(api_url, layer_data_json)
data = json.loads(response.content.decode('utf-8'))
self.assertEqual(response.status_code, 200)
self.assertEqual(data["error"], "ok")
self.assertTrue(
layer_data['name'] in
self.project.get_all_compatible_layer_versions().values_list(
'layer__name',
flat=True),
"Could not find imported layer in project's all layers list"
)
# Empty data passed
response = self.client.put(api_url, "{}")
data = json.loads(response.content.decode('utf-8'))
self.assertNotEqual(data["error"], "ok")
def test_custom_ok(self):
"""Test successful return from ReST API xhr_customrecipe"""
url = reverse('xhr_customrecipe')
params = {'name': 'custom', 'project': self.project.id,
'base': self.recipe1.id}
response = self.client.post(url, params)
self.assertEqual(response.status_code, 200)
data = json.loads(response.content.decode('utf-8'))
self.assertEqual(data['error'], 'ok')
self.assertTrue('url' in data)
# get recipe from the database
recipe = CustomImageRecipe.objects.get(project=self.project,
name=params['name'])
args = (self.project.id, recipe.id,)
self.assertEqual(reverse('customrecipe', args=args), data['url'])
def test_custom_incomplete_params(self):
"""Test not passing all required parameters to xhr_customrecipe"""
url = reverse('xhr_customrecipe')
for params in [{}, {'name': 'custom'},
{'name': 'custom', 'project': self.project.id}]:
response = self.client.post(url, params)
self.assertEqual(response.status_code, 200)
data = json.loads(response.content.decode('utf-8'))
self.assertNotEqual(data["error"], "ok")
def test_xhr_custom_wrong_project(self):
"""Test passing wrong project id to xhr_customrecipe"""
url = reverse('xhr_customrecipe')
params = {'name': 'custom', 'project': 0, "base": self.recipe1.id}
response = self.client.post(url, params)
self.assertEqual(response.status_code, 200)
data = json.loads(response.content.decode('utf-8'))
self.assertNotEqual(data["error"], "ok")
def test_xhr_custom_wrong_base(self):
"""Test passing wrong base recipe id to xhr_customrecipe"""
url = reverse('xhr_customrecipe')
params = {'name': 'custom', 'project': self.project.id, "base": 0}
response = self.client.post(url, params)
self.assertEqual(response.status_code, 200)
data = json.loads(response.content.decode('utf-8'))
self.assertNotEqual(data["error"], "ok")
def test_xhr_custom_details(self):
"""Test getting custom recipe details"""
url = reverse('xhr_customrecipe_id', args=(self.customr.id,))
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
expected = {"error": "ok",
"info": {'id': self.customr.id,
'name': self.customr.name,
'base_recipe_id': self.recipe1.id,
'project_id': self.project.id}}
self.assertEqual(json.loads(response.content.decode('utf-8')),
expected)
def test_xhr_custom_del(self):
"""Test deleting custom recipe"""
name = "to be deleted"
recipe = CustomImageRecipe.objects.create(
name=name, project=self.project,
base_recipe=self.recipe1,
file_path="/tmp/testing",
layer_version=self.customr.layer_version)
url = reverse('xhr_customrecipe_id', args=(recipe.id,))
response = self.client.delete(url)
self.assertEqual(response.status_code, 200)
gotoUrl = reverse('projectcustomimages', args=(self.project.pk,))
self.assertEqual(json.loads(response.content.decode('utf-8')),
{"error": "ok",
"gotoUrl": gotoUrl})
# try to delete not-existent recipe
url = reverse('xhr_customrecipe_id', args=(recipe.id,))
response = self.client.delete(url)
self.assertEqual(response.status_code, 200)
self.assertNotEqual(json.loads(
response.content.decode('utf-8'))["error"], "ok")
def test_xhr_custom_packages(self):
"""Test adding and deleting package to a custom recipe"""
# add self.package to recipe
response = self.client.put(reverse('xhr_customrecipe_packages',
args=(self.customr.id,
self.cust_package.id)))
self.assertEqual(response.status_code, 200)
self.assertEqual(json.loads(response.content.decode('utf-8')),
{"error": "ok"})
self.assertEqual(self.customr.appends_set.first().name,
self.cust_package.name)
# delete it
to_delete = self.customr.appends_set.first().pk
del_url = reverse('xhr_customrecipe_packages',
args=(self.customr.id, to_delete))
response = self.client.delete(del_url)
self.assertEqual(response.status_code, 200)
self.assertEqual(json.loads(response.content.decode('utf-8')),
{"error": "ok"})
all_packages = self.customr.get_all_packages().values_list('pk',
flat=True)
self.assertFalse(to_delete in all_packages)
# delete invalid package to test error condition
del_url = reverse('xhr_customrecipe_packages',
args=(self.customr.id,
99999))
response = self.client.delete(del_url)
self.assertEqual(response.status_code, 200)
self.assertNotEqual(json.loads(
response.content.decode('utf-8'))["error"], "ok")
def test_xhr_custom_packages_err(self):
"""Test error conditions of xhr_customrecipe_packages"""
# test calls with wrong recipe id and wrong package id
for args in [(0, self.package.id), (self.customr.id, 0)]:
url = reverse('xhr_customrecipe_packages', args=args)
# test put and delete methods
for method in (self.client.put, self.client.delete):
response = method(url)
self.assertEqual(response.status_code, 200)
self.assertNotEqual(json.loads(
response.content.decode('utf-8')),
{"error": "ok"})
def test_download_custom_recipe(self):
"""Download the recipe file generated for the custom image"""
# Create a dummy recipe file for the custom image generation to read
open("/tmp/a_recipe.bb", 'a').close()
response = self.client.get(reverse('customrecipedownload',
args=(self.project.id,
self.customr.id)))
self.assertEqual(response.status_code, 200)
def test_software_recipes_table(self):
"""Test structure returned for Software RecipesTable"""
table = SoftwareRecipesTable()
request = RequestFactory().get('/foo/', {'format': 'json'})
response = table.get(request, pid=self.project.id)
data = json.loads(response.content.decode('utf-8'))
recipes = Recipe.objects.filter(Q(is_image=False))
self.assertTrue(len(recipes) > 1,
"Need more than one software recipe to test "
"SoftwareRecipesTable")
recipe1 = recipes[0]
recipe2 = recipes[1]
rows = data['rows']
row1 = next(x for x in rows if x['name'] == recipe1.name)
row2 = next(x for x in rows if x['name'] == recipe2.name)
self.assertEqual(response.status_code, 200, 'should be 200 OK status')
# check other columns have been populated correctly
self.assertTrue(recipe1.name in row1['name'])
self.assertTrue(recipe1.version in row1['version'])
self.assertTrue(recipe1.description in
row1['get_description_or_summary'])
self.assertTrue(recipe1.layer_version.layer.name in
row1['layer_version__layer__name'])
self.assertTrue(recipe2.name in row2['name'])
self.assertTrue(recipe2.version in row2['version'])
self.assertTrue(recipe2.description in
row2['get_description_or_summary'])
self.assertTrue(recipe2.layer_version.layer.name in
row2['layer_version__layer__name'])
def test_toaster_tables(self):
"""Test all ToasterTables instances"""
def get_data(table, options={}):
"""Send a request and parse the json response"""
options['format'] = "json"
options['nocache'] = "true"
request = RequestFactory().get('/', options)
# This is the image recipe needed for a package list for
# PackagesTable do this here to throw a non exist exception
image_recipe = Recipe.objects.get(pk=4)
# Add any kwargs that are needed by any of the possible tables
args = {'pid': self.project.id,
'layerid': self.lver.pk,
'recipeid': self.recipe1.pk,
'recipe_id': image_recipe.pk,
'custrecipeid': self.customr.pk,
'build_id': 1,
'target_id': 1}
response = table.get(request, **args)
return json.loads(response.content.decode('utf-8'))
def get_text_from_td(td):
"""If we have html in the td then extract the text portion"""
# just so we don't waste time parsing non html
if "<" not in td:
ret = td
else:
ret = BeautifulSoup(td, "html.parser").text
if len(ret):
return "0"
else:
return ret
# Get a list of classes in tables module
tables = inspect.getmembers(toastergui.tables, inspect.isclass)
tables.extend(inspect.getmembers(toastergui.buildtables,
inspect.isclass))
for name, table_cls in tables:
# Filter out the non ToasterTables from the tables module
if not issubclass(table_cls, toastergui.widgets.ToasterTable) or \
table_cls == toastergui.widgets.ToasterTable or \
'Mixin' in name:
continue
# Get the table data without any options, this also does the
# initialisation of the table i.e. setup_columns,
# setup_filters and setup_queryset that we can use later
table = table_cls()
all_data = get_data(table)
self.assertTrue(len(all_data['rows']) > 1,
"Cannot test on a %s table with < 1 row" % name)
if table.default_orderby:
row_one = get_text_from_td(
all_data['rows'][0][table.default_orderby.strip("-")])
row_two = get_text_from_td(
all_data['rows'][1][table.default_orderby.strip("-")])
if '-' in table.default_orderby:
self.assertTrue(row_one >= row_two,
"Default ordering not working on %s"
" '%s' should be >= '%s'" %
(name, row_one, row_two))
else:
self.assertTrue(row_one <= row_two,
"Default ordering not working on %s"
" '%s' should be <= '%s'" %
(name, row_one, row_two))
# Test the column ordering and filtering functionality
for column in table.columns:
if column['orderable']:
# If a column is orderable test it in both order
# directions ordering on the columns field_name
ascending = get_data(table_cls(),
{"orderby": column['field_name']})
row_one = get_text_from_td(
ascending['rows'][0][column['field_name']])
row_two = get_text_from_td(
ascending['rows'][1][column['field_name']])
self.assertTrue(row_one <= row_two,
"Ascending sort applied but row 0: \"%s\""
" is less than row 1: \"%s\" "
"%s %s " %
(row_one, row_two,
column['field_name'], name))
descending = get_data(table_cls(),
{"orderby":
'-'+column['field_name']})
row_one = get_text_from_td(
descending['rows'][0][column['field_name']])
row_two = get_text_from_td(
descending['rows'][1][column['field_name']])
self.assertTrue(row_one >= row_two,
"Descending sort applied but row 0: %s"
"is greater than row 1: %s"
"field %s table %s" %
(row_one,
row_two,
column['field_name'], name))
# If the two start rows are the same we haven't actually
# changed the order
self.assertNotEqual(ascending['rows'][0],
descending['rows'][0],
"An orderby %s has not changed the "
"order of the data in table %s" %
(column['field_name'], name))
if column['filter_name']:
# If a filter is available for the column get the filter
# info. This contains what filter actions are defined.
filter_info = get_data(table_cls(),
{"cmd": "filterinfo",
"name": column['filter_name']})
self.assertTrue(len(filter_info['filter_actions']) > 0,
"Filter %s was defined but no actions "
"added to it" % column['filter_name'])
for filter_action in filter_info['filter_actions']:
# filter string to pass as the option
# This is the name of the filter:action
# e.g. project_filter:not_in_project
filter_string = "%s:%s" % (
column['filter_name'],
filter_action['action_name'])
# Now get the data with the filter applied
filtered_data = get_data(table_cls(),
{"filter": filter_string})
# date range filter actions can't specify the
# number of results they return, so their count is 0
if filter_action['count'] is not None:
self.assertEqual(
len(filtered_data['rows']),
int(filter_action['count']),
"We added a table filter for %s but "
"the number of rows returned was not "
"what the filter info said there "
"would be" % name)
# Test search functionality on the table
something_found = False
for search in list(string.ascii_letters):
search_data = get_data(table_cls(), {'search': search})
if len(search_data['rows']) > 0:
something_found = True
break
self.assertTrue(something_found,
"We went through the whole alphabet and nothing"
" was found for the search of table %s" % name)
# Test the limit functionality on the table
limited_data = get_data(table_cls(), {'limit': "1"})
self.assertEqual(len(limited_data['rows']),
1,
"Limit 1 set on table %s but not 1 row returned"
% name)
# Test the pagination functionality on the table
page_one_data = get_data(table_cls(), {'limit': "1",
"page": "1"})['rows'][0]
page_two_data = get_data(table_cls(), {'limit': "1",
"page": "2"})['rows'][0]
self.assertNotEqual(page_one_data,
page_two_data,
"Changed page on table %s but first row is"
" the same as the previous page" % name)