diff --git a/changelog/5202.feature.rst b/changelog/5202.feature.rst new file mode 100644 index 000000000..82b718d9c --- /dev/null +++ b/changelog/5202.feature.rst @@ -0,0 +1,5 @@ +New ``record_testsuite_property`` session-scoped fixture allows users to log ```` 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``. diff --git a/doc/en/reference.rst b/doc/en/reference.rst index f39f2a6e0..437d6694a 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -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 ~~~~~~ diff --git a/doc/en/usage.rst b/doc/en/usage.rst index 9c5d4e250..acf736f21 100644 --- a/doc/en/usage.rst +++ b/doc/en/usage.rst @@ -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: @@ -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 @@ -587,43 +578,45 @@ Instead, this will add an attribute ``assertions="REQ-1234"`` inside the generat -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 +^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. 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 import pytest - @pytest.fixture(scope="session") - def log_global_env_facts(f): - - if pytest.config.pluginmanager.hasplugin("junitxml"): - 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 + @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 ```` tag +added at the test-suite level of the generated xml: .. code-block:: xml - + @@ -631,11 +624,11 @@ This will add a property node below the testsuite node to the generated xml: -.. 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 ---------------------------------------------------- diff --git a/src/_pytest/junitxml.py b/src/_pytest/junitxml.py index f1b7763e2..e3c98c37e 100644 --- a/src/_pytest/junitxml.py +++ b/src/_pytest/junitxml.py @@ -345,6 +345,45 @@ def record_xml_attribute(request): 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 ```` tag as child of the root ````. 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( @@ -444,6 +483,7 @@ class LogXML(object): 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 @@ -632,7 +672,9 @@ class LogXML(object): 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. diff --git a/testing/test_junitxml.py b/testing/test_junitxml.py index a32eab2ec..cca0143a2 100644 --- a/testing/test_junitxml.py +++ b/testing/test_junitxml.py @@ -1243,6 +1243,53 @@ def test_url_property(testdir): ), "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: