]>
Commit | Line | Data |
---|---|---|
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 | ||
8 | import cgi | |
9679d339 | 9 | import datetime |
d201506c SW |
10 | import os.path |
11 | import shutil | |
12 | import subprocess | |
13 | ||
14 | mod_dir = os.path.dirname(os.path.abspath(__file__)) | |
15 | ||
16 | class 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 | ||
83 | class 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 | |
176 | class 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 | ||
204 | class 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() |