24 from subprocess
import Popen, PIPE, STDOUT
27 from html
import escape
as escape_for_html
29 from cgi
import escape
as escape_for_html
33 if sys.version_info < (3, 5):
36 from codecs
import register_error, backslashreplace_errors
39 if isinstance(exc, UnicodeDecodeError):
40 code =
hex(ord(exc.object[exc.start]))
41 return (
u'\\' + code[1:], exc.start + 1)
43 return backslashreplace_errors(exc)
45 register_error(
'backslashreplace', _new_backslashreplace_errors)
47 del backslashreplace_errors
48 del _new_backslashreplace_errors
53 Take a string with invalid ASCII/UTF characters and quote them so that the
54 string can be used in an XML text.
56 >>> sanitize_for_xml('this is \x1b')
57 'this is [NON-XML-CHAR-0x1B]'
59 bad_chars = re.compile(
60 u'[\x00-\x08\x0b\x0c\x0e-\x1F\uD800-\uDFFF\uFFFE\uFFFF]')
64 return ''.join(
'[NON-XML-CHAR-0x%2X]' % ord(c)
for c
in match.group())
66 return bad_chars.sub(quote, data)
70 '''helper to debug GAUDI-1084, dump the list of processes'''
71 from getpass
import getuser
72 if 'WORKSPACE' in os.environ:
73 p = Popen([
'ps',
'-fH',
'-U', getuser()], stdout=PIPE)
74 with open(os.path.join(os.environ[
'WORKSPACE'], name),
'wb')
as f:
75 f.write(p.communicate()[0])
80 Send a signal to a process and all its child processes (starting from the
83 log = logging.getLogger(
'kill_tree')
84 ps_cmd = [
'ps',
'--no-headers',
'-o',
'pid',
'--ppid', str(ppid)]
85 get_children = Popen(ps_cmd, stdout=PIPE, stderr=PIPE)
86 children =
map(int, get_children.communicate()[0].split())
87 for child
in children:
90 log.debug(
'killing process %d', ppid)
92 except OSError
as err:
95 log.debug(
'no such process %d', ppid)
103 _common_tmpdir =
None
132 logging.debug(
'running test %s', self.
name)
142 'TIMEOUT_DETAIL':
None
147 r'from\s+Gaudi.Configuration\s+import\s+\*|'
148 'from\s+Configurables\s+import', self.
options):
149 suffix, lang =
'.py',
'python'
151 suffix, lang =
'.opts',
'c++'
153 "Options"] =
'<code lang="{}"><pre>{}</pre></code>'.
format(
154 lang, escape_for_html(self.
options))
155 optionFile = tempfile.NamedTemporaryFile(suffix=suffix)
156 optionFile.file.write(self.
options.encode(
'utf-8'))
162 or platform.platform())
166 if re.search(prex, platform_id)
176 workdir = tempfile.mkdtemp()
187 prog_ext = os.path.splitext(prog)[1]
188 if prog_ext
not in [
".exe",
".py",
".bat"]:
192 prog =
which(prog)
or prog
194 args = list(
map(RationalizePath, self.
args))
196 if prog_ext ==
".py":
207 logging.debug(
'executing %r in %s', params, workdir)
209 params, stdout=PIPE, stderr=PIPE, env=self.
environment)
210 logging.debug(
'(pid: %d)', self.
proc.pid)
211 out, err = self.
proc.communicate()
212 self.
out = out.decode(
'utf-8', errors=
'backslashreplace')
213 self.
err = err.decode(
'utf-8', errors=
'backslashreplace')
215 thread = threading.Thread(target=target)
220 if thread.is_alive():
221 logging.debug(
'time out in test %s (pid %d)', self.
name,
226 str(self.
proc.pid),
'--batch',
227 '--eval-command=thread apply all backtrace'
229 gdb = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=STDOUT)
231 'utf-8', errors=
'backslashreplace')
235 if thread.is_alive():
237 self.
causes.append(
'timeout')
239 logging.debug(
'completed test %s', self.
name)
242 logging.debug(
'returnedCode = %s', self.
proc.returncode)
245 logging.debug(
'validating test...')
251 shutil.rmtree(workdir,
True)
256 if self.
signal is not None:
258 self.
causes.append(
'exit code')
262 self.
causes.append(
'exit code')
265 self.
causes.append(
"exit code")
275 logging.debug(
'%s: %s', self.
name, self.
status)
277 'Exit Code':
'returnedCode',
280 'Runtime Environment':
'environment',
283 'Program Name':
'program',
285 'Validator':
'validator',
286 'Output Reference File':
'reference',
287 'Error Reference File':
'error_reference',
290 'Unsupported Platforms':
'unsupported_platforms',
291 'Stack Trace':
'stack_trace'
293 resultDict = [(key, getattr(self, attr))
294 for key, attr
in field_mapping.items()
295 if getattr(self, attr)]
296 resultDict.append((
'Working Directory',
298 os.path.join(os.getcwd(), self.
workdir))))
300 resultDict.extend(self.
result.annotations.items())
302 resultDict = dict(resultDict)
305 if "Validator" in resultDict:
307 "Validator"] =
'<code lang="{}"><pre>{}</pre></code>'.
format(
308 "python", escape_for_html(resultDict[
"Validator"]))
318 elif stderr.strip() != self.
stderr.strip():
319 self.
causes.append(
'standard error')
320 return result, self.
causes
331 Given a block of text, tries to find it in the output. The block had to be identified by a signature line. By default, the first line is used as signature, or the line pointed to by signature_offset. If signature_offset points outside the block, a signature line can be passed as signature argument. Note: if 'signature' is None (the default), a negative signature_offset is interpreted as index in a list (e.g. -1 means the last line), otherwise the it is interpreted as the number of lines before the first one of the block the signature must appear. The parameter 'id' allow to distinguish between different calls to this function in the same validation code.
334 if reference
is None:
344 filter(
None,
map(
lambda s: s.rstrip(), reference.splitlines())))
346 raise RuntimeError(
"Empty (or null) reference")
349 filter(
None,
map(
lambda s: s.rstrip(), stdout.splitlines())))
351 res_field =
"GaudiTest.RefBlock"
353 res_field +=
"_%s" % id
355 if signature
is None:
356 if signature_offset < 0:
357 signature_offset = len(reference) + signature_offset
358 signature = reflines[signature_offset]
361 pos = outlines.index(signature)
362 outlines = outlines[pos - signature_offset:pos + len(reflines) -
364 if reflines != outlines:
365 msg =
"standard output"
368 if not msg
in causes:
370 result[res_field +
".observed"] = result.Quote(
373 causes.append(
"missing signature")
374 result[res_field +
".signature"] = result.Quote(signature)
375 if len(reflines) > 1
or signature != reflines[0]:
376 result[res_field +
".expected"] = result.Quote(
"\n".join(reflines))
388 Count the number of messages with required severity (by default ERROR and FATAL)
389 and check if their numbers match the expected ones (0 by default).
390 The dictionary "expected" can be used to tune the number of errors and fatals
391 allowed, or to limit the number of expected warnings etc.
406 outlines = stdout.splitlines()
407 from math
import log10
408 fmt =
"%%%dd - %%s" % (int(log10(len(outlines) + 1)))
414 if len(words) >= 2
and words[1]
in errors:
415 errors[words[1]].append(fmt % (linecount, l.rstrip()))
418 if len(errors[e]) != expected[e]:
419 causes.append(
'%s(%d)' % (e, len(errors[e])))
420 result[
"GaudiTest.lines.%s" % e] = result.Quote(
'\n'.join(
422 result[
"GaudiTest.lines.%s.expected#" % e] = result.Quote(
432 ignore=r"Basket|.*size|Compression"):
434 Compare the TTree summaries in stdout with the ones in trees_dict or in
435 the reference file. By default ignore the size, compression and basket
437 The presence of TTree summaries when none is expected is not a failure.
445 if trees_dict
is None:
448 if lreference
and os.path.isfile(lreference):
453 from pprint
import PrettyPrinter
456 result[
"GaudiTest.TTrees.expected"] = result.Quote(
457 pp.pformat(trees_dict))
459 result[
"GaudiTest.TTrees.ignore"] = result.Quote(ignore)
464 causes.append(
"trees summaries")
467 result[
"GaudiTest.TTrees.failure_on"] = result.Quote(msg)
468 result[
"GaudiTest.TTrees.found"] = result.Quote(pp.pformat(trees))
479 Compare the TTree summaries in stdout with the ones in trees_dict or in
480 the reference file. By default ignore the size, compression and basket
482 The presence of TTree summaries when none is expected is not a failure.
494 if lreference
and os.path.isfile(lreference):
499 from pprint
import PrettyPrinter
502 result[
"GaudiTest.Histos.expected"] = result.Quote(
505 result[
"GaudiTest.Histos.ignore"] = result.Quote(ignore)
510 causes.append(
"histos summaries")
512 result[
"GaudiTest.Histos.failure_on"] = result.Quote(msg)
513 result[
"GaudiTest.Histos.found"] = result.Quote(pp.pformat(histos))
524 Default validation acti*on: compare standard output and error to the
539 preproc = normalizeExamples
543 if lreference
and os.path.isfile(lreference):
545 lreference,
"standard output",
"Output Diff",
546 preproc=preproc)(stdout, result)
548 causes += [
"missing reference file"]
552 if causes
and lreference:
555 newrefname =
'.'.join([lreference,
'new'])
556 while os.path.exists(newrefname):
558 newrefname =
'.'.join([lreference,
'~%d~' % cnt,
'new'])
559 newref = open(newrefname,
"w")
561 for l
in stdout.splitlines():
562 newref.write(l.rstrip() +
'\n')
564 result[
'New Output Reference File'] = os.path.relpath(
575 if os.path.isfile(lreference):
580 preproc=preproc)(stderr, result)
582 newcauses = [
"missing error reference file"]
584 if newcauses
and lreference:
586 newrefname =
'.'.join([lreference,
'new'])
587 while os.path.exists(newrefname):
589 newrefname =
'.'.join([lreference,
'~%d~' % cnt,
'new'])
590 newref = open(newrefname,
"w")
592 for l
in stderr.splitlines():
593 newref.write(l.rstrip() +
'\n')
595 result[
'New Error Reference File'] = os.path.relpath(
599 "ExecTest.expected_stderr")(stderr,
609 def platformSplit(p):
611 delim = re.compile(
'-' in p
and r"[-+]" or r"_")
612 return set(delim.split(p))
614 reference = os.path.normpath(
615 os.path.join(self.
basedir, os.path.expandvars(reffile)))
618 spec_ref = reference[:-3] +
GetPlatform(self)[0:3] + reference[-3:]
619 if os.path.isfile(spec_ref):
623 dirname, basename = os.path.split(reference)
626 head = basename +
"."
629 if 'do0' in platform:
632 for f
in os.listdir(dirname):
633 if f.startswith(head):
634 req_plat = platformSplit(f[head_len:])
635 if platform.issuperset(req_plat):
636 candidates.append((len(req_plat), f))
641 reference = os.path.join(dirname, candidates[-1][1])
653 from GaudiKernel
import ROOT6WorkAroundEnabled
666 Function used to normalize the used path
668 newPath = os.path.normpath(os.path.expandvars(p))
669 if os.path.exists(newPath):
670 p = os.path.realpath(newPath)
676 Locates an executable in the executables path ($PATH) and returns the full
677 path to it. An application is looked for with or without the '.exe' suffix.
678 If the executable cannot be found, None is returned
680 if os.path.isabs(executable):
681 if not os.path.isfile(executable):
682 if executable.endswith(
'.exe'):
683 if os.path.isfile(executable[:-4]):
684 return executable[:-4]
686 executable = os.path.split(executable)[1]
689 for d
in os.environ.get(
"PATH").split(os.pathsep):
690 fullpath = os.path.join(d, executable)
691 if os.path.isfile(fullpath):
693 elif executable.endswith(
'.exe')
and os.path.isfile(fullpath[:-4]):
709 UNTESTED =
'UNTESTED'
719 def __init__(self, kind=None, id=None, outcome=PASS, annotations={}):
723 assert isinstance(key, six.string_types)
727 assert isinstance(key, six.string_types)
729 value, six.string_types),
'{!r} is not a string'.
format(value)
734 Convert text to html by escaping special chars and adding <pre> tags.
736 return "<pre>{}</pre>".
format(escape_for_html(text))
755 """Validate the output of the program.
756 'stdout' -- A string containing the data written to the standard output
758 'stderr' -- A string containing the data written to the standard error
760 'result' -- A 'Result' object. It may be used to annotate
761 the outcome according to the content of stderr.
762 returns -- A list of strings giving causes of failure."""
767 causes.append(self.
cause)
773 """Compare 's1' and 's2', ignoring line endings.
776 returns -- True if 's1' and 's2' are the same, ignoring
777 differences in line endings."""
781 to_ignore = re.compile(
782 r'Warning in <TInterpreter::ReadRootmapFile>: .* is already in .*'
786 return not to_ignore.match(l)
788 return list(filter(keep_line, s1.splitlines())) == list(
789 filter(keep_line, s2.splitlines()))
791 return s1.splitlines() == s2.splitlines()
796 """ Base class for a callable that takes a file and returns a modified
811 if not isinstance(input, six.string_types):
815 lines = input.splitlines()
819 output =
'\n'.join(output)
848 if line.find(s) >= 0:
863 if self.
start in line:
866 elif self.
end in line:
876 when = re.compile(when)
880 if isinstance(rhs, RegexpReplacer):
882 res._operations = self.
_operations + rhs._operations
884 res = FilePreprocessor.__add__(self, rhs)
889 if w
is None or w.search(line):
890 line = o.sub(r, line)
897 "[0-2]?[0-9]:[0-5][0-9]:[0-5][0-9] [0-9]{4}[-/][01][0-9][-/][0-3][0-9][ A-Z]*",
898 "00:00:00 1970-01-01")
900 normalizeEOL.__processLine__ =
lambda line: str(line).rstrip() +
'\n'
904 skipEmptyLines.__processLine__ =
lambda line: (line.strip()
and line)
or None
918 line = line[:(pos + self.
siglen)]
919 lst = line[(pos + self.
siglen):].split()
921 line +=
" ".join(lst)
927 Sort group of lines matching a regular expression
931 self.
exp = exp
if hasattr(exp,
'match')
else re.compile(exp)
934 match = self.
exp.match
950 normalizeExamples = maskPointers + normalizeDate
953 (
"TIMER.TIMER",
r"\s+[+-]?[0-9]+[0-9.]*",
" 0"),
954 (
"release all pending",
r"^.*/([^/]*:.*)",
r"\1"),
955 (
"^#.*file",
r"file '.*[/\\]([^/\\]*)$",
r"file '\1"),
956 (
"^JobOptionsSvc.*options successfully read in from",
957 r"read in from .*[/\\]([^/\\]*)$",
961 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}",
962 "00000000-0000-0000-0000-000000000000"),
964 (
"ServiceLocatorHelper::",
"ServiceLocatorHelper::(create|locate)Service",
965 "ServiceLocatorHelper::service"),
967 (
None,
r"e([-+])0([0-9][0-9])",
r"e\1\2"),
969 (
None,
r'Service reference count check:',
970 r'Looping over all active services...'),
973 r"^(.*(DEBUG|SUCCESS) List of ALL properties of .*#properties = )\d+",
975 (
'ApplicationMgr',
r'(declareMultiSvcType|addMultiSvc): ',
''),
976 (
r"Property 'Name': Value
",
r"( = '[^']+':)'(.*)'",
r'\1\2'),
977 (
'TimelineSvc',
"to file 'TimelineFile':",
"to file "),
978 (
'DataObjectHandleBase',
r'DataObjectHandleBase\("([^"]*)"\)',
r"'\1'"),
985 "JobOptionsSvc INFO # ",
986 "JobOptionsSvc WARNING # ",
989 "This machine has a speed",
992 "ToolSvc.Sequenc... INFO",
993 "DataListenerSvc INFO XML written to file:",
996 "DEBUG No writable file catalog found which contains FID:",
997 "DEBUG Service base class initialized successfully",
999 "DEBUG Incident timing:",
1003 "INFO 'CnvServices':[",
1005 "DEBUG 'CnvServices':[",
1010 'ServiceLocatorHelper::service: found service JobOptionsSvc',
1012 'mismatching case for property name:',
1014 'Histograms saving not required.',
1016 'Properties are dumped into',
1019 r"^JobOptionsSvc INFO *$",
1022 r"(Always|SUCCESS)\s*(Root f|[^ ]* F)ile version:",
1023 r"File '.*.xml' does not exist",
1024 r"INFO Refer to dataset .* by its file ID:",
1025 r"INFO Referring to dataset .* by its file ID:",
1026 r"INFO Disconnect from dataset",
1027 r"INFO Disconnected from dataset",
1028 r"INFO Disconnected data IO:",
1029 r"IncidentSvc\s*(DEBUG (Adding|Removing)|VERBOSE Calling)",
1031 r".*StatusCodeSvc.*",
1032 r"Num\s*\|\s*Function\s*\|\s*Source Library",
1035 r"ERROR Failed to modify file: .* Errno=2 No such file or directory",
1037 r"^ +[0-9]+ \|.*ROOT",
1038 r"^ +[0-9]+ \|.*\|.*Dict",
1040 r"EventLoopMgr.*---> Loop Finished",
1041 r"HiveSlimEventLo.*---> Loop Finished",
1046 r"SUCCESS\s*Booked \d+ Histogram\(s\)",
1050 r"Property(.*)'Audit(Algorithm|Tool|Service)s':",
1051 r"Property(.*)'Audit(Begin|End)Run':",
1053 r"Property(.*)'AuditRe(start|initialize)':",
1054 r"Property(.*)'Blocking':",
1056 r"Property(.*)'ErrorCount(er)?':",
1058 r"Property(.*)'Sequential':",
1060 r"Property(.*)'FilterCircularDependencies':",
1062 r"Property(.*)'IsClonable':",
1064 r"Property update for OutputLevel : new value =",
1065 r"EventLoopMgr\s*DEBUG Creating OutputStream",
1072 r'Warning in <TInterpreter::ReadRootmapFile>: .* is already in .*',
1075 normalizeExamples = (
1076 lineSkipper + normalizeExamples + skipEmptyLines + normalizeEOL +
1084 def __init__(self, reffile, cause, result_key, preproc=normalizeExamples):
1092 if os.path.isfile(self.
reffile):
1093 orig = open(self.
reffile).readlines()
1096 result[self.
result_key +
'.preproc.orig'] = \
1097 result.Quote(
'\n'.join(
map(str.strip, orig)))
1100 new = stdout.splitlines()
1104 diffs = difflib.ndiff(orig, new, charjunk=difflib.IS_CHARACTER_JUNK)
1106 map(
lambda x: x.strip(), filter(
lambda x: x[0] !=
" ", diffs)))
1108 result[self.
result_key] = result.Quote(
"\n".join(filterdiffs))
1112 +) standard output of the test""")
1114 result.Quote(
'\n'.join(
map(str.strip, new)))
1115 causes.append(self.
cause)
1121 Scan stdout to find ROOT TTree summaries and digest them.
1123 stars = re.compile(
r"^\*+$")
1124 outlines = stdout.splitlines()
1125 nlines = len(outlines)
1131 while i < nlines
and not stars.match(outlines[i]):
1136 trees[tree[
"Name"]] = tree
1143 Check that all the keys in reference are in to_check too, with the same value.
1144 If the value is a dict, the function is called recursively. to_check can
1145 contain more keys than reference, that will not be tested.
1146 The function returns at the first difference found.
1151 ignore_re = re.compile(ignore)
1152 keys = [key
for key
in reference
if not ignore_re.match(key)]
1154 keys = reference.keys()
1158 if (
type(reference[k])
is dict)
and (
type(to_check[k])
is dict):
1161 failed = fail_keys =
cmpTreesDicts(reference[k], to_check[k],
1165 failed = to_check[k] != reference[k]
1170 fail_keys.insert(0, k)
1181 if c
is None or r
is None:
1183 return (fail_path, r, c)
1187 h_count_re = re.compile(
1188 r"^(.*)SUCCESS\s+Booked (\d+) Histogram\(s\) :\s+([\s\w=-]*)")
1193 Parse the TTree summary table in lines, starting from pos.
1194 Returns a tuple with the dictionary with the digested informations and the
1195 position of the first line after the summary.
1202 return [f.strip()
for f
in l.strip(
"*\n").split(
':', 2)]
1206 cols = splitcols(ll[0])
1207 r[
"Name"], r[
"Title"] = cols[1:]
1209 cols = splitcols(ll[1])
1210 r[
"Entries"] = int(cols[1])
1212 sizes = cols[2].split()
1213 r[
"Total size"] = int(sizes[2])
1214 if sizes[-1] ==
"memory":
1217 r[
"File size"] = int(sizes[-1])
1219 cols = splitcols(ll[2])
1220 sizes = cols[2].split()
1221 if cols[0] ==
"Baskets":
1222 r[
"Baskets"] = int(cols[1])
1223 r[
"Basket size"] = int(sizes[2])
1224 r[
"Compression"] = float(sizes[-1])
1227 if i < (count - 3)
and lines[i].startswith(
"*Tree"):
1228 result = parseblock(lines[i:i + 3])
1229 result[
"Branches"] = {}
1231 while i < (count - 3)
and lines[i].startswith(
"*Br"):
1232 if i < (count - 2)
and lines[i].startswith(
"*Branch "):
1236 branch = parseblock(lines[i:i + 3])
1237 result[
"Branches"][branch[
"Name"]] = branch
1245 Extract the histograms infos from the lines starting at pos.
1246 Returns the position of the first line after the summary block.
1249 h_table_head = re.compile(
1250 r'SUCCESS\s+(1D|2D|3D|1D profile|2D profile) histograms in directory\s+"(\w*)"'
1252 h_short_summ = re.compile(
r"ID=([^\"]+)\s+\"([^\"]+)\"\s+(.*)")
1257 m = h_count_re.search(lines[pos])
1258 name = m.group(1).strip()
1259 total = int(m.group(2))
1261 for k, v
in [x.split(
"=")
for x
in m.group(3).split()]:
1264 header[
"Total"] = total
1268 m = h_table_head.search(lines[pos])
1271 t = t.replace(
" profile",
"Prof")
1278 if l.startswith(
" | ID"):
1280 titles = [x.strip()
for x
in l.split(
"|")][1:]
1282 while pos < nlines
and lines[pos].startswith(
" |"):
1284 values = [x.strip()
for x
in l.split(
"|")][1:]
1286 for i
in range(len(titles)):
1287 hcont[titles[i]] = values[i]
1288 cont[hcont[
"ID"]] = hcont
1290 elif l.startswith(
" ID="):
1291 while pos < nlines
and lines[pos].startswith(
" ID="):
1294 for x
in h_short_summ.search(lines[pos]).groups()
1296 cont[values[0]] = values
1300 "Cannot understand line %d: '%s'" % (pos, l))
1304 summ[d][
"header"] = header
1309 summ[name] = {
"header": header}
1315 Scan stdout to find ROOT TTree summaries and digest them.
1317 outlines = stdout.splitlines()
1318 nlines = len(outlines) - 1
1326 match = h_count_re.search(outlines[pos])
1327 while pos < nlines
and not match:
1329 match = h_count_re.search(outlines[pos])
1332 summaries.update(summ)
1338 Return the platform Id defined in CMTCONFIG or SCRAM_ARCH.
1342 if "BINARY_TAG" in os.environ:
1343 arch = os.environ[
"BINARY_TAG"]
1344 elif "CMTCONFIG" in os.environ:
1345 arch = os.environ[
"CMTCONFIG"]
1346 elif "SCRAM_ARCH" in os.environ:
1347 arch = os.environ[
"SCRAM_ARCH"]
1348 elif os.environ.get(
"ENV_CMAKE_BUILD_TYPE",
"")
in (
"Debug",
"FastDebug",
1351 elif os.environ.get(
"ENV_CMAKE_BUILD_TYPE",
1352 "")
in (
"Release",
"MinSizeRel",
"RelWithDebInfo",
1360 Return True if the current platform is Windows.
1362 This function was needed because of the change in the CMTCONFIG format,
1363 from win32_vc71_dbg to i686-winxp-vc9-dbg.
1366 return "winxp" in platform
or platform.startswith(
"win")