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 selector = 'div[data-latest-build-result="%s"] .rebuild-btn' % build1.id 204 run_again_button = self.find_all(selector) 205 self.assertEqual(len(run_again_button), 1, 206 'should see a rebuild button for non-cli builds') 207 208 # shouldn't see a rebuild button for command-line builds 209 selector = 'div[data-latest-build-result="%s"] .rebuild-btn' % default_build.id 210 run_again_button = self.find_all(selector) 211 self.assertEqual(len(run_again_button), 0, 212 'should not see a rebuild button for cli builds') 213 214 def test_tooltips_on_project_name(self): 215 """ 216 Test tooltips shown next to project name in the main table 217 218 A tooltip should be present next to the command line 219 builds project name in the all builds page, but not for 220 other projects 221 """ 222 Build.objects.create(**self.project1_build_success) 223 Build.objects.create(**self.default_project_build_success) 224 225 url = reverse('all-builds') 226 self.get(url) 227 self.wait_until_visible('#allbuildstable', poll=3) 228 229 # get the project name cells from the table 230 cells = self.find_all('#allbuildstable td[class="project"]') 231 232 selector = 'span.get-help' 233 234 for cell in cells: 235 content = cell.get_attribute('innerHTML') 236 help_icons = cell.find_elements(By.CSS_SELECTOR, selector) 237 238 if re.search(self.PROJECT_NAME, content): 239 # no help icon next to non-cli project name 240 msg = 'should not be a help icon for non-cli builds name' 241 self.assertEqual(len(help_icons), 0, msg) 242 elif re.search(self.CLI_BUILDS_PROJECT_NAME, content): 243 # help icon next to cli project name 244 msg = 'should be a help icon for cli builds name' 245 self.assertEqual(len(help_icons), 1, msg) 246 else: 247 msg = 'found unexpected project name cell in all builds table' 248 self.fail(msg) 249 250 def test_builds_time_links(self): 251 """ 252 Successful builds should have links on the time column and in the 253 recent builds area; failed builds should not have links on the time column, 254 or in the recent builds area 255 """ 256 build1, build2 = self._get_create_builds() 257 258 url = reverse('all-builds') 259 self.get(url) 260 self.wait_until_visible('#allbuildstable', poll=3) 261 262 # test recent builds area for successful build 263 element = self._get_build_time_element(build1) 264 links = element.find_elements(By.CSS_SELECTOR, 'a') 265 msg = 'should be a link on the build time for a successful recent build' 266 self.assertEqual(len(links), 1, msg) 267 268 # test recent builds area for failed build 269 element = self._get_build_time_element(build2) 270 links = element.find_elements(By.CSS_SELECTOR, 'a') 271 msg = 'should not be a link on the build time for a failed recent build' 272 self.assertEqual(len(links), 0, msg) 273 274 # test the time column for successful build 275 build1_row = self._get_row_for_build(build1) 276 links = build1_row.find_elements(By.CSS_SELECTOR, 'td.time a') 277 msg = 'should be a link on the build time for a successful build' 278 self.assertEqual(len(links), 1, msg) 279 280 # test the time column for failed build 281 build2_row = self._get_row_for_build(build2) 282 links = build2_row.find_elements(By.CSS_SELECTOR, 'td.time a') 283 msg = 'should not be a link on the build time for a failed build' 284 self.assertEqual(len(links), 0, msg) 285 286 def test_builds_table_search_box(self): 287 """ Test the search box in the builds table on the all builds page """ 288 self._get_create_builds() 289 290 url = reverse('all-builds') 291 self.get(url) 292 293 # Check search box is present and works 294 self.wait_until_visible('#allbuildstable tbody tr') 295 search_box = self.find('#search-input-allbuildstable') 296 self.assertTrue(search_box.is_displayed()) 297 298 # Check that we can search for a build by recipe name 299 search_box.send_keys('foo') 300 search_btn = self.find('#search-submit-allbuildstable') 301 search_btn.click() 302 self.wait_until_visible('#allbuildstable tbody tr') 303 rows = self.find_all('#allbuildstable tbody tr') 304 self.assertTrue(len(rows) >= 1) 305 306 def test_filtering_on_failure_tasks_column(self): 307 """ Test the filtering on failure tasks column in the builds table on the all builds page """ 308 def _check_if_filter_failed_tasks_column_is_visible(): 309 # check if failed tasks filter column is visible, if not click on it 310 # Check edit column 311 edit_column = self.find('#edit-columns-button') 312 self.assertTrue(edit_column.is_displayed()) 313 edit_column.click() 314 # Check dropdown is visible 315 self.wait_until_visible('ul.dropdown-menu.editcol') 316 filter_fails_task_checkbox = self.find('#checkbox-failed_tasks') 317 if not filter_fails_task_checkbox.is_selected(): 318 filter_fails_task_checkbox.click() 319 edit_column.click() 320 321 self._get_create_builds(success=10, failure=10) 322 323 url = reverse('all-builds') 324 self.get(url) 325 326 # Check filtering on failure tasks column 327 self.wait_until_visible('#allbuildstable tbody tr') 328 _check_if_filter_failed_tasks_column_is_visible() 329 failed_tasks_filter = self.find('#failed_tasks_filter') 330 failed_tasks_filter.click() 331 # Check popup is visible 332 self.wait_until_visible('#filter-modal-allbuildstable') 333 self.assertTrue( 334 self.find('#filter-modal-allbuildstable').is_displayed()) 335 # Check that we can filter by failure tasks 336 build_without_failure_tasks = self.find( 337 '#failed_tasks_filter\\:without_failed_tasks') 338 build_without_failure_tasks.click() 339 # click on apply button 340 self.find('#filter-modal-allbuildstable .btn-primary').click() 341 self.wait_until_visible('#allbuildstable tbody tr') 342 # Check if filter is applied, by checking if failed_tasks_filter has btn-primary class 343 self.assertTrue(self.find('#failed_tasks_filter').get_attribute( 344 'class').find('btn-primary') != -1) 345 346 def test_filtering_on_completedOn_column(self): 347 """ Test the filtering on completed_on column in the builds table on the all builds page """ 348 self._get_create_builds(success=10, failure=10) 349 350 url = reverse('all-builds') 351 self.get(url) 352 353 # Check filtering on failure tasks column 354 self.wait_until_visible('#allbuildstable tbody tr') 355 completed_on_filter = self.find('#completed_on_filter') 356 completed_on_filter.click() 357 # Check popup is visible 358 self.wait_until_visible('#filter-modal-allbuildstable') 359 self.assertTrue( 360 self.find('#filter-modal-allbuildstable').is_displayed()) 361 # Check that we can filter by failure tasks 362 build_without_failure_tasks = self.find( 363 '#completed_on_filter\\:date_range') 364 build_without_failure_tasks.click() 365 # click on apply button 366 self.find('#filter-modal-allbuildstable .btn-primary').click() 367 self.wait_until_visible('#allbuildstable tbody tr') 368 # Check if filter is applied, by checking if completed_on_filter has btn-primary class 369 self.assertTrue(self.find('#completed_on_filter').get_attribute( 370 'class').find('btn-primary') != -1) 371 372 # Filter by date range 373 self.find('#completed_on_filter').click() 374 self.wait_until_visible('#filter-modal-allbuildstable') 375 date_ranges = self.driver.find_elements( 376 By.XPATH, '//input[@class="form-control hasDatepicker"]') 377 today = timezone.now() 378 yestersday = today - timezone.timedelta(days=1) 379 date_ranges[0].send_keys(yestersday.strftime('%Y-%m-%d')) 380 date_ranges[1].send_keys(today.strftime('%Y-%m-%d')) 381 self.find('#filter-modal-allbuildstable .btn-primary').click() 382 self.wait_until_visible('#allbuildstable tbody tr') 383 self.assertTrue(self.find('#completed_on_filter').get_attribute( 384 'class').find('btn-primary') != -1) 385 # Check if filter is applied, number of builds displayed should be 6 386 self.assertTrue(len(self.find_all('#allbuildstable tbody tr')) >= 4) 387 388 def test_builds_table_editColumn(self): 389 """ Test the edit column feature in the builds table on the all builds page """ 390 self._get_create_builds(success=10, failure=10) 391 392 def test_edit_column(check_box_id): 393 # Check that we can hide/show table column 394 check_box = self.find(f'#{check_box_id}') 395 th_class = str(check_box_id).replace('checkbox-', '') 396 if check_box.is_selected(): 397 # check if column is visible in table 398 self.assertTrue( 399 self.find( 400 f'#allbuildstable thead th.{th_class}' 401 ).is_displayed(), 402 f"The {th_class} column is checked in EditColumn dropdown, but it's not visible in table" 403 ) 404 check_box.click() 405 # check if column is hidden in table 406 self.assertFalse( 407 self.find( 408 f'#allbuildstable thead th.{th_class}' 409 ).is_displayed(), 410 f"The {th_class} column is unchecked in EditColumn dropdown, but it's visible in table" 411 ) 412 else: 413 # check if column is hidden in table 414 self.assertFalse( 415 self.find( 416 f'#allbuildstable thead th.{th_class}' 417 ).is_displayed(), 418 f"The {th_class} column is unchecked in EditColumn dropdown, but it's visible in table" 419 ) 420 check_box.click() 421 # check if column is visible in table 422 self.assertTrue( 423 self.find( 424 f'#allbuildstable thead th.{th_class}' 425 ).is_displayed(), 426 f"The {th_class} column is checked in EditColumn dropdown, but it's not visible in table" 427 ) 428 url = reverse('all-builds') 429 self.get(url) 430 self.wait_until_visible('#allbuildstable tbody tr') 431 432 # Check edit column 433 edit_column = self.find('#edit-columns-button') 434 self.assertTrue(edit_column.is_displayed()) 435 edit_column.click() 436 # Check dropdown is visible 437 self.wait_until_visible('ul.dropdown-menu.editcol') 438 439 # Check that we can hide the edit column 440 test_edit_column('checkbox-errors_no') 441 test_edit_column('checkbox-failed_tasks') 442 test_edit_column('checkbox-image_files') 443 test_edit_column('checkbox-project') 444 test_edit_column('checkbox-started_on') 445 test_edit_column('checkbox-time') 446 test_edit_column('checkbox-warnings_no') 447 448 def test_builds_table_show_rows(self): 449 """ Test the show rows feature in the builds table on the all builds page """ 450 self._get_create_builds(success=100, failure=100) 451 452 def test_show_rows(row_to_show, show_row_link): 453 # Check that we can show rows == row_to_show 454 show_row_link.select_by_value(str(row_to_show)) 455 self.wait_until_visible('#allbuildstable tbody tr', poll=3) 456 # check at least some rows are visible 457 self.assertTrue( 458 len(self.find_all('#allbuildstable tbody tr')) > 0 459 ) 460 461 url = reverse('all-builds') 462 self.get(url) 463 self.wait_until_visible('#allbuildstable tbody tr') 464 465 show_rows = self.driver.find_elements( 466 By.XPATH, 467 '//select[@class="form-control pagesize-allbuildstable"]' 468 ) 469 # Check show rows 470 for show_row_link in show_rows: 471 show_row_link = Select(show_row_link) 472 test_show_rows(10, show_row_link) 473 test_show_rows(25, show_row_link) 474 test_show_rows(50, show_row_link) 475 test_show_rows(100, show_row_link) 476 test_show_rows(150, show_row_link) 477