00001
00002
00003
00004
00005 __author__ = 'Marco Clemencic CERN/PH-LBC'
00006 __version__ = "$Revision: 1.52 $"
00007 __tag__ = "$Name: $"
00008
00009
00010
00011 import os
00012 import sys
00013 import re
00014 import tempfile
00015 import shutil
00016 import string
00017 import difflib
00018 from subprocess import Popen, PIPE, STDOUT
00019
00020 import qm
00021 from qm.test.classes.command import ExecTestBase
00022 from qm.test.result_stream import ResultStream
00023
00024
00025 import qm.executable
00026 import time, signal
00027
00028
00029 if sys.platform == "win32":
00030 import msvcrt
00031 import pywintypes
00032 from threading import *
00033 import win32api
00034 import win32con
00035 import win32event
00036 import win32file
00037 import win32pipe
00038 import win32process
00039 else:
00040 import cPickle
00041 import fcntl
00042 import select
00043 import qm.sigmask
00044
00045
00046
00047
00048 class TemporaryEnvironment:
00049 """
00050 Class to changes the environment temporarily.
00051 """
00052 def __init__(self, orig = os.environ, keep_same = False):
00053 """
00054 Create a temporary environment on top of the one specified
00055 (it can be another TemporaryEnvironment instance).
00056 """
00057
00058 self.old_values = {}
00059 self.env = orig
00060 self._keep_same = keep_same
00061
00062 def __setitem__(self,key,value):
00063 """
00064 Set an environment variable recording the previous value.
00065 """
00066 if key not in self.old_values :
00067 if key in self.env :
00068 if not self._keep_same or self.env[key] != value:
00069 self.old_values[key] = self.env[key]
00070 else:
00071 self.old_values[key] = None
00072 self.env[key] = value
00073
00074 def __getitem__(self,key):
00075 """
00076 Get an environment variable.
00077 Needed to provide the same interface as os.environ.
00078 """
00079 return self.env[key]
00080
00081 def __delitem__(self,key):
00082 """
00083 Unset an environment variable.
00084 Needed to provide the same interface as os.environ.
00085 """
00086 if key not in self.env :
00087 raise KeyError(key)
00088 self.old_values[key] = self.env[key]
00089 del self.env[key]
00090
00091 def keys(self):
00092 """
00093 Return the list of defined environment variables.
00094 Needed to provide the same interface as os.environ.
00095 """
00096 return self.env.keys()
00097
00098 def items(self):
00099 """
00100 Return the list of (name,value) pairs for the defined environment variables.
00101 Needed to provide the same interface as os.environ.
00102 """
00103 return self.env.items()
00104
00105 def __contains__(self,key):
00106 """
00107 Operator 'in'.
00108 Needed to provide the same interface as os.environ.
00109 """
00110 return key in self.env
00111
00112 def restore(self):
00113 """
00114 Revert all the changes done to the orignal environment.
00115 """
00116 for key,value in self.old_values.items():
00117 if value is None:
00118 del self.env[key]
00119 else:
00120 self.env[key] = value
00121 self.old_values = {}
00122
00123 def __del__(self):
00124 """
00125 Revert the changes on destruction.
00126 """
00127
00128 self.restore()
00129
00130 def gen_script(self,shell_type):
00131 """
00132 Generate a shell script to reproduce the changes in the environment.
00133 """
00134 shells = [ 'csh', 'sh', 'bat' ]
00135 if shell_type not in shells:
00136 raise RuntimeError("Shell type '%s' unknown. Available: %s"%(shell_type,shells))
00137 out = ""
00138 for key,value in self.old_values.items():
00139 if key not in self.env:
00140
00141 if shell_type == 'csh':
00142 out += 'unsetenv %s\n'%key
00143 elif shell_type == 'sh':
00144 out += 'unset %s\n'%key
00145 elif shell_type == 'bat':
00146 out += 'set %s=\n'%key
00147 else:
00148
00149 if shell_type == 'csh':
00150 out += 'setenv %s "%s"\n'%(key,self.env[key])
00151 elif shell_type == 'sh':
00152 out += 'export %s="%s"\n'%(key,self.env[key])
00153 elif shell_type == 'bat':
00154 out += 'set %s=%s\n'%(key,self.env[key])
00155 return out
00156
00157 class TempDir:
00158 """Small class for temporary directories.
00159 When instantiated, it creates a temporary directory and the instance
00160 behaves as the string containing the directory name.
00161 When the instance goes out of scope, it removes all the content of
00162 the temporary directory (automatic clean-up).
00163 """
00164 def __init__(self, keep = False, chdir = False):
00165 self.name = tempfile.mkdtemp()
00166 self._keep = keep
00167 self._origdir = None
00168 if chdir:
00169 self._origdir = os.getcwd()
00170 os.chdir(self.name)
00171
00172 def __str__(self):
00173 return self.name
00174
00175 def __del__(self):
00176 if self._origdir:
00177 os.chdir(self._origdir)
00178 if self.name and not self._keep:
00179 shutil.rmtree(self.name)
00180
00181 def __getattr__(self,attr):
00182 return getattr(self.name,attr)
00183
00184 class TempFile:
00185 """Small class for temporary files.
00186 When instantiated, it creates a temporary directory and the instance
00187 behaves as the string containing the directory name.
00188 When the instance goes out of scope, it removes all the content of
00189 the temporary directory (automatic clean-up).
00190 """
00191 def __init__(self, suffix='', prefix='tmp', dir=None, text=False, keep = False):
00192 self.file = None
00193 self.name = None
00194 self._keep = keep
00195
00196 self._fd, self.name = tempfile.mkstemp(suffix,prefix,dir,text)
00197 self.file = os.fdopen(self._fd,"r+")
00198
00199 def __str__(self):
00200 return self.name
00201
00202 def __del__(self):
00203 if self.file:
00204 self.file.close()
00205 if self.name and not self._keep:
00206 os.remove(self.name)
00207
00208 def __getattr__(self,attr):
00209 return getattr(self.file,attr)
00210
00211 class CMT:
00212 """Small wrapper to call CMT.
00213 """
00214 def __init__(self,path=None):
00215 if path is None:
00216 path = os.getcwd()
00217 self.path = path
00218
00219 def _run_cmt(self,command,args):
00220
00221 if type(args) is str:
00222 args = [args]
00223 cmd = "cmt %s"%command
00224 for arg in args:
00225 cmd += ' "%s"'%arg
00226
00227
00228 olddir = os.getcwd()
00229 os.chdir(self.path)
00230
00231 result = os.popen4(cmd)[1].read()
00232
00233 os.chdir(olddir)
00234 return result
00235
00236 def __getattr__(self,attr):
00237 return lambda args=[]: self._run_cmt(attr, args)
00238
00239 def runtime_env(self,env = None):
00240 """Returns a dictionary containing the runtime environment produced by CMT.
00241 If a dictionary is passed a modified instance of it is returned.
00242 """
00243 if env is None:
00244 env = {}
00245 for l in self.setup("-csh").splitlines():
00246 l = l.strip()
00247 if l.startswith("setenv"):
00248 dummy,name,value = l.split(None,3)
00249 env[name] = value.strip('"')
00250 elif l.startswith("unsetenv"):
00251 dummy,name = l.split(None,2)
00252 if name in env:
00253 del env[name]
00254 return env
00255 def show_macro(self,k):
00256 r = self.show(["macro",k])
00257 if r.find("CMT> Error: symbol not found") >= 0:
00258 return None
00259 else:
00260 return self.show(["macro_value",k]).strip()
00261
00262
00263
00264
00265 def which(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 "Time User",
00451 "Welcome to",
00452 "This machine has a speed",
00453 "TIME:",
00454 "running on",
00455 "ToolSvc.Sequenc... INFO",
00456 "DataListenerSvc INFO XML written to file:",
00457 "[INFO]","[WARNING]",
00458 "DEBUG No writable file catalog found which contains FID:",
00459 "0 local",
00460 "DEBUG Service base class initialized successfully",
00461 "DEBUG Incident timing:",
00462
00463 'Note: (file "(tmpfile)", line 2) File "set" already loaded',
00464
00465 'SIGXCPU',
00466 ],regexps = [
00467 r"^#",
00468 r"(Always|SUCCESS)\s*(Root f|[^ ]* F)ile version:",
00469 r"0x[0-9a-fA-F#]+ *Algorithm::sysInitialize\(\) *\[",
00470 r"0x[0-9a-fA-F#]* *__gxx_personality_v0 *\[",
00471 r"File '.*.xml' does not exist",
00472 r"INFO Refer to dataset .* by its file ID:",
00473 r"INFO Referring to dataset .* by its file ID:",
00474 r"INFO Disconnect from dataset",
00475 r"INFO Disconnected from dataset",
00476 r"INFO Disconnected data IO:",
00477 r"IncidentSvc\s*(DEBUG (Adding|Removing)|VERBOSE Calling)",
00478
00479 r"^StatusCodeSvc.*listing all unchecked return codes:",
00480 r"^StatusCodeSvc\s*INFO\s*$",
00481 r"Num\s*|\s*Function\s*|\s*Source Library",
00482 r"^[-+]*\s*$",
00483
00484 r"ERROR Failed to modify file: .* Errno=2 No such file or directory",
00485
00486 r"^ +[0-9]+ \|.*ROOT",
00487 r"^ +[0-9]+ \|.*\|.*Dict",
00488
00489 r"^\*.*\*$",
00490
00491 r"SUCCESS\s*Booked \d+ Histogram\(s\)",
00492 r"^ \|",
00493 r"^ ID=",
00494 ] ) + normalizeExamples + skipEmptyLines + \
00495 normalizeEOL + \
00496 LineSorter("Services to release : ")
00497
00498 class ReferenceFileValidator:
00499 def __init__(self, reffile, cause, result_key, preproc = normalizeExamples):
00500 self.reffile = os.path.expandvars(reffile)
00501 self.cause = cause
00502 self.result_key = result_key
00503 self.preproc = preproc
00504 def __call__(self, stdout, result):
00505 causes = []
00506 if os.path.isfile(self.reffile):
00507 orig = open(self.reffile).xreadlines()
00508 if self.preproc:
00509 orig = self.preproc(orig)
00510 else:
00511 orig = []
00512
00513 new = stdout.splitlines()
00514 if self.preproc:
00515 new = self.preproc(new)
00516
00517 diffs = difflib.ndiff(orig,new,charjunk=difflib.IS_CHARACTER_JUNK)
00518 filterdiffs = map(lambda x: x.strip(),filter(lambda x: x[0] != " ",diffs))
00519
00520 if filterdiffs:
00521 result[self.result_key] = result.Quote("\n".join(filterdiffs))
00522 result[self.result_key] += result.Quote("""
00523 Legend:
00524 -) reference file
00525 +) standard output of the test""")
00526 causes.append(self.cause)
00527
00528 return causes
00529
00530
00531
00532
00533 def findReferenceBlock(reference, stdout, result, causes, signature_offset=0, signature=None,
00534 id = None):
00535 """
00536 Given a block of text, tries to find it in the output.
00537 The block had to be identified by a signature line. By default, the first
00538 line is used as signature, or the line pointed to by signature_offset. If
00539 signature_offset points outside the block, a signature line can be passed as
00540 signature argument. Note: if 'signature' is None (the default), a negative
00541 signature_offset is interpreted as index in a list (e.g. -1 means the last
00542 line), otherwise the it is interpreted as the number of lines before the
00543 first one of the block the signature must appear.
00544 The parameter 'id' allow to distinguish between different calls to this
00545 function in the same validation code.
00546 """
00547
00548 reflines = filter(None,map(lambda s: s.rstrip(), reference.splitlines()))
00549 if not reflines:
00550 raise RuntimeError("Empty (or null) reference")
00551
00552 outlines = filter(None,map(lambda s: s.rstrip(), stdout.splitlines()))
00553
00554 res_field = "GaudiTest.RefBlock"
00555 if id:
00556 res_field += "_%s" % id
00557
00558 if signature is None:
00559 if signature_offset < 0:
00560 signature_offset = len(reference)+signature_offset
00561 signature = reflines[signature_offset]
00562
00563 try:
00564 pos = outlines.index(signature)
00565 outlines = outlines[pos-signature_offset:pos+len(reflines)-signature_offset]
00566 if reflines != outlines:
00567 msg = "standard output"
00568
00569 if not msg in causes:
00570 causes.append(msg)
00571 result[res_field + ".observed"] = result.Quote("\n".join(outlines))
00572 except ValueError:
00573 causes.append("missing signature")
00574 result[res_field + ".signature"] = result.Quote(signature)
00575 if len(reflines) > 1 or signature != reflines[0]:
00576 result[res_field + ".expected"] = result.Quote("\n".join(reflines))
00577
00578 return causes
00579
00580 def countErrorLines(expected = {'ERROR':0, 'FATAL':0}, **kwargs):
00581 """
00582 Count the number of messages with required severity (by default ERROR and FATAL)
00583 and check if their numbers match the expected ones (0 by default).
00584 The dictionary "expected" can be used to tune the number of errors and fatals
00585 allowed, or to limit the number of expected warnings etc.
00586 """
00587 stdout = kwargs["stdout"]
00588 result = kwargs["result"]
00589 causes = kwargs["causes"]
00590
00591
00592 errors = {}
00593 for sev in expected:
00594 errors[sev] = []
00595
00596 outlines = stdout.splitlines()
00597 from math import log10
00598 fmt = "%%%dd - %%s" % (int(log10(len(outlines))+1))
00599
00600 linecount = 0
00601 for l in outlines:
00602 linecount += 1
00603 words = l.split()
00604 if len(words) >= 2 and words[1] in errors:
00605 errors[words[1]].append(fmt%(linecount,l.rstrip()))
00606
00607 for e in errors:
00608 if len(errors[e]) != expected[e]:
00609 causes.append('%s(%d)'%(e,len(errors[e])))
00610 result["GaudiTest.lines.%s"%e] = result.Quote('\n'.join(errors[e]))
00611 result["GaudiTest.lines.%s.expected#"%e] = result.Quote(str(expected[e]))
00612
00613 return causes
00614
00615
00616 def _parseTTreeSummary(lines, pos):
00617 """
00618 Parse the TTree summary table in lines, starting from pos.
00619 Returns a tuple with the dictionary with the digested informations and the
00620 position of the first line after the summary.
00621 """
00622 result = {}
00623 i = pos + 1
00624 count = len(lines)
00625
00626 splitcols = lambda l: [ f.strip() for f in l.strip("*\n").split(':',2) ]
00627 def parseblock(ll):
00628 r = {}
00629 cols = splitcols(ll[0])
00630 r["Name"], r["Title"] = cols[1:]
00631
00632 cols = splitcols(ll[1])
00633 r["Entries"] = int(cols[1])
00634
00635 sizes = cols[2].split()
00636 r["Total size"] = int(sizes[2])
00637 if sizes[-1] == "memory":
00638 r["File size"] = 0
00639 else:
00640 r["File size"] = int(sizes[-1])
00641
00642 cols = splitcols(ll[2])
00643 sizes = cols[2].split()
00644 if cols[0] == "Baskets":
00645 r["Baskets"] = int(cols[1])
00646 r["Basket size"] = int(sizes[2])
00647 r["Compression"] = float(sizes[-1])
00648 return r
00649
00650 if i < (count - 3) and lines[i].startswith("*Tree"):
00651 result = parseblock(lines[i:i+3])
00652 result["Branches"] = {}
00653 i += 4
00654 while i < (count - 3) and lines[i].startswith("*Br"):
00655 branch = parseblock(lines[i:i+3])
00656 result["Branches"][branch["Name"]] = branch
00657 i += 4
00658
00659 return (result, i)
00660
00661 def findTTreeSummaries(stdout):
00662 """
00663 Scan stdout to find ROOT TTree summaries and digest them.
00664 """
00665 stars = re.compile(r"^\*+$")
00666 outlines = stdout.splitlines()
00667 nlines = len(outlines)
00668 trees = {}
00669
00670 i = 0
00671 while i < nlines:
00672
00673 while i < nlines and not stars.match(outlines[i]):
00674 i += 1
00675 if i < nlines:
00676 tree, i = _parseTTreeSummary(outlines, i)
00677 if tree:
00678 trees[tree["Name"]] = tree
00679
00680 return trees
00681
00682 def cmpTreesDicts(reference, to_check, ignore = None):
00683 """
00684 Check that all the keys in reference are in to_check too, with the same value.
00685 If the value is a dict, the function is called recursively. to_check can
00686 contain more keys than reference, that will not be tested.
00687 The function returns at the first difference found.
00688 """
00689 fail_keys = []
00690
00691 if ignore:
00692 ignore_re = re.compile(ignore)
00693 keys = [ key for key in reference if not ignore_re.match(key) ]
00694 else:
00695 keys = reference.keys()
00696
00697 for k in keys:
00698 if k in to_check:
00699 if (type(reference[k]) is dict) and (type(to_check[k]) is dict):
00700
00701 failed = fail_keys = cmpTreesDicts(reference[k], to_check[k], ignore)
00702 else:
00703
00704 failed = to_check[k] != reference[k]
00705 else:
00706 to_check[k] = None
00707 failed = True
00708 if failed:
00709 fail_keys.insert(0, k)
00710 break
00711 return fail_keys
00712
00713 def getCmpFailingValues(reference, to_check, fail_path):
00714 c = to_check
00715 r = reference
00716 for k in fail_path:
00717 c = c.get(k,None)
00718 r = r.get(k,None)
00719 if c is None or r is None:
00720 break
00721 return (fail_path, r, c)
00722
00723
00724 h_count_re = re.compile(r"^(.*)SUCCESS\s+Booked (\d+) Histogram\(s\) :\s+(.*)")
00725
00726 def parseHistosSummary(lines, pos):
00727 """
00728 Extract the histograms infos from the lines starting at pos.
00729 Returns the position of the first line after the summary block.
00730 """
00731 global h_count_re
00732 h_table_head = re.compile(r'SUCCESS\s+List of booked (1D|2D|3D|1D profile|2D profile) histograms in directory\s+"(\w*)"')
00733 h_short_summ = re.compile(r"ID=([^\"]+)\s+\"([^\"]+)\"\s+(.*)")
00734
00735 nlines = len(lines)
00736
00737
00738 m = h_count_re.search(lines[pos])
00739 name = m.group(1).strip()
00740 total = int(m.group(2))
00741 header = {}
00742 for k, v in [ x.split("=") for x in m.group(3).split() ]:
00743 header[k] = int(v)
00744 pos += 1
00745 header["Total"] = total
00746
00747 summ = {}
00748 while pos < nlines:
00749 m = h_table_head.search(lines[pos])
00750 if m:
00751 t, d = m.groups(1)
00752 t = t.replace(" profile", "Prof")
00753 pos += 1
00754 if pos < nlines:
00755 l = lines[pos]
00756 else:
00757 l = ""
00758 cont = {}
00759 if l.startswith(" | ID"):
00760
00761 titles = [ x.strip() for x in l.split("|")][1:]
00762 pos += 1
00763 while pos < nlines and lines[pos].startswith(" |"):
00764 l = lines[pos]
00765 values = [ x.strip() for x in l.split("|")][1:]
00766 hcont = {}
00767 for i in range(len(titles)):
00768 hcont[titles[i]] = values[i]
00769 cont[hcont["ID"]] = hcont
00770 pos += 1
00771 elif l.startswith(" ID="):
00772 while pos < nlines and lines[pos].startswith(" ID="):
00773 values = [ x.strip() for x in h_short_summ.search(lines[pos]).groups() ]
00774 cont[values[0]] = values
00775 pos += 1
00776 else:
00777 raise RuntimeError("Cannot understand line %d: '%s'" % (pos, l))
00778 if not d in summ:
00779 summ[d] = {}
00780 summ[d][t] = cont
00781 summ[d]["header"] = header
00782 else:
00783 break
00784 if not summ:
00785
00786 summ[name] = {"header": header}
00787 return summ, pos
00788
00789 def findHistosSummaries(stdout):
00790 """
00791 Scan stdout to find ROOT TTree summaries and digest them.
00792 """
00793 outlines = stdout.splitlines()
00794 nlines = len(outlines) - 1
00795 summaries = {}
00796 global h_count_re
00797
00798 pos = 0
00799 while pos < nlines:
00800 summ = {}
00801
00802 match = h_count_re.search(outlines[pos])
00803 while pos < nlines and not match:
00804 pos += 1
00805 match = h_count_re.search(outlines[pos])
00806 if match:
00807 summ, pos = parseHistosSummary(outlines, pos)
00808 summaries.update(summ)
00809 return summaries
00810
00811 class GaudiFilterExecutable(qm.executable.Filter):
00812 def __init__(self, input, timeout = -1):
00813 """Create a new 'Filter'.
00814
00815 'input' -- The string containing the input to provide to the
00816 child process.
00817
00818 'timeout' -- As for 'TimeoutExecutable.__init__'."""
00819
00820 super(GaudiFilterExecutable, self).__init__(input, timeout)
00821 self.__input = input
00822 self.__timeout = timeout
00823 self.stack_trace_file = None
00824
00825
00826
00827 tmpf = tempfile.mkstemp()
00828 os.close(tmpf[0])
00829 self.stack_trace_file = tmpf[1]
00830
00831 def __UseSeparateProcessGroupForChild(self):
00832 """Copied from TimeoutExecutable to allow the re-implementation of
00833 _HandleChild.
00834 """
00835 if sys.platform == "win32":
00836
00837
00838
00839
00840 return 0
00841
00842 return self.__timeout >= 0 or self.__timeout == -2
00843
00844
00845 def _HandleChild(self):
00846 """Code copied from both FilterExecutable and TimeoutExecutable.
00847 """
00848
00849 if self._stdin_pipe:
00850 self._ClosePipeEnd(self._stdin_pipe[0])
00851 if self._stdout_pipe:
00852 self._ClosePipeEnd(self._stdout_pipe[1])
00853 if self._stderr_pipe:
00854 self._ClosePipeEnd(self._stderr_pipe[1])
00855
00856
00857
00858
00859
00860
00861
00862 super(qm.executable.TimeoutExecutable, self)._HandleChild()
00863
00864 if self.__UseSeparateProcessGroupForChild():
00865
00866
00867
00868
00869 child_pid = self._GetChildPID()
00870 try:
00871 os.setpgid(child_pid, child_pid)
00872 except:
00873
00874
00875
00876
00877 pass
00878
00879
00880
00881
00882
00883
00884
00885
00886
00887 self.__monitor_pid = os.fork()
00888 if self.__monitor_pid != 0:
00889
00890
00891
00892
00893 os.setpgid(self.__monitor_pid, child_pid)
00894 else:
00895
00896
00897
00898
00899 os.setpgid(0, child_pid)
00900
00901
00902
00903
00904
00905
00906
00907 try:
00908 max_fds = os.sysconf("SC_OPEN_MAX")
00909 except:
00910 max_fds = 256
00911 for fd in xrange(max_fds):
00912 try:
00913 os.close(fd)
00914 except:
00915 pass
00916 try:
00917 if self.__timeout >= 0:
00918
00919 time.sleep (self.__timeout)
00920
00921
00922 if sys.platform == "linux2":
00923 cmd = ["gdb",
00924 os.path.join("/proc", str(child_pid), "exe"),
00925 str(child_pid),
00926 "-batch", "-n", "-x",
00927 "'%s'" % os.path.join(os.path.dirname(__file__), "stack-trace.gdb")]
00928
00929
00930 o = os.popen(" ".join(cmd)).read()
00931 open(self.stack_trace_file,"w").write(o)
00932
00933
00934
00935 os.kill(0, signal.SIGKILL)
00936 else:
00937
00938 select.select ([], [], [])
00939 finally:
00940
00941
00942 os._exit(0)
00943 elif self.__timeout >= 0 and sys.platform == "win32":
00944
00945 self.__monitor_thread = Thread(target = self.__Monitor)
00946 self.__monitor_thread.start()
00947
00948 if sys.platform == "win32":
00949
00950 def __Monitor(self):
00951 """Code copied from FilterExecutable.
00952 Kill the child if the timeout expires.
00953
00954 This function is run in the monitoring thread."""
00955
00956
00957
00958
00959 timeout = int(self.__timeout * 1000)
00960
00961
00962 result = win32event.WaitForSingleObject(self._GetChildPID(),
00963 timeout)
00964
00965 if result == win32con.WAIT_TIMEOUT:
00966 self.Kill()
00967
00968
00969
00970
00971 class GaudiExeTest(ExecTestBase):
00972 """Standard Gaudi test.
00973 """
00974 arguments = [
00975 qm.fields.TextField(
00976 name="program",
00977 title="Program",
00978 not_empty_text=1,
00979 description="""The path to the program.
00980
00981 This field indicates the path to the program. If it is not
00982 an absolute path, the value of the 'PATH' environment
00983 variable will be used to search for the program.
00984 If not specified, $GAUDIEXE or Gaudi.exe are used.
00985 """
00986 ),
00987 qm.fields.SetField(qm.fields.TextField(
00988 name="args",
00989 title="Argument List",
00990 description="""The command-line arguments.
00991
00992 If this field is left blank, the program is run without any
00993 arguments.
00994
00995 Use this field to specify the option files.
00996
00997 An implicit 0th argument (the path to the program) is added
00998 automatically."""
00999 )),
01000 qm.fields.TextField(
01001 name="options",
01002 title="Options",
01003 description="""Options to be passed to the application.
01004
01005 This field allows to pass a list of options to the main program
01006 without the need of a separate option file.
01007
01008 The content of the field is written to a temporary file which name
01009 is passed the the application as last argument (appended to the
01010 field "Argument List".
01011 """,
01012 verbatim="true",
01013 multiline="true",
01014 default_value=""
01015 ),
01016 qm.fields.TextField(
01017 name="workdir",
01018 title="Working Directory",
01019 description="""Path to the working directory.
01020
01021 If this field is left blank, the program will be run from the qmtest
01022 directory, otherwise from the directory specified.""",
01023 default_value=""
01024 ),
01025 qm.fields.TextField(
01026 name="reference",
01027 title="Reference Output",
01028 description="""Path to the file containing the reference output.
01029
01030 If this field is left blank, any standard output will be considered
01031 valid.
01032
01033 If the reference file is specified, any output on standard error is
01034 ignored."""
01035 ),
01036 qm.fields.TextField(
01037 name="error_reference",
01038 title="Reference for standard error",
01039 description="""Path to the file containing the reference for the standard error.
01040
01041 If this field is left blank, any standard output will be considered
01042 valid.
01043
01044 If the reference file is specified, any output on standard error is
01045 ignored."""
01046 ),
01047 qm.fields.SetField(qm.fields.TextField(
01048 name = "unsupported_platforms",
01049 title = "Unsupported Platforms",
01050 description = """Platform on which the test must not be run.
01051
01052 List of regular expressions identifying the platforms on which the
01053 test is not run and the result is set to UNTESTED."""
01054 )),
01055
01056 qm.fields.TextField(
01057 name = "validator",
01058 title = "Validator",
01059 description = """Function to validate the output of the test.
01060
01061 If defined, the function is used to validate the products of the
01062 test.
01063 The function is called passing as arguments:
01064 self: the test class instance
01065 stdout: the standard output of the executed test
01066 stderr: the standard error of the executed test
01067 result: the Result objects to fill with messages
01068 The function must return a list of causes for the failure.
01069 If specified, overrides standard output, standard error and
01070 reference files.
01071 """,
01072 verbatim="true",
01073 multiline="true",
01074 default_value=""
01075 ),
01076
01077 qm.fields.BooleanField(
01078 name = "use_temp_dir",
01079 title = "Use temporary directory",
01080 description = """Use temporary directory.
01081
01082 If set to true, use a temporary directory as working directory.
01083 """,
01084 default_value="false"
01085 ),
01086 ]
01087
01088 def PlatformIsNotSupported(self, context, result):
01089 platform = self.GetPlatform()
01090 unsupported = [ re.compile(x)
01091 for x in [ str(y).strip()
01092 for y in self.unsupported_platforms ]
01093 if x
01094 ]
01095 for p_re in unsupported:
01096 if p_re.search(platform):
01097 result.SetOutcome(result.UNTESTED)
01098 result[result.CAUSE] = 'Platform not supported.'
01099 return True
01100 return False
01101
01102 def GetPlatform(self):
01103 """
01104 Return the platform Id defined in CMTCONFIG or SCRAM_ARCH.
01105 """
01106 arch = "None"
01107
01108 if "CMTCONFIG" in os.environ:
01109 arch = os.environ["CMTCONFIG"]
01110 elif "SCRAM_ARCH" in os.environ:
01111 arch = os.environ["SCRAM_ARCH"]
01112 return arch
01113
01114 def _expandReferenceFileName(self, reffile):
01115
01116 if not reffile:
01117 return ""
01118
01119 reference = os.path.normpath(os.path.expandvars(reffile))
01120
01121 spec_ref = reference[:-3] + self.GetPlatform()[0:3] + reference[-3:]
01122 if os.path.isfile(spec_ref):
01123 reference = spec_ref
01124 else:
01125
01126 dirname, basename = os.path.split(reference)
01127 if not dirname: dirname = '.'
01128 head = basename + "."
01129 head_len = len(head)
01130 platform = self.GetPlatform()
01131 candidates = []
01132 for f in os.listdir(dirname):
01133 if f.startswith(head) and platform.startswith(f[head_len:]):
01134 candidates.append( (len(f) - head_len, f) )
01135 if candidates:
01136 candidates.sort()
01137 reference = os.path.join(dirname, candidates[-1][1])
01138 return reference
01139
01140 def CheckTTreesSummaries(self, stdout, result, causes,
01141 trees_dict = None,
01142 ignore = r"Basket|.*size|Compression"):
01143 """
01144 Compare the TTree summaries in stdout with the ones in trees_dict or in
01145 the reference file. By default ignore the size, compression and basket
01146 fields.
01147 The presence of TTree summaries when none is expected is not a failure.
01148 """
01149 if trees_dict is None:
01150 reference = self._expandReferenceFileName(self.reference)
01151
01152 if reference and os.path.isfile(reference):
01153 trees_dict = findTTreeSummaries(open(reference).read())
01154 else:
01155 trees_dict = {}
01156
01157 from pprint import PrettyPrinter
01158 pp = PrettyPrinter()
01159 if trees_dict:
01160 result["GaudiTest.TTrees.expected"] = result.Quote(pp.pformat(trees_dict))
01161 if ignore:
01162 result["GaudiTest.TTrees.ignore"] = result.Quote(ignore)
01163
01164 trees = findTTreeSummaries(stdout)
01165 failed = cmpTreesDicts(trees_dict, trees, ignore)
01166 if failed:
01167 causes.append("trees summaries")
01168 msg = "%s: %s != %s" % getCmpFailingValues(trees_dict, trees, failed)
01169 result["GaudiTest.TTrees.failure_on"] = result.Quote(msg)
01170 result["GaudiTest.TTrees.found"] = result.Quote(pp.pformat(trees))
01171
01172 return causes
01173
01174 def CheckHistosSummaries(self, stdout, result, causes,
01175 dict = None,
01176 ignore = None):
01177 """
01178 Compare the TTree summaries in stdout with the ones in trees_dict or in
01179 the reference file. By default ignore the size, compression and basket
01180 fields.
01181 The presence of TTree summaries when none is expected is not a failure.
01182 """
01183 if dict is None:
01184 reference = self._expandReferenceFileName(self.reference)
01185
01186 if reference and os.path.isfile(reference):
01187 dict = findHistosSummaries(open(reference).read())
01188 else:
01189 dict = {}
01190
01191 from pprint import PrettyPrinter
01192 pp = PrettyPrinter()
01193 if dict:
01194 result["GaudiTest.Histos.expected"] = result.Quote(pp.pformat(dict))
01195 if ignore:
01196 result["GaudiTest.Histos.ignore"] = result.Quote(ignore)
01197
01198 histos = findHistosSummaries(stdout)
01199 failed = cmpTreesDicts(dict, histos, ignore)
01200 if failed:
01201 causes.append("histos summaries")
01202 msg = "%s: %s != %s" % getCmpFailingValues(dict, histos, failed)
01203 result["GaudiTest.Histos.failure_on"] = result.Quote(msg)
01204 result["GaudiTest.Histos.found"] = result.Quote(pp.pformat(histos))
01205
01206 return causes
01207
01208 def ValidateWithReference(self, stdout, stderr, result, causes, preproc = None):
01209 """
01210 Default validation action: compare standard output and error to the
01211 reference files.
01212 """
01213
01214 if preproc is None:
01215 preproc = normalizeExamples
01216
01217 reference = self._expandReferenceFileName(self.reference)
01218
01219 if reference and os.path.isfile(reference):
01220 result["GaudiTest.output_reference"] = reference
01221 causes += ReferenceFileValidator(reference,
01222 "standard output",
01223 "GaudiTest.output_diff",
01224 preproc = preproc)(stdout, result)
01225
01226
01227 causes = self.CheckTTreesSummaries(stdout, result, causes)
01228 causes = self.CheckHistosSummaries(stdout, result, causes)
01229
01230 if causes:
01231 try:
01232 newref = open(reference + ".new","w")
01233
01234 for l in stdout.splitlines():
01235 newref.write(l.rstrip() + '\n')
01236 del newref
01237 except IOError:
01238
01239
01240 pass
01241
01242
01243 reference = self._expandReferenceFileName(self.error_reference)
01244
01245 if reference and os.path.isfile(reference):
01246 result["GaudiTest.error_reference"] = reference
01247 newcauses = ReferenceFileValidator(reference,
01248 "standard error",
01249 "GaudiTest.error_diff",
01250 preproc = preproc)(stderr, result)
01251 causes += newcauses
01252 if newcauses:
01253 newref = open(reference + ".new","w")
01254
01255 for l in stderr.splitlines():
01256 newref.write(l.rstrip() + '\n')
01257 del newref
01258 else:
01259 causes += BasicOutputValidator(self.stderr,
01260 "standard error",
01261 "ExecTest.expected_stderr")(stderr, result)
01262
01263 return causes
01264
01265 def ValidateOutput(self, stdout, stderr, result):
01266 causes = []
01267
01268 if self.validator.strip() != "":
01269 class CallWrapper(object):
01270 """
01271 Small wrapper class to dynamically bind some default arguments
01272 to a callable.
01273 """
01274 def __init__(self, callable, extra_args = {}):
01275 self.callable = callable
01276 self.extra_args = extra_args
01277
01278 from inspect import getargspec
01279 self.args_order = getargspec(callable)[0]
01280
01281
01282 if self.args_order[0] == "self":
01283 del self.args_order[0]
01284 def __call__(self, *args, **kwargs):
01285
01286 positional = self.args_order[:len(args)]
01287
01288 kwargs = dict(kwargs)
01289 for a in self.extra_args:
01290
01291
01292 if a not in positional and a not in kwargs:
01293 kwargs[a] = self.extra_args[a]
01294 return apply(self.callable, args, kwargs)
01295
01296 exported_symbols = {"self":self,
01297 "stdout":stdout,
01298 "stderr":stderr,
01299 "result":result,
01300 "causes":causes,
01301 "findReferenceBlock":
01302 CallWrapper(findReferenceBlock, {"stdout":stdout,
01303 "result":result,
01304 "causes":causes}),
01305 "validateWithReference":
01306 CallWrapper(self.ValidateWithReference, {"stdout":stdout,
01307 "stderr":stderr,
01308 "result":result,
01309 "causes":causes}),
01310 "countErrorLines":
01311 CallWrapper(countErrorLines, {"stdout":stdout,
01312 "result":result,
01313 "causes":causes}),
01314 "checkTTreesSummaries":
01315 CallWrapper(self.CheckTTreesSummaries, {"stdout":stdout,
01316 "result":result,
01317 "causes":causes}),
01318 "checkHistosSummaries":
01319 CallWrapper(self.CheckHistosSummaries, {"stdout":stdout,
01320 "result":result,
01321 "causes":causes}),
01322
01323 }
01324 exec self.validator in globals(), exported_symbols
01325 else:
01326 self.ValidateWithReference(stdout, stderr, result, causes)
01327
01328 return causes
01329
01330 def DumpEnvironment(self, result):
01331 """
01332 Add the content of the environment to the result object.
01333
01334 Copied from the QMTest class of COOL.
01335 """
01336 vars = os.environ.keys()
01337 vars.sort()
01338 result['GaudiTest.environment'] = \
01339 result.Quote('\n'.join(["%s=%s"%(v,os.environ[v]) for v in vars]))
01340
01341 def _find_program(self,prog):
01342
01343
01344 if not os.path.isabs(prog) and not os.path.isfile(prog):
01345 for d in os.environ["PATH"].split(os.pathsep):
01346 p = os.path.join(d,prog)
01347 if os.path.isfile(p):
01348 return p
01349 return prog
01350
01351 def Run(self, context, result):
01352 """Run the test.
01353
01354 'context' -- A 'Context' giving run-time parameters to the
01355 test.
01356
01357 'result' -- A 'Result' object. The outcome will be
01358 'Result.PASS' when this method is called. The 'result' may be
01359 modified by this method to indicate outcomes other than
01360 'Result.PASS' or to add annotations."""
01361
01362
01363 if self.PlatformIsNotSupported(context, result):
01364 return
01365
01366
01367 if self.program:
01368 prog = rationalizepath(self.program)
01369 elif "GAUDIEXE" in os.environ:
01370 prog = os.environ["GAUDIEXE"]
01371 else:
01372 prog = "Gaudi.exe"
01373 self.program = prog
01374
01375 dummy, prog_ext = os.path.splitext(prog)
01376 if prog_ext not in [ ".exe", ".py", ".bat" ] and self.GetPlatform()[0:3] == "win":
01377 prog += ".exe"
01378 prog_ext = ".exe"
01379
01380 prog = self._find_program(prog)
01381
01382
01383 args = map(rationalizepath, self.args)
01384 self.reference = rationalizepath(self.reference)
01385 self.error_reference = rationalizepath(self.error_reference)
01386
01387
01388
01389 tmpfile = None
01390 if self.options.strip():
01391 ext = ".opts"
01392 if re.search(r"from\s*Gaudi.Configuration\s*import\s*\*", self.options):
01393 ext = ".py"
01394 tmpfile = TempFile(ext)
01395 tmpfile.writelines("\n".join(self.options.splitlines()))
01396 tmpfile.flush()
01397 args.append(tmpfile.name)
01398 result["GaudiTest.options"] = result.Quote(self.options)
01399
01400
01401 if prog_ext == ".py":
01402 args.insert(0,prog)
01403 if self.GetPlatform()[0:3] == "win":
01404 prog = self._find_program("python.exe")
01405 else:
01406 prog = self._find_program("python")
01407
01408
01409 origdir = os.getcwd()
01410 if self.workdir:
01411 os.chdir(str(os.path.normpath(os.path.expandvars(self.workdir))))
01412 elif self.use_temp_dir == "true":
01413 if "QMTEST_TMPDIR" in os.environ:
01414 os.chdir(os.environ["QMTEST_TMPDIR"])
01415 elif "qmtest.tmpdir" in context:
01416 os.chdir(context["qmtest.tmpdir"])
01417
01418 if "QMTEST_IGNORE_TIMEOUT" not in os.environ:
01419 self.timeout = max(self.timeout,600)
01420 else:
01421 self.timeout = -1
01422
01423 try:
01424
01425 self._CreateEclipseLaunch(prog, args, destdir = origdir)
01426
01427 self.RunProgram(prog,
01428 [ prog ] + args,
01429 context, result)
01430
01431 if result.GetOutcome() not in [ result.PASS ]:
01432 self.DumpEnvironment(result)
01433 finally:
01434
01435 os.chdir(origdir)
01436
01437 def RunProgram(self, program, arguments, context, result):
01438 """Run the 'program'.
01439
01440 'program' -- The path to the program to run.
01441
01442 'arguments' -- A list of the arguments to the program. This
01443 list must contain a first argument corresponding to 'argv[0]'.
01444
01445 'context' -- A 'Context' giving run-time parameters to the
01446 test.
01447
01448 'result' -- A 'Result' object. The outcome will be
01449 'Result.PASS' when this method is called. The 'result' may be
01450 modified by this method to indicate outcomes other than
01451 'Result.PASS' or to add annotations.
01452
01453 @attention: This method has been copied from command.ExecTestBase
01454 (QMTest 2.3.0) and modified to keep stdout and stderr
01455 for tests that have been terminated by a signal.
01456 (Fundamental for debugging in the Application Area)
01457 """
01458
01459
01460 environment = self.MakeEnvironment(context)
01461
01462 if self.timeout >= 0:
01463 timeout = self.timeout
01464 else:
01465
01466
01467
01468
01469
01470 timeout = -2
01471 e = GaudiFilterExecutable(self.stdin, timeout)
01472
01473 exit_status = e.Run(arguments, environment, path = program)
01474
01475 if e.stack_trace_file and os.path.exists(e.stack_trace_file):
01476 stack_trace = open(e.stack_trace_file).read()
01477 os.remove(e.stack_trace_file)
01478 else:
01479 stack_trace = None
01480 if stack_trace:
01481 result["ExecTest.stack_trace"] = result.Quote(stack_trace)
01482
01483
01484 if sys.platform == "win32" or os.WIFEXITED(exit_status):
01485
01486 causes = []
01487
01488
01489 if self.exit_code is None:
01490 exit_code = None
01491 elif sys.platform == "win32":
01492 exit_code = exit_status
01493 else:
01494 exit_code = os.WEXITSTATUS(exit_status)
01495
01496 stdout = e.stdout
01497 stderr = e.stderr
01498
01499 result["ExecTest.exit_code"] = str(exit_code)
01500 result["ExecTest.stdout"] = result.Quote(stdout)
01501 result["ExecTest.stderr"] = result.Quote(stderr)
01502
01503 if exit_code != self.exit_code:
01504 causes.append("exit_code")
01505 result["ExecTest.expected_exit_code"] \
01506 = str(self.exit_code)
01507
01508 causes += self.ValidateOutput(stdout, stderr, result)
01509
01510 if causes:
01511 result.Fail("Unexpected %s." % string.join(causes, ", "))
01512 elif os.WIFSIGNALED(exit_status):
01513
01514
01515 signal_number = str(os.WTERMSIG(exit_status))
01516 if not stack_trace:
01517 result.Fail("Program terminated by signal.")
01518 else:
01519
01520
01521 result.Fail("Exceeded time limit (%ds), terminated." % timeout)
01522 result["ExecTest.signal_number"] = signal_number
01523 result["ExecTest.stdout"] = result.Quote(e.stdout)
01524 result["ExecTest.stderr"] = result.Quote(e.stderr)
01525 elif os.WIFSTOPPED(exit_status):
01526
01527
01528 signal_number = str(os.WSTOPSIG(exit_status))
01529 if not stack_trace:
01530 result.Fail("Program stopped by signal.")
01531 else:
01532
01533
01534 result.Fail("Exceeded time limit (%ds), stopped." % timeout)
01535 result["ExecTest.signal_number"] = signal_number
01536 result["ExecTest.stdout"] = result.Quote(e.stdout)
01537 result["ExecTest.stderr"] = result.Quote(e.stderr)
01538 else:
01539
01540
01541 result.Fail("Program did not terminate normally.")
01542
01543
01544
01545 esc = '\x1b'
01546 repr_esc = '\\x1b'
01547 result["ExecTest.stdout"] = result["ExecTest.stdout"].replace(esc,repr_esc)
01548
01549
01550
01551 def _CreateEclipseLaunch(self, prog, args, destdir = None):
01552
01553
01554 projbasedir = os.path.normpath(destdir)
01555 while not os.path.exists(os.path.join(projbasedir, ".project")):
01556 oldprojdir = projbasedir
01557 projbasedir = os.path.normpath(os.path.join(projbasedir, os.pardir))
01558
01559
01560 if oldprojdir == projbasedir:
01561
01562 return
01563
01564 from xml.etree import ElementTree as ET
01565 t = ET.parse(os.path.join(projbasedir, ".project"))
01566 projectName = t.find("name").text
01567
01568
01569 destfile = "%s.launch" % self._Runnable__id
01570 if destdir:
01571 destfile = os.path.join(destdir, destfile)
01572
01573 if self.options.strip():
01574
01575
01576
01577 tempfile = args.pop()
01578 optsfile = destfile + os.path.splitext(tempfile)[1]
01579 shutil.copyfile(tempfile, optsfile)
01580 args.append(optsfile)
01581
01582
01583 from xml.sax.saxutils import quoteattr
01584 data = {}
01585
01586
01587 data["environment"] = "\n".join(['<mapEntry key=%s value=%s/>' % (quoteattr(k), quoteattr(v))
01588 for k, v in os.environ.iteritems()])
01589
01590 data["exec"] = which(prog)
01591
01592 data["args"] = " ".join(map(rationalizepath, args))
01593
01594 if not self.use_temp_dir:
01595 data["workdir"] = os.getcwd()
01596 else:
01597
01598
01599 data["workdir"] = destdir
01600
01601 data["project"] = projectName.strip()
01602
01603
01604 xml = """<?xml version="1.0" encoding="UTF-8" standalone="no"?>
01605 <launchConfiguration type="org.eclipse.cdt.launch.applicationLaunchType">
01606 <booleanAttribute key="org.eclipse.cdt.debug.mi.core.AUTO_SOLIB" value="true"/>
01607 <listAttribute key="org.eclipse.cdt.debug.mi.core.AUTO_SOLIB_LIST"/>
01608 <stringAttribute key="org.eclipse.cdt.debug.mi.core.DEBUG_NAME" value="gdb"/>
01609 <stringAttribute key="org.eclipse.cdt.debug.mi.core.GDB_INIT" value=".gdbinit"/>
01610 <listAttribute key="org.eclipse.cdt.debug.mi.core.SOLIB_PATH"/>
01611 <booleanAttribute key="org.eclipse.cdt.debug.mi.core.STOP_ON_SOLIB_EVENTS" value="false"/>
01612 <stringAttribute key="org.eclipse.cdt.debug.mi.core.protocol" value="mi"/>
01613 <stringAttribute key="org.eclipse.cdt.launch.COREFILE_PATH" value=""/>
01614 <stringAttribute key="org.eclipse.cdt.launch.DEBUGGER_ID" value="org.eclipse.cdt.debug.mi.core.CDebugger"/>
01615 <stringAttribute key="org.eclipse.cdt.launch.DEBUGGER_REGISTER_GROUPS" value=""/>
01616 <stringAttribute key="org.eclipse.cdt.launch.DEBUGGER_START_MODE" value="run"/>
01617 <booleanAttribute key="org.eclipse.cdt.launch.DEBUGGER_STOP_AT_MAIN" value="true"/>
01618 <stringAttribute key="org.eclipse.cdt.launch.DEBUGGER_STOP_AT_MAIN_SYMBOL" value="main"/>
01619 <booleanAttribute key="org.eclipse.cdt.launch.ENABLE_REGISTER_BOOKKEEPING" value="false"/>
01620 <booleanAttribute key="org.eclipse.cdt.launch.ENABLE_VARIABLE_BOOKKEEPING" value="false"/>
01621 <stringAttribute key="org.eclipse.cdt.launch.FORMAT" value="<?xml version="1.0" encoding="UTF-8" standalone="no"?><contentList/>"/>
01622 <stringAttribute key="org.eclipse.cdt.launch.GLOBAL_VARIABLES" value="<?xml version="1.0" encoding="UTF-8" standalone="no"?> <globalVariableList/> "/>
01623 <stringAttribute key="org.eclipse.cdt.launch.MEMORY_BLOCKS" value="<?xml version="1.0" encoding="UTF-8" standalone="no"?> <memoryBlockExpressionList/> "/>
01624 <stringAttribute key="org.eclipse.cdt.launch.PROGRAM_ARGUMENTS" value="%(args)s"/>
01625 <stringAttribute key="org.eclipse.cdt.launch.PROGRAM_NAME" value="%(exec)s"/>
01626 <stringAttribute key="org.eclipse.cdt.launch.PROJECT_ATTR" value="%(project)s"/>
01627 <stringAttribute key="org.eclipse.cdt.launch.PROJECT_BUILD_CONFIG_ID_ATTR" value=""/>
01628 <stringAttribute key="org.eclipse.cdt.launch.WORKING_DIRECTORY" value="%(workdir)s"/>
01629 <booleanAttribute key="org.eclipse.cdt.launch.use_terminal" value="true"/>
01630 <listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_PATHS">
01631 <listEntry value="/%(project)s"/>
01632 </listAttribute>
01633 <listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_TYPES">
01634 <listEntry value="4"/>
01635 </listAttribute>
01636 <booleanAttribute key="org.eclipse.debug.core.appendEnvironmentVariables" value="false"/>
01637 <mapAttribute key="org.eclipse.debug.core.environmentVariables">
01638 %(environment)s
01639 </mapAttribute>
01640 <mapAttribute key="org.eclipse.debug.core.preferred_launchers">
01641 <mapEntry key="[debug]" value="org.eclipse.cdt.cdi.launch.localCLaunch"/>
01642 </mapAttribute>
01643 <listAttribute key="org.eclipse.debug.ui.favoriteGroups">
01644 <listEntry value="org.eclipse.debug.ui.launchGroup.debug"/>
01645 </listAttribute>
01646 </launchConfiguration>
01647 """ % data
01648
01649
01650 open(destfile, "w").write(xml)
01651
01652
01653
01654 try:
01655 import json
01656 except ImportError:
01657
01658 sys.path.append(os.path.dirname(__file__))
01659 import simplejson as json
01660
01661 class HTMLResultStream(ResultStream):
01662 """An 'HTMLResultStream' writes its output to a set of HTML files.
01663
01664 The argument 'dir' is used to select the destination directory for the HTML
01665 report.
01666 The destination directory may already contain the report from a previous run
01667 (for example of a different package), in which case it will be extended to
01668 include the new data.
01669 """
01670 arguments = [
01671 qm.fields.TextField(
01672 name = "dir",
01673 title = "Destination Directory",
01674 description = """The name of the directory.
01675
01676 All results will be written to the directory indicated.""",
01677 verbatim = "true",
01678 default_value = ""),
01679 ]
01680
01681 def __init__(self, arguments = None, **args):
01682 """Prepare the destination directory.
01683
01684 Creates the destination directory and store in it some preliminary
01685 annotations and the static files found in the template directory
01686 'html_report'.
01687 """
01688 ResultStream.__init__(self, arguments, **args)
01689 self._summary = []
01690 self._summaryFile = os.path.join(self.dir, "summary.json")
01691 self._annotationsFile = os.path.join(self.dir, "annotations.json")
01692
01693 templateDir = os.path.join(os.path.dirname(__file__), "html_report")
01694 if not os.path.isdir(self.dir):
01695 os.makedirs(self.dir)
01696
01697 for f in os.listdir(templateDir):
01698 src = os.path.join(templateDir, f)
01699 dst = os.path.join(self.dir, f)
01700 if not os.path.isdir(src) and not os.path.exists(dst):
01701 shutil.copy(src, dst)
01702
01703 if "CMTCONFIG" in os.environ:
01704 self.WriteAnnotation("cmt.cmtconfig", os.environ["CMTCONFIG"])
01705 import socket
01706 self.WriteAnnotation("hostname", socket.gethostname())
01707
01708 def _updateSummary(self):
01709 """Helper function to extend the global summary file in the destination
01710 directory.
01711 """
01712 if os.path.exists(self._summaryFile):
01713 oldSummary = json.load(open(self._summaryFile))
01714 else:
01715 oldSummary = []
01716 ids = set([ i["id"] for i in self._summary ])
01717 newSummary = [ i for i in oldSummary if i["id"] not in ids ]
01718 newSummary.extend(self._summary)
01719 json.dump(newSummary, open(self._summaryFile, "w"),
01720 sort_keys = True)
01721
01722 def WriteAnnotation(self, key, value):
01723 """Writes the annotation to the annotation file.
01724 If the key is already present with a different value, the value becomes
01725 a list and the new value is appended to it, except for start_time and
01726 end_time.
01727 """
01728
01729 if os.path.exists(self._annotationsFile):
01730 annotations = json.load(open(self._annotationsFile))
01731 else:
01732 annotations = {}
01733
01734 key, value = map(str, [key, value])
01735 if key == "qmtest.run.start_time":
01736
01737
01738
01739
01740 if key not in annotations:
01741 annotations[key] = value
01742 if "qmtest.run.end_time" in annotations:
01743 del annotations["qmtest.run.end_time"]
01744 else:
01745
01746 if key in annotations:
01747 old = annotations[key]
01748 if type(old) is list:
01749 if value not in old:
01750 annotations[key].append(value)
01751 elif value != old:
01752 annotations[key] = [old, value]
01753 else:
01754 annotations[key] = value
01755
01756 json.dump(annotations, open(self._annotationsFile, "w"),
01757 sort_keys = True)
01758
01759 def WriteResult(self, result):
01760 """Prepare the test result directory in the destination directory storing
01761 into it the result fields.
01762 A summary of the test result is stored both in a file in the test directory
01763 and in the global summary file.
01764 """
01765 summary = {}
01766 summary["id"] = result.GetId()
01767 summary["outcome"] = result.GetOutcome()
01768 summary["cause"] = result.GetCause()
01769 summary["fields"] = result.keys()
01770 summary["fields"].sort()
01771
01772
01773 for f in ["id", "outcome", "cause"]:
01774 summary[f] = str(summary[f])
01775 summary["fields"] = map(str, summary["fields"])
01776
01777 self._summary.append(summary)
01778
01779
01780
01781
01782
01783 testOutDir = os.path.join(self.dir, summary["id"])
01784 if not os.path.isdir(testOutDir):
01785 os.makedirs(testOutDir)
01786 json.dump(summary, open(os.path.join(testOutDir, "summary.json"), "w"),
01787 sort_keys = True)
01788 for f in summary["fields"]:
01789 open(os.path.join(testOutDir, f), "w").write(result[f])
01790
01791 self._updateSummary()
01792
01793 def Summarize(self):
01794
01795 pass