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) 2012 by Delphix. All rights reserved. 16 # 17 18 import ConfigParser 19 import os 20 import logging 21 from datetime import datetime 22 from optparse import OptionParser 23 from pwd import getpwnam 24 from pwd import getpwuid 25 from select import select 26 from subprocess import PIPE 27 from subprocess import Popen 28 from sys import argv 29 from sys import exit 30 from threading import Timer 31 from time import time 32 33 BASEDIR = '/var/tmp/test_results' 34 KILL = '/usr/bin/kill' 35 TRUE = '/usr/bin/true' 36 SUDO = '/usr/bin/sudo' 37 38 39 class 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 70 class 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 114 class 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 user = ' (run as %s)' % self.user if len(self.user) else '' 238 msga = 'Test: %s%s ' % (self.pathname, user) 239 msgb = '[%s] [%s]' % (self.result.runtime, self.result.result) 240 pad = ' ' * (80 - (len(msga) + len(msgb))) 241 242 # If -q is specified, only print a line for tests that didn't pass. 243 # This means passing tests need to be logged as DEBUG, or the one 244 # line summary will only be printed in the logfile for failures. 245 if not options.quiet: 246 logger.info('%s%s%s' % (msga, pad, msgb)) 247 elif self.result.result is not 'PASS': 248 logger.info('%s%s%s' % (msga, pad, msgb)) 249 else: 250 logger.debug('%s%s%s' % (msga, pad, msgb)) 251 252 lines = self.result.stdout + self.result.stderr 253 for dt, line in sorted(lines): 254 logger.debug('%s %s' % (dt.strftime("%H:%M:%S.%f ")[:11], line)) 255 256 if len(self.result.stdout): 257 with open(os.path.join(self.outputdir, 'stdout'), 'w') as out: 258 for _, line in self.result.stdout: 259 os.write(out.fileno(), '%s\n' % line) 260 if len(self.result.stderr): 261 with open(os.path.join(self.outputdir, 'stderr'), 'w') as err: 262 for _, line in self.result.stderr: 263 os.write(err.fileno(), '%s\n' % line) 264 if len(self.result.stdout) and len(self.result.stderr): 265 with open(os.path.join(self.outputdir, 'merged'), 'w') as merged: 266 for _, line in sorted(lines): 267 os.write(merged.fileno(), '%s\n' % line) 268 269 270 class Test(Cmd): 271 props = ['outputdir', 'timeout', 'user', 'pre', 'pre_user', 'post', 272 'post_user'] 273 274 def __init__(self, pathname, outputdir=None, timeout=None, user=None, 275 pre=None, pre_user=None, post=None, post_user=None): 276 super(Test, self).__init__(pathname, outputdir, timeout, user) 277 self.pre = pre or '' 278 self.pre_user = pre_user or '' 279 self.post = post or '' 280 self.post_user = post_user or '' 281 282 def __str__(self): 283 post_user = pre_user = '' 284 if len(self.pre_user): 285 pre_user = ' (as %s)' % (self.pre_user) 286 if len(self.post_user): 287 post_user = ' (as %s)' % (self.post_user) 288 return "Pathname: %s\nOutputdir: %s\nTimeout: %s\nPre: %s%s\nPost: " \ 289 "%s%s\nUser: %s\n" % (self.pathname, self.outputdir, 290 self.timeout, self.pre, pre_user, self.post, post_user, 291 self.user) 292 293 def verify(self, logger): 294 """ 295 Check the pre/post scripts, user and Test. Omit the Test from this 296 run if there are any problems. 297 """ 298 files = [self.pre, self.pathname, self.post] 299 users = [self.pre_user, self.user, self.post_user] 300 301 for f in [f for f in files if len(f)]: 302 if not verify_file(f): 303 logger.info("Warning: Test '%s' not added to this run because" 304 " it failed verification." % f) 305 return False 306 307 for user in [user for user in users if len(user)]: 308 if not verify_user(user, logger): 309 logger.info("Not adding Test '%s' to this run." % 310 self.pathname) 311 return False 312 313 return True 314 315 def run(self, logger, options): 316 """ 317 Create Cmd instances for the pre/post scripts. If the pre script 318 doesn't pass, skip this Test. Run the post script regardless. 319 """ 320 pretest = Cmd(self.pre, outputdir=os.path.join(self.outputdir, 321 os.path.basename(self.pre)), timeout=self.timeout, 322 user=self.pre_user) 323 test = Cmd(self.pathname, outputdir=self.outputdir, 324 timeout=self.timeout, user=self.user) 325 posttest = Cmd(self.post, outputdir=os.path.join(self.outputdir, 326 os.path.basename(self.post)), timeout=self.timeout, 327 user=self.post_user) 328 329 cont = True 330 if len(pretest.pathname): 331 pretest.run(options) 332 cont = pretest.result.result is 'PASS' 333 pretest.log(logger, options) 334 335 if cont: 336 test.run(options) 337 else: 338 test.skip() 339 340 test.log(logger, options) 341 342 if len(posttest.pathname): 343 posttest.run(options) 344 posttest.log(logger, options) 345 346 347 class TestGroup(Test): 348 props = Test.props + ['tests'] 349 350 def __init__(self, pathname, outputdir=None, timeout=None, user=None, 351 pre=None, pre_user=None, post=None, post_user=None, 352 tests=None): 353 super(TestGroup, self).__init__(pathname, outputdir, timeout, user, 354 pre, pre_user, post, post_user) 355 self.tests = tests or [] 356 357 def __str__(self): 358 post_user = pre_user = '' 359 if len(self.pre_user): 360 pre_user = ' (as %s)' % (self.pre_user) 361 if len(self.post_user): 362 post_user = ' (as %s)' % (self.post_user) 363 return "Pathname: %s\nOutputdir: %s\nTests: %s\nTimeout: %s\n" \ 364 "Pre: %s%s\nPost: %s%s\nUser: %s\n" % (self.pathname, 365 self.outputdir, self.tests, self.timeout, self.pre, pre_user, 366 self.post, post_user, self.user) 367 368 def verify(self, logger): 369 """ 370 Check the pre/post scripts, user and tests in this TestGroup. Omit 371 the TestGroup entirely, or simply delete the relevant tests in the 372 group, if that's all that's required. 373 """ 374 # If the pre or post scripts are relative pathnames, convert to 375 # absolute, so they stand a chance of passing verification. 376 if len(self.pre) and not os.path.isabs(self.pre): 377 self.pre = os.path.join(self.pathname, self.pre) 378 if len(self.post) and not os.path.isabs(self.post): 379 self.post = os.path.join(self.pathname, self.post) 380 381 auxfiles = [self.pre, self.post] 382 users = [self.pre_user, self.user, self.post_user] 383 384 for f in [f for f in auxfiles if len(f)]: 385 if self.pathname != os.path.dirname(f): 386 logger.info("Warning: TestGroup '%s' not added to this run. " 387 "Auxiliary script '%s' exists in a different " 388 "directory." % (self.pathname, f)) 389 return False 390 391 if not verify_file(f): 392 logger.info("Warning: TestGroup '%s' not added to this run. " 393 "Auxiliary script '%s' failed verification." % 394 (self.pathname, f)) 395 return False 396 397 for user in [user for user in users if len(user)]: 398 if not verify_user(user, logger): 399 logger.info("Not adding TestGroup '%s' to this run." % 400 self.pathname) 401 return False 402 403 # If one of the tests is invalid, delete it, log it, and drive on. 404 for test in self.tests: 405 if not verify_file(os.path.join(self.pathname, test)): 406 del self.tests[self.tests.index(test)] 407 logger.info("Warning: Test '%s' removed from TestGroup '%s' " 408 "because it failed verification." % (test, 409 self.pathname)) 410 411 return len(self.tests) is not 0 412 413 def run(self, logger, options): 414 """ 415 Create Cmd instances for the pre/post scripts. If the pre script 416 doesn't pass, skip all the tests in this TestGroup. Run the post 417 script regardless. 418 """ 419 pretest = Cmd(self.pre, outputdir=os.path.join(self.outputdir, 420 os.path.basename(self.pre)), timeout=self.timeout, 421 user=self.pre_user) 422 posttest = Cmd(self.post, outputdir=os.path.join(self.outputdir, 423 os.path.basename(self.post)), timeout=self.timeout, 424 user=self.post_user) 425 426 cont = True 427 if len(pretest.pathname): 428 pretest.run(options) 429 cont = pretest.result.result is 'PASS' 430 pretest.log(logger, options) 431 432 for fname in self.tests: 433 test = Cmd(os.path.join(self.pathname, fname), 434 outputdir=os.path.join(self.outputdir, fname), 435 timeout=self.timeout, user=self.user) 436 if cont: 437 test.run(options) 438 else: 439 test.skip() 440 441 test.log(logger, options) 442 443 if len(posttest.pathname): 444 posttest.run(options) 445 posttest.log(logger, options) 446 447 448 class TestRun(object): 449 props = ['quiet', 'outputdir'] 450 451 def __init__(self, options): 452 self.tests = {} 453 self.testgroups = {} 454 self.starttime = time() 455 self.timestamp = datetime.now().strftime('%Y%m%dT%H%M%S') 456 self.outputdir = os.path.join(options.outputdir, self.timestamp) 457 self.logger = self.setup_logging(options) 458 self.defaults = [ 459 ('outputdir', BASEDIR), 460 ('quiet', False), 461 ('timeout', 60), 462 ('user', ''), 463 ('pre', ''), 464 ('pre_user', ''), 465 ('post', ''), 466 ('post_user', '') 467 ] 468 469 def __str__(self): 470 s = 'TestRun:\n outputdir: %s\n' % self.outputdir 471 s += 'TESTS:\n' 472 for key in sorted(self.tests.keys()): 473 s += '%s%s' % (self.tests[key].__str__(), '\n') 474 s += 'TESTGROUPS:\n' 475 for key in sorted(self.testgroups.keys()): 476 s += '%s%s' % (self.testgroups[key].__str__(), '\n') 477 return s 478 479 def addtest(self, pathname, options): 480 """ 481 Create a new Test, and apply any properties that were passed in 482 from the command line. If it passes verification, add it to the 483 TestRun. 484 """ 485 test = Test(pathname) 486 for prop in Test.props: 487 setattr(test, prop, getattr(options, prop)) 488 489 if test.verify(self.logger): 490 self.tests[pathname] = test 491 492 def addtestgroup(self, dirname, filenames, options): 493 """ 494 Create a new TestGroup, and apply any properties that were passed 495 in from the command line. If it passes verification, add it to the 496 TestRun. 497 """ 498 if dirname not in self.testgroups: 499 testgroup = TestGroup(dirname) 500 for prop in Test.props: 501 setattr(testgroup, prop, getattr(options, prop)) 502 503 # Prevent pre/post scripts from running as regular tests 504 for f in [testgroup.pre, testgroup.post]: 505 if f in filenames: 506 del filenames[filenames.index(f)] 507 508 self.testgroups[dirname] = testgroup 509 self.testgroups[dirname].tests = sorted(filenames) 510 511 testgroup.verify(self.logger) 512 513 def read(self, logger, options): 514 """ 515 Read in the specified runfile, and apply the TestRun properties 516 listed in the 'DEFAULT' section to our TestRun. Then read each 517 section, and apply the appropriate properties to the Test or 518 TestGroup. Properties from individual sections override those set 519 in the 'DEFAULT' section. If the Test or TestGroup passes 520 verification, add it to the TestRun. 521 """ 522 config = ConfigParser.RawConfigParser() 523 if not len(config.read(options.runfile)): 524 fail("Coulnd't read config file %s" % options.runfile) 525 526 for opt in TestRun.props: 527 if config.has_option('DEFAULT', opt): 528 setattr(self, opt, config.get('DEFAULT', opt)) 529 self.outputdir = os.path.join(self.outputdir, self.timestamp) 530 531 for section in config.sections(): 532 if 'tests' in config.options(section): 533 testgroup = TestGroup(section) 534 for prop in TestGroup.props: 535 try: 536 setattr(testgroup, prop, config.get('DEFAULT', prop)) 537 setattr(testgroup, prop, config.get(section, prop)) 538 except ConfigParser.NoOptionError: 539 pass 540 541 # Repopulate tests using eval to convert the string to a list 542 testgroup.tests = eval(config.get(section, 'tests')) 543 544 if testgroup.verify(logger): 545 self.testgroups[section] = testgroup 546 else: 547 test = Test(section) 548 for prop in Test.props: 549 try: 550 setattr(test, prop, config.get('DEFAULT', prop)) 551 setattr(test, prop, config.get(section, prop)) 552 except ConfigParser.NoOptionError: 553 pass 554 if test.verify(logger): 555 self.tests[section] = test 556 557 def write(self, options): 558 """ 559 Create a configuration file for editing and later use. The 560 'DEFAULT' section of the config file is created from the 561 properties that were specified on the command line. Tests are 562 simply added as sections that inherit everything from the 563 'DEFAULT' section. TestGroups are the same, except they get an 564 option including all the tests to run in that directory. 565 """ 566 567 defaults = dict([(prop, getattr(options, prop)) for prop, _ in 568 self.defaults]) 569 config = ConfigParser.RawConfigParser(defaults) 570 571 for test in sorted(self.tests.keys()): 572 config.add_section(test) 573 574 for testgroup in sorted(self.testgroups.keys()): 575 config.add_section(testgroup) 576 config.set(testgroup, 'tests', self.testgroups[testgroup].tests) 577 578 try: 579 with open(options.template, 'w') as f: 580 return config.write(f) 581 except IOError: 582 fail('Could not open \'%s\' for writing.' % options.template) 583 584 def complete_outputdirs(self, options): 585 """ 586 Collect all the pathnames for Tests, and TestGroups. Work 587 backwards one pathname component at a time, to create a unique 588 directory name in which to deposit test output. Tests will be able 589 to write output files directly in the newly modified outputdir. 590 TestGroups will be able to create one subdirectory per test in the 591 outputdir, and are guaranteed uniqueness because a group can only 592 contain files in one directory. Pre and post tests will create a 593 directory rooted at the outputdir of the Test or TestGroup in 594 question for their output. 595 """ 596 done = False 597 components = 0 598 tmp_dict = dict(self.tests.items() + self.testgroups.items()) 599 total = len(tmp_dict) 600 base = self.outputdir 601 602 while not done: 603 l = [] 604 components -= 1 605 for testfile in tmp_dict.keys(): 606 uniq = '/'.join(testfile.split('/')[components:]).lstrip('/') 607 if not uniq in l: 608 l.append(uniq) 609 tmp_dict[testfile].outputdir = os.path.join(base, uniq) 610 else: 611 break 612 done = total == len(l) 613 614 def setup_logging(self, options): 615 """ 616 Two loggers are set up here. The first is for the logfile which 617 will contain one line summarizing the test, including the test 618 name, result, and running time. This logger will also capture the 619 timestamped combined stdout and stderr of each run. The second 620 logger is optional console output, which will contain only the one 621 line summary. The loggers are initialized at two different levels 622 to facilitate segregating the output. 623 """ 624 if options.dryrun is True: 625 return 626 627 testlogger = logging.getLogger(__name__) 628 testlogger.setLevel(logging.DEBUG) 629 630 if options.cmd is not 'wrconfig': 631 try: 632 old = os.umask(0) 633 os.makedirs(self.outputdir, mode=0777) 634 os.umask(old) 635 except OSError, e: 636 fail('%s' % e) 637 filename = os.path.join(self.outputdir, 'log') 638 639 logfile = logging.FileHandler(filename) 640 logfile.setLevel(logging.DEBUG) 641 logfilefmt = logging.Formatter('%(message)s') 642 logfile.setFormatter(logfilefmt) 643 testlogger.addHandler(logfile) 644 645 cons = logging.StreamHandler() 646 cons.setLevel(logging.INFO) 647 consfmt = logging.Formatter('%(message)s') 648 cons.setFormatter(consfmt) 649 testlogger.addHandler(cons) 650 651 return testlogger 652 653 def run(self, options): 654 """ 655 Walk through all the Tests and TestGroups, calling run(). 656 """ 657 try: 658 os.chdir(self.outputdir) 659 except OSError: 660 fail('Could not change to directory %s' % self.outputdir) 661 for test in sorted(self.tests.keys()): 662 self.tests[test].run(self.logger, options) 663 for testgroup in sorted(self.testgroups.keys()): 664 self.testgroups[testgroup].run(self.logger, options) 665 666 def summary(self): 667 if Result.total is 0: 668 return 669 670 print '\nResults Summary' 671 for key in Result.runresults.keys(): 672 if Result.runresults[key] is not 0: 673 print '%s\t% 4d' % (key, Result.runresults[key]) 674 675 m, s = divmod(time() - self.starttime, 60) 676 h, m = divmod(m, 60) 677 print '\nRunning Time:\t%02d:%02d:%02d' % (h, m, s) 678 print 'Percent passed:\t%.1f%%' % ((float(Result.runresults['PASS']) / 679 float(Result.total)) * 100) 680 print 'Log directory:\t%s' % self.outputdir 681 682 683 def verify_file(pathname): 684 """ 685 Verify that the supplied pathname is an executable regular file. 686 """ 687 if os.path.isdir(pathname) or os.path.islink(pathname): 688 return False 689 690 if os.path.isfile(pathname) and os.access(pathname, os.X_OK): 691 return True 692 693 return False 694 695 696 def verify_user(user, logger): 697 """ 698 Verify that the specified user exists on this system, and can execute 699 sudo without being prompted for a password. 700 """ 701 testcmd = [SUDO, '-n', '-u', user, TRUE] 702 can_sudo = exists = True 703 704 if user in Cmd.verified_users: 705 return True 706 707 try: 708 _ = getpwnam(user) 709 except KeyError: 710 exists = False 711 logger.info("Warning: user '%s' does not exist.", user) 712 return False 713 714 p = Popen(testcmd) 715 p.wait() 716 if p.returncode is not 0: 717 logger.info("Warning: user '%s' cannot use passwordless sudo.", user) 718 return False 719 else: 720 Cmd.verified_users.append(user) 721 722 return True 723 724 725 def find_tests(testrun, options): 726 """ 727 For the given list of pathnames, add files as Tests. For directories, 728 if do_groups is True, add the directory as a TestGroup. If False, 729 recursively search for executable files. 730 """ 731 732 for p in sorted(options.pathnames): 733 if os.path.isdir(p): 734 for dirname, _, filenames in os.walk(p): 735 if options.do_groups: 736 testrun.addtestgroup(dirname, filenames, options) 737 else: 738 for f in sorted(filenames): 739 testrun.addtest(os.path.join(dirname, f), options) 740 else: 741 testrun.addtest(p, options) 742 743 744 def fail(retstr, ret=1): 745 print '%s: %s' % (argv[0], retstr) 746 exit(ret) 747 748 749 def options_cb(option, opt_str, value, parser): 750 path_options = ['runfile', 'outputdir', 'template'] 751 752 if option.dest is 'runfile' and '-w' in parser.rargs or \ 753 option.dest is 'template' and '-c' in parser.rargs: 754 fail('-c and -w are mutually exclusive.') 755 756 if opt_str in parser.rargs: 757 fail('%s may only be specified once.' % opt_str) 758 759 if option.dest is 'runfile': 760 parser.values.cmd = 'rdconfig' 761 if option.dest is 'template': 762 parser.values.cmd = 'wrconfig' 763 764 setattr(parser.values, option.dest, value) 765 if option.dest in path_options: 766 setattr(parser.values, option.dest, os.path.abspath(value)) 767 768 769 def parse_args(): 770 parser = OptionParser() 771 parser.add_option('-c', action='callback', callback=options_cb, 772 type='string', dest='runfile', metavar='runfile', 773 help='Specify tests to run via config file.') 774 parser.add_option('-d', action='store_true', default=False, dest='dryrun', 775 help='Dry run. Print tests, but take no other action.') 776 parser.add_option('-g', action='store_true', default=False, 777 dest='do_groups', help='Make directories TestGroups.') 778 parser.add_option('-o', action='callback', callback=options_cb, 779 default=BASEDIR, dest='outputdir', type='string', 780 metavar='outputdir', help='Specify an output directory.') 781 parser.add_option('-p', action='callback', callback=options_cb, 782 default='', dest='pre', metavar='script', 783 type='string', help='Specify a pre script.') 784 parser.add_option('-P', action='callback', callback=options_cb, 785 default='', dest='post', metavar='script', 786 type='string', help='Specify a post script.') 787 parser.add_option('-q', action='store_true', default=False, dest='quiet', 788 help='Silence on the console during a test run.') 789 parser.add_option('-t', action='callback', callback=options_cb, default=60, 790 dest='timeout', metavar='seconds', type='int', 791 help='Timeout (in seconds) for an individual test.') 792 parser.add_option('-u', action='callback', callback=options_cb, 793 default='', dest='user', metavar='user', type='string', 794 help='Specify a different user name to run as.') 795 parser.add_option('-w', action='callback', callback=options_cb, 796 default=None, dest='template', metavar='template', 797 type='string', help='Create a new config file.') 798 parser.add_option('-x', action='callback', callback=options_cb, default='', 799 dest='pre_user', metavar='pre_user', type='string', 800 help='Specify a user to execute the pre script.') 801 parser.add_option('-X', action='callback', callback=options_cb, default='', 802 dest='post_user', metavar='post_user', type='string', 803 help='Specify a user to execute the post script.') 804 (options, pathnames) = parser.parse_args() 805 806 if not options.runfile and not options.template: 807 options.cmd = 'runtests' 808 809 if options.runfile and len(pathnames): 810 fail('Extraneous arguments.') 811 812 options.pathnames = [os.path.abspath(path) for path in pathnames] 813 814 return options 815 816 817 def main(args): 818 options = parse_args() 819 testrun = TestRun(options) 820 821 if options.cmd is 'runtests': 822 find_tests(testrun, options) 823 elif options.cmd is 'rdconfig': 824 testrun.read(testrun.logger, options) 825 elif options.cmd is 'wrconfig': 826 find_tests(testrun, options) 827 testrun.write(options) 828 exit(0) 829 else: 830 fail('Unknown command specified') 831 832 testrun.complete_outputdirs(options) 833 testrun.run(options) 834 testrun.summary() 835 exit(0) 836 837 838 if __name__ == '__main__': 839 main(argv[1:])