From 88954d36208e21daadf2e19fced8a01d789e117d Mon Sep 17 00:00:00 2001 From: Dhruv Paranjape Date: Mon, 6 Apr 2026 12:52:52 +0200 Subject: [PATCH 1/2] fix: elapsed time calculation for parallel test runners. For test runners with python 3.12 or higher we should use `collectedDurations` to calculate the elapsed time. In parallel test runners the start/stopTest events are processed in the parent process which means they don't actually execute the test i.e. the test time is missing from our calculations. Using collectedDurations we can calculate the test time correctly. --- xmlrunner/result.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/xmlrunner/result.py b/xmlrunner/result.py index a759555..156220d 100644 --- a/xmlrunner/result.py +++ b/xmlrunner/result.py @@ -167,10 +167,12 @@ def __init__(self, test_result, test_method, outcome=SUCCESS, err=None, subTest= self.test_name = testcase_name(test_method) self.test_id = test_method.id() + self.test_str = str(test_method) if subTest: self.test_id = subTest.id() self.test_description = self.test_result.getDescription(subTest) + self.test_str = str(subTest) self.filename = filename self.lineno = lineno @@ -179,11 +181,25 @@ def __init__(self, test_result, test_method, outcome=SUCCESS, err=None, subTest= def id(self): return self.test_id + def _get_elapsed_time(self): + """Return elapsed time from addDuration (3.12+) or start/stop delta. + + In parallel test runners like Django's, startTest/stopTest events are + replayed in the parent process, making stop_time - start_time near-zero. + Python 3.12's addDuration carries the real worker-measured elapsed time + correctly allowing us to prevent this issue for 3.12+. + """ + collected = getattr(self.test_result, 'collectedDurations', []) + for test_repr, elapsed in reversed(collected): + if test_repr == self.test_str: + return elapsed + + return self.test_result.stop_time - self.test_result.start_time + def test_finished(self): """Save info that can only be calculated once a test has run. """ - self.elapsed_time = \ - self.test_result.stop_time - self.test_result.start_time + self.elapsed_time = self._get_elapsed_time() timestamp = datetime.datetime.fromtimestamp(self.test_result.stop_time) self.timestamp = timestamp.replace(microsecond=0).isoformat() From 31aa68f89abcea5ddbedd424c3f7f3f6ae89c7f6 Mon Sep 17 00:00:00 2001 From: Dhruv Paranjape Date: Mon, 6 Apr 2026 12:53:18 +0200 Subject: [PATCH 2/2] extend `_test_xmlrunner` to validate the elapsed time to prevent regression. --- tests/testsuite.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/tests/testsuite.py b/tests/testsuite.py index 68616e3..99b5e7c 100755 --- a/tests/testsuite.py +++ b/tests/testsuite.py @@ -240,8 +240,18 @@ def _test_xmlrunner(self, suite, runner=None, outdir=None): runner.run(suite) if isinstance(outdir, BytesIO): self.assertTrue(outdir.getvalue()) + xml_content = outdir.getvalue() else: - self.assertEqual(1, len(glob(os.path.join(outdir, '*xml')))) + xml_files = glob(os.path.join(outdir, '*xml')) + self.assertEqual(1, len(xml_files)) + with open(xml_files[0], 'rb') as f: + xml_content = f.read() + + doc = minidom.parseString(xml_content) + for testcase in doc.getElementsByTagName('testcase'): + elapsed = float(testcase.getAttribute('time')) + self.assertGreaterEqual(elapsed, 0.0) + return runner def test_basic_unittest_constructs(self): @@ -903,6 +913,15 @@ class TestWithPartialmethod(unittest.TestCase): suite.addTest(TestWithPartialmethod('test_partialmethod')) self._test_xmlrunner(suite) + def test_elapsed_time_with_docstring(self): + class DocstringTest(unittest.TestCase): + def test_with_doc(self): + """This test has a docstring.""" + pass + + suite = unittest.TestSuite() + suite.addTest(DocstringTest('test_with_doc')) + self._test_xmlrunner(suite) class DuplicateWriterTestCase(unittest.TestCase):