| # Gaudi Framework, version v20r4
Generated: 8 Jan 2009

# GaudiTest.py

Go to the documentation of this file.

    00001 ########################################################################
    00002 # File:   CoolTestExtensions.py
    00003 # Author: Marco Clemencic CERN/PH-LBC
    00004 # Date:   16/10/2007
    00005 #
    00006 # Contents:
    00007 #   TemporaryEnvironment: class to change the environment and revert to
    00008 #                         the original one
    00009 #
    00010 #   DBPreparer: handle the details for temporary test databases
    00011 #
    00012 #   StandardTest: extension to ShellCommandTest that finds Python
    00013 #                 scripts in the PATH ad run them through the
    00014 #                 interpreter
    00015 #
    00016 #   DatabaseTest: run a test that needs a database
    00017 #
    00018 #   SourceTargetTest: run a test that needs 2 databases
    00019 #                     (source and target)
    00020 #
    00021 # Copyright (c) 2003 by CodeSourcery, LLC.  All rights reserved.
    00022 #
    00023 ########################################################################
    00024 __author__  = 'Marco Clemencic CERN/PH-LBC'
    00025 __version__ = "$Revision: 1.52 $"
    00026 __tag__     = "$Name:  $"
    00027 ########################################################################
    00028 # Imports
    00029 ########################################################################
    00030 import os
    00031 import sys
    00032 import re
    00033 import tempfile
    00034 import shutil
    00035 import string
    00036 import difflib
    00037 from subprocess import Popen, PIPE, STDOUT
    00038
    00039 import qm
    00040 from qm.test.classes.command import ExecTestBase
    00041
    00042 ########################################################################
    00043 # Utility Classes
    00044 ########################################################################
    00045 class TemporaryEnvironment:
    00046     """
    00047     Class to changes the environment temporarily.
    00048     """
    00049     def __init__(self, orig = os.environ, keep_same = False):
    00050         """
    00051         Create a temporary environment on top of the one specified
    00052         (it can be another TemporaryEnvironment instance).
    00053         """
    00054         #print "New environment"
    00055         self.old_values = {}
    00056         self.env = orig
    00057         self._keep_same = keep_same
    00058
    00059     def __setitem__(self,key,value):
    00060         """
    00061         Set an environment variable recording the previous value.
    00062         """
    00063         if key not in self.old_values :
    00064             if key in self.env :
    00065                 if not self._keep_same or self.env[key] != value:
    00066                     self.old_values[key] = self.env[key]
    00067             else:
    00068                 self.old_values[key] = None
    00069         self.env[key] = value
    00070
    00071     def __getitem__(self,key):
    00072         """
    00073         Get an environment variable.
    00074         Needed to provide the same interface as os.environ.
    00075         """
    00076         return self.env[key]
    00077
    00078     def __delitem__(self,key):
    00079         """
    00080         Unset an environment variable.
    00081         Needed to provide the same interface as os.environ.
    00082         """
    00083         if key not in self.env :
    00084             raise KeyError(key)
    00085         self.old_values[key] = self.env[key]
    00086         del self.env[key]
    00087
    00088     def keys(self):
    00089         """
    00090         Return the list of defined environment variables.
    00091         Needed to provide the same interface as os.environ.
    00092         """
    00093         return self.env.keys()
    00094
    00095     def items(self):
    00096         """
    00097         Return the list of (name,value) pairs for the defined environment variables.
    00098         Needed to provide the same interface as os.environ.
    00099         """
    00100         return self.env.items()
    00101
    00102     def __contains__(self,key):
    00103         """
    00104         Operator 'in'.
    00105         Needed to provide the same interface as os.environ.
    00106         """
    00107         return key in self.env
    00108
    00109     def restore(self):
    00110         """
    00111         Revert all the changes done to the orignal environment.
    00112         """
    00113         for key,value in self.old_values.items():
    00114             if value is None:
    00115                 del self.env[key]
    00116             else:
    00117                 self.env[key] = value
    00118         self.old_values = {}
    00119
    00120     def __del__(self):
    00121         """
    00122         Revert the changes on destruction.
    00123         """
    00124         #print "Restoring the environment"
    00125         self.restore()
    00126
    00127     def gen_script(self,shell_type):
    00128         """
    00129         Generate a shell script to reproduce the changes in the environment.
    00130         """
    00131         shells = [ 'csh', 'sh', 'bat' ]
    00132         if shell_type not in shells:
    00133             raise RuntimeError("Shell type '%s' unknown. Available: %s"%(shell_type,shells))
    00134         out = ""
    00135         for key,value in self.old_values.items():
    00136             if key not in self.env:
    00137                 # unset variable
    00138                 if shell_type == 'csh':
    00139                     out += 'unsetenv %s\n'%key
    00140                 elif shell_type == 'sh':
    00141                     out += 'unset %s\n'%key
    00142                 elif shell_type == 'bat':
    00143                     out += 'set %s=\n'%key
    00144             else:
    00145                 # set variable
    00146                 if shell_type == 'csh':
    00147                     out += 'setenv %s "%s"\n'%(key,self.env[key])
    00148                 elif shell_type == 'sh':
    00149                     out += 'export %s="%s"\n'%(key,self.env[key])
    00150                 elif shell_type == 'bat':
    00151                     out += 'set %s=%s\n'%(key,self.env[key])
    00152         return out
    00153
    00154 class TempDir:
    00155     """Small class for temporary directories.
    00156     When instantiated, it creates a temporary directory and the instance
    00157     behaves as the string containing the directory name.
    00158     When the instance goes out of scope, it removes all the content of
    00159     the temporary directory (automatic clean-up).
    00160     """
    00161     def __init__(self, keep = False, chdir = False):
    00162         self.name = tempfile.mkdtemp()
    00163         self._keep = keep
    00164         self._origdir = None
    00165         if chdir:
    00166             self._origdir = os.getcwd()
    00167             os.chdir(self.name)
    00168
    00169     def __str__(self):
    00170         return self.name
    00171
    00172     def __del__(self):
    00173         if self._origdir:
    00174             os.chdir(self._origdir)
    00175         if self.name and not self._keep:
    00176             shutil.rmtree(self.name)
    00177
    00178     def __getattr__(self,attr):
    00179         return getattr(self.name,attr)
    00180
    00181 class TempFile:
    00182     """Small class for temporary files.
    00183     When instantiated, it creates a temporary directory and the instance
    00184     behaves as the string containing the directory name.
    00185     When the instance goes out of scope, it removes all the content of
    00186     the temporary directory (automatic clean-up).
    00187     """
    00188     def __init__(self, suffix='', prefix='tmp', dir=None, text=False, keep = False):
    00189         self.file = None
    00190         self.name = None
    00191         self._keep = keep
    00192
    00193         self._fd, self.name = tempfile.mkstemp(suffix,prefix,dir,text)
    00194         self.file = os.fdopen(self._fd,"r+")
    00195
    00196     def __str__(self):
    00197         return self.name
    00198
    00199     def __del__(self):
    00200         if self.file:
    00201             self.file.close()
    00202         if self.name and not self._keep:
    00203             os.remove(self.name)
    00204
    00205     def __getattr__(self,attr):
    00206         return getattr(self.file,attr)
    00207
    00208 class CMT:
    00209     """Small wrapper to call CMT.
    00210     """
    00211     def __init__(self,path=None):
    00212         if path is None:
    00213             path = os.getcwd()
    00214         self.path = path
    00215
    00216     def _run_cmt(self,command,args):
    00217         # prepare command line
    00218         if type(args) is str:
    00219             args = [args]
    00220         cmd = "cmt %s"%command
    00221         for arg in args:
    00222             cmd += ' "%s"'%arg
    00223
    00224         # go to the execution directory
    00225         olddir = os.getcwd()
    00226         os.chdir(self.path)
    00227         # run cmt
    00228         result = os.popen4(cmd)[1].read()
    00229         # return to the old directory
    00230         os.chdir(olddir)
    00231         return result
    00232
    00233     def __getattr__(self,attr):
    00234         return lambda args=[]: self._run_cmt(attr, args)
    00235
    00236     def runtime_env(self,env = None):
    00237         """Returns a dictionary containing the runtime environment produced by CMT.
    00238         If a dictionary is passed a modified instance of it is returned.
    00239         """
    00240         if env is None:
    00241             env = {}
    00242         for l in self.setup("-csh").splitlines():
    00243             l = l.strip()
    00244             if l.startswith("setenv"):
    00245                 dummy,name,value = l.split(None,3)
    00246                 env[name] = value.strip('"')
    00247             elif l.startswith("unsetenv"):
    00248                 dummy,name = l.split(None,2)
    00249                 if name in env:
    00250                     del env[name]
    00251         return env
    00252     def show_macro(self,k):
    00253         r = self.show(["macro",k])
    00254         if r.find("CMT> Error: symbol not found") >= 0:
    00255             return None
    00256         else:
    00257             return self.show(["macro_value",k]).strip()
    00258
    00259 ########################################################################
    00260 # Output Validation Classes
    00261 ########################################################################
    00262 class BasicOutputValidator:
    00263     """Basic implementation of an option validator for Gaudi tests.
    00264     This implementation is based on the standard (LCG) validation functions
    00265     used in QMTest.
    00266     """
    00267     def __init__(self,ref,cause,result_key):
    00268         self.reference = ref
    00269         self.cause = cause
    00270         self.result_key = result_key
    00271
    00272     def __call__(self, out, result):
    00273         """Validate the output of the program.
    00274
    00275         'stdout' -- A string containing the data written to the standard output
    00276         stream.
    00277
    00278         'stderr' -- A string containing the data written to the standard error
    00279         stream.
    00280
    00281         'result' -- A 'Result' object. It may be used to annotate
    00282         the outcome according to the content of stderr.
    00283
    00284         returns -- A list of strings giving causes of failure."""
    00285
    00286         causes = []
    00287         # Check to see if theoutput matches.
    00288         if not self.__CompareText(out, self.reference):
    00289             causes.append(self.cause)
    00290             result[self.result_key] = result.Quote(self.reference)
    00291
    00292         return causes
    00293
    00294     def __CompareText(self, s1, s2):
    00295         """Compare 's1' and 's2', ignoring line endings.
    00296
    00297         's1' -- A string.
    00298
    00299         's2' -- A string.
    00300
    00301         returns -- True if 's1' and 's2' are the same, ignoring
    00302         differences in line endings."""
    00303
    00304         # The "splitlines" method works independently of the line ending
    00305         # convention in use.
    00306         return s1.splitlines() == s2.splitlines()
    00307
    00308 class FilePreprocessor:
    00309     """ Base class for a callable that takes a file and returns a modified
    00310     version of it."""
    00311     def __processLine__(self, line):
    00312         return line
    00313     def __call__(self, input):
    00314         if hasattr(input,"__iter__"):
    00315             lines = input
    00316             mergeback = False
    00317         else:
    00318             lines = input.splitlines()
    00319             mergeback = True
    00320         output = []
    00321         for l in lines:
    00322             l = self.__processLine__(l)
    00323             if l: output.append(l)
    00324         if mergeback: output = '\n'.join(output)
    00325         return output
    00326     def __add__(self, rhs):
    00327         return FilePreprocessorSequence([self,rhs])
    00328
    00329 class FilePreprocessorSequence(FilePreprocessor):
    00330     def __init__(self, members = []):
    00331         self.members = members
    00332     def __add__(self, rhs):
    00333         return FilePreprocessorSequence(self.members + [rhs])
    00334     def __call__(self, input):
    00335         output = input
    00336         for pp in self.members:
    00337             output = pp(output)
    00338         return output
    00339
    00340 class LineSkipper(FilePreprocessor):
    00341     def __init__(self, strings = [], regexps = []):
    00342         import re
    00343         self.strings = strings
    00344         self.regexps = map(re.compile,regexps)
    00345
    00346     def __processLine__(self, line):
    00347         for s in self.strings:
    00348             if line.find(s) >= 0: return None
    00349         for r in self.regexps:
    00350             if r.search(line): return None
    00351         return line
    00352
    00353 class RegexpReplacer(FilePreprocessor):
    00354     def __init__(self, orig, repl = "", when = None):
    00355         if when:
    00356             when = re.compile(when)
    00357         self._operations = [ (when, re.compile(orig), repl) ]
    00358     def __add__(self,rhs):
    00359         if isinstance(rhs, RegexpReplacer):
    00360             res = RegexpReplacer("","",None)
    00361             res._operations = self._operations + rhs._operations
    00362         else:
    00363             res = FilePreprocessor.__add__(self, rhs)
    00364         return res
    00365     def __processLine__(self, line):
    00366         for w,o,r in self._operations:
    00367             if w is None or w.search(line):
    00368                 line = o.sub(r, line)
    00369         return line
    00370
    00371 # Common preprocessors
    00372 maskPointers  = RegexpReplacer("0x[0-9a-fA-F]{4,16}","0x########")
    00373 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)?",
    00374                                "00:00:00 1970-01-01")
    00375 normalizeEOL = FilePreprocessor()
    00376 normalizeEOL.__processLine__ = lambda line: str(line).rstrip() + '\n'
    00377
    00378 skipEmptyLines = FilePreprocessor()
    00379 # FIXME: that's ugly
    00380 skipEmptyLines.__processLine__ = lambda line: (line.strip() and line) or None
    00381
    00382 # Preprocessors for GaudiExamples
    00383 normalizeExamples = maskPointers + normalizeDate
    00384 for w,o,r in [
    00385               #("TIMER.TIMER",r"[0-9]", "0"), # Normalize time output
    00386               ("TIMER.TIMER",r"\s+[+-]?[0-9]+[0-9.]*", " 0"), # Normalize time output
    00387               ("release all pending",r"^.*/([^/]*:.*)",r"\1"),
    00388               ("0x########",r"\[.*/([^/]*.*)\]",r"[\1]"),
    00389               ("^#.*file",r"file '.*[/\\]([^/\\]*)$",r"file '\1"),
    00390               ("^JobOptionsSvc.*options successfully read in from",r"read in from .*[/\\]([^/\\]*)$",r"file \1"), # normalize path to options
    00391               # Normalize UUID, except those ending with all 0s (i.e. the class IDs)
    00392               (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")
    00393               ]: #[ ("TIMER.TIMER","[0-9]+[0-9.]*", "") ]
    00394     normalizeExamples += RegexpReplacer(o,r,w)
    00395 normalizeExamples = LineSkipper(["//GP:",
    00396                                  "Time User",
    00397                                  "Welcome to",
    00398                                  "This machine has a speed",
    00399                                  "TIME:",
    00400                                  "running on",
    00401                                  "ToolSvc.Sequenc...   INFO",
    00402                                  "DataListenerSvc      INFO XML written to file:",
    00403                                  "[INFO]","[WARNING]",
    00404                                  "DEBUG No writable file catalog found which contains FID:",
    00405                                  "0 local", # hack for ErrorLogExample
    00406                                  # This comes from ROOT, when using GaudiPython
    00407                                  'Note: (file "(tmpfile)", line 2) File "set" already loaded',
    00408                                  ],regexps = [
    00409                                  r"^#", # Ignore python comments
    00410                                  r"(Always|SUCCESS)\s*(Root f|[^ ]* F)ile version:", # skip the message reporting the version of the root file
    00411                                  r"0x[0-9a-fA-F#]+ *Algorithm::sysInitialize\(\) *\[", # hack for ErrorLogExample
    00412                                  r"0x[0-9a-fA-F#]* *__gxx_personality_v0 *\[", # hack for ErrorLogExample
    00413                                  r"File '.*.xml' does not exist",
    00414                                  r"INFO Refer to dataset .* by its file ID:",
    00415                                  r"INFO Referring to dataset .* by its file ID:",
    00416                                  r"INFO Disconnect from dataset",
    00417                                  r"INFO Disconnected from dataset",
    00418                                  r"INFO Disconnected data IO:",
    00419                                  r"IncidentSvc\s*(DEBUG (Adding|Removing)|VERBOSE Calling)",
    00420                                  # I want to ignore the header of the unchecked StatusCode report
    00421                                  r"^StatusCodeSvc.*listing all unchecked return codes:",
    00422                                  r"^StatusCodeSvc\s*INFO\s*$",
    00423                                  r"Num\s*|\s*Function\s*|\s*Source Library",
    00424                                  r"^[-+]*\s*$",
    00425                                  # Hide the fake error message coming from POOL/ROOT (ROOT 5.21)
    00426                                  r"ERROR Failed to modify file: .* Errno=2 No such file or directory",
    00427                                  # Hide unckeched StatusCodes  from dictionaries
    00428                                  r"^ +[0-9]+ \|.*ROOT",
    00429                                  r"^ +[0-9]+ \|.*\|.*Dict",
    00430                                  # Remove ROOT TTree summary table, which changes from one version to the other
    00431                                  r"^\*.*\*$",
    00432                                  # Remove Histos Summaries
    00433                                  r"SUCCESS\s*Booked \d+ Histogram\(s\)",
    00434                                  r"^ \|",
    00435                                  r"^ ID=",
    00436                                  ] ) + normalizeExamples + skipEmptyLines + \
    00437                                   normalizeEOL
    00438
    00439 class ReferenceFileValidator:
    00440     def __init__(self, reffile, cause, result_key, preproc = normalizeExamples):
    00441         self.reffile = os.path.expandvars(reffile)
    00442         self.cause = cause
    00443         self.result_key = result_key
    00444         self.preproc = preproc
    00445     def __call__(self, stdout, result):
    00446         causes = []
    00447         if os.path.isfile(self.reffile):
    00448             orig = open(self.reffile).xreadlines()
    00449             if self.preproc:
    00450                 orig = self.preproc(orig)
    00451         else:
    00452             orig = []
    00453
    00454         new = stdout.splitlines()
    00455         if self.preproc:
    00456             new = self.preproc(new)
    00457         #open(self.reffile + ".test","w").writelines(new)
    00458         diffs = difflib.ndiff(orig,new,charjunk=difflib.IS_CHARACTER_JUNK)
    00459         filterdiffs = map(lambda x: x.strip(),filter(lambda x: x[0] != " ",diffs))
    00460         #filterdiffs = [x.strip() for x in diffs]
    00461         if filterdiffs:
    00462             result[self.result_key] = result.Quote("\n".join(filterdiffs))
    00463             result[self.result_key] += result.Quote("""
    00464 Legend:
    00465         -) reference file
    00466         +) standard output of the test""")
    00467             causes.append(self.cause)
    00468
    00469         return causes
    00470
    00471 ########################################################################
    00472 # Useful validation functions
    00473 ########################################################################
    00474 def findReferenceBlock(reference, stdout, result, causes, signature_offset=0, signature=None,
    00475                        id = None):
    00476     """
    00477     Given a block of text, tries to find it in the output.
    00478     The block had to be identified by a signature line. By default, the first
    00479     line is used as signature, or the line pointed to by signature_offset. If
    00480     signature_offset points outside the block, a signature line can be passed as
    00481     signature argument. Note: if 'signature' is None (the default), a negative
    00482     signature_offset is interpreted as index in a list (e.g. -1 means the last
    00483     line), otherwise the it is interpreted as the number of lines before the
    00484     first one of the block the signature must appear.
    00485     The parameter 'id' allow to distinguish between different calls to this
    00486     function in the same validation code.
    00487     """
    00488     # split reference file, sanitize EOLs and remove empty lines
    00489     reflines = filter(None,map(lambda s: s.rstrip(), reference.splitlines()))
    00490     if not reflines:
    00491         raise RuntimeError("Empty (or null) reference")
    00492     # the same on standard output
    00493     outlines = filter(None,map(lambda s: s.rstrip(), stdout.splitlines()))
    00494
    00495     res_field = "GaudiTest.RefBlock"
    00496     if id:
    00497         res_field += "_%s" % id
    00498
    00499     if signature is None:
    00500         if signature_offset < 0:
    00501             signature_offset = len(reference)+signature_offset
    00502         signature = reflines[signature_offset]
    00503     # find the reference block in the output file
    00504     try:
    00505         pos = outlines.index(signature)
    00506         outlines = outlines[pos-signature_offset:pos+len(reflines)-signature_offset]
    00507         if reflines != outlines:
    00508             msg = "standard output"
    00509             # I do not want 2 messages in causes if teh function is called twice
    00510             if not msg in causes:
    00511                 causes.append(msg)
    00512             result[res_field + ".observed"] = result.Quote("\n".join(outlines))
    00513     except ValueError:
    00514         causes.append("missing signature")
    00515     result[res_field + ".signature"] = result.Quote(signature)
    00516     if len(reflines) > 1 or signature != reflines[0]:
    00517         result[res_field + ".expected"] = result.Quote("\n".join(reflines))
    00518
    00519     return causes
    00520
    00521 def countErrorLines(expected = {'ERROR':0, 'FATAL':0}, **kwargs):
    00522     """
    00523     Count the number of messages with required severity (by default ERROR and FATAL)
    00524     and check if their numbers match the expected ones (0 by default).
    00525     The dictionary "expected" can be used to tune the number of errors and fatals
    00526     allowed, or to limit the number of expected warnings etc.
    00527     """
    00528     stdout = kwargs["stdout"]
    00529     result = kwargs["result"]
    00530     causes = kwargs["causes"]
    00531
    00532     # prepare the dictionary to record the extracted lines
    00533     errors = {}
    00534     for sev in expected:
    00535         errors[sev] = []
    00536
    00537     outlines = stdout.splitlines()
    00538     from math import log10
    00539     fmt = "%%%dd - %%s" % (int(log10(len(outlines))+1))
    00540
    00541     linecount = 0
    00542     for l in outlines:
    00543         linecount += 1
    00544         words = l.split()
    00545         if len(words) >= 2 and words[1] in errors:
    00546             errors[words[1]].append(fmt%(linecount,l.rstrip()))
    00547
    00548     for e in errors:
    00549         if len(errors[e]) != expected[e]:
    00550             causes.append('%s(%d)'%(e,len(errors[e])))
    00551             result["GaudiTest.lines.%s"%e] = result.Quote('\n'.join(errors[e]))
    00552             result["GaudiTest.lines.%s.expected#"%e] = result.Quote(str(expected[e]))
    00553
    00554     return causes
    00555
    00556
    00557 def _parseTTreeSummary(lines, pos):
    00558     """
    00559     Parse the TTree summary table in lines, starting from pos.
    00560     Returns a tuple with the dictionary with the digested informations and the
    00561     position of the first line after the summary.
    00562     """
    00563     result = {}
    00564     i = pos + 1 # first line is a sequence of '*'
    00565     count = len(lines)
    00566
    00567     splitcols = lambda l: [ f.strip() for f in l.strip("*\n").split(':',2) ]
    00568     def parseblock(ll):
    00569         r = {}
    00570         cols = splitcols(ll[0])
    00571         r["Name"], r["Title"] = cols[1:]
    00572
    00573         cols = splitcols(ll[1])
    00574         r["Entries"] = int(cols[1])
    00575
    00576         sizes = cols[2].split()
    00577         r["Total size"] = int(sizes[2])
    00578         if sizes[-1] == "memory":
    00579             r["File size"] = 0
    00580         else:
    00581             r["File size"] = int(sizes[-1])
    00582
    00583         cols = splitcols(ll[2])
    00584         sizes = cols[2].split()
    00585         if cols[0] == "Baskets":
    00586             r["Baskets"] = int(cols[1])
    00587             r["Basket size"] = int(sizes[2])
    00588         r["Compression"] = float(sizes[-1])
    00589         return r
    00590
    00591     if i < (count - 3) and lines[i].startswith("*Tree"):
    00592         result = parseblock(lines[i:i+3])
    00593         result["Branches"] = {}
    00594         i += 4
    00595         while i < (count - 3) and lines[i].startswith("*Br"):
    00596             branch = parseblock(lines[i:i+3])
    00597             result["Branches"][branch["Name"]] = branch
    00598             i += 4
    00599
    00600     return (result, i)
    00601
    00602 def findTTreeSummaries(stdout):
    00603     """
    00604     Scan stdout to find ROOT TTree summaries and digest them.
    00605     """
    00606     stars = re.compile(r"^\*+$")
    00607     outlines = stdout.splitlines()
    00608     nlines = len(outlines)
    00609     trees = {}
    00610
    00611     i = 0
    00612     while i < nlines: #loop over the output
    00613         # look for
    00614         while i < nlines and not stars.match(outlines[i]):
    00615             i += 1
    00616         if i < nlines:
    00617             tree, i = _parseTTreeSummary(outlines, i)
    00618             if tree:
    00619                 trees[tree["Name"]] = tree
    00620
    00621     return trees
    00622
    00623 def cmpTreesDicts(reference, to_check, ignore = None):
    00624     """
    00625     Check that all the keys in reference are in to_check too, with the same value.
    00626     If the value is a dict, the function is called recursively. to_check can
    00627     contain more keys than reference, that will not be tested.
    00628     The function returns at the first difference found.
    00629     """
    00630     fail_keys = []
    00631     # filter the keys in the reference dictionary
    00632     if ignore:
    00633         ignore_re = re.compile(ignore)
    00634         keys = [ key for key in reference if not ignore_re.match(key) ]
    00635     else:
    00636         keys = reference.keys()
    00637     # loop over the keys (not ignored) in the reference dictionary
    00638     for k in keys:
    00639         if k in to_check: # the key must be in the dictionary to_check
    00640             if (type(reference[k]) is dict) and (type(to_check[k]) is dict):
    00641                 # if both reference and to_check values are dictionaries, recurse
    00642                 failed = fail_keys = cmpTreesDicts(reference[k], to_check[k], ignore)
    00643             else:
    00644                 # compare the two values
    00645                 failed = to_check[k] != reference[k]
    00646         else: # handle missing keys in the dictionary to check (i.e. failure)
    00647             to_check[k] = None
    00648             failed = True
    00649         if failed:
    00650             fail_keys.insert(0, k)
    00651             break # exit from the loop at the first failure
    00652     return fail_keys # return the list of keys bringing to the different values
    00653
    00654 def getCmpFailingValues(reference, to_check, fail_path):
    00655     c = to_check
    00656     r = reference
    00657     for k in fail_path:
    00658         c = c.get(k,None)
    00659         r = r.get(k,None)
    00660         if c is None or r is None:
    00661             break # one of the dictionaries is not deep enough
    00662     return (fail_path, r, c)
    00663
    00664 # signature of the print-out of the histograms
    00665 h_count_re = re.compile(r"SUCCESS\s+Booked (\d+) Histogram\(s\) :\s+(.*)")
    00666
    00667 def parseHistosSummary(lines, pos):
    00668     """
    00669     Extract the histograms infos from the lines starting at pos.
    00670     Returns the position of the first line after the summary block.
    00671     """
    00672     global h_count_re
    00673     h_table_head = re.compile(r'SUCCESS\s+List of booked (1D|2D|3D|1D profile|2D profile) histograms in directory\s+"(\w*)"')
    00674     h_short_summ = re.compile(r"ID=([^\"]+)\s+\"([^\"]+)\"\s+(.*)")
    00675
    00676     nlines = len(lines)
    00677
    00678     # decode header
    00679     m = h_count_re.search(lines[pos])
    00680     total = int(m.group(1))
    00681     partials = {}
    00682     for k, v in [ x.split("=") for x in  m.group(2).split() ]:
    00683         partials[k] = int(v)
    00684     pos += 1
    00685
    00686     summ = {}
    00687     while pos < nlines:
    00688         m = h_table_head.search(lines[pos])
    00689         if m:
    00690             t, d = m.groups(1) # type and directory
    00691             t = t.replace(" profile", "Prof")
    00692             pos += 1
    00693             if pos < nlines:
    00694                 l = lines[pos]
    00695             else:
    00696                 l = ""
    00697             cont = {}
    00698             if l.startswith(" | ID"):
    00699                 # table format
    00700                 titles = [ x.strip() for x in l.split("|")][1:]
    00701                 pos += 1
    00702                 while pos < nlines and lines[pos].startswith(" |"):
    00703                     l = lines[pos]
    00704                     values = [ x.strip() for x in l.split("|")][1:]
    00705                     hcont = {}
    00706                     for i in range(len(titles)):
    00707                         hcont[titles[i]] = values[i]
    00708                     cont[hcont["ID"]] = hcont
    00709                     pos += 1
    00710             elif l.startswith(" ID="):
    00711                 while pos < nlines and lines[pos].startswith(" ID="):
    00712                     values = [ x.strip() for x in  h_short_summ.search(lines[pos]).groups() ]
    00713                     cont[values[0]] = values
    00714                     pos += 1
    00715             else: # not interpreted
    00716                 raise RuntimeError("Cannot understand line %d: '%s'" % (pos, l))
    00717             if not d in summ:
    00718                 summ[d] = {}
    00719             summ[d][t] = cont
    00720             summ[d]["header"] = partials
    00721             summ[d]["header"]["Total"] = total
    00722         else:
    00723             break
    00724     return summ, pos
    00725
    00726 def findHistosSummaries(stdout):
    00727     """
    00728     Scan stdout to find ROOT TTree summaries and digest them.
    00729     """
    00730     outlines = stdout.splitlines()
    00731     nlines = len(outlines) - 1
    00732     summaries = {}
    00733     global h_count_re
    00734
    00735     pos = 0
    00736     while pos < nlines:
    00737         summ = {}
    00738         # find first line of block:
    00739         match = h_count_re.search(outlines[pos])
    00740         while pos < nlines and not match:
    00741             pos += 1
    00742             match = h_count_re.search(outlines[pos])
    00743         if match:
    00744             summ, pos = parseHistosSummary(outlines, pos)
    00745         summaries.update(summ)
    00746     return summaries
    00747
    00748 ########################################################################
    00749 # Test Classes
    00750 ########################################################################
    00751 class GaudiExeTest(ExecTestBase):
    00752     """Standard Gaudi test.
    00753     """
    00754     arguments = [
    00755         qm.fields.TextField(
    00756             name="program",
    00757             title="Program",
    00758             not_empty_text=1,
    00759             description="""The path to the program.
    00760
    00761             This field indicates the path to the program.  If it is not
    00762             an absolute path, the value of the 'PATH' environment
    00763             variable will be used to search for the program.
    00764             If not specified, $GAUDIEXE or Gaudi.exe are used.
    00765             """
    00766             ),
    00767         qm.fields.SetField(qm.fields.TextField(
    00768             name="args",
    00769             title="Argument List",
    00770             description="""The command-line arguments.
    00771
    00772             If this field is left blank, the program is run without any
    00773             arguments.
    00774
    00775             Use this field to specify the option files.
    00776
    00777             An implicit 0th argument (the path to the program) is added
    00778             automatically."""
    00779             )),
    00780         qm.fields.TextField(
    00781             name="options",
    00782             title="Options",
    00783             description="""Options to be passed to the application.
    00784
    00785             This field allows to pass a list of options to the main program
    00786             without the need of a separate option file.
    00787
    00788             The content of the field is written to a temporary file which name
    00789             is passed the the application as last argument (appended to the
    00790             field "Argument List".
    00791             """,
    00792             verbatim="true",
    00793             multiline="true",
    00794             default_value=""
    00795             ),
    00796         qm.fields.TextField(
    00797             name="workdir",
    00798             title="Working Directory",
    00799             description="""Path to the working directory.
    00800
    00801             If this field is left blank, the program will be run from the qmtest
    00802             directory, otherwise from the directory specified.""",
    00803             default_value=""
    00804             ),
    00805         qm.fields.TextField(
    00806             name="reference",
    00807             title="Reference Output",
    00808             description="""Path to the file containing the reference output.
    00809
    00810             If this field is left blank, any standard output will be considered
    00811             valid.
    00812
    00813             If the reference file is specified, any output on standard error is
    00814             ignored."""
    00815             ),
    00816         qm.fields.TextField(
    00817             name="error_reference",
    00818             title="Reference for standard error",
    00819             description="""Path to the file containing the reference for the standard error.
    00820
    00821             If this field is left blank, any standard output will be considered
    00822             valid.
    00823
    00824             If the reference file is specified, any output on standard error is
    00825             ignored."""
    00826             ),
    00827         qm.fields.SetField(qm.fields.TextField(
    00828             name = "unsupported_platforms",
    00829             title = "Unsupported Platforms",
    00830             description = """Platform on which the test must not be run.
    00831
    00832             List of regular expressions identifying the platforms on which the
    00833             test is not run and the result is set to UNTESTED."""
    00834             )),
    00835
    00836         qm.fields.TextField(
    00837             name = "validator",
    00838             title = "Validator",
    00839             description = """Function to validate the output of the test.
    00840
    00841             If defined, the function is used to validate the products of the
    00842             test.
    00843             The function is called passing as arguments:
    00844               self:   the test class instance
    00845               stdout: the standard output of the executed test
    00846               stderr: the standard error of the executed test
    00847               result: the Result objects to fill with messages
    00848             The function must return a list of causes for the failure.
    00849             If specified, overrides standard output, standard error and
    00850             reference files.
    00851             """,
    00852             verbatim="true",
    00853             multiline="true",
    00854             default_value=""
    00855             ),
    00856
    00857         qm.fields.BooleanField(
    00858             name = "use_temp_dir",
    00859             title = "Use temporary directory",
    00860             description = """Use temporary directory.
    00861
    00862             If set to true, use a temporary directory as working directory.
    00863             """,
    00864             default_value="false"
    00865             ),
    00866         ]
    00867
    00868     def PlatformIsNotSupported(self, context, result):
    00869         platform = self.GetPlatform()
    00870         unsupported = [ re.compile(x)
    00871                         for x in [ str(y).strip()
    00872                                    for y in self.unsupported_platforms ]
    00873                         if x
    00874                        ]
    00875         for p_re in unsupported:
    00876             if p_re.search(platform):
    00877                 result.SetOutcome(result.UNTESTED)
    00878                 result[result.CAUSE] = 'Platform not supported.'
    00879                 return True
    00880         return False
    00881
    00882     def GetPlatform(self):
    00883         """
    00884         Return the platform Id defined in CMTCONFIG or SCRAM_ARCH.
    00885         """
    00886         arch = "None"
    00887         # check architecture name
    00888         if "CMTCONFIG" in os.environ:
    00889             arch = os.environ["CMTCONFIG"]
    00890         elif "SCRAM_ARCH" in os.environ:
    00891             arch = os.environ["SCRAM_ARCH"]
    00892         return arch
    00893
    00894     def _expandReferenceFileName(self, reffile):
    00895         # if no file is passed, do nothing
    00896         if not reffile:
    00897             return ""
    00898
    00899         reference = os.path.normpath(os.path.expandvars(reffile))
    00900         # old-style platform-specific reference name
    00901         spec_ref = reference[:-3] + self.GetPlatform()[0:3] + reference[-3:]
    00902         if os.path.isfile(spec_ref):
    00903             reference = spec_ref
    00904         else: # look for new-style platform specific reference files:
    00905             # get all the files whose name start with the reference filename
    00906             dirname, basename = os.path.split(reference)
    00907             if not dirname: dirname = '.'
    00908             head = basename + "."
    00909             head_len = len(head)
    00910             platform = self.GetPlatform()
    00911             candidates = []
    00912             for f in os.listdir(dirname):
    00913                 if f.startswith(head) and platform.startswith(f[head_len:]):
    00914                     candidates.append( (len(f) - head_len, f) )
    00915             if candidates: # take the one with highest matching
    00916                 candidates.sort()
    00917                 reference = os.path.join(dirname, candidates[-1][1])
    00918         return reference
    00919
    00920     def CheckTTreesSummaries(self, stdout, result, causes,
    00921                              trees_dict = None,
    00922                              ignore = r"Basket|.*size|Compression"):
    00923         """
    00924         Compare the TTree summaries in stdout with the ones in trees_dict or in
    00925         the reference file. By default ignore the size, compression and basket
    00926         fields.
    00927         The presence of TTree summaries when none is expected is not a failure.
    00928         """
    00929         if trees_dict is None:
    00930             reference = self._expandReferenceFileName(self.reference)
    00931             # call the validator if the file exists
    00932             if reference and os.path.isfile(reference):
    00933                 trees_dict = findTTreeSummaries(open(reference).read())
    00934             else:
    00935                 trees_dict = {}
    00936
    00937         from pprint import PrettyPrinter
    00938         pp = PrettyPrinter()
    00939         if trees_dict:
    00940             result["GaudiTest.TTrees.expected"] = result.Quote(pp.pformat(trees_dict))
    00941             if ignore:
    00942                 result["GaudiTest.TTrees.ignore"] = result.Quote(ignore)
    00943
    00944         trees = findTTreeSummaries(stdout)
    00945         failed = cmpTreesDicts(trees_dict, trees, ignore)
    00946         if failed:
    00947             causes.append("trees summaries")
    00948             msg = "%s: %s != %s" % getCmpFailingValues(trees_dict, trees, failed)
    00949             result["GaudiTest.TTrees.failure_on"] = result.Quote(msg)
    00950             result["GaudiTest.TTrees.found"] = result.Quote(pp.pformat(trees))
    00951
    00952         return causes
    00953
    00954     def CheckHistosSummaries(self, stdout, result, causes,
    00955                              dict = None,
    00956                              ignore = None):
    00957         """
    00958         Compare the TTree summaries in stdout with the ones in trees_dict or in
    00959         the reference file. By default ignore the size, compression and basket
    00960         fields.
    00961         The presence of TTree summaries when none is expected is not a failure.
    00962         """
    00963         if dict is None:
    00964             reference = self._expandReferenceFileName(self.reference)
    00965             # call the validator if the file exists
    00966             if reference and os.path.isfile(reference):
    00967                 dict = findHistosSummaries(open(reference).read())
    00968             else:
    00969                 dict = {}
    00970
    00971         from pprint import PrettyPrinter
    00972         pp = PrettyPrinter()
    00973         if dict:
    00974             result["GaudiTest.Histos.expected"] = result.Quote(pp.pformat(dict))
    00975             if ignore:
    00976                 result["GaudiTest.Histos.ignore"] = result.Quote(ignore)
    00977
    00978         histos = findHistosSummaries(stdout)
    00979         failed = cmpTreesDicts(dict, histos, ignore)
    00980         if failed:
    00981             causes.append("histos summaries")
    00982             msg = "%s: %s != %s" % getCmpFailingValues(dict, histos, failed)
    00983             result["GaudiTest.Histos.failure_on"] = result.Quote(msg)
    00984             result["GaudiTest.Histos.found"] = result.Quote(pp.pformat(histos))
    00985
    00986         return causes
    00987
    00988     def ValidateWithReference(self, stdout, stderr, result, causes, preproc = None):
    00989         """
    00990         Default validation action: compare standard output and error to the
    00991         reference files.
    00992         """
    00993         # set the default output preprocessor
    00994         if preproc is None:
    00995             preproc = normalizeExamples
    00996         # check standard output
    00997         reference = self._expandReferenceFileName(self.reference)
    00998         # call the validator if the file exists
    00999         if reference and os.path.isfile(reference):
    01000             result["GaudiTest.output_reference"] = reference
    01001             causes += ReferenceFileValidator(reference,
    01002                                              "standard output",
    01003                                              "GaudiTest.output_diff",
    01004                                              preproc = preproc)(stdout, result)
    01005
    01006         # Compare TTree summaries
    01007         causes = self.CheckTTreesSummaries(stdout, result, causes)
    01008         causes = self.CheckHistosSummaries(stdout, result, causes)
    01009
    01010         if causes: # Write a new reference file for stdout
    01011             newref = open(reference + ".new","w")
    01012             # sanitize newlines
    01013             for l in stdout.splitlines():
    01014                 newref.write(l.rstrip() + '\n')
    01015             del newref # flush and close
    01016
    01017
    01018         # check standard error
    01019         reference = self._expandReferenceFileName(self.error_reference)
    01020         # call the validator if we have a file to use
    01021         if reference and os.path.isfile(reference):
    01022             result["GaudiTest.error_reference"] = reference
    01023             newcauses = ReferenceFileValidator(reference,
    01024                                                "standard error",
    01025                                                "GaudiTest.error_diff",
    01026                                                preproc = preproc)(stderr, result)
    01027             causes += newcauses
    01028             if newcauses: # Write a new reference file for stdedd
    01029                 newref = open(reference + ".new","w")
    01030                 # sanitize newlines
    01031                 for l in stderr.splitlines():
    01032                     newref.write(l.rstrip() + '\n')
    01033                 del newref # flush and close
    01034         else:
    01035             causes += BasicOutputValidator(self.stderr,
    01036                                            "standard error",
    01037                                            "ExecTest.expected_stderr")(stderr, result)
    01038
    01039         return causes
    01040
    01041     def ValidateOutput(self, stdout, stderr, result):
    01042         causes = []
    01043         # if the test definition contains a custom validator, use it
    01044         if self.validator.strip() != "":
    01045             class CallWrapper(object):
    01046                 """
    01047                 Small wrapper class to dynamically bind some default arguments
    01048                 to a callable.
    01049                 """
    01050                 def __init__(self, callable, extra_args = {}):
    01051                     self.callable = callable
    01052                     self.extra_args = extra_args
    01053                     # get the list of names of positional arguments
    01054                     from inspect import getargspec
    01055                     self.args_order = getargspec(callable)[0]
    01056                     # Remove "self" from the list of positional arguments
    01057                     # since it is added automatically
    01058                     if self.args_order[0] == "self":
    01059                         del self.args_order[0]
    01060                 def __call__(self, *args, **kwargs):
    01061                     # Check which positional arguments are used
    01062                     positional = self.args_order[:len(args)]
    01063
    01064                     kwargs = dict(kwargs) # copy the arguments dictionary
    01065                     for a in self.extra_args:
    01066                         # use "extra_args" for the arguments not specified as
    01067                         # positional or keyword
    01068                         if a not in positional and a not in kwargs:
    01069                             kwargs[a] = self.extra_args[a]
    01070                     return apply(self.callable, args, kwargs)
    01071             # local names to be exposed in the script
    01072             exported_symbols = {"self":self,
    01073                                 "stdout":stdout,
    01074                                 "stderr":stderr,
    01075                                 "result":result,
    01076                                 "causes":causes,
    01077                                 "findReferenceBlock":
    01078                                     CallWrapper(findReferenceBlock, {"stdout":stdout,
    01079                                                                      "result":result,
    01080                                                                      "causes":causes}),
    01081                                 "validateWithReference":
    01082                                     CallWrapper(self.ValidateWithReference, {"stdout":stdout,
    01083                                                                              "stderr":stderr,
    01084                                                                              "result":result,
    01085                                                                              "causes":causes}),
    01086                                 "countErrorLines":
    01087                                     CallWrapper(countErrorLines, {"stdout":stdout,
    01088                                                                   "result":result,
    01089                                                                   "causes":causes}),
    01090                                 "checkTTreesSummaries":
    01091                                     CallWrapper(self.CheckTTreesSummaries, {"stdout":stdout,
    01092                                                                             "result":result,
    01093                                                                             "causes":causes}),
    01094                                 "checkHistosSummaries":
    01095                                     CallWrapper(self.CheckHistosSummaries, {"stdout":stdout,
    01096                                                                             "result":result,
    01097                                                                             "causes":causes}),
    01098
    01099                                 }
    01100             exec self.validator in globals(), exported_symbols
    01101         else:
    01102             self.ValidateWithReference(stdout, stderr, result, causes)
    01103
    01104         return causes
    01105
    01106     def DumpEnvironment(self, result):
    01107         """
    01108         Add the content of the environment to the result object.
    01109
    01110         Copied from the QMTest class of COOL.
    01111         """
    01112         vars = os.environ.keys()
    01113         vars.sort()
    01114         result['GaudiTest.environment'] = \
    01115             result.Quote('\n'.join(["%s=%s"%(v,os.environ[v]) for v in vars]))
    01116
    01117     def _find_program(self,prog):
    01118         # check if it is an absolute path or the file can be found
    01119         # from the local directory, otherwise search for it in PATH
    01120         if not os.path.isabs(prog) and not os.path.isfile(prog):
    01121             for d in os.environ["PATH"].split(os.pathsep):
    01122                 p = os.path.join(d,prog)
    01123                 if os.path.isfile(p):
    01124                     return p
    01125         return prog
    01126
    01127     def Run(self, context, result):
    01128         """Run the test.
    01129
    01130         'context' -- A 'Context' giving run-time parameters to the
    01131         test.
    01132
    01133         'result' -- A 'Result' object.  The outcome will be
    01134         'Result.PASS' when this method is called.  The 'result' may be
    01135         modified by this method to indicate outcomes other than
    01136         'Result.PASS' or to add annotations."""
    01137
    01138         # Check if the platform is supported
    01139         if self.PlatformIsNotSupported(context, result):
    01140             return
    01141
    01142         def rationalizepath(p):
    01143             p = os.path.normpath(os.path.expandvars(p))
    01144             if os.path.exists(p):
    01145                 p = os.path.realpath(p)
    01146             return p
    01147
    01148         # Prepare program name and arguments (expanding variables, and converting to absolute)
    01149         if self.program:
    01150             prog = rationalizepath(self.program)
    01151         elif "GAUDIEXE" in os.environ:
    01152             prog = os.environ["GAUDIEXE"]
    01153         else:
    01154             prog = "Gaudi.exe"
    01155         self.program = prog
    01156
    01157         dummy, prog_ext = os.path.splitext(prog)
    01158         if prog_ext not in [ ".exe", ".py", ".bat" ] and self.GetPlatform()[0:3] == "win":
    01159             prog += ".exe"
    01160             prog_ext = ".exe"
    01161
    01162         prog = self._find_program(prog)
    01163
    01164         # Convert paths to absolute paths in arguments and reference files
    01165         args = map(rationalizepath, self.args)
    01166         self.reference = rationalizepath(self.reference)
    01167         self.error_reference = rationalizepath(self.error_reference)
    01168
    01169
    01170         # check if the user provided inline options
    01171         tmpfile = None
    01172         if self.options.strip():
    01173             ext = ".opts"
    01174             if re.search(r"from\s*Gaudi.Configuration\s*import\s*\*", self.options):
    01175                 ext = ".py"
    01176             tmpfile = TempFile(ext)
    01177             tmpfile.writelines("\n".join(self.options.splitlines()))
    01178             tmpfile.flush()
    01179             args.append(tmpfile.name)
    01180             result["GaudiTest.options"] = result.Quote(self.options)
    01181
    01182         # if the program is a python file, execute it through python
    01183         if prog_ext == ".py":
    01184             args.insert(0,prog)
    01185             if self.GetPlatform()[0:3] == "win":
    01186                 prog = self._find_program("python.exe")
    01187             else:
    01188                 prog = self._find_program("python")
    01189
    01190         # Change to the working directory if specified or to the default temporary
    01191         origdir = os.getcwd()
    01192         if self.workdir:
    01193             os.chdir(str(os.path.normpath(os.path.expandvars(self.workdir))))
    01194         elif "qmtest.tmpdir" in context and self.use_temp_dir == "true":
    01195             os.chdir(context["qmtest.tmpdir"])
    01196
    01197         if "QMTEST_IGNORE_TIMEOUT" not in os.environ:
    01198             self.timeout = max(self.timeout,600)
    01199         else:
    01200             self.timeout = -1
    01201
    01202         try:
    01203             # Run the test
    01204             self.RunProgram(prog,
    01205                             [ prog ] + args,
    01206                             context, result)
    01207             # Record the content of the enfironment for failing tests
    01208             if result.GetOutcome() not in [ result.PASS ]:
    01209                 self.DumpEnvironment(result)
    01210         finally:
    01211             # revert to the original directory
    01212             os.chdir(origdir)
    01213
    01214     def RunProgram(self, program, arguments, context, result):
    01215         """Run the 'program'.
    01216
    01217         'program' -- The path to the program to run.
    01218
    01219         'arguments' -- A list of the arguments to the program.  This
    01220         list must contain a first argument corresponding to 'argv[0]'.
    01221
    01222         'context' -- A 'Context' giving run-time parameters to the
    01223         test.
    01224
    01225         'result' -- A 'Result' object.  The outcome will be
    01226         'Result.PASS' when this method is called.  The 'result' may be
    01227         modified by this method to indicate outcomes other than
    01228         'Result.PASS' or to add annotations.
    01229
    01230         @attention: This method has been copied from command.ExecTestBase
    01231                     (QMTest 2.3.0) and modified to keep stdout and stderr
    01232                     for tests that have been terminated by a signal.
    01233                     (Fundamental for debugging in the Application Area)
    01234         """
    01235
    01236         # Construct the environment.
    01237         environment = self.MakeEnvironment(context)
    01238         # Create the executable.
    01239         if self.timeout >= 0:
    01240             timeout = self.timeout
    01241         else:
    01242             # If no timeout was specified, we sill run this process in a
    01243             # separate process group and kill the entire process group
    01244             # when the child is done executing.  That means that
    01245             # orphaned child processes created by the test will be
    01246             # cleaned up.
    01247             timeout = -2
    01248         e = qm.executable.Filter(self.stdin, timeout)
    01249         # Run it.
    01250         exit_status = e.Run(arguments, environment, path = program)
    01251
    01252         # If the process terminated normally, check the outputs.
    01253         if sys.platform == "win32" or os.WIFEXITED(exit_status):
    01254             # There are no causes of failure yet.
    01255             causes = []
    01256             # The target program terminated normally.  Extract the
    01257             # exit code, if this test checks it.
    01258             if self.exit_code is None:
    01259                 exit_code = None
    01260             elif sys.platform == "win32":
    01261                 exit_code = exit_status
    01262             else:
    01263                 exit_code = os.WEXITSTATUS(exit_status)
    01264             # Get the output generated by the program.
    01265             stdout = e.stdout
    01266             stderr = e.stderr
    01267             # Record the results.
    01268             result["ExecTest.exit_code"] = str(exit_code)
    01269             result["ExecTest.stdout"] = result.Quote(stdout)
    01270             result["ExecTest.stderr"] = result.Quote(stderr)
    01271             # Check to see if the exit code matches.
    01272             if exit_code != self.exit_code:
    01273                 causes.append("exit_code")
    01274                 result["ExecTest.expected_exit_code"] \
    01275                     = str(self.exit_code)
    01276             # Validate the output.
    01277             causes += self.ValidateOutput(stdout, stderr, result)
    01278             # If anything went wrong, the test failed.
    01279             if causes:
    01280                 result.Fail("Unexpected %s." % string.join(causes, ", "))
    01281         elif os.WIFSIGNALED(exit_status):
    01282             # The target program terminated with a signal.  Construe
    01283             # that as a test failure.
    01284             signal_number = str(os.WTERMSIG(exit_status))
    01285             result.Fail("Program terminated by signal.")
    01286             result["ExecTest.signal_number"] = signal_number
    01287             result["ExecTest.stdout"] = result.Quote(e.stdout)
    01288             result["ExecTest.stderr"] = result.Quote(e.stderr)
    01289         elif os.WIFSTOPPED(exit_status):
    01290             # The target program was stopped.  Construe that as a
    01291             # test failure.
    01292             signal_number = str(os.WSTOPSIG(exit_status))
    01293             result.Fail("Program stopped by signal.")
    01294             result["ExecTest.signal_number"] = signal_number
    01295             result["ExecTest.stdout"] = result.Quote(e.stdout)
    01296             result["ExecTest.stderr"] = result.Quote(e.stderr)
    01297         else:
    01298             # The target program terminated abnormally in some other
    01299             # manner.  (This shouldn't normally happen...)
    01300             result.Fail("Program did not terminate normally.")
    01301
    01302         # Marco Cl.: This is a special trick to fix a "problem" with the output
    01303         # of gaudi jobs when they use colors
    01304         esc = '\x1b'
    01305         repr_esc = '\\x1b'
    01306         result["ExecTest.stdout"] = result["ExecTest.stdout"].replace(esc,repr_esc)
    01307         # TODO: (MCl) improve the hack for colors in standard output
    01308         #             may be converting them to HTML tags