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