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