From 73bbff2b7459e2523973ac90dcbf15c3b4e1684a Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 3 May 2019 16:30:16 -0300 Subject: [PATCH] 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 --- changelog/5202.feature.rst | 5 ++++ doc/en/reference.rst | 8 ++++++ doc/en/usage.rst | 57 +++++++++++++++++--------------------- src/_pytest/junitxml.py | 44 ++++++++++++++++++++++++++++- testing/test_junitxml.py | 47 +++++++++++++++++++++++++++++++ 5 files changed, 128 insertions(+), 33 deletions(-) create mode 100644 changelog/5202.feature.rst diff --git a/changelog/5202.feature.rst b/changelog/5202.feature.rst new file mode 100644 index 00000000000..82b718d9cc3 --- /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 f39f2a6e06d..437d6694ad7 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 9c5d4e25028..acf736f211e 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 +^^^^^^^^^^^^^^^^^^^^^^^^^ -.. 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 ```` 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 f1b7763e251..e3c98c37ea8 100644 --- a/src/_pytest/junitxml.py +++ b/src/_pytest/junitxml.py @@ -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 ```` 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 @@ 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 @@ -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. diff --git a/testing/test_junitxml.py b/testing/test_junitxml.py index a32eab2ec49..cca0143a239 100644 --- a/testing/test_junitxml.py +++ b/testing/test_junitxml.py @@ -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: