test_builddashboard_page.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335
  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. from django.urls import reverse
  10. from django.utils import timezone
  11. from tests.browser.selenium_helpers import SeleniumTestCase
  12. from orm.models import Project, Release, BitbakeVersion, Build, LogMessage
  13. from orm.models import Layer, Layer_Version, Recipe, CustomImageRecipe, Variable
  14. class TestBuildDashboardPage(SeleniumTestCase):
  15. """ Tests for the build dashboard /build/X """
  16. def setUp(self):
  17. bbv = BitbakeVersion.objects.create(name='bbv1', giturl='/tmp/',
  18. branch='master', dirpath="")
  19. release = Release.objects.create(name='release1',
  20. bitbake_version=bbv)
  21. project = Project.objects.create_project(name='test project',
  22. release=release)
  23. now = timezone.now()
  24. self.build1 = Build.objects.create(project=project,
  25. started_on=now,
  26. completed_on=now,
  27. outcome=Build.SUCCEEDED)
  28. self.build2 = Build.objects.create(project=project,
  29. started_on=now,
  30. completed_on=now,
  31. outcome=Build.SUCCEEDED)
  32. self.build3 = Build.objects.create(project=project,
  33. started_on=now,
  34. completed_on=now,
  35. outcome=Build.FAILED)
  36. # add Variable objects to the successful builds, as this is the criterion
  37. # used to determine whether the left-hand panel should be displayed
  38. Variable.objects.create(build=self.build1,
  39. variable_name='Foo',
  40. variable_value='Bar')
  41. Variable.objects.create(build=self.build2,
  42. variable_name='Foo',
  43. variable_value='Bar')
  44. # exception
  45. msg1 = 'an exception was thrown'
  46. self.exception_message = LogMessage.objects.create(
  47. build=self.build1,
  48. level=LogMessage.EXCEPTION,
  49. message=msg1
  50. )
  51. # critical
  52. msg2 = 'a critical error occurred'
  53. self.critical_message = LogMessage.objects.create(
  54. build=self.build1,
  55. level=LogMessage.CRITICAL,
  56. message=msg2
  57. )
  58. # error on the failed build
  59. msg3 = 'an error occurred'
  60. self.error_message = LogMessage.objects.create(
  61. build=self.build3,
  62. level=LogMessage.ERROR,
  63. message=msg3
  64. )
  65. # warning on the failed build
  66. msg4 = 'DANGER WILL ROBINSON'
  67. self.warning_message = LogMessage.objects.create(
  68. build=self.build3,
  69. level=LogMessage.WARNING,
  70. message=msg4
  71. )
  72. # recipes related to the build, for testing the edit custom image/new
  73. # custom image buttons
  74. layer = Layer.objects.create(name='alayer')
  75. layer_version = Layer_Version.objects.create(
  76. layer=layer, build=self.build1
  77. )
  78. # non-image recipes related to a build, for testing the new custom
  79. # image button
  80. layer_version2 = Layer_Version.objects.create(layer=layer,
  81. build=self.build3)
  82. # image recipes
  83. self.image_recipe1 = Recipe.objects.create(
  84. name='recipeA',
  85. layer_version=layer_version,
  86. file_path='/foo/recipeA.bb',
  87. is_image=True
  88. )
  89. self.image_recipe2 = Recipe.objects.create(
  90. name='recipeB',
  91. layer_version=layer_version,
  92. file_path='/foo/recipeB.bb',
  93. is_image=True
  94. )
  95. # custom image recipes for this project
  96. self.custom_image_recipe1 = CustomImageRecipe.objects.create(
  97. name='customRecipeY',
  98. project=project,
  99. layer_version=layer_version,
  100. file_path='/foo/customRecipeY.bb',
  101. base_recipe=self.image_recipe1,
  102. is_image=True
  103. )
  104. self.custom_image_recipe2 = CustomImageRecipe.objects.create(
  105. name='customRecipeZ',
  106. project=project,
  107. layer_version=layer_version,
  108. file_path='/foo/customRecipeZ.bb',
  109. base_recipe=self.image_recipe2,
  110. is_image=True
  111. )
  112. # custom image recipe for a different project (to test filtering
  113. # of image recipes and custom image recipes is correct: this shouldn't
  114. # show up in either query against self.build1)
  115. self.custom_image_recipe3 = CustomImageRecipe.objects.create(
  116. name='customRecipeOmega',
  117. project=Project.objects.create(name='baz', release=release),
  118. layer_version=Layer_Version.objects.create(
  119. layer=layer, build=self.build2
  120. ),
  121. file_path='/foo/customRecipeOmega.bb',
  122. base_recipe=self.image_recipe2,
  123. is_image=True
  124. )
  125. # another non-image recipe (to test filtering of image recipes and
  126. # custom image recipes is correct: this shouldn't show up in either
  127. # for any build)
  128. self.non_image_recipe = Recipe.objects.create(
  129. name='nonImageRecipe',
  130. layer_version=layer_version,
  131. file_path='/foo/nonImageRecipe.bb',
  132. is_image=False
  133. )
  134. def _get_build_dashboard(self, build):
  135. """
  136. Navigate to the build dashboard for build
  137. """
  138. url = reverse('builddashboard', args=(build.id,))
  139. self.get(url)
  140. def _get_build_dashboard_errors(self, build):
  141. """
  142. Get a list of HTML fragments representing the errors on the
  143. dashboard for the Build object build
  144. """
  145. self._get_build_dashboard(build)
  146. return self.find_all('#errors div.alert-danger')
  147. def _check_for_log_message(self, message_elements, log_message):
  148. """
  149. Check that the LogMessage <log_message> has a representation in
  150. the HTML elements <message_elements>.
  151. message_elements: WebElements representing the log messages shown
  152. in the build dashboard; each should have a <pre> element inside
  153. it with a data-log-message-id attribute
  154. log_message: orm.models.LogMessage instance
  155. """
  156. expected_text = log_message.message
  157. expected_pk = str(log_message.pk)
  158. found = False
  159. for element in message_elements:
  160. log_message_text = element.find_element_by_tag_name('pre').text.strip()
  161. text_matches = (log_message_text == expected_text)
  162. log_message_pk = element.get_attribute('data-log-message-id')
  163. id_matches = (log_message_pk == expected_pk)
  164. if text_matches and id_matches:
  165. found = True
  166. break
  167. template_vars = (expected_text, expected_pk)
  168. assertion_failed_msg = 'message not found: ' \
  169. 'expected text "%s" and ID %s' % template_vars
  170. self.assertTrue(found, assertion_failed_msg)
  171. def _check_for_error_message(self, build, log_message):
  172. """
  173. Check whether the LogMessage instance <log_message> is
  174. represented as an HTML error in the dashboard page for the Build object
  175. build
  176. """
  177. errors = self._get_build_dashboard_errors(build)
  178. self._check_for_log_message(errors, log_message)
  179. def _check_labels_in_modal(self, modal, expected):
  180. """
  181. Check that the text values of the <label> elements inside
  182. the WebElement modal match the list of text values in expected
  183. """
  184. # labels containing the radio buttons we're testing for
  185. labels = modal.find_elements_by_css_selector(".radio")
  186. labels_text = [lab.text for lab in labels]
  187. self.assertEqual(len(labels_text), len(expected))
  188. for expected_text in expected:
  189. self.assertTrue(expected_text in labels_text,
  190. "Could not find %s in %s" % (expected_text,
  191. labels_text))
  192. def test_exceptions_show_as_errors(self):
  193. """
  194. LogMessages with level EXCEPTION should display in the errors
  195. section of the page
  196. """
  197. self._check_for_error_message(self.build1, self.exception_message)
  198. def test_criticals_show_as_errors(self):
  199. """
  200. LogMessages with level CRITICAL should display in the errors
  201. section of the page
  202. """
  203. self._check_for_error_message(self.build1, self.critical_message)
  204. def test_edit_custom_image_button(self):
  205. """
  206. A build which built two custom images should present a modal which lets
  207. the user choose one of them to edit
  208. """
  209. self._get_build_dashboard(self.build1)
  210. # click the "edit custom image" button, which populates the modal
  211. selector = '[data-role="edit-custom-image-trigger"]'
  212. self.click(selector)
  213. modal = self.driver.find_element_by_id('edit-custom-image-modal')
  214. self.wait_until_visible("#edit-custom-image-modal")
  215. # recipes we expect to see in the edit custom image modal
  216. expected_recipes = [
  217. self.custom_image_recipe1.name,
  218. self.custom_image_recipe2.name
  219. ]
  220. self._check_labels_in_modal(modal, expected_recipes)
  221. def test_new_custom_image_button(self):
  222. """
  223. Check that a build with multiple images and custom images presents
  224. all of them as options for creating a new custom image from
  225. """
  226. self._get_build_dashboard(self.build1)
  227. # click the "new custom image" button, which populates the modal
  228. selector = '[data-role="new-custom-image-trigger"]'
  229. self.click(selector)
  230. modal = self.driver.find_element_by_id('new-custom-image-modal')
  231. self.wait_until_visible("#new-custom-image-modal")
  232. # recipes we expect to see in the new custom image modal
  233. expected_recipes = [
  234. self.image_recipe1.name,
  235. self.image_recipe2.name,
  236. self.custom_image_recipe1.name,
  237. self.custom_image_recipe2.name
  238. ]
  239. self._check_labels_in_modal(modal, expected_recipes)
  240. def test_new_custom_image_button_no_image(self):
  241. """
  242. Check that a build which builds non-image recipes doesn't show
  243. the new custom image button on the dashboard.
  244. """
  245. self._get_build_dashboard(self.build3)
  246. selector = '[data-role="new-custom-image-trigger"]'
  247. self.assertFalse(self.element_exists(selector),
  248. 'new custom image button should not show for builds which ' \
  249. 'don\'t have any image recipes')
  250. def test_left_panel(self):
  251. """"
  252. Builds which succeed should have a left panel and a build summary
  253. """
  254. self._get_build_dashboard(self.build1)
  255. left_panel = self.find_all('#nav')
  256. self.assertEqual(len(left_panel), 1)
  257. build_summary = self.find_all('[data-role="build-summary-heading"]')
  258. self.assertEqual(len(build_summary), 1)
  259. def test_failed_no_left_panel(self):
  260. """
  261. Builds which fail should have no left panel and no build summary
  262. """
  263. self._get_build_dashboard(self.build3)
  264. left_panel = self.find_all('#nav')
  265. self.assertEqual(len(left_panel), 0)
  266. build_summary = self.find_all('[data-role="build-summary-heading"]')
  267. self.assertEqual(len(build_summary), 0)
  268. def test_failed_shows_errors_and_warnings(self):
  269. """
  270. Failed builds should still show error and warning messages
  271. """
  272. self._get_build_dashboard(self.build3)
  273. errors = self.find_all('#errors div.alert-danger')
  274. self._check_for_log_message(errors, self.error_message)
  275. # expand the warnings area
  276. self.click('#warning-toggle')
  277. self.wait_until_visible('#warnings div.alert-warning')
  278. warnings = self.find_all('#warnings div.alert-warning')
  279. self._check_for_log_message(warnings, self.warning_message)