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