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, anchor):
172 """Initialize a new object.
175 log: The Logfile object to log to.
176 marker: The name of the nested log section.
177 anchor: The anchor value to pass to start_section().
188 self.anchor = self.log.start_section(self.marker, self.anchor)
190 def __exit__(self, extype, value, traceback):
191 self.log.end_section(self.marker)
193 class Logfile(object):
194 """Generates an HTML-formatted log file containing multiple streams of
195 data, each represented in a well-delineated/-structured fashion."""
197 def __init__(self, fn):
198 """Initialize a new object.
201 fn: The filename to write to.
207 self.f = open(fn, 'wt')
208 self.last_stream = None
213 shutil.copy(mod_dir + '/multiplexed_log.css', os.path.dirname(fn))
217 <link rel="stylesheet" type="text/css" href="multiplexed_log.css">
218 <script src="http://code.jquery.com/jquery.min.js"></script>
220 $(document).ready(function () {
221 // Copy status report HTML to start of log for easy access
222 sts = $(".block#status_report")[0].outerHTML;
223 $("tt").prepend(sts);
225 // Add expand/contract buttons to all block headers
226 btns = "<span class=\\\"block-expand hidden\\\">[+] </span>" +
227 "<span class=\\\"block-contract\\\">[-] </span>";
228 $(".block-header").prepend(btns);
230 // Pre-contract all blocks which passed, leaving only problem cases
231 // expanded, to highlight issues the user should look at.
232 // Only top-level blocks (sections) should have any status
233 passed_bcs = $(".block-content:has(.status-pass)");
234 // Some blocks might have multiple status entries (e.g. the status
235 // report), so take care not to hide blocks with partial success.
236 passed_bcs = passed_bcs.not(":has(.status-fail)");
237 passed_bcs = passed_bcs.not(":has(.status-xfail)");
238 passed_bcs = passed_bcs.not(":has(.status-xpass)");
239 passed_bcs = passed_bcs.not(":has(.status-skipped)");
240 // Hide the passed blocks
241 passed_bcs.addClass("hidden");
242 // Flip the expand/contract button hiding for those blocks.
243 bhs = passed_bcs.parent().children(".block-header")
244 bhs.children(".block-expand").removeClass("hidden");
245 bhs.children(".block-contract").addClass("hidden");
247 // Add click handler to block headers.
248 // The handler expands/contracts the block.
249 $(".block-header").on("click", function (e) {
250 var header = $(this);
251 var content = header.next(".block-content");
252 var expanded = !content.hasClass("hidden");
254 content.addClass("hidden");
255 header.children(".block-expand").first().removeClass("hidden");
256 header.children(".block-contract").first().addClass("hidden");
258 header.children(".block-contract").first().removeClass("hidden");
259 header.children(".block-expand").first().addClass("hidden");
260 content.removeClass("hidden");
264 // When clicking on a link, expand the target block
265 $("a").on("click", function (e) {
266 var block = $($(this).attr("href"));
267 var header = block.children(".block-header");
268 var content = block.children(".block-content").first();
269 header.children(".block-contract").first().removeClass("hidden");
270 header.children(".block-expand").first().addClass("hidden");
271 content.removeClass("hidden");
281 """Close the log file.
283 After calling this function, no more data may be written to the log.
299 # The set of characters that should be represented as hexadecimal codes in
301 _nonprint = ('%' + ''.join(chr(c) for c in range(0, 32) if c not in (9, 10)) +
302 ''.join(chr(c) for c in range(127, 256)))
304 def _escape(self, data):
305 """Render data format suitable for inclusion in an HTML document.
307 This includes HTML-escaping certain characters, and translating
308 control characters to a hexadecimal representation.
311 data: The raw string data to be escaped.
314 An escaped version of the data.
317 data = data.replace(chr(13), '')
318 data = ''.join((c in self._nonprint) and ('%%%02x' % ord(c)) or
320 data = cgi.escape(data)
323 def _terminate_stream(self):
324 """Write HTML to the log file to terminate the current stream's data.
334 if not self.last_stream:
336 self.f.write('</pre>\n')
337 self.f.write('<div class="stream-trailer block-trailer">End stream: ' +
338 self.last_stream.name + '</div>\n')
339 self.f.write('</div>\n')
340 self.f.write('</div>\n')
341 self.last_stream = None
343 def _note(self, note_type, msg, anchor=None):
344 """Write a note or one-off message to the log file.
347 note_type: The type of note. This must be a value supported by the
348 accompanying multiplexed_log.css.
349 msg: The note/message to log.
350 anchor: Optional internal link target.
356 self._terminate_stream()
357 self.f.write('<div class="' + note_type + '">\n')
359 self.f.write('<a href="#%s">\n' % anchor)
360 self.f.write('<pre>')
361 self.f.write(self._escape(msg))
362 self.f.write('\n</pre>\n')
364 self.f.write('</a>\n')
365 self.f.write('</div>\n')
367 def start_section(self, marker, anchor=None):
368 """Begin a new nested section in the log file.
371 marker: The name of the section that is starting.
372 anchor: The value to use for the anchor. If None, a unique value
373 will be calculated and used
376 Name of the HTML anchor emitted before section.
379 self._terminate_stream()
380 self.blocks.append(marker)
383 anchor = str(self.anchor)
384 blk_path = '/'.join(self.blocks)
385 self.f.write('<div class="section block" id="' + anchor + '">\n')
386 self.f.write('<div class="section-header block-header">Section: ' +
387 blk_path + '</div>\n')
388 self.f.write('<div class="section-content block-content">\n')
392 def end_section(self, marker):
393 """Terminate the current nested section in the log file.
395 This function validates proper nesting of start_section() and
396 end_section() calls. If a mismatch is found, an exception is raised.
399 marker: The name of the section that is ending.
405 if (not self.blocks) or (marker != self.blocks[-1]):
406 raise Exception('Block nesting mismatch: "%s" "%s"' %
407 (marker, '/'.join(self.blocks)))
408 self._terminate_stream()
409 blk_path = '/'.join(self.blocks)
410 self.f.write('<div class="section-trailer block-trailer">' +
411 'End section: ' + blk_path + '</div>\n')
412 self.f.write('</div>\n')
413 self.f.write('</div>\n')
416 def section(self, marker, anchor=None):
417 """Create a temporary section in the log file.
419 This function creates a context manager for Python's "with" statement,
420 which allows a certain portion of test code to be logged to a separate
421 section of the log file.
424 with log.section("somename"):
428 marker: The name of the nested section.
429 anchor: The anchor value to pass to start_section().
432 A context manager object.
435 return SectionCtxMgr(self, marker, anchor)
437 def error(self, msg):
438 """Write an error note to the log file.
441 msg: A message describing the error.
447 self._note("error", msg)
449 def warning(self, msg):
450 """Write an warning note to the log file.
453 msg: A message describing the warning.
459 self._note("warning", msg)
462 """Write an informational note to the log file.
465 msg: An informational message.
471 self._note("info", msg)
473 def action(self, msg):
474 """Write an action note to the log file.
477 msg: A message describing the action that is being logged.
483 self._note("action", msg)
485 def status_pass(self, msg, anchor=None):
486 """Write a note to the log file describing test(s) which passed.
489 msg: A message describing the passed test(s).
490 anchor: Optional internal link target.
496 self._note("status-pass", msg, anchor)
498 def status_skipped(self, msg, anchor=None):
499 """Write a note to the log file describing skipped test(s).
502 msg: A message describing the skipped test(s).
503 anchor: Optional internal link target.
509 self._note("status-skipped", msg, anchor)
511 def status_xfail(self, msg, anchor=None):
512 """Write a note to the log file describing xfailed test(s).
515 msg: A message describing the xfailed test(s).
516 anchor: Optional internal link target.
522 self._note("status-xfail", msg, anchor)
524 def status_xpass(self, msg, anchor=None):
525 """Write a note to the log file describing xpassed test(s).
528 msg: A message describing the xpassed test(s).
529 anchor: Optional internal link target.
535 self._note("status-xpass", msg, anchor)
537 def status_fail(self, msg, anchor=None):
538 """Write a note to the log file describing failed test(s).
541 msg: A message describing the failed test(s).
542 anchor: Optional internal link target.
548 self._note("status-fail", msg, anchor)
550 def get_stream(self, name, chained_file=None):
551 """Create an object to log a single stream's data into the log file.
553 This creates a "file-like" object that can be written to in order to
554 write a single stream's data to the log file. The implementation will
555 handle any required interleaving of data (from multiple streams) in
556 the log, in a way that makes it obvious which stream each bit of data
560 name: The name of the stream.
561 chained_file: The file-like object to which all stream data should
562 be logged to in addition to this log. Can be None.
568 return LogfileStream(self, name, chained_file)
570 def get_runner(self, name, chained_file=None):
571 """Create an object that executes processes and logs their output.
574 name: The name of this sub-process.
575 chained_file: The file-like object to which all stream data should
576 be logged to in addition to logfile. Can be None.
582 return RunAndLog(self, name, chained_file)
584 def write(self, stream, data, implicit=False):
585 """Write stream data into the log file.
587 This function should only be used by instances of LogfileStream or
591 stream: The stream whose data is being logged.
592 data: The data to log.
593 implicit: Boolean indicating whether data actually appeared in the
594 stream, or was implicitly generated. A valid use-case is to
595 repeat a shell prompt at the start of each separate log
596 section, which makes the log sections more readable in
603 if stream != self.last_stream:
604 self._terminate_stream()
605 self.f.write('<div class="stream block">\n')
606 self.f.write('<div class="stream-header block-header">Stream: ' +
607 stream.name + '</div>\n')
608 self.f.write('<div class="stream-content block-content">\n')
609 self.f.write('<pre>')
611 self.f.write('<span class="implicit">')
612 self.f.write(self._escape(data))
614 self.f.write('</span>')
615 self.last_stream = stream
618 """Flush the log stream, to ensure correct log interleaving.