blob: 48f2b51de15f7f6a1ef3b9c407f5bcf49cc785f9 [file] [log] [blame]
Stephen Warren10e50632016-01-15 11:15:24 -07001# 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):
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
83class 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
105 def close(self):
106 '''Clean up any resources managed by this object.'''
107 pass
108
109 def run(self, cmd, cwd=None):
110 '''Run a command as a sub-process, and log the results.
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.
116
117 Returns:
118 Nothing.
119 '''
120
121 msg = "+" + " ".join(cmd) + "\n"
122 if self.chained_file:
123 self.chained_file.write(msg)
124 self.logfile.write(self, msg)
125
126 try:
127 p = subprocess.Popen(cmd, cwd=cwd,
128 stdin=None, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
129 (stdout, stderr) = p.communicate()
130 output = ''
131 if stdout:
132 if stderr:
133 output += 'stdout:\n'
134 output += stdout
135 if stderr:
136 if stdout:
137 output += 'stderr:\n'
138 output += stderr
139 exit_status = p.returncode
140 exception = None
141 except subprocess.CalledProcessError as cpe:
142 output = cpe.output
143 exit_status = cpe.returncode
144 exception = cpe
145 except Exception as e:
146 output = ''
147 exit_status = 0
148 exception = e
149 if output and not output.endswith('\n'):
150 output += '\n'
151 if exit_status and not exception:
152 exception = Exception('Exit code: ' + str(exit_status))
153 if exception:
154 output += str(exception) + '\n'
155 self.logfile.write(self, output)
156 if self.chained_file:
157 self.chained_file.write(output)
158 if exception:
159 raise exception
160
161class SectionCtxMgr(object):
162 '''A context manager for Python's "with" statement, which allows a certain
163 portion of test code to be logged to a separate section of the log file.
164 Objects of this type should be created by factory functions in the Logfile
165 class rather than directly.'''
166
167 def __init__(self, log, marker):
168 '''Initialize a new object.
169
170 Args:
171 log: The Logfile object to log to.
172 marker: The name of the nested log section.
173
174 Returns:
175 Nothing.
176 '''
177
178 self.log = log
179 self.marker = marker
180
181 def __enter__(self):
182 self.log.start_section(self.marker)
183
184 def __exit__(self, extype, value, traceback):
185 self.log.end_section(self.marker)
186
187class Logfile(object):
188 '''Generates an HTML-formatted log file containing multiple streams of
189 data, each represented in a well-delineated/-structured fashion.'''
190
191 def __init__(self, fn):
192 '''Initialize a new object.
193
194 Args:
195 fn: The filename to write to.
196
197 Returns:
198 Nothing.
199 '''
200
201 self.f = open(fn, "wt")
202 self.last_stream = None
203 self.blocks = []
204 self.cur_evt = 1
205 shutil.copy(mod_dir + "/multiplexed_log.css", os.path.dirname(fn))
206 self.f.write("""\
207<html>
208<head>
209<link rel="stylesheet" type="text/css" href="multiplexed_log.css">
210</head>
211<body>
212<tt>
213""")
214
215 def close(self):
216 '''Close the log file.
217
218 After calling this function, no more data may be written to the log.
219
220 Args:
221 None.
222
223 Returns:
224 Nothing.
225 '''
226
227 self.f.write("""\
228</tt>
229</body>
230</html>
231""")
232 self.f.close()
233
234 # The set of characters that should be represented as hexadecimal codes in
235 # the log file.
236 _nonprint = ("%" + "".join(chr(c) for c in range(0, 32) if c not in (9, 10)) +
237 "".join(chr(c) for c in range(127, 256)))
238
239 def _escape(self, data):
240 '''Render data format suitable for inclusion in an HTML document.
241
242 This includes HTML-escaping certain characters, and translating
243 control characters to a hexadecimal representation.
244
245 Args:
246 data: The raw string data to be escaped.
247
248 Returns:
249 An escaped version of the data.
250 '''
251
252 data = data.replace(chr(13), "")
253 data = "".join((c in self._nonprint) and ("%%%02x" % ord(c)) or
254 c for c in data)
255 data = cgi.escape(data)
256 return data
257
258 def _terminate_stream(self):
259 '''Write HTML to the log file to terminate the current stream's data.
260
261 Args:
262 None.
263
264 Returns:
265 Nothing.
266 '''
267
268 self.cur_evt += 1
269 if not self.last_stream:
270 return
271 self.f.write("</pre>\n")
272 self.f.write("<div class=\"stream-trailer\" id=\"" +
273 self.last_stream.name + "\">End stream: " +
274 self.last_stream.name + "</div>\n")
275 self.f.write("</div>\n")
276 self.last_stream = None
277
278 def _note(self, note_type, msg):
279 '''Write a note or one-off message to the log file.
280
281 Args:
282 note_type: The type of note. This must be a value supported by the
283 accompanying multiplexed_log.css.
284 msg: The note/message to log.
285
286 Returns:
287 Nothing.
288 '''
289
290 self._terminate_stream()
291 self.f.write("<div class=\"" + note_type + "\">\n<pre>")
292 self.f.write(self._escape(msg))
293 self.f.write("\n</pre></div>\n")
294
295 def start_section(self, marker):
296 '''Begin a new nested section in the log file.
297
298 Args:
299 marker: The name of the section that is starting.
300
301 Returns:
302 Nothing.
303 '''
304
305 self._terminate_stream()
306 self.blocks.append(marker)
307 blk_path = "/".join(self.blocks)
308 self.f.write("<div class=\"section\" id=\"" + blk_path + "\">\n")
309 self.f.write("<div class=\"section-header\" id=\"" + blk_path +
310 "\">Section: " + blk_path + "</div>\n")
311
312 def end_section(self, marker):
313 '''Terminate the current nested section in the log file.
314
315 This function validates proper nesting of start_section() and
316 end_section() calls. If a mismatch is found, an exception is raised.
317
318 Args:
319 marker: The name of the section that is ending.
320
321 Returns:
322 Nothing.
323 '''
324
325 if (not self.blocks) or (marker != self.blocks[-1]):
326 raise Exception("Block nesting mismatch: \"%s\" \"%s\"" %
327 (marker, "/".join(self.blocks)))
328 self._terminate_stream()
329 blk_path = "/".join(self.blocks)
330 self.f.write("<div class=\"section-trailer\" id=\"section-trailer-" +
331 blk_path + "\">End section: " + blk_path + "</div>\n")
332 self.f.write("</div>\n")
333 self.blocks.pop()
334
335 def section(self, marker):
336 '''Create a temporary section in the log file.
337
338 This function creates a context manager for Python's "with" statement,
339 which allows a certain portion of test code to be logged to a separate
340 section of the log file.
341
342 Usage:
343 with log.section("somename"):
344 some test code
345
346 Args:
347 marker: The name of the nested section.
348
349 Returns:
350 A context manager object.
351 '''
352
353 return SectionCtxMgr(self, marker)
354
355 def error(self, msg):
356 '''Write an error note to the log file.
357
358 Args:
359 msg: A message describing the error.
360
361 Returns:
362 Nothing.
363 '''
364
365 self._note("error", msg)
366
367 def warning(self, msg):
368 '''Write an warning note to the log file.
369
370 Args:
371 msg: A message describing the warning.
372
373 Returns:
374 Nothing.
375 '''
376
377 self._note("warning", msg)
378
379 def info(self, msg):
380 '''Write an informational note to the log file.
381
382 Args:
383 msg: An informational message.
384
385 Returns:
386 Nothing.
387 '''
388
389 self._note("info", msg)
390
391 def action(self, msg):
392 '''Write an action note to the log file.
393
394 Args:
395 msg: A message describing the action that is being logged.
396
397 Returns:
398 Nothing.
399 '''
400
401 self._note("action", msg)
402
403 def status_pass(self, msg):
404 '''Write a note to the log file describing test(s) which passed.
405
406 Args:
407 msg: A message describing passed test(s).
408
409 Returns:
410 Nothing.
411 '''
412
413 self._note("status-pass", msg)
414
415 def status_skipped(self, msg):
416 '''Write a note to the log file describing skipped test(s).
417
418 Args:
419 msg: A message describing passed test(s).
420
421 Returns:
422 Nothing.
423 '''
424
425 self._note("status-skipped", msg)
426
427 def status_fail(self, msg):
428 '''Write a note to the log file describing failed test(s).
429
430 Args:
431 msg: A message describing passed test(s).
432
433 Returns:
434 Nothing.
435 '''
436
437 self._note("status-fail", msg)
438
439 def get_stream(self, name, chained_file=None):
440 '''Create an object to log a single stream's data into the log file.
441
442 This creates a "file-like" object that can be written to in order to
443 write a single stream's data to the log file. The implementation will
444 handle any required interleaving of data (from multiple streams) in
445 the log, in a way that makes it obvious which stream each bit of data
446 came from.
447
448 Args:
449 name: The name of the stream.
450 chained_file: The file-like object to which all stream data should
451 be logged to in addition to this log. Can be None.
452
453 Returns:
454 A file-like object.
455 '''
456
457 return LogfileStream(self, name, chained_file)
458
459 def get_runner(self, name, chained_file=None):
460 '''Create an object that executes processes and logs their output.
461
462 Args:
463 name: The name of this sub-process.
464 chained_file: The file-like object to which all stream data should
465 be logged to in addition to logfile. Can be None.
466
467 Returns:
468 A RunAndLog object.
469 '''
470
471 return RunAndLog(self, name, chained_file)
472
473 def write(self, stream, data, implicit=False):
474 '''Write stream data into the log file.
475
476 This function should only be used by instances of LogfileStream or
477 RunAndLog.
478
479 Args:
480 stream: The stream whose data is being logged.
481 data: The data to log.
482 implicit: Boolean indicating whether data actually appeared in the
483 stream, or was implicitly generated. A valid use-case is to
484 repeat a shell prompt at the start of each separate log
485 section, which makes the log sections more readable in
486 isolation.
487
488 Returns:
489 Nothing.
490 '''
491
492 if stream != self.last_stream:
493 self._terminate_stream()
494 self.f.write("<div class=\"stream\" id=\"%s\">\n" % stream.name)
495 self.f.write("<div class=\"stream-header\" id=\"" + stream.name +
496 "\">Stream: " + stream.name + "</div>\n")
497 self.f.write("<pre>")
498 if implicit:
499 self.f.write("<span class=\"implicit\">")
500 self.f.write(self._escape(data))
501 if implicit:
502 self.f.write("</span>")
503 self.last_stream = stream
504
505 def flush(self):
506 '''Flush the log stream, to ensure correct log interleaving.
507
508 Args:
509 None.
510
511 Returns:
512 Nothing.
513 '''
514
515 self.f.flush()