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