Skip to content

Commit

Permalink
Introduce record_testsuite_property fixture
Browse files Browse the repository at this point in the history
This exposes the functionality introduced in fa6acdc as a session-scoped fixture.

Plugins that want to remain compatible with the `xunit2`
standard should use this fixture instead of `record_property`.

Fix #5202
  • Loading branch information
nicoddemus committed May 10, 2019
1 parent 3a4a815 commit 73bbff2
Show file tree
Hide file tree
Showing 5 changed files with 128 additions and 33 deletions.
5 changes: 5 additions & 0 deletions changelog/5202.feature.rst
@@ -0,0 +1,5 @@
New ``record_testsuite_property`` session-scoped fixture allows users to log ``<property>`` tags at the ``testsuite``
level with the ``junitxml`` plugin.

The generated XML is compatible with the latest xunit standard, contrary to
the properties recorded by ``record_property`` and ``record_xml_attribute``.
8 changes: 8 additions & 0 deletions doc/en/reference.rst
Expand Up @@ -424,6 +424,14 @@ record_property

.. autofunction:: _pytest.junitxml.record_property()


record_testsuite_property
~~~~~~~~~~~~~~~~~~~~~~~~~

**Tutorial**: :ref:`record_testsuite_property example`.

.. autofunction:: _pytest.junitxml.record_testsuite_property()

caplog
~~~~~~

Expand Down
57 changes: 25 additions & 32 deletions doc/en/usage.rst
Expand Up @@ -458,13 +458,6 @@ instead, configure the ``junit_duration_report`` option like this:
record_property
^^^^^^^^^^^^^^^




Fixture renamed from ``record_xml_property`` to ``record_property`` as user
properties are now available to all reporters.
``record_xml_property`` is now deprecated.

If you want to log additional information for a test, you can use the
``record_property`` fixture:

Expand Down Expand Up @@ -522,9 +515,7 @@ Will result in:
.. warning::

``record_property`` is an experimental feature and may change in the future.

Also please note that using this feature will break any schema verification.
Please note that using this feature will break schema verifications for the latest JUnitXML schema.
This might be a problem when used with some CI servers.

record_xml_attribute
Expand Down Expand Up @@ -587,55 +578,57 @@ Instead, this will add an attribute ``assertions="REQ-1234"`` inside the generat
</xs:complexType>
</xs:element>
LogXML: add_global_property
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. warning::

Please note that using this feature will break schema verifications for the latest JUnitXML schema.
This might be a problem when used with some CI servers.

.. _record_testsuite_property example:

If you want to add a properties node in the testsuite level, which may contains properties that are relevant
to all testcases you can use ``LogXML.add_global_properties``
record_testsuite_property
^^^^^^^^^^^^^^^^^^^^^^^^^

.. code-block:: python
import pytest
.. versionadded:: 4.5

If you want to add a properties node at the test-suite level, which may contains properties
that are relevant to all tests, you can use the ``record_testsuite_property`` session-scoped fixture:

@pytest.fixture(scope="session")
def log_global_env_facts(f):
The ``record_testsuite_property`` session-scoped fixture can be used to add properties relevant
to all tests.

if pytest.config.pluginmanager.hasplugin("junitxml"):
my_junit = getattr(pytest.config, "_xml", None)
.. code-block:: python
my_junit.add_global_property("ARCH", "PPC")
my_junit.add_global_property("STORAGE_TYPE", "CEPH")
import pytest
@pytest.mark.usefixtures(log_global_env_facts.__name__)
def start_and_prepare_env():
pass
@pytest.fixture(scope="session", autouse=True)
def log_global_env_facts(record_testsuite_property):
record_testsuite_property("ARCH", "PPC")
record_testsuite_property("STORAGE_TYPE", "CEPH")
class TestMe(object):
def test_foo(self):
assert True
This will add a property node below the testsuite node to the generated xml:
The fixture is a callable which receives ``name`` and ``value`` of a ``<property>`` tag
added at the test-suite level of the generated xml:

.. code-block:: xml
<testsuite errors="0" failures="0" name="pytest" skips="0" tests="1" time="0.006">
<testsuite errors="0" failures="0" name="pytest" skipped="0" tests="1" time="0.006">
<properties>
<property name="ARCH" value="PPC"/>
<property name="STORAGE_TYPE" value="CEPH"/>
</properties>
<testcase classname="test_me.TestMe" file="test_me.py" line="16" name="test_foo" time="0.000243663787842"/>
</testsuite>
.. warning::
``name`` must be a string, ``value`` will be converted to a string and properly xml-escaped.

The generated XML is compatible with the latest ``xunit`` standard, contrary to `record_property`_
and `record_xml_attribute`_.

This 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.

Creating resultlog format files
----------------------------------------------------
Expand Down
44 changes: 43 additions & 1 deletion src/_pytest/junitxml.py
Expand Up @@ -345,6 +345,45 @@ def add_attr_noop(name, value):
return attr_func


def _check_record_param_type(param, v):
"""Used by record_testsuite_property to check that the given parameter name is of the proper
type"""
__tracebackhide__ = True
if not isinstance(v, six.string_types):
msg = "{param} parameter needs to be a string, but {g} given"
raise TypeError(msg.format(param=param, g=type(v).__name__))


@pytest.fixture(scope="session")
def record_testsuite_property(request):
"""
Records a new ``<property>`` tag as child of the root ``<testsuite>``. This is suitable to
writing global information regarding the entire test suite, and is compatible with ``xunit2`` JUnit family.
This is a ``session``-scoped fixture which is called with ``(name, value)``. Example:
.. code-block:: python
def test_foo(record_testsuite_property):
record_testsuite_property("ARCH", "PPC")
record_testsuite_property("STORAGE_TYPE", "CEPH")
``name`` must be a string, ``value`` will be converted to a string and properly xml-escaped.
"""

__tracebackhide__ = True

def record_func(name, value):
"""noop function in case --junitxml was not passed in the command-line"""
__tracebackhide__ = True
_check_record_param_type("name", name)

xml = getattr(request.config, "_xml", None)
if xml is not None:
record_func = xml.add_global_property # noqa
return record_func


def pytest_addoption(parser):
group = parser.getgroup("terminal reporting")
group.addoption(
Expand Down Expand Up @@ -444,6 +483,7 @@ def __init__(
self.node_reporters = {} # nodeid -> _NodeReporter
self.node_reporters_ordered = []
self.global_properties = []

# List of reports that failed on call but teardown is pending.
self.open_reports = []
self.cnt_double_fail_tests = 0
Expand Down Expand Up @@ -632,7 +672,9 @@ def pytest_terminal_summary(self, terminalreporter):
terminalreporter.write_sep("-", "generated xml file: %s" % (self.logfile))

def add_global_property(self, name, value):
self.global_properties.append((str(name), bin_xml_escape(value)))
__tracebackhide__ = True
_check_record_param_type("name", name)
self.global_properties.append((name, bin_xml_escape(value)))

def _get_global_properties_node(self):
"""Return a Junit node containing custom properties, if any.
Expand Down
47 changes: 47 additions & 0 deletions testing/test_junitxml.py
Expand Up @@ -1243,6 +1243,53 @@ class Report(BaseReport):
), "The URL did not get written to the xml"


def test_record_testsuite_property(testdir):
testdir.makepyfile(
"""
def test_func1(record_testsuite_property):
record_testsuite_property("stats", "all good")
def test_func2(record_testsuite_property):
record_testsuite_property("stats", 10)
"""
)
result, dom = runandparse(testdir)
assert result.ret == 0
node = dom.find_first_by_tag("testsuite")
properties_node = node.find_first_by_tag("properties")
p1_node = properties_node.find_nth_by_tag("property", 0)
p2_node = properties_node.find_nth_by_tag("property", 1)
p1_node.assert_attr(name="stats", value="all good")
p2_node.assert_attr(name="stats", value="10")


def test_record_testsuite_property_junit_disabled(testdir):
testdir.makepyfile(
"""
def test_func1(record_testsuite_property):
record_testsuite_property("stats", "all good")
"""
)
result = testdir.runpytest()
assert result.ret == 0


@pytest.mark.parametrize("junit", [True, False])
def test_record_testsuite_property_type_checking(testdir, junit):
testdir.makepyfile(
"""
def test_func1(record_testsuite_property):
record_testsuite_property(1, 2)
"""
)
args = ("--junitxml=tests.xml",) if junit else ()
result = testdir.runpytest(*args)
assert result.ret == 1
result.stdout.fnmatch_lines(
["*TypeError: name parameter needs to be a string, but int given"]
)


@pytest.mark.parametrize("suite_name", ["my_suite", ""])
def test_set_suite_name(testdir, suite_name):
if suite_name:
Expand Down

0 comments on commit 73bbff2

Please sign in to comment.