]> git.ipfire.org Git - people/ms/u-boot.git/blame - test/py/multiplexed_log.py
test/py: correctly log xfail/xpass tests
[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
104
105 def close(self):
e8debf39 106 """Clean up any resources managed by this object."""
d201506c
SW
107 pass
108
3f2faf73 109 def run(self, cmd, cwd=None, ignore_errors=False):
e8debf39 110 """Run a command as a sub-process, and log the results.
d201506c
SW
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.
3f2faf73
SW
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.
d201506c
SW
120
121 Returns:
122 Nothing.
e8debf39 123 """
d201506c 124
a2ec5606 125 msg = '+' + ' '.join(cmd) + '\n'
d201506c
SW
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'
3f2faf73 155 if exit_status and not exception and not ignore_errors:
d201506c
SW
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
165class SectionCtxMgr(object):
e8debf39 166 """A context manager for Python's "with" statement, which allows a certain
d201506c
SW
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
e8debf39 169 class rather than directly."""
d201506c
SW
170
171 def __init__(self, log, marker):
e8debf39 172 """Initialize a new object.
d201506c
SW
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.
e8debf39 180 """
d201506c
SW
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
191class Logfile(object):
e8debf39
SW
192 """Generates an HTML-formatted log file containing multiple streams of
193 data, each represented in a well-delineated/-structured fashion."""
d201506c
SW
194
195 def __init__(self, fn):
e8debf39 196 """Initialize a new object.
d201506c
SW
197
198 Args:
199 fn: The filename to write to.
200
201 Returns:
202 Nothing.
e8debf39 203 """
d201506c 204
a2ec5606 205 self.f = open(fn, 'wt')
d201506c
SW
206 self.last_stream = None
207 self.blocks = []
208 self.cur_evt = 1
a2ec5606
SW
209 shutil.copy(mod_dir + '/multiplexed_log.css', os.path.dirname(fn))
210 self.f.write('''\
d201506c
SW
211<html>
212<head>
213<link rel="stylesheet" type="text/css" href="multiplexed_log.css">
214</head>
215<body>
216<tt>
a2ec5606 217''')
d201506c
SW
218
219 def close(self):
e8debf39 220 """Close the log file.
d201506c
SW
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.
e8debf39 229 """
d201506c 230
a2ec5606 231 self.f.write('''\
d201506c
SW
232</tt>
233</body>
234</html>
a2ec5606 235''')
d201506c
SW
236 self.f.close()
237
238 # The set of characters that should be represented as hexadecimal codes in
239 # the log file.
a2ec5606
SW
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)))
d201506c
SW
242
243 def _escape(self, data):
e8debf39 244 """Render data format suitable for inclusion in an HTML document.
d201506c
SW
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.
e8debf39 254 """
d201506c 255
a2ec5606
SW
256 data = data.replace(chr(13), '')
257 data = ''.join((c in self._nonprint) and ('%%%02x' % ord(c)) or
d201506c
SW
258 c for c in data)
259 data = cgi.escape(data)
260 return data
261
262 def _terminate_stream(self):
e8debf39 263 """Write HTML to the log file to terminate the current stream's data.
d201506c
SW
264
265 Args:
266 None.
267
268 Returns:
269 Nothing.
e8debf39 270 """
d201506c
SW
271
272 self.cur_evt += 1
273 if not self.last_stream:
274 return
a2ec5606
SW
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')
d201506c
SW
280 self.last_stream = None
281
282 def _note(self, note_type, msg):
e8debf39 283 """Write a note or one-off message to the log file.
d201506c
SW
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.
e8debf39 292 """
d201506c
SW
293
294 self._terminate_stream()
a2ec5606 295 self.f.write('<div class="' + note_type + '">\n<pre>')
d201506c 296 self.f.write(self._escape(msg))
a2ec5606 297 self.f.write('\n</pre></div>\n')
d201506c
SW
298
299 def start_section(self, marker):
e8debf39 300 """Begin a new nested section in the log file.
d201506c
SW
301
302 Args:
303 marker: The name of the section that is starting.
304
305 Returns:
306 Nothing.
e8debf39 307 """
d201506c
SW
308
309 self._terminate_stream()
310 self.blocks.append(marker)
a2ec5606
SW
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')
d201506c
SW
315
316 def end_section(self, marker):
e8debf39 317 """Terminate the current nested section in the log file.
d201506c
SW
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.
e8debf39 327 """
d201506c
SW
328
329 if (not self.blocks) or (marker != self.blocks[-1]):
a2ec5606
SW
330 raise Exception('Block nesting mismatch: "%s" "%s"' %
331 (marker, '/'.join(self.blocks)))
d201506c 332 self._terminate_stream()
a2ec5606
SW
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')
d201506c
SW
337 self.blocks.pop()
338
339 def section(self, marker):
e8debf39 340 """Create a temporary section in the log file.
d201506c
SW
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.
e8debf39 355 """
d201506c
SW
356
357 return SectionCtxMgr(self, marker)
358
359 def error(self, msg):
e8debf39 360 """Write an error note to the log file.
d201506c
SW
361
362 Args:
363 msg: A message describing the error.
364
365 Returns:
366 Nothing.
e8debf39 367 """
d201506c
SW
368
369 self._note("error", msg)
370
371 def warning(self, msg):
e8debf39 372 """Write an warning note to the log file.
d201506c
SW
373
374 Args:
375 msg: A message describing the warning.
376
377 Returns:
378 Nothing.
e8debf39 379 """
d201506c
SW
380
381 self._note("warning", msg)
382
383 def info(self, msg):
e8debf39 384 """Write an informational note to the log file.
d201506c
SW
385
386 Args:
387 msg: An informational message.
388
389 Returns:
390 Nothing.
e8debf39 391 """
d201506c
SW
392
393 self._note("info", msg)
394
395 def action(self, msg):
e8debf39 396 """Write an action note to the log file.
d201506c
SW
397
398 Args:
399 msg: A message describing the action that is being logged.
400
401 Returns:
402 Nothing.
e8debf39 403 """
d201506c
SW
404
405 self._note("action", msg)
406
407 def status_pass(self, msg):
e8debf39 408 """Write a note to the log file describing test(s) which passed.
d201506c
SW
409
410 Args:
78b39cc3 411 msg: A message describing the passed test(s).
d201506c
SW
412
413 Returns:
414 Nothing.
e8debf39 415 """
d201506c
SW
416
417 self._note("status-pass", msg)
418
419 def status_skipped(self, msg):
e8debf39 420 """Write a note to the log file describing skipped test(s).
d201506c
SW
421
422 Args:
78b39cc3 423 msg: A message describing the skipped test(s).
d201506c
SW
424
425 Returns:
426 Nothing.
e8debf39 427 """
d201506c
SW
428
429 self._note("status-skipped", msg)
430
78b39cc3
SW
431 def status_xfail(self, msg):
432 """Write a note to the log file describing xfailed test(s).
433
434 Args:
435 msg: A message describing the xfailed test(s).
436
437 Returns:
438 Nothing.
439 """
440
441 self._note("status-xfail", msg)
442
443 def status_xpass(self, msg):
444 """Write a note to the log file describing xpassed test(s).
445
446 Args:
447 msg: A message describing the xpassed test(s).
448
449 Returns:
450 Nothing.
451 """
452
453 self._note("status-xpass", msg)
454
d201506c 455 def status_fail(self, msg):
e8debf39 456 """Write a note to the log file describing failed test(s).
d201506c
SW
457
458 Args:
78b39cc3 459 msg: A message describing the failed test(s).
d201506c
SW
460
461 Returns:
462 Nothing.
e8debf39 463 """
d201506c
SW
464
465 self._note("status-fail", msg)
466
467 def get_stream(self, name, chained_file=None):
e8debf39 468 """Create an object to log a single stream's data into the log file.
d201506c
SW
469
470 This creates a "file-like" object that can be written to in order to
471 write a single stream's data to the log file. The implementation will
472 handle any required interleaving of data (from multiple streams) in
473 the log, in a way that makes it obvious which stream each bit of data
474 came from.
475
476 Args:
477 name: The name of the stream.
478 chained_file: The file-like object to which all stream data should
479 be logged to in addition to this log. Can be None.
480
481 Returns:
482 A file-like object.
e8debf39 483 """
d201506c
SW
484
485 return LogfileStream(self, name, chained_file)
486
487 def get_runner(self, name, chained_file=None):
e8debf39 488 """Create an object that executes processes and logs their output.
d201506c
SW
489
490 Args:
491 name: The name of this sub-process.
492 chained_file: The file-like object to which all stream data should
493 be logged to in addition to logfile. Can be None.
494
495 Returns:
496 A RunAndLog object.
e8debf39 497 """
d201506c
SW
498
499 return RunAndLog(self, name, chained_file)
500
501 def write(self, stream, data, implicit=False):
e8debf39 502 """Write stream data into the log file.
d201506c
SW
503
504 This function should only be used by instances of LogfileStream or
505 RunAndLog.
506
507 Args:
508 stream: The stream whose data is being logged.
509 data: The data to log.
510 implicit: Boolean indicating whether data actually appeared in the
511 stream, or was implicitly generated. A valid use-case is to
512 repeat a shell prompt at the start of each separate log
513 section, which makes the log sections more readable in
514 isolation.
515
516 Returns:
517 Nothing.
e8debf39 518 """
d201506c
SW
519
520 if stream != self.last_stream:
521 self._terminate_stream()
a2ec5606
SW
522 self.f.write('<div class="stream" id="%s">\n' % stream.name)
523 self.f.write('<div class="stream-header" id="' + stream.name +
524 '">Stream: ' + stream.name + '</div>\n')
525 self.f.write('<pre>')
d201506c 526 if implicit:
a2ec5606 527 self.f.write('<span class="implicit">')
d201506c
SW
528 self.f.write(self._escape(data))
529 if implicit:
a2ec5606 530 self.f.write('</span>')
d201506c
SW
531 self.last_stream = stream
532
533 def flush(self):
e8debf39 534 """Flush the log stream, to ensure correct log interleaving.
d201506c
SW
535
536 Args:
537 None.
538
539 Returns:
540 Nothing.
e8debf39 541 """
d201506c
SW
542
543 self.f.flush()