]> git.sur5r.net Git - u-boot/blob - test/py/multiplexed_log.py
test/py: Provide output from exceptions with RunAndLog()
[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         self.output = None
105
106     def close(self):
107         """Clean up any resources managed by this object."""
108         pass
109
110     def run(self, cmd, cwd=None, ignore_errors=False):
111         """Run a command as a sub-process, and log the results.
112
113         The output is available at self.output which can be useful if there is
114         an exception.
115
116         Args:
117             cmd: The command to execute.
118             cwd: The directory to run the command in. Can be None to use the
119                 current directory.
120             ignore_errors: Indicate whether to ignore errors. If True, the
121                 function will simply return if the command cannot be executed
122                 or exits with an error code, otherwise an exception will be
123                 raised if such problems occur.
124
125         Returns:
126             The output as a string.
127         """
128
129         msg = '+' + ' '.join(cmd) + '\n'
130         if self.chained_file:
131             self.chained_file.write(msg)
132         self.logfile.write(self, msg)
133
134         try:
135             p = subprocess.Popen(cmd, cwd=cwd,
136                 stdin=None, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
137             (stdout, stderr) = p.communicate()
138             output = ''
139             if stdout:
140                 if stderr:
141                     output += 'stdout:\n'
142                 output += stdout
143             if stderr:
144                 if stdout:
145                     output += 'stderr:\n'
146                 output += stderr
147             exit_status = p.returncode
148             exception = None
149         except subprocess.CalledProcessError as cpe:
150             output = cpe.output
151             exit_status = cpe.returncode
152             exception = cpe
153         except Exception as e:
154             output = ''
155             exit_status = 0
156             exception = e
157         if output and not output.endswith('\n'):
158             output += '\n'
159         if exit_status and not exception and not ignore_errors:
160             exception = Exception('Exit code: ' + str(exit_status))
161         if exception:
162             output += str(exception) + '\n'
163         self.logfile.write(self, output)
164         if self.chained_file:
165             self.chained_file.write(output)
166
167         # Store the output so it can be accessed if we raise an exception.
168         self.output = output
169         if exception:
170             raise exception
171         return output
172
173 class SectionCtxMgr(object):
174     """A context manager for Python's "with" statement, which allows a certain
175     portion of test code to be logged to a separate section of the log file.
176     Objects of this type should be created by factory functions in the Logfile
177     class rather than directly."""
178
179     def __init__(self, log, marker, anchor):
180         """Initialize a new object.
181
182         Args:
183             log: The Logfile object to log to.
184             marker: The name of the nested log section.
185             anchor: The anchor value to pass to start_section().
186
187         Returns:
188             Nothing.
189         """
190
191         self.log = log
192         self.marker = marker
193         self.anchor = anchor
194
195     def __enter__(self):
196         self.anchor = self.log.start_section(self.marker, self.anchor)
197
198     def __exit__(self, extype, value, traceback):
199         self.log.end_section(self.marker)
200
201 class Logfile(object):
202     """Generates an HTML-formatted log file containing multiple streams of
203     data, each represented in a well-delineated/-structured fashion."""
204
205     def __init__(self, fn):
206         """Initialize a new object.
207
208         Args:
209             fn: The filename to write to.
210
211         Returns:
212             Nothing.
213         """
214
215         self.f = open(fn, 'wt')
216         self.last_stream = None
217         self.blocks = []
218         self.cur_evt = 1
219         self.anchor = 0
220
221         shutil.copy(mod_dir + '/multiplexed_log.css', os.path.dirname(fn))
222         self.f.write('''\
223 <html>
224 <head>
225 <link rel="stylesheet" type="text/css" href="multiplexed_log.css">
226 <script src="http://code.jquery.com/jquery.min.js"></script>
227 <script>
228 $(document).ready(function () {
229     // Copy status report HTML to start of log for easy access
230     sts = $(".block#status_report")[0].outerHTML;
231     $("tt").prepend(sts);
232
233     // Add expand/contract buttons to all block headers
234     btns = "<span class=\\\"block-expand hidden\\\">[+] </span>" +
235         "<span class=\\\"block-contract\\\">[-] </span>";
236     $(".block-header").prepend(btns);
237
238     // Pre-contract all blocks which passed, leaving only problem cases
239     // expanded, to highlight issues the user should look at.
240     // Only top-level blocks (sections) should have any status
241     passed_bcs = $(".block-content:has(.status-pass)");
242     // Some blocks might have multiple status entries (e.g. the status
243     // report), so take care not to hide blocks with partial success.
244     passed_bcs = passed_bcs.not(":has(.status-fail)");
245     passed_bcs = passed_bcs.not(":has(.status-xfail)");
246     passed_bcs = passed_bcs.not(":has(.status-xpass)");
247     passed_bcs = passed_bcs.not(":has(.status-skipped)");
248     // Hide the passed blocks
249     passed_bcs.addClass("hidden");
250     // Flip the expand/contract button hiding for those blocks.
251     bhs = passed_bcs.parent().children(".block-header")
252     bhs.children(".block-expand").removeClass("hidden");
253     bhs.children(".block-contract").addClass("hidden");
254
255     // Add click handler to block headers.
256     // The handler expands/contracts the block.
257     $(".block-header").on("click", function (e) {
258         var header = $(this);
259         var content = header.next(".block-content");
260         var expanded = !content.hasClass("hidden");
261         if (expanded) {
262             content.addClass("hidden");
263             header.children(".block-expand").first().removeClass("hidden");
264             header.children(".block-contract").first().addClass("hidden");
265         } else {
266             header.children(".block-contract").first().removeClass("hidden");
267             header.children(".block-expand").first().addClass("hidden");
268             content.removeClass("hidden");
269         }
270     });
271
272     // When clicking on a link, expand the target block
273     $("a").on("click", function (e) {
274         var block = $($(this).attr("href"));
275         var header = block.children(".block-header");
276         var content = block.children(".block-content").first();
277         header.children(".block-contract").first().removeClass("hidden");
278         header.children(".block-expand").first().addClass("hidden");
279         content.removeClass("hidden");
280     });
281 });
282 </script>
283 </head>
284 <body>
285 <tt>
286 ''')
287
288     def close(self):
289         """Close the log file.
290
291         After calling this function, no more data may be written to the log.
292
293         Args:
294             None.
295
296         Returns:
297             Nothing.
298         """
299
300         self.f.write('''\
301 </tt>
302 </body>
303 </html>
304 ''')
305         self.f.close()
306
307     # The set of characters that should be represented as hexadecimal codes in
308     # the log file.
309     _nonprint = ('%' + ''.join(chr(c) for c in range(0, 32) if c not in (9, 10)) +
310                  ''.join(chr(c) for c in range(127, 256)))
311
312     def _escape(self, data):
313         """Render data format suitable for inclusion in an HTML document.
314
315         This includes HTML-escaping certain characters, and translating
316         control characters to a hexadecimal representation.
317
318         Args:
319             data: The raw string data to be escaped.
320
321         Returns:
322             An escaped version of the data.
323         """
324
325         data = data.replace(chr(13), '')
326         data = ''.join((c in self._nonprint) and ('%%%02x' % ord(c)) or
327                        c for c in data)
328         data = cgi.escape(data)
329         return data
330
331     def _terminate_stream(self):
332         """Write HTML to the log file to terminate the current stream's data.
333
334         Args:
335             None.
336
337         Returns:
338             Nothing.
339         """
340
341         self.cur_evt += 1
342         if not self.last_stream:
343             return
344         self.f.write('</pre>\n')
345         self.f.write('<div class="stream-trailer block-trailer">End stream: ' +
346                      self.last_stream.name + '</div>\n')
347         self.f.write('</div>\n')
348         self.f.write('</div>\n')
349         self.last_stream = None
350
351     def _note(self, note_type, msg, anchor=None):
352         """Write a note or one-off message to the log file.
353
354         Args:
355             note_type: The type of note. This must be a value supported by the
356                 accompanying multiplexed_log.css.
357             msg: The note/message to log.
358             anchor: Optional internal link target.
359
360         Returns:
361             Nothing.
362         """
363
364         self._terminate_stream()
365         self.f.write('<div class="' + note_type + '">\n')
366         if anchor:
367             self.f.write('<a href="#%s">\n' % anchor)
368         self.f.write('<pre>')
369         self.f.write(self._escape(msg))
370         self.f.write('\n</pre>\n')
371         if anchor:
372             self.f.write('</a>\n')
373         self.f.write('</div>\n')
374
375     def start_section(self, marker, anchor=None):
376         """Begin a new nested section in the log file.
377
378         Args:
379             marker: The name of the section that is starting.
380             anchor: The value to use for the anchor. If None, a unique value
381               will be calculated and used
382
383         Returns:
384             Name of the HTML anchor emitted before section.
385         """
386
387         self._terminate_stream()
388         self.blocks.append(marker)
389         if not anchor:
390             self.anchor += 1
391             anchor = str(self.anchor)
392         blk_path = '/'.join(self.blocks)
393         self.f.write('<div class="section block" id="' + anchor + '">\n')
394         self.f.write('<div class="section-header block-header">Section: ' +
395                      blk_path + '</div>\n')
396         self.f.write('<div class="section-content block-content">\n')
397
398         return anchor
399
400     def end_section(self, marker):
401         """Terminate the current nested section in the log file.
402
403         This function validates proper nesting of start_section() and
404         end_section() calls. If a mismatch is found, an exception is raised.
405
406         Args:
407             marker: The name of the section that is ending.
408
409         Returns:
410             Nothing.
411         """
412
413         if (not self.blocks) or (marker != self.blocks[-1]):
414             raise Exception('Block nesting mismatch: "%s" "%s"' %
415                             (marker, '/'.join(self.blocks)))
416         self._terminate_stream()
417         blk_path = '/'.join(self.blocks)
418         self.f.write('<div class="section-trailer block-trailer">' +
419                      'End section: ' + blk_path + '</div>\n')
420         self.f.write('</div>\n')
421         self.f.write('</div>\n')
422         self.blocks.pop()
423
424     def section(self, marker, anchor=None):
425         """Create a temporary section in the log file.
426
427         This function creates a context manager for Python's "with" statement,
428         which allows a certain portion of test code to be logged to a separate
429         section of the log file.
430
431         Usage:
432             with log.section("somename"):
433                 some test code
434
435         Args:
436             marker: The name of the nested section.
437             anchor: The anchor value to pass to start_section().
438
439         Returns:
440             A context manager object.
441         """
442
443         return SectionCtxMgr(self, marker, anchor)
444
445     def error(self, msg):
446         """Write an error note to the log file.
447
448         Args:
449             msg: A message describing the error.
450
451         Returns:
452             Nothing.
453         """
454
455         self._note("error", msg)
456
457     def warning(self, msg):
458         """Write an warning note to the log file.
459
460         Args:
461             msg: A message describing the warning.
462
463         Returns:
464             Nothing.
465         """
466
467         self._note("warning", msg)
468
469     def info(self, msg):
470         """Write an informational note to the log file.
471
472         Args:
473             msg: An informational message.
474
475         Returns:
476             Nothing.
477         """
478
479         self._note("info", msg)
480
481     def action(self, msg):
482         """Write an action note to the log file.
483
484         Args:
485             msg: A message describing the action that is being logged.
486
487         Returns:
488             Nothing.
489         """
490
491         self._note("action", msg)
492
493     def status_pass(self, msg, anchor=None):
494         """Write a note to the log file describing test(s) which passed.
495
496         Args:
497             msg: A message describing the passed test(s).
498             anchor: Optional internal link target.
499
500         Returns:
501             Nothing.
502         """
503
504         self._note("status-pass", msg, anchor)
505
506     def status_skipped(self, msg, anchor=None):
507         """Write a note to the log file describing skipped test(s).
508
509         Args:
510             msg: A message describing the skipped test(s).
511             anchor: Optional internal link target.
512
513         Returns:
514             Nothing.
515         """
516
517         self._note("status-skipped", msg, anchor)
518
519     def status_xfail(self, msg, anchor=None):
520         """Write a note to the log file describing xfailed test(s).
521
522         Args:
523             msg: A message describing the xfailed test(s).
524             anchor: Optional internal link target.
525
526         Returns:
527             Nothing.
528         """
529
530         self._note("status-xfail", msg, anchor)
531
532     def status_xpass(self, msg, anchor=None):
533         """Write a note to the log file describing xpassed test(s).
534
535         Args:
536             msg: A message describing the xpassed test(s).
537             anchor: Optional internal link target.
538
539         Returns:
540             Nothing.
541         """
542
543         self._note("status-xpass", msg, anchor)
544
545     def status_fail(self, msg, anchor=None):
546         """Write a note to the log file describing failed test(s).
547
548         Args:
549             msg: A message describing the failed test(s).
550             anchor: Optional internal link target.
551
552         Returns:
553             Nothing.
554         """
555
556         self._note("status-fail", msg, anchor)
557
558     def get_stream(self, name, chained_file=None):
559         """Create an object to log a single stream's data into the log file.
560
561         This creates a "file-like" object that can be written to in order to
562         write a single stream's data to the log file. The implementation will
563         handle any required interleaving of data (from multiple streams) in
564         the log, in a way that makes it obvious which stream each bit of data
565         came from.
566
567         Args:
568             name: The name of the stream.
569             chained_file: The file-like object to which all stream data should
570                 be logged to in addition to this log. Can be None.
571
572         Returns:
573             A file-like object.
574         """
575
576         return LogfileStream(self, name, chained_file)
577
578     def get_runner(self, name, chained_file=None):
579         """Create an object that executes processes and logs their output.
580
581         Args:
582             name: The name of this sub-process.
583             chained_file: The file-like object to which all stream data should
584                 be logged to in addition to logfile. Can be None.
585
586         Returns:
587             A RunAndLog object.
588         """
589
590         return RunAndLog(self, name, chained_file)
591
592     def write(self, stream, data, implicit=False):
593         """Write stream data into the log file.
594
595         This function should only be used by instances of LogfileStream or
596         RunAndLog.
597
598         Args:
599             stream: The stream whose data is being logged.
600             data: The data to log.
601             implicit: Boolean indicating whether data actually appeared in the
602                 stream, or was implicitly generated. A valid use-case is to
603                 repeat a shell prompt at the start of each separate log
604                 section, which makes the log sections more readable in
605                 isolation.
606
607         Returns:
608             Nothing.
609         """
610
611         if stream != self.last_stream:
612             self._terminate_stream()
613             self.f.write('<div class="stream block">\n')
614             self.f.write('<div class="stream-header block-header">Stream: ' +
615                          stream.name + '</div>\n')
616             self.f.write('<div class="stream-content block-content">\n')
617             self.f.write('<pre>')
618         if implicit:
619             self.f.write('<span class="implicit">')
620         self.f.write(self._escape(data))
621         if implicit:
622             self.f.write('</span>')
623         self.last_stream = stream
624
625     def flush(self):
626         """Flush the log stream, to ensure correct log interleaving.
627
628         Args:
629             None.
630
631         Returns:
632             Nothing.
633         """
634
635         self.f.flush()