| """ |
| QEMU development and testing utilities |
| |
| This package provides a small handful of utilities for performing |
| various tasks not directly related to the launching of a VM. |
| """ |
| |
| # Copyright (C) 2021 Red Hat Inc. |
| # |
| # Authors: |
| # John Snow <jsnow@redhat.com> |
| # Cleber Rosa <crosa@redhat.com> |
| # |
| # This work is licensed under the terms of the GNU GPL, version 2. See |
| # the COPYING file in the top-level directory. |
| # |
| |
| import os |
| import re |
| import shutil |
| from subprocess import CalledProcessError |
| import textwrap |
| from typing import Optional |
| |
| # pylint: disable=import-error |
| from .accel import kvm_available, list_accel, tcg_available |
| |
| |
| __all__ = ( |
| 'VerboseProcessError', |
| 'add_visual_margin', |
| 'get_info_usernet_hostfwd_port', |
| 'kvm_available', |
| 'list_accel', |
| 'tcg_available', |
| ) |
| |
| |
| def get_info_usernet_hostfwd_port(info_usernet_output: str) -> Optional[int]: |
| """ |
| Returns the port given to the hostfwd parameter via info usernet |
| |
| :param info_usernet_output: output generated by hmp command "info usernet" |
| :return: the port number allocated by the hostfwd option |
| """ |
| for line in info_usernet_output.split('\r\n'): |
| regex = r'TCP.HOST_FORWARD.*127\.0\.0\.1\s+(\d+)\s+10\.' |
| match = re.search(regex, line) |
| if match is not None: |
| return int(match[1]) |
| return None |
| |
| |
| # pylint: disable=too-many-arguments |
| def add_visual_margin( |
| content: str = '', |
| width: Optional[int] = None, |
| name: Optional[str] = None, |
| padding: int = 1, |
| upper_left: str = '┏', |
| lower_left: str = '┗', |
| horizontal: str = '━', |
| vertical: str = '┃', |
| ) -> str: |
| """ |
| Decorate and wrap some text with a visual decoration around it. |
| |
| This function assumes that the text decoration characters are single |
| characters that display using a single monospace column. |
| |
| ┏━ Example ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ |
| ┃ This is what this function looks like with text content that's |
| ┃ wrapped to 66 characters. The right-hand margin is left open to |
| ┃ accommodate the occasional unicode character that might make |
| ┃ predicting the total "visual" width of a line difficult. This |
| ┃ provides a visual distinction that's good-enough, though. |
| ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ |
| |
| :param content: The text to wrap and decorate. |
| :param width: |
| The number of columns to use, including for the decoration |
| itself. The default (None) uses the available width of the |
| current terminal, or a fallback of 72 lines. A negative number |
| subtracts a fixed-width from the default size. The default obeys |
| the COLUMNS environment variable, if set. |
| :param name: A label to apply to the upper-left of the box. |
| :param padding: How many columns of padding to apply inside. |
| :param upper_left: Upper-left single-width text decoration character. |
| :param lower_left: Lower-left single-width text decoration character. |
| :param horizontal: Horizontal single-width text decoration character. |
| :param vertical: Vertical single-width text decoration character. |
| """ |
| if width is None or width < 0: |
| avail = shutil.get_terminal_size(fallback=(72, 24))[0] |
| if width is None: |
| _width = avail |
| else: |
| _width = avail + width |
| else: |
| _width = width |
| |
| prefix = vertical + (' ' * padding) |
| |
| def _bar(name: Optional[str], top: bool = True) -> str: |
| ret = upper_left if top else lower_left |
| if name is not None: |
| ret += f"{horizontal} {name} " |
| |
| filler_len = _width - len(ret) |
| ret += f"{horizontal * filler_len}" |
| return ret |
| |
| def _wrap(line: str) -> str: |
| return os.linesep.join( |
| textwrap.wrap( |
| line, width=_width - padding, initial_indent=prefix, |
| subsequent_indent=prefix, replace_whitespace=False, |
| drop_whitespace=True, break_on_hyphens=False) |
| ) |
| |
| return os.linesep.join(( |
| _bar(name, top=True), |
| os.linesep.join(_wrap(line) for line in content.splitlines()), |
| _bar(None, top=False), |
| )) |
| |
| |
| class VerboseProcessError(CalledProcessError): |
| """ |
| The same as CalledProcessError, but more verbose. |
| |
| This is useful for debugging failed calls during test executions. |
| The return code, signal (if any), and terminal output will be displayed |
| on unhandled exceptions. |
| """ |
| def summary(self) -> str: |
| """Return the normal CalledProcessError str() output.""" |
| return super().__str__() |
| |
| def __str__(self) -> str: |
| lmargin = ' ' |
| width = -len(lmargin) |
| sections = [] |
| |
| # Does self.stdout contain both stdout and stderr? |
| has_combined_output = self.stderr is None |
| |
| name = 'output' if has_combined_output else 'stdout' |
| if self.stdout: |
| sections.append(add_visual_margin(self.stdout, width, name)) |
| else: |
| sections.append(f"{name}: N/A") |
| |
| if self.stderr: |
| sections.append(add_visual_margin(self.stderr, width, 'stderr')) |
| elif not has_combined_output: |
| sections.append("stderr: N/A") |
| |
| return os.linesep.join(( |
| self.summary(), |
| textwrap.indent(os.linesep.join(sections), prefix=lmargin), |
| )) |