Gaudi Framework, version v21r7

Home   Generated: 22 Jan 2010

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 ## Locates an executable in the executables path ($PATH) and returns the full
00262 #  path to it.
00263 #  If the excutable cannot be found, None is returned
00264 def which(executable):
00265     for d in os.environ.get("PATH").split(os.pathsep):
00266         fullpath = os.path.join(d,executable)
00267         if os.path.exists(fullpath):
00268             return fullpath
00269     return None
00270 
00271 def rationalizepath(p):
00272     p = os.path.normpath(os.path.expandvars(p))
00273     if os.path.exists(p):
00274         p = os.path.realpath(p)
00275     return p
00276 
00277 ########################################################################
00278 # Output Validation Classes
00279 ########################################################################
00280 class BasicOutputValidator:
00281     """Basic implementation of an option validator for Gaudi tests.
00282     This implementation is based on the standard (LCG) validation functions
00283     used in QMTest.
00284     """
00285     def __init__(self,ref,cause,result_key):
00286         self.reference = ref
00287         self.cause = cause
00288         self.result_key = result_key
00289     
00290     def __call__(self, out, result):
00291         """Validate the output of the program.
00292 
00293         'stdout' -- A string containing the data written to the standard output
00294         stream.
00295 
00296         'stderr' -- A string containing the data written to the standard error
00297         stream.
00298 
00299         'result' -- A 'Result' object. It may be used to annotate
00300         the outcome according to the content of stderr.
00301 
00302         returns -- A list of strings giving causes of failure."""
00303 
00304         causes = []
00305         # Check to see if theoutput matches.
00306         if not self.__CompareText(out, self.reference):
00307             causes.append(self.cause) 
00308             result[self.result_key] = result.Quote(self.reference)
00309 
00310         return causes
00311     
00312     def __CompareText(self, s1, s2):
00313         """Compare 's1' and 's2', ignoring line endings.
00314 
00315         's1' -- A string.
00316 
00317         's2' -- A string.
00318 
00319         returns -- True if 's1' and 's2' are the same, ignoring
00320         differences in line endings."""
00321 
00322         # The "splitlines" method works independently of the line ending
00323         # convention in use.
00324         return s1.splitlines() == s2.splitlines()
00325 
00326 class FilePreprocessor:
00327     """ Base class for a callable that takes a file and returns a modified
00328     version of it."""
00329     def __processLine__(self, line):
00330         return line
00331     def __call__(self, input):
00332         if hasattr(input,"__iter__"):
00333             lines = input
00334             mergeback = False
00335         else:
00336             lines = input.splitlines()
00337             mergeback = True
00338         output = []
00339         for l in lines:
00340             l = self.__processLine__(l)
00341             if l: output.append(l)
00342         if mergeback: output = '\n'.join(output)
00343         return output
00344     def __add__(self, rhs):
00345         return FilePreprocessorSequence([self,rhs])
00346 
00347 class FilePreprocessorSequence(FilePreprocessor):
00348     def __init__(self, members = []):
00349         self.members = members
00350     def __add__(self, rhs):
00351         return FilePreprocessorSequence(self.members + [rhs])
00352     def __call__(self, input):
00353         output = input
00354         for pp in self.members:
00355             output = pp(output)
00356         return output
00357 
00358 class LineSkipper(FilePreprocessor):
00359     def __init__(self, strings = [], regexps = []):
00360         import re
00361         self.strings = strings
00362         self.regexps = map(re.compile,regexps)
00363         
00364     def __processLine__(self, line):
00365         for s in self.strings:
00366             if line.find(s) >= 0: return None
00367         for r in self.regexps:
00368             if r.search(line): return None
00369         return line
00370 
00371 class BlockSkipper(FilePreprocessor):
00372     def __init__(self, start, end):
00373         self.start = start
00374         self.end = end
00375         self._skipping = False
00376         
00377     def __processLine__(self, line):
00378         if self.start in line:
00379             self._skipping = True
00380             return None
00381         elif self.end in line:
00382             self._skipping = False
00383         elif self._skipping:
00384             return None
00385         return line
00386 
00387 class RegexpReplacer(FilePreprocessor):
00388     def __init__(self, orig, repl = "", when = None):
00389         if when:
00390             when = re.compile(when)
00391         self._operations = [ (when, re.compile(orig), repl) ]
00392     def __add__(self,rhs):
00393         if isinstance(rhs, RegexpReplacer):
00394             res = RegexpReplacer("","",None)
00395             res._operations = self._operations + rhs._operations
00396         else:
00397             res = FilePreprocessor.__add__(self, rhs)
00398         return res
00399     def __processLine__(self, line):
00400         for w,o,r in self._operations:
00401             if w is None or w.search(line):
00402                 line = o.sub(r, line)
00403         return line
00404 
00405 # Common preprocessors        
00406 maskPointers  = RegexpReplacer("0x[0-9a-fA-F]{4,16}","0x########")
00407 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)?",
00408                                "00:00:00 1970-01-01")
00409 normalizeEOL = FilePreprocessor()
00410 normalizeEOL.__processLine__ = lambda line: str(line).rstrip() + '\n'
00411 
00412 skipEmptyLines = FilePreprocessor()
00413 # FIXME: that's ugly
00414 skipEmptyLines.__processLine__ = lambda line: (line.strip() and line) or None
00415 
00416 ## Special preprocessor sorting the list of strings (whitespace separated)
00417 #  that follow a signature on a single line
00418 class LineSorter(FilePreprocessor):
00419     def __init__(self, signature):
00420         self.signature = signature
00421         self.siglen = len(signature)
00422     def __processLine__(self, line):
00423         pos = line.find(self.signature)
00424         if pos >=0:
00425             line = line[:(pos+self.siglen)]
00426             lst = line[(pos+self.siglen):].split()
00427             lst.sort()
00428             line += " ".join(lst)
00429         return line
00430 
00431 # Preprocessors for GaudiExamples
00432 normalizeExamples = maskPointers + normalizeDate 
00433 for w,o,r in [
00434               #("TIMER.TIMER",r"[0-9]", "0"), # Normalize time output
00435               ("TIMER.TIMER",r"\s+[+-]?[0-9]+[0-9.]*", " 0"), # Normalize time output
00436               ("release all pending",r"^.*/([^/]*:.*)",r"\1"),
00437               ("0x########",r"\[.*/([^/]*.*)\]",r"[\1]"),
00438               ("^#.*file",r"file '.*[/\\]([^/\\]*)$",r"file '\1"),
00439               ("^JobOptionsSvc.*options successfully read in from",r"read in from .*[/\\]([^/\\]*)$",r"file \1"), # normalize path to options
00440               # Normalize UUID, except those ending with all 0s (i.e. the class IDs)
00441               (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"),
00442               # Absorb a change in ServiceLocatorHelper
00443               ("ServiceLocatorHelper::", "ServiceLocatorHelper::(create|locate)Service", "ServiceLocatorHelper::service"),
00444               # Remove the leading 0 in Windows' exponential format
00445               (None, r"e([-+])0([0-9][0-9])", r"e\1\2"),
00446               ]: #[ ("TIMER.TIMER","[0-9]+[0-9.]*", "") ]
00447     normalizeExamples += RegexpReplacer(o,r,w)
00448 normalizeExamples = LineSkipper(["//GP:",
00449                                  "Time User",
00450                                  "Welcome to",
00451                                  "This machine has a speed",
00452                                  "TIME:",
00453                                  "running on",
00454                                  "ToolSvc.Sequenc...   INFO",
00455                                  "DataListenerSvc      INFO XML written to file:",
00456                                  "[INFO]","[WARNING]",
00457                                  "DEBUG No writable file catalog found which contains FID:",
00458                                  "0 local", # hack for ErrorLogExample
00459                                  "DEBUG Service base class initialized successfully", # changed between v20 and v21 
00460                                  "DEBUG Incident  timing:", # introduced with patch #3487
00461                                  # This comes from ROOT, when using GaudiPython 
00462                                  'Note: (file "(tmpfile)", line 2) File "set" already loaded',
00463                                  ],regexps = [
00464                                  r"^#", # Ignore python comments
00465                                  r"(Always|SUCCESS)\s*(Root f|[^ ]* F)ile version:", # skip the message reporting the version of the root file
00466                                  r"0x[0-9a-fA-F#]+ *Algorithm::sysInitialize\(\) *\[", # hack for ErrorLogExample
00467                                  r"0x[0-9a-fA-F#]* *__gxx_personality_v0 *\[", # hack for ErrorLogExample
00468                                  r"File '.*.xml' does not exist",
00469                                  r"INFO Refer to dataset .* by its file ID:",
00470                                  r"INFO Referring to dataset .* by its file ID:",
00471                                  r"INFO Disconnect from dataset",
00472                                  r"INFO Disconnected from dataset",
00473                                  r"INFO Disconnected data IO:",
00474                                  r"IncidentSvc\s*(DEBUG (Adding|Removing)|VERBOSE Calling)",
00475                                  # I want to ignore the header of the unchecked StatusCode report
00476                                  r"^StatusCodeSvc.*listing all unchecked return codes:",
00477                                  r"^StatusCodeSvc\s*INFO\s*$",
00478                                  r"Num\s*|\s*Function\s*|\s*Source Library",
00479                                  r"^[-+]*\s*$",
00480                                  # Hide the fake error message coming from POOL/ROOT (ROOT 5.21)
00481                                  r"ERROR Failed to modify file: .* Errno=2 No such file or directory",
00482                                  # Hide unckeched StatusCodes  from dictionaries
00483                                  r"^ +[0-9]+ \|.*ROOT", 
00484                                  r"^ +[0-9]+ \|.*\|.*Dict", 
00485                                  # Remove ROOT TTree summary table, which changes from one version to the other
00486                                  r"^\*.*\*$",
00487                                  # Remove Histos Summaries
00488                                  r"SUCCESS\s*Booked \d+ Histogram\(s\)",
00489                                  r"^ \|",
00490                                  r"^ ID=",
00491                                  ] ) + normalizeExamples + skipEmptyLines + \
00492                                   normalizeEOL + \
00493                                   LineSorter("Services to release : ")
00494 
00495 class ReferenceFileValidator:
00496     def __init__(self, reffile, cause, result_key, preproc = normalizeExamples):
00497         self.reffile = os.path.expandvars(reffile)
00498         self.cause = cause
00499         self.result_key = result_key
00500         self.preproc = preproc
00501     def __call__(self, stdout, result):
00502         causes = []
00503         if os.path.isfile(self.reffile):
00504             orig = open(self.reffile).xreadlines()
00505             if self.preproc:
00506                 orig = self.preproc(orig)
00507         else:
00508             orig = []
00509         
00510         new = stdout.splitlines()
00511         if self.preproc:
00512             new = self.preproc(new)
00513         #open(self.reffile + ".test","w").writelines(new)
00514         diffs = difflib.ndiff(orig,new,charjunk=difflib.IS_CHARACTER_JUNK)
00515         filterdiffs = map(lambda x: x.strip(),filter(lambda x: x[0] != " ",diffs))
00516         #filterdiffs = [x.strip() for x in diffs]
00517         if filterdiffs:
00518             result[self.result_key] = result.Quote("\n".join(filterdiffs))
00519             result[self.result_key] += result.Quote("""
00520 Legend:
00521         -) reference file
00522         +) standard output of the test""")
00523             causes.append(self.cause)
00524         
00525         return causes
00526 
00527 ########################################################################
00528 # Useful validation functions
00529 ########################################################################
00530 def findReferenceBlock(reference, stdout, result, causes, signature_offset=0, signature=None,
00531                        id = None):
00532     """
00533     Given a block of text, tries to find it in the output.
00534     The block had to be identified by a signature line. By default, the first
00535     line is used as signature, or the line pointed to by signature_offset. If
00536     signature_offset points outside the block, a signature line can be passed as
00537     signature argument. Note: if 'signature' is None (the default), a negative
00538     signature_offset is interpreted as index in a list (e.g. -1 means the last
00539     line), otherwise the it is interpreted as the number of lines before the
00540     first one of the block the signature must appear.
00541     The parameter 'id' allow to distinguish between different calls to this
00542     function in the same validation code.
00543     """
00544     # split reference file, sanitize EOLs and remove empty lines
00545     reflines = filter(None,map(lambda s: s.rstrip(), reference.splitlines()))
00546     if not reflines:
00547         raise RuntimeError("Empty (or null) reference")
00548     # the same on standard output
00549     outlines = filter(None,map(lambda s: s.rstrip(), stdout.splitlines()))
00550     
00551     res_field = "GaudiTest.RefBlock"
00552     if id:
00553         res_field += "_%s" % id 
00554     
00555     if signature is None:
00556         if signature_offset < 0:
00557             signature_offset = len(reference)+signature_offset
00558         signature = reflines[signature_offset]
00559     # find the reference block in the output file
00560     try:
00561         pos = outlines.index(signature)
00562         outlines = outlines[pos-signature_offset:pos+len(reflines)-signature_offset]
00563         if reflines != outlines:
00564             msg = "standard output"
00565             # I do not want 2 messages in causes if teh function is called twice
00566             if not msg in causes: 
00567                 causes.append(msg)
00568             result[res_field + ".observed"] = result.Quote("\n".join(outlines))
00569     except ValueError:
00570         causes.append("missing signature")
00571     result[res_field + ".signature"] = result.Quote(signature)
00572     if len(reflines) > 1 or signature != reflines[0]:
00573         result[res_field + ".expected"] = result.Quote("\n".join(reflines))
00574     
00575     return causes
00576 
00577 def countErrorLines(expected = {'ERROR':0, 'FATAL':0}, **kwargs):
00578     """
00579     Count the number of messages with required severity (by default ERROR and FATAL)
00580     and check if their numbers match the expected ones (0 by default).
00581     The dictionary "expected" can be used to tune the number of errors and fatals
00582     allowed, or to limit the number of expected warnings etc.
00583     """
00584     stdout = kwargs["stdout"]
00585     result = kwargs["result"]
00586     causes = kwargs["causes"]
00587     
00588     # prepare the dictionary to record the extracted lines
00589     errors = {}
00590     for sev in expected:
00591         errors[sev] = []
00592     
00593     outlines = stdout.splitlines()
00594     from math import log10
00595     fmt = "%%%dd - %%s" % (int(log10(len(outlines))+1))
00596     
00597     linecount = 0
00598     for l in outlines:
00599         linecount += 1
00600         words = l.split()
00601         if len(words) >= 2 and words[1] in errors:
00602             errors[words[1]].append(fmt%(linecount,l.rstrip()))
00603     
00604     for e in errors:
00605         if len(errors[e]) != expected[e]:
00606             causes.append('%s(%d)'%(e,len(errors[e])))
00607             result["GaudiTest.lines.%s"%e] = result.Quote('\n'.join(errors[e]))
00608             result["GaudiTest.lines.%s.expected#"%e] = result.Quote(str(expected[e]))
00609     
00610     return causes
00611 
00612 
00613 def _parseTTreeSummary(lines, pos):
00614     """
00615     Parse the TTree summary table in lines, starting from pos.
00616     Returns a tuple with the dictionary with the digested informations and the
00617     position of the first line after the summary.
00618     """
00619     result = {}
00620     i = pos + 1 # first line is a sequence of '*'
00621     count = len(lines)
00622     
00623     splitcols = lambda l: [ f.strip() for f in l.strip("*\n").split(':',2) ]
00624     def parseblock(ll):
00625         r = {}
00626         cols = splitcols(ll[0])
00627         r["Name"], r["Title"] = cols[1:]
00628         
00629         cols = splitcols(ll[1])
00630         r["Entries"] = int(cols[1])
00631         
00632         sizes = cols[2].split()
00633         r["Total size"] = int(sizes[2])
00634         if sizes[-1] == "memory":
00635             r["File size"] = 0
00636         else:
00637             r["File size"] = int(sizes[-1])
00638         
00639         cols = splitcols(ll[2])
00640         sizes = cols[2].split()
00641         if cols[0] == "Baskets":
00642             r["Baskets"] = int(cols[1])
00643             r["Basket size"] = int(sizes[2])
00644         r["Compression"] = float(sizes[-1])
00645         return r    
00646         
00647     if i < (count - 3) and lines[i].startswith("*Tree"):
00648         result = parseblock(lines[i:i+3])
00649         result["Branches"] = {}
00650         i += 4
00651         while i < (count - 3) and lines[i].startswith("*Br"):
00652             branch = parseblock(lines[i:i+3])
00653             result["Branches"][branch["Name"]] = branch
00654             i += 4
00655         
00656     return (result, i)
00657 
00658 def findTTreeSummaries(stdout):
00659     """
00660     Scan stdout to find ROOT TTree summaries and digest them.
00661     """
00662     stars = re.compile(r"^\*+$")
00663     outlines = stdout.splitlines()
00664     nlines = len(outlines)
00665     trees = {}
00666     
00667     i = 0
00668     while i < nlines: #loop over the output
00669         # look for
00670         while i < nlines and not stars.match(outlines[i]):
00671             i += 1
00672         if i < nlines:
00673             tree, i = _parseTTreeSummary(outlines, i)
00674             if tree:
00675                 trees[tree["Name"]] = tree
00676     
00677     return trees
00678 
00679 def cmpTreesDicts(reference, to_check, ignore = None):
00680     """
00681     Check that all the keys in reference are in to_check too, with the same value.
00682     If the value is a dict, the function is called recursively. to_check can
00683     contain more keys than reference, that will not be tested.
00684     The function returns at the first difference found. 
00685     """
00686     fail_keys = []
00687     # filter the keys in the reference dictionary
00688     if ignore:
00689         ignore_re = re.compile(ignore)
00690         keys = [ key for key in reference if not ignore_re.match(key) ]
00691     else:
00692         keys = reference.keys()
00693     # loop over the keys (not ignored) in the reference dictionary
00694     for k in keys:
00695         if k in to_check: # the key must be in the dictionary to_check
00696             if (type(reference[k]) is dict) and (type(to_check[k]) is dict):
00697                 # if both reference and to_check values are dictionaries, recurse
00698                 failed = fail_keys = cmpTreesDicts(reference[k], to_check[k], ignore)
00699             else:
00700                 # compare the two values
00701                 failed = to_check[k] != reference[k]
00702         else: # handle missing keys in the dictionary to check (i.e. failure)
00703             to_check[k] = None
00704             failed = True
00705         if failed:
00706             fail_keys.insert(0, k)
00707             break # exit from the loop at the first failure
00708     return fail_keys # return the list of keys bringing to the different values
00709 
00710 def getCmpFailingValues(reference, to_check, fail_path):
00711     c = to_check
00712     r = reference
00713     for k in fail_path:
00714         c = c.get(k,None)
00715         r = r.get(k,None)
00716         if c is None or r is None:
00717             break # one of the dictionaries is not deep enough
00718     return (fail_path, r, c)
00719 
00720 # signature of the print-out of the histograms
00721 h_count_re = re.compile(r"^(.*)SUCCESS\s+Booked (\d+) Histogram\(s\) :\s+(.*)")
00722 
00723 def parseHistosSummary(lines, pos):
00724     """
00725     Extract the histograms infos from the lines starting at pos.
00726     Returns the position of the first line after the summary block.
00727     """
00728     global h_count_re
00729     h_table_head = re.compile(r'SUCCESS\s+List of booked (1D|2D|3D|1D profile|2D profile) histograms in directory\s+"(\w*)"')
00730     h_short_summ = re.compile(r"ID=([^\"]+)\s+\"([^\"]+)\"\s+(.*)")
00731     
00732     nlines = len(lines)
00733     
00734     # decode header
00735     m = h_count_re.search(lines[pos])
00736     name = m.group(1).strip()
00737     total = int(m.group(2))
00738     header = {}
00739     for k, v in [ x.split("=") for x in  m.group(3).split() ]:
00740         header[k] = int(v)
00741     pos += 1
00742     header["Total"] = total
00743     
00744     summ = {}
00745     while pos < nlines:
00746         m = h_table_head.search(lines[pos])
00747         if m:
00748             t, d = m.groups(1) # type and directory
00749             t = t.replace(" profile", "Prof")
00750             pos += 1
00751             if pos < nlines:
00752                 l = lines[pos]
00753             else:
00754                 l = ""
00755             cont = {}
00756             if l.startswith(" | ID"):
00757                 # table format
00758                 titles = [ x.strip() for x in l.split("|")][1:]
00759                 pos += 1
00760                 while pos < nlines and lines[pos].startswith(" |"):
00761                     l = lines[pos]
00762                     values = [ x.strip() for x in l.split("|")][1:]
00763                     hcont = {}
00764                     for i in range(len(titles)):
00765                         hcont[titles[i]] = values[i]
00766                     cont[hcont["ID"]] = hcont
00767                     pos += 1
00768             elif l.startswith(" ID="):
00769                 while pos < nlines and lines[pos].startswith(" ID="):
00770                     values = [ x.strip() for x in  h_short_summ.search(lines[pos]).groups() ]
00771                     cont[values[0]] = values
00772                     pos += 1
00773             else: # not interpreted
00774                 raise RuntimeError("Cannot understand line %d: '%s'" % (pos, l))
00775             if not d in summ:
00776                 summ[d] = {}
00777             summ[d][t] = cont
00778             summ[d]["header"] = header
00779         else:
00780             break
00781     if not summ:
00782         # If the full table is not present, we use only the header
00783         summ[name] = {"header": header}
00784     return summ, pos
00785 
00786 def findHistosSummaries(stdout):
00787     """
00788     Scan stdout to find ROOT TTree summaries and digest them.
00789     """
00790     outlines = stdout.splitlines()
00791     nlines = len(outlines) - 1
00792     summaries = {}
00793     global h_count_re
00794     
00795     pos = 0
00796     while pos < nlines:
00797         summ = {}
00798         # find first line of block:
00799         match = h_count_re.search(outlines[pos])
00800         while pos < nlines and not match:
00801             pos += 1
00802             match = h_count_re.search(outlines[pos])
00803         if match:
00804             summ, pos = parseHistosSummary(outlines, pos)
00805         summaries.update(summ)
00806     return summaries
00807 
00808 class GaudiFilterExecutable(qm.executable.Filter):
00809     def __init__(self, input, timeout = -1):
00810         """Create a new 'Filter'.
00811 
00812         'input' -- The string containing the input to provide to the
00813         child process.
00814 
00815         'timeout' -- As for 'TimeoutExecutable.__init__'."""
00816 
00817         super(GaudiFilterExecutable, self).__init__(input, timeout)
00818         self.__input = input
00819         self.__timeout = timeout
00820         self.stack_trace_file = None
00821         # Temporary file to pass the stack trace from one process to the other
00822         # The file must be closed and reopened when needed to avoid conflicts
00823         # between the processes
00824         tmpf = tempfile.mkstemp()
00825         os.close(tmpf[0])
00826         self.stack_trace_file = tmpf[1] # remember only the name
00827     
00828     def __UseSeparateProcessGroupForChild(self):
00829         """Copied from TimeoutExecutable to allow the re-implementation of
00830            _HandleChild.
00831         """
00832         if sys.platform == "win32":
00833             # In Windows 2000 (or later), we should use "jobs" by
00834             # analogy with UNIX process groups.  However, that
00835             # functionality is not (yet) provided by the Python Win32
00836             # extensions.
00837             return 0
00838         
00839         return self.__timeout >= 0 or self.__timeout == -2
00840     ##
00841     # Needs to replace the ones from RedirectedExecutable and TimeoutExecutable
00842     def _HandleChild(self):
00843         """Code copied from both FilterExecutable and TimeoutExecutable.
00844         """
00845         # Close the pipe ends that we do not need.
00846         if self._stdin_pipe:
00847             self._ClosePipeEnd(self._stdin_pipe[0])
00848         if self._stdout_pipe:
00849             self._ClosePipeEnd(self._stdout_pipe[1])
00850         if self._stderr_pipe:
00851             self._ClosePipeEnd(self._stderr_pipe[1])
00852 
00853         # The pipes created by 'RedirectedExecutable' must be closed
00854         # before the monitor process (created by 'TimeoutExecutable')
00855         # is created.  Otherwise, if the child process dies, 'select'
00856         # in the parent will not return if the monitor process may
00857         # still have one of the file descriptors open.
00858         
00859         super(qm.executable.TimeoutExecutable, self)._HandleChild()
00860         
00861         if self.__UseSeparateProcessGroupForChild():
00862             # Put the child into its own process group.  This step is
00863             # performed in both the parent and the child; therefore both
00864             # processes can safely assume that the creation of the process
00865             # group has taken place.
00866             child_pid = self._GetChildPID()
00867             try:
00868                 os.setpgid(child_pid, child_pid)
00869             except:
00870                 # The call to setpgid may fail if the child has exited,
00871                 # or has already called 'exec'.  In that case, we are
00872                 # guaranteed that the child has already put itself in the
00873                 # desired process group.
00874                 pass
00875             # Create the monitoring process.
00876             #
00877             # If the monitoring process is in parent's process group and
00878             # kills the child after waitpid has returned in the parent, we
00879             # may end up trying to kill a process group other than the one
00880             # that we intend to kill.  Therefore, we put the monitoring
00881             # process in the same process group as the child; that ensures
00882             # that the process group will persist until the monitoring
00883             # process kills it.
00884             self.__monitor_pid = os.fork()
00885             if self.__monitor_pid != 0:
00886                 # Make sure that the monitoring process is placed into the
00887                 # child's process group before the parent process calls
00888                 # 'waitpid'.  In this way, we are guaranteed that the process
00889                 # group as the child 
00890                 os.setpgid(self.__monitor_pid, child_pid)
00891             else:
00892                 # Put the monitoring process into the child's process
00893                 # group.  We know the process group still exists at
00894                 # this point because either (a) we are in the process
00895                 # group, or (b) the parent has not yet called waitpid.
00896                 os.setpgid(0, child_pid)
00897 
00898                 # Close all open file descriptors.  They are not needed
00899                 # in the monitor process.  Furthermore, when the parent
00900                 # closes the write end of the stdin pipe to the child,
00901                 # we do not want the pipe to remain open; leaving the
00902                 # pipe open in the monitor process might cause the child
00903                 # to block waiting for additional input.
00904                 try:
00905                     max_fds = os.sysconf("SC_OPEN_MAX")
00906                 except:
00907                     max_fds = 256
00908                 for fd in xrange(max_fds):
00909                     try:
00910                         os.close(fd)
00911                     except:
00912                         pass
00913                 try:
00914                     if self.__timeout >= 0:
00915                         # Give the child time to run.
00916                         time.sleep (self.__timeout)
00917                         #######################################################
00918                         ### This is the interesting part: dump the stack trace to a file
00919                         if sys.platform == "linux2": # we should be have /proc and gdb
00920                             cmd = ["gdb",
00921                                    os.path.join("/proc", str(child_pid), "exe"),
00922                                    str(child_pid),
00923                                    "-batch", "-n", "-x",
00924                                    "'%s'" % os.path.join(os.path.dirname(__file__), "stack-trace.gdb")]
00925                             # FIXME: I wanted to use subprocess.Popen, but it doesn't want to work
00926                             #        in this context.
00927                             o = os.popen(" ".join(cmd)).read()
00928                             open(self.stack_trace_file,"w").write(o)
00929                         #######################################################
00930                             
00931                         # Kill all processes in the child process group.
00932                         os.kill(0, signal.SIGKILL)
00933                     else:
00934                         # This call to select will never terminate.
00935                         select.select ([], [], [])
00936                 finally:
00937                     # Exit.  This code is in a finally clause so that
00938                     # we are guaranteed to get here no matter what.
00939                     os._exit(0)
00940         elif self.__timeout >= 0 and sys.platform == "win32":
00941             # Create a monitoring thread.
00942             self.__monitor_thread = Thread(target = self.__Monitor)
00943             self.__monitor_thread.start()
00944 
00945     if sys.platform == "win32":
00946 
00947         def __Monitor(self):
00948             """Code copied from FilterExecutable.
00949             Kill the child if the timeout expires.
00950 
00951             This function is run in the monitoring thread."""
00952         
00953             # The timeout may be expressed as a floating-point value
00954             # on UNIX, but it must be an integer number of
00955             # milliseconds when passed to WaitForSingleObject.
00956             timeout = int(self.__timeout * 1000)
00957             # Wait for the child process to terminate or for the
00958             # timer to expire.
00959             result = win32event.WaitForSingleObject(self._GetChildPID(),
00960                                                     timeout)
00961             # If the timeout occurred, kill the child process.
00962             if result == win32con.WAIT_TIMEOUT:
00963                 self.Kill()
00964 
00965 ########################################################################
00966 # Test Classes
00967 ########################################################################
00968 class GaudiExeTest(ExecTestBase):
00969     """Standard Gaudi test.
00970     """
00971     arguments = [
00972         qm.fields.TextField(
00973             name="program",
00974             title="Program",
00975             not_empty_text=1,
00976             description="""The path to the program.
00977 
00978             This field indicates the path to the program.  If it is not
00979             an absolute path, the value of the 'PATH' environment
00980             variable will be used to search for the program.
00981             If not specified, $GAUDIEXE or Gaudi.exe are used.
00982             """
00983             ),
00984         qm.fields.SetField(qm.fields.TextField(
00985             name="args",
00986             title="Argument List",
00987             description="""The command-line arguments.
00988 
00989             If this field is left blank, the program is run without any
00990             arguments.
00991 
00992             Use this field to specify the option files.
00993 
00994             An implicit 0th argument (the path to the program) is added
00995             automatically."""
00996             )),
00997         qm.fields.TextField(
00998             name="options",
00999             title="Options",
01000             description="""Options to be passed to the application.
01001             
01002             This field allows to pass a list of options to the main program
01003             without the need of a separate option file.
01004             
01005             The content of the field is written to a temporary file which name
01006             is passed the the application as last argument (appended to the
01007             field "Argument List".
01008             """,
01009             verbatim="true",
01010             multiline="true",
01011             default_value=""
01012             ),
01013         qm.fields.TextField(
01014             name="workdir",
01015             title="Working Directory",
01016             description="""Path to the working directory.
01017 
01018             If this field is left blank, the program will be run from the qmtest
01019             directory, otherwise from the directory specified.""",
01020             default_value=""
01021             ),
01022         qm.fields.TextField(
01023             name="reference",
01024             title="Reference Output",
01025             description="""Path to the file containing the reference output.
01026 
01027             If this field is left blank, any standard output will be considered
01028             valid.
01029             
01030             If the reference file is specified, any output on standard error is
01031             ignored."""
01032             ),
01033         qm.fields.TextField(
01034             name="error_reference",
01035             title="Reference for standard error",
01036             description="""Path to the file containing the reference for the standard error.
01037 
01038             If this field is left blank, any standard output will be considered
01039             valid.
01040             
01041             If the reference file is specified, any output on standard error is
01042             ignored."""
01043             ),
01044         qm.fields.SetField(qm.fields.TextField(
01045             name = "unsupported_platforms",
01046             title = "Unsupported Platforms",
01047             description = """Platform on which the test must not be run.
01048             
01049             List of regular expressions identifying the platforms on which the
01050             test is not run and the result is set to UNTESTED."""
01051             )),
01052                 
01053         qm.fields.TextField(
01054             name = "validator",
01055             title = "Validator",
01056             description = """Function to validate the output of the test.
01057             
01058             If defined, the function is used to validate the products of the
01059             test.
01060             The function is called passing as arguments:
01061               self:   the test class instance
01062               stdout: the standard output of the executed test
01063               stderr: the standard error of the executed test
01064               result: the Result objects to fill with messages
01065             The function must return a list of causes for the failure.
01066             If specified, overrides standard output, standard error and
01067             reference files.
01068             """,
01069             verbatim="true",
01070             multiline="true",
01071             default_value=""
01072             ),
01073             
01074         qm.fields.BooleanField(
01075             name = "use_temp_dir",
01076             title = "Use temporary directory",
01077             description = """Use temporary directory.
01078             
01079             If set to true, use a temporary directory as working directory.
01080             """,
01081             default_value="false"
01082             ),
01083         ]
01084     
01085     def PlatformIsNotSupported(self, context, result):
01086         platform = self.GetPlatform()
01087         unsupported = [ re.compile(x)
01088                         for x in [ str(y).strip()
01089                                    for y in self.unsupported_platforms ]
01090                         if x
01091                        ]
01092         for p_re in unsupported:
01093             if p_re.search(platform):
01094                 result.SetOutcome(result.UNTESTED)
01095                 result[result.CAUSE] = 'Platform not supported.'
01096                 return True
01097         return False
01098     
01099     def GetPlatform(self):
01100         """
01101         Return the platform Id defined in CMTCONFIG or SCRAM_ARCH.
01102         """
01103         arch = "None"
01104         # check architecture name
01105         if "CMTCONFIG" in os.environ:
01106             arch = os.environ["CMTCONFIG"]
01107         elif "SCRAM_ARCH" in os.environ:
01108             arch = os.environ["SCRAM_ARCH"]
01109         return arch
01110     
01111     def _expandReferenceFileName(self, reffile):
01112         # if no file is passed, do nothing 
01113         if not reffile:
01114             return ""
01115         
01116         reference = os.path.normpath(os.path.expandvars(reffile))
01117         # old-style platform-specific reference name
01118         spec_ref = reference[:-3] + self.GetPlatform()[0:3] + reference[-3:]
01119         if os.path.isfile(spec_ref):
01120             reference = spec_ref
01121         else: # look for new-style platform specific reference files:
01122             # get all the files whose name start with the reference filename
01123             dirname, basename = os.path.split(reference)
01124             if not dirname: dirname = '.'
01125             head = basename + "."
01126             head_len = len(head)
01127             platform = self.GetPlatform()
01128             candidates = []
01129             for f in os.listdir(dirname):
01130                 if f.startswith(head) and platform.startswith(f[head_len:]):
01131                     candidates.append( (len(f) - head_len, f) )
01132             if candidates: # take the one with highest matching
01133                 candidates.sort()
01134                 reference = os.path.join(dirname, candidates[-1][1])
01135         return reference
01136         
01137     def CheckTTreesSummaries(self, stdout, result, causes,
01138                              trees_dict = None,
01139                              ignore = r"Basket|.*size|Compression"):
01140         """
01141         Compare the TTree summaries in stdout with the ones in trees_dict or in
01142         the reference file. By default ignore the size, compression and basket
01143         fields.
01144         The presence of TTree summaries when none is expected is not a failure.
01145         """
01146         if trees_dict is None:
01147             reference = self._expandReferenceFileName(self.reference)
01148             # call the validator if the file exists
01149             if reference and os.path.isfile(reference):
01150                 trees_dict = findTTreeSummaries(open(reference).read())
01151             else:
01152                 trees_dict = {}
01153         
01154         from pprint import PrettyPrinter
01155         pp = PrettyPrinter()
01156         if trees_dict:
01157             result["GaudiTest.TTrees.expected"] = result.Quote(pp.pformat(trees_dict))        
01158             if ignore:
01159                 result["GaudiTest.TTrees.ignore"] = result.Quote(ignore)
01160         
01161         trees = findTTreeSummaries(stdout)
01162         failed = cmpTreesDicts(trees_dict, trees, ignore)
01163         if failed:
01164             causes.append("trees summaries")
01165             msg = "%s: %s != %s" % getCmpFailingValues(trees_dict, trees, failed)
01166             result["GaudiTest.TTrees.failure_on"] = result.Quote(msg)
01167             result["GaudiTest.TTrees.found"] = result.Quote(pp.pformat(trees))
01168         
01169         return causes
01170 
01171     def CheckHistosSummaries(self, stdout, result, causes,
01172                              dict = None,
01173                              ignore = None):
01174         """
01175         Compare the TTree summaries in stdout with the ones in trees_dict or in
01176         the reference file. By default ignore the size, compression and basket
01177         fields.
01178         The presence of TTree summaries when none is expected is not a failure.
01179         """
01180         if dict is None:
01181             reference = self._expandReferenceFileName(self.reference)
01182             # call the validator if the file exists
01183             if reference and os.path.isfile(reference):
01184                 dict = findHistosSummaries(open(reference).read())
01185             else:
01186                 dict = {}
01187         
01188         from pprint import PrettyPrinter
01189         pp = PrettyPrinter()
01190         if dict:
01191             result["GaudiTest.Histos.expected"] = result.Quote(pp.pformat(dict))        
01192             if ignore:
01193                 result["GaudiTest.Histos.ignore"] = result.Quote(ignore)
01194         
01195         histos = findHistosSummaries(stdout)
01196         failed = cmpTreesDicts(dict, histos, ignore)
01197         if failed:
01198             causes.append("histos summaries")
01199             msg = "%s: %s != %s" % getCmpFailingValues(dict, histos, failed)
01200             result["GaudiTest.Histos.failure_on"] = result.Quote(msg)
01201             result["GaudiTest.Histos.found"] = result.Quote(pp.pformat(histos))
01202         
01203         return causes
01204 
01205     def ValidateWithReference(self, stdout, stderr, result, causes, preproc = None):
01206         """
01207         Default validation action: compare standard output and error to the
01208         reference files.
01209         """
01210         # set the default output preprocessor
01211         if preproc is None:
01212             preproc = normalizeExamples
01213         # check standard output
01214         reference = self._expandReferenceFileName(self.reference)
01215         # call the validator if the file exists
01216         if reference and os.path.isfile(reference):
01217             result["GaudiTest.output_reference"] = reference
01218             causes += ReferenceFileValidator(reference,
01219                                              "standard output",
01220                                              "GaudiTest.output_diff",
01221                                              preproc = preproc)(stdout, result)
01222         
01223         # Compare TTree summaries
01224         causes = self.CheckTTreesSummaries(stdout, result, causes)
01225         causes = self.CheckHistosSummaries(stdout, result, causes)
01226         
01227         if causes: # Write a new reference file for stdout
01228             try:
01229                 newref = open(reference + ".new","w")
01230                 # sanitize newlines
01231                 for l in stdout.splitlines():
01232                     newref.write(l.rstrip() + '\n')
01233                 del newref # flush and close
01234             except IOError:
01235                 # Ignore IO errors when trying to update reference files
01236                 # because we may be in a read-only filesystem
01237                 pass
01238         
01239         # check standard error
01240         reference = self._expandReferenceFileName(self.error_reference)
01241         # call the validator if we have a file to use
01242         if reference and os.path.isfile(reference):
01243             result["GaudiTest.error_reference"] = reference
01244             newcauses = ReferenceFileValidator(reference,
01245                                                "standard error",
01246                                                "GaudiTest.error_diff",
01247                                                preproc = preproc)(stderr, result)
01248             causes += newcauses
01249             if newcauses: # Write a new reference file for stdedd
01250                 newref = open(reference + ".new","w")
01251                 # sanitize newlines
01252                 for l in stderr.splitlines():
01253                     newref.write(l.rstrip() + '\n')
01254                 del newref # flush and close    
01255         else:
01256             causes += BasicOutputValidator(self.stderr,
01257                                            "standard error",
01258                                            "ExecTest.expected_stderr")(stderr, result)
01259         
01260         return causes
01261         
01262     def ValidateOutput(self, stdout, stderr, result):
01263         causes = []
01264         # if the test definition contains a custom validator, use it
01265         if self.validator.strip() != "":
01266             class CallWrapper(object):
01267                 """
01268                 Small wrapper class to dynamically bind some default arguments
01269                 to a callable.
01270                 """
01271                 def __init__(self, callable, extra_args = {}):
01272                     self.callable = callable
01273                     self.extra_args = extra_args
01274                     # get the list of names of positional arguments
01275                     from inspect import getargspec
01276                     self.args_order = getargspec(callable)[0]
01277                     # Remove "self" from the list of positional arguments
01278                     # since it is added automatically 
01279                     if self.args_order[0] == "self":
01280                         del self.args_order[0]
01281                 def __call__(self, *args, **kwargs):
01282                     # Check which positional arguments are used
01283                     positional = self.args_order[:len(args)]
01284                     
01285                     kwargs = dict(kwargs) # copy the arguments dictionary
01286                     for a in self.extra_args:
01287                         # use "extra_args" for the arguments not specified as
01288                         # positional or keyword
01289                         if a not in positional and a not in kwargs:
01290                             kwargs[a] = self.extra_args[a]
01291                     return apply(self.callable, args, kwargs)
01292             # local names to be exposed in the script 
01293             exported_symbols = {"self":self,
01294                                 "stdout":stdout,
01295                                 "stderr":stderr,
01296                                 "result":result,
01297                                 "causes":causes,
01298                                 "findReferenceBlock":
01299                                     CallWrapper(findReferenceBlock, {"stdout":stdout,
01300                                                                      "result":result,
01301                                                                      "causes":causes}),
01302                                 "validateWithReference":
01303                                     CallWrapper(self.ValidateWithReference, {"stdout":stdout,
01304                                                                              "stderr":stderr,
01305                                                                              "result":result,
01306                                                                              "causes":causes}),
01307                                 "countErrorLines":
01308                                     CallWrapper(countErrorLines, {"stdout":stdout,
01309                                                                   "result":result,
01310                                                                   "causes":causes}),
01311                                 "checkTTreesSummaries":
01312                                     CallWrapper(self.CheckTTreesSummaries, {"stdout":stdout,
01313                                                                             "result":result,
01314                                                                             "causes":causes}),
01315                                 "checkHistosSummaries":
01316                                     CallWrapper(self.CheckHistosSummaries, {"stdout":stdout,
01317                                                                             "result":result,
01318                                                                             "causes":causes}),
01319                                 
01320                                 }
01321             exec self.validator in globals(), exported_symbols
01322         else:
01323             self.ValidateWithReference(stdout, stderr, result, causes)
01324         
01325         return causes
01326     
01327     def DumpEnvironment(self, result):
01328         """
01329         Add the content of the environment to the result object.
01330         
01331         Copied from the QMTest class of COOL.
01332         """
01333         vars = os.environ.keys()
01334         vars.sort()
01335         result['GaudiTest.environment'] = \
01336             result.Quote('\n'.join(["%s=%s"%(v,os.environ[v]) for v in vars]))
01337 
01338     def _find_program(self,prog):
01339         # check if it is an absolute path or the file can be found
01340         # from the local directory, otherwise search for it in PATH
01341         if not os.path.isabs(prog) and not os.path.isfile(prog):
01342             for d in os.environ["PATH"].split(os.pathsep):
01343                 p = os.path.join(d,prog)
01344                 if os.path.isfile(p):
01345                     return p
01346         return prog
01347         
01348     def Run(self, context, result):
01349         """Run the test.
01350 
01351         'context' -- A 'Context' giving run-time parameters to the
01352         test.
01353 
01354         'result' -- A 'Result' object.  The outcome will be
01355         'Result.PASS' when this method is called.  The 'result' may be
01356         modified by this method to indicate outcomes other than
01357         'Result.PASS' or to add annotations."""
01358         
01359         # Check if the platform is supported
01360         if self.PlatformIsNotSupported(context, result):
01361             return
01362         
01363         # Prepare program name and arguments (expanding variables, and converting to absolute) 
01364         if self.program:
01365             prog = rationalizepath(self.program)
01366         elif "GAUDIEXE" in os.environ:
01367             prog = os.environ["GAUDIEXE"]
01368         else:
01369             prog = "Gaudi.exe"
01370         self.program = prog
01371             
01372         dummy, prog_ext = os.path.splitext(prog)
01373         if prog_ext not in [ ".exe", ".py", ".bat" ] and self.GetPlatform()[0:3] == "win":
01374             prog += ".exe"
01375             prog_ext = ".exe"
01376             
01377         prog = self._find_program(prog)
01378         
01379         # Convert paths to absolute paths in arguments and reference files
01380         args = map(rationalizepath, self.args)
01381         self.reference = rationalizepath(self.reference)
01382         self.error_reference = rationalizepath(self.error_reference)
01383         
01384         
01385         # check if the user provided inline options
01386         tmpfile = None
01387         if self.options.strip():
01388             ext = ".opts"
01389             if re.search(r"from\s*Gaudi.Configuration\s*import\s*\*", self.options):
01390                 ext = ".py"
01391             tmpfile = TempFile(ext)
01392             tmpfile.writelines("\n".join(self.options.splitlines()))
01393             tmpfile.flush()
01394             args.append(tmpfile.name)
01395             result["GaudiTest.options"] = result.Quote(self.options)
01396         
01397         # if the program is a python file, execute it through python
01398         if prog_ext == ".py":
01399             args.insert(0,prog)
01400             if self.GetPlatform()[0:3] == "win":
01401                 prog = self._find_program("python.exe")
01402             else:
01403                 prog = self._find_program("python")
01404         
01405         # Change to the working directory if specified or to the default temporary
01406         origdir = os.getcwd()
01407         if self.workdir:
01408             os.chdir(str(os.path.normpath(os.path.expandvars(self.workdir))))
01409         elif self.use_temp_dir == "true":
01410             if "QMTEST_TMPDIR" in os.environ:
01411                 os.chdir(os.environ["QMTEST_TMPDIR"])
01412             elif "qmtest.tmpdir" in context: 
01413                 os.chdir(context["qmtest.tmpdir"])
01414         
01415         if "QMTEST_IGNORE_TIMEOUT" not in os.environ:
01416             self.timeout = max(self.timeout,600)
01417         else:
01418             self.timeout = -1
01419         
01420         try:
01421             # Generate eclipse.org debug launcher for the test 
01422             self._CreateEclipseLaunch(prog, args, destdir = origdir)
01423             # Run the test
01424             self.RunProgram(prog, 
01425                             [ prog ] + args,
01426                             context, result)
01427             # Record the content of the enfironment for failing tests 
01428             if result.GetOutcome() not in [ result.PASS ]:
01429                 self.DumpEnvironment(result)
01430         finally:
01431             # revert to the original directory
01432             os.chdir(origdir)
01433         
01434     def RunProgram(self, program, arguments, context, result):
01435         """Run the 'program'.
01436 
01437         'program' -- The path to the program to run.
01438 
01439         'arguments' -- A list of the arguments to the program.  This
01440         list must contain a first argument corresponding to 'argv[0]'.
01441 
01442         'context' -- A 'Context' giving run-time parameters to the
01443         test.
01444 
01445         'result' -- A 'Result' object.  The outcome will be
01446         'Result.PASS' when this method is called.  The 'result' may be
01447         modified by this method to indicate outcomes other than
01448         'Result.PASS' or to add annotations.
01449         
01450         @attention: This method has been copied from command.ExecTestBase
01451                     (QMTest 2.3.0) and modified to keep stdout and stderr
01452                     for tests that have been terminated by a signal.
01453                     (Fundamental for debugging in the Application Area)
01454         """
01455 
01456         # Construct the environment.
01457         environment = self.MakeEnvironment(context)
01458         # Create the executable.
01459         if self.timeout >= 0:
01460             timeout = self.timeout
01461         else:
01462             # If no timeout was specified, we sill run this process in a
01463             # separate process group and kill the entire process group
01464             # when the child is done executing.  That means that
01465             # orphaned child processes created by the test will be
01466             # cleaned up.
01467             timeout = -2
01468         e = GaudiFilterExecutable(self.stdin, timeout)
01469         # Run it.
01470         exit_status = e.Run(arguments, environment, path = program)
01471         # Get the stack trace from the temporary file (if present)
01472         if e.stack_trace_file and os.path.exists(e.stack_trace_file):
01473             stack_trace = open(e.stack_trace_file).read()
01474             os.remove(e.stack_trace_file)
01475         else:
01476             stack_trace = None
01477         if stack_trace:
01478             result["ExecTest.stack_trace"] = result.Quote(stack_trace)
01479 
01480         # If the process terminated normally, check the outputs.
01481         if sys.platform == "win32" or os.WIFEXITED(exit_status):
01482             # There are no causes of failure yet.
01483             causes = []
01484             # The target program terminated normally.  Extract the
01485             # exit code, if this test checks it.
01486             if self.exit_code is None:
01487                 exit_code = None
01488             elif sys.platform == "win32":
01489                 exit_code = exit_status
01490             else:
01491                 exit_code = os.WEXITSTATUS(exit_status)
01492             # Get the output generated by the program.
01493             stdout = e.stdout
01494             stderr = e.stderr
01495             # Record the results.
01496             result["ExecTest.exit_code"] = str(exit_code)
01497             result["ExecTest.stdout"] = result.Quote(stdout)
01498             result["ExecTest.stderr"] = result.Quote(stderr)
01499             # Check to see if the exit code matches.
01500             if exit_code != self.exit_code:
01501                 causes.append("exit_code")
01502                 result["ExecTest.expected_exit_code"] \
01503                     = str(self.exit_code)
01504             # Validate the output.
01505             causes += self.ValidateOutput(stdout, stderr, result)
01506             # If anything went wrong, the test failed.
01507             if causes:
01508                 result.Fail("Unexpected %s." % string.join(causes, ", ")) 
01509         elif os.WIFSIGNALED(exit_status):
01510             # The target program terminated with a signal.  Construe
01511             # that as a test failure.
01512             signal_number = str(os.WTERMSIG(exit_status))
01513             if not stack_trace:
01514                 result.Fail("Program terminated by signal.")
01515             else:
01516                 # The presence of stack_trace means tha we stopped the job because
01517                 # of a time-out
01518                 result.Fail("Exceeded time limit (%ds), terminated." % timeout)
01519             result["ExecTest.signal_number"] = signal_number
01520             result["ExecTest.stdout"] = result.Quote(e.stdout)
01521             result["ExecTest.stderr"] = result.Quote(e.stderr)
01522         elif os.WIFSTOPPED(exit_status):
01523             # The target program was stopped.  Construe that as a
01524             # test failure.
01525             signal_number = str(os.WSTOPSIG(exit_status))
01526             if not stack_trace:
01527                 result.Fail("Program stopped by signal.")
01528             else:
01529                 # The presence of stack_trace means tha we stopped the job because
01530                 # of a time-out
01531                 result.Fail("Exceeded time limit (%ds), stopped." % timeout)
01532             result["ExecTest.signal_number"] = signal_number
01533             result["ExecTest.stdout"] = result.Quote(e.stdout)
01534             result["ExecTest.stderr"] = result.Quote(e.stderr)
01535         else:
01536             # The target program terminated abnormally in some other
01537             # manner.  (This shouldn't normally happen...)
01538             result.Fail("Program did not terminate normally.")
01539         
01540         # Marco Cl.: This is a special trick to fix a "problem" with the output
01541         # of gaudi jobs when they use colors
01542         esc = '\x1b'
01543         repr_esc = '\\x1b'
01544         result["ExecTest.stdout"] = result["ExecTest.stdout"].replace(esc,repr_esc)
01545         # TODO: (MCl) improve the hack for colors in standard output
01546         #             may be converting them to HTML tags
01547 
01548     def _CreateEclipseLaunch(self, prog, args, destdir = None):
01549         # Find the project name used in ecplise.
01550         # The name is in a file called ".project" in one of the parent directories
01551         projbasedir = os.path.normpath(destdir)
01552         while not os.path.exists(os.path.join(projbasedir, ".project")):
01553             oldprojdir = projbasedir
01554             projbasedir = os.path.normpath(os.path.join(projbasedir, os.pardir))
01555             # FIXME: the root level is invariant when trying to go up one level,
01556             #        but it must be cheched on windows
01557             if oldprojdir == projbasedir:
01558                 # If we cannot find a .project, so no point in creating a .launch file
01559                 return
01560         # Use ElementTree to parse the XML file 
01561         from xml.etree import ElementTree as ET
01562         t = ET.parse(os.path.join(projbasedir, ".project"))
01563         projectName = t.find("name").text
01564 
01565         # prepare the name/path of the generated file
01566         destfile = "%s.launch" % self._Runnable__id
01567         if destdir:
01568             destfile = os.path.join(destdir, destfile)
01569         
01570         if self.options.strip():
01571             # this means we have some custom options in the qmt file, so we have
01572             # to copy them from the temporary file at the end of the arguments
01573             # in another file
01574             tempfile = args.pop()
01575             optsfile = destfile + os.path.splitext(tempfile)[1]
01576             shutil.copyfile(tempfile, optsfile)
01577             args.append(optsfile)
01578         
01579         # prepare the data to insert in the XML file
01580         from xml.sax.saxutils import quoteattr # useful to quote XML special chars
01581         data = {}
01582         # Note: the "quoteattr(k)" is not needed because special chars cannot be part of a variable name,
01583         # but it doesn't harm.
01584         data["environment"] = "\n".join(['<mapEntry key=%s value=%s/>' % (quoteattr(k), quoteattr(v))
01585                                          for k, v in os.environ.iteritems()])
01586         
01587         data["exec"] = which(prog)
01588         
01589         data["args"] = "&#10;".join(map(rationalizepath, args))
01590         
01591         if not self.use_temp_dir:
01592             data["workdir"] = os.getcwd()
01593         else:
01594             # If the test is using a tmporary directory, it is better to run it
01595             # in the same directory as the .launch file when debugged in eclipse
01596             data["workdir"] = destdir
01597         
01598         data["project"] = projectName.strip()
01599         
01600         # Template for the XML file, based on eclipse 3.4 
01601         xml = """<?xml version="1.0" encoding="UTF-8" standalone="no"?>
01602 <launchConfiguration type="org.eclipse.cdt.launch.applicationLaunchType">
01603 <booleanAttribute key="org.eclipse.cdt.debug.mi.core.AUTO_SOLIB" value="true"/>
01604 <listAttribute key="org.eclipse.cdt.debug.mi.core.AUTO_SOLIB_LIST"/>
01605 <stringAttribute key="org.eclipse.cdt.debug.mi.core.DEBUG_NAME" value="gdb"/>
01606 <stringAttribute key="org.eclipse.cdt.debug.mi.core.GDB_INIT" value=".gdbinit"/>
01607 <listAttribute key="org.eclipse.cdt.debug.mi.core.SOLIB_PATH"/>
01608 <booleanAttribute key="org.eclipse.cdt.debug.mi.core.STOP_ON_SOLIB_EVENTS" value="false"/>
01609 <stringAttribute key="org.eclipse.cdt.debug.mi.core.protocol" value="mi"/>
01610 <stringAttribute key="org.eclipse.cdt.launch.COREFILE_PATH" value=""/>
01611 <stringAttribute key="org.eclipse.cdt.launch.DEBUGGER_ID" value="org.eclipse.cdt.debug.mi.core.CDebugger"/>
01612 <stringAttribute key="org.eclipse.cdt.launch.DEBUGGER_REGISTER_GROUPS" value=""/>
01613 <stringAttribute key="org.eclipse.cdt.launch.DEBUGGER_START_MODE" value="run"/>
01614 <booleanAttribute key="org.eclipse.cdt.launch.DEBUGGER_STOP_AT_MAIN" value="true"/>
01615 <stringAttribute key="org.eclipse.cdt.launch.DEBUGGER_STOP_AT_MAIN_SYMBOL" value="main"/>
01616 <booleanAttribute key="org.eclipse.cdt.launch.ENABLE_REGISTER_BOOKKEEPING" value="false"/>
01617 <booleanAttribute key="org.eclipse.cdt.launch.ENABLE_VARIABLE_BOOKKEEPING" value="false"/>
01618 <stringAttribute key="org.eclipse.cdt.launch.FORMAT" value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&lt;contentList/&gt;"/>
01619 <stringAttribute key="org.eclipse.cdt.launch.GLOBAL_VARIABLES" value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;globalVariableList/&gt;&#10;"/>
01620 <stringAttribute key="org.eclipse.cdt.launch.MEMORY_BLOCKS" value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;memoryBlockExpressionList/&gt;&#10;"/>
01621 <stringAttribute key="org.eclipse.cdt.launch.PROGRAM_ARGUMENTS" value="%(args)s"/>
01622 <stringAttribute key="org.eclipse.cdt.launch.PROGRAM_NAME" value="%(exec)s"/>
01623 <stringAttribute key="org.eclipse.cdt.launch.PROJECT_ATTR" value="%(project)s"/>
01624 <stringAttribute key="org.eclipse.cdt.launch.PROJECT_BUILD_CONFIG_ID_ATTR" value=""/>
01625 <stringAttribute key="org.eclipse.cdt.launch.WORKING_DIRECTORY" value="%(workdir)s"/>
01626 <booleanAttribute key="org.eclipse.cdt.launch.use_terminal" value="true"/>
01627 <listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_PATHS">
01628 <listEntry value="/%(project)s"/>
01629 </listAttribute>
01630 <listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_TYPES">
01631 <listEntry value="4"/>
01632 </listAttribute>
01633 <booleanAttribute key="org.eclipse.debug.core.appendEnvironmentVariables" value="false"/>
01634 <mapAttribute key="org.eclipse.debug.core.environmentVariables">
01635 %(environment)s
01636 </mapAttribute>
01637 <mapAttribute key="org.eclipse.debug.core.preferred_launchers">
01638 <mapEntry key="[debug]" value="org.eclipse.cdt.cdi.launch.localCLaunch"/>
01639 </mapAttribute>
01640 <listAttribute key="org.eclipse.debug.ui.favoriteGroups">
01641 <listEntry value="org.eclipse.debug.ui.launchGroup.debug"/>
01642 </listAttribute>
01643 </launchConfiguration>
01644 """ % data
01645 
01646         # Write the output file
01647         open(destfile, "w").write(xml)
01648         #open(destfile + "_copy.xml", "w").write(xml)

Generated at Fri Jan 22 20:27:58 2010 for Gaudi Framework, version v21r7 by Doxygen version 1.5.6 written by Dimitri van Heesch, © 1997-2004