All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Friends Macros Groups Pages
BaseTest.py
Go to the documentation of this file.
1 # -*- coding: utf-8 -*-
2 
3 import subprocess
4 import time
5 from subprocess import *
6 import os
7 import signal
8 import threading
9 import sys
10 import platform
11 import tempfile
12 import inspect
13 import re
14 import xml.sax.saxutils as XSS
15 
16 #-------------------------------------------------------------------------#
17 class BaseTest :
18  def __init__(self):
19  self.program=''
20  self.args=[]
21  self.reference=''
23  self.options=''
24  self.stderr=''
25  self.timeout=120
26  self.exit_code=None
27  self.environment=None
28  self.target='local'
29  self.traceback=True
31  self.signal=None
32  #Variables not for users
33  self.status=None
34  self.name=''
35  self.causes = []
36  self.timeOut=False
37  self.result = Result(self)
38  self.returnedCode=0
39  self.out = ''
40  self.err = ''
41  self.proc=None
42 
43  def validator(self, stdout='',stderr=''):
44  pass
45 
46  def runTest (self):
47  #Setting time
48  begining = int(time.time())
49  DOB = time.localtime()
50  dateOfBegining = str(DOB[1])+"/"+str(DOB[2])+"/"+str(DOB[0])+" "+str(DOB[3])+":"+str(DOB[4])+":"+str(DOB[5])
51 
52 
53  if self.options != '' :
54  if self.options.startswith("import") or self.options.startswith("from") or self.options.startswith("\nfrom") or self.options.startswith("\nimport") or (self.options.startswith("\n#") and not self.options.startswith('\n#include')):
55  optionFile = tempfile.NamedTemporaryFile(suffix='.py')
56  else :
57  optionFile = tempfile.NamedTemporaryFile(suffix='.opts')
58  optionFile.file.write(self.options)
59  optionFile.seek(0)
60  self.args.append(RationalizePath(optionFile.name))
61 
62  #If not specified, setting the environment
63  if self.environment is None : self.environment = os.environ
64  else : self.environment=dict(self.environment.items()+os.environ.items())
65 
66  #launching test in a different thread to handle timeout exception
67  def run (self=self):
68  def target (self=self) :
69  prog=''
70  if self.program != '' :
71  prog = RationalizePath(self.program)
72  elif "GAUDIEXE" in os.environ :
73  prog = os.environ["GAUDIEXE"]
74  else :
75  prog = "Gaudi.exe"
76 
77  dummy, prog_ext = os.path.splitext(prog)
78  if prog_ext not in [ ".exe", ".py", ".bat" ]:
79  prog += ".exe"
80  prog_ext = ".exe"
81 
82  prog = which(prog) or prog
83 
84  if prog_ext == ".py" :
85  params = ["/afs/cern.ch/user/v/valentin/workspace/Gaudi/build.x86_64-slc6-gcc48-opt/run","/afs/cern.ch/sw/lcg/external/Python/2.7.3/x86_64-slc6-gcc48-opt/bin/python",RationalizePath(prog)]+self.args
86  else :
87  params = ["/afs/cern.ch/user/v/valentin/workspace/Gaudi/build.x86_64-slc6-gcc48-opt/run", RationalizePath(prog)]+self.args
88 
89  print self.name
90  print params
91  self.proc= Popen(params,stdout=PIPE,stderr=PIPE, env=self.environment)
92  self.proc.wait()
93 
94  thread = threading.Thread(target=target)
95  thread.start()
96  #catching timeout
97  thread.join(self.timeout)
98 
99  if thread.is_alive():
100  self.proc.send_signal(signal.SIGABRT)
101  print "abortion"
102  thread.join()
103  self.timeOut = True
104  self.out = self.proc.stdout.read()
105  if self.traceback:
106  self.err = self.proc.stderr.read()
107  #Getting the error code
108  self.returnedCode = self.proc.returncode
109 
110  unsupPlat = True
111  for p in self.unsupported_platforms :
112  if re.search(p,platform.platform()):
113  unsupPlat=False
114 
115  if unsupPlat :
116  run()
117 
118  #End time
119  end=time.time()
120  #Time lasted
121  lasted = end-begining
122  #Getting date and local time
123  DOE = time.localtime()
124  dateOfEnding = str(DOE[1])+"/"+str(DOE[2])+"/"+str(DOE[0])+" "+str(DOE[3])+":"+str(DOE[4])+":"+str(DOE[5])
125 
126  if unsupPlat :
127  validatorRes = Result({'CAUSE':None,'EXCEPTION':None,'RESOURCE':None,'TARGET':None,'TRACEBACK':None,'START_TIME':None,'END_TIME':None,'TIMEOUT_DETAIL':None})
128  self.result=validatorRes
129 
130  if self.timeOut:
131  self.result["Abort cause"]=self.result.Quote("Event Timeout")
132 
133  self.result,self.causes=self.ValidateOutput(stdout=self.out,stderr=self.err,result=validatorRes)
134 
135  #Setting status
136  self.status = "Failed"
137  if self.signal is not None :
138  if (int(self.returnedCode) - int(self.signal) - 128)!=0:
139  self.causes.append('wrong return code')
140  if self.exit_code is not None:
141  if int(self.returnedCode) != int(self.exit_code) :
142  self.causes.append('wrong return code')
143  if self.returnedCode!=0 and self.exit_code is None and self.signal is None:
144  self.causes.append("Return code !=0")
145  if self.causes == []:
146  self.status = "Passed"
147  else :
148  self.status="SKIPPED"
149 
150  resultDic = {'Execution Time':lasted, 'Exit code':self.returnedCode, 'Start Time':dateOfBegining, 'End Time':dateOfEnding, 'Stderr':self.err,'Arguments':self.args, 'Environment':self.environment, 'Expected stderr':self.stderr, 'Status':self.status, 'Measurement':self.out, 'Program Name':self.program,'Name':self.name, 'Validator':self.validator,'Reference file':self.reference,'Error reference file':self.error_reference,'Causes':self.causes,'Validator results':self.result.annotations,'Unsupported platforms':self.unsupported_platforms}
151  return resultDic
152 
153 
154  #-------------------------------------------------#
155  #----------------Validating tool------------------#
156  #-------------------------------------------------#
157 
158  def ValidateOutput(self, stdout, stderr, result):
159  causes = self.causes
160  reference =self.reference
161  error_reference=self.error_reference
162  #checking if default validation or not
163  if inspect.getsource(self.validator)!=""" def validator(self, stdout='',stderr=''):
164  pass
165 """ :
166  self.validator(stdout, stderr, result, causes, reference, error_reference)
167  else:
168  if self.stderr=='':
169  self.validateWithReference(stdout, stderr, result, causes)
170  elif stderr!=self.stderr:
171  self.causes.append("DIFFERENT STDERR THAN EXPECTED")
172 
173 
174  return result,causes
175 
176 
177 
178  def findReferenceBlock(self,reference=None, stdout=None, result=None, causes=None, signature_offset=0, signature=None, id = None):
179  """
180  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.
181  """
182 
183  if reference is None : reference=self.reference
184  if stdout is None : stdout=self.out
185  if result is None : result=self.result
186  if causes is None : causes=self.causes
187 
188  reflines = filter(None,map(lambda s: s.rstrip(), reference.splitlines()))
189  if not reflines:
190  raise RuntimeError("Empty (or null) reference")
191  # the same on standard output
192  outlines = filter(None,map(lambda s: s.rstrip(), stdout.splitlines()))
193 
194  res_field = "GaudiTest.RefBlock"
195  if id:
196  res_field += "_%s" % id
197 
198  if signature is None:
199  if signature_offset < 0:
200  signature_offset = len(reference)+signature_offset
201  signature = reflines[signature_offset]
202  # find the reference block in the output file
203  try:
204  pos = outlines.index(signature)
205  outlines = outlines[pos-signature_offset:pos+len(reflines)-signature_offset]
206  if reflines != outlines:
207  msg = "standard output"
208  # I do not want 2 messages in causes if teh function is called twice
209  if not msg in causes:
210  causes.append(msg)
211  result[res_field + ".observed"] = result.Quote("\n".join(outlines))
212  except ValueError:
213  causes.append("missing signature")
214  result[res_field + ".signature"] = result.Quote(signature)
215  if len(reflines) > 1 or signature != reflines[0]:
216  result[res_field + ".expected"] = result.Quote("\n".join(reflines))
217  return causes
218 
219  def countErrorLines(self, expected = {'ERROR':0, 'FATAL':0}, stdout=None, result=None,causes=None):
220  """
221  Count the number of messages with required severity (by default ERROR and FATAL)
222  and check if their numbers match the expected ones (0 by default).
223  The dictionary "expected" can be used to tune the number of errors and fatals
224  allowed, or to limit the number of expected warnings etc.
225  """
226 
227  if stdout is None : stdout=self.out
228  if result is None : result=self.result
229  if causes is None : causes=self.causes
230 
231  # prepare the dictionary to record the extracted lines
232  errors = {}
233  for sev in expected:
234  errors[sev] = []
235 
236  outlines = stdout.splitlines()
237  from math import log10
238  fmt = "%%%dd - %%s" % (int(log10(len(outlines)+1)))
239 
240  linecount = 0
241  for l in outlines:
242  linecount += 1
243  words = l.split()
244  if len(words) >= 2 and words[1] in errors:
245  errors[words[1]].append(fmt%(linecount,l.rstrip()))
246 
247  for e in errors:
248  if len(errors[e]) != expected[e]:
249  causes.append('%s(%d)'%(e,len(errors[e])))
250  result["GaudiTest.lines.%s"%e] = result.Quote('\n'.join(errors[e]))
251  result["GaudiTest.lines.%s.expected#"%e] = result.Quote(str(expected[e]))
252 
253  return causes
254 
255  def CheckTTreesSummaries(self, stdout=None, result=None, causes=None,
256  trees_dict = None,
257  ignore = r"Basket|.*size|Compression"):
258  """
259  Compare the TTree summaries in stdout with the ones in trees_dict or in
260  the reference file. By default ignore the size, compression and basket
261  fields.
262  The presence of TTree summaries when none is expected is not a failure.
263  """
264  if stdout is None : stdout=self.out
265  if result is None : result=self.result
266  if causes is None : causes=self.causes
267  if trees_dict is None:
268  lreference = _expandReferenceFileName(self,self.reference)
269  # call the validator if the file exists
270  if lreference and os.path.isfile(lreference):
271  trees_dict = findTTreeSummaries(open(lreference).read())
272  else:
273  trees_dict = {}
274 
275  from pprint import PrettyPrinter
276  pp = PrettyPrinter()
277  if trees_dict:
278  result["GaudiTest.TTrees.expected"] = result.Quote(pp.pformat(trees_dict))
279  if ignore:
280  result["GaudiTest.TTrees.ignore"] = result.Quote(ignore)
281 
282  trees = findTTreeSummaries(stdout)
283  failed = cmpTreesDicts(trees_dict, trees, ignore)
284  if failed:
285  causes.append("trees summaries")
286  msg = "%s: %s != %s" % getCmpFailingValues(trees_dict, trees, failed)
287  result["GaudiTest.TTrees.failure_on"] = result.Quote(msg)
288  result["GaudiTest.TTrees.found"] = result.Quote(pp.pformat(trees))
289 
290  return causes
291 
292  def CheckHistosSummaries(self, stdout=None, result=None, causes=None,
293  dict = None,
294  ignore = None):
295  """
296  Compare the TTree summaries in stdout with the ones in trees_dict or in
297  the reference file. By default ignore the size, compression and basket
298  fields.
299  The presence of TTree summaries when none is expected is not a failure.
300  """
301  if stdout is None : stdout=self.out
302  if result is None : result=self.result
303  if causes is None : causes=self.causes
304 
305  if dict is None:
306  lreference = _expandReferenceFileName(self,self.reference)
307  # call the validator if the file exists
308  if lreference and os.path.isfile(lreference):
309  dict = findHistosSummaries(open(lreference).read())
310  else:
311  dict = {}
312 
313  from pprint import PrettyPrinter
314  pp = PrettyPrinter()
315  if dict:
316  result["GaudiTest.Histos.expected"] = result.Quote(pp.pformat(dict))
317  if ignore:
318  result["GaudiTest.Histos.ignore"] = result.Quote(ignore)
319 
320  histos = findHistosSummaries(stdout)
321  failed = cmpTreesDicts(dict, histos, ignore)
322  if failed:
323  causes.append("histos summaries")
324  msg = "%s: %s != %s" % getCmpFailingValues(dict, histos, failed)
325  result["GaudiTest.Histos.failure_on"] = result.Quote(msg)
326  result["GaudiTest.Histos.found"] = result.Quote(pp.pformat(histos))
327 
328  return causes
329 
330  def validateWithReference(self, stdout=None, stderr=None, result=None, causes=None, preproc = None):
331  """
332  Default validation action: compare standard output and error to the
333  reference files.
334  """
335 
336  if stdout is None : stdout=self.out
337  if stderr is None : stderr=self.err
338  if result is None : result=self.result
339  if causes is None : causes=self.causes
340 
341  # set the default output preprocessor
342  if preproc is None:
343  preproc = normalizeExamples
344  # check standard output
345  lreference = _expandReferenceFileName(self,self.reference)
346  # call the validator if the file exists
347  if lreference and os.path.isfile(lreference):
348  result["GaudiTest.output_reference"] = lreference
349  causes += ReferenceFileValidator(lreference,
350  "standard output",
351  "GaudiTest.output_diff",
352  preproc = preproc)(stdout, result)
353  # Compare TTree summaries
354  causes = self.CheckTTreesSummaries(stdout, result, causes)
355  causes = self.CheckHistosSummaries(stdout, result, causes)
356  if causes: # Write a new reference file for stdout
357  try:
358  newref = open(lreference + ".new","w")
359  # sanitize newlines
360  for l in stdout.splitlines():
361  newref.write(l.rstrip() + '\n')
362  del newref # flush and close
363  except IOError:
364  # Ignore IO errors when trying to update reference files
365  # because we may be in a read-only filesystem
366  pass
367 
368  # check standard error
369  lreference = _expandReferenceFileName(self,self.error_reference)
370  # call the validator if we have a file to use
371  if lreference and os.path.isfile(self.error_reference):
372  result["GaudiTest.error_reference"] = self.error_reference
373  newcauses = ReferenceFileValidator(lreference, "standard error", "GaudiTest.error_diff", preproc = preproc)(stderr, result)
374  causes += newcauses
375  if newcauses: # Write a new reference file for stdedd
376  newref = open(self.reference + ".new","w")
377  # sanitize newlines
378  for l in stderr.splitlines():
379  newref.write(l.rstrip() + '\n')
380  del newref # flush and close
381  else:
382  causes += BasicOutputValidator(lreference, "standard error", "ExecTest.expected_stderr")(stderr, result)
383  return causes
384 
385 #---------------------------------------------------------------------------------------------------#
386 #---------------------------------------------------------------------------------------------------#
387 #-----------------------------------------GAUDI TOOLS-----------------------------------------------------#
388 #---------------------------------------------------------------------------------------------------#
389 #---------------------------------------------------------------------------------------------------#
390 
391 import os
392 import shutil
393 import string
394 import difflib
395 import calendar
396 from subprocess import Popen, PIPE, STDOUT
397 import re
398 import xml.etree.ElementTree as ET
399 
400 try:
401  from GaudiKernel import ROOT6WorkAroundEnabled
402 except ImportError:
404  # dummy implementation
405  return False
406 
407 if sys.platform == "win32":
408  import msvcrt
409  import pywintypes
410  from threading import *
411  import win32api
412  import win32con
413  import win32event
414  import win32file
415  import win32pipe
416  import win32process
417 else:
418  import cPickle
419  import fcntl
420  import select
421 
422 
423 #--------------------------------- TOOLS ---------------------------------#
424 
426  """
427  Function used to normalize the used path
428  """
429  newPath = os.path.normpath(os.path.expandvars(p))
430  if os.path.exists(newPath) :
431  p = os.path.realpath(newPath)
432  p = os.path.realpath(newPath)
433  return p
434 
435 
436 def which(executable):
437  """
438  Locates an executable in the executables path ($PATH) and returns the full
439  path to it. An application is looked for with or without the '.exe' suffix.
440  If the executable cannot be found, None is returned
441  """
442  if os.path.isabs(executable):
443  if not os.path.exists(executable):
444  if executable.endswith('.exe'):
445  if os.path.exists(executable[:-4]):
446  return executable[:-4]
447  else :
448  head,executable = os.path.split(executable)
449  else :
450  return executable
451  for d in os.environ.get("PATH").split(os.pathsep):
452  fullpath = os.path.join(d, executable)
453  if os.path.exists(fullpath):
454  return fullpath
455  if executable.endswith('.exe'):
456  return which(executable[:-4])
457  return None
458 
459 
460 
461 #-------------------------------------------------------------------------#
462 #----------------------------- Result Classe -----------------------------#
463 #-------------------------------------------------------------------------#
464 import types
465 
466 class Result:
467 
468  PASS='PASS'
469  FAIL='FAIL'
470  ERROR='ERROR'
471  UNTESTED='UNTESTED'
472 
473  EXCEPTION = ""
474  RESOURCE = ""
475  TARGET = ""
476  TRACEBACK = ""
477  START_TIME = ""
478  END_TIME = ""
479  TIMEOUT_DETAIL = ""
480 
481  def __init__(self,kind=None,id=None,outcome=PASS,annotations={}):
482  self.annotations = annotations.copy()
483 
484  def __getitem__(self,key):
485  assert type(key) in types.StringTypes
486  return self.annotations[key]
487 
488  def __setitem__(self,key,value):
489  assert type(key) in types.StringTypes
490  assert type(value) in types.StringTypes
491  self.annotations[key]=value
492 
493  def Quote(self,string):
494  return string
495 
496 
497 #-------------------------------------------------------------------------#
498 #--------------------------- Validator Classes ---------------------------#
499 #-------------------------------------------------------------------------#
500 
501 #Basic implementation of an option validator for Gaudi test. This implementation is based on the standard (LCG) validation functions used in QMTest.
502 
503 
505 
506  def __init__(self,ref,cause,result_key):
507  self.ref=ref
508  self.cause=cause
509  self.result_key=result_key
510 
511  def __call__(self,out,result):
512  """Validate the output of the program.
513  'stdout' -- A string containing the data written to the standard output
514  stream.
515  'stderr' -- A string containing the data written to the standard error
516  stream.
517  'result' -- A 'Result' object. It may be used to annotate
518  the outcome according to the content of stderr.
519  returns -- A list of strings giving causes of failure."""
520 
521  causes=[]
522  #Check the output
523  if not self.__CompareText(out,self.ref):
524  causes.append(self.cause)
525  result[self.result_key] =result.Quote(self.ref)
526 
527 
528 
529  return causes
530 
531  def __CompareText(self, s1, s2):
532  """Compare 's1' and 's2', ignoring line endings.
533  's1' -- A string.
534  's2' -- A string.
535  returns -- True if 's1' and 's2' are the same, ignoring
536  differences in line endings."""
537  if ROOT6WorkAroundEnabled('ReadRootmapCheck'):
538  # FIXME: (MCl) Hide warnings from new rootmap sanity check until we can fix them
539  to_ignore = re.compile(r'Warning in <TInterpreter::ReadRootmapFile>: .* is already in .*')
540  keep_line = lambda l: not to_ignore.match(l)
541  return filter(keep_line, s1.splitlines()) == filter(keep_line, s2.splitlines())
542  else:
543  return s1.splitlines() == s2.splitlines()
544 
545 
546 
547 #------------------------ Preprocessor elements ------------------------#
549  """ Base class for a callable that takes a file and returns a modified
550  version of it."""
551  def __processLine__(self, line):
552  return line
553  def __call__(self, input):
554  if hasattr(input,"__iter__"):
555  lines = input
556  mergeback = False
557  else:
558  lines = input.splitlines()
559  mergeback = True
560  output = []
561  for l in lines:
562  l = self.__processLine__(l)
563  if l: output.append(l)
564  if mergeback: output = '\n'.join(output)
565  return output
566  def __add__(self, rhs):
567  return FilePreprocessorSequence([self,rhs])
568 
570  def __init__(self, members = []):
571  self.members = members
572  def __add__(self, rhs):
573  return FilePreprocessorSequence(self.members + [rhs])
574  def __call__(self, input):
575  output = input
576  for pp in self.members:
577  output = pp(output)
578  return output
579 
581  def __init__(self, strings = [], regexps = []):
582  import re
583  self.strings = strings
584  self.regexps = map(re.compile,regexps)
585 
586  def __processLine__(self, line):
587  for s in self.strings:
588  if line.find(s) >= 0: return None
589  for r in self.regexps:
590  if r.search(line): return None
591  return line
592 
594  def __init__(self, start, end):
595  self.start = start
596  self.end = end
597  self._skipping = False
598 
599  def __processLine__(self, line):
600  if self.start in line:
601  self._skipping = True
602  return None
603  elif self.end in line:
604  self._skipping = False
605  elif self._skipping:
606  return None
607  return line
608 
610  def __init__(self, orig, repl = "", when = None):
611  if when:
612  when = re.compile(when)
613  self._operations = [ (when, re.compile(orig), repl) ]
614  def __add__(self,rhs):
615  if isinstance(rhs, RegexpReplacer):
616  res = RegexpReplacer("","",None)
617  res._operations = self._operations + rhs._operations
618  else:
619  res = FilePreprocessor.__add__(self, rhs)
620  return res
621  def __processLine__(self, line):
622  for w,o,r in self._operations:
623  if w is None or w.search(line):
624  line = o.sub(r, line)
625  return line
626 
627 # Common preprocessors
628 maskPointers = RegexpReplacer("0x[0-9a-fA-F]{4,16}","0x########")
629 normalizeDate = RegexpReplacer("[0-2]?[0-9]:[0-5][0-9]:[0-5][0-9] [0-9]{4}[-/][01][0-9][-/][0-3][0-9] *(CES?T)?",
630  "00:00:00 1970-01-01")
631 normalizeEOL = FilePreprocessor()
632 normalizeEOL.__processLine__ = lambda line: str(line).rstrip() + '\n'
633 
634 skipEmptyLines = FilePreprocessor()
635 # FIXME: that's ugly
636 skipEmptyLines.__processLine__ = lambda line: (line.strip() and line) or None
637 
638 ## Special preprocessor sorting the list of strings (whitespace separated)
639 # that follow a signature on a single line
641  def __init__(self, signature):
642  self.signature = signature
643  self.siglen = len(signature)
644  def __processLine__(self, line):
645  pos = line.find(self.signature)
646  if pos >=0:
647  line = line[:(pos+self.siglen)]
648  lst = line[(pos+self.siglen):].split()
649  lst.sort()
650  line += " ".join(lst)
651  return line
652 
653 # Preprocessors for GaudiExamples
654 normalizeExamples = maskPointers + normalizeDate
655 for w,o,r in [
656  #("TIMER.TIMER",r"[0-9]", "0"), # Normalize time output
657  ("TIMER.TIMER",r"\s+[+-]?[0-9]+[0-9.]*", " 0"), # Normalize time output
658  ("release all pending",r"^.*/([^/]*:.*)",r"\1"),
659  ("0x########",r"\[.*/([^/]*.*)\]",r"[\1]"),
660  ("^#.*file",r"file '.*[/\\]([^/\\]*)$",r"file '\1"),
661  ("^JobOptionsSvc.*options successfully read in from",r"read in from .*[/\\]([^/\\]*)$",r"file \1"), # normalize path to options
662  # Normalize UUID, except those ending with all 0s (i.e. the class IDs)
663  (None,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}","00000000-0000-0000-0000-000000000000"),
664  # Absorb a change in ServiceLocatorHelper
665  ("ServiceLocatorHelper::", "ServiceLocatorHelper::(create|locate)Service", "ServiceLocatorHelper::service"),
666  # Remove the leading 0 in Windows' exponential format
667  (None, r"e([-+])0([0-9][0-9])", r"e\1\2"),
668  # Output line changed in Gaudi v24
669  (None, r'Service reference count check:', r'Looping over all active services...'),
670  ]: #[ ("TIMER.TIMER","[0-9]+[0-9.]*", "") ]
671  normalizeExamples += RegexpReplacer(o,r,w)
672 
673 lineSkipper = LineSkipper(["//GP:",
674  "JobOptionsSvc INFO # ",
675  "JobOptionsSvc WARNING # ",
676  "Time User",
677  "Welcome to",
678  "This machine has a speed",
679  "TIME:",
680  "running on",
681  "ToolSvc.Sequenc... INFO",
682  "DataListenerSvc INFO XML written to file:",
683  "[INFO]","[WARNING]",
684  "DEBUG No writable file catalog found which contains FID:",
685  "0 local", # hack for ErrorLogExample
686  "DEBUG Service base class initialized successfully", # changed between v20 and v21
687  "DEBUG Incident timing:", # introduced with patch #3487
688  "INFO 'CnvServices':[", # changed the level of the message from INFO to DEBUG
689  # The signal handler complains about SIGXCPU not defined on some platforms
690  'SIGXCPU',
691  ],regexps = [
692  r"^JobOptionsSvc INFO *$",
693  r"^#", # Ignore python comments
694  r"(Always|SUCCESS)\s*(Root f|[^ ]* F)ile version:", # skip the message reporting the version of the root file
695  r"0x[0-9a-fA-F#]+ *Algorithm::sysInitialize\(\) *\[", # hack for ErrorLogExample
696  r"0x[0-9a-fA-F#]* *__gxx_personality_v0 *\[", # hack for ErrorLogExample
697  r"File '.*.xml' does not exist",
698  r"INFO Refer to dataset .* by its file ID:",
699  r"INFO Referring to dataset .* by its file ID:",
700  r"INFO Disconnect from dataset",
701  r"INFO Disconnected from dataset",
702  r"INFO Disconnected data IO:",
703  r"IncidentSvc\s*(DEBUG (Adding|Removing)|VERBOSE Calling)",
704  # I want to ignore the header of the unchecked StatusCode report
705  r"^StatusCodeSvc.*listing all unchecked return codes:",
706  r"^StatusCodeSvc\s*INFO\s*$",
707  r"Num\s*\|\s*Function\s*\|\s*Source Library",
708  r"^[-+]*\s*$",
709  # Hide the fake error message coming from POOL/ROOT (ROOT 5.21)
710  r"ERROR Failed to modify file: .* Errno=2 No such file or directory",
711  # Hide unckeched StatusCodes from dictionaries
712  r"^ +[0-9]+ \|.*ROOT",
713  r"^ +[0-9]+ \|.*\|.*Dict",
714  # Remove ROOT TTree summary table, which changes from one version to the other
715  r"^\*.*\*$",
716  # Remove Histos Summaries
717  r"SUCCESS\s*Booked \d+ Histogram\(s\)",
718  r"^ \|",
719  r"^ ID=",
720  ] )
721 
722 if ROOT6WorkAroundEnabled('ReadRootmapCheck'):
723  # FIXME: (MCl) Hide warnings from new rootmap sanity check until we can fix them
724  lineSkipper += LineSkipper(regexps = [
725  r'Warning in <TInterpreter::ReadRootmapFile>: .* is already in .*',
726  ])
727 
728 normalizeExamples = (lineSkipper + normalizeExamples + skipEmptyLines +
729  normalizeEOL + LineSorter("Services to release : "))
730 
731 
732 #--------------------- Validation functions/classes ---------------------#
733 
735  def __init__(self,reffile, cause, result_key, preproc=normalizeExamples):
736  self.reffile = os.path.expandvars(reffile)
737  self.cause=cause
738  self.result_key = result_key
739  self.preproc = preproc
740 
741  def __call__(self,stdout, result) :
742  causes=[]
743  if os.path.isfile(self.reffile):
744  orig=open(self.reffile).xreadlines()
745  if self.preproc:
746  orig = self.preproc(orig)
747  else:
748  orig = []
749  new = stdout.splitlines()
750  if self.preproc:
751  new = self.preproc(new)
752 
753  diffs = difflib.ndiff(orig,new,charjunk=difflib.IS_CHARACTER_JUNK)
754  filterdiffs = map(lambda x: x.strip(),filter(lambda x: x[0] != " ",diffs))
755  if filterdiffs:
756  result[self.result_key] = result.Quote("\n".join(filterdiffs))
757  result[self.result_key] += result.Quote("""
758  Legend:
759  -) reference file
760  +) standard output of the test""")
761  causes.append(self.cause)
762  return causes
763 
764 def findTTreeSummaries(stdout):
765  """
766  Scan stdout to find ROOT TTree summaries and digest them.
767  """
768  stars = re.compile(r"^\*+$")
769  outlines = stdout.splitlines()
770  nlines = len(outlines)
771  trees = {}
772 
773  i = 0
774  while i < nlines: #loop over the output
775  # look for
776  while i < nlines and not stars.match(outlines[i]):
777  i += 1
778  if i < nlines:
779  tree, i = _parseTTreeSummary(outlines, i)
780  if tree:
781  trees[tree["Name"]] = tree
782 
783  return trees
784 
785 def cmpTreesDicts(reference, to_check, ignore = None):
786  """
787  Check that all the keys in reference are in to_check too, with the same value.
788  If the value is a dict, the function is called recursively. to_check can
789  contain more keys than reference, that will not be tested.
790  The function returns at the first difference found.
791  """
792  fail_keys = []
793  # filter the keys in the reference dictionary
794  if ignore:
795  ignore_re = re.compile(ignore)
796  keys = [ key for key in reference if not ignore_re.match(key) ]
797  else:
798  keys = reference.keys()
799  # loop over the keys (not ignored) in the reference dictionary
800  for k in keys:
801  if k in to_check: # the key must be in the dictionary to_check
802  if (type(reference[k]) is dict) and (type(to_check[k]) is dict):
803  # if both reference and to_check values are dictionaries, recurse
804  failed = fail_keys = cmpTreesDicts(reference[k], to_check[k], ignore)
805  else:
806  # compare the two values
807  failed = to_check[k] != reference[k]
808  else: # handle missing keys in the dictionary to check (i.e. failure)
809  to_check[k] = None
810  failed = True
811  if failed:
812  fail_keys.insert(0, k)
813  break # exit from the loop at the first failure
814  return fail_keys # return the list of keys bringing to the different values
815 
816 def getCmpFailingValues(reference, to_check, fail_path):
817  c = to_check
818  r = reference
819  for k in fail_path:
820  c = c.get(k,None)
821  r = r.get(k,None)
822  if c is None or r is None:
823  break # one of the dictionaries is not deep enough
824  return (fail_path, r, c)
825 
826 # signature of the print-out of the histograms
827 h_count_re = re.compile(r"^(.*)SUCCESS\s+Booked (\d+) Histogram\(s\) :\s+(.*)")
828 
829 
830 def _parseTTreeSummary(lines, pos):
831  """
832  Parse the TTree summary table in lines, starting from pos.
833  Returns a tuple with the dictionary with the digested informations and the
834  position of the first line after the summary.
835  """
836  result = {}
837  i = pos + 1 # first line is a sequence of '*'
838  count = len(lines)
839 
840  splitcols = lambda l: [ f.strip() for f in l.strip("*\n").split(':',2) ]
841  def parseblock(ll):
842  r = {}
843  cols = splitcols(ll[0])
844  r["Name"], r["Title"] = cols[1:]
845 
846  cols = splitcols(ll[1])
847  r["Entries"] = int(cols[1])
848 
849  sizes = cols[2].split()
850  r["Total size"] = int(sizes[2])
851  if sizes[-1] == "memory":
852  r["File size"] = 0
853  else:
854  r["File size"] = int(sizes[-1])
855 
856  cols = splitcols(ll[2])
857  sizes = cols[2].split()
858  if cols[0] == "Baskets":
859  r["Baskets"] = int(cols[1])
860  r["Basket size"] = int(sizes[2])
861  r["Compression"] = float(sizes[-1])
862  return r
863 
864  if i < (count - 3) and lines[i].startswith("*Tree"):
865  result = parseblock(lines[i:i+3])
866  result["Branches"] = {}
867  i += 4
868  while i < (count - 3) and lines[i].startswith("*Br"):
869  if i < (count - 2) and lines[i].startswith("*Branch "):
870  # skip branch header
871  i += 3
872  continue
873  branch = parseblock(lines[i:i+3])
874  result["Branches"][branch["Name"]] = branch
875  i += 4
876 
877  return (result, i)
878 
879 def parseHistosSummary(lines, pos):
880  """
881  Extract the histograms infos from the lines starting at pos.
882  Returns the position of the first line after the summary block.
883  """
884  global h_count_re
885  h_table_head = re.compile(r'SUCCESS\s+List of booked (1D|2D|3D|1D profile|2D profile) histograms in directory\s+"(\w*)"')
886  h_short_summ = re.compile(r"ID=([^\"]+)\s+\"([^\"]+)\"\s+(.*)")
887 
888  nlines = len(lines)
889 
890  # decode header
891  m = h_count_re.search(lines[pos])
892  name = m.group(1).strip()
893  total = int(m.group(2))
894  header = {}
895  for k, v in [ x.split("=") for x in m.group(3).split() ]:
896  header[k] = int(v)
897  pos += 1
898  header["Total"] = total
899 
900  summ = {}
901  while pos < nlines:
902  m = h_table_head.search(lines[pos])
903  if m:
904  t, d = m.groups(1) # type and directory
905  t = t.replace(" profile", "Prof")
906  pos += 1
907  if pos < nlines:
908  l = lines[pos]
909  else:
910  l = ""
911  cont = {}
912  if l.startswith(" | ID"):
913  # table format
914  titles = [ x.strip() for x in l.split("|")][1:]
915  pos += 1
916  while pos < nlines and lines[pos].startswith(" |"):
917  l = lines[pos]
918  values = [ x.strip() for x in l.split("|")][1:]
919  hcont = {}
920  for i in range(len(titles)):
921  hcont[titles[i]] = values[i]
922  cont[hcont["ID"]] = hcont
923  pos += 1
924  elif l.startswith(" ID="):
925  while pos < nlines and lines[pos].startswith(" ID="):
926  values = [ x.strip() for x in h_short_summ.search(lines[pos]).groups() ]
927  cont[values[0]] = values
928  pos += 1
929  else: # not interpreted
930  raise RuntimeError("Cannot understand line %d: '%s'" % (pos, l))
931  if not d in summ:
932  summ[d] = {}
933  summ[d][t] = cont
934  summ[d]["header"] = header
935  else:
936  break
937  if not summ:
938  # If the full table is not present, we use only the header
939  summ[name] = {"header": header}
940  return summ, pos
941 
942 
943 
945  """
946  Scan stdout to find ROOT TTree summaries and digest them.
947  """
948  outlines = stdout.splitlines()
949  nlines = len(outlines) - 1
950  summaries = {}
951  global h_count_re
952 
953  pos = 0
954  while pos < nlines:
955  summ = {}
956  # find first line of block:
957  match = h_count_re.search(outlines[pos])
958  while pos < nlines and not match:
959  pos += 1
960  match = h_count_re.search(outlines[pos])
961  if match:
962  summ, pos = parseHistosSummary(outlines, pos)
963  summaries.update(summ)
964  return summaries
965 
966 def PlatformIsNotSupported(self, context, result):
967  platform = GetPlatform(self)
968  unsupported = [ re.compile(x) for x in [ str(y).strip() for y in unsupported_platforms ] if x]
969  for p_re in unsupported :
970  if p_re.search(platform):
971  result.SetOutcome(result.UNTESTED)
972  result[result.CAUSE] = 'Platform not supported.'
973  return True
974  return False
975 
976 def GetPlatform(self):
977  """
978  Return the platform Id defined in CMTCONFIG or SCRAM_ARCH.
979  """
980  arch = "None"
981  # check architecture name
982  if "CMTCONFIG" in os.environ:
983  arch = os.environ["CMTCONFIG"]
984  elif "SCRAM_ARCH" in os.environ:
985  arch = os.environ["SCRAM_ARCH"]
986  return arch
987 
988 def isWinPlatform(self):
989  """
990  Return True if the current platform is Windows.
991 
992  This function was needed because of the change in the CMTCONFIG format,
993  from win32_vc71_dbg to i686-winxp-vc9-dbg.
994  """
995  platform = GetPlatform(self)
996  return "winxp" in platform or platform.startswith("win")
997 
998 
999 def _expandReferenceFileName(self, reffile):
1000  # if no file is passed, do nothing
1001  if not reffile:
1002  return ""
1003 
1004  # function to split an extension in constituents parts
1005  platformSplit = lambda p: set(p.split('-' in p and '-' or '_'))
1006 
1007  reference = os.path.normpath(os.path.expandvars(reffile))
1008  # old-style platform-specific reference name
1009  spec_ref = reference[:-3] + GetPlatform(self)[0:3] + reference[-3:]
1010  if os.path.isfile(spec_ref):
1011  reference = spec_ref
1012  else: # look for new-style platform specific reference files:
1013  # get all the files whose name start with the reference filename
1014  dirname, basename = os.path.split(reference)
1015  if not dirname: dirname = '.'
1016  head = basename + "."
1017  head_len = len(head)
1018  platform = platformSplit(GetPlatform(self))
1019  candidates = []
1020  for f in os.listdir(dirname):
1021  if f.startswith(head):
1022  req_plat = platformSplit(f[head_len:])
1023  if platform.issuperset(req_plat):
1024  candidates.append( (len(req_plat), f) )
1025  if candidates: # take the one with highest matching
1026  # FIXME: it is not possible to say if x86_64-slc5-gcc43-dbg
1027  # has to use ref.x86_64-gcc43 or ref.slc5-dbg
1028  candidates.sort()
1029  reference = os.path.join(dirname, candidates[-1][1])
1030  return reference
1031 
1032 
1033 #-------------------------------------------------------------------------#
def ROOT6WorkAroundEnabled
Definition: BaseTest.py:403
def isWinPlatform
Definition: BaseTest.py:988
def validateWithReference
Definition: BaseTest.py:330
def PlatformIsNotSupported
Definition: BaseTest.py:966
def findTTreeSummaries
Definition: BaseTest.py:764
def which
Definition: BaseTest.py:436
def _parseTTreeSummary
Definition: BaseTest.py:830
string type
Definition: gaudirun.py:126
struct GAUDI_API map
Parametrisation class for map-like implementation.
Special preprocessor sorting the list of strings (whitespace separated) that follow a signature on a ...
Definition: BaseTest.py:640
def RationalizePath
Definition: BaseTest.py:425
def _expandReferenceFileName
Definition: BaseTest.py:999
def GetPlatform
Definition: BaseTest.py:976
def CheckTTreesSummaries
Definition: BaseTest.py:257
def CheckHistosSummaries
Definition: BaseTest.py:294
def findReferenceBlock
Definition: BaseTest.py:178
NamedRange_< CONTAINER > range(const CONTAINER &cnt, const std::string &name)
simple function to create the named range form arbitrary container
Definition: NamedRange.h:133
def getCmpFailingValues
Definition: BaseTest.py:816
def parseHistosSummary
Definition: BaseTest.py:879
def cmpTreesDicts
Definition: BaseTest.py:785
def findHistosSummaries
Definition: BaseTest.py:944