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