"""xUnit XML output"""
import locale
import os
import re
import socket
import sys
import time
from cram._encoding import u, ul
__all__ = ['runxunit']
_widecdataregex = ul(r"'(?:[^\x09\x0a\x0d\x20-\ud7ff\ue000-\ufffd"
r"\U00010000-\U0010ffff]|]]>)'")
_narrowcdataregex = ul(r"'(?:[^\x09\x0a\x0d\x20-\ud7ff\ue000-\ufffd]"
r"|]]>)'")
_widequoteattrregex = ul(r"'[^\x20\x21\x23-\x25\x27-\x3b\x3d"
r"\x3f-\ud7ff\ue000-\ufffd"
r"\U00010000-\U0010ffff]'")
_narrowquoteattrregex = ul(r"'[^\x20\x21\x23-\x25\x27-\x3b\x3d"
r"\x3f-\ud7ff\ue000-\ufffd]'")
_replacementchar = ul(r"'\N{REPLACEMENT CHARACTER}'")
if sys.maxunicode >= 0x10ffff: # pragma: nocover
_cdatasub = re.compile(_widecdataregex).sub
_quoteattrsub = re.compile(_widequoteattrregex).sub
else: # pragma: nocover
_cdatasub = re.compile(_narrowcdataregex).sub
_quoteattrsub = re.compile(_narrowquoteattrregex).sub
def _cdatareplace(m):
"""Replace _cdatasub() regex match"""
if m.group(0) == u(']]>'):
return u(']]>]]>>> from cram._encoding import ul
>>> (_cdata('1<\'2\'>&"3\x00]]>\t\r\n') ==
... ul(r"'&\"3\ufffd]]>]]>'"))
True
"""
return u('') % _cdatasub(_cdatareplace, s)
def _quoteattrreplace(m):
"""Replace _quoteattrsub() regex match"""
return {u('\t'): u(' '),
u('\n'): u('
'),
u('\r'): u('
'),
u('"'): u('"'),
u('&'): u('&'),
u('<'): u('<'),
u('>'): u('>')}.get(m.group(0), _replacementchar)
def _quoteattr(s):
r"""Escape a string for use as an XML attribute value.
>>> from cram._encoding import ul
>>> (_quoteattr('1<\'2\'>&"3\x00]]>\t\r\n') ==
... ul(r"'\"1<\'2\'>&"3\ufffd]]>
\"'"))
True
"""
return u('"%s"') % _quoteattrsub(_quoteattrreplace, s)
def _timestamp():
"""Return the current time in ISO 8601 format"""
tm = time.localtime()
if tm.tm_isdst == 1: # pragma: nocover
tz = time.altzone
else: # pragma: nocover
tz = time.timezone
timestamp = time.strftime('%Y-%m-%dT%H:%M:%S', tm)
tzhours = int(-tz / 60 / 60)
tzmins = int(abs(tz) / 60 % 60)
timestamp += u('%+03d:%02d') % (tzhours, tzmins)
return timestamp
def runxunit(tests, xmlpath):
"""Run tests with xUnit XML output.
tests should be a sequence of 2-tuples containing the following:
(test path, test function)
This function yields a new sequence where each test function is wrapped
with a function that writes test results to an xUnit XML file.
"""
suitestart = time.time()
timestamp = _timestamp()
hostname = socket.gethostname()
total, skipped, failed = [0], [0], [0]
testcases = []
for path, test in tests:
def testwrapper():
"""Run test and collect XML output"""
total[0] += 1
start = time.time()
refout, postout, diff = test()
testtime = time.time() - start
classname = path.decode(locale.getpreferredencoding(), 'replace')
name = os.path.basename(classname)
if postout is None:
skipped[0] += 1
testcase = (u(' \n'
' \n'
' \n') %
{'classname': _quoteattr(classname),
'name': _quoteattr(name),
'time': testtime})
elif diff:
failed[0] += 1
diff = list(diff)
diffu = u('').join(l.decode(locale.getpreferredencoding(),
'replace')
for l in diff)
testcase = (u(' \n'
' %(diff)s\n'
' \n') %
{'classname': _quoteattr(classname),
'name': _quoteattr(name),
'time': testtime,
'diff': _cdata(diffu)})
else:
testcase = (u(' \n') %
{'classname': _quoteattr(classname),
'name': _quoteattr(name),
'time': testtime})
testcases.append(testcase)
return refout, postout, diff
yield path, testwrapper
suitetime = time.time() - suitestart
header = (u('\n'
'\n') %
{'total': total[0],
'failed': failed[0],
'skipped': skipped[0],
'timestamp': _quoteattr(timestamp),
'hostname': _quoteattr(hostname),
'time': suitetime})
footer = u('\n')
xmlfile = open(xmlpath, 'wb')
try:
xmlfile.write(header.encode('utf-8'))
for testcase in testcases:
xmlfile.write(testcase.encode('utf-8'))
xmlfile.write(footer.encode('utf-8'))
finally:
xmlfile.close()