5 __author__ =
'Marco Clemencic CERN/PH-LBC'
16 from subprocess
import Popen, PIPE, STDOUT
19 from qm.test.classes.command
import ExecTestBase
20 from qm.test.result_stream
import ResultStream
27 if sys.platform ==
"win32":
30 from threading
import *
48 Class to changes the environment temporarily.
50 def __init__(self, orig = os.environ, keep_same = False):
52 Create a temporary environment on top of the one specified
53 (it can be another TemporaryEnvironment instance).
62 Set an environment variable recording the previous value.
74 Get an environment variable.
75 Needed to provide the same interface as os.environ.
81 Unset an environment variable.
82 Needed to provide the same interface as os.environ.
84 if key
not in self.
env :
91 Return the list of defined environment variables.
92 Needed to provide the same interface as os.environ.
94 return self.env.keys()
98 Return the list of (name,value) pairs for the defined environment variables.
99 Needed to provide the same interface as os.environ.
101 return self.env.items()
106 Needed to provide the same interface as os.environ.
108 return key
in self.
env
112 Revert all the changes done to the orignal environment.
114 for key,value
in self.old_values.items():
118 self.
env[key] = value
123 Revert the changes on destruction.
130 Generate a shell script to reproduce the changes in the environment.
132 shells = [
'csh',
'sh',
'bat' ]
133 if shell_type
not in shells:
134 raise RuntimeError(
"Shell type '%s' unknown. Available: %s"%(shell_type,shells))
136 for key,value
in self.old_values.items():
137 if key
not in self.
env:
139 if shell_type ==
'csh':
140 out +=
'unsetenv %s\n'%key
141 elif shell_type ==
'sh':
142 out +=
'unset %s\n'%key
143 elif shell_type ==
'bat':
144 out +=
'set %s=\n'%key
147 if shell_type ==
'csh':
148 out +=
'setenv %s "%s"\n'%(key,self.
env[key])
149 elif shell_type ==
'sh':
150 out +=
'export %s="%s"\n'%(key,self.
env[key])
151 elif shell_type ==
'bat':
152 out +=
'set %s=%s\n'%(key,self.
env[key])
156 """Small class for temporary directories.
157 When instantiated, it creates a temporary directory and the instance
158 behaves as the string containing the directory name.
159 When the instance goes out of scope, it removes all the content of
160 the temporary directory (automatic clean-up).
177 shutil.rmtree(self.
name)
180 return getattr(self.
name,attr)
183 """Small class for temporary files.
184 When instantiated, it creates a temporary directory and the instance
185 behaves as the string containing the directory name.
186 When the instance goes out of scope, it removes all the content of
187 the temporary directory (automatic clean-up).
189 def __init__(self, suffix='', prefix='tmp', dir=None, text=False, keep = False):
194 self._fd, self.
name = tempfile.mkstemp(suffix,prefix,dir,text)
195 self.
file = os.fdopen(self._fd,
"r+")
207 return getattr(self.
file,attr)
210 """Small wrapper to call CMT.
219 if type(args)
is str:
221 cmd =
"cmt %s"%command
229 result = os.popen4(cmd)[1].read()
235 return lambda args=[]: self.
_run_cmt(attr, args)
238 """Returns a dictionary containing the runtime environment produced by CMT.
239 If a dictionary is passed a modified instance of it is returned.
243 for l
in self.setup(
"-csh").splitlines():
245 if l.startswith(
"setenv"):
246 dummy,name,value = l.split(
None,3)
247 env[name] = value.strip(
'"')
248 elif l.startswith(
"unsetenv"):
249 dummy,name = l.split(
None,2)
254 r = self.show([
"macro",k])
255 if r.find(
"CMT> Error: symbol not found") >= 0:
258 return self.show([
"macro_value",k]).strip()
265 Locates an executable in the executables path ($PATH) and returns the full
266 path to it. An application is looked for with or without the '.exe' suffix.
267 If the executable cannot be found, None is returned
269 if os.path.isabs(executable):
270 if not os.path.exists(executable):
271 if executable.endswith(
'.exe'):
272 if os.path.exists(executable[:-4]):
273 return executable[:-4]
275 for d
in os.environ.get(
"PATH").split(os.pathsep):
276 fullpath = os.path.join(d, executable)
277 if os.path.exists(fullpath):
279 if executable.endswith(
'.exe'):
280 return which(executable[:-4])
284 p = os.path.normpath(os.path.expandvars(p))
285 if os.path.exists(p):
286 p = os.path.realpath(p)
293 """Basic implementation of an option validator for Gaudi tests.
294 This implementation is based on the standard (LCG) validation functions
303 """Validate the output of the program.
305 'stdout' -- A string containing the data written to the standard output
308 'stderr' -- A string containing the data written to the standard error
311 'result' -- A 'Result' object. It may be used to annotate
312 the outcome according to the content of stderr.
314 returns -- A list of strings giving causes of failure."""
319 causes.append(self.
cause)
325 """Compare 's1' and 's2', ignoring line endings.
331 returns -- True if 's1' and 's2' are the same, ignoring
332 differences in line endings."""
336 return s1.splitlines() == s2.splitlines()
339 """ Base class for a callable that takes a file and returns a modified
344 if hasattr(input,
"__iter__"):
348 lines = input.splitlines()
353 if l: output.append(l)
354 if mergeback: output =
'\n'.join(output)
378 if line.find(s) >= 0:
return None
380 if r.search(line):
return None
390 if self.
start in line:
393 elif self.
end in line:
402 when = re.compile(when)
405 if isinstance(rhs, RegexpReplacer):
407 res._operations = self.
_operations + rhs._operations
409 res = FilePreprocessor.__add__(self, rhs)
413 if w
is None or w.search(line):
414 line = o.sub(r, line)
419 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)?",
420 "00:00:00 1970-01-01")
422 normalizeEOL.__processLine__ =
lambda line: str(line).rstrip() +
'\n'
426 skipEmptyLines.__processLine__ =
lambda line: (line.strip()
and line)
or None
437 line = line[:(pos+self.
siglen)]
438 lst = line[(pos+self.
siglen):].split()
440 line +=
" ".join(lst)
444 normalizeExamples = maskPointers + normalizeDate
447 (
"TIMER.TIMER",
r"\s+[+-]?[0-9]+[0-9.]*",
" 0"),
448 (
"release all pending",
r"^.*/([^/]*:.*)",
r"\1"),
449 (
"0x########",
r"\[.*/([^/]*.*)\]",
r"[\1]"),
450 (
"^#.*file",
r"file '.*[/\\]([^/\\]*)$",
r"file '\1"),
451 (
"^JobOptionsSvc.*options successfully read in from",
r"read in from .*[/\\]([^/\\]*)$",
r"file \1"),
453 (
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"),
455 (
"ServiceLocatorHelper::",
"ServiceLocatorHelper::(create|locate)Service",
"ServiceLocatorHelper::service"),
457 (
None,
r"e([-+])0([0-9][0-9])",
r"e\1\2"),
461 "JobOptionsSvc INFO # ",
462 "JobOptionsSvc WARNING # ",
465 "This machine has a speed",
468 "ToolSvc.Sequenc... INFO",
469 "DataListenerSvc INFO XML written to file:",
470 "[INFO]",
"[WARNING]",
471 "DEBUG No writable file catalog found which contains FID:",
473 "DEBUG Service base class initialized successfully",
474 "DEBUG Incident timing:",
475 "INFO 'CnvServices':[",
477 'Note: (file "(tmpfile)", line 2) File "set" already loaded',
481 r"^JobOptionsSvc INFO *$",
483 r"(Always|SUCCESS)\s*(Root f|[^ ]* F)ile version:",
484 r"0x[0-9a-fA-F#]+ *Algorithm::sysInitialize\(\) *\[",
485 r"0x[0-9a-fA-F#]* *__gxx_personality_v0 *\[",
486 r"File '.*.xml' does not exist",
487 r"INFO Refer to dataset .* by its file ID:",
488 r"INFO Referring to dataset .* by its file ID:",
489 r"INFO Disconnect from dataset",
490 r"INFO Disconnected from dataset",
491 r"INFO Disconnected data IO:",
492 r"IncidentSvc\s*(DEBUG (Adding|Removing)|VERBOSE Calling)",
494 r"^StatusCodeSvc.*listing all unchecked return codes:",
495 r"^StatusCodeSvc\s*INFO\s*$",
496 r"Num\s*\|\s*Function\s*\|\s*Source Library",
499 r"ERROR Failed to modify file: .* Errno=2 No such file or directory",
501 r"^ +[0-9]+ \|.*ROOT",
502 r"^ +[0-9]+ \|.*\|.*Dict",
506 r"SUCCESS\s*Booked \d+ Histogram\(s\)",
509 ] ) + normalizeExamples + skipEmptyLines + \
514 def __init__(self, reffile, cause, result_key, preproc = normalizeExamples):
521 if os.path.isfile(self.
reffile):
522 orig = open(self.
reffile).xreadlines()
528 new = stdout.splitlines()
532 diffs = difflib.ndiff(orig,new,charjunk=difflib.IS_CHARACTER_JUNK)
533 filterdiffs = map(
lambda x: x.strip(),filter(
lambda x: x[0] !=
" ",diffs))
536 result[self.
result_key] = result.Quote(
"\n".join(filterdiffs))
540 +) standard output of the test""")
541 causes.append(self.
cause)
548 def findReferenceBlock(reference, stdout, result, causes, signature_offset=0, signature=None,
551 Given a block of text, tries to find it in the output.
552 The block had to be identified by a signature line. By default, the first
553 line is used as signature, or the line pointed to by signature_offset. If
554 signature_offset points outside the block, a signature line can be passed as
555 signature argument. Note: if 'signature' is None (the default), a negative
556 signature_offset is interpreted as index in a list (e.g. -1 means the last
557 line), otherwise the it is interpreted as the number of lines before the
558 first one of the block the signature must appear.
559 The parameter 'id' allow to distinguish between different calls to this
560 function in the same validation code.
563 reflines = filter(
None,map(
lambda s: s.rstrip(), reference.splitlines()))
565 raise RuntimeError(
"Empty (or null) reference")
567 outlines = filter(
None,map(
lambda s: s.rstrip(), stdout.splitlines()))
569 res_field =
"GaudiTest.RefBlock"
571 res_field +=
"_%s" % id
573 if signature
is None:
574 if signature_offset < 0:
575 signature_offset = len(reference)+signature_offset
576 signature = reflines[signature_offset]
579 pos = outlines.index(signature)
580 outlines = outlines[pos-signature_offset:pos+len(reflines)-signature_offset]
581 if reflines != outlines:
582 msg =
"standard output"
584 if not msg
in causes:
586 result[res_field +
".observed"] = result.Quote(
"\n".join(outlines))
588 causes.append(
"missing signature")
589 result[res_field +
".signature"] = result.Quote(signature)
590 if len(reflines) > 1
or signature != reflines[0]:
591 result[res_field +
".expected"] = result.Quote(
"\n".join(reflines))
597 Count the number of messages with required severity (by default ERROR and FATAL)
598 and check if their numbers match the expected ones (0 by default).
599 The dictionary "expected" can be used to tune the number of errors and fatals
600 allowed, or to limit the number of expected warnings etc.
602 stdout = kwargs[
"stdout"]
603 result = kwargs[
"result"]
604 causes = kwargs[
"causes"]
611 outlines = stdout.splitlines()
612 from math
import log10
613 fmt =
"%%%dd - %%s" % (int(log10(len(outlines))+1))
619 if len(words) >= 2
and words[1]
in errors:
620 errors[words[1]].append(fmt%(linecount,l.rstrip()))
623 if len(errors[e]) != expected[e]:
624 causes.append(
'%s(%d)'%(e,len(errors[e])))
625 result[
"GaudiTest.lines.%s"%e] = result.Quote(
'\n'.join(errors[e]))
626 result[
"GaudiTest.lines.%s.expected#"%e] = result.Quote(str(expected[e]))
633 Parse the TTree summary table in lines, starting from pos.
634 Returns a tuple with the dictionary with the digested informations and the
635 position of the first line after the summary.
641 splitcols =
lambda l: [ f.strip()
for f
in l.strip(
"*\n").split(
':',2) ]
644 cols = splitcols(ll[0])
645 r[
"Name"], r[
"Title"] = cols[1:]
647 cols = splitcols(ll[1])
648 r[
"Entries"] = int(cols[1])
650 sizes = cols[2].split()
651 r[
"Total size"] = int(sizes[2])
652 if sizes[-1] ==
"memory":
655 r[
"File size"] = int(sizes[-1])
657 cols = splitcols(ll[2])
658 sizes = cols[2].split()
659 if cols[0] ==
"Baskets":
660 r[
"Baskets"] = int(cols[1])
661 r[
"Basket size"] = int(sizes[2])
662 r[
"Compression"] = float(sizes[-1])
665 if i < (count - 3)
and lines[i].startswith(
"*Tree"):
666 result = parseblock(lines[i:i+3])
667 result[
"Branches"] = {}
669 while i < (count - 3)
and lines[i].startswith(
"*Br"):
670 if i < (count - 2)
and lines[i].startswith(
"*Branch "):
674 branch = parseblock(lines[i:i+3])
675 result[
"Branches"][branch[
"Name"]] = branch
682 Scan stdout to find ROOT TTree summaries and digest them.
684 stars = re.compile(
r"^\*+$")
685 outlines = stdout.splitlines()
686 nlines = len(outlines)
692 while i < nlines
and not stars.match(outlines[i]):
697 trees[tree[
"Name"]] = tree
703 Check that all the keys in reference are in to_check too, with the same value.
704 If the value is a dict, the function is called recursively. to_check can
705 contain more keys than reference, that will not be tested.
706 The function returns at the first difference found.
711 ignore_re = re.compile(ignore)
712 keys = [ key
for key
in reference
if not ignore_re.match(key) ]
714 keys = reference.keys()
718 if (
type(reference[k])
is dict)
and (
type(to_check[k])
is dict):
720 failed = fail_keys =
cmpTreesDicts(reference[k], to_check[k], ignore)
723 failed = to_check[k] != reference[k]
728 fail_keys.insert(0, k)
738 if c
is None or r
is None:
740 return (fail_path, r, c)
743 h_count_re = re.compile(
r"^(.*)SUCCESS\s+Booked (\d+) Histogram\(s\) :\s+(.*)")
747 Extract the histograms infos from the lines starting at pos.
748 Returns the position of the first line after the summary block.
751 h_table_head = re.compile(
r'SUCCESS\s+List of booked (1D|2D|3D|1D profile|2D profile) histograms in directory\s+"(\w*)"')
752 h_short_summ = re.compile(
r"ID=([^\"]+)\s+\"([^\"]+)\"\s+(.*)")
757 m = h_count_re.search(lines[pos])
758 name = m.group(1).strip()
759 total = int(m.group(2))
761 for k, v
in [ x.split(
"=")
for x
in m.group(3).split() ]:
764 header[
"Total"] = total
768 m = h_table_head.search(lines[pos])
771 t = t.replace(
" profile",
"Prof")
778 if l.startswith(
" | ID"):
780 titles = [ x.strip()
for x
in l.split(
"|")][1:]
782 while pos < nlines
and lines[pos].startswith(
" |"):
784 values = [ x.strip()
for x
in l.split(
"|")][1:]
786 for i
in range(len(titles)):
787 hcont[titles[i]] = values[i]
788 cont[hcont[
"ID"]] = hcont
790 elif l.startswith(
" ID="):
791 while pos < nlines
and lines[pos].startswith(
" ID="):
792 values = [ x.strip()
for x
in h_short_summ.search(lines[pos]).groups() ]
793 cont[values[0]] = values
796 raise RuntimeError(
"Cannot understand line %d: '%s'" % (pos, l))
800 summ[d][
"header"] = header
805 summ[name] = {
"header": header}
810 Scan stdout to find ROOT TTree summaries and digest them.
812 outlines = stdout.splitlines()
813 nlines = len(outlines) - 1
821 match = h_count_re.search(outlines[pos])
822 while pos < nlines
and not match:
824 match = h_count_re.search(outlines[pos])
827 summaries.update(summ)
832 """Create a new 'Filter'.
834 'input' -- The string containing the input to provide to the
837 'timeout' -- As for 'TimeoutExecutable.__init__'."""
839 super(GaudiFilterExecutable, self).
__init__(input, timeout)
846 tmpf = tempfile.mkstemp()
851 """Copied from TimeoutExecutable to allow the re-implementation of
854 if sys.platform ==
"win32":
865 """Code copied from both FilterExecutable and TimeoutExecutable.
869 self._ClosePipeEnd(self._stdin_pipe[0])
870 if self._stdout_pipe:
871 self._ClosePipeEnd(self._stdout_pipe[1])
872 if self._stderr_pipe:
873 self._ClosePipeEnd(self._stderr_pipe[1])
881 super(qm.executable.TimeoutExecutable, self).
_HandleChild()
888 child_pid = self._GetChildPID()
890 os.setpgid(child_pid, child_pid)
918 os.setpgid(0, child_pid)
927 max_fds = os.sysconf(
"SC_OPEN_MAX")
930 for fd
in xrange(max_fds):
941 if sys.platform ==
"linux2":
943 os.path.join(
"/proc", str(child_pid),
"exe"),
945 "-batch",
"-n",
"-x",
946 "'%s'" % os.path.join(os.path.dirname(__file__),
"stack-trace.gdb")]
949 o = os.popen(
" ".join(cmd)).read()
954 os.kill(0, signal.SIGKILL)
957 select.select ([], [], [])
962 elif self.
__timeout >= 0
and sys.platform ==
"win32":
965 self.__monitor_thread.start()
967 if sys.platform ==
"win32":
970 """Code copied from FilterExecutable.
971 Kill the child if the timeout expires.
973 This function is run in the monitoring thread."""
981 result = win32event.WaitForSingleObject(self._GetChildPID(),
984 if result == win32con.WAIT_TIMEOUT:
991 """Standard Gaudi test.
998 description=
"""The path to the program.
1000 This field indicates the path to the program. If it is not
1001 an absolute path, the value of the 'PATH' environment
1002 variable will be used to search for the program.
1003 If not specified, $GAUDIEXE or Gaudi.exe are used.
1006 qm.fields.SetField(qm.fields.TextField(
1008 title=
"Argument List",
1009 description=
"""The command-line arguments.
1011 If this field is left blank, the program is run without any
1014 Use this field to specify the option files.
1016 An implicit 0th argument (the path to the program) is added
1019 qm.fields.TextField(
1022 description=
"""Options to be passed to the application.
1024 This field allows to pass a list of options to the main program
1025 without the need of a separate option file.
1027 The content of the field is written to a temporary file which name
1028 is passed the the application as last argument (appended to the
1029 field "Argument List".
1035 qm.fields.TextField(
1037 title=
"Working Directory",
1038 description=
"""Path to the working directory.
1040 If this field is left blank, the program will be run from the qmtest
1041 directory, otherwise from the directory specified.""",
1044 qm.fields.TextField(
1046 title=
"Reference Output",
1047 description=
"""Path to the file containing the reference output.
1049 If this field is left blank, any standard output will be considered
1052 If the reference file is specified, any output on standard error is
1055 qm.fields.TextField(
1056 name=
"error_reference",
1057 title=
"Reference for standard error",
1058 description=
"""Path to the file containing the reference for the standard error.
1060 If this field is left blank, any standard output will be considered
1063 If the reference file is specified, any output on standard error is
1066 qm.fields.SetField(qm.fields.TextField(
1067 name =
"unsupported_platforms",
1068 title =
"Unsupported Platforms",
1069 description =
"""Platform on which the test must not be run.
1071 List of regular expressions identifying the platforms on which the
1072 test is not run and the result is set to UNTESTED."""
1075 qm.fields.TextField(
1077 title =
"Validator",
1078 description =
"""Function to validate the output of the test.
1080 If defined, the function is used to validate the products of the
1082 The function is called passing as arguments:
1083 self: the test class instance
1084 stdout: the standard output of the executed test
1085 stderr: the standard error of the executed test
1086 result: the Result objects to fill with messages
1087 The function must return a list of causes for the failure.
1088 If specified, overrides standard output, standard error and
1096 qm.fields.BooleanField(
1097 name =
"use_temp_dir",
1098 title =
"Use temporary directory",
1099 description =
"""Use temporary directory.
1101 If set to true, use a temporary directory as working directory.
1103 default_value=
"false"
1106 qm.fields.IntegerField(
1108 title =
"Expected signal",
1109 description =
"""Expect termination by signal.""",
1116 unsupported = [ re.compile(x)
1117 for x
in [ str(y).strip()
1118 for y
in self.unsupported_platforms ]
1121 for p_re
in unsupported:
1122 if p_re.search(platform):
1123 result.SetOutcome(result.UNTESTED)
1124 result[result.CAUSE] =
'Platform not supported.'
1130 Return the platform Id defined in CMTCONFIG or SCRAM_ARCH.
1134 if "CMTCONFIG" in os.environ:
1135 arch = os.environ[
"CMTCONFIG"]
1136 elif "SCRAM_ARCH" in os.environ:
1137 arch = os.environ[
"SCRAM_ARCH"]
1142 Return True if the current platform is Windows.
1144 This function was needed because of the change in the CMTCONFIG format,
1145 from win32_vc71_dbg to i686-winxp-vc9-dbg.
1148 return "winxp" in platform
or platform.startswith(
"win")
1156 platformSplit =
lambda p: set(p.split(
'-' in p
and '-' or '_'))
1158 reference = os.path.normpath(os.path.expandvars(reffile))
1160 spec_ref = reference[:-3] + self.
GetPlatform()[0:3] + reference[-3:]
1161 if os.path.isfile(spec_ref):
1162 reference = spec_ref
1165 dirname, basename = os.path.split(reference)
1166 if not dirname: dirname =
'.'
1167 head = basename +
"."
1168 head_len = len(head)
1171 for f
in os.listdir(dirname):
1172 if f.startswith(head):
1173 req_plat = platformSplit(f[head_len:])
1174 if platform.issuperset(req_plat):
1175 candidates.append( (len(req_plat), f) )
1180 reference = os.path.join(dirname, candidates[-1][1])
1185 ignore =
r"Basket|.*size|Compression"):
1187 Compare the TTree summaries in stdout with the ones in trees_dict or in
1188 the reference file. By default ignore the size, compression and basket
1190 The presence of TTree summaries when none is expected is not a failure.
1192 if trees_dict
is None:
1195 if reference
and os.path.isfile(reference):
1200 from pprint
import PrettyPrinter
1201 pp = PrettyPrinter()
1203 result[
"GaudiTest.TTrees.expected"] = result.Quote(pp.pformat(trees_dict))
1205 result[
"GaudiTest.TTrees.ignore"] = result.Quote(ignore)
1210 causes.append(
"trees summaries")
1212 result[
"GaudiTest.TTrees.failure_on"] = result.Quote(msg)
1213 result[
"GaudiTest.TTrees.found"] = result.Quote(pp.pformat(trees))
1221 Compare the TTree summaries in stdout with the ones in trees_dict or in
1222 the reference file. By default ignore the size, compression and basket
1224 The presence of TTree summaries when none is expected is not a failure.
1229 if reference
and os.path.isfile(reference):
1234 from pprint
import PrettyPrinter
1235 pp = PrettyPrinter()
1237 result[
"GaudiTest.Histos.expected"] = result.Quote(pp.pformat(dict))
1239 result[
"GaudiTest.Histos.ignore"] = result.Quote(ignore)
1244 causes.append(
"histos summaries")
1246 result[
"GaudiTest.Histos.failure_on"] = result.Quote(msg)
1247 result[
"GaudiTest.Histos.found"] = result.Quote(pp.pformat(histos))
1253 Default validation action: compare standard output and error to the
1258 preproc = normalizeExamples
1262 if reference
and os.path.isfile(reference):
1263 result[
"GaudiTest.output_reference"] = reference
1266 "GaudiTest.output_diff",
1267 preproc = preproc)(stdout, result)
1275 newref = open(reference +
".new",
"w")
1277 for l
in stdout.splitlines():
1278 newref.write(l.rstrip() +
'\n')
1288 if reference
and os.path.isfile(reference):
1289 result[
"GaudiTest.error_reference"] = reference
1292 "GaudiTest.error_diff",
1293 preproc = preproc)(stderr, result)
1296 newref = open(reference +
".new",
"w")
1298 for l
in stderr.splitlines():
1299 newref.write(l.rstrip() +
'\n')
1304 "ExecTest.expected_stderr")(stderr, result)
1311 if self.validator.strip() !=
"":
1312 class CallWrapper(object):
1314 Small wrapper class to dynamically bind some default arguments
1317 def __init__(self, callable, extra_args = {}):
1321 from inspect
import getargspec
1327 def __call__(self, *args, **kwargs):
1331 kwargs = dict(kwargs)
1335 if a
not in positional
and a
not in kwargs:
1337 return apply(self.
callable, args, kwargs)
1339 exported_symbols = {
"self":self,
1344 "findReferenceBlock":
1345 CallWrapper(findReferenceBlock, {
"stdout":stdout,
1348 "validateWithReference":
1354 CallWrapper(countErrorLines, {
"stdout":stdout,
1357 "checkTTreesSummaries":
1361 "checkHistosSummaries":
1367 exec self.validator
in globals(), exported_symbols
1375 Add the content of the environment to the result object.
1377 Copied from the QMTest class of COOL.
1379 vars = os.environ.keys()
1381 result[
'GaudiTest.environment'] = \
1382 result.Quote(
'\n'.join([
"%s=%s"%(v,os.environ[v])
for v
in vars]))
1384 def Run(self, context, result):
1387 'context' -- A 'Context' giving run-time parameters to the
1390 'result' -- A 'Result' object. The outcome will be
1391 'Result.PASS' when this method is called. The 'result' may be
1392 modified by this method to indicate outcomes other than
1393 'Result.PASS' or to add annotations."""
1402 elif "GAUDIEXE" in os.environ:
1403 prog = os.environ[
"GAUDIEXE"]
1408 dummy, prog_ext = os.path.splitext(prog)
1409 if prog_ext
not in [
".exe",
".py",
".bat" ]
and self.
isWinPlatform():
1413 prog =
which(prog)
or prog
1416 args = map(rationalizepath, self.args)
1423 if self.options.strip():
1425 if re.search(
r"from\s*Gaudi.Configuration\s*import\s*\*", self.options):
1428 tmpfile.writelines(
"\n".join(self.options.splitlines()))
1430 args.append(tmpfile.name)
1431 result[
"GaudiTest.options"] = result.Quote(self.options)
1434 if prog_ext ==
".py":
1437 prog =
which(
"python.exe")
or "python.exe"
1439 prog =
which(
"python")
or "python"
1442 origdir = os.getcwd()
1444 os.chdir(str(os.path.normpath(os.path.expandvars(self.workdir))))
1446 if "QMTEST_TMPDIR" in os.environ:
1447 qmtest_tmpdir = os.environ[
"QMTEST_TMPDIR"]
1448 if not os.path.exists(qmtest_tmpdir):
1449 os.makedirs(qmtest_tmpdir)
1450 os.chdir(qmtest_tmpdir)
1451 elif "qmtest.tmpdir" in context:
1452 os.chdir(context[
"qmtest.tmpdir"])
1454 if "QMTEST_IGNORE_TIMEOUT" not in os.environ:
1467 if result.GetOutcome()
not in [ result.PASS ]:
1474 """Run the 'program'.
1476 'program' -- The path to the program to run.
1478 'arguments' -- A list of the arguments to the program. This
1479 list must contain a first argument corresponding to 'argv[0]'.
1481 'context' -- A 'Context' giving run-time parameters to the
1484 'result' -- A 'Result' object. The outcome will be
1485 'Result.PASS' when this method is called. The 'result' may be
1486 modified by this method to indicate outcomes other than
1487 'Result.PASS' or to add annotations.
1489 @attention: This method has been copied from command.ExecTestBase
1490 (QMTest 2.3.0) and modified to keep stdout and stderr
1491 for tests that have been terminated by a signal.
1492 (Fundamental for debugging in the Application Area)
1496 environment = self.MakeEnvironment(context)
1498 if "slc6" in environment.get(
'CMTCONFIG',
''):
1499 environment[
'TERM'] =
'dumb'
1512 exit_status = e.Run(arguments, environment, path = program)
1514 if e.stack_trace_file
and os.path.exists(e.stack_trace_file):
1515 stack_trace = open(e.stack_trace_file).read()
1516 os.remove(e.stack_trace_file)
1520 result[
"ExecTest.stack_trace"] = result.Quote(stack_trace)
1522 print "===================", exit_status, os.WIFEXITED(exit_status)
1524 if (sys.platform ==
"win32" or os.WIFEXITED(exit_status)
1530 if self.exit_code
is None:
1532 elif sys.platform ==
"win32":
1533 exit_code = exit_status
1535 exit_code = os.WEXITSTATUS(exit_status)
1540 result[
"ExecTest.exit_code"] = str(exit_code)
1541 result[
"ExecTest.stdout"] = result.Quote(stdout)
1542 result[
"ExecTest.stderr"] = result.Quote(stderr)
1544 if exit_code != self.exit_code:
1545 causes.append(
"exit_code")
1546 result[
"ExecTest.expected_exit_code"] \
1547 = str(self.exit_code)
1552 result.Fail(
"Unexpected %s." % string.join(causes,
", "))
1553 elif os.WIFSIGNALED(exit_status):
1556 signal_number = str(os.WTERMSIG(exit_status))
1558 result.Fail(
"Program terminated by signal.")
1562 result.Fail(
"Exceeded time limit (%ds), terminated." % timeout)
1563 result[
"ExecTest.signal_number"] = signal_number
1564 result[
"ExecTest.stdout"] = result.Quote(e.stdout)
1565 result[
"ExecTest.stderr"] = result.Quote(e.stderr)
1567 result[
"ExecTest.expected_signal_number"] = str(self.
signal)
1568 elif os.WIFSTOPPED(exit_status):
1571 signal_number = str(os.WSTOPSIG(exit_status))
1573 result.Fail(
"Program stopped by signal.")
1577 result.Fail(
"Exceeded time limit (%ds), stopped." % timeout)
1578 result[
"ExecTest.signal_number"] = signal_number
1579 result[
"ExecTest.stdout"] = result.Quote(e.stdout)
1580 result[
"ExecTest.stderr"] = result.Quote(e.stderr)
1584 result.Fail(
"Program did not terminate normally.")
1590 result[
"ExecTest.stdout"] = result[
"ExecTest.stdout"].replace(esc,repr_esc)
1597 projbasedir = os.path.normpath(destdir)
1598 while not os.path.exists(os.path.join(projbasedir,
".project")):
1599 oldprojdir = projbasedir
1600 projbasedir = os.path.normpath(os.path.join(projbasedir, os.pardir))
1603 if oldprojdir == projbasedir:
1607 if not os.path.exists(destdir):
1608 os.makedirs(destdir)
1610 from xml.etree
import ElementTree
as ET
1611 t = ET.parse(os.path.join(projbasedir,
".project"))
1612 projectName = t.find(
"name").text
1615 destfile =
"%s.launch" % self._Runnable__id
1617 destfile = os.path.join(destdir, destfile)
1619 if self.options.strip():
1623 tempfile = args.pop()
1624 optsfile = destfile + os.path.splitext(tempfile)[1]
1625 shutil.copyfile(tempfile, optsfile)
1626 args.append(optsfile)
1629 from xml.sax.saxutils
import quoteattr
1633 data[
"environment"] =
"\n".join([
'<mapEntry key=%s value=%s/>' % (quoteattr(k), quoteattr(v))
1634 for k, v
in os.environ.iteritems()
1635 if k
not in (
'MAKEOVERRIDES',
'MAKEFLAGS',
'MAKELEVEL')])
1637 data[
"exec"] =
which(prog)
or prog
1638 if os.path.basename(data[
"exec"]).lower().startswith(
"python"):
1639 data[
"stopAtMain"] =
"false"
1641 data[
"stopAtMain"] =
"true"
1643 data[
"args"] =
" ".join(map(rationalizepath, args))
1645 data[
"args"] =
" ".join([
"/debugexe"] + map(rationalizepath, [data[
"exec"]] + args))
1646 data[
"exec"] =
which(
"vcexpress.exe")
1649 data[
"workdir"] = os.getcwd()
1653 data[
"workdir"] = destdir
1655 data[
"project"] = projectName.strip()
1658 xml =
"""<?xml version="1.0" encoding="UTF-8" standalone="no"?>
1659 <launchConfiguration type="org.eclipse.cdt.launch.applicationLaunchType">
1660 <booleanAttribute key="org.eclipse.cdt.debug.mi.core.AUTO_SOLIB" value="true"/>
1661 <listAttribute key="org.eclipse.cdt.debug.mi.core.AUTO_SOLIB_LIST"/>
1662 <stringAttribute key="org.eclipse.cdt.debug.mi.core.DEBUG_NAME" value="gdb"/>
1663 <stringAttribute key="org.eclipse.cdt.debug.mi.core.GDB_INIT" value=".gdbinit"/>
1664 <listAttribute key="org.eclipse.cdt.debug.mi.core.SOLIB_PATH"/>
1665 <booleanAttribute key="org.eclipse.cdt.debug.mi.core.STOP_ON_SOLIB_EVENTS" value="false"/>
1666 <booleanAttribute key="org.eclipse.cdt.debug.mi.core.breakpointsFullPath" value="false"/>
1667 <stringAttribute key="org.eclipse.cdt.debug.mi.core.commandFactory" value="org.eclipse.cdt.debug.mi.core.standardCommandFactory"/>
1668 <stringAttribute key="org.eclipse.cdt.debug.mi.core.protocol" value="mi"/>
1669 <booleanAttribute key="org.eclipse.cdt.debug.mi.core.verboseMode" value="false"/>
1670 <intAttribute key="org.eclipse.cdt.launch.ATTR_BUILD_BEFORE_LAUNCH_ATTR" value="0"/>
1671 <stringAttribute key="org.eclipse.cdt.launch.COREFILE_PATH" value=""/>
1672 <stringAttribute key="org.eclipse.cdt.launch.DEBUGGER_ID" value="org.eclipse.cdt.debug.mi.core.CDebuggerNew"/>
1673 <stringAttribute key="org.eclipse.cdt.launch.DEBUGGER_REGISTER_GROUPS" value=""/>
1674 <stringAttribute key="org.eclipse.cdt.launch.DEBUGGER_START_MODE" value="run"/>
1675 <booleanAttribute key="org.eclipse.cdt.launch.DEBUGGER_STOP_AT_MAIN" value="%(stopAtMain)s"/>
1676 <stringAttribute key="org.eclipse.cdt.launch.DEBUGGER_STOP_AT_MAIN_SYMBOL" value="main"/>
1677 <booleanAttribute key="org.eclipse.cdt.launch.ENABLE_REGISTER_BOOKKEEPING" value="false"/>
1678 <booleanAttribute key="org.eclipse.cdt.launch.ENABLE_VARIABLE_BOOKKEEPING" value="false"/>
1679 <stringAttribute key="org.eclipse.cdt.launch.FORMAT" value="<?xml version="1.0" encoding="UTF-8" standalone="no"?><contentList/>"/>
1680 <stringAttribute key="org.eclipse.cdt.launch.GLOBAL_VARIABLES" value="<?xml version="1.0" encoding="UTF-8" standalone="no"?> <globalVariableList/> "/>
1681 <stringAttribute key="org.eclipse.cdt.launch.MEMORY_BLOCKS" value="<?xml version="1.0" encoding="UTF-8" standalone="no"?> <memoryBlockExpressionList/> "/>
1682 <stringAttribute key="org.eclipse.cdt.launch.PROGRAM_ARGUMENTS" value="%(args)s"/>
1683 <stringAttribute key="org.eclipse.cdt.launch.PROGRAM_NAME" value="%(exec)s"/>
1684 <stringAttribute key="org.eclipse.cdt.launch.PROJECT_ATTR" value="%(project)s"/>
1685 <stringAttribute key="org.eclipse.cdt.launch.PROJECT_BUILD_CONFIG_ID_ATTR" value=""/>
1686 <stringAttribute key="org.eclipse.cdt.launch.WORKING_DIRECTORY" value="%(workdir)s"/>
1687 <booleanAttribute key="org.eclipse.cdt.launch.ui.ApplicationCDebuggerTab.DEFAULTS_SET" value="true"/>
1688 <booleanAttribute key="org.eclipse.cdt.launch.use_terminal" value="true"/>
1689 <listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_PATHS">
1690 <listEntry value="/%(project)s"/>
1692 <listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_TYPES">
1693 <listEntry value="4"/>
1695 <booleanAttribute key="org.eclipse.debug.core.appendEnvironmentVariables" value="false"/>
1696 <mapAttribute key="org.eclipse.debug.core.environmentVariables">
1699 <mapAttribute key="org.eclipse.debug.core.preferred_launchers">
1700 <mapEntry key="[debug]" value="org.eclipse.cdt.cdi.launch.localCLaunch"/>
1702 <listAttribute key="org.eclipse.debug.ui.favoriteGroups">
1703 <listEntry value="org.eclipse.debug.ui.launchGroup.debug"/>
1705 </launchConfiguration>
1709 open(destfile,
"w").write(xml)
1717 import simplejson
as json
1720 """An 'HTMLResultStream' writes its output to a set of HTML files.
1722 The argument 'dir' is used to select the destination directory for the HTML
1724 The destination directory may already contain the report from a previous run
1725 (for example of a different package), in which case it will be extended to
1726 include the new data.
1729 qm.fields.TextField(
1731 title =
"Destination Directory",
1732 description =
"""The name of the directory.
1734 All results will be written to the directory indicated.""",
1736 default_value =
""),
1740 """Prepare the destination directory.
1742 Creates the destination directory and store in it some preliminary
1743 annotations and the static files found in the template directory
1746 ResultStream.__init__(self, arguments, **args)
1751 templateDir = os.path.join(os.path.dirname(__file__),
"html_report")
1752 if not os.path.isdir(self.dir):
1753 os.makedirs(self.dir)
1755 for f
in os.listdir(templateDir):
1756 src = os.path.join(templateDir, f)
1757 dst = os.path.join(self.dir, f)
1758 if not os.path.isdir(src)
and not os.path.exists(dst):
1759 shutil.copy(src, dst)
1761 if "CMTCONFIG" in os.environ:
1767 """Helper function to extend the global summary file in the destination
1774 ids = set([ i[
"id"]
for i
in self.
_summary ])
1775 newSummary = [ i
for i
in oldSummary
if i[
"id"]
not in ids ]
1781 """Writes the annotation to the annotation file.
1782 If the key is already present with a different value, the value becomes
1783 a list and the new value is appended to it, except for start_time and
1792 key, value = map(str, [key, value])
1793 if key ==
"qmtest.run.start_time":
1798 if key
not in annotations:
1799 annotations[key] = value
1800 if "qmtest.run.end_time" in annotations:
1801 del annotations[
"qmtest.run.end_time"]
1804 if key
in annotations:
1805 old = annotations[key]
1806 if type(old)
is list:
1807 if value
not in old:
1808 annotations[key].append(value)
1810 annotations[key] = [old, value]
1812 annotations[key] = value
1818 """Prepare the test result directory in the destination directory storing
1819 into it the result fields.
1820 A summary of the test result is stored both in a file in the test directory
1821 and in the global summary file.
1824 summary[
"id"] = result.GetId()
1825 summary[
"outcome"] = result.GetOutcome()
1826 summary[
"cause"] = result.GetCause()
1827 summary[
"fields"] = result.keys()
1828 summary[
"fields"].sort()
1831 for f
in [
"id",
"outcome",
"cause"]:
1832 summary[f] = str(summary[f])
1833 summary[
"fields"] = map(str, summary[
"fields"])
1835 self._summary.append(summary)
1841 testOutDir = os.path.join(self.dir, summary[
"id"])
1842 if not os.path.isdir(testOutDir):
1843 os.makedirs(testOutDir)
1844 json.dump(summary, open(os.path.join(testOutDir,
"summary.json"),
"w"),
1846 for f
in summary[
"fields"]:
1847 open(os.path.join(testOutDir, f),
"w").write(result[f])