The Gaudi Framework  master (cc9a61f4)
Loading...
Searching...
No Matches
GaudiTesting.utils Namespace Reference

Classes

class  CodeWrapper

Functions

 platform_matches (List[str] unsupported_platforms)
 str_representer (dumper, data)
 kill_tree (ppid, sig)
 which (executable)
 get_platform ()
 expand_reference_file_name (reference)
Dict[str, Any] filter_dict (Dict[str, Any] d, re.Pattern ignore_re)
str compare_dicts (Dict[str, Any] d1, Dict[str, Any] d2, str ignore_re=None)
 _parse_ttree_summary (lines, pos)
 _parse_histos_summary (lines, pos)
 find_histos_summaries (stdout)
 find_ttree_summaries (stdout)
 file_path_for_class (cls)
str _format_path (List[str] path)
bool _floats_close (float a, float b, float rtol, float atol)
None _compare (Any obj1, Any obj2, List[str] path, float rtol, float atol, int max_differences, List[str] differences)
None assert_objects_equal (Any obj1, Any obj2, float rtol=1e-9, float atol=0.0, int max_differences=10)

Variables

 h_count_re

Function Documentation

◆ _compare()

None GaudiTesting.utils._compare ( Any obj1,
Any obj2,
List[str] path,
float rtol,
float atol,
int max_differences,
List[str] differences )
protected
Recursively compare two objects and append difference descriptions to the list.

Handles dicts, lists, floats (with tolerance), and other types (exact equality).
Stops early if max_differences is reached.

Definition at line 424 of file utils.py.

432) -> None:
433 """
434 Recursively compare two objects and append difference descriptions to the list.
435
436 Handles dicts, lists, floats (with tolerance), and other types (exact equality).
437 Stops early if max_differences is reached.
438 """
439 if len(differences) >= max_differences:
440 return
441
442 if type(obj1) is not type(obj2):
443 differences.append(
444 f"Type mismatch at {_format_path(path)}: {type(obj1).__name__} vs {type(obj2).__name__}"
445 )
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:
450 return
451 differences.append(
452 f"Extra key in first object at {_format_path(path)}: {key!r}"
453 )
454 for key in keys2 - keys1:
455 if len(differences) >= max_differences:
456 return
457 differences.append(
458 f"Extra key in second object at {_format_path(path)}: {key!r}"
459 )
460 for key in keys1 & keys2:
461 if len(differences) >= max_differences:
462 return
463 _compare(
464 obj1[key],
465 obj2[key],
466 path + [key],
467 rtol,
468 atol,
469 max_differences,
470 differences,
471 )
472 elif isinstance(obj1, list):
473 if len(obj1) != len(obj2):
474 differences.append(
475 f"List length mismatch at {_format_path(path)}: {len(obj1)} vs {len(obj2)}"
476 )
477 else:
478 for i, (item1, item2) in enumerate(zip(obj1, obj2)):
479 if len(differences) >= max_differences:
480 return
481 _compare(
482 item1, item2, path + [i], rtol, atol, max_differences, differences
483 )
484 elif isinstance(obj1, float):
485 if not _floats_close(obj1, obj2, rtol, atol):
486 differences.append(
487 f"Float mismatch at {_format_path(path)}: {obj1} vs {obj2} "
488 f"(diff={abs(obj1 - obj2)}, rtol={rtol}, atol={atol})"
489 )
490 elif obj1 != obj2:
491 differences.append(
492 f"Value mismatch at {_format_path(path)}: {obj1!r} vs {obj2!r}"
493 )
494
495

◆ _floats_close()

bool GaudiTesting.utils._floats_close ( float a,
float b,
float rtol,
float atol )
protected
Check if two floats are close within relative and absolute tolerance.

Definition at line 412 of file utils.py.

412def _floats_close(a: float, b: float, rtol: float, atol: float) -> bool:
413 """Check if two floats are close within relative and absolute tolerance."""
414 # Handle exact equality first (includes inf == inf case)
415 if a == b:
416 return True
417 # Handle cases where difference is infinite (e.g., inf vs -inf)
418 diff = abs(a - b)
419 if diff == float("inf"):
420 return False
421 return diff <= atol + rtol * max(abs(a), abs(b))
422
423

◆ _format_path()

str GaudiTesting.utils._format_path ( List[str] path)
protected
Format a path list as a bracket-notation string for error messages.

Definition at line 405 of file utils.py.

405def _format_path(path: List[str]) -> str:
406 """Format a path list as a bracket-notation string for error messages."""
407 if not path:
408 return "root"
409 return "".join(f"[{p!r}]" for p in path)
410
411

◆ _parse_histos_summary()

GaudiTesting.utils._parse_histos_summary ( lines,
pos )
protected
Extract the histograms infos from the lines starting at pos.
Returns the position of the first line after the summary block.

Definition at line 288 of file utils.py.

288def _parse_histos_summary(lines, pos):
289 """
290 Extract the histograms infos from the lines starting at pos.
291 Returns the position of the first line after the summary block.
292 """
293 global h_count_re
294 h_table_head = re.compile(
295 r'(?:SUCCESS|INFO)\s+(1D|2D|3D|1D profile|2D profile) histograms in directory\s+"(\w*)"'
296 )
297 h_short_summ = re.compile(r"ID=([^\"]+)\s+\"([^\"]*)\"\s+(.*)")
298
299 nlines = len(lines)
300
301 # decode header
302 m = h_count_re.search(lines[pos])
303 name = m.group(1).strip()
304 total = int(m.group(2))
305 header = {}
306 for k, v in [x.split("=") for x in m.group(3).split()]:
307 header[k] = int(v)
308 pos += 1
309 header["Total"] = total
310
311 summ = {}
312 while pos < nlines:
313 m = h_table_head.search(lines[pos])
314 if m:
315 t, d = m.groups(1) # type and directory
316 t = t.replace(" profile", "Prof")
317 pos += 1
318 if pos < nlines:
319 l = lines[pos]
320 else:
321 l = ""
322 cont = {}
323 if l.startswith(" | ID"):
324 # table format
325 titles = [x.strip() for x in l.split("|")][1:-1]
326 pos += 1
327 while pos < nlines and lines[pos].startswith(" |"):
328 l = lines[pos]
329 values = [x.strip() for x in l.split("|")][1:]
330 hcont = {}
331 for i in range(len(titles)):
332 hcont[titles[i]] = values[i]
333 cont[hcont["ID"]] = hcont
334 pos += 1
335 elif l.startswith(" ID="):
336 while pos < nlines and lines[pos].startswith(" ID="):
337 values = [
338 x.strip() for x in h_short_summ.search(lines[pos]).groups()
339 ]
340 cont[values[0]] = values
341 pos += 1
342 else: # not interpreted
343 raise RuntimeError("Cannot understand line %d: '%s'" % (pos, l))
344 if d not in summ:
345 summ[d] = {}
346 summ[d][t] = cont
347 summ[d]["header"] = header
348 else:
349 break
350 if not summ:
351 # If the full table is not present, we use only the header
352 summ[name] = {"header": header}
353 return summ, pos
354
355

◆ _parse_ttree_summary()

GaudiTesting.utils._parse_ttree_summary ( lines,
pos )
protected
Parse the TTree summary table in lines, starting from pos.
Returns a tuple with the dictionary with the digested informations and the
position of the first line after the summary.

Definition at line 207 of file utils.py.

207def _parse_ttree_summary(lines, pos):
208 """
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.
212 """
213 result = {}
214 i = pos + 1 # first line is a sequence of '*'
215 count = len(lines)
216
217 def splitcols(l):
218 return [f.strip() for f in l.strip("*\n").split(":", 2)]
219
220 def parseblock(ll):
221 r = {}
222 delta_i = 0
223 cols = splitcols(ll[0])
224
225 if len(ll) == 3:
226 # default one line name/title
227 r["Name"], r["Title"] = cols[1:]
228 elif len(ll) == 4:
229 # in case title is moved to next line due to too long name
230 delta_i = 1
231 r["Name"] = cols[1]
232 r["Title"] = ll[1].strip("*\n").split("|")[1].strip()
233 else:
234 assert False
235
236 cols = splitcols(ll[1 + delta_i])
237 r["Entries"] = int(cols[1])
238
239 sizes = cols[2].split()
240 r["Total size"] = int(sizes[2])
241 if sizes[-1] == "memory":
242 r["File size"] = 0
243 else:
244 r["File size"] = int(sizes[-1])
245
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])
252
253 return r
254
255 def nextblock(lines, i):
256 delta_i = 1
257 dots = re.compile(r"^\.+$")
258 stars = re.compile(r"^\*+$")
259 count = len(lines)
260 while (
261 i + delta_i < count
262 and not dots.match(lines[i + delta_i][1:-1])
263 and not stars.match(lines[i + delta_i])
264 ):
265 delta_i += 1
266 return i + delta_i
267
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"] = {}
272 i = i_nextblock + 1
273 while i < (count - 3) and lines[i].startswith("*Br"):
274 if i < (count - 2) and lines[i].startswith("*Branch "):
275 # skip branch header
276 i += 3
277 continue
278 i_nextblock = nextblock(lines, i)
279 if i_nextblock >= count:
280 break
281 branch = parseblock(lines[i:i_nextblock])
282 result["Branches"][branch["Name"]] = branch
283 i = i_nextblock + 1
284
285 return (result, i)
286
287

◆ assert_objects_equal()

None GaudiTesting.utils.assert_objects_equal ( Any obj1,
Any obj2,
float rtol = 1e-9,
float atol = 0.0,
int max_differences = 10 )
Assert that two JSON-like objects are equal, with tolerance for floating-point values.

Args:
    obj1: First object to compare (can be dict, list, or primitive types)
    obj2: Second object to compare
    rtol: Relative tolerance for float comparisons (default: 1e-9)
    atol: Absolute tolerance for float comparisons (default: 0.0)
    max_differences: Maximum number of differences to report before stopping (default: 10)

Raises:
    AssertionError: If the objects are not equal, with a detailed message
                   showing differences found (up to max_differences).

Example:
    >>> assert_objects_equal({"a": 1.0}, {"a": 1.0})  # passes
    >>> assert_objects_equal({"a": 1.0}, {"a": 1.001}, atol=0.01)  # passes
    >>> assert_objects_equal({"a": 1}, {"a": 2})  # raises AssertionError

Definition at line 496 of file utils.py.

502) -> None:
503 """
504 Assert that two JSON-like objects are equal, with tolerance for floating-point values.
505
506 Args:
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)
512
513 Raises:
514 AssertionError: If the objects are not equal, with a detailed message
515 showing differences found (up to max_differences).
516
517 Example:
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
521 """
522 # pytest will hide this frame from the reported traceback
523 __tracebackhide__ = True
524
525 differences: List[str] = []
526 _compare(obj1, obj2, [], rtol, atol, max_differences, differences)
527 if differences:
528 truncated = len(differences) >= max_differences
529 msg = f"Objects differ ({len(differences)} difference(s) found"
530 if truncated:
531 msg += ", output truncated"
532 msg += "):\n"
533 msg += "\n".join(f" - {d}" for d in differences)
534 assert False, msg

◆ compare_dicts()

str GaudiTesting.utils.compare_dicts ( Dict[str, Any] d1,
Dict[str, Any] d2,
str ignore_re = None )
Compare two dictionaries and return the diff as a string, ignoring keys that match the regex.

Definition at line 185 of file utils.py.

185def compare_dicts(d1: Dict[str, Any], d2: Dict[str, Any], ignore_re: str = None) -> str:
186 """
187 Compare two dictionaries and return the diff as a string, ignoring keys that match the regex.
188 """
189 ignore_re = re.compile(ignore_re)
190 filtered_d1 = filter_dict(d1, ignore_re)
191 filtered_d2 = filter_dict(d2, ignore_re)
192
193 return "\n" + "\n".join(
194 difflib.unified_diff(
195 pprint.pformat(filtered_d1).splitlines(),
196 pprint.pformat(filtered_d2).splitlines(),
197 )
198 )
199
200
201# signature of the print-out of the histograms

◆ expand_reference_file_name()

GaudiTesting.utils.expand_reference_file_name ( reference)

Definition at line 119 of file utils.py.

119def expand_reference_file_name(reference):
120 # if no file is passed, do nothing
121 if not reference:
122 return reference
123
124 # function to split an extension in constituents parts
125 def platform_split(p):
126 return set(re.split(r"[-+]", p)) if p else set()
127
128 # get all the files whose name start with the reference filename
129 dirname, basename = os.path.split(reference)
130 if not dirname:
131 dirname = "."
132
133 for suffix in (".yaml", ".yml"):
134 if basename.endswith(suffix):
135 prefix = f"{basename[:-(len(suffix))]}."
136 break
137 else:
138 # no special suffix matched, fallback on no suffix
139 prefix = f"{basename}."
140 suffix = ""
141
142 flags_slice = slice(len(prefix), -len(suffix) if suffix else None)
143
144 def get_flags(name):
145 """
146 Extract the platform flags from a filename, return None if name does not match prefix and suffix
147 """
148 if name.startswith(prefix) and name.endswith(suffix):
149 return platform_split(name[flags_slice])
150 return None
151
152 platform = platform_split(get_platform())
153 if "do0" in platform:
154 platform.add("dbg")
155 candidates = [
156 (len(flags), name)
157 for flags, name in [
158 (get_flags(name), name)
159 for name in (os.listdir(dirname) if os.path.isdir(dirname) else [])
160 ]
161 if flags and platform.issuperset(flags)
162 ]
163 if candidates: # take the one with highest matching
164 # FIXME: it is not possible to say if x86_64-slc5-gcc43-dbg
165 # has to use yaml.x86_64-gcc43 or yaml.slc5-dbg
166 candidates.sort()
167 return os.path.join(dirname, candidates[-1][1])
168 return os.path.join(dirname, basename)
169
170

◆ file_path_for_class()

GaudiTesting.utils.file_path_for_class ( cls)

Definition at line 401 of file utils.py.

401def file_path_for_class(cls):
402 return Path(sys.modules[cls.__module__].__file__)
403
404

◆ filter_dict()

Dict[str, Any] GaudiTesting.utils.filter_dict ( Dict[str, Any] d,
re.Pattern ignore_re )
Recursively filter out keys from the dictionary that match the ignore pattern.

Definition at line 171 of file utils.py.

171def filter_dict(d: Dict[str, Any], ignore_re: re.Pattern) -> Dict[str, Any]:
172 """
173 Recursively filter out keys from the dictionary that match the ignore pattern.
174 """
175 filteredDict = {}
176 for k, v in d.items():
177 if not ignore_re.match(k):
178 if isinstance(v, dict):
179 filteredDict[k] = filter_dict(v, ignore_re)
180 else:
181 filteredDict[k] = v
182 return filteredDict
183
184

◆ find_histos_summaries()

GaudiTesting.utils.find_histos_summaries ( stdout)
Scan stdout to find ROOT Histogram summaries and digest them.

Definition at line 356 of file utils.py.

356def find_histos_summaries(stdout):
357 """
358 Scan stdout to find ROOT Histogram summaries and digest them.
359 """
360 outlines = stdout.splitlines() if hasattr(stdout, "splitlines") else stdout
361 nlines = len(outlines) - 1
362 summaries = {}
363 global h_count_re
364
365 pos = 0
366 while pos < nlines:
367 summ = {}
368 # find first line of block:
369 match = h_count_re.search(outlines[pos])
370 while pos < nlines and not match:
371 pos += 1
372 match = h_count_re.search(outlines[pos])
373 if match:
374 summ, pos = _parse_histos_summary(outlines, pos)
375 summaries.update(summ)
376 return summaries
377
378

◆ find_ttree_summaries()

GaudiTesting.utils.find_ttree_summaries ( stdout)
Scan stdout to find ROOT TTree summaries and digest them.

Definition at line 379 of file utils.py.

379def find_ttree_summaries(stdout):
380 """
381 Scan stdout to find ROOT TTree summaries and digest them.
382 """
383 stars = re.compile(r"^\*+$")
384 outlines = stdout.splitlines() if hasattr(stdout, "splitlines") else stdout
385 nlines = len(outlines)
386 trees = {}
387
388 i = 0
389 while i < nlines: # loop over the output
390 # look for
391 while i < nlines and not stars.match(outlines[i]):
392 i += 1
393 if i < nlines:
394 tree, i = _parse_ttree_summary(outlines, i)
395 if tree:
396 trees[tree["Name"]] = tree
397
398 return trees
399
400

◆ get_platform()

GaudiTesting.utils.get_platform ( )
Return the platform Id defined in CMTCONFIG or SCRAM_ARCH.

Definition at line 91 of file utils.py.

91def get_platform():
92 """
93 Return the platform Id defined in CMTCONFIG or SCRAM_ARCH.
94 """
95 arch = "None"
96 # check architecture name
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 (
104 "Debug", # -O0 -g
105 "FastDebug", # -Og -g (LHCb only)
106 "Developer", # same as Debug, but with many warnings enabled
107 "", # no options (equivalent to -O0)
108 ):
109 arch = "unknown-dbg"
110 elif os.environ.get("ENV_CMAKE_BUILD_TYPE", "") in (
111 "Release", # -O3 -DNDEBUG
112 "MinSizeRel", # -Os -DNDEBUG
113 "RelWithDebInfo", # -O2 -g -DNDEBUG (-O3 for LHCb)
114 ):
115 arch = "unknown-opt"
116 return arch
117
118

◆ kill_tree()

GaudiTesting.utils.kill_tree ( ppid,
sig )
Send a signal to a process and all its child processes (starting from the
leaves).

Definition at line 44 of file utils.py.

44def kill_tree(ppid, sig):
45 """
46 Send a signal to a process and all its child processes (starting from the
47 leaves).
48 """
49 # Resolve ps via the parent PATH: kill_tree runs it with an empty
50 # environment (clean env workaround below), which also empties PATH and
51 # makes a bare "ps" unresolvable where it is not on a loader default path
52 ps_exe = shutil.which("ps") or "ps"
53 ps_cmd = [ps_exe, "--no-headers", "-o", "pid", "--ppid", str(ppid)]
54 # Note: start in a clean env to avoid a freeze with libasan.so
55 # See https://sourceware.org/bugzilla/show_bug.cgi?id=27653
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:
59 kill_tree(child, sig)
60 try:
61 os.kill(ppid, sig)
62 except OSError as err:
63 if err.errno != 3: # No such process
64 raise
65
66

◆ platform_matches()

GaudiTesting.utils.platform_matches ( List[str] unsupported_platforms)

Definition at line 32 of file utils.py.

32def platform_matches(unsupported_platforms: List[str]):
33 platform_id = get_platform()
34 return any(re.search(p, platform_id) for p in unsupported_platforms)
35
36
37# merci https://stackoverflow.com/a/33300001

◆ str_representer()

GaudiTesting.utils.str_representer ( dumper,
data )

Definition at line 38 of file utils.py.

38def str_representer(dumper, data):
39 if "\n" in data:
40 return dumper.represent_scalar("tag:yaml.org,2002:str", data, style="|")
41 return dumper.represent_scalar("tag:yaml.org,2002:str", data)
42
43

◆ which()

GaudiTesting.utils.which ( executable)
Locates an executable in the executables path ($PATH) and returns the full
path to it.  An application is looked for with or without the '.exe' suffix.
If the executable cannot be found, None is returned

Definition at line 67 of file utils.py.

67def which(executable):
68 """
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
72 """
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]
78 else:
79 executable = os.path.split(executable)[1]
80 else:
81 return executable
82 for d in os.environ.get("PATH").split(os.pathsep):
83 fullpath = os.path.join(d, executable)
84 if os.path.isfile(fullpath):
85 return fullpath
86 elif executable.endswith(".exe") and os.path.isfile(fullpath[:-4]):
87 return fullpath[:-4]
88 return None
89
90

Variable Documentation

◆ h_count_re

GaudiTesting.utils.h_count_re
Initial value:
1= re.compile(
2 r"^(.*)(?:SUCCESS|INFO)\s+Booked (\d+) Histogram\‍(s\‍) :\s+([\s\w=-]*)"
3)

Definition at line 202 of file utils.py.