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 12 13from tests.browser.selenium_helpers import SeleniumTestCase 14 15from orm.models import Project, Release, BitbakeVersion, Build, LogMessage 16from orm.models import Layer, Layer_Version, Recipe, CustomImageRecipe, Variable 17 18class TestBuildDashboardPage(SeleniumTestCase): 19 """ Tests for the build dashboard /build/X """ 20 21 def setUp(self): 22 bbv = BitbakeVersion.objects.create(name='bbv1', giturl='/tmp/', 23 branch='master', dirpath="") 24 release = Release.objects.create(name='release1', 25 bitbake_version=bbv) 26 project = Project.objects.create_project(name='test project', 27 release=release) 28 29 now = timezone.now() 30 31 self.build1 = Build.objects.create(project=project, 32 started_on=now, 33 completed_on=now, 34 outcome=Build.SUCCEEDED) 35 36 self.build2 = Build.objects.create(project=project, 37 started_on=now, 38 completed_on=now, 39 outcome=Build.SUCCEEDED) 40 41 self.build3 = Build.objects.create(project=project, 42 started_on=now, 43 completed_on=now, 44 outcome=Build.FAILED) 45 46 # add Variable objects to the successful builds, as this is the criterion 47 # used to determine whether the left-hand panel should be displayed 48 Variable.objects.create(build=self.build1, 49 variable_name='Foo', 50 variable_value='Bar') 51 Variable.objects.create(build=self.build2, 52 variable_name='Foo', 53 variable_value='Bar') 54 55 # exception 56 msg1 = 'an exception was thrown' 57 self.exception_message = LogMessage.objects.create( 58 build=self.build1, 59 level=LogMessage.EXCEPTION, 60 message=msg1 61 ) 62 63 # critical 64 msg2 = 'a critical error occurred' 65 self.critical_message = LogMessage.objects.create( 66 build=self.build1, 67 level=LogMessage.CRITICAL, 68 message=msg2 69 ) 70 71 # error on the failed build 72 msg3 = 'an error occurred' 73 self.error_message = LogMessage.objects.create( 74 build=self.build3, 75 level=LogMessage.ERROR, 76 message=msg3 77 ) 78 79 # warning on the failed build 80 msg4 = 'DANGER WILL ROBINSON' 81 self.warning_message = LogMessage.objects.create( 82 build=self.build3, 83 level=LogMessage.WARNING, 84 message=msg4 85 ) 86 87 # recipes related to the build, for testing the edit custom image/new 88 # custom image buttons 89 layer = Layer.objects.create(name='alayer') 90 layer_version = Layer_Version.objects.create( 91 layer=layer, build=self.build1 92 ) 93 94 # non-image recipes related to a build, for testing the new custom 95 # image button 96 layer_version2 = Layer_Version.objects.create(layer=layer, 97 build=self.build3) 98 99 # image recipes 100 self.image_recipe1 = Recipe.objects.create( 101 name='recipeA', 102 layer_version=layer_version, 103 file_path='/foo/recipeA.bb', 104 is_image=True 105 ) 106 self.image_recipe2 = Recipe.objects.create( 107 name='recipeB', 108 layer_version=layer_version, 109 file_path='/foo/recipeB.bb', 110 is_image=True 111 ) 112 113 # custom image recipes for this project 114 self.custom_image_recipe1 = CustomImageRecipe.objects.create( 115 name='customRecipeY', 116 project=project, 117 layer_version=layer_version, 118 file_path='/foo/customRecipeY.bb', 119 base_recipe=self.image_recipe1, 120 is_image=True 121 ) 122 self.custom_image_recipe2 = CustomImageRecipe.objects.create( 123 name='customRecipeZ', 124 project=project, 125 layer_version=layer_version, 126 file_path='/foo/customRecipeZ.bb', 127 base_recipe=self.image_recipe2, 128 is_image=True 129 ) 130 131 # custom image recipe for a different project (to test filtering 132 # of image recipes and custom image recipes is correct: this shouldn't 133 # show up in either query against self.build1) 134 self.custom_image_recipe3 = CustomImageRecipe.objects.create( 135 name='customRecipeOmega', 136 project=Project.objects.create(name='baz', release=release), 137 layer_version=Layer_Version.objects.create( 138 layer=layer, build=self.build2 139 ), 140 file_path='/foo/customRecipeOmega.bb', 141 base_recipe=self.image_recipe2, 142 is_image=True 143 ) 144 145 # another non-image recipe (to test filtering of image recipes and 146 # custom image recipes is correct: this shouldn't show up in either 147 # for any build) 148 self.non_image_recipe = Recipe.objects.create( 149 name='nonImageRecipe', 150 layer_version=layer_version, 151 file_path='/foo/nonImageRecipe.bb', 152 is_image=False 153 ) 154 155 def _get_build_dashboard(self, build): 156 """ 157 Navigate to the build dashboard for build 158 """ 159 url = reverse('builddashboard', args=(build.id,)) 160 self.get(url) 161 162 def _get_build_dashboard_errors(self, build): 163 """ 164 Get a list of HTML fragments representing the errors on the 165 dashboard for the Build object build 166 """ 167 self._get_build_dashboard(build) 168 return self.find_all('#errors div.alert-danger') 169 170 def _check_for_log_message(self, message_elements, log_message): 171 """ 172 Check that the LogMessage <log_message> has a representation in 173 the HTML elements <message_elements>. 174 175 message_elements: WebElements representing the log messages shown 176 in the build dashboard; each should have a <pre> element inside 177 it with a data-log-message-id attribute 178 179 log_message: orm.models.LogMessage instance 180 """ 181 expected_text = log_message.message 182 expected_pk = str(log_message.pk) 183 184 found = False 185 for element in message_elements: 186 log_message_text = element.find_element_by_tag_name('pre').text.strip() 187 text_matches = (log_message_text == expected_text) 188 189 log_message_pk = element.get_attribute('data-log-message-id') 190 id_matches = (log_message_pk == expected_pk) 191 192 if text_matches and id_matches: 193 found = True 194 break 195 196 template_vars = (expected_text, expected_pk) 197 assertion_failed_msg = 'message not found: ' \ 198 'expected text "%s" and ID %s' % template_vars 199 self.assertTrue(found, assertion_failed_msg) 200 201 def _check_for_error_message(self, build, log_message): 202 """ 203 Check whether the LogMessage instance <log_message> is 204 represented as an HTML error in the dashboard page for the Build object 205 build 206 """ 207 errors = self._get_build_dashboard_errors(build) 208 self._check_for_log_message(errors, log_message) 209 210 def _check_labels_in_modal(self, modal, expected): 211 """ 212 Check that the text values of the <label> elements inside 213 the WebElement modal match the list of text values in expected 214 """ 215 # labels containing the radio buttons we're testing for 216 labels = modal.find_elements_by_css_selector(".radio") 217 218 labels_text = [lab.text for lab in labels] 219 self.assertEqual(len(labels_text), len(expected)) 220 221 for expected_text in expected: 222 self.assertTrue(expected_text in labels_text, 223 "Could not find %s in %s" % (expected_text, 224 labels_text)) 225 226 def test_exceptions_show_as_errors(self): 227 """ 228 LogMessages with level EXCEPTION should display in the errors 229 section of the page 230 """ 231 self._check_for_error_message(self.build1, self.exception_message) 232 233 def test_criticals_show_as_errors(self): 234 """ 235 LogMessages with level CRITICAL should display in the errors 236 section of the page 237 """ 238 self._check_for_error_message(self.build1, self.critical_message) 239 240 def test_edit_custom_image_button(self): 241 """ 242 A build which built two custom images should present a modal which lets 243 the user choose one of them to edit 244 """ 245 self._get_build_dashboard(self.build1) 246 247 # click the "edit custom image" button, which populates the modal 248 selector = '[data-role="edit-custom-image-trigger"]' 249 self.click(selector) 250 251 modal = self.driver.find_element_by_id('edit-custom-image-modal') 252 self.wait_until_visible("#edit-custom-image-modal") 253 254 # recipes we expect to see in the edit custom image modal 255 expected_recipes = [ 256 self.custom_image_recipe1.name, 257 self.custom_image_recipe2.name 258 ] 259 260 self._check_labels_in_modal(modal, expected_recipes) 261 262 def test_new_custom_image_button(self): 263 """ 264 Check that a build with multiple images and custom images presents 265 all of them as options for creating a new custom image from 266 """ 267 self._get_build_dashboard(self.build1) 268 269 # click the "new custom image" button, which populates the modal 270 selector = '[data-role="new-custom-image-trigger"]' 271 self.click(selector) 272 273 modal = self.driver.find_element_by_id('new-custom-image-modal') 274 self.wait_until_visible("#new-custom-image-modal") 275 276 # recipes we expect to see in the new custom image modal 277 expected_recipes = [ 278 self.image_recipe1.name, 279 self.image_recipe2.name, 280 self.custom_image_recipe1.name, 281 self.custom_image_recipe2.name 282 ] 283 284 self._check_labels_in_modal(modal, expected_recipes) 285 286 def test_new_custom_image_button_no_image(self): 287 """ 288 Check that a build which builds non-image recipes doesn't show 289 the new custom image button on the dashboard. 290 """ 291 self._get_build_dashboard(self.build3) 292 selector = '[data-role="new-custom-image-trigger"]' 293 self.assertFalse(self.element_exists(selector), 294 'new custom image button should not show for builds which ' \ 295 'don\'t have any image recipes') 296 297 def test_left_panel(self): 298 """" 299 Builds which succeed should have a left panel and a build summary 300 """ 301 self._get_build_dashboard(self.build1) 302 303 left_panel = self.find_all('#nav') 304 self.assertEqual(len(left_panel), 1) 305 306 build_summary = self.find_all('[data-role="build-summary-heading"]') 307 self.assertEqual(len(build_summary), 1) 308 309 def test_failed_no_left_panel(self): 310 """ 311 Builds which fail should have no left panel and no build summary 312 """ 313 self._get_build_dashboard(self.build3) 314 315 left_panel = self.find_all('#nav') 316 self.assertEqual(len(left_panel), 0) 317 318 build_summary = self.find_all('[data-role="build-summary-heading"]') 319 self.assertEqual(len(build_summary), 0) 320 321 def test_failed_shows_errors_and_warnings(self): 322 """ 323 Failed builds should still show error and warning messages 324 """ 325 self._get_build_dashboard(self.build3) 326 327 errors = self.find_all('#errors div.alert-danger') 328 self._check_for_log_message(errors, self.error_message) 329 330 # expand the warnings area 331 self.click('#warning-toggle') 332 self.wait_until_visible('#warnings div.alert-warning') 333 334 warnings = self.find_all('#warnings div.alert-warning') 335 self._check_for_log_message(warnings, self.warning_message) 336