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 """
00265 Locates an executable in the executables path ($PATH) and returns the full
00266 path to it. An application is looked for with or without the '.exe' suffix.
00267 If the executable cannot be found, None is returned
00268 """
00269 if os.path.isabs(executable):
00270 if not os.path.exists(executable):
00271 if executable.endswith('.exe'):
00272 if os.path.exists(executable[:-4]):
00273 return executable[:-4]
00274 return executable
00275 for d in os.environ.get("PATH").split(os.pathsep):
00276 fullpath = os.path.join(d, executable)
00277 if os.path.exists(fullpath):
00278 return fullpath
00279 if executable.endswith('.exe'):
00280 return which(executable[:-4])
00281 return None
00282
00283 def rationalizepath(p):
00284 p = os.path.normpath(os.path.expandvars(p))
00285 if os.path.exists(p):
00286 p = os.path.realpath(p)
00287 return p
00288
00289
00290
00291
00292 class BasicOutputValidator:
00293 """Basic implementation of an option validator for Gaudi tests.
00294 This implementation is based on the standard (LCG) validation functions
00295 used in QMTest.
00296 """
00297 def __init__(self,ref,cause,result_key):
00298 self.reference = ref
00299 self.cause = cause
00300 self.result_key = result_key
00301
00302 def __call__(self, out, result):
00303 """Validate the output of the program.
00304
00305 'stdout' -- A string containing the data written to the standard output
00306 stream.
00307
00308 'stderr' -- A string containing the data written to the standard error
00309 stream.
00310
00311 'result' -- A 'Result' object. It may be used to annotate
00312 the outcome according to the content of stderr.
00313
00314 returns -- A list of strings giving causes of failure."""
00315
00316 causes = []
00317
00318 if not self.__CompareText(out, self.reference):
00319 causes.append(self.cause)
00320 result[self.result_key] = result.Quote(self.reference)
00321
00322 return causes
00323
00324 def __CompareText(self, s1, s2):
00325 """Compare 's1' and 's2', ignoring line endings.
00326
00327 's1' -- A string.
00328
00329 's2' -- A string.
00330
00331 returns -- True if 's1' and 's2' are the same, ignoring
00332 differences in line endings."""
00333
00334
00335
00336 return s1.splitlines() == s2.splitlines()
00337
00338 class FilePreprocessor:
00339 """ Base class for a callable that takes a file and returns a modified
00340 version of it."""
00341 def __processLine__(self, line):
00342 return line
00343 def __call__(self, input):
00344 if hasattr(input,"__iter__"):
00345 lines = input
00346 mergeback = False
00347 else:
00348 lines = input.splitlines()
00349 mergeback = True
00350 output = []
00351 for l in lines:
00352 l = self.__processLine__(l)
00353 if l: output.append(l)
00354 if mergeback: output = '\n'.join(output)
00355 return output
00356 def __add__(self, rhs):
00357 return FilePreprocessorSequence([self,rhs])
00358
00359 class FilePreprocessorSequence(FilePreprocessor):
00360 def __init__(self, members = []):
00361 self.members = members
00362 def __add__(self, rhs):
00363 return FilePreprocessorSequence(self.members + [rhs])
00364 def __call__(self, input):
00365 output = input
00366 for pp in self.members:
00367 output = pp(output)
00368 return output
00369
00370 class LineSkipper(FilePreprocessor):
00371 def __init__(self, strings = [], regexps = []):
00372 import re
00373 self.strings = strings
00374 self.regexps = map(re.compile,regexps)
00375
00376 def __processLine__(self, line):
00377 for s in self.strings:
00378 if line.find(s) >= 0: return None
00379 for r in self.regexps:
00380 if r.search(line): return None
00381 return line
00382
00383 class BlockSkipper(FilePreprocessor):
00384 def __init__(self, start, end):
00385 self.start = start
00386 self.end = end
00387 self._skipping = False
00388
00389 def __processLine__(self, line):
00390 if self.start in line:
00391 self._skipping = True
00392 return None
00393 elif self.end in line:
00394 self._skipping = False
00395 elif self._skipping:
00396 return None
00397 return line
00398
00399 class RegexpReplacer(FilePreprocessor):
00400 def __init__(self, orig, repl = "", when = None):
00401 if when:
00402 when = re.compile(when)
00403 self._operations = [ (when, re.compile(orig), repl) ]
00404 def __add__(self,rhs):
00405 if isinstance(rhs, RegexpReplacer):
00406 res = RegexpReplacer("","",None)
00407 res._operations = self._operations + rhs._operations
00408 else:
00409 res = FilePreprocessor.__add__(self, rhs)
00410 return res
00411 def __processLine__(self, line):
00412 for w,o,r in self._operations:
00413 if w is None or w.search(line):
00414 line = o.sub(r, line)
00415 return line
00416
00417
00418 maskPointers = RegexpReplacer("0x[0-9a-fA-F]{4,16}","0x########")
00419 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)?",
00420 "00:00:00 1970-01-01")
00421 normalizeEOL = FilePreprocessor()
00422 normalizeEOL.__processLine__ = lambda line: str(line).rstrip() + '\n'
00423
00424 skipEmptyLines = FilePreprocessor()
00425
00426 skipEmptyLines.__processLine__ = lambda line: (line.strip() and line) or None
00427
00428
00429
00430 class LineSorter(FilePreprocessor):
00431 def __init__(self, signature):
00432 self.signature = signature
00433 self.siglen = len(signature)
00434 def __processLine__(self, line):
00435 pos = line.find(self.signature)
00436 if pos >=0:
00437 line = line[:(pos+self.siglen)]
00438 lst = line[(pos+self.siglen):].split()
00439 lst.sort()
00440 line += " ".join(lst)
00441 return line
00442
00443
00444 normalizeExamples = maskPointers + normalizeDate
00445 for w,o,r in [
00446
00447 ("TIMER.TIMER",r"\s+[+-]?[0-9]+[0-9.]*", " 0"),
00448 ("release all pending",r"^.*/([^/]*:.*)",r"\1"),
00449 ("0x########",r"\[.*/([^/]*.*)\]",r"[\1]"),
00450 ("^#.*file",r"file '.*[/\\]([^/\\]*)$",r"file '\1"),
00451 ("^JobOptionsSvc.*options successfully read in from",r"read in from .*[/\\]([^/\\]*)$",r"file \1"),
00452
00453 (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"),
00454
00455 ("ServiceLocatorHelper::", "ServiceLocatorHelper::(create|locate)Service", "ServiceLocatorHelper::service"),
00456
00457 (None, r"e([-+])0([0-9][0-9])", r"e\1\2"),
00458 ]:
00459 normalizeExamples += RegexpReplacer(o,r,w)
00460 normalizeExamples = LineSkipper(["//GP:",
00461 "JobOptionsSvc INFO # ",
00462 "JobOptionsSvc WARNING # ",
00463 "Time User",
00464 "Welcome to",
00465 "This machine has a speed",
00466 "TIME:",
00467 "running on",
00468 "ToolSvc.Sequenc... INFO",
00469 "DataListenerSvc INFO XML written to file:",
00470 "[INFO]","[WARNING]",
00471 "DEBUG No writable file catalog found which contains FID:",
00472 "0 local",
00473 "DEBUG Service base class initialized successfully",
00474 "DEBUG Incident timing:",
00475 "INFO 'CnvServices':[",
00476
00477 'Note: (file "(tmpfile)", line 2) File "set" already loaded',
00478
00479 'SIGXCPU',
00480 ],regexps = [
00481 r"^JobOptionsSvc INFO *$",
00482 r"^#",
00483 r"(Always|SUCCESS)\s*(Root f|[^ ]* F)ile version:",
00484 r"0x[0-9a-fA-F#]+ *Algorithm::sysInitialize\(\) *\[",
00485 r"0x[0-9a-fA-F#]* *__gxx_personality_v0 *\[",
00486 r"File '.*.xml' does not exist",
00487 r"INFO Refer to dataset .* by its file ID:",
00488 r"INFO Referring to dataset .* by its file ID:",
00489 r"INFO Disconnect from dataset",
00490 r"INFO Disconnected from dataset",
00491 r"INFO Disconnected data IO:",
00492 r"IncidentSvc\s*(DEBUG (Adding|Removing)|VERBOSE Calling)",
00493
00494 r"^StatusCodeSvc.*listing all unchecked return codes:",
00495 r"^StatusCodeSvc\s*INFO\s*$",
00496 r"Num\s*\|\s*Function\s*\|\s*Source Library",
00497 r"^[-+]*\s*$",
00498
00499 r"ERROR Failed to modify file: .* Errno=2 No such file or directory",
00500
00501 r"^ +[0-9]+ \|.*ROOT",
00502 r"^ +[0-9]+ \|.*\|.*Dict",
00503
00504 r"^\*.*\*$",
00505
00506 r"SUCCESS\s*Booked \d+ Histogram\(s\)",
00507 r"^ \|",
00508 r"^ ID=",
00509 ] ) + normalizeExamples + skipEmptyLines + \
00510 normalizeEOL + \
00511 LineSorter("Services to release : ")
00512
00513 class ReferenceFileValidator:
00514 def __init__(self, reffile, cause, result_key, preproc = normalizeExamples):
00515 self.reffile = os.path.expandvars(reffile)
00516 self.cause = cause
00517 self.result_key = result_key
00518 self.preproc = preproc
00519 def __call__(self, stdout, result):
00520 causes = []
00521 if os.path.isfile(self.reffile):
00522 orig = open(self.reffile).xreadlines()
00523 if self.preproc:
00524 orig = self.preproc(orig)
00525 else:
00526 orig = []
00527
00528 new = stdout.splitlines()
00529 if self.preproc:
00530 new = self.preproc(new)
00531
00532 diffs = difflib.ndiff(orig,new,charjunk=difflib.IS_CHARACTER_JUNK)
00533 filterdiffs = map(lambda x: x.strip(),filter(lambda x: x[0] != " ",diffs))
00534
00535 if filterdiffs:
00536 result[self.result_key] = result.Quote("\n".join(filterdiffs))
00537 result[self.result_key] += result.Quote("""
00538 Legend:
00539 -) reference file
00540 +) standard output of the test""")
00541 causes.append(self.cause)
00542
00543 return causes
00544
00545
00546
00547
00548 def findReferenceBlock(reference, stdout, result, causes, signature_offset=0, signature=None,
00549 id = None):
00550 """
00551 Given a block of text, tries to find it in the output.
00552 The block had to be identified by a signature line. By default, the first
00553 line is used as signature, or the line pointed to by signature_offset. If
00554 signature_offset points outside the block, a signature line can be passed as
00555 signature argument. Note: if 'signature' is None (the default), a negative
00556 signature_offset is interpreted as index in a list (e.g. -1 means the last
00557 line), otherwise the it is interpreted as the number of lines before the
00558 first one of the block the signature must appear.
00559 The parameter 'id' allow to distinguish between different calls to this
00560 function in the same validation code.
00561 """
00562
00563 reflines = filter(None,map(lambda s: s.rstrip(), reference.splitlines()))
00564 if not reflines:
00565 raise RuntimeError("Empty (or null) reference")
00566
00567 outlines = filter(None,map(lambda s: s.rstrip(), stdout.splitlines()))
00568
00569 res_field = "GaudiTest.RefBlock"
00570 if id:
00571 res_field += "_%s" % id
00572
00573 if signature is None:
00574 if signature_offset < 0:
00575 signature_offset = len(reference)+signature_offset
00576 signature = reflines[signature_offset]
00577
00578 try:
00579 pos = outlines.index(signature)
00580 outlines = outlines[pos-signature_offset:pos+len(reflines)-signature_offset]
00581 if reflines != outlines:
00582 msg = "standard output"
00583
00584 if not msg in causes:
00585 causes.append(msg)
00586 result[res_field + ".observed"] = result.Quote("\n".join(outlines))
00587 except ValueError:
00588 causes.append("missing signature")
00589 result[res_field + ".signature"] = result.Quote(signature)
00590 if len(reflines) > 1 or signature != reflines[0]:
00591 result[res_field + ".expected"] = result.Quote("\n".join(reflines))
00592
00593 return causes
00594
00595 def countErrorLines(expected = {'ERROR':0, 'FATAL':0}, **kwargs):
00596 """
00597 Count the number of messages with required severity (by default ERROR and FATAL)
00598 and check if their numbers match the expected ones (0 by default).
00599 The dictionary "expected" can be used to tune the number of errors and fatals
00600 allowed, or to limit the number of expected warnings etc.
00601 """
00602 stdout = kwargs["stdout"]
00603 result = kwargs["result"]
00604 causes = kwargs["causes"]
00605
00606
00607 errors = {}
00608 for sev in expected:
00609 errors[sev] = []
00610
00611 outlines = stdout.splitlines()
00612 from math import log10
00613 fmt = "%%%dd - %%s" % (int(log10(len(outlines))+1))
00614
00615 linecount = 0
00616 for l in outlines:
00617 linecount += 1
00618 words = l.split()
00619 if len(words) >= 2 and words[1] in errors:
00620 errors[words[1]].append(fmt%(linecount,l.rstrip()))
00621
00622 for e in errors:
00623 if len(errors[e]) != expected[e]:
00624 causes.append('%s(%d)'%(e,len(errors[e])))
00625 result["GaudiTest.lines.%s"%e] = result.Quote('\n'.join(errors[e]))
00626 result["GaudiTest.lines.%s.expected#"%e] = result.Quote(str(expected[e]))
00627
00628 return causes
00629
00630
00631 def _parseTTreeSummary(lines, pos):
00632 """
00633 Parse the TTree summary table in lines, starting from pos.
00634 Returns a tuple with the dictionary with the digested informations and the
00635 position of the first line after the summary.
00636 """
00637 result = {}
00638 i = pos + 1
00639 count = len(lines)
00640
00641 splitcols = lambda l: [ f.strip() for f in l.strip("*\n").split(':',2) ]
00642 def parseblock(ll):
00643 r = {}
00644 cols = splitcols(ll[0])
00645 r["Name"], r["Title"] = cols[1:]
00646
00647 cols = splitcols(ll[1])
00648 r["Entries"] = int(cols[1])
00649
00650 sizes = cols[2].split()
00651 r["Total size"] = int(sizes[2])
00652 if sizes[-1] == "memory":
00653 r["File size"] = 0
00654 else:
00655 r["File size"] = int(sizes[-1])
00656
00657 cols = splitcols(ll[2])
00658 sizes = cols[2].split()
00659 if cols[0] == "Baskets":
00660 r["Baskets"] = int(cols[1])
00661 r["Basket size"] = int(sizes[2])
00662 r["Compression"] = float(sizes[-1])
00663 return r
00664
00665 if i < (count - 3) and lines[i].startswith("*Tree"):
00666 result = parseblock(lines[i:i+3])
00667 result["Branches"] = {}
00668 i += 4
00669 while i < (count - 3) and lines[i].startswith("*Br"):
00670 if i < (count - 2) and lines[i].startswith("*Branch "):
00671
00672 i += 3
00673 continue
00674 branch = parseblock(lines[i:i+3])
00675 result["Branches"][branch["Name"]] = branch
00676 i += 4
00677
00678 return (result, i)
00679
00680 def findTTreeSummaries(stdout):
00681 """
00682 Scan stdout to find ROOT TTree summaries and digest them.
00683 """
00684 stars = re.compile(r"^\*+$")
00685 outlines = stdout.splitlines()
00686 nlines = len(outlines)
00687 trees = {}
00688
00689 i = 0
00690 while i < nlines:
00691
00692 while i < nlines and not stars.match(outlines[i]):
00693 i += 1
00694 if i < nlines:
00695 tree, i = _parseTTreeSummary(outlines, i)
00696 if tree:
00697 trees[tree["Name"]] = tree
00698
00699 return trees
00700
00701 def cmpTreesDicts(reference, to_check, ignore = None):
00702 """
00703 Check that all the keys in reference are in to_check too, with the same value.
00704 If the value is a dict, the function is called recursively. to_check can
00705 contain more keys than reference, that will not be tested.
00706 The function returns at the first difference found.
00707 """
00708 fail_keys = []
00709
00710 if ignore:
00711 ignore_re = re.compile(ignore)
00712 keys = [ key for key in reference if not ignore_re.match(key) ]
00713 else:
00714 keys = reference.keys()
00715
00716 for k in keys:
00717 if k in to_check:
00718 if (type(reference[k]) is dict) and (type(to_check[k]) is dict):
00719
00720 failed = fail_keys = cmpTreesDicts(reference[k], to_check[k], ignore)
00721 else:
00722
00723 failed = to_check[k] != reference[k]
00724 else:
00725 to_check[k] = None
00726 failed = True
00727 if failed:
00728 fail_keys.insert(0, k)
00729 break
00730 return fail_keys
00731
00732 def getCmpFailingValues(reference, to_check, fail_path):
00733 c = to_check
00734 r = reference
00735 for k in fail_path:
00736 c = c.get(k,None)
00737 r = r.get(k,None)
00738 if c is None or r is None:
00739 break
00740 return (fail_path, r, c)
00741
00742
00743 h_count_re = re.compile(r"^(.*)SUCCESS\s+Booked (\d+) Histogram\(s\) :\s+(.*)")
00744
00745 def parseHistosSummary(lines, pos):
00746 """
00747 Extract the histograms infos from the lines starting at pos.
00748 Returns the position of the first line after the summary block.
00749 """
00750 global h_count_re
00751 h_table_head = re.compile(r'SUCCESS\s+List of booked (1D|2D|3D|1D profile|2D profile) histograms in directory\s+"(\w*)"')
00752 h_short_summ = re.compile(r"ID=([^\"]+)\s+\"([^\"]+)\"\s+(.*)")
00753
00754 nlines = len(lines)
00755
00756
00757 m = h_count_re.search(lines[pos])
00758 name = m.group(1).strip()
00759 total = int(m.group(2))
00760 header = {}
00761 for k, v in [ x.split("=") for x in m.group(3).split() ]:
00762 header[k] = int(v)
00763 pos += 1
00764 header["Total"] = total
00765
00766 summ = {}
00767 while pos < nlines:
00768 m = h_table_head.search(lines[pos])
00769 if m:
00770 t, d = m.groups(1)
00771 t = t.replace(" profile", "Prof")
00772 pos += 1
00773 if pos < nlines:
00774 l = lines[pos]
00775 else:
00776 l = ""
00777 cont = {}
00778 if l.startswith(" | ID"):
00779
00780 titles = [ x.strip() for x in l.split("|")][1:]
00781 pos += 1
00782 while pos < nlines and lines[pos].startswith(" |"):
00783 l = lines[pos]
00784 values = [ x.strip() for x in l.split("|")][1:]
00785 hcont = {}
00786 for i in range(len(titles)):
00787 hcont[titles[i]] = values[i]
00788 cont[hcont["ID"]] = hcont
00789 pos += 1
00790 elif l.startswith(" ID="):
00791 while pos < nlines and lines[pos].startswith(" ID="):
00792 values = [ x.strip() for x in h_short_summ.search(lines[pos]).groups() ]
00793 cont[values[0]] = values
00794 pos += 1
00795 else:
00796 raise RuntimeError("Cannot understand line %d: '%s'" % (pos, l))
00797 if not d in summ:
00798 summ[d] = {}
00799 summ[d][t] = cont
00800 summ[d]["header"] = header
00801 else:
00802 break
00803 if not summ:
00804
00805 summ[name] = {"header": header}
00806 return summ, pos
00807
00808 def findHistosSummaries(stdout):
00809 """
00810 Scan stdout to find ROOT TTree summaries and digest them.
00811 """
00812 outlines = stdout.splitlines()
00813 nlines = len(outlines) - 1
00814 summaries = {}
00815 global h_count_re
00816
00817 pos = 0
00818 while pos < nlines:
00819 summ = {}
00820
00821 match = h_count_re.search(outlines[pos])
00822 while pos < nlines and not match:
00823 pos += 1
00824 match = h_count_re.search(outlines[pos])
00825 if match:
00826 summ, pos = parseHistosSummary(outlines, pos)
00827 summaries.update(summ)
00828 return summaries
00829
00830 class GaudiFilterExecutable(qm.executable.Filter):
00831 def __init__(self, input, timeout = -1):
00832 """Create a new 'Filter'.
00833
00834 'input' -- The string containing the input to provide to the
00835 child process.
00836
00837 'timeout' -- As for 'TimeoutExecutable.__init__'."""
00838
00839 super(GaudiFilterExecutable, self).__init__(input, timeout)
00840 self.__input = input
00841 self.__timeout = timeout
00842 self.stack_trace_file = None
00843
00844
00845
00846 tmpf = tempfile.mkstemp()
00847 os.close(tmpf[0])
00848 self.stack_trace_file = tmpf[1]
00849
00850 def __UseSeparateProcessGroupForChild(self):
00851 """Copied from TimeoutExecutable to allow the re-implementation of
00852 _HandleChild.
00853 """
00854 if sys.platform == "win32":
00855
00856
00857
00858
00859 return 0
00860
00861 return self.__timeout >= 0 or self.__timeout == -2
00862
00863
00864 def _HandleChild(self):
00865 """Code copied from both FilterExecutable and TimeoutExecutable.
00866 """
00867
00868 if self._stdin_pipe:
00869 self._ClosePipeEnd(self._stdin_pipe[0])
00870 if self._stdout_pipe:
00871 self._ClosePipeEnd(self._stdout_pipe[1])
00872 if self._stderr_pipe:
00873 self._ClosePipeEnd(self._stderr_pipe[1])
00874
00875
00876
00877
00878
00879
00880
00881 super(qm.executable.TimeoutExecutable, self)._HandleChild()
00882
00883 if self.__UseSeparateProcessGroupForChild():
00884
00885
00886
00887
00888 child_pid = self._GetChildPID()
00889 try:
00890 os.setpgid(child_pid, child_pid)
00891 except:
00892
00893
00894
00895
00896 pass
00897
00898
00899
00900
00901
00902
00903
00904
00905
00906 self.__monitor_pid = os.fork()
00907 if self.__monitor_pid != 0:
00908
00909
00910
00911
00912 os.setpgid(self.__monitor_pid, child_pid)
00913 else:
00914
00915
00916
00917
00918 os.setpgid(0, child_pid)
00919
00920
00921
00922
00923
00924
00925
00926 try:
00927 max_fds = os.sysconf("SC_OPEN_MAX")
00928 except:
00929 max_fds = 256
00930 for fd in xrange(max_fds):
00931 try:
00932 os.close(fd)
00933 except:
00934 pass
00935 try:
00936 if self.__timeout >= 0:
00937
00938 time.sleep (self.__timeout)
00939
00940
00941 if sys.platform == "linux2":
00942 cmd = ["gdb",
00943 os.path.join("/proc", str(child_pid), "exe"),
00944 str(child_pid),
00945 "-batch", "-n", "-x",
00946 "'%s'" % os.path.join(os.path.dirname(__file__), "stack-trace.gdb")]
00947
00948
00949 o = os.popen(" ".join(cmd)).read()
00950 open(self.stack_trace_file,"w").write(o)
00951
00952
00953
00954 os.kill(0, signal.SIGKILL)
00955 else:
00956
00957 select.select ([], [], [])
00958 finally:
00959
00960
00961 os._exit(0)
00962 elif self.__timeout >= 0 and sys.platform == "win32":
00963
00964 self.__monitor_thread = Thread(target = self.__Monitor)
00965 self.__monitor_thread.start()
00966
00967 if sys.platform == "win32":
00968
00969 def __Monitor(self):
00970 """Code copied from FilterExecutable.
00971 Kill the child if the timeout expires.
00972
00973 This function is run in the monitoring thread."""
00974
00975
00976
00977
00978 timeout = int(self.__timeout * 1000)
00979
00980
00981 result = win32event.WaitForSingleObject(self._GetChildPID(),
00982 timeout)
00983
00984 if result == win32con.WAIT_TIMEOUT:
00985 self.Kill()
00986
00987
00988
00989
00990 class GaudiExeTest(ExecTestBase):
00991 """Standard Gaudi test.
00992 """
00993 arguments = [
00994 qm.fields.TextField(
00995 name="program",
00996 title="Program",
00997 not_empty_text=1,
00998 description="""The path to the program.
00999
01000 This field indicates the path to the program. If it is not
01001 an absolute path, the value of the 'PATH' environment
01002 variable will be used to search for the program.
01003 If not specified, $GAUDIEXE or Gaudi.exe are used.
01004 """
01005 ),
01006 qm.fields.SetField(qm.fields.TextField(
01007 name="args",
01008 title="Argument List",
01009 description="""The command-line arguments.
01010
01011 If this field is left blank, the program is run without any
01012 arguments.
01013
01014 Use this field to specify the option files.
01015
01016 An implicit 0th argument (the path to the program) is added
01017 automatically."""
01018 )),
01019 qm.fields.TextField(
01020 name="options",
01021 title="Options",
01022 description="""Options to be passed to the application.
01023
01024 This field allows to pass a list of options to the main program
01025 without the need of a separate option file.
01026
01027 The content of the field is written to a temporary file which name
01028 is passed the the application as last argument (appended to the
01029 field "Argument List".
01030 """,
01031 verbatim="true",
01032 multiline="true",
01033 default_value=""
01034 ),
01035 qm.fields.TextField(
01036 name="workdir",
01037 title="Working Directory",
01038 description="""Path to the working directory.
01039
01040 If this field is left blank, the program will be run from the qmtest
01041 directory, otherwise from the directory specified.""",
01042 default_value=""
01043 ),
01044 qm.fields.TextField(
01045 name="reference",
01046 title="Reference Output",
01047 description="""Path to the file containing the reference output.
01048
01049 If this field is left blank, any standard output will be considered
01050 valid.
01051
01052 If the reference file is specified, any output on standard error is
01053 ignored."""
01054 ),
01055 qm.fields.TextField(
01056 name="error_reference",
01057 title="Reference for standard error",
01058 description="""Path to the file containing the reference for the standard error.
01059
01060 If this field is left blank, any standard output will be considered
01061 valid.
01062
01063 If the reference file is specified, any output on standard error is
01064 ignored."""
01065 ),
01066 qm.fields.SetField(qm.fields.TextField(
01067 name = "unsupported_platforms",
01068 title = "Unsupported Platforms",
01069 description = """Platform on which the test must not be run.
01070
01071 List of regular expressions identifying the platforms on which the
01072 test is not run and the result is set to UNTESTED."""
01073 )),
01074
01075 qm.fields.TextField(
01076 name = "validator",
01077 title = "Validator",
01078 description = """Function to validate the output of the test.
01079
01080 If defined, the function is used to validate the products of the
01081 test.
01082 The function is called passing as arguments:
01083 self: the test class instance
01084 stdout: the standard output of the executed test
01085 stderr: the standard error of the executed test
01086 result: the Result objects to fill with messages
01087 The function must return a list of causes for the failure.
01088 If specified, overrides standard output, standard error and
01089 reference files.
01090 """,
01091 verbatim="true",
01092 multiline="true",
01093 default_value=""
01094 ),
01095
01096 qm.fields.BooleanField(
01097 name = "use_temp_dir",
01098 title = "Use temporary directory",
01099 description = """Use temporary directory.
01100
01101 If set to true, use a temporary directory as working directory.
01102 """,
01103 default_value="false"
01104 ),
01105 ]
01106
01107 def PlatformIsNotSupported(self, context, result):
01108 platform = self.GetPlatform()
01109 unsupported = [ re.compile(x)
01110 for x in [ str(y).strip()
01111 for y in self.unsupported_platforms ]
01112 if x
01113 ]
01114 for p_re in unsupported:
01115 if p_re.search(platform):
01116 result.SetOutcome(result.UNTESTED)
01117 result[result.CAUSE] = 'Platform not supported.'
01118 return True
01119 return False
01120
01121 def GetPlatform(self):
01122 """
01123 Return the platform Id defined in CMTCONFIG or SCRAM_ARCH.
01124 """
01125 arch = "None"
01126
01127 if "CMTCONFIG" in os.environ:
01128 arch = os.environ["CMTCONFIG"]
01129 elif "SCRAM_ARCH" in os.environ:
01130 arch = os.environ["SCRAM_ARCH"]
01131 return arch
01132
01133 def isWinPlatform(self):
01134 """
01135 Return True if the current platform is Windows.
01136
01137 This function was needed because of the change in the CMTCONFIG format,
01138 from win32_vc71_dbg to i686-winxp-vc9-dbg.
01139 """
01140 platform = self.GetPlatform()
01141 return "winxp" in platform or platform.startswith("win")
01142
01143 def _expandReferenceFileName(self, reffile):
01144
01145 if not reffile:
01146 return ""
01147
01148
01149 platformSplit = lambda p: set(p.split('-' in p and '-' or '_'))
01150
01151 reference = os.path.normpath(os.path.expandvars(reffile))
01152
01153 spec_ref = reference[:-3] + self.GetPlatform()[0:3] + reference[-3:]
01154 if os.path.isfile(spec_ref):
01155 reference = spec_ref
01156 else:
01157
01158 dirname, basename = os.path.split(reference)
01159 if not dirname: dirname = '.'
01160 head = basename + "."
01161 head_len = len(head)
01162 platform = platformSplit(self.GetPlatform())
01163 candidates = []
01164 for f in os.listdir(dirname):
01165 if f.startswith(head):
01166 req_plat = platformSplit(f[head_len:])
01167 if platform.issuperset(req_plat):
01168 candidates.append( (len(req_plat), f) )
01169 if candidates:
01170
01171
01172 candidates.sort()
01173 reference = os.path.join(dirname, candidates[-1][1])
01174 return reference
01175
01176 def CheckTTreesSummaries(self, stdout, result, causes,
01177 trees_dict = None,
01178 ignore = r"Basket|.*size|Compression"):
01179 """
01180 Compare the TTree summaries in stdout with the ones in trees_dict or in
01181 the reference file. By default ignore the size, compression and basket
01182 fields.
01183 The presence of TTree summaries when none is expected is not a failure.
01184 """
01185 if trees_dict is None:
01186 reference = self._expandReferenceFileName(self.reference)
01187
01188 if reference and os.path.isfile(reference):
01189 trees_dict = findTTreeSummaries(open(reference).read())
01190 else:
01191 trees_dict = {}
01192
01193 from pprint import PrettyPrinter
01194 pp = PrettyPrinter()
01195 if trees_dict:
01196 result["GaudiTest.TTrees.expected"] = result.Quote(pp.pformat(trees_dict))
01197 if ignore:
01198 result["GaudiTest.TTrees.ignore"] = result.Quote(ignore)
01199
01200 trees = findTTreeSummaries(stdout)
01201 failed = cmpTreesDicts(trees_dict, trees, ignore)
01202 if failed:
01203 causes.append("trees summaries")
01204 msg = "%s: %s != %s" % getCmpFailingValues(trees_dict, trees, failed)
01205 result["GaudiTest.TTrees.failure_on"] = result.Quote(msg)
01206 result["GaudiTest.TTrees.found"] = result.Quote(pp.pformat(trees))
01207
01208 return causes
01209
01210 def CheckHistosSummaries(self, stdout, result, causes,
01211 dict = None,
01212 ignore = None):
01213 """
01214 Compare the TTree summaries in stdout with the ones in trees_dict or in
01215 the reference file. By default ignore the size, compression and basket
01216 fields.
01217 The presence of TTree summaries when none is expected is not a failure.
01218 """
01219 if dict is None:
01220 reference = self._expandReferenceFileName(self.reference)
01221
01222 if reference and os.path.isfile(reference):
01223 dict = findHistosSummaries(open(reference).read())
01224 else:
01225 dict = {}
01226
01227 from pprint import PrettyPrinter
01228 pp = PrettyPrinter()
01229 if dict:
01230 result["GaudiTest.Histos.expected"] = result.Quote(pp.pformat(dict))
01231 if ignore:
01232 result["GaudiTest.Histos.ignore"] = result.Quote(ignore)
01233
01234 histos = findHistosSummaries(stdout)
01235 failed = cmpTreesDicts(dict, histos, ignore)
01236 if failed:
01237 causes.append("histos summaries")
01238 msg = "%s: %s != %s" % getCmpFailingValues(dict, histos, failed)
01239 result["GaudiTest.Histos.failure_on"] = result.Quote(msg)
01240 result["GaudiTest.Histos.found"] = result.Quote(pp.pformat(histos))
01241
01242 return causes
01243
01244 def ValidateWithReference(self, stdout, stderr, result, causes, preproc = None):
01245 """
01246 Default validation action: compare standard output and error to the
01247 reference files.
01248 """
01249
01250 if preproc is None:
01251 preproc = normalizeExamples
01252
01253 reference = self._expandReferenceFileName(self.reference)
01254
01255 if reference and os.path.isfile(reference):
01256 result["GaudiTest.output_reference"] = reference
01257 causes += ReferenceFileValidator(reference,
01258 "standard output",
01259 "GaudiTest.output_diff",
01260 preproc = preproc)(stdout, result)
01261
01262
01263 causes = self.CheckTTreesSummaries(stdout, result, causes)
01264 causes = self.CheckHistosSummaries(stdout, result, causes)
01265
01266 if causes:
01267 try:
01268 newref = open(reference + ".new","w")
01269
01270 for l in stdout.splitlines():
01271 newref.write(l.rstrip() + '\n')
01272 del newref
01273 except IOError:
01274
01275
01276 pass
01277
01278
01279 reference = self._expandReferenceFileName(self.error_reference)
01280
01281 if reference and os.path.isfile(reference):
01282 result["GaudiTest.error_reference"] = reference
01283 newcauses = ReferenceFileValidator(reference,
01284 "standard error",
01285 "GaudiTest.error_diff",
01286 preproc = preproc)(stderr, result)
01287 causes += newcauses
01288 if newcauses:
01289 newref = open(reference + ".new","w")
01290
01291 for l in stderr.splitlines():
01292 newref.write(l.rstrip() + '\n')
01293 del newref
01294 else:
01295 causes += BasicOutputValidator(self.stderr,
01296 "standard error",
01297 "ExecTest.expected_stderr")(stderr, result)
01298
01299 return causes
01300
01301 def ValidateOutput(self, stdout, stderr, result):
01302 causes = []
01303
01304 if self.validator.strip() != "":
01305 class CallWrapper(object):
01306 """
01307 Small wrapper class to dynamically bind some default arguments
01308 to a callable.
01309 """
01310 def __init__(self, callable, extra_args = {}):
01311 self.callable = callable
01312 self.extra_args = extra_args
01313
01314 from inspect import getargspec
01315 self.args_order = getargspec(callable)[0]
01316
01317
01318 if self.args_order[0] == "self":
01319 del self.args_order[0]
01320 def __call__(self, *args, **kwargs):
01321
01322 positional = self.args_order[:len(args)]
01323
01324 kwargs = dict(kwargs)
01325 for a in self.extra_args:
01326
01327
01328 if a not in positional and a not in kwargs:
01329 kwargs[a] = self.extra_args[a]
01330 return apply(self.callable, args, kwargs)
01331
01332 exported_symbols = {"self":self,
01333 "stdout":stdout,
01334 "stderr":stderr,
01335 "result":result,
01336 "causes":causes,
01337 "findReferenceBlock":
01338 CallWrapper(findReferenceBlock, {"stdout":stdout,
01339 "result":result,
01340 "causes":causes}),
01341 "validateWithReference":
01342 CallWrapper(self.ValidateWithReference, {"stdout":stdout,
01343 "stderr":stderr,
01344 "result":result,
01345 "causes":causes}),
01346 "countErrorLines":
01347 CallWrapper(countErrorLines, {"stdout":stdout,
01348 "result":result,
01349 "causes":causes}),
01350 "checkTTreesSummaries":
01351 CallWrapper(self.CheckTTreesSummaries, {"stdout":stdout,
01352 "result":result,
01353 "causes":causes}),
01354 "checkHistosSummaries":
01355 CallWrapper(self.CheckHistosSummaries, {"stdout":stdout,
01356 "result":result,
01357 "causes":causes}),
01358
01359 }
01360 exec self.validator in globals(), exported_symbols
01361 else:
01362 self.ValidateWithReference(stdout, stderr, result, causes)
01363
01364 return causes
01365
01366 def DumpEnvironment(self, result):
01367 """
01368 Add the content of the environment to the result object.
01369
01370 Copied from the QMTest class of COOL.
01371 """
01372 vars = os.environ.keys()
01373 vars.sort()
01374 result['GaudiTest.environment'] = \
01375 result.Quote('\n'.join(["%s=%s"%(v,os.environ[v]) for v in vars]))
01376
01377 def Run(self, context, result):
01378 """Run the test.
01379
01380 'context' -- A 'Context' giving run-time parameters to the
01381 test.
01382
01383 'result' -- A 'Result' object. The outcome will be
01384 'Result.PASS' when this method is called. The 'result' may be
01385 modified by this method to indicate outcomes other than
01386 'Result.PASS' or to add annotations."""
01387
01388
01389 if self.PlatformIsNotSupported(context, result):
01390 return
01391
01392
01393 if self.program:
01394 prog = rationalizepath(self.program)
01395 elif "GAUDIEXE" in os.environ:
01396 prog = os.environ["GAUDIEXE"]
01397 else:
01398 prog = "Gaudi.exe"
01399 self.program = prog
01400
01401 dummy, prog_ext = os.path.splitext(prog)
01402 if prog_ext not in [ ".exe", ".py", ".bat" ] and self.isWinPlatform():
01403 prog += ".exe"
01404 prog_ext = ".exe"
01405
01406 prog = which(prog) or prog
01407
01408
01409 args = map(rationalizepath, self.args)
01410 self.reference = rationalizepath(self.reference)
01411 self.error_reference = rationalizepath(self.error_reference)
01412
01413
01414
01415 tmpfile = None
01416 if self.options.strip():
01417 ext = ".opts"
01418 if re.search(r"from\s*Gaudi.Configuration\s*import\s*\*", self.options):
01419 ext = ".py"
01420 tmpfile = TempFile(ext)
01421 tmpfile.writelines("\n".join(self.options.splitlines()))
01422 tmpfile.flush()
01423 args.append(tmpfile.name)
01424 result["GaudiTest.options"] = result.Quote(self.options)
01425
01426
01427 if prog_ext == ".py":
01428 args.insert(0,prog)
01429 if self.isWinPlatform():
01430 prog = which("python.exe") or "python.exe"
01431 else:
01432 prog = which("python") or "python"
01433
01434
01435 origdir = os.getcwd()
01436 if self.workdir:
01437 os.chdir(str(os.path.normpath(os.path.expandvars(self.workdir))))
01438 elif self.use_temp_dir == "true":
01439 if "QMTEST_TMPDIR" in os.environ:
01440 qmtest_tmpdir = os.environ["QMTEST_TMPDIR"]
01441 if not os.path.exists(qmtest_tmpdir):
01442 os.makedirs(qmtest_tmpdir)
01443 os.chdir(qmtest_tmpdir)
01444 elif "qmtest.tmpdir" in context:
01445 os.chdir(context["qmtest.tmpdir"])
01446
01447 if "QMTEST_IGNORE_TIMEOUT" not in os.environ:
01448 self.timeout = max(self.timeout,600)
01449 else:
01450 self.timeout = -1
01451
01452 try:
01453
01454 self._CreateEclipseLaunch(prog, args, destdir = os.path.join(origdir, '.eclipse'))
01455
01456 self.RunProgram(prog,
01457 [ prog ] + args,
01458 context, result)
01459
01460 if result.GetOutcome() not in [ result.PASS ]:
01461 self.DumpEnvironment(result)
01462 finally:
01463
01464 os.chdir(origdir)
01465
01466 def RunProgram(self, program, arguments, context, result):
01467 """Run the 'program'.
01468
01469 'program' -- The path to the program to run.
01470
01471 'arguments' -- A list of the arguments to the program. This
01472 list must contain a first argument corresponding to 'argv[0]'.
01473
01474 'context' -- A 'Context' giving run-time parameters to the
01475 test.
01476
01477 'result' -- A 'Result' object. The outcome will be
01478 'Result.PASS' when this method is called. The 'result' may be
01479 modified by this method to indicate outcomes other than
01480 'Result.PASS' or to add annotations.
01481
01482 @attention: This method has been copied from command.ExecTestBase
01483 (QMTest 2.3.0) and modified to keep stdout and stderr
01484 for tests that have been terminated by a signal.
01485 (Fundamental for debugging in the Application Area)
01486 """
01487
01488
01489 environment = self.MakeEnvironment(context)
01490
01491 if "slc6" in environment.get('CMTCONFIG', ''):
01492 environment['TERM'] = 'dumb'
01493
01494 if self.timeout >= 0:
01495 timeout = self.timeout
01496 else:
01497
01498
01499
01500
01501
01502 timeout = -2
01503 e = GaudiFilterExecutable(self.stdin, timeout)
01504
01505 exit_status = e.Run(arguments, environment, path = program)
01506
01507 if e.stack_trace_file and os.path.exists(e.stack_trace_file):
01508 stack_trace = open(e.stack_trace_file).read()
01509 os.remove(e.stack_trace_file)
01510 else:
01511 stack_trace = None
01512 if stack_trace:
01513 result["ExecTest.stack_trace"] = result.Quote(stack_trace)
01514
01515
01516 if sys.platform == "win32" or os.WIFEXITED(exit_status):
01517
01518 causes = []
01519
01520
01521 if self.exit_code is None:
01522 exit_code = None
01523 elif sys.platform == "win32":
01524 exit_code = exit_status
01525 else:
01526 exit_code = os.WEXITSTATUS(exit_status)
01527
01528 stdout = e.stdout
01529 stderr = e.stderr
01530
01531 result["ExecTest.exit_code"] = str(exit_code)
01532 result["ExecTest.stdout"] = result.Quote(stdout)
01533 result["ExecTest.stderr"] = result.Quote(stderr)
01534
01535 if exit_code != self.exit_code:
01536 causes.append("exit_code")
01537 result["ExecTest.expected_exit_code"] \
01538 = str(self.exit_code)
01539
01540 causes += self.ValidateOutput(stdout, stderr, result)
01541
01542 if causes:
01543 result.Fail("Unexpected %s." % string.join(causes, ", "))
01544 elif os.WIFSIGNALED(exit_status):
01545
01546
01547 signal_number = str(os.WTERMSIG(exit_status))
01548 if not stack_trace:
01549 result.Fail("Program terminated by signal.")
01550 else:
01551
01552
01553 result.Fail("Exceeded time limit (%ds), terminated." % timeout)
01554 result["ExecTest.signal_number"] = signal_number
01555 result["ExecTest.stdout"] = result.Quote(e.stdout)
01556 result["ExecTest.stderr"] = result.Quote(e.stderr)
01557 elif os.WIFSTOPPED(exit_status):
01558
01559
01560 signal_number = str(os.WSTOPSIG(exit_status))
01561 if not stack_trace:
01562 result.Fail("Program stopped by signal.")
01563 else:
01564
01565
01566 result.Fail("Exceeded time limit (%ds), stopped." % timeout)
01567 result["ExecTest.signal_number"] = signal_number
01568 result["ExecTest.stdout"] = result.Quote(e.stdout)
01569 result["ExecTest.stderr"] = result.Quote(e.stderr)
01570 else:
01571
01572
01573 result.Fail("Program did not terminate normally.")
01574
01575
01576
01577 esc = '\x1b'
01578 repr_esc = '\\x1b'
01579 result["ExecTest.stdout"] = result["ExecTest.stdout"].replace(esc,repr_esc)
01580
01581
01582
01583 def _CreateEclipseLaunch(self, prog, args, destdir = None):
01584
01585
01586 projbasedir = os.path.normpath(destdir)
01587 while not os.path.exists(os.path.join(projbasedir, ".project")):
01588 oldprojdir = projbasedir
01589 projbasedir = os.path.normpath(os.path.join(projbasedir, os.pardir))
01590
01591
01592 if oldprojdir == projbasedir:
01593
01594 return
01595
01596 if not os.path.exists(destdir):
01597 os.makedirs(destdir)
01598
01599 from xml.etree import ElementTree as ET
01600 t = ET.parse(os.path.join(projbasedir, ".project"))
01601 projectName = t.find("name").text
01602
01603
01604 destfile = "%s.launch" % self._Runnable__id
01605 if destdir:
01606 destfile = os.path.join(destdir, destfile)
01607
01608 if self.options.strip():
01609
01610
01611
01612 tempfile = args.pop()
01613 optsfile = destfile + os.path.splitext(tempfile)[1]
01614 shutil.copyfile(tempfile, optsfile)
01615 args.append(optsfile)
01616
01617
01618 from xml.sax.saxutils import quoteattr
01619 data = {}
01620
01621
01622 data["environment"] = "\n".join(['<mapEntry key=%s value=%s/>' % (quoteattr(k), quoteattr(v))
01623 for k, v in os.environ.iteritems()])
01624
01625 data["exec"] = which(prog) or prog
01626 if os.path.basename(data["exec"]).lower().startswith("python"):
01627 data["stopAtMain"] = "false"
01628 else:
01629 data["stopAtMain"] = "true"
01630
01631 data["args"] = " ".join(map(rationalizepath, args))
01632 if self.isWinPlatform():
01633 data["args"] = " ".join(["/debugexe"] + map(rationalizepath, [data["exec"]] + args))
01634 data["exec"] = which("vcexpress.exe")
01635
01636 if not self.use_temp_dir:
01637 data["workdir"] = os.getcwd()
01638 else:
01639
01640
01641 data["workdir"] = destdir
01642
01643 data["project"] = projectName.strip()
01644
01645
01646 xml = """<?xml version="1.0" encoding="UTF-8" standalone="no"?>
01647 <launchConfiguration type="org.eclipse.cdt.launch.applicationLaunchType">
01648 <booleanAttribute key="org.eclipse.cdt.debug.mi.core.AUTO_SOLIB" value="true"/>
01649 <listAttribute key="org.eclipse.cdt.debug.mi.core.AUTO_SOLIB_LIST"/>
01650 <stringAttribute key="org.eclipse.cdt.debug.mi.core.DEBUG_NAME" value="gdb"/>
01651 <stringAttribute key="org.eclipse.cdt.debug.mi.core.GDB_INIT" value=".gdbinit"/>
01652 <listAttribute key="org.eclipse.cdt.debug.mi.core.SOLIB_PATH"/>
01653 <booleanAttribute key="org.eclipse.cdt.debug.mi.core.STOP_ON_SOLIB_EVENTS" value="false"/>
01654 <booleanAttribute key="org.eclipse.cdt.debug.mi.core.breakpointsFullPath" value="false"/>
01655 <stringAttribute key="org.eclipse.cdt.debug.mi.core.commandFactory" value="org.eclipse.cdt.debug.mi.core.standardCommandFactory"/>
01656 <stringAttribute key="org.eclipse.cdt.debug.mi.core.protocol" value="mi"/>
01657 <booleanAttribute key="org.eclipse.cdt.debug.mi.core.verboseMode" value="false"/>
01658 <intAttribute key="org.eclipse.cdt.launch.ATTR_BUILD_BEFORE_LAUNCH_ATTR" value="0"/>
01659 <stringAttribute key="org.eclipse.cdt.launch.COREFILE_PATH" value=""/>
01660 <stringAttribute key="org.eclipse.cdt.launch.DEBUGGER_ID" value="org.eclipse.cdt.debug.mi.core.CDebuggerNew"/>
01661 <stringAttribute key="org.eclipse.cdt.launch.DEBUGGER_REGISTER_GROUPS" value=""/>
01662 <stringAttribute key="org.eclipse.cdt.launch.DEBUGGER_START_MODE" value="run"/>
01663 <booleanAttribute key="org.eclipse.cdt.launch.DEBUGGER_STOP_AT_MAIN" value="%(stopAtMain)s"/>
01664 <stringAttribute key="org.eclipse.cdt.launch.DEBUGGER_STOP_AT_MAIN_SYMBOL" value="main"/>
01665 <booleanAttribute key="org.eclipse.cdt.launch.ENABLE_REGISTER_BOOKKEEPING" value="false"/>
01666 <booleanAttribute key="org.eclipse.cdt.launch.ENABLE_VARIABLE_BOOKKEEPING" value="false"/>
01667 <stringAttribute key="org.eclipse.cdt.launch.FORMAT" value="<?xml version="1.0" encoding="UTF-8" standalone="no"?><contentList/>"/>
01668 <stringAttribute key="org.eclipse.cdt.launch.GLOBAL_VARIABLES" value="<?xml version="1.0" encoding="UTF-8" standalone="no"?> <globalVariableList/> "/>
01669 <stringAttribute key="org.eclipse.cdt.launch.MEMORY_BLOCKS" value="<?xml version="1.0" encoding="UTF-8" standalone="no"?> <memoryBlockExpressionList/> "/>
01670 <stringAttribute key="org.eclipse.cdt.launch.PROGRAM_ARGUMENTS" value="%(args)s"/>
01671 <stringAttribute key="org.eclipse.cdt.launch.PROGRAM_NAME" value="%(exec)s"/>
01672 <stringAttribute key="org.eclipse.cdt.launch.PROJECT_ATTR" value="%(project)s"/>
01673 <stringAttribute key="org.eclipse.cdt.launch.PROJECT_BUILD_CONFIG_ID_ATTR" value=""/>
01674 <stringAttribute key="org.eclipse.cdt.launch.WORKING_DIRECTORY" value="%(workdir)s"/>
01675 <booleanAttribute key="org.eclipse.cdt.launch.ui.ApplicationCDebuggerTab.DEFAULTS_SET" value="true"/>
01676 <booleanAttribute key="org.eclipse.cdt.launch.use_terminal" value="true"/>
01677 <listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_PATHS">
01678 <listEntry value="/%(project)s"/>
01679 </listAttribute>
01680 <listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_TYPES">
01681 <listEntry value="4"/>
01682 </listAttribute>
01683 <booleanAttribute key="org.eclipse.debug.core.appendEnvironmentVariables" value="false"/>
01684 <mapAttribute key="org.eclipse.debug.core.environmentVariables">
01685 %(environment)s
01686 </mapAttribute>
01687 <mapAttribute key="org.eclipse.debug.core.preferred_launchers">
01688 <mapEntry key="[debug]" value="org.eclipse.cdt.cdi.launch.localCLaunch"/>
01689 </mapAttribute>
01690 <listAttribute key="org.eclipse.debug.ui.favoriteGroups">
01691 <listEntry value="org.eclipse.debug.ui.launchGroup.debug"/>
01692 </listAttribute>
01693 </launchConfiguration>
01694 """ % data
01695
01696
01697 open(destfile, "w").write(xml)
01698
01699
01700
01701 try:
01702 import json
01703 except ImportError:
01704
01705 import simplejson as json
01706
01707 class HTMLResultStream(ResultStream):
01708 """An 'HTMLResultStream' writes its output to a set of HTML files.
01709
01710 The argument 'dir' is used to select the destination directory for the HTML
01711 report.
01712 The destination directory may already contain the report from a previous run
01713 (for example of a different package), in which case it will be extended to
01714 include the new data.
01715 """
01716 arguments = [
01717 qm.fields.TextField(
01718 name = "dir",
01719 title = "Destination Directory",
01720 description = """The name of the directory.
01721
01722 All results will be written to the directory indicated.""",
01723 verbatim = "true",
01724 default_value = ""),
01725 ]
01726
01727 def __init__(self, arguments = None, **args):
01728 """Prepare the destination directory.
01729
01730 Creates the destination directory and store in it some preliminary
01731 annotations and the static files found in the template directory
01732 'html_report'.
01733 """
01734 ResultStream.__init__(self, arguments, **args)
01735 self._summary = []
01736 self._summaryFile = os.path.join(self.dir, "summary.json")
01737 self._annotationsFile = os.path.join(self.dir, "annotations.json")
01738
01739 templateDir = os.path.join(os.path.dirname(__file__), "html_report")
01740 if not os.path.isdir(self.dir):
01741 os.makedirs(self.dir)
01742
01743 for f in os.listdir(templateDir):
01744 src = os.path.join(templateDir, f)
01745 dst = os.path.join(self.dir, f)
01746 if not os.path.isdir(src) and not os.path.exists(dst):
01747 shutil.copy(src, dst)
01748
01749 if "CMTCONFIG" in os.environ:
01750 self.WriteAnnotation("cmt.cmtconfig", os.environ["CMTCONFIG"])
01751 import socket
01752 self.WriteAnnotation("hostname", socket.gethostname())
01753
01754 def _updateSummary(self):
01755 """Helper function to extend the global summary file in the destination
01756 directory.
01757 """
01758 if os.path.exists(self._summaryFile):
01759 oldSummary = json.load(open(self._summaryFile))
01760 else:
01761 oldSummary = []
01762 ids = set([ i["id"] for i in self._summary ])
01763 newSummary = [ i for i in oldSummary if i["id"] not in ids ]
01764 newSummary.extend(self._summary)
01765 json.dump(newSummary, open(self._summaryFile, "w"),
01766 sort_keys = True)
01767
01768 def WriteAnnotation(self, key, value):
01769 """Writes the annotation to the annotation file.
01770 If the key is already present with a different value, the value becomes
01771 a list and the new value is appended to it, except for start_time and
01772 end_time.
01773 """
01774
01775 if os.path.exists(self._annotationsFile):
01776 annotations = json.load(open(self._annotationsFile))
01777 else:
01778 annotations = {}
01779
01780 key, value = map(str, [key, value])
01781 if key == "qmtest.run.start_time":
01782
01783
01784
01785
01786 if key not in annotations:
01787 annotations[key] = value
01788 if "qmtest.run.end_time" in annotations:
01789 del annotations["qmtest.run.end_time"]
01790 else:
01791
01792 if key in annotations:
01793 old = annotations[key]
01794 if type(old) is list:
01795 if value not in old:
01796 annotations[key].append(value)
01797 elif value != old:
01798 annotations[key] = [old, value]
01799 else:
01800 annotations[key] = value
01801
01802 json.dump(annotations, open(self._annotationsFile, "w"),
01803 sort_keys = True)
01804
01805 def WriteResult(self, result):
01806 """Prepare the test result directory in the destination directory storing
01807 into it the result fields.
01808 A summary of the test result is stored both in a file in the test directory
01809 and in the global summary file.
01810 """
01811 summary = {}
01812 summary["id"] = result.GetId()
01813 summary["outcome"] = result.GetOutcome()
01814 summary["cause"] = result.GetCause()
01815 summary["fields"] = result.keys()
01816 summary["fields"].sort()
01817
01818
01819 for f in ["id", "outcome", "cause"]:
01820 summary[f] = str(summary[f])
01821 summary["fields"] = map(str, summary["fields"])
01822
01823 self._summary.append(summary)
01824
01825
01826
01827
01828
01829 testOutDir = os.path.join(self.dir, summary["id"])
01830 if not os.path.isdir(testOutDir):
01831 os.makedirs(testOutDir)
01832 json.dump(summary, open(os.path.join(testOutDir, "summary.json"), "w"),
01833 sort_keys = True)
01834 for f in summary["fields"]:
01835 open(os.path.join(testOutDir, f), "w").write(result[f])
01836
01837 self._updateSummary()
01838
01839 def Summarize(self):
01840
01841 pass