Gaudi Framework, version v23r3

Home   Generated: Thu Jun 28 2012

GaudiTest.py

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

Generated at Thu Jun 28 2012 12:30:00 for Gaudi Framework, version v23r3 by Doxygen version 1.7.2 written by Dimitri van Heesch, © 1997-2004