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