Skip to content

Commit 184ef92

Browse files
authored
Introduce record_testsuite_property fixture (#5205)
Introduce record_testsuite_property fixture
2 parents 3a4a815 + 73bbff2 commit 184ef92

File tree

5 files changed

+128
-33
lines changed

5 files changed

+128
-33
lines changed

changelog/5202.feature.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
New ``record_testsuite_property`` session-scoped fixture allows users to log ``<property>`` tags at the ``testsuite``
2+
level with the ``junitxml`` plugin.
3+
4+
The generated XML is compatible with the latest xunit standard, contrary to
5+
the properties recorded by ``record_property`` and ``record_xml_attribute``.

doc/en/reference.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,14 @@ record_property
424424

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

427+
428+
record_testsuite_property
429+
~~~~~~~~~~~~~~~~~~~~~~~~~
430+
431+
**Tutorial**: :ref:`record_testsuite_property example`.
432+
433+
.. autofunction:: _pytest.junitxml.record_testsuite_property()
434+
427435
caplog
428436
~~~~~~
429437

doc/en/usage.rst

Lines changed: 25 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -458,13 +458,6 @@ instead, configure the ``junit_duration_report`` option like this:
458458
record_property
459459
^^^^^^^^^^^^^^^
460460

461-
462-
463-
464-
Fixture renamed from ``record_xml_property`` to ``record_property`` as user
465-
properties are now available to all reporters.
466-
``record_xml_property`` is now deprecated.
467-
468461
If you want to log additional information for a test, you can use the
469462
``record_property`` fixture:
470463

@@ -522,9 +515,7 @@ Will result in:
522515
523516
.. warning::
524517

525-
``record_property`` is an experimental feature and may change in the future.
526-
527-
Also please note that using this feature will break any schema verification.
518+
Please note that using this feature will break schema verifications for the latest JUnitXML schema.
528519
This might be a problem when used with some CI servers.
529520

530521
record_xml_attribute
@@ -587,55 +578,57 @@ Instead, this will add an attribute ``assertions="REQ-1234"`` inside the generat
587578
</xs:complexType>
588579
</xs:element>
589580
590-
LogXML: add_global_property
591-
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
581+
.. warning::
592582

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

586+
.. _record_testsuite_property example:
594587

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

598-
.. code-block:: python
599-
600-
import pytest
591+
.. versionadded:: 4.5
601592

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

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

606-
if pytest.config.pluginmanager.hasplugin("junitxml"):
607-
my_junit = getattr(pytest.config, "_xml", None)
599+
.. code-block:: python
608600
609-
my_junit.add_global_property("ARCH", "PPC")
610-
my_junit.add_global_property("STORAGE_TYPE", "CEPH")
601+
import pytest
611602
612603
613-
@pytest.mark.usefixtures(log_global_env_facts.__name__)
614-
def start_and_prepare_env():
615-
pass
604+
@pytest.fixture(scope="session", autouse=True)
605+
def log_global_env_facts(record_testsuite_property):
606+
record_testsuite_property("ARCH", "PPC")
607+
record_testsuite_property("STORAGE_TYPE", "CEPH")
616608
617609
618610
class TestMe(object):
619611
def test_foo(self):
620612
assert True
621613
622-
This will add a property node below the testsuite node to the generated xml:
614+
The fixture is a callable which receives ``name`` and ``value`` of a ``<property>`` tag
615+
added at the test-suite level of the generated xml:
623616

624617
.. code-block:: xml
625618
626-
<testsuite errors="0" failures="0" name="pytest" skips="0" tests="1" time="0.006">
619+
<testsuite errors="0" failures="0" name="pytest" skipped="0" tests="1" time="0.006">
627620
<properties>
628621
<property name="ARCH" value="PPC"/>
629622
<property name="STORAGE_TYPE" value="CEPH"/>
630623
</properties>
631624
<testcase classname="test_me.TestMe" file="test_me.py" line="16" name="test_foo" time="0.000243663787842"/>
632625
</testsuite>
633626
634-
.. warning::
627+
``name`` must be a string, ``value`` will be converted to a string and properly xml-escaped.
628+
629+
The generated XML is compatible with the latest ``xunit`` standard, contrary to `record_property`_
630+
and `record_xml_attribute`_.
635631

636-
This is an experimental feature, and its interface might be replaced
637-
by something more powerful and general in future versions. The
638-
functionality per-se will be kept.
639632

640633
Creating resultlog format files
641634
----------------------------------------------------

src/_pytest/junitxml.py

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,45 @@ def add_attr_noop(name, value):
345345
return attr_func
346346

347347

348+
def _check_record_param_type(param, v):
349+
"""Used by record_testsuite_property to check that the given parameter name is of the proper
350+
type"""
351+
__tracebackhide__ = True
352+
if not isinstance(v, six.string_types):
353+
msg = "{param} parameter needs to be a string, but {g} given"
354+
raise TypeError(msg.format(param=param, g=type(v).__name__))
355+
356+
357+
@pytest.fixture(scope="session")
358+
def record_testsuite_property(request):
359+
"""
360+
Records a new ``<property>`` tag as child of the root ``<testsuite>``. This is suitable to
361+
writing global information regarding the entire test suite, and is compatible with ``xunit2`` JUnit family.
362+
363+
This is a ``session``-scoped fixture which is called with ``(name, value)``. Example:
364+
365+
.. code-block:: python
366+
367+
def test_foo(record_testsuite_property):
368+
record_testsuite_property("ARCH", "PPC")
369+
record_testsuite_property("STORAGE_TYPE", "CEPH")
370+
371+
``name`` must be a string, ``value`` will be converted to a string and properly xml-escaped.
372+
"""
373+
374+
__tracebackhide__ = True
375+
376+
def record_func(name, value):
377+
"""noop function in case --junitxml was not passed in the command-line"""
378+
__tracebackhide__ = True
379+
_check_record_param_type("name", name)
380+
381+
xml = getattr(request.config, "_xml", None)
382+
if xml is not None:
383+
record_func = xml.add_global_property # noqa
384+
return record_func
385+
386+
348387
def pytest_addoption(parser):
349388
group = parser.getgroup("terminal reporting")
350389
group.addoption(
@@ -444,6 +483,7 @@ def __init__(
444483
self.node_reporters = {} # nodeid -> _NodeReporter
445484
self.node_reporters_ordered = []
446485
self.global_properties = []
486+
447487
# List of reports that failed on call but teardown is pending.
448488
self.open_reports = []
449489
self.cnt_double_fail_tests = 0
@@ -632,7 +672,9 @@ def pytest_terminal_summary(self, terminalreporter):
632672
terminalreporter.write_sep("-", "generated xml file: %s" % (self.logfile))
633673

634674
def add_global_property(self, name, value):
635-
self.global_properties.append((str(name), bin_xml_escape(value)))
675+
__tracebackhide__ = True
676+
_check_record_param_type("name", name)
677+
self.global_properties.append((name, bin_xml_escape(value)))
636678

637679
def _get_global_properties_node(self):
638680
"""Return a Junit node containing custom properties, if any.

testing/test_junitxml.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1243,6 +1243,53 @@ class Report(BaseReport):
12431243
), "The URL did not get written to the xml"
12441244

12451245

1246+
def test_record_testsuite_property(testdir):
1247+
testdir.makepyfile(
1248+
"""
1249+
def test_func1(record_testsuite_property):
1250+
record_testsuite_property("stats", "all good")
1251+
1252+
def test_func2(record_testsuite_property):
1253+
record_testsuite_property("stats", 10)
1254+
"""
1255+
)
1256+
result, dom = runandparse(testdir)
1257+
assert result.ret == 0
1258+
node = dom.find_first_by_tag("testsuite")
1259+
properties_node = node.find_first_by_tag("properties")
1260+
p1_node = properties_node.find_nth_by_tag("property", 0)
1261+
p2_node = properties_node.find_nth_by_tag("property", 1)
1262+
p1_node.assert_attr(name="stats", value="all good")
1263+
p2_node.assert_attr(name="stats", value="10")
1264+
1265+
1266+
def test_record_testsuite_property_junit_disabled(testdir):
1267+
testdir.makepyfile(
1268+
"""
1269+
def test_func1(record_testsuite_property):
1270+
record_testsuite_property("stats", "all good")
1271+
"""
1272+
)
1273+
result = testdir.runpytest()
1274+
assert result.ret == 0
1275+
1276+
1277+
@pytest.mark.parametrize("junit", [True, False])
1278+
def test_record_testsuite_property_type_checking(testdir, junit):
1279+
testdir.makepyfile(
1280+
"""
1281+
def test_func1(record_testsuite_property):
1282+
record_testsuite_property(1, 2)
1283+
"""
1284+
)
1285+
args = ("--junitxml=tests.xml",) if junit else ()
1286+
result = testdir.runpytest(*args)
1287+
assert result.ret == 1
1288+
result.stdout.fnmatch_lines(
1289+
["*TypeError: name parameter needs to be a string, but int given"]
1290+
)
1291+
1292+
12461293
@pytest.mark.parametrize("suite_name", ["my_suite", ""])
12471294
def test_set_suite_name(testdir, suite_name):
12481295
if suite_name:

0 commit comments

Comments
 (0)