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.
122 The output as a string.
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)
166 class SectionCtxMgr(object):
167 """A context manager for Python's "with" statement, which allows a certain
168 portion of test code to be logged to a separate section of the log file.
169 Objects of this type should be created by factory functions in the Logfile
170 class rather than directly."""
172 def __init__(self, log, marker, anchor):
173 """Initialize a new object.
176 log: The Logfile object to log to.
177 marker: The name of the nested log section.
178 anchor: The anchor value to pass to start_section().
189 self.anchor = self.log.start_section(self.marker, self.anchor)
191 def __exit__(self, extype, value, traceback):
192 self.log.end_section(self.marker)
194 class Logfile(object):
195 """Generates an HTML-formatted log file containing multiple streams of
196 data, each represented in a well-delineated/-structured fashion."""
198 def __init__(self, fn):
199 """Initialize a new object.
202 fn: The filename to write to.
208 self.f = open(fn, 'wt')
209 self.last_stream = None
214 shutil.copy(mod_dir + '/multiplexed_log.css', os.path.dirname(fn))
218 <link rel="stylesheet" type="text/css" href="multiplexed_log.css">
219 <script src="http://code.jquery.com/jquery.min.js"></script>
221 $(document).ready(function () {
222 // Copy status report HTML to start of log for easy access
223 sts = $(".block#status_report")[0].outerHTML;
224 $("tt").prepend(sts);
226 // Add expand/contract buttons to all block headers
227 btns = "<span class=\\\"block-expand hidden\\\">[+] </span>" +
228 "<span class=\\\"block-contract\\\">[-] </span>";
229 $(".block-header").prepend(btns);
231 // Pre-contract all blocks which passed, leaving only problem cases
232 // expanded, to highlight issues the user should look at.
233 // Only top-level blocks (sections) should have any status
234 passed_bcs = $(".block-content:has(.status-pass)");
235 // Some blocks might have multiple status entries (e.g. the status
236 // report), so take care not to hide blocks with partial success.
237 passed_bcs = passed_bcs.not(":has(.status-fail)");
238 passed_bcs = passed_bcs.not(":has(.status-xfail)");
239 passed_bcs = passed_bcs.not(":has(.status-xpass)");
240 passed_bcs = passed_bcs.not(":has(.status-skipped)");
241 // Hide the passed blocks
242 passed_bcs.addClass("hidden");
243 // Flip the expand/contract button hiding for those blocks.
244 bhs = passed_bcs.parent().children(".block-header")
245 bhs.children(".block-expand").removeClass("hidden");
246 bhs.children(".block-contract").addClass("hidden");
248 // Add click handler to block headers.
249 // The handler expands/contracts the block.
250 $(".block-header").on("click", function (e) {
251 var header = $(this);
252 var content = header.next(".block-content");
253 var expanded = !content.hasClass("hidden");
255 content.addClass("hidden");
256 header.children(".block-expand").first().removeClass("hidden");
257 header.children(".block-contract").first().addClass("hidden");
259 header.children(".block-contract").first().removeClass("hidden");
260 header.children(".block-expand").first().addClass("hidden");
261 content.removeClass("hidden");
265 // When clicking on a link, expand the target block
266 $("a").on("click", function (e) {
267 var block = $($(this).attr("href"));
268 var header = block.children(".block-header");
269 var content = block.children(".block-content").first();
270 header.children(".block-contract").first().removeClass("hidden");
271 header.children(".block-expand").first().addClass("hidden");
272 content.removeClass("hidden");
282 """Close the log file.
284 After calling this function, no more data may be written to the log.
300 # The set of characters that should be represented as hexadecimal codes in
302 _nonprint = ('%' + ''.join(chr(c) for c in range(0, 32) if c not in (9, 10)) +
303 ''.join(chr(c) for c in range(127, 256)))
305 def _escape(self, data):
306 """Render data format suitable for inclusion in an HTML document.
308 This includes HTML-escaping certain characters, and translating
309 control characters to a hexadecimal representation.
312 data: The raw string data to be escaped.
315 An escaped version of the data.
318 data = data.replace(chr(13), '')
319 data = ''.join((c in self._nonprint) and ('%%%02x' % ord(c)) or
321 data = cgi.escape(data)
324 def _terminate_stream(self):
325 """Write HTML to the log file to terminate the current stream's data.
335 if not self.last_stream:
337 self.f.write('</pre>\n')
338 self.f.write('<div class="stream-trailer block-trailer">End stream: ' +
339 self.last_stream.name + '</div>\n')
340 self.f.write('</div>\n')
341 self.f.write('</div>\n')
342 self.last_stream = None
344 def _note(self, note_type, msg, anchor=None):
345 """Write a note or one-off message to the log file.
348 note_type: The type of note. This must be a value supported by the
349 accompanying multiplexed_log.css.
350 msg: The note/message to log.
351 anchor: Optional internal link target.
357 self._terminate_stream()
358 self.f.write('<div class="' + note_type + '">\n')
360 self.f.write('<a href="#%s">\n' % anchor)
361 self.f.write('<pre>')
362 self.f.write(self._escape(msg))
363 self.f.write('\n</pre>\n')
365 self.f.write('</a>\n')
366 self.f.write('</div>\n')
368 def start_section(self, marker, anchor=None):
369 """Begin a new nested section in the log file.
372 marker: The name of the section that is starting.
373 anchor: The value to use for the anchor. If None, a unique value
374 will be calculated and used
377 Name of the HTML anchor emitted before section.
380 self._terminate_stream()
381 self.blocks.append(marker)
384 anchor = str(self.anchor)
385 blk_path = '/'.join(self.blocks)
386 self.f.write('<div class="section block" id="' + anchor + '">\n')
387 self.f.write('<div class="section-header block-header">Section: ' +
388 blk_path + '</div>\n')
389 self.f.write('<div class="section-content block-content">\n')
393 def end_section(self, marker):
394 """Terminate the current nested section in the log file.
396 This function validates proper nesting of start_section() and
397 end_section() calls. If a mismatch is found, an exception is raised.
400 marker: The name of the section that is ending.
406 if (not self.blocks) or (marker != self.blocks[-1]):
407 raise Exception('Block nesting mismatch: "%s" "%s"' %
408 (marker, '/'.join(self.blocks)))
409 self._terminate_stream()
410 blk_path = '/'.join(self.blocks)
411 self.f.write('<div class="section-trailer block-trailer">' +
412 'End section: ' + blk_path + '</div>\n')
413 self.f.write('</div>\n')
414 self.f.write('</div>\n')
417 def section(self, marker, anchor=None):
418 """Create a temporary section in the log file.
420 This function creates a context manager for Python's "with" statement,
421 which allows a certain portion of test code to be logged to a separate
422 section of the log file.
425 with log.section("somename"):
429 marker: The name of the nested section.
430 anchor: The anchor value to pass to start_section().
433 A context manager object.
436 return SectionCtxMgr(self, marker, anchor)
438 def error(self, msg):
439 """Write an error note to the log file.
442 msg: A message describing the error.
448 self._note("error", msg)
450 def warning(self, msg):
451 """Write an warning note to the log file.
454 msg: A message describing the warning.
460 self._note("warning", msg)
463 """Write an informational note to the log file.
466 msg: An informational message.
472 self._note("info", msg)
474 def action(self, msg):
475 """Write an action note to the log file.
478 msg: A message describing the action that is being logged.
484 self._note("action", msg)
486 def status_pass(self, msg, anchor=None):
487 """Write a note to the log file describing test(s) which passed.
490 msg: A message describing the passed test(s).
491 anchor: Optional internal link target.
497 self._note("status-pass", msg, anchor)
499 def status_skipped(self, msg, anchor=None):
500 """Write a note to the log file describing skipped test(s).
503 msg: A message describing the skipped test(s).
504 anchor: Optional internal link target.
510 self._note("status-skipped", msg, anchor)
512 def status_xfail(self, msg, anchor=None):
513 """Write a note to the log file describing xfailed test(s).
516 msg: A message describing the xfailed test(s).
517 anchor: Optional internal link target.
523 self._note("status-xfail", msg, anchor)
525 def status_xpass(self, msg, anchor=None):
526 """Write a note to the log file describing xpassed test(s).
529 msg: A message describing the xpassed test(s).
530 anchor: Optional internal link target.
536 self._note("status-xpass", msg, anchor)
538 def status_fail(self, msg, anchor=None):
539 """Write a note to the log file describing failed test(s).
542 msg: A message describing the failed test(s).
543 anchor: Optional internal link target.
549 self._note("status-fail", msg, anchor)
551 def get_stream(self, name, chained_file=None):
552 """Create an object to log a single stream's data into the log file.
554 This creates a "file-like" object that can be written to in order to
555 write a single stream's data to the log file. The implementation will
556 handle any required interleaving of data (from multiple streams) in
557 the log, in a way that makes it obvious which stream each bit of data
561 name: The name of the stream.
562 chained_file: The file-like object to which all stream data should
563 be logged to in addition to this log. Can be None.
569 return LogfileStream(self, name, chained_file)
571 def get_runner(self, name, chained_file=None):
572 """Create an object that executes processes and logs their output.
575 name: The name of this sub-process.
576 chained_file: The file-like object to which all stream data should
577 be logged to in addition to logfile. Can be None.
583 return RunAndLog(self, name, chained_file)
585 def write(self, stream, data, implicit=False):
586 """Write stream data into the log file.
588 This function should only be used by instances of LogfileStream or
592 stream: The stream whose data is being logged.
593 data: The data to log.
594 implicit: Boolean indicating whether data actually appeared in the
595 stream, or was implicitly generated. A valid use-case is to
596 repeat a shell prompt at the start of each separate log
597 section, which makes the log sections more readable in
604 if stream != self.last_stream:
605 self._terminate_stream()
606 self.f.write('<div class="stream block">\n')
607 self.f.write('<div class="stream-header block-header">Stream: ' +
608 stream.name + '</div>\n')
609 self.f.write('<div class="stream-content block-content">\n')
610 self.f.write('<pre>')
612 self.f.write('<span class="implicit">')
613 self.f.write(self._escape(data))
615 self.f.write('</span>')
616 self.last_stream = stream
619 """Flush the log stream, to ensure correct log interleaving.