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 105 def close(self): 106 '''Clean up any resources managed by this object.''' 107 pass 108 109 def run(self, cmd, cwd=None): 110 '''Run a command as a sub-process, and log the results. 111 112 Args: 113 cmd: The command to execute. 114 cwd: The directory to run the command in. Can be None to use the 115 current directory. 116 117 Returns: 118 Nothing. 119 ''' 120 121 msg = "+" + " ".join(cmd) + "\n" 122 if self.chained_file: 123 self.chained_file.write(msg) 124 self.logfile.write(self, msg) 125 126 try: 127 p = subprocess.Popen(cmd, cwd=cwd, 128 stdin=None, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) 129 (stdout, stderr) = p.communicate() 130 output = '' 131 if stdout: 132 if stderr: 133 output += 'stdout:\n' 134 output += stdout 135 if stderr: 136 if stdout: 137 output += 'stderr:\n' 138 output += stderr 139 exit_status = p.returncode 140 exception = None 141 except subprocess.CalledProcessError as cpe: 142 output = cpe.output 143 exit_status = cpe.returncode 144 exception = cpe 145 except Exception as e: 146 output = '' 147 exit_status = 0 148 exception = e 149 if output and not output.endswith('\n'): 150 output += '\n' 151 if exit_status and not exception: 152 exception = Exception('Exit code: ' + str(exit_status)) 153 if exception: 154 output += str(exception) + '\n' 155 self.logfile.write(self, output) 156 if self.chained_file: 157 self.chained_file.write(output) 158 if exception: 159 raise exception 160 161class SectionCtxMgr(object): 162 '''A context manager for Python's "with" statement, which allows a certain 163 portion of test code to be logged to a separate section of the log file. 164 Objects of this type should be created by factory functions in the Logfile 165 class rather than directly.''' 166 167 def __init__(self, log, marker): 168 '''Initialize a new object. 169 170 Args: 171 log: The Logfile object to log to. 172 marker: The name of the nested log section. 173 174 Returns: 175 Nothing. 176 ''' 177 178 self.log = log 179 self.marker = marker 180 181 def __enter__(self): 182 self.log.start_section(self.marker) 183 184 def __exit__(self, extype, value, traceback): 185 self.log.end_section(self.marker) 186 187class Logfile(object): 188 '''Generates an HTML-formatted log file containing multiple streams of 189 data, each represented in a well-delineated/-structured fashion.''' 190 191 def __init__(self, fn): 192 '''Initialize a new object. 193 194 Args: 195 fn: The filename to write to. 196 197 Returns: 198 Nothing. 199 ''' 200 201 self.f = open(fn, "wt") 202 self.last_stream = None 203 self.blocks = [] 204 self.cur_evt = 1 205 shutil.copy(mod_dir + "/multiplexed_log.css", os.path.dirname(fn)) 206 self.f.write("""\ 207<html> 208<head> 209<link rel="stylesheet" type="text/css" href="multiplexed_log.css"> 210</head> 211<body> 212<tt> 213""") 214 215 def close(self): 216 '''Close the log file. 217 218 After calling this function, no more data may be written to the log. 219 220 Args: 221 None. 222 223 Returns: 224 Nothing. 225 ''' 226 227 self.f.write("""\ 228</tt> 229</body> 230</html> 231""") 232 self.f.close() 233 234 # The set of characters that should be represented as hexadecimal codes in 235 # the log file. 236 _nonprint = ("%" + "".join(chr(c) for c in range(0, 32) if c not in (9, 10)) + 237 "".join(chr(c) for c in range(127, 256))) 238 239 def _escape(self, data): 240 '''Render data format suitable for inclusion in an HTML document. 241 242 This includes HTML-escaping certain characters, and translating 243 control characters to a hexadecimal representation. 244 245 Args: 246 data: The raw string data to be escaped. 247 248 Returns: 249 An escaped version of the data. 250 ''' 251 252 data = data.replace(chr(13), "") 253 data = "".join((c in self._nonprint) and ("%%%02x" % ord(c)) or 254 c for c in data) 255 data = cgi.escape(data) 256 return data 257 258 def _terminate_stream(self): 259 '''Write HTML to the log file to terminate the current stream's data. 260 261 Args: 262 None. 263 264 Returns: 265 Nothing. 266 ''' 267 268 self.cur_evt += 1 269 if not self.last_stream: 270 return 271 self.f.write("</pre>\n") 272 self.f.write("<div class=\"stream-trailer\" id=\"" + 273 self.last_stream.name + "\">End stream: " + 274 self.last_stream.name + "</div>\n") 275 self.f.write("</div>\n") 276 self.last_stream = None 277 278 def _note(self, note_type, msg): 279 '''Write a note or one-off message to the log file. 280 281 Args: 282 note_type: The type of note. This must be a value supported by the 283 accompanying multiplexed_log.css. 284 msg: The note/message to log. 285 286 Returns: 287 Nothing. 288 ''' 289 290 self._terminate_stream() 291 self.f.write("<div class=\"" + note_type + "\">\n<pre>") 292 self.f.write(self._escape(msg)) 293 self.f.write("\n</pre></div>\n") 294 295 def start_section(self, marker): 296 '''Begin a new nested section in the log file. 297 298 Args: 299 marker: The name of the section that is starting. 300 301 Returns: 302 Nothing. 303 ''' 304 305 self._terminate_stream() 306 self.blocks.append(marker) 307 blk_path = "/".join(self.blocks) 308 self.f.write("<div class=\"section\" id=\"" + blk_path + "\">\n") 309 self.f.write("<div class=\"section-header\" id=\"" + blk_path + 310 "\">Section: " + blk_path + "</div>\n") 311 312 def end_section(self, marker): 313 '''Terminate the current nested section in the log file. 314 315 This function validates proper nesting of start_section() and 316 end_section() calls. If a mismatch is found, an exception is raised. 317 318 Args: 319 marker: The name of the section that is ending. 320 321 Returns: 322 Nothing. 323 ''' 324 325 if (not self.blocks) or (marker != self.blocks[-1]): 326 raise Exception("Block nesting mismatch: \"%s\" \"%s\"" % 327 (marker, "/".join(self.blocks))) 328 self._terminate_stream() 329 blk_path = "/".join(self.blocks) 330 self.f.write("<div class=\"section-trailer\" id=\"section-trailer-" + 331 blk_path + "\">End section: " + blk_path + "</div>\n") 332 self.f.write("</div>\n") 333 self.blocks.pop() 334 335 def section(self, marker): 336 '''Create a temporary section in the log file. 337 338 This function creates a context manager for Python's "with" statement, 339 which allows a certain portion of test code to be logged to a separate 340 section of the log file. 341 342 Usage: 343 with log.section("somename"): 344 some test code 345 346 Args: 347 marker: The name of the nested section. 348 349 Returns: 350 A context manager object. 351 ''' 352 353 return SectionCtxMgr(self, marker) 354 355 def error(self, msg): 356 '''Write an error note to the log file. 357 358 Args: 359 msg: A message describing the error. 360 361 Returns: 362 Nothing. 363 ''' 364 365 self._note("error", msg) 366 367 def warning(self, msg): 368 '''Write an warning note to the log file. 369 370 Args: 371 msg: A message describing the warning. 372 373 Returns: 374 Nothing. 375 ''' 376 377 self._note("warning", msg) 378 379 def info(self, msg): 380 '''Write an informational note to the log file. 381 382 Args: 383 msg: An informational message. 384 385 Returns: 386 Nothing. 387 ''' 388 389 self._note("info", msg) 390 391 def action(self, msg): 392 '''Write an action note to the log file. 393 394 Args: 395 msg: A message describing the action that is being logged. 396 397 Returns: 398 Nothing. 399 ''' 400 401 self._note("action", msg) 402 403 def status_pass(self, msg): 404 '''Write a note to the log file describing test(s) which passed. 405 406 Args: 407 msg: A message describing passed test(s). 408 409 Returns: 410 Nothing. 411 ''' 412 413 self._note("status-pass", msg) 414 415 def status_skipped(self, msg): 416 '''Write a note to the log file describing skipped test(s). 417 418 Args: 419 msg: A message describing passed test(s). 420 421 Returns: 422 Nothing. 423 ''' 424 425 self._note("status-skipped", msg) 426 427 def status_fail(self, msg): 428 '''Write a note to the log file describing failed test(s). 429 430 Args: 431 msg: A message describing passed test(s). 432 433 Returns: 434 Nothing. 435 ''' 436 437 self._note("status-fail", msg) 438 439 def get_stream(self, name, chained_file=None): 440 '''Create an object to log a single stream's data into the log file. 441 442 This creates a "file-like" object that can be written to in order to 443 write a single stream's data to the log file. The implementation will 444 handle any required interleaving of data (from multiple streams) in 445 the log, in a way that makes it obvious which stream each bit of data 446 came from. 447 448 Args: 449 name: The name of the stream. 450 chained_file: The file-like object to which all stream data should 451 be logged to in addition to this log. Can be None. 452 453 Returns: 454 A file-like object. 455 ''' 456 457 return LogfileStream(self, name, chained_file) 458 459 def get_runner(self, name, chained_file=None): 460 '''Create an object that executes processes and logs their output. 461 462 Args: 463 name: The name of this sub-process. 464 chained_file: The file-like object to which all stream data should 465 be logged to in addition to logfile. Can be None. 466 467 Returns: 468 A RunAndLog object. 469 ''' 470 471 return RunAndLog(self, name, chained_file) 472 473 def write(self, stream, data, implicit=False): 474 '''Write stream data into the log file. 475 476 This function should only be used by instances of LogfileStream or 477 RunAndLog. 478 479 Args: 480 stream: The stream whose data is being logged. 481 data: The data to log. 482 implicit: Boolean indicating whether data actually appeared in the 483 stream, or was implicitly generated. A valid use-case is to 484 repeat a shell prompt at the start of each separate log 485 section, which makes the log sections more readable in 486 isolation. 487 488 Returns: 489 Nothing. 490 ''' 491 492 if stream != self.last_stream: 493 self._terminate_stream() 494 self.f.write("<div class=\"stream\" id=\"%s\">\n" % stream.name) 495 self.f.write("<div class=\"stream-header\" id=\"" + stream.name + 496 "\">Stream: " + stream.name + "</div>\n") 497 self.f.write("<pre>") 498 if implicit: 499 self.f.write("<span class=\"implicit\">") 500 self.f.write(self._escape(data)) 501 if implicit: 502 self.f.write("</span>") 503 self.last_stream = stream 504 505 def flush(self): 506 '''Flush the log stream, to ensure correct log interleaving. 507 508 Args: 509 None. 510 511 Returns: 512 Nothing. 513 ''' 514 515 self.f.flush() 516