1#!/usr/bin/env python3 2# 3# Configure environment and run group of tests in it. 4# 5# Copyright (c) 2020-2021 Virtuozzo International GmbH 6# 7# This program is free software; you can redistribute it and/or 8# modify it under the terms of the GNU General Public License as 9# published by the Free Software Foundation. 10# 11# This program is distributed in the hope that it would be useful, 12# but WITHOUT ANY WARRANTY; without even the implied warranty of 13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14# GNU General Public License for more details. 15# 16# You should have received a copy of the GNU General Public License 17# along with this program. If not, see <http://www.gnu.org/licenses/>. 18 19import os 20import re 21import sys 22import argparse 23import shutil 24from pathlib import Path 25import warnings 26 27from findtests import TestFinder 28from testenv import TestEnv 29from testrunner import TestRunner 30 31def get_default_path(follow_link=False): 32 """ 33 Try to automagically figure out the path we are running from. 34 """ 35 # called from the build tree? 36 if os.path.islink(sys.argv[0]): 37 if follow_link: 38 return os.path.dirname(os.readlink(sys.argv[0])) 39 else: 40 return os.path.dirname(os.path.abspath(sys.argv[0])) 41 else: # or source tree? 42 return os.getcwd() 43 44def make_argparser() -> argparse.ArgumentParser: 45 p = argparse.ArgumentParser( 46 description="Test run options", 47 formatter_class=argparse.ArgumentDefaultsHelpFormatter) 48 49 p.add_argument('-n', '--dry-run', action='store_true', 50 help='show me, do not run tests') 51 p.add_argument('-j', dest='jobs', type=int, default=1, 52 help='run tests in multiple parallel jobs') 53 54 p.add_argument('-d', dest='debug', action='store_true', help='debug') 55 p.add_argument('-p', dest='print', action='store_true', 56 help='redirects qemu\'s stdout and stderr to ' 57 'the test output') 58 p.add_argument('-gdb', action='store_true', 59 help="start gdbserver with $GDB_OPTIONS options " 60 "('localhost:12345' if $GDB_OPTIONS is empty)") 61 p.add_argument('-valgrind', action='store_true', 62 help='use valgrind, sets VALGRIND_QEMU environment ' 63 'variable') 64 65 p.add_argument('-misalign', action='store_true', 66 help='misalign memory allocations') 67 p.add_argument('--color', choices=['on', 'off', 'auto'], 68 default='auto', help="use terminal colors. The default " 69 "'auto' value means use colors if terminal stdout detected") 70 p.add_argument('-tap', action='store_true', 71 help='produce TAP output') 72 73 g_env = p.add_argument_group('test environment options') 74 mg = g_env.add_mutually_exclusive_group() 75 # We don't set default for cachemode, as we need to distinguish default 76 # from user input later. 77 mg.add_argument('-nocache', dest='cachemode', action='store_const', 78 const='none', help='set cache mode "none" (O_DIRECT), ' 79 'sets CACHEMODE environment variable') 80 mg.add_argument('-c', dest='cachemode', 81 help='sets CACHEMODE environment variable') 82 83 g_env.add_argument('-i', dest='aiomode', default='threads', 84 help='sets AIOMODE environment variable') 85 86 p.set_defaults(imgproto='file') 87 88 format_list = ['raw', 'bochs', 'cloop', 'parallels', 'qcow', 'qcow2', 89 'qed', 'vdi', 'vpc', 'vhdx', 'vmdk', 'luks', 'dmg', 'vvfat'] 90 g_fmt = p.add_argument_group( 91 ' image format options', 92 'The following options set the IMGFMT environment variable. ' 93 'At most one choice is allowed, default is "raw"') 94 mg = g_fmt.add_mutually_exclusive_group() 95 for fmt in format_list: 96 mg.add_argument('-' + fmt, dest='imgfmt', action='store_const', 97 const=fmt, help=f'test {fmt}') 98 99 protocol_list = ['file', 'rbd', 'nbd', 'ssh', 'nfs', 'fuse'] 100 g_prt = p.add_argument_group( 101 ' image protocol options', 102 'The following options set the IMGPROTO environment variable. ' 103 'At most one choice is allowed, default is "file"') 104 mg = g_prt.add_mutually_exclusive_group() 105 for prt in protocol_list: 106 mg.add_argument('-' + prt, dest='imgproto', action='store_const', 107 const=prt, help=f'test {prt}') 108 109 g_bash = p.add_argument_group('bash tests options', 110 'The following options are ignored by ' 111 'python tests.') 112 # TODO: make support for the following options in iotests.py 113 g_bash.add_argument('-o', dest='imgopts', 114 help='options to pass to qemu-img create/convert, ' 115 'sets IMGOPTS environment variable') 116 117 g_sel = p.add_argument_group('test selecting options', 118 'The following options specify test set ' 119 'to run.') 120 g_sel.add_argument('-g', '--groups', metavar='group1,...', 121 help='include tests from these groups') 122 g_sel.add_argument('-x', '--exclude-groups', metavar='group1,...', 123 help='exclude tests from these groups') 124 g_sel.add_argument('--start-from', metavar='TEST', 125 help='Start from specified test: make sorted sequence ' 126 'of tests as usual and then drop tests from the first ' 127 'one to TEST (not inclusive). This may be used to ' 128 'rerun failed ./check command, starting from the ' 129 'middle of the process.') 130 g_sel.add_argument('tests', metavar='TEST_FILES', nargs='*', 131 help='tests to run, or "--" followed by a command') 132 g_sel.add_argument('--build-dir', default=get_default_path(), 133 help='Path to iotests build directory') 134 g_sel.add_argument('--source-dir', 135 default=get_default_path(follow_link=True), 136 help='Path to iotests build directory') 137 138 return p 139 140 141def dry_run_list(test_dir, imgfmt, testlist): 142 for t in testlist: 143 if not imgfmt: 144 print('\n'.join([os.path.basename(t)])) 145 continue 146 # If a format has been given, we look for the "supported_fmt" 147 # and the "unsupported_fmt" lines in the test and try to find out 148 # whether the format is supported or not. This is only heuristics 149 # (it can e.g. fail if the "unsupported_fmts" and "supported_fmts" 150 # statements are in the same line), but it should be good enough 151 # to get a proper list for "make check-block" 152 with open(os.path.join(test_dir, t), 'r', encoding='utf-8') as fh: 153 supported = True 154 check_next_line = False 155 sd = "[ \t'\"]" # Start delimiter 156 ed = "([ \t'\"]|$)" # End delimiter 157 for line in fh: 158 if 'unsupported_fmt' in line: 159 if re.search(sd + imgfmt + ed, line): 160 supported = False 161 break 162 elif 'supported_fmt' in line or check_next_line: 163 if re.search(sd + 'generic' + ed, line): 164 continue # Might be followed by "unsupported" line 165 supported = re.search(sd + imgfmt + ed, line) 166 check_next_line = not ']' in line and \ 167 ('supported_fmts=[' in line or check_next_line) 168 if supported or not check_next_line: 169 break 170 if supported: 171 print('\n'.join([os.path.basename(t)])) 172 173 174if __name__ == '__main__': 175 warnings.simplefilter("default") 176 os.environ["PYTHONWARNINGS"] = "default" 177 178 args = make_argparser().parse_args() 179 180 image_format = args.imgfmt or 'raw' 181 182 env = TestEnv(source_dir=args.source_dir, 183 build_dir=args.build_dir, 184 imgfmt=image_format, imgproto=args.imgproto, 185 aiomode=args.aiomode, cachemode=args.cachemode, 186 imgopts=args.imgopts, misalign=args.misalign, 187 debug=args.debug, valgrind=args.valgrind, 188 gdb=args.gdb, qprint=args.print, 189 dry_run=args.dry_run) 190 191 if len(sys.argv) > 1 and sys.argv[-len(args.tests)-1] == '--': 192 if not args.tests: 193 sys.exit("missing command after '--'") 194 cmd = args.tests 195 env.print_env() 196 exec_pathstr = shutil.which(cmd[0]) 197 if exec_pathstr is None: 198 sys.exit('command not found: ' + cmd[0]) 199 exec_path = Path(exec_pathstr).resolve() 200 cmd[0] = str(exec_path) 201 full_env = env.prepare_subprocess(cmd) 202 os.chdir(exec_path.parent) 203 os.execve(cmd[0], cmd, full_env) 204 205 testfinder = TestFinder(test_dir=env.source_iotests) 206 207 groups = args.groups.split(',') if args.groups else None 208 x_groups = args.exclude_groups.split(',') if args.exclude_groups else None 209 210 group_local = os.path.join(env.source_iotests, 'group.local') 211 if os.path.isfile(group_local): 212 try: 213 testfinder.add_group_file(group_local) 214 except ValueError as e: 215 sys.exit(f"Failed to parse group file '{group_local}': {e}") 216 217 try: 218 tests = testfinder.find_tests(groups=groups, exclude_groups=x_groups, 219 tests=args.tests, 220 start_from=args.start_from) 221 if not tests: 222 raise ValueError('No tests selected') 223 except ValueError as e: 224 sys.exit(str(e)) 225 226 if args.dry_run: 227 with env: 228 dry_run_list(env.source_iotests, args.imgfmt, tests) 229 else: 230 with TestRunner(env, tap=args.tap, 231 color=args.color) as tr: 232 paths = [os.path.join(env.source_iotests, t) for t in tests] 233 ok = tr.run_tests(paths, args.jobs) 234 if not ok: 235 sys.exit(1) 236