diff options
author | Elliot Smith <elliot.smith@intel.com> | 2016-04-19 17:28:47 +0100 |
---|---|---|
committer | Richard Purdie <richard.purdie@linuxfoundation.org> | 2016-04-19 21:11:26 +0100 |
commit | d9dd864c68bf968fb50ff96b6545dc40d608f9df (patch) | |
tree | a488f3e4997eac608c2e4ec35be44ba1f221f940 | |
parent | 1cf8f215b3543ce0f39ebd3321d58cfb518f1c1f (diff) | |
download | poky-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.py | 251 | ||||
-rw-r--r-- | bitbake/lib/toaster/toastergui/tests.py | 87 |
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 | |||
22 | from django.core.urlresolvers import reverse | ||
23 | from django.utils import timezone | ||
24 | |||
25 | from selenium_helpers import SeleniumTestCase | ||
26 | |||
27 | from orm.models import Project, Release, BitbakeVersion, Build, LogMessage | ||
28 | from orm.models import Layer, Layer_Version, Recipe, CustomImageRecipe | ||
29 | |||
30 | class 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 | |||
496 | class 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) | ||