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