diff options
Diffstat (limited to 'bitbake/lib/toaster/tests')
49 files changed, 0 insertions, 7291 deletions
diff --git a/bitbake/lib/toaster/tests/__init__.py b/bitbake/lib/toaster/tests/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 --- a/bitbake/lib/toaster/tests/__init__.py +++ /dev/null | |||
diff --git a/bitbake/lib/toaster/tests/browser/README b/bitbake/lib/toaster/tests/browser/README deleted file mode 100644 index 352c4fe3e9..0000000000 --- a/bitbake/lib/toaster/tests/browser/README +++ /dev/null | |||
| @@ -1,74 +0,0 @@ | |||
| 1 | # Running Toaster's browser-based test suite | ||
| 2 | |||
| 3 | These tests require Selenium to be installed in your Python environment. | ||
| 4 | |||
| 5 | The simplest way to install this is via pip3: | ||
| 6 | |||
| 7 | pip3 install selenium==2.53.2 | ||
| 8 | |||
| 9 | Note that if you use other versions of Selenium, some of the tests (such as | ||
| 10 | tests.browser.test_js_unit_tests.TestJsUnitTests) may fail, as these rely on | ||
| 11 | a Selenium test report with a version-specific format. | ||
| 12 | |||
| 13 | To run tests against Chrome: | ||
| 14 | |||
| 15 | * Download chromedriver for your host OS from | ||
| 16 | https://sites.google.com/a/chromium.org/chromedriver/downloads | ||
| 17 | * On *nix systems, put chromedriver on PATH | ||
| 18 | * On Windows, put chromedriver.exe in the same directory as chrome.exe | ||
| 19 | |||
| 20 | To run tests against PhantomJS (headless): | ||
| 21 | --NOTE - Selenium seems to be deprecating support for this mode --- | ||
| 22 | * Download and install PhantomJS: | ||
| 23 | http://phantomjs.org/download.html | ||
| 24 | * On *nix systems, put phantomjs on PATH | ||
| 25 | * Not tested on Windows | ||
| 26 | |||
| 27 | To run tests against Firefox, you may need to install the Marionette driver, | ||
| 28 | depending on how new your version of Firefox is. One clue that you need to do | ||
| 29 | this is if you see an exception like: | ||
| 30 | |||
| 31 | selenium.common.exceptions.WebDriverException: Message: The browser | ||
| 32 | appears to have exited before we could connect. If you specified | ||
| 33 | a log_file in the FirefoxBinary constructor, check it for details. | ||
| 34 | |||
| 35 | See https://developer.mozilla.org/en-US/docs/Mozilla/QA/Marionette/WebDriver | ||
| 36 | for installation instructions. Ensure that the Marionette executable (renamed | ||
| 37 | as wires on Linux or wires.exe on Windows) is on your PATH; and use "marionette" | ||
| 38 | as the browser string passed via TOASTER_TESTS_BROWSER (see below). | ||
| 39 | |||
| 40 | (Note: The Toaster tests have been checked against Firefox 47 with the | ||
| 41 | Marionette driver.) | ||
| 42 | |||
| 43 | The test cases will instantiate a Selenium driver set by the | ||
| 44 | TOASTER_TESTS_BROWSER environment variable, or Chrome if this is not specified. | ||
| 45 | |||
| 46 | To run tests against the Selenium Firefox Docker container: | ||
| 47 | More explanation is located at https://wiki.yoctoproject.org/wiki/TipsAndTricks/TestingToasterWithContainers | ||
| 48 | * Run the Selenium container: | ||
| 49 | ** docker run -it --rm=true -p 5900:5900 -p 4444:4444 --name=selenium selenium/standalone-firefox-debug:2.53.0 | ||
| 50 | *** 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 | ||
| 51 | *** 4444 is the default selenium sever port. | ||
| 52 | * Run the tests | ||
| 53 | ** 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 | ||
| 54 | ** 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 | ||
| 55 | *** 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. | ||
| 56 | *** --liveserver=xxx tells Django to run the test server on an interface and port reachable by both host and container. | ||
| 57 | **** 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" | ||
| 58 | * connect to the vnc server to see the tests if you would like | ||
| 59 | ** xtightvncviewer 127.0.0.1:5900 | ||
| 60 | ** note, you need to wait for the test container to come up before this can connect. | ||
| 61 | |||
| 62 | Available drivers: | ||
| 63 | |||
| 64 | * chrome (default) | ||
| 65 | * firefox | ||
| 66 | * marionette (for newer Firefoxes) | ||
| 67 | * ie | ||
| 68 | * phantomjs (deprecated) | ||
| 69 | * remote | ||
| 70 | |||
| 71 | e.g. to run the test suite with phantomjs where you have phantomjs installed | ||
| 72 | in /home/me/apps/phantomjs: | ||
| 73 | |||
| 74 | PATH=/home/me/apps/phantomjs/bin:$PATH TOASTER_TESTS_BROWSER=phantomjs manage.py test tests.browser | ||
diff --git a/bitbake/lib/toaster/tests/browser/__init__.py b/bitbake/lib/toaster/tests/browser/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 --- a/bitbake/lib/toaster/tests/browser/__init__.py +++ /dev/null | |||
diff --git a/bitbake/lib/toaster/tests/browser/selenium_helpers.py b/bitbake/lib/toaster/tests/browser/selenium_helpers.py deleted file mode 100644 index 02d4f4b5c7..0000000000 --- a/bitbake/lib/toaster/tests/browser/selenium_helpers.py +++ /dev/null | |||
| @@ -1,21 +0,0 @@ | |||
| 1 | #! /usr/bin/env python3 | ||
| 2 | # | ||
| 3 | # BitBake Toaster Implementation | ||
| 4 | # | ||
| 5 | # Copyright (C) 2013-2016 Intel Corporation | ||
| 6 | # | ||
| 7 | # SPDX-License-Identifier: GPL-2.0-only | ||
| 8 | # | ||
| 9 | # The Wait class and some of SeleniumDriverHelper and SeleniumTestCase are | ||
| 10 | # modified from Patchwork, released under the same licence terms as Toaster: | ||
| 11 | # https://github.com/dlespiau/patchwork/blob/master/patchwork/tests.browser.py | ||
| 12 | |||
| 13 | """ | ||
| 14 | Helper methods for creating Toaster Selenium tests which run within | ||
| 15 | the context of Django unit tests. | ||
| 16 | """ | ||
| 17 | from django.contrib.staticfiles.testing import StaticLiveServerTestCase | ||
| 18 | from tests.browser.selenium_helpers_base import SeleniumTestCaseBase | ||
| 19 | |||
| 20 | class SeleniumTestCase(SeleniumTestCaseBase, StaticLiveServerTestCase): | ||
| 21 | pass | ||
diff --git a/bitbake/lib/toaster/tests/browser/selenium_helpers_base.py b/bitbake/lib/toaster/tests/browser/selenium_helpers_base.py deleted file mode 100644 index 6953541ab5..0000000000 --- a/bitbake/lib/toaster/tests/browser/selenium_helpers_base.py +++ /dev/null | |||
| @@ -1,277 +0,0 @@ | |||
| 1 | #! /usr/bin/env python3 | ||
| 2 | # | ||
| 3 | # BitBake Toaster Implementation | ||
| 4 | # | ||
| 5 | # Copyright (C) 2013-2016 Intel Corporation | ||
| 6 | # | ||
| 7 | # SPDX-License-Identifier: GPL-2.0-only | ||
| 8 | # | ||
| 9 | # The Wait class and some of SeleniumDriverHelper and SeleniumTestCase are | ||
| 10 | # modified from Patchwork, released under the same licence terms as Toaster: | ||
| 11 | # https://github.com/dlespiau/patchwork/blob/master/patchwork/tests.browser.py | ||
| 12 | |||
| 13 | """ | ||
| 14 | Helper methods for creating Toaster Selenium tests which run within | ||
| 15 | the context of Django unit tests. | ||
| 16 | """ | ||
| 17 | |||
| 18 | import os | ||
| 19 | import time | ||
| 20 | import unittest | ||
| 21 | |||
| 22 | import pytest | ||
| 23 | from selenium import webdriver | ||
| 24 | from selenium.webdriver.support import expected_conditions as EC | ||
| 25 | from selenium.webdriver.support.ui import WebDriverWait | ||
| 26 | from selenium.webdriver.common.by import By | ||
| 27 | from selenium.webdriver.common.desired_capabilities import DesiredCapabilities | ||
| 28 | from selenium.common.exceptions import NoSuchElementException, \ | ||
| 29 | StaleElementReferenceException, TimeoutException, \ | ||
| 30 | SessionNotCreatedException, WebDriverException | ||
| 31 | |||
| 32 | def create_selenium_driver(cls,browser='chrome'): | ||
| 33 | # set default browser string based on env (if available) | ||
| 34 | env_browser = os.environ.get('TOASTER_TESTS_BROWSER') | ||
| 35 | if env_browser: | ||
| 36 | browser = env_browser | ||
| 37 | |||
| 38 | if browser == 'chrome': | ||
| 39 | options = webdriver.ChromeOptions() | ||
| 40 | options.add_argument('--headless') | ||
| 41 | options.add_argument('--disable-infobars') | ||
| 42 | options.add_argument('--disable-dev-shm-usage') | ||
| 43 | options.add_argument('--no-sandbox') | ||
| 44 | options.add_argument('--remote-debugging-port=9222') | ||
| 45 | try: | ||
| 46 | return webdriver.Chrome(options=options) | ||
| 47 | except SessionNotCreatedException as e: | ||
| 48 | exit_message = "Halting tests prematurely to avoid cascading errors." | ||
| 49 | # check if chrome / chromedriver exists | ||
| 50 | chrome_path = os.popen("find ~/.cache/selenium/chrome/ -name 'chrome' -type f -print -quit").read().strip() | ||
| 51 | if not chrome_path: | ||
| 52 | pytest.exit(f"Failed to install/find chrome.\n{exit_message}") | ||
| 53 | chromedriver_path = os.popen("find ~/.cache/selenium/chromedriver/ -name 'chromedriver' -type f -print -quit").read().strip() | ||
| 54 | if not chromedriver_path: | ||
| 55 | pytest.exit(f"Failed to install/find chromedriver.\n{exit_message}") | ||
| 56 | # check if depends on each are fulfilled | ||
| 57 | depends_chrome = os.popen(f"ldd {chrome_path} | grep 'not found'").read().strip() | ||
| 58 | if depends_chrome: | ||
| 59 | pytest.exit(f"Missing chrome dependencies.\n{depends_chrome}\n{exit_message}") | ||
| 60 | depends_chromedriver = os.popen(f"ldd {chromedriver_path} | grep 'not found'").read().strip() | ||
| 61 | if depends_chromedriver: | ||
| 62 | pytest.exit(f"Missing chromedriver dependencies.\n{depends_chromedriver}\n{exit_message}") | ||
| 63 | # print original error otherwise | ||
| 64 | pytest.exit(f"Failed to start chromedriver.\n{e}\n{exit_message}") | ||
| 65 | elif browser == 'firefox': | ||
| 66 | return webdriver.Firefox() | ||
| 67 | elif browser == 'marionette': | ||
| 68 | capabilities = DesiredCapabilities.FIREFOX | ||
| 69 | capabilities['marionette'] = True | ||
| 70 | return webdriver.Firefox(capabilities=capabilities) | ||
| 71 | elif browser == 'ie': | ||
| 72 | return webdriver.Ie() | ||
| 73 | elif browser == 'phantomjs': | ||
| 74 | return webdriver.PhantomJS() | ||
| 75 | elif browser == 'remote': | ||
| 76 | # if we were to add yet another env variable like TOASTER_REMOTE_BROWSER | ||
| 77 | # we could let people pick firefox or chrome, left for later | ||
| 78 | remote_hub= os.environ.get('TOASTER_REMOTE_HUB') | ||
| 79 | driver = webdriver.Remote(remote_hub, | ||
| 80 | webdriver.DesiredCapabilities.FIREFOX.copy()) | ||
| 81 | |||
| 82 | driver.get("http://%s:%s"%(cls.server_thread.host,cls.server_thread.port)) | ||
| 83 | return driver | ||
| 84 | else: | ||
| 85 | msg = 'Selenium driver for browser %s is not available' % browser | ||
| 86 | raise RuntimeError(msg) | ||
| 87 | |||
| 88 | class Wait(WebDriverWait): | ||
| 89 | """ | ||
| 90 | Subclass of WebDriverWait with predetermined timeout and poll | ||
| 91 | frequency. Also deals with a wider variety of exceptions. | ||
| 92 | """ | ||
| 93 | _TIMEOUT = 20 | ||
| 94 | _POLL_FREQUENCY = 0.5 | ||
| 95 | |||
| 96 | def __init__(self, driver, timeout=_TIMEOUT, poll=_POLL_FREQUENCY): | ||
| 97 | self._TIMEOUT = timeout | ||
| 98 | self._POLL_FREQUENCY = poll | ||
| 99 | super(Wait, self).__init__(driver, self._TIMEOUT, self._POLL_FREQUENCY) | ||
| 100 | |||
| 101 | def until(self, method, message=''): | ||
| 102 | """ | ||
| 103 | Calls the method provided with the driver as an argument until the | ||
| 104 | return value is not False. | ||
| 105 | """ | ||
| 106 | |||
| 107 | end_time = time.time() + self._timeout | ||
| 108 | while True: | ||
| 109 | try: | ||
| 110 | value = method(self._driver) | ||
| 111 | if value: | ||
| 112 | return value | ||
| 113 | except NoSuchElementException: | ||
| 114 | pass | ||
| 115 | except StaleElementReferenceException: | ||
| 116 | pass | ||
| 117 | except WebDriverException: | ||
| 118 | # selenium.common.exceptions.WebDriverException: Message: unknown error: unhandled inspector error: {"code":-32000,"message":"Node with given id does not belong to the document"} | ||
| 119 | pass | ||
| 120 | |||
| 121 | time.sleep(self._poll) | ||
| 122 | if time.time() > end_time: | ||
| 123 | break | ||
| 124 | |||
| 125 | raise TimeoutException(message) | ||
| 126 | |||
| 127 | def until_not(self, method, message=''): | ||
| 128 | """ | ||
| 129 | Calls the method provided with the driver as an argument until the | ||
| 130 | return value is False. | ||
| 131 | """ | ||
| 132 | |||
| 133 | end_time = time.time() + self._timeout | ||
| 134 | while True: | ||
| 135 | try: | ||
| 136 | value = method(self._driver) | ||
| 137 | if not value: | ||
| 138 | return value | ||
| 139 | except NoSuchElementException: | ||
| 140 | return True | ||
| 141 | except StaleElementReferenceException: | ||
| 142 | pass | ||
| 143 | |||
| 144 | time.sleep(self._poll) | ||
| 145 | if time.time() > end_time: | ||
| 146 | break | ||
| 147 | |||
| 148 | raise TimeoutException(message) | ||
| 149 | |||
| 150 | class SeleniumTestCaseBase(unittest.TestCase): | ||
| 151 | """ | ||
| 152 | NB StaticLiveServerTestCase is used as the base test case so that | ||
| 153 | static files are served correctly in a Selenium test run context; see | ||
| 154 | https://docs.djangoproject.com/en/1.9/ref/contrib/staticfiles/#specialized-test-case-to-support-live-testing | ||
| 155 | """ | ||
| 156 | |||
| 157 | @classmethod | ||
| 158 | def setUpClass(cls): | ||
| 159 | """ Create a webdriver driver at the class level """ | ||
| 160 | |||
| 161 | super(SeleniumTestCaseBase, cls).setUpClass() | ||
| 162 | |||
| 163 | # instantiate the Selenium webdriver once for all the test methods | ||
| 164 | # in this test case | ||
| 165 | cls.driver = create_selenium_driver(cls) | ||
| 166 | cls.driver.maximize_window() | ||
| 167 | |||
| 168 | @classmethod | ||
| 169 | def tearDownClass(cls): | ||
| 170 | """ Clean up webdriver driver """ | ||
| 171 | |||
| 172 | cls.driver.quit() | ||
| 173 | # Allow driver resources to be properly freed before proceeding with further tests | ||
| 174 | time.sleep(5) | ||
| 175 | super(SeleniumTestCaseBase, cls).tearDownClass() | ||
| 176 | |||
| 177 | def get(self, url): | ||
| 178 | """ | ||
| 179 | Selenium requires absolute URLs, so convert Django URLs returned | ||
| 180 | by resolve() or similar to absolute ones and get using the | ||
| 181 | webdriver instance. | ||
| 182 | |||
| 183 | url: a relative URL | ||
| 184 | """ | ||
| 185 | abs_url = '%s%s' % (self.live_server_url, url) | ||
| 186 | self.driver.get(abs_url) | ||
| 187 | |||
| 188 | try: # Ensure page is loaded before proceeding | ||
| 189 | self.wait_until_visible("#global-nav") | ||
| 190 | except NoSuchElementException: | ||
| 191 | self.driver.implicitly_wait(3) | ||
| 192 | except TimeoutException: | ||
| 193 | self.driver.implicitly_wait(3) | ||
| 194 | |||
| 195 | def find(self, selector): | ||
| 196 | """ Find single element by CSS selector """ | ||
| 197 | return self.driver.find_element(By.CSS_SELECTOR, selector) | ||
| 198 | |||
| 199 | def find_all(self, selector): | ||
| 200 | """ Find all elements matching CSS selector """ | ||
| 201 | return self.driver.find_elements(By.CSS_SELECTOR, selector) | ||
| 202 | |||
| 203 | def element_exists(self, selector): | ||
| 204 | """ | ||
| 205 | Return True if one element matching selector exists, | ||
| 206 | False otherwise | ||
| 207 | """ | ||
| 208 | return len(self.find_all(selector)) == 1 | ||
| 209 | |||
| 210 | def focused_element(self): | ||
| 211 | """ Return the element which currently has focus on the page """ | ||
| 212 | return self.driver.switch_to.active_element | ||
| 213 | |||
| 214 | def wait_until_present(self, selector, timeout=Wait._TIMEOUT): | ||
| 215 | """ Wait until element matching CSS selector is on the page """ | ||
| 216 | is_present = lambda driver: self.find(selector) | ||
| 217 | msg = 'An element matching "%s" should be on the page' % selector | ||
| 218 | element = Wait(self.driver, timeout=timeout).until(is_present, msg) | ||
| 219 | return element | ||
| 220 | |||
| 221 | def wait_until_visible(self, selector, timeout=Wait._TIMEOUT): | ||
| 222 | """ Wait until element matching CSS selector is visible on the page """ | ||
| 223 | is_visible = lambda driver: self.find(selector).is_displayed() | ||
| 224 | msg = 'An element matching "%s" should be visible' % selector | ||
| 225 | Wait(self.driver, timeout=timeout).until(is_visible, msg) | ||
| 226 | return self.find(selector) | ||
| 227 | |||
| 228 | def wait_until_not_visible(self, selector, timeout=Wait._TIMEOUT): | ||
| 229 | """ Wait until element matching CSS selector is not visible on the page """ | ||
| 230 | is_visible = lambda driver: self.find(selector).is_displayed() | ||
| 231 | msg = 'An element matching "%s" should be visible' % selector | ||
| 232 | Wait(self.driver, timeout=timeout).until_not(is_visible, msg) | ||
| 233 | return self.find(selector) | ||
| 234 | |||
| 235 | def wait_until_clickable(self, selector, timeout=Wait._TIMEOUT): | ||
| 236 | """ Wait until element matching CSS selector is visible on the page """ | ||
| 237 | WebDriverWait(self.driver, timeout=timeout).until(lambda driver: self.driver.execute_script("return jQuery.active == 0")) | ||
| 238 | is_clickable = lambda driver: (self.find(selector).is_displayed() and self.find(selector).is_enabled()) | ||
| 239 | msg = 'An element matching "%s" should be clickable' % selector | ||
| 240 | Wait(self.driver, timeout=timeout).until(is_clickable, msg) | ||
| 241 | return self.find(selector) | ||
| 242 | |||
| 243 | def wait_until_element_clickable(self, finder, timeout=Wait._TIMEOUT): | ||
| 244 | """ Wait until element is clickable """ | ||
| 245 | WebDriverWait(self.driver, timeout=timeout).until(lambda driver: self.driver.execute_script("return jQuery.active == 0")) | ||
| 246 | is_clickable = lambda driver: (finder(driver).is_displayed() and finder(driver).is_enabled()) | ||
| 247 | msg = 'A matching element never became be clickable' | ||
| 248 | Wait(self.driver, timeout=timeout).until(is_clickable, msg) | ||
| 249 | return finder(self.driver) | ||
| 250 | |||
| 251 | def wait_until_focused(self, selector): | ||
| 252 | """ Wait until element matching CSS selector has focus """ | ||
| 253 | is_focused = \ | ||
| 254 | lambda driver: self.find(selector) == self.focused_element() | ||
| 255 | msg = 'An element matching "%s" should be focused' % selector | ||
| 256 | Wait(self.driver).until(is_focused, msg) | ||
| 257 | return self.find(selector) | ||
| 258 | |||
| 259 | def enter_text(self, selector, value): | ||
| 260 | """ Insert text into element matching selector """ | ||
| 261 | # note that keyup events don't occur until the element is clicked | ||
| 262 | # (in the case of <input type="text"...>, for example), so simulate | ||
| 263 | # user clicking the element before inserting text into it | ||
| 264 | field = self.click(selector) | ||
| 265 | |||
| 266 | field.send_keys(value) | ||
| 267 | return field | ||
| 268 | |||
| 269 | def click(self, selector): | ||
| 270 | """ Click on element which matches CSS selector """ | ||
| 271 | element = self.wait_until_visible(selector) | ||
| 272 | element.click() | ||
| 273 | return element | ||
| 274 | |||
| 275 | def get_page_source(self): | ||
| 276 | """ Get raw HTML for the current page """ | ||
| 277 | return self.driver.page_source | ||
diff --git a/bitbake/lib/toaster/tests/browser/test_all_builds_page.py b/bitbake/lib/toaster/tests/browser/test_all_builds_page.py deleted file mode 100644 index 9ab81fb11b..0000000000 --- a/bitbake/lib/toaster/tests/browser/test_all_builds_page.py +++ /dev/null | |||
| @@ -1,477 +0,0 @@ | |||
| 1 | #! /usr/bin/env python3 | ||
| 2 | # | ||
| 3 | # BitBake Toaster Implementation | ||
| 4 | # | ||
| 5 | # Copyright (C) 2013-2016 Intel Corporation | ||
| 6 | # | ||
| 7 | # SPDX-License-Identifier: GPL-2.0-only | ||
| 8 | # | ||
| 9 | |||
| 10 | import os | ||
| 11 | import re | ||
| 12 | |||
| 13 | from django.urls import reverse | ||
| 14 | from selenium.webdriver.support.select import Select | ||
| 15 | from django.utils import timezone | ||
| 16 | from bldcontrol.models import BuildRequest | ||
| 17 | from tests.browser.selenium_helpers import SeleniumTestCase | ||
| 18 | |||
| 19 | from orm.models import BitbakeVersion, Layer, Layer_Version, Recipe, Release, Project, Build, Target, Task | ||
| 20 | |||
| 21 | from selenium.webdriver.common.by import By | ||
| 22 | |||
| 23 | |||
| 24 | class TestAllBuildsPage(SeleniumTestCase): | ||
| 25 | """ Tests for all builds page /builds/ """ | ||
| 26 | |||
| 27 | PROJECT_NAME = 'test project' | ||
| 28 | CLI_BUILDS_PROJECT_NAME = 'command line builds' | ||
| 29 | |||
| 30 | def setUp(self): | ||
| 31 | builldir = os.environ.get('BUILDDIR', './') | ||
| 32 | bbv = BitbakeVersion.objects.create(name='bbv1', giturl=f'{builldir}/', | ||
| 33 | branch='master', dirpath='') | ||
| 34 | release = Release.objects.create(name='release1', | ||
| 35 | bitbake_version=bbv) | ||
| 36 | self.project1 = Project.objects.create_project(name=self.PROJECT_NAME, | ||
| 37 | release=release) | ||
| 38 | self.default_project = Project.objects.create_project( | ||
| 39 | name=self.CLI_BUILDS_PROJECT_NAME, | ||
| 40 | release=release | ||
| 41 | ) | ||
| 42 | self.default_project.is_default = True | ||
| 43 | self.default_project.save() | ||
| 44 | |||
| 45 | # parameters for builds to associate with the projects | ||
| 46 | now = timezone.now() | ||
| 47 | |||
| 48 | self.project1_build_success = { | ||
| 49 | 'project': self.project1, | ||
| 50 | 'started_on': now, | ||
| 51 | 'completed_on': now, | ||
| 52 | 'outcome': Build.SUCCEEDED | ||
| 53 | } | ||
| 54 | |||
| 55 | self.project1_build_failure = { | ||
| 56 | 'project': self.project1, | ||
| 57 | 'started_on': now, | ||
| 58 | 'completed_on': now, | ||
| 59 | 'outcome': Build.FAILED | ||
| 60 | } | ||
| 61 | |||
| 62 | self.default_project_build_success = { | ||
| 63 | 'project': self.default_project, | ||
| 64 | 'started_on': now, | ||
| 65 | 'completed_on': now, | ||
| 66 | 'outcome': Build.SUCCEEDED | ||
| 67 | } | ||
| 68 | |||
| 69 | def _get_build_time_element(self, build): | ||
| 70 | """ | ||
| 71 | Return the HTML element containing the build time for a build | ||
| 72 | in the recent builds area | ||
| 73 | """ | ||
| 74 | selector = 'div[data-latest-build-result="%s"] ' \ | ||
| 75 | '[data-role="data-recent-build-buildtime-field"]' % build.id | ||
| 76 | |||
| 77 | # because this loads via Ajax, wait for it to be visible | ||
| 78 | self.wait_until_visible(selector) | ||
| 79 | |||
| 80 | build_time_spans = self.find_all(selector) | ||
| 81 | |||
| 82 | self.assertEqual(len(build_time_spans), 1) | ||
| 83 | |||
| 84 | return build_time_spans[0] | ||
| 85 | |||
| 86 | def _get_row_for_build(self, build): | ||
| 87 | """ Get the table row for the build from the all builds table """ | ||
| 88 | self.wait_until_visible('#allbuildstable') | ||
| 89 | |||
| 90 | rows = self.find_all('#allbuildstable tr') | ||
| 91 | |||
| 92 | # look for the row with a download link on the recipe which matches the | ||
| 93 | # build ID | ||
| 94 | url = reverse('builddashboard', args=(build.id,)) | ||
| 95 | selector = 'td.target a[href="%s"]' % url | ||
| 96 | |||
| 97 | found_row = None | ||
| 98 | for row in rows: | ||
| 99 | |||
| 100 | outcome_links = row.find_elements(By.CSS_SELECTOR, selector) | ||
| 101 | if len(outcome_links) == 1: | ||
| 102 | found_row = row | ||
| 103 | break | ||
| 104 | |||
| 105 | self.assertNotEqual(found_row, None) | ||
| 106 | |||
| 107 | return found_row | ||
| 108 | |||
| 109 | def _get_create_builds(self, **kwargs): | ||
| 110 | """ Create a build and return the build object """ | ||
| 111 | build1 = Build.objects.create(**self.project1_build_success) | ||
| 112 | build2 = Build.objects.create(**self.project1_build_failure) | ||
| 113 | |||
| 114 | # add some targets to these builds so they have recipe links | ||
| 115 | # (and so we can find the row in the ToasterTable corresponding to | ||
| 116 | # a particular build) | ||
| 117 | Target.objects.create(build=build1, target='foo') | ||
| 118 | Target.objects.create(build=build2, target='bar') | ||
| 119 | |||
| 120 | if kwargs: | ||
| 121 | # Create kwargs.get('success') builds with success status with target | ||
| 122 | # and kwargs.get('failure') builds with failure status with target | ||
| 123 | for i in range(kwargs.get('success', 0)): | ||
| 124 | now = timezone.now() | ||
| 125 | self.project1_build_success['started_on'] = now | ||
| 126 | self.project1_build_success[ | ||
| 127 | 'completed_on'] = now - timezone.timedelta(days=i) | ||
| 128 | build = Build.objects.create(**self.project1_build_success) | ||
| 129 | Target.objects.create(build=build, | ||
| 130 | target=f'{i}_success_recipe', | ||
| 131 | task=f'{i}_success_task') | ||
| 132 | |||
| 133 | self._set_buildRequest_and_task_on_build(build) | ||
| 134 | for i in range(kwargs.get('failure', 0)): | ||
| 135 | now = timezone.now() | ||
| 136 | self.project1_build_failure['started_on'] = now | ||
| 137 | self.project1_build_failure[ | ||
| 138 | 'completed_on'] = now - timezone.timedelta(days=i) | ||
| 139 | build = Build.objects.create(**self.project1_build_failure) | ||
| 140 | Target.objects.create(build=build, | ||
| 141 | target=f'{i}_fail_recipe', | ||
| 142 | task=f'{i}_fail_task') | ||
| 143 | self._set_buildRequest_and_task_on_build(build) | ||
| 144 | return build1, build2 | ||
| 145 | |||
| 146 | def _create_recipe(self): | ||
| 147 | """ Add a recipe to the database and return it """ | ||
| 148 | layer = Layer.objects.create() | ||
| 149 | layer_version = Layer_Version.objects.create(layer=layer) | ||
| 150 | return Recipe.objects.create(name='recipe_foo', layer_version=layer_version) | ||
| 151 | |||
| 152 | def _set_buildRequest_and_task_on_build(self, build): | ||
| 153 | """ Set buildRequest and task on build """ | ||
| 154 | build.recipes_parsed = 1 | ||
| 155 | build.save() | ||
| 156 | buildRequest = BuildRequest.objects.create( | ||
| 157 | build=build, | ||
| 158 | project=self.project1, | ||
| 159 | state=BuildRequest.REQ_COMPLETED) | ||
| 160 | build.build_request = buildRequest | ||
| 161 | recipe = self._create_recipe() | ||
| 162 | task = Task.objects.create(build=build, | ||
| 163 | recipe=recipe, | ||
| 164 | task_name='task', | ||
| 165 | outcome=Task.OUTCOME_SUCCESS) | ||
| 166 | task.save() | ||
| 167 | build.save() | ||
| 168 | |||
| 169 | def test_show_tasks_with_suffix(self): | ||
| 170 | """ Task should be shown as suffix on build name """ | ||
| 171 | build = Build.objects.create(**self.project1_build_success) | ||
| 172 | target = 'bash' | ||
| 173 | task = 'clean' | ||
| 174 | Target.objects.create(build=build, target=target, task=task) | ||
| 175 | |||
| 176 | url = reverse('all-builds') | ||
| 177 | self.get(url) | ||
| 178 | self.wait_until_visible('td[class="target"]') | ||
| 179 | |||
| 180 | cell = self.find('td[class="target"]') | ||
| 181 | content = cell.get_attribute('innerHTML') | ||
| 182 | expected_text = '%s:%s' % (target, task) | ||
| 183 | |||
| 184 | self.assertTrue(re.search(expected_text, content), | ||
| 185 | '"target" cell should contain text %s' % expected_text) | ||
| 186 | |||
| 187 | def test_rebuild_buttons(self): | ||
| 188 | """ | ||
| 189 | Test 'Rebuild' buttons in recent builds section | ||
| 190 | |||
| 191 | 'Rebuild' button should not be shown for command-line builds, | ||
| 192 | but should be shown for other builds | ||
| 193 | """ | ||
| 194 | build1 = Build.objects.create(**self.project1_build_success) | ||
| 195 | default_build = Build.objects.create( | ||
| 196 | **self.default_project_build_success) | ||
| 197 | |||
| 198 | url = reverse('all-builds') | ||
| 199 | self.get(url) | ||
| 200 | |||
| 201 | # should see a rebuild button for non-command-line builds | ||
| 202 | self.wait_until_visible('#allbuildstable tbody tr') | ||
| 203 | self.wait_until_visible('.rebuild-btn') | ||
| 204 | selector = 'div[data-latest-build-result="%s"] .rebuild-btn' % build1.id | ||
| 205 | run_again_button = self.find_all(selector) | ||
| 206 | self.assertEqual(len(run_again_button), 1, | ||
| 207 | 'should see a rebuild button for non-cli builds') | ||
| 208 | |||
| 209 | # shouldn't see a rebuild button for command-line builds | ||
| 210 | selector = 'div[data-latest-build-result="%s"] .rebuild-btn' % default_build.id | ||
| 211 | run_again_button = self.find_all(selector) | ||
| 212 | self.assertEqual(len(run_again_button), 0, | ||
| 213 | 'should not see a rebuild button for cli builds') | ||
| 214 | |||
| 215 | def test_tooltips_on_project_name(self): | ||
| 216 | """ | ||
| 217 | Test tooltips shown next to project name in the main table | ||
| 218 | |||
| 219 | A tooltip should be present next to the command line | ||
| 220 | builds project name in the all builds page, but not for | ||
| 221 | other projects | ||
| 222 | """ | ||
| 223 | Build.objects.create(**self.project1_build_success) | ||
| 224 | Build.objects.create(**self.default_project_build_success) | ||
| 225 | |||
| 226 | url = reverse('all-builds') | ||
| 227 | self.get(url) | ||
| 228 | self.wait_until_visible('#allbuildstable') | ||
| 229 | |||
| 230 | # get the project name cells from the table | ||
| 231 | cells = self.find_all('#allbuildstable td[class="project"]') | ||
| 232 | |||
| 233 | selector = 'span.get-help' | ||
| 234 | |||
| 235 | for cell in cells: | ||
| 236 | content = cell.get_attribute('innerHTML') | ||
| 237 | help_icons = cell.find_elements(By.CSS_SELECTOR, selector) | ||
| 238 | |||
| 239 | if re.search(self.PROJECT_NAME, content): | ||
| 240 | # no help icon next to non-cli project name | ||
| 241 | msg = 'should not be a help icon for non-cli builds name' | ||
| 242 | self.assertEqual(len(help_icons), 0, msg) | ||
| 243 | elif re.search(self.CLI_BUILDS_PROJECT_NAME, content): | ||
| 244 | # help icon next to cli project name | ||
| 245 | msg = 'should be a help icon for cli builds name' | ||
| 246 | self.assertEqual(len(help_icons), 1, msg) | ||
| 247 | else: | ||
| 248 | msg = 'found unexpected project name cell in all builds table' | ||
| 249 | self.fail(msg) | ||
| 250 | |||
| 251 | def test_builds_time_links(self): | ||
| 252 | """ | ||
| 253 | Successful builds should have links on the time column and in the | ||
| 254 | recent builds area; failed builds should not have links on the time column, | ||
| 255 | or in the recent builds area | ||
| 256 | """ | ||
| 257 | build1, build2 = self._get_create_builds() | ||
| 258 | |||
| 259 | url = reverse('all-builds') | ||
| 260 | self.get(url) | ||
| 261 | self.wait_until_visible('#allbuildstable') | ||
| 262 | |||
| 263 | # test recent builds area for successful build | ||
| 264 | element = self._get_build_time_element(build1) | ||
| 265 | links = element.find_elements(By.CSS_SELECTOR, 'a') | ||
| 266 | msg = 'should be a link on the build time for a successful recent build' | ||
| 267 | self.assertEqual(len(links), 1, msg) | ||
| 268 | |||
| 269 | # test recent builds area for failed build | ||
| 270 | element = self._get_build_time_element(build2) | ||
| 271 | links = element.find_elements(By.CSS_SELECTOR, 'a') | ||
| 272 | msg = 'should not be a link on the build time for a failed recent build' | ||
| 273 | self.assertEqual(len(links), 0, msg) | ||
| 274 | |||
| 275 | # test the time column for successful build | ||
| 276 | build1_row = self._get_row_for_build(build1) | ||
| 277 | links = build1_row.find_elements(By.CSS_SELECTOR, 'td.time a') | ||
| 278 | msg = 'should be a link on the build time for a successful build' | ||
| 279 | self.assertEqual(len(links), 1, msg) | ||
| 280 | |||
| 281 | # test the time column for failed build | ||
| 282 | build2_row = self._get_row_for_build(build2) | ||
| 283 | links = build2_row.find_elements(By.CSS_SELECTOR, 'td.time a') | ||
| 284 | msg = 'should not be a link on the build time for a failed build' | ||
| 285 | self.assertEqual(len(links), 0, msg) | ||
| 286 | |||
| 287 | def test_builds_table_search_box(self): | ||
| 288 | """ Test the search box in the builds table on the all builds page """ | ||
| 289 | self._get_create_builds() | ||
| 290 | |||
| 291 | url = reverse('all-builds') | ||
| 292 | self.get(url) | ||
| 293 | |||
| 294 | # Check search box is present and works | ||
| 295 | self.wait_until_visible('#allbuildstable tbody tr') | ||
| 296 | search_box = self.find('#search-input-allbuildstable') | ||
| 297 | self.assertTrue(search_box.is_displayed()) | ||
| 298 | |||
| 299 | # Check that we can search for a build by recipe name | ||
| 300 | search_box.send_keys('foo') | ||
| 301 | search_btn = self.find('#search-submit-allbuildstable') | ||
| 302 | search_btn.click() | ||
| 303 | self.wait_until_visible('#allbuildstable tbody tr') | ||
| 304 | rows = self.find_all('#allbuildstable tbody tr') | ||
| 305 | self.assertTrue(len(rows) >= 1) | ||
| 306 | |||
| 307 | def test_filtering_on_failure_tasks_column(self): | ||
| 308 | """ Test the filtering on failure tasks column in the builds table on the all builds page """ | ||
| 309 | def _check_if_filter_failed_tasks_column_is_visible(): | ||
| 310 | # check if failed tasks filter column is visible, if not click on it | ||
| 311 | # Check edit column | ||
| 312 | edit_column = self.find('#edit-columns-button') | ||
| 313 | self.assertTrue(edit_column.is_displayed()) | ||
| 314 | edit_column.click() | ||
| 315 | # Check dropdown is visible | ||
| 316 | self.wait_until_visible('ul.dropdown-menu.editcol') | ||
| 317 | filter_fails_task_checkbox = self.find('#checkbox-failed_tasks') | ||
| 318 | if not filter_fails_task_checkbox.is_selected(): | ||
| 319 | filter_fails_task_checkbox.click() | ||
| 320 | edit_column.click() | ||
| 321 | |||
| 322 | self._get_create_builds(success=10, failure=10) | ||
| 323 | |||
| 324 | url = reverse('all-builds') | ||
| 325 | self.get(url) | ||
| 326 | |||
| 327 | # Check filtering on failure tasks column | ||
| 328 | self.wait_until_visible('#allbuildstable tbody tr') | ||
| 329 | _check_if_filter_failed_tasks_column_is_visible() | ||
| 330 | failed_tasks_filter = self.find('#failed_tasks_filter') | ||
| 331 | failed_tasks_filter.click() | ||
| 332 | # Check popup is visible | ||
| 333 | self.wait_until_visible('#filter-modal-allbuildstable') | ||
| 334 | self.assertTrue( | ||
| 335 | self.find('#filter-modal-allbuildstable').is_displayed()) | ||
| 336 | # Check that we can filter by failure tasks | ||
| 337 | build_without_failure_tasks = self.find( | ||
| 338 | '#failed_tasks_filter\\:without_failed_tasks') | ||
| 339 | build_without_failure_tasks.click() | ||
| 340 | # click on apply button | ||
| 341 | self.find('#filter-modal-allbuildstable .btn-primary').click() | ||
| 342 | self.wait_until_visible('#allbuildstable tbody tr') | ||
| 343 | # Check if filter is applied, by checking if failed_tasks_filter has btn-primary class | ||
| 344 | self.assertTrue(self.find('#failed_tasks_filter').get_attribute( | ||
| 345 | 'class').find('btn-primary') != -1) | ||
| 346 | |||
| 347 | def test_filtering_on_completedOn_column(self): | ||
| 348 | """ Test the filtering on completed_on column in the builds table on the all builds page """ | ||
| 349 | self._get_create_builds(success=10, failure=10) | ||
| 350 | |||
| 351 | url = reverse('all-builds') | ||
| 352 | self.get(url) | ||
| 353 | |||
| 354 | # Check filtering on failure tasks column | ||
| 355 | self.wait_until_visible('#allbuildstable tbody tr') | ||
| 356 | completed_on_filter = self.find('#completed_on_filter') | ||
| 357 | completed_on_filter.click() | ||
| 358 | # Check popup is visible | ||
| 359 | self.wait_until_visible('#filter-modal-allbuildstable') | ||
| 360 | self.assertTrue( | ||
| 361 | self.find('#filter-modal-allbuildstable').is_displayed()) | ||
| 362 | # Check that we can filter by failure tasks | ||
| 363 | build_without_failure_tasks = self.find( | ||
| 364 | '#completed_on_filter\\:date_range') | ||
| 365 | build_without_failure_tasks.click() | ||
| 366 | # click on apply button | ||
| 367 | self.find('#filter-modal-allbuildstable .btn-primary').click() | ||
| 368 | self.wait_until_visible('#allbuildstable tbody tr') | ||
| 369 | # Check if filter is applied, by checking if completed_on_filter has btn-primary class | ||
| 370 | self.assertTrue(self.find('#completed_on_filter').get_attribute( | ||
| 371 | 'class').find('btn-primary') != -1) | ||
| 372 | |||
| 373 | # Filter by date range | ||
| 374 | self.find('#completed_on_filter').click() | ||
| 375 | self.wait_until_visible('#filter-modal-allbuildstable') | ||
| 376 | date_ranges = self.driver.find_elements( | ||
| 377 | By.XPATH, '//input[@class="form-control hasDatepicker"]') | ||
| 378 | today = timezone.now() | ||
| 379 | yestersday = today - timezone.timedelta(days=1) | ||
| 380 | date_ranges[0].send_keys(yestersday.strftime('%Y-%m-%d')) | ||
| 381 | date_ranges[1].send_keys(today.strftime('%Y-%m-%d')) | ||
| 382 | self.find('#filter-modal-allbuildstable .btn-primary').click() | ||
| 383 | self.wait_until_visible('#allbuildstable tbody tr') | ||
| 384 | self.assertTrue(self.find('#completed_on_filter').get_attribute( | ||
| 385 | 'class').find('btn-primary') != -1) | ||
| 386 | # Check if filter is applied, number of builds displayed should be 6 | ||
| 387 | self.assertTrue(len(self.find_all('#allbuildstable tbody tr')) >= 4) | ||
| 388 | |||
| 389 | def test_builds_table_editColumn(self): | ||
| 390 | """ Test the edit column feature in the builds table on the all builds page """ | ||
| 391 | self._get_create_builds(success=10, failure=10) | ||
| 392 | |||
| 393 | def test_edit_column(check_box_id): | ||
| 394 | # Check that we can hide/show table column | ||
| 395 | check_box = self.find(f'#{check_box_id}') | ||
| 396 | th_class = str(check_box_id).replace('checkbox-', '') | ||
| 397 | if check_box.is_selected(): | ||
| 398 | # check if column is visible in table | ||
| 399 | self.assertTrue( | ||
| 400 | self.find( | ||
| 401 | f'#allbuildstable thead th.{th_class}' | ||
| 402 | ).is_displayed(), | ||
| 403 | f"The {th_class} column is checked in EditColumn dropdown, but it's not visible in table" | ||
| 404 | ) | ||
| 405 | check_box.click() | ||
| 406 | # check if column is hidden in table | ||
| 407 | self.assertFalse( | ||
| 408 | self.find( | ||
| 409 | f'#allbuildstable thead th.{th_class}' | ||
| 410 | ).is_displayed(), | ||
| 411 | f"The {th_class} column is unchecked in EditColumn dropdown, but it's visible in table" | ||
| 412 | ) | ||
| 413 | else: | ||
| 414 | # check if column is hidden in table | ||
| 415 | self.assertFalse( | ||
| 416 | self.find( | ||
| 417 | f'#allbuildstable thead th.{th_class}' | ||
| 418 | ).is_displayed(), | ||
| 419 | f"The {th_class} column is unchecked in EditColumn dropdown, but it's visible in table" | ||
| 420 | ) | ||
| 421 | check_box.click() | ||
| 422 | # check if column is visible in table | ||
| 423 | self.assertTrue( | ||
| 424 | self.find( | ||
| 425 | f'#allbuildstable thead th.{th_class}' | ||
| 426 | ).is_displayed(), | ||
| 427 | f"The {th_class} column is checked in EditColumn dropdown, but it's not visible in table" | ||
| 428 | ) | ||
| 429 | url = reverse('all-builds') | ||
| 430 | self.get(url) | ||
| 431 | self.wait_until_visible('#allbuildstable tbody tr') | ||
| 432 | |||
| 433 | # Check edit column | ||
| 434 | edit_column = self.find('#edit-columns-button') | ||
| 435 | self.assertTrue(edit_column.is_displayed()) | ||
| 436 | edit_column.click() | ||
| 437 | # Check dropdown is visible | ||
| 438 | self.wait_until_visible('ul.dropdown-menu.editcol') | ||
| 439 | |||
| 440 | # Check that we can hide the edit column | ||
| 441 | test_edit_column('checkbox-errors_no') | ||
| 442 | test_edit_column('checkbox-failed_tasks') | ||
| 443 | test_edit_column('checkbox-image_files') | ||
| 444 | test_edit_column('checkbox-project') | ||
| 445 | test_edit_column('checkbox-started_on') | ||
| 446 | test_edit_column('checkbox-time') | ||
| 447 | test_edit_column('checkbox-warnings_no') | ||
| 448 | |||
| 449 | def test_builds_table_show_rows(self): | ||
| 450 | """ Test the show rows feature in the builds table on the all builds page """ | ||
| 451 | self._get_create_builds(success=100, failure=100) | ||
| 452 | |||
| 453 | def test_show_rows(row_to_show, show_row_link): | ||
| 454 | # Check that we can show rows == row_to_show | ||
| 455 | show_row_link.select_by_value(str(row_to_show)) | ||
| 456 | self.wait_until_visible('#allbuildstable tbody tr') | ||
| 457 | # check at least some rows are visible | ||
| 458 | self.assertTrue( | ||
| 459 | len(self.find_all('#allbuildstable tbody tr')) > 0 | ||
| 460 | ) | ||
| 461 | |||
| 462 | url = reverse('all-builds') | ||
| 463 | self.get(url) | ||
| 464 | self.wait_until_visible('#allbuildstable tbody tr') | ||
| 465 | |||
| 466 | show_rows = self.driver.find_elements( | ||
| 467 | By.XPATH, | ||
| 468 | '//select[@class="form-control pagesize-allbuildstable"]' | ||
| 469 | ) | ||
| 470 | # Check show rows | ||
| 471 | for show_row_link in show_rows: | ||
| 472 | show_row_link = Select(show_row_link) | ||
| 473 | test_show_rows(10, show_row_link) | ||
| 474 | test_show_rows(25, show_row_link) | ||
| 475 | test_show_rows(50, show_row_link) | ||
| 476 | test_show_rows(100, show_row_link) | ||
| 477 | test_show_rows(150, show_row_link) | ||
diff --git a/bitbake/lib/toaster/tests/browser/test_all_projects_page.py b/bitbake/lib/toaster/tests/browser/test_all_projects_page.py deleted file mode 100644 index 05e12892be..0000000000 --- a/bitbake/lib/toaster/tests/browser/test_all_projects_page.py +++ /dev/null | |||
| @@ -1,337 +0,0 @@ | |||
| 1 | #! /usr/bin/env python3 | ||
| 2 | # | ||
| 3 | # BitBake Toaster Implementation | ||
| 4 | # | ||
| 5 | # Copyright (C) 2013-2016 Intel Corporation | ||
| 6 | # | ||
| 7 | # SPDX-License-Identifier: GPL-2.0-only | ||
| 8 | # | ||
| 9 | |||
| 10 | import os | ||
| 11 | import re | ||
| 12 | |||
| 13 | from django.urls import reverse | ||
| 14 | from django.utils import timezone | ||
| 15 | from selenium.webdriver.support.select import Select | ||
| 16 | from tests.browser.selenium_helpers import SeleniumTestCase | ||
| 17 | |||
| 18 | from orm.models import BitbakeVersion, Release, Project, Build | ||
| 19 | from orm.models import ProjectVariable | ||
| 20 | |||
| 21 | from selenium.webdriver.common.by import By | ||
| 22 | |||
| 23 | |||
| 24 | class TestAllProjectsPage(SeleniumTestCase): | ||
| 25 | """ Browser tests for projects page /projects/ """ | ||
| 26 | |||
| 27 | PROJECT_NAME = 'test project' | ||
| 28 | CLI_BUILDS_PROJECT_NAME = 'command line builds' | ||
| 29 | MACHINE_NAME = 'delorean' | ||
| 30 | |||
| 31 | def setUp(self): | ||
| 32 | """ Add default project manually """ | ||
| 33 | project = Project.objects.create_project( | ||
| 34 | self.CLI_BUILDS_PROJECT_NAME, None) | ||
| 35 | self.default_project = project | ||
| 36 | self.default_project.is_default = True | ||
| 37 | self.default_project.save() | ||
| 38 | |||
| 39 | # this project is only set for some of the tests | ||
| 40 | self.project = None | ||
| 41 | |||
| 42 | self.release = None | ||
| 43 | |||
| 44 | def _create_projects(self, nb_project=10): | ||
| 45 | projects = [] | ||
| 46 | for i in range(1, nb_project + 1): | ||
| 47 | projects.append( | ||
| 48 | Project( | ||
| 49 | name='test project {}'.format(i), | ||
| 50 | release=self.release, | ||
| 51 | ) | ||
| 52 | ) | ||
| 53 | Project.objects.bulk_create(projects) | ||
| 54 | |||
| 55 | def _add_build_to_default_project(self): | ||
| 56 | """ Add a build to the default project (not used in all tests) """ | ||
| 57 | now = timezone.now() | ||
| 58 | build = Build.objects.create(project=self.default_project, | ||
| 59 | started_on=now, | ||
| 60 | completed_on=now) | ||
| 61 | build.save() | ||
| 62 | |||
| 63 | def _add_non_default_project(self): | ||
| 64 | """ Add another project """ | ||
| 65 | builldir = os.environ.get('BUILDDIR', './') | ||
| 66 | bbv = BitbakeVersion.objects.create(name='test bbv', giturl=f'{builldir}/', | ||
| 67 | branch='master', dirpath='') | ||
| 68 | self.release = Release.objects.create(name='test release', | ||
| 69 | branch_name='master', | ||
| 70 | bitbake_version=bbv) | ||
| 71 | self.project = Project.objects.create_project( | ||
| 72 | self.PROJECT_NAME, self.release) | ||
| 73 | self.project.is_default = False | ||
| 74 | self.project.save() | ||
| 75 | |||
| 76 | # fake the MACHINE variable | ||
| 77 | project_var = ProjectVariable.objects.create(project=self.project, | ||
| 78 | name='MACHINE', | ||
| 79 | value=self.MACHINE_NAME) | ||
| 80 | project_var.save() | ||
| 81 | |||
| 82 | def _get_row_for_project(self, project_name): | ||
| 83 | """ Get the HTML row for a project, or None if not found """ | ||
| 84 | self.wait_until_visible('#projectstable tbody tr') | ||
| 85 | rows = self.find_all('#projectstable tbody tr') | ||
| 86 | |||
| 87 | # find the row with a project name matching the one supplied | ||
| 88 | found_row = None | ||
| 89 | for row in rows: | ||
| 90 | if re.search(project_name, row.get_attribute('innerHTML')): | ||
| 91 | found_row = row | ||
| 92 | break | ||
| 93 | |||
| 94 | return found_row | ||
| 95 | |||
| 96 | def test_default_project_hidden(self): | ||
| 97 | """ | ||
| 98 | The default project should be hidden if it has no builds | ||
| 99 | and we should see the "no results" area | ||
| 100 | """ | ||
| 101 | url = reverse('all-projects') | ||
| 102 | self.get(url) | ||
| 103 | self.wait_until_visible('#empty-state-projectstable') | ||
| 104 | |||
| 105 | rows = self.find_all('#projectstable tbody tr') | ||
| 106 | self.assertEqual(len(rows), 0, 'should be no projects displayed') | ||
| 107 | |||
| 108 | def test_default_project_has_build(self): | ||
| 109 | """ The default project should be shown if it has builds """ | ||
| 110 | self._add_build_to_default_project() | ||
| 111 | |||
| 112 | url = reverse('all-projects') | ||
| 113 | self.get(url) | ||
| 114 | |||
| 115 | default_project_row = self._get_row_for_project( | ||
| 116 | self.default_project.name) | ||
| 117 | |||
| 118 | self.assertNotEqual(default_project_row, None, | ||
| 119 | 'default project "cli builds" should be in page') | ||
| 120 | |||
| 121 | def test_default_project_release(self): | ||
| 122 | """ | ||
| 123 | The release for the default project should display as | ||
| 124 | 'Not applicable' | ||
| 125 | """ | ||
| 126 | # need a build, otherwise project doesn't display at all | ||
| 127 | self._add_build_to_default_project() | ||
| 128 | |||
| 129 | # another project to test, which should show release | ||
| 130 | self._add_non_default_project() | ||
| 131 | |||
| 132 | self.get(reverse('all-projects')) | ||
| 133 | self.wait_until_visible("#projectstable tr") | ||
| 134 | |||
| 135 | # find the row for the default project | ||
| 136 | default_project_row = self._get_row_for_project( | ||
| 137 | self.default_project.name) | ||
| 138 | |||
| 139 | # check the release text for the default project | ||
| 140 | selector = 'span[data-project-field="release"] span.text-muted' | ||
| 141 | element = default_project_row.find_element(By.CSS_SELECTOR, selector) | ||
| 142 | text = element.text.strip() | ||
| 143 | self.assertEqual(text, 'Not applicable', | ||
| 144 | 'release should be "not applicable" for default project') | ||
| 145 | |||
| 146 | # find the row for the default project | ||
| 147 | other_project_row = self._get_row_for_project(self.project.name) | ||
| 148 | |||
| 149 | # check the link in the release cell for the other project | ||
| 150 | selector = 'span[data-project-field="release"]' | ||
| 151 | element = other_project_row.find_element(By.CSS_SELECTOR, selector) | ||
| 152 | text = element.text.strip() | ||
| 153 | self.assertEqual(text, self.release.name, | ||
| 154 | 'release name should be shown for non-default project') | ||
| 155 | |||
| 156 | def test_default_project_machine(self): | ||
| 157 | """ | ||
| 158 | The machine for the default project should display as | ||
| 159 | 'Not applicable' | ||
| 160 | """ | ||
| 161 | # need a build, otherwise project doesn't display at all | ||
| 162 | self._add_build_to_default_project() | ||
| 163 | |||
| 164 | # another project to test, which should show machine | ||
| 165 | self._add_non_default_project() | ||
| 166 | |||
| 167 | self.get(reverse('all-projects')) | ||
| 168 | |||
| 169 | self.wait_until_visible("#projectstable tr") | ||
| 170 | |||
| 171 | # find the row for the default project | ||
| 172 | default_project_row = self._get_row_for_project( | ||
| 173 | self.default_project.name) | ||
| 174 | |||
| 175 | # check the machine cell for the default project | ||
| 176 | selector = 'span[data-project-field="machine"] span.text-muted' | ||
| 177 | element = default_project_row.find_element(By.CSS_SELECTOR, selector) | ||
| 178 | text = element.text.strip() | ||
| 179 | self.assertEqual(text, 'Not applicable', | ||
| 180 | 'machine should be not applicable for default project') | ||
| 181 | |||
| 182 | # find the row for the default project | ||
| 183 | other_project_row = self._get_row_for_project(self.project.name) | ||
| 184 | |||
| 185 | # check the link in the machine cell for the other project | ||
| 186 | selector = 'span[data-project-field="machine"]' | ||
| 187 | element = other_project_row.find_element(By.CSS_SELECTOR, selector) | ||
| 188 | text = element.text.strip() | ||
| 189 | self.assertEqual(text, self.MACHINE_NAME, | ||
| 190 | 'machine name should be shown for non-default project') | ||
| 191 | |||
| 192 | def test_project_page_links(self): | ||
| 193 | """ | ||
| 194 | Test that links for the default project point to the builds | ||
| 195 | page /projects/X/builds for that project, and that links for | ||
| 196 | other projects point to their configuration pages /projects/X/ | ||
| 197 | """ | ||
| 198 | |||
| 199 | # need a build, otherwise project doesn't display at all | ||
| 200 | self._add_build_to_default_project() | ||
| 201 | |||
| 202 | # another project to test | ||
| 203 | self._add_non_default_project() | ||
| 204 | |||
| 205 | self.get(reverse('all-projects')) | ||
| 206 | |||
| 207 | # find the row for the default project | ||
| 208 | default_project_row = self._get_row_for_project( | ||
| 209 | self.default_project.name) | ||
| 210 | |||
| 211 | # check the link on the name field | ||
| 212 | selector = 'span[data-project-field="name"] a' | ||
| 213 | element = default_project_row.find_element(By.CSS_SELECTOR, selector) | ||
| 214 | link_url = element.get_attribute('href').strip() | ||
| 215 | expected_url = reverse( | ||
| 216 | 'projectbuilds', args=(self.default_project.id,)) | ||
| 217 | msg = 'link on default project name should point to builds but was %s' % link_url | ||
| 218 | self.assertTrue(link_url.endswith(expected_url), msg) | ||
| 219 | |||
| 220 | # find the row for the other project | ||
| 221 | other_project_row = self._get_row_for_project(self.project.name) | ||
| 222 | |||
| 223 | # check the link for the other project | ||
| 224 | selector = 'span[data-project-field="name"] a' | ||
| 225 | element = other_project_row.find_element(By.CSS_SELECTOR, selector) | ||
| 226 | link_url = element.get_attribute('href').strip() | ||
| 227 | expected_url = reverse('project', args=(self.project.id,)) | ||
| 228 | msg = 'link on project name should point to configuration but was %s' % link_url | ||
| 229 | self.assertTrue(link_url.endswith(expected_url), msg) | ||
| 230 | |||
| 231 | def test_allProject_table_search_box(self): | ||
| 232 | """ Test the search box in the all project table on the all projects page """ | ||
| 233 | self._create_projects() | ||
| 234 | |||
| 235 | url = reverse('all-projects') | ||
| 236 | self.get(url) | ||
| 237 | |||
| 238 | # Chseck search box is present and works | ||
| 239 | self.wait_until_visible('#projectstable tbody tr') | ||
| 240 | search_box = self.find('#search-input-projectstable') | ||
| 241 | self.assertTrue(search_box.is_displayed()) | ||
| 242 | |||
| 243 | # Check that we can search for a project by project name | ||
| 244 | search_box.send_keys('test project 10') | ||
| 245 | search_btn = self.find('#search-submit-projectstable') | ||
| 246 | search_btn.click() | ||
| 247 | self.wait_until_visible('#projectstable tbody tr') | ||
| 248 | rows = self.find_all('#projectstable tbody tr') | ||
| 249 | self.assertTrue(len(rows) == 1) | ||
| 250 | |||
| 251 | def test_allProject_table_editColumn(self): | ||
| 252 | """ Test the edit column feature in the projects table on the all projects page """ | ||
| 253 | self._create_projects() | ||
| 254 | |||
| 255 | def test_edit_column(check_box_id): | ||
| 256 | # Check that we can hide/show table column | ||
| 257 | check_box = self.find(f'#{check_box_id}') | ||
| 258 | th_class = str(check_box_id).replace('checkbox-', '') | ||
| 259 | if check_box.is_selected(): | ||
| 260 | # check if column is visible in table | ||
| 261 | self.assertTrue( | ||
| 262 | self.find( | ||
| 263 | f'#projectstable thead th.{th_class}' | ||
| 264 | ).is_displayed(), | ||
| 265 | f"The {th_class} column is checked in EditColumn dropdown, but it's not visible in table" | ||
| 266 | ) | ||
| 267 | check_box.click() | ||
| 268 | # check if column is hidden in table | ||
| 269 | self.assertFalse( | ||
| 270 | self.find( | ||
| 271 | f'#projectstable thead th.{th_class}' | ||
| 272 | ).is_displayed(), | ||
| 273 | f"The {th_class} column is unchecked in EditColumn dropdown, but it's visible in table" | ||
| 274 | ) | ||
| 275 | else: | ||
| 276 | # check if column is hidden in table | ||
| 277 | self.assertFalse( | ||
| 278 | self.find( | ||
| 279 | f'#projectstable thead th.{th_class}' | ||
| 280 | ).is_displayed(), | ||
| 281 | f"The {th_class} column is unchecked in EditColumn dropdown, but it's visible in table" | ||
| 282 | ) | ||
| 283 | check_box.click() | ||
| 284 | # check if column is visible in table | ||
| 285 | self.assertTrue( | ||
| 286 | self.find( | ||
| 287 | f'#projectstable thead th.{th_class}' | ||
| 288 | ).is_displayed(), | ||
| 289 | f"The {th_class} column is checked in EditColumn dropdown, but it's not visible in table" | ||
| 290 | ) | ||
| 291 | url = reverse('all-projects') | ||
| 292 | self.get(url) | ||
| 293 | self.wait_until_visible('#projectstable tbody tr') | ||
| 294 | |||
| 295 | # Check edit column | ||
| 296 | edit_column = self.find('#edit-columns-button') | ||
| 297 | self.assertTrue(edit_column.is_displayed()) | ||
| 298 | edit_column.click() | ||
| 299 | # Check dropdown is visible | ||
| 300 | self.wait_until_visible('ul.dropdown-menu.editcol') | ||
| 301 | |||
| 302 | # Check that we can hide the edit column | ||
| 303 | test_edit_column('checkbox-errors') | ||
| 304 | test_edit_column('checkbox-image_files') | ||
| 305 | test_edit_column('checkbox-last_build_outcome') | ||
| 306 | test_edit_column('checkbox-recipe_name') | ||
| 307 | test_edit_column('checkbox-warnings') | ||
| 308 | |||
| 309 | def test_allProject_table_show_rows(self): | ||
| 310 | """ Test the show rows feature in the projects table on the all projects page """ | ||
| 311 | self._create_projects(nb_project=200) | ||
| 312 | |||
| 313 | def test_show_rows(row_to_show, show_row_link): | ||
| 314 | # Check that we can show rows == row_to_show | ||
| 315 | show_row_link.select_by_value(str(row_to_show)) | ||
| 316 | self.wait_until_visible('#projectstable tbody tr') | ||
| 317 | # check at least some rows are visible | ||
| 318 | self.assertTrue( | ||
| 319 | len(self.find_all('#projectstable tbody tr')) > 0 | ||
| 320 | ) | ||
| 321 | |||
| 322 | url = reverse('all-projects') | ||
| 323 | self.get(url) | ||
| 324 | self.wait_until_visible('#projectstable tbody tr') | ||
| 325 | |||
| 326 | show_rows = self.driver.find_elements( | ||
| 327 | By.XPATH, | ||
| 328 | '//select[@class="form-control pagesize-projectstable"]' | ||
| 329 | ) | ||
| 330 | # Check show rows | ||
| 331 | for show_row_link in show_rows: | ||
| 332 | show_row_link = Select(show_row_link) | ||
| 333 | test_show_rows(10, show_row_link) | ||
| 334 | test_show_rows(25, show_row_link) | ||
| 335 | test_show_rows(50, show_row_link) | ||
| 336 | test_show_rows(100, show_row_link) | ||
| 337 | test_show_rows(150, show_row_link) | ||
diff --git a/bitbake/lib/toaster/tests/browser/test_builddashboard_page.py b/bitbake/lib/toaster/tests/browser/test_builddashboard_page.py deleted file mode 100644 index 82367108e2..0000000000 --- a/bitbake/lib/toaster/tests/browser/test_builddashboard_page.py +++ /dev/null | |||
| @@ -1,340 +0,0 @@ | |||
| 1 | #! /usr/bin/env python3 | ||
| 2 | # | ||
| 3 | # BitBake Toaster Implementation | ||
| 4 | # | ||
| 5 | # Copyright (C) 2013-2016 Intel Corporation | ||
| 6 | # | ||
| 7 | # SPDX-License-Identifier: GPL-2.0-only | ||
| 8 | # | ||
| 9 | |||
| 10 | import os | ||
| 11 | from django.urls import reverse | ||
| 12 | from django.utils import timezone | ||
| 13 | |||
| 14 | from tests.browser.selenium_helpers import SeleniumTestCase | ||
| 15 | |||
| 16 | from orm.models import Project, Release, BitbakeVersion, Build, LogMessage | ||
| 17 | from orm.models import Layer, Layer_Version, Recipe, CustomImageRecipe, Variable | ||
| 18 | |||
| 19 | from selenium.webdriver.common.by import By | ||
| 20 | |||
| 21 | class TestBuildDashboardPage(SeleniumTestCase): | ||
| 22 | """ Tests for the build dashboard /build/X """ | ||
| 23 | |||
| 24 | def setUp(self): | ||
| 25 | builldir = os.environ.get('BUILDDIR', './') | ||
| 26 | bbv = BitbakeVersion.objects.create(name='bbv1', giturl=f'{builldir}/', | ||
| 27 | branch='master', dirpath="") | ||
| 28 | release = Release.objects.create(name='release1', | ||
| 29 | bitbake_version=bbv) | ||
| 30 | project = Project.objects.create_project(name='test project', | ||
| 31 | release=release) | ||
| 32 | |||
| 33 | now = timezone.now() | ||
| 34 | |||
| 35 | self.build1 = Build.objects.create(project=project, | ||
| 36 | started_on=now, | ||
| 37 | completed_on=now, | ||
| 38 | outcome=Build.SUCCEEDED) | ||
| 39 | |||
| 40 | self.build2 = Build.objects.create(project=project, | ||
| 41 | started_on=now, | ||
| 42 | completed_on=now, | ||
| 43 | outcome=Build.SUCCEEDED) | ||
| 44 | |||
| 45 | self.build3 = Build.objects.create(project=project, | ||
| 46 | started_on=now, | ||
| 47 | completed_on=now, | ||
| 48 | outcome=Build.FAILED) | ||
| 49 | |||
| 50 | # add Variable objects to the successful builds, as this is the criterion | ||
| 51 | # used to determine whether the left-hand panel should be displayed | ||
| 52 | Variable.objects.create(build=self.build1, | ||
| 53 | variable_name='Foo', | ||
| 54 | variable_value='Bar') | ||
| 55 | Variable.objects.create(build=self.build2, | ||
| 56 | variable_name='Foo', | ||
| 57 | variable_value='Bar') | ||
| 58 | |||
| 59 | # exception | ||
| 60 | msg1 = 'an exception was thrown' | ||
| 61 | self.exception_message = LogMessage.objects.create( | ||
| 62 | build=self.build1, | ||
| 63 | level=LogMessage.EXCEPTION, | ||
| 64 | message=msg1 | ||
| 65 | ) | ||
| 66 | |||
| 67 | # critical | ||
| 68 | msg2 = 'a critical error occurred' | ||
| 69 | self.critical_message = LogMessage.objects.create( | ||
| 70 | build=self.build1, | ||
| 71 | level=LogMessage.CRITICAL, | ||
| 72 | message=msg2 | ||
| 73 | ) | ||
| 74 | |||
| 75 | # error on the failed build | ||
| 76 | msg3 = 'an error occurred' | ||
| 77 | self.error_message = LogMessage.objects.create( | ||
| 78 | build=self.build3, | ||
| 79 | level=LogMessage.ERROR, | ||
| 80 | message=msg3 | ||
| 81 | ) | ||
| 82 | |||
| 83 | # warning on the failed build | ||
| 84 | msg4 = 'DANGER WILL ROBINSON' | ||
| 85 | self.warning_message = LogMessage.objects.create( | ||
| 86 | build=self.build3, | ||
| 87 | level=LogMessage.WARNING, | ||
| 88 | message=msg4 | ||
| 89 | ) | ||
| 90 | |||
| 91 | # recipes related to the build, for testing the edit custom image/new | ||
| 92 | # custom image buttons | ||
| 93 | layer = Layer.objects.create(name='alayer') | ||
| 94 | layer_version = Layer_Version.objects.create( | ||
| 95 | layer=layer, build=self.build1 | ||
| 96 | ) | ||
| 97 | |||
| 98 | # non-image recipes related to a build, for testing the new custom | ||
| 99 | # image button | ||
| 100 | layer_version2 = Layer_Version.objects.create(layer=layer, | ||
| 101 | build=self.build3) | ||
| 102 | |||
| 103 | # image recipes | ||
| 104 | self.image_recipe1 = Recipe.objects.create( | ||
| 105 | name='recipeA', | ||
| 106 | layer_version=layer_version, | ||
| 107 | file_path='/foo/recipeA.bb', | ||
| 108 | is_image=True | ||
| 109 | ) | ||
| 110 | self.image_recipe2 = Recipe.objects.create( | ||
| 111 | name='recipeB', | ||
| 112 | layer_version=layer_version, | ||
| 113 | file_path='/foo/recipeB.bb', | ||
| 114 | is_image=True | ||
| 115 | ) | ||
| 116 | |||
| 117 | # custom image recipes for this project | ||
| 118 | self.custom_image_recipe1 = CustomImageRecipe.objects.create( | ||
| 119 | name='customRecipeY', | ||
| 120 | project=project, | ||
| 121 | layer_version=layer_version, | ||
| 122 | file_path='/foo/customRecipeY.bb', | ||
| 123 | base_recipe=self.image_recipe1, | ||
| 124 | is_image=True | ||
| 125 | ) | ||
| 126 | self.custom_image_recipe2 = CustomImageRecipe.objects.create( | ||
| 127 | name='customRecipeZ', | ||
| 128 | project=project, | ||
| 129 | layer_version=layer_version, | ||
| 130 | file_path='/foo/customRecipeZ.bb', | ||
| 131 | base_recipe=self.image_recipe2, | ||
| 132 | is_image=True | ||
| 133 | ) | ||
| 134 | |||
| 135 | # custom image recipe for a different project (to test filtering | ||
| 136 | # of image recipes and custom image recipes is correct: this shouldn't | ||
| 137 | # show up in either query against self.build1) | ||
| 138 | self.custom_image_recipe3 = CustomImageRecipe.objects.create( | ||
| 139 | name='customRecipeOmega', | ||
| 140 | project=Project.objects.create(name='baz', release=release), | ||
| 141 | layer_version=Layer_Version.objects.create( | ||
| 142 | layer=layer, build=self.build2 | ||
| 143 | ), | ||
| 144 | file_path='/foo/customRecipeOmega.bb', | ||
| 145 | base_recipe=self.image_recipe2, | ||
| 146 | is_image=True | ||
| 147 | ) | ||
| 148 | |||
| 149 | # another non-image recipe (to test filtering of image recipes and | ||
| 150 | # custom image recipes is correct: this shouldn't show up in either | ||
| 151 | # for any build) | ||
| 152 | self.non_image_recipe = Recipe.objects.create( | ||
| 153 | name='nonImageRecipe', | ||
| 154 | layer_version=layer_version, | ||
| 155 | file_path='/foo/nonImageRecipe.bb', | ||
| 156 | is_image=False | ||
| 157 | ) | ||
| 158 | |||
| 159 | def _get_build_dashboard(self, build): | ||
| 160 | """ | ||
| 161 | Navigate to the build dashboard for build | ||
| 162 | """ | ||
| 163 | url = reverse('builddashboard', args=(build.id,)) | ||
| 164 | self.get(url) | ||
| 165 | self.wait_until_visible('#global-nav') | ||
| 166 | |||
| 167 | def _get_build_dashboard_errors(self, build): | ||
| 168 | """ | ||
| 169 | Get a list of HTML fragments representing the errors on the | ||
| 170 | dashboard for the Build object build | ||
| 171 | """ | ||
| 172 | self._get_build_dashboard(build) | ||
| 173 | return self.find_all('#errors div.alert-danger') | ||
| 174 | |||
| 175 | def _check_for_log_message(self, message_elements, log_message): | ||
| 176 | """ | ||
| 177 | Check that the LogMessage <log_message> has a representation in | ||
| 178 | the HTML elements <message_elements>. | ||
| 179 | |||
| 180 | message_elements: WebElements representing the log messages shown | ||
| 181 | in the build dashboard; each should have a <pre> element inside | ||
| 182 | it with a data-log-message-id attribute | ||
| 183 | |||
| 184 | log_message: orm.models.LogMessage instance | ||
| 185 | """ | ||
| 186 | expected_text = log_message.message | ||
| 187 | expected_pk = str(log_message.pk) | ||
| 188 | |||
| 189 | found = False | ||
| 190 | for element in message_elements: | ||
| 191 | log_message_text = element.find_element(By.TAG_NAME, 'pre').text.strip() | ||
| 192 | text_matches = (log_message_text == expected_text) | ||
| 193 | |||
| 194 | log_message_pk = element.get_attribute('data-log-message-id') | ||
| 195 | id_matches = (log_message_pk == expected_pk) | ||
| 196 | |||
| 197 | if text_matches and id_matches: | ||
| 198 | found = True | ||
| 199 | break | ||
| 200 | |||
| 201 | template_vars = (expected_text, expected_pk) | ||
| 202 | assertion_failed_msg = 'message not found: ' \ | ||
| 203 | 'expected text "%s" and ID %s' % template_vars | ||
| 204 | self.assertTrue(found, assertion_failed_msg) | ||
| 205 | |||
| 206 | def _check_for_error_message(self, build, log_message): | ||
| 207 | """ | ||
| 208 | Check whether the LogMessage instance <log_message> is | ||
| 209 | represented as an HTML error in the dashboard page for the Build object | ||
| 210 | build | ||
| 211 | """ | ||
| 212 | errors = self._get_build_dashboard_errors(build) | ||
| 213 | self._check_for_log_message(errors, log_message) | ||
| 214 | |||
| 215 | def _check_labels_in_modal(self, modal, expected): | ||
| 216 | """ | ||
| 217 | Check that the text values of the <label> elements inside | ||
| 218 | the WebElement modal match the list of text values in expected | ||
| 219 | """ | ||
| 220 | # labels containing the radio buttons we're testing for | ||
| 221 | labels = modal.find_elements(By.CSS_SELECTOR,".radio") | ||
| 222 | |||
| 223 | labels_text = [lab.text for lab in labels] | ||
| 224 | self.assertEqual(len(labels_text), len(expected)) | ||
| 225 | |||
| 226 | for expected_text in expected: | ||
| 227 | self.assertTrue(expected_text in labels_text, | ||
| 228 | "Could not find %s in %s" % (expected_text, | ||
| 229 | labels_text)) | ||
| 230 | |||
| 231 | def test_exceptions_show_as_errors(self): | ||
| 232 | """ | ||
| 233 | LogMessages with level EXCEPTION should display in the errors | ||
| 234 | section of the page | ||
| 235 | """ | ||
| 236 | self._check_for_error_message(self.build1, self.exception_message) | ||
| 237 | |||
| 238 | def test_criticals_show_as_errors(self): | ||
| 239 | """ | ||
| 240 | LogMessages with level CRITICAL should display in the errors | ||
| 241 | section of the page | ||
| 242 | """ | ||
| 243 | self._check_for_error_message(self.build1, self.critical_message) | ||
| 244 | |||
| 245 | def test_edit_custom_image_button(self): | ||
| 246 | """ | ||
| 247 | A build which built two custom images should present a modal which lets | ||
| 248 | the user choose one of them to edit | ||
| 249 | """ | ||
| 250 | self._get_build_dashboard(self.build1) | ||
| 251 | |||
| 252 | # click the "edit custom image" button, which populates the modal | ||
| 253 | selector = '[data-role="edit-custom-image-trigger"]' | ||
| 254 | self.click(selector) | ||
| 255 | |||
| 256 | modal = self.driver.find_element(By.ID, 'edit-custom-image-modal') | ||
| 257 | self.wait_until_visible("#edit-custom-image-modal") | ||
| 258 | |||
| 259 | # recipes we expect to see in the edit custom image modal | ||
| 260 | expected_recipes = [ | ||
| 261 | self.custom_image_recipe1.name, | ||
| 262 | self.custom_image_recipe2.name | ||
| 263 | ] | ||
| 264 | |||
| 265 | self._check_labels_in_modal(modal, expected_recipes) | ||
| 266 | |||
| 267 | def test_new_custom_image_button(self): | ||
| 268 | """ | ||
| 269 | Check that a build with multiple images and custom images presents | ||
| 270 | all of them as options for creating a new custom image from | ||
| 271 | """ | ||
| 272 | self._get_build_dashboard(self.build1) | ||
| 273 | |||
| 274 | # click the "new custom image" button, which populates the modal | ||
| 275 | selector = '[data-role="new-custom-image-trigger"]' | ||
| 276 | self.click(selector) | ||
| 277 | |||
| 278 | modal = self.driver.find_element(By.ID,'new-custom-image-modal') | ||
| 279 | self.wait_until_visible("#new-custom-image-modal") | ||
| 280 | |||
| 281 | # recipes we expect to see in the new custom image modal | ||
| 282 | expected_recipes = [ | ||
| 283 | self.image_recipe1.name, | ||
| 284 | self.image_recipe2.name, | ||
| 285 | self.custom_image_recipe1.name, | ||
| 286 | self.custom_image_recipe2.name | ||
| 287 | ] | ||
| 288 | |||
| 289 | self._check_labels_in_modal(modal, expected_recipes) | ||
| 290 | |||
| 291 | def test_new_custom_image_button_no_image(self): | ||
| 292 | """ | ||
| 293 | Check that a build which builds non-image recipes doesn't show | ||
| 294 | the new custom image button on the dashboard. | ||
| 295 | """ | ||
| 296 | self._get_build_dashboard(self.build3) | ||
| 297 | selector = '[data-role="new-custom-image-trigger"]' | ||
| 298 | self.assertFalse(self.element_exists(selector), | ||
| 299 | 'new custom image button should not show for builds which ' \ | ||
| 300 | 'don\'t have any image recipes') | ||
| 301 | |||
| 302 | def test_left_panel(self): | ||
| 303 | """" | ||
| 304 | Builds which succeed should have a left panel and a build summary | ||
| 305 | """ | ||
| 306 | self._get_build_dashboard(self.build1) | ||
| 307 | |||
| 308 | left_panel = self.find_all('#nav') | ||
| 309 | self.assertEqual(len(left_panel), 1) | ||
| 310 | |||
| 311 | build_summary = self.find_all('[data-role="build-summary-heading"]') | ||
| 312 | self.assertEqual(len(build_summary), 1) | ||
| 313 | |||
| 314 | def test_failed_no_left_panel(self): | ||
| 315 | """ | ||
| 316 | Builds which fail should have no left panel and no build summary | ||
| 317 | """ | ||
| 318 | self._get_build_dashboard(self.build3) | ||
| 319 | |||
| 320 | left_panel = self.find_all('#nav') | ||
| 321 | self.assertEqual(len(left_panel), 0) | ||
| 322 | |||
| 323 | build_summary = self.find_all('[data-role="build-summary-heading"]') | ||
| 324 | self.assertEqual(len(build_summary), 0) | ||
| 325 | |||
| 326 | def test_failed_shows_errors_and_warnings(self): | ||
| 327 | """ | ||
| 328 | Failed builds should still show error and warning messages | ||
| 329 | """ | ||
| 330 | self._get_build_dashboard(self.build3) | ||
| 331 | |||
| 332 | errors = self.find_all('#errors div.alert-danger') | ||
| 333 | self._check_for_log_message(errors, self.error_message) | ||
| 334 | |||
| 335 | # expand the warnings area | ||
| 336 | self.click('#warning-toggle') | ||
| 337 | self.wait_until_visible('#warnings div.alert-warning') | ||
| 338 | |||
| 339 | warnings = self.find_all('#warnings div.alert-warning') | ||
| 340 | self._check_for_log_message(warnings, self.warning_message) | ||
diff --git a/bitbake/lib/toaster/tests/browser/test_builddashboard_page_artifacts.py b/bitbake/lib/toaster/tests/browser/test_builddashboard_page_artifacts.py deleted file mode 100644 index 675825bd40..0000000000 --- a/bitbake/lib/toaster/tests/browser/test_builddashboard_page_artifacts.py +++ /dev/null | |||
| @@ -1,212 +0,0 @@ | |||
| 1 | #! /usr/bin/env python3 | ||
| 2 | # | ||
| 3 | # BitBake Toaster Implementation | ||
| 4 | # | ||
| 5 | # Copyright (C) 2013-2016 Intel Corporation | ||
| 6 | # | ||
| 7 | # SPDX-License-Identifier: GPL-2.0-only | ||
| 8 | # | ||
| 9 | |||
| 10 | import os | ||
| 11 | from django.urls import reverse | ||
| 12 | from django.utils import timezone | ||
| 13 | |||
| 14 | from tests.browser.selenium_helpers import SeleniumTestCase | ||
| 15 | |||
| 16 | from orm.models import Project, Release, BitbakeVersion, Build, Target, Package | ||
| 17 | from orm.models import Target_Image_File, TargetSDKFile, TargetKernelFile | ||
| 18 | from orm.models import Target_Installed_Package, Variable | ||
| 19 | |||
| 20 | class TestBuildDashboardPageArtifacts(SeleniumTestCase): | ||
| 21 | """ Tests for artifacts on the build dashboard /build/X """ | ||
| 22 | |||
| 23 | def setUp(self): | ||
| 24 | builldir = os.environ.get('BUILDDIR', './') | ||
| 25 | bbv = BitbakeVersion.objects.create(name='bbv1', giturl=f'{builldir}/', | ||
| 26 | branch='master', dirpath="") | ||
| 27 | release = Release.objects.create(name='release1', | ||
| 28 | bitbake_version=bbv) | ||
| 29 | self.project = Project.objects.create_project(name='test project', | ||
| 30 | release=release) | ||
| 31 | |||
| 32 | def _get_build_dashboard(self, build): | ||
| 33 | """ | ||
| 34 | Navigate to the build dashboard for build | ||
| 35 | """ | ||
| 36 | url = reverse('builddashboard', args=(build.id,)) | ||
| 37 | self.get(url) | ||
| 38 | |||
| 39 | def _has_build_artifacts_heading(self): | ||
| 40 | """ | ||
| 41 | Check whether the "Build artifacts" heading is visible (True if it | ||
| 42 | is, False otherwise). | ||
| 43 | """ | ||
| 44 | return self.element_exists('[data-heading="build-artifacts"]') | ||
| 45 | |||
| 46 | def _has_images_menu_option(self): | ||
| 47 | """ | ||
| 48 | Try to get the "Images" list element from the left-hand menu in the | ||
| 49 | build dashboard, and return True if it is present, False otherwise. | ||
| 50 | """ | ||
| 51 | return self.element_exists('li.nav-header[data-menu-heading="images"]') | ||
| 52 | |||
| 53 | def test_no_artifacts(self): | ||
| 54 | """ | ||
| 55 | If a build produced no artifacts, the artifacts heading and images | ||
| 56 | menu option shouldn't show. | ||
| 57 | """ | ||
| 58 | now = timezone.now() | ||
| 59 | build = Build.objects.create(project=self.project, | ||
| 60 | started_on=now, completed_on=now, outcome=Build.SUCCEEDED) | ||
| 61 | |||
| 62 | Target.objects.create(is_image=False, build=build, task='', | ||
| 63 | target='mpfr-native') | ||
| 64 | |||
| 65 | self._get_build_dashboard(build) | ||
| 66 | |||
| 67 | # check build artifacts heading | ||
| 68 | msg = 'Build artifacts heading should not be displayed for non-image' \ | ||
| 69 | 'builds' | ||
| 70 | self.assertFalse(self._has_build_artifacts_heading(), msg) | ||
| 71 | |||
| 72 | # check "Images" option in left-hand menu (should not be there) | ||
| 73 | msg = 'Images option should not be shown in left-hand menu' | ||
| 74 | self.assertFalse(self._has_images_menu_option(), msg) | ||
| 75 | |||
| 76 | def test_sdk_artifacts(self): | ||
| 77 | """ | ||
| 78 | If a build produced SDK artifacts, they should be shown, but the section | ||
| 79 | for image files and the images menu option should be hidden. | ||
| 80 | |||
| 81 | The packages count and size should also be hidden. | ||
| 82 | """ | ||
| 83 | now = timezone.now() | ||
| 84 | build = Build.objects.create(project=self.project, | ||
| 85 | started_on=now, completed_on=timezone.now(), | ||
| 86 | outcome=Build.SUCCEEDED) | ||
| 87 | |||
| 88 | target = Target.objects.create(is_image=True, build=build, | ||
| 89 | task='populate_sdk', target='core-image-minimal') | ||
| 90 | |||
| 91 | sdk_file1 = TargetSDKFile.objects.create(target=target, | ||
| 92 | file_size=100000, | ||
| 93 | file_name='/home/foo/core-image-minimal.toolchain.sh') | ||
| 94 | |||
| 95 | sdk_file2 = TargetSDKFile.objects.create(target=target, | ||
| 96 | file_size=120000, | ||
| 97 | file_name='/home/foo/x86_64.toolchain.sh') | ||
| 98 | |||
| 99 | self._get_build_dashboard(build) | ||
| 100 | |||
| 101 | # check build artifacts heading | ||
| 102 | msg = 'Build artifacts heading should be displayed for SDK ' \ | ||
| 103 | 'builds which generate artifacts' | ||
| 104 | self.assertTrue(self._has_build_artifacts_heading(), msg) | ||
| 105 | |||
| 106 | # check "Images" option in left-hand menu (should not be there) | ||
| 107 | msg = 'Images option should not be shown in left-hand menu for ' \ | ||
| 108 | 'builds which didn\'t generate an image file' | ||
| 109 | self.assertFalse(self._has_images_menu_option(), msg) | ||
| 110 | |||
| 111 | # check links to SDK artifacts | ||
| 112 | sdk_artifact_links = self.find_all('[data-links="sdk-artifacts"] li') | ||
| 113 | self.assertEqual(len(sdk_artifact_links), 2, | ||
| 114 | 'should be links to 2 SDK artifacts') | ||
| 115 | |||
| 116 | # package count and size should not be visible, no link on | ||
| 117 | # target name | ||
| 118 | selector = '[data-value="target-package-count"]' | ||
| 119 | self.assertFalse(self.element_exists(selector), | ||
| 120 | 'package count should not be shown for non-image builds') | ||
| 121 | |||
| 122 | selector = '[data-value="target-package-size"]' | ||
| 123 | self.assertFalse(self.element_exists(selector), | ||
| 124 | 'package size should not be shown for non-image builds') | ||
| 125 | |||
| 126 | selector = '[data-link="target-packages"]' | ||
| 127 | self.assertFalse(self.element_exists(selector), | ||
| 128 | 'link to target packages should not be on target heading') | ||
| 129 | |||
| 130 | def test_image_artifacts(self): | ||
| 131 | """ | ||
| 132 | If a build produced image files, kernel artifacts, and manifests, | ||
| 133 | they should all be shown, as well as the image link in the left-hand | ||
| 134 | menu. | ||
| 135 | |||
| 136 | The packages count and size should be shown, with a link to the | ||
| 137 | package display page. | ||
| 138 | """ | ||
| 139 | now = timezone.now() | ||
| 140 | build = Build.objects.create(project=self.project, | ||
| 141 | started_on=now, completed_on=timezone.now(), | ||
| 142 | outcome=Build.SUCCEEDED) | ||
| 143 | |||
| 144 | # add a variable to the build so that it counts as "started" | ||
| 145 | Variable.objects.create(build=build, | ||
| 146 | variable_name='Christopher', | ||
| 147 | variable_value='Lee') | ||
| 148 | |||
| 149 | target = Target.objects.create(is_image=True, build=build, | ||
| 150 | task='', target='core-image-minimal', | ||
| 151 | license_manifest_path='/home/foo/license.manifest', | ||
| 152 | package_manifest_path='/home/foo/package.manifest') | ||
| 153 | |||
| 154 | image_file = Target_Image_File.objects.create(target=target, | ||
| 155 | file_name='/home/foo/core-image-minimal.ext4', file_size=9000) | ||
| 156 | |||
| 157 | kernel_file1 = TargetKernelFile.objects.create(target=target, | ||
| 158 | file_name='/home/foo/bzImage', file_size=2000) | ||
| 159 | |||
| 160 | kernel_file2 = TargetKernelFile.objects.create(target=target, | ||
| 161 | file_name='/home/foo/bzImage', file_size=2000) | ||
| 162 | |||
| 163 | package = Package.objects.create(build=build, name='foo', size=1024, | ||
| 164 | installed_name='foo1') | ||
| 165 | installed_package = Target_Installed_Package.objects.create( | ||
| 166 | target=target, package=package) | ||
| 167 | |||
| 168 | self._get_build_dashboard(build) | ||
| 169 | |||
| 170 | # check build artifacts heading | ||
| 171 | msg = 'Build artifacts heading should be displayed for image ' \ | ||
| 172 | 'builds' | ||
| 173 | self.assertTrue(self._has_build_artifacts_heading(), msg) | ||
| 174 | |||
| 175 | # check "Images" option in left-hand menu (should be there) | ||
| 176 | msg = 'Images option should be shown in left-hand menu for image builds' | ||
| 177 | self.assertTrue(self._has_images_menu_option(), msg) | ||
| 178 | |||
| 179 | # check link to image file | ||
| 180 | selector = '[data-links="image-artifacts"] li' | ||
| 181 | self.assertTrue(self.element_exists(selector), | ||
| 182 | 'should be a link to the image file (selector %s)' % selector) | ||
| 183 | |||
| 184 | # check links to kernel artifacts | ||
| 185 | kernel_artifact_links = \ | ||
| 186 | self.find_all('[data-links="kernel-artifacts"] li') | ||
| 187 | self.assertEqual(len(kernel_artifact_links), 2, | ||
| 188 | 'should be links to 2 kernel artifacts') | ||
| 189 | |||
| 190 | # check manifest links | ||
| 191 | selector = 'a[data-link="license-manifest"]' | ||
| 192 | self.assertTrue(self.element_exists(selector), | ||
| 193 | 'should be a link to the license manifest (selector %s)' % selector) | ||
| 194 | |||
| 195 | selector = 'a[data-link="package-manifest"]' | ||
| 196 | self.assertTrue(self.element_exists(selector), | ||
| 197 | 'should be a link to the package manifest (selector %s)' % selector) | ||
| 198 | |||
| 199 | # check package count and size, link on target name | ||
| 200 | selector = '[data-value="target-package-count"]' | ||
| 201 | element = self.find(selector) | ||
| 202 | self.assertEqual(element.text, '1', | ||
| 203 | 'package count should be shown for image builds') | ||
| 204 | |||
| 205 | selector = '[data-value="target-package-size"]' | ||
| 206 | element = self.find(selector) | ||
| 207 | self.assertEqual(element.text, '1.0 KB', | ||
| 208 | 'package size should be shown for image builds') | ||
| 209 | |||
| 210 | selector = '[data-link="target-packages"]' | ||
| 211 | self.assertTrue(self.element_exists(selector), | ||
| 212 | 'link to target packages should be on target heading') | ||
diff --git a/bitbake/lib/toaster/tests/browser/test_builddashboard_page_recipes.py b/bitbake/lib/toaster/tests/browser/test_builddashboard_page_recipes.py deleted file mode 100644 index 9d85ba990c..0000000000 --- a/bitbake/lib/toaster/tests/browser/test_builddashboard_page_recipes.py +++ /dev/null | |||
| @@ -1,54 +0,0 @@ | |||
| 1 | #! /usr/bin/env python3 | ||
| 2 | # | ||
| 3 | # BitBake Toaster Implementation | ||
| 4 | # | ||
| 5 | # Copyright (C) 2013-2016 Intel Corporation | ||
| 6 | # | ||
| 7 | # SPDX-License-Identifier: GPL-2.0-only | ||
| 8 | # | ||
| 9 | |||
| 10 | from django.urls import reverse | ||
| 11 | from django.utils import timezone | ||
| 12 | from tests.browser.selenium_helpers import SeleniumTestCase | ||
| 13 | from orm.models import Project, Build, Recipe, Task, Layer, Layer_Version | ||
| 14 | from orm.models import Target | ||
| 15 | |||
| 16 | class TestBuilddashboardPageRecipes(SeleniumTestCase): | ||
| 17 | """ Test build dashboard recipes sub-page """ | ||
| 18 | |||
| 19 | def setUp(self): | ||
| 20 | project = Project.objects.get_or_create_default_project() | ||
| 21 | |||
| 22 | now = timezone.now() | ||
| 23 | |||
| 24 | self.build = Build.objects.create(project=project, | ||
| 25 | started_on=now, | ||
| 26 | completed_on=now) | ||
| 27 | |||
| 28 | layer = Layer.objects.create() | ||
| 29 | |||
| 30 | layer_version = Layer_Version.objects.create(layer=layer, | ||
| 31 | build=self.build) | ||
| 32 | |||
| 33 | recipe = Recipe.objects.create(layer_version=layer_version) | ||
| 34 | |||
| 35 | task = Task.objects.create(build=self.build, recipe=recipe, order=1) | ||
| 36 | |||
| 37 | Target.objects.create(build=self.build, task=task, target='do_build') | ||
| 38 | |||
| 39 | def test_build_recipes_columns(self): | ||
| 40 | """ | ||
| 41 | Check that non-hideable columns of the table on the recipes sub-page | ||
| 42 | are disabled on the edit columns dropdown. | ||
| 43 | """ | ||
| 44 | url = reverse('recipes', args=(self.build.id,)) | ||
| 45 | self.get(url) | ||
| 46 | |||
| 47 | self.wait_until_visible('#edit-columns-button') | ||
| 48 | |||
| 49 | # check that options for the non-hideable columns are disabled | ||
| 50 | non_hideable = ['name', 'version'] | ||
| 51 | |||
| 52 | for column in non_hideable: | ||
| 53 | selector = 'input#checkbox-%s[disabled="disabled"]' % column | ||
| 54 | self.wait_until_present(selector) | ||
diff --git a/bitbake/lib/toaster/tests/browser/test_builddashboard_page_tasks.py b/bitbake/lib/toaster/tests/browser/test_builddashboard_page_tasks.py deleted file mode 100644 index 7fdf75d0a8..0000000000 --- a/bitbake/lib/toaster/tests/browser/test_builddashboard_page_tasks.py +++ /dev/null | |||
| @@ -1,53 +0,0 @@ | |||
| 1 | #! /usr/bin/env python3 | ||
| 2 | # | ||
| 3 | # BitBake Toaster Implementation | ||
| 4 | # | ||
| 5 | # Copyright (C) 2013-2016 Intel Corporation | ||
| 6 | # | ||
| 7 | # SPDX-License-Identifier: GPL-2.0-only | ||
| 8 | # | ||
| 9 | |||
| 10 | from django.urls import reverse | ||
| 11 | from django.utils import timezone | ||
| 12 | from tests.browser.selenium_helpers import SeleniumTestCase | ||
| 13 | from orm.models import Project, Build, Recipe, Task, Layer, Layer_Version | ||
| 14 | from orm.models import Target | ||
| 15 | |||
| 16 | class TestBuilddashboardPageTasks(SeleniumTestCase): | ||
| 17 | """ Test build dashboard tasks sub-page """ | ||
| 18 | |||
| 19 | def setUp(self): | ||
| 20 | project = Project.objects.get_or_create_default_project() | ||
| 21 | |||
| 22 | now = timezone.now() | ||
| 23 | |||
| 24 | self.build = Build.objects.create(project=project, | ||
| 25 | started_on=now, | ||
| 26 | completed_on=now) | ||
| 27 | |||
| 28 | layer = Layer.objects.create() | ||
| 29 | |||
| 30 | layer_version = Layer_Version.objects.create(layer=layer) | ||
| 31 | |||
| 32 | recipe = Recipe.objects.create(layer_version=layer_version) | ||
| 33 | |||
| 34 | task = Task.objects.create(build=self.build, recipe=recipe, order=1) | ||
| 35 | |||
| 36 | Target.objects.create(build=self.build, task=task, target='do_build') | ||
| 37 | |||
| 38 | def test_build_tasks_columns(self): | ||
| 39 | """ | ||
| 40 | Check that non-hideable columns of the table on the tasks sub-page | ||
| 41 | are disabled on the edit columns dropdown. | ||
| 42 | """ | ||
| 43 | url = reverse('tasks', args=(self.build.id,)) | ||
| 44 | self.get(url) | ||
| 45 | |||
| 46 | self.wait_until_visible('#edit-columns-button') | ||
| 47 | |||
| 48 | # check that options for the non-hideable columns are disabled | ||
| 49 | non_hideable = ['order', 'task_name', 'recipe__name'] | ||
| 50 | |||
| 51 | for column in non_hideable: | ||
| 52 | selector = 'input#checkbox-%s[disabled="disabled"]' % column | ||
| 53 | self.wait_until_present(selector) | ||
diff --git a/bitbake/lib/toaster/tests/browser/test_delete_project.py b/bitbake/lib/toaster/tests/browser/test_delete_project.py deleted file mode 100644 index 1941777ccc..0000000000 --- a/bitbake/lib/toaster/tests/browser/test_delete_project.py +++ /dev/null | |||
| @@ -1,103 +0,0 @@ | |||
| 1 | #!/usr/bin/env python3 | ||
| 2 | # -*- coding: utf-8 -*- | ||
| 3 | # BitBake Toaster UI tests implementation | ||
| 4 | # | ||
| 5 | # Copyright (C) 2023 Savoir-faire Linux Inc | ||
| 6 | # | ||
| 7 | # SPDX-License-Identifier: GPL-2.0-only | ||
| 8 | |||
| 9 | import pytest | ||
| 10 | from django.urls import reverse | ||
| 11 | from selenium.webdriver.support.ui import Select | ||
| 12 | from tests.browser.selenium_helpers import SeleniumTestCase | ||
| 13 | from orm.models import BitbakeVersion, Project, Release | ||
| 14 | from selenium.webdriver.common.by import By | ||
| 15 | |||
| 16 | class TestDeleteProject(SeleniumTestCase): | ||
| 17 | |||
| 18 | def setUp(self): | ||
| 19 | bitbake, _ = BitbakeVersion.objects.get_or_create( | ||
| 20 | name="master", | ||
| 21 | giturl="git://master", | ||
| 22 | branch="master", | ||
| 23 | dirpath="master") | ||
| 24 | |||
| 25 | self.release, _ = Release.objects.get_or_create( | ||
| 26 | name="master", | ||
| 27 | description="Yocto Project master", | ||
| 28 | branch_name="master", | ||
| 29 | helptext="latest", | ||
| 30 | bitbake_version=bitbake) | ||
| 31 | |||
| 32 | Release.objects.get_or_create( | ||
| 33 | name="foo", | ||
| 34 | description="Yocto Project foo", | ||
| 35 | branch_name="foo", | ||
| 36 | helptext="latest", | ||
| 37 | bitbake_version=bitbake) | ||
| 38 | |||
| 39 | @pytest.mark.django_db | ||
| 40 | def test_delete_project(self): | ||
| 41 | """ Test delete a project | ||
| 42 | - Check delete modal is visible | ||
| 43 | - Check delete modal has right text | ||
| 44 | - Confirm delete | ||
| 45 | - Check project is deleted | ||
| 46 | """ | ||
| 47 | project_name = "project_to_delete" | ||
| 48 | url = reverse('newproject') | ||
| 49 | self.get(url) | ||
| 50 | self.enter_text('#new-project-name', project_name) | ||
| 51 | select = Select(self.find('#projectversion')) | ||
| 52 | select.select_by_value(str(self.release.pk)) | ||
| 53 | self.click("#create-project-button") | ||
| 54 | # We should get redirected to the new project's page with the | ||
| 55 | # notification at the top | ||
| 56 | element = self.wait_until_visible('#project-created-notification') | ||
| 57 | self.assertTrue(project_name in element.text, | ||
| 58 | "New project name not in new project notification") | ||
| 59 | self.assertTrue(Project.objects.filter(name=project_name).count(), | ||
| 60 | "New project not found in database") | ||
| 61 | |||
| 62 | # Delete project | ||
| 63 | delete_project_link = self.driver.find_element( | ||
| 64 | By.XPATH, '//a[@href="#delete-project-modal"]') | ||
| 65 | delete_project_link.click() | ||
| 66 | |||
| 67 | # Check delete modal is visible | ||
| 68 | self.wait_until_visible('#delete-project-modal') | ||
| 69 | |||
| 70 | # Check delete modal has right text | ||
| 71 | modal_header_text = self.find('#delete-project-modal .modal-header').text | ||
| 72 | self.assertTrue( | ||
| 73 | "Are you sure you want to delete this project?" in modal_header_text, | ||
| 74 | "Delete project modal header text is wrong") | ||
| 75 | |||
| 76 | modal_body_text = self.find('#delete-project-modal .modal-body').text | ||
| 77 | self.assertTrue( | ||
| 78 | "Cancel its builds currently in progress" in modal_body_text, | ||
| 79 | "Modal body doesn't contain: Cancel its builds currently in progress") | ||
| 80 | self.assertTrue( | ||
| 81 | "Remove its configuration information" in modal_body_text, | ||
| 82 | "Modal body doesn't contain: Remove its configuration information") | ||
| 83 | self.assertTrue( | ||
| 84 | "Remove its imported layers" in modal_body_text, | ||
| 85 | "Modal body doesn't contain: Remove its imported layers") | ||
| 86 | self.assertTrue( | ||
| 87 | "Remove its custom images" in modal_body_text, | ||
| 88 | "Modal body doesn't contain: Remove its custom images") | ||
| 89 | self.assertTrue( | ||
| 90 | "Remove all its build information" in modal_body_text, | ||
| 91 | "Modal body doesn't contain: Remove all its build information") | ||
| 92 | |||
| 93 | # Confirm delete | ||
| 94 | delete_btn = self.find('#delete-project-confirmed') | ||
| 95 | delete_btn.click() | ||
| 96 | |||
| 97 | # Check project is deleted | ||
| 98 | self.wait_until_visible('#change-notification') | ||
| 99 | delete_notification = self.find('#change-notification-msg') | ||
| 100 | self.assertTrue("You have deleted 1 project:" in delete_notification.text) | ||
| 101 | self.assertTrue(project_name in delete_notification.text) | ||
| 102 | self.assertFalse(Project.objects.filter(name=project_name).exists(), | ||
| 103 | "Project not deleted from database") | ||
diff --git a/bitbake/lib/toaster/tests/browser/test_js_unit_tests.py b/bitbake/lib/toaster/tests/browser/test_js_unit_tests.py deleted file mode 100644 index e6163bb3b2..0000000000 --- a/bitbake/lib/toaster/tests/browser/test_js_unit_tests.py +++ /dev/null | |||
| @@ -1,45 +0,0 @@ | |||
| 1 | #! /usr/bin/env python3 | ||
| 2 | # | ||
| 3 | # BitBake Toaster Implementation | ||
| 4 | # | ||
| 5 | # Copyright (C) 2013-2016 Intel Corporation | ||
| 6 | # | ||
| 7 | # SPDX-License-Identifier: GPL-2.0-only | ||
| 8 | # | ||
| 9 | |||
| 10 | """ | ||
| 11 | Run the js unit tests | ||
| 12 | """ | ||
| 13 | |||
| 14 | from django.urls import reverse | ||
| 15 | from tests.browser.selenium_helpers import SeleniumTestCase | ||
| 16 | import logging | ||
| 17 | |||
| 18 | logger = logging.getLogger("toaster") | ||
| 19 | |||
| 20 | |||
| 21 | class TestJsUnitTests(SeleniumTestCase): | ||
| 22 | """ Test landing page shows the Toaster brand """ | ||
| 23 | |||
| 24 | fixtures = ['toastergui-unittest-data'] | ||
| 25 | |||
| 26 | def test_that_js_unit_tests_pass(self): | ||
| 27 | url = reverse('js-unit-tests') | ||
| 28 | self.get(url) | ||
| 29 | self.wait_until_present('#qunit-testresult .failed') | ||
| 30 | |||
| 31 | failed = self.find("#qunit-testresult .failed").text | ||
| 32 | passed = self.find("#qunit-testresult .passed").text | ||
| 33 | total = self.find("#qunit-testresult .total").text | ||
| 34 | |||
| 35 | logger.info("Js unit tests completed %s out of %s passed, %s failed", | ||
| 36 | passed, | ||
| 37 | total, | ||
| 38 | failed) | ||
| 39 | |||
| 40 | failed_tests = self.find_all("li .fail .test-message") | ||
| 41 | for fail in failed_tests: | ||
| 42 | logger.error("JS unit test failed: %s" % fail.text) | ||
| 43 | |||
| 44 | self.assertEqual(failed, '0', | ||
| 45 | "%s JS unit tests failed" % failed) | ||
diff --git a/bitbake/lib/toaster/tests/browser/test_landing_page.py b/bitbake/lib/toaster/tests/browser/test_landing_page.py deleted file mode 100644 index 210359d561..0000000000 --- a/bitbake/lib/toaster/tests/browser/test_landing_page.py +++ /dev/null | |||
| @@ -1,233 +0,0 @@ | |||
| 1 | #! /usr/bin/env python3 | ||
| 2 | # | ||
| 3 | # BitBake Toaster Implementation | ||
| 4 | # | ||
| 5 | # SPDX-License-Identifier: GPL-2.0-only | ||
| 6 | # | ||
| 7 | # Copyright (C) 2013-2016 Intel Corporation | ||
| 8 | # | ||
| 9 | |||
| 10 | from django.urls import reverse | ||
| 11 | from django.utils import timezone | ||
| 12 | from tests.browser.selenium_helpers import SeleniumTestCase | ||
| 13 | from selenium.webdriver.common.by import By | ||
| 14 | |||
| 15 | from orm.models import Layer, Layer_Version, Project, Build | ||
| 16 | |||
| 17 | |||
| 18 | class TestLandingPage(SeleniumTestCase): | ||
| 19 | """ Tests for redirects on the landing page """ | ||
| 20 | |||
| 21 | PROJECT_NAME = 'test project' | ||
| 22 | LANDING_PAGE_TITLE = 'This is Toaster' | ||
| 23 | CLI_BUILDS_PROJECT_NAME = 'command line builds' | ||
| 24 | |||
| 25 | def setUp(self): | ||
| 26 | """ Add default project manually """ | ||
| 27 | self.project = Project.objects.create_project( | ||
| 28 | self.CLI_BUILDS_PROJECT_NAME, | ||
| 29 | None | ||
| 30 | ) | ||
| 31 | self.project.is_default = True | ||
| 32 | self.project.save() | ||
| 33 | |||
| 34 | def test_icon_info_visible_and_clickable(self): | ||
| 35 | """ Test that the information icon is visible and clickable """ | ||
| 36 | self.get(reverse('landing')) | ||
| 37 | self.wait_until_visible('#toaster-version-info-sign') | ||
| 38 | info_sign = self.find('#toaster-version-info-sign') | ||
| 39 | |||
| 40 | # check that the info sign is visible | ||
| 41 | self.assertTrue(info_sign.is_displayed()) | ||
| 42 | |||
| 43 | # check that the info sign is clickable | ||
| 44 | # and info modal is appearing when clicking on the info sign | ||
| 45 | info_sign.click() # click on the info sign make attribute 'aria-describedby' visible | ||
| 46 | info_model_id = info_sign.get_attribute('aria-describedby') | ||
| 47 | self.wait_until_visible(f'#{info_model_id}') | ||
| 48 | info_modal = self.find(f'#{info_model_id}') | ||
| 49 | self.assertTrue(info_modal.is_displayed()) | ||
| 50 | self.assertTrue("Toaster version information" in info_modal.text) | ||
| 51 | |||
| 52 | def test_documentation_link_displayed(self): | ||
| 53 | """ Test that the documentation link is displayed """ | ||
| 54 | self.get(reverse('landing')) | ||
| 55 | self.wait_until_visible('#navbar-docs') | ||
| 56 | documentation_link = self.find('#navbar-docs > a') | ||
| 57 | |||
| 58 | # check that the documentation link is visible | ||
| 59 | self.assertTrue(documentation_link.is_displayed()) | ||
| 60 | |||
| 61 | # check browser open new tab toaster manual when clicking on the documentation link | ||
| 62 | self.assertEqual(documentation_link.get_attribute('target'), '_blank') | ||
| 63 | self.assertEqual( | ||
| 64 | documentation_link.get_attribute('href'), | ||
| 65 | 'http://docs.yoctoproject.org/toaster-manual/index.html#toaster-user-manual') | ||
| 66 | self.assertTrue("Documentation" in documentation_link.text) | ||
| 67 | |||
| 68 | def test_openembedded_jumbotron_link_visible_and_clickable(self): | ||
| 69 | """ Test OpenEmbedded link jumbotron is visible and clickable: """ | ||
| 70 | self.get(reverse('landing')) | ||
| 71 | self.wait_until_visible('.jumbotron') | ||
| 72 | jumbotron = self.find('.jumbotron') | ||
| 73 | |||
| 74 | # check OpenEmbedded | ||
| 75 | openembedded = jumbotron.find_element(By.LINK_TEXT, 'OpenEmbedded') | ||
| 76 | self.assertTrue(openembedded.is_displayed()) | ||
| 77 | openembedded.click() | ||
| 78 | self.assertTrue("openembedded.org" in self.driver.current_url) | ||
| 79 | |||
| 80 | def test_bitbake_jumbotron_link_visible_and_clickable(self): | ||
| 81 | """ Test BitBake link jumbotron is visible and clickable: """ | ||
| 82 | self.get(reverse('landing')) | ||
| 83 | self.wait_until_visible('.jumbotron') | ||
| 84 | jumbotron = self.find('.jumbotron') | ||
| 85 | |||
| 86 | # check BitBake | ||
| 87 | bitbake = jumbotron.find_element(By.LINK_TEXT, 'BitBake') | ||
| 88 | self.assertTrue(bitbake.is_displayed()) | ||
| 89 | bitbake.click() | ||
| 90 | self.assertTrue( | ||
| 91 | "docs.yoctoproject.org/bitbake.html" in self.driver.current_url) | ||
| 92 | |||
| 93 | def test_yoctoproject_jumbotron_link_visible_and_clickable(self): | ||
| 94 | """ Test Yocto Project link jumbotron is visible and clickable: """ | ||
| 95 | self.get(reverse('landing')) | ||
| 96 | self.wait_until_visible('.jumbotron') | ||
| 97 | jumbotron = self.find('.jumbotron') | ||
| 98 | |||
| 99 | # check Yocto Project | ||
| 100 | yoctoproject = jumbotron.find_element(By.LINK_TEXT, 'Yocto Project') | ||
| 101 | self.assertTrue(yoctoproject.is_displayed()) | ||
| 102 | yoctoproject.click() | ||
| 103 | self.assertTrue("yoctoproject.org" in self.driver.current_url) | ||
| 104 | |||
| 105 | def test_link_setup_using_toaster_visible_and_clickable(self): | ||
| 106 | """ Test big magenta button setting up and using toaster link in jumbotron | ||
| 107 | if visible and clickable | ||
| 108 | """ | ||
| 109 | self.get(reverse('landing')) | ||
| 110 | self.wait_until_visible('.jumbotron') | ||
| 111 | jumbotron = self.find('.jumbotron') | ||
| 112 | |||
| 113 | # check Big magenta button | ||
| 114 | big_magenta_button = jumbotron.find_element(By.LINK_TEXT, | ||
| 115 | 'Toaster is ready to capture your command line builds' | ||
| 116 | ) | ||
| 117 | self.assertTrue(big_magenta_button.is_displayed()) | ||
| 118 | big_magenta_button.click() | ||
| 119 | self.assertTrue( | ||
| 120 | "docs.yoctoproject.org/toaster-manual/setup-and-use.html#setting-up-and-using-toaster" in self.driver.current_url) | ||
| 121 | |||
| 122 | def test_link_create_new_project_in_jumbotron_visible_and_clickable(self): | ||
| 123 | """ Test big blue button create new project jumbotron if visible and clickable """ | ||
| 124 | # Create a layer and a layer version to make visible the big blue button | ||
| 125 | layer = Layer.objects.create(name='bar') | ||
| 126 | Layer_Version.objects.create(layer=layer) | ||
| 127 | |||
| 128 | self.get(reverse('landing')) | ||
| 129 | self.wait_until_visible('.jumbotron') | ||
| 130 | jumbotron = self.find('.jumbotron') | ||
| 131 | |||
| 132 | # check Big Blue button | ||
| 133 | big_blue_button = jumbotron.find_element(By.LINK_TEXT, | ||
| 134 | 'Create your first Toaster project to run manage builds' | ||
| 135 | ) | ||
| 136 | self.assertTrue(big_blue_button.is_displayed()) | ||
| 137 | big_blue_button.click() | ||
| 138 | self.assertTrue("toastergui/newproject/" in self.driver.current_url) | ||
| 139 | |||
| 140 | def test_toaster_manual_link_visible_and_clickable(self): | ||
| 141 | """ Test Read the Toaster manual link jumbotron is visible and clickable: """ | ||
| 142 | self.get(reverse('landing')) | ||
| 143 | self.wait_until_visible('.jumbotron') | ||
| 144 | jumbotron = self.find('.jumbotron') | ||
| 145 | |||
| 146 | # check Read the Toaster manual | ||
| 147 | toaster_manual = jumbotron.find_element( | ||
| 148 | By.LINK_TEXT, 'Read the Toaster manual') | ||
| 149 | self.assertTrue(toaster_manual.is_displayed()) | ||
| 150 | toaster_manual.click() | ||
| 151 | self.assertTrue( | ||
| 152 | "https://docs.yoctoproject.org/toaster-manual/index.html#toaster-user-manual" in self.driver.current_url) | ||
| 153 | |||
| 154 | def test_contrib_to_toaster_link_visible_and_clickable(self): | ||
| 155 | """ Test Contribute to Toaster link jumbotron is visible and clickable: """ | ||
| 156 | self.get(reverse('landing')) | ||
| 157 | self.wait_until_visible('.jumbotron') | ||
| 158 | jumbotron = self.find('.jumbotron') | ||
| 159 | |||
| 160 | # check Contribute to Toaster | ||
| 161 | contribute_to_toaster = jumbotron.find_element( | ||
| 162 | By.LINK_TEXT, 'Contribute to Toaster') | ||
| 163 | self.assertTrue(contribute_to_toaster.is_displayed()) | ||
| 164 | contribute_to_toaster.click() | ||
| 165 | self.assertTrue( | ||
| 166 | "wiki.yoctoproject.org/wiki/contribute_to_toaster" in str(self.driver.current_url).lower()) | ||
| 167 | |||
| 168 | def test_only_default_project(self): | ||
| 169 | """ | ||
| 170 | No projects except default | ||
| 171 | => should see the landing page | ||
| 172 | """ | ||
| 173 | self.get(reverse('landing')) | ||
| 174 | self.wait_until_visible('.jumbotron') | ||
| 175 | self.assertTrue(self.LANDING_PAGE_TITLE in self.get_page_source()) | ||
| 176 | |||
| 177 | def test_default_project_has_build(self): | ||
| 178 | """ | ||
| 179 | Default project has a build, no other projects | ||
| 180 | => should see the builds page | ||
| 181 | """ | ||
| 182 | now = timezone.now() | ||
| 183 | build = Build.objects.create(project=self.project, | ||
| 184 | started_on=now, | ||
| 185 | completed_on=now) | ||
| 186 | build.save() | ||
| 187 | |||
| 188 | self.get(reverse('landing')) | ||
| 189 | |||
| 190 | elements = self.find_all('#allbuildstable') | ||
| 191 | self.assertEqual(len(elements), 1, 'should redirect to builds') | ||
| 192 | content = self.get_page_source() | ||
| 193 | self.assertFalse(self.PROJECT_NAME in content, | ||
| 194 | 'should not show builds for project %s' % self.PROJECT_NAME) | ||
| 195 | self.assertTrue(self.CLI_BUILDS_PROJECT_NAME in content, | ||
| 196 | 'should show builds for cli project') | ||
| 197 | |||
| 198 | def test_user_project_exists(self): | ||
| 199 | """ | ||
| 200 | User has added a project (without builds) | ||
| 201 | => should see the projects page | ||
| 202 | """ | ||
| 203 | user_project = Project.objects.create_project('foo', None) | ||
| 204 | user_project.save() | ||
| 205 | |||
| 206 | self.get(reverse('landing')) | ||
| 207 | self.wait_until_visible('#projectstable') | ||
| 208 | |||
| 209 | elements = self.find_all('#projectstable') | ||
| 210 | self.assertEqual(len(elements), 1, 'should redirect to projects') | ||
| 211 | |||
| 212 | def test_user_project_has_build(self): | ||
| 213 | """ | ||
| 214 | User has added a project (with builds), command line builds doesn't | ||
| 215 | => should see the builds page | ||
| 216 | """ | ||
| 217 | user_project = Project.objects.create_project(self.PROJECT_NAME, None) | ||
| 218 | user_project.save() | ||
| 219 | |||
| 220 | now = timezone.now() | ||
| 221 | build = Build.objects.create(project=user_project, | ||
| 222 | started_on=now, | ||
| 223 | completed_on=now) | ||
| 224 | build.save() | ||
| 225 | |||
| 226 | self.get(reverse('landing')) | ||
| 227 | |||
| 228 | self.wait_until_visible("#latest-builds") | ||
| 229 | elements = self.find_all('#allbuildstable') | ||
| 230 | self.assertEqual(len(elements), 1, 'should redirect to builds') | ||
| 231 | content = self.get_page_source() | ||
| 232 | self.assertTrue(self.PROJECT_NAME in content, | ||
| 233 | 'should show builds for project %s' % self.PROJECT_NAME) | ||
diff --git a/bitbake/lib/toaster/tests/browser/test_layerdetails_page.py b/bitbake/lib/toaster/tests/browser/test_layerdetails_page.py deleted file mode 100644 index 69493833f4..0000000000 --- a/bitbake/lib/toaster/tests/browser/test_layerdetails_page.py +++ /dev/null | |||
| @@ -1,223 +0,0 @@ | |||
| 1 | #! /usr/bin/env python3 | ||
| 2 | # | ||
| 3 | # BitBake Toaster Implementation | ||
| 4 | # | ||
| 5 | # SPDX-License-Identifier: GPL-2.0-only | ||
| 6 | # | ||
| 7 | # Copyright (C) 2013-2016 Intel Corporation | ||
| 8 | # | ||
| 9 | |||
| 10 | from django.urls import reverse | ||
| 11 | from selenium.common.exceptions import ElementClickInterceptedException, TimeoutException | ||
| 12 | from tests.browser.selenium_helpers import SeleniumTestCase | ||
| 13 | |||
| 14 | from orm.models import Layer, Layer_Version, Project, LayerSource, Release | ||
| 15 | from orm.models import BitbakeVersion | ||
| 16 | |||
| 17 | from selenium.webdriver.support import expected_conditions as EC | ||
| 18 | from selenium.webdriver.support.ui import WebDriverWait | ||
| 19 | from selenium.webdriver.common.by import By | ||
| 20 | |||
| 21 | |||
| 22 | class TestLayerDetailsPage(SeleniumTestCase): | ||
| 23 | """ Test layerdetails page works correctly """ | ||
| 24 | |||
| 25 | def __init__(self, *args, **kwargs): | ||
| 26 | super(TestLayerDetailsPage, self).__init__(*args, **kwargs) | ||
| 27 | |||
| 28 | self.initial_values = None | ||
| 29 | self.url = None | ||
| 30 | self.imported_layer_version = None | ||
| 31 | |||
| 32 | def setUp(self): | ||
| 33 | release = Release.objects.create( | ||
| 34 | name='baz', | ||
| 35 | bitbake_version=BitbakeVersion.objects.create(name='v1') | ||
| 36 | ) | ||
| 37 | |||
| 38 | # project to add new custom images to | ||
| 39 | self.project = Project.objects.create(name='foo', release=release) | ||
| 40 | |||
| 41 | name = "meta-imported" | ||
| 42 | vcs_url = "git://example.com/meta-imported" | ||
| 43 | subdir = "/layer" | ||
| 44 | gitrev = "d33d" | ||
| 45 | summary = "A imported layer" | ||
| 46 | description = "This was imported" | ||
| 47 | |||
| 48 | imported_layer = Layer.objects.create(name=name, | ||
| 49 | vcs_url=vcs_url, | ||
| 50 | summary=summary, | ||
| 51 | description=description) | ||
| 52 | |||
| 53 | self.imported_layer_version = Layer_Version.objects.create( | ||
| 54 | layer=imported_layer, | ||
| 55 | layer_source=LayerSource.TYPE_IMPORTED, | ||
| 56 | branch=gitrev, | ||
| 57 | commit=gitrev, | ||
| 58 | dirpath=subdir, | ||
| 59 | project=self.project) | ||
| 60 | |||
| 61 | self.initial_values = [name, vcs_url, subdir, gitrev, summary, | ||
| 62 | description] | ||
| 63 | self.url = reverse('layerdetails', | ||
| 64 | args=(self.project.pk, | ||
| 65 | self.imported_layer_version.pk)) | ||
| 66 | |||
| 67 | def test_edit_layerdetails_page(self): | ||
| 68 | """ Edit all the editable fields for the layer refresh the page and | ||
| 69 | check that the new values exist""" | ||
| 70 | |||
| 71 | self.get(self.url) | ||
| 72 | self.wait_until_visible("#add-remove-layer-btn") | ||
| 73 | |||
| 74 | self.click("#add-remove-layer-btn") | ||
| 75 | self.click("#edit-layer-source") | ||
| 76 | self.click("#repo") | ||
| 77 | |||
| 78 | self.wait_until_visible("#layer-git-repo-url") | ||
| 79 | |||
| 80 | # Open every edit box | ||
| 81 | for btn in self.find_all("dd .glyphicon-edit"): | ||
| 82 | btn.click() | ||
| 83 | |||
| 84 | # Wait for the inputs to become visible after animation | ||
| 85 | self.wait_until_visible("#layer-git input[type=text]") | ||
| 86 | self.wait_until_visible("dd textarea") | ||
| 87 | self.wait_until_visible("dd .change-btn") | ||
| 88 | |||
| 89 | # Edit each value | ||
| 90 | for inputs in self.find_all("#layer-git input[type=text]") + \ | ||
| 91 | self.find_all("dd textarea"): | ||
| 92 | # ignore the tt inputs (twitter typeahead input) | ||
| 93 | if "tt-" in inputs.get_attribute("class"): | ||
| 94 | continue | ||
| 95 | |||
| 96 | value = inputs.get_attribute("value") | ||
| 97 | |||
| 98 | self.assertTrue(value in self.initial_values, | ||
| 99 | "Expecting any of \"%s\"but got \"%s\"" % | ||
| 100 | (self.initial_values, value)) | ||
| 101 | |||
| 102 | # Make sure the input visible beofre sending keys | ||
| 103 | self.wait_until_clickable("#layer-git input[type=text]") | ||
| 104 | inputs.send_keys("-edited") | ||
| 105 | |||
| 106 | # Save the new values | ||
| 107 | for save_btn in self.find_all(".change-btn"): | ||
| 108 | save_btn.click() | ||
| 109 | |||
| 110 | self.wait_until_visible("#save-changes-for-switch") | ||
| 111 | btn_save_chg_for_switch = self.wait_until_clickable( | ||
| 112 | "#save-changes-for-switch") | ||
| 113 | btn_save_chg_for_switch.click() | ||
| 114 | |||
| 115 | self.wait_until_visible("#edit-layer-source") | ||
| 116 | |||
| 117 | # Refresh the page to see if the new values are returned | ||
| 118 | self.get(self.url) | ||
| 119 | |||
| 120 | new_values = ["%s-edited" % old_val | ||
| 121 | for old_val in self.initial_values] | ||
| 122 | |||
| 123 | for inputs in self.find_all('#layer-git input[type="text"]') + \ | ||
| 124 | self.find_all('dd textarea'): | ||
| 125 | # ignore the tt inputs (twitter typeahead input) | ||
| 126 | if "tt-" in inputs.get_attribute("class"): | ||
| 127 | continue | ||
| 128 | |||
| 129 | value = inputs.get_attribute("value") | ||
| 130 | |||
| 131 | self.assertTrue(value in new_values, | ||
| 132 | "Expecting any of \"%s\" but got \"%s\"" % | ||
| 133 | (new_values, value)) | ||
| 134 | |||
| 135 | # Now convert it to a local layer | ||
| 136 | self.click("#edit-layer-source") | ||
| 137 | self.click("#dir") | ||
| 138 | dir_input = self.wait_until_visible("#layer-dir-path-in-details") | ||
| 139 | |||
| 140 | new_dir = "/home/test/my-meta-dir" | ||
| 141 | dir_input.send_keys(new_dir) | ||
| 142 | |||
| 143 | self.wait_until_visible("#save-changes-for-switch") | ||
| 144 | btn_save_chg_for_switch = self.wait_until_clickable( | ||
| 145 | "#save-changes-for-switch") | ||
| 146 | btn_save_chg_for_switch.click() | ||
| 147 | |||
| 148 | self.wait_until_visible("#edit-layer-source") | ||
| 149 | |||
| 150 | # Refresh the page to see if the new values are returned | ||
| 151 | self.get(self.url) | ||
| 152 | dir_input = self.find("#layer-dir-path-in-details") | ||
| 153 | self.assertTrue(new_dir in dir_input.get_attribute("value"), | ||
| 154 | "Expected %s in the dir value for layer directory" % | ||
| 155 | new_dir) | ||
| 156 | |||
| 157 | |||
| 158 | def test_delete_layer(self): | ||
| 159 | """ Delete the layer """ | ||
| 160 | |||
| 161 | self.get(self.url) | ||
| 162 | |||
| 163 | # Wait for the tables to load to avoid a race condition where the | ||
| 164 | # toaster tables have made an async request. If the layer is deleted | ||
| 165 | # before the request finishes it will cause an exception and fail this | ||
| 166 | # test. | ||
| 167 | wait = WebDriverWait(self.driver, 30) | ||
| 168 | |||
| 169 | wait.until(EC.text_to_be_present_in_element( | ||
| 170 | (By.CLASS_NAME, | ||
| 171 | "table-count-recipestable"), "0")) | ||
| 172 | |||
| 173 | wait.until(EC.text_to_be_present_in_element( | ||
| 174 | (By.CLASS_NAME, | ||
| 175 | "table-count-machinestable"), "0")) | ||
| 176 | |||
| 177 | self.click('a[data-target="#delete-layer-modal"]') | ||
| 178 | self.wait_until_visible("#delete-layer-modal") | ||
| 179 | self.click("#layer-delete-confirmed") | ||
| 180 | |||
| 181 | notification = self.wait_until_visible("#change-notification-msg") | ||
| 182 | expected_text = "You have deleted 1 layer from your project: %s" % \ | ||
| 183 | self.imported_layer_version.layer.name | ||
| 184 | |||
| 185 | self.assertTrue(expected_text in notification.text, | ||
| 186 | "Expected notification text \"%s\" not found instead" | ||
| 187 | "it was \"%s\"" % | ||
| 188 | (expected_text, notification.text)) | ||
| 189 | |||
| 190 | def test_addrm_to_project(self): | ||
| 191 | self.get(self.url) | ||
| 192 | |||
| 193 | # Add the layer | ||
| 194 | self.wait_until_clickable("#add-remove-layer-btn") | ||
| 195 | self.click("#add-remove-layer-btn") | ||
| 196 | |||
| 197 | notification = self.wait_until_visible("#change-notification-msg") | ||
| 198 | |||
| 199 | expected_text = "You have added 1 layer to your project: %s" % \ | ||
| 200 | self.imported_layer_version.layer.name | ||
| 201 | |||
| 202 | self.assertIn(expected_text, notification.text, | ||
| 203 | "Expected notification text %s not found was " | ||
| 204 | " \"%s\" instead" % | ||
| 205 | (expected_text, notification.text)) | ||
| 206 | |||
| 207 | hide_button = self.find('#hide-alert') | ||
| 208 | hide_button.click() | ||
| 209 | self.wait_until_not_visible('#change-notification') | ||
| 210 | |||
| 211 | # Remove the layer | ||
| 212 | self.wait_until_clickable("#add-remove-layer-btn") | ||
| 213 | self.click("#add-remove-layer-btn") | ||
| 214 | |||
| 215 | notification = self.wait_until_visible("#change-notification-msg") | ||
| 216 | |||
| 217 | expected_text = "You have removed 1 layer from your project: %s" % \ | ||
| 218 | self.imported_layer_version.layer.name | ||
| 219 | |||
| 220 | self.assertIn(expected_text, notification.text, | ||
| 221 | "Expected notification text %s not found was " | ||
| 222 | " \"%s\" instead" % | ||
| 223 | (expected_text, notification.text)) | ||
diff --git a/bitbake/lib/toaster/tests/browser/test_most_recent_builds_states.py b/bitbake/lib/toaster/tests/browser/test_most_recent_builds_states.py deleted file mode 100644 index d7a4c34532..0000000000 --- a/bitbake/lib/toaster/tests/browser/test_most_recent_builds_states.py +++ /dev/null | |||
| @@ -1,201 +0,0 @@ | |||
| 1 | #! /usr/bin/env python3 | ||
| 2 | # | ||
| 3 | # BitBake Toaster Implementation | ||
| 4 | # | ||
| 5 | # SPDX-License-Identifier: GPL-2.0-only | ||
| 6 | # | ||
| 7 | # Copyright (C) 2013-2016 Intel Corporation | ||
| 8 | # | ||
| 9 | from django.urls import reverse | ||
| 10 | from django.utils import timezone | ||
| 11 | from tests.browser.selenium_helpers import SeleniumTestCase | ||
| 12 | from tests.browser.selenium_helpers_base import Wait | ||
| 13 | from orm.models import Project, Build, Task, Recipe, Layer, Layer_Version | ||
| 14 | from bldcontrol.models import BuildRequest | ||
| 15 | |||
| 16 | from selenium.webdriver.common.by import By | ||
| 17 | |||
| 18 | class TestMostRecentBuildsStates(SeleniumTestCase): | ||
| 19 | """ Test states update correctly in most recent builds area """ | ||
| 20 | |||
| 21 | def _create_build_request(self): | ||
| 22 | project = Project.objects.get_or_create_default_project() | ||
| 23 | |||
| 24 | now = timezone.now() | ||
| 25 | |||
| 26 | build = Build.objects.create(project=project, build_name='fakebuild', | ||
| 27 | started_on=now, completed_on=now) | ||
| 28 | |||
| 29 | return BuildRequest.objects.create(build=build, project=project, | ||
| 30 | state=BuildRequest.REQ_QUEUED) | ||
| 31 | |||
| 32 | def _create_recipe(self): | ||
| 33 | """ Add a recipe to the database and return it """ | ||
| 34 | layer = Layer.objects.create() | ||
| 35 | layer_version = Layer_Version.objects.create(layer=layer) | ||
| 36 | return Recipe.objects.create(name='foo', layer_version=layer_version) | ||
| 37 | |||
| 38 | def _check_build_states(self, build_request): | ||
| 39 | recipes_to_parse = 10 | ||
| 40 | url = reverse('all-builds') | ||
| 41 | self.get(url) | ||
| 42 | |||
| 43 | build = build_request.build | ||
| 44 | base_selector = '[data-latest-build-result="%s"] ' % build.id | ||
| 45 | |||
| 46 | # build queued; check shown as queued | ||
| 47 | selector = base_selector + '[data-build-state="Queued"]' | ||
| 48 | element = self.wait_until_visible(selector) | ||
| 49 | self.assertRegex(element.get_attribute('innerHTML'), | ||
| 50 | 'Build queued', 'build should show queued status') | ||
| 51 | |||
| 52 | # waiting for recipes to be parsed | ||
| 53 | build.outcome = Build.IN_PROGRESS | ||
| 54 | build.recipes_to_parse = recipes_to_parse | ||
| 55 | build.recipes_parsed = 0 | ||
| 56 | build.save() | ||
| 57 | |||
| 58 | build_request.state = BuildRequest.REQ_INPROGRESS | ||
| 59 | build_request.save() | ||
| 60 | |||
| 61 | self.get(url) | ||
| 62 | |||
| 63 | selector = base_selector + '[data-build-state="Parsing"]' | ||
| 64 | element = self.wait_until_visible(selector) | ||
| 65 | |||
| 66 | bar_selector = '#recipes-parsed-percentage-bar-%s' % build.id | ||
| 67 | bar_element = element.find_element(By.CSS_SELECTOR, bar_selector) | ||
| 68 | self.assertEqual(bar_element.value_of_css_property('width'), '0px', | ||
| 69 | 'recipe parse progress should be at 0') | ||
| 70 | |||
| 71 | # recipes being parsed; check parse progress | ||
| 72 | build.recipes_parsed = 5 | ||
| 73 | build.save() | ||
| 74 | |||
| 75 | self.get(url) | ||
| 76 | |||
| 77 | element = self.wait_until_visible(selector) | ||
| 78 | bar_element = element.find_element(By.CSS_SELECTOR, bar_selector) | ||
| 79 | recipe_bar_updated = lambda driver: \ | ||
| 80 | bar_element.get_attribute('style') == 'width: 50%;' | ||
| 81 | msg = 'recipe parse progress bar should update to 50%' | ||
| 82 | element = Wait(self.driver).until(recipe_bar_updated, msg) | ||
| 83 | |||
| 84 | # all recipes parsed, task started, waiting for first task to finish; | ||
| 85 | # check status is shown as "Tasks starting..." | ||
| 86 | build.recipes_parsed = recipes_to_parse | ||
| 87 | build.save() | ||
| 88 | |||
| 89 | recipe = self._create_recipe() | ||
| 90 | task1 = Task.objects.create(build=build, recipe=recipe, | ||
| 91 | task_name='Lionel') | ||
| 92 | task2 = Task.objects.create(build=build, recipe=recipe, | ||
| 93 | task_name='Jeffries') | ||
| 94 | |||
| 95 | self.get(url) | ||
| 96 | |||
| 97 | selector = base_selector + '[data-build-state="Starting"]' | ||
| 98 | element = self.wait_until_visible(selector) | ||
| 99 | self.assertRegex(element.get_attribute('innerHTML'), | ||
| 100 | 'Tasks starting', 'build should show "tasks starting" status') | ||
| 101 | |||
| 102 | # first task finished; check tasks progress bar | ||
| 103 | task1.outcome = Task.OUTCOME_SUCCESS | ||
| 104 | task1.save() | ||
| 105 | |||
| 106 | self.get(url) | ||
| 107 | |||
| 108 | selector = base_selector + '[data-build-state="In Progress"]' | ||
| 109 | element = self.wait_until_visible(selector) | ||
| 110 | |||
| 111 | bar_selector = '#build-pc-done-bar-%s' % build.id | ||
| 112 | bar_element = element.find_element(By.CSS_SELECTOR, bar_selector) | ||
| 113 | |||
| 114 | task_bar_updated = lambda driver: \ | ||
| 115 | bar_element.get_attribute('style') == 'width: 50%;' | ||
| 116 | msg = 'tasks progress bar should update to 50%' | ||
| 117 | element = Wait(self.driver).until(task_bar_updated, msg) | ||
| 118 | |||
| 119 | # last task finished; check tasks progress bar updates | ||
| 120 | task2.outcome = Task.OUTCOME_SUCCESS | ||
| 121 | task2.save() | ||
| 122 | |||
| 123 | self.get(url) | ||
| 124 | |||
| 125 | element = self.wait_until_visible(selector) | ||
| 126 | bar_element = element.find_element(By.CSS_SELECTOR, bar_selector) | ||
| 127 | task_bar_updated = lambda driver: \ | ||
| 128 | bar_element.get_attribute('style') == 'width: 100%;' | ||
| 129 | msg = 'tasks progress bar should update to 100%' | ||
| 130 | element = Wait(self.driver).until(task_bar_updated, msg) | ||
| 131 | |||
| 132 | def test_states_to_success(self): | ||
| 133 | """ | ||
| 134 | Test state transitions in the recent builds area for a build which | ||
| 135 | completes successfully. | ||
| 136 | """ | ||
| 137 | build_request = self._create_build_request() | ||
| 138 | |||
| 139 | self._check_build_states(build_request) | ||
| 140 | |||
| 141 | # all tasks complete and build succeeded; check success state shown | ||
| 142 | build = build_request.build | ||
| 143 | build.outcome = Build.SUCCEEDED | ||
| 144 | build.save() | ||
| 145 | |||
| 146 | selector = '[data-latest-build-result="%s"] ' \ | ||
| 147 | '[data-build-state="Succeeded"]' % build.id | ||
| 148 | element = self.wait_until_visible(selector) | ||
| 149 | |||
| 150 | def test_states_to_failure(self): | ||
| 151 | """ | ||
| 152 | Test state transitions in the recent builds area for a build which | ||
| 153 | completes in a failure. | ||
| 154 | """ | ||
| 155 | build_request = self._create_build_request() | ||
| 156 | |||
| 157 | self._check_build_states(build_request) | ||
| 158 | |||
| 159 | # all tasks complete and build succeeded; check fail state shown | ||
| 160 | build = build_request.build | ||
| 161 | build.outcome = Build.FAILED | ||
| 162 | build.save() | ||
| 163 | |||
| 164 | selector = '[data-latest-build-result="%s"] ' \ | ||
| 165 | '[data-build-state="Failed"]' % build.id | ||
| 166 | element = self.wait_until_visible(selector) | ||
| 167 | |||
| 168 | def test_states_cancelling(self): | ||
| 169 | """ | ||
| 170 | Test that most recent build area updates correctly for a build | ||
| 171 | which is cancelled. | ||
| 172 | """ | ||
| 173 | url = reverse('all-builds') | ||
| 174 | |||
| 175 | build_request = self._create_build_request() | ||
| 176 | build = build_request.build | ||
| 177 | |||
| 178 | # cancel the build | ||
| 179 | build_request.state = BuildRequest.REQ_CANCELLING | ||
| 180 | build_request.save() | ||
| 181 | |||
| 182 | self.get(url) | ||
| 183 | |||
| 184 | # check cancelling state | ||
| 185 | selector = '[data-latest-build-result="%s"] ' \ | ||
| 186 | '[data-build-state="Cancelling"]' % build.id | ||
| 187 | element = self.wait_until_visible(selector) | ||
| 188 | self.assertRegex(element.get_attribute('innerHTML'), | ||
| 189 | 'Cancelling the build', 'build should show "cancelling" status') | ||
| 190 | |||
| 191 | # check cancelled state | ||
| 192 | build.outcome = Build.CANCELLED | ||
| 193 | build.save() | ||
| 194 | |||
| 195 | self.get(url) | ||
| 196 | |||
| 197 | selector = '[data-latest-build-result="%s"] ' \ | ||
| 198 | '[data-build-state="Cancelled"]' % build.id | ||
| 199 | element = self.wait_until_visible(selector) | ||
| 200 | self.assertRegex(element.get_attribute('innerHTML'), | ||
| 201 | 'Build cancelled', 'build should show "cancelled" status') | ||
diff --git a/bitbake/lib/toaster/tests/browser/test_new_custom_image_page.py b/bitbake/lib/toaster/tests/browser/test_new_custom_image_page.py deleted file mode 100644 index bf0304dbec..0000000000 --- a/bitbake/lib/toaster/tests/browser/test_new_custom_image_page.py +++ /dev/null | |||
| @@ -1,159 +0,0 @@ | |||
| 1 | #! /usr/bin/env python3 | ||
| 2 | # | ||
| 3 | # BitBake Toaster Implementation | ||
| 4 | # | ||
| 5 | # Copyright (C) 2013-2016 Intel Corporation | ||
| 6 | # | ||
| 7 | # SPDX-License-Identifier: GPL-2.0-only | ||
| 8 | # | ||
| 9 | from bldcontrol.models import BuildEnvironment | ||
| 10 | |||
| 11 | from django.urls import reverse | ||
| 12 | from tests.browser.selenium_helpers import SeleniumTestCase | ||
| 13 | |||
| 14 | from orm.models import BitbakeVersion, Release, Project, ProjectLayer, Layer | ||
| 15 | from orm.models import Layer_Version, Recipe, CustomImageRecipe | ||
| 16 | |||
| 17 | |||
| 18 | class TestNewCustomImagePage(SeleniumTestCase): | ||
| 19 | CUSTOM_IMAGE_NAME = 'roopa-doopa' | ||
| 20 | |||
| 21 | def setUp(self): | ||
| 22 | BuildEnvironment.objects.get_or_create( | ||
| 23 | betype=BuildEnvironment.TYPE_LOCAL, | ||
| 24 | ) | ||
| 25 | release = Release.objects.create( | ||
| 26 | name='baz', | ||
| 27 | bitbake_version=BitbakeVersion.objects.create(name='v1') | ||
| 28 | ) | ||
| 29 | |||
| 30 | # project to add new custom images to | ||
| 31 | self.project = Project.objects.create(name='foo', release=release) | ||
| 32 | |||
| 33 | # layer associated with the project | ||
| 34 | layer = Layer.objects.create(name='bar') | ||
| 35 | layer_version = Layer_Version.objects.create( | ||
| 36 | layer=layer, | ||
| 37 | project=self.project | ||
| 38 | ) | ||
| 39 | |||
| 40 | # properly add the layer to the project | ||
| 41 | ProjectLayer.objects.create( | ||
| 42 | project=self.project, | ||
| 43 | layercommit=layer_version, | ||
| 44 | optional=False | ||
| 45 | ) | ||
| 46 | |||
| 47 | # add a fake image recipe to the layer that can be customised | ||
| 48 | builldir = os.environ.get('BUILDDIR', './') | ||
| 49 | self.recipe = Recipe.objects.create( | ||
| 50 | name='core-image-minimal', | ||
| 51 | layer_version=layer_version, | ||
| 52 | file_path=f'{builldir}/core-image-minimal.bb', | ||
| 53 | is_image=True | ||
| 54 | ) | ||
| 55 | # create a tmp file for the recipe | ||
| 56 | with open(self.recipe.file_path, 'w') as f: | ||
| 57 | f.write('foo') | ||
| 58 | |||
| 59 | # another project with a custom image already in it | ||
| 60 | project2 = Project.objects.create(name='whoop', release=release) | ||
| 61 | layer_version2 = Layer_Version.objects.create( | ||
| 62 | layer=layer, | ||
| 63 | project=project2 | ||
| 64 | ) | ||
| 65 | ProjectLayer.objects.create( | ||
| 66 | project=project2, | ||
| 67 | layercommit=layer_version2, | ||
| 68 | optional=False | ||
| 69 | ) | ||
| 70 | recipe2 = Recipe.objects.create( | ||
| 71 | name='core-image-minimal', | ||
| 72 | layer_version=layer_version2, | ||
| 73 | is_image=True | ||
| 74 | ) | ||
| 75 | CustomImageRecipe.objects.create( | ||
| 76 | name=self.CUSTOM_IMAGE_NAME, | ||
| 77 | base_recipe=recipe2, | ||
| 78 | layer_version=layer_version2, | ||
| 79 | file_path='/1/2', | ||
| 80 | project=project2 | ||
| 81 | ) | ||
| 82 | |||
| 83 | def _create_custom_image(self, new_custom_image_name): | ||
| 84 | """ | ||
| 85 | 1. Go to the 'new custom image' page | ||
| 86 | 2. Click the button for the fake core-image-minimal | ||
| 87 | 3. Wait for the dialog box for setting the name of the new custom | ||
| 88 | image | ||
| 89 | 4. Insert new_custom_image_name into that dialog's text box | ||
| 90 | """ | ||
| 91 | url = reverse('newcustomimage', args=(self.project.id,)) | ||
| 92 | self.get(url) | ||
| 93 | self.wait_until_visible('#global-nav') | ||
| 94 | |||
| 95 | self.click('button[data-recipe="%s"]' % self.recipe.id) | ||
| 96 | |||
| 97 | selector = '#new-custom-image-modal input[type="text"]' | ||
| 98 | self.enter_text(selector, new_custom_image_name) | ||
| 99 | |||
| 100 | self.click('#create-new-custom-image-btn') | ||
| 101 | |||
| 102 | def _check_for_custom_image(self, image_name): | ||
| 103 | """ | ||
| 104 | Fetch the list of custom images for the project and check the | ||
| 105 | image with name image_name is listed there | ||
| 106 | """ | ||
| 107 | url = reverse('projectcustomimages', args=(self.project.id,)) | ||
| 108 | self.get(url) | ||
| 109 | |||
| 110 | self.wait_until_visible('#customimagestable') | ||
| 111 | |||
| 112 | element = self.find('#customimagestable td[class="name"] a') | ||
| 113 | msg = 'should be a custom image link with text %s' % image_name | ||
| 114 | self.assertEqual(element.text.strip(), image_name, msg) | ||
| 115 | |||
| 116 | def test_new_image(self): | ||
| 117 | """ | ||
| 118 | Should be able to create a new custom image | ||
| 119 | """ | ||
| 120 | custom_image_name = 'boo-image' | ||
| 121 | self._create_custom_image(custom_image_name) | ||
| 122 | self.wait_until_visible('#image-created-notification') | ||
| 123 | self._check_for_custom_image(custom_image_name) | ||
| 124 | |||
| 125 | def test_new_duplicates_other_project_image(self): | ||
| 126 | """ | ||
| 127 | Should be able to create a new custom image if its name is the same | ||
| 128 | as a custom image in another project | ||
| 129 | """ | ||
| 130 | self._create_custom_image(self.CUSTOM_IMAGE_NAME) | ||
| 131 | self.wait_until_visible('#image-created-notification') | ||
| 132 | self._check_for_custom_image(self.CUSTOM_IMAGE_NAME) | ||
| 133 | |||
| 134 | def test_new_duplicates_non_image_recipe(self): | ||
| 135 | """ | ||
| 136 | Should not be able to create a new custom image whose name is the | ||
| 137 | same as an existing non-image recipe | ||
| 138 | """ | ||
| 139 | self._create_custom_image(self.recipe.name) | ||
| 140 | element = self.wait_until_visible('#invalid-name-help') | ||
| 141 | self.assertRegex(element.text.strip(), | ||
| 142 | 'image with this name already exists') | ||
| 143 | |||
| 144 | def test_new_duplicates_project_image(self): | ||
| 145 | """ | ||
| 146 | Should not be able to create a new custom image whose name is the same | ||
| 147 | as a custom image in this project | ||
| 148 | """ | ||
| 149 | # create the image | ||
| 150 | custom_image_name = 'doh-image' | ||
| 151 | self._create_custom_image(custom_image_name) | ||
| 152 | self.wait_until_visible('#image-created-notification') | ||
| 153 | self._check_for_custom_image(custom_image_name) | ||
| 154 | |||
| 155 | # try to create an image with the same name | ||
| 156 | self._create_custom_image(custom_image_name) | ||
| 157 | element = self.wait_until_visible('#invalid-name-help') | ||
| 158 | expected = 'An image with this name already exists in this project' | ||
| 159 | self.assertRegex(element.text.strip(), expected) | ||
diff --git a/bitbake/lib/toaster/tests/browser/test_new_project_page.py b/bitbake/lib/toaster/tests/browser/test_new_project_page.py deleted file mode 100644 index e50f236c32..0000000000 --- a/bitbake/lib/toaster/tests/browser/test_new_project_page.py +++ /dev/null | |||
| @@ -1,107 +0,0 @@ | |||
| 1 | #! /usr/bin/env python3 | ||
| 2 | # | ||
| 3 | # BitBake Toaster Implementation | ||
| 4 | # | ||
| 5 | # Copyright (C) 2013-2016 Intel Corporation | ||
| 6 | # | ||
| 7 | # SPDX-License-Identifier: GPL-2.0-only | ||
| 8 | # | ||
| 9 | from django.urls import reverse | ||
| 10 | from tests.browser.selenium_helpers import SeleniumTestCase | ||
| 11 | from selenium.webdriver.support.ui import Select | ||
| 12 | from selenium.common.exceptions import InvalidElementStateException | ||
| 13 | from selenium.webdriver.common.by import By | ||
| 14 | |||
| 15 | from orm.models import Project, Release, BitbakeVersion | ||
| 16 | |||
| 17 | |||
| 18 | class TestNewProjectPage(SeleniumTestCase): | ||
| 19 | """ Test project data at /project/X/ is displayed correctly """ | ||
| 20 | |||
| 21 | def setUp(self): | ||
| 22 | bitbake, c = BitbakeVersion.objects.get_or_create( | ||
| 23 | name="master", | ||
| 24 | giturl="git://master", | ||
| 25 | branch="master", | ||
| 26 | dirpath="master") | ||
| 27 | |||
| 28 | release, c = Release.objects.get_or_create(name="msater", | ||
| 29 | description="master" | ||
| 30 | "release", | ||
| 31 | branch_name="master", | ||
| 32 | helptext="latest", | ||
| 33 | bitbake_version=bitbake) | ||
| 34 | |||
| 35 | self.release, c = Release.objects.get_or_create( | ||
| 36 | name="msater2", | ||
| 37 | description="master2" | ||
| 38 | "release2", | ||
| 39 | branch_name="master2", | ||
| 40 | helptext="latest2", | ||
| 41 | bitbake_version=bitbake) | ||
| 42 | |||
| 43 | def test_create_new_project(self): | ||
| 44 | """ Test creating a project """ | ||
| 45 | |||
| 46 | project_name = "masterproject" | ||
| 47 | |||
| 48 | url = reverse('newproject') | ||
| 49 | self.get(url) | ||
| 50 | self.wait_until_visible('#new-project-name') | ||
| 51 | self.enter_text('#new-project-name', project_name) | ||
| 52 | |||
| 53 | select = Select(self.find('#projectversion')) | ||
| 54 | select.select_by_value(str(self.release.pk)) | ||
| 55 | |||
| 56 | self.click("#create-project-button") | ||
| 57 | |||
| 58 | # We should get redirected to the new project's page with the | ||
| 59 | # notification at the top | ||
| 60 | element = self.wait_until_visible( | ||
| 61 | '#project-created-notification') | ||
| 62 | |||
| 63 | self.assertTrue(project_name in element.text, | ||
| 64 | "New project name not in new project notification") | ||
| 65 | |||
| 66 | self.assertTrue(Project.objects.filter(name=project_name).count(), | ||
| 67 | "New project not found in database") | ||
| 68 | |||
| 69 | def test_new_duplicates_project_name(self): | ||
| 70 | """ | ||
| 71 | Should not be able to create a new project whose name is the same | ||
| 72 | as an existing project | ||
| 73 | """ | ||
| 74 | |||
| 75 | project_name = "dupproject" | ||
| 76 | |||
| 77 | Project.objects.create_project(name=project_name, | ||
| 78 | release=self.release) | ||
| 79 | |||
| 80 | url = reverse('newproject') | ||
| 81 | self.get(url) | ||
| 82 | self.wait_until_visible('#new-project-name') | ||
| 83 | |||
| 84 | self.enter_text('#new-project-name', project_name) | ||
| 85 | |||
| 86 | select = Select(self.find('#projectversion')) | ||
| 87 | select.select_by_value(str(self.release.pk)) | ||
| 88 | |||
| 89 | radio = self.driver.find_element(By.ID, 'type-new') | ||
| 90 | radio.click() | ||
| 91 | |||
| 92 | self.wait_until_visible('#hint-error-project-name') | ||
| 93 | element = self.find('#hint-error-project-name') | ||
| 94 | |||
| 95 | self.assertIn("Project names must be unique", element.text, | ||
| 96 | "Did not find unique project name error message") | ||
| 97 | |||
| 98 | # Try and click it anyway, if it submits we'll have a new project in | ||
| 99 | # the db and assert then | ||
| 100 | try: | ||
| 101 | self.click("#create-project-button") | ||
| 102 | except InvalidElementStateException: | ||
| 103 | pass | ||
| 104 | |||
| 105 | self.assertTrue( | ||
| 106 | (Project.objects.filter(name=project_name).count() == 1), | ||
| 107 | "New project not found in database") | ||
diff --git a/bitbake/lib/toaster/tests/browser/test_project_builds_page.py b/bitbake/lib/toaster/tests/browser/test_project_builds_page.py deleted file mode 100644 index 0dba33b9c8..0000000000 --- a/bitbake/lib/toaster/tests/browser/test_project_builds_page.py +++ /dev/null | |||
| @@ -1,158 +0,0 @@ | |||
| 1 | #! /usr/bin/env python3 | ||
| 2 | # | ||
| 3 | # BitBake Toaster Implementation | ||
| 4 | # | ||
| 5 | # Copyright (C) 2013-2016 Intel Corporation | ||
| 6 | # | ||
| 7 | # SPDX-License-Identifier: GPL-2.0-only | ||
| 8 | # | ||
| 9 | |||
| 10 | import os | ||
| 11 | import re | ||
| 12 | |||
| 13 | from django.urls import reverse | ||
| 14 | from django.utils import timezone | ||
| 15 | from tests.browser.selenium_helpers import SeleniumTestCase | ||
| 16 | |||
| 17 | from orm.models import BitbakeVersion, Release, Project, Build, Target | ||
| 18 | |||
| 19 | class TestProjectBuildsPage(SeleniumTestCase): | ||
| 20 | """ Test data at /project/X/builds is displayed correctly """ | ||
| 21 | |||
| 22 | PROJECT_NAME = 'test project' | ||
| 23 | CLI_BUILDS_PROJECT_NAME = 'command line builds' | ||
| 24 | |||
| 25 | def setUp(self): | ||
| 26 | builldir = os.environ.get('BUILDDIR', './') | ||
| 27 | bbv = BitbakeVersion.objects.create(name='bbv1', giturl=f'{builldir}/', | ||
| 28 | branch='master', dirpath='') | ||
| 29 | release = Release.objects.create(name='release1', | ||
| 30 | bitbake_version=bbv) | ||
| 31 | self.project1 = Project.objects.create_project(name=self.PROJECT_NAME, | ||
| 32 | release=release) | ||
| 33 | self.project1.save() | ||
| 34 | |||
| 35 | self.project2 = Project.objects.create_project(name=self.PROJECT_NAME, | ||
| 36 | release=release) | ||
| 37 | self.project2.save() | ||
| 38 | |||
| 39 | self.default_project = Project.objects.create_project( | ||
| 40 | name=self.CLI_BUILDS_PROJECT_NAME, | ||
| 41 | release=release | ||
| 42 | ) | ||
| 43 | self.default_project.is_default = True | ||
| 44 | self.default_project.save() | ||
| 45 | |||
| 46 | # parameters for builds to associate with the projects | ||
| 47 | now = timezone.now() | ||
| 48 | |||
| 49 | self.project1_build_success = { | ||
| 50 | 'project': self.project1, | ||
| 51 | 'started_on': now, | ||
| 52 | 'completed_on': now, | ||
| 53 | 'outcome': Build.SUCCEEDED | ||
| 54 | } | ||
| 55 | |||
| 56 | self.project1_build_in_progress = { | ||
| 57 | 'project': self.project1, | ||
| 58 | 'started_on': now, | ||
| 59 | 'completed_on': now, | ||
| 60 | 'outcome': Build.IN_PROGRESS | ||
| 61 | } | ||
| 62 | |||
| 63 | self.project2_build_success = { | ||
| 64 | 'project': self.project2, | ||
| 65 | 'started_on': now, | ||
| 66 | 'completed_on': now, | ||
| 67 | 'outcome': Build.SUCCEEDED | ||
| 68 | } | ||
| 69 | |||
| 70 | self.project2_build_in_progress = { | ||
| 71 | 'project': self.project2, | ||
| 72 | 'started_on': now, | ||
| 73 | 'completed_on': now, | ||
| 74 | 'outcome': Build.IN_PROGRESS | ||
| 75 | } | ||
| 76 | |||
| 77 | def _get_rows_for_project(self, project_id): | ||
| 78 | """ | ||
| 79 | Helper to retrieve HTML rows for a project's builds, | ||
| 80 | as shown in the main table of the page | ||
| 81 | """ | ||
| 82 | url = reverse('projectbuilds', args=(project_id,)) | ||
| 83 | self.get(url) | ||
| 84 | self.wait_until_present('#projectbuildstable tbody tr') | ||
| 85 | return self.find_all('#projectbuildstable tbody tr') | ||
| 86 | |||
| 87 | def test_show_builds_for_project(self): | ||
| 88 | """ Builds for a project should be displayed in the main table """ | ||
| 89 | Build.objects.create(**self.project1_build_success) | ||
| 90 | Build.objects.create(**self.project1_build_success) | ||
| 91 | build_rows = self._get_rows_for_project(self.project1.id) | ||
| 92 | self.assertEqual(len(build_rows), 2) | ||
| 93 | |||
| 94 | def test_show_builds_project_only(self): | ||
| 95 | """ Builds for other projects should be excluded """ | ||
| 96 | Build.objects.create(**self.project1_build_success) | ||
| 97 | Build.objects.create(**self.project1_build_success) | ||
| 98 | Build.objects.create(**self.project1_build_success) | ||
| 99 | |||
| 100 | # shouldn't see these two | ||
| 101 | Build.objects.create(**self.project2_build_success) | ||
| 102 | Build.objects.create(**self.project2_build_in_progress) | ||
| 103 | |||
| 104 | build_rows = self._get_rows_for_project(self.project1.id) | ||
| 105 | self.assertEqual(len(build_rows), 3) | ||
| 106 | |||
| 107 | def test_builds_exclude_in_progress(self): | ||
| 108 | """ "in progress" builds should not be shown in main table """ | ||
| 109 | Build.objects.create(**self.project1_build_success) | ||
| 110 | Build.objects.create(**self.project1_build_success) | ||
| 111 | |||
| 112 | # shouldn't see this one | ||
| 113 | Build.objects.create(**self.project1_build_in_progress) | ||
| 114 | |||
| 115 | # shouldn't see these two either, as they belong to a different project | ||
| 116 | Build.objects.create(**self.project2_build_success) | ||
| 117 | Build.objects.create(**self.project2_build_in_progress) | ||
| 118 | |||
| 119 | build_rows = self._get_rows_for_project(self.project1.id) | ||
| 120 | self.assertEqual(len(build_rows), 2) | ||
| 121 | |||
| 122 | def test_show_tasks_with_suffix(self): | ||
| 123 | """ Task should be shown as suffixes on build names """ | ||
| 124 | build = Build.objects.create(**self.project1_build_success) | ||
| 125 | target = 'bash' | ||
| 126 | task = 'clean' | ||
| 127 | Target.objects.create(build=build, target=target, task=task) | ||
| 128 | |||
| 129 | url = reverse('projectbuilds', args=(self.project1.id,)) | ||
| 130 | self.get(url) | ||
| 131 | self.wait_until_present('td[class="target"]') | ||
| 132 | |||
| 133 | cell = self.find('td[class="target"]') | ||
| 134 | content = cell.get_attribute('innerHTML') | ||
| 135 | expected_text = '%s:%s' % (target, task) | ||
| 136 | |||
| 137 | self.assertTrue(re.search(expected_text, content), | ||
| 138 | '"target" cell should contain text %s' % expected_text) | ||
| 139 | |||
| 140 | def test_cli_builds_hides_tabs(self): | ||
| 141 | """ | ||
| 142 | Display for command line builds should hide tabs | ||
| 143 | """ | ||
| 144 | url = reverse('projectbuilds', args=(self.default_project.id,)) | ||
| 145 | self.get(url) | ||
| 146 | tabs = self.find_all('#project-topbar') | ||
| 147 | self.assertEqual(len(tabs), 0, | ||
| 148 | 'should be no top bar shown for command line builds') | ||
| 149 | |||
| 150 | def test_non_cli_builds_has_tabs(self): | ||
| 151 | """ | ||
| 152 | Non-command-line builds projects should show the tabs | ||
| 153 | """ | ||
| 154 | url = reverse('projectbuilds', args=(self.project1.id,)) | ||
| 155 | self.get(url) | ||
| 156 | tabs = self.find_all('#project-topbar') | ||
| 157 | self.assertEqual(len(tabs), 1, | ||
| 158 | 'should be a top bar shown for non-command-line builds') | ||
diff --git a/bitbake/lib/toaster/tests/browser/test_project_config_page.py b/bitbake/lib/toaster/tests/browser/test_project_config_page.py deleted file mode 100644 index b9de541efa..0000000000 --- a/bitbake/lib/toaster/tests/browser/test_project_config_page.py +++ /dev/null | |||
| @@ -1,220 +0,0 @@ | |||
| 1 | #! /usr/bin/env python3 | ||
| 2 | # | ||
| 3 | # BitBake Toaster Implementation | ||
| 4 | # | ||
| 5 | # Copyright (C) 2013-2016 Intel Corporation | ||
| 6 | # | ||
| 7 | # SPDX-License-Identifier: GPL-2.0-only | ||
| 8 | # | ||
| 9 | |||
| 10 | import os | ||
| 11 | from django.urls import reverse | ||
| 12 | from tests.browser.selenium_helpers import SeleniumTestCase | ||
| 13 | |||
| 14 | from orm.models import BitbakeVersion, Release, Project, ProjectVariable | ||
| 15 | from selenium.webdriver.common.by import By | ||
| 16 | |||
| 17 | class TestProjectConfigsPage(SeleniumTestCase): | ||
| 18 | """ Test data at /project/X/builds is displayed correctly """ | ||
| 19 | |||
| 20 | PROJECT_NAME = 'test project' | ||
| 21 | INVALID_PATH_START_TEXT = 'The directory path should either start with a /' | ||
| 22 | INVALID_PATH_CHAR_TEXT = 'The directory path cannot include spaces or ' \ | ||
| 23 | 'any of these characters' | ||
| 24 | |||
| 25 | def setUp(self): | ||
| 26 | builldir = os.environ.get('BUILDDIR', './') | ||
| 27 | bbv = BitbakeVersion.objects.create(name='bbv1', giturl=f'{builldir}/', | ||
| 28 | branch='master', dirpath='') | ||
| 29 | release = Release.objects.create(name='release1', | ||
| 30 | bitbake_version=bbv) | ||
| 31 | self.project1 = Project.objects.create_project(name=self.PROJECT_NAME, | ||
| 32 | release=release) | ||
| 33 | self.project1.save() | ||
| 34 | |||
| 35 | |||
| 36 | def test_no_underscore_iamgefs_type(self): | ||
| 37 | """ | ||
| 38 | Should not accept IMAGEFS_TYPE with an underscore | ||
| 39 | """ | ||
| 40 | |||
| 41 | imagefs_type = "foo_bar" | ||
| 42 | |||
| 43 | ProjectVariable.objects.get_or_create(project = self.project1, name = "IMAGE_FSTYPES", value = "abcd ") | ||
| 44 | url = reverse('projectconf', args=(self.project1.id,)); | ||
| 45 | self.get(url); | ||
| 46 | |||
| 47 | self.click('#change-image_fstypes-icon') | ||
| 48 | |||
| 49 | self.enter_text('#new-imagefs_types', imagefs_type) | ||
| 50 | |||
| 51 | element = self.wait_until_visible('#hintError-image-fs_type') | ||
| 52 | |||
| 53 | self.assertTrue(("A valid image type cannot include underscores" in element.text), | ||
| 54 | "Did not find underscore error message") | ||
| 55 | |||
| 56 | |||
| 57 | def test_checkbox_verification(self): | ||
| 58 | """ | ||
| 59 | Should automatically check the checkbox if user enters value | ||
| 60 | text box, if value is there in the checkbox. | ||
| 61 | """ | ||
| 62 | imagefs_type = "btrfs" | ||
| 63 | |||
| 64 | ProjectVariable.objects.get_or_create(project = self.project1, name = "IMAGE_FSTYPES", value = "abcd ") | ||
| 65 | url = reverse('projectconf', args=(self.project1.id,)); | ||
| 66 | self.get(url); | ||
| 67 | |||
| 68 | self.click('#change-image_fstypes-icon') | ||
| 69 | |||
| 70 | self.enter_text('#new-imagefs_types', imagefs_type) | ||
| 71 | |||
| 72 | checkboxes = self.driver.find_elements(By.XPATH, "//input[@class='fs-checkbox-fstypes']") | ||
| 73 | |||
| 74 | for checkbox in checkboxes: | ||
| 75 | if checkbox.get_attribute("value") == "btrfs": | ||
| 76 | self.assertEqual(checkbox.is_selected(), True) | ||
| 77 | |||
| 78 | |||
| 79 | def test_textbox_with_checkbox_verification(self): | ||
| 80 | """ | ||
| 81 | Should automatically add or remove value in textbox, if user checks | ||
| 82 | or unchecks checkboxes. | ||
| 83 | """ | ||
| 84 | |||
| 85 | ProjectVariable.objects.get_or_create(project = self.project1, name = "IMAGE_FSTYPES", value = "abcd ") | ||
| 86 | url = reverse('projectconf', args=(self.project1.id,)); | ||
| 87 | self.get(url); | ||
| 88 | |||
| 89 | self.click('#change-image_fstypes-icon') | ||
| 90 | |||
| 91 | self.wait_until_visible('#new-imagefs_types') | ||
| 92 | |||
| 93 | checkboxes_selector = '.fs-checkbox-fstypes' | ||
| 94 | |||
| 95 | self.wait_until_visible(checkboxes_selector) | ||
| 96 | checkboxes = self.find_all(checkboxes_selector) | ||
| 97 | |||
| 98 | for checkbox in checkboxes: | ||
| 99 | if checkbox.get_attribute("value") == "cpio": | ||
| 100 | checkbox.click() | ||
| 101 | element = self.driver.find_element(By.ID, 'new-imagefs_types') | ||
| 102 | |||
| 103 | self.wait_until_visible('#new-imagefs_types') | ||
| 104 | |||
| 105 | self.assertTrue(("cpio" in element.get_attribute('value'), | ||
| 106 | "Imagefs not added into the textbox")) | ||
| 107 | checkbox.click() | ||
| 108 | self.assertTrue(("cpio" not in element.text), | ||
| 109 | "Image still present in the textbox") | ||
| 110 | |||
| 111 | def test_set_download_dir(self): | ||
| 112 | """ | ||
| 113 | Validate the allowed and disallowed types in the directory field for | ||
| 114 | DL_DIR | ||
| 115 | """ | ||
| 116 | |||
| 117 | ProjectVariable.objects.get_or_create(project=self.project1, | ||
| 118 | name='DL_DIR') | ||
| 119 | url = reverse('projectconf', args=(self.project1.id,)) | ||
| 120 | self.get(url) | ||
| 121 | |||
| 122 | # activate the input to edit download dir | ||
| 123 | self.click('#change-dl_dir-icon') | ||
| 124 | self.wait_until_visible('#new-dl_dir') | ||
| 125 | |||
| 126 | # downloads dir path doesn't start with / or ${...} | ||
| 127 | self.enter_text('#new-dl_dir', 'home/foo') | ||
| 128 | element = self.wait_until_visible('#hintError-initialChar-dl_dir') | ||
| 129 | |||
| 130 | msg = 'downloads directory path starts with invalid character but ' \ | ||
| 131 | 'treated as valid' | ||
| 132 | self.assertTrue((self.INVALID_PATH_START_TEXT in element.text), msg) | ||
| 133 | |||
| 134 | # downloads dir path has a space | ||
| 135 | self.driver.find_element(By.ID, 'new-dl_dir').clear() | ||
| 136 | self.enter_text('#new-dl_dir', '/foo/bar a') | ||
| 137 | |||
| 138 | element = self.wait_until_visible('#hintError-dl_dir') | ||
| 139 | msg = 'downloads directory path characters invalid but treated as valid' | ||
| 140 | self.assertTrue((self.INVALID_PATH_CHAR_TEXT in element.text), msg) | ||
| 141 | |||
| 142 | # downloads dir path starts with ${...} but has a space | ||
| 143 | self.driver.find_element(By.ID,'new-dl_dir').clear() | ||
| 144 | self.enter_text('#new-dl_dir', '${TOPDIR}/down foo') | ||
| 145 | |||
| 146 | element = self.wait_until_visible('#hintError-dl_dir') | ||
| 147 | msg = 'downloads directory path characters invalid but treated as valid' | ||
| 148 | self.assertTrue((self.INVALID_PATH_CHAR_TEXT in element.text), msg) | ||
| 149 | |||
| 150 | # downloads dir path starts with / | ||
| 151 | self.driver.find_element(By.ID,'new-dl_dir').clear() | ||
| 152 | self.enter_text('#new-dl_dir', '/bar/foo') | ||
| 153 | |||
| 154 | hidden_element = self.driver.find_element(By.ID,'hintError-dl_dir') | ||
| 155 | self.assertEqual(hidden_element.is_displayed(), False, | ||
| 156 | 'downloads directory path valid but treated as invalid') | ||
| 157 | |||
| 158 | # downloads dir path starts with ${...} | ||
| 159 | self.driver.find_element(By.ID,'new-dl_dir').clear() | ||
| 160 | self.enter_text('#new-dl_dir', '${TOPDIR}/down') | ||
| 161 | |||
| 162 | hidden_element = self.driver.find_element(By.ID,'hintError-dl_dir') | ||
| 163 | self.assertEqual(hidden_element.is_displayed(), False, | ||
| 164 | 'downloads directory path valid but treated as invalid') | ||
| 165 | |||
| 166 | def test_set_sstate_dir(self): | ||
| 167 | """ | ||
| 168 | Validate the allowed and disallowed types in the directory field for | ||
| 169 | SSTATE_DIR | ||
| 170 | """ | ||
| 171 | |||
| 172 | ProjectVariable.objects.get_or_create(project=self.project1, | ||
| 173 | name='SSTATE_DIR') | ||
| 174 | url = reverse('projectconf', args=(self.project1.id,)) | ||
| 175 | self.get(url) | ||
| 176 | |||
| 177 | self.click('#change-sstate_dir-icon') | ||
| 178 | |||
| 179 | self.wait_until_visible('#new-sstate_dir') | ||
| 180 | |||
| 181 | # path doesn't start with / or ${...} | ||
| 182 | self.enter_text('#new-sstate_dir', 'home/foo') | ||
| 183 | element = self.wait_until_visible('#hintError-initialChar-sstate_dir') | ||
| 184 | |||
| 185 | msg = 'sstate directory path starts with invalid character but ' \ | ||
| 186 | 'treated as valid' | ||
| 187 | self.assertTrue((self.INVALID_PATH_START_TEXT in element.text), msg) | ||
| 188 | |||
| 189 | # path has a space | ||
| 190 | self.driver.find_element(By.ID, 'new-sstate_dir').clear() | ||
| 191 | self.enter_text('#new-sstate_dir', '/foo/bar a') | ||
| 192 | |||
| 193 | element = self.wait_until_visible('#hintError-sstate_dir') | ||
| 194 | msg = 'sstate directory path characters invalid but treated as valid' | ||
| 195 | self.assertTrue((self.INVALID_PATH_CHAR_TEXT in element.text), msg) | ||
| 196 | |||
| 197 | # path starts with ${...} but has a space | ||
| 198 | self.driver.find_element(By.ID,'new-sstate_dir').clear() | ||
| 199 | self.enter_text('#new-sstate_dir', '${TOPDIR}/down foo') | ||
| 200 | |||
| 201 | element = self.wait_until_visible('#hintError-sstate_dir') | ||
| 202 | msg = 'sstate directory path characters invalid but treated as valid' | ||
| 203 | self.assertTrue((self.INVALID_PATH_CHAR_TEXT in element.text), msg) | ||
| 204 | |||
| 205 | # path starts with / | ||
| 206 | self.driver.find_element(By.ID,'new-sstate_dir').clear() | ||
| 207 | self.enter_text('#new-sstate_dir', '/bar/foo') | ||
| 208 | |||
| 209 | hidden_element = self.driver.find_element(By.ID, 'hintError-sstate_dir') | ||
| 210 | self.assertEqual(hidden_element.is_displayed(), False, | ||
| 211 | 'sstate directory path valid but treated as invalid') | ||
| 212 | |||
| 213 | # paths starts with ${...} | ||
| 214 | self.driver.find_element(By.ID, 'new-sstate_dir').clear() | ||
| 215 | self.enter_text('#new-sstate_dir', '${TOPDIR}/down') | ||
| 216 | |||
| 217 | hidden_element = self.driver.find_element(By.ID, 'hintError-sstate_dir') | ||
| 218 | self.assertEqual(hidden_element.is_displayed(), False, | ||
| 219 | 'sstate directory path valid but treated as invalid') | ||
| 220 | |||
diff --git a/bitbake/lib/toaster/tests/browser/test_project_page.py b/bitbake/lib/toaster/tests/browser/test_project_page.py deleted file mode 100644 index 546293f1ee..0000000000 --- a/bitbake/lib/toaster/tests/browser/test_project_page.py +++ /dev/null | |||
| @@ -1,47 +0,0 @@ | |||
| 1 | #! /usr/bin/env python3 | ||
| 2 | # | ||
| 3 | # BitBake Toaster Implementation | ||
| 4 | # | ||
| 5 | # Copyright (C) 2013-2016 Intel Corporation | ||
| 6 | # | ||
| 7 | # SPDX-License-Identifier: GPL-2.0-only | ||
| 8 | # | ||
| 9 | |||
| 10 | from django.urls import reverse | ||
| 11 | from django.utils import timezone | ||
| 12 | from tests.browser.selenium_helpers import SeleniumTestCase | ||
| 13 | |||
| 14 | from orm.models import Build, Project | ||
| 15 | |||
| 16 | class TestProjectPage(SeleniumTestCase): | ||
| 17 | """ Test project data at /project/X/ is displayed correctly """ | ||
| 18 | |||
| 19 | CLI_BUILDS_PROJECT_NAME = 'Command line builds' | ||
| 20 | |||
| 21 | def test_cli_builds_in_progress(self): | ||
| 22 | """ | ||
| 23 | In progress builds should not cause an error to be thrown | ||
| 24 | when navigating to "command line builds" project page; | ||
| 25 | see https://bugzilla.yoctoproject.org/show_bug.cgi?id=8277 | ||
| 26 | """ | ||
| 27 | |||
| 28 | # add the "command line builds" default project; this mirrors what | ||
| 29 | # we do with get_or_create_default_project() | ||
| 30 | default_project = Project.objects.create_project(self.CLI_BUILDS_PROJECT_NAME, None) | ||
| 31 | default_project.is_default = True | ||
| 32 | default_project.save() | ||
| 33 | |||
| 34 | # add an "in progress" build for the default project | ||
| 35 | now = timezone.now() | ||
| 36 | Build.objects.create(project=default_project, | ||
| 37 | started_on=now, | ||
| 38 | completed_on=now, | ||
| 39 | outcome=Build.IN_PROGRESS) | ||
| 40 | |||
| 41 | # navigate to the project page for the default project | ||
| 42 | url = reverse("project", args=(default_project.id,)) | ||
| 43 | self.get(url) | ||
| 44 | |||
| 45 | # check that we get a project page with the correct heading | ||
| 46 | project_name = self.find('.project-name').text.strip() | ||
| 47 | self.assertEqual(project_name, self.CLI_BUILDS_PROJECT_NAME) | ||
diff --git a/bitbake/lib/toaster/tests/browser/test_sample.py b/bitbake/lib/toaster/tests/browser/test_sample.py deleted file mode 100644 index f04f1d9a16..0000000000 --- a/bitbake/lib/toaster/tests/browser/test_sample.py +++ /dev/null | |||
| @@ -1,39 +0,0 @@ | |||
| 1 | #! /usr/bin/env python3 | ||
| 2 | # | ||
| 3 | # BitBake Toaster Implementation | ||
| 4 | # | ||
| 5 | # Copyright (C) 2013-2016 Intel Corporation | ||
| 6 | # | ||
| 7 | # SPDX-License-Identifier: GPL-2.0-only | ||
| 8 | # | ||
| 9 | |||
| 10 | """ | ||
| 11 | A small example test demonstrating the basics of writing a test with | ||
| 12 | Toaster's SeleniumTestCase; this just fetches the Toaster home page | ||
| 13 | and checks it has the word "Toaster" in the brand link | ||
| 14 | |||
| 15 | New test files should follow this structure, should be named "test_*.py", | ||
| 16 | and should be in the same directory as this sample. | ||
| 17 | """ | ||
| 18 | |||
| 19 | from django.urls import reverse | ||
| 20 | from tests.browser.selenium_helpers import SeleniumTestCase | ||
| 21 | |||
| 22 | class TestSample(SeleniumTestCase): | ||
| 23 | """ Test landing page shows the Toaster brand """ | ||
| 24 | |||
| 25 | def test_landing_page_has_brand(self): | ||
| 26 | url = reverse('landing') | ||
| 27 | self.get(url) | ||
| 28 | brand_link = self.find('.toaster-navbar-brand a.brand') | ||
| 29 | self.assertEqual(brand_link.text.strip(), 'Toaster') | ||
| 30 | |||
| 31 | def test_no_builds_message(self): | ||
| 32 | """ Test that a message is shown when there are no builds """ | ||
| 33 | url = reverse('all-builds') | ||
| 34 | self.get(url) | ||
| 35 | self.wait_until_visible('#empty-state-allbuildstable') # wait for the empty state div to appear | ||
| 36 | div_msg = self.find('#empty-state-allbuildstable .alert-info') | ||
| 37 | |||
| 38 | msg = 'Sorry - no data found' | ||
| 39 | self.assertEqual(div_msg.text, msg) | ||
diff --git a/bitbake/lib/toaster/tests/browser/test_task_page.py b/bitbake/lib/toaster/tests/browser/test_task_page.py deleted file mode 100644 index 011b5854ae..0000000000 --- a/bitbake/lib/toaster/tests/browser/test_task_page.py +++ /dev/null | |||
| @@ -1,64 +0,0 @@ | |||
| 1 | #! /usr/bin/env python3 | ||
| 2 | # | ||
| 3 | # BitBake Toaster Implementation | ||
| 4 | # | ||
| 5 | # Copyright (C) 2013-2016 Intel Corporation | ||
| 6 | # | ||
| 7 | # SPDX-License-Identifier: GPL-2.0-only | ||
| 8 | # | ||
| 9 | |||
| 10 | from django.urls import reverse | ||
| 11 | from django.utils import timezone | ||
| 12 | from tests.browser.selenium_helpers import SeleniumTestCase | ||
| 13 | from orm.models import Project, Build, Layer, Layer_Version, Recipe, Target | ||
| 14 | from orm.models import Task, Task_Dependency | ||
| 15 | |||
| 16 | class TestTaskPage(SeleniumTestCase): | ||
| 17 | """ Test page which shows an individual task """ | ||
| 18 | RECIPE_NAME = 'bar' | ||
| 19 | RECIPE_VERSION = '0.1' | ||
| 20 | TASK_NAME = 'do_da_doo_ron_ron' | ||
| 21 | |||
| 22 | def setUp(self): | ||
| 23 | now = timezone.now() | ||
| 24 | |||
| 25 | project = Project.objects.get_or_create_default_project() | ||
| 26 | |||
| 27 | self.build = Build.objects.create(project=project, started_on=now, | ||
| 28 | completed_on=now) | ||
| 29 | |||
| 30 | Target.objects.create(target='foo', build=self.build) | ||
| 31 | |||
| 32 | layer = Layer.objects.create() | ||
| 33 | |||
| 34 | layer_version = Layer_Version.objects.create(layer=layer) | ||
| 35 | |||
| 36 | recipe = Recipe.objects.create(name=TestTaskPage.RECIPE_NAME, | ||
| 37 | layer_version=layer_version, version=TestTaskPage.RECIPE_VERSION) | ||
| 38 | |||
| 39 | self.task = Task.objects.create(build=self.build, recipe=recipe, | ||
| 40 | order=1, outcome=Task.OUTCOME_COVERED, task_executed=False, | ||
| 41 | task_name=TestTaskPage.TASK_NAME) | ||
| 42 | |||
| 43 | def test_covered_task(self): | ||
| 44 | """ | ||
| 45 | Check that covered tasks are displayed for tasks which have | ||
| 46 | dependencies on themselves | ||
| 47 | """ | ||
| 48 | |||
| 49 | # the infinite loop which of bug 9952 was down to tasks which | ||
| 50 | # depend on themselves, so add self-dependent tasks to replicate the | ||
| 51 | # situation which caused the infinite loop (now fixed) | ||
| 52 | Task_Dependency.objects.create(task=self.task, depends_on=self.task) | ||
| 53 | |||
| 54 | url = reverse('task', args=(self.build.id, self.task.id,)) | ||
| 55 | self.get(url) | ||
| 56 | |||
| 57 | # check that we see the task name | ||
| 58 | self.wait_until_visible('.page-header h1') | ||
| 59 | |||
| 60 | heading = self.find('.page-header h1') | ||
| 61 | expected_heading = '%s_%s %s' % (TestTaskPage.RECIPE_NAME, | ||
| 62 | TestTaskPage.RECIPE_VERSION, TestTaskPage.TASK_NAME) | ||
| 63 | self.assertEqual(heading.text, expected_heading, | ||
| 64 | 'Heading should show recipe name, version and task') | ||
diff --git a/bitbake/lib/toaster/tests/browser/test_toastertable_ui.py b/bitbake/lib/toaster/tests/browser/test_toastertable_ui.py deleted file mode 100644 index 691aca1ef0..0000000000 --- a/bitbake/lib/toaster/tests/browser/test_toastertable_ui.py +++ /dev/null | |||
| @@ -1,151 +0,0 @@ | |||
| 1 | #! /usr/bin/env python3 | ||
| 2 | # | ||
| 3 | # BitBake Toaster Implementation | ||
| 4 | # | ||
| 5 | # Copyright (C) 2013-2016 Intel Corporation | ||
| 6 | # | ||
| 7 | # SPDX-License-Identifier: GPL-2.0-only | ||
| 8 | # | ||
| 9 | |||
| 10 | from datetime import datetime | ||
| 11 | import os | ||
| 12 | |||
| 13 | from django.urls import reverse | ||
| 14 | from django.utils import timezone | ||
| 15 | from tests.browser.selenium_helpers import SeleniumTestCase | ||
| 16 | from orm.models import BitbakeVersion, Release, Project, Build | ||
| 17 | from selenium.webdriver.common.by import By | ||
| 18 | |||
| 19 | class TestToasterTableUI(SeleniumTestCase): | ||
| 20 | """ | ||
| 21 | Tests for the UI elements of ToasterTable (sorting etc.); | ||
| 22 | note that the tests cover generic functionality of ToasterTable which | ||
| 23 | manifests as UI elements in the browser, and can only be tested via | ||
| 24 | Selenium. | ||
| 25 | """ | ||
| 26 | |||
| 27 | def setUp(self): | ||
| 28 | pass | ||
| 29 | |||
| 30 | def _get_orderby_heading(self, table): | ||
| 31 | """ | ||
| 32 | Get the current order by finding the column heading in <table> with | ||
| 33 | the sorted class on it. | ||
| 34 | |||
| 35 | table: WebElement for a ToasterTable | ||
| 36 | """ | ||
| 37 | selector = 'thead a.sorted' | ||
| 38 | heading = table.find_element(By.CSS_SELECTOR, selector) | ||
| 39 | return heading.get_attribute('innerHTML').strip() | ||
| 40 | |||
| 41 | def _get_datetime_from_cell(self, row, selector): | ||
| 42 | """ | ||
| 43 | Return the value in the cell selected by <selector> on <row> as a | ||
| 44 | datetime. | ||
| 45 | |||
| 46 | row: <tr> WebElement for a row in the ToasterTable | ||
| 47 | selector: CSS selector to use to find the cell containing the date time | ||
| 48 | string | ||
| 49 | """ | ||
| 50 | cell = row.find_element(By.CSS_SELECTOR, selector) | ||
| 51 | cell_text = cell.get_attribute('innerHTML').strip() | ||
| 52 | return datetime.strptime(cell_text, '%d/%m/%y %H:%M') | ||
| 53 | |||
| 54 | def test_revert_orderby(self): | ||
| 55 | """ | ||
| 56 | Test that sort order for a table reverts to the default sort order | ||
| 57 | if the current sort column is hidden. | ||
| 58 | """ | ||
| 59 | now = timezone.now() | ||
| 60 | later = now + timezone.timedelta(hours=1) | ||
| 61 | even_later = later + timezone.timedelta(hours=1) | ||
| 62 | |||
| 63 | builldir = os.environ.get('BUILDDIR', './') | ||
| 64 | bbv = BitbakeVersion.objects.create(name='test bbv', giturl=f'{builldir}/', | ||
| 65 | branch='master', dirpath='') | ||
| 66 | release = Release.objects.create(name='test release', | ||
| 67 | branch_name='master', | ||
| 68 | bitbake_version=bbv) | ||
| 69 | |||
| 70 | project = Project.objects.create_project('project', release) | ||
| 71 | |||
| 72 | # set up two builds which will order differently when sorted by | ||
| 73 | # started_on or completed_on | ||
| 74 | |||
| 75 | # started first, finished last | ||
| 76 | build1 = Build.objects.create(project=project, | ||
| 77 | started_on=now, | ||
| 78 | completed_on=even_later, | ||
| 79 | outcome=Build.SUCCEEDED) | ||
| 80 | |||
| 81 | # started second, finished first | ||
| 82 | build2 = Build.objects.create(project=project, | ||
| 83 | started_on=later, | ||
| 84 | completed_on=later, | ||
| 85 | outcome=Build.SUCCEEDED) | ||
| 86 | |||
| 87 | url = reverse('all-builds') | ||
| 88 | self.get(url) | ||
| 89 | table = self.wait_until_visible('#allbuildstable') | ||
| 90 | |||
| 91 | # check ordering (default is by -completed_on); so build1 should be | ||
| 92 | # first as it finished last | ||
| 93 | active_heading = self._get_orderby_heading(table) | ||
| 94 | self.assertEqual(active_heading, 'Completed on', | ||
| 95 | 'table should be sorted by "Completed on" by default') | ||
| 96 | |||
| 97 | row_selector = '#allbuildstable tbody tr' | ||
| 98 | cell_selector = 'td.completed_on' | ||
| 99 | |||
| 100 | rows = self.find_all(row_selector) | ||
| 101 | row1_completed_on = self._get_datetime_from_cell(rows[0], cell_selector) | ||
| 102 | row2_completed_on = self._get_datetime_from_cell(rows[1], cell_selector) | ||
| 103 | self.assertTrue(row1_completed_on > row2_completed_on, | ||
| 104 | 'table should be sorted by -completed_on') | ||
| 105 | |||
| 106 | # turn on started_on column | ||
| 107 | self.click('#edit-columns-button') | ||
| 108 | self.click('#checkbox-started_on') | ||
| 109 | |||
| 110 | # sort by started_on column | ||
| 111 | links = table.find_elements(By.CSS_SELECTOR, 'th.started_on a') | ||
| 112 | for link in links: | ||
| 113 | if link.get_attribute('innerHTML').strip() == 'Started on': | ||
| 114 | link.click() | ||
| 115 | break | ||
| 116 | |||
| 117 | # wait for table data to reload in response to new sort | ||
| 118 | self.wait_until_visible('#allbuildstable') | ||
| 119 | |||
| 120 | # check ordering; build1 should be first | ||
| 121 | active_heading = self._get_orderby_heading(table) | ||
| 122 | self.assertEqual(active_heading, 'Started on', | ||
| 123 | 'table should be sorted by "Started on"') | ||
| 124 | |||
| 125 | cell_selector = 'td.started_on' | ||
| 126 | |||
| 127 | rows = self.find_all(row_selector) | ||
| 128 | row1_started_on = self._get_datetime_from_cell(rows[0], cell_selector) | ||
| 129 | row2_started_on = self._get_datetime_from_cell(rows[1], cell_selector) | ||
| 130 | self.assertTrue(row1_started_on < row2_started_on, | ||
| 131 | 'table should be sorted by started_on') | ||
| 132 | |||
| 133 | # turn off started_on column | ||
| 134 | self.click('#edit-columns-button') | ||
| 135 | self.click('#checkbox-started_on') | ||
| 136 | |||
| 137 | # wait for table data to reload in response to new sort | ||
| 138 | self.wait_until_visible('#allbuildstable') | ||
| 139 | |||
| 140 | # check ordering (should revert to completed_on); build2 should be first | ||
| 141 | active_heading = self._get_orderby_heading(table) | ||
| 142 | self.assertEqual(active_heading, 'Completed on', | ||
| 143 | 'table should be sorted by "Completed on" after hiding sort column') | ||
| 144 | |||
| 145 | cell_selector = 'td.completed_on' | ||
| 146 | |||
| 147 | rows = self.find_all(row_selector) | ||
| 148 | row1_completed_on = self._get_datetime_from_cell(rows[0], cell_selector) | ||
| 149 | row2_completed_on = self._get_datetime_from_cell(rows[1], cell_selector) | ||
| 150 | self.assertTrue(row1_completed_on > row2_completed_on, | ||
| 151 | 'table should be sorted by -completed_on') | ||
diff --git a/bitbake/lib/toaster/tests/builds/README b/bitbake/lib/toaster/tests/builds/README deleted file mode 100644 index 4a3b5328b8..0000000000 --- a/bitbake/lib/toaster/tests/builds/README +++ /dev/null | |||
| @@ -1,14 +0,0 @@ | |||
| 1 | # Running build tests | ||
| 2 | |||
| 3 | These tests are to test the running of builds and the data produced by the builds. | ||
| 4 | Your oe build environment must be sourced/initialised for these tests to run. | ||
| 5 | |||
| 6 | The simplest way to run the tests are the following commands: | ||
| 7 | |||
| 8 | $ . oe-init-build-env | ||
| 9 | $ cd bitbake/lib/toaster/ # path my vary but this is into toaster's directory | ||
| 10 | $ DJANGO_SETTINGS_MODULE='toastermain.settings_test' ./manage.py test tests.builds | ||
| 11 | |||
| 12 | Optional environment variables: | ||
| 13 | - TOASTER_DIR (where toaster keeps it's artifacts) | ||
| 14 | - 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/bitbake/lib/toaster/tests/builds/__init__.py b/bitbake/lib/toaster/tests/builds/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 --- a/bitbake/lib/toaster/tests/builds/__init__.py +++ /dev/null | |||
diff --git a/bitbake/lib/toaster/tests/builds/buildtest.py b/bitbake/lib/toaster/tests/builds/buildtest.py deleted file mode 100644 index e54d561334..0000000000 --- a/bitbake/lib/toaster/tests/builds/buildtest.py +++ /dev/null | |||
| @@ -1,166 +0,0 @@ | |||
| 1 | #! /usr/bin/env python3 | ||
| 2 | # | ||
| 3 | # BitBake Toaster Implementation | ||
| 4 | # | ||
| 5 | # Copyright (C) 2016 Intel Corporation | ||
| 6 | # | ||
| 7 | # SPDX-License-Identifier: GPL-2.0-only | ||
| 8 | # | ||
| 9 | |||
| 10 | import os | ||
| 11 | import sys | ||
| 12 | import time | ||
| 13 | import unittest | ||
| 14 | |||
| 15 | from orm.models import Project, Release, ProjectTarget, Build, ProjectVariable | ||
| 16 | from bldcontrol.models import BuildEnvironment | ||
| 17 | |||
| 18 | from bldcontrol.management.commands.runbuilds import Command\ | ||
| 19 | as RunBuildsCommand | ||
| 20 | |||
| 21 | from django.core.management import call_command | ||
| 22 | |||
| 23 | import subprocess | ||
| 24 | import logging | ||
| 25 | |||
| 26 | logger = logging.getLogger("toaster") | ||
| 27 | |||
| 28 | # We use unittest.TestCase instead of django.test.TestCase because we don't | ||
| 29 | # want to wrap everything in a database transaction as an external process | ||
| 30 | # (bitbake needs access to the database) | ||
| 31 | |||
| 32 | def load_build_environment(): | ||
| 33 | call_command('loaddata', 'settings.xml', app_label="orm") | ||
| 34 | call_command('loaddata', 'poky.xml', app_label="orm") | ||
| 35 | |||
| 36 | current_builddir = os.environ.get("BUILDDIR") | ||
| 37 | if current_builddir: | ||
| 38 | BuildTest.BUILDDIR = current_builddir | ||
| 39 | else: | ||
| 40 | # Setup a builddir based on default layout | ||
| 41 | # bitbake inside openebedded-core | ||
| 42 | oe_init_build_env_path = os.path.join( | ||
| 43 | os.path.dirname(os.path.abspath(__file__)), | ||
| 44 | os.pardir, | ||
| 45 | os.pardir, | ||
| 46 | os.pardir, | ||
| 47 | os.pardir, | ||
| 48 | os.pardir, | ||
| 49 | 'oe-init-build-env' | ||
| 50 | ) | ||
| 51 | if not os.path.exists(oe_init_build_env_path): | ||
| 52 | raise Exception("We had no BUILDDIR set and couldn't " | ||
| 53 | "find oe-init-build-env to set this up " | ||
| 54 | "ourselves please run oe-init-build-env " | ||
| 55 | "before running these tests") | ||
| 56 | |||
| 57 | oe_init_build_env_path = os.path.realpath(oe_init_build_env_path) | ||
| 58 | cmd = "bash -c 'source oe-init-build-env %s'" % BuildTest.BUILDDIR | ||
| 59 | p = subprocess.Popen( | ||
| 60 | cmd, | ||
| 61 | cwd=os.path.dirname(oe_init_build_env_path), | ||
| 62 | shell=True, | ||
| 63 | stdout=subprocess.PIPE, | ||
| 64 | stderr=subprocess.PIPE) | ||
| 65 | |||
| 66 | output, err = p.communicate() | ||
| 67 | p.wait() | ||
| 68 | |||
| 69 | logger.info("oe-init-build-env %s %s" % (output, err)) | ||
| 70 | |||
| 71 | os.environ['BUILDDIR'] = BuildTest.BUILDDIR | ||
| 72 | |||
| 73 | # Setup the path to bitbake we know where to find this | ||
| 74 | bitbake_path = os.path.join( | ||
| 75 | os.path.dirname(os.path.abspath(__file__)), | ||
| 76 | os.pardir, | ||
| 77 | os.pardir, | ||
| 78 | os.pardir, | ||
| 79 | os.pardir, | ||
| 80 | 'bin', | ||
| 81 | 'bitbake') | ||
| 82 | if not os.path.exists(bitbake_path): | ||
| 83 | raise Exception("Could not find bitbake at the expected path %s" | ||
| 84 | % bitbake_path) | ||
| 85 | |||
| 86 | os.environ['BBBASEDIR'] = bitbake_path | ||
| 87 | |||
| 88 | class BuildTest(unittest.TestCase): | ||
| 89 | |||
| 90 | PROJECT_NAME = "Testbuild" | ||
| 91 | BUILDDIR = os.environ.get("BUILDDIR") | ||
| 92 | |||
| 93 | def build(self, target): | ||
| 94 | # So that the buildinfo helper uses the test database' | ||
| 95 | self.assertEqual( | ||
| 96 | os.environ.get('DJANGO_SETTINGS_MODULE', ''), | ||
| 97 | 'toastermain.settings_test', | ||
| 98 | "Please initialise django with the tests settings: " | ||
| 99 | "DJANGO_SETTINGS_MODULE='toastermain.settings_test'") | ||
| 100 | |||
| 101 | built = self.target_already_built(target) | ||
| 102 | if built: | ||
| 103 | return built | ||
| 104 | |||
| 105 | load_build_environment() | ||
| 106 | |||
| 107 | BuildEnvironment.objects.get_or_create( | ||
| 108 | betype=BuildEnvironment.TYPE_LOCAL, | ||
| 109 | sourcedir=BuildTest.BUILDDIR, | ||
| 110 | builddir=BuildTest.BUILDDIR | ||
| 111 | ) | ||
| 112 | |||
| 113 | release = Release.objects.get(name='local') | ||
| 114 | |||
| 115 | # Create a project for this build to run in | ||
| 116 | project = Project.objects.create_project(name=BuildTest.PROJECT_NAME, | ||
| 117 | release=release) | ||
| 118 | |||
| 119 | passthrough_variable_names = ["SSTATE_DIR", "DL_DIR", "SSTATE_MIRRORS", "BB_HASHSERVE", "BB_HASHSERVE_UPSTREAM"] | ||
| 120 | for variable_name in passthrough_variable_names: | ||
| 121 | current_variable = os.environ.get(variable_name) | ||
| 122 | if current_variable: | ||
| 123 | ProjectVariable.objects.get_or_create( | ||
| 124 | name=variable_name, | ||
| 125 | value=current_variable, | ||
| 126 | project=project) | ||
| 127 | |||
| 128 | if os.environ.get("TOASTER_TEST_USE_SSTATE_MIRROR"): | ||
| 129 | ProjectVariable.objects.get_or_create( | ||
| 130 | name="SSTATE_MIRRORS", | ||
| 131 | value="file://.* http://sstate.yoctoproject.org/all/PATH;downloadfilename=PATH", | ||
| 132 | project=project) | ||
| 133 | |||
| 134 | ProjectTarget.objects.create(project=project, | ||
| 135 | target=target, | ||
| 136 | task="") | ||
| 137 | build_request = project.schedule_build() | ||
| 138 | |||
| 139 | # run runbuilds command to dispatch the build | ||
| 140 | # e.g. manage.py runubilds | ||
| 141 | RunBuildsCommand().runbuild() | ||
| 142 | |||
| 143 | build_pk = build_request.build.pk | ||
| 144 | while Build.objects.get(pk=build_pk).outcome == Build.IN_PROGRESS: | ||
| 145 | sys.stdout.write("\rBuilding %s %d%%" % | ||
| 146 | (target, | ||
| 147 | build_request.build.completeper())) | ||
| 148 | sys.stdout.flush() | ||
| 149 | time.sleep(1) | ||
| 150 | |||
| 151 | self.assertEqual(Build.objects.get(pk=build_pk).outcome, | ||
| 152 | Build.SUCCEEDED, | ||
| 153 | "Build did not SUCCEEDED") | ||
| 154 | |||
| 155 | logger.info("\nBuild finished %s" % build_request.build.outcome) | ||
| 156 | return build_request.build | ||
| 157 | |||
| 158 | def target_already_built(self, target): | ||
| 159 | """ If the target is already built no need to build it again""" | ||
| 160 | for build in Build.objects.filter( | ||
| 161 | project__name=BuildTest.PROJECT_NAME): | ||
| 162 | targets = build.target_set.values_list('target', flat=True) | ||
| 163 | if target in targets: | ||
| 164 | return build | ||
| 165 | |||
| 166 | return None | ||
diff --git a/bitbake/lib/toaster/tests/builds/test_core_image_min.py b/bitbake/lib/toaster/tests/builds/test_core_image_min.py deleted file mode 100644 index c5bfdbfbb5..0000000000 --- a/bitbake/lib/toaster/tests/builds/test_core_image_min.py +++ /dev/null | |||
| @@ -1,363 +0,0 @@ | |||
| 1 | #! /usr/bin/env python3 | ||
| 2 | # | ||
| 3 | # BitBake Toaster Implementation | ||
| 4 | # | ||
| 5 | # Copyright (C) 2016 Intel Corporation | ||
| 6 | # | ||
| 7 | # SPDX-License-Identifier: GPL-2.0-only | ||
| 8 | # | ||
| 9 | # Tests were part of openembedded-core oe selftest Authored by: Lucian Musat | ||
| 10 | # Ionut Chisanovici, Paul Eggleton and Cristian Iorga | ||
| 11 | |||
| 12 | import os | ||
| 13 | import pytest | ||
| 14 | |||
| 15 | from django.db.models import Q | ||
| 16 | |||
| 17 | from orm.models import Target_Image_File, Target_Installed_Package, Task | ||
| 18 | from orm.models import Package_Dependency, Recipe_Dependency, Build | ||
| 19 | from orm.models import Task_Dependency, Package, Target, Recipe | ||
| 20 | from orm.models import CustomImagePackage | ||
| 21 | |||
| 22 | from tests.builds.buildtest import BuildTest | ||
| 23 | |||
| 24 | @pytest.mark.order(4) | ||
| 25 | @pytest.mark.django_db(True) | ||
| 26 | class BuildCoreImageMinimal(BuildTest): | ||
| 27 | """Build core-image-minimal and test the results""" | ||
| 28 | |||
| 29 | def setUp(self): | ||
| 30 | self.completed_build = self.target_already_built("core-image-minimal") | ||
| 31 | |||
| 32 | # Check if build name is unique - tc_id=795 | ||
| 33 | def test_Build_Unique_Name(self): | ||
| 34 | all_builds = Build.objects.all().count() | ||
| 35 | distinct_builds = Build.objects.values('id').distinct().count() | ||
| 36 | self.assertEqual(distinct_builds, | ||
| 37 | all_builds, | ||
| 38 | msg='Build name is not unique') | ||
| 39 | |||
| 40 | # Check if build cooker log path is unique - tc_id=819 | ||
| 41 | def test_Build_Unique_Cooker_Log_Path(self): | ||
| 42 | distinct_path = Build.objects.values( | ||
| 43 | 'cooker_log_path').distinct().count() | ||
| 44 | total_builds = Build.objects.values('id').count() | ||
| 45 | self.assertEqual(distinct_path, | ||
| 46 | total_builds, | ||
| 47 | msg='Build cooker log path is not unique') | ||
| 48 | |||
| 49 | # Check task order sequence for one build - tc=825 | ||
| 50 | def test_Task_Order_Sequence(self): | ||
| 51 | cnt_err = [] | ||
| 52 | tasks = Task.objects.filter( | ||
| 53 | Q(build=self.completed_build), | ||
| 54 | ~Q(order=None), | ||
| 55 | ~Q(task_name__contains='_setscene') | ||
| 56 | ).values('id', 'order').order_by("order") | ||
| 57 | |||
| 58 | cnt_tasks = 0 | ||
| 59 | for task in tasks: | ||
| 60 | cnt_tasks += 1 | ||
| 61 | if (task['order'] != cnt_tasks): | ||
| 62 | cnt_err.append(task['id']) | ||
| 63 | self.assertEqual( | ||
| 64 | len(cnt_err), 0, msg='Errors for task id: %s' % cnt_err) | ||
| 65 | |||
| 66 | # Check if disk_io matches the difference between EndTimeIO and | ||
| 67 | # StartTimeIO in build stats - tc=828 | ||
| 68 | # def test_Task_Disk_IO_TC828(self): | ||
| 69 | |||
| 70 | # Check if outcome = 2 (SSTATE) then sstate_result must be 3 (RESTORED) - | ||
| 71 | # tc=832 | ||
| 72 | def test_Task_If_Outcome_2_Sstate_Result_Must_Be_3(self): | ||
| 73 | tasks = Task.objects.filter(outcome=2).values('id', 'sstate_result') | ||
| 74 | cnt_err = [] | ||
| 75 | for task in tasks: | ||
| 76 | if (task['sstate_result'] != 3): | ||
| 77 | cnt_err.append(task['id']) | ||
| 78 | |||
| 79 | self.assertEqual(len(cnt_err), | ||
| 80 | 0, | ||
| 81 | msg='Errors for task id: %s' % cnt_err) | ||
| 82 | |||
| 83 | # Check if outcome = 1 (COVERED) or 3 (EXISTING) then sstate_result must | ||
| 84 | # be 0 (SSTATE_NA) - tc=833 | ||
| 85 | def test_Task_If_Outcome_1_3_Sstate_Result_Must_Be_0(self): | ||
| 86 | tasks = Task.objects.filter( | ||
| 87 | outcome__in=(Task.OUTCOME_COVERED, | ||
| 88 | Task.OUTCOME_PREBUILT)).values('id', | ||
| 89 | 'task_name', | ||
| 90 | 'sstate_result') | ||
| 91 | cnt_err = [] | ||
| 92 | for task in tasks: | ||
| 93 | if (task['sstate_result'] != Task.SSTATE_NA and | ||
| 94 | task['sstate_result'] != Task.SSTATE_MISS): | ||
| 95 | cnt_err.append({'id': task['id'], | ||
| 96 | 'name': task['task_name'], | ||
| 97 | 'sstate_result': task['sstate_result']}) | ||
| 98 | |||
| 99 | self.assertEqual(len(cnt_err), | ||
| 100 | 0, | ||
| 101 | msg='Errors for task id: %s' % cnt_err) | ||
| 102 | |||
| 103 | # Check if outcome is 0 (SUCCESS) or 4 (FAILED) then sstate_result must be | ||
| 104 | # 0 (NA), 1 (MISS) or 2 (FAILED) - tc=834 | ||
| 105 | def test_Task_If_Outcome_0_4_Sstate_Result_Must_Be_0_1_2(self): | ||
| 106 | tasks = Task.objects.filter( | ||
| 107 | outcome__in=(0, 4)).values('id', 'sstate_result') | ||
| 108 | cnt_err = [] | ||
| 109 | |||
| 110 | for task in tasks: | ||
| 111 | if (task['sstate_result'] not in [0, 1, 2]): | ||
| 112 | cnt_err.append(task['id']) | ||
| 113 | |||
| 114 | self.assertEqual(len(cnt_err), | ||
| 115 | 0, | ||
| 116 | msg='Errors for task id: %s' % cnt_err) | ||
| 117 | |||
| 118 | # Check if task_executed = TRUE (1), script_type must be 0 (CODING_NA), 2 | ||
| 119 | # (CODING_PYTHON), 3 (CODING_SHELL) - tc=891 | ||
| 120 | def test_Task_If_Task_Executed_True_Script_Type_0_2_3(self): | ||
| 121 | tasks = Task.objects.filter( | ||
| 122 | task_executed=1).values('id', 'script_type') | ||
| 123 | cnt_err = [] | ||
| 124 | |||
| 125 | for task in tasks: | ||
| 126 | if (task['script_type'] not in [0, 2, 3]): | ||
| 127 | cnt_err.append(task['id']) | ||
| 128 | self.assertEqual(len(cnt_err), | ||
| 129 | 0, | ||
| 130 | msg='Errors for task id: %s' % cnt_err) | ||
| 131 | |||
| 132 | # Check if task_executed = TRUE (1), outcome must be 0 (SUCCESS) or 4 | ||
| 133 | # (FAILED) - tc=836 | ||
| 134 | def test_Task_If_Task_Executed_True_Outcome_0_4(self): | ||
| 135 | tasks = Task.objects.filter(task_executed=1).values('id', 'outcome') | ||
| 136 | cnt_err = [] | ||
| 137 | |||
| 138 | for task in tasks: | ||
| 139 | if (task['outcome'] not in [0, 4]): | ||
| 140 | cnt_err.append(task['id']) | ||
| 141 | |||
| 142 | self.assertEqual(len(cnt_err), | ||
| 143 | 0, | ||
| 144 | msg='Errors for task id: %s' % cnt_err) | ||
| 145 | |||
| 146 | # Check if task_executed = FALSE (0), script_type must be 0 - tc=890 | ||
| 147 | def test_Task_If_Task_Executed_False_Script_Type_0(self): | ||
| 148 | tasks = Task.objects.filter( | ||
| 149 | task_executed=0).values('id', 'script_type') | ||
| 150 | cnt_err = [] | ||
| 151 | |||
| 152 | for task in tasks: | ||
| 153 | if (task['script_type'] != 0): | ||
| 154 | cnt_err.append(task['id']) | ||
| 155 | |||
| 156 | self.assertEqual(len(cnt_err), | ||
| 157 | 0, | ||
| 158 | msg='Errors for task id: %s' % cnt_err) | ||
| 159 | |||
| 160 | # Check if task_executed = FALSE (0) and build outcome = SUCCEEDED (0), | ||
| 161 | # task outcome must be 1 (COVERED), 2 (CACHED), 3 (PREBUILT), 5 (EMPTY) - | ||
| 162 | # tc=837 | ||
| 163 | def test_Task_If_Task_Executed_False_Outcome_1_2_3_5(self): | ||
| 164 | builds = Build.objects.filter(outcome=0).values('id') | ||
| 165 | cnt_err = [] | ||
| 166 | for build in builds: | ||
| 167 | tasks = Task.objects.filter( | ||
| 168 | build=build['id'], task_executed=0).values('id', 'outcome') | ||
| 169 | for task in tasks: | ||
| 170 | if (task['outcome'] not in [1, 2, 3, 5]): | ||
| 171 | cnt_err.append(task['id']) | ||
| 172 | |||
| 173 | self.assertEqual(len(cnt_err), | ||
| 174 | 0, | ||
| 175 | msg='Errors for task id: %s' % cnt_err) | ||
| 176 | |||
| 177 | # Key verification - tc=888 | ||
| 178 | def test_Target_Installed_Package(self): | ||
| 179 | rows = Target_Installed_Package.objects.values('id', | ||
| 180 | 'target_id', | ||
| 181 | 'package_id') | ||
| 182 | cnt_err = [] | ||
| 183 | |||
| 184 | for row in rows: | ||
| 185 | target = Target.objects.filter(id=row['target_id']).values('id') | ||
| 186 | package = Package.objects.filter(id=row['package_id']).values('id') | ||
| 187 | if (not target or not package): | ||
| 188 | cnt_err.append(row['id']) | ||
| 189 | self.assertEqual(len(cnt_err), | ||
| 190 | 0, | ||
| 191 | msg='Errors for target installed package id: %s' % | ||
| 192 | cnt_err) | ||
| 193 | |||
| 194 | # Key verification - tc=889 | ||
| 195 | def test_Task_Dependency(self): | ||
| 196 | rows = Task_Dependency.objects.values('id', | ||
| 197 | 'task_id', | ||
| 198 | 'depends_on_id') | ||
| 199 | cnt_err = [] | ||
| 200 | for row in rows: | ||
| 201 | task_id = Task.objects.filter(id=row['task_id']).values('id') | ||
| 202 | depends_on_id = Task.objects.filter( | ||
| 203 | id=row['depends_on_id']).values('id') | ||
| 204 | if (not task_id or not depends_on_id): | ||
| 205 | cnt_err.append(row['id']) | ||
| 206 | self.assertEqual(len(cnt_err), | ||
| 207 | 0, | ||
| 208 | msg='Errors for task dependency id: %s' % cnt_err) | ||
| 209 | |||
| 210 | # Check if build target file_name is populated only if is_image=true AND | ||
| 211 | # orm_build.outcome=0 then if the file exists and its size matches | ||
| 212 | # the file_size value. Need to add the tc in the test run | ||
| 213 | def test_Target_File_Name_Populated(self): | ||
| 214 | cnt_err = [] | ||
| 215 | builds = Build.objects.filter(outcome=0).values('id') | ||
| 216 | for build in builds: | ||
| 217 | targets = Target.objects.filter( | ||
| 218 | build_id=build['id'], is_image=1).values('id') | ||
| 219 | for target in targets: | ||
| 220 | target_files = Target_Image_File.objects.filter( | ||
| 221 | target_id=target['id']).values('id', | ||
| 222 | 'file_name', | ||
| 223 | 'file_size') | ||
| 224 | for file_info in target_files: | ||
| 225 | target_id = file_info['id'] | ||
| 226 | target_file_name = file_info['file_name'] | ||
| 227 | target_file_size = file_info['file_size'] | ||
| 228 | if (not target_file_name or not target_file_size): | ||
| 229 | cnt_err.append(target_id) | ||
| 230 | else: | ||
| 231 | if (not os.path.exists(target_file_name)): | ||
| 232 | cnt_err.append(target_id) | ||
| 233 | else: | ||
| 234 | if (os.path.getsize(target_file_name) != | ||
| 235 | target_file_size): | ||
| 236 | cnt_err.append(target_id) | ||
| 237 | self.assertEqual(len(cnt_err), 0, | ||
| 238 | msg='Errors for target image file id: %s' % | ||
| 239 | cnt_err) | ||
| 240 | |||
| 241 | # Key verification - tc=884 | ||
| 242 | def test_Package_Dependency(self): | ||
| 243 | cnt_err = [] | ||
| 244 | deps = Package_Dependency.objects.values( | ||
| 245 | 'id', 'package_id', 'depends_on_id') | ||
| 246 | for dep in deps: | ||
| 247 | if (dep['package_id'] == dep['depends_on_id']): | ||
| 248 | cnt_err.append(dep['id']) | ||
| 249 | self.assertEqual(len(cnt_err), 0, | ||
| 250 | msg='Errors for package dependency id: %s' % cnt_err) | ||
| 251 | |||
| 252 | # Recipe key verification, recipe name does not depends on a recipe having | ||
| 253 | # the same name - tc=883 | ||
| 254 | def test_Recipe_Dependency(self): | ||
| 255 | deps = Recipe_Dependency.objects.values( | ||
| 256 | 'id', 'recipe_id', 'depends_on_id') | ||
| 257 | cnt_err = [] | ||
| 258 | for dep in deps: | ||
| 259 | if (not dep['recipe_id'] or not dep['depends_on_id']): | ||
| 260 | cnt_err.append(dep['id']) | ||
| 261 | else: | ||
| 262 | name = Recipe.objects.filter( | ||
| 263 | id=dep['recipe_id']).values('name') | ||
| 264 | dep_name = Recipe.objects.filter( | ||
| 265 | id=dep['depends_on_id']).values('name') | ||
| 266 | if (name == dep_name): | ||
| 267 | cnt_err.append(dep['id']) | ||
| 268 | self.assertEqual(len(cnt_err), 0, | ||
| 269 | msg='Errors for recipe dependency id: %s' % cnt_err) | ||
| 270 | |||
| 271 | # Check if package name does not start with a number (0-9) - tc=846 | ||
| 272 | def test_Package_Name_For_Number(self): | ||
| 273 | packages = Package.objects.filter(~Q(size=-1)).values('id', 'name') | ||
| 274 | cnt_err = [] | ||
| 275 | for package in packages: | ||
| 276 | if (package['name'][0].isdigit() is True): | ||
| 277 | cnt_err.append(package['id']) | ||
| 278 | self.assertEqual( | ||
| 279 | len(cnt_err), 0, msg='Errors for package id: %s' % cnt_err) | ||
| 280 | |||
| 281 | # Check if package version starts with a number (0-9) - tc=847 | ||
| 282 | def test_Package_Version_Starts_With_Number(self): | ||
| 283 | packages = Package.objects.filter( | ||
| 284 | ~Q(size=-1)).values('id', 'version') | ||
| 285 | cnt_err = [] | ||
| 286 | for package in packages: | ||
| 287 | if (package['version'][0].isdigit() is False): | ||
| 288 | cnt_err.append(package['id']) | ||
| 289 | self.assertEqual( | ||
| 290 | len(cnt_err), 0, msg='Errors for package id: %s' % cnt_err) | ||
| 291 | |||
| 292 | # Check if package revision starts with 'r' - tc=848 | ||
| 293 | def test_Package_Revision_Starts_With_r(self): | ||
| 294 | packages = Package.objects.filter( | ||
| 295 | ~Q(size=-1)).values('id', 'revision') | ||
| 296 | cnt_err = [] | ||
| 297 | for package in packages: | ||
| 298 | if (package['revision'][0].startswith("r") is False): | ||
| 299 | cnt_err.append(package['id']) | ||
| 300 | self.assertEqual( | ||
| 301 | len(cnt_err), 0, msg='Errors for package id: %s' % cnt_err) | ||
| 302 | |||
| 303 | # Check the validity of the package build_id | ||
| 304 | # TC must be added in test run | ||
| 305 | def test_Package_Build_Id(self): | ||
| 306 | packages = Package.objects.filter( | ||
| 307 | ~Q(size=-1)).values('id', 'build_id') | ||
| 308 | cnt_err = [] | ||
| 309 | for package in packages: | ||
| 310 | build_id = Build.objects.filter( | ||
| 311 | id=package['build_id']).values('id') | ||
| 312 | if (not build_id): | ||
| 313 | # They have no build_id but if they are | ||
| 314 | # CustomImagePackage that's expected | ||
| 315 | try: | ||
| 316 | CustomImagePackage.objects.get(pk=package['id']) | ||
| 317 | except CustomImagePackage.DoesNotExist: | ||
| 318 | cnt_err.append(package['id']) | ||
| 319 | |||
| 320 | self.assertEqual(len(cnt_err), | ||
| 321 | 0, | ||
| 322 | msg="Errors for package id: %s they have no build" | ||
| 323 | "associated with them" % cnt_err) | ||
| 324 | |||
| 325 | # Check the validity of package recipe_id | ||
| 326 | # TC must be added in test run | ||
| 327 | def test_Package_Recipe_Id(self): | ||
| 328 | packages = Package.objects.filter( | ||
| 329 | ~Q(size=-1)).values('id', 'recipe_id') | ||
| 330 | cnt_err = [] | ||
| 331 | for package in packages: | ||
| 332 | recipe_id = Recipe.objects.filter( | ||
| 333 | id=package['recipe_id']).values('id') | ||
| 334 | if (not recipe_id): | ||
| 335 | cnt_err.append(package['id']) | ||
| 336 | self.assertEqual( | ||
| 337 | len(cnt_err), 0, msg='Errors for package id: %s' % cnt_err) | ||
| 338 | |||
| 339 | # Check if package installed_size field is not null | ||
| 340 | # TC must be aded in test run | ||
| 341 | def test_Package_Installed_Size_Not_NULL(self): | ||
| 342 | packages = Package.objects.filter( | ||
| 343 | installed_size__isnull=True).values('id') | ||
| 344 | cnt_err = [] | ||
| 345 | for package in packages: | ||
| 346 | cnt_err.append(package['id']) | ||
| 347 | self.assertEqual( | ||
| 348 | len(cnt_err), 0, msg='Errors for package id: %s' % cnt_err) | ||
| 349 | |||
| 350 | def test_custom_packages_generated(self): | ||
| 351 | """Test if there is a corresponding generated CustomImagePackage""" | ||
| 352 | """ for each of the packages generated""" | ||
| 353 | missing_packages = [] | ||
| 354 | |||
| 355 | for package in Package.objects.all(): | ||
| 356 | try: | ||
| 357 | CustomImagePackage.objects.get(name=package.name) | ||
| 358 | except CustomImagePackage.DoesNotExist: | ||
| 359 | missing_packages.append(package.name) | ||
| 360 | |||
| 361 | self.assertEqual(len(missing_packages), 0, | ||
| 362 | "Some package were created from the build but their" | ||
| 363 | " corresponding CustomImagePackage was not found") | ||
diff --git a/bitbake/lib/toaster/tests/commands/__init__.py b/bitbake/lib/toaster/tests/commands/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 --- a/bitbake/lib/toaster/tests/commands/__init__.py +++ /dev/null | |||
diff --git a/bitbake/lib/toaster/tests/commands/test_loaddata.py b/bitbake/lib/toaster/tests/commands/test_loaddata.py deleted file mode 100644 index 7d04f030ee..0000000000 --- a/bitbake/lib/toaster/tests/commands/test_loaddata.py +++ /dev/null | |||
| @@ -1,49 +0,0 @@ | |||
| 1 | #! /usr/bin/env python3 | ||
| 2 | # | ||
| 3 | # BitBake Toaster Implementation | ||
| 4 | # | ||
| 5 | # Copyright (C) 2016 Intel Corporation | ||
| 6 | # | ||
| 7 | # SPDX-License-Identifier: GPL-2.0-only | ||
| 8 | # | ||
| 9 | import pytest | ||
| 10 | from django.test import TestCase | ||
| 11 | from django.core import management | ||
| 12 | |||
| 13 | from orm.models import Layer_Version, Layer, Release, ToasterSetting | ||
| 14 | |||
| 15 | @pytest.mark.order(2) | ||
| 16 | class TestLoadDataFixtures(TestCase): | ||
| 17 | """ Test loading our 3 provided fixtures """ | ||
| 18 | def test_run_loaddata_poky_command(self): | ||
| 19 | management.call_command('loaddata', 'poky') | ||
| 20 | |||
| 21 | num_releases = Release.objects.count() | ||
| 22 | |||
| 23 | self.assertTrue( | ||
| 24 | Layer_Version.objects.filter( | ||
| 25 | layer__name="meta-poky").count() == num_releases, | ||
| 26 | "Loaded poky fixture but don't have a meta-poky for all releases" | ||
| 27 | " defined") | ||
| 28 | |||
| 29 | def test_run_loaddata_oecore_command(self): | ||
| 30 | management.call_command('loaddata', 'oe-core') | ||
| 31 | |||
| 32 | # We only have the one layer for oe-core setup | ||
| 33 | self.assertTrue( | ||
| 34 | Layer.objects.filter(name="openembedded-core").count() > 0, | ||
| 35 | "Loaded oe-core fixture but still have no openemebedded-core" | ||
| 36 | " layer") | ||
| 37 | |||
| 38 | def test_run_loaddata_settings_command(self): | ||
| 39 | management.call_command('loaddata', 'settings') | ||
| 40 | |||
| 41 | self.assertTrue( | ||
| 42 | ToasterSetting.objects.filter(name="DEFAULT_RELEASE").count() > 0, | ||
| 43 | "Loaded settings but have no DEFAULT_RELEASE") | ||
| 44 | |||
| 45 | self.assertTrue( | ||
| 46 | ToasterSetting.objects.filter( | ||
| 47 | name__startswith="DEFCONF").count() > 0, | ||
| 48 | "Loaded settings but have no DEFCONF (default project " | ||
| 49 | "configuration values)") | ||
diff --git a/bitbake/lib/toaster/tests/commands/test_lsupdates.py b/bitbake/lib/toaster/tests/commands/test_lsupdates.py deleted file mode 100644 index 30c6eeb4ac..0000000000 --- a/bitbake/lib/toaster/tests/commands/test_lsupdates.py +++ /dev/null | |||
| @@ -1,34 +0,0 @@ | |||
| 1 | #! /usr/bin/env python3 | ||
| 2 | # | ||
| 3 | # BitBake Toaster Implementation | ||
| 4 | # | ||
| 5 | # Copyright (C) 2016 Intel Corporation | ||
| 6 | # | ||
| 7 | # SPDX-License-Identifier: GPL-2.0-only | ||
| 8 | # | ||
| 9 | |||
| 10 | import pytest | ||
| 11 | from django.test import TestCase | ||
| 12 | from django.core import management | ||
| 13 | |||
| 14 | from orm.models import Layer_Version, Machine, Recipe | ||
| 15 | |||
| 16 | @pytest.mark.order(3) | ||
| 17 | class TestLayerIndexUpdater(TestCase): | ||
| 18 | def test_run_lsupdates_command(self): | ||
| 19 | # Load some release information for us to fetch from the layer index | ||
| 20 | management.call_command('loaddata', 'poky') | ||
| 21 | |||
| 22 | old_layers_count = Layer_Version.objects.count() | ||
| 23 | old_recipes_count = Recipe.objects.count() | ||
| 24 | old_machines_count = Machine.objects.count() | ||
| 25 | |||
| 26 | # Now fetch the metadata from the layer index | ||
| 27 | management.call_command('lsupdates') | ||
| 28 | |||
| 29 | self.assertTrue(Layer_Version.objects.count() > old_layers_count, | ||
| 30 | "lsupdates ran but we still have no more layers!") | ||
| 31 | self.assertTrue(Recipe.objects.count() > old_recipes_count, | ||
| 32 | "lsupdates ran but we still have no more Recipes!") | ||
| 33 | self.assertTrue(Machine.objects.count() > old_machines_count, | ||
| 34 | "lsupdates ran but we still have no more Machines!") | ||
diff --git a/bitbake/lib/toaster/tests/commands/test_runbuilds.py b/bitbake/lib/toaster/tests/commands/test_runbuilds.py deleted file mode 100644 index 849c227edc..0000000000 --- a/bitbake/lib/toaster/tests/commands/test_runbuilds.py +++ /dev/null | |||
| @@ -1,81 +0,0 @@ | |||
| 1 | #! /usr/bin/env python3 | ||
| 2 | # | ||
| 3 | # BitBake Toaster Implementation | ||
| 4 | # | ||
| 5 | # Copyright (C) 2016 Intel Corporation | ||
| 6 | # | ||
| 7 | # SPDX-License-Identifier: GPL-2.0-only | ||
| 8 | # | ||
| 9 | |||
| 10 | import os | ||
| 11 | |||
| 12 | from django.test import TestCase | ||
| 13 | from django.core import management | ||
| 14 | |||
| 15 | from orm.models import signal_runbuilds | ||
| 16 | |||
| 17 | import threading | ||
| 18 | import time | ||
| 19 | import subprocess | ||
| 20 | import signal | ||
| 21 | |||
| 22 | import logging | ||
| 23 | |||
| 24 | |||
| 25 | class KillRunbuilds(threading.Thread): | ||
| 26 | """ Kill the runbuilds process after an amount of time """ | ||
| 27 | def __init__(self, *args, **kwargs): | ||
| 28 | super(KillRunbuilds, self).__init__(*args, **kwargs) | ||
| 29 | self.daemon = True | ||
| 30 | |||
| 31 | def run(self): | ||
| 32 | time.sleep(5) | ||
| 33 | signal_runbuilds() | ||
| 34 | time.sleep(1) | ||
| 35 | |||
| 36 | pidfile_path = os.path.join(os.environ.get("BUILDDIR", "."), | ||
| 37 | ".runbuilds.pid") | ||
| 38 | |||
| 39 | try: | ||
| 40 | with open(pidfile_path) as pidfile: | ||
| 41 | pid = pidfile.read() | ||
| 42 | os.kill(int(pid), signal.SIGTERM) | ||
| 43 | except ProcessLookupError: | ||
| 44 | logging.warning("Runbuilds not running or already killed") | ||
| 45 | |||
| 46 | |||
| 47 | class TestCommands(TestCase): | ||
| 48 | """ Sanity test that runbuilds executes OK """ | ||
| 49 | |||
| 50 | def setUp(self): | ||
| 51 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", | ||
| 52 | "toastermain.settings_test") | ||
| 53 | os.environ.setdefault("BUILDDIR", | ||
| 54 | "/tmp/") | ||
| 55 | |||
| 56 | # Setup a real database if needed for runbuilds process | ||
| 57 | # to connect to | ||
| 58 | management.call_command('migrate') | ||
| 59 | |||
| 60 | def test_runbuilds_command(self): | ||
| 61 | kill_runbuilds = KillRunbuilds() | ||
| 62 | kill_runbuilds.start() | ||
| 63 | |||
| 64 | manage_py = os.path.join( | ||
| 65 | os.path.dirname(os.path.abspath(__file__)), | ||
| 66 | os.pardir, | ||
| 67 | os.pardir, | ||
| 68 | "manage.py") | ||
| 69 | |||
| 70 | command = "%s runbuilds" % manage_py | ||
| 71 | |||
| 72 | process = subprocess.Popen(command, | ||
| 73 | shell=True, | ||
| 74 | stdout=subprocess.PIPE, | ||
| 75 | stderr=subprocess.PIPE) | ||
| 76 | |||
| 77 | (out, err) = process.communicate() | ||
| 78 | process.wait() | ||
| 79 | |||
| 80 | self.assertNotEqual(process.returncode, 1, | ||
| 81 | "Runbuilds returned an error %s" % err) | ||
diff --git a/bitbake/lib/toaster/tests/db/__init__.py b/bitbake/lib/toaster/tests/db/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 --- a/bitbake/lib/toaster/tests/db/__init__.py +++ /dev/null | |||
diff --git a/bitbake/lib/toaster/tests/db/test_db.py b/bitbake/lib/toaster/tests/db/test_db.py deleted file mode 100644 index 072ab94363..0000000000 --- a/bitbake/lib/toaster/tests/db/test_db.py +++ /dev/null | |||
| @@ -1,58 +0,0 @@ | |||
| 1 | # The MIT License (MIT) | ||
| 2 | # | ||
| 3 | # Copyright (c) 2016 Damien Lespiau | ||
| 4 | # | ||
| 5 | # SPDX-License-Identifier: MIT | ||
| 6 | # | ||
| 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy | ||
| 8 | # of this software and associated documentation files (the "Software"), to deal | ||
| 9 | # in the Software without restriction, including without limitation the rights | ||
| 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
| 11 | # copies of the Software, and to permit persons to whom the Software is | ||
| 12 | # furnished to do so, subject to the following conditions: | ||
| 13 | # | ||
| 14 | # The above copyright notice and this permission notice shall be included in | ||
| 15 | # all copies or substantial portions of the Software. | ||
| 16 | # | ||
| 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
| 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
| 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
| 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
| 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
| 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
| 23 | # SOFTWARE. | ||
| 24 | |||
| 25 | import sys | ||
| 26 | import pytest | ||
| 27 | |||
| 28 | try: | ||
| 29 | from StringIO import StringIO | ||
| 30 | except ImportError: | ||
| 31 | from io import StringIO | ||
| 32 | |||
| 33 | from contextlib import contextmanager | ||
| 34 | |||
| 35 | from django.core import management | ||
| 36 | from django.test import TestCase | ||
| 37 | |||
| 38 | |||
| 39 | @contextmanager | ||
| 40 | def capture(command, *args, **kwargs): | ||
| 41 | out, sys.stdout = sys.stdout, StringIO() | ||
| 42 | command(*args, **kwargs) | ||
| 43 | sys.stdout.seek(0) | ||
| 44 | yield sys.stdout.read() | ||
| 45 | sys.stdout = out | ||
| 46 | |||
| 47 | |||
| 48 | def makemigrations(): | ||
| 49 | management.call_command('makemigrations') | ||
| 50 | |||
| 51 | @pytest.mark.order(1) | ||
| 52 | class MigrationTest(TestCase): | ||
| 53 | |||
| 54 | def testPendingMigration(self): | ||
| 55 | """Make sure there's no pending migration.""" | ||
| 56 | |||
| 57 | with capture(makemigrations) as output: | ||
| 58 | self.assertEqual(output, "No changes detected\n") | ||
diff --git a/bitbake/lib/toaster/tests/eventreplay/README b/bitbake/lib/toaster/tests/eventreplay/README deleted file mode 100644 index 8c5bb64323..0000000000 --- a/bitbake/lib/toaster/tests/eventreplay/README +++ /dev/null | |||
| @@ -1,22 +0,0 @@ | |||
| 1 | # Running eventreplay tests | ||
| 2 | |||
| 3 | These tests use event log files produced by bitbake <target> -w <event log file> | ||
| 4 | You need to have event log files produced before running this tests. | ||
| 5 | |||
| 6 | At the moment of writing this document tests use 2 event log files: zlib.events | ||
| 7 | and core-image-minimal.events. They're not provided with the tests due to their | ||
| 8 | significant size. | ||
| 9 | |||
| 10 | Here is how to produce them: | ||
| 11 | |||
| 12 | $ . oe-init-build-env | ||
| 13 | $ rm -r tmp sstate-cache | ||
| 14 | $ bitbake core-image-minimal -w core-image-minimal.events | ||
| 15 | $ rm -rf tmp sstate-cache | ||
| 16 | $ bitbake zlib -w zlib.events | ||
| 17 | |||
| 18 | After that it should be possible to run eventreplay tests this way: | ||
| 19 | |||
| 20 | $ EVENTREPLAY_DIR=./ DJANGO_SETTINGS_MODULE=toastermain.settings_test ../bitbake/lib/toaster/manage.py test -v2 tests.eventreplay | ||
| 21 | |||
| 22 | Note that environment variable EVENTREPLAY_DIR should point to the directory with event log files. | ||
diff --git a/bitbake/lib/toaster/tests/eventreplay/__init__.py b/bitbake/lib/toaster/tests/eventreplay/__init__.py deleted file mode 100644 index 8ed6792ef6..0000000000 --- a/bitbake/lib/toaster/tests/eventreplay/__init__.py +++ /dev/null | |||
| @@ -1,85 +0,0 @@ | |||
| 1 | #! /usr/bin/env python3 | ||
| 2 | # | ||
| 3 | # BitBake Toaster Implementation | ||
| 4 | # | ||
| 5 | # Copyright (C) 2016 Intel Corporation | ||
| 6 | # | ||
| 7 | # SPDX-License-Identifier: GPL-2.0-only | ||
| 8 | # | ||
| 9 | |||
| 10 | # Tests were part of openembedded-core oe selftest Authored by: Lucian Musat | ||
| 11 | # Ionut Chisanovici, Paul Eggleton and Cristian Iorga | ||
| 12 | |||
| 13 | """ | ||
| 14 | Test toaster backend by playing build event log files | ||
| 15 | using toaster-eventreplay script | ||
| 16 | """ | ||
| 17 | |||
| 18 | import os | ||
| 19 | |||
| 20 | from subprocess import getstatusoutput | ||
| 21 | from pathlib import Path | ||
| 22 | |||
| 23 | from django.test import TestCase | ||
| 24 | |||
| 25 | from orm.models import Target_Installed_Package, Package, Build | ||
| 26 | |||
| 27 | class EventReplay(TestCase): | ||
| 28 | """Base class for eventreplay test cases""" | ||
| 29 | |||
| 30 | def setUp(self): | ||
| 31 | """ | ||
| 32 | Setup build environment: | ||
| 33 | - set self.script to toaster-eventreplay path | ||
| 34 | - set self.eventplay_dir to the value of EVENTPLAY_DIR env variable | ||
| 35 | """ | ||
| 36 | bitbake_dir = Path(__file__.split('lib/toaster')[0]) | ||
| 37 | self.script = bitbake_dir / 'bin' / 'toaster-eventreplay' | ||
| 38 | self.assertTrue(self.script.exists(), "%s doesn't exist") | ||
| 39 | self.eventplay_dir = os.getenv("EVENTREPLAY_DIR") | ||
| 40 | self.assertTrue(self.eventplay_dir, | ||
| 41 | "Environment variable EVENTREPLAY_DIR is not set") | ||
| 42 | |||
| 43 | def _replay(self, eventfile): | ||
| 44 | """Run toaster-eventplay <eventfile>""" | ||
| 45 | eventpath = Path(self.eventplay_dir) / eventfile | ||
| 46 | status, output = getstatusoutput('%s %s' % (self.script, eventpath)) | ||
| 47 | if status: | ||
| 48 | print(output) | ||
| 49 | |||
| 50 | self.assertEqual(status, 0) | ||
| 51 | |||
| 52 | class CoreImageMinimalEventReplay(EventReplay): | ||
| 53 | """Replay core-image-minimal events""" | ||
| 54 | |||
| 55 | def test_installed_packages(self): | ||
| 56 | """Test if all required packages have been installed""" | ||
| 57 | |||
| 58 | self._replay('core-image-minimal.events') | ||
| 59 | |||
| 60 | # test installed packages | ||
| 61 | packages = sorted(Target_Installed_Package.objects.\ | ||
| 62 | values_list('package__name', flat=True)) | ||
| 63 | self.assertEqual(packages, ['base-files', 'base-passwd', 'busybox', | ||
| 64 | 'busybox-hwclock', 'busybox-syslog', | ||
| 65 | 'busybox-udhcpc', 'eudev', 'glibc', | ||
| 66 | 'init-ifupdown', 'initscripts', | ||
| 67 | 'initscripts-functions', 'kernel-base', | ||
| 68 | 'kernel-module-uvesafb', 'libkmod', | ||
| 69 | 'modutils-initscripts', 'netbase', | ||
| 70 | 'packagegroup-core-boot', 'run-postinsts', | ||
| 71 | 'sysvinit', 'sysvinit-inittab', | ||
| 72 | 'sysvinit-pidof', 'udev-cache', | ||
| 73 | 'update-alternatives-opkg', | ||
| 74 | 'update-rc.d', 'util-linux-libblkid', | ||
| 75 | 'util-linux-libuuid', 'v86d', 'zlib']) | ||
| 76 | |||
| 77 | class ZlibEventReplay(EventReplay): | ||
| 78 | """Replay zlib events""" | ||
| 79 | |||
| 80 | def test_replay_zlib(self): | ||
| 81 | """Test if zlib build and package are in the database""" | ||
| 82 | self._replay("zlib.events") | ||
| 83 | |||
| 84 | self.assertEqual(Build.objects.last().target_set.last().target, "zlib") | ||
| 85 | self.assertTrue('zlib' in Package.objects.values_list('name', flat=True)) | ||
diff --git a/bitbake/lib/toaster/tests/functional/README b/bitbake/lib/toaster/tests/functional/README deleted file mode 100644 index e69de29bb2..0000000000 --- a/bitbake/lib/toaster/tests/functional/README +++ /dev/null | |||
diff --git a/bitbake/lib/toaster/tests/functional/__init__.py b/bitbake/lib/toaster/tests/functional/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 --- a/bitbake/lib/toaster/tests/functional/__init__.py +++ /dev/null | |||
diff --git a/bitbake/lib/toaster/tests/functional/functional_helpers.py b/bitbake/lib/toaster/tests/functional/functional_helpers.py deleted file mode 100644 index e28f2024f5..0000000000 --- a/bitbake/lib/toaster/tests/functional/functional_helpers.py +++ /dev/null | |||
| @@ -1,224 +0,0 @@ | |||
| 1 | #! /usr/bin/env python3 | ||
| 2 | # | ||
| 3 | # BitBake Toaster functional tests implementation | ||
| 4 | # | ||
| 5 | # Copyright (C) 2017 Intel Corporation | ||
| 6 | # | ||
| 7 | # SPDX-License-Identifier: GPL-2.0-only | ||
| 8 | # | ||
| 9 | |||
| 10 | import os | ||
| 11 | import logging | ||
| 12 | import subprocess | ||
| 13 | import signal | ||
| 14 | import re | ||
| 15 | import requests | ||
| 16 | |||
| 17 | from django.urls import reverse | ||
| 18 | from tests.browser.selenium_helpers_base import SeleniumTestCaseBase | ||
| 19 | from selenium.webdriver.common.by import By | ||
| 20 | from selenium.webdriver.support.select import Select | ||
| 21 | from selenium.common.exceptions import NoSuchElementException | ||
| 22 | |||
| 23 | logger = logging.getLogger("toaster") | ||
| 24 | toaster_processes = [] | ||
| 25 | |||
| 26 | class SeleniumFunctionalTestCase(SeleniumTestCaseBase): | ||
| 27 | wait_toaster_time = 10 | ||
| 28 | |||
| 29 | @classmethod | ||
| 30 | def setUpClass(cls): | ||
| 31 | # So that the buildinfo helper uses the test database' | ||
| 32 | if os.environ.get('DJANGO_SETTINGS_MODULE', '') != \ | ||
| 33 | 'toastermain.settings_test': | ||
| 34 | raise RuntimeError("Please initialise django with the tests settings: " | ||
| 35 | "DJANGO_SETTINGS_MODULE='toastermain.settings_test'") | ||
| 36 | |||
| 37 | # Wait for any known toaster processes to exit | ||
| 38 | global toaster_processes | ||
| 39 | for toaster_process in toaster_processes: | ||
| 40 | try: | ||
| 41 | os.waitpid(toaster_process, os.WNOHANG) | ||
| 42 | except ChildProcessError: | ||
| 43 | pass | ||
| 44 | |||
| 45 | # start toaster | ||
| 46 | cmd = "bash -c 'source toaster start'" | ||
| 47 | start_process = subprocess.Popen( | ||
| 48 | cmd, | ||
| 49 | cwd=os.environ.get("BUILDDIR"), | ||
| 50 | shell=True) | ||
| 51 | toaster_processes = [start_process.pid] | ||
| 52 | if start_process.wait() != 0: | ||
| 53 | port_use = os.popen("lsof -i -P -n | grep '8000 (LISTEN)'").read().strip() | ||
| 54 | message = '' | ||
| 55 | if port_use: | ||
| 56 | process_id = port_use.split()[1] | ||
| 57 | process = os.popen(f"ps -o cmd= -p {process_id}").read().strip() | ||
| 58 | message = f"Port 8000 occupied by {process}" | ||
| 59 | raise RuntimeError(f"Can't initialize toaster. {message}") | ||
| 60 | |||
| 61 | builddir = os.environ.get("BUILDDIR") | ||
| 62 | with open(os.path.join(builddir, '.toastermain.pid'), 'r') as f: | ||
| 63 | toaster_processes.append(int(f.read())) | ||
| 64 | with open(os.path.join(builddir, '.runbuilds.pid'), 'r') as f: | ||
| 65 | toaster_processes.append(int(f.read())) | ||
| 66 | |||
| 67 | super(SeleniumFunctionalTestCase, cls).setUpClass() | ||
| 68 | cls.live_server_url = 'http://localhost:8000/' | ||
| 69 | |||
| 70 | @classmethod | ||
| 71 | def tearDownClass(cls): | ||
| 72 | super(SeleniumFunctionalTestCase, cls).tearDownClass() | ||
| 73 | |||
| 74 | global toaster_processes | ||
| 75 | |||
| 76 | cmd = "bash -c 'source toaster stop'" | ||
| 77 | stop_process = subprocess.Popen( | ||
| 78 | cmd, | ||
| 79 | cwd=os.environ.get("BUILDDIR"), | ||
| 80 | shell=True) | ||
| 81 | # Toaster stop has been known to hang in these tests so force kill if it stalls | ||
| 82 | try: | ||
| 83 | if stop_process.wait(cls.wait_toaster_time) != 0: | ||
| 84 | raise Exception('Toaster stop process failed') | ||
| 85 | except Exception as e: | ||
| 86 | if e is subprocess.TimeoutExpired: | ||
| 87 | print('Toaster stop process took too long. Force killing toaster...') | ||
| 88 | else: | ||
| 89 | print('Toaster stop process failed. Force killing toaster...') | ||
| 90 | stop_process.kill() | ||
| 91 | for toaster_process in toaster_processes: | ||
| 92 | os.kill(toaster_process, signal.SIGTERM) | ||
| 93 | |||
| 94 | |||
| 95 | def get_URL(self): | ||
| 96 | rc=self.get_page_source() | ||
| 97 | project_url=re.search(r"(projectPageUrl\s:\s\")(.*)(\",)",rc) | ||
| 98 | return project_url.group(2) | ||
| 99 | |||
| 100 | |||
| 101 | def find_element_by_link_text_in_table(self, table_id, link_text): | ||
| 102 | """ | ||
| 103 | Assume there're multiple suitable "find_element_by_link_text". | ||
| 104 | In this circumstance we need to specify "table". | ||
| 105 | """ | ||
| 106 | try: | ||
| 107 | table_element = self.get_table_element(table_id) | ||
| 108 | element = table_element.find_element(By.LINK_TEXT, link_text) | ||
| 109 | except NoSuchElementException: | ||
| 110 | print('no element found') | ||
| 111 | raise | ||
| 112 | return element | ||
| 113 | |||
| 114 | def get_table_element(self, table_id, *coordinate): | ||
| 115 | if len(coordinate) == 0: | ||
| 116 | #return whole-table element | ||
| 117 | element_xpath = "//*[@id='" + table_id + "']" | ||
| 118 | try: | ||
| 119 | element = self.driver.find_element(By.XPATH, element_xpath) | ||
| 120 | except NoSuchElementException: | ||
| 121 | raise | ||
| 122 | return element | ||
| 123 | row = coordinate[0] | ||
| 124 | |||
| 125 | if len(coordinate) == 1: | ||
| 126 | #return whole-row element | ||
| 127 | element_xpath = "//*[@id='" + table_id + "']/tbody/tr[" + str(row) + "]" | ||
| 128 | try: | ||
| 129 | element = self.driver.find_element(By.XPATH, element_xpath) | ||
| 130 | except NoSuchElementException: | ||
| 131 | return False | ||
| 132 | return element | ||
| 133 | #now we are looking for an element with specified X and Y | ||
| 134 | column = coordinate[1] | ||
| 135 | |||
| 136 | element_xpath = "//*[@id='" + table_id + "']/tbody/tr[" + str(row) + "]/td[" + str(column) + "]" | ||
| 137 | try: | ||
| 138 | element = self.driver.find_element(By.XPATH, element_xpath) | ||
| 139 | except NoSuchElementException: | ||
| 140 | return False | ||
| 141 | return element | ||
| 142 | |||
| 143 | def create_new_project( | ||
| 144 | self, | ||
| 145 | project_name, | ||
| 146 | release, | ||
| 147 | release_title, | ||
| 148 | merge_toaster_settings, | ||
| 149 | ): | ||
| 150 | """ Create/Test new project using: | ||
| 151 | - Project Name: Any string | ||
| 152 | - Release: Any string | ||
| 153 | - Merge Toaster settings: True or False | ||
| 154 | """ | ||
| 155 | |||
| 156 | # Obtain a CSRF token from a suitable URL | ||
| 157 | projs = requests.get(self.live_server_url + reverse('newproject')) | ||
| 158 | csrftoken = projs.cookies.get('csrftoken') | ||
| 159 | |||
| 160 | # Use the projects typeahead to find out if the project already exists | ||
| 161 | req = requests.get(self.live_server_url + reverse('xhr_projectstypeahead'), {'search': project_name, 'format' : 'json'}) | ||
| 162 | data = req.json() | ||
| 163 | # Delete any existing projects | ||
| 164 | for result in data['results']: | ||
| 165 | del_url = reverse('xhr_project', args=(result['id'],)) | ||
| 166 | del_response = requests.delete(self.live_server_url + del_url, cookies={'csrftoken': csrftoken}, headers={'X-CSRFToken': csrftoken}) | ||
| 167 | self.assertEqual(del_response.status_code, 200) | ||
| 168 | |||
| 169 | self.get(reverse('newproject')) | ||
| 170 | self.wait_until_visible('#new-project-name') | ||
| 171 | self.driver.find_element(By.ID, | ||
| 172 | "new-project-name").send_keys(project_name) | ||
| 173 | |||
| 174 | select = Select(self.find('#projectversion')) | ||
| 175 | select.select_by_value(release) | ||
| 176 | |||
| 177 | # check merge toaster settings | ||
| 178 | checkbox = self.find('.checkbox-mergeattr') | ||
| 179 | if merge_toaster_settings: | ||
| 180 | if not checkbox.is_selected(): | ||
| 181 | checkbox.click() | ||
| 182 | else: | ||
| 183 | if checkbox.is_selected(): | ||
| 184 | checkbox.click() | ||
| 185 | |||
| 186 | self.wait_until_clickable('#create-project-button') | ||
| 187 | |||
| 188 | self.driver.find_element(By.ID, "create-project-button").click() | ||
| 189 | |||
| 190 | element = self.wait_until_visible('#project-created-notification') | ||
| 191 | self.assertTrue( | ||
| 192 | self.element_exists('#project-created-notification'), | ||
| 193 | f"Project:{project_name} creation notification not shown" | ||
| 194 | ) | ||
| 195 | self.assertTrue( | ||
| 196 | project_name in element.text, | ||
| 197 | f"New project name:{project_name} not in new project notification" | ||
| 198 | ) | ||
| 199 | |||
| 200 | # Use the projects typeahead again to check the project now exists | ||
| 201 | req = requests.get(self.live_server_url + reverse('xhr_projectstypeahead'), {'search': project_name, 'format' : 'json'}) | ||
| 202 | data = req.json() | ||
| 203 | self.assertGreater(len(data['results']), 0, f"New project:{project_name} not found in database") | ||
| 204 | |||
| 205 | project_id = data['results'][0]['id'] | ||
| 206 | |||
| 207 | self.wait_until_visible('#project-release-title') | ||
| 208 | |||
| 209 | # check release | ||
| 210 | if release_title is not None: | ||
| 211 | self.assertTrue(re.search( | ||
| 212 | release_title, | ||
| 213 | self.driver.find_element(By.XPATH, | ||
| 214 | "//span[@id='project-release-title']" | ||
| 215 | ).text), | ||
| 216 | 'The project release is not defined') | ||
| 217 | |||
| 218 | return project_id | ||
| 219 | |||
| 220 | def load_projects_page_helper(self): | ||
| 221 | self.wait_until_present('#projectstable') | ||
| 222 | # Need to wait for some data in the table too | ||
| 223 | self.wait_until_present('td[class="updated"]') | ||
| 224 | |||
diff --git a/bitbake/lib/toaster/tests/functional/test_create_new_project.py b/bitbake/lib/toaster/tests/functional/test_create_new_project.py deleted file mode 100644 index 66213c736e..0000000000 --- a/bitbake/lib/toaster/tests/functional/test_create_new_project.py +++ /dev/null | |||
| @@ -1,124 +0,0 @@ | |||
| 1 | #! /usr/bin/env python3 | ||
| 2 | # BitBake Toaster UI tests implementation | ||
| 3 | # | ||
| 4 | # Copyright (C) 2023 Savoir-faire Linux | ||
| 5 | # | ||
| 6 | # SPDX-License-Identifier: GPL-2.0-only | ||
| 7 | # | ||
| 8 | |||
| 9 | import re | ||
| 10 | import pytest | ||
| 11 | from django.urls import reverse | ||
| 12 | from selenium.webdriver.support.select import Select | ||
| 13 | from tests.functional.functional_helpers import SeleniumFunctionalTestCase | ||
| 14 | from selenium.webdriver.common.by import By | ||
| 15 | |||
| 16 | class TestCreateNewProject(SeleniumFunctionalTestCase): | ||
| 17 | |||
| 18 | def test_create_new_project_master(self): | ||
| 19 | """ Test create new project using: | ||
| 20 | - Project Name: Any string | ||
| 21 | - Release: Yocto Project master (option value: 3) | ||
| 22 | - Merge Toaster settings: False | ||
| 23 | """ | ||
| 24 | release = '3' | ||
| 25 | release_title = 'Yocto Project master' | ||
| 26 | project_name = 'projectmaster' | ||
| 27 | self.create_new_project( | ||
| 28 | project_name, | ||
| 29 | release, | ||
| 30 | release_title, | ||
| 31 | False, | ||
| 32 | ) | ||
| 33 | |||
| 34 | def test_create_new_project_scarthgap(self): | ||
| 35 | """ Test create new project using: | ||
| 36 | - Project Name: Any string | ||
| 37 | - Release: Yocto Project 5.0 "Scarthgap" (option value: 1) | ||
| 38 | - Merge Toaster settings: True | ||
| 39 | """ | ||
| 40 | release = '1' | ||
| 41 | release_title = 'Yocto Project 5.0 "Scarthgap"' | ||
| 42 | project_name = 'projectscarthgap' | ||
| 43 | self.create_new_project( | ||
| 44 | project_name, | ||
| 45 | release, | ||
| 46 | release_title, | ||
| 47 | True, | ||
| 48 | ) | ||
| 49 | |||
| 50 | def test_create_new_project_kirkstone(self): | ||
| 51 | """ Test create new project using: | ||
| 52 | - Project Name: Any string | ||
| 53 | - Release: Yocto Project 4.0 "Kirkstone" (option value: 6) | ||
| 54 | - Merge Toaster settings: True | ||
| 55 | """ | ||
| 56 | release = '7' | ||
| 57 | release_title = 'Yocto Project 4.0 "Kirkstone"' | ||
| 58 | project_name = 'projectkirkstone' | ||
| 59 | self.create_new_project( | ||
| 60 | project_name, | ||
| 61 | release, | ||
| 62 | release_title, | ||
| 63 | True, | ||
| 64 | ) | ||
| 65 | |||
| 66 | def test_create_new_project_local(self): | ||
| 67 | """ Test create new project using: | ||
| 68 | - Project Name: Any string | ||
| 69 | - Release: Local Yocto Project (option value: 2) | ||
| 70 | - Merge Toaster settings: True | ||
| 71 | """ | ||
| 72 | release = '2' | ||
| 73 | release_title = 'Local Yocto Project' | ||
| 74 | project_name = 'projectlocal' | ||
| 75 | self.create_new_project( | ||
| 76 | project_name, | ||
| 77 | release, | ||
| 78 | release_title, | ||
| 79 | True, | ||
| 80 | ) | ||
| 81 | |||
| 82 | def test_create_new_project_without_name(self): | ||
| 83 | """ Test create new project without project name """ | ||
| 84 | self.get(reverse('newproject')) | ||
| 85 | |||
| 86 | select = Select(self.find('#projectversion')) | ||
| 87 | select.select_by_value(str(3)) | ||
| 88 | |||
| 89 | # Check input name has required attribute | ||
| 90 | input_name = self.driver.find_element(By.ID, "new-project-name") | ||
| 91 | self.assertIsNotNone(input_name.get_attribute('required'), | ||
| 92 | 'Input name has not required attribute') | ||
| 93 | |||
| 94 | # Check create button is disabled | ||
| 95 | create_btn = self.driver.find_element(By.ID, "create-project-button") | ||
| 96 | self.assertIsNotNone(create_btn.get_attribute('disabled'), | ||
| 97 | 'Create button is not disabled') | ||
| 98 | |||
| 99 | def test_import_new_project(self): | ||
| 100 | """ Test import new project using: | ||
| 101 | - Project Name: Any string | ||
| 102 | - Project type: select (Import command line project) | ||
| 103 | - Import existing project directory: Wrong Path | ||
| 104 | """ | ||
| 105 | project_name = 'projectimport' | ||
| 106 | self.get(reverse('newproject')) | ||
| 107 | self.driver.find_element(By.ID, | ||
| 108 | "new-project-name").send_keys(project_name) | ||
| 109 | # select import project | ||
| 110 | self.find('#type-import').click() | ||
| 111 | |||
| 112 | # set wrong path | ||
| 113 | wrong_path = '/wrongpath' | ||
| 114 | self.driver.find_element(By.ID, | ||
| 115 | "import-project-dir").send_keys(wrong_path) | ||
| 116 | self.driver.find_element(By.ID, "create-project-button").click() | ||
| 117 | |||
| 118 | self.wait_until_visible('.alert-danger') | ||
| 119 | |||
| 120 | # check error message | ||
| 121 | self.assertTrue(self.element_exists('.alert-danger'), | ||
| 122 | 'Alert message not shown') | ||
| 123 | self.assertTrue(wrong_path in self.find('.alert-danger').text, | ||
| 124 | "Wrong path not in alert message") | ||
diff --git a/bitbake/lib/toaster/tests/functional/test_functional_basic.py b/bitbake/lib/toaster/tests/functional/test_functional_basic.py deleted file mode 100644 index d5c9708617..0000000000 --- a/bitbake/lib/toaster/tests/functional/test_functional_basic.py +++ /dev/null | |||
| @@ -1,257 +0,0 @@ | |||
| 1 | #! /usr/bin/env python3 | ||
| 2 | # | ||
| 3 | # BitBake Toaster functional tests implementation | ||
| 4 | # | ||
| 5 | # Copyright (C) 2017 Intel Corporation | ||
| 6 | # | ||
| 7 | # SPDX-License-Identifier: GPL-2.0-only | ||
| 8 | # | ||
| 9 | |||
| 10 | import re | ||
| 11 | from django.urls import reverse | ||
| 12 | import pytest | ||
| 13 | from tests.functional.functional_helpers import SeleniumFunctionalTestCase | ||
| 14 | from orm.models import Project | ||
| 15 | from selenium.webdriver.common.by import By | ||
| 16 | |||
| 17 | from tests.functional.utils import get_projectId_from_url | ||
| 18 | |||
| 19 | |||
| 20 | class FuntionalTestBasic(SeleniumFunctionalTestCase): | ||
| 21 | """Basic functional tests for Toaster""" | ||
| 22 | project_id = None | ||
| 23 | project_url = None | ||
| 24 | |||
| 25 | def setUp(self): | ||
| 26 | super(FuntionalTestBasic, self).setUp() | ||
| 27 | if not FuntionalTestBasic.project_id: | ||
| 28 | FuntionalTestBasic.project_id = self.create_new_project('selenium-project', '3', None, False) | ||
| 29 | |||
| 30 | # testcase (1515) | ||
| 31 | def test_verify_left_bar_menu(self): | ||
| 32 | self.get(reverse('all-projects')) | ||
| 33 | self.load_projects_page_helper() | ||
| 34 | self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click() | ||
| 35 | self.wait_until_present('#config-nav') | ||
| 36 | self.assertTrue(self.element_exists('#config-nav'),'Configuration Tab does not exist') | ||
| 37 | project_URL=self.get_URL() | ||
| 38 | self.driver.find_element(By.XPATH, '//a[@href="'+project_URL+'"]').click() | ||
| 39 | |||
| 40 | try: | ||
| 41 | self.wait_until_present('#config-nav') | ||
| 42 | self.driver.find_element(By.XPATH, "//*[@id='config-nav']/ul/li/a[@href="+'"'+project_URL+'customimages/"'+"]").click() | ||
| 43 | self.wait_until_present('#filter-modal-customimagestable') | ||
| 44 | except: | ||
| 45 | self.fail(msg='No Custom images tab available') | ||
| 46 | 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') | ||
| 47 | |||
| 48 | try: | ||
| 49 | self.driver.find_element(By.XPATH, "//*[@id='config-nav']/ul/li/a[@href="+'"'+project_URL+'images/"'+"]").click() | ||
| 50 | self.wait_until_present('#filter-modal-imagerecipestable') | ||
| 51 | except: | ||
| 52 | self.fail(msg='No Compatible image tab available') | ||
| 53 | 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') | ||
| 54 | |||
| 55 | try: | ||
| 56 | self.driver.find_element(By.XPATH, "//*[@id='config-nav']/ul/li/a[@href="+'"'+project_URL+'softwarerecipes/"'+"]").click() | ||
| 57 | self.wait_until_present('#filter-modal-softwarerecipestable') | ||
| 58 | except: | ||
| 59 | self.fail(msg='No Compatible software recipe tab available') | ||
| 60 | 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') | ||
| 61 | |||
| 62 | try: | ||
| 63 | self.driver.find_element(By.XPATH, "//*[@id='config-nav']/ul/li/a[@href="+'"'+project_URL+'machines/"'+"]").click() | ||
| 64 | self.wait_until_present('#filter-modal-machinestable') | ||
| 65 | except: | ||
| 66 | self.fail(msg='No Compatible machines tab available') | ||
| 67 | 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') | ||
| 68 | |||
| 69 | try: | ||
| 70 | self.driver.find_element(By.XPATH, "//*[@id='config-nav']/ul/li/a[@href="+'"'+project_URL+'layers/"'+"]").click() | ||
| 71 | self.wait_until_present('#filter-modal-layerstable') | ||
| 72 | except: | ||
| 73 | self.fail(msg='No Compatible layers tab available') | ||
| 74 | 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') | ||
| 75 | |||
| 76 | try: | ||
| 77 | self.driver.find_element(By.XPATH, "//*[@id='config-nav']/ul/li/a[@href="+'"'+project_URL+'configuration"'+"]").click() | ||
| 78 | self.wait_until_present('#configvar-list') | ||
| 79 | except: | ||
| 80 | self.fail(msg='No Bitbake variables tab available') | ||
| 81 | 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') | ||
| 82 | |||
| 83 | # testcase (1516) | ||
| 84 | def test_review_configuration_information(self): | ||
| 85 | self.get(reverse('all-projects')) | ||
| 86 | self.load_projects_page_helper() | ||
| 87 | self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click() | ||
| 88 | project_URL=self.get_URL() | ||
| 89 | |||
| 90 | # Machine section of page | ||
| 91 | self.wait_until_visible('#machine-section') | ||
| 92 | self.assertTrue(self.element_exists('#machine-section'),'Machine section for the project configuration page does not exist') | ||
| 93 | self.assertTrue(re.search("qemux86-64",self.driver.find_element(By.XPATH, "//span[@id='project-machine-name']").text),'The machine type is not assigned') | ||
| 94 | try: | ||
| 95 | self.driver.find_element(By.XPATH, "//span[@id='change-machine-toggle']").click() | ||
| 96 | self.wait_until_visible('#select-machine-form') | ||
| 97 | self.wait_until_visible('#cancel-machine-change') | ||
| 98 | self.driver.find_element(By.XPATH, "//form[@id='select-machine-form']/a[@id='cancel-machine-change']").click() | ||
| 99 | except: | ||
| 100 | self.fail(msg='The machine information is wrong in the configuration page') | ||
| 101 | |||
| 102 | # Most built recipes section | ||
| 103 | self.wait_until_visible('#no-most-built') | ||
| 104 | try: | ||
| 105 | self.driver.find_element(By.ID, 'no-most-built') | ||
| 106 | except: | ||
| 107 | self.fail(msg='No Most built information in project detail page') | ||
| 108 | |||
| 109 | # Project Release title | ||
| 110 | 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 in the project detail page') | ||
| 111 | |||
| 112 | # List of layers in project | ||
| 113 | self.wait_until_visible('#layer-container') | ||
| 114 | self.driver.find_element(By.XPATH, "//div[@id='layer-container']") | ||
| 115 | 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') | ||
| 116 | try: | ||
| 117 | layer_list = self.driver.find_element(By.ID, "layers-in-project-list") | ||
| 118 | layers = layer_list.find_elements(By.TAG_NAME, "li") | ||
| 119 | except: | ||
| 120 | self.fail(msg='No Layer information in project detail page') | ||
| 121 | |||
| 122 | for layer in layers: | ||
| 123 | if re.match ("openembedded-core", layer.text): | ||
| 124 | print ("openembedded-core layer is a default layer in the project configuration") | ||
| 125 | elif re.match ("meta-poky", layer.text): | ||
| 126 | print ("meta-poky layer is a default layer in the project configuration") | ||
| 127 | elif re.match ("meta-yocto-bsp", layer.text): | ||
| 128 | print ("meta-yocto-bsp is a default layer in the project configuratoin") | ||
| 129 | else: | ||
| 130 | self.fail(msg='default layers are missing from the project configuration') | ||
| 131 | |||
| 132 | # testcase (1517) | ||
| 133 | def test_verify_machine_information(self): | ||
| 134 | self.get(reverse('all-projects')) | ||
| 135 | self.load_projects_page_helper() | ||
| 136 | self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click() | ||
| 137 | |||
| 138 | self.wait_until_visible('#machine-section') | ||
| 139 | self.assertTrue(self.element_exists('#machine-section'),'Machine section for the project configuration page does not exist') | ||
| 140 | self.wait_until_visible('#project-machine-name') | ||
| 141 | self.assertTrue(re.search("qemux86-64",self.driver.find_element(By.ID, "project-machine-name").text),'The machine type is not assigned') | ||
| 142 | try: | ||
| 143 | self.driver.find_element(By.ID, "change-machine-toggle").click() | ||
| 144 | self.wait_until_visible('#select-machine-form') | ||
| 145 | self.wait_until_visible('#cancel-machine-change') | ||
| 146 | self.driver.find_element(By.ID, "cancel-machine-change").click() | ||
| 147 | except: | ||
| 148 | self.fail(msg='The machine information is wrong in the configuration page') | ||
| 149 | |||
| 150 | # testcase (1518) | ||
| 151 | def test_verify_most_built_recipes_information(self): | ||
| 152 | self.get(reverse('all-projects')) | ||
| 153 | self.load_projects_page_helper() | ||
| 154 | self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click() | ||
| 155 | self.wait_until_present('#config-nav') | ||
| 156 | project_URL=self.get_URL() | ||
| 157 | |||
| 158 | self.wait_until_visible('#no-most-built') | ||
| 159 | 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') | ||
| 160 | try: | ||
| 161 | self.driver.find_element(By.XPATH, "//div[@id='no-most-built']/p/a[@href="+'"'+project_URL+'images/"'+"]").click() | ||
| 162 | except: | ||
| 163 | self.fail(msg='No Most built information in project detail page') | ||
| 164 | self.wait_until_visible('#config-nav') | ||
| 165 | 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') | ||
| 166 | |||
| 167 | # testcase (1519) | ||
| 168 | def test_verify_project_release_information(self): | ||
| 169 | self.get(reverse('all-projects')) | ||
| 170 | self.load_projects_page_helper() | ||
| 171 | self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click() | ||
| 172 | self.wait_until_visible('#project-release-title') | ||
| 173 | self.assertTrue(re.search("Yocto Project master",self.driver.find_element(By.ID, "project-release-title").text), 'No project release title information in project detail page') | ||
| 174 | |||
| 175 | # testcase (1520) | ||
| 176 | def test_verify_layer_information(self): | ||
| 177 | self.get(reverse('all-projects')) | ||
| 178 | self.load_projects_page_helper() | ||
| 179 | self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click() | ||
| 180 | self.wait_until_present('#config-nav') | ||
| 181 | project_URL=self.get_URL() | ||
| 182 | self.wait_until_visible('#layer-container') | ||
| 183 | self.driver.find_element(By.XPATH, "//div[@id='layer-container']") | ||
| 184 | self.wait_until_visible('#project-layers-count') | ||
| 185 | 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') | ||
| 186 | |||
| 187 | try: | ||
| 188 | layer_list = self.driver.find_element(By.ID, "layers-in-project-list") | ||
| 189 | layers = layer_list.find_elements(By.TAG_NAME, "li") | ||
| 190 | except: | ||
| 191 | self.fail(msg='No Layer information in project detail page') | ||
| 192 | |||
| 193 | for layer in layers: | ||
| 194 | if re.match ("openembedded-core",layer.text): | ||
| 195 | print ("openembedded-core layer is a default layer in the project configuration") | ||
| 196 | elif re.match ("meta-poky",layer.text): | ||
| 197 | print ("meta-poky layer is a default layer in the project configuration") | ||
| 198 | elif re.match ("meta-yocto-bsp",layer.text): | ||
| 199 | print ("meta-yocto-bsp is a default layer in the project configuratoin") | ||
| 200 | else: | ||
| 201 | self.fail(msg='default layers are missing from the project configuration') | ||
| 202 | |||
| 203 | try: | ||
| 204 | self.driver.find_element(By.XPATH, "//input[@id='layer-add-input']") | ||
| 205 | self.driver.find_element(By.XPATH, "//button[@id='add-layer-btn']") | ||
| 206 | self.driver.find_element(By.XPATH, "//div[@id='layer-container']/form[@class='form-inline']/p/a[@id='view-compatible-layers']") | ||
| 207 | self.driver.find_element(By.XPATH, "//div[@id='layer-container']/form[@class='form-inline']/p/a[@href="+'"'+project_URL+'importlayer"'+"]") | ||
| 208 | except: | ||
| 209 | self.fail(msg='Layer configuration controls missing') | ||
| 210 | |||
| 211 | # testcase (1521) | ||
| 212 | def test_verify_project_detail_links(self): | ||
| 213 | self.get(reverse('all-projects')) | ||
| 214 | self.load_projects_page_helper() | ||
| 215 | self.find_element_by_link_text_in_table('projectstable', 'selenium-project').click() | ||
| 216 | self.wait_until_present('#config-nav') | ||
| 217 | project_URL=self.get_URL() | ||
| 218 | 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() | ||
| 219 | self.wait_until_visible('#topbar-configuration-tab') | ||
| 220 | 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') | ||
| 221 | |||
| 222 | try: | ||
| 223 | self.driver.find_element(By.XPATH, "//div[@id='project-topbar']/ul[@class='nav nav-tabs']/li/a[@href="+'"'+project_URL+'builds/"'+"]").click() | ||
| 224 | except: | ||
| 225 | self.fail(msg='Builds tab information is not present') | ||
| 226 | |||
| 227 | self.wait_until_visible('#project-topbar') | ||
| 228 | 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') | ||
| 229 | try: | ||
| 230 | self.driver.find_element(By.XPATH, "//div[@id='empty-state-projectbuildstable']") | ||
| 231 | except: | ||
| 232 | self.fail(msg='Builds tab information is not present') | ||
| 233 | |||
| 234 | try: | ||
| 235 | self.driver.find_element(By.XPATH, "//div[@id='project-topbar']/ul[@class='nav nav-tabs']/li/a[@href="+'"'+project_URL+'importlayer"'+"]").click() | ||
| 236 | except: | ||
| 237 | self.fail(msg='Import layer tab not loading properly') | ||
| 238 | |||
| 239 | self.wait_until_visible('#project-topbar') | ||
| 240 | 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') | ||
| 241 | try: | ||
| 242 | self.driver.find_element(By.XPATH, "//fieldset[@id='repo-select']") | ||
| 243 | self.driver.find_element(By.XPATH, "//fieldset[@id='git-repo']") | ||
| 244 | except: | ||
| 245 | self.fail(msg='Import layer tab not loading properly') | ||
| 246 | |||
| 247 | try: | ||
| 248 | self.driver.find_element(By.XPATH, "//div[@id='project-topbar']/ul[@class='nav nav-tabs']/li/a[@href="+'"'+project_URL+'newcustomimage/"'+"]").click() | ||
| 249 | except: | ||
| 250 | self.fail(msg='New custom image tab not loading properly') | ||
| 251 | |||
| 252 | self.wait_until_visible('#project-topbar') | ||
| 253 | 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') | ||
| 254 | 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') | ||
| 255 | |||
| 256 | |||
| 257 | |||
diff --git a/bitbake/lib/toaster/tests/functional/test_project_config.py b/bitbake/lib/toaster/tests/functional/test_project_config.py deleted file mode 100644 index fcb1bc3284..0000000000 --- a/bitbake/lib/toaster/tests/functional/test_project_config.py +++ /dev/null | |||
| @@ -1,294 +0,0 @@ | |||
| 1 | #! /usr/bin/env python3 # | ||
| 2 | # BitBake Toaster UI tests implementation | ||
| 3 | # | ||
| 4 | # Copyright (C) 2023 Savoir-faire Linux | ||
| 5 | # | ||
| 6 | # SPDX-License-Identifier: GPL-2.0-only | ||
| 7 | # | ||
| 8 | |||
| 9 | import string | ||
| 10 | import pytest | ||
| 11 | from django.urls import reverse | ||
| 12 | from selenium.webdriver import Keys | ||
| 13 | from selenium.webdriver.support.select import Select | ||
| 14 | from selenium.common.exceptions import TimeoutException | ||
| 15 | from tests.functional.functional_helpers import SeleniumFunctionalTestCase | ||
| 16 | from selenium.webdriver.common.by import By | ||
| 17 | |||
| 18 | from .utils import get_projectId_from_url | ||
| 19 | |||
| 20 | class TestProjectConfig(SeleniumFunctionalTestCase): | ||
| 21 | project_id = None | ||
| 22 | PROJECT_NAME = 'TestProjectConfig' | ||
| 23 | INVALID_PATH_START_TEXT = 'The directory path should either start with a /' | ||
| 24 | INVALID_PATH_CHAR_TEXT = 'The directory path cannot include spaces or ' \ | ||
| 25 | 'any of these characters' | ||
| 26 | |||
| 27 | def _get_config_nav_item(self, index): | ||
| 28 | config_nav = self.find('#config-nav') | ||
| 29 | return config_nav.find_elements(By.TAG_NAME, 'li')[index] | ||
| 30 | |||
| 31 | def _navigate_bbv_page(self): | ||
| 32 | """ Navigate to project BitBake variables page """ | ||
| 33 | # check if the menu is displayed | ||
| 34 | if TestProjectConfig.project_id is None: | ||
| 35 | TestProjectConfig.project_id = self.create_new_project(self.PROJECT_NAME, '3', None, True) | ||
| 36 | |||
| 37 | url = reverse('projectconf', args=(TestProjectConfig.project_id,)) | ||
| 38 | self.get(url) | ||
| 39 | self.wait_until_visible('#config-nav') | ||
| 40 | bbv_page_link = self._get_config_nav_item(9) | ||
| 41 | bbv_page_link.click() | ||
| 42 | self.wait_until_visible('#config-nav') | ||
| 43 | |||
| 44 | def test_no_underscore_iamgefs_type(self): | ||
| 45 | """ | ||
| 46 | Should not accept IMAGEFS_TYPE with an underscore | ||
| 47 | """ | ||
| 48 | self._navigate_bbv_page() | ||
| 49 | imagefs_type = "foo_bar" | ||
| 50 | |||
| 51 | self.wait_until_visible('#change-image_fstypes-icon') | ||
| 52 | |||
| 53 | self.click('#change-image_fstypes-icon') | ||
| 54 | |||
| 55 | self.enter_text('#new-imagefs_types', imagefs_type) | ||
| 56 | |||
| 57 | element = self.wait_until_visible('#hintError-image-fs_type') | ||
| 58 | |||
| 59 | self.assertTrue(("A valid image type cannot include underscores" in element.text), | ||
| 60 | "Did not find underscore error message") | ||
| 61 | |||
| 62 | def test_checkbox_verification(self): | ||
| 63 | """ | ||
| 64 | Should automatically check the checkbox if user enters value | ||
| 65 | text box, if value is there in the checkbox. | ||
| 66 | """ | ||
| 67 | self._navigate_bbv_page() | ||
| 68 | |||
| 69 | imagefs_type = "btrfs" | ||
| 70 | |||
| 71 | self.wait_until_visible('#change-image_fstypes-icon') | ||
| 72 | |||
| 73 | self.click('#change-image_fstypes-icon') | ||
| 74 | |||
| 75 | self.enter_text('#new-imagefs_types', imagefs_type) | ||
| 76 | |||
| 77 | checkboxes = self.driver.find_elements(By.XPATH, "//input[@class='fs-checkbox-fstypes']") | ||
| 78 | |||
| 79 | for checkbox in checkboxes: | ||
| 80 | if checkbox.get_attribute("value") == "btrfs": | ||
| 81 | self.assertEqual(checkbox.is_selected(), True) | ||
| 82 | |||
| 83 | def test_textbox_with_checkbox_verification(self): | ||
| 84 | """ | ||
| 85 | Should automatically add or remove value in textbox, if user checks | ||
| 86 | or unchecks checkboxes. | ||
| 87 | """ | ||
| 88 | self._navigate_bbv_page() | ||
| 89 | |||
| 90 | self.wait_until_visible('#change-image_fstypes-icon') | ||
| 91 | self.click('#change-image_fstypes-icon') | ||
| 92 | |||
| 93 | checkboxes_selector = '.fs-checkbox-fstypes' | ||
| 94 | |||
| 95 | self.wait_until_visible(checkboxes_selector) | ||
| 96 | checkboxes = self.find_all(checkboxes_selector) | ||
| 97 | |||
| 98 | for checkbox in checkboxes: | ||
| 99 | if checkbox.get_attribute("value") == "cpio": | ||
| 100 | checkbox.click() | ||
| 101 | self.wait_until_visible('#new-imagefs_types') | ||
| 102 | element = self.driver.find_element(By.ID, 'new-imagefs_types') | ||
| 103 | |||
| 104 | self.assertTrue(("cpio" in element.get_attribute('value'), | ||
| 105 | "Imagefs not added into the textbox")) | ||
| 106 | checkbox.click() | ||
| 107 | self.assertTrue(("cpio" not in element.text), | ||
| 108 | "Image still present in the textbox") | ||
| 109 | |||
| 110 | def test_set_download_dir(self): | ||
| 111 | """ | ||
| 112 | Validate the allowed and disallowed types in the directory field for | ||
| 113 | DL_DIR | ||
| 114 | """ | ||
| 115 | self._navigate_bbv_page() | ||
| 116 | |||
| 117 | # activate the input to edit download dir | ||
| 118 | try: | ||
| 119 | change_dl_dir_btn = self.wait_until_visible('#change-dl_dir-icon') | ||
| 120 | except TimeoutException: | ||
| 121 | # If download dir is not displayed, test is skipped | ||
| 122 | change_dl_dir_btn = None | ||
| 123 | |||
| 124 | if change_dl_dir_btn: | ||
| 125 | change_dl_dir_btn.click() | ||
| 126 | |||
| 127 | # downloads dir path doesn't start with / or ${...} | ||
| 128 | input_field = self.wait_until_visible('#new-dl_dir') | ||
| 129 | input_field.clear() | ||
| 130 | self.enter_text('#new-dl_dir', 'home/foo') | ||
| 131 | element = self.wait_until_visible('#hintError-initialChar-dl_dir') | ||
| 132 | |||
| 133 | msg = 'downloads directory path starts with invalid character but ' \ | ||
| 134 | 'treated as valid' | ||
| 135 | self.assertTrue((self.INVALID_PATH_START_TEXT in element.text), msg) | ||
| 136 | |||
| 137 | # downloads dir path has a space | ||
| 138 | self.driver.find_element(By.ID, 'new-dl_dir').clear() | ||
| 139 | self.enter_text('#new-dl_dir', '/foo/bar a') | ||
| 140 | |||
| 141 | element = self.wait_until_visible('#hintError-dl_dir') | ||
| 142 | msg = 'downloads directory path characters invalid but treated as valid' | ||
| 143 | self.assertTrue((self.INVALID_PATH_CHAR_TEXT in element.text), msg) | ||
| 144 | |||
| 145 | # downloads dir path starts with ${...} but has a space | ||
| 146 | self.driver.find_element(By.ID,'new-dl_dir').clear() | ||
| 147 | self.enter_text('#new-dl_dir', '${TOPDIR}/down foo') | ||
| 148 | |||
| 149 | element = self.wait_until_visible('#hintError-dl_dir') | ||
| 150 | msg = 'downloads directory path characters invalid but treated as valid' | ||
| 151 | self.assertTrue((self.INVALID_PATH_CHAR_TEXT in element.text), msg) | ||
| 152 | |||
| 153 | # downloads dir path starts with / | ||
| 154 | self.driver.find_element(By.ID,'new-dl_dir').clear() | ||
| 155 | self.enter_text('#new-dl_dir', '/bar/foo') | ||
| 156 | |||
| 157 | hidden_element = self.driver.find_element(By.ID,'hintError-dl_dir') | ||
| 158 | self.assertEqual(hidden_element.is_displayed(), False, | ||
| 159 | 'downloads directory path valid but treated as invalid') | ||
| 160 | |||
| 161 | # downloads dir path starts with ${...} | ||
| 162 | self.driver.find_element(By.ID,'new-dl_dir').clear() | ||
| 163 | self.enter_text('#new-dl_dir', '${TOPDIR}/down') | ||
| 164 | |||
| 165 | hidden_element = self.driver.find_element(By.ID,'hintError-dl_dir') | ||
| 166 | self.assertEqual(hidden_element.is_displayed(), False, | ||
| 167 | 'downloads directory path valid but treated as invalid') | ||
| 168 | |||
| 169 | def test_set_sstate_dir(self): | ||
| 170 | """ | ||
| 171 | Validate the allowed and disallowed types in the directory field for | ||
| 172 | SSTATE_DIR | ||
| 173 | """ | ||
| 174 | self._navigate_bbv_page() | ||
| 175 | |||
| 176 | try: | ||
| 177 | btn_chg_sstate_dir = self.wait_until_visible('#change-sstate_dir-icon') | ||
| 178 | self.click('#change-sstate_dir-icon') | ||
| 179 | except TimeoutException: | ||
| 180 | # If sstate_dir is not displayed, test is skipped | ||
| 181 | btn_chg_sstate_dir = None | ||
| 182 | |||
| 183 | if btn_chg_sstate_dir: # Skip continuation if sstate_dir is not displayed | ||
| 184 | # path doesn't start with / or ${...} | ||
| 185 | input_field = self.wait_until_visible('#new-sstate_dir') | ||
| 186 | input_field.clear() | ||
| 187 | self.enter_text('#new-sstate_dir', 'home/foo') | ||
| 188 | element = self.wait_until_visible('#hintError-initialChar-sstate_dir') | ||
| 189 | |||
| 190 | msg = 'sstate directory path starts with invalid character but ' \ | ||
| 191 | 'treated as valid' | ||
| 192 | self.assertTrue((self.INVALID_PATH_START_TEXT in element.text), msg) | ||
| 193 | |||
| 194 | # path has a space | ||
| 195 | self.driver.find_element(By.ID, 'new-sstate_dir').clear() | ||
| 196 | self.enter_text('#new-sstate_dir', '/foo/bar a') | ||
| 197 | |||
| 198 | element = self.wait_until_visible('#hintError-sstate_dir') | ||
| 199 | msg = 'sstate directory path characters invalid but treated as valid' | ||
| 200 | self.assertTrue((self.INVALID_PATH_CHAR_TEXT in element.text), msg) | ||
| 201 | |||
| 202 | # path starts with ${...} but has a space | ||
| 203 | self.driver.find_element(By.ID,'new-sstate_dir').clear() | ||
| 204 | self.enter_text('#new-sstate_dir', '${TOPDIR}/down foo') | ||
| 205 | |||
| 206 | element = self.wait_until_visible('#hintError-sstate_dir') | ||
| 207 | msg = 'sstate directory path characters invalid but treated as valid' | ||
| 208 | self.assertTrue((self.INVALID_PATH_CHAR_TEXT in element.text), msg) | ||
| 209 | |||
| 210 | # path starts with / | ||
| 211 | self.driver.find_element(By.ID,'new-sstate_dir').clear() | ||
| 212 | self.enter_text('#new-sstate_dir', '/bar/foo') | ||
| 213 | |||
| 214 | hidden_element = self.driver.find_element(By.ID, 'hintError-sstate_dir') | ||
| 215 | self.assertEqual(hidden_element.is_displayed(), False, | ||
| 216 | 'sstate directory path valid but treated as invalid') | ||
| 217 | |||
| 218 | # paths starts with ${...} | ||
| 219 | self.driver.find_element(By.ID, 'new-sstate_dir').clear() | ||
| 220 | self.enter_text('#new-sstate_dir', '${TOPDIR}/down') | ||
| 221 | |||
| 222 | hidden_element = self.driver.find_element(By.ID, 'hintError-sstate_dir') | ||
| 223 | self.assertEqual(hidden_element.is_displayed(), False, | ||
| 224 | 'sstate directory path valid but treated as invalid') | ||
| 225 | |||
| 226 | def _change_bbv_value(self, **kwargs): | ||
| 227 | var_name, field, btn_id, input_id, value, save_btn, *_ = kwargs.values() | ||
| 228 | """ Change bitbake variable value """ | ||
| 229 | self._navigate_bbv_page() | ||
| 230 | self.wait_until_visible(f'#{btn_id}') | ||
| 231 | if kwargs.get('new_variable'): | ||
| 232 | self.find(f"#{btn_id}").clear() | ||
| 233 | self.enter_text(f"#{btn_id}", f"{var_name}") | ||
| 234 | else: | ||
| 235 | self.click(f'#{btn_id}') | ||
| 236 | |||
| 237 | self.wait_until_visible(f'#{input_id}') | ||
| 238 | |||
| 239 | if kwargs.get('is_select'): | ||
| 240 | select = Select(self.find(f'#{input_id}')) | ||
| 241 | select.select_by_visible_text(value) | ||
| 242 | else: | ||
| 243 | self.find(f"#{input_id}").clear() | ||
| 244 | self.enter_text(f'#{input_id}', f'{value}') | ||
| 245 | self.click(f'#{save_btn}') | ||
| 246 | value_displayed = str(self.wait_until_visible(f'#{field}').text).lower() | ||
| 247 | msg = f'{var_name} variable not changed' | ||
| 248 | self.assertTrue(str(value).lower() in value_displayed, msg) | ||
| 249 | |||
| 250 | def test_change_distro_var(self): | ||
| 251 | """ Test changing distro variable """ | ||
| 252 | self._change_bbv_value( | ||
| 253 | var_name='DISTRO', | ||
| 254 | field='distro', | ||
| 255 | btn_id='change-distro-icon', | ||
| 256 | input_id='new-distro', | ||
| 257 | value='poky-changed', | ||
| 258 | save_btn="apply-change-distro", | ||
| 259 | ) | ||
| 260 | |||
| 261 | def test_set_image_install_append_var(self): | ||
| 262 | """ Test setting IMAGE_INSTALL:append variable """ | ||
| 263 | self._change_bbv_value( | ||
| 264 | var_name='IMAGE_INSTALL:append', | ||
| 265 | field='image_install', | ||
| 266 | btn_id='change-image_install-icon', | ||
| 267 | input_id='new-image_install', | ||
| 268 | value='bash, apt, busybox', | ||
| 269 | save_btn="apply-change-image_install", | ||
| 270 | ) | ||
| 271 | |||
| 272 | def test_set_package_classes_var(self): | ||
| 273 | """ Test setting PACKAGE_CLASSES variable """ | ||
| 274 | self._change_bbv_value( | ||
| 275 | var_name='PACKAGE_CLASSES', | ||
| 276 | field='package_classes', | ||
| 277 | btn_id='change-package_classes-icon', | ||
| 278 | input_id='package_classes-select', | ||
| 279 | value='package_deb', | ||
| 280 | save_btn="apply-change-package_classes", | ||
| 281 | is_select=True, | ||
| 282 | ) | ||
| 283 | |||
| 284 | def test_create_new_bbv(self): | ||
| 285 | """ Test creating new bitbake variable """ | ||
| 286 | self._change_bbv_value( | ||
| 287 | var_name='New_Custom_Variable', | ||
| 288 | field='configvar-list', | ||
| 289 | btn_id='variable', | ||
| 290 | input_id='value', | ||
| 291 | value='new variable value', | ||
| 292 | save_btn="add-configvar-button", | ||
| 293 | new_variable=True | ||
| 294 | ) | ||
diff --git a/bitbake/lib/toaster/tests/functional/test_project_page.py b/bitbake/lib/toaster/tests/functional/test_project_page.py deleted file mode 100644 index c6dad0eb5d..0000000000 --- a/bitbake/lib/toaster/tests/functional/test_project_page.py +++ /dev/null | |||
| @@ -1,775 +0,0 @@ | |||
| 1 | #! /usr/bin/env python3 # | ||
| 2 | # BitBake Toaster UI tests implementation | ||
| 3 | # | ||
| 4 | # Copyright (C) 2023 Savoir-faire Linux | ||
| 5 | # | ||
| 6 | # SPDX-License-Identifier: GPL-2.0-only | ||
| 7 | # | ||
| 8 | |||
| 9 | import os | ||
| 10 | import string | ||
| 11 | import time | ||
| 12 | from unittest import skip | ||
| 13 | import pytest | ||
| 14 | from django.urls import reverse | ||
| 15 | from django.utils import timezone | ||
| 16 | from selenium.webdriver.common.keys import Keys | ||
| 17 | from selenium.webdriver.support.select import Select | ||
| 18 | from selenium.common.exceptions import TimeoutException | ||
| 19 | from tests.functional.functional_helpers import SeleniumFunctionalTestCase | ||
| 20 | from orm.models import Build, Project, Target | ||
| 21 | from selenium.webdriver.common.by import By | ||
| 22 | |||
| 23 | from .utils import get_projectId_from_url, wait_until_build, wait_until_build_cancelled | ||
| 24 | |||
| 25 | class TestProjectPageBase(SeleniumFunctionalTestCase): | ||
| 26 | project_id = None | ||
| 27 | PROJECT_NAME = 'TestProjectPage' | ||
| 28 | |||
| 29 | def _navigate_to_project_page(self): | ||
| 30 | # Navigate to project page | ||
| 31 | if TestProjectPageBase.project_id is None: | ||
| 32 | TestProjectPageBase.project_id = self.create_new_project(self.PROJECT_NAME, '3', None, True) | ||
| 33 | |||
| 34 | url = reverse('project', args=(TestProjectPageBase.project_id,)) | ||
| 35 | self.get(url) | ||
| 36 | self.wait_until_visible('#config-nav') | ||
| 37 | |||
| 38 | def _get_create_builds(self, **kwargs): | ||
| 39 | """ Create a build and return the build object """ | ||
| 40 | # parameters for builds to associate with the projects | ||
| 41 | now = timezone.now() | ||
| 42 | self.project1_build_success = { | ||
| 43 | 'project': Project.objects.get(id=TestProjectPageBase.project_id), | ||
| 44 | 'started_on': now, | ||
| 45 | 'completed_on': now, | ||
| 46 | 'outcome': Build.SUCCEEDED | ||
| 47 | } | ||
| 48 | |||
| 49 | self.project1_build_failure = { | ||
| 50 | 'project': Project.objects.get(id=TestProjectPageBase.project_id), | ||
| 51 | 'started_on': now, | ||
| 52 | 'completed_on': now, | ||
| 53 | 'outcome': Build.FAILED | ||
| 54 | } | ||
| 55 | build1 = Build.objects.create(**self.project1_build_success) | ||
| 56 | build2 = Build.objects.create(**self.project1_build_failure) | ||
| 57 | |||
| 58 | # add some targets to these builds so they have recipe links | ||
| 59 | # (and so we can find the row in the ToasterTable corresponding to | ||
| 60 | # a particular build) | ||
| 61 | Target.objects.create(build=build1, target='foo') | ||
| 62 | Target.objects.create(build=build2, target='bar') | ||
| 63 | |||
| 64 | if kwargs: | ||
| 65 | # Create kwargs.get('success') builds with success status with target | ||
| 66 | # and kwargs.get('failure') builds with failure status with target | ||
| 67 | for i in range(kwargs.get('success', 0)): | ||
| 68 | now = timezone.now() | ||
| 69 | self.project1_build_success['started_on'] = now | ||
| 70 | self.project1_build_success[ | ||
| 71 | 'completed_on'] = now - timezone.timedelta(days=i) | ||
| 72 | build = Build.objects.create(**self.project1_build_success) | ||
| 73 | Target.objects.create(build=build, | ||
| 74 | target=f'{i}_success_recipe', | ||
| 75 | task=f'{i}_success_task') | ||
| 76 | |||
| 77 | for i in range(kwargs.get('failure', 0)): | ||
| 78 | now = timezone.now() | ||
| 79 | self.project1_build_failure['started_on'] = now | ||
| 80 | self.project1_build_failure[ | ||
| 81 | 'completed_on'] = now - timezone.timedelta(days=i) | ||
| 82 | build = Build.objects.create(**self.project1_build_failure) | ||
| 83 | Target.objects.create(build=build, | ||
| 84 | target=f'{i}_fail_recipe', | ||
| 85 | task=f'{i}_fail_task') | ||
| 86 | return build1, build2 | ||
| 87 | |||
| 88 | def _mixin_test_table_edit_column( | ||
| 89 | self, | ||
| 90 | table_id, | ||
| 91 | edit_btn_id, | ||
| 92 | list_check_box_id: list | ||
| 93 | ): | ||
| 94 | # Check edit column | ||
| 95 | finder = lambda driver: self.find(f'#{edit_btn_id}') | ||
| 96 | edit_column = self.wait_until_element_clickable(finder) | ||
| 97 | self.assertTrue(edit_column.is_displayed()) | ||
| 98 | edit_column.click() | ||
| 99 | # Check dropdown is visible | ||
| 100 | self.wait_until_visible('ul.dropdown-menu.editcol') | ||
| 101 | for check_box_id in list_check_box_id: | ||
| 102 | # Check that we can hide/show table column | ||
| 103 | check_box = self.find(f'#{check_box_id}') | ||
| 104 | th_class = str(check_box_id).replace('checkbox-', '') | ||
| 105 | if check_box.is_selected(): | ||
| 106 | # check if column is visible in table | ||
| 107 | self.assertTrue( | ||
| 108 | self.find( | ||
| 109 | f'#{table_id} thead th.{th_class}' | ||
| 110 | ).is_displayed(), | ||
| 111 | f"The {th_class} column is checked in EditColumn dropdown, but it's not visible in table" | ||
| 112 | ) | ||
| 113 | check_box.click() | ||
| 114 | # check if column is hidden in table | ||
| 115 | self.assertFalse( | ||
| 116 | self.find( | ||
| 117 | f'#{table_id} thead th.{th_class}' | ||
| 118 | ).is_displayed(), | ||
| 119 | f"The {th_class} column is unchecked in EditColumn dropdown, but it's visible in table" | ||
| 120 | ) | ||
| 121 | else: | ||
| 122 | # check if column is hidden in table | ||
| 123 | self.assertFalse( | ||
| 124 | self.find( | ||
| 125 | f'#{table_id} thead th.{th_class}' | ||
| 126 | ).is_displayed(), | ||
| 127 | f"The {th_class} column is unchecked in EditColumn dropdown, but it's visible in table" | ||
| 128 | ) | ||
| 129 | check_box.click() | ||
| 130 | # check if column is visible in table | ||
| 131 | self.assertTrue( | ||
| 132 | self.find( | ||
| 133 | f'#{table_id} thead th.{th_class}' | ||
| 134 | ).is_displayed(), | ||
| 135 | f"The {th_class} column is checked in EditColumn dropdown, but it's not visible in table" | ||
| 136 | ) | ||
| 137 | |||
| 138 | def _get_config_nav_item(self, index): | ||
| 139 | config_nav = self.find('#config-nav') | ||
| 140 | return config_nav.find_elements(By.TAG_NAME, 'li')[index] | ||
| 141 | |||
| 142 | def _navigate_to_config_nav(self, nav_id, nav_index): | ||
| 143 | # navigate to the project page | ||
| 144 | self._navigate_to_project_page() | ||
| 145 | # click on "Software recipe" tab | ||
| 146 | soft_recipe = self._get_config_nav_item(nav_index) | ||
| 147 | soft_recipe.click() | ||
| 148 | self.wait_until_visible(f'#{nav_id}') | ||
| 149 | |||
| 150 | def _mixin_test_table_show_rows(self, table_selector, **kwargs): | ||
| 151 | """ Test the show rows feature in the builds table on the all builds page """ | ||
| 152 | def test_show_rows(row_to_show, show_row_link): | ||
| 153 | # Check that we can show rows == row_to_show | ||
| 154 | show_row_link.select_by_value(str(row_to_show)) | ||
| 155 | self.wait_until_visible(f'#{table_selector} tbody tr') | ||
| 156 | # check at least some rows are visible | ||
| 157 | self.assertTrue( | ||
| 158 | len(self.find_all(f'#{table_selector} tbody tr')) > 0 | ||
| 159 | ) | ||
| 160 | self.wait_until_present(f'#{table_selector} tbody tr') | ||
| 161 | show_rows = self.driver.find_elements( | ||
| 162 | By.XPATH, | ||
| 163 | f'//select[@class="form-control pagesize-{table_selector}"]' | ||
| 164 | ) | ||
| 165 | rows_to_show = [10, 25, 50, 100, 150] | ||
| 166 | to_skip = kwargs.get('to_skip', []) | ||
| 167 | # Check show rows | ||
| 168 | for show_row_link in show_rows: | ||
| 169 | show_row_link = Select(show_row_link) | ||
| 170 | for row_to_show in rows_to_show: | ||
| 171 | if row_to_show not in to_skip: | ||
| 172 | test_show_rows(row_to_show, show_row_link) | ||
| 173 | |||
| 174 | def _mixin_test_table_search_input(self, **kwargs): | ||
| 175 | input_selector, input_text, searchBtn_selector, table_selector, *_ = kwargs.values() | ||
| 176 | # Test search input | ||
| 177 | self.wait_until_visible(f'#{input_selector}') | ||
| 178 | recipe_input = self.find(f'#{input_selector}') | ||
| 179 | recipe_input.send_keys(input_text) | ||
| 180 | self.find(f'#{searchBtn_selector}').click() | ||
| 181 | self.wait_until_visible(f'#{table_selector} tbody tr') | ||
| 182 | rows = self.find_all(f'#{table_selector} tbody tr') | ||
| 183 | self.assertTrue(len(rows) > 0) | ||
| 184 | |||
| 185 | class TestProjectPage(TestProjectPageBase): | ||
| 186 | |||
| 187 | def test_page_header_on_project_page(self): | ||
| 188 | """ Check page header in project page: | ||
| 189 | - AT LEFT -> Logo of Yocto project, displayed, clickable | ||
| 190 | - "Toaster"+" Information icon", displayed, clickable | ||
| 191 | - "Server Icon" + "All builds", displayed, clickable | ||
| 192 | - "Directory Icon" + "All projects", displayed, clickable | ||
| 193 | - "Book Icon" + "Documentation", displayed, clickable | ||
| 194 | - AT RIGHT -> button "New project", displayed, clickable | ||
| 195 | """ | ||
| 196 | # navigate to the project page | ||
| 197 | self._navigate_to_project_page() | ||
| 198 | |||
| 199 | # check page header | ||
| 200 | # AT LEFT -> Logo of Yocto project | ||
| 201 | logo = self.driver.find_element( | ||
| 202 | By.XPATH, | ||
| 203 | "//div[@class='toaster-navbar-brand']", | ||
| 204 | ) | ||
| 205 | logo_img = logo.find_element(By.TAG_NAME, 'img') | ||
| 206 | self.assertTrue(logo_img.is_displayed(), | ||
| 207 | 'Logo of Yocto project not found') | ||
| 208 | self.assertIn( | ||
| 209 | '/static/img/logo.png', str(logo_img.get_attribute('src')), | ||
| 210 | 'Logo of Yocto project not found' | ||
| 211 | ) | ||
| 212 | # "Toaster"+" Information icon", clickable | ||
| 213 | toaster = self.driver.find_element( | ||
| 214 | By.XPATH, | ||
| 215 | "//div[@class='toaster-navbar-brand']//a[@class='brand']", | ||
| 216 | ) | ||
| 217 | self.assertTrue(toaster.is_displayed(), 'Toaster not found') | ||
| 218 | self.assertEqual(toaster.text, 'Toaster') | ||
| 219 | info_sign = self.find('.glyphicon-info-sign') | ||
| 220 | self.assertTrue(info_sign.is_displayed()) | ||
| 221 | |||
| 222 | # "Server Icon" + "All builds" | ||
| 223 | all_builds = self.find('#navbar-all-builds') | ||
| 224 | all_builds_link = all_builds.find_element(By.TAG_NAME, 'a') | ||
| 225 | self.assertIn("All builds", all_builds_link.text) | ||
| 226 | self.assertIn( | ||
| 227 | '/toastergui/builds/', str(all_builds_link.get_attribute('href')) | ||
| 228 | ) | ||
| 229 | server_icon = all_builds.find_element(By.TAG_NAME, 'i') | ||
| 230 | self.assertEqual( | ||
| 231 | server_icon.get_attribute('class'), 'glyphicon glyphicon-tasks' | ||
| 232 | ) | ||
| 233 | self.assertTrue(server_icon.is_displayed()) | ||
| 234 | |||
| 235 | # "Directory Icon" + "All projects" | ||
| 236 | all_projects = self.find('#navbar-all-projects') | ||
| 237 | all_projects_link = all_projects.find_element(By.TAG_NAME, 'a') | ||
| 238 | self.assertIn("All projects", all_projects_link.text) | ||
| 239 | self.assertIn( | ||
| 240 | '/toastergui/projects/', str(all_projects_link.get_attribute( | ||
| 241 | 'href')) | ||
| 242 | ) | ||
| 243 | dir_icon = all_projects.find_element(By.TAG_NAME, 'i') | ||
| 244 | self.assertEqual( | ||
| 245 | dir_icon.get_attribute('class'), 'icon-folder-open' | ||
| 246 | ) | ||
| 247 | self.assertTrue(dir_icon.is_displayed()) | ||
| 248 | |||
| 249 | # "Book Icon" + "Documentation" | ||
| 250 | toaster_docs_link = self.find('#navbar-docs') | ||
| 251 | toaster_docs_link_link = toaster_docs_link.find_element(By.TAG_NAME, | ||
| 252 | 'a') | ||
| 253 | self.assertIn("Documentation", toaster_docs_link_link.text) | ||
| 254 | self.assertEqual( | ||
| 255 | toaster_docs_link_link.get_attribute('href'), 'http://docs.yoctoproject.org/toaster-manual/index.html#toaster-user-manual' | ||
| 256 | ) | ||
| 257 | book_icon = toaster_docs_link.find_element(By.TAG_NAME, 'i') | ||
| 258 | self.assertEqual( | ||
| 259 | book_icon.get_attribute('class'), 'glyphicon glyphicon-book' | ||
| 260 | ) | ||
| 261 | self.assertTrue(book_icon.is_displayed()) | ||
| 262 | |||
| 263 | # AT RIGHT -> button "New project" | ||
| 264 | new_project_button = self.find('#new-project-button') | ||
| 265 | self.assertTrue(new_project_button.is_displayed()) | ||
| 266 | self.assertEqual(new_project_button.text, 'New project') | ||
| 267 | new_project_button.click() | ||
| 268 | self.assertIn( | ||
| 269 | '/toastergui/newproject/', str(self.driver.current_url) | ||
| 270 | ) | ||
| 271 | |||
| 272 | def test_edit_project_name(self): | ||
| 273 | """ Test edit project name: | ||
| 274 | - Click on "Edit" icon button | ||
| 275 | - Change project name | ||
| 276 | - Click on "Save" button | ||
| 277 | - Check project name is changed | ||
| 278 | """ | ||
| 279 | # navigate to the project page | ||
| 280 | self._navigate_to_project_page() | ||
| 281 | |||
| 282 | # click on "Edit" icon button | ||
| 283 | self.wait_until_visible('#project-name-container') | ||
| 284 | finder = lambda driver: self.find('#project-change-form-toggle') | ||
| 285 | edit_button = self.wait_until_element_clickable(finder) | ||
| 286 | edit_button.click() | ||
| 287 | project_name_input = self.find('#project-name-change-input') | ||
| 288 | self.assertTrue(project_name_input.is_displayed()) | ||
| 289 | project_name_input.clear() | ||
| 290 | project_name_input.send_keys('New Name') | ||
| 291 | self.find('#project-name-change-btn').click() | ||
| 292 | |||
| 293 | # check project name is changed | ||
| 294 | self.wait_until_visible('#project-name-container') | ||
| 295 | self.assertIn( | ||
| 296 | 'New Name', str(self.find('#project-name-container').text) | ||
| 297 | ) | ||
| 298 | |||
| 299 | def test_project_page_tabs(self): | ||
| 300 | """ Test project tabs: | ||
| 301 | - "configuration" tab | ||
| 302 | - "Builds" tab | ||
| 303 | - "Import layers" tab | ||
| 304 | - "New custom image" tab | ||
| 305 | Check search box used to build recipes | ||
| 306 | """ | ||
| 307 | # navigate to the project page | ||
| 308 | self._navigate_to_project_page() | ||
| 309 | |||
| 310 | # check "configuration" tab | ||
| 311 | self.wait_until_visible('#topbar-configuration-tab') | ||
| 312 | config_tab = self.find('#topbar-configuration-tab') | ||
| 313 | self.assertEqual(config_tab.get_attribute('class'), 'active') | ||
| 314 | self.assertIn('Configuration', str(config_tab.text)) | ||
| 315 | self.assertIn( | ||
| 316 | f"/toastergui/project/{TestProjectPageBase.project_id}", str(self.driver.current_url) | ||
| 317 | ) | ||
| 318 | |||
| 319 | def get_tabs(): | ||
| 320 | # tabs links list | ||
| 321 | return self.driver.find_elements( | ||
| 322 | By.XPATH, | ||
| 323 | '//div[@id="project-topbar"]//li' | ||
| 324 | ) | ||
| 325 | |||
| 326 | def check_tab_link(tab_index, tab_name, url): | ||
| 327 | tab = get_tabs()[tab_index] | ||
| 328 | tab_link = tab.find_element(By.TAG_NAME, 'a') | ||
| 329 | self.assertIn(url, tab_link.get_attribute('href')) | ||
| 330 | self.assertIn(tab_name, tab_link.text) | ||
| 331 | self.assertEqual(tab.get_attribute('class'), 'active') | ||
| 332 | |||
| 333 | # check "Builds" tab | ||
| 334 | builds_tab = get_tabs()[1] | ||
| 335 | builds_tab.find_element(By.TAG_NAME, 'a').click() | ||
| 336 | check_tab_link( | ||
| 337 | 1, | ||
| 338 | 'Builds', | ||
| 339 | f"/toastergui/project/{TestProjectPageBase.project_id}/builds" | ||
| 340 | ) | ||
| 341 | |||
| 342 | # check "Import layers" tab | ||
| 343 | import_layers_tab = get_tabs()[2] | ||
| 344 | import_layers_tab.find_element(By.TAG_NAME, 'a').click() | ||
| 345 | check_tab_link( | ||
| 346 | 2, | ||
| 347 | 'Import layer', | ||
| 348 | f"/toastergui/project/{TestProjectPageBase.project_id}/importlayer" | ||
| 349 | ) | ||
| 350 | |||
| 351 | # check "New custom image" tab | ||
| 352 | new_custom_image_tab = get_tabs()[3] | ||
| 353 | new_custom_image_tab.find_element(By.TAG_NAME, 'a').click() | ||
| 354 | check_tab_link( | ||
| 355 | 3, | ||
| 356 | 'New custom image', | ||
| 357 | f"/toastergui/project/{TestProjectPageBase.project_id}/newcustomimage" | ||
| 358 | ) | ||
| 359 | |||
| 360 | # check search box can be use to build recipes | ||
| 361 | search_box = self.find('#build-input') | ||
| 362 | search_box.send_keys('core-image-minimal') | ||
| 363 | self.find('#build-button').click() | ||
| 364 | self.wait_until_visible('#latest-builds') | ||
| 365 | buildtext = "Loading" | ||
| 366 | while "Loading" in buildtext: | ||
| 367 | time.sleep(1) | ||
| 368 | lastest_builds = self.driver.find_elements( | ||
| 369 | By.XPATH, | ||
| 370 | '//div[@id="latest-builds"]', | ||
| 371 | ) | ||
| 372 | last_build = lastest_builds[0] | ||
| 373 | buildtext = last_build.text | ||
| 374 | self.assertIn( | ||
| 375 | 'core-image-minimal', str(last_build.text) | ||
| 376 | ) | ||
| 377 | |||
| 378 | def test_softwareRecipe_page(self): | ||
| 379 | """ Test software recipe page | ||
| 380 | - Check title "Compatible software recipes" is displayed | ||
| 381 | - Check search input | ||
| 382 | - Check "build recipe" button works | ||
| 383 | - Check software recipe table feature(show/hide column, pagination) | ||
| 384 | """ | ||
| 385 | self._navigate_to_config_nav('softwarerecipestable', 4) | ||
| 386 | # check title "Compatible software recipes" is displayed | ||
| 387 | self.assertIn("Compatible software recipes", self.get_page_source()) | ||
| 388 | # Test search input | ||
| 389 | self._mixin_test_table_search_input( | ||
| 390 | input_selector='search-input-softwarerecipestable', | ||
| 391 | input_text='busybox', | ||
| 392 | searchBtn_selector='search-submit-softwarerecipestable', | ||
| 393 | table_selector='softwarerecipestable' | ||
| 394 | ) | ||
| 395 | # check "build recipe" button works | ||
| 396 | finder = lambda driver: self.find_all('#softwarerecipestable tbody tr')[0].find_element(By.XPATH, '//td[@class="add-del-layers"]/a') | ||
| 397 | build_btn = self.wait_until_element_clickable(finder) | ||
| 398 | build_btn.click() | ||
| 399 | build_state = wait_until_build(self, 'queued cloning starting parsing failed') | ||
| 400 | lastest_builds = self.driver.find_elements( | ||
| 401 | By.XPATH, | ||
| 402 | '//div[@id="latest-builds"]/div' | ||
| 403 | ) | ||
| 404 | self.assertTrue(len(lastest_builds) > 0) | ||
| 405 | # Find the latest builds, the last build and then the cancel button | ||
| 406 | |||
| 407 | finder = lambda driver: driver.find_elements(By.XPATH, '//div[@id="latest-builds"]/div')[0].find_element(By.XPATH, '//span[@class="cancel-build-btn pull-right alert-link"]') | ||
| 408 | cancel_button = self.wait_until_element_clickable(finder) | ||
| 409 | cancel_button.click() | ||
| 410 | if 'starting' not in build_state: # change build state when cancelled in starting state | ||
| 411 | wait_until_build_cancelled(self) | ||
| 412 | |||
| 413 | # check software recipe table feature(show/hide column, pagination) | ||
| 414 | self._navigate_to_config_nav('softwarerecipestable', 4) | ||
| 415 | column_list = [ | ||
| 416 | 'get_description_or_summary', | ||
| 417 | 'layer_version__get_vcs_reference', | ||
| 418 | 'layer_version__layer__name', | ||
| 419 | 'license', | ||
| 420 | 'recipe-file', | ||
| 421 | 'section', | ||
| 422 | 'version', | ||
| 423 | ] | ||
| 424 | self._mixin_test_table_edit_column( | ||
| 425 | 'softwarerecipestable', | ||
| 426 | 'edit-columns-button', | ||
| 427 | [f'checkbox-{column}' for column in column_list] | ||
| 428 | ) | ||
| 429 | self._navigate_to_config_nav('softwarerecipestable', 4) | ||
| 430 | # check show rows(pagination) | ||
| 431 | self._mixin_test_table_show_rows( | ||
| 432 | table_selector='softwarerecipestable', | ||
| 433 | to_skip=[150], | ||
| 434 | ) | ||
| 435 | |||
| 436 | def test_machines_page(self): | ||
| 437 | """ Test Machine page | ||
| 438 | - Check if title "Compatible machines" is displayed | ||
| 439 | - Check search input | ||
| 440 | - Check "Select machine" button works | ||
| 441 | - Check "Add layer" button works | ||
| 442 | - Check Machine table feature(show/hide column, pagination) | ||
| 443 | """ | ||
| 444 | self._navigate_to_config_nav('machinestable', 5) | ||
| 445 | # check title "Compatible software recipes" is displayed | ||
| 446 | self.assertIn("Compatible machines", self.get_page_source()) | ||
| 447 | # Test search input | ||
| 448 | self._mixin_test_table_search_input( | ||
| 449 | input_selector='search-input-machinestable', | ||
| 450 | input_text='qemux86-64', | ||
| 451 | searchBtn_selector='search-submit-machinestable', | ||
| 452 | table_selector='machinestable' | ||
| 453 | ) | ||
| 454 | # check "Select machine" button works | ||
| 455 | finder = lambda driver: self.find_all('#machinestable tbody tr')[0].find_element(By.XPATH, '//td[@class="add-del-layers"]') | ||
| 456 | select_btn = self.wait_until_element_clickable(finder) | ||
| 457 | select_btn.click() | ||
| 458 | self.wait_until_visible('#project-machine-name') | ||
| 459 | project_machine_name = self.find('#project-machine-name') | ||
| 460 | self.assertIn( | ||
| 461 | 'qemux86-64', project_machine_name.text | ||
| 462 | ) | ||
| 463 | # check "Add layer" button works | ||
| 464 | self._navigate_to_config_nav('machinestable', 5) | ||
| 465 | # Search for a machine whit layer not in project | ||
| 466 | self._mixin_test_table_search_input( | ||
| 467 | input_selector='search-input-machinestable', | ||
| 468 | input_text='qemux86-64-tpm2', | ||
| 469 | searchBtn_selector='search-submit-machinestable', | ||
| 470 | table_selector='machinestable' | ||
| 471 | ) | ||
| 472 | |||
| 473 | self.wait_until_visible('#machinestable tbody tr') | ||
| 474 | # Locate a machine to add button | ||
| 475 | finder = lambda driver: self.find_all('#machinestable tbody tr')[0].find_element(By.XPATH, '//td[@class="add-del-layers"]') | ||
| 476 | add_btn = self.wait_until_element_clickable(finder) | ||
| 477 | add_btn.click() | ||
| 478 | self.wait_until_visible('#change-notification') | ||
| 479 | change_notification = self.find('#change-notification') | ||
| 480 | self.assertIn( | ||
| 481 | f'You have added 1 layer to your project', str(change_notification.text) | ||
| 482 | ) | ||
| 483 | |||
| 484 | finder = lambda driver: self.find('#hide-alert') | ||
| 485 | hide_button = self.wait_until_element_clickable(finder) | ||
| 486 | hide_button.click() | ||
| 487 | self.wait_until_not_visible('#change-notification') | ||
| 488 | |||
| 489 | # check Machine table feature(show/hide column, pagination) | ||
| 490 | self._navigate_to_config_nav('machinestable', 5) | ||
| 491 | column_list = [ | ||
| 492 | 'description', | ||
| 493 | 'layer_version__get_vcs_reference', | ||
| 494 | 'layer_version__layer__name', | ||
| 495 | 'machinefile', | ||
| 496 | ] | ||
| 497 | self._mixin_test_table_edit_column( | ||
| 498 | 'machinestable', | ||
| 499 | 'edit-columns-button', | ||
| 500 | [f'checkbox-{column}' for column in column_list] | ||
| 501 | ) | ||
| 502 | self._navigate_to_config_nav('machinestable', 5) | ||
| 503 | # check show rows(pagination) | ||
| 504 | self._mixin_test_table_show_rows( | ||
| 505 | table_selector='machinestable', | ||
| 506 | to_skip=[150], | ||
| 507 | ) | ||
| 508 | |||
| 509 | def test_layers_page(self): | ||
| 510 | """ Test layers page | ||
| 511 | - Check if title "Compatible layerss" is displayed | ||
| 512 | - Check search input | ||
| 513 | - Check "Add layer" button works | ||
| 514 | - Check "Remove layer" button works | ||
| 515 | - Check layers table feature(show/hide column, pagination) | ||
| 516 | """ | ||
| 517 | self._navigate_to_config_nav('layerstable', 6) | ||
| 518 | # check title "Compatible layers" is displayed | ||
| 519 | self.assertIn("Compatible layers", self.get_page_source()) | ||
| 520 | # Test search input | ||
| 521 | input_text='meta-tanowrt' | ||
| 522 | self._mixin_test_table_search_input( | ||
| 523 | input_selector='search-input-layerstable', | ||
| 524 | input_text=input_text, | ||
| 525 | searchBtn_selector='search-submit-layerstable', | ||
| 526 | table_selector='layerstable' | ||
| 527 | ) | ||
| 528 | # check "Add layer" button works | ||
| 529 | self.wait_until_visible('#layerstable tbody tr') | ||
| 530 | finder = lambda driver: self.find_all('#layerstable tbody tr')[0].find_element(By.XPATH, '//td[@class="add-del-layers"]/a[@data-directive="add"]') | ||
| 531 | add_btn = self.wait_until_element_clickable(finder) | ||
| 532 | add_btn.click() | ||
| 533 | # check modal is displayed | ||
| 534 | self.wait_until_visible('#dependencies-modal') | ||
| 535 | list_dependencies = self.find_all('#dependencies-list li') | ||
| 536 | # click on add-layers button | ||
| 537 | finder = lambda driver: self.driver.find_element(By.XPATH, '//form[@id="dependencies-modal-form"]//button[@class="btn btn-primary"]') | ||
| 538 | add_layers_btn = self.wait_until_element_clickable(finder) | ||
| 539 | add_layers_btn.click() | ||
| 540 | self.wait_until_visible('#change-notification') | ||
| 541 | change_notification = self.find('#change-notification') | ||
| 542 | self.assertIn( | ||
| 543 | f'You have added {len(list_dependencies)+1} layers to your project: {input_text} and its dependencies', str(change_notification.text) | ||
| 544 | ) | ||
| 545 | |||
| 546 | finder = lambda driver: self.find('#hide-alert') | ||
| 547 | hide_button = self.wait_until_element_clickable(finder) | ||
| 548 | hide_button.click() | ||
| 549 | self.wait_until_not_visible('#change-notification') | ||
| 550 | |||
| 551 | # check "Remove layer" button works | ||
| 552 | self.wait_until_visible('#layerstable tbody tr') | ||
| 553 | finder = lambda driver: self.find_all('#layerstable tbody tr')[0].find_element(By.XPATH, '//td[@class="add-del-layers"]/a[@data-directive="remove"]') | ||
| 554 | remove_btn = self.wait_until_element_clickable(finder) | ||
| 555 | remove_btn.click() | ||
| 556 | self.wait_until_visible('#change-notification') | ||
| 557 | change_notification = self.find('#change-notification') | ||
| 558 | self.assertIn( | ||
| 559 | f'You have removed 1 layer from your project: {input_text}', str(change_notification.text) | ||
| 560 | ) | ||
| 561 | |||
| 562 | finder = lambda driver: self.find('#hide-alert') | ||
| 563 | hide_button = self.wait_until_element_clickable(finder) | ||
| 564 | hide_button.click() | ||
| 565 | self.wait_until_not_visible('#change-notification') | ||
| 566 | |||
| 567 | # check layers table feature(show/hide column, pagination) | ||
| 568 | self._navigate_to_config_nav('layerstable', 6) | ||
| 569 | column_list = [ | ||
| 570 | 'dependencies', | ||
| 571 | 'revision', | ||
| 572 | 'layer__vcs_url', | ||
| 573 | 'git_subdir', | ||
| 574 | 'layer__summary', | ||
| 575 | ] | ||
| 576 | self._mixin_test_table_edit_column( | ||
| 577 | 'layerstable', | ||
| 578 | 'edit-columns-button', | ||
| 579 | [f'checkbox-{column}' for column in column_list] | ||
| 580 | ) | ||
| 581 | self._navigate_to_config_nav('layerstable', 6) | ||
| 582 | # check show rows(pagination) | ||
| 583 | self._mixin_test_table_show_rows( | ||
| 584 | table_selector='layerstable', | ||
| 585 | to_skip=[150], | ||
| 586 | ) | ||
| 587 | |||
| 588 | def test_distro_page(self): | ||
| 589 | """ Test distros page | ||
| 590 | - Check if title "Compatible distros" is displayed | ||
| 591 | - Check search input | ||
| 592 | - Check "Add layer" button works | ||
| 593 | - Check distro table feature(show/hide column, pagination) | ||
| 594 | """ | ||
| 595 | self._navigate_to_config_nav('distrostable', 7) | ||
| 596 | # check title "Compatible distros" is displayed | ||
| 597 | self.assertIn("Compatible Distros", self.get_page_source()) | ||
| 598 | # Test search input | ||
| 599 | input_text='poky-altcfg' | ||
| 600 | self._mixin_test_table_search_input( | ||
| 601 | input_selector='search-input-distrostable', | ||
| 602 | input_text=input_text, | ||
| 603 | searchBtn_selector='search-submit-distrostable', | ||
| 604 | table_selector='distrostable' | ||
| 605 | ) | ||
| 606 | # check "Add distro" button works | ||
| 607 | self.wait_until_visible(".add-del-layers") | ||
| 608 | finder = lambda driver: self.find_all('#distrostable tbody tr')[0].find_element(By.XPATH, '//td[@class="add-del-layers"]') | ||
| 609 | add_btn = self.wait_until_element_clickable(finder) | ||
| 610 | add_btn.click() | ||
| 611 | self.wait_until_visible('#change-notification') | ||
| 612 | change_notification = self.find('#change-notification') | ||
| 613 | self.assertIn( | ||
| 614 | f'You have changed the distro to: {input_text}', str(change_notification.text) | ||
| 615 | ) | ||
| 616 | # check distro table feature(show/hide column, pagination) | ||
| 617 | self._navigate_to_config_nav('distrostable', 7) | ||
| 618 | column_list = [ | ||
| 619 | 'description', | ||
| 620 | 'templatefile', | ||
| 621 | 'layer_version__get_vcs_reference', | ||
| 622 | 'layer_version__layer__name', | ||
| 623 | ] | ||
| 624 | self._mixin_test_table_edit_column( | ||
| 625 | 'distrostable', | ||
| 626 | 'edit-columns-button', | ||
| 627 | [f'checkbox-{column}' for column in column_list] | ||
| 628 | ) | ||
| 629 | self._navigate_to_config_nav('distrostable', 7) | ||
| 630 | # check show rows(pagination) | ||
| 631 | self._mixin_test_table_show_rows( | ||
| 632 | table_selector='distrostable', | ||
| 633 | to_skip=[150], | ||
| 634 | ) | ||
| 635 | |||
| 636 | def test_single_layer_page(self): | ||
| 637 | """ Test layer details page using meta-poky as an example (assumes is added to start with) | ||
| 638 | - Check if title is displayed | ||
| 639 | - Check add/remove layer button works | ||
| 640 | - Check tabs(layers, recipes, machines) are displayed | ||
| 641 | - Check left section is displayed | ||
| 642 | - Check layer name | ||
| 643 | - Check layer summary | ||
| 644 | - Check layer description | ||
| 645 | """ | ||
| 646 | self._navigate_to_config_nav('layerstable', 6) | ||
| 647 | layer_link = self.driver.find_element(By.XPATH, '//tr/td[@class="layer__name"]/a[contains(text(),"meta-poky")]') | ||
| 648 | layer_link.click() | ||
| 649 | self.wait_until_visible('.page-header') | ||
| 650 | # check title is displayed | ||
| 651 | self.assertTrue(self.find('.page-header h1').is_displayed()) | ||
| 652 | |||
| 653 | # check remove layer button works | ||
| 654 | finder = lambda driver: self.find('#add-remove-layer-btn') | ||
| 655 | remove_layer_btn = self.wait_until_element_clickable(finder) | ||
| 656 | remove_layer_btn.click() | ||
| 657 | self.wait_until_visible('#change-notification') | ||
| 658 | change_notification = self.find('#change-notification') | ||
| 659 | self.assertIn( | ||
| 660 | f'You have removed 1 layer from your project', str(change_notification.text) | ||
| 661 | ) | ||
| 662 | finder = lambda driver: self.find('#hide-alert') | ||
| 663 | hide_button = self.wait_until_element_clickable(finder) | ||
| 664 | hide_button.click() | ||
| 665 | # check add layer button works | ||
| 666 | self.wait_until_not_visible('#change-notification') | ||
| 667 | finder = lambda driver: self.find('#add-remove-layer-btn') | ||
| 668 | add_layer_btn = self.wait_until_element_clickable(finder) | ||
| 669 | add_layer_btn.click() | ||
| 670 | self.wait_until_visible('#change-notification') | ||
| 671 | change_notification = self.find('#change-notification') | ||
| 672 | self.assertIn( | ||
| 673 | f'You have added 1 layer to your project', str(change_notification.text) | ||
| 674 | ) | ||
| 675 | finder = lambda driver: self.find('#hide-alert') | ||
| 676 | hide_button = self.wait_until_element_clickable(finder) | ||
| 677 | hide_button.click() | ||
| 678 | self.wait_until_not_visible('#change-notification') | ||
| 679 | # check tabs(layers, recipes, machines) are displayed | ||
| 680 | tabs = self.find_all('.nav-tabs li') | ||
| 681 | self.assertEqual(len(tabs), 3) | ||
| 682 | # Check first tab | ||
| 683 | tabs[0].click() | ||
| 684 | self.assertIn( | ||
| 685 | 'active', str(self.find('#information').get_attribute('class')) | ||
| 686 | ) | ||
| 687 | # Check second tab (recipes) | ||
| 688 | # Ensure page is scrolled to the top | ||
| 689 | self.driver.find_element(By.XPATH, '//body').send_keys(Keys.CONTROL + Keys.HOME) | ||
| 690 | self.wait_until_visible('.nav-tabs') | ||
| 691 | tabs[1].click() | ||
| 692 | self.assertIn( | ||
| 693 | 'active', str(self.find('#recipes').get_attribute('class')) | ||
| 694 | ) | ||
| 695 | # Check third tab (machines) | ||
| 696 | # Ensure page is scrolled to the top | ||
| 697 | self.driver.find_element(By.XPATH, '//body').send_keys(Keys.CONTROL + Keys.HOME) | ||
| 698 | self.wait_until_visible('.nav-tabs') | ||
| 699 | tabs[2].click() | ||
| 700 | self.assertIn( | ||
| 701 | 'active', str(self.find('#machines').get_attribute('class')) | ||
| 702 | ) | ||
| 703 | # Check left section is displayed | ||
| 704 | section = self.find('.well') | ||
| 705 | # Check layer name | ||
| 706 | self.assertTrue( | ||
| 707 | section.find_element(By.XPATH, '//h2[1]').is_displayed() | ||
| 708 | ) | ||
| 709 | # Check layer summary | ||
| 710 | self.assertIn("Summary", section.text) | ||
| 711 | # Check layer description | ||
| 712 | self.assertIn("Description", section.text) | ||
| 713 | |||
| 714 | @pytest.mark.django_db | ||
| 715 | @pytest.mark.order("last") | ||
| 716 | class TestProjectPageRecipes(TestProjectPageBase): | ||
| 717 | |||
| 718 | def test_single_recipe_page(self): | ||
| 719 | """ Test recipe page | ||
| 720 | - Check if title is displayed | ||
| 721 | - Check add recipe layer displayed | ||
| 722 | - Check left section is displayed | ||
| 723 | - Check recipe: name, summary, description, Version, Section, | ||
| 724 | License, Approx. packages included, Approx. size, Recipe file | ||
| 725 | """ | ||
| 726 | # Use a recipe which is likely to exist in the layer index but not enabled | ||
| 727 | # in poky out the box - xen-image-minimal from meta-virtualization | ||
| 728 | self._navigate_to_project_page() | ||
| 729 | prj = Project.objects.get(pk=TestProjectPageBase.project_id) | ||
| 730 | recipe_id = prj.get_all_compatible_recipes().get(name="xen-image-minimal").pk | ||
| 731 | url = reverse("recipedetails", args=(TestProjectPageBase.project_id, recipe_id)) | ||
| 732 | self.get(url) | ||
| 733 | self.wait_until_visible('.page-header') | ||
| 734 | # check title is displayed | ||
| 735 | self.assertTrue(self.find('.page-header h1').is_displayed()) | ||
| 736 | # check add recipe layer displayed | ||
| 737 | add_recipe_layer_btn = self.find('#add-layer-btn') | ||
| 738 | self.assertTrue(add_recipe_layer_btn.is_displayed()) | ||
| 739 | # check left section is displayed | ||
| 740 | section = self.find('.well') | ||
| 741 | # Check recipe name | ||
| 742 | self.assertTrue( | ||
| 743 | section.find_element(By.XPATH, '//h2[1]').is_displayed() | ||
| 744 | ) | ||
| 745 | # Check recipe sections details info are displayed | ||
| 746 | self.assertIn("Summary", section.text) | ||
| 747 | self.assertIn("Description", section.text) | ||
| 748 | self.assertIn("Version", section.text) | ||
| 749 | self.assertIn("Section", section.text) | ||
| 750 | self.assertIn("License", section.text) | ||
| 751 | self.assertIn("Approx. packages included", section.text) | ||
| 752 | self.assertIn("Approx. package size", section.text) | ||
| 753 | self.assertIn("Recipe file", section.text) | ||
| 754 | |||
| 755 | def test_image_recipe_editColumn(self): | ||
| 756 | """ Test the edit column feature in image recipe table on project page """ | ||
| 757 | self._get_create_builds(success=10, failure=10) | ||
| 758 | |||
| 759 | url = reverse('projectimagerecipes', args=(TestProjectPageBase.project_id,)) | ||
| 760 | self.get(url) | ||
| 761 | self.wait_until_present('#imagerecipestable tbody tr') | ||
| 762 | |||
| 763 | column_list = [ | ||
| 764 | 'get_description_or_summary', 'layer_version__get_vcs_reference', | ||
| 765 | 'layer_version__layer__name', 'license', 'recipe-file', 'section', | ||
| 766 | 'version' | ||
| 767 | ] | ||
| 768 | |||
| 769 | # Check that we can hide the edit column | ||
| 770 | self._mixin_test_table_edit_column( | ||
| 771 | 'imagerecipestable', | ||
| 772 | 'edit-columns-button', | ||
| 773 | [f'checkbox-{column}' for column in column_list] | ||
| 774 | ) | ||
| 775 | |||
diff --git a/bitbake/lib/toaster/tests/functional/test_project_page_tab_config.py b/bitbake/lib/toaster/tests/functional/test_project_page_tab_config.py deleted file mode 100644 index 80c53e1544..0000000000 --- a/bitbake/lib/toaster/tests/functional/test_project_page_tab_config.py +++ /dev/null | |||
| @@ -1,507 +0,0 @@ | |||
| 1 | #! /usr/bin/env python3 # | ||
| 2 | # BitBake Toaster UI tests implementation | ||
| 3 | # | ||
| 4 | # Copyright (C) 2023 Savoir-faire Linux | ||
| 5 | # | ||
| 6 | # SPDX-License-Identifier: GPL-2.0-only | ||
| 7 | # | ||
| 8 | |||
| 9 | import string | ||
| 10 | import time | ||
| 11 | import pytest | ||
| 12 | from django.urls import reverse | ||
| 13 | from selenium.webdriver import Keys | ||
| 14 | from selenium.webdriver.support.select import Select | ||
| 15 | from selenium.common.exceptions import ElementClickInterceptedException, NoSuchElementException, TimeoutException | ||
| 16 | from tests.functional.functional_helpers import SeleniumFunctionalTestCase | ||
| 17 | from selenium.webdriver.common.by import By | ||
| 18 | |||
| 19 | from .utils import get_projectId_from_url, wait_until_build, wait_until_build_cancelled | ||
| 20 | |||
| 21 | class TestProjectConfigTabBase(SeleniumFunctionalTestCase): | ||
| 22 | PROJECT_NAME = 'TestProjectConfigTab' | ||
| 23 | project_id = None | ||
| 24 | |||
| 25 | def _navigate_to_project_page(self): | ||
| 26 | # Navigate to project page | ||
| 27 | if TestProjectConfigTabBase.project_id is None: | ||
| 28 | TestProjectConfigTabBase.project_id = self.create_new_project(self.PROJECT_NAME, '3', None, True) | ||
| 29 | url = reverse('project', args=(TestProjectConfigTabBase.project_id,)) | ||
| 30 | self.get(url) | ||
| 31 | self.wait_until_visible('#config-nav') | ||
| 32 | |||
| 33 | def _create_builds(self): | ||
| 34 | # check search box can be use to build recipes | ||
| 35 | search_box = self.find('#build-input') | ||
| 36 | search_box.send_keys('foo') | ||
| 37 | self.find('#build-button').click() | ||
| 38 | self.wait_until_present('#latest-builds') | ||
| 39 | # loop until reach the parsing state | ||
| 40 | wait_until_build(self, 'queued cloning starting parsing failed') | ||
| 41 | lastest_builds = self.driver.find_elements( | ||
| 42 | By.XPATH, | ||
| 43 | '//div[@id="latest-builds"]/div', | ||
| 44 | ) | ||
| 45 | last_build = lastest_builds[0] | ||
| 46 | self.assertIn( | ||
| 47 | 'foo', str(last_build.text) | ||
| 48 | ) | ||
| 49 | last_build = lastest_builds[0] | ||
| 50 | try: | ||
| 51 | cancel_button = last_build.find_element( | ||
| 52 | By.XPATH, | ||
| 53 | '//span[@class="cancel-build-btn pull-right alert-link"]', | ||
| 54 | ) | ||
| 55 | cancel_button.click() | ||
| 56 | except NoSuchElementException: | ||
| 57 | # Skip if the build is already cancelled | ||
| 58 | pass | ||
| 59 | wait_until_build_cancelled(self) | ||
| 60 | |||
| 61 | def _get_tabs(self): | ||
| 62 | # tabs links list | ||
| 63 | return self.driver.find_elements( | ||
| 64 | By.XPATH, | ||
| 65 | '//div[@id="project-topbar"]//li' | ||
| 66 | ) | ||
| 67 | |||
| 68 | def _get_config_nav_item(self, index): | ||
| 69 | config_nav = self.find('#config-nav') | ||
| 70 | return config_nav.find_elements(By.TAG_NAME, 'li')[index] | ||
| 71 | |||
| 72 | class TestProjectConfigTab(TestProjectConfigTabBase): | ||
| 73 | |||
| 74 | def test_project_config_nav(self): | ||
| 75 | """ Test project config tab navigation: | ||
| 76 | - Check if the menu is displayed and contains the right elements: | ||
| 77 | - Configuration | ||
| 78 | - COMPATIBLE METADATA | ||
| 79 | - Custom images | ||
| 80 | - Image recipes | ||
| 81 | - Software recipes | ||
| 82 | - Machines | ||
| 83 | - Layers | ||
| 84 | - Distro | ||
| 85 | - EXTRA CONFIGURATION | ||
| 86 | - Bitbake variables | ||
| 87 | - Actions | ||
| 88 | - Delete project | ||
| 89 | """ | ||
| 90 | self._navigate_to_project_page() | ||
| 91 | |||
| 92 | def _get_config_nav_item(index): | ||
| 93 | config_nav = self.find('#config-nav') | ||
| 94 | return config_nav.find_elements(By.TAG_NAME, 'li')[index] | ||
| 95 | |||
| 96 | def check_config_nav_item(index, item_name, url): | ||
| 97 | item = _get_config_nav_item(index) | ||
| 98 | self.assertIn(item_name, item.text) | ||
| 99 | self.assertEqual(item.get_attribute('class'), 'active') | ||
| 100 | self.assertIn(url, self.driver.current_url) | ||
| 101 | |||
| 102 | # check if the menu contains the right elements | ||
| 103 | # COMPATIBLE METADATA | ||
| 104 | compatible_metadata = _get_config_nav_item(1) | ||
| 105 | self.assertIn( | ||
| 106 | "compatible metadata", compatible_metadata.text.lower() | ||
| 107 | ) | ||
| 108 | # EXTRA CONFIGURATION | ||
| 109 | extra_configuration = _get_config_nav_item(8) | ||
| 110 | self.assertIn( | ||
| 111 | "extra configuration", extra_configuration.text.lower() | ||
| 112 | ) | ||
| 113 | # Actions | ||
| 114 | actions = _get_config_nav_item(10) | ||
| 115 | self.assertIn("actions", str(actions.text).lower()) | ||
| 116 | |||
| 117 | conf_nav_list = [ | ||
| 118 | # config | ||
| 119 | [0, 'Configuration', | ||
| 120 | f"/toastergui/project/{TestProjectConfigTabBase.project_id}"], | ||
| 121 | # custom images | ||
| 122 | [2, 'Custom images', | ||
| 123 | f"/toastergui/project/{TestProjectConfigTabBase.project_id}/customimages"], | ||
| 124 | # image recipes | ||
| 125 | [3, 'Image recipes', | ||
| 126 | f"/toastergui/project/{TestProjectConfigTabBase.project_id}/images"], | ||
| 127 | # software recipes | ||
| 128 | [4, 'Software recipes', | ||
| 129 | f"/toastergui/project/{TestProjectConfigTabBase.project_id}/softwarerecipes"], | ||
| 130 | # machines | ||
| 131 | [5, 'Machines', | ||
| 132 | f"/toastergui/project/{TestProjectConfigTabBase.project_id}/machines"], | ||
| 133 | # layers | ||
| 134 | [6, 'Layers', | ||
| 135 | f"/toastergui/project/{TestProjectConfigTabBase.project_id}/layers"], | ||
| 136 | # distro | ||
| 137 | [7, 'Distros', | ||
| 138 | f"/toastergui/project/{TestProjectConfigTabBase.project_id}/distros"], | ||
| 139 | # [9, 'BitBake variables', f"/toastergui/project/{TestProjectConfigTabBase.project_id}/configuration"], # bitbake variables | ||
| 140 | ] | ||
| 141 | for index, item_name, url in conf_nav_list: | ||
| 142 | item = _get_config_nav_item(index) | ||
| 143 | if item.get_attribute('class') != 'active': | ||
| 144 | item.click() | ||
| 145 | check_config_nav_item(index, item_name, url) | ||
| 146 | |||
| 147 | def test_image_recipe_editColumn(self): | ||
| 148 | """ Test the edit column feature in image recipe table on project page """ | ||
| 149 | def test_edit_column(check_box_id): | ||
| 150 | # Check that we can hide/show table column | ||
| 151 | check_box = self.find(f'#{check_box_id}') | ||
| 152 | th_class = str(check_box_id).replace('checkbox-', '') | ||
| 153 | if check_box.is_selected(): | ||
| 154 | # check if column is visible in table | ||
| 155 | self.assertTrue( | ||
| 156 | self.find( | ||
| 157 | f'#imagerecipestable thead th.{th_class}' | ||
| 158 | ).is_displayed(), | ||
| 159 | f"The {th_class} column is checked in EditColumn dropdown, but it's not visible in table" | ||
| 160 | ) | ||
| 161 | check_box.click() | ||
| 162 | # check if column is hidden in table | ||
| 163 | self.assertFalse( | ||
| 164 | self.find( | ||
| 165 | f'#imagerecipestable thead th.{th_class}' | ||
| 166 | ).is_displayed(), | ||
| 167 | f"The {th_class} column is unchecked in EditColumn dropdown, but it's visible in table" | ||
| 168 | ) | ||
| 169 | else: | ||
| 170 | # check if column is hidden in table | ||
| 171 | self.assertFalse( | ||
| 172 | self.find( | ||
| 173 | f'#imagerecipestable thead th.{th_class}' | ||
| 174 | ).is_displayed(), | ||
| 175 | f"The {th_class} column is unchecked in EditColumn dropdown, but it's visible in table" | ||
| 176 | ) | ||
| 177 | check_box.click() | ||
| 178 | # check if column is visible in table | ||
| 179 | self.assertTrue( | ||
| 180 | self.find( | ||
| 181 | f'#imagerecipestable thead th.{th_class}' | ||
| 182 | ).is_displayed(), | ||
| 183 | f"The {th_class} column is checked in EditColumn dropdown, but it's not visible in table" | ||
| 184 | ) | ||
| 185 | |||
| 186 | self._navigate_to_project_page() | ||
| 187 | # navigate to project image recipe page | ||
| 188 | recipe_image_page_link = self._get_config_nav_item(3) | ||
| 189 | recipe_image_page_link.click() | ||
| 190 | self.wait_until_present('#imagerecipestable tbody tr') | ||
| 191 | |||
| 192 | # Check edit column | ||
| 193 | edit_column = self.find('#edit-columns-button') | ||
| 194 | self.assertTrue(edit_column.is_displayed()) | ||
| 195 | edit_column.click() | ||
| 196 | # Check dropdown is visible | ||
| 197 | self.wait_until_visible('ul.dropdown-menu.editcol') | ||
| 198 | |||
| 199 | # Check that we can hide the edit column | ||
| 200 | test_edit_column('checkbox-get_description_or_summary') | ||
| 201 | test_edit_column('checkbox-layer_version__get_vcs_reference') | ||
| 202 | test_edit_column('checkbox-layer_version__layer__name') | ||
| 203 | test_edit_column('checkbox-license') | ||
| 204 | test_edit_column('checkbox-recipe-file') | ||
| 205 | test_edit_column('checkbox-section') | ||
| 206 | test_edit_column('checkbox-version') | ||
| 207 | |||
| 208 | def test_image_recipe_show_rows(self): | ||
| 209 | """ Test the show rows feature in image recipe table on project page """ | ||
| 210 | def test_show_rows(row_to_show, show_row_link): | ||
| 211 | # Check that we can show rows == row_to_show | ||
| 212 | show_row_link.select_by_value(str(row_to_show)) | ||
| 213 | self.wait_until_visible('#imagerecipestable tbody tr') | ||
| 214 | # check at least some rows are visible | ||
| 215 | self.assertTrue( | ||
| 216 | len(self.find_all('#imagerecipestable tbody tr')) > 0 | ||
| 217 | ) | ||
| 218 | |||
| 219 | self._navigate_to_project_page() | ||
| 220 | # navigate to project image recipe page | ||
| 221 | recipe_image_page_link = self._get_config_nav_item(3) | ||
| 222 | recipe_image_page_link.click() | ||
| 223 | self.wait_until_present('#imagerecipestable tbody tr') | ||
| 224 | |||
| 225 | show_rows = self.driver.find_elements( | ||
| 226 | By.XPATH, | ||
| 227 | '//select[@class="form-control pagesize-imagerecipestable"]' | ||
| 228 | ) | ||
| 229 | # Check show rows | ||
| 230 | for show_row_link in show_rows: | ||
| 231 | show_row_link = Select(show_row_link) | ||
| 232 | test_show_rows(10, show_row_link) | ||
| 233 | test_show_rows(25, show_row_link) | ||
| 234 | test_show_rows(50, show_row_link) | ||
| 235 | test_show_rows(100, show_row_link) | ||
| 236 | test_show_rows(150, show_row_link) | ||
| 237 | |||
| 238 | def test_project_config_tab_right_section(self): | ||
| 239 | """ Test project config tab right section contains five blocks: | ||
| 240 | - Machine: | ||
| 241 | - check 'Machine' is displayed | ||
| 242 | - check can change Machine | ||
| 243 | - Distro: | ||
| 244 | - check 'Distro' is displayed | ||
| 245 | - check can change Distro | ||
| 246 | - Most built recipes: | ||
| 247 | - check 'Most built recipes' is displayed | ||
| 248 | - check can select a recipe and build it | ||
| 249 | - Project release: | ||
| 250 | - check 'Project release' is displayed | ||
| 251 | - check project has right release displayed | ||
| 252 | - Layers: | ||
| 253 | - check can add a layer if exists | ||
| 254 | - check at least three layers are displayed | ||
| 255 | - openembedded-core | ||
| 256 | - meta-poky | ||
| 257 | - meta-yocto-bsp | ||
| 258 | """ | ||
| 259 | project_id = self.create_new_project(self.PROJECT_NAME + "-ST", '3', None, True) | ||
| 260 | url = reverse('project', args=(project_id,)) | ||
| 261 | self.get(url) | ||
| 262 | self.wait_until_visible('#config-nav') | ||
| 263 | |||
| 264 | # check if the menu is displayed | ||
| 265 | self.wait_until_visible('#project-page') | ||
| 266 | block_l = self.driver.find_element( | ||
| 267 | By.XPATH, '//*[@id="project-page"]/div[2]') | ||
| 268 | project_release = self.driver.find_element( | ||
| 269 | By.XPATH, '//*[@id="project-page"]/div[1]/div[4]') | ||
| 270 | layers = block_l.find_element(By.ID, 'layer-container') | ||
| 271 | |||
| 272 | def check_machine_distro(self, item_name, new_item_name, block_id): | ||
| 273 | block = self.find(f'#{block_id}') | ||
| 274 | title = block.find_element(By.TAG_NAME, 'h3') | ||
| 275 | self.assertIn(item_name.capitalize(), title.text) | ||
| 276 | edit_btn = self.find(f'#change-{item_name}-toggle') | ||
| 277 | edit_btn.click() | ||
| 278 | self.wait_until_visible(f'#{item_name}-change-input') | ||
| 279 | name_input = self.find(f'#{item_name}-change-input') | ||
| 280 | name_input.clear() | ||
| 281 | name_input.send_keys(new_item_name) | ||
| 282 | change_btn = self.find(f'#{item_name}-change-btn') | ||
| 283 | change_btn.click() | ||
| 284 | self.wait_until_visible(f'#project-{item_name}-name') | ||
| 285 | project_name = self.find(f'#project-{item_name}-name') | ||
| 286 | self.assertIn(new_item_name, project_name.text) | ||
| 287 | # check change notificaiton is displayed | ||
| 288 | change_notification = self.find('#change-notification') | ||
| 289 | self.assertIn( | ||
| 290 | f'You have changed the {item_name} to: {new_item_name}', change_notification.text | ||
| 291 | ) | ||
| 292 | hide_button = self.find('#hide-alert') | ||
| 293 | hide_button.click() | ||
| 294 | self.wait_until_not_visible('#change-notification') | ||
| 295 | |||
| 296 | # Machine | ||
| 297 | check_machine_distro(self, 'machine', 'qemux86-64', 'machine-section') | ||
| 298 | # Distro | ||
| 299 | check_machine_distro(self, 'distro', 'poky-altcfg', 'distro-section') | ||
| 300 | |||
| 301 | # Project release | ||
| 302 | title = project_release.find_element(By.TAG_NAME, 'h3') | ||
| 303 | self.assertIn("Project release", title.text) | ||
| 304 | self.assertIn( | ||
| 305 | "Yocto Project master", self.find('#project-release-title').text | ||
| 306 | ) | ||
| 307 | # Layers | ||
| 308 | title = layers.find_element(By.TAG_NAME, 'h3') | ||
| 309 | self.assertIn("Layers", title.text) | ||
| 310 | self.wait_until_clickable('#layer-add-input') | ||
| 311 | # check at least three layers are displayed | ||
| 312 | # openembedded-core | ||
| 313 | # meta-poky | ||
| 314 | # meta-yocto-bsp | ||
| 315 | layer_list_items = [] | ||
| 316 | starttime = time.time() | ||
| 317 | while len(layer_list_items) < 3: | ||
| 318 | layers_list = self.driver.find_element(By.ID, 'layers-in-project-list') | ||
| 319 | layer_list_items = layers_list.find_elements(By.TAG_NAME, 'li') | ||
| 320 | if time.time() > (starttime + 30): | ||
| 321 | self.fail("Layer list didn't contain at least 3 items within 30s (contained %d)" % len(layer_list_items)) | ||
| 322 | |||
| 323 | # remove all layers except the first three layers | ||
| 324 | for i in range(3, len(layer_list_items)): | ||
| 325 | layer_list_items[i].find_element(By.TAG_NAME, 'span').click() | ||
| 326 | |||
| 327 | # check can add a layer if exists | ||
| 328 | add_layer_input = layers.find_element(By.ID, 'layer-add-input') | ||
| 329 | add_layer_input.send_keys('meta-oe') | ||
| 330 | self.wait_until_visible('#layer-container > form > div > span > div') | ||
| 331 | self.wait_until_visible('.dropdown-menu') | ||
| 332 | finder = lambda driver: driver.find_element(By.XPATH, '//*[@id="layer-container"]/form/div/span/div/div/div') | ||
| 333 | dropdown_item = self.wait_until_element_clickable(finder) | ||
| 334 | dropdown_item.click() | ||
| 335 | self.wait_until_clickable('#add-layer-btn') | ||
| 336 | add_layer_btn = layers.find_element(By.ID, 'add-layer-btn') | ||
| 337 | add_layer_btn.click() | ||
| 338 | self.wait_until_visible('#layers-in-project-list') | ||
| 339 | |||
| 340 | # check layer is added | ||
| 341 | layer_list_items = [] | ||
| 342 | starttime = time.time() | ||
| 343 | while len(layer_list_items) < 4: | ||
| 344 | layers_list = self.driver.find_element(By.ID, 'layers-in-project-list') | ||
| 345 | layer_list_items = layers_list.find_elements(By.TAG_NAME, 'li') | ||
| 346 | if time.time() > (starttime + 30): | ||
| 347 | self.fail("Layer list didn't contain at least 4 items within 30s (contained %d)" % len(layer_list_items)) | ||
| 348 | |||
| 349 | def test_project_page_tab_importlayer(self): | ||
| 350 | """ Test project page tab import layer """ | ||
| 351 | self._navigate_to_project_page() | ||
| 352 | # navigate to "Import layers" tab | ||
| 353 | import_layers_tab = self._get_tabs()[2] | ||
| 354 | import_layers_tab.find_element(By.TAG_NAME, 'a').click() | ||
| 355 | self.wait_until_visible('#layer-git-repo-url') | ||
| 356 | |||
| 357 | # Check git repo radio button | ||
| 358 | git_repo_radio = self.find('#git-repo-radio') | ||
| 359 | git_repo_radio.click() | ||
| 360 | |||
| 361 | # Set git repo url | ||
| 362 | input_repo_url = self.find('#layer-git-repo-url') | ||
| 363 | input_repo_url.send_keys('git://git.yoctoproject.org/meta-fake') | ||
| 364 | # Blur the input to trigger the validation | ||
| 365 | input_repo_url.send_keys(Keys.TAB) | ||
| 366 | |||
| 367 | # Check name is set | ||
| 368 | input_layer_name = self.find('#import-layer-name') | ||
| 369 | self.assertTrue(input_layer_name.get_attribute('value') == 'meta-fake') | ||
| 370 | |||
| 371 | # Set branch | ||
| 372 | input_branch = self.find('#layer-git-ref') | ||
| 373 | input_branch.send_keys('master') | ||
| 374 | |||
| 375 | # Import layer | ||
| 376 | self.find('#import-and-add-btn').click() | ||
| 377 | |||
| 378 | # Check layer is added | ||
| 379 | self.wait_until_visible('#layer-container') | ||
| 380 | block_l = self.driver.find_element( | ||
| 381 | By.XPATH, '//*[@id="project-page"]/div[2]') | ||
| 382 | layers = block_l.find_element(By.ID, 'layer-container') | ||
| 383 | layers_list = layers.find_element(By.ID, 'layers-in-project-list') | ||
| 384 | layers_list_items = layers_list.find_elements(By.TAG_NAME, 'li') | ||
| 385 | self.assertIn( | ||
| 386 | 'meta-fake', str(layers_list_items[-1].text) | ||
| 387 | ) | ||
| 388 | |||
| 389 | def test_project_page_custom_image_no_image(self): | ||
| 390 | """ Test project page tab "New custom image" when no custom image """ | ||
| 391 | project_id = self.create_new_project(self.PROJECT_NAME + "-CustomImage", '3', None, True) | ||
| 392 | url = reverse('project', args=(project_id,)) | ||
| 393 | self.get(url) | ||
| 394 | self.wait_until_visible('#config-nav') | ||
| 395 | |||
| 396 | # navigate to "Custom image" tab | ||
| 397 | custom_image_section = self._get_config_nav_item(2) | ||
| 398 | custom_image_section.click() | ||
| 399 | self.wait_until_visible('#empty-state-customimagestable') | ||
| 400 | |||
| 401 | # Check message when no custom image | ||
| 402 | self.assertIn( | ||
| 403 | "You have not created any custom images yet.", str( | ||
| 404 | self.find('#empty-state-customimagestable').text | ||
| 405 | ) | ||
| 406 | ) | ||
| 407 | div_empty_msg = self.find('#empty-state-customimagestable') | ||
| 408 | link_create_custom_image = div_empty_msg.find_element( | ||
| 409 | By.TAG_NAME, 'a') | ||
| 410 | self.assertTrue(TestProjectConfigTabBase.project_id is not None) | ||
| 411 | self.assertIn( | ||
| 412 | f"/toastergui/project/{project_id}/newcustomimage", str( | ||
| 413 | link_create_custom_image.get_attribute('href') | ||
| 414 | ) | ||
| 415 | ) | ||
| 416 | self.assertIn( | ||
| 417 | "Create your first custom image", str( | ||
| 418 | link_create_custom_image.text | ||
| 419 | ) | ||
| 420 | ) | ||
| 421 | |||
| 422 | def test_project_page_image_recipe(self): | ||
| 423 | """ Test project page section images | ||
| 424 | - Check image recipes are displayed | ||
| 425 | - Check search input | ||
| 426 | - Check image recipe build button works | ||
| 427 | - Check image recipe table features(show/hide column, pagination) | ||
| 428 | """ | ||
| 429 | self._navigate_to_project_page() | ||
| 430 | # navigate to "Images section" | ||
| 431 | images_section = self._get_config_nav_item(3) | ||
| 432 | images_section.click() | ||
| 433 | self.wait_until_visible('#imagerecipestable') | ||
| 434 | rows = self.find_all('#imagerecipestable tbody tr') | ||
| 435 | self.assertTrue(len(rows) > 0) | ||
| 436 | |||
| 437 | # Test search input | ||
| 438 | self.wait_until_visible('#search-input-imagerecipestable') | ||
| 439 | recipe_input = self.find('#search-input-imagerecipestable') | ||
| 440 | recipe_input.send_keys('core-image-minimal') | ||
| 441 | self.find('#search-submit-imagerecipestable').click() | ||
| 442 | self.wait_until_visible('#imagerecipestable tbody tr') | ||
| 443 | rows = self.find_all('#imagerecipestable tbody tr') | ||
| 444 | self.assertTrue(len(rows) > 0) | ||
| 445 | |||
| 446 | @pytest.mark.django_db | ||
| 447 | @pytest.mark.order("last") | ||
| 448 | class TestProjectConfigTabDB(TestProjectConfigTabBase): | ||
| 449 | |||
| 450 | def test_most_build_recipes(self): | ||
| 451 | """ Test most build recipes block contains""" | ||
| 452 | def rebuild_from_most_build_recipes(recipe_list_items): | ||
| 453 | checkbox = recipe_list_items[0].find_element(By.TAG_NAME, 'input') | ||
| 454 | checkbox.click() | ||
| 455 | build_btn = self.find('#freq-build-btn') | ||
| 456 | build_btn.click() | ||
| 457 | self.wait_until_visible('#latest-builds') | ||
| 458 | wait_until_build(self, 'queued cloning starting parsing failed') | ||
| 459 | lastest_builds = self.driver.find_elements( | ||
| 460 | By.XPATH, | ||
| 461 | '//div[@id="latest-builds"]/div' | ||
| 462 | ) | ||
| 463 | self.assertTrue(len(lastest_builds) >= 2) | ||
| 464 | last_build = lastest_builds[0] | ||
| 465 | try: | ||
| 466 | cancel_button = last_build.find_element( | ||
| 467 | By.XPATH, | ||
| 468 | '//span[@class="cancel-build-btn pull-right alert-link"]', | ||
| 469 | ) | ||
| 470 | cancel_button.click() | ||
| 471 | except NoSuchElementException: | ||
| 472 | # Skip if the build is already cancelled | ||
| 473 | pass | ||
| 474 | wait_until_build_cancelled(self) | ||
| 475 | |||
| 476 | # Create a new project for remaining asserts | ||
| 477 | project_id = self.create_new_project(self.PROJECT_NAME + "-MostBuilt", '2', None, True) | ||
| 478 | url = reverse('project', args=(project_id,)) | ||
| 479 | self.get(url) | ||
| 480 | self.wait_until_visible('#config-nav') | ||
| 481 | |||
| 482 | current_url = self.driver.current_url | ||
| 483 | url = current_url.split('?')[0] | ||
| 484 | |||
| 485 | # Create a new builds | ||
| 486 | self._create_builds() | ||
| 487 | |||
| 488 | # back to project page | ||
| 489 | self.driver.get(url) | ||
| 490 | |||
| 491 | self.wait_until_visible('#project-page') | ||
| 492 | |||
| 493 | # Most built recipes | ||
| 494 | most_built_recipes = self.driver.find_element( | ||
| 495 | By.XPATH, '//*[@id="project-page"]/div[1]/div[3]') | ||
| 496 | title = most_built_recipes.find_element(By.TAG_NAME, 'h3') | ||
| 497 | self.assertIn("Most built recipes", title.text) | ||
| 498 | # check can select a recipe and build it | ||
| 499 | self.wait_until_visible('#freq-build-list') | ||
| 500 | recipe_list = self.find('#freq-build-list') | ||
| 501 | recipe_list_items = recipe_list.find_elements(By.TAG_NAME, 'li') | ||
| 502 | self.assertTrue( | ||
| 503 | len(recipe_list_items) > 0, | ||
| 504 | msg="No recipes found in the most built recipes list", | ||
| 505 | ) | ||
| 506 | rebuild_from_most_build_recipes(recipe_list_items) | ||
| 507 | |||
diff --git a/bitbake/lib/toaster/tests/functional/utils.py b/bitbake/lib/toaster/tests/functional/utils.py deleted file mode 100644 index 72345aef9f..0000000000 --- a/bitbake/lib/toaster/tests/functional/utils.py +++ /dev/null | |||
| @@ -1,86 +0,0 @@ | |||
| 1 | #!/usr/bin/env python3 | ||
| 2 | # -*- coding: utf-8 -*- | ||
| 3 | # BitBake Toaster UI tests implementation | ||
| 4 | # | ||
| 5 | # Copyright (C) 2023 Savoir-faire Linux | ||
| 6 | # | ||
| 7 | # SPDX-License-Identifier: GPL-2.0-only | ||
| 8 | |||
| 9 | |||
| 10 | from time import sleep | ||
| 11 | from selenium.common.exceptions import NoSuchElementException, StaleElementReferenceException, TimeoutException, WebDriverException | ||
| 12 | from selenium.webdriver.common.by import By | ||
| 13 | |||
| 14 | from orm.models import Build | ||
| 15 | |||
| 16 | |||
| 17 | def wait_until_build(test_instance, state): | ||
| 18 | timeout = 60 | ||
| 19 | start_time = 0 | ||
| 20 | build_state = '' | ||
| 21 | while True: | ||
| 22 | try: | ||
| 23 | if start_time > timeout: | ||
| 24 | raise TimeoutException( | ||
| 25 | f'Build did not reach {state} state within {timeout} seconds' | ||
| 26 | ) | ||
| 27 | last_build_state = test_instance.driver.find_element( | ||
| 28 | By.XPATH, | ||
| 29 | '//*[@id="latest-builds"]/div[1]//div[@class="build-state"]', | ||
| 30 | ) | ||
| 31 | build_state = last_build_state.get_attribute( | ||
| 32 | 'data-build-state') | ||
| 33 | state_text = state.lower().split() | ||
| 34 | if any(x in str(build_state).lower() for x in state_text): | ||
| 35 | return str(build_state).lower() | ||
| 36 | if 'failed' in str(build_state).lower(): | ||
| 37 | break | ||
| 38 | except NoSuchElementException: | ||
| 39 | pass | ||
| 40 | except TimeoutException: | ||
| 41 | break | ||
| 42 | start_time += 1 | ||
| 43 | sleep(1) # take a breath and try again | ||
| 44 | |||
| 45 | def wait_until_build_cancelled(test_instance): | ||
| 46 | """ Cancel build take a while sometime, the method is to wait driver action | ||
| 47 | until build being cancelled | ||
| 48 | """ | ||
| 49 | timeout = 30 | ||
| 50 | start_time = 0 | ||
| 51 | while True: | ||
| 52 | try: | ||
| 53 | if start_time > timeout: | ||
| 54 | raise TimeoutException( | ||
| 55 | f'Build did not reach cancelled state within {timeout} seconds' | ||
| 56 | ) | ||
| 57 | last_build_state = test_instance.driver.find_element( | ||
| 58 | By.XPATH, | ||
| 59 | '//*[@id="latest-builds"]/div[1]//div[@class="build-state"]', | ||
| 60 | ) | ||
| 61 | build_state = last_build_state.get_attribute( | ||
| 62 | 'data-build-state') | ||
| 63 | if 'failed' in str(build_state).lower(): | ||
| 64 | break | ||
| 65 | if 'cancelling' in str(build_state).lower(): | ||
| 66 | pass | ||
| 67 | if 'cancelled' in str(build_state).lower(): | ||
| 68 | break | ||
| 69 | except TimeoutException: | ||
| 70 | break | ||
| 71 | except NoSuchElementException: | ||
| 72 | pass | ||
| 73 | except StaleElementReferenceException: | ||
| 74 | pass | ||
| 75 | except WebDriverException: | ||
| 76 | pass | ||
| 77 | start_time += 1 | ||
| 78 | sleep(1) # take a breath and try again | ||
| 79 | |||
| 80 | def get_projectId_from_url(url): | ||
| 81 | # url = 'http://domainename.com/toastergui/project/1656/whatever | ||
| 82 | # or url = 'http://domainename.com/toastergui/project/1/ | ||
| 83 | # or url = 'http://domainename.com/toastergui/project/186 | ||
| 84 | assert '/toastergui/project/' in url, "URL is not valid" | ||
| 85 | url_to_list = url.split('/toastergui/project/') | ||
| 86 | return int(url_to_list[1].split('/')[0]) # project_id | ||
diff --git a/bitbake/lib/toaster/tests/toaster-tests-requirements.txt b/bitbake/lib/toaster/tests/toaster-tests-requirements.txt deleted file mode 100644 index 6243c00a36..0000000000 --- a/bitbake/lib/toaster/tests/toaster-tests-requirements.txt +++ /dev/null | |||
| @@ -1,9 +0,0 @@ | |||
| 1 | selenium>=4.13.0 | ||
| 2 | pytest==7.4.2 | ||
| 3 | pytest-django==4.5.2 | ||
| 4 | pytest-env==1.1.0 | ||
| 5 | pytest-html==4.0.2 | ||
| 6 | pytest-metadata==3.0.0 | ||
| 7 | pytest-order==1.1.0 | ||
| 8 | requests | ||
| 9 | |||
diff --git a/bitbake/lib/toaster/tests/views/README b/bitbake/lib/toaster/tests/views/README deleted file mode 100644 index 950c7c9897..0000000000 --- a/bitbake/lib/toaster/tests/views/README +++ /dev/null | |||
| @@ -1,4 +0,0 @@ | |||
| 1 | |||
| 2 | Django unit tests to verify classes and functions based on django Views | ||
| 3 | |||
| 4 | To run just these tests use ./manage.py test tests.views | ||
diff --git a/bitbake/lib/toaster/tests/views/__init__.py b/bitbake/lib/toaster/tests/views/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 --- a/bitbake/lib/toaster/tests/views/__init__.py +++ /dev/null | |||
diff --git a/bitbake/lib/toaster/tests/views/test_views.py b/bitbake/lib/toaster/tests/views/test_views.py deleted file mode 100644 index e1adfcf86a..0000000000 --- a/bitbake/lib/toaster/tests/views/test_views.py +++ /dev/null | |||
| @@ -1,544 +0,0 @@ | |||
| 1 | #! /usr/bin/env python3 | ||
| 2 | # | ||
| 3 | # BitBake Toaster Implementation | ||
| 4 | # | ||
| 5 | # Copyright (C) 2013-2015 Intel Corporation | ||
| 6 | # | ||
| 7 | # SPDX-License-Identifier: GPL-2.0-only | ||
| 8 | # | ||
| 9 | |||
| 10 | """Test cases for Toaster GUI and ReST.""" | ||
| 11 | |||
| 12 | import os | ||
| 13 | import pytest | ||
| 14 | from django.test import TestCase | ||
| 15 | from django.test.client import RequestFactory | ||
| 16 | from django.urls import reverse | ||
| 17 | from django.db.models import Q | ||
| 18 | |||
| 19 | from orm.models import Project, Package | ||
| 20 | from orm.models import Layer_Version, Recipe | ||
| 21 | from orm.models import CustomImageRecipe | ||
| 22 | from orm.models import CustomImagePackage | ||
| 23 | |||
| 24 | from bldcontrol.models import BuildEnvironment | ||
| 25 | import inspect | ||
| 26 | import toastergui | ||
| 27 | |||
| 28 | from toastergui.tables import SoftwareRecipesTable | ||
| 29 | import json | ||
| 30 | from bs4 import BeautifulSoup | ||
| 31 | import string | ||
| 32 | |||
| 33 | PROJECT_NAME = "test project" | ||
| 34 | PROJECT_NAME2 = "test project 2" | ||
| 35 | CLI_BUILDS_PROJECT_NAME = 'Command line builds' | ||
| 36 | |||
| 37 | |||
| 38 | |||
| 39 | class ViewTests(TestCase): | ||
| 40 | """Tests to verify view APIs.""" | ||
| 41 | |||
| 42 | fixtures = ['toastergui-unittest-data'] | ||
| 43 | builldir = os.environ.get('BUILDDIR') | ||
| 44 | |||
| 45 | def setUp(self): | ||
| 46 | |||
| 47 | self.project = Project.objects.first() | ||
| 48 | |||
| 49 | self.recipe1 = Recipe.objects.get(pk=2) | ||
| 50 | # create a file and to recipe1 file_path | ||
| 51 | file_path = f"{self.builldir}/{self.recipe1.name.strip().replace(' ', '-')}.bb" | ||
| 52 | with open(file_path, 'w') as f: | ||
| 53 | f.write('foo') | ||
| 54 | self.recipe1.file_path = file_path | ||
| 55 | self.recipe1.save() | ||
| 56 | |||
| 57 | self.customr = CustomImageRecipe.objects.first() | ||
| 58 | self.cust_package = CustomImagePackage.objects.first() | ||
| 59 | self.package = Package.objects.first() | ||
| 60 | self.lver = Layer_Version.objects.first() | ||
| 61 | if BuildEnvironment.objects.count() == 0: | ||
| 62 | BuildEnvironment.objects.create(betype=BuildEnvironment.TYPE_LOCAL) | ||
| 63 | |||
| 64 | |||
| 65 | def test_get_base_call_returns_html(self): | ||
| 66 | """Basic test for all-projects view""" | ||
| 67 | response = self.client.get(reverse('all-projects'), follow=True) | ||
| 68 | self.assertEqual(response.status_code, 200) | ||
| 69 | self.assertTrue(response['Content-Type'].startswith('text/html')) | ||
| 70 | self.assertTemplateUsed(response, "projects-toastertable.html") | ||
| 71 | |||
| 72 | def test_get_json_call_returns_json(self): | ||
| 73 | """Test for all projects output in json format""" | ||
| 74 | url = reverse('all-projects') | ||
| 75 | response = self.client.get(url, {"format": "json"}, follow=True) | ||
| 76 | self.assertEqual(response.status_code, 200) | ||
| 77 | self.assertTrue(response['Content-Type'].startswith( | ||
| 78 | 'application/json')) | ||
| 79 | |||
| 80 | data = json.loads(response.content.decode('utf-8')) | ||
| 81 | |||
| 82 | self.assertTrue("error" in data) | ||
| 83 | self.assertEqual(data["error"], "ok") | ||
| 84 | self.assertTrue("rows" in data) | ||
| 85 | |||
| 86 | name_found = False | ||
| 87 | for row in data["rows"]: | ||
| 88 | name_found = row['name'].find(self.project.name) | ||
| 89 | |||
| 90 | self.assertTrue(name_found, | ||
| 91 | "project name not found in projects table") | ||
| 92 | |||
| 93 | def test_typeaheads(self): | ||
| 94 | """Test typeahead ReST API""" | ||
| 95 | layers_url = reverse('xhr_layerstypeahead', args=(self.project.id,)) | ||
| 96 | prj_url = reverse('xhr_projectstypeahead') | ||
| 97 | |||
| 98 | urls = [layers_url, | ||
| 99 | prj_url, | ||
| 100 | reverse('xhr_recipestypeahead', args=(self.project.id,)), | ||
| 101 | reverse('xhr_machinestypeahead', args=(self.project.id,))] | ||
| 102 | |||
| 103 | def basic_reponse_check(response, url): | ||
| 104 | """Check data structure of http response.""" | ||
| 105 | self.assertEqual(response.status_code, 200) | ||
| 106 | self.assertTrue(response['Content-Type'].startswith( | ||
| 107 | 'application/json')) | ||
| 108 | |||
| 109 | data = json.loads(response.content.decode('utf-8')) | ||
| 110 | |||
| 111 | self.assertTrue("error" in data) | ||
| 112 | self.assertEqual(data["error"], "ok") | ||
| 113 | self.assertTrue("results" in data) | ||
| 114 | |||
| 115 | # We got a result so now check the fields | ||
| 116 | if len(data['results']) > 0: | ||
| 117 | result = data['results'][0] | ||
| 118 | |||
| 119 | self.assertTrue(len(result['name']) > 0) | ||
| 120 | self.assertTrue("detail" in result) | ||
| 121 | self.assertTrue(result['id'] > 0) | ||
| 122 | |||
| 123 | # Special check for the layers typeahead's extra fields | ||
| 124 | if url == layers_url: | ||
| 125 | self.assertTrue(len(result['layerdetailurl']) > 0) | ||
| 126 | self.assertTrue(len(result['vcs_url']) > 0) | ||
| 127 | self.assertTrue(len(result['vcs_reference']) > 0) | ||
| 128 | # Special check for project typeahead extra fields | ||
| 129 | elif url == prj_url: | ||
| 130 | self.assertTrue(len(result['projectPageUrl']) > 0) | ||
| 131 | |||
| 132 | return True | ||
| 133 | |||
| 134 | return False | ||
| 135 | |||
| 136 | for url in urls: | ||
| 137 | results = False | ||
| 138 | |||
| 139 | for typeing in list(string.ascii_letters): | ||
| 140 | response = self.client.get(url, {'search': typeing}) | ||
| 141 | results = basic_reponse_check(response, url) | ||
| 142 | if results: | ||
| 143 | break | ||
| 144 | |||
| 145 | # After "typeing" the alpabet we should have result true | ||
| 146 | # from each of the urls | ||
| 147 | self.assertTrue(results) | ||
| 148 | |||
| 149 | def test_xhr_add_layer(self): | ||
| 150 | """Test xhr_add API""" | ||
| 151 | # Test for importing an already existing layer | ||
| 152 | api_url = reverse('xhr_layer', args=(self.project.id,)) | ||
| 153 | |||
| 154 | layer_data = {'vcs_url': "git://git.example.com/test", | ||
| 155 | 'name': "base-layer", | ||
| 156 | 'git_ref': "c12b9596afd236116b25ce26dbe0d793de9dc7ce", | ||
| 157 | 'project_id': self.project.id, | ||
| 158 | 'local_source_dir': "", | ||
| 159 | 'add_to_project': True, | ||
| 160 | 'dir_path': "/path/in/repository"} | ||
| 161 | |||
| 162 | layer_data_json = json.dumps(layer_data) | ||
| 163 | |||
| 164 | response = self.client.put(api_url, layer_data_json) | ||
| 165 | data = json.loads(response.content.decode('utf-8')) | ||
| 166 | self.assertEqual(response.status_code, 200) | ||
| 167 | self.assertEqual(data["error"], "ok") | ||
| 168 | |||
| 169 | self.assertTrue( | ||
| 170 | layer_data['name'] in | ||
| 171 | self.project.get_all_compatible_layer_versions().values_list( | ||
| 172 | 'layer__name', | ||
| 173 | flat=True), | ||
| 174 | "Could not find imported layer in project's all layers list" | ||
| 175 | ) | ||
| 176 | |||
| 177 | # Empty data passed | ||
| 178 | response = self.client.put(api_url, "{}") | ||
| 179 | data = json.loads(response.content.decode('utf-8')) | ||
| 180 | self.assertNotEqual(data["error"], "ok") | ||
| 181 | |||
| 182 | def test_custom_ok(self): | ||
| 183 | """Test successful return from ReST API xhr_customrecipe""" | ||
| 184 | url = reverse('xhr_customrecipe') | ||
| 185 | params = {'name': 'custom', 'project': self.project.id, | ||
| 186 | 'base': self.recipe1.id} | ||
| 187 | response = self.client.post(url, params) | ||
| 188 | self.assertEqual(response.status_code, 200) | ||
| 189 | data = json.loads(response.content.decode('utf-8')) | ||
| 190 | self.assertEqual(data['error'], 'ok') | ||
| 191 | self.assertTrue('url' in data) | ||
| 192 | # get recipe from the database | ||
| 193 | recipe = CustomImageRecipe.objects.get(project=self.project, | ||
| 194 | name=params['name']) | ||
| 195 | args = (self.project.id, recipe.id,) | ||
| 196 | self.assertEqual(reverse('customrecipe', args=args), data['url']) | ||
| 197 | |||
| 198 | def test_custom_incomplete_params(self): | ||
| 199 | """Test not passing all required parameters to xhr_customrecipe""" | ||
| 200 | url = reverse('xhr_customrecipe') | ||
| 201 | for params in [{}, {'name': 'custom'}, | ||
| 202 | {'name': 'custom', 'project': self.project.id}]: | ||
| 203 | response = self.client.post(url, params) | ||
| 204 | self.assertEqual(response.status_code, 200) | ||
| 205 | data = json.loads(response.content.decode('utf-8')) | ||
| 206 | self.assertNotEqual(data["error"], "ok") | ||
| 207 | |||
| 208 | def test_xhr_custom_wrong_project(self): | ||
| 209 | """Test passing wrong project id to xhr_customrecipe""" | ||
| 210 | url = reverse('xhr_customrecipe') | ||
| 211 | params = {'name': 'custom', 'project': 0, "base": self.recipe1.id} | ||
| 212 | response = self.client.post(url, params) | ||
| 213 | self.assertEqual(response.status_code, 200) | ||
| 214 | data = json.loads(response.content.decode('utf-8')) | ||
| 215 | self.assertNotEqual(data["error"], "ok") | ||
| 216 | |||
| 217 | def test_xhr_custom_wrong_base(self): | ||
| 218 | """Test passing wrong base recipe id to xhr_customrecipe""" | ||
| 219 | url = reverse('xhr_customrecipe') | ||
| 220 | params = {'name': 'custom', 'project': self.project.id, "base": 0} | ||
| 221 | response = self.client.post(url, params) | ||
| 222 | self.assertEqual(response.status_code, 200) | ||
| 223 | data = json.loads(response.content.decode('utf-8')) | ||
| 224 | self.assertNotEqual(data["error"], "ok") | ||
| 225 | |||
| 226 | def test_xhr_custom_details(self): | ||
| 227 | """Test getting custom recipe details""" | ||
| 228 | url = reverse('xhr_customrecipe_id', args=(self.customr.id,)) | ||
| 229 | response = self.client.get(url) | ||
| 230 | self.assertEqual(response.status_code, 200) | ||
| 231 | expected = {"error": "ok", | ||
| 232 | "info": {'id': self.customr.id, | ||
| 233 | 'name': self.customr.name, | ||
| 234 | 'base_recipe_id': self.recipe1.id, | ||
| 235 | 'project_id': self.project.id}} | ||
| 236 | self.assertEqual(json.loads(response.content.decode('utf-8')), | ||
| 237 | expected) | ||
| 238 | |||
| 239 | def test_xhr_custom_del(self): | ||
| 240 | """Test deleting custom recipe""" | ||
| 241 | name = "to be deleted" | ||
| 242 | recipe = CustomImageRecipe.objects.create( | ||
| 243 | name=name, project=self.project, | ||
| 244 | base_recipe=self.recipe1, | ||
| 245 | file_path=f"{self.builldir}/testing", | ||
| 246 | layer_version=self.customr.layer_version) | ||
| 247 | url = reverse('xhr_customrecipe_id', args=(recipe.id,)) | ||
| 248 | response = self.client.delete(url) | ||
| 249 | self.assertEqual(response.status_code, 200) | ||
| 250 | |||
| 251 | gotoUrl = reverse('projectcustomimages', args=(self.project.pk,)) | ||
| 252 | |||
| 253 | self.assertEqual(json.loads(response.content.decode('utf-8')), | ||
| 254 | {"error": "ok", | ||
| 255 | "gotoUrl": gotoUrl}) | ||
| 256 | |||
| 257 | # try to delete not-existent recipe | ||
| 258 | url = reverse('xhr_customrecipe_id', args=(recipe.id,)) | ||
| 259 | response = self.client.delete(url) | ||
| 260 | self.assertEqual(response.status_code, 200) | ||
| 261 | self.assertNotEqual(json.loads( | ||
| 262 | response.content.decode('utf-8'))["error"], "ok") | ||
| 263 | |||
| 264 | def test_xhr_custom_packages(self): | ||
| 265 | """Test adding and deleting package to a custom recipe""" | ||
| 266 | # add self.package to recipe | ||
| 267 | response = self.client.put(reverse('xhr_customrecipe_packages', | ||
| 268 | args=(self.customr.id, | ||
| 269 | self.cust_package.id))) | ||
| 270 | |||
| 271 | self.assertEqual(response.status_code, 200) | ||
| 272 | self.assertEqual(json.loads(response.content.decode('utf-8')), | ||
| 273 | {"error": "ok"}) | ||
| 274 | self.assertEqual(self.customr.appends_set.first().name, | ||
| 275 | self.cust_package.name) | ||
| 276 | # delete it | ||
| 277 | to_delete = self.customr.appends_set.first().pk | ||
| 278 | del_url = reverse('xhr_customrecipe_packages', | ||
| 279 | args=(self.customr.id, to_delete)) | ||
| 280 | |||
| 281 | response = self.client.delete(del_url) | ||
| 282 | self.assertEqual(response.status_code, 200) | ||
| 283 | self.assertEqual(json.loads(response.content.decode('utf-8')), | ||
| 284 | {"error": "ok"}) | ||
| 285 | all_packages = self.customr.get_all_packages().values_list('pk', | ||
| 286 | flat=True) | ||
| 287 | |||
| 288 | self.assertFalse(to_delete in all_packages) | ||
| 289 | # delete invalid package to test error condition | ||
| 290 | del_url = reverse('xhr_customrecipe_packages', | ||
| 291 | args=(self.customr.id, | ||
| 292 | 99999)) | ||
| 293 | |||
| 294 | response = self.client.delete(del_url) | ||
| 295 | self.assertEqual(response.status_code, 200) | ||
| 296 | self.assertNotEqual(json.loads( | ||
| 297 | response.content.decode('utf-8'))["error"], "ok") | ||
| 298 | |||
| 299 | def test_xhr_custom_packages_err(self): | ||
| 300 | """Test error conditions of xhr_customrecipe_packages""" | ||
| 301 | # test calls with wrong recipe id and wrong package id | ||
| 302 | for args in [(0, self.package.id), (self.customr.id, 0)]: | ||
| 303 | url = reverse('xhr_customrecipe_packages', args=args) | ||
| 304 | # test put and delete methods | ||
| 305 | for method in (self.client.put, self.client.delete): | ||
| 306 | response = method(url) | ||
| 307 | self.assertEqual(response.status_code, 200) | ||
| 308 | self.assertNotEqual(json.loads( | ||
| 309 | response.content.decode('utf-8')), | ||
| 310 | {"error": "ok"}) | ||
| 311 | |||
| 312 | def test_download_custom_recipe(self): | ||
| 313 | """Download the recipe file generated for the custom image""" | ||
| 314 | |||
| 315 | # Create a dummy recipe file for the custom image generation to read | ||
| 316 | open(f"{self.builldir}/a_recipe.bb", 'a').close() | ||
| 317 | response = self.client.get(reverse('customrecipedownload', | ||
| 318 | args=(self.project.id, | ||
| 319 | self.customr.id))) | ||
| 320 | |||
| 321 | self.assertEqual(response.status_code, 200) | ||
| 322 | |||
| 323 | def test_software_recipes_table(self): | ||
| 324 | """Test structure returned for Software RecipesTable""" | ||
| 325 | table = SoftwareRecipesTable() | ||
| 326 | request = RequestFactory().get('/foo/', {'format': 'json'}) | ||
| 327 | response = table.get(request, pid=self.project.id) | ||
| 328 | data = json.loads(response.content.decode('utf-8')) | ||
| 329 | |||
| 330 | recipes = Recipe.objects.filter(Q(is_image=False)) | ||
| 331 | self.assertTrue(len(recipes) > 1, | ||
| 332 | "Need more than one software recipe to test " | ||
| 333 | "SoftwareRecipesTable") | ||
| 334 | |||
| 335 | recipe1 = recipes[0] | ||
| 336 | recipe2 = recipes[1] | ||
| 337 | |||
| 338 | rows = data['rows'] | ||
| 339 | row1 = next(x for x in rows if x['name'] == recipe1.name) | ||
| 340 | row2 = next(x for x in rows if x['name'] == recipe2.name) | ||
| 341 | |||
| 342 | self.assertEqual(response.status_code, 200, 'should be 200 OK status') | ||
| 343 | |||
| 344 | # check other columns have been populated correctly | ||
| 345 | self.assertTrue(recipe1.name in row1['name']) | ||
| 346 | self.assertTrue(recipe1.version in row1['version']) | ||
| 347 | self.assertTrue(recipe1.description in | ||
| 348 | row1['get_description_or_summary']) | ||
| 349 | |||
| 350 | self.assertTrue(recipe1.layer_version.layer.name in | ||
| 351 | row1['layer_version__layer__name']) | ||
| 352 | |||
| 353 | self.assertTrue(recipe2.name in row2['name']) | ||
| 354 | self.assertTrue(recipe2.version in row2['version']) | ||
| 355 | self.assertTrue(recipe2.description in | ||
| 356 | row2['get_description_or_summary']) | ||
| 357 | |||
| 358 | self.assertTrue(recipe2.layer_version.layer.name in | ||
| 359 | row2['layer_version__layer__name']) | ||
| 360 | |||
| 361 | def test_toaster_tables(self): | ||
| 362 | """Test all ToasterTables instances""" | ||
| 363 | |||
| 364 | def get_data(table, options={}): | ||
| 365 | """Send a request and parse the json response""" | ||
| 366 | options['format'] = "json" | ||
| 367 | options['nocache'] = "true" | ||
| 368 | request = RequestFactory().get('/', options) | ||
| 369 | |||
| 370 | # This is the image recipe needed for a package list for | ||
| 371 | # PackagesTable do this here to throw a non exist exception | ||
| 372 | image_recipe = Recipe.objects.get(pk=4) | ||
| 373 | |||
| 374 | # Add any kwargs that are needed by any of the possible tables | ||
| 375 | args = {'pid': self.project.id, | ||
| 376 | 'layerid': self.lver.pk, | ||
| 377 | 'recipeid': self.recipe1.pk, | ||
| 378 | 'recipe_id': image_recipe.pk, | ||
| 379 | 'custrecipeid': self.customr.pk, | ||
| 380 | 'build_id': 1, | ||
| 381 | 'target_id': 1} | ||
| 382 | |||
| 383 | response = table.get(request, **args) | ||
| 384 | return json.loads(response.content.decode('utf-8')) | ||
| 385 | |||
| 386 | def get_text_from_td(td): | ||
| 387 | """If we have html in the td then extract the text portion""" | ||
| 388 | # just so we don't waste time parsing non html | ||
| 389 | if "<" not in td: | ||
| 390 | ret = td | ||
| 391 | else: | ||
| 392 | ret = BeautifulSoup(td, "html.parser").text | ||
| 393 | |||
| 394 | if len(ret): | ||
| 395 | return "0" | ||
| 396 | else: | ||
| 397 | return ret | ||
| 398 | |||
| 399 | # Get a list of classes in tables module | ||
| 400 | tables = inspect.getmembers(toastergui.tables, inspect.isclass) | ||
| 401 | tables.extend(inspect.getmembers(toastergui.buildtables, | ||
| 402 | inspect.isclass)) | ||
| 403 | |||
| 404 | for name, table_cls in tables: | ||
| 405 | # Filter out the non ToasterTables from the tables module | ||
| 406 | if not issubclass(table_cls, toastergui.widgets.ToasterTable) or \ | ||
| 407 | table_cls == toastergui.widgets.ToasterTable or \ | ||
| 408 | 'Mixin' in name: | ||
| 409 | continue | ||
| 410 | |||
| 411 | # Get the table data without any options, this also does the | ||
| 412 | # initialisation of the table i.e. setup_columns, | ||
| 413 | # setup_filters and setup_queryset that we can use later | ||
| 414 | table = table_cls() | ||
| 415 | all_data = get_data(table) | ||
| 416 | |||
| 417 | self.assertTrue(len(all_data['rows']) > 1, | ||
| 418 | "Cannot test on a %s table with < 1 row" % name) | ||
| 419 | |||
| 420 | if table.default_orderby: | ||
| 421 | row_one = get_text_from_td( | ||
| 422 | all_data['rows'][0][table.default_orderby.strip("-")]) | ||
| 423 | row_two = get_text_from_td( | ||
| 424 | all_data['rows'][1][table.default_orderby.strip("-")]) | ||
| 425 | |||
| 426 | if '-' in table.default_orderby: | ||
| 427 | self.assertTrue(row_one >= row_two, | ||
| 428 | "Default ordering not working on %s" | ||
| 429 | " '%s' should be >= '%s'" % | ||
| 430 | (name, row_one, row_two)) | ||
| 431 | else: | ||
| 432 | self.assertTrue(row_one <= row_two, | ||
| 433 | "Default ordering not working on %s" | ||
| 434 | " '%s' should be <= '%s'" % | ||
| 435 | (name, row_one, row_two)) | ||
| 436 | |||
| 437 | # Test the column ordering and filtering functionality | ||
| 438 | for column in table.columns: | ||
| 439 | if column['orderable']: | ||
| 440 | # If a column is orderable test it in both order | ||
| 441 | # directions ordering on the columns field_name | ||
| 442 | ascending = get_data(table_cls(), | ||
| 443 | {"orderby": column['field_name']}) | ||
| 444 | |||
| 445 | row_one = get_text_from_td( | ||
| 446 | ascending['rows'][0][column['field_name']]) | ||
| 447 | row_two = get_text_from_td( | ||
| 448 | ascending['rows'][1][column['field_name']]) | ||
| 449 | |||
| 450 | self.assertTrue(row_one <= row_two, | ||
| 451 | "Ascending sort applied but row 0: \"%s\"" | ||
| 452 | " is less than row 1: \"%s\" " | ||
| 453 | "%s %s " % | ||
| 454 | (row_one, row_two, | ||
| 455 | column['field_name'], name)) | ||
| 456 | |||
| 457 | descending = get_data(table_cls(), | ||
| 458 | {"orderby": | ||
| 459 | '-'+column['field_name']}) | ||
| 460 | |||
| 461 | row_one = get_text_from_td( | ||
| 462 | descending['rows'][0][column['field_name']]) | ||
| 463 | row_two = get_text_from_td( | ||
| 464 | descending['rows'][1][column['field_name']]) | ||
| 465 | |||
| 466 | self.assertTrue(row_one >= row_two, | ||
| 467 | "Descending sort applied but row 0: %s" | ||
| 468 | "is greater than row 1: %s" | ||
| 469 | "field %s table %s" % | ||
| 470 | (row_one, | ||
| 471 | row_two, | ||
| 472 | column['field_name'], name)) | ||
| 473 | |||
| 474 | # If the two start rows are the same we haven't actually | ||
| 475 | # changed the order | ||
| 476 | self.assertNotEqual(ascending['rows'][0], | ||
| 477 | descending['rows'][0], | ||
| 478 | "An orderby %s has not changed the " | ||
| 479 | "order of the data in table %s" % | ||
| 480 | (column['field_name'], name)) | ||
| 481 | |||
| 482 | if column['filter_name']: | ||
| 483 | # If a filter is available for the column get the filter | ||
| 484 | # info. This contains what filter actions are defined. | ||
| 485 | filter_info = get_data(table_cls(), | ||
| 486 | {"cmd": "filterinfo", | ||
| 487 | "name": column['filter_name']}) | ||
| 488 | self.assertTrue(len(filter_info['filter_actions']) > 0, | ||
| 489 | "Filter %s was defined but no actions " | ||
| 490 | "added to it" % column['filter_name']) | ||
| 491 | |||
| 492 | for filter_action in filter_info['filter_actions']: | ||
| 493 | # filter string to pass as the option | ||
| 494 | # This is the name of the filter:action | ||
| 495 | # e.g. project_filter:not_in_project | ||
| 496 | filter_string = "%s:%s" % ( | ||
| 497 | column['filter_name'], | ||
| 498 | filter_action['action_name']) | ||
| 499 | # Now get the data with the filter applied | ||
| 500 | filtered_data = get_data(table_cls(), | ||
| 501 | {"filter": filter_string}) | ||
| 502 | |||
| 503 | # date range filter actions can't specify the | ||
| 504 | # number of results they return, so their count is 0 | ||
| 505 | if filter_action['count'] is not None: | ||
| 506 | self.assertEqual( | ||
| 507 | len(filtered_data['rows']), | ||
| 508 | int(filter_action['count']), | ||
| 509 | "We added a table filter for %s but " | ||
| 510 | "the number of rows returned was not " | ||
| 511 | "what the filter info said there " | ||
| 512 | "would be" % name) | ||
| 513 | |||
| 514 | # Test search functionality on the table | ||
| 515 | something_found = False | ||
| 516 | for search in list(string.ascii_letters): | ||
| 517 | search_data = get_data(table_cls(), {'search': search}) | ||
| 518 | |||
| 519 | if len(search_data['rows']) > 0: | ||
| 520 | something_found = True | ||
| 521 | break | ||
| 522 | |||
| 523 | self.assertTrue(something_found, | ||
| 524 | "We went through the whole alphabet and nothing" | ||
| 525 | " was found for the search of table %s" % name) | ||
| 526 | |||
| 527 | # Test the limit functionality on the table | ||
| 528 | limited_data = get_data(table_cls(), {'limit': "1"}) | ||
| 529 | self.assertEqual(len(limited_data['rows']), | ||
| 530 | 1, | ||
| 531 | "Limit 1 set on table %s but not 1 row returned" | ||
| 532 | % name) | ||
| 533 | |||
| 534 | # Test the pagination functionality on the table | ||
| 535 | page_one_data = get_data(table_cls(), {'limit': "1", | ||
| 536 | "page": "1"})['rows'][0] | ||
| 537 | |||
| 538 | page_two_data = get_data(table_cls(), {'limit': "1", | ||
| 539 | "page": "2"})['rows'][0] | ||
| 540 | |||
| 541 | self.assertNotEqual(page_one_data, | ||
| 542 | page_two_data, | ||
| 543 | "Changed page on table %s but first row is" | ||
| 544 | " the same as the previous page" % name) | ||
