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"),
462 (
None,
r'Service reference count check:',
r'Looping over all active services...'),
466 "JobOptionsSvc INFO # ",
467 "JobOptionsSvc WARNING # ",
470 "This machine has a speed",
473 "ToolSvc.Sequenc... INFO",
474 "DataListenerSvc INFO XML written to file:",
475 "[INFO]",
"[WARNING]",
476 "DEBUG No writable file catalog found which contains FID:",
478 "DEBUG Service base class initialized successfully",
479 "DEBUG Incident timing:",
480 "INFO 'CnvServices':[",
482 'Note: (file "(tmpfile)", line 2) File "set" already loaded',
486 r"^JobOptionsSvc INFO *$",
488 r"(Always|SUCCESS)\s*(Root f|[^ ]* F)ile version:",
489 r"0x[0-9a-fA-F#]+ *Algorithm::sysInitialize\(\) *\[",
490 r"0x[0-9a-fA-F#]* *__gxx_personality_v0 *\[",
491 r"File '.*.xml' does not exist",
492 r"INFO Refer to dataset .* by its file ID:",
493 r"INFO Referring to dataset .* by its file ID:",
494 r"INFO Disconnect from dataset",
495 r"INFO Disconnected from dataset",
496 r"INFO Disconnected data IO:",
497 r"IncidentSvc\s*(DEBUG (Adding|Removing)|VERBOSE Calling)",
499 r"^StatusCodeSvc.*listing all unchecked return codes:",
500 r"^StatusCodeSvc\s*INFO\s*$",
501 r"Num\s*\|\s*Function\s*\|\s*Source Library",
504 r"ERROR Failed to modify file: .* Errno=2 No such file or directory",
506 r"^ +[0-9]+ \|.*ROOT",
507 r"^ +[0-9]+ \|.*\|.*Dict",
511 r"SUCCESS\s*Booked \d+ Histogram\(s\)",
514 ] ) + normalizeExamples + skipEmptyLines + \
519 def __init__(self, reffile, cause, result_key, preproc = normalizeExamples):
526 if os.path.isfile(self.
reffile):
527 orig = open(self.
reffile).xreadlines()
533 new = stdout.splitlines()
537 diffs = difflib.ndiff(orig,new,charjunk=difflib.IS_CHARACTER_JUNK)
538 filterdiffs =
map(
lambda x: x.strip(),filter(
lambda x: x[0] !=
" ",diffs))
541 result[self.
result_key] = result.Quote(
"\n".join(filterdiffs))
545 +) standard output of the test""")
546 causes.append(self.
cause)
553 def findReferenceBlock(reference, stdout, result, causes, signature_offset=0, signature=None,
556 Given a block of text, tries to find it in the output.
557 The block had to be identified by a signature line. By default, the first
558 line is used as signature, or the line pointed to by signature_offset. If
559 signature_offset points outside the block, a signature line can be passed as
560 signature argument. Note: if 'signature' is None (the default), a negative
561 signature_offset is interpreted as index in a list (e.g. -1 means the last
562 line), otherwise the it is interpreted as the number of lines before the
563 first one of the block the signature must appear.
564 The parameter 'id' allow to distinguish between different calls to this
565 function in the same validation code.
568 reflines = filter(
None,
map(
lambda s: s.rstrip(), reference.splitlines()))
570 raise RuntimeError(
"Empty (or null) reference")
572 outlines = filter(
None,
map(
lambda s: s.rstrip(), stdout.splitlines()))
574 res_field =
"GaudiTest.RefBlock"
576 res_field +=
"_%s" % id
578 if signature
is None:
579 if signature_offset < 0:
580 signature_offset = len(reference)+signature_offset
581 signature = reflines[signature_offset]
584 pos = outlines.index(signature)
585 outlines = outlines[pos-signature_offset:pos+len(reflines)-signature_offset]
586 if reflines != outlines:
587 msg =
"standard output"
589 if not msg
in causes:
591 result[res_field +
".observed"] = result.Quote(
"\n".join(outlines))
593 causes.append(
"missing signature")
594 result[res_field +
".signature"] = result.Quote(signature)
595 if len(reflines) > 1
or signature != reflines[0]:
596 result[res_field +
".expected"] = result.Quote(
"\n".join(reflines))
602 Count the number of messages with required severity (by default ERROR and FATAL)
603 and check if their numbers match the expected ones (0 by default).
604 The dictionary "expected" can be used to tune the number of errors and fatals
605 allowed, or to limit the number of expected warnings etc.
607 stdout = kwargs[
"stdout"]
608 result = kwargs[
"result"]
609 causes = kwargs[
"causes"]
616 outlines = stdout.splitlines()
617 from math
import log10
618 fmt =
"%%%dd - %%s" % (int(log10(len(outlines))+1))
624 if len(words) >= 2
and words[1]
in errors:
625 errors[words[1]].append(fmt%(linecount,l.rstrip()))
628 if len(errors[e]) != expected[e]:
629 causes.append(
'%s(%d)'%(e,len(errors[e])))
630 result[
"GaudiTest.lines.%s"%e] = result.Quote(
'\n'.join(errors[e]))
631 result[
"GaudiTest.lines.%s.expected#"%e] = result.Quote(str(expected[e]))
638 Parse the TTree summary table in lines, starting from pos.
639 Returns a tuple with the dictionary with the digested informations and the
640 position of the first line after the summary.
646 splitcols =
lambda l: [ f.strip()
for f
in l.strip(
"*\n").split(
':',2) ]
649 cols = splitcols(ll[0])
650 r[
"Name"], r[
"Title"] = cols[1:]
652 cols = splitcols(ll[1])
653 r[
"Entries"] = int(cols[1])
655 sizes = cols[2].split()
656 r[
"Total size"] = int(sizes[2])
657 if sizes[-1] ==
"memory":
660 r[
"File size"] = int(sizes[-1])
662 cols = splitcols(ll[2])
663 sizes = cols[2].split()
664 if cols[0] ==
"Baskets":
665 r[
"Baskets"] = int(cols[1])
666 r[
"Basket size"] = int(sizes[2])
667 r[
"Compression"] = float(sizes[-1])
670 if i < (count - 3)
and lines[i].startswith(
"*Tree"):
671 result = parseblock(lines[i:i+3])
672 result[
"Branches"] = {}
674 while i < (count - 3)
and lines[i].startswith(
"*Br"):
675 if i < (count - 2)
and lines[i].startswith(
"*Branch "):
679 branch = parseblock(lines[i:i+3])
680 result[
"Branches"][branch[
"Name"]] = branch
687 Scan stdout to find ROOT TTree summaries and digest them.
689 stars = re.compile(
r"^\*+$")
690 outlines = stdout.splitlines()
691 nlines = len(outlines)
697 while i < nlines
and not stars.match(outlines[i]):
702 trees[tree[
"Name"]] = tree
708 Check that all the keys in reference are in to_check too, with the same value.
709 If the value is a dict, the function is called recursively. to_check can
710 contain more keys than reference, that will not be tested.
711 The function returns at the first difference found.
716 ignore_re = re.compile(ignore)
717 keys = [ key
for key
in reference
if not ignore_re.match(key) ]
719 keys = reference.keys()
723 if (
type(reference[k])
is dict)
and (
type(to_check[k])
is dict):
725 failed = fail_keys =
cmpTreesDicts(reference[k], to_check[k], ignore)
728 failed = to_check[k] != reference[k]
733 fail_keys.insert(0, k)
743 if c
is None or r
is None:
745 return (fail_path, r, c)
748 h_count_re = re.compile(
r"^(.*)SUCCESS\s+Booked (\d+) Histogram\(s\) :\s+(.*)")
752 Extract the histograms infos from the lines starting at pos.
753 Returns the position of the first line after the summary block.
756 h_table_head = re.compile(
r'SUCCESS\s+List of booked (1D|2D|3D|1D profile|2D profile) histograms in directory\s+"(\w*)"')
757 h_short_summ = re.compile(
r"ID=([^\"]+)\s+\"([^\"]+)\"\s+(.*)")
762 m = h_count_re.search(lines[pos])
763 name = m.group(1).strip()
764 total = int(m.group(2))
766 for k, v
in [ x.split(
"=")
for x
in m.group(3).split() ]:
769 header[
"Total"] = total
773 m = h_table_head.search(lines[pos])
776 t = t.replace(
" profile",
"Prof")
783 if l.startswith(
" | ID"):
785 titles = [ x.strip()
for x
in l.split(
"|")][1:]
787 while pos < nlines
and lines[pos].startswith(
" |"):
789 values = [ x.strip()
for x
in l.split(
"|")][1:]
791 for i
in range(len(titles)):
792 hcont[titles[i]] = values[i]
793 cont[hcont[
"ID"]] = hcont
795 elif l.startswith(
" ID="):
796 while pos < nlines
and lines[pos].startswith(
" ID="):
797 values = [ x.strip()
for x
in h_short_summ.search(lines[pos]).groups() ]
798 cont[values[0]] = values
801 raise RuntimeError(
"Cannot understand line %d: '%s'" % (pos, l))
805 summ[d][
"header"] = header
810 summ[name] = {
"header": header}
815 Scan stdout to find ROOT TTree summaries and digest them.
817 outlines = stdout.splitlines()
818 nlines = len(outlines) - 1
826 match = h_count_re.search(outlines[pos])
827 while pos < nlines
and not match:
829 match = h_count_re.search(outlines[pos])
832 summaries.update(summ)
837 """Create a new 'Filter'.
839 'input' -- The string containing the input to provide to the
842 'timeout' -- As for 'TimeoutExecutable.__init__'."""
844 super(GaudiFilterExecutable, self).
__init__(input, timeout)
851 tmpf = tempfile.mkstemp()
856 """Copied from TimeoutExecutable to allow the re-implementation of
859 if sys.platform ==
"win32":
870 """Code copied from both FilterExecutable and TimeoutExecutable.
874 self._ClosePipeEnd(self._stdin_pipe[0])
875 if self._stdout_pipe:
876 self._ClosePipeEnd(self._stdout_pipe[1])
877 if self._stderr_pipe:
878 self._ClosePipeEnd(self._stderr_pipe[1])
886 super(qm.executable.TimeoutExecutable, self).
_HandleChild()
893 child_pid = self._GetChildPID()
895 os.setpgid(child_pid, child_pid)
923 os.setpgid(0, child_pid)
932 max_fds = os.sysconf(
"SC_OPEN_MAX")
935 for fd
in xrange(max_fds):
946 if sys.platform ==
"linux2":
948 os.path.join(
"/proc", str(child_pid),
"exe"),
950 "-batch",
"-n",
"-x",
951 "'%s'" % os.path.join(os.path.dirname(__file__),
"stack-trace.gdb")]
954 o = os.popen(
" ".join(cmd)).read()
959 os.kill(0, signal.SIGKILL)
962 select.select ([], [], [])
967 elif self.
__timeout >= 0
and sys.platform ==
"win32":
970 self.__monitor_thread.start()
972 if sys.platform ==
"win32":
975 """Code copied from FilterExecutable.
976 Kill the child if the timeout expires.
978 This function is run in the monitoring thread."""
986 result = win32event.WaitForSingleObject(self._GetChildPID(),
989 if result == win32con.WAIT_TIMEOUT:
996 """Standard Gaudi test.
1003 description=
"""The path to the program.
1005 This field indicates the path to the program. If it is not
1006 an absolute path, the value of the 'PATH' environment
1007 variable will be used to search for the program.
1008 If not specified, $GAUDIEXE or Gaudi.exe are used.
1011 qm.fields.SetField(qm.fields.TextField(
1013 title=
"Argument List",
1014 description=
"""The command-line arguments.
1016 If this field is left blank, the program is run without any
1019 Use this field to specify the option files.
1021 An implicit 0th argument (the path to the program) is added
1024 qm.fields.TextField(
1027 description=
"""Options to be passed to the application.
1029 This field allows to pass a list of options to the main program
1030 without the need of a separate option file.
1032 The content of the field is written to a temporary file which name
1033 is passed the the application as last argument (appended to the
1034 field "Argument List".
1040 qm.fields.TextField(
1042 title=
"Working Directory",
1043 description=
"""Path to the working directory.
1045 If this field is left blank, the program will be run from the qmtest
1046 directory, otherwise from the directory specified.""",
1049 qm.fields.TextField(
1051 title=
"Reference Output",
1052 description=
"""Path to the file containing the reference output.
1054 If this field is left blank, any standard output will be considered
1057 If the reference file is specified, any output on standard error is
1060 qm.fields.TextField(
1061 name=
"error_reference",
1062 title=
"Reference for standard error",
1063 description=
"""Path to the file containing the reference for the standard error.
1065 If this field is left blank, any standard output will be considered
1068 If the reference file is specified, any output on standard error is
1071 qm.fields.SetField(qm.fields.TextField(
1072 name =
"unsupported_platforms",
1073 title =
"Unsupported Platforms",
1074 description =
"""Platform on which the test must not be run.
1076 List of regular expressions identifying the platforms on which the
1077 test is not run and the result is set to UNTESTED."""
1080 qm.fields.TextField(
1082 title =
"Validator",
1083 description =
"""Function to validate the output of the test.
1085 If defined, the function is used to validate the products of the
1087 The function is called passing as arguments:
1088 self: the test class instance
1089 stdout: the standard output of the executed test
1090 stderr: the standard error of the executed test
1091 result: the Result objects to fill with messages
1092 The function must return a list of causes for the failure.
1093 If specified, overrides standard output, standard error and
1101 qm.fields.BooleanField(
1102 name =
"use_temp_dir",
1103 title =
"Use temporary directory",
1104 description =
"""Use temporary directory.
1106 If set to true, use a temporary directory as working directory.
1108 default_value=
"false"
1111 qm.fields.IntegerField(
1113 title =
"Expected signal",
1114 description =
"""Expect termination by signal.""",
1121 unsupported = [ re.compile(x)
1122 for x
in [ str(y).strip()
1123 for y
in self.unsupported_platforms ]
1126 for p_re
in unsupported:
1127 if p_re.search(platform):
1128 result.SetOutcome(result.UNTESTED)
1129 result[result.CAUSE] =
'Platform not supported.'
1135 Return the platform Id defined in CMTCONFIG or SCRAM_ARCH.
1139 if "CMTCONFIG" in os.environ:
1140 arch = os.environ[
"CMTCONFIG"]
1141 elif "SCRAM_ARCH" in os.environ:
1142 arch = os.environ[
"SCRAM_ARCH"]
1147 Return True if the current platform is Windows.
1149 This function was needed because of the change in the CMTCONFIG format,
1150 from win32_vc71_dbg to i686-winxp-vc9-dbg.
1153 return "winxp" in platform
or platform.startswith(
"win")
1161 platformSplit =
lambda p: set(p.split(
'-' in p
and '-' or '_'))
1163 reference = os.path.normpath(os.path.expandvars(reffile))
1165 spec_ref = reference[:-3] + self.
GetPlatform()[0:3] + reference[-3:]
1166 if os.path.isfile(spec_ref):
1167 reference = spec_ref
1170 dirname, basename = os.path.split(reference)
1171 if not dirname: dirname =
'.'
1172 head = basename +
"."
1173 head_len = len(head)
1176 for f
in os.listdir(dirname):
1177 if f.startswith(head):
1178 req_plat = platformSplit(f[head_len:])
1179 if platform.issuperset(req_plat):
1180 candidates.append( (len(req_plat), f) )
1185 reference = os.path.join(dirname, candidates[-1][1])
1190 ignore =
r"Basket|.*size|Compression"):
1192 Compare the TTree summaries in stdout with the ones in trees_dict or in
1193 the reference file. By default ignore the size, compression and basket
1195 The presence of TTree summaries when none is expected is not a failure.
1197 if trees_dict
is None:
1200 if reference
and os.path.isfile(reference):
1205 from pprint
import PrettyPrinter
1206 pp = PrettyPrinter()
1208 result[
"GaudiTest.TTrees.expected"] = result.Quote(pp.pformat(trees_dict))
1210 result[
"GaudiTest.TTrees.ignore"] = result.Quote(ignore)
1215 causes.append(
"trees summaries")
1217 result[
"GaudiTest.TTrees.failure_on"] = result.Quote(msg)
1218 result[
"GaudiTest.TTrees.found"] = result.Quote(pp.pformat(trees))
1226 Compare the TTree summaries in stdout with the ones in trees_dict or in
1227 the reference file. By default ignore the size, compression and basket
1229 The presence of TTree summaries when none is expected is not a failure.
1234 if reference
and os.path.isfile(reference):
1239 from pprint
import PrettyPrinter
1240 pp = PrettyPrinter()
1242 result[
"GaudiTest.Histos.expected"] = result.Quote(pp.pformat(dict))
1244 result[
"GaudiTest.Histos.ignore"] = result.Quote(ignore)
1249 causes.append(
"histos summaries")
1251 result[
"GaudiTest.Histos.failure_on"] = result.Quote(msg)
1252 result[
"GaudiTest.Histos.found"] = result.Quote(pp.pformat(histos))
1258 Default validation action: compare standard output and error to the
1263 preproc = normalizeExamples
1267 if reference
and os.path.isfile(reference):
1268 result[
"GaudiTest.output_reference"] = reference
1271 "GaudiTest.output_diff",
1272 preproc = preproc)(stdout, result)
1280 newref = open(reference +
".new",
"w")
1282 for l
in stdout.splitlines():
1283 newref.write(l.rstrip() +
'\n')
1293 if reference
and os.path.isfile(reference):
1294 result[
"GaudiTest.error_reference"] = reference
1297 "GaudiTest.error_diff",
1298 preproc = preproc)(stderr, result)
1301 newref = open(reference +
".new",
"w")
1303 for l
in stderr.splitlines():
1304 newref.write(l.rstrip() +
'\n')
1309 "ExecTest.expected_stderr")(stderr, result)
1316 if self.validator.strip() !=
"":
1317 class CallWrapper(object):
1319 Small wrapper class to dynamically bind some default arguments
1322 def __init__(self, callable, extra_args = {}):
1326 from inspect
import getargspec
1332 def __call__(self, *args, **kwargs):
1336 kwargs = dict(kwargs)
1340 if a
not in positional
and a
not in kwargs:
1342 return apply(self.
callable, args, kwargs)
1344 exported_symbols = {
"self":self,
1349 "findReferenceBlock":
1350 CallWrapper(findReferenceBlock, {
"stdout":stdout,
1353 "validateWithReference":
1359 CallWrapper(countErrorLines, {
"stdout":stdout,
1362 "checkTTreesSummaries":
1366 "checkHistosSummaries":
1372 exec self.validator
in globals(), exported_symbols
1380 Add the content of the environment to the result object.
1382 Copied from the QMTest class of COOL.
1384 vars = os.environ.keys()
1386 result[
'GaudiTest.environment'] = \
1387 result.Quote(
'\n'.join([
"%s=%s"%(v,os.environ[v])
for v
in vars]))
1389 def Run(self, context, result):
1392 'context' -- A 'Context' giving run-time parameters to the
1395 'result' -- A 'Result' object. The outcome will be
1396 'Result.PASS' when this method is called. The 'result' may be
1397 modified by this method to indicate outcomes other than
1398 'Result.PASS' or to add annotations."""
1407 elif "GAUDIEXE" in os.environ:
1408 prog = os.environ[
"GAUDIEXE"]
1413 dummy, prog_ext = os.path.splitext(prog)
1414 if prog_ext
not in [
".exe",
".py",
".bat" ]
and self.
isWinPlatform():
1418 prog =
which(prog)
or prog
1421 args =
map(rationalizepath, self.args)
1428 if self.options.strip():
1430 if re.search(
r"from\s+Gaudi.Configuration\s+import\s+\*|from\s+Configurables\s+import", self.options):
1433 tmpfile.writelines(
"\n".join(self.options.splitlines()))
1435 args.append(tmpfile.name)
1436 result[
"GaudiTest.options"] = result.Quote(self.options)
1439 if prog_ext ==
".py":
1442 prog =
which(
"python.exe")
or "python.exe"
1444 prog =
which(
"python")
or "python"
1447 origdir = os.getcwd()
1449 os.chdir(str(os.path.normpath(os.path.expandvars(self.workdir))))
1451 if "QMTEST_TMPDIR" in os.environ:
1452 qmtest_tmpdir = os.environ[
"QMTEST_TMPDIR"]
1453 if not os.path.exists(qmtest_tmpdir):
1454 os.makedirs(qmtest_tmpdir)
1455 os.chdir(qmtest_tmpdir)
1456 elif "qmtest.tmpdir" in context:
1457 os.chdir(context[
"qmtest.tmpdir"])
1459 if "QMTEST_IGNORE_TIMEOUT" not in os.environ:
1472 if result.GetOutcome()
not in [ result.PASS ]:
1479 """Run the 'program'.
1481 'program' -- The path to the program to run.
1483 'arguments' -- A list of the arguments to the program. This
1484 list must contain a first argument corresponding to 'argv[0]'.
1486 'context' -- A 'Context' giving run-time parameters to the
1489 'result' -- A 'Result' object. The outcome will be
1490 'Result.PASS' when this method is called. The 'result' may be
1491 modified by this method to indicate outcomes other than
1492 'Result.PASS' or to add annotations.
1494 @attention: This method has been copied from command.ExecTestBase
1495 (QMTest 2.3.0) and modified to keep stdout and stderr
1496 for tests that have been terminated by a signal.
1497 (Fundamental for debugging in the Application Area)
1501 environment = self.MakeEnvironment(context)
1503 if "slc6" in environment.get(
'CMTCONFIG',
''):
1504 environment[
'TERM'] =
'dumb'
1517 exit_status = e.Run(arguments, environment, path = program)
1519 if e.stack_trace_file
and os.path.exists(e.stack_trace_file):
1520 stack_trace = open(e.stack_trace_file).read()
1521 os.remove(e.stack_trace_file)
1525 result[
"ExecTest.stack_trace"] = result.Quote(stack_trace)
1528 if (sys.platform ==
"win32" or os.WIFEXITED(exit_status)
1534 if self.exit_code
is None:
1536 elif sys.platform ==
"win32":
1537 exit_code = exit_status
1539 exit_code = os.WEXITSTATUS(exit_status)
1544 result[
"ExecTest.exit_code"] = str(exit_code)
1545 result[
"ExecTest.stdout"] = result.Quote(stdout)
1546 result[
"ExecTest.stderr"] = result.Quote(stderr)
1548 if exit_code != self.exit_code:
1549 causes.append(
"exit_code")
1550 result[
"ExecTest.expected_exit_code"] \
1551 = str(self.exit_code)
1556 result.Fail(
"Unexpected %s." % string.join(causes,
", "))
1557 elif os.WIFSIGNALED(exit_status):
1560 signal_number = str(os.WTERMSIG(exit_status))
1562 result.Fail(
"Program terminated by signal.")
1566 result.Fail(
"Exceeded time limit (%ds), terminated." % timeout)
1567 result[
"ExecTest.signal_number"] = signal_number
1568 result[
"ExecTest.stdout"] = result.Quote(e.stdout)
1569 result[
"ExecTest.stderr"] = result.Quote(e.stderr)
1571 result[
"ExecTest.expected_signal_number"] = str(self.
signal)
1572 elif os.WIFSTOPPED(exit_status):
1575 signal_number = str(os.WSTOPSIG(exit_status))
1577 result.Fail(
"Program stopped by signal.")
1581 result.Fail(
"Exceeded time limit (%ds), stopped." % timeout)
1582 result[
"ExecTest.signal_number"] = signal_number
1583 result[
"ExecTest.stdout"] = result.Quote(e.stdout)
1584 result[
"ExecTest.stderr"] = result.Quote(e.stderr)
1588 result.Fail(
"Program did not terminate normally.")
1594 result[
"ExecTest.stdout"] = result[
"ExecTest.stdout"].replace(esc,repr_esc)
1601 projbasedir = os.path.normpath(destdir)
1602 while not os.path.exists(os.path.join(projbasedir,
".project")):
1603 oldprojdir = projbasedir
1604 projbasedir = os.path.normpath(os.path.join(projbasedir, os.pardir))
1607 if oldprojdir == projbasedir:
1611 if not os.path.exists(destdir):
1612 os.makedirs(destdir)
1614 from xml.etree
import ElementTree
as ET
1615 t = ET.parse(os.path.join(projbasedir,
".project"))
1616 projectName = t.find(
"name").text
1619 destfile =
"%s.launch" % self._Runnable__id
1621 destfile = os.path.join(destdir, destfile)
1623 if self.options.strip():
1627 tempfile = args.pop()
1628 optsfile = destfile + os.path.splitext(tempfile)[1]
1629 shutil.copyfile(tempfile, optsfile)
1630 args.append(optsfile)
1633 from xml.sax.saxutils
import quoteattr
1637 data[
"environment"] =
"\n".join([
'<mapEntry key=%s value=%s/>' % (quoteattr(k), quoteattr(v))
1638 for k, v
in os.environ.iteritems()
1639 if k
not in (
'MAKEOVERRIDES',
'MAKEFLAGS',
'MAKELEVEL')])
1641 data[
"exec"] =
which(prog)
or prog
1642 if os.path.basename(data[
"exec"]).lower().startswith(
"python"):
1643 data[
"stopAtMain"] =
"false"
1645 data[
"stopAtMain"] =
"true"
1647 data[
"args"] =
" ".join(
map(rationalizepath, args))
1649 data[
"args"] =
" ".join([
"/debugexe"] +
map(rationalizepath, [data[
"exec"]] + args))
1650 data[
"exec"] =
which(
"vcexpress.exe")
1653 data[
"workdir"] = os.getcwd()
1657 data[
"workdir"] = destdir
1659 data[
"project"] = projectName.strip()
1662 xml =
"""<?xml version="1.0" encoding="UTF-8" standalone="no"?>
1663 <launchConfiguration type="org.eclipse.cdt.launch.applicationLaunchType">
1664 <booleanAttribute key="org.eclipse.cdt.debug.mi.core.AUTO_SOLIB" value="true"/>
1665 <listAttribute key="org.eclipse.cdt.debug.mi.core.AUTO_SOLIB_LIST"/>
1666 <stringAttribute key="org.eclipse.cdt.debug.mi.core.DEBUG_NAME" value="gdb"/>
1667 <stringAttribute key="org.eclipse.cdt.debug.mi.core.GDB_INIT" value=".gdbinit"/>
1668 <listAttribute key="org.eclipse.cdt.debug.mi.core.SOLIB_PATH"/>
1669 <booleanAttribute key="org.eclipse.cdt.debug.mi.core.STOP_ON_SOLIB_EVENTS" value="false"/>
1670 <booleanAttribute key="org.eclipse.cdt.debug.mi.core.breakpointsFullPath" value="false"/>
1671 <stringAttribute key="org.eclipse.cdt.debug.mi.core.commandFactory" value="org.eclipse.cdt.debug.mi.core.standardCommandFactory"/>
1672 <stringAttribute key="org.eclipse.cdt.debug.mi.core.protocol" value="mi"/>
1673 <booleanAttribute key="org.eclipse.cdt.debug.mi.core.verboseMode" value="false"/>
1674 <intAttribute key="org.eclipse.cdt.launch.ATTR_BUILD_BEFORE_LAUNCH_ATTR" value="0"/>
1675 <stringAttribute key="org.eclipse.cdt.launch.COREFILE_PATH" value=""/>
1676 <stringAttribute key="org.eclipse.cdt.launch.DEBUGGER_ID" value="org.eclipse.cdt.debug.mi.core.CDebuggerNew"/>
1677 <stringAttribute key="org.eclipse.cdt.launch.DEBUGGER_REGISTER_GROUPS" value=""/>
1678 <stringAttribute key="org.eclipse.cdt.launch.DEBUGGER_START_MODE" value="run"/>
1679 <booleanAttribute key="org.eclipse.cdt.launch.DEBUGGER_STOP_AT_MAIN" value="%(stopAtMain)s"/>
1680 <stringAttribute key="org.eclipse.cdt.launch.DEBUGGER_STOP_AT_MAIN_SYMBOL" value="main"/>
1681 <booleanAttribute key="org.eclipse.cdt.launch.ENABLE_REGISTER_BOOKKEEPING" value="false"/>
1682 <booleanAttribute key="org.eclipse.cdt.launch.ENABLE_VARIABLE_BOOKKEEPING" value="false"/>
1683 <stringAttribute key="org.eclipse.cdt.launch.FORMAT" value="<?xml version="1.0" encoding="UTF-8" standalone="no"?><contentList/>"/>
1684 <stringAttribute key="org.eclipse.cdt.launch.GLOBAL_VARIABLES" value="<?xml version="1.0" encoding="UTF-8" standalone="no"?> <globalVariableList/> "/>
1685 <stringAttribute key="org.eclipse.cdt.launch.MEMORY_BLOCKS" value="<?xml version="1.0" encoding="UTF-8" standalone="no"?> <memoryBlockExpressionList/> "/>
1686 <stringAttribute key="org.eclipse.cdt.launch.PROGRAM_ARGUMENTS" value="%(args)s"/>
1687 <stringAttribute key="org.eclipse.cdt.launch.PROGRAM_NAME" value="%(exec)s"/>
1688 <stringAttribute key="org.eclipse.cdt.launch.PROJECT_ATTR" value="%(project)s"/>
1689 <stringAttribute key="org.eclipse.cdt.launch.PROJECT_BUILD_CONFIG_ID_ATTR" value=""/>
1690 <stringAttribute key="org.eclipse.cdt.launch.WORKING_DIRECTORY" value="%(workdir)s"/>
1691 <booleanAttribute key="org.eclipse.cdt.launch.ui.ApplicationCDebuggerTab.DEFAULTS_SET" value="true"/>
1692 <booleanAttribute key="org.eclipse.cdt.launch.use_terminal" value="true"/>
1693 <listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_PATHS">
1694 <listEntry value="/%(project)s"/>
1696 <listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_TYPES">
1697 <listEntry value="4"/>
1699 <booleanAttribute key="org.eclipse.debug.core.appendEnvironmentVariables" value="false"/>
1700 <mapAttribute key="org.eclipse.debug.core.environmentVariables">
1703 <mapAttribute key="org.eclipse.debug.core.preferred_launchers">
1704 <mapEntry key="[debug]" value="org.eclipse.cdt.cdi.launch.localCLaunch"/>
1706 <listAttribute key="org.eclipse.debug.ui.favoriteGroups">
1707 <listEntry value="org.eclipse.debug.ui.launchGroup.debug"/>
1709 </launchConfiguration>
1713 open(destfile,
"w").write(xml)
1721 import simplejson
as json
1724 """An 'HTMLResultStream' writes its output to a set of HTML files.
1726 The argument 'dir' is used to select the destination directory for the HTML
1728 The destination directory may already contain the report from a previous run
1729 (for example of a different package), in which case it will be extended to
1730 include the new data.
1733 qm.fields.TextField(
1735 title =
"Destination Directory",
1736 description =
"""The name of the directory.
1738 All results will be written to the directory indicated.""",
1740 default_value =
""),
1744 """Prepare the destination directory.
1746 Creates the destination directory and store in it some preliminary
1747 annotations and the static files found in the template directory
1750 ResultStream.__init__(self, arguments, **args)
1755 templateDir = os.path.join(os.path.dirname(__file__),
"html_report")
1756 if not os.path.isdir(self.dir):
1757 os.makedirs(self.dir)
1759 for f
in os.listdir(templateDir):
1760 src = os.path.join(templateDir, f)
1761 dst = os.path.join(self.dir, f)
1762 if not os.path.isdir(src)
and not os.path.exists(dst):
1763 shutil.copy(src, dst)
1765 if "CMTCONFIG" in os.environ:
1771 """Helper function to extend the global summary file in the destination
1778 ids = set([ i[
"id"]
for i
in self.
_summary ])
1779 newSummary = [ i
for i
in oldSummary
if i[
"id"]
not in ids ]
1785 """Writes the annotation to the annotation file.
1786 If the key is already present with a different value, the value becomes
1787 a list and the new value is appended to it, except for start_time and
1796 key, value =
map(str, [key, value])
1797 if key ==
"qmtest.run.start_time":
1802 if key
not in annotations:
1803 annotations[key] = value
1804 if "qmtest.run.end_time" in annotations:
1805 del annotations[
"qmtest.run.end_time"]
1808 if key
in annotations:
1809 old = annotations[key]
1810 if type(old)
is list:
1811 if value
not in old:
1812 annotations[key].append(value)
1814 annotations[key] = [old, value]
1816 annotations[key] = value
1822 """Prepare the test result directory in the destination directory storing
1823 into it the result fields.
1824 A summary of the test result is stored both in a file in the test directory
1825 and in the global summary file.
1828 summary[
"id"] = result.GetId()
1829 summary[
"outcome"] = result.GetOutcome()
1830 summary[
"cause"] = result.GetCause()
1831 summary[
"fields"] = result.keys()
1832 summary[
"fields"].sort()
1835 for f
in [
"id",
"outcome",
"cause"]:
1836 summary[f] = str(summary[f])
1837 summary[
"fields"] =
map(str, summary[
"fields"])
1839 self._summary.append(summary)
1845 testOutDir = os.path.join(self.dir, summary[
"id"])
1846 if not os.path.isdir(testOutDir):
1847 os.makedirs(testOutDir)
1848 json.dump(summary, open(os.path.join(testOutDir,
"summary.json"),
"w"),
1850 for f
in summary[
"fields"]:
1851 open(os.path.join(testOutDir, f),
"w").write(result[f])