xref: /illumos-gate/usr/src/test/smbsrv-tests/cmd/run_smbtorture.py (revision 56726c7e321b6e5ecb2f10215f5386016547e68c)
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