xref: /openbmc/u-boot/test/py/multiplexed_log.py (revision 83d290c56fab2d38cd1ab4c4cc7099559c1d5046)
1*83d290c5STom Rini# SPDX-License-Identifier: GPL-2.0
2d201506cSStephen Warren# Copyright (c) 2015 Stephen Warren
3d201506cSStephen Warren# Copyright (c) 2015-2016, NVIDIA CORPORATION. All rights reserved.
4d201506cSStephen Warren
5d201506cSStephen Warren# Generate an HTML-formatted log file containing multiple streams of data,
6d201506cSStephen Warren# each represented in a well-delineated/-structured fashion.
7d201506cSStephen Warren
8d201506cSStephen Warrenimport cgi
99679d339SStephen Warrenimport datetime
10d201506cSStephen Warrenimport os.path
11d201506cSStephen Warrenimport shutil
12d201506cSStephen Warrenimport subprocess
13d201506cSStephen Warren
14d201506cSStephen Warrenmod_dir = os.path.dirname(os.path.abspath(__file__))
15d201506cSStephen Warren
16d201506cSStephen Warrenclass LogfileStream(object):
17e8debf39SStephen Warren    """A file-like object used to write a single logical stream of data into
18d201506cSStephen Warren    a multiplexed log file. Objects of this type should be created by factory
19e8debf39SStephen Warren    functions in the Logfile class rather than directly."""
20d201506cSStephen Warren
21d201506cSStephen Warren    def __init__(self, logfile, name, chained_file):
22e8debf39SStephen Warren        """Initialize a new object.
23d201506cSStephen Warren
24d201506cSStephen Warren        Args:
25d201506cSStephen Warren            logfile: The Logfile object to log to.
26d201506cSStephen Warren            name: The name of this log stream.
27d201506cSStephen Warren            chained_file: The file-like object to which all stream data should be
28d201506cSStephen Warren            logged to in addition to logfile. Can be None.
29d201506cSStephen Warren
30d201506cSStephen Warren        Returns:
31d201506cSStephen Warren            Nothing.
32e8debf39SStephen Warren        """
33d201506cSStephen Warren
34d201506cSStephen Warren        self.logfile = logfile
35d201506cSStephen Warren        self.name = name
36d201506cSStephen Warren        self.chained_file = chained_file
37d201506cSStephen Warren
38d201506cSStephen Warren    def close(self):
39e8debf39SStephen Warren        """Dummy function so that this class is "file-like".
40d201506cSStephen Warren
41d201506cSStephen Warren        Args:
42d201506cSStephen Warren            None.
43d201506cSStephen Warren
44d201506cSStephen Warren        Returns:
45d201506cSStephen Warren            Nothing.
46e8debf39SStephen Warren        """
47d201506cSStephen Warren
48d201506cSStephen Warren        pass
49d201506cSStephen Warren
50d201506cSStephen Warren    def write(self, data, implicit=False):
51e8debf39SStephen Warren        """Write data to the log stream.
52d201506cSStephen Warren
53d201506cSStephen Warren        Args:
54d201506cSStephen Warren            data: The data to write tot he file.
55d201506cSStephen Warren            implicit: Boolean indicating whether data actually appeared in the
56d201506cSStephen Warren                stream, or was implicitly generated. A valid use-case is to
57d201506cSStephen Warren                repeat a shell prompt at the start of each separate log
58d201506cSStephen Warren                section, which makes the log sections more readable in
59d201506cSStephen Warren                isolation.
60d201506cSStephen Warren
61d201506cSStephen Warren        Returns:
62d201506cSStephen Warren            Nothing.
63e8debf39SStephen Warren        """
64d201506cSStephen Warren
65d201506cSStephen Warren        self.logfile.write(self, data, implicit)
66d201506cSStephen Warren        if self.chained_file:
67d201506cSStephen Warren            self.chained_file.write(data)
68d201506cSStephen Warren
69d201506cSStephen Warren    def flush(self):
70e8debf39SStephen Warren        """Flush the log stream, to ensure correct log interleaving.
71d201506cSStephen Warren
72d201506cSStephen Warren        Args:
73d201506cSStephen Warren            None.
74d201506cSStephen Warren
75d201506cSStephen Warren        Returns:
76d201506cSStephen Warren            Nothing.
77e8debf39SStephen Warren        """
78d201506cSStephen Warren
79d201506cSStephen Warren        self.logfile.flush()
80d201506cSStephen Warren        if self.chained_file:
81d201506cSStephen Warren            self.chained_file.flush()
82d201506cSStephen Warren
83d201506cSStephen Warrenclass RunAndLog(object):
84e8debf39SStephen Warren    """A utility object used to execute sub-processes and log their output to
85d201506cSStephen Warren    a multiplexed log file. Objects of this type should be created by factory
86e8debf39SStephen Warren    functions in the Logfile class rather than directly."""
87d201506cSStephen Warren
88d201506cSStephen Warren    def __init__(self, logfile, name, chained_file):
89e8debf39SStephen Warren        """Initialize a new object.
90d201506cSStephen Warren
91d201506cSStephen Warren        Args:
92d201506cSStephen Warren            logfile: The Logfile object to log to.
93d201506cSStephen Warren            name: The name of this log stream or sub-process.
94d201506cSStephen Warren            chained_file: The file-like object to which all stream data should
95d201506cSStephen Warren                be logged to in addition to logfile. Can be None.
96d201506cSStephen Warren
97d201506cSStephen Warren        Returns:
98d201506cSStephen Warren            Nothing.
99e8debf39SStephen Warren        """
100d201506cSStephen Warren
101d201506cSStephen Warren        self.logfile = logfile
102d201506cSStephen Warren        self.name = name
103d201506cSStephen Warren        self.chained_file = chained_file
10486845bf3SSimon Glass        self.output = None
1057f64b187SSimon Glass        self.exit_status = None
106d201506cSStephen Warren
107d201506cSStephen Warren    def close(self):
108e8debf39SStephen Warren        """Clean up any resources managed by this object."""
109d201506cSStephen Warren        pass
110d201506cSStephen Warren
1113f2faf73SStephen Warren    def run(self, cmd, cwd=None, ignore_errors=False):
112e8debf39SStephen Warren        """Run a command as a sub-process, and log the results.
113d201506cSStephen Warren
11486845bf3SSimon Glass        The output is available at self.output which can be useful if there is
11586845bf3SSimon Glass        an exception.
11686845bf3SSimon Glass
117d201506cSStephen Warren        Args:
118d201506cSStephen Warren            cmd: The command to execute.
119d201506cSStephen Warren            cwd: The directory to run the command in. Can be None to use the
120d201506cSStephen Warren                current directory.
1213f2faf73SStephen Warren            ignore_errors: Indicate whether to ignore errors. If True, the
1223f2faf73SStephen Warren                function will simply return if the command cannot be executed
1233f2faf73SStephen Warren                or exits with an error code, otherwise an exception will be
1243f2faf73SStephen Warren                raised if such problems occur.
125d201506cSStephen Warren
126d201506cSStephen Warren        Returns:
1273b8d9d97SSimon Glass            The output as a string.
128e8debf39SStephen Warren        """
129d201506cSStephen Warren
130a2ec5606SStephen Warren        msg = '+' + ' '.join(cmd) + '\n'
131d201506cSStephen Warren        if self.chained_file:
132d201506cSStephen Warren            self.chained_file.write(msg)
133d201506cSStephen Warren        self.logfile.write(self, msg)
134d201506cSStephen Warren
135d201506cSStephen Warren        try:
136d201506cSStephen Warren            p = subprocess.Popen(cmd, cwd=cwd,
137d201506cSStephen Warren                stdin=None, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
138d201506cSStephen Warren            (stdout, stderr) = p.communicate()
139d201506cSStephen Warren            output = ''
140d201506cSStephen Warren            if stdout:
141d201506cSStephen Warren                if stderr:
142d201506cSStephen Warren                    output += 'stdout:\n'
143d201506cSStephen Warren                output += stdout
144d201506cSStephen Warren            if stderr:
145d201506cSStephen Warren                if stdout:
146d201506cSStephen Warren                    output += 'stderr:\n'
147d201506cSStephen Warren                output += stderr
148d201506cSStephen Warren            exit_status = p.returncode
149d201506cSStephen Warren            exception = None
150d201506cSStephen Warren        except subprocess.CalledProcessError as cpe:
151d201506cSStephen Warren            output = cpe.output
152d201506cSStephen Warren            exit_status = cpe.returncode
153d201506cSStephen Warren            exception = cpe
154d201506cSStephen Warren        except Exception as e:
155d201506cSStephen Warren            output = ''
156d201506cSStephen Warren            exit_status = 0
157d201506cSStephen Warren            exception = e
158d201506cSStephen Warren        if output and not output.endswith('\n'):
159d201506cSStephen Warren            output += '\n'
1603f2faf73SStephen Warren        if exit_status and not exception and not ignore_errors:
161d201506cSStephen Warren            exception = Exception('Exit code: ' + str(exit_status))
162d201506cSStephen Warren        if exception:
163d201506cSStephen Warren            output += str(exception) + '\n'
164d201506cSStephen Warren        self.logfile.write(self, output)
165d201506cSStephen Warren        if self.chained_file:
166d201506cSStephen Warren            self.chained_file.write(output)
1679679d339SStephen Warren        self.logfile.timestamp()
16886845bf3SSimon Glass
16986845bf3SSimon Glass        # Store the output so it can be accessed if we raise an exception.
17086845bf3SSimon Glass        self.output = output
1717f64b187SSimon Glass        self.exit_status = exit_status
172d201506cSStephen Warren        if exception:
173d201506cSStephen Warren            raise exception
1743b8d9d97SSimon Glass        return output
175d201506cSStephen Warren
176d201506cSStephen Warrenclass SectionCtxMgr(object):
177e8debf39SStephen Warren    """A context manager for Python's "with" statement, which allows a certain
178d201506cSStephen Warren    portion of test code to be logged to a separate section of the log file.
179d201506cSStephen Warren    Objects of this type should be created by factory functions in the Logfile
180e8debf39SStephen Warren    class rather than directly."""
181d201506cSStephen Warren
18283357fd5SStephen Warren    def __init__(self, log, marker, anchor):
183e8debf39SStephen Warren        """Initialize a new object.
184d201506cSStephen Warren
185d201506cSStephen Warren        Args:
186d201506cSStephen Warren            log: The Logfile object to log to.
187d201506cSStephen Warren            marker: The name of the nested log section.
18883357fd5SStephen Warren            anchor: The anchor value to pass to start_section().
189d201506cSStephen Warren
190d201506cSStephen Warren        Returns:
191d201506cSStephen Warren            Nothing.
192e8debf39SStephen Warren        """
193d201506cSStephen Warren
194d201506cSStephen Warren        self.log = log
195d201506cSStephen Warren        self.marker = marker
19683357fd5SStephen Warren        self.anchor = anchor
197d201506cSStephen Warren
198d201506cSStephen Warren    def __enter__(self):
19983357fd5SStephen Warren        self.anchor = self.log.start_section(self.marker, self.anchor)
200d201506cSStephen Warren
201d201506cSStephen Warren    def __exit__(self, extype, value, traceback):
202d201506cSStephen Warren        self.log.end_section(self.marker)
203d201506cSStephen Warren
204d201506cSStephen Warrenclass Logfile(object):
205e8debf39SStephen Warren    """Generates an HTML-formatted log file containing multiple streams of
206e8debf39SStephen Warren    data, each represented in a well-delineated/-structured fashion."""
207d201506cSStephen Warren
208d201506cSStephen Warren    def __init__(self, fn):
209e8debf39SStephen Warren        """Initialize a new object.
210d201506cSStephen Warren
211d201506cSStephen Warren        Args:
212d201506cSStephen Warren            fn: The filename to write to.
213d201506cSStephen Warren
214d201506cSStephen Warren        Returns:
215d201506cSStephen Warren            Nothing.
216e8debf39SStephen Warren        """
217d201506cSStephen Warren
218a2ec5606SStephen Warren        self.f = open(fn, 'wt')
219d201506cSStephen Warren        self.last_stream = None
220d201506cSStephen Warren        self.blocks = []
221d201506cSStephen Warren        self.cur_evt = 1
22283357fd5SStephen Warren        self.anchor = 0
2239679d339SStephen Warren        self.timestamp_start = self._get_time()
2249679d339SStephen Warren        self.timestamp_prev = self.timestamp_start
2259679d339SStephen Warren        self.timestamp_blocks = []
22632090e50SStephen Warren        self.seen_warning = False
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)");
25532090e50SStephen Warren    passed_bcs = passed_bcs.not(":has(.status-warning)");
25683357fd5SStephen Warren    // Hide the passed blocks
25783357fd5SStephen Warren    passed_bcs.addClass("hidden");
25883357fd5SStephen Warren    // Flip the expand/contract button hiding for those blocks.
25983357fd5SStephen Warren    bhs = passed_bcs.parent().children(".block-header")
26083357fd5SStephen Warren    bhs.children(".block-expand").removeClass("hidden");
26183357fd5SStephen Warren    bhs.children(".block-contract").addClass("hidden");
26283357fd5SStephen Warren
26383357fd5SStephen Warren    // Add click handler to block headers.
26483357fd5SStephen Warren    // The handler expands/contracts the block.
26583357fd5SStephen Warren    $(".block-header").on("click", function (e) {
26683357fd5SStephen Warren        var header = $(this);
26783357fd5SStephen Warren        var content = header.next(".block-content");
26883357fd5SStephen Warren        var expanded = !content.hasClass("hidden");
26983357fd5SStephen Warren        if (expanded) {
27083357fd5SStephen Warren            content.addClass("hidden");
27183357fd5SStephen Warren            header.children(".block-expand").first().removeClass("hidden");
27283357fd5SStephen Warren            header.children(".block-contract").first().addClass("hidden");
27383357fd5SStephen Warren        } else {
27483357fd5SStephen Warren            header.children(".block-contract").first().removeClass("hidden");
27583357fd5SStephen Warren            header.children(".block-expand").first().addClass("hidden");
27683357fd5SStephen Warren            content.removeClass("hidden");
27783357fd5SStephen Warren        }
27883357fd5SStephen Warren    });
27983357fd5SStephen Warren
28083357fd5SStephen Warren    // When clicking on a link, expand the target block
28183357fd5SStephen Warren    $("a").on("click", function (e) {
28283357fd5SStephen Warren        var block = $($(this).attr("href"));
28383357fd5SStephen Warren        var header = block.children(".block-header");
28483357fd5SStephen Warren        var content = block.children(".block-content").first();
28583357fd5SStephen Warren        header.children(".block-contract").first().removeClass("hidden");
28683357fd5SStephen Warren        header.children(".block-expand").first().addClass("hidden");
28783357fd5SStephen Warren        content.removeClass("hidden");
28883357fd5SStephen Warren    });
28983357fd5SStephen Warren});
29083357fd5SStephen Warren</script>
291d201506cSStephen Warren</head>
292d201506cSStephen Warren<body>
293d201506cSStephen Warren<tt>
294a2ec5606SStephen Warren''')
295d201506cSStephen Warren
296d201506cSStephen Warren    def close(self):
297e8debf39SStephen Warren        """Close the log file.
298d201506cSStephen Warren
299d201506cSStephen Warren        After calling this function, no more data may be written to the log.
300d201506cSStephen Warren
301d201506cSStephen Warren        Args:
302d201506cSStephen Warren            None.
303d201506cSStephen Warren
304d201506cSStephen Warren        Returns:
305d201506cSStephen Warren            Nothing.
306e8debf39SStephen Warren        """
307d201506cSStephen Warren
308a2ec5606SStephen Warren        self.f.write('''\
309d201506cSStephen Warren</tt>
310d201506cSStephen Warren</body>
311d201506cSStephen Warren</html>
312a2ec5606SStephen Warren''')
313d201506cSStephen Warren        self.f.close()
314d201506cSStephen Warren
315d201506cSStephen Warren    # The set of characters that should be represented as hexadecimal codes in
316d201506cSStephen Warren    # the log file.
317a2ec5606SStephen Warren    _nonprint = ('%' + ''.join(chr(c) for c in range(0, 32) if c not in (9, 10)) +
318a2ec5606SStephen Warren                 ''.join(chr(c) for c in range(127, 256)))
319d201506cSStephen Warren
320d201506cSStephen Warren    def _escape(self, data):
321e8debf39SStephen Warren        """Render data format suitable for inclusion in an HTML document.
322d201506cSStephen Warren
323d201506cSStephen Warren        This includes HTML-escaping certain characters, and translating
324d201506cSStephen Warren        control characters to a hexadecimal representation.
325d201506cSStephen Warren
326d201506cSStephen Warren        Args:
327d201506cSStephen Warren            data: The raw string data to be escaped.
328d201506cSStephen Warren
329d201506cSStephen Warren        Returns:
330d201506cSStephen Warren            An escaped version of the data.
331e8debf39SStephen Warren        """
332d201506cSStephen Warren
333a2ec5606SStephen Warren        data = data.replace(chr(13), '')
334a2ec5606SStephen Warren        data = ''.join((c in self._nonprint) and ('%%%02x' % ord(c)) or
335d201506cSStephen Warren                       c for c in data)
336d201506cSStephen Warren        data = cgi.escape(data)
337d201506cSStephen Warren        return data
338d201506cSStephen Warren
339d201506cSStephen Warren    def _terminate_stream(self):
340e8debf39SStephen Warren        """Write HTML to the log file to terminate the current stream's data.
341d201506cSStephen Warren
342d201506cSStephen Warren        Args:
343d201506cSStephen Warren            None.
344d201506cSStephen Warren
345d201506cSStephen Warren        Returns:
346d201506cSStephen Warren            Nothing.
347e8debf39SStephen Warren        """
348d201506cSStephen Warren
349d201506cSStephen Warren        self.cur_evt += 1
350d201506cSStephen Warren        if not self.last_stream:
351d201506cSStephen Warren            return
352a2ec5606SStephen Warren        self.f.write('</pre>\n')
35383357fd5SStephen Warren        self.f.write('<div class="stream-trailer block-trailer">End stream: ' +
354a2ec5606SStephen Warren                     self.last_stream.name + '</div>\n')
355a2ec5606SStephen Warren        self.f.write('</div>\n')
35683357fd5SStephen Warren        self.f.write('</div>\n')
357d201506cSStephen Warren        self.last_stream = None
358d201506cSStephen Warren
35983357fd5SStephen Warren    def _note(self, note_type, msg, anchor=None):
360e8debf39SStephen Warren        """Write a note or one-off message to the log file.
361d201506cSStephen Warren
362d201506cSStephen Warren        Args:
363d201506cSStephen Warren            note_type: The type of note. This must be a value supported by the
364d201506cSStephen Warren                accompanying multiplexed_log.css.
365d201506cSStephen Warren            msg: The note/message to log.
36683357fd5SStephen Warren            anchor: Optional internal link target.
367d201506cSStephen Warren
368d201506cSStephen Warren        Returns:
369d201506cSStephen Warren            Nothing.
370e8debf39SStephen Warren        """
371d201506cSStephen Warren
372d201506cSStephen Warren        self._terminate_stream()
37383357fd5SStephen Warren        self.f.write('<div class="' + note_type + '">\n')
37483357fd5SStephen Warren        self.f.write('<pre>')
37583357fd5SStephen Warren        if anchor:
376117eeb7fSStephen Warren            self.f.write('<a href="#%s">' % anchor)
377117eeb7fSStephen Warren        self.f.write(self._escape(msg))
378117eeb7fSStephen Warren        if anchor:
379117eeb7fSStephen Warren            self.f.write('</a>')
380117eeb7fSStephen Warren        self.f.write('\n</pre>\n')
38183357fd5SStephen Warren        self.f.write('</div>\n')
382d201506cSStephen Warren
38383357fd5SStephen Warren    def start_section(self, marker, anchor=None):
384e8debf39SStephen Warren        """Begin a new nested section in the log file.
385d201506cSStephen Warren
386d201506cSStephen Warren        Args:
387d201506cSStephen Warren            marker: The name of the section that is starting.
38883357fd5SStephen Warren            anchor: The value to use for the anchor. If None, a unique value
38983357fd5SStephen Warren              will be calculated and used
390d201506cSStephen Warren
391d201506cSStephen Warren        Returns:
39283357fd5SStephen Warren            Name of the HTML anchor emitted before section.
393e8debf39SStephen Warren        """
394d201506cSStephen Warren
395d201506cSStephen Warren        self._terminate_stream()
396d201506cSStephen Warren        self.blocks.append(marker)
3979679d339SStephen Warren        self.timestamp_blocks.append(self._get_time())
39883357fd5SStephen Warren        if not anchor:
39983357fd5SStephen Warren            self.anchor += 1
40083357fd5SStephen Warren            anchor = str(self.anchor)
401a2ec5606SStephen Warren        blk_path = '/'.join(self.blocks)
40283357fd5SStephen Warren        self.f.write('<div class="section block" id="' + anchor + '">\n')
40383357fd5SStephen Warren        self.f.write('<div class="section-header block-header">Section: ' +
40483357fd5SStephen Warren                     blk_path + '</div>\n')
40583357fd5SStephen Warren        self.f.write('<div class="section-content block-content">\n')
4069679d339SStephen Warren        self.timestamp()
40783357fd5SStephen Warren
40883357fd5SStephen Warren        return anchor
409d201506cSStephen Warren
410d201506cSStephen Warren    def end_section(self, marker):
411e8debf39SStephen Warren        """Terminate the current nested section in the log file.
412d201506cSStephen Warren
413d201506cSStephen Warren        This function validates proper nesting of start_section() and
414d201506cSStephen Warren        end_section() calls. If a mismatch is found, an exception is raised.
415d201506cSStephen Warren
416d201506cSStephen Warren        Args:
417d201506cSStephen Warren            marker: The name of the section that is ending.
418d201506cSStephen Warren
419d201506cSStephen Warren        Returns:
420d201506cSStephen Warren            Nothing.
421e8debf39SStephen Warren        """
422d201506cSStephen Warren
423d201506cSStephen Warren        if (not self.blocks) or (marker != self.blocks[-1]):
424a2ec5606SStephen Warren            raise Exception('Block nesting mismatch: "%s" "%s"' %
425a2ec5606SStephen Warren                            (marker, '/'.join(self.blocks)))
426d201506cSStephen Warren        self._terminate_stream()
4279679d339SStephen Warren        timestamp_now = self._get_time()
4289679d339SStephen Warren        timestamp_section_start = self.timestamp_blocks.pop()
4299679d339SStephen Warren        delta_section = timestamp_now - timestamp_section_start
4309679d339SStephen Warren        self._note("timestamp",
4319679d339SStephen Warren            "TIME: SINCE-SECTION: " + str(delta_section))
432a2ec5606SStephen Warren        blk_path = '/'.join(self.blocks)
43383357fd5SStephen Warren        self.f.write('<div class="section-trailer block-trailer">' +
43483357fd5SStephen Warren                     'End section: ' + blk_path + '</div>\n')
43583357fd5SStephen Warren        self.f.write('</div>\n')
436a2ec5606SStephen Warren        self.f.write('</div>\n')
437d201506cSStephen Warren        self.blocks.pop()
438d201506cSStephen Warren
43983357fd5SStephen Warren    def section(self, marker, anchor=None):
440e8debf39SStephen Warren        """Create a temporary section in the log file.
441d201506cSStephen Warren
442d201506cSStephen Warren        This function creates a context manager for Python's "with" statement,
443d201506cSStephen Warren        which allows a certain portion of test code to be logged to a separate
444d201506cSStephen Warren        section of the log file.
445d201506cSStephen Warren
446d201506cSStephen Warren        Usage:
447d201506cSStephen Warren            with log.section("somename"):
448d201506cSStephen Warren                some test code
449d201506cSStephen Warren
450d201506cSStephen Warren        Args:
451d201506cSStephen Warren            marker: The name of the nested section.
45283357fd5SStephen Warren            anchor: The anchor value to pass to start_section().
453d201506cSStephen Warren
454d201506cSStephen Warren        Returns:
455d201506cSStephen Warren            A context manager object.
456e8debf39SStephen Warren        """
457d201506cSStephen Warren
45883357fd5SStephen Warren        return SectionCtxMgr(self, marker, anchor)
459d201506cSStephen Warren
460d201506cSStephen Warren    def error(self, msg):
461e8debf39SStephen Warren        """Write an error note to the log file.
462d201506cSStephen Warren
463d201506cSStephen Warren        Args:
464d201506cSStephen Warren            msg: A message describing the error.
465d201506cSStephen Warren
466d201506cSStephen Warren        Returns:
467d201506cSStephen Warren            Nothing.
468e8debf39SStephen Warren        """
469d201506cSStephen Warren
470d201506cSStephen Warren        self._note("error", msg)
471d201506cSStephen Warren
472d201506cSStephen Warren    def warning(self, msg):
473e8debf39SStephen Warren        """Write an warning note to the log file.
474d201506cSStephen Warren
475d201506cSStephen Warren        Args:
476d201506cSStephen Warren            msg: A message describing the warning.
477d201506cSStephen Warren
478d201506cSStephen Warren        Returns:
479d201506cSStephen Warren            Nothing.
480e8debf39SStephen Warren        """
481d201506cSStephen Warren
48232090e50SStephen Warren        self.seen_warning = True
483d201506cSStephen Warren        self._note("warning", msg)
484d201506cSStephen Warren
48532090e50SStephen Warren    def get_and_reset_warning(self):
48632090e50SStephen Warren        """Get and reset the log warning flag.
48732090e50SStephen Warren
48832090e50SStephen Warren        Args:
48932090e50SStephen Warren            None
49032090e50SStephen Warren
49132090e50SStephen Warren        Returns:
49232090e50SStephen Warren            Whether a warning was seen since the last call.
49332090e50SStephen Warren        """
49432090e50SStephen Warren
49532090e50SStephen Warren        ret = self.seen_warning
49632090e50SStephen Warren        self.seen_warning = False
49732090e50SStephen Warren        return ret
49832090e50SStephen Warren
499d201506cSStephen Warren    def info(self, msg):
500e8debf39SStephen Warren        """Write an informational note to the log file.
501d201506cSStephen Warren
502d201506cSStephen Warren        Args:
503d201506cSStephen Warren            msg: An informational message.
504d201506cSStephen Warren
505d201506cSStephen Warren        Returns:
506d201506cSStephen Warren            Nothing.
507e8debf39SStephen Warren        """
508d201506cSStephen Warren
509d201506cSStephen Warren        self._note("info", msg)
510d201506cSStephen Warren
511d201506cSStephen Warren    def action(self, msg):
512e8debf39SStephen Warren        """Write an action note to the log file.
513d201506cSStephen Warren
514d201506cSStephen Warren        Args:
515d201506cSStephen Warren            msg: A message describing the action that is being logged.
516d201506cSStephen Warren
517d201506cSStephen Warren        Returns:
518d201506cSStephen Warren            Nothing.
519e8debf39SStephen Warren        """
520d201506cSStephen Warren
521d201506cSStephen Warren        self._note("action", msg)
522d201506cSStephen Warren
5239679d339SStephen Warren    def _get_time(self):
5249679d339SStephen Warren        return datetime.datetime.now()
5259679d339SStephen Warren
5269679d339SStephen Warren    def timestamp(self):
5279679d339SStephen Warren        """Write a timestamp to the log file.
5289679d339SStephen Warren
5299679d339SStephen Warren        Args:
5309679d339SStephen Warren            None
5319679d339SStephen Warren
5329679d339SStephen Warren        Returns:
5339679d339SStephen Warren            Nothing.
5349679d339SStephen Warren        """
5359679d339SStephen Warren
5369679d339SStephen Warren        timestamp_now = self._get_time()
5379679d339SStephen Warren        delta_prev = timestamp_now - self.timestamp_prev
5389679d339SStephen Warren        delta_start = timestamp_now - self.timestamp_start
5399679d339SStephen Warren        self.timestamp_prev = timestamp_now
5409679d339SStephen Warren
5419679d339SStephen Warren        self._note("timestamp",
5429679d339SStephen Warren            "TIME: NOW: " + timestamp_now.strftime("%Y/%m/%d %H:%M:%S.%f"))
5439679d339SStephen Warren        self._note("timestamp",
5449679d339SStephen Warren            "TIME: SINCE-PREV: " + str(delta_prev))
5459679d339SStephen Warren        self._note("timestamp",
5469679d339SStephen Warren            "TIME: SINCE-START: " + str(delta_start))
5479679d339SStephen Warren
54883357fd5SStephen Warren    def status_pass(self, msg, anchor=None):
549e8debf39SStephen Warren        """Write a note to the log file describing test(s) which passed.
550d201506cSStephen Warren
551d201506cSStephen Warren        Args:
55278b39cc3SStephen Warren            msg: A message describing the passed test(s).
55383357fd5SStephen Warren            anchor: Optional internal link target.
554d201506cSStephen Warren
555d201506cSStephen Warren        Returns:
556d201506cSStephen Warren            Nothing.
557e8debf39SStephen Warren        """
558d201506cSStephen Warren
55983357fd5SStephen Warren        self._note("status-pass", msg, anchor)
560d201506cSStephen Warren
56132090e50SStephen Warren    def status_warning(self, msg, anchor=None):
56232090e50SStephen Warren        """Write a note to the log file describing test(s) which passed.
56332090e50SStephen Warren
56432090e50SStephen Warren        Args:
56532090e50SStephen Warren            msg: A message describing the passed test(s).
56632090e50SStephen Warren            anchor: Optional internal link target.
56732090e50SStephen Warren
56832090e50SStephen Warren        Returns:
56932090e50SStephen Warren            Nothing.
57032090e50SStephen Warren        """
57132090e50SStephen Warren
57232090e50SStephen Warren        self._note("status-warning", msg, anchor)
57332090e50SStephen Warren
57483357fd5SStephen Warren    def status_skipped(self, msg, anchor=None):
575e8debf39SStephen Warren        """Write a note to the log file describing skipped test(s).
576d201506cSStephen Warren
577d201506cSStephen Warren        Args:
57878b39cc3SStephen Warren            msg: A message describing the skipped test(s).
57983357fd5SStephen Warren            anchor: Optional internal link target.
580d201506cSStephen Warren
581d201506cSStephen Warren        Returns:
582d201506cSStephen Warren            Nothing.
583e8debf39SStephen Warren        """
584d201506cSStephen Warren
58583357fd5SStephen Warren        self._note("status-skipped", msg, anchor)
586d201506cSStephen Warren
58783357fd5SStephen Warren    def status_xfail(self, msg, anchor=None):
58878b39cc3SStephen Warren        """Write a note to the log file describing xfailed test(s).
58978b39cc3SStephen Warren
59078b39cc3SStephen Warren        Args:
59178b39cc3SStephen Warren            msg: A message describing the xfailed test(s).
59283357fd5SStephen Warren            anchor: Optional internal link target.
59378b39cc3SStephen Warren
59478b39cc3SStephen Warren        Returns:
59578b39cc3SStephen Warren            Nothing.
59678b39cc3SStephen Warren        """
59778b39cc3SStephen Warren
59883357fd5SStephen Warren        self._note("status-xfail", msg, anchor)
59978b39cc3SStephen Warren
60083357fd5SStephen Warren    def status_xpass(self, msg, anchor=None):
60178b39cc3SStephen Warren        """Write a note to the log file describing xpassed test(s).
60278b39cc3SStephen Warren
60378b39cc3SStephen Warren        Args:
60478b39cc3SStephen Warren            msg: A message describing the xpassed test(s).
60583357fd5SStephen Warren            anchor: Optional internal link target.
60678b39cc3SStephen Warren
60778b39cc3SStephen Warren        Returns:
60878b39cc3SStephen Warren            Nothing.
60978b39cc3SStephen Warren        """
61078b39cc3SStephen Warren
61183357fd5SStephen Warren        self._note("status-xpass", msg, anchor)
61278b39cc3SStephen Warren
61383357fd5SStephen Warren    def status_fail(self, msg, anchor=None):
614e8debf39SStephen Warren        """Write a note to the log file describing failed test(s).
615d201506cSStephen Warren
616d201506cSStephen Warren        Args:
61778b39cc3SStephen Warren            msg: A message describing the failed test(s).
61883357fd5SStephen Warren            anchor: Optional internal link target.
619d201506cSStephen Warren
620d201506cSStephen Warren        Returns:
621d201506cSStephen Warren            Nothing.
622e8debf39SStephen Warren        """
623d201506cSStephen Warren
62483357fd5SStephen Warren        self._note("status-fail", msg, anchor)
625d201506cSStephen Warren
626d201506cSStephen Warren    def get_stream(self, name, chained_file=None):
627e8debf39SStephen Warren        """Create an object to log a single stream's data into the log file.
628d201506cSStephen Warren
629d201506cSStephen Warren        This creates a "file-like" object that can be written to in order to
630d201506cSStephen Warren        write a single stream's data to the log file. The implementation will
631d201506cSStephen Warren        handle any required interleaving of data (from multiple streams) in
632d201506cSStephen Warren        the log, in a way that makes it obvious which stream each bit of data
633d201506cSStephen Warren        came from.
634d201506cSStephen Warren
635d201506cSStephen Warren        Args:
636d201506cSStephen Warren            name: The name of the stream.
637d201506cSStephen Warren            chained_file: The file-like object to which all stream data should
638d201506cSStephen Warren                be logged to in addition to this log. Can be None.
639d201506cSStephen Warren
640d201506cSStephen Warren        Returns:
641d201506cSStephen Warren            A file-like object.
642e8debf39SStephen Warren        """
643d201506cSStephen Warren
644d201506cSStephen Warren        return LogfileStream(self, name, chained_file)
645d201506cSStephen Warren
646d201506cSStephen Warren    def get_runner(self, name, chained_file=None):
647e8debf39SStephen Warren        """Create an object that executes processes and logs their output.
648d201506cSStephen Warren
649d201506cSStephen Warren        Args:
650d201506cSStephen Warren            name: The name of this sub-process.
651d201506cSStephen Warren            chained_file: The file-like object to which all stream data should
652d201506cSStephen Warren                be logged to in addition to logfile. Can be None.
653d201506cSStephen Warren
654d201506cSStephen Warren        Returns:
655d201506cSStephen Warren            A RunAndLog object.
656e8debf39SStephen Warren        """
657d201506cSStephen Warren
658d201506cSStephen Warren        return RunAndLog(self, name, chained_file)
659d201506cSStephen Warren
660d201506cSStephen Warren    def write(self, stream, data, implicit=False):
661e8debf39SStephen Warren        """Write stream data into the log file.
662d201506cSStephen Warren
663d201506cSStephen Warren        This function should only be used by instances of LogfileStream or
664d201506cSStephen Warren        RunAndLog.
665d201506cSStephen Warren
666d201506cSStephen Warren        Args:
667d201506cSStephen Warren            stream: The stream whose data is being logged.
668d201506cSStephen Warren            data: The data to log.
669d201506cSStephen Warren            implicit: Boolean indicating whether data actually appeared in the
670d201506cSStephen Warren                stream, or was implicitly generated. A valid use-case is to
671d201506cSStephen Warren                repeat a shell prompt at the start of each separate log
672d201506cSStephen Warren                section, which makes the log sections more readable in
673d201506cSStephen Warren                isolation.
674d201506cSStephen Warren
675d201506cSStephen Warren        Returns:
676d201506cSStephen Warren            Nothing.
677e8debf39SStephen Warren        """
678d201506cSStephen Warren
679d201506cSStephen Warren        if stream != self.last_stream:
680d201506cSStephen Warren            self._terminate_stream()
68183357fd5SStephen Warren            self.f.write('<div class="stream block">\n')
68283357fd5SStephen Warren            self.f.write('<div class="stream-header block-header">Stream: ' +
68383357fd5SStephen Warren                         stream.name + '</div>\n')
68483357fd5SStephen Warren            self.f.write('<div class="stream-content block-content">\n')
685a2ec5606SStephen Warren            self.f.write('<pre>')
686d201506cSStephen Warren        if implicit:
687a2ec5606SStephen Warren            self.f.write('<span class="implicit">')
688d201506cSStephen Warren        self.f.write(self._escape(data))
689d201506cSStephen Warren        if implicit:
690a2ec5606SStephen Warren            self.f.write('</span>')
691d201506cSStephen Warren        self.last_stream = stream
692d201506cSStephen Warren
693d201506cSStephen Warren    def flush(self):
694e8debf39SStephen Warren        """Flush the log stream, to ensure correct log interleaving.
695d201506cSStephen Warren
696d201506cSStephen Warren        Args:
697d201506cSStephen Warren            None.
698d201506cSStephen Warren
699d201506cSStephen Warren        Returns:
700d201506cSStephen Warren            Nothing.
701e8debf39SStephen Warren        """
702d201506cSStephen Warren
703d201506cSStephen Warren        self.f.flush()
704