Skip to content

Commit 661dfdd

Browse files
dckisarahboyce
authored andcommitted
Fixed #35849 -- Made ParallelTestSuite report correct error location.
1 parent 41da8a4 commit 661dfdd

File tree

3 files changed

+158
-9
lines changed

3 files changed

+158
-9
lines changed

AUTHORS

+1
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,7 @@ answer newbie questions, and generally made Django that much better:
282282
David Sanders <[email protected]>
283283
David Schein
284284
David Tulig <[email protected]>
285+
David Winiecki <[email protected]>
285286
David Winterbottom <[email protected]>
286287
David Wobrock <[email protected]>
287288
Davide Ceretti <[email protected]>

django/test/runner.py

+23-3
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import sys
1313
import textwrap
1414
import unittest
15+
import unittest.suite
1516
from collections import defaultdict
1617
from contextlib import contextmanager
1718
from importlib import import_module
@@ -292,7 +293,15 @@ def addDuration(self, test, elapsed):
292293

293294
def addError(self, test, err):
294295
self.check_picklable(test, err)
295-
self.events.append(("addError", self.test_index, err))
296+
297+
event_occurred_before_first_test = self.test_index == -1
298+
if event_occurred_before_first_test and isinstance(
299+
test, unittest.suite._ErrorHolder
300+
):
301+
self.events.append(("addError", self.test_index, test.id(), err))
302+
else:
303+
self.events.append(("addError", self.test_index, err))
304+
296305
super().addError(test, err)
297306

298307
def addFailure(self, test, err):
@@ -558,8 +567,19 @@ def handle_event(self, result, tests, event):
558567
handler = getattr(result, event_name, None)
559568
if handler is None:
560569
return
561-
test = tests[event[1]]
562-
args = event[2:]
570+
test_index = event[1]
571+
event_occurred_before_first_test = test_index == -1
572+
if (
573+
event_name == "addError"
574+
and event_occurred_before_first_test
575+
and len(event) >= 4
576+
):
577+
test_id = event[2]
578+
test = unittest.suite._ErrorHolder(test_id)
579+
args = event[3:]
580+
else:
581+
test = tests[test_index]
582+
args = event[2:]
563583
handler(test, *args)
564584

565585
def __iter__(self):

tests/test_runner/test_parallel.py

+134-6
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import pickle
22
import sys
33
import unittest
4+
from unittest.case import TestCase
5+
from unittest.result import TestResult
6+
from unittest.suite import TestSuite, _ErrorHolder
47

58
from django.test import SimpleTestCase
6-
from django.test.runner import RemoteTestResult
9+
from django.test.runner import ParallelTestSuite, RemoteTestResult
710
from django.utils.version import PY311, PY312
811

912
try:
@@ -59,6 +62,18 @@ def pickle_error_test(self):
5962
self.fail("expected failure")
6063

6164

65+
class SampleErrorTest(SimpleTestCase):
66+
@classmethod
67+
def setUpClass(cls):
68+
raise ValueError("woops")
69+
super().setUpClass()
70+
71+
# This method name doesn't begin with "test" to prevent test discovery
72+
# from seeing it.
73+
def dummy_test(self):
74+
raise AssertionError("SampleErrorTest.dummy_test() was called")
75+
76+
6277
class RemoteTestResultTest(SimpleTestCase):
6378
def _test_error_exc_info(self):
6479
try:
@@ -72,29 +87,70 @@ def test_was_successful_no_events(self):
7287

7388
def test_was_successful_one_success(self):
7489
result = RemoteTestResult()
75-
result.addSuccess(None)
90+
test = None
91+
result.startTest(test)
92+
try:
93+
result.addSuccess(test)
94+
finally:
95+
result.stopTest(test)
7696
self.assertIs(result.wasSuccessful(), True)
7797

7898
def test_was_successful_one_expected_failure(self):
7999
result = RemoteTestResult()
80-
result.addExpectedFailure(None, self._test_error_exc_info())
100+
test = None
101+
result.startTest(test)
102+
try:
103+
result.addExpectedFailure(test, self._test_error_exc_info())
104+
finally:
105+
result.stopTest(test)
81106
self.assertIs(result.wasSuccessful(), True)
82107

83108
def test_was_successful_one_skip(self):
84109
result = RemoteTestResult()
85-
result.addSkip(None, "Skipped")
110+
test = None
111+
result.startTest(test)
112+
try:
113+
result.addSkip(test, "Skipped")
114+
finally:
115+
result.stopTest(test)
86116
self.assertIs(result.wasSuccessful(), True)
87117

88118
@unittest.skipUnless(tblib is not None, "requires tblib to be installed")
89119
def test_was_successful_one_error(self):
90120
result = RemoteTestResult()
91-
result.addError(None, self._test_error_exc_info())
121+
test = None
122+
result.startTest(test)
123+
try:
124+
result.addError(test, self._test_error_exc_info())
125+
finally:
126+
result.stopTest(test)
92127
self.assertIs(result.wasSuccessful(), False)
93128

94129
@unittest.skipUnless(tblib is not None, "requires tblib to be installed")
95130
def test_was_successful_one_failure(self):
96131
result = RemoteTestResult()
97-
result.addFailure(None, self._test_error_exc_info())
132+
test = None
133+
result.startTest(test)
134+
try:
135+
result.addFailure(test, self._test_error_exc_info())
136+
finally:
137+
result.stopTest(test)
138+
self.assertIs(result.wasSuccessful(), False)
139+
140+
@unittest.skipUnless(tblib is not None, "requires tblib to be installed")
141+
def test_add_error_before_first_test(self):
142+
result = RemoteTestResult()
143+
test_id = "test_foo (tests.test_foo.FooTest.test_foo)"
144+
test = _ErrorHolder(test_id)
145+
# Call addError() without a call to startTest().
146+
result.addError(test, self._test_error_exc_info())
147+
148+
(event,) = result.events
149+
self.assertEqual(event[0], "addError")
150+
self.assertEqual(event[1], -1)
151+
self.assertEqual(event[2], test_id)
152+
(error_type, _, _) = event[3]
153+
self.assertEqual(error_type, ValueError)
98154
self.assertIs(result.wasSuccessful(), False)
99155

100156
def test_picklable(self):
@@ -161,3 +217,75 @@ def test_add_duration(self):
161217
result = RemoteTestResult()
162218
result.addDuration(None, 2.3)
163219
self.assertEqual(result.collectedDurations, [("None", 2.3)])
220+
221+
222+
class ParallelTestSuiteTest(SimpleTestCase):
223+
def test_handle_add_error_before_first_test(self):
224+
dummy_subsuites = []
225+
pts = ParallelTestSuite(dummy_subsuites, processes=2)
226+
result = TestResult()
227+
remote_result = RemoteTestResult()
228+
test = SampleErrorTest(methodName="dummy_test")
229+
suite = TestSuite([test])
230+
suite.run(remote_result)
231+
for event in remote_result.events:
232+
pts.handle_event(result, tests=list(suite), event=event)
233+
234+
self.assertEqual(len(result.errors), 1)
235+
actual_test, tb_and_details_str = result.errors[0]
236+
self.assertIsInstance(actual_test, _ErrorHolder)
237+
self.assertEqual(
238+
actual_test.id(), "setUpClass (test_runner.test_parallel.SampleErrorTest)"
239+
)
240+
self.assertIn("Traceback (most recent call last):", tb_and_details_str)
241+
self.assertIn("ValueError: woops", tb_and_details_str)
242+
243+
def test_handle_add_error_during_test(self):
244+
dummy_subsuites = []
245+
pts = ParallelTestSuite(dummy_subsuites, processes=2)
246+
result = TestResult()
247+
test = TestCase()
248+
err = _test_error_exc_info()
249+
event = ("addError", 0, err)
250+
pts.handle_event(result, tests=[test], event=event)
251+
252+
self.assertEqual(len(result.errors), 1)
253+
actual_test, tb_and_details_str = result.errors[0]
254+
self.assertIsInstance(actual_test, TestCase)
255+
self.assertEqual(actual_test.id(), "unittest.case.TestCase.runTest")
256+
self.assertIn("Traceback (most recent call last):", tb_and_details_str)
257+
self.assertIn("ValueError: woops", tb_and_details_str)
258+
259+
def test_handle_add_failure(self):
260+
dummy_subsuites = []
261+
pts = ParallelTestSuite(dummy_subsuites, processes=2)
262+
result = TestResult()
263+
test = TestCase()
264+
err = _test_error_exc_info()
265+
event = ("addFailure", 0, err)
266+
pts.handle_event(result, tests=[test], event=event)
267+
268+
self.assertEqual(len(result.failures), 1)
269+
actual_test, tb_and_details_str = result.failures[0]
270+
self.assertIsInstance(actual_test, TestCase)
271+
self.assertEqual(actual_test.id(), "unittest.case.TestCase.runTest")
272+
self.assertIn("Traceback (most recent call last):", tb_and_details_str)
273+
self.assertIn("ValueError: woops", tb_and_details_str)
274+
275+
def test_handle_add_success(self):
276+
dummy_subsuites = []
277+
pts = ParallelTestSuite(dummy_subsuites, processes=2)
278+
result = TestResult()
279+
test = TestCase()
280+
event = ("addSuccess", 0)
281+
pts.handle_event(result, tests=[test], event=event)
282+
283+
self.assertEqual(len(result.errors), 0)
284+
self.assertEqual(len(result.failures), 0)
285+
286+
287+
def _test_error_exc_info():
288+
try:
289+
raise ValueError("woops")
290+
except ValueError:
291+
return sys.exc_info()

0 commit comments

Comments
 (0)