00001
00002
00003
00004
00005 __author__ = 'Marco Clemencic CERN/PH-LBC'
00006
00007
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
00023 import qm.executable
00024 import time, signal
00025
00026
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
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
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
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
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
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
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
00226 olddir = os.getcwd()
00227 os.chdir(self.path)
00228
00229 result = os.popen4(cmd)[1].read()
00230
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
00261
00262
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
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
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
00324
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
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
00415 skipEmptyLines.__processLine__ = lambda line: (line.strip() and line) or None
00416
00417
00418
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
00433 normalizeExamples = maskPointers + normalizeDate
00434 for w,o,r in [
00435
00436 ("TIMER.TIMER",r"\s+[+-]?[0-9]+[0-9.]*", " 0"),
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"),
00441
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
00444 ("ServiceLocatorHelper::", "ServiceLocatorHelper::(create|locate)Service", "ServiceLocatorHelper::service"),
00445
00446 (None, r"e([-+])0([0-9][0-9])", r"e\1\2"),
00447 ]:
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",
00462 "DEBUG Service base class initialized successfully",
00463 "DEBUG Incident timing:",
00464 "INFO 'CnvServices':[",
00465
00466 'Note: (file "(tmpfile)", line 2) File "set" already loaded',
00467
00468 'SIGXCPU',
00469 ],regexps = [
00470 r"^JobOptionsSvc INFO *$",
00471 r"^#",
00472 r"(Always|SUCCESS)\s*(Root f|[^ ]* F)ile version:",
00473 r"0x[0-9a-fA-F#]+ *Algorithm::sysInitialize\(\) *\[",
00474 r"0x[0-9a-fA-F#]* *__gxx_personality_v0 *\[",
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
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
00488 r"ERROR Failed to modify file: .* Errno=2 No such file or directory",
00489
00490 r"^ +[0-9]+ \|.*ROOT",
00491 r"^ +[0-9]+ \|.*\|.*Dict",
00492
00493 r"^\*.*\*$",
00494
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
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
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
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
00552 reflines = filter(None,map(lambda s: s.rstrip(), reference.splitlines()))
00553 if not reflines:
00554 raise RuntimeError("Empty (or null) reference")
00555
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
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
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
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
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
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:
00680
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
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
00705 for k in keys:
00706 if k in to_check:
00707 if (type(reference[k]) is dict) and (type(to_check[k]) is dict):
00708
00709 failed = fail_keys = cmpTreesDicts(reference[k], to_check[k], ignore)
00710 else:
00711
00712 failed = to_check[k] != reference[k]
00713 else:
00714 to_check[k] = None
00715 failed = True
00716 if failed:
00717 fail_keys.insert(0, k)
00718 break
00719 return fail_keys
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
00729 return (fail_path, r, c)
00730
00731
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
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)
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
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:
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
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
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
00833
00834
00835 tmpf = tempfile.mkstemp()
00836 os.close(tmpf[0])
00837 self.stack_trace_file = tmpf[1]
00838
00839 def __UseSeparateProcessGroupForChild(self):
00840 """Copied from TimeoutExecutable to allow the re-implementation of
00841 _HandleChild.
00842 """
00843 if sys.platform == "win32":
00844
00845
00846
00847
00848 return 0
00849
00850 return self.__timeout >= 0 or self.__timeout == -2
00851
00852
00853 def _HandleChild(self):
00854 """Code copied from both FilterExecutable and TimeoutExecutable.
00855 """
00856
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
00865
00866
00867
00868
00869
00870 super(qm.executable.TimeoutExecutable, self)._HandleChild()
00871
00872 if self.__UseSeparateProcessGroupForChild():
00873
00874
00875
00876
00877 child_pid = self._GetChildPID()
00878 try:
00879 os.setpgid(child_pid, child_pid)
00880 except:
00881
00882
00883
00884
00885 pass
00886
00887
00888
00889
00890
00891
00892
00893
00894
00895 self.__monitor_pid = os.fork()
00896 if self.__monitor_pid != 0:
00897
00898
00899
00900
00901 os.setpgid(self.__monitor_pid, child_pid)
00902 else:
00903
00904
00905
00906
00907 os.setpgid(0, child_pid)
00908
00909
00910
00911
00912
00913
00914
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
00927 time.sleep (self.__timeout)
00928
00929
00930 if sys.platform == "linux2":
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
00937
00938 o = os.popen(" ".join(cmd)).read()
00939 open(self.stack_trace_file,"w").write(o)
00940
00941
00942
00943 os.kill(0, signal.SIGKILL)
00944 else:
00945
00946 select.select ([], [], [])
00947 finally:
00948
00949
00950 os._exit(0)
00951 elif self.__timeout >= 0 and sys.platform == "win32":
00952
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
00965
00966
00967 timeout = int(self.__timeout * 1000)
00968
00969
00970 result = win32event.WaitForSingleObject(self._GetChildPID(),
00971 timeout)
00972
00973 if result == win32con.WAIT_TIMEOUT:
00974 self.Kill()
00975
00976
00977
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
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
01134 if not reffile:
01135 return ""
01136
01137
01138 platformSplit = lambda p: set(p.split('-' in p and '-' or '_'))
01139
01140 reference = os.path.normpath(os.path.expandvars(reffile))
01141
01142 spec_ref = reference[:-3] + self.GetPlatform()[0:3] + reference[-3:]
01143 if os.path.isfile(spec_ref):
01144 reference = spec_ref
01145 else:
01146
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:
01159
01160
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
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
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
01239 if preproc is None:
01240 preproc = normalizeExamples
01241
01242 reference = self._expandReferenceFileName(self.reference)
01243
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
01252 causes = self.CheckTTreesSummaries(stdout, result, causes)
01253 causes = self.CheckHistosSummaries(stdout, result, causes)
01254
01255 if causes:
01256 try:
01257 newref = open(reference + ".new","w")
01258
01259 for l in stdout.splitlines():
01260 newref.write(l.rstrip() + '\n')
01261 del newref
01262 except IOError:
01263
01264
01265 pass
01266
01267
01268 reference = self._expandReferenceFileName(self.error_reference)
01269
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:
01278 newref = open(reference + ".new","w")
01279
01280 for l in stderr.splitlines():
01281 newref.write(l.rstrip() + '\n')
01282 del newref
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
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
01303 from inspect import getargspec
01304 self.args_order = getargspec(callable)[0]
01305
01306
01307 if self.args_order[0] == "self":
01308 del self.args_order[0]
01309 def __call__(self, *args, **kwargs):
01310
01311 positional = self.args_order[:len(args)]
01312
01313 kwargs = dict(kwargs)
01314 for a in self.extra_args:
01315
01316
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
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
01378 if self.PlatformIsNotSupported(context, result):
01379 return
01380
01381
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
01398 args = map(rationalizepath, self.args)
01399 self.reference = rationalizepath(self.reference)
01400 self.error_reference = rationalizepath(self.error_reference)
01401
01402
01403
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
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
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
01440 self._CreateEclipseLaunch(prog, args, destdir = origdir)
01441
01442 self.RunProgram(prog,
01443 [ prog ] + args,
01444 context, result)
01445
01446 if result.GetOutcome() not in [ result.PASS ]:
01447 self.DumpEnvironment(result)
01448 finally:
01449
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
01475 environment = self.MakeEnvironment(context)
01476
01477 if self.timeout >= 0:
01478 timeout = self.timeout
01479 else:
01480
01481
01482
01483
01484
01485 timeout = -2
01486 e = GaudiFilterExecutable(self.stdin, timeout)
01487
01488 exit_status = e.Run(arguments, environment, path = program)
01489
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
01499 if sys.platform == "win32" or os.WIFEXITED(exit_status):
01500
01501 causes = []
01502
01503
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
01511 stdout = e.stdout
01512 stderr = e.stderr
01513
01514 result["ExecTest.exit_code"] = str(exit_code)
01515 result["ExecTest.stdout"] = result.Quote(stdout)
01516 result["ExecTest.stderr"] = result.Quote(stderr)
01517
01518 if exit_code != self.exit_code:
01519 causes.append("exit_code")
01520 result["ExecTest.expected_exit_code"] \
01521 = str(self.exit_code)
01522
01523 causes += self.ValidateOutput(stdout, stderr, result)
01524
01525 if causes:
01526 result.Fail("Unexpected %s." % string.join(causes, ", "))
01527 elif os.WIFSIGNALED(exit_status):
01528
01529
01530 signal_number = str(os.WTERMSIG(exit_status))
01531 if not stack_trace:
01532 result.Fail("Program terminated by signal.")
01533 else:
01534
01535
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
01542
01543 signal_number = str(os.WSTOPSIG(exit_status))
01544 if not stack_trace:
01545 result.Fail("Program stopped by signal.")
01546 else:
01547
01548
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
01555
01556 result.Fail("Program did not terminate normally.")
01557
01558
01559
01560 esc = '\x1b'
01561 repr_esc = '\\x1b'
01562 result["ExecTest.stdout"] = result["ExecTest.stdout"].replace(esc,repr_esc)
01563
01564
01565
01566 def _CreateEclipseLaunch(self, prog, args, destdir = None):
01567
01568
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
01574
01575 if oldprojdir == projbasedir:
01576
01577 return
01578
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
01584 destfile = "%s.launch" % self._Runnable__id
01585 if destdir:
01586 destfile = os.path.join(destdir, destfile)
01587
01588 if self.options.strip():
01589
01590
01591
01592 tempfile = args.pop()
01593 optsfile = destfile + os.path.splitext(tempfile)[1]
01594 shutil.copyfile(tempfile, optsfile)
01595 args.append(optsfile)
01596
01597
01598 from xml.sax.saxutils import quoteattr
01599 data = {}
01600
01601
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"
01608 else:
01609 data["stopAtMain"] = "true"
01610
01611 data["args"] = " ".join(map(rationalizepath, args))
01612 if self.isWinPlatform():
01613 data["args"] = " ".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
01620
01621 data["workdir"] = destdir
01622
01623 data["project"] = projectName.strip()
01624
01625
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="<?xml version="1.0" encoding="UTF-8" standalone="no"?><contentList/>"/>
01648 <stringAttribute key="org.eclipse.cdt.launch.GLOBAL_VARIABLES" value="<?xml version="1.0" encoding="UTF-8" standalone="no"?> <globalVariableList/> "/>
01649 <stringAttribute key="org.eclipse.cdt.launch.MEMORY_BLOCKS" value="<?xml version="1.0" encoding="UTF-8" standalone="no"?> <memoryBlockExpressionList/> "/>
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
01677 open(destfile, "w").write(xml)
01678
01679
01680
01681 try:
01682 import json
01683 except ImportError:
01684
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
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
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
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
01755 if os.path.exists(self._annotationsFile):
01756 annotations = json.load(open(self._annotationsFile))
01757 else:
01758 annotations = {}
01759
01760 key, value = map(str, [key, value])
01761 if key == "qmtest.run.start_time":
01762
01763
01764
01765
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
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
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
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
01806
01807
01808
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
01821 pass