blob: 68d9e9de147f90e8052e4e0e9317c419e48193ae [file] [log] [blame]
Brad Bishop96ff1982019-08-19 13:50:42 -04001#! /usr/bin/env python3
Brad Bishop6e60e8b2018-02-01 10:27:11 -05002#
3# BitBake Toaster Implementation
4#
5# Copyright (C) 2013-2015 Intel Corporation
6#
Brad Bishopc342db32019-05-15 21:57:59 -04007# SPDX-License-Identifier: GPL-2.0-only
Brad Bishop6e60e8b2018-02-01 10:27:11 -05008#
Brad Bishop6e60e8b2018-02-01 10:27:11 -05009
10"""Test cases for Toaster GUI and ReST."""
11
12from django.test import TestCase
13from django.test.client import RequestFactory
14from django.core.urlresolvers import reverse
15from django.db.models import Q
16
17from orm.models import Project, Package
18from orm.models import Layer_Version, Recipe
19from orm.models import CustomImageRecipe
20from orm.models import CustomImagePackage
21
22import inspect
23import toastergui
24
25from toastergui.tables import SoftwareRecipesTable
26import json
27from bs4 import BeautifulSoup
28import string
29
30PROJECT_NAME = "test project"
31PROJECT_NAME2 = "test project 2"
32CLI_BUILDS_PROJECT_NAME = 'Command line builds'
33
34
35class ViewTests(TestCase):
36 """Tests to verify view APIs."""
37
38 fixtures = ['toastergui-unittest-data']
39
40 def setUp(self):
41
42 self.project = Project.objects.first()
43 self.recipe1 = Recipe.objects.get(pk=2)
44 self.customr = CustomImageRecipe.objects.first()
45 self.cust_package = CustomImagePackage.objects.first()
46 self.package = Package.objects.first()
47 self.lver = Layer_Version.objects.first()
48
49 def test_get_base_call_returns_html(self):
50 """Basic test for all-projects view"""
51 response = self.client.get(reverse('all-projects'), follow=True)
52 self.assertEqual(response.status_code, 200)
53 self.assertTrue(response['Content-Type'].startswith('text/html'))
54 self.assertTemplateUsed(response, "projects-toastertable.html")
55
56 def test_get_json_call_returns_json(self):
57 """Test for all projects output in json format"""
58 url = reverse('all-projects')
59 response = self.client.get(url, {"format": "json"}, follow=True)
60 self.assertEqual(response.status_code, 200)
61 self.assertTrue(response['Content-Type'].startswith(
62 'application/json'))
63
64 data = json.loads(response.content.decode('utf-8'))
65
66 self.assertTrue("error" in data)
67 self.assertEqual(data["error"], "ok")
68 self.assertTrue("rows" in data)
69
70 name_found = False
71 for row in data["rows"]:
72 name_found = row['name'].find(self.project.name)
73
74 self.assertTrue(name_found,
75 "project name not found in projects table")
76
77 def test_typeaheads(self):
78 """Test typeahead ReST API"""
79 layers_url = reverse('xhr_layerstypeahead', args=(self.project.id,))
80 prj_url = reverse('xhr_projectstypeahead')
81
82 urls = [layers_url,
83 prj_url,
84 reverse('xhr_recipestypeahead', args=(self.project.id,)),
85 reverse('xhr_machinestypeahead', args=(self.project.id,))]
86
87 def basic_reponse_check(response, url):
88 """Check data structure of http response."""
89 self.assertEqual(response.status_code, 200)
90 self.assertTrue(response['Content-Type'].startswith(
91 'application/json'))
92
93 data = json.loads(response.content.decode('utf-8'))
94
95 self.assertTrue("error" in data)
96 self.assertEqual(data["error"], "ok")
97 self.assertTrue("results" in data)
98
99 # We got a result so now check the fields
100 if len(data['results']) > 0:
101 result = data['results'][0]
102
103 self.assertTrue(len(result['name']) > 0)
104 self.assertTrue("detail" in result)
105 self.assertTrue(result['id'] > 0)
106
107 # Special check for the layers typeahead's extra fields
108 if url == layers_url:
109 self.assertTrue(len(result['layerdetailurl']) > 0)
110 self.assertTrue(len(result['vcs_url']) > 0)
111 self.assertTrue(len(result['vcs_reference']) > 0)
112 # Special check for project typeahead extra fields
113 elif url == prj_url:
114 self.assertTrue(len(result['projectPageUrl']) > 0)
115
116 return True
117
118 return False
119
120 for url in urls:
121 results = False
122
123 for typeing in list(string.ascii_letters):
124 response = self.client.get(url, {'search': typeing})
125 results = basic_reponse_check(response, url)
126 if results:
127 break
128
129 # After "typeing" the alpabet we should have result true
130 # from each of the urls
131 self.assertTrue(results)
132
133 def test_xhr_add_layer(self):
134 """Test xhr_add API"""
135 # Test for importing an already existing layer
136 api_url = reverse('xhr_layer', args=(self.project.id,))
137
138 layer_data = {'vcs_url': "git://git.example.com/test",
139 'name': "base-layer",
140 'git_ref': "c12b9596afd236116b25ce26dbe0d793de9dc7ce",
141 'project_id': self.project.id,
142 'local_source_dir': "",
143 'add_to_project': True,
144 'dir_path': "/path/in/repository"}
145
146 layer_data_json = json.dumps(layer_data)
147
148 response = self.client.put(api_url, layer_data_json)
149 data = json.loads(response.content.decode('utf-8'))
150 self.assertEqual(response.status_code, 200)
151 self.assertEqual(data["error"], "ok")
152
153 self.assertTrue(
154 layer_data['name'] in
155 self.project.get_all_compatible_layer_versions().values_list(
156 'layer__name',
157 flat=True),
158 "Could not find imported layer in project's all layers list"
159 )
160
161 # Empty data passed
162 response = self.client.put(api_url, "{}")
163 data = json.loads(response.content.decode('utf-8'))
164 self.assertNotEqual(data["error"], "ok")
165
166 def test_custom_ok(self):
167 """Test successful return from ReST API xhr_customrecipe"""
168 url = reverse('xhr_customrecipe')
169 params = {'name': 'custom', 'project': self.project.id,
170 'base': self.recipe1.id}
171 response = self.client.post(url, params)
172 self.assertEqual(response.status_code, 200)
173 data = json.loads(response.content.decode('utf-8'))
174 self.assertEqual(data['error'], 'ok')
175 self.assertTrue('url' in data)
176 # get recipe from the database
177 recipe = CustomImageRecipe.objects.get(project=self.project,
178 name=params['name'])
179 args = (self.project.id, recipe.id,)
180 self.assertEqual(reverse('customrecipe', args=args), data['url'])
181
182 def test_custom_incomplete_params(self):
183 """Test not passing all required parameters to xhr_customrecipe"""
184 url = reverse('xhr_customrecipe')
185 for params in [{}, {'name': 'custom'},
186 {'name': 'custom', 'project': self.project.id}]:
187 response = self.client.post(url, params)
188 self.assertEqual(response.status_code, 200)
189 data = json.loads(response.content.decode('utf-8'))
190 self.assertNotEqual(data["error"], "ok")
191
192 def test_xhr_custom_wrong_project(self):
193 """Test passing wrong project id to xhr_customrecipe"""
194 url = reverse('xhr_customrecipe')
195 params = {'name': 'custom', 'project': 0, "base": self.recipe1.id}
196 response = self.client.post(url, params)
197 self.assertEqual(response.status_code, 200)
198 data = json.loads(response.content.decode('utf-8'))
199 self.assertNotEqual(data["error"], "ok")
200
201 def test_xhr_custom_wrong_base(self):
202 """Test passing wrong base recipe id to xhr_customrecipe"""
203 url = reverse('xhr_customrecipe')
204 params = {'name': 'custom', 'project': self.project.id, "base": 0}
205 response = self.client.post(url, params)
206 self.assertEqual(response.status_code, 200)
207 data = json.loads(response.content.decode('utf-8'))
208 self.assertNotEqual(data["error"], "ok")
209
210 def test_xhr_custom_details(self):
211 """Test getting custom recipe details"""
212 url = reverse('xhr_customrecipe_id', args=(self.customr.id,))
213 response = self.client.get(url)
214 self.assertEqual(response.status_code, 200)
215 expected = {"error": "ok",
216 "info": {'id': self.customr.id,
217 'name': self.customr.name,
218 'base_recipe_id': self.recipe1.id,
219 'project_id': self.project.id}}
220 self.assertEqual(json.loads(response.content.decode('utf-8')),
221 expected)
222
223 def test_xhr_custom_del(self):
224 """Test deleting custom recipe"""
225 name = "to be deleted"
226 recipe = CustomImageRecipe.objects.create(
227 name=name, project=self.project,
228 base_recipe=self.recipe1,
229 file_path="/tmp/testing",
230 layer_version=self.customr.layer_version)
231 url = reverse('xhr_customrecipe_id', args=(recipe.id,))
232 response = self.client.delete(url)
233 self.assertEqual(response.status_code, 200)
234
235 gotoUrl = reverse('projectcustomimages', args=(self.project.pk,))
236
237 self.assertEqual(json.loads(response.content.decode('utf-8')),
238 {"error": "ok",
239 "gotoUrl": gotoUrl})
240
241 # try to delete not-existent recipe
242 url = reverse('xhr_customrecipe_id', args=(recipe.id,))
243 response = self.client.delete(url)
244 self.assertEqual(response.status_code, 200)
245 self.assertNotEqual(json.loads(
246 response.content.decode('utf-8'))["error"], "ok")
247
248 def test_xhr_custom_packages(self):
249 """Test adding and deleting package to a custom recipe"""
250 # add self.package to recipe
251 response = self.client.put(reverse('xhr_customrecipe_packages',
252 args=(self.customr.id,
253 self.cust_package.id)))
254
255 self.assertEqual(response.status_code, 200)
256 self.assertEqual(json.loads(response.content.decode('utf-8')),
257 {"error": "ok"})
258 self.assertEqual(self.customr.appends_set.first().name,
259 self.cust_package.name)
260 # delete it
261 to_delete = self.customr.appends_set.first().pk
262 del_url = reverse('xhr_customrecipe_packages',
263 args=(self.customr.id, to_delete))
264
265 response = self.client.delete(del_url)
266 self.assertEqual(response.status_code, 200)
267 self.assertEqual(json.loads(response.content.decode('utf-8')),
268 {"error": "ok"})
269 all_packages = self.customr.get_all_packages().values_list('pk',
270 flat=True)
271
272 self.assertFalse(to_delete in all_packages)
273 # delete invalid package to test error condition
274 del_url = reverse('xhr_customrecipe_packages',
275 args=(self.customr.id,
276 99999))
277
278 response = self.client.delete(del_url)
279 self.assertEqual(response.status_code, 200)
280 self.assertNotEqual(json.loads(
281 response.content.decode('utf-8'))["error"], "ok")
282
283 def test_xhr_custom_packages_err(self):
284 """Test error conditions of xhr_customrecipe_packages"""
285 # test calls with wrong recipe id and wrong package id
286 for args in [(0, self.package.id), (self.customr.id, 0)]:
287 url = reverse('xhr_customrecipe_packages', args=args)
288 # test put and delete methods
289 for method in (self.client.put, self.client.delete):
290 response = method(url)
291 self.assertEqual(response.status_code, 200)
292 self.assertNotEqual(json.loads(
293 response.content.decode('utf-8')),
294 {"error": "ok"})
295
296 def test_download_custom_recipe(self):
297 """Download the recipe file generated for the custom image"""
298
299 # Create a dummy recipe file for the custom image generation to read
300 open("/tmp/a_recipe.bb", 'a').close()
301 response = self.client.get(reverse('customrecipedownload',
302 args=(self.project.id,
303 self.customr.id)))
304
305 self.assertEqual(response.status_code, 200)
306
307 def test_software_recipes_table(self):
308 """Test structure returned for Software RecipesTable"""
309 table = SoftwareRecipesTable()
310 request = RequestFactory().get('/foo/', {'format': 'json'})
311 response = table.get(request, pid=self.project.id)
312 data = json.loads(response.content.decode('utf-8'))
313
314 recipes = Recipe.objects.filter(Q(is_image=False))
315 self.assertTrue(len(recipes) > 1,
316 "Need more than one software recipe to test "
317 "SoftwareRecipesTable")
318
319 recipe1 = recipes[0]
320 recipe2 = recipes[1]
321
322 rows = data['rows']
323 row1 = next(x for x in rows if x['name'] == recipe1.name)
324 row2 = next(x for x in rows if x['name'] == recipe2.name)
325
326 self.assertEqual(response.status_code, 200, 'should be 200 OK status')
327
328 # check other columns have been populated correctly
329 self.assertTrue(recipe1.name in row1['name'])
330 self.assertTrue(recipe1.version in row1['version'])
331 self.assertTrue(recipe1.description in
332 row1['get_description_or_summary'])
333
334 self.assertTrue(recipe1.layer_version.layer.name in
335 row1['layer_version__layer__name'])
336
337 self.assertTrue(recipe2.name in row2['name'])
338 self.assertTrue(recipe2.version in row2['version'])
339 self.assertTrue(recipe2.description in
340 row2['get_description_or_summary'])
341
342 self.assertTrue(recipe2.layer_version.layer.name in
343 row2['layer_version__layer__name'])
344
345 def test_toaster_tables(self):
346 """Test all ToasterTables instances"""
347
348 def get_data(table, options={}):
349 """Send a request and parse the json response"""
350 options['format'] = "json"
351 options['nocache'] = "true"
352 request = RequestFactory().get('/', options)
353
354 # This is the image recipe needed for a package list for
355 # PackagesTable do this here to throw a non exist exception
356 image_recipe = Recipe.objects.get(pk=4)
357
358 # Add any kwargs that are needed by any of the possible tables
359 args = {'pid': self.project.id,
360 'layerid': self.lver.pk,
361 'recipeid': self.recipe1.pk,
362 'recipe_id': image_recipe.pk,
363 'custrecipeid': self.customr.pk,
364 'build_id': 1,
365 'target_id': 1}
366
367 response = table.get(request, **args)
368 return json.loads(response.content.decode('utf-8'))
369
370 def get_text_from_td(td):
371 """If we have html in the td then extract the text portion"""
372 # just so we don't waste time parsing non html
373 if "<" not in td:
374 ret = td
375 else:
376 ret = BeautifulSoup(td, "html.parser").text
377
378 if len(ret):
379 return "0"
380 else:
381 return ret
382
383 # Get a list of classes in tables module
384 tables = inspect.getmembers(toastergui.tables, inspect.isclass)
385 tables.extend(inspect.getmembers(toastergui.buildtables,
386 inspect.isclass))
387
388 for name, table_cls in tables:
389 # Filter out the non ToasterTables from the tables module
390 if not issubclass(table_cls, toastergui.widgets.ToasterTable) or \
391 table_cls == toastergui.widgets.ToasterTable or \
392 'Mixin' in name:
393 continue
394
395 # Get the table data without any options, this also does the
396 # initialisation of the table i.e. setup_columns,
397 # setup_filters and setup_queryset that we can use later
398 table = table_cls()
399 all_data = get_data(table)
400
401 self.assertTrue(len(all_data['rows']) > 1,
402 "Cannot test on a %s table with < 1 row" % name)
403
404 if table.default_orderby:
405 row_one = get_text_from_td(
406 all_data['rows'][0][table.default_orderby.strip("-")])
407 row_two = get_text_from_td(
408 all_data['rows'][1][table.default_orderby.strip("-")])
409
410 if '-' in table.default_orderby:
411 self.assertTrue(row_one >= row_two,
412 "Default ordering not working on %s"
413 " '%s' should be >= '%s'" %
414 (name, row_one, row_two))
415 else:
416 self.assertTrue(row_one <= row_two,
417 "Default ordering not working on %s"
418 " '%s' should be <= '%s'" %
419 (name, row_one, row_two))
420
421 # Test the column ordering and filtering functionality
422 for column in table.columns:
423 if column['orderable']:
424 # If a column is orderable test it in both order
425 # directions ordering on the columns field_name
426 ascending = get_data(table_cls(),
427 {"orderby": column['field_name']})
428
429 row_one = get_text_from_td(
430 ascending['rows'][0][column['field_name']])
431 row_two = get_text_from_td(
432 ascending['rows'][1][column['field_name']])
433
434 self.assertTrue(row_one <= row_two,
435 "Ascending sort applied but row 0: \"%s\""
436 " is less than row 1: \"%s\" "
437 "%s %s " %
438 (row_one, row_two,
439 column['field_name'], name))
440
441 descending = get_data(table_cls(),
442 {"orderby":
443 '-'+column['field_name']})
444
445 row_one = get_text_from_td(
446 descending['rows'][0][column['field_name']])
447 row_two = get_text_from_td(
448 descending['rows'][1][column['field_name']])
449
450 self.assertTrue(row_one >= row_two,
451 "Descending sort applied but row 0: %s"
452 "is greater than row 1: %s"
453 "field %s table %s" %
454 (row_one,
455 row_two,
456 column['field_name'], name))
457
458 # If the two start rows are the same we haven't actually
459 # changed the order
460 self.assertNotEqual(ascending['rows'][0],
461 descending['rows'][0],
462 "An orderby %s has not changed the "
463 "order of the data in table %s" %
464 (column['field_name'], name))
465
466 if column['filter_name']:
467 # If a filter is available for the column get the filter
468 # info. This contains what filter actions are defined.
469 filter_info = get_data(table_cls(),
470 {"cmd": "filterinfo",
471 "name": column['filter_name']})
472 self.assertTrue(len(filter_info['filter_actions']) > 0,
473 "Filter %s was defined but no actions "
474 "added to it" % column['filter_name'])
475
476 for filter_action in filter_info['filter_actions']:
477 # filter string to pass as the option
478 # This is the name of the filter:action
479 # e.g. project_filter:not_in_project
480 filter_string = "%s:%s" % (
481 column['filter_name'],
482 filter_action['action_name'])
483 # Now get the data with the filter applied
484 filtered_data = get_data(table_cls(),
485 {"filter": filter_string})
486
487 # date range filter actions can't specify the
488 # number of results they return, so their count is 0
489 if filter_action['count'] is not None:
490 self.assertEqual(
491 len(filtered_data['rows']),
492 int(filter_action['count']),
493 "We added a table filter for %s but "
494 "the number of rows returned was not "
495 "what the filter info said there "
496 "would be" % name)
497
498 # Test search functionality on the table
499 something_found = False
500 for search in list(string.ascii_letters):
501 search_data = get_data(table_cls(), {'search': search})
502
503 if len(search_data['rows']) > 0:
504 something_found = True
505 break
506
507 self.assertTrue(something_found,
508 "We went through the whole alphabet and nothing"
509 " was found for the search of table %s" % name)
510
511 # Test the limit functionality on the table
512 limited_data = get_data(table_cls(), {'limit': "1"})
513 self.assertEqual(len(limited_data['rows']),
514 1,
515 "Limit 1 set on table %s but not 1 row returned"
516 % name)
517
518 # Test the pagination functionality on the table
519 page_one_data = get_data(table_cls(), {'limit': "1",
520 "page": "1"})['rows'][0]
521
522 page_two_data = get_data(table_cls(), {'limit': "1",
523 "page": "2"})['rows'][0]
524
525 self.assertNotEqual(page_one_data,
526 page_two_data,
527 "Changed page on table %s but first row is"
528 " the same as the previous page" % name)