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 "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
01120 platformSplit = lambda p: set(p.split('-' in p and '-' or '_'))
01121
01122 reference = os.path.normpath(os.path.expandvars(reffile))
01123
01124 spec_ref = reference[:-3] + self.GetPlatform()[0:3] + reference[-3:]
01125 if os.path.isfile(spec_ref):
01126 reference = spec_ref
01127 else:
01128
01129 dirname, basename = os.path.split(reference)
01130 if not dirname: dirname = '.'
01131 head = basename + "."
01132 head_len = len(head)
01133 platform = platformSplit(self.GetPlatform())
01134 candidates = []
01135 for f in os.listdir(dirname):
01136 if f.startswith(head):
01137 req_plat = platformSplit(f[head_len:])
01138 if platform.issuperset(req_plat):
01139 candidates.append( (len(req_plat), f) )
01140 if candidates:
01141
01142
01143 candidates.sort()
01144 reference = os.path.join(dirname, candidates[-1][1])
01145 return reference
01146
01147 def CheckTTreesSummaries(self, stdout, result, causes,
01148 trees_dict = None,
01149 ignore = r"Basket|.*size|Compression"):
01150 """
01151 Compare the TTree summaries in stdout with the ones in trees_dict or in
01152 the reference file. By default ignore the size, compression and basket
01153 fields.
01154 The presence of TTree summaries when none is expected is not a failure.
01155 """
01156 if trees_dict is None:
01157 reference = self._expandReferenceFileName(self.reference)
01158
01159 if reference and os.path.isfile(reference):
01160 trees_dict = findTTreeSummaries(open(reference).read())
01161 else:
01162 trees_dict = {}
01163
01164 from pprint import PrettyPrinter
01165 pp = PrettyPrinter()
01166 if trees_dict:
01167 result["GaudiTest.TTrees.expected"] = result.Quote(pp.pformat(trees_dict))
01168 if ignore:
01169 result["GaudiTest.TTrees.ignore"] = result.Quote(ignore)
01170
01171 trees = findTTreeSummaries(stdout)
01172 failed = cmpTreesDicts(trees_dict, trees, ignore)
01173 if failed:
01174 causes.append("trees summaries")
01175 msg = "%s: %s != %s" % getCmpFailingValues(trees_dict, trees, failed)
01176 result["GaudiTest.TTrees.failure_on"] = result.Quote(msg)
01177 result["GaudiTest.TTrees.found"] = result.Quote(pp.pformat(trees))
01178
01179 return causes
01180
01181 def CheckHistosSummaries(self, stdout, result, causes,
01182 dict = None,
01183 ignore = None):
01184 """
01185 Compare the TTree summaries in stdout with the ones in trees_dict or in
01186 the reference file. By default ignore the size, compression and basket
01187 fields.
01188 The presence of TTree summaries when none is expected is not a failure.
01189 """
01190 if dict is None:
01191 reference = self._expandReferenceFileName(self.reference)
01192
01193 if reference and os.path.isfile(reference):
01194 dict = findHistosSummaries(open(reference).read())
01195 else:
01196 dict = {}
01197
01198 from pprint import PrettyPrinter
01199 pp = PrettyPrinter()
01200 if dict:
01201 result["GaudiTest.Histos.expected"] = result.Quote(pp.pformat(dict))
01202 if ignore:
01203 result["GaudiTest.Histos.ignore"] = result.Quote(ignore)
01204
01205 histos = findHistosSummaries(stdout)
01206 failed = cmpTreesDicts(dict, histos, ignore)
01207 if failed:
01208 causes.append("histos summaries")
01209 msg = "%s: %s != %s" % getCmpFailingValues(dict, histos, failed)
01210 result["GaudiTest.Histos.failure_on"] = result.Quote(msg)
01211 result["GaudiTest.Histos.found"] = result.Quote(pp.pformat(histos))
01212
01213 return causes
01214
01215 def ValidateWithReference(self, stdout, stderr, result, causes, preproc = None):
01216 """
01217 Default validation action: compare standard output and error to the
01218 reference files.
01219 """
01220
01221 if preproc is None:
01222 preproc = normalizeExamples
01223
01224 reference = self._expandReferenceFileName(self.reference)
01225
01226 if reference and os.path.isfile(reference):
01227 result["GaudiTest.output_reference"] = reference
01228 causes += ReferenceFileValidator(reference,
01229 "standard output",
01230 "GaudiTest.output_diff",
01231 preproc = preproc)(stdout, result)
01232
01233
01234 causes = self.CheckTTreesSummaries(stdout, result, causes)
01235 causes = self.CheckHistosSummaries(stdout, result, causes)
01236
01237 if causes:
01238 try:
01239 newref = open(reference + ".new","w")
01240
01241 for l in stdout.splitlines():
01242 newref.write(l.rstrip() + '\n')
01243 del newref
01244 except IOError:
01245
01246
01247 pass
01248
01249
01250 reference = self._expandReferenceFileName(self.error_reference)
01251
01252 if reference and os.path.isfile(reference):
01253 result["GaudiTest.error_reference"] = reference
01254 newcauses = ReferenceFileValidator(reference,
01255 "standard error",
01256 "GaudiTest.error_diff",
01257 preproc = preproc)(stderr, result)
01258 causes += newcauses
01259 if newcauses:
01260 newref = open(reference + ".new","w")
01261
01262 for l in stderr.splitlines():
01263 newref.write(l.rstrip() + '\n')
01264 del newref
01265 else:
01266 causes += BasicOutputValidator(self.stderr,
01267 "standard error",
01268 "ExecTest.expected_stderr")(stderr, result)
01269
01270 return causes
01271
01272 def ValidateOutput(self, stdout, stderr, result):
01273 causes = []
01274
01275 if self.validator.strip() != "":
01276 class CallWrapper(object):
01277 """
01278 Small wrapper class to dynamically bind some default arguments
01279 to a callable.
01280 """
01281 def __init__(self, callable, extra_args = {}):
01282 self.callable = callable
01283 self.extra_args = extra_args
01284
01285 from inspect import getargspec
01286 self.args_order = getargspec(callable)[0]
01287
01288
01289 if self.args_order[0] == "self":
01290 del self.args_order[0]
01291 def __call__(self, *args, **kwargs):
01292
01293 positional = self.args_order[:len(args)]
01294
01295 kwargs = dict(kwargs)
01296 for a in self.extra_args:
01297
01298
01299 if a not in positional and a not in kwargs:
01300 kwargs[a] = self.extra_args[a]
01301 return apply(self.callable, args, kwargs)
01302
01303 exported_symbols = {"self":self,
01304 "stdout":stdout,
01305 "stderr":stderr,
01306 "result":result,
01307 "causes":causes,
01308 "findReferenceBlock":
01309 CallWrapper(findReferenceBlock, {"stdout":stdout,
01310 "result":result,
01311 "causes":causes}),
01312 "validateWithReference":
01313 CallWrapper(self.ValidateWithReference, {"stdout":stdout,
01314 "stderr":stderr,
01315 "result":result,
01316 "causes":causes}),
01317 "countErrorLines":
01318 CallWrapper(countErrorLines, {"stdout":stdout,
01319 "result":result,
01320 "causes":causes}),
01321 "checkTTreesSummaries":
01322 CallWrapper(self.CheckTTreesSummaries, {"stdout":stdout,
01323 "result":result,
01324 "causes":causes}),
01325 "checkHistosSummaries":
01326 CallWrapper(self.CheckHistosSummaries, {"stdout":stdout,
01327 "result":result,
01328 "causes":causes}),
01329
01330 }
01331 exec self.validator in globals(), exported_symbols
01332 else:
01333 self.ValidateWithReference(stdout, stderr, result, causes)
01334
01335 return causes
01336
01337 def DumpEnvironment(self, result):
01338 """
01339 Add the content of the environment to the result object.
01340
01341 Copied from the QMTest class of COOL.
01342 """
01343 vars = os.environ.keys()
01344 vars.sort()
01345 result['GaudiTest.environment'] = \
01346 result.Quote('\n'.join(["%s=%s"%(v,os.environ[v]) for v in vars]))
01347
01348 def _find_program(self,prog):
01349
01350
01351 if not os.path.isabs(prog) and not os.path.isfile(prog):
01352 for d in os.environ["PATH"].split(os.pathsep):
01353 p = os.path.join(d,prog)
01354 if os.path.isfile(p):
01355 return p
01356 return prog
01357
01358 def Run(self, context, result):
01359 """Run the test.
01360
01361 'context' -- A 'Context' giving run-time parameters to the
01362 test.
01363
01364 'result' -- A 'Result' object. The outcome will be
01365 'Result.PASS' when this method is called. The 'result' may be
01366 modified by this method to indicate outcomes other than
01367 'Result.PASS' or to add annotations."""
01368
01369
01370 if self.PlatformIsNotSupported(context, result):
01371 return
01372
01373
01374 if self.program:
01375 prog = rationalizepath(self.program)
01376 elif "GAUDIEXE" in os.environ:
01377 prog = os.environ["GAUDIEXE"]
01378 else:
01379 prog = "Gaudi.exe"
01380 self.program = prog
01381
01382 dummy, prog_ext = os.path.splitext(prog)
01383 if prog_ext not in [ ".exe", ".py", ".bat" ] and self.GetPlatform()[0:3] == "win":
01384 prog += ".exe"
01385 prog_ext = ".exe"
01386
01387 prog = self._find_program(prog)
01388
01389
01390 args = map(rationalizepath, self.args)
01391 self.reference = rationalizepath(self.reference)
01392 self.error_reference = rationalizepath(self.error_reference)
01393
01394
01395
01396 tmpfile = None
01397 if self.options.strip():
01398 ext = ".opts"
01399 if re.search(r"from\s*Gaudi.Configuration\s*import\s*\*", self.options):
01400 ext = ".py"
01401 tmpfile = TempFile(ext)
01402 tmpfile.writelines("\n".join(self.options.splitlines()))
01403 tmpfile.flush()
01404 args.append(tmpfile.name)
01405 result["GaudiTest.options"] = result.Quote(self.options)
01406
01407
01408 if prog_ext == ".py":
01409 args.insert(0,prog)
01410 if self.GetPlatform()[0:3] == "win":
01411 prog = self._find_program("python.exe")
01412 else:
01413 prog = self._find_program("python")
01414
01415
01416 origdir = os.getcwd()
01417 if self.workdir:
01418 os.chdir(str(os.path.normpath(os.path.expandvars(self.workdir))))
01419 elif self.use_temp_dir == "true":
01420 if "QMTEST_TMPDIR" in os.environ:
01421 os.chdir(os.environ["QMTEST_TMPDIR"])
01422 elif "qmtest.tmpdir" in context:
01423 os.chdir(context["qmtest.tmpdir"])
01424
01425 if "QMTEST_IGNORE_TIMEOUT" not in os.environ:
01426 self.timeout = max(self.timeout,600)
01427 else:
01428 self.timeout = -1
01429
01430 try:
01431
01432 self._CreateEclipseLaunch(prog, args, destdir = origdir)
01433
01434 self.RunProgram(prog,
01435 [ prog ] + args,
01436 context, result)
01437
01438 if result.GetOutcome() not in [ result.PASS ]:
01439 self.DumpEnvironment(result)
01440 finally:
01441
01442 os.chdir(origdir)
01443
01444 def RunProgram(self, program, arguments, context, result):
01445 """Run the 'program'.
01446
01447 'program' -- The path to the program to run.
01448
01449 'arguments' -- A list of the arguments to the program. This
01450 list must contain a first argument corresponding to 'argv[0]'.
01451
01452 'context' -- A 'Context' giving run-time parameters to the
01453 test.
01454
01455 'result' -- A 'Result' object. The outcome will be
01456 'Result.PASS' when this method is called. The 'result' may be
01457 modified by this method to indicate outcomes other than
01458 'Result.PASS' or to add annotations.
01459
01460 @attention: This method has been copied from command.ExecTestBase
01461 (QMTest 2.3.0) and modified to keep stdout and stderr
01462 for tests that have been terminated by a signal.
01463 (Fundamental for debugging in the Application Area)
01464 """
01465
01466
01467 environment = self.MakeEnvironment(context)
01468
01469 if self.timeout >= 0:
01470 timeout = self.timeout
01471 else:
01472
01473
01474
01475
01476
01477 timeout = -2
01478 e = GaudiFilterExecutable(self.stdin, timeout)
01479
01480 exit_status = e.Run(arguments, environment, path = program)
01481
01482 if e.stack_trace_file and os.path.exists(e.stack_trace_file):
01483 stack_trace = open(e.stack_trace_file).read()
01484 os.remove(e.stack_trace_file)
01485 else:
01486 stack_trace = None
01487 if stack_trace:
01488 result["ExecTest.stack_trace"] = result.Quote(stack_trace)
01489
01490
01491 if sys.platform == "win32" or os.WIFEXITED(exit_status):
01492
01493 causes = []
01494
01495
01496 if self.exit_code is None:
01497 exit_code = None
01498 elif sys.platform == "win32":
01499 exit_code = exit_status
01500 else:
01501 exit_code = os.WEXITSTATUS(exit_status)
01502
01503 stdout = e.stdout
01504 stderr = e.stderr
01505
01506 result["ExecTest.exit_code"] = str(exit_code)
01507 result["ExecTest.stdout"] = result.Quote(stdout)
01508 result["ExecTest.stderr"] = result.Quote(stderr)
01509
01510 if exit_code != self.exit_code:
01511 causes.append("exit_code")
01512 result["ExecTest.expected_exit_code"] \
01513 = str(self.exit_code)
01514
01515 causes += self.ValidateOutput(stdout, stderr, result)
01516
01517 if causes:
01518 result.Fail("Unexpected %s." % string.join(causes, ", "))
01519 elif os.WIFSIGNALED(exit_status):
01520
01521
01522 signal_number = str(os.WTERMSIG(exit_status))
01523 if not stack_trace:
01524 result.Fail("Program terminated by signal.")
01525 else:
01526
01527
01528 result.Fail("Exceeded time limit (%ds), terminated." % timeout)
01529 result["ExecTest.signal_number"] = signal_number
01530 result["ExecTest.stdout"] = result.Quote(e.stdout)
01531 result["ExecTest.stderr"] = result.Quote(e.stderr)
01532 elif os.WIFSTOPPED(exit_status):
01533
01534
01535 signal_number = str(os.WSTOPSIG(exit_status))
01536 if not stack_trace:
01537 result.Fail("Program stopped by signal.")
01538 else:
01539
01540
01541 result.Fail("Exceeded time limit (%ds), stopped." % timeout)
01542 result["ExecTest.signal_number"] = signal_number
01543 result["ExecTest.stdout"] = result.Quote(e.stdout)
01544 result["ExecTest.stderr"] = result.Quote(e.stderr)
01545 else:
01546
01547
01548 result.Fail("Program did not terminate normally.")
01549
01550
01551
01552 esc = '\x1b'
01553 repr_esc = '\\x1b'
01554 result["ExecTest.stdout"] = result["ExecTest.stdout"].replace(esc,repr_esc)
01555
01556
01557
01558 def _CreateEclipseLaunch(self, prog, args, destdir = None):
01559
01560
01561 projbasedir = os.path.normpath(destdir)
01562 while not os.path.exists(os.path.join(projbasedir, ".project")):
01563 oldprojdir = projbasedir
01564 projbasedir = os.path.normpath(os.path.join(projbasedir, os.pardir))
01565
01566
01567 if oldprojdir == projbasedir:
01568
01569 return
01570
01571 from xml.etree import ElementTree as ET
01572 t = ET.parse(os.path.join(projbasedir, ".project"))
01573 projectName = t.find("name").text
01574
01575
01576 destfile = "%s.launch" % self._Runnable__id
01577 if destdir:
01578 destfile = os.path.join(destdir, destfile)
01579
01580 if self.options.strip():
01581
01582
01583
01584 tempfile = args.pop()
01585 optsfile = destfile + os.path.splitext(tempfile)[1]
01586 shutil.copyfile(tempfile, optsfile)
01587 args.append(optsfile)
01588
01589
01590 from xml.sax.saxutils import quoteattr
01591 data = {}
01592
01593
01594 data["environment"] = "\n".join(['<mapEntry key=%s value=%s/>' % (quoteattr(k), quoteattr(v))
01595 for k, v in os.environ.iteritems()])
01596
01597 data["exec"] = which(prog)
01598 if os.path.basename(data["exec"]).lower().startswith("python"):
01599 data["stopAtMain"] = "false"
01600 else:
01601 data["stopAtMain"] = "true"
01602
01603 data["args"] = " ".join(map(rationalizepath, args))
01604
01605 if not self.use_temp_dir:
01606 data["workdir"] = os.getcwd()
01607 else:
01608
01609
01610 data["workdir"] = destdir
01611
01612 data["project"] = projectName.strip()
01613
01614
01615 xml = """<?xml version="1.0" encoding="UTF-8" standalone="no"?>
01616 <launchConfiguration type="org.eclipse.cdt.launch.applicationLaunchType">
01617 <booleanAttribute key="org.eclipse.cdt.debug.mi.core.AUTO_SOLIB" value="true"/>
01618 <listAttribute key="org.eclipse.cdt.debug.mi.core.AUTO_SOLIB_LIST"/>
01619 <stringAttribute key="org.eclipse.cdt.debug.mi.core.DEBUG_NAME" value="gdb"/>
01620 <stringAttribute key="org.eclipse.cdt.debug.mi.core.GDB_INIT" value=".gdbinit"/>
01621 <listAttribute key="org.eclipse.cdt.debug.mi.core.SOLIB_PATH"/>
01622 <booleanAttribute key="org.eclipse.cdt.debug.mi.core.STOP_ON_SOLIB_EVENTS" value="false"/>
01623 <booleanAttribute key="org.eclipse.cdt.debug.mi.core.breakpointsFullPath" value="false"/>
01624 <stringAttribute key="org.eclipse.cdt.debug.mi.core.commandFactory" value="org.eclipse.cdt.debug.mi.core.standardCommandFactory"/>
01625 <stringAttribute key="org.eclipse.cdt.debug.mi.core.protocol" value="mi"/>
01626 <booleanAttribute key="org.eclipse.cdt.debug.mi.core.verboseMode" value="false"/>
01627 <intAttribute key="org.eclipse.cdt.launch.ATTR_BUILD_BEFORE_LAUNCH_ATTR" value="0"/>
01628 <stringAttribute key="org.eclipse.cdt.launch.COREFILE_PATH" value=""/>
01629 <stringAttribute key="org.eclipse.cdt.launch.DEBUGGER_ID" value="org.eclipse.cdt.debug.mi.core.CDebuggerNew"/>
01630 <stringAttribute key="org.eclipse.cdt.launch.DEBUGGER_REGISTER_GROUPS" value=""/>
01631 <stringAttribute key="org.eclipse.cdt.launch.DEBUGGER_START_MODE" value="run"/>
01632 <booleanAttribute key="org.eclipse.cdt.launch.DEBUGGER_STOP_AT_MAIN" value="%(stopAtMain)s"/>
01633 <stringAttribute key="org.eclipse.cdt.launch.DEBUGGER_STOP_AT_MAIN_SYMBOL" value="main"/>
01634 <booleanAttribute key="org.eclipse.cdt.launch.ENABLE_REGISTER_BOOKKEEPING" value="false"/>
01635 <booleanAttribute key="org.eclipse.cdt.launch.ENABLE_VARIABLE_BOOKKEEPING" value="false"/>
01636 <stringAttribute key="org.eclipse.cdt.launch.FORMAT" value="<?xml version="1.0" encoding="UTF-8" standalone="no"?><contentList/>"/>
01637 <stringAttribute key="org.eclipse.cdt.launch.GLOBAL_VARIABLES" value="<?xml version="1.0" encoding="UTF-8" standalone="no"?> <globalVariableList/> "/>
01638 <stringAttribute key="org.eclipse.cdt.launch.MEMORY_BLOCKS" value="<?xml version="1.0" encoding="UTF-8" standalone="no"?> <memoryBlockExpressionList/> "/>
01639 <stringAttribute key="org.eclipse.cdt.launch.PROGRAM_ARGUMENTS" value="%(args)s"/>
01640 <stringAttribute key="org.eclipse.cdt.launch.PROGRAM_NAME" value="%(exec)s"/>
01641 <stringAttribute key="org.eclipse.cdt.launch.PROJECT_ATTR" value="%(project)s"/>
01642 <stringAttribute key="org.eclipse.cdt.launch.PROJECT_BUILD_CONFIG_ID_ATTR" value=""/>
01643 <stringAttribute key="org.eclipse.cdt.launch.WORKING_DIRECTORY" value="%(workdir)s"/>
01644 <booleanAttribute key="org.eclipse.cdt.launch.ui.ApplicationCDebuggerTab.DEFAULTS_SET" value="true"/>
01645 <booleanAttribute key="org.eclipse.cdt.launch.use_terminal" value="true"/>
01646 <listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_PATHS">
01647 <listEntry value="/%(project)s"/>
01648 </listAttribute>
01649 <listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_TYPES">
01650 <listEntry value="4"/>
01651 </listAttribute>
01652 <booleanAttribute key="org.eclipse.debug.core.appendEnvironmentVariables" value="false"/>
01653 <mapAttribute key="org.eclipse.debug.core.environmentVariables">
01654 %(environment)s
01655 </mapAttribute>
01656 <mapAttribute key="org.eclipse.debug.core.preferred_launchers">
01657 <mapEntry key="[debug]" value="org.eclipse.cdt.cdi.launch.localCLaunch"/>
01658 </mapAttribute>
01659 <listAttribute key="org.eclipse.debug.ui.favoriteGroups">
01660 <listEntry value="org.eclipse.debug.ui.launchGroup.debug"/>
01661 </listAttribute>
01662 </launchConfiguration>
01663 """ % data
01664
01665
01666 open(destfile, "w").write(xml)
01667
01668
01669
01670 try:
01671 import json
01672 except ImportError:
01673
01674 import simplejson as json
01675
01676 class HTMLResultStream(ResultStream):
01677 """An 'HTMLResultStream' writes its output to a set of HTML files.
01678
01679 The argument 'dir' is used to select the destination directory for the HTML
01680 report.
01681 The destination directory may already contain the report from a previous run
01682 (for example of a different package), in which case it will be extended to
01683 include the new data.
01684 """
01685 arguments = [
01686 qm.fields.TextField(
01687 name = "dir",
01688 title = "Destination Directory",
01689 description = """The name of the directory.
01690
01691 All results will be written to the directory indicated.""",
01692 verbatim = "true",
01693 default_value = ""),
01694 ]
01695
01696 def __init__(self, arguments = None, **args):
01697 """Prepare the destination directory.
01698
01699 Creates the destination directory and store in it some preliminary
01700 annotations and the static files found in the template directory
01701 'html_report'.
01702 """
01703 ResultStream.__init__(self, arguments, **args)
01704 self._summary = []
01705 self._summaryFile = os.path.join(self.dir, "summary.json")
01706 self._annotationsFile = os.path.join(self.dir, "annotations.json")
01707
01708 templateDir = os.path.join(os.path.dirname(__file__), "html_report")
01709 if not os.path.isdir(self.dir):
01710 os.makedirs(self.dir)
01711
01712 for f in os.listdir(templateDir):
01713 src = os.path.join(templateDir, f)
01714 dst = os.path.join(self.dir, f)
01715 if not os.path.isdir(src) and not os.path.exists(dst):
01716 shutil.copy(src, dst)
01717
01718 if "CMTCONFIG" in os.environ:
01719 self.WriteAnnotation("cmt.cmtconfig", os.environ["CMTCONFIG"])
01720 import socket
01721 self.WriteAnnotation("hostname", socket.gethostname())
01722
01723 def _updateSummary(self):
01724 """Helper function to extend the global summary file in the destination
01725 directory.
01726 """
01727 if os.path.exists(self._summaryFile):
01728 oldSummary = json.load(open(self._summaryFile))
01729 else:
01730 oldSummary = []
01731 ids = set([ i["id"] for i in self._summary ])
01732 newSummary = [ i for i in oldSummary if i["id"] not in ids ]
01733 newSummary.extend(self._summary)
01734 json.dump(newSummary, open(self._summaryFile, "w"),
01735 sort_keys = True)
01736
01737 def WriteAnnotation(self, key, value):
01738 """Writes the annotation to the annotation file.
01739 If the key is already present with a different value, the value becomes
01740 a list and the new value is appended to it, except for start_time and
01741 end_time.
01742 """
01743
01744 if os.path.exists(self._annotationsFile):
01745 annotations = json.load(open(self._annotationsFile))
01746 else:
01747 annotations = {}
01748
01749 key, value = map(str, [key, value])
01750 if key == "qmtest.run.start_time":
01751
01752
01753
01754
01755 if key not in annotations:
01756 annotations[key] = value
01757 if "qmtest.run.end_time" in annotations:
01758 del annotations["qmtest.run.end_time"]
01759 else:
01760
01761 if key in annotations:
01762 old = annotations[key]
01763 if type(old) is list:
01764 if value not in old:
01765 annotations[key].append(value)
01766 elif value != old:
01767 annotations[key] = [old, value]
01768 else:
01769 annotations[key] = value
01770
01771 json.dump(annotations, open(self._annotationsFile, "w"),
01772 sort_keys = True)
01773
01774 def WriteResult(self, result):
01775 """Prepare the test result directory in the destination directory storing
01776 into it the result fields.
01777 A summary of the test result is stored both in a file in the test directory
01778 and in the global summary file.
01779 """
01780 summary = {}
01781 summary["id"] = result.GetId()
01782 summary["outcome"] = result.GetOutcome()
01783 summary["cause"] = result.GetCause()
01784 summary["fields"] = result.keys()
01785 summary["fields"].sort()
01786
01787
01788 for f in ["id", "outcome", "cause"]:
01789 summary[f] = str(summary[f])
01790 summary["fields"] = map(str, summary["fields"])
01791
01792 self._summary.append(summary)
01793
01794
01795
01796
01797
01798 testOutDir = os.path.join(self.dir, summary["id"])
01799 if not os.path.isdir(testOutDir):
01800 os.makedirs(testOutDir)
01801 json.dump(summary, open(os.path.join(testOutDir, "summary.json"), "w"),
01802 sort_keys = True)
01803 for f in summary["fields"]:
01804 open(os.path.join(testOutDir, f), "w").write(result[f])
01805
01806 self._updateSummary()
01807
01808 def Summarize(self):
01809
01810 pass