1 # Copyright (c) 2015 Stephen Warren
2 # Copyright (c) 2015-2016, NVIDIA CORPORATION. All rights reserved.
4 # SPDX-License-Identifier: GPL-2.0
6 # Generate an HTML-formatted log file containing multiple streams of data,
7 # each represented in a well-delineated/-structured fashion.
14 mod_dir = os.path.dirname(os.path.abspath(__file__))
16 class LogfileStream(object):
17 """A file-like object used to write a single logical stream of data into
18 a multiplexed log file. Objects of this type should be created by factory
19 functions in the Logfile class rather than directly."""
21 def __init__(self, logfile, name, chained_file):
22 """Initialize a new object.
25 logfile: The Logfile object to log to.
26 name: The name of this log stream.
27 chained_file: The file-like object to which all stream data should be
28 logged to in addition to logfile. Can be None.
34 self.logfile = logfile
36 self.chained_file = chained_file
39 """Dummy function so that this class is "file-like".
50 def write(self, data, implicit=False):
51 """Write data to the log stream.
54 data: The data to write tot he file.
55 implicit: Boolean indicating whether data actually appeared in the
56 stream, or was implicitly generated. A valid use-case is to
57 repeat a shell prompt at the start of each separate log
58 section, which makes the log sections more readable in
65 self.logfile.write(self, data, implicit)
67 self.chained_file.write(data)
70 """Flush the log stream, to ensure correct log interleaving.
81 self.chained_file.flush()
83 class RunAndLog(object):
84 """A utility object used to execute sub-processes and log their output to
85 a multiplexed log file. Objects of this type should be created by factory
86 functions in the Logfile class rather than directly."""
88 def __init__(self, logfile, name, chained_file):
89 """Initialize a new object.
92 logfile: The Logfile object to log to.
93 name: The name of this log stream or sub-process.
94 chained_file: The file-like object to which all stream data should
95 be logged to in addition to logfile. Can be None.
101 self.logfile = logfile
103 self.chained_file = chained_file
106 """Clean up any resources managed by this object."""
109 def run(self, cmd, cwd=None, ignore_errors=False):
110 """Run a command as a sub-process, and log the results.
113 cmd: The command to execute.
114 cwd: The directory to run the command in. Can be None to use the
116 ignore_errors: Indicate whether to ignore errors. If True, the
117 function will simply return if the command cannot be executed
118 or exits with an error code, otherwise an exception will be
119 raised if such problems occur.
125 msg = "+" + " ".join(cmd) + "\n"
126 if self.chained_file:
127 self.chained_file.write(msg)
128 self.logfile.write(self, msg)
131 p = subprocess.Popen(cmd, cwd=cwd,
132 stdin=None, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
133 (stdout, stderr) = p.communicate()
137 output += 'stdout:\n'
141 output += 'stderr:\n'
143 exit_status = p.returncode
145 except subprocess.CalledProcessError as cpe:
147 exit_status = cpe.returncode
149 except Exception as e:
153 if output and not output.endswith('\n'):
155 if exit_status and not exception and not ignore_errors:
156 exception = Exception('Exit code: ' + str(exit_status))
158 output += str(exception) + '\n'
159 self.logfile.write(self, output)
160 if self.chained_file:
161 self.chained_file.write(output)
165 class SectionCtxMgr(object):
166 """A context manager for Python's "with" statement, which allows a certain
167 portion of test code to be logged to a separate section of the log file.
168 Objects of this type should be created by factory functions in the Logfile
169 class rather than directly."""
171 def __init__(self, log, marker):
172 """Initialize a new object.
175 log: The Logfile object to log to.
176 marker: The name of the nested log section.
186 self.log.start_section(self.marker)
188 def __exit__(self, extype, value, traceback):
189 self.log.end_section(self.marker)
191 class Logfile(object):
192 """Generates an HTML-formatted log file containing multiple streams of
193 data, each represented in a well-delineated/-structured fashion."""
195 def __init__(self, fn):
196 """Initialize a new object.
199 fn: The filename to write to.
205 self.f = open(fn, "wt")
206 self.last_stream = None
209 shutil.copy(mod_dir + "/multiplexed_log.css", os.path.dirname(fn))
213 <link rel="stylesheet" type="text/css" href="multiplexed_log.css">
220 """Close the log file.
222 After calling this function, no more data may be written to the log.
238 # The set of characters that should be represented as hexadecimal codes in
240 _nonprint = ("%" + "".join(chr(c) for c in range(0, 32) if c not in (9, 10)) +
241 "".join(chr(c) for c in range(127, 256)))
243 def _escape(self, data):
244 """Render data format suitable for inclusion in an HTML document.
246 This includes HTML-escaping certain characters, and translating
247 control characters to a hexadecimal representation.
250 data: The raw string data to be escaped.
253 An escaped version of the data.
256 data = data.replace(chr(13), "")
257 data = "".join((c in self._nonprint) and ("%%%02x" % ord(c)) or
259 data = cgi.escape(data)
262 def _terminate_stream(self):
263 """Write HTML to the log file to terminate the current stream's data.
273 if not self.last_stream:
275 self.f.write("</pre>\n")
276 self.f.write("<div class=\"stream-trailer\" id=\"" +
277 self.last_stream.name + "\">End stream: " +
278 self.last_stream.name + "</div>\n")
279 self.f.write("</div>\n")
280 self.last_stream = None
282 def _note(self, note_type, msg):
283 """Write a note or one-off message to the log file.
286 note_type: The type of note. This must be a value supported by the
287 accompanying multiplexed_log.css.
288 msg: The note/message to log.
294 self._terminate_stream()
295 self.f.write("<div class=\"" + note_type + "\">\n<pre>")
296 self.f.write(self._escape(msg))
297 self.f.write("\n</pre></div>\n")
299 def start_section(self, marker):
300 """Begin a new nested section in the log file.
303 marker: The name of the section that is starting.
309 self._terminate_stream()
310 self.blocks.append(marker)
311 blk_path = "/".join(self.blocks)
312 self.f.write("<div class=\"section\" id=\"" + blk_path + "\">\n")
313 self.f.write("<div class=\"section-header\" id=\"" + blk_path +
314 "\">Section: " + blk_path + "</div>\n")
316 def end_section(self, marker):
317 """Terminate the current nested section in the log file.
319 This function validates proper nesting of start_section() and
320 end_section() calls. If a mismatch is found, an exception is raised.
323 marker: The name of the section that is ending.
329 if (not self.blocks) or (marker != self.blocks[-1]):
330 raise Exception("Block nesting mismatch: \"%s\" \"%s\"" %
331 (marker, "/".join(self.blocks)))
332 self._terminate_stream()
333 blk_path = "/".join(self.blocks)
334 self.f.write("<div class=\"section-trailer\" id=\"section-trailer-" +
335 blk_path + "\">End section: " + blk_path + "</div>\n")
336 self.f.write("</div>\n")
339 def section(self, marker):
340 """Create a temporary section in the log file.
342 This function creates a context manager for Python's "with" statement,
343 which allows a certain portion of test code to be logged to a separate
344 section of the log file.
347 with log.section("somename"):
351 marker: The name of the nested section.
354 A context manager object.
357 return SectionCtxMgr(self, marker)
359 def error(self, msg):
360 """Write an error note to the log file.
363 msg: A message describing the error.
369 self._note("error", msg)
371 def warning(self, msg):
372 """Write an warning note to the log file.
375 msg: A message describing the warning.
381 self._note("warning", msg)
384 """Write an informational note to the log file.
387 msg: An informational message.
393 self._note("info", msg)
395 def action(self, msg):
396 """Write an action note to the log file.
399 msg: A message describing the action that is being logged.
405 self._note("action", msg)
407 def status_pass(self, msg):
408 """Write a note to the log file describing test(s) which passed.
411 msg: A message describing passed test(s).
417 self._note("status-pass", msg)
419 def status_skipped(self, msg):
420 """Write a note to the log file describing skipped test(s).
423 msg: A message describing passed test(s).
429 self._note("status-skipped", msg)
431 def status_fail(self, msg):
432 """Write a note to the log file describing failed test(s).
435 msg: A message describing passed test(s).
441 self._note("status-fail", msg)
443 def get_stream(self, name, chained_file=None):
444 """Create an object to log a single stream's data into the log file.
446 This creates a "file-like" object that can be written to in order to
447 write a single stream's data to the log file. The implementation will
448 handle any required interleaving of data (from multiple streams) in
449 the log, in a way that makes it obvious which stream each bit of data
453 name: The name of the stream.
454 chained_file: The file-like object to which all stream data should
455 be logged to in addition to this log. Can be None.
461 return LogfileStream(self, name, chained_file)
463 def get_runner(self, name, chained_file=None):
464 """Create an object that executes processes and logs their output.
467 name: The name of this sub-process.
468 chained_file: The file-like object to which all stream data should
469 be logged to in addition to logfile. Can be None.
475 return RunAndLog(self, name, chained_file)
477 def write(self, stream, data, implicit=False):
478 """Write stream data into the log file.
480 This function should only be used by instances of LogfileStream or
484 stream: The stream whose data is being logged.
485 data: The data to log.
486 implicit: Boolean indicating whether data actually appeared in the
487 stream, or was implicitly generated. A valid use-case is to
488 repeat a shell prompt at the start of each separate log
489 section, which makes the log sections more readable in
496 if stream != self.last_stream:
497 self._terminate_stream()
498 self.f.write("<div class=\"stream\" id=\"%s\">\n" % stream.name)
499 self.f.write("<div class=\"stream-header\" id=\"" + stream.name +
500 "\">Stream: " + stream.name + "</div>\n")
501 self.f.write("<pre>")
503 self.f.write("<span class=\"implicit\">")
504 self.f.write(self._escape(data))
506 self.f.write("</span>")
507 self.last_stream = stream
510 """Flush the log stream, to ensure correct log interleaving.