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