| # Gaudi Framework, version v23r1 |

Home |  | Generated: Wed Feb 29 2012

# GaudiTest.py

Go to the documentation of this file.

    00001 ########################################################################
    00002 # File:   GaudiTest.py
    00003 # Author: Marco Clemencic CERN/PH-LBC
    00004 ########################################################################
    00005 __author__  = 'Marco Clemencic CERN/PH-LBC'
    00006 ########################################################################
    00007 # Imports
    00008 ########################################################################
    00009 import os
    00010 import sys
    00011 import re
    00012 import tempfile
    00013 import shutil
    00014 import string
    00015 import difflib
    00016 from subprocess import Popen, PIPE, STDOUT
    00017
    00018 import qm
    00019 from qm.test.classes.command import ExecTestBase
    00020 from qm.test.result_stream import ResultStream
    00021
    00022 ### Needed by the re-implementation of TimeoutExecutable
    00023 import qm.executable
    00024 import time, signal
    00025 # The classes in this module are implemented differently depending on
    00026 # the operating system in use.
    00027 if sys.platform == "win32":
    00028     import msvcrt
    00029     import pywintypes
    00030     from   threading import *
    00031     import win32api
    00032     import win32con
    00033     import win32event
    00034     import win32file
    00035     import win32pipe
    00036     import win32process
    00037 else:
    00038     import cPickle
    00039     import fcntl
    00040     import select
    00041     import qm.sigmask
    00042
    00043 ########################################################################
    00044 # Utility Classes
    00045 ########################################################################
    00046 class TemporaryEnvironment:
    00047     """
    00048     Class to changes the environment temporarily.
    00049     """
    00050     def __init__(self, orig = os.environ, keep_same = False):
    00051         """
    00052         Create a temporary environment on top of the one specified
    00053         (it can be another TemporaryEnvironment instance).
    00054         """
    00055         #print "New environment"
    00056         self.old_values = {}
    00057         self.env = orig
    00058         self._keep_same = keep_same
    00059
    00060     def __setitem__(self,key,value):
    00061         """
    00062         Set an environment variable recording the previous value.
    00063         """
    00064         if key not in self.old_values :
    00065             if key in self.env :
    00066                 if not self._keep_same or self.env[key] != value:
    00067                     self.old_values[key] = self.env[key]
    00068             else:
    00069                 self.old_values[key] = None
    00070         self.env[key] = value
    00071
    00072     def __getitem__(self,key):
    00073         """
    00074         Get an environment variable.
    00075         Needed to provide the same interface as os.environ.
    00076         """
    00077         return self.env[key]
    00078
    00079     def __delitem__(self,key):
    00080         """
    00081         Unset an environment variable.
    00082         Needed to provide the same interface as os.environ.
    00083         """
    00084         if key not in self.env :
    00085             raise KeyError(key)
    00086         self.old_values[key] = self.env[key]
    00087         del self.env[key]
    00088
    00089     def keys(self):
    00090         """
    00091         Return the list of defined environment variables.
    00092         Needed to provide the same interface as os.environ.
    00093         """
    00094         return self.env.keys()
    00095
    00096     def items(self):
    00097         """
    00098         Return the list of (name,value) pairs for the defined environment variables.
    00099         Needed to provide the same interface as os.environ.
    00100         """
    00101         return self.env.items()
    00102
    00103     def __contains__(self,key):
    00104         """
    00105         Operator 'in'.
    00106         Needed to provide the same interface as os.environ.
    00107         """
    00108         return key in self.env
    00109
    00110     def restore(self):
    00111         """
    00112         Revert all the changes done to the orignal environment.
    00113         """
    00114         for key,value in self.old_values.items():
    00115             if value is None:
    00116                 del self.env[key]
    00117             else:
    00118                 self.env[key] = value
    00119         self.old_values = {}
    00120
    00121     def __del__(self):
    00122         """
    00123         Revert the changes on destruction.
    00124         """
    00125         #print "Restoring the environment"
    00126         self.restore()
    00127
    00128     def gen_script(self,shell_type):
    00129         """
    00130         Generate a shell script to reproduce the changes in the environment.
    00131         """
    00132         shells = [ 'csh', 'sh', 'bat' ]
    00133         if shell_type not in shells:
    00134             raise RuntimeError("Shell type '%s' unknown. Available: %s"%(shell_type,shells))
    00135         out = ""
    00136         for key,value in self.old_values.items():
    00137             if key not in self.env:
    00138                 # unset variable
    00139                 if shell_type == 'csh':
    00140                     out += 'unsetenv %s\n'%key
    00141                 elif shell_type == 'sh':
    00142                     out += 'unset %s\n'%key
    00143                 elif shell_type == 'bat':
    00144                     out += 'set %s=\n'%key
    00145             else:
    00146                 # set variable
    00147                 if shell_type == 'csh':
    00148                     out += 'setenv %s "%s"\n'%(key,self.env[key])
    00149                 elif shell_type == 'sh':
    00150                     out += 'export %s="%s"\n'%(key,self.env[key])
    00151                 elif shell_type == 'bat':
    00152                     out += 'set %s=%s\n'%(key,self.env[key])
    00153         return out
    00154
    00155 class TempDir:
    00156     """Small class for temporary directories.
    00157     When instantiated, it creates a temporary directory and the instance
    00158     behaves as the string containing the directory name.
    00159     When the instance goes out of scope, it removes all the content of
    00160     the temporary directory (automatic clean-up).
    00161     """
    00162     def __init__(self, keep = False, chdir = False):
    00163         self.name = tempfile.mkdtemp()
    00164         self._keep = keep
    00165         self._origdir = None
    00166         if chdir:
    00167             self._origdir = os.getcwd()
    00168             os.chdir(self.name)
    00169
    00170     def __str__(self):
    00171         return self.name
    00172
    00173     def __del__(self):
    00174         if self._origdir:
    00175             os.chdir(self._origdir)
    00176         if self.name and not self._keep:
    00177             shutil.rmtree(self.name)
    00178
    00179     def __getattr__(self,attr):
    00180         return getattr(self.name,attr)
    00181
    00182 class TempFile:
    00183     """Small class for temporary files.
    00184     When instantiated, it creates a temporary directory and the instance
    00185     behaves as the string containing the directory name.
    00186     When the instance goes out of scope, it removes all the content of
    00187     the temporary directory (automatic clean-up).
    00188     """
    00189     def __init__(self, suffix='', prefix='tmp', dir=None, text=False, keep = False):
    00190         self.file = None
    00191         self.name = None
    00192         self._keep = keep
    00193
    00194         self._fd, self.name = tempfile.mkstemp(suffix,prefix,dir,text)
    00195         self.file = os.fdopen(self._fd,"r+")
    00196
    00197     def __str__(self):
    00198         return self.name
    00199
    00200     def __del__(self):
    00201         if self.file:
    00202             self.file.close()
    00203         if self.name and not self._keep:
    00204             os.remove(self.name)
    00205
    00206     def __getattr__(self,attr):
    00207         return getattr(self.file,attr)
    00208
    00209 class CMT:
    00210     """Small wrapper to call CMT.
    00211     """
    00212     def __init__(self,path=None):
    00213         if path is None:
    00214             path = os.getcwd()
    00215         self.path = path
    00216
    00217     def _run_cmt(self,command,args):
    00218         # prepare command line
    00219         if type(args) is str:
    00220             args = [args]
    00221         cmd = "cmt %s"%command
    00222         for arg in args:
    00223             cmd += ' "%s"'%arg
    00224
    00225         # go to the execution directory
    00226         olddir = os.getcwd()
    00227         os.chdir(self.path)
    00228         # run cmt
    00229         result = os.popen4(cmd)[1].read()
    00230         # return to the old directory
    00231         os.chdir(olddir)
    00232         return result
    00233
    00234     def __getattr__(self,attr):
    00235         return lambda args=[]: self._run_cmt(attr, args)
    00236
    00237     def runtime_env(self,env = None):
    00238         """Returns a dictionary containing the runtime environment produced by CMT.
    00239         If a dictionary is passed a modified instance of it is returned.
    00240         """
    00241         if env is None:
    00242             env = {}
    00243         for l in self.setup("-csh").splitlines():
    00244             l = l.strip()
    00245             if l.startswith("setenv"):
    00246                 dummy,name,value = l.split(None,3)
    00247                 env[name] = value.strip('"')
    00248             elif l.startswith("unsetenv"):
    00249                 dummy,name = l.split(None,2)
    00250                 if name in env:
    00251                     del env[name]
    00252         return env
    00253     def show_macro(self,k):
    00254         r = self.show(["macro",k])
    00255         if r.find("CMT> Error: symbol not found") >= 0:
    00256             return None
    00257         else:
    00258             return self.show(["macro_value",k]).strip()
    00259
    00260 ## Locates an executable in the executables path ($PATH) and returns the full
    00261 #  path to it.
    00262 #  If the executable cannot be found, None is returned
    00263 def which(executable):
    00264     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             if i < (count - 2) and lines[i].startswith("*Branch "):
    00660                 # skip branch header
    00661                 i += 3
    00662                 continue
    00663             branch = parseblock(lines[i:i+3])
    00664             result["Branches"][branch["Name"]] = branch
    00665             i += 4
    00666
    00667     return (result, i)
    00668
    00669 def findTTreeSummaries(stdout):
    00670     """
    00671     Scan stdout to find ROOT TTree summaries and digest them.
    00672     """
    00673     stars = re.compile(r"^\*+$")
    00674     outlines = stdout.splitlines()
    00675     nlines = len(outlines)
    00676     trees = {}
    00677
    00678     i = 0
    00679     while i < nlines: #loop over the output
    00680         # look for
    00681         while i < nlines and not stars.match(outlines[i]):
    00682             i += 1
    00683         if i < nlines:
    00684             tree, i = _parseTTreeSummary(outlines, i)
    00685             if tree:
    00686                 trees[tree["Name"]] = tree
    00687
    00688     return trees
    00689
    00690 def cmpTreesDicts(reference, to_check, ignore = None):
    00691     """
    00692     Check that all the keys in reference are in to_check too, with the same value.
    00693     If the value is a dict, the function is called recursively. to_check can
    00694     contain more keys than reference, that will not be tested.
    00695     The function returns at the first difference found.
    00696     """
    00697     fail_keys = []
    00698     # filter the keys in the reference dictionary
    00699     if ignore:
    00700         ignore_re = re.compile(ignore)
    00701         keys = [ key for key in reference if not ignore_re.match(key) ]
    00702     else:
    00703         keys = reference.keys()
    00704     # loop over the keys (not ignored) in the reference dictionary
    00705     for k in keys:
    00706         if k in to_check: # the key must be in the dictionary to_check
    00707             if (type(reference[k]) is dict) and (type(to_check[k]) is dict):
    00708                 # if both reference and to_check values are dictionaries, recurse
    00709                 failed = fail_keys = cmpTreesDicts(reference[k], to_check[k], ignore)
    00710             else:
    00711                 # compare the two values
    00712                 failed = to_check[k] != reference[k]
    00713         else: # handle missing keys in the dictionary to check (i.e. failure)
    00714             to_check[k] = None
    00715             failed = True
    00716         if failed:
    00717             fail_keys.insert(0, k)
    00718             break # exit from the loop at the first failure
    00719     return fail_keys # return the list of keys bringing to the different values
    00720
    00721 def getCmpFailingValues(reference, to_check, fail_path):
    00722     c = to_check
    00723     r = reference
    00724     for k in fail_path:
    00725         c = c.get(k,None)
    00726         r = r.get(k,None)
    00727         if c is None or r is None:
    00728             break # one of the dictionaries is not deep enough
    00729     return (fail_path, r, c)
    00730
    00731 # signature of the print-out of the histograms
    00732 h_count_re = re.compile(r"^(.*)SUCCESS\s+Booked (\d+) Histogram\(s\) :\s+(.*)")
    00733
    00734 def parseHistosSummary(lines, pos):
    00735     """
    00736     Extract the histograms infos from the lines starting at pos.
    00737     Returns the position of the first line after the summary block.
    00738     """
    00739     global h_count_re
    00740     h_table_head = re.compile(r'SUCCESS\s+List of booked (1D|2D|3D|1D profile|2D profile) histograms in directory\s+"(\w*)"')
    00741     h_short_summ = re.compile(r"ID=([^\"]+)\s+\"([^\"]+)\"\s+(.*)")
    00742
    00743     nlines = len(lines)
    00744
    00745     # decode header
    00746     m = h_count_re.search(lines[pos])
    00747     name = m.group(1).strip()
    00748     total = int(m.group(2))
    00749     header = {}
    00750     for k, v in [ x.split("=") for x in  m.group(3).split() ]:
    00751         header[k] = int(v)
    00752     pos += 1
    00753     header["Total"] = total
    00754
    00755     summ = {}
    00756     while pos < nlines:
    00757         m = h_table_head.search(lines[pos])
    00758         if m:
    00759             t, d = m.groups(1) # type and directory
    00760             t = t.replace(" profile", "Prof")
    00761             pos += 1
    00762             if pos < nlines:
    00763                 l = lines[pos]
    00764             else:
    00765                 l = ""
    00766             cont = {}
    00767             if l.startswith(" | ID"):
    00768                 # table format
    00769                 titles = [ x.strip() for x in l.split("|")][1:]
    00770                 pos += 1
    00771                 while pos < nlines and lines[pos].startswith(" |"):
    00772                     l = lines[pos]
    00773                     values = [ x.strip() for x in l.split("|")][1:]
    00774                     hcont = {}
    00775                     for i in range(len(titles)):
    00776                         hcont[titles[i]] = values[i]
    00777                     cont[hcont["ID"]] = hcont
    00778                     pos += 1
    00779             elif l.startswith(" ID="):
    00780                 while pos < nlines and lines[pos].startswith(" ID="):
    00781                     values = [ x.strip() for x in  h_short_summ.search(lines[pos]).groups() ]
    00782                     cont[values[0]] = values
    00783                     pos += 1
    00784             else: # not interpreted
    00785                 raise RuntimeError("Cannot understand line %d: '%s'" % (pos, l))
    00786             if not d in summ:
    00787                 summ[d] = {}
    00788             summ[d][t] = cont
    00789             summ[d]["header"] = header
    00790         else:
    00791             break
    00792     if not summ:
    00793         # If the full table is not present, we use only the header
    00794         summ[name] = {"header": header}
    00795     return summ, pos
    00796
    00797 def findHistosSummaries(stdout):
    00798     """
    00799     Scan stdout to find ROOT TTree summaries and digest them.
    00800     """
    00801     outlines = stdout.splitlines()
    00802     nlines = len(outlines) - 1
    00803     summaries = {}
    00804     global h_count_re
    00805
    00806     pos = 0
    00807     while pos < nlines:
    00808         summ = {}
    00809         # find first line of block:
    00810         match = h_count_re.search(outlines[pos])
    00811         while pos < nlines and not match:
    00812             pos += 1
    00813             match = h_count_re.search(outlines[pos])
    00814         if match:
    00815             summ, pos = parseHistosSummary(outlines, pos)
    00816         summaries.update(summ)
    00817     return summaries
    00818
    00819 class GaudiFilterExecutable(qm.executable.Filter):
    00820     def __init__(self, input, timeout = -1):
    00821         """Create a new 'Filter'.
    00822
    00823         'input' -- The string containing the input to provide to the
    00824         child process.
    00825
    00826         'timeout' -- As for 'TimeoutExecutable.__init__'."""
    00827
    00828         super(GaudiFilterExecutable, self).__init__(input, timeout)
    00829         self.__input = input
    00830         self.__timeout = timeout
    00831         self.stack_trace_file = None
    00832         # Temporary file to pass the stack trace from one process to the other
    00833         # The file must be closed and reopened when needed to avoid conflicts
    00834         # between the processes
    00835         tmpf = tempfile.mkstemp()
    00836         os.close(tmpf[0])
    00837         self.stack_trace_file = tmpf[1] # remember only the name
    00838
    00839     def __UseSeparateProcessGroupForChild(self):
    00840         """Copied from TimeoutExecutable to allow the re-implementation of
    00841            _HandleChild.
    00842         """
    00843         if sys.platform == "win32":
    00844             # In Windows 2000 (or later), we should use "jobs" by
    00845             # analogy with UNIX process groups.  However, that
    00846             # functionality is not (yet) provided by the Python Win32
    00847             # extensions.
    00848             return 0
    00849
    00850         return self.__timeout >= 0 or self.__timeout == -2
    00851     ##
    00852     # Needs to replace the ones from RedirectedExecutable and TimeoutExecutable
    00853     def _HandleChild(self):
    00854         """Code copied from both FilterExecutable and TimeoutExecutable.
    00855         """
    00856         # Close the pipe ends that we do not need.
    00857         if self._stdin_pipe:
    00858             self._ClosePipeEnd(self._stdin_pipe[0])
    00859         if self._stdout_pipe:
    00860             self._ClosePipeEnd(self._stdout_pipe[1])
    00861         if self._stderr_pipe:
    00862             self._ClosePipeEnd(self._stderr_pipe[1])
    00863
    00864         # The pipes created by 'RedirectedExecutable' must be closed
    00865         # before the monitor process (created by 'TimeoutExecutable')
    00866         # is created.  Otherwise, if the child process dies, 'select'
    00867         # in the parent will not return if the monitor process may
    00868         # still have one of the file descriptors open.
    00869
    00870         super(qm.executable.TimeoutExecutable, self)._HandleChild()
    00871
    00872         if self.__UseSeparateProcessGroupForChild():
    00873             # Put the child into its own process group.  This step is
    00874             # performed in both the parent and the child; therefore both
    00875             # processes can safely assume that the creation of the process
    00876             # group has taken place.
    00877             child_pid = self._GetChildPID()
    00878             try:
    00879                 os.setpgid(child_pid, child_pid)
    00880             except:
    00881                 # The call to setpgid may fail if the child has exited,
    00882                 # or has already called 'exec'.  In that case, we are
    00883                 # guaranteed that the child has already put itself in the
    00884                 # desired process group.
    00885                 pass
    00886             # Create the monitoring process.
    00887             #
    00888             # If the monitoring process is in parent's process group and
    00889             # kills the child after waitpid has returned in the parent, we
    00890             # may end up trying to kill a process group other than the one
    00891             # that we intend to kill.  Therefore, we put the monitoring
    00892             # process in the same process group as the child; that ensures
    00893             # that the process group will persist until the monitoring
    00894             # process kills it.
    00895             self.__monitor_pid = os.fork()
    00896             if self.__monitor_pid != 0:
    00897                 # Make sure that the monitoring process is placed into the
    00898                 # child's process group before the parent process calls
    00899                 # 'waitpid'.  In this way, we are guaranteed that the process
    00900                 # group as the child
    00901                 os.setpgid(self.__monitor_pid, child_pid)
    00902             else:
    00903                 # Put the monitoring process into the child's process
    00904                 # group.  We know the process group still exists at
    00905                 # this point because either (a) we are in the process
    00906                 # group, or (b) the parent has not yet called waitpid.
    00907                 os.setpgid(0, child_pid)
    00908
    00909                 # Close all open file descriptors.  They are not needed
    00910                 # in the monitor process.  Furthermore, when the parent
    00911                 # closes the write end of the stdin pipe to the child,
    00912                 # we do not want the pipe to remain open; leaving the
    00913                 # pipe open in the monitor process might cause the child
    00914                 # to block waiting for additional input.
    00915                 try:
    00916                     max_fds = os.sysconf("SC_OPEN_MAX")
    00917                 except:
    00918                     max_fds = 256
    00919                 for fd in xrange(max_fds):
    00920                     try:
    00921                         os.close(fd)
    00922                     except:
    00923                         pass
    00924                 try:
    00925                     if self.__timeout >= 0:
    00926                         # Give the child time to run.
    00927                         time.sleep (self.__timeout)
    00928                         #######################################################
    00929                         ### This is the interesting part: dump the stack trace to a file
    00930                         if sys.platform == "linux2": # we should be have /proc and gdb
    00931                             cmd = ["gdb",
    00932                                    os.path.join("/proc", str(child_pid), "exe"),
    00933                                    str(child_pid),
    00934                                    "-batch", "-n", "-x",
    00935                                    "'%s'" % os.path.join(os.path.dirname(__file__), "stack-trace.gdb")]
    00936                             # FIXME: I wanted to use subprocess.Popen, but it doesn't want to work
    00937                             #        in this context.
    00938                             o = os.popen(" ".join(cmd)).read()
    00939                             open(self.stack_trace_file,"w").write(o)
    00940                         #######################################################
    00941
    00942                         # Kill all processes in the child process group.
    00943                         os.kill(0, signal.SIGKILL)
    00944                     else:
    00945                         # This call to select will never terminate.
    00946                         select.select ([], [], [])
    00947                 finally:
    00948                     # Exit.  This code is in a finally clause so that
    00949                     # we are guaranteed to get here no matter what.
    00950                     os._exit(0)
    00951         elif self.__timeout >= 0 and sys.platform == "win32":
    00952             # Create a monitoring thread.
    00953             self.__monitor_thread = Thread(target = self.__Monitor)
    00954             self.__monitor_thread.start()
    00955
    00956     if sys.platform == "win32":
    00957
    00958         def __Monitor(self):
    00959             """Code copied from FilterExecutable.
    00960             Kill the child if the timeout expires.
    00961
    00962             This function is run in the monitoring thread."""
    00963
    00964             # The timeout may be expressed as a floating-point value
    00965             # on UNIX, but it must be an integer number of
    00966             # milliseconds when passed to WaitForSingleObject.
    00967             timeout = int(self.__timeout * 1000)
    00968             # Wait for the child process to terminate or for the
    00969             # timer to expire.
    00970             result = win32event.WaitForSingleObject(self._GetChildPID(),
    00971                                                     timeout)
    00972             # If the timeout occurred, kill the child process.
    00973             if result == win32con.WAIT_TIMEOUT:
    00974                 self.Kill()
    00975
    00976 ########################################################################
    00977 # Test Classes
    00978 ########################################################################
    00979 class GaudiExeTest(ExecTestBase):
    00980     """Standard Gaudi test.
    00981     """
    00982     arguments = [
    00983         qm.fields.TextField(
    00984             name="program",
    00985             title="Program",
    00986             not_empty_text=1,
    00987             description="""The path to the program.
    00988
    00989             This field indicates the path to the program.  If it is not
    00990             an absolute path, the value of the 'PATH' environment
    00991             variable will be used to search for the program.
    00992             If not specified, $GAUDIEXE or Gaudi.exe are used.
    00993             """
    00994             ),
    00995         qm.fields.SetField(qm.fields.TextField(
    00996             name="args",
    00997             title="Argument List",
    00998             description="""The command-line arguments.
    00999
    01000             If this field is left blank, the program is run without any
    01001             arguments.
    01002
    01003             Use this field to specify the option files.
    01004
    01005             An implicit 0th argument (the path to the program) is added
    01006             automatically."""
    01007             )),
    01008         qm.fields.TextField(
    01009             name="options",
    01010             title="Options",
    01011             description="""Options to be passed to the application.
    01012
    01013             This field allows to pass a list of options to the main program
    01014             without the need of a separate option file.
    01015
    01016             The content of the field is written to a temporary file which name
    01017             is passed the the application as last argument (appended to the
    01018             field "Argument List".
    01019             """,
    01020             verbatim="true",
    01021             multiline="true",
    01022             default_value=""
    01023             ),
    01024         qm.fields.TextField(
    01025             name="workdir",
    01026             title="Working Directory",
    01027             description="""Path to the working directory.
    01028
    01029             If this field is left blank, the program will be run from the qmtest
    01030             directory, otherwise from the directory specified.""",
    01031             default_value=""
    01032             ),
    01033         qm.fields.TextField(
    01034             name="reference",
    01035             title="Reference Output",
    01036             description="""Path to the file containing the reference output.
    01037
    01038             If this field is left blank, any standard output will be considered
    01039             valid.
    01040
    01041             If the reference file is specified, any output on standard error is
    01042             ignored."""
    01043             ),
    01044         qm.fields.TextField(
    01045             name="error_reference",
    01046             title="Reference for standard error",
    01047             description="""Path to the file containing the reference for the standard error.
    01048
    01049             If this field is left blank, any standard output will be considered
    01050             valid.
    01051
    01052             If the reference file is specified, any output on standard error is
    01053             ignored."""
    01054             ),
    01055         qm.fields.SetField(qm.fields.TextField(
    01056             name = "unsupported_platforms",
    01057             title = "Unsupported Platforms",
    01058             description = """Platform on which the test must not be run.
    01059
    01060             List of regular expressions identifying the platforms on which the
    01061             test is not run and the result is set to UNTESTED."""
    01062             )),
    01063
    01064         qm.fields.TextField(
    01065             name = "validator",
    01066             title = "Validator",
    01067             description = """Function to validate the output of the test.
    01068
    01069             If defined, the function is used to validate the products of the
    01070             test.
    01071             The function is called passing as arguments:
    01072               self:   the test class instance
    01073               stdout: the standard output of the executed test
    01074               stderr: the standard error of the executed test
    01075               result: the Result objects to fill with messages
    01076             The function must return a list of causes for the failure.
    01077             If specified, overrides standard output, standard error and
    01078             reference files.
    01079             """,
    01080             verbatim="true",
    01081             multiline="true",
    01082             default_value=""
    01083             ),
    01084
    01085         qm.fields.BooleanField(
    01086             name = "use_temp_dir",
    01087             title = "Use temporary directory",
    01088             description = """Use temporary directory.
    01089
    01090             If set to true, use a temporary directory as working directory.
    01091             """,
    01092             default_value="false"
    01093             ),
    01094         ]
    01095
    01096     def PlatformIsNotSupported(self, context, result):
    01097         platform = self.GetPlatform()
    01098         unsupported = [ re.compile(x)
    01099                         for x in [ str(y).strip()
    01100                                    for y in self.unsupported_platforms ]
    01101                         if x
    01102                        ]
    01103         for p_re in unsupported:
    01104             if p_re.search(platform):
    01105                 result.SetOutcome(result.UNTESTED)
    01106                 result[result.CAUSE] = 'Platform not supported.'
    01107                 return True
    01108         return False
    01109
    01110     def GetPlatform(self):
    01111         """
    01112         Return the platform Id defined in CMTCONFIG or SCRAM_ARCH.
    01113         """
    01114         arch = "None"
    01115         # check architecture name
    01116         if "CMTCONFIG" in os.environ:
    01117             arch = os.environ["CMTCONFIG"]
    01118         elif "SCRAM_ARCH" in os.environ:
    01119             arch = os.environ["SCRAM_ARCH"]
    01120         return arch
    01121
    01122     def isWinPlatform(self):
    01123         """
    01124         Return True if the current platform is Windows.
    01125
    01126         This function was needed because of the change in the CMTCONFIG format,
    01127         from win32_vc71_dbg to i686-winxp-vc9-dbg.
    01128         """
    01129         platform = self.GetPlatform()
    01130         return "winxp" in platform or platform.startswith("win")
    01131
    01132     def _expandReferenceFileName(self, reffile):
    01133         # if no file is passed, do nothing
    01134         if not reffile:
    01135             return ""
    01136
    01137         # function to split an extension in constituents parts
    01138         platformSplit = lambda p: set(p.split('-' in p and '-' or '_'))
    01139
    01140         reference = os.path.normpath(os.path.expandvars(reffile))
    01141         # old-style platform-specific reference name
    01142         spec_ref = reference[:-3] + self.GetPlatform()[0:3] + reference[-3:]
    01143         if os.path.isfile(spec_ref):
    01144             reference = spec_ref
    01145         else: # look for new-style platform specific reference files:
    01146             # get all the files whose name start with the reference filename
    01147             dirname, basename = os.path.split(reference)
    01148             if not dirname: dirname = '.'
    01149             head = basename + "."
    01150             head_len = len(head)
    01151             platform = platformSplit(self.GetPlatform())
    01152             candidates = []
    01153             for f in os.listdir(dirname):
    01154                 if f.startswith(head):
    01155                     req_plat = platformSplit(f[head_len:])
    01156                     if platform.issuperset(req_plat):
    01157                         candidates.append( (len(req_plat), f) )
    01158             if candidates: # take the one with highest matching
    01159                 # FIXME: it is not possible to say if x86_64-slc5-gcc43-dbg
    01160                 #        has to use ref.x86_64-gcc43 or ref.slc5-dbg
    01161                 candidates.sort()
    01162                 reference = os.path.join(dirname, candidates[-1][1])
    01163         return reference
    01164
    01165     def CheckTTreesSummaries(self, stdout, result, causes,
    01166                              trees_dict = None,
    01167                              ignore = r"Basket|.*size|Compression"):
    01168         """
    01169         Compare the TTree summaries in stdout with the ones in trees_dict or in
    01170         the reference file. By default ignore the size, compression and basket
    01171         fields.
    01172         The presence of TTree summaries when none is expected is not a failure.
    01173         """
    01174         if trees_dict is None:
    01175             reference = self._expandReferenceFileName(self.reference)
    01176             # call the validator if the file exists
    01177             if reference and os.path.isfile(reference):
    01178                 trees_dict = findTTreeSummaries(open(reference).read())
    01179             else:
    01180                 trees_dict = {}
    01181
    01182         from pprint import PrettyPrinter
    01183         pp = PrettyPrinter()
    01184         if trees_dict:
    01185             result["GaudiTest.TTrees.expected"] = result.Quote(pp.pformat(trees_dict))
    01186             if ignore:
    01187                 result["GaudiTest.TTrees.ignore"] = result.Quote(ignore)
    01188
    01189         trees = findTTreeSummaries(stdout)
    01190         failed = cmpTreesDicts(trees_dict, trees, ignore)
    01191         if failed:
    01192             causes.append("trees summaries")
    01193             msg = "%s: %s != %s" % getCmpFailingValues(trees_dict, trees, failed)
    01194             result["GaudiTest.TTrees.failure_on"] = result.Quote(msg)
    01195             result["GaudiTest.TTrees.found"] = result.Quote(pp.pformat(trees))
    01196
    01197         return causes
    01198
    01199     def CheckHistosSummaries(self, stdout, result, causes,
    01200                              dict = None,
    01201                              ignore = None):
    01202         """
    01203         Compare the TTree summaries in stdout with the ones in trees_dict or in
    01204         the reference file. By default ignore the size, compression and basket
    01205         fields.
    01206         The presence of TTree summaries when none is expected is not a failure.
    01207         """
    01208         if dict is None:
    01209             reference = self._expandReferenceFileName(self.reference)
    01210             # call the validator if the file exists
    01211             if reference and os.path.isfile(reference):
    01212                 dict = findHistosSummaries(open(reference).read())
    01213             else:
    01214                 dict = {}
    01215
    01216         from pprint import PrettyPrinter
    01217         pp = PrettyPrinter()
    01218         if dict:
    01219             result["GaudiTest.Histos.expected"] = result.Quote(pp.pformat(dict))
    01220             if ignore:
    01221                 result["GaudiTest.Histos.ignore"] = result.Quote(ignore)
    01222
    01223         histos = findHistosSummaries(stdout)
    01224         failed = cmpTreesDicts(dict, histos, ignore)
    01225         if failed:
    01226             causes.append("histos summaries")
    01227             msg = "%s: %s != %s" % getCmpFailingValues(dict, histos, failed)
    01228             result["GaudiTest.Histos.failure_on"] = result.Quote(msg)
    01229             result["GaudiTest.Histos.found"] = result.Quote(pp.pformat(histos))
    01230
    01231         return causes
    01232
    01233     def ValidateWithReference(self, stdout, stderr, result, causes, preproc = None):
    01234         """
    01235         Default validation action: compare standard output and error to the
    01236         reference files.
    01237         """
    01238         # set the default output preprocessor
    01239         if preproc is None:
    01240             preproc = normalizeExamples
    01241         # check standard output
    01242         reference = self._expandReferenceFileName(self.reference)
    01243         # call the validator if the file exists
    01244         if reference and os.path.isfile(reference):
    01245             result["GaudiTest.output_reference"] = reference
    01246             causes += ReferenceFileValidator(reference,
    01247                                              "standard output",
    01248                                              "GaudiTest.output_diff",
    01249                                              preproc = preproc)(stdout, result)
    01250
    01251         # Compare TTree summaries
    01252         causes = self.CheckTTreesSummaries(stdout, result, causes)
    01253         causes = self.CheckHistosSummaries(stdout, result, causes)
    01254
    01255         if causes: # Write a new reference file for stdout
    01256             try:
    01257                 newref = open(reference + ".new","w")
    01258                 # sanitize newlines
    01259                 for l in stdout.splitlines():
    01260                     newref.write(l.rstrip() + '\n')
    01261                 del newref # flush and close
    01262             except IOError:
    01263                 # Ignore IO errors when trying to update reference files
    01264                 # because we may be in a read-only filesystem
    01265                 pass
    01266
    01267         # check standard error
    01268         reference = self._expandReferenceFileName(self.error_reference)
    01269         # call the validator if we have a file to use
    01270         if reference and os.path.isfile(reference):
    01271             result["GaudiTest.error_reference"] = reference
    01272             newcauses = ReferenceFileValidator(reference,
    01273                                                "standard error",
    01274                                                "GaudiTest.error_diff",
    01275                                                preproc = preproc)(stderr, result)
    01276             causes += newcauses
    01277             if newcauses: # Write a new reference file for stdedd
    01278                 newref = open(reference + ".new","w")
    01279                 # sanitize newlines
    01280                 for l in stderr.splitlines():
    01281                     newref.write(l.rstrip() + '\n')
    01282                 del newref # flush and close
    01283         else:
    01284             causes += BasicOutputValidator(self.stderr,
    01285                                            "standard error",
    01286                                            "ExecTest.expected_stderr")(stderr, result)
    01287
    01288         return causes
    01289
    01290     def ValidateOutput(self, stdout, stderr, result):
    01291         causes = []
    01292         # if the test definition contains a custom validator, use it
    01293         if self.validator.strip() != "":
    01294             class CallWrapper(object):
    01295                 """
    01296                 Small wrapper class to dynamically bind some default arguments
    01297                 to a callable.
    01298                 """
    01299                 def __init__(self, callable, extra_args = {}):
    01300                     self.callable = callable
    01301                     self.extra_args = extra_args
    01302                     # get the list of names of positional arguments
    01303                     from inspect import getargspec
    01304                     self.args_order = getargspec(callable)[0]
    01305                     # Remove "self" from the list of positional arguments
    01306                     # since it is added automatically
    01307                     if self.args_order[0] == "self":
    01308                         del self.args_order[0]
    01309                 def __call__(self, *args, **kwargs):
    01310                     # Check which positional arguments are used
    01311                     positional = self.args_order[:len(args)]
    01312
    01313                     kwargs = dict(kwargs) # copy the arguments dictionary
    01314                     for a in self.extra_args:
    01315                         # use "extra_args" for the arguments not specified as
    01316                         # positional or keyword
    01317                         if a not in positional and a not in kwargs:
    01318                             kwargs[a] = self.extra_args[a]
    01319                     return apply(self.callable, args, kwargs)
    01320             # local names to be exposed in the script
    01321             exported_symbols = {"self":self,
    01322                                 "stdout":stdout,
    01323                                 "stderr":stderr,
    01324                                 "result":result,
    01325                                 "causes":causes,
    01326                                 "findReferenceBlock":
    01327                                     CallWrapper(findReferenceBlock, {"stdout":stdout,
    01328                                                                      "result":result,
    01329                                                                      "causes":causes}),
    01330                                 "validateWithReference":
    01331                                     CallWrapper(self.ValidateWithReference, {"stdout":stdout,
    01332                                                                              "stderr":stderr,
    01333                                                                              "result":result,
    01334                                                                              "causes":causes}),
    01335                                 "countErrorLines":
    01336                                     CallWrapper(countErrorLines, {"stdout":stdout,
    01337                                                                   "result":result,
    01338                                                                   "causes":causes}),
    01339                                 "checkTTreesSummaries":
    01340                                     CallWrapper(self.CheckTTreesSummaries, {"stdout":stdout,
    01341                                                                             "result":result,
    01342                                                                             "causes":causes}),
    01343                                 "checkHistosSummaries":
    01344                                     CallWrapper(self.CheckHistosSummaries, {"stdout":stdout,
    01345                                                                             "result":result,
    01346                                                                             "causes":causes}),
    01347
    01348                                 }
    01349             exec self.validator in globals(), exported_symbols
    01350         else:
    01351             self.ValidateWithReference(stdout, stderr, result, causes)
    01352
    01353         return causes
    01354
    01355     def DumpEnvironment(self, result):
    01356         """
    01357         Add the content of the environment to the result object.
    01358
    01359         Copied from the QMTest class of COOL.
    01360         """
    01361         vars = os.environ.keys()
    01362         vars.sort()
    01363         result['GaudiTest.environment'] = \
    01364             result.Quote('\n'.join(["%s=%s"%(v,os.environ[v]) for v in vars]))
    01365
    01366     def Run(self, context, result):
    01367         """Run the test.
    01368
    01369         'context' -- A 'Context' giving run-time parameters to the
    01370         test.
    01371
    01372         'result' -- A 'Result' object.  The outcome will be
    01373         'Result.PASS' when this method is called.  The 'result' may be
    01374         modified by this method to indicate outcomes other than
    01375         'Result.PASS' or to add annotations."""
    01376
    01377         # Check if the platform is supported
    01378         if self.PlatformIsNotSupported(context, result):
    01379             return
    01380
    01381         # Prepare program name and arguments (expanding variables, and converting to absolute)
    01382         if self.program:
    01383             prog = rationalizepath(self.program)
    01384         elif "GAUDIEXE" in os.environ:
    01385             prog = os.environ["GAUDIEXE"]
    01386         else:
    01387             prog = "Gaudi.exe"
    01388         self.program = prog
    01389
    01390         dummy, prog_ext = os.path.splitext(prog)
    01391         if prog_ext not in [ ".exe", ".py", ".bat" ] and self.isWinPlatform():
    01392             prog += ".exe"
    01393             prog_ext = ".exe"
    01394
    01395         prog = which(prog) or prog
    01396
    01397         # Convert paths to absolute paths in arguments and reference files
    01398         args = map(rationalizepath, self.args)
    01399         self.reference = rationalizepath(self.reference)
    01400         self.error_reference = rationalizepath(self.error_reference)
    01401
    01402
    01403         # check if the user provided inline options
    01404         tmpfile = None
    01405         if self.options.strip():
    01406             ext = ".opts"
    01407             if re.search(r"from\s*Gaudi.Configuration\s*import\s*\*", self.options):
    01408                 ext = ".py"
    01409             tmpfile = TempFile(ext)
    01410             tmpfile.writelines("\n".join(self.options.splitlines()))
    01411             tmpfile.flush()
    01412             args.append(tmpfile.name)
    01413             result["GaudiTest.options"] = result.Quote(self.options)
    01414
    01415         # if the program is a python file, execute it through python
    01416         if prog_ext == ".py":
    01417             args.insert(0,prog)
    01418             if self.isWinPlatform():
    01419                 prog = which("python.exe") or "python.exe"
    01420             else:
    01421                 prog = which("python") or "python"
    01422
    01423         # Change to the working directory if specified or to the default temporary
    01424         origdir = os.getcwd()
    01425         if self.workdir:
    01426             os.chdir(str(os.path.normpath(os.path.expandvars(self.workdir))))
    01427         elif self.use_temp_dir == "true":
    01428             if "QMTEST_TMPDIR" in os.environ:
    01429                 os.chdir(os.environ["QMTEST_TMPDIR"])
    01430             elif "qmtest.tmpdir" in context:
    01431                 os.chdir(context["qmtest.tmpdir"])
    01432
    01433         if "QMTEST_IGNORE_TIMEOUT" not in os.environ:
    01434             self.timeout = max(self.timeout,600)
    01435         else:
    01436             self.timeout = -1
    01437
    01438         try:
    01439             # Generate eclipse.org debug launcher for the test
    01440             self._CreateEclipseLaunch(prog, args, destdir = origdir)
    01441             # Run the test
    01442             self.RunProgram(prog,
    01443                             [ prog ] + args,
    01444                             context, result)
    01445             # Record the content of the enfironment for failing tests
    01446             if result.GetOutcome() not in [ result.PASS ]:
    01447                 self.DumpEnvironment(result)
    01448         finally:
    01449             # revert to the original directory
    01450             os.chdir(origdir)
    01451
    01452     def RunProgram(self, program, arguments, context, result):
    01453         """Run the 'program'.
    01454
    01455         'program' -- The path to the program to run.
    01456
    01457         'arguments' -- A list of the arguments to the program.  This
    01458         list must contain a first argument corresponding to 'argv[0]'.
    01459
    01460         'context' -- A 'Context' giving run-time parameters to the
    01461         test.
    01462
    01463         'result' -- A 'Result' object.  The outcome will be
    01464         'Result.PASS' when this method is called.  The 'result' may be
    01465         modified by this method to indicate outcomes other than
    01466         'Result.PASS' or to add annotations.
    01467
    01468         @attention: This method has been copied from command.ExecTestBase
    01469                     (QMTest 2.3.0) and modified to keep stdout and stderr
    01470                     for tests that have been terminated by a signal.
    01471                     (Fundamental for debugging in the Application Area)
    01472         """
    01473
    01474         # Construct the environment.
    01475         environment = self.MakeEnvironment(context)
    01476         # Create the executable.
    01477         if self.timeout >= 0:
    01478             timeout = self.timeout
    01479         else:
    01480             # If no timeout was specified, we sill run this process in a
    01481             # separate process group and kill the entire process group
    01482             # when the child is done executing.  That means that
    01483             # orphaned child processes created by the test will be
    01484             # cleaned up.
    01485             timeout = -2
    01486         e = GaudiFilterExecutable(self.stdin, timeout)
    01487         # Run it.
    01488         exit_status = e.Run(arguments, environment, path = program)
    01489         # Get the stack trace from the temporary file (if present)
    01490         if e.stack_trace_file and os.path.exists(e.stack_trace_file):
    01491             stack_trace = open(e.stack_trace_file).read()
    01492             os.remove(e.stack_trace_file)
    01493         else:
    01494             stack_trace = None
    01495         if stack_trace:
    01496             result["ExecTest.stack_trace"] = result.Quote(stack_trace)
    01497
    01498         # If the process terminated normally, check the outputs.
    01499         if sys.platform == "win32" or os.WIFEXITED(exit_status):
    01500             # There are no causes of failure yet.
    01501             causes = []
    01502             # The target program terminated normally.  Extract the
    01503             # exit code, if this test checks it.
    01504             if self.exit_code is None:
    01505                 exit_code = None
    01506             elif sys.platform == "win32":
    01507                 exit_code = exit_status
    01508             else:
    01509                 exit_code = os.WEXITSTATUS(exit_status)
    01510             # Get the output generated by the program.
    01511             stdout = e.stdout
    01512             stderr = e.stderr
    01513             # Record the results.
    01514             result["ExecTest.exit_code"] = str(exit_code)
    01515             result["ExecTest.stdout"] = result.Quote(stdout)
    01516             result["ExecTest.stderr"] = result.Quote(stderr)
    01517             # Check to see if the exit code matches.
    01518             if exit_code != self.exit_code:
    01519                 causes.append("exit_code")
    01520                 result["ExecTest.expected_exit_code"] \
    01521                     = str(self.exit_code)
    01522             # Validate the output.
    01523             causes += self.ValidateOutput(stdout, stderr, result)
    01524             # If anything went wrong, the test failed.
    01525             if causes:
    01526                 result.Fail("Unexpected %s." % string.join(causes, ", "))
    01527         elif os.WIFSIGNALED(exit_status):
    01528             # The target program terminated with a signal.  Construe
    01529             # that as a test failure.
    01530             signal_number = str(os.WTERMSIG(exit_status))
    01531             if not stack_trace:
    01532                 result.Fail("Program terminated by signal.")
    01533             else:
    01534                 # The presence of stack_trace means tha we stopped the job because
    01535                 # of a time-out
    01536                 result.Fail("Exceeded time limit (%ds), terminated." % timeout)
    01537             result["ExecTest.signal_number"] = signal_number
    01538             result["ExecTest.stdout"] = result.Quote(e.stdout)
    01539             result["ExecTest.stderr"] = result.Quote(e.stderr)
    01540         elif os.WIFSTOPPED(exit_status):
    01541             # The target program was stopped.  Construe that as a
    01542             # test failure.
    01543             signal_number = str(os.WSTOPSIG(exit_status))
    01544             if not stack_trace:
    01545                 result.Fail("Program stopped by signal.")
    01546             else:
    01547                 # The presence of stack_trace means tha we stopped the job because
    01548                 # of a time-out
    01549                 result.Fail("Exceeded time limit (%ds), stopped." % timeout)
    01550             result["ExecTest.signal_number"] = signal_number
    01551             result["ExecTest.stdout"] = result.Quote(e.stdout)
    01552             result["ExecTest.stderr"] = result.Quote(e.stderr)
    01553         else:
    01554             # The target program terminated abnormally in some other
    01555             # manner.  (This shouldn't normally happen...)
    01556             result.Fail("Program did not terminate normally.")
    01557
    01558         # Marco Cl.: This is a special trick to fix a "problem" with the output
    01559         # of gaudi jobs when they use colors
    01560         esc = '\x1b'
    01561         repr_esc = '\\x1b'
    01562         result["ExecTest.stdout"] = result["ExecTest.stdout"].replace(esc,repr_esc)
    01563         # TODO: (MCl) improve the hack for colors in standard output
    01564         #             may be converting them to HTML tags
    01565
    01566     def _CreateEclipseLaunch(self, prog, args, destdir = None):
    01567         # Find the project name used in ecplise.
    01568         # The name is in a file called ".project" in one of the parent directories
    01569         projbasedir = os.path.normpath(destdir)
    01570         while not os.path.exists(os.path.join(projbasedir, ".project")):
    01571             oldprojdir = projbasedir
    01572             projbasedir = os.path.normpath(os.path.join(projbasedir, os.pardir))
    01573             # FIXME: the root level is invariant when trying to go up one level,
    01574             #        but it must be cheched on windows
    01575             if oldprojdir == projbasedir:
    01576                 # If we cannot find a .project, so no point in creating a .launch file
    01577                 return
    01578         # Use ElementTree to parse the XML file
    01579         from xml.etree import ElementTree as ET
    01580         t = ET.parse(os.path.join(projbasedir, ".project"))
    01581         projectName = t.find("name").text
    01582
    01583         # prepare the name/path of the generated file
    01584         destfile = "%s.launch" % self._Runnable__id
    01585         if destdir:
    01586             destfile = os.path.join(destdir, destfile)
    01587
    01588         if self.options.strip():
    01589             # this means we have some custom options in the qmt file, so we have
    01590             # to copy them from the temporary file at the end of the arguments
    01591             # in another file
    01592             tempfile = args.pop()
    01593             optsfile = destfile + os.path.splitext(tempfile)[1]
    01594             shutil.copyfile(tempfile, optsfile)
    01595             args.append(optsfile)
    01596
    01597         # prepare the data to insert in the XML file
    01598         from xml.sax.saxutils import quoteattr # useful to quote XML special chars
    01599         data = {}
    01600         # Note: the "quoteattr(k)" is not needed because special chars cannot be part of a variable name,
    01601         # but it doesn't harm.
    01602         data["environment"] = "\n".join(['<mapEntry key=%s value=%s/>' % (quoteattr(k), quoteattr(v))
    01603                                          for k, v in os.environ.iteritems()])
    01604
    01605         data["exec"] = which(prog) or prog
    01606         if os.path.basename(data["exec"]).lower().startswith("python"):
    01607             data["stopAtMain"] = "false" # do not stop at main when debugging Python scripts
    01608         else:
    01609             data["stopAtMain"] = "true"
    01610
    01611         data["args"] = "&#10;".join(map(rationalizepath, args))
    01612         if self.isWinPlatform():
    01613             data["args"] = "&#10;".join(["/debugexe"] + map(rationalizepath, [data["exec"]] + args))
    01614             data["exec"] = which("vcexpress.exe")
    01615
    01616         if not self.use_temp_dir:
    01617             data["workdir"] = os.getcwd()
    01618         else:
    01619             # If the test is using a tmporary directory, it is better to run it
    01620             # in the same directory as the .launch file when debugged in eclipse
    01621             data["workdir"] = destdir
    01622
    01623         data["project"] = projectName.strip()
    01624
    01625         # Template for the XML file, based on eclipse 3.4
    01626         xml = """<?xml version="1.0" encoding="UTF-8" standalone="no"?>
    01627 <launchConfiguration type="org.eclipse.cdt.launch.applicationLaunchType">
    01628 <booleanAttribute key="org.eclipse.cdt.debug.mi.core.AUTO_SOLIB" value="true"/>
    01629 <listAttribute key="org.eclipse.cdt.debug.mi.core.AUTO_SOLIB_LIST"/>
    01630 <stringAttribute key="org.eclipse.cdt.debug.mi.core.DEBUG_NAME" value="gdb"/>
    01631 <stringAttribute key="org.eclipse.cdt.debug.mi.core.GDB_INIT" value=".gdbinit"/>
    01632 <listAttribute key="org.eclipse.cdt.debug.mi.core.SOLIB_PATH"/>
    01633 <booleanAttribute key="org.eclipse.cdt.debug.mi.core.STOP_ON_SOLIB_EVENTS" value="false"/>
    01634 <booleanAttribute key="org.eclipse.cdt.debug.mi.core.breakpointsFullPath" value="false"/>
    01635 <stringAttribute key="org.eclipse.cdt.debug.mi.core.commandFactory" value="org.eclipse.cdt.debug.mi.core.standardCommandFactory"/>
    01636 <stringAttribute key="org.eclipse.cdt.debug.mi.core.protocol" value="mi"/>
    01637 <booleanAttribute key="org.eclipse.cdt.debug.mi.core.verboseMode" value="false"/>
    01638 <intAttribute key="org.eclipse.cdt.launch.ATTR_BUILD_BEFORE_LAUNCH_ATTR" value="0"/>
    01639 <stringAttribute key="org.eclipse.cdt.launch.COREFILE_PATH" value=""/>
    01640 <stringAttribute key="org.eclipse.cdt.launch.DEBUGGER_ID" value="org.eclipse.cdt.debug.mi.core.CDebuggerNew"/>
    01641 <stringAttribute key="org.eclipse.cdt.launch.DEBUGGER_REGISTER_GROUPS" value=""/>
    01642 <stringAttribute key="org.eclipse.cdt.launch.DEBUGGER_START_MODE" value="run"/>
    01643 <booleanAttribute key="org.eclipse.cdt.launch.DEBUGGER_STOP_AT_MAIN" value="%(stopAtMain)s"/>
    01644 <stringAttribute key="org.eclipse.cdt.launch.DEBUGGER_STOP_AT_MAIN_SYMBOL" value="main"/>
    01645 <booleanAttribute key="org.eclipse.cdt.launch.ENABLE_REGISTER_BOOKKEEPING" value="false"/>
    01646 <booleanAttribute key="org.eclipse.cdt.launch.ENABLE_VARIABLE_BOOKKEEPING" value="false"/>
    01647 <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;"/>
    01648 <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;"/>
    01649 <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;"/>
    01650 <stringAttribute key="org.eclipse.cdt.launch.PROGRAM_ARGUMENTS" value="%(args)s"/>
    01651 <stringAttribute key="org.eclipse.cdt.launch.PROGRAM_NAME" value="%(exec)s"/>
    01652 <stringAttribute key="org.eclipse.cdt.launch.PROJECT_ATTR" value="%(project)s"/>
    01653 <stringAttribute key="org.eclipse.cdt.launch.PROJECT_BUILD_CONFIG_ID_ATTR" value=""/>
    01654 <stringAttribute key="org.eclipse.cdt.launch.WORKING_DIRECTORY" value="%(workdir)s"/>
    01655 <booleanAttribute key="org.eclipse.cdt.launch.ui.ApplicationCDebuggerTab.DEFAULTS_SET" value="true"/>
    01656 <booleanAttribute key="org.eclipse.cdt.launch.use_terminal" value="true"/>
    01657 <listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_PATHS">
    01658 <listEntry value="/%(project)s"/>
    01659 </listAttribute>
    01660 <listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_TYPES">
    01661 <listEntry value="4"/>
    01662 </listAttribute>
    01663 <booleanAttribute key="org.eclipse.debug.core.appendEnvironmentVariables" value="false"/>
    01664 <mapAttribute key="org.eclipse.debug.core.environmentVariables">
    01665 %(environment)s
    01666 </mapAttribute>
    01667 <mapAttribute key="org.eclipse.debug.core.preferred_launchers">
    01668 <mapEntry key="[debug]" value="org.eclipse.cdt.cdi.launch.localCLaunch"/>
    01669 </mapAttribute>
    01670 <listAttribute key="org.eclipse.debug.ui.favoriteGroups">
    01671 <listEntry value="org.eclipse.debug.ui.launchGroup.debug"/>
    01672 </listAttribute>
    01673 </launchConfiguration>
    01674 """ % data
    01675
    01676         # Write the output file
    01677         open(destfile, "w").write(xml)
    01678         #open(destfile + "_copy.xml", "w").write(xml)
    01679
    01680
    01681 try:
    01682     import json
    01683 except ImportError:
    01684     # Use simplejson for LCG
    01685     import simplejson as json
    01686
    01687 class HTMLResultStream(ResultStream):
    01688     """An 'HTMLResultStream' writes its output to a set of HTML files.
    01689
    01690     The argument 'dir' is used to select the destination directory for the HTML
    01691     report.
    01692     The destination directory may already contain the report from a previous run
    01693     (for example of a different package), in which case it will be extended to
    01694     include the new data.
    01695     """
    01696     arguments = [
    01697         qm.fields.TextField(
    01698             name = "dir",
    01699             title = "Destination Directory",
    01700             description = """The name of the directory.
    01701
    01702             All results will be written to the directory indicated.""",
    01703             verbatim = "true",
    01704             default_value = ""),
    01705     ]
    01706
    01707     def __init__(self, arguments = None, **args):
    01708         """Prepare the destination directory.
    01709
    01710         Creates the destination directory and store in it some preliminary
    01711         annotations and the static files found in the template directory
    01712         'html_report'.
    01713         """
    01714         ResultStream.__init__(self, arguments, **args)
    01715         self._summary = []
    01716         self._summaryFile = os.path.join(self.dir, "summary.json")
    01717         self._annotationsFile = os.path.join(self.dir, "annotations.json")
    01718         # Prepare the destination directory using the template
    01719         templateDir = os.path.join(os.path.dirname(__file__), "html_report")
    01720         if not os.path.isdir(self.dir):
    01721             os.makedirs(self.dir)
    01722         # Copy the files in the template directory excluding the directories
    01723         for f in os.listdir(templateDir):
    01724             src = os.path.join(templateDir, f)
    01725             dst = os.path.join(self.dir, f)
    01726             if not os.path.isdir(src) and not os.path.exists(dst):
    01727                 shutil.copy(src, dst)
    01728         # Add some non-QMTest attributes
    01729         if "CMTCONFIG" in os.environ:
    01730             self.WriteAnnotation("cmt.cmtconfig", os.environ["CMTCONFIG"])
    01731         import socket
    01732         self.WriteAnnotation("hostname", socket.gethostname())
    01733
    01734     def _updateSummary(self):
    01735         """Helper function to extend the global summary file in the destination
    01736         directory.
    01737         """
    01738         if os.path.exists(self._summaryFile):
    01739             oldSummary = json.load(open(self._summaryFile))
    01740         else:
    01741             oldSummary = []
    01742         ids = set([ i["id"] for i in self._summary ])
    01743         newSummary = [ i for i in oldSummary if i["id"] not in ids ]
    01744         newSummary.extend(self._summary)
    01745         json.dump(newSummary, open(self._summaryFile, "w"),
    01746                   sort_keys = True)
    01747
    01748     def WriteAnnotation(self, key, value):
    01749         """Writes the annotation to the annotation file.
    01750         If the key is already present with a different value, the value becomes
    01751         a list and the new value is appended to it, except for start_time and
    01752         end_time.
    01753         """
    01754         # Initialize the annotation dict from the file (if present)
    01755         if os.path.exists(self._annotationsFile):
    01756             annotations = json.load(open(self._annotationsFile))
    01757         else:
    01758             annotations = {}
    01759         # hack because we do not have proper JSON support
    01760         key, value = map(str, [key, value])
    01761         if key == "qmtest.run.start_time":
    01762             # Special handling of the start time:
    01763             # if we are updating a result, we have to keep the original start
    01764             # time, but remove the original end time to mark the report to be
    01765             # in progress.
    01766             if key not in annotations:
    01767                 annotations[key] = value
    01768             if "qmtest.run.end_time" in annotations:
    01769                 del annotations["qmtest.run.end_time"]
    01770         else:
    01771             # All other annotations are added to a list
    01772             if key in annotations:
    01773                 old = annotations[key]
    01774                 if type(old) is list:
    01775                     if value not in old:
    01776                         annotations[key].append(value)
    01777                 elif value != old:
    01778                     annotations[key] = [old, value]
    01779             else:
    01780                 annotations[key] = value
    01781         # Write the new annotations file
    01782         json.dump(annotations, open(self._annotationsFile, "w"),
    01783                   sort_keys = True)
    01784
    01785     def WriteResult(self, result):
    01786         """Prepare the test result directory in the destination directory storing
    01787         into it the result fields.
    01788         A summary of the test result is stored both in a file in the test directory
    01789         and in the global summary file.
    01790         """
    01791         summary = {}
    01792         summary["id"] = result.GetId()
    01793         summary["outcome"] = result.GetOutcome()
    01794         summary["cause"] = result.GetCause()
    01795         summary["fields"] = result.keys()
    01796         summary["fields"].sort()
    01797
    01798         # Since we miss proper JSON support, I hack a bit
    01799         for f in ["id", "outcome", "cause"]:
    01800             summary[f] = str(summary[f])
    01801         summary["fields"] = map(str, summary["fields"])
    01802
    01803         self._summary.append(summary)
    01804
    01805         # format:
    01806         # testname/summary.json
    01807         # testname/field1
    01808         # testname/field2
    01809         testOutDir = os.path.join(self.dir, summary["id"])
    01810         if not os.path.isdir(testOutDir):
    01811             os.makedirs(testOutDir)
    01812         json.dump(summary, open(os.path.join(testOutDir, "summary.json"), "w"),
    01813                   sort_keys = True)
    01814         for f in summary["fields"]:
    01815             open(os.path.join(testOutDir, f), "w").write(result[f])
    01816
    01817         self._updateSummary()
    01818
    01819     def Summarize(self):
    01820         # Not implemented.
    01821         pass

All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Friends Defines