summaryrefslogtreecommitdiffstats
path: root/poky/bitbake/lib/toaster/tests
diff options
context:
space:
mode:
Diffstat (limited to 'poky/bitbake/lib/toaster/tests')
-rw-r--r--poky/bitbake/lib/toaster/tests/__init__.py0
-rw-r--r--poky/bitbake/lib/toaster/tests/browser/README74
-rw-r--r--poky/bitbake/lib/toaster/tests/browser/__init__.py0
-rw-r--r--poky/bitbake/lib/toaster/tests/browser/selenium_helpers.py34
-rw-r--r--poky/bitbake/lib/toaster/tests/browser/selenium_helpers_base.py227
-rw-r--r--poky/bitbake/lib/toaster/tests/browser/test_all_builds_page.py233
-rw-r--r--poky/bitbake/lib/toaster/tests/browser/test_all_projects_page.py217
-rw-r--r--poky/bitbake/lib/toaster/tests/browser/test_builddashboard_page.py347
-rw-r--r--poky/bitbake/lib/toaster/tests/browser/test_builddashboard_page_artifacts.py222
-rw-r--r--poky/bitbake/lib/toaster/tests/browser/test_builddashboard_page_recipes.py66
-rw-r--r--poky/bitbake/lib/toaster/tests/browser/test_builddashboard_page_tasks.py65
-rw-r--r--poky/bitbake/lib/toaster/tests/browser/test_js_unit_tests.py57
-rw-r--r--poky/bitbake/lib/toaster/tests/browser/test_landing_page.py108
-rw-r--r--poky/bitbake/lib/toaster/tests/browser/test_layerdetails_page.py216
-rw-r--r--poky/bitbake/lib/toaster/tests/browser/test_most_recent_builds_states.py211
-rw-r--r--poky/bitbake/lib/toaster/tests/browser/test_new_custom_image_page.py161
-rw-r--r--poky/bitbake/lib/toaster/tests/browser/test_new_project_page.py113
-rw-r--r--poky/bitbake/lib/toaster/tests/browser/test_project_builds_page.py168
-rw-r--r--poky/bitbake/lib/toaster/tests/browser/test_project_config_page.py231
-rw-r--r--poky/bitbake/lib/toaster/tests/browser/test_project_page.py59
-rw-r--r--poky/bitbake/lib/toaster/tests/browser/test_sample.py41
-rw-r--r--poky/bitbake/lib/toaster/tests/browser/test_task_page.py76
-rw-r--r--poky/bitbake/lib/toaster/tests/browser/test_toastertable_ui.py160
-rw-r--r--poky/bitbake/lib/toaster/tests/builds/README14
-rw-r--r--poky/bitbake/lib/toaster/tests/builds/__init__.py0
-rw-r--r--poky/bitbake/lib/toaster/tests/builds/buildtest.py169
-rw-r--r--poky/bitbake/lib/toaster/tests/builds/test_core_image_min.py386
-rw-r--r--poky/bitbake/lib/toaster/tests/commands/__init__.py0
-rw-r--r--poky/bitbake/lib/toaster/tests/commands/test_loaddata.py61
-rw-r--r--poky/bitbake/lib/toaster/tests/commands/test_lsupdates.py45
-rw-r--r--poky/bitbake/lib/toaster/tests/commands/test_runbuilds.py88
-rw-r--r--poky/bitbake/lib/toaster/tests/db/__init__.py0
-rw-r--r--poky/bitbake/lib/toaster/tests/db/test_db.py55
-rw-r--r--poky/bitbake/lib/toaster/tests/eventreplay/README22
-rw-r--r--poky/bitbake/lib/toaster/tests/eventreplay/__init__.py97
-rw-r--r--poky/bitbake/lib/toaster/tests/functional/README0
-rw-r--r--poky/bitbake/lib/toaster/tests/functional/__init__.py0
-rw-r--r--poky/bitbake/lib/toaster/tests/functional/functional_helpers.py122
-rw-r--r--poky/bitbake/lib/toaster/tests/functional/test_functional_basic.py243
-rw-r--r--poky/bitbake/lib/toaster/tests/toaster-tests-requirements.txt1
-rw-r--r--poky/bitbake/lib/toaster/tests/views/README4
-rw-r--r--poky/bitbake/lib/toaster/tests/views/__init__.py0
-rw-r--r--poky/bitbake/lib/toaster/tests/views/test_views.py540
43 files changed, 4933 insertions, 0 deletions
diff --git a/poky/bitbake/lib/toaster/tests/__init__.py b/poky/bitbake/lib/toaster/tests/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/poky/bitbake/lib/toaster/tests/__init__.py
diff --git a/poky/bitbake/lib/toaster/tests/browser/README b/poky/bitbake/lib/toaster/tests/browser/README
new file mode 100644
index 000000000..352c4fe3e
--- /dev/null
+++ b/poky/bitbake/lib/toaster/tests/browser/README
@@ -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
diff --git a/poky/bitbake/lib/toaster/tests/browser/__init__.py b/poky/bitbake/lib/toaster/tests/browser/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/poky/bitbake/lib/toaster/tests/browser/__init__.py
diff --git a/poky/bitbake/lib/toaster/tests/browser/selenium_helpers.py b/poky/bitbake/lib/toaster/tests/browser/selenium_helpers.py
new file mode 100644
index 000000000..08711e455
--- /dev/null
+++ b/poky/bitbake/lib/toaster/tests/browser/selenium_helpers.py
@@ -0,0 +1,34 @@
+#! /usr/bin/env python
+# ex:ts=4:sw=4:sts=4:et
+# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
+#
+# BitBake Toaster Implementation
+#
+# Copyright (C) 2013-2016 Intel Corporation
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+#
+# 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
diff --git a/poky/bitbake/lib/toaster/tests/browser/selenium_helpers_base.py b/poky/bitbake/lib/toaster/tests/browser/selenium_helpers_base.py
new file mode 100644
index 000000000..156d639b1
--- /dev/null
+++ b/poky/bitbake/lib/toaster/tests/browser/selenium_helpers_base.py
@@ -0,0 +1,227 @@
+#! /usr/bin/env python
+# ex:ts=4:sw=4:sts=4:et
+# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
+#
+# BitBake Toaster Implementation
+#
+# Copyright (C) 2013-2016 Intel Corporation
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+#
+# 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 django.contrib.staticfiles.testing import StaticLiveServerTestCase
+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
diff --git a/poky/bitbake/lib/toaster/tests/browser/test_all_builds_page.py b/poky/bitbake/lib/toaster/tests/browser/test_all_builds_page.py
new file mode 100644
index 000000000..b86f29bdd
--- /dev/null
+++ b/poky/bitbake/lib/toaster/tests/browser/test_all_builds_page.py
@@ -0,0 +1,233 @@
+#! /usr/bin/env python
+# ex:ts=4:sw=4:sts=4:et
+# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
+#
+# BitBake Toaster Implementation
+#
+# Copyright (C) 2013-2016 Intel Corporation
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+import re
+
+from django.core.urlresolvers 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)
diff --git a/poky/bitbake/lib/toaster/tests/browser/test_all_projects_page.py b/poky/bitbake/lib/toaster/tests/browser/test_all_projects_page.py
new file mode 100644
index 000000000..44da64075
--- /dev/null
+++ b/poky/bitbake/lib/toaster/tests/browser/test_all_projects_page.py
@@ -0,0 +1,217 @@
+#! /usr/bin/env python
+# ex:ts=4:sw=4:sts=4:et
+# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
+#
+# BitBake Toaster Implementation
+#
+# Copyright (C) 2013-2016 Intel Corporation
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+import re
+
+from django.core.urlresolvers 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)
diff --git a/poky/bitbake/lib/toaster/tests/browser/test_builddashboard_page.py b/poky/bitbake/lib/toaster/tests/browser/test_builddashboard_page.py
new file mode 100644
index 000000000..f8ccb5452
--- /dev/null
+++ b/poky/bitbake/lib/toaster/tests/browser/test_builddashboard_page.py
@@ -0,0 +1,347 @@
+#! /usr/bin/env python
+# ex:ts=4:sw=4:sts=4:et
+# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
+#
+# BitBake Toaster Implementation
+#
+# Copyright (C) 2013-2016 Intel Corporation
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+from django.core.urlresolvers 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)
diff --git a/poky/bitbake/lib/toaster/tests/browser/test_builddashboard_page_artifacts.py b/poky/bitbake/lib/toaster/tests/browser/test_builddashboard_page_artifacts.py
new file mode 100644
index 000000000..1c627ad49
--- /dev/null
+++ b/poky/bitbake/lib/toaster/tests/browser/test_builddashboard_page_artifacts.py
@@ -0,0 +1,222 @@
+#! /usr/bin/env python
+# ex:ts=4:sw=4:sts=4:et
+# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
+#
+# BitBake Toaster Implementation
+#
+# Copyright (C) 2013-2016 Intel Corporation
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+from django.core.urlresolvers 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')
diff --git a/poky/bitbake/lib/toaster/tests/browser/test_builddashboard_page_recipes.py b/poky/bitbake/lib/toaster/tests/browser/test_builddashboard_page_recipes.py
new file mode 100644
index 000000000..ed18324e5
--- /dev/null
+++ b/poky/bitbake/lib/toaster/tests/browser/test_builddashboard_page_recipes.py
@@ -0,0 +1,66 @@
+#! /usr/bin/env python
+# ex:ts=4:sw=4:sts=4:et
+# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
+#
+# BitBake Toaster Implementation
+#
+# Copyright (C) 2013-2016 Intel Corporation
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+from django.core.urlresolvers 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)
diff --git a/poky/bitbake/lib/toaster/tests/browser/test_builddashboard_page_tasks.py b/poky/bitbake/lib/toaster/tests/browser/test_builddashboard_page_tasks.py
new file mode 100644
index 000000000..da50f1601
--- /dev/null
+++ b/poky/bitbake/lib/toaster/tests/browser/test_builddashboard_page_tasks.py
@@ -0,0 +1,65 @@
+#! /usr/bin/env python
+# ex:ts=4:sw=4:sts=4:et
+# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
+#
+# BitBake Toaster Implementation
+#
+# Copyright (C) 2013-2016 Intel Corporation
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+from django.core.urlresolvers 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)
diff --git a/poky/bitbake/lib/toaster/tests/browser/test_js_unit_tests.py b/poky/bitbake/lib/toaster/tests/browser/test_js_unit_tests.py
new file mode 100644
index 000000000..3c0b96252
--- /dev/null
+++ b/poky/bitbake/lib/toaster/tests/browser/test_js_unit_tests.py
@@ -0,0 +1,57 @@
+#! /usr/bin/env python
+# ex:ts=4:sw=4:sts=4:et
+# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
+#
+# BitBake Toaster Implementation
+#
+# Copyright (C) 2013-2016 Intel Corporation
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+"""
+Run the js unit tests
+"""
+
+from django.core.urlresolvers 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)
diff --git a/poky/bitbake/lib/toaster/tests/browser/test_landing_page.py b/poky/bitbake/lib/toaster/tests/browser/test_landing_page.py
new file mode 100644
index 000000000..4d4cd660f
--- /dev/null
+++ b/poky/bitbake/lib/toaster/tests/browser/test_landing_page.py
@@ -0,0 +1,108 @@
+#! /usr/bin/env python
+# ex:ts=4:sw=4:sts=4:et
+# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
+#
+# BitBake Toaster Implementation
+#
+# Copyright (C) 2013-2016 Intel Corporation
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+from django.core.urlresolvers 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')
diff --git a/poky/bitbake/lib/toaster/tests/browser/test_layerdetails_page.py b/poky/bitbake/lib/toaster/tests/browser/test_layerdetails_page.py
new file mode 100644
index 000000000..f24fb093a
--- /dev/null
+++ b/poky/bitbake/lib/toaster/tests/browser/test_layerdetails_page.py
@@ -0,0 +1,216 @@
+#! /usr/bin/env python
+# ex:ts=4:sw=4:sts=4:et
+# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
+#
+# BitBake Toaster Implementation
+#
+# Copyright (C) 2013-2016 Intel Corporation
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+from django.core.urlresolvers 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))
diff --git a/poky/bitbake/lib/toaster/tests/browser/test_most_recent_builds_states.py b/poky/bitbake/lib/toaster/tests/browser/test_most_recent_builds_states.py
new file mode 100644
index 000000000..abc0b0bc8
--- /dev/null
+++ b/poky/bitbake/lib/toaster/tests/browser/test_most_recent_builds_states.py
@@ -0,0 +1,211 @@
+#! /usr/bin/env python
+# ex:ts=4:sw=4:sts=4:et
+# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
+#
+# BitBake Toaster Implementation
+#
+# Copyright (C) 2013-2016 Intel Corporation
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+from django.core.urlresolvers 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')
diff --git a/poky/bitbake/lib/toaster/tests/browser/test_new_custom_image_page.py b/poky/bitbake/lib/toaster/tests/browser/test_new_custom_image_page.py
new file mode 100644
index 000000000..ab5a8e66b
--- /dev/null
+++ b/poky/bitbake/lib/toaster/tests/browser/test_new_custom_image_page.py
@@ -0,0 +1,161 @@
+#! /usr/bin/env python
+# ex:ts=4:sw=4:sts=4:et
+# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
+#
+# BitBake Toaster Implementation
+#
+# Copyright (C) 2013-2016 Intel Corporation
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+from django.core.urlresolvers 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)
diff --git a/poky/bitbake/lib/toaster/tests/browser/test_new_project_page.py b/poky/bitbake/lib/toaster/tests/browser/test_new_project_page.py
new file mode 100644
index 000000000..77e5f1526
--- /dev/null
+++ b/poky/bitbake/lib/toaster/tests/browser/test_new_project_page.py
@@ -0,0 +1,113 @@
+#! /usr/bin/env python
+# ex:ts=4:sw=4:sts=4:et
+# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
+#
+# BitBake Toaster Implementation
+#
+# Copyright (C) 2013-2016 Intel Corporation
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+from django.core.urlresolvers 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")
diff --git a/poky/bitbake/lib/toaster/tests/browser/test_project_builds_page.py b/poky/bitbake/lib/toaster/tests/browser/test_project_builds_page.py
new file mode 100644
index 000000000..9fe91ab06
--- /dev/null
+++ b/poky/bitbake/lib/toaster/tests/browser/test_project_builds_page.py
@@ -0,0 +1,168 @@
+#! /usr/bin/env python
+# ex:ts=4:sw=4:sts=4:et
+# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
+#
+# BitBake Toaster Implementation
+#
+# Copyright (C) 2013-2016 Intel Corporation
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+import re
+
+from django.core.urlresolvers 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')
diff --git a/poky/bitbake/lib/toaster/tests/browser/test_project_config_page.py b/poky/bitbake/lib/toaster/tests/browser/test_project_config_page.py
new file mode 100644
index 000000000..071008499
--- /dev/null
+++ b/poky/bitbake/lib/toaster/tests/browser/test_project_config_page.py
@@ -0,0 +1,231 @@
+#! /usr/bin/env python
+# ex:ts=4:sw=4:sts=4:et
+# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
+#
+# BitBake Toaster Implementation
+#
+# Copyright (C) 2013-2016 Intel Corporation
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+import re
+
+from django.core.urlresolvers import reverse
+from django.utils import timezone
+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') \ No newline at end of file
diff --git a/poky/bitbake/lib/toaster/tests/browser/test_project_page.py b/poky/bitbake/lib/toaster/tests/browser/test_project_page.py
new file mode 100644
index 000000000..018646332
--- /dev/null
+++ b/poky/bitbake/lib/toaster/tests/browser/test_project_page.py
@@ -0,0 +1,59 @@
+#! /usr/bin/env python
+# ex:ts=4:sw=4:sts=4:et
+# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
+#
+# BitBake Toaster Implementation
+#
+# Copyright (C) 2013-2016 Intel Corporation
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+from django.core.urlresolvers 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)
diff --git a/poky/bitbake/lib/toaster/tests/browser/test_sample.py b/poky/bitbake/lib/toaster/tests/browser/test_sample.py
new file mode 100644
index 000000000..20ec53c28
--- /dev/null
+++ b/poky/bitbake/lib/toaster/tests/browser/test_sample.py
@@ -0,0 +1,41 @@
+#! /usr/bin/env python
+# ex:ts=4:sw=4:sts=4:et
+# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
+#
+# BitBake Toaster Implementation
+#
+# Copyright (C) 2013-2016 Intel Corporation
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+"""
+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.core.urlresolvers 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')
diff --git a/poky/bitbake/lib/toaster/tests/browser/test_task_page.py b/poky/bitbake/lib/toaster/tests/browser/test_task_page.py
new file mode 100644
index 000000000..690d116cb
--- /dev/null
+++ b/poky/bitbake/lib/toaster/tests/browser/test_task_page.py
@@ -0,0 +1,76 @@
+#! /usr/bin/env python
+# ex:ts=4:sw=4:sts=4:et
+# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
+#
+# BitBake Toaster Implementation
+#
+# Copyright (C) 2013-2016 Intel Corporation
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+from django.core.urlresolvers 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')
diff --git a/poky/bitbake/lib/toaster/tests/browser/test_toastertable_ui.py b/poky/bitbake/lib/toaster/tests/browser/test_toastertable_ui.py
new file mode 100644
index 000000000..53ddf30c3
--- /dev/null
+++ b/poky/bitbake/lib/toaster/tests/browser/test_toastertable_ui.py
@@ -0,0 +1,160 @@
+#! /usr/bin/env python
+# ex:ts=4:sw=4:sts=4:et
+# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
+#
+# BitBake Toaster Implementation
+#
+# Copyright (C) 2013-2016 Intel Corporation
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+from datetime import datetime
+
+from django.core.urlresolvers 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')
diff --git a/poky/bitbake/lib/toaster/tests/builds/README b/poky/bitbake/lib/toaster/tests/builds/README
new file mode 100644
index 000000000..4a3b5328b
--- /dev/null
+++ b/poky/bitbake/lib/toaster/tests/builds/README
@@ -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.
diff --git a/poky/bitbake/lib/toaster/tests/builds/__init__.py b/poky/bitbake/lib/toaster/tests/builds/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/poky/bitbake/lib/toaster/tests/builds/__init__.py
diff --git a/poky/bitbake/lib/toaster/tests/builds/buildtest.py b/poky/bitbake/lib/toaster/tests/builds/buildtest.py
new file mode 100644
index 000000000..5a56a110a
--- /dev/null
+++ b/poky/bitbake/lib/toaster/tests/builds/buildtest.py
@@ -0,0 +1,169 @@
+#! /usr/bin/env python
+# ex:ts=4:sw=4:sts=4:et
+# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
+#
+# BitBake Toaster Implementation
+#
+# Copyright (C) 2016 Intel Corporation
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+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://autobuilder.yoctoproject.org/pub/sstate/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
diff --git a/poky/bitbake/lib/toaster/tests/builds/test_core_image_min.py b/poky/bitbake/lib/toaster/tests/builds/test_core_image_min.py
new file mode 100644
index 000000000..586f4a8f7
--- /dev/null
+++ b/poky/bitbake/lib/toaster/tests/builds/test_core_image_min.py
@@ -0,0 +1,386 @@
+#! /usr/bin/env python
+# ex:ts=4:sw=4:sts=4:et
+# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
+#
+# BitBake Toaster Implementation
+#
+# Copyright (C) 2016 Intel Corporation
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+# 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")
diff --git a/poky/bitbake/lib/toaster/tests/commands/__init__.py b/poky/bitbake/lib/toaster/tests/commands/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/poky/bitbake/lib/toaster/tests/commands/__init__.py
diff --git a/poky/bitbake/lib/toaster/tests/commands/test_loaddata.py b/poky/bitbake/lib/toaster/tests/commands/test_loaddata.py
new file mode 100644
index 000000000..951f6ff5a
--- /dev/null
+++ b/poky/bitbake/lib/toaster/tests/commands/test_loaddata.py
@@ -0,0 +1,61 @@
+#! /usr/bin/env python
+# ex:ts=4:sw=4:sts=4:et
+# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
+#
+# BitBake Toaster Implementation
+#
+# Copyright (C) 2016 Intel Corporation
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+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)")
diff --git a/poky/bitbake/lib/toaster/tests/commands/test_lsupdates.py b/poky/bitbake/lib/toaster/tests/commands/test_lsupdates.py
new file mode 100644
index 000000000..49897a476
--- /dev/null
+++ b/poky/bitbake/lib/toaster/tests/commands/test_lsupdates.py
@@ -0,0 +1,45 @@
+#! /usr/bin/env python
+# ex:ts=4:sw=4:sts=4:et
+# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
+#
+# BitBake Toaster Implementation
+#
+# Copyright (C) 2016 Intel Corporation
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+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!")
diff --git a/poky/bitbake/lib/toaster/tests/commands/test_runbuilds.py b/poky/bitbake/lib/toaster/tests/commands/test_runbuilds.py
new file mode 100644
index 000000000..3e634835e
--- /dev/null
+++ b/poky/bitbake/lib/toaster/tests/commands/test_runbuilds.py
@@ -0,0 +1,88 @@
+#! /usr/bin/env python
+# ex:ts=4:sw=4:sts=4:et
+# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
+#
+# BitBake Toaster Implementation
+#
+# Copyright (C) 2016 Intel Corporation
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+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.setDaemon(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)
diff --git a/poky/bitbake/lib/toaster/tests/db/__init__.py b/poky/bitbake/lib/toaster/tests/db/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/poky/bitbake/lib/toaster/tests/db/__init__.py
diff --git a/poky/bitbake/lib/toaster/tests/db/test_db.py b/poky/bitbake/lib/toaster/tests/db/test_db.py
new file mode 100644
index 000000000..a0f5f6ec0
--- /dev/null
+++ b/poky/bitbake/lib/toaster/tests/db/test_db.py
@@ -0,0 +1,55 @@
+# The MIT License (MIT)
+#
+# Copyright (c) 2016 Damien Lespiau
+#
+# 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")
diff --git a/poky/bitbake/lib/toaster/tests/eventreplay/README b/poky/bitbake/lib/toaster/tests/eventreplay/README
new file mode 100644
index 000000000..8c5bb6432
--- /dev/null
+++ b/poky/bitbake/lib/toaster/tests/eventreplay/README
@@ -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.
diff --git a/poky/bitbake/lib/toaster/tests/eventreplay/__init__.py b/poky/bitbake/lib/toaster/tests/eventreplay/__init__.py
new file mode 100644
index 000000000..695661947
--- /dev/null
+++ b/poky/bitbake/lib/toaster/tests/eventreplay/__init__.py
@@ -0,0 +1,97 @@
+#! /usr/bin/env python
+# ex:ts=4:sw=4:sts=4:et
+# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
+#
+# BitBake Toaster Implementation
+#
+# Copyright (C) 2016 Intel Corporation
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+# 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))
diff --git a/poky/bitbake/lib/toaster/tests/functional/README b/poky/bitbake/lib/toaster/tests/functional/README
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/poky/bitbake/lib/toaster/tests/functional/README
diff --git a/poky/bitbake/lib/toaster/tests/functional/__init__.py b/poky/bitbake/lib/toaster/tests/functional/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/poky/bitbake/lib/toaster/tests/functional/__init__.py
diff --git a/poky/bitbake/lib/toaster/tests/functional/functional_helpers.py b/poky/bitbake/lib/toaster/tests/functional/functional_helpers.py
new file mode 100644
index 000000000..486078a61
--- /dev/null
+++ b/poky/bitbake/lib/toaster/tests/functional/functional_helpers.py
@@ -0,0 +1,122 @@
+#! /usr/bin/env python
+# ex:ts=4:sw=4:sts=4:et
+# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
+#
+# BitBake Toaster functional tests implementation
+#
+# Copyright (C) 2017 Intel Corporation
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+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 NoSuchElementException as e:
+ print('no element found')
+ raise
+ return element
+
+ def get_table_element(self, table_id, *coordinate):
+ if len(coordinate) == 0:
+#return whole-table element
+ element_xpath = "//*[@id='" + table_id + "']"
+ try:
+ element = self.driver.find_element_by_xpath(element_xpath)
+ except NoSuchElementException as e:
+ raise
+ return element
+ row = coordinate[0]
+
+ if len(coordinate) == 1:
+#return whole-row element
+ element_xpath = "//*[@id='" + table_id + "']/tbody/tr[" + str(row) + "]"
+ try:
+ element = self.driver.find_element_by_xpath(element_xpath)
+ except NoSuchElementException as e:
+ return False
+ return element
+#now we are looking for an element with specified X and Y
+ column = coordinate[1]
+
+ element_xpath = "//*[@id='" + table_id + "']/tbody/tr[" + str(row) + "]/td[" + str(column) + "]"
+ try:
+ element = self.driver.find_element_by_xpath(element_xpath)
+ except NoSuchElementException as e:
+ return False
+ return element
diff --git a/poky/bitbake/lib/toaster/tests/functional/test_functional_basic.py b/poky/bitbake/lib/toaster/tests/functional/test_functional_basic.py
new file mode 100644
index 000000000..cfa2b0fdf
--- /dev/null
+++ b/poky/bitbake/lib/toaster/tests/functional/test_functional_basic.py
@@ -0,0 +1,243 @@
+#! /usr/bin/env python
+# ex:ts=4:sw=4:sts=4:et
+# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
+#
+# BitBake Toaster functional tests implementation
+#
+# Copyright (C) 2017 Intel Corporation
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+import time
+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')
+
+
+
diff --git a/poky/bitbake/lib/toaster/tests/toaster-tests-requirements.txt b/poky/bitbake/lib/toaster/tests/toaster-tests-requirements.txt
new file mode 100644
index 000000000..4f9fcc46d
--- /dev/null
+++ b/poky/bitbake/lib/toaster/tests/toaster-tests-requirements.txt
@@ -0,0 +1 @@
+selenium==2.49.2
diff --git a/poky/bitbake/lib/toaster/tests/views/README b/poky/bitbake/lib/toaster/tests/views/README
new file mode 100644
index 000000000..950c7c989
--- /dev/null
+++ b/poky/bitbake/lib/toaster/tests/views/README
@@ -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
diff --git a/poky/bitbake/lib/toaster/tests/views/__init__.py b/poky/bitbake/lib/toaster/tests/views/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/poky/bitbake/lib/toaster/tests/views/__init__.py
diff --git a/poky/bitbake/lib/toaster/tests/views/test_views.py b/poky/bitbake/lib/toaster/tests/views/test_views.py
new file mode 100644
index 000000000..1463077e9
--- /dev/null
+++ b/poky/bitbake/lib/toaster/tests/views/test_views.py
@@ -0,0 +1,540 @@
+#! /usr/bin/env python
+# ex:ts=4:sw=4:sts=4:et
+# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
+#
+# BitBake Toaster Implementation
+#
+# Copyright (C) 2013-2015 Intel Corporation
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+"""Test cases for Toaster GUI and ReST."""
+
+from django.test import TestCase
+from django.test.client import RequestFactory
+from django.core.urlresolvers 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)
OpenPOWER on IntegriCloud