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