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, utils 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 update_tests_in_db(tests, dry_run=False, add_experimental=False, 200 add_noncompliant=False, autotest_dir=None): 201 """ 202 Scans through all tests and add them to the database. 203 204 This function invoked when -t supplied and for update_all. 205 When test code is discovered in the file system new tests may be added 206 207 @param tests: list of tests found in the filesystem. 208 @param dry_run: not used at this time. 209 @param add_experimental: add tests with experimental attribute set. 210 @param add_noncompliant: attempt adding test with invalid control files. 211 @param autotest_dir: prepended to path strings (/usr/local/autotest). 212 """ 213 site_set_attributes_module = utils.import_site_module( 214 __file__, 'autotest_lib.utils.site_test_importer_attributes') 215 216 for test in tests: 217 new_test = models.Test.objects.get_or_create( 218 path=test.replace(autotest_dir, '').lstrip('/'))[0] 219 logging.info("Processing %s", new_test.path) 220 221 # Set the test's attributes 222 data = tests[test] 223 _set_attributes_clean(new_test, data) 224 225 # Custom Attribute Update 226 if site_set_attributes_module: 227 site_set_attributes_module._set_attributes_custom(new_test, data) 228 229 # This only takes place if --add-noncompliant is provided on the CLI 230 if not new_test.name: 231 test_new_test = test.split('/') 232 if test_new_test[-1] == 'control': 233 new_test.name = test_new_test[-2] 234 else: 235 control_name = "%s:%s" 236 control_name %= (test_new_test[-2], 237 test_new_test[-1]) 238 new_test.name = control_name.replace('control.', '') 239 240 # Experimental Check 241 if not add_experimental and new_test.experimental: 242 continue 243 244 _log_or_execute(repr(new_test), new_test.save) 245 add_label_dependencies(new_test) 246 247 # save TestParameter 248 for para_name in data.test_parameters: 249 test_parameter = models.TestParameter.objects.get_or_create( 250 test=new_test, name=para_name)[0] 251 test_parameter.save() 252 253 254def _set_attributes_clean(test, data): 255 """ 256 First pass sets the attributes of the Test object from file system. 257 258 @param test: a test object to be populated for the database. 259 @param data: object with test data from the file system. 260 """ 261 test_time = { 'short' : 1, 262 'medium' : 2, 263 'long' : 3, } 264 265 266 string_attributes = ('name', 'author', 'test_class', 'test_category', 267 'test_category', 'sync_count') 268 for attribute in string_attributes: 269 setattr(test, attribute, getattr(data, attribute)) 270 271 test.description = data.doc 272 test.dependencies = ", ".join(data.dependencies) 273 274 try: 275 test.test_type = control_data.CONTROL_TYPE.get_value(data.test_type) 276 except AttributeError: 277 raise Exception('Unknown test_type %s for test %s', data.test_type, 278 data.name) 279 280 int_attributes = ('experimental', 'run_verify') 281 for attribute in int_attributes: 282 setattr(test, attribute, int(getattr(data, attribute))) 283 284 try: 285 test.test_time = int(data.time) 286 if test.test_time < 1 or test.time > 3: 287 raise Exception('Incorrect number %d for time' % test.time) 288 except ValueError: 289 pass 290 291 if not test.test_time and str == type(data.time): 292 test.test_time = test_time[data.time.lower()] 293 294 test.test_retry = data.retries 295 296 297def add_label_dependencies(test): 298 """ 299 Add proper many-to-many relationships from DEPENDENCIES field. 300 301 @param test: test object for the database. 302 """ 303 304 # clear out old relationships 305 _log_or_execute(repr(test), test.dependency_labels.clear, 306 subject='clear dependencies from') 307 308 for label_name in test.dependencies.split(','): 309 label_name = label_name.strip().lower() 310 if not label_name: 311 continue 312 313 try: 314 label = models.Label.objects.get(name=label_name) 315 except models.Label.DoesNotExist: 316 log_dependency_not_found(label_name) 317 continue 318 319 _log_or_execute(repr(label), test.dependency_labels.add, label, 320 subject='add dependency to %s' % test.name) 321 322 323def log_dependency_not_found(label_name): 324 """ 325 Exception processing when label not found in database. 326 327 @param label_name: from test dependencies. 328 """ 329 if label_name in DEPENDENCIES_NOT_FOUND: 330 return 331 logging.info("Dependency %s not found", label_name) 332 DEPENDENCIES_NOT_FOUND.add(label_name) 333 334 335def get_tests_from_fs(parent_dir, control_pattern, add_noncompliant=False): 336 """ 337 Find control files in file system and load a list with their info. 338 339 @param parent_dir: directory to search recursively. 340 @param control_pattern: name format of control file. 341 @param add_noncompliant: ignore control file parse errors. 342 343 @return dictionary of the form: tests[file_path] = parsed_object 344 """ 345 tests = {} 346 profilers = False 347 if 'client/profilers' in parent_dir: 348 profilers = True 349 for dir in [ parent_dir ]: 350 files = recursive_walk(dir, control_pattern) 351 for file in files: 352 if '__init__.py' in file or '.svn' in file: 353 continue 354 if not profilers: 355 if not add_noncompliant: 356 try: 357 found_test = control_data.parse_control(file, 358 raise_warnings=True) 359 tests[file] = found_test 360 except control_data.ControlVariableException, e: 361 logging.warning("Skipping %s\n%s", file, e) 362 except Exception, e: 363 logging.error("Bad %s\n%s", file, e) 364 else: 365 found_test = control_data.parse_control(file) 366 tests[file] = found_test 367 else: 368 tests[file] = compiler.parseFile(file).doc 369 return tests 370 371 372def recursive_walk(path, wildcard): 373 """ 374 Recursively go through a directory. 375 376 This function invoked by get_tests_from_fs(). 377 378 @param path: base directory to start search. 379 @param wildcard: name format to match. 380 381 @return A list of files that match wildcard 382 """ 383 files = [] 384 directories = [ path ] 385 while len(directories)>0: 386 directory = directories.pop() 387 for name in os.listdir(directory): 388 fullpath = os.path.join(directory, name) 389 if os.path.isfile(fullpath): 390 # if we are a control file 391 if re.search(wildcard, name): 392 files.append(fullpath) 393 elif os.path.isdir(fullpath): 394 directories.append(fullpath) 395 return files 396 397 398def _log_or_execute(content, func, *args, **kwargs): 399 """ 400 Log a message if dry_run is enabled, or execute the given function. 401 402 Relies on the DRY_RUN global variable. 403 404 @param content: the actual log message. 405 @param func: function to execute if dry_run is not enabled. 406 @param subject: (Optional) The type of log being written. Defaults to 407 the name of the provided function. 408 """ 409 subject = kwargs.get('subject', func.__name__) 410 411 if DRY_RUN: 412 logging.info("Would %s: %s", subject, content) 413 else: 414 func(*args) 415 416 417def _create_whitelist_set(whitelist_path): 418 """ 419 Create a set with contents from a whitelist file for membership testing. 420 421 @param whitelist_path: full path to the whitelist file. 422 423 @return set with files listed one/line - newlines included. 424 """ 425 f = open(whitelist_path, 'r') 426 whitelist_set = set([line.strip() for line in f]) 427 f.close() 428 return whitelist_set 429 430 431def update_from_whitelist(whitelist_set, add_experimental, add_noncompliant, 432 autotest_dir): 433 """ 434 Scans through all tests in the whitelist and add them to the database. 435 436 This function invoked when -w supplied. 437 438 @param whitelist_set: set of tests in full-path form from a whitelist. 439 @param add_experimental: add tests with experimental attribute set. 440 @param add_noncompliant: attempt adding test with invalid control files. 441 @param autotest_dir: prepended to path strings (/usr/local/autotest). 442 """ 443 tests = {} 444 profilers = {} 445 for file_path in whitelist_set: 446 if file_path.find('client/profilers') == -1: 447 try: 448 found_test = control_data.parse_control(file_path, 449 raise_warnings=True) 450 tests[file_path] = found_test 451 except control_data.ControlVariableException, e: 452 logging.warning("Skipping %s\n%s", file, e) 453 else: 454 profilers[file_path] = compiler.parseFile(file_path).doc 455 456 if len(tests) > 0: 457 update_tests_in_db(tests, add_experimental=add_experimental, 458 add_noncompliant=add_noncompliant, 459 autotest_dir=autotest_dir) 460 if len(profilers) > 0: 461 update_profilers_in_db(profilers, add_noncompliant=add_noncompliant, 462 description='NA') 463 464 465def main(argv): 466 """Main function 467 @param argv: List of command line parameters. 468 """ 469 470 global DRY_RUN 471 parser = optparse.OptionParser() 472 parser.add_option('-c', '--db-clean-tests', 473 dest='clean_tests', action='store_true', 474 default=False, 475 help='Clean client and server tests with invalid control files') 476 parser.add_option('-C', '--db-clear-all-tests', 477 dest='clear_all_tests', action='store_true', 478 default=False, 479 help='Clear ALL client and server tests') 480 parser.add_option('-d', '--dry-run', 481 dest='dry_run', action='store_true', default=False, 482 help='Dry run for operation') 483 parser.add_option('-A', '--add-all', 484 dest='add_all', action='store_true', 485 default=False, 486 help='Add site_tests, tests, and test_suites') 487 parser.add_option('-S', '--add-samples', 488 dest='add_samples', action='store_true', 489 default=False, 490 help='Add samples.') 491 parser.add_option('-E', '--add-experimental', 492 dest='add_experimental', action='store_true', 493 default=True, 494 help='Add experimental tests to frontend, works only ' 495 'with -A (--add-all) option') 496 parser.add_option('-N', '--add-noncompliant', 497 dest='add_noncompliant', action='store_true', 498 default=False, 499 help='Add non-compliant tests (i.e. tests that do not ' 500 'define all required control variables), works ' 501 'only with -A (--add-all) option') 502 parser.add_option('-p', '--profile-dir', dest='profile_dir', 503 help='Directory to recursively check for profiles') 504 parser.add_option('-t', '--tests-dir', dest='tests_dir', 505 help='Directory to recursively check for control.*') 506 parser.add_option('-r', '--control-pattern', dest='control_pattern', 507 default='^control.*', 508 help='The pattern to look for in directories for control files') 509 parser.add_option('-v', '--verbose', 510 dest='verbose', action='store_true', default=False, 511 help='Run in verbose mode') 512 parser.add_option('-w', '--whitelist-file', dest='whitelist_file', 513 help='Filename for list of test names that must match') 514 parser.add_option('-z', '--autotest-dir', dest='autotest_dir', 515 default=os.path.join(os.path.dirname(__file__), '..'), 516 help='Autotest directory root') 517 options, args = parser.parse_args() 518 519 logging_manager.configure_logging(TestImporterLoggingConfig(), 520 verbose=options.verbose) 521 522 DRY_RUN = options.dry_run 523 if DRY_RUN: 524 logging.getLogger().setLevel(logging.WARN) 525 526 if len(argv) > 1 and options.add_noncompliant and not options.add_all: 527 logging.error('-N (--add-noncompliant) must be ran with option -A ' 528 '(--add-All).') 529 return 1 530 531 if len(argv) > 1 and options.add_experimental and not options.add_all: 532 logging.error('-E (--add-experimental) must be ran with option -A ' 533 '(--add-All).') 534 return 1 535 536 # Make sure autotest_dir is the absolute path 537 options.autotest_dir = os.path.abspath(options.autotest_dir) 538 539 if len(args) > 0: 540 logging.error("Invalid option(s) provided: %s", args) 541 parser.print_help() 542 return 1 543 544 if options.verbose: 545 logging.getLogger().setLevel(logging.DEBUG) 546 547 if len(argv) == 1 or (len(argv) == 2 and options.verbose): 548 update_all(options.autotest_dir, options.add_noncompliant, 549 options.add_experimental) 550 db_clean_broken(options.autotest_dir) 551 return 0 552 553 if options.clear_all_tests: 554 if (options.clean_tests or options.add_all or options.add_samples or 555 options.add_noncompliant): 556 logging.error( 557 "Can only pass --autotest-dir, --dry-run and --verbose with " 558 "--db-clear-all-tests") 559 return 1 560 db_clean_all(options.autotest_dir) 561 562 whitelist_set = None 563 if options.whitelist_file: 564 if options.add_all: 565 logging.error("Cannot pass both --add-all and --whitelist-file") 566 return 1 567 whitelist_path = os.path.abspath(options.whitelist_file) 568 if not os.path.isfile(whitelist_path): 569 logging.error("--whitelist-file (%s) not found", whitelist_path) 570 return 1 571 logging.info("Using whitelist file %s", whitelist_path) 572 whitelist_set = _create_whitelist_set(whitelist_path) 573 update_from_whitelist(whitelist_set, 574 add_experimental=options.add_experimental, 575 add_noncompliant=options.add_noncompliant, 576 autotest_dir=options.autotest_dir) 577 if options.add_all: 578 update_all(options.autotest_dir, options.add_noncompliant, 579 options.add_experimental) 580 if options.add_samples: 581 update_samples(options.autotest_dir, options.add_noncompliant, 582 options.add_experimental) 583 if options.tests_dir: 584 options.tests_dir = os.path.abspath(options.tests_dir) 585 tests = get_tests_from_fs(options.tests_dir, options.control_pattern, 586 add_noncompliant=options.add_noncompliant) 587 update_tests_in_db(tests, add_experimental=options.add_experimental, 588 add_noncompliant=options.add_noncompliant, 589 autotest_dir=options.autotest_dir) 590 if options.profile_dir: 591 profilers = get_tests_from_fs(options.profile_dir, '.*py$') 592 update_profilers_in_db(profilers, 593 add_noncompliant=options.add_noncompliant, 594 description='NA') 595 if options.clean_tests: 596 db_clean_broken(options.autotest_dir) 597 598 599if __name__ == "__main__": 600 sys.exit(main(sys.argv)) 601