diff options
Diffstat (limited to 'import-layers/yocto-poky/bitbake/lib/toaster/tests/browser')
12 files changed, 1446 insertions, 0 deletions
diff --git a/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/README b/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/README new file mode 100644 index 000000000..63e8169c1 --- /dev/null +++ b/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/README @@ -0,0 +1,41 @@ +# 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 pip: + + pip install selenium + +Alternatively, if you used pip to install the libraries required by Toaster, +selenium will already be installed. + +To run tests against Chrome: + +* Download chromedriver for your host OS from + https://code.google.com/p/chromedriver/downloads/list +* 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): + +* Download and install PhantomJS: + http://phantomjs.org/download.html +* On *nix systems, put phantomjs on PATH +* Not tested on Windows + +Firefox should work without requiring additional software to be installed. + +The test case will instantiate a Selenium driver set by the +TOASTER_TESTS_BROWSER environment variable, or Chrome if this is not specified. + +Available drivers: + +* chrome (default) +* firefox +* ie +* phantomjs + +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/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/__init__.py b/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/__init__.py diff --git a/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/selenium_helpers.py b/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/selenium_helpers.py new file mode 100644 index 000000000..56dbe2b34 --- /dev/null +++ b/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/selenium_helpers.py @@ -0,0 +1,204 @@ +#! /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 + +from django.contrib.staticfiles.testing import StaticLiveServerTestCase +from selenium import webdriver +from selenium.webdriver.support.ui import WebDriverWait +from selenium.common.exceptions import NoSuchElementException, \ + StaleElementReferenceException, TimeoutException + +def create_selenium_driver(browser='chrome'): + # set default browser string based on env (if available) + env_browser = os.environ.get('TOASTER_TESTS_BROWSER') + if env_browser: + browser = env_browser + + if browser == 'chrome': + return webdriver.Chrome( + service_args=["--verbose", "--log-path=selenium.log"] + ) + elif browser == 'firefox': + return webdriver.Firefox() + elif browser == 'ie': + return webdriver.Ie() + elif browser == 'phantomjs': + return webdriver.PhantomJS() + else: + msg = 'Selenium driver for browser %s is not available' % browser + raise RuntimeError(msg) + +class Wait(WebDriverWait): + """ + Subclass of WebDriverWait with predetermined timeout and poll + frequency. Also deals with a wider variety of exceptions. + """ + _TIMEOUT = 10 + _POLL_FREQUENCY = 0.5 + + def __init__(self, driver): + super(Wait, self).__init__(driver, self._TIMEOUT, self._POLL_FREQUENCY) + + def until(self, method, message=''): + """ + Calls the method provided with the driver as an argument until the + return value is not False. + """ + + end_time = time.time() + self._timeout + while True: + try: + value = method(self._driver) + if value: + return value + except NoSuchElementException: + pass + except StaleElementReferenceException: + pass + + time.sleep(self._poll) + if time.time() > end_time: + break + + raise TimeoutException(message) + + def until_not(self, method, message=''): + """ + Calls the method provided with the driver as an argument until the + return value is False. + """ + + end_time = time.time() + self._timeout + while True: + try: + value = method(self._driver) + if not value: + return value + except NoSuchElementException: + return True + except StaleElementReferenceException: + pass + + time.sleep(self._poll) + if time.time() > end_time: + break + + raise TimeoutException(message) + +class SeleniumTestCase(StaticLiveServerTestCase): + """ + NB StaticLiveServerTestCase is used as the base test case so that + static files are served correctly in a Selenium test run context; see + https://docs.djangoproject.com/en/1.9/ref/contrib/staticfiles/#specialized-test-case-to-support-live-testing + """ + + @classmethod + def setUpClass(cls): + """ Create a webdriver driver at the class level """ + + super(SeleniumTestCase, cls).setUpClass() + + # instantiate the Selenium webdriver once for all the test methods + # in this test case + cls.driver = create_selenium_driver() + + @classmethod + def tearDownClass(cls): + """ Clean up webdriver driver """ + + cls.driver.quit() + super(SeleniumTestCase, cls).tearDownClass() + + def get(self, url): + """ + Selenium requires absolute URLs, so convert Django URLs returned + by resolve() or similar to absolute ones and get using the + webdriver instance. + + url: a relative URL + """ + abs_url = '%s%s' % (self.live_server_url, url) + self.driver.get(abs_url) + + def find(self, selector): + """ Find single element by CSS selector """ + return self.driver.find_element_by_css_selector(selector) + + def find_all(self, selector): + """ Find all elements matching CSS selector """ + return self.driver.find_elements_by_css_selector(selector) + + def focused_element(self): + """ Return the element which currently has focus on the page """ + return self.driver.switch_to.active_element + + def wait_until_present(self, selector): + """ Wait until element matching CSS selector is on the page """ + is_present = lambda driver: self.find(selector) + msg = 'An element matching "%s" should be on the page' % selector + element = Wait(self.driver).until(is_present, msg) + return element + + def wait_until_visible(self, selector): + """ Wait until element matching CSS selector is visible on the page """ + is_visible = lambda driver: self.find(selector).is_displayed() + msg = 'An element matching "%s" should be visible' % selector + Wait(self.driver).until(is_visible, msg) + return self.find(selector) + + def wait_until_focused(self, selector): + """ Wait until element matching CSS selector has focus """ + is_focused = \ + lambda driver: self.find(selector) == self.focused_element() + msg = 'An element matching "%s" should be focused' % selector + Wait(self.driver).until(is_focused, msg) + return self.find(selector) + + def enter_text(self, selector, value): + """ Insert text into element matching selector """ + # note that keyup events don't occur until the element is clicked + # (in the case of <input type="text"...>, for example), so simulate + # user clicking the element before inserting text into it + field = self.click(selector) + + field.send_keys(value) + return field + + def click(self, selector): + """ Click on element which matches CSS selector """ + element = self.wait_until_visible(selector) + element.click() + return element + + def get_page_source(self): + """ Get raw HTML for the current page """ + return self.driver.page_source diff --git a/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_all_builds_page.py b/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_all_builds_page.py new file mode 100644 index 000000000..e4223f482 --- /dev/null +++ b/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_all_builds_page.py @@ -0,0 +1,143 @@ +#! /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.default_project_build_success = { + 'project': self.default_project, + 'started_on': now, + 'completed_on': now, + 'outcome': Build.SUCCEEDED + } + + 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 run again button for command-line builds + selector = 'div[data-latest-build-result="%s"] button' % default_build.id + run_again_button = self.find_all(selector) + self.assertEqual(len(run_again_button), 0, + 'should not see a run again button for cli builds') + + # should see a run again button for non-command-line builds + selector = 'div[data-latest-build-result="%s"] button' % build1.id + run_again_button = self.find_all(selector) + self.assertEqual(len(run_again_button), 1, + 'should see a run again button for non-cli builds') + + 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 = 'i.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) diff --git a/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_all_projects_page.py b/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_all_projects_page.py new file mode 100644 index 000000000..ed8e620db --- /dev/null +++ b/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_all_projects_page.py @@ -0,0 +1,214 @@ +#! /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('#no-results-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')) + + # find the row for the default project + default_project_row = self._get_row_for_project(self.default_project.name) + + # check the release text for the default project + selector = 'span[data-project-field="release"] span.muted' + 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"] a' + 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')) + + # find the row for the default project + default_project_row = self._get_row_for_project(self.default_project.name) + + # check the machine cell for the default project + selector = 'span[data-project-field="machine"] span.muted' + 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"] a' + 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/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_builddashboard_page.py b/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_builddashboard_page.py new file mode 100644 index 000000000..5e0874947 --- /dev/null +++ b/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_builddashboard_page.py @@ -0,0 +1,251 @@ +#! /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 selenium_helpers import SeleniumTestCase + +from orm.models import Project, Release, BitbakeVersion, Build, LogMessage +from orm.models import Layer, Layer_Version, Recipe, CustomImageRecipe + +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) + + self.build2 = Build.objects.create(project=project, + started_on=now, + completed_on=now) + + # 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 + ) + + # 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 + ) + + # 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-error') + + def _check_for_log_message(self, build, log_message): + """ + Check whether the LogMessage instance <log_message> is + represented as an HTML error in the dashboard page for the Build object + build + """ + errors = self._get_build_dashboard_errors(build) + self.assertEqual(len(errors), 2) + + expected_text = log_message.message + expected_id = str(log_message.id) + + found = False + for error in errors: + error_text = error.find_element_by_tag_name('pre').text + text_matches = (error_text == expected_text) + + error_id = error.get_attribute('data-error') + id_matches = (error_id == expected_id) + + if text_matches and id_matches: + found = True + break + + template_vars = (expected_text, error_text, + expected_id, error_id) + assertion_error_msg = 'exception not found as error: ' \ + 'expected text "%s" and got "%s"; ' \ + 'expected ID %s and got %s' % template_vars + self.assertTrue(found, assertion_error_msg) + + 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_tag_name('label') + + # because the label content has the structure + # label text + # <input...> + # we have to regex on its innerHTML, as we can't just retrieve the + # "label text" on its own via the Selenium API + labels_text = sorted(map( + lambda label: label.get_attribute('innerHTML'), labels + )) + + expected = sorted(expected) + + self.assertEqual(len(labels_text), len(expected)) + + for idx, label_text in enumerate(labels_text): + self.assertRegexpMatches(label_text, expected[idx]) + + def test_exceptions_show_as_errors(self): + """ + LogMessages with level EXCEPTION should display in the errors + section of the page + """ + self._check_for_log_message(self.build1, self.exception_message) + + def test_criticals_show_as_errors(self): + """ + LogMessages with level CRITICAL should display in the errors + section of the page + """ + self._check_for_log_message(self.build1, self.critical_message) + + 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) + modal = self.driver.find_element_by_id('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"] button' + self.click(selector) + + modal = self.driver.find_element_by_id('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) diff --git a/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_js_unit_tests.py b/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_js_unit_tests.py new file mode 100644 index 000000000..e63da8e7a --- /dev/null +++ b/import-layers/yocto-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('#tests-failed') + + failed = self.find("#tests-failed").text + passed = self.find("#tests-passed").text + total = self.find("#tests-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/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_landing_page.py b/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_landing_page.py new file mode 100644 index 000000000..4d4cd660f --- /dev/null +++ b/import-layers/yocto-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/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_new_custom_image_page.py b/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_new_custom_image_page.py new file mode 100644 index 000000000..8906cb27d --- /dev/null +++ b/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_new_custom_image_page.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 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(), + 'recipe 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/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_project_builds_page.py b/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_project_builds_page.py new file mode 100644 index 000000000..9fe91ab06 --- /dev/null +++ b/import-layers/yocto-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/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_project_page.py b/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_project_page.py new file mode 100644 index 000000000..786bef1c6 --- /dev/null +++ b/import-layers/yocto-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/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_sample.py b/import-layers/yocto-poky/bitbake/lib/toaster/tests/browser/test_sample.py new file mode 100644 index 000000000..7bb8b97e8 --- /dev/null +++ b/import-layers/yocto-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('span.brand a') + self.assertEqual(brand_link.text.strip(), 'Toaster') |