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
10import os
11import re
12
13from django.urls import reverse
14from selenium.webdriver.support.select import Select
15from django.utils import timezone
16from bldcontrol.models import BuildRequest
17from tests.browser.selenium_helpers import SeleniumTestCase
18
19from orm.models import BitbakeVersion, Layer, Layer_Version, Recipe, Release, Project, Build, Target, Task
20
21from selenium.webdriver.common.by import By
22
23
24class TestAllBuildsPage(SeleniumTestCase):
25    """ Tests for all builds page /builds/ """
26
27    PROJECT_NAME = 'test project'
28    CLI_BUILDS_PROJECT_NAME = 'command line builds'
29
30    def setUp(self):
31        builldir = os.environ.get('BUILDDIR', './')
32        bbv = BitbakeVersion.objects.create(name='bbv1', giturl=f'{builldir}/',
33                                            branch='master', dirpath='')
34        release = Release.objects.create(name='release1',
35                                         bitbake_version=bbv)
36        self.project1 = Project.objects.create_project(name=self.PROJECT_NAME,
37                                                       release=release)
38        self.default_project = Project.objects.create_project(
39            name=self.CLI_BUILDS_PROJECT_NAME,
40            release=release
41        )
42        self.default_project.is_default = True
43        self.default_project.save()
44
45        # parameters for builds to associate with the projects
46        now = timezone.now()
47
48        self.project1_build_success = {
49            'project': self.project1,
50            'started_on': now,
51            'completed_on': now,
52            'outcome': Build.SUCCEEDED
53        }
54
55        self.project1_build_failure = {
56            'project': self.project1,
57            'started_on': now,
58            'completed_on': now,
59            'outcome': Build.FAILED
60        }
61
62        self.default_project_build_success = {
63            'project': self.default_project,
64            'started_on': now,
65            'completed_on': now,
66            'outcome': Build.SUCCEEDED
67        }
68
69    def _get_build_time_element(self, build):
70        """
71        Return the HTML element containing the build time for a build
72        in the recent builds area
73        """
74        selector = 'div[data-latest-build-result="%s"] ' \
75            '[data-role="data-recent-build-buildtime-field"]' % build.id
76
77        # because this loads via Ajax, wait for it to be visible
78        self.wait_until_visible(selector)
79
80        build_time_spans = self.find_all(selector)
81
82        self.assertEqual(len(build_time_spans), 1)
83
84        return build_time_spans[0]
85
86    def _get_row_for_build(self, build):
87        """ Get the table row for the build from the all builds table """
88        self.wait_until_visible('#allbuildstable')
89
90        rows = self.find_all('#allbuildstable tr')
91
92        # look for the row with a download link on the recipe which matches the
93        # build ID
94        url = reverse('builddashboard', args=(build.id,))
95        selector = 'td.target a[href="%s"]' % url
96
97        found_row = None
98        for row in rows:
99
100            outcome_links = row.find_elements(By.CSS_SELECTOR, selector)
101            if len(outcome_links) == 1:
102                found_row = row
103                break
104
105        self.assertNotEqual(found_row, None)
106
107        return found_row
108
109    def _get_create_builds(self, **kwargs):
110        """ Create a build and return the build object """
111        build1 = Build.objects.create(**self.project1_build_success)
112        build2 = Build.objects.create(**self.project1_build_failure)
113
114        # add some targets to these builds so they have recipe links
115        # (and so we can find the row in the ToasterTable corresponding to
116        # a particular build)
117        Target.objects.create(build=build1, target='foo')
118        Target.objects.create(build=build2, target='bar')
119
120        if kwargs:
121            # Create kwargs.get('success') builds with success status with target
122            # and kwargs.get('failure') builds with failure status with target
123            for i in range(kwargs.get('success', 0)):
124                now = timezone.now()
125                self.project1_build_success['started_on'] = now
126                self.project1_build_success[
127                    'completed_on'] = now - timezone.timedelta(days=i)
128                build = Build.objects.create(**self.project1_build_success)
129                Target.objects.create(build=build,
130                                      target=f'{i}_success_recipe',
131                                      task=f'{i}_success_task')
132
133                self._set_buildRequest_and_task_on_build(build)
134            for i in range(kwargs.get('failure', 0)):
135                now = timezone.now()
136                self.project1_build_failure['started_on'] = now
137                self.project1_build_failure[
138                    'completed_on'] = now - timezone.timedelta(days=i)
139                build = Build.objects.create(**self.project1_build_failure)
140                Target.objects.create(build=build,
141                                      target=f'{i}_fail_recipe',
142                                      task=f'{i}_fail_task')
143                self._set_buildRequest_and_task_on_build(build)
144        return build1, build2
145
146    def _create_recipe(self):
147        """ Add a recipe to the database and return it """
148        layer = Layer.objects.create()
149        layer_version = Layer_Version.objects.create(layer=layer)
150        return Recipe.objects.create(name='recipe_foo', layer_version=layer_version)
151
152    def _set_buildRequest_and_task_on_build(self, build):
153        """ Set buildRequest and task on build """
154        build.recipes_parsed = 1
155        build.save()
156        buildRequest = BuildRequest.objects.create(
157            build=build,
158            project=self.project1,
159            state=BuildRequest.REQ_COMPLETED)
160        build.build_request = buildRequest
161        recipe = self._create_recipe()
162        task = Task.objects.create(build=build,
163                                   recipe=recipe,
164                                   task_name='task',
165                                   outcome=Task.OUTCOME_SUCCESS)
166        task.save()
167        build.save()
168
169    def test_show_tasks_with_suffix(self):
170        """ Task should be shown as suffix on build name """
171        build = Build.objects.create(**self.project1_build_success)
172        target = 'bash'
173        task = 'clean'
174        Target.objects.create(build=build, target=target, task=task)
175
176        url = reverse('all-builds')
177        self.get(url)
178        self.wait_until_visible('td[class="target"]')
179
180        cell = self.find('td[class="target"]')
181        content = cell.get_attribute('innerHTML')
182        expected_text = '%s:%s' % (target, task)
183
184        self.assertTrue(re.search(expected_text, content),
185                        '"target" cell should contain text %s' % expected_text)
186
187    def test_rebuild_buttons(self):
188        """
189        Test 'Rebuild' buttons in recent builds section
190
191        'Rebuild' button should not be shown for command-line builds,
192        but should be shown for other builds
193        """
194        build1 = Build.objects.create(**self.project1_build_success)
195        default_build = Build.objects.create(
196            **self.default_project_build_success)
197
198        url = reverse('all-builds')
199        self.get(url)
200
201        # should see a rebuild button for non-command-line builds
202        self.wait_until_visible('#allbuildstable tbody tr')
203        selector = 'div[data-latest-build-result="%s"] .rebuild-btn' % build1.id
204        run_again_button = self.find_all(selector)
205        self.assertEqual(len(run_again_button), 1,
206                         'should see a rebuild button for non-cli builds')
207
208        # shouldn't see a rebuild button for command-line builds
209        selector = 'div[data-latest-build-result="%s"] .rebuild-btn' % default_build.id
210        run_again_button = self.find_all(selector)
211        self.assertEqual(len(run_again_button), 0,
212                         'should not see a rebuild button for cli builds')
213
214    def test_tooltips_on_project_name(self):
215        """
216        Test tooltips shown next to project name in the main table
217
218        A tooltip should be present next to the command line
219        builds project name in the all builds page, but not for
220        other projects
221        """
222        Build.objects.create(**self.project1_build_success)
223        Build.objects.create(**self.default_project_build_success)
224
225        url = reverse('all-builds')
226        self.get(url)
227        self.wait_until_visible('#allbuildstable', poll=3)
228
229        # get the project name cells from the table
230        cells = self.find_all('#allbuildstable td[class="project"]')
231
232        selector = 'span.get-help'
233
234        for cell in cells:
235            content = cell.get_attribute('innerHTML')
236            help_icons = cell.find_elements(By.CSS_SELECTOR, selector)
237
238            if re.search(self.PROJECT_NAME, content):
239                # no help icon next to non-cli project name
240                msg = 'should not be a help icon for non-cli builds name'
241                self.assertEqual(len(help_icons), 0, msg)
242            elif re.search(self.CLI_BUILDS_PROJECT_NAME, content):
243                # help icon next to cli project name
244                msg = 'should be a help icon for cli builds name'
245                self.assertEqual(len(help_icons), 1, msg)
246            else:
247                msg = 'found unexpected project name cell in all builds table'
248                self.fail(msg)
249
250    def test_builds_time_links(self):
251        """
252        Successful builds should have links on the time column and in the
253        recent builds area; failed builds should not have links on the time column,
254        or in the recent builds area
255        """
256        build1, build2 = self._get_create_builds()
257
258        url = reverse('all-builds')
259        self.get(url)
260        self.wait_until_visible('#allbuildstable', poll=3)
261
262        # test recent builds area for successful build
263        element = self._get_build_time_element(build1)
264        links = element.find_elements(By.CSS_SELECTOR, 'a')
265        msg = 'should be a link on the build time for a successful recent build'
266        self.assertEqual(len(links), 1, msg)
267
268        # test recent builds area for failed build
269        element = self._get_build_time_element(build2)
270        links = element.find_elements(By.CSS_SELECTOR, 'a')
271        msg = 'should not be a link on the build time for a failed recent build'
272        self.assertEqual(len(links), 0, msg)
273
274        # test the time column for successful build
275        build1_row = self._get_row_for_build(build1)
276        links = build1_row.find_elements(By.CSS_SELECTOR, 'td.time a')
277        msg = 'should be a link on the build time for a successful build'
278        self.assertEqual(len(links), 1, msg)
279
280        # test the time column for failed build
281        build2_row = self._get_row_for_build(build2)
282        links = build2_row.find_elements(By.CSS_SELECTOR, 'td.time a')
283        msg = 'should not be a link on the build time for a failed build'
284        self.assertEqual(len(links), 0, msg)
285
286    def test_builds_table_search_box(self):
287        """ Test the search box in the builds table on the all builds page """
288        self._get_create_builds()
289
290        url = reverse('all-builds')
291        self.get(url)
292
293        # Check search box is present and works
294        self.wait_until_visible('#allbuildstable tbody tr')
295        search_box = self.find('#search-input-allbuildstable')
296        self.assertTrue(search_box.is_displayed())
297
298        # Check that we can search for a build by recipe name
299        search_box.send_keys('foo')
300        search_btn = self.find('#search-submit-allbuildstable')
301        search_btn.click()
302        self.wait_until_visible('#allbuildstable tbody tr')
303        rows = self.find_all('#allbuildstable tbody tr')
304        self.assertTrue(len(rows) >= 1)
305
306    def test_filtering_on_failure_tasks_column(self):
307        """ Test the filtering on failure tasks column in the builds table on the all builds page """
308        def _check_if_filter_failed_tasks_column_is_visible():
309            # check if failed tasks filter column is visible, if not click on it
310            # Check edit column
311            edit_column = self.find('#edit-columns-button')
312            self.assertTrue(edit_column.is_displayed())
313            edit_column.click()
314            # Check dropdown is visible
315            self.wait_until_visible('ul.dropdown-menu.editcol')
316            filter_fails_task_checkbox = self.find('#checkbox-failed_tasks')
317            if not filter_fails_task_checkbox.is_selected():
318                filter_fails_task_checkbox.click()
319            edit_column.click()
320
321        self._get_create_builds(success=10, failure=10)
322
323        url = reverse('all-builds')
324        self.get(url)
325
326        # Check filtering on failure tasks column
327        self.wait_until_visible('#allbuildstable tbody tr')
328        _check_if_filter_failed_tasks_column_is_visible()
329        failed_tasks_filter = self.find('#failed_tasks_filter')
330        failed_tasks_filter.click()
331        # Check popup is visible
332        self.wait_until_visible('#filter-modal-allbuildstable')
333        self.assertTrue(
334            self.find('#filter-modal-allbuildstable').is_displayed())
335        # Check that we can filter by failure tasks
336        build_without_failure_tasks = self.find(
337            '#failed_tasks_filter\\:without_failed_tasks')
338        build_without_failure_tasks.click()
339        # click on apply button
340        self.find('#filter-modal-allbuildstable .btn-primary').click()
341        self.wait_until_visible('#allbuildstable tbody tr')
342        # Check if filter is applied, by checking if failed_tasks_filter has btn-primary class
343        self.assertTrue(self.find('#failed_tasks_filter').get_attribute(
344            'class').find('btn-primary') != -1)
345
346    def test_filtering_on_completedOn_column(self):
347        """ Test the filtering on completed_on column in the builds table on the all builds page """
348        self._get_create_builds(success=10, failure=10)
349
350        url = reverse('all-builds')
351        self.get(url)
352
353        # Check filtering on failure tasks column
354        self.wait_until_visible('#allbuildstable tbody tr')
355        completed_on_filter = self.find('#completed_on_filter')
356        completed_on_filter.click()
357        # Check popup is visible
358        self.wait_until_visible('#filter-modal-allbuildstable')
359        self.assertTrue(
360            self.find('#filter-modal-allbuildstable').is_displayed())
361        # Check that we can filter by failure tasks
362        build_without_failure_tasks = self.find(
363            '#completed_on_filter\\:date_range')
364        build_without_failure_tasks.click()
365        # click on apply button
366        self.find('#filter-modal-allbuildstable .btn-primary').click()
367        self.wait_until_visible('#allbuildstable tbody tr')
368        # Check if filter is applied, by checking if completed_on_filter has btn-primary class
369        self.assertTrue(self.find('#completed_on_filter').get_attribute(
370            'class').find('btn-primary') != -1)
371
372        # Filter by date range
373        self.find('#completed_on_filter').click()
374        self.wait_until_visible('#filter-modal-allbuildstable')
375        date_ranges = self.driver.find_elements(
376            By.XPATH, '//input[@class="form-control hasDatepicker"]')
377        today = timezone.now()
378        yestersday = today - timezone.timedelta(days=1)
379        date_ranges[0].send_keys(yestersday.strftime('%Y-%m-%d'))
380        date_ranges[1].send_keys(today.strftime('%Y-%m-%d'))
381        self.find('#filter-modal-allbuildstable .btn-primary').click()
382        self.wait_until_visible('#allbuildstable tbody tr')
383        self.assertTrue(self.find('#completed_on_filter').get_attribute(
384            'class').find('btn-primary') != -1)
385        # Check if filter is applied, number of builds displayed should be 6
386        self.assertTrue(len(self.find_all('#allbuildstable tbody tr')) >= 4)
387
388    def test_builds_table_editColumn(self):
389        """ Test the edit column feature in the builds table on the all builds page """
390        self._get_create_builds(success=10, failure=10)
391
392        def test_edit_column(check_box_id):
393            # Check that we can hide/show table column
394            check_box = self.find(f'#{check_box_id}')
395            th_class = str(check_box_id).replace('checkbox-', '')
396            if check_box.is_selected():
397                # check if column is visible in table
398                self.assertTrue(
399                    self.find(
400                        f'#allbuildstable thead th.{th_class}'
401                    ).is_displayed(),
402                    f"The {th_class} column is checked in EditColumn dropdown, but it's not visible in table"
403                )
404                check_box.click()
405                # check if column is hidden in table
406                self.assertFalse(
407                    self.find(
408                        f'#allbuildstable thead th.{th_class}'
409                    ).is_displayed(),
410                    f"The {th_class} column is unchecked in EditColumn dropdown, but it's visible in table"
411                )
412            else:
413                # check if column is hidden in table
414                self.assertFalse(
415                    self.find(
416                        f'#allbuildstable thead th.{th_class}'
417                    ).is_displayed(),
418                    f"The {th_class} column is unchecked in EditColumn dropdown, but it's visible in table"
419                )
420                check_box.click()
421                # check if column is visible in table
422                self.assertTrue(
423                    self.find(
424                        f'#allbuildstable thead th.{th_class}'
425                    ).is_displayed(),
426                    f"The {th_class} column is checked in EditColumn dropdown, but it's not visible in table"
427                )
428        url = reverse('all-builds')
429        self.get(url)
430        self.wait_until_visible('#allbuildstable tbody tr')
431
432        # Check edit column
433        edit_column = self.find('#edit-columns-button')
434        self.assertTrue(edit_column.is_displayed())
435        edit_column.click()
436        # Check dropdown is visible
437        self.wait_until_visible('ul.dropdown-menu.editcol')
438
439        # Check that we can hide the edit column
440        test_edit_column('checkbox-errors_no')
441        test_edit_column('checkbox-failed_tasks')
442        test_edit_column('checkbox-image_files')
443        test_edit_column('checkbox-project')
444        test_edit_column('checkbox-started_on')
445        test_edit_column('checkbox-time')
446        test_edit_column('checkbox-warnings_no')
447
448    def test_builds_table_show_rows(self):
449        """ Test the show rows feature in the builds table on the all builds page """
450        self._get_create_builds(success=100, failure=100)
451
452        def test_show_rows(row_to_show, show_row_link):
453            # Check that we can show rows == row_to_show
454            show_row_link.select_by_value(str(row_to_show))
455            self.wait_until_visible('#allbuildstable tbody tr', poll=3)
456            # check at least some rows are visible
457            self.assertTrue(
458                len(self.find_all('#allbuildstable tbody tr')) > 0
459            )
460
461        url = reverse('all-builds')
462        self.get(url)
463        self.wait_until_visible('#allbuildstable tbody tr')
464
465        show_rows = self.driver.find_elements(
466            By.XPATH,
467            '//select[@class="form-control pagesize-allbuildstable"]'
468        )
469        # Check show rows
470        for show_row_link in show_rows:
471            show_row_link = Select(show_row_link)
472            test_show_rows(10, show_row_link)
473            test_show_rows(25, show_row_link)
474            test_show_rows(50, show_row_link)
475            test_show_rows(100, show_row_link)
476            test_show_rows(150, show_row_link)
477