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