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