| #! /usr/bin/env python3 |
| # |
| # BitBake Toaster Implementation |
| # |
| # Copyright (C) 2013-2016 Intel Corporation |
| # |
| # SPDX-License-Identifier: GPL-2.0-only |
| # |
| |
| import os |
| import re |
| |
| from django.urls import reverse |
| from selenium.webdriver.support.select import Select |
| from django.utils import timezone |
| from bldcontrol.models import BuildRequest |
| from tests.browser.selenium_helpers import SeleniumTestCase |
| |
| from orm.models import BitbakeVersion, Layer, Layer_Version, Recipe, Release, Project, Build, Target, Task |
| |
| from selenium.webdriver.common.by import By |
| |
| |
| class TestAllBuildsPage(SeleniumTestCase): |
| """ Tests for all builds page /builds/ """ |
| |
| PROJECT_NAME = 'test project' |
| CLI_BUILDS_PROJECT_NAME = 'command line builds' |
| |
| def setUp(self): |
| builldir = os.environ.get('BUILDDIR', './') |
| bbv = BitbakeVersion.objects.create(name='bbv1', giturl=f'{builldir}/', |
| branch='master', dirpath='') |
| release = Release.objects.create(name='release1', |
| bitbake_version=bbv) |
| self.project1 = Project.objects.create_project(name=self.PROJECT_NAME, |
| release=release) |
| self.default_project = Project.objects.create_project( |
| name=self.CLI_BUILDS_PROJECT_NAME, |
| release=release |
| ) |
| self.default_project.is_default = True |
| self.default_project.save() |
| |
| # parameters for builds to associate with the projects |
| now = timezone.now() |
| |
| self.project1_build_success = { |
| 'project': self.project1, |
| 'started_on': now, |
| 'completed_on': now, |
| 'outcome': Build.SUCCEEDED |
| } |
| |
| self.project1_build_failure = { |
| 'project': self.project1, |
| 'started_on': now, |
| 'completed_on': now, |
| 'outcome': Build.FAILED |
| } |
| |
| self.default_project_build_success = { |
| 'project': self.default_project, |
| 'started_on': now, |
| 'completed_on': now, |
| 'outcome': Build.SUCCEEDED |
| } |
| |
| def _get_build_time_element(self, build): |
| """ |
| Return the HTML element containing the build time for a build |
| in the recent builds area |
| """ |
| selector = 'div[data-latest-build-result="%s"] ' \ |
| '[data-role="data-recent-build-buildtime-field"]' % build.id |
| |
| # because this loads via Ajax, wait for it to be visible |
| self.wait_until_visible(selector) |
| |
| build_time_spans = self.find_all(selector) |
| |
| self.assertEqual(len(build_time_spans), 1) |
| |
| return build_time_spans[0] |
| |
| def _get_row_for_build(self, build): |
| """ Get the table row for the build from the all builds table """ |
| self.wait_until_visible('#allbuildstable') |
| |
| rows = self.find_all('#allbuildstable tr') |
| |
| # look for the row with a download link on the recipe which matches the |
| # build ID |
| url = reverse('builddashboard', args=(build.id,)) |
| selector = 'td.target a[href="%s"]' % url |
| |
| found_row = None |
| for row in rows: |
| |
| outcome_links = row.find_elements(By.CSS_SELECTOR, selector) |
| if len(outcome_links) == 1: |
| found_row = row |
| break |
| |
| self.assertNotEqual(found_row, None) |
| |
| return found_row |
| |
| def _get_create_builds(self, **kwargs): |
| """ Create a build and return the build object """ |
| build1 = Build.objects.create(**self.project1_build_success) |
| build2 = Build.objects.create(**self.project1_build_failure) |
| |
| # add some targets to these builds so they have recipe links |
| # (and so we can find the row in the ToasterTable corresponding to |
| # a particular build) |
| Target.objects.create(build=build1, target='foo') |
| Target.objects.create(build=build2, target='bar') |
| |
| if kwargs: |
| # Create kwargs.get('success') builds with success status with target |
| # and kwargs.get('failure') builds with failure status with target |
| for i in range(kwargs.get('success', 0)): |
| now = timezone.now() |
| self.project1_build_success['started_on'] = now |
| self.project1_build_success[ |
| 'completed_on'] = now - timezone.timedelta(days=i) |
| build = Build.objects.create(**self.project1_build_success) |
| Target.objects.create(build=build, |
| target=f'{i}_success_recipe', |
| task=f'{i}_success_task') |
| |
| self._set_buildRequest_and_task_on_build(build) |
| for i in range(kwargs.get('failure', 0)): |
| now = timezone.now() |
| self.project1_build_failure['started_on'] = now |
| self.project1_build_failure[ |
| 'completed_on'] = now - timezone.timedelta(days=i) |
| build = Build.objects.create(**self.project1_build_failure) |
| Target.objects.create(build=build, |
| target=f'{i}_fail_recipe', |
| task=f'{i}_fail_task') |
| self._set_buildRequest_and_task_on_build(build) |
| return build1, build2 |
| |
| def _create_recipe(self): |
| """ Add a recipe to the database and return it """ |
| layer = Layer.objects.create() |
| layer_version = Layer_Version.objects.create(layer=layer) |
| return Recipe.objects.create(name='recipe_foo', layer_version=layer_version) |
| |
| def _set_buildRequest_and_task_on_build(self, build): |
| """ Set buildRequest and task on build """ |
| build.recipes_parsed = 1 |
| build.save() |
| buildRequest = BuildRequest.objects.create( |
| build=build, |
| project=self.project1, |
| state=BuildRequest.REQ_COMPLETED) |
| build.build_request = buildRequest |
| recipe = self._create_recipe() |
| task = Task.objects.create(build=build, |
| recipe=recipe, |
| task_name='task', |
| outcome=Task.OUTCOME_SUCCESS) |
| task.save() |
| build.save() |
| |
| def test_show_tasks_with_suffix(self): |
| """ Task should be shown as suffix on build name """ |
| build = Build.objects.create(**self.project1_build_success) |
| target = 'bash' |
| task = 'clean' |
| Target.objects.create(build=build, target=target, task=task) |
| |
| url = reverse('all-builds') |
| self.get(url) |
| self.wait_until_visible('td[class="target"]') |
| |
| cell = self.find('td[class="target"]') |
| content = cell.get_attribute('innerHTML') |
| expected_text = '%s:%s' % (target, task) |
| |
| self.assertTrue(re.search(expected_text, content), |
| '"target" cell should contain text %s' % expected_text) |
| |
| def test_rebuild_buttons(self): |
| """ |
| Test 'Rebuild' buttons in recent builds section |
| |
| 'Rebuild' button should not be shown for command-line builds, |
| but should be shown for other builds |
| """ |
| build1 = Build.objects.create(**self.project1_build_success) |
| default_build = Build.objects.create( |
| **self.default_project_build_success) |
| |
| url = reverse('all-builds') |
| self.get(url) |
| |
| # should see a rebuild button for non-command-line builds |
| self.wait_until_visible('#allbuildstable tbody tr') |
| selector = 'div[data-latest-build-result="%s"] .rebuild-btn' % build1.id |
| run_again_button = self.find_all(selector) |
| self.assertEqual(len(run_again_button), 1, |
| 'should see a rebuild button for non-cli builds') |
| |
| # shouldn't see a rebuild button for command-line builds |
| selector = 'div[data-latest-build-result="%s"] .rebuild-btn' % default_build.id |
| run_again_button = self.find_all(selector) |
| self.assertEqual(len(run_again_button), 0, |
| 'should not see a rebuild button for cli builds') |
| |
| def test_tooltips_on_project_name(self): |
| """ |
| Test tooltips shown next to project name in the main table |
| |
| A tooltip should be present next to the command line |
| builds project name in the all builds page, but not for |
| other projects |
| """ |
| Build.objects.create(**self.project1_build_success) |
| Build.objects.create(**self.default_project_build_success) |
| |
| url = reverse('all-builds') |
| self.get(url) |
| self.wait_until_visible('#allbuildstable', poll=3) |
| |
| # get the project name cells from the table |
| cells = self.find_all('#allbuildstable td[class="project"]') |
| |
| selector = 'span.get-help' |
| |
| for cell in cells: |
| content = cell.get_attribute('innerHTML') |
| help_icons = cell.find_elements(By.CSS_SELECTOR, selector) |
| |
| if re.search(self.PROJECT_NAME, content): |
| # no help icon next to non-cli project name |
| msg = 'should not be a help icon for non-cli builds name' |
| self.assertEqual(len(help_icons), 0, msg) |
| elif re.search(self.CLI_BUILDS_PROJECT_NAME, content): |
| # help icon next to cli project name |
| msg = 'should be a help icon for cli builds name' |
| self.assertEqual(len(help_icons), 1, msg) |
| else: |
| msg = 'found unexpected project name cell in all builds table' |
| self.fail(msg) |
| |
| def test_builds_time_links(self): |
| """ |
| Successful builds should have links on the time column and in the |
| recent builds area; failed builds should not have links on the time column, |
| or in the recent builds area |
| """ |
| build1, build2 = self._get_create_builds() |
| |
| url = reverse('all-builds') |
| self.get(url) |
| self.wait_until_visible('#allbuildstable', poll=3) |
| |
| # test recent builds area for successful build |
| element = self._get_build_time_element(build1) |
| links = element.find_elements(By.CSS_SELECTOR, 'a') |
| msg = 'should be a link on the build time for a successful recent build' |
| self.assertEqual(len(links), 1, msg) |
| |
| # test recent builds area for failed build |
| element = self._get_build_time_element(build2) |
| links = element.find_elements(By.CSS_SELECTOR, 'a') |
| msg = 'should not be a link on the build time for a failed recent build' |
| self.assertEqual(len(links), 0, msg) |
| |
| # test the time column for successful build |
| build1_row = self._get_row_for_build(build1) |
| links = build1_row.find_elements(By.CSS_SELECTOR, 'td.time a') |
| msg = 'should be a link on the build time for a successful build' |
| self.assertEqual(len(links), 1, msg) |
| |
| # test the time column for failed build |
| build2_row = self._get_row_for_build(build2) |
| links = build2_row.find_elements(By.CSS_SELECTOR, 'td.time a') |
| msg = 'should not be a link on the build time for a failed build' |
| self.assertEqual(len(links), 0, msg) |
| |
| def test_builds_table_search_box(self): |
| """ Test the search box in the builds table on the all builds page """ |
| self._get_create_builds() |
| |
| url = reverse('all-builds') |
| self.get(url) |
| |
| # Check search box is present and works |
| self.wait_until_visible('#allbuildstable tbody tr') |
| search_box = self.find('#search-input-allbuildstable') |
| self.assertTrue(search_box.is_displayed()) |
| |
| # Check that we can search for a build by recipe name |
| search_box.send_keys('foo') |
| search_btn = self.find('#search-submit-allbuildstable') |
| search_btn.click() |
| self.wait_until_visible('#allbuildstable tbody tr') |
| rows = self.find_all('#allbuildstable tbody tr') |
| self.assertTrue(len(rows) >= 1) |
| |
| def test_filtering_on_failure_tasks_column(self): |
| """ Test the filtering on failure tasks column in the builds table on the all builds page """ |
| def _check_if_filter_failed_tasks_column_is_visible(): |
| # check if failed tasks filter column is visible, if not click on it |
| # Check edit column |
| edit_column = self.find('#edit-columns-button') |
| self.assertTrue(edit_column.is_displayed()) |
| edit_column.click() |
| # Check dropdown is visible |
| self.wait_until_visible('ul.dropdown-menu.editcol') |
| filter_fails_task_checkbox = self.find('#checkbox-failed_tasks') |
| if not filter_fails_task_checkbox.is_selected(): |
| filter_fails_task_checkbox.click() |
| edit_column.click() |
| |
| self._get_create_builds(success=10, failure=10) |
| |
| url = reverse('all-builds') |
| self.get(url) |
| |
| # Check filtering on failure tasks column |
| self.wait_until_visible('#allbuildstable tbody tr') |
| _check_if_filter_failed_tasks_column_is_visible() |
| failed_tasks_filter = self.find('#failed_tasks_filter') |
| failed_tasks_filter.click() |
| # Check popup is visible |
| self.wait_until_visible('#filter-modal-allbuildstable') |
| self.assertTrue( |
| self.find('#filter-modal-allbuildstable').is_displayed()) |
| # Check that we can filter by failure tasks |
| build_without_failure_tasks = self.find( |
| '#failed_tasks_filter\\:without_failed_tasks') |
| build_without_failure_tasks.click() |
| # click on apply button |
| self.find('#filter-modal-allbuildstable .btn-primary').click() |
| self.wait_until_visible('#allbuildstable tbody tr') |
| # Check if filter is applied, by checking if failed_tasks_filter has btn-primary class |
| self.assertTrue(self.find('#failed_tasks_filter').get_attribute( |
| 'class').find('btn-primary') != -1) |
| |
| def test_filtering_on_completedOn_column(self): |
| """ Test the filtering on completed_on column in the builds table on the all builds page """ |
| self._get_create_builds(success=10, failure=10) |
| |
| url = reverse('all-builds') |
| self.get(url) |
| |
| # Check filtering on failure tasks column |
| self.wait_until_visible('#allbuildstable tbody tr') |
| completed_on_filter = self.find('#completed_on_filter') |
| completed_on_filter.click() |
| # Check popup is visible |
| self.wait_until_visible('#filter-modal-allbuildstable') |
| self.assertTrue( |
| self.find('#filter-modal-allbuildstable').is_displayed()) |
| # Check that we can filter by failure tasks |
| build_without_failure_tasks = self.find( |
| '#completed_on_filter\\:date_range') |
| build_without_failure_tasks.click() |
| # click on apply button |
| self.find('#filter-modal-allbuildstable .btn-primary').click() |
| self.wait_until_visible('#allbuildstable tbody tr') |
| # Check if filter is applied, by checking if completed_on_filter has btn-primary class |
| self.assertTrue(self.find('#completed_on_filter').get_attribute( |
| 'class').find('btn-primary') != -1) |
| |
| # Filter by date range |
| self.find('#completed_on_filter').click() |
| self.wait_until_visible('#filter-modal-allbuildstable') |
| date_ranges = self.driver.find_elements( |
| By.XPATH, '//input[@class="form-control hasDatepicker"]') |
| today = timezone.now() |
| yestersday = today - timezone.timedelta(days=1) |
| date_ranges[0].send_keys(yestersday.strftime('%Y-%m-%d')) |
| date_ranges[1].send_keys(today.strftime('%Y-%m-%d')) |
| self.find('#filter-modal-allbuildstable .btn-primary').click() |
| self.wait_until_visible('#allbuildstable tbody tr') |
| self.assertTrue(self.find('#completed_on_filter').get_attribute( |
| 'class').find('btn-primary') != -1) |
| # Check if filter is applied, number of builds displayed should be 6 |
| self.assertTrue(len(self.find_all('#allbuildstable tbody tr')) >= 4) |
| |
| def test_builds_table_editColumn(self): |
| """ Test the edit column feature in the builds table on the all builds page """ |
| self._get_create_builds(success=10, failure=10) |
| |
| def test_edit_column(check_box_id): |
| # Check that we can hide/show table column |
| check_box = self.find(f'#{check_box_id}') |
| th_class = str(check_box_id).replace('checkbox-', '') |
| if check_box.is_selected(): |
| # check if column is visible in table |
| self.assertTrue( |
| self.find( |
| f'#allbuildstable thead th.{th_class}' |
| ).is_displayed(), |
| f"The {th_class} column is checked in EditColumn dropdown, but it's not visible in table" |
| ) |
| check_box.click() |
| # check if column is hidden in table |
| self.assertFalse( |
| self.find( |
| f'#allbuildstable thead th.{th_class}' |
| ).is_displayed(), |
| f"The {th_class} column is unchecked in EditColumn dropdown, but it's visible in table" |
| ) |
| else: |
| # check if column is hidden in table |
| self.assertFalse( |
| self.find( |
| f'#allbuildstable thead th.{th_class}' |
| ).is_displayed(), |
| f"The {th_class} column is unchecked in EditColumn dropdown, but it's visible in table" |
| ) |
| check_box.click() |
| # check if column is visible in table |
| self.assertTrue( |
| self.find( |
| f'#allbuildstable thead th.{th_class}' |
| ).is_displayed(), |
| f"The {th_class} column is checked in EditColumn dropdown, but it's not visible in table" |
| ) |
| url = reverse('all-builds') |
| self.get(url) |
| self.wait_until_visible('#allbuildstable tbody tr') |
| |
| # Check edit column |
| edit_column = self.find('#edit-columns-button') |
| self.assertTrue(edit_column.is_displayed()) |
| edit_column.click() |
| # Check dropdown is visible |
| self.wait_until_visible('ul.dropdown-menu.editcol') |
| |
| # Check that we can hide the edit column |
| test_edit_column('checkbox-errors_no') |
| test_edit_column('checkbox-failed_tasks') |
| test_edit_column('checkbox-image_files') |
| test_edit_column('checkbox-project') |
| test_edit_column('checkbox-started_on') |
| test_edit_column('checkbox-time') |
| test_edit_column('checkbox-warnings_no') |
| |
| def test_builds_table_show_rows(self): |
| """ Test the show rows feature in the builds table on the all builds page """ |
| self._get_create_builds(success=100, failure=100) |
| |
| def test_show_rows(row_to_show, show_row_link): |
| # Check that we can show rows == row_to_show |
| show_row_link.select_by_value(str(row_to_show)) |
| self.wait_until_visible('#allbuildstable tbody tr', poll=3) |
| # check at least some rows are visible |
| self.assertTrue( |
| len(self.find_all('#allbuildstable tbody tr')) > 0 |
| ) |
| |
| url = reverse('all-builds') |
| self.get(url) |
| self.wait_until_visible('#allbuildstable tbody tr') |
| |
| show_rows = self.driver.find_elements( |
| By.XPATH, |
| '//select[@class="form-control pagesize-allbuildstable"]' |
| ) |
| # Check show rows |
| for show_row_link in show_rows: |
| show_row_link = Select(show_row_link) |
| test_show_rows(10, show_row_link) |
| test_show_rows(25, show_row_link) |
| test_show_rows(50, show_row_link) |
| test_show_rows(100, show_row_link) |
| test_show_rows(150, show_row_link) |