Stephen Warren | 10e5063 | 2016-01-15 11:15:24 -0700 | [diff] [blame^] | 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 | |
| 9 | import cgi |
| 10 | import os.path |
| 11 | import shutil |
| 12 | import subprocess |
| 13 | |
| 14 | mod_dir = os.path.dirname(os.path.abspath(__file__)) |
| 15 | |
| 16 | class 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 | |
| 83 | class 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 | |
| 161 | class 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 | |
| 187 | class 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() |