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