diff --git a/AUTHORS b/AUTHORS index 684063778f7..00ced49f018 100644 --- a/AUTHORS +++ b/AUTHORS @@ -17,6 +17,7 @@ Anders Hovmöller Andras Tim Andrea Cimatoribus Andreas Zeidler +Andrey Paramonov Andrzej Ostrowski Andy Freeland Anthon van der Neut diff --git a/changelog/4483.feature.rst b/changelog/4483.feature.rst new file mode 100644 index 00000000000..d9bd4c7173f --- /dev/null +++ b/changelog/4483.feature.rst @@ -0,0 +1,2 @@ +Add ini parameter ``junit_time`` to optionally report test call +durations less setup and teardown times. diff --git a/doc/en/usage.rst b/doc/en/usage.rst index 49c2aa577b8..7c3ef19fbdf 100644 --- a/doc/en/usage.rst +++ b/doc/en/usage.rst @@ -294,6 +294,20 @@ To set the name of the root test suite xml item, you can configure the ``junit_s [pytest] junit_suite_name = my_suite +.. versionadded:: 4.0 + +JUnit XML specification seems to indicate that ``"time"`` attribute +should report total test execution times, including setup and teardown +(`1 `_, `2 +`_). +It is the default pytest behavior. To report just call durations +instead, configure the ``junit_time`` option like this: + +.. code-block:: ini + + [pytest] + junit_time = call + .. _record_property example: record_property diff --git a/src/_pytest/junitxml.py b/src/_pytest/junitxml.py index 09847c942da..696deb6e9bd 100644 --- a/src/_pytest/junitxml.py +++ b/src/_pytest/junitxml.py @@ -323,6 +323,9 @@ def pytest_addoption(parser): "one of no|system-out|system-err", default="no", ) # choices=['no', 'stdout', 'stderr']) + parser.addini( + "junit_time", "Duration time to report: one of total|call", default="total" + ) # choices=['total', 'call']) def pytest_configure(config): @@ -334,6 +337,7 @@ def pytest_configure(config): config.option.junitprefix, config.getini("junit_suite_name"), config.getini("junit_logging"), + config.getini("junit_time"), ) config.pluginmanager.register(config._xml) @@ -361,12 +365,20 @@ def mangle_test_address(address): class LogXML(object): - def __init__(self, logfile, prefix, suite_name="pytest", logging="no"): + def __init__( + self, + logfile, + prefix, + suite_name="pytest", + logging="no", + report_duration="total", + ): logfile = os.path.expanduser(os.path.expandvars(logfile)) self.logfile = os.path.normpath(os.path.abspath(logfile)) self.prefix = prefix self.suite_name = suite_name self.logging = logging + self.report_duration = report_duration self.stats = dict.fromkeys(["error", "passed", "failure", "skipped"], 0) self.node_reporters = {} # nodeid -> _NodeReporter self.node_reporters_ordered = [] @@ -500,8 +512,9 @@ def update_testcase_duration(self, report): """accumulates total duration for nodeid from given report and updates the Junit.testcase with the new total if already created. """ - reporter = self.node_reporter(report) - reporter.duration += getattr(report, "duration", 0.0) + if self.report_duration == "total" or report.when == self.report_duration: + reporter = self.node_reporter(report) + reporter.duration += getattr(report, "duration", 0.0) def pytest_collectreport(self, report): if not report.passed: diff --git a/testing/test_junitxml.py b/testing/test_junitxml.py index c9dc39f82b7..aafbb8da9df 100644 --- a/testing/test_junitxml.py +++ b/testing/test_junitxml.py @@ -153,6 +153,24 @@ def test_sleep(): val = tnode["time"] assert round(float(val), 2) >= 0.03 + def test_call_time(self, testdir): + testdir.makepyfile( + """ + import time, pytest + def setup_module(): + time.sleep(0.1) + def teardown_module(): + time.sleep(0.1) + def test_sleep(): + time.sleep(0.1) + """ + ) + result, dom = runandparse(testdir, "-o", "junit_time=call") + node = dom.find_first_by_tag("testsuite") + tnode = node.find_first_by_tag("testcase") + val = tnode["time"] + assert 0.1 <= round(float(val), 2) < 0.2 + def test_setup_error(self, testdir): testdir.makepyfile( """