Signed-off-by: Daniel P. Berrangé <berrange@redhat.com> Message-Id: <20220707163720.1421716-5-berrange@redhat.com> Reviewed-by: Peter Maydell <peter.maydell@linaro.org> Signed-off-by: Thomas Huth <thuth@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 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),
 | 
						|
        ))
 |