Loading [MathJax]/extensions/tex2jax.js
The Gaudi Framework  v31r0 (aeb156f0)
All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Friends Macros Modules Pages
BaseTest.py
Go to the documentation of this file.
1 # -*- coding: utf-8 -*-
2 
3 import os
4 import sys
5 import time
6 import signal
7 import threading
8 import platform
9 import tempfile
10 import inspect
11 import re
12 import logging
13 
14 from subprocess import Popen, PIPE, STDOUT
15 
16 
17 def sanitize_for_xml(data):
18  '''
19  Take a string with invalid ASCII/UTF characters and quote them so that the
20  string can be used in an XML text.
21 
22  >>> sanitize_for_xml('this is \x1b')
23  'this is [NON-XML-CHAR-0x1B]'
24  '''
25  bad_chars = re.compile(
26  u'[\x00-\x08\x0b\x0c\x0e-\x1F\uD800-\uDFFF\uFFFE\uFFFF]')
27 
28  def quote(match):
29  'helper function'
30  return ''.join('[NON-XML-CHAR-0x%2X]' % ord(c) for c in match.group())
31 
32  return bad_chars.sub(quote, data)
33 
34 
35 def dumpProcs(name):
36  '''helper to debug GAUDI-1084, dump the list of processes'''
37  from getpass import getuser
38  if 'WORKSPACE' in os.environ:
39  p = Popen(['ps', '-fH', '-U', getuser()], stdout=PIPE)
40  with open(os.path.join(os.environ['WORKSPACE'], name), 'w') as f:
41  f.write(p.communicate()[0])
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  log = logging.getLogger('kill_tree')
50  ps_cmd = ['ps', '--no-headers', '-o', 'pid', '--ppid', str(ppid)]
51  get_children = Popen(ps_cmd, stdout=PIPE, stderr=PIPE)
52  children = map(int, get_children.communicate()[0].split())
53  for child in children:
54  kill_tree(child, sig)
55  try:
56  log.debug('killing process %d', ppid)
57  os.kill(ppid, sig)
58  except OSError, err:
59  if err.errno != 3: # No such process
60  raise
61  log.debug('no such process %d', ppid)
62 
63 
64 # -------------------------------------------------------------------------#
65 
66 
67 class BaseTest(object):
68 
69  _common_tmpdir = None
70 
71  def __init__(self):
72  self.program = ''
73  self.args = []
74  self.reference = ''
75  self.error_reference = ''
76  self.options = ''
77  self.stderr = ''
78  self.timeout = 600
79  self.exit_code = None
80  self.environment = None
82  self.signal = None
83  self.workdir = os.curdir
84  self.use_temp_dir = False
85  # Variables not for users
86  self.status = None
87  self.name = ''
88  self.causes = []
89  self.result = Result(self)
90  self.returnedCode = 0
91  self.out = ''
92  self.err = ''
93  self.proc = None
94  self.stack_trace = None
95  self.basedir = os.getcwd()
96 
97  def run(self):
98  logging.debug('running test %s', self.name)
99 
100  if self.options:
101  if re.search(
102  r'from\s+Gaudi.Configuration\s+import\s+\*|'
103  'from\s+Configurables\s+import', self.options):
104  optionFile = tempfile.NamedTemporaryFile(suffix='.py')
105  else:
106  optionFile = tempfile.NamedTemporaryFile(suffix='.opts')
107  optionFile.file.write(self.options)
108  optionFile.seek(0)
109  self.args.append(RationalizePath(optionFile.name))
110 
111  # If not specified, setting the environment
112  if self.environment is None:
113  self.environment = os.environ
114  else:
115  self.environment = dict(self.environment.items() +
116  os.environ.items())
117 
118  platform_id = (os.environ.get('BINARY_TAG')
119  or os.environ.get('CMTCONFIG') or platform.platform())
120  # If at least one regex matches we skip the test.
121  skip_test = bool([
122  None for prex in self.unsupported_platforms
123  if re.search(prex, platform_id)
124  ])
125 
126  if not skip_test:
127  # handle working/temporary directory options
128  workdir = self.workdir
129  if self.use_temp_dir:
130  if self._common_tmpdir:
131  workdir = self._common_tmpdir
132  else:
133  workdir = tempfile.mkdtemp()
134 
135  # prepare the command to execute
136  prog = ''
137  if self.program != '':
138  prog = self.program
139  elif "GAUDIEXE" in os.environ:
140  prog = os.environ["GAUDIEXE"]
141  else:
142  prog = "Gaudi.exe"
143 
144  dummy, prog_ext = os.path.splitext(prog)
145  if prog_ext not in [".exe", ".py", ".bat"]:
146  prog += ".exe"
147  prog_ext = ".exe"
148 
149  prog = which(prog) or prog
150 
151  args = map(RationalizePath, self.args)
152 
153  if prog_ext == ".py":
154  params = ['python', RationalizePath(prog)] + args
155  else:
156  params = [RationalizePath(prog)] + args
157 
158  validatorRes = Result({
159  'CAUSE': None,
160  'EXCEPTION': None,
161  'RESOURCE': None,
162  'TARGET': None,
163  'TRACEBACK': None,
164  'START_TIME': None,
165  'END_TIME': None,
166  'TIMEOUT_DETAIL': None
167  })
168  self.result = validatorRes
169 
170  # we need to switch directory because the validator expects to run
171  # in the same dir as the program
172  os.chdir(workdir)
173 
174  # launching test in a different thread to handle timeout exception
175  def target():
176  logging.debug('executing %r in %s', params, workdir)
177  self.proc = Popen(
178  params, stdout=PIPE, stderr=PIPE, env=self.environment)
179  logging.debug('(pid: %d)', self.proc.pid)
180  self.out, self.err = self.proc.communicate()
181 
182  thread = threading.Thread(target=target)
183  thread.start()
184  # catching timeout
185  thread.join(self.timeout)
186 
187  if thread.is_alive():
188  logging.debug('time out in test %s (pid %d)', self.name,
189  self.proc.pid)
190  # get the stack trace of the stuck process
191  cmd = [
192  'gdb', '--pid',
193  str(self.proc.pid), '--batch',
194  '--eval-command=thread apply all backtrace'
195  ]
196  gdb = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=STDOUT)
197  self.stack_trace = gdb.communicate()[0]
198 
199  kill_tree(self.proc.pid, signal.SIGTERM)
200  thread.join(60)
201  if thread.is_alive():
202  kill_tree(self.proc.pid, signal.SIGKILL)
203  self.causes.append('timeout')
204  else:
205  logging.debug('completed test %s', self.name)
206 
207  # Getting the error code
208  logging.debug('returnedCode = %s', self.proc.returncode)
209  self.returnedCode = self.proc.returncode
210 
211  logging.debug('validating test...')
212  self.result, self.causes = self.ValidateOutput(
213  stdout=self.out, stderr=self.err, result=validatorRes)
214 
215  # remove the temporary directory if we created it
216  if self.use_temp_dir and not self._common_tmpdir:
217  shutil.rmtree(workdir, True)
218 
219  os.chdir(self.basedir)
220 
221  # handle application exit code
222  if self.signal is not None:
223  if int(self.returnedCode) != -int(self.signal):
224  self.causes.append('exit code')
225 
226  elif self.exit_code is not None:
227  if int(self.returnedCode) != int(self.exit_code):
228  self.causes.append('exit code')
229 
230  elif self.returnedCode != 0:
231  self.causes.append("exit code")
232 
233  if self.causes:
234  self.status = "failed"
235  else:
236  self.status = "passed"
237 
238  else:
239  self.status = "skipped"
240 
241  logging.debug('%s: %s', self.name, self.status)
242  field_mapping = {
243  'Exit Code': 'returnedCode',
244  'stderr': 'err',
245  'Arguments': 'args',
246  'Environment': 'environment',
247  'Status': 'status',
248  'stdout': 'out',
249  'Program Name': 'program',
250  'Name': 'name',
251  'Validator': 'validator',
252  'Output Reference File': 'reference',
253  'Error Reference File': 'error_reference',
254  'Causes': 'causes',
255  # 'Validator Result': 'result.annotations',
256  'Unsupported Platforms': 'unsupported_platforms',
257  'Stack Trace': 'stack_trace'
258  }
259  resultDict = [(key, getattr(self, attr))
260  for key, attr in field_mapping.iteritems()
261  if getattr(self, attr)]
262  resultDict.append(('Working Directory',
264  os.path.join(os.getcwd(), self.workdir))))
265  # print dict(resultDict).keys()
266  resultDict.extend(self.result.annotations.iteritems())
267  # print self.result.annotations.keys()
268  return dict(resultDict)
269 
270  # -------------------------------------------------#
271  # ----------------Validating tool------------------#
272  # -------------------------------------------------#
273 
274  def ValidateOutput(self, stdout, stderr, result):
275  if not self.stderr:
276  self.validateWithReference(stdout, stderr, result, self.causes)
277  elif stderr.strip() != self.stderr.strip():
278  self.causes.append('standard error')
279  return result, self.causes
280 
281  def findReferenceBlock(self,
282  reference=None,
283  stdout=None,
284  result=None,
285  causes=None,
286  signature_offset=0,
287  signature=None,
288  id=None):
289  """
290  Given a block of text, tries to find it in the output. The block had to be identified by a signature line. By default, the first line is used as signature, or the line pointed to by signature_offset. If signature_offset points outside the block, a signature line can be passed as signature argument. Note: if 'signature' is None (the default), a negative signature_offset is interpreted as index in a list (e.g. -1 means the last line), otherwise the it is interpreted as the number of lines before the first one of the block the signature must appear. The parameter 'id' allow to distinguish between different calls to this function in the same validation code.
291  """
292 
293  if reference is None:
294  reference = self.reference
295  if stdout is None:
296  stdout = self.out
297  if result is None:
298  result = self.result
299  if causes is None:
300  causes = self.causes
301 
302  reflines = filter(None,
303  map(lambda s: s.rstrip(), reference.splitlines()))
304  if not reflines:
305  raise RuntimeError("Empty (or null) reference")
306  # the same on standard output
307  outlines = filter(None, map(lambda s: s.rstrip(), stdout.splitlines()))
308 
309  res_field = "GaudiTest.RefBlock"
310  if id:
311  res_field += "_%s" % id
312 
313  if signature is None:
314  if signature_offset < 0:
315  signature_offset = len(reference) + signature_offset
316  signature = reflines[signature_offset]
317  # find the reference block in the output file
318  try:
319  pos = outlines.index(signature)
320  outlines = outlines[pos - signature_offset:pos + len(reflines) -
321  signature_offset]
322  if reflines != outlines:
323  msg = "standard output"
324  # I do not want 2 messages in causes if the function is called
325  # twice
326  if not msg in causes:
327  causes.append(msg)
328  result[res_field + ".observed"] = result.Quote(
329  "\n".join(outlines))
330  except ValueError:
331  causes.append("missing signature")
332  result[res_field + ".signature"] = result.Quote(signature)
333  if len(reflines) > 1 or signature != reflines[0]:
334  result[res_field + ".expected"] = result.Quote("\n".join(reflines))
335  return causes
336 
337  def countErrorLines(self,
338  expected={
339  'ERROR': 0,
340  'FATAL': 0
341  },
342  stdout=None,
343  result=None,
344  causes=None):
345  """
346  Count the number of messages with required severity (by default ERROR and FATAL)
347  and check if their numbers match the expected ones (0 by default).
348  The dictionary "expected" can be used to tune the number of errors and fatals
349  allowed, or to limit the number of expected warnings etc.
350  """
351 
352  if stdout is None:
353  stdout = self.out
354  if result is None:
355  result = self.result
356  if causes is None:
357  causes = self.causes
358 
359  # prepare the dictionary to record the extracted lines
360  errors = {}
361  for sev in expected:
362  errors[sev] = []
363 
364  outlines = stdout.splitlines()
365  from math import log10
366  fmt = "%%%dd - %%s" % (int(log10(len(outlines) + 1)))
367 
368  linecount = 0
369  for l in outlines:
370  linecount += 1
371  words = l.split()
372  if len(words) >= 2 and words[1] in errors:
373  errors[words[1]].append(fmt % (linecount, l.rstrip()))
374 
375  for e in errors:
376  if len(errors[e]) != expected[e]:
377  causes.append('%s(%d)' % (e, len(errors[e])))
378  result["GaudiTest.lines.%s" % e] = result.Quote('\n'.join(
379  errors[e]))
380  result["GaudiTest.lines.%s.expected#" % e] = result.Quote(
381  str(expected[e]))
382 
383  return causes
384 
385  def CheckTTreesSummaries(self,
386  stdout=None,
387  result=None,
388  causes=None,
389  trees_dict=None,
390  ignore=r"Basket|.*size|Compression"):
391  """
392  Compare the TTree summaries in stdout with the ones in trees_dict or in
393  the reference file. By default ignore the size, compression and basket
394  fields.
395  The presence of TTree summaries when none is expected is not a failure.
396  """
397  if stdout is None:
398  stdout = self.out
399  if result is None:
400  result = self.result
401  if causes is None:
402  causes = self.causes
403  if trees_dict is None:
404  lreference = self._expandReferenceFileName(self.reference)
405  # call the validator if the file exists
406  if lreference and os.path.isfile(lreference):
407  trees_dict = findTTreeSummaries(open(lreference).read())
408  else:
409  trees_dict = {}
410 
411  from pprint import PrettyPrinter
412  pp = PrettyPrinter()
413  if trees_dict:
414  result["GaudiTest.TTrees.expected"] = result.Quote(
415  pp.pformat(trees_dict))
416  if ignore:
417  result["GaudiTest.TTrees.ignore"] = result.Quote(ignore)
418 
419  trees = findTTreeSummaries(stdout)
420  failed = cmpTreesDicts(trees_dict, trees, ignore)
421  if failed:
422  causes.append("trees summaries")
423  msg = "%s: %s != %s" % getCmpFailingValues(trees_dict, trees,
424  failed)
425  result["GaudiTest.TTrees.failure_on"] = result.Quote(msg)
426  result["GaudiTest.TTrees.found"] = result.Quote(pp.pformat(trees))
427 
428  return causes
429 
430  def CheckHistosSummaries(self,
431  stdout=None,
432  result=None,
433  causes=None,
434  dict=None,
435  ignore=None):
436  """
437  Compare the TTree summaries in stdout with the ones in trees_dict or in
438  the reference file. By default ignore the size, compression and basket
439  fields.
440  The presence of TTree summaries when none is expected is not a failure.
441  """
442  if stdout is None:
443  stdout = self.out
444  if result is None:
445  result = self.result
446  if causes is None:
447  causes = self.causes
448 
449  if dict is None:
450  lreference = self._expandReferenceFileName(self.reference)
451  # call the validator if the file exists
452  if lreference and os.path.isfile(lreference):
453  dict = findHistosSummaries(open(lreference).read())
454  else:
455  dict = {}
456 
457  from pprint import PrettyPrinter
458  pp = PrettyPrinter()
459  if dict:
460  result["GaudiTest.Histos.expected"] = result.Quote(
461  pp.pformat(dict))
462  if ignore:
463  result["GaudiTest.Histos.ignore"] = result.Quote(ignore)
464 
465  histos = findHistosSummaries(stdout)
466  failed = cmpTreesDicts(dict, histos, ignore)
467  if failed:
468  causes.append("histos summaries")
469  msg = "%s: %s != %s" % getCmpFailingValues(dict, histos, failed)
470  result["GaudiTest.Histos.failure_on"] = result.Quote(msg)
471  result["GaudiTest.Histos.found"] = result.Quote(pp.pformat(histos))
472 
473  return causes
474 
475  def validateWithReference(self,
476  stdout=None,
477  stderr=None,
478  result=None,
479  causes=None,
480  preproc=None):
481  '''
482  Default validation acti*on: compare standard output and error to the
483  reference files.
484  '''
485 
486  if stdout is None:
487  stdout = self.out
488  if stderr is None:
489  stderr = self.err
490  if result is None:
491  result = self.result
492  if causes is None:
493  causes = self.causes
494 
495  # set the default output preprocessor
496  if preproc is None:
497  preproc = normalizeExamples
498  # check standard output
499  lreference = self._expandReferenceFileName(self.reference)
500  # call the validator if the file exists
501  if lreference and os.path.isfile(lreference):
502  causes += ReferenceFileValidator(
503  lreference, "standard output", "Output Diff",
504  preproc=preproc)(stdout, result)
505  elif lreference:
506  causes += ["missing reference file"]
507  # Compare TTree summaries
508  causes = self.CheckTTreesSummaries(stdout, result, causes)
509  causes = self.CheckHistosSummaries(stdout, result, causes)
510  if causes and lreference: # Write a new reference file for stdout
511  try:
512  cnt = 0
513  newrefname = '.'.join([lreference, 'new'])
514  while os.path.exists(newrefname):
515  cnt += 1
516  newrefname = '.'.join([lreference, '~%d~' % cnt, 'new'])
517  newref = open(newrefname, "w")
518  # sanitize newlines
519  for l in stdout.splitlines():
520  newref.write(l.rstrip() + '\n')
521  del newref # flush and close
522  result['New Output Reference File'] = os.path.relpath(
523  newrefname, self.basedir)
524  except IOError:
525  # Ignore IO errors when trying to update reference files
526  # because we may be in a read-only filesystem
527  pass
528 
529  # check standard error
530  lreference = self._expandReferenceFileName(self.error_reference)
531  # call the validator if we have a file to use
532  if lreference:
533  if os.path.isfile(lreference):
534  newcauses = ReferenceFileValidator(
535  lreference,
536  "standard error",
537  "Error Diff",
538  preproc=preproc)(stderr, result)
539  else:
540  newcauses += ["missing error reference file"]
541  causes += newcauses
542  if newcauses and lreference: # Write a new reference file for stdedd
543  cnt = 0
544  newrefname = '.'.join([lreference, 'new'])
545  while os.path.exists(newrefname):
546  cnt += 1
547  newrefname = '.'.join([lreference, '~%d~' % cnt, 'new'])
548  newref = open(newrefname, "w")
549  # sanitize newlines
550  for l in stderr.splitlines():
551  newref.write(l.rstrip() + '\n')
552  del newref # flush and close
553  result['New Error Reference File'] = os.path.relpath(
554  newrefname, self.basedir)
555  else:
556  causes += BasicOutputValidator(lreference, "standard error",
557  "ExecTest.expected_stderr")(stderr,
558  result)
559  return causes
560 
561  def _expandReferenceFileName(self, reffile):
562  # if no file is passed, do nothing
563  if not reffile:
564  return ""
565 
566  # function to split an extension in constituents parts
567  def platformSplit(p):
568  import re
569  delim = re.compile('-' in p and r"[-+]" or r"_")
570  return set(delim.split(p))
571 
572  reference = os.path.normpath(
573  os.path.join(self.basedir, os.path.expandvars(reffile)))
574 
575  # old-style platform-specific reference name
576  spec_ref = reference[:-3] + GetPlatform(self)[0:3] + reference[-3:]
577  if os.path.isfile(spec_ref):
578  reference = spec_ref
579  else: # look for new-style platform specific reference files:
580  # get all the files whose name start with the reference filename
581  dirname, basename = os.path.split(reference)
582  if not dirname:
583  dirname = '.'
584  head = basename + "."
585  head_len = len(head)
586  platform = platformSplit(GetPlatform(self))
587  if 'do0' in platform:
588  platform.add('dbg')
589  candidates = []
590  for f in os.listdir(dirname):
591  if f.startswith(head):
592  req_plat = platformSplit(f[head_len:])
593  if platform.issuperset(req_plat):
594  candidates.append((len(req_plat), f))
595  if candidates: # take the one with highest matching
596  # FIXME: it is not possible to say if x86_64-slc5-gcc43-dbg
597  # has to use ref.x86_64-gcc43 or ref.slc5-dbg
598  candidates.sort()
599  reference = os.path.join(dirname, candidates[-1][1])
600  return reference
601 
602 
603 # ======= GAUDI TOOLS =======
604 
605 import shutil
606 import string
607 import difflib
608 import calendar
609 
610 try:
611  from GaudiKernel import ROOT6WorkAroundEnabled
612 except ImportError:
613 
615  # dummy implementation
616  return False
617 
618 
619 # --------------------------------- TOOLS ---------------------------------#
620 
621 
623  """
624  Function used to normalize the used path
625  """
626  newPath = os.path.normpath(os.path.expandvars(p))
627  if os.path.exists(newPath):
628  p = os.path.realpath(newPath)
629  return p
630 
631 
632 def which(executable):
633  """
634  Locates an executable in the executables path ($PATH) and returns the full
635  path to it. An application is looked for with or without the '.exe' suffix.
636  If the executable cannot be found, None is returned
637  """
638  if os.path.isabs(executable):
639  if not os.path.exists(executable):
640  if executable.endswith('.exe'):
641  if os.path.exists(executable[:-4]):
642  return executable[:-4]
643  else:
644  head, executable = os.path.split(executable)
645  else:
646  return executable
647  for d in os.environ.get("PATH").split(os.pathsep):
648  fullpath = os.path.join(d, executable)
649  if os.path.exists(fullpath):
650  return fullpath
651  if executable.endswith('.exe'):
652  return which(executable[:-4])
653  return None
654 
655 
656 # -------------------------------------------------------------------------#
657 # ----------------------------- Result Classe -----------------------------#
658 # -------------------------------------------------------------------------#
659 import types
660 
661 
662 class Result:
663 
664  PASS = 'PASS'
665  FAIL = 'FAIL'
666  ERROR = 'ERROR'
667  UNTESTED = 'UNTESTED'
668 
669  EXCEPTION = ""
670  RESOURCE = ""
671  TARGET = ""
672  TRACEBACK = ""
673  START_TIME = ""
674  END_TIME = ""
675  TIMEOUT_DETAIL = ""
676 
677  def __init__(self, kind=None, id=None, outcome=PASS, annotations={}):
678  self.annotations = annotations.copy()
679 
680  def __getitem__(self, key):
681  assert type(key) in types.StringTypes
682  return self.annotations[key]
683 
684  def __setitem__(self, key, value):
685  assert type(key) in types.StringTypes
686  assert type(value) in types.StringTypes
687  self.annotations[key] = value
688 
689  def Quote(self, string):
690  return string
691 
692 
693 # -------------------------------------------------------------------------#
694 # --------------------------- Validator Classes ---------------------------#
695 # -------------------------------------------------------------------------#
696 
697 # Basic implementation of an option validator for Gaudi test. This
698 # implementation is based on the standard (LCG) validation functions used
699 # in QMTest.
700 
701 
703  def __init__(self, ref, cause, result_key):
704  self.ref = ref
705  self.cause = cause
706  self.result_key = result_key
707 
708  def __call__(self, out, result):
709  """Validate the output of the program.
710  'stdout' -- A string containing the data written to the standard output
711  stream.
712  'stderr' -- A string containing the data written to the standard error
713  stream.
714  'result' -- A 'Result' object. It may be used to annotate
715  the outcome according to the content of stderr.
716  returns -- A list of strings giving causes of failure."""
717 
718  causes = []
719  # Check the output
720  if not self.__CompareText(out, self.ref):
721  causes.append(self.cause)
722  result[self.result_key] = result.Quote(self.ref)
723 
724  return causes
725 
726  def __CompareText(self, s1, s2):
727  """Compare 's1' and 's2', ignoring line endings.
728  's1' -- A string.
729  's2' -- A string.
730  returns -- True if 's1' and 's2' are the same, ignoring
731  differences in line endings."""
732  if ROOT6WorkAroundEnabled('ReadRootmapCheck'):
733  # FIXME: (MCl) Hide warnings from new rootmap sanity check until we
734  # can fix them
735  to_ignore = re.compile(
736  r'Warning in <TInterpreter::ReadRootmapFile>: .* is already in .*'
737  )
738 
739  def keep_line(l):
740  return not to_ignore.match(l)
741 
742  return filter(keep_line, s1.splitlines()) == filter(
743  keep_line, s2.splitlines())
744  else:
745  return s1.splitlines() == s2.splitlines()
746 
747 
748 # ------------------------ Preprocessor elements ------------------------#
750  """ Base class for a callable that takes a file and returns a modified
751  version of it."""
752 
753  def __processLine__(self, line):
754  return line
755 
756  def __processFile__(self, lines):
757  output = []
758  for l in lines:
759  l = self.__processLine__(l)
760  if l:
761  output.append(l)
762  return output
763 
764  def __call__(self, input):
765  if hasattr(input, "__iter__"):
766  lines = input
767  mergeback = False
768  else:
769  lines = input.splitlines()
770  mergeback = True
771  output = self.__processFile__(lines)
772  if mergeback:
773  output = '\n'.join(output)
774  return output
775 
776  def __add__(self, rhs):
777  return FilePreprocessorSequence([self, rhs])
778 
779 
781  def __init__(self, members=[]):
782  self.members = members
783 
784  def __add__(self, rhs):
785  return FilePreprocessorSequence(self.members + [rhs])
786 
787  def __call__(self, input):
788  output = input
789  for pp in self.members:
790  output = pp(output)
791  return output
792 
793 
795  def __init__(self, strings=[], regexps=[]):
796  import re
797  self.strings = strings
798  self.regexps = map(re.compile, regexps)
799 
800  def __processLine__(self, line):
801  for s in self.strings:
802  if line.find(s) >= 0:
803  return None
804  for r in self.regexps:
805  if r.search(line):
806  return None
807  return line
808 
809 
811  def __init__(self, start, end):
812  self.start = start
813  self.end = end
814  self._skipping = False
815 
816  def __processLine__(self, line):
817  if self.start in line:
818  self._skipping = True
819  return None
820  elif self.end in line:
821  self._skipping = False
822  elif self._skipping:
823  return None
824  return line
825 
826 
828  def __init__(self, orig, repl="", when=None):
829  if when:
830  when = re.compile(when)
831  self._operations = [(when, re.compile(orig), repl)]
832 
833  def __add__(self, rhs):
834  if isinstance(rhs, RegexpReplacer):
835  res = RegexpReplacer("", "", None)
836  res._operations = self._operations + rhs._operations
837  else:
838  res = FilePreprocessor.__add__(self, rhs)
839  return res
840 
841  def __processLine__(self, line):
842  for w, o, r in self._operations:
843  if w is None or w.search(line):
844  line = o.sub(r, line)
845  return line
846 
847 
848 # Common preprocessors
849 maskPointers = RegexpReplacer("0x[0-9a-fA-F]{4,16}", "0x########")
850 normalizeDate = RegexpReplacer(
851  "[0-2]?[0-9]:[0-5][0-9]:[0-5][0-9] [0-9]{4}[-/][01][0-9][-/][0-3][0-9][ A-Z]*",
852  "00:00:00 1970-01-01")
853 normalizeEOL = FilePreprocessor()
854 normalizeEOL.__processLine__ = lambda line: str(line).rstrip() + '\n'
855 
856 skipEmptyLines = FilePreprocessor()
857 # FIXME: that's ugly
858 skipEmptyLines.__processLine__ = lambda line: (line.strip() and line) or None
859 
860 # Special preprocessor sorting the list of strings (whitespace separated)
861 # that follow a signature on a single line
862 
863 
865  def __init__(self, signature):
866  self.signature = signature
867  self.siglen = len(signature)
868 
869  def __processLine__(self, line):
870  pos = line.find(self.signature)
871  if pos >= 0:
872  line = line[:(pos + self.siglen)]
873  lst = line[(pos + self.siglen):].split()
874  lst.sort()
875  line += " ".join(lst)
876  return line
877 
878 
880  '''
881  Sort group of lines matching a regular expression
882  '''
883 
884  def __init__(self, exp):
885  self.exp = exp if hasattr(exp, 'match') else re.compile(exp)
886 
887  def __processFile__(self, lines):
888  match = self.exp.match
889  output = []
890  group = []
891  for l in lines:
892  if match(l):
893  group.append(l)
894  else:
895  if group:
896  group.sort()
897  output.extend(group)
898  group = []
899  output.append(l)
900  return output
901 
902 
903 # Preprocessors for GaudiExamples
904 normalizeExamples = maskPointers + normalizeDate
905 for w, o, r in [
906  # ("TIMER.TIMER",r"[0-9]", "0"), # Normalize time output
907  ("TIMER.TIMER", r"\s+[+-]?[0-9]+[0-9.]*", " 0"), # Normalize time output
908  ("release all pending", r"^.*/([^/]*:.*)", r"\1"),
909  ("^#.*file", r"file '.*[/\\]([^/\\]*)$", r"file '\1"),
910  ("^JobOptionsSvc.*options successfully read in from",
911  r"read in from .*[/\\]([^/\\]*)$",
912  r"file \1"), # normalize path to options
913  # Normalize UUID, except those ending with all 0s (i.e. the class IDs)
914  (None,
915  r"[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}(?!-0{12})-[0-9A-Fa-f]{12}",
916  "00000000-0000-0000-0000-000000000000"),
917  # Absorb a change in ServiceLocatorHelper
918  ("ServiceLocatorHelper::", "ServiceLocatorHelper::(create|locate)Service",
919  "ServiceLocatorHelper::service"),
920  # Remove the leading 0 in Windows' exponential format
921  (None, r"e([-+])0([0-9][0-9])", r"e\1\2"),
922  # Output line changed in Gaudi v24
923  (None, r'Service reference count check:',
924  r'Looping over all active services...'),
925  # Ignore count of declared properties (anyway they are all printed)
926  (None,
927  r"^(.*(DEBUG|SUCCESS) List of ALL properties of .*#properties = )\d+",
928  r"\1NN"),
929  ('ApplicationMgr', r'(declareMultiSvcType|addMultiSvc): ', ''),
930 ]: # [ ("TIMER.TIMER","[0-9]+[0-9.]*", "") ]
931  normalizeExamples += RegexpReplacer(o, r, w)
932 
933 lineSkipper = LineSkipper(
934  [
935  "//GP:",
936  "JobOptionsSvc INFO # ",
937  "JobOptionsSvc WARNING # ",
938  "Time User",
939  "Welcome to",
940  "This machine has a speed",
941  "TIME:",
942  "running on",
943  "ToolSvc.Sequenc... INFO",
944  "DataListenerSvc INFO XML written to file:",
945  "[INFO]",
946  "[WARNING]",
947  "DEBUG No writable file catalog found which contains FID:",
948  "DEBUG Service base class initialized successfully",
949  # changed between v20 and v21
950  "DEBUG Incident timing:",
951  # introduced with patch #3487
952  # changed the level of the message from INFO to
953  # DEBUG
954  "INFO 'CnvServices':[",
955  # message removed because could be printed in constructor
956  "DEBUG 'CnvServices':[",
957  # The signal handler complains about SIGXCPU not
958  # defined on some platforms
959  'SIGXCPU',
960  ],
961  regexps=[
962  r"^JobOptionsSvc INFO *$",
963  r"^# ", # Ignore python comments
964  # skip the message reporting the version of the root file
965  r"(Always|SUCCESS)\s*(Root f|[^ ]* F)ile version:",
966  r"File '.*.xml' does not exist",
967  r"INFO Refer to dataset .* by its file ID:",
968  r"INFO Referring to dataset .* by its file ID:",
969  r"INFO Disconnect from dataset",
970  r"INFO Disconnected from dataset",
971  r"INFO Disconnected data IO:",
972  r"IncidentSvc\s*(DEBUG (Adding|Removing)|VERBOSE Calling)",
973  # I want to ignore the header of the unchecked StatusCode report
974  r"^StatusCodeSvc.*listing all unchecked return codes:",
975  r"^StatusCodeSvc\s*INFO\s*$",
976  r"Num\s*\|\s*Function\s*\|\s*Source Library",
977  r"^[-+]*\s*$",
978  # Hide the fake error message coming from POOL/ROOT (ROOT 5.21)
979  r"ERROR Failed to modify file: .* Errno=2 No such file or directory",
980  # Hide unchecked StatusCodes from dictionaries
981  r"^ +[0-9]+ \|.*ROOT",
982  r"^ +[0-9]+ \|.*\|.*Dict",
983  # Hide success StatusCodeSvc message
984  r"StatusCodeSvc.*all StatusCode instances where checked",
985  # Hide EventLoopMgr total timing report
986  r"EventLoopMgr.*---> Loop Finished",
987  r"HiveSlimEventLo.*---> Loop Finished",
988  # Remove ROOT TTree summary table, which changes from one version to the
989  # other
990  r"^\*.*\*$",
991  # Remove Histos Summaries
992  r"SUCCESS\s*Booked \d+ Histogram\(s\)",
993  r"^ \|",
994  r"^ ID=",
995  # Ignore added/removed properties
996  r"Property(.*)'Audit(Algorithm|Tool|Service)s':",
997  # these were missing in tools
998  r"Property(.*)'AuditRe(start|initialize)':",
999  r"Property(.*)'IsIOBound':",
1000  # removed with gaudi/Gaudi!273
1001  r"Property(.*)'ErrorCount(er)?':",
1002  # added with gaudi/Gaudi!306
1003  r"Property(.*)'Sequential':",
1004  # added with gaudi/Gaudi!314
1005  r"Property(.*)'FilterCircularDependencies':",
1006  # removed with gaudi/Gaudi!316
1007  r"Property(.*)'IsClonable':",
1008  # ignore uninteresting/obsolete messages
1009  r"Property update for OutputLevel : new value =",
1010  r"EventLoopMgr\s*DEBUG Creating OutputStream",
1011  ])
1012 
1013 if ROOT6WorkAroundEnabled('ReadRootmapCheck'):
1014  # FIXME: (MCl) Hide warnings from new rootmap sanity check until we can
1015  # fix them
1016  lineSkipper += LineSkipper(regexps=[
1017  r'Warning in <TInterpreter::ReadRootmapFile>: .* is already in .*',
1018  ])
1019 
1020 normalizeExamples = (
1021  lineSkipper + normalizeExamples + skipEmptyLines + normalizeEOL +
1022  LineSorter("Services to release : ") +
1023  SortGroupOfLines(r'^\S+\s+(DEBUG|SUCCESS) Property \[\'Name\':'))
1024 
1025 # --------------------- Validation functions/classes ---------------------#
1026 
1027 
1029  def __init__(self, reffile, cause, result_key, preproc=normalizeExamples):
1030  self.reffile = os.path.expandvars(reffile)
1031  self.cause = cause
1032  self.result_key = result_key
1033  self.preproc = preproc
1034 
1035  def __call__(self, stdout, result):
1036  causes = []
1037  if os.path.isfile(self.reffile):
1038  orig = open(self.reffile).xreadlines()
1039  if self.preproc:
1040  orig = self.preproc(orig)
1041  result[self.result_key + '.preproc.orig'] = \
1042  result.Quote('\n'.join(map(str.strip, orig)))
1043  else:
1044  orig = []
1045  new = stdout.splitlines()
1046  if self.preproc:
1047  new = self.preproc(new)
1048 
1049  diffs = difflib.ndiff(orig, new, charjunk=difflib.IS_CHARACTER_JUNK)
1050  filterdiffs = map(lambda x: x.strip(),
1051  filter(lambda x: x[0] != " ", diffs))
1052  if filterdiffs:
1053  result[self.result_key] = result.Quote("\n".join(filterdiffs))
1054  result[self.result_key] += result.Quote("""
1055  Legend:
1056  -) reference file
1057  +) standard output of the test""")
1058  result[self.result_key + '.preproc.new'] = \
1059  result.Quote('\n'.join(map(str.strip, new)))
1060  causes.append(self.cause)
1061  return causes
1062 
1063 
1065  """
1066  Scan stdout to find ROOT TTree summaries and digest them.
1067  """
1068  stars = re.compile(r"^\*+$")
1069  outlines = stdout.splitlines()
1070  nlines = len(outlines)
1071  trees = {}
1072 
1073  i = 0
1074  while i < nlines: # loop over the output
1075  # look for
1076  while i < nlines and not stars.match(outlines[i]):
1077  i += 1
1078  if i < nlines:
1079  tree, i = _parseTTreeSummary(outlines, i)
1080  if tree:
1081  trees[tree["Name"]] = tree
1082 
1083  return trees
1084 
1085 
1086 def cmpTreesDicts(reference, to_check, ignore=None):
1087  """
1088  Check that all the keys in reference are in to_check too, with the same value.
1089  If the value is a dict, the function is called recursively. to_check can
1090  contain more keys than reference, that will not be tested.
1091  The function returns at the first difference found.
1092  """
1093  fail_keys = []
1094  # filter the keys in the reference dictionary
1095  if ignore:
1096  ignore_re = re.compile(ignore)
1097  keys = [key for key in reference if not ignore_re.match(key)]
1098  else:
1099  keys = reference.keys()
1100  # loop over the keys (not ignored) in the reference dictionary
1101  for k in keys:
1102  if k in to_check: # the key must be in the dictionary to_check
1103  if (type(reference[k]) is dict) and (type(to_check[k]) is dict):
1104  # if both reference and to_check values are dictionaries,
1105  # recurse
1106  failed = fail_keys = cmpTreesDicts(reference[k], to_check[k],
1107  ignore)
1108  else:
1109  # compare the two values
1110  failed = to_check[k] != reference[k]
1111  else: # handle missing keys in the dictionary to check (i.e. failure)
1112  to_check[k] = None
1113  failed = True
1114  if failed:
1115  fail_keys.insert(0, k)
1116  break # exit from the loop at the first failure
1117  return fail_keys # return the list of keys bringing to the different values
1118 
1119 
1120 def getCmpFailingValues(reference, to_check, fail_path):
1121  c = to_check
1122  r = reference
1123  for k in fail_path:
1124  c = c.get(k, None)
1125  r = r.get(k, None)
1126  if c is None or r is None:
1127  break # one of the dictionaries is not deep enough
1128  return (fail_path, r, c)
1129 
1130 
1131 # signature of the print-out of the histograms
1132 h_count_re = re.compile(
1133  r"^(.*)SUCCESS\s+Booked (\d+) Histogram\(s\) :\s+([\s\w=-]*)")
1134 
1135 
1136 def _parseTTreeSummary(lines, pos):
1137  """
1138  Parse the TTree summary table in lines, starting from pos.
1139  Returns a tuple with the dictionary with the digested informations and the
1140  position of the first line after the summary.
1141  """
1142  result = {}
1143  i = pos + 1 # first line is a sequence of '*'
1144  count = len(lines)
1145 
1146  def splitcols(l):
1147  return [f.strip() for f in l.strip("*\n").split(':', 2)]
1148 
1149  def parseblock(ll):
1150  r = {}
1151  cols = splitcols(ll[0])
1152  r["Name"], r["Title"] = cols[1:]
1153 
1154  cols = splitcols(ll[1])
1155  r["Entries"] = int(cols[1])
1156 
1157  sizes = cols[2].split()
1158  r["Total size"] = int(sizes[2])
1159  if sizes[-1] == "memory":
1160  r["File size"] = 0
1161  else:
1162  r["File size"] = int(sizes[-1])
1163 
1164  cols = splitcols(ll[2])
1165  sizes = cols[2].split()
1166  if cols[0] == "Baskets":
1167  r["Baskets"] = int(cols[1])
1168  r["Basket size"] = int(sizes[2])
1169  r["Compression"] = float(sizes[-1])
1170  return r
1171 
1172  if i < (count - 3) and lines[i].startswith("*Tree"):
1173  result = parseblock(lines[i:i + 3])
1174  result["Branches"] = {}
1175  i += 4
1176  while i < (count - 3) and lines[i].startswith("*Br"):
1177  if i < (count - 2) and lines[i].startswith("*Branch "):
1178  # skip branch header
1179  i += 3
1180  continue
1181  branch = parseblock(lines[i:i + 3])
1182  result["Branches"][branch["Name"]] = branch
1183  i += 4
1184 
1185  return (result, i)
1186 
1187 
1188 def parseHistosSummary(lines, pos):
1189  """
1190  Extract the histograms infos from the lines starting at pos.
1191  Returns the position of the first line after the summary block.
1192  """
1193  global h_count_re
1194  h_table_head = re.compile(
1195  r'SUCCESS\s+(1D|2D|3D|1D profile|2D profile) histograms in directory\s+"(\w*)"'
1196  )
1197  h_short_summ = re.compile(r"ID=([^\"]+)\s+\"([^\"]+)\"\s+(.*)")
1198 
1199  nlines = len(lines)
1200 
1201  # decode header
1202  m = h_count_re.search(lines[pos])
1203  name = m.group(1).strip()
1204  total = int(m.group(2))
1205  header = {}
1206  for k, v in [x.split("=") for x in m.group(3).split()]:
1207  header[k] = int(v)
1208  pos += 1
1209  header["Total"] = total
1210 
1211  summ = {}
1212  while pos < nlines:
1213  m = h_table_head.search(lines[pos])
1214  if m:
1215  t, d = m.groups(1) # type and directory
1216  t = t.replace(" profile", "Prof")
1217  pos += 1
1218  if pos < nlines:
1219  l = lines[pos]
1220  else:
1221  l = ""
1222  cont = {}
1223  if l.startswith(" | ID"):
1224  # table format
1225  titles = [x.strip() for x in l.split("|")][1:]
1226  pos += 1
1227  while pos < nlines and lines[pos].startswith(" |"):
1228  l = lines[pos]
1229  values = [x.strip() for x in l.split("|")][1:]
1230  hcont = {}
1231  for i in range(len(titles)):
1232  hcont[titles[i]] = values[i]
1233  cont[hcont["ID"]] = hcont
1234  pos += 1
1235  elif l.startswith(" ID="):
1236  while pos < nlines and lines[pos].startswith(" ID="):
1237  values = [
1238  x.strip()
1239  for x in h_short_summ.search(lines[pos]).groups()
1240  ]
1241  cont[values[0]] = values
1242  pos += 1
1243  else: # not interpreted
1244  raise RuntimeError(
1245  "Cannot understand line %d: '%s'" % (pos, l))
1246  if not d in summ:
1247  summ[d] = {}
1248  summ[d][t] = cont
1249  summ[d]["header"] = header
1250  else:
1251  break
1252  if not summ:
1253  # If the full table is not present, we use only the header
1254  summ[name] = {"header": header}
1255  return summ, pos
1256 
1257 
1259  """
1260  Scan stdout to find ROOT TTree summaries and digest them.
1261  """
1262  outlines = stdout.splitlines()
1263  nlines = len(outlines) - 1
1264  summaries = {}
1265  global h_count_re
1266 
1267  pos = 0
1268  while pos < nlines:
1269  summ = {}
1270  # find first line of block:
1271  match = h_count_re.search(outlines[pos])
1272  while pos < nlines and not match:
1273  pos += 1
1274  match = h_count_re.search(outlines[pos])
1275  if match:
1276  summ, pos = parseHistosSummary(outlines, pos)
1277  summaries.update(summ)
1278  return summaries
1279 
1280 
1281 def PlatformIsNotSupported(self, context, result):
1282  platform = GetPlatform(self)
1283  unsupported = [
1284  re.compile(x) for x in [str(y).strip() for y in unsupported_platforms]
1285  if x
1286  ]
1287  for p_re in unsupported:
1288  if p_re.search(platform):
1289  result.SetOutcome(result.UNTESTED)
1290  result[result.CAUSE] = 'Platform not supported.'
1291  return True
1292  return False
1293 
1294 
1295 def GetPlatform(self):
1296  """
1297  Return the platform Id defined in CMTCONFIG or SCRAM_ARCH.
1298  """
1299  arch = "None"
1300  # check architecture name
1301  if "BINARY_TAG" in os.environ:
1302  arch = os.environ["BINARY_TAG"]
1303  elif "CMTCONFIG" in os.environ:
1304  arch = os.environ["CMTCONFIG"]
1305  elif "SCRAM_ARCH" in os.environ:
1306  arch = os.environ["SCRAM_ARCH"]
1307  return arch
1308 
1309 
1310 def isWinPlatform(self):
1311  """
1312  Return True if the current platform is Windows.
1313 
1314  This function was needed because of the change in the CMTCONFIG format,
1315  from win32_vc71_dbg to i686-winxp-vc9-dbg.
1316  """
1317  platform = GetPlatform(self)
1318  return "winxp" in platform or platform.startswith("win")
def dumpProcs(name)
Definition: BaseTest.py:35
def GetPlatform(self)
Definition: BaseTest.py:1295
def PlatformIsNotSupported(self, context, result)
Definition: BaseTest.py:1281
def __init__(self, start, end)
Definition: BaseTest.py:811
def validateWithReference(self, stdout=None, stderr=None, result=None, causes=None, preproc=None)
Definition: BaseTest.py:480
def cmpTreesDicts(reference, to_check, ignore=None)
Definition: BaseTest.py:1086
def ValidateOutput(self, stdout, stderr, result)
Definition: BaseTest.py:274
def read(f, regex='.*', skipevents=0)
Definition: hivetimeline.py:22
def __processLine__(self, line)
Definition: BaseTest.py:869
def findHistosSummaries(stdout)
Definition: BaseTest.py:1258
def _parseTTreeSummary(lines, pos)
Definition: BaseTest.py:1136
struct GAUDI_API map
Parametrisation class for map-like implementation.
def __call__(self, stdout, result)
Definition: BaseTest.py:1035
def __processLine__(self, line)
Definition: BaseTest.py:800
def __init__(self, orig, repl="", when=None)
Definition: BaseTest.py:828
decltype(auto) range(Args &&...args)
Zips multiple containers together to form a single range.
def __init__(self, signature)
Definition: BaseTest.py:865
def sanitize_for_xml(data)
Definition: BaseTest.py:17
def isWinPlatform(self)
Definition: BaseTest.py:1310
def getCmpFailingValues(reference, to_check, fail_path)
Definition: BaseTest.py:1120
def __init__(self, strings=[], regexps=[])
Definition: BaseTest.py:795
def __setitem__(self, key, value)
Definition: BaseTest.py:684
def __init__(self, kind=None, id=None, outcome=PASS, annotations={})
Definition: BaseTest.py:677
def which(executable)
Definition: BaseTest.py:632
def parseHistosSummary(lines, pos)
Definition: BaseTest.py:1188
def _expandReferenceFileName(self, reffile)
Definition: BaseTest.py:561
def findReferenceBlock(self, reference=None, stdout=None, result=None, causes=None, signature_offset=0, signature=None, id=None)
Definition: BaseTest.py:288
def CheckHistosSummaries(self, stdout=None, result=None, causes=None, dict=None, ignore=None)
Definition: BaseTest.py:435
def __init__(self, reffile, cause, result_key, preproc=normalizeExamples)
Definition: BaseTest.py:1029
def __getitem__(self, key)
Definition: BaseTest.py:680
def kill_tree(ppid, sig)
Definition: BaseTest.py:44
def findTTreeSummaries(stdout)
Definition: BaseTest.py:1064
def __init__(self, ref, cause, result_key)
Definition: BaseTest.py:703
def ROOT6WorkAroundEnabled(id=None)
Definition: BaseTest.py:614
def CheckTTreesSummaries(self, stdout=None, result=None, causes=None, trees_dict=None, ignore=r"Basket|.*size|Compression")
Definition: BaseTest.py:390
def Quote(self, string)
Definition: BaseTest.py:689