21 from datetime
import datetime, timedelta
22 from html
import escape
as escape_for_html
23 from subprocess
import PIPE, STDOUT, Popen
24 from tempfile
import NamedTemporaryFile, mkdtemp
25 from unittest
import TestCase
27 if sys.version_info < (3, 5):
30 from codecs
import backslashreplace_errors, register_error
33 if isinstance(exc, UnicodeDecodeError):
34 code =
hex(ord(exc.object[exc.start]))
35 return (
"\\" + code[1:], exc.start + 1)
37 return backslashreplace_errors(exc)
39 register_error(
"backslashreplace", _new_backslashreplace_errors)
41 del backslashreplace_errors
42 del _new_backslashreplace_errors
47 OUTPUT_LIMIT = int(os.environ.get(
"GAUDI_TEST_STDOUT_LIMIT", 100 * 1024**2))
52 Take a string with invalid ASCII/UTF characters and quote them so that the
53 string can be used in an XML text.
55 >>> sanitize_for_xml('this is \x1b')
56 'this is [NON-XML-CHAR-0x1B]'
58 bad_chars = re.compile(
"[\x00-\x08\x0b\x0c\x0e-\x1f\ud800-\udfff\ufffe\uffff]")
62 return "".join(
"[NON-XML-CHAR-0x%2X]" % ord(c)
for c
in match.group())
64 return bad_chars.sub(quote, data)
68 """helper to debug GAUDI-1084, dump the list of processes"""
69 from getpass
import getuser
71 if "WORKSPACE" in os.environ:
72 p = Popen([
"ps",
"-fH",
"-U", getuser()], stdout=PIPE)
73 with open(os.path.join(os.environ[
"WORKSPACE"], name),
"wb")
as f:
74 f.write(p.communicate()[0])
79 Send a signal to a process and all its child processes (starting from the
82 log = logging.getLogger(
"kill_tree")
83 ps_cmd = [
"ps",
"--no-headers",
"-o",
"pid",
"--ppid", str(ppid)]
86 get_children = Popen(ps_cmd, stdout=PIPE, stderr=PIPE, env={})
87 children =
map(int, get_children.communicate()[0].split())
88 for child
in children:
91 log.debug(
"killing process %d", ppid)
93 except OSError
as err:
96 log.debug(
"no such process %d", ppid)
103 _common_tmpdir =
None
133 logging.debug(
"running test %s", self.
name)
144 "TIMEOUT_DETAIL":
None,
150 r"from\s+Gaudi.Configuration\s+import\s+\*|"
151 r"from\s+Configurables\s+import",
154 suffix, lang =
".py",
"python"
156 suffix, lang =
".opts",
"c++"
157 self.
result[
"Options"] = (
158 '<pre><code class="language-{}">{}</code></pre>'.
format(
159 lang, escape_for_html(self.
options)
162 optionFile = NamedTemporaryFile(suffix=suffix)
163 optionFile.file.write(self.
options.encode(
"utf-8"))
170 or platform.platform()
177 if re.search(prex, platform_id)
199 prog_ext = os.path.splitext(prog)[1]
200 if prog_ext
not in [
".exe",
".py",
".bat"]:
204 prog =
which(prog)
or prog
206 args = list(
map(RationalizePath, self.
args))
208 if prog_ext ==
".py":
218 "stdout": NamedTemporaryFile(),
219 "stderr": NamedTemporaryFile(),
224 logging.debug(
"executing %r in %s", params, workdir)
227 stdout=tmp_streams[
"stdout"],
228 stderr=tmp_streams[
"stderr"],
231 logging.debug(
"(pid: %d)", self.
proc.pid)
232 self.
proc.communicate()
233 tmp_streams[
"stdout"].seek(0)
235 tmp_streams[
"stdout"]
237 .decode(
"utf-8", errors=
"backslashreplace")
239 tmp_streams[
"stderr"].seek(0)
241 tmp_streams[
"stderr"]
243 .decode(
"utf-8", errors=
"backslashreplace")
246 thread = threading.Thread(target=target)
249 when_to_stop = datetime.now() + timedelta(seconds=self.
timeout)
250 too_big_stream =
None
252 datetime.now() < when_to_stop
253 and thread.is_alive()
254 and not too_big_stream
259 if thread.is_alive():
260 for stream
in tmp_streams:
261 if os.path.getsize(tmp_streams[stream].name) > OUTPUT_LIMIT:
262 too_big_stream = stream
264 if thread.is_alive():
265 if not too_big_stream:
267 "time out in test %s (pid %d)", self.
name, self.
proc.pid
275 "--eval-command=thread apply all backtrace",
277 gdb = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=STDOUT)
279 "utf-8", errors=
"backslashreplace"
281 self.
causes.append(
"timeout")
284 "too big %s detected (pid %d)", too_big_stream, self.
proc.pid
286 self.
result[f
"{too_big_stream} limit"] = str(OUTPUT_LIMIT)
287 self.
result[f
"{too_big_stream} size"] = str(
288 os.path.getsize(tmp_streams[too_big_stream].name)
290 self.
causes.append(f
"too big {too_big_stream}")
294 if thread.is_alive():
301 f
"completed test {self.name} with returncode = {self.returnedCode}"
303 logging.debug(
"validating test...")
304 val_start_time = time.perf_counter()
308 self.
validate_time = round(time.perf_counter() - val_start_time, 2)
310 logging.debug(f
"skipped test {self.name}")
315 shutil.rmtree(workdir,
True)
319 if self.
status !=
"skipped":
321 if self.
signal is not None:
323 self.
causes.append(
"exit code")
327 self.
causes.append(
"exit code")
330 self.
causes.append(
"exit code")
340 logging.debug(
"%s: %s", self.
name, self.
status)
342 "Exit Code":
"returnedCode",
345 "Runtime Environment":
"environment",
348 "Program Name":
"program",
350 "Validator":
"validator",
351 "Validation execution time":
"validate_time",
352 "Output Reference File":
"reference",
353 "Error Reference File":
"error_reference",
356 "Unsupported Platforms":
"unsupported_platforms",
357 "Stack Trace":
"stack_trace",
360 (key, getattr(self, attr))
361 for key, attr
in field_mapping.items()
362 if getattr(self, attr)
371 resultDict.extend(self.
result.annotations.items())
373 resultDict = dict(resultDict)
376 if "Validator" in resultDict:
377 resultDict[
"Validator"] = (
378 '<pre><code class="language-{}">{}</code></pre>'.
format(
379 "python", escape_for_html(resultDict[
"Validator"])
391 elif stderr.strip() != self.
stderr.strip():
392 self.
causes.append(
"standard error")
393 return result, self.
causes
406 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.
409 if reference
is None:
418 reflines = list(filter(
None,
map(
lambda s: s.rstrip(), reference.splitlines())))
420 raise RuntimeError(
"Empty (or null) reference")
422 outlines = list(filter(
None,
map(
lambda s: s.rstrip(), stdout.splitlines())))
424 res_field =
"GaudiTest.RefBlock"
426 res_field +=
"_%s" % id
428 if signature
is None:
429 if signature_offset < 0:
430 signature_offset = len(reference) + signature_offset
431 signature = reflines[signature_offset]
434 pos = outlines.index(signature)
436 pos - signature_offset : pos + len(reflines) - signature_offset
438 if reflines != outlines:
439 msg =
"standard output"
442 if msg
not in causes:
444 result[res_field +
".observed"] = result.Quote(
"\n".join(outlines))
446 causes.append(
"missing signature")
447 result[res_field +
".signature"] = result.Quote(signature)
448 if len(reflines) > 1
or signature != reflines[0]:
449 result[res_field +
".expected"] = result.Quote(
"\n".join(reflines))
453 self, expected={"ERROR": 0,
"FATAL": 0}, stdout=
None, result=
None, causes=
None
456 Count the number of messages with required severity (by default ERROR and FATAL)
457 and check if their numbers match the expected ones (0 by default).
458 The dictionary "expected" can be used to tune the number of errors and fatals
459 allowed, or to limit the number of expected warnings etc.
474 outlines = stdout.splitlines()
475 from math
import log10
477 fmt =
"%%%dd - %%s" % (int(log10(len(outlines) + 1)))
483 if len(words) >= 2
and words[1]
in errors:
484 errors[words[1]].append(fmt % (linecount, l.rstrip()))
487 if len(errors[e]) != expected[e]:
488 causes.append(
"%s(%d)" % (e, len(errors[e])))
489 result[
"GaudiTest.lines.%s" % e] = result.Quote(
"\n".join(errors[e]))
490 result[
"GaudiTest.lines.%s.expected#" % e] = result.Quote(
502 ignore=r"Basket|.*size|Compression",
505 Compare the TTree summaries in stdout with the ones in trees_dict or in
506 the reference file. By default ignore the size, compression and basket
508 The presence of TTree summaries when none is expected is not a failure.
516 if trees_dict
is None:
519 if lreference
and os.path.isfile(lreference):
524 from pprint
import PrettyPrinter
528 result[
"GaudiTest.TTrees.expected"] = result.Quote(pp.pformat(trees_dict))
530 result[
"GaudiTest.TTrees.ignore"] = result.Quote(ignore)
535 causes.append(
"trees summaries")
537 result[
"GaudiTest.TTrees.failure_on"] = result.Quote(msg)
538 result[
"GaudiTest.TTrees.found"] = result.Quote(pp.pformat(trees))
543 self, stdout=None, result=None, causes=None, dict=None, ignore=None
546 Compare the TTree summaries in stdout with the ones in trees_dict or in
547 the reference file. By default ignore the size, compression and basket
549 The presence of TTree summaries when none is expected is not a failure.
561 if lreference
and os.path.isfile(lreference):
566 from pprint
import PrettyPrinter
570 result[
"GaudiTest.Histos.expected"] = result.Quote(pp.pformat(dict))
572 result[
"GaudiTest.Histos.ignore"] = result.Quote(ignore)
577 causes.append(
"histos summaries")
579 result[
"GaudiTest.Histos.failure_on"] = result.Quote(msg)
580 result[
"GaudiTest.Histos.found"] = result.Quote(pp.pformat(histos))
585 self, stdout=None, stderr=None, result=None, causes=None, preproc=None
588 Default validation acti*on: compare standard output and error to the
603 preproc = normalizeTestSuite
607 if lreference
and os.path.isfile(lreference):
609 lreference,
"standard output",
"Output Diff", preproc=preproc
612 causes += [
"missing reference file"]
616 if causes
and lreference:
619 newrefname =
".".join([lreference,
"new"])
620 while os.path.exists(newrefname):
622 newrefname =
".".join([lreference,
"~%d~" % cnt,
"new"])
623 newref = open(newrefname,
"w")
625 for l
in stdout.splitlines():
626 newref.write(l.rstrip() +
"\n")
628 result[
"New Output Reference File"] = os.path.relpath(
640 if os.path.isfile(lreference):
642 lreference,
"standard error",
"Error Diff", preproc=preproc
645 newcauses = [
"missing error reference file"]
647 if newcauses
and lreference:
649 newrefname =
".".join([lreference,
"new"])
650 while os.path.exists(newrefname):
652 newrefname =
".".join([lreference,
"~%d~" % cnt,
"new"])
653 newref = open(newrefname,
"w")
655 for l
in stderr.splitlines():
656 newref.write(l.rstrip() +
"\n")
658 result[
"New Error Reference File"] = os.path.relpath(
663 lreference,
"standard error",
"ExecTest.expected_stderr"
676 JSON validation action: compare json file to reference file
684 if not os.path.isfile(output_file):
685 causes.append(f
"output file {output_file} does not exist")
689 with open(output_file)
as f:
690 output = json.load(f)
691 except json.JSONDecodeError
as err:
692 causes.append(
"json parser error")
693 result[
"output_parse_error"] = f
"json parser error in {output_file}: {err}"
698 causes.append(
"reference file not set")
699 elif not os.path.isfile(lreference):
700 causes.append(
"reference file does not exist")
703 if causes
and lreference:
706 newrefname =
".".join([lreference,
"new"])
707 while os.path.exists(newrefname):
709 newrefname =
".".join([lreference,
"~%d~" % cnt,
"new"])
710 with open(newrefname,
"w")
as newref:
711 json.dump(output, newref, indent=4)
712 result[
"New JSON Output Reference File"] = os.path.relpath(
729 def platformSplit(p):
730 return set(re.split(
r"[-+]", p))
732 reference = os.path.normpath(
733 os.path.join(self.
basedir, os.path.expandvars(reffile))
737 spec_ref = reference[:-3] +
GetPlatform(self)[0:3] + reference[-3:]
738 if os.path.isfile(spec_ref):
742 dirname, basename = os.path.split(reference)
745 head = basename +
"."
748 if "do0" in platform:
751 for f
in os.listdir(dirname):
752 if f.startswith(head):
753 req_plat = platformSplit(f[head_len:])
754 if platform.issuperset(req_plat):
755 candidates.append((len(req_plat), f))
760 reference = os.path.join(dirname, candidates[-1][1])
770 from GaudiKernel
import ROOT6WorkAroundEnabled
783 Function used to normalize the used path
785 newPath = os.path.normpath(os.path.expandvars(p))
786 if os.path.exists(newPath):
787 p = os.path.realpath(newPath)
793 Locates an executable in the executables path ($PATH) and returns the full
794 path to it. An application is looked for with or without the '.exe' suffix.
795 If the executable cannot be found, None is returned
797 if os.path.isabs(executable):
798 if not os.path.isfile(executable):
799 if executable.endswith(
".exe"):
800 if os.path.isfile(executable[:-4]):
801 return executable[:-4]
803 executable = os.path.split(executable)[1]
806 for d
in os.environ.get(
"PATH").split(os.pathsep):
807 fullpath = os.path.join(d, executable)
808 if os.path.isfile(fullpath):
810 elif executable.endswith(
".exe")
and os.path.isfile(fullpath[:-4]):
824 UNTESTED =
"UNTESTED"
834 def __init__(self, kind=None, id=None, outcome=PASS, annotations={}):
838 assert isinstance(key, str)
842 assert isinstance(key, str)
843 assert isinstance(value, str),
"{!r} is not a string".
format(value)
848 Convert text to html by escaping special chars and adding <pre> tags.
850 return "<pre>{}</pre>".
format(escape_for_html(text))
869 """Validate the output of the program.
870 'stdout' -- A string containing the data written to the standard output
872 'stderr' -- A string containing the data written to the standard error
874 'result' -- A 'Result' object. It may be used to annotate
875 the outcome according to the content of stderr.
876 returns -- A list of strings giving causes of failure."""
881 causes.append(self.
cause)
887 """Compare 's1' and 's2', ignoring line endings.
890 returns -- True if 's1' and 's2' are the same, ignoring
891 differences in line endings."""
895 to_ignore = re.compile(
896 r"Warning in <TInterpreter::ReadRootmapFile>: .* is already in .*"
900 return not to_ignore.match(l)
902 return list(filter(keep_line, s1.splitlines())) == list(
903 filter(keep_line, s2.splitlines())
906 return s1.splitlines() == s2.splitlines()
911 """Base class for a callable that takes a file and returns a modified
926 if not isinstance(input, str):
930 lines = input.splitlines()
934 output =
"\n".join(output)
964 if line.find(s) >= 0:
979 if self.
start in line:
982 elif self.
end in line:
992 when = re.compile(when)
996 if isinstance(rhs, RegexpReplacer):
998 res._operations = self.
_operations + rhs._operations
1000 res = FilePreprocessor.__add__(self, rhs)
1005 if w
is None or w.search(line):
1006 line = o.sub(r, line)
1013 "[0-2]?[0-9]:[0-5][0-9]:[0-5][0-9] [0-9]{4}[-/][01][0-9][-/][0-3][0-9][ A-Z]*",
1014 "00:00:00 1970-01-01",
1017 normalizeEOL.__processLine__ =
lambda line: str(line).rstrip() +
"\n"
1021 skipEmptyLines.__processLine__ =
lambda line: (line.strip()
and line)
or None
1035 line = line[: (pos + self.
siglen)]
1036 lst = line[(pos + self.
siglen) :].split()
1038 line +=
" ".join(lst)
1044 Sort group of lines matching a regular expression
1048 self.
exp = exp
if hasattr(exp,
"match")
else re.compile(exp)
1051 match = self.
exp.match
1060 output.extend(group)
1067 normalizeTestSuite = maskPointers + normalizeDate
1069 (
"TIMER",
r"\s+[+-]?[0-9]+[0-9.e+-]*",
" 0"),
1070 (
"release all pending",
r"^.*/([^/]*:.*)",
r"\1"),
1071 (
"^#.*file",
r"file '.*[/\\]([^/\\]*)$",
r"file '\1"),
1073 "^JobOptionsSvc.*options successfully read in from",
1074 r"read in from .*[/\\]([^/\\]*)$",
1080 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}",
1081 "00000000-0000-0000-0000-000000000000",
1085 "ServiceLocatorHelper::",
1086 "ServiceLocatorHelper::(create|locate)Service",
1087 "ServiceLocatorHelper::service",
1090 (
None,
r"e([-+])0([0-9][0-9])",
r"e\1\2"),
1092 (
None,
r"Service reference count check:",
r"Looping over all active services..."),
1096 r"^(.*(DEBUG|SUCCESS) List of ALL properties of .*#properties = )\d+",
1099 (
"ApplicationMgr",
r"(declareMultiSvcType|addMultiSvc): ",
""),
1100 (
r"Property \['Name': Value\]",
r"( = '[^']+':)'(.*)'",
r"\1\2"),
1101 (
"TimelineSvc",
"to file 'TimelineFile':",
"to file "),
1102 (
"DataObjectHandleBase",
r'DataObjectHandleBase\("([^"]*)"\)',
r"'\1'"),
1105 "Added successfully Conversion service:",
1106 "Added successfully Conversion service:",
1107 "Added successfully Conversion service ",
1115 "JobOptionsSvc INFO # ",
1116 "JobOptionsSvc WARNING # ",
1119 "This machine has a speed",
1121 "ToolSvc.Sequenc... INFO",
1122 "DataListenerSvc INFO XML written to file:",
1125 "DEBUG No writable file catalog found which contains FID:",
1126 "DEBUG Service base class initialized successfully",
1128 "DEBUG Incident timing:",
1132 "INFO 'CnvServices':[",
1134 "DEBUG 'CnvServices':[",
1139 "ServiceLocatorHelper::service: found service JobOptionsSvc",
1141 "mismatching case for property name:",
1143 "Histograms saving not required.",
1145 "Properties are dumped into",
1147 "WARNING no ROOT output file name",
1148 "INFO Writing ROOT histograms to:",
1149 "INFO Completed update of ROOT histograms in:",
1152 "data dependencies:",
1155 r"^JobOptionsSvc INFO *$",
1158 r"(Always|SUCCESS)\s*(Root f|[^ ]* F)ile version:",
1159 r"File '.*.xml' does not exist",
1160 r"INFO Refer to dataset .* by its file ID:",
1161 r"INFO Referring to dataset .* by its file ID:",
1162 r"INFO Disconnect from dataset",
1163 r"INFO Disconnected from dataset",
1164 r"INFO Disconnected data IO:",
1165 r"IncidentSvc\s*(DEBUG (Adding|Removing)|VERBOSE Calling)",
1167 r".*StatusCodeSvc.*",
1168 r".*StatusCodeCheck.*",
1169 r"Num\s*\|\s*Function\s*\|\s*Source Library",
1172 r"ERROR Failed to modify file: .* Errno=2 No such file or directory",
1174 r"^ +[0-9]+ \|.*ROOT",
1175 r"^ +[0-9]+ \|.*\|.*Dict",
1177 r"EventLoopMgr.*---> Loop Finished",
1178 r"HiveSlimEventLo.*---> Loop Finished",
1183 r"SUCCESS\s*Booked \d+ Histogram\(s\)",
1187 r"Property(.*)'Audit(Algorithm|Tool|Service)s':",
1188 r"Property(.*)'Audit(Begin|End)Run':",
1190 r"Property(.*)'AuditRe(start|initialize)':",
1191 r"Property(.*)'Blocking':",
1193 r"Property(.*)'ErrorCount(er)?':",
1195 r"Property(.*)'Sequential':",
1197 r"Property(.*)'FilterCircularDependencies':",
1199 r"Property(.*)'IsClonable':",
1201 r"Property update for OutputLevel : new value =",
1202 r"EventLoopMgr\s*DEBUG Creating OutputStream",
1203 r".*StalledEventMonitoring.*",
1212 r"Warning in <TInterpreter::ReadRootmapFile>: .* is already in .*",
1216 normalizeTestSuite = (
1218 + normalizeTestSuite
1225 normalizeExamples = normalizeTestSuite
1231 def __init__(self, reffile, cause, result_key, preproc=normalizeTestSuite):
1239 if os.path.isfile(self.
reffile):
1240 orig = open(self.
reffile).readlines()
1243 result[self.
result_key +
".preproc.orig"] = result.Quote(
1244 "\n".join(
map(str.strip, orig))
1248 new = stdout.splitlines()
1254 difflib.unified_diff(
1255 [l.rstrip()
for l
in orig],
1256 [l.rstrip()
for l
in new],
1258 fromfile=
"Reference file",
1259 tofile=
"Actual output",
1264 result[self.
result_key] = result.Quote(
"\n".join(filterdiffs))
1265 result[self.
result_key +
".preproc.new"] = result.Quote(
1266 "\n".join(
map(str.strip, new))
1268 causes.append(self.
cause)
1274 Scan stdout to find ROOT TTree summaries and digest them.
1276 stars = re.compile(
r"^\*+$")
1277 outlines = stdout.splitlines()
1278 nlines = len(outlines)
1284 while i < nlines
and not stars.match(outlines[i]):
1289 trees[tree[
"Name"]] = tree
1296 Check that all the keys in reference are in to_check too, with the same value.
1297 If the value is a dict, the function is called recursively. to_check can
1298 contain more keys than reference, that will not be tested.
1299 The function returns at the first difference found.
1304 ignore_re = re.compile(ignore)
1305 keys = [key
for key
in reference
if not ignore_re.match(key)]
1307 keys = reference.keys()
1311 if isinstance(reference[k], dict)
and isinstance(to_check[k], dict):
1314 failed = fail_keys =
cmpTreesDicts(reference[k], to_check[k], ignore)
1317 failed = to_check[k] != reference[k]
1322 fail_keys.insert(0, k)
1333 if c
is None or r
is None:
1335 return (fail_path, r, c)
1339 h_count_re = re.compile(
r"^(.*)SUCCESS\s+Booked (\d+) Histogram\(s\) :\s+([\s\w=-]*)")
1344 Parse the TTree summary table in lines, starting from pos.
1345 Returns a tuple with the dictionary with the digested informations and the
1346 position of the first line after the summary.
1353 return [f.strip()
for f
in l.strip(
"*\n").split(
":", 2)]
1358 cols = splitcols(ll[0])
1362 r[
"Name"], r[
"Title"] = cols[1:]
1367 r[
"Title"] = ll[1].strip(
"*\n").split(
"|")[1].strip()
1371 cols = splitcols(ll[1 + delta_i])
1372 r[
"Entries"] = int(cols[1])
1374 sizes = cols[2].split()
1375 r[
"Total size"] = int(sizes[2])
1376 if sizes[-1] ==
"memory":
1379 r[
"File size"] = int(sizes[-1])
1381 cols = splitcols(ll[2 + delta_i])
1382 sizes = cols[2].split()
1383 if cols[0] ==
"Baskets":
1384 r[
"Baskets"] = int(cols[1])
1385 r[
"Basket size"] = int(sizes[2])
1386 r[
"Compression"] = float(sizes[-1])
1390 def nextblock(lines, i):
1392 dots = re.compile(
r"^\.+$")
1393 stars = re.compile(
r"^\*+$")
1397 and not dots.match(lines[i + delta_i][1:-1])
1398 and not stars.match(lines[i + delta_i])
1403 if i < (count - 3)
and lines[i].startswith(
"*Tree"):
1404 i_nextblock = nextblock(lines, i)
1405 result = parseblock(lines[i:i_nextblock])
1406 result[
"Branches"] = {}
1408 while i < (count - 3)
and lines[i].startswith(
"*Br"):
1409 if i < (count - 2)
and lines[i].startswith(
"*Branch "):
1413 i_nextblock = nextblock(lines, i)
1414 if i_nextblock >= count:
1416 branch = parseblock(lines[i:i_nextblock])
1417 result[
"Branches"][branch[
"Name"]] = branch
1425 Extract the histograms infos from the lines starting at pos.
1426 Returns the position of the first line after the summary block.
1429 h_table_head = re.compile(
1430 r'SUCCESS\s+(1D|2D|3D|1D profile|2D profile) histograms in directory\s+"(\w*)"'
1432 h_short_summ = re.compile(
r"ID=([^\"]+)\s+\"([^\"]+)\"\s+(.*)")
1437 m = h_count_re.search(lines[pos])
1438 name = m.group(1).strip()
1439 total = int(m.group(2))
1441 for k, v
in [x.split(
"=")
for x
in m.group(3).split()]:
1444 header[
"Total"] = total
1448 m = h_table_head.search(lines[pos])
1451 t = t.replace(
" profile",
"Prof")
1458 if l.startswith(
" | ID"):
1460 titles = [x.strip()
for x
in l.split(
"|")][1:]
1462 while pos < nlines
and lines[pos].startswith(
" |"):
1464 values = [x.strip()
for x
in l.split(
"|")][1:]
1466 for i
in range(len(titles)):
1467 hcont[titles[i]] = values[i]
1468 cont[hcont[
"ID"]] = hcont
1470 elif l.startswith(
" ID="):
1471 while pos < nlines
and lines[pos].startswith(
" ID="):
1473 x.strip()
for x
in h_short_summ.search(lines[pos]).groups()
1475 cont[values[0]] = values
1478 raise RuntimeError(
"Cannot understand line %d: '%s'" % (pos, l))
1482 summ[d][
"header"] = header
1487 summ[name] = {
"header": header}
1493 Scan stdout to find ROOT TTree summaries and digest them.
1495 outlines = stdout.splitlines()
1496 nlines = len(outlines) - 1
1504 match = h_count_re.search(outlines[pos])
1505 while pos < nlines
and not match:
1507 match = h_count_re.search(outlines[pos])
1510 summaries.update(summ)
1516 Return the platform Id defined in CMTCONFIG or SCRAM_ARCH.
1520 if "BINARY_TAG" in os.environ:
1521 arch = os.environ[
"BINARY_TAG"]
1522 elif "CMTCONFIG" in os.environ:
1523 arch = os.environ[
"CMTCONFIG"]
1524 elif "SCRAM_ARCH" in os.environ:
1525 arch = os.environ[
"SCRAM_ARCH"]
1526 elif os.environ.get(
"ENV_CMAKE_BUILD_TYPE",
"")
in (
1532 elif os.environ.get(
"ENV_CMAKE_BUILD_TYPE",
"")
in (
1544 Return True if the current platform is Windows.
1546 This function was needed because of the change in the CMTCONFIG format,
1547 from win32_vc71_dbg to i686-winxp-vc9-dbg.
1550 return "winxp" in platform
or platform.startswith(
"win")
1555 """Validate JSON output.
1556 returns -- A list of strings giving causes of failure."""
1560 with open(ref)
as f:
1561 expected = json.load(f)
1562 except json.JSONDecodeError
as err:
1563 causes.append(
"json parser error")
1564 result[
"reference_parse_error"] = f
"json parser error in {ref}: {err}"
1569 causes.append(
"json content")
1570 result[
"json_diff"] =
"detailed diff was turned off"
1578 expected = sorted(expected, key=
lambda item: (item[
"component"], item[
"name"]))
1579 out = sorted(out, key=
lambda item: (item[
"component"], item[
"name"]))
1581 t.assertEqual(expected, out)
1582 except AssertionError
as err:
1583 causes.append(
"json content")
1584 result[
"json_diff"] = str(err).splitlines()[0]