Gaudi Framework, version v22r4

Home   Generated: Fri Sep 2 2011

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

Generated at Fri Sep 2 2011 16:24:47 for Gaudi Framework, version v22r4 by Doxygen version 1.7.2 written by Dimitri van Heesch, © 1997-2004