implement #3130 - adding record_xml_attribute fixture

update incorrect expected attribute value in test_record_attribute

attr names must be strings

Update CHANGELOG formatting

update usage documentation

Fix versionadded for record_xml_attribute

Indent the xml schema properly inside the warning box in the docs
This commit is contained in:
Raphael Castaneda 2018-01-18 16:06:42 -08:00
parent 1fd67c9000
commit a5e60b6a2d
5 changed files with 109 additions and 0 deletions

View File

@ -149,6 +149,7 @@ Punyashloka Biswal
Quentin Pradet Quentin Pradet
Ralf Schmitt Ralf Schmitt
Ran Benita Ran Benita
Raphael Castaneda
Raphael Pierzina Raphael Pierzina
Raquel Alegre Raquel Alegre
Ravi Chandra Ravi Chandra

View File

@ -85,6 +85,9 @@ class _NodeReporter(object):
def add_property(self, name, value): def add_property(self, name, value):
self.properties.append((str(name), bin_xml_escape(value))) self.properties.append((str(name), bin_xml_escape(value)))
def add_attribute(self, name, value):
self.attrs[str(name)] = bin_xml_escape(value)
def make_properties_node(self): def make_properties_node(self):
"""Return a Junit node containing custom properties, if any. """Return a Junit node containing custom properties, if any.
""" """
@ -98,6 +101,7 @@ class _NodeReporter(object):
def record_testreport(self, testreport): def record_testreport(self, testreport):
assert not self.testcase assert not self.testcase
names = mangle_test_address(testreport.nodeid) names = mangle_test_address(testreport.nodeid)
existing_attrs = self.attrs
classnames = names[:-1] classnames = names[:-1]
if self.xml.prefix: if self.xml.prefix:
classnames.insert(0, self.xml.prefix) classnames.insert(0, self.xml.prefix)
@ -111,6 +115,7 @@ class _NodeReporter(object):
if hasattr(testreport, "url"): if hasattr(testreport, "url"):
attrs["url"] = testreport.url attrs["url"] = testreport.url
self.attrs = attrs self.attrs = attrs
self.attrs.update(existing_attrs) # restore any user-defined attributes
def to_xml(self): def to_xml(self):
testcase = Junit.testcase(time=self.duration, **self.attrs) testcase = Junit.testcase(time=self.duration, **self.attrs)
@ -211,6 +216,27 @@ def record_xml_property(request):
return add_property_noop return add_property_noop
@pytest.fixture
def record_xml_attribute(request):
"""Add extra xml attributes to the tag for the calling test.
The fixture is callable with ``(name, value)``, with value being automatically
xml-encoded
"""
request.node.warn(
code='C3',
message='record_xml_attribute is an experimental feature',
)
xml = getattr(request.config, "_xml", None)
if xml is not None:
node_reporter = xml.node_reporter(request.node.nodeid)
return node_reporter.add_attribute
else:
def add_attr_noop(name, value):
pass
return add_attr_noop
def pytest_addoption(parser): def pytest_addoption(parser):
group = parser.getgroup("terminal reporting") group = parser.getgroup("terminal reporting")
group.addoption( group.addoption(

1
changelog/3130.feature Normal file
View File

@ -0,0 +1 @@
New fixture ``record_xml_attribute`` that allows modifying and inserting attributes on the ``<testcase>`` xml node in JUnit reports.

View File

@ -256,6 +256,66 @@ This will add an extra property ``example_key="1"`` to the generated
Also please note that using this feature will break any schema verification. Also please note that using this feature will break any schema verification.
This might be a problem when used with some CI servers. This might be a problem when used with some CI servers.
record_xml_attribute
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. versionadded:: 3.4
To add an additional xml attribute to a testcase element, you can use
``record_xml_attribute`` fixture. This can also be used to override existing values:
.. code-block:: python
def test_function(record_xml_attribute):
record_xml_attribute("assertions", "REQ-1234")
record_xml_attribute("classname", "custom_classname")
print('hello world')
assert True
Unlike ``record_xml_property``, this will not add a new child element.
Instead, this will add an attribute ``assertions="REQ-1234"`` inside the generated
``testcase`` tag and override the default ``classname`` with ``"classname=custom_classname"``:
.. code-block:: xml
<testcase classname="custom_classname" file="test_function.py" line="0" name="test_function" time="0.003" assertions="REQ-1234">
<system-out>
hello world
</system-out>
</testcase>
.. warning::
``record_xml_attribute`` is an experimental feature, and its interface might be replaced
by something more powerful and general in future versions. The
functionality per-se will be kept, however.
Using this over ``record_xml_property`` can help when using ci tools to parse the xml report.
However, some parsers are quite strict about the elements and attributes that are allowed.
Many tools use an xsd schema (like the example below) to validate incoming xml.
Make sure you are using attribute names that are allowed by your parser.
Below is the Scheme used by Jenkins to validate the XML report:
.. code-block:: xml
<xs:element name="testcase">
<xs:complexType>
<xs:sequence>
<xs:element ref="skipped" minOccurs="0" maxOccurs="1"/>
<xs:element ref="error" minOccurs="0" maxOccurs="unbounded"/>
<xs:element ref="failure" minOccurs="0" maxOccurs="unbounded"/>
<xs:element ref="system-out" minOccurs="0" maxOccurs="unbounded"/>
<xs:element ref="system-err" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
<xs:attribute name="name" type="xs:string" use="required"/>
<xs:attribute name="assertions" type="xs:string" use="optional"/>
<xs:attribute name="time" type="xs:string" use="optional"/>
<xs:attribute name="classname" type="xs:string" use="optional"/>
<xs:attribute name="status" type="xs:string" use="optional"/>
</xs:complexType>
</xs:element>
LogXML: add_global_property LogXML: add_global_property
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

View File

@ -879,6 +879,27 @@ def test_record_property_same_name(testdir):
pnodes[1].assert_attr(name="foo", value="baz") pnodes[1].assert_attr(name="foo", value="baz")
def test_record_attribute(testdir):
testdir.makepyfile("""
import pytest
@pytest.fixture
def other(record_xml_attribute):
record_xml_attribute("bar", 1)
def test_record(record_xml_attribute, other):
record_xml_attribute("foo", "<1");
""")
result, dom = runandparse(testdir, '-rw')
node = dom.find_first_by_tag("testsuite")
tnode = node.find_first_by_tag("testcase")
tnode.assert_attr(bar="1")
tnode.assert_attr(foo="<1")
result.stdout.fnmatch_lines([
'test_record_attribute.py::test_record',
'*record_xml_attribute*experimental*',
])
def test_random_report_log_xdist(testdir): def test_random_report_log_xdist(testdir):
"""xdist calls pytest_runtest_logreport as they are executed by the slaves, """xdist calls pytest_runtest_logreport as they are executed by the slaves,
with nodes from several nodes overlapping, so junitxml must cope with that with nodes from several nodes overlapping, so junitxml must cope with that