xref: /openbmc/u-boot/test/py/multiplexed_log.py (revision 77b93e5e)
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 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
106    def close(self):
107        """Clean up any resources managed by this object."""
108        pass
109
110    def run(self, cmd, cwd=None, ignore_errors=False):
111        """Run a command as a sub-process, and log the results.
112
113        The output is available at self.output which can be useful if there is
114        an exception.
115
116        Args:
117            cmd: The command to execute.
118            cwd: The directory to run the command in. Can be None to use the
119                current directory.
120            ignore_errors: Indicate whether to ignore errors. If True, the
121                function will simply return if the command cannot be executed
122                or exits with an error code, otherwise an exception will be
123                raised if such problems occur.
124
125        Returns:
126            The output as a string.
127        """
128
129        msg = '+' + ' '.join(cmd) + '\n'
130        if self.chained_file:
131            self.chained_file.write(msg)
132        self.logfile.write(self, msg)
133
134        try:
135            p = subprocess.Popen(cmd, cwd=cwd,
136                stdin=None, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
137            (stdout, stderr) = p.communicate()
138            output = ''
139            if stdout:
140                if stderr:
141                    output += 'stdout:\n'
142                output += stdout
143            if stderr:
144                if stdout:
145                    output += 'stderr:\n'
146                output += stderr
147            exit_status = p.returncode
148            exception = None
149        except subprocess.CalledProcessError as cpe:
150            output = cpe.output
151            exit_status = cpe.returncode
152            exception = cpe
153        except Exception as e:
154            output = ''
155            exit_status = 0
156            exception = e
157        if output and not output.endswith('\n'):
158            output += '\n'
159        if exit_status and not exception and not ignore_errors:
160            exception = Exception('Exit code: ' + str(exit_status))
161        if exception:
162            output += str(exception) + '\n'
163        self.logfile.write(self, output)
164        if self.chained_file:
165            self.chained_file.write(output)
166
167        # Store the output so it can be accessed if we raise an exception.
168        self.output = output
169        if exception:
170            raise exception
171        return output
172
173class SectionCtxMgr(object):
174    """A context manager for Python's "with" statement, which allows a certain
175    portion of test code to be logged to a separate section of the log file.
176    Objects of this type should be created by factory functions in the Logfile
177    class rather than directly."""
178
179    def __init__(self, log, marker, anchor):
180        """Initialize a new object.
181
182        Args:
183            log: The Logfile object to log to.
184            marker: The name of the nested log section.
185            anchor: The anchor value to pass to start_section().
186
187        Returns:
188            Nothing.
189        """
190
191        self.log = log
192        self.marker = marker
193        self.anchor = anchor
194
195    def __enter__(self):
196        self.anchor = self.log.start_section(self.marker, self.anchor)
197
198    def __exit__(self, extype, value, traceback):
199        self.log.end_section(self.marker)
200
201class Logfile(object):
202    """Generates an HTML-formatted log file containing multiple streams of
203    data, each represented in a well-delineated/-structured fashion."""
204
205    def __init__(self, fn):
206        """Initialize a new object.
207
208        Args:
209            fn: The filename to write to.
210
211        Returns:
212            Nothing.
213        """
214
215        self.f = open(fn, 'wt')
216        self.last_stream = None
217        self.blocks = []
218        self.cur_evt = 1
219        self.anchor = 0
220
221        shutil.copy(mod_dir + '/multiplexed_log.css', os.path.dirname(fn))
222        self.f.write('''\
223<html>
224<head>
225<link rel="stylesheet" type="text/css" href="multiplexed_log.css">
226<script src="http://code.jquery.com/jquery.min.js"></script>
227<script>
228$(document).ready(function () {
229    // Copy status report HTML to start of log for easy access
230    sts = $(".block#status_report")[0].outerHTML;
231    $("tt").prepend(sts);
232
233    // Add expand/contract buttons to all block headers
234    btns = "<span class=\\\"block-expand hidden\\\">[+] </span>" +
235        "<span class=\\\"block-contract\\\">[-] </span>";
236    $(".block-header").prepend(btns);
237
238    // Pre-contract all blocks which passed, leaving only problem cases
239    // expanded, to highlight issues the user should look at.
240    // Only top-level blocks (sections) should have any status
241    passed_bcs = $(".block-content:has(.status-pass)");
242    // Some blocks might have multiple status entries (e.g. the status
243    // report), so take care not to hide blocks with partial success.
244    passed_bcs = passed_bcs.not(":has(.status-fail)");
245    passed_bcs = passed_bcs.not(":has(.status-xfail)");
246    passed_bcs = passed_bcs.not(":has(.status-xpass)");
247    passed_bcs = passed_bcs.not(":has(.status-skipped)");
248    // Hide the passed blocks
249    passed_bcs.addClass("hidden");
250    // Flip the expand/contract button hiding for those blocks.
251    bhs = passed_bcs.parent().children(".block-header")
252    bhs.children(".block-expand").removeClass("hidden");
253    bhs.children(".block-contract").addClass("hidden");
254
255    // Add click handler to block headers.
256    // The handler expands/contracts the block.
257    $(".block-header").on("click", function (e) {
258        var header = $(this);
259        var content = header.next(".block-content");
260        var expanded = !content.hasClass("hidden");
261        if (expanded) {
262            content.addClass("hidden");
263            header.children(".block-expand").first().removeClass("hidden");
264            header.children(".block-contract").first().addClass("hidden");
265        } else {
266            header.children(".block-contract").first().removeClass("hidden");
267            header.children(".block-expand").first().addClass("hidden");
268            content.removeClass("hidden");
269        }
270    });
271
272    // When clicking on a link, expand the target block
273    $("a").on("click", function (e) {
274        var block = $($(this).attr("href"));
275        var header = block.children(".block-header");
276        var content = block.children(".block-content").first();
277        header.children(".block-contract").first().removeClass("hidden");
278        header.children(".block-expand").first().addClass("hidden");
279        content.removeClass("hidden");
280    });
281});
282</script>
283</head>
284<body>
285<tt>
286''')
287
288    def close(self):
289        """Close the log file.
290
291        After calling this function, no more data may be written to the log.
292
293        Args:
294            None.
295
296        Returns:
297            Nothing.
298        """
299
300        self.f.write('''\
301</tt>
302</body>
303</html>
304''')
305        self.f.close()
306
307    # The set of characters that should be represented as hexadecimal codes in
308    # the log file.
309    _nonprint = ('%' + ''.join(chr(c) for c in range(0, 32) if c not in (9, 10)) +
310                 ''.join(chr(c) for c in range(127, 256)))
311
312    def _escape(self, data):
313        """Render data format suitable for inclusion in an HTML document.
314
315        This includes HTML-escaping certain characters, and translating
316        control characters to a hexadecimal representation.
317
318        Args:
319            data: The raw string data to be escaped.
320
321        Returns:
322            An escaped version of the data.
323        """
324
325        data = data.replace(chr(13), '')
326        data = ''.join((c in self._nonprint) and ('%%%02x' % ord(c)) or
327                       c for c in data)
328        data = cgi.escape(data)
329        return data
330
331    def _terminate_stream(self):
332        """Write HTML to the log file to terminate the current stream's data.
333
334        Args:
335            None.
336
337        Returns:
338            Nothing.
339        """
340
341        self.cur_evt += 1
342        if not self.last_stream:
343            return
344        self.f.write('</pre>\n')
345        self.f.write('<div class="stream-trailer block-trailer">End stream: ' +
346                     self.last_stream.name + '</div>\n')
347        self.f.write('</div>\n')
348        self.f.write('</div>\n')
349        self.last_stream = None
350
351    def _note(self, note_type, msg, anchor=None):
352        """Write a note or one-off message to the log file.
353
354        Args:
355            note_type: The type of note. This must be a value supported by the
356                accompanying multiplexed_log.css.
357            msg: The note/message to log.
358            anchor: Optional internal link target.
359
360        Returns:
361            Nothing.
362        """
363
364        self._terminate_stream()
365        self.f.write('<div class="' + note_type + '">\n')
366        if anchor:
367            self.f.write('<a href="#%s">\n' % anchor)
368        self.f.write('<pre>')
369        self.f.write(self._escape(msg))
370        self.f.write('\n</pre>\n')
371        if anchor:
372            self.f.write('</a>\n')
373        self.f.write('</div>\n')
374
375    def start_section(self, marker, anchor=None):
376        """Begin a new nested section in the log file.
377
378        Args:
379            marker: The name of the section that is starting.
380            anchor: The value to use for the anchor. If None, a unique value
381              will be calculated and used
382
383        Returns:
384            Name of the HTML anchor emitted before section.
385        """
386
387        self._terminate_stream()
388        self.blocks.append(marker)
389        if not anchor:
390            self.anchor += 1
391            anchor = str(self.anchor)
392        blk_path = '/'.join(self.blocks)
393        self.f.write('<div class="section block" id="' + anchor + '">\n')
394        self.f.write('<div class="section-header block-header">Section: ' +
395                     blk_path + '</div>\n')
396        self.f.write('<div class="section-content block-content">\n')
397
398        return anchor
399
400    def end_section(self, marker):
401        """Terminate the current nested section in the log file.
402
403        This function validates proper nesting of start_section() and
404        end_section() calls. If a mismatch is found, an exception is raised.
405
406        Args:
407            marker: The name of the section that is ending.
408
409        Returns:
410            Nothing.
411        """
412
413        if (not self.blocks) or (marker != self.blocks[-1]):
414            raise Exception('Block nesting mismatch: "%s" "%s"' %
415                            (marker, '/'.join(self.blocks)))
416        self._terminate_stream()
417        blk_path = '/'.join(self.blocks)
418        self.f.write('<div class="section-trailer block-trailer">' +
419                     'End section: ' + blk_path + '</div>\n')
420        self.f.write('</div>\n')
421        self.f.write('</div>\n')
422        self.blocks.pop()
423
424    def section(self, marker, anchor=None):
425        """Create a temporary section in the log file.
426
427        This function creates a context manager for Python's "with" statement,
428        which allows a certain portion of test code to be logged to a separate
429        section of the log file.
430
431        Usage:
432            with log.section("somename"):
433                some test code
434
435        Args:
436            marker: The name of the nested section.
437            anchor: The anchor value to pass to start_section().
438
439        Returns:
440            A context manager object.
441        """
442
443        return SectionCtxMgr(self, marker, anchor)
444
445    def error(self, msg):
446        """Write an error note to the log file.
447
448        Args:
449            msg: A message describing the error.
450
451        Returns:
452            Nothing.
453        """
454
455        self._note("error", msg)
456
457    def warning(self, msg):
458        """Write an warning note to the log file.
459
460        Args:
461            msg: A message describing the warning.
462
463        Returns:
464            Nothing.
465        """
466
467        self._note("warning", msg)
468
469    def info(self, msg):
470        """Write an informational note to the log file.
471
472        Args:
473            msg: An informational message.
474
475        Returns:
476            Nothing.
477        """
478
479        self._note("info", msg)
480
481    def action(self, msg):
482        """Write an action note to the log file.
483
484        Args:
485            msg: A message describing the action that is being logged.
486
487        Returns:
488            Nothing.
489        """
490
491        self._note("action", msg)
492
493    def status_pass(self, msg, anchor=None):
494        """Write a note to the log file describing test(s) which passed.
495
496        Args:
497            msg: A message describing the passed test(s).
498            anchor: Optional internal link target.
499
500        Returns:
501            Nothing.
502        """
503
504        self._note("status-pass", msg, anchor)
505
506    def status_skipped(self, msg, anchor=None):
507        """Write a note to the log file describing skipped test(s).
508
509        Args:
510            msg: A message describing the skipped test(s).
511            anchor: Optional internal link target.
512
513        Returns:
514            Nothing.
515        """
516
517        self._note("status-skipped", msg, anchor)
518
519    def status_xfail(self, msg, anchor=None):
520        """Write a note to the log file describing xfailed test(s).
521
522        Args:
523            msg: A message describing the xfailed test(s).
524            anchor: Optional internal link target.
525
526        Returns:
527            Nothing.
528        """
529
530        self._note("status-xfail", msg, anchor)
531
532    def status_xpass(self, msg, anchor=None):
533        """Write a note to the log file describing xpassed test(s).
534
535        Args:
536            msg: A message describing the xpassed test(s).
537            anchor: Optional internal link target.
538
539        Returns:
540            Nothing.
541        """
542
543        self._note("status-xpass", msg, anchor)
544
545    def status_fail(self, msg, anchor=None):
546        """Write a note to the log file describing failed test(s).
547
548        Args:
549            msg: A message describing the failed test(s).
550            anchor: Optional internal link target.
551
552        Returns:
553            Nothing.
554        """
555
556        self._note("status-fail", msg, anchor)
557
558    def get_stream(self, name, chained_file=None):
559        """Create an object to log a single stream's data into the log file.
560
561        This creates a "file-like" object that can be written to in order to
562        write a single stream's data to the log file. The implementation will
563        handle any required interleaving of data (from multiple streams) in
564        the log, in a way that makes it obvious which stream each bit of data
565        came from.
566
567        Args:
568            name: The name of the stream.
569            chained_file: The file-like object to which all stream data should
570                be logged to in addition to this log. Can be None.
571
572        Returns:
573            A file-like object.
574        """
575
576        return LogfileStream(self, name, chained_file)
577
578    def get_runner(self, name, chained_file=None):
579        """Create an object that executes processes and logs their output.
580
581        Args:
582            name: The name of this sub-process.
583            chained_file: The file-like object to which all stream data should
584                be logged to in addition to logfile. Can be None.
585
586        Returns:
587            A RunAndLog object.
588        """
589
590        return RunAndLog(self, name, chained_file)
591
592    def write(self, stream, data, implicit=False):
593        """Write stream data into the log file.
594
595        This function should only be used by instances of LogfileStream or
596        RunAndLog.
597
598        Args:
599            stream: The stream whose data is being logged.
600            data: The data to log.
601            implicit: Boolean indicating whether data actually appeared in the
602                stream, or was implicitly generated. A valid use-case is to
603                repeat a shell prompt at the start of each separate log
604                section, which makes the log sections more readable in
605                isolation.
606
607        Returns:
608            Nothing.
609        """
610
611        if stream != self.last_stream:
612            self._terminate_stream()
613            self.f.write('<div class="stream block">\n')
614            self.f.write('<div class="stream-header block-header">Stream: ' +
615                         stream.name + '</div>\n')
616            self.f.write('<div class="stream-content block-content">\n')
617            self.f.write('<pre>')
618        if implicit:
619            self.f.write('<span class="implicit">')
620        self.f.write(self._escape(data))
621        if implicit:
622            self.f.write('</span>')
623        self.last_stream = stream
624
625    def flush(self):
626        """Flush the log stream, to ensure correct log interleaving.
627
628        Args:
629            None.
630
631        Returns:
632            Nothing.
633        """
634
635        self.f.flush()
636