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