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 to the 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 # Chained file is console, convert things a little
68 self.chained_file.write((data.encode('ascii', 'replace')).decode())
71 """Flush the log stream, to ensure correct log interleaving.
82 self.chained_file.flush()
84 class RunAndLog(object):
85 """A utility object used to execute sub-processes and log their output to
86 a multiplexed log file. Objects of this type should be created by factory
87 functions in the Logfile class rather than directly."""
89 def __init__(self, logfile, name, chained_file):
90 """Initialize a new object.
93 logfile: The Logfile object to log to.
94 name: The name of this log stream or sub-process.
95 chained_file: The file-like object to which all stream data should
96 be logged to in addition to logfile. Can be None.
102 self.logfile = logfile
104 self.chained_file = chained_file
106 self.exit_status = None
109 """Clean up any resources managed by this object."""
112 def run(self, cmd, cwd=None, ignore_errors=False):
113 """Run a command as a sub-process, and log the results.
115 The output is available at self.output which can be useful if there is
119 cmd: The command to execute.
120 cwd: The directory to run the command in. Can be None to use the
122 ignore_errors: Indicate whether to ignore errors. If True, the
123 function will simply return if the command cannot be executed
124 or exits with an error code, otherwise an exception will be
125 raised if such problems occur.
128 The output as a string.
131 msg = '+' + ' '.join(cmd) + '\n'
132 if self.chained_file:
133 self.chained_file.write(msg)
134 self.logfile.write(self, msg)
137 p = subprocess.Popen(cmd, cwd=cwd,
138 stdin=None, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
139 (stdout, stderr) = p.communicate()
140 if stdout is not None:
141 stdout = stdout.decode('utf-8')
142 if stderr is not None:
143 stderr = stderr.decode('utf-8')
147 output += 'stdout:\n'
151 output += 'stderr:\n'
153 exit_status = p.returncode
155 except subprocess.CalledProcessError as cpe:
157 exit_status = cpe.returncode
159 except Exception as e:
163 if output and not output.endswith('\n'):
165 if exit_status and not exception and not ignore_errors:
166 exception = Exception('Exit code: ' + str(exit_status))
168 output += str(exception) + '\n'
169 self.logfile.write(self, output)
170 if self.chained_file:
171 self.chained_file.write(output)
172 self.logfile.timestamp()
174 # Store the output so it can be accessed if we raise an exception.
176 self.exit_status = exit_status
181 class SectionCtxMgr(object):
182 """A context manager for Python's "with" statement, which allows a certain
183 portion of test code to be logged to a separate section of the log file.
184 Objects of this type should be created by factory functions in the Logfile
185 class rather than directly."""
187 def __init__(self, log, marker, anchor):
188 """Initialize a new object.
191 log: The Logfile object to log to.
192 marker: The name of the nested log section.
193 anchor: The anchor value to pass to start_section().
204 self.anchor = self.log.start_section(self.marker, self.anchor)
206 def __exit__(self, extype, value, traceback):
207 self.log.end_section(self.marker)
209 class Logfile(object):
210 """Generates an HTML-formatted log file containing multiple streams of
211 data, each represented in a well-delineated/-structured fashion."""
213 def __init__(self, fn):
214 """Initialize a new object.
217 fn: The filename to write to.
223 self.f = open(fn, 'wt', encoding='utf-8')
224 self.last_stream = None
228 self.timestamp_start = self._get_time()
229 self.timestamp_prev = self.timestamp_start
230 self.timestamp_blocks = []
231 self.seen_warning = False
233 shutil.copy(mod_dir + '/multiplexed_log.css', os.path.dirname(fn))
237 <link rel="stylesheet" type="text/css" href="multiplexed_log.css">
238 <script src="http://code.jquery.com/jquery.min.js"></script>
240 $(document).ready(function () {
241 // Copy status report HTML to start of log for easy access
242 sts = $(".block#status_report")[0].outerHTML;
243 $("tt").prepend(sts);
245 // Add expand/contract buttons to all block headers
246 btns = "<span class=\\\"block-expand hidden\\\">[+] </span>" +
247 "<span class=\\\"block-contract\\\">[-] </span>";
248 $(".block-header").prepend(btns);
250 // Pre-contract all blocks which passed, leaving only problem cases
251 // expanded, to highlight issues the user should look at.
252 // Only top-level blocks (sections) should have any status
253 passed_bcs = $(".block-content:has(.status-pass)");
254 // Some blocks might have multiple status entries (e.g. the status
255 // report), so take care not to hide blocks with partial success.
256 passed_bcs = passed_bcs.not(":has(.status-fail)");
257 passed_bcs = passed_bcs.not(":has(.status-xfail)");
258 passed_bcs = passed_bcs.not(":has(.status-xpass)");
259 passed_bcs = passed_bcs.not(":has(.status-skipped)");
260 passed_bcs = passed_bcs.not(":has(.status-warning)");
261 // Hide the passed blocks
262 passed_bcs.addClass("hidden");
263 // Flip the expand/contract button hiding for those blocks.
264 bhs = passed_bcs.parent().children(".block-header")
265 bhs.children(".block-expand").removeClass("hidden");
266 bhs.children(".block-contract").addClass("hidden");
268 // Add click handler to block headers.
269 // The handler expands/contracts the block.
270 $(".block-header").on("click", function (e) {
271 var header = $(this);
272 var content = header.next(".block-content");
273 var expanded = !content.hasClass("hidden");
275 content.addClass("hidden");
276 header.children(".block-expand").first().removeClass("hidden");
277 header.children(".block-contract").first().addClass("hidden");
279 header.children(".block-contract").first().removeClass("hidden");
280 header.children(".block-expand").first().addClass("hidden");
281 content.removeClass("hidden");
285 // When clicking on a link, expand the target block
286 $("a").on("click", function (e) {
287 var block = $($(this).attr("href"));
288 var header = block.children(".block-header");
289 var content = block.children(".block-content").first();
290 header.children(".block-contract").first().removeClass("hidden");
291 header.children(".block-expand").first().addClass("hidden");
292 content.removeClass("hidden");
302 """Close the log file.
304 After calling this function, no more data may be written to the log.
320 # The set of characters that should be represented as hexadecimal codes in
322 _nonprint = {ord('%')}
323 _nonprint.update({c for c in range(0, 32) if c not in (9, 10)})
324 _nonprint.update({c for c in range(127, 256)})
326 def _escape(self, data):
327 """Render data format suitable for inclusion in an HTML document.
329 This includes HTML-escaping certain characters, and translating
330 control characters to a hexadecimal representation.
333 data: The raw string data to be escaped.
336 An escaped version of the data.
339 data = data.replace(chr(13), '')
340 data = ''.join((ord(c) in self._nonprint) and ('%%%02x' % ord(c)) or
342 data = html.escape(data)
345 def _terminate_stream(self):
346 """Write HTML to the log file to terminate the current stream's data.
356 if not self.last_stream:
358 self.f.write('</pre>\n')
359 self.f.write('<div class="stream-trailer block-trailer">End stream: ' +
360 self.last_stream.name + '</div>\n')
361 self.f.write('</div>\n')
362 self.f.write('</div>\n')
363 self.last_stream = None
365 def _note(self, note_type, msg, anchor=None):
366 """Write a note or one-off message to the log file.
369 note_type: The type of note. This must be a value supported by the
370 accompanying multiplexed_log.css.
371 msg: The note/message to log.
372 anchor: Optional internal link target.
378 self._terminate_stream()
379 self.f.write('<div class="' + note_type + '">\n')
380 self.f.write('<pre>')
382 self.f.write('<a href="#%s">' % anchor)
383 self.f.write(self._escape(msg))
386 self.f.write('\n</pre>\n')
387 self.f.write('</div>\n')
389 def start_section(self, marker, anchor=None):
390 """Begin a new nested section in the log file.
393 marker: The name of the section that is starting.
394 anchor: The value to use for the anchor. If None, a unique value
395 will be calculated and used
398 Name of the HTML anchor emitted before section.
401 self._terminate_stream()
402 self.blocks.append(marker)
403 self.timestamp_blocks.append(self._get_time())
406 anchor = str(self.anchor)
407 blk_path = '/'.join(self.blocks)
408 self.f.write('<div class="section block" id="' + anchor + '">\n')
409 self.f.write('<div class="section-header block-header">Section: ' +
410 blk_path + '</div>\n')
411 self.f.write('<div class="section-content block-content">\n')
416 def end_section(self, marker):
417 """Terminate the current nested section in the log file.
419 This function validates proper nesting of start_section() and
420 end_section() calls. If a mismatch is found, an exception is raised.
423 marker: The name of the section that is ending.
429 if (not self.blocks) or (marker != self.blocks[-1]):
430 raise Exception('Block nesting mismatch: "%s" "%s"' %
431 (marker, '/'.join(self.blocks)))
432 self._terminate_stream()
433 timestamp_now = self._get_time()
434 timestamp_section_start = self.timestamp_blocks.pop()
435 delta_section = timestamp_now - timestamp_section_start
436 self._note("timestamp",
437 "TIME: SINCE-SECTION: " + str(delta_section))
438 blk_path = '/'.join(self.blocks)
439 self.f.write('<div class="section-trailer block-trailer">' +
440 'End section: ' + blk_path + '</div>\n')
441 self.f.write('</div>\n')
442 self.f.write('</div>\n')
445 def section(self, marker, anchor=None):
446 """Create a temporary section in the log file.
448 This function creates a context manager for Python's "with" statement,
449 which allows a certain portion of test code to be logged to a separate
450 section of the log file.
453 with log.section("somename"):
457 marker: The name of the nested section.
458 anchor: The anchor value to pass to start_section().
461 A context manager object.
464 return SectionCtxMgr(self, marker, anchor)
466 def error(self, msg):
467 """Write an error note to the log file.
470 msg: A message describing the error.
476 self._note("error", msg)
478 def warning(self, msg):
479 """Write an warning note to the log file.
482 msg: A message describing the warning.
488 self.seen_warning = True
489 self._note("warning", msg)
491 def get_and_reset_warning(self):
492 """Get and reset the log warning flag.
498 Whether a warning was seen since the last call.
501 ret = self.seen_warning
502 self.seen_warning = False
506 """Write an informational note to the log file.
509 msg: An informational message.
515 self._note("info", msg)
517 def action(self, msg):
518 """Write an action note to the log file.
521 msg: A message describing the action that is being logged.
527 self._note("action", msg)
530 return datetime.datetime.now()
533 """Write a timestamp to the log file.
542 timestamp_now = self._get_time()
543 delta_prev = timestamp_now - self.timestamp_prev
544 delta_start = timestamp_now - self.timestamp_start
545 self.timestamp_prev = timestamp_now
547 self._note("timestamp",
548 "TIME: NOW: " + timestamp_now.strftime("%Y/%m/%d %H:%M:%S.%f"))
549 self._note("timestamp",
550 "TIME: SINCE-PREV: " + str(delta_prev))
551 self._note("timestamp",
552 "TIME: SINCE-START: " + str(delta_start))
554 def status_pass(self, msg, anchor=None):
555 """Write a note to the log file describing test(s) which passed.
558 msg: A message describing the passed test(s).
559 anchor: Optional internal link target.
565 self._note("status-pass", msg, anchor)
567 def status_warning(self, msg, anchor=None):
568 """Write a note to the log file describing test(s) which passed.
571 msg: A message describing the passed test(s).
572 anchor: Optional internal link target.
578 self._note("status-warning", msg, anchor)
580 def status_skipped(self, msg, anchor=None):
581 """Write a note to the log file describing skipped test(s).
584 msg: A message describing the skipped test(s).
585 anchor: Optional internal link target.
591 self._note("status-skipped", msg, anchor)
593 def status_xfail(self, msg, anchor=None):
594 """Write a note to the log file describing xfailed test(s).
597 msg: A message describing the xfailed test(s).
598 anchor: Optional internal link target.
604 self._note("status-xfail", msg, anchor)
606 def status_xpass(self, msg, anchor=None):
607 """Write a note to the log file describing xpassed test(s).
610 msg: A message describing the xpassed test(s).
611 anchor: Optional internal link target.
617 self._note("status-xpass", msg, anchor)
619 def status_fail(self, msg, anchor=None):
620 """Write a note to the log file describing failed test(s).
623 msg: A message describing the failed test(s).
624 anchor: Optional internal link target.
630 self._note("status-fail", msg, anchor)
632 def get_stream(self, name, chained_file=None):
633 """Create an object to log a single stream's data into the log file.
635 This creates a "file-like" object that can be written to in order to
636 write a single stream's data to the log file. The implementation will
637 handle any required interleaving of data (from multiple streams) in
638 the log, in a way that makes it obvious which stream each bit of data
642 name: The name of the stream.
643 chained_file: The file-like object to which all stream data should
644 be logged to in addition to this log. Can be None.
650 return LogfileStream(self, name, chained_file)
652 def get_runner(self, name, chained_file=None):
653 """Create an object that executes processes and logs their output.
656 name: The name of this sub-process.
657 chained_file: The file-like object to which all stream data should
658 be logged to in addition to logfile. Can be None.
664 return RunAndLog(self, name, chained_file)
666 def write(self, stream, data, implicit=False):
667 """Write stream data into the log file.
669 This function should only be used by instances of LogfileStream or
673 stream: The stream whose data is being logged.
674 data: The data to log.
675 implicit: Boolean indicating whether data actually appeared in the
676 stream, or was implicitly generated. A valid use-case is to
677 repeat a shell prompt at the start of each separate log
678 section, which makes the log sections more readable in
685 if stream != self.last_stream:
686 self._terminate_stream()
687 self.f.write('<div class="stream block">\n')
688 self.f.write('<div class="stream-header block-header">Stream: ' +
689 stream.name + '</div>\n')
690 self.f.write('<div class="stream-content block-content">\n')
691 self.f.write('<pre>')
693 self.f.write('<span class="implicit">')
694 self.f.write(self._escape(data))
696 self.f.write('</span>')
697 self.last_stream = stream
700 """Flush the log stream, to ensure correct log interleaving.