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