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