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 branch = parseblock(lines[i:i+3])
00660 result["Branches"][branch["Name"]] = branch
00661 i += 4
00662
00663 return (result, i)
00664
00665 def findTTreeSummaries(stdout):
00666 """
00667 Scan stdout to find ROOT TTree summaries and digest them.
00668 """
00669 stars = re.compile(r"^\*+$")
00670 outlines = stdout.splitlines()
00671 nlines = len(outlines)
00672 trees = {}
00673
00674 i = 0
00675 while i < nlines:
00676
00677 while i < nlines and not stars.match(outlines[i]):
00678 i += 1
00679 if i < nlines:
00680 tree, i = _parseTTreeSummary(outlines, i)
00681 if tree:
00682 trees[tree["Name"]] = tree
00683
00684 return trees
00685
00686 def cmpTreesDicts(reference, to_check, ignore = None):
00687 """
00688 Check that all the keys in reference are in to_check too, with the same value.
00689 If the value is a dict, the function is called recursively. to_check can
00690 contain more keys than reference, that will not be tested.
00691 The function returns at the first difference found.
00692 """
00693 fail_keys = []
00694
00695 if ignore:
00696 ignore_re = re.compile(ignore)
00697 keys = [ key for key in reference if not ignore_re.match(key) ]
00698 else:
00699 keys = reference.keys()
00700
00701 for k in keys:
00702 if k in to_check:
00703 if (type(reference[k]) is dict) and (type(to_check[k]) is dict):
00704
00705 failed = fail_keys = cmpTreesDicts(reference[k], to_check[k], ignore)
00706 else:
00707
00708 failed = to_check[k] != reference[k]
00709 else:
00710 to_check[k] = None
00711 failed = True
00712 if failed:
00713 fail_keys.insert(0, k)
00714 break
00715 return fail_keys
00716
00717 def getCmpFailingValues(reference, to_check, fail_path):
00718 c = to_check
00719 r = reference
00720 for k in fail_path:
00721 c = c.get(k,None)
00722 r = r.get(k,None)
00723 if c is None or r is None:
00724 break
00725 return (fail_path, r, c)
00726
00727
00728 h_count_re = re.compile(r"^(.*)SUCCESS\s+Booked (\d+) Histogram\(s\) :\s+(.*)")
00729
00730 def parseHistosSummary(lines, pos):
00731 """
00732 Extract the histograms infos from the lines starting at pos.
00733 Returns the position of the first line after the summary block.
00734 """
00735 global h_count_re
00736 h_table_head = re.compile(r'SUCCESS\s+List of booked (1D|2D|3D|1D profile|2D profile) histograms in directory\s+"(\w*)"')
00737 h_short_summ = re.compile(r"ID=([^\"]+)\s+\"([^\"]+)\"\s+(.*)")
00738
00739 nlines = len(lines)
00740
00741
00742 m = h_count_re.search(lines[pos])
00743 name = m.group(1).strip()
00744 total = int(m.group(2))
00745 header = {}
00746 for k, v in [ x.split("=") for x in m.group(3).split() ]:
00747 header[k] = int(v)
00748 pos += 1
00749 header["Total"] = total
00750
00751 summ = {}
00752 while pos < nlines:
00753 m = h_table_head.search(lines[pos])
00754 if m:
00755 t, d = m.groups(1)
00756 t = t.replace(" profile", "Prof")
00757 pos += 1
00758 if pos < nlines:
00759 l = lines[pos]
00760 else:
00761 l = ""
00762 cont = {}
00763 if l.startswith(" | ID"):
00764
00765 titles = [ x.strip() for x in l.split("|")][1:]
00766 pos += 1
00767 while pos < nlines and lines[pos].startswith(" |"):
00768 l = lines[pos]
00769 values = [ x.strip() for x in l.split("|")][1:]
00770 hcont = {}
00771 for i in range(len(titles)):
00772 hcont[titles[i]] = values[i]
00773 cont[hcont["ID"]] = hcont
00774 pos += 1
00775 elif l.startswith(" ID="):
00776 while pos < nlines and lines[pos].startswith(" ID="):
00777 values = [ x.strip() for x in h_short_summ.search(lines[pos]).groups() ]
00778 cont[values[0]] = values
00779 pos += 1
00780 else:
00781 raise RuntimeError("Cannot understand line %d: '%s'" % (pos, l))
00782 if not d in summ:
00783 summ[d] = {}
00784 summ[d][t] = cont
00785 summ[d]["header"] = header
00786 else:
00787 break
00788 if not summ:
00789
00790 summ[name] = {"header": header}
00791 return summ, pos
00792
00793 def findHistosSummaries(stdout):
00794 """
00795 Scan stdout to find ROOT TTree summaries and digest them.
00796 """
00797 outlines = stdout.splitlines()
00798 nlines = len(outlines) - 1
00799 summaries = {}
00800 global h_count_re
00801
00802 pos = 0
00803 while pos < nlines:
00804 summ = {}
00805
00806 match = h_count_re.search(outlines[pos])
00807 while pos < nlines and not match:
00808 pos += 1
00809 match = h_count_re.search(outlines[pos])
00810 if match:
00811 summ, pos = parseHistosSummary(outlines, pos)
00812 summaries.update(summ)
00813 return summaries
00814
00815 class GaudiFilterExecutable(qm.executable.Filter):
00816 def __init__(self, input, timeout = -1):
00817 """Create a new 'Filter'.
00818
00819 'input' -- The string containing the input to provide to the
00820 child process.
00821
00822 'timeout' -- As for 'TimeoutExecutable.__init__'."""
00823
00824 super(GaudiFilterExecutable, self).__init__(input, timeout)
00825 self.__input = input
00826 self.__timeout = timeout
00827 self.stack_trace_file = None
00828
00829
00830
00831 tmpf = tempfile.mkstemp()
00832 os.close(tmpf[0])
00833 self.stack_trace_file = tmpf[1]
00834
00835 def __UseSeparateProcessGroupForChild(self):
00836 """Copied from TimeoutExecutable to allow the re-implementation of
00837 _HandleChild.
00838 """
00839 if sys.platform == "win32":
00840
00841
00842
00843
00844 return 0
00845
00846 return self.__timeout >= 0 or self.__timeout == -2
00847
00848
00849 def _HandleChild(self):
00850 """Code copied from both FilterExecutable and TimeoutExecutable.
00851 """
00852
00853 if self._stdin_pipe:
00854 self._ClosePipeEnd(self._stdin_pipe[0])
00855 if self._stdout_pipe:
00856 self._ClosePipeEnd(self._stdout_pipe[1])
00857 if self._stderr_pipe:
00858 self._ClosePipeEnd(self._stderr_pipe[1])
00859
00860
00861
00862
00863
00864
00865
00866 super(qm.executable.TimeoutExecutable, self)._HandleChild()
00867
00868 if self.__UseSeparateProcessGroupForChild():
00869
00870
00871
00872
00873 child_pid = self._GetChildPID()
00874 try:
00875 os.setpgid(child_pid, child_pid)
00876 except:
00877
00878
00879
00880
00881 pass
00882
00883
00884
00885
00886
00887
00888
00889
00890
00891 self.__monitor_pid = os.fork()
00892 if self.__monitor_pid != 0:
00893
00894
00895
00896
00897 os.setpgid(self.__monitor_pid, child_pid)
00898 else:
00899
00900
00901
00902
00903 os.setpgid(0, child_pid)
00904
00905
00906
00907
00908
00909
00910
00911 try:
00912 max_fds = os.sysconf("SC_OPEN_MAX")
00913 except:
00914 max_fds = 256
00915 for fd in xrange(max_fds):
00916 try:
00917 os.close(fd)
00918 except:
00919 pass
00920 try:
00921 if self.__timeout >= 0:
00922
00923 time.sleep (self.__timeout)
00924
00925
00926 if sys.platform == "linux2":
00927 cmd = ["gdb",
00928 os.path.join("/proc", str(child_pid), "exe"),
00929 str(child_pid),
00930 "-batch", "-n", "-x",
00931 "'%s'" % os.path.join(os.path.dirname(__file__), "stack-trace.gdb")]
00932
00933
00934 o = os.popen(" ".join(cmd)).read()
00935 open(self.stack_trace_file,"w").write(o)
00936
00937
00938
00939 os.kill(0, signal.SIGKILL)
00940 else:
00941
00942 select.select ([], [], [])
00943 finally:
00944
00945
00946 os._exit(0)
00947 elif self.__timeout >= 0 and sys.platform == "win32":
00948
00949 self.__monitor_thread = Thread(target = self.__Monitor)
00950 self.__monitor_thread.start()
00951
00952 if sys.platform == "win32":
00953
00954 def __Monitor(self):
00955 """Code copied from FilterExecutable.
00956 Kill the child if the timeout expires.
00957
00958 This function is run in the monitoring thread."""
00959
00960
00961
00962
00963 timeout = int(self.__timeout * 1000)
00964
00965
00966 result = win32event.WaitForSingleObject(self._GetChildPID(),
00967 timeout)
00968
00969 if result == win32con.WAIT_TIMEOUT:
00970 self.Kill()
00971
00972
00973
00974
00975 class GaudiExeTest(ExecTestBase):
00976 """Standard Gaudi test.
00977 """
00978 arguments = [
00979 qm.fields.TextField(
00980 name="program",
00981 title="Program",
00982 not_empty_text=1,
00983 description="""The path to the program.
00984
00985 This field indicates the path to the program. If it is not
00986 an absolute path, the value of the 'PATH' environment
00987 variable will be used to search for the program.
00988 If not specified, $GAUDIEXE or Gaudi.exe are used.
00989 """
00990 ),
00991 qm.fields.SetField(qm.fields.TextField(
00992 name="args",
00993 title="Argument List",
00994 description="""The command-line arguments.
00995
00996 If this field is left blank, the program is run without any
00997 arguments.
00998
00999 Use this field to specify the option files.
01000
01001 An implicit 0th argument (the path to the program) is added
01002 automatically."""
01003 )),
01004 qm.fields.TextField(
01005 name="options",
01006 title="Options",
01007 description="""Options to be passed to the application.
01008
01009 This field allows to pass a list of options to the main program
01010 without the need of a separate option file.
01011
01012 The content of the field is written to a temporary file which name
01013 is passed the the application as last argument (appended to the
01014 field "Argument List".
01015 """,
01016 verbatim="true",
01017 multiline="true",
01018 default_value=""
01019 ),
01020 qm.fields.TextField(
01021 name="workdir",
01022 title="Working Directory",
01023 description="""Path to the working directory.
01024
01025 If this field is left blank, the program will be run from the qmtest
01026 directory, otherwise from the directory specified.""",
01027 default_value=""
01028 ),
01029 qm.fields.TextField(
01030 name="reference",
01031 title="Reference Output",
01032 description="""Path to the file containing the reference output.
01033
01034 If this field is left blank, any standard output will be considered
01035 valid.
01036
01037 If the reference file is specified, any output on standard error is
01038 ignored."""
01039 ),
01040 qm.fields.TextField(
01041 name="error_reference",
01042 title="Reference for standard error",
01043 description="""Path to the file containing the reference for the standard error.
01044
01045 If this field is left blank, any standard output will be considered
01046 valid.
01047
01048 If the reference file is specified, any output on standard error is
01049 ignored."""
01050 ),
01051 qm.fields.SetField(qm.fields.TextField(
01052 name = "unsupported_platforms",
01053 title = "Unsupported Platforms",
01054 description = """Platform on which the test must not be run.
01055
01056 List of regular expressions identifying the platforms on which the
01057 test is not run and the result is set to UNTESTED."""
01058 )),
01059
01060 qm.fields.TextField(
01061 name = "validator",
01062 title = "Validator",
01063 description = """Function to validate the output of the test.
01064
01065 If defined, the function is used to validate the products of the
01066 test.
01067 The function is called passing as arguments:
01068 self: the test class instance
01069 stdout: the standard output of the executed test
01070 stderr: the standard error of the executed test
01071 result: the Result objects to fill with messages
01072 The function must return a list of causes for the failure.
01073 If specified, overrides standard output, standard error and
01074 reference files.
01075 """,
01076 verbatim="true",
01077 multiline="true",
01078 default_value=""
01079 ),
01080
01081 qm.fields.BooleanField(
01082 name = "use_temp_dir",
01083 title = "Use temporary directory",
01084 description = """Use temporary directory.
01085
01086 If set to true, use a temporary directory as working directory.
01087 """,
01088 default_value="false"
01089 ),
01090 ]
01091
01092 def PlatformIsNotSupported(self, context, result):
01093 platform = self.GetPlatform()
01094 unsupported = [ re.compile(x)
01095 for x in [ str(y).strip()
01096 for y in self.unsupported_platforms ]
01097 if x
01098 ]
01099 for p_re in unsupported:
01100 if p_re.search(platform):
01101 result.SetOutcome(result.UNTESTED)
01102 result[result.CAUSE] = 'Platform not supported.'
01103 return True
01104 return False
01105
01106 def GetPlatform(self):
01107 """
01108 Return the platform Id defined in CMTCONFIG or SCRAM_ARCH.
01109 """
01110 arch = "None"
01111
01112 if "CMTCONFIG" in os.environ:
01113 arch = os.environ["CMTCONFIG"]
01114 elif "SCRAM_ARCH" in os.environ:
01115 arch = os.environ["SCRAM_ARCH"]
01116 return arch
01117
01118 def isWinPlatform(self):
01119 """
01120 Return True if the current platform is Windows.
01121
01122 This function was needed because of the change in the CMTCONFIG format,
01123 from win32_vc71_dbg to i686-winxp-vc9-dbg.
01124 """
01125 platform = self.GetPlatform()
01126 return "winxp" in platform or platform.startswith("win")
01127
01128 def _expandReferenceFileName(self, reffile):
01129
01130 if not reffile:
01131 return ""
01132
01133
01134 platformSplit = lambda p: set(p.split('-' in p and '-' or '_'))
01135
01136 reference = os.path.normpath(os.path.expandvars(reffile))
01137
01138 spec_ref = reference[:-3] + self.GetPlatform()[0:3] + reference[-3:]
01139 if os.path.isfile(spec_ref):
01140 reference = spec_ref
01141 else:
01142
01143 dirname, basename = os.path.split(reference)
01144 if not dirname: dirname = '.'
01145 head = basename + "."
01146 head_len = len(head)
01147 platform = platformSplit(self.GetPlatform())
01148 candidates = []
01149 for f in os.listdir(dirname):
01150 if f.startswith(head):
01151 req_plat = platformSplit(f[head_len:])
01152 if platform.issuperset(req_plat):
01153 candidates.append( (len(req_plat), f) )
01154 if candidates:
01155
01156
01157 candidates.sort()
01158 reference = os.path.join(dirname, candidates[-1][1])
01159 return reference
01160
01161 def CheckTTreesSummaries(self, stdout, result, causes,
01162 trees_dict = None,
01163 ignore = r"Basket|.*size|Compression"):
01164 """
01165 Compare the TTree summaries in stdout with the ones in trees_dict or in
01166 the reference file. By default ignore the size, compression and basket
01167 fields.
01168 The presence of TTree summaries when none is expected is not a failure.
01169 """
01170 if trees_dict is None:
01171 reference = self._expandReferenceFileName(self.reference)
01172
01173 if reference and os.path.isfile(reference):
01174 trees_dict = findTTreeSummaries(open(reference).read())
01175 else:
01176 trees_dict = {}
01177
01178 from pprint import PrettyPrinter
01179 pp = PrettyPrinter()
01180 if trees_dict:
01181 result["GaudiTest.TTrees.expected"] = result.Quote(pp.pformat(trees_dict))
01182 if ignore:
01183 result["GaudiTest.TTrees.ignore"] = result.Quote(ignore)
01184
01185 trees = findTTreeSummaries(stdout)
01186 failed = cmpTreesDicts(trees_dict, trees, ignore)
01187 if failed:
01188 causes.append("trees summaries")
01189 msg = "%s: %s != %s" % getCmpFailingValues(trees_dict, trees, failed)
01190 result["GaudiTest.TTrees.failure_on"] = result.Quote(msg)
01191 result["GaudiTest.TTrees.found"] = result.Quote(pp.pformat(trees))
01192
01193 return causes
01194
01195 def CheckHistosSummaries(self, stdout, result, causes,
01196 dict = None,
01197 ignore = None):
01198 """
01199 Compare the TTree summaries in stdout with the ones in trees_dict or in
01200 the reference file. By default ignore the size, compression and basket
01201 fields.
01202 The presence of TTree summaries when none is expected is not a failure.
01203 """
01204 if dict is None:
01205 reference = self._expandReferenceFileName(self.reference)
01206
01207 if reference and os.path.isfile(reference):
01208 dict = findHistosSummaries(open(reference).read())
01209 else:
01210 dict = {}
01211
01212 from pprint import PrettyPrinter
01213 pp = PrettyPrinter()
01214 if dict:
01215 result["GaudiTest.Histos.expected"] = result.Quote(pp.pformat(dict))
01216 if ignore:
01217 result["GaudiTest.Histos.ignore"] = result.Quote(ignore)
01218
01219 histos = findHistosSummaries(stdout)
01220 failed = cmpTreesDicts(dict, histos, ignore)
01221 if failed:
01222 causes.append("histos summaries")
01223 msg = "%s: %s != %s" % getCmpFailingValues(dict, histos, failed)
01224 result["GaudiTest.Histos.failure_on"] = result.Quote(msg)
01225 result["GaudiTest.Histos.found"] = result.Quote(pp.pformat(histos))
01226
01227 return causes
01228
01229 def ValidateWithReference(self, stdout, stderr, result, causes, preproc = None):
01230 """
01231 Default validation action: compare standard output and error to the
01232 reference files.
01233 """
01234
01235 if preproc is None:
01236 preproc = normalizeExamples
01237
01238 reference = self._expandReferenceFileName(self.reference)
01239
01240 if reference and os.path.isfile(reference):
01241 result["GaudiTest.output_reference"] = reference
01242 causes += ReferenceFileValidator(reference,
01243 "standard output",
01244 "GaudiTest.output_diff",
01245 preproc = preproc)(stdout, result)
01246
01247
01248 causes = self.CheckTTreesSummaries(stdout, result, causes)
01249 causes = self.CheckHistosSummaries(stdout, result, causes)
01250
01251 if causes:
01252 try:
01253 newref = open(reference + ".new","w")
01254
01255 for l in stdout.splitlines():
01256 newref.write(l.rstrip() + '\n')
01257 del newref
01258 except IOError:
01259
01260
01261 pass
01262
01263
01264 reference = self._expandReferenceFileName(self.error_reference)
01265
01266 if reference and os.path.isfile(reference):
01267 result["GaudiTest.error_reference"] = reference
01268 newcauses = ReferenceFileValidator(reference,
01269 "standard error",
01270 "GaudiTest.error_diff",
01271 preproc = preproc)(stderr, result)
01272 causes += newcauses
01273 if newcauses:
01274 newref = open(reference + ".new","w")
01275
01276 for l in stderr.splitlines():
01277 newref.write(l.rstrip() + '\n')
01278 del newref
01279 else:
01280 causes += BasicOutputValidator(self.stderr,
01281 "standard error",
01282 "ExecTest.expected_stderr")(stderr, result)
01283
01284 return causes
01285
01286 def ValidateOutput(self, stdout, stderr, result):
01287 causes = []
01288
01289 if self.validator.strip() != "":
01290 class CallWrapper(object):
01291 """
01292 Small wrapper class to dynamically bind some default arguments
01293 to a callable.
01294 """
01295 def __init__(self, callable, extra_args = {}):
01296 self.callable = callable
01297 self.extra_args = extra_args
01298
01299 from inspect import getargspec
01300 self.args_order = getargspec(callable)[0]
01301
01302
01303 if self.args_order[0] == "self":
01304 del self.args_order[0]
01305 def __call__(self, *args, **kwargs):
01306
01307 positional = self.args_order[:len(args)]
01308
01309 kwargs = dict(kwargs)
01310 for a in self.extra_args:
01311
01312
01313 if a not in positional and a not in kwargs:
01314 kwargs[a] = self.extra_args[a]
01315 return apply(self.callable, args, kwargs)
01316
01317 exported_symbols = {"self":self,
01318 "stdout":stdout,
01319 "stderr":stderr,
01320 "result":result,
01321 "causes":causes,
01322 "findReferenceBlock":
01323 CallWrapper(findReferenceBlock, {"stdout":stdout,
01324 "result":result,
01325 "causes":causes}),
01326 "validateWithReference":
01327 CallWrapper(self.ValidateWithReference, {"stdout":stdout,
01328 "stderr":stderr,
01329 "result":result,
01330 "causes":causes}),
01331 "countErrorLines":
01332 CallWrapper(countErrorLines, {"stdout":stdout,
01333 "result":result,
01334 "causes":causes}),
01335 "checkTTreesSummaries":
01336 CallWrapper(self.CheckTTreesSummaries, {"stdout":stdout,
01337 "result":result,
01338 "causes":causes}),
01339 "checkHistosSummaries":
01340 CallWrapper(self.CheckHistosSummaries, {"stdout":stdout,
01341 "result":result,
01342 "causes":causes}),
01343
01344 }
01345 exec self.validator in globals(), exported_symbols
01346 else:
01347 self.ValidateWithReference(stdout, stderr, result, causes)
01348
01349 return causes
01350
01351 def DumpEnvironment(self, result):
01352 """
01353 Add the content of the environment to the result object.
01354
01355 Copied from the QMTest class of COOL.
01356 """
01357 vars = os.environ.keys()
01358 vars.sort()
01359 result['GaudiTest.environment'] = \
01360 result.Quote('\n'.join(["%s=%s"%(v,os.environ[v]) for v in vars]))
01361
01362 def Run(self, context, result):
01363 """Run the test.
01364
01365 'context' -- A 'Context' giving run-time parameters to the
01366 test.
01367
01368 'result' -- A 'Result' object. The outcome will be
01369 'Result.PASS' when this method is called. The 'result' may be
01370 modified by this method to indicate outcomes other than
01371 'Result.PASS' or to add annotations."""
01372
01373
01374 if self.PlatformIsNotSupported(context, result):
01375 return
01376
01377
01378 if self.program:
01379 prog = rationalizepath(self.program)
01380 elif "GAUDIEXE" in os.environ:
01381 prog = os.environ["GAUDIEXE"]
01382 else:
01383 prog = "Gaudi.exe"
01384 self.program = prog
01385
01386 dummy, prog_ext = os.path.splitext(prog)
01387 if prog_ext not in [ ".exe", ".py", ".bat" ] and self.isWinPlatform():
01388 prog += ".exe"
01389 prog_ext = ".exe"
01390
01391 prog = which(prog) or prog
01392
01393
01394 args = map(rationalizepath, self.args)
01395 self.reference = rationalizepath(self.reference)
01396 self.error_reference = rationalizepath(self.error_reference)
01397
01398
01399
01400 tmpfile = None
01401 if self.options.strip():
01402 ext = ".opts"
01403 if re.search(r"from\s*Gaudi.Configuration\s*import\s*\*", self.options):
01404 ext = ".py"
01405 tmpfile = TempFile(ext)
01406 tmpfile.writelines("\n".join(self.options.splitlines()))
01407 tmpfile.flush()
01408 args.append(tmpfile.name)
01409 result["GaudiTest.options"] = result.Quote(self.options)
01410
01411
01412 if prog_ext == ".py":
01413 args.insert(0,prog)
01414 if self.isWinPlatform():
01415 prog = which("python.exe") or "python.exe"
01416 else:
01417 prog = which("python") or "python"
01418
01419
01420 origdir = os.getcwd()
01421 if self.workdir:
01422 os.chdir(str(os.path.normpath(os.path.expandvars(self.workdir))))
01423 elif self.use_temp_dir == "true":
01424 if "QMTEST_TMPDIR" in os.environ:
01425 os.chdir(os.environ["QMTEST_TMPDIR"])
01426 elif "qmtest.tmpdir" in context:
01427 os.chdir(context["qmtest.tmpdir"])
01428
01429 if "QMTEST_IGNORE_TIMEOUT" not in os.environ:
01430 self.timeout = max(self.timeout,600)
01431 else:
01432 self.timeout = -1
01433
01434 try:
01435
01436 self._CreateEclipseLaunch(prog, args, destdir = origdir)
01437
01438 self.RunProgram(prog,
01439 [ prog ] + args,
01440 context, result)
01441
01442 if result.GetOutcome() not in [ result.PASS ]:
01443 self.DumpEnvironment(result)
01444 finally:
01445
01446 os.chdir(origdir)
01447
01448 def RunProgram(self, program, arguments, context, result):
01449 """Run the 'program'.
01450
01451 'program' -- The path to the program to run.
01452
01453 'arguments' -- A list of the arguments to the program. This
01454 list must contain a first argument corresponding to 'argv[0]'.
01455
01456 'context' -- A 'Context' giving run-time parameters to the
01457 test.
01458
01459 'result' -- A 'Result' object. The outcome will be
01460 'Result.PASS' when this method is called. The 'result' may be
01461 modified by this method to indicate outcomes other than
01462 'Result.PASS' or to add annotations.
01463
01464 @attention: This method has been copied from command.ExecTestBase
01465 (QMTest 2.3.0) and modified to keep stdout and stderr
01466 for tests that have been terminated by a signal.
01467 (Fundamental for debugging in the Application Area)
01468 """
01469
01470
01471 environment = self.MakeEnvironment(context)
01472
01473 if self.timeout >= 0:
01474 timeout = self.timeout
01475 else:
01476
01477
01478
01479
01480
01481 timeout = -2
01482 e = GaudiFilterExecutable(self.stdin, timeout)
01483
01484 exit_status = e.Run(arguments, environment, path = program)
01485
01486 if e.stack_trace_file and os.path.exists(e.stack_trace_file):
01487 stack_trace = open(e.stack_trace_file).read()
01488 os.remove(e.stack_trace_file)
01489 else:
01490 stack_trace = None
01491 if stack_trace:
01492 result["ExecTest.stack_trace"] = result.Quote(stack_trace)
01493
01494
01495 if sys.platform == "win32" or os.WIFEXITED(exit_status):
01496
01497 causes = []
01498
01499
01500 if self.exit_code is None:
01501 exit_code = None
01502 elif sys.platform == "win32":
01503 exit_code = exit_status
01504 else:
01505 exit_code = os.WEXITSTATUS(exit_status)
01506
01507 stdout = e.stdout
01508 stderr = e.stderr
01509
01510 result["ExecTest.exit_code"] = str(exit_code)
01511 result["ExecTest.stdout"] = result.Quote(stdout)
01512 result["ExecTest.stderr"] = result.Quote(stderr)
01513
01514 if exit_code != self.exit_code:
01515 causes.append("exit_code")
01516 result["ExecTest.expected_exit_code"] \
01517 = str(self.exit_code)
01518
01519 causes += self.ValidateOutput(stdout, stderr, result)
01520
01521 if causes:
01522 result.Fail("Unexpected %s." % string.join(causes, ", "))
01523 elif os.WIFSIGNALED(exit_status):
01524
01525
01526 signal_number = str(os.WTERMSIG(exit_status))
01527 if not stack_trace:
01528 result.Fail("Program terminated by signal.")
01529 else:
01530
01531
01532 result.Fail("Exceeded time limit (%ds), terminated." % timeout)
01533 result["ExecTest.signal_number"] = signal_number
01534 result["ExecTest.stdout"] = result.Quote(e.stdout)
01535 result["ExecTest.stderr"] = result.Quote(e.stderr)
01536 elif os.WIFSTOPPED(exit_status):
01537
01538
01539 signal_number = str(os.WSTOPSIG(exit_status))
01540 if not stack_trace:
01541 result.Fail("Program stopped by signal.")
01542 else:
01543
01544
01545 result.Fail("Exceeded time limit (%ds), stopped." % timeout)
01546 result["ExecTest.signal_number"] = signal_number
01547 result["ExecTest.stdout"] = result.Quote(e.stdout)
01548 result["ExecTest.stderr"] = result.Quote(e.stderr)
01549 else:
01550
01551
01552 result.Fail("Program did not terminate normally.")
01553
01554
01555
01556 esc = '\x1b'
01557 repr_esc = '\\x1b'
01558 result["ExecTest.stdout"] = result["ExecTest.stdout"].replace(esc,repr_esc)
01559
01560
01561
01562 def _CreateEclipseLaunch(self, prog, args, destdir = None):
01563
01564
01565 projbasedir = os.path.normpath(destdir)
01566 while not os.path.exists(os.path.join(projbasedir, ".project")):
01567 oldprojdir = projbasedir
01568 projbasedir = os.path.normpath(os.path.join(projbasedir, os.pardir))
01569
01570
01571 if oldprojdir == projbasedir:
01572
01573 return
01574
01575 from xml.etree import ElementTree as ET
01576 t = ET.parse(os.path.join(projbasedir, ".project"))
01577 projectName = t.find("name").text
01578
01579
01580 destfile = "%s.launch" % self._Runnable__id
01581 if destdir:
01582 destfile = os.path.join(destdir, destfile)
01583
01584 if self.options.strip():
01585
01586
01587
01588 tempfile = args.pop()
01589 optsfile = destfile + os.path.splitext(tempfile)[1]
01590 shutil.copyfile(tempfile, optsfile)
01591 args.append(optsfile)
01592
01593
01594 from xml.sax.saxutils import quoteattr
01595 data = {}
01596
01597
01598 data["environment"] = "\n".join(['<mapEntry key=%s value=%s/>' % (quoteattr(k), quoteattr(v))
01599 for k, v in os.environ.iteritems()])
01600
01601 data["exec"] = which(prog) or prog
01602 if os.path.basename(data["exec"]).lower().startswith("python"):
01603 data["stopAtMain"] = "false"
01604 else:
01605 data["stopAtMain"] = "true"
01606
01607 data["args"] = " ".join(map(rationalizepath, args))
01608 if self.isWinPlatform():
01609 data["args"] = " ".join(["/debugexe"] + map(rationalizepath, [data["exec"]] + args))
01610 data["exec"] = which("vcexpress.exe")
01611
01612 if not self.use_temp_dir:
01613 data["workdir"] = os.getcwd()
01614 else:
01615
01616
01617 data["workdir"] = destdir
01618
01619 data["project"] = projectName.strip()
01620
01621
01622 xml = """<?xml version="1.0" encoding="UTF-8" standalone="no"?>
01623 <launchConfiguration type="org.eclipse.cdt.launch.applicationLaunchType">
01624 <booleanAttribute key="org.eclipse.cdt.debug.mi.core.AUTO_SOLIB" value="true"/>
01625 <listAttribute key="org.eclipse.cdt.debug.mi.core.AUTO_SOLIB_LIST"/>
01626 <stringAttribute key="org.eclipse.cdt.debug.mi.core.DEBUG_NAME" value="gdb"/>
01627 <stringAttribute key="org.eclipse.cdt.debug.mi.core.GDB_INIT" value=".gdbinit"/>
01628 <listAttribute key="org.eclipse.cdt.debug.mi.core.SOLIB_PATH"/>
01629 <booleanAttribute key="org.eclipse.cdt.debug.mi.core.STOP_ON_SOLIB_EVENTS" value="false"/>
01630 <booleanAttribute key="org.eclipse.cdt.debug.mi.core.breakpointsFullPath" value="false"/>
01631 <stringAttribute key="org.eclipse.cdt.debug.mi.core.commandFactory" value="org.eclipse.cdt.debug.mi.core.standardCommandFactory"/>
01632 <stringAttribute key="org.eclipse.cdt.debug.mi.core.protocol" value="mi"/>
01633 <booleanAttribute key="org.eclipse.cdt.debug.mi.core.verboseMode" value="false"/>
01634 <intAttribute key="org.eclipse.cdt.launch.ATTR_BUILD_BEFORE_LAUNCH_ATTR" value="0"/>
01635 <stringAttribute key="org.eclipse.cdt.launch.COREFILE_PATH" value=""/>
01636 <stringAttribute key="org.eclipse.cdt.launch.DEBUGGER_ID" value="org.eclipse.cdt.debug.mi.core.CDebuggerNew"/>
01637 <stringAttribute key="org.eclipse.cdt.launch.DEBUGGER_REGISTER_GROUPS" value=""/>
01638 <stringAttribute key="org.eclipse.cdt.launch.DEBUGGER_START_MODE" value="run"/>
01639 <booleanAttribute key="org.eclipse.cdt.launch.DEBUGGER_STOP_AT_MAIN" value="%(stopAtMain)s"/>
01640 <stringAttribute key="org.eclipse.cdt.launch.DEBUGGER_STOP_AT_MAIN_SYMBOL" value="main"/>
01641 <booleanAttribute key="org.eclipse.cdt.launch.ENABLE_REGISTER_BOOKKEEPING" value="false"/>
01642 <booleanAttribute key="org.eclipse.cdt.launch.ENABLE_VARIABLE_BOOKKEEPING" value="false"/>
01643 <stringAttribute key="org.eclipse.cdt.launch.FORMAT" value="<?xml version="1.0" encoding="UTF-8" standalone="no"?><contentList/>"/>
01644 <stringAttribute key="org.eclipse.cdt.launch.GLOBAL_VARIABLES" value="<?xml version="1.0" encoding="UTF-8" standalone="no"?> <globalVariableList/> "/>
01645 <stringAttribute key="org.eclipse.cdt.launch.MEMORY_BLOCKS" value="<?xml version="1.0" encoding="UTF-8" standalone="no"?> <memoryBlockExpressionList/> "/>
01646 <stringAttribute key="org.eclipse.cdt.launch.PROGRAM_ARGUMENTS" value="%(args)s"/>
01647 <stringAttribute key="org.eclipse.cdt.launch.PROGRAM_NAME" value="%(exec)s"/>
01648 <stringAttribute key="org.eclipse.cdt.launch.PROJECT_ATTR" value="%(project)s"/>
01649 <stringAttribute key="org.eclipse.cdt.launch.PROJECT_BUILD_CONFIG_ID_ATTR" value=""/>
01650 <stringAttribute key="org.eclipse.cdt.launch.WORKING_DIRECTORY" value="%(workdir)s"/>
01651 <booleanAttribute key="org.eclipse.cdt.launch.ui.ApplicationCDebuggerTab.DEFAULTS_SET" value="true"/>
01652 <booleanAttribute key="org.eclipse.cdt.launch.use_terminal" value="true"/>
01653 <listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_PATHS">
01654 <listEntry value="/%(project)s"/>
01655 </listAttribute>
01656 <listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_TYPES">
01657 <listEntry value="4"/>
01658 </listAttribute>
01659 <booleanAttribute key="org.eclipse.debug.core.appendEnvironmentVariables" value="false"/>
01660 <mapAttribute key="org.eclipse.debug.core.environmentVariables">
01661 %(environment)s
01662 </mapAttribute>
01663 <mapAttribute key="org.eclipse.debug.core.preferred_launchers">
01664 <mapEntry key="[debug]" value="org.eclipse.cdt.cdi.launch.localCLaunch"/>
01665 </mapAttribute>
01666 <listAttribute key="org.eclipse.debug.ui.favoriteGroups">
01667 <listEntry value="org.eclipse.debug.ui.launchGroup.debug"/>
01668 </listAttribute>
01669 </launchConfiguration>
01670 """ % data
01671
01672
01673 open(destfile, "w").write(xml)
01674
01675
01676
01677 try:
01678 import json
01679 except ImportError:
01680
01681 import simplejson as json
01682
01683 class HTMLResultStream(ResultStream):
01684 """An 'HTMLResultStream' writes its output to a set of HTML files.
01685
01686 The argument 'dir' is used to select the destination directory for the HTML
01687 report.
01688 The destination directory may already contain the report from a previous run
01689 (for example of a different package), in which case it will be extended to
01690 include the new data.
01691 """
01692 arguments = [
01693 qm.fields.TextField(
01694 name = "dir",
01695 title = "Destination Directory",
01696 description = """The name of the directory.
01697
01698 All results will be written to the directory indicated.""",
01699 verbatim = "true",
01700 default_value = ""),
01701 ]
01702
01703 def __init__(self, arguments = None, **args):
01704 """Prepare the destination directory.
01705
01706 Creates the destination directory and store in it some preliminary
01707 annotations and the static files found in the template directory
01708 'html_report'.
01709 """
01710 ResultStream.__init__(self, arguments, **args)
01711 self._summary = []
01712 self._summaryFile = os.path.join(self.dir, "summary.json")
01713 self._annotationsFile = os.path.join(self.dir, "annotations.json")
01714
01715 templateDir = os.path.join(os.path.dirname(__file__), "html_report")
01716 if not os.path.isdir(self.dir):
01717 os.makedirs(self.dir)
01718
01719 for f in os.listdir(templateDir):
01720 src = os.path.join(templateDir, f)
01721 dst = os.path.join(self.dir, f)
01722 if not os.path.isdir(src) and not os.path.exists(dst):
01723 shutil.copy(src, dst)
01724
01725 if "CMTCONFIG" in os.environ:
01726 self.WriteAnnotation("cmt.cmtconfig", os.environ["CMTCONFIG"])
01727 import socket
01728 self.WriteAnnotation("hostname", socket.gethostname())
01729
01730 def _updateSummary(self):
01731 """Helper function to extend the global summary file in the destination
01732 directory.
01733 """
01734 if os.path.exists(self._summaryFile):
01735 oldSummary = json.load(open(self._summaryFile))
01736 else:
01737 oldSummary = []
01738 ids = set([ i["id"] for i in self._summary ])
01739 newSummary = [ i for i in oldSummary if i["id"] not in ids ]
01740 newSummary.extend(self._summary)
01741 json.dump(newSummary, open(self._summaryFile, "w"),
01742 sort_keys = True)
01743
01744 def WriteAnnotation(self, key, value):
01745 """Writes the annotation to the annotation file.
01746 If the key is already present with a different value, the value becomes
01747 a list and the new value is appended to it, except for start_time and
01748 end_time.
01749 """
01750
01751 if os.path.exists(self._annotationsFile):
01752 annotations = json.load(open(self._annotationsFile))
01753 else:
01754 annotations = {}
01755
01756 key, value = map(str, [key, value])
01757 if key == "qmtest.run.start_time":
01758
01759
01760
01761
01762 if key not in annotations:
01763 annotations[key] = value
01764 if "qmtest.run.end_time" in annotations:
01765 del annotations["qmtest.run.end_time"]
01766 else:
01767
01768 if key in annotations:
01769 old = annotations[key]
01770 if type(old) is list:
01771 if value not in old:
01772 annotations[key].append(value)
01773 elif value != old:
01774 annotations[key] = [old, value]
01775 else:
01776 annotations[key] = value
01777
01778 json.dump(annotations, open(self._annotationsFile, "w"),
01779 sort_keys = True)
01780
01781 def WriteResult(self, result):
01782 """Prepare the test result directory in the destination directory storing
01783 into it the result fields.
01784 A summary of the test result is stored both in a file in the test directory
01785 and in the global summary file.
01786 """
01787 summary = {}
01788 summary["id"] = result.GetId()
01789 summary["outcome"] = result.GetOutcome()
01790 summary["cause"] = result.GetCause()
01791 summary["fields"] = result.keys()
01792 summary["fields"].sort()
01793
01794
01795 for f in ["id", "outcome", "cause"]:
01796 summary[f] = str(summary[f])
01797 summary["fields"] = map(str, summary["fields"])
01798
01799 self._summary.append(summary)
01800
01801
01802
01803
01804
01805 testOutDir = os.path.join(self.dir, summary["id"])
01806 if not os.path.isdir(testOutDir):
01807 os.makedirs(testOutDir)
01808 json.dump(summary, open(os.path.join(testOutDir, "summary.json"), "w"),
01809 sort_keys = True)
01810 for f in summary["fields"]:
01811 open(os.path.join(testOutDir, f), "w").write(result[f])
01812
01813 self._updateSummary()
01814
01815 def Summarize(self):
01816
01817 pass