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