summaryrefslogtreecommitdiffstats
path: root/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser
diff options
context:
space:
mode:
Diffstat (limited to 'import-layers/yocto-poky/bitbake/lib/toaster/tests/browser')
-rw-r--r--import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/README28
-rw-r--r--import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/selenium_helpers.py176
-rw-r--r--import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/selenium_helpers_base.py218
-rw-r--r--import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_all_builds_page.py104
-rw-r--r--import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_all_projects_page.py13
-rw-r--r--import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_builddashboard_page.py178
-rw-r--r--import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_builddashboard_page_artifacts.py222
-rw-r--r--import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_builddashboard_page_recipes.py66
-rw-r--r--import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_builddashboard_page_tasks.py65
-rw-r--r--import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_js_unit_tests.py8
-rw-r--r--import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_layerdetails_page.py215
-rw-r--r--import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_most_recent_builds_states.py211
-rw-r--r--import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_new_custom_image_page.py3
-rw-r--r--import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_new_project_page.py113
-rw-r--r--import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_project_config_page.py231
-rw-r--r--import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_project_page.py2
-rw-r--r--import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_sample.py2
-rw-r--r--import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_task_page.py76
-rw-r--r--import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_toastertable_ui.py160
19 files changed, 1852 insertions, 239 deletions
diff --git a/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/README b/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/README
index 63e8169c1..6b09d20d8 100644
--- a/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/README
+++ b/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/README
@@ -4,15 +4,16 @@ These tests require Selenium to be installed in your Python environment.
The simplest way to install this is via pip:
- pip install selenium
+ pip install selenium==2.53.2
-Alternatively, if you used pip to install the libraries required by Toaster,
-selenium will already be installed.
+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://code.google.com/p/chromedriver/downloads/list
+ 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
@@ -23,15 +24,30 @@ To run tests against PhantomJS (headless):
* On *nix systems, put phantomjs on PATH
* Not tested on Windows
-Firefox should work without requiring additional software to be installed.
+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:
-The test case will instantiate a Selenium driver set by the
+ 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.
Available drivers:
* chrome (default)
* firefox
+* marionette (for newer Firefoxes)
* ie
* phantomjs
diff --git a/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/selenium_helpers.py b/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/selenium_helpers.py
index 56dbe2b34..08711e455 100644
--- a/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/selenium_helpers.py
+++ b/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/selenium_helpers.py
@@ -27,178 +27,8 @@
Helper methods for creating Toaster Selenium tests which run within
the context of Django unit tests.
"""
-
-import os
-import time
-
from django.contrib.staticfiles.testing import StaticLiveServerTestCase
-from selenium import webdriver
-from selenium.webdriver.support.ui import WebDriverWait
-from selenium.common.exceptions import NoSuchElementException, \
- StaleElementReferenceException, TimeoutException
-
-def create_selenium_driver(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 == 'ie':
- return webdriver.Ie()
- elif browser == 'phantomjs':
- return webdriver.PhantomJS()
- 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 SeleniumTestCase(StaticLiveServerTestCase):
- """
- 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(SeleniumTestCase, cls).setUpClass()
-
- # instantiate the Selenium webdriver once for all the test methods
- # in this test case
- cls.driver = create_selenium_driver()
-
- @classmethod
- def tearDownClass(cls):
- """ Clean up webdriver driver """
-
- cls.driver.quit()
- super(SeleniumTestCase, 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 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
+from tests.browser.selenium_helpers_base import SeleniumTestCaseBase
- def get_page_source(self):
- """ Get raw HTML for the current page """
- return self.driver.page_source
+class SeleniumTestCase(SeleniumTestCaseBase, StaticLiveServerTestCase):
+ pass
diff --git a/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/selenium_helpers_base.py b/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/selenium_helpers_base.py
new file mode 100644
index 000000000..14e9c1564
--- /dev/null
+++ b/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/selenium_helpers_base.py
@@ -0,0 +1,218 @@
+#! /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(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()
+ 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.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/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_all_builds_page.py b/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_all_builds_page.py
index e4223f482..b86f29bdd 100644
--- a/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_all_builds_page.py
+++ b/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_all_builds_page.py
@@ -27,6 +27,7 @@ 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/ """
@@ -57,6 +58,13 @@ class TestAllBuildsPage(SeleniumTestCase):
'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,
@@ -64,6 +72,46 @@ class TestAllBuildsPage(SeleniumTestCase):
'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)
@@ -95,17 +143,17 @@ class TestAllBuildsPage(SeleniumTestCase):
url = reverse('all-builds')
self.get(url)
- # shouldn't see a run again button for command-line builds
- selector = 'div[data-latest-build-result="%s"] button' % default_build.id
+ # 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 run again button for cli builds')
+ 'should not see a rebuild button for cli builds')
- # should see a run again button for non-command-line builds
- selector = 'div[data-latest-build-result="%s"] button' % build1.id
+ # 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 run again button for non-cli builds')
+ 'should see a rebuild button for non-cli builds')
def test_tooltips_on_project_name(self):
"""
@@ -124,7 +172,7 @@ class TestAllBuildsPage(SeleniumTestCase):
# get the project name cells from the table
cells = self.find_all('#allbuildstable td[class="project"]')
- selector = 'i.get-help'
+ selector = 'span.get-help'
for cell in cells:
content = cell.get_attribute('innerHTML')
@@ -141,3 +189,45 @@ class TestAllBuildsPage(SeleniumTestCase):
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/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_all_projects_page.py b/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_all_projects_page.py
index ed8e620db..44da64075 100644
--- a/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_all_projects_page.py
+++ b/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_all_projects_page.py
@@ -93,7 +93,7 @@ class TestAllProjectsPage(SeleniumTestCase):
"""
url = reverse('all-projects')
self.get(url)
- self.wait_until_visible('#no-results-projectstable')
+ self.wait_until_visible('#empty-state-projectstable')
rows = self.find_all('#projectstable tbody tr')
self.assertEqual(len(rows), 0, 'should be no projects displayed')
@@ -122,12 +122,13 @@ class TestAllProjectsPage(SeleniumTestCase):
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.muted'
+ 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',
@@ -137,7 +138,7 @@ class TestAllProjectsPage(SeleniumTestCase):
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"] a'
+ 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,
@@ -156,11 +157,13 @@ class TestAllProjectsPage(SeleniumTestCase):
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.muted'
+ 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',
@@ -170,7 +173,7 @@ class TestAllProjectsPage(SeleniumTestCase):
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"] a'
+ 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,
diff --git a/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_builddashboard_page.py b/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_builddashboard_page.py
index 5e0874947..f8ccb5452 100644
--- a/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_builddashboard_page.py
+++ b/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_builddashboard_page.py
@@ -22,10 +22,10 @@
from django.core.urlresolvers import reverse
from django.utils import timezone
-from selenium_helpers import SeleniumTestCase
+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
+from orm.models import Layer, Layer_Version, Recipe, CustomImageRecipe, Variable
class TestBuildDashboardPage(SeleniumTestCase):
""" Tests for the build dashboard /build/X """
@@ -42,11 +42,27 @@ class TestBuildDashboardPage(SeleniumTestCase):
self.build1 = Build.objects.create(project=project,
started_on=now,
- completed_on=now)
+ completed_on=now,
+ outcome=Build.SUCCEEDED)
self.build2 = Build.objects.create(project=project,
started_on=now,
- completed_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'
@@ -64,6 +80,22 @@ class TestBuildDashboardPage(SeleniumTestCase):
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')
@@ -71,6 +103,11 @@ class TestBuildDashboardPage(SeleniumTestCase):
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',
@@ -140,38 +177,47 @@ class TestBuildDashboardPage(SeleniumTestCase):
dashboard for the Build object build
"""
self._get_build_dashboard(build)
- return self.find_all('#errors div.alert-error')
+ return self.find_all('#errors div.alert-danger')
- def _check_for_log_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
+ def _check_for_log_message(self, message_elements, log_message):
"""
- errors = self._get_build_dashboard_errors(build)
- self.assertEqual(len(errors), 2)
+ 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_id = str(log_message.id)
+ expected_pk = str(log_message.pk)
found = False
- for error in errors:
- error_text = error.find_element_by_tag_name('pre').text
- text_matches = (error_text == expected_text)
+ for element in message_elements:
+ log_message_text = element.find_element_by_tag_name('pre').text.strip()
+ text_matches = (log_message_text == expected_text)
- error_id = error.get_attribute('data-error')
- id_matches = (error_id == expected_id)
+ 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, error_text,
- expected_id, error_id)
- assertion_error_msg = 'exception not found as error: ' \
- 'expected text "%s" and got "%s"; ' \
- 'expected ID %s and got %s' % template_vars
- self.assertTrue(found, assertion_error_msg)
+ 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):
"""
@@ -179,37 +225,29 @@ class TestBuildDashboardPage(SeleniumTestCase):
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_tag_name('label')
-
- # because the label content has the structure
- # label text
- # <input...>
- # we have to regex on its innerHTML, as we can't just retrieve the
- # "label text" on its own via the Selenium API
- labels_text = sorted(map(
- lambda label: label.get_attribute('innerHTML'), labels
- ))
-
- expected = sorted(expected)
+ labels = modal.find_elements_by_css_selector(".radio")
+ labels_text = [lab.text for lab in labels]
self.assertEqual(len(labels_text), len(expected))
- for idx, label_text in enumerate(labels_text):
- self.assertRegexpMatches(label_text, expected[idx])
+ 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_log_message(self.build1, self.exception_message)
+ 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_log_message(self.build1, self.critical_message)
+ self._check_for_error_message(self.build1, self.critical_message)
def test_edit_custom_image_button(self):
"""
@@ -217,7 +255,13 @@ class TestBuildDashboardPage(SeleniumTestCase):
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 = [
@@ -235,10 +279,11 @@ class TestBuildDashboardPage(SeleniumTestCase):
self._get_build_dashboard(self.build1)
# click the "new custom image" button, which populates the modal
- selector = '[data-role="new-custom-image-trigger"] button'
+ 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 = [
@@ -249,3 +294,54 @@ class TestBuildDashboardPage(SeleniumTestCase):
]
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/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_builddashboard_page_artifacts.py b/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_builddashboard_page_artifacts.py
new file mode 100644
index 000000000..1c627ad49
--- /dev/null
+++ b/import-layers/yocto-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/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_builddashboard_page_recipes.py b/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_builddashboard_page_recipes.py
new file mode 100644
index 000000000..ed18324e5
--- /dev/null
+++ b/import-layers/yocto-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/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_builddashboard_page_tasks.py b/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_builddashboard_page_tasks.py
new file mode 100644
index 000000000..da50f1601
--- /dev/null
+++ b/import-layers/yocto-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/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_js_unit_tests.py b/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_js_unit_tests.py
index e63da8e7a..3c0b96252 100644
--- a/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_js_unit_tests.py
+++ b/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_js_unit_tests.py
@@ -38,11 +38,11 @@ class TestJsUnitTests(SeleniumTestCase):
def test_that_js_unit_tests_pass(self):
url = reverse('js-unit-tests')
self.get(url)
- self.wait_until_present('#tests-failed')
+ self.wait_until_present('#qunit-testresult .failed')
- failed = self.find("#tests-failed").text
- passed = self.find("#tests-passed").text
- total = self.find("#tests-total").text
+ 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,
diff --git a/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_layerdetails_page.py b/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_layerdetails_page.py
new file mode 100644
index 000000000..6392d1efb
--- /dev/null
+++ b/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_layerdetails_page.py
@@ -0,0 +1,215 @@
+#! /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
+ self.wait_until_visible("#layer-git input[type=text]")
+ self.wait_until_visible("dd textarea")
+
+ # 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/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_most_recent_builds_states.py b/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_most_recent_builds_states.py
new file mode 100644
index 000000000..abc0b0bc8
--- /dev/null
+++ b/import-layers/yocto-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/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_new_custom_image_page.py b/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_new_custom_image_page.py
index 8906cb27d..ab5a8e66b 100644
--- a/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_new_custom_image_page.py
+++ b/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_new_custom_image_page.py
@@ -25,6 +25,7 @@ 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'
@@ -140,7 +141,7 @@ class TestNewCustomImagePage(SeleniumTestCase):
self._create_custom_image(self.recipe.name)
element = self.wait_until_visible('#invalid-name-help')
self.assertRegexpMatches(element.text.strip(),
- 'recipe with this name already exists')
+ 'image with this name already exists')
def test_new_duplicates_project_image(self):
"""
diff --git a/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_new_project_page.py b/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_new_project_page.py
new file mode 100644
index 000000000..77e5f1526
--- /dev/null
+++ b/import-layers/yocto-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/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_project_config_page.py b/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_project_config_page.py
new file mode 100644
index 000000000..071008499
--- /dev/null
+++ b/import-layers/yocto-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/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_project_page.py b/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_project_page.py
index 786bef1c6..018646332 100644
--- a/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_project_page.py
+++ b/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_project_page.py
@@ -55,5 +55,5 @@ class TestProjectPage(SeleniumTestCase):
self.get(url)
# check that we get a project page with the correct heading
- project_name = self.find('#project-name').text.strip()
+ project_name = self.find('.project-name').text.strip()
self.assertEqual(project_name, self.CLI_BUILDS_PROJECT_NAME)
diff --git a/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_sample.py b/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_sample.py
index 7bb8b97e8..20ec53c28 100644
--- a/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_sample.py
+++ b/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_sample.py
@@ -37,5 +37,5 @@ class TestSample(SeleniumTestCase):
def test_landing_page_has_brand(self):
url = reverse('landing')
self.get(url)
- brand_link = self.find('span.brand a')
+ brand_link = self.find('.toaster-navbar-brand a.brand')
self.assertEqual(brand_link.text.strip(), 'Toaster')
diff --git a/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_task_page.py b/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_task_page.py
new file mode 100644
index 000000000..690d116cb
--- /dev/null
+++ b/import-layers/yocto-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/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_toastertable_ui.py b/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_toastertable_ui.py
new file mode 100644
index 000000000..53ddf30c3
--- /dev/null
+++ b/import-layers/yocto-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')
OpenPOWER on IntegriCloud