blob: 9e6c46a25310238b9d918ff47a5957b091b0a2d6 [file] [log] [blame]
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001#! /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-2015 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"""Test cases for Toaster GUI and ReST."""
23
24from django.test import TestCase
Patrick Williamsf1e5d692016-03-30 15:21:19 -050025from django.test.client import RequestFactory
Patrick Williamsc124f4f2015-09-15 14:41:29 -050026from django.core.urlresolvers import reverse
27from django.utils import timezone
Patrick Williamsf1e5d692016-03-30 15:21:19 -050028
29from orm.models import Project, Release, BitbakeVersion, Package, LogMessage
Patrick Williamsd7e96312015-09-22 08:09:05 -050030from orm.models import ReleaseLayerSourcePriority, LayerSource, Layer, Build
Patrick Williamsf1e5d692016-03-30 15:21:19 -050031from orm.models import Layer_Version, Recipe, Machine, ProjectLayer, Target
32from orm.models import CustomImageRecipe, ProjectVariable
33from orm.models import Branch
34
35import toastermain
36
37from toastergui.tables import SoftwareRecipesTable
Patrick Williamsc124f4f2015-09-15 14:41:29 -050038import json
Patrick Williamsd7e96312015-09-22 08:09:05 -050039from bs4 import BeautifulSoup
Patrick Williamsf1e5d692016-03-30 15:21:19 -050040import re
Patrick Williamsc124f4f2015-09-15 14:41:29 -050041
42PROJECT_NAME = "test project"
Patrick Williamsf1e5d692016-03-30 15:21:19 -050043CLI_BUILDS_PROJECT_NAME = 'Command line builds'
44
45# by default, tests are run in build mode; to run in analysis mode,
46# set this to False in individual test cases
47toastermain.settings.BUILD_MODE = True
Patrick Williamsc124f4f2015-09-15 14:41:29 -050048
49class ViewTests(TestCase):
50 """Tests to verify view APIs."""
51
52 def setUp(self):
53 bbv = BitbakeVersion.objects.create(name="test bbv", giturl="/tmp/",
54 branch="master", dirpath="")
55 release = Release.objects.create(name="test release",
Patrick Williamsf1e5d692016-03-30 15:21:19 -050056 branch_name="master",
Patrick Williamsc124f4f2015-09-15 14:41:29 -050057 bitbake_version=bbv)
58 self.project = Project.objects.create_project(name=PROJECT_NAME,
59 release=release)
Patrick Williamsf1e5d692016-03-30 15:21:19 -050060 now = timezone.now()
61
62 build = Build.objects.create(project=self.project,
63 started_on=now,
64 completed_on=now)
65
Patrick Williamsc124f4f2015-09-15 14:41:29 -050066 layersrc = LayerSource.objects.create(sourcetype=LayerSource.TYPE_IMPORTED)
67 self.priority = ReleaseLayerSourcePriority.objects.create(release=release,
68 layer_source=layersrc)
69 layer = Layer.objects.create(name="base-layer", layer_source=layersrc,
70 vcs_url="/tmp/")
71
Patrick Williamsf1e5d692016-03-30 15:21:19 -050072 branch = Branch.objects.create(name="master", layer_source=layersrc)
Patrick Williamsc124f4f2015-09-15 14:41:29 -050073
Patrick Williamsf1e5d692016-03-30 15:21:19 -050074 lver = Layer_Version.objects.create(layer=layer, project=self.project,
75 layer_source=layersrc, commit="master",
76 up_branch=branch)
77
78 self.recipe1 = Recipe.objects.create(layer_source=layersrc,
79 name="base-recipe",
80 version="1.2",
81 summary="one recipe",
82 description="recipe",
83 layer_version=lver)
Patrick Williamsc124f4f2015-09-15 14:41:29 -050084
85 Machine.objects.create(layer_version=lver, name="wisk",
86 description="wisking machine")
87
88 ProjectLayer.objects.create(project=self.project, layercommit=lver)
89
Patrick Williamsf1e5d692016-03-30 15:21:19 -050090
91 self.customr = CustomImageRecipe.objects.create(\
92 name="custom recipe", project=self.project,
93 base_recipe=self.recipe1)
94
95 self.package = Package.objects.create(name='pkg1', recipe=self.recipe1,
96 build=build)
97
98
99 # recipe with project for testing AvailableRecipe table
100 self.recipe2 = Recipe.objects.create(layer_source=layersrc,
101 name="fancy-recipe",
102 version="1.4",
103 summary="a fancy recipe",
104 description="fancy recipe",
105 layer_version=lver,
106 file_path='/home/foo')
107
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500108 self.assertTrue(lver in self.project.compatible_layerversions())
109
110 def test_get_base_call_returns_html(self):
111 """Basic test for all-projects view"""
112 response = self.client.get(reverse('all-projects'), follow=True)
113 self.assertEqual(response.status_code, 200)
114 self.assertTrue(response['Content-Type'].startswith('text/html'))
115 self.assertTemplateUsed(response, "projects.html")
116 self.assertTrue(PROJECT_NAME in response.content)
117
118 def test_get_json_call_returns_json(self):
119 """Test for all projects output in json format"""
120 url = reverse('all-projects')
121 response = self.client.get(url, {"format": "json"}, follow=True)
122 self.assertEqual(response.status_code, 200)
123 self.assertTrue(response['Content-Type'].startswith('application/json'))
124
125 data = json.loads(response.content)
126
127 self.assertTrue("error" in data)
128 self.assertEqual(data["error"], "ok")
129 self.assertTrue("rows" in data)
130
131 self.assertTrue(PROJECT_NAME in [x["name"] for x in data["rows"]])
132 self.assertTrue("id" in data["rows"][0])
133
134 self.assertEqual(sorted(data["rows"][0]),
135 ['bitbake_version_id', 'created', 'id',
136 'is_default', 'layersTypeAheadUrl', 'name',
137 'num_builds', 'projectBuildsUrl', 'projectPageUrl',
138 'recipesTypeAheadUrl', 'release_id',
139 'short_description', 'updated', 'user_id'])
140
141 def test_typeaheads(self):
142 """Test typeahead ReST API"""
143 layers_url = reverse('xhr_layerstypeahead', args=(self.project.id,))
144 prj_url = reverse('xhr_projectstypeahead')
145
146 urls = [layers_url,
147 prj_url,
148 reverse('xhr_recipestypeahead', args=(self.project.id,)),
149 reverse('xhr_machinestypeahead', args=(self.project.id,)),
150 ]
151
152 def basic_reponse_check(response, url):
153 """Check data structure of http response."""
154 self.assertEqual(response.status_code, 200)
155 self.assertTrue(response['Content-Type'].startswith('application/json'))
156
157 data = json.loads(response.content)
158
159 self.assertTrue("error" in data)
160 self.assertEqual(data["error"], "ok")
161 self.assertTrue("results" in data)
162
163 # We got a result so now check the fields
164 if len(data['results']) > 0:
165 result = data['results'][0]
166
167 self.assertTrue(len(result['name']) > 0)
168 self.assertTrue("detail" in result)
169 self.assertTrue(result['id'] > 0)
170
171 # Special check for the layers typeahead's extra fields
172 if url == layers_url:
173 self.assertTrue(len(result['layerdetailurl']) > 0)
174 self.assertTrue(len(result['vcs_url']) > 0)
175 self.assertTrue(len(result['vcs_reference']) > 0)
176 # Special check for project typeahead extra fields
177 elif url == prj_url:
178 self.assertTrue(len(result['projectPageUrl']) > 0)
179
180 return True
181
182 return False
183
184 import string
185
186 for url in urls:
187 results = False
188
189 for typeing in list(string.ascii_letters):
190 response = self.client.get(url, {'search': typeing})
191 results = basic_reponse_check(response, url)
192 if results:
193 break
194
195 # After "typeing" the alpabet we should have result true
196 # from each of the urls
197 self.assertTrue(results)
198
199 def test_xhr_import_layer(self):
200 """Test xhr_importlayer API"""
201 #Test for importing an already existing layer
202 args = {'vcs_url' : "git://git.example.com/test",
203 'name' : "base-layer",
204 'git_ref': "c12b9596afd236116b25ce26dbe0d793de9dc7ce",
205 'project_id': 1, 'dir_path' : "/path/in/repository"}
206 response = self.client.post(reverse('xhr_importlayer'), args)
207 data = json.loads(response.content)
208 self.assertEqual(response.status_code, 200)
209 self.assertNotEqual(data["error"], "ok")
210
211 #Test to verify import of a layer successful
212 args['name'] = "meta-oe"
213 response = self.client.post(reverse('xhr_importlayer'), args)
214 data = json.loads(response.content)
215 self.assertTrue(data["error"], "ok")
216
217 #Test for html tag in the data
218 args['<'] = "testing html tag"
219 response = self.client.post(reverse('xhr_importlayer'), args)
220 data = json.loads(response.content)
221 self.assertNotEqual(data["error"], "ok")
222
223 #Empty data passed
224 args = {}
225 response = self.client.post(reverse('xhr_importlayer'), args)
226 data = json.loads(response.content)
227 self.assertNotEqual(data["error"], "ok")
228
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500229 def test_custom_ok(self):
230 """Test successful return from ReST API xhr_customrecipe"""
231 url = reverse('xhr_customrecipe')
232 params = {'name': 'custom', 'project': self.project.id,
233 'base': self.recipe1.id}
234 response = self.client.post(url, params)
235 self.assertEqual(response.status_code, 200)
236 data = json.loads(response.content)
237 self.assertEqual(data['error'], 'ok')
238 self.assertTrue('url' in data)
239 # get recipe from the database
240 recipe = CustomImageRecipe.objects.get(project=self.project,
241 name=params['name'])
242 args = (self.project.id, recipe.id,)
243 self.assertEqual(reverse('customrecipe', args=args), data['url'])
244
245 def test_custom_incomplete_params(self):
246 """Test not passing all required parameters to xhr_customrecipe"""
247 url = reverse('xhr_customrecipe')
248 for params in [{}, {'name': 'custom'},
249 {'name': 'custom', 'project': self.project.id}]:
250 response = self.client.post(url, params)
251 self.assertEqual(response.status_code, 200)
252 data = json.loads(response.content)
253 self.assertNotEqual(data["error"], "ok")
254
255 def test_xhr_custom_wrong_project(self):
256 """Test passing wrong project id to xhr_customrecipe"""
257 url = reverse('xhr_customrecipe')
258 params = {'name': 'custom', 'project': 0, "base": self.recipe1.id}
259 response = self.client.post(url, params)
260 self.assertEqual(response.status_code, 200)
261 data = json.loads(response.content)
262 self.assertNotEqual(data["error"], "ok")
263
264 def test_xhr_custom_wrong_base(self):
265 """Test passing wrong base recipe id to xhr_customrecipe"""
266 url = reverse('xhr_customrecipe')
267 params = {'name': 'custom', 'project': self.project.id, "base": 0}
268 response = self.client.post(url, params)
269 self.assertEqual(response.status_code, 200)
270 data = json.loads(response.content)
271 self.assertNotEqual(data["error"], "ok")
272
273 def test_xhr_custom_details(self):
274 """Test getting custom recipe details"""
275 name = "custom recipe"
276 url = reverse('xhr_customrecipe_id', args=(self.customr.id,))
277 response = self.client.get(url)
278 self.assertEqual(response.status_code, 200)
279 expected = {"error": "ok",
280 "info": {'id': self.customr.id,
281 'name': name,
282 'base_recipe_id': self.recipe1.id,
283 'project_id': self.project.id,
284 }
285 }
286 self.assertEqual(json.loads(response.content), expected)
287
288 def test_xhr_custom_del(self):
289 """Test deleting custom recipe"""
290 name = "to be deleted"
291 recipe = CustomImageRecipe.objects.create(\
292 name=name, project=self.project,
293 base_recipe=self.recipe1)
294 url = reverse('xhr_customrecipe_id', args=(recipe.id,))
295 response = self.client.delete(url)
296 self.assertEqual(response.status_code, 200)
297 self.assertEqual(json.loads(response.content), {"error": "ok"})
298 # try to delete not-existent recipe
299 url = reverse('xhr_customrecipe_id', args=(recipe.id,))
300 response = self.client.delete(url)
301 self.assertEqual(response.status_code, 200)
302 self.assertNotEqual(json.loads(response.content)["error"], "ok")
303
304 def test_xhr_custom_packages(self):
305 """Test adding and deleting package to a custom recipe"""
306 url = reverse('xhr_customrecipe_packages',
307 args=(self.customr.id, self.package.id))
308 # add self.package1 to recipe
309 response = self.client.put(url)
310 self.assertEqual(response.status_code, 200)
311 self.assertEqual(json.loads(response.content), {"error": "ok"})
312 self.assertEqual(self.customr.packages.all()[0].id, self.package.id)
313 # delete it
314 response = self.client.delete(url)
315 self.assertEqual(response.status_code, 200)
316 self.assertEqual(json.loads(response.content), {"error": "ok"})
317 self.assertFalse(self.customr.packages.all())
318 # delete it again to test error condition
319 response = self.client.delete(url)
320 self.assertEqual(response.status_code, 200)
321 self.assertNotEqual(json.loads(response.content)["error"], "ok")
322
323 def test_xhr_custom_packages_err(self):
324 """Test error conditions of xhr_customrecipe_packages"""
325 # test calls with wrong recipe id and wrong package id
326 for args in [(0, self.package.id), (self.customr.id, 0)]:
327 url = reverse('xhr_customrecipe_packages', args=args)
328 # test put and delete methods
329 for method in (self.client.put, self.client.delete):
330 response = method(url)
331 self.assertEqual(response.status_code, 200)
332 self.assertNotEqual(json.loads(response.content),
333 {"error": "ok"})
334
335 def test_software_recipes_table(self):
336 """Test structure returned for Software RecipesTable"""
337 table = SoftwareRecipesTable()
338 request = RequestFactory().get('/foo/', {'format': 'json'})
339 response = table.get(request, pid=self.project.id)
340 data = json.loads(response.content)
341
342 rows = data['rows']
343 row1 = next(x for x in rows if x['name'] == self.recipe1.name)
344 row2 = next(x for x in rows if x['name'] == self.recipe2.name)
345
346 self.assertEqual(response.status_code, 200, 'should be 200 OK status')
347 self.assertEqual(len(rows), 2, 'should be 2 recipes')
348
349 # check other columns have been populated correctly
350 self.assertEqual(row1['name'], self.recipe1.name)
351 self.assertEqual(row1['version'], self.recipe1.version)
352 self.assertEqual(row1['get_description_or_summary'],
353 self.recipe1.description)
354 self.assertEqual(row1['layer_version__layer__name'],
355 self.recipe1.layer_version.layer.name)
356 self.assertEqual(row2['name'], self.recipe2.name)
357 self.assertEqual(row2['version'], self.recipe2.version)
358 self.assertEqual(row2['get_description_or_summary'],
359 self.recipe2.description)
360 self.assertEqual(row2['layer_version__layer__name'],
361 self.recipe2.layer_version.layer.name)
362
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500363class LandingPageTests(TestCase):
364 """ Tests for redirects on the landing page """
365 # disable bogus pylint message error:
366 # "Instance of 'WSGIRequest' has no 'url' member (no-member)"
367 # (see https://github.com/landscapeio/pylint-django/issues/42)
368 # pylint: disable=E1103
369
370 LANDING_PAGE_TITLE = 'This is Toaster'
371
372 def setUp(self):
373 """ Add default project manually """
374 self.project = Project.objects.create_project('foo', None)
375 self.project.is_default = True
376 self.project.save()
377
378 def test_only_default_project(self):
379 """
380 No projects except default
381 => get the landing page
382 """
383 response = self.client.get(reverse('landing'))
384 self.assertTrue(self.LANDING_PAGE_TITLE in response.content)
385
386 def test_default_project_has_build(self):
387 """
388 Default project has a build, no other projects
389 => get the builds page
390 """
391 now = timezone.now()
392 build = Build.objects.create(project=self.project,
393 started_on=now,
394 completed_on=now)
395 build.save()
396
397 response = self.client.get(reverse('landing'))
398 self.assertEqual(response.status_code, 302,
399 'response should be a redirect')
400 self.assertTrue('/builds' in response.url,
401 'should redirect to builds')
402
403 def test_user_project_exists(self):
404 """
405 User has added a project (without builds)
406 => get the projects page
407 """
408 user_project = Project.objects.create_project('foo', None)
409 user_project.save()
410
411 response = self.client.get(reverse('landing'))
412 self.assertEqual(response.status_code, 302,
413 'response should be a redirect')
414 self.assertTrue('/projects' in response.url,
415 'should redirect to projects')
416
417 def test_user_project_has_build(self):
418 """
419 User has added a project (with builds)
420 => get the builds page
421 """
422 user_project = Project.objects.create_project('foo', None)
423 user_project.save()
424
425 now = timezone.now()
426 build = Build.objects.create(project=user_project,
427 started_on=now,
428 completed_on=now)
429 build.save()
430
431 response = self.client.get(reverse('landing'))
432 self.assertEqual(response.status_code, 302,
433 'response should be a redirect')
434 self.assertTrue('/builds' in response.url,
435 'should redirect to builds')
436
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500437class AllProjectsPageTests(TestCase):
438 """ Tests for projects page /projects/ """
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500439
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500440 MACHINE_NAME = 'delorean'
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500441
442 def setUp(self):
443 """ Add default project manually """
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500444 project = Project.objects.create_project(CLI_BUILDS_PROJECT_NAME, None)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500445 self.default_project = project
446 self.default_project.is_default = True
447 self.default_project.save()
448
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500449 # this project is only set for some of the tests
450 self.project = None
451
452 self.release = None
453
454 def _add_build_to_default_project(self):
455 """ Add a build to the default project (not used in all tests) """
456 now = timezone.now()
457 build = Build.objects.create(project=self.default_project,
458 started_on=now,
459 completed_on=now)
460 build.save()
461
462 def _add_non_default_project(self):
463 """ Add another project """
464 bbv = BitbakeVersion.objects.create(name="test bbv", giturl="/tmp/",
465 branch="master", dirpath="")
466 self.release = Release.objects.create(name="test release",
467 branch_name="master",
468 bitbake_version=bbv)
469 self.project = Project.objects.create_project(PROJECT_NAME, self.release)
470 self.project.is_default = False
471 self.project.save()
472
473 # fake the MACHINE variable
474 project_var = ProjectVariable.objects.create(project=self.project,
475 name='MACHINE',
476 value=self.MACHINE_NAME)
477 project_var.save()
478
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500479 def test_default_project_hidden(self):
480 """ The default project should be hidden if it has no builds """
481 params = {"count": 10, "orderby": "updated:-", "page": 1}
482 response = self.client.get(reverse('all-projects'), params)
483
484 self.assertTrue(not('tr class="data"' in response.content),
485 'should be no project rows in the page')
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500486 self.assertTrue(not(CLI_BUILDS_PROJECT_NAME in response.content),
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500487 'default project "cli builds" should not be in page')
488
489 def test_default_project_has_build(self):
490 """ The default project should be shown if it has builds """
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500491 self._add_build_to_default_project()
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500492
493 params = {"count": 10, "orderby": "updated:-", "page": 1}
494 response = self.client.get(reverse('all-projects'), params)
495
496 self.assertTrue('tr class="data"' in response.content,
497 'should be a project row in the page')
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500498 self.assertTrue(CLI_BUILDS_PROJECT_NAME in response.content,
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500499 'default project "cli builds" should be in page')
Patrick Williamsd7e96312015-09-22 08:09:05 -0500500
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500501 def test_default_project_release(self):
502 """
503 The release for the default project should display as
504 'Not applicable'
505 """
506 # need a build, otherwise project doesn't display at all
507 self._add_build_to_default_project()
508
509 # another project to test, which should show release
510 self._add_non_default_project()
511
512 response = self.client.get(reverse('all-projects'), follow=True)
513 soup = BeautifulSoup(response.content)
514
515 # check the release cell for the default project
516 attrs = {'data-project': str(self.default_project.id)}
517 rows = soup.find_all('tr', attrs=attrs)
518 self.assertEqual(len(rows), 1, 'should be one row for default project')
519 cells = rows[0].find_all('td', attrs={'data-project-field': 'release'})
520 self.assertEqual(len(cells), 1, 'should be one release cell')
521 text = cells[0].select('span.muted')[0].text
522 self.assertEqual(text, 'Not applicable',
523 'release should be not applicable for default project')
524
525 # check the link in the release cell for the other project
526 attrs = {'data-project': str(self.project.id)}
527 rows = soup.find_all('tr', attrs=attrs)
528 cells = rows[0].find_all('td', attrs={'data-project-field': 'release'})
529 text = cells[0].select('a')[0].text
530 self.assertEqual(text, self.release.name,
531 'release name should be shown for non-default project')
532
533 def test_default_project_machine(self):
534 """
535 The machine for the default project should display as
536 'Not applicable'
537 """
538 # need a build, otherwise project doesn't display at all
539 self._add_build_to_default_project()
540
541 # another project to test, which should show machine
542 self._add_non_default_project()
543
544 response = self.client.get(reverse('all-projects'), follow=True)
545 soup = BeautifulSoup(response.content)
546
547 # check the machine cell for the default project
548 attrs = {'data-project': str(self.default_project.id)}
549 rows = soup.find_all('tr', attrs=attrs)
550 self.assertEqual(len(rows), 1, 'should be one row for default project')
551 cells = rows[0].find_all('td', attrs={'data-project-field': 'machine'})
552 self.assertEqual(len(cells), 1, 'should be one machine cell')
553 text = cells[0].select('span.muted')[0].text
554 self.assertEqual(text, 'Not applicable',
555 'machine should be not applicable for default project')
556
557 # check the link in the machine cell for the other project
558 attrs = {'data-project': str(self.project.id)}
559 rows = soup.find_all('tr', attrs=attrs)
560 cells = rows[0].find_all('td', attrs={'data-project-field': 'machine'})
561 text = cells[0].select('a')[0].text
562 self.assertEqual(text, self.MACHINE_NAME,
563 'machine name should be shown for non-default project')
564
565 def test_project_page_links(self):
566 """
567 Test that links for the default project point to the builds
568 page /projects/X/builds for that project, and that links for
569 other projects point to their configuration pages /projects/X/
570 """
571
572 # need a build, otherwise project doesn't display at all
573 self._add_build_to_default_project()
574
575 # another project to test, which should show machine
576 self._add_non_default_project()
577
578 response = self.client.get(reverse('all-projects'), follow=True)
579 soup = BeautifulSoup(response.content)
580
581 # link for default project
582 row = soup.find('tr', attrs={'data-project': self.default_project.id})
583 cell = row.find('td', attrs={'data-project-field': 'name'})
584 expected_url = reverse('projectbuilds', args=(self.default_project.id,))
585 self.assertEqual(cell.find('a')['href'], expected_url,
586 'link on default project name should point to builds')
587
588 # link for other project
589 row = soup.find('tr', attrs={'data-project': self.project.id})
590 cell = row.find('td', attrs={'data-project-field': 'name'})
591 expected_url = reverse('project', args=(self.project.id,))
592 self.assertEqual(cell.find('a')['href'], expected_url,
593 'link on project name should point to configuration')
594
595class ProjectBuildsPageTests(TestCase):
Patrick Williamsd7e96312015-09-22 08:09:05 -0500596 """ Test data at /project/X/builds is displayed correctly """
597
598 def setUp(self):
599 bbv = BitbakeVersion.objects.create(name="bbv1", giturl="/tmp/",
600 branch="master", dirpath="")
601 release = Release.objects.create(name="release1",
602 bitbake_version=bbv)
603 self.project1 = Project.objects.create_project(name=PROJECT_NAME,
604 release=release)
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500605 self.project1.save()
606
Patrick Williamsd7e96312015-09-22 08:09:05 -0500607 self.project2 = Project.objects.create_project(name=PROJECT_NAME,
608 release=release)
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500609 self.project2.save()
610
611 self.default_project = Project.objects.create_project(
612 name=CLI_BUILDS_PROJECT_NAME,
613 release=release
614 )
615 self.default_project.is_default = True
616 self.default_project.save()
Patrick Williamsd7e96312015-09-22 08:09:05 -0500617
618 # parameters for builds to associate with the projects
619 now = timezone.now()
620
621 self.project1_build_success = {
622 "project": self.project1,
623 "started_on": now,
624 "completed_on": now,
625 "outcome": Build.SUCCEEDED
626 }
627
628 self.project1_build_in_progress = {
629 "project": self.project1,
630 "started_on": now,
631 "completed_on": now,
632 "outcome": Build.IN_PROGRESS
633 }
634
635 self.project2_build_success = {
636 "project": self.project2,
637 "started_on": now,
638 "completed_on": now,
639 "outcome": Build.SUCCEEDED
640 }
641
642 self.project2_build_in_progress = {
643 "project": self.project2,
644 "started_on": now,
645 "completed_on": now,
646 "outcome": Build.IN_PROGRESS
647 }
648
649 def _get_rows_for_project(self, project_id):
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500650 """ Helper to retrieve HTML rows for a project """
Patrick Williamsd7e96312015-09-22 08:09:05 -0500651 url = reverse("projectbuilds", args=(project_id,))
652 response = self.client.get(url, follow=True)
653 soup = BeautifulSoup(response.content)
654 return soup.select('tr[class="data"]')
655
656 def test_show_builds_for_project(self):
657 """ Builds for a project should be displayed """
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500658 Build.objects.create(**self.project1_build_success)
659 Build.objects.create(**self.project1_build_success)
Patrick Williamsd7e96312015-09-22 08:09:05 -0500660 build_rows = self._get_rows_for_project(self.project1.id)
661 self.assertEqual(len(build_rows), 2)
662
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500663 def test_show_builds_project_only(self):
Patrick Williamsd7e96312015-09-22 08:09:05 -0500664 """ Builds for other projects should be excluded """
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500665 Build.objects.create(**self.project1_build_success)
666 Build.objects.create(**self.project1_build_success)
667 Build.objects.create(**self.project1_build_success)
Patrick Williamsd7e96312015-09-22 08:09:05 -0500668
669 # shouldn't see these two
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500670 Build.objects.create(**self.project2_build_success)
671 Build.objects.create(**self.project2_build_in_progress)
Patrick Williamsd7e96312015-09-22 08:09:05 -0500672
673 build_rows = self._get_rows_for_project(self.project1.id)
674 self.assertEqual(len(build_rows), 3)
675
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500676 def test_builds_exclude_in_progress(self):
Patrick Williamsd7e96312015-09-22 08:09:05 -0500677 """ "in progress" builds should not be shown """
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500678 Build.objects.create(**self.project1_build_success)
679 Build.objects.create(**self.project1_build_success)
Patrick Williamsd7e96312015-09-22 08:09:05 -0500680
681 # shouldn't see this one
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500682 Build.objects.create(**self.project1_build_in_progress)
Patrick Williamsd7e96312015-09-22 08:09:05 -0500683
684 # shouldn't see these two either, as they belong to a different project
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500685 Build.objects.create(**self.project2_build_success)
686 Build.objects.create(**self.project2_build_in_progress)
Patrick Williamsd7e96312015-09-22 08:09:05 -0500687
688 build_rows = self._get_rows_for_project(self.project1.id)
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500689 self.assertEqual(len(build_rows), 2)
690
691 def test_tasks_in_projectbuilds(self):
692 """ Task should be shown as suffix on build name """
693 build = Build.objects.create(**self.project1_build_success)
694 Target.objects.create(build=build, target='bash', task='clean')
695 url = reverse("projectbuilds", args=(self.project1.id,))
696 response = self.client.get(url, follow=True)
697 result = re.findall('^ +bash:clean$', response.content, re.MULTILINE)
698 self.assertEqual(len(result), 2)
699
700 def test_cli_builds_hides_tabs(self):
701 """
702 Display for command line builds should hide tabs;
703 note that the latest builds section is already tested in
704 AllBuildsPageTests, as the template is the same
705 """
706 url = reverse("projectbuilds", args=(self.default_project.id,))
707 response = self.client.get(url, follow=True)
708 soup = BeautifulSoup(response.content)
709 tabs = soup.select('#project-topbar')
710 self.assertEqual(len(tabs), 0,
711 'should be no top bar shown for command line builds')
712
713 def test_non_cli_builds_has_tabs(self):
714 """
715 Non-command-line builds projects should show the tabs
716 """
717 url = reverse("projectbuilds", args=(self.project1.id,))
718 response = self.client.get(url, follow=True)
719 soup = BeautifulSoup(response.content)
720 tabs = soup.select('#project-topbar')
721 self.assertEqual(len(tabs), 1,
722 'should be a top bar shown for non-command-line builds')
723
724class AllBuildsPageTests(TestCase):
725 """ Tests for all builds page /builds/ """
726
727 def setUp(self):
728 bbv = BitbakeVersion.objects.create(name="bbv1", giturl="/tmp/",
729 branch="master", dirpath="")
730 release = Release.objects.create(name="release1",
731 bitbake_version=bbv)
732 self.project1 = Project.objects.create_project(name=PROJECT_NAME,
733 release=release)
734 self.default_project = Project.objects.create_project(
735 name=CLI_BUILDS_PROJECT_NAME,
736 release=release
737 )
738 self.default_project.is_default = True
739 self.default_project.save()
740
741 # parameters for builds to associate with the projects
742 now = timezone.now()
743
744 self.project1_build_success = {
745 "project": self.project1,
746 "started_on": now,
747 "completed_on": now,
748 "outcome": Build.SUCCEEDED
749 }
750
751 self.default_project_build_success = {
752 "project": self.default_project,
753 "started_on": now,
754 "completed_on": now,
755 "outcome": Build.SUCCEEDED
756 }
757
758 def test_show_tasks_in_allbuilds(self):
759 """ Task should be shown as suffix on build name """
760 build = Build.objects.create(**self.project1_build_success)
761 Target.objects.create(build=build, target='bash', task='clean')
762 url = reverse('all-builds')
763 response = self.client.get(url, follow=True)
764 result = re.findall('bash:clean', response.content, re.MULTILINE)
765 self.assertEqual(len(result), 3)
766
767 def test_no_run_again_for_cli_build(self):
768 """ "Run again" button should not be shown for command-line builds """
769 build = Build.objects.create(**self.default_project_build_success)
770 url = reverse('all-builds')
771 response = self.client.get(url, follow=True)
772 soup = BeautifulSoup(response.content)
773
774 attrs = {'data-latest-build-result': build.id}
775 result = soup.find('div', attrs=attrs)
776
777 # shouldn't see a run again button for command-line builds
778 run_again_button = result.select('button')
779 self.assertEqual(len(run_again_button), 0)
780
781 # should see a help icon for command-line builds
782 help_icon = result.select('i.get-help-green')
783 self.assertEqual(len(help_icon), 1)
784
785 def test_tooltips_on_project_name(self):
786 """
787 A tooltip should be present next to the command line
788 builds project name in the all builds page, but not for
789 other projects
790 """
791 build1 = Build.objects.create(**self.project1_build_success)
792 default_build = Build.objects.create(**self.default_project_build_success)
793
794 url = reverse('all-builds')
795 response = self.client.get(url, follow=True)
796 soup = BeautifulSoup(response.content)
797
798 # no help icon on non-default project name
799 result = soup.find('tr', attrs={'data-table-build-result': build1.id})
800 name = result.select('td.project-name')[0]
801 icons = name.select('i.get-help')
802 self.assertEqual(len(icons), 0,
803 'should not be a help icon for non-cli builds name')
804
805 # help icon on default project name
806 result = soup.find('tr', attrs={'data-table-build-result': default_build.id})
807 name = result.select('td.project-name')[0]
808 icons = name.select('i.get-help')
809 self.assertEqual(len(icons), 1,
810 'should be a help icon for cli builds name')
811
812class ProjectPageTests(TestCase):
813 """ Test project data at /project/X/ is displayed correctly """
814 CLI_BUILDS_PROJECT_NAME = 'Command line builds'
815
816 def test_command_line_builds_in_progress(self):
817 """
818 In progress builds should not cause an error to be thrown
819 when navigating to "command line builds" project page;
820 see https://bugzilla.yoctoproject.org/show_bug.cgi?id=8277
821 """
822
823 # add the "command line builds" default project; this mirrors what
824 # we do in migration 0026_set_default_project.py
825 default_project = Project.objects.create_project(self.CLI_BUILDS_PROJECT_NAME, None)
826 default_project.is_default = True
827 default_project.save()
828
829 # add an "in progress" build for the default project
830 now = timezone.now()
831 build = Build.objects.create(project=default_project,
832 started_on=now,
833 completed_on=now,
834 outcome=Build.IN_PROGRESS)
835
836 # navigate to the project page for the default project
837 url = reverse("project", args=(default_project.id,))
838 response = self.client.get(url, follow=True)
839
840 self.assertEqual(response.status_code, 200)
841
842class BuildDashboardTests(TestCase):
843 """ Tests for the build dashboard /build/X """
844
845 def setUp(self):
846 bbv = BitbakeVersion.objects.create(name="bbv1", giturl="/tmp/",
847 branch="master", dirpath="")
848 release = Release.objects.create(name="release1",
849 bitbake_version=bbv)
850 project = Project.objects.create_project(name=PROJECT_NAME,
851 release=release)
852
853 now = timezone.now()
854
855 self.build1 = Build.objects.create(project=project,
856 started_on=now,
857 completed_on=now)
858
859 # exception
860 msg1 = 'an exception was thrown'
861 self.exception_message = LogMessage.objects.create(
862 build=self.build1,
863 level=LogMessage.EXCEPTION,
864 message=msg1
865 )
866
867 # critical
868 msg2 = 'a critical error occurred'
869 self.critical_message = LogMessage.objects.create(
870 build=self.build1,
871 level=LogMessage.CRITICAL,
872 message=msg2
873 )
874
875 def _get_build_dashboard_errors(self):
876 """
877 Get a list of HTML fragments representing the errors on the
878 build dashboard
879 """
880 url = reverse('builddashboard', args=(self.build1.id,))
881 response = self.client.get(url)
882 soup = BeautifulSoup(response.content)
883 return soup.select('#errors div.alert-error')
884
885 def _check_for_log_message(self, log_message):
886 """
887 Check whether the LogMessage instance <log_message> is
888 represented as an HTML error in the build dashboard page
889 """
890 errors = self._get_build_dashboard_errors()
891 self.assertEqual(len(errors), 2)
892
893 expected_text = log_message.message
894 expected_id = str(log_message.id)
895
896 found = False
897 for error in errors:
898 error_text = error.find('pre').text
899 text_matches = (error_text == expected_text)
900
901 error_id = error['data-error']
902 id_matches = (error_id == expected_id)
903
904 if text_matches and id_matches:
905 found = True
906 break
907
908 template_vars = (expected_text, error_text,
909 expected_id, error_id)
910 assertion_error_msg = 'exception not found as error: ' \
911 'expected text "%s" and got "%s"; ' \
912 'expected ID %s and got %s' % template_vars
913 self.assertTrue(found, assertion_error_msg)
914
915 def test_exceptions_show_as_errors(self):
916 """
917 LogMessages with level EXCEPTION should display in the errors
918 section of the page
919 """
920 self._check_for_log_message(self.exception_message)
921
922 def test_criticals_show_as_errors(self):
923 """
924 LogMessages with level CRITICAL should display in the errors
925 section of the page
926 """
927 self._check_for_log_message(self.critical_message)