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