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