Package pysys :: Package process :: Module user
[hide private]
[frames] | no frames]

Source Code for Module pysys.process.user

  1  #!/usr/bin/env python 
  2  # PySys System Test Framework, Copyright (C) 2006-2016  M.B.Grieve 
  3   
  4  # This library is free software; you can redistribute it and/or 
  5  # modify it under the terms of the GNU Lesser General Public 
  6  # License as published by the Free Software Foundation; either 
  7  # version 2.1 of the License, or (at your option) any later version. 
  8   
  9  # This library is distributed in the hope that it will be useful, 
 10  # but WITHOUT ANY WARRANTY; without even the implied warranty of 
 11  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU 
 12  # Lesser General Public License for more details. 
 13   
 14  # You should have received a copy of the GNU Lesser General Public 
 15  # License along with this library; if not, write to the Free Software 
 16  # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 
 17   
 18  # Contact: moraygrieve@users.sourceforge.net 
 19   
 20  import sys, os, time, collections, inspect 
 21   
 22  from pysys import log 
 23  from pysys.constants import * 
 24  from pysys.exceptions import * 
 25  from pysys.utils.filegrep import getmatches 
 26  from pysys.process.helper import ProcessWrapper 
 27  from pysys.utils.allocport import TCPPortOwner 
 28   
 29  STDOUTERR_TUPLE = collections.namedtuple('stdouterr', ['stdout', 'stderr']) 
 30   
 31   
32 -class ProcessUser(object):
33 """Class providing basic operations over interacting with processes. 34 35 The ProcessUser class provides the minimum set of operations for managing and interacting with 36 processes. The class is designed to be extended by the L{pysys.baserunner.BaseRunner} and 37 L{pysys.basetest.BaseTest} classes so that they prescribe a common set of process operations 38 that any child test can use. Process operations have associated potential outcomes in their 39 execution, e.g. C{BLOCKED}, C{TIMEDOUT}, C{DUMPEDCORE} etc. As such the class additionally acts 40 as the container for storing the list of outcomes from all child test related actions. 41 42 @ivar input: Location for input to any processes (defaults to current working directory) 43 @type input: string 44 @ivar output: Location for output from any processes (defaults to current working directory) 45 @type output: string 46 47 """ 48
49 - def __init__(self):
50 """Default constructor. 51 52 """ 53 self.log = log 54 self.project = PROJECT 55 56 self.processList = [] 57 self.processCount = {} 58 self.__cleanupFunctions = [] 59 60 self.outcome = [] 61 self.__outcomeReason = '' 62 63 self.defaultAbortOnError = PROJECT.defaultAbortOnError.lower()=='true' if hasattr(PROJECT, 'defaultAbortOnError') else DEFAULT_ABORT_ON_ERROR 64 self.__uniqueProcessKeys = {}
65 66
67 - def __getattr__(self, name):
68 """Set self.input or self.output to the current working directory if not defined. 69 70 """ 71 if name == "input" or name == "output": 72 return os.getcwd() 73 else: 74 raise AttributeError("Unknown class attribute ", name)
75 76
77 - def getInstanceCount(self, displayName):
78 """Return the number of processes started within the testcase matching the supplied displayName. 79 80 The ProcessUser class maintains a reference count of processes started within the class instance 81 via the L{startProcess()} method. The reference count is maintained against a logical name for 82 the process, which is the displayName used in the method call to L{startProcess()}, or the 83 basename of the command if no displayName was supplied. The method returns the number of 84 processes started with the supplied logical name, or 0 if no processes have been started. 85 86 @deprecated: The recommended way to allocate unique names is now L{allocateUniqueStdOutErr} 87 @param displayName: The process display name 88 @return: The number of processes started matching the command basename 89 @rtype: integer 90 91 """ 92 if self.processCount.has_key(displayName): 93 return self.processCount[displayName] 94 else: 95 return 0
96 97
98 - def allocateUniqueStdOutErr(self, processKey):
99 """Allocate filenames of the form processKey[.n].out (similarly for .err). 100 101 @param processKey: A user-defined identifier that will form the prefix onto which [.n].out is appended 102 @return: A STDOUTERR_TUPLE named tuple of (stdout, stderr) 103 @rtype: tuple 104 105 """ 106 newval = self.__uniqueProcessKeys.get(processKey, -1)+1 107 self.__uniqueProcessKeys[processKey] = newval 108 109 suffix = '.%d'%(newval) if newval > 0 else '' 110 111 return STDOUTERR_TUPLE( 112 os.path.join(self.output, processKey+suffix+'.out'), 113 os.path.join(self.output, processKey+suffix+'.err'), 114 )
115 116
117 - def startProcess(self, command, arguments, environs=None, workingDir=None, state=FOREGROUND, 118 timeout=TIMEOUTS['WaitForProcess'], stdout=None, stderr=None, displayName=None, 119 abortOnError=None, ignoreExitStatus=True):
120 """Start a process running in the foreground or background, and return the process handle. 121 122 The method allows spawning of new processes in a platform independent way. The command, arguments, 123 environment and working directory to run the process in can all be specified in the arguments to the 124 method, along with the filenames used for capturing the stdout and stderr of the process. Processes may 125 be started in the C{FOREGROUND}, in which case the method does not return until the process has completed 126 or a time out occurs, or in the C{BACKGROUND} in which case the method returns immediately to the caller 127 returning a handle to the process to allow manipulation at a later stage. All processes started in the 128 C{BACKGROUND} and not explicitly killed using the returned process handle are automatically killed on 129 completion of the test via the L{cleanup()} destructor. 130 131 @param command: The command to start the process (should include the full path) 132 @param arguments: A list of arguments to pass to the command 133 @param environs: A dictionary of the environment to run the process in (defaults to clean environment) 134 @param workingDir: The working directory for the process to run in (defaults to the testcase output subdirectory) 135 @param state: Run the process either in the C{FOREGROUND} or C{BACKGROUND} (defaults to C{FOREGROUND}) 136 @param timeout: The timeout period after which to termintate processes running in the C{FOREGROUND} 137 @param stdout: The filename used to capture the stdout of the process. Consider using L{allocateUniqueStdOutErr} to get this. 138 @param stderr: The filename user to capture the stderr of the process. Consider using L{allocateUniqueStdOutErr} to get this. 139 @param displayName: Logical name of the process used for display and reference counting (defaults to the basename of the command) 140 @param abortOnError: If true abort the test on any error outcome (defaults to the defaultAbortOnError 141 project setting) 142 @param ignoreExitStatus: If False, non-zero exit codes are reported as an error outcome 143 @return: The process handle of the process (L{pysys.process.helper.ProcessWrapper}) 144 @rtype: handle 145 146 """ 147 if not workingDir: workingDir = r'%s' % self.output 148 if not displayName: displayName = os.path.basename(command) 149 if abortOnError == None: abortOnError = self.defaultAbortOnError 150 151 try: 152 startTime = time.time() 153 process = ProcessWrapper(command, arguments, environs or {}, workingDir, state, timeout, stdout, stderr, displayName=displayName) 154 process.start() 155 if state == FOREGROUND: 156 (log.info if process.exitStatus == 0 else log.warn)("Executed %s, exit status %d%s", displayName, process.exitStatus, 157 ", duration %d secs" % (time.time()-startTime) if (int(time.time()-startTime)) > 0 else "") 158 159 if not ignoreExitStatus and process.exitStatus != 0: 160 self.addOutcome(BLOCKED, '%s returned non-zero exit code %d'%(process, process.exitStatus), abortOnError=abortOnError) 161 162 elif state == BACKGROUND: 163 log.info("Started %s with process id %d", displayName, process.pid) 164 except ProcessError: 165 log.info("%s", sys.exc_info()[1], exc_info=0) 166 except ProcessTimeout: 167 self.addOutcome(TIMEDOUT, '%s timed out after %d seconds'%(process, timeout), printReason=False, abortOnError=abortOnError) 168 log.warn("Process %r timed out after %d seconds, stopping process", process, timeout) 169 process.stop() 170 else: 171 self.processList.append(process) 172 try: 173 if self.processCount.has_key(displayName): 174 self.processCount[displayName] = self.processCount[displayName] + 1 175 else: 176 self.processCount[displayName] = 1 177 except: 178 pass 179 return process
180 181
182 - def stopProcess(self, process, abortOnError=None):
183 """Send a soft or hard kill to a running process to stop its execution. 184 185 This method uses the L{pysys.process.helper} module to stop a running process. Should the request to 186 stop the running process fail, a C{BLOCKED} outcome will be added to the outcome list. Failures will 187 result in an exception unless the project property defaultAbortOnError=False. 188 189 @param process: The process handle returned from the L{startProcess} method 190 @param abortOnError: If true abort the test on any error outcome (defaults to the defaultAbortOnError 191 project setting) 192 193 """ 194 if abortOnError == None: abortOnError = self.defaultAbortOnError 195 if process.running(): 196 try: 197 process.stop() 198 log.info("Stopped process with process id %d", process.pid) 199 except ProcessError, e: 200 if not abortOnError: 201 log.warn("Ignoring failure to stop process %r due to: %s", process, e) 202 else: 203 self.abort(BLOCKED, 'Unable to stop process %r'%(process), self.__callRecord())
204 205
206 - def signalProcess(self, process, signal, abortOnError=None):
207 """Send a signal to a running process (Unix only). 208 209 This method uses the L{pysys.process.helper} module to send a signal to a running process. Should the 210 request to send the signal to the running process fail, a C{BLOCKED} outcome will be added to the 211 outcome list. 212 213 @param process: The process handle returned from the L{startProcess} method 214 @param signal: The integer value of the signal to send 215 @param abortOnError: If true abort the test on any error outcome (defaults to the defaultAbortOnError 216 project setting) 217 218 """ 219 if abortOnError == None: abortOnError = self.defaultAbortOnError 220 if process.running(): 221 try: 222 process.signal(signal) 223 log.info("Sent %d signal to process %r", signal, process) 224 except ProcessError, e: 225 if not abortOnError: 226 log.warn("Ignoring failure to signal process %r due to: %s", process, e) 227 else: 228 self.abort(BLOCKED, 'Unable to signal process %r'%(process), self.__callRecord())
229 230
231 - def waitProcess(self, process, timeout, abortOnError=None):
232 """Wait for a background process to terminate, return on termination or expiry of the timeout. 233 234 Timeouts will result in an exception unless the project property defaultAbortOnError=False. 235 236 @param process: The process handle returned from the L{startProcess} method 237 @param timeout: The timeout value in seconds to wait before returning 238 @param abortOnError: If true abort the test on any error outcome (defaults to the defaultAbortOnError 239 project setting) 240 241 """ 242 if abortOnError == None: abortOnError = self.defaultAbortOnError 243 assert timeout > 0 244 try: 245 log.info("Waiting up to %d secs for process %r", timeout, process) 246 t = time.time() 247 process.wait(timeout) 248 if time.time()-t > 10: 249 log.info("Process %s terminated after %d secs", process, time.time()-t) 250 except ProcessTimeout: 251 if not abortOnError: 252 log.warn("Ignoring timeout waiting for process %r after %d secs", process, time.time()-t) 253 else: 254 self.abort(TIMEDOUT, 'Timed out waiting for process %s after %d secs'%(process, timeout), self.__callRecord())
255 256
257 - def writeProcess(self, process, data, addNewLine=True):
258 """Write data to the stdin of a process. 259 260 This method uses the L{pysys.process.helper} module to write a data string to the stdin of a process. This 261 wrapper around the write method of the process helper only adds checking of the process running status prior 262 to the write being performed, and logging to the testcase run log to detail the write. 263 264 @param process: The process handle returned from the L{startProcess()} method 265 @param data: The data to write to the process 266 @param addNewLine: True if a new line character is to be added to the end of the data string 267 268 """ 269 if process.running(): 270 process.write(data, addNewLine) 271 log.info("Written to stdin of process %r", process) 272 log.debug(" %s" % data) 273 else: 274 raise Exception("Write to process %r stdin not performed as process is not running", process)
275 276
277 - def waitForSocket(self, port, host='localhost', timeout=TIMEOUTS['WaitForSocket'], abortOnError=None):
278 """Wait for a socket connection to be established. 279 280 This method blocks until connection to a particular host:port pair can be established. This is useful for 281 test timing where a component under test creates a socket for client server interaction - calling of this 282 method ensures that on return of the method call the server process is running and a client is able to 283 create connections to it. If a connection cannot be made within the specified timeout interval, the method 284 returns to the caller. 285 286 @param port: The port value in the socket host:port pair 287 @param host: The host value in the socket host:port pair 288 @param timeout: The timeout in seconds to wait for connection to the socket 289 @param abortOnError: If true abort the test on any error outcome (defaults to the defaultAbortOnError 290 project setting) 291 292 """ 293 if abortOnError == None: abortOnError = self.defaultAbortOnError 294 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 295 296 log.debug("Performing wait for socket creation:") 297 log.debug(" file: %d" % port) 298 log.debug(" filedir: %s" % host) 299 300 startTime = time.time() 301 while not exit: 302 try: 303 s.connect((host, port)) 304 log.debug("Wait for socket creation completed successfully") 305 if time.time()-startTime>10: 306 log.info("Wait for socket creation completed") 307 except socket.error: 308 if timeout: 309 currentTime = time.time() 310 if currentTime > startTime + timeout: 311 msg = "Timed out waiting for creation of socket after %d secs"%(time.time()-startTime) 312 if abortOnError: 313 self.abort(TIMEDOUT, msg, self.__callRecord()) 314 else: 315 log.warn(msg) 316 break 317 time.sleep(0.01)
318 319
320 - def waitForFile(self, file, filedir=None, timeout=TIMEOUTS['WaitForFile'], abortOnError=None):
321 """Wait for a file to exist on disk. 322 323 This method blocks until a file is created on disk. This is useful for test timing where 324 a component under test creates a file (e.g. for logging) indicating it has performed all 325 initialisation actions and is ready for the test execution steps. If a file is not created 326 on disk within the specified timeout interval, the method returns to the caller. 327 328 @param file: The basename of the file used to wait to be created 329 @param filedir: The dirname of the file (defaults to the testcase output subdirectory) 330 @param timeout: The timeout in seconds to wait for the file to be created 331 @param abortOnError: If true abort the test on any error outcome (defaults to the defaultAbortOnError 332 project setting) 333 334 """ 335 if abortOnError == None: abortOnError = self.defaultAbortOnError 336 if filedir is None: filedir = self.output 337 f = os.path.join(filedir, file) 338 339 log.debug("Performing wait for file creation:") 340 log.debug(" file: %s" % file) 341 log.debug(" filedir: %s" % filedir) 342 343 startTime = time.time() 344 while True: 345 if timeout: 346 currentTime = time.time() 347 if currentTime > startTime + timeout: 348 349 msg = "Timed out waiting for creation of file %s after %d secs" % (file, time.time()-startTime) 350 if abortOnError: 351 self.abort(TIMEDOUT, msg, self.__callRecord()) 352 else: 353 log.warn(msg) 354 break 355 356 time.sleep(0.01) 357 if os.path.exists(f): 358 log.debug("Wait for '%s' file creation completed successfully", file) 359 return
360 361
362 - def waitForSignal(self, file, filedir=None, expr="", condition=">=1", timeout=TIMEOUTS['WaitForSignal'], poll=0.25, 363 process=None, errorExpr=[], abortOnError=None):
364 """Wait for a particular regular expression to be seen on a set number of lines in a text file. 365 366 This method blocks until a particular regular expression is seen in a text file on a set 367 number of lines. The number of lines which should match the regular expression is given by 368 the C{condition} argument in textual form i.e. for a match on more than 2 lines use condition =\">2\". 369 If the regular expression is not seen in the file matching the supplied condition within the 370 specified timeout interval, the method returns to the caller. 371 372 @param file: The absolute or relative name of the file used to wait for the signal 373 @param filedir: The dirname of the file (defaults to the testcase output subdirectory) 374 @param expr: The regular expression to search for in the text file 375 @param condition: The condition to be met for the number of lines matching the regular expression 376 @param timeout: The timeout in seconds to wait for the regular expression and to check against the condition 377 @param poll: The time in seconds to poll the file looking for the regular expression and to check against the condition 378 @param process: If a handle to the process object producing output is specified, the wait will abort if 379 the process dies before the expected signal appears. 380 @param errorExpr: Optional list of regular expressions, which if found in the file will cause waiting 381 for the main expression to be aborted with an error outcome. This is useful to avoid waiting a long time for 382 the expected expression when an ERROR is logged that means it will never happen, and also provides 383 much clearer test failure messages in this case. 384 @param abortOnError: If true abort the test on any error outcome (defaults to the defaultAbortOnError 385 project setting) 386 387 """ 388 if abortOnError == None: abortOnError = self.defaultAbortOnError 389 if filedir is None: filedir = self.output 390 f = os.path.join(filedir, file) 391 392 log.debug("Performing wait for signal in file:") 393 log.debug(" file: %s" % file) 394 log.debug(" filedir: %s" % filedir) 395 log.debug(" expression: %s" % expr) 396 log.debug(" condition: %s" % condition) 397 398 if errorExpr: assert not isinstance(errorExpr, basestring), 'errorExpr must be a list of strings not a string' 399 400 matches = [] 401 startTime = time.time() 402 msg = "Wait for signal \"%s\" %s in %s" % (expr, condition, os.path.basename(file)) 403 while 1: 404 if os.path.exists(f): 405 matches = getmatches(f, expr) 406 if eval("%d %s" % (len(matches), condition)): 407 if PROJECT.verboseWaitForSignal.lower()=='true' if hasattr(PROJECT, 'verboseWaitForSignal') else False: 408 log.info("%s completed successfully", msg) 409 else: 410 log.info("Wait for signal in %s completed successfully", file) 411 break 412 413 if errorExpr: 414 for err in errorExpr: 415 errmatches = getmatches(f, err+'.*') # add .* to capture entire err msg for a better outcome reason 416 if errmatches: 417 err = errmatches[0].group(0).strip() 418 msg = '"%s" found during %s'%(err, msg) 419 # always report outcome for this case; additionally abort if requested to 420 self.addOutcome(BLOCKED, outcomeReason=msg, abortOnError=abortOnError, callRecord=self.__callRecord()) 421 return matches 422 423 currentTime = time.time() 424 if currentTime > startTime + timeout: 425 msg = "%s timed out after %d secs, with %d matches"%(msg, timeout, len(matches)) 426 if abortOnError: 427 self.abort(TIMEDOUT, msg, self.__callRecord()) 428 else: 429 log.warn(msg) 430 break 431 432 if process and not process.running(): 433 msg = "%s aborted due to process %s termination"%(msg, process) 434 if abortOnError: 435 self.abort(BLOCKED, msg, self.__callRecord()) 436 else: 437 log.warn(msg) 438 break 439 440 time.sleep(poll) 441 return matches
442 443
444 - def addCleanupFunction(self, fn):
445 """ Registers a zero-arg function that will be called as part of the cleanup of this object. 446 447 Cleanup functions are invoked in reverse order with the most recently added first (LIFO), and 448 before the automatic termination of any remaining processes associated with this object. 449 450 e.g. self.addCleanupFunction(lambda: self.cleanlyShutdownProcessX(params)) 451 452 """ 453 if fn and fn not in self.__cleanupFunctions: 454 self.__cleanupFunctions.append(fn)
455 456
457 - def cleanup(self):
458 """ Cleanup function that frees resources managed by this object. 459 460 Should be called exactly once when this object is no longer needed. Instead of overriding 461 this function, use L{addCleanupFunction}. 462 463 """ 464 try: 465 if self.__cleanupFunctions: 466 log.info('') 467 log.info('cleanup:') 468 for fn in reversed(self.__cleanupFunctions): 469 try: 470 log.debug('Running registered cleanup function: %r'%fn) 471 fn() 472 except Exception, e: 473 log.error('Error while running cleanup function: %s'%e) 474 self.__cleanupFunctions = [] 475 finally: 476 for process in self.processList: 477 try: 478 if process.running(): process.stop() 479 except: 480 log.info("caught %s: %s", sys.exc_info()[0], sys.exc_info()[1], exc_info=1) 481 self.processList = [] 482 self.processCount = {} 483 484 log.debug('ProcessUser cleanup function done.')
485 486
487 - def addOutcome(self, outcome, outcomeReason='', printReason=True, abortOnError=None, callRecord=None):
488 """Add a validation outcome (and optionally a reason string) to the validation list. 489 490 The method provides the ability to add a validation outcome to the internal data structure 491 storing the list of validation outcomes. Multiple validations may be performed, the current 492 supported validation outcomes of which are: 493 494 SKIPPED: An execution/validation step of the test was skipped (e.g. deliberately) 495 BLOCKED: An execution/validation step of the test could not be run (e.g. a missing resource) 496 DUMPEDCORE: A process started by the test produced a core file (unix only) 497 TIMEDOUT: An execution/validation step of the test timed out (e.g. process deadlock) 498 FAILED: A validation step of the test failed 499 NOTVERIFIED: No validation steps were performed 500 INSPECT: A validation step of the test requires manual inspection 501 PASSED: A validation step of the test passed 502 503 The outcomes are considered to have a precedence order, as defined by the order of the outcomes listed 504 above. Thus a C{BLOCKED} outcome has a higher precedence than a C{PASSED} outcome. The outcomes are defined 505 in L{pysys.constants}. 506 507 @param outcome: The outcome to add 508 @param outcomeReason: A string summarizing the reason for the outcome 509 @param printReason: If True the specified outcomeReason will be printed 510 @param abortOnError: If true abort the test on any error outcome (defaults to the defaultAbortOnError 511 project setting if not specified) 512 @param callRecord: An array of strings indicating the call stack that lead to this outcome. This will be appended 513 to the log output for better test triage. 514 515 """ 516 assert outcome in PRECEDENT, outcome # ensure outcome type is known, and that numeric not string constant was specified! 517 if abortOnError == None: abortOnError = self.defaultAbortOnError 518 outcomeReason = outcomeReason.strip() if outcomeReason else '' 519 520 old = self.getOutcome() 521 self.outcome.append(outcome) 522 523 #store the reason of the highest precedent outcome 524 if self.getOutcome() != old: self.__outcomeReason = outcomeReason 525 526 if outcome in FAILS and abortOnError: 527 if callRecord==None: callRecord = self.__callRecord() 528 self.abort(outcome, outcomeReason, callRecord) 529 530 if outcomeReason and printReason: 531 if outcome in FAILS: 532 if callRecord==None: callRecord = self.__callRecord() 533 log.warn('%s ... %s %s', outcomeReason, LOOKUP[outcome].lower(), '[%s]'%','.join(callRecord) if callRecord!=None else '') 534 else: 535 log.info('%s ... %s', outcomeReason, LOOKUP[outcome].lower())
536 537
538 - def abort(self, outcome, outcomeReason, callRecord=None):
539 """Raise an AbortException. 540 541 @param outcome: The outcome, which will override any existing outcomes previously recorded. 542 @param outcomeReason: A string summarizing the reason for the outcome 543 544 """ 545 raise AbortExecution(outcome, outcomeReason, callRecord)
546 547
548 - def getOutcome(self):
549 """Get the overall outcome based on the precedence order. 550 551 The method returns the overall outcome of the test based on the outcomes stored in the internal data 552 structure. The precedence order of the possible outcomes is used to determined the overall outcome 553 of the test, e.g. if C{PASSED}, C{BLOCKED} and C{FAILED} were recorded during the execution of the test, 554 the overall outcome would be C{BLOCKED}. 555 556 The method returns the integer value of the outcome as defined in L{pysys.constants}. To convert this 557 to a string representation use the C{LOOKUP} dictionary i.e. C{LOOKUP}[test.getOutcome()] 558 559 @return: The overall outcome 560 @rtype: integer 561 562 """ 563 if len(self.outcome) == 0: return NOTVERIFIED 564 return sorted(self.outcome, key=lambda x: PRECEDENT.index(x))[0]
565 566
567 - def getOutcomeReason(self):
568 """Get the reason string for the current overall outcome (if specified). 569 570 @return: The overall test outcome reason or '' if not specified 571 @rtype: string 572 573 """ 574 return self.__outcomeReason
575 576
577 - def getNextAvailableTCPPort(self):
578 """Allocate a TCP port. 579 580 """ 581 o = TCPPortOwner() 582 self.addCleanupFunction(lambda: o.cleanup()) 583 return o.port
584 585
586 - def __callRecord(self):
587 """Retrieve a call record outside of this module, up to the execute or validate method of the test case. 588 589 """ 590 stack=[] 591 from pysys.basetest import BaseTest 592 if isinstance(self, BaseTest): 593 for record in inspect.stack(): 594 info = inspect.getframeinfo(record[0]) 595 if (self.__skipFrame(info.filename, ProcessUser) ): continue 596 if (self.__skipFrame(info.filename, BaseTest) ): continue 597 stack.append( '%s:%s' % (os.path.basename(info.filename).strip(), info.lineno) ) 598 if (os.path.splitext(info.filename)[0] == os.path.splitext(self.descriptor.module)[0] and (info.function == 'execute' or info.function == 'validate')): return stack 599 return None
600 601
602 - def __skipFrame(self, file, clazz):
603 """Private method to check if a file is that for a particular class. 604 605 @param file: The filepatch to check 606 @param clazz: The class to check against 607 608 """ 609 return os.path.splitext(file)[0] == os.path.splitext(sys.modules[clazz.__module__].__file__)[0]
610
611 - def logFileContents(self, path, includes=None, excludes=None, maxLines=20, tail=False):
612 """ Logs some or all the lines in the specified file. 613 614 If the file does not exist or cannot be opened, does nothing. 615 616 This method is useful for providing key diagnostic information 617 (e.g. error messages from tools executed by the test) directly in run.log, 618 to make test failures easier to triage quickly. 619 620 @param path: May be an absolute path, or relative to the 621 test output directory. If None or cannot be opened, this is a no-op. 622 @param includes: Optional list of regex strings, for example [' ERROR.*', ' WARN.*']. 623 If specified, only matches of these regexes will be logged. 624 @param excludes: Optional list of regex strings. If specified, 625 no line containing these will be logged. 626 @param maxLines: Upper limit on the number of lines from the file 627 that will be logged. Set to zero for unlimited. 628 @param tail: Prints the _last_ 'maxLines' in the file rather than 629 the first 'maxLines'. 630 631 @return: True if anything was logged, False if not 632 633 """ 634 if not path: return False 635 actualpath= os.path.join(self.output, path) 636 try: 637 f = open(actualpath, 'r') 638 except Exception, e: 639 self.log.debug('Cannot open file "%s": %s', actualpath, e) 640 return False 641 try: 642 lineno = 0 643 def matchesany(s, regexes): 644 assert not isinstance(regexes, basestring), 'must be a list of strings not a string' 645 for x in regexes: 646 m = re.search(x, s) 647 if m: return m.group(0) 648 return None
649 650 tolog = [] 651 652 for l in f: 653 l = l.strip() 654 if not l: continue 655 if includes: 656 l = matchesany(l, includes) 657 if not l: continue 658 if excludes and matchesany(l, excludes): continue 659 lineno +=1 660 tolog.append(l) 661 if maxLines: 662 if not tail and len(tolog) == maxLines: 663 tolog.append('...') 664 break 665 if tail and len(tolog)==maxLines+1: 666 del tolog[0] 667 finally: 668 f.close() 669 670 if not tolog: 671 return False 672 673 self.log.info('Contents of %s%s: ', os.path.normpath(path), ' (filtered)' if includes or excludes else '') 674 for l in tolog: 675 self.log.info(' %s', l) 676 self.log.info(' -----') 677 self.log.info('') 678 return True
679