xref: /openbmc/qemu/tests/qemu-iotests/check (revision 9febfa94b69b7146582c48a868bd2330ac45037f)
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