062fd1dad2
This adds an Exception that extends the Python stdlib subprocess.CalledProcessError. The difference is that the str() method of this exception also adds the stdout/stderr logs. In effect, if this exception goes unhandled, Python will print the output in a visually distinct wrapper to the terminal so that it's easy to spot in a sea of traceback information. Signed-off-by: John Snow <jsnow@redhat.com> Reviewed-by: Eric Blake <eblake@redhat.com> Reviewed-by: Hanna Reitz <hreitz@redhat.com> Message-Id: <20220321201618.903471-3-jsnow@redhat.com> Signed-off-by: Hanna Reitz <hreitz@redhat.com>
163 lines
5.5 KiB
Python
163 lines
5.5 KiB
Python
"""
|
|
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 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),
|
|
))
|