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