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
55 Take a string with invalid ASCII/UTF characters and quote them so that the
56 string can be used in an XML text.
58 >>> sanitize_for_xml('this is \x1b')
59 'this is [NON-XML-CHAR-0x1B]'
61 bad_chars = re.compile(
62 u'[\x00-\x08\x0b\x0c\x0e-\x1F\uD800-\uDFFF\uFFFE\uFFFF]')
66 return ''.join(
'[NON-XML-CHAR-0x%2X]' % ord(c)
for c
in match.group())
68 return bad_chars.sub(quote, data)
72 '''helper to debug GAUDI-1084, dump the list of processes'''
73 from getpass
import getuser
74 if 'WORKSPACE' in os.environ:
75 p = Popen([
'ps',
'-fH',
'-U', getuser()], stdout=PIPE)
76 with open(os.path.join(os.environ[
'WORKSPACE'], name),
'wb')
as f:
77 f.write(p.communicate()[0])
82 Send a signal to a process and all its child processes (starting from the
85 log = logging.getLogger(
'kill_tree')
86 ps_cmd = [
'ps',
'--no-headers',
'-o',
'pid',
'--ppid', str(ppid)]
87 get_children = Popen(ps_cmd, stdout=PIPE, stderr=PIPE)
88 children =
map(int, get_children.communicate()[0].split())
89 for child
in children:
92 log.debug(
'killing process %d', ppid)
94 except OSError
as err:
97 log.debug(
'no such process %d', ppid)
105 _common_tmpdir =
None
134 logging.debug(
'running test %s', self.
name)
144 'TIMEOUT_DETAIL':
None
149 r'from\s+Gaudi.Configuration\s+import\s+\*|'
150 'from\s+Configurables\s+import', self.
options):
151 suffix, lang =
'.py',
'python'
153 suffix, lang =
'.opts',
'c++'
155 "Options"] =
'<code lang="{}"><pre>{}</pre></code>'.
format(
156 lang, escape_for_html(self.
options))
157 optionFile = tempfile.NamedTemporaryFile(suffix=suffix)
158 optionFile.file.write(self.
options.encode(
'utf-8'))
164 or platform.platform())
168 if re.search(prex, platform_id)
178 workdir = tempfile.mkdtemp()
189 prog_ext = os.path.splitext(prog)[1]
190 if prog_ext
not in [
".exe",
".py",
".bat"]:
194 prog =
which(prog)
or prog
196 args = list(
map(RationalizePath, self.
args))
198 if prog_ext ==
".py":
209 logging.debug(
'executing %r in %s', params, workdir)
211 params, stdout=PIPE, stderr=PIPE, env=self.
environment)
212 logging.debug(
'(pid: %d)', self.
proc.pid)
213 out, err = self.
proc.communicate()
214 self.
out = out.decode(
'utf-8', errors=
'backslashreplace')
215 self.
err = err.decode(
'utf-8', errors=
'backslashreplace')
217 thread = threading.Thread(target=target)
222 if thread.is_alive():
223 logging.debug(
'time out in test %s (pid %d)', self.
name,
228 str(self.
proc.pid),
'--batch',
229 '--eval-command=thread apply all backtrace'
231 gdb = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=STDOUT)
233 'utf-8', errors=
'backslashreplace')
237 if thread.is_alive():
239 self.
causes.append(
'timeout')
244 f
'completed test {self.name} with returncode = {self.returnedCode}'
246 logging.debug(
'validating test...')
250 logging.debug(f
'skipped test {self.name}')
255 shutil.rmtree(workdir,
True)
259 if self.
status !=
"skipped":
261 if self.
signal is not None:
263 self.
causes.append(
'exit code')
267 self.
causes.append(
'exit code')
270 self.
causes.append(
"exit code")
280 logging.debug(
'%s: %s', self.
name, self.
status)
282 'Exit Code':
'returnedCode',
285 'Runtime Environment':
'environment',
288 'Program Name':
'program',
290 'Validator':
'validator',
291 'Output Reference File':
'reference',
292 'Error Reference File':
'error_reference',
295 'Unsupported Platforms':
'unsupported_platforms',
296 'Stack Trace':
'stack_trace'
298 resultDict = [(key, getattr(self, attr))
299 for key, attr
in field_mapping.items()
300 if getattr(self, attr)]
301 resultDict.append((
'Working Directory',
303 os.path.join(os.getcwd(), self.
workdir))))
305 resultDict.extend(self.
result.annotations.items())
307 resultDict = dict(resultDict)
310 if "Validator" in resultDict:
312 "Validator"] =
'<code lang="{}"><pre>{}</pre></code>'.
format(
313 "python", escape_for_html(resultDict[
"Validator"]))
323 elif stderr.strip() != self.
stderr.strip():
324 self.
causes.append(
'standard error')
325 return result, self.
causes
336 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.
339 if reference
is None:
349 filter(
None,
map(
lambda s: s.rstrip(), reference.splitlines())))
351 raise RuntimeError(
"Empty (or null) reference")
354 filter(
None,
map(
lambda s: s.rstrip(), stdout.splitlines())))
356 res_field =
"GaudiTest.RefBlock"
358 res_field +=
"_%s" % id
360 if signature
is None:
361 if signature_offset < 0:
362 signature_offset = len(reference) + signature_offset
363 signature = reflines[signature_offset]
366 pos = outlines.index(signature)
367 outlines = outlines[pos - signature_offset:pos + len(reflines) -
369 if reflines != outlines:
370 msg =
"standard output"
373 if not msg
in causes:
375 result[res_field +
".observed"] = result.Quote(
378 causes.append(
"missing signature")
379 result[res_field +
".signature"] = result.Quote(signature)
380 if len(reflines) > 1
or signature != reflines[0]:
381 result[res_field +
".expected"] = result.Quote(
"\n".join(reflines))
393 Count the number of messages with required severity (by default ERROR and FATAL)
394 and check if their numbers match the expected ones (0 by default).
395 The dictionary "expected" can be used to tune the number of errors and fatals
396 allowed, or to limit the number of expected warnings etc.
411 outlines = stdout.splitlines()
412 from math
import log10
413 fmt =
"%%%dd - %%s" % (int(log10(len(outlines) + 1)))
419 if len(words) >= 2
and words[1]
in errors:
420 errors[words[1]].append(fmt % (linecount, l.rstrip()))
423 if len(errors[e]) != expected[e]:
424 causes.append(
'%s(%d)' % (e, len(errors[e])))
425 result[
"GaudiTest.lines.%s" % e] = result.Quote(
'\n'.join(
427 result[
"GaudiTest.lines.%s.expected#" % e] = result.Quote(
437 ignore=r"Basket|.*size|Compression"):
439 Compare the TTree summaries in stdout with the ones in trees_dict or in
440 the reference file. By default ignore the size, compression and basket
442 The presence of TTree summaries when none is expected is not a failure.
450 if trees_dict
is None:
453 if lreference
and os.path.isfile(lreference):
458 from pprint
import PrettyPrinter
461 result[
"GaudiTest.TTrees.expected"] = result.Quote(
462 pp.pformat(trees_dict))
464 result[
"GaudiTest.TTrees.ignore"] = result.Quote(ignore)
469 causes.append(
"trees summaries")
472 result[
"GaudiTest.TTrees.failure_on"] = result.Quote(msg)
473 result[
"GaudiTest.TTrees.found"] = result.Quote(pp.pformat(trees))
484 Compare the TTree summaries in stdout with the ones in trees_dict or in
485 the reference file. By default ignore the size, compression and basket
487 The presence of TTree summaries when none is expected is not a failure.
499 if lreference
and os.path.isfile(lreference):
504 from pprint
import PrettyPrinter
507 result[
"GaudiTest.Histos.expected"] = result.Quote(
510 result[
"GaudiTest.Histos.ignore"] = result.Quote(ignore)
515 causes.append(
"histos summaries")
517 result[
"GaudiTest.Histos.failure_on"] = result.Quote(msg)
518 result[
"GaudiTest.Histos.found"] = result.Quote(pp.pformat(histos))
529 Default validation acti*on: compare standard output and error to the
544 preproc = normalizeExamples
548 if lreference
and os.path.isfile(lreference):
550 lreference,
"standard output",
"Output Diff",
551 preproc=preproc)(stdout, result)
553 causes += [
"missing reference file"]
557 if causes
and lreference:
560 newrefname =
'.'.join([lreference,
'new'])
561 while os.path.exists(newrefname):
563 newrefname =
'.'.join([lreference,
'~%d~' % cnt,
'new'])
564 newref = open(newrefname,
"w")
566 for l
in stdout.splitlines():
567 newref.write(l.rstrip() +
'\n')
569 result[
'New Output Reference File'] = os.path.relpath(
580 if os.path.isfile(lreference):
585 preproc=preproc)(stderr, result)
587 newcauses = [
"missing error reference file"]
589 if newcauses
and lreference:
591 newrefname =
'.'.join([lreference,
'new'])
592 while os.path.exists(newrefname):
594 newrefname =
'.'.join([lreference,
'~%d~' % cnt,
'new'])
595 newref = open(newrefname,
"w")
597 for l
in stderr.splitlines():
598 newref.write(l.rstrip() +
'\n')
600 result[
'New Error Reference File'] = os.path.relpath(
604 "ExecTest.expected_stderr")(stderr,
614 def platformSplit(p):
616 delim = re.compile(
'-' in p
and r"[-+]" or r"_")
617 return set(delim.split(p))
619 reference = os.path.normpath(
620 os.path.join(self.
basedir, os.path.expandvars(reffile)))
623 spec_ref = reference[:-3] +
GetPlatform(self)[0:3] + reference[-3:]
624 if os.path.isfile(spec_ref):
628 dirname, basename = os.path.split(reference)
631 head = basename +
"."
634 if 'do0' in platform:
637 for f
in os.listdir(dirname):
638 if f.startswith(head):
639 req_plat = platformSplit(f[head_len:])
640 if platform.issuperset(req_plat):
641 candidates.append((len(req_plat), f))
646 reference = os.path.join(dirname, candidates[-1][1])
658 from GaudiKernel
import ROOT6WorkAroundEnabled
671 Function used to normalize the used path
673 newPath = os.path.normpath(os.path.expandvars(p))
674 if os.path.exists(newPath):
675 p = os.path.realpath(newPath)
681 Locates an executable in the executables path ($PATH) and returns the full
682 path to it. An application is looked for with or without the '.exe' suffix.
683 If the executable cannot be found, None is returned
685 if os.path.isabs(executable):
686 if not os.path.isfile(executable):
687 if executable.endswith(
'.exe'):
688 if os.path.isfile(executable[:-4]):
689 return executable[:-4]
691 executable = os.path.split(executable)[1]
694 for d
in os.environ.get(
"PATH").split(os.pathsep):
695 fullpath = os.path.join(d, executable)
696 if os.path.isfile(fullpath):
698 elif executable.endswith(
'.exe')
and os.path.isfile(fullpath[:-4]):
714 UNTESTED =
'UNTESTED'
724 def __init__(self, kind=None, id=None, outcome=PASS, annotations={}):
728 assert isinstance(key, six.string_types)
732 assert isinstance(key, six.string_types)
734 value, six.string_types),
'{!r} is not a string'.
format(value)
739 Convert text to html by escaping special chars and adding <pre> tags.
741 return "<pre>{}</pre>".
format(escape_for_html(text))
760 """Validate the output of the program.
761 'stdout' -- A string containing the data written to the standard output
763 'stderr' -- A string containing the data written to the standard error
765 'result' -- A 'Result' object. It may be used to annotate
766 the outcome according to the content of stderr.
767 returns -- A list of strings giving causes of failure."""
772 causes.append(self.
cause)
778 """Compare 's1' and 's2', ignoring line endings.
781 returns -- True if 's1' and 's2' are the same, ignoring
782 differences in line endings."""
786 to_ignore = re.compile(
787 r'Warning in <TInterpreter::ReadRootmapFile>: .* is already in .*'
791 return not to_ignore.match(l)
793 return list(filter(keep_line, s1.splitlines())) == list(
794 filter(keep_line, s2.splitlines()))
796 return s1.splitlines() == s2.splitlines()
801 """ Base class for a callable that takes a file and returns a modified
816 if not isinstance(input, six.string_types):
820 lines = input.splitlines()
824 output =
'\n'.join(output)
853 if line.find(s) >= 0:
868 if self.
start in line:
871 elif self.
end in line:
881 when = re.compile(when)
885 if isinstance(rhs, RegexpReplacer):
887 res._operations = self.
_operations + rhs._operations
889 res = FilePreprocessor.__add__(self, rhs)
894 if w
is None or w.search(line):
895 line = o.sub(r, line)
902 "[0-2]?[0-9]:[0-5][0-9]:[0-5][0-9] [0-9]{4}[-/][01][0-9][-/][0-3][0-9][ A-Z]*",
903 "00:00:00 1970-01-01")
905 normalizeEOL.__processLine__ =
lambda line: str(line).rstrip() +
'\n'
909 skipEmptyLines.__processLine__ =
lambda line: (line.strip()
and line)
or None
923 line = line[:(pos + self.
siglen)]
924 lst = line[(pos + self.
siglen):].split()
926 line +=
" ".join(lst)
932 Sort group of lines matching a regular expression
936 self.
exp = exp
if hasattr(exp,
'match')
else re.compile(exp)
939 match = self.
exp.match
955 normalizeExamples = maskPointers + normalizeDate
958 (
"TIMER.TIMER",
r"\s+[+-]?[0-9]+[0-9.]*",
" 0"),
959 (
"release all pending",
r"^.*/([^/]*:.*)",
r"\1"),
960 (
"^#.*file",
r"file '.*[/\\]([^/\\]*)$",
r"file '\1"),
961 (
"^JobOptionsSvc.*options successfully read in from",
962 r"read in from .*[/\\]([^/\\]*)$",
966 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}",
967 "00000000-0000-0000-0000-000000000000"),
969 (
"ServiceLocatorHelper::",
"ServiceLocatorHelper::(create|locate)Service",
970 "ServiceLocatorHelper::service"),
972 (
None,
r"e([-+])0([0-9][0-9])",
r"e\1\2"),
974 (
None,
r'Service reference count check:',
975 r'Looping over all active services...'),
978 r"^(.*(DEBUG|SUCCESS) List of ALL properties of .*#properties = )\d+",
980 (
'ApplicationMgr',
r'(declareMultiSvcType|addMultiSvc): ',
''),
981 (
r"Property \['Name': Value\]",
r"( = '[^']+':)'(.*)'",
r'\1\2'),
982 (
'TimelineSvc',
"to file 'TimelineFile':",
"to file "),
983 (
'DataObjectHandleBase',
r'DataObjectHandleBase\("([^"]*)"\)',
r"'\1'"),
990 "JobOptionsSvc INFO # ",
991 "JobOptionsSvc WARNING # ",
994 "This machine has a speed",
997 "ToolSvc.Sequenc... INFO",
998 "DataListenerSvc INFO XML written to file:",
1001 "DEBUG No writable file catalog found which contains FID:",
1002 "DEBUG Service base class initialized successfully",
1004 "DEBUG Incident timing:",
1008 "INFO 'CnvServices':[",
1010 "DEBUG 'CnvServices':[",
1015 'ServiceLocatorHelper::service: found service JobOptionsSvc',
1017 'mismatching case for property name:',
1019 'Histograms saving not required.',
1021 'Properties are dumped into',
1024 r"^JobOptionsSvc INFO *$",
1027 r"(Always|SUCCESS)\s*(Root f|[^ ]* F)ile version:",
1028 r"File '.*.xml' does not exist",
1029 r"INFO Refer to dataset .* by its file ID:",
1030 r"INFO Referring to dataset .* by its file ID:",
1031 r"INFO Disconnect from dataset",
1032 r"INFO Disconnected from dataset",
1033 r"INFO Disconnected data IO:",
1034 r"IncidentSvc\s*(DEBUG (Adding|Removing)|VERBOSE Calling)",
1036 r".*StatusCodeSvc.*",
1037 r"Num\s*\|\s*Function\s*\|\s*Source Library",
1040 r"ERROR Failed to modify file: .* Errno=2 No such file or directory",
1042 r"^ +[0-9]+ \|.*ROOT",
1043 r"^ +[0-9]+ \|.*\|.*Dict",
1045 r"EventLoopMgr.*---> Loop Finished",
1046 r"HiveSlimEventLo.*---> Loop Finished",
1051 r"SUCCESS\s*Booked \d+ Histogram\(s\)",
1055 r"Property(.*)'Audit(Algorithm|Tool|Service)s':",
1056 r"Property(.*)'Audit(Begin|End)Run':",
1058 r"Property(.*)'AuditRe(start|initialize)':",
1059 r"Property(.*)'Blocking':",
1061 r"Property(.*)'ErrorCount(er)?':",
1063 r"Property(.*)'Sequential':",
1065 r"Property(.*)'FilterCircularDependencies':",
1067 r"Property(.*)'IsClonable':",
1069 r"Property update for OutputLevel : new value =",
1070 r"EventLoopMgr\s*DEBUG Creating OutputStream",
1077 r'Warning in <TInterpreter::ReadRootmapFile>: .* is already in .*',
1080 normalizeExamples = (
1081 lineSkipper + normalizeExamples + skipEmptyLines + normalizeEOL +
1089 def __init__(self, reffile, cause, result_key, preproc=normalizeExamples):
1097 if os.path.isfile(self.
reffile):
1098 orig = open(self.
reffile).readlines()
1101 result[self.
result_key +
'.preproc.orig'] = \
1102 result.Quote(
'\n'.join(
map(str.strip, orig)))
1105 new = stdout.splitlines()
1109 diffs = difflib.ndiff(orig, new, charjunk=difflib.IS_CHARACTER_JUNK)
1111 map(
lambda x: x.strip(), filter(
lambda x: x[0] !=
" ", diffs)))
1113 result[self.
result_key] = result.Quote(
"\n".join(filterdiffs))
1117 +) standard output of the test""")
1119 result.Quote(
'\n'.join(
map(str.strip, new)))
1120 causes.append(self.
cause)
1126 Scan stdout to find ROOT TTree summaries and digest them.
1128 stars = re.compile(
r"^\*+$")
1129 outlines = stdout.splitlines()
1130 nlines = len(outlines)
1136 while i < nlines
and not stars.match(outlines[i]):
1141 trees[tree[
"Name"]] = tree
1148 Check that all the keys in reference are in to_check too, with the same value.
1149 If the value is a dict, the function is called recursively. to_check can
1150 contain more keys than reference, that will not be tested.
1151 The function returns at the first difference found.
1156 ignore_re = re.compile(ignore)
1157 keys = [key
for key
in reference
if not ignore_re.match(key)]
1159 keys = reference.keys()
1163 if (
type(reference[k])
is dict)
and (
type(to_check[k])
is dict):
1166 failed = fail_keys =
cmpTreesDicts(reference[k], to_check[k],
1170 failed = to_check[k] != reference[k]
1175 fail_keys.insert(0, k)
1186 if c
is None or r
is None:
1188 return (fail_path, r, c)
1192 h_count_re = re.compile(
1193 r"^(.*)SUCCESS\s+Booked (\d+) Histogram\(s\) :\s+([\s\w=-]*)")
1198 Parse the TTree summary table in lines, starting from pos.
1199 Returns a tuple with the dictionary with the digested informations and the
1200 position of the first line after the summary.
1207 return [f.strip()
for f
in l.strip(
"*\n").split(
':', 2)]
1211 cols = splitcols(ll[0])
1212 r[
"Name"], r[
"Title"] = cols[1:]
1214 cols = splitcols(ll[1])
1215 r[
"Entries"] = int(cols[1])
1217 sizes = cols[2].split()
1218 r[
"Total size"] = int(sizes[2])
1219 if sizes[-1] ==
"memory":
1222 r[
"File size"] = int(sizes[-1])
1224 cols = splitcols(ll[2])
1225 sizes = cols[2].split()
1226 if cols[0] ==
"Baskets":
1227 r[
"Baskets"] = int(cols[1])
1228 r[
"Basket size"] = int(sizes[2])
1229 r[
"Compression"] = float(sizes[-1])
1232 if i < (count - 3)
and lines[i].startswith(
"*Tree"):
1233 result = parseblock(lines[i:i + 3])
1234 result[
"Branches"] = {}
1236 while i < (count - 3)
and lines[i].startswith(
"*Br"):
1237 if i < (count - 2)
and lines[i].startswith(
"*Branch "):
1241 branch = parseblock(lines[i:i + 3])
1242 result[
"Branches"][branch[
"Name"]] = branch
1250 Extract the histograms infos from the lines starting at pos.
1251 Returns the position of the first line after the summary block.
1254 h_table_head = re.compile(
1255 r'SUCCESS\s+(1D|2D|3D|1D profile|2D profile) histograms in directory\s+"(\w*)"'
1257 h_short_summ = re.compile(
r"ID=([^\"]+)\s+\"([^\"]+)\"\s+(.*)")
1262 m = h_count_re.search(lines[pos])
1263 name = m.group(1).strip()
1264 total = int(m.group(2))
1266 for k, v
in [x.split(
"=")
for x
in m.group(3).split()]:
1269 header[
"Total"] = total
1273 m = h_table_head.search(lines[pos])
1276 t = t.replace(
" profile",
"Prof")
1283 if l.startswith(
" | ID"):
1285 titles = [x.strip()
for x
in l.split(
"|")][1:]
1287 while pos < nlines
and lines[pos].startswith(
" |"):
1289 values = [x.strip()
for x
in l.split(
"|")][1:]
1291 for i
in range(len(titles)):
1292 hcont[titles[i]] = values[i]
1293 cont[hcont[
"ID"]] = hcont
1295 elif l.startswith(
" ID="):
1296 while pos < nlines
and lines[pos].startswith(
" ID="):
1299 for x
in h_short_summ.search(lines[pos]).groups()
1301 cont[values[0]] = values
1305 "Cannot understand line %d: '%s'" % (pos, l))
1309 summ[d][
"header"] = header
1314 summ[name] = {
"header": header}
1320 Scan stdout to find ROOT TTree summaries and digest them.
1322 outlines = stdout.splitlines()
1323 nlines = len(outlines) - 1
1331 match = h_count_re.search(outlines[pos])
1332 while pos < nlines
and not match:
1334 match = h_count_re.search(outlines[pos])
1337 summaries.update(summ)
1343 Return the platform Id defined in CMTCONFIG or SCRAM_ARCH.
1347 if "BINARY_TAG" in os.environ:
1348 arch = os.environ[
"BINARY_TAG"]
1349 elif "CMTCONFIG" in os.environ:
1350 arch = os.environ[
"CMTCONFIG"]
1351 elif "SCRAM_ARCH" in os.environ:
1352 arch = os.environ[
"SCRAM_ARCH"]
1353 elif os.environ.get(
"ENV_CMAKE_BUILD_TYPE",
"")
in (
"Debug",
"FastDebug",
1356 elif os.environ.get(
"ENV_CMAKE_BUILD_TYPE",
1357 "")
in (
"Release",
"MinSizeRel",
"RelWithDebInfo",
1365 Return True if the current platform is Windows.
1367 This function was needed because of the change in the CMTCONFIG format,
1368 from win32_vc71_dbg to i686-winxp-vc9-dbg.
1371 return "winxp" in platform
or platform.startswith(
"win")