summaryrefslogtreecommitdiffstats
path: root/bitbake/lib/toaster/tests
diff options
context:
space:
mode:
Diffstat (limited to 'bitbake/lib/toaster/tests')
-rw-r--r--bitbake/lib/toaster/tests/__init__.py0
-rw-r--r--bitbake/lib/toaster/tests/browser/README74
-rw-r--r--bitbake/lib/toaster/tests/browser/__init__.py0
-rw-r--r--bitbake/lib/toaster/tests/browser/selenium_helpers.py21
-rw-r--r--bitbake/lib/toaster/tests/browser/selenium_helpers_base.py277
-rw-r--r--bitbake/lib/toaster/tests/browser/test_all_builds_page.py477
-rw-r--r--bitbake/lib/toaster/tests/browser/test_all_projects_page.py337
-rw-r--r--bitbake/lib/toaster/tests/browser/test_builddashboard_page.py340
-rw-r--r--bitbake/lib/toaster/tests/browser/test_builddashboard_page_artifacts.py212
-rw-r--r--bitbake/lib/toaster/tests/browser/test_builddashboard_page_recipes.py54
-rw-r--r--bitbake/lib/toaster/tests/browser/test_builddashboard_page_tasks.py53
-rw-r--r--bitbake/lib/toaster/tests/browser/test_delete_project.py103
-rw-r--r--bitbake/lib/toaster/tests/browser/test_js_unit_tests.py45
-rw-r--r--bitbake/lib/toaster/tests/browser/test_landing_page.py233
-rw-r--r--bitbake/lib/toaster/tests/browser/test_layerdetails_page.py223
-rw-r--r--bitbake/lib/toaster/tests/browser/test_most_recent_builds_states.py201
-rw-r--r--bitbake/lib/toaster/tests/browser/test_new_custom_image_page.py159
-rw-r--r--bitbake/lib/toaster/tests/browser/test_new_project_page.py107
-rw-r--r--bitbake/lib/toaster/tests/browser/test_project_builds_page.py158
-rw-r--r--bitbake/lib/toaster/tests/browser/test_project_config_page.py220
-rw-r--r--bitbake/lib/toaster/tests/browser/test_project_page.py47
-rw-r--r--bitbake/lib/toaster/tests/browser/test_sample.py39
-rw-r--r--bitbake/lib/toaster/tests/browser/test_task_page.py64
-rw-r--r--bitbake/lib/toaster/tests/browser/test_toastertable_ui.py151
-rw-r--r--bitbake/lib/toaster/tests/builds/README14
-rw-r--r--bitbake/lib/toaster/tests/builds/__init__.py0
-rw-r--r--bitbake/lib/toaster/tests/builds/buildtest.py166
-rw-r--r--bitbake/lib/toaster/tests/builds/test_core_image_min.py363
-rw-r--r--bitbake/lib/toaster/tests/commands/__init__.py0
-rw-r--r--bitbake/lib/toaster/tests/commands/test_loaddata.py49
-rw-r--r--bitbake/lib/toaster/tests/commands/test_lsupdates.py34
-rw-r--r--bitbake/lib/toaster/tests/commands/test_runbuilds.py81
-rw-r--r--bitbake/lib/toaster/tests/db/__init__.py0
-rw-r--r--bitbake/lib/toaster/tests/db/test_db.py58
-rw-r--r--bitbake/lib/toaster/tests/eventreplay/README22
-rw-r--r--bitbake/lib/toaster/tests/eventreplay/__init__.py85
-rw-r--r--bitbake/lib/toaster/tests/functional/README0
-rw-r--r--bitbake/lib/toaster/tests/functional/__init__.py0
-rw-r--r--bitbake/lib/toaster/tests/functional/functional_helpers.py224
-rw-r--r--bitbake/lib/toaster/tests/functional/test_create_new_project.py124
-rw-r--r--bitbake/lib/toaster/tests/functional/test_functional_basic.py257
-rw-r--r--bitbake/lib/toaster/tests/functional/test_project_config.py294
-rw-r--r--bitbake/lib/toaster/tests/functional/test_project_page.py775
-rw-r--r--bitbake/lib/toaster/tests/functional/test_project_page_tab_config.py507
-rw-r--r--bitbake/lib/toaster/tests/functional/utils.py86
-rw-r--r--bitbake/lib/toaster/tests/toaster-tests-requirements.txt9
-rw-r--r--bitbake/lib/toaster/tests/views/README4
-rw-r--r--bitbake/lib/toaster/tests/views/__init__.py0
-rw-r--r--bitbake/lib/toaster/tests/views/test_views.py544
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
3These tests require Selenium to be installed in your Python environment.
4
5The simplest way to install this is via pip3:
6
7 pip3 install selenium==2.53.2
8
9Note that if you use other versions of Selenium, some of the tests (such as
10tests.browser.test_js_unit_tests.TestJsUnitTests) may fail, as these rely on
11a Selenium test report with a version-specific format.
12
13To 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
20To 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
27To run tests against Firefox, you may need to install the Marionette driver,
28depending on how new your version of Firefox is. One clue that you need to do
29this 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
35See https://developer.mozilla.org/en-US/docs/Mozilla/QA/Marionette/WebDriver
36for installation instructions. Ensure that the Marionette executable (renamed
37as wires on Linux or wires.exe on Windows) is on your PATH; and use "marionette"
38as the browser string passed via TOASTER_TESTS_BROWSER (see below).
39
40(Note: The Toaster tests have been checked against Firefox 47 with the
41Marionette driver.)
42
43The test cases will instantiate a Selenium driver set by the
44TOASTER_TESTS_BROWSER environment variable, or Chrome if this is not specified.
45
46To run tests against the Selenium Firefox Docker container:
47More 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
62Available drivers:
63
64* chrome (default)
65* firefox
66* marionette (for newer Firefoxes)
67* ie
68* phantomjs (deprecated)
69* remote
70
71e.g. to run the test suite with phantomjs where you have phantomjs installed
72in /home/me/apps/phantomjs:
73
74PATH=/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"""
14Helper methods for creating Toaster Selenium tests which run within
15the context of Django unit tests.
16"""
17from django.contrib.staticfiles.testing import StaticLiveServerTestCase
18from tests.browser.selenium_helpers_base import SeleniumTestCaseBase
19
20class 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"""
14Helper methods for creating Toaster Selenium tests which run within
15the context of Django unit tests.
16"""
17
18import os
19import time
20import unittest
21
22import pytest
23from selenium import webdriver
24from selenium.webdriver.support import expected_conditions as EC
25from selenium.webdriver.support.ui import WebDriverWait
26from selenium.webdriver.common.by import By
27from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
28from selenium.common.exceptions import NoSuchElementException, \
29 StaleElementReferenceException, TimeoutException, \
30 SessionNotCreatedException, WebDriverException
31
32def 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
88class 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
150class 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
10import os
11import re
12
13from django.urls import reverse
14from selenium.webdriver.support.select import Select
15from django.utils import timezone
16from bldcontrol.models import BuildRequest
17from tests.browser.selenium_helpers import SeleniumTestCase
18
19from orm.models import BitbakeVersion, Layer, Layer_Version, Recipe, Release, Project, Build, Target, Task
20
21from selenium.webdriver.common.by import By
22
23
24class 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
10import os
11import re
12
13from django.urls import reverse
14from django.utils import timezone
15from selenium.webdriver.support.select import Select
16from tests.browser.selenium_helpers import SeleniumTestCase
17
18from orm.models import BitbakeVersion, Release, Project, Build
19from orm.models import ProjectVariable
20
21from selenium.webdriver.common.by import By
22
23
24class 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
10import os
11from django.urls import reverse
12from django.utils import timezone
13
14from tests.browser.selenium_helpers import SeleniumTestCase
15
16from orm.models import Project, Release, BitbakeVersion, Build, LogMessage
17from orm.models import Layer, Layer_Version, Recipe, CustomImageRecipe, Variable
18
19from selenium.webdriver.common.by import By
20
21class 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
10import os
11from django.urls import reverse
12from django.utils import timezone
13
14from tests.browser.selenium_helpers import SeleniumTestCase
15
16from orm.models import Project, Release, BitbakeVersion, Build, Target, Package
17from orm.models import Target_Image_File, TargetSDKFile, TargetKernelFile
18from orm.models import Target_Installed_Package, Variable
19
20class 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
10from django.urls import reverse
11from django.utils import timezone
12from tests.browser.selenium_helpers import SeleniumTestCase
13from orm.models import Project, Build, Recipe, Task, Layer, Layer_Version
14from orm.models import Target
15
16class 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
10from django.urls import reverse
11from django.utils import timezone
12from tests.browser.selenium_helpers import SeleniumTestCase
13from orm.models import Project, Build, Recipe, Task, Layer, Layer_Version
14from orm.models import Target
15
16class 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
9import pytest
10from django.urls import reverse
11from selenium.webdriver.support.ui import Select
12from tests.browser.selenium_helpers import SeleniumTestCase
13from orm.models import BitbakeVersion, Project, Release
14from selenium.webdriver.common.by import By
15
16class 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"""
11Run the js unit tests
12"""
13
14from django.urls import reverse
15from tests.browser.selenium_helpers import SeleniumTestCase
16import logging
17
18logger = logging.getLogger("toaster")
19
20
21class 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
10from django.urls import reverse
11from django.utils import timezone
12from tests.browser.selenium_helpers import SeleniumTestCase
13from selenium.webdriver.common.by import By
14
15from orm.models import Layer, Layer_Version, Project, Build
16
17
18class 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
10from django.urls import reverse
11from selenium.common.exceptions import ElementClickInterceptedException, TimeoutException
12from tests.browser.selenium_helpers import SeleniumTestCase
13
14from orm.models import Layer, Layer_Version, Project, LayerSource, Release
15from orm.models import BitbakeVersion
16
17from selenium.webdriver.support import expected_conditions as EC
18from selenium.webdriver.support.ui import WebDriverWait
19from selenium.webdriver.common.by import By
20
21
22class 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#
9from django.urls import reverse
10from django.utils import timezone
11from tests.browser.selenium_helpers import SeleniumTestCase
12from tests.browser.selenium_helpers_base import Wait
13from orm.models import Project, Build, Task, Recipe, Layer, Layer_Version
14from bldcontrol.models import BuildRequest
15
16from selenium.webdriver.common.by import By
17
18class 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#
9from bldcontrol.models import BuildEnvironment
10
11from django.urls import reverse
12from tests.browser.selenium_helpers import SeleniumTestCase
13
14from orm.models import BitbakeVersion, Release, Project, ProjectLayer, Layer
15from orm.models import Layer_Version, Recipe, CustomImageRecipe
16
17
18class 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#
9from django.urls import reverse
10from tests.browser.selenium_helpers import SeleniumTestCase
11from selenium.webdriver.support.ui import Select
12from selenium.common.exceptions import InvalidElementStateException
13from selenium.webdriver.common.by import By
14
15from orm.models import Project, Release, BitbakeVersion
16
17
18class 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
10import os
11import re
12
13from django.urls import reverse
14from django.utils import timezone
15from tests.browser.selenium_helpers import SeleniumTestCase
16
17from orm.models import BitbakeVersion, Release, Project, Build, Target
18
19class 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
10import os
11from django.urls import reverse
12from tests.browser.selenium_helpers import SeleniumTestCase
13
14from orm.models import BitbakeVersion, Release, Project, ProjectVariable
15from selenium.webdriver.common.by import By
16
17class 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
10from django.urls import reverse
11from django.utils import timezone
12from tests.browser.selenium_helpers import SeleniumTestCase
13
14from orm.models import Build, Project
15
16class 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"""
11A small example test demonstrating the basics of writing a test with
12Toaster's SeleniumTestCase; this just fetches the Toaster home page
13and checks it has the word "Toaster" in the brand link
14
15New test files should follow this structure, should be named "test_*.py",
16and should be in the same directory as this sample.
17"""
18
19from django.urls import reverse
20from tests.browser.selenium_helpers import SeleniumTestCase
21
22class 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
10from django.urls import reverse
11from django.utils import timezone
12from tests.browser.selenium_helpers import SeleniumTestCase
13from orm.models import Project, Build, Layer, Layer_Version, Recipe, Target
14from orm.models import Task, Task_Dependency
15
16class 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
10from datetime import datetime
11import os
12
13from django.urls import reverse
14from django.utils import timezone
15from tests.browser.selenium_helpers import SeleniumTestCase
16from orm.models import BitbakeVersion, Release, Project, Build
17from selenium.webdriver.common.by import By
18
19class 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
3These tests are to test the running of builds and the data produced by the builds.
4Your oe build environment must be sourced/initialised for these tests to run.
5
6The 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
12Optional 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
10import os
11import sys
12import time
13import unittest
14
15from orm.models import Project, Release, ProjectTarget, Build, ProjectVariable
16from bldcontrol.models import BuildEnvironment
17
18from bldcontrol.management.commands.runbuilds import Command\
19 as RunBuildsCommand
20
21from django.core.management import call_command
22
23import subprocess
24import logging
25
26logger = 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
32def 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
88class 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
12import os
13import pytest
14
15from django.db.models import Q
16
17from orm.models import Target_Image_File, Target_Installed_Package, Task
18from orm.models import Package_Dependency, Recipe_Dependency, Build
19from orm.models import Task_Dependency, Package, Target, Recipe
20from orm.models import CustomImagePackage
21
22from tests.builds.buildtest import BuildTest
23
24@pytest.mark.order(4)
25@pytest.mark.django_db(True)
26class 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#
9import pytest
10from django.test import TestCase
11from django.core import management
12
13from orm.models import Layer_Version, Layer, Release, ToasterSetting
14
15@pytest.mark.order(2)
16class 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
10import pytest
11from django.test import TestCase
12from django.core import management
13
14from orm.models import Layer_Version, Machine, Recipe
15
16@pytest.mark.order(3)
17class 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
10import os
11
12from django.test import TestCase
13from django.core import management
14
15from orm.models import signal_runbuilds
16
17import threading
18import time
19import subprocess
20import signal
21
22import logging
23
24
25class 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
47class 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
25import sys
26import pytest
27
28try:
29 from StringIO import StringIO
30except ImportError:
31 from io import StringIO
32
33from contextlib import contextmanager
34
35from django.core import management
36from django.test import TestCase
37
38
39@contextmanager
40def 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
48def makemigrations():
49 management.call_command('makemigrations')
50
51@pytest.mark.order(1)
52class 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
3These tests use event log files produced by bitbake <target> -w <event log file>
4You need to have event log files produced before running this tests.
5
6At the moment of writing this document tests use 2 event log files: zlib.events
7and core-image-minimal.events. They're not provided with the tests due to their
8significant size.
9
10Here 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
18After 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
22Note 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"""
14Test toaster backend by playing build event log files
15using toaster-eventreplay script
16"""
17
18import os
19
20from subprocess import getstatusoutput
21from pathlib import Path
22
23from django.test import TestCase
24
25from orm.models import Target_Installed_Package, Package, Build
26
27class 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
52class 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
77class 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
10import os
11import logging
12import subprocess
13import signal
14import re
15import requests
16
17from django.urls import reverse
18from tests.browser.selenium_helpers_base import SeleniumTestCaseBase
19from selenium.webdriver.common.by import By
20from selenium.webdriver.support.select import Select
21from selenium.common.exceptions import NoSuchElementException
22
23logger = logging.getLogger("toaster")
24toaster_processes = []
25
26class 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
9import re
10import pytest
11from django.urls import reverse
12from selenium.webdriver.support.select import Select
13from tests.functional.functional_helpers import SeleniumFunctionalTestCase
14from selenium.webdriver.common.by import By
15
16class 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
10import re
11from django.urls import reverse
12import pytest
13from tests.functional.functional_helpers import SeleniumFunctionalTestCase
14from orm.models import Project
15from selenium.webdriver.common.by import By
16
17from tests.functional.utils import get_projectId_from_url
18
19
20class 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
9import string
10import pytest
11from django.urls import reverse
12from selenium.webdriver import Keys
13from selenium.webdriver.support.select import Select
14from selenium.common.exceptions import TimeoutException
15from tests.functional.functional_helpers import SeleniumFunctionalTestCase
16from selenium.webdriver.common.by import By
17
18from .utils import get_projectId_from_url
19
20class 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
9import os
10import string
11import time
12from unittest import skip
13import pytest
14from django.urls import reverse
15from django.utils import timezone
16from selenium.webdriver.common.keys import Keys
17from selenium.webdriver.support.select import Select
18from selenium.common.exceptions import TimeoutException
19from tests.functional.functional_helpers import SeleniumFunctionalTestCase
20from orm.models import Build, Project, Target
21from selenium.webdriver.common.by import By
22
23from .utils import get_projectId_from_url, wait_until_build, wait_until_build_cancelled
24
25class 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
185class 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")
716class 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
9import string
10import time
11import pytest
12from django.urls import reverse
13from selenium.webdriver import Keys
14from selenium.webdriver.support.select import Select
15from selenium.common.exceptions import ElementClickInterceptedException, NoSuchElementException, TimeoutException
16from tests.functional.functional_helpers import SeleniumFunctionalTestCase
17from selenium.webdriver.common.by import By
18
19from .utils import get_projectId_from_url, wait_until_build, wait_until_build_cancelled
20
21class 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
72class 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")
448class 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
10from time import sleep
11from selenium.common.exceptions import NoSuchElementException, StaleElementReferenceException, TimeoutException, WebDriverException
12from selenium.webdriver.common.by import By
13
14from orm.models import Build
15
16
17def 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
45def 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
80def 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 @@
1selenium>=4.13.0
2pytest==7.4.2
3pytest-django==4.5.2
4pytest-env==1.1.0
5pytest-html==4.0.2
6pytest-metadata==3.0.0
7pytest-order==1.1.0
8requests
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
2Django unit tests to verify classes and functions based on django Views
3
4To 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
12import os
13import pytest
14from django.test import TestCase
15from django.test.client import RequestFactory
16from django.urls import reverse
17from django.db.models import Q
18
19from orm.models import Project, Package
20from orm.models import Layer_Version, Recipe
21from orm.models import CustomImageRecipe
22from orm.models import CustomImagePackage
23
24from bldcontrol.models import BuildEnvironment
25import inspect
26import toastergui
27
28from toastergui.tables import SoftwareRecipesTable
29import json
30from bs4 import BeautifulSoup
31import string
32
33PROJECT_NAME = "test project"
34PROJECT_NAME2 = "test project 2"
35CLI_BUILDS_PROJECT_NAME = 'Command line builds'
36
37
38
39class 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)