test_importer.py revision 68558223091c765d0cef92438f09c3f9918fa3fc
1#!/usr/bin/python 2# 3# Copyright 2008 Google Inc. All Rights Reserved. 4""" 5This utility allows for easy updating, removing and importing 6of tests into the autotest_web autotests table. 7 8Example of updating client side tests: 9./tests.py -t /usr/local/autotest/client/tests 10 11If for example not all of your control files adhere to the standard outlined at 12http://test.kernel.org/autotest/ControlRequirements 13 14You can force options: 15./tests.py --test-type server -t /usr/local/autotest/server/tests 16 17 18Most options should be fairly self explanatory use --help to display them. 19""" 20 21 22import logging, time, re, os, MySQLdb, sys, optparse, compiler 23import common 24from autotest_lib.client.common_lib import control_data, test, global_config 25from autotest_lib.client.common_lib import utils 26 27 28logging.basicConfig(logging.DEBUG) 29# Global 30DRY_RUN = False 31DEPENDENCIES_NOT_FOUND = set() 32 33def main(argv): 34 """Main function""" 35 global DRY_RUN 36 parser = optparse.OptionParser() 37 parser.add_option('-c', '--db-clear-tests', 38 dest='clear_tests', action='store_true', 39 default=False, 40 help='Clear client and server tests with invalid control files') 41 parser.add_option('-d', '--dry-run', 42 dest='dry_run', action='store_true', default=False, 43 help='Dry run for operation') 44 parser.add_option('-A', '--add-all', 45 dest='add_all', action='store_true', 46 default=False, 47 help='Add samples, site_tests, tests, and test_suites') 48 parser.add_option('-E', '--add-experimental', 49 dest='add_experimental', action='store_true', 50 default=True, 51 help='Add experimental tests to frontend') 52 parser.add_option('-N', '--add-noncompliant', 53 dest='add_noncompliant', action='store_true', 54 default=False, 55 help='Skip any tests that are not compliant') 56 parser.add_option('-p', '--profile-dir', dest='profile_dir', 57 help='Directory to recursively check for profiles') 58 parser.add_option('-t', '--tests-dir', dest='tests_dir', 59 help='Directory to recursively check for control.*') 60 parser.add_option('-r', '--control-pattern', dest='control_pattern', 61 default='^control.*', 62 help='The pattern to look for in directories for control files') 63 parser.add_option('-v', '--verbose', 64 dest='verbose', action='store_true', default=False, 65 help='Run in verbose mode') 66 parser.add_option('-z', '--autotest_dir', dest='autotest_dir', 67 default=os.path.join(os.path.dirname(__file__), '..'), 68 help='Autotest directory root') 69 options, args = parser.parse_args() 70 DRY_RUN = options.dry_run 71 # Make sure autotest_dir is the absolute path 72 options.autotest_dir = os.path.abspath(options.autotest_dir) 73 74 if len(args) > 0: 75 print "Invalid option(s) provided: ", args 76 parser.print_help() 77 return 1 78 79 if len(argv) == 1: 80 update_all(options.autotest_dir, options.add_noncompliant, 81 options.add_experimental, options.verbose) 82 db_clean_broken(options.autotest_dir, options.verbose) 83 return 0 84 85 if options.add_all: 86 update_all(options.autotest_dir, options.add_noncompliant, 87 options.add_experimental, options.verbose) 88 if options.clear_tests: 89 db_clean_broken(options.autotest_dir, options.verbose) 90 if options.tests_dir: 91 if ".." in options.tests_dir: 92 path = os.path.join(os.getcwd(), options.tests_dir) 93 options.tests_dir = os.path.abspath(path) 94 tests = get_tests_from_fs(options.tests_dir, options.control_pattern, 95 add_noncompliant=options.add_noncompliant) 96 update_tests_in_db(tests, add_experimental=options.add_experimental, 97 add_noncompliant=options.add_noncompliant, 98 autotest_dir=options.autotest_dir, 99 verbose=options.verbose) 100 if options.profile_dir: 101 profilers = get_tests_from_fs(options.profile_dir, '.*py$') 102 update_profilers_in_db(profilers, verbose=options.verbose, 103 add_noncompliant=options.add_noncompliant, 104 description='NA') 105 106 107def update_all(autotest_dir, add_noncompliant, add_experimental, verbose): 108 """Function to scan through all tests and add them to the database.""" 109 for path in [ 'server/tests', 'server/site_tests', 'client/tests', 110 'client/site_tests']: 111 test_path = os.path.join(autotest_dir, path) 112 if not os.path.exists(test_path): 113 continue 114 if verbose: 115 print "Scanning " + test_path 116 tests = [] 117 tests = get_tests_from_fs(test_path, "^control.*", 118 add_noncompliant=add_noncompliant) 119 update_tests_in_db(tests, add_experimental=add_experimental, 120 add_noncompliant=add_noncompliant, 121 autotest_dir=autotest_dir, 122 verbose=verbose) 123 test_suite_path = os.path.join(autotest_dir, 'test_suites') 124 if os.path.exists(test_suite_path): 125 if verbose: 126 print "Scanning " + test_suite_path 127 tests = get_tests_from_fs(test_suite_path, '.*', 128 add_noncompliant=add_noncompliant) 129 update_tests_in_db(tests, add_experimental=add_experimental, 130 add_noncompliant=add_noncompliant, 131 autotest_dir=autotest_dir, 132 verbose=verbose) 133 sample_path = os.path.join(autotest_dir, 'server/samples') 134 if os.path.exists(sample_path): 135 if verbose: 136 print "Scanning " + sample_path 137 tests = get_tests_from_fs(sample_path, '.*srv$', 138 add_noncompliant=add_noncompliant) 139 update_tests_in_db(tests, add_experimental=add_experimental, 140 add_noncompliant=add_noncompliant, 141 autotest_dir=autotest_dir, 142 verbose=verbose) 143 144 profilers_path = os.path.join(autotest_dir, "client/profilers") 145 if os.path.exists(profilers_path): 146 if verbose: 147 print "Scanning " + profilers_path 148 profilers = get_tests_from_fs(profilers_path, '.*py$') 149 update_profilers_in_db(profilers, verbose=verbose, 150 add_noncompliant=add_noncompliant, 151 description='NA') 152 # Clean bad db entries 153 db_clean_broken(autotest_dir, verbose) 154 155 156def db_clean_broken(autotest_dir, verbose): 157 """Remove tests from autotest_web that do not have valid control files 158 159 Arguments: 160 tests: a list of control file relative paths used as keys for deletion. 161 """ 162 connection=db_connect() 163 cursor = connection.cursor() 164 # Get tests 165 sql = "SELECT id, path FROM autotests"; 166 cursor.execute(sql) 167 results = cursor.fetchall() 168 for test_id, path in results: 169 full_path = os.path.join(autotest_dir, path) 170 if not os.path.isfile(full_path): 171 if verbose: 172 print "Removing " + path 173 db_execute(cursor, "DELETE FROM autotests WHERE id=%s" % test_id) 174 db_execute(cursor, "DELETE FROM autotests_dependency_labels WHERE " 175 "test_id=%s" % test_id) 176 177 # Find profilers that are no longer present 178 profilers = [] 179 sql = "SELECT name FROM profilers" 180 cursor.execute(sql) 181 results = cursor.fetchall() 182 for path in results: 183 full_path = os.path.join(autotest_dir, "client/profilers", path[0]) 184 if not os.path.exists(full_path): 185 if verbose: 186 print "Removing " + path[0] 187 sql = "DELETE FROM profilers WHERE name='%s'" % path[0] 188 db_execute(cursor, sql) 189 190 191 connection.commit() 192 connection.close() 193 194 195def update_profilers_in_db(profilers, verbose=False, description='NA', 196 add_noncompliant=False): 197 """Update profilers in autotest_web database""" 198 connection=db_connect() 199 cursor = connection.cursor() 200 for profiler in profilers: 201 name = os.path.basename(profiler).rstrip(".py") 202 if not profilers[profiler]: 203 if add_noncompliant: 204 doc = description 205 else: 206 print "Skipping %s, missing docstring" % profiler 207 else: 208 doc = profilers[profiler] 209 # check if test exists 210 sql = "SELECT name FROM profilers WHERE name='%s'" % name 211 cursor.execute(sql) 212 results = cursor.fetchall() 213 if results: 214 sql = "UPDATE profilers SET name='%s', description='%s' "\ 215 "WHERE name='%s'" 216 sql %= (MySQLdb.escape_string(name), MySQLdb.escape_string(doc), 217 MySQLdb.escape_string(name)) 218 else: 219 # Insert newly into DB 220 sql = "INSERT into profilers (name, description) VALUES('%s', '%s')" 221 sql %= (MySQLdb.escape_string(name), MySQLdb.escape_string(doc)) 222 223 db_execute(cursor, sql) 224 225 connection.commit() 226 connection.close() 227 228 229def update_tests_in_db(tests, dry_run=False, add_experimental=False, 230 add_noncompliant=False, verbose=False, 231 autotest_dir=None): 232 """Update or add each test to the database""" 233 connection=db_connect() 234 cursor = connection.cursor() 235 new_test_dicts = [] 236 for test in tests: 237 new_test = {} 238 new_test['path'] = test.replace(autotest_dir, '').lstrip('/') 239 if verbose: 240 print "Processing " + new_test['path'] 241 # Create a name for the test 242 for key in dir(tests[test]): 243 if not key.startswith('__'): 244 value = getattr(tests[test], key) 245 if not callable(value): 246 new_test[key] = value 247 # This only takes place if --add-noncompliant is provided on the CLI 248 if 'name' not in new_test: 249 test_new_test = test.split('/') 250 if test_new_test[-1] == 'control': 251 new_test['name'] = test_new_test[-2] 252 else: 253 control_name = "%s:%s" 254 control_name %= (test_new_test[-2], 255 test_new_test[-1]) 256 new_test['name'] = control_name.replace('control.', '') 257 # Experimental Check 258 if not add_experimental: 259 if int(new_test['experimental']): 260 continue 261 # clean tests for insertion into db 262 new_test = dict_db_clean(new_test) 263 new_test_dicts.append(new_test) 264 sql = "SELECT name,path FROM autotests WHERE path='%s' LIMIT 1" 265 sql %= new_test['path'] 266 cursor.execute(sql) 267 # check for entries already in existence 268 results = cursor.fetchall() 269 if results: 270 sql = ("UPDATE autotests SET name='%s', test_class='%s'," 271 "description='%s', test_type=%d, path='%s'," 272 "author='%s', dependencies='%s'," 273 "experimental=%d, run_verify=%d, test_time=%d," 274 "test_category='%s', sync_count=%d" 275 " WHERE path='%s'") 276 sql %= (new_test['name'], new_test['test_class'], new_test['doc'], 277 int(new_test['test_type']), new_test['path'], 278 new_test['author'], 279 new_test['dependencies'], int(new_test['experimental']), 280 int(new_test['run_verify']), new_test['time'], 281 new_test['test_category'], new_test['sync_count'], 282 new_test['path']) 283 else: 284 # Create a relative path 285 path = test.replace(autotest_dir, '') 286 sql = ("INSERT INTO autotests" 287 "(name, test_class, description, test_type, path, " 288 "author, dependencies, experimental, " 289 "run_verify, test_time, test_category, sync_count) " 290 "VALUES('%s','%s','%s',%d,'%s','%s','%s',%d,%d,%d," 291 "'%s',%d)") 292 sql %= (new_test['name'], new_test['test_class'], new_test['doc'], 293 int(new_test['test_type']), new_test['path'], 294 new_test['author'], new_test['dependencies'], 295 int(new_test['experimental']), int(new_test['run_verify']), 296 new_test['time'], new_test['test_category'], 297 new_test['sync_count']) 298 299 db_execute(cursor, sql) 300 301 add_label_dependencies(new_test_dicts, cursor) 302 303 connection.commit() 304 connection.close() 305 306 307def dict_db_clean(test): 308 """Take a tests dictionary from update_db and make it pretty for SQL""" 309 310 test_type = { 'client' : 1, 311 'server' : 2, } 312 test_time = { 'short' : 1, 313 'medium' : 2, 314 'long' : 3, } 315 316 test['name'] = MySQLdb.escape_string(test['name']) 317 test['author'] = MySQLdb.escape_string(test['author']) 318 test['test_class'] = MySQLdb.escape_string(test['test_class']) 319 test['test_category'] = MySQLdb.escape_string(test['test_category']) 320 test['doc'] = MySQLdb.escape_string(test['doc']) 321 test['dependencies'] = ", ".join(test['dependencies']) 322 # TODO Fix when we move from synch_type to sync_count 323 if test['sync_count'] == 1: 324 test['synch_type'] = 1 325 else: 326 test['synch_type'] = 2 327 try: 328 test['test_type'] = int(test['test_type']) 329 if test['test_type'] != 1 and test['test_type'] != 2: 330 raise Exception('Incorrect number %d for test_type' % 331 test['test_type']) 332 except ValueError: 333 pass 334 try: 335 test['time'] = int(test['time']) 336 if test['time'] < 1 or test['time'] > 3: 337 raise Exception('Incorrect number %d for time' % 338 test['time']) 339 except ValueError: 340 pass 341 342 if str == type(test['time']): 343 test['time'] = test_time[test['time'].lower()] 344 if str == type(test['test_type']): 345 test['test_type'] = test_type[test['test_type'].lower()] 346 347 return test 348 349 350def add_label_dependencies(tests, cursor): 351 """ 352 Look at the DEPENDENCIES field for each test and add the proper many-to-many 353 relationships. 354 """ 355 label_name_to_id = get_id_map(cursor, 'labels', 'name') 356 test_path_to_id = get_id_map(cursor, 'autotests', 'path') 357 358 # clear out old relationships 359 test_ids = ','.join(str(test_path_to_id[test['path']]) 360 for test in tests) 361 db_execute(cursor, 362 'DELETE FROM autotests_dependency_labels WHERE test_id IN (%s)' % 363 test_ids) 364 365 value_pairs = [] 366 for test in tests: 367 test_id = test_path_to_id[test['path']] 368 for label_name in test['dependencies'].split(','): 369 label_name = label_name.strip().lower() 370 if not label_name: 371 continue 372 if label_name not in label_name_to_id: 373 log_dependency_not_found(label_name) 374 continue 375 label_id = label_name_to_id[label_name] 376 value_pairs.append('(%s, %s)' % (test_id, label_id)) 377 378 if not value_pairs: 379 return 380 381 query = ('INSERT INTO autotests_dependency_labels (test_id, label_id) ' 382 'VALUES ' + ','.join(value_pairs)) 383 db_execute(cursor, query) 384 385 386def log_dependency_not_found(label_name): 387 if label_name in DEPENDENCIES_NOT_FOUND: 388 return 389 print 'Dependency %s not found' % label_name 390 DEPENDENCIES_NOT_FOUND.add(label_name) 391 392 393def get_id_map(cursor, table_name, name_field): 394 cursor.execute('SELECT id, %s FROM %s' % (name_field, table_name)) 395 name_to_id = {} 396 for item_id, item_name in cursor.fetchall(): 397 name_to_id[item_name] = item_id 398 return name_to_id 399 400 401def get_tests_from_fs(parent_dir, control_pattern, add_noncompliant=False): 402 """Find control jobs in location and create one big job 403 Returns: 404 dictionary of the form: 405 tests[file_path] = parsed_object 406 407 """ 408 tests = {} 409 profilers = False 410 if 'client/profilers' in parent_dir: 411 profilers = True 412 for dir in [ parent_dir ]: 413 files = recursive_walk(dir, control_pattern) 414 for file in files: 415 if '__init__.py' in file or '.svn' in file: 416 continue 417 if not profilers: 418 if not add_noncompliant: 419 try: 420 found_test = control_data.parse_control(file, 421 raise_warnings=True) 422 tests[file] = found_test 423 except control_data.ControlVariableException, e: 424 print "Skipping %s\n%s" % (file, e) 425 pass 426 else: 427 found_test = control_data.parse_control(file) 428 tests[file] = found_test 429 else: 430 script = file.rstrip(".py") 431 tests[file] = compiler.parseFile(file).doc 432 return tests 433 434 435def recursive_walk(path, wildcard): 436 """Recurisvely go through a directory. 437 Returns: 438 A list of files that match wildcard 439 """ 440 files = [] 441 directories = [ path ] 442 while len(directories)>0: 443 directory = directories.pop() 444 for name in os.listdir(directory): 445 fullpath = os.path.join(directory, name) 446 if os.path.isfile(fullpath): 447 # if we are a control file 448 if re.search(wildcard, name): 449 files.append(fullpath) 450 elif os.path.isdir(fullpath): 451 directories.append(fullpath) 452 return files 453 454 455def db_connect(): 456 """Connect to the AUTOTEST_WEB database and return a connect object.""" 457 c = global_config.global_config 458 db_host = c.get_config_value('AUTOTEST_WEB', 'host') 459 db_name = c.get_config_value('AUTOTEST_WEB', 'database') 460 username = c.get_config_value('AUTOTEST_WEB', 'user') 461 password = c.get_config_value('AUTOTEST_WEB', 'password') 462 connection = MySQLdb.connect(host=db_host, db=db_name, 463 user=username, 464 passwd=password) 465 return connection 466 467 468def db_execute(cursor, sql): 469 """Execute SQL or print out what would be executed if dry_run is defined""" 470 471 if DRY_RUN: 472 print "Would run: " + sql 473 else: 474 cursor.execute(sql) 475 476 477if __name__ == "__main__": 478 main(sys.argv) 479