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