1#!@PYTHON@ 2# 3# This file and its contents are supplied under the terms of the 4# Common Development and Distribution License ("CDDL"), version 1.0. 5# You may only use this file in accordance with the terms of version 6# 1.0 of the CDDL. 7# 8# A full copy of the text of the CDDL should have accompanied this 9# source. A copy of the CDDL is also available via the Internet at 10# http://www.illumos.org/license/CDDL. 11# 12 13# 14# Copyright 2021 Tintri by DDN, Inc. All rights reserved. 15# 16 17# 18# Run tests provided by smbtorture. 19# 20 21import subprocess 22import argparse 23import re 24import fnmatch 25 26from enum import Enum 27from datetime import datetime 28from tempfile import TemporaryFile 29 30def stripped_file(f): 31 """Strips trailing whitespace from lines in f""" 32 33 for line in f: 34 yield line.strip() 35 36def parse_tests(f): 37 """Returns test names from f, skipping commented lines""" 38 39 yield from (line for line in f 40 if line and not line.startswith('#')) 41 42def matched_suites(m): 43 """Gets all smbtorture tests that match pattern m""" 44 45 with TemporaryFile('w+') as tmp: 46 subprocess.run(['smbtorture', '--list'], stdout=tmp, 47 universal_newlines=True) 48 tmp.seek(0) 49 yield from (line for line in stripped_file(tmp) 50 if not line.startswith('smbtorture') and 51 m.match(line)) 52 53class TestResult(Enum): 54 PASS = 0 55 FAIL = 1 56 UNKNOWN = 2 57 SKIP = 3 58 KILLED = 4 59 60 def __str__(self): 61 return self.name 62 63 def __len__(self): 64 return len(self.name) 65 66class TestCase: 67 """A particular instance of an smbtorture test""" 68 69 __slots__ = 'name', 'result' 70 71 def __init__(self, name, skip=False): 72 self.name = name 73 self.result = TestResult.SKIP if skip else TestResult.UNKNOWN 74 75 def __str__(self): 76 return '{0.name} | {0.result}'.format(self) 77 78 def run(self, rfd, wfd, timeout, cmd): 79 """Run cmd, setting the last element to the test name, and setting result 80 based on rfd. Output is sent to wfd, and the test is killed based on timeout.""" 81 82 def finish(self, start, wfd): 83 timediff = datetime.now() - start 84 wfd.write('END | {} | {}\n'.format(self, timediff)) 85 return self.result 86 87 starttime = datetime.now() 88 wfd.write('START | {} | {}\n'.format(self.name, starttime.time())) 89 if self.result == TestResult.SKIP: 90 return finish(self, starttime, wfd) 91 92 cmd[-1] = self.name 93 try: 94 subprocess.run(cmd, universal_newlines=True, stdout=wfd, 95 stderr=subprocess.STDOUT, timeout=timeout) 96 for line in stripped_file(rfd): 97 if self.result != TestResult.UNKNOWN: 98 continue 99 elif line.startswith('failure:') or line.startswith('error:'): 100 self.result = TestResult.FAIL 101 elif line.startswith('success:'): 102 self.result = TestResult.PASS 103 elif line.startswith('skip:'): 104 self.result = TestResult.SKIP 105 except subprocess.TimeoutExpired: 106 self.result = TestResult.KILLED 107 wfd.write('\nKilled due to timeout\n') 108 rfd.read() 109 110 return finish(self, starttime, wfd) 111 112def should_skip(test, pattern, verbose): 113 """Returns whether test matches pattern, indicating it should be skipped.""" 114 115 if not pattern or not pattern.match(test): 116 return False 117 118 if verbose: 119 print('{} matches exception pattern; marking as skipped'.format(test)) 120 return True 121 122def fnm2regex(fnm_pat): 123 """Maps an fnmatch(7) pattern to a regex pattern that will match against 124 any suite that encapsulates the test name""" 125 126 rpat = fnmatch.translate(fnm_pat) 127 return r'{}|{}'.format(rpat, rpat.replace(r'\Z', r'\.')) 128 129def combine_patterns(iterable, verbose): 130 """Combines patterns in an iterable into a single REGEX""" 131 132 pat = re.compile('|'.join(map(fnm2regex, iterable)), 133 flags=re.DEBUG if verbose > 1 else 0) 134 if verbose > 1: 135 print('final pattern: {}'.format(pat.pattern)) 136 return pat 137 138class ArgumentFile(argparse.FileType): 139 """argparse.FileType, but wrapped in stripped_file()""" 140 141 def __call__(self, *args, **kwargs): 142 return stripped_file(argparse.FileType.__call__(self, *args, **kwargs)) 143 144 145def main(): 146 parser = argparse.ArgumentParser(description= 147 'Run a set of smbtorture tests, parsing the results.') 148 149 parser.add_argument('server', help='The target server') 150 parser.add_argument('share', help='The target share') 151 parser.add_argument('user', help='Username for smbtorture') 152 parser.add_argument('password', help='Password for user') 153 154 parser.add_argument('--except', '-e', 155 type=ArgumentFile('r'), metavar='EXCEPTIONS_FILE', dest='skip_list', 156 help='A file containing fnmatch(5) patterns of tests to skip') 157 parser.add_argument('--list', '-l', 158 type=ArgumentFile('r'), metavar='LIST_FILE', 159 help='A file containing the list of tests to run') 160 parser.add_argument('--match', '-m', 161 action='append', metavar='FNMATCH', 162 help='An fnmatch(5) pattern to select tests from smbtorture --list') 163 parser.add_argument('--output', '-o', 164 default='/tmp/lastrun.log', metavar='LOG_FILE', 165 help='Location to store full smbtorture output') 166 parser.add_argument('--seed', '-s', 167 type=int, 168 help='Seed passed to smbtorture') 169 parser.add_argument('--timeout', '-t', 170 default=120, type=float, 171 help='Timeout after which test is killed') 172 parser.add_argument('--verbose', '-v', 173 action='count', default=0, 174 help='Verbose output') 175 176 args = parser.parse_args() 177 178 if (args.match == None) == (args.list == None): 179 print('Must provide one of -l and -m') 180 return 181 182 server = args.server 183 share = args.share 184 user = args.user 185 pswd = args.password 186 fout = args.output 187 188 if args.match != None: 189 if args.verbose > 1: 190 print('Patterns to match:') 191 print(*args.match) 192 193 testgen = matched_suites(combine_patterns(args.match, args.verbose)) 194 else: 195 testgen = args.list 196 197 if args.skip_list != None: 198 skip_pat = combine_patterns(parse_tests(args.skip_list), args.verbose) 199 if args.verbose > 1: 200 print('Exceptions pattern (in REGEX): {}'.format(skip_pat.pattern)) 201 else: 202 skip_pat = None 203 204 tests = [TestCase(line, should_skip(line, skip_pat, args.verbose)) 205 for line in parse_tests(testgen)] 206 207 if args.verbose: 208 print('Tests to run:') 209 for test in tests: 210 if test.result != TestResult.SKIP: 211 print(test.name) 212 213 outw = open(fout, 'w', buffering=1) 214 outr = open(fout, 'r') 215 216 cmd = 'smbtorture //{srv}/{shr} -U{usr}%{pswd}'.format( 217 srv=server, shr=share, usr=user, pswd=pswd).split() 218 if args.seed != None: 219 cmd.append('--seed={}'.format(args.seed)) 220 cmd.append('TEST_HERE') 221 222 if args.verbose: 223 print('Command to run:') 224 print(*cmd) 225 226 results = {res: 0 for res in TestResult} 227 for test in tests: 228 print(test.name, end=': ', flush=True) 229 res = test.run(outr, outw, args.timeout, cmd) 230 results[res] += 1 231 print(res, flush=True) 232 233 print('\n\nRESULTS:') 234 print('=' * 22) 235 for res in TestResult: 236 print('{}: {:>{}}'.format(res, results[res], 20 - len(res))) 237 print('=' * 22) 238 print('Total: {:>15}'.format(len(tests))) 239 240if __name__ == '__main__': 241 try: 242 main() 243 except KeyboardInterrupt: 244 print('Terminated by KeyboardInterrupt.') 245