diff options
Diffstat (limited to 'poky/bitbake/lib/toaster/tests/browser')
22 files changed, 3086 insertions, 0 deletions
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') |