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):
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.
130 The output as a string.
133 msg = '+' + ' '.join(cmd) + '\n'
134 if self.chained_file:
135 self.chained_file.write(msg)
136 self.logfile.write(self, msg)
139 p = subprocess.Popen(cmd, cwd=cwd,
140 stdin=None, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
141 (stdout, stderr) = p.communicate()
142 if stdout is not None:
143 stdout = stdout.decode('utf-8')
144 if stderr is not None:
145 stderr = stderr.decode('utf-8')
149 output += 'stdout:\n'
153 output += 'stderr:\n'
155 exit_status = p.returncode
157 except subprocess.CalledProcessError as cpe:
159 exit_status = cpe.returncode
161 except Exception as e:
165 if output and not output.endswith('\n'):
167 if exit_status and not exception and not ignore_errors:
168 exception = Exception('Exit code: ' + str(exit_status))
170 output += str(exception) + '\n'
171 self.logfile.write(self, output)
172 if self.chained_file:
173 self.chained_file.write(output)
174 self.logfile.timestamp()
176 # Store the output so it can be accessed if we raise an exception.
178 self.exit_status = exit_status
184 """A context manager for Python's "with" statement, which allows a certain
185 portion of test code to be logged to a separate section of the log file.
186 Objects of this type should be created by factory functions in the Logfile
187 class rather than directly."""
189 def __init__(self, log, marker, anchor):
190 """Initialize a new object.
193 log: The Logfile object to log to.
194 marker: The name of the nested log section.
195 anchor: The anchor value to pass to start_section().
206 self.anchor = self.log.start_section(self.marker, self.anchor)
208 def __exit__(self, extype, value, traceback):
209 self.log.end_section(self.marker)
212 """Generates an HTML-formatted log file containing multiple streams of
213 data, each represented in a well-delineated/-structured fashion."""
215 def __init__(self, fn):
216 """Initialize a new object.
219 fn: The filename to write to.
225 self.f = open(fn, 'wt', encoding='utf-8')
226 self.last_stream = None
230 self.timestamp_start = self._get_time()
231 self.timestamp_prev = self.timestamp_start
232 self.timestamp_blocks = []
233 self.seen_warning = False
235 shutil.copy(mod_dir + '/multiplexed_log.css', os.path.dirname(fn))
239 <link rel="stylesheet" type="text/css" href="multiplexed_log.css">
240 <script src="http://code.jquery.com/jquery.min.js"></script>
242 $(document).ready(function () {
243 // Copy status report HTML to start of log for easy access
244 sts = $(".block#status_report")[0].outerHTML;
245 $("tt").prepend(sts);
247 // Add expand/contract buttons to all block headers
248 btns = "<span class=\\\"block-expand hidden\\\">[+] </span>" +
249 "<span class=\\\"block-contract\\\">[-] </span>";
250 $(".block-header").prepend(btns);
252 // Pre-contract all blocks which passed, leaving only problem cases
253 // expanded, to highlight issues the user should look at.
254 // Only top-level blocks (sections) should have any status
255 passed_bcs = $(".block-content:has(.status-pass)");
256 // Some blocks might have multiple status entries (e.g. the status
257 // report), so take care not to hide blocks with partial success.
258 passed_bcs = passed_bcs.not(":has(.status-fail)");
259 passed_bcs = passed_bcs.not(":has(.status-xfail)");
260 passed_bcs = passed_bcs.not(":has(.status-xpass)");
261 passed_bcs = passed_bcs.not(":has(.status-skipped)");
262 passed_bcs = passed_bcs.not(":has(.status-warning)");
263 // Hide the passed blocks
264 passed_bcs.addClass("hidden");
265 // Flip the expand/contract button hiding for those blocks.
266 bhs = passed_bcs.parent().children(".block-header")
267 bhs.children(".block-expand").removeClass("hidden");
268 bhs.children(".block-contract").addClass("hidden");
270 // Add click handler to block headers.
271 // The handler expands/contracts the block.
272 $(".block-header").on("click", function (e) {
273 var header = $(this);
274 var content = header.next(".block-content");
275 var expanded = !content.hasClass("hidden");
277 content.addClass("hidden");
278 header.children(".block-expand").first().removeClass("hidden");
279 header.children(".block-contract").first().addClass("hidden");
281 header.children(".block-contract").first().removeClass("hidden");
282 header.children(".block-expand").first().addClass("hidden");
283 content.removeClass("hidden");
287 // When clicking on a link, expand the target block
288 $("a").on("click", function (e) {
289 var block = $($(this).attr("href"));
290 var header = block.children(".block-header");
291 var content = block.children(".block-content").first();
292 header.children(".block-contract").first().removeClass("hidden");
293 header.children(".block-expand").first().addClass("hidden");
294 content.removeClass("hidden");
304 """Close the log file.
306 After calling this function, no more data may be written to the log.
322 # The set of characters that should be represented as hexadecimal codes in
324 _nonprint = {ord('%')}
325 _nonprint.update(c for c in range(0, 32) if c not in (9, 10))
326 _nonprint.update(range(127, 256))
328 def _escape(self, data):
329 """Render data format suitable for inclusion in an HTML document.
331 This includes HTML-escaping certain characters, and translating
332 control characters to a hexadecimal representation.
335 data: The raw string data to be escaped.
338 An escaped version of the data.
341 data = data.replace(chr(13), '')
342 data = ''.join((ord(c) in self._nonprint) and ('%%%02x' % ord(c)) or
344 data = html.escape(data)
347 def _terminate_stream(self):
348 """Write HTML to the log file to terminate the current stream's data.
358 if not self.last_stream:
360 self.f.write('</pre>\n')
361 self.f.write('<div class="stream-trailer block-trailer">End stream: ' +
362 self.last_stream.name + '</div>\n')
363 self.f.write('</div>\n')
364 self.f.write('</div>\n')
365 self.last_stream = None
367 def _note(self, note_type, msg, anchor=None):
368 """Write a note or one-off message to the log file.
371 note_type: The type of note. This must be a value supported by the
372 accompanying multiplexed_log.css.
373 msg: The note/message to log.
374 anchor: Optional internal link target.
380 self._terminate_stream()
381 self.f.write('<div class="' + note_type + '">\n')
382 self.f.write('<pre>')
384 self.f.write('<a href="#%s">' % anchor)
385 self.f.write(self._escape(msg))
388 self.f.write('\n</pre>\n')
389 self.f.write('</div>\n')
391 def start_section(self, marker, anchor=None):
392 """Begin a new nested section in the log file.
395 marker: The name of the section that is starting.
396 anchor: The value to use for the anchor. If None, a unique value
397 will be calculated and used
400 Name of the HTML anchor emitted before section.
403 self._terminate_stream()
404 self.blocks.append(marker)
405 self.timestamp_blocks.append(self._get_time())
408 anchor = str(self.anchor)
409 blk_path = '/'.join(self.blocks)
410 self.f.write('<div class="section block" id="' + anchor + '">\n')
411 self.f.write('<div class="section-header block-header">Section: ' +
412 blk_path + '</div>\n')
413 self.f.write('<div class="section-content block-content">\n')
418 def end_section(self, marker):
419 """Terminate the current nested section in the log file.
421 This function validates proper nesting of start_section() and
422 end_section() calls. If a mismatch is found, an exception is raised.
425 marker: The name of the section that is ending.
431 if (not self.blocks) or (marker != self.blocks[-1]):
432 raise Exception('Block nesting mismatch: "%s" "%s"' %
433 (marker, '/'.join(self.blocks)))
434 self._terminate_stream()
435 timestamp_now = self._get_time()
436 timestamp_section_start = self.timestamp_blocks.pop()
437 delta_section = timestamp_now - timestamp_section_start
438 self._note("timestamp",
439 "TIME: SINCE-SECTION: " + str(delta_section))
440 blk_path = '/'.join(self.blocks)
441 self.f.write('<div class="section-trailer block-trailer">' +
442 'End section: ' + blk_path + '</div>\n')
443 self.f.write('</div>\n')
444 self.f.write('</div>\n')
447 def section(self, marker, anchor=None):
448 """Create a temporary section in the log file.
450 This function creates a context manager for Python's "with" statement,
451 which allows a certain portion of test code to be logged to a separate
452 section of the log file.
455 with log.section("somename"):
459 marker: The name of the nested section.
460 anchor: The anchor value to pass to start_section().
463 A context manager object.
466 return SectionCtxMgr(self, marker, anchor)
468 def error(self, msg):
469 """Write an error note to the log file.
472 msg: A message describing the error.
478 self._note("error", msg)
480 def warning(self, msg):
481 """Write an warning note to the log file.
484 msg: A message describing the warning.
490 self.seen_warning = True
491 self._note("warning", msg)
493 def get_and_reset_warning(self):
494 """Get and reset the log warning flag.
500 Whether a warning was seen since the last call.
503 ret = self.seen_warning
504 self.seen_warning = False
508 """Write an informational note to the log file.
511 msg: An informational message.
517 self._note("info", msg)
519 def action(self, msg):
520 """Write an action note to the log file.
523 msg: A message describing the action that is being logged.
529 self._note("action", msg)
532 return datetime.datetime.now()
535 """Write a timestamp to the log file.
544 timestamp_now = self._get_time()
545 delta_prev = timestamp_now - self.timestamp_prev
546 delta_start = timestamp_now - self.timestamp_start
547 self.timestamp_prev = timestamp_now
549 self._note("timestamp",
550 "TIME: NOW: " + timestamp_now.strftime("%Y/%m/%d %H:%M:%S.%f"))
551 self._note("timestamp",
552 "TIME: SINCE-PREV: " + str(delta_prev))
553 self._note("timestamp",
554 "TIME: SINCE-START: " + str(delta_start))
556 def status_pass(self, msg, anchor=None):
557 """Write a note to the log file describing test(s) which passed.
560 msg: A message describing the passed test(s).
561 anchor: Optional internal link target.
567 self._note("status-pass", msg, anchor)
569 def status_warning(self, msg, anchor=None):
570 """Write a note to the log file describing test(s) which passed.
573 msg: A message describing the passed test(s).
574 anchor: Optional internal link target.
580 self._note("status-warning", msg, anchor)
582 def status_skipped(self, msg, anchor=None):
583 """Write a note to the log file describing skipped test(s).
586 msg: A message describing the skipped test(s).
587 anchor: Optional internal link target.
593 self._note("status-skipped", msg, anchor)
595 def status_xfail(self, msg, anchor=None):
596 """Write a note to the log file describing xfailed test(s).
599 msg: A message describing the xfailed test(s).
600 anchor: Optional internal link target.
606 self._note("status-xfail", msg, anchor)
608 def status_xpass(self, msg, anchor=None):
609 """Write a note to the log file describing xpassed test(s).
612 msg: A message describing the xpassed test(s).
613 anchor: Optional internal link target.
619 self._note("status-xpass", msg, anchor)
621 def status_fail(self, msg, anchor=None):
622 """Write a note to the log file describing failed test(s).
625 msg: A message describing the failed test(s).
626 anchor: Optional internal link target.
632 self._note("status-fail", msg, anchor)
634 def get_stream(self, name, chained_file=None):
635 """Create an object to log a single stream's data into the log file.
637 This creates a "file-like" object that can be written to in order to
638 write a single stream's data to the log file. The implementation will
639 handle any required interleaving of data (from multiple streams) in
640 the log, in a way that makes it obvious which stream each bit of data
644 name: The name of the stream.
645 chained_file: The file-like object to which all stream data should
646 be logged to in addition to this log. Can be None.
652 return LogfileStream(self, name, chained_file)
654 def get_runner(self, name, chained_file=None):
655 """Create an object that executes processes and logs their output.
658 name: The name of this sub-process.
659 chained_file: The file-like object to which all stream data should
660 be logged to in addition to logfile. Can be None.
666 return RunAndLog(self, name, chained_file)
668 def write(self, stream, data, implicit=False):
669 """Write stream data into the log file.
671 This function should only be used by instances of LogfileStream or
675 stream: The stream whose data is being logged.
676 data: The data to log.
677 implicit: Boolean indicating whether data actually appeared in the
678 stream, or was implicitly generated. A valid use-case is to
679 repeat a shell prompt at the start of each separate log
680 section, which makes the log sections more readable in
687 if stream != self.last_stream:
688 self._terminate_stream()
689 self.f.write('<div class="stream block">\n')
690 self.f.write('<div class="stream-header block-header">Stream: ' +
691 stream.name + '</div>\n')
692 self.f.write('<div class="stream-content block-content">\n')
693 self.f.write('<pre>')
695 self.f.write('<span class="implicit">')
696 self.f.write(self._escape(data))
698 self.f.write('</span>')
699 self.last_stream = stream
702 """Flush the log stream, to ensure correct log interleaving.