summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorElliot Smith <elliot.smith@intel.com>2016-04-19 17:28:47 +0100
committerRichard Purdie <richard.purdie@linuxfoundation.org>2016-04-19 21:11:26 +0100
commitd9dd864c68bf968fb50ff96b6545dc40d608f9df (patch)
treea488f3e4997eac608c2e4ec35be44ba1f221f940
parent1cf8f215b3543ce0f39ebd3321d58cfb518f1c1f (diff)
downloadpoky-d9dd864c68bf968fb50ff96b6545dc40d608f9df.tar.gz
bitbake: toaster-tests: tests for build dashboard
Convert existing tests to Selenium. Add basic tests to check that the modal contains radio buttons to select a custom image to edit when a build built multiple custom images, and to create a new custom image from one of the images built during the build. [YOCTO #9123] (Bitbake rev: c07f65feaba50b13a38635bd8149804c823d446a) Signed-off-by: Elliot Smith <elliot.smith@intel.com> Signed-off-by: Michael Wood <michael.g.wood@intel.com> Signed-off-by: Richard Purdie <richard.purdie@linuxfoundation.org>
-rw-r--r--bitbake/lib/toaster/tests/browser/test_builddashboard_page.py251
-rw-r--r--bitbake/lib/toaster/toastergui/tests.py87
2 files changed, 251 insertions, 87 deletions
diff --git a/bitbake/lib/toaster/tests/browser/test_builddashboard_page.py b/bitbake/lib/toaster/tests/browser/test_builddashboard_page.py
new file mode 100644
index 0000000000..5e08749470
--- /dev/null
+++ b/bitbake/lib/toaster/tests/browser/test_builddashboard_page.py
@@ -0,0 +1,251 @@
1#! /usr/bin/env python
2# ex:ts=4:sw=4:sts=4:et
3# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
4#
5# BitBake Toaster Implementation
6#
7# Copyright (C) 2013-2016 Intel Corporation
8#
9# This program is free software; you can redistribute it and/or modify
10# it under the terms of the GNU General Public License version 2 as
11# published by the Free Software Foundation.
12#
13# This program is distributed in the hope that it will be useful,
14# but WITHOUT ANY WARRANTY; without even the implied warranty of
15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16# GNU General Public License for more details.
17#
18# You should have received a copy of the GNU General Public License along
19# with this program; if not, write to the Free Software Foundation, Inc.,
20# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
21
22from django.core.urlresolvers import reverse
23from django.utils import timezone
24
25from selenium_helpers import SeleniumTestCase
26
27from orm.models import Project, Release, BitbakeVersion, Build, LogMessage
28from orm.models import Layer, Layer_Version, Recipe, CustomImageRecipe
29
30class TestBuildDashboardPage(SeleniumTestCase):
31 """ Tests for the build dashboard /build/X """
32
33 def setUp(self):
34 bbv = BitbakeVersion.objects.create(name='bbv1', giturl='/tmp/',
35 branch='master', dirpath="")
36 release = Release.objects.create(name='release1',
37 bitbake_version=bbv)
38 project = Project.objects.create_project(name='test project',
39 release=release)
40
41 now = timezone.now()
42
43 self.build1 = Build.objects.create(project=project,
44 started_on=now,
45 completed_on=now)
46
47 self.build2 = Build.objects.create(project=project,
48 started_on=now,
49 completed_on=now)
50
51 # exception
52 msg1 = 'an exception was thrown'
53 self.exception_message = LogMessage.objects.create(
54 build=self.build1,
55 level=LogMessage.EXCEPTION,
56 message=msg1
57 )
58
59 # critical
60 msg2 = 'a critical error occurred'
61 self.critical_message = LogMessage.objects.create(
62 build=self.build1,
63 level=LogMessage.CRITICAL,
64 message=msg2
65 )
66
67 # recipes related to the build, for testing the edit custom image/new
68 # custom image buttons
69 layer = Layer.objects.create(name='alayer')
70 layer_version = Layer_Version.objects.create(
71 layer=layer, build=self.build1
72 )
73
74 # image recipes
75 self.image_recipe1 = Recipe.objects.create(
76 name='recipeA',
77 layer_version=layer_version,
78 file_path='/foo/recipeA.bb',
79 is_image=True
80 )
81 self.image_recipe2 = Recipe.objects.create(
82 name='recipeB',
83 layer_version=layer_version,
84 file_path='/foo/recipeB.bb',
85 is_image=True
86 )
87
88 # custom image recipes for this project
89 self.custom_image_recipe1 = CustomImageRecipe.objects.create(
90 name='customRecipeY',
91 project=project,
92 layer_version=layer_version,
93 file_path='/foo/customRecipeY.bb',
94 base_recipe=self.image_recipe1,
95 is_image=True
96 )
97 self.custom_image_recipe2 = CustomImageRecipe.objects.create(
98 name='customRecipeZ',
99 project=project,
100 layer_version=layer_version,
101 file_path='/foo/customRecipeZ.bb',
102 base_recipe=self.image_recipe2,
103 is_image=True
104 )
105
106 # custom image recipe for a different project (to test filtering
107 # of image recipes and custom image recipes is correct: this shouldn't
108 # show up in either query against self.build1)
109 self.custom_image_recipe3 = CustomImageRecipe.objects.create(
110 name='customRecipeOmega',
111 project=Project.objects.create(name='baz', release=release),
112 layer_version=Layer_Version.objects.create(
113 layer=layer, build=self.build2
114 ),
115 file_path='/foo/customRecipeOmega.bb',
116 base_recipe=self.image_recipe2,
117 is_image=True
118 )
119
120 # another non-image recipe (to test filtering of image recipes and
121 # custom image recipes is correct: this shouldn't show up in either
122 # for any build)
123 self.non_image_recipe = Recipe.objects.create(
124 name='nonImageRecipe',
125 layer_version=layer_version,
126 file_path='/foo/nonImageRecipe.bb',
127 is_image=False
128 )
129
130 def _get_build_dashboard(self, build):
131 """
132 Navigate to the build dashboard for build
133 """
134 url = reverse('builddashboard', args=(build.id,))
135 self.get(url)
136
137 def _get_build_dashboard_errors(self, build):
138 """
139 Get a list of HTML fragments representing the errors on the
140 dashboard for the Build object build
141 """
142 self._get_build_dashboard(build)
143 return self.find_all('#errors div.alert-error')
144
145 def _check_for_log_message(self, build, log_message):
146 """
147 Check whether the LogMessage instance <log_message> is
148 represented as an HTML error in the dashboard page for the Build object
149 build
150 """
151 errors = self._get_build_dashboard_errors(build)
152 self.assertEqual(len(errors), 2)
153
154 expected_text = log_message.message
155 expected_id = str(log_message.id)
156
157 found = False
158 for error in errors:
159 error_text = error.find_element_by_tag_name('pre').text
160 text_matches = (error_text == expected_text)
161
162 error_id = error.get_attribute('data-error')
163 id_matches = (error_id == expected_id)
164
165 if text_matches and id_matches:
166 found = True
167 break
168
169 template_vars = (expected_text, error_text,
170 expected_id, error_id)
171 assertion_error_msg = 'exception not found as error: ' \
172 'expected text "%s" and got "%s"; ' \
173 'expected ID %s and got %s' % template_vars
174 self.assertTrue(found, assertion_error_msg)
175
176 def _check_labels_in_modal(self, modal, expected):
177 """
178 Check that the text values of the <label> elements inside
179 the WebElement modal match the list of text values in expected
180 """
181 # labels containing the radio buttons we're testing for
182 labels = modal.find_elements_by_tag_name('label')
183
184 # because the label content has the structure
185 # label text
186 # <input...>
187 # we have to regex on its innerHTML, as we can't just retrieve the
188 # "label text" on its own via the Selenium API
189 labels_text = sorted(map(
190 lambda label: label.get_attribute('innerHTML'), labels
191 ))
192
193 expected = sorted(expected)
194
195 self.assertEqual(len(labels_text), len(expected))
196
197 for idx, label_text in enumerate(labels_text):
198 self.assertRegexpMatches(label_text, expected[idx])
199
200 def test_exceptions_show_as_errors(self):
201 """
202 LogMessages with level EXCEPTION should display in the errors
203 section of the page
204 """
205 self._check_for_log_message(self.build1, self.exception_message)
206
207 def test_criticals_show_as_errors(self):
208 """
209 LogMessages with level CRITICAL should display in the errors
210 section of the page
211 """
212 self._check_for_log_message(self.build1, self.critical_message)
213
214 def test_edit_custom_image_button(self):
215 """
216 A build which built two custom images should present a modal which lets
217 the user choose one of them to edit
218 """
219 self._get_build_dashboard(self.build1)
220 modal = self.driver.find_element_by_id('edit-custom-image-modal')
221
222 # recipes we expect to see in the edit custom image modal
223 expected_recipes = [
224 self.custom_image_recipe1.name,
225 self.custom_image_recipe2.name
226 ]
227
228 self._check_labels_in_modal(modal, expected_recipes)
229
230 def test_new_custom_image_button(self):
231 """
232 Check that a build with multiple images and custom images presents
233 all of them as options for creating a new custom image from
234 """
235 self._get_build_dashboard(self.build1)
236
237 # click the "new custom image" button, which populates the modal
238 selector = '[data-role="new-custom-image-trigger"] button'
239 self.click(selector)
240
241 modal = self.driver.find_element_by_id('new-custom-image-modal')
242
243 # recipes we expect to see in the new custom image modal
244 expected_recipes = [
245 self.image_recipe1.name,
246 self.image_recipe2.name,
247 self.custom_image_recipe1.name,
248 self.custom_image_recipe2.name
249 ]
250
251 self._check_labels_in_modal(modal, expected_recipes)
diff --git a/bitbake/lib/toaster/toastergui/tests.py b/bitbake/lib/toaster/toastergui/tests.py
index eebd1b79ba..a4cab58483 100644
--- a/bitbake/lib/toaster/toastergui/tests.py
+++ b/bitbake/lib/toaster/toastergui/tests.py
@@ -492,90 +492,3 @@ class ViewTests(TestCase):
492 page_two_data, 492 page_two_data,
493 "Changed page on table %s but first row is the " 493 "Changed page on table %s but first row is the "
494 "same as the previous page" % name) 494 "same as the previous page" % name)
495
496class BuildDashboardTests(TestCase):
497 """ Tests for the build dashboard /build/X """
498
499 def setUp(self):
500 bbv = BitbakeVersion.objects.create(name="bbv1", giturl="/tmp/",
501 branch="master", dirpath="")
502 release = Release.objects.create(name="release1",
503 bitbake_version=bbv)
504 project = Project.objects.create_project(name=PROJECT_NAME,
505 release=release)
506
507 now = timezone.now()
508
509 self.build1 = Build.objects.create(project=project,
510 started_on=now,
511 completed_on=now)
512
513 # exception
514 msg1 = 'an exception was thrown'
515 self.exception_message = LogMessage.objects.create(
516 build=self.build1,
517 level=LogMessage.EXCEPTION,
518 message=msg1
519 )
520
521 # critical
522 msg2 = 'a critical error occurred'
523 self.critical_message = LogMessage.objects.create(
524 build=self.build1,
525 level=LogMessage.CRITICAL,
526 message=msg2
527 )
528
529 def _get_build_dashboard_errors(self):
530 """
531 Get a list of HTML fragments representing the errors on the
532 build dashboard
533 """
534 url = reverse('builddashboard', args=(self.build1.id,))
535 response = self.client.get(url)
536 soup = BeautifulSoup(response.content)
537 return soup.select('#errors div.alert-error')
538
539 def _check_for_log_message(self, log_message):
540 """
541 Check whether the LogMessage instance <log_message> is
542 represented as an HTML error in the build dashboard page
543 """
544 errors = self._get_build_dashboard_errors()
545 self.assertEqual(len(errors), 2)
546
547 expected_text = log_message.message
548 expected_id = str(log_message.id)
549
550 found = False
551 for error in errors:
552 error_text = error.find('pre').text
553 text_matches = (error_text == expected_text)
554
555 error_id = error['data-error']
556 id_matches = (error_id == expected_id)
557
558 if text_matches and id_matches:
559 found = True
560 break
561
562 template_vars = (expected_text, error_text,
563 expected_id, error_id)
564 assertion_error_msg = 'exception not found as error: ' \
565 'expected text "%s" and got "%s"; ' \
566 'expected ID %s and got %s' % template_vars
567 self.assertTrue(found, assertion_error_msg)
568
569 def test_exceptions_show_as_errors(self):
570 """
571 LogMessages with level EXCEPTION should display in the errors
572 section of the page
573 """
574 self._check_for_log_message(self.exception_message)
575
576 def test_criticals_show_as_errors(self):
577 """
578 LogMessages with level CRITICAL should display in the errors
579 section of the page
580 """
581 self._check_for_log_message(self.critical_message)