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