The Gaudi Framework
master (bb4415cc)
Toggle main menu visibility
Loading...
Searching...
No Matches
utils.py
Go to the documentation of this file.
1
11
import
difflib
12
import
os
13
import
pprint
14
import
re
15
import
shutil
16
import
sys
17
import
xml.sax.saxutils
as
XSS
18
from
pathlib
import
Path
19
from
subprocess
import
PIPE, Popen
20
from
typing
import
Any, Dict, List
21
22
23
class
CodeWrapper
:
24
def
__init__
(self, code, language) -> None:
25
self.
code
= code
26
self.
language
= language
27
28
def
__str__
(self) -> str:
29
return
f
'<pre><code class="language-{self.language}">{XSS.escape(self.code)}</code></pre>'
30
31
32
def
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
38
def
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
44
def
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
67
def
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
91
def
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
119
def
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
171
def
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
185
def
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
202
h_count_re = re.compile(
203
r"^(.*)(?:SUCCESS|INFO)\s+Booked (\d+) Histogram\(s\) :\s+([\s\w=-]*)"
204
)
205
206
207
def
_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
288
def
_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
356
def
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
379
def
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
401
def
file_path_for_class
(cls):
402
return
Path(sys.modules[cls.__module__].__file__)
403
404
405
def
_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
412
def
_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
424
def
_compare
(
425
obj1: Any,
426
obj2: Any,
427
path: List[str],
428
rtol: float,
429
atol: float,
430
max_differences: int,
431
differences: List[str],
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
496
def
assert_objects_equal
(
497
obj1: Any,
498
obj2: Any,
499
rtol: float = 1e-9,
500
atol: float = 0.0,
501
max_differences: int = 10,
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
GaudiTesting.utils.CodeWrapper
Definition
utils.py:23
GaudiTesting.utils.CodeWrapper.language
language
Definition
utils.py:26
GaudiTesting.utils.CodeWrapper.__str__
str __str__(self)
Definition
utils.py:28
GaudiTesting.utils.CodeWrapper.code
code
Definition
utils.py:25
GaudiTesting.utils.CodeWrapper.__init__
None __init__(self, code, language)
Definition
utils.py:24
GaudiTesting.utils._parse_ttree_summary
_parse_ttree_summary(lines, pos)
Definition
utils.py:207
GaudiTesting.utils.compare_dicts
str compare_dicts(Dict[str, Any] d1, Dict[str, Any] d2, str ignore_re=None)
Definition
utils.py:185
GaudiTesting.utils.platform_matches
platform_matches(List[str] unsupported_platforms)
Definition
utils.py:32
GaudiTesting.utils._format_path
str _format_path(List[str] path)
Definition
utils.py:405
GaudiTesting.utils._parse_histos_summary
_parse_histos_summary(lines, pos)
Definition
utils.py:288
GaudiTesting.utils.kill_tree
kill_tree(ppid, sig)
Definition
utils.py:44
GaudiTesting.utils.find_histos_summaries
find_histos_summaries(stdout)
Definition
utils.py:356
GaudiTesting.utils._floats_close
bool _floats_close(float a, float b, float rtol, float atol)
Definition
utils.py:412
GaudiTesting.utils.str_representer
str_representer(dumper, data)
Definition
utils.py:38
GaudiTesting.utils.which
which(executable)
Definition
utils.py:67
GaudiTesting.utils.expand_reference_file_name
expand_reference_file_name(reference)
Definition
utils.py:119
GaudiTesting.utils.assert_objects_equal
None assert_objects_equal(Any obj1, Any obj2, float rtol=1e-9, float atol=0.0, int max_differences=10)
Definition
utils.py:502
GaudiTesting.utils.file_path_for_class
file_path_for_class(cls)
Definition
utils.py:401
GaudiTesting.utils.filter_dict
Dict[str, Any] filter_dict(Dict[str, Any] d, re.Pattern ignore_re)
Definition
utils.py:171
GaudiTesting.utils.find_ttree_summaries
find_ttree_summaries(stdout)
Definition
utils.py:379
GaudiTesting.utils.get_platform
get_platform()
Definition
utils.py:91
GaudiTesting.utils._compare
None _compare(Any obj1, Any obj2, List[str] path, float rtol, float atol, int max_differences, List[str] differences)
Definition
utils.py:432
GaudiPolicy
python
GaudiTesting
utils.py
Generated on
for The Gaudi Framework by
1.17.0