# Copyright (c) 2015 Stephen Warren # Copyright (c) 2015-2016, NVIDIA CORPORATION. All rights reserved. # # SPDX-License-Identifier: GPL-2.0 # Generate an HTML-formatted log file containing multiple streams of data, # each represented in a well-delineated/-structured fashion. import cgi 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 tot he 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: self.chained_file.write(data) 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 def close(self): '''Clean up any resources managed by this object.''' pass def run(self, cmd, cwd=None): '''Run a command as a sub-process, and log the results. Args: cmd: The command to execute. cwd: The directory to run the command in. Can be None to use the current directory. Returns: Nothing. ''' 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() 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: 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) if exception: raise exception class SectionCtxMgr(object): '''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): '''Initialize a new object. Args: log: The Logfile object to log to. marker: The name of the nested log section. Returns: Nothing. ''' self.log = log self.marker = marker def __enter__(self): self.log.start_section(self.marker) def __exit__(self, extype, value, traceback): self.log.end_section(self.marker) class Logfile(object): '''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") self.last_stream = None self.blocks = [] self.cur_evt = 1 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 = ("%" + "".join(chr(c) for c in range(0, 32) if c not in (9, 10)) + "".join(chr(c) for c in 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((c in self._nonprint) and ("%%%02x" % ord(c)) or c for c in data) data = cgi.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("") self.f.write(self._escape(msg)) self.f.write("\n
")
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()