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