diff options
Diffstat (limited to 'bitbake/lib/toaster/tests/functional/functional_helpers.py')
-rw-r--r-- | bitbake/lib/toaster/tests/functional/functional_helpers.py | 168 |
1 files changed, 141 insertions, 27 deletions
diff --git a/bitbake/lib/toaster/tests/functional/functional_helpers.py b/bitbake/lib/toaster/tests/functional/functional_helpers.py index 5c4ea71794..e28f2024f5 100644 --- a/bitbake/lib/toaster/tests/functional/functional_helpers.py +++ b/bitbake/lib/toaster/tests/functional/functional_helpers.py | |||
@@ -11,35 +11,58 @@ import os | |||
11 | import logging | 11 | import logging |
12 | import subprocess | 12 | import subprocess |
13 | import signal | 13 | import signal |
14 | import time | ||
15 | import re | 14 | import re |
15 | import requests | ||
16 | 16 | ||
17 | from django.urls import reverse | ||
17 | from tests.browser.selenium_helpers_base import SeleniumTestCaseBase | 18 | from tests.browser.selenium_helpers_base import SeleniumTestCaseBase |
18 | from tests.builds.buildtest import load_build_environment | 19 | from selenium.webdriver.common.by import By |
20 | from selenium.webdriver.support.select import Select | ||
21 | from selenium.common.exceptions import NoSuchElementException | ||
19 | 22 | ||
20 | logger = logging.getLogger("toaster") | 23 | logger = logging.getLogger("toaster") |
24 | toaster_processes = [] | ||
21 | 25 | ||
22 | class SeleniumFunctionalTestCase(SeleniumTestCaseBase): | 26 | class SeleniumFunctionalTestCase(SeleniumTestCaseBase): |
23 | wait_toaster_time = 5 | 27 | wait_toaster_time = 10 |
24 | 28 | ||
25 | @classmethod | 29 | @classmethod |
26 | def setUpClass(cls): | 30 | def setUpClass(cls): |
27 | # So that the buildinfo helper uses the test database' | 31 | # So that the buildinfo helper uses the test database' |
28 | if os.environ.get('DJANGO_SETTINGS_MODULE', '') != \ | 32 | if os.environ.get('DJANGO_SETTINGS_MODULE', '') != \ |
29 | 'toastermain.settings_test': | 33 | 'toastermain.settings_test': |
30 | raise RuntimeError("Please initialise django with the tests settings: " \ | 34 | raise RuntimeError("Please initialise django with the tests settings: " |
31 | "DJANGO_SETTINGS_MODULE='toastermain.settings_test'") | 35 | "DJANGO_SETTINGS_MODULE='toastermain.settings_test'") |
32 | 36 | ||
33 | load_build_environment() | 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 | ||
34 | 44 | ||
35 | # start toaster | 45 | # start toaster |
36 | cmd = "bash -c 'source toaster start'" | 46 | cmd = "bash -c 'source toaster start'" |
37 | p = subprocess.Popen( | 47 | start_process = subprocess.Popen( |
38 | cmd, | 48 | cmd, |
39 | cwd=os.environ.get("BUILDDIR"), | 49 | cwd=os.environ.get("BUILDDIR"), |
40 | shell=True) | 50 | shell=True) |
41 | if p.wait() != 0: | 51 | toaster_processes = [start_process.pid] |
42 | raise RuntimeError("Can't initialize toaster") | 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())) | ||
43 | 66 | ||
44 | super(SeleniumFunctionalTestCase, cls).setUpClass() | 67 | super(SeleniumFunctionalTestCase, cls).setUpClass() |
45 | cls.live_server_url = 'http://localhost:8000/' | 68 | cls.live_server_url = 'http://localhost:8000/' |
@@ -48,22 +71,30 @@ class SeleniumFunctionalTestCase(SeleniumTestCaseBase): | |||
48 | def tearDownClass(cls): | 71 | def tearDownClass(cls): |
49 | super(SeleniumFunctionalTestCase, cls).tearDownClass() | 72 | super(SeleniumFunctionalTestCase, cls).tearDownClass() |
50 | 73 | ||
51 | # XXX: source toaster stop gets blocked, to review why? | 74 | global toaster_processes |
52 | # from now send SIGTERM by hand | ||
53 | time.sleep(cls.wait_toaster_time) | ||
54 | builddir = os.environ.get("BUILDDIR") | ||
55 | 75 | ||
56 | with open(os.path.join(builddir, '.toastermain.pid'), 'r') as f: | 76 | cmd = "bash -c 'source toaster stop'" |
57 | toastermain_pid = int(f.read()) | 77 | stop_process = subprocess.Popen( |
58 | os.kill(toastermain_pid, signal.SIGTERM) | 78 | cmd, |
59 | with open(os.path.join(builddir, '.runbuilds.pid'), 'r') as f: | 79 | cwd=os.environ.get("BUILDDIR"), |
60 | runbuilds_pid = int(f.read()) | 80 | shell=True) |
61 | os.kill(runbuilds_pid, signal.SIGTERM) | 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) | ||
62 | 93 | ||
63 | 94 | ||
64 | def get_URL(self): | 95 | def get_URL(self): |
65 | rc=self.get_page_source() | 96 | rc=self.get_page_source() |
66 | project_url=re.search("(projectPageUrl\s:\s\")(.*)(\",)",rc) | 97 | project_url=re.search(r"(projectPageUrl\s:\s\")(.*)(\",)",rc) |
67 | return project_url.group(2) | 98 | return project_url.group(2) |
68 | 99 | ||
69 | 100 | ||
@@ -74,8 +105,8 @@ class SeleniumFunctionalTestCase(SeleniumTestCaseBase): | |||
74 | """ | 105 | """ |
75 | try: | 106 | try: |
76 | table_element = self.get_table_element(table_id) | 107 | table_element = self.get_table_element(table_id) |
77 | element = table_element.find_element_by_link_text(link_text) | 108 | element = table_element.find_element(By.LINK_TEXT, link_text) |
78 | except self.NoSuchElementException: | 109 | except NoSuchElementException: |
79 | print('no element found') | 110 | print('no element found') |
80 | raise | 111 | raise |
81 | return element | 112 | return element |
@@ -85,8 +116,8 @@ class SeleniumFunctionalTestCase(SeleniumTestCaseBase): | |||
85 | #return whole-table element | 116 | #return whole-table element |
86 | element_xpath = "//*[@id='" + table_id + "']" | 117 | element_xpath = "//*[@id='" + table_id + "']" |
87 | try: | 118 | try: |
88 | element = self.driver.find_element_by_xpath(element_xpath) | 119 | element = self.driver.find_element(By.XPATH, element_xpath) |
89 | except self.NoSuchElementException: | 120 | except NoSuchElementException: |
90 | raise | 121 | raise |
91 | return element | 122 | return element |
92 | row = coordinate[0] | 123 | row = coordinate[0] |
@@ -95,8 +126,8 @@ class SeleniumFunctionalTestCase(SeleniumTestCaseBase): | |||
95 | #return whole-row element | 126 | #return whole-row element |
96 | element_xpath = "//*[@id='" + table_id + "']/tbody/tr[" + str(row) + "]" | 127 | element_xpath = "//*[@id='" + table_id + "']/tbody/tr[" + str(row) + "]" |
97 | try: | 128 | try: |
98 | element = self.driver.find_element_by_xpath(element_xpath) | 129 | element = self.driver.find_element(By.XPATH, element_xpath) |
99 | except self.NoSuchElementException: | 130 | except NoSuchElementException: |
100 | return False | 131 | return False |
101 | return element | 132 | return element |
102 | #now we are looking for an element with specified X and Y | 133 | #now we are looking for an element with specified X and Y |
@@ -104,7 +135,90 @@ class SeleniumFunctionalTestCase(SeleniumTestCaseBase): | |||
104 | 135 | ||
105 | element_xpath = "//*[@id='" + table_id + "']/tbody/tr[" + str(row) + "]/td[" + str(column) + "]" | 136 | element_xpath = "//*[@id='" + table_id + "']/tbody/tr[" + str(row) + "]/td[" + str(column) + "]" |
106 | try: | 137 | try: |
107 | element = self.driver.find_element_by_xpath(element_xpath) | 138 | element = self.driver.find_element(By.XPATH, element_xpath) |
108 | except self.NoSuchElementException: | 139 | except NoSuchElementException: |
109 | return False | 140 | return False |
110 | return element | 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 | |||