5 __author__ =
'Marco Clemencic CERN/PH-LBC'
18 from subprocess
import Popen, PIPE, STDOUT
21 from GaudiKernel
import ROOT6WorkAroundEnabled
28 os.environ[
'LC_ALL'] =
'C'
32 import xml.etree.cElementTree
as ET
34 import xml.etree.ElementTree
as ET
38 return timedelta.days*86400 + timedelta.seconds + timedelta.microseconds/1000000
42 from qm.test.classes.command
import ExecTestBase
43 from qm.test.result_stream
import ResultStream
50 if sys.platform ==
"win32":
53 from threading
import *
71 Class to changes the environment temporarily.
73 def __init__(self, orig = os.environ, keep_same = False):
75 Create a temporary environment on top of the one specified
76 (it can be another TemporaryEnvironment instance).
85 Set an environment variable recording the previous value.
97 Get an environment variable.
98 Needed to provide the same interface as os.environ.
104 Unset an environment variable.
105 Needed to provide the same interface as os.environ.
107 if key
not in self.
env :
114 Return the list of defined environment variables.
115 Needed to provide the same interface as os.environ.
117 return self.env.keys()
121 Return the list of (name,value) pairs for the defined environment variables.
122 Needed to provide the same interface as os.environ.
124 return self.env.items()
129 Needed to provide the same interface as os.environ.
131 return key
in self.
env
135 Revert all the changes done to the original environment.
137 for key,value
in self.old_values.items():
141 self.
env[key] = value
146 Revert the changes on destruction.
153 Generate a shell script to reproduce the changes in the environment.
155 shells = [
'csh',
'sh',
'bat' ]
156 if shell_type
not in shells:
157 raise RuntimeError(
"Shell type '%s' unknown. Available: %s"%(shell_type,shells))
159 for key,value
in self.old_values.items():
160 if key
not in self.
env:
162 if shell_type ==
'csh':
163 out +=
'unsetenv %s\n'%key
164 elif shell_type ==
'sh':
165 out +=
'unset %s\n'%key
166 elif shell_type ==
'bat':
167 out +=
'set %s=\n'%key
170 if shell_type ==
'csh':
171 out +=
'setenv %s "%s"\n'%(key,self.
env[key])
172 elif shell_type ==
'sh':
173 out +=
'export %s="%s"\n'%(key,self.
env[key])
174 elif shell_type ==
'bat':
175 out +=
'set %s=%s\n'%(key,self.
env[key])
179 """Small class for temporary directories.
180 When instantiated, it creates a temporary directory and the instance
181 behaves as the string containing the directory name.
182 When the instance goes out of scope, it removes all the content of
183 the temporary directory (automatic clean-up).
200 shutil.rmtree(self.
name)
203 return getattr(self.
name,attr)
206 """Small class for temporary files.
207 When instantiated, it creates a temporary directory and the instance
208 behaves as the string containing the directory name.
209 When the instance goes out of scope, it removes all the content of
210 the temporary directory (automatic clean-up).
212 def __init__(self, suffix='', prefix='tmp', dir=None, text=False, keep = False):
217 self._fd, self.
name = tempfile.mkstemp(suffix,prefix,dir,text)
218 self.
file = os.fdopen(self._fd,
"r+")
230 return getattr(self.
file,attr)
233 """Small wrapper to call CMT.
242 if type(args)
is str:
244 cmd =
"cmt %s"%command
252 result = os.popen4(cmd)[1].read()
258 return lambda args=[]: self.
_run_cmt(attr, args)
261 """Returns a dictionary containing the runtime environment produced by CMT.
262 If a dictionary is passed a modified instance of it is returned.
266 for l
in self.setup(
"-csh").splitlines():
268 if l.startswith(
"setenv"):
269 dummy,name,value = l.split(
None,3)
270 env[name] = value.strip(
'"')
271 elif l.startswith(
"unsetenv"):
272 dummy,name = l.split(
None,2)
277 r = self.show([
"macro",k])
278 if r.find(
"CMT> Error: symbol not found") >= 0:
281 return self.show([
"macro_value",k]).strip()
289 Locates an executable in the executables path ($PATH) and returns the full
290 path to it. An application is looked for with or without the '.exe' suffix.
291 If the executable cannot be found, None is returned
293 if os.path.isabs(executable):
294 if not os.path.exists(executable):
295 if executable.endswith(
'.exe'):
296 if os.path.exists(executable[:-4]):
297 return executable[:-4]
299 for d
in os.environ.get(
"PATH").split(os.pathsep):
300 fullpath = os.path.join(d, executable)
301 if os.path.exists(fullpath):
303 if executable.endswith(
'.exe'):
304 return which(executable[:-4])
308 np = os.path.normpath(os.path.expandvars(p))
309 if os.path.exists(np):
310 p = os.path.realpath(np)
327 _illegal_xml_chars_RE = re.compile(
u'[\x00-\x08\x0b\x0c\x0e-\x1F\uD800-\uDFFF\uFFFE\uFFFF]')
330 "Return the hex string "
331 return "".join(
map(hexConvert,match.group()))
334 return hex(ord(char))
336 return _illegal_xml_chars_RE.sub(hexreplace, val)
339 """Filter out characters that are illegal in XML.
340 Looks for any character in val that is not allowed in XML
341 and replaces it with replacement ('?' by default).
344 return _illegal_xml_chars_RE.sub(replacement, val)
350 """Basic implementation of an option validator for Gaudi tests.
351 This implementation is based on the standard (LCG) validation functions
360 """Validate the output of the program.
362 'stdout' -- A string containing the data written to the standard output
365 'stderr' -- A string containing the data written to the standard error
368 'result' -- A 'Result' object. It may be used to annotate
369 the outcome according to the content of stderr.
371 returns -- A list of strings giving causes of failure."""
376 causes.append(self.
cause)
382 """Compare 's1' and 's2', ignoring line endings.
388 returns -- True if 's1' and 's2' are the same, ignoring
389 differences in line endings."""
395 to_ignore = re.compile(
r'Warning in <TInterpreter::ReadRootmapFile>: .* is already in .*')
396 keep_line =
lambda l:
not to_ignore.match(l)
397 return filter(keep_line, s1.splitlines()) == filter(keep_line, s2.splitlines())
399 return s1.splitlines() == s2.splitlines()
402 """ Base class for a callable that takes a file and returns a modified
407 if hasattr(input,
"__iter__"):
411 lines = input.splitlines()
416 if l: output.append(l)
417 if mergeback: output =
'\n'.join(output)
441 if line.find(s) >= 0:
return None
443 if r.search(line):
return None
453 if self.
start in line:
456 elif self.
end in line:
465 when = re.compile(when)
468 if isinstance(rhs, RegexpReplacer):
470 res._operations = self.
_operations + rhs._operations
472 res = FilePreprocessor.__add__(self, rhs)
476 if w
is None or w.search(line):
477 line = o.sub(r, line)
482 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)?",
483 "00:00:00 1970-01-01")
485 normalizeEOL.__processLine__ =
lambda line: str(line).rstrip() +
'\n'
489 skipEmptyLines.__processLine__ =
lambda line: (line.strip()
and line)
or None
500 line = line[:(pos+self.
siglen)]
501 lst = line[(pos+self.
siglen):].split()
503 line +=
" ".join(lst)
507 normalizeExamples = maskPointers + normalizeDate
510 (
"TIMER.TIMER",
r"\s+[+-]?[0-9]+[0-9.]*",
" 0"),
511 (
"release all pending",
r"^.*/([^/]*:.*)",
r"\1"),
512 (
"0x########",
r"\[.*/([^/]*.*)\]",
r"[\1]"),
513 (
"^#.*file",
r"file '.*[/\\]([^/\\]*)$",
r"file '\1"),
514 (
"^JobOptionsSvc.*options successfully read in from",
r"read in from .*[/\\]([^/\\]*)$",
r"file \1"),
516 (
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"),
518 (
"ServiceLocatorHelper::",
"ServiceLocatorHelper::(create|locate)Service",
"ServiceLocatorHelper::service"),
520 (
None,
r"e([-+])0([0-9][0-9])",
r"e\1\2"),
522 (
None,
r'Service reference count check:',
r'Looping over all active services...'),
527 "JobOptionsSvc INFO # ",
528 "JobOptionsSvc WARNING # ",
531 "This machine has a speed",
534 "ToolSvc.Sequenc... INFO",
535 "DataListenerSvc INFO XML written to file:",
536 "[INFO]",
"[WARNING]",
537 "DEBUG No writable file catalog found which contains FID:",
539 "DEBUG Service base class initialized successfully",
540 "DEBUG Incident timing:",
541 "INFO 'CnvServices':[",
545 r"^JobOptionsSvc INFO *$",
547 r"(Always|SUCCESS)\s*(Root f|[^ ]* F)ile version:",
548 r"0x[0-9a-fA-F#]+ *Algorithm::sysInitialize\(\) *\[",
549 r"0x[0-9a-fA-F#]* *__gxx_personality_v0 *\[",
550 r"File '.*.xml' does not exist",
551 r"INFO Refer to dataset .* by its file ID:",
552 r"INFO Referring to dataset .* by its file ID:",
553 r"INFO Disconnect from dataset",
554 r"INFO Disconnected from dataset",
555 r"INFO Disconnected data IO:",
556 r"IncidentSvc\s*(DEBUG (Adding|Removing)|VERBOSE Calling)",
558 r"^StatusCodeSvc.*listing all unchecked return codes:",
559 r"^StatusCodeSvc\s*INFO\s*$",
560 r"Num\s*\|\s*Function\s*\|\s*Source Library",
563 r"ERROR Failed to modify file: .* Errno=2 No such file or directory",
565 r"^ +[0-9]+ \|.*ROOT",
566 r"^ +[0-9]+ \|.*\|.*Dict",
570 r"SUCCESS\s*Booked \d+ Histogram\(s\)",
577 r'Warning in <TInterpreter::ReadRootmapFile>: .* is already in .*',
580 normalizeExamples = (lineSkipper + normalizeExamples + skipEmptyLines +
581 normalizeEOL +
LineSorter(
"Services to release : "))
584 def __init__(self, reffile, cause, result_key, preproc = normalizeExamples):
591 if os.path.isfile(self.
reffile):
592 orig = open(self.
reffile).xreadlines()
598 new = stdout.splitlines()
602 diffs = difflib.ndiff(orig,new,charjunk=difflib.IS_CHARACTER_JUNK)
603 filterdiffs =
map(
lambda x: x.strip(),filter(
lambda x: x[0] !=
" ",diffs))
606 result[self.
result_key] = result.Quote(
"\n".join(filterdiffs))
610 +) standard output of the test""")
611 causes.append(self.
cause)
618 def findReferenceBlock(reference, stdout, result, causes, signature_offset=0, signature=None,
621 Given a block of text, tries to find it in the output.
622 The block had to be identified by a signature line. By default, the first
623 line is used as signature, or the line pointed to by signature_offset. If
624 signature_offset points outside the block, a signature line can be passed as
625 signature argument. Note: if 'signature' is None (the default), a negative
626 signature_offset is interpreted as index in a list (e.g. -1 means the last
627 line), otherwise the it is interpreted as the number of lines before the
628 first one of the block the signature must appear.
629 The parameter 'id' allow to distinguish between different calls to this
630 function in the same validation code.
633 reflines = filter(
None,
map(
lambda s: s.rstrip(), reference.splitlines()))
635 raise RuntimeError(
"Empty (or null) reference")
637 outlines = filter(
None,
map(
lambda s: s.rstrip(), stdout.splitlines()))
639 res_field =
"GaudiTest.RefBlock"
641 res_field +=
"_%s" % id
643 if signature
is None:
644 if signature_offset < 0:
645 signature_offset = len(reference)+signature_offset
646 signature = reflines[signature_offset]
649 pos = outlines.index(signature)
650 outlines = outlines[pos-signature_offset:pos+len(reflines)-signature_offset]
651 if reflines != outlines:
652 msg =
"standard output"
654 if not msg
in causes:
656 result[res_field +
".observed"] = result.Quote(
"\n".join(outlines))
658 causes.append(
"missing signature")
659 result[res_field +
".signature"] = result.Quote(signature)
660 if len(reflines) > 1
or signature != reflines[0]:
661 result[res_field +
".expected"] = result.Quote(
"\n".join(reflines))
667 Count the number of messages with required severity (by default ERROR and FATAL)
668 and check if their numbers match the expected ones (0 by default).
669 The dictionary "expected" can be used to tune the number of errors and fatals
670 allowed, or to limit the number of expected warnings etc.
672 stdout = kwargs[
"stdout"]
673 result = kwargs[
"result"]
674 causes = kwargs[
"causes"]
681 outlines = stdout.splitlines()
682 from math
import log10
683 fmt =
"%%%dd - %%s" % (int(log10(len(outlines))+1))
689 if len(words) >= 2
and words[1]
in errors:
690 errors[words[1]].append(fmt%(linecount,l.rstrip()))
693 if len(errors[e]) != expected[e]:
694 causes.append(
'%s(%d)'%(e,len(errors[e])))
695 result[
"GaudiTest.lines.%s"%e] = result.Quote(
'\n'.join(errors[e]))
696 result[
"GaudiTest.lines.%s.expected#"%e] = result.Quote(str(expected[e]))
703 Parse the TTree summary table in lines, starting from pos.
704 Returns a tuple with the dictionary with the digested informations and the
705 position of the first line after the summary.
711 splitcols =
lambda l: [ f.strip()
for f
in l.strip(
"*\n").split(
':',2) ]
714 cols = splitcols(ll[0])
715 r[
"Name"], r[
"Title"] = cols[1:]
717 cols = splitcols(ll[1])
718 r[
"Entries"] = int(cols[1])
720 sizes = cols[2].split()
721 r[
"Total size"] = int(sizes[2])
722 if sizes[-1] ==
"memory":
725 r[
"File size"] = int(sizes[-1])
727 cols = splitcols(ll[2])
728 sizes = cols[2].split()
729 if cols[0] ==
"Baskets":
730 r[
"Baskets"] = int(cols[1])
731 r[
"Basket size"] = int(sizes[2])
732 r[
"Compression"] = float(sizes[-1])
735 if i < (count - 3)
and lines[i].startswith(
"*Tree"):
736 result = parseblock(lines[i:i+3])
737 result[
"Branches"] = {}
739 while i < (count - 3)
and lines[i].startswith(
"*Br"):
740 if i < (count - 2)
and lines[i].startswith(
"*Branch "):
744 branch = parseblock(lines[i:i+3])
745 result[
"Branches"][branch[
"Name"]] = branch
752 Scan stdout to find ROOT TTree summaries and digest them.
754 stars = re.compile(
r"^\*+$")
755 outlines = stdout.splitlines()
756 nlines = len(outlines)
762 while i < nlines
and not stars.match(outlines[i]):
767 trees[tree[
"Name"]] = tree
773 Check that all the keys in reference are in to_check too, with the same value.
774 If the value is a dict, the function is called recursively. to_check can
775 contain more keys than reference, that will not be tested.
776 The function returns at the first difference found.
781 ignore_re = re.compile(ignore)
782 keys = [ key
for key
in reference
if not ignore_re.match(key) ]
784 keys = reference.keys()
788 if (
type(reference[k])
is dict)
and (
type(to_check[k])
is dict):
790 failed = fail_keys =
cmpTreesDicts(reference[k], to_check[k], ignore)
793 failed = to_check[k] != reference[k]
798 fail_keys.insert(0, k)
808 if c
is None or r
is None:
810 return (fail_path, r, c)
813 h_count_re = re.compile(
r"^(.*)SUCCESS\s+Booked (\d+) Histogram\(s\) :\s+(.*)")
817 Extract the histograms infos from the lines starting at pos.
818 Returns the position of the first line after the summary block.
821 h_table_head = re.compile(
r'SUCCESS\s+List of booked (1D|2D|3D|1D profile|2D profile) histograms in directory\s+"(\w*)"')
822 h_short_summ = re.compile(
r"ID=([^\"]+)\s+\"([^\"]+)\"\s+(.*)")
827 m = h_count_re.search(lines[pos])
828 name = m.group(1).strip()
829 total = int(m.group(2))
831 for k, v
in [ x.split(
"=")
for x
in m.group(3).split() ]:
834 header[
"Total"] = total
838 m = h_table_head.search(lines[pos])
841 t = t.replace(
" profile",
"Prof")
848 if l.startswith(
" | ID"):
850 titles = [ x.strip()
for x
in l.split(
"|")][1:]
852 while pos < nlines
and lines[pos].startswith(
" |"):
854 values = [ x.strip()
for x
in l.split(
"|")][1:]
856 for i
in range(len(titles)):
857 hcont[titles[i]] = values[i]
858 cont[hcont[
"ID"]] = hcont
860 elif l.startswith(
" ID="):
861 while pos < nlines
and lines[pos].startswith(
" ID="):
862 values = [ x.strip()
for x
in h_short_summ.search(lines[pos]).groups() ]
863 cont[values[0]] = values
866 raise RuntimeError(
"Cannot understand line %d: '%s'" % (pos, l))
870 summ[d][
"header"] = header
875 summ[name] = {
"header": header}
880 Scan stdout to find ROOT TTree summaries and digest them.
882 outlines = stdout.splitlines()
883 nlines = len(outlines) - 1
891 match = h_count_re.search(outlines[pos])
892 while pos < nlines
and not match:
894 match = h_count_re.search(outlines[pos])
897 summaries.update(summ)
902 """Create a new 'Filter'.
904 'input' -- The string containing the input to provide to the
907 'timeout' -- As for 'TimeoutExecutable.__init__'."""
909 super(GaudiFilterExecutable, self).
__init__(input, timeout)
916 tmpf = tempfile.mkstemp()
921 """Copied from TimeoutExecutable to allow the re-implementation of
924 if sys.platform ==
"win32":
935 """Code copied from both FilterExecutable and TimeoutExecutable.
939 self._ClosePipeEnd(self._stdin_pipe[0])
940 if self._stdout_pipe:
941 self._ClosePipeEnd(self._stdout_pipe[1])
942 if self._stderr_pipe:
943 self._ClosePipeEnd(self._stderr_pipe[1])
951 super(qm.executable.TimeoutExecutable, self).
_HandleChild()
958 child_pid = self._GetChildPID()
960 os.setpgid(child_pid, child_pid)
988 os.setpgid(0, child_pid)
997 max_fds = os.sysconf(
"SC_OPEN_MAX")
1000 for fd
in xrange(max_fds):
1011 if sys.platform ==
"linux2":
1013 os.path.join(
"/proc", str(child_pid),
"exe"),
1015 "-batch",
"-n",
"-x",
1016 "'%s'" % os.path.join(os.path.dirname(__file__),
"stack-trace.gdb")]
1019 o = os.popen(
" ".join(cmd)).read()
1024 os.kill(0, signal.SIGKILL)
1027 select.select ([], [], [])
1032 elif self.
__timeout >= 0
and sys.platform ==
"win32":
1035 self.__monitor_thread.start()
1037 if sys.platform ==
"win32":
1040 """Code copied from FilterExecutable.
1041 Kill the child if the timeout expires.
1043 This function is run in the monitoring thread."""
1051 result = win32event.WaitForSingleObject(self._GetChildPID(),
1054 if result == win32con.WAIT_TIMEOUT:
1061 """Standard Gaudi test.
1064 qm.fields.TextField(
1068 description=
"""The path to the program.
1070 This field indicates the path to the program. If it is not
1071 an absolute path, the value of the 'PATH' environment
1072 variable will be used to search for the program.
1073 If not specified, $GAUDIEXE or Gaudi.exe are used.
1076 qm.fields.SetField(qm.fields.TextField(
1078 title=
"Argument List",
1079 description=
"""The command-line arguments.
1081 If this field is left blank, the program is run without any
1084 Use this field to specify the option files.
1086 An implicit 0th argument (the path to the program) is added
1089 qm.fields.TextField(
1092 description=
"""Options to be passed to the application.
1094 This field allows to pass a list of options to the main program
1095 without the need of a separate option file.
1097 The content of the field is written to a temporary file which name
1098 is passed the the application as last argument (appended to the
1099 field "Argument List".
1105 qm.fields.TextField(
1107 title=
"Working Directory",
1108 description=
"""Path to the working directory.
1110 If this field is left blank, the program will be run from the qmtest
1111 directory, otherwise from the directory specified.""",
1114 qm.fields.TextField(
1116 title=
"Reference Output",
1117 description=
"""Path to the file containing the reference output.
1119 If this field is left blank, any standard output will be considered
1122 If the reference file is specified, any output on standard error is
1125 qm.fields.TextField(
1126 name=
"error_reference",
1127 title=
"Reference for standard error",
1128 description=
"""Path to the file containing the reference for the standard error.
1130 If this field is left blank, any standard output will be considered
1133 If the reference file is specified, any output on standard error is
1136 qm.fields.SetField(qm.fields.TextField(
1137 name =
"unsupported_platforms",
1138 title =
"Unsupported Platforms",
1139 description =
"""Platform on which the test must not be run.
1141 List of regular expressions identifying the platforms on which the
1142 test is not run and the result is set to UNTESTED."""
1145 qm.fields.TextField(
1147 title =
"Validator",
1148 description =
"""Function to validate the output of the test.
1150 If defined, the function is used to validate the products of the
1152 The function is called passing as arguments:
1153 self: the test class instance
1154 stdout: the standard output of the executed test
1155 stderr: the standard error of the executed test
1156 result: the Result objects to fill with messages
1157 The function must return a list of causes for the failure.
1158 If specified, overrides standard output, standard error and
1166 qm.fields.BooleanField(
1167 name =
"use_temp_dir",
1168 title =
"Use temporary directory",
1169 description =
"""Use temporary directory.
1171 If set to true, use a temporary directory as working directory.
1173 default_value=
"false"
1176 qm.fields.IntegerField(
1178 title =
"Expected signal",
1179 description =
"""Expect termination by signal.""",
1186 unsupported = [ re.compile(x)
1187 for x
in [ str(y).strip()
1188 for y
in self.unsupported_platforms ]
1191 for p_re
in unsupported:
1192 if p_re.search(platform):
1193 result.SetOutcome(result.UNTESTED)
1194 result[result.CAUSE] =
'Platform not supported.'
1200 Return the platform Id defined in CMTCONFIG or SCRAM_ARCH.
1204 if "CMTCONFIG" in os.environ:
1205 arch = os.environ[
"CMTCONFIG"]
1206 elif "SCRAM_ARCH" in os.environ:
1207 arch = os.environ[
"SCRAM_ARCH"]
1212 Return True if the current platform is Windows.
1214 This function was needed because of the change in the CMTCONFIG format,
1215 from win32_vc71_dbg to i686-winxp-vc9-dbg.
1218 return "winxp" in platform
or platform.startswith(
"win")
1226 platformSplit =
lambda p: set(p.split(
'-' in p
and '-' or '_'))
1228 reference = os.path.normpath(os.path.expandvars(reffile))
1230 spec_ref = reference[:-3] + self.
GetPlatform()[0:3] + reference[-3:]
1231 if os.path.isfile(spec_ref):
1232 reference = spec_ref
1235 dirname, basename = os.path.split(reference)
1236 if not dirname: dirname =
'.'
1237 head = basename +
"."
1238 head_len = len(head)
1241 for f
in os.listdir(dirname):
1242 if f.startswith(head):
1243 req_plat = platformSplit(f[head_len:])
1244 if platform.issuperset(req_plat):
1245 candidates.append( (len(req_plat), f) )
1250 reference = os.path.join(dirname, candidates[-1][1])
1255 ignore =
r"Basket|.*size|Compression"):
1257 Compare the TTree summaries in stdout with the ones in trees_dict or in
1258 the reference file. By default ignore the size, compression and basket
1260 The presence of TTree summaries when none is expected is not a failure.
1262 if trees_dict
is None:
1265 if reference
and os.path.isfile(reference):
1270 from pprint
import PrettyPrinter
1271 pp = PrettyPrinter()
1273 result[
"GaudiTest.TTrees.expected"] = result.Quote(pp.pformat(trees_dict))
1275 result[
"GaudiTest.TTrees.ignore"] = result.Quote(ignore)
1280 causes.append(
"trees summaries")
1282 result[
"GaudiTest.TTrees.failure_on"] = result.Quote(msg)
1283 result[
"GaudiTest.TTrees.found"] = result.Quote(pp.pformat(trees))
1291 Compare the TTree summaries in stdout with the ones in trees_dict or in
1292 the reference file. By default ignore the size, compression and basket
1294 The presence of TTree summaries when none is expected is not a failure.
1299 if reference
and os.path.isfile(reference):
1304 from pprint
import PrettyPrinter
1305 pp = PrettyPrinter()
1307 result[
"GaudiTest.Histos.expected"] = result.Quote(pp.pformat(dict))
1309 result[
"GaudiTest.Histos.ignore"] = result.Quote(ignore)
1314 causes.append(
"histos summaries")
1316 result[
"GaudiTest.Histos.failure_on"] = result.Quote(msg)
1317 result[
"GaudiTest.Histos.found"] = result.Quote(pp.pformat(histos))
1323 Default validation action: compare standard output and error to the
1328 preproc = normalizeExamples
1332 if reference
and os.path.isfile(reference):
1333 result[
"GaudiTest.output_reference"] = reference
1336 "GaudiTest.output_diff",
1337 preproc = preproc)(stdout, result)
1345 newref = open(reference +
".new",
"w")
1347 for l
in stdout.splitlines():
1348 newref.write(l.rstrip() +
'\n')
1358 if reference
and os.path.isfile(reference):
1359 result[
"GaudiTest.error_reference"] = reference
1362 "GaudiTest.error_diff",
1363 preproc = preproc)(stderr, result)
1366 newref = open(reference +
".new",
"w")
1368 for l
in stderr.splitlines():
1369 newref.write(l.rstrip() +
'\n')
1374 "ExecTest.expected_stderr")(stderr, result)
1381 if self.validator.strip() !=
"":
1382 class CallWrapper(object):
1384 Small wrapper class to dynamically bind some default arguments
1387 def __init__(self, callable, extra_args = {}):
1391 from inspect
import getargspec
1397 def __call__(self, *args, **kwargs):
1401 kwargs = dict(kwargs)
1405 if a
not in positional
and a
not in kwargs:
1407 return apply(self.
callable, args, kwargs)
1409 exported_symbols = {
"self":self,
1414 "findReferenceBlock":
1415 CallWrapper(findReferenceBlock, {
"stdout":stdout,
1418 "validateWithReference":
1424 CallWrapper(countErrorLines, {
"stdout":stdout,
1427 "checkTTreesSummaries":
1431 "checkHistosSummaries":
1437 exec self.validator
in globals(), exported_symbols
1445 Add the content of the environment to the result object.
1447 Copied from the QMTest class of COOL.
1449 vars = os.environ.keys()
1451 result[
'GaudiTest.environment'] = \
1452 result.Quote(
'\n'.join([
"%s=%s"%(v,os.environ[v])
for v
in vars]))
1454 def Run(self, context, result):
1457 'context' -- A 'Context' giving run-time parameters to the
1460 'result' -- A 'Result' object. The outcome will be
1461 'Result.PASS' when this method is called. The 'result' may be
1462 modified by this method to indicate outcomes other than
1463 'Result.PASS' or to add annotations."""
1472 elif "GAUDIEXE" in os.environ:
1473 prog = os.environ[
"GAUDIEXE"]
1478 dummy, prog_ext = os.path.splitext(prog)
1479 if prog_ext
not in [
".exe",
".py",
".bat" ]
and self.
isWinPlatform():
1483 prog =
which(prog)
or prog
1486 args =
map(rationalizepath, self.args)
1493 if self.options.strip():
1495 if re.search(
r"from\s+Gaudi.Configuration\s+import\s+\*|from\s+Configurables\s+import", self.options):
1498 tmpfile.writelines(
"\n".join(self.options.splitlines()))
1500 args.append(tmpfile.name)
1501 result[
"GaudiTest.options"] = result.Quote(self.options)
1504 if prog_ext ==
".py":
1507 prog =
which(
"python.exe")
or "python.exe"
1509 prog =
which(
"python")
or "python"
1512 origdir = os.getcwd()
1514 os.chdir(str(os.path.normpath(os.path.expandvars(self.workdir))))
1516 if "QMTEST_TMPDIR" in os.environ:
1517 qmtest_tmpdir = os.environ[
"QMTEST_TMPDIR"]
1518 if not os.path.exists(qmtest_tmpdir):
1519 os.makedirs(qmtest_tmpdir)
1520 os.chdir(qmtest_tmpdir)
1521 elif "qmtest.tmpdir" in context:
1522 os.chdir(context[
"qmtest.tmpdir"])
1524 if "QMTEST_IGNORE_TIMEOUT" not in os.environ:
1537 if result.GetOutcome()
not in [ result.PASS ]:
1544 """Run the 'program'.
1546 'program' -- The path to the program to run.
1548 'arguments' -- A list of the arguments to the program. This
1549 list must contain a first argument corresponding to 'argv[0]'.
1551 'context' -- A 'Context' giving run-time parameters to the
1554 'result' -- A 'Result' object. The outcome will be
1555 'Result.PASS' when this method is called. The 'result' may be
1556 modified by this method to indicate outcomes other than
1557 'Result.PASS' or to add annotations.
1559 @attention: This method has been copied from command.ExecTestBase
1560 (QMTest 2.3.0) and modified to keep stdout and stderr
1561 for tests that have been terminated by a signal.
1562 (Fundamental for debugging in the Application Area)
1566 environment = self.MakeEnvironment(context)
1568 if "slc6" in environment.get(
'CMTCONFIG',
''):
1569 environment[
'TERM'] =
'dumb'
1582 exit_status = e.Run(arguments, environment, path = program)
1584 if e.stack_trace_file
and os.path.exists(e.stack_trace_file):
1585 stack_trace = open(e.stack_trace_file).read()
1586 os.remove(e.stack_trace_file)
1590 result[
"ExecTest.stack_trace"] = result.Quote(stack_trace)
1593 if (sys.platform ==
"win32" or os.WIFEXITED(exit_status)
1599 if self.exit_code
is None:
1601 elif sys.platform ==
"win32":
1602 exit_code = exit_status
1604 exit_code = os.WEXITSTATUS(exit_status)
1609 result[
"ExecTest.exit_code"] = str(exit_code)
1610 result[
"ExecTest.stdout"] = result.Quote(stdout)
1611 result[
"ExecTest.stderr"] = result.Quote(stderr)
1613 if exit_code != self.exit_code:
1614 causes.append(
"exit_code")
1615 result[
"ExecTest.expected_exit_code"] \
1616 = str(self.exit_code)
1621 result.Fail(
"Unexpected %s." % string.join(causes,
", "))
1622 elif os.WIFSIGNALED(exit_status):
1625 signal_number = str(os.WTERMSIG(exit_status))
1627 result.Fail(
"Program terminated by signal.")
1631 result.Fail(
"Exceeded time limit (%ds), terminated." % timeout)
1632 result[
"ExecTest.signal_number"] = signal_number
1633 result[
"ExecTest.stdout"] = result.Quote(e.stdout)
1634 result[
"ExecTest.stderr"] = result.Quote(e.stderr)
1636 result[
"ExecTest.expected_signal_number"] = str(self.
signal)
1637 elif os.WIFSTOPPED(exit_status):
1640 signal_number = str(os.WSTOPSIG(exit_status))
1642 result.Fail(
"Program stopped by signal.")
1646 result.Fail(
"Exceeded time limit (%ds), stopped." % timeout)
1647 result[
"ExecTest.signal_number"] = signal_number
1648 result[
"ExecTest.stdout"] = result.Quote(e.stdout)
1649 result[
"ExecTest.stderr"] = result.Quote(e.stderr)
1653 result.Fail(
"Program did not terminate normally.")
1659 result[
"ExecTest.stdout"] = result[
"ExecTest.stdout"].replace(esc,repr_esc)
1664 if 'NO_ECLIPSE_LAUNCHERS' in os.environ:
1669 projbasedir = os.path.normpath(destdir)
1670 while not os.path.exists(os.path.join(projbasedir,
".project")):
1671 oldprojdir = projbasedir
1672 projbasedir = os.path.normpath(os.path.join(projbasedir, os.pardir))
1675 if oldprojdir == projbasedir:
1679 if not os.path.exists(destdir):
1680 os.makedirs(destdir)
1682 from xml.etree
import ElementTree
as ET
1683 t = ET.parse(os.path.join(projbasedir,
".project"))
1684 projectName = t.find(
"name").text
1687 destfile =
"%s.launch" % self._Runnable__id
1689 destfile = os.path.join(destdir, destfile)
1691 if self.options.strip():
1695 tempfile = args.pop()
1696 optsfile = destfile + os.path.splitext(tempfile)[1]
1697 shutil.copyfile(tempfile, optsfile)
1698 args.append(optsfile)
1701 from xml.sax.saxutils
import quoteattr
1705 data[
"environment"] =
"\n".join([
'<mapEntry key=%s value=%s/>' % (quoteattr(k), quoteattr(v))
1706 for k, v
in os.environ.iteritems()
1707 if k
not in (
'MAKEOVERRIDES',
'MAKEFLAGS',
'MAKELEVEL')])
1709 data[
"exec"] =
which(prog)
or prog
1710 if os.path.basename(data[
"exec"]).lower().startswith(
"python"):
1711 data[
"stopAtMain"] =
"false"
1713 data[
"stopAtMain"] =
"true"
1715 data[
"args"] =
" ".join(
map(rationalizepath, args))
1717 data[
"args"] =
" ".join([
"/debugexe"] +
map(rationalizepath, [data[
"exec"]] + args))
1718 data[
"exec"] =
which(
"vcexpress.exe")
1721 data[
"workdir"] = os.getcwd()
1725 data[
"workdir"] = destdir
1727 data[
"project"] = projectName.strip()
1730 xml =
u"""<?xml version="1.0" encoding="UTF-8" standalone="no"?>
1731 <launchConfiguration type="org.eclipse.cdt.launch.applicationLaunchType">
1732 <booleanAttribute key="org.eclipse.cdt.debug.mi.core.AUTO_SOLIB" value="true"/>
1733 <listAttribute key="org.eclipse.cdt.debug.mi.core.AUTO_SOLIB_LIST"/>
1734 <stringAttribute key="org.eclipse.cdt.debug.mi.core.DEBUG_NAME" value="gdb"/>
1735 <stringAttribute key="org.eclipse.cdt.debug.mi.core.GDB_INIT" value=".gdbinit"/>
1736 <listAttribute key="org.eclipse.cdt.debug.mi.core.SOLIB_PATH"/>
1737 <booleanAttribute key="org.eclipse.cdt.debug.mi.core.STOP_ON_SOLIB_EVENTS" value="false"/>
1738 <booleanAttribute key="org.eclipse.cdt.debug.mi.core.breakpointsFullPath" value="false"/>
1739 <stringAttribute key="org.eclipse.cdt.debug.mi.core.commandFactory" value="org.eclipse.cdt.debug.mi.core.standardCommandFactory"/>
1740 <stringAttribute key="org.eclipse.cdt.debug.mi.core.protocol" value="mi"/>
1741 <booleanAttribute key="org.eclipse.cdt.debug.mi.core.verboseMode" value="false"/>
1742 <intAttribute key="org.eclipse.cdt.launch.ATTR_BUILD_BEFORE_LAUNCH_ATTR" value="0"/>
1743 <stringAttribute key="org.eclipse.cdt.launch.COREFILE_PATH" value=""/>
1744 <stringAttribute key="org.eclipse.cdt.launch.DEBUGGER_ID" value="org.eclipse.cdt.debug.mi.core.CDebuggerNew"/>
1745 <stringAttribute key="org.eclipse.cdt.launch.DEBUGGER_REGISTER_GROUPS" value=""/>
1746 <stringAttribute key="org.eclipse.cdt.launch.DEBUGGER_START_MODE" value="run"/>
1747 <booleanAttribute key="org.eclipse.cdt.launch.DEBUGGER_STOP_AT_MAIN" value="%(stopAtMain)s"/>
1748 <stringAttribute key="org.eclipse.cdt.launch.DEBUGGER_STOP_AT_MAIN_SYMBOL" value="main"/>
1749 <booleanAttribute key="org.eclipse.cdt.launch.ENABLE_REGISTER_BOOKKEEPING" value="false"/>
1750 <booleanAttribute key="org.eclipse.cdt.launch.ENABLE_VARIABLE_BOOKKEEPING" value="false"/>
1751 <stringAttribute key="org.eclipse.cdt.launch.FORMAT" value="<?xml version="1.0" encoding="UTF-8" standalone="no"?><contentList/>"/>
1752 <stringAttribute key="org.eclipse.cdt.launch.GLOBAL_VARIABLES" value="<?xml version="1.0" encoding="UTF-8" standalone="no"?> <globalVariableList/> "/>
1753 <stringAttribute key="org.eclipse.cdt.launch.MEMORY_BLOCKS" value="<?xml version="1.0" encoding="UTF-8" standalone="no"?> <memoryBlockExpressionList/> "/>
1754 <stringAttribute key="org.eclipse.cdt.launch.PROGRAM_ARGUMENTS" value="%(args)s"/>
1755 <stringAttribute key="org.eclipse.cdt.launch.PROGRAM_NAME" value="%(exec)s"/>
1756 <stringAttribute key="org.eclipse.cdt.launch.PROJECT_ATTR" value="%(project)s"/>
1757 <stringAttribute key="org.eclipse.cdt.launch.PROJECT_BUILD_CONFIG_ID_ATTR" value=""/>
1758 <stringAttribute key="org.eclipse.cdt.launch.WORKING_DIRECTORY" value="%(workdir)s"/>
1759 <booleanAttribute key="org.eclipse.cdt.launch.ui.ApplicationCDebuggerTab.DEFAULTS_SET" value="true"/>
1760 <booleanAttribute key="org.eclipse.cdt.launch.use_terminal" value="true"/>
1761 <listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_PATHS">
1762 <listEntry value="/%(project)s"/>
1764 <listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_TYPES">
1765 <listEntry value="4"/>
1767 <booleanAttribute key="org.eclipse.debug.core.appendEnvironmentVariables" value="false"/>
1768 <mapAttribute key="org.eclipse.debug.core.environmentVariables">
1771 <mapAttribute key="org.eclipse.debug.core.preferred_launchers">
1772 <mapEntry key="[debug]" value="org.eclipse.cdt.cdi.launch.localCLaunch"/>
1774 <listAttribute key="org.eclipse.debug.ui.favoriteGroups">
1775 <listEntry value="org.eclipse.debug.ui.launchGroup.debug"/>
1777 </launchConfiguration>
1781 open(destfile,
"w").write(xml)
1789 import simplejson
as json
1792 """An 'HTMLResultStream' writes its output to a set of HTML files.
1794 The argument 'dir' is used to select the destination directory for the HTML
1796 The destination directory may already contain the report from a previous run
1797 (for example of a different package), in which case it will be extended to
1798 include the new data.
1801 qm.fields.TextField(
1803 title =
"Destination Directory",
1804 description =
"""The name of the directory.
1806 All results will be written to the directory indicated.""",
1808 default_value =
""),
1812 """Prepare the destination directory.
1814 Creates the destination directory and store in it some preliminary
1815 annotations and the static files found in the template directory
1818 ResultStream.__init__(self, arguments, **args)
1823 templateDir = os.path.join(os.path.dirname(__file__),
"html_report")
1824 if not os.path.isdir(self.dir):
1825 os.makedirs(self.dir)
1827 for f
in os.listdir(templateDir):
1828 src = os.path.join(templateDir, f)
1829 dst = os.path.join(self.dir, f)
1830 if not os.path.isdir(src)
and not os.path.exists(dst):
1831 shutil.copy(src, dst)
1833 if "CMTCONFIG" in os.environ:
1839 """Helper function to extend the global summary file in the destination
1846 ids = set([ i[
"id"]
for i
in self.
_summary ])
1847 newSummary = [ i
for i
in oldSummary
if i[
"id"]
not in ids ]
1853 """Writes the annotation to the annotation file.
1854 If the key is already present with a different value, the value becomes
1855 a list and the new value is appended to it, except for start_time and
1864 key, value =
map(str, [key, value])
1865 if key ==
"qmtest.run.start_time":
1870 if key
not in annotations:
1871 annotations[key] = value
1872 if "qmtest.run.end_time" in annotations:
1873 del annotations[
"qmtest.run.end_time"]
1876 if key
in annotations:
1877 old = annotations[key]
1878 if type(old)
is list:
1879 if value
not in old:
1880 annotations[key].append(value)
1882 annotations[key] = [old, value]
1884 annotations[key] = value
1890 """Prepare the test result directory in the destination directory storing
1891 into it the result fields.
1892 A summary of the test result is stored both in a file in the test directory
1893 and in the global summary file.
1896 summary[
"id"] = result.GetId()
1897 summary[
"outcome"] = result.GetOutcome()
1898 summary[
"cause"] = result.GetCause()
1899 summary[
"fields"] = result.keys()
1900 summary[
"fields"].sort()
1903 for f
in [
"id",
"outcome",
"cause"]:
1904 summary[f] = str(summary[f])
1905 summary[
"fields"] =
map(str, summary[
"fields"])
1907 self._summary.append(summary)
1913 testOutDir = os.path.join(self.dir, summary[
"id"])
1914 if not os.path.isdir(testOutDir):
1915 os.makedirs(testOutDir)
1916 json.dump(summary, open(os.path.join(testOutDir,
"summary.json"),
"w"),
1918 for f
in summary[
"fields"]:
1919 open(os.path.join(testOutDir, f),
"w").write(result[f])
1930 class XMLResultStream(ResultStream):
1931 """An 'XMLResultStream' writes its output to a Ctest XML file.
1933 The argument 'dir' is used to select the destination file for the XML
1935 The destination directory may already contain the report from a previous run
1936 (for example of a different package), in which case it will be overrided to
1940 qm.fields.TextField(
1942 title =
"Destination Directory",
1943 description =
"""The name of the directory.
1945 All results will be written to the directory indicated.""",
1947 default_value =
""),
1948 qm.fields.TextField(
1950 title =
"Output File Prefix",
1951 description =
"""The output file name will be the specified prefix
1952 followed by 'Test.xml' (CTest convention).""",
1954 default_value =
""),
1958 """Prepare the destination directory.
1960 Creates the destination directory and store in it some preliminary
1963 ResultStream.__init__(self, arguments, **args)
1965 self.
_xmlFile = os.path.join(self.dir, self.prefix +
'Test.xml')
1971 if not os.path.isfile(self.
_xmlFile):
1973 if not os.path.exists(os.path.dirname(self.
_xmlFile)):
1974 os.makedirs(os.path.dirname(self.
_xmlFile))
1976 newdataset = ET.Element(
"newdataset")
1982 newdataset = self._tree.getroot()
1989 for site
in newdataset.getiterator() :
1990 if site.get(
"OSPlatform") == os.uname()[4]:
2000 import multiprocessing
2002 "BuildName" : os.getenv(
"CMTCONFIG"),
2003 "Name" : os.uname()[1] ,
2004 "Generator" :
"QMTest "+qm.version ,
2005 "OSName" : os.uname()[0] ,
2006 "Hostname" : socket.gethostname() ,
2007 "OSRelease" : os.uname()[2] ,
2008 "OSVersion" :os.uname()[3] ,
2009 "OSPlatform" :os.uname()[4] ,
2010 "Is64Bits" :
"unknown" ,
2011 "VendorString" :
"unknown" ,
2012 "VendorID" :
"unknown" ,
2013 "FamilyID" :
"unknown" ,
2014 "ModelID" :
"unknown" ,
2015 "ProcessorCacheSize" :
"unknown" ,
2016 "NumberOfLogicalCPU" : str(multiprocessing.cpu_count()) ,
2017 "NumberOfPhysicalCPU" :
"0" ,
2018 "TotalVirtualMemory" :
"0" ,
2019 "TotalPhysicalMemory" :
"0" ,
2020 "LogicalProcessorsPerPhysical" :
"0" ,
2021 "ProcessorClockFrequency" :
"0" ,
2023 self.
_site = ET.SubElement(newdataset,
"site", attrib)
2046 self.
_Testing = self._site.find(
"Testing")
2049 self.
_TestList = self._Testing.find(
"TestList")
2055 # Add some non-QMTest attributes
2056 if "CMTCONFIG" in os.environ:
2057 self.WriteAnnotation("cmt.cmtconfig", os.environ["CMTCONFIG"])
2059 self.WriteAnnotation("hostname", socket.gethostname())
2064 if key ==
"qmtest.run.start_time":
2065 if self._site.get(
"qmtest.run.start_time")
is not None :
2067 self._site.set(str(key),str(value))
2069 """Prepare the test result directory in the destination directory storing
2070 into it the result fields.
2071 A summary of the test result is stored both in a file in the test directory
2072 and in the global summary file.
2075 summary[
"id"] = result.GetId()
2076 summary[
"outcome"] = result.GetOutcome()
2077 summary[
"cause"] = result.GetCause()
2078 summary[
"fields"] = result.keys()
2079 summary[
"fields"].sort()
2083 for f
in [
"id",
"outcome",
"cause"]:
2084 summary[f] = str(summary[f])
2085 summary[
"fields"] =
map(str, summary[
"fields"])
2091 if "qmtest.start_time" in summary[
"fields"]:
2092 haveStartDate =
True
2094 haveStartDate =
False
2095 if "qmtest.end_time" in summary[
"fields"]:
2102 self.
_startTime = calendar.timegm(time.strptime(result[
"qmtest.start_time"],
"%Y-%m-%dT%H:%M:%SZ"))
2103 if self._StartTestTime.text
is None:
2104 self._StartDateTime.text = time.strftime(
"%b %d %H:%M %Z", time.localtime(self.
_startTime))
2105 self._StartTestTime.text = str(self.
_startTime)
2106 self._site.set(
"BuildStamp" , result[
"qmtest.start_time"] )
2110 self.
_endTime = calendar.timegm(time.strptime(result[
"qmtest.end_time"],
"%Y-%m-%dT%H:%M:%SZ"))
2114 tl = ET.Element(
"Test")
2115 tl.text = summary[
"id"]
2116 self._TestList.insert(0,tl)
2119 Test = ET.Element(
"Test")
2120 if summary[
"outcome"] ==
"PASS":
2121 Test.set(
"Status",
"passed")
2122 elif summary[
"outcome"] ==
"FAIL":
2123 Test.set(
"Status",
"failed")
2124 elif summary[
"outcome"] ==
"SKIPPED" or summary[
"outcome"] ==
"UNTESTED":
2125 Test.set(
"Status",
"skipped")
2126 elif summary[
"outcome"] ==
"ERROR":
2127 Test.set(
"Status",
"failed")
2128 Name = ET.SubElement(Test,
"Name",)
2129 Name.text = summary[
"id"]
2130 Results = ET.SubElement(Test,
"Results")
2133 self._Testing.insert(3,Test)
2135 if haveStartDate
and haveEndDate:
2138 testduration = str(delta)
2139 Testduration= ET.SubElement(Results,
"NamedMeasurement")
2140 Testduration.set(
"name",
"Execution Time")
2141 Testduration.set(
"type",
"numeric/float" )
2142 value = ET.SubElement(Testduration,
"Value")
2143 value.text = testduration
2146 for n
in (
"qmtest.end_time",
"qmtest.start_time",
"qmtest.cause",
"ExecTest.stdout"):
2147 if n
in summary[
"fields"]:
2148 summary[
"fields"].
remove(n)
2152 if "ExecTest.exit_code" in summary[
"fields"] :
2153 summary[
"fields"].
remove(
"ExecTest.exit_code")
2154 ExitCode= ET.SubElement(Results,
"NamedMeasurement")
2155 ExitCode.set(
"name",
"exit_code")
2156 ExitCode.set(
"type",
"numeric/integer" )
2157 value = ET.SubElement(ExitCode,
"Value")
2160 TestStartTime= ET.SubElement(Results,
"NamedMeasurement")
2161 TestStartTime.set(
"name",
"Start_Time")
2162 TestStartTime.set(
"type",
"String" )
2163 value = ET.SubElement(TestStartTime,
"Value")
2169 TestEndTime= ET.SubElement(Results,
"NamedMeasurement")
2170 TestEndTime.set(
"name",
"End_Time")
2171 TestEndTime.set(
"type",
"String" )
2172 value = ET.SubElement(TestEndTime,
"Value")
2178 if summary[
"cause"]:
2179 FailureCause= ET.SubElement(Results,
"NamedMeasurement")
2180 FailureCause.set(
"name",
"Cause")
2181 FailureCause.set(
"type",
"String" )
2182 value = ET.SubElement(FailureCause,
"Value")
2187 for field
in summary[
"fields"] :
2188 fields[field] = ET.SubElement(Results,
"NamedMeasurement")
2189 fields[field].set(
"type",
"String")
2190 fields[field].set(
"name",field)
2191 value = ET.SubElement(fields[field],
"Value")
2193 if "<pre>" in result[field][0:6] :
2199 if result.has_key(
"ExecTest.stdout" ) :
2200 Measurement = ET.SubElement(Results,
"Measurement")
2201 value = ET.SubElement(Measurement,
"Value")
2202 if "<pre>" in result[
"ExecTest.stdout"][0:6] :
2209 self._tree.write(self.
_xmlFile,
"utf-8")
2215 self._EndTestTime.text = str(self.
_endTime)
2216 self._EndDateTime.text = time.strftime(
"%b %d %H:%M %Z", time.localtime(self.
_endTime))
2223 self._ElapsedMinutes.text = str(delta/60)
2226 self._tree.write(self.
_xmlFile,
"utf-8")