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