blob: d972aff1bd9ed8a499ea84687884db2cf8d87ab8 [file] [log] [blame]
Brad Bishop96ff1982019-08-19 13:50:42 -04001#! /usr/bin/env python3
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05002#
3# BitBake Toaster Implementation
4#
5# Copyright (C) 2013-2016 Intel Corporation
6#
Brad Bishopc342db32019-05-15 21:57:59 -04007# SPDX-License-Identifier: GPL-2.0-only
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05008#
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05009
10from django.core.urlresolvers import reverse
11from django.utils import timezone
12
Patrick Williamsc0f7c042017-02-23 20:41:17 -060013from tests.browser.selenium_helpers import SeleniumTestCase
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050014
15from orm.models import Project, Release, BitbakeVersion, Build, LogMessage
Patrick Williamsc0f7c042017-02-23 20:41:17 -060016from orm.models import Layer, Layer_Version, Recipe, CustomImageRecipe, Variable
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050017
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,
Patrick Williamsc0f7c042017-02-23 20:41:17 -060033 completed_on=now,
34 outcome=Build.SUCCEEDED)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050035
36 self.build2 = Build.objects.create(project=project,
37 started_on=now,
Patrick Williamsc0f7c042017-02-23 20:41:17 -060038 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')
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050054
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
Patrick Williamsc0f7c042017-02-23 20:41:17 -060071 # 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
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050087 # 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
Patrick Williamsc0f7c042017-02-23 20:41:17 -060094 # 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
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050099 # 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)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600168 return self.find_all('#errors div.alert-danger')
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500169
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600170 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):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500202 """
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)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600208 self._check_for_log_message(errors, log_message)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500209
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
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600216 labels = modal.find_elements_by_css_selector(".radio")
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500217
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600218 labels_text = [lab.text for lab in labels]
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500219 self.assertEqual(len(labels_text), len(expected))
220
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600221 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))
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500225
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 """
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600231 self._check_for_error_message(self.build1, self.exception_message)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500232
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 """
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600238 self._check_for_error_message(self.build1, self.critical_message)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500239
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)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600246
247 # click the "edit custom image" button, which populates the modal
248 selector = '[data-role="edit-custom-image-trigger"]'
249 self.click(selector)
250
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500251 modal = self.driver.find_element_by_id('edit-custom-image-modal')
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600252 self.wait_until_visible("#edit-custom-image-modal")
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500253
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
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600270 selector = '[data-role="new-custom-image-trigger"]'
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500271 self.click(selector)
272
273 modal = self.driver.find_element_by_id('new-custom-image-modal')
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600274 self.wait_until_visible("#new-custom-image-modal")
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500275
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)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600285
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)