diff options
author | Dave Cobbley <david.j.cobbley@linux.intel.com> | 2018-08-14 20:05:37 +0300 |
---|---|---|
committer | Brad Bishop <bradleyb@fuzziesquirrel.com> | 2018-08-23 04:26:31 +0300 |
commit | eb8dc40360f0cfef56fb6947cc817a547d6d9bc6 (patch) | |
tree | de291a73dc37168da6370e2cf16c347d1eba9df8 /poky/bitbake/lib/toaster/tests | |
parent | 9c3cf826d853102535ead04cebc2d6023eff3032 (diff) | |
download | openbmc-eb8dc40360f0cfef56fb6947cc817a547d6d9bc6.tar.xz |
[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 <david.j.cobbley@linux.intel.com>
Signed-off-by: Brad Bishop <bradleyb@fuzziesquirrel.com>
Diffstat (limited to 'poky/bitbake/lib/toaster/tests')
43 files changed, 4933 insertions, 0 deletions
diff --git a/poky/bitbake/lib/toaster/tests/__init__.py b/poky/bitbake/lib/toaster/tests/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/poky/bitbake/lib/toaster/tests/__init__.py diff --git a/poky/bitbake/lib/toaster/tests/browser/README b/poky/bitbake/lib/toaster/tests/browser/README new file mode 100644 index 000000000..352c4fe3e --- /dev/null +++ b/poky/bitbake/lib/toaster/tests/browser/README @@ -0,0 +1,74 @@ +# Running Toaster's browser-based test suite + +These tests require Selenium to be installed in your Python environment. + +The simplest way to install this is via pip3: + + pip3 install selenium==2.53.2 + +Note that if you use other versions of Selenium, some of the tests (such as +tests.browser.test_js_unit_tests.TestJsUnitTests) may fail, as these rely on +a Selenium test report with a version-specific format. + +To run tests against Chrome: + +* Download chromedriver for your host OS from + https://sites.google.com/a/chromium.org/chromedriver/downloads +* On *nix systems, put chromedriver on PATH +* On Windows, put chromedriver.exe in the same directory as chrome.exe + +To run tests against PhantomJS (headless): +--NOTE - Selenium seems to be deprecating support for this mode --- +* Download and install PhantomJS: + http://phantomjs.org/download.html +* On *nix systems, put phantomjs on PATH +* Not tested on Windows + +To run tests against Firefox, you may need to install the Marionette driver, +depending on how new your version of Firefox is. One clue that you need to do +this is if you see an exception like: + + selenium.common.exceptions.WebDriverException: Message: The browser + appears to have exited before we could connect. If you specified + a log_file in the FirefoxBinary constructor, check it for details. + +See https://developer.mozilla.org/en-US/docs/Mozilla/QA/Marionette/WebDriver +for installation instructions. Ensure that the Marionette executable (renamed +as wires on Linux or wires.exe on Windows) is on your PATH; and use "marionette" +as the browser string passed via TOASTER_TESTS_BROWSER (see below). + +(Note: The Toaster tests have been checked against Firefox 47 with the +Marionette driver.) + +The test cases will instantiate a Selenium driver set by the +TOASTER_TESTS_BROWSER environment variable, or Chrome if this is not specified. + +To run tests against the Selenium Firefox Docker container: +More explanation is located at https://wiki.yoctoproject.org/wiki/TipsAndTricks/TestingToasterWithContainers +* Run the Selenium container: + ** docker run -it --rm=true -p 5900:5900 -p 4444:4444 --name=selenium selenium/standalone-firefox-debug:2.53.0 + *** 5900 is the default vnc port. If you are runing a vnc server on your machine map a different port e.g. -p 6900:5900 and connect vnc client to 127.0.0.1:6900 + *** 4444 is the default selenium sever port. +* Run the tests + ** TOASTER_TESTS_BROWSER=http://127.0.0.1:4444/wd/hub TOASTER_TESTS_URL=http://172.17.0.1:8000 ./bitbake/lib/toaster/manage.py test --liveserver=172.17.0.1:8000 tests.browser + ** TOASTER_TESTS_BROWSER=remote TOASTER_REMOTE_HUB=http://127.0.0.1:4444/wd/hub ./bitbake/lib/toaster/manage.py test --liveserver=172.17.0.1:8000 tests.browser + *** TOASTER_REMOTE_HUB - This is the address for the Selenium Remote Web Driver hub. Assuming you ran the contianer with -p 4444:4444 it will be http://127.0.0.1:4444/wd/hub. + *** --liveserver=xxx tells Django to run the test server on an interface and port reachable by both host and container. + **** 172.17.0.1 is the default docker bridge on linux, viewable from inside and outside the contianers. Find it with "ip -4 addr show dev docker0" +* connect to the vnc server to see the tests if you would like + ** xtightvncviewer 127.0.0.1:5900 + ** note, you need to wait for the test container to come up before this can connect. + +Available drivers: + +* chrome (default) +* firefox +* marionette (for newer Firefoxes) +* ie +* phantomjs (deprecated) +* remote + +e.g. to run the test suite with phantomjs where you have phantomjs installed +in /home/me/apps/phantomjs: + +PATH=/home/me/apps/phantomjs/bin:$PATH TOASTER_TESTS_BROWSER=phantomjs manage.py test tests.browser diff --git a/poky/bitbake/lib/toaster/tests/browser/__init__.py b/poky/bitbake/lib/toaster/tests/browser/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/poky/bitbake/lib/toaster/tests/browser/__init__.py diff --git a/poky/bitbake/lib/toaster/tests/browser/selenium_helpers.py b/poky/bitbake/lib/toaster/tests/browser/selenium_helpers.py new file mode 100644 index 000000000..08711e455 --- /dev/null +++ b/poky/bitbake/lib/toaster/tests/browser/selenium_helpers.py @@ -0,0 +1,34 @@ +#! /usr/bin/env python +# ex:ts=4:sw=4:sts=4:et +# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- +# +# BitBake Toaster Implementation +# +# Copyright (C) 2013-2016 Intel Corporation +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +# +# The Wait class and some of SeleniumDriverHelper and SeleniumTestCase are +# modified from Patchwork, released under the same licence terms as Toaster: +# https://github.com/dlespiau/patchwork/blob/master/patchwork/tests.browser.py + +""" +Helper methods for creating Toaster Selenium tests which run within +the context of Django unit tests. +""" +from django.contrib.staticfiles.testing import StaticLiveServerTestCase +from tests.browser.selenium_helpers_base import SeleniumTestCaseBase + +class SeleniumTestCase(SeleniumTestCaseBase, StaticLiveServerTestCase): + pass diff --git a/poky/bitbake/lib/toaster/tests/browser/selenium_helpers_base.py b/poky/bitbake/lib/toaster/tests/browser/selenium_helpers_base.py new file mode 100644 index 000000000..156d639b1 --- /dev/null +++ b/poky/bitbake/lib/toaster/tests/browser/selenium_helpers_base.py @@ -0,0 +1,227 @@ +#! /usr/bin/env python +# ex:ts=4:sw=4:sts=4:et +# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- +# +# BitBake Toaster Implementation +# +# Copyright (C) 2013-2016 Intel Corporation +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +# +# The Wait class and some of SeleniumDriverHelper and SeleniumTestCase are +# modified from Patchwork, released under the same licence terms as Toaster: +# https://github.com/dlespiau/patchwork/blob/master/patchwork/tests.browser.py + +""" +Helper methods for creating Toaster Selenium tests which run within +the context of Django unit tests. +""" + +import os +import time +import unittest + +from django.contrib.staticfiles.testing import StaticLiveServerTestCase +from selenium import webdriver +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.common.desired_capabilities import DesiredCapabilities +from selenium.common.exceptions import NoSuchElementException, \ + StaleElementReferenceException, TimeoutException + +def create_selenium_driver(cls,browser='chrome'): + # set default browser string based on env (if available) + env_browser = os.environ.get('TOASTER_TESTS_BROWSER') + if env_browser: + browser = env_browser + + if browser == 'chrome': + return webdriver.Chrome( + service_args=["--verbose", "--log-path=selenium.log"] + ) + elif browser == 'firefox': + return webdriver.Firefox() + elif browser == 'marionette': + capabilities = DesiredCapabilities.FIREFOX + capabilities['marionette'] = True + return webdriver.Firefox(capabilities=capabilities) + elif browser == 'ie': + return webdriver.Ie() + elif browser == 'phantomjs': + return webdriver.PhantomJS() + elif browser == 'remote': + # if we were to add yet another env variable like TOASTER_REMOTE_BROWSER + # we could let people pick firefox or chrome, left for later + remote_hub= os.environ.get('TOASTER_REMOTE_HUB') + driver = webdriver.Remote(remote_hub, + webdriver.DesiredCapabilities.FIREFOX.copy()) + + driver.get("http://%s:%s"%(cls.server_thread.host,cls.server_thread.port)) + return driver + else: + msg = 'Selenium driver for browser %s is not available' % browser + raise RuntimeError(msg) + +class Wait(WebDriverWait): + """ + Subclass of WebDriverWait with predetermined timeout and poll + frequency. Also deals with a wider variety of exceptions. + """ + _TIMEOUT = 10 + _POLL_FREQUENCY = 0.5 + + def __init__(self, driver): + super(Wait, self).__init__(driver, self._TIMEOUT, self._POLL_FREQUENCY) + + def until(self, method, message=''): + """ + Calls the method provided with the driver as an argument until the + return value is not False. + """ + + end_time = time.time() + self._timeout + while True: + try: + value = method(self._driver) + if value: + return value + except NoSuchElementException: + pass + except StaleElementReferenceException: + pass + + time.sleep(self._poll) + if time.time() > end_time: + break + + raise TimeoutException(message) + + def until_not(self, method, message=''): + """ + Calls the method provided with the driver as an argument until the + return value is False. + """ + + end_time = time.time() + self._timeout + while True: + try: + value = method(self._driver) + if not value: + return value + except NoSuchElementException: + return True + except StaleElementReferenceException: + pass + + time.sleep(self._poll) + if time.time() > end_time: + break + + raise TimeoutException(message) + +class SeleniumTestCaseBase(unittest.TestCase): + """ + NB StaticLiveServerTestCase is used as the base test case so that + static files are served correctly in a Selenium test run context; see + https://docs.djangoproject.com/en/1.9/ref/contrib/staticfiles/#specialized-test-case-to-support-live-testing + """ + + @classmethod + def setUpClass(cls): + """ Create a webdriver driver at the class level """ + + super(SeleniumTestCaseBase, cls).setUpClass() + + # instantiate the Selenium webdriver once for all the test methods + # in this test case + cls.driver = create_selenium_driver(cls) + cls.driver.maximize_window() + + @classmethod + def tearDownClass(cls): + """ Clean up webdriver driver """ + + cls.driver.quit() + super(SeleniumTestCaseBase, cls).tearDownClass() + + def get(self, url): + """ + Selenium requires absolute URLs, so convert Django URLs returned + by resolve() or similar to absolute ones and get using the + webdriver instance. + + url: a relative URL + """ + abs_url = '%s%s' % (self.live_server_url, url) + self.driver.get(abs_url) + + def find(self, selector): + """ Find single element by CSS selector """ + return self.driver.find_element_by_css_selector(selector) + + def find_all(self, selector): + """ Find all elements matching CSS selector """ + return self.driver.find_elements_by_css_selector(selector) + + def element_exists(self, selector): + """ + Return True if one element matching selector exists, + False otherwise + """ + return len(self.find_all(selector)) == 1 + + def focused_element(self): + """ Return the element which currently has focus on the page """ + return self.driver.switch_to.active_element + + def wait_until_present(self, selector): + """ Wait until element matching CSS selector is on the page """ + is_present = lambda driver: self.find(selector) + msg = 'An element matching "%s" should be on the page' % selector + element = Wait(self.driver).until(is_present, msg) + return element + + def wait_until_visible(self, selector): + """ Wait until element matching CSS selector is visible on the page """ + is_visible = lambda driver: self.find(selector).is_displayed() + msg = 'An element matching "%s" should be visible' % selector + Wait(self.driver).until(is_visible, msg) + return self.find(selector) + + def wait_until_focused(self, selector): + """ Wait until element matching CSS selector has focus """ + is_focused = \ + lambda driver: self.find(selector) == self.focused_element() + msg = 'An element matching "%s" should be focused' % selector + Wait(self.driver).until(is_focused, msg) + return self.find(selector) + + def enter_text(self, selector, value): + """ Insert text into element matching selector """ + # note that keyup events don't occur until the element is clicked + # (in the case of <input type="text"...>, for example), so simulate + # user clicking the element before inserting text into it + field = self.click(selector) + + field.send_keys(value) + return field + + def click(self, selector): + """ Click on element which matches CSS selector """ + element = self.wait_until_visible(selector) + element.click() + return element + + def get_page_source(self): + """ Get raw HTML for the current page """ + return self.driver.page_source diff --git a/poky/bitbake/lib/toaster/tests/browser/test_all_builds_page.py b/poky/bitbake/lib/toaster/tests/browser/test_all_builds_page.py new file mode 100644 index 000000000..b86f29bdd --- /dev/null +++ b/poky/bitbake/lib/toaster/tests/browser/test_all_builds_page.py @@ -0,0 +1,233 @@ +#! /usr/bin/env python +# ex:ts=4:sw=4:sts=4:et +# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- +# +# BitBake Toaster Implementation +# +# Copyright (C) 2013-2016 Intel Corporation +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import re + +from django.core.urlresolvers import reverse +from django.utils import timezone +from tests.browser.selenium_helpers import SeleniumTestCase + +from orm.models import BitbakeVersion, Release, Project, Build, Target + + +class TestAllBuildsPage(SeleniumTestCase): + """ Tests for all builds page /builds/ """ + + PROJECT_NAME = 'test project' + CLI_BUILDS_PROJECT_NAME = 'command line builds' + + def setUp(self): + bbv = BitbakeVersion.objects.create(name='bbv1', giturl='/tmp/', + branch='master', dirpath='') + release = Release.objects.create(name='release1', + bitbake_version=bbv) + self.project1 = Project.objects.create_project(name=self.PROJECT_NAME, + release=release) + self.default_project = Project.objects.create_project( + name=self.CLI_BUILDS_PROJECT_NAME, + release=release + ) + self.default_project.is_default = True + self.default_project.save() + + # parameters for builds to associate with the projects + now = timezone.now() + + self.project1_build_success = { + 'project': self.project1, + 'started_on': now, + 'completed_on': now, + 'outcome': Build.SUCCEEDED + } + + self.project1_build_failure = { + 'project': self.project1, + 'started_on': now, + 'completed_on': now, + 'outcome': Build.FAILED + } + + self.default_project_build_success = { + 'project': self.default_project, + 'started_on': now, + 'completed_on': now, + 'outcome': Build.SUCCEEDED + } + + def _get_build_time_element(self, build): + """ + Return the HTML element containing the build time for a build + in the recent builds area + """ + selector = 'div[data-latest-build-result="%s"] ' \ + '[data-role="data-recent-build-buildtime-field"]' % build.id + + # because this loads via Ajax, wait for it to be visible + self.wait_until_present(selector) + + build_time_spans = self.find_all(selector) + + self.assertEqual(len(build_time_spans), 1) + + return build_time_spans[0] + + def _get_row_for_build(self, build): + """ Get the table row for the build from the all builds table """ + self.wait_until_present('#allbuildstable') + + rows = self.find_all('#allbuildstable tr') + + # look for the row with a download link on the recipe which matches the + # build ID + url = reverse('builddashboard', args=(build.id,)) + selector = 'td.target a[href="%s"]' % url + + found_row = None + for row in rows: + + outcome_links = row.find_elements_by_css_selector(selector) + if len(outcome_links) == 1: + found_row = row + break + + self.assertNotEqual(found_row, None) + + return found_row + + def test_show_tasks_with_suffix(self): + """ Task should be shown as suffix on build name """ + build = Build.objects.create(**self.project1_build_success) + target = 'bash' + task = 'clean' + Target.objects.create(build=build, target=target, task=task) + + url = reverse('all-builds') + self.get(url) + self.wait_until_present('td[class="target"]') + + cell = self.find('td[class="target"]') + content = cell.get_attribute('innerHTML') + expected_text = '%s:%s' % (target, task) + + self.assertTrue(re.search(expected_text, content), + '"target" cell should contain text %s' % expected_text) + + def test_rebuild_buttons(self): + """ + Test 'Rebuild' buttons in recent builds section + + 'Rebuild' button should not be shown for command-line builds, + but should be shown for other builds + """ + build1 = Build.objects.create(**self.project1_build_success) + default_build = Build.objects.create(**self.default_project_build_success) + + url = reverse('all-builds') + self.get(url) + + # shouldn't see a rebuild button for command-line builds + selector = 'div[data-latest-build-result="%s"] .rebuild-btn' % default_build.id + run_again_button = self.find_all(selector) + self.assertEqual(len(run_again_button), 0, + 'should not see a rebuild button for cli builds') + + # should see a rebuild button for non-command-line builds + selector = 'div[data-latest-build-result="%s"] .rebuild-btn' % build1.id + run_again_button = self.find_all(selector) + self.assertEqual(len(run_again_button), 1, + 'should see a rebuild button for non-cli builds') + + def test_tooltips_on_project_name(self): + """ + Test tooltips shown next to project name in the main table + + A tooltip should be present next to the command line + builds project name in the all builds page, but not for + other projects + """ + Build.objects.create(**self.project1_build_success) + Build.objects.create(**self.default_project_build_success) + + url = reverse('all-builds') + self.get(url) + + # get the project name cells from the table + cells = self.find_all('#allbuildstable td[class="project"]') + + selector = 'span.get-help' + + for cell in cells: + content = cell.get_attribute('innerHTML') + help_icons = cell.find_elements_by_css_selector(selector) + + if re.search(self.PROJECT_NAME, content): + # no help icon next to non-cli project name + msg = 'should not be a help icon for non-cli builds name' + self.assertEqual(len(help_icons), 0, msg) + elif re.search(self.CLI_BUILDS_PROJECT_NAME, content): + # help icon next to cli project name + msg = 'should be a help icon for cli builds name' + self.assertEqual(len(help_icons), 1, msg) + else: + msg = 'found unexpected project name cell in all builds table' + self.fail(msg) + + def test_builds_time_links(self): + """ + Successful builds should have links on the time column and in the + recent builds area; failed builds should not have links on the time column, + or in the recent builds area + """ + build1 = Build.objects.create(**self.project1_build_success) + build2 = Build.objects.create(**self.project1_build_failure) + + # add some targets to these builds so they have recipe links + # (and so we can find the row in the ToasterTable corresponding to + # a particular build) + Target.objects.create(build=build1, target='foo') + Target.objects.create(build=build2, target='bar') + + url = reverse('all-builds') + self.get(url) + + # test recent builds area for successful build + element = self._get_build_time_element(build1) + links = element.find_elements_by_css_selector('a') + msg = 'should be a link on the build time for a successful recent build' + self.assertEquals(len(links), 1, msg) + + # test recent builds area for failed build + element = self._get_build_time_element(build2) + links = element.find_elements_by_css_selector('a') + msg = 'should not be a link on the build time for a failed recent build' + self.assertEquals(len(links), 0, msg) + + # test the time column for successful build + build1_row = self._get_row_for_build(build1) + links = build1_row.find_elements_by_css_selector('td.time a') + msg = 'should be a link on the build time for a successful build' + self.assertEquals(len(links), 1, msg) + + # test the time column for failed build + build2_row = self._get_row_for_build(build2) + links = build2_row.find_elements_by_css_selector('td.time a') + msg = 'should not be a link on the build time for a failed build' + self.assertEquals(len(links), 0, msg) diff --git a/poky/bitbake/lib/toaster/tests/browser/test_all_projects_page.py b/poky/bitbake/lib/toaster/tests/browser/test_all_projects_page.py new file mode 100644 index 000000000..44da64075 --- /dev/null +++ b/poky/bitbake/lib/toaster/tests/browser/test_all_projects_page.py @@ -0,0 +1,217 @@ +#! /usr/bin/env python +# ex:ts=4:sw=4:sts=4:et +# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- +# +# BitBake Toaster Implementation +# +# Copyright (C) 2013-2016 Intel Corporation +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import re + +from django.core.urlresolvers import reverse +from django.utils import timezone +from tests.browser.selenium_helpers import SeleniumTestCase + +from orm.models import BitbakeVersion, Release, Project, Build +from orm.models import ProjectVariable + +class TestAllProjectsPage(SeleniumTestCase): + """ Browser tests for projects page /projects/ """ + + PROJECT_NAME = 'test project' + CLI_BUILDS_PROJECT_NAME = 'command line builds' + MACHINE_NAME = 'delorean' + + def setUp(self): + """ Add default project manually """ + project = Project.objects.create_project(self.CLI_BUILDS_PROJECT_NAME, None) + self.default_project = project + self.default_project.is_default = True + self.default_project.save() + + # this project is only set for some of the tests + self.project = None + + self.release = None + + def _add_build_to_default_project(self): + """ Add a build to the default project (not used in all tests) """ + now = timezone.now() + build = Build.objects.create(project=self.default_project, + started_on=now, + completed_on=now) + build.save() + + def _add_non_default_project(self): + """ Add another project """ + bbv = BitbakeVersion.objects.create(name='test bbv', giturl='/tmp/', + branch='master', dirpath='') + self.release = Release.objects.create(name='test release', + branch_name='master', + bitbake_version=bbv) + self.project = Project.objects.create_project(self.PROJECT_NAME, self.release) + self.project.is_default = False + self.project.save() + + # fake the MACHINE variable + project_var = ProjectVariable.objects.create(project=self.project, + name='MACHINE', + value=self.MACHINE_NAME) + project_var.save() + + def _get_row_for_project(self, project_name): + """ Get the HTML row for a project, or None if not found """ + self.wait_until_present('#projectstable tbody tr') + rows = self.find_all('#projectstable tbody tr') + + # find the row with a project name matching the one supplied + found_row = None + for row in rows: + if re.search(project_name, row.get_attribute('innerHTML')): + found_row = row + break + + return found_row + + def test_default_project_hidden(self): + """ + The default project should be hidden if it has no builds + and we should see the "no results" area + """ + url = reverse('all-projects') + self.get(url) + self.wait_until_visible('#empty-state-projectstable') + + rows = self.find_all('#projectstable tbody tr') + self.assertEqual(len(rows), 0, 'should be no projects displayed') + + def test_default_project_has_build(self): + """ The default project should be shown if it has builds """ + self._add_build_to_default_project() + + url = reverse('all-projects') + self.get(url) + + default_project_row = self._get_row_for_project(self.default_project.name) + + self.assertNotEqual(default_project_row, None, + 'default project "cli builds" should be in page') + + def test_default_project_release(self): + """ + The release for the default project should display as + 'Not applicable' + """ + # need a build, otherwise project doesn't display at all + self._add_build_to_default_project() + + # another project to test, which should show release + self._add_non_default_project() + + self.get(reverse('all-projects')) + self.wait_until_visible("#projectstable tr") + + # find the row for the default project + default_project_row = self._get_row_for_project(self.default_project.name) + + # check the release text for the default project + selector = 'span[data-project-field="release"] span.text-muted' + element = default_project_row.find_element_by_css_selector(selector) + text = element.text.strip() + self.assertEqual(text, 'Not applicable', + 'release should be "not applicable" for default project') + + # find the row for the default project + other_project_row = self._get_row_for_project(self.project.name) + + # check the link in the release cell for the other project + selector = 'span[data-project-field="release"]' + element = other_project_row.find_element_by_css_selector(selector) + text = element.text.strip() + self.assertEqual(text, self.release.name, + 'release name should be shown for non-default project') + + def test_default_project_machine(self): + """ + The machine for the default project should display as + 'Not applicable' + """ + # need a build, otherwise project doesn't display at all + self._add_build_to_default_project() + + # another project to test, which should show machine + self._add_non_default_project() + + self.get(reverse('all-projects')) + + self.wait_until_visible("#projectstable tr") + + # find the row for the default project + default_project_row = self._get_row_for_project(self.default_project.name) + + # check the machine cell for the default project + selector = 'span[data-project-field="machine"] span.text-muted' + element = default_project_row.find_element_by_css_selector(selector) + text = element.text.strip() + self.assertEqual(text, 'Not applicable', + 'machine should be not applicable for default project') + + # find the row for the default project + other_project_row = self._get_row_for_project(self.project.name) + + # check the link in the machine cell for the other project + selector = 'span[data-project-field="machine"]' + element = other_project_row.find_element_by_css_selector(selector) + text = element.text.strip() + self.assertEqual(text, self.MACHINE_NAME, + 'machine name should be shown for non-default project') + + def test_project_page_links(self): + """ + Test that links for the default project point to the builds + page /projects/X/builds for that project, and that links for + other projects point to their configuration pages /projects/X/ + """ + + # need a build, otherwise project doesn't display at all + self._add_build_to_default_project() + + # another project to test + self._add_non_default_project() + + self.get(reverse('all-projects')) + + # find the row for the default project + default_project_row = self._get_row_for_project(self.default_project.name) + + # check the link on the name field + selector = 'span[data-project-field="name"] a' + element = default_project_row.find_element_by_css_selector(selector) + link_url = element.get_attribute('href').strip() + expected_url = reverse('projectbuilds', args=(self.default_project.id,)) + msg = 'link on default project name should point to builds but was %s' % link_url + self.assertTrue(link_url.endswith(expected_url), msg) + + # find the row for the other project + other_project_row = self._get_row_for_project(self.project.name) + + # check the link for the other project + selector = 'span[data-project-field="name"] a' + element = other_project_row.find_element_by_css_selector(selector) + link_url = element.get_attribute('href').strip() + expected_url = reverse('project', args=(self.project.id,)) + msg = 'link on project name should point to configuration but was %s' % link_url + self.assertTrue(link_url.endswith(expected_url), msg) diff --git a/poky/bitbake/lib/toaster/tests/browser/test_builddashboard_page.py b/poky/bitbake/lib/toaster/tests/browser/test_builddashboard_page.py new file mode 100644 index 000000000..f8ccb5452 --- /dev/null +++ b/poky/bitbake/lib/toaster/tests/browser/test_builddashboard_page.py @@ -0,0 +1,347 @@ +#! /usr/bin/env python +# ex:ts=4:sw=4:sts=4:et +# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- +# +# BitBake Toaster Implementation +# +# Copyright (C) 2013-2016 Intel Corporation +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +from django.core.urlresolvers import reverse +from django.utils import timezone + +from tests.browser.selenium_helpers import SeleniumTestCase + +from orm.models import Project, Release, BitbakeVersion, Build, LogMessage +from orm.models import Layer, Layer_Version, Recipe, CustomImageRecipe, Variable + +class TestBuildDashboardPage(SeleniumTestCase): + """ Tests for the build dashboard /build/X """ + + def setUp(self): + bbv = BitbakeVersion.objects.create(name='bbv1', giturl='/tmp/', + branch='master', dirpath="") + release = Release.objects.create(name='release1', + bitbake_version=bbv) + project = Project.objects.create_project(name='test project', + release=release) + + now = timezone.now() + + self.build1 = Build.objects.create(project=project, + started_on=now, + completed_on=now, + outcome=Build.SUCCEEDED) + + self.build2 = Build.objects.create(project=project, + started_on=now, + completed_on=now, + outcome=Build.SUCCEEDED) + + self.build3 = Build.objects.create(project=project, + started_on=now, + completed_on=now, + outcome=Build.FAILED) + + # add Variable objects to the successful builds, as this is the criterion + # used to determine whether the left-hand panel should be displayed + Variable.objects.create(build=self.build1, + variable_name='Foo', + variable_value='Bar') + Variable.objects.create(build=self.build2, + variable_name='Foo', + variable_value='Bar') + + # exception + msg1 = 'an exception was thrown' + self.exception_message = LogMessage.objects.create( + build=self.build1, + level=LogMessage.EXCEPTION, + message=msg1 + ) + + # critical + msg2 = 'a critical error occurred' + self.critical_message = LogMessage.objects.create( + build=self.build1, + level=LogMessage.CRITICAL, + message=msg2 + ) + + # error on the failed build + msg3 = 'an error occurred' + self.error_message = LogMessage.objects.create( + build=self.build3, + level=LogMessage.ERROR, + message=msg3 + ) + + # warning on the failed build + msg4 = 'DANGER WILL ROBINSON' + self.warning_message = LogMessage.objects.create( + build=self.build3, + level=LogMessage.WARNING, + message=msg4 + ) + + # recipes related to the build, for testing the edit custom image/new + # custom image buttons + layer = Layer.objects.create(name='alayer') + layer_version = Layer_Version.objects.create( + layer=layer, build=self.build1 + ) + + # non-image recipes related to a build, for testing the new custom + # image button + layer_version2 = Layer_Version.objects.create(layer=layer, + build=self.build3) + + # image recipes + self.image_recipe1 = Recipe.objects.create( + name='recipeA', + layer_version=layer_version, + file_path='/foo/recipeA.bb', + is_image=True + ) + self.image_recipe2 = Recipe.objects.create( + name='recipeB', + layer_version=layer_version, + file_path='/foo/recipeB.bb', + is_image=True + ) + + # custom image recipes for this project + self.custom_image_recipe1 = CustomImageRecipe.objects.create( + name='customRecipeY', + project=project, + layer_version=layer_version, + file_path='/foo/customRecipeY.bb', + base_recipe=self.image_recipe1, + is_image=True + ) + self.custom_image_recipe2 = CustomImageRecipe.objects.create( + name='customRecipeZ', + project=project, + layer_version=layer_version, + file_path='/foo/customRecipeZ.bb', + base_recipe=self.image_recipe2, + is_image=True + ) + + # custom image recipe for a different project (to test filtering + # of image recipes and custom image recipes is correct: this shouldn't + # show up in either query against self.build1) + self.custom_image_recipe3 = CustomImageRecipe.objects.create( + name='customRecipeOmega', + project=Project.objects.create(name='baz', release=release), + layer_version=Layer_Version.objects.create( + layer=layer, build=self.build2 + ), + file_path='/foo/customRecipeOmega.bb', + base_recipe=self.image_recipe2, + is_image=True + ) + + # another non-image recipe (to test filtering of image recipes and + # custom image recipes is correct: this shouldn't show up in either + # for any build) + self.non_image_recipe = Recipe.objects.create( + name='nonImageRecipe', + layer_version=layer_version, + file_path='/foo/nonImageRecipe.bb', + is_image=False + ) + + def _get_build_dashboard(self, build): + """ + Navigate to the build dashboard for build + """ + url = reverse('builddashboard', args=(build.id,)) + self.get(url) + + def _get_build_dashboard_errors(self, build): + """ + Get a list of HTML fragments representing the errors on the + dashboard for the Build object build + """ + self._get_build_dashboard(build) + return self.find_all('#errors div.alert-danger') + + def _check_for_log_message(self, message_elements, log_message): + """ + Check that the LogMessage <log_message> has a representation in + the HTML elements <message_elements>. + + message_elements: WebElements representing the log messages shown + in the build dashboard; each should have a <pre> element inside + it with a data-log-message-id attribute + + log_message: orm.models.LogMessage instance + """ + expected_text = log_message.message + expected_pk = str(log_message.pk) + + found = False + for element in message_elements: + log_message_text = element.find_element_by_tag_name('pre').text.strip() + text_matches = (log_message_text == expected_text) + + log_message_pk = element.get_attribute('data-log-message-id') + id_matches = (log_message_pk == expected_pk) + + if text_matches and id_matches: + found = True + break + + template_vars = (expected_text, expected_pk) + assertion_failed_msg = 'message not found: ' \ + 'expected text "%s" and ID %s' % template_vars + self.assertTrue(found, assertion_failed_msg) + + def _check_for_error_message(self, build, log_message): + """ + Check whether the LogMessage instance <log_message> is + represented as an HTML error in the dashboard page for the Build object + build + """ + errors = self._get_build_dashboard_errors(build) + self._check_for_log_message(errors, log_message) + + def _check_labels_in_modal(self, modal, expected): + """ + Check that the text values of the <label> elements inside + the WebElement modal match the list of text values in expected + """ + # labels containing the radio buttons we're testing for + labels = modal.find_elements_by_css_selector(".radio") + + labels_text = [lab.text for lab in labels] + self.assertEqual(len(labels_text), len(expected)) + + for expected_text in expected: + self.assertTrue(expected_text in labels_text, + "Could not find %s in %s" % (expected_text, + labels_text)) + + def test_exceptions_show_as_errors(self): + """ + LogMessages with level EXCEPTION should display in the errors + section of the page + """ + self._check_for_error_message(self.build1, self.exception_message) + + def test_criticals_show_as_errors(self): + """ + LogMessages with level CRITICAL should display in the errors + section of the page + """ + self._check_for_error_message(self.build1, self.critical_message) + + def test_edit_custom_image_button(self): + """ + A build which built two custom images should present a modal which lets + the user choose one of them to edit + """ + self._get_build_dashboard(self.build1) + + # click the "edit custom image" button, which populates the modal + selector = '[data-role="edit-custom-image-trigger"]' + self.click(selector) + + modal = self.driver.find_element_by_id('edit-custom-image-modal') + self.wait_until_visible("#edit-custom-image-modal") + + # recipes we expect to see in the edit custom image modal + expected_recipes = [ + self.custom_image_recipe1.name, + self.custom_image_recipe2.name + ] + + self._check_labels_in_modal(modal, expected_recipes) + + def test_new_custom_image_button(self): + """ + Check that a build with multiple images and custom images presents + all of them as options for creating a new custom image from + """ + self._get_build_dashboard(self.build1) + + # click the "new custom image" button, which populates the modal + selector = '[data-role="new-custom-image-trigger"]' + self.click(selector) + + modal = self.driver.find_element_by_id('new-custom-image-modal') + self.wait_until_visible("#new-custom-image-modal") + + # recipes we expect to see in the new custom image modal + expected_recipes = [ + self.image_recipe1.name, + self.image_recipe2.name, + self.custom_image_recipe1.name, + self.custom_image_recipe2.name + ] + + self._check_labels_in_modal(modal, expected_recipes) + + def test_new_custom_image_button_no_image(self): + """ + Check that a build which builds non-image recipes doesn't show + the new custom image button on the dashboard. + """ + self._get_build_dashboard(self.build3) + selector = '[data-role="new-custom-image-trigger"]' + self.assertFalse(self.element_exists(selector), + 'new custom image button should not show for builds which ' \ + 'don\'t have any image recipes') + + def test_left_panel(self): + """" + Builds which succeed should have a left panel and a build summary + """ + self._get_build_dashboard(self.build1) + + left_panel = self.find_all('#nav') + self.assertEqual(len(left_panel), 1) + + build_summary = self.find_all('[data-role="build-summary-heading"]') + self.assertEqual(len(build_summary), 1) + + def test_failed_no_left_panel(self): + """ + Builds which fail should have no left panel and no build summary + """ + self._get_build_dashboard(self.build3) + + left_panel = self.find_all('#nav') + self.assertEqual(len(left_panel), 0) + + build_summary = self.find_all('[data-role="build-summary-heading"]') + self.assertEqual(len(build_summary), 0) + + def test_failed_shows_errors_and_warnings(self): + """ + Failed builds should still show error and warning messages + """ + self._get_build_dashboard(self.build3) + + errors = self.find_all('#errors div.alert-danger') + self._check_for_log_message(errors, self.error_message) + + # expand the warnings area + self.click('#warning-toggle') + self.wait_until_visible('#warnings div.alert-warning') + + warnings = self.find_all('#warnings div.alert-warning') + self._check_for_log_message(warnings, self.warning_message) diff --git a/poky/bitbake/lib/toaster/tests/browser/test_builddashboard_page_artifacts.py b/poky/bitbake/lib/toaster/tests/browser/test_builddashboard_page_artifacts.py new file mode 100644 index 000000000..1c627ad49 --- /dev/null +++ b/poky/bitbake/lib/toaster/tests/browser/test_builddashboard_page_artifacts.py @@ -0,0 +1,222 @@ +#! /usr/bin/env python +# ex:ts=4:sw=4:sts=4:et +# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- +# +# BitBake Toaster Implementation +# +# Copyright (C) 2013-2016 Intel Corporation +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +from django.core.urlresolvers import reverse +from django.utils import timezone + +from tests.browser.selenium_helpers import SeleniumTestCase + +from orm.models import Project, Release, BitbakeVersion, Build, Target, Package +from orm.models import Target_Image_File, TargetSDKFile, TargetKernelFile +from orm.models import Target_Installed_Package, Variable + +class TestBuildDashboardPageArtifacts(SeleniumTestCase): + """ Tests for artifacts on the build dashboard /build/X """ + + def setUp(self): + bbv = BitbakeVersion.objects.create(name='bbv1', giturl='/tmp/', + branch='master', dirpath="") + release = Release.objects.create(name='release1', + bitbake_version=bbv) + self.project = Project.objects.create_project(name='test project', + release=release) + + def _get_build_dashboard(self, build): + """ + Navigate to the build dashboard for build + """ + url = reverse('builddashboard', args=(build.id,)) + self.get(url) + + def _has_build_artifacts_heading(self): + """ + Check whether the "Build artifacts" heading is visible (True if it + is, False otherwise). + """ + return self.element_exists('[data-heading="build-artifacts"]') + + def _has_images_menu_option(self): + """ + Try to get the "Images" list element from the left-hand menu in the + build dashboard, and return True if it is present, False otherwise. + """ + return self.element_exists('li.nav-header[data-menu-heading="images"]') + + def test_no_artifacts(self): + """ + If a build produced no artifacts, the artifacts heading and images + menu option shouldn't show. + """ + now = timezone.now() + build = Build.objects.create(project=self.project, + started_on=now, completed_on=now, outcome=Build.SUCCEEDED) + + Target.objects.create(is_image=False, build=build, task='', + target='mpfr-native') + + self._get_build_dashboard(build) + + # check build artifacts heading + msg = 'Build artifacts heading should not be displayed for non-image' \ + 'builds' + self.assertFalse(self._has_build_artifacts_heading(), msg) + + # check "Images" option in left-hand menu (should not be there) + msg = 'Images option should not be shown in left-hand menu' + self.assertFalse(self._has_images_menu_option(), msg) + + def test_sdk_artifacts(self): + """ + If a build produced SDK artifacts, they should be shown, but the section + for image files and the images menu option should be hidden. + + The packages count and size should also be hidden. + """ + now = timezone.now() + build = Build.objects.create(project=self.project, + started_on=now, completed_on=timezone.now(), + outcome=Build.SUCCEEDED) + + target = Target.objects.create(is_image=True, build=build, + task='populate_sdk', target='core-image-minimal') + + sdk_file1 = TargetSDKFile.objects.create(target=target, + file_size=100000, + file_name='/home/foo/core-image-minimal.toolchain.sh') + + sdk_file2 = TargetSDKFile.objects.create(target=target, + file_size=120000, + file_name='/home/foo/x86_64.toolchain.sh') + + self._get_build_dashboard(build) + + # check build artifacts heading + msg = 'Build artifacts heading should be displayed for SDK ' \ + 'builds which generate artifacts' + self.assertTrue(self._has_build_artifacts_heading(), msg) + + # check "Images" option in left-hand menu (should not be there) + msg = 'Images option should not be shown in left-hand menu for ' \ + 'builds which didn\'t generate an image file' + self.assertFalse(self._has_images_menu_option(), msg) + + # check links to SDK artifacts + sdk_artifact_links = self.find_all('[data-links="sdk-artifacts"] li') + self.assertEqual(len(sdk_artifact_links), 2, + 'should be links to 2 SDK artifacts') + + # package count and size should not be visible, no link on + # target name + selector = '[data-value="target-package-count"]' + self.assertFalse(self.element_exists(selector), + 'package count should not be shown for non-image builds') + + selector = '[data-value="target-package-size"]' + self.assertFalse(self.element_exists(selector), + 'package size should not be shown for non-image builds') + + selector = '[data-link="target-packages"]' + self.assertFalse(self.element_exists(selector), + 'link to target packages should not be on target heading') + + def test_image_artifacts(self): + """ + If a build produced image files, kernel artifacts, and manifests, + they should all be shown, as well as the image link in the left-hand + menu. + + The packages count and size should be shown, with a link to the + package display page. + """ + now = timezone.now() + build = Build.objects.create(project=self.project, + started_on=now, completed_on=timezone.now(), + outcome=Build.SUCCEEDED) + + # add a variable to the build so that it counts as "started" + Variable.objects.create(build=build, + variable_name='Christopher', + variable_value='Lee') + + target = Target.objects.create(is_image=True, build=build, + task='', target='core-image-minimal', + license_manifest_path='/home/foo/license.manifest', + package_manifest_path='/home/foo/package.manifest') + + image_file = Target_Image_File.objects.create(target=target, + file_name='/home/foo/core-image-minimal.ext4', file_size=9000) + + kernel_file1 = TargetKernelFile.objects.create(target=target, + file_name='/home/foo/bzImage', file_size=2000) + + kernel_file2 = TargetKernelFile.objects.create(target=target, + file_name='/home/foo/bzImage', file_size=2000) + + package = Package.objects.create(build=build, name='foo', size=1024, + installed_name='foo1') + installed_package = Target_Installed_Package.objects.create( + target=target, package=package) + + self._get_build_dashboard(build) + + # check build artifacts heading + msg = 'Build artifacts heading should be displayed for image ' \ + 'builds' + self.assertTrue(self._has_build_artifacts_heading(), msg) + + # check "Images" option in left-hand menu (should be there) + msg = 'Images option should be shown in left-hand menu for image builds' + self.assertTrue(self._has_images_menu_option(), msg) + + # check link to image file + selector = '[data-links="image-artifacts"] li' + self.assertTrue(self.element_exists(selector), + 'should be a link to the image file (selector %s)' % selector) + + # check links to kernel artifacts + kernel_artifact_links = \ + self.find_all('[data-links="kernel-artifacts"] li') + self.assertEqual(len(kernel_artifact_links), 2, + 'should be links to 2 kernel artifacts') + + # check manifest links + selector = 'a[data-link="license-manifest"]' + self.assertTrue(self.element_exists(selector), + 'should be a link to the license manifest (selector %s)' % selector) + + selector = 'a[data-link="package-manifest"]' + self.assertTrue(self.element_exists(selector), + 'should be a link to the package manifest (selector %s)' % selector) + + # check package count and size, link on target name + selector = '[data-value="target-package-count"]' + element = self.find(selector) + self.assertEquals(element.text, '1', + 'package count should be shown for image builds') + + selector = '[data-value="target-package-size"]' + element = self.find(selector) + self.assertEquals(element.text, '1.0 KB', + 'package size should be shown for image builds') + + selector = '[data-link="target-packages"]' + self.assertTrue(self.element_exists(selector), + 'link to target packages should be on target heading') diff --git a/poky/bitbake/lib/toaster/tests/browser/test_builddashboard_page_recipes.py b/poky/bitbake/lib/toaster/tests/browser/test_builddashboard_page_recipes.py new file mode 100644 index 000000000..ed18324e5 --- /dev/null +++ b/poky/bitbake/lib/toaster/tests/browser/test_builddashboard_page_recipes.py @@ -0,0 +1,66 @@ +#! /usr/bin/env python +# ex:ts=4:sw=4:sts=4:et +# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- +# +# BitBake Toaster Implementation +# +# Copyright (C) 2013-2016 Intel Corporation +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +from django.core.urlresolvers import reverse +from django.utils import timezone +from tests.browser.selenium_helpers import SeleniumTestCase +from orm.models import Project, Build, Recipe, Task, Layer, Layer_Version +from orm.models import Target + +class TestBuilddashboardPageRecipes(SeleniumTestCase): + """ Test build dashboard recipes sub-page """ + + def setUp(self): + project = Project.objects.get_or_create_default_project() + + now = timezone.now() + + self.build = Build.objects.create(project=project, + started_on=now, + completed_on=now) + + layer = Layer.objects.create() + + layer_version = Layer_Version.objects.create(layer=layer, + build=self.build) + + recipe = Recipe.objects.create(layer_version=layer_version) + + task = Task.objects.create(build=self.build, recipe=recipe, order=1) + + Target.objects.create(build=self.build, task=task, target='do_build') + + def test_build_recipes_columns(self): + """ + Check that non-hideable columns of the table on the recipes sub-page + are disabled on the edit columns dropdown. + """ + url = reverse('recipes', args=(self.build.id,)) + self.get(url) + + self.wait_until_visible('#edit-columns-button') + + # check that options for the non-hideable columns are disabled + non_hideable = ['name', 'version'] + + for column in non_hideable: + selector = 'input#checkbox-%s[disabled="disabled"]' % column + self.wait_until_present(selector) diff --git a/poky/bitbake/lib/toaster/tests/browser/test_builddashboard_page_tasks.py b/poky/bitbake/lib/toaster/tests/browser/test_builddashboard_page_tasks.py new file mode 100644 index 000000000..da50f1601 --- /dev/null +++ b/poky/bitbake/lib/toaster/tests/browser/test_builddashboard_page_tasks.py @@ -0,0 +1,65 @@ +#! /usr/bin/env python +# ex:ts=4:sw=4:sts=4:et +# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- +# +# BitBake Toaster Implementation +# +# Copyright (C) 2013-2016 Intel Corporation +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +from django.core.urlresolvers import reverse +from django.utils import timezone +from tests.browser.selenium_helpers import SeleniumTestCase +from orm.models import Project, Build, Recipe, Task, Layer, Layer_Version +from orm.models import Target + +class TestBuilddashboardPageTasks(SeleniumTestCase): + """ Test build dashboard tasks sub-page """ + + def setUp(self): + project = Project.objects.get_or_create_default_project() + + now = timezone.now() + + self.build = Build.objects.create(project=project, + started_on=now, + completed_on=now) + + layer = Layer.objects.create() + + layer_version = Layer_Version.objects.create(layer=layer) + + recipe = Recipe.objects.create(layer_version=layer_version) + + task = Task.objects.create(build=self.build, recipe=recipe, order=1) + + Target.objects.create(build=self.build, task=task, target='do_build') + + def test_build_tasks_columns(self): + """ + Check that non-hideable columns of the table on the tasks sub-page + are disabled on the edit columns dropdown. + """ + url = reverse('tasks', args=(self.build.id,)) + self.get(url) + + self.wait_until_visible('#edit-columns-button') + + # check that options for the non-hideable columns are disabled + non_hideable = ['order', 'task_name', 'recipe__name'] + + for column in non_hideable: + selector = 'input#checkbox-%s[disabled="disabled"]' % column + self.wait_until_present(selector) diff --git a/poky/bitbake/lib/toaster/tests/browser/test_js_unit_tests.py b/poky/bitbake/lib/toaster/tests/browser/test_js_unit_tests.py new file mode 100644 index 000000000..3c0b96252 --- /dev/null +++ b/poky/bitbake/lib/toaster/tests/browser/test_js_unit_tests.py @@ -0,0 +1,57 @@ +#! /usr/bin/env python +# ex:ts=4:sw=4:sts=4:et +# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- +# +# BitBake Toaster Implementation +# +# Copyright (C) 2013-2016 Intel Corporation +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +""" +Run the js unit tests +""" + +from django.core.urlresolvers import reverse +from tests.browser.selenium_helpers import SeleniumTestCase +import logging + +logger = logging.getLogger("toaster") + + +class TestJsUnitTests(SeleniumTestCase): + """ Test landing page shows the Toaster brand """ + + fixtures = ['toastergui-unittest-data'] + + def test_that_js_unit_tests_pass(self): + url = reverse('js-unit-tests') + self.get(url) + self.wait_until_present('#qunit-testresult .failed') + + failed = self.find("#qunit-testresult .failed").text + passed = self.find("#qunit-testresult .passed").text + total = self.find("#qunit-testresult .total").text + + logger.info("Js unit tests completed %s out of %s passed, %s failed", + passed, + total, + failed) + + failed_tests = self.find_all("li .fail .test-message") + for fail in failed_tests: + logger.error("JS unit test failed: %s" % fail.text) + + self.assertEqual(failed, '0', + "%s JS unit tests failed" % failed) diff --git a/poky/bitbake/lib/toaster/tests/browser/test_landing_page.py b/poky/bitbake/lib/toaster/tests/browser/test_landing_page.py new file mode 100644 index 000000000..4d4cd660f --- /dev/null +++ b/poky/bitbake/lib/toaster/tests/browser/test_landing_page.py @@ -0,0 +1,108 @@ +#! /usr/bin/env python +# ex:ts=4:sw=4:sts=4:et +# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- +# +# BitBake Toaster Implementation +# +# Copyright (C) 2013-2016 Intel Corporation +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +from django.core.urlresolvers import reverse +from django.utils import timezone +from tests.browser.selenium_helpers import SeleniumTestCase + +from orm.models import Project, Build + +class TestLandingPage(SeleniumTestCase): + """ Tests for redirects on the landing page """ + + PROJECT_NAME = 'test project' + LANDING_PAGE_TITLE = 'This is Toaster' + CLI_BUILDS_PROJECT_NAME = 'command line builds' + + def setUp(self): + """ Add default project manually """ + self.project = Project.objects.create_project( + self.CLI_BUILDS_PROJECT_NAME, + None + ) + self.project.is_default = True + self.project.save() + + def test_only_default_project(self): + """ + No projects except default + => should see the landing page + """ + self.get(reverse('landing')) + self.assertTrue(self.LANDING_PAGE_TITLE in self.get_page_source()) + + def test_default_project_has_build(self): + """ + Default project has a build, no other projects + => should see the builds page + """ + now = timezone.now() + build = Build.objects.create(project=self.project, + started_on=now, + completed_on=now) + build.save() + + self.get(reverse('landing')) + + elements = self.find_all('#allbuildstable') + self.assertEqual(len(elements), 1, 'should redirect to builds') + content = self.get_page_source() + self.assertFalse(self.PROJECT_NAME in content, + 'should not show builds for project %s' % self.PROJECT_NAME) + self.assertTrue(self.CLI_BUILDS_PROJECT_NAME in content, + 'should show builds for cli project') + + def test_user_project_exists(self): + """ + User has added a project (without builds) + => should see the projects page + """ + user_project = Project.objects.create_project('foo', None) + user_project.save() + + self.get(reverse('landing')) + + elements = self.find_all('#projectstable') + self.assertEqual(len(elements), 1, 'should redirect to projects') + + def test_user_project_has_build(self): + """ + User has added a project (with builds), command line builds doesn't + => should see the builds page + """ + user_project = Project.objects.create_project(self.PROJECT_NAME, None) + user_project.save() + + now = timezone.now() + build = Build.objects.create(project=user_project, + started_on=now, + completed_on=now) + build.save() + + self.get(reverse('landing')) + + elements = self.find_all('#allbuildstable') + self.assertEqual(len(elements), 1, 'should redirect to builds') + content = self.get_page_source() + self.assertTrue(self.PROJECT_NAME in content, + 'should show builds for project %s' % self.PROJECT_NAME) + self.assertFalse(self.CLI_BUILDS_PROJECT_NAME in content, + 'should not show builds for cli project') diff --git a/poky/bitbake/lib/toaster/tests/browser/test_layerdetails_page.py b/poky/bitbake/lib/toaster/tests/browser/test_layerdetails_page.py new file mode 100644 index 000000000..f24fb093a --- /dev/null +++ b/poky/bitbake/lib/toaster/tests/browser/test_layerdetails_page.py @@ -0,0 +1,216 @@ +#! /usr/bin/env python +# ex:ts=4:sw=4:sts=4:et +# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- +# +# BitBake Toaster Implementation +# +# Copyright (C) 2013-2016 Intel Corporation +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +from django.core.urlresolvers import reverse +from tests.browser.selenium_helpers import SeleniumTestCase + +from orm.models import Layer, Layer_Version, Project, LayerSource, Release +from orm.models import BitbakeVersion + +from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.common.by import By + + +class TestLayerDetailsPage(SeleniumTestCase): + """ Test layerdetails page works correctly """ + + def __init__(self, *args, **kwargs): + super(TestLayerDetailsPage, self).__init__(*args, **kwargs) + + self.initial_values = None + self.url = None + self.imported_layer_version = None + + def setUp(self): + release = Release.objects.create( + name='baz', + bitbake_version=BitbakeVersion.objects.create(name='v1') + ) + + # project to add new custom images to + self.project = Project.objects.create(name='foo', release=release) + + name = "meta-imported" + vcs_url = "git://example.com/meta-imported" + subdir = "/layer" + gitrev = "d33d" + summary = "A imported layer" + description = "This was imported" + + imported_layer = Layer.objects.create(name=name, + vcs_url=vcs_url, + summary=summary, + description=description) + + self.imported_layer_version = Layer_Version.objects.create( + layer=imported_layer, + layer_source=LayerSource.TYPE_IMPORTED, + branch=gitrev, + commit=gitrev, + dirpath=subdir, + project=self.project) + + self.initial_values = [name, vcs_url, subdir, gitrev, summary, + description] + self.url = reverse('layerdetails', + args=(self.project.pk, + self.imported_layer_version.pk)) + + def test_edit_layerdetails(self): + """ Edit all the editable fields for the layer refresh the page and + check that the new values exist""" + + self.get(self.url) + + self.click("#add-remove-layer-btn") + self.click("#edit-layer-source") + self.click("#repo") + + self.wait_until_visible("#layer-git-repo-url") + + # Open every edit box + for btn in self.find_all("dd .glyphicon-edit"): + btn.click() + + # Wait for the inputs to become visible after animation + self.wait_until_visible("#layer-git input[type=text]") + self.wait_until_visible("dd textarea") + self.wait_until_visible("dd .change-btn") + + # Edit each value + for inputs in self.find_all("#layer-git input[type=text]") + \ + self.find_all("dd textarea"): + # ignore the tt inputs (twitter typeahead input) + if "tt-" in inputs.get_attribute("class"): + continue + + value = inputs.get_attribute("value") + + self.assertTrue(value in self.initial_values, + "Expecting any of \"%s\"but got \"%s\"" % + (self.initial_values, value)) + + inputs.send_keys("-edited") + + # Save the new values + for save_btn in self.find_all(".change-btn"): + save_btn.click() + + self.click("#save-changes-for-switch") + self.wait_until_visible("#edit-layer-source") + + # Refresh the page to see if the new values are returned + self.get(self.url) + + new_values = ["%s-edited" % old_val + for old_val in self.initial_values] + + for inputs in self.find_all('#layer-git input[type="text"]') + \ + self.find_all('dd textarea'): + # ignore the tt inputs (twitter typeahead input) + if "tt-" in inputs.get_attribute("class"): + continue + + value = inputs.get_attribute("value") + + self.assertTrue(value in new_values, + "Expecting any of \"%s\" but got \"%s\"" % + (new_values, value)) + + # Now convert it to a local layer + self.click("#edit-layer-source") + self.click("#dir") + dir_input = self.wait_until_visible("#layer-dir-path-in-details") + + new_dir = "/home/test/my-meta-dir" + dir_input.send_keys(new_dir) + + self.click("#save-changes-for-switch") + self.wait_until_visible("#edit-layer-source") + + # Refresh the page to see if the new values are returned + self.get(self.url) + dir_input = self.find("#layer-dir-path-in-details") + self.assertTrue(new_dir in dir_input.get_attribute("value"), + "Expected %s in the dir value for layer directory" % + new_dir) + + def test_delete_layer(self): + """ Delete the layer """ + + self.get(self.url) + + # Wait for the tables to load to avoid a race condition where the + # toaster tables have made an async request. If the layer is deleted + # before the request finishes it will cause an exception and fail this + # test. + wait = WebDriverWait(self.driver, 30) + + wait.until(EC.text_to_be_present_in_element( + (By.CLASS_NAME, + "table-count-recipestable"), "0")) + + wait.until(EC.text_to_be_present_in_element( + (By.CLASS_NAME, + "table-count-machinestable"), "0")) + + self.click('a[data-target="#delete-layer-modal"]') + self.wait_until_visible("#delete-layer-modal") + self.click("#layer-delete-confirmed") + + notification = self.wait_until_visible("#change-notification-msg") + expected_text = "You have deleted 1 layer from your project: %s" % \ + self.imported_layer_version.layer.name + + self.assertTrue(expected_text in notification.text, + "Expected notification text \"%s\" not found instead" + "it was \"%s\"" % + (expected_text, notification.text)) + + def test_addrm_to_project(self): + self.get(self.url) + + # Add the layer + self.click("#add-remove-layer-btn") + + notification = self.wait_until_visible("#change-notification-msg") + + expected_text = "You have added 1 layer to your project: %s" % \ + self.imported_layer_version.layer.name + + self.assertTrue(expected_text in notification.text, + "Expected notification text %s not found was " + " \"%s\" instead" % + (expected_text, notification.text)) + + # Remove the layer + self.click("#add-remove-layer-btn") + + notification = self.wait_until_visible("#change-notification-msg") + + expected_text = "You have removed 1 layer from your project: %s" % \ + self.imported_layer_version.layer.name + + self.assertTrue(expected_text in notification.text, + "Expected notification text %s not found was " + " \"%s\" instead" % + (expected_text, notification.text)) diff --git a/poky/bitbake/lib/toaster/tests/browser/test_most_recent_builds_states.py b/poky/bitbake/lib/toaster/tests/browser/test_most_recent_builds_states.py new file mode 100644 index 000000000..abc0b0bc8 --- /dev/null +++ b/poky/bitbake/lib/toaster/tests/browser/test_most_recent_builds_states.py @@ -0,0 +1,211 @@ +#! /usr/bin/env python +# ex:ts=4:sw=4:sts=4:et +# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- +# +# BitBake Toaster Implementation +# +# Copyright (C) 2013-2016 Intel Corporation +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +from django.core.urlresolvers import reverse +from django.utils import timezone +from tests.browser.selenium_helpers import SeleniumTestCase +from tests.browser.selenium_helpers_base import Wait +from orm.models import Project, Build, Task, Recipe, Layer, Layer_Version +from bldcontrol.models import BuildRequest + +class TestMostRecentBuildsStates(SeleniumTestCase): + """ Test states update correctly in most recent builds area """ + + def _create_build_request(self): + project = Project.objects.get_or_create_default_project() + + now = timezone.now() + + build = Build.objects.create(project=project, build_name='fakebuild', + started_on=now, completed_on=now) + + return BuildRequest.objects.create(build=build, project=project, + state=BuildRequest.REQ_QUEUED) + + def _create_recipe(self): + """ Add a recipe to the database and return it """ + layer = Layer.objects.create() + layer_version = Layer_Version.objects.create(layer=layer) + return Recipe.objects.create(name='foo', layer_version=layer_version) + + def _check_build_states(self, build_request): + recipes_to_parse = 10 + url = reverse('all-builds') + self.get(url) + + build = build_request.build + base_selector = '[data-latest-build-result="%s"] ' % build.id + + # build queued; check shown as queued + selector = base_selector + '[data-build-state="Queued"]' + element = self.wait_until_visible(selector) + self.assertRegexpMatches(element.get_attribute('innerHTML'), + 'Build queued', 'build should show queued status') + + # waiting for recipes to be parsed + build.outcome = Build.IN_PROGRESS + build.recipes_to_parse = recipes_to_parse + build.recipes_parsed = 0 + + build_request.state = BuildRequest.REQ_INPROGRESS + build_request.save() + + self.get(url) + + selector = base_selector + '[data-build-state="Parsing"]' + element = self.wait_until_visible(selector) + + bar_selector = '#recipes-parsed-percentage-bar-%s' % build.id + bar_element = element.find_element_by_css_selector(bar_selector) + self.assertEqual(bar_element.value_of_css_property('width'), '0px', + 'recipe parse progress should be at 0') + + # recipes being parsed; check parse progress + build.recipes_parsed = 5 + build.save() + + self.get(url) + + element = self.wait_until_visible(selector) + bar_element = element.find_element_by_css_selector(bar_selector) + recipe_bar_updated = lambda driver: \ + bar_element.get_attribute('style') == 'width: 50%;' + msg = 'recipe parse progress bar should update to 50%' + element = Wait(self.driver).until(recipe_bar_updated, msg) + + # all recipes parsed, task started, waiting for first task to finish; + # check status is shown as "Tasks starting..." + build.recipes_parsed = recipes_to_parse + build.save() + + recipe = self._create_recipe() + task1 = Task.objects.create(build=build, recipe=recipe, + task_name='Lionel') + task2 = Task.objects.create(build=build, recipe=recipe, + task_name='Jeffries') + + self.get(url) + + selector = base_selector + '[data-build-state="Starting"]' + element = self.wait_until_visible(selector) + self.assertRegexpMatches(element.get_attribute('innerHTML'), + 'Tasks starting', 'build should show "tasks starting" status') + + # first task finished; check tasks progress bar + task1.order = 1 + task1.save() + + self.get(url) + + selector = base_selector + '[data-build-state="In Progress"]' + element = self.wait_until_visible(selector) + + bar_selector = '#build-pc-done-bar-%s' % build.id + bar_element = element.find_element_by_css_selector(bar_selector) + + task_bar_updated = lambda driver: \ + bar_element.get_attribute('style') == 'width: 50%;' + msg = 'tasks progress bar should update to 50%' + element = Wait(self.driver).until(task_bar_updated, msg) + + # last task finished; check tasks progress bar updates + task2.order = 2 + task2.save() + + self.get(url) + + element = self.wait_until_visible(selector) + bar_element = element.find_element_by_css_selector(bar_selector) + task_bar_updated = lambda driver: \ + bar_element.get_attribute('style') == 'width: 100%;' + msg = 'tasks progress bar should update to 100%' + element = Wait(self.driver).until(task_bar_updated, msg) + + def test_states_to_success(self): + """ + Test state transitions in the recent builds area for a build which + completes successfully. + """ + build_request = self._create_build_request() + + self._check_build_states(build_request) + + # all tasks complete and build succeeded; check success state shown + build = build_request.build + build.outcome = Build.SUCCEEDED + build.save() + + selector = '[data-latest-build-result="%s"] ' \ + '[data-build-state="Succeeded"]' % build.id + element = self.wait_until_visible(selector) + + def test_states_to_failure(self): + """ + Test state transitions in the recent builds area for a build which + completes in a failure. + """ + build_request = self._create_build_request() + + self._check_build_states(build_request) + + # all tasks complete and build succeeded; check fail state shown + build = build_request.build + build.outcome = Build.FAILED + build.save() + + selector = '[data-latest-build-result="%s"] ' \ + '[data-build-state="Failed"]' % build.id + element = self.wait_until_visible(selector) + + def test_states_cancelling(self): + """ + Test that most recent build area updates correctly for a build + which is cancelled. + """ + url = reverse('all-builds') + + build_request = self._create_build_request() + build = build_request.build + + # cancel the build + build_request.state = BuildRequest.REQ_CANCELLING + build_request.save() + + self.get(url) + + # check cancelling state + selector = '[data-latest-build-result="%s"] ' \ + '[data-build-state="Cancelling"]' % build.id + element = self.wait_until_visible(selector) + self.assertRegexpMatches(element.get_attribute('innerHTML'), + 'Cancelling the build', 'build should show "cancelling" status') + + # check cancelled state + build.outcome = Build.CANCELLED + build.save() + + self.get(url) + + selector = '[data-latest-build-result="%s"] ' \ + '[data-build-state="Cancelled"]' % build.id + element = self.wait_until_visible(selector) + self.assertRegexpMatches(element.get_attribute('innerHTML'), + 'Build cancelled', 'build should show "cancelled" status') diff --git a/poky/bitbake/lib/toaster/tests/browser/test_new_custom_image_page.py b/poky/bitbake/lib/toaster/tests/browser/test_new_custom_image_page.py new file mode 100644 index 000000000..ab5a8e66b --- /dev/null +++ b/poky/bitbake/lib/toaster/tests/browser/test_new_custom_image_page.py @@ -0,0 +1,161 @@ +#! /usr/bin/env python +# ex:ts=4:sw=4:sts=4:et +# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- +# +# BitBake Toaster Implementation +# +# Copyright (C) 2013-2016 Intel Corporation +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +from django.core.urlresolvers import reverse +from tests.browser.selenium_helpers import SeleniumTestCase + +from orm.models import BitbakeVersion, Release, Project, ProjectLayer, Layer +from orm.models import Layer_Version, Recipe, CustomImageRecipe + + +class TestNewCustomImagePage(SeleniumTestCase): + CUSTOM_IMAGE_NAME = 'roopa-doopa' + + def setUp(self): + release = Release.objects.create( + name='baz', + bitbake_version=BitbakeVersion.objects.create(name='v1') + ) + + # project to add new custom images to + self.project = Project.objects.create(name='foo', release=release) + + # layer associated with the project + layer = Layer.objects.create(name='bar') + layer_version = Layer_Version.objects.create( + layer=layer, + project=self.project + ) + + # properly add the layer to the project + ProjectLayer.objects.create( + project=self.project, + layercommit=layer_version, + optional=False + ) + + # add a fake image recipe to the layer that can be customised + self.recipe = Recipe.objects.create( + name='core-image-minimal', + layer_version=layer_version, + is_image=True + ) + + # another project with a custom image already in it + project2 = Project.objects.create(name='whoop', release=release) + layer_version2 = Layer_Version.objects.create( + layer=layer, + project=project2 + ) + ProjectLayer.objects.create( + project=project2, + layercommit=layer_version2, + optional=False + ) + recipe2 = Recipe.objects.create( + name='core-image-minimal', + layer_version=layer_version2, + is_image=True + ) + CustomImageRecipe.objects.create( + name=self.CUSTOM_IMAGE_NAME, + base_recipe=recipe2, + layer_version=layer_version2, + file_path='/1/2', + project=project2 + ) + + def _create_custom_image(self, new_custom_image_name): + """ + 1. Go to the 'new custom image' page + 2. Click the button for the fake core-image-minimal + 3. Wait for the dialog box for setting the name of the new custom + image + 4. Insert new_custom_image_name into that dialog's text box + """ + url = reverse('newcustomimage', args=(self.project.id,)) + self.get(url) + + self.click('button[data-recipe="%s"]' % self.recipe.id) + + selector = '#new-custom-image-modal input[type="text"]' + self.enter_text(selector, new_custom_image_name) + + self.click('#create-new-custom-image-btn') + + def _check_for_custom_image(self, image_name): + """ + Fetch the list of custom images for the project and check the + image with name image_name is listed there + """ + url = reverse('projectcustomimages', args=(self.project.id,)) + self.get(url) + + self.wait_until_visible('#customimagestable') + + element = self.find('#customimagestable td[class="name"] a') + msg = 'should be a custom image link with text %s' % image_name + self.assertEqual(element.text.strip(), image_name, msg) + + def test_new_image(self): + """ + Should be able to create a new custom image + """ + custom_image_name = 'boo-image' + self._create_custom_image(custom_image_name) + self.wait_until_visible('#image-created-notification') + self._check_for_custom_image(custom_image_name) + + def test_new_duplicates_other_project_image(self): + """ + Should be able to create a new custom image if its name is the same + as a custom image in another project + """ + self._create_custom_image(self.CUSTOM_IMAGE_NAME) + self.wait_until_visible('#image-created-notification') + self._check_for_custom_image(self.CUSTOM_IMAGE_NAME) + + def test_new_duplicates_non_image_recipe(self): + """ + Should not be able to create a new custom image whose name is the + same as an existing non-image recipe + """ + self._create_custom_image(self.recipe.name) + element = self.wait_until_visible('#invalid-name-help') + self.assertRegexpMatches(element.text.strip(), + 'image with this name already exists') + + def test_new_duplicates_project_image(self): + """ + Should not be able to create a new custom image whose name is the same + as a custom image in this project + """ + # create the image + custom_image_name = 'doh-image' + self._create_custom_image(custom_image_name) + self.wait_until_visible('#image-created-notification') + self._check_for_custom_image(custom_image_name) + + # try to create an image with the same name + self._create_custom_image(custom_image_name) + element = self.wait_until_visible('#invalid-name-help') + expected = 'An image with this name already exists in this project' + self.assertRegexpMatches(element.text.strip(), expected) diff --git a/poky/bitbake/lib/toaster/tests/browser/test_new_project_page.py b/poky/bitbake/lib/toaster/tests/browser/test_new_project_page.py new file mode 100644 index 000000000..77e5f1526 --- /dev/null +++ b/poky/bitbake/lib/toaster/tests/browser/test_new_project_page.py @@ -0,0 +1,113 @@ +#! /usr/bin/env python +# ex:ts=4:sw=4:sts=4:et +# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- +# +# BitBake Toaster Implementation +# +# Copyright (C) 2013-2016 Intel Corporation +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +from django.core.urlresolvers import reverse +from tests.browser.selenium_helpers import SeleniumTestCase +from selenium.webdriver.support.ui import Select +from selenium.common.exceptions import InvalidElementStateException + +from orm.models import Project, Release, BitbakeVersion + + +class TestNewProjectPage(SeleniumTestCase): + """ Test project data at /project/X/ is displayed correctly """ + + def setUp(self): + bitbake, c = BitbakeVersion.objects.get_or_create( + name="master", + giturl="git://master", + branch="master", + dirpath="master") + + release, c = Release.objects.get_or_create(name="msater", + description="master" + "release", + branch_name="master", + helptext="latest", + bitbake_version=bitbake) + + self.release, c = Release.objects.get_or_create( + name="msater2", + description="master2" + "release2", + branch_name="master2", + helptext="latest2", + bitbake_version=bitbake) + + def test_create_new_project(self): + """ Test creating a project """ + + project_name = "masterproject" + + url = reverse('newproject') + self.get(url) + + self.enter_text('#new-project-name', project_name) + + select = Select(self.find('#projectversion')) + select.select_by_value(str(self.release.pk)) + + self.click("#create-project-button") + + # We should get redirected to the new project's page with the + # notification at the top + element = self.wait_until_visible('#project-created-notification') + + self.assertTrue(project_name in element.text, + "New project name not in new project notification") + + self.assertTrue(Project.objects.filter(name=project_name).count(), + "New project not found in database") + + def test_new_duplicates_project_name(self): + """ + Should not be able to create a new project whose name is the same + as an existing project + """ + + project_name = "dupproject" + + Project.objects.create_project(name=project_name, + release=self.release) + + url = reverse('newproject') + self.get(url) + + self.enter_text('#new-project-name', project_name) + + select = Select(self.find('#projectversion')) + select.select_by_value(str(self.release.pk)) + + element = self.wait_until_visible('#hint-error-project-name') + + self.assertTrue(("Project names must be unique" in element.text), + "Did not find unique project name error message") + + # Try and click it anyway, if it submits we'll have a new project in + # the db and assert then + try: + self.click("#create-project-button") + except InvalidElementStateException: + pass + + self.assertTrue( + (Project.objects.filter(name=project_name).count() == 1), + "New project not found in database") diff --git a/poky/bitbake/lib/toaster/tests/browser/test_project_builds_page.py b/poky/bitbake/lib/toaster/tests/browser/test_project_builds_page.py new file mode 100644 index 000000000..9fe91ab06 --- /dev/null +++ b/poky/bitbake/lib/toaster/tests/browser/test_project_builds_page.py @@ -0,0 +1,168 @@ +#! /usr/bin/env python +# ex:ts=4:sw=4:sts=4:et +# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- +# +# BitBake Toaster Implementation +# +# Copyright (C) 2013-2016 Intel Corporation +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import re + +from django.core.urlresolvers import reverse +from django.utils import timezone +from tests.browser.selenium_helpers import SeleniumTestCase + +from orm.models import BitbakeVersion, Release, Project, Build, Target + +class TestProjectBuildsPage(SeleniumTestCase): + """ Test data at /project/X/builds is displayed correctly """ + + PROJECT_NAME = 'test project' + CLI_BUILDS_PROJECT_NAME = 'command line builds' + + def setUp(self): + bbv = BitbakeVersion.objects.create(name='bbv1', giturl='/tmp/', + branch='master', dirpath='') + release = Release.objects.create(name='release1', + bitbake_version=bbv) + self.project1 = Project.objects.create_project(name=self.PROJECT_NAME, + release=release) + self.project1.save() + + self.project2 = Project.objects.create_project(name=self.PROJECT_NAME, + release=release) + self.project2.save() + + self.default_project = Project.objects.create_project( + name=self.CLI_BUILDS_PROJECT_NAME, + release=release + ) + self.default_project.is_default = True + self.default_project.save() + + # parameters for builds to associate with the projects + now = timezone.now() + + self.project1_build_success = { + 'project': self.project1, + 'started_on': now, + 'completed_on': now, + 'outcome': Build.SUCCEEDED + } + + self.project1_build_in_progress = { + 'project': self.project1, + 'started_on': now, + 'completed_on': now, + 'outcome': Build.IN_PROGRESS + } + + self.project2_build_success = { + 'project': self.project2, + 'started_on': now, + 'completed_on': now, + 'outcome': Build.SUCCEEDED + } + + self.project2_build_in_progress = { + 'project': self.project2, + 'started_on': now, + 'completed_on': now, + 'outcome': Build.IN_PROGRESS + } + + def _get_rows_for_project(self, project_id): + """ + Helper to retrieve HTML rows for a project's builds, + as shown in the main table of the page + """ + url = reverse('projectbuilds', args=(project_id,)) + self.get(url) + self.wait_until_present('#projectbuildstable tbody tr') + return self.find_all('#projectbuildstable tbody tr') + + def test_show_builds_for_project(self): + """ Builds for a project should be displayed in the main table """ + Build.objects.create(**self.project1_build_success) + Build.objects.create(**self.project1_build_success) + build_rows = self._get_rows_for_project(self.project1.id) + self.assertEqual(len(build_rows), 2) + + def test_show_builds_project_only(self): + """ Builds for other projects should be excluded """ + Build.objects.create(**self.project1_build_success) + Build.objects.create(**self.project1_build_success) + Build.objects.create(**self.project1_build_success) + + # shouldn't see these two + Build.objects.create(**self.project2_build_success) + Build.objects.create(**self.project2_build_in_progress) + + build_rows = self._get_rows_for_project(self.project1.id) + self.assertEqual(len(build_rows), 3) + + def test_builds_exclude_in_progress(self): + """ "in progress" builds should not be shown in main table """ + Build.objects.create(**self.project1_build_success) + Build.objects.create(**self.project1_build_success) + + # shouldn't see this one + Build.objects.create(**self.project1_build_in_progress) + + # shouldn't see these two either, as they belong to a different project + Build.objects.create(**self.project2_build_success) + Build.objects.create(**self.project2_build_in_progress) + + build_rows = self._get_rows_for_project(self.project1.id) + self.assertEqual(len(build_rows), 2) + + def test_show_tasks_with_suffix(self): + """ Task should be shown as suffixes on build names """ + build = Build.objects.create(**self.project1_build_success) + target = 'bash' + task = 'clean' + Target.objects.create(build=build, target=target, task=task) + + url = reverse('projectbuilds', args=(self.project1.id,)) + self.get(url) + self.wait_until_present('td[class="target"]') + + cell = self.find('td[class="target"]') + content = cell.get_attribute('innerHTML') + expected_text = '%s:%s' % (target, task) + + self.assertTrue(re.search(expected_text, content), + '"target" cell should contain text %s' % expected_text) + + def test_cli_builds_hides_tabs(self): + """ + Display for command line builds should hide tabs + """ + url = reverse('projectbuilds', args=(self.default_project.id,)) + self.get(url) + tabs = self.find_all('#project-topbar') + self.assertEqual(len(tabs), 0, + 'should be no top bar shown for command line builds') + + def test_non_cli_builds_has_tabs(self): + """ + Non-command-line builds projects should show the tabs + """ + url = reverse('projectbuilds', args=(self.project1.id,)) + self.get(url) + tabs = self.find_all('#project-topbar') + self.assertEqual(len(tabs), 1, + 'should be a top bar shown for non-command-line builds') diff --git a/poky/bitbake/lib/toaster/tests/browser/test_project_config_page.py b/poky/bitbake/lib/toaster/tests/browser/test_project_config_page.py new file mode 100644 index 000000000..071008499 --- /dev/null +++ b/poky/bitbake/lib/toaster/tests/browser/test_project_config_page.py @@ -0,0 +1,231 @@ +#! /usr/bin/env python +# ex:ts=4:sw=4:sts=4:et +# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- +# +# BitBake Toaster Implementation +# +# Copyright (C) 2013-2016 Intel Corporation +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import re + +from django.core.urlresolvers import reverse +from django.utils import timezone +from tests.browser.selenium_helpers import SeleniumTestCase + +from orm.models import BitbakeVersion, Release, Project, ProjectVariable + +class TestProjectConfigsPage(SeleniumTestCase): + """ Test data at /project/X/builds is displayed correctly """ + + PROJECT_NAME = 'test project' + INVALID_PATH_START_TEXT = 'The directory path should either start with a /' + INVALID_PATH_CHAR_TEXT = 'The directory path cannot include spaces or ' \ + 'any of these characters' + + def setUp(self): + bbv = BitbakeVersion.objects.create(name='bbv1', giturl='/tmp/', + branch='master', dirpath='') + release = Release.objects.create(name='release1', + bitbake_version=bbv) + self.project1 = Project.objects.create_project(name=self.PROJECT_NAME, + release=release) + self.project1.save() + + + def test_no_underscore_iamgefs_type(self): + """ + Should not accept IMAGEFS_TYPE with an underscore + """ + + imagefs_type = "foo_bar" + + ProjectVariable.objects.get_or_create(project = self.project1, name = "IMAGE_FSTYPES", value = "abcd ") + url = reverse('projectconf', args=(self.project1.id,)); + self.get(url); + + self.click('#change-image_fstypes-icon') + + self.enter_text('#new-imagefs_types', imagefs_type) + + element = self.wait_until_visible('#hintError-image-fs_type') + + self.assertTrue(("A valid image type cannot include underscores" in element.text), + "Did not find underscore error message") + + + def test_checkbox_verification(self): + """ + Should automatically check the checkbox if user enters value + text box, if value is there in the checkbox. + """ + imagefs_type = "btrfs" + + ProjectVariable.objects.get_or_create(project = self.project1, name = "IMAGE_FSTYPES", value = "abcd ") + url = reverse('projectconf', args=(self.project1.id,)); + self.get(url); + + self.click('#change-image_fstypes-icon') + + self.enter_text('#new-imagefs_types', imagefs_type) + + checkboxes = self.driver.find_elements_by_xpath("//input[@class='fs-checkbox-fstypes']") + + for checkbox in checkboxes: + if checkbox.get_attribute("value") == "btrfs": + self.assertEqual(checkbox.is_selected(), True) + + + def test_textbox_with_checkbox_verification(self): + """ + Should automatically add or remove value in textbox, if user checks + or unchecks checkboxes. + """ + + ProjectVariable.objects.get_or_create(project = self.project1, name = "IMAGE_FSTYPES", value = "abcd ") + url = reverse('projectconf', args=(self.project1.id,)); + self.get(url); + + self.click('#change-image_fstypes-icon') + + self.wait_until_visible('#new-imagefs_types') + + checkboxes_selector = '.fs-checkbox-fstypes' + + self.wait_until_visible(checkboxes_selector) + checkboxes = self.find_all(checkboxes_selector) + + for checkbox in checkboxes: + if checkbox.get_attribute("value") == "cpio": + checkbox.click() + element = self.driver.find_element_by_id('new-imagefs_types') + + self.wait_until_visible('#new-imagefs_types') + + self.assertTrue(("cpio" in element.get_attribute('value'), + "Imagefs not added into the textbox")) + checkbox.click() + self.assertTrue(("cpio" not in element.text), + "Image still present in the textbox") + + def test_set_download_dir(self): + """ + Validate the allowed and disallowed types in the directory field for + DL_DIR + """ + + ProjectVariable.objects.get_or_create(project=self.project1, + name='DL_DIR') + url = reverse('projectconf', args=(self.project1.id,)) + self.get(url) + + # activate the input to edit download dir + self.click('#change-dl_dir-icon') + self.wait_until_visible('#new-dl_dir') + + # downloads dir path doesn't start with / or ${...} + self.enter_text('#new-dl_dir', 'home/foo') + element = self.wait_until_visible('#hintError-initialChar-dl_dir') + + msg = 'downloads directory path starts with invalid character but ' \ + 'treated as valid' + self.assertTrue((self.INVALID_PATH_START_TEXT in element.text), msg) + + # downloads dir path has a space + self.driver.find_element_by_id('new-dl_dir').clear() + self.enter_text('#new-dl_dir', '/foo/bar a') + + element = self.wait_until_visible('#hintError-dl_dir') + msg = 'downloads directory path characters invalid but treated as valid' + self.assertTrue((self.INVALID_PATH_CHAR_TEXT in element.text), msg) + + # downloads dir path starts with ${...} but has a space + self.driver.find_element_by_id('new-dl_dir').clear() + self.enter_text('#new-dl_dir', '${TOPDIR}/down foo') + + element = self.wait_until_visible('#hintError-dl_dir') + msg = 'downloads directory path characters invalid but treated as valid' + self.assertTrue((self.INVALID_PATH_CHAR_TEXT in element.text), msg) + + # downloads dir path starts with / + self.driver.find_element_by_id('new-dl_dir').clear() + self.enter_text('#new-dl_dir', '/bar/foo') + + hidden_element = self.driver.find_element_by_id('hintError-dl_dir') + self.assertEqual(hidden_element.is_displayed(), False, + 'downloads directory path valid but treated as invalid') + + # downloads dir path starts with ${...} + self.driver.find_element_by_id('new-dl_dir').clear() + self.enter_text('#new-dl_dir', '${TOPDIR}/down') + + hidden_element = self.driver.find_element_by_id('hintError-dl_dir') + self.assertEqual(hidden_element.is_displayed(), False, + 'downloads directory path valid but treated as invalid') + + def test_set_sstate_dir(self): + """ + Validate the allowed and disallowed types in the directory field for + SSTATE_DIR + """ + + ProjectVariable.objects.get_or_create(project=self.project1, + name='SSTATE_DIR') + url = reverse('projectconf', args=(self.project1.id,)) + self.get(url) + + self.click('#change-sstate_dir-icon') + + self.wait_until_visible('#new-sstate_dir') + + # path doesn't start with / or ${...} + self.enter_text('#new-sstate_dir', 'home/foo') + element = self.wait_until_visible('#hintError-initialChar-sstate_dir') + + msg = 'sstate directory path starts with invalid character but ' \ + 'treated as valid' + self.assertTrue((self.INVALID_PATH_START_TEXT in element.text), msg) + + # path has a space + self.driver.find_element_by_id('new-sstate_dir').clear() + self.enter_text('#new-sstate_dir', '/foo/bar a') + + element = self.wait_until_visible('#hintError-sstate_dir') + msg = 'sstate directory path characters invalid but treated as valid' + self.assertTrue((self.INVALID_PATH_CHAR_TEXT in element.text), msg) + + # path starts with ${...} but has a space + self.driver.find_element_by_id('new-sstate_dir').clear() + self.enter_text('#new-sstate_dir', '${TOPDIR}/down foo') + + element = self.wait_until_visible('#hintError-sstate_dir') + msg = 'sstate directory path characters invalid but treated as valid' + self.assertTrue((self.INVALID_PATH_CHAR_TEXT in element.text), msg) + + # path starts with / + self.driver.find_element_by_id('new-sstate_dir').clear() + self.enter_text('#new-sstate_dir', '/bar/foo') + + hidden_element = self.driver.find_element_by_id('hintError-sstate_dir') + self.assertEqual(hidden_element.is_displayed(), False, + 'sstate directory path valid but treated as invalid') + + # paths starts with ${...} + self.driver.find_element_by_id('new-sstate_dir').clear() + self.enter_text('#new-sstate_dir', '${TOPDIR}/down') + + hidden_element = self.driver.find_element_by_id('hintError-sstate_dir') + self.assertEqual(hidden_element.is_displayed(), False, + 'sstate directory path valid but treated as invalid')
\ No newline at end of file diff --git a/poky/bitbake/lib/toaster/tests/browser/test_project_page.py b/poky/bitbake/lib/toaster/tests/browser/test_project_page.py new file mode 100644 index 000000000..018646332 --- /dev/null +++ b/poky/bitbake/lib/toaster/tests/browser/test_project_page.py @@ -0,0 +1,59 @@ +#! /usr/bin/env python +# ex:ts=4:sw=4:sts=4:et +# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- +# +# BitBake Toaster Implementation +# +# Copyright (C) 2013-2016 Intel Corporation +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +from django.core.urlresolvers import reverse +from django.utils import timezone +from tests.browser.selenium_helpers import SeleniumTestCase + +from orm.models import Build, Project + +class TestProjectPage(SeleniumTestCase): + """ Test project data at /project/X/ is displayed correctly """ + + CLI_BUILDS_PROJECT_NAME = 'Command line builds' + + def test_cli_builds_in_progress(self): + """ + In progress builds should not cause an error to be thrown + when navigating to "command line builds" project page; + see https://bugzilla.yoctoproject.org/show_bug.cgi?id=8277 + """ + + # add the "command line builds" default project; this mirrors what + # we do with get_or_create_default_project() + default_project = Project.objects.create_project(self.CLI_BUILDS_PROJECT_NAME, None) + default_project.is_default = True + default_project.save() + + # add an "in progress" build for the default project + now = timezone.now() + Build.objects.create(project=default_project, + started_on=now, + completed_on=now, + outcome=Build.IN_PROGRESS) + + # navigate to the project page for the default project + url = reverse("project", args=(default_project.id,)) + self.get(url) + + # check that we get a project page with the correct heading + project_name = self.find('.project-name').text.strip() + self.assertEqual(project_name, self.CLI_BUILDS_PROJECT_NAME) diff --git a/poky/bitbake/lib/toaster/tests/browser/test_sample.py b/poky/bitbake/lib/toaster/tests/browser/test_sample.py new file mode 100644 index 000000000..20ec53c28 --- /dev/null +++ b/poky/bitbake/lib/toaster/tests/browser/test_sample.py @@ -0,0 +1,41 @@ +#! /usr/bin/env python +# ex:ts=4:sw=4:sts=4:et +# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- +# +# BitBake Toaster Implementation +# +# Copyright (C) 2013-2016 Intel Corporation +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +""" +A small example test demonstrating the basics of writing a test with +Toaster's SeleniumTestCase; this just fetches the Toaster home page +and checks it has the word "Toaster" in the brand link + +New test files should follow this structure, should be named "test_*.py", +and should be in the same directory as this sample. +""" + +from django.core.urlresolvers import reverse +from tests.browser.selenium_helpers import SeleniumTestCase + +class TestSample(SeleniumTestCase): + """ Test landing page shows the Toaster brand """ + + def test_landing_page_has_brand(self): + url = reverse('landing') + self.get(url) + brand_link = self.find('.toaster-navbar-brand a.brand') + self.assertEqual(brand_link.text.strip(), 'Toaster') diff --git a/poky/bitbake/lib/toaster/tests/browser/test_task_page.py b/poky/bitbake/lib/toaster/tests/browser/test_task_page.py new file mode 100644 index 000000000..690d116cb --- /dev/null +++ b/poky/bitbake/lib/toaster/tests/browser/test_task_page.py @@ -0,0 +1,76 @@ +#! /usr/bin/env python +# ex:ts=4:sw=4:sts=4:et +# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- +# +# BitBake Toaster Implementation +# +# Copyright (C) 2013-2016 Intel Corporation +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +from django.core.urlresolvers import reverse +from django.utils import timezone +from tests.browser.selenium_helpers import SeleniumTestCase +from orm.models import Project, Build, Layer, Layer_Version, Recipe, Target +from orm.models import Task, Task_Dependency + +class TestTaskPage(SeleniumTestCase): + """ Test page which shows an individual task """ + RECIPE_NAME = 'bar' + RECIPE_VERSION = '0.1' + TASK_NAME = 'do_da_doo_ron_ron' + + def setUp(self): + now = timezone.now() + + project = Project.objects.get_or_create_default_project() + + self.build = Build.objects.create(project=project, started_on=now, + completed_on=now) + + Target.objects.create(target='foo', build=self.build) + + layer = Layer.objects.create() + + layer_version = Layer_Version.objects.create(layer=layer) + + recipe = Recipe.objects.create(name=TestTaskPage.RECIPE_NAME, + layer_version=layer_version, version=TestTaskPage.RECIPE_VERSION) + + self.task = Task.objects.create(build=self.build, recipe=recipe, + order=1, outcome=Task.OUTCOME_COVERED, task_executed=False, + task_name=TestTaskPage.TASK_NAME) + + def test_covered_task(self): + """ + Check that covered tasks are displayed for tasks which have + dependencies on themselves + """ + + # the infinite loop which of bug 9952 was down to tasks which + # depend on themselves, so add self-dependent tasks to replicate the + # situation which caused the infinite loop (now fixed) + Task_Dependency.objects.create(task=self.task, depends_on=self.task) + + url = reverse('task', args=(self.build.id, self.task.id,)) + self.get(url) + + # check that we see the task name + self.wait_until_visible('.page-header h1') + + heading = self.find('.page-header h1') + expected_heading = '%s_%s %s' % (TestTaskPage.RECIPE_NAME, + TestTaskPage.RECIPE_VERSION, TestTaskPage.TASK_NAME) + self.assertEqual(heading.text, expected_heading, + 'Heading should show recipe name, version and task') diff --git a/poky/bitbake/lib/toaster/tests/browser/test_toastertable_ui.py b/poky/bitbake/lib/toaster/tests/browser/test_toastertable_ui.py new file mode 100644 index 000000000..53ddf30c3 --- /dev/null +++ b/poky/bitbake/lib/toaster/tests/browser/test_toastertable_ui.py @@ -0,0 +1,160 @@ +#! /usr/bin/env python +# ex:ts=4:sw=4:sts=4:et +# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- +# +# BitBake Toaster Implementation +# +# Copyright (C) 2013-2016 Intel Corporation +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +from datetime import datetime + +from django.core.urlresolvers import reverse +from django.utils import timezone +from tests.browser.selenium_helpers import SeleniumTestCase +from orm.models import BitbakeVersion, Release, Project, Build + +class TestToasterTableUI(SeleniumTestCase): + """ + Tests for the UI elements of ToasterTable (sorting etc.); + note that the tests cover generic functionality of ToasterTable which + manifests as UI elements in the browser, and can only be tested via + Selenium. + """ + + def setUp(self): + pass + + def _get_orderby_heading(self, table): + """ + Get the current order by finding the column heading in <table> with + the sorted class on it. + + table: WebElement for a ToasterTable + """ + selector = 'thead a.sorted' + heading = table.find_element_by_css_selector(selector) + return heading.get_attribute('innerHTML').strip() + + def _get_datetime_from_cell(self, row, selector): + """ + Return the value in the cell selected by <selector> on <row> as a + datetime. + + row: <tr> WebElement for a row in the ToasterTable + selector: CSS selector to use to find the cell containing the date time + string + """ + cell = row.find_element_by_css_selector(selector) + cell_text = cell.get_attribute('innerHTML').strip() + return datetime.strptime(cell_text, '%d/%m/%y %H:%M') + + def test_revert_orderby(self): + """ + Test that sort order for a table reverts to the default sort order + if the current sort column is hidden. + """ + now = timezone.now() + later = now + timezone.timedelta(hours=1) + even_later = later + timezone.timedelta(hours=1) + + bbv = BitbakeVersion.objects.create(name='test bbv', giturl='/tmp/', + branch='master', dirpath='') + release = Release.objects.create(name='test release', + branch_name='master', + bitbake_version=bbv) + + project = Project.objects.create_project('project', release) + + # set up two builds which will order differently when sorted by + # started_on or completed_on + + # started first, finished last + build1 = Build.objects.create(project=project, + started_on=now, + completed_on=even_later, + outcome=Build.SUCCEEDED) + + # started second, finished first + build2 = Build.objects.create(project=project, + started_on=later, + completed_on=later, + outcome=Build.SUCCEEDED) + + url = reverse('all-builds') + self.get(url) + table = self.wait_until_visible('#allbuildstable') + + # check ordering (default is by -completed_on); so build1 should be + # first as it finished last + active_heading = self._get_orderby_heading(table) + self.assertEqual(active_heading, 'Completed on', + 'table should be sorted by "Completed on" by default') + + row_selector = '#allbuildstable tbody tr' + cell_selector = 'td.completed_on' + + rows = self.find_all(row_selector) + row1_completed_on = self._get_datetime_from_cell(rows[0], cell_selector) + row2_completed_on = self._get_datetime_from_cell(rows[1], cell_selector) + self.assertTrue(row1_completed_on > row2_completed_on, + 'table should be sorted by -completed_on') + + # turn on started_on column + self.click('#edit-columns-button') + self.click('#checkbox-started_on') + + # sort by started_on column + links = table.find_elements_by_css_selector('th.started_on a') + for link in links: + if link.get_attribute('innerHTML').strip() == 'Started on': + link.click() + break + + # wait for table data to reload in response to new sort + self.wait_until_visible('#allbuildstable') + + # check ordering; build1 should be first + active_heading = self._get_orderby_heading(table) + self.assertEqual(active_heading, 'Started on', + 'table should be sorted by "Started on"') + + cell_selector = 'td.started_on' + + rows = self.find_all(row_selector) + row1_started_on = self._get_datetime_from_cell(rows[0], cell_selector) + row2_started_on = self._get_datetime_from_cell(rows[1], cell_selector) + self.assertTrue(row1_started_on < row2_started_on, + 'table should be sorted by started_on') + + # turn off started_on column + self.click('#edit-columns-button') + self.click('#checkbox-started_on') + + # wait for table data to reload in response to new sort + self.wait_until_visible('#allbuildstable') + + # check ordering (should revert to completed_on); build2 should be first + active_heading = self._get_orderby_heading(table) + self.assertEqual(active_heading, 'Completed on', + 'table should be sorted by "Completed on" after hiding sort column') + + cell_selector = 'td.completed_on' + + rows = self.find_all(row_selector) + row1_completed_on = self._get_datetime_from_cell(rows[0], cell_selector) + row2_completed_on = self._get_datetime_from_cell(rows[1], cell_selector) + self.assertTrue(row1_completed_on > row2_completed_on, + 'table should be sorted by -completed_on') diff --git a/poky/bitbake/lib/toaster/tests/builds/README b/poky/bitbake/lib/toaster/tests/builds/README new file mode 100644 index 000000000..4a3b5328b --- /dev/null +++ b/poky/bitbake/lib/toaster/tests/builds/README @@ -0,0 +1,14 @@ +# Running build tests + +These tests are to test the running of builds and the data produced by the builds. +Your oe build environment must be sourced/initialised for these tests to run. + +The simplest way to run the tests are the following commands: + +$ . oe-init-build-env +$ cd bitbake/lib/toaster/ # path my vary but this is into toaster's directory +$ DJANGO_SETTINGS_MODULE='toastermain.settings_test' ./manage.py test tests.builds + +Optional environment variables: + - TOASTER_DIR (where toaster keeps it's artifacts) + - TOASTER_CONF a path to the toasterconf.json file. This will need to be set if you don't execute the tests from toaster's own directory. diff --git a/poky/bitbake/lib/toaster/tests/builds/__init__.py b/poky/bitbake/lib/toaster/tests/builds/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/poky/bitbake/lib/toaster/tests/builds/__init__.py diff --git a/poky/bitbake/lib/toaster/tests/builds/buildtest.py b/poky/bitbake/lib/toaster/tests/builds/buildtest.py new file mode 100644 index 000000000..5a56a110a --- /dev/null +++ b/poky/bitbake/lib/toaster/tests/builds/buildtest.py @@ -0,0 +1,169 @@ +#! /usr/bin/env python +# ex:ts=4:sw=4:sts=4:et +# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- +# +# BitBake Toaster Implementation +# +# Copyright (C) 2016 Intel Corporation +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import os +import sys +import time +import unittest + +from orm.models import Project, Release, ProjectTarget, Build, ProjectVariable +from bldcontrol.models import BuildEnvironment + +from bldcontrol.management.commands.runbuilds import Command\ + as RunBuildsCommand + +from django.core.management import call_command + +import subprocess +import logging + +logger = logging.getLogger("toaster") + +# We use unittest.TestCase instead of django.test.TestCase because we don't +# want to wrap everything in a database transaction as an external process +# (bitbake needs access to the database) + +def load_build_environment(): + call_command('loaddata', 'settings.xml', app_label="orm") + call_command('loaddata', 'poky.xml', app_label="orm") + + current_builddir = os.environ.get("BUILDDIR") + if current_builddir: + BuildTest.BUILDDIR = current_builddir + else: + # Setup a builddir based on default layout + # bitbake inside openebedded-core + oe_init_build_env_path = os.path.join( + os.path.dirname(os.path.abspath(__file__)), + os.pardir, + os.pardir, + os.pardir, + os.pardir, + os.pardir, + 'oe-init-build-env' + ) + if not os.path.exists(oe_init_build_env_path): + raise Exception("We had no BUILDDIR set and couldn't " + "find oe-init-build-env to set this up " + "ourselves please run oe-init-build-env " + "before running these tests") + + oe_init_build_env_path = os.path.realpath(oe_init_build_env_path) + cmd = "bash -c 'source oe-init-build-env %s'" % BuildTest.BUILDDIR + p = subprocess.Popen( + cmd, + cwd=os.path.dirname(oe_init_build_env_path), + shell=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + + output, err = p.communicate() + p.wait() + + logger.info("oe-init-build-env %s %s" % (output, err)) + + os.environ['BUILDDIR'] = BuildTest.BUILDDIR + + # Setup the path to bitbake we know where to find this + bitbake_path = os.path.join( + os.path.dirname(os.path.abspath(__file__)), + os.pardir, + os.pardir, + os.pardir, + os.pardir, + 'bin', + 'bitbake') + if not os.path.exists(bitbake_path): + raise Exception("Could not find bitbake at the expected path %s" + % bitbake_path) + + os.environ['BBBASEDIR'] = bitbake_path + +class BuildTest(unittest.TestCase): + + PROJECT_NAME = "Testbuild" + BUILDDIR = "/tmp/build/" + + def build(self, target): + # So that the buildinfo helper uses the test database' + self.assertEqual( + os.environ.get('DJANGO_SETTINGS_MODULE', ''), + 'toastermain.settings_test', + "Please initialise django with the tests settings: " + "DJANGO_SETTINGS_MODULE='toastermain.settings_test'") + + built = self.target_already_built(target) + if built: + return built + + load_build_environment() + + BuildEnvironment.objects.get_or_create( + betype=BuildEnvironment.TYPE_LOCAL, + sourcedir=BuildTest.BUILDDIR, + builddir=BuildTest.BUILDDIR + ) + + release = Release.objects.get(name='local') + + # Create a project for this build to run in + project = Project.objects.create_project(name=BuildTest.PROJECT_NAME, + release=release) + + if os.environ.get("TOASTER_TEST_USE_SSTATE_MIRROR"): + ProjectVariable.objects.get_or_create( + name="SSTATE_MIRRORS", + value="file://.* http://autobuilder.yoctoproject.org/pub/sstate/PATH;downloadfilename=PATH", + project=project) + + ProjectTarget.objects.create(project=project, + target=target, + task="") + build_request = project.schedule_build() + + # run runbuilds command to dispatch the build + # e.g. manage.py runubilds + RunBuildsCommand().runbuild() + + build_pk = build_request.build.pk + while Build.objects.get(pk=build_pk).outcome == Build.IN_PROGRESS: + sys.stdout.write("\rBuilding %s %d%%" % + (target, + build_request.build.completeper())) + sys.stdout.flush() + time.sleep(1) + + self.assertEqual(Build.objects.get(pk=build_pk).outcome, + Build.SUCCEEDED, + "Build did not SUCCEEDED") + + logger.info("\nBuild finished %s" % build_request.build.outcome) + return build_request.build + + def target_already_built(self, target): + """ If the target is already built no need to build it again""" + for build in Build.objects.filter( + project__name=BuildTest.PROJECT_NAME): + targets = build.target_set.values_list('target', flat=True) + if target in targets: + return build + + return None diff --git a/poky/bitbake/lib/toaster/tests/builds/test_core_image_min.py b/poky/bitbake/lib/toaster/tests/builds/test_core_image_min.py new file mode 100644 index 000000000..586f4a8f7 --- /dev/null +++ b/poky/bitbake/lib/toaster/tests/builds/test_core_image_min.py @@ -0,0 +1,386 @@ +#! /usr/bin/env python +# ex:ts=4:sw=4:sts=4:et +# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- +# +# BitBake Toaster Implementation +# +# Copyright (C) 2016 Intel Corporation +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +# Tests were part of openembedded-core oe selftest Authored by: Lucian Musat +# Ionut Chisanovici, Paul Eggleton and Cristian Iorga + +import os + +from django.db.models import Q + +from orm.models import Target_Image_File, Target_Installed_Package, Task +from orm.models import Package_Dependency, Recipe_Dependency, Build +from orm.models import Task_Dependency, Package, Target, Recipe +from orm.models import CustomImagePackage + +from tests.builds.buildtest import BuildTest + + +class BuildCoreImageMinimal(BuildTest): + """Build core-image-minimal and test the results""" + + def setUp(self): + self.completed_build = self.build("core-image-minimal") + + # Check if build name is unique - tc_id=795 + def test_Build_Unique_Name(self): + all_builds = Build.objects.all().count() + distinct_builds = Build.objects.values('id').distinct().count() + self.assertEqual(distinct_builds, + all_builds, + msg='Build name is not unique') + + # Check if build cooker log path is unique - tc_id=819 + def test_Build_Unique_Cooker_Log_Path(self): + distinct_path = Build.objects.values( + 'cooker_log_path').distinct().count() + total_builds = Build.objects.values('id').count() + self.assertEqual(distinct_path, + total_builds, + msg='Build cooker log path is not unique') + + # Check if task order is unique for one build - tc=824 + def test_Task_Unique_Order(self): + total_task_order = Task.objects.filter( + build=self.built).values('order').count() + distinct_task_order = Task.objects.filter( + build=self.completed_build).values('order').distinct().count() + + self.assertEqual(total_task_order, + distinct_task_order, + msg='Errors task order is not unique') + + # Check task order sequence for one build - tc=825 + def test_Task_Order_Sequence(self): + cnt_err = [] + tasks = Task.objects.filter( + Q(build=self.completed_build), + ~Q(order=None), + ~Q(task_name__contains='_setscene') + ).values('id', 'order').order_by("order") + + cnt_tasks = 0 + for task in tasks: + cnt_tasks += 1 + if (task['order'] != cnt_tasks): + cnt_err.append(task['id']) + self.assertEqual( + len(cnt_err), 0, msg='Errors for task id: %s' % cnt_err) + + # Check if disk_io matches the difference between EndTimeIO and + # StartTimeIO in build stats - tc=828 + # def test_Task_Disk_IO_TC828(self): + + # Check if outcome = 2 (SSTATE) then sstate_result must be 3 (RESTORED) - + # tc=832 + def test_Task_If_Outcome_2_Sstate_Result_Must_Be_3(self): + tasks = Task.objects.filter(outcome=2).values('id', 'sstate_result') + cnt_err = [] + for task in tasks: + if (task['sstate_result'] != 3): + cnt_err.append(task['id']) + + self.assertEqual(len(cnt_err), + 0, + msg='Errors for task id: %s' % cnt_err) + + # Check if outcome = 1 (COVERED) or 3 (EXISTING) then sstate_result must + # be 0 (SSTATE_NA) - tc=833 + def test_Task_If_Outcome_1_3_Sstate_Result_Must_Be_0(self): + tasks = Task.objects.filter( + outcome__in=(Task.OUTCOME_COVERED, + Task.OUTCOME_PREBUILT)).values('id', + 'task_name', + 'sstate_result') + cnt_err = [] + + for task in tasks: + if (task['sstate_result'] != Task.SSTATE_NA and + task['sstate_result'] != Task.SSTATE_MISS): + cnt_err.append({'id': task['id'], + 'name': task['task_name'], + 'sstate_result': task['sstate_result']}) + + self.assertEqual(len(cnt_err), + 0, + msg='Errors for task id: %s' % cnt_err) + + # Check if outcome is 0 (SUCCESS) or 4 (FAILED) then sstate_result must be + # 0 (NA), 1 (MISS) or 2 (FAILED) - tc=834 + def test_Task_If_Outcome_0_4_Sstate_Result_Must_Be_0_1_2(self): + tasks = Task.objects.filter( + outcome__in=(0, 4)).values('id', 'sstate_result') + cnt_err = [] + + for task in tasks: + if (task['sstate_result'] not in [0, 1, 2]): + cnt_err.append(task['id']) + + self.assertEqual(len(cnt_err), + 0, + msg='Errors for task id: %s' % cnt_err) + + # Check if task_executed = TRUE (1), script_type must be 0 (CODING_NA), 2 + # (CODING_PYTHON), 3 (CODING_SHELL) - tc=891 + def test_Task_If_Task_Executed_True_Script_Type_0_2_3(self): + tasks = Task.objects.filter( + task_executed=1).values('id', 'script_type') + cnt_err = [] + + for task in tasks: + if (task['script_type'] not in [0, 2, 3]): + cnt_err.append(task['id']) + self.assertEqual(len(cnt_err), + 0, + msg='Errors for task id: %s' % cnt_err) + + # Check if task_executed = TRUE (1), outcome must be 0 (SUCCESS) or 4 + # (FAILED) - tc=836 + def test_Task_If_Task_Executed_True_Outcome_0_4(self): + tasks = Task.objects.filter(task_executed=1).values('id', 'outcome') + cnt_err = [] + + for task in tasks: + if (task['outcome'] not in [0, 4]): + cnt_err.append(task['id']) + + self.assertEqual(len(cnt_err), + 0, + msg='Errors for task id: %s' % cnt_err) + + # Check if task_executed = FALSE (0), script_type must be 0 - tc=890 + def test_Task_If_Task_Executed_False_Script_Type_0(self): + tasks = Task.objects.filter( + task_executed=0).values('id', 'script_type') + cnt_err = [] + + for task in tasks: + if (task['script_type'] != 0): + cnt_err.append(task['id']) + + self.assertEqual(len(cnt_err), + 0, + msg='Errors for task id: %s' % cnt_err) + + # Check if task_executed = FALSE (0) and build outcome = SUCCEEDED (0), + # task outcome must be 1 (COVERED), 2 (CACHED), 3 (PREBUILT), 5 (EMPTY) - + # tc=837 + def test_Task_If_Task_Executed_False_Outcome_1_2_3_5(self): + builds = Build.objects.filter(outcome=0).values('id') + cnt_err = [] + for build in builds: + tasks = Task.objects.filter( + build=build['id'], task_executed=0).values('id', 'outcome') + for task in tasks: + if (task['outcome'] not in [1, 2, 3, 5]): + cnt_err.append(task['id']) + + self.assertEqual(len(cnt_err), + 0, + msg='Errors for task id: %s' % cnt_err) + + # Key verification - tc=888 + def test_Target_Installed_Package(self): + rows = Target_Installed_Package.objects.values('id', + 'target_id', + 'package_id') + cnt_err = [] + + for row in rows: + target = Target.objects.filter(id=row['target_id']).values('id') + package = Package.objects.filter(id=row['package_id']).values('id') + if (not target or not package): + cnt_err.append(row['id']) + self.assertEqual(len(cnt_err), + 0, + msg='Errors for target installed package id: %s' % + cnt_err) + + # Key verification - tc=889 + def test_Task_Dependency(self): + rows = Task_Dependency.objects.values('id', + 'task_id', + 'depends_on_id') + cnt_err = [] + for row in rows: + task_id = Task.objects.filter(id=row['task_id']).values('id') + depends_on_id = Task.objects.filter( + id=row['depends_on_id']).values('id') + if (not task_id or not depends_on_id): + cnt_err.append(row['id']) + self.assertEqual(len(cnt_err), + 0, + msg='Errors for task dependency id: %s' % cnt_err) + + # Check if build target file_name is populated only if is_image=true AND + # orm_build.outcome=0 then if the file exists and its size matches + # the file_size value. Need to add the tc in the test run + def test_Target_File_Name_Populated(self): + builds = Build.objects.filter(outcome=0).values('id') + for build in builds: + targets = Target.objects.filter( + build_id=build['id'], is_image=1).values('id') + for target in targets: + target_files = Target_Image_File.objects.filter( + target_id=target['id']).values('id', + 'file_name', + 'file_size') + cnt_err = [] + for file_info in target_files: + target_id = file_info['id'] + target_file_name = file_info['file_name'] + target_file_size = file_info['file_size'] + if (not target_file_name or not target_file_size): + cnt_err.append(target_id) + else: + if (not os.path.exists(target_file_name)): + cnt_err.append(target_id) + else: + if (os.path.getsize(target_file_name) != + target_file_size): + cnt_err.append(target_id) + self.assertEqual(len(cnt_err), 0, + msg='Errors for target image file id: %s' % + cnt_err) + + # Key verification - tc=884 + def test_Package_Dependency(self): + cnt_err = [] + deps = Package_Dependency.objects.values( + 'id', 'package_id', 'depends_on_id') + for dep in deps: + if (dep['package_id'] == dep['depends_on_id']): + cnt_err.append(dep['id']) + self.assertEqual(len(cnt_err), 0, + msg='Errors for package dependency id: %s' % cnt_err) + + # Recipe key verification, recipe name does not depends on a recipe having + # the same name - tc=883 + def test_Recipe_Dependency(self): + deps = Recipe_Dependency.objects.values( + 'id', 'recipe_id', 'depends_on_id') + cnt_err = [] + for dep in deps: + if (not dep['recipe_id'] or not dep['depends_on_id']): + cnt_err.append(dep['id']) + else: + name = Recipe.objects.filter( + id=dep['recipe_id']).values('name') + dep_name = Recipe.objects.filter( + id=dep['depends_on_id']).values('name') + if (name == dep_name): + cnt_err.append(dep['id']) + self.assertEqual(len(cnt_err), 0, + msg='Errors for recipe dependency id: %s' % cnt_err) + + # Check if package name does not start with a number (0-9) - tc=846 + def test_Package_Name_For_Number(self): + packages = Package.objects.filter(~Q(size=-1)).values('id', 'name') + cnt_err = [] + for package in packages: + if (package['name'][0].isdigit() is True): + cnt_err.append(package['id']) + self.assertEqual( + len(cnt_err), 0, msg='Errors for package id: %s' % cnt_err) + + # Check if package version starts with a number (0-9) - tc=847 + def test_Package_Version_Starts_With_Number(self): + packages = Package.objects.filter( + ~Q(size=-1)).values('id', 'version') + cnt_err = [] + for package in packages: + if (package['version'][0].isdigit() is False): + cnt_err.append(package['id']) + self.assertEqual( + len(cnt_err), 0, msg='Errors for package id: %s' % cnt_err) + + # Check if package revision starts with 'r' - tc=848 + def test_Package_Revision_Starts_With_r(self): + packages = Package.objects.filter( + ~Q(size=-1)).values('id', 'revision') + cnt_err = [] + for package in packages: + if (package['revision'][0].startswith("r") is False): + cnt_err.append(package['id']) + self.assertEqual( + len(cnt_err), 0, msg='Errors for package id: %s' % cnt_err) + + # Check the validity of the package build_id + # TC must be added in test run + def test_Package_Build_Id(self): + packages = Package.objects.filter( + ~Q(size=-1)).values('id', 'build_id') + cnt_err = [] + for package in packages: + build_id = Build.objects.filter( + id=package['build_id']).values('id') + if (not build_id): + # They have no build_id but if they are + # CustomImagePackage that's expected + try: + CustomImagePackage.objects.get(pk=package['id']) + except CustomImagePackage.DoesNotExist: + cnt_err.append(package['id']) + + self.assertEqual(len(cnt_err), + 0, + msg="Errors for package id: %s they have no build" + "associated with them" % cnt_err) + + # Check the validity of package recipe_id + # TC must be added in test run + def test_Package_Recipe_Id(self): + packages = Package.objects.filter( + ~Q(size=-1)).values('id', 'recipe_id') + cnt_err = [] + for package in packages: + recipe_id = Recipe.objects.filter( + id=package['recipe_id']).values('id') + if (not recipe_id): + cnt_err.append(package['id']) + self.assertEqual( + len(cnt_err), 0, msg='Errors for package id: %s' % cnt_err) + + # Check if package installed_size field is not null + # TC must be aded in test run + def test_Package_Installed_Size_Not_NULL(self): + packages = Package.objects.filter( + installed_size__isnull=True).values('id') + cnt_err = [] + for package in packages: + cnt_err.append(package['id']) + self.assertEqual( + len(cnt_err), 0, msg='Errors for package id: %s' % cnt_err) + + def test_custom_packages_generated(self): + """Test if there is a corresponding generated CustomImagePackage""" + """ for each of the packages generated""" + missing_packages = [] + + for package in Package.objects.all(): + try: + CustomImagePackage.objects.get(name=package.name) + except CustomImagePackage.DoesNotExist: + missing_packages.append(package.name) + + self.assertEqual(len(missing_packages), 0, + "Some package were created from the build but their" + " corresponding CustomImagePackage was not found") diff --git a/poky/bitbake/lib/toaster/tests/commands/__init__.py b/poky/bitbake/lib/toaster/tests/commands/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/poky/bitbake/lib/toaster/tests/commands/__init__.py diff --git a/poky/bitbake/lib/toaster/tests/commands/test_loaddata.py b/poky/bitbake/lib/toaster/tests/commands/test_loaddata.py new file mode 100644 index 000000000..951f6ff5a --- /dev/null +++ b/poky/bitbake/lib/toaster/tests/commands/test_loaddata.py @@ -0,0 +1,61 @@ +#! /usr/bin/env python +# ex:ts=4:sw=4:sts=4:et +# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- +# +# BitBake Toaster Implementation +# +# Copyright (C) 2016 Intel Corporation +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +from django.test import TestCase +from django.core import management + +from orm.models import Layer_Version, Layer, Release, ToasterSetting + + +class TestLoadDataFixtures(TestCase): + """ Test loading our 3 provided fixtures """ + def test_run_loaddata_poky_command(self): + management.call_command('loaddata', 'poky') + + num_releases = Release.objects.count() + + self.assertTrue( + Layer_Version.objects.filter( + layer__name="meta-poky").count() == num_releases, + "Loaded poky fixture but don't have a meta-poky for all releases" + " defined") + + def test_run_loaddata_oecore_command(self): + management.call_command('loaddata', 'oe-core') + + # We only have the one layer for oe-core setup + self.assertTrue( + Layer.objects.filter(name="openembedded-core").count() > 0, + "Loaded oe-core fixture but still have no openemebedded-core" + " layer") + + def test_run_loaddata_settings_command(self): + management.call_command('loaddata', 'settings') + + self.assertTrue( + ToasterSetting.objects.filter(name="DEFAULT_RELEASE").count() > 0, + "Loaded settings but have no DEFAULT_RELEASE") + + self.assertTrue( + ToasterSetting.objects.filter( + name__startswith="DEFCONF").count() > 0, + "Loaded settings but have no DEFCONF (default project " + "configuration values)") diff --git a/poky/bitbake/lib/toaster/tests/commands/test_lsupdates.py b/poky/bitbake/lib/toaster/tests/commands/test_lsupdates.py new file mode 100644 index 000000000..49897a476 --- /dev/null +++ b/poky/bitbake/lib/toaster/tests/commands/test_lsupdates.py @@ -0,0 +1,45 @@ +#! /usr/bin/env python +# ex:ts=4:sw=4:sts=4:et +# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- +# +# BitBake Toaster Implementation +# +# Copyright (C) 2016 Intel Corporation +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +from django.test import TestCase +from django.core import management + +from orm.models import Layer_Version, Machine, Recipe + + +class TestLayerIndexUpdater(TestCase): + def test_run_lsupdates_command(self): + # Load some release information for us to fetch from the layer index + management.call_command('loaddata', 'poky') + + old_layers_count = Layer_Version.objects.count() + old_recipes_count = Recipe.objects.count() + old_machines_count = Machine.objects.count() + + # Now fetch the metadata from the layer index + management.call_command('lsupdates') + + self.assertTrue(Layer_Version.objects.count() > old_layers_count, + "lsupdates ran but we still have no more layers!") + self.assertTrue(Recipe.objects.count() > old_recipes_count, + "lsupdates ran but we still have no more Recipes!") + self.assertTrue(Machine.objects.count() > old_machines_count, + "lsupdates ran but we still have no more Machines!") diff --git a/poky/bitbake/lib/toaster/tests/commands/test_runbuilds.py b/poky/bitbake/lib/toaster/tests/commands/test_runbuilds.py new file mode 100644 index 000000000..3e634835e --- /dev/null +++ b/poky/bitbake/lib/toaster/tests/commands/test_runbuilds.py @@ -0,0 +1,88 @@ +#! /usr/bin/env python +# ex:ts=4:sw=4:sts=4:et +# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- +# +# BitBake Toaster Implementation +# +# Copyright (C) 2016 Intel Corporation +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import os + +from django.test import TestCase +from django.core import management + +from orm.models import signal_runbuilds + +import threading +import time +import subprocess +import signal + + +class KillRunbuilds(threading.Thread): + """ Kill the runbuilds process after an amount of time """ + def __init__(self, *args, **kwargs): + super(KillRunbuilds, self).__init__(*args, **kwargs) + self.setDaemon(True) + + def run(self): + time.sleep(5) + signal_runbuilds() + time.sleep(1) + + pidfile_path = os.path.join(os.environ.get("BUILDDIR", "."), + ".runbuilds.pid") + + with open(pidfile_path) as pidfile: + pid = pidfile.read() + os.kill(int(pid), signal.SIGTERM) + + +class TestCommands(TestCase): + """ Sanity test that runbuilds executes OK """ + + def setUp(self): + os.environ.setdefault("DJANGO_SETTINGS_MODULE", + "toastermain.settings_test") + os.environ.setdefault("BUILDDIR", + "/tmp/") + + # Setup a real database if needed for runbuilds process + # to connect to + management.call_command('migrate') + + def test_runbuilds_command(self): + kill_runbuilds = KillRunbuilds() + kill_runbuilds.start() + + manage_py = os.path.join( + os.path.dirname(os.path.abspath(__file__)), + os.pardir, + os.pardir, + "manage.py") + + command = "%s runbuilds" % manage_py + + process = subprocess.Popen(command, + shell=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + + (out, err) = process.communicate() + process.wait() + + self.assertNotEqual(process.returncode, 1, + "Runbuilds returned an error %s" % err) diff --git a/poky/bitbake/lib/toaster/tests/db/__init__.py b/poky/bitbake/lib/toaster/tests/db/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/poky/bitbake/lib/toaster/tests/db/__init__.py diff --git a/poky/bitbake/lib/toaster/tests/db/test_db.py b/poky/bitbake/lib/toaster/tests/db/test_db.py new file mode 100644 index 000000000..a0f5f6ec0 --- /dev/null +++ b/poky/bitbake/lib/toaster/tests/db/test_db.py @@ -0,0 +1,55 @@ +# The MIT License (MIT) +# +# Copyright (c) 2016 Damien Lespiau +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import sys + +try: + from StringIO import StringIO +except ImportError: + from io import StringIO + +from contextlib import contextmanager + +from django.core import management +from django.test import TestCase + + +@contextmanager +def capture(command, *args, **kwargs): + out, sys.stdout = sys.stdout, StringIO() + command(*args, **kwargs) + sys.stdout.seek(0) + yield sys.stdout.read() + sys.stdout = out + + +def makemigrations(): + management.call_command('makemigrations') + + +class MigrationTest(TestCase): + + def testPendingMigration(self): + """Make sure there's no pending migration.""" + + with capture(makemigrations) as output: + self.assertEqual(output, "No changes detected\n") diff --git a/poky/bitbake/lib/toaster/tests/eventreplay/README b/poky/bitbake/lib/toaster/tests/eventreplay/README new file mode 100644 index 000000000..8c5bb6432 --- /dev/null +++ b/poky/bitbake/lib/toaster/tests/eventreplay/README @@ -0,0 +1,22 @@ +# Running eventreplay tests + +These tests use event log files produced by bitbake <target> -w <event log file> +You need to have event log files produced before running this tests. + +At the moment of writing this document tests use 2 event log files: zlib.events +and core-image-minimal.events. They're not provided with the tests due to their +significant size. + +Here is how to produce them: + +$ . oe-init-build-env +$ rm -r tmp sstate-cache +$ bitbake core-image-minimal -w core-image-minimal.events +$ rm -rf tmp sstate-cache +$ bitbake zlib -w zlib.events + +After that it should be possible to run eventreplay tests this way: + +$ EVENTREPLAY_DIR=./ DJANGO_SETTINGS_MODULE=toastermain.settings_test ../bitbake/lib/toaster/manage.py test -v2 tests.eventreplay + +Note that environment variable EVENTREPLAY_DIR should point to the directory with event log files. diff --git a/poky/bitbake/lib/toaster/tests/eventreplay/__init__.py b/poky/bitbake/lib/toaster/tests/eventreplay/__init__.py new file mode 100644 index 000000000..695661947 --- /dev/null +++ b/poky/bitbake/lib/toaster/tests/eventreplay/__init__.py @@ -0,0 +1,97 @@ +#! /usr/bin/env python +# ex:ts=4:sw=4:sts=4:et +# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- +# +# BitBake Toaster Implementation +# +# Copyright (C) 2016 Intel Corporation +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +# Tests were part of openembedded-core oe selftest Authored by: Lucian Musat +# Ionut Chisanovici, Paul Eggleton and Cristian Iorga + +""" +Test toaster backend by playing build event log files +using toaster-eventreplay script +""" + +import os + +from subprocess import getstatusoutput +from pathlib import Path + +from django.test import TestCase + +from orm.models import Target_Installed_Package, Package, Build + +class EventReplay(TestCase): + """Base class for eventreplay test cases""" + + def setUp(self): + """ + Setup build environment: + - set self.script to toaster-eventreplay path + - set self.eventplay_dir to the value of EVENTPLAY_DIR env variable + """ + bitbake_dir = Path(__file__.split('lib/toaster')[0]) + self.script = bitbake_dir / 'bin' / 'toaster-eventreplay' + self.assertTrue(self.script.exists(), "%s doesn't exist") + self.eventplay_dir = os.getenv("EVENTREPLAY_DIR") + self.assertTrue(self.eventplay_dir, + "Environment variable EVENTREPLAY_DIR is not set") + + def _replay(self, eventfile): + """Run toaster-eventplay <eventfile>""" + eventpath = Path(self.eventplay_dir) / eventfile + status, output = getstatusoutput('%s %s' % (self.script, eventpath)) + if status: + print(output) + + self.assertEqual(status, 0) + +class CoreImageMinimalEventReplay(EventReplay): + """Replay core-image-minimal events""" + + def test_installed_packages(self): + """Test if all required packages have been installed""" + + self._replay('core-image-minimal.events') + + # test installed packages + packages = sorted(Target_Installed_Package.objects.\ + values_list('package__name', flat=True)) + self.assertEqual(packages, ['base-files', 'base-passwd', 'busybox', + 'busybox-hwclock', 'busybox-syslog', + 'busybox-udhcpc', 'eudev', 'glibc', + 'init-ifupdown', 'initscripts', + 'initscripts-functions', 'kernel-base', + 'kernel-module-uvesafb', 'libkmod', + 'modutils-initscripts', 'netbase', + 'packagegroup-core-boot', 'run-postinsts', + 'sysvinit', 'sysvinit-inittab', + 'sysvinit-pidof', 'udev-cache', + 'update-alternatives-opkg', + 'update-rc.d', 'util-linux-libblkid', + 'util-linux-libuuid', 'v86d', 'zlib']) + +class ZlibEventReplay(EventReplay): + """Replay zlib events""" + + def test_replay_zlib(self): + """Test if zlib build and package are in the database""" + self._replay("zlib.events") + + self.assertEqual(Build.objects.last().target_set.last().target, "zlib") + self.assertTrue('zlib' in Package.objects.values_list('name', flat=True)) diff --git a/poky/bitbake/lib/toaster/tests/functional/README b/poky/bitbake/lib/toaster/tests/functional/README new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/poky/bitbake/lib/toaster/tests/functional/README diff --git a/poky/bitbake/lib/toaster/tests/functional/__init__.py b/poky/bitbake/lib/toaster/tests/functional/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/poky/bitbake/lib/toaster/tests/functional/__init__.py diff --git a/poky/bitbake/lib/toaster/tests/functional/functional_helpers.py b/poky/bitbake/lib/toaster/tests/functional/functional_helpers.py new file mode 100644 index 000000000..486078a61 --- /dev/null +++ b/poky/bitbake/lib/toaster/tests/functional/functional_helpers.py @@ -0,0 +1,122 @@ +#! /usr/bin/env python +# ex:ts=4:sw=4:sts=4:et +# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- +# +# BitBake Toaster functional tests implementation +# +# Copyright (C) 2017 Intel Corporation +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import os +import logging +import subprocess +import signal +import time +import re + +from tests.browser.selenium_helpers_base import SeleniumTestCaseBase +from tests.builds.buildtest import load_build_environment + +logger = logging.getLogger("toaster") + +class SeleniumFunctionalTestCase(SeleniumTestCaseBase): + wait_toaster_time = 5 + + @classmethod + def setUpClass(cls): + # So that the buildinfo helper uses the test database' + if os.environ.get('DJANGO_SETTINGS_MODULE', '') != \ + 'toastermain.settings_test': + raise RuntimeError("Please initialise django with the tests settings: " \ + "DJANGO_SETTINGS_MODULE='toastermain.settings_test'") + + load_build_environment() + + # start toaster + cmd = "bash -c 'source toaster start'" + p = subprocess.Popen( + cmd, + cwd=os.environ.get("BUILDDIR"), + shell=True) + if p.wait() != 0: + raise RuntimeError("Can't initialize toaster") + + super(SeleniumFunctionalTestCase, cls).setUpClass() + cls.live_server_url = 'http://localhost:8000/' + + @classmethod + def tearDownClass(cls): + super(SeleniumFunctionalTestCase, cls).tearDownClass() + + # XXX: source toaster stop gets blocked, to review why? + # from now send SIGTERM by hand + time.sleep(cls.wait_toaster_time) + builddir = os.environ.get("BUILDDIR") + + with open(os.path.join(builddir, '.toastermain.pid'), 'r') as f: + toastermain_pid = int(f.read()) + os.kill(toastermain_pid, signal.SIGTERM) + with open(os.path.join(builddir, '.runbuilds.pid'), 'r') as f: + runbuilds_pid = int(f.read()) + os.kill(runbuilds_pid, signal.SIGTERM) + + + def get_URL(self): + rc=self.get_page_source() + project_url=re.search("(projectPageUrl\s:\s\")(.*)(\",)",rc) + return project_url.group(2) + + + def find_element_by_link_text_in_table(self, table_id, link_text): + """ + Assume there're multiple suitable "find_element_by_link_text". + In this circumstance we need to specify "table". + """ + try: + table_element = self.get_table_element(table_id) + element = table_element.find_element_by_link_text(link_text) + except NoSuchElementException as e: + print('no element found') + raise + return element + + def get_table_element(self, table_id, *coordinate): + if len(coordinate) == 0: +#return whole-table element + element_xpath = "//*[@id='" + table_id + "']" + try: + element = self.driver.find_element_by_xpath(element_xpath) + except NoSuchElementException as e: + raise + return element + row = coordinate[0] + + if len(coordinate) == 1: +#return whole-row element + element_xpath = "//*[@id='" + table_id + "']/tbody/tr[" + str(row) + "]" + try: + element = self.driver.find_element_by_xpath(element_xpath) + except NoSuchElementException as e: + return False + return element +#now we are looking for an element with specified X and Y + column = coordinate[1] + + element_xpath = "//*[@id='" + table_id + "']/tbody/tr[" + str(row) + "]/td[" + str(column) + "]" + try: + element = self.driver.find_element_by_xpath(element_xpath) + except NoSuchElementException as e: + return False + return element diff --git a/poky/bitbake/lib/toaster/tests/functional/test_functional_basic.py b/poky/bitbake/lib/toaster/tests/functional/test_functional_basic.py new file mode 100644 index 000000000..cfa2b0fdf --- /dev/null +++ b/poky/bitbake/lib/toaster/tests/functional/test_functional_basic.py @@ -0,0 +1,243 @@ +#! /usr/bin/env python +# ex:ts=4:sw=4:sts=4:et +# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- +# +# BitBake Toaster functional tests implementation +# +# Copyright (C) 2017 Intel Corporation +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import time +import re +from tests.functional.functional_helpers import SeleniumFunctionalTestCase +from orm.models import Project + +class FuntionalTestBasic(SeleniumFunctionalTestCase): + +# testcase (1514) + def test_create_slenium_project(self): + project_name = 'selenium-project' + self.get('') + self.driver.find_element_by_link_text("To start building, create your first Toaster project").click() + self.driver.find_element_by_id("new-project-name").send_keys(project_name) + self.driver.find_element_by_id('projectversion').click() + self.driver.find_element_by_id("create-project-button").click() + element = self.wait_until_visible('#project-created-notification') + self.assertTrue(self.element_exists('#project-created-notification'),'Project creation notification not shown') + self.assertTrue(project_name in element.text, + "New project name not in new project notification") + self.assertTrue(Project.objects.filter(name=project_name).count(), + "New project not found in database") + + # testcase (1515) + def test_verify_left_bar_menu(self): + self.get('') + self.wait_until_visible('#projectstable') + self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click() + self.assertTrue(self.element_exists('#config-nav'),'Configuration Tab does not exist') + project_URL=self.get_URL() + self.driver.find_element_by_xpath('//a[@href="'+project_URL+'"]').click() + + try: + self.driver.find_element_by_xpath("//*[@id='config-nav']/ul/li/a[@href="+'"'+project_URL+'customimages/"'+"]").click() + self.assertTrue(re.search("Custom images",self.driver.find_element_by_xpath("//div[@class='col-md-10']").text),'Custom images information is not loading properly') + except: + self.fail(msg='No Custom images tab available') + + try: + self.driver.find_element_by_xpath("//*[@id='config-nav']/ul/li/a[@href="+'"'+project_URL+'images/"'+"]").click() + self.assertTrue(re.search("Compatible image recipes",self.driver.find_element_by_xpath("//div[@class='col-md-10']").text),'The Compatible image recipes information is not loading properly') + except: + self.fail(msg='No Compatible image tab available') + + try: + self.driver.find_element_by_xpath("//*[@id='config-nav']/ul/li/a[@href="+'"'+project_URL+'softwarerecipes/"'+"]").click() + self.assertTrue(re.search("Compatible software recipes",self.driver.find_element_by_xpath("//div[@class='col-md-10']").text),'The Compatible software recipe information is not loading properly') + except: + self.fail(msg='No Compatible software recipe tab available') + + try: + self.driver.find_element_by_xpath("//*[@id='config-nav']/ul/li/a[@href="+'"'+project_URL+'machines/"'+"]").click() + self.assertTrue(re.search("Compatible machines",self.driver.find_element_by_xpath("//div[@class='col-md-10']").text),'The Compatible machine information is not loading properly') + except: + self.fail(msg='No Compatible machines tab available') + + try: + self.driver.find_element_by_xpath("//*[@id='config-nav']/ul/li/a[@href="+'"'+project_URL+'layers/"'+"]").click() + self.assertTrue(re.search("Compatible layers",self.driver.find_element_by_xpath("//div[@class='col-md-10']").text),'The Compatible layer information is not loading properly') + except: + self.fail(msg='No Compatible layers tab available') + + try: + self.driver.find_element_by_xpath("//*[@id='config-nav']/ul/li/a[@href="+'"'+project_URL+'configuration"'+"]").click() + self.assertTrue(re.search("Bitbake variables",self.driver.find_element_by_xpath("//div[@class='col-md-10']").text),'The Bitbake variables information is not loading properly') + except: + self.fail(msg='No Bitbake variables tab available') + +# testcase (1516) + def test_review_configuration_information(self): + self.get('') + self.driver.find_element_by_xpath("//div[@id='global-nav']/ul/li/a[@href="+'"'+'/toastergui/projects/'+'"'+"]").click() + self.wait_until_visible('#projectstable') + self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click() + project_URL=self.get_URL() + + try: + self.assertTrue(self.element_exists('#machine-section'),'Machine section for the project configuration page does not exist') + self.assertTrue(re.search("qemux86",self.driver.find_element_by_xpath("//span[@id='project-machine-name']").text),'The machine type is not assigned') + self.driver.find_element_by_xpath("//span[@id='change-machine-toggle']").click() + self.wait_until_visible('#select-machine-form') + self.wait_until_visible('#cancel-machine-change') + self.driver.find_element_by_xpath("//form[@id='select-machine-form']/a[@id='cancel-machine-change']").click() + except: + self.fail(msg='The machine information is wrong in the configuration page') + + try: + self.driver.find_element_by_id('no-most-built') + except: + self.fail(msg='No Most built information in project detail page') + + try: + self.assertTrue(re.search("Yocto Project master",self.driver.find_element_by_xpath("//span[@id='project-release-title']").text),'The project release is not defined') + except: + self.fail(msg='No project release title information in project detail page') + + try: + self.driver.find_element_by_xpath("//div[@id='layer-container']") + self.assertTrue(re.search("3",self.driver.find_element_by_id("project-layers-count").text),'There should be 3 layers listed in the layer count') + layer_list = self.driver.find_element_by_id("layers-in-project-list") + layers = layer_list.find_elements_by_tag_name("li") + for layer in layers: + if re.match ("openembedded-core",layer.text): + print ("openembedded-core layer is a default layer in the project configuration") + elif re.match ("meta-poky",layer.text): + print ("meta-poky layer is a default layer in the project configuration") + elif re.match ("meta-yocto-bsp",layer.text): + print ("meta-yocto-bsp is a default layer in the project configuratoin") + else: + self.fail(msg='default layers are missing from the project configuration') + except: + self.fail(msg='No Layer information in project detail page') + +# testcase (1517) + def test_verify_machine_information(self): + self.get('') + self.driver.find_element_by_xpath("//div[@id='global-nav']/ul/li/a[@href="+'"'+'/toastergui/projects/'+'"'+"]").click() + self.wait_until_visible('#projectstable') + self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click() + + try: + self.assertTrue(self.element_exists('#machine-section'),'Machine section for the project configuration page does not exist') + self.assertTrue(re.search("qemux86",self.driver.find_element_by_id("project-machine-name").text),'The machine type is not assigned') + self.driver.find_element_by_id("change-machine-toggle").click() + self.wait_until_visible('#select-machine-form') + self.wait_until_visible('#cancel-machine-change') + self.driver.find_element_by_id("cancel-machine-change").click() + except: + self.fail(msg='The machine information is wrong in the configuration page') + +# testcase (1518) + def test_verify_most_built_recipes_information(self): + self.get('') + self.driver.find_element_by_xpath("//div[@id='global-nav']/ul/li/a[@href="+'"'+'/toastergui/projects/'+'"'+"]").click() + self.wait_until_visible('#projectstable') + self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click() + project_URL=self.get_URL() + + try: + self.assertTrue(re.search("You haven't built any recipes yet",self.driver.find_element_by_id("no-most-built").text),'Default message of no builds is not present') + self.driver.find_element_by_xpath("//div[@id='no-most-built']/p/a[@href="+'"'+project_URL+'images/"'+"]").click() + self.assertTrue(re.search("Compatible image recipes",self.driver.find_element_by_xpath("//div[@class='col-md-10']").text),'The Choose a recipe to build link is not working properly') + except: + self.fail(msg='No Most built information in project detail page') + +# testcase (1519) + def test_verify_project_release_information(self): + self.get('') + self.driver.find_element_by_xpath("//div[@id='global-nav']/ul/li/a[@href="+'"'+'/toastergui/projects/'+'"'+"]").click() + self.wait_until_visible('#projectstable') + self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click() + + try: + self.assertTrue(re.search("Yocto Project master",self.driver.find_element_by_id("project-release-title").text),'The project release is not defined') + except: + self.fail(msg='No project release title information in project detail page') + +# testcase (1520) + def test_verify_layer_information(self): + self.get('') + self.driver.find_element_by_xpath("//div[@id='global-nav']/ul/li/a[@href="+'"'+'/toastergui/projects/'+'"'+"]").click() + self.wait_until_visible('#projectstable') + self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click() + project_URL=self.get_URL() + + try: + self.driver.find_element_by_xpath("//div[@id='layer-container']") + self.assertTrue(re.search("3",self.driver.find_element_by_id("project-layers-count").text),'There should be 3 layers listed in the layer count') + layer_list = self.driver.find_element_by_id("layers-in-project-list") + layers = layer_list.find_elements_by_tag_name("li") + + for layer in layers: + if re.match ("openembedded-core",layer.text): + print ("openembedded-core layer is a default layer in the project configuration") + elif re.match ("meta-poky",layer.text): + print ("meta-poky layer is a default layer in the project configuration") + elif re.match ("meta-yocto-bsp",layer.text): + print ("meta-yocto-bsp is a default layer in the project configuratoin") + else: + self.fail(msg='default layers are missing from the project configuration') + + self.driver.find_element_by_xpath("//input[@id='layer-add-input']") + self.driver.find_element_by_xpath("//button[@id='add-layer-btn']") + self.driver.find_element_by_xpath("//div[@id='layer-container']/form[@class='form-inline']/p/a[@id='view-compatible-layers']") + self.driver.find_element_by_xpath("//div[@id='layer-container']/form[@class='form-inline']/p/a[@href="+'"'+project_URL+'importlayer"'+"]") + except: + self.fail(msg='No Layer information in project detail page') + +# testcase (1521) + def test_verify_project_detail_links(self): + self.get('') + self.driver.find_element_by_xpath("//div[@id='global-nav']/ul/li/a[@href="+'"'+'/toastergui/projects/'+'"'+"]").click() + self.wait_until_visible('#projectstable') + self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click() + project_URL=self.get_URL() + + self.driver.find_element_by_xpath("//div[@id='project-topbar']/ul[@class='nav nav-tabs']/li[@id='topbar-configuration-tab']/a[@href="+'"'+project_URL+'"'+"]").click() + self.assertTrue(re.search("Configuration",self.driver.find_element_by_xpath("//div[@id='project-topbar']/ul[@class='nav nav-tabs']/li[@id='topbar-configuration-tab']/a[@href="+'"'+project_URL+'"'+"]").text), 'Configuration tab in project topbar is misspelled') + + try: + self.driver.find_element_by_xpath("//div[@id='project-topbar']/ul[@class='nav nav-tabs']/li/a[@href="+'"'+project_URL+'builds/"'+"]").click() + self.assertTrue(re.search("Builds",self.driver.find_element_by_xpath("//div[@id='project-topbar']/ul[@class='nav nav-tabs']/li/a[@href="+'"'+project_URL+'builds/"'+"]").text), 'Builds tab in project topbar is misspelled') + self.driver.find_element_by_xpath("//div[@id='empty-state-projectbuildstable']") + except: + self.fail(msg='Builds tab information is not present') + + try: + self.driver.find_element_by_xpath("//div[@id='project-topbar']/ul[@class='nav nav-tabs']/li/a[@href="+'"'+project_URL+'importlayer"'+"]").click() + self.assertTrue(re.search("Import layer",self.driver.find_element_by_xpath("//div[@id='project-topbar']/ul[@class='nav nav-tabs']/li/a[@href="+'"'+project_URL+'importlayer"'+"]").text), 'Import layer tab in project topbar is misspelled') + self.driver.find_element_by_xpath("//fieldset[@id='repo-select']") + self.driver.find_element_by_xpath("//fieldset[@id='git-repo']") + except: + self.fail(msg='Import layer tab not loading properly') + + try: + self.driver.find_element_by_xpath("//div[@id='project-topbar']/ul[@class='nav nav-tabs']/li/a[@href="+'"'+project_URL+'newcustomimage/"'+"]").click() + self.assertTrue(re.search("New custom image",self.driver.find_element_by_xpath("//div[@id='project-topbar']/ul[@class='nav nav-tabs']/li/a[@href="+'"'+project_URL+'newcustomimage/"'+"]").text), 'New custom image tab in project topbar is misspelled') + self.assertTrue(re.search("Select the image recipe you want to customise",self.driver.find_element_by_xpath("//div[@class='col-md-12']/h2").text),'The new custom image tab is not loading correctly') + except: + self.fail(msg='New custom image tab not loading properly') + + + diff --git a/poky/bitbake/lib/toaster/tests/toaster-tests-requirements.txt b/poky/bitbake/lib/toaster/tests/toaster-tests-requirements.txt new file mode 100644 index 000000000..4f9fcc46d --- /dev/null +++ b/poky/bitbake/lib/toaster/tests/toaster-tests-requirements.txt @@ -0,0 +1 @@ +selenium==2.49.2 diff --git a/poky/bitbake/lib/toaster/tests/views/README b/poky/bitbake/lib/toaster/tests/views/README new file mode 100644 index 000000000..950c7c989 --- /dev/null +++ b/poky/bitbake/lib/toaster/tests/views/README @@ -0,0 +1,4 @@ + +Django unit tests to verify classes and functions based on django Views + +To run just these tests use ./manage.py test tests.views diff --git a/poky/bitbake/lib/toaster/tests/views/__init__.py b/poky/bitbake/lib/toaster/tests/views/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/poky/bitbake/lib/toaster/tests/views/__init__.py diff --git a/poky/bitbake/lib/toaster/tests/views/test_views.py b/poky/bitbake/lib/toaster/tests/views/test_views.py new file mode 100644 index 000000000..1463077e9 --- /dev/null +++ b/poky/bitbake/lib/toaster/tests/views/test_views.py @@ -0,0 +1,540 @@ +#! /usr/bin/env python +# ex:ts=4:sw=4:sts=4:et +# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- +# +# BitBake Toaster Implementation +# +# Copyright (C) 2013-2015 Intel Corporation +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +"""Test cases for Toaster GUI and ReST.""" + +from django.test import TestCase +from django.test.client import RequestFactory +from django.core.urlresolvers import reverse +from django.db.models import Q + +from orm.models import Project, Package +from orm.models import Layer_Version, Recipe +from orm.models import CustomImageRecipe +from orm.models import CustomImagePackage + +import inspect +import toastergui + +from toastergui.tables import SoftwareRecipesTable +import json +from bs4 import BeautifulSoup +import string + +PROJECT_NAME = "test project" +PROJECT_NAME2 = "test project 2" +CLI_BUILDS_PROJECT_NAME = 'Command line builds' + + +class ViewTests(TestCase): + """Tests to verify view APIs.""" + + fixtures = ['toastergui-unittest-data'] + + def setUp(self): + + self.project = Project.objects.first() + self.recipe1 = Recipe.objects.get(pk=2) + self.customr = CustomImageRecipe.objects.first() + self.cust_package = CustomImagePackage.objects.first() + self.package = Package.objects.first() + self.lver = Layer_Version.objects.first() + + def test_get_base_call_returns_html(self): + """Basic test for all-projects view""" + response = self.client.get(reverse('all-projects'), follow=True) + self.assertEqual(response.status_code, 200) + self.assertTrue(response['Content-Type'].startswith('text/html')) + self.assertTemplateUsed(response, "projects-toastertable.html") + + def test_get_json_call_returns_json(self): + """Test for all projects output in json format""" + url = reverse('all-projects') + response = self.client.get(url, {"format": "json"}, follow=True) + self.assertEqual(response.status_code, 200) + self.assertTrue(response['Content-Type'].startswith( + 'application/json')) + + data = json.loads(response.content.decode('utf-8')) + + self.assertTrue("error" in data) + self.assertEqual(data["error"], "ok") + self.assertTrue("rows" in data) + + name_found = False + for row in data["rows"]: + name_found = row['name'].find(self.project.name) + + self.assertTrue(name_found, + "project name not found in projects table") + + def test_typeaheads(self): + """Test typeahead ReST API""" + layers_url = reverse('xhr_layerstypeahead', args=(self.project.id,)) + prj_url = reverse('xhr_projectstypeahead') + + urls = [layers_url, + prj_url, + reverse('xhr_recipestypeahead', args=(self.project.id,)), + reverse('xhr_machinestypeahead', args=(self.project.id,))] + + def basic_reponse_check(response, url): + """Check data structure of http response.""" + self.assertEqual(response.status_code, 200) + self.assertTrue(response['Content-Type'].startswith( + 'application/json')) + + data = json.loads(response.content.decode('utf-8')) + + self.assertTrue("error" in data) + self.assertEqual(data["error"], "ok") + self.assertTrue("results" in data) + + # We got a result so now check the fields + if len(data['results']) > 0: + result = data['results'][0] + + self.assertTrue(len(result['name']) > 0) + self.assertTrue("detail" in result) + self.assertTrue(result['id'] > 0) + + # Special check for the layers typeahead's extra fields + if url == layers_url: + self.assertTrue(len(result['layerdetailurl']) > 0) + self.assertTrue(len(result['vcs_url']) > 0) + self.assertTrue(len(result['vcs_reference']) > 0) + # Special check for project typeahead extra fields + elif url == prj_url: + self.assertTrue(len(result['projectPageUrl']) > 0) + + return True + + return False + + for url in urls: + results = False + + for typeing in list(string.ascii_letters): + response = self.client.get(url, {'search': typeing}) + results = basic_reponse_check(response, url) + if results: + break + + # After "typeing" the alpabet we should have result true + # from each of the urls + self.assertTrue(results) + + def test_xhr_add_layer(self): + """Test xhr_add API""" + # Test for importing an already existing layer + api_url = reverse('xhr_layer', args=(self.project.id,)) + + layer_data = {'vcs_url': "git://git.example.com/test", + 'name': "base-layer", + 'git_ref': "c12b9596afd236116b25ce26dbe0d793de9dc7ce", + 'project_id': self.project.id, + 'local_source_dir': "", + 'add_to_project': True, + 'dir_path': "/path/in/repository"} + + layer_data_json = json.dumps(layer_data) + + response = self.client.put(api_url, layer_data_json) + data = json.loads(response.content.decode('utf-8')) + self.assertEqual(response.status_code, 200) + self.assertEqual(data["error"], "ok") + + self.assertTrue( + layer_data['name'] in + self.project.get_all_compatible_layer_versions().values_list( + 'layer__name', + flat=True), + "Could not find imported layer in project's all layers list" + ) + + # Empty data passed + response = self.client.put(api_url, "{}") + data = json.loads(response.content.decode('utf-8')) + self.assertNotEqual(data["error"], "ok") + + def test_custom_ok(self): + """Test successful return from ReST API xhr_customrecipe""" + url = reverse('xhr_customrecipe') + params = {'name': 'custom', 'project': self.project.id, + 'base': self.recipe1.id} + response = self.client.post(url, params) + self.assertEqual(response.status_code, 200) + data = json.loads(response.content.decode('utf-8')) + self.assertEqual(data['error'], 'ok') + self.assertTrue('url' in data) + # get recipe from the database + recipe = CustomImageRecipe.objects.get(project=self.project, + name=params['name']) + args = (self.project.id, recipe.id,) + self.assertEqual(reverse('customrecipe', args=args), data['url']) + + def test_custom_incomplete_params(self): + """Test not passing all required parameters to xhr_customrecipe""" + url = reverse('xhr_customrecipe') + for params in [{}, {'name': 'custom'}, + {'name': 'custom', 'project': self.project.id}]: + response = self.client.post(url, params) + self.assertEqual(response.status_code, 200) + data = json.loads(response.content.decode('utf-8')) + self.assertNotEqual(data["error"], "ok") + + def test_xhr_custom_wrong_project(self): + """Test passing wrong project id to xhr_customrecipe""" + url = reverse('xhr_customrecipe') + params = {'name': 'custom', 'project': 0, "base": self.recipe1.id} + response = self.client.post(url, params) + self.assertEqual(response.status_code, 200) + data = json.loads(response.content.decode('utf-8')) + self.assertNotEqual(data["error"], "ok") + + def test_xhr_custom_wrong_base(self): + """Test passing wrong base recipe id to xhr_customrecipe""" + url = reverse('xhr_customrecipe') + params = {'name': 'custom', 'project': self.project.id, "base": 0} + response = self.client.post(url, params) + self.assertEqual(response.status_code, 200) + data = json.loads(response.content.decode('utf-8')) + self.assertNotEqual(data["error"], "ok") + + def test_xhr_custom_details(self): + """Test getting custom recipe details""" + url = reverse('xhr_customrecipe_id', args=(self.customr.id,)) + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + expected = {"error": "ok", + "info": {'id': self.customr.id, + 'name': self.customr.name, + 'base_recipe_id': self.recipe1.id, + 'project_id': self.project.id}} + self.assertEqual(json.loads(response.content.decode('utf-8')), + expected) + + def test_xhr_custom_del(self): + """Test deleting custom recipe""" + name = "to be deleted" + recipe = CustomImageRecipe.objects.create( + name=name, project=self.project, + base_recipe=self.recipe1, + file_path="/tmp/testing", + layer_version=self.customr.layer_version) + url = reverse('xhr_customrecipe_id', args=(recipe.id,)) + response = self.client.delete(url) + self.assertEqual(response.status_code, 200) + + gotoUrl = reverse('projectcustomimages', args=(self.project.pk,)) + + self.assertEqual(json.loads(response.content.decode('utf-8')), + {"error": "ok", + "gotoUrl": gotoUrl}) + + # try to delete not-existent recipe + url = reverse('xhr_customrecipe_id', args=(recipe.id,)) + response = self.client.delete(url) + self.assertEqual(response.status_code, 200) + self.assertNotEqual(json.loads( + response.content.decode('utf-8'))["error"], "ok") + + def test_xhr_custom_packages(self): + """Test adding and deleting package to a custom recipe""" + # add self.package to recipe + response = self.client.put(reverse('xhr_customrecipe_packages', + args=(self.customr.id, + self.cust_package.id))) + + self.assertEqual(response.status_code, 200) + self.assertEqual(json.loads(response.content.decode('utf-8')), + {"error": "ok"}) + self.assertEqual(self.customr.appends_set.first().name, + self.cust_package.name) + # delete it + to_delete = self.customr.appends_set.first().pk + del_url = reverse('xhr_customrecipe_packages', + args=(self.customr.id, to_delete)) + + response = self.client.delete(del_url) + self.assertEqual(response.status_code, 200) + self.assertEqual(json.loads(response.content.decode('utf-8')), + {"error": "ok"}) + all_packages = self.customr.get_all_packages().values_list('pk', + flat=True) + + self.assertFalse(to_delete in all_packages) + # delete invalid package to test error condition + del_url = reverse('xhr_customrecipe_packages', + args=(self.customr.id, + 99999)) + + response = self.client.delete(del_url) + self.assertEqual(response.status_code, 200) + self.assertNotEqual(json.loads( + response.content.decode('utf-8'))["error"], "ok") + + def test_xhr_custom_packages_err(self): + """Test error conditions of xhr_customrecipe_packages""" + # test calls with wrong recipe id and wrong package id + for args in [(0, self.package.id), (self.customr.id, 0)]: + url = reverse('xhr_customrecipe_packages', args=args) + # test put and delete methods + for method in (self.client.put, self.client.delete): + response = method(url) + self.assertEqual(response.status_code, 200) + self.assertNotEqual(json.loads( + response.content.decode('utf-8')), + {"error": "ok"}) + + def test_download_custom_recipe(self): + """Download the recipe file generated for the custom image""" + + # Create a dummy recipe file for the custom image generation to read + open("/tmp/a_recipe.bb", 'a').close() + response = self.client.get(reverse('customrecipedownload', + args=(self.project.id, + self.customr.id))) + + self.assertEqual(response.status_code, 200) + + def test_software_recipes_table(self): + """Test structure returned for Software RecipesTable""" + table = SoftwareRecipesTable() + request = RequestFactory().get('/foo/', {'format': 'json'}) + response = table.get(request, pid=self.project.id) + data = json.loads(response.content.decode('utf-8')) + + recipes = Recipe.objects.filter(Q(is_image=False)) + self.assertTrue(len(recipes) > 1, + "Need more than one software recipe to test " + "SoftwareRecipesTable") + + recipe1 = recipes[0] + recipe2 = recipes[1] + + rows = data['rows'] + row1 = next(x for x in rows if x['name'] == recipe1.name) + row2 = next(x for x in rows if x['name'] == recipe2.name) + + self.assertEqual(response.status_code, 200, 'should be 200 OK status') + + # check other columns have been populated correctly + self.assertTrue(recipe1.name in row1['name']) + self.assertTrue(recipe1.version in row1['version']) + self.assertTrue(recipe1.description in + row1['get_description_or_summary']) + + self.assertTrue(recipe1.layer_version.layer.name in + row1['layer_version__layer__name']) + + self.assertTrue(recipe2.name in row2['name']) + self.assertTrue(recipe2.version in row2['version']) + self.assertTrue(recipe2.description in + row2['get_description_or_summary']) + + self.assertTrue(recipe2.layer_version.layer.name in + row2['layer_version__layer__name']) + + def test_toaster_tables(self): + """Test all ToasterTables instances""" + + def get_data(table, options={}): + """Send a request and parse the json response""" + options['format'] = "json" + options['nocache'] = "true" + request = RequestFactory().get('/', options) + + # This is the image recipe needed for a package list for + # PackagesTable do this here to throw a non exist exception + image_recipe = Recipe.objects.get(pk=4) + + # Add any kwargs that are needed by any of the possible tables + args = {'pid': self.project.id, + 'layerid': self.lver.pk, + 'recipeid': self.recipe1.pk, + 'recipe_id': image_recipe.pk, + 'custrecipeid': self.customr.pk, + 'build_id': 1, + 'target_id': 1} + + response = table.get(request, **args) + return json.loads(response.content.decode('utf-8')) + + def get_text_from_td(td): + """If we have html in the td then extract the text portion""" + # just so we don't waste time parsing non html + if "<" not in td: + ret = td + else: + ret = BeautifulSoup(td, "html.parser").text + + if len(ret): + return "0" + else: + return ret + + # Get a list of classes in tables module + tables = inspect.getmembers(toastergui.tables, inspect.isclass) + tables.extend(inspect.getmembers(toastergui.buildtables, + inspect.isclass)) + + for name, table_cls in tables: + # Filter out the non ToasterTables from the tables module + if not issubclass(table_cls, toastergui.widgets.ToasterTable) or \ + table_cls == toastergui.widgets.ToasterTable or \ + 'Mixin' in name: + continue + + # Get the table data without any options, this also does the + # initialisation of the table i.e. setup_columns, + # setup_filters and setup_queryset that we can use later + table = table_cls() + all_data = get_data(table) + + self.assertTrue(len(all_data['rows']) > 1, + "Cannot test on a %s table with < 1 row" % name) + + if table.default_orderby: + row_one = get_text_from_td( + all_data['rows'][0][table.default_orderby.strip("-")]) + row_two = get_text_from_td( + all_data['rows'][1][table.default_orderby.strip("-")]) + + if '-' in table.default_orderby: + self.assertTrue(row_one >= row_two, + "Default ordering not working on %s" + " '%s' should be >= '%s'" % + (name, row_one, row_two)) + else: + self.assertTrue(row_one <= row_two, + "Default ordering not working on %s" + " '%s' should be <= '%s'" % + (name, row_one, row_two)) + + # Test the column ordering and filtering functionality + for column in table.columns: + if column['orderable']: + # If a column is orderable test it in both order + # directions ordering on the columns field_name + ascending = get_data(table_cls(), + {"orderby": column['field_name']}) + + row_one = get_text_from_td( + ascending['rows'][0][column['field_name']]) + row_two = get_text_from_td( + ascending['rows'][1][column['field_name']]) + + self.assertTrue(row_one <= row_two, + "Ascending sort applied but row 0: \"%s\"" + " is less than row 1: \"%s\" " + "%s %s " % + (row_one, row_two, + column['field_name'], name)) + + descending = get_data(table_cls(), + {"orderby": + '-'+column['field_name']}) + + row_one = get_text_from_td( + descending['rows'][0][column['field_name']]) + row_two = get_text_from_td( + descending['rows'][1][column['field_name']]) + + self.assertTrue(row_one >= row_two, + "Descending sort applied but row 0: %s" + "is greater than row 1: %s" + "field %s table %s" % + (row_one, + row_two, + column['field_name'], name)) + + # If the two start rows are the same we haven't actually + # changed the order + self.assertNotEqual(ascending['rows'][0], + descending['rows'][0], + "An orderby %s has not changed the " + "order of the data in table %s" % + (column['field_name'], name)) + + if column['filter_name']: + # If a filter is available for the column get the filter + # info. This contains what filter actions are defined. + filter_info = get_data(table_cls(), + {"cmd": "filterinfo", + "name": column['filter_name']}) + self.assertTrue(len(filter_info['filter_actions']) > 0, + "Filter %s was defined but no actions " + "added to it" % column['filter_name']) + + for filter_action in filter_info['filter_actions']: + # filter string to pass as the option + # This is the name of the filter:action + # e.g. project_filter:not_in_project + filter_string = "%s:%s" % ( + column['filter_name'], + filter_action['action_name']) + # Now get the data with the filter applied + filtered_data = get_data(table_cls(), + {"filter": filter_string}) + + # date range filter actions can't specify the + # number of results they return, so their count is 0 + if filter_action['count'] is not None: + self.assertEqual( + len(filtered_data['rows']), + int(filter_action['count']), + "We added a table filter for %s but " + "the number of rows returned was not " + "what the filter info said there " + "would be" % name) + + # Test search functionality on the table + something_found = False + for search in list(string.ascii_letters): + search_data = get_data(table_cls(), {'search': search}) + + if len(search_data['rows']) > 0: + something_found = True + break + + self.assertTrue(something_found, + "We went through the whole alphabet and nothing" + " was found for the search of table %s" % name) + + # Test the limit functionality on the table + limited_data = get_data(table_cls(), {'limit': "1"}) + self.assertEqual(len(limited_data['rows']), + 1, + "Limit 1 set on table %s but not 1 row returned" + % name) + + # Test the pagination functionality on the table + page_one_data = get_data(table_cls(), {'limit': "1", + "page": "1"})['rows'][0] + + page_two_data = get_data(table_cls(), {'limit': "1", + "page": "2"})['rows'][0] + + self.assertNotEqual(page_one_data, + page_two_data, + "Changed page on table %s but first row is" + " the same as the previous page" % name) |