1#!/usr/bin/env @PYTHON_SHEBANG@
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, 2018 by Delphix. All rights reserved.
16# Copyright (c) 2019 Datto Inc.
17#
18# This script must remain compatible with Python 3.6+.
19#
20
21import os
22import sys
23import ctypes
24import re
25import configparser
26
27from datetime import datetime
28from optparse import OptionParser
29from pwd import getpwnam
30from pwd import getpwuid
31from select import select
32from subprocess import PIPE
33from subprocess import Popen
34from subprocess import check_output
35from threading import Timer
36from time import time, CLOCK_MONOTONIC
37from os.path import exists
38
39BASEDIR = '/var/tmp/test_results'
40TESTDIR = '/usr/share/zfs/'
41KMEMLEAK_FILE = '/sys/kernel/debug/kmemleak'
42KILL = 'kill'
43TRUE = 'true'
44SUDO = 'sudo'
45LOG_FILE = 'LOG_FILE'
46LOG_OUT = 'LOG_OUT'
47LOG_ERR = 'LOG_ERR'
48LOG_FILE_OBJ = None
49
50try:
51    from time import monotonic as monotonic_time
52except ImportError:
53    class timespec(ctypes.Structure):
54        _fields_ = [
55            ('tv_sec', ctypes.c_long),
56            ('tv_nsec', ctypes.c_long)
57        ]
58
59    librt = ctypes.CDLL('librt.so.1', use_errno=True)
60    clock_gettime = librt.clock_gettime
61    clock_gettime.argtypes = [ctypes.c_int, ctypes.POINTER(timespec)]
62
63    def monotonic_time():
64        t = timespec()
65        if clock_gettime(CLOCK_MONOTONIC, ctypes.pointer(t)) != 0:
66            errno_ = ctypes.get_errno()
67            raise OSError(errno_, os.strerror(errno_))
68        return t.tv_sec + t.tv_nsec * 1e-9
69
70
71class Result(object):
72    total = 0
73    runresults = {'PASS': 0, 'FAIL': 0, 'SKIP': 0, 'KILLED': 0, 'RERAN': 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.kmemleak = ''
82        self.result = ''
83
84    def done(self, proc, killed, reran):
85        """
86        Finalize the results of this Cmd.
87        """
88        Result.total += 1
89        m, s = divmod(monotonic_time() - self.starttime, 60)
90        self.runtime = '%02d:%02d' % (m, s)
91        self.returncode = proc.returncode
92        if reran is True:
93            Result.runresults['RERAN'] += 1
94        if killed:
95            self.result = 'KILLED'
96            Result.runresults['KILLED'] += 1
97        elif len(self.kmemleak) > 0:
98            self.result = 'FAIL'
99            Result.runresults['FAIL'] += 1
100        elif self.returncode == 0:
101            self.result = 'PASS'
102            Result.runresults['PASS'] += 1
103        elif self.returncode == 4:
104            self.result = 'SKIP'
105            Result.runresults['SKIP'] += 1
106        elif self.returncode != 0:
107            self.result = 'FAIL'
108            Result.runresults['FAIL'] += 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, debug=False):
117        self.stream = stream
118        self.debug = debug
119        self._buf = b''
120        self.lines = []
121
122    def fileno(self):
123        return self.stream.fileno()
124
125    def read(self, drain=0):
126        """
127        Read from the file descriptor. If 'drain' set, read until EOF.
128        """
129        while self._read() is not None:
130            if not drain:
131                break
132
133    def _read(self):
134        """
135        Read up to 4k of data from this output stream. Collect the output
136        up to the last newline, and append it to any leftover data from a
137        previous call. The lines are stored as a (timestamp, data) tuple
138        for easy sorting/merging later.
139        """
140        fd = self.fileno()
141        buf = os.read(fd, 4096)
142        if not buf:
143            return None
144        if self.debug:
145            os.write(sys.stderr.fileno(), buf)
146        if b'\n' not in buf:
147            self._buf += buf
148            return []
149
150        buf = self._buf + buf
151        tmp, rest = buf.rsplit(b'\n', 1)
152        self._buf = rest
153        now = datetime.now()
154        rows = tmp.split(b'\n')
155        self.lines += [(now, r) for r in rows]
156
157
158class Cmd(object):
159    verified_users = []
160
161    def __init__(self, pathname, identifier=None, outputdir=None,
162                 timeout=None, user=None, tags=None):
163        self.pathname = pathname
164        self.identifier = identifier
165        self.outputdir = outputdir or 'BASEDIR'
166        """
167        The timeout for tests is measured in wall-clock time
168        """
169        self.timeout = timeout
170        self.user = user or ''
171        self.killed = False
172        self.reran = None
173        self.result = Result()
174
175        if self.timeout is None:
176            self.timeout = 60
177
178    def __str__(self):
179        return '''\
180Pathname: %s
181Identifier: %s
182Outputdir: %s
183Timeout: %d
184User: %s
185''' % (self.pathname, self.identifier, self.outputdir, self.timeout, self.user)
186
187    def kill_cmd(self, proc, options, kmemleak, keyboard_interrupt=False):
188        """
189        Kill a running command due to timeout, or ^C from the keyboard. If
190        sudo is required, this user was verified previously.
191        """
192        self.killed = True
193        do_sudo = len(self.user) != 0
194        signal = '-TERM'
195
196        cmd = [SUDO, KILL, signal, str(proc.pid)]
197        if not do_sudo:
198            del cmd[0]
199
200        try:
201            kp = Popen(cmd)
202            kp.wait()
203        except Exception:
204            pass
205
206        """
207        If this is not a user-initiated kill and the test has not been
208        reran before we consider if the test needs to be reran:
209        If the test has spent some time hibernating and didn't run the whole
210        length of time before being timed out we will rerun the test.
211        """
212        if keyboard_interrupt is False and self.reran is None:
213            runtime = monotonic_time() - self.result.starttime
214            if int(self.timeout) > runtime:
215                self.killed = False
216                self.reran = False
217                self.run(options, dryrun=False, kmemleak=kmemleak)
218                self.reran = True
219
220    def update_cmd_privs(self, cmd, user):
221        """
222        If a user has been specified to run this Cmd and we're not already
223        running as that user, prepend the appropriate sudo command to run
224        as that user.
225        """
226        me = getpwuid(os.getuid())
227
228        if not user or user is me:
229            if os.path.isfile(cmd+'.ksh') and os.access(cmd+'.ksh', os.X_OK):
230                cmd += '.ksh'
231            if os.path.isfile(cmd+'.sh') and os.access(cmd+'.sh', os.X_OK):
232                cmd += '.sh'
233            return cmd
234
235        if not os.path.isfile(cmd):
236            if os.path.isfile(cmd+'.ksh') and os.access(cmd+'.ksh', os.X_OK):
237                cmd += '.ksh'
238            if os.path.isfile(cmd+'.sh') and os.access(cmd+'.sh', os.X_OK):
239                cmd += '.sh'
240
241        ret = '%s -E -u %s %s' % (SUDO, user, cmd)
242        return ret.split(' ')
243
244    def collect_output(self, proc, debug=False):
245        """
246        Read from stdout/stderr as data becomes available, until the
247        process is no longer running. Return the lines from the stdout and
248        stderr Output objects.
249        """
250        out = Output(proc.stdout, debug)
251        err = Output(proc.stderr, debug)
252        res = []
253        while proc.returncode is None:
254            proc.poll()
255            res = select([out, err], [], [], .1)
256            for fd in res[0]:
257                fd.read()
258        for fd in res[0]:
259            fd.read(drain=1)
260
261        return out.lines, err.lines
262
263    def run(self, options, dryrun=None, kmemleak=None):
264        """
265        This is the main function that runs each individual test.
266        Determine whether or not the command requires sudo, and modify it
267        if needed. Run the command, and update the result object.
268        """
269        if dryrun is None:
270            dryrun = options.dryrun
271        if dryrun is True:
272            print(self)
273            return
274        if kmemleak is None:
275            kmemleak = options.kmemleak
276
277        privcmd = self.update_cmd_privs(self.pathname, self.user)
278        try:
279            old = os.umask(0)
280            if not os.path.isdir(self.outputdir):
281                os.makedirs(self.outputdir, mode=0o777)
282            os.umask(old)
283        except OSError as e:
284            fail('%s' % e)
285
286        """
287        Log each test we run to /dev/kmsg (on Linux), so if there's a kernel
288        warning we'll be able to match it up to a particular test.
289        """
290        if options.kmsg is True and exists("/dev/kmsg"):
291            try:
292                kp = Popen([SUDO, "sh", "-c",
293                            f"echo ZTS run {self.pathname} > /dev/kmsg"])
294                kp.wait()
295            except Exception:
296                pass
297
298        self.result.starttime = monotonic_time()
299
300        if kmemleak:
301            cmd = f'{SUDO} sh -c "echo clear > {KMEMLEAK_FILE}"'
302            check_output(cmd, shell=True)
303
304        proc = Popen(privcmd, stdout=PIPE, stderr=PIPE)
305        # Allow a special timeout value of 0 to mean infinity
306        if int(self.timeout) == 0:
307            self.timeout = sys.maxsize / (10 ** 9)
308        t = Timer(
309            int(self.timeout), self.kill_cmd, [proc, options, kmemleak]
310        )
311
312        try:
313            t.start()
314
315            out, err = self.collect_output(proc, options.debug)
316            self.result.stdout = out
317            self.result.stderr = err
318
319            if kmemleak:
320                cmd = f'{SUDO} sh -c "echo scan > {KMEMLEAK_FILE}"'
321                check_output(cmd, shell=True)
322                cmd = f'{SUDO} cat {KMEMLEAK_FILE}'
323                self.result.kmemleak = check_output(cmd, shell=True)
324        except KeyboardInterrupt:
325            self.kill_cmd(proc, options, kmemleak, True)
326            fail('\nRun terminated at user request.')
327        finally:
328            t.cancel()
329
330        if self.reran is not False:
331            self.result.done(proc, self.killed, self.reran)
332
333    def skip(self):
334        """
335        Initialize enough of the test result that we can log a skipped
336        command.
337        """
338        Result.total += 1
339        Result.runresults['SKIP'] += 1
340        self.result.stdout = self.result.stderr = []
341        self.result.starttime = monotonic_time()
342        m, s = divmod(monotonic_time() - self.result.starttime, 60)
343        self.result.runtime = '%02d:%02d' % (m, s)
344        self.result.result = 'SKIP'
345
346    def log(self, options, suppress_console=False):
347        """
348        This function is responsible for writing all output. This includes
349        the console output, the logfile of all results (with timestamped
350        merged stdout and stderr), and for each test, the unmodified
351        stdout/stderr/merged in its own file.
352        """
353
354        logname = getpwuid(os.getuid()).pw_name
355        rer = ''
356        if self.reran is True:
357            rer = ' (RERAN)'
358        user = ' (run as %s)' % (self.user if len(self.user) else logname)
359        if self.identifier:
360            msga = 'Test (%s): %s%s ' % (self.identifier, self.pathname, user)
361        else:
362            msga = 'Test: %s%s ' % (self.pathname, user)
363        msgb = '[%s] [%s]%s\n' % (self.result.runtime, self.result.result, rer)
364        pad = ' ' * (80 - (len(msga) + len(msgb)))
365        result_line = msga + pad + msgb
366
367        # The result line is always written to the log file. If -q was
368        # specified only failures are written to the console, otherwise
369        # the result line is written to the console. The console output
370        # may be suppressed by calling log() with suppress_console=True.
371        write_log(bytearray(result_line, encoding='utf-8'), LOG_FILE)
372        if not suppress_console:
373            if not options.quiet:
374                write_log(result_line, LOG_OUT)
375            elif options.quiet and self.result.result != 'PASS':
376                write_log(result_line, LOG_OUT)
377
378        lines = sorted(self.result.stdout + self.result.stderr,
379                       key=lambda x: x[0])
380
381        # Write timestamped output (stdout and stderr) to the logfile
382        for dt, line in lines:
383            timestamp = bytearray(dt.strftime("%H:%M:%S.%f ")[:11],
384                                  encoding='utf-8')
385            write_log(b'%s %s\n' % (timestamp, line), LOG_FILE)
386
387        # Write the separate stdout/stderr/merged files, if the data exists
388        if len(self.result.stdout):
389            with open(os.path.join(self.outputdir, 'stdout'), 'wb') as out:
390                for _, line in self.result.stdout:
391                    os.write(out.fileno(), b'%s\n' % line)
392        if len(self.result.stderr):
393            with open(os.path.join(self.outputdir, 'stderr'), 'wb') as err:
394                for _, line in self.result.stderr:
395                    os.write(err.fileno(), b'%s\n' % line)
396        if len(self.result.stdout) and len(self.result.stderr):
397            with open(os.path.join(self.outputdir, 'merged'), 'wb') as merged:
398                for _, line in lines:
399                    os.write(merged.fileno(), b'%s\n' % line)
400        if len(self.result.kmemleak):
401            with open(os.path.join(self.outputdir, 'kmemleak'), 'wb') as kmem:
402                kmem.write(self.result.kmemleak)
403
404
405class Test(Cmd):
406    props = ['outputdir', 'timeout', 'user', 'pre', 'pre_user', 'post',
407             'post_user', 'failsafe', 'failsafe_user', 'tags']
408
409    def __init__(self, pathname,
410                 pre=None, pre_user=None, post=None, post_user=None,
411                 failsafe=None, failsafe_user=None, tags=None, **kwargs):
412        super(Test, self).__init__(pathname, **kwargs)
413        self.pre = pre or ''
414        self.pre_user = pre_user or ''
415        self.post = post or ''
416        self.post_user = post_user or ''
417        self.failsafe = failsafe or ''
418        self.failsafe_user = failsafe_user or ''
419        self.tags = tags or []
420
421    def __str__(self):
422        post_user = pre_user = failsafe_user = ''
423        if len(self.pre_user):
424            pre_user = ' (as %s)' % (self.pre_user)
425        if len(self.post_user):
426            post_user = ' (as %s)' % (self.post_user)
427        if len(self.failsafe_user):
428            failsafe_user = ' (as %s)' % (self.failsafe_user)
429        return '''\
430Pathname: %s
431Identifier: %s
432Outputdir: %s
433Timeout: %d
434User: %s
435Pre: %s%s
436Post: %s%s
437Failsafe: %s%s
438Tags: %s
439''' % (self.pathname, self.identifier, self.outputdir, self.timeout, self.user,
440            self.pre, pre_user, self.post, post_user, self.failsafe,
441            failsafe_user, self.tags)
442
443    def verify(self):
444        """
445        Check the pre/post/failsafe scripts, user and Test. Omit the Test from
446        this run if there are any problems.
447        """
448        files = [self.pre, self.pathname, self.post, self.failsafe]
449        users = [self.pre_user, self.user, self.post_user, self.failsafe_user]
450
451        for f in [f for f in files if len(f)]:
452            if not verify_file(f):
453                write_log("Warning: Test '%s' not added to this run because"
454                          " it failed verification.\n" % f, LOG_ERR)
455                return False
456
457        for user in [user for user in users if len(user)]:
458            if not verify_user(user):
459                write_log("Not adding Test '%s' to this run.\n" %
460                          self.pathname, LOG_ERR)
461                return False
462
463        return True
464
465    def run(self, options, dryrun=None, kmemleak=None):
466        """
467        Create Cmd instances for the pre/post/failsafe scripts. If the pre
468        script doesn't pass, skip this Test. Run the post script regardless.
469        If the Test is killed, also run the failsafe script.
470        """
471        odir = os.path.join(self.outputdir, os.path.basename(self.pre))
472        pretest = Cmd(self.pre, identifier=self.identifier, outputdir=odir,
473                      timeout=self.timeout, user=self.pre_user)
474        test = Cmd(self.pathname, identifier=self.identifier,
475                   outputdir=self.outputdir, timeout=self.timeout,
476                   user=self.user)
477        odir = os.path.join(self.outputdir, os.path.basename(self.failsafe))
478        failsafe = Cmd(self.failsafe, identifier=self.identifier,
479                       outputdir=odir, timeout=self.timeout,
480                       user=self.failsafe_user)
481        odir = os.path.join(self.outputdir, os.path.basename(self.post))
482        posttest = Cmd(self.post, identifier=self.identifier, outputdir=odir,
483                       timeout=self.timeout, user=self.post_user)
484
485        cont = True
486        if len(pretest.pathname):
487            pretest.run(options, kmemleak=False)
488            cont = pretest.result.result == 'PASS'
489            pretest.log(options)
490
491        if cont:
492            test.run(options, kmemleak=kmemleak)
493            if test.result.result == 'KILLED' and len(failsafe.pathname):
494                failsafe.run(options, kmemleak=False)
495                failsafe.log(options, suppress_console=True)
496        else:
497            test.skip()
498
499        test.log(options)
500
501        if len(posttest.pathname):
502            posttest.run(options, kmemleak=False)
503            posttest.log(options)
504
505
506class TestGroup(Test):
507    props = Test.props + ['tests']
508
509    def __init__(self, pathname, tests=None, **kwargs):
510        super(TestGroup, self).__init__(pathname, **kwargs)
511        self.tests = tests or []
512
513    def __str__(self):
514        post_user = pre_user = failsafe_user = ''
515        if len(self.pre_user):
516            pre_user = ' (as %s)' % (self.pre_user)
517        if len(self.post_user):
518            post_user = ' (as %s)' % (self.post_user)
519        if len(self.failsafe_user):
520            failsafe_user = ' (as %s)' % (self.failsafe_user)
521        return '''\
522Pathname: %s
523Identifier: %s
524Outputdir: %s
525Tests: %s
526Timeout: %s
527User: %s
528Pre: %s%s
529Post: %s%s
530Failsafe: %s%s
531Tags: %s
532''' % (self.pathname, self.identifier, self.outputdir, self.tests,
533            self.timeout, self.user, self.pre, pre_user, self.post, post_user,
534            self.failsafe, failsafe_user, self.tags)
535
536    def filter(self, keeplist):
537        self.tests = [x for x in self.tests if x in keeplist]
538
539    def verify(self):
540        """
541        Check the pre/post/failsafe scripts, user and tests in this TestGroup.
542        Omit the TestGroup entirely, or simply delete the relevant tests in the
543        group, if that's all that's required.
544        """
545        # If the pre/post/failsafe scripts are relative pathnames, convert to
546        # absolute, so they stand a chance of passing verification.
547        if len(self.pre) and not os.path.isabs(self.pre):
548            self.pre = os.path.join(self.pathname, self.pre)
549        if len(self.post) and not os.path.isabs(self.post):
550            self.post = os.path.join(self.pathname, self.post)
551        if len(self.failsafe) and not os.path.isabs(self.failsafe):
552            self.post = os.path.join(self.pathname, self.post)
553
554        auxfiles = [self.pre, self.post, self.failsafe]
555        users = [self.pre_user, self.user, self.post_user, self.failsafe_user]
556
557        for f in [f for f in auxfiles if len(f)]:
558            if f != self.failsafe and self.pathname != os.path.dirname(f):
559                write_log("Warning: TestGroup '%s' not added to this run. "
560                          "Auxiliary script '%s' exists in a different "
561                          "directory.\n" % (self.pathname, f), LOG_ERR)
562                return False
563
564            if not verify_file(f):
565                write_log("Warning: TestGroup '%s' not added to this run. "
566                          "Auxiliary script '%s' failed verification.\n" %
567                          (self.pathname, f), LOG_ERR)
568                return False
569
570        for user in [user for user in users if len(user)]:
571            if not verify_user(user):
572                write_log("Not adding TestGroup '%s' to this run.\n" %
573                          self.pathname, LOG_ERR)
574                return False
575
576        # If one of the tests is invalid, delete it, log it, and drive on.
577        for test in self.tests:
578            if not verify_file(os.path.join(self.pathname, test)):
579                del self.tests[self.tests.index(test)]
580                write_log("Warning: Test '%s' removed from TestGroup '%s' "
581                          "because it failed verification.\n" %
582                          (test, self.pathname), LOG_ERR)
583
584        return len(self.tests) != 0
585
586    def run(self, options, dryrun=None, kmemleak=None):
587        """
588        Create Cmd instances for the pre/post/failsafe scripts. If the pre
589        script doesn't pass, skip all the tests in this TestGroup. Run the
590        post script regardless. Run the failsafe script when a test is killed.
591        """
592        # tags assigned to this test group also include the test names
593        if options.tags and not set(self.tags).intersection(set(options.tags)):
594            return
595
596        odir = os.path.join(self.outputdir, os.path.basename(self.pre))
597        pretest = Cmd(self.pre, outputdir=odir, timeout=self.timeout,
598                      user=self.pre_user, identifier=self.identifier)
599        odir = os.path.join(self.outputdir, os.path.basename(self.post))
600        posttest = Cmd(self.post, outputdir=odir, timeout=self.timeout,
601                       user=self.post_user, identifier=self.identifier)
602
603        cont = True
604        if len(pretest.pathname):
605            pretest.run(options, dryrun=dryrun, kmemleak=False)
606            cont = pretest.result.result == 'PASS'
607            pretest.log(options)
608
609        for fname in self.tests:
610            odir = os.path.join(self.outputdir, fname)
611            test = Cmd(os.path.join(self.pathname, fname), outputdir=odir,
612                       timeout=self.timeout, user=self.user,
613                       identifier=self.identifier)
614            odir = os.path.join(odir, os.path.basename(self.failsafe))
615            failsafe = Cmd(self.failsafe, outputdir=odir, timeout=self.timeout,
616                           user=self.failsafe_user, identifier=self.identifier)
617            if cont:
618                test.run(options, dryrun=dryrun, kmemleak=kmemleak)
619                if test.result.result == 'KILLED' and len(failsafe.pathname):
620                    failsafe.run(options, dryrun=dryrun, kmemleak=False)
621                    failsafe.log(options, suppress_console=True)
622            else:
623                test.skip()
624
625            test.log(options)
626
627        if len(posttest.pathname):
628            posttest.run(options, dryrun=dryrun, kmemleak=False)
629            posttest.log(options)
630
631
632class TestRun(object):
633    props = ['quiet', 'outputdir', 'debug']
634
635    def __init__(self, options):
636        self.tests = {}
637        self.testgroups = {}
638        self.starttime = time()
639        self.timestamp = datetime.now().strftime('%Y%m%dT%H%M%S')
640        self.outputdir = os.path.join(options.outputdir, self.timestamp)
641        self.setup_logging(options)
642        self.defaults = [
643            ('outputdir', BASEDIR),
644            ('quiet', False),
645            ('timeout', 60),
646            ('user', ''),
647            ('pre', ''),
648            ('pre_user', ''),
649            ('post', ''),
650            ('post_user', ''),
651            ('failsafe', ''),
652            ('failsafe_user', ''),
653            ('tags', []),
654            ('debug', False)
655        ]
656
657    def __str__(self):
658        s = 'TestRun:\n    outputdir: %s\n' % self.outputdir
659        s += 'TESTS:\n'
660        for key in sorted(self.tests.keys()):
661            s += '%s%s' % (self.tests[key].__str__(), '\n')
662        s += 'TESTGROUPS:\n'
663        for key in sorted(self.testgroups.keys()):
664            s += '%s%s' % (self.testgroups[key].__str__(), '\n')
665        return s
666
667    def addtest(self, pathname, options):
668        """
669        Create a new Test, and apply any properties that were passed in
670        from the command line. If it passes verification, add it to the
671        TestRun.
672        """
673        test = Test(pathname)
674        for prop in Test.props:
675            setattr(test, prop, getattr(options, prop))
676
677        if test.verify():
678            self.tests[pathname] = test
679
680    def addtestgroup(self, dirname, filenames, options):
681        """
682        Create a new TestGroup, and apply any properties that were passed
683        in from the command line. If it passes verification, add it to the
684        TestRun.
685        """
686        if dirname not in self.testgroups:
687            testgroup = TestGroup(dirname)
688            for prop in Test.props:
689                setattr(testgroup, prop, getattr(options, prop))
690
691            # Prevent pre/post/failsafe scripts from running as regular tests
692            for f in [testgroup.pre, testgroup.post, testgroup.failsafe]:
693                if f in filenames:
694                    del filenames[filenames.index(f)]
695
696            self.testgroups[dirname] = testgroup
697            self.testgroups[dirname].tests = sorted(filenames)
698
699            testgroup.verify()
700
701    def filter(self, keeplist):
702        for group in list(self.testgroups.keys()):
703            if group not in keeplist:
704                del self.testgroups[group]
705                continue
706
707            g = self.testgroups[group]
708
709            if g.pre and os.path.basename(g.pre) in keeplist[group]:
710                continue
711
712            g.filter(keeplist[group])
713
714        for test in list(self.tests.keys()):
715            directory, base = os.path.split(test)
716            if directory not in keeplist or base not in keeplist[directory]:
717                del self.tests[test]
718
719    def read(self, options):
720        """
721        Read in the specified runfiles, and apply the TestRun properties
722        listed in the 'DEFAULT' section to our TestRun. Then read each
723        section, and apply the appropriate properties to the Test or
724        TestGroup. Properties from individual sections override those set
725        in the 'DEFAULT' section. If the Test or TestGroup passes
726        verification, add it to the TestRun.
727        """
728        config = configparser.RawConfigParser()
729        parsed = config.read(options.runfiles)
730        failed = options.runfiles - set(parsed)
731        if len(failed):
732            files = ' '.join(sorted(failed))
733            fail("Couldn't read config files: %s" % files)
734
735        for opt in TestRun.props:
736            if config.has_option('DEFAULT', opt):
737                setattr(self, opt, config.get('DEFAULT', opt))
738        self.outputdir = os.path.join(self.outputdir, self.timestamp)
739
740        testdir = options.testdir
741
742        for section in config.sections():
743            if 'tests' in config.options(section):
744                parts = section.split(':', 1)
745                sectiondir = parts[0]
746                identifier = parts[1] if len(parts) == 2 else None
747                if os.path.isdir(sectiondir):
748                    pathname = sectiondir
749                elif os.path.isdir(os.path.join(testdir, sectiondir)):
750                    pathname = os.path.join(testdir, sectiondir)
751                else:
752                    pathname = sectiondir
753
754                testgroup = TestGroup(os.path.abspath(pathname),
755                                      identifier=identifier)
756                for prop in TestGroup.props:
757                    for sect in ['DEFAULT', section]:
758                        if config.has_option(sect, prop):
759                            if prop == 'tags':
760                                setattr(testgroup, prop,
761                                        eval(config.get(sect, prop)))
762                            elif prop == 'failsafe':
763                                failsafe = config.get(sect, prop)
764                                setattr(testgroup, prop,
765                                        os.path.join(testdir, failsafe))
766                            else:
767                                setattr(testgroup, prop,
768                                        config.get(sect, prop))
769
770                # Repopulate tests using eval to convert the string to a list
771                testgroup.tests = eval(config.get(section, 'tests'))
772
773                if testgroup.verify():
774                    self.testgroups[section] = testgroup
775            else:
776                test = Test(section)
777                for prop in Test.props:
778                    for sect in ['DEFAULT', section]:
779                        if config.has_option(sect, prop):
780                            if prop == 'failsafe':
781                                failsafe = config.get(sect, prop)
782                                setattr(test, prop,
783                                        os.path.join(testdir, failsafe))
784                            else:
785                                setattr(test, prop, config.get(sect, prop))
786
787                if test.verify():
788                    self.tests[section] = test
789
790    def write(self, options):
791        """
792        Create a configuration file for editing and later use. The
793        'DEFAULT' section of the config file is created from the
794        properties that were specified on the command line. Tests are
795        simply added as sections that inherit everything from the
796        'DEFAULT' section. TestGroups are the same, except they get an
797        option including all the tests to run in that directory.
798        """
799
800        defaults = dict([(prop, getattr(options, prop)) for prop, _ in
801                         self.defaults])
802        config = configparser.RawConfigParser(defaults)
803
804        for test in sorted(self.tests.keys()):
805            config.add_section(test)
806            for prop in Test.props:
807                if prop not in self.props:
808                    config.set(test, prop,
809                               getattr(self.tests[test], prop))
810
811        for testgroup in sorted(self.testgroups.keys()):
812            config.add_section(testgroup)
813            config.set(testgroup, 'tests', self.testgroups[testgroup].tests)
814            for prop in TestGroup.props:
815                if prop not in self.props:
816                    config.set(testgroup, prop,
817                               getattr(self.testgroups[testgroup], prop))
818
819        try:
820            with open(options.template, 'w') as f:
821                return config.write(f)
822        except IOError:
823            fail('Could not open \'%s\' for writing.' % options.template)
824
825    def complete_outputdirs(self):
826        """
827        Collect all the pathnames for Tests, and TestGroups. Work
828        backwards one pathname component at a time, to create a unique
829        directory name in which to deposit test output. Tests will be able
830        to write output files directly in the newly modified outputdir.
831        TestGroups will be able to create one subdirectory per test in the
832        outputdir, and are guaranteed uniqueness because a group can only
833        contain files in one directory. Pre and post tests will create a
834        directory rooted at the outputdir of the Test or TestGroup in
835        question for their output. Failsafe scripts will create a directory
836        rooted at the outputdir of each Test for their output.
837        """
838        done = False
839        components = 0
840        tmp_dict = dict(list(self.tests.items()) +
841                        list(self.testgroups.items()))
842        total = len(tmp_dict)
843        base = self.outputdir
844
845        while not done:
846            paths = []
847            components -= 1
848            for testfile in list(tmp_dict.keys()):
849                uniq = '/'.join(testfile.split('/')[components:]).lstrip('/')
850                if uniq not in paths:
851                    paths.append(uniq)
852                    tmp_dict[testfile].outputdir = os.path.join(base, uniq)
853                else:
854                    break
855            done = total == len(paths)
856
857    def setup_logging(self, options):
858        """
859        This function creates the output directory and gets a file object
860        for the logfile. This function must be called before write_log()
861        can be used.
862        """
863        if options.dryrun is True:
864            return
865
866        global LOG_FILE_OBJ
867        if not options.template:
868            try:
869                old = os.umask(0)
870                os.makedirs(self.outputdir, mode=0o777)
871                os.umask(old)
872                filename = os.path.join(self.outputdir, 'log')
873                LOG_FILE_OBJ = open(filename, buffering=0, mode='wb')
874            except OSError as e:
875                fail('%s' % e)
876
877    def run(self, options):
878        """
879        Walk through all the Tests and TestGroups, calling run().
880        """
881        try:
882            os.chdir(self.outputdir)
883        except OSError:
884            fail('Could not change to directory %s' % self.outputdir)
885        # make a symlink to the output for the currently running test
886        logsymlink = os.path.join(self.outputdir, '../current')
887        if os.path.islink(logsymlink):
888            os.unlink(logsymlink)
889        if not os.path.exists(logsymlink):
890            os.symlink(self.outputdir, logsymlink)
891        else:
892            write_log('Could not make a symlink to directory %s\n' %
893                      self.outputdir, LOG_ERR)
894
895        if options.kmemleak:
896            cmd = f'{SUDO} -c "echo scan=0 > {KMEMLEAK_FILE}"'
897            check_output(cmd, shell=True)
898
899        iteration = 0
900        while iteration < options.iterations:
901            for test in sorted(self.tests.keys()):
902                self.tests[test].run(options)
903            for testgroup in sorted(self.testgroups.keys()):
904                self.testgroups[testgroup].run(options)
905            iteration += 1
906
907    def summary(self):
908        if Result.total == 0:
909            return 2
910
911        print('\nResults Summary')
912        for key in list(Result.runresults.keys()):
913            if Result.runresults[key] != 0:
914                print('%s\t% 4d' % (key, Result.runresults[key]))
915
916        m, s = divmod(time() - self.starttime, 60)
917        h, m = divmod(m, 60)
918        print('\nRunning Time:\t%02d:%02d:%02d' % (h, m, s))
919        print('Percent passed:\t%.1f%%' % ((float(Result.runresults['PASS']) /
920                                            float(Result.total)) * 100))
921        print('Log directory:\t%s' % self.outputdir)
922
923        if Result.runresults['FAIL'] > 0:
924            return 1
925
926        if Result.runresults['KILLED'] > 0:
927            return 1
928
929        if Result.runresults['RERAN'] > 0:
930            return 3
931
932        return 0
933
934
935def write_log(msg, target):
936    """
937    Write the provided message to standard out, standard error or
938    the logfile. If specifying LOG_FILE, then `msg` must be a bytes
939    like object. This way we can still handle output from tests that
940    may be in unexpected encodings.
941    """
942    if target == LOG_OUT:
943        os.write(sys.stdout.fileno(), bytearray(msg, encoding='utf-8'))
944    elif target == LOG_ERR:
945        os.write(sys.stderr.fileno(), bytearray(msg, encoding='utf-8'))
946    elif target == LOG_FILE:
947        os.write(LOG_FILE_OBJ.fileno(), msg)
948    else:
949        fail('log_msg called with unknown target "%s"' % target)
950
951
952def verify_file(pathname):
953    """
954    Verify that the supplied pathname is an executable regular file.
955    """
956    if os.path.isdir(pathname) or os.path.islink(pathname):
957        return False
958
959    for ext in '', '.ksh', '.sh':
960        script_path = pathname + ext
961        if os.path.isfile(script_path) and os.access(script_path, os.X_OK):
962            return True
963
964    return False
965
966
967def verify_user(user):
968    """
969    Verify that the specified user exists on this system, and can execute
970    sudo without being prompted for a password.
971    """
972    testcmd = [SUDO, '-n', '-u', user, TRUE]
973
974    if user in Cmd.verified_users:
975        return True
976
977    try:
978        getpwnam(user)
979    except KeyError:
980        write_log("Warning: user '%s' does not exist.\n" % user,
981                  LOG_ERR)
982        return False
983
984    p = Popen(testcmd)
985    p.wait()
986    if p.returncode != 0:
987        write_log("Warning: user '%s' cannot use passwordless sudo.\n" % user,
988                  LOG_ERR)
989        return False
990    else:
991        Cmd.verified_users.append(user)
992
993    return True
994
995
996def find_tests(testrun, options):
997    """
998    For the given list of pathnames, add files as Tests. For directories,
999    if do_groups is True, add the directory as a TestGroup. If False,
1000    recursively search for executable files.
1001    """
1002
1003    for p in sorted(options.pathnames):
1004        if os.path.isdir(p):
1005            for dirname, _, filenames in os.walk(p):
1006                if options.do_groups:
1007                    testrun.addtestgroup(dirname, filenames, options)
1008                else:
1009                    for f in sorted(filenames):
1010                        testrun.addtest(os.path.join(dirname, f), options)
1011        else:
1012            testrun.addtest(p, options)
1013
1014
1015def filter_tests(testrun, options):
1016    try:
1017        fh = open(options.logfile, "r")
1018    except Exception as e:
1019        fail('%s' % e)
1020
1021    failed = {}
1022    while True:
1023        line = fh.readline()
1024        if not line:
1025            break
1026        m = re.match(r'Test: .*(tests/.*)/(\S+).*\[FAIL\]', line)
1027        if not m:
1028            continue
1029        group, test = m.group(1, 2)
1030        try:
1031            failed[group].append(test)
1032        except KeyError:
1033            failed[group] = [test]
1034    fh.close()
1035
1036    testrun.filter(failed)
1037
1038
1039def fail(retstr, ret=1):
1040    print('%s: %s' % (sys.argv[0], retstr))
1041    exit(ret)
1042
1043
1044def kmemleak_cb(option, opt_str, value, parser):
1045    if not os.path.exists(KMEMLEAK_FILE):
1046        fail(f"File '{KMEMLEAK_FILE}' doesn't exist. " +
1047             "Enable CONFIG_DEBUG_KMEMLEAK in kernel configuration.")
1048
1049    setattr(parser.values, option.dest, True)
1050
1051
1052def options_cb(option, opt_str, value, parser):
1053    path_options = ['outputdir', 'template', 'testdir', 'logfile']
1054
1055    if opt_str in parser.rargs:
1056        fail('%s may only be specified once.' % opt_str)
1057
1058    if option.dest == 'runfiles':
1059        parser.values.cmd = 'rdconfig'
1060        value = set(os.path.abspath(p) for p in value.split(','))
1061    if option.dest == 'tags':
1062        value = [x.strip() for x in value.split(',')]
1063
1064    if option.dest in path_options:
1065        setattr(parser.values, option.dest, os.path.abspath(value))
1066    else:
1067        setattr(parser.values, option.dest, value)
1068
1069
1070def parse_args():
1071    parser = OptionParser()
1072    parser.add_option('-c', action='callback', callback=options_cb,
1073                      type='string', dest='runfiles', metavar='runfiles',
1074                      help='Specify tests to run via config files.')
1075    parser.add_option('-d', action='store_true', default=False, dest='dryrun',
1076                      help='Dry run. Print tests, but take no other action.')
1077    parser.add_option('-D', action='store_true', default=False, dest='debug',
1078                      help='Write all test output to stdout as it arrives.')
1079    parser.add_option('-l', action='callback', callback=options_cb,
1080                      default=None, dest='logfile', metavar='logfile',
1081                      type='string',
1082                      help='Read logfile and re-run tests which failed.')
1083    parser.add_option('-g', action='store_true', default=False,
1084                      dest='do_groups', help='Make directories TestGroups.')
1085    parser.add_option('-o', action='callback', callback=options_cb,
1086                      default=BASEDIR, dest='outputdir', type='string',
1087                      metavar='outputdir', help='Specify an output directory.')
1088    parser.add_option('-i', action='callback', callback=options_cb,
1089                      default=TESTDIR, dest='testdir', type='string',
1090                      metavar='testdir', help='Specify a test directory.')
1091    parser.add_option('-K', action='store_true', default=False, dest='kmsg',
1092                      help='Log tests names to /dev/kmsg')
1093    parser.add_option('-m', action='callback', callback=kmemleak_cb,
1094                      default=False, dest='kmemleak',
1095                      help='Enable kmemleak reporting (Linux only)')
1096    parser.add_option('-p', action='callback', callback=options_cb,
1097                      default='', dest='pre', metavar='script',
1098                      type='string', help='Specify a pre script.')
1099    parser.add_option('-P', action='callback', callback=options_cb,
1100                      default='', dest='post', metavar='script',
1101                      type='string', help='Specify a post script.')
1102    parser.add_option('-q', action='store_true', default=False, dest='quiet',
1103                      help='Silence on the console during a test run.')
1104    parser.add_option('-s', action='callback', callback=options_cb,
1105                      default='', dest='failsafe', metavar='script',
1106                      type='string', help='Specify a failsafe script.')
1107    parser.add_option('-S', action='callback', callback=options_cb,
1108                      default='', dest='failsafe_user',
1109                      metavar='failsafe_user', type='string',
1110                      help='Specify a user to execute the failsafe script.')
1111    parser.add_option('-t', action='callback', callback=options_cb, default=60,
1112                      dest='timeout', metavar='seconds', type='int',
1113                      help='Timeout (in seconds) for an individual test.')
1114    parser.add_option('-u', action='callback', callback=options_cb,
1115                      default='', dest='user', metavar='user', type='string',
1116                      help='Specify a different user name to run as.')
1117    parser.add_option('-w', action='callback', callback=options_cb,
1118                      default=None, dest='template', metavar='template',
1119                      type='string', help='Create a new config file.')
1120    parser.add_option('-x', action='callback', callback=options_cb, default='',
1121                      dest='pre_user', metavar='pre_user', type='string',
1122                      help='Specify a user to execute the pre script.')
1123    parser.add_option('-X', action='callback', callback=options_cb, default='',
1124                      dest='post_user', metavar='post_user', type='string',
1125                      help='Specify a user to execute the post script.')
1126    parser.add_option('-T', action='callback', callback=options_cb, default='',
1127                      dest='tags', metavar='tags', type='string',
1128                      help='Specify tags to execute specific test groups.')
1129    parser.add_option('-I', action='callback', callback=options_cb, default=1,
1130                      dest='iterations', metavar='iterations', type='int',
1131                      help='Number of times to run the test run.')
1132    (options, pathnames) = parser.parse_args()
1133
1134    if options.runfiles and len(pathnames):
1135        fail('Extraneous arguments.')
1136
1137    options.pathnames = [os.path.abspath(path) for path in pathnames]
1138
1139    return options
1140
1141
1142def main():
1143    options = parse_args()
1144
1145    testrun = TestRun(options)
1146
1147    if options.runfiles:
1148        testrun.read(options)
1149    else:
1150        find_tests(testrun, options)
1151
1152    if options.logfile:
1153        filter_tests(testrun, options)
1154
1155    if options.template:
1156        testrun.write(options)
1157        exit(0)
1158
1159    testrun.complete_outputdirs()
1160    testrun.run(options)
1161    exit(testrun.summary())
1162
1163
1164if __name__ == '__main__':
1165    main()
1166