1 # SPDX-License-Identifier: GPL-2.0
2 # Copyright (c) 2015 Stephen Warren
3 # Copyright (c) 2015-2016, NVIDIA CORPORATION. All rights reserved.
5 # Generate an HTML-formatted log file containing multiple streams of data,
6 # 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
105 self.exit_status = None
108 """Clean up any resources managed by this object."""
111 def run(self, cmd, cwd=None, ignore_errors=False):
112 """Run a command as a sub-process, and log the results.
114 The output is available at self.output which can be useful if there is
118 cmd: The command to execute.
119 cwd: The directory to run the command in. Can be None to use the
121 ignore_errors: Indicate whether to ignore errors. If True, the
122 function will simply return if the command cannot be executed
123 or exits with an error code, otherwise an exception will be
124 raised if such problems occur.
127 The output as a string.
130 msg = '+' + ' '.join(cmd) + '\n'
131 if self.chained_file:
132 self.chained_file.write(msg)
133 self.logfile.write(self, msg)
136 p = subprocess.Popen(cmd, cwd=cwd,
137 stdin=None, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
138 (stdout, stderr) = p.communicate()
142 output += 'stdout:\n'
146 output += 'stderr:\n'
148 exit_status = p.returncode
150 except subprocess.CalledProcessError as cpe:
152 exit_status = cpe.returncode
154 except Exception as e:
158 if output and not output.endswith('\n'):
160 if exit_status and not exception and not ignore_errors:
161 exception = Exception('Exit code: ' + str(exit_status))
163 output += str(exception) + '\n'
164 self.logfile.write(self, output)
165 if self.chained_file:
166 self.chained_file.write(output)
167 self.logfile.timestamp()
169 # Store the output so it can be accessed if we raise an exception.
171 self.exit_status = exit_status
176 class SectionCtxMgr(object):
177 """A context manager for Python's "with" statement, which allows a certain
178 portion of test code to be logged to a separate section of the log file.
179 Objects of this type should be created by factory functions in the Logfile
180 class rather than directly."""
182 def __init__(self, log, marker, anchor):
183 """Initialize a new object.
186 log: The Logfile object to log to.
187 marker: The name of the nested log section.
188 anchor: The anchor value to pass to start_section().
199 self.anchor = self.log.start_section(self.marker, self.anchor)
201 def __exit__(self, extype, value, traceback):
202 self.log.end_section(self.marker)
204 class Logfile(object):
205 """Generates an HTML-formatted log file containing multiple streams of
206 data, each represented in a well-delineated/-structured fashion."""
208 def __init__(self, fn):
209 """Initialize a new object.
212 fn: The filename to write to.
218 self.f = open(fn, 'wt')
219 self.last_stream = None
223 self.timestamp_start = self._get_time()
224 self.timestamp_prev = self.timestamp_start
225 self.timestamp_blocks = []
226 self.seen_warning = False
228 shutil.copy(mod_dir + '/multiplexed_log.css', os.path.dirname(fn))
232 <link rel="stylesheet" type="text/css" href="multiplexed_log.css">
233 <script src="http://code.jquery.com/jquery.min.js"></script>
235 $(document).ready(function () {
236 // Copy status report HTML to start of log for easy access
237 sts = $(".block#status_report")[0].outerHTML;
238 $("tt").prepend(sts);
240 // Add expand/contract buttons to all block headers
241 btns = "<span class=\\\"block-expand hidden\\\">[+] </span>" +
242 "<span class=\\\"block-contract\\\">[-] </span>";
243 $(".block-header").prepend(btns);
245 // Pre-contract all blocks which passed, leaving only problem cases
246 // expanded, to highlight issues the user should look at.
247 // Only top-level blocks (sections) should have any status
248 passed_bcs = $(".block-content:has(.status-pass)");
249 // Some blocks might have multiple status entries (e.g. the status
250 // report), so take care not to hide blocks with partial success.
251 passed_bcs = passed_bcs.not(":has(.status-fail)");
252 passed_bcs = passed_bcs.not(":has(.status-xfail)");
253 passed_bcs = passed_bcs.not(":has(.status-xpass)");
254 passed_bcs = passed_bcs.not(":has(.status-skipped)");
255 passed_bcs = passed_bcs.not(":has(.status-warning)");
256 // Hide the passed blocks
257 passed_bcs.addClass("hidden");
258 // Flip the expand/contract button hiding for those blocks.
259 bhs = passed_bcs.parent().children(".block-header")
260 bhs.children(".block-expand").removeClass("hidden");
261 bhs.children(".block-contract").addClass("hidden");
263 // Add click handler to block headers.
264 // The handler expands/contracts the block.
265 $(".block-header").on("click", function (e) {
266 var header = $(this);
267 var content = header.next(".block-content");
268 var expanded = !content.hasClass("hidden");
270 content.addClass("hidden");
271 header.children(".block-expand").first().removeClass("hidden");
272 header.children(".block-contract").first().addClass("hidden");
274 header.children(".block-contract").first().removeClass("hidden");
275 header.children(".block-expand").first().addClass("hidden");
276 content.removeClass("hidden");
280 // When clicking on a link, expand the target block
281 $("a").on("click", function (e) {
282 var block = $($(this).attr("href"));
283 var header = block.children(".block-header");
284 var content = block.children(".block-content").first();
285 header.children(".block-contract").first().removeClass("hidden");
286 header.children(".block-expand").first().addClass("hidden");
287 content.removeClass("hidden");
297 """Close the log file.
299 After calling this function, no more data may be written to the log.
315 # The set of characters that should be represented as hexadecimal codes in
317 _nonprint = ('%' + ''.join(chr(c) for c in range(0, 32) if c not in (9, 10)) +
318 ''.join(chr(c) for c in range(127, 256)))
320 def _escape(self, data):
321 """Render data format suitable for inclusion in an HTML document.
323 This includes HTML-escaping certain characters, and translating
324 control characters to a hexadecimal representation.
327 data: The raw string data to be escaped.
330 An escaped version of the data.
333 data = data.replace(chr(13), '')
334 data = ''.join((c in self._nonprint) and ('%%%02x' % ord(c)) or
336 data = cgi.escape(data)
339 def _terminate_stream(self):
340 """Write HTML to the log file to terminate the current stream's data.
350 if not self.last_stream:
352 self.f.write('</pre>\n')
353 self.f.write('<div class="stream-trailer block-trailer">End stream: ' +
354 self.last_stream.name + '</div>\n')
355 self.f.write('</div>\n')
356 self.f.write('</div>\n')
357 self.last_stream = None
359 def _note(self, note_type, msg, anchor=None):
360 """Write a note or one-off message to the log file.
363 note_type: The type of note. This must be a value supported by the
364 accompanying multiplexed_log.css.
365 msg: The note/message to log.
366 anchor: Optional internal link target.
372 self._terminate_stream()
373 self.f.write('<div class="' + note_type + '">\n')
374 self.f.write('<pre>')
376 self.f.write('<a href="#%s">' % anchor)
377 self.f.write(self._escape(msg))
380 self.f.write('\n</pre>\n')
381 self.f.write('</div>\n')
383 def start_section(self, marker, anchor=None):
384 """Begin a new nested section in the log file.
387 marker: The name of the section that is starting.
388 anchor: The value to use for the anchor. If None, a unique value
389 will be calculated and used
392 Name of the HTML anchor emitted before section.
395 self._terminate_stream()
396 self.blocks.append(marker)
397 self.timestamp_blocks.append(self._get_time())
400 anchor = str(self.anchor)
401 blk_path = '/'.join(self.blocks)
402 self.f.write('<div class="section block" id="' + anchor + '">\n')
403 self.f.write('<div class="section-header block-header">Section: ' +
404 blk_path + '</div>\n')
405 self.f.write('<div class="section-content block-content">\n')
410 def end_section(self, marker):
411 """Terminate the current nested section in the log file.
413 This function validates proper nesting of start_section() and
414 end_section() calls. If a mismatch is found, an exception is raised.
417 marker: The name of the section that is ending.
423 if (not self.blocks) or (marker != self.blocks[-1]):
424 raise Exception('Block nesting mismatch: "%s" "%s"' %
425 (marker, '/'.join(self.blocks)))
426 self._terminate_stream()
427 timestamp_now = self._get_time()
428 timestamp_section_start = self.timestamp_blocks.pop()
429 delta_section = timestamp_now - timestamp_section_start
430 self._note("timestamp",
431 "TIME: SINCE-SECTION: " + str(delta_section))
432 blk_path = '/'.join(self.blocks)
433 self.f.write('<div class="section-trailer block-trailer">' +
434 'End section: ' + blk_path + '</div>\n')
435 self.f.write('</div>\n')
436 self.f.write('</div>\n')
439 def section(self, marker, anchor=None):
440 """Create a temporary section in the log file.
442 This function creates a context manager for Python's "with" statement,
443 which allows a certain portion of test code to be logged to a separate
444 section of the log file.
447 with log.section("somename"):
451 marker: The name of the nested section.
452 anchor: The anchor value to pass to start_section().
455 A context manager object.
458 return SectionCtxMgr(self, marker, anchor)
460 def error(self, msg):
461 """Write an error note to the log file.
464 msg: A message describing the error.
470 self._note("error", msg)
472 def warning(self, msg):
473 """Write an warning note to the log file.
476 msg: A message describing the warning.
482 self.seen_warning = True
483 self._note("warning", msg)
485 def get_and_reset_warning(self):
486 """Get and reset the log warning flag.
492 Whether a warning was seen since the last call.
495 ret = self.seen_warning
496 self.seen_warning = False
500 """Write an informational note to the log file.
503 msg: An informational message.
509 self._note("info", msg)
511 def action(self, msg):
512 """Write an action note to the log file.
515 msg: A message describing the action that is being logged.
521 self._note("action", msg)
524 return datetime.datetime.now()
527 """Write a timestamp to the log file.
536 timestamp_now = self._get_time()
537 delta_prev = timestamp_now - self.timestamp_prev
538 delta_start = timestamp_now - self.timestamp_start
539 self.timestamp_prev = timestamp_now
541 self._note("timestamp",
542 "TIME: NOW: " + timestamp_now.strftime("%Y/%m/%d %H:%M:%S.%f"))
543 self._note("timestamp",
544 "TIME: SINCE-PREV: " + str(delta_prev))
545 self._note("timestamp",
546 "TIME: SINCE-START: " + str(delta_start))
548 def status_pass(self, msg, anchor=None):
549 """Write a note to the log file describing test(s) which passed.
552 msg: A message describing the passed test(s).
553 anchor: Optional internal link target.
559 self._note("status-pass", msg, anchor)
561 def status_warning(self, msg, anchor=None):
562 """Write a note to the log file describing test(s) which passed.
565 msg: A message describing the passed test(s).
566 anchor: Optional internal link target.
572 self._note("status-warning", msg, anchor)
574 def status_skipped(self, msg, anchor=None):
575 """Write a note to the log file describing skipped test(s).
578 msg: A message describing the skipped test(s).
579 anchor: Optional internal link target.
585 self._note("status-skipped", msg, anchor)
587 def status_xfail(self, msg, anchor=None):
588 """Write a note to the log file describing xfailed test(s).
591 msg: A message describing the xfailed test(s).
592 anchor: Optional internal link target.
598 self._note("status-xfail", msg, anchor)
600 def status_xpass(self, msg, anchor=None):
601 """Write a note to the log file describing xpassed test(s).
604 msg: A message describing the xpassed test(s).
605 anchor: Optional internal link target.
611 self._note("status-xpass", msg, anchor)
613 def status_fail(self, msg, anchor=None):
614 """Write a note to the log file describing failed test(s).
617 msg: A message describing the failed test(s).
618 anchor: Optional internal link target.
624 self._note("status-fail", msg, anchor)
626 def get_stream(self, name, chained_file=None):
627 """Create an object to log a single stream's data into the log file.
629 This creates a "file-like" object that can be written to in order to
630 write a single stream's data to the log file. The implementation will
631 handle any required interleaving of data (from multiple streams) in
632 the log, in a way that makes it obvious which stream each bit of data
636 name: The name of the stream.
637 chained_file: The file-like object to which all stream data should
638 be logged to in addition to this log. Can be None.
644 return LogfileStream(self, name, chained_file)
646 def get_runner(self, name, chained_file=None):
647 """Create an object that executes processes and logs their output.
650 name: The name of this sub-process.
651 chained_file: The file-like object to which all stream data should
652 be logged to in addition to logfile. Can be None.
658 return RunAndLog(self, name, chained_file)
660 def write(self, stream, data, implicit=False):
661 """Write stream data into the log file.
663 This function should only be used by instances of LogfileStream or
667 stream: The stream whose data is being logged.
668 data: The data to log.
669 implicit: Boolean indicating whether data actually appeared in the
670 stream, or was implicitly generated. A valid use-case is to
671 repeat a shell prompt at the start of each separate log
672 section, which makes the log sections more readable in
679 if stream != self.last_stream:
680 self._terminate_stream()
681 self.f.write('<div class="stream block">\n')
682 self.f.write('<div class="stream-header block-header">Stream: ' +
683 stream.name + '</div>\n')
684 self.f.write('<div class="stream-content block-content">\n')
685 self.f.write('<pre>')
687 self.f.write('<span class="implicit">')
688 self.f.write(self._escape(data))
690 self.f.write('</span>')
691 self.last_stream = stream
694 """Flush the log stream, to ensure correct log interleaving.