5 __author__ =
'Marco Clemencic CERN/PH-LBC'
20 from subprocess
import Popen, PIPE, STDOUT
23 from GaudiKernel
import ROOT6WorkAroundEnabled
30 os.environ[
'LC_ALL'] =
'C'
34 import xml.etree.cElementTree
as ET
36 import xml.etree.ElementTree
as ET
40 return timedelta.days*86400 + timedelta.seconds + timedelta.microseconds/1000000
44 from qm.test.classes.command
import ExecTestBase
45 from qm.test.result_stream
import ResultStream
52 if sys.platform ==
"win32":
55 from threading
import *
71 class TemporaryEnvironment:
73 Class to changes the environment temporarily.
75 def __init__(self, orig = os.environ, keep_same = False):
77 Create a temporary environment on top of the one specified
78 (it can be another TemporaryEnvironment instance).
83 self._keep_same = keep_same
85 def __setitem__(self,key,value):
87 Set an environment variable recording the previous value.
89 if key
not in self.old_values :
91 if not self._keep_same
or self.env[key] != value:
92 self.old_values[key] = self.env[key]
94 self.old_values[key] =
None
97 def __getitem__(self,key):
99 Get an environment variable.
100 Needed to provide the same interface as os.environ.
104 def __delitem__(self,key):
106 Unset an environment variable.
107 Needed to provide the same interface as os.environ.
109 if key
not in self.env :
111 self.old_values[key] = self.env[key]
116 Return the list of defined environment variables.
117 Needed to provide the same interface as os.environ.
119 return self.env.keys()
123 Return the list of (name,value) pairs for the defined environment variables.
124 Needed to provide the same interface as os.environ.
126 return self.env.items()
128 def __contains__(self,key):
131 Needed to provide the same interface as os.environ.
133 return key
in self.env
137 Revert all the changes done to the original environment.
139 for key,value
in self.old_values.items():
143 self.env[key] = value
148 Revert the changes on destruction.
153 def gen_script(self,shell_type):
155 Generate a shell script to reproduce the changes in the environment.
157 shells = [
'csh',
'sh',
'bat' ]
158 if shell_type
not in shells:
159 raise RuntimeError(
"Shell type '%s' unknown. Available: %s"%(shell_type,shells))
161 for key,value
in self.old_values.items():
162 if key
not in self.env:
164 if shell_type ==
'csh':
165 out +=
'unsetenv %s\n'%key
166 elif shell_type ==
'sh':
167 out +=
'unset %s\n'%key
168 elif shell_type ==
'bat':
169 out +=
'set %s=\n'%key
172 if shell_type ==
'csh':
173 out +=
'setenv %s "%s"\n'%(key,self.env[key])
174 elif shell_type ==
'sh':
175 out +=
'export %s="%s"\n'%(key,self.env[key])
176 elif shell_type ==
'bat':
177 out +=
'set %s=%s\n'%(key,self.env[key])
181 """Small class for temporary directories.
182 When instantiated, it creates a temporary directory and the instance
183 behaves as the string containing the directory name.
184 When the instance goes out of scope, it removes all the content of
185 the temporary directory (automatic clean-up).
187 def __init__(self, keep = False, chdir = False):
188 self.name = tempfile.mkdtemp()
192 self._origdir = os.getcwd()
200 os.chdir(self._origdir)
201 if self.name
and not self._keep:
202 shutil.rmtree(self.name)
204 def __getattr__(self,attr):
205 return getattr(self.name,attr)
208 """Small class for temporary files.
209 When instantiated, it creates a temporary directory and the instance
210 behaves as the string containing the directory name.
211 When the instance goes out of scope, it removes all the content of
212 the temporary directory (automatic clean-up).
214 def __init__(self, suffix='', prefix='tmp', dir=None, text=False, keep = False):
219 self._fd, self.name = tempfile.mkstemp(suffix,prefix,dir,text)
220 self.file = os.fdopen(self._fd,
"r+")
228 if self.name
and not self._keep:
231 def __getattr__(self,attr):
232 return getattr(self.file,attr)
235 """Small wrapper to call CMT.
237 def __init__(self,path=None):
242 def _run_cmt(self,command,args):
244 if type(args)
is str:
246 cmd =
"cmt %s"%command
254 result = os.popen4(cmd)[1].read()
259 def __getattr__(self,attr):
260 return lambda args=[]: self._run_cmt(attr, args)
262 def runtime_env(self,env = None):
263 """Returns a dictionary containing the runtime environment produced by CMT.
264 If a dictionary is passed a modified instance of it is returned.
268 for l
in self.setup(
"-csh").splitlines():
270 if l.startswith(
"setenv"):
271 dummy,name,value = l.split(
None,3)
272 env[name] = value.strip(
'"')
273 elif l.startswith(
"unsetenv"):
274 dummy,name = l.split(
None,2)
278 def show_macro(self,k):
279 r = self.show([
"macro",k])
280 if r.find(
"CMT> Error: symbol not found") >= 0:
283 return self.show([
"macro_value",k]).strip()
289 def which(executable):
291 Locates an executable in the executables path ($PATH) and returns the full
292 path to it. An application is looked for with or without the '.exe' suffix.
293 If the executable cannot be found, None is returned
295 if os.path.isabs(executable):
296 if not os.path.exists(executable):
297 if executable.endswith(
'.exe'):
298 if os.path.exists(executable[:-4]):
299 return executable[:-4]
301 for d
in os.environ.get(
"PATH").split(os.pathsep):
302 fullpath = os.path.join(d, executable)
303 if os.path.exists(fullpath):
305 if executable.endswith(
'.exe'):
306 return which(executable[:-4])
310 np = os.path.normpath(os.path.expandvars(p))
311 if os.path.exists(np):
312 p = os.path.realpath(np)
329 _illegal_xml_chars_RE = re.compile(
u'[\x00-\x08\x0b\x0c\x0e-\x1F\uD800-\uDFFF\uFFFE\uFFFF]')
332 "Return the hex string "
333 return "".join(
map(hexConvert,match.group()))
336 return hex(ord(char))
338 return _illegal_xml_chars_RE.sub(hexreplace, val)
341 """Filter out characters that are illegal in XML.
342 Looks for any character in val that is not allowed in XML
343 and replaces it with replacement ('?' by default).
346 return _illegal_xml_chars_RE.sub(replacement, val)
351 class BasicOutputValidator:
352 """Basic implementation of an option validator for Gaudi tests.
353 This implementation is based on the standard (LCG) validation functions
356 def __init__(self,ref,cause,result_key):
359 self.result_key = result_key
361 def __call__(self, out, result):
362 """Validate the output of the program.
364 'stdout' -- A string containing the data written to the standard output
367 'stderr' -- A string containing the data written to the standard error
370 'result' -- A 'Result' object. It may be used to annotate
371 the outcome according to the content of stderr.
373 returns -- A list of strings giving causes of failure."""
377 if not self.__CompareText(out, self.reference):
378 causes.append(self.cause)
379 result[self.result_key] = result.Quote(self.reference)
383 def __CompareText(self, s1, s2):
384 """Compare 's1' and 's2', ignoring line endings.
390 returns -- True if 's1' and 's2' are the same, ignoring
391 differences in line endings."""
397 to_ignore = re.compile(
r'Warning in <TInterpreter::ReadRootmapFile>: .* is already in .*')
398 keep_line =
lambda l:
not to_ignore.match(l)
399 return filter(keep_line, s1.splitlines()) == filter(keep_line, s2.splitlines())
401 return s1.splitlines() == s2.splitlines()
403 class FilePreprocessor:
404 """ Base class for a callable that takes a file and returns a modified
406 def __processLine__(self, line):
408 def __call__(self, input):
409 if hasattr(input,
"__iter__"):
413 lines = input.splitlines()
417 l = self.__processLine__(l)
418 if l: output.append(l)
419 if mergeback: output =
'\n'.join(output)
421 def __add__(self, rhs):
422 return FilePreprocessorSequence([self,rhs])
424 class FilePreprocessorSequence(FilePreprocessor):
425 def __init__(self, members = []):
426 self.members = members
427 def __add__(self, rhs):
428 return FilePreprocessorSequence(self.members + [rhs])
429 def __call__(self, input):
431 for pp
in self.members:
435 class LineSkipper(FilePreprocessor):
436 def __init__(self, strings = [], regexps = []):
438 self.strings = strings
439 self.regexps =
map(re.compile,regexps)
441 def __processLine__(self, line):
442 for s
in self.strings:
443 if line.find(s) >= 0:
return None
444 for r
in self.regexps:
445 if r.search(line):
return None
448 class BlockSkipper(FilePreprocessor):
449 def __init__(self, start, end):
452 self._skipping =
False
454 def __processLine__(self, line):
455 if self.start
in line:
456 self._skipping =
True
458 elif self.end
in line:
459 self._skipping =
False
464 class RegexpReplacer(FilePreprocessor):
465 def __init__(self, orig, repl = "", when = None):
467 when = re.compile(when)
468 self._operations = [ (when, re.compile(orig), repl) ]
469 def __add__(self,rhs):
470 if isinstance(rhs, RegexpReplacer):
471 res = RegexpReplacer(
"",
"",
None)
472 res._operations = self._operations + rhs._operations
474 res = FilePreprocessor.__add__(self, rhs)
476 def __processLine__(self, line):
477 for w,o,r
in self._operations:
478 if w
is None or w.search(line):
479 line = o.sub(r, line)
483 maskPointers = RegexpReplacer(
"0x[0-9a-fA-F]{4,16}",
"0x########")
484 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)?",
485 "00:00:00 1970-01-01")
486 normalizeEOL = FilePreprocessor()
487 normalizeEOL.__processLine__ =
lambda line: str(line).rstrip() +
'\n'
489 skipEmptyLines = FilePreprocessor()
491 skipEmptyLines.__processLine__ =
lambda line: (line.strip()
and line)
or None
495 class LineSorter(FilePreprocessor):
496 def __init__(self, signature):
497 self.signature = signature
498 self.siglen = len(signature)
499 def __processLine__(self, line):
500 pos = line.find(self.signature)
502 line = line[:(pos+self.siglen)]
503 lst = line[(pos+self.siglen):].split()
505 line +=
" ".join(lst)
509 normalizeExamples = maskPointers + normalizeDate
512 (
"TIMER.TIMER",
r"\s+[+-]?[0-9]+[0-9.]*",
" 0"),
513 (
"release all pending",
r"^.*/([^/]*:.*)",
r"\1"),
514 (
"0x########",
r"\[.*/([^/]*.*)\]",
r"[\1]"),
515 (
"^#.*file",
r"file '.*[/\\]([^/\\]*)$",
r"file '\1"),
516 (
"^JobOptionsSvc.*options successfully read in from",
r"read in from .*[/\\]([^/\\]*)$",
r"file \1"),
518 (
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"),
520 (
"ServiceLocatorHelper::",
"ServiceLocatorHelper::(create|locate)Service",
"ServiceLocatorHelper::service"),
522 (
None,
r"e([-+])0([0-9][0-9])",
r"e\1\2"),
524 (
None,
r'Service reference count check:',
r'Looping over all active services...'),
526 (
None,
r"Property(.*)'ErrorCount':",
r"Property\1'ErrorCounter':"),
528 normalizeExamples += RegexpReplacer(o,r,w)
530 lineSkipper = LineSkipper([
"//GP:",
531 "JobOptionsSvc INFO # ",
532 "JobOptionsSvc WARNING # ",
535 "This machine has a speed",
538 "ToolSvc.Sequenc... INFO",
539 "DataListenerSvc INFO XML written to file:",
540 "[INFO]",
"[WARNING]",
541 "DEBUG No writable file catalog found which contains FID:",
543 "DEBUG Service base class initialized successfully",
544 "DEBUG Incident timing:",
545 "INFO 'CnvServices':[",
549 'EventLoopMgr SUCCESS Event Number = ',
550 'EventLoopMgr SUCCESS ---> Loop Finished',
552 r"^JobOptionsSvc INFO *$",
554 r"(Always|SUCCESS)\s*(Root f|[^ ]* F)ile version:",
555 r"0x[0-9a-fA-F#]+ *Algorithm::sysInitialize\(\) *\[",
556 r"0x[0-9a-fA-F#]* *__gxx_personality_v0 *\[",
557 r"File '.*.xml' does not exist",
558 r"INFO Refer to dataset .* by its file ID:",
559 r"INFO Referring to dataset .* by its file ID:",
560 r"INFO Disconnect from dataset",
561 r"INFO Disconnected from dataset",
562 r"INFO Disconnected data IO:",
563 r"IncidentSvc\s*(DEBUG (Adding|Removing)|VERBOSE Calling)",
565 r"^StatusCodeSvc.*listing all unchecked return codes:",
566 r"^StatusCodeSvc\s*INFO\s*$",
567 r"Num\s*\|\s*Function\s*\|\s*Source Library",
570 r"ERROR Failed to modify file: .* Errno=2 No such file or directory",
572 r"^ +[0-9]+ \|.*ROOT",
573 r"^ +[0-9]+ \|.*\|.*Dict",
575 r"StatusCodeSvc.*all StatusCode instances where checked",
577 r"EventLoopMgr.*---> Loop Finished",
581 r"SUCCESS\s*Booked \d+ Histogram\(s\)",
587 lineSkipper += LineSkipper(regexps = [
588 r'Warning in <TInterpreter::ReadRootmapFile>: .* is already in .*',
591 normalizeExamples = (lineSkipper + normalizeExamples + skipEmptyLines +
592 normalizeEOL + LineSorter(
"Services to release : "))
594 class ReferenceFileValidator:
595 def __init__(self, reffile, cause, result_key, preproc = normalizeExamples):
596 self.reffile = os.path.expandvars(reffile)
598 self.result_key = result_key
599 self.preproc = preproc
600 def __call__(self, stdout, result):
602 if os.path.isfile(self.reffile):
603 orig = open(self.reffile).xreadlines()
605 orig = self.preproc(orig)
609 new = stdout.splitlines()
611 new = self.preproc(new)
613 diffs = difflib.ndiff(orig,new,charjunk=difflib.IS_CHARACTER_JUNK)
614 filterdiffs =
map(
lambda x: x.strip(),filter(
lambda x: x[0] !=
" ",diffs))
617 result[self.result_key] = result.Quote(
"\n".join(filterdiffs))
618 result[self.result_key] += result.Quote(
"""
621 +) standard output of the test""")
622 causes.append(self.cause)
629 def findReferenceBlock(reference, stdout, result, causes, signature_offset=0, signature=None,
632 Given a block of text, tries to find it in the output.
633 The block had to be identified by a signature line. By default, the first
634 line is used as signature, or the line pointed to by signature_offset. If
635 signature_offset points outside the block, a signature line can be passed as
636 signature argument. Note: if 'signature' is None (the default), a negative
637 signature_offset is interpreted as index in a list (e.g. -1 means the last
638 line), otherwise the it is interpreted as the number of lines before the
639 first one of the block the signature must appear.
640 The parameter 'id' allow to distinguish between different calls to this
641 function in the same validation code.
644 reflines = filter(
None,
map(
lambda s: s.rstrip(), reference.splitlines()))
646 raise RuntimeError(
"Empty (or null) reference")
648 outlines = filter(
None,
map(
lambda s: s.rstrip(), stdout.splitlines()))
650 res_field =
"GaudiTest.RefBlock"
652 res_field +=
"_%s" % id
654 if signature
is None:
655 if signature_offset < 0:
656 signature_offset = len(reference)+signature_offset
657 signature = reflines[signature_offset]
660 pos = outlines.index(signature)
661 outlines = outlines[pos-signature_offset:pos+len(reflines)-signature_offset]
662 if reflines != outlines:
663 msg =
"standard output"
665 if not msg
in causes:
667 result[res_field +
".observed"] = result.Quote(
"\n".join(outlines))
669 causes.append(
"missing signature")
670 result[res_field +
".signature"] = result.Quote(signature)
671 if len(reflines) > 1
or signature != reflines[0]:
672 result[res_field +
".expected"] = result.Quote(
"\n".join(reflines))
678 Count the number of messages with required severity (by default ERROR and FATAL)
679 and check if their numbers match the expected ones (0 by default).
680 The dictionary "expected" can be used to tune the number of errors and fatals
681 allowed, or to limit the number of expected warnings etc.
683 stdout = kwargs[
"stdout"]
684 result = kwargs[
"result"]
685 causes = kwargs[
"causes"]
692 outlines = stdout.splitlines()
693 from math
import log10
694 fmt =
"%%%dd - %%s" % (int(log10(len(outlines))+1))
700 if len(words) >= 2
and words[1]
in errors:
701 errors[words[1]].append(fmt%(linecount,l.rstrip()))
704 if len(errors[e]) != expected[e]:
705 causes.append(
'%s(%d)'%(e,len(errors[e])))
706 result[
"GaudiTest.lines.%s"%e] = result.Quote(
'\n'.join(errors[e]))
707 result[
"GaudiTest.lines.%s.expected#"%e] = result.Quote(str(expected[e]))
714 Parse the TTree summary table in lines, starting from pos.
715 Returns a tuple with the dictionary with the digested informations and the
716 position of the first line after the summary.
722 splitcols =
lambda l: [ f.strip()
for f
in l.strip(
"*\n").split(
':',2) ]
725 cols = splitcols(ll[0])
726 r[
"Name"], r[
"Title"] = cols[1:]
728 cols = splitcols(ll[1])
729 r[
"Entries"] = int(cols[1])
731 sizes = cols[2].split()
732 r[
"Total size"] = int(sizes[2])
733 if sizes[-1] ==
"memory":
736 r[
"File size"] = int(sizes[-1])
738 cols = splitcols(ll[2])
739 sizes = cols[2].split()
740 if cols[0] ==
"Baskets":
741 r[
"Baskets"] = int(cols[1])
742 r[
"Basket size"] = int(sizes[2])
743 r[
"Compression"] = float(sizes[-1])
746 if i < (count - 3)
and lines[i].startswith(
"*Tree"):
747 result = parseblock(lines[i:i+3])
748 result[
"Branches"] = {}
750 while i < (count - 3)
and lines[i].startswith(
"*Br"):
751 if i < (count - 2)
and lines[i].startswith(
"*Branch "):
755 branch = parseblock(lines[i:i+3])
756 result[
"Branches"][branch[
"Name"]] = branch
763 Scan stdout to find ROOT TTree summaries and digest them.
765 stars = re.compile(
r"^\*+$")
766 outlines = stdout.splitlines()
767 nlines = len(outlines)
773 while i < nlines
and not stars.match(outlines[i]):
778 trees[tree[
"Name"]] = tree
784 Check that all the keys in reference are in to_check too, with the same value.
785 If the value is a dict, the function is called recursively. to_check can
786 contain more keys than reference, that will not be tested.
787 The function returns at the first difference found.
792 ignore_re = re.compile(ignore)
793 keys = [ key
for key
in reference
if not ignore_re.match(key) ]
795 keys = reference.keys()
799 if (
type(reference[k])
is dict)
and (
type(to_check[k])
is dict):
801 failed = fail_keys =
cmpTreesDicts(reference[k], to_check[k], ignore)
804 failed = to_check[k] != reference[k]
809 fail_keys.insert(0, k)
819 if c
is None or r
is None:
821 return (fail_path, r, c)
824 h_count_re = re.compile(
r"^(.*)SUCCESS\s+Booked (\d+) Histogram\(s\) :\s+(.*)")
828 Extract the histograms infos from the lines starting at pos.
829 Returns the position of the first line after the summary block.
832 h_table_head = re.compile(
r'SUCCESS\s+List of booked (1D|2D|3D|1D profile|2D profile) histograms in directory\s+"(\w*)"')
833 h_short_summ = re.compile(
r"ID=([^\"]+)\s+\"([^\"]+)\"\s+(.*)")
838 m = h_count_re.search(lines[pos])
839 name = m.group(1).strip()
840 total = int(m.group(2))
842 for k, v
in [ x.split(
"=")
for x
in m.group(3).split() ]:
845 header[
"Total"] = total
849 m = h_table_head.search(lines[pos])
852 t = t.replace(
" profile",
"Prof")
859 if l.startswith(
" | ID"):
861 titles = [ x.strip()
for x
in l.split(
"|")][1:]
863 while pos < nlines
and lines[pos].startswith(
" |"):
865 values = [ x.strip()
for x
in l.split(
"|")][1:]
867 for i
in range(len(titles)):
868 hcont[titles[i]] = values[i]
869 cont[hcont[
"ID"]] = hcont
871 elif l.startswith(
" ID="):
872 while pos < nlines
and lines[pos].startswith(
" ID="):
873 values = [ x.strip()
for x
in h_short_summ.search(lines[pos]).groups() ]
874 cont[values[0]] = values
877 raise RuntimeError(
"Cannot understand line %d: '%s'" % (pos, l))
881 summ[d][
"header"] = header
886 summ[name] = {
"header": header}
891 Scan stdout to find ROOT TTree summaries and digest them.
893 outlines = stdout.splitlines()
894 nlines = len(outlines) - 1
902 match = h_count_re.search(outlines[pos])
903 while pos < nlines
and not match:
905 match = h_count_re.search(outlines[pos])
908 summaries.update(summ)
911 class GaudiFilterExecutable(qm.executable.Filter):
912 def __init__(self, input, timeout = -1):
913 """Create a new 'Filter'.
915 'input' -- The string containing the input to provide to the
918 'timeout' -- As for 'TimeoutExecutable.__init__'."""
920 super(GaudiFilterExecutable, self).__init__(input, timeout)
922 self.__timeout = timeout
923 self.stack_trace_file =
None
927 tmpf = tempfile.mkstemp()
929 self.stack_trace_file = tmpf[1]
931 def __UseSeparateProcessGroupForChild(self):
932 """Copied from TimeoutExecutable to allow the re-implementation of
935 if sys.platform ==
"win32":
942 return self.__timeout >= 0
or self.__timeout == -2
945 def _HandleChild(self):
946 """Code copied from both FilterExecutable and TimeoutExecutable.
950 self._ClosePipeEnd(self._stdin_pipe[0])
951 if self._stdout_pipe:
952 self._ClosePipeEnd(self._stdout_pipe[1])
953 if self._stderr_pipe:
954 self._ClosePipeEnd(self._stderr_pipe[1])
962 super(qm.executable.TimeoutExecutable, self)._HandleChild()
964 if self.__UseSeparateProcessGroupForChild():
969 child_pid = self._GetChildPID()
971 os.setpgid(child_pid, child_pid)
987 self.__monitor_pid = os.fork()
988 if self.__monitor_pid != 0:
993 os.setpgid(self.__monitor_pid, child_pid)
999 os.setpgid(0, child_pid)
1008 max_fds = os.sysconf(
"SC_OPEN_MAX")
1011 for fd
in xrange(max_fds):
1017 if self.__timeout >= 0:
1019 time.sleep (self.__timeout)
1022 if sys.platform ==
"linux2":
1024 os.path.join(
"/proc", str(child_pid),
"exe"),
1026 "-batch",
"-n",
"-x",
1027 "'%s'" % os.path.join(os.path.dirname(__file__),
"stack-trace.gdb")]
1030 o = os.popen(
" ".join(cmd)).read()
1031 open(self.stack_trace_file,
"w").write(o)
1035 os.kill(0, signal.SIGKILL)
1038 select.select ([], [], [])
1043 elif self.__timeout >= 0
and sys.platform ==
"win32":
1045 self.__monitor_thread = Thread(target = self.__Monitor)
1046 self.__monitor_thread.start()
1048 if sys.platform ==
"win32":
1050 def __Monitor(self):
1051 """Code copied from FilterExecutable.
1052 Kill the child if the timeout expires.
1054 This function is run in the monitoring thread."""
1059 timeout = int(self.__timeout * 1000)
1062 result = win32event.WaitForSingleObject(self._GetChildPID(),
1065 if result == win32con.WAIT_TIMEOUT:
1071 class GaudiExeTest(ExecTestBase):
1072 """Standard Gaudi test.
1075 qm.fields.TextField(
1079 description=
"""The path to the program.
1081 This field indicates the path to the program. If it is not
1082 an absolute path, the value of the 'PATH' environment
1083 variable will be used to search for the program.
1084 If not specified, $GAUDIEXE or Gaudi.exe are used.
1087 qm.fields.SetField(qm.fields.TextField(
1089 title=
"Argument List",
1090 description=
"""The command-line arguments.
1092 If this field is left blank, the program is run without any
1095 Use this field to specify the option files.
1097 An implicit 0th argument (the path to the program) is added
1100 qm.fields.TextField(
1103 description=
"""Options to be passed to the application.
1105 This field allows to pass a list of options to the main program
1106 without the need of a separate option file.
1108 The content of the field is written to a temporary file which name
1109 is passed the the application as last argument (appended to the
1110 field "Argument List".
1116 qm.fields.TextField(
1118 title=
"Working Directory",
1119 description=
"""Path to the working directory.
1121 If this field is left blank, the program will be run from the qmtest
1122 directory, otherwise from the directory specified.""",
1125 qm.fields.TextField(
1127 title=
"Reference Output",
1128 description=
"""Path to the file containing the reference output.
1130 If this field is left blank, any standard output will be considered
1133 If the reference file is specified, any output on standard error is
1136 qm.fields.TextField(
1137 name=
"error_reference",
1138 title=
"Reference for standard error",
1139 description=
"""Path to the file containing the reference for the standard error.
1141 If this field is left blank, any standard output will be considered
1144 If the reference file is specified, any output on standard error is
1147 qm.fields.SetField(qm.fields.TextField(
1148 name =
"unsupported_platforms",
1149 title =
"Unsupported Platforms",
1150 description =
"""Platform on which the test must not be run.
1152 List of regular expressions identifying the platforms on which the
1153 test is not run and the result is set to UNTESTED."""
1156 qm.fields.TextField(
1158 title =
"Validator",
1159 description =
"""Function to validate the output of the test.
1161 If defined, the function is used to validate the products of the
1163 The function is called passing as arguments:
1164 self: the test class instance
1165 stdout: the standard output of the executed test
1166 stderr: the standard error of the executed test
1167 result: the Result objects to fill with messages
1168 The function must return a list of causes for the failure.
1169 If specified, overrides standard output, standard error and
1177 qm.fields.BooleanField(
1178 name =
"use_temp_dir",
1179 title =
"Use temporary directory",
1180 description =
"""Use temporary directory.
1182 If set to true, use a temporary directory as working directory.
1184 default_value=
"false"
1187 qm.fields.IntegerField(
1189 title =
"Expected signal",
1190 description =
"""Expect termination by signal.""",
1196 platform = self.GetPlatform()
1197 unsupported = [ re.compile(x)
1198 for x
in [ str(y).strip()
1199 for y
in self.unsupported_platforms ]
1202 for p_re
in unsupported:
1203 if p_re.search(platform):
1204 result.SetOutcome(result.UNTESTED)
1205 result[result.CAUSE] =
'Platform not supported.'
1211 Return the platform Id defined in CMTCONFIG or SCRAM_ARCH.
1215 if "CMTCONFIG" in os.environ:
1216 arch = os.environ[
"CMTCONFIG"]
1217 elif "SCRAM_ARCH" in os.environ:
1218 arch = os.environ[
"SCRAM_ARCH"]
1223 Return True if the current platform is Windows.
1225 This function was needed because of the change in the CMTCONFIG format,
1226 from win32_vc71_dbg to i686-winxp-vc9-dbg.
1228 platform = self.GetPlatform()
1229 return "winxp" in platform
or platform.startswith(
"win")
1231 def _expandReferenceFileName(self, reffile):
1237 platformSplit =
lambda p: set(p.split(
'-' in p
and '-' or '_'))
1239 reference = os.path.normpath(os.path.expandvars(reffile))
1241 spec_ref = reference[:-3] + self.GetPlatform()[0:3] + reference[-3:]
1242 if os.path.isfile(spec_ref):
1243 reference = spec_ref
1246 dirname, basename = os.path.split(reference)
1247 if not dirname: dirname =
'.'
1248 head = basename +
"."
1249 head_len = len(head)
1250 platform = platformSplit(self.GetPlatform())
1251 if 'do0' in platform:
1254 for f
in os.listdir(dirname):
1255 if f.startswith(head):
1256 req_plat = platformSplit(f[head_len:])
1257 if platform.issuperset(req_plat):
1258 candidates.append( (len(req_plat), f) )
1263 reference = os.path.join(dirname, candidates[-1][1])
1266 def CheckTTreesSummaries(self, stdout, result, causes,
1268 ignore =
r"Basket|.*size|Compression"):
1270 Compare the TTree summaries in stdout with the ones in trees_dict or in
1271 the reference file. By default ignore the size, compression and basket
1273 The presence of TTree summaries when none is expected is not a failure.
1275 if trees_dict
is None:
1276 reference = self._expandReferenceFileName(self.reference)
1278 if reference
and os.path.isfile(reference):
1283 from pprint
import PrettyPrinter
1284 pp = PrettyPrinter()
1286 result[
"GaudiTest.TTrees.expected"] = result.Quote(pp.pformat(trees_dict))
1288 result[
"GaudiTest.TTrees.ignore"] = result.Quote(ignore)
1293 causes.append(
"trees summaries")
1295 result[
"GaudiTest.TTrees.failure_on"] = result.Quote(msg)
1296 result[
"GaudiTest.TTrees.found"] = result.Quote(pp.pformat(trees))
1300 def CheckHistosSummaries(self, stdout, result, causes,
1304 Compare the TTree summaries in stdout with the ones in trees_dict or in
1305 the reference file. By default ignore the size, compression and basket
1307 The presence of TTree summaries when none is expected is not a failure.
1310 reference = self._expandReferenceFileName(self.reference)
1312 if reference
and os.path.isfile(reference):
1317 from pprint
import PrettyPrinter
1318 pp = PrettyPrinter()
1320 result[
"GaudiTest.Histos.expected"] = result.Quote(pp.pformat(dict))
1322 result[
"GaudiTest.Histos.ignore"] = result.Quote(ignore)
1327 causes.append(
"histos summaries")
1329 result[
"GaudiTest.Histos.failure_on"] = result.Quote(msg)
1330 result[
"GaudiTest.Histos.found"] = result.Quote(pp.pformat(histos))
1334 def ValidateWithReference(self, stdout, stderr, result, causes, preproc = None):
1336 Default validation action: compare standard output and error to the
1341 preproc = normalizeExamples
1343 reference = self._expandReferenceFileName(self.reference)
1345 if reference
and os.path.isfile(reference):
1346 result[
"GaudiTest.output_reference"] = reference
1347 causes += ReferenceFileValidator(reference,
1349 "GaudiTest.output_diff",
1350 preproc = preproc)(stdout, result)
1353 causes = self.CheckTTreesSummaries(stdout, result, causes)
1354 causes = self.CheckHistosSummaries(stdout, result, causes)
1358 newref = open(reference +
".new",
"w")
1360 for l
in stdout.splitlines():
1361 newref.write(l.rstrip() +
'\n')
1369 reference = self._expandReferenceFileName(self.error_reference)
1371 if reference
and os.path.isfile(reference):
1372 result[
"GaudiTest.error_reference"] = reference
1373 newcauses = ReferenceFileValidator(reference,
1375 "GaudiTest.error_diff",
1376 preproc = preproc)(stderr, result)
1379 newref = open(reference +
".new",
"w")
1381 for l
in stderr.splitlines():
1382 newref.write(l.rstrip() +
'\n')
1385 causes += BasicOutputValidator(self.stderr,
1387 "ExecTest.expected_stderr")(stderr, result)
1391 def ValidateOutput(self, stdout, stderr, result):
1394 if self.validator.strip() !=
"":
1395 class CallWrapper(object):
1397 Small wrapper class to dynamically bind some default arguments
1400 def __init__(self, callable, extra_args = {}):
1401 self.callable = callable
1402 self.extra_args = extra_args
1404 from inspect
import getargspec
1405 self.args_order = getargspec(callable)[0]
1408 if self.args_order[0] ==
"self":
1409 del self.args_order[0]
1410 def __call__(self, *args, **kwargs):
1412 positional = self.args_order[:len(args)]
1414 kwargs = dict(kwargs)
1415 for a
in self.extra_args:
1418 if a
not in positional
and a
not in kwargs:
1419 kwargs[a] = self.extra_args[a]
1420 return apply(self.callable, args, kwargs)
1422 exported_symbols = {
"self":self,
1427 "findReferenceBlock":
1428 CallWrapper(findReferenceBlock, {
"stdout":stdout,
1431 "validateWithReference":
1432 CallWrapper(self.ValidateWithReference, {
"stdout":stdout,
1437 CallWrapper(countErrorLines, {
"stdout":stdout,
1440 "checkTTreesSummaries":
1441 CallWrapper(self.CheckTTreesSummaries, {
"stdout":stdout,
1444 "checkHistosSummaries":
1445 CallWrapper(self.CheckHistosSummaries, {
"stdout":stdout,
1450 exec self.validator
in globals(), exported_symbols
1452 self.ValidateWithReference(stdout, stderr, result, causes)
1456 def DumpEnvironment(self, result):
1458 Add the content of the environment to the result object.
1460 Copied from the QMTest class of COOL.
1462 vars = os.environ.keys()
1464 result[
'GaudiTest.environment'] = \
1465 result.Quote(
'\n'.join([
"%s=%s"%(v,os.environ[v])
for v
in vars]))
1467 def Run(self, context, result):
1470 'context' -- A 'Context' giving run-time parameters to the
1473 'result' -- A 'Result' object. The outcome will be
1474 'Result.PASS' when this method is called. The 'result' may be
1475 modified by this method to indicate outcomes other than
1476 'Result.PASS' or to add annotations."""
1479 if self.PlatformIsNotSupported(context, result):
1485 elif "GAUDIEXE" in os.environ:
1486 prog = os.environ[
"GAUDIEXE"]
1491 dummy, prog_ext = os.path.splitext(prog)
1492 if prog_ext
not in [
".exe",
".py",
".bat" ]
and self.isWinPlatform():
1496 prog =
which(prog)
or prog
1499 args =
map(rationalizepath, self.args)
1506 if self.options.strip():
1508 if re.search(
r"from\s+Gaudi.Configuration\s+import\s+\*|from\s+Configurables\s+import", self.options):
1510 tmpfile = TempFile(ext)
1511 tmpfile.writelines(
"\n".join(self.options.splitlines()))
1513 args.append(tmpfile.name)
1514 result[
"GaudiTest.options"] = result.Quote(self.options)
1517 if prog_ext ==
".py":
1519 if self.isWinPlatform():
1520 prog =
which(
"python.exe")
or "python.exe"
1522 prog =
which(
"python")
or "python"
1525 origdir = os.getcwd()
1527 os.chdir(str(os.path.normpath(os.path.expandvars(self.workdir))))
1528 elif self.use_temp_dir ==
"true":
1529 if "QMTEST_TMPDIR" in os.environ:
1530 qmtest_tmpdir = os.environ[
"QMTEST_TMPDIR"]
1531 if not os.path.exists(qmtest_tmpdir):
1532 os.makedirs(qmtest_tmpdir)
1533 os.chdir(qmtest_tmpdir)
1534 elif "qmtest.tmpdir" in context:
1535 os.chdir(context[
"qmtest.tmpdir"])
1537 if "QMTEST_IGNORE_TIMEOUT" not in os.environ:
1538 self.timeout = max(self.timeout,600)
1544 self._CreateEclipseLaunch(prog, args, destdir = os.path.join(origdir,
'.eclipse'))
1546 self.RunProgram(prog,
1550 if result.GetOutcome()
not in [ result.PASS ]:
1551 self.DumpEnvironment(result)
1556 def RunProgram(self, program, arguments, context, result):
1557 """Run the 'program'.
1559 'program' -- The path to the program to run.
1561 'arguments' -- A list of the arguments to the program. This
1562 list must contain a first argument corresponding to 'argv[0]'.
1564 'context' -- A 'Context' giving run-time parameters to the
1567 'result' -- A 'Result' object. The outcome will be
1568 'Result.PASS' when this method is called. The 'result' may be
1569 modified by this method to indicate outcomes other than
1570 'Result.PASS' or to add annotations.
1572 @attention: This method has been copied from command.ExecTestBase
1573 (QMTest 2.3.0) and modified to keep stdout and stderr
1574 for tests that have been terminated by a signal.
1575 (Fundamental for debugging in the Application Area)
1579 environment = self.MakeEnvironment(context)
1581 if "slc6" in environment.get(
'CMTCONFIG',
''):
1582 environment[
'TERM'] =
'dumb'
1584 if self.timeout >= 0:
1585 timeout = self.timeout
1593 e = GaudiFilterExecutable(self.stdin, timeout)
1595 exit_status = e.Run(arguments, environment, path = program)
1597 if e.stack_trace_file
and os.path.exists(e.stack_trace_file):
1598 stack_trace = open(e.stack_trace_file).read()
1599 os.remove(e.stack_trace_file)
1603 result[
"ExecTest.stack_trace"] = result.Quote(stack_trace)
1606 if (sys.platform ==
"win32" or os.WIFEXITED(exit_status)
1607 or self.signal == os.WTERMSIG(exit_status)):
1612 if self.exit_code
is None:
1614 elif sys.platform ==
"win32":
1615 exit_code = exit_status
1617 exit_code = os.WEXITSTATUS(exit_status)
1622 result[
"ExecTest.exit_code"] = str(exit_code)
1623 result[
"ExecTest.stdout"] = result.Quote(stdout)
1624 result[
"ExecTest.stderr"] = result.Quote(stderr)
1626 if exit_code != self.exit_code:
1627 causes.append(
"exit_code")
1628 result[
"ExecTest.expected_exit_code"] \
1629 = str(self.exit_code)
1631 causes += self.ValidateOutput(stdout, stderr, result)
1634 result.Fail(
"Unexpected %s." % string.join(causes,
", "))
1635 elif os.WIFSIGNALED(exit_status):
1638 signal_number = str(os.WTERMSIG(exit_status))
1640 result.Fail(
"Program terminated by signal.")
1644 result.Fail(
"Exceeded time limit (%ds), terminated." % timeout)
1645 result[
"ExecTest.signal_number"] = signal_number
1646 result[
"ExecTest.stdout"] = result.Quote(e.stdout)
1647 result[
"ExecTest.stderr"] = result.Quote(e.stderr)
1649 result[
"ExecTest.expected_signal_number"] = str(self.signal)
1650 elif os.WIFSTOPPED(exit_status):
1653 signal_number = str(os.WSTOPSIG(exit_status))
1655 result.Fail(
"Program stopped by signal.")
1659 result.Fail(
"Exceeded time limit (%ds), stopped." % timeout)
1660 result[
"ExecTest.signal_number"] = signal_number
1661 result[
"ExecTest.stdout"] = result.Quote(e.stdout)
1662 result[
"ExecTest.stderr"] = result.Quote(e.stderr)
1666 result.Fail(
"Program did not terminate normally.")
1672 result[
"ExecTest.stdout"] = result[
"ExecTest.stdout"].replace(esc,repr_esc)
1676 def _CreateEclipseLaunch(self, prog, args, destdir = None):
1677 if 'NO_ECLIPSE_LAUNCHERS' in os.environ:
1682 projbasedir = os.path.normpath(destdir)
1683 while not os.path.exists(os.path.join(projbasedir,
".project")):
1684 oldprojdir = projbasedir
1685 projbasedir = os.path.normpath(os.path.join(projbasedir, os.pardir))
1688 if oldprojdir == projbasedir:
1692 if not os.path.exists(destdir):
1693 os.makedirs(destdir)
1695 from xml.etree
import ElementTree
as ET
1696 t = ET.parse(os.path.join(projbasedir,
".project"))
1697 projectName = t.find(
"name").text
1700 destfile =
"%s.launch" % self._Runnable__id
1702 destfile = os.path.join(destdir, destfile)
1704 if self.options.strip():
1708 tempfile = args.pop()
1709 optsfile = destfile + os.path.splitext(tempfile)[1]
1710 shutil.copyfile(tempfile, optsfile)
1711 args.append(optsfile)
1714 from xml.sax.saxutils
import quoteattr
1718 data[
"environment"] =
"\n".join([
'<mapEntry key=%s value=%s/>' % (quoteattr(k), quoteattr(v))
1719 for k, v
in os.environ.iteritems()
1720 if k
not in (
'MAKEOVERRIDES',
'MAKEFLAGS',
'MAKELEVEL')])
1722 data[
"exec"] =
which(prog)
or prog
1723 if os.path.basename(data[
"exec"]).lower().startswith(
"python"):
1724 data[
"stopAtMain"] =
"false"
1726 data[
"stopAtMain"] =
"true"
1728 data[
"args"] =
" ".join(
map(rationalizepath, args))
1729 if self.isWinPlatform():
1730 data[
"args"] =
" ".join([
"/debugexe"] +
map(rationalizepath, [data[
"exec"]] + args))
1731 data[
"exec"] =
which(
"vcexpress.exe")
1733 if not self.use_temp_dir:
1734 data[
"workdir"] = os.getcwd()
1738 data[
"workdir"] = destdir
1740 data[
"project"] = projectName.strip()
1743 xml_template =
u"""<?xml version="1.0" encoding="UTF-8" standalone="no"?>
1744 <launchConfiguration type="org.eclipse.cdt.launch.applicationLaunchType">
1745 <booleanAttribute key="org.eclipse.cdt.debug.mi.core.AUTO_SOLIB" value="true"/>
1746 <listAttribute key="org.eclipse.cdt.debug.mi.core.AUTO_SOLIB_LIST"/>
1747 <stringAttribute key="org.eclipse.cdt.debug.mi.core.DEBUG_NAME" value="gdb"/>
1748 <stringAttribute key="org.eclipse.cdt.debug.mi.core.GDB_INIT" value=".gdbinit"/>
1749 <listAttribute key="org.eclipse.cdt.debug.mi.core.SOLIB_PATH"/>
1750 <booleanAttribute key="org.eclipse.cdt.debug.mi.core.STOP_ON_SOLIB_EVENTS" value="false"/>
1751 <booleanAttribute key="org.eclipse.cdt.debug.mi.core.breakpointsFullPath" value="false"/>
1752 <stringAttribute key="org.eclipse.cdt.debug.mi.core.commandFactory" value="org.eclipse.cdt.debug.mi.core.standardCommandFactory"/>
1753 <stringAttribute key="org.eclipse.cdt.debug.mi.core.protocol" value="mi"/>
1754 <booleanAttribute key="org.eclipse.cdt.debug.mi.core.verboseMode" value="false"/>
1755 <intAttribute key="org.eclipse.cdt.launch.ATTR_BUILD_BEFORE_LAUNCH_ATTR" value="0"/>
1756 <stringAttribute key="org.eclipse.cdt.launch.COREFILE_PATH" value=""/>
1757 <stringAttribute key="org.eclipse.cdt.launch.DEBUGGER_ID" value="org.eclipse.cdt.debug.mi.core.CDebuggerNew"/>
1758 <stringAttribute key="org.eclipse.cdt.launch.DEBUGGER_REGISTER_GROUPS" value=""/>
1759 <stringAttribute key="org.eclipse.cdt.launch.DEBUGGER_START_MODE" value="run"/>
1760 <booleanAttribute key="org.eclipse.cdt.launch.DEBUGGER_STOP_AT_MAIN" value="%(stopAtMain)s"/>
1761 <stringAttribute key="org.eclipse.cdt.launch.DEBUGGER_STOP_AT_MAIN_SYMBOL" value="main"/>
1762 <booleanAttribute key="org.eclipse.cdt.launch.ENABLE_REGISTER_BOOKKEEPING" value="false"/>
1763 <booleanAttribute key="org.eclipse.cdt.launch.ENABLE_VARIABLE_BOOKKEEPING" value="false"/>
1764 <stringAttribute key="org.eclipse.cdt.launch.FORMAT" value="<?xml version="1.0" encoding="UTF-8" standalone="no"?><contentList/>"/>
1765 <stringAttribute key="org.eclipse.cdt.launch.GLOBAL_VARIABLES" value="<?xml version="1.0" encoding="UTF-8" standalone="no"?> <globalVariableList/> "/>
1766 <stringAttribute key="org.eclipse.cdt.launch.MEMORY_BLOCKS" value="<?xml version="1.0" encoding="UTF-8" standalone="no"?> <memoryBlockExpressionList/> "/>
1767 <stringAttribute key="org.eclipse.cdt.launch.PROGRAM_ARGUMENTS" value="%(args)s"/>
1768 <stringAttribute key="org.eclipse.cdt.launch.PROGRAM_NAME" value="%(exec)s"/>
1769 <stringAttribute key="org.eclipse.cdt.launch.PROJECT_ATTR" value="%(project)s"/>
1770 <stringAttribute key="org.eclipse.cdt.launch.PROJECT_BUILD_CONFIG_ID_ATTR" value=""/>
1771 <stringAttribute key="org.eclipse.cdt.launch.WORKING_DIRECTORY" value="%(workdir)s"/>
1772 <booleanAttribute key="org.eclipse.cdt.launch.ui.ApplicationCDebuggerTab.DEFAULTS_SET" value="true"/>
1773 <booleanAttribute key="org.eclipse.cdt.launch.use_terminal" value="true"/>
1774 <listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_PATHS">
1775 <listEntry value="/%(project)s"/>
1777 <listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_TYPES">
1778 <listEntry value="4"/>
1780 <booleanAttribute key="org.eclipse.debug.core.appendEnvironmentVariables" value="false"/>
1781 <mapAttribute key="org.eclipse.debug.core.environmentVariables">
1784 <mapAttribute key="org.eclipse.debug.core.preferred_launchers">
1785 <mapEntry key="[debug]" value="org.eclipse.cdt.cdi.launch.localCLaunch"/>
1787 <listAttribute key="org.eclipse.debug.ui.favoriteGroups">
1788 <listEntry value="org.eclipse.debug.ui.launchGroup.debug"/>
1790 </launchConfiguration>
1795 data[k] = codecs.decode(data[k],
'utf-8')
1796 xml = xml_template % data
1799 codecs.open(destfile,
"w", encoding=
'utf-8').write(xml)
1801 print 'WARNING: problem generating Eclipse launcher'
1808 import simplejson
as json
1810 class HTMLResultStream(ResultStream):
1811 """An 'HTMLResultStream' writes its output to a set of HTML files.
1813 The argument 'dir' is used to select the destination directory for the HTML
1815 The destination directory may already contain the report from a previous run
1816 (for example of a different package), in which case it will be extended to
1817 include the new data.
1820 qm.fields.TextField(
1822 title =
"Destination Directory",
1823 description =
"""The name of the directory.
1825 All results will be written to the directory indicated.""",
1827 default_value =
""),
1830 def __init__(self, arguments = None, **args):
1831 """Prepare the destination directory.
1833 Creates the destination directory and store in it some preliminary
1834 annotations and the static files found in the template directory
1837 ResultStream.__init__(self, arguments, **args)
1839 self._summaryFile = os.path.join(self.dir,
"summary.json")
1840 self._annotationsFile = os.path.join(self.dir,
"annotations.json")
1842 templateDir = os.path.join(os.path.dirname(__file__),
"html_report")
1843 if not os.path.isdir(self.dir):
1844 os.makedirs(self.dir)
1846 for f
in os.listdir(templateDir):
1847 src = os.path.join(templateDir, f)
1848 dst = os.path.join(self.dir, f)
1849 if not os.path.isdir(src)
and not os.path.exists(dst):
1850 shutil.copy(src, dst)
1852 if "CMTCONFIG" in os.environ:
1853 self.WriteAnnotation(
"cmt.cmtconfig", os.environ[
"CMTCONFIG"])
1855 self.WriteAnnotation(
"hostname", socket.gethostname())
1857 def _updateSummary(self):
1858 """Helper function to extend the global summary file in the destination
1861 if os.path.exists(self._summaryFile):
1862 oldSummary = json.load(open(self._summaryFile))
1865 ids = set([ i[
"id"]
for i
in self._summary ])
1866 newSummary = [ i
for i
in oldSummary
if i[
"id"]
not in ids ]
1867 newSummary.extend(self._summary)
1868 json.dump(newSummary, open(self._summaryFile,
"w"),
1871 def WriteAnnotation(self, key, value):
1872 """Writes the annotation to the annotation file.
1873 If the key is already present with a different value, the value becomes
1874 a list and the new value is appended to it, except for start_time and
1878 if os.path.exists(self._annotationsFile):
1879 annotations = json.load(open(self._annotationsFile))
1883 key, value =
map(str, [key, value])
1884 if key ==
"qmtest.run.start_time":
1889 if key
not in annotations:
1890 annotations[key] = value
1891 if "qmtest.run.end_time" in annotations:
1892 del annotations[
"qmtest.run.end_time"]
1895 if key
in annotations:
1896 old = annotations[key]
1897 if type(old)
is list:
1898 if value
not in old:
1899 annotations[key].append(value)
1901 annotations[key] = [old, value]
1903 annotations[key] = value
1905 json.dump(annotations, open(self._annotationsFile,
"w"),
1908 def WriteResult(self, result):
1909 """Prepare the test result directory in the destination directory storing
1910 into it the result fields.
1911 A summary of the test result is stored both in a file in the test directory
1912 and in the global summary file.
1915 summary[
"id"] = result.GetId()
1916 summary[
"outcome"] = result.GetOutcome()
1917 summary[
"cause"] = result.GetCause()
1918 summary[
"fields"] = result.keys()
1919 summary[
"fields"].sort()
1922 for f
in [
"id",
"outcome",
"cause"]:
1923 summary[f] = str(summary[f])
1924 summary[
"fields"] =
map(str, summary[
"fields"])
1926 self._summary.append(summary)
1932 testOutDir = os.path.join(self.dir, summary[
"id"])
1933 if not os.path.isdir(testOutDir):
1934 os.makedirs(testOutDir)
1935 json.dump(summary, open(os.path.join(testOutDir,
"summary.json"),
"w"),
1937 for f
in summary[
"fields"]:
1938 open(os.path.join(testOutDir, f),
"w").write(result[f])
1940 self._updateSummary()
1942 def Summarize(self):
1949 class XMLResultStream(ResultStream):
1950 """An 'XMLResultStream' writes its output to a Ctest XML file.
1952 The argument 'dir' is used to select the destination file for the XML
1954 The destination directory may already contain the report from a previous run
1955 (for example of a different package), in which case it will be overrided to
1959 qm.fields.TextField(
1961 title =
"Destination Directory",
1962 description =
"""The name of the directory.
1964 All results will be written to the directory indicated.""",
1966 default_value =
""),
1967 qm.fields.TextField(
1969 title =
"Output File Prefix",
1970 description =
"""The output file name will be the specified prefix
1971 followed by 'Test.xml' (CTest convention).""",
1973 default_value =
""),
1976 def __init__(self, arguments = None, **args):
1977 """Prepare the destination directory.
1979 Creates the destination directory and store in it some preliminary
1982 ResultStream.__init__(self, arguments, **args)
1984 self._xmlFile = os.path.join(self.dir, self.prefix +
'Test.xml')
1987 self._startTime =
None
1988 self._endTime =
None
1990 if not os.path.isfile(self._xmlFile):
1992 if not os.path.exists(os.path.dirname(self._xmlFile)):
1993 os.makedirs(os.path.dirname(self._xmlFile))
1995 newdataset = ET.Element(
"newdataset")
1996 self._tree = ET.ElementTree(newdataset)
1997 self._tree.write(self._xmlFile)
2000 self._tree = ET.parse(self._xmlFile)
2001 newdataset = self._tree.getroot()
2008 for site
in newdataset.getiterator() :
2009 if site.get(
"OSPlatform") == os.uname()[4]:
2019 import multiprocessing
2021 "BuildName" : os.getenv(
"CMTCONFIG"),
2022 "Name" : os.uname()[1] ,
2023 "Generator" :
"QMTest "+qm.version ,
2024 "OSName" : os.uname()[0] ,
2025 "Hostname" : socket.gethostname() ,
2026 "OSRelease" : os.uname()[2] ,
2027 "OSVersion" :os.uname()[3] ,
2028 "OSPlatform" :os.uname()[4] ,
2029 "Is64Bits" :
"unknown" ,
2030 "VendorString" :
"unknown" ,
2031 "VendorID" :
"unknown" ,
2032 "FamilyID" :
"unknown" ,
2033 "ModelID" :
"unknown" ,
2034 "ProcessorCacheSize" :
"unknown" ,
2035 "NumberOfLogicalCPU" : str(multiprocessing.cpu_count()) ,
2036 "NumberOfPhysicalCPU" :
"0" ,
2037 "TotalVirtualMemory" :
"0" ,
2038 "TotalPhysicalMemory" :
"0" ,
2039 "LogicalProcessorsPerPhysical" :
"0" ,
2040 "ProcessorClockFrequency" :
"0" ,
2042 self._site = ET.SubElement(newdataset,
"Site", attrib)
2043 self._Testing = ET.SubElement(self._site,
"Testing")
2046 self._StartDateTime = ET.SubElement(self._Testing,
"StartDateTime")
2048 self._StartTestTime = ET.SubElement(self._Testing,
"StartTestTime")
2051 self._TestList = ET.SubElement(self._Testing,
"TestList")
2054 self._EndDateTime = ET.SubElement(self._Testing,
"EndDateTime")
2057 self._EndTestTime = ET.SubElement(self._Testing,
"EndTestTime")
2061 self._ElapsedMinutes = ET.SubElement(self._Testing,
"ElapsedMinutes")
2065 self._Testing = self._site.find(
"Testing")
2066 self._StartDateTime = self._Testing.find(
"StartDateTime")
2067 self._StartTestTime = self._Testing.find(
"StartTestTime")
2068 self._TestList = self._Testing.find(
"TestList")
2069 self._EndDateTime = self._Testing.find(
"EndDateTime")
2070 self._EndTestTime = self._Testing.find(
"EndTestTime")
2071 self._ElapsedMinutes = self._Testing.find(
"ElapsedMinutes")
2074 # Add some non-QMTest attributes
2075 if "CMTCONFIG" in os.environ:
2076 self.WriteAnnotation("cmt.cmtconfig", os.environ["CMTCONFIG"])
2078 self.WriteAnnotation("hostname", socket.gethostname())
2082 def WriteAnnotation(self, key, value):
2083 if key ==
"qmtest.run.start_time":
2084 if self._site.get(
"qmtest.run.start_time")
is not None :
2086 self._site.set(str(key),str(value))
2087 def WriteResult(self, result):
2088 """Prepare the test result directory in the destination directory storing
2089 into it the result fields.
2090 A summary of the test result is stored both in a file in the test directory
2091 and in the global summary file.
2094 summary[
"id"] = result.GetId()
2095 summary[
"outcome"] = result.GetOutcome()
2096 summary[
"cause"] = result.GetCause()
2097 summary[
"fields"] = result.keys()
2098 summary[
"fields"].sort()
2102 for f
in [
"id",
"outcome",
"cause"]:
2103 summary[f] = str(summary[f])
2104 summary[
"fields"] =
map(str, summary[
"fields"])
2110 if "qmtest.start_time" in summary[
"fields"]:
2111 haveStartDate =
True
2113 haveStartDate =
False
2114 if "qmtest.end_time" in summary[
"fields"]:
2121 self._startTime = calendar.timegm(time.strptime(result[
"qmtest.start_time"],
"%Y-%m-%dT%H:%M:%SZ"))
2122 if self._StartTestTime.text
is None:
2123 self._StartDateTime.text = time.strftime(
"%b %d %H:%M %Z", time.localtime(self._startTime))
2124 self._StartTestTime.text = str(self._startTime)
2125 self._site.set(
"BuildStamp" , result[
"qmtest.start_time"] )
2129 self._endTime = calendar.timegm(time.strptime(result[
"qmtest.end_time"],
"%Y-%m-%dT%H:%M:%SZ"))
2133 tl = ET.Element(
"Test")
2134 tl.text = summary[
"id"]
2135 self._TestList.insert(0,tl)
2138 Test = ET.Element(
"Test")
2139 if summary[
"outcome"] ==
"PASS":
2140 Test.set(
"Status",
"passed")
2141 elif summary[
"outcome"] ==
"FAIL":
2142 Test.set(
"Status",
"failed")
2143 elif summary[
"outcome"] ==
"SKIPPED" or summary[
"outcome"] ==
"UNTESTED":
2144 Test.set(
"Status",
"skipped")
2145 elif summary[
"outcome"] ==
"ERROR":
2146 Test.set(
"Status",
"failed")
2147 Name = ET.SubElement(Test,
"Name",)
2148 Name.text = summary[
"id"]
2149 Results = ET.SubElement(Test,
"Results")
2152 self._Testing.insert(3,Test)
2154 if haveStartDate
and haveEndDate:
2156 delta = self._endTime - self._startTime
2157 testduration = str(delta)
2158 Testduration= ET.SubElement(Results,
"NamedMeasurement")
2159 Testduration.set(
"name",
"Execution Time")
2160 Testduration.set(
"type",
"numeric/float" )
2161 value = ET.SubElement(Testduration,
"Value")
2162 value.text = testduration
2165 for n
in (
"qmtest.end_time",
"qmtest.start_time",
"qmtest.cause",
"ExecTest.stdout"):
2166 if n
in summary[
"fields"]:
2167 summary[
"fields"].
remove(n)
2171 if "ExecTest.exit_code" in summary[
"fields"] :
2172 summary[
"fields"].
remove(
"ExecTest.exit_code")
2173 ExitCode= ET.SubElement(Results,
"NamedMeasurement")
2174 ExitCode.set(
"name",
"exit_code")
2175 ExitCode.set(
"type",
"numeric/integer" )
2176 value = ET.SubElement(ExitCode,
"Value")
2179 TestStartTime= ET.SubElement(Results,
"NamedMeasurement")
2180 TestStartTime.set(
"name",
"Start_Time")
2181 TestStartTime.set(
"type",
"String" )
2182 value = ET.SubElement(TestStartTime,
"Value")
2188 TestEndTime= ET.SubElement(Results,
"NamedMeasurement")
2189 TestEndTime.set(
"name",
"End_Time")
2190 TestEndTime.set(
"type",
"String" )
2191 value = ET.SubElement(TestEndTime,
"Value")
2197 if summary[
"cause"]:
2198 FailureCause= ET.SubElement(Results,
"NamedMeasurement")
2199 FailureCause.set(
"name",
"Cause")
2200 FailureCause.set(
"type",
"String" )
2201 value = ET.SubElement(FailureCause,
"Value")
2206 for field
in summary[
"fields"] :
2207 fields[field] = ET.SubElement(Results,
"NamedMeasurement")
2208 fields[field].set(
"type",
"String")
2209 fields[field].set(
"name",field)
2210 value = ET.SubElement(fields[field],
"Value")
2212 if "<pre>" in result[field][0:6] :
2218 if result.has_key(
"ExecTest.stdout" ) :
2219 Measurement = ET.SubElement(Results,
"Measurement")
2220 value = ET.SubElement(Measurement,
"Value")
2221 if "<pre>" in result[
"ExecTest.stdout"][0:6] :
2228 self._tree.write(self._xmlFile,
"utf-8")
2231 def Summarize(self):
2234 self._EndTestTime.text = str(self._endTime)
2235 self._EndDateTime.text = time.strftime(
"%b %d %H:%M %Z", time.localtime(self._endTime))
2238 if self._endTime
and self._startTime:
2239 delta = self._endTime - self._startTime
2242 self._ElapsedMinutes.text = str(delta/60)
2245 self._tree.write(self._xmlFile,
"utf-8")
def findTTreeSummaries(stdout)
def _parseTTreeSummary(lines, pos)
def total_seconds_replacement(timedelta)
def parseHistosSummary(lines, pos)
struct GAUDI_API map
Parametrisation class for map-like implementation.
NamedRange_< CONTAINER > range(const CONTAINER &cnt, std::string name)
simple function to create the named range form arbitrary container
def getCmpFailingValues(reference, to_check, fail_path)
def escape_xml_illegal_chars
def which(executable)
Locates an executable in the executables path ($PATH) and returns the full path to it...
def findHistosSummaries(stdout)
def PlatformIsNotSupported(self, context, result)
def convert_xml_illegal_chars(val)
def ROOT6WorkAroundEnabled