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