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