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
107 """Clean up any resources managed by this object."""
110 def run(self, cmd, cwd=None, ignore_errors=False):
111 """Run a command as a sub-process, and log the results.
113 The output is available at self.output which can be useful if there is
117 cmd: The command to execute.
118 cwd: The directory to run the command in. Can be None to use the
120 ignore_errors: Indicate whether to ignore errors. If True, the
121 function will simply return if the command cannot be executed
122 or exits with an error code, otherwise an exception will be
123 raised if such problems occur.
126 The output as a string.
129 msg = '+' + ' '.join(cmd) + '\n'
130 if self.chained_file:
131 self.chained_file.write(msg)
132 self.logfile.write(self, msg)
135 p = subprocess.Popen(cmd, cwd=cwd,
136 stdin=None, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
137 (stdout, stderr) = p.communicate()
141 output += 'stdout:\n'
145 output += 'stderr:\n'
147 exit_status = p.returncode
149 except subprocess.CalledProcessError as cpe:
151 exit_status = cpe.returncode
153 except Exception as e:
157 if output and not output.endswith('\n'):
159 if exit_status and not exception and not ignore_errors:
160 exception = Exception('Exit code: ' + str(exit_status))
162 output += str(exception) + '\n'
163 self.logfile.write(self, output)
164 if self.chained_file:
165 self.chained_file.write(output)
167 # Store the output so it can be accessed if we raise an exception.
173 class SectionCtxMgr(object):
174 """A context manager for Python's "with" statement, which allows a certain
175 portion of test code to be logged to a separate section of the log file.
176 Objects of this type should be created by factory functions in the Logfile
177 class rather than directly."""
179 def __init__(self, log, marker, anchor):
180 """Initialize a new object.
183 log: The Logfile object to log to.
184 marker: The name of the nested log section.
185 anchor: The anchor value to pass to start_section().
196 self.anchor = self.log.start_section(self.marker, self.anchor)
198 def __exit__(self, extype, value, traceback):
199 self.log.end_section(self.marker)
201 class Logfile(object):
202 """Generates an HTML-formatted log file containing multiple streams of
203 data, each represented in a well-delineated/-structured fashion."""
205 def __init__(self, fn):
206 """Initialize a new object.
209 fn: The filename to write to.
215 self.f = open(fn, 'wt')
216 self.last_stream = None
221 shutil.copy(mod_dir + '/multiplexed_log.css', os.path.dirname(fn))
225 <link rel="stylesheet" type="text/css" href="multiplexed_log.css">
226 <script src="http://code.jquery.com/jquery.min.js"></script>
228 $(document).ready(function () {
229 // Copy status report HTML to start of log for easy access
230 sts = $(".block#status_report")[0].outerHTML;
231 $("tt").prepend(sts);
233 // Add expand/contract buttons to all block headers
234 btns = "<span class=\\\"block-expand hidden\\\">[+] </span>" +
235 "<span class=\\\"block-contract\\\">[-] </span>";
236 $(".block-header").prepend(btns);
238 // Pre-contract all blocks which passed, leaving only problem cases
239 // expanded, to highlight issues the user should look at.
240 // Only top-level blocks (sections) should have any status
241 passed_bcs = $(".block-content:has(.status-pass)");
242 // Some blocks might have multiple status entries (e.g. the status
243 // report), so take care not to hide blocks with partial success.
244 passed_bcs = passed_bcs.not(":has(.status-fail)");
245 passed_bcs = passed_bcs.not(":has(.status-xfail)");
246 passed_bcs = passed_bcs.not(":has(.status-xpass)");
247 passed_bcs = passed_bcs.not(":has(.status-skipped)");
248 // Hide the passed blocks
249 passed_bcs.addClass("hidden");
250 // Flip the expand/contract button hiding for those blocks.
251 bhs = passed_bcs.parent().children(".block-header")
252 bhs.children(".block-expand").removeClass("hidden");
253 bhs.children(".block-contract").addClass("hidden");
255 // Add click handler to block headers.
256 // The handler expands/contracts the block.
257 $(".block-header").on("click", function (e) {
258 var header = $(this);
259 var content = header.next(".block-content");
260 var expanded = !content.hasClass("hidden");
262 content.addClass("hidden");
263 header.children(".block-expand").first().removeClass("hidden");
264 header.children(".block-contract").first().addClass("hidden");
266 header.children(".block-contract").first().removeClass("hidden");
267 header.children(".block-expand").first().addClass("hidden");
268 content.removeClass("hidden");
272 // When clicking on a link, expand the target block
273 $("a").on("click", function (e) {
274 var block = $($(this).attr("href"));
275 var header = block.children(".block-header");
276 var content = block.children(".block-content").first();
277 header.children(".block-contract").first().removeClass("hidden");
278 header.children(".block-expand").first().addClass("hidden");
279 content.removeClass("hidden");
289 """Close the log file.
291 After calling this function, no more data may be written to the log.
307 # The set of characters that should be represented as hexadecimal codes in
309 _nonprint = ('%' + ''.join(chr(c) for c in range(0, 32) if c not in (9, 10)) +
310 ''.join(chr(c) for c in range(127, 256)))
312 def _escape(self, data):
313 """Render data format suitable for inclusion in an HTML document.
315 This includes HTML-escaping certain characters, and translating
316 control characters to a hexadecimal representation.
319 data: The raw string data to be escaped.
322 An escaped version of the data.
325 data = data.replace(chr(13), '')
326 data = ''.join((c in self._nonprint) and ('%%%02x' % ord(c)) or
328 data = cgi.escape(data)
331 def _terminate_stream(self):
332 """Write HTML to the log file to terminate the current stream's data.
342 if not self.last_stream:
344 self.f.write('</pre>\n')
345 self.f.write('<div class="stream-trailer block-trailer">End stream: ' +
346 self.last_stream.name + '</div>\n')
347 self.f.write('</div>\n')
348 self.f.write('</div>\n')
349 self.last_stream = None
351 def _note(self, note_type, msg, anchor=None):
352 """Write a note or one-off message to the log file.
355 note_type: The type of note. This must be a value supported by the
356 accompanying multiplexed_log.css.
357 msg: The note/message to log.
358 anchor: Optional internal link target.
364 self._terminate_stream()
365 self.f.write('<div class="' + note_type + '">\n')
367 self.f.write('<a href="#%s">\n' % anchor)
368 self.f.write('<pre>')
369 self.f.write(self._escape(msg))
370 self.f.write('\n</pre>\n')
372 self.f.write('</a>\n')
373 self.f.write('</div>\n')
375 def start_section(self, marker, anchor=None):
376 """Begin a new nested section in the log file.
379 marker: The name of the section that is starting.
380 anchor: The value to use for the anchor. If None, a unique value
381 will be calculated and used
384 Name of the HTML anchor emitted before section.
387 self._terminate_stream()
388 self.blocks.append(marker)
391 anchor = str(self.anchor)
392 blk_path = '/'.join(self.blocks)
393 self.f.write('<div class="section block" id="' + anchor + '">\n')
394 self.f.write('<div class="section-header block-header">Section: ' +
395 blk_path + '</div>\n')
396 self.f.write('<div class="section-content block-content">\n')
400 def end_section(self, marker):
401 """Terminate the current nested section in the log file.
403 This function validates proper nesting of start_section() and
404 end_section() calls. If a mismatch is found, an exception is raised.
407 marker: The name of the section that is ending.
413 if (not self.blocks) or (marker != self.blocks[-1]):
414 raise Exception('Block nesting mismatch: "%s" "%s"' %
415 (marker, '/'.join(self.blocks)))
416 self._terminate_stream()
417 blk_path = '/'.join(self.blocks)
418 self.f.write('<div class="section-trailer block-trailer">' +
419 'End section: ' + blk_path + '</div>\n')
420 self.f.write('</div>\n')
421 self.f.write('</div>\n')
424 def section(self, marker, anchor=None):
425 """Create a temporary section in the log file.
427 This function creates a context manager for Python's "with" statement,
428 which allows a certain portion of test code to be logged to a separate
429 section of the log file.
432 with log.section("somename"):
436 marker: The name of the nested section.
437 anchor: The anchor value to pass to start_section().
440 A context manager object.
443 return SectionCtxMgr(self, marker, anchor)
445 def error(self, msg):
446 """Write an error note to the log file.
449 msg: A message describing the error.
455 self._note("error", msg)
457 def warning(self, msg):
458 """Write an warning note to the log file.
461 msg: A message describing the warning.
467 self._note("warning", msg)
470 """Write an informational note to the log file.
473 msg: An informational message.
479 self._note("info", msg)
481 def action(self, msg):
482 """Write an action note to the log file.
485 msg: A message describing the action that is being logged.
491 self._note("action", msg)
493 def status_pass(self, msg, anchor=None):
494 """Write a note to the log file describing test(s) which passed.
497 msg: A message describing the passed test(s).
498 anchor: Optional internal link target.
504 self._note("status-pass", msg, anchor)
506 def status_skipped(self, msg, anchor=None):
507 """Write a note to the log file describing skipped test(s).
510 msg: A message describing the skipped test(s).
511 anchor: Optional internal link target.
517 self._note("status-skipped", msg, anchor)
519 def status_xfail(self, msg, anchor=None):
520 """Write a note to the log file describing xfailed test(s).
523 msg: A message describing the xfailed test(s).
524 anchor: Optional internal link target.
530 self._note("status-xfail", msg, anchor)
532 def status_xpass(self, msg, anchor=None):
533 """Write a note to the log file describing xpassed test(s).
536 msg: A message describing the xpassed test(s).
537 anchor: Optional internal link target.
543 self._note("status-xpass", msg, anchor)
545 def status_fail(self, msg, anchor=None):
546 """Write a note to the log file describing failed test(s).
549 msg: A message describing the failed test(s).
550 anchor: Optional internal link target.
556 self._note("status-fail", msg, anchor)
558 def get_stream(self, name, chained_file=None):
559 """Create an object to log a single stream's data into the log file.
561 This creates a "file-like" object that can be written to in order to
562 write a single stream's data to the log file. The implementation will
563 handle any required interleaving of data (from multiple streams) in
564 the log, in a way that makes it obvious which stream each bit of data
568 name: The name of the stream.
569 chained_file: The file-like object to which all stream data should
570 be logged to in addition to this log. Can be None.
576 return LogfileStream(self, name, chained_file)
578 def get_runner(self, name, chained_file=None):
579 """Create an object that executes processes and logs their output.
582 name: The name of this sub-process.
583 chained_file: The file-like object to which all stream data should
584 be logged to in addition to logfile. Can be None.
590 return RunAndLog(self, name, chained_file)
592 def write(self, stream, data, implicit=False):
593 """Write stream data into the log file.
595 This function should only be used by instances of LogfileStream or
599 stream: The stream whose data is being logged.
600 data: The data to log.
601 implicit: Boolean indicating whether data actually appeared in the
602 stream, or was implicitly generated. A valid use-case is to
603 repeat a shell prompt at the start of each separate log
604 section, which makes the log sections more readable in
611 if stream != self.last_stream:
612 self._terminate_stream()
613 self.f.write('<div class="stream block">\n')
614 self.f.write('<div class="stream-header block-header">Stream: ' +
615 stream.name + '</div>\n')
616 self.f.write('<div class="stream-content block-content">\n')
617 self.f.write('<pre>')
619 self.f.write('<span class="implicit">')
620 self.f.write(self._escape(data))
622 self.f.write('</span>')
623 self.last_stream = stream
626 """Flush the log stream, to ensure correct log interleaving.