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