1#!/usr/bin/python2.6 2 3# 4# This file and its contents are supplied under the terms of the 5# Common Development and Distribution License ("CDDL"), version 1.0. 6# You may only use this file in accordance with the terms of version 7# 1.0 of the CDDL. 8# 9# A full copy of the text of the CDDL should have accompanied this 10# source. A copy of the CDDL is also available via the Internet at 11# http://www.illumos.org/license/CDDL. 12# 13 14# 15# Copyright (c) 2013 by Delphix. All rights reserved. 16# 17 18import ConfigParser 19import os 20import logging 21from datetime import datetime 22from optparse import OptionParser 23from pwd import getpwnam 24from pwd import getpwuid 25from select import select 26from subprocess import PIPE 27from subprocess import Popen 28from sys import argv 29from sys import exit 30from threading import Timer 31from time import time 32 33BASEDIR = '/var/tmp/test_results' 34KILL = '/usr/bin/kill' 35TRUE = '/usr/bin/true' 36SUDO = '/usr/bin/sudo' 37 38 39class Result(object): 40 total = 0 41 runresults = {'PASS': 0, 'FAIL': 0, 'SKIP': 0, 'KILLED': 0} 42 43 def __init__(self): 44 self.starttime = None 45 self.returncode = None 46 self.runtime = '' 47 self.stdout = [] 48 self.stderr = [] 49 self.result = '' 50 51 def done(self, proc, killed): 52 """ 53 Finalize the results of this Cmd. 54 """ 55 Result.total += 1 56 m, s = divmod(time() - self.starttime, 60) 57 self.runtime = '%02d:%02d' % (m, s) 58 self.returncode = proc.returncode 59 if killed: 60 self.result = 'KILLED' 61 Result.runresults['KILLED'] += 1 62 elif self.returncode is 0: 63 self.result = 'PASS' 64 Result.runresults['PASS'] += 1 65 elif self.returncode is not 0: 66 self.result = 'FAIL' 67 Result.runresults['FAIL'] += 1 68 69 70class Output(object): 71 """ 72 This class is a slightly modified version of the 'Stream' class found 73 here: http://goo.gl/aSGfv 74 """ 75 def __init__(self, stream): 76 self.stream = stream 77 self._buf = '' 78 self.lines = [] 79 80 def fileno(self): 81 return self.stream.fileno() 82 83 def read(self, drain=0): 84 """ 85 Read from the file descriptor. If 'drain' set, read until EOF. 86 """ 87 while self._read() is not None: 88 if not drain: 89 break 90 91 def _read(self): 92 """ 93 Read up to 4k of data from this output stream. Collect the output 94 up to the last newline, and append it to any leftover data from a 95 previous call. The lines are stored as a (timestamp, data) tuple 96 for easy sorting/merging later. 97 """ 98 fd = self.fileno() 99 buf = os.read(fd, 4096) 100 if not buf: 101 return None 102 if '\n' not in buf: 103 self._buf += buf 104 return [] 105 106 buf = self._buf + buf 107 tmp, rest = buf.rsplit('\n', 1) 108 self._buf = rest 109 now = datetime.now() 110 rows = tmp.split('\n') 111 self.lines += [(now, r) for r in rows] 112 113 114class Cmd(object): 115 verified_users = [] 116 117 def __init__(self, pathname, outputdir=None, timeout=None, user=None): 118 self.pathname = pathname 119 self.outputdir = outputdir or 'BASEDIR' 120 self.timeout = timeout or 60 121 self.user = user or '' 122 self.killed = False 123 self.result = Result() 124 125 def __str__(self): 126 return "Pathname: %s\nOutputdir: %s\nTimeout: %s\nUser: %s\n" % ( 127 self.pathname, self.outputdir, self.timeout, self.user) 128 129 def kill_cmd(self, proc): 130 """ 131 Kill a running command due to timeout, or ^C from the keyboard. If 132 sudo is required, this user was verified previously. 133 """ 134 self.killed = True 135 do_sudo = len(self.user) != 0 136 signal = '-TERM' 137 138 cmd = [SUDO, KILL, signal, str(proc.pid)] 139 if not do_sudo: 140 del cmd[0] 141 142 try: 143 kp = Popen(cmd) 144 kp.wait() 145 except: 146 pass 147 148 def update_cmd_privs(self, cmd, user): 149 """ 150 If a user has been specified to run this Cmd and we're not already 151 running as that user, prepend the appropriate sudo command to run 152 as that user. 153 """ 154 me = getpwuid(os.getuid()) 155 156 if not user or user is me: 157 return cmd 158 159 ret = '%s -E -u %s %s' % (SUDO, user, cmd) 160 return ret.split(' ') 161 162 def collect_output(self, proc): 163 """ 164 Read from stdout/stderr as data becomes available, until the 165 process is no longer running. Return the lines from the stdout and 166 stderr Output objects. 167 """ 168 out = Output(proc.stdout) 169 err = Output(proc.stderr) 170 res = [] 171 while proc.returncode is None: 172 proc.poll() 173 res = select([out, err], [], [], .1) 174 for fd in res[0]: 175 fd.read() 176 for fd in res[0]: 177 fd.read(drain=1) 178 179 return out.lines, err.lines 180 181 def run(self, options): 182 """ 183 This is the main function that runs each individual test. 184 Determine whether or not the command requires sudo, and modify it 185 if needed. Run the command, and update the result object. 186 """ 187 if options.dryrun is True: 188 print self 189 return 190 191 privcmd = self.update_cmd_privs(self.pathname, self.user) 192 try: 193 old = os.umask(0) 194 if not os.path.isdir(self.outputdir): 195 os.makedirs(self.outputdir, mode=0777) 196 os.umask(old) 197 except OSError, e: 198 fail('%s' % e) 199 200 try: 201 self.result.starttime = time() 202 proc = Popen(privcmd, stdout=PIPE, stderr=PIPE) 203 t = Timer(int(self.timeout), self.kill_cmd, [proc]) 204 t.start() 205 self.result.stdout, self.result.stderr = self.collect_output(proc) 206 except KeyboardInterrupt: 207 self.kill_cmd(proc) 208 fail('\nRun terminated at user request.') 209 finally: 210 t.cancel() 211 212 self.result.done(proc, self.killed) 213 214 def skip(self): 215 """ 216 Initialize enough of the test result that we can log a skipped 217 command. 218 """ 219 Result.total += 1 220 Result.runresults['SKIP'] += 1 221 self.result.stdout = self.result.stderr = [] 222 self.result.starttime = time() 223 m, s = divmod(time() - self.result.starttime, 60) 224 self.result.runtime = '%02d:%02d' % (m, s) 225 self.result.result = 'SKIP' 226 227 def log(self, logger, options): 228 """ 229 This function is responsible for writing all output. This includes 230 the console output, the logfile of all results (with timestamped 231 merged stdout and stderr), and for each test, the unmodified 232 stdout/stderr/merged in it's own file. 233 """ 234 if logger is None: 235 return 236 237 logname = getpwuid(os.getuid()).pw_name 238 user = ' (run as %s)' % (self.user if len(self.user) else logname) 239 msga = 'Test: %s%s ' % (self.pathname, user) 240 msgb = '[%s] [%s]' % (self.result.runtime, self.result.result) 241 pad = ' ' * (80 - (len(msga) + len(msgb))) 242 243 # If -q is specified, only print a line for tests that didn't pass. 244 # This means passing tests need to be logged as DEBUG, or the one 245 # line summary will only be printed in the logfile for failures. 246 if not options.quiet: 247 logger.info('%s%s%s' % (msga, pad, msgb)) 248 elif self.result.result is not 'PASS': 249 logger.info('%s%s%s' % (msga, pad, msgb)) 250 else: 251 logger.debug('%s%s%s' % (msga, pad, msgb)) 252 253 lines = self.result.stdout + self.result.stderr 254 for dt, line in sorted(lines): 255 logger.debug('%s %s' % (dt.strftime("%H:%M:%S.%f ")[:11], line)) 256 257 if len(self.result.stdout): 258 with open(os.path.join(self.outputdir, 'stdout'), 'w') as out: 259 for _, line in self.result.stdout: 260 os.write(out.fileno(), '%s\n' % line) 261 if len(self.result.stderr): 262 with open(os.path.join(self.outputdir, 'stderr'), 'w') as err: 263 for _, line in self.result.stderr: 264 os.write(err.fileno(), '%s\n' % line) 265 if len(self.result.stdout) and len(self.result.stderr): 266 with open(os.path.join(self.outputdir, 'merged'), 'w') as merged: 267 for _, line in sorted(lines): 268 os.write(merged.fileno(), '%s\n' % line) 269 270 271class Test(Cmd): 272 props = ['outputdir', 'timeout', 'user', 'pre', 'pre_user', 'post', 273 'post_user'] 274 275 def __init__(self, pathname, outputdir=None, timeout=None, user=None, 276 pre=None, pre_user=None, post=None, post_user=None): 277 super(Test, self).__init__(pathname, outputdir, timeout, user) 278 self.pre = pre or '' 279 self.pre_user = pre_user or '' 280 self.post = post or '' 281 self.post_user = post_user or '' 282 283 def __str__(self): 284 post_user = pre_user = '' 285 if len(self.pre_user): 286 pre_user = ' (as %s)' % (self.pre_user) 287 if len(self.post_user): 288 post_user = ' (as %s)' % (self.post_user) 289 return "Pathname: %s\nOutputdir: %s\nTimeout: %s\nPre: %s%s\nPost: " \ 290 "%s%s\nUser: %s\n" % (self.pathname, self.outputdir, 291 self.timeout, self.pre, pre_user, self.post, post_user, 292 self.user) 293 294 def verify(self, logger): 295 """ 296 Check the pre/post scripts, user and Test. Omit the Test from this 297 run if there are any problems. 298 """ 299 files = [self.pre, self.pathname, self.post] 300 users = [self.pre_user, self.user, self.post_user] 301 302 for f in [f for f in files if len(f)]: 303 if not verify_file(f): 304 logger.info("Warning: Test '%s' not added to this run because" 305 " it failed verification." % f) 306 return False 307 308 for user in [user for user in users if len(user)]: 309 if not verify_user(user, logger): 310 logger.info("Not adding Test '%s' to this run." % 311 self.pathname) 312 return False 313 314 return True 315 316 def run(self, logger, options): 317 """ 318 Create Cmd instances for the pre/post scripts. If the pre script 319 doesn't pass, skip this Test. Run the post script regardless. 320 """ 321 pretest = Cmd(self.pre, outputdir=os.path.join(self.outputdir, 322 os.path.basename(self.pre)), timeout=self.timeout, 323 user=self.pre_user) 324 test = Cmd(self.pathname, outputdir=self.outputdir, 325 timeout=self.timeout, user=self.user) 326 posttest = Cmd(self.post, outputdir=os.path.join(self.outputdir, 327 os.path.basename(self.post)), timeout=self.timeout, 328 user=self.post_user) 329 330 cont = True 331 if len(pretest.pathname): 332 pretest.run(options) 333 cont = pretest.result.result is 'PASS' 334 pretest.log(logger, options) 335 336 if cont: 337 test.run(options) 338 else: 339 test.skip() 340 341 test.log(logger, options) 342 343 if len(posttest.pathname): 344 posttest.run(options) 345 posttest.log(logger, options) 346 347 348class TestGroup(Test): 349 props = Test.props + ['tests'] 350 351 def __init__(self, pathname, outputdir=None, timeout=None, user=None, 352 pre=None, pre_user=None, post=None, post_user=None, 353 tests=None): 354 super(TestGroup, self).__init__(pathname, outputdir, timeout, user, 355 pre, pre_user, post, post_user) 356 self.tests = tests or [] 357 358 def __str__(self): 359 post_user = pre_user = '' 360 if len(self.pre_user): 361 pre_user = ' (as %s)' % (self.pre_user) 362 if len(self.post_user): 363 post_user = ' (as %s)' % (self.post_user) 364 return "Pathname: %s\nOutputdir: %s\nTests: %s\nTimeout: %s\n" \ 365 "Pre: %s%s\nPost: %s%s\nUser: %s\n" % (self.pathname, 366 self.outputdir, self.tests, self.timeout, self.pre, pre_user, 367 self.post, post_user, self.user) 368 369 def verify(self, logger): 370 """ 371 Check the pre/post scripts, user and tests in this TestGroup. Omit 372 the TestGroup entirely, or simply delete the relevant tests in the 373 group, if that's all that's required. 374 """ 375 # If the pre or post scripts are relative pathnames, convert to 376 # absolute, so they stand a chance of passing verification. 377 if len(self.pre) and not os.path.isabs(self.pre): 378 self.pre = os.path.join(self.pathname, self.pre) 379 if len(self.post) and not os.path.isabs(self.post): 380 self.post = os.path.join(self.pathname, self.post) 381 382 auxfiles = [self.pre, self.post] 383 users = [self.pre_user, self.user, self.post_user] 384 385 for f in [f for f in auxfiles if len(f)]: 386 if self.pathname != os.path.dirname(f): 387 logger.info("Warning: TestGroup '%s' not added to this run. " 388 "Auxiliary script '%s' exists in a different " 389 "directory." % (self.pathname, f)) 390 return False 391 392 if not verify_file(f): 393 logger.info("Warning: TestGroup '%s' not added to this run. " 394 "Auxiliary script '%s' failed verification." % 395 (self.pathname, f)) 396 return False 397 398 for user in [user for user in users if len(user)]: 399 if not verify_user(user, logger): 400 logger.info("Not adding TestGroup '%s' to this run." % 401 self.pathname) 402 return False 403 404 # If one of the tests is invalid, delete it, log it, and drive on. 405 for test in self.tests: 406 if not verify_file(os.path.join(self.pathname, test)): 407 del self.tests[self.tests.index(test)] 408 logger.info("Warning: Test '%s' removed from TestGroup '%s' " 409 "because it failed verification." % (test, 410 self.pathname)) 411 412 return len(self.tests) is not 0 413 414 def run(self, logger, options): 415 """ 416 Create Cmd instances for the pre/post scripts. If the pre script 417 doesn't pass, skip all the tests in this TestGroup. Run the post 418 script regardless. 419 """ 420 pretest = Cmd(self.pre, outputdir=os.path.join(self.outputdir, 421 os.path.basename(self.pre)), timeout=self.timeout, 422 user=self.pre_user) 423 posttest = Cmd(self.post, outputdir=os.path.join(self.outputdir, 424 os.path.basename(self.post)), timeout=self.timeout, 425 user=self.post_user) 426 427 cont = True 428 if len(pretest.pathname): 429 pretest.run(options) 430 cont = pretest.result.result is 'PASS' 431 pretest.log(logger, options) 432 433 for fname in self.tests: 434 test = Cmd(os.path.join(self.pathname, fname), 435 outputdir=os.path.join(self.outputdir, fname), 436 timeout=self.timeout, user=self.user) 437 if cont: 438 test.run(options) 439 else: 440 test.skip() 441 442 test.log(logger, options) 443 444 if len(posttest.pathname): 445 posttest.run(options) 446 posttest.log(logger, options) 447 448 449class TestRun(object): 450 props = ['quiet', 'outputdir'] 451 452 def __init__(self, options): 453 self.tests = {} 454 self.testgroups = {} 455 self.starttime = time() 456 self.timestamp = datetime.now().strftime('%Y%m%dT%H%M%S') 457 self.outputdir = os.path.join(options.outputdir, self.timestamp) 458 self.logger = self.setup_logging(options) 459 self.defaults = [ 460 ('outputdir', BASEDIR), 461 ('quiet', False), 462 ('timeout', 60), 463 ('user', ''), 464 ('pre', ''), 465 ('pre_user', ''), 466 ('post', ''), 467 ('post_user', '') 468 ] 469 470 def __str__(self): 471 s = 'TestRun:\n outputdir: %s\n' % self.outputdir 472 s += 'TESTS:\n' 473 for key in sorted(self.tests.keys()): 474 s += '%s%s' % (self.tests[key].__str__(), '\n') 475 s += 'TESTGROUPS:\n' 476 for key in sorted(self.testgroups.keys()): 477 s += '%s%s' % (self.testgroups[key].__str__(), '\n') 478 return s 479 480 def addtest(self, pathname, options): 481 """ 482 Create a new Test, and apply any properties that were passed in 483 from the command line. If it passes verification, add it to the 484 TestRun. 485 """ 486 test = Test(pathname) 487 for prop in Test.props: 488 setattr(test, prop, getattr(options, prop)) 489 490 if test.verify(self.logger): 491 self.tests[pathname] = test 492 493 def addtestgroup(self, dirname, filenames, options): 494 """ 495 Create a new TestGroup, and apply any properties that were passed 496 in from the command line. If it passes verification, add it to the 497 TestRun. 498 """ 499 if dirname not in self.testgroups: 500 testgroup = TestGroup(dirname) 501 for prop in Test.props: 502 setattr(testgroup, prop, getattr(options, prop)) 503 504 # Prevent pre/post scripts from running as regular tests 505 for f in [testgroup.pre, testgroup.post]: 506 if f in filenames: 507 del filenames[filenames.index(f)] 508 509 self.testgroups[dirname] = testgroup 510 self.testgroups[dirname].tests = sorted(filenames) 511 512 testgroup.verify(self.logger) 513 514 def read(self, logger, options): 515 """ 516 Read in the specified runfile, and apply the TestRun properties 517 listed in the 'DEFAULT' section to our TestRun. Then read each 518 section, and apply the appropriate properties to the Test or 519 TestGroup. Properties from individual sections override those set 520 in the 'DEFAULT' section. If the Test or TestGroup passes 521 verification, add it to the TestRun. 522 """ 523 config = ConfigParser.RawConfigParser() 524 if not len(config.read(options.runfile)): 525 fail("Coulnd't read config file %s" % options.runfile) 526 527 for opt in TestRun.props: 528 if config.has_option('DEFAULT', opt): 529 setattr(self, opt, config.get('DEFAULT', opt)) 530 self.outputdir = os.path.join(self.outputdir, self.timestamp) 531 532 for section in config.sections(): 533 if 'tests' in config.options(section): 534 testgroup = TestGroup(section) 535 for prop in TestGroup.props: 536 try: 537 setattr(testgroup, prop, config.get('DEFAULT', prop)) 538 setattr(testgroup, prop, config.get(section, prop)) 539 except ConfigParser.NoOptionError: 540 pass 541 542 # Repopulate tests using eval to convert the string to a list 543 testgroup.tests = eval(config.get(section, 'tests')) 544 545 if testgroup.verify(logger): 546 self.testgroups[section] = testgroup 547 else: 548 test = Test(section) 549 for prop in Test.props: 550 try: 551 setattr(test, prop, config.get('DEFAULT', prop)) 552 setattr(test, prop, config.get(section, prop)) 553 except ConfigParser.NoOptionError: 554 pass 555 if test.verify(logger): 556 self.tests[section] = test 557 558 def write(self, options): 559 """ 560 Create a configuration file for editing and later use. The 561 'DEFAULT' section of the config file is created from the 562 properties that were specified on the command line. Tests are 563 simply added as sections that inherit everything from the 564 'DEFAULT' section. TestGroups are the same, except they get an 565 option including all the tests to run in that directory. 566 """ 567 568 defaults = dict([(prop, getattr(options, prop)) for prop, _ in 569 self.defaults]) 570 config = ConfigParser.RawConfigParser(defaults) 571 572 for test in sorted(self.tests.keys()): 573 config.add_section(test) 574 575 for testgroup in sorted(self.testgroups.keys()): 576 config.add_section(testgroup) 577 config.set(testgroup, 'tests', self.testgroups[testgroup].tests) 578 579 try: 580 with open(options.template, 'w') as f: 581 return config.write(f) 582 except IOError: 583 fail('Could not open \'%s\' for writing.' % options.template) 584 585 def complete_outputdirs(self, options): 586 """ 587 Collect all the pathnames for Tests, and TestGroups. Work 588 backwards one pathname component at a time, to create a unique 589 directory name in which to deposit test output. Tests will be able 590 to write output files directly in the newly modified outputdir. 591 TestGroups will be able to create one subdirectory per test in the 592 outputdir, and are guaranteed uniqueness because a group can only 593 contain files in one directory. Pre and post tests will create a 594 directory rooted at the outputdir of the Test or TestGroup in 595 question for their output. 596 """ 597 done = False 598 components = 0 599 tmp_dict = dict(self.tests.items() + self.testgroups.items()) 600 total = len(tmp_dict) 601 base = self.outputdir 602 603 while not done: 604 l = [] 605 components -= 1 606 for testfile in tmp_dict.keys(): 607 uniq = '/'.join(testfile.split('/')[components:]).lstrip('/') 608 if not uniq in l: 609 l.append(uniq) 610 tmp_dict[testfile].outputdir = os.path.join(base, uniq) 611 else: 612 break 613 done = total == len(l) 614 615 def setup_logging(self, options): 616 """ 617 Two loggers are set up here. The first is for the logfile which 618 will contain one line summarizing the test, including the test 619 name, result, and running time. This logger will also capture the 620 timestamped combined stdout and stderr of each run. The second 621 logger is optional console output, which will contain only the one 622 line summary. The loggers are initialized at two different levels 623 to facilitate segregating the output. 624 """ 625 if options.dryrun is True: 626 return 627 628 testlogger = logging.getLogger(__name__) 629 testlogger.setLevel(logging.DEBUG) 630 631 if options.cmd is not 'wrconfig': 632 try: 633 old = os.umask(0) 634 os.makedirs(self.outputdir, mode=0777) 635 os.umask(old) 636 except OSError, e: 637 fail('%s' % e) 638 filename = os.path.join(self.outputdir, 'log') 639 640 logfile = logging.FileHandler(filename) 641 logfile.setLevel(logging.DEBUG) 642 logfilefmt = logging.Formatter('%(message)s') 643 logfile.setFormatter(logfilefmt) 644 testlogger.addHandler(logfile) 645 646 cons = logging.StreamHandler() 647 cons.setLevel(logging.INFO) 648 consfmt = logging.Formatter('%(message)s') 649 cons.setFormatter(consfmt) 650 testlogger.addHandler(cons) 651 652 return testlogger 653 654 def run(self, options): 655 """ 656 Walk through all the Tests and TestGroups, calling run(). 657 """ 658 try: 659 os.chdir(self.outputdir) 660 except OSError: 661 fail('Could not change to directory %s' % self.outputdir) 662 for test in sorted(self.tests.keys()): 663 self.tests[test].run(self.logger, options) 664 for testgroup in sorted(self.testgroups.keys()): 665 self.testgroups[testgroup].run(self.logger, options) 666 667 def summary(self): 668 if Result.total is 0: 669 return 670 671 print '\nResults Summary' 672 for key in Result.runresults.keys(): 673 if Result.runresults[key] is not 0: 674 print '%s\t% 4d' % (key, Result.runresults[key]) 675 676 m, s = divmod(time() - self.starttime, 60) 677 h, m = divmod(m, 60) 678 print '\nRunning Time:\t%02d:%02d:%02d' % (h, m, s) 679 print 'Percent passed:\t%.1f%%' % ((float(Result.runresults['PASS']) / 680 float(Result.total)) * 100) 681 print 'Log directory:\t%s' % self.outputdir 682 683 684def verify_file(pathname): 685 """ 686 Verify that the supplied pathname is an executable regular file. 687 """ 688 if os.path.isdir(pathname) or os.path.islink(pathname): 689 return False 690 691 if os.path.isfile(pathname) and os.access(pathname, os.X_OK): 692 return True 693 694 return False 695 696 697def verify_user(user, logger): 698 """ 699 Verify that the specified user exists on this system, and can execute 700 sudo without being prompted for a password. 701 """ 702 testcmd = [SUDO, '-n', '-u', user, TRUE] 703 can_sudo = exists = True 704 705 if user in Cmd.verified_users: 706 return True 707 708 try: 709 _ = getpwnam(user) 710 except KeyError: 711 exists = False 712 logger.info("Warning: user '%s' does not exist.", user) 713 return False 714 715 p = Popen(testcmd) 716 p.wait() 717 if p.returncode is not 0: 718 logger.info("Warning: user '%s' cannot use passwordless sudo.", user) 719 return False 720 else: 721 Cmd.verified_users.append(user) 722 723 return True 724 725 726def find_tests(testrun, options): 727 """ 728 For the given list of pathnames, add files as Tests. For directories, 729 if do_groups is True, add the directory as a TestGroup. If False, 730 recursively search for executable files. 731 """ 732 733 for p in sorted(options.pathnames): 734 if os.path.isdir(p): 735 for dirname, _, filenames in os.walk(p): 736 if options.do_groups: 737 testrun.addtestgroup(dirname, filenames, options) 738 else: 739 for f in sorted(filenames): 740 testrun.addtest(os.path.join(dirname, f), options) 741 else: 742 testrun.addtest(p, options) 743 744 745def fail(retstr, ret=1): 746 print '%s: %s' % (argv[0], retstr) 747 exit(ret) 748 749 750def options_cb(option, opt_str, value, parser): 751 path_options = ['runfile', 'outputdir', 'template'] 752 753 if option.dest is 'runfile' and '-w' in parser.rargs or \ 754 option.dest is 'template' and '-c' in parser.rargs: 755 fail('-c and -w are mutually exclusive.') 756 757 if opt_str in parser.rargs: 758 fail('%s may only be specified once.' % opt_str) 759 760 if option.dest is 'runfile': 761 parser.values.cmd = 'rdconfig' 762 if option.dest is 'template': 763 parser.values.cmd = 'wrconfig' 764 765 setattr(parser.values, option.dest, value) 766 if option.dest in path_options: 767 setattr(parser.values, option.dest, os.path.abspath(value)) 768 769 770def parse_args(): 771 parser = OptionParser() 772 parser.add_option('-c', action='callback', callback=options_cb, 773 type='string', dest='runfile', metavar='runfile', 774 help='Specify tests to run via config file.') 775 parser.add_option('-d', action='store_true', default=False, dest='dryrun', 776 help='Dry run. Print tests, but take no other action.') 777 parser.add_option('-g', action='store_true', default=False, 778 dest='do_groups', help='Make directories TestGroups.') 779 parser.add_option('-o', action='callback', callback=options_cb, 780 default=BASEDIR, dest='outputdir', type='string', 781 metavar='outputdir', help='Specify an output directory.') 782 parser.add_option('-p', action='callback', callback=options_cb, 783 default='', dest='pre', metavar='script', 784 type='string', help='Specify a pre script.') 785 parser.add_option('-P', action='callback', callback=options_cb, 786 default='', dest='post', metavar='script', 787 type='string', help='Specify a post script.') 788 parser.add_option('-q', action='store_true', default=False, dest='quiet', 789 help='Silence on the console during a test run.') 790 parser.add_option('-t', action='callback', callback=options_cb, default=60, 791 dest='timeout', metavar='seconds', type='int', 792 help='Timeout (in seconds) for an individual test.') 793 parser.add_option('-u', action='callback', callback=options_cb, 794 default='', dest='user', metavar='user', type='string', 795 help='Specify a different user name to run as.') 796 parser.add_option('-w', action='callback', callback=options_cb, 797 default=None, dest='template', metavar='template', 798 type='string', help='Create a new config file.') 799 parser.add_option('-x', action='callback', callback=options_cb, default='', 800 dest='pre_user', metavar='pre_user', type='string', 801 help='Specify a user to execute the pre script.') 802 parser.add_option('-X', action='callback', callback=options_cb, default='', 803 dest='post_user', metavar='post_user', type='string', 804 help='Specify a user to execute the post script.') 805 (options, pathnames) = parser.parse_args() 806 807 if not options.runfile and not options.template: 808 options.cmd = 'runtests' 809 810 if options.runfile and len(pathnames): 811 fail('Extraneous arguments.') 812 813 options.pathnames = [os.path.abspath(path) for path in pathnames] 814 815 return options 816 817 818def main(args): 819 options = parse_args() 820 testrun = TestRun(options) 821 822 if options.cmd is 'runtests': 823 find_tests(testrun, options) 824 elif options.cmd is 'rdconfig': 825 testrun.read(testrun.logger, options) 826 elif options.cmd is 'wrconfig': 827 find_tests(testrun, options) 828 testrun.write(options) 829 exit(0) 830 else: 831 fail('Unknown command specified') 832 833 testrun.complete_outputdirs(options) 834 testrun.run(options) 835 testrun.summary() 836 exit(0) 837 838 839if __name__ == '__main__': 840 main(argv[1:]) 841