17import xml.sax.saxutils
as XSS
18from pathlib
import Path
19from subprocess
import PIPE, Popen
20from typing
import Any, Dict, List
29 return f
'<pre><code class="language-{self.language}">{XSS.escape(self.code)}</code></pre>'
34 return any(re.search(p, platform_id)
for p
in unsupported_platforms)
40 return dumper.represent_scalar(
"tag:yaml.org,2002:str", data, style=
"|")
41 return dumper.represent_scalar(
"tag:yaml.org,2002:str", data)
46 Send a signal to a process and all its child processes (starting from the
52 ps_exe = shutil.which(
"ps")
or "ps"
53 ps_cmd = [ps_exe,
"--no-headers",
"-o",
"pid",
"--ppid", str(ppid)]
56 get_children = Popen(ps_cmd, stdout=PIPE, stderr=PIPE, env={})
57 children = map(int, get_children.communicate()[0].split())
58 for child
in children:
62 except OSError
as err:
69 Locates an executable in the executables path ($PATH) and returns the full
70 path to it. An application is looked for with or without the '.exe' suffix.
71 If the executable cannot be found, None is returned
73 if os.path.isabs(executable):
74 if not os.path.isfile(executable):
75 if executable.endswith(
".exe"):
76 if os.path.isfile(executable[:-4]):
77 return executable[:-4]
79 executable = os.path.split(executable)[1]
82 for d
in os.environ.get(
"PATH").split(os.pathsep):
83 fullpath = os.path.join(d, executable)
84 if os.path.isfile(fullpath):
86 elif executable.endswith(
".exe")
and os.path.isfile(fullpath[:-4]):
93 Return the platform Id defined in CMTCONFIG or SCRAM_ARCH.
97 if "BINARY_TAG" in os.environ:
98 arch = os.environ[
"BINARY_TAG"]
99 elif "CMTCONFIG" in os.environ:
100 arch = os.environ[
"CMTCONFIG"]
101 elif "SCRAM_ARCH" in os.environ:
102 arch = os.environ[
"SCRAM_ARCH"]
103 elif os.environ.get(
"ENV_CMAKE_BUILD_TYPE",
"")
in (
110 elif os.environ.get(
"ENV_CMAKE_BUILD_TYPE",
"")
in (
125 def platform_split(p):
126 return set(re.split(
r"[-+]", p))
if p
else set()
129 dirname, basename = os.path.split(reference)
133 for suffix
in (
".yaml",
".yml"):
134 if basename.endswith(suffix):
135 prefix = f
"{basename[:-(len(suffix))]}."
139 prefix = f
"{basename}."
142 flags_slice = slice(len(prefix), -len(suffix)
if suffix
else None)
146 Extract the platform flags from a filename, return None if name does not match prefix and suffix
148 if name.startswith(prefix)
and name.endswith(suffix):
149 return platform_split(name[flags_slice])
153 if "do0" in platform:
158 (get_flags(name), name)
159 for name
in (os.listdir(dirname)
if os.path.isdir(dirname)
else [])
161 if flags
and platform.issuperset(flags)
167 return os.path.join(dirname, candidates[-1][1])
168 return os.path.join(dirname, basename)
171def filter_dict(d: Dict[str, Any], ignore_re: re.Pattern) -> Dict[str, Any]:
173 Recursively filter out keys from the dictionary that match the ignore pattern.
176 for k, v
in d.items():
177 if not ignore_re.match(k):
178 if isinstance(v, dict):
185def compare_dicts(d1: Dict[str, Any], d2: Dict[str, Any], ignore_re: str =
None) -> str:
187 Compare two dictionaries and return the diff as a string, ignoring keys that match the regex.
189 ignore_re = re.compile(ignore_re)
193 return "\n" +
"\n".join(
194 difflib.unified_diff(
195 pprint.pformat(filtered_d1).splitlines(),
196 pprint.pformat(filtered_d2).splitlines(),
202h_count_re = re.compile(
203 r"^(.*)(?:SUCCESS|INFO)\s+Booked (\d+) Histogram\(s\) :\s+([\s\w=-]*)"
209 Parse the TTree summary table in lines, starting from pos.
210 Returns a tuple with the dictionary with the digested informations and the
211 position of the first line after the summary.
218 return [f.strip()
for f
in l.strip(
"*\n").split(
":", 2)]
223 cols = splitcols(ll[0])
227 r[
"Name"], r[
"Title"] = cols[1:]
232 r[
"Title"] = ll[1].strip(
"*\n").split(
"|")[1].strip()
236 cols = splitcols(ll[1 + delta_i])
237 r[
"Entries"] = int(cols[1])
239 sizes = cols[2].split()
240 r[
"Total size"] = int(sizes[2])
241 if sizes[-1] ==
"memory":
244 r[
"File size"] = int(sizes[-1])
246 cols = splitcols(ll[2 + delta_i])
247 sizes = cols[2].split()
248 if cols[0] ==
"Baskets":
249 r[
"Baskets"] = int(cols[1])
250 r[
"Basket size"] = int(sizes[2])
251 r[
"Compression"] = float(sizes[-1])
255 def nextblock(lines, i):
257 dots = re.compile(
r"^\.+$")
258 stars = re.compile(
r"^\*+$")
262 and not dots.match(lines[i + delta_i][1:-1])
263 and not stars.match(lines[i + delta_i])
268 if i < (count - 3)
and lines[i].startswith(
"*Tree"):
269 i_nextblock = nextblock(lines, i)
270 result = parseblock(lines[i:i_nextblock])
271 result[
"Branches"] = {}
273 while i < (count - 3)
and lines[i].startswith(
"*Br"):
274 if i < (count - 2)
and lines[i].startswith(
"*Branch "):
278 i_nextblock = nextblock(lines, i)
279 if i_nextblock >= count:
281 branch = parseblock(lines[i:i_nextblock])
282 result[
"Branches"][branch[
"Name"]] = branch
290 Extract the histograms infos from the lines starting at pos.
291 Returns the position of the first line after the summary block.
294 h_table_head = re.compile(
295 r'(?:SUCCESS|INFO)\s+(1D|2D|3D|1D profile|2D profile) histograms in directory\s+"(\w*)"'
297 h_short_summ = re.compile(
r"ID=([^\"]+)\s+\"([^\"]*)\"\s+(.*)")
302 m = h_count_re.search(lines[pos])
303 name = m.group(1).strip()
304 total = int(m.group(2))
306 for k, v
in [x.split(
"=")
for x
in m.group(3).split()]:
309 header[
"Total"] = total
313 m = h_table_head.search(lines[pos])
316 t = t.replace(
" profile",
"Prof")
323 if l.startswith(
" | ID"):
325 titles = [x.strip()
for x
in l.split(
"|")][1:-1]
327 while pos < nlines
and lines[pos].startswith(
" |"):
329 values = [x.strip()
for x
in l.split(
"|")][1:]
331 for i
in range(len(titles)):
332 hcont[titles[i]] = values[i]
333 cont[hcont[
"ID"]] = hcont
335 elif l.startswith(
" ID="):
336 while pos < nlines
and lines[pos].startswith(
" ID="):
338 x.strip()
for x
in h_short_summ.search(lines[pos]).groups()
340 cont[values[0]] = values
343 raise RuntimeError(
"Cannot understand line %d: '%s'" % (pos, l))
347 summ[d][
"header"] = header
352 summ[name] = {
"header": header}
358 Scan stdout to find ROOT Histogram summaries and digest them.
360 outlines = stdout.splitlines()
if hasattr(stdout,
"splitlines")
else stdout
361 nlines = len(outlines) - 1
369 match = h_count_re.search(outlines[pos])
370 while pos < nlines
and not match:
372 match = h_count_re.search(outlines[pos])
375 summaries.update(summ)
381 Scan stdout to find ROOT TTree summaries and digest them.
383 stars = re.compile(
r"^\*+$")
384 outlines = stdout.splitlines()
if hasattr(stdout,
"splitlines")
else stdout
385 nlines = len(outlines)
391 while i < nlines
and not stars.match(outlines[i]):
396 trees[tree[
"Name"]] = tree
402 return Path(sys.modules[cls.__module__].__file__)
406 """Format a path list as a bracket-notation string for error messages."""
409 return "".join(f
"[{p!r}]" for p
in path)
413 """Check if two floats are close within relative and absolute tolerance."""
419 if diff == float(
"inf"):
421 return diff <= atol + rtol * max(abs(a), abs(b))
430 max_differences: int,
431 differences: List[str],
434 Recursively compare two objects and append difference descriptions to the list.
436 Handles dicts, lists, floats (with tolerance), and other types (exact equality).
437 Stops early if max_differences is reached.
439 if len(differences) >= max_differences:
442 if type(obj1)
is not type(obj2):
444 f
"Type mismatch at {_format_path(path)}: {type(obj1).__name__} vs {type(obj2).__name__}"
446 elif isinstance(obj1, dict):
447 keys1, keys2 = set(obj1.keys()), set(obj2.keys())
448 for key
in keys1 - keys2:
449 if len(differences) >= max_differences:
452 f
"Extra key in first object at {_format_path(path)}: {key!r}"
454 for key
in keys2 - keys1:
455 if len(differences) >= max_differences:
458 f
"Extra key in second object at {_format_path(path)}: {key!r}"
460 for key
in keys1 & keys2:
461 if len(differences) >= max_differences:
472 elif isinstance(obj1, list):
473 if len(obj1) != len(obj2):
475 f
"List length mismatch at {_format_path(path)}: {len(obj1)} vs {len(obj2)}"
478 for i, (item1, item2)
in enumerate(zip(obj1, obj2)):
479 if len(differences) >= max_differences:
482 item1, item2, path + [i], rtol, atol, max_differences, differences
484 elif isinstance(obj1, float):
487 f
"Float mismatch at {_format_path(path)}: {obj1} vs {obj2} "
488 f
"(diff={abs(obj1 - obj2)}, rtol={rtol}, atol={atol})"
492 f
"Value mismatch at {_format_path(path)}: {obj1!r} vs {obj2!r}"
501 max_differences: int = 10,
504 Assert that two JSON-like objects are equal, with tolerance for floating-point values.
507 obj1: First object to compare (can be dict, list, or primitive types)
508 obj2: Second object to compare
509 rtol: Relative tolerance for float comparisons (default: 1e-9)
510 atol: Absolute tolerance for float comparisons (default: 0.0)
511 max_differences: Maximum number of differences to report before stopping (default: 10)
514 AssertionError: If the objects are not equal, with a detailed message
515 showing differences found (up to max_differences).
518 >>> assert_objects_equal({"a": 1.0}, {"a": 1.0}) # passes
519 >>> assert_objects_equal({"a": 1.0}, {"a": 1.001}, atol=0.01) # passes
520 >>> assert_objects_equal({"a": 1}, {"a": 2}) # raises AssertionError
523 __tracebackhide__ =
True
525 differences: List[str] = []
526 _compare(obj1, obj2, [], rtol, atol, max_differences, differences)
528 truncated = len(differences) >= max_differences
529 msg = f
"Objects differ ({len(differences)} difference(s) found"
531 msg +=
", output truncated"
533 msg +=
"\n".join(f
" - {d}" for d
in differences)
None __init__(self, code, language)
str compare_dicts(Dict[str, Any] d1, Dict[str, Any] d2, str ignore_re=None)
bool _floats_close(float a, float b, float rtol, float atol)
find_ttree_summaries(stdout)
expand_reference_file_name(reference)
Dict[str, Any] filter_dict(Dict[str, Any] d, re.Pattern ignore_re)
_parse_histos_summary(lines, pos)
_parse_ttree_summary(lines, pos)
find_histos_summaries(stdout)
None assert_objects_equal(Any obj1, Any obj2, float rtol=1e-9, float atol=0.0, int max_differences=10)
platform_matches(List[str] unsupported_platforms)
str _format_path(List[str] path)
None _compare(Any obj1, Any obj2, List[str] path, float rtol, float atol, int max_differences, List[str] differences)
str_representer(dumper, data)