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