Skip to content

Commit

Permalink
Merge pull request #3132 from raphaelcastaneda/feature/add-record-xml…
Browse files Browse the repository at this point in the history
…-attribute

implement #3130 - add record_xml_attribute fixture
  • Loading branch information
nicoddemus authored Jan 29, 2018
2 parents 32979de + a5e60b6 commit 49773b5
Show file tree
Hide file tree
Showing 5 changed files with 109 additions and 0 deletions.
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ Punyashloka Biswal
Quentin Pradet
Ralf Schmitt
Ran Benita
Raphael Castaneda
Raphael Pierzina
Raquel Alegre
Ravi Chandra
Expand Down
26 changes: 26 additions & 0 deletions _pytest/junitxml.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@ def append(self, node):
def add_property(self, name, 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):
"""Return a Junit node containing custom properties, if any.
"""
Expand All @@ -98,6 +101,7 @@ def make_properties_node(self):
def record_testreport(self, testreport):
assert not self.testcase
names = mangle_test_address(testreport.nodeid)
existing_attrs = self.attrs
classnames = names[:-1]
if self.xml.prefix:
classnames.insert(0, self.xml.prefix)
Expand All @@ -111,6 +115,7 @@ def record_testreport(self, testreport):
if hasattr(testreport, "url"):
attrs["url"] = testreport.url
self.attrs = attrs
self.attrs.update(existing_attrs) # restore any user-defined attributes

def to_xml(self):
testcase = Junit.testcase(time=self.duration, **self.attrs)
Expand Down Expand Up @@ -211,6 +216,27 @@ def add_property_noop(name, value):
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):
group = parser.getgroup("terminal reporting")
group.addoption(
Expand Down
1 change: 1 addition & 0 deletions changelog/3130.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
New fixture ``record_xml_attribute`` that allows modifying and inserting attributes on the ``<testcase>`` xml node in JUnit reports.
60 changes: 60 additions & 0 deletions doc/en/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
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
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Expand Down
21 changes: 21 additions & 0 deletions testing/test_junitxml.py
Original file line number Diff line number Diff line change
Expand Up @@ -879,6 +879,27 @@ def test_record_with_same_name(record_xml_property):
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):
"""xdist calls pytest_runtest_logreport as they are executed by the slaves,
with nodes from several nodes overlapping, so junitxml must cope with that
Expand Down

0 comments on commit 49773b5

Please sign in to comment.