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