1 # SPDX-License-Identifier: GPL-2.0
2 # Copyright (c) 2015 Stephen Warren
3 # Copyright (c) 2015-2016, NVIDIA CORPORATION. All rights reserved.
6 Generate an HTML-formatted log file containing multiple streams of data,
7 each represented in a well-delineated/-structured fashion.
16 mod_dir = os.path.dirname(os.path.abspath(__file__))
18 class LogfileStream(object):
19 """A file-like object used to write a single logical stream of data into
20 a multiplexed log file. Objects of this type should be created by factory
21 functions in the Logfile class rather than directly."""
23 def __init__(self, logfile, name, chained_file):
24 """Initialize a new object.
27 logfile: The Logfile object to log to.
28 name: The name of this log stream.
29 chained_file: The file-like object to which all stream data should be
30 logged to in addition to logfile. Can be None.
36 self.logfile = logfile
38 self.chained_file = chained_file
41 """Dummy function so that this class is "file-like".
52 def write(self, data, implicit=False):
53 """Write data to the log stream.
56 data: The data to write to the file.
57 implicit: Boolean indicating whether data actually appeared in the
58 stream, or was implicitly generated. A valid use-case is to
59 repeat a shell prompt at the start of each separate log
60 section, which makes the log sections more readable in
67 self.logfile.write(self, data, implicit)
69 # Chained file is console, convert things a little
70 self.chained_file.write((data.encode('ascii', 'replace')).decode())
73 """Flush the log stream, to ensure correct log interleaving.
84 self.chained_file.flush()
86 class RunAndLog(object):
87 """A utility object used to execute sub-processes and log their output to
88 a multiplexed log file. Objects of this type should be created by factory
89 functions in the Logfile class rather than directly."""
91 def __init__(self, logfile, name, chained_file):
92 """Initialize a new object.
95 logfile: The Logfile object to log to.
96 name: The name of this log stream or sub-process.
97 chained_file: The file-like object to which all stream data should
98 be logged to in addition to logfile. Can be None.
104 self.logfile = logfile
106 self.chained_file = chained_file
108 self.exit_status = None
111 """Clean up any resources managed by this object."""
114 def run(self, cmd, cwd=None, ignore_errors=False, stdin=None, env=None):
115 """Run a command as a sub-process, and log the results.
117 The output is available at self.output which can be useful if there is
121 cmd: The command to execute.
122 cwd: The directory to run the command in. Can be None to use the
124 ignore_errors: Indicate whether to ignore errors. If True, the
125 function will simply return if the command cannot be executed
126 or exits with an error code, otherwise an exception will be
127 raised if such problems occur.
128 stdin: Input string to pass to the command as stdin (or None)
129 env: Environment to use, or None to use the current one
132 The output as a string.
135 msg = '+' + ' '.join(cmd) + '\n'
136 if self.chained_file:
137 self.chained_file.write(msg)
138 self.logfile.write(self, msg)
141 p = subprocess.Popen(cmd, cwd=cwd,
142 stdin=subprocess.PIPE if stdin else None,
143 stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=env)
144 (stdout, stderr) = p.communicate(input=stdin)
145 if stdout is not None:
146 stdout = stdout.decode('utf-8')
147 if stderr is not None:
148 stderr = stderr.decode('utf-8')
152 output += 'stdout:\n'
156 output += 'stderr:\n'
158 exit_status = p.returncode
160 except subprocess.CalledProcessError as cpe:
162 exit_status = cpe.returncode
164 except Exception as e:
168 if output and not output.endswith('\n'):
170 if exit_status and not exception and not ignore_errors:
171 exception = ValueError('Exit code: ' + str(exit_status))
173 output += str(exception) + '\n'
174 self.logfile.write(self, output)
175 if self.chained_file:
176 self.chained_file.write(output)
177 self.logfile.timestamp()
179 # Store the output so it can be accessed if we raise an exception.
181 self.exit_status = exit_status
187 """A context manager for Python's "with" statement, which allows a certain
188 portion of test code to be logged to a separate section of the log file.
189 Objects of this type should be created by factory functions in the Logfile
190 class rather than directly."""
192 def __init__(self, log, marker, anchor):
193 """Initialize a new object.
196 log: The Logfile object to log to.
197 marker: The name of the nested log section.
198 anchor: The anchor value to pass to start_section().
209 self.anchor = self.log.start_section(self.marker, self.anchor)
211 def __exit__(self, extype, value, traceback):
212 self.log.end_section(self.marker)
215 """Generates an HTML-formatted log file containing multiple streams of
216 data, each represented in a well-delineated/-structured fashion."""
218 def __init__(self, fn):
219 """Initialize a new object.
222 fn: The filename to write to.
228 self.f = open(fn, 'wt', encoding='utf-8')
229 self.last_stream = None
233 self.timestamp_start = self._get_time()
234 self.timestamp_prev = self.timestamp_start
235 self.timestamp_blocks = []
236 self.seen_warning = False
238 shutil.copy(mod_dir + '/multiplexed_log.css', os.path.dirname(fn))
242 <link rel="stylesheet" type="text/css" href="multiplexed_log.css">
243 <script src="http://code.jquery.com/jquery.min.js"></script>
245 $(document).ready(function () {
246 // Copy status report HTML to start of log for easy access
247 sts = $(".block#status_report")[0].outerHTML;
248 $("tt").prepend(sts);
250 // Add expand/contract buttons to all block headers
251 btns = "<span class=\\\"block-expand hidden\\\">[+] </span>" +
252 "<span class=\\\"block-contract\\\">[-] </span>";
253 $(".block-header").prepend(btns);
255 // Pre-contract all blocks which passed, leaving only problem cases
256 // expanded, to highlight issues the user should look at.
257 // Only top-level blocks (sections) should have any status
258 passed_bcs = $(".block-content:has(.status-pass)");
259 // Some blocks might have multiple status entries (e.g. the status
260 // report), so take care not to hide blocks with partial success.
261 passed_bcs = passed_bcs.not(":has(.status-fail)");
262 passed_bcs = passed_bcs.not(":has(.status-xfail)");
263 passed_bcs = passed_bcs.not(":has(.status-xpass)");
264 passed_bcs = passed_bcs.not(":has(.status-skipped)");
265 passed_bcs = passed_bcs.not(":has(.status-warning)");
266 // Hide the passed blocks
267 passed_bcs.addClass("hidden");
268 // Flip the expand/contract button hiding for those blocks.
269 bhs = passed_bcs.parent().children(".block-header")
270 bhs.children(".block-expand").removeClass("hidden");
271 bhs.children(".block-contract").addClass("hidden");
273 // Add click handler to block headers.
274 // The handler expands/contracts the block.
275 $(".block-header").on("click", function (e) {
276 var header = $(this);
277 var content = header.next(".block-content");
278 var expanded = !content.hasClass("hidden");
280 content.addClass("hidden");
281 header.children(".block-expand").first().removeClass("hidden");
282 header.children(".block-contract").first().addClass("hidden");
284 header.children(".block-contract").first().removeClass("hidden");
285 header.children(".block-expand").first().addClass("hidden");
286 content.removeClass("hidden");
290 // When clicking on a link, expand the target block
291 $("a").on("click", function (e) {
292 var block = $($(this).attr("href"));
293 var header = block.children(".block-header");
294 var content = block.children(".block-content").first();
295 header.children(".block-contract").first().removeClass("hidden");
296 header.children(".block-expand").first().addClass("hidden");
297 content.removeClass("hidden");
307 """Close the log file.
309 After calling this function, no more data may be written to the log.
325 # The set of characters that should be represented as hexadecimal codes in
327 _nonprint = {ord('%')}
328 _nonprint.update(c for c in range(0, 32) if c not in (9, 10))
329 _nonprint.update(range(127, 256))
331 def _escape(self, data):
332 """Render data format suitable for inclusion in an HTML document.
334 This includes HTML-escaping certain characters, and translating
335 control characters to a hexadecimal representation.
338 data: The raw string data to be escaped.
341 An escaped version of the data.
344 data = data.replace(chr(13), '')
345 data = ''.join((ord(c) in self._nonprint) and ('%%%02x' % ord(c)) or
347 data = html.escape(data)
350 def _terminate_stream(self):
351 """Write HTML to the log file to terminate the current stream's data.
361 if not self.last_stream:
363 self.f.write('</pre>\n')
364 self.f.write('<div class="stream-trailer block-trailer">End stream: ' +
365 self.last_stream.name + '</div>\n')
366 self.f.write('</div>\n')
367 self.f.write('</div>\n')
368 self.last_stream = None
370 def _note(self, note_type, msg, anchor=None):
371 """Write a note or one-off message to the log file.
374 note_type: The type of note. This must be a value supported by the
375 accompanying multiplexed_log.css.
376 msg: The note/message to log.
377 anchor: Optional internal link target.
383 self._terminate_stream()
384 self.f.write('<div class="' + note_type + '">\n')
385 self.f.write('<pre>')
387 self.f.write('<a href="#%s">' % anchor)
388 self.f.write(self._escape(msg))
391 self.f.write('\n</pre>\n')
392 self.f.write('</div>\n')
394 def start_section(self, marker, anchor=None):
395 """Begin a new nested section in the log file.
398 marker: The name of the section that is starting.
399 anchor: The value to use for the anchor. If None, a unique value
400 will be calculated and used
403 Name of the HTML anchor emitted before section.
406 self._terminate_stream()
407 self.blocks.append(marker)
408 self.timestamp_blocks.append(self._get_time())
411 anchor = str(self.anchor)
412 blk_path = '/'.join(self.blocks)
413 self.f.write('<div class="section block" id="' + anchor + '">\n')
414 self.f.write('<div class="section-header block-header">Section: ' +
415 blk_path + '</div>\n')
416 self.f.write('<div class="section-content block-content">\n')
421 def end_section(self, marker):
422 """Terminate the current nested section in the log file.
424 This function validates proper nesting of start_section() and
425 end_section() calls. If a mismatch is found, an exception is raised.
428 marker: The name of the section that is ending.
434 if (not self.blocks) or (marker != self.blocks[-1]):
435 raise Exception('Block nesting mismatch: "%s" "%s"' %
436 (marker, '/'.join(self.blocks)))
437 self._terminate_stream()
438 timestamp_now = self._get_time()
439 timestamp_section_start = self.timestamp_blocks.pop()
440 delta_section = timestamp_now - timestamp_section_start
441 self._note("timestamp",
442 "TIME: SINCE-SECTION: " + str(delta_section))
443 blk_path = '/'.join(self.blocks)
444 self.f.write('<div class="section-trailer block-trailer">' +
445 'End section: ' + blk_path + '</div>\n')
446 self.f.write('</div>\n')
447 self.f.write('</div>\n')
450 def section(self, marker, anchor=None):
451 """Create a temporary section in the log file.
453 This function creates a context manager for Python's "with" statement,
454 which allows a certain portion of test code to be logged to a separate
455 section of the log file.
458 with log.section("somename"):
462 marker: The name of the nested section.
463 anchor: The anchor value to pass to start_section().
466 A context manager object.
469 return SectionCtxMgr(self, marker, anchor)
471 def error(self, msg):
472 """Write an error note to the log file.
475 msg: A message describing the error.
481 self._note("error", msg)
483 def warning(self, msg):
484 """Write an warning note to the log file.
487 msg: A message describing the warning.
493 self.seen_warning = True
494 self._note("warning", msg)
496 def get_and_reset_warning(self):
497 """Get and reset the log warning flag.
503 Whether a warning was seen since the last call.
506 ret = self.seen_warning
507 self.seen_warning = False
511 """Write an informational note to the log file.
514 msg: An informational message.
520 self._note("info", msg)
522 def action(self, msg):
523 """Write an action note to the log file.
526 msg: A message describing the action that is being logged.
532 self._note("action", msg)
535 return datetime.datetime.now()
538 """Write a timestamp to the log file.
547 timestamp_now = self._get_time()
548 delta_prev = timestamp_now - self.timestamp_prev
549 delta_start = timestamp_now - self.timestamp_start
550 self.timestamp_prev = timestamp_now
552 self._note("timestamp",
553 "TIME: NOW: " + timestamp_now.strftime("%Y/%m/%d %H:%M:%S.%f"))
554 self._note("timestamp",
555 "TIME: SINCE-PREV: " + str(delta_prev))
556 self._note("timestamp",
557 "TIME: SINCE-START: " + str(delta_start))
559 def status_pass(self, msg, anchor=None):
560 """Write a note to the log file describing test(s) which passed.
563 msg: A message describing the passed test(s).
564 anchor: Optional internal link target.
570 self._note("status-pass", msg, anchor)
572 def status_warning(self, msg, anchor=None):
573 """Write a note to the log file describing test(s) which passed.
576 msg: A message describing the passed test(s).
577 anchor: Optional internal link target.
583 self._note("status-warning", msg, anchor)
585 def status_skipped(self, msg, anchor=None):
586 """Write a note to the log file describing skipped test(s).
589 msg: A message describing the skipped test(s).
590 anchor: Optional internal link target.
596 self._note("status-skipped", msg, anchor)
598 def status_xfail(self, msg, anchor=None):
599 """Write a note to the log file describing xfailed test(s).
602 msg: A message describing the xfailed test(s).
603 anchor: Optional internal link target.
609 self._note("status-xfail", msg, anchor)
611 def status_xpass(self, msg, anchor=None):
612 """Write a note to the log file describing xpassed test(s).
615 msg: A message describing the xpassed test(s).
616 anchor: Optional internal link target.
622 self._note("status-xpass", msg, anchor)
624 def status_fail(self, msg, anchor=None):
625 """Write a note to the log file describing failed test(s).
628 msg: A message describing the failed test(s).
629 anchor: Optional internal link target.
635 self._note("status-fail", msg, anchor)
637 def get_stream(self, name, chained_file=None):
638 """Create an object to log a single stream's data into the log file.
640 This creates a "file-like" object that can be written to in order to
641 write a single stream's data to the log file. The implementation will
642 handle any required interleaving of data (from multiple streams) in
643 the log, in a way that makes it obvious which stream each bit of data
647 name: The name of the stream.
648 chained_file: The file-like object to which all stream data should
649 be logged to in addition to this log. Can be None.
655 return LogfileStream(self, name, chained_file)
657 def get_runner(self, name, chained_file=None):
658 """Create an object that executes processes and logs their output.
661 name: The name of this sub-process.
662 chained_file: The file-like object to which all stream data should
663 be logged to in addition to logfile. Can be None.
669 return RunAndLog(self, name, chained_file)
671 def write(self, stream, data, implicit=False):
672 """Write stream data into the log file.
674 This function should only be used by instances of LogfileStream or
678 stream: The stream whose data is being logged.
679 data: The data to log.
680 implicit: Boolean indicating whether data actually appeared in the
681 stream, or was implicitly generated. A valid use-case is to
682 repeat a shell prompt at the start of each separate log
683 section, which makes the log sections more readable in
690 if stream != self.last_stream:
691 self._terminate_stream()
692 self.f.write('<div class="stream block">\n')
693 self.f.write('<div class="stream-header block-header">Stream: ' +
694 stream.name + '</div>\n')
695 self.f.write('<div class="stream-content block-content">\n')
696 self.f.write('<pre>')
698 self.f.write('<span class="implicit">')
699 self.f.write(self._escape(data))
701 self.f.write('</span>')
702 self.last_stream = stream
705 """Flush the log stream, to ensure correct log interleaving.