5 __author__ =
'Marco Clemencic CERN/PH-LBC'
16 from subprocess
import Popen, PIPE, STDOUT
19 os.environ[
'LC_ALL'] =
'C'
22 from qm.test.classes.command
import ExecTestBase
23 from qm.test.result_stream
import ResultStream
30 if sys.platform ==
"win32":
33 from threading
import *
51 Class to changes the environment temporarily.
53 def __init__(self, orig = os.environ, keep_same = False):
55 Create a temporary environment on top of the one specified
56 (it can be another TemporaryEnvironment instance).
65 Set an environment variable recording the previous value.
77 Get an environment variable.
78 Needed to provide the same interface as os.environ.
84 Unset an environment variable.
85 Needed to provide the same interface as os.environ.
87 if key
not in self.
env :
94 Return the list of defined environment variables.
95 Needed to provide the same interface as os.environ.
97 return self.env.keys()
101 Return the list of (name,value) pairs for the defined environment variables.
102 Needed to provide the same interface as os.environ.
104 return self.env.items()
109 Needed to provide the same interface as os.environ.
111 return key
in self.
env
115 Revert all the changes done to the orignal environment.
117 for key,value
in self.old_values.items():
121 self.
env[key] = value
126 Revert the changes on destruction.
133 Generate a shell script to reproduce the changes in the environment.
135 shells = [
'csh',
'sh',
'bat' ]
136 if shell_type
not in shells:
137 raise RuntimeError(
"Shell type '%s' unknown. Available: %s"%(shell_type,shells))
139 for key,value
in self.old_values.items():
140 if key
not in self.
env:
142 if shell_type ==
'csh':
143 out +=
'unsetenv %s\n'%key
144 elif shell_type ==
'sh':
145 out +=
'unset %s\n'%key
146 elif shell_type ==
'bat':
147 out +=
'set %s=\n'%key
150 if shell_type ==
'csh':
151 out +=
'setenv %s "%s"\n'%(key,self.
env[key])
152 elif shell_type ==
'sh':
153 out +=
'export %s="%s"\n'%(key,self.
env[key])
154 elif shell_type ==
'bat':
155 out +=
'set %s=%s\n'%(key,self.
env[key])
159 """Small class for temporary directories.
160 When instantiated, it creates a temporary directory and the instance
161 behaves as the string containing the directory name.
162 When the instance goes out of scope, it removes all the content of
163 the temporary directory (automatic clean-up).
180 shutil.rmtree(self.
name)
183 return getattr(self.
name,attr)
186 """Small class for temporary files.
187 When instantiated, it creates a temporary directory and the instance
188 behaves as the string containing the directory name.
189 When the instance goes out of scope, it removes all the content of
190 the temporary directory (automatic clean-up).
192 def __init__(self, suffix='', prefix='tmp', dir=None, text=False, keep = False):
197 self._fd, self.
name = tempfile.mkstemp(suffix,prefix,dir,text)
198 self.
file = os.fdopen(self._fd,
"r+")
210 return getattr(self.
file,attr)
213 """Small wrapper to call CMT.
222 if type(args)
is str:
224 cmd =
"cmt %s"%command
232 result = os.popen4(cmd)[1].read()
238 return lambda args=[]: self.
_run_cmt(attr, args)
241 """Returns a dictionary containing the runtime environment produced by CMT.
242 If a dictionary is passed a modified instance of it is returned.
246 for l
in self.setup(
"-csh").splitlines():
248 if l.startswith(
"setenv"):
249 dummy,name,value = l.split(
None,3)
250 env[name] = value.strip(
'"')
251 elif l.startswith(
"unsetenv"):
252 dummy,name = l.split(
None,2)
257 r = self.show([
"macro",k])
258 if r.find(
"CMT> Error: symbol not found") >= 0:
261 return self.show([
"macro_value",k]).strip()
268 Locates an executable in the executables path ($PATH) and returns the full
269 path to it. An application is looked for with or without the '.exe' suffix.
270 If the executable cannot be found, None is returned
272 if os.path.isabs(executable):
273 if not os.path.exists(executable):
274 if executable.endswith(
'.exe'):
275 if os.path.exists(executable[:-4]):
276 return executable[:-4]
278 for d
in os.environ.get(
"PATH").split(os.pathsep):
279 fullpath = os.path.join(d, executable)
280 if os.path.exists(fullpath):
282 if executable.endswith(
'.exe'):
283 return which(executable[:-4])
287 p = os.path.normpath(os.path.expandvars(p))
288 if os.path.exists(p):
289 p = os.path.realpath(p)
296 """Basic implementation of an option validator for Gaudi tests.
297 This implementation is based on the standard (LCG) validation functions
306 """Validate the output of the program.
308 'stdout' -- A string containing the data written to the standard output
311 'stderr' -- A string containing the data written to the standard error
314 'result' -- A 'Result' object. It may be used to annotate
315 the outcome according to the content of stderr.
317 returns -- A list of strings giving causes of failure."""
322 causes.append(self.
cause)
328 """Compare 's1' and 's2', ignoring line endings.
334 returns -- True if 's1' and 's2' are the same, ignoring
335 differences in line endings."""
339 return s1.splitlines() == s2.splitlines()
342 """ Base class for a callable that takes a file and returns a modified
347 if hasattr(input,
"__iter__"):
351 lines = input.splitlines()
356 if l: output.append(l)
357 if mergeback: output =
'\n'.join(output)
381 if line.find(s) >= 0:
return None
383 if r.search(line):
return None
393 if self.
start in line:
396 elif self.
end in line:
405 when = re.compile(when)
408 if isinstance(rhs, RegexpReplacer):
410 res._operations = self.
_operations + rhs._operations
412 res = FilePreprocessor.__add__(self, rhs)
416 if w
is None or w.search(line):
417 line = o.sub(r, line)
422 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)?",
423 "00:00:00 1970-01-01")
425 normalizeEOL.__processLine__ =
lambda line: str(line).rstrip() +
'\n'
429 skipEmptyLines.__processLine__ =
lambda line: (line.strip()
and line)
or None
440 line = line[:(pos+self.
siglen)]
441 lst = line[(pos+self.
siglen):].split()
443 line +=
" ".join(lst)
447 normalizeExamples = maskPointers + normalizeDate
450 (
"TIMER.TIMER",
r"\s+[+-]?[0-9]+[0-9.]*",
" 0"),
451 (
"release all pending",
r"^.*/([^/]*:.*)",
r"\1"),
452 (
"0x########",
r"\[.*/([^/]*.*)\]",
r"[\1]"),
453 (
"^#.*file",
r"file '.*[/\\]([^/\\]*)$",
r"file '\1"),
454 (
"^JobOptionsSvc.*options successfully read in from",
r"read in from .*[/\\]([^/\\]*)$",
r"file \1"),
456 (
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"),
458 (
"ServiceLocatorHelper::",
"ServiceLocatorHelper::(create|locate)Service",
"ServiceLocatorHelper::service"),
460 (
None,
r"e([-+])0([0-9][0-9])",
r"e\1\2"),
464 "JobOptionsSvc INFO # ",
465 "JobOptionsSvc WARNING # ",
468 "This machine has a speed",
471 "ToolSvc.Sequenc... INFO",
472 "DataListenerSvc INFO XML written to file:",
473 "[INFO]",
"[WARNING]",
474 "DEBUG No writable file catalog found which contains FID:",
476 "DEBUG Service base class initialized successfully",
477 "DEBUG Incident timing:",
478 "INFO 'CnvServices':[",
480 'Note: (file "(tmpfile)", line 2) File "set" already loaded',
484 r"^JobOptionsSvc INFO *$",
486 r"(Always|SUCCESS)\s*(Root f|[^ ]* F)ile version:",
487 r"0x[0-9a-fA-F#]+ *Algorithm::sysInitialize\(\) *\[",
488 r"0x[0-9a-fA-F#]* *__gxx_personality_v0 *\[",
489 r"File '.*.xml' does not exist",
490 r"INFO Refer to dataset .* by its file ID:",
491 r"INFO Referring to dataset .* by its file ID:",
492 r"INFO Disconnect from dataset",
493 r"INFO Disconnected from dataset",
494 r"INFO Disconnected data IO:",
495 r"IncidentSvc\s*(DEBUG (Adding|Removing)|VERBOSE Calling)",
497 r"^StatusCodeSvc.*listing all unchecked return codes:",
498 r"^StatusCodeSvc\s*INFO\s*$",
499 r"Num\s*\|\s*Function\s*\|\s*Source Library",
502 r"ERROR Failed to modify file: .* Errno=2 No such file or directory",
504 r"^ +[0-9]+ \|.*ROOT",
505 r"^ +[0-9]+ \|.*\|.*Dict",
509 r"SUCCESS\s*Booked \d+ Histogram\(s\)",
512 ] ) + normalizeExamples + skipEmptyLines + \
517 def __init__(self, reffile, cause, result_key, preproc = normalizeExamples):
524 if os.path.isfile(self.
reffile):
525 orig = open(self.
reffile).xreadlines()
531 new = stdout.splitlines()
535 diffs = difflib.ndiff(orig,new,charjunk=difflib.IS_CHARACTER_JUNK)
536 filterdiffs =
map(
lambda x: x.strip(),filter(
lambda x: x[0] !=
" ",diffs))
539 result[self.
result_key] = result.Quote(
"\n".join(filterdiffs))
543 +) standard output of the test""")
544 causes.append(self.
cause)
551 def findReferenceBlock(reference, stdout, result, causes, signature_offset=0, signature=None,
554 Given a block of text, tries to find it in the output.
555 The block had to be identified by a signature line. By default, the first
556 line is used as signature, or the line pointed to by signature_offset. If
557 signature_offset points outside the block, a signature line can be passed as
558 signature argument. Note: if 'signature' is None (the default), a negative
559 signature_offset is interpreted as index in a list (e.g. -1 means the last
560 line), otherwise the it is interpreted as the number of lines before the
561 first one of the block the signature must appear.
562 The parameter 'id' allow to distinguish between different calls to this
563 function in the same validation code.
566 reflines = filter(
None,
map(
lambda s: s.rstrip(), reference.splitlines()))
568 raise RuntimeError(
"Empty (or null) reference")
570 outlines = filter(
None,
map(
lambda s: s.rstrip(), stdout.splitlines()))
572 res_field =
"GaudiTest.RefBlock"
574 res_field +=
"_%s" % id
576 if signature
is None:
577 if signature_offset < 0:
578 signature_offset = len(reference)+signature_offset
579 signature = reflines[signature_offset]
582 pos = outlines.index(signature)
583 outlines = outlines[pos-signature_offset:pos+len(reflines)-signature_offset]
584 if reflines != outlines:
585 msg =
"standard output"
587 if not msg
in causes:
589 result[res_field +
".observed"] = result.Quote(
"\n".join(outlines))
591 causes.append(
"missing signature")
592 result[res_field +
".signature"] = result.Quote(signature)
593 if len(reflines) > 1
or signature != reflines[0]:
594 result[res_field +
".expected"] = result.Quote(
"\n".join(reflines))
600 Count the number of messages with required severity (by default ERROR and FATAL)
601 and check if their numbers match the expected ones (0 by default).
602 The dictionary "expected" can be used to tune the number of errors and fatals
603 allowed, or to limit the number of expected warnings etc.
605 stdout = kwargs[
"stdout"]
606 result = kwargs[
"result"]
607 causes = kwargs[
"causes"]
614 outlines = stdout.splitlines()
615 from math
import log10
616 fmt =
"%%%dd - %%s" % (int(log10(len(outlines))+1))
622 if len(words) >= 2
and words[1]
in errors:
623 errors[words[1]].append(fmt%(linecount,l.rstrip()))
626 if len(errors[e]) != expected[e]:
627 causes.append(
'%s(%d)'%(e,len(errors[e])))
628 result[
"GaudiTest.lines.%s"%e] = result.Quote(
'\n'.join(errors[e]))
629 result[
"GaudiTest.lines.%s.expected#"%e] = result.Quote(str(expected[e]))
636 Parse the TTree summary table in lines, starting from pos.
637 Returns a tuple with the dictionary with the digested informations and the
638 position of the first line after the summary.
644 splitcols =
lambda l: [ f.strip()
for f
in l.strip(
"*\n").split(
':',2) ]
647 cols = splitcols(ll[0])
648 r[
"Name"], r[
"Title"] = cols[1:]
650 cols = splitcols(ll[1])
651 r[
"Entries"] = int(cols[1])
653 sizes = cols[2].split()
654 r[
"Total size"] = int(sizes[2])
655 if sizes[-1] ==
"memory":
658 r[
"File size"] = int(sizes[-1])
660 cols = splitcols(ll[2])
661 sizes = cols[2].split()
662 if cols[0] ==
"Baskets":
663 r[
"Baskets"] = int(cols[1])
664 r[
"Basket size"] = int(sizes[2])
665 r[
"Compression"] = float(sizes[-1])
668 if i < (count - 3)
and lines[i].startswith(
"*Tree"):
669 result = parseblock(lines[i:i+3])
670 result[
"Branches"] = {}
672 while i < (count - 3)
and lines[i].startswith(
"*Br"):
673 if i < (count - 2)
and lines[i].startswith(
"*Branch "):
677 branch = parseblock(lines[i:i+3])
678 result[
"Branches"][branch[
"Name"]] = branch
685 Scan stdout to find ROOT TTree summaries and digest them.
687 stars = re.compile(
r"^\*+$")
688 outlines = stdout.splitlines()
689 nlines = len(outlines)
695 while i < nlines
and not stars.match(outlines[i]):
700 trees[tree[
"Name"]] = tree
706 Check that all the keys in reference are in to_check too, with the same value.
707 If the value is a dict, the function is called recursively. to_check can
708 contain more keys than reference, that will not be tested.
709 The function returns at the first difference found.
714 ignore_re = re.compile(ignore)
715 keys = [ key
for key
in reference
if not ignore_re.match(key) ]
717 keys = reference.keys()
721 if (
type(reference[k])
is dict)
and (
type(to_check[k])
is dict):
723 failed = fail_keys =
cmpTreesDicts(reference[k], to_check[k], ignore)
726 failed = to_check[k] != reference[k]
731 fail_keys.insert(0, k)
741 if c
is None or r
is None:
743 return (fail_path, r, c)
746 h_count_re = re.compile(
r"^(.*)SUCCESS\s+Booked (\d+) Histogram\(s\) :\s+(.*)")
750 Extract the histograms infos from the lines starting at pos.
751 Returns the position of the first line after the summary block.
754 h_table_head = re.compile(
r'SUCCESS\s+List of booked (1D|2D|3D|1D profile|2D profile) histograms in directory\s+"(\w*)"')
755 h_short_summ = re.compile(
r"ID=([^\"]+)\s+\"([^\"]+)\"\s+(.*)")
760 m = h_count_re.search(lines[pos])
761 name = m.group(1).strip()
762 total = int(m.group(2))
764 for k, v
in [ x.split(
"=")
for x
in m.group(3).split() ]:
767 header[
"Total"] = total
771 m = h_table_head.search(lines[pos])
774 t = t.replace(
" profile",
"Prof")
781 if l.startswith(
" | ID"):
783 titles = [ x.strip()
for x
in l.split(
"|")][1:]
785 while pos < nlines
and lines[pos].startswith(
" |"):
787 values = [ x.strip()
for x
in l.split(
"|")][1:]
789 for i
in range(len(titles)):
790 hcont[titles[i]] = values[i]
791 cont[hcont[
"ID"]] = hcont
793 elif l.startswith(
" ID="):
794 while pos < nlines
and lines[pos].startswith(
" ID="):
795 values = [ x.strip()
for x
in h_short_summ.search(lines[pos]).groups() ]
796 cont[values[0]] = values
799 raise RuntimeError(
"Cannot understand line %d: '%s'" % (pos, l))
803 summ[d][
"header"] = header
808 summ[name] = {
"header": header}
813 Scan stdout to find ROOT TTree summaries and digest them.
815 outlines = stdout.splitlines()
816 nlines = len(outlines) - 1
824 match = h_count_re.search(outlines[pos])
825 while pos < nlines
and not match:
827 match = h_count_re.search(outlines[pos])
830 summaries.update(summ)
835 """Create a new 'Filter'.
837 'input' -- The string containing the input to provide to the
840 'timeout' -- As for 'TimeoutExecutable.__init__'."""
842 super(GaudiFilterExecutable, self).
__init__(input, timeout)
849 tmpf = tempfile.mkstemp()
854 """Copied from TimeoutExecutable to allow the re-implementation of
857 if sys.platform ==
"win32":
868 """Code copied from both FilterExecutable and TimeoutExecutable.
872 self._ClosePipeEnd(self._stdin_pipe[0])
873 if self._stdout_pipe:
874 self._ClosePipeEnd(self._stdout_pipe[1])
875 if self._stderr_pipe:
876 self._ClosePipeEnd(self._stderr_pipe[1])
884 super(qm.executable.TimeoutExecutable, self).
_HandleChild()
891 child_pid = self._GetChildPID()
893 os.setpgid(child_pid, child_pid)
921 os.setpgid(0, child_pid)
930 max_fds = os.sysconf(
"SC_OPEN_MAX")
933 for fd
in xrange(max_fds):
944 if sys.platform ==
"linux2":
946 os.path.join(
"/proc", str(child_pid),
"exe"),
948 "-batch",
"-n",
"-x",
949 "'%s'" % os.path.join(os.path.dirname(__file__),
"stack-trace.gdb")]
952 o = os.popen(
" ".join(cmd)).read()
957 os.kill(0, signal.SIGKILL)
960 select.select ([], [], [])
965 elif self.
__timeout >= 0
and sys.platform ==
"win32":
968 self.__monitor_thread.start()
970 if sys.platform ==
"win32":
973 """Code copied from FilterExecutable.
974 Kill the child if the timeout expires.
976 This function is run in the monitoring thread."""
984 result = win32event.WaitForSingleObject(self._GetChildPID(),
987 if result == win32con.WAIT_TIMEOUT:
994 """Standard Gaudi test.
1001 description=
"""The path to the program.
1003 This field indicates the path to the program. If it is not
1004 an absolute path, the value of the 'PATH' environment
1005 variable will be used to search for the program.
1006 If not specified, $GAUDIEXE or Gaudi.exe are used.
1009 qm.fields.SetField(qm.fields.TextField(
1011 title=
"Argument List",
1012 description=
"""The command-line arguments.
1014 If this field is left blank, the program is run without any
1017 Use this field to specify the option files.
1019 An implicit 0th argument (the path to the program) is added
1022 qm.fields.TextField(
1025 description=
"""Options to be passed to the application.
1027 This field allows to pass a list of options to the main program
1028 without the need of a separate option file.
1030 The content of the field is written to a temporary file which name
1031 is passed the the application as last argument (appended to the
1032 field "Argument List".
1038 qm.fields.TextField(
1040 title=
"Working Directory",
1041 description=
"""Path to the working directory.
1043 If this field is left blank, the program will be run from the qmtest
1044 directory, otherwise from the directory specified.""",
1047 qm.fields.TextField(
1049 title=
"Reference Output",
1050 description=
"""Path to the file containing the reference output.
1052 If this field is left blank, any standard output will be considered
1055 If the reference file is specified, any output on standard error is
1058 qm.fields.TextField(
1059 name=
"error_reference",
1060 title=
"Reference for standard error",
1061 description=
"""Path to the file containing the reference for the standard error.
1063 If this field is left blank, any standard output will be considered
1066 If the reference file is specified, any output on standard error is
1069 qm.fields.SetField(qm.fields.TextField(
1070 name =
"unsupported_platforms",
1071 title =
"Unsupported Platforms",
1072 description =
"""Platform on which the test must not be run.
1074 List of regular expressions identifying the platforms on which the
1075 test is not run and the result is set to UNTESTED."""
1078 qm.fields.TextField(
1080 title =
"Validator",
1081 description =
"""Function to validate the output of the test.
1083 If defined, the function is used to validate the products of the
1085 The function is called passing as arguments:
1086 self: the test class instance
1087 stdout: the standard output of the executed test
1088 stderr: the standard error of the executed test
1089 result: the Result objects to fill with messages
1090 The function must return a list of causes for the failure.
1091 If specified, overrides standard output, standard error and
1099 qm.fields.BooleanField(
1100 name =
"use_temp_dir",
1101 title =
"Use temporary directory",
1102 description =
"""Use temporary directory.
1104 If set to true, use a temporary directory as working directory.
1106 default_value=
"false"
1109 qm.fields.IntegerField(
1111 title =
"Expected signal",
1112 description =
"""Expect termination by signal.""",
1119 unsupported = [ re.compile(x)
1120 for x
in [ str(y).strip()
1121 for y
in self.unsupported_platforms ]
1124 for p_re
in unsupported:
1125 if p_re.search(platform):
1126 result.SetOutcome(result.UNTESTED)
1127 result[result.CAUSE] =
'Platform not supported.'
1133 Return the platform Id defined in CMTCONFIG or SCRAM_ARCH.
1137 if "CMTCONFIG" in os.environ:
1138 arch = os.environ[
"CMTCONFIG"]
1139 elif "SCRAM_ARCH" in os.environ:
1140 arch = os.environ[
"SCRAM_ARCH"]
1145 Return True if the current platform is Windows.
1147 This function was needed because of the change in the CMTCONFIG format,
1148 from win32_vc71_dbg to i686-winxp-vc9-dbg.
1151 return "winxp" in platform
or platform.startswith(
"win")
1159 platformSplit =
lambda p: set(p.split(
'-' in p
and '-' or '_'))
1161 reference = os.path.normpath(os.path.expandvars(reffile))
1163 spec_ref = reference[:-3] + self.
GetPlatform()[0:3] + reference[-3:]
1164 if os.path.isfile(spec_ref):
1165 reference = spec_ref
1168 dirname, basename = os.path.split(reference)
1169 if not dirname: dirname =
'.'
1170 head = basename +
"."
1171 head_len = len(head)
1174 for f
in os.listdir(dirname):
1175 if f.startswith(head):
1176 req_plat = platformSplit(f[head_len:])
1177 if platform.issuperset(req_plat):
1178 candidates.append( (len(req_plat), f) )
1183 reference = os.path.join(dirname, candidates[-1][1])
1188 ignore =
r"Basket|.*size|Compression"):
1190 Compare the TTree summaries in stdout with the ones in trees_dict or in
1191 the reference file. By default ignore the size, compression and basket
1193 The presence of TTree summaries when none is expected is not a failure.
1195 if trees_dict
is None:
1198 if reference
and os.path.isfile(reference):
1203 from pprint
import PrettyPrinter
1204 pp = PrettyPrinter()
1206 result[
"GaudiTest.TTrees.expected"] = result.Quote(pp.pformat(trees_dict))
1208 result[
"GaudiTest.TTrees.ignore"] = result.Quote(ignore)
1213 causes.append(
"trees summaries")
1215 result[
"GaudiTest.TTrees.failure_on"] = result.Quote(msg)
1216 result[
"GaudiTest.TTrees.found"] = result.Quote(pp.pformat(trees))
1224 Compare the TTree summaries in stdout with the ones in trees_dict or in
1225 the reference file. By default ignore the size, compression and basket
1227 The presence of TTree summaries when none is expected is not a failure.
1232 if reference
and os.path.isfile(reference):
1237 from pprint
import PrettyPrinter
1238 pp = PrettyPrinter()
1240 result[
"GaudiTest.Histos.expected"] = result.Quote(pp.pformat(dict))
1242 result[
"GaudiTest.Histos.ignore"] = result.Quote(ignore)
1247 causes.append(
"histos summaries")
1249 result[
"GaudiTest.Histos.failure_on"] = result.Quote(msg)
1250 result[
"GaudiTest.Histos.found"] = result.Quote(pp.pformat(histos))
1256 Default validation action: compare standard output and error to the
1261 preproc = normalizeExamples
1265 if reference
and os.path.isfile(reference):
1266 result[
"GaudiTest.output_reference"] = reference
1269 "GaudiTest.output_diff",
1270 preproc = preproc)(stdout, result)
1278 newref = open(reference +
".new",
"w")
1280 for l
in stdout.splitlines():
1281 newref.write(l.rstrip() +
'\n')
1291 if reference
and os.path.isfile(reference):
1292 result[
"GaudiTest.error_reference"] = reference
1295 "GaudiTest.error_diff",
1296 preproc = preproc)(stderr, result)
1299 newref = open(reference +
".new",
"w")
1301 for l
in stderr.splitlines():
1302 newref.write(l.rstrip() +
'\n')
1307 "ExecTest.expected_stderr")(stderr, result)
1314 if self.validator.strip() !=
"":
1315 class CallWrapper(object):
1317 Small wrapper class to dynamically bind some default arguments
1320 def __init__(self, callable, extra_args = {}):
1324 from inspect
import getargspec
1330 def __call__(self, *args, **kwargs):
1334 kwargs = dict(kwargs)
1338 if a
not in positional
and a
not in kwargs:
1340 return apply(self.
callable, args, kwargs)
1342 exported_symbols = {
"self":self,
1347 "findReferenceBlock":
1348 CallWrapper(findReferenceBlock, {
"stdout":stdout,
1351 "validateWithReference":
1357 CallWrapper(countErrorLines, {
"stdout":stdout,
1360 "checkTTreesSummaries":
1364 "checkHistosSummaries":
1370 exec self.validator
in globals(), exported_symbols
1378 Add the content of the environment to the result object.
1380 Copied from the QMTest class of COOL.
1382 vars = os.environ.keys()
1384 result[
'GaudiTest.environment'] = \
1385 result.Quote(
'\n'.join([
"%s=%s"%(v,os.environ[v])
for v
in vars]))
1387 def Run(self, context, result):
1390 'context' -- A 'Context' giving run-time parameters to the
1393 'result' -- A 'Result' object. The outcome will be
1394 'Result.PASS' when this method is called. The 'result' may be
1395 modified by this method to indicate outcomes other than
1396 'Result.PASS' or to add annotations."""
1405 elif "GAUDIEXE" in os.environ:
1406 prog = os.environ[
"GAUDIEXE"]
1411 dummy, prog_ext = os.path.splitext(prog)
1412 if prog_ext
not in [
".exe",
".py",
".bat" ]
and self.
isWinPlatform():
1416 prog =
which(prog)
or prog
1419 args =
map(rationalizepath, self.args)
1426 if self.options.strip():
1428 if re.search(
r"from\s+Gaudi.Configuration\s+import\s+\*|from\s+Configurables\s+import", self.options):
1431 tmpfile.writelines(
"\n".join(self.options.splitlines()))
1433 args.append(tmpfile.name)
1434 result[
"GaudiTest.options"] = result.Quote(self.options)
1437 if prog_ext ==
".py":
1440 prog =
which(
"python.exe")
or "python.exe"
1442 prog =
which(
"python")
or "python"
1445 origdir = os.getcwd()
1447 os.chdir(str(os.path.normpath(os.path.expandvars(self.workdir))))
1449 if "QMTEST_TMPDIR" in os.environ:
1450 qmtest_tmpdir = os.environ[
"QMTEST_TMPDIR"]
1451 if not os.path.exists(qmtest_tmpdir):
1452 os.makedirs(qmtest_tmpdir)
1453 os.chdir(qmtest_tmpdir)
1454 elif "qmtest.tmpdir" in context:
1455 os.chdir(context[
"qmtest.tmpdir"])
1457 if "QMTEST_IGNORE_TIMEOUT" not in os.environ:
1470 if result.GetOutcome()
not in [ result.PASS ]:
1477 """Run the 'program'.
1479 'program' -- The path to the program to run.
1481 'arguments' -- A list of the arguments to the program. This
1482 list must contain a first argument corresponding to 'argv[0]'.
1484 'context' -- A 'Context' giving run-time parameters to the
1487 'result' -- A 'Result' object. The outcome will be
1488 'Result.PASS' when this method is called. The 'result' may be
1489 modified by this method to indicate outcomes other than
1490 'Result.PASS' or to add annotations.
1492 @attention: This method has been copied from command.ExecTestBase
1493 (QMTest 2.3.0) and modified to keep stdout and stderr
1494 for tests that have been terminated by a signal.
1495 (Fundamental for debugging in the Application Area)
1499 environment = self.MakeEnvironment(context)
1501 if "slc6" in environment.get(
'CMTCONFIG',
''):
1502 environment[
'TERM'] =
'dumb'
1515 exit_status = e.Run(arguments, environment, path = program)
1517 if e.stack_trace_file
and os.path.exists(e.stack_trace_file):
1518 stack_trace = open(e.stack_trace_file).read()
1519 os.remove(e.stack_trace_file)
1523 result[
"ExecTest.stack_trace"] = result.Quote(stack_trace)
1526 if (sys.platform ==
"win32" or os.WIFEXITED(exit_status)
1532 if self.exit_code
is None:
1534 elif sys.platform ==
"win32":
1535 exit_code = exit_status
1537 exit_code = os.WEXITSTATUS(exit_status)
1542 result[
"ExecTest.exit_code"] = str(exit_code)
1543 result[
"ExecTest.stdout"] = result.Quote(stdout)
1544 result[
"ExecTest.stderr"] = result.Quote(stderr)
1546 if exit_code != self.exit_code:
1547 causes.append(
"exit_code")
1548 result[
"ExecTest.expected_exit_code"] \
1549 = str(self.exit_code)
1554 result.Fail(
"Unexpected %s." % string.join(causes,
", "))
1555 elif os.WIFSIGNALED(exit_status):
1558 signal_number = str(os.WTERMSIG(exit_status))
1560 result.Fail(
"Program terminated by signal.")
1564 result.Fail(
"Exceeded time limit (%ds), terminated." % timeout)
1565 result[
"ExecTest.signal_number"] = signal_number
1566 result[
"ExecTest.stdout"] = result.Quote(e.stdout)
1567 result[
"ExecTest.stderr"] = result.Quote(e.stderr)
1569 result[
"ExecTest.expected_signal_number"] = str(self.
signal)
1570 elif os.WIFSTOPPED(exit_status):
1573 signal_number = str(os.WSTOPSIG(exit_status))
1575 result.Fail(
"Program stopped by signal.")
1579 result.Fail(
"Exceeded time limit (%ds), stopped." % timeout)
1580 result[
"ExecTest.signal_number"] = signal_number
1581 result[
"ExecTest.stdout"] = result.Quote(e.stdout)
1582 result[
"ExecTest.stderr"] = result.Quote(e.stderr)
1586 result.Fail(
"Program did not terminate normally.")
1592 result[
"ExecTest.stdout"] = result[
"ExecTest.stdout"].replace(esc,repr_esc)
1599 projbasedir = os.path.normpath(destdir)
1600 while not os.path.exists(os.path.join(projbasedir,
".project")):
1601 oldprojdir = projbasedir
1602 projbasedir = os.path.normpath(os.path.join(projbasedir, os.pardir))
1605 if oldprojdir == projbasedir:
1609 if not os.path.exists(destdir):
1610 os.makedirs(destdir)
1612 from xml.etree
import ElementTree
as ET
1613 t = ET.parse(os.path.join(projbasedir,
".project"))
1614 projectName = t.find(
"name").text
1617 destfile =
"%s.launch" % self._Runnable__id
1619 destfile = os.path.join(destdir, destfile)
1621 if self.options.strip():
1625 tempfile = args.pop()
1626 optsfile = destfile + os.path.splitext(tempfile)[1]
1627 shutil.copyfile(tempfile, optsfile)
1628 args.append(optsfile)
1631 from xml.sax.saxutils
import quoteattr
1635 data[
"environment"] =
"\n".join([
'<mapEntry key=%s value=%s/>' % (quoteattr(k), quoteattr(v))
1636 for k, v
in os.environ.iteritems()
1637 if k
not in (
'MAKEOVERRIDES',
'MAKEFLAGS',
'MAKELEVEL')])
1639 data[
"exec"] =
which(prog)
or prog
1640 if os.path.basename(data[
"exec"]).lower().startswith(
"python"):
1641 data[
"stopAtMain"] =
"false"
1643 data[
"stopAtMain"] =
"true"
1645 data[
"args"] =
" ".join(
map(rationalizepath, args))
1647 data[
"args"] =
" ".join([
"/debugexe"] +
map(rationalizepath, [data[
"exec"]] + args))
1648 data[
"exec"] =
which(
"vcexpress.exe")
1651 data[
"workdir"] = os.getcwd()
1655 data[
"workdir"] = destdir
1657 data[
"project"] = projectName.strip()
1660 xml =
"""<?xml version="1.0" encoding="UTF-8" standalone="no"?>
1661 <launchConfiguration type="org.eclipse.cdt.launch.applicationLaunchType">
1662 <booleanAttribute key="org.eclipse.cdt.debug.mi.core.AUTO_SOLIB" value="true"/>
1663 <listAttribute key="org.eclipse.cdt.debug.mi.core.AUTO_SOLIB_LIST"/>
1664 <stringAttribute key="org.eclipse.cdt.debug.mi.core.DEBUG_NAME" value="gdb"/>
1665 <stringAttribute key="org.eclipse.cdt.debug.mi.core.GDB_INIT" value=".gdbinit"/>
1666 <listAttribute key="org.eclipse.cdt.debug.mi.core.SOLIB_PATH"/>
1667 <booleanAttribute key="org.eclipse.cdt.debug.mi.core.STOP_ON_SOLIB_EVENTS" value="false"/>
1668 <booleanAttribute key="org.eclipse.cdt.debug.mi.core.breakpointsFullPath" value="false"/>
1669 <stringAttribute key="org.eclipse.cdt.debug.mi.core.commandFactory" value="org.eclipse.cdt.debug.mi.core.standardCommandFactory"/>
1670 <stringAttribute key="org.eclipse.cdt.debug.mi.core.protocol" value="mi"/>
1671 <booleanAttribute key="org.eclipse.cdt.debug.mi.core.verboseMode" value="false"/>
1672 <intAttribute key="org.eclipse.cdt.launch.ATTR_BUILD_BEFORE_LAUNCH_ATTR" value="0"/>
1673 <stringAttribute key="org.eclipse.cdt.launch.COREFILE_PATH" value=""/>
1674 <stringAttribute key="org.eclipse.cdt.launch.DEBUGGER_ID" value="org.eclipse.cdt.debug.mi.core.CDebuggerNew"/>
1675 <stringAttribute key="org.eclipse.cdt.launch.DEBUGGER_REGISTER_GROUPS" value=""/>
1676 <stringAttribute key="org.eclipse.cdt.launch.DEBUGGER_START_MODE" value="run"/>
1677 <booleanAttribute key="org.eclipse.cdt.launch.DEBUGGER_STOP_AT_MAIN" value="%(stopAtMain)s"/>
1678 <stringAttribute key="org.eclipse.cdt.launch.DEBUGGER_STOP_AT_MAIN_SYMBOL" value="main"/>
1679 <booleanAttribute key="org.eclipse.cdt.launch.ENABLE_REGISTER_BOOKKEEPING" value="false"/>
1680 <booleanAttribute key="org.eclipse.cdt.launch.ENABLE_VARIABLE_BOOKKEEPING" value="false"/>
1681 <stringAttribute key="org.eclipse.cdt.launch.FORMAT" value="<?xml version="1.0" encoding="UTF-8" standalone="no"?><contentList/>"/>
1682 <stringAttribute key="org.eclipse.cdt.launch.GLOBAL_VARIABLES" value="<?xml version="1.0" encoding="UTF-8" standalone="no"?> <globalVariableList/> "/>
1683 <stringAttribute key="org.eclipse.cdt.launch.MEMORY_BLOCKS" value="<?xml version="1.0" encoding="UTF-8" standalone="no"?> <memoryBlockExpressionList/> "/>
1684 <stringAttribute key="org.eclipse.cdt.launch.PROGRAM_ARGUMENTS" value="%(args)s"/>
1685 <stringAttribute key="org.eclipse.cdt.launch.PROGRAM_NAME" value="%(exec)s"/>
1686 <stringAttribute key="org.eclipse.cdt.launch.PROJECT_ATTR" value="%(project)s"/>
1687 <stringAttribute key="org.eclipse.cdt.launch.PROJECT_BUILD_CONFIG_ID_ATTR" value=""/>
1688 <stringAttribute key="org.eclipse.cdt.launch.WORKING_DIRECTORY" value="%(workdir)s"/>
1689 <booleanAttribute key="org.eclipse.cdt.launch.ui.ApplicationCDebuggerTab.DEFAULTS_SET" value="true"/>
1690 <booleanAttribute key="org.eclipse.cdt.launch.use_terminal" value="true"/>
1691 <listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_PATHS">
1692 <listEntry value="/%(project)s"/>
1694 <listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_TYPES">
1695 <listEntry value="4"/>
1697 <booleanAttribute key="org.eclipse.debug.core.appendEnvironmentVariables" value="false"/>
1698 <mapAttribute key="org.eclipse.debug.core.environmentVariables">
1701 <mapAttribute key="org.eclipse.debug.core.preferred_launchers">
1702 <mapEntry key="[debug]" value="org.eclipse.cdt.cdi.launch.localCLaunch"/>
1704 <listAttribute key="org.eclipse.debug.ui.favoriteGroups">
1705 <listEntry value="org.eclipse.debug.ui.launchGroup.debug"/>
1707 </launchConfiguration>
1711 open(destfile,
"w").write(xml)
1719 import simplejson
as json
1722 """An 'HTMLResultStream' writes its output to a set of HTML files.
1724 The argument 'dir' is used to select the destination directory for the HTML
1726 The destination directory may already contain the report from a previous run
1727 (for example of a different package), in which case it will be extended to
1728 include the new data.
1731 qm.fields.TextField(
1733 title =
"Destination Directory",
1734 description =
"""The name of the directory.
1736 All results will be written to the directory indicated.""",
1738 default_value =
""),
1742 """Prepare the destination directory.
1744 Creates the destination directory and store in it some preliminary
1745 annotations and the static files found in the template directory
1748 ResultStream.__init__(self, arguments, **args)
1753 templateDir = os.path.join(os.path.dirname(__file__),
"html_report")
1754 if not os.path.isdir(self.dir):
1755 os.makedirs(self.dir)
1757 for f
in os.listdir(templateDir):
1758 src = os.path.join(templateDir, f)
1759 dst = os.path.join(self.dir, f)
1760 if not os.path.isdir(src)
and not os.path.exists(dst):
1761 shutil.copy(src, dst)
1763 if "CMTCONFIG" in os.environ:
1769 """Helper function to extend the global summary file in the destination
1776 ids = set([ i[
"id"]
for i
in self.
_summary ])
1777 newSummary = [ i
for i
in oldSummary
if i[
"id"]
not in ids ]
1783 """Writes the annotation to the annotation file.
1784 If the key is already present with a different value, the value becomes
1785 a list and the new value is appended to it, except for start_time and
1794 key, value =
map(str, [key, value])
1795 if key ==
"qmtest.run.start_time":
1800 if key
not in annotations:
1801 annotations[key] = value
1802 if "qmtest.run.end_time" in annotations:
1803 del annotations[
"qmtest.run.end_time"]
1806 if key
in annotations:
1807 old = annotations[key]
1808 if type(old)
is list:
1809 if value
not in old:
1810 annotations[key].append(value)
1812 annotations[key] = [old, value]
1814 annotations[key] = value
1820 """Prepare the test result directory in the destination directory storing
1821 into it the result fields.
1822 A summary of the test result is stored both in a file in the test directory
1823 and in the global summary file.
1826 summary[
"id"] = result.GetId()
1827 summary[
"outcome"] = result.GetOutcome()
1828 summary[
"cause"] = result.GetCause()
1829 summary[
"fields"] = result.keys()
1830 summary[
"fields"].sort()
1833 for f
in [
"id",
"outcome",
"cause"]:
1834 summary[f] = str(summary[f])
1835 summary[
"fields"] =
map(str, summary[
"fields"])
1837 self._summary.append(summary)
1843 testOutDir = os.path.join(self.dir, summary[
"id"])
1844 if not os.path.isdir(testOutDir):
1845 os.makedirs(testOutDir)
1846 json.dump(summary, open(os.path.join(testOutDir,
"summary.json"),
"w"),
1848 for f
in summary[
"fields"]:
1849 open(os.path.join(testOutDir, f),
"w").write(result[f])