]> git.ipfire.org Git - people/ms/u-boot.git/blame - test/py/multiplexed_log.py
test/py: Provide output from exceptions with RunAndLog()
[people/ms/u-boot.git] / test / py / multiplexed_log.py
CommitLineData
d201506c
SW
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
9import cgi
10import os.path
11import shutil
12import subprocess
13
14mod_dir = os.path.dirname(os.path.abspath(__file__))
15
16class LogfileStream(object):
e8debf39 17 """A file-like object used to write a single logical stream of data into
d201506c 18 a multiplexed log file. Objects of this type should be created by factory
e8debf39 19 functions in the Logfile class rather than directly."""
d201506c
SW
20
21 def __init__(self, logfile, name, chained_file):
e8debf39 22 """Initialize a new object.
d201506c
SW
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.
e8debf39 32 """
d201506c
SW
33
34 self.logfile = logfile
35 self.name = name
36 self.chained_file = chained_file
37
38 def close(self):
e8debf39 39 """Dummy function so that this class is "file-like".
d201506c
SW
40
41 Args:
42 None.
43
44 Returns:
45 Nothing.
e8debf39 46 """
d201506c
SW
47
48 pass
49
50 def write(self, data, implicit=False):
e8debf39 51 """Write data to the log stream.
d201506c
SW
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.
e8debf39 63 """
d201506c
SW
64
65 self.logfile.write(self, data, implicit)
66 if self.chained_file:
67 self.chained_file.write(data)
68
69 def flush(self):
e8debf39 70 """Flush the log stream, to ensure correct log interleaving.
d201506c
SW
71
72 Args:
73 None.
74
75 Returns:
76 Nothing.
e8debf39 77 """
d201506c
SW
78
79 self.logfile.flush()
80 if self.chained_file:
81 self.chained_file.flush()
82
83class RunAndLog(object):
e8debf39 84 """A utility object used to execute sub-processes and log their output to
d201506c 85 a multiplexed log file. Objects of this type should be created by factory
e8debf39 86 functions in the Logfile class rather than directly."""
d201506c
SW
87
88 def __init__(self, logfile, name, chained_file):
e8debf39 89 """Initialize a new object.
d201506c
SW
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.
e8debf39 99 """
d201506c
SW
100
101 self.logfile = logfile
102 self.name = name
103 self.chained_file = chained_file
86845bf3 104 self.output = None
d201506c
SW
105
106 def close(self):
e8debf39 107 """Clean up any resources managed by this object."""
d201506c
SW
108 pass
109
3f2faf73 110 def run(self, cmd, cwd=None, ignore_errors=False):
e8debf39 111 """Run a command as a sub-process, and log the results.
d201506c 112
86845bf3
SG
113 The output is available at self.output which can be useful if there is
114 an exception.
115
d201506c
SW
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.
3f2faf73
SW
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.
d201506c
SW
124
125 Returns:
3b8d9d97 126 The output as a string.
e8debf39 127 """
d201506c 128
a2ec5606 129 msg = '+' + ' '.join(cmd) + '\n'
d201506c
SW
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'
3f2faf73 159 if exit_status and not exception and not ignore_errors:
d201506c
SW
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)
86845bf3
SG
166
167 # Store the output so it can be accessed if we raise an exception.
168 self.output = output
d201506c
SW
169 if exception:
170 raise exception
3b8d9d97 171 return output
d201506c
SW
172
173class SectionCtxMgr(object):
e8debf39 174 """A context manager for Python's "with" statement, which allows a certain
d201506c
SW
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
e8debf39 177 class rather than directly."""
d201506c 178
83357fd5 179 def __init__(self, log, marker, anchor):
e8debf39 180 """Initialize a new object.
d201506c
SW
181
182 Args:
183 log: The Logfile object to log to.
184 marker: The name of the nested log section.
83357fd5 185 anchor: The anchor value to pass to start_section().
d201506c
SW
186
187 Returns:
188 Nothing.
e8debf39 189 """
d201506c
SW
190
191 self.log = log
192 self.marker = marker
83357fd5 193 self.anchor = anchor
d201506c
SW
194
195 def __enter__(self):
83357fd5 196 self.anchor = self.log.start_section(self.marker, self.anchor)
d201506c
SW
197
198 def __exit__(self, extype, value, traceback):
199 self.log.end_section(self.marker)
200
201class Logfile(object):
e8debf39
SW
202 """Generates an HTML-formatted log file containing multiple streams of
203 data, each represented in a well-delineated/-structured fashion."""
d201506c
SW
204
205 def __init__(self, fn):
e8debf39 206 """Initialize a new object.
d201506c
SW
207
208 Args:
209 fn: The filename to write to.
210
211 Returns:
212 Nothing.
e8debf39 213 """
d201506c 214
a2ec5606 215 self.f = open(fn, 'wt')
d201506c
SW
216 self.last_stream = None
217 self.blocks = []
218 self.cur_evt = 1
83357fd5
SW
219 self.anchor = 0
220
a2ec5606
SW
221 shutil.copy(mod_dir + '/multiplexed_log.css', os.path.dirname(fn))
222 self.f.write('''\
d201506c
SW
223<html>
224<head>
225<link rel="stylesheet" type="text/css" href="multiplexed_log.css">
83357fd5
SW
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>
d201506c
SW
283</head>
284<body>
285<tt>
a2ec5606 286''')
d201506c
SW
287
288 def close(self):
e8debf39 289 """Close the log file.
d201506c
SW
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.
e8debf39 298 """
d201506c 299
a2ec5606 300 self.f.write('''\
d201506c
SW
301</tt>
302</body>
303</html>
a2ec5606 304''')
d201506c
SW
305 self.f.close()
306
307 # The set of characters that should be represented as hexadecimal codes in
308 # the log file.
a2ec5606
SW
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)))
d201506c
SW
311
312 def _escape(self, data):
e8debf39 313 """Render data format suitable for inclusion in an HTML document.
d201506c
SW
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.
e8debf39 323 """
d201506c 324
a2ec5606
SW
325 data = data.replace(chr(13), '')
326 data = ''.join((c in self._nonprint) and ('%%%02x' % ord(c)) or
d201506c
SW
327 c for c in data)
328 data = cgi.escape(data)
329 return data
330
331 def _terminate_stream(self):
e8debf39 332 """Write HTML to the log file to terminate the current stream's data.
d201506c
SW
333
334 Args:
335 None.
336
337 Returns:
338 Nothing.
e8debf39 339 """
d201506c
SW
340
341 self.cur_evt += 1
342 if not self.last_stream:
343 return
a2ec5606 344 self.f.write('</pre>\n')
83357fd5 345 self.f.write('<div class="stream-trailer block-trailer">End stream: ' +
a2ec5606
SW
346 self.last_stream.name + '</div>\n')
347 self.f.write('</div>\n')
83357fd5 348 self.f.write('</div>\n')
d201506c
SW
349 self.last_stream = None
350
83357fd5 351 def _note(self, note_type, msg, anchor=None):
e8debf39 352 """Write a note or one-off message to the log file.
d201506c
SW
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.
83357fd5 358 anchor: Optional internal link target.
d201506c
SW
359
360 Returns:
361 Nothing.
e8debf39 362 """
d201506c
SW
363
364 self._terminate_stream()
83357fd5
SW
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>')
d201506c 369 self.f.write(self._escape(msg))
83357fd5
SW
370 self.f.write('\n</pre>\n')
371 if anchor:
372 self.f.write('</a>\n')
373 self.f.write('</div>\n')
d201506c 374
83357fd5 375 def start_section(self, marker, anchor=None):
e8debf39 376 """Begin a new nested section in the log file.
d201506c
SW
377
378 Args:
379 marker: The name of the section that is starting.
83357fd5
SW
380 anchor: The value to use for the anchor. If None, a unique value
381 will be calculated and used
d201506c
SW
382
383 Returns:
83357fd5 384 Name of the HTML anchor emitted before section.
e8debf39 385 """
d201506c
SW
386
387 self._terminate_stream()
388 self.blocks.append(marker)
83357fd5
SW
389 if not anchor:
390 self.anchor += 1
391 anchor = str(self.anchor)
a2ec5606 392 blk_path = '/'.join(self.blocks)
83357fd5
SW
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
d201506c
SW
399
400 def end_section(self, marker):
e8debf39 401 """Terminate the current nested section in the log file.
d201506c
SW
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.
e8debf39 411 """
d201506c
SW
412
413 if (not self.blocks) or (marker != self.blocks[-1]):
a2ec5606
SW
414 raise Exception('Block nesting mismatch: "%s" "%s"' %
415 (marker, '/'.join(self.blocks)))
d201506c 416 self._terminate_stream()
a2ec5606 417 blk_path = '/'.join(self.blocks)
83357fd5
SW
418 self.f.write('<div class="section-trailer block-trailer">' +
419 'End section: ' + blk_path + '</div>\n')
420 self.f.write('</div>\n')
a2ec5606 421 self.f.write('</div>\n')
d201506c
SW
422 self.blocks.pop()
423
83357fd5 424 def section(self, marker, anchor=None):
e8debf39 425 """Create a temporary section in the log file.
d201506c
SW
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.
83357fd5 437 anchor: The anchor value to pass to start_section().
d201506c
SW
438
439 Returns:
440 A context manager object.
e8debf39 441 """
d201506c 442
83357fd5 443 return SectionCtxMgr(self, marker, anchor)
d201506c
SW
444
445 def error(self, msg):
e8debf39 446 """Write an error note to the log file.
d201506c
SW
447
448 Args:
449 msg: A message describing the error.
450
451 Returns:
452 Nothing.
e8debf39 453 """
d201506c
SW
454
455 self._note("error", msg)
456
457 def warning(self, msg):
e8debf39 458 """Write an warning note to the log file.
d201506c
SW
459
460 Args:
461 msg: A message describing the warning.
462
463 Returns:
464 Nothing.
e8debf39 465 """
d201506c
SW
466
467 self._note("warning", msg)
468
469 def info(self, msg):
e8debf39 470 """Write an informational note to the log file.
d201506c
SW
471
472 Args:
473 msg: An informational message.
474
475 Returns:
476 Nothing.
e8debf39 477 """
d201506c
SW
478
479 self._note("info", msg)
480
481 def action(self, msg):
e8debf39 482 """Write an action note to the log file.
d201506c
SW
483
484 Args:
485 msg: A message describing the action that is being logged.
486
487 Returns:
488 Nothing.
e8debf39 489 """
d201506c
SW
490
491 self._note("action", msg)
492
83357fd5 493 def status_pass(self, msg, anchor=None):
e8debf39 494 """Write a note to the log file describing test(s) which passed.
d201506c
SW
495
496 Args:
78b39cc3 497 msg: A message describing the passed test(s).
83357fd5 498 anchor: Optional internal link target.
d201506c
SW
499
500 Returns:
501 Nothing.
e8debf39 502 """
d201506c 503
83357fd5 504 self._note("status-pass", msg, anchor)
d201506c 505
83357fd5 506 def status_skipped(self, msg, anchor=None):
e8debf39 507 """Write a note to the log file describing skipped test(s).
d201506c
SW
508
509 Args:
78b39cc3 510 msg: A message describing the skipped test(s).
83357fd5 511 anchor: Optional internal link target.
d201506c
SW
512
513 Returns:
514 Nothing.
e8debf39 515 """
d201506c 516
83357fd5 517 self._note("status-skipped", msg, anchor)
d201506c 518
83357fd5 519 def status_xfail(self, msg, anchor=None):
78b39cc3
SW
520 """Write a note to the log file describing xfailed test(s).
521
522 Args:
523 msg: A message describing the xfailed test(s).
83357fd5 524 anchor: Optional internal link target.
78b39cc3
SW
525
526 Returns:
527 Nothing.
528 """
529
83357fd5 530 self._note("status-xfail", msg, anchor)
78b39cc3 531
83357fd5 532 def status_xpass(self, msg, anchor=None):
78b39cc3
SW
533 """Write a note to the log file describing xpassed test(s).
534
535 Args:
536 msg: A message describing the xpassed test(s).
83357fd5 537 anchor: Optional internal link target.
78b39cc3
SW
538
539 Returns:
540 Nothing.
541 """
542
83357fd5 543 self._note("status-xpass", msg, anchor)
78b39cc3 544
83357fd5 545 def status_fail(self, msg, anchor=None):
e8debf39 546 """Write a note to the log file describing failed test(s).
d201506c
SW
547
548 Args:
78b39cc3 549 msg: A message describing the failed test(s).
83357fd5 550 anchor: Optional internal link target.
d201506c
SW
551
552 Returns:
553 Nothing.
e8debf39 554 """
d201506c 555
83357fd5 556 self._note("status-fail", msg, anchor)
d201506c
SW
557
558 def get_stream(self, name, chained_file=None):
e8debf39 559 """Create an object to log a single stream's data into the log file.
d201506c
SW
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.
e8debf39 574 """
d201506c
SW
575
576 return LogfileStream(self, name, chained_file)
577
578 def get_runner(self, name, chained_file=None):
e8debf39 579 """Create an object that executes processes and logs their output.
d201506c
SW
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.
e8debf39 588 """
d201506c
SW
589
590 return RunAndLog(self, name, chained_file)
591
592 def write(self, stream, data, implicit=False):
e8debf39 593 """Write stream data into the log file.
d201506c
SW
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.
e8debf39 609 """
d201506c
SW
610
611 if stream != self.last_stream:
612 self._terminate_stream()
83357fd5
SW
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')
a2ec5606 617 self.f.write('<pre>')
d201506c 618 if implicit:
a2ec5606 619 self.f.write('<span class="implicit">')
d201506c
SW
620 self.f.write(self._escape(data))
621 if implicit:
a2ec5606 622 self.f.write('</span>')
d201506c
SW
623 self.last_stream = stream
624
625 def flush(self):
e8debf39 626 """Flush the log stream, to ensure correct log interleaving.
d201506c
SW
627
628 Args:
629 None.
630
631 Returns:
632 Nothing.
e8debf39 633 """
d201506c
SW
634
635 self.f.flush()