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