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