xref: /OK3568_Linux_fs/yocto/poky/bitbake/lib/toaster/tests/browser/test_builddashboard_page.py (revision 4882a59341e53eb6f0b4789bf948001014eff981)
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