Gaudi Framework, version v21r6

Home   Generated: 11 Nov 2009

GaudiTest.py

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

Generated at Wed Nov 11 16:23:08 2009 for Gaudi Framework, version v21r6 by Doxygen version 1.5.6 written by Dimitri van Heesch, © 1997-2004