python/utils: add VerboseProcessError

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>
This commit is contained in:
John Snow 2022-03-21 16:16:02 -04:00 committed by Hanna Reitz
parent be73231ba8
commit 062fd1dad2

View File

@ -18,6 +18,7 @@ various tasks not directly related to the launching of a VM.
import os import os
import re import re
import shutil import shutil
from subprocess import CalledProcessError
import textwrap import textwrap
from typing import Optional from typing import Optional
@ -26,6 +27,7 @@ from .accel import kvm_available, list_accel, tcg_available
__all__ = ( __all__ = (
'VerboseProcessError',
'add_visual_margin', 'add_visual_margin',
'get_info_usernet_hostfwd_port', 'get_info_usernet_hostfwd_port',
'kvm_available', 'kvm_available',
@ -121,3 +123,40 @@ def add_visual_margin(
os.linesep.join(_wrap(line) for line in content.splitlines()), os.linesep.join(_wrap(line) for line in content.splitlines()),
_bar(None, top=False), _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),
))