Opened 3 years ago

Closed 3 years ago

Last modified 20 months ago

#32655 closed Cleanup/optimization (fixed)

Deprecate DiscoverRunner.build_suite()'s extra_tests argument.

Reported by: Chris Jerdonek Owned by: Jacob Walls
Component: Testing framework Version: 4.0
Severity: Normal Keywords: iter_test_cases, DiscoverRunner, extra_tests
Cc: Triage Stage: Ready for checkin
Has patch: yes Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description (last modified by Chris Jerdonek)

Currently, if you pass a string like 'abc' rather than a TestCase instance as one of the items in the extra_tests argument to DiscoverRunner.build_suite(), you will get a not-too-helpful error that looks something like this:

Traceback (most recent call last):
  File "./runtests.py", line 593, in <module>
    options.timing,
  File "./runtests.py", line 325, in django_tests
    failures = test_runner.run_tests(test_labels or get_installed())
  File ".../django/test/runner.py", line 759, in run_tests
    suite = self.build_suite(test_labels, extra_tests)
  File ".../django/test/runner.py", line 641, in build_suite
    all_tests.extend(iter_test_cases(extra_tests))
  File ".../django/test/utils.py", line 249, in iter_test_cases
    yield from iter_test_cases(test)
  File ".../django/test/utils.py", line 249, in iter_test_cases
    yield from iter_test_cases(test)
  File ".../django/test/utils.py", line 249, in iter_test_cases
    yield from iter_test_cases(test)
  [Previous line repeated 991 more times]
  File ".../django/test/utils.py", line 245, in iter_test_cases
    if isinstance(test, TestCase):
RecursionError: maximum recursion depth exceeded while calling a Python object

This is because iter_test_cases() goes into an infinite loop when it gets to iter_test_cases('a') (iterating the first element of 'abc').

Since passing a string is a plausible mistake, I think it would be worth doing something to short-circuit this case and prevent a RecursionError. The following would accomplish this with a pointer to the problem and without adding too much complexity:

diff --git a/django/test/utils.py b/django/test/utils.py
index e977db8a10..ec4dc1837d 100644
--- a/django/test/utils.py
+++ b/django/test/utils.py
@@ -244,6 +244,8 @@ def iter_test_cases(tests):
     for test in tests:
         if isinstance(test, TestCase):
             yield test
+        elif isinstance(test, str):
+            # Prevent an unfriendly RecursionError that can happen with strings.
+            raise TypeError(f'test {test!r} must be a test case or test suite not string')
         else:
             # Otherwise, assume it is a test suite.
             yield from iter_test_cases(test)

It would result in the following:

Traceback (most recent call last):
  File "./runtests.py", line 593, in <module>
    options.timing,
  File "./runtests.py", line 325, in django_tests
    failures = test_runner.run_tests(test_labels or get_installed())
  File ".../django/test/runner.py", line 758, in run_tests
    suite = self.build_suite(test_labels, extra_tests)
  File ".../django/test/runner.py", line 641, in build_suite
    all_tests.extend(iter_test_cases(extra_tests))
  File ".../django/test/utils.py", line 248, in iter_test_cases
    raise TypeError(f'test {test!r} must be a test case or test suite not string')
TypeError: test 'abc' must be a test case or test suite not string

Note that prior to #32489 ("Add a generator function in runner.py to iterate over a test suite's test cases"), the error looked like the following, which was better than what it is now, but not quite as good as what I propose above:

Traceback (most recent call last):
  File "./runtests.py", line 593, in <module>
    options.timing,
  File "./runtests.py", line 325, in django_tests
    failures = test_runner.run_tests(test_labels or get_installed())
  File ".../django/test/runner.py", line 724, in run_tests
    suite = self.build_suite(test_labels, extra_tests)
  File ".../django/test/runner.py", line 615, in build_suite
    suite.addTest(test)
  File ".../unittest/suite.py", line 47, in addTest
    raise TypeError("{} is not callable".format(repr(test)))
TypeError: 'abc' is not callable

Change History (16)

comment:1 by Chris Jerdonek, 3 years ago

Description: modified (diff)

comment:2 by Chris Jerdonek, 3 years ago

Note that this issue was inspired by this error coming up in real-life here: https://github.com/django/django/pull/14261#issuecomment-819546454

(The person didn't know why the error was happening.)

Last edited 3 years ago by Chris Jerdonek (previous) (diff)

comment:3 by Mariusz Felisiak, 3 years ago

extra_tests is unused since 1e72b1c5c11d1d2dc3ce3660a1eb6b775dcca5a5. Maybe we should deprecate it (and remove in Django 5.0), I don't see much value in keeping this argument.

comment:4 by Carlton Gibson, 3 years ago

Triage Stage: UnreviewedAccepted

OK, one way or the other, we can do something here. Thanks Chris.

comment:5 by Chris Jerdonek, 3 years ago

Thanks. A couple thoughts: Before deprecating extra_tests, we might want to think through what the alternative escape hatch is for users that might want to add tests not currently available via discovery. Though iter_test_cases() is an internal function, having the extra check could still help to avoid some puzzling moments.

in reply to:  5 comment:6 by Mariusz Felisiak, 3 years ago

Thanks. A couple thoughts: Before deprecating extra_tests, we might want to think through what the alternative escape hatch is for users that might want to add tests not currently available via discovery.

They can always subclass DiscoverRunner. extra_tests is not used internally and it's untested. I don't think we need to provide any alternative. We can always ask for opinions on the DevelopersMailingList.

Though iter_test_cases() is an internal function, having the extra check could still help to avoid some puzzling moments.

Agreed.

Version 0, edited 3 years ago by Mariusz Felisiak (next)

comment:8 by Mariusz Felisiak, 3 years ago

Summary: Improve error if a string is passed as an extra_test to DiscoverRunner.build_suite()Deprecate DiscoverRunner.build_suite()'s extra_tests argument.

comment:9 by Mariusz Felisiak <felisiak.mariusz@…>, 3 years ago

In 8bca838:

Refs #32655 -- Improved error if iter_test_cases() is passed a string.

comment:10 by Mariusz Felisiak, 3 years ago

Has patch: unset

comment:11 by Jacob Walls, 3 years ago

Has patch: set
Owner: changed from nobody to Jacob Walls
Status: newassigned

comment:12 by Mariusz Felisiak, 3 years ago

Patch needs improvement: set

comment:13 by Jacob Walls, 3 years ago

Patch needs improvement: unset

comment:14 by Mariusz Felisiak, 3 years ago

Triage Stage: AcceptedReady for checkin

comment:15 by Mariusz Felisiak <felisiak.mariusz@…>, 3 years ago

Resolution: fixed
Status: assignedclosed

In 56f9579:

Fixed #32655 -- Deprecated extra_tests argument for DiscoverRunner.build_suite()/run_tests().

comment:16 by Mariusz Felisiak <felisiak.mariusz@…>, 20 months ago

In 43b01300:

Refs #32655 -- Removed extra_tests argument for DiscoverRunner.build_suite()/run_tests().

Per deprecation timeline.

Note: See TracTickets for help on using tickets.
Back to Top