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