123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318 |
- # TestEnv class to manage test environment variables.
- #
- # Copyright (c) 2020-2021 Virtuozzo International GmbH
- #
- # This program is free software; you can redistribute it and/or modify
- # it under the terms of the GNU General Public License as published by
- # the Free Software Foundation; either version 2 of the License, or
- # (at your option) any later version.
- #
- # This program is distributed in the hope that it will be useful,
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- # GNU General Public License for more details.
- #
- # You should have received a copy of the GNU General Public License
- # along with this program. If not, see <http://www.gnu.org/licenses/>.
- #
- import os
- import sys
- import tempfile
- from pathlib import Path
- import shutil
- import collections
- import random
- import subprocess
- import glob
- from typing import List, Dict, Any, Optional
- if sys.version_info >= (3, 9):
- from contextlib import AbstractContextManager as ContextManager
- else:
- from typing import ContextManager
- DEF_GDB_OPTIONS = 'localhost:12345'
- def isxfile(path: str) -> bool:
- return os.path.isfile(path) and os.access(path, os.X_OK)
- def get_default_machine(qemu_prog: str) -> str:
- outp = subprocess.run([qemu_prog, '-machine', 'help'], check=True,
- universal_newlines=True,
- stdout=subprocess.PIPE).stdout
- machines = outp.split('\n')
- try:
- default_machine = next(m for m in machines if ' (default)' in m)
- except StopIteration:
- return ''
- default_machine = default_machine.split(' ', 1)[0]
- alias_suf = ' (alias of {})'.format(default_machine)
- alias = next((m for m in machines if m.endswith(alias_suf)), None)
- if alias is not None:
- default_machine = alias.split(' ', 1)[0]
- return default_machine
- class TestEnv(ContextManager['TestEnv']):
- """
- Manage system environment for running tests
- The following variables are supported/provided. They are represented by
- lower-cased TestEnv attributes.
- """
- # We store environment variables as instance attributes, and there are a
- # lot of them. Silence pylint:
- # pylint: disable=too-many-instance-attributes
- env_variables = ['PYTHONPATH', 'TEST_DIR', 'SOCK_DIR', 'SAMPLE_IMG_DIR',
- 'PYTHON', 'QEMU_PROG', 'QEMU_IMG_PROG',
- 'QEMU_IO_PROG', 'QEMU_NBD_PROG', 'QSD_PROG',
- 'QEMU_OPTIONS', 'QEMU_IMG_OPTIONS',
- 'QEMU_IO_OPTIONS', 'QEMU_IO_OPTIONS_NO_FMT',
- 'QEMU_NBD_OPTIONS', 'IMGOPTS', 'IMGFMT', 'IMGPROTO',
- 'AIOMODE', 'CACHEMODE', 'VALGRIND_QEMU',
- 'CACHEMODE_IS_DEFAULT', 'IMGFMT_GENERIC', 'IMGOPTSSYNTAX',
- 'IMGKEYSECRET', 'QEMU_DEFAULT_MACHINE', 'MALLOC_PERTURB_',
- 'GDB_OPTIONS', 'PRINT_QEMU']
- def prepare_subprocess(self, args: List[str]) -> Dict[str, str]:
- if self.debug:
- args.append('-d')
- with open(args[0], encoding="utf-8") as f:
- try:
- if f.readline().rstrip() == '#!/usr/bin/env python3':
- args.insert(0, self.python)
- except UnicodeDecodeError: # binary test? for future.
- pass
- os_env = os.environ.copy()
- os_env.update(self.get_env())
- return os_env
- def get_env(self) -> Dict[str, str]:
- env = {}
- for v in self.env_variables:
- val = getattr(self, v.lower(), None)
- if val is not None:
- env[v] = val
- return env
- def init_directories(self) -> None:
- """Init directory variables:
- PYTHONPATH
- TEST_DIR
- SOCK_DIR
- SAMPLE_IMG_DIR
- """
- # Path where qemu goodies live in this source tree.
- qemu_srctree_path = Path(__file__, '../../../python').resolve()
- self.pythonpath = os.pathsep.join(filter(None, (
- self.source_iotests,
- str(qemu_srctree_path),
- os.getenv('PYTHONPATH'),
- )))
- self.test_dir = os.getenv('TEST_DIR',
- os.path.join(os.getcwd(), 'scratch'))
- Path(self.test_dir).mkdir(parents=True, exist_ok=True)
- try:
- self.sock_dir = os.environ['SOCK_DIR']
- self.tmp_sock_dir = False
- Path(self.sock_dir).mkdir(parents=True, exist_ok=True)
- except KeyError:
- self.sock_dir = tempfile.mkdtemp(prefix="qemu-iotests-")
- self.tmp_sock_dir = True
- self.sample_img_dir = os.getenv('SAMPLE_IMG_DIR',
- os.path.join(self.source_iotests,
- 'sample_images'))
- def init_binaries(self) -> None:
- """Init binary path variables:
- PYTHON (for bash tests)
- QEMU_PROG, QEMU_IMG_PROG, QEMU_IO_PROG, QEMU_NBD_PROG, QSD_PROG
- """
- self.python = sys.executable
- def root(*names: str) -> str:
- return os.path.join(self.build_root, *names)
- arch = os.uname().machine
- if 'ppc64' in arch:
- arch = 'ppc64'
- self.qemu_prog = os.getenv('QEMU_PROG', root(f'qemu-system-{arch}'))
- if not os.path.exists(self.qemu_prog):
- pattern = root('qemu-system-*')
- try:
- progs = sorted(glob.iglob(pattern))
- self.qemu_prog = next(p for p in progs if isxfile(p))
- except StopIteration:
- sys.exit("Not found any Qemu executable binary by pattern "
- f"'{pattern}'")
- self.qemu_img_prog = os.getenv('QEMU_IMG_PROG', root('qemu-img'))
- self.qemu_io_prog = os.getenv('QEMU_IO_PROG', root('qemu-io'))
- self.qemu_nbd_prog = os.getenv('QEMU_NBD_PROG', root('qemu-nbd'))
- self.qsd_prog = os.getenv('QSD_PROG', root('storage-daemon',
- 'qemu-storage-daemon'))
- for b in [self.qemu_img_prog, self.qemu_io_prog, self.qemu_nbd_prog,
- self.qemu_prog, self.qsd_prog]:
- if not os.path.exists(b):
- sys.exit('No such file: ' + b)
- if not isxfile(b):
- sys.exit('Not executable: ' + b)
- def __init__(self, source_dir: str, build_dir: str,
- imgfmt: str, imgproto: str, aiomode: str,
- cachemode: Optional[str] = None,
- imgopts: Optional[str] = None,
- misalign: bool = False,
- debug: bool = False,
- valgrind: bool = False,
- gdb: bool = False,
- qprint: bool = False,
- dry_run: bool = False) -> None:
- self.imgfmt = imgfmt
- self.imgproto = imgproto
- self.aiomode = aiomode
- self.imgopts = imgopts
- self.misalign = misalign
- self.debug = debug
- if qprint:
- self.print_qemu = 'y'
- if gdb:
- self.gdb_options = os.getenv('GDB_OPTIONS', DEF_GDB_OPTIONS)
- if not self.gdb_options:
- # cover the case 'export GDB_OPTIONS='
- self.gdb_options = DEF_GDB_OPTIONS
- elif 'GDB_OPTIONS' in os.environ:
- # to not propagate it in prepare_subprocess()
- del os.environ['GDB_OPTIONS']
- if valgrind:
- self.valgrind_qemu = 'y'
- if cachemode is None:
- self.cachemode_is_default = 'true'
- self.cachemode = 'writeback'
- else:
- self.cachemode_is_default = 'false'
- self.cachemode = cachemode
- # Initialize generic paths: build_root, build_iotests, source_iotests,
- # which are needed to initialize some environment variables. They are
- # used by init_*() functions as well.
- self.source_iotests = source_dir
- self.build_iotests = build_dir
- self.build_root = Path(self.build_iotests).parent.parent
- self.init_directories()
- if dry_run:
- return
- self.init_binaries()
- self.malloc_perturb_ = os.getenv('MALLOC_PERTURB_',
- str(random.randrange(1, 255)))
- # QEMU_OPTIONS
- self.qemu_options = '-nodefaults -display none -accel qtest'
- machine_map = (
- ('arm', 'virt'),
- ('aarch64', 'virt'),
- ('avr', 'mega2560'),
- ('m68k', 'virt'),
- ('riscv32', 'virt'),
- ('riscv64', 'virt'),
- ('rx', 'gdbsim-r5f562n8'),
- ('tricore', 'tricore_testboard')
- )
- for suffix, machine in machine_map:
- if self.qemu_prog.endswith(f'qemu-system-{suffix}'):
- self.qemu_options += f' -machine {machine}'
- # QEMU_DEFAULT_MACHINE
- self.qemu_default_machine = get_default_machine(self.qemu_prog)
- self.qemu_img_options = os.getenv('QEMU_IMG_OPTIONS')
- self.qemu_nbd_options = os.getenv('QEMU_NBD_OPTIONS')
- is_generic = self.imgfmt not in ['bochs', 'cloop', 'dmg', 'vvfat']
- self.imgfmt_generic = 'true' if is_generic else 'false'
- self.qemu_io_options = f'--cache {self.cachemode} --aio {self.aiomode}'
- if self.misalign:
- self.qemu_io_options += ' --misalign'
- self.qemu_io_options_no_fmt = self.qemu_io_options
- if self.imgfmt == 'luks':
- self.imgoptssyntax = 'true'
- self.imgkeysecret = '123456'
- if not self.imgopts:
- self.imgopts = 'iter-time=10'
- elif 'iter-time=' not in self.imgopts:
- self.imgopts += ',iter-time=10'
- else:
- self.imgoptssyntax = 'false'
- self.qemu_io_options += ' -f ' + self.imgfmt
- if self.imgfmt == 'vmdk':
- if not self.imgopts:
- self.imgopts = 'zeroed_grain=on'
- elif 'zeroed_grain=' not in self.imgopts:
- self.imgopts += ',zeroed_grain=on'
- def close(self) -> None:
- if self.tmp_sock_dir:
- shutil.rmtree(self.sock_dir)
- def __enter__(self) -> 'TestEnv':
- return self
- def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None:
- self.close()
- def print_env(self, prefix: str = '') -> None:
- template = """\
- {prefix}QEMU -- "{QEMU_PROG}" {QEMU_OPTIONS}
- {prefix}QEMU_IMG -- "{QEMU_IMG_PROG}" {QEMU_IMG_OPTIONS}
- {prefix}QEMU_IO -- "{QEMU_IO_PROG}" {QEMU_IO_OPTIONS}
- {prefix}QEMU_NBD -- "{QEMU_NBD_PROG}" {QEMU_NBD_OPTIONS}
- {prefix}IMGFMT -- {IMGFMT}{imgopts}
- {prefix}IMGPROTO -- {IMGPROTO}
- {prefix}PLATFORM -- {platform}
- {prefix}TEST_DIR -- {TEST_DIR}
- {prefix}SOCK_DIR -- {SOCK_DIR}
- {prefix}GDB_OPTIONS -- {GDB_OPTIONS}
- {prefix}VALGRIND_QEMU -- {VALGRIND_QEMU}
- {prefix}PRINT_QEMU_OUTPUT -- {PRINT_QEMU}
- {prefix}"""
- args = collections.defaultdict(str, self.get_env())
- if 'IMGOPTS' in args:
- args['imgopts'] = f" ({args['IMGOPTS']})"
- u = os.uname()
- args['platform'] = f'{u.sysname}/{u.machine} {u.nodename} {u.release}'
- args['prefix'] = prefix
- print(template.format_map(args))
|