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 normalizeExamples += RegexpReplacer(o,r,w)
528 lineSkipper = LineSkipper([
"//GP:",
529 "JobOptionsSvc INFO # ",
530 "JobOptionsSvc WARNING # ",
533 "This machine has a speed",
536 "ToolSvc.Sequenc... INFO",
537 "DataListenerSvc INFO XML written to file:",
538 "[INFO]",
"[WARNING]",
539 "DEBUG No writable file catalog found which contains FID:",
541 "DEBUG Service base class initialized successfully",
542 "DEBUG Incident timing:",
543 "INFO 'CnvServices':[",
547 r"^JobOptionsSvc INFO *$",
549 r"(Always|SUCCESS)\s*(Root f|[^ ]* F)ile version:",
550 r"0x[0-9a-fA-F#]+ *Algorithm::sysInitialize\(\) *\[",
551 r"0x[0-9a-fA-F#]* *__gxx_personality_v0 *\[",
552 r"File '.*.xml' does not exist",
553 r"INFO Refer to dataset .* by its file ID:",
554 r"INFO Referring to dataset .* by its file ID:",
555 r"INFO Disconnect from dataset",
556 r"INFO Disconnected from dataset",
557 r"INFO Disconnected data IO:",
558 r"IncidentSvc\s*(DEBUG (Adding|Removing)|VERBOSE Calling)",
560 r"^StatusCodeSvc.*listing all unchecked return codes:",
561 r"^StatusCodeSvc\s*INFO\s*$",
562 r"Num\s*\|\s*Function\s*\|\s*Source Library",
565 r"ERROR Failed to modify file: .* Errno=2 No such file or directory",
567 r"^ +[0-9]+ \|.*ROOT",
568 r"^ +[0-9]+ \|.*\|.*Dict",
570 r"StatusCodeSvc.*all StatusCode instances where checked",
574 r"SUCCESS\s*Booked \d+ Histogram\(s\)",
580 lineSkipper += LineSkipper(regexps = [
581 r'Warning in <TInterpreter::ReadRootmapFile>: .* is already in .*',
584 normalizeExamples = (lineSkipper + normalizeExamples + skipEmptyLines +
585 normalizeEOL + LineSorter(
"Services to release : "))
587 class ReferenceFileValidator:
588 def __init__(self, reffile, cause, result_key, preproc = normalizeExamples):
589 self.reffile = os.path.expandvars(reffile)
591 self.result_key = result_key
592 self.preproc = preproc
593 def __call__(self, stdout, result):
595 if os.path.isfile(self.reffile):
596 orig = open(self.reffile).xreadlines()
598 orig = self.preproc(orig)
602 new = stdout.splitlines()
604 new = self.preproc(new)
606 diffs = difflib.ndiff(orig,new,charjunk=difflib.IS_CHARACTER_JUNK)
607 filterdiffs =
map(
lambda x: x.strip(),filter(
lambda x: x[0] !=
" ",diffs))
610 result[self.result_key] = result.Quote(
"\n".join(filterdiffs))
611 result[self.result_key] += result.Quote(
"""
614 +) standard output of the test""")
615 causes.append(self.cause)
622 def findReferenceBlock(reference, stdout, result, causes, signature_offset=0, signature=None,
625 Given a block of text, tries to find it in the output.
626 The block had to be identified by a signature line. By default, the first
627 line is used as signature, or the line pointed to by signature_offset. If
628 signature_offset points outside the block, a signature line can be passed as
629 signature argument. Note: if 'signature' is None (the default), a negative
630 signature_offset is interpreted as index in a list (e.g. -1 means the last
631 line), otherwise the it is interpreted as the number of lines before the
632 first one of the block the signature must appear.
633 The parameter 'id' allow to distinguish between different calls to this
634 function in the same validation code.
637 reflines = filter(
None,
map(
lambda s: s.rstrip(), reference.splitlines()))
639 raise RuntimeError(
"Empty (or null) reference")
641 outlines = filter(
None,
map(
lambda s: s.rstrip(), stdout.splitlines()))
643 res_field =
"GaudiTest.RefBlock"
645 res_field +=
"_%s" % id
647 if signature
is None:
648 if signature_offset < 0:
649 signature_offset = len(reference)+signature_offset
650 signature = reflines[signature_offset]
653 pos = outlines.index(signature)
654 outlines = outlines[pos-signature_offset:pos+len(reflines)-signature_offset]
655 if reflines != outlines:
656 msg =
"standard output"
658 if not msg
in causes:
660 result[res_field +
".observed"] = result.Quote(
"\n".join(outlines))
662 causes.append(
"missing signature")
663 result[res_field +
".signature"] = result.Quote(signature)
664 if len(reflines) > 1
or signature != reflines[0]:
665 result[res_field +
".expected"] = result.Quote(
"\n".join(reflines))
671 Count the number of messages with required severity (by default ERROR and FATAL)
672 and check if their numbers match the expected ones (0 by default).
673 The dictionary "expected" can be used to tune the number of errors and fatals
674 allowed, or to limit the number of expected warnings etc.
676 stdout = kwargs[
"stdout"]
677 result = kwargs[
"result"]
678 causes = kwargs[
"causes"]
685 outlines = stdout.splitlines()
686 from math
import log10
687 fmt =
"%%%dd - %%s" % (int(log10(len(outlines))+1))
693 if len(words) >= 2
and words[1]
in errors:
694 errors[words[1]].append(fmt%(linecount,l.rstrip()))
697 if len(errors[e]) != expected[e]:
698 causes.append(
'%s(%d)'%(e,len(errors[e])))
699 result[
"GaudiTest.lines.%s"%e] = result.Quote(
'\n'.join(errors[e]))
700 result[
"GaudiTest.lines.%s.expected#"%e] = result.Quote(str(expected[e]))
707 Parse the TTree summary table in lines, starting from pos.
708 Returns a tuple with the dictionary with the digested informations and the
709 position of the first line after the summary.
715 splitcols =
lambda l: [ f.strip()
for f
in l.strip(
"*\n").split(
':',2) ]
718 cols = splitcols(ll[0])
719 r[
"Name"], r[
"Title"] = cols[1:]
721 cols = splitcols(ll[1])
722 r[
"Entries"] = int(cols[1])
724 sizes = cols[2].split()
725 r[
"Total size"] = int(sizes[2])
726 if sizes[-1] ==
"memory":
729 r[
"File size"] = int(sizes[-1])
731 cols = splitcols(ll[2])
732 sizes = cols[2].split()
733 if cols[0] ==
"Baskets":
734 r[
"Baskets"] = int(cols[1])
735 r[
"Basket size"] = int(sizes[2])
736 r[
"Compression"] = float(sizes[-1])
739 if i < (count - 3)
and lines[i].startswith(
"*Tree"):
740 result = parseblock(lines[i:i+3])
741 result[
"Branches"] = {}
743 while i < (count - 3)
and lines[i].startswith(
"*Br"):
744 if i < (count - 2)
and lines[i].startswith(
"*Branch "):
748 branch = parseblock(lines[i:i+3])
749 result[
"Branches"][branch[
"Name"]] = branch
756 Scan stdout to find ROOT TTree summaries and digest them.
758 stars = re.compile(
r"^\*+$")
759 outlines = stdout.splitlines()
760 nlines = len(outlines)
766 while i < nlines
and not stars.match(outlines[i]):
771 trees[tree[
"Name"]] = tree
777 Check that all the keys in reference are in to_check too, with the same value.
778 If the value is a dict, the function is called recursively. to_check can
779 contain more keys than reference, that will not be tested.
780 The function returns at the first difference found.
785 ignore_re = re.compile(ignore)
786 keys = [ key
for key
in reference
if not ignore_re.match(key) ]
788 keys = reference.keys()
792 if (
type(reference[k])
is dict)
and (
type(to_check[k])
is dict):
794 failed = fail_keys =
cmpTreesDicts(reference[k], to_check[k], ignore)
797 failed = to_check[k] != reference[k]
802 fail_keys.insert(0, k)
812 if c
is None or r
is None:
814 return (fail_path, r, c)
817 h_count_re = re.compile(
r"^(.*)SUCCESS\s+Booked (\d+) Histogram\(s\) :\s+(.*)")
821 Extract the histograms infos from the lines starting at pos.
822 Returns the position of the first line after the summary block.
825 h_table_head = re.compile(
r'SUCCESS\s+List of booked (1D|2D|3D|1D profile|2D profile) histograms in directory\s+"(\w*)"')
826 h_short_summ = re.compile(
r"ID=([^\"]+)\s+\"([^\"]+)\"\s+(.*)")
831 m = h_count_re.search(lines[pos])
832 name = m.group(1).strip()
833 total = int(m.group(2))
835 for k, v
in [ x.split(
"=")
for x
in m.group(3).split() ]:
838 header[
"Total"] = total
842 m = h_table_head.search(lines[pos])
845 t = t.replace(
" profile",
"Prof")
852 if l.startswith(
" | ID"):
854 titles = [ x.strip()
for x
in l.split(
"|")][1:]
856 while pos < nlines
and lines[pos].startswith(
" |"):
858 values = [ x.strip()
for x
in l.split(
"|")][1:]
860 for i
in range(len(titles)):
861 hcont[titles[i]] = values[i]
862 cont[hcont[
"ID"]] = hcont
864 elif l.startswith(
" ID="):
865 while pos < nlines
and lines[pos].startswith(
" ID="):
866 values = [ x.strip()
for x
in h_short_summ.search(lines[pos]).groups() ]
867 cont[values[0]] = values
870 raise RuntimeError(
"Cannot understand line %d: '%s'" % (pos, l))
874 summ[d][
"header"] = header
879 summ[name] = {
"header": header}
884 Scan stdout to find ROOT TTree summaries and digest them.
886 outlines = stdout.splitlines()
887 nlines = len(outlines) - 1
895 match = h_count_re.search(outlines[pos])
896 while pos < nlines
and not match:
898 match = h_count_re.search(outlines[pos])
901 summaries.update(summ)
904 class GaudiFilterExecutable(qm.executable.Filter):
905 def __init__(self, input, timeout = -1):
906 """Create a new 'Filter'.
908 'input' -- The string containing the input to provide to the
911 'timeout' -- As for 'TimeoutExecutable.__init__'."""
913 super(GaudiFilterExecutable, self).__init__(input, timeout)
915 self.__timeout = timeout
916 self.stack_trace_file =
None
920 tmpf = tempfile.mkstemp()
922 self.stack_trace_file = tmpf[1]
924 def __UseSeparateProcessGroupForChild(self):
925 """Copied from TimeoutExecutable to allow the re-implementation of
928 if sys.platform ==
"win32":
935 return self.__timeout >= 0
or self.__timeout == -2
938 def _HandleChild(self):
939 """Code copied from both FilterExecutable and TimeoutExecutable.
943 self._ClosePipeEnd(self._stdin_pipe[0])
944 if self._stdout_pipe:
945 self._ClosePipeEnd(self._stdout_pipe[1])
946 if self._stderr_pipe:
947 self._ClosePipeEnd(self._stderr_pipe[1])
955 super(qm.executable.TimeoutExecutable, self)._HandleChild()
957 if self.__UseSeparateProcessGroupForChild():
962 child_pid = self._GetChildPID()
964 os.setpgid(child_pid, child_pid)
980 self.__monitor_pid = os.fork()
981 if self.__monitor_pid != 0:
986 os.setpgid(self.__monitor_pid, child_pid)
992 os.setpgid(0, child_pid)
1001 max_fds = os.sysconf(
"SC_OPEN_MAX")
1004 for fd
in xrange(max_fds):
1010 if self.__timeout >= 0:
1012 time.sleep (self.__timeout)
1015 if sys.platform ==
"linux2":
1017 os.path.join(
"/proc", str(child_pid),
"exe"),
1019 "-batch",
"-n",
"-x",
1020 "'%s'" % os.path.join(os.path.dirname(__file__),
"stack-trace.gdb")]
1023 o = os.popen(
" ".join(cmd)).read()
1024 open(self.stack_trace_file,
"w").write(o)
1028 os.kill(0, signal.SIGKILL)
1031 select.select ([], [], [])
1036 elif self.__timeout >= 0
and sys.platform ==
"win32":
1038 self.__monitor_thread = Thread(target = self.__Monitor)
1039 self.__monitor_thread.start()
1041 if sys.platform ==
"win32":
1043 def __Monitor(self):
1044 """Code copied from FilterExecutable.
1045 Kill the child if the timeout expires.
1047 This function is run in the monitoring thread."""
1052 timeout = int(self.__timeout * 1000)
1055 result = win32event.WaitForSingleObject(self._GetChildPID(),
1058 if result == win32con.WAIT_TIMEOUT:
1064 class GaudiExeTest(ExecTestBase):
1065 """Standard Gaudi test.
1068 qm.fields.TextField(
1072 description=
"""The path to the program.
1074 This field indicates the path to the program. If it is not
1075 an absolute path, the value of the 'PATH' environment
1076 variable will be used to search for the program.
1077 If not specified, $GAUDIEXE or Gaudi.exe are used.
1080 qm.fields.SetField(qm.fields.TextField(
1082 title=
"Argument List",
1083 description=
"""The command-line arguments.
1085 If this field is left blank, the program is run without any
1088 Use this field to specify the option files.
1090 An implicit 0th argument (the path to the program) is added
1093 qm.fields.TextField(
1096 description=
"""Options to be passed to the application.
1098 This field allows to pass a list of options to the main program
1099 without the need of a separate option file.
1101 The content of the field is written to a temporary file which name
1102 is passed the the application as last argument (appended to the
1103 field "Argument List".
1109 qm.fields.TextField(
1111 title=
"Working Directory",
1112 description=
"""Path to the working directory.
1114 If this field is left blank, the program will be run from the qmtest
1115 directory, otherwise from the directory specified.""",
1118 qm.fields.TextField(
1120 title=
"Reference Output",
1121 description=
"""Path to the file containing the reference output.
1123 If this field is left blank, any standard output will be considered
1126 If the reference file is specified, any output on standard error is
1129 qm.fields.TextField(
1130 name=
"error_reference",
1131 title=
"Reference for standard error",
1132 description=
"""Path to the file containing the reference for the standard error.
1134 If this field is left blank, any standard output will be considered
1137 If the reference file is specified, any output on standard error is
1140 qm.fields.SetField(qm.fields.TextField(
1141 name =
"unsupported_platforms",
1142 title =
"Unsupported Platforms",
1143 description =
"""Platform on which the test must not be run.
1145 List of regular expressions identifying the platforms on which the
1146 test is not run and the result is set to UNTESTED."""
1149 qm.fields.TextField(
1151 title =
"Validator",
1152 description =
"""Function to validate the output of the test.
1154 If defined, the function is used to validate the products of the
1156 The function is called passing as arguments:
1157 self: the test class instance
1158 stdout: the standard output of the executed test
1159 stderr: the standard error of the executed test
1160 result: the Result objects to fill with messages
1161 The function must return a list of causes for the failure.
1162 If specified, overrides standard output, standard error and
1170 qm.fields.BooleanField(
1171 name =
"use_temp_dir",
1172 title =
"Use temporary directory",
1173 description =
"""Use temporary directory.
1175 If set to true, use a temporary directory as working directory.
1177 default_value=
"false"
1180 qm.fields.IntegerField(
1182 title =
"Expected signal",
1183 description =
"""Expect termination by signal.""",
1189 platform = self.GetPlatform()
1190 unsupported = [ re.compile(x)
1191 for x
in [ str(y).strip()
1192 for y
in self.unsupported_platforms ]
1195 for p_re
in unsupported:
1196 if p_re.search(platform):
1197 result.SetOutcome(result.UNTESTED)
1198 result[result.CAUSE] =
'Platform not supported.'
1204 Return the platform Id defined in CMTCONFIG or SCRAM_ARCH.
1208 if "CMTCONFIG" in os.environ:
1209 arch = os.environ[
"CMTCONFIG"]
1210 elif "SCRAM_ARCH" in os.environ:
1211 arch = os.environ[
"SCRAM_ARCH"]
1216 Return True if the current platform is Windows.
1218 This function was needed because of the change in the CMTCONFIG format,
1219 from win32_vc71_dbg to i686-winxp-vc9-dbg.
1221 platform = self.GetPlatform()
1222 return "winxp" in platform
or platform.startswith(
"win")
1224 def _expandReferenceFileName(self, reffile):
1230 platformSplit =
lambda p: set(p.split(
'-' in p
and '-' or '_'))
1232 reference = os.path.normpath(os.path.expandvars(reffile))
1234 spec_ref = reference[:-3] + self.GetPlatform()[0:3] + reference[-3:]
1235 if os.path.isfile(spec_ref):
1236 reference = spec_ref
1239 dirname, basename = os.path.split(reference)
1240 if not dirname: dirname =
'.'
1241 head = basename +
"."
1242 head_len = len(head)
1243 platform = platformSplit(self.GetPlatform())
1245 for f
in os.listdir(dirname):
1246 if f.startswith(head):
1247 req_plat = platformSplit(f[head_len:])
1248 if platform.issuperset(req_plat):
1249 candidates.append( (len(req_plat), f) )
1254 reference = os.path.join(dirname, candidates[-1][1])
1257 def CheckTTreesSummaries(self, stdout, result, causes,
1259 ignore =
r"Basket|.*size|Compression"):
1261 Compare the TTree summaries in stdout with the ones in trees_dict or in
1262 the reference file. By default ignore the size, compression and basket
1264 The presence of TTree summaries when none is expected is not a failure.
1266 if trees_dict
is None:
1267 reference = self._expandReferenceFileName(self.reference)
1269 if reference
and os.path.isfile(reference):
1274 from pprint
import PrettyPrinter
1275 pp = PrettyPrinter()
1277 result[
"GaudiTest.TTrees.expected"] = result.Quote(pp.pformat(trees_dict))
1279 result[
"GaudiTest.TTrees.ignore"] = result.Quote(ignore)
1284 causes.append(
"trees summaries")
1286 result[
"GaudiTest.TTrees.failure_on"] = result.Quote(msg)
1287 result[
"GaudiTest.TTrees.found"] = result.Quote(pp.pformat(trees))
1291 def CheckHistosSummaries(self, stdout, result, causes,
1295 Compare the TTree summaries in stdout with the ones in trees_dict or in
1296 the reference file. By default ignore the size, compression and basket
1298 The presence of TTree summaries when none is expected is not a failure.
1301 reference = self._expandReferenceFileName(self.reference)
1303 if reference
and os.path.isfile(reference):
1308 from pprint
import PrettyPrinter
1309 pp = PrettyPrinter()
1311 result[
"GaudiTest.Histos.expected"] = result.Quote(pp.pformat(dict))
1313 result[
"GaudiTest.Histos.ignore"] = result.Quote(ignore)
1318 causes.append(
"histos summaries")
1320 result[
"GaudiTest.Histos.failure_on"] = result.Quote(msg)
1321 result[
"GaudiTest.Histos.found"] = result.Quote(pp.pformat(histos))
1325 def ValidateWithReference(self, stdout, stderr, result, causes, preproc = None):
1327 Default validation action: compare standard output and error to the
1332 preproc = normalizeExamples
1334 reference = self._expandReferenceFileName(self.reference)
1336 if reference
and os.path.isfile(reference):
1337 result[
"GaudiTest.output_reference"] = reference
1338 causes += ReferenceFileValidator(reference,
1340 "GaudiTest.output_diff",
1341 preproc = preproc)(stdout, result)
1344 causes = self.CheckTTreesSummaries(stdout, result, causes)
1345 causes = self.CheckHistosSummaries(stdout, result, causes)
1349 newref = open(reference +
".new",
"w")
1351 for l
in stdout.splitlines():
1352 newref.write(l.rstrip() +
'\n')
1360 reference = self._expandReferenceFileName(self.error_reference)
1362 if reference
and os.path.isfile(reference):
1363 result[
"GaudiTest.error_reference"] = reference
1364 newcauses = ReferenceFileValidator(reference,
1366 "GaudiTest.error_diff",
1367 preproc = preproc)(stderr, result)
1370 newref = open(reference +
".new",
"w")
1372 for l
in stderr.splitlines():
1373 newref.write(l.rstrip() +
'\n')
1376 causes += BasicOutputValidator(self.stderr,
1378 "ExecTest.expected_stderr")(stderr, result)
1382 def ValidateOutput(self, stdout, stderr, result):
1385 if self.validator.strip() !=
"":
1386 class CallWrapper(object):
1388 Small wrapper class to dynamically bind some default arguments
1391 def __init__(self, callable, extra_args = {}):
1392 self.callable = callable
1393 self.extra_args = extra_args
1395 from inspect
import getargspec
1396 self.args_order = getargspec(callable)[0]
1399 if self.args_order[0] ==
"self":
1400 del self.args_order[0]
1401 def __call__(self, *args, **kwargs):
1403 positional = self.args_order[:len(args)]
1405 kwargs = dict(kwargs)
1406 for a
in self.extra_args:
1409 if a
not in positional
and a
not in kwargs:
1410 kwargs[a] = self.extra_args[a]
1411 return apply(self.callable, args, kwargs)
1413 exported_symbols = {
"self":self,
1418 "findReferenceBlock":
1419 CallWrapper(findReferenceBlock, {
"stdout":stdout,
1422 "validateWithReference":
1423 CallWrapper(self.ValidateWithReference, {
"stdout":stdout,
1428 CallWrapper(countErrorLines, {
"stdout":stdout,
1431 "checkTTreesSummaries":
1432 CallWrapper(self.CheckTTreesSummaries, {
"stdout":stdout,
1435 "checkHistosSummaries":
1436 CallWrapper(self.CheckHistosSummaries, {
"stdout":stdout,
1441 exec self.validator
in globals(), exported_symbols
1443 self.ValidateWithReference(stdout, stderr, result, causes)
1447 def DumpEnvironment(self, result):
1449 Add the content of the environment to the result object.
1451 Copied from the QMTest class of COOL.
1453 vars = os.environ.keys()
1455 result[
'GaudiTest.environment'] = \
1456 result.Quote(
'\n'.join([
"%s=%s"%(v,os.environ[v])
for v
in vars]))
1458 def Run(self, context, result):
1461 'context' -- A 'Context' giving run-time parameters to the
1464 'result' -- A 'Result' object. The outcome will be
1465 'Result.PASS' when this method is called. The 'result' may be
1466 modified by this method to indicate outcomes other than
1467 'Result.PASS' or to add annotations."""
1470 if self.PlatformIsNotSupported(context, result):
1476 elif "GAUDIEXE" in os.environ:
1477 prog = os.environ[
"GAUDIEXE"]
1482 dummy, prog_ext = os.path.splitext(prog)
1483 if prog_ext
not in [
".exe",
".py",
".bat" ]
and self.isWinPlatform():
1487 prog =
which(prog)
or prog
1490 args =
map(rationalizepath, self.args)
1497 if self.options.strip():
1499 if re.search(
r"from\s+Gaudi.Configuration\s+import\s+\*|from\s+Configurables\s+import", self.options):
1501 tmpfile = TempFile(ext)
1502 tmpfile.writelines(
"\n".join(self.options.splitlines()))
1504 args.append(tmpfile.name)
1505 result[
"GaudiTest.options"] = result.Quote(self.options)
1508 if prog_ext ==
".py":
1510 if self.isWinPlatform():
1511 prog =
which(
"python.exe")
or "python.exe"
1513 prog =
which(
"python")
or "python"
1516 origdir = os.getcwd()
1518 os.chdir(str(os.path.normpath(os.path.expandvars(self.workdir))))
1519 elif self.use_temp_dir ==
"true":
1520 if "QMTEST_TMPDIR" in os.environ:
1521 qmtest_tmpdir = os.environ[
"QMTEST_TMPDIR"]
1522 if not os.path.exists(qmtest_tmpdir):
1523 os.makedirs(qmtest_tmpdir)
1524 os.chdir(qmtest_tmpdir)
1525 elif "qmtest.tmpdir" in context:
1526 os.chdir(context[
"qmtest.tmpdir"])
1528 if "QMTEST_IGNORE_TIMEOUT" not in os.environ:
1529 self.timeout = max(self.timeout,600)
1535 self._CreateEclipseLaunch(prog, args, destdir = os.path.join(origdir,
'.eclipse'))
1537 self.RunProgram(prog,
1541 if result.GetOutcome()
not in [ result.PASS ]:
1542 self.DumpEnvironment(result)
1547 def RunProgram(self, program, arguments, context, result):
1548 """Run the 'program'.
1550 'program' -- The path to the program to run.
1552 'arguments' -- A list of the arguments to the program. This
1553 list must contain a first argument corresponding to 'argv[0]'.
1555 'context' -- A 'Context' giving run-time parameters to the
1558 'result' -- A 'Result' object. The outcome will be
1559 'Result.PASS' when this method is called. The 'result' may be
1560 modified by this method to indicate outcomes other than
1561 'Result.PASS' or to add annotations.
1563 @attention: This method has been copied from command.ExecTestBase
1564 (QMTest 2.3.0) and modified to keep stdout and stderr
1565 for tests that have been terminated by a signal.
1566 (Fundamental for debugging in the Application Area)
1570 environment = self.MakeEnvironment(context)
1572 if "slc6" in environment.get(
'CMTCONFIG',
''):
1573 environment[
'TERM'] =
'dumb'
1575 if self.timeout >= 0:
1576 timeout = self.timeout
1584 e = GaudiFilterExecutable(self.stdin, timeout)
1586 exit_status = e.Run(arguments, environment, path = program)
1588 if e.stack_trace_file
and os.path.exists(e.stack_trace_file):
1589 stack_trace = open(e.stack_trace_file).read()
1590 os.remove(e.stack_trace_file)
1594 result[
"ExecTest.stack_trace"] = result.Quote(stack_trace)
1597 if (sys.platform ==
"win32" or os.WIFEXITED(exit_status)
1598 or self.signal == os.WTERMSIG(exit_status)):
1603 if self.exit_code
is None:
1605 elif sys.platform ==
"win32":
1606 exit_code = exit_status
1608 exit_code = os.WEXITSTATUS(exit_status)
1613 result[
"ExecTest.exit_code"] = str(exit_code)
1614 result[
"ExecTest.stdout"] = result.Quote(stdout)
1615 result[
"ExecTest.stderr"] = result.Quote(stderr)
1617 if exit_code != self.exit_code:
1618 causes.append(
"exit_code")
1619 result[
"ExecTest.expected_exit_code"] \
1620 = str(self.exit_code)
1622 causes += self.ValidateOutput(stdout, stderr, result)
1625 result.Fail(
"Unexpected %s." % string.join(causes,
", "))
1626 elif os.WIFSIGNALED(exit_status):
1629 signal_number = str(os.WTERMSIG(exit_status))
1631 result.Fail(
"Program terminated by signal.")
1635 result.Fail(
"Exceeded time limit (%ds), terminated." % timeout)
1636 result[
"ExecTest.signal_number"] = signal_number
1637 result[
"ExecTest.stdout"] = result.Quote(e.stdout)
1638 result[
"ExecTest.stderr"] = result.Quote(e.stderr)
1640 result[
"ExecTest.expected_signal_number"] = str(self.signal)
1641 elif os.WIFSTOPPED(exit_status):
1644 signal_number = str(os.WSTOPSIG(exit_status))
1646 result.Fail(
"Program stopped by signal.")
1650 result.Fail(
"Exceeded time limit (%ds), stopped." % timeout)
1651 result[
"ExecTest.signal_number"] = signal_number
1652 result[
"ExecTest.stdout"] = result.Quote(e.stdout)
1653 result[
"ExecTest.stderr"] = result.Quote(e.stderr)
1657 result.Fail(
"Program did not terminate normally.")
1663 result[
"ExecTest.stdout"] = result[
"ExecTest.stdout"].replace(esc,repr_esc)
1667 def _CreateEclipseLaunch(self, prog, args, destdir = None):
1668 if 'NO_ECLIPSE_LAUNCHERS' in os.environ:
1673 projbasedir = os.path.normpath(destdir)
1674 while not os.path.exists(os.path.join(projbasedir,
".project")):
1675 oldprojdir = projbasedir
1676 projbasedir = os.path.normpath(os.path.join(projbasedir, os.pardir))
1679 if oldprojdir == projbasedir:
1683 if not os.path.exists(destdir):
1684 os.makedirs(destdir)
1686 from xml.etree
import ElementTree
as ET
1687 t = ET.parse(os.path.join(projbasedir,
".project"))
1688 projectName = t.find(
"name").text
1691 destfile =
"%s.launch" % self._Runnable__id
1693 destfile = os.path.join(destdir, destfile)
1695 if self.options.strip():
1699 tempfile = args.pop()
1700 optsfile = destfile + os.path.splitext(tempfile)[1]
1701 shutil.copyfile(tempfile, optsfile)
1702 args.append(optsfile)
1705 from xml.sax.saxutils
import quoteattr
1709 data[
"environment"] =
"\n".join([
'<mapEntry key=%s value=%s/>' % (quoteattr(k), quoteattr(v))
1710 for k, v
in os.environ.iteritems()
1711 if k
not in (
'MAKEOVERRIDES',
'MAKEFLAGS',
'MAKELEVEL')])
1713 data[
"exec"] =
which(prog)
or prog
1714 if os.path.basename(data[
"exec"]).lower().startswith(
"python"):
1715 data[
"stopAtMain"] =
"false"
1717 data[
"stopAtMain"] =
"true"
1719 data[
"args"] =
" ".join(
map(rationalizepath, args))
1720 if self.isWinPlatform():
1721 data[
"args"] =
" ".join([
"/debugexe"] +
map(rationalizepath, [data[
"exec"]] + args))
1722 data[
"exec"] =
which(
"vcexpress.exe")
1724 if not self.use_temp_dir:
1725 data[
"workdir"] = os.getcwd()
1729 data[
"workdir"] = destdir
1731 data[
"project"] = projectName.strip()
1734 xml_template =
u"""<?xml version="1.0" encoding="UTF-8" standalone="no"?>
1735 <launchConfiguration type="org.eclipse.cdt.launch.applicationLaunchType">
1736 <booleanAttribute key="org.eclipse.cdt.debug.mi.core.AUTO_SOLIB" value="true"/>
1737 <listAttribute key="org.eclipse.cdt.debug.mi.core.AUTO_SOLIB_LIST"/>
1738 <stringAttribute key="org.eclipse.cdt.debug.mi.core.DEBUG_NAME" value="gdb"/>
1739 <stringAttribute key="org.eclipse.cdt.debug.mi.core.GDB_INIT" value=".gdbinit"/>
1740 <listAttribute key="org.eclipse.cdt.debug.mi.core.SOLIB_PATH"/>
1741 <booleanAttribute key="org.eclipse.cdt.debug.mi.core.STOP_ON_SOLIB_EVENTS" value="false"/>
1742 <booleanAttribute key="org.eclipse.cdt.debug.mi.core.breakpointsFullPath" value="false"/>
1743 <stringAttribute key="org.eclipse.cdt.debug.mi.core.commandFactory" value="org.eclipse.cdt.debug.mi.core.standardCommandFactory"/>
1744 <stringAttribute key="org.eclipse.cdt.debug.mi.core.protocol" value="mi"/>
1745 <booleanAttribute key="org.eclipse.cdt.debug.mi.core.verboseMode" value="false"/>
1746 <intAttribute key="org.eclipse.cdt.launch.ATTR_BUILD_BEFORE_LAUNCH_ATTR" value="0"/>
1747 <stringAttribute key="org.eclipse.cdt.launch.COREFILE_PATH" value=""/>
1748 <stringAttribute key="org.eclipse.cdt.launch.DEBUGGER_ID" value="org.eclipse.cdt.debug.mi.core.CDebuggerNew"/>
1749 <stringAttribute key="org.eclipse.cdt.launch.DEBUGGER_REGISTER_GROUPS" value=""/>
1750 <stringAttribute key="org.eclipse.cdt.launch.DEBUGGER_START_MODE" value="run"/>
1751 <booleanAttribute key="org.eclipse.cdt.launch.DEBUGGER_STOP_AT_MAIN" value="%(stopAtMain)s"/>
1752 <stringAttribute key="org.eclipse.cdt.launch.DEBUGGER_STOP_AT_MAIN_SYMBOL" value="main"/>
1753 <booleanAttribute key="org.eclipse.cdt.launch.ENABLE_REGISTER_BOOKKEEPING" value="false"/>
1754 <booleanAttribute key="org.eclipse.cdt.launch.ENABLE_VARIABLE_BOOKKEEPING" value="false"/>
1755 <stringAttribute key="org.eclipse.cdt.launch.FORMAT" value="<?xml version="1.0" encoding="UTF-8" standalone="no"?><contentList/>"/>
1756 <stringAttribute key="org.eclipse.cdt.launch.GLOBAL_VARIABLES" value="<?xml version="1.0" encoding="UTF-8" standalone="no"?> <globalVariableList/> "/>
1757 <stringAttribute key="org.eclipse.cdt.launch.MEMORY_BLOCKS" value="<?xml version="1.0" encoding="UTF-8" standalone="no"?> <memoryBlockExpressionList/> "/>
1758 <stringAttribute key="org.eclipse.cdt.launch.PROGRAM_ARGUMENTS" value="%(args)s"/>
1759 <stringAttribute key="org.eclipse.cdt.launch.PROGRAM_NAME" value="%(exec)s"/>
1760 <stringAttribute key="org.eclipse.cdt.launch.PROJECT_ATTR" value="%(project)s"/>
1761 <stringAttribute key="org.eclipse.cdt.launch.PROJECT_BUILD_CONFIG_ID_ATTR" value=""/>
1762 <stringAttribute key="org.eclipse.cdt.launch.WORKING_DIRECTORY" value="%(workdir)s"/>
1763 <booleanAttribute key="org.eclipse.cdt.launch.ui.ApplicationCDebuggerTab.DEFAULTS_SET" value="true"/>
1764 <booleanAttribute key="org.eclipse.cdt.launch.use_terminal" value="true"/>
1765 <listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_PATHS">
1766 <listEntry value="/%(project)s"/>
1768 <listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_TYPES">
1769 <listEntry value="4"/>
1771 <booleanAttribute key="org.eclipse.debug.core.appendEnvironmentVariables" value="false"/>
1772 <mapAttribute key="org.eclipse.debug.core.environmentVariables">
1775 <mapAttribute key="org.eclipse.debug.core.preferred_launchers">
1776 <mapEntry key="[debug]" value="org.eclipse.cdt.cdi.launch.localCLaunch"/>
1778 <listAttribute key="org.eclipse.debug.ui.favoriteGroups">
1779 <listEntry value="org.eclipse.debug.ui.launchGroup.debug"/>
1781 </launchConfiguration>
1786 data[k] = codecs.decode(data[k],
'utf-8')
1787 xml = xml_template % data
1790 codecs.open(destfile,
"w", encoding=
'utf-8').write(xml)
1792 print 'WARNING: problem generating Eclipse launcher'
1799 import simplejson
as json
1801 class HTMLResultStream(ResultStream):
1802 """An 'HTMLResultStream' writes its output to a set of HTML files.
1804 The argument 'dir' is used to select the destination directory for the HTML
1806 The destination directory may already contain the report from a previous run
1807 (for example of a different package), in which case it will be extended to
1808 include the new data.
1811 qm.fields.TextField(
1813 title =
"Destination Directory",
1814 description =
"""The name of the directory.
1816 All results will be written to the directory indicated.""",
1818 default_value =
""),
1821 def __init__(self, arguments = None, **args):
1822 """Prepare the destination directory.
1824 Creates the destination directory and store in it some preliminary
1825 annotations and the static files found in the template directory
1828 ResultStream.__init__(self, arguments, **args)
1830 self._summaryFile = os.path.join(self.dir,
"summary.json")
1831 self._annotationsFile = os.path.join(self.dir,
"annotations.json")
1833 templateDir = os.path.join(os.path.dirname(__file__),
"html_report")
1834 if not os.path.isdir(self.dir):
1835 os.makedirs(self.dir)
1837 for f
in os.listdir(templateDir):
1838 src = os.path.join(templateDir, f)
1839 dst = os.path.join(self.dir, f)
1840 if not os.path.isdir(src)
and not os.path.exists(dst):
1841 shutil.copy(src, dst)
1843 if "CMTCONFIG" in os.environ:
1844 self.WriteAnnotation(
"cmt.cmtconfig", os.environ[
"CMTCONFIG"])
1846 self.WriteAnnotation(
"hostname", socket.gethostname())
1848 def _updateSummary(self):
1849 """Helper function to extend the global summary file in the destination
1852 if os.path.exists(self._summaryFile):
1853 oldSummary = json.load(open(self._summaryFile))
1856 ids = set([ i[
"id"]
for i
in self._summary ])
1857 newSummary = [ i
for i
in oldSummary
if i[
"id"]
not in ids ]
1858 newSummary.extend(self._summary)
1859 json.dump(newSummary, open(self._summaryFile,
"w"),
1862 def WriteAnnotation(self, key, value):
1863 """Writes the annotation to the annotation file.
1864 If the key is already present with a different value, the value becomes
1865 a list and the new value is appended to it, except for start_time and
1869 if os.path.exists(self._annotationsFile):
1870 annotations = json.load(open(self._annotationsFile))
1874 key, value =
map(str, [key, value])
1875 if key ==
"qmtest.run.start_time":
1880 if key
not in annotations:
1881 annotations[key] = value
1882 if "qmtest.run.end_time" in annotations:
1883 del annotations[
"qmtest.run.end_time"]
1886 if key
in annotations:
1887 old = annotations[key]
1888 if type(old)
is list:
1889 if value
not in old:
1890 annotations[key].append(value)
1892 annotations[key] = [old, value]
1894 annotations[key] = value
1896 json.dump(annotations, open(self._annotationsFile,
"w"),
1899 def WriteResult(self, result):
1900 """Prepare the test result directory in the destination directory storing
1901 into it the result fields.
1902 A summary of the test result is stored both in a file in the test directory
1903 and in the global summary file.
1906 summary[
"id"] = result.GetId()
1907 summary[
"outcome"] = result.GetOutcome()
1908 summary[
"cause"] = result.GetCause()
1909 summary[
"fields"] = result.keys()
1910 summary[
"fields"].sort()
1913 for f
in [
"id",
"outcome",
"cause"]:
1914 summary[f] = str(summary[f])
1915 summary[
"fields"] =
map(str, summary[
"fields"])
1917 self._summary.append(summary)
1923 testOutDir = os.path.join(self.dir, summary[
"id"])
1924 if not os.path.isdir(testOutDir):
1925 os.makedirs(testOutDir)
1926 json.dump(summary, open(os.path.join(testOutDir,
"summary.json"),
"w"),
1928 for f
in summary[
"fields"]:
1929 open(os.path.join(testOutDir, f),
"w").write(result[f])
1931 self._updateSummary()
1933 def Summarize(self):
1940 class XMLResultStream(ResultStream):
1941 """An 'XMLResultStream' writes its output to a Ctest XML file.
1943 The argument 'dir' is used to select the destination file for the XML
1945 The destination directory may already contain the report from a previous run
1946 (for example of a different package), in which case it will be overrided to
1950 qm.fields.TextField(
1952 title =
"Destination Directory",
1953 description =
"""The name of the directory.
1955 All results will be written to the directory indicated.""",
1957 default_value =
""),
1958 qm.fields.TextField(
1960 title =
"Output File Prefix",
1961 description =
"""The output file name will be the specified prefix
1962 followed by 'Test.xml' (CTest convention).""",
1964 default_value =
""),
1967 def __init__(self, arguments = None, **args):
1968 """Prepare the destination directory.
1970 Creates the destination directory and store in it some preliminary
1973 ResultStream.__init__(self, arguments, **args)
1975 self._xmlFile = os.path.join(self.dir, self.prefix +
'Test.xml')
1978 self._startTime =
None
1979 self._endTime =
None
1981 if not os.path.isfile(self._xmlFile):
1983 if not os.path.exists(os.path.dirname(self._xmlFile)):
1984 os.makedirs(os.path.dirname(self._xmlFile))
1986 newdataset = ET.Element(
"newdataset")
1987 self._tree = ET.ElementTree(newdataset)
1988 self._tree.write(self._xmlFile)
1991 self._tree = ET.parse(self._xmlFile)
1992 newdataset = self._tree.getroot()
1999 for site
in newdataset.getiterator() :
2000 if site.get(
"OSPlatform") == os.uname()[4]:
2010 import multiprocessing
2012 "BuildName" : os.getenv(
"CMTCONFIG"),
2013 "Name" : os.uname()[1] ,
2014 "Generator" :
"QMTest "+qm.version ,
2015 "OSName" : os.uname()[0] ,
2016 "Hostname" : socket.gethostname() ,
2017 "OSRelease" : os.uname()[2] ,
2018 "OSVersion" :os.uname()[3] ,
2019 "OSPlatform" :os.uname()[4] ,
2020 "Is64Bits" :
"unknown" ,
2021 "VendorString" :
"unknown" ,
2022 "VendorID" :
"unknown" ,
2023 "FamilyID" :
"unknown" ,
2024 "ModelID" :
"unknown" ,
2025 "ProcessorCacheSize" :
"unknown" ,
2026 "NumberOfLogicalCPU" : str(multiprocessing.cpu_count()) ,
2027 "NumberOfPhysicalCPU" :
"0" ,
2028 "TotalVirtualMemory" :
"0" ,
2029 "TotalPhysicalMemory" :
"0" ,
2030 "LogicalProcessorsPerPhysical" :
"0" ,
2031 "ProcessorClockFrequency" :
"0" ,
2033 self._site = ET.SubElement(newdataset,
"Site", attrib)
2034 self._Testing = ET.SubElement(self._site,
"Testing")
2037 self._StartDateTime = ET.SubElement(self._Testing,
"StartDateTime")
2039 self._StartTestTime = ET.SubElement(self._Testing,
"StartTestTime")
2042 self._TestList = ET.SubElement(self._Testing,
"TestList")
2045 self._EndDateTime = ET.SubElement(self._Testing,
"EndDateTime")
2048 self._EndTestTime = ET.SubElement(self._Testing,
"EndTestTime")
2052 self._ElapsedMinutes = ET.SubElement(self._Testing,
"ElapsedMinutes")
2056 self._Testing = self._site.find(
"Testing")
2057 self._StartDateTime = self._Testing.find(
"StartDateTime")
2058 self._StartTestTime = self._Testing.find(
"StartTestTime")
2059 self._TestList = self._Testing.find(
"TestList")
2060 self._EndDateTime = self._Testing.find(
"EndDateTime")
2061 self._EndTestTime = self._Testing.find(
"EndTestTime")
2062 self._ElapsedMinutes = self._Testing.find(
"ElapsedMinutes")
2065 # Add some non-QMTest attributes
2066 if "CMTCONFIG" in os.environ:
2067 self.WriteAnnotation("cmt.cmtconfig", os.environ["CMTCONFIG"])
2069 self.WriteAnnotation("hostname", socket.gethostname())
2073 def WriteAnnotation(self, key, value):
2074 if key ==
"qmtest.run.start_time":
2075 if self._site.get(
"qmtest.run.start_time")
is not None :
2077 self._site.set(str(key),str(value))
2078 def WriteResult(self, result):
2079 """Prepare the test result directory in the destination directory storing
2080 into it the result fields.
2081 A summary of the test result is stored both in a file in the test directory
2082 and in the global summary file.
2085 summary[
"id"] = result.GetId()
2086 summary[
"outcome"] = result.GetOutcome()
2087 summary[
"cause"] = result.GetCause()
2088 summary[
"fields"] = result.keys()
2089 summary[
"fields"].sort()
2093 for f
in [
"id",
"outcome",
"cause"]:
2094 summary[f] = str(summary[f])
2095 summary[
"fields"] =
map(str, summary[
"fields"])
2101 if "qmtest.start_time" in summary[
"fields"]:
2102 haveStartDate =
True
2104 haveStartDate =
False
2105 if "qmtest.end_time" in summary[
"fields"]:
2112 self._startTime = calendar.timegm(time.strptime(result[
"qmtest.start_time"],
"%Y-%m-%dT%H:%M:%SZ"))
2113 if self._StartTestTime.text
is None:
2114 self._StartDateTime.text = time.strftime(
"%b %d %H:%M %Z", time.localtime(self._startTime))
2115 self._StartTestTime.text = str(self._startTime)
2116 self._site.set(
"BuildStamp" , result[
"qmtest.start_time"] )
2120 self._endTime = calendar.timegm(time.strptime(result[
"qmtest.end_time"],
"%Y-%m-%dT%H:%M:%SZ"))
2124 tl = ET.Element(
"Test")
2125 tl.text = summary[
"id"]
2126 self._TestList.insert(0,tl)
2129 Test = ET.Element(
"Test")
2130 if summary[
"outcome"] ==
"PASS":
2131 Test.set(
"Status",
"passed")
2132 elif summary[
"outcome"] ==
"FAIL":
2133 Test.set(
"Status",
"failed")
2134 elif summary[
"outcome"] ==
"SKIPPED" or summary[
"outcome"] ==
"UNTESTED":
2135 Test.set(
"Status",
"skipped")
2136 elif summary[
"outcome"] ==
"ERROR":
2137 Test.set(
"Status",
"failed")
2138 Name = ET.SubElement(Test,
"Name",)
2139 Name.text = summary[
"id"]
2140 Results = ET.SubElement(Test,
"Results")
2143 self._Testing.insert(3,Test)
2145 if haveStartDate
and haveEndDate:
2147 delta = self._endTime - self._startTime
2148 testduration = str(delta)
2149 Testduration= ET.SubElement(Results,
"NamedMeasurement")
2150 Testduration.set(
"name",
"Execution Time")
2151 Testduration.set(
"type",
"numeric/float" )
2152 value = ET.SubElement(Testduration,
"Value")
2153 value.text = testduration
2156 for n
in (
"qmtest.end_time",
"qmtest.start_time",
"qmtest.cause",
"ExecTest.stdout"):
2157 if n
in summary[
"fields"]:
2158 summary[
"fields"].
remove(n)
2162 if "ExecTest.exit_code" in summary[
"fields"] :
2163 summary[
"fields"].
remove(
"ExecTest.exit_code")
2164 ExitCode= ET.SubElement(Results,
"NamedMeasurement")
2165 ExitCode.set(
"name",
"exit_code")
2166 ExitCode.set(
"type",
"numeric/integer" )
2167 value = ET.SubElement(ExitCode,
"Value")
2170 TestStartTime= ET.SubElement(Results,
"NamedMeasurement")
2171 TestStartTime.set(
"name",
"Start_Time")
2172 TestStartTime.set(
"type",
"String" )
2173 value = ET.SubElement(TestStartTime,
"Value")
2179 TestEndTime= ET.SubElement(Results,
"NamedMeasurement")
2180 TestEndTime.set(
"name",
"End_Time")
2181 TestEndTime.set(
"type",
"String" )
2182 value = ET.SubElement(TestEndTime,
"Value")
2188 if summary[
"cause"]:
2189 FailureCause= ET.SubElement(Results,
"NamedMeasurement")
2190 FailureCause.set(
"name",
"Cause")
2191 FailureCause.set(
"type",
"String" )
2192 value = ET.SubElement(FailureCause,
"Value")
2197 for field
in summary[
"fields"] :
2198 fields[field] = ET.SubElement(Results,
"NamedMeasurement")
2199 fields[field].set(
"type",
"String")
2200 fields[field].set(
"name",field)
2201 value = ET.SubElement(fields[field],
"Value")
2203 if "<pre>" in result[field][0:6] :
2209 if result.has_key(
"ExecTest.stdout" ) :
2210 Measurement = ET.SubElement(Results,
"Measurement")
2211 value = ET.SubElement(Measurement,
"Value")
2212 if "<pre>" in result[
"ExecTest.stdout"][0:6] :
2219 self._tree.write(self._xmlFile,
"utf-8")
2222 def Summarize(self):
2225 self._EndTestTime.text = str(self._endTime)
2226 self._EndDateTime.text = time.strftime(
"%b %d %H:%M %Z", time.localtime(self._endTime))
2229 if self._endTime
and self._startTime:
2230 delta = self._endTime - self._startTime
2233 self._ElapsedMinutes.text = str(delta/60)
2236 self._tree.write(self._xmlFile,
"utf-8")