1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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
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
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
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
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
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
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
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
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
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+'.*')
416 if errmatches:
417 err = errmatches[0].group(0).strip()
418 msg = '"%s" found during %s'%(err, msg)
419
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
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
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
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
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
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
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
584
585
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
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