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