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