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