Gaudi Framework, version v23r4

Home   Generated: Mon Sep 17 2012

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

Generated at Mon Sep 17 2012 13:49:35 for Gaudi Framework, version v23r4 by Doxygen version 1.7.2 written by Dimitri van Heesch, © 1997-2004