Merge tag 'pull-request-2024-02-23' of https://gitlab.com/thuth/qemu into staging
[qemu/kevin.git] / tests / qemu-iotests / testenv.py
blob588f30a4f14ec75979ba6e239265c3cb6244180b
1 # TestEnv class to manage test environment variables.
3 # Copyright (c) 2020-2021 Virtuozzo International GmbH
5 # This program is free software; you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 2 of the License, or
8 # (at your option) any later version.
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
15 # You should have received a copy of the GNU General Public License
16 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19 import os
20 import sys
21 import tempfile
22 from pathlib import Path
23 import shutil
24 import collections
25 import random
26 import subprocess
27 import glob
28 from typing import List, Dict, Any, Optional, ContextManager
30 DEF_GDB_OPTIONS = 'localhost:12345'
32 def isxfile(path: str) -> bool:
33 return os.path.isfile(path) and os.access(path, os.X_OK)
36 def get_default_machine(qemu_prog: str) -> str:
37 outp = subprocess.run([qemu_prog, '-machine', 'help'], check=True,
38 universal_newlines=True,
39 stdout=subprocess.PIPE).stdout
41 machines = outp.split('\n')
42 try:
43 default_machine = next(m for m in machines if ' (default)' in m)
44 except StopIteration:
45 return ''
46 default_machine = default_machine.split(' ', 1)[0]
48 alias_suf = ' (alias of {})'.format(default_machine)
49 alias = next((m for m in machines if m.endswith(alias_suf)), None)
50 if alias is not None:
51 default_machine = alias.split(' ', 1)[0]
53 return default_machine
56 class TestEnv(ContextManager['TestEnv']):
57 """
58 Manage system environment for running tests
60 The following variables are supported/provided. They are represented by
61 lower-cased TestEnv attributes.
62 """
64 # We store environment variables as instance attributes, and there are a
65 # lot of them. Silence pylint:
66 # pylint: disable=too-many-instance-attributes
68 env_variables = ['PYTHONPATH', 'TEST_DIR', 'SOCK_DIR', 'SAMPLE_IMG_DIR',
69 'PYTHON', 'QEMU_PROG', 'QEMU_IMG_PROG',
70 'QEMU_IO_PROG', 'QEMU_NBD_PROG', 'QSD_PROG',
71 'QEMU_OPTIONS', 'QEMU_IMG_OPTIONS',
72 'QEMU_IO_OPTIONS', 'QEMU_IO_OPTIONS_NO_FMT',
73 'QEMU_NBD_OPTIONS', 'IMGOPTS', 'IMGFMT', 'IMGPROTO',
74 'AIOMODE', 'CACHEMODE', 'VALGRIND_QEMU',
75 'CACHEMODE_IS_DEFAULT', 'IMGFMT_GENERIC', 'IMGOPTSSYNTAX',
76 'IMGKEYSECRET', 'QEMU_DEFAULT_MACHINE', 'MALLOC_PERTURB_',
77 'GDB_OPTIONS', 'PRINT_QEMU']
79 def prepare_subprocess(self, args: List[str]) -> Dict[str, str]:
80 if self.debug:
81 args.append('-d')
83 with open(args[0], encoding="utf-8") as f:
84 try:
85 if f.readline().rstrip() == '#!/usr/bin/env python3':
86 args.insert(0, self.python)
87 except UnicodeDecodeError: # binary test? for future.
88 pass
90 os_env = os.environ.copy()
91 os_env.update(self.get_env())
92 return os_env
94 def get_env(self) -> Dict[str, str]:
95 env = {}
96 for v in self.env_variables:
97 val = getattr(self, v.lower(), None)
98 if val is not None:
99 env[v] = val
101 return env
103 def init_directories(self) -> None:
104 """Init directory variables:
105 PYTHONPATH
106 TEST_DIR
107 SOCK_DIR
108 SAMPLE_IMG_DIR
111 # Path where qemu goodies live in this source tree.
112 qemu_srctree_path = Path(__file__, '../../../python').resolve()
114 self.pythonpath = os.pathsep.join(filter(None, (
115 self.source_iotests,
116 str(qemu_srctree_path),
117 os.getenv('PYTHONPATH'),
120 self.test_dir = os.getenv('TEST_DIR',
121 os.path.join(os.getcwd(), 'scratch'))
122 Path(self.test_dir).mkdir(parents=True, exist_ok=True)
124 try:
125 self.sock_dir = os.environ['SOCK_DIR']
126 self.tmp_sock_dir = False
127 Path(self.sock_dir).mkdir(parents=True, exist_ok=True)
128 except KeyError:
129 self.sock_dir = tempfile.mkdtemp(prefix="qemu-iotests-")
130 self.tmp_sock_dir = True
132 self.sample_img_dir = os.getenv('SAMPLE_IMG_DIR',
133 os.path.join(self.source_iotests,
134 'sample_images'))
136 def init_binaries(self) -> None:
137 """Init binary path variables:
138 PYTHON (for bash tests)
139 QEMU_PROG, QEMU_IMG_PROG, QEMU_IO_PROG, QEMU_NBD_PROG, QSD_PROG
141 self.python = sys.executable
143 def root(*names: str) -> str:
144 return os.path.join(self.build_root, *names)
146 arch = os.uname().machine
147 if 'ppc64' in arch:
148 arch = 'ppc64'
150 self.qemu_prog = os.getenv('QEMU_PROG', root(f'qemu-system-{arch}'))
151 if not os.path.exists(self.qemu_prog):
152 pattern = root('qemu-system-*')
153 try:
154 progs = sorted(glob.iglob(pattern))
155 self.qemu_prog = next(p for p in progs if isxfile(p))
156 except StopIteration:
157 sys.exit("Not found any Qemu executable binary by pattern "
158 f"'{pattern}'")
160 self.qemu_img_prog = os.getenv('QEMU_IMG_PROG', root('qemu-img'))
161 self.qemu_io_prog = os.getenv('QEMU_IO_PROG', root('qemu-io'))
162 self.qemu_nbd_prog = os.getenv('QEMU_NBD_PROG', root('qemu-nbd'))
163 self.qsd_prog = os.getenv('QSD_PROG', root('storage-daemon',
164 'qemu-storage-daemon'))
166 for b in [self.qemu_img_prog, self.qemu_io_prog, self.qemu_nbd_prog,
167 self.qemu_prog, self.qsd_prog]:
168 if not os.path.exists(b):
169 sys.exit('No such file: ' + b)
170 if not isxfile(b):
171 sys.exit('Not executable: ' + b)
173 def __init__(self, source_dir: str, build_dir: str,
174 imgfmt: str, imgproto: str, aiomode: str,
175 cachemode: Optional[str] = None,
176 imgopts: Optional[str] = None,
177 misalign: bool = False,
178 debug: bool = False,
179 valgrind: bool = False,
180 gdb: bool = False,
181 qprint: bool = False,
182 dry_run: bool = False) -> None:
183 self.imgfmt = imgfmt
184 self.imgproto = imgproto
185 self.aiomode = aiomode
186 self.imgopts = imgopts
187 self.misalign = misalign
188 self.debug = debug
190 if qprint:
191 self.print_qemu = 'y'
193 if gdb:
194 self.gdb_options = os.getenv('GDB_OPTIONS', DEF_GDB_OPTIONS)
195 if not self.gdb_options:
196 # cover the case 'export GDB_OPTIONS='
197 self.gdb_options = DEF_GDB_OPTIONS
198 elif 'GDB_OPTIONS' in os.environ:
199 # to not propagate it in prepare_subprocess()
200 del os.environ['GDB_OPTIONS']
202 if valgrind:
203 self.valgrind_qemu = 'y'
205 if cachemode is None:
206 self.cachemode_is_default = 'true'
207 self.cachemode = 'writeback'
208 else:
209 self.cachemode_is_default = 'false'
210 self.cachemode = cachemode
212 # Initialize generic paths: build_root, build_iotests, source_iotests,
213 # which are needed to initialize some environment variables. They are
214 # used by init_*() functions as well.
216 self.source_iotests = source_dir
217 self.build_iotests = build_dir
219 self.build_root = Path(self.build_iotests).parent.parent
221 self.init_directories()
223 if dry_run:
224 return
226 self.init_binaries()
228 self.malloc_perturb_ = os.getenv('MALLOC_PERTURB_',
229 str(random.randrange(1, 255)))
231 # QEMU_OPTIONS
232 self.qemu_options = '-nodefaults -display none -accel qtest'
233 machine_map = (
234 ('arm', 'virt'),
235 ('aarch64', 'virt'),
236 ('avr', 'mega2560'),
237 ('m68k', 'virt'),
238 ('riscv32', 'virt'),
239 ('riscv64', 'virt'),
240 ('rx', 'gdbsim-r5f562n8'),
241 ('tricore', 'tricore_testboard')
243 for suffix, machine in machine_map:
244 if self.qemu_prog.endswith(f'qemu-system-{suffix}'):
245 self.qemu_options += f' -machine {machine}'
247 # QEMU_DEFAULT_MACHINE
248 self.qemu_default_machine = get_default_machine(self.qemu_prog)
250 self.qemu_img_options = os.getenv('QEMU_IMG_OPTIONS')
251 self.qemu_nbd_options = os.getenv('QEMU_NBD_OPTIONS')
253 is_generic = self.imgfmt not in ['bochs', 'cloop', 'dmg']
254 self.imgfmt_generic = 'true' if is_generic else 'false'
256 self.qemu_io_options = f'--cache {self.cachemode} --aio {self.aiomode}'
257 if self.misalign:
258 self.qemu_io_options += ' --misalign'
260 self.qemu_io_options_no_fmt = self.qemu_io_options
262 if self.imgfmt == 'luks':
263 self.imgoptssyntax = 'true'
264 self.imgkeysecret = '123456'
265 if not self.imgopts:
266 self.imgopts = 'iter-time=10'
267 elif 'iter-time=' not in self.imgopts:
268 self.imgopts += ',iter-time=10'
269 else:
270 self.imgoptssyntax = 'false'
271 self.qemu_io_options += ' -f ' + self.imgfmt
273 if self.imgfmt == 'vmdk':
274 if not self.imgopts:
275 self.imgopts = 'zeroed_grain=on'
276 elif 'zeroed_grain=' not in self.imgopts:
277 self.imgopts += ',zeroed_grain=on'
279 def close(self) -> None:
280 if self.tmp_sock_dir:
281 shutil.rmtree(self.sock_dir)
283 def __enter__(self) -> 'TestEnv':
284 return self
286 def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None:
287 self.close()
289 def print_env(self, prefix: str = '') -> None:
290 template = """\
291 {prefix}QEMU -- "{QEMU_PROG}" {QEMU_OPTIONS}
292 {prefix}QEMU_IMG -- "{QEMU_IMG_PROG}" {QEMU_IMG_OPTIONS}
293 {prefix}QEMU_IO -- "{QEMU_IO_PROG}" {QEMU_IO_OPTIONS}
294 {prefix}QEMU_NBD -- "{QEMU_NBD_PROG}" {QEMU_NBD_OPTIONS}
295 {prefix}IMGFMT -- {IMGFMT}{imgopts}
296 {prefix}IMGPROTO -- {IMGPROTO}
297 {prefix}PLATFORM -- {platform}
298 {prefix}TEST_DIR -- {TEST_DIR}
299 {prefix}SOCK_DIR -- {SOCK_DIR}
300 {prefix}GDB_OPTIONS -- {GDB_OPTIONS}
301 {prefix}VALGRIND_QEMU -- {VALGRIND_QEMU}
302 {prefix}PRINT_QEMU_OUTPUT -- {PRINT_QEMU}
303 {prefix}"""
305 args = collections.defaultdict(str, self.get_env())
307 if 'IMGOPTS' in args:
308 args['imgopts'] = f" ({args['IMGOPTS']})"
310 u = os.uname()
311 args['platform'] = f'{u.sysname}/{u.machine} {u.nodename} {u.release}'
312 args['prefix'] = prefix
313 print(template.format_map(args))