Skip to content

Commit

Permalink
Fixed #35849 -- Made ParallelTestSuite report correct error location.
Browse files Browse the repository at this point in the history
  • Loading branch information
dcki authored and sarahboyce committed Nov 6, 2024
1 parent 41da8a4 commit 661dfdd
Show file tree
Hide file tree
Showing 3 changed files with 158 additions and 9 deletions.
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,7 @@ answer newbie questions, and generally made Django that much better:
David Sanders <[email protected]>
David Schein
David Tulig <[email protected]>
David Winiecki <[email protected]>
David Winterbottom <[email protected]>
David Wobrock <[email protected]>
Davide Ceretti <[email protected]>
Expand Down
26 changes: 23 additions & 3 deletions django/test/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import sys
import textwrap
import unittest
import unittest.suite
from collections import defaultdict
from contextlib import contextmanager
from importlib import import_module
Expand Down Expand Up @@ -292,7 +293,15 @@ def addDuration(self, test, elapsed):

def addError(self, test, err):
self.check_picklable(test, err)
self.events.append(("addError", self.test_index, err))

event_occurred_before_first_test = self.test_index == -1
if event_occurred_before_first_test and isinstance(
test, unittest.suite._ErrorHolder
):
self.events.append(("addError", self.test_index, test.id(), err))
else:
self.events.append(("addError", self.test_index, err))

super().addError(test, err)

def addFailure(self, test, err):
Expand Down Expand Up @@ -558,8 +567,19 @@ def handle_event(self, result, tests, event):
handler = getattr(result, event_name, None)
if handler is None:
return
test = tests[event[1]]
args = event[2:]
test_index = event[1]
event_occurred_before_first_test = test_index == -1
if (
event_name == "addError"
and event_occurred_before_first_test
and len(event) >= 4
):
test_id = event[2]
test = unittest.suite._ErrorHolder(test_id)
args = event[3:]
else:
test = tests[test_index]
args = event[2:]
handler(test, *args)

def __iter__(self):
Expand Down
140 changes: 134 additions & 6 deletions tests/test_runner/test_parallel.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import pickle
import sys
import unittest
from unittest.case import TestCase
from unittest.result import TestResult
from unittest.suite import TestSuite, _ErrorHolder

from django.test import SimpleTestCase
from django.test.runner import RemoteTestResult
from django.test.runner import ParallelTestSuite, RemoteTestResult
from django.utils.version import PY311, PY312

try:
Expand Down Expand Up @@ -59,6 +62,18 @@ def pickle_error_test(self):
self.fail("expected failure")


class SampleErrorTest(SimpleTestCase):
@classmethod
def setUpClass(cls):
raise ValueError("woops")
super().setUpClass()

# This method name doesn't begin with "test" to prevent test discovery
# from seeing it.
def dummy_test(self):
raise AssertionError("SampleErrorTest.dummy_test() was called")


class RemoteTestResultTest(SimpleTestCase):
def _test_error_exc_info(self):
try:
Expand All @@ -72,29 +87,70 @@ def test_was_successful_no_events(self):

def test_was_successful_one_success(self):
result = RemoteTestResult()
result.addSuccess(None)
test = None
result.startTest(test)
try:
result.addSuccess(test)
finally:
result.stopTest(test)
self.assertIs(result.wasSuccessful(), True)

def test_was_successful_one_expected_failure(self):
result = RemoteTestResult()
result.addExpectedFailure(None, self._test_error_exc_info())
test = None
result.startTest(test)
try:
result.addExpectedFailure(test, self._test_error_exc_info())
finally:
result.stopTest(test)
self.assertIs(result.wasSuccessful(), True)

def test_was_successful_one_skip(self):
result = RemoteTestResult()
result.addSkip(None, "Skipped")
test = None
result.startTest(test)
try:
result.addSkip(test, "Skipped")
finally:
result.stopTest(test)
self.assertIs(result.wasSuccessful(), True)

@unittest.skipUnless(tblib is not None, "requires tblib to be installed")
def test_was_successful_one_error(self):
result = RemoteTestResult()
result.addError(None, self._test_error_exc_info())
test = None
result.startTest(test)
try:
result.addError(test, self._test_error_exc_info())
finally:
result.stopTest(test)
self.assertIs(result.wasSuccessful(), False)

@unittest.skipUnless(tblib is not None, "requires tblib to be installed")
def test_was_successful_one_failure(self):
result = RemoteTestResult()
result.addFailure(None, self._test_error_exc_info())
test = None
result.startTest(test)
try:
result.addFailure(test, self._test_error_exc_info())
finally:
result.stopTest(test)
self.assertIs(result.wasSuccessful(), False)

@unittest.skipUnless(tblib is not None, "requires tblib to be installed")
def test_add_error_before_first_test(self):
result = RemoteTestResult()
test_id = "test_foo (tests.test_foo.FooTest.test_foo)"
test = _ErrorHolder(test_id)
# Call addError() without a call to startTest().
result.addError(test, self._test_error_exc_info())

(event,) = result.events
self.assertEqual(event[0], "addError")
self.assertEqual(event[1], -1)
self.assertEqual(event[2], test_id)
(error_type, _, _) = event[3]
self.assertEqual(error_type, ValueError)
self.assertIs(result.wasSuccessful(), False)

def test_picklable(self):
Expand Down Expand Up @@ -161,3 +217,75 @@ def test_add_duration(self):
result = RemoteTestResult()
result.addDuration(None, 2.3)
self.assertEqual(result.collectedDurations, [("None", 2.3)])


class ParallelTestSuiteTest(SimpleTestCase):
def test_handle_add_error_before_first_test(self):
dummy_subsuites = []
pts = ParallelTestSuite(dummy_subsuites, processes=2)
result = TestResult()
remote_result = RemoteTestResult()
test = SampleErrorTest(methodName="dummy_test")
suite = TestSuite([test])
suite.run(remote_result)
for event in remote_result.events:
pts.handle_event(result, tests=list(suite), event=event)

self.assertEqual(len(result.errors), 1)
actual_test, tb_and_details_str = result.errors[0]
self.assertIsInstance(actual_test, _ErrorHolder)
self.assertEqual(
actual_test.id(), "setUpClass (test_runner.test_parallel.SampleErrorTest)"
)
self.assertIn("Traceback (most recent call last):", tb_and_details_str)
self.assertIn("ValueError: woops", tb_and_details_str)

def test_handle_add_error_during_test(self):
dummy_subsuites = []
pts = ParallelTestSuite(dummy_subsuites, processes=2)
result = TestResult()
test = TestCase()
err = _test_error_exc_info()
event = ("addError", 0, err)
pts.handle_event(result, tests=[test], event=event)

self.assertEqual(len(result.errors), 1)
actual_test, tb_and_details_str = result.errors[0]
self.assertIsInstance(actual_test, TestCase)
self.assertEqual(actual_test.id(), "unittest.case.TestCase.runTest")
self.assertIn("Traceback (most recent call last):", tb_and_details_str)
self.assertIn("ValueError: woops", tb_and_details_str)

def test_handle_add_failure(self):
dummy_subsuites = []
pts = ParallelTestSuite(dummy_subsuites, processes=2)
result = TestResult()
test = TestCase()
err = _test_error_exc_info()
event = ("addFailure", 0, err)
pts.handle_event(result, tests=[test], event=event)

self.assertEqual(len(result.failures), 1)
actual_test, tb_and_details_str = result.failures[0]
self.assertIsInstance(actual_test, TestCase)
self.assertEqual(actual_test.id(), "unittest.case.TestCase.runTest")
self.assertIn("Traceback (most recent call last):", tb_and_details_str)
self.assertIn("ValueError: woops", tb_and_details_str)

def test_handle_add_success(self):
dummy_subsuites = []
pts = ParallelTestSuite(dummy_subsuites, processes=2)
result = TestResult()
test = TestCase()
event = ("addSuccess", 0)
pts.handle_event(result, tests=[test], event=event)

self.assertEqual(len(result.errors), 0)
self.assertEqual(len(result.failures), 0)


def _test_error_exc_info():
try:
raise ValueError("woops")
except ValueError:
return sys.exc_info()

0 comments on commit 661dfdd

Please sign in to comment.