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