From eb8dc40360f0cfef56fb6947cc817a547d6d9bc6 Mon Sep 17 00:00:00 2001 From: Dave Cobbley Date: Tue, 14 Aug 2018 10:05:37 -0700 Subject: [Subtree] Removing import-layers directory As part of the move to subtrees, need to bring all the import layers content to the top level. Change-Id: I4a163d10898cbc6e11c27f776f60e1a470049d8f Signed-off-by: Dave Cobbley Signed-off-by: Brad Bishop --- poky/bitbake/lib/toaster/tests/__init__.py | 0 poky/bitbake/lib/toaster/tests/browser/README | 74 +++ poky/bitbake/lib/toaster/tests/browser/__init__.py | 0 .../lib/toaster/tests/browser/selenium_helpers.py | 34 ++ .../toaster/tests/browser/selenium_helpers_base.py | 227 +++++++++ .../toaster/tests/browser/test_all_builds_page.py | 233 +++++++++ .../tests/browser/test_all_projects_page.py | 217 +++++++++ .../tests/browser/test_builddashboard_page.py | 347 +++++++++++++ .../browser/test_builddashboard_page_artifacts.py | 222 +++++++++ .../browser/test_builddashboard_page_recipes.py | 66 +++ .../browser/test_builddashboard_page_tasks.py | 65 +++ .../toaster/tests/browser/test_js_unit_tests.py | 57 +++ .../lib/toaster/tests/browser/test_landing_page.py | 108 +++++ .../tests/browser/test_layerdetails_page.py | 216 +++++++++ .../browser/test_most_recent_builds_states.py | 211 ++++++++ .../tests/browser/test_new_custom_image_page.py | 161 ++++++ .../toaster/tests/browser/test_new_project_page.py | 113 +++++ .../tests/browser/test_project_builds_page.py | 168 +++++++ .../tests/browser/test_project_config_page.py | 231 +++++++++ .../lib/toaster/tests/browser/test_project_page.py | 59 +++ .../lib/toaster/tests/browser/test_sample.py | 41 ++ .../lib/toaster/tests/browser/test_task_page.py | 76 +++ .../toaster/tests/browser/test_toastertable_ui.py | 160 ++++++ poky/bitbake/lib/toaster/tests/builds/README | 14 + poky/bitbake/lib/toaster/tests/builds/__init__.py | 0 poky/bitbake/lib/toaster/tests/builds/buildtest.py | 169 +++++++ .../toaster/tests/builds/test_core_image_min.py | 386 +++++++++++++++ .../bitbake/lib/toaster/tests/commands/__init__.py | 0 .../lib/toaster/tests/commands/test_loaddata.py | 61 +++ .../lib/toaster/tests/commands/test_lsupdates.py | 45 ++ .../lib/toaster/tests/commands/test_runbuilds.py | 88 ++++ poky/bitbake/lib/toaster/tests/db/__init__.py | 0 poky/bitbake/lib/toaster/tests/db/test_db.py | 55 +++ poky/bitbake/lib/toaster/tests/eventreplay/README | 22 + .../lib/toaster/tests/eventreplay/__init__.py | 97 ++++ poky/bitbake/lib/toaster/tests/functional/README | 0 .../lib/toaster/tests/functional/__init__.py | 0 .../toaster/tests/functional/functional_helpers.py | 122 +++++ .../tests/functional/test_functional_basic.py | 243 ++++++++++ .../toaster/tests/toaster-tests-requirements.txt | 1 + poky/bitbake/lib/toaster/tests/views/README | 4 + poky/bitbake/lib/toaster/tests/views/__init__.py | 0 poky/bitbake/lib/toaster/tests/views/test_views.py | 540 +++++++++++++++++++++ 43 files changed, 4933 insertions(+) create mode 100644 poky/bitbake/lib/toaster/tests/__init__.py create mode 100644 poky/bitbake/lib/toaster/tests/browser/README create mode 100644 poky/bitbake/lib/toaster/tests/browser/__init__.py create mode 100644 poky/bitbake/lib/toaster/tests/browser/selenium_helpers.py create mode 100644 poky/bitbake/lib/toaster/tests/browser/selenium_helpers_base.py create mode 100644 poky/bitbake/lib/toaster/tests/browser/test_all_builds_page.py create mode 100644 poky/bitbake/lib/toaster/tests/browser/test_all_projects_page.py create mode 100644 poky/bitbake/lib/toaster/tests/browser/test_builddashboard_page.py create mode 100644 poky/bitbake/lib/toaster/tests/browser/test_builddashboard_page_artifacts.py create mode 100644 poky/bitbake/lib/toaster/tests/browser/test_builddashboard_page_recipes.py create mode 100644 poky/bitbake/lib/toaster/tests/browser/test_builddashboard_page_tasks.py create mode 100644 poky/bitbake/lib/toaster/tests/browser/test_js_unit_tests.py create mode 100644 poky/bitbake/lib/toaster/tests/browser/test_landing_page.py create mode 100644 poky/bitbake/lib/toaster/tests/browser/test_layerdetails_page.py create mode 100644 poky/bitbake/lib/toaster/tests/browser/test_most_recent_builds_states.py create mode 100644 poky/bitbake/lib/toaster/tests/browser/test_new_custom_image_page.py create mode 100644 poky/bitbake/lib/toaster/tests/browser/test_new_project_page.py create mode 100644 poky/bitbake/lib/toaster/tests/browser/test_project_builds_page.py create mode 100644 poky/bitbake/lib/toaster/tests/browser/test_project_config_page.py create mode 100644 poky/bitbake/lib/toaster/tests/browser/test_project_page.py create mode 100644 poky/bitbake/lib/toaster/tests/browser/test_sample.py create mode 100644 poky/bitbake/lib/toaster/tests/browser/test_task_page.py create mode 100644 poky/bitbake/lib/toaster/tests/browser/test_toastertable_ui.py create mode 100644 poky/bitbake/lib/toaster/tests/builds/README create mode 100644 poky/bitbake/lib/toaster/tests/builds/__init__.py create mode 100644 poky/bitbake/lib/toaster/tests/builds/buildtest.py create mode 100644 poky/bitbake/lib/toaster/tests/builds/test_core_image_min.py create mode 100644 poky/bitbake/lib/toaster/tests/commands/__init__.py create mode 100644 poky/bitbake/lib/toaster/tests/commands/test_loaddata.py create mode 100644 poky/bitbake/lib/toaster/tests/commands/test_lsupdates.py create mode 100644 poky/bitbake/lib/toaster/tests/commands/test_runbuilds.py create mode 100644 poky/bitbake/lib/toaster/tests/db/__init__.py create mode 100644 poky/bitbake/lib/toaster/tests/db/test_db.py create mode 100644 poky/bitbake/lib/toaster/tests/eventreplay/README create mode 100644 poky/bitbake/lib/toaster/tests/eventreplay/__init__.py create mode 100644 poky/bitbake/lib/toaster/tests/functional/README create mode 100644 poky/bitbake/lib/toaster/tests/functional/__init__.py create mode 100644 poky/bitbake/lib/toaster/tests/functional/functional_helpers.py create mode 100644 poky/bitbake/lib/toaster/tests/functional/test_functional_basic.py create mode 100644 poky/bitbake/lib/toaster/tests/toaster-tests-requirements.txt create mode 100644 poky/bitbake/lib/toaster/tests/views/README create mode 100644 poky/bitbake/lib/toaster/tests/views/__init__.py create mode 100644 poky/bitbake/lib/toaster/tests/views/test_views.py (limited to 'poky/bitbake/lib/toaster/tests') diff --git a/poky/bitbake/lib/toaster/tests/__init__.py b/poky/bitbake/lib/toaster/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/poky/bitbake/lib/toaster/tests/browser/README b/poky/bitbake/lib/toaster/tests/browser/README new file mode 100644 index 0000000000..352c4fe3e9 --- /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 0000000000..e69de29bb2 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 0000000000..08711e4558 --- /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 0000000000..156d639b1e --- /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 , 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 0000000000..b86f29bdd4 --- /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 0000000000..44da640751 --- /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 0000000000..f8ccb54528 --- /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 has a representation in + the HTML elements . + + message_elements: WebElements representing the log messages shown + in the build dashboard; each should have a
 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  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