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 afe_autotests table. 7 8Example of updating client side tests: 9./test_importer.py -t /usr/local/autotest/client/tests 10 11If, for example, not all of your control files adhere to the standard outlined 12at http://autotest.kernel.org/wiki/ControlRequirements, you can force options: 13 14./test_importer.py --test-type server -t /usr/local/autotest/server/tests 15 16You would need to pass --add-noncompliant to include such control files, 17however. An easy way to check for compliance is to run in dry mode: 18 19./test_importer.py --dry-run -t /usr/local/autotest/server/tests/mytest 20 21Or to check a single control file, you can use check_control_file_vars.py. 22 23Running with no options is equivalent to --add-all --db-clear-tests. 24 25Most options should be fairly self explanatory, use --help to display them. 26""" 27 28 29import common 30import logging, re, os, sys, optparse, compiler 31 32from autotest_lib.frontend import setup_django_environment 33from autotest_lib.frontend.afe import models 34from autotest_lib.client.common_lib import control_data 35from autotest_lib.client.common_lib import logging_config, logging_manager 36 37 38class TestImporterLoggingConfig(logging_config.LoggingConfig): 39 #pylint: disable-msg=C0111 40 def configure_logging(self, results_dir=None, verbose=False): 41 super(TestImporterLoggingConfig, self).configure_logging( 42 use_console=True, 43 verbose=verbose) 44 45 46# Global 47DRY_RUN = False 48DEPENDENCIES_NOT_FOUND = set() 49 50 51def update_all(autotest_dir, add_noncompliant, add_experimental): 52 """ 53 Function to scan through all tests and add them to the database. 54 55 This function invoked when no parameters supplied to the command line. 56 It 'synchronizes' the test database with the current contents of the 57 client and server test directories. When test code is discovered 58 in the file system new tests may be added to the db. Likewise, 59 if test code is not found in the filesystem, tests may be removed 60 from the db. The base test directories are hard-coded to client/tests, 61 client/site_tests, server/tests and server/site_tests. 62 63 @param autotest_dir: prepended to path strings (/usr/local/autotest). 64 @param add_noncompliant: attempt adding test with invalid control files. 65 @param add_experimental: add tests with experimental attribute set. 66 """ 67 for path in [ 'server/tests', 'server/site_tests', 'client/tests', 68 'client/site_tests']: 69 test_path = os.path.join(autotest_dir, path) 70 if not os.path.exists(test_path): 71 continue 72 logging.info("Scanning %s", test_path) 73 tests = [] 74 tests = get_tests_from_fs(test_path, "^control.*", 75 add_noncompliant=add_noncompliant) 76 update_tests_in_db(tests, add_experimental=add_experimental, 77 add_noncompliant=add_noncompliant, 78 autotest_dir=autotest_dir) 79 test_suite_path = os.path.join(autotest_dir, 'test_suites') 80 if os.path.exists(test_suite_path): 81 logging.info("Scanning %s", test_suite_path) 82 tests = get_tests_from_fs(test_suite_path, '.*', 83 add_noncompliant=add_noncompliant) 84 update_tests_in_db(tests, add_experimental=add_experimental, 85 add_noncompliant=add_noncompliant, 86 autotest_dir=autotest_dir) 87 88 profilers_path = os.path.join(autotest_dir, "client/profilers") 89 if os.path.exists(profilers_path): 90 logging.info("Scanning %s", profilers_path) 91 profilers = get_tests_from_fs(profilers_path, '.*py$') 92 update_profilers_in_db(profilers, add_noncompliant=add_noncompliant, 93 description='NA') 94 # Clean bad db entries 95 db_clean_broken(autotest_dir) 96 97 98def update_samples(autotest_dir, add_noncompliant, add_experimental): 99 """ 100 Add only sample tests to the database from the filesystem. 101 102 This function invoked when -S supplied on command line. 103 Only adds tests to the database - does not delete any. 104 Samples tests are formatted slightly differently than other tests. 105 106 @param autotest_dir: prepended to path strings (/usr/local/autotest). 107 @param add_noncompliant: attempt adding test with invalid control files. 108 @param add_experimental: add tests with experimental attribute set. 109 """ 110 sample_path = os.path.join(autotest_dir, 'server/samples') 111 if os.path.exists(sample_path): 112 logging.info("Scanning %s", sample_path) 113 tests = get_tests_from_fs(sample_path, '.*srv$', 114 add_noncompliant=add_noncompliant) 115 update_tests_in_db(tests, add_experimental=add_experimental, 116 add_noncompliant=add_noncompliant, 117 autotest_dir=autotest_dir) 118 119 120def db_clean_broken(autotest_dir): 121 """ 122 Remove tests from autotest_web that do not have valid control files 123 124 This function invoked when -c supplied on the command line and when 125 running update_all(). Removes tests from database which are not 126 found in the filesystem. Also removes profilers which are just 127 a special case of tests. 128 129 @param autotest_dir: prepended to path strings (/usr/local/autotest). 130 """ 131 for test in models.Test.objects.all(): 132 full_path = os.path.join(autotest_dir, test.path) 133 if not os.path.isfile(full_path): 134 logging.info("Removing %s", test.path) 135 _log_or_execute(repr(test), test.delete) 136 137 # Find profilers that are no longer present 138 for profiler in models.Profiler.objects.all(): 139 full_path = os.path.join(autotest_dir, "client", "profilers", 140 profiler.name) 141 if not os.path.exists(full_path): 142 logging.info("Removing %s", profiler.name) 143 _log_or_execute(repr(profiler), profiler.delete) 144 145 146def db_clean_all(autotest_dir): 147 """ 148 Remove all tests from autotest_web - very destructive 149 150 This function invoked when -C supplied on the command line. 151 Removes ALL tests from the database. 152 153 @param autotest_dir: prepended to path strings (/usr/local/autotest). 154 """ 155 for test in models.Test.objects.all(): 156 full_path = os.path.join(autotest_dir, test.path) 157 logging.info("Removing %s", test.path) 158 _log_or_execute(repr(test), test.delete) 159 160 # Find profilers that are no longer present 161 for profiler in models.Profiler.objects.all(): 162 full_path = os.path.join(autotest_dir, "client", "profilers", 163 profiler.name) 164 logging.info("Removing %s", profiler.name) 165 _log_or_execute(repr(profiler), profiler.delete) 166 167 168def update_profilers_in_db(profilers, description='NA', 169 add_noncompliant=False): 170 """ 171 Add only profilers to the database from the filesystem. 172 173 This function invoked when -p supplied on command line. 174 Only adds profilers to the database - does not delete any. 175 Profilers are formatted slightly differently than tests. 176 177 @param profilers: list of profilers found in the file system. 178 @param description: simple text to satisfy docstring. 179 @param add_noncompliant: attempt adding test with invalid control files. 180 """ 181 for profiler in profilers: 182 name = os.path.basename(profiler) 183 if name.endswith('.py'): 184 name = name[:-3] 185 if not profilers[profiler]: 186 if add_noncompliant: 187 doc = description 188 else: 189 logging.warning("Skipping %s, missing docstring", profiler) 190 continue 191 else: 192 doc = profilers[profiler] 193 194 model = models.Profiler.objects.get_or_create(name=name)[0] 195 model.description = doc 196 _log_or_execute(repr(model), model.save) 197 198 199def _set_attributes_custom(test, data): 200 # We set the test name to the dirname of the control file. 201 test_new_name = test.path.split('/') 202 if test_new_name[-1] == 'control' or test_new_name[-1] == 'control.srv': 203 test.name = test_new_name[-2] 204 else: 205 control_name = "%s:%s" 206 control_name %= (test_new_name[-2], 207 test_new_name[-1]) 208 test.name = re.sub('control.*\.', '', control_name) 209 210 # We set verify to always False (0). 211 test.run_verify = 0 212 213 if hasattr(data, 'test_parameters'): 214 for para_name in data.test_parameters: 215 test_parameter = models.TestParameter.objects.get_or_create( 216 test=test, name=para_name)[0] 217 test_parameter.save() 218 219 220def update_tests_in_db(tests, dry_run=False, add_experimental=False, 221 add_noncompliant=False, autotest_dir=None): 222 """ 223 Scans through all tests and add them to the database. 224 225 This function invoked when -t supplied and for update_all. 226 When test code is discovered in the file system new tests may be added 227 228 @param tests: list of tests found in the filesystem. 229 @param dry_run: not used at this time. 230 @param add_experimental: add tests with experimental attribute set. 231 @param add_noncompliant: attempt adding test with invalid control files. 232 @param autotest_dir: prepended to path strings (/usr/local/autotest). 233 """ 234 for test in tests: 235 new_test = models.Test.objects.get_or_create( 236 path=test.replace(autotest_dir, '').lstrip('/'))[0] 237 logging.info("Processing %s", new_test.path) 238 239 # Set the test's attributes 240 data = tests[test] 241 _set_attributes_clean(new_test, data) 242 243 # Custom Attribute Update 244 _set_attributes_custom(new_test, data) 245 246 # This only takes place if --add-noncompliant is provided on the CLI 247 if not new_test.name: 248 test_new_test = test.split('/') 249 if test_new_test[-1] == 'control': 250 new_test.name = test_new_test[-2] 251 else: 252 control_name = "%s:%s" 253 control_name %= (test_new_test[-2], 254 test_new_test[-1]) 255 new_test.name = control_name.replace('control.', '') 256 257 # Experimental Check 258 if not add_experimental and new_test.experimental: 259 continue 260 261 _log_or_execute(repr(new_test), new_test.save) 262 add_label_dependencies(new_test) 263 264 # save TestParameter 265 for para_name in data.test_parameters: 266 test_parameter = models.TestParameter.objects.get_or_create( 267 test=new_test, name=para_name)[0] 268 test_parameter.save() 269 270 271def _set_attributes_clean(test, data): 272 """ 273 First pass sets the attributes of the Test object from file system. 274 275 @param test: a test object to be populated for the database. 276 @param data: object with test data from the file system. 277 """ 278 test_time = { 'short' : 1, 279 'medium' : 2, 280 'long' : 3, } 281 282 283 string_attributes = ('name', 'author', 'test_class', 'test_category', 284 'test_category', 'sync_count') 285 for attribute in string_attributes: 286 setattr(test, attribute, getattr(data, attribute)) 287 288 test.description = data.doc 289 test.dependencies = ", ".join(data.dependencies) 290 291 try: 292 test.test_type = control_data.CONTROL_TYPE.get_value(data.test_type) 293 except AttributeError: 294 raise Exception('Unknown test_type %s for test %s', data.test_type, 295 data.name) 296 297 int_attributes = ('experimental', 'run_verify') 298 for attribute in int_attributes: 299 setattr(test, attribute, int(getattr(data, attribute))) 300 301 try: 302 test.test_time = int(data.time) 303 if test.test_time < 1 or test.time > 3: 304 raise Exception('Incorrect number %d for time' % test.time) 305 except ValueError: 306 pass 307 308 if not test.test_time and str == type(data.time): 309 test.test_time = test_time[data.time.lower()] 310 311 test.test_retry = data.retries 312 313 314def add_label_dependencies(test): 315 """ 316 Add proper many-to-many relationships from DEPENDENCIES field. 317 318 @param test: test object for the database. 319 """ 320 321 # clear out old relationships 322 _log_or_execute(repr(test), test.dependency_labels.clear, 323 subject='clear dependencies from') 324 325 for label_name in test.dependencies.split(','): 326 label_name = label_name.strip().lower() 327 if not label_name: 328 continue 329 330 try: 331 label = models.Label.objects.get(name=label_name) 332 except models.Label.DoesNotExist: 333 log_dependency_not_found(label_name) 334 continue 335 336 _log_or_execute(repr(label), test.dependency_labels.add, label, 337 subject='add dependency to %s' % test.name) 338 339 340def log_dependency_not_found(label_name): 341 """ 342 Exception processing when label not found in database. 343 344 @param label_name: from test dependencies. 345 """ 346 if label_name in DEPENDENCIES_NOT_FOUND: 347 return 348 logging.info("Dependency %s not found", label_name) 349 DEPENDENCIES_NOT_FOUND.add(label_name) 350 351 352def get_tests_from_fs(parent_dir, control_pattern, add_noncompliant=False): 353 """ 354 Find control files in file system and load a list with their info. 355 356 @param parent_dir: directory to search recursively. 357 @param control_pattern: name format of control file. 358 @param add_noncompliant: ignore control file parse errors. 359 360 @return dictionary of the form: tests[file_path] = parsed_object 361 """ 362 tests = {} 363 profilers = False 364 if 'client/profilers' in parent_dir: 365 profilers = True 366 for dir in [ parent_dir ]: 367 files = recursive_walk(dir, control_pattern) 368 for file in files: 369 if '__init__.py' in file or '.svn' in file: 370 continue 371 if not profilers: 372 if not add_noncompliant: 373 try: 374 found_test = control_data.parse_control(file, 375 raise_warnings=True) 376 tests[file] = found_test 377 except control_data.ControlVariableException, e: 378 logging.warning("Skipping %s\n%s", file, e) 379 except Exception, e: 380 logging.error("Bad %s\n%s", file, e) 381 else: 382 found_test = control_data.parse_control(file) 383 tests[file] = found_test 384 else: 385 tests[file] = compiler.parseFile(file).doc 386 return tests 387 388 389def recursive_walk(path, wildcard): 390 """ 391 Recursively go through a directory. 392 393 This function invoked by get_tests_from_fs(). 394 395 @param path: base directory to start search. 396 @param wildcard: name format to match. 397 398 @return A list of files that match wildcard 399 """ 400 files = [] 401 directories = [ path ] 402 while len(directories)>0: 403 directory = directories.pop() 404 for name in os.listdir(directory): 405 fullpath = os.path.join(directory, name) 406 if os.path.isfile(fullpath): 407 # if we are a control file 408 if re.search(wildcard, name): 409 files.append(fullpath) 410 elif os.path.isdir(fullpath): 411 directories.append(fullpath) 412 return files 413 414 415def _log_or_execute(content, func, *args, **kwargs): 416 """ 417 Log a message if dry_run is enabled, or execute the given function. 418 419 Relies on the DRY_RUN global variable. 420 421 @param content: the actual log message. 422 @param func: function to execute if dry_run is not enabled. 423 @param subject: (Optional) The type of log being written. Defaults to 424 the name of the provided function. 425 """ 426 subject = kwargs.get('subject', func.__name__) 427 428 if DRY_RUN: 429 logging.info("Would %s: %s", subject, content) 430 else: 431 func(*args) 432 433 434def _create_whitelist_set(whitelist_path): 435 """ 436 Create a set with contents from a whitelist file for membership testing. 437 438 @param whitelist_path: full path to the whitelist file. 439 440 @return set with files listed one/line - newlines included. 441 """ 442 f = open(whitelist_path, 'r') 443 whitelist_set = set([line.strip() for line in f]) 444 f.close() 445 return whitelist_set 446 447 448def update_from_whitelist(whitelist_set, add_experimental, add_noncompliant, 449 autotest_dir): 450 """ 451 Scans through all tests in the whitelist and add them to the database. 452 453 This function invoked when -w supplied. 454 455 @param whitelist_set: set of tests in full-path form from a whitelist. 456 @param add_experimental: add tests with experimental attribute set. 457 @param add_noncompliant: attempt adding test with invalid control files. 458 @param autotest_dir: prepended to path strings (/usr/local/autotest). 459 """ 460 tests = {} 461 profilers = {} 462 for file_path in whitelist_set: 463 if file_path.find('client/profilers') == -1: 464 try: 465 found_test = control_data.parse_control(file_path, 466 raise_warnings=True) 467 tests[file_path] = found_test 468 except control_data.ControlVariableException, e: 469 logging.warning("Skipping %s\n%s", file, e) 470 else: 471 profilers[file_path] = compiler.parseFile(file_path).doc 472 473 if len(tests) > 0: 474 update_tests_in_db(tests, add_experimental=add_experimental, 475 add_noncompliant=add_noncompliant, 476 autotest_dir=autotest_dir) 477 if len(profilers) > 0: 478 update_profilers_in_db(profilers, add_noncompliant=add_noncompliant, 479 description='NA') 480 481 482def main(argv): 483 """Main function 484 @param argv: List of command line parameters. 485 """ 486 487 global DRY_RUN 488 parser = optparse.OptionParser() 489 parser.add_option('-c', '--db-clean-tests', 490 dest='clean_tests', action='store_true', 491 default=False, 492 help='Clean client and server tests with invalid control files') 493 parser.add_option('-C', '--db-clear-all-tests', 494 dest='clear_all_tests', action='store_true', 495 default=False, 496 help='Clear ALL client and server tests') 497 parser.add_option('-d', '--dry-run', 498 dest='dry_run', action='store_true', default=False, 499 help='Dry run for operation') 500 parser.add_option('-A', '--add-all', 501 dest='add_all', action='store_true', 502 default=False, 503 help='Add site_tests, tests, and test_suites') 504 parser.add_option('-S', '--add-samples', 505 dest='add_samples', action='store_true', 506 default=False, 507 help='Add samples.') 508 parser.add_option('-E', '--add-experimental', 509 dest='add_experimental', action='store_true', 510 default=True, 511 help='Add experimental tests to frontend, works only ' 512 'with -A (--add-all) option') 513 parser.add_option('-N', '--add-noncompliant', 514 dest='add_noncompliant', action='store_true', 515 default=False, 516 help='Add non-compliant tests (i.e. tests that do not ' 517 'define all required control variables), works ' 518 'only with -A (--add-all) option') 519 parser.add_option('-p', '--profile-dir', dest='profile_dir', 520 help='Directory to recursively check for profiles') 521 parser.add_option('-t', '--tests-dir', dest='tests_dir', 522 help='Directory to recursively check for control.*') 523 parser.add_option('-r', '--control-pattern', dest='control_pattern', 524 default='^control.*', 525 help='The pattern to look for in directories for control files') 526 parser.add_option('-v', '--verbose', 527 dest='verbose', action='store_true', default=False, 528 help='Run in verbose mode') 529 parser.add_option('-w', '--whitelist-file', dest='whitelist_file', 530 help='Filename for list of test names that must match') 531 parser.add_option('-z', '--autotest-dir', dest='autotest_dir', 532 default=os.path.join(os.path.dirname(__file__), '..'), 533 help='Autotest directory root') 534 options, args = parser.parse_args() 535 536 logging_manager.configure_logging(TestImporterLoggingConfig(), 537 verbose=options.verbose) 538 539 DRY_RUN = options.dry_run 540 if DRY_RUN: 541 logging.getLogger().setLevel(logging.WARN) 542 543 if len(argv) > 1 and options.add_noncompliant and not options.add_all: 544 logging.error('-N (--add-noncompliant) must be ran with option -A ' 545 '(--add-All).') 546 return 1 547 548 if len(argv) > 1 and options.add_experimental and not options.add_all: 549 logging.error('-E (--add-experimental) must be ran with option -A ' 550 '(--add-All).') 551 return 1 552 553 # Make sure autotest_dir is the absolute path 554 options.autotest_dir = os.path.abspath(options.autotest_dir) 555 556 if len(args) > 0: 557 logging.error("Invalid option(s) provided: %s", args) 558 parser.print_help() 559 return 1 560 561 if options.verbose: 562 logging.getLogger().setLevel(logging.DEBUG) 563 564 if len(argv) == 1 or (len(argv) == 2 and options.verbose): 565 update_all(options.autotest_dir, options.add_noncompliant, 566 options.add_experimental) 567 db_clean_broken(options.autotest_dir) 568 return 0 569 570 if options.clear_all_tests: 571 if (options.clean_tests or options.add_all or options.add_samples or 572 options.add_noncompliant): 573 logging.error( 574 "Can only pass --autotest-dir, --dry-run and --verbose with " 575 "--db-clear-all-tests") 576 return 1 577 db_clean_all(options.autotest_dir) 578 579 whitelist_set = None 580 if options.whitelist_file: 581 if options.add_all: 582 logging.error("Cannot pass both --add-all and --whitelist-file") 583 return 1 584 whitelist_path = os.path.abspath(options.whitelist_file) 585 if not os.path.isfile(whitelist_path): 586 logging.error("--whitelist-file (%s) not found", whitelist_path) 587 return 1 588 logging.info("Using whitelist file %s", whitelist_path) 589 whitelist_set = _create_whitelist_set(whitelist_path) 590 update_from_whitelist(whitelist_set, 591 add_experimental=options.add_experimental, 592 add_noncompliant=options.add_noncompliant, 593 autotest_dir=options.autotest_dir) 594 if options.add_all: 595 update_all(options.autotest_dir, options.add_noncompliant, 596 options.add_experimental) 597 if options.add_samples: 598 update_samples(options.autotest_dir, options.add_noncompliant, 599 options.add_experimental) 600 if options.tests_dir: 601 options.tests_dir = os.path.abspath(options.tests_dir) 602 tests = get_tests_from_fs(options.tests_dir, options.control_pattern, 603 add_noncompliant=options.add_noncompliant) 604 update_tests_in_db(tests, add_experimental=options.add_experimental, 605 add_noncompliant=options.add_noncompliant, 606 autotest_dir=options.autotest_dir) 607 if options.profile_dir: 608 profilers = get_tests_from_fs(options.profile_dir, '.*py$') 609 update_profilers_in_db(profilers, 610 add_noncompliant=options.add_noncompliant, 611 description='NA') 612 if options.clean_tests: 613 db_clean_broken(options.autotest_dir) 614 615 616if __name__ == "__main__": 617 sys.exit(main(sys.argv)) 618