Vladimir Sementsov-Ogievskiy | 2e5a2f5 | 2021-01-25 21:50:53 +0300 | [diff] [blame] | 1 | # TestEnv class to manage test environment variables. |
| 2 | # |
| 3 | # Copyright (c) 2020-2021 Virtuozzo International GmbH |
| 4 | # |
| 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. |
| 9 | # |
| 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. |
| 14 | # |
| 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/>. |
| 17 | # |
| 18 | |
| 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 |
Paolo Bonzini | c64430d | 2021-05-03 13:01:08 +0200 | [diff] [blame] | 28 | from typing import List, Dict, Any, Optional, ContextManager |
Vladimir Sementsov-Ogievskiy | 2e5a2f5 | 2021-01-25 21:50:53 +0300 | [diff] [blame] | 29 | |
Emanuele Giuseppe Esposito | cfb9b0b | 2021-08-09 11:01:03 +0200 | [diff] [blame] | 30 | DEF_GDB_OPTIONS = 'localhost:12345' |
Vladimir Sementsov-Ogievskiy | 2e5a2f5 | 2021-01-25 21:50:53 +0300 | [diff] [blame] | 31 | |
| 32 | def isxfile(path: str) -> bool: |
| 33 | return os.path.isfile(path) and os.access(path, os.X_OK) |
| 34 | |
| 35 | |
| 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 |
| 40 | |
| 41 | machines = outp.split('\n') |
| 42 | try: |
Andrey Drobyshev | 3b7094f | 2023-11-22 14:15:38 +0200 | [diff] [blame] | 43 | default_machine = next(m for m in machines if ' (default)' in m) |
Vladimir Sementsov-Ogievskiy | 2e5a2f5 | 2021-01-25 21:50:53 +0300 | [diff] [blame] | 44 | except StopIteration: |
| 45 | return '' |
| 46 | default_machine = default_machine.split(' ', 1)[0] |
| 47 | |
| 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] |
| 52 | |
| 53 | return default_machine |
| 54 | |
| 55 | |
| 56 | class TestEnv(ContextManager['TestEnv']): |
| 57 | """ |
| 58 | Manage system environment for running tests |
| 59 | |
| 60 | The following variables are supported/provided. They are represented by |
| 61 | lower-cased TestEnv attributes. |
| 62 | """ |
| 63 | |
| 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 |
| 67 | |
| 68 | env_variables = ['PYTHONPATH', 'TEST_DIR', 'SOCK_DIR', 'SAMPLE_IMG_DIR', |
Hanna Reitz | 1a8fcca | 2022-02-21 18:29:09 +0100 | [diff] [blame] | 69 | 'PYTHON', 'QEMU_PROG', 'QEMU_IMG_PROG', |
Vladimir Sementsov-Ogievskiy | 2e5a2f5 | 2021-01-25 21:50:53 +0300 | [diff] [blame] | 70 | 'QEMU_IO_PROG', 'QEMU_NBD_PROG', 'QSD_PROG', |
John Snow | c163c72 | 2021-09-22 20:49:31 -0400 | [diff] [blame] | 71 | 'QEMU_OPTIONS', 'QEMU_IMG_OPTIONS', |
Vladimir Sementsov-Ogievskiy | 2e5a2f5 | 2021-01-25 21:50:53 +0300 | [diff] [blame] | 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', |
Emanuele Giuseppe Esposito | cfb9b0b | 2021-08-09 11:01:03 +0200 | [diff] [blame] | 76 | 'IMGKEYSECRET', 'QEMU_DEFAULT_MACHINE', 'MALLOC_PERTURB_', |
Emanuele Giuseppe Esposito | eb7a91d | 2021-08-09 11:01:13 +0200 | [diff] [blame] | 77 | 'GDB_OPTIONS', 'PRINT_QEMU'] |
Vladimir Sementsov-Ogievskiy | 2e5a2f5 | 2021-01-25 21:50:53 +0300 | [diff] [blame] | 78 | |
Paolo Bonzini | c64430d | 2021-05-03 13:01:08 +0200 | [diff] [blame] | 79 | def prepare_subprocess(self, args: List[str]) -> Dict[str, str]: |
| 80 | if self.debug: |
| 81 | args.append('-d') |
| 82 | |
| 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 |
| 89 | |
| 90 | os_env = os.environ.copy() |
| 91 | os_env.update(self.get_env()) |
| 92 | return os_env |
| 93 | |
Vladimir Sementsov-Ogievskiy | 2e5a2f5 | 2021-01-25 21:50:53 +0300 | [diff] [blame] | 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 |
| 100 | |
| 101 | return env |
| 102 | |
| 103 | def init_directories(self) -> None: |
| 104 | """Init directory variables: |
| 105 | PYTHONPATH |
| 106 | TEST_DIR |
| 107 | SOCK_DIR |
| 108 | SAMPLE_IMG_DIR |
Vladimir Sementsov-Ogievskiy | 2e5a2f5 | 2021-01-25 21:50:53 +0300 | [diff] [blame] | 109 | """ |
John Snow | af6d4c5 | 2021-09-23 14:07:10 -0400 | [diff] [blame] | 110 | |
| 111 | # Path where qemu goodies live in this source tree. |
| 112 | qemu_srctree_path = Path(__file__, '../../../python').resolve() |
| 113 | |
| 114 | self.pythonpath = os.pathsep.join(filter(None, ( |
| 115 | self.source_iotests, |
| 116 | str(qemu_srctree_path), |
| 117 | os.getenv('PYTHONPATH'), |
| 118 | ))) |
Vladimir Sementsov-Ogievskiy | 2e5a2f5 | 2021-01-25 21:50:53 +0300 | [diff] [blame] | 119 | |
| 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) |
| 123 | |
| 124 | try: |
| 125 | self.sock_dir = os.environ['SOCK_DIR'] |
| 126 | self.tmp_sock_dir = False |
Paolo Bonzini | c3d479a | 2021-05-03 13:01:10 +0200 | [diff] [blame] | 127 | Path(self.sock_dir).mkdir(parents=True, exist_ok=True) |
Vladimir Sementsov-Ogievskiy | 2e5a2f5 | 2021-01-25 21:50:53 +0300 | [diff] [blame] | 128 | except KeyError: |
| 129 | self.sock_dir = tempfile.mkdtemp() |
| 130 | self.tmp_sock_dir = True |
| 131 | |
| 132 | self.sample_img_dir = os.getenv('SAMPLE_IMG_DIR', |
| 133 | os.path.join(self.source_iotests, |
| 134 | 'sample_images')) |
| 135 | |
Vladimir Sementsov-Ogievskiy | 2e5a2f5 | 2021-01-25 21:50:53 +0300 | [diff] [blame] | 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 |
Vladimir Sementsov-Ogievskiy | 2e5a2f5 | 2021-01-25 21:50:53 +0300 | [diff] [blame] | 140 | """ |
| 141 | self.python = sys.executable |
| 142 | |
| 143 | def root(*names: str) -> str: |
| 144 | return os.path.join(self.build_root, *names) |
| 145 | |
| 146 | arch = os.uname().machine |
| 147 | if 'ppc64' in arch: |
| 148 | arch = 'ppc64' |
| 149 | |
| 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: |
Kevin Wolf | ca502ca | 2021-02-02 15:28:02 +0100 | [diff] [blame] | 154 | progs = sorted(glob.iglob(pattern)) |
Vladimir Sementsov-Ogievskiy | 2e5a2f5 | 2021-01-25 21:50:53 +0300 | [diff] [blame] | 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}'") |
| 159 | |
| 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')) |
| 165 | |
| 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) |
| 172 | |
Daniel P. Berrangé | 0c8076b | 2023-03-15 17:43:17 +0000 | [diff] [blame] | 173 | def __init__(self, source_dir: str, build_dir: str, |
| 174 | imgfmt: str, imgproto: str, aiomode: str, |
Vladimir Sementsov-Ogievskiy | 2e5a2f5 | 2021-01-25 21:50:53 +0300 | [diff] [blame] | 175 | cachemode: Optional[str] = None, |
| 176 | imgopts: Optional[str] = None, |
| 177 | misalign: bool = False, |
| 178 | debug: bool = False, |
Emanuele Giuseppe Esposito | cfb9b0b | 2021-08-09 11:01:03 +0200 | [diff] [blame] | 179 | valgrind: bool = False, |
Emanuele Giuseppe Esposito | eb7a91d | 2021-08-09 11:01:13 +0200 | [diff] [blame] | 180 | gdb: bool = False, |
Daniel P. Berrangé | a9e2178 | 2023-03-15 17:43:18 +0000 | [diff] [blame] | 181 | qprint: bool = False, |
| 182 | dry_run: bool = False) -> None: |
Vladimir Sementsov-Ogievskiy | 2e5a2f5 | 2021-01-25 21:50:53 +0300 | [diff] [blame] | 183 | self.imgfmt = imgfmt |
| 184 | self.imgproto = imgproto |
| 185 | self.aiomode = aiomode |
| 186 | self.imgopts = imgopts |
| 187 | self.misalign = misalign |
| 188 | self.debug = debug |
| 189 | |
Emanuele Giuseppe Esposito | eb7a91d | 2021-08-09 11:01:13 +0200 | [diff] [blame] | 190 | if qprint: |
| 191 | self.print_qemu = 'y' |
| 192 | |
Emanuele Giuseppe Esposito | cfb9b0b | 2021-08-09 11:01:03 +0200 | [diff] [blame] | 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'] |
| 201 | |
Vladimir Sementsov-Ogievskiy | 2e5a2f5 | 2021-01-25 21:50:53 +0300 | [diff] [blame] | 202 | if valgrind: |
| 203 | self.valgrind_qemu = 'y' |
| 204 | |
| 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 |
| 211 | |
| 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. |
| 215 | |
Daniel P. Berrangé | 0c8076b | 2023-03-15 17:43:17 +0000 | [diff] [blame] | 216 | self.source_iotests = source_dir |
| 217 | self.build_iotests = build_dir |
Vladimir Sementsov-Ogievskiy | 2e5a2f5 | 2021-01-25 21:50:53 +0300 | [diff] [blame] | 218 | |
John Snow | 9444e5f | 2023-06-20 20:21:18 -0400 | [diff] [blame] | 219 | self.build_root = Path(self.build_iotests).parent.parent |
Vladimir Sementsov-Ogievskiy | 2e5a2f5 | 2021-01-25 21:50:53 +0300 | [diff] [blame] | 220 | |
| 221 | self.init_directories() |
Daniel P. Berrangé | a9e2178 | 2023-03-15 17:43:18 +0000 | [diff] [blame] | 222 | |
| 223 | if dry_run: |
| 224 | return |
| 225 | |
Vladimir Sementsov-Ogievskiy | 2e5a2f5 | 2021-01-25 21:50:53 +0300 | [diff] [blame] | 226 | self.init_binaries() |
| 227 | |
| 228 | self.malloc_perturb_ = os.getenv('MALLOC_PERTURB_', |
| 229 | str(random.randrange(1, 255))) |
| 230 | |
| 231 | # QEMU_OPTIONS |
| 232 | self.qemu_options = '-nodefaults -display none -accel qtest' |
| 233 | machine_map = ( |
| 234 | ('arm', 'virt'), |
| 235 | ('aarch64', 'virt'), |
| 236 | ('avr', 'mega2560'), |
Laurent Vivier | 7033f1f | 2021-03-23 16:53:06 +0000 | [diff] [blame] | 237 | ('m68k', 'virt'), |
laokz | 3213bba | 2022-03-08 12:33:39 +0800 | [diff] [blame] | 238 | ('riscv32', 'virt'), |
| 239 | ('riscv64', 'virt'), |
Vladimir Sementsov-Ogievskiy | 2e5a2f5 | 2021-01-25 21:50:53 +0300 | [diff] [blame] | 240 | ('rx', 'gdbsim-r5f562n8'), |
| 241 | ('tricore', 'tricore_testboard') |
| 242 | ) |
| 243 | for suffix, machine in machine_map: |
| 244 | if self.qemu_prog.endswith(f'qemu-system-{suffix}'): |
| 245 | self.qemu_options += f' -machine {machine}' |
| 246 | |
| 247 | # QEMU_DEFAULT_MACHINE |
| 248 | self.qemu_default_machine = get_default_machine(self.qemu_prog) |
| 249 | |
| 250 | self.qemu_img_options = os.getenv('QEMU_IMG_OPTIONS') |
| 251 | self.qemu_nbd_options = os.getenv('QEMU_NBD_OPTIONS') |
| 252 | |
| 253 | is_generic = self.imgfmt not in ['bochs', 'cloop', 'dmg'] |
| 254 | self.imgfmt_generic = 'true' if is_generic else 'false' |
| 255 | |
| 256 | self.qemu_io_options = f'--cache {self.cachemode} --aio {self.aiomode}' |
| 257 | if self.misalign: |
| 258 | self.qemu_io_options += ' --misalign' |
| 259 | |
| 260 | self.qemu_io_options_no_fmt = self.qemu_io_options |
| 261 | |
| 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 |
| 272 | |
| 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' |
| 278 | |
| 279 | def close(self) -> None: |
| 280 | if self.tmp_sock_dir: |
| 281 | shutil.rmtree(self.sock_dir) |
| 282 | |
| 283 | def __enter__(self) -> 'TestEnv': |
| 284 | return self |
| 285 | |
| 286 | def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None: |
| 287 | self.close() |
| 288 | |
Paolo Bonzini | d316859 | 2022-01-07 13:18:11 +0100 | [diff] [blame] | 289 | def print_env(self, prefix: str = '') -> None: |
Vladimir Sementsov-Ogievskiy | 2e5a2f5 | 2021-01-25 21:50:53 +0300 | [diff] [blame] | 290 | template = """\ |
Paolo Bonzini | d316859 | 2022-01-07 13:18:11 +0100 | [diff] [blame] | 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}""" |
Vladimir Sementsov-Ogievskiy | 2e5a2f5 | 2021-01-25 21:50:53 +0300 | [diff] [blame] | 304 | |
| 305 | args = collections.defaultdict(str, self.get_env()) |
| 306 | |
| 307 | if 'IMGOPTS' in args: |
| 308 | args['imgopts'] = f" ({args['IMGOPTS']})" |
| 309 | |
| 310 | u = os.uname() |
| 311 | args['platform'] = f'{u.sysname}/{u.machine} {u.nodename} {u.release}' |
Paolo Bonzini | d316859 | 2022-01-07 13:18:11 +0100 | [diff] [blame] | 312 | args['prefix'] = prefix |
Vladimir Sementsov-Ogievskiy | 2e5a2f5 | 2021-01-25 21:50:53 +0300 | [diff] [blame] | 313 | print(template.format_map(args)) |