14 from subprocess
import Popen, PIPE, STDOUT
18 Take a string with invalid ASCII/UTF characters and quote them so that the
19 string can be used in an XML text.
21 >>> sanitize_for_xml('this is \x1b')
22 'this is [NON-XML-CHAR-0x1B]'
24 bad_chars = re.compile(
u'[\x00-\x08\x0b\x0c\x0e-\x1F\uD800-\uDFFF\uFFFE\uFFFF]')
27 return ''.join(
'[NON-XML-CHAR-0x%2X]' % ord(c)
for c
in match.group())
28 return bad_chars.sub(quote, data)
65 logging.debug(
'running test %s', self.name)
68 if re.search(
r'from\s+Gaudi.Configuration\s+import\s+\*|'
69 'from\s+Configurables\s+import', self.options):
70 optionFile = tempfile.NamedTemporaryFile(suffix=
'.py')
72 optionFile = tempfile.NamedTemporaryFile(suffix=
'.opts')
73 optionFile.file.write(self.options)
78 if self.environment
is None : self.environment = os.environ
79 else : self.environment=dict(self.environment.items()+os.environ.items())
81 platform_id = (os.environ.get(
'BINARY_TAG')
or
82 os.environ.get(
'CMTCONFIG')
or
85 skip_test = bool([
None
86 for prex
in self.unsupported_platforms
87 if re.search(prex, platform_id)])
91 workdir = self.workdir
93 if self._common_tmpdir:
94 workdir = self._common_tmpdir
96 workdir = tempfile.mkdtemp()
100 if self.program !=
'':
102 elif "GAUDIEXE" in os.environ :
103 prog = os.environ[
"GAUDIEXE"]
107 dummy, prog_ext = os.path.splitext(prog)
108 if prog_ext
not in [
".exe",
".py",
".bat" ]:
112 prog =
which(prog)
or prog
114 args =
map(RationalizePath, self.args)
116 if prog_ext ==
".py" :
121 validatorRes = Result({
'CAUSE':
None,
'EXCEPTION':
None,
122 'RESOURCE':
None,
'TARGET':
None,
123 'TRACEBACK':
None,
'START_TIME':
None,
124 'END_TIME':
None,
'TIMEOUT_DETAIL':
None})
125 self.result = validatorRes
133 logging.debug(
'executing %r in %s',
135 self.proc = Popen(params, stdout=PIPE, stderr=PIPE,
136 env=self.environment)
137 self.out, self.err = self.proc.communicate()
139 thread = threading.Thread(target=target)
142 thread.join(self.timeout)
144 if thread.is_alive():
146 cmd = [
'gdb',
'-p', str(self.proc.pid),
'-n',
'-q']
147 gdb = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=STDOUT)
148 self.stack_trace = gdb.communicate(
'set pagination off\n'
150 'thread apply all backtrace\n'
153 self.proc.send_signal(signal.SIGKILL)
154 logging.debug(
'time out in test %s', self.name)
156 self.causes.append(
'timeout')
158 logging.debug(
'completed test %s', self.name)
161 logging.debug(
'returnedCode = %s', self.proc.returncode)
162 self.returnedCode = self.proc.returncode
164 logging.debug(
'validating test...')
165 self.result, self.causes = self.ValidateOutput(stdout=self.out,
170 if self.use_temp_dir
and not self._common_tmpdir:
171 shutil.rmtree(workdir,
True)
173 os.chdir(self.basedir)
176 if self.signal
is not None:
177 if int(self.returnedCode) != -int(self.signal):
178 self.causes.append(
'exit code')
180 elif self.exit_code
is not None:
181 if int(self.returnedCode) != int(self.exit_code):
182 self.causes.append(
'exit code')
184 elif self.returnedCode != 0:
185 self.causes.append(
"exit code")
188 self.status =
"failed"
190 self.status =
"passed"
193 self.status =
"skipped"
195 logging.debug(
'%s: %s', self.name, self.status)
196 field_mapping = {
'Exit Code':
'returnedCode',
199 'Environment':
'environment',
202 'Program Name':
'program',
204 'Validator':
'validator',
205 'Output Reference File':
'reference',
206 'Error Reference File':
'error_reference',
209 'Unsupported Platforms':
'unsupported_platforms',
210 'Stack Trace':
'stack_trace'}
211 resultDict = [(key, getattr(self, attr))
212 for key, attr
in field_mapping.iteritems()
213 if getattr(self, attr)]
214 resultDict.append((
'Working Directory',
218 resultDict.extend(self.result.annotations.iteritems())
220 return dict(resultDict)
229 if self.
validator is not BaseTest.validator:
235 elif stderr.strip() != self.stderr.strip():
236 self.causes.append(
'standard error')
239 return result, causes
243 def findReferenceBlock(self,reference=None, stdout=None, result=None, causes=None, signature_offset=0, signature=None, id = None):
245 Given a block of text, tries to find it in the output. The block had to be identified by a signature line. By default, the first line is used as signature, or the line pointed to by signature_offset. If signature_offset points outside the block, a signature line can be passed as signature argument. Note: if 'signature' is None (the default), a negative signature_offset is interpreted as index in a list (e.g. -1 means the last line), otherwise the it is interpreted as the number of lines before the first one of the block the signature must appear. The parameter 'id' allow to distinguish between different calls to this function in the same validation code.
248 if reference
is None : reference=self.
reference
249 if stdout
is None : stdout=self.
out
250 if result
is None : result=self.
result
251 if causes
is None : causes=self.
causes
253 reflines = filter(
None,
map(
lambda s: s.rstrip(), reference.splitlines()))
255 raise RuntimeError(
"Empty (or null) reference")
257 outlines = filter(
None,
map(
lambda s: s.rstrip(), stdout.splitlines()))
259 res_field =
"GaudiTest.RefBlock"
261 res_field +=
"_%s" % id
263 if signature
is None:
264 if signature_offset < 0:
265 signature_offset = len(reference)+signature_offset
266 signature = reflines[signature_offset]
269 pos = outlines.index(signature)
270 outlines = outlines[pos-signature_offset:pos+len(reflines)-signature_offset]
271 if reflines != outlines:
272 msg =
"standard output"
274 if not msg
in causes:
276 result[res_field +
".observed"] = result.Quote(
"\n".join(outlines))
278 causes.append(
"missing signature")
279 result[res_field +
".signature"] = result.Quote(signature)
280 if len(reflines) > 1
or signature != reflines[0]:
281 result[res_field +
".expected"] = result.Quote(
"\n".join(reflines))
284 def countErrorLines(self, expected = {'ERROR':0,
'FATAL':0}, stdout=
None, result=
None,causes=
None):
286 Count the number of messages with required severity (by default ERROR and FATAL)
287 and check if their numbers match the expected ones (0 by default).
288 The dictionary "expected" can be used to tune the number of errors and fatals
289 allowed, or to limit the number of expected warnings etc.
292 if stdout
is None : stdout=self.
out
293 if result
is None : result=self.
result
294 if causes
is None : causes=self.
causes
301 outlines = stdout.splitlines()
302 from math
import log10
303 fmt =
"%%%dd - %%s" % (int(log10(len(outlines)+1)))
309 if len(words) >= 2
and words[1]
in errors:
310 errors[words[1]].append(fmt%(linecount,l.rstrip()))
313 if len(errors[e]) != expected[e]:
314 causes.append(
'%s(%d)'%(e,len(errors[e])))
315 result[
"GaudiTest.lines.%s"%e] = result.Quote(
'\n'.join(errors[e]))
316 result[
"GaudiTest.lines.%s.expected#"%e] = result.Quote(str(expected[e]))
322 ignore =
r"Basket|.*size|Compression"):
324 Compare the TTree summaries in stdout with the ones in trees_dict or in
325 the reference file. By default ignore the size, compression and basket
327 The presence of TTree summaries when none is expected is not a failure.
329 if stdout
is None : stdout=self.
out
330 if result
is None : result=self.
result
331 if causes
is None : causes=self.
causes
332 if trees_dict
is None:
335 if lreference
and os.path.isfile(lreference):
340 from pprint
import PrettyPrinter
343 result[
"GaudiTest.TTrees.expected"] = result.Quote(pp.pformat(trees_dict))
345 result[
"GaudiTest.TTrees.ignore"] = result.Quote(ignore)
350 causes.append(
"trees summaries")
352 result[
"GaudiTest.TTrees.failure_on"] = result.Quote(msg)
353 result[
"GaudiTest.TTrees.found"] = result.Quote(pp.pformat(trees))
361 Compare the TTree summaries in stdout with the ones in trees_dict or in
362 the reference file. By default ignore the size, compression and basket
364 The presence of TTree summaries when none is expected is not a failure.
366 if stdout
is None : stdout=self.
out
367 if result
is None : result=self.
result
368 if causes
is None : causes=self.
causes
373 if lreference
and os.path.isfile(lreference):
378 from pprint
import PrettyPrinter
381 result[
"GaudiTest.Histos.expected"] = result.Quote(pp.pformat(dict))
383 result[
"GaudiTest.Histos.ignore"] = result.Quote(ignore)
388 causes.append(
"histos summaries")
390 result[
"GaudiTest.Histos.failure_on"] = result.Quote(msg)
391 result[
"GaudiTest.Histos.found"] = result.Quote(pp.pformat(histos))
396 causes=
None, preproc=
None):
398 Default validation acti*on: compare standard output and error to the
402 if stdout
is None : stdout = self.
out
403 if stderr
is None : stderr = self.
err
404 if result
is None : result = self.
result
405 if causes
is None : causes = self.
causes
409 preproc = normalizeExamples
413 if lreference
and os.path.isfile(lreference):
417 preproc=preproc)(stdout, result)
423 newref = open(lreference +
".new",
"w")
425 for l
in stdout.splitlines():
426 newref.write(l.rstrip() +
'\n')
436 if lreference
and os.path.isfile(lreference):
440 preproc=preproc)(stderr, result)
443 newref = open(lreference +
".new",
"w")
445 for l
in stderr.splitlines():
446 newref.write(l.rstrip() +
'\n')
449 causes +=
BasicOutputValidator(lreference,
"standard error",
"ExecTest.expected_stderr")(stderr, result)
458 platformSplit =
lambda p: set(p.split(
'-' in p
and '-' or '_'))
460 reference = os.path.normpath(os.path.join(self.
basedir,
461 os.path.expandvars(reffile)))
464 spec_ref = reference[:-3] +
GetPlatform(self)[0:3] + reference[-3:]
465 if os.path.isfile(spec_ref):
469 dirname, basename = os.path.split(reference)
470 if not dirname: dirname =
'.'
471 head = basename +
"."
474 if 'do0' in platform:
477 for f
in os.listdir(dirname):
478 if f.startswith(head):
479 req_plat = platformSplit(f[head_len:])
480 if platform.issuperset(req_plat):
481 candidates.append( (len(req_plat), f) )
486 reference = os.path.join(dirname, candidates[-1][1])
501 from GaudiKernel
import ROOT6WorkAroundEnabled
511 Function used to normalize the used path
513 newPath = os.path.normpath(os.path.expandvars(p))
514 if os.path.exists(newPath) :
515 p = os.path.realpath(newPath)
521 Locates an executable in the executables path ($PATH) and returns the full
522 path to it. An application is looked for with or without the '.exe' suffix.
523 If the executable cannot be found, None is returned
525 if os.path.isabs(executable):
526 if not os.path.exists(executable):
527 if executable.endswith(
'.exe'):
528 if os.path.exists(executable[:-4]):
529 return executable[:-4]
531 head,executable = os.path.split(executable)
534 for d
in os.environ.get(
"PATH").split(os.pathsep):
535 fullpath = os.path.join(d, executable)
536 if os.path.exists(fullpath):
538 if executable.endswith(
'.exe'):
539 return which(executable[:-4])
564 def __init__(self,kind=None,id=None,outcome=PASS,annotations={}):
568 assert type(key)
in types.StringTypes
572 assert type(key)
in types.StringTypes
573 assert type(value)
in types.StringTypes
595 """Validate the output of the program.
596 'stdout' -- A string containing the data written to the standard output
598 'stderr' -- A string containing the data written to the standard error
600 'result' -- A 'Result' object. It may be used to annotate
601 the outcome according to the content of stderr.
602 returns -- A list of strings giving causes of failure."""
607 causes.append(self.
cause)
615 """Compare 's1' and 's2', ignoring line endings.
618 returns -- True if 's1' and 's2' are the same, ignoring
619 differences in line endings."""
622 to_ignore = re.compile(
r'Warning in <TInterpreter::ReadRootmapFile>: .* is already in .*')
623 keep_line =
lambda l:
not to_ignore.match(l)
624 return filter(keep_line, s1.splitlines()) == filter(keep_line, s2.splitlines())
626 return s1.splitlines() == s2.splitlines()
632 """ Base class for a callable that takes a file and returns a modified
637 if hasattr(input,
"__iter__"):
641 lines = input.splitlines()
646 if l: output.append(l)
647 if mergeback: output =
'\n'.join(output)
671 if line.find(s) >= 0:
return None
673 if r.search(line):
return None
683 if self.
start in line:
686 elif self.
end in line:
695 when = re.compile(when)
698 if isinstance(rhs, RegexpReplacer):
700 res._operations = self.
_operations + rhs._operations
702 res = FilePreprocessor.__add__(self, rhs)
706 if w
is None or w.search(line):
707 line = o.sub(r, line)
712 normalizeDate =
RegexpReplacer(
"[0-2]?[0-9]:[0-5][0-9]:[0-5][0-9] [0-9]{4}[-/][01][0-9][-/][0-3][0-9][ A-Z]*",
713 "00:00:00 1970-01-01")
715 normalizeEOL.__processLine__ =
lambda line: str(line).rstrip() +
'\n'
719 skipEmptyLines.__processLine__ =
lambda line: (line.strip()
and line)
or None
730 line = line[:(pos+self.
siglen)]
731 lst = line[(pos+self.
siglen):].split()
733 line +=
" ".join(lst)
737 normalizeExamples = maskPointers + normalizeDate
740 (
"TIMER.TIMER",
r"\s+[+-]?[0-9]+[0-9.]*",
" 0"),
741 (
"release all pending",
r"^.*/([^/]*:.*)",
r"\1"),
742 (
"0x########",
r"\[.*/([^/]*.*)\]",
r"[\1]"),
743 (
"^#.*file",
r"file '.*[/\\]([^/\\]*)$",
r"file '\1"),
744 (
"^JobOptionsSvc.*options successfully read in from",
r"read in from .*[/\\]([^/\\]*)$",
r"file \1"),
746 (
None,
r"[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}(?!-0{12})-[0-9A-Fa-f]{12}",
"00000000-0000-0000-0000-000000000000"),
748 (
"ServiceLocatorHelper::",
"ServiceLocatorHelper::(create|locate)Service",
"ServiceLocatorHelper::service"),
750 (
None,
r"e([-+])0([0-9][0-9])",
r"e\1\2"),
752 (
None,
r'Service reference count check:',
r'Looping over all active services...'),
754 (
None,
r"Property(.*)'ErrorCount':",
r"Property\1'ErrorCounter':"),
759 "JobOptionsSvc INFO # ",
760 "JobOptionsSvc WARNING # ",
763 "This machine has a speed",
766 "ToolSvc.Sequenc... INFO",
767 "DataListenerSvc INFO XML written to file:",
768 "[INFO]",
"[WARNING]",
769 "DEBUG No writable file catalog found which contains FID:",
771 "DEBUG Service base class initialized successfully",
772 "DEBUG Incident timing:",
773 "INFO 'CnvServices':[",
777 r"^JobOptionsSvc INFO *$",
779 r"(Always|SUCCESS)\s*(Root f|[^ ]* F)ile version:",
780 r"0x[0-9a-fA-F#]+ *Algorithm::sysInitialize\(\) *\[",
781 r"0x[0-9a-fA-F#]* *__gxx_personality_v0 *\[",
782 r"File '.*.xml' does not exist",
783 r"INFO Refer to dataset .* by its file ID:",
784 r"INFO Referring to dataset .* by its file ID:",
785 r"INFO Disconnect from dataset",
786 r"INFO Disconnected from dataset",
787 r"INFO Disconnected data IO:",
788 r"IncidentSvc\s*(DEBUG (Adding|Removing)|VERBOSE Calling)",
790 r"^StatusCodeSvc.*listing all unchecked return codes:",
791 r"^StatusCodeSvc\s*INFO\s*$",
792 r"Num\s*\|\s*Function\s*\|\s*Source Library",
795 r"ERROR Failed to modify file: .* Errno=2 No such file or directory",
797 r"^ +[0-9]+ \|.*ROOT",
798 r"^ +[0-9]+ \|.*\|.*Dict",
800 r"StatusCodeSvc.*all StatusCode instances where checked",\
804 r"SUCCESS\s*Booked \d+ Histogram\(s\)",
812 r'Warning in <TInterpreter::ReadRootmapFile>: .* is already in .*',
815 normalizeExamples = (lineSkipper + normalizeExamples + skipEmptyLines +
816 normalizeEOL +
LineSorter(
"Services to release : "))
822 def __init__(self,reffile, cause, result_key, preproc=normalizeExamples):
830 if os.path.isfile(self.
reffile):
831 orig=open(self.
reffile).xreadlines()
836 new = stdout.splitlines()
840 diffs = difflib.ndiff(orig,new,charjunk=difflib.IS_CHARACTER_JUNK)
841 filterdiffs =
map(
lambda x: x.strip(),filter(
lambda x: x[0] !=
" ",diffs))
843 result[self.
result_key] = result.Quote(
"\n".join(filterdiffs))
847 +) standard output of the test""")
848 causes.append(self.
cause)
853 Scan stdout to find ROOT TTree summaries and digest them.
855 stars = re.compile(
r"^\*+$")
856 outlines = stdout.splitlines()
857 nlines = len(outlines)
863 while i < nlines
and not stars.match(outlines[i]):
868 trees[tree[
"Name"]] = tree
874 Check that all the keys in reference are in to_check too, with the same value.
875 If the value is a dict, the function is called recursively. to_check can
876 contain more keys than reference, that will not be tested.
877 The function returns at the first difference found.
882 ignore_re = re.compile(ignore)
883 keys = [ key
for key
in reference
if not ignore_re.match(key) ]
885 keys = reference.keys()
889 if (
type(reference[k])
is dict)
and (
type(to_check[k])
is dict):
891 failed = fail_keys =
cmpTreesDicts(reference[k], to_check[k], ignore)
894 failed = to_check[k] != reference[k]
899 fail_keys.insert(0, k)
909 if c
is None or r
is None:
911 return (fail_path, r, c)
914 h_count_re = re.compile(
r"^(.*)SUCCESS\s+Booked (\d+) Histogram\(s\) :\s+(.*)")
919 Parse the TTree summary table in lines, starting from pos.
920 Returns a tuple with the dictionary with the digested informations and the
921 position of the first line after the summary.
927 splitcols =
lambda l: [ f.strip()
for f
in l.strip(
"*\n").split(
':',2) ]
930 cols = splitcols(ll[0])
931 r[
"Name"], r[
"Title"] = cols[1:]
933 cols = splitcols(ll[1])
934 r[
"Entries"] = int(cols[1])
936 sizes = cols[2].split()
937 r[
"Total size"] = int(sizes[2])
938 if sizes[-1] ==
"memory":
941 r[
"File size"] = int(sizes[-1])
943 cols = splitcols(ll[2])
944 sizes = cols[2].split()
945 if cols[0] ==
"Baskets":
946 r[
"Baskets"] = int(cols[1])
947 r[
"Basket size"] = int(sizes[2])
948 r[
"Compression"] = float(sizes[-1])
951 if i < (count - 3)
and lines[i].startswith(
"*Tree"):
952 result = parseblock(lines[i:i+3])
953 result[
"Branches"] = {}
955 while i < (count - 3)
and lines[i].startswith(
"*Br"):
956 if i < (count - 2)
and lines[i].startswith(
"*Branch "):
960 branch = parseblock(lines[i:i+3])
961 result[
"Branches"][branch[
"Name"]] = branch
968 Extract the histograms infos from the lines starting at pos.
969 Returns the position of the first line after the summary block.
972 h_table_head = re.compile(
r'SUCCESS\s+List of booked (1D|2D|3D|1D profile|2D profile) histograms in directory\s+"(\w*)"')
973 h_short_summ = re.compile(
r"ID=([^\"]+)\s+\"([^\"]+)\"\s+(.*)")
978 m = h_count_re.search(lines[pos])
979 name = m.group(1).strip()
980 total = int(m.group(2))
982 for k, v
in [ x.split(
"=")
for x
in m.group(3).split() ]:
985 header[
"Total"] = total
989 m = h_table_head.search(lines[pos])
992 t = t.replace(
" profile",
"Prof")
999 if l.startswith(
" | ID"):
1001 titles = [ x.strip()
for x
in l.split(
"|")][1:]
1003 while pos < nlines
and lines[pos].startswith(
" |"):
1005 values = [ x.strip()
for x
in l.split(
"|")][1:]
1007 for i
in range(len(titles)):
1008 hcont[titles[i]] = values[i]
1009 cont[hcont[
"ID"]] = hcont
1011 elif l.startswith(
" ID="):
1012 while pos < nlines
and lines[pos].startswith(
" ID="):
1013 values = [ x.strip()
for x
in h_short_summ.search(lines[pos]).groups() ]
1014 cont[values[0]] = values
1017 raise RuntimeError(
"Cannot understand line %d: '%s'" % (pos, l))
1021 summ[d][
"header"] = header
1026 summ[name] = {
"header": header}
1033 Scan stdout to find ROOT TTree summaries and digest them.
1035 outlines = stdout.splitlines()
1036 nlines = len(outlines) - 1
1044 match = h_count_re.search(outlines[pos])
1045 while pos < nlines
and not match:
1047 match = h_count_re.search(outlines[pos])
1050 summaries.update(summ)
1055 unsupported = [ re.compile(x)
for x
in [ str(y).strip()
for y
in unsupported_platforms ]
if x]
1056 for p_re
in unsupported :
1057 if p_re.search(platform):
1058 result.SetOutcome(result.UNTESTED)
1059 result[result.CAUSE] =
'Platform not supported.'
1065 Return the platform Id defined in CMTCONFIG or SCRAM_ARCH.
1069 if "CMTCONFIG" in os.environ:
1070 arch = os.environ[
"CMTCONFIG"]
1071 elif "SCRAM_ARCH" in os.environ:
1072 arch = os.environ[
"SCRAM_ARCH"]
1077 Return True if the current platform is Windows.
1079 This function was needed because of the change in the CMTCONFIG format,
1080 from win32_vc71_dbg to i686-winxp-vc9-dbg.
1083 return "winxp" in platform
or platform.startswith(
"win")
def PlatformIsNotSupported(self, context, result)
def __processLine__(self, line)
def __init__(self, start, end)
def __call__(self, input)
def validateWithReference
def __processLine__(self, line)
def ValidateOutput(self, stdout, stderr, result)
def __processLine__(self, line)
def __call__(self, out, result)
def findHistosSummaries(stdout)
def _parseTTreeSummary(lines, pos)
struct GAUDI_API map
Parametrisation class for map-like implementation.
def __call__(self, stdout, result)
def __processLine__(self, line)
def __init__(self, signature)
def __call__(self, input)
def sanitize_for_xml(data)
def getCmpFailingValues(reference, to_check, fail_path)
def __setitem__(self, key, value)
def __processLine__(self, line)
def parseHistosSummary(lines, pos)
def _expandReferenceFileName(self, reffile)
def ROOT6WorkAroundEnabled
def __CompareText(self, s1, s2)
def __getitem__(self, key)
def findTTreeSummaries(stdout)
def __init__(self, ref, cause, result_key)
NamedRange_< CONTAINER > range(const CONTAINER &cnt, const std::string &name)
simple function to create the named range form arbitrary container
Special preprocessor sorting the list of strings (whitespace separated) that follow a signature on a ...