#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.

Reply via email to