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