The Gaudi Framework  v39r0 (5b8b5eda)
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(r"^(.*)SUCCESS\s+Booked (\d+) Histogram\‍(s\‍) :\s+([\s\w=-]*)")
198 
199 
200 def _parse_ttree_summary(lines, pos):
201  """
202  Parse the TTree summary table in lines, starting from pos.
203  Returns a tuple with the dictionary with the digested informations and the
204  position of the first line after the summary.
205  """
206  result = {}
207  i = pos + 1 # first line is a sequence of '*'
208  count = len(lines)
209 
210  def splitcols(l):
211  return [f.strip() for f in l.strip("*\n").split(":", 2)]
212 
213  def parseblock(ll):
214  r = {}
215  delta_i = 0
216  cols = splitcols(ll[0])
217 
218  if len(ll) == 3:
219  # default one line name/title
220  r["Name"], r["Title"] = cols[1:]
221  elif len(ll) == 4:
222  # in case title is moved to next line due to too long name
223  delta_i = 1
224  r["Name"] = cols[1]
225  r["Title"] = ll[1].strip("*\n").split("|")[1].strip()
226  else:
227  assert False
228 
229  cols = splitcols(ll[1 + delta_i])
230  r["Entries"] = int(cols[1])
231 
232  sizes = cols[2].split()
233  r["Total size"] = int(sizes[2])
234  if sizes[-1] == "memory":
235  r["File size"] = 0
236  else:
237  r["File size"] = int(sizes[-1])
238 
239  cols = splitcols(ll[2 + delta_i])
240  sizes = cols[2].split()
241  if cols[0] == "Baskets":
242  r["Baskets"] = int(cols[1])
243  r["Basket size"] = int(sizes[2])
244  r["Compression"] = float(sizes[-1])
245 
246  return r
247 
248  def nextblock(lines, i):
249  delta_i = 1
250  dots = re.compile(r"^\.+$")
251  stars = re.compile(r"^\*+$")
252  count = len(lines)
253  while (
254  i + delta_i < count
255  and not dots.match(lines[i + delta_i][1:-1])
256  and not stars.match(lines[i + delta_i])
257  ):
258  delta_i += 1
259  return i + delta_i
260 
261  if i < (count - 3) and lines[i].startswith("*Tree"):
262  i_nextblock = nextblock(lines, i)
263  result = parseblock(lines[i:i_nextblock])
264  result["Branches"] = {}
265  i = i_nextblock + 1
266  while i < (count - 3) and lines[i].startswith("*Br"):
267  if i < (count - 2) and lines[i].startswith("*Branch "):
268  # skip branch header
269  i += 3
270  continue
271  i_nextblock = nextblock(lines, i)
272  if i_nextblock >= count:
273  break
274  branch = parseblock(lines[i:i_nextblock])
275  result["Branches"][branch["Name"]] = branch
276  i = i_nextblock + 1
277 
278  return (result, i)
279 
280 
281 def _parse_histos_summary(lines, pos):
282  """
283  Extract the histograms infos from the lines starting at pos.
284  Returns the position of the first line after the summary block.
285  """
286  global h_count_re
287  h_table_head = re.compile(
288  r'SUCCESS\s+(1D|2D|3D|1D profile|2D profile) histograms in directory\s+"(\w*)"'
289  )
290  h_short_summ = re.compile(r"ID=([^\"]+)\s+\"([^\"]+)\"\s+(.*)")
291 
292  nlines = len(lines)
293 
294  # decode header
295  m = h_count_re.search(lines[pos])
296  name = m.group(1).strip()
297  total = int(m.group(2))
298  header = {}
299  for k, v in [x.split("=") for x in m.group(3).split()]:
300  header[k] = int(v)
301  pos += 1
302  header["Total"] = total
303 
304  summ = {}
305  while pos < nlines:
306  m = h_table_head.search(lines[pos])
307  if m:
308  t, d = m.groups(1) # type and directory
309  t = t.replace(" profile", "Prof")
310  pos += 1
311  if pos < nlines:
312  l = lines[pos]
313  else:
314  l = ""
315  cont = {}
316  if l.startswith(" | ID"):
317  # table format
318  titles = [x.strip() for x in l.split("|")][1:]
319  pos += 1
320  while pos < nlines and lines[pos].startswith(" |"):
321  l = lines[pos]
322  values = [x.strip() for x in l.split("|")][1:]
323  hcont = {}
324  for i in range(len(titles)):
325  hcont[titles[i]] = values[i]
326  cont[hcont["ID"]] = hcont
327  pos += 1
328  elif l.startswith(" ID="):
329  while pos < nlines and lines[pos].startswith(" ID="):
330  values = [
331  x.strip() for x in h_short_summ.search(lines[pos]).groups()
332  ]
333  cont[values[0]] = values
334  pos += 1
335  else: # not interpreted
336  raise RuntimeError("Cannot understand line %d: '%s'" % (pos, l))
337  if d not in summ:
338  summ[d] = {}
339  summ[d][t] = cont
340  summ[d]["header"] = header
341  else:
342  break
343  if not summ:
344  # If the full table is not present, we use only the header
345  summ[name] = {"header": header}
346  return summ, pos
347 
348 
350  """
351  Scan stdout to find ROOT Histogram summaries and digest them.
352  """
353  outlines = stdout.splitlines() if hasattr(stdout, "splitlines") else stdout
354  nlines = len(outlines) - 1
355  summaries = {}
356  global h_count_re
357 
358  pos = 0
359  while pos < nlines:
360  summ = {}
361  # find first line of block:
362  match = h_count_re.search(outlines[pos])
363  while pos < nlines and not match:
364  pos += 1
365  match = h_count_re.search(outlines[pos])
366  if match:
367  summ, pos = _parse_histos_summary(outlines, pos)
368  summaries.update(summ)
369  return summaries
370 
371 
373  """
374  Scan stdout to find ROOT TTree summaries and digest them.
375  """
376  stars = re.compile(r"^\*+$")
377  outlines = stdout.splitlines() if hasattr(stdout, "splitlines") else stdout
378  nlines = len(outlines)
379  trees = {}
380 
381  i = 0
382  while i < nlines: # loop over the output
383  # look for
384  while i < nlines and not stars.match(outlines[i]):
385  i += 1
386  if i < nlines:
387  tree, i = _parse_ttree_summary(outlines, i)
388  if tree:
389  trees[tree["Name"]] = tree
390 
391  return trees
392 
393 
395  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:394
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:372
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:200
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:349
GaudiTesting.utils.CodeWrapper
Definition: utils.py:22
GaudiTesting.utils._parse_histos_summary
def _parse_histos_summary(lines, pos)
Definition: utils.py:281
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:98