Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a helper for saving command output in a file #3286

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions tests/unit/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -1789,3 +1789,68 @@ def test_create_link_relation(self, mock_config_tree, mock_add_simple_link) -> N
# Load the test object again with the link present
test = tmt.Tree(logger=self.logger, path=self.tmp).tests(names=['tmp/test'])[0]
assert test.link.get('verifies')[0].target == 'https://issues.redhat.com/browse/TT-262'


def test_render_command_report_output():
delimiter = (tmt.utils.OUTPUT_WIDTH - 2) * '~'

assert '\n'.join(tmt.utils.render_command_report(
label='foo',
command=ShellScript('/bar/baz'),
output=tmt.utils.CommandOutput(
stdout='This is some stdout',
stderr='This is some stderr'
)
)) == f"""## foo
# /bar/baz
# stdout (1 lines)
# {delimiter}
This is some stdout
# {delimiter}
# stderr (1 lines)
# {delimiter}
This is some stderr
# {delimiter}
"""


def test_render_command_report_exception():
delimiter = (tmt.utils.OUTPUT_WIDTH - 2) * '~'

assert '\n'.join(tmt.utils.render_command_report(
label='foo',
command=ShellScript('/bar/baz'),
exc=tmt.utils.RunError(
'foo failed',
ShellScript('/bar/baz').to_shell_command(),
1,
stdout='This is some stdout',
stderr='This is some stderr'
)
)) == f"""## foo
# /bar/baz
# stdout (1 lines)
# {delimiter}
This is some stdout
# {delimiter}
# stderr (1 lines)
# {delimiter}
This is some stderr
# {delimiter}
"""


def test_render_command_report_minimal():
print(list(tmt.utils.render_command_report(
label='foo'
)))
assert '\n'.join(tmt.utils.render_command_report(
label='foo'
)) == """## foo
"""
20 changes: 4 additions & 16 deletions tmt/checks/avc.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
Path,
ShellScript,
format_timestamp,
render_run_exception_streams,
render_command_report,
)

if TYPE_CHECKING:
Expand Down Expand Up @@ -117,21 +117,13 @@ def _output_logger(
def _report_success(label: str, output: tmt.utils.CommandOutput) -> list[str]:
""" Format successful command output for the report """

return [
f'# {label}',
output.stdout or '',
''
]
return list(render_command_report(label=label, output=output))


def _report_failure(label: str, exc: tmt.utils.RunError) -> list[str]:
""" Format failed command output for the report """

return [
f'# {label}',
"\n".join(render_run_exception_streams(exc.stdout, exc.stderr, verbose=1)),
''
]
return list(render_command_report(label=label, exc=exc))


def create_ausearch_timestamp(
Expand Down Expand Up @@ -243,11 +235,7 @@ def create_final_report(
got_ausearch = True
got_denials = True

report += [
'# ausearch',
"\n".join(render_run_exception_streams(output.stdout, output.stderr, verbose=1)),
''
]
report += list(render_command_report(label='ausearch', output=output))

else:
if exc.returncode == 1 and exc.stderr and '<no matches>' in exc.stderr.strip():
Expand Down
2 changes: 1 addition & 1 deletion tmt/checks/dmesg.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ def _save_dmesg(

except tmt.utils.RunError as exc:
outcome = ResultOutcome.ERROR
output = "\n".join(render_run_exception_streams(exc.stdout, exc.stderr, verbose=1))
output = "\n".join(render_run_exception_streams(exc.output, verbose=1))

else:
outcome = ResultOutcome.PASS
Expand Down
4 changes: 2 additions & 2 deletions tmt/checks/watchdog.py
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,7 @@ def _handle_output(ping_output: str) -> None:
_handle_output(exc.stdout or '')

else:
_handle_output('\n'.join(render_run_exception_streams(exc.stdout, exc.stderr)))
_handle_output('\n'.join(render_run_exception_streams(exc.output)))

def do_ssh_ping(
self,
Expand Down Expand Up @@ -337,7 +337,7 @@ def _success(ncat_output: str) -> None:
_fail_connection_refused(exc.stderr or '')

else:
_fail_unknown('\n'.join(render_run_exception_streams(exc.stdout, exc.stderr)))
_fail_unknown('\n'.join(render_run_exception_streams(exc.output)))

logger.debug(
f'failed {guest_context.ssh_ping_failures}'
Expand Down
120 changes: 106 additions & 14 deletions tmt/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2289,6 +2289,21 @@ def __init__(
# `finish`), save a logger for later.
self.logger = caller._logger if isinstance(caller, Common) else None

@functools.cached_property
def output(self) -> CommandOutput:
"""
Captured output of the command.
.. note::
This field contains basically the same info as :py:attr:`stdout`
and :py:attr:`stderr`, but it's bundled into a single object.
This is how command output is passed between functions, therefore
the exception should offer it as well.
"""

return CommandOutput(self.stdout, self.stderr)


class MetadataError(GeneralError):
""" General metadata error """
Expand Down Expand Up @@ -2446,28 +2461,105 @@ def from_env(cls) -> 'TracebackVerbosity':


def render_run_exception_streams(
stdout: Optional[str],
stderr: Optional[str],
verbose: int = 0) -> Iterator[str]:
output: CommandOutput,
verbose: int = 0,
comment_sign: str = '#') -> Iterator[str]:
""" Render run exception output streams for printing """

for name, output in (('stdout', stdout), ('stderr', stderr)):
if not output:
for name, content in (('stdout', output.stdout), ('stderr', output.stderr)):
if not content:
continue
output_lines = output.strip().split('\n')
content_lines = content.strip().split('\n')
# Show all lines in verbose mode, limit to maximum otherwise
if verbose > 0:
line_summary = f"{len(output_lines)}"
line_summary = f"{len(content_lines)}"
else:
line_summary = f"{min(len(output_lines), OUTPUT_LINES)}/{len(output_lines)}"
output_lines = output_lines[-OUTPUT_LINES:]
line_summary = f"{min(len(content_lines), OUTPUT_LINES)}/{len(content_lines)}"
content_lines = content_lines[-OUTPUT_LINES:]

line_intro = f'{comment_sign} '

yield line_intro + f'{name} ({line_summary} lines)'
yield line_intro + (OUTPUT_WIDTH - 2) * '~'
yield from content_lines
yield line_intro + (OUTPUT_WIDTH - 2) * '~'
yield ''


@overload
def render_command_report(
*,
label: str,
command: Optional[Union[ShellScript, Command]] = None,
output: CommandOutput,
exc: None = None) -> Iterator[str]:
pass


@overload
def render_command_report(
*,
label: str,
command: Optional[Union[ShellScript, Command]] = None,
output: None = None,
exc: RunError) -> Iterator[str]:
pass


def render_command_report(
*,
label: str,
command: Optional[Union[ShellScript, Command]] = None,
output: Optional[CommandOutput] = None,
exc: Optional[RunError] = None,
comment_sign: str = '#') -> Iterator[str]:
"""
Format a command output for a report file.
To provide unified look of various files reporting command outputs,
this helper would combine its arguments and emit lines the caller
may then write to a file. The following template is used:
.. code-block::
yield f'{name} ({line_summary} lines)'
yield OUTPUT_WIDTH * '~'
yield from output_lines
yield OUTPUT_WIDTH * '~'
## ${label}
# ${command}
# stdout (N lines)
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Here's a couple more in the docstring.

...
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# stderr (N lines)
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
...
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
:param label: a string describing the intent of the command. It is
useful for user who reads the report file eventually.
:param command: command that was executed.
:param output: if set, it contains output of the command. It has
higher priority than ``exc``.
:param exc: if set, it represents a failed command, and input stored
in it is rendered.
:param comment_sign: a character to mark lines with comments that
document the report.
"""

yield f'{comment_sign}{comment_sign} {label}'
yield ''

if command:
yield f'{comment_sign} {command.to_element()}'
yield ''

if output is not None:
yield from render_run_exception_streams(output, verbose=1)

elif exc is not None:
yield from render_run_exception_streams(exc.output, verbose=1)


def render_run_exception(exception: RunError) -> Iterator[str]:
""" Render detailed output upon command execution errors for printing """
Expand All @@ -2480,7 +2572,7 @@ def render_run_exception(exception: RunError) -> Iterator[str]:
else:
verbose = 0

yield from render_run_exception_streams(exception.stdout, exception.stderr, verbose=verbose)
yield from render_run_exception_streams(exception.output, verbose=verbose)


def render_exception_stack(
Expand Down
Loading