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