Gaudi Framework, version v23r5

Home   Generated: Wed Nov 28 2012
 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 
1107  def PlatformIsNotSupported(self, context, result):
1108  platform = self.GetPlatform()
1109  unsupported = [ re.compile(x)
1110  for x in [ str(y).strip()
1111  for y in self.unsupported_platforms ]
1112  if x
1113  ]
1114  for p_re in unsupported:
1115  if p_re.search(platform):
1116  result.SetOutcome(result.UNTESTED)
1117  result[result.CAUSE] = 'Platform not supported.'
1118  return True
1119  return False
1120 
1121  def GetPlatform(self):
1122  """
1123  Return the platform Id defined in CMTCONFIG or SCRAM_ARCH.
1124  """
1125  arch = "None"
1126  # check architecture name
1127  if "CMTCONFIG" in os.environ:
1128  arch = os.environ["CMTCONFIG"]
1129  elif "SCRAM_ARCH" in os.environ:
1130  arch = os.environ["SCRAM_ARCH"]
1131  return arch
1132 
1133  def isWinPlatform(self):
1134  """
1135  Return True if the current platform is Windows.
1136 
1137  This function was needed because of the change in the CMTCONFIG format,
1138  from win32_vc71_dbg to i686-winxp-vc9-dbg.
1139  """
1140  platform = self.GetPlatform()
1141  return "winxp" in platform or platform.startswith("win")
1142 
1143  def _expandReferenceFileName(self, reffile):
1144  # if no file is passed, do nothing
1145  if not reffile:
1146  return ""
1147 
1148  # function to split an extension in constituents parts
1149  platformSplit = lambda p: set(p.split('-' in p and '-' or '_'))
1150 
1151  reference = os.path.normpath(os.path.expandvars(reffile))
1152  # old-style platform-specific reference name
1153  spec_ref = reference[:-3] + self.GetPlatform()[0:3] + reference[-3:]
1154  if os.path.isfile(spec_ref):
1155  reference = spec_ref
1156  else: # look for new-style platform specific reference files:
1157  # get all the files whose name start with the reference filename
1158  dirname, basename = os.path.split(reference)
1159  if not dirname: dirname = '.'
1160  head = basename + "."
1161  head_len = len(head)
1162  platform = platformSplit(self.GetPlatform())
1163  candidates = []
1164  for f in os.listdir(dirname):
1165  if f.startswith(head):
1166  req_plat = platformSplit(f[head_len:])
1167  if platform.issuperset(req_plat):
1168  candidates.append( (len(req_plat), f) )
1169  if candidates: # take the one with highest matching
1170  # FIXME: it is not possible to say if x86_64-slc5-gcc43-dbg
1171  # has to use ref.x86_64-gcc43 or ref.slc5-dbg
1172  candidates.sort()
1173  reference = os.path.join(dirname, candidates[-1][1])
1174  return reference
1175 
1176  def CheckTTreesSummaries(self, stdout, result, causes,
1177  trees_dict = None,
1178  ignore = r"Basket|.*size|Compression"):
1179  """
1180  Compare the TTree summaries in stdout with the ones in trees_dict or in
1181  the reference file. By default ignore the size, compression and basket
1182  fields.
1183  The presence of TTree summaries when none is expected is not a failure.
1184  """
1185  if trees_dict is None:
1186  reference = self._expandReferenceFileName(self.reference)
1187  # call the validator if the file exists
1188  if reference and os.path.isfile(reference):
1189  trees_dict = findTTreeSummaries(open(reference).read())
1190  else:
1191  trees_dict = {}
1192 
1193  from pprint import PrettyPrinter
1194  pp = PrettyPrinter()
1195  if trees_dict:
1196  result["GaudiTest.TTrees.expected"] = result.Quote(pp.pformat(trees_dict))
1197  if ignore:
1198  result["GaudiTest.TTrees.ignore"] = result.Quote(ignore)
1199 
1200  trees = findTTreeSummaries(stdout)
1201  failed = cmpTreesDicts(trees_dict, trees, ignore)
1202  if failed:
1203  causes.append("trees summaries")
1204  msg = "%s: %s != %s" % getCmpFailingValues(trees_dict, trees, failed)
1205  result["GaudiTest.TTrees.failure_on"] = result.Quote(msg)
1206  result["GaudiTest.TTrees.found"] = result.Quote(pp.pformat(trees))
1207 
1208  return causes
1209 
1210  def CheckHistosSummaries(self, stdout, result, causes,
1211  dict = None,
1212  ignore = None):
1213  """
1214  Compare the TTree summaries in stdout with the ones in trees_dict or in
1215  the reference file. By default ignore the size, compression and basket
1216  fields.
1217  The presence of TTree summaries when none is expected is not a failure.
1218  """
1219  if dict is None:
1220  reference = self._expandReferenceFileName(self.reference)
1221  # call the validator if the file exists
1222  if reference and os.path.isfile(reference):
1223  dict = findHistosSummaries(open(reference).read())
1224  else:
1225  dict = {}
1226 
1227  from pprint import PrettyPrinter
1228  pp = PrettyPrinter()
1229  if dict:
1230  result["GaudiTest.Histos.expected"] = result.Quote(pp.pformat(dict))
1231  if ignore:
1232  result["GaudiTest.Histos.ignore"] = result.Quote(ignore)
1233 
1234  histos = findHistosSummaries(stdout)
1235  failed = cmpTreesDicts(dict, histos, ignore)
1236  if failed:
1237  causes.append("histos summaries")
1238  msg = "%s: %s != %s" % getCmpFailingValues(dict, histos, failed)
1239  result["GaudiTest.Histos.failure_on"] = result.Quote(msg)
1240  result["GaudiTest.Histos.found"] = result.Quote(pp.pformat(histos))
1241 
1242  return causes
1243 
1244  def ValidateWithReference(self, stdout, stderr, result, causes, preproc = None):
1245  """
1246  Default validation action: compare standard output and error to the
1247  reference files.
1248  """
1249  # set the default output preprocessor
1250  if preproc is None:
1251  preproc = normalizeExamples
1252  # check standard output
1253  reference = self._expandReferenceFileName(self.reference)
1254  # call the validator if the file exists
1255  if reference and os.path.isfile(reference):
1256  result["GaudiTest.output_reference"] = reference
1257  causes += ReferenceFileValidator(reference,
1258  "standard output",
1259  "GaudiTest.output_diff",
1260  preproc = preproc)(stdout, result)
1261 
1262  # Compare TTree summaries
1263  causes = self.CheckTTreesSummaries(stdout, result, causes)
1264  causes = self.CheckHistosSummaries(stdout, result, causes)
1265 
1266  if causes: # Write a new reference file for stdout
1267  try:
1268  newref = open(reference + ".new","w")
1269  # sanitize newlines
1270  for l in stdout.splitlines():
1271  newref.write(l.rstrip() + '\n')
1272  del newref # flush and close
1273  except IOError:
1274  # Ignore IO errors when trying to update reference files
1275  # because we may be in a read-only filesystem
1276  pass
1277 
1278  # check standard error
1279  reference = self._expandReferenceFileName(self.error_reference)
1280  # call the validator if we have a file to use
1281  if reference and os.path.isfile(reference):
1282  result["GaudiTest.error_reference"] = reference
1283  newcauses = ReferenceFileValidator(reference,
1284  "standard error",
1285  "GaudiTest.error_diff",
1286  preproc = preproc)(stderr, result)
1287  causes += newcauses
1288  if newcauses: # Write a new reference file for stdedd
1289  newref = open(reference + ".new","w")
1290  # sanitize newlines
1291  for l in stderr.splitlines():
1292  newref.write(l.rstrip() + '\n')
1293  del newref # flush and close
1294  else:
1295  causes += BasicOutputValidator(self.stderr,
1296  "standard error",
1297  "ExecTest.expected_stderr")(stderr, result)
1298 
1299  return causes
1300 
1301  def ValidateOutput(self, stdout, stderr, result):
1302  causes = []
1303  # if the test definition contains a custom validator, use it
1304  if self.validator.strip() != "":
1305  class CallWrapper(object):
1306  """
1307  Small wrapper class to dynamically bind some default arguments
1308  to a callable.
1309  """
1310  def __init__(self, callable, extra_args = {}):
1311  self.callable = callable
1312  self.extra_args = extra_args
1313  # get the list of names of positional arguments
1314  from inspect import getargspec
1315  self.args_order = getargspec(callable)[0]
1316  # Remove "self" from the list of positional arguments
1317  # since it is added automatically
1318  if self.args_order[0] == "self":
1319  del self.args_order[0]
1320  def __call__(self, *args, **kwargs):
1321  # Check which positional arguments are used
1322  positional = self.args_order[:len(args)]
1323 
1324  kwargs = dict(kwargs) # copy the arguments dictionary
1325  for a in self.extra_args:
1326  # use "extra_args" for the arguments not specified as
1327  # positional or keyword
1328  if a not in positional and a not in kwargs:
1329  kwargs[a] = self.extra_args[a]
1330  return apply(self.callable, args, kwargs)
1331  # local names to be exposed in the script
1332  exported_symbols = {"self":self,
1333  "stdout":stdout,
1334  "stderr":stderr,
1335  "result":result,
1336  "causes":causes,
1337  "findReferenceBlock":
1338  CallWrapper(findReferenceBlock, {"stdout":stdout,
1339  "result":result,
1340  "causes":causes}),
1341  "validateWithReference":
1342  CallWrapper(self.ValidateWithReference, {"stdout":stdout,
1343  "stderr":stderr,
1344  "result":result,
1345  "causes":causes}),
1346  "countErrorLines":
1347  CallWrapper(countErrorLines, {"stdout":stdout,
1348  "result":result,
1349  "causes":causes}),
1350  "checkTTreesSummaries":
1351  CallWrapper(self.CheckTTreesSummaries, {"stdout":stdout,
1352  "result":result,
1353  "causes":causes}),
1354  "checkHistosSummaries":
1355  CallWrapper(self.CheckHistosSummaries, {"stdout":stdout,
1356  "result":result,
1357  "causes":causes}),
1358 
1359  }
1360  exec self.validator in globals(), exported_symbols
1361  else:
1362  self.ValidateWithReference(stdout, stderr, result, causes)
1363 
1364  return causes
1365 
1366  def DumpEnvironment(self, result):
1367  """
1368  Add the content of the environment to the result object.
1369 
1370  Copied from the QMTest class of COOL.
1371  """
1372  vars = os.environ.keys()
1373  vars.sort()
1374  result['GaudiTest.environment'] = \
1375  result.Quote('\n'.join(["%s=%s"%(v,os.environ[v]) for v in vars]))
1376 
1377  def Run(self, context, result):
1378  """Run the test.
1379 
1380  'context' -- A 'Context' giving run-time parameters to the
1381  test.
1382 
1383  'result' -- A 'Result' object. The outcome will be
1384  'Result.PASS' when this method is called. The 'result' may be
1385  modified by this method to indicate outcomes other than
1386  'Result.PASS' or to add annotations."""
1387 
1388  # Check if the platform is supported
1389  if self.PlatformIsNotSupported(context, result):
1390  return
1391 
1392  # Prepare program name and arguments (expanding variables, and converting to absolute)
1393  if self.program:
1394  prog = rationalizepath(self.program)
1395  elif "GAUDIEXE" in os.environ:
1396  prog = os.environ["GAUDIEXE"]
1397  else:
1398  prog = "Gaudi.exe"
1399  self.program = prog
1400 
1401  dummy, prog_ext = os.path.splitext(prog)
1402  if prog_ext not in [ ".exe", ".py", ".bat" ] and self.isWinPlatform():
1403  prog += ".exe"
1404  prog_ext = ".exe"
1405 
1406  prog = which(prog) or prog
1407 
1408  # Convert paths to absolute paths in arguments and reference files
1409  args = map(rationalizepath, self.args)
1412 
1413 
1414  # check if the user provided inline options
1415  tmpfile = None
1416  if self.options.strip():
1417  ext = ".opts"
1418  if re.search(r"from\s*Gaudi.Configuration\s*import\s*\*", self.options):
1419  ext = ".py"
1420  tmpfile = TempFile(ext)
1421  tmpfile.writelines("\n".join(self.options.splitlines()))
1422  tmpfile.flush()
1423  args.append(tmpfile.name)
1424  result["GaudiTest.options"] = result.Quote(self.options)
1425 
1426  # if the program is a python file, execute it through python
1427  if prog_ext == ".py":
1428  args.insert(0,prog)
1429  if self.isWinPlatform():
1430  prog = which("python.exe") or "python.exe"
1431  else:
1432  prog = which("python") or "python"
1433 
1434  # Change to the working directory if specified or to the default temporary
1435  origdir = os.getcwd()
1436  if self.workdir:
1437  os.chdir(str(os.path.normpath(os.path.expandvars(self.workdir))))
1438  elif self.use_temp_dir == "true":
1439  if "QMTEST_TMPDIR" in os.environ:
1440  qmtest_tmpdir = os.environ["QMTEST_TMPDIR"]
1441  if not os.path.exists(qmtest_tmpdir):
1442  os.makedirs(qmtest_tmpdir)
1443  os.chdir(qmtest_tmpdir)
1444  elif "qmtest.tmpdir" in context:
1445  os.chdir(context["qmtest.tmpdir"])
1446 
1447  if "QMTEST_IGNORE_TIMEOUT" not in os.environ:
1448  self.timeout = max(self.timeout,600)
1449  else:
1450  self.timeout = -1
1451 
1452  try:
1453  # Generate eclipse.org debug launcher for the test
1454  self._CreateEclipseLaunch(prog, args, destdir = os.path.join(origdir, '.eclipse'))
1455  # Run the test
1456  self.RunProgram(prog,
1457  [ prog ] + args,
1458  context, result)
1459  # Record the content of the enfironment for failing tests
1460  if result.GetOutcome() not in [ result.PASS ]:
1461  self.DumpEnvironment(result)
1462  finally:
1463  # revert to the original directory
1464  os.chdir(origdir)
1465 
1466  def RunProgram(self, program, arguments, context, result):
1467  """Run the 'program'.
1468 
1469  'program' -- The path to the program to run.
1470 
1471  'arguments' -- A list of the arguments to the program. This
1472  list must contain a first argument corresponding to 'argv[0]'.
1473 
1474  'context' -- A 'Context' giving run-time parameters to the
1475  test.
1476 
1477  'result' -- A 'Result' object. The outcome will be
1478  'Result.PASS' when this method is called. The 'result' may be
1479  modified by this method to indicate outcomes other than
1480  'Result.PASS' or to add annotations.
1481 
1482  @attention: This method has been copied from command.ExecTestBase
1483  (QMTest 2.3.0) and modified to keep stdout and stderr
1484  for tests that have been terminated by a signal.
1485  (Fundamental for debugging in the Application Area)
1486  """
1487 
1488  # Construct the environment.
1489  environment = self.MakeEnvironment(context)
1490  # FIXME: whithout this, we get some spurious '\x1b[?1034' in the std out on SLC6
1491  if "slc6" in environment.get('CMTCONFIG', ''):
1492  environment['TERM'] = 'dumb'
1493  # Create the executable.
1494  if self.timeout >= 0:
1495  timeout = self.timeout
1496  else:
1497  # If no timeout was specified, we sill run this process in a
1498  # separate process group and kill the entire process group
1499  # when the child is done executing. That means that
1500  # orphaned child processes created by the test will be
1501  # cleaned up.
1502  timeout = -2
1503  e = GaudiFilterExecutable(self.stdin, timeout)
1504  # Run it.
1505  exit_status = e.Run(arguments, environment, path = program)
1506  # Get the stack trace from the temporary file (if present)
1507  if e.stack_trace_file and os.path.exists(e.stack_trace_file):
1508  stack_trace = open(e.stack_trace_file).read()
1509  os.remove(e.stack_trace_file)
1510  else:
1511  stack_trace = None
1512  if stack_trace:
1513  result["ExecTest.stack_trace"] = result.Quote(stack_trace)
1514 
1515  # If the process terminated normally, check the outputs.
1516  if sys.platform == "win32" or os.WIFEXITED(exit_status):
1517  # There are no causes of failure yet.
1518  causes = []
1519  # The target program terminated normally. Extract the
1520  # exit code, if this test checks it.
1521  if self.exit_code is None:
1522  exit_code = None
1523  elif sys.platform == "win32":
1524  exit_code = exit_status
1525  else:
1526  exit_code = os.WEXITSTATUS(exit_status)
1527  # Get the output generated by the program.
1528  stdout = e.stdout
1529  stderr = e.stderr
1530  # Record the results.
1531  result["ExecTest.exit_code"] = str(exit_code)
1532  result["ExecTest.stdout"] = result.Quote(stdout)
1533  result["ExecTest.stderr"] = result.Quote(stderr)
1534  # Check to see if the exit code matches.
1535  if exit_code != self.exit_code:
1536  causes.append("exit_code")
1537  result["ExecTest.expected_exit_code"] \
1538  = str(self.exit_code)
1539  # Validate the output.
1540  causes += self.ValidateOutput(stdout, stderr, result)
1541  # If anything went wrong, the test failed.
1542  if causes:
1543  result.Fail("Unexpected %s." % string.join(causes, ", "))
1544  elif os.WIFSIGNALED(exit_status):
1545  # The target program terminated with a signal. Construe
1546  # that as a test failure.
1547  signal_number = str(os.WTERMSIG(exit_status))
1548  if not stack_trace:
1549  result.Fail("Program terminated by signal.")
1550  else:
1551  # The presence of stack_trace means tha we stopped the job because
1552  # of a time-out
1553  result.Fail("Exceeded time limit (%ds), terminated." % timeout)
1554  result["ExecTest.signal_number"] = signal_number
1555  result["ExecTest.stdout"] = result.Quote(e.stdout)
1556  result["ExecTest.stderr"] = result.Quote(e.stderr)
1557  elif os.WIFSTOPPED(exit_status):
1558  # The target program was stopped. Construe that as a
1559  # test failure.
1560  signal_number = str(os.WSTOPSIG(exit_status))
1561  if not stack_trace:
1562  result.Fail("Program stopped 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), stopped." % 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  else:
1571  # The target program terminated abnormally in some other
1572  # manner. (This shouldn't normally happen...)
1573  result.Fail("Program did not terminate normally.")
1574 
1575  # Marco Cl.: This is a special trick to fix a "problem" with the output
1576  # of gaudi jobs when they use colors
1577  esc = '\x1b'
1578  repr_esc = '\\x1b'
1579  result["ExecTest.stdout"] = result["ExecTest.stdout"].replace(esc,repr_esc)
1580  # TODO: (MCl) improve the hack for colors in standard output
1581  # may be converting them to HTML tags
1582 
1583  def _CreateEclipseLaunch(self, prog, args, destdir = None):
1584  # Find the project name used in ecplise.
1585  # The name is in a file called ".project" in one of the parent directories
1586  projbasedir = os.path.normpath(destdir)
1587  while not os.path.exists(os.path.join(projbasedir, ".project")):
1588  oldprojdir = projbasedir
1589  projbasedir = os.path.normpath(os.path.join(projbasedir, os.pardir))
1590  # FIXME: the root level is invariant when trying to go up one level,
1591  # but it must be cheched on windows
1592  if oldprojdir == projbasedir:
1593  # If we cannot find a .project, so no point in creating a .launch file
1594  return
1595  # Ensure that we have a place where to write.
1596  if not os.path.exists(destdir):
1597  os.makedirs(destdir)
1598  # Use ElementTree to parse the XML file
1599  from xml.etree import ElementTree as ET
1600  t = ET.parse(os.path.join(projbasedir, ".project"))
1601  projectName = t.find("name").text
1602 
1603  # prepare the name/path of the generated file
1604  destfile = "%s.launch" % self._Runnable__id
1605  if destdir:
1606  destfile = os.path.join(destdir, destfile)
1607 
1608  if self.options.strip():
1609  # this means we have some custom options in the qmt file, so we have
1610  # to copy them from the temporary file at the end of the arguments
1611  # in another file
1612  tempfile = args.pop()
1613  optsfile = destfile + os.path.splitext(tempfile)[1]
1614  shutil.copyfile(tempfile, optsfile)
1615  args.append(optsfile)
1616 
1617  # prepare the data to insert in the XML file
1618  from xml.sax.saxutils import quoteattr # useful to quote XML special chars
1619  data = {}
1620  # Note: the "quoteattr(k)" is not needed because special chars cannot be part of a variable name,
1621  # but it doesn't harm.
1622  data["environment"] = "\n".join(['<mapEntry key=%s value=%s/>' % (quoteattr(k), quoteattr(v))
1623  for k, v in os.environ.iteritems()])
1624 
1625  data["exec"] = which(prog) or prog
1626  if os.path.basename(data["exec"]).lower().startswith("python"):
1627  data["stopAtMain"] = "false" # do not stop at main when debugging Python scripts
1628  else:
1629  data["stopAtMain"] = "true"
1630 
1631  data["args"] = "&#10;".join(map(rationalizepath, args))
1632  if self.isWinPlatform():
1633  data["args"] = "&#10;".join(["/debugexe"] + map(rationalizepath, [data["exec"]] + args))
1634  data["exec"] = which("vcexpress.exe")
1635 
1636  if not self.use_temp_dir:
1637  data["workdir"] = os.getcwd()
1638  else:
1639  # If the test is using a tmporary directory, it is better to run it
1640  # in the same directory as the .launch file when debugged in eclipse
1641  data["workdir"] = destdir
1642 
1643  data["project"] = projectName.strip()
1644 
1645  # Template for the XML file, based on eclipse 3.4
1646  xml = """<?xml version="1.0" encoding="UTF-8" standalone="no"?>
1647 <launchConfiguration type="org.eclipse.cdt.launch.applicationLaunchType">
1648 <booleanAttribute key="org.eclipse.cdt.debug.mi.core.AUTO_SOLIB" value="true"/>
1649 <listAttribute key="org.eclipse.cdt.debug.mi.core.AUTO_SOLIB_LIST"/>
1650 <stringAttribute key="org.eclipse.cdt.debug.mi.core.DEBUG_NAME" value="gdb"/>
1651 <stringAttribute key="org.eclipse.cdt.debug.mi.core.GDB_INIT" value=".gdbinit"/>
1652 <listAttribute key="org.eclipse.cdt.debug.mi.core.SOLIB_PATH"/>
1653 <booleanAttribute key="org.eclipse.cdt.debug.mi.core.STOP_ON_SOLIB_EVENTS" value="false"/>
1654 <booleanAttribute key="org.eclipse.cdt.debug.mi.core.breakpointsFullPath" value="false"/>
1655 <stringAttribute key="org.eclipse.cdt.debug.mi.core.commandFactory" value="org.eclipse.cdt.debug.mi.core.standardCommandFactory"/>
1656 <stringAttribute key="org.eclipse.cdt.debug.mi.core.protocol" value="mi"/>
1657 <booleanAttribute key="org.eclipse.cdt.debug.mi.core.verboseMode" value="false"/>
1658 <intAttribute key="org.eclipse.cdt.launch.ATTR_BUILD_BEFORE_LAUNCH_ATTR" value="0"/>
1659 <stringAttribute key="org.eclipse.cdt.launch.COREFILE_PATH" value=""/>
1660 <stringAttribute key="org.eclipse.cdt.launch.DEBUGGER_ID" value="org.eclipse.cdt.debug.mi.core.CDebuggerNew"/>
1661 <stringAttribute key="org.eclipse.cdt.launch.DEBUGGER_REGISTER_GROUPS" value=""/>
1662 <stringAttribute key="org.eclipse.cdt.launch.DEBUGGER_START_MODE" value="run"/>
1663 <booleanAttribute key="org.eclipse.cdt.launch.DEBUGGER_STOP_AT_MAIN" value="%(stopAtMain)s"/>
1664 <stringAttribute key="org.eclipse.cdt.launch.DEBUGGER_STOP_AT_MAIN_SYMBOL" value="main"/>
1665 <booleanAttribute key="org.eclipse.cdt.launch.ENABLE_REGISTER_BOOKKEEPING" value="false"/>
1666 <booleanAttribute key="org.eclipse.cdt.launch.ENABLE_VARIABLE_BOOKKEEPING" value="false"/>
1667 <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;"/>
1668 <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;"/>
1669 <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;"/>
1670 <stringAttribute key="org.eclipse.cdt.launch.PROGRAM_ARGUMENTS" value="%(args)s"/>
1671 <stringAttribute key="org.eclipse.cdt.launch.PROGRAM_NAME" value="%(exec)s"/>
1672 <stringAttribute key="org.eclipse.cdt.launch.PROJECT_ATTR" value="%(project)s"/>
1673 <stringAttribute key="org.eclipse.cdt.launch.PROJECT_BUILD_CONFIG_ID_ATTR" value=""/>
1674 <stringAttribute key="org.eclipse.cdt.launch.WORKING_DIRECTORY" value="%(workdir)s"/>
1675 <booleanAttribute key="org.eclipse.cdt.launch.ui.ApplicationCDebuggerTab.DEFAULTS_SET" value="true"/>
1676 <booleanAttribute key="org.eclipse.cdt.launch.use_terminal" value="true"/>
1677 <listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_PATHS">
1678 <listEntry value="/%(project)s"/>
1679 </listAttribute>
1680 <listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_TYPES">
1681 <listEntry value="4"/>
1682 </listAttribute>
1683 <booleanAttribute key="org.eclipse.debug.core.appendEnvironmentVariables" value="false"/>
1684 <mapAttribute key="org.eclipse.debug.core.environmentVariables">
1685 %(environment)s
1686 </mapAttribute>
1687 <mapAttribute key="org.eclipse.debug.core.preferred_launchers">
1688 <mapEntry key="[debug]" value="org.eclipse.cdt.cdi.launch.localCLaunch"/>
1689 </mapAttribute>
1690 <listAttribute key="org.eclipse.debug.ui.favoriteGroups">
1691 <listEntry value="org.eclipse.debug.ui.launchGroup.debug"/>
1692 </listAttribute>
1693 </launchConfiguration>
1694 """ % data
1695 
1696  # Write the output file
1697  open(destfile, "w").write(xml)
1698  #open(destfile + "_copy.xml", "w").write(xml)
1699 
1700 
1701 try:
1702  import json
1703 except ImportError:
1704  # Use simplejson for LCG
1705  import simplejson as json
1706 
1707 class HTMLResultStream(ResultStream):
1708  """An 'HTMLResultStream' writes its output to a set of HTML files.
1709 
1710  The argument 'dir' is used to select the destination directory for the HTML
1711  report.
1712  The destination directory may already contain the report from a previous run
1713  (for example of a different package), in which case it will be extended to
1714  include the new data.
1715  """
1716  arguments = [
1717  qm.fields.TextField(
1718  name = "dir",
1719  title = "Destination Directory",
1720  description = """The name of the directory.
1721 
1722  All results will be written to the directory indicated.""",
1723  verbatim = "true",
1724  default_value = ""),
1725  ]
1726 
1727  def __init__(self, arguments = None, **args):
1728  """Prepare the destination directory.
1729 
1730  Creates the destination directory and store in it some preliminary
1731  annotations and the static files found in the template directory
1732  'html_report'.
1733  """
1734  ResultStream.__init__(self, arguments, **args)
1735  self._summary = []
1736  self._summaryFile = os.path.join(self.dir, "summary.json")
1737  self._annotationsFile = os.path.join(self.dir, "annotations.json")
1738  # Prepare the destination directory using the template
1739  templateDir = os.path.join(os.path.dirname(__file__), "html_report")
1740  if not os.path.isdir(self.dir):
1741  os.makedirs(self.dir)
1742  # Copy the files in the template directory excluding the directories
1743  for f in os.listdir(templateDir):
1744  src = os.path.join(templateDir, f)
1745  dst = os.path.join(self.dir, f)
1746  if not os.path.isdir(src) and not os.path.exists(dst):
1747  shutil.copy(src, dst)
1748  # Add some non-QMTest attributes
1749  if "CMTCONFIG" in os.environ:
1750  self.WriteAnnotation("cmt.cmtconfig", os.environ["CMTCONFIG"])
1751  import socket
1752  self.WriteAnnotation("hostname", socket.gethostname())
1753 
1754  def _updateSummary(self):
1755  """Helper function to extend the global summary file in the destination
1756  directory.
1757  """
1758  if os.path.exists(self._summaryFile):
1759  oldSummary = json.load(open(self._summaryFile))
1760  else:
1761  oldSummary = []
1762  ids = set([ i["id"] for i in self._summary ])
1763  newSummary = [ i for i in oldSummary if i["id"] not in ids ]
1764  newSummary.extend(self._summary)
1765  json.dump(newSummary, open(self._summaryFile, "w"),
1766  sort_keys = True)
1767 
1768  def WriteAnnotation(self, key, value):
1769  """Writes the annotation to the annotation file.
1770  If the key is already present with a different value, the value becomes
1771  a list and the new value is appended to it, except for start_time and
1772  end_time.
1773  """
1774  # Initialize the annotation dict from the file (if present)
1775  if os.path.exists(self._annotationsFile):
1776  annotations = json.load(open(self._annotationsFile))
1777  else:
1778  annotations = {}
1779  # hack because we do not have proper JSON support
1780  key, value = map(str, [key, value])
1781  if key == "qmtest.run.start_time":
1782  # Special handling of the start time:
1783  # if we are updating a result, we have to keep the original start
1784  # time, but remove the original end time to mark the report to be
1785  # in progress.
1786  if key not in annotations:
1787  annotations[key] = value
1788  if "qmtest.run.end_time" in annotations:
1789  del annotations["qmtest.run.end_time"]
1790  else:
1791  # All other annotations are added to a list
1792  if key in annotations:
1793  old = annotations[key]
1794  if type(old) is list:
1795  if value not in old:
1796  annotations[key].append(value)
1797  elif value != old:
1798  annotations[key] = [old, value]
1799  else:
1800  annotations[key] = value
1801  # Write the new annotations file
1802  json.dump(annotations, open(self._annotationsFile, "w"),
1803  sort_keys = True)
1804 
1805  def WriteResult(self, result):
1806  """Prepare the test result directory in the destination directory storing
1807  into it the result fields.
1808  A summary of the test result is stored both in a file in the test directory
1809  and in the global summary file.
1810  """
1811  summary = {}
1812  summary["id"] = result.GetId()
1813  summary["outcome"] = result.GetOutcome()
1814  summary["cause"] = result.GetCause()
1815  summary["fields"] = result.keys()
1816  summary["fields"].sort()
1817 
1818  # Since we miss proper JSON support, I hack a bit
1819  for f in ["id", "outcome", "cause"]:
1820  summary[f] = str(summary[f])
1821  summary["fields"] = map(str, summary["fields"])
1822 
1823  self._summary.append(summary)
1824 
1825  # format:
1826  # testname/summary.json
1827  # testname/field1
1828  # testname/field2
1829  testOutDir = os.path.join(self.dir, summary["id"])
1830  if not os.path.isdir(testOutDir):
1831  os.makedirs(testOutDir)
1832  json.dump(summary, open(os.path.join(testOutDir, "summary.json"), "w"),
1833  sort_keys = True)
1834  for f in summary["fields"]:
1835  open(os.path.join(testOutDir, f), "w").write(result[f])
1836 
1837  self._updateSummary()
1838 
1839  def Summarize(self):
1840  # Not implemented.
1841  pass

Generated at Wed Nov 28 2012 12:17:17 for Gaudi Framework, version v23r5 by Doxygen version 1.8.2 written by Dimitri van Heesch, © 1997-2004