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