Gaudi Framework, version v24r2

Home   Generated: Wed Dec 4 2013
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Friends Macros Groups Pages
GaudiTest.py
Go to the documentation of this file.
1 ########################################################################
2 # File: GaudiTest.py
3 # Author: Marco Clemencic CERN/PH-LBC
4 ########################################################################
5 __author__ = 'Marco Clemencic CERN/PH-LBC'
6 ########################################################################
7 # Imports
8 ########################################################################
9 import os
10 import sys
11 import re
12 import tempfile
13 import shutil
14 import string
15 import difflib
16 from subprocess import Popen, PIPE, STDOUT
17 
18 # ensure the preferred locale
19 os.environ['LC_ALL'] = 'C'
20 
21 import qm
22 from qm.test.classes.command import ExecTestBase
23 from qm.test.result_stream import ResultStream
24 
25 ### Needed by the re-implementation of TimeoutExecutable
26 import qm.executable
27 import time, signal
28 # The classes in this module are implemented differently depending on
29 # the operating system in use.
30 if sys.platform == "win32":
31  import msvcrt
32  import pywintypes
33  from threading import *
34  import win32api
35  import win32con
36  import win32event
37  import win32file
38  import win32pipe
39  import win32process
40 else:
41  import cPickle
42  import fcntl
43  import select
44  import qm.sigmask
45 
46 ########################################################################
47 # Utility Classes
48 ########################################################################
50  """
51  Class to changes the environment temporarily.
52  """
53  def __init__(self, orig = os.environ, keep_same = False):
54  """
55  Create a temporary environment on top of the one specified
56  (it can be another TemporaryEnvironment instance).
57  """
58  #print "New environment"
59  self.old_values = {}
60  self.env = orig
61  self._keep_same = keep_same
62 
63  def __setitem__(self,key,value):
64  """
65  Set an environment variable recording the previous value.
66  """
67  if key not in self.old_values :
68  if key in self.env :
69  if not self._keep_same or self.env[key] != value:
70  self.old_values[key] = self.env[key]
71  else:
72  self.old_values[key] = None
73  self.env[key] = value
74 
75  def __getitem__(self,key):
76  """
77  Get an environment variable.
78  Needed to provide the same interface as os.environ.
79  """
80  return self.env[key]
81 
82  def __delitem__(self,key):
83  """
84  Unset an environment variable.
85  Needed to provide the same interface as os.environ.
86  """
87  if key not in self.env :
88  raise KeyError(key)
89  self.old_values[key] = self.env[key]
90  del self.env[key]
91 
92  def keys(self):
93  """
94  Return the list of defined environment variables.
95  Needed to provide the same interface as os.environ.
96  """
97  return self.env.keys()
98 
99  def items(self):
100  """
101  Return the list of (name,value) pairs for the defined environment variables.
102  Needed to provide the same interface as os.environ.
103  """
104  return self.env.items()
105 
106  def __contains__(self,key):
107  """
108  Operator 'in'.
109  Needed to provide the same interface as os.environ.
110  """
111  return key in self.env
112 
113  def restore(self):
114  """
115  Revert all the changes done to the orignal environment.
116  """
117  for key,value in self.old_values.items():
118  if value is None:
119  del self.env[key]
120  else:
121  self.env[key] = value
122  self.old_values = {}
123 
124  def __del__(self):
125  """
126  Revert the changes on destruction.
127  """
128  #print "Restoring the environment"
129  self.restore()
130 
131  def gen_script(self,shell_type):
132  """
133  Generate a shell script to reproduce the changes in the environment.
134  """
135  shells = [ 'csh', 'sh', 'bat' ]
136  if shell_type not in shells:
137  raise RuntimeError("Shell type '%s' unknown. Available: %s"%(shell_type,shells))
138  out = ""
139  for key,value in self.old_values.items():
140  if key not in self.env:
141  # unset variable
142  if shell_type == 'csh':
143  out += 'unsetenv %s\n'%key
144  elif shell_type == 'sh':
145  out += 'unset %s\n'%key
146  elif shell_type == 'bat':
147  out += 'set %s=\n'%key
148  else:
149  # set variable
150  if shell_type == 'csh':
151  out += 'setenv %s "%s"\n'%(key,self.env[key])
152  elif shell_type == 'sh':
153  out += 'export %s="%s"\n'%(key,self.env[key])
154  elif shell_type == 'bat':
155  out += 'set %s=%s\n'%(key,self.env[key])
156  return out
157 
158 class TempDir:
159  """Small class for temporary directories.
160  When instantiated, it creates a temporary directory and the instance
161  behaves as the string containing the directory name.
162  When the instance goes out of scope, it removes all the content of
163  the temporary directory (automatic clean-up).
164  """
165  def __init__(self, keep = False, chdir = False):
166  self.name = tempfile.mkdtemp()
167  self._keep = keep
168  self._origdir = None
169  if chdir:
170  self._origdir = os.getcwd()
171  os.chdir(self.name)
172 
173  def __str__(self):
174  return self.name
175 
176  def __del__(self):
177  if self._origdir:
178  os.chdir(self._origdir)
179  if self.name and not self._keep:
180  shutil.rmtree(self.name)
181 
182  def __getattr__(self,attr):
183  return getattr(self.name,attr)
184 
185 class TempFile:
186  """Small class for temporary files.
187  When instantiated, it creates a temporary directory and the instance
188  behaves as the string containing the directory name.
189  When the instance goes out of scope, it removes all the content of
190  the temporary directory (automatic clean-up).
191  """
192  def __init__(self, suffix='', prefix='tmp', dir=None, text=False, keep = False):
193  self.file = None
194  self.name = None
195  self._keep = keep
196 
197  self._fd, self.name = tempfile.mkstemp(suffix,prefix,dir,text)
198  self.file = os.fdopen(self._fd,"r+")
199 
200  def __str__(self):
201  return self.name
202 
203  def __del__(self):
204  if self.file:
205  self.file.close()
206  if self.name and not self._keep:
207  os.remove(self.name)
208 
209  def __getattr__(self,attr):
210  return getattr(self.file,attr)
211 
212 class CMT:
213  """Small wrapper to call CMT.
214  """
215  def __init__(self,path=None):
216  if path is None:
217  path = os.getcwd()
218  self.path = path
219 
220  def _run_cmt(self,command,args):
221  # prepare command line
222  if type(args) is str:
223  args = [args]
224  cmd = "cmt %s"%command
225  for arg in args:
226  cmd += ' "%s"'%arg
227 
228  # go to the execution directory
229  olddir = os.getcwd()
230  os.chdir(self.path)
231  # run cmt
232  result = os.popen4(cmd)[1].read()
233  # return to the old directory
234  os.chdir(olddir)
235  return result
236 
237  def __getattr__(self,attr):
238  return lambda args=[]: self._run_cmt(attr, args)
239 
240  def runtime_env(self,env = None):
241  """Returns a dictionary containing the runtime environment produced by CMT.
242  If a dictionary is passed a modified instance of it is returned.
243  """
244  if env is None:
245  env = {}
246  for l in self.setup("-csh").splitlines():
247  l = l.strip()
248  if l.startswith("setenv"):
249  dummy,name,value = l.split(None,3)
250  env[name] = value.strip('"')
251  elif l.startswith("unsetenv"):
252  dummy,name = l.split(None,2)
253  if name in env:
254  del env[name]
255  return env
256  def show_macro(self,k):
257  r = self.show(["macro",k])
258  if r.find("CMT> Error: symbol not found") >= 0:
259  return None
260  else:
261  return self.show(["macro_value",k]).strip()
262 
263 ## Locates an executable in the executables path ($PATH) and returns the full
264 # path to it.
265 # If the executable cannot be found, None is returned
266 def which(executable):
267  """
268  Locates an executable in the executables path ($PATH) and returns the full
269  path to it. An application is looked for with or without the '.exe' suffix.
270  If the executable cannot be found, None is returned
271  """
272  if os.path.isabs(executable):
273  if not os.path.exists(executable):
274  if executable.endswith('.exe'):
275  if os.path.exists(executable[:-4]):
276  return executable[:-4]
277  return executable
278  for d in os.environ.get("PATH").split(os.pathsep):
279  fullpath = os.path.join(d, executable)
280  if os.path.exists(fullpath):
281  return fullpath
282  if executable.endswith('.exe'):
283  return which(executable[:-4])
284  return None
285 
287  p = os.path.normpath(os.path.expandvars(p))
288  if os.path.exists(p):
289  p = os.path.realpath(p)
290  return p
291 
292 ########################################################################
293 # Output Validation Classes
294 ########################################################################
296  """Basic implementation of an option validator for Gaudi tests.
297  This implementation is based on the standard (LCG) validation functions
298  used in QMTest.
299  """
300  def __init__(self,ref,cause,result_key):
301  self.reference = ref
302  self.cause = cause
303  self.result_key = result_key
304 
305  def __call__(self, out, result):
306  """Validate the output of the program.
307 
308  'stdout' -- A string containing the data written to the standard output
309  stream.
310 
311  'stderr' -- A string containing the data written to the standard error
312  stream.
313 
314  'result' -- A 'Result' object. It may be used to annotate
315  the outcome according to the content of stderr.
316 
317  returns -- A list of strings giving causes of failure."""
318 
319  causes = []
320  # Check to see if theoutput matches.
321  if not self.__CompareText(out, self.reference):
322  causes.append(self.cause)
323  result[self.result_key] = result.Quote(self.reference)
324 
325  return causes
326 
327  def __CompareText(self, s1, s2):
328  """Compare 's1' and 's2', ignoring line endings.
329 
330  's1' -- A string.
331 
332  's2' -- A string.
333 
334  returns -- True if 's1' and 's2' are the same, ignoring
335  differences in line endings."""
336 
337  # The "splitlines" method works independently of the line ending
338  # convention in use.
339  return s1.splitlines() == s2.splitlines()
340 
342  """ Base class for a callable that takes a file and returns a modified
343  version of it."""
344  def __processLine__(self, line):
345  return line
346  def __call__(self, input):
347  if hasattr(input,"__iter__"):
348  lines = input
349  mergeback = False
350  else:
351  lines = input.splitlines()
352  mergeback = True
353  output = []
354  for l in lines:
355  l = self.__processLine__(l)
356  if l: output.append(l)
357  if mergeback: output = '\n'.join(output)
358  return output
359  def __add__(self, rhs):
360  return FilePreprocessorSequence([self,rhs])
361 
363  def __init__(self, members = []):
364  self.members = members
365  def __add__(self, rhs):
366  return FilePreprocessorSequence(self.members + [rhs])
367  def __call__(self, input):
368  output = input
369  for pp in self.members:
370  output = pp(output)
371  return output
372 
374  def __init__(self, strings = [], regexps = []):
375  import re
376  self.strings = strings
377  self.regexps = map(re.compile,regexps)
378 
379  def __processLine__(self, line):
380  for s in self.strings:
381  if line.find(s) >= 0: return None
382  for r in self.regexps:
383  if r.search(line): return None
384  return line
385 
387  def __init__(self, start, end):
388  self.start = start
389  self.end = end
390  self._skipping = False
391 
392  def __processLine__(self, line):
393  if self.start in line:
394  self._skipping = True
395  return None
396  elif self.end in line:
397  self._skipping = False
398  elif self._skipping:
399  return None
400  return line
401 
403  def __init__(self, orig, repl = "", when = None):
404  if when:
405  when = re.compile(when)
406  self._operations = [ (when, re.compile(orig), repl) ]
407  def __add__(self,rhs):
408  if isinstance(rhs, RegexpReplacer):
409  res = RegexpReplacer("","",None)
410  res._operations = self._operations + rhs._operations
411  else:
412  res = FilePreprocessor.__add__(self, rhs)
413  return res
414  def __processLine__(self, line):
415  for w,o,r in self._operations:
416  if w is None or w.search(line):
417  line = o.sub(r, line)
418  return line
419 
420 # Common preprocessors
421 maskPointers = RegexpReplacer("0x[0-9a-fA-F]{4,16}","0x########")
422 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)?",
423  "00:00:00 1970-01-01")
424 normalizeEOL = FilePreprocessor()
425 normalizeEOL.__processLine__ = lambda line: str(line).rstrip() + '\n'
426 
427 skipEmptyLines = FilePreprocessor()
428 # FIXME: that's ugly
429 skipEmptyLines.__processLine__ = lambda line: (line.strip() and line) or None
430 
431 ## Special preprocessor sorting the list of strings (whitespace separated)
432 # that follow a signature on a single line
434  def __init__(self, signature):
435  self.signature = signature
436  self.siglen = len(signature)
437  def __processLine__(self, line):
438  pos = line.find(self.signature)
439  if pos >=0:
440  line = line[:(pos+self.siglen)]
441  lst = line[(pos+self.siglen):].split()
442  lst.sort()
443  line += " ".join(lst)
444  return line
445 
446 # Preprocessors for GaudiExamples
447 normalizeExamples = maskPointers + normalizeDate
448 for w,o,r in [
449  #("TIMER.TIMER",r"[0-9]", "0"), # Normalize time output
450  ("TIMER.TIMER",r"\s+[+-]?[0-9]+[0-9.]*", " 0"), # Normalize time output
451  ("release all pending",r"^.*/([^/]*:.*)",r"\1"),
452  ("0x########",r"\[.*/([^/]*.*)\]",r"[\1]"),
453  ("^#.*file",r"file '.*[/\\]([^/\\]*)$",r"file '\1"),
454  ("^JobOptionsSvc.*options successfully read in from",r"read in from .*[/\\]([^/\\]*)$",r"file \1"), # normalize path to options
455  # Normalize UUID, except those ending with all 0s (i.e. the class IDs)
456  (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"),
457  # Absorb a change in ServiceLocatorHelper
458  ("ServiceLocatorHelper::", "ServiceLocatorHelper::(create|locate)Service", "ServiceLocatorHelper::service"),
459  # Remove the leading 0 in Windows' exponential format
460  (None, r"e([-+])0([0-9][0-9])", r"e\1\2"),
461  # Output line changed in Gaudi v24
462  (None, r'Service reference count check:', r'Looping over all active services...'),
463  ]: #[ ("TIMER.TIMER","[0-9]+[0-9.]*", "") ]
464  normalizeExamples += RegexpReplacer(o,r,w)
465 normalizeExamples = LineSkipper(["//GP:",
466  "JobOptionsSvc INFO # ",
467  "JobOptionsSvc WARNING # ",
468  "Time User",
469  "Welcome to",
470  "This machine has a speed",
471  "TIME:",
472  "running on",
473  "ToolSvc.Sequenc... INFO",
474  "DataListenerSvc INFO XML written to file:",
475  "[INFO]","[WARNING]",
476  "DEBUG No writable file catalog found which contains FID:",
477  "0 local", # hack for ErrorLogExample
478  "DEBUG Service base class initialized successfully", # changed between v20 and v21
479  "DEBUG Incident timing:", # introduced with patch #3487
480  "INFO 'CnvServices':[", # changed the level of the message from INFO to DEBUG
481  # This comes from ROOT, when using GaudiPython
482  'Note: (file "(tmpfile)", line 2) File "set" already loaded',
483  # The signal handler complains about SIGXCPU not defined on some platforms
484  'SIGXCPU',
485  ],regexps = [
486  r"^JobOptionsSvc INFO *$",
487  r"^#", # Ignore python comments
488  r"(Always|SUCCESS)\s*(Root f|[^ ]* F)ile version:", # skip the message reporting the version of the root file
489  r"0x[0-9a-fA-F#]+ *Algorithm::sysInitialize\(\) *\[", # hack for ErrorLogExample
490  r"0x[0-9a-fA-F#]* *__gxx_personality_v0 *\[", # hack for ErrorLogExample
491  r"File '.*.xml' does not exist",
492  r"INFO Refer to dataset .* by its file ID:",
493  r"INFO Referring to dataset .* by its file ID:",
494  r"INFO Disconnect from dataset",
495  r"INFO Disconnected from dataset",
496  r"INFO Disconnected data IO:",
497  r"IncidentSvc\s*(DEBUG (Adding|Removing)|VERBOSE Calling)",
498  # I want to ignore the header of the unchecked StatusCode report
499  r"^StatusCodeSvc.*listing all unchecked return codes:",
500  r"^StatusCodeSvc\s*INFO\s*$",
501  r"Num\s*\|\s*Function\s*\|\s*Source Library",
502  r"^[-+]*\s*$",
503  # Hide the fake error message coming from POOL/ROOT (ROOT 5.21)
504  r"ERROR Failed to modify file: .* Errno=2 No such file or directory",
505  # Hide unckeched StatusCodes from dictionaries
506  r"^ +[0-9]+ \|.*ROOT",
507  r"^ +[0-9]+ \|.*\|.*Dict",
508  # Remove ROOT TTree summary table, which changes from one version to the other
509  r"^\*.*\*$",
510  # Remove Histos Summaries
511  r"SUCCESS\s*Booked \d+ Histogram\(s\)",
512  r"^ \|",
513  r"^ ID=",
514  ] ) + normalizeExamples + skipEmptyLines + \
515  normalizeEOL + \
516  LineSorter("Services to release : ")
517 
519  def __init__(self, reffile, cause, result_key, preproc = normalizeExamples):
520  self.reffile = os.path.expandvars(reffile)
521  self.cause = cause
522  self.result_key = result_key
523  self.preproc = preproc
524  def __call__(self, stdout, result):
525  causes = []
526  if os.path.isfile(self.reffile):
527  orig = open(self.reffile).xreadlines()
528  if self.preproc:
529  orig = self.preproc(orig)
530  else:
531  orig = []
532 
533  new = stdout.splitlines()
534  if self.preproc:
535  new = self.preproc(new)
536  #open(self.reffile + ".test","w").writelines(new)
537  diffs = difflib.ndiff(orig,new,charjunk=difflib.IS_CHARACTER_JUNK)
538  filterdiffs = map(lambda x: x.strip(),filter(lambda x: x[0] != " ",diffs))
539  #filterdiffs = [x.strip() for x in diffs]
540  if filterdiffs:
541  result[self.result_key] = result.Quote("\n".join(filterdiffs))
542  result[self.result_key] += result.Quote("""
543 Legend:
544  -) reference file
545  +) standard output of the test""")
546  causes.append(self.cause)
547 
548  return causes
549 
550 ########################################################################
551 # Useful validation functions
552 ########################################################################
553 def findReferenceBlock(reference, stdout, result, causes, signature_offset=0, signature=None,
554  id = None):
555  """
556  Given a block of text, tries to find it in the output.
557  The block had to be identified by a signature line. By default, the first
558  line is used as signature, or the line pointed to by signature_offset. If
559  signature_offset points outside the block, a signature line can be passed as
560  signature argument. Note: if 'signature' is None (the default), a negative
561  signature_offset is interpreted as index in a list (e.g. -1 means the last
562  line), otherwise the it is interpreted as the number of lines before the
563  first one of the block the signature must appear.
564  The parameter 'id' allow to distinguish between different calls to this
565  function in the same validation code.
566  """
567  # split reference file, sanitize EOLs and remove empty lines
568  reflines = filter(None,map(lambda s: s.rstrip(), reference.splitlines()))
569  if not reflines:
570  raise RuntimeError("Empty (or null) reference")
571  # the same on standard output
572  outlines = filter(None,map(lambda s: s.rstrip(), stdout.splitlines()))
573 
574  res_field = "GaudiTest.RefBlock"
575  if id:
576  res_field += "_%s" % id
577 
578  if signature is None:
579  if signature_offset < 0:
580  signature_offset = len(reference)+signature_offset
581  signature = reflines[signature_offset]
582  # find the reference block in the output file
583  try:
584  pos = outlines.index(signature)
585  outlines = outlines[pos-signature_offset:pos+len(reflines)-signature_offset]
586  if reflines != outlines:
587  msg = "standard output"
588  # I do not want 2 messages in causes if teh function is called twice
589  if not msg in causes:
590  causes.append(msg)
591  result[res_field + ".observed"] = result.Quote("\n".join(outlines))
592  except ValueError:
593  causes.append("missing signature")
594  result[res_field + ".signature"] = result.Quote(signature)
595  if len(reflines) > 1 or signature != reflines[0]:
596  result[res_field + ".expected"] = result.Quote("\n".join(reflines))
597 
598  return causes
599 
600 def countErrorLines(expected = {'ERROR':0, 'FATAL':0}, **kwargs):
601  """
602  Count the number of messages with required severity (by default ERROR and FATAL)
603  and check if their numbers match the expected ones (0 by default).
604  The dictionary "expected" can be used to tune the number of errors and fatals
605  allowed, or to limit the number of expected warnings etc.
606  """
607  stdout = kwargs["stdout"]
608  result = kwargs["result"]
609  causes = kwargs["causes"]
610 
611  # prepare the dictionary to record the extracted lines
612  errors = {}
613  for sev in expected:
614  errors[sev] = []
615 
616  outlines = stdout.splitlines()
617  from math import log10
618  fmt = "%%%dd - %%s" % (int(log10(len(outlines))+1))
619 
620  linecount = 0
621  for l in outlines:
622  linecount += 1
623  words = l.split()
624  if len(words) >= 2 and words[1] in errors:
625  errors[words[1]].append(fmt%(linecount,l.rstrip()))
626 
627  for e in errors:
628  if len(errors[e]) != expected[e]:
629  causes.append('%s(%d)'%(e,len(errors[e])))
630  result["GaudiTest.lines.%s"%e] = result.Quote('\n'.join(errors[e]))
631  result["GaudiTest.lines.%s.expected#"%e] = result.Quote(str(expected[e]))
632 
633  return causes
634 
635 
636 def _parseTTreeSummary(lines, pos):
637  """
638  Parse the TTree summary table in lines, starting from pos.
639  Returns a tuple with the dictionary with the digested informations and the
640  position of the first line after the summary.
641  """
642  result = {}
643  i = pos + 1 # first line is a sequence of '*'
644  count = len(lines)
645 
646  splitcols = lambda l: [ f.strip() for f in l.strip("*\n").split(':',2) ]
647  def parseblock(ll):
648  r = {}
649  cols = splitcols(ll[0])
650  r["Name"], r["Title"] = cols[1:]
651 
652  cols = splitcols(ll[1])
653  r["Entries"] = int(cols[1])
654 
655  sizes = cols[2].split()
656  r["Total size"] = int(sizes[2])
657  if sizes[-1] == "memory":
658  r["File size"] = 0
659  else:
660  r["File size"] = int(sizes[-1])
661 
662  cols = splitcols(ll[2])
663  sizes = cols[2].split()
664  if cols[0] == "Baskets":
665  r["Baskets"] = int(cols[1])
666  r["Basket size"] = int(sizes[2])
667  r["Compression"] = float(sizes[-1])
668  return r
669 
670  if i < (count - 3) and lines[i].startswith("*Tree"):
671  result = parseblock(lines[i:i+3])
672  result["Branches"] = {}
673  i += 4
674  while i < (count - 3) and lines[i].startswith("*Br"):
675  if i < (count - 2) and lines[i].startswith("*Branch "):
676  # skip branch header
677  i += 3
678  continue
679  branch = parseblock(lines[i:i+3])
680  result["Branches"][branch["Name"]] = branch
681  i += 4
682 
683  return (result, i)
684 
685 def findTTreeSummaries(stdout):
686  """
687  Scan stdout to find ROOT TTree summaries and digest them.
688  """
689  stars = re.compile(r"^\*+$")
690  outlines = stdout.splitlines()
691  nlines = len(outlines)
692  trees = {}
693 
694  i = 0
695  while i < nlines: #loop over the output
696  # look for
697  while i < nlines and not stars.match(outlines[i]):
698  i += 1
699  if i < nlines:
700  tree, i = _parseTTreeSummary(outlines, i)
701  if tree:
702  trees[tree["Name"]] = tree
703 
704  return trees
705 
706 def cmpTreesDicts(reference, to_check, ignore = None):
707  """
708  Check that all the keys in reference are in to_check too, with the same value.
709  If the value is a dict, the function is called recursively. to_check can
710  contain more keys than reference, that will not be tested.
711  The function returns at the first difference found.
712  """
713  fail_keys = []
714  # filter the keys in the reference dictionary
715  if ignore:
716  ignore_re = re.compile(ignore)
717  keys = [ key for key in reference if not ignore_re.match(key) ]
718  else:
719  keys = reference.keys()
720  # loop over the keys (not ignored) in the reference dictionary
721  for k in keys:
722  if k in to_check: # the key must be in the dictionary to_check
723  if (type(reference[k]) is dict) and (type(to_check[k]) is dict):
724  # if both reference and to_check values are dictionaries, recurse
725  failed = fail_keys = cmpTreesDicts(reference[k], to_check[k], ignore)
726  else:
727  # compare the two values
728  failed = to_check[k] != reference[k]
729  else: # handle missing keys in the dictionary to check (i.e. failure)
730  to_check[k] = None
731  failed = True
732  if failed:
733  fail_keys.insert(0, k)
734  break # exit from the loop at the first failure
735  return fail_keys # return the list of keys bringing to the different values
736 
737 def getCmpFailingValues(reference, to_check, fail_path):
738  c = to_check
739  r = reference
740  for k in fail_path:
741  c = c.get(k,None)
742  r = r.get(k,None)
743  if c is None or r is None:
744  break # one of the dictionaries is not deep enough
745  return (fail_path, r, c)
746 
747 # signature of the print-out of the histograms
748 h_count_re = re.compile(r"^(.*)SUCCESS\s+Booked (\d+) Histogram\(s\) :\s+(.*)")
749 
750 def parseHistosSummary(lines, pos):
751  """
752  Extract the histograms infos from the lines starting at pos.
753  Returns the position of the first line after the summary block.
754  """
755  global h_count_re
756  h_table_head = re.compile(r'SUCCESS\s+List of booked (1D|2D|3D|1D profile|2D profile) histograms in directory\s+"(\w*)"')
757  h_short_summ = re.compile(r"ID=([^\"]+)\s+\"([^\"]+)\"\s+(.*)")
758 
759  nlines = len(lines)
760 
761  # decode header
762  m = h_count_re.search(lines[pos])
763  name = m.group(1).strip()
764  total = int(m.group(2))
765  header = {}
766  for k, v in [ x.split("=") for x in m.group(3).split() ]:
767  header[k] = int(v)
768  pos += 1
769  header["Total"] = total
770 
771  summ = {}
772  while pos < nlines:
773  m = h_table_head.search(lines[pos])
774  if m:
775  t, d = m.groups(1) # type and directory
776  t = t.replace(" profile", "Prof")
777  pos += 1
778  if pos < nlines:
779  l = lines[pos]
780  else:
781  l = ""
782  cont = {}
783  if l.startswith(" | ID"):
784  # table format
785  titles = [ x.strip() for x in l.split("|")][1:]
786  pos += 1
787  while pos < nlines and lines[pos].startswith(" |"):
788  l = lines[pos]
789  values = [ x.strip() for x in l.split("|")][1:]
790  hcont = {}
791  for i in range(len(titles)):
792  hcont[titles[i]] = values[i]
793  cont[hcont["ID"]] = hcont
794  pos += 1
795  elif l.startswith(" ID="):
796  while pos < nlines and lines[pos].startswith(" ID="):
797  values = [ x.strip() for x in h_short_summ.search(lines[pos]).groups() ]
798  cont[values[0]] = values
799  pos += 1
800  else: # not interpreted
801  raise RuntimeError("Cannot understand line %d: '%s'" % (pos, l))
802  if not d in summ:
803  summ[d] = {}
804  summ[d][t] = cont
805  summ[d]["header"] = header
806  else:
807  break
808  if not summ:
809  # If the full table is not present, we use only the header
810  summ[name] = {"header": header}
811  return summ, pos
812 
814  """
815  Scan stdout to find ROOT TTree summaries and digest them.
816  """
817  outlines = stdout.splitlines()
818  nlines = len(outlines) - 1
819  summaries = {}
820  global h_count_re
821 
822  pos = 0
823  while pos < nlines:
824  summ = {}
825  # find first line of block:
826  match = h_count_re.search(outlines[pos])
827  while pos < nlines and not match:
828  pos += 1
829  match = h_count_re.search(outlines[pos])
830  if match:
831  summ, pos = parseHistosSummary(outlines, pos)
832  summaries.update(summ)
833  return summaries
834 
835 class GaudiFilterExecutable(qm.executable.Filter):
836  def __init__(self, input, timeout = -1):
837  """Create a new 'Filter'.
838 
839  'input' -- The string containing the input to provide to the
840  child process.
841 
842  'timeout' -- As for 'TimeoutExecutable.__init__'."""
843 
844  super(GaudiFilterExecutable, self).__init__(input, timeout)
845  self.__input = input
846  self.__timeout = timeout
847  self.stack_trace_file = None
848  # Temporary file to pass the stack trace from one process to the other
849  # The file must be closed and reopened when needed to avoid conflicts
850  # between the processes
851  tmpf = tempfile.mkstemp()
852  os.close(tmpf[0])
853  self.stack_trace_file = tmpf[1] # remember only the name
854 
856  """Copied from TimeoutExecutable to allow the re-implementation of
857  _HandleChild.
858  """
859  if sys.platform == "win32":
860  # In Windows 2000 (or later), we should use "jobs" by
861  # analogy with UNIX process groups. However, that
862  # functionality is not (yet) provided by the Python Win32
863  # extensions.
864  return 0
865 
866  return self.__timeout >= 0 or self.__timeout == -2
867  ##
868  # Needs to replace the ones from RedirectedExecutable and TimeoutExecutable
869  def _HandleChild(self):
870  """Code copied from both FilterExecutable and TimeoutExecutable.
871  """
872  # Close the pipe ends that we do not need.
873  if self._stdin_pipe:
874  self._ClosePipeEnd(self._stdin_pipe[0])
875  if self._stdout_pipe:
876  self._ClosePipeEnd(self._stdout_pipe[1])
877  if self._stderr_pipe:
878  self._ClosePipeEnd(self._stderr_pipe[1])
879 
880  # The pipes created by 'RedirectedExecutable' must be closed
881  # before the monitor process (created by 'TimeoutExecutable')
882  # is created. Otherwise, if the child process dies, 'select'
883  # in the parent will not return if the monitor process may
884  # still have one of the file descriptors open.
885 
886  super(qm.executable.TimeoutExecutable, self)._HandleChild()
887 
889  # Put the child into its own process group. This step is
890  # performed in both the parent and the child; therefore both
891  # processes can safely assume that the creation of the process
892  # group has taken place.
893  child_pid = self._GetChildPID()
894  try:
895  os.setpgid(child_pid, child_pid)
896  except:
897  # The call to setpgid may fail if the child has exited,
898  # or has already called 'exec'. In that case, we are
899  # guaranteed that the child has already put itself in the
900  # desired process group.
901  pass
902  # Create the monitoring process.
903  #
904  # If the monitoring process is in parent's process group and
905  # kills the child after waitpid has returned in the parent, we
906  # may end up trying to kill a process group other than the one
907  # that we intend to kill. Therefore, we put the monitoring
908  # process in the same process group as the child; that ensures
909  # that the process group will persist until the monitoring
910  # process kills it.
911  self.__monitor_pid = os.fork()
912  if self.__monitor_pid != 0:
913  # Make sure that the monitoring process is placed into the
914  # child's process group before the parent process calls
915  # 'waitpid'. In this way, we are guaranteed that the process
916  # group as the child
917  os.setpgid(self.__monitor_pid, child_pid)
918  else:
919  # Put the monitoring process into the child's process
920  # group. We know the process group still exists at
921  # this point because either (a) we are in the process
922  # group, or (b) the parent has not yet called waitpid.
923  os.setpgid(0, child_pid)
924 
925  # Close all open file descriptors. They are not needed
926  # in the monitor process. Furthermore, when the parent
927  # closes the write end of the stdin pipe to the child,
928  # we do not want the pipe to remain open; leaving the
929  # pipe open in the monitor process might cause the child
930  # to block waiting for additional input.
931  try:
932  max_fds = os.sysconf("SC_OPEN_MAX")
933  except:
934  max_fds = 256
935  for fd in xrange(max_fds):
936  try:
937  os.close(fd)
938  except:
939  pass
940  try:
941  if self.__timeout >= 0:
942  # Give the child time to run.
943  time.sleep (self.__timeout)
944  #######################################################
945  ### This is the interesting part: dump the stack trace to a file
946  if sys.platform == "linux2": # we should be have /proc and gdb
947  cmd = ["gdb",
948  os.path.join("/proc", str(child_pid), "exe"),
949  str(child_pid),
950  "-batch", "-n", "-x",
951  "'%s'" % os.path.join(os.path.dirname(__file__), "stack-trace.gdb")]
952  # FIXME: I wanted to use subprocess.Popen, but it doesn't want to work
953  # in this context.
954  o = os.popen(" ".join(cmd)).read()
955  open(self.stack_trace_file,"w").write(o)
956  #######################################################
957 
958  # Kill all processes in the child process group.
959  os.kill(0, signal.SIGKILL)
960  else:
961  # This call to select will never terminate.
962  select.select ([], [], [])
963  finally:
964  # Exit. This code is in a finally clause so that
965  # we are guaranteed to get here no matter what.
966  os._exit(0)
967  elif self.__timeout >= 0 and sys.platform == "win32":
968  # Create a monitoring thread.
969  self.__monitor_thread = Thread(target = self.__Monitor)
970  self.__monitor_thread.start()
971 
972  if sys.platform == "win32":
973 
974  def __Monitor(self):
975  """Code copied from FilterExecutable.
976  Kill the child if the timeout expires.
977 
978  This function is run in the monitoring thread."""
979 
980  # The timeout may be expressed as a floating-point value
981  # on UNIX, but it must be an integer number of
982  # milliseconds when passed to WaitForSingleObject.
983  timeout = int(self.__timeout * 1000)
984  # Wait for the child process to terminate or for the
985  # timer to expire.
986  result = win32event.WaitForSingleObject(self._GetChildPID(),
987  timeout)
988  # If the timeout occurred, kill the child process.
989  if result == win32con.WAIT_TIMEOUT:
990  self.Kill()
991 
992 ########################################################################
993 # Test Classes
994 ########################################################################
995 class GaudiExeTest(ExecTestBase):
996  """Standard Gaudi test.
997  """
998  arguments = [
999  qm.fields.TextField(
1000  name="program",
1001  title="Program",
1002  not_empty_text=1,
1003  description="""The path to the program.
1004 
1005  This field indicates the path to the program. If it is not
1006  an absolute path, the value of the 'PATH' environment
1007  variable will be used to search for the program.
1008  If not specified, $GAUDIEXE or Gaudi.exe are used.
1009  """
1010  ),
1011  qm.fields.SetField(qm.fields.TextField(
1012  name="args",
1013  title="Argument List",
1014  description="""The command-line arguments.
1015 
1016  If this field is left blank, the program is run without any
1017  arguments.
1018 
1019  Use this field to specify the option files.
1020 
1021  An implicit 0th argument (the path to the program) is added
1022  automatically."""
1023  )),
1024  qm.fields.TextField(
1025  name="options",
1026  title="Options",
1027  description="""Options to be passed to the application.
1028 
1029  This field allows to pass a list of options to the main program
1030  without the need of a separate option file.
1031 
1032  The content of the field is written to a temporary file which name
1033  is passed the the application as last argument (appended to the
1034  field "Argument List".
1035  """,
1036  verbatim="true",
1037  multiline="true",
1038  default_value=""
1039  ),
1040  qm.fields.TextField(
1041  name="workdir",
1042  title="Working Directory",
1043  description="""Path to the working directory.
1044 
1045  If this field is left blank, the program will be run from the qmtest
1046  directory, otherwise from the directory specified.""",
1047  default_value=""
1048  ),
1049  qm.fields.TextField(
1050  name="reference",
1051  title="Reference Output",
1052  description="""Path to the file containing the reference output.
1053 
1054  If this field is left blank, any standard output will be considered
1055  valid.
1056 
1057  If the reference file is specified, any output on standard error is
1058  ignored."""
1059  ),
1060  qm.fields.TextField(
1061  name="error_reference",
1062  title="Reference for standard error",
1063  description="""Path to the file containing the reference for the standard error.
1064 
1065  If this field is left blank, any standard output will be considered
1066  valid.
1067 
1068  If the reference file is specified, any output on standard error is
1069  ignored."""
1070  ),
1071  qm.fields.SetField(qm.fields.TextField(
1072  name = "unsupported_platforms",
1073  title = "Unsupported Platforms",
1074  description = """Platform on which the test must not be run.
1075 
1076  List of regular expressions identifying the platforms on which the
1077  test is not run and the result is set to UNTESTED."""
1078  )),
1079 
1080  qm.fields.TextField(
1081  name = "validator",
1082  title = "Validator",
1083  description = """Function to validate the output of the test.
1084 
1085  If defined, the function is used to validate the products of the
1086  test.
1087  The function is called passing as arguments:
1088  self: the test class instance
1089  stdout: the standard output of the executed test
1090  stderr: the standard error of the executed test
1091  result: the Result objects to fill with messages
1092  The function must return a list of causes for the failure.
1093  If specified, overrides standard output, standard error and
1094  reference files.
1095  """,
1096  verbatim="true",
1097  multiline="true",
1098  default_value=""
1099  ),
1100 
1101  qm.fields.BooleanField(
1102  name = "use_temp_dir",
1103  title = "Use temporary directory",
1104  description = """Use temporary directory.
1105 
1106  If set to true, use a temporary directory as working directory.
1107  """,
1108  default_value="false"
1109  ),
1110 
1111  qm.fields.IntegerField(
1112  name = "signal",
1113  title = "Expected signal",
1114  description = """Expect termination by signal.""",
1115  default_value=None
1116  ),
1117  ]
1118 
1119  def PlatformIsNotSupported(self, context, result):
1120  platform = self.GetPlatform()
1121  unsupported = [ re.compile(x)
1122  for x in [ str(y).strip()
1123  for y in self.unsupported_platforms ]
1124  if x
1125  ]
1126  for p_re in unsupported:
1127  if p_re.search(platform):
1128  result.SetOutcome(result.UNTESTED)
1129  result[result.CAUSE] = 'Platform not supported.'
1130  return True
1131  return False
1132 
1133  def GetPlatform(self):
1134  """
1135  Return the platform Id defined in CMTCONFIG or SCRAM_ARCH.
1136  """
1137  arch = "None"
1138  # check architecture name
1139  if "CMTCONFIG" in os.environ:
1140  arch = os.environ["CMTCONFIG"]
1141  elif "SCRAM_ARCH" in os.environ:
1142  arch = os.environ["SCRAM_ARCH"]
1143  return arch
1144 
1145  def isWinPlatform(self):
1146  """
1147  Return True if the current platform is Windows.
1148 
1149  This function was needed because of the change in the CMTCONFIG format,
1150  from win32_vc71_dbg to i686-winxp-vc9-dbg.
1151  """
1152  platform = self.GetPlatform()
1153  return "winxp" in platform or platform.startswith("win")
1154 
1155  def _expandReferenceFileName(self, reffile):
1156  # if no file is passed, do nothing
1157  if not reffile:
1158  return ""
1159 
1160  # function to split an extension in constituents parts
1161  platformSplit = lambda p: set(p.split('-' in p and '-' or '_'))
1162 
1163  reference = os.path.normpath(os.path.expandvars(reffile))
1164  # old-style platform-specific reference name
1165  spec_ref = reference[:-3] + self.GetPlatform()[0:3] + reference[-3:]
1166  if os.path.isfile(spec_ref):
1167  reference = spec_ref
1168  else: # look for new-style platform specific reference files:
1169  # get all the files whose name start with the reference filename
1170  dirname, basename = os.path.split(reference)
1171  if not dirname: dirname = '.'
1172  head = basename + "."
1173  head_len = len(head)
1174  platform = platformSplit(self.GetPlatform())
1175  candidates = []
1176  for f in os.listdir(dirname):
1177  if f.startswith(head):
1178  req_plat = platformSplit(f[head_len:])
1179  if platform.issuperset(req_plat):
1180  candidates.append( (len(req_plat), f) )
1181  if candidates: # take the one with highest matching
1182  # FIXME: it is not possible to say if x86_64-slc5-gcc43-dbg
1183  # has to use ref.x86_64-gcc43 or ref.slc5-dbg
1184  candidates.sort()
1185  reference = os.path.join(dirname, candidates[-1][1])
1186  return reference
1187 
1188  def CheckTTreesSummaries(self, stdout, result, causes,
1189  trees_dict = None,
1190  ignore = r"Basket|.*size|Compression"):
1191  """
1192  Compare the TTree summaries in stdout with the ones in trees_dict or in
1193  the reference file. By default ignore the size, compression and basket
1194  fields.
1195  The presence of TTree summaries when none is expected is not a failure.
1196  """
1197  if trees_dict is None:
1198  reference = self._expandReferenceFileName(self.reference)
1199  # call the validator if the file exists
1200  if reference and os.path.isfile(reference):
1201  trees_dict = findTTreeSummaries(open(reference).read())
1202  else:
1203  trees_dict = {}
1204 
1205  from pprint import PrettyPrinter
1206  pp = PrettyPrinter()
1207  if trees_dict:
1208  result["GaudiTest.TTrees.expected"] = result.Quote(pp.pformat(trees_dict))
1209  if ignore:
1210  result["GaudiTest.TTrees.ignore"] = result.Quote(ignore)
1211 
1212  trees = findTTreeSummaries(stdout)
1213  failed = cmpTreesDicts(trees_dict, trees, ignore)
1214  if failed:
1215  causes.append("trees summaries")
1216  msg = "%s: %s != %s" % getCmpFailingValues(trees_dict, trees, failed)
1217  result["GaudiTest.TTrees.failure_on"] = result.Quote(msg)
1218  result["GaudiTest.TTrees.found"] = result.Quote(pp.pformat(trees))
1219 
1220  return causes
1221 
1222  def CheckHistosSummaries(self, stdout, result, causes,
1223  dict = None,
1224  ignore = None):
1225  """
1226  Compare the TTree summaries in stdout with the ones in trees_dict or in
1227  the reference file. By default ignore the size, compression and basket
1228  fields.
1229  The presence of TTree summaries when none is expected is not a failure.
1230  """
1231  if dict is None:
1232  reference = self._expandReferenceFileName(self.reference)
1233  # call the validator if the file exists
1234  if reference and os.path.isfile(reference):
1235  dict = findHistosSummaries(open(reference).read())
1236  else:
1237  dict = {}
1238 
1239  from pprint import PrettyPrinter
1240  pp = PrettyPrinter()
1241  if dict:
1242  result["GaudiTest.Histos.expected"] = result.Quote(pp.pformat(dict))
1243  if ignore:
1244  result["GaudiTest.Histos.ignore"] = result.Quote(ignore)
1245 
1246  histos = findHistosSummaries(stdout)
1247  failed = cmpTreesDicts(dict, histos, ignore)
1248  if failed:
1249  causes.append("histos summaries")
1250  msg = "%s: %s != %s" % getCmpFailingValues(dict, histos, failed)
1251  result["GaudiTest.Histos.failure_on"] = result.Quote(msg)
1252  result["GaudiTest.Histos.found"] = result.Quote(pp.pformat(histos))
1253 
1254  return causes
1255 
1256  def ValidateWithReference(self, stdout, stderr, result, causes, preproc = None):
1257  """
1258  Default validation action: compare standard output and error to the
1259  reference files.
1260  """
1261  # set the default output preprocessor
1262  if preproc is None:
1263  preproc = normalizeExamples
1264  # check standard output
1265  reference = self._expandReferenceFileName(self.reference)
1266  # call the validator if the file exists
1267  if reference and os.path.isfile(reference):
1268  result["GaudiTest.output_reference"] = reference
1269  causes += ReferenceFileValidator(reference,
1270  "standard output",
1271  "GaudiTest.output_diff",
1272  preproc = preproc)(stdout, result)
1273 
1274  # Compare TTree summaries
1275  causes = self.CheckTTreesSummaries(stdout, result, causes)
1276  causes = self.CheckHistosSummaries(stdout, result, causes)
1277 
1278  if causes: # Write a new reference file for stdout
1279  try:
1280  newref = open(reference + ".new","w")
1281  # sanitize newlines
1282  for l in stdout.splitlines():
1283  newref.write(l.rstrip() + '\n')
1284  del newref # flush and close
1285  except IOError:
1286  # Ignore IO errors when trying to update reference files
1287  # because we may be in a read-only filesystem
1288  pass
1289 
1290  # check standard error
1291  reference = self._expandReferenceFileName(self.error_reference)
1292  # call the validator if we have a file to use
1293  if reference and os.path.isfile(reference):
1294  result["GaudiTest.error_reference"] = reference
1295  newcauses = ReferenceFileValidator(reference,
1296  "standard error",
1297  "GaudiTest.error_diff",
1298  preproc = preproc)(stderr, result)
1299  causes += newcauses
1300  if newcauses: # Write a new reference file for stdedd
1301  newref = open(reference + ".new","w")
1302  # sanitize newlines
1303  for l in stderr.splitlines():
1304  newref.write(l.rstrip() + '\n')
1305  del newref # flush and close
1306  else:
1307  causes += BasicOutputValidator(self.stderr,
1308  "standard error",
1309  "ExecTest.expected_stderr")(stderr, result)
1310 
1311  return causes
1312 
1313  def ValidateOutput(self, stdout, stderr, result):
1314  causes = []
1315  # if the test definition contains a custom validator, use it
1316  if self.validator.strip() != "":
1317  class CallWrapper(object):
1318  """
1319  Small wrapper class to dynamically bind some default arguments
1320  to a callable.
1321  """
1322  def __init__(self, callable, extra_args = {}):
1323  self.callable = callable
1324  self.extra_args = extra_args
1325  # get the list of names of positional arguments
1326  from inspect import getargspec
1327  self.args_order = getargspec(callable)[0]
1328  # Remove "self" from the list of positional arguments
1329  # since it is added automatically
1330  if self.args_order[0] == "self":
1331  del self.args_order[0]
1332  def __call__(self, *args, **kwargs):
1333  # Check which positional arguments are used
1334  positional = self.args_order[:len(args)]
1335 
1336  kwargs = dict(kwargs) # copy the arguments dictionary
1337  for a in self.extra_args:
1338  # use "extra_args" for the arguments not specified as
1339  # positional or keyword
1340  if a not in positional and a not in kwargs:
1341  kwargs[a] = self.extra_args[a]
1342  return apply(self.callable, args, kwargs)
1343  # local names to be exposed in the script
1344  exported_symbols = {"self":self,
1345  "stdout":stdout,
1346  "stderr":stderr,
1347  "result":result,
1348  "causes":causes,
1349  "findReferenceBlock":
1350  CallWrapper(findReferenceBlock, {"stdout":stdout,
1351  "result":result,
1352  "causes":causes}),
1353  "validateWithReference":
1354  CallWrapper(self.ValidateWithReference, {"stdout":stdout,
1355  "stderr":stderr,
1356  "result":result,
1357  "causes":causes}),
1358  "countErrorLines":
1359  CallWrapper(countErrorLines, {"stdout":stdout,
1360  "result":result,
1361  "causes":causes}),
1362  "checkTTreesSummaries":
1363  CallWrapper(self.CheckTTreesSummaries, {"stdout":stdout,
1364  "result":result,
1365  "causes":causes}),
1366  "checkHistosSummaries":
1367  CallWrapper(self.CheckHistosSummaries, {"stdout":stdout,
1368  "result":result,
1369  "causes":causes}),
1370 
1371  }
1372  exec self.validator in globals(), exported_symbols
1373  else:
1374  self.ValidateWithReference(stdout, stderr, result, causes)
1375 
1376  return causes
1377 
1378  def DumpEnvironment(self, result):
1379  """
1380  Add the content of the environment to the result object.
1381 
1382  Copied from the QMTest class of COOL.
1383  """
1384  vars = os.environ.keys()
1385  vars.sort()
1386  result['GaudiTest.environment'] = \
1387  result.Quote('\n'.join(["%s=%s"%(v,os.environ[v]) for v in vars]))
1388 
1389  def Run(self, context, result):
1390  """Run the test.
1391 
1392  'context' -- A 'Context' giving run-time parameters to the
1393  test.
1394 
1395  'result' -- A 'Result' object. The outcome will be
1396  'Result.PASS' when this method is called. The 'result' may be
1397  modified by this method to indicate outcomes other than
1398  'Result.PASS' or to add annotations."""
1399 
1400  # Check if the platform is supported
1401  if self.PlatformIsNotSupported(context, result):
1402  return
1403 
1404  # Prepare program name and arguments (expanding variables, and converting to absolute)
1405  if self.program:
1406  prog = rationalizepath(self.program)
1407  elif "GAUDIEXE" in os.environ:
1408  prog = os.environ["GAUDIEXE"]
1409  else:
1410  prog = "Gaudi.exe"
1411  self.program = prog
1412 
1413  dummy, prog_ext = os.path.splitext(prog)
1414  if prog_ext not in [ ".exe", ".py", ".bat" ] and self.isWinPlatform():
1415  prog += ".exe"
1416  prog_ext = ".exe"
1417 
1418  prog = which(prog) or prog
1419 
1420  # Convert paths to absolute paths in arguments and reference files
1421  args = map(rationalizepath, self.args)
1424 
1425 
1426  # check if the user provided inline options
1427  tmpfile = None
1428  if self.options.strip():
1429  ext = ".opts"
1430  if re.search(r"from\s+Gaudi.Configuration\s+import\s+\*|from\s+Configurables\s+import", self.options):
1431  ext = ".py"
1432  tmpfile = TempFile(ext)
1433  tmpfile.writelines("\n".join(self.options.splitlines()))
1434  tmpfile.flush()
1435  args.append(tmpfile.name)
1436  result["GaudiTest.options"] = result.Quote(self.options)
1437 
1438  # if the program is a python file, execute it through python
1439  if prog_ext == ".py":
1440  args.insert(0,prog)
1441  if self.isWinPlatform():
1442  prog = which("python.exe") or "python.exe"
1443  else:
1444  prog = which("python") or "python"
1445 
1446  # Change to the working directory if specified or to the default temporary
1447  origdir = os.getcwd()
1448  if self.workdir:
1449  os.chdir(str(os.path.normpath(os.path.expandvars(self.workdir))))
1450  elif self.use_temp_dir == "true":
1451  if "QMTEST_TMPDIR" in os.environ:
1452  qmtest_tmpdir = os.environ["QMTEST_TMPDIR"]
1453  if not os.path.exists(qmtest_tmpdir):
1454  os.makedirs(qmtest_tmpdir)
1455  os.chdir(qmtest_tmpdir)
1456  elif "qmtest.tmpdir" in context:
1457  os.chdir(context["qmtest.tmpdir"])
1458 
1459  if "QMTEST_IGNORE_TIMEOUT" not in os.environ:
1460  self.timeout = max(self.timeout,600)
1461  else:
1462  self.timeout = -1
1463 
1464  try:
1465  # Generate eclipse.org debug launcher for the test
1466  self._CreateEclipseLaunch(prog, args, destdir = os.path.join(origdir, '.eclipse'))
1467  # Run the test
1468  self.RunProgram(prog,
1469  [ prog ] + args,
1470  context, result)
1471  # Record the content of the enfironment for failing tests
1472  if result.GetOutcome() not in [ result.PASS ]:
1473  self.DumpEnvironment(result)
1474  finally:
1475  # revert to the original directory
1476  os.chdir(origdir)
1477 
1478  def RunProgram(self, program, arguments, context, result):
1479  """Run the 'program'.
1480 
1481  'program' -- The path to the program to run.
1482 
1483  'arguments' -- A list of the arguments to the program. This
1484  list must contain a first argument corresponding to 'argv[0]'.
1485 
1486  'context' -- A 'Context' giving run-time parameters to the
1487  test.
1488 
1489  'result' -- A 'Result' object. The outcome will be
1490  'Result.PASS' when this method is called. The 'result' may be
1491  modified by this method to indicate outcomes other than
1492  'Result.PASS' or to add annotations.
1493 
1494  @attention: This method has been copied from command.ExecTestBase
1495  (QMTest 2.3.0) and modified to keep stdout and stderr
1496  for tests that have been terminated by a signal.
1497  (Fundamental for debugging in the Application Area)
1498  """
1499 
1500  # Construct the environment.
1501  environment = self.MakeEnvironment(context)
1502  # FIXME: whithout this, we get some spurious '\x1b[?1034' in the std out on SLC6
1503  if "slc6" in environment.get('CMTCONFIG', ''):
1504  environment['TERM'] = 'dumb'
1505  # Create the executable.
1506  if self.timeout >= 0:
1507  timeout = self.timeout
1508  else:
1509  # If no timeout was specified, we sill run this process in a
1510  # separate process group and kill the entire process group
1511  # when the child is done executing. That means that
1512  # orphaned child processes created by the test will be
1513  # cleaned up.
1514  timeout = -2
1515  e = GaudiFilterExecutable(self.stdin, timeout)
1516  # Run it.
1517  exit_status = e.Run(arguments, environment, path = program)
1518  # Get the stack trace from the temporary file (if present)
1519  if e.stack_trace_file and os.path.exists(e.stack_trace_file):
1520  stack_trace = open(e.stack_trace_file).read()
1521  os.remove(e.stack_trace_file)
1522  else:
1523  stack_trace = None
1524  if stack_trace:
1525  result["ExecTest.stack_trace"] = result.Quote(stack_trace)
1526 
1527  # If the process terminated normally, check the outputs.
1528  if (sys.platform == "win32" or os.WIFEXITED(exit_status)
1529  or self.signal == os.WTERMSIG(exit_status)):
1530  # There are no causes of failure yet.
1531  causes = []
1532  # The target program terminated normally. Extract the
1533  # exit code, if this test checks it.
1534  if self.exit_code is None:
1535  exit_code = None
1536  elif sys.platform == "win32":
1537  exit_code = exit_status
1538  else:
1539  exit_code = os.WEXITSTATUS(exit_status)
1540  # Get the output generated by the program.
1541  stdout = e.stdout
1542  stderr = e.stderr
1543  # Record the results.
1544  result["ExecTest.exit_code"] = str(exit_code)
1545  result["ExecTest.stdout"] = result.Quote(stdout)
1546  result["ExecTest.stderr"] = result.Quote(stderr)
1547  # Check to see if the exit code matches.
1548  if exit_code != self.exit_code:
1549  causes.append("exit_code")
1550  result["ExecTest.expected_exit_code"] \
1551  = str(self.exit_code)
1552  # Validate the output.
1553  causes += self.ValidateOutput(stdout, stderr, result)
1554  # If anything went wrong, the test failed.
1555  if causes:
1556  result.Fail("Unexpected %s." % string.join(causes, ", "))
1557  elif os.WIFSIGNALED(exit_status):
1558  # The target program terminated with a signal. Construe
1559  # that as a test failure.
1560  signal_number = str(os.WTERMSIG(exit_status))
1561  if not stack_trace:
1562  result.Fail("Program terminated by signal.")
1563  else:
1564  # The presence of stack_trace means tha we stopped the job because
1565  # of a time-out
1566  result.Fail("Exceeded time limit (%ds), terminated." % timeout)
1567  result["ExecTest.signal_number"] = signal_number
1568  result["ExecTest.stdout"] = result.Quote(e.stdout)
1569  result["ExecTest.stderr"] = result.Quote(e.stderr)
1570  if self.signal:
1571  result["ExecTest.expected_signal_number"] = str(self.signal)
1572  elif os.WIFSTOPPED(exit_status):
1573  # The target program was stopped. Construe that as a
1574  # test failure.
1575  signal_number = str(os.WSTOPSIG(exit_status))
1576  if not stack_trace:
1577  result.Fail("Program stopped by signal.")
1578  else:
1579  # The presence of stack_trace means tha we stopped the job because
1580  # of a time-out
1581  result.Fail("Exceeded time limit (%ds), stopped." % timeout)
1582  result["ExecTest.signal_number"] = signal_number
1583  result["ExecTest.stdout"] = result.Quote(e.stdout)
1584  result["ExecTest.stderr"] = result.Quote(e.stderr)
1585  else:
1586  # The target program terminated abnormally in some other
1587  # manner. (This shouldn't normally happen...)
1588  result.Fail("Program did not terminate normally.")
1589 
1590  # Marco Cl.: This is a special trick to fix a "problem" with the output
1591  # of gaudi jobs when they use colors
1592  esc = '\x1b'
1593  repr_esc = '\\x1b'
1594  result["ExecTest.stdout"] = result["ExecTest.stdout"].replace(esc,repr_esc)
1595  # TODO: (MCl) improve the hack for colors in standard output
1596  # may be converting them to HTML tags
1597 
1598  def _CreateEclipseLaunch(self, prog, args, destdir = None):
1599  # Find the project name used in ecplise.
1600  # The name is in a file called ".project" in one of the parent directories
1601  projbasedir = os.path.normpath(destdir)
1602  while not os.path.exists(os.path.join(projbasedir, ".project")):
1603  oldprojdir = projbasedir
1604  projbasedir = os.path.normpath(os.path.join(projbasedir, os.pardir))
1605  # FIXME: the root level is invariant when trying to go up one level,
1606  # but it must be cheched on windows
1607  if oldprojdir == projbasedir:
1608  # If we cannot find a .project, so no point in creating a .launch file
1609  return
1610  # Ensure that we have a place where to write.
1611  if not os.path.exists(destdir):
1612  os.makedirs(destdir)
1613  # Use ElementTree to parse the XML file
1614  from xml.etree import ElementTree as ET
1615  t = ET.parse(os.path.join(projbasedir, ".project"))
1616  projectName = t.find("name").text
1617 
1618  # prepare the name/path of the generated file
1619  destfile = "%s.launch" % self._Runnable__id
1620  if destdir:
1621  destfile = os.path.join(destdir, destfile)
1622 
1623  if self.options.strip():
1624  # this means we have some custom options in the qmt file, so we have
1625  # to copy them from the temporary file at the end of the arguments
1626  # in another file
1627  tempfile = args.pop()
1628  optsfile = destfile + os.path.splitext(tempfile)[1]
1629  shutil.copyfile(tempfile, optsfile)
1630  args.append(optsfile)
1631 
1632  # prepare the data to insert in the XML file
1633  from xml.sax.saxutils import quoteattr # useful to quote XML special chars
1634  data = {}
1635  # Note: the "quoteattr(k)" is not needed because special chars cannot be part of a variable name,
1636  # but it doesn't harm.
1637  data["environment"] = "\n".join(['<mapEntry key=%s value=%s/>' % (quoteattr(k), quoteattr(v))
1638  for k, v in os.environ.iteritems()
1639  if k not in ('MAKEOVERRIDES', 'MAKEFLAGS', 'MAKELEVEL')])
1640 
1641  data["exec"] = which(prog) or prog
1642  if os.path.basename(data["exec"]).lower().startswith("python"):
1643  data["stopAtMain"] = "false" # do not stop at main when debugging Python scripts
1644  else:
1645  data["stopAtMain"] = "true"
1646 
1647  data["args"] = "&#10;".join(map(rationalizepath, args))
1648  if self.isWinPlatform():
1649  data["args"] = "&#10;".join(["/debugexe"] + map(rationalizepath, [data["exec"]] + args))
1650  data["exec"] = which("vcexpress.exe")
1651 
1652  if not self.use_temp_dir:
1653  data["workdir"] = os.getcwd()
1654  else:
1655  # If the test is using a tmporary directory, it is better to run it
1656  # in the same directory as the .launch file when debugged in eclipse
1657  data["workdir"] = destdir
1658 
1659  data["project"] = projectName.strip()
1660 
1661  # Template for the XML file, based on eclipse 3.4
1662  xml = """<?xml version="1.0" encoding="UTF-8" standalone="no"?>
1663 <launchConfiguration type="org.eclipse.cdt.launch.applicationLaunchType">
1664 <booleanAttribute key="org.eclipse.cdt.debug.mi.core.AUTO_SOLIB" value="true"/>
1665 <listAttribute key="org.eclipse.cdt.debug.mi.core.AUTO_SOLIB_LIST"/>
1666 <stringAttribute key="org.eclipse.cdt.debug.mi.core.DEBUG_NAME" value="gdb"/>
1667 <stringAttribute key="org.eclipse.cdt.debug.mi.core.GDB_INIT" value=".gdbinit"/>
1668 <listAttribute key="org.eclipse.cdt.debug.mi.core.SOLIB_PATH"/>
1669 <booleanAttribute key="org.eclipse.cdt.debug.mi.core.STOP_ON_SOLIB_EVENTS" value="false"/>
1670 <booleanAttribute key="org.eclipse.cdt.debug.mi.core.breakpointsFullPath" value="false"/>
1671 <stringAttribute key="org.eclipse.cdt.debug.mi.core.commandFactory" value="org.eclipse.cdt.debug.mi.core.standardCommandFactory"/>
1672 <stringAttribute key="org.eclipse.cdt.debug.mi.core.protocol" value="mi"/>
1673 <booleanAttribute key="org.eclipse.cdt.debug.mi.core.verboseMode" value="false"/>
1674 <intAttribute key="org.eclipse.cdt.launch.ATTR_BUILD_BEFORE_LAUNCH_ATTR" value="0"/>
1675 <stringAttribute key="org.eclipse.cdt.launch.COREFILE_PATH" value=""/>
1676 <stringAttribute key="org.eclipse.cdt.launch.DEBUGGER_ID" value="org.eclipse.cdt.debug.mi.core.CDebuggerNew"/>
1677 <stringAttribute key="org.eclipse.cdt.launch.DEBUGGER_REGISTER_GROUPS" value=""/>
1678 <stringAttribute key="org.eclipse.cdt.launch.DEBUGGER_START_MODE" value="run"/>
1679 <booleanAttribute key="org.eclipse.cdt.launch.DEBUGGER_STOP_AT_MAIN" value="%(stopAtMain)s"/>
1680 <stringAttribute key="org.eclipse.cdt.launch.DEBUGGER_STOP_AT_MAIN_SYMBOL" value="main"/>
1681 <booleanAttribute key="org.eclipse.cdt.launch.ENABLE_REGISTER_BOOKKEEPING" value="false"/>
1682 <booleanAttribute key="org.eclipse.cdt.launch.ENABLE_VARIABLE_BOOKKEEPING" value="false"/>
1683 <stringAttribute key="org.eclipse.cdt.launch.FORMAT" value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&lt;contentList/&gt;"/>
1684 <stringAttribute key="org.eclipse.cdt.launch.GLOBAL_VARIABLES" value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;globalVariableList/&gt;&#10;"/>
1685 <stringAttribute key="org.eclipse.cdt.launch.MEMORY_BLOCKS" value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;memoryBlockExpressionList/&gt;&#10;"/>
1686 <stringAttribute key="org.eclipse.cdt.launch.PROGRAM_ARGUMENTS" value="%(args)s"/>
1687 <stringAttribute key="org.eclipse.cdt.launch.PROGRAM_NAME" value="%(exec)s"/>
1688 <stringAttribute key="org.eclipse.cdt.launch.PROJECT_ATTR" value="%(project)s"/>
1689 <stringAttribute key="org.eclipse.cdt.launch.PROJECT_BUILD_CONFIG_ID_ATTR" value=""/>
1690 <stringAttribute key="org.eclipse.cdt.launch.WORKING_DIRECTORY" value="%(workdir)s"/>
1691 <booleanAttribute key="org.eclipse.cdt.launch.ui.ApplicationCDebuggerTab.DEFAULTS_SET" value="true"/>
1692 <booleanAttribute key="org.eclipse.cdt.launch.use_terminal" value="true"/>
1693 <listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_PATHS">
1694 <listEntry value="/%(project)s"/>
1695 </listAttribute>
1696 <listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_TYPES">
1697 <listEntry value="4"/>
1698 </listAttribute>
1699 <booleanAttribute key="org.eclipse.debug.core.appendEnvironmentVariables" value="false"/>
1700 <mapAttribute key="org.eclipse.debug.core.environmentVariables">
1701 %(environment)s
1702 </mapAttribute>
1703 <mapAttribute key="org.eclipse.debug.core.preferred_launchers">
1704 <mapEntry key="[debug]" value="org.eclipse.cdt.cdi.launch.localCLaunch"/>
1705 </mapAttribute>
1706 <listAttribute key="org.eclipse.debug.ui.favoriteGroups">
1707 <listEntry value="org.eclipse.debug.ui.launchGroup.debug"/>
1708 </listAttribute>
1709 </launchConfiguration>
1710 """ % data
1711 
1712  # Write the output file
1713  open(destfile, "w").write(xml)
1714  #open(destfile + "_copy.xml", "w").write(xml)
1715 
1716 
1717 try:
1718  import json
1719 except ImportError:
1720  # Use simplejson for LCG
1721  import simplejson as json
1722 
1723 class HTMLResultStream(ResultStream):
1724  """An 'HTMLResultStream' writes its output to a set of HTML files.
1725 
1726  The argument 'dir' is used to select the destination directory for the HTML
1727  report.
1728  The destination directory may already contain the report from a previous run
1729  (for example of a different package), in which case it will be extended to
1730  include the new data.
1731  """
1732  arguments = [
1733  qm.fields.TextField(
1734  name = "dir",
1735  title = "Destination Directory",
1736  description = """The name of the directory.
1737 
1738  All results will be written to the directory indicated.""",
1739  verbatim = "true",
1740  default_value = ""),
1741  ]
1742 
1743  def __init__(self, arguments = None, **args):
1744  """Prepare the destination directory.
1745 
1746  Creates the destination directory and store in it some preliminary
1747  annotations and the static files found in the template directory
1748  'html_report'.
1749  """
1750  ResultStream.__init__(self, arguments, **args)
1751  self._summary = []
1752  self._summaryFile = os.path.join(self.dir, "summary.json")
1753  self._annotationsFile = os.path.join(self.dir, "annotations.json")
1754  # Prepare the destination directory using the template
1755  templateDir = os.path.join(os.path.dirname(__file__), "html_report")
1756  if not os.path.isdir(self.dir):
1757  os.makedirs(self.dir)
1758  # Copy the files in the template directory excluding the directories
1759  for f in os.listdir(templateDir):
1760  src = os.path.join(templateDir, f)
1761  dst = os.path.join(self.dir, f)
1762  if not os.path.isdir(src) and not os.path.exists(dst):
1763  shutil.copy(src, dst)
1764  # Add some non-QMTest attributes
1765  if "CMTCONFIG" in os.environ:
1766  self.WriteAnnotation("cmt.cmtconfig", os.environ["CMTCONFIG"])
1767  import socket
1768  self.WriteAnnotation("hostname", socket.gethostname())
1769 
1770  def _updateSummary(self):
1771  """Helper function to extend the global summary file in the destination
1772  directory.
1773  """
1774  if os.path.exists(self._summaryFile):
1775  oldSummary = json.load(open(self._summaryFile))
1776  else:
1777  oldSummary = []
1778  ids = set([ i["id"] for i in self._summary ])
1779  newSummary = [ i for i in oldSummary if i["id"] not in ids ]
1780  newSummary.extend(self._summary)
1781  json.dump(newSummary, open(self._summaryFile, "w"),
1782  sort_keys = True)
1783 
1784  def WriteAnnotation(self, key, value):
1785  """Writes the annotation to the annotation file.
1786  If the key is already present with a different value, the value becomes
1787  a list and the new value is appended to it, except for start_time and
1788  end_time.
1789  """
1790  # Initialize the annotation dict from the file (if present)
1791  if os.path.exists(self._annotationsFile):
1792  annotations = json.load(open(self._annotationsFile))
1793  else:
1794  annotations = {}
1795  # hack because we do not have proper JSON support
1796  key, value = map(str, [key, value])
1797  if key == "qmtest.run.start_time":
1798  # Special handling of the start time:
1799  # if we are updating a result, we have to keep the original start
1800  # time, but remove the original end time to mark the report to be
1801  # in progress.
1802  if key not in annotations:
1803  annotations[key] = value
1804  if "qmtest.run.end_time" in annotations:
1805  del annotations["qmtest.run.end_time"]
1806  else:
1807  # All other annotations are added to a list
1808  if key in annotations:
1809  old = annotations[key]
1810  if type(old) is list:
1811  if value not in old:
1812  annotations[key].append(value)
1813  elif value != old:
1814  annotations[key] = [old, value]
1815  else:
1816  annotations[key] = value
1817  # Write the new annotations file
1818  json.dump(annotations, open(self._annotationsFile, "w"),
1819  sort_keys = True)
1820 
1821  def WriteResult(self, result):
1822  """Prepare the test result directory in the destination directory storing
1823  into it the result fields.
1824  A summary of the test result is stored both in a file in the test directory
1825  and in the global summary file.
1826  """
1827  summary = {}
1828  summary["id"] = result.GetId()
1829  summary["outcome"] = result.GetOutcome()
1830  summary["cause"] = result.GetCause()
1831  summary["fields"] = result.keys()
1832  summary["fields"].sort()
1833 
1834  # Since we miss proper JSON support, I hack a bit
1835  for f in ["id", "outcome", "cause"]:
1836  summary[f] = str(summary[f])
1837  summary["fields"] = map(str, summary["fields"])
1838 
1839  self._summary.append(summary)
1840 
1841  # format:
1842  # testname/summary.json
1843  # testname/field1
1844  # testname/field2
1845  testOutDir = os.path.join(self.dir, summary["id"])
1846  if not os.path.isdir(testOutDir):
1847  os.makedirs(testOutDir)
1848  json.dump(summary, open(os.path.join(testOutDir, "summary.json"), "w"),
1849  sort_keys = True)
1850  for f in summary["fields"]:
1851  open(os.path.join(testOutDir, f), "w").write(result[f])
1852 
1853  self._updateSummary()
1854 
1855  def Summarize(self):
1856  # Not implemented.
1857  pass

Generated at Wed Dec 4 2013 14:33:10 for Gaudi Framework, version v24r2 by Doxygen version 1.8.2 written by Dimitri van Heesch, © 1997-2004