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 orignal 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."""
393 return s1.splitlines() == s2.splitlines()
396 """ Base class for a callable that takes a file and returns a modified
401 if hasattr(input,
"__iter__"):
405 lines = input.splitlines()
410 if l: output.append(l)
411 if mergeback: output =
'\n'.join(output)
435 if line.find(s) >= 0:
return None
437 if r.search(line):
return None
447 if self.
start in line:
450 elif self.
end in line:
459 when = re.compile(when)
462 if isinstance(rhs, RegexpReplacer):
464 res._operations = self.
_operations + rhs._operations
466 res = FilePreprocessor.__add__(self, rhs)
470 if w
is None or w.search(line):
471 line = o.sub(r, line)
476 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)?",
477 "00:00:00 1970-01-01")
479 normalizeEOL.__processLine__ =
lambda line: str(line).rstrip() +
'\n'
483 skipEmptyLines.__processLine__ =
lambda line: (line.strip()
and line)
or None
494 line = line[:(pos+self.
siglen)]
495 lst = line[(pos+self.
siglen):].split()
497 line +=
" ".join(lst)
501 normalizeExamples = maskPointers + normalizeDate
504 (
"TIMER.TIMER",
r"\s+[+-]?[0-9]+[0-9.]*",
" 0"),
505 (
"release all pending",
r"^.*/([^/]*:.*)",
r"\1"),
506 (
"0x########",
r"\[.*/([^/]*.*)\]",
r"[\1]"),
507 (
"^#.*file",
r"file '.*[/\\]([^/\\]*)$",
r"file '\1"),
508 (
"^JobOptionsSvc.*options successfully read in from",
r"read in from .*[/\\]([^/\\]*)$",
r"file \1"),
510 (
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"),
512 (
"ServiceLocatorHelper::",
"ServiceLocatorHelper::(create|locate)Service",
"ServiceLocatorHelper::service"),
514 (
None,
r"e([-+])0([0-9][0-9])",
r"e\1\2"),
516 (
None,
r'Service reference count check:',
r'Looping over all active services...'),
521 "JobOptionsSvc INFO # ",
522 "JobOptionsSvc WARNING # ",
525 "This machine has a speed",
528 "ToolSvc.Sequenc... INFO",
529 "DataListenerSvc INFO XML written to file:",
530 "[INFO]",
"[WARNING]",
531 "DEBUG No writable file catalog found which contains FID:",
533 "DEBUG Service base class initialized successfully",
534 "DEBUG Incident timing:",
535 "INFO 'CnvServices':[",
539 r"^JobOptionsSvc INFO *$",
541 r"(Always|SUCCESS)\s*(Root f|[^ ]* F)ile version:",
542 r"0x[0-9a-fA-F#]+ *Algorithm::sysInitialize\(\) *\[",
543 r"0x[0-9a-fA-F#]* *__gxx_personality_v0 *\[",
544 r"File '.*.xml' does not exist",
545 r"INFO Refer to dataset .* by its file ID:",
546 r"INFO Referring to dataset .* by its file ID:",
547 r"INFO Disconnect from dataset",
548 r"INFO Disconnected from dataset",
549 r"INFO Disconnected data IO:",
550 r"IncidentSvc\s*(DEBUG (Adding|Removing)|VERBOSE Calling)",
552 r"^StatusCodeSvc.*listing all unchecked return codes:",
553 r"^StatusCodeSvc\s*INFO\s*$",
554 r"Num\s*\|\s*Function\s*\|\s*Source Library",
557 r"ERROR Failed to modify file: .* Errno=2 No such file or directory",
559 r"^ +[0-9]+ \|.*ROOT",
560 r"^ +[0-9]+ \|.*\|.*Dict",
564 r"SUCCESS\s*Booked \d+ Histogram\(s\)",
569 normalizeExamples = (lineSkipper + normalizeExamples + skipEmptyLines +
570 normalizeEOL +
LineSorter(
"Services to release : "))
573 def __init__(self, reffile, cause, result_key, preproc = normalizeExamples):
580 if os.path.isfile(self.
reffile):
581 orig = open(self.
reffile).xreadlines()
587 new = stdout.splitlines()
591 diffs = difflib.ndiff(orig,new,charjunk=difflib.IS_CHARACTER_JUNK)
592 filterdiffs =
map(
lambda x: x.strip(),filter(
lambda x: x[0] !=
" ",diffs))
595 result[self.
result_key] = result.Quote(
"\n".join(filterdiffs))
599 +) standard output of the test""")
600 causes.append(self.
cause)
607 def findReferenceBlock(reference, stdout, result, causes, signature_offset=0, signature=None,
610 Given a block of text, tries to find it in the output.
611 The block had to be identified by a signature line. By default, the first
612 line is used as signature, or the line pointed to by signature_offset. If
613 signature_offset points outside the block, a signature line can be passed as
614 signature argument. Note: if 'signature' is None (the default), a negative
615 signature_offset is interpreted as index in a list (e.g. -1 means the last
616 line), otherwise the it is interpreted as the number of lines before the
617 first one of the block the signature must appear.
618 The parameter 'id' allow to distinguish between different calls to this
619 function in the same validation code.
622 reflines = filter(
None,
map(
lambda s: s.rstrip(), reference.splitlines()))
624 raise RuntimeError(
"Empty (or null) reference")
626 outlines = filter(
None,
map(
lambda s: s.rstrip(), stdout.splitlines()))
628 res_field =
"GaudiTest.RefBlock"
630 res_field +=
"_%s" % id
632 if signature
is None:
633 if signature_offset < 0:
634 signature_offset = len(reference)+signature_offset
635 signature = reflines[signature_offset]
638 pos = outlines.index(signature)
639 outlines = outlines[pos-signature_offset:pos+len(reflines)-signature_offset]
640 if reflines != outlines:
641 msg =
"standard output"
643 if not msg
in causes:
645 result[res_field +
".observed"] = result.Quote(
"\n".join(outlines))
647 causes.append(
"missing signature")
648 result[res_field +
".signature"] = result.Quote(signature)
649 if len(reflines) > 1
or signature != reflines[0]:
650 result[res_field +
".expected"] = result.Quote(
"\n".join(reflines))
656 Count the number of messages with required severity (by default ERROR and FATAL)
657 and check if their numbers match the expected ones (0 by default).
658 The dictionary "expected" can be used to tune the number of errors and fatals
659 allowed, or to limit the number of expected warnings etc.
661 stdout = kwargs[
"stdout"]
662 result = kwargs[
"result"]
663 causes = kwargs[
"causes"]
670 outlines = stdout.splitlines()
671 from math
import log10
672 fmt =
"%%%dd - %%s" % (int(log10(len(outlines))+1))
678 if len(words) >= 2
and words[1]
in errors:
679 errors[words[1]].append(fmt%(linecount,l.rstrip()))
682 if len(errors[e]) != expected[e]:
683 causes.append(
'%s(%d)'%(e,len(errors[e])))
684 result[
"GaudiTest.lines.%s"%e] = result.Quote(
'\n'.join(errors[e]))
685 result[
"GaudiTest.lines.%s.expected#"%e] = result.Quote(str(expected[e]))
692 Parse the TTree summary table in lines, starting from pos.
693 Returns a tuple with the dictionary with the digested informations and the
694 position of the first line after the summary.
700 splitcols =
lambda l: [ f.strip()
for f
in l.strip(
"*\n").split(
':',2) ]
703 cols = splitcols(ll[0])
704 r[
"Name"], r[
"Title"] = cols[1:]
706 cols = splitcols(ll[1])
707 r[
"Entries"] = int(cols[1])
709 sizes = cols[2].split()
710 r[
"Total size"] = int(sizes[2])
711 if sizes[-1] ==
"memory":
714 r[
"File size"] = int(sizes[-1])
716 cols = splitcols(ll[2])
717 sizes = cols[2].split()
718 if cols[0] ==
"Baskets":
719 r[
"Baskets"] = int(cols[1])
720 r[
"Basket size"] = int(sizes[2])
721 r[
"Compression"] = float(sizes[-1])
724 if i < (count - 3)
and lines[i].startswith(
"*Tree"):
725 result = parseblock(lines[i:i+3])
726 result[
"Branches"] = {}
728 while i < (count - 3)
and lines[i].startswith(
"*Br"):
729 if i < (count - 2)
and lines[i].startswith(
"*Branch "):
733 branch = parseblock(lines[i:i+3])
734 result[
"Branches"][branch[
"Name"]] = branch
741 Scan stdout to find ROOT TTree summaries and digest them.
743 stars = re.compile(
r"^\*+$")
744 outlines = stdout.splitlines()
745 nlines = len(outlines)
751 while i < nlines
and not stars.match(outlines[i]):
756 trees[tree[
"Name"]] = tree
762 Check that all the keys in reference are in to_check too, with the same value.
763 If the value is a dict, the function is called recursively. to_check can
764 contain more keys than reference, that will not be tested.
765 The function returns at the first difference found.
770 ignore_re = re.compile(ignore)
771 keys = [ key
for key
in reference
if not ignore_re.match(key) ]
773 keys = reference.keys()
777 if (
type(reference[k])
is dict)
and (
type(to_check[k])
is dict):
779 failed = fail_keys =
cmpTreesDicts(reference[k], to_check[k], ignore)
782 failed = to_check[k] != reference[k]
787 fail_keys.insert(0, k)
797 if c
is None or r
is None:
799 return (fail_path, r, c)
802 h_count_re = re.compile(
r"^(.*)SUCCESS\s+Booked (\d+) Histogram\(s\) :\s+(.*)")
806 Extract the histograms infos from the lines starting at pos.
807 Returns the position of the first line after the summary block.
810 h_table_head = re.compile(
r'SUCCESS\s+List of booked (1D|2D|3D|1D profile|2D profile) histograms in directory\s+"(\w*)"')
811 h_short_summ = re.compile(
r"ID=([^\"]+)\s+\"([^\"]+)\"\s+(.*)")
816 m = h_count_re.search(lines[pos])
817 name = m.group(1).strip()
818 total = int(m.group(2))
820 for k, v
in [ x.split(
"=")
for x
in m.group(3).split() ]:
823 header[
"Total"] = total
827 m = h_table_head.search(lines[pos])
830 t = t.replace(
" profile",
"Prof")
837 if l.startswith(
" | ID"):
839 titles = [ x.strip()
for x
in l.split(
"|")][1:]
841 while pos < nlines
and lines[pos].startswith(
" |"):
843 values = [ x.strip()
for x
in l.split(
"|")][1:]
845 for i
in range(len(titles)):
846 hcont[titles[i]] = values[i]
847 cont[hcont[
"ID"]] = hcont
849 elif l.startswith(
" ID="):
850 while pos < nlines
and lines[pos].startswith(
" ID="):
851 values = [ x.strip()
for x
in h_short_summ.search(lines[pos]).groups() ]
852 cont[values[0]] = values
855 raise RuntimeError(
"Cannot understand line %d: '%s'" % (pos, l))
859 summ[d][
"header"] = header
864 summ[name] = {
"header": header}
869 Scan stdout to find ROOT TTree summaries and digest them.
871 outlines = stdout.splitlines()
872 nlines = len(outlines) - 1
880 match = h_count_re.search(outlines[pos])
881 while pos < nlines
and not match:
883 match = h_count_re.search(outlines[pos])
886 summaries.update(summ)
891 """Create a new 'Filter'.
893 'input' -- The string containing the input to provide to the
896 'timeout' -- As for 'TimeoutExecutable.__init__'."""
898 super(GaudiFilterExecutable, self).
__init__(input, timeout)
905 tmpf = tempfile.mkstemp()
910 """Copied from TimeoutExecutable to allow the re-implementation of
913 if sys.platform ==
"win32":
924 """Code copied from both FilterExecutable and TimeoutExecutable.
928 self._ClosePipeEnd(self._stdin_pipe[0])
929 if self._stdout_pipe:
930 self._ClosePipeEnd(self._stdout_pipe[1])
931 if self._stderr_pipe:
932 self._ClosePipeEnd(self._stderr_pipe[1])
940 super(qm.executable.TimeoutExecutable, self).
_HandleChild()
947 child_pid = self._GetChildPID()
949 os.setpgid(child_pid, child_pid)
977 os.setpgid(0, child_pid)
986 max_fds = os.sysconf(
"SC_OPEN_MAX")
989 for fd
in xrange(max_fds):
1000 if sys.platform ==
"linux2":
1002 os.path.join(
"/proc", str(child_pid),
"exe"),
1004 "-batch",
"-n",
"-x",
1005 "'%s'" % os.path.join(os.path.dirname(__file__),
"stack-trace.gdb")]
1008 o = os.popen(
" ".join(cmd)).read()
1013 os.kill(0, signal.SIGKILL)
1016 select.select ([], [], [])
1021 elif self.
__timeout >= 0
and sys.platform ==
"win32":
1024 self.__monitor_thread.start()
1026 if sys.platform ==
"win32":
1029 """Code copied from FilterExecutable.
1030 Kill the child if the timeout expires.
1032 This function is run in the monitoring thread."""
1040 result = win32event.WaitForSingleObject(self._GetChildPID(),
1043 if result == win32con.WAIT_TIMEOUT:
1050 """Standard Gaudi test.
1053 qm.fields.TextField(
1057 description=
"""The path to the program.
1059 This field indicates the path to the program. If it is not
1060 an absolute path, the value of the 'PATH' environment
1061 variable will be used to search for the program.
1062 If not specified, $GAUDIEXE or Gaudi.exe are used.
1065 qm.fields.SetField(qm.fields.TextField(
1067 title=
"Argument List",
1068 description=
"""The command-line arguments.
1070 If this field is left blank, the program is run without any
1073 Use this field to specify the option files.
1075 An implicit 0th argument (the path to the program) is added
1078 qm.fields.TextField(
1081 description=
"""Options to be passed to the application.
1083 This field allows to pass a list of options to the main program
1084 without the need of a separate option file.
1086 The content of the field is written to a temporary file which name
1087 is passed the the application as last argument (appended to the
1088 field "Argument List".
1094 qm.fields.TextField(
1096 title=
"Working Directory",
1097 description=
"""Path to the working directory.
1099 If this field is left blank, the program will be run from the qmtest
1100 directory, otherwise from the directory specified.""",
1103 qm.fields.TextField(
1105 title=
"Reference Output",
1106 description=
"""Path to the file containing the reference output.
1108 If this field is left blank, any standard output will be considered
1111 If the reference file is specified, any output on standard error is
1114 qm.fields.TextField(
1115 name=
"error_reference",
1116 title=
"Reference for standard error",
1117 description=
"""Path to the file containing the reference for the standard error.
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.SetField(qm.fields.TextField(
1126 name =
"unsupported_platforms",
1127 title =
"Unsupported Platforms",
1128 description =
"""Platform on which the test must not be run.
1130 List of regular expressions identifying the platforms on which the
1131 test is not run and the result is set to UNTESTED."""
1134 qm.fields.TextField(
1136 title =
"Validator",
1137 description =
"""Function to validate the output of the test.
1139 If defined, the function is used to validate the products of the
1141 The function is called passing as arguments:
1142 self: the test class instance
1143 stdout: the standard output of the executed test
1144 stderr: the standard error of the executed test
1145 result: the Result objects to fill with messages
1146 The function must return a list of causes for the failure.
1147 If specified, overrides standard output, standard error and
1155 qm.fields.BooleanField(
1156 name =
"use_temp_dir",
1157 title =
"Use temporary directory",
1158 description =
"""Use temporary directory.
1160 If set to true, use a temporary directory as working directory.
1162 default_value=
"false"
1165 qm.fields.IntegerField(
1167 title =
"Expected signal",
1168 description =
"""Expect termination by signal.""",
1175 unsupported = [ re.compile(x)
1176 for x
in [ str(y).strip()
1177 for y
in self.unsupported_platforms ]
1180 for p_re
in unsupported:
1181 if p_re.search(platform):
1182 result.SetOutcome(result.UNTESTED)
1183 result[result.CAUSE] =
'Platform not supported.'
1189 Return the platform Id defined in CMTCONFIG or SCRAM_ARCH.
1193 if "CMTCONFIG" in os.environ:
1194 arch = os.environ[
"CMTCONFIG"]
1195 elif "SCRAM_ARCH" in os.environ:
1196 arch = os.environ[
"SCRAM_ARCH"]
1201 Return True if the current platform is Windows.
1203 This function was needed because of the change in the CMTCONFIG format,
1204 from win32_vc71_dbg to i686-winxp-vc9-dbg.
1207 return "winxp" in platform
or platform.startswith(
"win")
1215 platformSplit =
lambda p: set(p.split(
'-' in p
and '-' or '_'))
1217 reference = os.path.normpath(os.path.expandvars(reffile))
1219 spec_ref = reference[:-3] + self.
GetPlatform()[0:3] + reference[-3:]
1220 if os.path.isfile(spec_ref):
1221 reference = spec_ref
1224 dirname, basename = os.path.split(reference)
1225 if not dirname: dirname =
'.'
1226 head = basename +
"."
1227 head_len = len(head)
1230 for f
in os.listdir(dirname):
1231 if f.startswith(head):
1232 req_plat = platformSplit(f[head_len:])
1233 if platform.issuperset(req_plat):
1234 candidates.append( (len(req_plat), f) )
1239 reference = os.path.join(dirname, candidates[-1][1])
1244 ignore =
r"Basket|.*size|Compression"):
1246 Compare the TTree summaries in stdout with the ones in trees_dict or in
1247 the reference file. By default ignore the size, compression and basket
1249 The presence of TTree summaries when none is expected is not a failure.
1251 if trees_dict
is None:
1254 if reference
and os.path.isfile(reference):
1259 from pprint
import PrettyPrinter
1260 pp = PrettyPrinter()
1262 result[
"GaudiTest.TTrees.expected"] = result.Quote(pp.pformat(trees_dict))
1264 result[
"GaudiTest.TTrees.ignore"] = result.Quote(ignore)
1269 causes.append(
"trees summaries")
1271 result[
"GaudiTest.TTrees.failure_on"] = result.Quote(msg)
1272 result[
"GaudiTest.TTrees.found"] = result.Quote(pp.pformat(trees))
1280 Compare the TTree summaries in stdout with the ones in trees_dict or in
1281 the reference file. By default ignore the size, compression and basket
1283 The presence of TTree summaries when none is expected is not a failure.
1288 if reference
and os.path.isfile(reference):
1293 from pprint
import PrettyPrinter
1294 pp = PrettyPrinter()
1296 result[
"GaudiTest.Histos.expected"] = result.Quote(pp.pformat(dict))
1298 result[
"GaudiTest.Histos.ignore"] = result.Quote(ignore)
1303 causes.append(
"histos summaries")
1305 result[
"GaudiTest.Histos.failure_on"] = result.Quote(msg)
1306 result[
"GaudiTest.Histos.found"] = result.Quote(pp.pformat(histos))
1312 Default validation action: compare standard output and error to the
1317 preproc = normalizeExamples
1321 if reference
and os.path.isfile(reference):
1322 result[
"GaudiTest.output_reference"] = reference
1325 "GaudiTest.output_diff",
1326 preproc = preproc)(stdout, result)
1334 newref = open(reference +
".new",
"w")
1336 for l
in stdout.splitlines():
1337 newref.write(l.rstrip() +
'\n')
1347 if reference
and os.path.isfile(reference):
1348 result[
"GaudiTest.error_reference"] = reference
1351 "GaudiTest.error_diff",
1352 preproc = preproc)(stderr, result)
1355 newref = open(reference +
".new",
"w")
1357 for l
in stderr.splitlines():
1358 newref.write(l.rstrip() +
'\n')
1363 "ExecTest.expected_stderr")(stderr, result)
1370 if self.validator.strip() !=
"":
1371 class CallWrapper(object):
1373 Small wrapper class to dynamically bind some default arguments
1376 def __init__(self, callable, extra_args = {}):
1380 from inspect
import getargspec
1386 def __call__(self, *args, **kwargs):
1390 kwargs = dict(kwargs)
1394 if a
not in positional
and a
not in kwargs:
1396 return apply(self.
callable, args, kwargs)
1398 exported_symbols = {
"self":self,
1403 "findReferenceBlock":
1404 CallWrapper(findReferenceBlock, {
"stdout":stdout,
1407 "validateWithReference":
1413 CallWrapper(countErrorLines, {
"stdout":stdout,
1416 "checkTTreesSummaries":
1420 "checkHistosSummaries":
1426 exec self.validator
in globals(), exported_symbols
1434 Add the content of the environment to the result object.
1436 Copied from the QMTest class of COOL.
1438 vars = os.environ.keys()
1440 result[
'GaudiTest.environment'] = \
1441 result.Quote(
'\n'.join([
"%s=%s"%(v,os.environ[v])
for v
in vars]))
1443 def Run(self, context, result):
1446 'context' -- A 'Context' giving run-time parameters to the
1449 'result' -- A 'Result' object. The outcome will be
1450 'Result.PASS' when this method is called. The 'result' may be
1451 modified by this method to indicate outcomes other than
1452 'Result.PASS' or to add annotations."""
1461 elif "GAUDIEXE" in os.environ:
1462 prog = os.environ[
"GAUDIEXE"]
1467 dummy, prog_ext = os.path.splitext(prog)
1468 if prog_ext
not in [
".exe",
".py",
".bat" ]
and self.
isWinPlatform():
1472 prog =
which(prog)
or prog
1475 args =
map(rationalizepath, self.args)
1482 if self.options.strip():
1484 if re.search(
r"from\s+Gaudi.Configuration\s+import\s+\*|from\s+Configurables\s+import", self.options):
1487 tmpfile.writelines(
"\n".join(self.options.splitlines()))
1489 args.append(tmpfile.name)
1490 result[
"GaudiTest.options"] = result.Quote(self.options)
1493 if prog_ext ==
".py":
1496 prog =
which(
"python.exe")
or "python.exe"
1498 prog =
which(
"python")
or "python"
1501 origdir = os.getcwd()
1503 os.chdir(str(os.path.normpath(os.path.expandvars(self.workdir))))
1505 if "QMTEST_TMPDIR" in os.environ:
1506 qmtest_tmpdir = os.environ[
"QMTEST_TMPDIR"]
1507 if not os.path.exists(qmtest_tmpdir):
1508 os.makedirs(qmtest_tmpdir)
1509 os.chdir(qmtest_tmpdir)
1510 elif "qmtest.tmpdir" in context:
1511 os.chdir(context[
"qmtest.tmpdir"])
1513 if "QMTEST_IGNORE_TIMEOUT" not in os.environ:
1526 if result.GetOutcome()
not in [ result.PASS ]:
1533 """Run the 'program'.
1535 'program' -- The path to the program to run.
1537 'arguments' -- A list of the arguments to the program. This
1538 list must contain a first argument corresponding to 'argv[0]'.
1540 'context' -- A 'Context' giving run-time parameters to the
1543 'result' -- A 'Result' object. The outcome will be
1544 'Result.PASS' when this method is called. The 'result' may be
1545 modified by this method to indicate outcomes other than
1546 'Result.PASS' or to add annotations.
1548 @attention: This method has been copied from command.ExecTestBase
1549 (QMTest 2.3.0) and modified to keep stdout and stderr
1550 for tests that have been terminated by a signal.
1551 (Fundamental for debugging in the Application Area)
1555 environment = self.MakeEnvironment(context)
1557 if "slc6" in environment.get(
'CMTCONFIG',
''):
1558 environment[
'TERM'] =
'dumb'
1571 exit_status = e.Run(arguments, environment, path = program)
1573 if e.stack_trace_file
and os.path.exists(e.stack_trace_file):
1574 stack_trace = open(e.stack_trace_file).read()
1575 os.remove(e.stack_trace_file)
1579 result[
"ExecTest.stack_trace"] = result.Quote(stack_trace)
1582 if (sys.platform ==
"win32" or os.WIFEXITED(exit_status)
1588 if self.exit_code
is None:
1590 elif sys.platform ==
"win32":
1591 exit_code = exit_status
1593 exit_code = os.WEXITSTATUS(exit_status)
1598 result[
"ExecTest.exit_code"] = str(exit_code)
1599 result[
"ExecTest.stdout"] = result.Quote(stdout)
1600 result[
"ExecTest.stderr"] = result.Quote(stderr)
1602 if exit_code != self.exit_code:
1603 causes.append(
"exit_code")
1604 result[
"ExecTest.expected_exit_code"] \
1605 = str(self.exit_code)
1610 result.Fail(
"Unexpected %s." % string.join(causes,
", "))
1611 elif os.WIFSIGNALED(exit_status):
1614 signal_number = str(os.WTERMSIG(exit_status))
1616 result.Fail(
"Program terminated by signal.")
1620 result.Fail(
"Exceeded time limit (%ds), terminated." % timeout)
1621 result[
"ExecTest.signal_number"] = signal_number
1622 result[
"ExecTest.stdout"] = result.Quote(e.stdout)
1623 result[
"ExecTest.stderr"] = result.Quote(e.stderr)
1625 result[
"ExecTest.expected_signal_number"] = str(self.
signal)
1626 elif os.WIFSTOPPED(exit_status):
1629 signal_number = str(os.WSTOPSIG(exit_status))
1631 result.Fail(
"Program stopped by signal.")
1635 result.Fail(
"Exceeded time limit (%ds), stopped." % timeout)
1636 result[
"ExecTest.signal_number"] = signal_number
1637 result[
"ExecTest.stdout"] = result.Quote(e.stdout)
1638 result[
"ExecTest.stderr"] = result.Quote(e.stderr)
1642 result.Fail(
"Program did not terminate normally.")
1648 result[
"ExecTest.stdout"] = result[
"ExecTest.stdout"].replace(esc,repr_esc)
1655 projbasedir = os.path.normpath(destdir)
1656 while not os.path.exists(os.path.join(projbasedir,
".project")):
1657 oldprojdir = projbasedir
1658 projbasedir = os.path.normpath(os.path.join(projbasedir, os.pardir))
1661 if oldprojdir == projbasedir:
1665 if not os.path.exists(destdir):
1666 os.makedirs(destdir)
1668 from xml.etree
import ElementTree
as ET
1669 t = ET.parse(os.path.join(projbasedir,
".project"))
1670 projectName = t.find(
"name").text
1673 destfile =
"%s.launch" % self._Runnable__id
1675 destfile = os.path.join(destdir, destfile)
1677 if self.options.strip():
1681 tempfile = args.pop()
1682 optsfile = destfile + os.path.splitext(tempfile)[1]
1683 shutil.copyfile(tempfile, optsfile)
1684 args.append(optsfile)
1687 from xml.sax.saxutils
import quoteattr
1691 data[
"environment"] =
"\n".join([
'<mapEntry key=%s value=%s/>' % (quoteattr(k), quoteattr(v))
1692 for k, v
in os.environ.iteritems()
1693 if k
not in (
'MAKEOVERRIDES',
'MAKEFLAGS',
'MAKELEVEL')])
1695 data[
"exec"] =
which(prog)
or prog
1696 if os.path.basename(data[
"exec"]).lower().startswith(
"python"):
1697 data[
"stopAtMain"] =
"false"
1699 data[
"stopAtMain"] =
"true"
1701 data[
"args"] =
" ".join(
map(rationalizepath, args))
1703 data[
"args"] =
" ".join([
"/debugexe"] +
map(rationalizepath, [data[
"exec"]] + args))
1704 data[
"exec"] =
which(
"vcexpress.exe")
1707 data[
"workdir"] = os.getcwd()
1711 data[
"workdir"] = destdir
1713 data[
"project"] = projectName.strip()
1716 xml =
"""<?xml version="1.0" encoding="UTF-8" standalone="no"?>
1717 <launchConfiguration type="org.eclipse.cdt.launch.applicationLaunchType">
1718 <booleanAttribute key="org.eclipse.cdt.debug.mi.core.AUTO_SOLIB" value="true"/>
1719 <listAttribute key="org.eclipse.cdt.debug.mi.core.AUTO_SOLIB_LIST"/>
1720 <stringAttribute key="org.eclipse.cdt.debug.mi.core.DEBUG_NAME" value="gdb"/>
1721 <stringAttribute key="org.eclipse.cdt.debug.mi.core.GDB_INIT" value=".gdbinit"/>
1722 <listAttribute key="org.eclipse.cdt.debug.mi.core.SOLIB_PATH"/>
1723 <booleanAttribute key="org.eclipse.cdt.debug.mi.core.STOP_ON_SOLIB_EVENTS" value="false"/>
1724 <booleanAttribute key="org.eclipse.cdt.debug.mi.core.breakpointsFullPath" value="false"/>
1725 <stringAttribute key="org.eclipse.cdt.debug.mi.core.commandFactory" value="org.eclipse.cdt.debug.mi.core.standardCommandFactory"/>
1726 <stringAttribute key="org.eclipse.cdt.debug.mi.core.protocol" value="mi"/>
1727 <booleanAttribute key="org.eclipse.cdt.debug.mi.core.verboseMode" value="false"/>
1728 <intAttribute key="org.eclipse.cdt.launch.ATTR_BUILD_BEFORE_LAUNCH_ATTR" value="0"/>
1729 <stringAttribute key="org.eclipse.cdt.launch.COREFILE_PATH" value=""/>
1730 <stringAttribute key="org.eclipse.cdt.launch.DEBUGGER_ID" value="org.eclipse.cdt.debug.mi.core.CDebuggerNew"/>
1731 <stringAttribute key="org.eclipse.cdt.launch.DEBUGGER_REGISTER_GROUPS" value=""/>
1732 <stringAttribute key="org.eclipse.cdt.launch.DEBUGGER_START_MODE" value="run"/>
1733 <booleanAttribute key="org.eclipse.cdt.launch.DEBUGGER_STOP_AT_MAIN" value="%(stopAtMain)s"/>
1734 <stringAttribute key="org.eclipse.cdt.launch.DEBUGGER_STOP_AT_MAIN_SYMBOL" value="main"/>
1735 <booleanAttribute key="org.eclipse.cdt.launch.ENABLE_REGISTER_BOOKKEEPING" value="false"/>
1736 <booleanAttribute key="org.eclipse.cdt.launch.ENABLE_VARIABLE_BOOKKEEPING" value="false"/>
1737 <stringAttribute key="org.eclipse.cdt.launch.FORMAT" value="<?xml version="1.0" encoding="UTF-8" standalone="no"?><contentList/>"/>
1738 <stringAttribute key="org.eclipse.cdt.launch.GLOBAL_VARIABLES" value="<?xml version="1.0" encoding="UTF-8" standalone="no"?> <globalVariableList/> "/>
1739 <stringAttribute key="org.eclipse.cdt.launch.MEMORY_BLOCKS" value="<?xml version="1.0" encoding="UTF-8" standalone="no"?> <memoryBlockExpressionList/> "/>
1740 <stringAttribute key="org.eclipse.cdt.launch.PROGRAM_ARGUMENTS" value="%(args)s"/>
1741 <stringAttribute key="org.eclipse.cdt.launch.PROGRAM_NAME" value="%(exec)s"/>
1742 <stringAttribute key="org.eclipse.cdt.launch.PROJECT_ATTR" value="%(project)s"/>
1743 <stringAttribute key="org.eclipse.cdt.launch.PROJECT_BUILD_CONFIG_ID_ATTR" value=""/>
1744 <stringAttribute key="org.eclipse.cdt.launch.WORKING_DIRECTORY" value="%(workdir)s"/>
1745 <booleanAttribute key="org.eclipse.cdt.launch.ui.ApplicationCDebuggerTab.DEFAULTS_SET" value="true"/>
1746 <booleanAttribute key="org.eclipse.cdt.launch.use_terminal" value="true"/>
1747 <listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_PATHS">
1748 <listEntry value="/%(project)s"/>
1750 <listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_TYPES">
1751 <listEntry value="4"/>
1753 <booleanAttribute key="org.eclipse.debug.core.appendEnvironmentVariables" value="false"/>
1754 <mapAttribute key="org.eclipse.debug.core.environmentVariables">
1757 <mapAttribute key="org.eclipse.debug.core.preferred_launchers">
1758 <mapEntry key="[debug]" value="org.eclipse.cdt.cdi.launch.localCLaunch"/>
1760 <listAttribute key="org.eclipse.debug.ui.favoriteGroups">
1761 <listEntry value="org.eclipse.debug.ui.launchGroup.debug"/>
1763 </launchConfiguration>
1767 open(destfile,
"w").write(xml)
1775 import simplejson
as json
1778 """An 'HTMLResultStream' writes its output to a set of HTML files.
1780 The argument 'dir' is used to select the destination directory for the HTML
1782 The destination directory may already contain the report from a previous run
1783 (for example of a different package), in which case it will be extended to
1784 include the new data.
1787 qm.fields.TextField(
1789 title =
"Destination Directory",
1790 description =
"""The name of the directory.
1792 All results will be written to the directory indicated.""",
1794 default_value =
""),
1798 """Prepare the destination directory.
1800 Creates the destination directory and store in it some preliminary
1801 annotations and the static files found in the template directory
1804 ResultStream.__init__(self, arguments, **args)
1809 templateDir = os.path.join(os.path.dirname(__file__),
"html_report")
1810 if not os.path.isdir(self.dir):
1811 os.makedirs(self.dir)
1813 for f
in os.listdir(templateDir):
1814 src = os.path.join(templateDir, f)
1815 dst = os.path.join(self.dir, f)
1816 if not os.path.isdir(src)
and not os.path.exists(dst):
1817 shutil.copy(src, dst)
1819 if "CMTCONFIG" in os.environ:
1825 """Helper function to extend the global summary file in the destination
1832 ids = set([ i[
"id"]
for i
in self.
_summary ])
1833 newSummary = [ i
for i
in oldSummary
if i[
"id"]
not in ids ]
1839 """Writes the annotation to the annotation file.
1840 If the key is already present with a different value, the value becomes
1841 a list and the new value is appended to it, except for start_time and
1850 key, value =
map(str, [key, value])
1851 if key ==
"qmtest.run.start_time":
1856 if key
not in annotations:
1857 annotations[key] = value
1858 if "qmtest.run.end_time" in annotations:
1859 del annotations[
"qmtest.run.end_time"]
1862 if key
in annotations:
1863 old = annotations[key]
1864 if type(old)
is list:
1865 if value
not in old:
1866 annotations[key].append(value)
1868 annotations[key] = [old, value]
1870 annotations[key] = value
1876 """Prepare the test result directory in the destination directory storing
1877 into it the result fields.
1878 A summary of the test result is stored both in a file in the test directory
1879 and in the global summary file.
1882 summary[
"id"] = result.GetId()
1883 summary[
"outcome"] = result.GetOutcome()
1884 summary[
"cause"] = result.GetCause()
1885 summary[
"fields"] = result.keys()
1886 summary[
"fields"].sort()
1889 for f
in [
"id",
"outcome",
"cause"]:
1890 summary[f] = str(summary[f])
1891 summary[
"fields"] =
map(str, summary[
"fields"])
1893 self._summary.append(summary)
1899 testOutDir = os.path.join(self.dir, summary[
"id"])
1900 if not os.path.isdir(testOutDir):
1901 os.makedirs(testOutDir)
1902 json.dump(summary, open(os.path.join(testOutDir,
"summary.json"),
"w"),
1904 for f
in summary[
"fields"]:
1905 open(os.path.join(testOutDir, f),
"w").write(result[f])
1916 class XMLResultStream(ResultStream):
1917 """An 'XMLResultStream' writes its output to a Ctest XML file.
1919 The argument 'dir' is used to select the destination file for the XML
1921 The destination directory may already contain the report from a previous run
1922 (for example of a different package), in which case it will be overrided to
1926 qm.fields.TextField(
1928 title =
"Destination Directory",
1929 description =
"""The name of the directory.
1931 All results will be written to the directory indicated.""",
1933 default_value =
""),
1934 qm.fields.TextField(
1936 title =
"Output File Prefix",
1937 description =
"""The output file name will be the specified prefix
1938 followed by 'Test.xml' (CTest convention).""",
1940 default_value =
""),
1944 """Prepare the destination directory.
1946 Creates the destination directory and store in it some preliminary
1949 ResultStream.__init__(self, arguments, **args)
1951 self.
_xmlFile = os.path.join(self.dir, self.prefix +
'Test.xml')
1957 if not os.path.isfile(self.
_xmlFile):
1959 if not os.path.exists(os.path.dirname(self.
_xmlFile)):
1960 os.makedirs(os.path.dirname(self.
_xmlFile))
1962 newdataset = ET.Element(
"newdataset")
1968 newdataset = self._tree.getroot()
1975 for site
in newdataset.getiterator() :
1976 if site.get(
"OSPlatform") == os.uname()[4]:
1986 import multiprocessing
1988 "BuildName" : os.getenv(
"CMTCONFIG"),
1989 "Name" : os.uname()[1] ,
1990 "Generator" :
"QMTest "+qm.version ,
1991 "OSName" : os.uname()[0] ,
1992 "Hostname" : socket.gethostname() ,
1993 "OSRelease" : os.uname()[2] ,
1994 "OSVersion" :os.uname()[3] ,
1995 "OSPlatform" :os.uname()[4] ,
1996 "Is64Bits" :
"unknown" ,
1997 "VendorString" :
"unknown" ,
1998 "VendorID" :
"unknown" ,
1999 "FamilyID" :
"unknown" ,
2000 "ModelID" :
"unknown" ,
2001 "ProcessorCacheSize" :
"unknown" ,
2002 "NumberOfLogicalCPU" : str(multiprocessing.cpu_count()) ,
2003 "NumberOfPhysicalCPU" :
"0" ,
2004 "TotalVirtualMemory" :
"0" ,
2005 "TotalPhysicalMemory" :
"0" ,
2006 "LogicalProcessorsPerPhysical" :
"0" ,
2007 "ProcessorClockFrequency" :
"0" ,
2009 self.
_site = ET.SubElement(newdataset,
"site", attrib)
2032 self.
_Testing = self._site.find(
"Testing")
2035 self.
_TestList = self._Testing.find(
"TestList")
2041 # Add some non-QMTest attributes
2042 if "CMTCONFIG" in os.environ:
2043 self.WriteAnnotation("cmt.cmtconfig", os.environ["CMTCONFIG"])
2045 self.WriteAnnotation("hostname", socket.gethostname())
2050 if key ==
"qmtest.run.start_time":
2051 if self._site.get(
"qmtest.run.start_time")
is not None :
2053 self._site.set(str(key),str(value))
2055 """Prepare the test result directory in the destination directory storing
2056 into it the result fields.
2057 A summary of the test result is stored both in a file in the test directory
2058 and in the global summary file.
2061 summary[
"id"] = result.GetId()
2062 summary[
"outcome"] = result.GetOutcome()
2063 summary[
"cause"] = result.GetCause()
2064 summary[
"fields"] = result.keys()
2065 summary[
"fields"].sort()
2069 for f
in [
"id",
"outcome",
"cause"]:
2070 summary[f] = str(summary[f])
2071 summary[
"fields"] =
map(str, summary[
"fields"])
2077 if "qmtest.start_time" in summary[
"fields"]:
2078 haveStartDate =
True
2080 haveStartDate =
False
2081 if "qmtest.end_time" in summary[
"fields"]:
2088 self.
_startTime = calendar.timegm(time.strptime(result[
"qmtest.start_time"],
"%Y-%m-%dT%H:%M:%SZ"))
2089 if self._StartTestTime.text
is None:
2090 self._StartDateTime.text = time.strftime(
"%b %d %H:%M %Z", time.localtime(self.
_startTime))
2091 self._StartTestTime.text = str(self.
_startTime)
2092 self._site.set(
"BuildStamp" , result[
"qmtest.start_time"] )
2096 self.
_endTime = calendar.timegm(time.strptime(result[
"qmtest.end_time"],
"%Y-%m-%dT%H:%M:%SZ"))
2100 tl = ET.Element(
"Test")
2101 tl.text = summary[
"id"]
2102 self._TestList.insert(0,tl)
2105 Test = ET.Element(
"Test")
2106 if summary[
"outcome"] ==
"PASS":
2107 Test.set(
"Status",
"passed")
2108 elif summary[
"outcome"] ==
"FAIL":
2109 Test.set(
"Status",
"failed")
2110 elif summary[
"outcome"] ==
"SKIPPED" or summary[
"outcome"] ==
"UNTESTED":
2111 Test.set(
"Status",
"skipped")
2112 elif summary[
"outcome"] ==
"ERROR":
2113 Test.set(
"Status",
"failed")
2114 Name = ET.SubElement(Test,
"Name",)
2115 Name.text = summary[
"id"]
2116 Results = ET.SubElement(Test,
"Results")
2119 self._Testing.insert(3,Test)
2121 if haveStartDate
and haveEndDate:
2124 testduration = str(delta)
2125 Testduration= ET.SubElement(Results,
"NamedMeasurement")
2126 Testduration.set(
"name",
"Execution Time")
2127 Testduration.set(
"type",
"numeric/float" )
2128 value = ET.SubElement(Testduration,
"Value")
2129 value.text = testduration
2132 for n
in (
"qmtest.end_time",
"qmtest.start_time",
"qmtest.cause",
"ExecTest.stdout"):
2133 if n
in summary[
"fields"]:
2134 summary[
"fields"].
remove(n)
2138 if "ExecTest.exit_code" in summary[
"fields"] :
2139 summary[
"fields"].
remove(
"ExecTest.exit_code")
2140 ExitCode= ET.SubElement(Results,
"NamedMeasurement")
2141 ExitCode.set(
"name",
"exit_code")
2142 ExitCode.set(
"type",
"numeric/integer" )
2143 value = ET.SubElement(ExitCode,
"Value")
2146 TestStartTime= ET.SubElement(Results,
"NamedMeasurement")
2147 TestStartTime.set(
"name",
"Start_Time")
2148 TestStartTime.set(
"type",
"String" )
2149 value = ET.SubElement(TestStartTime,
"Value")
2155 TestEndTime= ET.SubElement(Results,
"NamedMeasurement")
2156 TestEndTime.set(
"name",
"End_Time")
2157 TestEndTime.set(
"type",
"String" )
2158 value = ET.SubElement(TestEndTime,
"Value")
2164 if summary[
"cause"]:
2165 FailureCause= ET.SubElement(Results,
"NamedMeasurement")
2166 FailureCause.set(
"name",
"Cause")
2167 FailureCause.set(
"type",
"String" )
2168 value = ET.SubElement(FailureCause,
"Value")
2173 for field
in summary[
"fields"] :
2174 fields[field] = ET.SubElement(Results,
"NamedMeasurement")
2175 fields[field].set(
"type",
"String")
2176 fields[field].set(
"name",field)
2177 value = ET.SubElement(fields[field],
"Value")
2179 if "<pre>" in result[field][0:6] :
2185 if result.has_key(
"ExecTest.stdout" ) :
2186 Measurement = ET.SubElement(Results,
"Measurement")
2187 value = ET.SubElement(Measurement,
"Value")
2188 if "<pre>" in result[
"ExecTest.stdout"][0:6] :
2195 self._tree.write(self.
_xmlFile,
"utf-8")
2201 self._EndTestTime.text = str(self.
_endTime)
2202 self._EndDateTime.text = time.strftime(
"%b %d %H:%M %Z", time.localtime(self.
_endTime))
2209 self._ElapsedMinutes.text = str(delta/60)
2212 self._tree.write(self.
_xmlFile,
"utf-8")