xref: /openbmc/u-boot/test/py/multiplexed_log.py (revision ae485b54)
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
8import cgi
9import datetime
10import os.path
11import shutil
12import subprocess
13
14mod_dir = os.path.dirname(os.path.abspath(__file__))
15
16class 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
83class 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
176class 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
204class 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 = ('%' + ''.join(chr(c) for c in range(0, 32) if c not in (9, 10)) +
318                 ''.join(chr(c) for c in range(127, 256)))
319
320    def _escape(self, data):
321        """Render data format suitable for inclusion in an HTML document.
322
323        This includes HTML-escaping certain characters, and translating
324        control characters to a hexadecimal representation.
325
326        Args:
327            data: The raw string data to be escaped.
328
329        Returns:
330            An escaped version of the data.
331        """
332
333        data = data.replace(chr(13), '')
334        data = ''.join((c in self._nonprint) and ('%%%02x' % ord(c)) or
335                       c for c in data)
336        data = cgi.escape(data)
337        return data
338
339    def _terminate_stream(self):
340        """Write HTML to the log file to terminate the current stream's data.
341
342        Args:
343            None.
344
345        Returns:
346            Nothing.
347        """
348
349        self.cur_evt += 1
350        if not self.last_stream:
351            return
352        self.f.write('</pre>\n')
353        self.f.write('<div class="stream-trailer block-trailer">End stream: ' +
354                     self.last_stream.name + '</div>\n')
355        self.f.write('</div>\n')
356        self.f.write('</div>\n')
357        self.last_stream = None
358
359    def _note(self, note_type, msg, anchor=None):
360        """Write a note or one-off message to the log file.
361
362        Args:
363            note_type: The type of note. This must be a value supported by the
364                accompanying multiplexed_log.css.
365            msg: The note/message to log.
366            anchor: Optional internal link target.
367
368        Returns:
369            Nothing.
370        """
371
372        self._terminate_stream()
373        self.f.write('<div class="' + note_type + '">\n')
374        self.f.write('<pre>')
375        if anchor:
376            self.f.write('<a href="#%s">' % anchor)
377        self.f.write(self._escape(msg))
378        if anchor:
379            self.f.write('</a>')
380        self.f.write('\n</pre>\n')
381        self.f.write('</div>\n')
382
383    def start_section(self, marker, anchor=None):
384        """Begin a new nested section in the log file.
385
386        Args:
387            marker: The name of the section that is starting.
388            anchor: The value to use for the anchor. If None, a unique value
389              will be calculated and used
390
391        Returns:
392            Name of the HTML anchor emitted before section.
393        """
394
395        self._terminate_stream()
396        self.blocks.append(marker)
397        self.timestamp_blocks.append(self._get_time())
398        if not anchor:
399            self.anchor += 1
400            anchor = str(self.anchor)
401        blk_path = '/'.join(self.blocks)
402        self.f.write('<div class="section block" id="' + anchor + '">\n')
403        self.f.write('<div class="section-header block-header">Section: ' +
404                     blk_path + '</div>\n')
405        self.f.write('<div class="section-content block-content">\n')
406        self.timestamp()
407
408        return anchor
409
410    def end_section(self, marker):
411        """Terminate the current nested section in the log file.
412
413        This function validates proper nesting of start_section() and
414        end_section() calls. If a mismatch is found, an exception is raised.
415
416        Args:
417            marker: The name of the section that is ending.
418
419        Returns:
420            Nothing.
421        """
422
423        if (not self.blocks) or (marker != self.blocks[-1]):
424            raise Exception('Block nesting mismatch: "%s" "%s"' %
425                            (marker, '/'.join(self.blocks)))
426        self._terminate_stream()
427        timestamp_now = self._get_time()
428        timestamp_section_start = self.timestamp_blocks.pop()
429        delta_section = timestamp_now - timestamp_section_start
430        self._note("timestamp",
431            "TIME: SINCE-SECTION: " + str(delta_section))
432        blk_path = '/'.join(self.blocks)
433        self.f.write('<div class="section-trailer block-trailer">' +
434                     'End section: ' + blk_path + '</div>\n')
435        self.f.write('</div>\n')
436        self.f.write('</div>\n')
437        self.blocks.pop()
438
439    def section(self, marker, anchor=None):
440        """Create a temporary section in the log file.
441
442        This function creates a context manager for Python's "with" statement,
443        which allows a certain portion of test code to be logged to a separate
444        section of the log file.
445
446        Usage:
447            with log.section("somename"):
448                some test code
449
450        Args:
451            marker: The name of the nested section.
452            anchor: The anchor value to pass to start_section().
453
454        Returns:
455            A context manager object.
456        """
457
458        return SectionCtxMgr(self, marker, anchor)
459
460    def error(self, msg):
461        """Write an error note to the log file.
462
463        Args:
464            msg: A message describing the error.
465
466        Returns:
467            Nothing.
468        """
469
470        self._note("error", msg)
471
472    def warning(self, msg):
473        """Write an warning note to the log file.
474
475        Args:
476            msg: A message describing the warning.
477
478        Returns:
479            Nothing.
480        """
481
482        self.seen_warning = True
483        self._note("warning", msg)
484
485    def get_and_reset_warning(self):
486        """Get and reset the log warning flag.
487
488        Args:
489            None
490
491        Returns:
492            Whether a warning was seen since the last call.
493        """
494
495        ret = self.seen_warning
496        self.seen_warning = False
497        return ret
498
499    def info(self, msg):
500        """Write an informational note to the log file.
501
502        Args:
503            msg: An informational message.
504
505        Returns:
506            Nothing.
507        """
508
509        self._note("info", msg)
510
511    def action(self, msg):
512        """Write an action note to the log file.
513
514        Args:
515            msg: A message describing the action that is being logged.
516
517        Returns:
518            Nothing.
519        """
520
521        self._note("action", msg)
522
523    def _get_time(self):
524        return datetime.datetime.now()
525
526    def timestamp(self):
527        """Write a timestamp to the log file.
528
529        Args:
530            None
531
532        Returns:
533            Nothing.
534        """
535
536        timestamp_now = self._get_time()
537        delta_prev = timestamp_now - self.timestamp_prev
538        delta_start = timestamp_now - self.timestamp_start
539        self.timestamp_prev = timestamp_now
540
541        self._note("timestamp",
542            "TIME: NOW: " + timestamp_now.strftime("%Y/%m/%d %H:%M:%S.%f"))
543        self._note("timestamp",
544            "TIME: SINCE-PREV: " + str(delta_prev))
545        self._note("timestamp",
546            "TIME: SINCE-START: " + str(delta_start))
547
548    def status_pass(self, msg, anchor=None):
549        """Write a note to the log file describing test(s) which passed.
550
551        Args:
552            msg: A message describing the passed test(s).
553            anchor: Optional internal link target.
554
555        Returns:
556            Nothing.
557        """
558
559        self._note("status-pass", msg, anchor)
560
561    def status_warning(self, msg, anchor=None):
562        """Write a note to the log file describing test(s) which passed.
563
564        Args:
565            msg: A message describing the passed test(s).
566            anchor: Optional internal link target.
567
568        Returns:
569            Nothing.
570        """
571
572        self._note("status-warning", msg, anchor)
573
574    def status_skipped(self, msg, anchor=None):
575        """Write a note to the log file describing skipped test(s).
576
577        Args:
578            msg: A message describing the skipped test(s).
579            anchor: Optional internal link target.
580
581        Returns:
582            Nothing.
583        """
584
585        self._note("status-skipped", msg, anchor)
586
587    def status_xfail(self, msg, anchor=None):
588        """Write a note to the log file describing xfailed test(s).
589
590        Args:
591            msg: A message describing the xfailed test(s).
592            anchor: Optional internal link target.
593
594        Returns:
595            Nothing.
596        """
597
598        self._note("status-xfail", msg, anchor)
599
600    def status_xpass(self, msg, anchor=None):
601        """Write a note to the log file describing xpassed test(s).
602
603        Args:
604            msg: A message describing the xpassed test(s).
605            anchor: Optional internal link target.
606
607        Returns:
608            Nothing.
609        """
610
611        self._note("status-xpass", msg, anchor)
612
613    def status_fail(self, msg, anchor=None):
614        """Write a note to the log file describing failed test(s).
615
616        Args:
617            msg: A message describing the failed test(s).
618            anchor: Optional internal link target.
619
620        Returns:
621            Nothing.
622        """
623
624        self._note("status-fail", msg, anchor)
625
626    def get_stream(self, name, chained_file=None):
627        """Create an object to log a single stream's data into the log file.
628
629        This creates a "file-like" object that can be written to in order to
630        write a single stream's data to the log file. The implementation will
631        handle any required interleaving of data (from multiple streams) in
632        the log, in a way that makes it obvious which stream each bit of data
633        came from.
634
635        Args:
636            name: The name of the stream.
637            chained_file: The file-like object to which all stream data should
638                be logged to in addition to this log. Can be None.
639
640        Returns:
641            A file-like object.
642        """
643
644        return LogfileStream(self, name, chained_file)
645
646    def get_runner(self, name, chained_file=None):
647        """Create an object that executes processes and logs their output.
648
649        Args:
650            name: The name of this sub-process.
651            chained_file: The file-like object to which all stream data should
652                be logged to in addition to logfile. Can be None.
653
654        Returns:
655            A RunAndLog object.
656        """
657
658        return RunAndLog(self, name, chained_file)
659
660    def write(self, stream, data, implicit=False):
661        """Write stream data into the log file.
662
663        This function should only be used by instances of LogfileStream or
664        RunAndLog.
665
666        Args:
667            stream: The stream whose data is being logged.
668            data: The data to log.
669            implicit: Boolean indicating whether data actually appeared in the
670                stream, or was implicitly generated. A valid use-case is to
671                repeat a shell prompt at the start of each separate log
672                section, which makes the log sections more readable in
673                isolation.
674
675        Returns:
676            Nothing.
677        """
678
679        if stream != self.last_stream:
680            self._terminate_stream()
681            self.f.write('<div class="stream block">\n')
682            self.f.write('<div class="stream-header block-header">Stream: ' +
683                         stream.name + '</div>\n')
684            self.f.write('<div class="stream-content block-content">\n')
685            self.f.write('<pre>')
686        if implicit:
687            self.f.write('<span class="implicit">')
688        self.f.write(self._escape(data))
689        if implicit:
690            self.f.write('</span>')
691        self.last_stream = stream
692
693    def flush(self):
694        """Flush the log stream, to ensure correct log interleaving.
695
696        Args:
697            None.
698
699        Returns:
700            Nothing.
701        """
702
703        self.f.flush()
704