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 r"^JobOptionsSvc INFO *$",
551 r"(Always|SUCCESS)\s*(Root f|[^ ]* F)ile version:",
552 r"0x[0-9a-fA-F#]+ *Algorithm::sysInitialize\(\) *\[",
553 r"0x[0-9a-fA-F#]* *__gxx_personality_v0 *\[",
554 r"File '.*.xml' does not exist",
555 r"INFO Refer to dataset .* by its file ID:",
556 r"INFO Referring to dataset .* by its file ID:",
557 r"INFO Disconnect from dataset",
558 r"INFO Disconnected from dataset",
559 r"INFO Disconnected data IO:",
560 r"IncidentSvc\s*(DEBUG (Adding|Removing)|VERBOSE Calling)",
562 r"^StatusCodeSvc.*listing all unchecked return codes:",
563 r"^StatusCodeSvc\s*INFO\s*$",
564 r"Num\s*\|\s*Function\s*\|\s*Source Library",
567 r"ERROR Failed to modify file: .* Errno=2 No such file or directory",
569 r"^ +[0-9]+ \|.*ROOT",
570 r"^ +[0-9]+ \|.*\|.*Dict",
572 r"StatusCodeSvc.*all StatusCode instances where checked",
576 r"SUCCESS\s*Booked \d+ Histogram\(s\)",
582 lineSkipper += LineSkipper(regexps = [
583 r'Warning in <TInterpreter::ReadRootmapFile>: .* is already in .*',
586 normalizeExamples = (lineSkipper + normalizeExamples + skipEmptyLines +
587 normalizeEOL + LineSorter(
"Services to release : "))
589 class ReferenceFileValidator:
590 def __init__(self, reffile, cause, result_key, preproc = normalizeExamples):
591 self.reffile = os.path.expandvars(reffile)
593 self.result_key = result_key
594 self.preproc = preproc
595 def __call__(self, stdout, result):
597 if os.path.isfile(self.reffile):
598 orig = open(self.reffile).xreadlines()
600 orig = self.preproc(orig)
604 new = stdout.splitlines()
606 new = self.preproc(new)
608 diffs = difflib.ndiff(orig,new,charjunk=difflib.IS_CHARACTER_JUNK)
609 filterdiffs =
map(
lambda x: x.strip(),filter(
lambda x: x[0] !=
" ",diffs))
612 result[self.result_key] = result.Quote(
"\n".join(filterdiffs))
613 result[self.result_key] += result.Quote(
"""
616 +) standard output of the test""")
617 causes.append(self.cause)
624 def findReferenceBlock(reference, stdout, result, causes, signature_offset=0, signature=None,
627 Given a block of text, tries to find it in the output.
628 The block had to be identified by a signature line. By default, the first
629 line is used as signature, or the line pointed to by signature_offset. If
630 signature_offset points outside the block, a signature line can be passed as
631 signature argument. Note: if 'signature' is None (the default), a negative
632 signature_offset is interpreted as index in a list (e.g. -1 means the last
633 line), otherwise the it is interpreted as the number of lines before the
634 first one of the block the signature must appear.
635 The parameter 'id' allow to distinguish between different calls to this
636 function in the same validation code.
639 reflines = filter(
None,
map(
lambda s: s.rstrip(), reference.splitlines()))
641 raise RuntimeError(
"Empty (or null) reference")
643 outlines = filter(
None,
map(
lambda s: s.rstrip(), stdout.splitlines()))
645 res_field =
"GaudiTest.RefBlock"
647 res_field +=
"_%s" % id
649 if signature
is None:
650 if signature_offset < 0:
651 signature_offset = len(reference)+signature_offset
652 signature = reflines[signature_offset]
655 pos = outlines.index(signature)
656 outlines = outlines[pos-signature_offset:pos+len(reflines)-signature_offset]
657 if reflines != outlines:
658 msg =
"standard output"
660 if not msg
in causes:
662 result[res_field +
".observed"] = result.Quote(
"\n".join(outlines))
664 causes.append(
"missing signature")
665 result[res_field +
".signature"] = result.Quote(signature)
666 if len(reflines) > 1
or signature != reflines[0]:
667 result[res_field +
".expected"] = result.Quote(
"\n".join(reflines))
673 Count the number of messages with required severity (by default ERROR and FATAL)
674 and check if their numbers match the expected ones (0 by default).
675 The dictionary "expected" can be used to tune the number of errors and fatals
676 allowed, or to limit the number of expected warnings etc.
678 stdout = kwargs[
"stdout"]
679 result = kwargs[
"result"]
680 causes = kwargs[
"causes"]
687 outlines = stdout.splitlines()
688 from math
import log10
689 fmt =
"%%%dd - %%s" % (int(log10(len(outlines))+1))
695 if len(words) >= 2
and words[1]
in errors:
696 errors[words[1]].append(fmt%(linecount,l.rstrip()))
699 if len(errors[e]) != expected[e]:
700 causes.append(
'%s(%d)'%(e,len(errors[e])))
701 result[
"GaudiTest.lines.%s"%e] = result.Quote(
'\n'.join(errors[e]))
702 result[
"GaudiTest.lines.%s.expected#"%e] = result.Quote(str(expected[e]))
709 Parse the TTree summary table in lines, starting from pos.
710 Returns a tuple with the dictionary with the digested informations and the
711 position of the first line after the summary.
717 splitcols =
lambda l: [ f.strip()
for f
in l.strip(
"*\n").split(
':',2) ]
720 cols = splitcols(ll[0])
721 r[
"Name"], r[
"Title"] = cols[1:]
723 cols = splitcols(ll[1])
724 r[
"Entries"] = int(cols[1])
726 sizes = cols[2].split()
727 r[
"Total size"] = int(sizes[2])
728 if sizes[-1] ==
"memory":
731 r[
"File size"] = int(sizes[-1])
733 cols = splitcols(ll[2])
734 sizes = cols[2].split()
735 if cols[0] ==
"Baskets":
736 r[
"Baskets"] = int(cols[1])
737 r[
"Basket size"] = int(sizes[2])
738 r[
"Compression"] = float(sizes[-1])
741 if i < (count - 3)
and lines[i].startswith(
"*Tree"):
742 result = parseblock(lines[i:i+3])
743 result[
"Branches"] = {}
745 while i < (count - 3)
and lines[i].startswith(
"*Br"):
746 if i < (count - 2)
and lines[i].startswith(
"*Branch "):
750 branch = parseblock(lines[i:i+3])
751 result[
"Branches"][branch[
"Name"]] = branch
758 Scan stdout to find ROOT TTree summaries and digest them.
760 stars = re.compile(
r"^\*+$")
761 outlines = stdout.splitlines()
762 nlines = len(outlines)
768 while i < nlines
and not stars.match(outlines[i]):
773 trees[tree[
"Name"]] = tree
779 Check that all the keys in reference are in to_check too, with the same value.
780 If the value is a dict, the function is called recursively. to_check can
781 contain more keys than reference, that will not be tested.
782 The function returns at the first difference found.
787 ignore_re = re.compile(ignore)
788 keys = [ key
for key
in reference
if not ignore_re.match(key) ]
790 keys = reference.keys()
794 if (
type(reference[k])
is dict)
and (
type(to_check[k])
is dict):
796 failed = fail_keys =
cmpTreesDicts(reference[k], to_check[k], ignore)
799 failed = to_check[k] != reference[k]
804 fail_keys.insert(0, k)
814 if c
is None or r
is None:
816 return (fail_path, r, c)
819 h_count_re = re.compile(
r"^(.*)SUCCESS\s+Booked (\d+) Histogram\(s\) :\s+(.*)")
823 Extract the histograms infos from the lines starting at pos.
824 Returns the position of the first line after the summary block.
827 h_table_head = re.compile(
r'SUCCESS\s+List of booked (1D|2D|3D|1D profile|2D profile) histograms in directory\s+"(\w*)"')
828 h_short_summ = re.compile(
r"ID=([^\"]+)\s+\"([^\"]+)\"\s+(.*)")
833 m = h_count_re.search(lines[pos])
834 name = m.group(1).strip()
835 total = int(m.group(2))
837 for k, v
in [ x.split(
"=")
for x
in m.group(3).split() ]:
840 header[
"Total"] = total
844 m = h_table_head.search(lines[pos])
847 t = t.replace(
" profile",
"Prof")
854 if l.startswith(
" | ID"):
856 titles = [ x.strip()
for x
in l.split(
"|")][1:]
858 while pos < nlines
and lines[pos].startswith(
" |"):
860 values = [ x.strip()
for x
in l.split(
"|")][1:]
862 for i
in range(len(titles)):
863 hcont[titles[i]] = values[i]
864 cont[hcont[
"ID"]] = hcont
866 elif l.startswith(
" ID="):
867 while pos < nlines
and lines[pos].startswith(
" ID="):
868 values = [ x.strip()
for x
in h_short_summ.search(lines[pos]).groups() ]
869 cont[values[0]] = values
872 raise RuntimeError(
"Cannot understand line %d: '%s'" % (pos, l))
876 summ[d][
"header"] = header
881 summ[name] = {
"header": header}
886 Scan stdout to find ROOT TTree summaries and digest them.
888 outlines = stdout.splitlines()
889 nlines = len(outlines) - 1
897 match = h_count_re.search(outlines[pos])
898 while pos < nlines
and not match:
900 match = h_count_re.search(outlines[pos])
903 summaries.update(summ)
906 class GaudiFilterExecutable(qm.executable.Filter):
907 def __init__(self, input, timeout = -1):
908 """Create a new 'Filter'.
910 'input' -- The string containing the input to provide to the
913 'timeout' -- As for 'TimeoutExecutable.__init__'."""
915 super(GaudiFilterExecutable, self).__init__(input, timeout)
917 self.__timeout = timeout
918 self.stack_trace_file =
None
922 tmpf = tempfile.mkstemp()
924 self.stack_trace_file = tmpf[1]
926 def __UseSeparateProcessGroupForChild(self):
927 """Copied from TimeoutExecutable to allow the re-implementation of
930 if sys.platform ==
"win32":
937 return self.__timeout >= 0
or self.__timeout == -2
940 def _HandleChild(self):
941 """Code copied from both FilterExecutable and TimeoutExecutable.
945 self._ClosePipeEnd(self._stdin_pipe[0])
946 if self._stdout_pipe:
947 self._ClosePipeEnd(self._stdout_pipe[1])
948 if self._stderr_pipe:
949 self._ClosePipeEnd(self._stderr_pipe[1])
957 super(qm.executable.TimeoutExecutable, self)._HandleChild()
959 if self.__UseSeparateProcessGroupForChild():
964 child_pid = self._GetChildPID()
966 os.setpgid(child_pid, child_pid)
982 self.__monitor_pid = os.fork()
983 if self.__monitor_pid != 0:
988 os.setpgid(self.__monitor_pid, child_pid)
994 os.setpgid(0, child_pid)
1003 max_fds = os.sysconf(
"SC_OPEN_MAX")
1006 for fd
in xrange(max_fds):
1012 if self.__timeout >= 0:
1014 time.sleep (self.__timeout)
1017 if sys.platform ==
"linux2":
1019 os.path.join(
"/proc", str(child_pid),
"exe"),
1021 "-batch",
"-n",
"-x",
1022 "'%s'" % os.path.join(os.path.dirname(__file__),
"stack-trace.gdb")]
1025 o = os.popen(
" ".join(cmd)).read()
1026 open(self.stack_trace_file,
"w").write(o)
1030 os.kill(0, signal.SIGKILL)
1033 select.select ([], [], [])
1038 elif self.__timeout >= 0
and sys.platform ==
"win32":
1040 self.__monitor_thread = Thread(target = self.__Monitor)
1041 self.__monitor_thread.start()
1043 if sys.platform ==
"win32":
1045 def __Monitor(self):
1046 """Code copied from FilterExecutable.
1047 Kill the child if the timeout expires.
1049 This function is run in the monitoring thread."""
1054 timeout = int(self.__timeout * 1000)
1057 result = win32event.WaitForSingleObject(self._GetChildPID(),
1060 if result == win32con.WAIT_TIMEOUT:
1066 class GaudiExeTest(ExecTestBase):
1067 """Standard Gaudi test.
1070 qm.fields.TextField(
1074 description=
"""The path to the program.
1076 This field indicates the path to the program. If it is not
1077 an absolute path, the value of the 'PATH' environment
1078 variable will be used to search for the program.
1079 If not specified, $GAUDIEXE or Gaudi.exe are used.
1082 qm.fields.SetField(qm.fields.TextField(
1084 title=
"Argument List",
1085 description=
"""The command-line arguments.
1087 If this field is left blank, the program is run without any
1090 Use this field to specify the option files.
1092 An implicit 0th argument (the path to the program) is added
1095 qm.fields.TextField(
1098 description=
"""Options to be passed to the application.
1100 This field allows to pass a list of options to the main program
1101 without the need of a separate option file.
1103 The content of the field is written to a temporary file which name
1104 is passed the the application as last argument (appended to the
1105 field "Argument List".
1111 qm.fields.TextField(
1113 title=
"Working Directory",
1114 description=
"""Path to the working directory.
1116 If this field is left blank, the program will be run from the qmtest
1117 directory, otherwise from the directory specified.""",
1120 qm.fields.TextField(
1122 title=
"Reference Output",
1123 description=
"""Path to the file containing the reference output.
1125 If this field is left blank, any standard output will be considered
1128 If the reference file is specified, any output on standard error is
1131 qm.fields.TextField(
1132 name=
"error_reference",
1133 title=
"Reference for standard error",
1134 description=
"""Path to the file containing the reference for the standard error.
1136 If this field is left blank, any standard output will be considered
1139 If the reference file is specified, any output on standard error is
1142 qm.fields.SetField(qm.fields.TextField(
1143 name =
"unsupported_platforms",
1144 title =
"Unsupported Platforms",
1145 description =
"""Platform on which the test must not be run.
1147 List of regular expressions identifying the platforms on which the
1148 test is not run and the result is set to UNTESTED."""
1151 qm.fields.TextField(
1153 title =
"Validator",
1154 description =
"""Function to validate the output of the test.
1156 If defined, the function is used to validate the products of the
1158 The function is called passing as arguments:
1159 self: the test class instance
1160 stdout: the standard output of the executed test
1161 stderr: the standard error of the executed test
1162 result: the Result objects to fill with messages
1163 The function must return a list of causes for the failure.
1164 If specified, overrides standard output, standard error and
1172 qm.fields.BooleanField(
1173 name =
"use_temp_dir",
1174 title =
"Use temporary directory",
1175 description =
"""Use temporary directory.
1177 If set to true, use a temporary directory as working directory.
1179 default_value=
"false"
1182 qm.fields.IntegerField(
1184 title =
"Expected signal",
1185 description =
"""Expect termination by signal.""",
1191 platform = self.GetPlatform()
1192 unsupported = [ re.compile(x)
1193 for x
in [ str(y).strip()
1194 for y
in self.unsupported_platforms ]
1197 for p_re
in unsupported:
1198 if p_re.search(platform):
1199 result.SetOutcome(result.UNTESTED)
1200 result[result.CAUSE] =
'Platform not supported.'
1206 Return the platform Id defined in CMTCONFIG or SCRAM_ARCH.
1210 if "CMTCONFIG" in os.environ:
1211 arch = os.environ[
"CMTCONFIG"]
1212 elif "SCRAM_ARCH" in os.environ:
1213 arch = os.environ[
"SCRAM_ARCH"]
1218 Return True if the current platform is Windows.
1220 This function was needed because of the change in the CMTCONFIG format,
1221 from win32_vc71_dbg to i686-winxp-vc9-dbg.
1223 platform = self.GetPlatform()
1224 return "winxp" in platform
or platform.startswith(
"win")
1226 def _expandReferenceFileName(self, reffile):
1232 platformSplit =
lambda p: set(p.split(
'-' in p
and '-' or '_'))
1234 reference = os.path.normpath(os.path.expandvars(reffile))
1236 spec_ref = reference[:-3] + self.GetPlatform()[0:3] + reference[-3:]
1237 if os.path.isfile(spec_ref):
1238 reference = spec_ref
1241 dirname, basename = os.path.split(reference)
1242 if not dirname: dirname =
'.'
1243 head = basename +
"."
1244 head_len = len(head)
1245 platform = platformSplit(self.GetPlatform())
1246 if 'do0' in platform:
1249 for f
in os.listdir(dirname):
1250 if f.startswith(head):
1251 req_plat = platformSplit(f[head_len:])
1252 if platform.issuperset(req_plat):
1253 candidates.append( (len(req_plat), f) )
1258 reference = os.path.join(dirname, candidates[-1][1])
1261 def CheckTTreesSummaries(self, stdout, result, causes,
1263 ignore =
r"Basket|.*size|Compression"):
1265 Compare the TTree summaries in stdout with the ones in trees_dict or in
1266 the reference file. By default ignore the size, compression and basket
1268 The presence of TTree summaries when none is expected is not a failure.
1270 if trees_dict
is None:
1271 reference = self._expandReferenceFileName(self.reference)
1273 if reference
and os.path.isfile(reference):
1278 from pprint
import PrettyPrinter
1279 pp = PrettyPrinter()
1281 result[
"GaudiTest.TTrees.expected"] = result.Quote(pp.pformat(trees_dict))
1283 result[
"GaudiTest.TTrees.ignore"] = result.Quote(ignore)
1288 causes.append(
"trees summaries")
1290 result[
"GaudiTest.TTrees.failure_on"] = result.Quote(msg)
1291 result[
"GaudiTest.TTrees.found"] = result.Quote(pp.pformat(trees))
1295 def CheckHistosSummaries(self, stdout, result, causes,
1299 Compare the TTree summaries in stdout with the ones in trees_dict or in
1300 the reference file. By default ignore the size, compression and basket
1302 The presence of TTree summaries when none is expected is not a failure.
1305 reference = self._expandReferenceFileName(self.reference)
1307 if reference
and os.path.isfile(reference):
1312 from pprint
import PrettyPrinter
1313 pp = PrettyPrinter()
1315 result[
"GaudiTest.Histos.expected"] = result.Quote(pp.pformat(dict))
1317 result[
"GaudiTest.Histos.ignore"] = result.Quote(ignore)
1322 causes.append(
"histos summaries")
1324 result[
"GaudiTest.Histos.failure_on"] = result.Quote(msg)
1325 result[
"GaudiTest.Histos.found"] = result.Quote(pp.pformat(histos))
1329 def ValidateWithReference(self, stdout, stderr, result, causes, preproc = None):
1331 Default validation action: compare standard output and error to the
1336 preproc = normalizeExamples
1338 reference = self._expandReferenceFileName(self.reference)
1340 if reference
and os.path.isfile(reference):
1341 result[
"GaudiTest.output_reference"] = reference
1342 causes += ReferenceFileValidator(reference,
1344 "GaudiTest.output_diff",
1345 preproc = preproc)(stdout, result)
1348 causes = self.CheckTTreesSummaries(stdout, result, causes)
1349 causes = self.CheckHistosSummaries(stdout, result, causes)
1353 newref = open(reference +
".new",
"w")
1355 for l
in stdout.splitlines():
1356 newref.write(l.rstrip() +
'\n')
1364 reference = self._expandReferenceFileName(self.error_reference)
1366 if reference
and os.path.isfile(reference):
1367 result[
"GaudiTest.error_reference"] = reference
1368 newcauses = ReferenceFileValidator(reference,
1370 "GaudiTest.error_diff",
1371 preproc = preproc)(stderr, result)
1374 newref = open(reference +
".new",
"w")
1376 for l
in stderr.splitlines():
1377 newref.write(l.rstrip() +
'\n')
1380 causes += BasicOutputValidator(self.stderr,
1382 "ExecTest.expected_stderr")(stderr, result)
1386 def ValidateOutput(self, stdout, stderr, result):
1389 if self.validator.strip() !=
"":
1390 class CallWrapper(object):
1392 Small wrapper class to dynamically bind some default arguments
1395 def __init__(self, callable, extra_args = {}):
1396 self.callable = callable
1397 self.extra_args = extra_args
1399 from inspect
import getargspec
1400 self.args_order = getargspec(callable)[0]
1403 if self.args_order[0] ==
"self":
1404 del self.args_order[0]
1405 def __call__(self, *args, **kwargs):
1407 positional = self.args_order[:len(args)]
1409 kwargs = dict(kwargs)
1410 for a
in self.extra_args:
1413 if a
not in positional
and a
not in kwargs:
1414 kwargs[a] = self.extra_args[a]
1415 return apply(self.callable, args, kwargs)
1417 exported_symbols = {
"self":self,
1422 "findReferenceBlock":
1423 CallWrapper(findReferenceBlock, {
"stdout":stdout,
1426 "validateWithReference":
1427 CallWrapper(self.ValidateWithReference, {
"stdout":stdout,
1432 CallWrapper(countErrorLines, {
"stdout":stdout,
1435 "checkTTreesSummaries":
1436 CallWrapper(self.CheckTTreesSummaries, {
"stdout":stdout,
1439 "checkHistosSummaries":
1440 CallWrapper(self.CheckHistosSummaries, {
"stdout":stdout,
1445 exec self.validator
in globals(), exported_symbols
1447 self.ValidateWithReference(stdout, stderr, result, causes)
1451 def DumpEnvironment(self, result):
1453 Add the content of the environment to the result object.
1455 Copied from the QMTest class of COOL.
1457 vars = os.environ.keys()
1459 result[
'GaudiTest.environment'] = \
1460 result.Quote(
'\n'.join([
"%s=%s"%(v,os.environ[v])
for v
in vars]))
1462 def Run(self, context, result):
1465 'context' -- A 'Context' giving run-time parameters to the
1468 'result' -- A 'Result' object. The outcome will be
1469 'Result.PASS' when this method is called. The 'result' may be
1470 modified by this method to indicate outcomes other than
1471 'Result.PASS' or to add annotations."""
1474 if self.PlatformIsNotSupported(context, result):
1480 elif "GAUDIEXE" in os.environ:
1481 prog = os.environ[
"GAUDIEXE"]
1486 dummy, prog_ext = os.path.splitext(prog)
1487 if prog_ext
not in [
".exe",
".py",
".bat" ]
and self.isWinPlatform():
1491 prog =
which(prog)
or prog
1494 args =
map(rationalizepath, self.args)
1501 if self.options.strip():
1503 if re.search(
r"from\s+Gaudi.Configuration\s+import\s+\*|from\s+Configurables\s+import", self.options):
1505 tmpfile = TempFile(ext)
1506 tmpfile.writelines(
"\n".join(self.options.splitlines()))
1508 args.append(tmpfile.name)
1509 result[
"GaudiTest.options"] = result.Quote(self.options)
1512 if prog_ext ==
".py":
1514 if self.isWinPlatform():
1515 prog =
which(
"python.exe")
or "python.exe"
1517 prog =
which(
"python")
or "python"
1520 origdir = os.getcwd()
1522 os.chdir(str(os.path.normpath(os.path.expandvars(self.workdir))))
1523 elif self.use_temp_dir ==
"true":
1524 if "QMTEST_TMPDIR" in os.environ:
1525 qmtest_tmpdir = os.environ[
"QMTEST_TMPDIR"]
1526 if not os.path.exists(qmtest_tmpdir):
1527 os.makedirs(qmtest_tmpdir)
1528 os.chdir(qmtest_tmpdir)
1529 elif "qmtest.tmpdir" in context:
1530 os.chdir(context[
"qmtest.tmpdir"])
1532 if "QMTEST_IGNORE_TIMEOUT" not in os.environ:
1533 self.timeout = max(self.timeout,600)
1539 self._CreateEclipseLaunch(prog, args, destdir = os.path.join(origdir,
'.eclipse'))
1541 self.RunProgram(prog,
1545 if result.GetOutcome()
not in [ result.PASS ]:
1546 self.DumpEnvironment(result)
1551 def RunProgram(self, program, arguments, context, result):
1552 """Run the 'program'.
1554 'program' -- The path to the program to run.
1556 'arguments' -- A list of the arguments to the program. This
1557 list must contain a first argument corresponding to 'argv[0]'.
1559 'context' -- A 'Context' giving run-time parameters to the
1562 'result' -- A 'Result' object. The outcome will be
1563 'Result.PASS' when this method is called. The 'result' may be
1564 modified by this method to indicate outcomes other than
1565 'Result.PASS' or to add annotations.
1567 @attention: This method has been copied from command.ExecTestBase
1568 (QMTest 2.3.0) and modified to keep stdout and stderr
1569 for tests that have been terminated by a signal.
1570 (Fundamental for debugging in the Application Area)
1574 environment = self.MakeEnvironment(context)
1576 if "slc6" in environment.get(
'CMTCONFIG',
''):
1577 environment[
'TERM'] =
'dumb'
1579 if self.timeout >= 0:
1580 timeout = self.timeout
1588 e = GaudiFilterExecutable(self.stdin, timeout)
1590 exit_status = e.Run(arguments, environment, path = program)
1592 if e.stack_trace_file
and os.path.exists(e.stack_trace_file):
1593 stack_trace = open(e.stack_trace_file).read()
1594 os.remove(e.stack_trace_file)
1598 result[
"ExecTest.stack_trace"] = result.Quote(stack_trace)
1601 if (sys.platform ==
"win32" or os.WIFEXITED(exit_status)
1602 or self.signal == os.WTERMSIG(exit_status)):
1607 if self.exit_code
is None:
1609 elif sys.platform ==
"win32":
1610 exit_code = exit_status
1612 exit_code = os.WEXITSTATUS(exit_status)
1617 result[
"ExecTest.exit_code"] = str(exit_code)
1618 result[
"ExecTest.stdout"] = result.Quote(stdout)
1619 result[
"ExecTest.stderr"] = result.Quote(stderr)
1621 if exit_code != self.exit_code:
1622 causes.append(
"exit_code")
1623 result[
"ExecTest.expected_exit_code"] \
1624 = str(self.exit_code)
1626 causes += self.ValidateOutput(stdout, stderr, result)
1629 result.Fail(
"Unexpected %s." % string.join(causes,
", "))
1630 elif os.WIFSIGNALED(exit_status):
1633 signal_number = str(os.WTERMSIG(exit_status))
1635 result.Fail(
"Program terminated by signal.")
1639 result.Fail(
"Exceeded time limit (%ds), terminated." % timeout)
1640 result[
"ExecTest.signal_number"] = signal_number
1641 result[
"ExecTest.stdout"] = result.Quote(e.stdout)
1642 result[
"ExecTest.stderr"] = result.Quote(e.stderr)
1644 result[
"ExecTest.expected_signal_number"] = str(self.signal)
1645 elif os.WIFSTOPPED(exit_status):
1648 signal_number = str(os.WSTOPSIG(exit_status))
1650 result.Fail(
"Program stopped by signal.")
1654 result.Fail(
"Exceeded time limit (%ds), stopped." % timeout)
1655 result[
"ExecTest.signal_number"] = signal_number
1656 result[
"ExecTest.stdout"] = result.Quote(e.stdout)
1657 result[
"ExecTest.stderr"] = result.Quote(e.stderr)
1661 result.Fail(
"Program did not terminate normally.")
1667 result[
"ExecTest.stdout"] = result[
"ExecTest.stdout"].replace(esc,repr_esc)
1671 def _CreateEclipseLaunch(self, prog, args, destdir = None):
1672 if 'NO_ECLIPSE_LAUNCHERS' in os.environ:
1677 projbasedir = os.path.normpath(destdir)
1678 while not os.path.exists(os.path.join(projbasedir,
".project")):
1679 oldprojdir = projbasedir
1680 projbasedir = os.path.normpath(os.path.join(projbasedir, os.pardir))
1683 if oldprojdir == projbasedir:
1687 if not os.path.exists(destdir):
1688 os.makedirs(destdir)
1690 from xml.etree
import ElementTree
as ET
1691 t = ET.parse(os.path.join(projbasedir,
".project"))
1692 projectName = t.find(
"name").text
1695 destfile =
"%s.launch" % self._Runnable__id
1697 destfile = os.path.join(destdir, destfile)
1699 if self.options.strip():
1703 tempfile = args.pop()
1704 optsfile = destfile + os.path.splitext(tempfile)[1]
1705 shutil.copyfile(tempfile, optsfile)
1706 args.append(optsfile)
1709 from xml.sax.saxutils
import quoteattr
1713 data[
"environment"] =
"\n".join([
'<mapEntry key=%s value=%s/>' % (quoteattr(k), quoteattr(v))
1714 for k, v
in os.environ.iteritems()
1715 if k
not in (
'MAKEOVERRIDES',
'MAKEFLAGS',
'MAKELEVEL')])
1717 data[
"exec"] =
which(prog)
or prog
1718 if os.path.basename(data[
"exec"]).lower().startswith(
"python"):
1719 data[
"stopAtMain"] =
"false"
1721 data[
"stopAtMain"] =
"true"
1723 data[
"args"] =
" ".join(
map(rationalizepath, args))
1724 if self.isWinPlatform():
1725 data[
"args"] =
" ".join([
"/debugexe"] +
map(rationalizepath, [data[
"exec"]] + args))
1726 data[
"exec"] =
which(
"vcexpress.exe")
1728 if not self.use_temp_dir:
1729 data[
"workdir"] = os.getcwd()
1733 data[
"workdir"] = destdir
1735 data[
"project"] = projectName.strip()
1738 xml_template =
u"""<?xml version="1.0" encoding="UTF-8" standalone="no"?>
1739 <launchConfiguration type="org.eclipse.cdt.launch.applicationLaunchType">
1740 <booleanAttribute key="org.eclipse.cdt.debug.mi.core.AUTO_SOLIB" value="true"/>
1741 <listAttribute key="org.eclipse.cdt.debug.mi.core.AUTO_SOLIB_LIST"/>
1742 <stringAttribute key="org.eclipse.cdt.debug.mi.core.DEBUG_NAME" value="gdb"/>
1743 <stringAttribute key="org.eclipse.cdt.debug.mi.core.GDB_INIT" value=".gdbinit"/>
1744 <listAttribute key="org.eclipse.cdt.debug.mi.core.SOLIB_PATH"/>
1745 <booleanAttribute key="org.eclipse.cdt.debug.mi.core.STOP_ON_SOLIB_EVENTS" value="false"/>
1746 <booleanAttribute key="org.eclipse.cdt.debug.mi.core.breakpointsFullPath" value="false"/>
1747 <stringAttribute key="org.eclipse.cdt.debug.mi.core.commandFactory" value="org.eclipse.cdt.debug.mi.core.standardCommandFactory"/>
1748 <stringAttribute key="org.eclipse.cdt.debug.mi.core.protocol" value="mi"/>
1749 <booleanAttribute key="org.eclipse.cdt.debug.mi.core.verboseMode" value="false"/>
1750 <intAttribute key="org.eclipse.cdt.launch.ATTR_BUILD_BEFORE_LAUNCH_ATTR" value="0"/>
1751 <stringAttribute key="org.eclipse.cdt.launch.COREFILE_PATH" value=""/>
1752 <stringAttribute key="org.eclipse.cdt.launch.DEBUGGER_ID" value="org.eclipse.cdt.debug.mi.core.CDebuggerNew"/>
1753 <stringAttribute key="org.eclipse.cdt.launch.DEBUGGER_REGISTER_GROUPS" value=""/>
1754 <stringAttribute key="org.eclipse.cdt.launch.DEBUGGER_START_MODE" value="run"/>
1755 <booleanAttribute key="org.eclipse.cdt.launch.DEBUGGER_STOP_AT_MAIN" value="%(stopAtMain)s"/>
1756 <stringAttribute key="org.eclipse.cdt.launch.DEBUGGER_STOP_AT_MAIN_SYMBOL" value="main"/>
1757 <booleanAttribute key="org.eclipse.cdt.launch.ENABLE_REGISTER_BOOKKEEPING" value="false"/>
1758 <booleanAttribute key="org.eclipse.cdt.launch.ENABLE_VARIABLE_BOOKKEEPING" value="false"/>
1759 <stringAttribute key="org.eclipse.cdt.launch.FORMAT" value="<?xml version="1.0" encoding="UTF-8" standalone="no"?><contentList/>"/>
1760 <stringAttribute key="org.eclipse.cdt.launch.GLOBAL_VARIABLES" value="<?xml version="1.0" encoding="UTF-8" standalone="no"?> <globalVariableList/> "/>
1761 <stringAttribute key="org.eclipse.cdt.launch.MEMORY_BLOCKS" value="<?xml version="1.0" encoding="UTF-8" standalone="no"?> <memoryBlockExpressionList/> "/>
1762 <stringAttribute key="org.eclipse.cdt.launch.PROGRAM_ARGUMENTS" value="%(args)s"/>
1763 <stringAttribute key="org.eclipse.cdt.launch.PROGRAM_NAME" value="%(exec)s"/>
1764 <stringAttribute key="org.eclipse.cdt.launch.PROJECT_ATTR" value="%(project)s"/>
1765 <stringAttribute key="org.eclipse.cdt.launch.PROJECT_BUILD_CONFIG_ID_ATTR" value=""/>
1766 <stringAttribute key="org.eclipse.cdt.launch.WORKING_DIRECTORY" value="%(workdir)s"/>
1767 <booleanAttribute key="org.eclipse.cdt.launch.ui.ApplicationCDebuggerTab.DEFAULTS_SET" value="true"/>
1768 <booleanAttribute key="org.eclipse.cdt.launch.use_terminal" value="true"/>
1769 <listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_PATHS">
1770 <listEntry value="/%(project)s"/>
1772 <listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_TYPES">
1773 <listEntry value="4"/>
1775 <booleanAttribute key="org.eclipse.debug.core.appendEnvironmentVariables" value="false"/>
1776 <mapAttribute key="org.eclipse.debug.core.environmentVariables">
1779 <mapAttribute key="org.eclipse.debug.core.preferred_launchers">
1780 <mapEntry key="[debug]" value="org.eclipse.cdt.cdi.launch.localCLaunch"/>
1782 <listAttribute key="org.eclipse.debug.ui.favoriteGroups">
1783 <listEntry value="org.eclipse.debug.ui.launchGroup.debug"/>
1785 </launchConfiguration>
1790 data[k] = codecs.decode(data[k],
'utf-8')
1791 xml = xml_template % data
1794 codecs.open(destfile,
"w", encoding=
'utf-8').write(xml)
1796 print 'WARNING: problem generating Eclipse launcher'
1803 import simplejson
as json
1805 class HTMLResultStream(ResultStream):
1806 """An 'HTMLResultStream' writes its output to a set of HTML files.
1808 The argument 'dir' is used to select the destination directory for the HTML
1810 The destination directory may already contain the report from a previous run
1811 (for example of a different package), in which case it will be extended to
1812 include the new data.
1815 qm.fields.TextField(
1817 title =
"Destination Directory",
1818 description =
"""The name of the directory.
1820 All results will be written to the directory indicated.""",
1822 default_value =
""),
1825 def __init__(self, arguments = None, **args):
1826 """Prepare the destination directory.
1828 Creates the destination directory and store in it some preliminary
1829 annotations and the static files found in the template directory
1832 ResultStream.__init__(self, arguments, **args)
1834 self._summaryFile = os.path.join(self.dir,
"summary.json")
1835 self._annotationsFile = os.path.join(self.dir,
"annotations.json")
1837 templateDir = os.path.join(os.path.dirname(__file__),
"html_report")
1838 if not os.path.isdir(self.dir):
1839 os.makedirs(self.dir)
1841 for f
in os.listdir(templateDir):
1842 src = os.path.join(templateDir, f)
1843 dst = os.path.join(self.dir, f)
1844 if not os.path.isdir(src)
and not os.path.exists(dst):
1845 shutil.copy(src, dst)
1847 if "CMTCONFIG" in os.environ:
1848 self.WriteAnnotation(
"cmt.cmtconfig", os.environ[
"CMTCONFIG"])
1850 self.WriteAnnotation(
"hostname", socket.gethostname())
1852 def _updateSummary(self):
1853 """Helper function to extend the global summary file in the destination
1856 if os.path.exists(self._summaryFile):
1857 oldSummary = json.load(open(self._summaryFile))
1860 ids = set([ i[
"id"]
for i
in self._summary ])
1861 newSummary = [ i
for i
in oldSummary
if i[
"id"]
not in ids ]
1862 newSummary.extend(self._summary)
1863 json.dump(newSummary, open(self._summaryFile,
"w"),
1866 def WriteAnnotation(self, key, value):
1867 """Writes the annotation to the annotation file.
1868 If the key is already present with a different value, the value becomes
1869 a list and the new value is appended to it, except for start_time and
1873 if os.path.exists(self._annotationsFile):
1874 annotations = json.load(open(self._annotationsFile))
1878 key, value =
map(str, [key, value])
1879 if key ==
"qmtest.run.start_time":
1884 if key
not in annotations:
1885 annotations[key] = value
1886 if "qmtest.run.end_time" in annotations:
1887 del annotations[
"qmtest.run.end_time"]
1890 if key
in annotations:
1891 old = annotations[key]
1892 if type(old)
is list:
1893 if value
not in old:
1894 annotations[key].append(value)
1896 annotations[key] = [old, value]
1898 annotations[key] = value
1900 json.dump(annotations, open(self._annotationsFile,
"w"),
1903 def WriteResult(self, result):
1904 """Prepare the test result directory in the destination directory storing
1905 into it the result fields.
1906 A summary of the test result is stored both in a file in the test directory
1907 and in the global summary file.
1910 summary[
"id"] = result.GetId()
1911 summary[
"outcome"] = result.GetOutcome()
1912 summary[
"cause"] = result.GetCause()
1913 summary[
"fields"] = result.keys()
1914 summary[
"fields"].sort()
1917 for f
in [
"id",
"outcome",
"cause"]:
1918 summary[f] = str(summary[f])
1919 summary[
"fields"] =
map(str, summary[
"fields"])
1921 self._summary.append(summary)
1927 testOutDir = os.path.join(self.dir, summary[
"id"])
1928 if not os.path.isdir(testOutDir):
1929 os.makedirs(testOutDir)
1930 json.dump(summary, open(os.path.join(testOutDir,
"summary.json"),
"w"),
1932 for f
in summary[
"fields"]:
1933 open(os.path.join(testOutDir, f),
"w").write(result[f])
1935 self._updateSummary()
1937 def Summarize(self):
1944 class XMLResultStream(ResultStream):
1945 """An 'XMLResultStream' writes its output to a Ctest XML file.
1947 The argument 'dir' is used to select the destination file for the XML
1949 The destination directory may already contain the report from a previous run
1950 (for example of a different package), in which case it will be overrided to
1954 qm.fields.TextField(
1956 title =
"Destination Directory",
1957 description =
"""The name of the directory.
1959 All results will be written to the directory indicated.""",
1961 default_value =
""),
1962 qm.fields.TextField(
1964 title =
"Output File Prefix",
1965 description =
"""The output file name will be the specified prefix
1966 followed by 'Test.xml' (CTest convention).""",
1968 default_value =
""),
1971 def __init__(self, arguments = None, **args):
1972 """Prepare the destination directory.
1974 Creates the destination directory and store in it some preliminary
1977 ResultStream.__init__(self, arguments, **args)
1979 self._xmlFile = os.path.join(self.dir, self.prefix +
'Test.xml')
1982 self._startTime =
None
1983 self._endTime =
None
1985 if not os.path.isfile(self._xmlFile):
1987 if not os.path.exists(os.path.dirname(self._xmlFile)):
1988 os.makedirs(os.path.dirname(self._xmlFile))
1990 newdataset = ET.Element(
"newdataset")
1991 self._tree = ET.ElementTree(newdataset)
1992 self._tree.write(self._xmlFile)
1995 self._tree = ET.parse(self._xmlFile)
1996 newdataset = self._tree.getroot()
2003 for site
in newdataset.getiterator() :
2004 if site.get(
"OSPlatform") == os.uname()[4]:
2014 import multiprocessing
2016 "BuildName" : os.getenv(
"CMTCONFIG"),
2017 "Name" : os.uname()[1] ,
2018 "Generator" :
"QMTest "+qm.version ,
2019 "OSName" : os.uname()[0] ,
2020 "Hostname" : socket.gethostname() ,
2021 "OSRelease" : os.uname()[2] ,
2022 "OSVersion" :os.uname()[3] ,
2023 "OSPlatform" :os.uname()[4] ,
2024 "Is64Bits" :
"unknown" ,
2025 "VendorString" :
"unknown" ,
2026 "VendorID" :
"unknown" ,
2027 "FamilyID" :
"unknown" ,
2028 "ModelID" :
"unknown" ,
2029 "ProcessorCacheSize" :
"unknown" ,
2030 "NumberOfLogicalCPU" : str(multiprocessing.cpu_count()) ,
2031 "NumberOfPhysicalCPU" :
"0" ,
2032 "TotalVirtualMemory" :
"0" ,
2033 "TotalPhysicalMemory" :
"0" ,
2034 "LogicalProcessorsPerPhysical" :
"0" ,
2035 "ProcessorClockFrequency" :
"0" ,
2037 self._site = ET.SubElement(newdataset,
"Site", attrib)
2038 self._Testing = ET.SubElement(self._site,
"Testing")
2041 self._StartDateTime = ET.SubElement(self._Testing,
"StartDateTime")
2043 self._StartTestTime = ET.SubElement(self._Testing,
"StartTestTime")
2046 self._TestList = ET.SubElement(self._Testing,
"TestList")
2049 self._EndDateTime = ET.SubElement(self._Testing,
"EndDateTime")
2052 self._EndTestTime = ET.SubElement(self._Testing,
"EndTestTime")
2056 self._ElapsedMinutes = ET.SubElement(self._Testing,
"ElapsedMinutes")
2060 self._Testing = self._site.find(
"Testing")
2061 self._StartDateTime = self._Testing.find(
"StartDateTime")
2062 self._StartTestTime = self._Testing.find(
"StartTestTime")
2063 self._TestList = self._Testing.find(
"TestList")
2064 self._EndDateTime = self._Testing.find(
"EndDateTime")
2065 self._EndTestTime = self._Testing.find(
"EndTestTime")
2066 self._ElapsedMinutes = self._Testing.find(
"ElapsedMinutes")
2069 # Add some non-QMTest attributes
2070 if "CMTCONFIG" in os.environ:
2071 self.WriteAnnotation("cmt.cmtconfig", os.environ["CMTCONFIG"])
2073 self.WriteAnnotation("hostname", socket.gethostname())
2077 def WriteAnnotation(self, key, value):
2078 if key ==
"qmtest.run.start_time":
2079 if self._site.get(
"qmtest.run.start_time")
is not None :
2081 self._site.set(str(key),str(value))
2082 def WriteResult(self, result):
2083 """Prepare the test result directory in the destination directory storing
2084 into it the result fields.
2085 A summary of the test result is stored both in a file in the test directory
2086 and in the global summary file.
2089 summary[
"id"] = result.GetId()
2090 summary[
"outcome"] = result.GetOutcome()
2091 summary[
"cause"] = result.GetCause()
2092 summary[
"fields"] = result.keys()
2093 summary[
"fields"].sort()
2097 for f
in [
"id",
"outcome",
"cause"]:
2098 summary[f] = str(summary[f])
2099 summary[
"fields"] =
map(str, summary[
"fields"])
2105 if "qmtest.start_time" in summary[
"fields"]:
2106 haveStartDate =
True
2108 haveStartDate =
False
2109 if "qmtest.end_time" in summary[
"fields"]:
2116 self._startTime = calendar.timegm(time.strptime(result[
"qmtest.start_time"],
"%Y-%m-%dT%H:%M:%SZ"))
2117 if self._StartTestTime.text
is None:
2118 self._StartDateTime.text = time.strftime(
"%b %d %H:%M %Z", time.localtime(self._startTime))
2119 self._StartTestTime.text = str(self._startTime)
2120 self._site.set(
"BuildStamp" , result[
"qmtest.start_time"] )
2124 self._endTime = calendar.timegm(time.strptime(result[
"qmtest.end_time"],
"%Y-%m-%dT%H:%M:%SZ"))
2128 tl = ET.Element(
"Test")
2129 tl.text = summary[
"id"]
2130 self._TestList.insert(0,tl)
2133 Test = ET.Element(
"Test")
2134 if summary[
"outcome"] ==
"PASS":
2135 Test.set(
"Status",
"passed")
2136 elif summary[
"outcome"] ==
"FAIL":
2137 Test.set(
"Status",
"failed")
2138 elif summary[
"outcome"] ==
"SKIPPED" or summary[
"outcome"] ==
"UNTESTED":
2139 Test.set(
"Status",
"skipped")
2140 elif summary[
"outcome"] ==
"ERROR":
2141 Test.set(
"Status",
"failed")
2142 Name = ET.SubElement(Test,
"Name",)
2143 Name.text = summary[
"id"]
2144 Results = ET.SubElement(Test,
"Results")
2147 self._Testing.insert(3,Test)
2149 if haveStartDate
and haveEndDate:
2151 delta = self._endTime - self._startTime
2152 testduration = str(delta)
2153 Testduration= ET.SubElement(Results,
"NamedMeasurement")
2154 Testduration.set(
"name",
"Execution Time")
2155 Testduration.set(
"type",
"numeric/float" )
2156 value = ET.SubElement(Testduration,
"Value")
2157 value.text = testduration
2160 for n
in (
"qmtest.end_time",
"qmtest.start_time",
"qmtest.cause",
"ExecTest.stdout"):
2161 if n
in summary[
"fields"]:
2162 summary[
"fields"].
remove(n)
2166 if "ExecTest.exit_code" in summary[
"fields"] :
2167 summary[
"fields"].
remove(
"ExecTest.exit_code")
2168 ExitCode= ET.SubElement(Results,
"NamedMeasurement")
2169 ExitCode.set(
"name",
"exit_code")
2170 ExitCode.set(
"type",
"numeric/integer" )
2171 value = ET.SubElement(ExitCode,
"Value")
2174 TestStartTime= ET.SubElement(Results,
"NamedMeasurement")
2175 TestStartTime.set(
"name",
"Start_Time")
2176 TestStartTime.set(
"type",
"String" )
2177 value = ET.SubElement(TestStartTime,
"Value")
2183 TestEndTime= ET.SubElement(Results,
"NamedMeasurement")
2184 TestEndTime.set(
"name",
"End_Time")
2185 TestEndTime.set(
"type",
"String" )
2186 value = ET.SubElement(TestEndTime,
"Value")
2192 if summary[
"cause"]:
2193 FailureCause= ET.SubElement(Results,
"NamedMeasurement")
2194 FailureCause.set(
"name",
"Cause")
2195 FailureCause.set(
"type",
"String" )
2196 value = ET.SubElement(FailureCause,
"Value")
2201 for field
in summary[
"fields"] :
2202 fields[field] = ET.SubElement(Results,
"NamedMeasurement")
2203 fields[field].set(
"type",
"String")
2204 fields[field].set(
"name",field)
2205 value = ET.SubElement(fields[field],
"Value")
2207 if "<pre>" in result[field][0:6] :
2213 if result.has_key(
"ExecTest.stdout" ) :
2214 Measurement = ET.SubElement(Results,
"Measurement")
2215 value = ET.SubElement(Measurement,
"Value")
2216 if "<pre>" in result[
"ExecTest.stdout"][0:6] :
2223 self._tree.write(self._xmlFile,
"utf-8")
2226 def Summarize(self):
2229 self._EndTestTime.text = str(self._endTime)
2230 self._EndDateTime.text = time.strftime(
"%b %d %H:%M %Z", time.localtime(self._endTime))
2233 if self._endTime
and self._startTime:
2234 delta = self._endTime - self._startTime
2237 self._ElapsedMinutes.text = str(delta/60)
2240 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.
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)
NamedRange_< CONTAINER > range(const CONTAINER &cnt, const std::string &name)
simple function to create the named range form arbitrary container
def ROOT6WorkAroundEnabled