The Gaudi Framework  master (37c0b60a)
utils.py
Go to the documentation of this file.
1 
11 import difflib
12 import os
13 import pprint
14 import re
15 import sys
16 import xml.sax.saxutils as XSS
17 from pathlib import Path
18 from subprocess import PIPE, Popen
19 from typing import Any, Dict, List
20 
21 
23  def __init__(self, code, language) -> None:
24  self.code = code
25  self.language = language
26 
27  def __str__(self) -> str:
28  return f'<pre><code class="language-{self.language}">{XSS.escape(self.code)}</code></pre>'
29 
30 
31 def platform_matches(unsupported_platforms: List[str]):
32  platform_id = get_platform()
33  return any(re.search(p, platform_id) for p in unsupported_platforms)
34 
35 
36 # merci https://stackoverflow.com/a/33300001
37 def str_representer(dumper, data):
38  if "\n" in data:
39  return dumper.represent_scalar("tag:yaml.org,2002:str", data, style="|")
40  return dumper.represent_scalar("tag:yaml.org,2002:str", data)
41 
42 
43 def kill_tree(ppid, sig):
44  """
45  Send a signal to a process and all its child processes (starting from the
46  leaves).
47  """
48  ps_cmd = ["ps", "--no-headers", "-o", "pid", "--ppid", str(ppid)]
49  # Note: start in a clean env to avoid a freeze with libasan.so
50  # See https://sourceware.org/bugzilla/show_bug.cgi?id=27653
51  get_children = Popen(ps_cmd, stdout=PIPE, stderr=PIPE, env={})
52  children = map(int, get_children.communicate()[0].split())
53  for child in children:
54  kill_tree(child, sig)
55  try:
56  os.kill(ppid, sig)
57  except OSError as err:
58  if err.errno != 3: # No such process
59  raise
60 
61 
62 def which(executable):
63  """
64  Locates an executable in the executables path ($PATH) and returns the full
65  path to it. An application is looked for with or without the '.exe' suffix.
66  If the executable cannot be found, None is returned
67  """
68  if os.path.isabs(executable):
69  if not os.path.isfile(executable):
70  if executable.endswith(".exe"):
71  if os.path.isfile(executable[:-4]):
72  return executable[:-4]
73  else:
74  executable = os.path.split(executable)[1]
75  else:
76  return executable
77  for d in os.environ.get("PATH").split(os.pathsep):
78  fullpath = os.path.join(d, executable)
79  if os.path.isfile(fullpath):
80  return fullpath
81  elif executable.endswith(".exe") and os.path.isfile(fullpath[:-4]):
82  return fullpath[:-4]
83  return None
84 
85 
87  """
88  Return the platform Id defined in CMTCONFIG or SCRAM_ARCH.
89  """
90  arch = "None"
91  # check architecture name
92  if "BINARY_TAG" in os.environ:
93  arch = os.environ["BINARY_TAG"]
94  elif "CMTCONFIG" in os.environ:
95  arch = os.environ["CMTCONFIG"]
96  elif "SCRAM_ARCH" in os.environ:
97  arch = os.environ["SCRAM_ARCH"]
98  elif os.environ.get("ENV_CMAKE_BUILD_TYPE", "") in (
99  "Debug", # -O0 -g
100  "FastDebug", # -Og -g (LHCb only)
101  "Developer", # same as Debug, but with many warnings enabled
102  "", # no options (equivalent to -O0)
103  ):
104  arch = "unknown-dbg"
105  elif os.environ.get("ENV_CMAKE_BUILD_TYPE", "") in (
106  "Release", # -O3 -DNDEBUG
107  "MinSizeRel", # -Os -DNDEBUG
108  "RelWithDebInfo", # -O2 -g -DNDEBUG (-O3 for LHCb)
109  ):
110  arch = "unknown-opt"
111  return arch
112 
113 
115  # if no file is passed, do nothing
116  if not reference:
117  return reference
118 
119  # function to split an extension in constituents parts
120  def platform_split(p):
121  return set(re.split(r"[-+]", p)) if p else set()
122 
123  # get all the files whose name start with the reference filename
124  dirname, basename = os.path.split(reference)
125  if not dirname:
126  dirname = "."
127 
128  for suffix in (".yaml", ".yml"):
129  if basename.endswith(suffix):
130  prefix = f"{basename[:-(len(suffix))]}."
131  break
132  else:
133  # no special suffix matched, fallback on no suffix
134  prefix = f"{basename}."
135  suffix = ""
136 
137  flags_slice = slice(len(prefix), -len(suffix) if suffix else None)
138 
139  def get_flags(name):
140  """
141  Extract the platform flags from a filename, return None if name does not match prefix and suffix
142  """
143  if name.startswith(prefix) and name.endswith(suffix):
144  return platform_split(name[flags_slice])
145  return None
146 
147  platform = platform_split(get_platform())
148  if "do0" in platform:
149  platform.add("dbg")
150  candidates = [
151  (len(flags), name)
152  for flags, name in [
153  (get_flags(name), name)
154  for name in (os.listdir(dirname) if os.path.isdir(dirname) else [])
155  ]
156  if flags and platform.issuperset(flags)
157  ]
158  if candidates: # take the one with highest matching
159  # FIXME: it is not possible to say if x86_64-slc5-gcc43-dbg
160  # has to use yaml.x86_64-gcc43 or yaml.slc5-dbg
161  candidates.sort()
162  return os.path.join(dirname, candidates[-1][1])
163  return os.path.join(dirname, basename)
164 
165 
166 def filter_dict(d: Dict[str, Any], ignore_re: re.Pattern) -> Dict[str, Any]:
167  """
168  Recursively filter out keys from the dictionary that match the ignore pattern.
169  """
170  filteredDict = {}
171  for k, v in d.items():
172  if not ignore_re.match(k):
173  if isinstance(v, dict):
174  filteredDict[k] = filter_dict(v, ignore_re)
175  else:
176  filteredDict[k] = v
177  return filteredDict
178 
179 
180 def compare_dicts(d1: Dict[str, Any], d2: Dict[str, Any], ignore_re: str = None) -> str:
181  """
182  Compare two dictionaries and return the diff as a string, ignoring keys that match the regex.
183  """
184  ignore_re = re.compile(ignore_re)
185  filtered_d1 = filter_dict(d1, ignore_re)
186  filtered_d2 = filter_dict(d2, ignore_re)
187 
188  return "\n" + "\n".join(
189  difflib.unified_diff(
190  pprint.pformat(filtered_d1).splitlines(),
191  pprint.pformat(filtered_d2).splitlines(),
192  )
193  )
194 
195 
196 # signature of the print-out of the histograms
197 h_count_re = re.compile(
198  r"^(.*)(?:SUCCESS|INFO)\s+Booked (\d+) Histogram\‍(s\‍) :\s+([\s\w=-]*)"
199 )
200 
201 
202 def _parse_ttree_summary(lines, pos):
203  """
204  Parse the TTree summary table in lines, starting from pos.
205  Returns a tuple with the dictionary with the digested informations and the
206  position of the first line after the summary.
207  """
208  result = {}
209  i = pos + 1 # first line is a sequence of '*'
210  count = len(lines)
211 
212  def splitcols(l):
213  return [f.strip() for f in l.strip("*\n").split(":", 2)]
214 
215  def parseblock(ll):
216  r = {}
217  delta_i = 0
218  cols = splitcols(ll[0])
219 
220  if len(ll) == 3:
221  # default one line name/title
222  r["Name"], r["Title"] = cols[1:]
223  elif len(ll) == 4:
224  # in case title is moved to next line due to too long name
225  delta_i = 1
226  r["Name"] = cols[1]
227  r["Title"] = ll[1].strip("*\n").split("|")[1].strip()
228  else:
229  assert False
230 
231  cols = splitcols(ll[1 + delta_i])
232  r["Entries"] = int(cols[1])
233 
234  sizes = cols[2].split()
235  r["Total size"] = int(sizes[2])
236  if sizes[-1] == "memory":
237  r["File size"] = 0
238  else:
239  r["File size"] = int(sizes[-1])
240 
241  cols = splitcols(ll[2 + delta_i])
242  sizes = cols[2].split()
243  if cols[0] == "Baskets":
244  r["Baskets"] = int(cols[1])
245  r["Basket size"] = int(sizes[2])
246  r["Compression"] = float(sizes[-1])
247 
248  return r
249 
250  def nextblock(lines, i):
251  delta_i = 1
252  dots = re.compile(r"^\.+$")
253  stars = re.compile(r"^\*+$")
254  count = len(lines)
255  while (
256  i + delta_i < count
257  and not dots.match(lines[i + delta_i][1:-1])
258  and not stars.match(lines[i + delta_i])
259  ):
260  delta_i += 1
261  return i + delta_i
262 
263  if i < (count - 3) and lines[i].startswith("*Tree"):
264  i_nextblock = nextblock(lines, i)
265  result = parseblock(lines[i:i_nextblock])
266  result["Branches"] = {}
267  i = i_nextblock + 1
268  while i < (count - 3) and lines[i].startswith("*Br"):
269  if i < (count - 2) and lines[i].startswith("*Branch "):
270  # skip branch header
271  i += 3
272  continue
273  i_nextblock = nextblock(lines, i)
274  if i_nextblock >= count:
275  break
276  branch = parseblock(lines[i:i_nextblock])
277  result["Branches"][branch["Name"]] = branch
278  i = i_nextblock + 1
279 
280  return (result, i)
281 
282 
283 def _parse_histos_summary(lines, pos):
284  """
285  Extract the histograms infos from the lines starting at pos.
286  Returns the position of the first line after the summary block.
287  """
288  global h_count_re
289  h_table_head = re.compile(
290  r'(?:SUCCESS|INFO)\s+(1D|2D|3D|1D profile|2D profile) histograms in directory\s+"(\w*)"'
291  )
292  h_short_summ = re.compile(r"ID=([^\"]+)\s+\"([^\"]*)\"\s+(.*)")
293 
294  nlines = len(lines)
295 
296  # decode header
297  m = h_count_re.search(lines[pos])
298  name = m.group(1).strip()
299  total = int(m.group(2))
300  header = {}
301  for k, v in [x.split("=") for x in m.group(3).split()]:
302  header[k] = int(v)
303  pos += 1
304  header["Total"] = total
305 
306  summ = {}
307  while pos < nlines:
308  m = h_table_head.search(lines[pos])
309  if m:
310  t, d = m.groups(1) # type and directory
311  t = t.replace(" profile", "Prof")
312  pos += 1
313  if pos < nlines:
314  l = lines[pos]
315  else:
316  l = ""
317  cont = {}
318  if l.startswith(" | ID"):
319  # table format
320  titles = [x.strip() for x in l.split("|")][1:]
321  pos += 1
322  while pos < nlines and lines[pos].startswith(" |"):
323  l = lines[pos]
324  values = [x.strip() for x in l.split("|")][1:]
325  hcont = {}
326  for i in range(len(titles)):
327  hcont[titles[i]] = values[i]
328  cont[hcont["ID"]] = hcont
329  pos += 1
330  elif l.startswith(" ID="):
331  while pos < nlines and lines[pos].startswith(" ID="):
332  values = [
333  x.strip() for x in h_short_summ.search(lines[pos]).groups()
334  ]
335  cont[values[0]] = values
336  pos += 1
337  else: # not interpreted
338  raise RuntimeError("Cannot understand line %d: '%s'" % (pos, l))
339  if d not in summ:
340  summ[d] = {}
341  summ[d][t] = cont
342  summ[d]["header"] = header
343  else:
344  break
345  if not summ:
346  # If the full table is not present, we use only the header
347  summ[name] = {"header": header}
348  return summ, pos
349 
350 
352  """
353  Scan stdout to find ROOT Histogram summaries and digest them.
354  """
355  outlines = stdout.splitlines() if hasattr(stdout, "splitlines") else stdout
356  nlines = len(outlines) - 1
357  summaries = {}
358  global h_count_re
359 
360  pos = 0
361  while pos < nlines:
362  summ = {}
363  # find first line of block:
364  match = h_count_re.search(outlines[pos])
365  while pos < nlines and not match:
366  pos += 1
367  match = h_count_re.search(outlines[pos])
368  if match:
369  summ, pos = _parse_histos_summary(outlines, pos)
370  summaries.update(summ)
371  return summaries
372 
373 
375  """
376  Scan stdout to find ROOT TTree summaries and digest them.
377  """
378  stars = re.compile(r"^\*+$")
379  outlines = stdout.splitlines() if hasattr(stdout, "splitlines") else stdout
380  nlines = len(outlines)
381  trees = {}
382 
383  i = 0
384  while i < nlines: # loop over the output
385  # look for
386  while i < nlines and not stars.match(outlines[i]):
387  i += 1
388  if i < nlines:
389  tree, i = _parse_ttree_summary(outlines, i)
390  if tree:
391  trees[tree["Name"]] = tree
392 
393  return trees
394 
395 
397  return Path(sys.modules[cls.__module__].__file__)
ReadAndWriteWhiteBoard.Path
Path
Definition: ReadAndWriteWhiteBoard.py:58
GaudiTesting.utils.str_representer
def str_representer(dumper, data)
Definition: utils.py:37
GaudiTesting.utils.file_path_for_class
def file_path_for_class(cls)
Definition: utils.py:396
GaudiTesting.utils.CodeWrapper.__init__
None __init__(self, code, language)
Definition: utils.py:23
GaudiTesting.utils.find_ttree_summaries
def find_ttree_summaries(stdout)
Definition: utils.py:374
Containers::map
struct GAUDI_API map
Parametrisation class for map-like implementation.
Definition: KeyedObjectManager.h:35
GaudiTesting.utils._parse_ttree_summary
def _parse_ttree_summary(lines, pos)
Definition: utils.py:202
GaudiTesting.utils.get_platform
def get_platform()
Definition: utils.py:86
GaudiTesting.utils.which
def which(executable)
Definition: utils.py:62
GaudiTesting.utils.CodeWrapper.code
code
Definition: utils.py:24
GaudiTesting.utils.CodeWrapper.__str__
str __str__(self)
Definition: utils.py:27
GaudiTesting.utils.platform_matches
def platform_matches(List[str] unsupported_platforms)
Definition: utils.py:31
GaudiTesting.utils.compare_dicts
str compare_dicts(Dict[str, Any] d1, Dict[str, Any] d2, str ignore_re=None)
Definition: utils.py:180
GaudiTesting.utils.CodeWrapper.language
language
Definition: utils.py:25
GaudiTesting.utils.find_histos_summaries
def find_histos_summaries(stdout)
Definition: utils.py:351
GaudiTesting.utils.CodeWrapper
Definition: utils.py:22
GaudiTesting.utils._parse_histos_summary
def _parse_histos_summary(lines, pos)
Definition: utils.py:283
GaudiTesting.utils.expand_reference_file_name
def expand_reference_file_name(reference)
Definition: utils.py:114
GaudiTesting.utils.filter_dict
Dict[str, Any] filter_dict(Dict[str, Any] d, re.Pattern ignore_re)
Definition: utils.py:166
GaudiTesting.utils.kill_tree
def kill_tree(ppid, sig)
Definition: utils.py:43
Gaudi::Functional::details::zip::range
decltype(auto) range(Args &&... args)
Zips multiple containers together to form a single range.
Definition: details.h:97