#37064: SimpleTestCase._remove_databases_failures() crashes on un-wrapped
connection methods
-------------------------------------+-------------------------------------
Reporter: Rio Weber | Type: Bug
Status: new | Component: Testing
| framework
Version: 6.0 | Severity: Normal
Keywords: testcase, | Triage Stage:
databases, multi-db, regression | Unreviewed
Has patch: 1 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------
Re-filing #36942 - got closed for lack of a repro, I have one
TLDR: _remove_databases_failures blindly does method.wrapped on every
connection method it visits. If the method isn't a _DatabaseFailure at
that point (e.g. SQLite recycled the connection between setUpClass and
tearDownClass, or the class's databases set changed after setup),
teardown dies with AttributeError: 'function' object has no attribute
'wrapped'.
Repro, traceback, one-line fix (isinstance guard), and two regression
tests below. Verified on 6.0.3 and on main @ 526b548c (that commit
touches auth/backends.py, not test/testcases.py).
Note: between 6.0.3 and main, the list of disallowed connection methods
moved from a SimpleTestCase class attribute to
connection.features.disallowed_simple_test_case_connection_methods.
Orthogonal to the bug - both branches still blindly access
method.wrapped - but the patch surface differs slightly per branch.
**Minimal repro**
settings.py:
{{{
#!python
DATABASES = {
"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": BASE_DIR /
"db.sqlite3"},
"shard_b": {"ENGINE": "django.db.backends.sqlite3", "NAME": BASE_DIR /
"db_shard_b.sqlite3"},
}
}}}
tests/test_repro.py:
{{{
#!python
from django.test import TransactionTestCase
class ReproTests(TransactionTestCase):
# Implicit databases = {"default"} - shard_b is NOT declared
def test_anything(self):
assert True
}}}
Run under pytest-django; the test body doesn't need to touch the DB:
{{{
pytest tests/test_repro.py
}}}
**Traceback (abridged)**
{{{
.../django/test/testcases.py:280
in _remove_databases_failures
setattr(connection, name, method.wrapped)
^^^^^^^^^^^^^^
AttributeError: 'function' object has no attribute 'wrapped'
}}}
**Root cause**
_add_databases_failures installs _DatabaseFailure wrappers;
_remove_databases_failures assumes every method it iterates is still
one. That symmetry breaks most often because the SQLite backend
replaces connection objects between setUpClass and tearDownClass the
new connection has fresh, un-wrapped methods. Also reproducible if a
subclass/fixture mutates cls.databases after setup or if a class
skipped super().setUpClass() but still inherits the addClassCleanup
**Proposed fix**
Add an isinstance guard so teardown only touches what setup installed.
On main:
{{{
#!python
@classmethod
def _remove_databases_failures(cls):
for alias in connections:
if alias in cls.databases:
continue
connection = connections[alias]
disallowed_methods = (
connection.features.disallowed_simple_test_case_connection_methods
)
for name, _ in disallowed_methods:
method = getattr(connection, name)
if isinstance(method, _DatabaseFailure):
setattr(connection, name, method.wrapped)
}}}
Backport for stable/6.0.x is identical except the iteration source
stays cls._disallowed_connection_methods
isinstance is preferable to the hasattr("wrapped") approach in #20758
because it only unwraps Django's own wrapper class and won't chase
third-party objects that happen to expose .wrapped
**Regression tests**
Two tests both scoped to teardown:
* test_teardown_noops_when_method_is_not_wrapped - replaces a
connection method with a plain function and asserts
_remove_databases_failures does not raise.
* test_teardown_still_unwraps_database_failures - installs a
_DatabaseFailure manually and asserts it is restored
Full source attached as a patch
**Related**
* #36942 (closed needsinfo) - same bug, no reprodce
* [https://github.com/django/django/pull/20758 PR #20758] by Michele
Fiori uses a hasattr guard; happy to rebase onto isinstance + tests
or open a fresh PR crediting the original author
--
Ticket URL: <https://code.djangoproject.com/ticket/37064>
Django <https://code.djangoproject.com/>
The Web framework for perfectionists with deadlines.
--
You received this message because you are subscribed to the Google Groups
"Django updates" group.
To unsubscribe from this group and stop receiving emails from it, send an email
to [email protected].
To view this discussion visit
https://groups.google.com/d/msgid/django-updates/0107019dc0eea6b3-7ee20aef-7eeb-4ec2-8aa5-bbbe97b944e0-000000%40eu-central-1.amazonses.com.