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) 2012 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         user = ' (run as %s)' % self.user if len(self.user) else ''
 238         msga = 'Test: %s%s ' % (self.pathname, user)
 239         msgb = '[%s] [%s]' % (self.result.runtime, self.result.result)
 240         pad = ' ' * (80 - (len(msga) + len(msgb)))
 241 
 242         # If -q is specified, only print a line for tests that didn't pass.
 243         # This means passing tests need to be logged as DEBUG, or the one
 244         # line summary will only be printed in the logfile for failures.
 245         if not options.quiet:
 246             logger.info('%s%s%s' % (msga, pad, msgb))
 247         elif self.result.result is not 'PASS':
 248             logger.info('%s%s%s' % (msga, pad, msgb))
 249         else:
 250             logger.debug('%s%s%s' % (msga, pad, msgb))
 251 
 252         lines = self.result.stdout + self.result.stderr
 253         for dt, line in sorted(lines):
 254             logger.debug('%s %s' % (dt.strftime("%H:%M:%S.%f ")[:11], line))
 255 
 256         if len(self.result.stdout):
 257             with open(os.path.join(self.outputdir, 'stdout'), 'w') as out:
 258                 for _, line in self.result.stdout:
 259                     os.write(out.fileno(), '%s\n' % line)
 260         if len(self.result.stderr):
 261             with open(os.path.join(self.outputdir, 'stderr'), 'w') as err:
 262                 for _, line in self.result.stderr:
 263                     os.write(err.fileno(), '%s\n' % line)
 264         if len(self.result.stdout) and len(self.result.stderr):
 265             with open(os.path.join(self.outputdir, 'merged'), 'w') as merged:
 266                 for _, line in sorted(lines):
 267                     os.write(merged.fileno(), '%s\n' % line)
 268 
 269 
 270 class Test(Cmd):
 271     props = ['outputdir', 'timeout', 'user', 'pre', 'pre_user', 'post',
 272              'post_user']
 273 
 274     def __init__(self, pathname, outputdir=None, timeout=None, user=None,
 275                  pre=None, pre_user=None, post=None, post_user=None):
 276         super(Test, self).__init__(pathname, outputdir, timeout, user)
 277         self.pre = pre or ''
 278         self.pre_user = pre_user or ''
 279         self.post = post or ''
 280         self.post_user = post_user or ''
 281 
 282     def __str__(self):
 283         post_user = pre_user = ''
 284         if len(self.pre_user):
 285             pre_user = ' (as %s)' % (self.pre_user)
 286         if len(self.post_user):
 287             post_user = ' (as %s)' % (self.post_user)
 288         return "Pathname: %s\nOutputdir: %s\nTimeout: %s\nPre: %s%s\nPost: " \
 289                "%s%s\nUser: %s\n" % (self.pathname, self.outputdir,
 290                 self.timeout, self.pre, pre_user, self.post, post_user,
 291                 self.user)
 292 
 293     def verify(self, logger):
 294         """
 295         Check the pre/post scripts, user and Test. Omit the Test from this
 296         run if there are any problems.
 297         """
 298         files = [self.pre, self.pathname, self.post]
 299         users = [self.pre_user, self.user, self.post_user]
 300 
 301         for f in [f for f in files if len(f)]:
 302             if not verify_file(f):
 303                 logger.info("Warning: Test '%s' not added to this run because"
 304                             " it failed verification." % f)
 305                 return False
 306 
 307         for user in [user for user in users if len(user)]:
 308             if not verify_user(user, logger):
 309                 logger.info("Not adding Test '%s' to this run." %
 310                             self.pathname)
 311                 return False
 312 
 313         return True
 314 
 315     def run(self, logger, options):
 316         """
 317         Create Cmd instances for the pre/post scripts. If the pre script
 318         doesn't pass, skip this Test. Run the post script regardless.
 319         """
 320         pretest = Cmd(self.pre, outputdir=os.path.join(self.outputdir,
 321                       os.path.basename(self.pre)), timeout=self.timeout,
 322                       user=self.pre_user)
 323         test = Cmd(self.pathname, outputdir=self.outputdir,
 324                    timeout=self.timeout, user=self.user)
 325         posttest = Cmd(self.post, outputdir=os.path.join(self.outputdir,
 326                        os.path.basename(self.post)), timeout=self.timeout,
 327                        user=self.post_user)
 328 
 329         cont = True
 330         if len(pretest.pathname):
 331             pretest.run(options)
 332             cont = pretest.result.result is 'PASS'
 333             pretest.log(logger, options)
 334 
 335         if cont:
 336             test.run(options)
 337         else:
 338             test.skip()
 339 
 340         test.log(logger, options)
 341 
 342         if len(posttest.pathname):
 343             posttest.run(options)
 344             posttest.log(logger, options)
 345 
 346 
 347 class TestGroup(Test):
 348     props = Test.props + ['tests']
 349 
 350     def __init__(self, pathname, outputdir=None, timeout=None, user=None,
 351                  pre=None, pre_user=None, post=None, post_user=None,
 352                  tests=None):
 353         super(TestGroup, self).__init__(pathname, outputdir, timeout, user,
 354                                         pre, pre_user, post, post_user)
 355         self.tests = tests or []
 356 
 357     def __str__(self):
 358         post_user = pre_user = ''
 359         if len(self.pre_user):
 360             pre_user = ' (as %s)' % (self.pre_user)
 361         if len(self.post_user):
 362             post_user = ' (as %s)' % (self.post_user)
 363         return "Pathname: %s\nOutputdir: %s\nTests: %s\nTimeout: %s\n" \
 364                "Pre: %s%s\nPost: %s%s\nUser: %s\n" % (self.pathname,
 365                 self.outputdir, self.tests, self.timeout, self.pre, pre_user,
 366                 self.post, post_user, self.user)
 367 
 368     def verify(self, logger):
 369         """
 370         Check the pre/post scripts, user and tests in this TestGroup. Omit
 371         the TestGroup entirely, or simply delete the relevant tests in the
 372         group, if that's all that's required.
 373         """
 374         # If the pre or post scripts are relative pathnames, convert to
 375         # absolute, so they stand a chance of passing verification.
 376         if len(self.pre) and not os.path.isabs(self.pre):
 377             self.pre = os.path.join(self.pathname, self.pre)
 378         if len(self.post) and not os.path.isabs(self.post):
 379             self.post = os.path.join(self.pathname, self.post)
 380 
 381         auxfiles = [self.pre, self.post]
 382         users = [self.pre_user, self.user, self.post_user]
 383 
 384         for f in [f for f in auxfiles if len(f)]:
 385             if self.pathname != os.path.dirname(f):
 386                 logger.info("Warning: TestGroup '%s' not added to this run. "
 387                             "Auxiliary script '%s' exists in a different "
 388                             "directory." % (self.pathname, f))
 389                 return False
 390 
 391             if not verify_file(f):
 392                 logger.info("Warning: TestGroup '%s' not added to this run. "
 393                             "Auxiliary script '%s' failed verification." %
 394                             (self.pathname, f))
 395                 return False
 396 
 397         for user in [user for user in users if len(user)]:
 398             if not verify_user(user, logger):
 399                 logger.info("Not adding TestGroup '%s' to this run." %
 400                             self.pathname)
 401                 return False
 402 
 403         # If one of the tests is invalid, delete it, log it, and drive on.
 404         for test in self.tests:
 405             if not verify_file(os.path.join(self.pathname, test)):
 406                 del self.tests[self.tests.index(test)]
 407                 logger.info("Warning: Test '%s' removed from TestGroup '%s' "
 408                             "because it failed verification." % (test,
 409                             self.pathname))
 410 
 411         return len(self.tests) is not 0
 412 
 413     def run(self, logger, options):
 414         """
 415         Create Cmd instances for the pre/post scripts. If the pre script
 416         doesn't pass, skip all the tests in this TestGroup. Run the post
 417         script regardless.
 418         """
 419         pretest = Cmd(self.pre, outputdir=os.path.join(self.outputdir,
 420                       os.path.basename(self.pre)), timeout=self.timeout,
 421                       user=self.pre_user)
 422         posttest = Cmd(self.post, outputdir=os.path.join(self.outputdir,
 423                        os.path.basename(self.post)), timeout=self.timeout,
 424                        user=self.post_user)
 425 
 426         cont = True
 427         if len(pretest.pathname):
 428             pretest.run(options)
 429             cont = pretest.result.result is 'PASS'
 430             pretest.log(logger, options)
 431 
 432         for fname in self.tests:
 433             test = Cmd(os.path.join(self.pathname, fname),
 434                        outputdir=os.path.join(self.outputdir, fname),
 435                        timeout=self.timeout, user=self.user)
 436             if cont:
 437                 test.run(options)
 438             else:
 439                 test.skip()
 440 
 441             test.log(logger, options)
 442 
 443         if len(posttest.pathname):
 444             posttest.run(options)
 445             posttest.log(logger, options)
 446 
 447 
 448 class TestRun(object):
 449     props = ['quiet', 'outputdir']
 450 
 451     def __init__(self, options):
 452         self.tests = {}
 453         self.testgroups = {}
 454         self.starttime = time()
 455         self.timestamp = datetime.now().strftime('%Y%m%dT%H%M%S')
 456         self.outputdir = os.path.join(options.outputdir, self.timestamp)
 457         self.logger = self.setup_logging(options)
 458         self.defaults = [
 459             ('outputdir', BASEDIR),
 460             ('quiet', False),
 461             ('timeout', 60),
 462             ('user', ''),
 463             ('pre', ''),
 464             ('pre_user', ''),
 465             ('post', ''),
 466             ('post_user', '')
 467         ]
 468 
 469     def __str__(self):
 470         s = 'TestRun:\n    outputdir: %s\n' % self.outputdir
 471         s += 'TESTS:\n'
 472         for key in sorted(self.tests.keys()):
 473             s += '%s%s' % (self.tests[key].__str__(), '\n')
 474         s += 'TESTGROUPS:\n'
 475         for key in sorted(self.testgroups.keys()):
 476             s += '%s%s' % (self.testgroups[key].__str__(), '\n')
 477         return s
 478 
 479     def addtest(self, pathname, options):
 480         """
 481         Create a new Test, and apply any properties that were passed in
 482         from the command line. If it passes verification, add it to the
 483         TestRun.
 484         """
 485         test = Test(pathname)
 486         for prop in Test.props:
 487             setattr(test, prop, getattr(options, prop))
 488 
 489         if test.verify(self.logger):
 490             self.tests[pathname] = test
 491 
 492     def addtestgroup(self, dirname, filenames, options):
 493         """
 494         Create a new TestGroup, and apply any properties that were passed
 495         in from the command line. If it passes verification, add it to the
 496         TestRun.
 497         """
 498         if dirname not in self.testgroups:
 499             testgroup = TestGroup(dirname)
 500             for prop in Test.props:
 501                 setattr(testgroup, prop, getattr(options, prop))
 502 
 503             # Prevent pre/post scripts from running as regular tests
 504             for f in [testgroup.pre, testgroup.post]:
 505                 if f in filenames:
 506                     del filenames[filenames.index(f)]
 507 
 508             self.testgroups[dirname] = testgroup
 509             self.testgroups[dirname].tests = sorted(filenames)
 510 
 511             testgroup.verify(self.logger)
 512 
 513     def read(self, logger, options):
 514         """
 515         Read in the specified runfile, and apply the TestRun properties
 516         listed in the 'DEFAULT' section to our TestRun. Then read each
 517         section, and apply the appropriate properties to the Test or
 518         TestGroup. Properties from individual sections override those set
 519         in the 'DEFAULT' section. If the Test or TestGroup passes
 520         verification, add it to the TestRun.
 521         """
 522         config = ConfigParser.RawConfigParser()
 523         if not len(config.read(options.runfile)):
 524             fail("Coulnd't read config file %s" % options.runfile)
 525 
 526         for opt in TestRun.props:
 527             if config.has_option('DEFAULT', opt):
 528                 setattr(self, opt, config.get('DEFAULT', opt))
 529         self.outputdir = os.path.join(self.outputdir, self.timestamp)
 530 
 531         for section in config.sections():
 532             if 'tests' in config.options(section):
 533                 testgroup = TestGroup(section)
 534                 for prop in TestGroup.props:
 535                     try:
 536                         setattr(testgroup, prop, config.get('DEFAULT', prop))
 537                         setattr(testgroup, prop, config.get(section, prop))
 538                     except ConfigParser.NoOptionError:
 539                         pass
 540 
 541                 # Repopulate tests using eval to convert the string to a list
 542                 testgroup.tests = eval(config.get(section, 'tests'))
 543 
 544                 if testgroup.verify(logger):
 545                     self.testgroups[section] = testgroup
 546             else:
 547                 test = Test(section)
 548                 for prop in Test.props:
 549                     try:
 550                         setattr(test, prop, config.get('DEFAULT', prop))
 551                         setattr(test, prop, config.get(section, prop))
 552                     except ConfigParser.NoOptionError:
 553                         pass
 554                 if test.verify(logger):
 555                     self.tests[section] = test
 556 
 557     def write(self, options):
 558         """
 559         Create a configuration file for editing and later use. The
 560         'DEFAULT' section of the config file is created from the
 561         properties that were specified on the command line. Tests are
 562         simply added as sections that inherit everything from the
 563         'DEFAULT' section. TestGroups are the same, except they get an
 564         option including all the tests to run in that directory.
 565         """
 566 
 567         defaults = dict([(prop, getattr(options, prop)) for prop, _ in
 568                         self.defaults])
 569         config = ConfigParser.RawConfigParser(defaults)
 570 
 571         for test in sorted(self.tests.keys()):
 572             config.add_section(test)
 573 
 574         for testgroup in sorted(self.testgroups.keys()):
 575             config.add_section(testgroup)
 576             config.set(testgroup, 'tests', self.testgroups[testgroup].tests)
 577 
 578         try:
 579             with open(options.template, 'w') as f:
 580                 return config.write(f)
 581         except IOError:
 582             fail('Could not open \'%s\' for writing.' % options.template)
 583 
 584     def complete_outputdirs(self, options):
 585         """
 586         Collect all the pathnames for Tests, and TestGroups. Work
 587         backwards one pathname component at a time, to create a unique
 588         directory name in which to deposit test output. Tests will be able
 589         to write output files directly in the newly modified outputdir.
 590         TestGroups will be able to create one subdirectory per test in the
 591         outputdir, and are guaranteed uniqueness because a group can only
 592         contain files in one directory. Pre and post tests will create a
 593         directory rooted at the outputdir of the Test or TestGroup in
 594         question for their output.
 595         """
 596         done = False
 597         components = 0
 598         tmp_dict = dict(self.tests.items() + self.testgroups.items())
 599         total = len(tmp_dict)
 600         base = self.outputdir
 601 
 602         while not done:
 603             l = []
 604             components -= 1
 605             for testfile in tmp_dict.keys():
 606                 uniq = '/'.join(testfile.split('/')[components:]).lstrip('/')
 607                 if not uniq in l:
 608                     l.append(uniq)
 609                     tmp_dict[testfile].outputdir = os.path.join(base, uniq)
 610                 else:
 611                     break
 612             done = total == len(l)
 613 
 614     def setup_logging(self, options):
 615         """
 616         Two loggers are set up here. The first is for the logfile which
 617         will contain one line summarizing the test, including the test
 618         name, result, and running time. This logger will also capture the
 619         timestamped combined stdout and stderr of each run. The second
 620         logger is optional console output, which will contain only the one
 621         line summary. The loggers are initialized at two different levels
 622         to facilitate segregating the output.
 623         """
 624         if options.dryrun is True:
 625             return
 626 
 627         testlogger = logging.getLogger(__name__)
 628         testlogger.setLevel(logging.DEBUG)
 629 
 630         if options.cmd is not 'wrconfig':
 631             try:
 632                 old = os.umask(0)
 633                 os.makedirs(self.outputdir, mode=0777)
 634                 os.umask(old)
 635             except OSError, e:
 636                 fail('%s' % e)
 637             filename = os.path.join(self.outputdir, 'log')
 638 
 639             logfile = logging.FileHandler(filename)
 640             logfile.setLevel(logging.DEBUG)
 641             logfilefmt = logging.Formatter('%(message)s')
 642             logfile.setFormatter(logfilefmt)
 643             testlogger.addHandler(logfile)
 644 
 645         cons = logging.StreamHandler()
 646         cons.setLevel(logging.INFO)
 647         consfmt = logging.Formatter('%(message)s')
 648         cons.setFormatter(consfmt)
 649         testlogger.addHandler(cons)
 650 
 651         return testlogger
 652 
 653     def run(self, options):
 654         """
 655         Walk through all the Tests and TestGroups, calling run().
 656         """
 657         try:
 658             os.chdir(self.outputdir)
 659         except OSError:
 660             fail('Could not change to directory %s' % self.outputdir)
 661         for test in sorted(self.tests.keys()):
 662             self.tests[test].run(self.logger, options)
 663         for testgroup in sorted(self.testgroups.keys()):
 664             self.testgroups[testgroup].run(self.logger, options)
 665 
 666     def summary(self):
 667         if Result.total is 0:
 668             return
 669 
 670         print '\nResults Summary'
 671         for key in Result.runresults.keys():
 672             if Result.runresults[key] is not 0:
 673                 print '%s\t% 4d' % (key, Result.runresults[key])
 674 
 675         m, s = divmod(time() - self.starttime, 60)
 676         h, m = divmod(m, 60)
 677         print '\nRunning Time:\t%02d:%02d:%02d' % (h, m, s)
 678         print 'Percent passed:\t%.1f%%' % ((float(Result.runresults['PASS']) /
 679                float(Result.total)) * 100)
 680         print 'Log directory:\t%s' % self.outputdir
 681 
 682 
 683 def verify_file(pathname):
 684     """
 685     Verify that the supplied pathname is an executable regular file.
 686     """
 687     if os.path.isdir(pathname) or os.path.islink(pathname):
 688         return False
 689 
 690     if os.path.isfile(pathname) and os.access(pathname, os.X_OK):
 691         return True
 692 
 693     return False
 694 
 695 
 696 def verify_user(user, logger):
 697     """
 698     Verify that the specified user exists on this system, and can execute
 699     sudo without being prompted for a password.
 700     """
 701     testcmd = [SUDO, '-n', '-u', user, TRUE]
 702     can_sudo = exists = True
 703 
 704     if user in Cmd.verified_users:
 705         return True
 706 
 707     try:
 708         _ = getpwnam(user)
 709     except KeyError:
 710         exists = False
 711         logger.info("Warning: user '%s' does not exist.", user)
 712         return False
 713 
 714     p = Popen(testcmd)
 715     p.wait()
 716     if p.returncode is not 0:
 717         logger.info("Warning: user '%s' cannot use passwordless sudo.", user)
 718         return False
 719     else:
 720         Cmd.verified_users.append(user)
 721 
 722     return True
 723 
 724 
 725 def find_tests(testrun, options):
 726     """
 727     For the given list of pathnames, add files as Tests. For directories,
 728     if do_groups is True, add the directory as a TestGroup. If False,
 729     recursively search for executable files.
 730     """
 731 
 732     for p in sorted(options.pathnames):
 733         if os.path.isdir(p):
 734             for dirname, _, filenames in os.walk(p):
 735                 if options.do_groups:
 736                     testrun.addtestgroup(dirname, filenames, options)
 737                 else:
 738                     for f in sorted(filenames):
 739                         testrun.addtest(os.path.join(dirname, f), options)
 740         else:
 741             testrun.addtest(p, options)
 742 
 743 
 744 def fail(retstr, ret=1):
 745     print '%s: %s' % (argv[0], retstr)
 746     exit(ret)
 747 
 748 
 749 def options_cb(option, opt_str, value, parser):
 750     path_options = ['runfile', 'outputdir', 'template']
 751 
 752     if option.dest is 'runfile' and '-w' in parser.rargs or \
 753         option.dest is 'template' and '-c' in parser.rargs:
 754         fail('-c and -w are mutually exclusive.')
 755 
 756     if opt_str in parser.rargs:
 757         fail('%s may only be specified once.' % opt_str)
 758 
 759     if option.dest is 'runfile':
 760         parser.values.cmd = 'rdconfig'
 761     if option.dest is 'template':
 762         parser.values.cmd = 'wrconfig'
 763 
 764     setattr(parser.values, option.dest, value)
 765     if option.dest in path_options:
 766         setattr(parser.values, option.dest, os.path.abspath(value))
 767 
 768 
 769 def parse_args():
 770     parser = OptionParser()
 771     parser.add_option('-c', action='callback', callback=options_cb,
 772                       type='string', dest='runfile', metavar='runfile',
 773                       help='Specify tests to run via config file.')
 774     parser.add_option('-d', action='store_true', default=False, dest='dryrun',
 775                       help='Dry run. Print tests, but take no other action.')
 776     parser.add_option('-g', action='store_true', default=False,
 777                       dest='do_groups', help='Make directories TestGroups.')
 778     parser.add_option('-o', action='callback', callback=options_cb,
 779                       default=BASEDIR, dest='outputdir', type='string',
 780                       metavar='outputdir', help='Specify an output directory.')
 781     parser.add_option('-p', action='callback', callback=options_cb,
 782                       default='', dest='pre', metavar='script',
 783                       type='string', help='Specify a pre script.')
 784     parser.add_option('-P', action='callback', callback=options_cb,
 785                       default='', dest='post', metavar='script',
 786                       type='string', help='Specify a post script.')
 787     parser.add_option('-q', action='store_true', default=False, dest='quiet',
 788                       help='Silence on the console during a test run.')
 789     parser.add_option('-t', action='callback', callback=options_cb, default=60,
 790                       dest='timeout', metavar='seconds', type='int',
 791                       help='Timeout (in seconds) for an individual test.')
 792     parser.add_option('-u', action='callback', callback=options_cb,
 793                       default='', dest='user', metavar='user', type='string',
 794                       help='Specify a different user name to run as.')
 795     parser.add_option('-w', action='callback', callback=options_cb,
 796                       default=None, dest='template', metavar='template',
 797                       type='string', help='Create a new config file.')
 798     parser.add_option('-x', action='callback', callback=options_cb, default='',
 799                       dest='pre_user', metavar='pre_user', type='string',
 800                       help='Specify a user to execute the pre script.')
 801     parser.add_option('-X', action='callback', callback=options_cb, default='',
 802                       dest='post_user', metavar='post_user', type='string',
 803                       help='Specify a user to execute the post script.')
 804     (options, pathnames) = parser.parse_args()
 805 
 806     if not options.runfile and not options.template:
 807         options.cmd = 'runtests'
 808 
 809     if options.runfile and len(pathnames):
 810         fail('Extraneous arguments.')
 811 
 812     options.pathnames = [os.path.abspath(path) for path in pathnames]
 813 
 814     return options
 815 
 816 
 817 def main(args):
 818     options = parse_args()
 819     testrun = TestRun(options)
 820 
 821     if options.cmd is 'runtests':
 822         find_tests(testrun, options)
 823     elif options.cmd is 'rdconfig':
 824         testrun.read(testrun.logger, options)
 825     elif options.cmd is 'wrconfig':
 826         find_tests(testrun, options)
 827         testrun.write(options)
 828         exit(0)
 829     else:
 830         fail('Unknown command specified')
 831 
 832     testrun.complete_outputdirs(options)
 833     testrun.run(options)
 834     testrun.summary()
 835     exit(0)
 836 
 837 
 838 if __name__ == '__main__':
 839     main(argv[1:])