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