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