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