xref: /rk3399_rockchip-uboot/test/py/multiplexed_log.py (revision d201506cca782c54309b488170623094f252aab5)
1*d201506cSStephen Warren# Copyright (c) 2015 Stephen Warren
2*d201506cSStephen Warren# Copyright (c) 2015-2016, NVIDIA CORPORATION. All rights reserved.
3*d201506cSStephen Warren#
4*d201506cSStephen Warren# SPDX-License-Identifier: GPL-2.0
5*d201506cSStephen Warren
6*d201506cSStephen Warren# Generate an HTML-formatted log file containing multiple streams of data,
7*d201506cSStephen Warren# each represented in a well-delineated/-structured fashion.
8*d201506cSStephen Warren
9*d201506cSStephen Warrenimport cgi
10*d201506cSStephen Warrenimport os.path
11*d201506cSStephen Warrenimport shutil
12*d201506cSStephen Warrenimport subprocess
13*d201506cSStephen Warren
14*d201506cSStephen Warrenmod_dir = os.path.dirname(os.path.abspath(__file__))
15*d201506cSStephen Warren
16*d201506cSStephen Warrenclass LogfileStream(object):
17*d201506cSStephen Warren    '''A file-like object used to write a single logical stream of data into
18*d201506cSStephen Warren    a multiplexed log file. Objects of this type should be created by factory
19*d201506cSStephen Warren    functions in the Logfile class rather than directly.'''
20*d201506cSStephen Warren
21*d201506cSStephen Warren    def __init__(self, logfile, name, chained_file):
22*d201506cSStephen Warren        '''Initialize a new object.
23*d201506cSStephen Warren
24*d201506cSStephen Warren        Args:
25*d201506cSStephen Warren            logfile: The Logfile object to log to.
26*d201506cSStephen Warren            name: The name of this log stream.
27*d201506cSStephen Warren            chained_file: The file-like object to which all stream data should be
28*d201506cSStephen Warren            logged to in addition to logfile. Can be None.
29*d201506cSStephen Warren
30*d201506cSStephen Warren        Returns:
31*d201506cSStephen Warren            Nothing.
32*d201506cSStephen Warren        '''
33*d201506cSStephen Warren
34*d201506cSStephen Warren        self.logfile = logfile
35*d201506cSStephen Warren        self.name = name
36*d201506cSStephen Warren        self.chained_file = chained_file
37*d201506cSStephen Warren
38*d201506cSStephen Warren    def close(self):
39*d201506cSStephen Warren        '''Dummy function so that this class is "file-like".
40*d201506cSStephen Warren
41*d201506cSStephen Warren        Args:
42*d201506cSStephen Warren            None.
43*d201506cSStephen Warren
44*d201506cSStephen Warren        Returns:
45*d201506cSStephen Warren            Nothing.
46*d201506cSStephen Warren        '''
47*d201506cSStephen Warren
48*d201506cSStephen Warren        pass
49*d201506cSStephen Warren
50*d201506cSStephen Warren    def write(self, data, implicit=False):
51*d201506cSStephen Warren        '''Write data to the log stream.
52*d201506cSStephen Warren
53*d201506cSStephen Warren        Args:
54*d201506cSStephen Warren            data: The data to write tot he file.
55*d201506cSStephen Warren            implicit: Boolean indicating whether data actually appeared in the
56*d201506cSStephen Warren                stream, or was implicitly generated. A valid use-case is to
57*d201506cSStephen Warren                repeat a shell prompt at the start of each separate log
58*d201506cSStephen Warren                section, which makes the log sections more readable in
59*d201506cSStephen Warren                isolation.
60*d201506cSStephen Warren
61*d201506cSStephen Warren        Returns:
62*d201506cSStephen Warren            Nothing.
63*d201506cSStephen Warren        '''
64*d201506cSStephen Warren
65*d201506cSStephen Warren        self.logfile.write(self, data, implicit)
66*d201506cSStephen Warren        if self.chained_file:
67*d201506cSStephen Warren            self.chained_file.write(data)
68*d201506cSStephen Warren
69*d201506cSStephen Warren    def flush(self):
70*d201506cSStephen Warren        '''Flush the log stream, to ensure correct log interleaving.
71*d201506cSStephen Warren
72*d201506cSStephen Warren        Args:
73*d201506cSStephen Warren            None.
74*d201506cSStephen Warren
75*d201506cSStephen Warren        Returns:
76*d201506cSStephen Warren            Nothing.
77*d201506cSStephen Warren        '''
78*d201506cSStephen Warren
79*d201506cSStephen Warren        self.logfile.flush()
80*d201506cSStephen Warren        if self.chained_file:
81*d201506cSStephen Warren            self.chained_file.flush()
82*d201506cSStephen Warren
83*d201506cSStephen Warrenclass RunAndLog(object):
84*d201506cSStephen Warren    '''A utility object used to execute sub-processes and log their output to
85*d201506cSStephen Warren    a multiplexed log file. Objects of this type should be created by factory
86*d201506cSStephen Warren    functions in the Logfile class rather than directly.'''
87*d201506cSStephen Warren
88*d201506cSStephen Warren    def __init__(self, logfile, name, chained_file):
89*d201506cSStephen Warren        '''Initialize a new object.
90*d201506cSStephen Warren
91*d201506cSStephen Warren        Args:
92*d201506cSStephen Warren            logfile: The Logfile object to log to.
93*d201506cSStephen Warren            name: The name of this log stream or sub-process.
94*d201506cSStephen Warren            chained_file: The file-like object to which all stream data should
95*d201506cSStephen Warren                be logged to in addition to logfile. Can be None.
96*d201506cSStephen Warren
97*d201506cSStephen Warren        Returns:
98*d201506cSStephen Warren            Nothing.
99*d201506cSStephen Warren        '''
100*d201506cSStephen Warren
101*d201506cSStephen Warren        self.logfile = logfile
102*d201506cSStephen Warren        self.name = name
103*d201506cSStephen Warren        self.chained_file = chained_file
104*d201506cSStephen Warren
105*d201506cSStephen Warren    def close(self):
106*d201506cSStephen Warren        '''Clean up any resources managed by this object.'''
107*d201506cSStephen Warren        pass
108*d201506cSStephen Warren
109*d201506cSStephen Warren    def run(self, cmd, cwd=None):
110*d201506cSStephen Warren        '''Run a command as a sub-process, and log the results.
111*d201506cSStephen Warren
112*d201506cSStephen Warren        Args:
113*d201506cSStephen Warren            cmd: The command to execute.
114*d201506cSStephen Warren            cwd: The directory to run the command in. Can be None to use the
115*d201506cSStephen Warren                current directory.
116*d201506cSStephen Warren
117*d201506cSStephen Warren        Returns:
118*d201506cSStephen Warren            Nothing.
119*d201506cSStephen Warren        '''
120*d201506cSStephen Warren
121*d201506cSStephen Warren        msg = "+" + " ".join(cmd) + "\n"
122*d201506cSStephen Warren        if self.chained_file:
123*d201506cSStephen Warren            self.chained_file.write(msg)
124*d201506cSStephen Warren        self.logfile.write(self, msg)
125*d201506cSStephen Warren
126*d201506cSStephen Warren        try:
127*d201506cSStephen Warren            p = subprocess.Popen(cmd, cwd=cwd,
128*d201506cSStephen Warren                stdin=None, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
129*d201506cSStephen Warren            (stdout, stderr) = p.communicate()
130*d201506cSStephen Warren            output = ''
131*d201506cSStephen Warren            if stdout:
132*d201506cSStephen Warren                if stderr:
133*d201506cSStephen Warren                    output += 'stdout:\n'
134*d201506cSStephen Warren                output += stdout
135*d201506cSStephen Warren            if stderr:
136*d201506cSStephen Warren                if stdout:
137*d201506cSStephen Warren                    output += 'stderr:\n'
138*d201506cSStephen Warren                output += stderr
139*d201506cSStephen Warren            exit_status = p.returncode
140*d201506cSStephen Warren            exception = None
141*d201506cSStephen Warren        except subprocess.CalledProcessError as cpe:
142*d201506cSStephen Warren            output = cpe.output
143*d201506cSStephen Warren            exit_status = cpe.returncode
144*d201506cSStephen Warren            exception = cpe
145*d201506cSStephen Warren        except Exception as e:
146*d201506cSStephen Warren            output = ''
147*d201506cSStephen Warren            exit_status = 0
148*d201506cSStephen Warren            exception = e
149*d201506cSStephen Warren        if output and not output.endswith('\n'):
150*d201506cSStephen Warren            output += '\n'
151*d201506cSStephen Warren        if exit_status and not exception:
152*d201506cSStephen Warren            exception = Exception('Exit code: ' + str(exit_status))
153*d201506cSStephen Warren        if exception:
154*d201506cSStephen Warren            output += str(exception) + '\n'
155*d201506cSStephen Warren        self.logfile.write(self, output)
156*d201506cSStephen Warren        if self.chained_file:
157*d201506cSStephen Warren            self.chained_file.write(output)
158*d201506cSStephen Warren        if exception:
159*d201506cSStephen Warren            raise exception
160*d201506cSStephen Warren
161*d201506cSStephen Warrenclass SectionCtxMgr(object):
162*d201506cSStephen Warren    '''A context manager for Python's "with" statement, which allows a certain
163*d201506cSStephen Warren    portion of test code to be logged to a separate section of the log file.
164*d201506cSStephen Warren    Objects of this type should be created by factory functions in the Logfile
165*d201506cSStephen Warren    class rather than directly.'''
166*d201506cSStephen Warren
167*d201506cSStephen Warren    def __init__(self, log, marker):
168*d201506cSStephen Warren        '''Initialize a new object.
169*d201506cSStephen Warren
170*d201506cSStephen Warren        Args:
171*d201506cSStephen Warren            log: The Logfile object to log to.
172*d201506cSStephen Warren            marker: The name of the nested log section.
173*d201506cSStephen Warren
174*d201506cSStephen Warren        Returns:
175*d201506cSStephen Warren            Nothing.
176*d201506cSStephen Warren        '''
177*d201506cSStephen Warren
178*d201506cSStephen Warren        self.log = log
179*d201506cSStephen Warren        self.marker = marker
180*d201506cSStephen Warren
181*d201506cSStephen Warren    def __enter__(self):
182*d201506cSStephen Warren        self.log.start_section(self.marker)
183*d201506cSStephen Warren
184*d201506cSStephen Warren    def __exit__(self, extype, value, traceback):
185*d201506cSStephen Warren        self.log.end_section(self.marker)
186*d201506cSStephen Warren
187*d201506cSStephen Warrenclass Logfile(object):
188*d201506cSStephen Warren    '''Generates an HTML-formatted log file containing multiple streams of
189*d201506cSStephen Warren    data, each represented in a well-delineated/-structured fashion.'''
190*d201506cSStephen Warren
191*d201506cSStephen Warren    def __init__(self, fn):
192*d201506cSStephen Warren        '''Initialize a new object.
193*d201506cSStephen Warren
194*d201506cSStephen Warren        Args:
195*d201506cSStephen Warren            fn: The filename to write to.
196*d201506cSStephen Warren
197*d201506cSStephen Warren        Returns:
198*d201506cSStephen Warren            Nothing.
199*d201506cSStephen Warren        '''
200*d201506cSStephen Warren
201*d201506cSStephen Warren        self.f = open(fn, "wt")
202*d201506cSStephen Warren        self.last_stream = None
203*d201506cSStephen Warren        self.blocks = []
204*d201506cSStephen Warren        self.cur_evt = 1
205*d201506cSStephen Warren        shutil.copy(mod_dir + "/multiplexed_log.css", os.path.dirname(fn))
206*d201506cSStephen Warren        self.f.write("""\
207*d201506cSStephen Warren<html>
208*d201506cSStephen Warren<head>
209*d201506cSStephen Warren<link rel="stylesheet" type="text/css" href="multiplexed_log.css">
210*d201506cSStephen Warren</head>
211*d201506cSStephen Warren<body>
212*d201506cSStephen Warren<tt>
213*d201506cSStephen Warren""")
214*d201506cSStephen Warren
215*d201506cSStephen Warren    def close(self):
216*d201506cSStephen Warren        '''Close the log file.
217*d201506cSStephen Warren
218*d201506cSStephen Warren        After calling this function, no more data may be written to the log.
219*d201506cSStephen Warren
220*d201506cSStephen Warren        Args:
221*d201506cSStephen Warren            None.
222*d201506cSStephen Warren
223*d201506cSStephen Warren        Returns:
224*d201506cSStephen Warren            Nothing.
225*d201506cSStephen Warren        '''
226*d201506cSStephen Warren
227*d201506cSStephen Warren        self.f.write("""\
228*d201506cSStephen Warren</tt>
229*d201506cSStephen Warren</body>
230*d201506cSStephen Warren</html>
231*d201506cSStephen Warren""")
232*d201506cSStephen Warren        self.f.close()
233*d201506cSStephen Warren
234*d201506cSStephen Warren    # The set of characters that should be represented as hexadecimal codes in
235*d201506cSStephen Warren    # the log file.
236*d201506cSStephen Warren    _nonprint = ("%" + "".join(chr(c) for c in range(0, 32) if c not in (9, 10)) +
237*d201506cSStephen Warren                 "".join(chr(c) for c in range(127, 256)))
238*d201506cSStephen Warren
239*d201506cSStephen Warren    def _escape(self, data):
240*d201506cSStephen Warren        '''Render data format suitable for inclusion in an HTML document.
241*d201506cSStephen Warren
242*d201506cSStephen Warren        This includes HTML-escaping certain characters, and translating
243*d201506cSStephen Warren        control characters to a hexadecimal representation.
244*d201506cSStephen Warren
245*d201506cSStephen Warren        Args:
246*d201506cSStephen Warren            data: The raw string data to be escaped.
247*d201506cSStephen Warren
248*d201506cSStephen Warren        Returns:
249*d201506cSStephen Warren            An escaped version of the data.
250*d201506cSStephen Warren        '''
251*d201506cSStephen Warren
252*d201506cSStephen Warren        data = data.replace(chr(13), "")
253*d201506cSStephen Warren        data = "".join((c in self._nonprint) and ("%%%02x" % ord(c)) or
254*d201506cSStephen Warren                       c for c in data)
255*d201506cSStephen Warren        data = cgi.escape(data)
256*d201506cSStephen Warren        return data
257*d201506cSStephen Warren
258*d201506cSStephen Warren    def _terminate_stream(self):
259*d201506cSStephen Warren        '''Write HTML to the log file to terminate the current stream's data.
260*d201506cSStephen Warren
261*d201506cSStephen Warren        Args:
262*d201506cSStephen Warren            None.
263*d201506cSStephen Warren
264*d201506cSStephen Warren        Returns:
265*d201506cSStephen Warren            Nothing.
266*d201506cSStephen Warren        '''
267*d201506cSStephen Warren
268*d201506cSStephen Warren        self.cur_evt += 1
269*d201506cSStephen Warren        if not self.last_stream:
270*d201506cSStephen Warren            return
271*d201506cSStephen Warren        self.f.write("</pre>\n")
272*d201506cSStephen Warren        self.f.write("<div class=\"stream-trailer\" id=\"" +
273*d201506cSStephen Warren                     self.last_stream.name + "\">End stream: " +
274*d201506cSStephen Warren                     self.last_stream.name + "</div>\n")
275*d201506cSStephen Warren        self.f.write("</div>\n")
276*d201506cSStephen Warren        self.last_stream = None
277*d201506cSStephen Warren
278*d201506cSStephen Warren    def _note(self, note_type, msg):
279*d201506cSStephen Warren        '''Write a note or one-off message to the log file.
280*d201506cSStephen Warren
281*d201506cSStephen Warren        Args:
282*d201506cSStephen Warren            note_type: The type of note. This must be a value supported by the
283*d201506cSStephen Warren                accompanying multiplexed_log.css.
284*d201506cSStephen Warren            msg: The note/message to log.
285*d201506cSStephen Warren
286*d201506cSStephen Warren        Returns:
287*d201506cSStephen Warren            Nothing.
288*d201506cSStephen Warren        '''
289*d201506cSStephen Warren
290*d201506cSStephen Warren        self._terminate_stream()
291*d201506cSStephen Warren        self.f.write("<div class=\"" + note_type + "\">\n<pre>")
292*d201506cSStephen Warren        self.f.write(self._escape(msg))
293*d201506cSStephen Warren        self.f.write("\n</pre></div>\n")
294*d201506cSStephen Warren
295*d201506cSStephen Warren    def start_section(self, marker):
296*d201506cSStephen Warren        '''Begin a new nested section in the log file.
297*d201506cSStephen Warren
298*d201506cSStephen Warren        Args:
299*d201506cSStephen Warren            marker: The name of the section that is starting.
300*d201506cSStephen Warren
301*d201506cSStephen Warren        Returns:
302*d201506cSStephen Warren            Nothing.
303*d201506cSStephen Warren        '''
304*d201506cSStephen Warren
305*d201506cSStephen Warren        self._terminate_stream()
306*d201506cSStephen Warren        self.blocks.append(marker)
307*d201506cSStephen Warren        blk_path = "/".join(self.blocks)
308*d201506cSStephen Warren        self.f.write("<div class=\"section\" id=\"" + blk_path + "\">\n")
309*d201506cSStephen Warren        self.f.write("<div class=\"section-header\" id=\"" + blk_path +
310*d201506cSStephen Warren                     "\">Section: " + blk_path + "</div>\n")
311*d201506cSStephen Warren
312*d201506cSStephen Warren    def end_section(self, marker):
313*d201506cSStephen Warren        '''Terminate the current nested section in the log file.
314*d201506cSStephen Warren
315*d201506cSStephen Warren        This function validates proper nesting of start_section() and
316*d201506cSStephen Warren        end_section() calls. If a mismatch is found, an exception is raised.
317*d201506cSStephen Warren
318*d201506cSStephen Warren        Args:
319*d201506cSStephen Warren            marker: The name of the section that is ending.
320*d201506cSStephen Warren
321*d201506cSStephen Warren        Returns:
322*d201506cSStephen Warren            Nothing.
323*d201506cSStephen Warren        '''
324*d201506cSStephen Warren
325*d201506cSStephen Warren        if (not self.blocks) or (marker != self.blocks[-1]):
326*d201506cSStephen Warren            raise Exception("Block nesting mismatch: \"%s\" \"%s\"" %
327*d201506cSStephen Warren                            (marker, "/".join(self.blocks)))
328*d201506cSStephen Warren        self._terminate_stream()
329*d201506cSStephen Warren        blk_path = "/".join(self.blocks)
330*d201506cSStephen Warren        self.f.write("<div class=\"section-trailer\" id=\"section-trailer-" +
331*d201506cSStephen Warren                     blk_path + "\">End section: " + blk_path + "</div>\n")
332*d201506cSStephen Warren        self.f.write("</div>\n")
333*d201506cSStephen Warren        self.blocks.pop()
334*d201506cSStephen Warren
335*d201506cSStephen Warren    def section(self, marker):
336*d201506cSStephen Warren        '''Create a temporary section in the log file.
337*d201506cSStephen Warren
338*d201506cSStephen Warren        This function creates a context manager for Python's "with" statement,
339*d201506cSStephen Warren        which allows a certain portion of test code to be logged to a separate
340*d201506cSStephen Warren        section of the log file.
341*d201506cSStephen Warren
342*d201506cSStephen Warren        Usage:
343*d201506cSStephen Warren            with log.section("somename"):
344*d201506cSStephen Warren                some test code
345*d201506cSStephen Warren
346*d201506cSStephen Warren        Args:
347*d201506cSStephen Warren            marker: The name of the nested section.
348*d201506cSStephen Warren
349*d201506cSStephen Warren        Returns:
350*d201506cSStephen Warren            A context manager object.
351*d201506cSStephen Warren        '''
352*d201506cSStephen Warren
353*d201506cSStephen Warren        return SectionCtxMgr(self, marker)
354*d201506cSStephen Warren
355*d201506cSStephen Warren    def error(self, msg):
356*d201506cSStephen Warren        '''Write an error note to the log file.
357*d201506cSStephen Warren
358*d201506cSStephen Warren        Args:
359*d201506cSStephen Warren            msg: A message describing the error.
360*d201506cSStephen Warren
361*d201506cSStephen Warren        Returns:
362*d201506cSStephen Warren            Nothing.
363*d201506cSStephen Warren        '''
364*d201506cSStephen Warren
365*d201506cSStephen Warren        self._note("error", msg)
366*d201506cSStephen Warren
367*d201506cSStephen Warren    def warning(self, msg):
368*d201506cSStephen Warren        '''Write an warning note to the log file.
369*d201506cSStephen Warren
370*d201506cSStephen Warren        Args:
371*d201506cSStephen Warren            msg: A message describing the warning.
372*d201506cSStephen Warren
373*d201506cSStephen Warren        Returns:
374*d201506cSStephen Warren            Nothing.
375*d201506cSStephen Warren        '''
376*d201506cSStephen Warren
377*d201506cSStephen Warren        self._note("warning", msg)
378*d201506cSStephen Warren
379*d201506cSStephen Warren    def info(self, msg):
380*d201506cSStephen Warren        '''Write an informational note to the log file.
381*d201506cSStephen Warren
382*d201506cSStephen Warren        Args:
383*d201506cSStephen Warren            msg: An informational message.
384*d201506cSStephen Warren
385*d201506cSStephen Warren        Returns:
386*d201506cSStephen Warren            Nothing.
387*d201506cSStephen Warren        '''
388*d201506cSStephen Warren
389*d201506cSStephen Warren        self._note("info", msg)
390*d201506cSStephen Warren
391*d201506cSStephen Warren    def action(self, msg):
392*d201506cSStephen Warren        '''Write an action note to the log file.
393*d201506cSStephen Warren
394*d201506cSStephen Warren        Args:
395*d201506cSStephen Warren            msg: A message describing the action that is being logged.
396*d201506cSStephen Warren
397*d201506cSStephen Warren        Returns:
398*d201506cSStephen Warren            Nothing.
399*d201506cSStephen Warren        '''
400*d201506cSStephen Warren
401*d201506cSStephen Warren        self._note("action", msg)
402*d201506cSStephen Warren
403*d201506cSStephen Warren    def status_pass(self, msg):
404*d201506cSStephen Warren        '''Write a note to the log file describing test(s) which passed.
405*d201506cSStephen Warren
406*d201506cSStephen Warren        Args:
407*d201506cSStephen Warren            msg: A message describing passed test(s).
408*d201506cSStephen Warren
409*d201506cSStephen Warren        Returns:
410*d201506cSStephen Warren            Nothing.
411*d201506cSStephen Warren        '''
412*d201506cSStephen Warren
413*d201506cSStephen Warren        self._note("status-pass", msg)
414*d201506cSStephen Warren
415*d201506cSStephen Warren    def status_skipped(self, msg):
416*d201506cSStephen Warren        '''Write a note to the log file describing skipped test(s).
417*d201506cSStephen Warren
418*d201506cSStephen Warren        Args:
419*d201506cSStephen Warren            msg: A message describing passed test(s).
420*d201506cSStephen Warren
421*d201506cSStephen Warren        Returns:
422*d201506cSStephen Warren            Nothing.
423*d201506cSStephen Warren        '''
424*d201506cSStephen Warren
425*d201506cSStephen Warren        self._note("status-skipped", msg)
426*d201506cSStephen Warren
427*d201506cSStephen Warren    def status_fail(self, msg):
428*d201506cSStephen Warren        '''Write a note to the log file describing failed test(s).
429*d201506cSStephen Warren
430*d201506cSStephen Warren        Args:
431*d201506cSStephen Warren            msg: A message describing passed test(s).
432*d201506cSStephen Warren
433*d201506cSStephen Warren        Returns:
434*d201506cSStephen Warren            Nothing.
435*d201506cSStephen Warren        '''
436*d201506cSStephen Warren
437*d201506cSStephen Warren        self._note("status-fail", msg)
438*d201506cSStephen Warren
439*d201506cSStephen Warren    def get_stream(self, name, chained_file=None):
440*d201506cSStephen Warren        '''Create an object to log a single stream's data into the log file.
441*d201506cSStephen Warren
442*d201506cSStephen Warren        This creates a "file-like" object that can be written to in order to
443*d201506cSStephen Warren        write a single stream's data to the log file. The implementation will
444*d201506cSStephen Warren        handle any required interleaving of data (from multiple streams) in
445*d201506cSStephen Warren        the log, in a way that makes it obvious which stream each bit of data
446*d201506cSStephen Warren        came from.
447*d201506cSStephen Warren
448*d201506cSStephen Warren        Args:
449*d201506cSStephen Warren            name: The name of the stream.
450*d201506cSStephen Warren            chained_file: The file-like object to which all stream data should
451*d201506cSStephen Warren                be logged to in addition to this log. Can be None.
452*d201506cSStephen Warren
453*d201506cSStephen Warren        Returns:
454*d201506cSStephen Warren            A file-like object.
455*d201506cSStephen Warren        '''
456*d201506cSStephen Warren
457*d201506cSStephen Warren        return LogfileStream(self, name, chained_file)
458*d201506cSStephen Warren
459*d201506cSStephen Warren    def get_runner(self, name, chained_file=None):
460*d201506cSStephen Warren        '''Create an object that executes processes and logs their output.
461*d201506cSStephen Warren
462*d201506cSStephen Warren        Args:
463*d201506cSStephen Warren            name: The name of this sub-process.
464*d201506cSStephen Warren            chained_file: The file-like object to which all stream data should
465*d201506cSStephen Warren                be logged to in addition to logfile. Can be None.
466*d201506cSStephen Warren
467*d201506cSStephen Warren        Returns:
468*d201506cSStephen Warren            A RunAndLog object.
469*d201506cSStephen Warren        '''
470*d201506cSStephen Warren
471*d201506cSStephen Warren        return RunAndLog(self, name, chained_file)
472*d201506cSStephen Warren
473*d201506cSStephen Warren    def write(self, stream, data, implicit=False):
474*d201506cSStephen Warren        '''Write stream data into the log file.
475*d201506cSStephen Warren
476*d201506cSStephen Warren        This function should only be used by instances of LogfileStream or
477*d201506cSStephen Warren        RunAndLog.
478*d201506cSStephen Warren
479*d201506cSStephen Warren        Args:
480*d201506cSStephen Warren            stream: The stream whose data is being logged.
481*d201506cSStephen Warren            data: The data to log.
482*d201506cSStephen Warren            implicit: Boolean indicating whether data actually appeared in the
483*d201506cSStephen Warren                stream, or was implicitly generated. A valid use-case is to
484*d201506cSStephen Warren                repeat a shell prompt at the start of each separate log
485*d201506cSStephen Warren                section, which makes the log sections more readable in
486*d201506cSStephen Warren                isolation.
487*d201506cSStephen Warren
488*d201506cSStephen Warren        Returns:
489*d201506cSStephen Warren            Nothing.
490*d201506cSStephen Warren        '''
491*d201506cSStephen Warren
492*d201506cSStephen Warren        if stream != self.last_stream:
493*d201506cSStephen Warren            self._terminate_stream()
494*d201506cSStephen Warren            self.f.write("<div class=\"stream\" id=\"%s\">\n" % stream.name)
495*d201506cSStephen Warren            self.f.write("<div class=\"stream-header\" id=\"" + stream.name +
496*d201506cSStephen Warren                         "\">Stream: " + stream.name + "</div>\n")
497*d201506cSStephen Warren            self.f.write("<pre>")
498*d201506cSStephen Warren        if implicit:
499*d201506cSStephen Warren            self.f.write("<span class=\"implicit\">")
500*d201506cSStephen Warren        self.f.write(self._escape(data))
501*d201506cSStephen Warren        if implicit:
502*d201506cSStephen Warren            self.f.write("</span>")
503*d201506cSStephen Warren        self.last_stream = stream
504*d201506cSStephen Warren
505*d201506cSStephen Warren    def flush(self):
506*d201506cSStephen Warren        '''Flush the log stream, to ensure correct log interleaving.
507*d201506cSStephen Warren
508*d201506cSStephen Warren        Args:
509*d201506cSStephen Warren            None.
510*d201506cSStephen Warren
511*d201506cSStephen Warren        Returns:
512*d201506cSStephen Warren            Nothing.
513*d201506cSStephen Warren        '''
514*d201506cSStephen Warren
515*d201506cSStephen Warren        self.f.flush()
516