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