Gaudi Framework, version v23r9

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

Generated at Thu Jul 18 2013 12:18:04 for Gaudi Framework, version v23r9 by Doxygen version 1.8.2 written by Dimitri van Heesch, © 1997-2004