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()