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