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