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