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