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