Opened 88 minutes ago
#37064 new Bug
SimpleTestCase._remove_databases_failures() crashes on un-wrapped connection methods
| Reported by: | Rio Weber | Owned by: | |
|---|---|---|---|
| Component: | Testing framework | Version: | 6.0 |
| Severity: | Normal | Keywords: | testcase, databases, multi-db, regression |
| Cc: | Rio Weber | Triage Stage: | Unreviewed |
| Has patch: | yes | Needs documentation: | no |
| Needs tests: | no | Patch needs improvement: | no |
| Easy pickings: | no | UI/UX: | no |
Description
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:
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:
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:
@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
- PR #20758 by Michele Fiori uses a hasattr guard; happy to rebase onto isinstance + tests or open a fresh PR crediting the original author
Attachments (1)
Change History (1)
by , 87 minutes ago
| Attachment: | remove-databases-failures-isinstance-guard.diff added |
|---|