dmake: do not set MAKEFLAGS=k
[unleashed/tickless.git] / usr / src / test / test-runner / cmd / run
blob306f382e4a1addb820c13a03a2550f9f16710f2d
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.
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         self.tests[:] = [f for f in self.tests if
442           verify_file(os.path.join(self.pathname, f))]
444         return len(self.tests) is not 0
446     def run(self, logger, options):
447         """
448         Create Cmd instances for the pre/post scripts. If the pre script
449         doesn't pass, skip all the tests in this TestGroup. Run the post
450         script regardless.
451         """
452         odir = os.path.join(self.outputdir, os.path.basename(self.pre))
453         pretest = Cmd(self.pre, outputdir=odir, timeout=self.timeout,
454                       user=self.pre_user)
455         odir = os.path.join(self.outputdir, os.path.basename(self.post))
456         posttest = Cmd(self.post, outputdir=odir, timeout=self.timeout,
457                        user=self.post_user)
459         cont = True
460         if len(pretest.pathname):
461             pretest.run(options)
462             cont = pretest.result.result is 'PASS'
463             pretest.log(logger, options)
465         for fname in self.tests:
466             test = Cmd(os.path.join(self.pathname, fname),
467                        outputdir=os.path.join(self.outputdir, fname),
468                        timeout=self.timeout, user=self.user)
469             if cont:
470                 test.run(options)
471             else:
472                 test.skip()
474             test.log(logger, options)
476         if len(posttest.pathname):
477             posttest.run(options)
478             posttest.log(logger, options)
481 class TestRun(object):
482     props = ['quiet', 'outputdir']
484     def __init__(self, options):
485         self.tests = {}
486         self.testgroups = {}
487         self.starttime = time()
488         self.timestamp = datetime.now().strftime('%Y%m%dT%H%M%S')
489         self.outputdir = os.path.join(options.outputdir, self.timestamp)
490         self.logger = self.setup_logging(options)
491         self.defaults = [
492             ('outputdir', BASEDIR),
493             ('quiet', False),
494             ('timeout', 60),
495             ('user', ''),
496             ('pre', ''),
497             ('pre_user', ''),
498             ('post', ''),
499             ('post_user', '')
500         ]
502     def __str__(self):
503         s = 'TestRun:\n    outputdir: %s\n' % self.outputdir
504         s += 'TESTS:\n'
505         for key in sorted(self.tests.keys()):
506             s += '%s%s' % (self.tests[key].__str__(), '\n')
507         s += 'TESTGROUPS:\n'
508         for key in sorted(self.testgroups.keys()):
509             s += '%s%s' % (self.testgroups[key].__str__(), '\n')
510         return s
512     def addtest(self, pathname, options):
513         """
514         Create a new Test, and apply any properties that were passed in
515         from the command line. If it passes verification, add it to the
516         TestRun.
517         """
518         test = Test(pathname)
519         for prop in Test.props:
520             setattr(test, prop, getattr(options, prop))
522         if test.verify(self.logger):
523             self.tests[pathname] = test
525     def addtestgroup(self, dirname, filenames, options):
526         """
527         Create a new TestGroup, and apply any properties that were passed
528         in from the command line. If it passes verification, add it to the
529         TestRun.
530         """
531         if dirname not in self.testgroups:
532             testgroup = TestGroup(dirname)
533             for prop in Test.props:
534                 setattr(testgroup, prop, getattr(options, prop))
536             # Prevent pre/post scripts from running as regular tests
537             for f in [testgroup.pre, testgroup.post]:
538                 if f in filenames:
539                     del filenames[filenames.index(f)]
541             self.testgroups[dirname] = testgroup
542             self.testgroups[dirname].tests = sorted(filenames)
544             testgroup.verify(self.logger)
546     def read(self, logger, options):
547         """
548         Read in the specified runfile, and apply the TestRun properties
549         listed in the 'DEFAULT' section to our TestRun. Then read each
550         section, and apply the appropriate properties to the Test or
551         TestGroup. Properties from individual sections override those set
552         in the 'DEFAULT' section. If the Test or TestGroup passes
553         verification, add it to the TestRun.
554         """
555         config = ConfigParser.RawConfigParser()
556         if not len(config.read(options.runfile)):
557             fail("Coulnd't read config file %s" % options.runfile)
559         for opt in TestRun.props:
560             if config.has_option('DEFAULT', opt):
561                 setattr(self, opt, config.get('DEFAULT', opt))
562         self.outputdir = os.path.join(self.outputdir, self.timestamp)
564         for section in config.sections():
565             if 'tests' in config.options(section):
566                 testgroup = TestGroup(section)
567                 for prop in TestGroup.props:
568                     for sect in ['DEFAULT', section]:
569                         if config.has_option(sect, prop):
570                             setattr(testgroup, prop, config.get(sect, prop))
572                 # Repopulate tests using eval to convert the string to a list
573                 testgroup.tests = eval(config.get(section, 'tests'))
575                 if testgroup.verify(logger):
576                     self.testgroups[section] = testgroup
578             elif 'autotests' in config.options(section):
579                 testgroup = TestGroup(section)
580                 for prop in TestGroup.props:
581                     for sect in ['DEFAULT', section]:
582                         if config.has_option(sect, prop):
583                             setattr(testgroup, prop, config.get(sect, prop))
585                 filenames = os.listdir(section)
586                 # only files starting with "tst." are considered tests
587                 filenames = [f for f in filenames if f.startswith("tst.")]
588                 testgroup.tests = sorted(filenames)
590                 if testgroup.verify(logger):
591                     self.testgroups[section] = testgroup
593             else:
594                 test = Test(section)
595                 for prop in Test.props:
596                     for sect in ['DEFAULT', section]:
597                         if config.has_option(sect, prop):
598                             setattr(test, prop, config.get(sect, prop))
600                 if test.verify(logger):
601                     self.tests[section] = test
603     def write(self, options):
604         """
605         Create a configuration file for editing and later use. The
606         'DEFAULT' section of the config file is created from the
607         properties that were specified on the command line. Tests are
608         simply added as sections that inherit everything from the
609         'DEFAULT' section. TestGroups are the same, except they get an
610         option including all the tests to run in that directory.
611         """
613         defaults = dict([(prop, getattr(options, prop)) for prop, _ in
614                          self.defaults])
615         config = ConfigParser.RawConfigParser(defaults)
617         for test in sorted(self.tests.keys()):
618             config.add_section(test)
620         for testgroup in sorted(self.testgroups.keys()):
621             config.add_section(testgroup)
622             config.set(testgroup, 'tests', self.testgroups[testgroup].tests)
624         try:
625             with open(options.template, 'w') as f:
626                 return config.write(f)
627         except IOError:
628             fail('Could not open \'%s\' for writing.' % options.template)
630     def complete_outputdirs(self):
631         """
632         Collect all the pathnames for Tests, and TestGroups. Work
633         backwards one pathname component at a time, to create a unique
634         directory name in which to deposit test output. Tests will be able
635         to write output files directly in the newly modified outputdir.
636         TestGroups will be able to create one subdirectory per test in the
637         outputdir, and are guaranteed uniqueness because a group can only
638         contain files in one directory. Pre and post tests will create a
639         directory rooted at the outputdir of the Test or TestGroup in
640         question for their output.
641         """
642         done = False
643         components = 0
644         tmp_dict = dict(self.tests.items() + self.testgroups.items())
645         total = len(tmp_dict)
646         base = self.outputdir
648         while not done:
649             l = []
650             components -= 1
651             for testfile in tmp_dict.keys():
652                 uniq = '/'.join(testfile.split('/')[components:]).lstrip('/')
653                 if uniq not in l:
654                     l.append(uniq)
655                     tmp_dict[testfile].outputdir = os.path.join(base, uniq)
656                 else:
657                     break
658             done = total == len(l)
660     def setup_logging(self, options):
661         """
662         Two loggers are set up here. The first is for the logfile which
663         will contain one line summarizing the test, including the test
664         name, result, and running time. This logger will also capture the
665         timestamped combined stdout and stderr of each run. The second
666         logger is optional console output, which will contain only the one
667         line summary. The loggers are initialized at two different levels
668         to facilitate segregating the output.
669         """
670         if options.dryrun is True:
671             return
673         testlogger = logging.getLogger(__name__)
674         testlogger.setLevel(logging.DEBUG)
676         if options.cmd is not 'wrconfig':
677             try:
678                 old = os.umask(0)
679                 os.makedirs(self.outputdir, mode=0777)
680                 os.umask(old)
681             except OSError, e:
682                 fail('%s' % e)
683             filename = os.path.join(self.outputdir, 'log')
685             logfile = WatchedFileHandlerClosed(filename)
686             logfile.setLevel(logging.DEBUG)
687             logfilefmt = logging.Formatter('%(message)s')
688             logfile.setFormatter(logfilefmt)
689             testlogger.addHandler(logfile)
691         cons = logging.StreamHandler()
692         cons.setLevel(logging.INFO)
693         consfmt = logging.Formatter('%(message)s')
694         cons.setFormatter(consfmt)
695         testlogger.addHandler(cons)
697         return testlogger
699     def run(self, options):
700         """
701         Walk through all the Tests and TestGroups, calling run().
702         """
703         try:
704             os.chdir(self.outputdir)
705         except OSError:
706             fail('Could not change to directory %s' % self.outputdir)
707         for test in sorted(self.tests.keys()):
708             self.tests[test].run(self.logger, options)
709         for testgroup in sorted(self.testgroups.keys()):
710             self.testgroups[testgroup].run(self.logger, options)
712     def summary(self):
713         if Result.total is 0:
714             return
716         print '\nResults Summary'
717         for key in Result.runresults.keys():
718             if Result.runresults[key] is not 0:
719                 print '%s\t% 4d' % (key, Result.runresults[key])
721         m, s = divmod(time() - self.starttime, 60)
722         h, m = divmod(m, 60)
723         print '\nRunning Time:\t%02d:%02d:%02d' % (h, m, s)
724         print 'Percent passed:\t%.1f%%' % ((float(Result.runresults['PASS']) /
725                                             float(Result.total)) * 100)
726         print 'Log directory:\t%s' % self.outputdir
729 def verify_file(pathname):
730     """
731     Verify that the supplied pathname is an executable regular file.
732     """
733     if os.path.isdir(pathname) or os.path.islink(pathname):
734         return False
736     if os.path.isfile(pathname) and os.access(pathname, os.X_OK):
737         return True
739     return False
742 def verify_user(user, logger):
743     """
744     Verify that the specified user exists on this system, and can execute
745     sudo without being prompted for a password.
746     """
747     testcmd = [SUDO, '-n', '-u', user, TRUE]
749     if user in Cmd.verified_users:
750         return True
752     try:
753         _ = getpwnam(user)
754     except KeyError:
755         logger.info("Warning: user '%s' does not exist.", user)
756         return False
758     p = Popen(testcmd)
759     p.wait()
760     if p.returncode is not 0:
761         logger.info("Warning: user '%s' cannot use passwordless sudo.", user)
762         return False
763     else:
764         Cmd.verified_users.append(user)
766     return True
769 def find_tests(testrun, options):
770     """
771     For the given list of pathnames, add files as Tests. For directories,
772     if do_groups is True, add the directory as a TestGroup. If False,
773     recursively search for executable files.
774     """
776     for p in sorted(options.pathnames):
777         if os.path.isdir(p):
778             for dirname, _, filenames in os.walk(p):
779                 if options.do_groups:
780                     testrun.addtestgroup(dirname, filenames, options)
781                 else:
782                     for f in sorted(filenames):
783                         testrun.addtest(os.path.join(dirname, f), options)
784         else:
785             testrun.addtest(p, options)
788 def fail(retstr, ret=1):
789     print '%s: %s' % (argv[0], retstr)
790     exit(ret)
793 def options_cb(option, opt_str, value, parser):
794     path_options = ['runfile', 'outputdir', 'template']
796     if option.dest is 'runfile' and '-w' in parser.rargs or \
797             option.dest is 'template' and '-c' in parser.rargs:
798         fail('-c and -w are mutually exclusive.')
800     if opt_str in parser.rargs:
801         fail('%s may only be specified once.' % opt_str)
803     if option.dest is 'runfile':
804         parser.values.cmd = 'rdconfig'
805     if option.dest is 'template':
806         parser.values.cmd = 'wrconfig'
808     setattr(parser.values, option.dest, value)
809     if option.dest in path_options:
810         setattr(parser.values, option.dest, os.path.abspath(value))
813 def parse_args():
814     parser = OptionParser()
815     parser.add_option('-c', action='callback', callback=options_cb,
816                       type='string', dest='runfile', metavar='runfile',
817                       help='Specify tests to run via config file.')
818     parser.add_option('-d', action='store_true', default=False, dest='dryrun',
819                       help='Dry run. Print tests, but take no other action.')
820     parser.add_option('-g', action='store_true', default=False,
821                       dest='do_groups', help='Make directories TestGroups.')
822     parser.add_option('-o', action='callback', callback=options_cb,
823                       default=BASEDIR, dest='outputdir', type='string',
824                       metavar='outputdir', help='Specify an output directory.')
825     parser.add_option('-p', action='callback', callback=options_cb,
826                       default='', dest='pre', metavar='script',
827                       type='string', help='Specify a pre script.')
828     parser.add_option('-P', action='callback', callback=options_cb,
829                       default='', dest='post', metavar='script',
830                       type='string', help='Specify a post script.')
831     parser.add_option('-q', action='store_true', default=False, dest='quiet',
832                       help='Silence on the console during a test run.')
833     parser.add_option('-t', action='callback', callback=options_cb, default=60,
834                       dest='timeout', metavar='seconds', type='int',
835                       help='Timeout (in seconds) for an individual test.')
836     parser.add_option('-u', action='callback', callback=options_cb,
837                       default='', dest='user', metavar='user', type='string',
838                       help='Specify a different user name to run as.')
839     parser.add_option('-w', action='callback', callback=options_cb,
840                       default=None, dest='template', metavar='template',
841                       type='string', help='Create a new config file.')
842     parser.add_option('-x', action='callback', callback=options_cb, default='',
843                       dest='pre_user', metavar='pre_user', type='string',
844                       help='Specify a user to execute the pre script.')
845     parser.add_option('-X', action='callback', callback=options_cb, default='',
846                       dest='post_user', metavar='post_user', type='string',
847                       help='Specify a user to execute the post script.')
848     (options, pathnames) = parser.parse_args()
850     if not options.runfile and not options.template:
851         options.cmd = 'runtests'
853     if options.runfile and len(pathnames):
854         fail('Extraneous arguments.')
856     options.pathnames = [os.path.abspath(path) for path in pathnames]
858     return options
861 def main():
862     options = parse_args()
863     testrun = TestRun(options)
865     if options.cmd is 'runtests':
866         find_tests(testrun, options)
867     elif options.cmd is 'rdconfig':
868         testrun.read(testrun.logger, options)
869     elif options.cmd is 'wrconfig':
870         find_tests(testrun, options)
871         testrun.write(options)
872         exit(0)
873     else:
874         fail('Unknown command specified')
876     testrun.complete_outputdirs()
877     testrun.run(options)
878     testrun.summary()
879     exit(0)
882 if __name__ == '__main__':
883     main()