Gaudi Framework, version v21r4

Home   Generated: 7 Sep 2009

GaudiTest.py

Go to the documentation of this file.
00001 ########################################################################
00002 # File:   GaudiTest.py
00003 # Author: Marco Clemencic CERN/PH-LBC
00004 ########################################################################
00005 __author__  = 'Marco Clemencic CERN/PH-LBC' 
00006 __version__ = "$Revision: 1.52 $" 
00007 __tag__     = "$Name:  $" 
00008 ########################################################################
00009 # Imports
00010 ########################################################################
00011 import os
00012 import sys
00013 import re
00014 import tempfile
00015 import shutil
00016 import string
00017 import difflib
00018 from subprocess import Popen, PIPE, STDOUT
00019 
00020 import qm
00021 from qm.test.classes.command import ExecTestBase
00022 
00023 ### Needed by the re-implementation of TimeoutExecutable
00024 import qm.executable
00025 import time, signal
00026 # The classes in this module are implemented differently depending on
00027 # the operating system in use.
00028 if sys.platform == "win32":
00029     import msvcrt
00030     import pywintypes
00031     from   threading import *
00032     import win32api
00033     import win32con
00034     import win32event
00035     import win32file
00036     import win32pipe
00037     import win32process
00038 else:
00039     import cPickle
00040     import fcntl
00041     import select
00042     import qm.sigmask
00043 
00044 ########################################################################
00045 # Utility Classes
00046 ########################################################################
00047 class TemporaryEnvironment:
00048     """
00049     Class to changes the environment temporarily.
00050     """
00051     def __init__(self, orig = os.environ, keep_same = False):
00052         """
00053         Create a temporary environment on top of the one specified
00054         (it can be another TemporaryEnvironment instance).
00055         """
00056         #print "New environment"
00057         self.old_values = {}
00058         self.env = orig
00059         self._keep_same = keep_same
00060         
00061     def __setitem__(self,key,value):
00062         """
00063         Set an environment variable recording the previous value.
00064         """
00065         if key not in self.old_values :
00066             if key in self.env :
00067                 if not self._keep_same or self.env[key] != value:
00068                     self.old_values[key] = self.env[key]
00069             else:
00070                 self.old_values[key] = None
00071         self.env[key] = value
00072         
00073     def __getitem__(self,key):
00074         """
00075         Get an environment variable.
00076         Needed to provide the same interface as os.environ.
00077         """
00078         return self.env[key]
00079     
00080     def __delitem__(self,key):
00081         """
00082         Unset an environment variable.
00083         Needed to provide the same interface as os.environ.
00084         """
00085         if key not in self.env :
00086             raise KeyError(key)
00087         self.old_values[key] = self.env[key]
00088         del self.env[key]
00089 
00090     def keys(self):
00091         """
00092         Return the list of defined environment variables.
00093         Needed to provide the same interface as os.environ.
00094         """
00095         return self.env.keys()
00096         
00097     def items(self):
00098         """
00099         Return the list of (name,value) pairs for the defined environment variables.
00100         Needed to provide the same interface as os.environ.
00101         """
00102         return self.env.items()
00103     
00104     def __contains__(self,key):
00105         """
00106         Operator 'in'.
00107         Needed to provide the same interface as os.environ.
00108         """
00109         return key in self.env
00110     
00111     def restore(self):
00112         """
00113         Revert all the changes done to the orignal environment.
00114         """
00115         for key,value in self.old_values.items():
00116             if value is None:
00117                 del self.env[key]
00118             else:
00119                 self.env[key] = value
00120         self.old_values = {}
00121         
00122     def __del__(self):
00123         """
00124         Revert the changes on destruction.
00125         """
00126         #print "Restoring the environment"
00127         self.restore()
00128 
00129     def gen_script(self,shell_type):
00130         """
00131         Generate a shell script to reproduce the changes in the environment.
00132         """
00133         shells = [ 'csh', 'sh', 'bat' ]
00134         if shell_type not in shells:
00135             raise RuntimeError("Shell type '%s' unknown. Available: %s"%(shell_type,shells))
00136         out = ""
00137         for key,value in self.old_values.items():
00138             if key not in self.env:
00139                 # unset variable
00140                 if shell_type == 'csh':
00141                     out += 'unsetenv %s\n'%key
00142                 elif shell_type == 'sh':
00143                     out += 'unset %s\n'%key
00144                 elif shell_type == 'bat':
00145                     out += 'set %s=\n'%key
00146             else:
00147                 # set variable
00148                 if shell_type == 'csh':
00149                     out += 'setenv %s "%s"\n'%(key,self.env[key])
00150                 elif shell_type == 'sh':
00151                     out += 'export %s="%s"\n'%(key,self.env[key])
00152                 elif shell_type == 'bat':
00153                     out += 'set %s=%s\n'%(key,self.env[key])
00154         return out
00155 
00156 class TempDir:
00157     """Small class for temporary directories.
00158     When instantiated, it creates a temporary directory and the instance
00159     behaves as the string containing the directory name.
00160     When the instance goes out of scope, it removes all the content of
00161     the temporary directory (automatic clean-up).
00162     """
00163     def __init__(self, keep = False, chdir = False):
00164         self.name = tempfile.mkdtemp()
00165         self._keep = keep
00166         self._origdir = None
00167         if chdir:
00168             self._origdir = os.getcwd()
00169             os.chdir(self.name)
00170         
00171     def __str__(self):
00172         return self.name
00173     
00174     def __del__(self):
00175         if self._origdir:
00176             os.chdir(self._origdir)
00177         if self.name and not self._keep:
00178             shutil.rmtree(self.name)
00179     
00180     def __getattr__(self,attr):
00181         return getattr(self.name,attr)
00182 
00183 class TempFile:
00184     """Small class for temporary files.
00185     When instantiated, it creates a temporary directory and the instance
00186     behaves as the string containing the directory name.
00187     When the instance goes out of scope, it removes all the content of
00188     the temporary directory (automatic clean-up).
00189     """
00190     def __init__(self, suffix='', prefix='tmp', dir=None, text=False, keep = False):
00191         self.file = None
00192         self.name = None
00193         self._keep = keep
00194         
00195         self._fd, self.name = tempfile.mkstemp(suffix,prefix,dir,text)
00196         self.file = os.fdopen(self._fd,"r+")
00197         
00198     def __str__(self):
00199         return self.name
00200     
00201     def __del__(self):
00202         if self.file:
00203             self.file.close()
00204         if self.name and not self._keep:
00205             os.remove(self.name)
00206     
00207     def __getattr__(self,attr):
00208         return getattr(self.file,attr)
00209 
00210 class CMT:
00211     """Small wrapper to call CMT.
00212     """
00213     def __init__(self,path=None):
00214         if path is None:
00215             path = os.getcwd()
00216         self.path = path
00217     
00218     def _run_cmt(self,command,args):
00219         # prepare command line
00220         if type(args) is str:
00221             args = [args]
00222         cmd = "cmt %s"%command
00223         for arg in args:
00224             cmd += ' "%s"'%arg
00225         
00226         # go to the execution directory
00227         olddir = os.getcwd()
00228         os.chdir(self.path)
00229         # run cmt
00230         result = os.popen4(cmd)[1].read()
00231         # return to the old directory
00232         os.chdir(olddir)
00233         return result
00234     
00235     def __getattr__(self,attr):
00236         return lambda args=[]: self._run_cmt(attr, args)
00237     
00238     def runtime_env(self,env = None):
00239         """Returns a dictionary containing the runtime environment produced by CMT.
00240         If a dictionary is passed a modified instance of it is returned.
00241         """
00242         if env is None:
00243             env = {}
00244         for l in self.setup("-csh").splitlines():
00245             l = l.strip()
00246             if l.startswith("setenv"):
00247                 dummy,name,value = l.split(None,3)
00248                 env[name] = value.strip('"')
00249             elif l.startswith("unsetenv"):
00250                 dummy,name = l.split(None,2)
00251                 if name in env:
00252                     del env[name]
00253         return env
00254     def show_macro(self,k):
00255         r = self.show(["macro",k])
00256         if r.find("CMT> Error: symbol not found") >= 0:
00257             return None
00258         else:
00259             return self.show(["macro_value",k]).strip()
00260 
00261 ########################################################################
00262 # Output Validation Classes
00263 ########################################################################
00264 class BasicOutputValidator:
00265     """Basic implementation of an option validator for Gaudi tests.
00266     This implementation is based on the standard (LCG) validation functions
00267     used in QMTest.
00268     """
00269     def __init__(self,ref,cause,result_key):
00270         self.reference = ref
00271         self.cause = cause
00272         self.result_key = result_key
00273     
00274     def __call__(self, out, result):
00275         """Validate the output of the program.
00276 
00277         'stdout' -- A string containing the data written to the standard output
00278         stream.
00279 
00280         'stderr' -- A string containing the data written to the standard error
00281         stream.
00282 
00283         'result' -- A 'Result' object. It may be used to annotate
00284         the outcome according to the content of stderr.
00285 
00286         returns -- A list of strings giving causes of failure."""
00287 
00288         causes = []
00289         # Check to see if theoutput matches.
00290         if not self.__CompareText(out, self.reference):
00291             causes.append(self.cause) 
00292             result[self.result_key] = result.Quote(self.reference)
00293 
00294         return causes
00295     
00296     def __CompareText(self, s1, s2):
00297         """Compare 's1' and 's2', ignoring line endings.
00298 
00299         's1' -- A string.
00300 
00301         's2' -- A string.
00302 
00303         returns -- True if 's1' and 's2' are the same, ignoring
00304         differences in line endings."""
00305 
00306         # The "splitlines" method works independently of the line ending
00307         # convention in use.
00308         return s1.splitlines() == s2.splitlines()
00309 
00310 class FilePreprocessor:
00311     """ Base class for a callable that takes a file and returns a modified
00312     version of it."""
00313     def __processLine__(self, line):
00314         return line
00315     def __call__(self, input):
00316         if hasattr(input,"__iter__"):
00317             lines = input
00318             mergeback = False
00319         else:
00320             lines = input.splitlines()
00321             mergeback = True
00322         output = []
00323         for l in lines:
00324             l = self.__processLine__(l)
00325             if l: output.append(l)
00326         if mergeback: output = '\n'.join(output)
00327         return output
00328     def __add__(self, rhs):
00329         return FilePreprocessorSequence([self,rhs])
00330 
00331 class FilePreprocessorSequence(FilePreprocessor):
00332     def __init__(self, members = []):
00333         self.members = members
00334     def __add__(self, rhs):
00335         return FilePreprocessorSequence(self.members + [rhs])
00336     def __call__(self, input):
00337         output = input
00338         for pp in self.members:
00339             output = pp(output)
00340         return output
00341 
00342 class LineSkipper(FilePreprocessor):
00343     def __init__(self, strings = [], regexps = []):
00344         import re
00345         self.strings = strings
00346         self.regexps = map(re.compile,regexps)
00347         
00348     def __processLine__(self, line):
00349         for s in self.strings:
00350             if line.find(s) >= 0: return None
00351         for r in self.regexps:
00352             if r.search(line): return None
00353         return line
00354 
00355 class BlockSkipper(FilePreprocessor):
00356     def __init__(self, start, end):
00357         self.start = start
00358         self.end = end
00359         self._skipping = False
00360         
00361     def __processLine__(self, line):
00362         if self.start in line:
00363             self._skipping = True
00364             return None
00365         elif self.end in line:
00366             self._skipping = False
00367         elif self._skipping:
00368             return None
00369         return line
00370 
00371 class RegexpReplacer(FilePreprocessor):
00372     def __init__(self, orig, repl = "", when = None):
00373         if when:
00374             when = re.compile(when)
00375         self._operations = [ (when, re.compile(orig), repl) ]
00376     def __add__(self,rhs):
00377         if isinstance(rhs, RegexpReplacer):
00378             res = RegexpReplacer("","",None)
00379             res._operations = self._operations + rhs._operations
00380         else:
00381             res = FilePreprocessor.__add__(self, rhs)
00382         return res
00383     def __processLine__(self, line):
00384         for w,o,r in self._operations:
00385             if w is None or w.search(line):
00386                 line = o.sub(r, line)
00387         return line
00388 
00389 # Common preprocessors        
00390 maskPointers  = RegexpReplacer("0x[0-9a-fA-F]{4,16}","0x########")
00391 normalizeDate = RegexpReplacer("[0-2]?[0-9]:[0-5][0-9]:[0-5][0-9] [0-9]{4}[-/][01][0-9][-/][0-3][0-9] *(CES?T)?",
00392                                "00:00:00 1970-01-01")
00393 normalizeEOL = FilePreprocessor()
00394 normalizeEOL.__processLine__ = lambda line: str(line).rstrip() + '\n'
00395 
00396 skipEmptyLines = FilePreprocessor()
00397 # FIXME: that's ugly
00398 skipEmptyLines.__processLine__ = lambda line: (line.strip() and line) or None
00399 
00400 ## Special preprocessor sorting the list of strings (whitespace separated)
00401 #  that follow a signature on a single line
00402 class LineSorter(FilePreprocessor):
00403     def __init__(self, signature):
00404         self.signature = signature
00405         self.siglen = len(signature)
00406     def __processLine__(self, line):
00407         pos = line.find(self.signature)
00408         if pos >=0:
00409             line = line[:(pos+self.siglen)]
00410             lst = line[(pos+self.siglen):].split()
00411             lst.sort()
00412             line += " ".join(lst)
00413         return line
00414 
00415 # Preprocessors for GaudiExamples
00416 normalizeExamples = maskPointers + normalizeDate 
00417 for w,o,r in [
00418               #("TIMER.TIMER",r"[0-9]", "0"), # Normalize time output
00419               ("TIMER.TIMER",r"\s+[+-]?[0-9]+[0-9.]*", " 0"), # Normalize time output
00420               ("release all pending",r"^.*/([^/]*:.*)",r"\1"),
00421               ("0x########",r"\[.*/([^/]*.*)\]",r"[\1]"),
00422               ("^#.*file",r"file '.*[/\\]([^/\\]*)$",r"file '\1"),
00423               ("^JobOptionsSvc.*options successfully read in from",r"read in from .*[/\\]([^/\\]*)$",r"file \1"), # normalize path to options
00424               # Normalize UUID, except those ending with all 0s (i.e. the class IDs)
00425               (None,r"[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}(?!-0{12})-[0-9A-Fa-f]{12}","00000000-0000-0000-0000-000000000000"),
00426               # Absorb a change in ServiceLocatorHelper
00427               ("ServiceLocatorHelper::", "ServiceLocatorHelper::(create|locate)Service", "ServiceLocatorHelper::service")
00428               ]: #[ ("TIMER.TIMER","[0-9]+[0-9.]*", "") ]
00429     normalizeExamples += RegexpReplacer(o,r,w)
00430 normalizeExamples = LineSkipper(["//GP:",
00431                                  "Time User",
00432                                  "Welcome to",
00433                                  "This machine has a speed",
00434                                  "TIME:",
00435                                  "running on",
00436                                  "ToolSvc.Sequenc...   INFO",
00437                                  "DataListenerSvc      INFO XML written to file:",
00438                                  "[INFO]","[WARNING]",
00439                                  "DEBUG No writable file catalog found which contains FID:",
00440                                  "0 local", # hack for ErrorLogExample
00441                                  "DEBUG Service base class initialized successfully", # changed between v20 and v21 
00442                                  # This comes from ROOT, when using GaudiPython 
00443                                  'Note: (file "(tmpfile)", line 2) File "set" already loaded',
00444                                  ],regexps = [
00445                                  r"^#", # Ignore python comments
00446                                  r"(Always|SUCCESS)\s*(Root f|[^ ]* F)ile version:", # skip the message reporting the version of the root file
00447                                  r"0x[0-9a-fA-F#]+ *Algorithm::sysInitialize\(\) *\[", # hack for ErrorLogExample
00448                                  r"0x[0-9a-fA-F#]* *__gxx_personality_v0 *\[", # hack for ErrorLogExample
00449                                  r"File '.*.xml' does not exist",
00450                                  r"INFO Refer to dataset .* by its file ID:",
00451                                  r"INFO Referring to dataset .* by its file ID:",
00452                                  r"INFO Disconnect from dataset",
00453                                  r"INFO Disconnected from dataset",
00454                                  r"INFO Disconnected data IO:",
00455                                  r"IncidentSvc\s*(DEBUG (Adding|Removing)|VERBOSE Calling)",
00456                                  # I want to ignore the header of the unchecked StatusCode report
00457                                  r"^StatusCodeSvc.*listing all unchecked return codes:",
00458                                  r"^StatusCodeSvc\s*INFO\s*$",
00459                                  r"Num\s*|\s*Function\s*|\s*Source Library",
00460                                  r"^[-+]*\s*$",
00461                                  # Hide the fake error message coming from POOL/ROOT (ROOT 5.21)
00462                                  r"ERROR Failed to modify file: .* Errno=2 No such file or directory",
00463                                  # Hide unckeched StatusCodes  from dictionaries
00464                                  r"^ +[0-9]+ \|.*ROOT", 
00465                                  r"^ +[0-9]+ \|.*\|.*Dict", 
00466                                  # Remove ROOT TTree summary table, which changes from one version to the other
00467                                  r"^\*.*\*$",
00468                                  # Remove Histos Summaries
00469                                  r"SUCCESS\s*Booked \d+ Histogram\(s\)",
00470                                  r"^ \|",
00471                                  r"^ ID=",
00472                                  ] ) + normalizeExamples + skipEmptyLines + \
00473                                   normalizeEOL + \
00474                                   LineSorter("Services to release : ")
00475 
00476 class ReferenceFileValidator:
00477     def __init__(self, reffile, cause, result_key, preproc = normalizeExamples):
00478         self.reffile = os.path.expandvars(reffile)
00479         self.cause = cause
00480         self.result_key = result_key
00481         self.preproc = preproc
00482     def __call__(self, stdout, result):
00483         causes = []
00484         if os.path.isfile(self.reffile):
00485             orig = open(self.reffile).xreadlines()
00486             if self.preproc:
00487                 orig = self.preproc(orig)
00488         else:
00489             orig = []
00490         
00491         new = stdout.splitlines()
00492         if self.preproc:
00493             new = self.preproc(new)
00494         #open(self.reffile + ".test","w").writelines(new)
00495         diffs = difflib.ndiff(orig,new,charjunk=difflib.IS_CHARACTER_JUNK)
00496         filterdiffs = map(lambda x: x.strip(),filter(lambda x: x[0] != " ",diffs))
00497         #filterdiffs = [x.strip() for x in diffs]
00498         if filterdiffs:
00499             result[self.result_key] = result.Quote("\n".join(filterdiffs))
00500             result[self.result_key] += result.Quote("""
00501 Legend:
00502         -) reference file
00503         +) standard output of the test""")
00504             causes.append(self.cause)
00505         
00506         return causes
00507 
00508 ########################################################################
00509 # Useful validation functions
00510 ########################################################################
00511 def findReferenceBlock(reference, stdout, result, causes, signature_offset=0, signature=None,
00512                        id = None):
00513     """
00514     Given a block of text, tries to find it in the output.
00515     The block had to be identified by a signature line. By default, the first
00516     line is used as signature, or the line pointed to by signature_offset. If
00517     signature_offset points outside the block, a signature line can be passed as
00518     signature argument. Note: if 'signature' is None (the default), a negative
00519     signature_offset is interpreted as index in a list (e.g. -1 means the last
00520     line), otherwise the it is interpreted as the number of lines before the
00521     first one of the block the signature must appear.
00522     The parameter 'id' allow to distinguish between different calls to this
00523     function in the same validation code.
00524     """
00525     # split reference file, sanitize EOLs and remove empty lines
00526     reflines = filter(None,map(lambda s: s.rstrip(), reference.splitlines()))
00527     if not reflines:
00528         raise RuntimeError("Empty (or null) reference")
00529     # the same on standard output
00530     outlines = filter(None,map(lambda s: s.rstrip(), stdout.splitlines()))
00531     
00532     res_field = "GaudiTest.RefBlock"
00533     if id:
00534         res_field += "_%s" % id 
00535     
00536     if signature is None:
00537         if signature_offset < 0:
00538             signature_offset = len(reference)+signature_offset
00539         signature = reflines[signature_offset]
00540     # find the reference block in the output file
00541     try:
00542         pos = outlines.index(signature)
00543         outlines = outlines[pos-signature_offset:pos+len(reflines)-signature_offset]
00544         if reflines != outlines:
00545             msg = "standard output"
00546             # I do not want 2 messages in causes if teh function is called twice
00547             if not msg in causes: 
00548                 causes.append(msg)
00549             result[res_field + ".observed"] = result.Quote("\n".join(outlines))
00550     except ValueError:
00551         causes.append("missing signature")
00552     result[res_field + ".signature"] = result.Quote(signature)
00553     if len(reflines) > 1 or signature != reflines[0]:
00554         result[res_field + ".expected"] = result.Quote("\n".join(reflines))
00555     
00556     return causes
00557 
00558 def countErrorLines(expected = {'ERROR':0, 'FATAL':0}, **kwargs):
00559     """
00560     Count the number of messages with required severity (by default ERROR and FATAL)
00561     and check if their numbers match the expected ones (0 by default).
00562     The dictionary "expected" can be used to tune the number of errors and fatals
00563     allowed, or to limit the number of expected warnings etc.
00564     """
00565     stdout = kwargs["stdout"]
00566     result = kwargs["result"]
00567     causes = kwargs["causes"]
00568     
00569     # prepare the dictionary to record the extracted lines
00570     errors = {}
00571     for sev in expected:
00572         errors[sev] = []
00573     
00574     outlines = stdout.splitlines()
00575     from math import log10
00576     fmt = "%%%dd - %%s" % (int(log10(len(outlines))+1))
00577     
00578     linecount = 0
00579     for l in outlines:
00580         linecount += 1
00581         words = l.split()
00582         if len(words) >= 2 and words[1] in errors:
00583             errors[words[1]].append(fmt%(linecount,l.rstrip()))
00584     
00585     for e in errors:
00586         if len(errors[e]) != expected[e]:
00587             causes.append('%s(%d)'%(e,len(errors[e])))
00588             result["GaudiTest.lines.%s"%e] = result.Quote('\n'.join(errors[e]))
00589             result["GaudiTest.lines.%s.expected#"%e] = result.Quote(str(expected[e]))
00590     
00591     return causes
00592 
00593 
00594 def _parseTTreeSummary(lines, pos):
00595     """
00596     Parse the TTree summary table in lines, starting from pos.
00597     Returns a tuple with the dictionary with the digested informations and the
00598     position of the first line after the summary.
00599     """
00600     result = {}
00601     i = pos + 1 # first line is a sequence of '*'
00602     count = len(lines)
00603     
00604     splitcols = lambda l: [ f.strip() for f in l.strip("*\n").split(':',2) ]
00605     def parseblock(ll):
00606         r = {}
00607         cols = splitcols(ll[0])
00608         r["Name"], r["Title"] = cols[1:]
00609         
00610         cols = splitcols(ll[1])
00611         r["Entries"] = int(cols[1])
00612         
00613         sizes = cols[2].split()
00614         r["Total size"] = int(sizes[2])
00615         if sizes[-1] == "memory":
00616             r["File size"] = 0
00617         else:
00618             r["File size"] = int(sizes[-1])
00619         
00620         cols = splitcols(ll[2])
00621         sizes = cols[2].split()
00622         if cols[0] == "Baskets":
00623             r["Baskets"] = int(cols[1])
00624             r["Basket size"] = int(sizes[2])
00625         r["Compression"] = float(sizes[-1])
00626         return r    
00627         
00628     if i < (count - 3) and lines[i].startswith("*Tree"):
00629         result = parseblock(lines[i:i+3])
00630         result["Branches"] = {}
00631         i += 4
00632         while i < (count - 3) and lines[i].startswith("*Br"):
00633             branch = parseblock(lines[i:i+3])
00634             result["Branches"][branch["Name"]] = branch
00635             i += 4
00636         
00637     return (result, i)
00638 
00639 def findTTreeSummaries(stdout):
00640     """
00641     Scan stdout to find ROOT TTree summaries and digest them.
00642     """
00643     stars = re.compile(r"^\*+$")
00644     outlines = stdout.splitlines()
00645     nlines = len(outlines)
00646     trees = {}
00647     
00648     i = 0
00649     while i < nlines: #loop over the output
00650         # look for
00651         while i < nlines and not stars.match(outlines[i]):
00652             i += 1
00653         if i < nlines:
00654             tree, i = _parseTTreeSummary(outlines, i)
00655             if tree:
00656                 trees[tree["Name"]] = tree
00657     
00658     return trees
00659 
00660 def cmpTreesDicts(reference, to_check, ignore = None):
00661     """
00662     Check that all the keys in reference are in to_check too, with the same value.
00663     If the value is a dict, the function is called recursively. to_check can
00664     contain more keys than reference, that will not be tested.
00665     The function returns at the first difference found. 
00666     """
00667     fail_keys = []
00668     # filter the keys in the reference dictionary
00669     if ignore:
00670         ignore_re = re.compile(ignore)
00671         keys = [ key for key in reference if not ignore_re.match(key) ]
00672     else:
00673         keys = reference.keys()
00674     # loop over the keys (not ignored) in the reference dictionary
00675     for k in keys:
00676         if k in to_check: # the key must be in the dictionary to_check
00677             if (type(reference[k]) is dict) and (type(to_check[k]) is dict):
00678                 # if both reference and to_check values are dictionaries, recurse
00679                 failed = fail_keys = cmpTreesDicts(reference[k], to_check[k], ignore)
00680             else:
00681                 # compare the two values
00682                 failed = to_check[k] != reference[k]
00683         else: # handle missing keys in the dictionary to check (i.e. failure)
00684             to_check[k] = None
00685             failed = True
00686         if failed:
00687             fail_keys.insert(0, k)
00688             break # exit from the loop at the first failure
00689     return fail_keys # return the list of keys bringing to the different values
00690 
00691 def getCmpFailingValues(reference, to_check, fail_path):
00692     c = to_check
00693     r = reference
00694     for k in fail_path:
00695         c = c.get(k,None)
00696         r = r.get(k,None)
00697         if c is None or r is None:
00698             break # one of the dictionaries is not deep enough
00699     return (fail_path, r, c)
00700 
00701 # signature of the print-out of the histograms
00702 h_count_re = re.compile(r"^(.*)SUCCESS\s+Booked (\d+) Histogram\(s\) :\s+(.*)")
00703 
00704 def parseHistosSummary(lines, pos):
00705     """
00706     Extract the histograms infos from the lines starting at pos.
00707     Returns the position of the first line after the summary block.
00708     """
00709     global h_count_re
00710     h_table_head = re.compile(r'SUCCESS\s+List of booked (1D|2D|3D|1D profile|2D profile) histograms in directory\s+"(\w*)"')
00711     h_short_summ = re.compile(r"ID=([^\"]+)\s+\"([^\"]+)\"\s+(.*)")
00712     
00713     nlines = len(lines)
00714     
00715     # decode header
00716     m = h_count_re.search(lines[pos])
00717     name = m.group(1).strip()
00718     total = int(m.group(2))
00719     header = {}
00720     for k, v in [ x.split("=") for x in  m.group(3).split() ]:
00721         header[k] = int(v)
00722     pos += 1
00723     header["Total"] = total
00724     
00725     summ = {}
00726     while pos < nlines:
00727         m = h_table_head.search(lines[pos])
00728         if m:
00729             t, d = m.groups(1) # type and directory
00730             t = t.replace(" profile", "Prof")
00731             pos += 1
00732             if pos < nlines:
00733                 l = lines[pos]
00734             else:
00735                 l = ""
00736             cont = {}
00737             if l.startswith(" | ID"):
00738                 # table format
00739                 titles = [ x.strip() for x in l.split("|")][1:]
00740                 pos += 1
00741                 while pos < nlines and lines[pos].startswith(" |"):
00742                     l = lines[pos]
00743                     values = [ x.strip() for x in l.split("|")][1:]
00744                     hcont = {}
00745                     for i in range(len(titles)):
00746                         hcont[titles[i]] = values[i]
00747                     cont[hcont["ID"]] = hcont
00748                     pos += 1
00749             elif l.startswith(" ID="):
00750                 while pos < nlines and lines[pos].startswith(" ID="):
00751                     values = [ x.strip() for x in  h_short_summ.search(lines[pos]).groups() ]
00752                     cont[values[0]] = values
00753                     pos += 1
00754             else: # not interpreted
00755                 raise RuntimeError("Cannot understand line %d: '%s'" % (pos, l))
00756             if not d in summ:
00757                 summ[d] = {}
00758             summ[d][t] = cont
00759             summ[d]["header"] = header
00760         else:
00761             break
00762     if not summ:
00763         # If the full table is not present, we use only the header
00764         summ[name] = {"header": header}
00765     return summ, pos
00766 
00767 def findHistosSummaries(stdout):
00768     """
00769     Scan stdout to find ROOT TTree summaries and digest them.
00770     """
00771     outlines = stdout.splitlines()
00772     nlines = len(outlines) - 1
00773     summaries = {}
00774     global h_count_re
00775     
00776     pos = 0
00777     while pos < nlines:
00778         summ = {}
00779         # find first line of block:
00780         match = h_count_re.search(outlines[pos])
00781         while pos < nlines and not match:
00782             pos += 1
00783             match = h_count_re.search(outlines[pos])
00784         if match:
00785             summ, pos = parseHistosSummary(outlines, pos)
00786         summaries.update(summ)
00787     return summaries
00788 
00789 class GaudiFilterExecutable(qm.executable.Filter):
00790     def __init__(self, input, timeout = -1):
00791         """Create a new 'Filter'.
00792 
00793         'input' -- The string containing the input to provide to the
00794         child process.
00795 
00796         'timeout' -- As for 'TimeoutExecutable.__init__'."""
00797 
00798         super(GaudiFilterExecutable, self).__init__(input, timeout)
00799         self.__input = input
00800         self.__timeout = timeout
00801         self.stack_trace_file = None
00802         # Temporary file to pass the stack trace from one process to the other
00803         # The file must be closed and reopened when needed to avoid conflicts
00804         # between the processes
00805         tmpf = tempfile.mkstemp()
00806         os.close(tmpf[0])
00807         self.stack_trace_file = tmpf[1] # remember only the name
00808     
00809     def __UseSeparateProcessGroupForChild(self):
00810         """Copied from TimeoutExecutable to allow the re-implementation of
00811            _HandleChild.
00812         """
00813         if sys.platform == "win32":
00814             # In Windows 2000 (or later), we should use "jobs" by
00815             # analogy with UNIX process groups.  However, that
00816             # functionality is not (yet) provided by the Python Win32
00817             # extensions.
00818             return 0
00819         
00820         return self.__timeout >= 0 or self.__timeout == -2
00821     ##
00822     # Needs to replace the ones from RedirectedExecutable and TimeoutExecutable
00823     def _HandleChild(self):
00824         """Code copied from both FilterExecutable and TimeoutExecutable.
00825         """
00826         # Close the pipe ends that we do not need.
00827         if self._stdin_pipe:
00828             self._ClosePipeEnd(self._stdin_pipe[0])
00829         if self._stdout_pipe:
00830             self._ClosePipeEnd(self._stdout_pipe[1])
00831         if self._stderr_pipe:
00832             self._ClosePipeEnd(self._stderr_pipe[1])
00833 
00834         # The pipes created by 'RedirectedExecutable' must be closed
00835         # before the monitor process (created by 'TimeoutExecutable')
00836         # is created.  Otherwise, if the child process dies, 'select'
00837         # in the parent will not return if the monitor process may
00838         # still have one of the file descriptors open.
00839         
00840         super(qm.executable.TimeoutExecutable, self)._HandleChild()
00841         
00842         if self.__UseSeparateProcessGroupForChild():
00843             # Put the child into its own process group.  This step is
00844             # performed in both the parent and the child; therefore both
00845             # processes can safely assume that the creation of the process
00846             # group has taken place.
00847             child_pid = self._GetChildPID()
00848             try:
00849                 os.setpgid(child_pid, child_pid)
00850             except:
00851                 # The call to setpgid may fail if the child has exited,
00852                 # or has already called 'exec'.  In that case, we are
00853                 # guaranteed that the child has already put itself in the
00854                 # desired process group.
00855                 pass
00856             # Create the monitoring process.
00857             #
00858             # If the monitoring process is in parent's process group and
00859             # kills the child after waitpid has returned in the parent, we
00860             # may end up trying to kill a process group other than the one
00861             # that we intend to kill.  Therefore, we put the monitoring
00862             # process in the same process group as the child; that ensures
00863             # that the process group will persist until the monitoring
00864             # process kills it.
00865             self.__monitor_pid = os.fork()
00866             if self.__monitor_pid != 0:
00867                 # Make sure that the monitoring process is placed into the
00868                 # child's process group before the parent process calls
00869                 # 'waitpid'.  In this way, we are guaranteed that the process
00870                 # group as the child 
00871                 os.setpgid(self.__monitor_pid, child_pid)
00872             else:
00873                 # Put the monitoring process into the child's process
00874                 # group.  We know the process group still exists at
00875                 # this point because either (a) we are in the process
00876                 # group, or (b) the parent has not yet called waitpid.
00877                 os.setpgid(0, child_pid)
00878 
00879                 # Close all open file descriptors.  They are not needed
00880                 # in the monitor process.  Furthermore, when the parent
00881                 # closes the write end of the stdin pipe to the child,
00882                 # we do not want the pipe to remain open; leaving the
00883                 # pipe open in the monitor process might cause the child
00884                 # to block waiting for additional input.
00885                 try:
00886                     max_fds = os.sysconf("SC_OPEN_MAX")
00887                 except:
00888                     max_fds = 256
00889                 for fd in xrange(max_fds):
00890                     try:
00891                         os.close(fd)
00892                     except:
00893                         pass
00894                 try:
00895                     if self.__timeout >= 0:
00896                         # Give the child time to run.
00897                         time.sleep (self.__timeout)
00898                         #######################################################
00899                         ### This is the interesting part: dump the stack trace to a file
00900                         if sys.platform == "linux2": # we should be have /proc and gdb
00901                             cmd = ["gdb",
00902                                    os.path.join("/proc", str(child_pid), "exe"),
00903                                    str(child_pid),
00904                                    "-batch", "-n", "-x",
00905                                    "'%s'" % os.path.join(os.path.dirname(__file__), "stack-trace.gdb")]
00906                             # FIXME: I wanted to use subprocess.Popen, but it doesn't want to work
00907                             #        in this context.
00908                             o = os.popen(" ".join(cmd)).read()
00909                             open(self.stack_trace_file,"w").write(o)
00910                         #######################################################
00911                             
00912                         # Kill all processes in the child process group.
00913                         os.kill(0, signal.SIGKILL)
00914                     else:
00915                         # This call to select will never terminate.
00916                         select.select ([], [], [])
00917                 finally:
00918                     # Exit.  This code is in a finally clause so that
00919                     # we are guaranteed to get here no matter what.
00920                     os._exit(0)
00921         elif self.__timeout >= 0 and sys.platform == "win32":
00922             # Create a monitoring thread.
00923             self.__monitor_thread = Thread(target = self.__Monitor)
00924             self.__monitor_thread.start()
00925 
00926     if sys.platform == "win32":
00927 
00928         def __Monitor(self):
00929             """Code copied from FilterExecutable.
00930             Kill the child if the timeout expires.
00931 
00932             This function is run in the monitoring thread."""
00933         
00934             # The timeout may be expressed as a floating-point value
00935             # on UNIX, but it must be an integer number of
00936             # milliseconds when passed to WaitForSingleObject.
00937             timeout = int(self.__timeout * 1000)
00938             # Wait for the child process to terminate or for the
00939             # timer to expire.
00940             result = win32event.WaitForSingleObject(self._GetChildPID(),
00941                                                     timeout)
00942             # If the timeout occurred, kill the child process.
00943             if result == win32con.WAIT_TIMEOUT:
00944                 self.Kill()
00945 
00946 ########################################################################
00947 # Test Classes
00948 ########################################################################
00949 class GaudiExeTest(ExecTestBase):
00950     """Standard Gaudi test.
00951     """
00952     arguments = [
00953         qm.fields.TextField(
00954             name="program",
00955             title="Program",
00956             not_empty_text=1,
00957             description="""The path to the program.
00958 
00959             This field indicates the path to the program.  If it is not
00960             an absolute path, the value of the 'PATH' environment
00961             variable will be used to search for the program.
00962             If not specified, $GAUDIEXE or Gaudi.exe are used.
00963             """
00964             ),
00965         qm.fields.SetField(qm.fields.TextField(
00966             name="args",
00967             title="Argument List",
00968             description="""The command-line arguments.
00969 
00970             If this field is left blank, the program is run without any
00971             arguments.
00972 
00973             Use this field to specify the option files.
00974 
00975             An implicit 0th argument (the path to the program) is added
00976             automatically."""
00977             )),
00978         qm.fields.TextField(
00979             name="options",
00980             title="Options",
00981             description="""Options to be passed to the application.
00982             
00983             This field allows to pass a list of options to the main program
00984             without the need of a separate option file.
00985             
00986             The content of the field is written to a temporary file which name
00987             is passed the the application as last argument (appended to the
00988             field "Argument List".
00989             """,
00990             verbatim="true",
00991             multiline="true",
00992             default_value=""
00993             ),
00994         qm.fields.TextField(
00995             name="workdir",
00996             title="Working Directory",
00997             description="""Path to the working directory.
00998 
00999             If this field is left blank, the program will be run from the qmtest
01000             directory, otherwise from the directory specified.""",
01001             default_value=""
01002             ),
01003         qm.fields.TextField(
01004             name="reference",
01005             title="Reference Output",
01006             description="""Path to the file containing the reference output.
01007 
01008             If this field is left blank, any standard output will be considered
01009             valid.
01010             
01011             If the reference file is specified, any output on standard error is
01012             ignored."""
01013             ),
01014         qm.fields.TextField(
01015             name="error_reference",
01016             title="Reference for standard error",
01017             description="""Path to the file containing the reference for the standard error.
01018 
01019             If this field is left blank, any standard output will be considered
01020             valid.
01021             
01022             If the reference file is specified, any output on standard error is
01023             ignored."""
01024             ),
01025         qm.fields.SetField(qm.fields.TextField(
01026             name = "unsupported_platforms",
01027             title = "Unsupported Platforms",
01028             description = """Platform on which the test must not be run.
01029             
01030             List of regular expressions identifying the platforms on which the
01031             test is not run and the result is set to UNTESTED."""
01032             )),
01033                 
01034         qm.fields.TextField(
01035             name = "validator",
01036             title = "Validator",
01037             description = """Function to validate the output of the test.
01038             
01039             If defined, the function is used to validate the products of the
01040             test.
01041             The function is called passing as arguments:
01042               self:   the test class instance
01043               stdout: the standard output of the executed test
01044               stderr: the standard error of the executed test
01045               result: the Result objects to fill with messages
01046             The function must return a list of causes for the failure.
01047             If specified, overrides standard output, standard error and
01048             reference files.
01049             """,
01050             verbatim="true",
01051             multiline="true",
01052             default_value=""
01053             ),
01054             
01055         qm.fields.BooleanField(
01056             name = "use_temp_dir",
01057             title = "Use temporary directory",
01058             description = """Use temporary directory.
01059             
01060             If set to true, use a temporary directory as working directory.
01061             """,
01062             default_value="false"
01063             ),
01064         ]
01065     
01066     def PlatformIsNotSupported(self, context, result):
01067         platform = self.GetPlatform()
01068         unsupported = [ re.compile(x)
01069                         for x in [ str(y).strip()
01070                                    for y in self.unsupported_platforms ]
01071                         if x
01072                        ]
01073         for p_re in unsupported:
01074             if p_re.search(platform):
01075                 result.SetOutcome(result.UNTESTED)
01076                 result[result.CAUSE] = 'Platform not supported.'
01077                 return True
01078         return False
01079     
01080     def GetPlatform(self):
01081         """
01082         Return the platform Id defined in CMTCONFIG or SCRAM_ARCH.
01083         """
01084         arch = "None"
01085         # check architecture name
01086         if "CMTCONFIG" in os.environ:
01087             arch = os.environ["CMTCONFIG"]
01088         elif "SCRAM_ARCH" in os.environ:
01089             arch = os.environ["SCRAM_ARCH"]
01090         return arch
01091     
01092     def _expandReferenceFileName(self, reffile):
01093         # if no file is passed, do nothing 
01094         if not reffile:
01095             return ""
01096         
01097         reference = os.path.normpath(os.path.expandvars(reffile))
01098         # old-style platform-specific reference name
01099         spec_ref = reference[:-3] + self.GetPlatform()[0:3] + reference[-3:]
01100         if os.path.isfile(spec_ref):
01101             reference = spec_ref
01102         else: # look for new-style platform specific reference files:
01103             # get all the files whose name start with the reference filename
01104             dirname, basename = os.path.split(reference)
01105             if not dirname: dirname = '.'
01106             head = basename + "."
01107             head_len = len(head)
01108             platform = self.GetPlatform()
01109             candidates = []
01110             for f in os.listdir(dirname):
01111                 if f.startswith(head) and platform.startswith(f[head_len:]):
01112                     candidates.append( (len(f) - head_len, f) )
01113             if candidates: # take the one with highest matching
01114                 candidates.sort()
01115                 reference = os.path.join(dirname, candidates[-1][1])
01116         return reference
01117         
01118     def CheckTTreesSummaries(self, stdout, result, causes,
01119                              trees_dict = None,
01120                              ignore = r"Basket|.*size|Compression"):
01121         """
01122         Compare the TTree summaries in stdout with the ones in trees_dict or in
01123         the reference file. By default ignore the size, compression and basket
01124         fields.
01125         The presence of TTree summaries when none is expected is not a failure.
01126         """
01127         if trees_dict is None:
01128             reference = self._expandReferenceFileName(self.reference)
01129             # call the validator if the file exists
01130             if reference and os.path.isfile(reference):
01131                 trees_dict = findTTreeSummaries(open(reference).read())
01132             else:
01133                 trees_dict = {}
01134         
01135         from pprint import PrettyPrinter
01136         pp = PrettyPrinter()
01137         if trees_dict:
01138             result["GaudiTest.TTrees.expected"] = result.Quote(pp.pformat(trees_dict))        
01139             if ignore:
01140                 result["GaudiTest.TTrees.ignore"] = result.Quote(ignore)
01141         
01142         trees = findTTreeSummaries(stdout)
01143         failed = cmpTreesDicts(trees_dict, trees, ignore)
01144         if failed:
01145             causes.append("trees summaries")
01146             msg = "%s: %s != %s" % getCmpFailingValues(trees_dict, trees, failed)
01147             result["GaudiTest.TTrees.failure_on"] = result.Quote(msg)
01148             result["GaudiTest.TTrees.found"] = result.Quote(pp.pformat(trees))
01149         
01150         return causes
01151 
01152     def CheckHistosSummaries(self, stdout, result, causes,
01153                              dict = None,
01154                              ignore = None):
01155         """
01156         Compare the TTree summaries in stdout with the ones in trees_dict or in
01157         the reference file. By default ignore the size, compression and basket
01158         fields.
01159         The presence of TTree summaries when none is expected is not a failure.
01160         """
01161         if dict is None:
01162             reference = self._expandReferenceFileName(self.reference)
01163             # call the validator if the file exists
01164             if reference and os.path.isfile(reference):
01165                 dict = findHistosSummaries(open(reference).read())
01166             else:
01167                 dict = {}
01168         
01169         from pprint import PrettyPrinter
01170         pp = PrettyPrinter()
01171         if dict:
01172             result["GaudiTest.Histos.expected"] = result.Quote(pp.pformat(dict))        
01173             if ignore:
01174                 result["GaudiTest.Histos.ignore"] = result.Quote(ignore)
01175         
01176         histos = findHistosSummaries(stdout)
01177         failed = cmpTreesDicts(dict, histos, ignore)
01178         if failed:
01179             causes.append("histos summaries")
01180             msg = "%s: %s != %s" % getCmpFailingValues(dict, histos, failed)
01181             result["GaudiTest.Histos.failure_on"] = result.Quote(msg)
01182             result["GaudiTest.Histos.found"] = result.Quote(pp.pformat(histos))
01183         
01184         return causes
01185 
01186     def ValidateWithReference(self, stdout, stderr, result, causes, preproc = None):
01187         """
01188         Default validation action: compare standard output and error to the
01189         reference files.
01190         """
01191         # set the default output preprocessor
01192         if preproc is None:
01193             preproc = normalizeExamples
01194         # check standard output
01195         reference = self._expandReferenceFileName(self.reference)
01196         # call the validator if the file exists
01197         if reference and os.path.isfile(reference):
01198             result["GaudiTest.output_reference"] = reference
01199             causes += ReferenceFileValidator(reference,
01200                                              "standard output",
01201                                              "GaudiTest.output_diff",
01202                                              preproc = preproc)(stdout, result)
01203         
01204         # Compare TTree summaries
01205         causes = self.CheckTTreesSummaries(stdout, result, causes)
01206         causes = self.CheckHistosSummaries(stdout, result, causes)
01207         
01208         if causes: # Write a new reference file for stdout
01209             newref = open(reference + ".new","w")
01210             # sanitize newlines
01211             for l in stdout.splitlines():
01212                 newref.write(l.rstrip() + '\n')
01213             del newref # flush and close
01214         
01215         
01216         # check standard error
01217         reference = self._expandReferenceFileName(self.error_reference)
01218         # call the validator if we have a file to use
01219         if reference and os.path.isfile(reference):
01220             result["GaudiTest.error_reference"] = reference
01221             newcauses = ReferenceFileValidator(reference,
01222                                                "standard error",
01223                                                "GaudiTest.error_diff",
01224                                                preproc = preproc)(stderr, result)
01225             causes += newcauses
01226             if newcauses: # Write a new reference file for stdedd
01227                 newref = open(reference + ".new","w")
01228                 # sanitize newlines
01229                 for l in stderr.splitlines():
01230                     newref.write(l.rstrip() + '\n')
01231                 del newref # flush and close    
01232         else:
01233             causes += BasicOutputValidator(self.stderr,
01234                                            "standard error",
01235                                            "ExecTest.expected_stderr")(stderr, result)
01236         
01237         return causes
01238         
01239     def ValidateOutput(self, stdout, stderr, result):
01240         causes = []
01241         # if the test definition contains a custom validator, use it
01242         if self.validator.strip() != "":
01243             class CallWrapper(object):
01244                 """
01245                 Small wrapper class to dynamically bind some default arguments
01246                 to a callable.
01247                 """
01248                 def __init__(self, callable, extra_args = {}):
01249                     self.callable = callable
01250                     self.extra_args = extra_args
01251                     # get the list of names of positional arguments
01252                     from inspect import getargspec
01253                     self.args_order = getargspec(callable)[0]
01254                     # Remove "self" from the list of positional arguments
01255                     # since it is added automatically 
01256                     if self.args_order[0] == "self":
01257                         del self.args_order[0]
01258                 def __call__(self, *args, **kwargs):
01259                     # Check which positional arguments are used
01260                     positional = self.args_order[:len(args)]
01261                     
01262                     kwargs = dict(kwargs) # copy the arguments dictionary
01263                     for a in self.extra_args:
01264                         # use "extra_args" for the arguments not specified as
01265                         # positional or keyword
01266                         if a not in positional and a not in kwargs:
01267                             kwargs[a] = self.extra_args[a]
01268                     return apply(self.callable, args, kwargs)
01269             # local names to be exposed in the script 
01270             exported_symbols = {"self":self,
01271                                 "stdout":stdout,
01272                                 "stderr":stderr,
01273                                 "result":result,
01274                                 "causes":causes,
01275                                 "findReferenceBlock":
01276                                     CallWrapper(findReferenceBlock, {"stdout":stdout,
01277                                                                      "result":result,
01278                                                                      "causes":causes}),
01279                                 "validateWithReference":
01280                                     CallWrapper(self.ValidateWithReference, {"stdout":stdout,
01281                                                                              "stderr":stderr,
01282                                                                              "result":result,
01283                                                                              "causes":causes}),
01284                                 "countErrorLines":
01285                                     CallWrapper(countErrorLines, {"stdout":stdout,
01286                                                                   "result":result,
01287                                                                   "causes":causes}),
01288                                 "checkTTreesSummaries":
01289                                     CallWrapper(self.CheckTTreesSummaries, {"stdout":stdout,
01290                                                                             "result":result,
01291                                                                             "causes":causes}),
01292                                 "checkHistosSummaries":
01293                                     CallWrapper(self.CheckHistosSummaries, {"stdout":stdout,
01294                                                                             "result":result,
01295                                                                             "causes":causes}),
01296                                 
01297                                 }
01298             exec self.validator in globals(), exported_symbols
01299         else:
01300             self.ValidateWithReference(stdout, stderr, result, causes)
01301         
01302         return causes
01303     
01304     def DumpEnvironment(self, result):
01305         """
01306         Add the content of the environment to the result object.
01307         
01308         Copied from the QMTest class of COOL.
01309         """
01310         vars = os.environ.keys()
01311         vars.sort()
01312         result['GaudiTest.environment'] = \
01313             result.Quote('\n'.join(["%s=%s"%(v,os.environ[v]) for v in vars]))
01314 
01315     def _find_program(self,prog):
01316         # check if it is an absolute path or the file can be found
01317         # from the local directory, otherwise search for it in PATH
01318         if not os.path.isabs(prog) and not os.path.isfile(prog):
01319             for d in os.environ["PATH"].split(os.pathsep):
01320                 p = os.path.join(d,prog)
01321                 if os.path.isfile(p):
01322                     return p
01323         return prog
01324         
01325     def Run(self, context, result):
01326         """Run the test.
01327 
01328         'context' -- A 'Context' giving run-time parameters to the
01329         test.
01330 
01331         'result' -- A 'Result' object.  The outcome will be
01332         'Result.PASS' when this method is called.  The 'result' may be
01333         modified by this method to indicate outcomes other than
01334         'Result.PASS' or to add annotations."""
01335         
01336         # Check if the platform is supported
01337         if self.PlatformIsNotSupported(context, result):
01338             return
01339         
01340         def rationalizepath(p):
01341             p = os.path.normpath(os.path.expandvars(p))
01342             if os.path.exists(p):
01343                 p = os.path.realpath(p)
01344             return p
01345         
01346         # Prepare program name and arguments (expanding variables, and converting to absolute) 
01347         if self.program:
01348             prog = rationalizepath(self.program)
01349         elif "GAUDIEXE" in os.environ:
01350             prog = os.environ["GAUDIEXE"]
01351         else:
01352             prog = "Gaudi.exe"
01353         self.program = prog
01354             
01355         dummy, prog_ext = os.path.splitext(prog)
01356         if prog_ext not in [ ".exe", ".py", ".bat" ] and self.GetPlatform()[0:3] == "win":
01357             prog += ".exe"
01358             prog_ext = ".exe"
01359             
01360         prog = self._find_program(prog)
01361         
01362         # Convert paths to absolute paths in arguments and reference files
01363         args = map(rationalizepath, self.args)
01364         self.reference = rationalizepath(self.reference)
01365         self.error_reference = rationalizepath(self.error_reference)
01366         
01367         
01368         # check if the user provided inline options
01369         tmpfile = None
01370         if self.options.strip():
01371             ext = ".opts"
01372             if re.search(r"from\s*Gaudi.Configuration\s*import\s*\*", self.options):
01373                 ext = ".py"
01374             tmpfile = TempFile(ext)
01375             tmpfile.writelines("\n".join(self.options.splitlines()))
01376             tmpfile.flush()
01377             args.append(tmpfile.name)
01378             result["GaudiTest.options"] = result.Quote(self.options)
01379         
01380         # if the program is a python file, execute it through python
01381         if prog_ext == ".py":
01382             args.insert(0,prog)
01383             if self.GetPlatform()[0:3] == "win":
01384                 prog = self._find_program("python.exe")
01385             else:
01386                 prog = self._find_program("python")
01387         
01388         # Change to the working directory if specified or to the default temporary
01389         origdir = os.getcwd()
01390         if self.workdir:
01391             os.chdir(str(os.path.normpath(os.path.expandvars(self.workdir))))
01392         elif "qmtest.tmpdir" in context and self.use_temp_dir == "true":
01393             os.chdir(context["qmtest.tmpdir"])
01394         
01395         if "QMTEST_IGNORE_TIMEOUT" not in os.environ:
01396             self.timeout = max(self.timeout,600)
01397         else:
01398             self.timeout = -1
01399         
01400         try:
01401             # Run the test
01402             self.RunProgram(prog, 
01403                             [ prog ] + args,
01404                             context, result)
01405             # Record the content of the enfironment for failing tests 
01406             if result.GetOutcome() not in [ result.PASS ]:
01407                 self.DumpEnvironment(result)
01408         finally:
01409             # revert to the original directory
01410             os.chdir(origdir)
01411         
01412     def RunProgram(self, program, arguments, context, result):
01413         """Run the 'program'.
01414 
01415         'program' -- The path to the program to run.
01416 
01417         'arguments' -- A list of the arguments to the program.  This
01418         list must contain a first argument corresponding to 'argv[0]'.
01419 
01420         'context' -- A 'Context' giving run-time parameters to the
01421         test.
01422 
01423         'result' -- A 'Result' object.  The outcome will be
01424         'Result.PASS' when this method is called.  The 'result' may be
01425         modified by this method to indicate outcomes other than
01426         'Result.PASS' or to add annotations.
01427         
01428         @attention: This method has been copied from command.ExecTestBase
01429                     (QMTest 2.3.0) and modified to keep stdout and stderr
01430                     for tests that have been terminated by a signal.
01431                     (Fundamental for debugging in the Application Area)
01432         """
01433 
01434         # Construct the environment.
01435         environment = self.MakeEnvironment(context)
01436         # Create the executable.
01437         if self.timeout >= 0:
01438             timeout = self.timeout
01439         else:
01440             # If no timeout was specified, we sill run this process in a
01441             # separate process group and kill the entire process group
01442             # when the child is done executing.  That means that
01443             # orphaned child processes created by the test will be
01444             # cleaned up.
01445             timeout = -2
01446         e = GaudiFilterExecutable(self.stdin, timeout)
01447         # Run it.
01448         exit_status = e.Run(arguments, environment, path = program)
01449         # Get the stack trace from the temporary file (if present)
01450         if e.stack_trace_file and os.path.exists(e.stack_trace_file):
01451             stack_trace = open(e.stack_trace_file).read()
01452             os.remove(e.stack_trace_file)
01453         else:
01454             stack_trace = None
01455         if stack_trace:
01456             result["ExecTest.stack_trace"] = result.Quote(stack_trace)
01457 
01458         # If the process terminated normally, check the outputs.
01459         if sys.platform == "win32" or os.WIFEXITED(exit_status):
01460             # There are no causes of failure yet.
01461             causes = []
01462             # The target program terminated normally.  Extract the
01463             # exit code, if this test checks it.
01464             if self.exit_code is None:
01465                 exit_code = None
01466             elif sys.platform == "win32":
01467                 exit_code = exit_status
01468             else:
01469                 exit_code = os.WEXITSTATUS(exit_status)
01470             # Get the output generated by the program.
01471             stdout = e.stdout
01472             stderr = e.stderr
01473             # Record the results.
01474             result["ExecTest.exit_code"] = str(exit_code)
01475             result["ExecTest.stdout"] = result.Quote(stdout)
01476             result["ExecTest.stderr"] = result.Quote(stderr)
01477             # Check to see if the exit code matches.
01478             if exit_code != self.exit_code:
01479                 causes.append("exit_code")
01480                 result["ExecTest.expected_exit_code"] \
01481                     = str(self.exit_code)
01482             # Validate the output.
01483             causes += self.ValidateOutput(stdout, stderr, result)
01484             # If anything went wrong, the test failed.
01485             if causes:
01486                 result.Fail("Unexpected %s." % string.join(causes, ", ")) 
01487         elif os.WIFSIGNALED(exit_status):
01488             # The target program terminated with a signal.  Construe
01489             # that as a test failure.
01490             signal_number = str(os.WTERMSIG(exit_status))
01491             result.Fail("Program terminated by signal.")
01492             result["ExecTest.signal_number"] = signal_number
01493             result["ExecTest.stdout"] = result.Quote(e.stdout)
01494             result["ExecTest.stderr"] = result.Quote(e.stderr)
01495         elif os.WIFSTOPPED(exit_status):
01496             # The target program was stopped.  Construe that as a
01497             # test failure.
01498             signal_number = str(os.WSTOPSIG(exit_status))
01499             result.Fail("Program stopped by signal.")
01500             result["ExecTest.signal_number"] = signal_number
01501             result["ExecTest.stdout"] = result.Quote(e.stdout)
01502             result["ExecTest.stderr"] = result.Quote(e.stderr)
01503         else:
01504             # The target program terminated abnormally in some other
01505             # manner.  (This shouldn't normally happen...)
01506             result.Fail("Program did not terminate normally.")
01507         
01508         # Marco Cl.: This is a special trick to fix a "problem" with the output
01509         # of gaudi jobs when they use colors
01510         esc = '\x1b'
01511         repr_esc = '\\x1b'
01512         result["ExecTest.stdout"] = result["ExecTest.stdout"].replace(esc,repr_esc)
01513         # TODO: (MCl) improve the hack for colors in standard output
01514         #             may be converting them to HTML tags

Generated at Mon Sep 7 18:05:45 2009 for Gaudi Framework, version v21r4 by Doxygen version 1.5.6 written by Dimitri van Heesch, © 1997-2004