| # 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)