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