Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce record_testsuite_property fixture #5205

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions changelog/5202.feature.rst
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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.
nicoddemus marked this conversation as resolved.
Show resolved Hide resolved
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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