# SPDX-License-Identifier: GPL-2.0 # Copyright (c) 2015 Stephen Warren # Copyright (c) 2015-2016, NVIDIA CORPORATION. All rights reserved. """ Generate an HTML-formatted log file containing multiple streams of data, each represented in a well-delineated/-structured fashion. """ import datetime import html import os.path import shutil import subprocess mod_dir = os.path.dirname(os.path.abspath(__file__)) class LogfileStream(object): """A file-like object used to write a single logical stream of data into a multiplexed log file. Objects of this type should be created by factory functions in the Logfile class rather than directly.""" def __init__(self, logfile, name, chained_file): """Initialize a new object. Args: logfile: The Logfile object to log to. name: The name of this log stream. chained_file: The file-like object to which all stream data should be logged to in addition to logfile. Can be None. Returns: Nothing. """ self.logfile = logfile self.name = name self.chained_file = chained_file def close(self): """Dummy function so that this class is "file-like". Args: None. Returns: Nothing. """ pass def write(self, data, implicit=False): """Write data to the log stream. Args: data: The data to write to the file. implicit: Boolean indicating whether data actually appeared in the stream, or was implicitly generated. A valid use-case is to repeat a shell prompt at the start of each separate log section, which makes the log sections more readable in isolation. Returns: Nothing. """ self.logfile.write(self, data, implicit) if self.chained_file: # Chained file is console, convert things a little self.chained_file.write((data.encode('ascii', 'replace')).decode()) def flush(self): """Flush the log stream, to ensure correct log interleaving. Args: None. Returns: Nothing. """ self.logfile.flush() if self.chained_file: self.chained_file.flush() class RunAndLog(object): """A utility object used to execute sub-processes and log their output to a multiplexed log file. Objects of this type should be created by factory functions in the Logfile class rather than directly.""" def __init__(self, logfile, name, chained_file): """Initialize a new object. Args: logfile: The Logfile object to log to. name: The name of this log stream or sub-process. chained_file: The file-like object to which all stream data should be logged to in addition to logfile. Can be None. Returns: Nothing. """ self.logfile = logfile self.name = name self.chained_file = chained_file self.output = None self.exit_status = None def close(self): """Clean up any resources managed by this object.""" pass def run(self, cmd, cwd=None, ignore_errors=False): """Run a command as a sub-process, and log the results. The output is available at self.output which can be useful if there is an exception. Args: cmd: The command to execute. cwd: The directory to run the command in. Can be None to use the current directory. ignore_errors: Indicate whether to ignore errors. If True, the function will simply return if the command cannot be executed or exits with an error code, otherwise an exception will be raised if such problems occur. Returns: The output as a string. """ msg = '+' + ' '.join(cmd) + '\n' if self.chained_file: self.chained_file.write(msg) self.logfile.write(self, msg) try: p = subprocess.Popen(cmd, cwd=cwd, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) (stdout, stderr) = p.communicate() if stdout is not None: stdout = stdout.decode('utf-8') if stderr is not None: stderr = stderr.decode('utf-8') output = '' if stdout: if stderr: output += 'stdout:\n' output += stdout if stderr: if stdout: output += 'stderr:\n' output += stderr exit_status = p.returncode exception = None except subprocess.CalledProcessError as cpe: output = cpe.output exit_status = cpe.returncode exception = cpe except Exception as e: output = '' exit_status = 0 exception = e if output and not output.endswith('\n'): output += '\n' if exit_status and not exception and not ignore_errors: exception = Exception('Exit code: ' + str(exit_status)) if exception: output += str(exception) + '\n' self.logfile.write(self, output) if self.chained_file: self.chained_file.write(output) self.logfile.timestamp() # Store the output so it can be accessed if we raise an exception. self.output = output self.exit_status = exit_status if exception: raise exception return output class SectionCtxMgr: """A context manager for Python's "with" statement, which allows a certain portion of test code to be logged to a separate section of the log file. Objects of this type should be created by factory functions in the Logfile class rather than directly.""" def __init__(self, log, marker, anchor): """Initialize a new object. Args: log: The Logfile object to log to. marker: The name of the nested log section. anchor: The anchor value to pass to start_section(). Returns: Nothing. """ self.log = log self.marker = marker self.anchor = anchor def __enter__(self): self.anchor = self.log.start_section(self.marker, self.anchor) def __exit__(self, extype, value, traceback): self.log.end_section(self.marker) class Logfile: """Generates an HTML-formatted log file containing multiple streams of data, each represented in a well-delineated/-structured fashion.""" def __init__(self, fn): """Initialize a new object. Args: fn: The filename to write to. Returns: Nothing. """ self.f = open(fn, 'wt', encoding='utf-8') self.last_stream = None self.blocks = [] self.cur_evt = 1 self.anchor = 0 self.timestamp_start = self._get_time() self.timestamp_prev = self.timestamp_start self.timestamp_blocks = [] self.seen_warning = False shutil.copy(mod_dir + '/multiplexed_log.css', os.path.dirname(fn)) self.f.write('''\
''') def close(self): """Close the log file. After calling this function, no more data may be written to the log. Args: None. Returns: Nothing. """ self.f.write('''\ ''') self.f.close() # The set of characters that should be represented as hexadecimal codes in # the log file. _nonprint = {ord('%')} _nonprint.update(c for c in range(0, 32) if c not in (9, 10)) _nonprint.update(range(127, 256)) def _escape(self, data): """Render data format suitable for inclusion in an HTML document. This includes HTML-escaping certain characters, and translating control characters to a hexadecimal representation. Args: data: The raw string data to be escaped. Returns: An escaped version of the data. """ data = data.replace(chr(13), '') data = ''.join((ord(c) in self._nonprint) and ('%%%02x' % ord(c)) or c for c in data) data = html.escape(data) return data def _terminate_stream(self): """Write HTML to the log file to terminate the current stream's data. Args: None. Returns: Nothing. """ self.cur_evt += 1 if not self.last_stream: return self.f.write('\n') self.f.write('') if anchor: self.f.write('' % anchor) self.f.write(self._escape(msg)) if anchor: self.f.write('') self.f.write('\n\n') self.f.write('
')
if implicit:
self.f.write('')
self.f.write(self._escape(data))
if implicit:
self.f.write('')
self.last_stream = stream
def flush(self):
"""Flush the log stream, to ensure correct log interleaving.
Args:
None.
Returns:
Nothing.
"""
self.f.flush()