Gaudi Framework, version v23r10

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

Generated at Mon Sep 30 2013 14:51:57 for Gaudi Framework, version v23r10 by Doxygen version 1.8.2 written by Dimitri van Heesch, © 1997-2004