xref: /openbmc/u-boot/test/py/multiplexed_log.py (revision 3d5ced9e22d32112a20f9dc0f5fb1f22ef088079)
1 # SPDX-License-Identifier: GPL-2.0
2 # Copyright (c) 2015 Stephen Warren
3 # Copyright (c) 2015-2016, NVIDIA CORPORATION. All rights reserved.
4 
5 # Generate an HTML-formatted log file containing multiple streams of data,
6 # each represented in a well-delineated/-structured fashion.
7 
8 import cgi
9 import datetime
10 import os.path
11 import shutil
12 import subprocess
13 
14 mod_dir = os.path.dirname(os.path.abspath(__file__))
15 
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."""
20 
21     def __init__(self, logfile, name, chained_file):
22         """Initialize a new object.
23 
24         Args:
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.
29 
30         Returns:
31             Nothing.
32         """
33 
34         self.logfile = logfile
35         self.name = name
36         self.chained_file = chained_file
37 
38     def close(self):
39         """Dummy function so that this class is "file-like".
40 
41         Args:
42             None.
43 
44         Returns:
45             Nothing.
46         """
47 
48         pass
49 
50     def write(self, data, implicit=False):
51         """Write data to the log stream.
52 
53         Args:
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
59                 isolation.
60 
61         Returns:
62             Nothing.
63         """
64 
65         self.logfile.write(self, data, implicit)
66         if self.chained_file:
67             self.chained_file.write(data)
68 
69     def flush(self):
70         """Flush the log stream, to ensure correct log interleaving.
71 
72         Args:
73             None.
74 
75         Returns:
76             Nothing.
77         """
78 
79         self.logfile.flush()
80         if self.chained_file:
81             self.chained_file.flush()
82 
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."""
87 
88     def __init__(self, logfile, name, chained_file):
89         """Initialize a new object.
90 
91         Args:
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.
96 
97         Returns:
98             Nothing.
99         """
100 
101         self.logfile = logfile
102         self.name = name
103         self.chained_file = chained_file
104         self.output = None
105         self.exit_status = None
106 
107     def close(self):
108         """Clean up any resources managed by this object."""
109         pass
110 
111     def run(self, cmd, cwd=None, ignore_errors=False):
112         """Run a command as a sub-process, and log the results.
113 
114         The output is available at self.output which can be useful if there is
115         an exception.
116 
117         Args:
118             cmd: The command to execute.
119             cwd: The directory to run the command in. Can be None to use the
120                 current directory.
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.
125 
126         Returns:
127             The output as a string.
128         """
129 
130         msg = '+' + ' '.join(cmd) + '\n'
131         if self.chained_file:
132             self.chained_file.write(msg)
133         self.logfile.write(self, msg)
134 
135         try:
136             p = subprocess.Popen(cmd, cwd=cwd,
137                 stdin=None, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
138             (stdout, stderr) = p.communicate()
139             output = ''
140             if stdout:
141                 if stderr:
142                     output += 'stdout:\n'
143                 output += stdout
144             if stderr:
145                 if stdout:
146                     output += 'stderr:\n'
147                 output += stderr
148             exit_status = p.returncode
149             exception = None
150         except subprocess.CalledProcessError as cpe:
151             output = cpe.output
152             exit_status = cpe.returncode
153             exception = cpe
154         except Exception as e:
155             output = ''
156             exit_status = 0
157             exception = e
158         if output and not output.endswith('\n'):
159             output += '\n'
160         if exit_status and not exception and not ignore_errors:
161             exception = Exception('Exit code: ' + str(exit_status))
162         if exception:
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()
168 
169         # Store the output so it can be accessed if we raise an exception.
170         self.output = output
171         self.exit_status = exit_status
172         if exception:
173             raise exception
174         return output
175 
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."""
181 
182     def __init__(self, log, marker, anchor):
183         """Initialize a new object.
184 
185         Args:
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().
189 
190         Returns:
191             Nothing.
192         """
193 
194         self.log = log
195         self.marker = marker
196         self.anchor = anchor
197 
198     def __enter__(self):
199         self.anchor = self.log.start_section(self.marker, self.anchor)
200 
201     def __exit__(self, extype, value, traceback):
202         self.log.end_section(self.marker)
203 
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."""
207 
208     def __init__(self, fn):
209         """Initialize a new object.
210 
211         Args:
212             fn: The filename to write to.
213 
214         Returns:
215             Nothing.
216         """
217 
218         self.f = open(fn, 'wt')
219         self.last_stream = None
220         self.blocks = []
221         self.cur_evt = 1
222         self.anchor = 0
223         self.timestamp_start = self._get_time()
224         self.timestamp_prev = self.timestamp_start
225         self.timestamp_blocks = []
226         self.seen_warning = False
227 
228         shutil.copy(mod_dir + '/multiplexed_log.css', os.path.dirname(fn))
229         self.f.write('''\
230 <html>
231 <head>
232 <link rel="stylesheet" type="text/css" href="multiplexed_log.css">
233 <script src="http://code.jquery.com/jquery.min.js"></script>
234 <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);
239 
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);
244 
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");
262 
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");
269         if (expanded) {
270             content.addClass("hidden");
271             header.children(".block-expand").first().removeClass("hidden");
272             header.children(".block-contract").first().addClass("hidden");
273         } else {
274             header.children(".block-contract").first().removeClass("hidden");
275             header.children(".block-expand").first().addClass("hidden");
276             content.removeClass("hidden");
277         }
278     });
279 
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");
288     });
289 });
290 </script>
291 </head>
292 <body>
293 <tt>
294 ''')
295 
296     def close(self):
297         """Close the log file.
298 
299         After calling this function, no more data may be written to the log.
300 
301         Args:
302             None.
303 
304         Returns:
305             Nothing.
306         """
307 
308         self.f.write('''\
309 </tt>
310 </body>
311 </html>
312 ''')
313         self.f.close()
314 
315     # The set of characters that should be represented as hexadecimal codes in
316     # the log file.
317     _nonprint = {ord('%')}
318     _nonprint.update({c for c in range(0, 32) if c not in (9, 10)})
319     _nonprint.update({c for c in range(127, 256)})
320 
321     def _escape(self, data):
322         """Render data format suitable for inclusion in an HTML document.
323 
324         This includes HTML-escaping certain characters, and translating
325         control characters to a hexadecimal representation.
326 
327         Args:
328             data: The raw string data to be escaped.
329 
330         Returns:
331             An escaped version of the data.
332         """
333 
334         data = data.replace(chr(13), '')
335         data = ''.join((ord(c) in self._nonprint) and ('%%%02x' % ord(c)) or
336                        c for c in data)
337         data = cgi.escape(data)
338         return data
339 
340     def _terminate_stream(self):
341         """Write HTML to the log file to terminate the current stream's data.
342 
343         Args:
344             None.
345 
346         Returns:
347             Nothing.
348         """
349 
350         self.cur_evt += 1
351         if not self.last_stream:
352             return
353         self.f.write('</pre>\n')
354         self.f.write('<div class="stream-trailer block-trailer">End stream: ' +
355                      self.last_stream.name + '</div>\n')
356         self.f.write('</div>\n')
357         self.f.write('</div>\n')
358         self.last_stream = None
359 
360     def _note(self, note_type, msg, anchor=None):
361         """Write a note or one-off message to the log file.
362 
363         Args:
364             note_type: The type of note. This must be a value supported by the
365                 accompanying multiplexed_log.css.
366             msg: The note/message to log.
367             anchor: Optional internal link target.
368 
369         Returns:
370             Nothing.
371         """
372 
373         self._terminate_stream()
374         self.f.write('<div class="' + note_type + '">\n')
375         self.f.write('<pre>')
376         if anchor:
377             self.f.write('<a href="#%s">' % anchor)
378         self.f.write(self._escape(msg))
379         if anchor:
380             self.f.write('</a>')
381         self.f.write('\n</pre>\n')
382         self.f.write('</div>\n')
383 
384     def start_section(self, marker, anchor=None):
385         """Begin a new nested section in the log file.
386 
387         Args:
388             marker: The name of the section that is starting.
389             anchor: The value to use for the anchor. If None, a unique value
390               will be calculated and used
391 
392         Returns:
393             Name of the HTML anchor emitted before section.
394         """
395 
396         self._terminate_stream()
397         self.blocks.append(marker)
398         self.timestamp_blocks.append(self._get_time())
399         if not anchor:
400             self.anchor += 1
401             anchor = str(self.anchor)
402         blk_path = '/'.join(self.blocks)
403         self.f.write('<div class="section block" id="' + anchor + '">\n')
404         self.f.write('<div class="section-header block-header">Section: ' +
405                      blk_path + '</div>\n')
406         self.f.write('<div class="section-content block-content">\n')
407         self.timestamp()
408 
409         return anchor
410 
411     def end_section(self, marker):
412         """Terminate the current nested section in the log file.
413 
414         This function validates proper nesting of start_section() and
415         end_section() calls. If a mismatch is found, an exception is raised.
416 
417         Args:
418             marker: The name of the section that is ending.
419 
420         Returns:
421             Nothing.
422         """
423 
424         if (not self.blocks) or (marker != self.blocks[-1]):
425             raise Exception('Block nesting mismatch: "%s" "%s"' %
426                             (marker, '/'.join(self.blocks)))
427         self._terminate_stream()
428         timestamp_now = self._get_time()
429         timestamp_section_start = self.timestamp_blocks.pop()
430         delta_section = timestamp_now - timestamp_section_start
431         self._note("timestamp",
432             "TIME: SINCE-SECTION: " + str(delta_section))
433         blk_path = '/'.join(self.blocks)
434         self.f.write('<div class="section-trailer block-trailer">' +
435                      'End section: ' + blk_path + '</div>\n')
436         self.f.write('</div>\n')
437         self.f.write('</div>\n')
438         self.blocks.pop()
439 
440     def section(self, marker, anchor=None):
441         """Create a temporary section in the log file.
442 
443         This function creates a context manager for Python's "with" statement,
444         which allows a certain portion of test code to be logged to a separate
445         section of the log file.
446 
447         Usage:
448             with log.section("somename"):
449                 some test code
450 
451         Args:
452             marker: The name of the nested section.
453             anchor: The anchor value to pass to start_section().
454 
455         Returns:
456             A context manager object.
457         """
458 
459         return SectionCtxMgr(self, marker, anchor)
460 
461     def error(self, msg):
462         """Write an error note to the log file.
463 
464         Args:
465             msg: A message describing the error.
466 
467         Returns:
468             Nothing.
469         """
470 
471         self._note("error", msg)
472 
473     def warning(self, msg):
474         """Write an warning note to the log file.
475 
476         Args:
477             msg: A message describing the warning.
478 
479         Returns:
480             Nothing.
481         """
482 
483         self.seen_warning = True
484         self._note("warning", msg)
485 
486     def get_and_reset_warning(self):
487         """Get and reset the log warning flag.
488 
489         Args:
490             None
491 
492         Returns:
493             Whether a warning was seen since the last call.
494         """
495 
496         ret = self.seen_warning
497         self.seen_warning = False
498         return ret
499 
500     def info(self, msg):
501         """Write an informational note to the log file.
502 
503         Args:
504             msg: An informational message.
505 
506         Returns:
507             Nothing.
508         """
509 
510         self._note("info", msg)
511 
512     def action(self, msg):
513         """Write an action note to the log file.
514 
515         Args:
516             msg: A message describing the action that is being logged.
517 
518         Returns:
519             Nothing.
520         """
521 
522         self._note("action", msg)
523 
524     def _get_time(self):
525         return datetime.datetime.now()
526 
527     def timestamp(self):
528         """Write a timestamp to the log file.
529 
530         Args:
531             None
532 
533         Returns:
534             Nothing.
535         """
536 
537         timestamp_now = self._get_time()
538         delta_prev = timestamp_now - self.timestamp_prev
539         delta_start = timestamp_now - self.timestamp_start
540         self.timestamp_prev = timestamp_now
541 
542         self._note("timestamp",
543             "TIME: NOW: " + timestamp_now.strftime("%Y/%m/%d %H:%M:%S.%f"))
544         self._note("timestamp",
545             "TIME: SINCE-PREV: " + str(delta_prev))
546         self._note("timestamp",
547             "TIME: SINCE-START: " + str(delta_start))
548 
549     def status_pass(self, msg, anchor=None):
550         """Write a note to the log file describing test(s) which passed.
551 
552         Args:
553             msg: A message describing the passed test(s).
554             anchor: Optional internal link target.
555 
556         Returns:
557             Nothing.
558         """
559 
560         self._note("status-pass", msg, anchor)
561 
562     def status_warning(self, msg, anchor=None):
563         """Write a note to the log file describing test(s) which passed.
564 
565         Args:
566             msg: A message describing the passed test(s).
567             anchor: Optional internal link target.
568 
569         Returns:
570             Nothing.
571         """
572 
573         self._note("status-warning", msg, anchor)
574 
575     def status_skipped(self, msg, anchor=None):
576         """Write a note to the log file describing skipped test(s).
577 
578         Args:
579             msg: A message describing the skipped test(s).
580             anchor: Optional internal link target.
581 
582         Returns:
583             Nothing.
584         """
585 
586         self._note("status-skipped", msg, anchor)
587 
588     def status_xfail(self, msg, anchor=None):
589         """Write a note to the log file describing xfailed test(s).
590 
591         Args:
592             msg: A message describing the xfailed test(s).
593             anchor: Optional internal link target.
594 
595         Returns:
596             Nothing.
597         """
598 
599         self._note("status-xfail", msg, anchor)
600 
601     def status_xpass(self, msg, anchor=None):
602         """Write a note to the log file describing xpassed test(s).
603 
604         Args:
605             msg: A message describing the xpassed test(s).
606             anchor: Optional internal link target.
607 
608         Returns:
609             Nothing.
610         """
611 
612         self._note("status-xpass", msg, anchor)
613 
614     def status_fail(self, msg, anchor=None):
615         """Write a note to the log file describing failed test(s).
616 
617         Args:
618             msg: A message describing the failed test(s).
619             anchor: Optional internal link target.
620 
621         Returns:
622             Nothing.
623         """
624 
625         self._note("status-fail", msg, anchor)
626 
627     def get_stream(self, name, chained_file=None):
628         """Create an object to log a single stream's data into the log file.
629 
630         This creates a "file-like" object that can be written to in order to
631         write a single stream's data to the log file. The implementation will
632         handle any required interleaving of data (from multiple streams) in
633         the log, in a way that makes it obvious which stream each bit of data
634         came from.
635 
636         Args:
637             name: The name of the stream.
638             chained_file: The file-like object to which all stream data should
639                 be logged to in addition to this log. Can be None.
640 
641         Returns:
642             A file-like object.
643         """
644 
645         return LogfileStream(self, name, chained_file)
646 
647     def get_runner(self, name, chained_file=None):
648         """Create an object that executes processes and logs their output.
649 
650         Args:
651             name: The name of this sub-process.
652             chained_file: The file-like object to which all stream data should
653                 be logged to in addition to logfile. Can be None.
654 
655         Returns:
656             A RunAndLog object.
657         """
658 
659         return RunAndLog(self, name, chained_file)
660 
661     def write(self, stream, data, implicit=False):
662         """Write stream data into the log file.
663 
664         This function should only be used by instances of LogfileStream or
665         RunAndLog.
666 
667         Args:
668             stream: The stream whose data is being logged.
669             data: The data to log.
670             implicit: Boolean indicating whether data actually appeared in the
671                 stream, or was implicitly generated. A valid use-case is to
672                 repeat a shell prompt at the start of each separate log
673                 section, which makes the log sections more readable in
674                 isolation.
675 
676         Returns:
677             Nothing.
678         """
679 
680         if stream != self.last_stream:
681             self._terminate_stream()
682             self.f.write('<div class="stream block">\n')
683             self.f.write('<div class="stream-header block-header">Stream: ' +
684                          stream.name + '</div>\n')
685             self.f.write('<div class="stream-content block-content">\n')
686             self.f.write('<pre>')
687         if implicit:
688             self.f.write('<span class="implicit">')
689         self.f.write(self._escape(data))
690         if implicit:
691             self.f.write('</span>')
692         self.last_stream = stream
693 
694     def flush(self):
695         """Flush the log stream, to ensure correct log interleaving.
696 
697         Args:
698             None.
699 
700         Returns:
701             Nothing.
702         """
703 
704         self.f.flush()
705