| # Gaudi Framework, version v21r7p1 |

Home |  | Generated: 15 Feb 2010

# GaudiTest.py

Go to the documentation of this file.

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