Gaudi Framework, version v25r2

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

Generated at Wed Jun 4 2014 14:48:58 for Gaudi Framework, version v25r2 by Doxygen version 1.8.2 written by Dimitri van Heesch, © 1997-2004