xref: /openbmc/u-boot/test/py/multiplexed_log.py (revision 9679d339ad2d4c495d734bad3a0fb7be6c4215eb)
1d201506cSStephen Warren# Copyright (c) 2015 Stephen Warren
2d201506cSStephen Warren# Copyright (c) 2015-2016, NVIDIA CORPORATION. All rights reserved.
3d201506cSStephen Warren#
4d201506cSStephen Warren# SPDX-License-Identifier: GPL-2.0
5d201506cSStephen Warren
6d201506cSStephen Warren# Generate an HTML-formatted log file containing multiple streams of data,
7d201506cSStephen Warren# each represented in a well-delineated/-structured fashion.
8d201506cSStephen Warren
9d201506cSStephen Warrenimport cgi
10*9679d339SStephen Warrenimport datetime
11d201506cSStephen Warrenimport os.path
12d201506cSStephen Warrenimport shutil
13d201506cSStephen Warrenimport subprocess
14d201506cSStephen Warren
15d201506cSStephen Warrenmod_dir = os.path.dirname(os.path.abspath(__file__))
16d201506cSStephen Warren
17d201506cSStephen Warrenclass LogfileStream(object):
18e8debf39SStephen Warren    """A file-like object used to write a single logical stream of data into
19d201506cSStephen Warren    a multiplexed log file. Objects of this type should be created by factory
20e8debf39SStephen Warren    functions in the Logfile class rather than directly."""
21d201506cSStephen Warren
22d201506cSStephen Warren    def __init__(self, logfile, name, chained_file):
23e8debf39SStephen Warren        """Initialize a new object.
24d201506cSStephen Warren
25d201506cSStephen Warren        Args:
26d201506cSStephen Warren            logfile: The Logfile object to log to.
27d201506cSStephen Warren            name: The name of this log stream.
28d201506cSStephen Warren            chained_file: The file-like object to which all stream data should be
29d201506cSStephen Warren            logged to in addition to logfile. Can be None.
30d201506cSStephen Warren
31d201506cSStephen Warren        Returns:
32d201506cSStephen Warren            Nothing.
33e8debf39SStephen Warren        """
34d201506cSStephen Warren
35d201506cSStephen Warren        self.logfile = logfile
36d201506cSStephen Warren        self.name = name
37d201506cSStephen Warren        self.chained_file = chained_file
38d201506cSStephen Warren
39d201506cSStephen Warren    def close(self):
40e8debf39SStephen Warren        """Dummy function so that this class is "file-like".
41d201506cSStephen Warren
42d201506cSStephen Warren        Args:
43d201506cSStephen Warren            None.
44d201506cSStephen Warren
45d201506cSStephen Warren        Returns:
46d201506cSStephen Warren            Nothing.
47e8debf39SStephen Warren        """
48d201506cSStephen Warren
49d201506cSStephen Warren        pass
50d201506cSStephen Warren
51d201506cSStephen Warren    def write(self, data, implicit=False):
52e8debf39SStephen Warren        """Write data to the log stream.
53d201506cSStephen Warren
54d201506cSStephen Warren        Args:
55d201506cSStephen Warren            data: The data to write tot he file.
56d201506cSStephen Warren            implicit: Boolean indicating whether data actually appeared in the
57d201506cSStephen Warren                stream, or was implicitly generated. A valid use-case is to
58d201506cSStephen Warren                repeat a shell prompt at the start of each separate log
59d201506cSStephen Warren                section, which makes the log sections more readable in
60d201506cSStephen Warren                isolation.
61d201506cSStephen Warren
62d201506cSStephen Warren        Returns:
63d201506cSStephen Warren            Nothing.
64e8debf39SStephen Warren        """
65d201506cSStephen Warren
66d201506cSStephen Warren        self.logfile.write(self, data, implicit)
67d201506cSStephen Warren        if self.chained_file:
68d201506cSStephen Warren            self.chained_file.write(data)
69d201506cSStephen Warren
70d201506cSStephen Warren    def flush(self):
71e8debf39SStephen Warren        """Flush the log stream, to ensure correct log interleaving.
72d201506cSStephen Warren
73d201506cSStephen Warren        Args:
74d201506cSStephen Warren            None.
75d201506cSStephen Warren
76d201506cSStephen Warren        Returns:
77d201506cSStephen Warren            Nothing.
78e8debf39SStephen Warren        """
79d201506cSStephen Warren
80d201506cSStephen Warren        self.logfile.flush()
81d201506cSStephen Warren        if self.chained_file:
82d201506cSStephen Warren            self.chained_file.flush()
83d201506cSStephen Warren
84d201506cSStephen Warrenclass RunAndLog(object):
85e8debf39SStephen Warren    """A utility object used to execute sub-processes and log their output to
86d201506cSStephen Warren    a multiplexed log file. Objects of this type should be created by factory
87e8debf39SStephen Warren    functions in the Logfile class rather than directly."""
88d201506cSStephen Warren
89d201506cSStephen Warren    def __init__(self, logfile, name, chained_file):
90e8debf39SStephen Warren        """Initialize a new object.
91d201506cSStephen Warren
92d201506cSStephen Warren        Args:
93d201506cSStephen Warren            logfile: The Logfile object to log to.
94d201506cSStephen Warren            name: The name of this log stream or sub-process.
95d201506cSStephen Warren            chained_file: The file-like object to which all stream data should
96d201506cSStephen Warren                be logged to in addition to logfile. Can be None.
97d201506cSStephen Warren
98d201506cSStephen Warren        Returns:
99d201506cSStephen Warren            Nothing.
100e8debf39SStephen Warren        """
101d201506cSStephen Warren
102d201506cSStephen Warren        self.logfile = logfile
103d201506cSStephen Warren        self.name = name
104d201506cSStephen Warren        self.chained_file = chained_file
10586845bf3SSimon Glass        self.output = None
1067f64b187SSimon Glass        self.exit_status = None
107d201506cSStephen Warren
108d201506cSStephen Warren    def close(self):
109e8debf39SStephen Warren        """Clean up any resources managed by this object."""
110d201506cSStephen Warren        pass
111d201506cSStephen Warren
1123f2faf73SStephen Warren    def run(self, cmd, cwd=None, ignore_errors=False):
113e8debf39SStephen Warren        """Run a command as a sub-process, and log the results.
114d201506cSStephen Warren
11586845bf3SSimon Glass        The output is available at self.output which can be useful if there is
11686845bf3SSimon Glass        an exception.
11786845bf3SSimon Glass
118d201506cSStephen Warren        Args:
119d201506cSStephen Warren            cmd: The command to execute.
120d201506cSStephen Warren            cwd: The directory to run the command in. Can be None to use the
121d201506cSStephen Warren                current directory.
1223f2faf73SStephen Warren            ignore_errors: Indicate whether to ignore errors. If True, the
1233f2faf73SStephen Warren                function will simply return if the command cannot be executed
1243f2faf73SStephen Warren                or exits with an error code, otherwise an exception will be
1253f2faf73SStephen Warren                raised if such problems occur.
126d201506cSStephen Warren
127d201506cSStephen Warren        Returns:
1283b8d9d97SSimon Glass            The output as a string.
129e8debf39SStephen Warren        """
130d201506cSStephen Warren
131a2ec5606SStephen Warren        msg = '+' + ' '.join(cmd) + '\n'
132d201506cSStephen Warren        if self.chained_file:
133d201506cSStephen Warren            self.chained_file.write(msg)
134d201506cSStephen Warren        self.logfile.write(self, msg)
135d201506cSStephen Warren
136d201506cSStephen Warren        try:
137d201506cSStephen Warren            p = subprocess.Popen(cmd, cwd=cwd,
138d201506cSStephen Warren                stdin=None, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
139d201506cSStephen Warren            (stdout, stderr) = p.communicate()
140d201506cSStephen Warren            output = ''
141d201506cSStephen Warren            if stdout:
142d201506cSStephen Warren                if stderr:
143d201506cSStephen Warren                    output += 'stdout:\n'
144d201506cSStephen Warren                output += stdout
145d201506cSStephen Warren            if stderr:
146d201506cSStephen Warren                if stdout:
147d201506cSStephen Warren                    output += 'stderr:\n'
148d201506cSStephen Warren                output += stderr
149d201506cSStephen Warren            exit_status = p.returncode
150d201506cSStephen Warren            exception = None
151d201506cSStephen Warren        except subprocess.CalledProcessError as cpe:
152d201506cSStephen Warren            output = cpe.output
153d201506cSStephen Warren            exit_status = cpe.returncode
154d201506cSStephen Warren            exception = cpe
155d201506cSStephen Warren        except Exception as e:
156d201506cSStephen Warren            output = ''
157d201506cSStephen Warren            exit_status = 0
158d201506cSStephen Warren            exception = e
159d201506cSStephen Warren        if output and not output.endswith('\n'):
160d201506cSStephen Warren            output += '\n'
1613f2faf73SStephen Warren        if exit_status and not exception and not ignore_errors:
162d201506cSStephen Warren            exception = Exception('Exit code: ' + str(exit_status))
163d201506cSStephen Warren        if exception:
164d201506cSStephen Warren            output += str(exception) + '\n'
165d201506cSStephen Warren        self.logfile.write(self, output)
166d201506cSStephen Warren        if self.chained_file:
167d201506cSStephen Warren            self.chained_file.write(output)
168*9679d339SStephen Warren        self.logfile.timestamp()
16986845bf3SSimon Glass
17086845bf3SSimon Glass        # Store the output so it can be accessed if we raise an exception.
17186845bf3SSimon Glass        self.output = output
1727f64b187SSimon Glass        self.exit_status = exit_status
173d201506cSStephen Warren        if exception:
174d201506cSStephen Warren            raise exception
1753b8d9d97SSimon Glass        return output
176d201506cSStephen Warren
177d201506cSStephen Warrenclass SectionCtxMgr(object):
178e8debf39SStephen Warren    """A context manager for Python's "with" statement, which allows a certain
179d201506cSStephen Warren    portion of test code to be logged to a separate section of the log file.
180d201506cSStephen Warren    Objects of this type should be created by factory functions in the Logfile
181e8debf39SStephen Warren    class rather than directly."""
182d201506cSStephen Warren
18383357fd5SStephen Warren    def __init__(self, log, marker, anchor):
184e8debf39SStephen Warren        """Initialize a new object.
185d201506cSStephen Warren
186d201506cSStephen Warren        Args:
187d201506cSStephen Warren            log: The Logfile object to log to.
188d201506cSStephen Warren            marker: The name of the nested log section.
18983357fd5SStephen Warren            anchor: The anchor value to pass to start_section().
190d201506cSStephen Warren
191d201506cSStephen Warren        Returns:
192d201506cSStephen Warren            Nothing.
193e8debf39SStephen Warren        """
194d201506cSStephen Warren
195d201506cSStephen Warren        self.log = log
196d201506cSStephen Warren        self.marker = marker
19783357fd5SStephen Warren        self.anchor = anchor
198d201506cSStephen Warren
199d201506cSStephen Warren    def __enter__(self):
20083357fd5SStephen Warren        self.anchor = self.log.start_section(self.marker, self.anchor)
201d201506cSStephen Warren
202d201506cSStephen Warren    def __exit__(self, extype, value, traceback):
203d201506cSStephen Warren        self.log.end_section(self.marker)
204d201506cSStephen Warren
205d201506cSStephen Warrenclass Logfile(object):
206e8debf39SStephen Warren    """Generates an HTML-formatted log file containing multiple streams of
207e8debf39SStephen Warren    data, each represented in a well-delineated/-structured fashion."""
208d201506cSStephen Warren
209d201506cSStephen Warren    def __init__(self, fn):
210e8debf39SStephen Warren        """Initialize a new object.
211d201506cSStephen Warren
212d201506cSStephen Warren        Args:
213d201506cSStephen Warren            fn: The filename to write to.
214d201506cSStephen Warren
215d201506cSStephen Warren        Returns:
216d201506cSStephen Warren            Nothing.
217e8debf39SStephen Warren        """
218d201506cSStephen Warren
219a2ec5606SStephen Warren        self.f = open(fn, 'wt')
220d201506cSStephen Warren        self.last_stream = None
221d201506cSStephen Warren        self.blocks = []
222d201506cSStephen Warren        self.cur_evt = 1
22383357fd5SStephen Warren        self.anchor = 0
224*9679d339SStephen Warren        self.timestamp_start = self._get_time()
225*9679d339SStephen Warren        self.timestamp_prev = self.timestamp_start
226*9679d339SStephen Warren        self.timestamp_blocks = []
22783357fd5SStephen Warren
228a2ec5606SStephen Warren        shutil.copy(mod_dir + '/multiplexed_log.css', os.path.dirname(fn))
229a2ec5606SStephen Warren        self.f.write('''\
230d201506cSStephen Warren<html>
231d201506cSStephen Warren<head>
232d201506cSStephen Warren<link rel="stylesheet" type="text/css" href="multiplexed_log.css">
23383357fd5SStephen Warren<script src="http://code.jquery.com/jquery.min.js"></script>
23483357fd5SStephen Warren<script>
23583357fd5SStephen Warren$(document).ready(function () {
23683357fd5SStephen Warren    // Copy status report HTML to start of log for easy access
23783357fd5SStephen Warren    sts = $(".block#status_report")[0].outerHTML;
23883357fd5SStephen Warren    $("tt").prepend(sts);
23983357fd5SStephen Warren
24083357fd5SStephen Warren    // Add expand/contract buttons to all block headers
24183357fd5SStephen Warren    btns = "<span class=\\\"block-expand hidden\\\">[+] </span>" +
24283357fd5SStephen Warren        "<span class=\\\"block-contract\\\">[-] </span>";
24383357fd5SStephen Warren    $(".block-header").prepend(btns);
24483357fd5SStephen Warren
24583357fd5SStephen Warren    // Pre-contract all blocks which passed, leaving only problem cases
24683357fd5SStephen Warren    // expanded, to highlight issues the user should look at.
24783357fd5SStephen Warren    // Only top-level blocks (sections) should have any status
24883357fd5SStephen Warren    passed_bcs = $(".block-content:has(.status-pass)");
24983357fd5SStephen Warren    // Some blocks might have multiple status entries (e.g. the status
25083357fd5SStephen Warren    // report), so take care not to hide blocks with partial success.
25183357fd5SStephen Warren    passed_bcs = passed_bcs.not(":has(.status-fail)");
25283357fd5SStephen Warren    passed_bcs = passed_bcs.not(":has(.status-xfail)");
25383357fd5SStephen Warren    passed_bcs = passed_bcs.not(":has(.status-xpass)");
25483357fd5SStephen Warren    passed_bcs = passed_bcs.not(":has(.status-skipped)");
25583357fd5SStephen Warren    // Hide the passed blocks
25683357fd5SStephen Warren    passed_bcs.addClass("hidden");
25783357fd5SStephen Warren    // Flip the expand/contract button hiding for those blocks.
25883357fd5SStephen Warren    bhs = passed_bcs.parent().children(".block-header")
25983357fd5SStephen Warren    bhs.children(".block-expand").removeClass("hidden");
26083357fd5SStephen Warren    bhs.children(".block-contract").addClass("hidden");
26183357fd5SStephen Warren
26283357fd5SStephen Warren    // Add click handler to block headers.
26383357fd5SStephen Warren    // The handler expands/contracts the block.
26483357fd5SStephen Warren    $(".block-header").on("click", function (e) {
26583357fd5SStephen Warren        var header = $(this);
26683357fd5SStephen Warren        var content = header.next(".block-content");
26783357fd5SStephen Warren        var expanded = !content.hasClass("hidden");
26883357fd5SStephen Warren        if (expanded) {
26983357fd5SStephen Warren            content.addClass("hidden");
27083357fd5SStephen Warren            header.children(".block-expand").first().removeClass("hidden");
27183357fd5SStephen Warren            header.children(".block-contract").first().addClass("hidden");
27283357fd5SStephen Warren        } else {
27383357fd5SStephen Warren            header.children(".block-contract").first().removeClass("hidden");
27483357fd5SStephen Warren            header.children(".block-expand").first().addClass("hidden");
27583357fd5SStephen Warren            content.removeClass("hidden");
27683357fd5SStephen Warren        }
27783357fd5SStephen Warren    });
27883357fd5SStephen Warren
27983357fd5SStephen Warren    // When clicking on a link, expand the target block
28083357fd5SStephen Warren    $("a").on("click", function (e) {
28183357fd5SStephen Warren        var block = $($(this).attr("href"));
28283357fd5SStephen Warren        var header = block.children(".block-header");
28383357fd5SStephen Warren        var content = block.children(".block-content").first();
28483357fd5SStephen Warren        header.children(".block-contract").first().removeClass("hidden");
28583357fd5SStephen Warren        header.children(".block-expand").first().addClass("hidden");
28683357fd5SStephen Warren        content.removeClass("hidden");
28783357fd5SStephen Warren    });
28883357fd5SStephen Warren});
28983357fd5SStephen Warren</script>
290d201506cSStephen Warren</head>
291d201506cSStephen Warren<body>
292d201506cSStephen Warren<tt>
293a2ec5606SStephen Warren''')
294d201506cSStephen Warren
295d201506cSStephen Warren    def close(self):
296e8debf39SStephen Warren        """Close the log file.
297d201506cSStephen Warren
298d201506cSStephen Warren        After calling this function, no more data may be written to the log.
299d201506cSStephen Warren
300d201506cSStephen Warren        Args:
301d201506cSStephen Warren            None.
302d201506cSStephen Warren
303d201506cSStephen Warren        Returns:
304d201506cSStephen Warren            Nothing.
305e8debf39SStephen Warren        """
306d201506cSStephen Warren
307a2ec5606SStephen Warren        self.f.write('''\
308d201506cSStephen Warren</tt>
309d201506cSStephen Warren</body>
310d201506cSStephen Warren</html>
311a2ec5606SStephen Warren''')
312d201506cSStephen Warren        self.f.close()
313d201506cSStephen Warren
314d201506cSStephen Warren    # The set of characters that should be represented as hexadecimal codes in
315d201506cSStephen Warren    # the log file.
316a2ec5606SStephen Warren    _nonprint = ('%' + ''.join(chr(c) for c in range(0, 32) if c not in (9, 10)) +
317a2ec5606SStephen Warren                 ''.join(chr(c) for c in range(127, 256)))
318d201506cSStephen Warren
319d201506cSStephen Warren    def _escape(self, data):
320e8debf39SStephen Warren        """Render data format suitable for inclusion in an HTML document.
321d201506cSStephen Warren
322d201506cSStephen Warren        This includes HTML-escaping certain characters, and translating
323d201506cSStephen Warren        control characters to a hexadecimal representation.
324d201506cSStephen Warren
325d201506cSStephen Warren        Args:
326d201506cSStephen Warren            data: The raw string data to be escaped.
327d201506cSStephen Warren
328d201506cSStephen Warren        Returns:
329d201506cSStephen Warren            An escaped version of the data.
330e8debf39SStephen Warren        """
331d201506cSStephen Warren
332a2ec5606SStephen Warren        data = data.replace(chr(13), '')
333a2ec5606SStephen Warren        data = ''.join((c in self._nonprint) and ('%%%02x' % ord(c)) or
334d201506cSStephen Warren                       c for c in data)
335d201506cSStephen Warren        data = cgi.escape(data)
336d201506cSStephen Warren        return data
337d201506cSStephen Warren
338d201506cSStephen Warren    def _terminate_stream(self):
339e8debf39SStephen Warren        """Write HTML to the log file to terminate the current stream's data.
340d201506cSStephen Warren
341d201506cSStephen Warren        Args:
342d201506cSStephen Warren            None.
343d201506cSStephen Warren
344d201506cSStephen Warren        Returns:
345d201506cSStephen Warren            Nothing.
346e8debf39SStephen Warren        """
347d201506cSStephen Warren
348d201506cSStephen Warren        self.cur_evt += 1
349d201506cSStephen Warren        if not self.last_stream:
350d201506cSStephen Warren            return
351a2ec5606SStephen Warren        self.f.write('</pre>\n')
35283357fd5SStephen Warren        self.f.write('<div class="stream-trailer block-trailer">End stream: ' +
353a2ec5606SStephen Warren                     self.last_stream.name + '</div>\n')
354a2ec5606SStephen Warren        self.f.write('</div>\n')
35583357fd5SStephen Warren        self.f.write('</div>\n')
356d201506cSStephen Warren        self.last_stream = None
357d201506cSStephen Warren
35883357fd5SStephen Warren    def _note(self, note_type, msg, anchor=None):
359e8debf39SStephen Warren        """Write a note or one-off message to the log file.
360d201506cSStephen Warren
361d201506cSStephen Warren        Args:
362d201506cSStephen Warren            note_type: The type of note. This must be a value supported by the
363d201506cSStephen Warren                accompanying multiplexed_log.css.
364d201506cSStephen Warren            msg: The note/message to log.
36583357fd5SStephen Warren            anchor: Optional internal link target.
366d201506cSStephen Warren
367d201506cSStephen Warren        Returns:
368d201506cSStephen Warren            Nothing.
369e8debf39SStephen Warren        """
370d201506cSStephen Warren
371d201506cSStephen Warren        self._terminate_stream()
37283357fd5SStephen Warren        self.f.write('<div class="' + note_type + '">\n')
37383357fd5SStephen Warren        self.f.write('<pre>')
37483357fd5SStephen Warren        if anchor:
375117eeb7fSStephen Warren            self.f.write('<a href="#%s">' % anchor)
376117eeb7fSStephen Warren        self.f.write(self._escape(msg))
377117eeb7fSStephen Warren        if anchor:
378117eeb7fSStephen Warren            self.f.write('</a>')
379117eeb7fSStephen Warren        self.f.write('\n</pre>\n')
38083357fd5SStephen Warren        self.f.write('</div>\n')
381d201506cSStephen Warren
38283357fd5SStephen Warren    def start_section(self, marker, anchor=None):
383e8debf39SStephen Warren        """Begin a new nested section in the log file.
384d201506cSStephen Warren
385d201506cSStephen Warren        Args:
386d201506cSStephen Warren            marker: The name of the section that is starting.
38783357fd5SStephen Warren            anchor: The value to use for the anchor. If None, a unique value
38883357fd5SStephen Warren              will be calculated and used
389d201506cSStephen Warren
390d201506cSStephen Warren        Returns:
39183357fd5SStephen Warren            Name of the HTML anchor emitted before section.
392e8debf39SStephen Warren        """
393d201506cSStephen Warren
394d201506cSStephen Warren        self._terminate_stream()
395d201506cSStephen Warren        self.blocks.append(marker)
396*9679d339SStephen Warren        self.timestamp_blocks.append(self._get_time())
39783357fd5SStephen Warren        if not anchor:
39883357fd5SStephen Warren            self.anchor += 1
39983357fd5SStephen Warren            anchor = str(self.anchor)
400a2ec5606SStephen Warren        blk_path = '/'.join(self.blocks)
40183357fd5SStephen Warren        self.f.write('<div class="section block" id="' + anchor + '">\n')
40283357fd5SStephen Warren        self.f.write('<div class="section-header block-header">Section: ' +
40383357fd5SStephen Warren                     blk_path + '</div>\n')
40483357fd5SStephen Warren        self.f.write('<div class="section-content block-content">\n')
405*9679d339SStephen Warren        self.timestamp()
40683357fd5SStephen Warren
40783357fd5SStephen Warren        return anchor
408d201506cSStephen Warren
409d201506cSStephen Warren    def end_section(self, marker):
410e8debf39SStephen Warren        """Terminate the current nested section in the log file.
411d201506cSStephen Warren
412d201506cSStephen Warren        This function validates proper nesting of start_section() and
413d201506cSStephen Warren        end_section() calls. If a mismatch is found, an exception is raised.
414d201506cSStephen Warren
415d201506cSStephen Warren        Args:
416d201506cSStephen Warren            marker: The name of the section that is ending.
417d201506cSStephen Warren
418d201506cSStephen Warren        Returns:
419d201506cSStephen Warren            Nothing.
420e8debf39SStephen Warren        """
421d201506cSStephen Warren
422d201506cSStephen Warren        if (not self.blocks) or (marker != self.blocks[-1]):
423a2ec5606SStephen Warren            raise Exception('Block nesting mismatch: "%s" "%s"' %
424a2ec5606SStephen Warren                            (marker, '/'.join(self.blocks)))
425d201506cSStephen Warren        self._terminate_stream()
426*9679d339SStephen Warren        timestamp_now = self._get_time()
427*9679d339SStephen Warren        timestamp_section_start = self.timestamp_blocks.pop()
428*9679d339SStephen Warren        delta_section = timestamp_now - timestamp_section_start
429*9679d339SStephen Warren        self._note("timestamp",
430*9679d339SStephen Warren            "TIME: SINCE-SECTION: " + str(delta_section))
431a2ec5606SStephen Warren        blk_path = '/'.join(self.blocks)
43283357fd5SStephen Warren        self.f.write('<div class="section-trailer block-trailer">' +
43383357fd5SStephen Warren                     'End section: ' + blk_path + '</div>\n')
43483357fd5SStephen Warren        self.f.write('</div>\n')
435a2ec5606SStephen Warren        self.f.write('</div>\n')
436d201506cSStephen Warren        self.blocks.pop()
437d201506cSStephen Warren
43883357fd5SStephen Warren    def section(self, marker, anchor=None):
439e8debf39SStephen Warren        """Create a temporary section in the log file.
440d201506cSStephen Warren
441d201506cSStephen Warren        This function creates a context manager for Python's "with" statement,
442d201506cSStephen Warren        which allows a certain portion of test code to be logged to a separate
443d201506cSStephen Warren        section of the log file.
444d201506cSStephen Warren
445d201506cSStephen Warren        Usage:
446d201506cSStephen Warren            with log.section("somename"):
447d201506cSStephen Warren                some test code
448d201506cSStephen Warren
449d201506cSStephen Warren        Args:
450d201506cSStephen Warren            marker: The name of the nested section.
45183357fd5SStephen Warren            anchor: The anchor value to pass to start_section().
452d201506cSStephen Warren
453d201506cSStephen Warren        Returns:
454d201506cSStephen Warren            A context manager object.
455e8debf39SStephen Warren        """
456d201506cSStephen Warren
45783357fd5SStephen Warren        return SectionCtxMgr(self, marker, anchor)
458d201506cSStephen Warren
459d201506cSStephen Warren    def error(self, msg):
460e8debf39SStephen Warren        """Write an error note to the log file.
461d201506cSStephen Warren
462d201506cSStephen Warren        Args:
463d201506cSStephen Warren            msg: A message describing the error.
464d201506cSStephen Warren
465d201506cSStephen Warren        Returns:
466d201506cSStephen Warren            Nothing.
467e8debf39SStephen Warren        """
468d201506cSStephen Warren
469d201506cSStephen Warren        self._note("error", msg)
470d201506cSStephen Warren
471d201506cSStephen Warren    def warning(self, msg):
472e8debf39SStephen Warren        """Write an warning note to the log file.
473d201506cSStephen Warren
474d201506cSStephen Warren        Args:
475d201506cSStephen Warren            msg: A message describing the warning.
476d201506cSStephen Warren
477d201506cSStephen Warren        Returns:
478d201506cSStephen Warren            Nothing.
479e8debf39SStephen Warren        """
480d201506cSStephen Warren
481d201506cSStephen Warren        self._note("warning", msg)
482d201506cSStephen Warren
483d201506cSStephen Warren    def info(self, msg):
484e8debf39SStephen Warren        """Write an informational note to the log file.
485d201506cSStephen Warren
486d201506cSStephen Warren        Args:
487d201506cSStephen Warren            msg: An informational message.
488d201506cSStephen Warren
489d201506cSStephen Warren        Returns:
490d201506cSStephen Warren            Nothing.
491e8debf39SStephen Warren        """
492d201506cSStephen Warren
493d201506cSStephen Warren        self._note("info", msg)
494d201506cSStephen Warren
495d201506cSStephen Warren    def action(self, msg):
496e8debf39SStephen Warren        """Write an action note to the log file.
497d201506cSStephen Warren
498d201506cSStephen Warren        Args:
499d201506cSStephen Warren            msg: A message describing the action that is being logged.
500d201506cSStephen Warren
501d201506cSStephen Warren        Returns:
502d201506cSStephen Warren            Nothing.
503e8debf39SStephen Warren        """
504d201506cSStephen Warren
505d201506cSStephen Warren        self._note("action", msg)
506d201506cSStephen Warren
507*9679d339SStephen Warren    def _get_time(self):
508*9679d339SStephen Warren        return datetime.datetime.now()
509*9679d339SStephen Warren
510*9679d339SStephen Warren    def timestamp(self):
511*9679d339SStephen Warren        """Write a timestamp to the log file.
512*9679d339SStephen Warren
513*9679d339SStephen Warren        Args:
514*9679d339SStephen Warren            None
515*9679d339SStephen Warren
516*9679d339SStephen Warren        Returns:
517*9679d339SStephen Warren            Nothing.
518*9679d339SStephen Warren        """
519*9679d339SStephen Warren
520*9679d339SStephen Warren        timestamp_now = self._get_time()
521*9679d339SStephen Warren        delta_prev = timestamp_now - self.timestamp_prev
522*9679d339SStephen Warren        delta_start = timestamp_now - self.timestamp_start
523*9679d339SStephen Warren        self.timestamp_prev = timestamp_now
524*9679d339SStephen Warren
525*9679d339SStephen Warren        self._note("timestamp",
526*9679d339SStephen Warren            "TIME: NOW: " + timestamp_now.strftime("%Y/%m/%d %H:%M:%S.%f"))
527*9679d339SStephen Warren        self._note("timestamp",
528*9679d339SStephen Warren            "TIME: SINCE-PREV: " + str(delta_prev))
529*9679d339SStephen Warren        self._note("timestamp",
530*9679d339SStephen Warren            "TIME: SINCE-START: " + str(delta_start))
531*9679d339SStephen Warren
53283357fd5SStephen Warren    def status_pass(self, msg, anchor=None):
533e8debf39SStephen Warren        """Write a note to the log file describing test(s) which passed.
534d201506cSStephen Warren
535d201506cSStephen Warren        Args:
53678b39cc3SStephen Warren            msg: A message describing the passed test(s).
53783357fd5SStephen Warren            anchor: Optional internal link target.
538d201506cSStephen Warren
539d201506cSStephen Warren        Returns:
540d201506cSStephen Warren            Nothing.
541e8debf39SStephen Warren        """
542d201506cSStephen Warren
54383357fd5SStephen Warren        self._note("status-pass", msg, anchor)
544d201506cSStephen Warren
54583357fd5SStephen Warren    def status_skipped(self, msg, anchor=None):
546e8debf39SStephen Warren        """Write a note to the log file describing skipped test(s).
547d201506cSStephen Warren
548d201506cSStephen Warren        Args:
54978b39cc3SStephen Warren            msg: A message describing the skipped test(s).
55083357fd5SStephen Warren            anchor: Optional internal link target.
551d201506cSStephen Warren
552d201506cSStephen Warren        Returns:
553d201506cSStephen Warren            Nothing.
554e8debf39SStephen Warren        """
555d201506cSStephen Warren
55683357fd5SStephen Warren        self._note("status-skipped", msg, anchor)
557d201506cSStephen Warren
55883357fd5SStephen Warren    def status_xfail(self, msg, anchor=None):
55978b39cc3SStephen Warren        """Write a note to the log file describing xfailed test(s).
56078b39cc3SStephen Warren
56178b39cc3SStephen Warren        Args:
56278b39cc3SStephen Warren            msg: A message describing the xfailed test(s).
56383357fd5SStephen Warren            anchor: Optional internal link target.
56478b39cc3SStephen Warren
56578b39cc3SStephen Warren        Returns:
56678b39cc3SStephen Warren            Nothing.
56778b39cc3SStephen Warren        """
56878b39cc3SStephen Warren
56983357fd5SStephen Warren        self._note("status-xfail", msg, anchor)
57078b39cc3SStephen Warren
57183357fd5SStephen Warren    def status_xpass(self, msg, anchor=None):
57278b39cc3SStephen Warren        """Write a note to the log file describing xpassed test(s).
57378b39cc3SStephen Warren
57478b39cc3SStephen Warren        Args:
57578b39cc3SStephen Warren            msg: A message describing the xpassed test(s).
57683357fd5SStephen Warren            anchor: Optional internal link target.
57778b39cc3SStephen Warren
57878b39cc3SStephen Warren        Returns:
57978b39cc3SStephen Warren            Nothing.
58078b39cc3SStephen Warren        """
58178b39cc3SStephen Warren
58283357fd5SStephen Warren        self._note("status-xpass", msg, anchor)
58378b39cc3SStephen Warren
58483357fd5SStephen Warren    def status_fail(self, msg, anchor=None):
585e8debf39SStephen Warren        """Write a note to the log file describing failed test(s).
586d201506cSStephen Warren
587d201506cSStephen Warren        Args:
58878b39cc3SStephen Warren            msg: A message describing the failed test(s).
58983357fd5SStephen Warren            anchor: Optional internal link target.
590d201506cSStephen Warren
591d201506cSStephen Warren        Returns:
592d201506cSStephen Warren            Nothing.
593e8debf39SStephen Warren        """
594d201506cSStephen Warren
59583357fd5SStephen Warren        self._note("status-fail", msg, anchor)
596d201506cSStephen Warren
597d201506cSStephen Warren    def get_stream(self, name, chained_file=None):
598e8debf39SStephen Warren        """Create an object to log a single stream's data into the log file.
599d201506cSStephen Warren
600d201506cSStephen Warren        This creates a "file-like" object that can be written to in order to
601d201506cSStephen Warren        write a single stream's data to the log file. The implementation will
602d201506cSStephen Warren        handle any required interleaving of data (from multiple streams) in
603d201506cSStephen Warren        the log, in a way that makes it obvious which stream each bit of data
604d201506cSStephen Warren        came from.
605d201506cSStephen Warren
606d201506cSStephen Warren        Args:
607d201506cSStephen Warren            name: The name of the stream.
608d201506cSStephen Warren            chained_file: The file-like object to which all stream data should
609d201506cSStephen Warren                be logged to in addition to this log. Can be None.
610d201506cSStephen Warren
611d201506cSStephen Warren        Returns:
612d201506cSStephen Warren            A file-like object.
613e8debf39SStephen Warren        """
614d201506cSStephen Warren
615d201506cSStephen Warren        return LogfileStream(self, name, chained_file)
616d201506cSStephen Warren
617d201506cSStephen Warren    def get_runner(self, name, chained_file=None):
618e8debf39SStephen Warren        """Create an object that executes processes and logs their output.
619d201506cSStephen Warren
620d201506cSStephen Warren        Args:
621d201506cSStephen Warren            name: The name of this sub-process.
622d201506cSStephen Warren            chained_file: The file-like object to which all stream data should
623d201506cSStephen Warren                be logged to in addition to logfile. Can be None.
624d201506cSStephen Warren
625d201506cSStephen Warren        Returns:
626d201506cSStephen Warren            A RunAndLog object.
627e8debf39SStephen Warren        """
628d201506cSStephen Warren
629d201506cSStephen Warren        return RunAndLog(self, name, chained_file)
630d201506cSStephen Warren
631d201506cSStephen Warren    def write(self, stream, data, implicit=False):
632e8debf39SStephen Warren        """Write stream data into the log file.
633d201506cSStephen Warren
634d201506cSStephen Warren        This function should only be used by instances of LogfileStream or
635d201506cSStephen Warren        RunAndLog.
636d201506cSStephen Warren
637d201506cSStephen Warren        Args:
638d201506cSStephen Warren            stream: The stream whose data is being logged.
639d201506cSStephen Warren            data: The data to log.
640d201506cSStephen Warren            implicit: Boolean indicating whether data actually appeared in the
641d201506cSStephen Warren                stream, or was implicitly generated. A valid use-case is to
642d201506cSStephen Warren                repeat a shell prompt at the start of each separate log
643d201506cSStephen Warren                section, which makes the log sections more readable in
644d201506cSStephen Warren                isolation.
645d201506cSStephen Warren
646d201506cSStephen Warren        Returns:
647d201506cSStephen Warren            Nothing.
648e8debf39SStephen Warren        """
649d201506cSStephen Warren
650d201506cSStephen Warren        if stream != self.last_stream:
651d201506cSStephen Warren            self._terminate_stream()
65283357fd5SStephen Warren            self.f.write('<div class="stream block">\n')
65383357fd5SStephen Warren            self.f.write('<div class="stream-header block-header">Stream: ' +
65483357fd5SStephen Warren                         stream.name + '</div>\n')
65583357fd5SStephen Warren            self.f.write('<div class="stream-content block-content">\n')
656a2ec5606SStephen Warren            self.f.write('<pre>')
657d201506cSStephen Warren        if implicit:
658a2ec5606SStephen Warren            self.f.write('<span class="implicit">')
659d201506cSStephen Warren        self.f.write(self._escape(data))
660d201506cSStephen Warren        if implicit:
661a2ec5606SStephen Warren            self.f.write('</span>')
662d201506cSStephen Warren        self.last_stream = stream
663d201506cSStephen Warren
664d201506cSStephen Warren    def flush(self):
665e8debf39SStephen Warren        """Flush the log stream, to ensure correct log interleaving.
666d201506cSStephen Warren
667d201506cSStephen Warren        Args:
668d201506cSStephen Warren            None.
669d201506cSStephen Warren
670d201506cSStephen Warren        Returns:
671d201506cSStephen Warren            Nothing.
672e8debf39SStephen Warren        """
673d201506cSStephen Warren
674d201506cSStephen Warren        self.f.flush()
675