Introduce record_testsuite_property fixture

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
This commit is contained in:
Bruno Oliveira 2019-05-03 16:30:16 -03:00 committed by Bruno Oliveira
parent 3a4a815c41
commit 73bbff2b74
5 changed files with 129 additions and 34 deletions

View File

@ -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``.

View File

@ -424,6 +424,14 @@ record_property
.. autofunction:: _pytest.junitxml.record_property() .. autofunction:: _pytest.junitxml.record_property()
record_testsuite_property
~~~~~~~~~~~~~~~~~~~~~~~~~
**Tutorial**: :ref:`record_testsuite_property example`.
.. autofunction:: _pytest.junitxml.record_testsuite_property()
caplog caplog
~~~~~~ ~~~~~~

View File

@ -458,13 +458,6 @@ instead, configure the ``junit_duration_report`` option like this:
record_property 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 If you want to log additional information for a test, you can use the
``record_property`` fixture: ``record_property`` fixture:
@ -522,9 +515,7 @@ Will result in:
.. warning:: .. warning::
``record_property`` is an experimental feature and may change in the future. Please note that using this feature will break schema verifications for the latest JUnitXML schema.
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 record_xml_attribute
@ -587,43 +578,45 @@ Instead, this will add an attribute ``assertions="REQ-1234"`` inside the generat
</xs:complexType> </xs:complexType>
</xs:element> </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 record_testsuite_property
to all testcases you can use ``LogXML.add_global_properties`` ^^^^^^^^^^^^^^^^^^^^^^^^^
.. 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:
The ``record_testsuite_property`` session-scoped fixture can be used to add properties relevant
to all tests.
.. code-block:: python .. code-block:: python
import pytest import pytest
@pytest.fixture(scope="session") @pytest.fixture(scope="session", autouse=True)
def log_global_env_facts(f): def log_global_env_facts(record_testsuite_property):
record_testsuite_property("ARCH", "PPC")
if pytest.config.pluginmanager.hasplugin("junitxml"): record_testsuite_property("STORAGE_TYPE", "CEPH")
my_junit = getattr(pytest.config, "_xml", None)
my_junit.add_global_property("ARCH", "PPC")
my_junit.add_global_property("STORAGE_TYPE", "CEPH")
@pytest.mark.usefixtures(log_global_env_facts.__name__)
def start_and_prepare_env():
pass
class TestMe(object): class TestMe(object):
def test_foo(self): def test_foo(self):
assert True 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 .. 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> <properties>
<property name="ARCH" value="PPC"/> <property name="ARCH" value="PPC"/>
<property name="STORAGE_TYPE" value="CEPH"/> <property name="STORAGE_TYPE" value="CEPH"/>
@ -631,11 +624,11 @@ This will add a property node below the testsuite node to the generated xml:
<testcase classname="test_me.TestMe" file="test_me.py" line="16" name="test_foo" time="0.000243663787842"/> <testcase classname="test_me.TestMe" file="test_me.py" line="16" name="test_foo" time="0.000243663787842"/>
</testsuite> </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 Creating resultlog format files
---------------------------------------------------- ----------------------------------------------------

View File

@ -345,6 +345,45 @@ def record_xml_attribute(request):
return attr_func 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): def pytest_addoption(parser):
group = parser.getgroup("terminal reporting") group = parser.getgroup("terminal reporting")
group.addoption( group.addoption(
@ -444,6 +483,7 @@ class LogXML(object):
self.node_reporters = {} # nodeid -> _NodeReporter self.node_reporters = {} # nodeid -> _NodeReporter
self.node_reporters_ordered = [] self.node_reporters_ordered = []
self.global_properties = [] self.global_properties = []
# List of reports that failed on call but teardown is pending. # List of reports that failed on call but teardown is pending.
self.open_reports = [] self.open_reports = []
self.cnt_double_fail_tests = 0 self.cnt_double_fail_tests = 0
@ -632,7 +672,9 @@ class LogXML(object):
terminalreporter.write_sep("-", "generated xml file: %s" % (self.logfile)) terminalreporter.write_sep("-", "generated xml file: %s" % (self.logfile))
def add_global_property(self, name, value): 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): def _get_global_properties_node(self):
"""Return a Junit node containing custom properties, if any. """Return a Junit node containing custom properties, if any.

View File

@ -1243,6 +1243,53 @@ def test_url_property(testdir):
), "The URL did not get written to the xml" ), "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", ""]) @pytest.mark.parametrize("suite_name", ["my_suite", ""])
def test_set_suite_name(testdir, suite_name): def test_set_suite_name(testdir, suite_name):
if suite_name: if suite_name: