| 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 | |
| 19 | import os |
| 20 | import re |
| 21 | import sys |
| 22 | import argparse |
| 23 | import shutil |
| 24 | from pathlib import Path |
| 25 | import warnings |
| 26 | |
| 27 | from findtests import TestFinder |
| 28 | from testenv import TestEnv |
| 29 | from testrunner import TestRunner |
| 30 | |
| 31 | def 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 | |
| 44 | def 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 | |
| 141 | def 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 | |
| 174 | if __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) |