You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
237 lines
7.6 KiB
237 lines
7.6 KiB
import re
|
|
import sys
|
|
import time
|
|
import six
|
|
|
|
from xml.dom.minidom import Document
|
|
|
|
|
|
__all__ = ('TestXMLBuilder', 'TestXMLContext')
|
|
|
|
|
|
# see issue #74, the encoding name needs to be one of
|
|
# http://www.iana.org/assignments/character-sets/character-sets.xhtml
|
|
UTF8 = 'UTF-8'
|
|
|
|
# Workaround for Python bug #5166
|
|
# http://bugs.python.org/issue5166
|
|
|
|
_char_tail = ''
|
|
|
|
if sys.maxunicode > 0x10000:
|
|
_char_tail = six.u('%s-%s') % (
|
|
six.unichr(0x10000),
|
|
six.unichr(min(sys.maxunicode, 0x10FFFF))
|
|
)
|
|
|
|
_nontext_sub = re.compile(
|
|
six.u(r'[^\x09\x0A\x0D\x20-\uD7FF\uE000-\uFFFD%s]') % _char_tail,
|
|
re.U
|
|
).sub
|
|
|
|
|
|
def replace_nontext(text, replacement=six.u('\uFFFD')):
|
|
return _nontext_sub(replacement, text)
|
|
|
|
|
|
class TestXMLContext(object):
|
|
"""A XML report file have a distinct hierarchy. The outermost element is
|
|
'testsuites', which contains one or more 'testsuite' elements. The role of
|
|
these elements is to give the proper context to 'testcase' elements.
|
|
|
|
These contexts have a few things in common: they all have some sort of
|
|
counters (i.e. how many testcases are inside that context, how many failed,
|
|
and so on), they all have a 'time' attribute indicating how long it took
|
|
for their testcases to run, etc.
|
|
|
|
The purpose of this class is to abstract the job of composing this
|
|
hierarchy while keeping track of counters and how long it took for a
|
|
context to be processed.
|
|
"""
|
|
|
|
# Allowed keys for self.counters
|
|
_allowed_counters = ('tests', 'errors', 'failures', 'skipped',)
|
|
|
|
def __init__(self, xml_doc, parent_context=None):
|
|
"""Creates a new instance of a root or nested context (depending whether
|
|
`parent_context` is provided or not).
|
|
"""
|
|
self.xml_doc = xml_doc
|
|
self.parent = parent_context
|
|
self._start_time = self._stop_time = 0
|
|
self.counters = {}
|
|
|
|
def element_tag(self):
|
|
"""Returns the name of the tag represented by this context.
|
|
"""
|
|
return self.element.tagName
|
|
|
|
def begin(self, tag, name):
|
|
"""Begins the creation of this context in the XML document by creating
|
|
an empty tag <tag name='param'>.
|
|
"""
|
|
self.element = self.xml_doc.createElement(tag)
|
|
self.element.setAttribute('name', replace_nontext(name))
|
|
self._start_time = time.time()
|
|
|
|
def end(self):
|
|
"""Closes this context (started with a call to `begin`) and creates an
|
|
attribute for each counter and another for the elapsed time.
|
|
"""
|
|
self._stop_time = time.time()
|
|
self.element.setAttribute('time', self.elapsed_time())
|
|
self._set_result_counters()
|
|
return self.element
|
|
|
|
def _set_result_counters(self):
|
|
"""Sets an attribute in this context's tag for each counter considering
|
|
what's valid for each tag name.
|
|
"""
|
|
tag = self.element_tag()
|
|
|
|
for counter_name in TestXMLContext._allowed_counters:
|
|
valid_counter_for_element = False
|
|
|
|
if counter_name == 'skipped':
|
|
valid_counter_for_element = (
|
|
tag == 'testsuite'
|
|
)
|
|
else:
|
|
valid_counter_for_element = (
|
|
tag in ('testsuites', 'testsuite')
|
|
)
|
|
|
|
if valid_counter_for_element:
|
|
value = six.text_type(
|
|
self.counters.get(counter_name, 0)
|
|
)
|
|
self.element.setAttribute(counter_name, value)
|
|
|
|
def increment_counter(self, counter_name):
|
|
"""Increments a counter named by `counter_name`, which can be any one
|
|
defined in `_allowed_counters`.
|
|
"""
|
|
if counter_name in TestXMLContext._allowed_counters:
|
|
self.counters[counter_name] = \
|
|
self.counters.get(counter_name, 0) + 1
|
|
|
|
def elapsed_time(self):
|
|
"""Returns the time the context took to run between the calls to
|
|
`begin()` and `end()`, in seconds.
|
|
"""
|
|
return format(self._stop_time - self._start_time, '.3f')
|
|
|
|
|
|
class TestXMLBuilder(object):
|
|
"""This class encapsulates most rules needed to create a XML test report
|
|
behind a simple interface.
|
|
"""
|
|
|
|
def __init__(self):
|
|
"""Creates a new instance.
|
|
"""
|
|
self._xml_doc = Document()
|
|
self._current_context = None
|
|
|
|
def current_context(self):
|
|
"""Returns the current context.
|
|
"""
|
|
return self._current_context
|
|
|
|
def begin_context(self, tag, name):
|
|
"""Begins a new context in the XML test report, which usually is defined
|
|
by one on the tags 'testsuites', 'testsuite', or 'testcase'.
|
|
"""
|
|
context = TestXMLContext(self._xml_doc, self._current_context)
|
|
context.begin(tag, name)
|
|
|
|
self._current_context = context
|
|
|
|
def context_tag(self):
|
|
"""Returns the tag represented by the current context.
|
|
"""
|
|
return self._current_context.element_tag()
|
|
|
|
def _create_cdata_section(self, content):
|
|
"""Returns a new CDATA section containing the string defined in
|
|
`content`.
|
|
"""
|
|
filtered_content = replace_nontext(content)
|
|
return self._xml_doc.createCDATASection(filtered_content)
|
|
|
|
def append_cdata_section(self, tag, content):
|
|
"""Appends a tag in the format <tag>CDATA</tag> into the tag represented
|
|
by the current context. Returns the created tag.
|
|
"""
|
|
element = self._xml_doc.createElement(tag)
|
|
|
|
pos = content.find(']]>')
|
|
while pos >= 0:
|
|
tmp = content[0:pos+2]
|
|
element.appendChild(self._create_cdata_section(tmp))
|
|
content = content[pos+2:]
|
|
pos = content.find(']]>')
|
|
|
|
element.appendChild(self._create_cdata_section(content))
|
|
|
|
self._append_child(element)
|
|
return element
|
|
|
|
def append(self, tag, content, **kwargs):
|
|
"""Apends a tag in the format <tag attr='val' attr2='val2'>CDATA</tag>
|
|
into the tag represented by the current context. Returns the created
|
|
tag.
|
|
"""
|
|
element = self._xml_doc.createElement(tag)
|
|
|
|
for key, value in kwargs.items():
|
|
filtered_value = replace_nontext(six.text_type(value))
|
|
element.setAttribute(key, filtered_value)
|
|
|
|
if content:
|
|
element.appendChild(self._create_cdata_section(content))
|
|
|
|
self._append_child(element)
|
|
return element
|
|
|
|
def _append_child(self, element):
|
|
"""Appends a tag object represented by `element` into the tag
|
|
represented by the current context.
|
|
"""
|
|
if self._current_context:
|
|
self._current_context.element.appendChild(element)
|
|
else:
|
|
self._xml_doc.appendChild(element)
|
|
|
|
def increment_counter(self, counter_name):
|
|
"""Increments a counter in the current context and their parents.
|
|
"""
|
|
context = self._current_context
|
|
|
|
while context:
|
|
context.increment_counter(counter_name)
|
|
context = context.parent
|
|
|
|
def end_context(self):
|
|
"""Ends the current context and sets the current context as being the
|
|
previous one (if it exists). Also, when a context ends, its tag is
|
|
appended in the proper place inside the document.
|
|
"""
|
|
if not self._current_context:
|
|
return False
|
|
|
|
element = self._current_context.end()
|
|
|
|
self._current_context = self._current_context.parent
|
|
self._append_child(element)
|
|
|
|
return True
|
|
|
|
def finish(self):
|
|
"""Ends all open contexts and returns a pretty printed version of the
|
|
generated XML document.
|
|
"""
|
|
while self.end_context():
|
|
pass
|
|
return self._xml_doc.toprettyxml(indent='\t', encoding=UTF8)
|