xref: /titanic_41/usr/src/test/test-runner/cmd/run.py (revision 5e989a96186a37eb528fb7bb4d28a150874ec799)
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) 2012 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        user = ' (run as %s)' % self.user if len(self.user) else ''
238        msga = 'Test: %s%s ' % (self.pathname, user)
239        msgb = '[%s] [%s]' % (self.result.runtime, self.result.result)
240        pad = ' ' * (80 - (len(msga) + len(msgb)))
241
242        # If -q is specified, only print a line for tests that didn't pass.
243        # This means passing tests need to be logged as DEBUG, or the one
244        # line summary will only be printed in the logfile for failures.
245        if not options.quiet:
246            logger.info('%s%s%s' % (msga, pad, msgb))
247        elif self.result.result is not 'PASS':
248            logger.info('%s%s%s' % (msga, pad, msgb))
249        else:
250            logger.debug('%s%s%s' % (msga, pad, msgb))
251
252        lines = self.result.stdout + self.result.stderr
253        for dt, line in sorted(lines):
254            logger.debug('%s %s' % (dt.strftime("%H:%M:%S.%f ")[:11], line))
255
256        if len(self.result.stdout):
257            with open(os.path.join(self.outputdir, 'stdout'), 'w') as out:
258                for _, line in self.result.stdout:
259                    os.write(out.fileno(), '%s\n' % line)
260        if len(self.result.stderr):
261            with open(os.path.join(self.outputdir, 'stderr'), 'w') as err:
262                for _, line in self.result.stderr:
263                    os.write(err.fileno(), '%s\n' % line)
264        if len(self.result.stdout) and len(self.result.stderr):
265            with open(os.path.join(self.outputdir, 'merged'), 'w') as merged:
266                for _, line in sorted(lines):
267                    os.write(merged.fileno(), '%s\n' % line)
268
269
270class Test(Cmd):
271    props = ['outputdir', 'timeout', 'user', 'pre', 'pre_user', 'post',
272             'post_user']
273
274    def __init__(self, pathname, outputdir=None, timeout=None, user=None,
275                 pre=None, pre_user=None, post=None, post_user=None):
276        super(Test, self).__init__(pathname, outputdir, timeout, user)
277        self.pre = pre or ''
278        self.pre_user = pre_user or ''
279        self.post = post or ''
280        self.post_user = post_user or ''
281
282    def __str__(self):
283        post_user = pre_user = ''
284        if len(self.pre_user):
285            pre_user = ' (as %s)' % (self.pre_user)
286        if len(self.post_user):
287            post_user = ' (as %s)' % (self.post_user)
288        return "Pathname: %s\nOutputdir: %s\nTimeout: %s\nPre: %s%s\nPost: " \
289               "%s%s\nUser: %s\n" % (self.pathname, self.outputdir,
290                self.timeout, self.pre, pre_user, self.post, post_user,
291                self.user)
292
293    def verify(self, logger):
294        """
295        Check the pre/post scripts, user and Test. Omit the Test from this
296        run if there are any problems.
297        """
298        files = [self.pre, self.pathname, self.post]
299        users = [self.pre_user, self.user, self.post_user]
300
301        for f in [f for f in files if len(f)]:
302            if not verify_file(f):
303                logger.info("Warning: Test '%s' not added to this run because"
304                            " it failed verification." % f)
305                return False
306
307        for user in [user for user in users if len(user)]:
308            if not verify_user(user, logger):
309                logger.info("Not adding Test '%s' to this run." %
310                            self.pathname)
311                return False
312
313        return True
314
315    def run(self, logger, options):
316        """
317        Create Cmd instances for the pre/post scripts. If the pre script
318        doesn't pass, skip this Test. Run the post script regardless.
319        """
320        pretest = Cmd(self.pre, outputdir=os.path.join(self.outputdir,
321                      os.path.basename(self.pre)), timeout=self.timeout,
322                      user=self.pre_user)
323        test = Cmd(self.pathname, outputdir=self.outputdir,
324                   timeout=self.timeout, user=self.user)
325        posttest = Cmd(self.post, outputdir=os.path.join(self.outputdir,
326                       os.path.basename(self.post)), timeout=self.timeout,
327                       user=self.post_user)
328
329        cont = True
330        if len(pretest.pathname):
331            pretest.run(options)
332            cont = pretest.result.result is 'PASS'
333            pretest.log(logger, options)
334
335        if cont:
336            test.run(options)
337        else:
338            test.skip()
339
340        test.log(logger, options)
341
342        if len(posttest.pathname):
343            posttest.run(options)
344            posttest.log(logger, options)
345
346
347class TestGroup(Test):
348    props = Test.props + ['tests']
349
350    def __init__(self, pathname, outputdir=None, timeout=None, user=None,
351                 pre=None, pre_user=None, post=None, post_user=None,
352                 tests=None):
353        super(TestGroup, self).__init__(pathname, outputdir, timeout, user,
354                                        pre, pre_user, post, post_user)
355        self.tests = tests or []
356
357    def __str__(self):
358        post_user = pre_user = ''
359        if len(self.pre_user):
360            pre_user = ' (as %s)' % (self.pre_user)
361        if len(self.post_user):
362            post_user = ' (as %s)' % (self.post_user)
363        return "Pathname: %s\nOutputdir: %s\nTests: %s\nTimeout: %s\n" \
364               "Pre: %s%s\nPost: %s%s\nUser: %s\n" % (self.pathname,
365                self.outputdir, self.tests, self.timeout, self.pre, pre_user,
366                self.post, post_user, self.user)
367
368    def verify(self, logger):
369        """
370        Check the pre/post scripts, user and tests in this TestGroup. Omit
371        the TestGroup entirely, or simply delete the relevant tests in the
372        group, if that's all that's required.
373        """
374        # If the pre or post scripts are relative pathnames, convert to
375        # absolute, so they stand a chance of passing verification.
376        if len(self.pre) and not os.path.isabs(self.pre):
377            self.pre = os.path.join(self.pathname, self.pre)
378        if len(self.post) and not os.path.isabs(self.post):
379            self.post = os.path.join(self.pathname, self.post)
380
381        auxfiles = [self.pre, self.post]
382        users = [self.pre_user, self.user, self.post_user]
383
384        for f in [f for f in auxfiles if len(f)]:
385            if self.pathname != os.path.dirname(f):
386                logger.info("Warning: TestGroup '%s' not added to this run. "
387                            "Auxiliary script '%s' exists in a different "
388                            "directory." % (self.pathname, f))
389                return False
390
391            if not verify_file(f):
392                logger.info("Warning: TestGroup '%s' not added to this run. "
393                            "Auxiliary script '%s' failed verification." %
394                            (self.pathname, f))
395                return False
396
397        for user in [user for user in users if len(user)]:
398            if not verify_user(user, logger):
399                logger.info("Not adding TestGroup '%s' to this run." %
400                            self.pathname)
401                return False
402
403        # If one of the tests is invalid, delete it, log it, and drive on.
404        for test in self.tests:
405            if not verify_file(os.path.join(self.pathname, test)):
406                del self.tests[self.tests.index(test)]
407                logger.info("Warning: Test '%s' removed from TestGroup '%s' "
408                            "because it failed verification." % (test,
409                            self.pathname))
410
411        return len(self.tests) is not 0
412
413    def run(self, logger, options):
414        """
415        Create Cmd instances for the pre/post scripts. If the pre script
416        doesn't pass, skip all the tests in this TestGroup. Run the post
417        script regardless.
418        """
419        pretest = Cmd(self.pre, outputdir=os.path.join(self.outputdir,
420                      os.path.basename(self.pre)), timeout=self.timeout,
421                      user=self.pre_user)
422        posttest = Cmd(self.post, outputdir=os.path.join(self.outputdir,
423                       os.path.basename(self.post)), timeout=self.timeout,
424                       user=self.post_user)
425
426        cont = True
427        if len(pretest.pathname):
428            pretest.run(options)
429            cont = pretest.result.result is 'PASS'
430            pretest.log(logger, options)
431
432        for fname in self.tests:
433            test = Cmd(os.path.join(self.pathname, fname),
434                       outputdir=os.path.join(self.outputdir, fname),
435                       timeout=self.timeout, user=self.user)
436            if cont:
437                test.run(options)
438            else:
439                test.skip()
440
441            test.log(logger, options)
442
443        if len(posttest.pathname):
444            posttest.run(options)
445            posttest.log(logger, options)
446
447
448class TestRun(object):
449    props = ['quiet', 'outputdir']
450
451    def __init__(self, options):
452        self.tests = {}
453        self.testgroups = {}
454        self.starttime = time()
455        self.timestamp = datetime.now().strftime('%Y%m%dT%H%M%S')
456        self.outputdir = os.path.join(options.outputdir, self.timestamp)
457        self.logger = self.setup_logging(options)
458        self.defaults = [
459            ('outputdir', BASEDIR),
460            ('quiet', False),
461            ('timeout', 60),
462            ('user', ''),
463            ('pre', ''),
464            ('pre_user', ''),
465            ('post', ''),
466            ('post_user', '')
467        ]
468
469    def __str__(self):
470        s = 'TestRun:\n    outputdir: %s\n' % self.outputdir
471        s += 'TESTS:\n'
472        for key in sorted(self.tests.keys()):
473            s += '%s%s' % (self.tests[key].__str__(), '\n')
474        s += 'TESTGROUPS:\n'
475        for key in sorted(self.testgroups.keys()):
476            s += '%s%s' % (self.testgroups[key].__str__(), '\n')
477        return s
478
479    def addtest(self, pathname, options):
480        """
481        Create a new Test, and apply any properties that were passed in
482        from the command line. If it passes verification, add it to the
483        TestRun.
484        """
485        test = Test(pathname)
486        for prop in Test.props:
487            setattr(test, prop, getattr(options, prop))
488
489        if test.verify(self.logger):
490            self.tests[pathname] = test
491
492    def addtestgroup(self, dirname, filenames, options):
493        """
494        Create a new TestGroup, and apply any properties that were passed
495        in from the command line. If it passes verification, add it to the
496        TestRun.
497        """
498        if dirname not in self.testgroups:
499            testgroup = TestGroup(dirname)
500            for prop in Test.props:
501                setattr(testgroup, prop, getattr(options, prop))
502
503            # Prevent pre/post scripts from running as regular tests
504            for f in [testgroup.pre, testgroup.post]:
505                if f in filenames:
506                    del filenames[filenames.index(f)]
507
508            self.testgroups[dirname] = testgroup
509            self.testgroups[dirname].tests = sorted(filenames)
510
511            testgroup.verify(self.logger)
512
513    def read(self, logger, options):
514        """
515        Read in the specified runfile, and apply the TestRun properties
516        listed in the 'DEFAULT' section to our TestRun. Then read each
517        section, and apply the appropriate properties to the Test or
518        TestGroup. Properties from individual sections override those set
519        in the 'DEFAULT' section. If the Test or TestGroup passes
520        verification, add it to the TestRun.
521        """
522        config = ConfigParser.RawConfigParser()
523        if not len(config.read(options.runfile)):
524            fail("Coulnd't read config file %s" % options.runfile)
525
526        for opt in TestRun.props:
527            if config.has_option('DEFAULT', opt):
528                setattr(self, opt, config.get('DEFAULT', opt))
529        self.outputdir = os.path.join(self.outputdir, self.timestamp)
530
531        for section in config.sections():
532            if 'tests' in config.options(section):
533                testgroup = TestGroup(section)
534                for prop in TestGroup.props:
535                    try:
536                        setattr(testgroup, prop, config.get('DEFAULT', prop))
537                        setattr(testgroup, prop, config.get(section, prop))
538                    except ConfigParser.NoOptionError:
539                        pass
540
541                # Repopulate tests using eval to convert the string to a list
542                testgroup.tests = eval(config.get(section, 'tests'))
543
544                if testgroup.verify(logger):
545                    self.testgroups[section] = testgroup
546            else:
547                test = Test(section)
548                for prop in Test.props:
549                    try:
550                        setattr(test, prop, config.get('DEFAULT', prop))
551                        setattr(test, prop, config.get(section, prop))
552                    except ConfigParser.NoOptionError:
553                        pass
554                if test.verify(logger):
555                    self.tests[section] = test
556
557    def write(self, options):
558        """
559        Create a configuration file for editing and later use. The
560        'DEFAULT' section of the config file is created from the
561        properties that were specified on the command line. Tests are
562        simply added as sections that inherit everything from the
563        'DEFAULT' section. TestGroups are the same, except they get an
564        option including all the tests to run in that directory.
565        """
566
567        defaults = dict([(prop, getattr(options, prop)) for prop, _ in
568                        self.defaults])
569        config = ConfigParser.RawConfigParser(defaults)
570
571        for test in sorted(self.tests.keys()):
572            config.add_section(test)
573
574        for testgroup in sorted(self.testgroups.keys()):
575            config.add_section(testgroup)
576            config.set(testgroup, 'tests', self.testgroups[testgroup].tests)
577
578        try:
579            with open(options.template, 'w') as f:
580                return config.write(f)
581        except IOError:
582            fail('Could not open \'%s\' for writing.' % options.template)
583
584    def complete_outputdirs(self, options):
585        """
586        Collect all the pathnames for Tests, and TestGroups. Work
587        backwards one pathname component at a time, to create a unique
588        directory name in which to deposit test output. Tests will be able
589        to write output files directly in the newly modified outputdir.
590        TestGroups will be able to create one subdirectory per test in the
591        outputdir, and are guaranteed uniqueness because a group can only
592        contain files in one directory. Pre and post tests will create a
593        directory rooted at the outputdir of the Test or TestGroup in
594        question for their output.
595        """
596        done = False
597        components = 0
598        tmp_dict = dict(self.tests.items() + self.testgroups.items())
599        total = len(tmp_dict)
600        base = self.outputdir
601
602        while not done:
603            l = []
604            components -= 1
605            for testfile in tmp_dict.keys():
606                uniq = '/'.join(testfile.split('/')[components:]).lstrip('/')
607                if not uniq in l:
608                    l.append(uniq)
609                    tmp_dict[testfile].outputdir = os.path.join(base, uniq)
610                else:
611                    break
612            done = total == len(l)
613
614    def setup_logging(self, options):
615        """
616        Two loggers are set up here. The first is for the logfile which
617        will contain one line summarizing the test, including the test
618        name, result, and running time. This logger will also capture the
619        timestamped combined stdout and stderr of each run. The second
620        logger is optional console output, which will contain only the one
621        line summary. The loggers are initialized at two different levels
622        to facilitate segregating the output.
623        """
624        if options.dryrun is True:
625            return
626
627        testlogger = logging.getLogger(__name__)
628        testlogger.setLevel(logging.DEBUG)
629
630        if options.cmd is not 'wrconfig':
631            try:
632                old = os.umask(0)
633                os.makedirs(self.outputdir, mode=0777)
634                os.umask(old)
635            except OSError, e:
636                fail('%s' % e)
637            filename = os.path.join(self.outputdir, 'log')
638
639            logfile = logging.FileHandler(filename)
640            logfile.setLevel(logging.DEBUG)
641            logfilefmt = logging.Formatter('%(message)s')
642            logfile.setFormatter(logfilefmt)
643            testlogger.addHandler(logfile)
644
645        cons = logging.StreamHandler()
646        cons.setLevel(logging.INFO)
647        consfmt = logging.Formatter('%(message)s')
648        cons.setFormatter(consfmt)
649        testlogger.addHandler(cons)
650
651        return testlogger
652
653    def run(self, options):
654        """
655        Walk through all the Tests and TestGroups, calling run().
656        """
657        try:
658            os.chdir(self.outputdir)
659        except OSError:
660            fail('Could not change to directory %s' % self.outputdir)
661        for test in sorted(self.tests.keys()):
662            self.tests[test].run(self.logger, options)
663        for testgroup in sorted(self.testgroups.keys()):
664            self.testgroups[testgroup].run(self.logger, options)
665
666    def summary(self):
667        if Result.total is 0:
668            return
669
670        print '\nResults Summary'
671        for key in Result.runresults.keys():
672            if Result.runresults[key] is not 0:
673                print '%s\t% 4d' % (key, Result.runresults[key])
674
675        m, s = divmod(time() - self.starttime, 60)
676        h, m = divmod(m, 60)
677        print '\nRunning Time:\t%02d:%02d:%02d' % (h, m, s)
678        print 'Percent passed:\t%.1f%%' % ((float(Result.runresults['PASS']) /
679               float(Result.total)) * 100)
680        print 'Log directory:\t%s' % self.outputdir
681
682
683def verify_file(pathname):
684    """
685    Verify that the supplied pathname is an executable regular file.
686    """
687    if os.path.isdir(pathname) or os.path.islink(pathname):
688        return False
689
690    if os.path.isfile(pathname) and os.access(pathname, os.X_OK):
691        return True
692
693    return False
694
695
696def verify_user(user, logger):
697    """
698    Verify that the specified user exists on this system, and can execute
699    sudo without being prompted for a password.
700    """
701    testcmd = [SUDO, '-n', '-u', user, TRUE]
702    can_sudo = exists = True
703
704    if user in Cmd.verified_users:
705        return True
706
707    try:
708        _ = getpwnam(user)
709    except KeyError:
710        exists = False
711        logger.info("Warning: user '%s' does not exist.", user)
712        return False
713
714    p = Popen(testcmd)
715    p.wait()
716    if p.returncode is not 0:
717        logger.info("Warning: user '%s' cannot use passwordless sudo.", user)
718        return False
719    else:
720        Cmd.verified_users.append(user)
721
722    return True
723
724
725def find_tests(testrun, options):
726    """
727    For the given list of pathnames, add files as Tests. For directories,
728    if do_groups is True, add the directory as a TestGroup. If False,
729    recursively search for executable files.
730    """
731
732    for p in sorted(options.pathnames):
733        if os.path.isdir(p):
734            for dirname, _, filenames in os.walk(p):
735                if options.do_groups:
736                    testrun.addtestgroup(dirname, filenames, options)
737                else:
738                    for f in sorted(filenames):
739                        testrun.addtest(os.path.join(dirname, f), options)
740        else:
741            testrun.addtest(p, options)
742
743
744def fail(retstr, ret=1):
745    print '%s: %s' % (argv[0], retstr)
746    exit(ret)
747
748
749def options_cb(option, opt_str, value, parser):
750    path_options = ['runfile', 'outputdir', 'template']
751
752    if option.dest is 'runfile' and '-w' in parser.rargs or \
753        option.dest is 'template' and '-c' in parser.rargs:
754        fail('-c and -w are mutually exclusive.')
755
756    if opt_str in parser.rargs:
757        fail('%s may only be specified once.' % opt_str)
758
759    if option.dest is 'runfile':
760        parser.values.cmd = 'rdconfig'
761    if option.dest is 'template':
762        parser.values.cmd = 'wrconfig'
763
764    setattr(parser.values, option.dest, value)
765    if option.dest in path_options:
766        setattr(parser.values, option.dest, os.path.abspath(value))
767
768
769def parse_args():
770    parser = OptionParser()
771    parser.add_option('-c', action='callback', callback=options_cb,
772                      type='string', dest='runfile', metavar='runfile',
773                      help='Specify tests to run via config file.')
774    parser.add_option('-d', action='store_true', default=False, dest='dryrun',
775                      help='Dry run. Print tests, but take no other action.')
776    parser.add_option('-g', action='store_true', default=False,
777                      dest='do_groups', help='Make directories TestGroups.')
778    parser.add_option('-o', action='callback', callback=options_cb,
779                      default=BASEDIR, dest='outputdir', type='string',
780                      metavar='outputdir', help='Specify an output directory.')
781    parser.add_option('-p', action='callback', callback=options_cb,
782                      default='', dest='pre', metavar='script',
783                      type='string', help='Specify a pre script.')
784    parser.add_option('-P', action='callback', callback=options_cb,
785                      default='', dest='post', metavar='script',
786                      type='string', help='Specify a post script.')
787    parser.add_option('-q', action='store_true', default=False, dest='quiet',
788                      help='Silence on the console during a test run.')
789    parser.add_option('-t', action='callback', callback=options_cb, default=60,
790                      dest='timeout', metavar='seconds', type='int',
791                      help='Timeout (in seconds) for an individual test.')
792    parser.add_option('-u', action='callback', callback=options_cb,
793                      default='', dest='user', metavar='user', type='string',
794                      help='Specify a different user name to run as.')
795    parser.add_option('-w', action='callback', callback=options_cb,
796                      default=None, dest='template', metavar='template',
797                      type='string', help='Create a new config file.')
798    parser.add_option('-x', action='callback', callback=options_cb, default='',
799                      dest='pre_user', metavar='pre_user', type='string',
800                      help='Specify a user to execute the pre script.')
801    parser.add_option('-X', action='callback', callback=options_cb, default='',
802                      dest='post_user', metavar='post_user', type='string',
803                      help='Specify a user to execute the post script.')
804    (options, pathnames) = parser.parse_args()
805
806    if not options.runfile and not options.template:
807        options.cmd = 'runtests'
808
809    if options.runfile and len(pathnames):
810        fail('Extraneous arguments.')
811
812    options.pathnames = [os.path.abspath(path) for path in pathnames]
813
814    return options
815
816
817def main(args):
818    options = parse_args()
819    testrun = TestRun(options)
820
821    if options.cmd is 'runtests':
822        find_tests(testrun, options)
823    elif options.cmd is 'rdconfig':
824        testrun.read(testrun.logger, options)
825    elif options.cmd is 'wrconfig':
826        find_tests(testrun, options)
827        testrun.write(options)
828        exit(0)
829    else:
830        fail('Unknown command specified')
831
832    testrun.complete_outputdirs(options)
833    testrun.run(options)
834    testrun.summary()
835    exit(0)
836
837
838if __name__ == '__main__':
839    main(argv[1:])
840