xref: /qemu/python/qemu/utils/__init__.py (revision b2a3cbb8)
1"""
2QEMU development and testing utilities
3
4This package provides a small handful of utilities for performing
5various tasks not directly related to the launching of a VM.
6"""
7
8# Copyright (C) 2021 Red Hat Inc.
9#
10# Authors:
11#  John Snow <jsnow@redhat.com>
12#  Cleber Rosa <crosa@redhat.com>
13#
14# This work is licensed under the terms of the GNU GPL, version 2.  See
15# the COPYING file in the top-level directory.
16#
17
18import os
19import re
20import shutil
21from subprocess import CalledProcessError
22import textwrap
23from typing import Optional
24
25# pylint: disable=import-error
26from .accel import kvm_available, list_accel, tcg_available
27
28
29__all__ = (
30    'VerboseProcessError',
31    'add_visual_margin',
32    'get_info_usernet_hostfwd_port',
33    'kvm_available',
34    'list_accel',
35    'tcg_available',
36)
37
38
39def get_info_usernet_hostfwd_port(info_usernet_output: str) -> Optional[int]:
40    """
41    Returns the port given to the hostfwd parameter via info usernet
42
43    :param info_usernet_output: output generated by hmp command "info usernet"
44    :return: the port number allocated by the hostfwd option
45    """
46    for line in info_usernet_output.split('\r\n'):
47        regex = r'TCP.HOST_FORWARD.*127\.0\.0\.1\s+(\d+)\s+10\.'
48        match = re.search(regex, line)
49        if match is not None:
50            return int(match[1])
51    return None
52
53
54# pylint: disable=too-many-arguments
55def add_visual_margin(
56        content: str = '',
57        width: Optional[int] = None,
58        name: Optional[str] = None,
59        padding: int = 1,
60        upper_left: str = '┏',
61        lower_left: str = '┗',
62        horizontal: str = '━',
63        vertical: str = '┃',
64) -> str:
65    """
66    Decorate and wrap some text with a visual decoration around it.
67
68    This function assumes that the text decoration characters are single
69    characters that display using a single monospace column.
70
71    ┏━ Example ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
72    ┃ This is what this function looks like with text content that's
73    ┃ wrapped to 66 characters. The right-hand margin is left open to
74    ┃ accommodate the occasional unicode character that might make
75    ┃ predicting the total "visual" width of a line difficult. This
76    ┃ provides a visual distinction that's good-enough, though.
77    ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
78
79    :param content: The text to wrap and decorate.
80    :param width:
81        The number of columns to use, including for the decoration
82        itself. The default (None) uses the available width of the
83        current terminal, or a fallback of 72 lines. A negative number
84        subtracts a fixed-width from the default size. The default obeys
85        the COLUMNS environment variable, if set.
86    :param name: A label to apply to the upper-left of the box.
87    :param padding: How many columns of padding to apply inside.
88    :param upper_left: Upper-left single-width text decoration character.
89    :param lower_left: Lower-left single-width text decoration character.
90    :param horizontal: Horizontal single-width text decoration character.
91    :param vertical: Vertical single-width text decoration character.
92    """
93    if width is None or width < 0:
94        avail = shutil.get_terminal_size(fallback=(72, 24))[0]
95        if width is None:
96            _width = avail
97        else:
98            _width = avail + width
99    else:
100        _width = width
101
102    prefix = vertical + (' ' * padding)
103
104    def _bar(name: Optional[str], top: bool = True) -> str:
105        ret = upper_left if top else lower_left
106        if name is not None:
107            ret += f"{horizontal} {name} "
108
109        filler_len = _width - len(ret)
110        ret += f"{horizontal * filler_len}"
111        return ret
112
113    def _wrap(line: str) -> str:
114        return os.linesep.join(
115            textwrap.wrap(
116                line, width=_width - padding, initial_indent=prefix,
117                subsequent_indent=prefix, replace_whitespace=False,
118                drop_whitespace=True, break_on_hyphens=False)
119        )
120
121    return os.linesep.join((
122        _bar(name, top=True),
123        os.linesep.join(_wrap(line) for line in content.splitlines()),
124        _bar(None, top=False),
125    ))
126
127
128class VerboseProcessError(CalledProcessError):
129    """
130    The same as CalledProcessError, but more verbose.
131
132    This is useful for debugging failed calls during test executions.
133    The return code, signal (if any), and terminal output will be displayed
134    on unhandled exceptions.
135    """
136    def summary(self) -> str:
137        """Return the normal CalledProcessError str() output."""
138        return super().__str__()
139
140    def __str__(self) -> str:
141        lmargin = '  '
142        width = -len(lmargin)
143        sections = []
144
145        # Does self.stdout contain both stdout and stderr?
146        has_combined_output = self.stderr is None
147
148        name = 'output' if has_combined_output else 'stdout'
149        if self.stdout:
150            sections.append(add_visual_margin(self.stdout, width, name))
151        else:
152            sections.append(f"{name}: N/A")
153
154        if self.stderr:
155            sections.append(add_visual_margin(self.stderr, width, 'stderr'))
156        elif not has_combined_output:
157            sections.append("stderr: N/A")
158
159        return os.linesep.join((
160            self.summary(),
161            textwrap.indent(os.linesep.join(sections), prefix=lmargin),
162        ))
163