xref: /openbmc/u-boot/test/py/multiplexed_log.py (revision e246b728)
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 = {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