| #! /usr/bin/env python3 |
| # |
| # BitBake Toaster Implementation |
| # |
| # Copyright (C) 2013-2016 Intel Corporation |
| # |
| # SPDX-License-Identifier: GPL-2.0-only |
| # |
| |
| from django.core.urlresolvers import reverse |
| from django.utils import timezone |
| |
| from tests.browser.selenium_helpers import SeleniumTestCase |
| |
| from orm.models import Project, Release, BitbakeVersion, Build, LogMessage |
| from orm.models import Layer, Layer_Version, Recipe, CustomImageRecipe, Variable |
| |
| class TestBuildDashboardPage(SeleniumTestCase): |
| """ Tests for the build dashboard /build/X """ |
| |
| def setUp(self): |
| bbv = BitbakeVersion.objects.create(name='bbv1', giturl='/tmp/', |
| branch='master', dirpath="") |
| release = Release.objects.create(name='release1', |
| bitbake_version=bbv) |
| project = Project.objects.create_project(name='test project', |
| release=release) |
| |
| now = timezone.now() |
| |
| self.build1 = Build.objects.create(project=project, |
| started_on=now, |
| completed_on=now, |
| outcome=Build.SUCCEEDED) |
| |
| self.build2 = Build.objects.create(project=project, |
| started_on=now, |
| completed_on=now, |
| outcome=Build.SUCCEEDED) |
| |
| self.build3 = Build.objects.create(project=project, |
| started_on=now, |
| completed_on=now, |
| outcome=Build.FAILED) |
| |
| # add Variable objects to the successful builds, as this is the criterion |
| # used to determine whether the left-hand panel should be displayed |
| Variable.objects.create(build=self.build1, |
| variable_name='Foo', |
| variable_value='Bar') |
| Variable.objects.create(build=self.build2, |
| variable_name='Foo', |
| variable_value='Bar') |
| |
| # exception |
| msg1 = 'an exception was thrown' |
| self.exception_message = LogMessage.objects.create( |
| build=self.build1, |
| level=LogMessage.EXCEPTION, |
| message=msg1 |
| ) |
| |
| # critical |
| msg2 = 'a critical error occurred' |
| self.critical_message = LogMessage.objects.create( |
| build=self.build1, |
| level=LogMessage.CRITICAL, |
| message=msg2 |
| ) |
| |
| # error on the failed build |
| msg3 = 'an error occurred' |
| self.error_message = LogMessage.objects.create( |
| build=self.build3, |
| level=LogMessage.ERROR, |
| message=msg3 |
| ) |
| |
| # warning on the failed build |
| msg4 = 'DANGER WILL ROBINSON' |
| self.warning_message = LogMessage.objects.create( |
| build=self.build3, |
| level=LogMessage.WARNING, |
| message=msg4 |
| ) |
| |
| # recipes related to the build, for testing the edit custom image/new |
| # custom image buttons |
| layer = Layer.objects.create(name='alayer') |
| layer_version = Layer_Version.objects.create( |
| layer=layer, build=self.build1 |
| ) |
| |
| # non-image recipes related to a build, for testing the new custom |
| # image button |
| layer_version2 = Layer_Version.objects.create(layer=layer, |
| build=self.build3) |
| |
| # image recipes |
| self.image_recipe1 = Recipe.objects.create( |
| name='recipeA', |
| layer_version=layer_version, |
| file_path='/foo/recipeA.bb', |
| is_image=True |
| ) |
| self.image_recipe2 = Recipe.objects.create( |
| name='recipeB', |
| layer_version=layer_version, |
| file_path='/foo/recipeB.bb', |
| is_image=True |
| ) |
| |
| # custom image recipes for this project |
| self.custom_image_recipe1 = CustomImageRecipe.objects.create( |
| name='customRecipeY', |
| project=project, |
| layer_version=layer_version, |
| file_path='/foo/customRecipeY.bb', |
| base_recipe=self.image_recipe1, |
| is_image=True |
| ) |
| self.custom_image_recipe2 = CustomImageRecipe.objects.create( |
| name='customRecipeZ', |
| project=project, |
| layer_version=layer_version, |
| file_path='/foo/customRecipeZ.bb', |
| base_recipe=self.image_recipe2, |
| is_image=True |
| ) |
| |
| # custom image recipe for a different project (to test filtering |
| # of image recipes and custom image recipes is correct: this shouldn't |
| # show up in either query against self.build1) |
| self.custom_image_recipe3 = CustomImageRecipe.objects.create( |
| name='customRecipeOmega', |
| project=Project.objects.create(name='baz', release=release), |
| layer_version=Layer_Version.objects.create( |
| layer=layer, build=self.build2 |
| ), |
| file_path='/foo/customRecipeOmega.bb', |
| base_recipe=self.image_recipe2, |
| is_image=True |
| ) |
| |
| # another non-image recipe (to test filtering of image recipes and |
| # custom image recipes is correct: this shouldn't show up in either |
| # for any build) |
| self.non_image_recipe = Recipe.objects.create( |
| name='nonImageRecipe', |
| layer_version=layer_version, |
| file_path='/foo/nonImageRecipe.bb', |
| is_image=False |
| ) |
| |
| def _get_build_dashboard(self, build): |
| """ |
| Navigate to the build dashboard for build |
| """ |
| url = reverse('builddashboard', args=(build.id,)) |
| self.get(url) |
| |
| def _get_build_dashboard_errors(self, build): |
| """ |
| Get a list of HTML fragments representing the errors on the |
| dashboard for the Build object build |
| """ |
| self._get_build_dashboard(build) |
| return self.find_all('#errors div.alert-danger') |
| |
| def _check_for_log_message(self, message_elements, log_message): |
| """ |
| Check that the LogMessage <log_message> has a representation in |
| the HTML elements <message_elements>. |
| |
| message_elements: WebElements representing the log messages shown |
| in the build dashboard; each should have a <pre> element inside |
| it with a data-log-message-id attribute |
| |
| log_message: orm.models.LogMessage instance |
| """ |
| expected_text = log_message.message |
| expected_pk = str(log_message.pk) |
| |
| found = False |
| for element in message_elements: |
| log_message_text = element.find_element_by_tag_name('pre').text.strip() |
| text_matches = (log_message_text == expected_text) |
| |
| log_message_pk = element.get_attribute('data-log-message-id') |
| id_matches = (log_message_pk == expected_pk) |
| |
| if text_matches and id_matches: |
| found = True |
| break |
| |
| template_vars = (expected_text, expected_pk) |
| assertion_failed_msg = 'message not found: ' \ |
| 'expected text "%s" and ID %s' % template_vars |
| self.assertTrue(found, assertion_failed_msg) |
| |
| def _check_for_error_message(self, build, log_message): |
| """ |
| Check whether the LogMessage instance <log_message> is |
| represented as an HTML error in the dashboard page for the Build object |
| build |
| """ |
| errors = self._get_build_dashboard_errors(build) |
| self._check_for_log_message(errors, log_message) |
| |
| def _check_labels_in_modal(self, modal, expected): |
| """ |
| Check that the text values of the <label> elements inside |
| the WebElement modal match the list of text values in expected |
| """ |
| # labels containing the radio buttons we're testing for |
| labels = modal.find_elements_by_css_selector(".radio") |
| |
| labels_text = [lab.text for lab in labels] |
| self.assertEqual(len(labels_text), len(expected)) |
| |
| for expected_text in expected: |
| self.assertTrue(expected_text in labels_text, |
| "Could not find %s in %s" % (expected_text, |
| labels_text)) |
| |
| def test_exceptions_show_as_errors(self): |
| """ |
| LogMessages with level EXCEPTION should display in the errors |
| section of the page |
| """ |
| self._check_for_error_message(self.build1, self.exception_message) |
| |
| def test_criticals_show_as_errors(self): |
| """ |
| LogMessages with level CRITICAL should display in the errors |
| section of the page |
| """ |
| self._check_for_error_message(self.build1, self.critical_message) |
| |
| def test_edit_custom_image_button(self): |
| """ |
| A build which built two custom images should present a modal which lets |
| the user choose one of them to edit |
| """ |
| self._get_build_dashboard(self.build1) |
| |
| # click the "edit custom image" button, which populates the modal |
| selector = '[data-role="edit-custom-image-trigger"]' |
| self.click(selector) |
| |
| modal = self.driver.find_element_by_id('edit-custom-image-modal') |
| self.wait_until_visible("#edit-custom-image-modal") |
| |
| # recipes we expect to see in the edit custom image modal |
| expected_recipes = [ |
| self.custom_image_recipe1.name, |
| self.custom_image_recipe2.name |
| ] |
| |
| self._check_labels_in_modal(modal, expected_recipes) |
| |
| def test_new_custom_image_button(self): |
| """ |
| Check that a build with multiple images and custom images presents |
| all of them as options for creating a new custom image from |
| """ |
| self._get_build_dashboard(self.build1) |
| |
| # click the "new custom image" button, which populates the modal |
| selector = '[data-role="new-custom-image-trigger"]' |
| self.click(selector) |
| |
| modal = self.driver.find_element_by_id('new-custom-image-modal') |
| self.wait_until_visible("#new-custom-image-modal") |
| |
| # recipes we expect to see in the new custom image modal |
| expected_recipes = [ |
| self.image_recipe1.name, |
| self.image_recipe2.name, |
| self.custom_image_recipe1.name, |
| self.custom_image_recipe2.name |
| ] |
| |
| self._check_labels_in_modal(modal, expected_recipes) |
| |
| def test_new_custom_image_button_no_image(self): |
| """ |
| Check that a build which builds non-image recipes doesn't show |
| the new custom image button on the dashboard. |
| """ |
| self._get_build_dashboard(self.build3) |
| selector = '[data-role="new-custom-image-trigger"]' |
| self.assertFalse(self.element_exists(selector), |
| 'new custom image button should not show for builds which ' \ |
| 'don\'t have any image recipes') |
| |
| def test_left_panel(self): |
| """" |
| Builds which succeed should have a left panel and a build summary |
| """ |
| self._get_build_dashboard(self.build1) |
| |
| left_panel = self.find_all('#nav') |
| self.assertEqual(len(left_panel), 1) |
| |
| build_summary = self.find_all('[data-role="build-summary-heading"]') |
| self.assertEqual(len(build_summary), 1) |
| |
| def test_failed_no_left_panel(self): |
| """ |
| Builds which fail should have no left panel and no build summary |
| """ |
| self._get_build_dashboard(self.build3) |
| |
| left_panel = self.find_all('#nav') |
| self.assertEqual(len(left_panel), 0) |
| |
| build_summary = self.find_all('[data-role="build-summary-heading"]') |
| self.assertEqual(len(build_summary), 0) |
| |
| def test_failed_shows_errors_and_warnings(self): |
| """ |
| Failed builds should still show error and warning messages |
| """ |
| self._get_build_dashboard(self.build3) |
| |
| errors = self.find_all('#errors div.alert-danger') |
| self._check_for_log_message(errors, self.error_message) |
| |
| # expand the warnings area |
| self.click('#warning-toggle') |
| self.wait_until_visible('#warnings div.alert-warning') |
| |
| warnings = self.find_all('#warnings div.alert-warning') |
| self._check_for_log_message(warnings, self.warning_message) |